Fix naming: book parts vs. chapters.
[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 != "") {
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() == "") {
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     toc.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 = toc;
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     return ret;
174 }
175
176 bool Book::clearDir(const QString &dir)
177 {
178     QDir d(dir);
179     if (!d.exists()) {
180         return true;
181     }
182     QDirIterator i(dir, QDirIterator::Subdirectories);
183     while (i.hasNext()) {
184         QString entry = i.next();
185         if (entry.endsWith("/.") || entry.endsWith("/..")) {
186             continue;
187         }
188         QFileInfo info(entry);
189         if (info.isDir()) {
190             if (!clearDir(entry)) {
191                 return false;
192             }
193         }
194         else {
195             if (!QFile::remove(entry)) {
196                 qCritical() << "Book::clearDir: Could not remove" << entry;
197                 // FIXME: To be investigated: This is happening too often
198                 // return false;
199             }
200         }
201     }
202     (void)d.rmpath(dir);
203     return true;
204 }
205
206 void Book::clear()
207 {
208     close();
209     title = "";
210     creators.clear();
211     date = "";
212     publisher = "";
213     datePublished = "";
214     subject = "";
215     source = "";
216     rights = "";
217 }
218
219 void Book::load()
220 {
221     Trace t("Book::load");
222     t.trace("path: " + path());
223     QSettings settings;
224     QString key = "book/" + path() + "/";
225     t.trace("key: " + key);
226
227     // Load book info
228     title = settings.value(key + "title").toString();
229     t.trace(title);
230     creators = settings.value(key + "creators").toStringList();
231     date = settings.value(key + "date").toString();
232     publisher = settings.value(key + "publisher").toString();
233     datePublished = settings.value(key + "datepublished").toString();
234     subject = settings.value(key + "subject").toString();
235     source = settings.value(key + "source").toString();
236     rights = settings.value(key + "rights").toString();
237     mLastBookmark.part = settings.value(key + "lastpart").toInt();
238     mLastBookmark.pos = settings.value(key + "lastpos").toReal();
239     cover = settings.value(key + "cover").value<QImage>().scaled(COVER_WIDTH,
240         COVER_HEIGHT, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
241     if (cover.isNull()) {
242         cover = QImage(":/icons/book.png").scaled(COVER_WIDTH, COVER_HEIGHT,
243             Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation).
244             scaled(COVER_WIDTH, COVER_HEIGHT, Qt::KeepAspectRatio);
245     }
246
247     // Load bookmarks
248     int size = settings.value(key + "bookmarks").toInt();
249     for (int i = 0; i < size; i++) {
250         int part = settings.value(key + "bookmark" + QString::number(i) +
251                                      "/part").toInt();
252         qreal pos = settings.value(key + "bookmark" + QString::number(i) +
253                                    "/pos").toReal();
254         t.trace(QString("Bookmark %1 at part %2, %3").
255                 arg(i).arg(part).arg(pos));
256         mBookmarks.append(Bookmark(part, pos));
257     }
258 }
259
260 void Book::save()
261 {
262     Trace t("Book::save");
263     QSettings settings;
264     QString key = "book/" + path() + "/";
265     t.trace("key: " + key);
266
267     // Save book info
268     settings.setValue(key + "title", title);
269     t.trace("title: " + title);
270     settings.setValue(key + "creators", creators);
271     settings.setValue(key + "date", date);
272     settings.setValue(key + "publisher", publisher);
273     settings.setValue(key + "datepublished", datePublished);
274     settings.setValue(key + "subject", subject);
275     settings.setValue(key + "source", source);
276     settings.setValue(key + "rights", rights);
277     settings.setValue(key + "lastpart", mLastBookmark.part);
278     settings.setValue(key + "lastpos", mLastBookmark.pos);
279     settings.setValue(key + "cover", cover);
280
281     // Save bookmarks
282     settings.setValue(key + "bookmarks", mBookmarks.size());
283     for (int i = 0; i < mBookmarks.size(); i++) {
284         t.trace(QString("Bookmark %1 at %2, %3").
285                 arg(i).arg(mBookmarks[i].part).arg(mBookmarks[i].pos));
286         settings.setValue(key + "bookmark" + QString::number(i) + "/part",
287                           mBookmarks[i].part);
288         settings.setValue(key + "bookmark" + QString::number(i) + "/pos",
289                           mBookmarks[i].pos);
290     }
291 }
292
293 void Book::setLastBookmark(int part, qreal position)
294 {
295     mLastBookmark.part = part;
296     mLastBookmark.pos = position;
297     save();
298 }
299
300 Book::Bookmark Book::lastBookmark() const
301 {
302     return Book::Bookmark(mLastBookmark);
303 }
304
305 void Book::addBookmark(int part, qreal position)
306 {
307     mBookmarks.append(Bookmark(part, position));
308     qSort(mBookmarks.begin(), mBookmarks.end());
309     save();
310 }
311
312 void Book::deleteBookmark(int index)
313 {
314     mBookmarks.removeAt(index);
315     save();
316 }
317
318 QList<Book::Bookmark> Book::bookmarks() const
319 {
320     return mBookmarks;
321 }
322
323 QString Book::opsPath()
324 {
325     Trace t("Book::opsPath");
326     QString ret;
327
328     QFile container(tmpDir() + "/META-INF/container.xml");
329     t.trace(container.fileName());
330     QXmlSimpleReader reader;
331     QXmlInputSource *source = new QXmlInputSource(&container);
332     ContainerHandler *containerHandler = new ContainerHandler();
333     XmlErrorHandler *errorHandler = new XmlErrorHandler();
334     reader.setContentHandler(containerHandler);
335     reader.setErrorHandler(errorHandler);
336     if (reader.parse(source)) {
337         ret = tmpDir() + "/" + containerHandler->rootFile;
338         mRootPath = QFileInfo(ret).absoluteDir().absolutePath();
339         t.trace("OSP path: " + ret);
340         t.trace("Root dir: " + mRootPath);
341     }
342     delete errorHandler;
343     delete containerHandler;
344     delete source;
345     return ret;
346 }
347
348 QString Book::rootPath() const
349 {
350     return mRootPath;
351 }
352
353 QString Book::name() const
354 {
355     if (title != "") {
356         QString ret = title;
357         if (creators.length()) {
358             ret += "\nBy " + creators[0];
359             for (int i = 1; i < creators.length(); i++) {
360                 ret += ", " + creators[i];
361             }
362         }
363         return ret;
364     } else {
365         return path();
366     }
367 }
368
369 QString Book::shortName() const
370 {
371     if (title == "") {
372         return QFileInfo(path()).baseName();
373     } else {
374         return title;
375     }
376 }
377
378 int Book::chapterFromToc(int index)
379 {
380     int ret = -1;
381     return ret;
382 }
383
384 int Book::tocFromChapter(int index)
385 {
386     Trace t("Book::tocFromChapter");
387     QString id = chapters[index];
388     QString href = content[id].href;
389     QString baseRef(href);
390     QUrl url(QString("file://") + href);
391     if (url.hasFragment()) {
392         QString fragment = url.fragment();
393         baseRef.chop(fragment.length() + 1);
394     }
395
396     // Swipe through all content items to find the one matching the chapter href
397     // FIXME: Do we need to index content items by href, too?
398     QString contentKey;
399     bool found = false;
400     foreach (contentKey, content.keys()) {
401         if (contentKey == id) {
402             continue;
403         }
404         if (content[contentKey].href == baseRef) {
405             found = true;
406             t.trace(QString("Key for %1 is %2").arg(baseRef).arg(contentKey));
407             break;
408         }
409     }
410     if (!found) {
411         t.trace("Could not find key for " + baseRef);
412         return -1;
413     }
414     int tocIndex = toc.indexOf(contentKey);
415     if (tocIndex == -1) {
416         qCritical() << "Book::tocFromChapter: Could not find toc index of chapter"
417                 << id;
418     }
419     return tocIndex;
420 }