package com.gmail.dpierron.calibre.opf; import com.gmail.dpierron.calibre.cache.CachedFile; import com.gmail.dpierron.calibre.cache.CachedFileManager; import com.gmail.dpierron.calibre.datamodel.*; import com.gmail.dpierron.calibre.opds.JDOMManager; import com.gmail.dpierron.calibre.opds.JDOMManager.Namespace; import com.gmail.dpierron.tools.Helper; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.jdom2.*; import java.io.*; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.*; import java.util.zip.ZipEntry; import java.util.zip.ZipException; import java.util.zip.ZipFile; import java.util.zip.ZipOutputStream; public class OpfOutput { private final static Logger logger = LogManager.getLogger(OpfOutput.class); private static final DateFormat CALIBRE_DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd"); private static final DateFormat CALIBRE_TIME_FORMAT = new SimpleDateFormat("HH:mm:ss"); private Book book; private boolean removeCss = false; private boolean restoreCss = false; private File defaultCss = null; public OpfOutput(Book book, boolean removeCss, boolean restoreCss, File defaultCss) { this.book = book; if (defaultCss != null && defaultCss.exists()) this.defaultCss = defaultCss; this.removeCss = (this.defaultCss != null) || removeCss; // copyDefaultCss forces removeCss this.restoreCss = restoreCss; } public OpfOutput(Book book) { this(book, false, false, null); } public boolean isRemoveCss() { return removeCss; } public boolean isRestoreCss() { return restoreCss; } public File getDefaultCss() { return defaultCss; } private String convertRatingToCalibreRating(BookRating rating) { if (rating == null) return "0.00"; switch (rating) { case NOTRATED: return "0.00"; case ONE: return "1.00"; case TWO: return "2.00"; case THREE: return "3.00"; case FOUR: return "4.00"; case FIVE: return "5.00"; default: return "0.00"; } } private void removeMetaElement(Element source, String name) { Content childToRemove = null; for (Object childO : source.getChildren()) { if (childO instanceof Element) { Element child = (Element) childO; if (child.getName().equalsIgnoreCase("meta")) { if (child.getAttributeValue("name").equalsIgnoreCase(name)) { childToRemove = child; break; } } } } if (childToRemove != null) source.removeContent(childToRemove); } private void removeDcElements(Element source, String name) { source.removeChildren(name, Namespace.Dc.getJdomNamespace()); } private void addMetaElement(Element source, String name, String content) { Element meta = JDOMManager.element("meta", Namespace.Opf); meta.setAttribute("name", name); meta.setAttribute("content", content); source.addContent(meta); } private void addDublinCoreElement(Element source, String name, String content) { source.addContent(getDublinCoreElement(source, name, content)); } private Element getDublinCoreElement(Element source, String name, String content) { Element dc = JDOMManager.element(name, Namespace.Dc); dc.setText(content); return dc; } private String convertDate(Date date) { if (date == null) return ""; String sDate = CALIBRE_DATE_FORMAT.format(date); String sTime = CALIBRE_TIME_FORMAT.format(date); return sDate + "T" + sTime; } private void processMetadataElement(Element source) { /* Calibre stuff */ removeMetaElement(source, "calibre:rating"); addMetaElement(source, "calibre:rating", convertRatingToCalibreRating(book.getRating())); removeMetaElement(source, "calibre:series"); removeMetaElement(source, "calibre:series_index"); if (book.getSeries() != null) { addMetaElement(source, "calibre:series", book.getSeries().getName()); addMetaElement(source, "calibre:series_index", "" + book.getSerieIndex()); } removeMetaElement(source, "calibre:timestamp"); if (book.getTimestamp() != null) { addMetaElement(source, "calibre:timestamp", convertDate(book.getTimestamp())); } /* Dublin Core stuff */ if (Helper.isNotNullOrEmpty(book.getUuid())) { removeDcElements(source, "identifier"); Element dc = getDublinCoreElement(source, "identifier", book.getUuid()); dc.setAttribute("id", "calibre-uuid"); source.addContent(dc); } List<Language> bookLanguages = book.getBookLanguages(); if (Helper.isNotNullOrEmpty(bookLanguages)) { removeDcElements(source, "language"); for (Language language : bookLanguages) { addDublinCoreElement(source, "language", language.getIso3()); } } if (Helper.isNotNullOrEmpty(book.getAuthors())) { removeDcElements(source, "creator"); for (Author author : book.getAuthors()) { Element dc = getDublinCoreElement(source, "creator", author.getName()); Attribute att = new Attribute("file-as", author.getSort(), Namespace.Opf.getJdomNamespace()); dc.setAttribute(att); att = new Attribute("role", "aut", Namespace.Opf.getJdomNamespace()); dc.setAttribute(att); source.addContent(dc); } } if (Helper.isNotNullOrEmpty(book.getTitle())) { removeDcElements(source, "title"); addDublinCoreElement(source, "title", book.getTitle()); } if (book.getTimestamp() != null) { removeDcElements(source, "date"); addDublinCoreElement(source, "date", convertDate(book.getTimestamp())); } boolean subjectsRemoved = false; if (Helper.isNotNullOrEmpty(book.getTags())) { if (!subjectsRemoved) { removeDcElements(source, "subject"); subjectsRemoved = true; } for (Tag tag : book.getTags()) { addDublinCoreElement(source, "subject", tag.getName()); } } if (book.getSeries() != null) { if (!subjectsRemoved) { removeDcElements(source, "subject"); subjectsRemoved = true; } addDublinCoreElement(source, "subject", book.getSeries().getName()); } } public void processEPubFile() throws IOException { if (book == null || book.getEpubFile() == null) return; File outputFile = File.createTempFile("calibre-epub-opfoutput", ".epub"); try { processEPubFile(outputFile); CachedFile epubfile = book.getEpubFile().getFile(); Helper.copy(outputFile, epubfile); // Clear any cached information for this file as we have created a new one if (CachedFileManager.inCache(epubfile) != null) { epubfile.clearCachedInformation(); } } catch (ZipException e ) { logger.warn("Failed to process EPUB metadata for book " + book); } finally { outputFile.delete(); } } public void processEPubFile(File outputFile) throws IOException { if (book.getEpubFile() == null) return; File inputFile = book.getEpubFile().getFile(); ZipFile zipInputFile = null; ZipOutputStream zos = null; try { try { // Map<String, ZipEntry> cssFilesBackupMap = new HashMap<String, ZipEntry>(); zipInputFile = new ZipFile(inputFile); outputFile.getParentFile().mkdirs(); zos = new ZipOutputStream(new BufferedOutputStream(new FileOutputStream(outputFile), 512 *1024)); Enumeration entries = zipInputFile.entries(); while (entries.hasMoreElements()) { Object o = entries.nextElement(); if (o instanceof ZipEntry) { ZipEntry zipEntry = (ZipEntry) o; InputStream inputStream = zipInputFile.getInputStream(zipEntry); if (zipEntry.getName().toUpperCase().endsWith("CONTENT.OPF")) { // process the XML in the file try { Document doc = JDOMManager.getSaxBuilder().build(inputStream); try { doc.getRootElement().addNamespaceDeclaration(Namespace.Opf.getJdomNamespace()); } catch (org.jdom2.IllegalAddException e) { logger.warn("processEbubFile: Unable to add namespace declaration '" + Namespace.Opf + "' for book: " + book.getTitle() + " (file " + inputFile + ")"); } try { doc.getRootElement().addNamespaceDeclaration(Namespace.Dc.getJdomNamespace()); } catch (org.jdom2.IllegalAddException e) { logger.warn("processEbubFile: Unable to add namespace declaration '" + Namespace.Dc + "' for book: " + book.getTitle() + " (file " + inputFile + ")"); } try { doc.getRootElement().addNamespaceDeclaration(Namespace.DcTerms.getJdomNamespace()); } catch (org.jdom2.IllegalAddException e) { logger.warn("processEbubFile: Unable to add namespace declaration '" + Namespace.DcTerms + "' for book: " + book.getTitle() + " (file " + inputFile + ")"); } try { doc.getRootElement().addNamespaceDeclaration(Namespace.Calibre.getJdomNamespace()); } catch (org.jdom2.IllegalAddException e) { logger.warn("processEbubFile: Unable to add namespace declaration '" + Namespace.Calibre + "' for book: " + book.getTitle() + " (file " + inputFile + ")"); } Element metadata = doc.getRootElement().getChild("metadata", Namespace.Opf.getJdomNamespace()); if (metadata != null) processMetadataElement(metadata); try { ZipEntry newEntry = new ZipEntry(zipEntry.getName()); zos.putNextEntry(newEntry); JDOMManager.getPrettyXML().output(doc, zos); } finally { zos.closeEntry(); } } catch (IOException io) { logger.error(io); logger.error("... for book: " + book.getTitle() + " (file " + inputFile + ")"); } } else { // copy the entry to the output file BufferedInputStream in = null; try { String filename = zipEntry.getName(); if (isRestoreCss()) { // check if we must restore the CSS files (rename them from .css_bak to .css) if (filename.toUpperCase().endsWith(".CSS_BAK")) { filename = filename.substring(0, filename.length() - 4); } else if (filename.toUpperCase().endsWith(".CSS")) { if (zipInputFile.getEntry(filename+"_BAK") != null) filename = null; // skip it } } else if (isRemoveCss()) { // check if we must remove the CSS files (rename them to .css_bak) if (filename.toUpperCase().endsWith(".CSS_BAK")) { filename = null; // skip it } else if (filename.toUpperCase().endsWith(".CSS")) { // copy the default stylesheet if needed if (getDefaultCss() != null) { try { BufferedInputStream in2 = null; try { ZipEntry newEntry = new ZipEntry(filename); newEntry.setMethod(ZipEntry.DEFLATED); zos.putNextEntry(newEntry); byte[] data = new byte[1024]; in2 = new BufferedInputStream(new FileInputStream(getDefaultCss()), 512 * 1024); int count; while ((count = in2.read(data, 0, data.length)) != -1) { zos.write(data, 0, count); } } finally { zos.closeEntry(); if (in2 != null) in2.close(); } } catch (IOException e) { logger.error(e); logger.error("... for book: " + book.getTitle() + " (cannot copy the default stylesheet)"); } } filename += "_BAK"; // don't duplicate entries if (zipInputFile.getEntry(filename) != null) filename = null; } } if (filename != null) { try { ZipEntry newEntry = new ZipEntry(filename); newEntry.setMethod(zipEntry.getMethod()); if (newEntry.getMethod() == ZipEntry.STORED) { newEntry.setSize(zipEntry.getSize()); newEntry.setCrc(zipEntry.getCrc()); } zos.putNextEntry(newEntry); byte[] data = new byte[1024]; in = new BufferedInputStream(inputStream, 512 * 1024); int count; while ((count = in.read(data, 0, data.length)) != -1) { zos.write(data, 0, count); } } finally { zos.closeEntry(); if (in != null) in.close(); } } } catch (IOException e) { logger.error(e); logger.error("... for book: " + book.getTitle() + " (file " + inputFile + ")"); } } } } } finally { if (zos != null) { zos.close(); } if (zipInputFile != null) { zipInputFile.close(); } } } catch (JDOMException je) { logger.warn("ProcessePubFile: Unexpected JDOMException for book: " + book.getTitle() + " (file " + inputFile + ")"); logger.warn(je); throw new IOException(je); } catch (ZipException z) { logger.warn("ProcessePubFile: EPUB file is not valid ZIP for book: " + book.getTitle() + " (file " + inputFile + ")"); throw new IOException(z); } catch (Exception e) { logger.warn("ProcessePubFile: Unexpected Exception for book: " + book.getTitle() + " (file " + inputFile + ")"); logger.warn(e); throw new IOException(e); } } }