Improve identification if bookmark's chapter. Add some AppAward
[dorian] / model / book.cpp
index dab120c..d209816 100644 (file)
@@ -1,11 +1,5 @@
-#include <QDir>
-#include <QString>
-#include <QDebug>
-#include <QtXml>
 #include <qtextdocument.h>  // Qt::escape is currently defined here...
-#include <QDirIterator>
-#include <QFileInfo>
-#include <QtAlgorithms>
+#include <QtGui>
 
 #include "book.h"
 #include "opshandler.h"
 #include "containerhandler.h"
 #include "ncxhandler.h"
 #include "trace.h"
+#include "bookdb.h"
 
 const int COVER_WIDTH = 53;
 const int COVER_HEIGHT = 59;
+const int COVER_MAX = 512 * 1024;
 
-Book::Book(const QString &p, QObject *parent): QObject(parent)
+Book::Book(const QString &p, QObject *parent): QObject(parent), loaded(false)
 {
     mPath = "";
-    if (p != "") {
+    if (p.size()) {
         QFileInfo info(p);
         mPath = info.absoluteFilePath();
         title = info.baseName();
-        cover = QImage(":/icons/book.png").scaled(COVER_WIDTH, COVER_HEIGHT,
-            Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation).
-            scaled(COVER_WIDTH, COVER_HEIGHT, Qt::KeepAspectRatio);
+        mTempFile.open();
     }
 }
 
-QString Book::path() const
+Book::~Book()
+{
+    close();
+}
+
+QString Book::path()
 {
     return mPath;
 }
 
 bool Book::open()
 {
-    Trace t("Book::open");
-    t.trace(path());
+    TRACE;
+    qDebug() << path();
     close();
     clear();
-    if (path() == "") {
+    load();
+    if (path().isEmpty()) {
         title = "No book";
         return false;
     }
-    if (!extract()) {
+    if (!extract(QStringList())) {
         return false;
     }
     if (!parse()) {
@@ -58,46 +58,72 @@ bool Book::open()
     return true;
 }
 
+void Book::peek()
+{
+    TRACE;
+    qDebug() << path();
+    close();
+    clear();
+    load();
+    if (path().isEmpty()) {
+        title = "No book";
+        return;
+    }
+    if (!extractMetaData()) {
+        return;
+    }
+    if (!parse()) {
+        return;
+    }
+    save();
+    close();
+}
+
 void Book::close()
 {
-    Trace t("Book::close");
+    TRACE;
     content.clear();
-    toc.clear();
+    parts.clear();
     QDir::setCurrent(QDir::rootPath());
     clearDir(tmpDir());
 }
 
 QString Book::tmpDir() const
 {
-    return QDir::tempPath() + "/dorian/book";
+    QString tmpName = QFileInfo(mTempFile.fileName()).fileName();
+    return QDir(QDir::temp().absoluteFilePath("dorian")).
+            absoluteFilePath(tmpName);
 }
 
-bool Book::extract()
+bool Book::extract(const QStringList &excludedExtensions)
 {
-    Trace t("Book::extract");
+    TRACE;
     bool ret = false;
     QString tmp = tmpDir();
-    t.trace("Extracting " + mPath + " to " + tmp);
+    qDebug() << "Extracting" << mPath << "to" << tmp;
 
+    load();
     QDir::setCurrent(QDir::rootPath());
     if (!clearDir(tmp)) {
         qCritical() << "Book::extract: Failed to remove" << tmp;
         return false;
     }
-    QDir d;
-    if (!d.mkpath(tmp)) {
-        qCritical() << "Book::extract: Could not create" << tmp;
-        return false;
+    QDir d(tmp);
+    if (!d.exists()) {
+        if (!d.mkpath(tmp)) {
+            qCritical() << "Book::extract: Could not create" << tmp;
+            return false;
+        }
     }
 
     // If book comes from resource, copy it to the temporary directory first
     QString bookPath = path();
     if (bookPath.startsWith(":/books/")) {
         QFile src(bookPath);
-        QString dst(tmp + "/book.epub");
+        QString dst(QDir(tmp).absoluteFilePath("book.epub"));
         if (!src.copy(dst)) {
-            qCritical() << "Book::extract: Failed to copy built-in book to"
-                    << dst;
+            qCritical() << "Book::extract: Failed to copy built-in book"
+                    << bookPath << "to" << dst;
             return false;
         }
         bookPath = dst;
@@ -108,7 +134,7 @@ bool Book::extract()
         qCritical() << "Book::extract: Could not change to" << tmp;
         return false;
     }
-    ret = extractZip(bookPath);
+    ret = extractZip(bookPath, excludedExtensions);
     if (!ret) {
         qCritical() << "Book::extract: Extracting ZIP failed";
     }
@@ -118,12 +144,14 @@ bool Book::extract()
 
 bool Book::parse()
 {
-    Trace t("Book::parse");
+    TRACE;
+
+    load();
 
     // Parse OPS file
     bool ret = false;
     QString opsFileName = opsPath();
-    t.trace("Parsing OPS file" + opsFileName);
+    qDebug() << "Parsing OPS file" << opsFileName;
     QFile opsFile(opsFileName);
     QXmlSimpleReader reader;
     QXmlInputSource *source = new QXmlInputSource(&opsFile);
@@ -137,39 +165,65 @@ bool Book::parse()
     delete source;
 
     // Initially, put all content items in the chapter list.
-    // This can be refined by parsing the NCX file later
-    chapters = toc;
+    // This will be refined by parsing the NCX file later
+    chapters = parts;
 
     // Load cover image
+    QString coverPath;
     QStringList coverKeys;
     coverKeys << "cover-image" << "img-cover-jpeg" << "cover";
     foreach (QString key, coverKeys) {
         if (content.contains(key)) {
-            t.trace("Loading cover image from " + content[key].href);
-            cover = QImage(content[key].href).scaled(COVER_WIDTH, COVER_HEIGHT,
-                Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation).
-                scaled(COVER_WIDTH, COVER_HEIGHT, Qt::KeepAspectRatio);
+            coverPath = QDir(rootPath()).absoluteFilePath(content[key].href);
             break;
         }
     }
+    if (coverPath.isEmpty()) {
+        // Last resort
+        QString coverJpeg = QDir(rootPath()).absoluteFilePath("cover.jpg");
+        if (QFileInfo(coverJpeg).exists()) {
+            coverPath = coverJpeg;
+        }
+    }
+    if (!coverPath.isEmpty()) {
+        qDebug() << "Loading cover image from" << coverPath;
+        cover = makeCover(coverPath);
+    }
 
     // If there is an "ncx" item in content, parse it: That's the real table of
     // contents
+    QString ncxFileName;
     if (content.contains("ncx")) {
-        QString ncxFileName = content["ncx"].href;
-        t.trace("Parsing NCX file " + ncxFileName);
-        QFile ncxFile(ncxFileName);
+        ncxFileName = content["ncx"].href;
+    } else if (content.contains("ncxtoc")) {
+        ncxFileName = content["ncxtoc"].href;
+    } else if (content.contains("toc")) {
+        ncxFileName = content["toc"].href;
+    } else {
+        qDebug() << "No NCX table of contents";
+    }
+    if (!ncxFileName.isEmpty()) {
+        qDebug() << "Parsing NCX file" << ncxFileName;
+        QFile ncxFile(QDir(rootPath()).absoluteFilePath(ncxFileName));
         source = new QXmlInputSource(&ncxFile);
         NcxHandler *ncxHandler = new NcxHandler(*this);
         errorHandler = new XmlErrorHandler();
         reader.setContentHandler(ncxHandler);
         reader.setErrorHandler(errorHandler);
         ret = reader.parse(source);
-        delete ncxHandler;
         delete errorHandler;
+        delete ncxHandler;
         delete source;
     }
 
+    // Calculate book part sizes
+    size = 0;
+    foreach (QString part, parts) {
+        QFileInfo info(QDir(rootPath()).absoluteFilePath(content[part].href));
+        content[part].size = info.size();
+        size += content[part].size;
+    }
+
     return ret;
 }
 
@@ -218,115 +272,109 @@ void Book::clear()
 
 void Book::load()
 {
-    Trace t("Book::load");
-    t.trace("path: " + path());
-    QSettings settings;
-    QString key = "book/" + path() + "/";
-    t.trace("key: " + key);
+    if (loaded) {
+        return;
+    }
 
-    // Load book info
-    title = settings.value(key + "title").toString();
-    t.trace(title);
-    creators = settings.value(key + "creators").toStringList();
-    date = settings.value(key + "date").toString();
-    publisher = settings.value(key + "publisher").toString();
-    datePublished = settings.value(key + "datepublished").toString();
-    subject = settings.value(key + "subject").toString();
-    source = settings.value(key + "source").toString();
-    rights = settings.value(key + "rights").toString();
-    mLastBookmark.chapter = settings.value(key + "lastchapter").toInt();
-    mLastBookmark.pos = settings.value(key + "lastpos").toReal();
-    cover = settings.value(key + "cover").value<QImage>().scaled(COVER_WIDTH,
-        COVER_HEIGHT, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
+    TRACE;
+    loaded = true;
+    qDebug() << "path" << path();
+
+    QVariantHash data = BookDb::instance()->load(path());
+    title = data["title"].toString();
+    qDebug() << title;
+    creators = data["creators"].toStringList();
+    date = data["date"].toString();
+    publisher = data["publisher"].toString();
+    datePublished = data["datepublished"].toString();
+    subject = data["subject"].toString();
+    source = data["source"].toString();
+    rights = data["rights"].toString();
+    mLastBookmark.part = data["lastpart"].toInt();
+    mLastBookmark.pos = data["lastpos"].toReal();
+    cover = data["cover"].value<QImage>();
     if (cover.isNull()) {
-        cover = QImage(":/icons/book.png").scaled(COVER_WIDTH, COVER_HEIGHT,
-            Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation).
-            scaled(COVER_WIDTH, COVER_HEIGHT, Qt::KeepAspectRatio);
+        cover = makeCover(":/icons/book.png");
     }
-
-    // Load bookmarks
-    int size = settings.value(key + "bookmarks").toInt();
+    int size = data["bookmarks"].toInt();
     for (int i = 0; i < size; i++) {
-        int chapter = settings.value(key + "bookmark" + QString::number(i) +
-                                     "/chapter").toInt();
-        qreal pos = settings.value(key + "bookmark" + QString::number(i) +
-                                   "/pos").toReal();
-        t.trace(QString("Bookmark %1 at chapter %2, %3").
-                arg(i).arg(chapter).arg(pos));
-        mBookmarks.append(Bookmark(chapter, pos));
+        int part = data[QString("bookmark%1part").arg(i)].toInt();
+        qreal pos = data[QString("bookmark%1pos").arg(i)].toReal();
+        QString note = data[QString("bookmark%1note").arg(i)].toString();
+        mBookmarks.append(Bookmark(part, pos, note));
     }
 }
 
 void Book::save()
 {
-    Trace t("Book::save");
-    QSettings settings;
-    QString key = "book/" + path() + "/";
-    t.trace("key: " + key);
-
-    // Save book info
-    settings.setValue(key + "title", title);
-    t.trace("title: " + title);
-    settings.setValue(key + "creators", creators);
-    settings.setValue(key + "date", date);
-    settings.setValue(key + "publisher", publisher);
-    settings.setValue(key + "datepublished", datePublished);
-    settings.setValue(key + "subject", subject);
-    settings.setValue(key + "source", source);
-    settings.setValue(key + "rights", rights);
-    settings.setValue(key + "lastchapter", mLastBookmark.chapter);
-    settings.setValue(key + "lastpos", mLastBookmark.pos);
-    settings.setValue(key + "cover", cover);
-
-    // Save bookmarks
-    settings.setValue(key + "bookmarks", mBookmarks.size());
+    TRACE;
+
+    load();
+    QVariantHash data;
+    data["title"] = title;
+    data["creators"] = creators;
+    data["date"] = date;
+    data["publisher"] = publisher;
+    data["datepublished"] = datePublished;
+    data["subject"] = subject;
+    data["source"] = source;
+    data["rights"] = rights;
+    data["lastpart"] = mLastBookmark.part;
+    data["lastpos"] = mLastBookmark.pos;
+    data["cover"] = cover;
+    data["bookmarks"] = mBookmarks.size();
     for (int i = 0; i < mBookmarks.size(); i++) {
-        t.trace(QString("Bookmark %1 at %2, %3").
-                arg(i).arg(mBookmarks[i].chapter).arg(mBookmarks[i].pos));
-        settings.setValue(key + "bookmark" + QString::number(i) + "/chapter",
-                          mBookmarks[i].chapter);
-        settings.setValue(key + "bookmark" + QString::number(i) + "/pos",
-                          mBookmarks[i].pos);
+        data[QString("bookmark%1part").arg(i)] = mBookmarks[i].part;
+        data[QString("bookmark%1pos").arg(i)] = mBookmarks[i].pos;
+        data[QString("bookmark%1note").arg(i)] = mBookmarks[i].note;
     }
+    BookDb::instance()->save(path(), data);
 }
 
-void Book::setLastBookmark(int chapter, qreal position)
+void Book::setLastBookmark(int part, qreal position)
 {
-    mLastBookmark.chapter = chapter;
+    TRACE;
+    load();
+    mLastBookmark.part = part;
     mLastBookmark.pos = position;
     save();
 }
 
-Book::Bookmark Book::lastBookmark() const
+Book::Bookmark Book::lastBookmark()
 {
+    load();
     return Book::Bookmark(mLastBookmark);
 }
 
-void Book::addBookmark(int chapter, qreal position)
+void Book::addBookmark(int part, qreal position, const QString &note)
 {
-    mBookmarks.append(Bookmark(chapter, position));
+    load();
+    mBookmarks.append(Bookmark(part, position, note));
     qSort(mBookmarks.begin(), mBookmarks.end());
     save();
 }
 
 void Book::deleteBookmark(int index)
 {
+    load();
     mBookmarks.removeAt(index);
     save();
 }
 
-QList<Book::Bookmark> Book::bookmarks() const
+QList<Book::Bookmark> Book::bookmarks()
 {
+    load();
     return mBookmarks;
 }
 
 QString Book::opsPath()
 {
-    Trace t("Book::opsPath");
+    TRACE;
+    load();
     QString ret;
 
     QFile container(tmpDir() + "/META-INF/container.xml");
-    t.trace(container.fileName());
+    qDebug() << container.fileName();
     QXmlSimpleReader reader;
     QXmlInputSource *source = new QXmlInputSource(&container);
     ContainerHandler *containerHandler = new ContainerHandler();
@@ -336,8 +384,7 @@ QString Book::opsPath()
     if (reader.parse(source)) {
         ret = tmpDir() + "/" + containerHandler->rootFile;
         mRootPath = QFileInfo(ret).absoluteDir().absolutePath();
-        t.trace("OSP path: " + ret);
-        t.trace("Root dir: " + mRootPath);
+        qDebug() << "OSP path" << ret << "\nRoot dir" << mRootPath;
     }
     delete errorHandler;
     delete containerHandler;
@@ -345,32 +392,193 @@ QString Book::opsPath()
     return ret;
 }
 
-QString Book::rootPath() const
+QString Book::rootPath()
 {
+    load();
     return mRootPath;
 }
 
-QString Book::name() const
+QString Book::name()
 {
-    if (title != "") {
+    load();
+    if (title.size()) {
         QString ret = title;
         if (creators.length()) {
-            ret += "\nBy " + creators[0];
-            for (int i = 1; i < creators.length(); i++) {
-                ret += ", " + creators[i];
-            }
+            ret += "\nBy " + creators.join(", ");
         }
         return ret;
-    } else {
-        return path();
     }
+    return path();
+}
+
+QString Book::shortName()
+{
+    load();
+    return (title.isEmpty())? QFileInfo(path()).baseName(): title;
+}
+
+QImage Book::coverImage()
+{
+    load();
+    return cover;
+}
+
+int Book::chapterFromPart(int index)
+{
+    TRACE;
+    load();
+    int ret = -1;
+
+    QString partId = parts[index];
+    QString partHref = content[partId].href;
+
+    for (int i = 0; i < chapters.size(); i++) {
+        QString id = chapters[i];
+        QString href = content[id].href;
+        int hashPos = href.indexOf("#");
+        if (hashPos != -1) {
+            href = href.left(hashPos);
+        }
+        if (href == partHref) {
+            ret = i;
+            // Don't break, keep looking
+        }
+    }
+
+    qDebug() << "Part" << index << partId << partHref << ":" << ret;
+    return ret;
 }
 
-QString Book::shortName() const
+int Book::partFromChapter(int index, QString &fragment)
 {
-    if (title == "") {
-        return QFileInfo(path()).baseName();
+    TRACE;
+    load();
+    fragment.clear();
+    QString id = chapters[index];
+    QString href = content[id].href;
+    int hashPos = href.indexOf("#");
+    if (hashPos != -1) {
+        fragment = href.mid(hashPos);
+        href = href.left(hashPos);
+    }
+
+    qDebug() << "Chapter" << index;
+    qDebug() << " id" << id;
+    qDebug() << " href" << href;
+    qDebug() << " fragment" << fragment;
+
+    for (int i = 0; i < parts.size(); i++) {
+        QString partId = parts[i];
+        if (content[partId].href == href) {
+            qDebug() << "Part index for" << href << "is" << i;
+            return i;
+        }
+    }
+
+    qWarning() << "Book::partFromChapter: Could not find part index for"
+            << href;
+    return -1;
+}
+
+qreal Book::getProgress(int part, qreal position)
+{
+    load();
+    Q_ASSERT(part < parts.size());
+    QString key;
+    qreal partSize = 0;
+    for (int i = 0; i < part; i++) {
+        key = parts[i];
+        partSize += content[key].size;
+    }
+    key = parts[part];
+    partSize += content[key].size * position;
+    return partSize / (qreal)size;
+}
+
+bool Book::extractMetaData()
+{
+    QStringList excludedExtensions;
+    excludedExtensions << ".html" << ".xhtml" << ".xht" << ".htm" << ".gif"
+            << ".css" << "*.ttf" << "mimetype";
+    return extract(excludedExtensions);
+}
+
+void Book::upgrade()
+{
+    TRACE;
+
+    // Load book from old database (QSettings)
+    QSettings settings;
+    QString key = "book/" + path() + "/";
+    title = settings.value(key + "title").toString();
+    qDebug() << title;
+    creators = settings.value(key + "creators").toStringList();
+    date = settings.value(key + "date").toString();
+    publisher = settings.value(key + "publisher").toString();
+    datePublished = settings.value(key + "datepublished").toString();
+    subject = settings.value(key + "subject").toString();
+    source = settings.value(key + "source").toString();
+    rights = settings.value(key + "rights").toString();
+    mLastBookmark.part = settings.value(key + "lastpart").toInt();
+    mLastBookmark.pos = settings.value(key + "lastpos").toReal();
+    cover = settings.value(key + "cover").value<QImage>();
+    if (cover.isNull()) {
+        cover = makeCover(":/icons/book.png");
     } else {
-        return title;
+        cover = makeCover(QPixmap::fromImage(cover));
+    }
+    int size = settings.value(key + "bookmarks").toInt();
+    for (int i = 0; i < size; i++) {
+        int part = settings.value(key + "bookmark" + QString::number(i) +
+                                     "/part").toInt();
+        qreal pos = settings.value(key + "bookmark" + QString::number(i) +
+                                   "/pos").toReal();
+        qDebug() << QString("Bookmark %1 at part %2, %3").
+                arg(i).arg(part).arg(pos);
+        mBookmarks.append(Bookmark(part, pos));
+    }
+
+    // Remove QSettings
+    settings.remove("book/" + path());
+
+    // Save book to new database
+    save();
+}
+
+void Book::remove()
+{
+    TRACE;
+    close();
+    BookDb::instance()->remove(path());
+}
+
+QImage Book::makeCover(const QString &fileName)
+{
+    TRACE;
+    qDebug() << fileName;
+    QFileInfo info(fileName);
+    if (info.isReadable() && (info.size() < COVER_MAX)) {
+        return makeCover(QPixmap(fileName));
     }
+    return makeCover(QPixmap(":/icons/book.png"));
 }
+
+QImage Book::makeCover(const QPixmap &pixmap)
+{
+    TRACE;
+    QPixmap src = pixmap.scaled(COVER_WIDTH, COVER_HEIGHT,
+        Qt::KeepAspectRatio, Qt::SmoothTransformation);
+    QPixmap transparent(COVER_WIDTH, COVER_HEIGHT);
+    transparent.fill(Qt::transparent);
+
+    QPainter p;
+    p.begin(&transparent);
+    p.setCompositionMode(QPainter::CompositionMode_Source);
+    p.drawPixmap((COVER_WIDTH - src.width()) / 2,
+                 (COVER_HEIGHT - src.height()) / 2, src);
+    p.end();
+
+    return transparent.toImage();
+}
+
+