Store library in database.
[dorian] / model / book.cpp
1 #include <QDir>
2 #include <QString>
3 #include <QDebug>
4 #include <QtXml>
5 #include <qtextdocument.h>  // Qt::escape is currently defined here...
6 #include <QDirIterator>
7 #include <QFileInfo>
8 #include <QtAlgorithms>
9 #include <QVariantHash>
10
11 #include "book.h"
12 #include "opshandler.h"
13 #include "xmlerrorhandler.h"
14 #include "extractzip.h"
15 #include "library.h"
16 #include "containerhandler.h"
17 #include "ncxhandler.h"
18 #include "trace.h"
19 #include "bookdb.h"
20
21 const int COVER_WIDTH = 53;
22 const int COVER_HEIGHT = 59;
23
24 static QImage makeCover(const QString &path)
25 {
26     return QImage(path).scaled(COVER_WIDTH, COVER_HEIGHT,
27         Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation).
28         scaled(COVER_WIDTH, COVER_HEIGHT, Qt::KeepAspectRatio);
29 }
30
31 Book::Book(const QString &p, QObject *parent): QObject(parent)
32 {
33     mPath = "";
34     if (p.size()) {
35         QFileInfo info(p);
36         mPath = info.absoluteFilePath();
37         title = info.baseName();
38         cover = makeCover(":/icons/book.png");
39         mTempFile.open();
40     }
41 }
42
43 QString Book::path() const
44 {
45     return mPath;
46 }
47
48 bool Book::open()
49 {
50     Trace t("Book::open");
51     qDebug() << path();
52     close();
53     clear();
54     if (path().isEmpty()) {
55         title = "No book";
56         return false;
57     }
58     if (!extract(QStringList())) {
59         return false;
60     }
61     if (!parse()) {
62         return false;
63     }
64     save();
65     emit opened(path());
66     return true;
67 }
68
69 void Book::peek()
70 {
71     Trace t("Book::peek");
72     qDebug() << path();
73     close();
74     clear();
75     if (path().isEmpty()) {
76         title = "No book";
77         return;
78     }
79     if (!extractMetaData()) {
80         return;
81     }
82     if (!parse()) {
83         return;
84     }
85     save();
86     close();
87 }
88
89 void Book::close()
90 {
91     Trace t("Book::close");
92     content.clear();
93     parts.clear();
94     QDir::setCurrent(QDir::rootPath());
95     clearDir(tmpDir());
96 }
97
98 QString Book::tmpDir() const
99 {
100     QString tmpName = QFileInfo(mTempFile.fileName()).fileName();
101     return QDir(QDir::temp().absoluteFilePath("dorian")).
102             absoluteFilePath(tmpName);
103 }
104
105 bool Book::extract(const QStringList &excludedExtensions)
106 {
107     Trace t("Book::extract");
108     bool ret = false;
109     QString tmp = tmpDir();
110     qDebug() << "Extracting" << mPath << "to" << tmp;
111
112     QDir::setCurrent(QDir::rootPath());
113     if (!clearDir(tmp)) {
114         qCritical() << "Book::extract: Failed to remove" << tmp;
115         return false;
116     }
117     QDir d(tmp);
118     if (!d.exists()) {
119         if (!d.mkpath(tmp)) {
120             qCritical() << "Book::extract: Could not create" << tmp;
121             return false;
122         }
123     }
124
125     // If book comes from resource, copy it to the temporary directory first
126     QString bookPath = path();
127     if (bookPath.startsWith(":/books/")) {
128         QFile src(bookPath);
129         QString dst(QDir(tmp).absoluteFilePath("book.epub"));
130         if (!src.copy(dst)) {
131             qCritical() << "Book::extract: Failed to copy built-in book"
132                     << bookPath << "to" << dst;
133             return false;
134         }
135         bookPath = dst;
136     }
137
138     QString oldDir = QDir::currentPath();
139     if (!QDir::setCurrent(tmp)) {
140         qCritical() << "Book::extract: Could not change to" << tmp;
141         return false;
142     }
143     ret = extractZip(bookPath, excludedExtensions);
144     if (!ret) {
145         qCritical() << "Book::extract: Extracting ZIP failed";
146     }
147     QDir::setCurrent(oldDir);
148     return ret;
149 }
150
151 bool Book::parse()
152 {
153     Trace t("Book::parse");
154
155     // Parse OPS file
156     bool ret = false;
157     QString opsFileName = opsPath();
158     qDebug() << "Parsing OPS file" << opsFileName;
159     QFile opsFile(opsFileName);
160     QXmlSimpleReader reader;
161     QXmlInputSource *source = new QXmlInputSource(&opsFile);
162     OpsHandler *opsHandler = new OpsHandler(*this);
163     XmlErrorHandler *errorHandler = new XmlErrorHandler();
164     reader.setContentHandler(opsHandler);
165     reader.setErrorHandler(errorHandler);
166     ret = reader.parse(source);
167     delete errorHandler;
168     delete opsHandler;
169     delete source;
170
171     // Initially, put all content items in the chapter list.
172     // This will be refined by parsing the NCX file later
173     chapters = parts;
174
175     // Load cover image
176     QString coverPath;
177     QStringList coverKeys;
178     coverKeys << "cover-image" << "img-cover-jpeg" << "cover";
179     foreach (QString key, coverKeys) {
180         if (content.contains(key)) {
181             coverPath = QDir(rootPath()).absoluteFilePath(content[key].href);
182             break;
183         }
184     }
185     if (coverPath.isEmpty()) {
186         // Last resort
187         QString coverJpeg = QDir(rootPath()).absoluteFilePath("cover.jpg");
188         if (QFileInfo(coverJpeg).exists()) {
189             coverPath = coverJpeg;
190         }
191     }
192     if (!coverPath.isEmpty()) {
193         qDebug() << "Loading cover image from" << coverPath;
194         cover = makeCover(coverPath);
195     }
196
197     // If there is an "ncx" item in content, parse it: That's the real table of
198     // contents
199     QString ncxFileName;
200     if (content.contains("ncx")) {
201         ncxFileName = content["ncx"].href;
202     } else if (content.contains("ncxtoc")) {
203         ncxFileName = content["ncxtoc"].href;
204     } else if (content.contains("toc")) {
205         ncxFileName = content["toc"].href;
206     } else {
207         qDebug() << "No NCX table of contents";
208     }
209     if (!ncxFileName.isEmpty()) {
210         qDebug() << "Parsing NCX file" << ncxFileName;
211         QFile ncxFile(QDir(rootPath()).absoluteFilePath(ncxFileName));
212         source = new QXmlInputSource(&ncxFile);
213         NcxHandler *ncxHandler = new NcxHandler(*this);
214         errorHandler = new XmlErrorHandler();
215         reader.setContentHandler(ncxHandler);
216         reader.setErrorHandler(errorHandler);
217         ret = reader.parse(source);
218         delete errorHandler;
219         delete ncxHandler;
220         delete source;
221     }
222
223     // Calculate book part sizes
224     size = 0;
225     foreach (QString part, parts) {
226         QFileInfo info(QDir(rootPath()).absoluteFilePath(content[part].href));
227         content[part].size = info.size();
228         size += content[part].size;
229     }
230
231     return ret;
232 }
233
234 bool Book::clearDir(const QString &dir)
235 {
236     QDir d(dir);
237     if (!d.exists()) {
238         return true;
239     }
240     QDirIterator i(dir, QDirIterator::Subdirectories);
241     while (i.hasNext()) {
242         QString entry = i.next();
243         if (entry.endsWith("/.") || entry.endsWith("/..")) {
244             continue;
245         }
246         QFileInfo info(entry);
247         if (info.isDir()) {
248             if (!clearDir(entry)) {
249                 return false;
250             }
251         }
252         else {
253             if (!QFile::remove(entry)) {
254                 qCritical() << "Book::clearDir: Could not remove" << entry;
255                 // FIXME: To be investigated: This is happening too often
256                 // return false;
257             }
258         }
259     }
260     (void)d.rmpath(dir);
261     return true;
262 }
263
264 void Book::clear()
265 {
266     close();
267     title = "";
268     creators.clear();
269     date = "";
270     publisher = "";
271     datePublished = "";
272     subject = "";
273     source = "";
274     rights = "";
275 }
276
277 void Book::load()
278 {
279     Trace t("Book::load");
280     qDebug() << "path" << path();
281
282 #if 1
283     QVariantHash data = BookDb::instance()->load(path());
284     title = data["title"].toString();
285     qDebug() << title;
286     creators = data["creators"].toStringList();
287     date = data["date"].toString();
288     publisher = data["publisher"].toString();
289     datePublished = data["datepublished"].toString();
290     subject = data["subject"].toString();
291     source = data["source"].toString();
292     rights = data["rights"].toString();
293     mLastBookmark.part = data["lastpart"].toInt();
294     mLastBookmark.pos = data["lastpos"].toReal();
295     cover = data["cover"].value<QImage>().scaled(COVER_WIDTH,
296         COVER_HEIGHT, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
297     if (cover.isNull()) {
298         cover = makeCover(":/icons/book.png");
299     }
300     int size = data["bookmarks"].toInt();
301     for (int i = 0; i < size; i++) {
302         int part = data[QString("bookmark%1part").arg(i)].toInt();
303         qreal pos = data[QString("bookmark%1pos").arg(i)].toReal();
304         mBookmarks.append(Bookmark(part, pos));
305     }
306 #else
307     QSettings settings;
308     QString key = "book/" + path() + "/";
309     qDebug() << "key" << key;
310
311     // Load book info
312     title = settings.value(key + "title").toString();
313     qDebug() << title;
314     creators = settings.value(key + "creators").toStringList();
315     date = settings.value(key + "date").toString();
316     publisher = settings.value(key + "publisher").toString();
317     datePublished = settings.value(key + "datepublished").toString();
318     subject = settings.value(key + "subject").toString();
319     source = settings.value(key + "source").toString();
320     rights = settings.value(key + "rights").toString();
321     mLastBookmark.part = settings.value(key + "lastpart").toInt();
322     mLastBookmark.pos = settings.value(key + "lastpos").toReal();
323     cover = settings.value(key + "cover").value<QImage>().scaled(COVER_WIDTH,
324         COVER_HEIGHT, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
325     if (cover.isNull()) {
326         cover = makeCover(":/icons/book.png");
327     }
328
329     // Load bookmarks
330     int size = settings.value(key + "bookmarks").toInt();
331     for (int i = 0; i < size; i++) {
332         int part = settings.value(key + "bookmark" + QString::number(i) +
333                                      "/part").toInt();
334         qreal pos = settings.value(key + "bookmark" + QString::number(i) +
335                                    "/pos").toReal();
336         qDebug() << QString("Bookmark %1 at part %2, %3").
337                 arg(i).arg(part).arg(pos);
338         mBookmarks.append(Bookmark(part, pos));
339     }
340 #endif
341 }
342
343 void Book::save()
344 {
345     Trace t("Book::save");
346
347 #if 1
348     QVariantHash data;
349     data["title"] = title;
350     data["creators"] = creators;
351     data["date"] = date;
352     data["publisher"] = publisher;
353     data["datepublished"] = datePublished;
354     data["subject"] = subject;
355     data["source"] = source;
356     data["rights"] = rights;
357     data["lastpart"] = mLastBookmark.part;
358     data["lastpos"] = mLastBookmark.pos;
359     data["cover"] = cover;
360     data["bookmarks"] = mBookmarks.size();
361     for (int i = 0; i < mBookmarks.size(); i++) {
362         data[QString("bookmark%1part").arg(i)] = mBookmarks[i].part;
363         data[QString("bookmark%1pos").arg(i)] = mBookmarks[i].pos;
364     }
365     BookDb::instance()->save(path(), data);
366 #else
367     QSettings settings;
368     QString key = "book/" + path() + "/";
369     qDebug() << "key" << key;
370
371     // Save book info
372     settings.setValue(key + "title", title);
373     qDebug() << "title" << title;
374     settings.setValue(key + "creators", creators);
375     settings.setValue(key + "date", date);
376     settings.setValue(key + "publisher", publisher);
377     settings.setValue(key + "datepublished", datePublished);
378     settings.setValue(key + "subject", subject);
379     settings.setValue(key + "source", source);
380     settings.setValue(key + "rights", rights);
381     settings.setValue(key + "lastpart", mLastBookmark.part);
382     settings.setValue(key + "lastpos", mLastBookmark.pos);
383     settings.setValue(key + "cover", cover);
384
385     // Save bookmarks
386     settings.setValue(key + "bookmarks", mBookmarks.size());
387     for (int i = 0; i < mBookmarks.size(); i++) {
388         qDebug() << QString("Bookmark %1 at %2, %3").
389                 arg(i).arg(mBookmarks[i].part).arg(mBookmarks[i].pos);
390         settings.setValue(key + "bookmark" + QString::number(i) + "/part",
391                           mBookmarks[i].part);
392         settings.setValue(key + "bookmark" + QString::number(i) + "/pos",
393                           mBookmarks[i].pos);
394     }
395 #endif
396 }
397
398 void Book::setLastBookmark(int part, qreal position)
399 {
400     mLastBookmark.part = part;
401     mLastBookmark.pos = position;
402     save();
403 }
404
405 Book::Bookmark Book::lastBookmark() const
406 {
407     return Book::Bookmark(mLastBookmark);
408 }
409
410 void Book::addBookmark(int part, qreal position)
411 {
412     mBookmarks.append(Bookmark(part, position));
413     qSort(mBookmarks.begin(), mBookmarks.end());
414     save();
415 }
416
417 void Book::deleteBookmark(int index)
418 {
419     mBookmarks.removeAt(index);
420     save();
421 }
422
423 QList<Book::Bookmark> Book::bookmarks() const
424 {
425     return mBookmarks;
426 }
427
428 QString Book::opsPath()
429 {
430     Trace t("Book::opsPath");
431     QString ret;
432
433     QFile container(tmpDir() + "/META-INF/container.xml");
434     qDebug() << container.fileName();
435     QXmlSimpleReader reader;
436     QXmlInputSource *source = new QXmlInputSource(&container);
437     ContainerHandler *containerHandler = new ContainerHandler();
438     XmlErrorHandler *errorHandler = new XmlErrorHandler();
439     reader.setContentHandler(containerHandler);
440     reader.setErrorHandler(errorHandler);
441     if (reader.parse(source)) {
442         ret = tmpDir() + "/" + containerHandler->rootFile;
443         mRootPath = QFileInfo(ret).absoluteDir().absolutePath();
444         qDebug() << "OSP path" << ret << "\nRoot dir" << mRootPath;
445     }
446     delete errorHandler;
447     delete containerHandler;
448     delete source;
449     return ret;
450 }
451
452 QString Book::rootPath() const
453 {
454     return mRootPath;
455 }
456
457 QString Book::name() const
458 {
459     if (title.size()) {
460         QString ret = title;
461         if (creators.length()) {
462             ret += "\nBy " + creators[0];
463             for (int i = 1; i < creators.length(); i++) {
464                 ret += ", " + creators[i];
465             }
466         }
467         return ret;
468     } else {
469         return path();
470     }
471 }
472
473 QString Book::shortName() const
474 {
475     return (title.isEmpty())? QFileInfo(path()).baseName(): title;
476 }
477
478 int Book::chapterFromPart(int index)
479 {
480     int ret = -1;
481
482     QString partId = parts[index];
483     QString partHref = content[partId].href;
484
485     for (int i = 0; i < chapters.size(); i++) {
486         QString id = chapters[i];
487         QString href = content[id].href;
488         QString baseRef(href);
489         QUrl url(QString("file://") + href);
490         if (url.hasFragment()) {
491             QString fragment = url.fragment();
492             baseRef.chop(fragment.length() + 1);
493         }
494         if (baseRef == partHref) {
495             ret = i;
496             // Don't break, keep looking
497         }
498     }
499
500     return ret;
501 }
502
503 int Book::partFromChapter(int index)
504 {
505     Trace t("Book::partFromChapter");
506     QString id = chapters[index];
507     QString href = content[id].href;
508     int hashPos = href.indexOf("#");
509     if (hashPos != -1) {
510         href = href.left(hashPos);
511     }
512
513     qDebug() << "Chapter" << index;
514     qDebug() << " id" << id;
515     qDebug() << " href" << href;
516
517     for (int i = 0; i < parts.size(); i++) {
518         QString partId = parts[i];
519         if (content[partId].href == href) {
520             qDebug() << "Part index for" << href << "is" << i;
521             return i;
522         }
523     }
524
525     qWarning() << "Book::partFromChapter: Could not find part index for"
526             << href;
527     return -1;
528 }
529
530 qreal Book::getProgress(int part, qreal position)
531 {
532     Q_ASSERT(part < parts.size());
533     QString key;
534     qreal partSize = 0;
535     for (int i = 0; i < part; i++) {
536         key = parts[i];
537         partSize += content[key].size;
538     }
539     key = parts[part];
540     partSize += content[key].size * position;
541     return partSize / (qreal)size;
542 }
543
544 bool Book::extractMetaData()
545 {
546     QStringList excludedExtensions;
547     excludedExtensions << ".html" << ".xhtml" << ".xht" << ".htm";
548     return extract(excludedExtensions);
549 }
550
551 void Book::upgrade()
552 {
553     Trace t("Book::upgrade");
554     qDebug() << path();
555 }