3  * COPS (Calibre OPDS PHP Server) class file
 
   5  * @license    GPL 2 (http://www.gnu.org/licenses/gpl.html)
 
   6  * @author     Sébastien Lucas <sebastien@slucas.fr>
 
   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');
 
  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 ");
 
  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";
 
  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;
 
  60     public $authors = NULL;
 
  63     public $languages = NULL;
 
  64     public $format = array ();
 
  67     public function __construct($line) {
 
  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"))) {
 
  85     public function getEntryId () {
 
  86         return self::ALL_BOOKS_UUID.":".$this->id;
 
  89     public static function getEntryIdByLetter ($startingLetter) {
 
  90         return self::ALL_BOOKS_ID.":letter:".$startingLetter;
 
  93     public function getUri () {
 
  94         return "?page=".parent::PAGE_BOOK_DETAIL."&id=$this->id";
 
  97     public function getContentArray () {
 
 100         $preferedData = array ();
 
 101         foreach ($config['cops_prefered_format'] as $format)
 
 103             if ($i == 2) { break; }
 
 104             if ($data = $this->getDataFormat ($format)) {
 
 106                 array_push ($preferedData, array ("url" => $data->getHtmlLink (), "name" => $format));
 
 109         $serie = $this->getSerie ();
 
 110         if (is_null ($serie)) {
 
 116             $scn = str_format (localize ("content.series.data"), $this->seriesIndex, $serie->name);
 
 117             $link = new LinkNavigation ($serie->getUri ());
 
 118             $su = $link->hrefXhtml ();
 
 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 (),
 
 129                       "seriesIndex" => $this->seriesIndex,
 
 130                       "seriesCompleteName" => $scn,
 
 134     public function getFullContentArray () {
 
 136         $out = $this->getContentArray ();
 
 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) {
 
 149             array_push ($out ["datas"], $tab);
 
 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 ()));
 
 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 ()));
 
 165     public function getDetailUrl ($permalink = false) {
 
 167         $urlParam = $this->getUri ();
 
 168         return 'index.php' . $urlParam; 
 
 171     public function getTitle () {
 
 175     public function getAuthors () {
 
 176         if (is_null ($this->authors)) {
 
 177             $this->authors = Author::getAuthorByBookId ($this->id);
 
 179         return $this->authors;
 
 182     public static function getFilterString () {
 
 183         $filter = getURLParam ("tag", NULL);
 
 184         if (empty ($filter)) return "";
 
 187         if (preg_match ("/^!(.*)$/", $filter, $matches)) {
 
 189             $filter = $matches[1];    
 
 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 . "')";
 
 195             $result = "not " . $result;
 
 198         return "and " . $result;
 
 201     public function getAuthorsName () {
 
 202         return implode (", ", array_map (function ($author) { return $author->name; }, $this->getAuthors ()));
 
 205     public function getSerie () {
 
 206         if (is_null ($this->serie)) {
 
 207             $this->serie = Serie::getSerieByBookId ($this->id);
 
 212     public function getLanguages () {
 
 214         $result = parent::getDb ()->prepare('select languages.lang_code
 
 215                 from books_languages_link, languages
 
 216                 where books_languages_link.lang_code = languages.id
 
 218                 order by item_order');
 
 219         $result->execute (array ($this->id));
 
 220         while ($post = $result->fetchObject ())
 
 222             array_push ($lang, Language::getLanguageString($post->lang_code));
 
 224         return implode (", ", $lang);
 
 227     public function getTags () {
 
 228         if (is_null ($this->tags)) {
 
 229             $this->tags = array ();
 
 231             $result = parent::getDb ()->prepare('select tags.id as id, name
 
 232                 from books_tags_link, tags
 
 236             $result->execute (array ($this->id));
 
 237             while ($post = $result->fetchObject ())
 
 239                 array_push ($this->tags, new Tag ($post->id, Tag::getTagString ($post->name)));
 
 245     public function getDatas ()
 
 247         if (is_null ($this->datas)) {
 
 248             $this->datas = array ();
 
 250             $result = parent::getDb ()->prepare('select id, format, name
 
 251     from data where book = ?');
 
 252             $result->execute (array ($this->id));
 
 254             while ($post = $result->fetchObject ())
 
 256                 array_push ($this->datas, new Data ($post, $this));
 
 262         public function GetMostInterestingDataToSendToKindle ()
 
 264                 $bestFormatForKindle = array ("EPUB", "PDF", "MOBI");
 
 267                 foreach ($this->getDatas () as $data) {
 
 268                         $key = array_search ($data->format, $bestFormatForKindle);
 
 269                         if ($key !== false && $key > $bestRank) {
 
 277     public function getDataById ($idData)
 
 279         foreach ($this->getDatas () as $data) {
 
 280             if ($data->id == $idData) {
 
 288     public function getTagsName () {
 
 289         return implode (", ", array_map (function ($tag) { return $tag->name; }, $this->getTags ()));
 
 292     public function getPubDate () {
 
 293         if (is_null ($this->pubdate) || ($this->pubdate <= -58979923200)) {
 
 297             return date ("Y", $this->pubdate);
 
 301     public function getComment ($withSerie = true) {
 
 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";
 
 307         if (preg_match ("/<\/(div|p|a|span)>/", $this->comment))
 
 309             return $addition . html2xhtml ($this->comment);
 
 313             return $addition . htmlspecialchars ($this->comment);
 
 317     public function getDataFormat ($format) {
 
 318         foreach ($this->getDatas () as $data)
 
 320             if ($data->format == $format)
 
 328     public function getFilePath ($extension, $idData = NULL, $relative = false)
 
 331         if ($extension == "jpg")
 
 337             $data = $this->getDataById ($idData);
 
 338             if (!$data) return NULL;
 
 339             $file = $data->name . "." . strtolower ($data->format);
 
 344             return $this->relativePath."/".$file;
 
 348             return $this->path."/".$file;
 
 352     public function getUpdatedEpub ($idData)
 
 355         $data = $this->getDataById ($idData);
 
 359             $epub = new EPub ($data->getLocalPath ());
 
 361             $epub->Title ($this->title);
 
 362             $authorArray = array ();
 
 363             foreach ($this->getAuthors() as $author) {
 
 364                 $authorArray [$author->sort] = $author->name;
 
 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);
 
 377             if ($config['cops_provide_kepub'] == "1"  && preg_match("/Kobo/", $_SERVER['HTTP_USER_AGENT'])) {
 
 378                 $epub->updateForKepub ();
 
 380             $epub->download ($data->getUpdatedFilenameEpub ());
 
 384             echo "Exception : " . $e->getMessage();
 
 388     public function getLinkArray ()
 
 391         $linkArray = array();
 
 395             array_push ($linkArray, Data::getLink ($this, "jpg", "image/jpeg", Link::OPDS_IMAGE_TYPE, "cover.jpg", NULL));
 
 397             array_push ($linkArray, Data::getLink ($this, "jpg", "image/jpeg", Link::OPDS_THUMBNAIL_TYPE, "cover.jpg", NULL));
 
 400         foreach ($this->getDatas () as $data)
 
 402             if ($data->isKnownType ())
 
 404                 array_push ($linkArray, $data->getDataLink (Link::OPDS_ACQUISITION_TYPE, "Download"));
 
 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)));
 
 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)));
 
 421     public function getEntry () {    
 
 422         return new EntryBook ($this->getTitle (), $this->getEntryId (), 
 
 423             $this->getComment (), "text/html", 
 
 424             $this->getLinkArray (), $this);
 
 427     public static function getBookCount() {
 
 429         $nBooks = parent::getDb ()->query('select count(*) from books')->fetchColumn(0);
 
 433     public static function getCount() {
 
 435         $nBooks = parent::getDb ()->query('select count(*) from books')->fetchColumn(0);
 
 437         $entry = new Entry (localize ("allbooks.title"), 
 
 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);
 
 450     public static function getBooksByAuthor($authorId, $n) {
 
 451         return self::getEntryArray (self::SQL_BOOKS_BY_AUTHOR, array ($authorId), $n);
 
 455     public static function getBooksBySeries($serieId, $n) {
 
 456         return self::getEntryArray (self::SQL_BOOKS_BY_SERIE, array ($serieId), $n);
 
 459     public static function getBooksByTag($tagId, $n) {
 
 460         return self::getEntryArray (self::SQL_BOOKS_BY_TAG, array ($tagId), $n);
 
 463     public static function getBooksByLanguage($languageId, $n) {
 
 464         return self::getEntryArray (self::SQL_BOOKS_BY_LANGUAGE, array ($languageId), $n);
 
 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 ())
 
 474             $book = new Book ($post);
 
 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 ())
 
 487             $book = new Book ($post);
 
 488             $data = new Data ($post, $book);
 
 490             $book->datas = array ($data);
 
 496     public static function getBooksByQuery($query, $n) {
 
 497         return self::getEntryArray (self::SQL_BOOKS_QUERY, array ("%" . $query . "%", "%" . $query . "%"), $n);
 
 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
 
 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 ())
 
 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)))));
 
 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)))));
 
 528     public static function getBooksByStartingLetter($letter, $n) {
 
 529         return self::getEntryArray (self::SQL_BOOKS_BY_FIRST_LETTER, array ($letter . "%"), $n);
 
 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 ())
 
 537             $book = new Book ($post);
 
 538             array_push ($entryArray, $book->getEntry ());
 
 540         return array ($entryArray, $totalNumber);
 
 544     public static function getAllRecentBooks() {
 
 546         list ($entryArray, $totalNumber) = self::getEntryArray (self::SQL_BOOKS_RECENT . $config['cops_recentbooks_limit'], array (), -1);