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