Calibre OPDS (and HTML) PHP Server : web-based light alternative to Calibre content server / Calibre2OPDS to serve ebooks (epub, mobi, pdf, ...) http://blog.slucas.fr/en/oss/calibre-opds-php-server
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

279 lines
13KB

  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. require_once ("base.php");
  9. class OPDSRenderer
  10. {
  11. private $xmlStream = NULL;
  12. private $updated = NULL;
  13. private function getUpdatedTime () {
  14. if (is_null ($this->updated)) {
  15. $this->updated = time();
  16. }
  17. return date (DATE_ATOM, $this->updated);
  18. }
  19. private function getXmlStream () {
  20. if (is_null ($this->xmlStream)) {
  21. $this->xmlStream = new XMLWriter();
  22. $this->xmlStream->openMemory();
  23. $this->xmlStream->setIndent (true);
  24. }
  25. return $this->xmlStream;
  26. }
  27. public function getOpenSearch () {
  28. global $config;
  29. $xml = new XMLWriter ();
  30. $xml->openMemory ();
  31. $xml->setIndent (true);
  32. $xml->startDocument('1.0','UTF-8');
  33. $xml->startElement ("OpenSearchDescription");
  34. $xml->writeAttribute ("xmlns", "http://a9.com/-/spec/opensearch/1.1/");
  35. $xml->startElement ("ShortName");
  36. $xml->text ("My catalog");
  37. $xml->endElement ();
  38. $xml->startElement ("Description");
  39. $xml->text ("Search for ebooks");
  40. $xml->endElement ();
  41. $xml->startElement ("InputEncoding");
  42. $xml->text ("UTF-8");
  43. $xml->endElement ();
  44. $xml->startElement ("OutputEncoding");
  45. $xml->text ("UTF-8");
  46. $xml->endElement ();
  47. $xml->startElement ("Image");
  48. $xml->writeAttribute ("type", "image/x-icon");
  49. $xml->writeAttribute ("width", "16");
  50. $xml->writeAttribute ("height", "16");
  51. $xml->text ($config['cops_icon']);
  52. $xml->endElement ();
  53. $xml->startElement ("Url");
  54. $xml->writeAttribute ("type", 'application/atom+xml');
  55. $urlparam = "?query={searchTerms}";
  56. if (!is_null (GetUrlParam (DB))) $urlparam = addURLParameter ($urlparam, DB, GetUrlParam (DB));
  57. $urlparam = str_replace ("%7B", "{", $urlparam);
  58. $urlparam = str_replace ("%7D", "}", $urlparam);
  59. $xml->writeAttribute ("template", $config['cops_full_url'] . 'feed.php' . $urlparam);
  60. $xml->endElement ();
  61. $xml->startElement ("Query");
  62. $xml->writeAttribute ("role", "example");
  63. $xml->writeAttribute ("searchTerms", "robot");
  64. $xml->endElement ();
  65. $xml->endElement ();
  66. $xml->endDocument();
  67. return $xml->outputMemory(true);
  68. }
  69. private function startXmlDocument ($page) {
  70. global $config;
  71. self::getXmlStream ()->startDocument('1.0','UTF-8');
  72. self::getXmlStream ()->startElement ("feed");
  73. self::getXmlStream ()->writeAttribute ("xmlns", "http://www.w3.org/2005/Atom");
  74. self::getXmlStream ()->writeAttribute ("xmlns:xhtml", "http://www.w3.org/1999/xhtml");
  75. self::getXmlStream ()->writeAttribute ("xmlns:opds", "http://opds-spec.org/2010/catalog");
  76. self::getXmlStream ()->writeAttribute ("xmlns:opensearch", "http://a9.com/-/spec/opensearch/1.1/");
  77. self::getXmlStream ()->writeAttribute ("xmlns:dcterms", "http://purl.org/dc/terms/");
  78. self::getXmlStream ()->startElement ("title");
  79. self::getXmlStream ()->text ($page->title);
  80. self::getXmlStream ()->endElement ();
  81. if ($page->subtitle != "")
  82. {
  83. self::getXmlStream ()->startElement ("subtitle");
  84. self::getXmlStream ()->text ($page->subtitle);
  85. self::getXmlStream ()->endElement ();
  86. }
  87. self::getXmlStream ()->startElement ("id");
  88. if ($page->idPage)
  89. {
  90. $idPage = $page->idPage;
  91. if (!is_null (GetUrlParam (DB))) $idPage = str_replace ("cops:", "cops:" . GetUrlParam (DB) . ":", $idPage);
  92. self::getXmlStream ()->text ($idPage);
  93. }
  94. else
  95. {
  96. self::getXmlStream ()->text ($_SERVER['REQUEST_URI']);
  97. }
  98. self::getXmlStream ()->endElement ();
  99. self::getXmlStream ()->startElement ("updated");
  100. self::getXmlStream ()->text (self::getUpdatedTime ());
  101. self::getXmlStream ()->endElement ();
  102. self::getXmlStream ()->startElement ("icon");
  103. self::getXmlStream ()->text ($page->favicon);
  104. self::getXmlStream ()->endElement ();
  105. self::getXmlStream ()->startElement ("author");
  106. self::getXmlStream ()->startElement ("name");
  107. self::getXmlStream ()->text ($page->authorName);
  108. self::getXmlStream ()->endElement ();
  109. self::getXmlStream ()->startElement ("uri");
  110. self::getXmlStream ()->text ($page->authorUri);
  111. self::getXmlStream ()->endElement ();
  112. self::getXmlStream ()->startElement ("email");
  113. self::getXmlStream ()->text ($page->authorEmail);
  114. self::getXmlStream ()->endElement ();
  115. self::getXmlStream ()->endElement ();
  116. $link = new LinkNavigation ("", "start", "Home");
  117. self::renderLink ($link);
  118. $link = new LinkNavigation ("?" . getQueryString (), "self");
  119. self::renderLink ($link);
  120. $urlparam = "?";
  121. if (!is_null (GetUrlParam (DB))) $urlparam = addURLParameter ($urlparam, DB, GetUrlParam (DB));
  122. if ($config['cops_generate_invalid_opds_stream'] == 0 || preg_match("/(MantanoReader|FBReader)/", $_SERVER['HTTP_USER_AGENT'])) {
  123. // Good and compliant way of handling search
  124. $urlparam = addURLParameter ($urlparam, "page", Base::PAGE_OPENSEARCH);
  125. $link = new Link ("feed.php" . $urlparam, "application/opensearchdescription+xml", "search", "Search here");
  126. }
  127. else
  128. {
  129. // Bad way, will be removed when OPDS client are fixed
  130. $urlparam = addURLParameter ($urlparam, "query", "{searchTerms}");
  131. $urlparam = str_replace ("%7B", "{", $urlparam);
  132. $urlparam = str_replace ("%7D", "}", $urlparam);
  133. $link = new Link ($config['cops_full_url'] . 'feed.php' . $urlparam, "application/atom+xml", "search", "Search here");
  134. }
  135. self::renderLink ($link);
  136. if ($page->containsBook () && !is_null ($config['cops_books_filter']) && count ($config['cops_books_filter']) > 0) {
  137. $Urlfilter = getURLParam ("tag", "");
  138. foreach ($config['cops_books_filter'] as $lib => $filter) {
  139. $link = new LinkFacet ("?" . addURLParameter (getQueryString (), "tag", $filter), $lib, localize ("tagword.title"), $filter == $Urlfilter);
  140. self::renderLink ($link);
  141. }
  142. }
  143. }
  144. private function endXmlDocument () {
  145. self::getXmlStream ()->endElement ();
  146. self::getXmlStream ()->endDocument ();
  147. return self::getXmlStream ()->outputMemory(true);
  148. }
  149. private function renderLink ($link) {
  150. self::getXmlStream ()->startElement ("link");
  151. self::getXmlStream ()->writeAttribute ("href", $link->href);
  152. self::getXmlStream ()->writeAttribute ("type", $link->type);
  153. if (!is_null ($link->rel)) {
  154. self::getXmlStream ()->writeAttribute ("rel", $link->rel);
  155. }
  156. if (!is_null ($link->title)) {
  157. self::getXmlStream ()->writeAttribute ("title", $link->title);
  158. }
  159. if (!is_null ($link->facetGroup)) {
  160. self::getXmlStream ()->writeAttribute ("opds:facetGroup", $link->facetGroup);
  161. }
  162. if ($link->activeFacet) {
  163. self::getXmlStream ()->writeAttribute ("opds:activeFacet", "true");
  164. }
  165. self::getXmlStream ()->endElement ();
  166. }
  167. private function getPublicationDate($book) {
  168. $dateYmd = substr($book->pubdate, 0, 10);
  169. $pubdate = \DateTime::createFromFormat('Y-m-d', $dateYmd);
  170. if ($pubdate === false ||
  171. $pubdate->format ("Y") == "0101" ||
  172. $pubdate->format ("Y") == "0100") {
  173. return "";
  174. }
  175. return $pubdate->format("Y-m-d");
  176. }
  177. private function renderEntry ($entry) {
  178. self::getXmlStream ()->startElement ("title");
  179. self::getXmlStream ()->text ($entry->title);
  180. self::getXmlStream ()->endElement ();
  181. self::getXmlStream ()->startElement ("updated");
  182. self::getXmlStream ()->text (self::getUpdatedTime ());
  183. self::getXmlStream ()->endElement ();
  184. self::getXmlStream ()->startElement ("id");
  185. self::getXmlStream ()->text ($entry->id);
  186. self::getXmlStream ()->endElement ();
  187. self::getXmlStream ()->startElement ("content");
  188. self::getXmlStream ()->writeAttribute ("type", $entry->contentType);
  189. if ($entry->contentType == "text") {
  190. self::getXmlStream ()->text ($entry->content);
  191. } else {
  192. self::getXmlStream ()->writeRaw ($entry->content);
  193. }
  194. self::getXmlStream ()->endElement ();
  195. foreach ($entry->linkArray as $link) {
  196. self::renderLink ($link);
  197. }
  198. if (get_class ($entry) != "EntryBook") {
  199. return;
  200. }
  201. foreach ($entry->book->getAuthors () as $author) {
  202. self::getXmlStream ()->startElement ("author");
  203. self::getXmlStream ()->startElement ("name");
  204. self::getXmlStream ()->text ($author->name);
  205. self::getXmlStream ()->endElement ();
  206. self::getXmlStream ()->startElement ("uri");
  207. self::getXmlStream ()->text ("feed.php" . $author->getUri ());
  208. self::getXmlStream ()->endElement ();
  209. self::getXmlStream ()->endElement ();
  210. }
  211. foreach ($entry->book->getTags () as $category) {
  212. self::getXmlStream ()->startElement ("category");
  213. self::getXmlStream ()->writeAttribute ("term", $category->name);
  214. self::getXmlStream ()->writeAttribute ("label", $category->name);
  215. self::getXmlStream ()->endElement ();
  216. }
  217. if ($entry->book->getPubDate () != "") {
  218. self::getXmlStream ()->startElement ("dcterms:issued");
  219. self::getXmlStream ()->text (self::getPublicationDate($entry->book));
  220. self::getXmlStream ()->endElement ();
  221. self::getXmlStream ()->startElement ("published");
  222. self::getXmlStream ()->text (self::getPublicationDate($entry->book) . "T08:08:08Z");
  223. self::getXmlStream ()->endElement ();
  224. }
  225. $lang = $entry->book->getLanguages ();
  226. if (!empty ($lang)) {
  227. self::getXmlStream ()->startElement ("dcterms:language");
  228. self::getXmlStream ()->text ($lang);
  229. self::getXmlStream ()->endElement ();
  230. }
  231. }
  232. public function render ($page) {
  233. global $config;
  234. self::startXmlDocument ($page);
  235. if ($page->isPaginated ())
  236. {
  237. self::getXmlStream ()->startElement ("opensearch:totalResults");
  238. self::getXmlStream ()->text ($page->totalNumber);
  239. self::getXmlStream ()->endElement ();
  240. self::getXmlStream ()->startElement ("opensearch:itemsPerPage");
  241. self::getXmlStream ()->text ($config['cops_max_item_per_page']);
  242. self::getXmlStream ()->endElement ();
  243. self::getXmlStream ()->startElement ("opensearch:startIndex");
  244. self::getXmlStream ()->text (($page->n - 1) * $config['cops_max_item_per_page'] + 1);
  245. self::getXmlStream ()->endElement ();
  246. $prevLink = $page->getPrevLink ();
  247. $nextLink = $page->getNextLink ();
  248. if (!is_null ($prevLink)) {
  249. self::renderLink ($prevLink);
  250. }
  251. if (!is_null ($nextLink)) {
  252. self::renderLink ($nextLink);
  253. }
  254. }
  255. foreach ($page->entryArray as $entry) {
  256. self::getXmlStream ()->startElement ("entry");
  257. self::renderEntry ($entry);
  258. self::getXmlStream ()->endElement ();
  259. }
  260. return self::endXmlDocument ();
  261. }
  262. }