diff --git a/CHANGELOG b/CHANGELOG new file mode 100644 index 0000000..0c416b3 --- /dev/null +++ b/CHANGELOG @@ -0,0 +1,2 @@ +0.0.1 - 20120302 + * First public release diff --git a/COPYING b/COPYING new file mode 100644 index 0000000..d60c31a --- /dev/null +++ b/COPYING @@ -0,0 +1,340 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc. + 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Library General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program; if not, write to the Free Software + Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Library General +Public License instead of this License. diff --git a/README b/README new file mode 100644 index 0000000..bc0ebbd --- /dev/null +++ b/README @@ -0,0 +1,96 @@ += COPS = + +COPS stands for Calibre OPDS Php Server. + +COPS output is valid the unofficial OPDS validator : +http://opds-validator.appspot.com/ + += Why ? = + +In my opinion Calibre is a marvelous tool but is too big and has too much +dependancies to be used for its content server. + +That's the main reason why I coded this OPDS server. I needed a simple +tool to be installed on a small server (Seagate Dockstar in my case). + +I initially thought of Calibre2OPDS but as it generate static file no +search was possible. + +So COPS's main advantages are : + * No need for many dependancies. + * No need for a lot of CPU or RAM. + * Not much code. + * Search is available. + * With Dropbox it's very easy to have an up to date OPDS server. + * It was fun to code. + += Prerequisites = + +1. PHP 5.3 with GD image processing & SQLite3 support. +2. A web server with PHP support. I only tested with Nginx 1.1.14. For now + there is support for X-Accel-Redirect which is Nginx specific. +3. The path to a calibre library (metadata.db, format, & cover files). + +On any Debian base Linux you can use : + aptitude install php5-gd php5-sqlite + += Install = + +1. Extract the zip file to a folder in web space (visible to the web server). +2. If a first-time install, copy config_default.php to config_local.php +3. Edit config_local.php to match your config. + +In my case I installed COPS in a subdomain. Here is my nginx config file : +server { + + listen [::]:80; + + server_name opds.mydomain.com; + + access_log /var/log/nginx/opds.access.log; + error_log /var/log/nginx/opds.error.log; + root /var/www/opds; + index feed.php; + + location ~ \.php$ { + include /etc/nginx/fastcgi_params; + fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; + fastcgi_pass unix:/tmp/fcgi.sock; + } + + location /Calibre { + root /home/calibre/Dropbox; + internal; + } +} + +Beware in this case my Calibre database is in /home/calibre/Dropbox/Calibre/ so +the internal location of nginx has to split like that. + +If your Calibre database is inside your web directory then there is no need for +an internal location. + +If you choose to put your Calibre directory inside your web directory then you +will have to edit /etc/nginx/mime.types to add this line : +application/epub+zip epub; + += Known problems = + + * Only tested with Nginx. + * Contain Nginx specific code. + * Only works with EPUB (not MOBI or PDF). + * certainly many many more. + += Disclaimer = + +I only tested on Debian with Nginx so I have stricly no idea if it works +with Apache or any other web server. + +On the OPDS client side I mainly tested with FBReader and Aldiko on Android. + += Copyright & License = + +COPS - 2012 (c) Sébastien Lucas + +See COPYING and file headers for license info + diff --git a/author.php b/author.php new file mode 100644 index 0000000..686a03b --- /dev/null +++ b/author.php @@ -0,0 +1,73 @@ + + */ + +require_once('base.php'); + +class Author extends Base { + const ALL_AUTHORS_ID = "calibre:authors"; + + public $id; + public $name; + public $sort; + + public function __construct($pid, $pname) { + $this->id = $pid; + $this->name = $pname; + } + + public function getUri () { + return "feed.php?page=".parent::PAGE_AUTHOR_DETAIL."&id=$this->id"; + } + + public function getEntryId () { + return self::ALL_AUTHORS_ID.":".$this->id; + } + + + public static function getCount() { + $nAuthors = parent::getDb ()->query('select count(*) from authors')->fetchColumn(); + parent::addEntryClass ( new Entry ("Authors", self::ALL_AUTHORS_ID, + "Alphabetical index of the $nAuthors authors", "text", + array ( new LinkNavigation ("feed.php?page=".parent::PAGE_ALL_AUTHORS)))); + } + + public static function getAllAuthors() { + $result = parent::getDb ()->query('select authors.id as id, authors.name as name, authors.sort as sort, count(*) as count +from authors, books_authors_link +where author = authors.id +group by authors.id, authors.name, authors.sort +order by sort'); + while ($post = $result->fetchObject ()) + { + $author = new Author ($post->id, $post->sort); + parent::addEntryClass ( new Entry ($post->sort, $author->getEntryId (), + "$post->count books", "text", + array ( new LinkNavigation ($author->getUri ())))); + } + } + + public static function getAuthorName ($authorId) { + $result = parent::getDb ()->prepare('select sort from authors where id = ?'); + $result->execute (array ($authorId)); + return $result->fetchColumn (); + } + + public static function getAuthorByBookId ($bookId) { + $result = parent::getDb ()->prepare('select authors.id as id, authors.sort as sort +from authors, books_authors_link +where author = authors.id +and book = ?'); + $result->execute (array ($bookId)); + $authorArray = array (); + while ($post = $result->fetchObject ()) { + array_push ($authorArray, new Author ($post->id, $post->sort)); + } + return $authorArray; + } +} +?> \ No newline at end of file diff --git a/base.php b/base.php new file mode 100644 index 0000000..58e82ad --- /dev/null +++ b/base.php @@ -0,0 +1,277 @@ + + */ + + +class Link +{ + public $href; + public $type; + public $rel; + public $title; + + public function __construct($phref, $ptype, $prel = NULL, $ptitle = NULL) { + $this->href = $phref; + $this->type = $ptype; + $this->rel = $prel; + $this->title = $ptitle; + } + + public function render ($xml) { + $xml->startElement ("link"); + $xml->writeAttribute ("href", $this->href); + $xml->writeAttribute ("type", $this->type); + if (!is_null ($this->rel)) { + $xml->writeAttribute ("rel", $this->rel); + } + if (!is_null ($this->title)) { + $xml->writeAttribute ("title", $this->title); + } + $xml->endElement (); + } +} + +class LinkNavigation extends Link +{ + const OPDS_NAVIGATION_TYPE = "application/atom+xml;profile=opds-catalog;kind=navigation"; + + public function __construct($phref, $prel = NULL, $ptitle = NULL) { + parent::__construct ($phref, self::OPDS_NAVIGATION_TYPE, $prel, $ptitle); + } +} + + +class Entry +{ + public $title; + public $id; + public $content; + public $contentType; + public $linkArray; + public $localUpdated; + private static $updated = NULL; + + public function getUpdatedTime () { + if (!is_null ($this->localUpdated)) { + return date (DATE_ATOM, $this->localUpdated); + } + if (is_null (self::$updated)) { + self::$updated = time(); + } + return date (DATE_ATOM, self::$updated); + } + + public function __construct($ptitle, $pid, $pcontent, $pcontentType, $plinkArray) { + $this->title = $ptitle; + $this->id = $pid; + $this->content = $pcontent; + $this->contentType = $pcontentType; + $this->linkArray = $plinkArray; + } + + public function renderContent ($xml) { + $xml->startElement ("title"); + $xml->text ($this->title); + $xml->endElement (); + $xml->startElement ("updated"); + $xml->text (self::getUpdatedTime ()); + $xml->endElement (); + $xml->startElement ("id"); + $xml->text ($this->id); + $xml->endElement (); + $xml->startElement ("content"); + $xml->writeAttribute ("type", $this->contentType); + if ($this->contentType == "text") { + $xml->text ($this->content); + } else { + $xml->writeRaw ($this->content); + } + $xml->endElement (); + foreach ($this->linkArray as $link) { + $link->render ($xml); + } + } + + public function render ($xml) { + $xml->startElement ("entry"); + self::renderContent ($xml); + $xml->endElement (); + } + +} + +class EntryBook extends Entry +{ + public $book; + + public function __construct($ptitle, $pid, $pcontent, $pcontentType, $plinkArray, $pbook) { + parent::__construct ($ptitle, $pid, $pcontent, $pcontentType, $plinkArray); + $this->book = $pbook; + $this->localUpdated = $pbook->timestamp; + } + + public function renderContent ($xml) { + parent::renderContent ($xml); + foreach ($this->book->getAuthors () as $author) { + $xml->startElement ("author"); + $xml->startElement ("name"); + $xml->text ($author->name); + $xml->endElement (); + $xml->startElement ("uri"); + $xml->text ($author->getUri ()); + $xml->endElement (); + $xml->endElement (); + } + foreach ($this->book->getTags () as $category) { + $xml->startElement ("category"); + $xml->writeAttribute ("term", $category); + $xml->writeAttribute ("label", $category); + $xml->endElement (); + } + if (!is_null ($this->book->pubdate)) { + $xml->startElement ("dcterms:issued"); + $xml->text (date ("Y-m-d", $this->book->pubdate)); + $xml->endElement (); + } + } + + /* Polymorphism is strange with PHP */ + public function render ($xml) { + $xml->startElement ("entry"); + self::renderContent ($xml); + $xml->endElement (); + } + +} + +abstract class Base +{ + const PAGE_INDEX = "index"; + const PAGE_ALL_AUTHORS = "1"; + const PAGE_AUTHORS_FIRST_LETTER = "2"; + const PAGE_AUTHOR_DETAIL = "3"; + const PAGE_ALL_BOOKS = "4"; + const PAGE_ALL_BOOKS_LETTER = "5"; + const PAGE_ALL_SERIES = "6"; + const PAGE_SERIE_DETAIL = "7"; + const PAGE_OPENSEARCH = "8"; + const PAGE_OPENSEARCH_QUERY = "9"; + const PAGE_ALL_RECENT_BOOKS = "10"; + const COMPATIBILITY_XML_ALDIKO = "aldiko"; + + private static $db = NULL; + private static $xmlStream = NULL; + private static $updated = NULL; + + public static function getUpdatedTime () { + if (is_null (self::$updated)) { + self::$updated = time(); + } + return date (DATE_ATOM, self::$updated); + } + + public static function getDb () { + global $config; + if (is_null (self::$db)) { + try { + self::$db = new PDO('sqlite:'. $config['calibre_directory'] .'metadata.db'); + } catch (Exception $e) { + echo $e; + die($e); + } + } + return self::$db; + } + + public static function getXmlStream () { + if (is_null (self::$xmlStream)) { + self::$xmlStream = new XMLWriter(); + self::$xmlStream->openMemory(); + self::$xmlStream->setIndent (true); + } + return self::$xmlStream; + } + + public static function getOpenSearch () { + $xml = new XMLWriter (); + $xml->openMemory (); + $xml->setIndent (true); + $xml->startDocument('1.0','UTF-8'); + $xml->startElement ("OpenSearchDescription"); + $xml->startElement ("ShortName"); + $xml->text ("My catalog"); + $xml->endElement (); + $xml->startElement ("InputEncoding"); + $xml->text ("UTF-8"); + $xml->endElement (); + $xml->startElement ("OutputEncoding"); + $xml->text ("UTF-8"); + $xml->endElement (); + $xml->startElement ("Image"); + $xml->text ("favicon.ico"); + $xml->endElement (); + $xml->startElement ("Url"); + $xml->writeAttribute ("type", 'application/atom+xml'); + $xml->writeAttribute ("template", 'feed.php?page=' . self::PAGE_OPENSEARCH_QUERY . '&query={searchTerms}'); + $xml->endElement (); + $xml->endElement (); + $xml->endDocument(); + return $xml->outputMemory(true); + } + + public static function startXmlDocument ($title) { + self::getXmlStream ()->startDocument('1.0','UTF-8'); + self::getXmlStream ()->startElement ("feed"); + self::getXmlStream ()->writeAttribute ("xmlns", "http://www.w3.org/2005/Atom"); + self::getXmlStream ()->writeAttribute ("xmlns:xhtml", "http://www.w3.org/1999/xhtml"); + self::getXmlStream ()->writeAttribute ("xmlns:opds", "http://opds-spec.org/2010/catalog"); + self::getXmlStream ()->writeAttribute ("xmlns:opensearch", "http://a9.com/-/spec/opensearch/1.1/"); + self::getXmlStream ()->writeAttribute ("xmlns:dcterms", "http://purl.org/dc/terms/"); + self::getXmlStream ()->startElement ("title"); + self::getXmlStream ()->text ($title); + self::getXmlStream ()->endElement (); + self::getXmlStream ()->startElement ("id"); + self::getXmlStream ()->text ($_SERVER['REQUEST_URI']); + self::getXmlStream ()->endElement (); + self::getXmlStream ()->startElement ("updated"); + self::getXmlStream ()->text (self::getUpdatedTime ()); + self::getXmlStream ()->endElement (); + self::getXmlStream ()->startElement ("icon"); + self::getXmlStream ()->text ("favicon.ico"); + self::getXmlStream ()->endElement (); + self::getXmlStream ()->startElement ("author"); + self::getXmlStream ()->startElement ("name"); + self::getXmlStream ()->text (utf8_encode ("Sébastien Lucas")); + self::getXmlStream ()->endElement (); + self::getXmlStream ()->startElement ("uri"); + self::getXmlStream ()->text ("http://blog.slucas.fr"); + self::getXmlStream ()->endElement (); + self::getXmlStream ()->startElement ("email"); + self::getXmlStream ()->text ("sebastien@slucas.fr"); + self::getXmlStream ()->endElement (); + self::getXmlStream ()->endElement (); + $link = new LinkNavigation ("feed.php", "start", "Home"); + $link->render (self::getXmlStream ()); + $link = new LinkNavigation ($_SERVER['REQUEST_URI'], "self"); + $link->render (self::getXmlStream ()); + $link = new Link ("feed.php?page=" . self::PAGE_OPENSEARCH, "application/opensearchdescription+xml", "search", "Search here"); + $link->render (self::getXmlStream ()); + $link = new LinkNavigation ("feed.php?page=7&id=9", "http://opds-spec.org/shelf", "Biblio"); + $link->render (self::getXmlStream ()); + } + + public static function addEntryClass ($entry) { + $entry->render (self::getXmlStream ()); + } + + public static function endXmlDocument () { + self::getXmlStream ()->endElement (); + self::getXmlStream ()->endDocument (); + return self::getXmlStream ()->outputMemory(true); + } +} +?> \ No newline at end of file diff --git a/book.php b/book.php new file mode 100644 index 0000000..adb52d7 --- /dev/null +++ b/book.php @@ -0,0 +1,263 @@ + + */ + +require_once('base.php'); + +class Book extends Base { + const ALL_BOOKS_ID = "calibre:books"; + const ALL_RECENT_BOOKS_ID = "calibre:recentbooks"; + + public $id; + public $title; + public $timestamp; + public $pubdate; + public $path; + public $relativePath; + public $seriesIndex; + public $comment; + public $authors = NULL; + public $serie = NULL; + public $tags = NULL; + + public function __construct($pid, $ptitle, $ptimestamp, $ppubdate, $ppath, $pseriesIndex, $pcomment) { + global $config; + $this->id = $pid; + $this->title = $ptitle; + $this->timestamp = strtotime ($ptimestamp); + $this->pubdate = strtotime ($ppubdate); + $this->path = $config['calibre_directory'] . $ppath; + $this->relativePath = $ppath; + $this->seriesIndex = $pseriesIndex; + $this->comment = $pcomment; + } + + public function getEntryId () { + return self::ALL_BOOKS_ID.":".$this->id; + } + + public function getTitle () { + return $this->title; + } + + public function getAuthors () { + if (is_null ($this->authors)) { + $this->authors = Author::getAuthorByBookId ($this->id); + } + return $this->authors; + } + + public function getSerie () { + if (is_null ($this->serie)) { + $this->serie = Serie::getSerieByBookId ($this->id); + } + return $this->serie; + } + + public function getTags () { + if (is_null ($this->tags)) { + $this->tags = array (); + + $result = parent::getDb ()->prepare('select name + from books_tags_link, tags + where tag = tags.id + and book = ? + order by name'); + $result->execute (array ($this->id)); + while ($post = $result->fetchObject ()) + { + array_push ($this->tags, $post->name); + } + } + return $this->tags; + } + + public function getComment () { + $addition = ""; + $se = $this->getSerie (); + if (!is_null ($se)) { + $addition = $addition . "Series : Book $this->seriesIndex in $se->name
\n"; + } + return $addition . strip_tags ($this->comment, '
'); + } + + public function getFilePath ($extension, $relative = false) + { + if ($handle = opendir($this->path)) { + while (false !== ($file = readdir($handle))) { + if (preg_match ('/' . $extension . '$/', $file)) { + if ($relative) + { + return $this->relativePath."/".$file; + } + else + { + return $this->path."/".$file; + } + } + } + } + return NULL; + } + + public function getLinkArray () + { + global $config; + $linkArray = array(); + if ($handle = opendir($this->path)) { + while (false !== ($file = readdir($handle))) { + if (preg_match ('/jpg$/', $file)) { + if (preg_match ('/^\//', $config['calibre_directory'])) + { + array_push ($linkArray, new Link ("fetch.php?id=$this->id", "image/jpeg", "http://opds-spec.org/image")); + } + else + { + array_push ($linkArray, new Link (rawurlencode ($this->path."/".$file), "image/jpeg", "http://opds-spec.org/image")); + } + array_push ($linkArray, new Link ("fetch.php?id=$this->id&width=50", "image/jpeg", "http://opds-spec.org/image/thumbnail")); + } + if (preg_match ('/epub$/', $file)) { + if (preg_match ('/^\//', $config['calibre_directory'])) + { + array_push ($linkArray, new Link ("fetch.php?id=$this->id&type=epub", "application/epub+zip", "http://opds-spec.org/acquisition", "Download")); + } + else + { + array_push ($linkArray, new Link (rawurlencode ($this->path."/".$file), "application/epub+zip", "http://opds-spec.org/acquisition", "Download")); + } + } + } + } + + foreach ($this->getAuthors () as $author) { + array_push ($linkArray, new LinkNavigation ($author->getUri (), "related", "Other books by $author->name")); + } + + $serie = $this->getSerie (); + if (!is_null ($serie)) { + array_push ($linkArray, new LinkNavigation ($serie->getUri (), "related", "Other books by the serie $serie->name")); + } + + return $linkArray; + } + + + public function getEntry () { + parent::addEntryClass (new EntryBook ($this->getTitle (), $this->getEntryId (), + $this->getComment (), "text/html", + $this->getLinkArray (), $this)); + } + + public static function getCount() { + $nBooks = parent::getDb ()->query('select count(*) from books')->fetchColumn(); + parent::addEntryClass (new Entry ("Books", + self::ALL_BOOKS_ID, + "Alphabetical index of the $nBooks books", "text", + array ( new LinkNavigation ("feed.php?page=".parent::PAGE_ALL_BOOKS)))); + parent::addEntryClass (new Entry ("Recents books", + self::ALL_RECENT_BOOKS_ID, + "Alphabetical index of the 50 most recent books", "text", + array ( new LinkNavigation ("feed.php?page=".parent::PAGE_ALL_RECENT_BOOKS)))); + } + + public static function getBooksByAuthor($authorId) { + $result = parent::getDb ()->prepare('select books.id as id, books.title as title, text as comment, path, timestamp, pubdate, series_index +from books_authors_link, books left outer join comments on comments.book = books.id +where books_authors_link.book = books.id +and author = ? +order by pubdate'); + $result->execute (array ($authorId)); + while ($post = $result->fetchObject ()) + { + $book = new Book ($post->id, $post->title, $post->timestamp, $post->pubdate, $post->path, $post->series_index, $post->comment); + $book->getEntry (); + } + } + + + public static function getBooksBySeries($serieId) { + $result = parent::getDb ()->prepare('select books.id as id, books.title as title, text as comment, path, timestamp, pubdate, series_index +from books_series_link, books left outer join comments on comments.book = books.id +where books_series_link.book = books.id and series = ? +order by series_index'); + $result->execute (array ($serieId)); + while ($post = $result->fetchObject ()) + { + $book = new Book ($post->id, $post->title, $post->timestamp, $post->pubdate, $post->path, $post->series_index, $post->comment); + $book->getEntry (); + } + } + + public static function getBookById($bookId) { + $result = parent::getDb ()->prepare('select books.id as id, books.title as title, text as comment, path, timestamp, pubdate, series_index +from books left outer join comments on book = books.id +where books.id = ?'); + $result->execute (array ($bookId)); + while ($post = $result->fetchObject ()) + { + $book = new Book ($post->id, $post->title, $post->timestamp, $post->pubdate, $post->path, $post->series_index, $post->comment); + return $book; + } + return NULL; + } + + public static function getBooksByQuery($query) { + $result = parent::getDb ()->prepare("select books.id as id, books.title as title, text as comment, path, timestamp, pubdate, series_index +from books left outer join comments on book = books.id +where exists (select null from authors, books_authors_link where book = books.id and author = authors.id and authors.name like ?) +or title like ?"); + $queryLike = "%" . $query . "%"; + $result->execute (array ($queryLike, $queryLike)); + while ($post = $result->fetchObject ()) + { + $book = new Book ($post->id, $post->title, $post->timestamp, $post->pubdate, $post->path, $post->series_index, $post->comment); + $book->getEntry (); + } + } + + public static function getAllBooks() { + $result = parent::getDb ()->query("select substr (upper (sort), 1, 1) as title, count(*) as count +from books +group by substr (upper (sort), 1, 1) +order by substr (upper (sort), 1, 1)"); + while ($post = $result->fetchObject ()) + { + parent::addEntryClass (new Entry ($post->title, "allbooks_" . $post->title, + "$post->count books", "text", + array ( new LinkNavigation ("feed.php?page=".parent::PAGE_ALL_BOOKS_LETTER."&id=".$post->title)))); + } + } + + public static function getBooksByStartingLetter($letter) { + $result = parent::getDb ()->prepare('select books.id as id, books.title as title, text as comment, path, timestamp, pubdate, series_index +from books left outer join comments on book = books.id +where upper (books.sort) like ?'); + $queryLike = $letter . "%"; + $result->execute (array ($queryLike)); + while ($post = $result->fetchObject ()) + { + $book = new Book ($post->id, $post->title, $post->timestamp, $post->pubdate, $post->path, $post->series_index, $post->comment); + $book->getEntry (); + } + } + + + public static function getAllRecentBooks() { + global $config; + $result = parent::getDb ()->query("select books.id as id, books.title as title, text as comment, path, timestamp, pubdate, series_index +from books left outer join comments on book = books.id +order by timestamp desc limit " . $config['cops_recentbooks_limit']); + while ($post = $result->fetchObject ()) + { + $book = new Book ($post->id, $post->title, $post->timestamp, $post->pubdate, $post->path, $post->series_index, $post->comment); + $book->getEntry (); + } + } + +} +?> \ No newline at end of file diff --git a/config.php b/config.php new file mode 100644 index 0000000..0519436 --- /dev/null +++ b/config.php @@ -0,0 +1,12 @@ + + */ + + require_once 'config_default.php'; + if (file_exists('config_local.php')) + require_once 'config_local.php'; +?> \ No newline at end of file diff --git a/config_default.php b/config_default.php new file mode 100644 index 0000000..3fa9912 --- /dev/null +++ b/config_default.php @@ -0,0 +1,34 @@ + + */ + + $config = array(); + + /* + * The directory containing calibre's metadata.db file, with sub-directories + * containing all the formats. + * If this directory starts with a / EPUB download will only work with Nginx + * and if the calibre_internal_directory is set + */ + $config['calibre_directory'] = './'; + + /* + * The internal directory set in nginx config file + */ + $config['calibre_internal_directory'] = '/Calibre/'; + + /* + * Number of books + */ + $config['cops_recentbooks_limit'] = '50'; + + /* + * The internal directory set in nginx config file + */ + $config['cops_title_default'] = "Sebastien's COPS"; + +?> \ No newline at end of file diff --git a/favicon.ico b/favicon.ico new file mode 100644 index 0000000..9be986b Binary files /dev/null and b/favicon.ico differ diff --git a/feed.php b/feed.php new file mode 100644 index 0000000..5c832c7 --- /dev/null +++ b/feed.php @@ -0,0 +1,86 @@ + + * + */ + + require_once ("config.php"); + require_once ("base.php"); + require_once ("author.php"); + require_once ("serie.php"); + require_once ("book.php"); + header ("Content-Type:application/xml"); + $page = Base::PAGE_INDEX; + global $config; + if (!empty ($_GET) && isset($_GET["page"])) { + $page = $_GET["page"]; + } + switch ($page) { + case Base::PAGE_ALL_AUTHORS : + $title = "All authors"; + break; + case Base::PAGE_AUTHOR_DETAIL : + $title = Author::getAuthorName ($_GET["id"]); + break; + case Base::PAGE_ALL_SERIES : + $title = "All series"; + break; + case Base::PAGE_ALL_BOOKS : + $title = "All books by starting letter"; + break; + case Base::PAGE_ALL_BOOKS_LETTER: + $title = "All books starting by " . $_GET["id"]; + break; + case Base::PAGE_ALL_RECENT_BOOKS : + $title = "Most recent books"; + break; + case Base::PAGE_SERIE_DETAIL : + $title = "Series : " . Serie::getSerieById ($_GET["id"])->name; + break; + case Base::PAGE_OPENSEARCH : + echo Base::getOpenSearch (); + return; + case Base::PAGE_OPENSEARCH_QUERY : + $title = "Search result for query <" . $_GET["query"] . ">"; + break; + default: + $title = $config['cops_title_default']; + break; + } + Base::startXmlDocument ($title); + switch ($page) { + case Base::PAGE_ALL_AUTHORS : + Author::getAllAuthors(); + break; + case Base::PAGE_AUTHOR_DETAIL : + Book::getBooksByAuthor ($_GET["id"]); + break; + case Base::PAGE_ALL_SERIES : + Serie::getAllSeries(); + break; + case Base::PAGE_ALL_BOOKS : + Book::getAllBooks (); + break; + case Base::PAGE_ALL_BOOKS_LETTER: + Book::getBooksByStartingLetter ($_GET["id"]); + break; + case Base::PAGE_ALL_RECENT_BOOKS : + Book::getAllRecentBooks (); + break; + case Base::PAGE_SERIE_DETAIL : + Book::getBooksBySeries ($_GET["id"]); + break; + case Base::PAGE_OPENSEARCH_QUERY : + Book::getBooksByQuery ($_GET["query"]); + break; + default: + Author::getCount(); + Serie::getCount(); + Book::getCount(); + break; + } + echo Base::endXmlDocument (); +?> diff --git a/fetch.php b/fetch.php new file mode 100644 index 0000000..7238a13 --- /dev/null +++ b/fetch.php @@ -0,0 +1,58 @@ + with integration/modification by Sébastien Lucas + */ + + require_once ("config.php"); + require_once('book.php'); + + global $config; + $bookId = $_GET["id"]; + $book = Book::getBookById($bookId); + $type = "jpg"; + + if (!empty ($_GET) && isset($_GET["type"])) { + $type = $_GET["type"]; + } + + switch ($type) + { + case "jpg": + header("Content-type: image/jpeg"); + if (isset($_GET["width"])) + { + $file = $book->getFilePath ($type); + // get image size + if($size = GetImageSize($file)){ + $w = $size[0]; + $h = $size[1]; + //set new size + $nw = $_GET["width"]; + $nh = ($nw*$h)/$w; + } + else{ + //set new size + $nw = "160"; + $nh = "120"; + } + //draw the image + $src_img = imagecreatefromjpeg($file); + $dst_img = imagecreatetruecolor($nw,$nh); + imagecopyresampled($dst_img, $src_img, 0, 0, 0, 0, $nw, $nh, $w, $h);//resizing the image + imagejpeg($dst_img,"",100); + imagedestroy($src_img); + imagedestroy($dst_img); + return; + } + break; + case "epub": + header("Content-type: application/epub+zip"); + break; + } + $file = $book->getFilePath ($type, true); + header('Content-Disposition: attachement; filename="' . basename ($file) . '"'); + header ("X-Accel-Redirect: " . $config['calibre_internal_directory'] . $file); +?> \ No newline at end of file diff --git a/serie.php b/serie.php new file mode 100644 index 0000000..b628144 --- /dev/null +++ b/serie.php @@ -0,0 +1,67 @@ + + */ + +require_once('base.php'); + +class Serie extends Base { + const ALL_SERIES_ID = "calibre:series"; + + public $id; + public $name; + + public function __construct($pid, $pname) { + $this->id = $pid; + $this->name = $pname; + } + + public function getUri () { + return "feed.php?page=".parent::PAGE_SERIE_DETAIL."&id=$this->id"; + } + + public static function getCount() { + $nSeries = parent::getDb ()->query('select count(*) from series')->fetchColumn(); + parent::addEntryClass (new Entry ("Series", self::ALL_SERIES_ID, + "Alphabetical index of the $nSeries series", "text", + array ( new LinkNavigation ("feed.php?page=".parent::PAGE_ALL_SERIES)))); + } + + public static function getSerieByBookId ($bookId) { + $result = parent::getDb ()->prepare('select series.id as id, name +from books_series_link, series +where series.id = series and book = ?'); + $result->execute (array ($bookId)); + if ($post = $result->fetchObject ()) { + return new Serie ($post->id, $post->name); + } + return NULL; + } + + public static function getSerieById ($serieId) { + $result = parent::getDb ()->prepare('select id, name from series where id = ?'); + $result->execute (array ($serieId)); + if ($post = $result->fetchObject ()) { + return new Serie ($post->id, $post->name); + } + return NULL; + } + + public static function getAllSeries() { + $result = parent::getDb ()->query('select series.id as id, series.name as name, series.sort as sort, count(*) as count +from series, books_series_link +where series.id = series +group by series.id, series.name, series.sort +order by series.sort'); + while ($post = $result->fetchObject ()) + { + parent::addEntryClass (new Entry ($post->sort, self::ALL_SERIES_ID.":".$post->id, + "$post->count books", "text", + array ( new LinkNavigation ("feed.php?page=".parent::PAGE_SERIE_DETAIL."&id=$post->id")))); + } + } +} +?> \ No newline at end of file