.
[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 QString Book::path()
36 {
37     return mPath;
38 }
39
40 bool Book::open()
41 {
42     TRACE;
43     qDebug() << path();
44     close();
45     clear();
46     load();
47     if (path().isEmpty()) {
48         title = "No book";
49         return false;
50     }
51     if (!extract(QStringList())) {
52         return false;
53     }
54     if (!parse()) {
55         return false;
56     }
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 of
195     // 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>().scaled(COVER_WIDTH,
297         COVER_HEIGHT, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
298     if (cover.isNull()) {
299         cover = makeCover(":/icons/book.png");
300     }
301     int size = data["bookmarks"].toInt();
302     for (int i = 0; i < size; i++) {
303         int part = data[QString("bookmark%1part").arg(i)].toInt();
304         qreal pos = data[QString("bookmark%1pos").arg(i)].toReal();
305         QString note = data[QString("bookmark%1note").arg(i)].toString();
306         mBookmarks.append(Bookmark(part, pos, note));
307     }
308 }
309
310 void Book::save()
311 {
312     TRACE;
313
314     load();
315     QVariantHash data;
316     data["title"] = title;
317     data["creators"] = creators;
318     data["date"] = date;
319     data["publisher"] = publisher;
320     data["datepublished"] = datePublished;
321     data["subject"] = subject;
322     data["source"] = source;
323     data["rights"] = rights;
324     data["lastpart"] = mLastBookmark.part;
325     data["lastpos"] = mLastBookmark.pos;
326     data["cover"] = cover;
327     data["bookmarks"] = mBookmarks.size();
328     for (int i = 0; i < mBookmarks.size(); i++) {
329         data[QString("bookmark%1part").arg(i)] = mBookmarks[i].part;
330         data[QString("bookmark%1pos").arg(i)] = mBookmarks[i].pos;
331         data[QString("bookmark%1note").arg(i)] = mBookmarks[i].note;
332     }
333     BookDb::instance()->save(path(), data);
334 }
335
336 void Book::setLastBookmark(int part, qreal position)
337 {
338     TRACE;
339     load();
340     mLastBookmark.part = part;
341     mLastBookmark.pos = position;
342     save();
343 }
344
345 Book::Bookmark Book::lastBookmark()
346 {
347     load();
348     return Book::Bookmark(mLastBookmark);
349 }
350
351 void Book::addBookmark(int part, qreal position, const QString &note)
352 {
353     load();
354     mBookmarks.append(Bookmark(part, position, note));
355     qSort(mBookmarks.begin(), mBookmarks.end());
356     save();
357 }
358
359 void Book::deleteBookmark(int index)
360 {
361     load();
362     mBookmarks.removeAt(index);
363     save();
364 }
365
366 QList<Book::Bookmark> Book::bookmarks()
367 {
368     load();
369     return mBookmarks;
370 }
371
372 QString Book::opsPath()
373 {
374     TRACE;
375     load();
376     QString ret;
377
378     QFile container(tmpDir() + "/META-INF/container.xml");
379     qDebug() << container.fileName();
380     QXmlSimpleReader reader;
381     QXmlInputSource *source = new QXmlInputSource(&container);
382     ContainerHandler *containerHandler = new ContainerHandler();
383     XmlErrorHandler *errorHandler = new XmlErrorHandler();
384     reader.setContentHandler(containerHandler);
385     reader.setErrorHandler(errorHandler);
386     if (reader.parse(source)) {
387         ret = tmpDir() + "/" + containerHandler->rootFile;
388         mRootPath = QFileInfo(ret).absoluteDir().absolutePath();
389         qDebug() << "OSP path" << ret << "\nRoot dir" << mRootPath;
390     }
391     delete errorHandler;
392     delete containerHandler;
393     delete source;
394     return ret;
395 }
396
397 QString Book::rootPath()
398 {
399     load();
400     return mRootPath;
401 }
402
403 QString Book::name()
404 {
405     load();
406     if (title.size()) {
407         QString ret = title;
408         if (creators.length()) {
409             ret += "\nBy " + creators[0];
410             for (int i = 1; i < creators.length(); i++) {
411                 ret += ", " + creators[i];
412             }
413         }
414         return ret;
415     } else {
416         return path();
417     }
418 }
419
420 QString Book::shortName()
421 {
422     load();
423     return (title.isEmpty())? QFileInfo(path()).baseName(): title;
424 }
425
426 int Book::chapterFromPart(int index)
427 {
428     TRACE;
429     load();
430     int ret = -1;
431
432     QString partId = parts[index];
433     QString partHref = content[partId].href;
434
435     for (int i = 0; i < chapters.size(); i++) {
436         QString id = chapters[i];
437         QString href = content[id].href;
438         QString baseRef(href);
439         QUrl url(QString("file://") + href);
440         if (url.hasFragment()) {
441             QString fragment = url.fragment();
442             baseRef.chop(fragment.length() + 1);
443         }
444         if (baseRef == partHref) {
445             ret = i;
446             // Don't break, keep looking
447         }
448     }
449
450     return ret;
451 }
452
453 int Book::partFromChapter(int index, QString &fragment)
454 {
455     TRACE;
456     load();
457     fragment.clear();
458     QString id = chapters[index];
459     QString href = content[id].href;
460     int hashPos = href.indexOf("#");
461     if (hashPos != -1) {
462         fragment = href.mid(hashPos);
463         href = href.left(hashPos);
464     }
465
466     qDebug() << "Chapter" << index;
467     qDebug() << " id" << id;
468     qDebug() << " href" << href;
469     qDebug() << " fragment" << fragment;
470
471     for (int i = 0; i < parts.size(); i++) {
472         QString partId = parts[i];
473         if (content[partId].href == href) {
474             qDebug() << "Part index for" << href << "is" << i;
475             return i;
476         }
477     }
478
479     qWarning() << "Book::partFromChapter: Could not find part index for"
480             << href;
481     return -1;
482 }
483
484 qreal Book::getProgress(int part, qreal position)
485 {
486     load();
487     Q_ASSERT(part < parts.size());
488     QString key;
489     qreal partSize = 0;
490     for (int i = 0; i < part; i++) {
491         key = parts[i];
492         partSize += content[key].size;
493     }
494     key = parts[part];
495     partSize += content[key].size * position;
496     return partSize / (qreal)size;
497 }
498
499 bool Book::extractMetaData()
500 {
501     QStringList excludedExtensions;
502     excludedExtensions << ".html" << ".xhtml" << ".xht" << ".htm" << ".gif"
503             << ".png" << ".css";
504     return extract(excludedExtensions);
505 }
506
507 void Book::upgrade()
508 {
509     TRACE;
510
511     // Load book from old database (QSettings)
512
513     QSettings settings;
514     QString key = "book/" + path() + "/";
515     title = settings.value(key + "title").toString();
516     qDebug() << title;
517     creators = settings.value(key + "creators").toStringList();
518     date = settings.value(key + "date").toString();
519     publisher = settings.value(key + "publisher").toString();
520     datePublished = settings.value(key + "datepublished").toString();
521     subject = settings.value(key + "subject").toString();
522     source = settings.value(key + "source").toString();
523     rights = settings.value(key + "rights").toString();
524     mLastBookmark.part = settings.value(key + "lastpart").toInt();
525     mLastBookmark.pos = settings.value(key + "lastpos").toReal();
526     cover = settings.value(key + "cover").value<QImage>().scaled(COVER_WIDTH,
527         COVER_HEIGHT, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
528     if (cover.isNull()) {
529         cover = makeCover(":/icons/book.png");
530     }
531     int size = settings.value(key + "bookmarks").toInt();
532     for (int i = 0; i < size; i++) {
533         int part = settings.value(key + "bookmark" + QString::number(i) +
534                                      "/part").toInt();
535         qreal pos = settings.value(key + "bookmark" + QString::number(i) +
536                                    "/pos").toReal();
537         qDebug() << QString("Bookmark %1 at part %2, %3").
538                 arg(i).arg(part).arg(pos);
539         mBookmarks.append(Bookmark(part, pos));
540     }
541
542     // Save book to new database
543
544     save();
545 }
546
547 void Book::remove()
548 {
549     TRACE;
550     load();
551     BookDb::instance()->remove(path());
552 }