More reorg.
[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     if (content.contains("cover-image")) {
140         t.trace("Loading cover image from " + content["cover-image"].href);
141         cover = QImage(content["cover-image"].href).scaled(COVER_WIDTH,
142             COVER_HEIGHT, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
143     } else if (content.contains("img-cover-jpeg")) {
144         t.trace("Loading cover image from " + content["img-cover-jpeg"].href);
145         cover = QImage(content["img-cover-jpeg"].href).scaled(COVER_WIDTH,
146             COVER_HEIGHT, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
147     }
148
149     // If there is an "ncx" item in content, parse it: That's the real table of
150     // contents
151     if (content.contains("ncx")) {
152         QString ncxFileName = content["ncx"].href;
153         t.trace("Parsing NCX file " + ncxFileName);
154         QFile ncxFile(ncxFileName);
155         source = new QXmlInputSource(&ncxFile);
156         NcxHandler *ncxHandler = new NcxHandler(*this);
157         errorHandler = new XmlErrorHandler();
158         reader.setContentHandler(ncxHandler);
159         reader.setErrorHandler(errorHandler);
160         ret = reader.parse(source);
161         delete ncxHandler;
162         delete errorHandler;
163         delete source;
164     }
165
166     return ret;
167 }
168
169 bool Book::clearDir(const QString &dir)
170 {
171     QDir d(dir);
172     if (!d.exists()) {
173         return true;
174     }
175     QDirIterator i(dir, QDirIterator::Subdirectories);
176     while (i.hasNext()) {
177         QString entry = i.next();
178         if (entry.endsWith("/.") || entry.endsWith("/..")) {
179             continue;
180         }
181         QFileInfo info(entry);
182         if (info.isDir()) {
183             if (!clearDir(entry)) {
184                 return false;
185             }
186         }
187         else {
188             if (!QFile::remove(entry)) {
189                 qCritical() << "Book::clearDir: Could not remove" << entry;
190                 // FIXME: To be investigated: This is happening too often
191                 // return false;
192             }
193         }
194     }
195     (void)d.rmpath(dir);
196     return true;
197 }
198
199 void Book::clear()
200 {
201     close();
202     title = "";
203     creators.clear();
204     date = "";
205     publisher = "";
206     datePublished = "";
207     subject = "";
208     source = "";
209     rights = "";
210 }
211
212 void Book::load()
213 {
214     Trace t("Book::load");
215     t.trace("path: " + path());
216     QSettings settings;
217     QString key = "book/" + path() + "/";
218     t.trace("key: " + key);
219
220     // Load book info
221     title = settings.value(key + "title").toString();
222     t.trace(title);
223     creators = settings.value(key + "creators").toStringList();
224     date = settings.value(key + "date").toString();
225     publisher = settings.value(key + "publisher").toString();
226     datePublished = settings.value(key + "datepublished").toString();
227     subject = settings.value(key + "subject").toString();
228     source = settings.value(key + "source").toString();
229     rights = settings.value(key + "rights").toString();
230     mLastBookmark.chapter = settings.value(key + "lastchapter").toInt();
231     mLastBookmark.pos = settings.value(key + "lastpos").toReal();
232     cover = settings.value(key + "cover").value<QImage>().scaled(COVER_WIDTH,
233         COVER_HEIGHT, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
234     if (cover.isNull()) {
235         cover = QImage(":/icons/book.png").scaled(COVER_WIDTH, COVER_HEIGHT,
236             Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
237     }
238
239     // Load bookmarks
240     int size = settings.value(key + "bookmarks").toInt();
241     for (int i = 0; i < size; i++) {
242         int chapter = settings.value(key + "bookmark" + QString::number(i) +
243                                      "/chapter").toInt();
244         qreal pos = settings.value(key + "bookmark" + QString::number(i) +
245                                    "/pos").toReal();
246         t.trace(QString("Bookmark %1 at chapter %2, %3").
247                 arg(i).arg(chapter).arg(pos));
248         mBookmarks.append(Bookmark(chapter, pos));
249     }
250 }
251
252 void Book::save()
253 {
254     Trace t("Book::save");
255     QSettings settings;
256     QString key = "book/" + path() + "/";
257     t.trace("key: " + key);
258
259     // Save book info
260     settings.setValue(key + "title", title);
261     t.trace("title: " + title);
262     settings.setValue(key + "creators", creators);
263     settings.setValue(key + "date", date);
264     settings.setValue(key + "publisher", publisher);
265     settings.setValue(key + "datepublished", datePublished);
266     settings.setValue(key + "subject", subject);
267     settings.setValue(key + "source", source);
268     settings.setValue(key + "rights", rights);
269     settings.setValue(key + "lastchapter", mLastBookmark.chapter);
270     settings.setValue(key + "lastpos", mLastBookmark.pos);
271     settings.setValue(key + "cover", cover);
272
273     // Save bookmarks
274     settings.setValue(key + "bookmarks", mBookmarks.size());
275     for (int i = 0; i < mBookmarks.size(); i++) {
276         t.trace(QString("Bookmark %1 at %2, %3").
277                 arg(i).arg(mBookmarks[i].chapter).arg(mBookmarks[i].pos));
278         settings.setValue(key + "bookmark" + QString::number(i) + "/chapter",
279                           mBookmarks[i].chapter);
280         settings.setValue(key + "bookmark" + QString::number(i) + "/pos",
281                           mBookmarks[i].pos);
282     }
283 }
284
285 void Book::setLastBookmark(int chapter, qreal position)
286 {
287     mLastBookmark.chapter = chapter;
288     mLastBookmark.pos = position;
289     save();
290 }
291
292 Book::Bookmark Book::lastBookmark() const
293 {
294     return Book::Bookmark(mLastBookmark);
295 }
296
297 void Book::addBookmark(int chapter, qreal position)
298 {
299     mBookmarks.append(Bookmark(chapter, position));
300     qSort(mBookmarks.begin(), mBookmarks.end());
301     save();
302 }
303
304 void Book::deleteBookmark(int index)
305 {
306     mBookmarks.removeAt(index);
307     save();
308 }
309
310 QList<Book::Bookmark> Book::bookmarks() const
311 {
312     return mBookmarks;
313 }
314
315 QString Book::opsPath()
316 {
317     Trace t("Book::opsPath");
318     QString ret;
319
320     QFile container(tmpDir() + "/META-INF/container.xml");
321     t.trace(container.fileName());
322     QXmlSimpleReader reader;
323     QXmlInputSource *source = new QXmlInputSource(&container);
324     ContainerHandler *containerHandler = new ContainerHandler();
325     XmlErrorHandler *errorHandler = new XmlErrorHandler();
326     reader.setContentHandler(containerHandler);
327     reader.setErrorHandler(errorHandler);
328     if (reader.parse(source)) {
329         ret = tmpDir() + "/" + containerHandler->rootFile;
330         mRootPath = QFileInfo(ret).absoluteDir().absolutePath();
331         t.trace("OSP path: " + ret);
332         t.trace("Root dir: " + mRootPath);
333     }
334     delete errorHandler;
335     delete containerHandler;
336     delete source;
337     return ret;
338 }
339
340 QString Book::rootPath() const
341 {
342     return mRootPath;
343 }
344
345 QString Book::name() const
346 {
347     if (title != "") {
348         QString ret = title;
349         if (creators.length()) {
350             ret += "\nBy " + creators[0];
351             for (int i = 1; i < creators.length(); i++) {
352                 ret += ", " + creators[i];
353             }
354         }
355         return ret;
356     } else {
357         return path();
358     }
359 }
360
361 QString Book::shortName() const
362 {
363     if (title == "") {
364         return QFileInfo(path()).baseName();
365     } else {
366         return title;
367     }
368 }