Show reading progress.
[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
10 #include "book.h"
11 #include "opshandler.h"
12 #include "xmlerrorhandler.h"
13 #include "extractzip.h"
14 #include "library.h"
15 #include "containerhandler.h"
16 #include "ncxhandler.h"
17 #include "trace.h"
18
19 const int COVER_WIDTH = 53;
20 const int COVER_HEIGHT = 59;
21
22 Book::Book(const QString &p, QObject *parent): QObject(parent)
23 {
24     mPath = "";
25     if (p.size()) {
26         QFileInfo info(p);
27         mPath = info.absoluteFilePath();
28         title = info.baseName();
29         cover = QImage(":/icons/book.png").scaled(COVER_WIDTH, COVER_HEIGHT,
30             Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation).
31             scaled(COVER_WIDTH, COVER_HEIGHT, Qt::KeepAspectRatio);
32     }
33 }
34
35 QString Book::path() const
36 {
37     return mPath;
38 }
39
40 bool Book::open()
41 {
42     Trace t("Book::open");
43     t.trace(path());
44     close();
45     clear();
46     if (path().isEmpty()) {
47         title = "No book";
48         return false;
49     }
50     if (!extract()) {
51         return false;
52     }
53     if (!parse()) {
54         return false;
55     }
56     save();
57     emit opened(path());
58     return true;
59 }
60
61 void Book::close()
62 {
63     Trace t("Book::close");
64     content.clear();
65     parts.clear();
66     QDir::setCurrent(QDir::rootPath());
67     clearDir(tmpDir());
68 }
69
70 QString Book::tmpDir() const
71 {
72     return QDir::tempPath() + "/dorian/book";
73 }
74
75 bool Book::extract()
76 {
77     Trace t("Book::extract");
78     bool ret = false;
79     QString tmp = tmpDir();
80     t.trace("Extracting " + mPath + " to " + tmp);
81
82     QDir::setCurrent(QDir::rootPath());
83     if (!clearDir(tmp)) {
84         qCritical() << "Book::extract: Failed to remove" << tmp;
85         return false;
86     }
87     QDir d;
88     if (!d.mkpath(tmp)) {
89         qCritical() << "Book::extract: Could not create" << tmp;
90         return false;
91     }
92
93     // If book comes from resource, copy it to the temporary directory first
94     QString bookPath = path();
95     if (bookPath.startsWith(":/books/")) {
96         QFile src(bookPath);
97         QString dst(tmp + "/book.epub");
98         if (!src.copy(dst)) {
99             qCritical() << "Book::extract: Failed to copy built-in book to"
100                     << dst;
101             return false;
102         }
103         bookPath = dst;
104     }
105
106     QString oldDir = QDir::currentPath();
107     if (!QDir::setCurrent(tmp)) {
108         qCritical() << "Book::extract: Could not change to" << tmp;
109         return false;
110     }
111     ret = extractZip(bookPath);
112     if (!ret) {
113         qCritical() << "Book::extract: Extracting ZIP failed";
114     }
115     QDir::setCurrent(oldDir);
116     return ret;
117 }
118
119 bool Book::parse()
120 {
121     Trace t("Book::parse");
122
123     // Parse OPS file
124     bool ret = false;
125     QString opsFileName = opsPath();
126     t.trace("Parsing OPS file" + opsFileName);
127     QFile opsFile(opsFileName);
128     QXmlSimpleReader reader;
129     QXmlInputSource *source = new QXmlInputSource(&opsFile);
130     OpsHandler *opsHandler = new OpsHandler(*this);
131     XmlErrorHandler *errorHandler = new XmlErrorHandler();
132     reader.setContentHandler(opsHandler);
133     reader.setErrorHandler(errorHandler);
134     ret = reader.parse(source);
135     delete errorHandler;
136     delete opsHandler;
137     delete source;
138
139     // Initially, put all content items in the chapter list.
140     // This will be refined by parsing the NCX file later
141     chapters = parts;
142
143     // Load cover image
144     QStringList coverKeys;
145     coverKeys << "cover-image" << "img-cover-jpeg" << "cover";
146     foreach (QString key, coverKeys) {
147         if (content.contains(key)) {
148             t.trace("Loading cover image from " + content[key].href);
149             cover = QImage(content[key].href).scaled(COVER_WIDTH, COVER_HEIGHT,
150                 Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation).
151                 scaled(COVER_WIDTH, COVER_HEIGHT, Qt::KeepAspectRatio);
152             break;
153         }
154     }
155
156     // If there is an "ncx" item in content, parse it: That's the real table of
157     // contents
158     if (content.contains("ncx")) {
159         QString ncxFileName = content["ncx"].href;
160         t.trace("Parsing NCX file " + ncxFileName);
161         QFile ncxFile(ncxFileName);
162         source = new QXmlInputSource(&ncxFile);
163         NcxHandler *ncxHandler = new NcxHandler(*this);
164         errorHandler = new XmlErrorHandler();
165         reader.setContentHandler(ncxHandler);
166         reader.setErrorHandler(errorHandler);
167         ret = reader.parse(source);
168         delete ncxHandler;
169         delete errorHandler;
170         delete source;
171     }
172
173     // Calculate book part sizes
174     size = 0;
175     foreach (QString part, parts) {
176         QFileInfo info(content[part].href);
177         content[part].size = info.size();
178         size += content[part].size;
179         t.trace(QString("Size of part %1: %2").arg(part).arg(content[part].size));
180     }
181
182     return ret;
183 }
184
185 bool Book::clearDir(const QString &dir)
186 {
187     QDir d(dir);
188     if (!d.exists()) {
189         return true;
190     }
191     QDirIterator i(dir, QDirIterator::Subdirectories);
192     while (i.hasNext()) {
193         QString entry = i.next();
194         if (entry.endsWith("/.") || entry.endsWith("/..")) {
195             continue;
196         }
197         QFileInfo info(entry);
198         if (info.isDir()) {
199             if (!clearDir(entry)) {
200                 return false;
201             }
202         }
203         else {
204             if (!QFile::remove(entry)) {
205                 qCritical() << "Book::clearDir: Could not remove" << entry;
206                 // FIXME: To be investigated: This is happening too often
207                 // return false;
208             }
209         }
210     }
211     (void)d.rmpath(dir);
212     return true;
213 }
214
215 void Book::clear()
216 {
217     close();
218     title = "";
219     creators.clear();
220     date = "";
221     publisher = "";
222     datePublished = "";
223     subject = "";
224     source = "";
225     rights = "";
226 }
227
228 void Book::load()
229 {
230     Trace t("Book::load");
231     t.trace("path: " + path());
232     QSettings settings;
233     QString key = "book/" + path() + "/";
234     t.trace("key: " + key);
235
236     // Load book info
237     title = settings.value(key + "title").toString();
238     t.trace(title);
239     creators = settings.value(key + "creators").toStringList();
240     date = settings.value(key + "date").toString();
241     publisher = settings.value(key + "publisher").toString();
242     datePublished = settings.value(key + "datepublished").toString();
243     subject = settings.value(key + "subject").toString();
244     source = settings.value(key + "source").toString();
245     rights = settings.value(key + "rights").toString();
246     mLastBookmark.part = settings.value(key + "lastpart").toInt();
247     mLastBookmark.pos = settings.value(key + "lastpos").toReal();
248     cover = settings.value(key + "cover").value<QImage>().scaled(COVER_WIDTH,
249         COVER_HEIGHT, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
250     if (cover.isNull()) {
251         cover = QImage(":/icons/book.png").scaled(COVER_WIDTH, COVER_HEIGHT,
252             Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation).
253             scaled(COVER_WIDTH, COVER_HEIGHT, Qt::KeepAspectRatio);
254     }
255
256     // Load bookmarks
257     int size = settings.value(key + "bookmarks").toInt();
258     for (int i = 0; i < size; i++) {
259         int part = settings.value(key + "bookmark" + QString::number(i) +
260                                      "/part").toInt();
261         qreal pos = settings.value(key + "bookmark" + QString::number(i) +
262                                    "/pos").toReal();
263         t.trace(QString("Bookmark %1 at part %2, %3").
264                 arg(i).arg(part).arg(pos));
265         mBookmarks.append(Bookmark(part, pos));
266     }
267 }
268
269 void Book::save()
270 {
271     Trace t("Book::save");
272     QSettings settings;
273     QString key = "book/" + path() + "/";
274     t.trace("key: " + key);
275
276     // Save book info
277     settings.setValue(key + "title", title);
278     t.trace("title: " + title);
279     settings.setValue(key + "creators", creators);
280     settings.setValue(key + "date", date);
281     settings.setValue(key + "publisher", publisher);
282     settings.setValue(key + "datepublished", datePublished);
283     settings.setValue(key + "subject", subject);
284     settings.setValue(key + "source", source);
285     settings.setValue(key + "rights", rights);
286     settings.setValue(key + "lastpart", mLastBookmark.part);
287     settings.setValue(key + "lastpos", mLastBookmark.pos);
288     settings.setValue(key + "cover", cover);
289
290     // Save bookmarks
291     settings.setValue(key + "bookmarks", mBookmarks.size());
292     for (int i = 0; i < mBookmarks.size(); i++) {
293         t.trace(QString("Bookmark %1 at %2, %3").
294                 arg(i).arg(mBookmarks[i].part).arg(mBookmarks[i].pos));
295         settings.setValue(key + "bookmark" + QString::number(i) + "/part",
296                           mBookmarks[i].part);
297         settings.setValue(key + "bookmark" + QString::number(i) + "/pos",
298                           mBookmarks[i].pos);
299     }
300 }
301
302 void Book::setLastBookmark(int part, qreal position)
303 {
304     mLastBookmark.part = part;
305     mLastBookmark.pos = position;
306     save();
307 }
308
309 Book::Bookmark Book::lastBookmark() const
310 {
311     return Book::Bookmark(mLastBookmark);
312 }
313
314 void Book::addBookmark(int part, qreal position)
315 {
316     mBookmarks.append(Bookmark(part, position));
317     qSort(mBookmarks.begin(), mBookmarks.end());
318     save();
319 }
320
321 void Book::deleteBookmark(int index)
322 {
323     mBookmarks.removeAt(index);
324     save();
325 }
326
327 QList<Book::Bookmark> Book::bookmarks() const
328 {
329     return mBookmarks;
330 }
331
332 QString Book::opsPath()
333 {
334     Trace t("Book::opsPath");
335     QString ret;
336
337     QFile container(tmpDir() + "/META-INF/container.xml");
338     t.trace(container.fileName());
339     QXmlSimpleReader reader;
340     QXmlInputSource *source = new QXmlInputSource(&container);
341     ContainerHandler *containerHandler = new ContainerHandler();
342     XmlErrorHandler *errorHandler = new XmlErrorHandler();
343     reader.setContentHandler(containerHandler);
344     reader.setErrorHandler(errorHandler);
345     if (reader.parse(source)) {
346         ret = tmpDir() + "/" + containerHandler->rootFile;
347         mRootPath = QFileInfo(ret).absoluteDir().absolutePath();
348         t.trace("OSP path: " + ret);
349         t.trace("Root dir: " + mRootPath);
350     }
351     delete errorHandler;
352     delete containerHandler;
353     delete source;
354     return ret;
355 }
356
357 QString Book::rootPath() const
358 {
359     return mRootPath;
360 }
361
362 QString Book::name() const
363 {
364     if (title.size()) {
365         QString ret = title;
366         if (creators.length()) {
367             ret += "\nBy " + creators[0];
368             for (int i = 1; i < creators.length(); i++) {
369                 ret += ", " + creators[i];
370             }
371         }
372         return ret;
373     } else {
374         return path();
375     }
376 }
377
378 QString Book::shortName() const
379 {
380     return (title.isEmpty())? QFileInfo(path()).baseName(): title;
381 }
382
383 int Book::chapterFromPart(int index)
384 {
385     // FIXME
386     Q_UNUSED(index);
387     int ret = -1;
388     return ret;
389 }
390
391 int Book::partFromChapter(int index)
392 {
393     Trace t("Book::partFromChapter");
394     QString id = chapters[index];
395     QString href = content[id].href;
396     QString baseRef(href);
397     QUrl url(QString("file://") + href);
398     if (url.hasFragment()) {
399         QString fragment = url.fragment();
400         baseRef.chop(fragment.length() + 1);
401     }
402
403     // Swipe through all content items to find the one matching the chapter href
404     // FIXME: Do we need to index content items by href, too?
405     QString contentKey;
406     bool found = false;
407     foreach (contentKey, content.keys()) {
408         if (contentKey == id) {
409             continue;
410         }
411         if (content[contentKey].href == baseRef) {
412             found = true;
413             t.trace(QString("Key for %1 is %2").arg(baseRef).arg(contentKey));
414             break;
415         }
416     }
417     if (!found) {
418         t.trace("Could not find key for " + baseRef);
419         return -1;
420     }
421     int partIndex = parts.indexOf(contentKey);
422     if (partIndex == -1) {
423         qCritical()
424             << "Book::partFromChapter: Could not find part index of chapter"
425             << id;
426     }
427     return partIndex;
428 }
429
430 qreal Book::getProgress(int part, qreal position)
431 {
432     Q_ASSERT(part < parts.size());
433     QString key;
434     qreal partSize = 0;
435     for (int i = 0; i < part; i++) {
436         key = parts[i];
437         partSize += content[key].size;
438     }
439     key = parts[part];
440     partSize += content[key].size * position;
441     return partSize / (qreal)size;
442 }