Improve chances to find cover image.
[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::IgnoreAspectRatio, Qt::SmoothTransformation);
31     }
32 }
33
34 QString Book::path() const
35 {
36     return mPath;
37 }
38
39 bool Book::open()
40 {
41     Trace t("Book::open");
42     t.trace(path());
43     close();
44     clear();
45     if (path() == "") {
46         title = "No book";
47         return false;
48     }
49     if (!extract()) {
50         return false;
51     }
52     if (!parse()) {
53         return false;
54     }
55     save();
56     emit opened(path());
57     return true;
58 }
59
60 void Book::close()
61 {
62     Trace t("Book::close");
63     content.clear();
64     toc.clear();
65     QDir::setCurrent(QDir::rootPath());
66     clearDir(tmpDir());
67 }
68
69 QString Book::tmpDir() const
70 {
71     return QDir::tempPath() + "/dorian/book";
72 }
73
74 bool Book::extract()
75 {
76     Trace t("Book::extract");
77     bool ret = false;
78     QString tmp = tmpDir();
79     t.trace("Extracting " + mPath + " to " + tmp);
80
81     QDir::setCurrent(QDir::rootPath());
82     if (!clearDir(tmp)) {
83         qCritical() << "Book::extract: Failed to remove" << tmp;
84         return false;
85     }
86     QDir d;
87     if (!d.mkpath(tmp)) {
88         qCritical() << "Book::extract: Could not create" << tmp;
89         return false;
90     }
91
92     // If book comes from resource, copy it to the temporary directory first
93     QString bookPath = path();
94     if (bookPath.startsWith(":/books/")) {
95         QFile src(bookPath);
96         QString dst(tmp + "/book.epub");
97         if (!src.copy(dst)) {
98             qCritical() << "Book::extract: Failed to copy built-in book to"
99                     << dst;
100             return false;
101         }
102         bookPath = dst;
103     }
104
105     QString oldDir = QDir::currentPath();
106     if (!QDir::setCurrent(tmp)) {
107         qCritical() << "Book::extract: Could not change to" << tmp;
108         return false;
109     }
110     ret = extractZip(bookPath);
111     if (!ret) {
112         qCritical() << "Book::extract: Extracting ZIP failed";
113     }
114     QDir::setCurrent(oldDir);
115     return ret;
116 }
117
118 bool Book::parse()
119 {
120     Trace t("Book::parse");
121
122     // Parse OPS file
123     bool ret = false;
124     QString opsFileName = opsPath();
125     t.trace("Parsing OPS file" + opsFileName);
126     QFile opsFile(opsFileName);
127     QXmlSimpleReader reader;
128     QXmlInputSource *source = new QXmlInputSource(&opsFile);
129     OpsHandler *opsHandler = new OpsHandler(*this);
130     XmlErrorHandler *errorHandler = new XmlErrorHandler();
131     reader.setContentHandler(opsHandler);
132     reader.setErrorHandler(errorHandler);
133     ret = reader.parse(source);
134     delete errorHandler;
135     delete opsHandler;
136     delete source;
137
138     // Load cover image
139     QStringList coverKeys;
140     coverKeys << "cover-image" << "img-cover-jpeg" << "cover";
141     foreach (QString key, coverKeys) {
142         if (content.contains(key)) {
143             t.trace("Loading cover image from " + content[key].href);
144             cover = QImage(content[key].href).scaled(COVER_WIDTH,
145                 COVER_HEIGHT, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
146             break;
147         }
148     }
149
150     // If there is an "ncx" item in content, parse it: That's the real table of
151     // contents
152     if (content.contains("ncx")) {
153         QString ncxFileName = content["ncx"].href;
154         t.trace("Parsing NCX file " + ncxFileName);
155         QFile ncxFile(ncxFileName);
156         source = new QXmlInputSource(&ncxFile);
157         NcxHandler *ncxHandler = new NcxHandler(*this);
158         errorHandler = new XmlErrorHandler();
159         reader.setContentHandler(ncxHandler);
160         reader.setErrorHandler(errorHandler);
161         ret = reader.parse(source);
162         delete ncxHandler;
163         delete errorHandler;
164         delete source;
165     }
166
167     return ret;
168 }
169
170 bool Book::clearDir(const QString &dir)
171 {
172     QDir d(dir);
173     if (!d.exists()) {
174         return true;
175     }
176     QDirIterator i(dir, QDirIterator::Subdirectories);
177     while (i.hasNext()) {
178         QString entry = i.next();
179         if (entry.endsWith("/.") || entry.endsWith("/..")) {
180             continue;
181         }
182         QFileInfo info(entry);
183         if (info.isDir()) {
184             if (!clearDir(entry)) {
185                 return false;
186             }
187         }
188         else {
189             if (!QFile::remove(entry)) {
190                 qCritical() << "Book::clearDir: Could not remove" << entry;
191                 // FIXME: To be investigated: This is happening too often
192                 // return false;
193             }
194         }
195     }
196     (void)d.rmpath(dir);
197     return true;
198 }
199
200 void Book::clear()
201 {
202     close();
203     title = "";
204     creators.clear();
205     date = "";
206     publisher = "";
207     datePublished = "";
208     subject = "";
209     source = "";
210     rights = "";
211 }
212
213 void Book::load()
214 {
215     Trace t("Book::load");
216     t.trace("path: " + path());
217     QSettings settings;
218     QString key = "book/" + path() + "/";
219     t.trace("key: " + key);
220
221     // Load book info
222     title = settings.value(key + "title").toString();
223     t.trace(title);
224     creators = settings.value(key + "creators").toStringList();
225     date = settings.value(key + "date").toString();
226     publisher = settings.value(key + "publisher").toString();
227     datePublished = settings.value(key + "datepublished").toString();
228     subject = settings.value(key + "subject").toString();
229     source = settings.value(key + "source").toString();
230     rights = settings.value(key + "rights").toString();
231     mLastBookmark.chapter = settings.value(key + "lastchapter").toInt();
232     mLastBookmark.pos = settings.value(key + "lastpos").toReal();
233     cover = settings.value(key + "cover").value<QImage>().scaled(COVER_WIDTH,
234         COVER_HEIGHT, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
235     if (cover.isNull()) {
236         cover = QImage(":/icons/book.png").scaled(COVER_WIDTH, COVER_HEIGHT,
237             Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
238     }
239
240     // Load bookmarks
241     int size = settings.value(key + "bookmarks").toInt();
242     for (int i = 0; i < size; i++) {
243         int chapter = settings.value(key + "bookmark" + QString::number(i) +
244                                      "/chapter").toInt();
245         qreal pos = settings.value(key + "bookmark" + QString::number(i) +
246                                    "/pos").toReal();
247         t.trace(QString("Bookmark %1 at chapter %2, %3").
248                 arg(i).arg(chapter).arg(pos));
249         mBookmarks.append(Bookmark(chapter, pos));
250     }
251 }
252
253 void Book::save()
254 {
255     Trace t("Book::save");
256     QSettings settings;
257     QString key = "book/" + path() + "/";
258     t.trace("key: " + key);
259
260     // Save book info
261     settings.setValue(key + "title", title);
262     t.trace("title: " + title);
263     settings.setValue(key + "creators", creators);
264     settings.setValue(key + "date", date);
265     settings.setValue(key + "publisher", publisher);
266     settings.setValue(key + "datepublished", datePublished);
267     settings.setValue(key + "subject", subject);
268     settings.setValue(key + "source", source);
269     settings.setValue(key + "rights", rights);
270     settings.setValue(key + "lastchapter", mLastBookmark.chapter);
271     settings.setValue(key + "lastpos", mLastBookmark.pos);
272     settings.setValue(key + "cover", cover);
273
274     // Save bookmarks
275     settings.setValue(key + "bookmarks", mBookmarks.size());
276     for (int i = 0; i < mBookmarks.size(); i++) {
277         t.trace(QString("Bookmark %1 at %2, %3").
278                 arg(i).arg(mBookmarks[i].chapter).arg(mBookmarks[i].pos));
279         settings.setValue(key + "bookmark" + QString::number(i) + "/chapter",
280                           mBookmarks[i].chapter);
281         settings.setValue(key + "bookmark" + QString::number(i) + "/pos",
282                           mBookmarks[i].pos);
283     }
284 }
285
286 void Book::setLastBookmark(int chapter, qreal position)
287 {
288     mLastBookmark.chapter = chapter;
289     mLastBookmark.pos = position;
290     save();
291 }
292
293 Book::Bookmark Book::lastBookmark() const
294 {
295     return Book::Bookmark(mLastBookmark);
296 }
297
298 void Book::addBookmark(int chapter, qreal position)
299 {
300     mBookmarks.append(Bookmark(chapter, position));
301     qSort(mBookmarks.begin(), mBookmarks.end());
302     save();
303 }
304
305 void Book::deleteBookmark(int index)
306 {
307     mBookmarks.removeAt(index);
308     save();
309 }
310
311 QList<Book::Bookmark> Book::bookmarks() const
312 {
313     return mBookmarks;
314 }
315
316 QString Book::opsPath()
317 {
318     Trace t("Book::opsPath");
319     QString ret;
320
321     QFile container(tmpDir() + "/META-INF/container.xml");
322     t.trace(container.fileName());
323     QXmlSimpleReader reader;
324     QXmlInputSource *source = new QXmlInputSource(&container);
325     ContainerHandler *containerHandler = new ContainerHandler();
326     XmlErrorHandler *errorHandler = new XmlErrorHandler();
327     reader.setContentHandler(containerHandler);
328     reader.setErrorHandler(errorHandler);
329     if (reader.parse(source)) {
330         ret = tmpDir() + "/" + containerHandler->rootFile;
331         mRootPath = QFileInfo(ret).absoluteDir().absolutePath();
332         t.trace("OSP path: " + ret);
333         t.trace("Root dir: " + mRootPath);
334     }
335     delete errorHandler;
336     delete containerHandler;
337     delete source;
338     return ret;
339 }
340
341 QString Book::rootPath() const
342 {
343     return mRootPath;
344 }
345
346 QString Book::name() const
347 {
348     if (title != "") {
349         QString ret = title;
350         if (creators.length()) {
351             ret += "\nBy " + creators[0];
352             for (int i = 1; i < creators.length(); i++) {
353                 ret += ", " + creators[i];
354             }
355         }
356         return ret;
357     } else {
358         return path();
359     }
360 }
361
362 QString Book::shortName() const
363 {
364     if (title == "") {
365         return QFileInfo(path()).baseName();
366     } else {
367         return title;
368     }
369 }