pgaskin / lithiumpatch

Adds additional functionality to the Lithium EPUB Reader Android app.
MIT License
44 stars 2 forks source link

Use EPUB3 series metadata #2

Closed alastortenebris closed 10 months ago

alastortenebris commented 10 months ago

Currently, these patches only check for series metadata under the calibre:series tag. This tag is only used with EPUB2 books. EPUB3 supports series using the belongs-to-collection tag listed here. Calibre will not use the calibre:series tag when embedding metadata if the ebook is an EPUB3 file.

pgaskin commented 10 months ago

This is planned, but will require quite a bit more code. For simplicity, I'll probably write an entirely new extra OPF parsing function since the existing one isn't really suited for parsing the refines property.

I'll probably make calibre:series take precedence, then the first EPUB3-style collection with a group-position similar to what I do with NickelSeries for Kobo eReaders.

pgaskin commented 10 months ago

Testing a simple Java implementation. Will rewrite in smali later.

import org.xmlpull.v1.XmlPullParser;
import org.xmlpull.v1.XmlPullParserException;
import org.xmlpull.v1.XmlPullParserFactory;

import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;

public class SeriesParser {
    public static void main(String[] args) {
        try {
            final SeriesParser p = new SeriesParser();
            System.out.println(p.parseSeries(Files.newInputStream(Paths.get("package.opf"))));
            System.out.println(p.mSeries + " #" + p.mSeriesIndex);
        } catch (Exception ex) {
            throw new RuntimeException(ex);
        }
    }

    public String mSeries;
    public String mSeriesIndex;

    public String parseSeries(InputStream is) throws XmlPullParserException, IOException {
        final XmlPullParser xpp = XmlPullParserFactory.newInstance().newPullParser(); // Landroid/util/Xml;->newPullParser()Lorg/xmlpull/v1/XmlPullParser;
        xpp.setFeature("http://xmlpull.org/v1/doc/features.html#process-namespaces", true);
        xpp.setInput(is, null);
        final LinkedHashSet<String> hSeriesSkip = new LinkedHashSet<>();
        final LinkedHashMap<String, String> hSeries = new LinkedHashMap<>();
        final LinkedHashMap<String, String> hSeriesIndex = new LinkedHashMap<>();
        hSeries.put(null, null); // calibre series metadata first
        final StringBuilder txt = new StringBuilder();
        for (int depth = 0, depthMatch = 0, evt = xpp.getEventType(); evt != XmlPullParser.END_DOCUMENT; ) {
            switch (evt) {
                case XmlPullParser.END_TAG:
                    if (depth-- < depthMatch) {
                        depthMatch--;
                    }
                    break;
                case XmlPullParser.START_TAG:
                    if (depth++ == depthMatch) {
                        if ("http://www.idpf.org/2007/opf".equals(xpp.getNamespace())) {
                            switch (depth) {
                                case 1:
                                    if ("package".equals(xpp.getName()))
                                        depthMatch++;
                                    break;
                                case 2:
                                    if ("metadata".equals(xpp.getName()))
                                        depthMatch++;
                                    break;
                                case 3:
                                    if ("meta".equals(xpp.getName()))
                                        depthMatch++;
                                    break;
                            }
                        }
                    }
                    // if we're at a package>metadata>meta
                    if (depthMatch == 3) {
                        // get the attributes we want
                        final String pName = xpp.getAttributeValue(null, "name");
                        final String pContent = xpp.getAttributeValue(null, "content");
                        final String pProperty = xpp.getAttributeValue(null, "property");
                        final String pId = xpp.getAttributeValue(null, "id");
                        final String pRefines = xpp.getAttributeValue(null, "refines");
                        // get the text within the element
                        txt.setLength(0);
                        for (evt = xpp.next(); !(depth == 3 && evt == XmlPullParser.END_TAG); evt = xpp.next()) {
                            switch (evt) {
                                case XmlPullParser.START_TAG:
                                    depth++;
                                    break;
                                case XmlPullParser.END_TAG:
                                    depth--;
                                    break;
                                case XmlPullParser.TEXT:
                                    final String tmp = xpp.getText();
                                    if (tmp != null) {
                                        txt.append(tmp);
                                    }
                                    break;
                            }
                        }
                        // get an identifier (null for calibre metadata) and key/value meta pair
                        String vSrc, vKey, vValue;
                        if (pName != null) {
                            vSrc = null;
                            vKey = pName;
                            vValue = pContent;
                        } else {
                            if (pRefines != null && pRefines.startsWith("#")) {
                                vSrc = pRefines.substring(1);
                            } else if (pId != null) {
                                vSrc = pId;
                            } else {
                                vSrc = "";
                            }
                            vKey = pProperty;
                            vValue = txt.toString().trim();
                            if (vValue.isEmpty()) {
                                vValue = null;
                            }
                        }
                        // if we have a key/value pair, process it
                        if (vKey != null && vValue != null)
                            if ("calibre:series".equals(vKey) || "belongs-to-collection".equals(vKey))
                                hSeries.put(vSrc, vValue);
                            else if ("calibre:series_index".equals(vKey) || "group-position".equals(vKey))
                                hSeriesIndex.put(vSrc, vValue);
                            else if ("collection-type".equals(vKey) && !"series".equals(vValue))
                                hSeriesSkip.add(vSrc);
                        continue; // we already consumed the next token (END_TAG) in the txt loop
                    }
                    break;
            }
            evt = xpp.next();
        }
        // get the first series
        for (final String src : hSeries.keySet()) {
            final String series = hSeries.get(src);
            if (series != null) {
                final String seriesIndex = hSeriesIndex.get(src);
                if (seriesIndex != null) {
                    if (!hSeriesSkip.contains(src)) {
                        mSeries = series;
                        mSeriesIndex = seriesIndex;
                        return src != null ? "#" + src : "calibre";
                    }
                }
            }
        }
        return null;
    }
}