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