1) Исправления в связи со сменой API MySQL
[openlib.git] / www / book.php
1 <?php
2 /**
3  * COPS (Calibre OPDS PHP Server) class file
4  *
5  * @license    GPL 2 (http://www.gnu.org/licenses/gpl.html)
6  * @author     Sébastien Lucas <sebastien@slucas.fr>
7  */
8
9 require_once('base.php');
10 require_once('serie.php');
11 require_once('author.php');
12 require_once('tag.php');
13 require_once('language.php');
14 require_once('data.php');
15 require_once('resources/php-epub-meta/epub.php');
16
17 // Silly thing because PHP forbid string concatenation in class const
18 define ('SQL_BOOKS_LEFT_JOIN', "left outer join comments on comments.book = books.id");
19 define ('SQL_BOOKS_BY_FIRST_LETTER', "select {0} from books " . SQL_BOOKS_LEFT_JOIN . "
20                                                     where books.sort like ? order by books.sort");
21 define ('SQL_BOOKS_BY_AUTHOR', "select {0} from books_authors_link, books " . SQL_BOOKS_LEFT_JOIN . "
22                                                     where books_authors_link.book = books.id and author = ? order by pubdate");
23 define ('SQL_BOOKS_BY_SERIE', "select {0} from books_series_link, books " . SQL_BOOKS_LEFT_JOIN . "
24                                                     where books_series_link.book = books.id and series = ? order by series_index");
25 define ('SQL_BOOKS_BY_TAG', "select {0} from books_tags_link, books " . SQL_BOOKS_LEFT_JOIN . "
26                                                     where books_tags_link.book = books.id and tag = ? order by sort");
27 define ('SQL_BOOKS_BY_LANGUAGE', "select {0} from books_languages_link, books " . SQL_BOOKS_LEFT_JOIN . "
28                                                     where books_languages_link.book = books.id and lang_code = ? order by sort");
29 define ('SQL_BOOKS_QUERY', "select {0} from books " . SQL_BOOKS_LEFT_JOIN . "
30                                                     where (exists (select null from authors, books_authors_link where book = books.id and author = authors.id and authors.name like ?) or title like ?) order by books.sort");
31 define ('SQL_BOOKS_RECENT', "select {0} from books " . SQL_BOOKS_LEFT_JOIN . "
32                                                     where 1=1 order by timestamp desc limit ");
33
34 class Book extends Base {
35     const ALL_BOOKS_UUID = "tag:book";
36     const ALL_BOOKS_ID = "calibre:books";
37     const ALL_RECENT_BOOKS_ID = "calibre:recentbooks";
38     const BOOK_COLUMNS = "books.id as id, books.title as title, text as comment, path, timestamp, pubdate, series_index, uuid, has_cover";
39     
40     const SQL_BOOKS_LEFT_JOIN = SQL_BOOKS_LEFT_JOIN;
41     const SQL_BOOKS_BY_FIRST_LETTER = SQL_BOOKS_BY_FIRST_LETTER;
42     const SQL_BOOKS_BY_AUTHOR = SQL_BOOKS_BY_AUTHOR;
43     const SQL_BOOKS_BY_SERIE = SQL_BOOKS_BY_SERIE;
44     const SQL_BOOKS_BY_TAG = SQL_BOOKS_BY_TAG;
45     const SQL_BOOKS_BY_LANGUAGE = SQL_BOOKS_BY_LANGUAGE;
46     const SQL_BOOKS_QUERY = SQL_BOOKS_QUERY;
47     const SQL_BOOKS_RECENT = SQL_BOOKS_RECENT;
48
49     public $id;
50     public $title;
51     public $timestamp;
52     public $pubdate;
53     public $path;
54     public $uuid;
55     public $hasCover;
56     public $relativePath;
57     public $seriesIndex;
58     public $comment;
59     public $datas = NULL;
60     public $authors = NULL;
61     public $serie = NULL;
62     public $tags = NULL;
63     public $languages = NULL;
64     public $format = array ();
65
66     
67     public function __construct($line) {
68         global $config;
69         $this->id = $line->id;
70         $this->title = $line->title;
71         $this->timestamp = strtotime ($line->timestamp);
72         $this->pubdate = strtotime ($line->pubdate);
73         $this->path = Base::getFileDirectory () . $line->path;
74         $this->relativePath = $line->path;
75         $this->seriesIndex = $line->series_index;
76         $this->comment = $line->comment;
77         $this->uuid = $line->uuid;
78         $this->hasCover = $line->has_cover;
79         if (!file_exists ($this->getFilePath ("jpg"))) {
80             // double check
81             $this->hasCover = 0;
82         }
83     }
84         
85     public function getEntryId () {
86         return self::ALL_BOOKS_UUID.":".$this->id;
87     }
88     
89     public static function getEntryIdByLetter ($startingLetter) {
90         return self::ALL_BOOKS_ID.":letter:".$startingLetter;
91     }
92     
93     public function getUri () {
94         return "?page=".parent::PAGE_BOOK_DETAIL."&id=$this->id";
95     }
96     
97     public function getContentArray () {
98         global $config;
99         $i = 0;
100         $preferedData = array ();
101         foreach ($config['cops_prefered_format'] as $format)
102         {
103             if ($i == 2) { break; }
104             if ($data = $this->getDataFormat ($format)) {
105                 $i++;
106                 array_push ($preferedData, array ("url" => $data->getHtmlLink (), "name" => $format));
107             }
108         }
109         $serie = $this->getSerie ();
110         if (is_null ($serie)) {
111             $sn = "";
112             $scn = "";
113             $su = "";
114         } else {
115             $sn = $serie->name;
116             $scn = str_format (localize ("content.series.data"), $this->seriesIndex, $serie->name);
117             $link = new LinkNavigation ($serie->getUri ());
118             $su = $link->hrefXhtml ();
119         }
120         
121         return array ("id" => $this->id,
122                       "hasCover" => $this->hasCover,
123                       "preferedData" => $preferedData,
124                       "pubDate" => $this->getPubDate (),
125                       "languagesName" => $this->getLanguages (),
126                       "authorsName" => $this->getAuthorsName (),
127                       "tagsName" => $this->getTagsName (),
128                       "seriesName" => $sn,
129                       "seriesIndex" => $this->seriesIndex,
130                       "seriesCompleteName" => $scn,
131                       "seriesurl" => $su);  
132     
133     }
134     public function getFullContentArray () {
135         global $config;
136         $out = $this->getContentArray ();
137         
138         $out ["coverurl"] = Data::getLink ($this, "jpg", "image/jpeg", Link::OPDS_IMAGE_TYPE, "cover.jpg", NULL)->hrefXhtml ();
139         $out ["thumbnailurl"] = Data::getLink ($this, "jpg", "image/jpeg", Link::OPDS_THUMBNAIL_TYPE, "cover.jpg", NULL, NULL, $config['cops_html_thumbnail_height'] * 2)->hrefXhtml ();
140         $out ["content"] = $this->getComment (false);
141         $out ["datas"] = array ();
142         $dataKindle = $this->GetMostInterestingDataToSendToKindle ();
143         foreach ($this->getDatas() as $data) {
144             if ($data->format=="FB2.ZIP") { $dataformat = "FB2*"; } else { $dataformat = $data->format; }
145             $tab = array ("id" => $data->id, "format" => $dataformat, "url" => $data->getHtmlLink (), "mail" => 0);
146             if (!empty ($config['cops_mail_configuration']) && !is_null ($dataKindle) && $data->id == $dataKindle->id) {
147                 $tab ["mail"] = 1;
148             }
149             array_push ($out ["datas"], $tab);
150         }
151         $out ["authors"] = array ();
152         foreach ($this->getAuthors () as $author) {
153             $link = new LinkNavigation ($author->getUri ());
154             array_push ($out ["authors"], array ("name" => $author->name, "url" => $link->hrefXhtml ()));
155         }
156         $out ["tags"] = array ();
157         foreach ($this->getTags () as $tag) {
158             $link = new LinkNavigation ($tag->getUri ());
159             array_push ($out ["tags"], array ("name" => $tag->name, "url" => $link->hrefXhtml ()));
160         }
161         ;
162         return $out;
163     }
164     
165     public function getDetailUrl ($permalink = false) {
166         global $config;
167         $urlParam = $this->getUri ();
168         return 'index.php' . $urlParam; 
169     }
170     
171     public function getTitle () {
172         return $this->title;
173     }
174     
175     public function getAuthors () {
176         if (is_null ($this->authors)) {
177             $this->authors = Author::getAuthorByBookId ($this->id);
178         }
179         return $this->authors;
180     }
181     
182     public static function getFilterString () {
183         $filter = getURLParam ("tag", NULL);
184         if (empty ($filter)) return "";
185         
186         $exists = true;
187         if (preg_match ("/^!(.*)$/", $filter, $matches)) {
188             $exists = false;
189             $filter = $matches[1];    
190         }
191         
192         $result = "exists (select null from books_tags_link, tags where books_tags_link.book = books.id and books_tags_link.tag = tags.id and tags.name = '" . $filter . "')";
193         
194         if (!$exists) {
195             $result = "not " . $result;
196         }
197     
198         return "and " . $result;
199     }
200     
201     public function getAuthorsName () {
202         return implode (", ", array_map (function ($author) { return $author->name; }, $this->getAuthors ()));
203     }
204     
205     public function getSerie () {
206         if (is_null ($this->serie)) {
207             $this->serie = Serie::getSerieByBookId ($this->id);
208         }
209         return $this->serie;
210     }
211     
212     public function getLanguages () {
213         $lang = array ();
214         $result = parent::getDb ()->prepare('select languages.lang_code
215                 from books_languages_link, languages
216                 where books_languages_link.lang_code = languages.id
217                 and book = ?
218                 order by item_order');
219         $result->execute (array ($this->id));
220         while ($post = $result->fetchObject ())
221         {
222             array_push ($lang, Language::getLanguageString($post->lang_code));
223         }
224         return implode (", ", $lang);
225     }
226     
227     public function getTags () {
228         if (is_null ($this->tags)) {
229             $this->tags = array ();
230             
231             $result = parent::getDb ()->prepare('select tags.id as id, name
232                 from books_tags_link, tags
233                 where tag = tags.id
234                 and book = ?
235                 order by name');
236             $result->execute (array ($this->id));
237             while ($post = $result->fetchObject ())
238             {
239                 array_push ($this->tags, new Tag ($post->id, Tag::getTagString ($post->name)));
240             }
241         }
242         return $this->tags;
243     }
244     
245     public function getDatas ()
246     {
247         if (is_null ($this->datas)) {
248             $this->datas = array ();
249         
250             $result = parent::getDb ()->prepare('select id, format, name
251     from data where book = ?');
252             $result->execute (array ($this->id));
253             
254             while ($post = $result->fetchObject ())
255             {
256                 array_push ($this->datas, new Data ($post, $this));
257             }
258         }
259         return $this->datas;
260     }
261         
262         public function GetMostInterestingDataToSendToKindle ()
263         {
264                 $bestFormatForKindle = array ("EPUB", "PDF", "MOBI");
265                 $bestRank = -1;
266                 $bestData = NULL;
267                 foreach ($this->getDatas () as $data) {
268                         $key = array_search ($data->format, $bestFormatForKindle);
269                         if ($key !== false && $key > $bestRank) {
270                                 $bestRank = $key;
271                                 $bestData = $data;
272                         }
273                 }
274                 return $bestData;
275         }
276     
277     public function getDataById ($idData)
278     {
279         foreach ($this->getDatas () as $data) {
280             if ($data->id == $idData) {
281                 return $data;
282             }
283         }
284         return NULL;
285     }
286
287     
288     public function getTagsName () {
289         return implode (", ", array_map (function ($tag) { return $tag->name; }, $this->getTags ()));
290     }
291     
292     public function getPubDate () {
293         if (is_null ($this->pubdate) || ($this->pubdate <= -58979923200)) {
294             return "";
295         }
296         else {
297             return date ("Y", $this->pubdate);
298         }
299     }
300     
301     public function getComment ($withSerie = true) {
302         $addition = "";
303         $se = $this->getSerie ();
304         if (!is_null ($se) && $withSerie) {
305             $addition = $addition . "<strong>" . localize("content.series") . "</strong>" . str_format (localize ("content.series.data"), $this->seriesIndex, htmlspecialchars ($se->name)) . "<br />\n";
306         }
307         if (preg_match ("/<\/(div|p|a|span)>/", $this->comment))
308         {
309             return $addition . html2xhtml ($this->comment);
310         }
311         else
312         {
313             return $addition . htmlspecialchars ($this->comment);
314         }
315     }
316     
317     public function getDataFormat ($format) {
318         foreach ($this->getDatas () as $data)
319         {
320             if ($data->format == $format)
321             {
322                 return $data;
323             }
324         }
325         return NULL;
326     }
327     
328     public function getFilePath ($extension, $idData = NULL, $relative = false)
329     {
330         $file = NULL;
331         if ($extension == "jpg")
332         {
333             $file = "cover.jpg";
334         }
335         else
336         {
337             $data = $this->getDataById ($idData);
338             if (!$data) return NULL;
339             $file = $data->name . "." . strtolower ($data->format);
340         }
341
342         if ($relative)
343         {
344             return $this->relativePath."/".$file;
345         }
346         else
347         {
348             return $this->path."/".$file;
349         }
350     }
351     
352     public function getUpdatedEpub ($idData)
353     {
354         global $config;
355         $data = $this->getDataById ($idData);
356             
357         try
358         {
359             $epub = new EPub ($data->getLocalPath ());
360             
361             $epub->Title ($this->title);
362             $authorArray = array ();
363             foreach ($this->getAuthors() as $author) {
364                 $authorArray [$author->sort] = $author->name;
365             }
366             $epub->Authors ($authorArray);
367             $epub->Language ($this->getLanguages ());
368             $epub->Description ($this->getComment (false));
369             $epub->Subjects ($this->getTagsName ());
370             $epub->Cover2 ($this->getFilePath ("jpg"), "image/jpeg");
371             $epub->Calibre ($this->uuid);
372             $se = $this->getSerie ();
373             if (!is_null ($se)) {
374                 $epub->Serie ($se->name);
375                 $epub->SerieIndex ($this->seriesIndex);
376             }
377             if ($config['cops_provide_kepub'] == "1"  && preg_match("/Kobo/", $_SERVER['HTTP_USER_AGENT'])) {
378                 $epub->updateForKepub ();
379             }
380             $epub->download ($data->getUpdatedFilenameEpub ());
381         }
382         catch (Exception $e)
383         {
384             echo "Exception : " . $e->getMessage();
385         }
386     }
387     
388     public function getLinkArray ()
389     {
390         global $config;
391         $linkArray = array();
392         
393         if ($this->hasCover)
394         {
395             array_push ($linkArray, Data::getLink ($this, "jpg", "image/jpeg", Link::OPDS_IMAGE_TYPE, "cover.jpg", NULL));
396             
397             array_push ($linkArray, Data::getLink ($this, "jpg", "image/jpeg", Link::OPDS_THUMBNAIL_TYPE, "cover.jpg", NULL));
398         }
399         
400         foreach ($this->getDatas () as $data)
401         {
402             if ($data->isKnownType ())
403             {
404                 array_push ($linkArray, $data->getDataLink (Link::OPDS_ACQUISITION_TYPE, "Download"));
405             }
406         }
407                 
408         foreach ($this->getAuthors () as $author) {
409             array_push ($linkArray, new LinkNavigation ($author->getUri (), "related", str_format (localize ("bookentry.author"), localize ("splitByLetter.book.other"), $author->name)));
410         }
411         
412         $serie = $this->getSerie ();
413         if (!is_null ($serie)) {
414             array_push ($linkArray, new LinkNavigation ($serie->getUri (), "related", str_format (localize ("content.series.data"), $this->seriesIndex, $serie->name)));
415         }
416         
417         return $linkArray;
418     }
419
420     
421     public function getEntry () {    
422         return new EntryBook ($this->getTitle (), $this->getEntryId (), 
423             $this->getComment (), "text/html", 
424             $this->getLinkArray (), $this);
425     }
426     
427     public static function getBookCount() {
428         global $config;
429         $nBooks = parent::getDb ()->query('select count(*) from books')->fetchColumn(0);
430         return $nBooks;
431     }
432
433     public static function getCount() {
434         global $config;
435         $nBooks = parent::getDb ()->query('select count(*) from books')->fetchColumn(0);
436         $result = array();
437         $entry = new Entry (localize ("allbooks.title"), 
438                           self::ALL_BOOKS_ID, 
439                           str_format (localize ("allbooks.alphabetical", $nBooks), $nBooks), "text", 
440                           array ( new LinkNavigation ("?page=".parent::PAGE_ALL_BOOKS)));
441         array_push ($result, $entry);
442         $entry = new Entry (localize ("recent.title"), 
443                           self::ALL_RECENT_BOOKS_ID, 
444                           str_format (localize ("recent.list"), $config['cops_recentbooks_limit']), "text", 
445                           array ( new LinkNavigation ("?page=".parent::PAGE_ALL_RECENT_BOOKS)));
446         array_push ($result, $entry);
447         return $result;
448     }
449         
450     public static function getBooksByAuthor($authorId, $n) {
451         return self::getEntryArray (self::SQL_BOOKS_BY_AUTHOR, array ($authorId), $n);
452     }
453
454     
455     public static function getBooksBySeries($serieId, $n) {
456         return self::getEntryArray (self::SQL_BOOKS_BY_SERIE, array ($serieId), $n);
457     }
458     
459     public static function getBooksByTag($tagId, $n) {
460         return self::getEntryArray (self::SQL_BOOKS_BY_TAG, array ($tagId), $n);
461     }
462     
463     public static function getBooksByLanguage($languageId, $n) {
464         return self::getEntryArray (self::SQL_BOOKS_BY_LANGUAGE, array ($languageId), $n);
465     }
466
467     public static function getBookById($bookId) {
468         $result = parent::getDb ()->prepare('select ' . self::BOOK_COLUMNS . '
469 from books ' . self::SQL_BOOKS_LEFT_JOIN . '
470 where books.id = ?');
471         $result->execute (array($bookId));
472         while ($post = $result->fetchObject ())
473         {
474             $book = new Book ($post);
475             return $book;
476         }
477         return NULL;
478     }
479     
480     public static function getBookByDataId($dataId) {
481         $result = parent::getDb ()->prepare('select ' . self::BOOK_COLUMNS . ', data.name, data.format
482 from data, books ' . self::SQL_BOOKS_LEFT_JOIN . '
483 where data.book = books.id and data.id = ?');
484         $result->execute (array ($dataId));
485         while ($post = $result->fetchObject ())
486         {
487             $book = new Book ($post);
488             $data = new Data ($post, $book);
489             $data->id = $dataId;
490             $book->datas = array ($data);
491             return $book;
492         }
493         return NULL;
494     }
495     
496     public static function getBooksByQuery($query, $n) {
497         return self::getEntryArray (self::SQL_BOOKS_QUERY, array ("%" . $query . "%", "%" . $query . "%"), $n);
498     }
499     
500     public static function getAllBooks($letters) {
501         if (!$letters) { $letters=''; }
502         $len = mb_strlen($letters,'UTF-8')+1;
503         $result = parent::getDb ()->query('select substr(sort, 1, '.$len.') as title, count(distinct sort) sort_cnt,count(*) as count, min(id) min_id, min(sort) min_sort
504 from books 
505 where sort like "'.$letters.'%"
506 group by substr(sort, 1, '.$len.')
507 order by substr(sort, 1, '.$len.')');
508         $entryArray = array();
509         while ($post = $result->fetchObject ())
510         { 
511           if ($post->count == 1) {
512             array_push ($entryArray, new Entry ($post->min_sort, Book::getEntryIdByLetter ($post->min_sort), 
513                 str_format (localize("bookword", $post->count), $post->count), "text", 
514                 array ( new LinkNavigation ("?page=".parent::PAGE_BOOK_DETAIL."&id=". rawurlencode ($post->min_id)))));
515           } elseif ($post->count>parent::booksPages()*parent::maxItemsPerPage() && $post->sort_cnt>1) {
516             array_push ($entryArray, new Entry ($post->title.'...', Book::getEntryIdByLetter ($post->title), 
517                 str_format (localize("bookword", $post->count), $post->count), "text", 
518                 array ( new LinkNavigation ("?page=".parent::PAGE_ALL_BOOKS."&id=". rawurlencode ($post->title)))));
519           } else {
520             array_push ($entryArray, new Entry ($post->title.'...', Book::getEntryIdByLetter ($post->title), 
521                 str_format (localize("bookword", $post->count), $post->count), "text", 
522                 array ( new LinkNavigation ("?page=".parent::PAGE_ALL_BOOKS_LETTER."&id=". rawurlencode ($post->title)))));
523           }      
524         }
525         return $entryArray;
526     }
527     
528     public static function getBooksByStartingLetter($letter, $n) {
529         return self::getEntryArray (self::SQL_BOOKS_BY_FIRST_LETTER, array ($letter . "%"), $n);
530     }
531     
532     public static function getEntryArray ($query, $params, $n) {
533         list ($totalNumber, $result) = parent::executeQuery ($query, self::BOOK_COLUMNS, self::getFilterString (), $params, $n);
534         $entryArray = array();
535         while ($post = $result->fetchObject ())
536         {
537             $book = new Book ($post);
538             array_push ($entryArray, $book->getEntry ());
539         }
540         return array ($entryArray, $totalNumber);
541     }
542
543     
544     public static function getAllRecentBooks() {
545         global $config;
546         list ($entryArray, $totalNumber) = self::getEntryArray (self::SQL_BOOKS_RECENT . $config['cops_recentbooks_limit'], array (), -1);
547         return $entryArray;
548     }
549
550 }