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