@@ -1,8 +1,17 @@ | |||
0.0.3 - 20120507 | |||
* Fixed many things blocking opensearch from working | |||
* There was a bug introduced in 0.0.2 | |||
* The URL can't be relative for Mantano reader, so I added a configuration item. | |||
* I continued the refactoring to bring HTML to COPS | |||
* Thumbnails have bigger size (I'll add a configuration item later) | |||
* Add headers to help caching image and thumbnail to the browser | |||
* | |||
0.0.2 - 20120411 | |||
* Add support for MOBI and PDF | |||
* Major refactoring to prepare something nice for the future ;) | |||
* Add a config item to make use of X-Sendfile instead of X-Accel-Redirect | |||
if needed | |||
* Add support for MOBI and PDF | |||
* Major refactoring to prepare something nice for the future ;) | |||
* Add a config item to make use of X-Sendfile instead of X-Accel-Redirect if needed | |||
0.0.1 - 20120302 | |||
* First public release | |||
* First public release | |||
@@ -11,6 +11,7 @@ require_once ("base.php"); | |||
class OPDSRenderer | |||
{ | |||
const PAGE_OPENSEARCH = "8"; | |||
const PAGE_OPENSEARCH_QUERY = "9"; | |||
private $xmlStream = NULL; | |||
private $updated = NULL; | |||
@@ -32,11 +33,13 @@ class OPDSRenderer | |||
} | |||
public function getOpenSearch () { | |||
global $config; | |||
$xml = new XMLWriter (); | |||
$xml->openMemory (); | |||
$xml->setIndent (true); | |||
$xml->startDocument('1.0','UTF-8'); | |||
$xml->startElement ("OpenSearchDescription"); | |||
$xml->writeAttribute ("xmlns", "http://a9.com/-/spec/opensearch/1.1/"); | |||
$xml->startElement ("ShortName"); | |||
$xml->text ("My catalog"); | |||
$xml->endElement (); | |||
@@ -47,11 +50,18 @@ class OPDSRenderer | |||
$xml->text ("UTF-8"); | |||
$xml->endElement (); | |||
$xml->startElement ("Image"); | |||
$xml->writeAttribute ("type", "image/x-icon"); | |||
$xml->writeAttribute ("width", "16"); | |||
$xml->writeAttribute ("height", "16"); | |||
$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->writeAttribute ("template", $config['cops_full_url'] . 'feed.php?query={searchTerms}'); | |||
$xml->endElement (); | |||
$xml->startElement ("Query"); | |||
$xml->writeAttribute ("role", "example"); | |||
$xml->writeAttribute ("searchTerms", "robot"); | |||
$xml->endElement (); | |||
$xml->endElement (); | |||
$xml->endDocument(); | |||
@@ -89,9 +99,9 @@ class OPDSRenderer | |||
self::getXmlStream ()->text ("sebastien@slucas.fr"); | |||
self::getXmlStream ()->endElement (); | |||
self::getXmlStream ()->endElement (); | |||
$link = new LinkNavigation ("feed.php", "start", "Home"); | |||
$link = new LinkNavigation ("", "start", "Home"); | |||
self::renderLink ($link); | |||
$link = new LinkNavigation ($_SERVER['REQUEST_URI'], "self"); | |||
$link = new LinkNavigation ("?" . $_SERVER['QUERY_STRING'], "self"); | |||
self::renderLink ($link); | |||
$link = new Link ("feed.php?page=" . self::PAGE_OPENSEARCH, "application/opensearchdescription+xml", "search", "Search here"); | |||
self::renderLink ($link); | |||
@@ -169,6 +179,18 @@ class OPDSRenderer | |||
public function render ($page) { | |||
self::startXmlDocument ($page->title); | |||
if ($page->query) | |||
{ | |||
self::getXmlStream ()->startElement ("opensearch:totalResults"); | |||
self::getXmlStream ()->text (count($page->entryArray)); | |||
self::getXmlStream ()->endElement (); | |||
self::getXmlStream ()->startElement ("opensearch:itemsPerPage"); | |||
self::getXmlStream ()->text (count($page->entryArray)); | |||
self::getXmlStream ()->endElement (); | |||
self::getXmlStream ()->startElement ("opensearch:startIndex"); | |||
self::getXmlStream ()->text ("1"); | |||
self::getXmlStream ()->endElement (); | |||
} | |||
foreach ($page->entryArray as $entry) { | |||
self::getXmlStream ()->startElement ("entry"); | |||
self::renderEntry ($entry); | |||
@@ -178,4 +200,4 @@ class OPDSRenderer | |||
} | |||
} | |||
?> | |||
?> |
@@ -74,6 +74,14 @@ 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; | |||
= Notes on Opensearch = | |||
Opensearch allow searching through an OPDS catalog. After many tests, I've been | |||
able to make it work with FBReader and Mantano Reader. | |||
It seems that Aldiko didn't implement it properly so it won't work with COPS or | |||
any other custom OPDS catalog. | |||
= Known problems = | |||
* Only tested with Nginx. | |||
@@ -21,7 +21,7 @@ class Author extends Base { | |||
} | |||
public function getUri () { | |||
return "feed.php?page=".parent::PAGE_AUTHOR_DETAIL."&id=$this->id"; | |||
return "?page=".parent::PAGE_AUTHOR_DETAIL."&id=$this->id"; | |||
} | |||
public function getEntryId () { | |||
@@ -33,7 +33,7 @@ class Author extends Base { | |||
$nAuthors = parent::getDb ()->query('select count(*) from authors')->fetchColumn(); | |||
$entry = new Entry ("Authors", self::ALL_AUTHORS_ID, | |||
"Alphabetical index of the $nAuthors authors", "text", | |||
array ( new LinkNavigation ("feed.php?page=".parent::PAGE_ALL_AUTHORS))); | |||
array ( new LinkNavigation ("?page=".parent::PAGE_ALL_AUTHORS))); | |||
return $entry; | |||
} | |||
@@ -26,6 +26,10 @@ class Link | |||
$this->rel = $prel; | |||
$this->title = $ptitle; | |||
} | |||
public function hrefXhtml () { | |||
return str_replace ("&", "&", $this->href); | |||
} | |||
} | |||
class LinkNavigation extends Link | |||
@@ -34,6 +38,7 @@ class LinkNavigation extends Link | |||
public function __construct($phref, $prel = NULL, $ptitle = NULL) { | |||
parent::__construct ($phref, self::OPDS_NAVIGATION_TYPE, $prel, $ptitle); | |||
$this->href = $_SERVER["SCRIPT_NAME"] . $this->href; | |||
} | |||
} | |||
@@ -76,6 +81,14 @@ class EntryBook extends Entry | |||
$this->book = $pbook; | |||
$this->localUpdated = $pbook->timestamp; | |||
} | |||
public function getCoverThumbnail () { | |||
foreach ($this->linkArray as $link) { | |||
if ($link->rel == "http://opds-spec.org/image/thumbnail") | |||
return $link->hrefXhtml (); | |||
} | |||
return null; | |||
} | |||
} | |||
class Page | |||
@@ -25,6 +25,7 @@ class Book extends Base { | |||
public $authors = NULL; | |||
public $serie = NULL; | |||
public $tags = NULL; | |||
public $format = array (); | |||
public static $mimetypes = array( | |||
'epub' => 'application/epub+zip', | |||
'mobi' => 'application/x-mobipocket-ebook', | |||
@@ -59,6 +60,20 @@ class Book extends Base { | |||
return $this->authors; | |||
} | |||
public function getAuthorsName () { | |||
$authorList = null; | |||
foreach ($this->getAuthors () as $author) { | |||
if ($authorList) { | |||
$authorList = $authorList . ", " . $author->name; | |||
} | |||
else | |||
{ | |||
$authorList = $author->name; | |||
} | |||
} | |||
return $authorList; | |||
} | |||
public function getSerie () { | |||
if (is_null ($this->serie)) { | |||
$this->serie = Serie::getSerieByBookId ($this->id); | |||
@@ -84,6 +99,20 @@ class Book extends Base { | |||
return $this->tags; | |||
} | |||
public function getTagsName () { | |||
$tagList = null; | |||
foreach ($this->getTags () as $tag) { | |||
if ($tagList) { | |||
$tagList = $tagList . ", " . $tag; | |||
} | |||
else | |||
{ | |||
$tagList = $tag; | |||
} | |||
} | |||
return $tagList; | |||
} | |||
public function getComment () { | |||
$addition = ""; | |||
$se = $this->getSerie (); | |||
@@ -125,20 +154,21 @@ class Book extends Base { | |||
} | |||
else | |||
{ | |||
array_push ($linkArray, new Link (rawurlencode ($this->path."/".$file), "image/jpeg", "http://opds-spec.org/image")); | |||
array_push ($linkArray, new Link (str_replace('%2F','/',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")); | |||
array_push ($linkArray, new Link ("fetch.php?id=$this->id&height=70", "image/jpeg", "http://opds-spec.org/image/thumbnail")); | |||
} | |||
foreach (self::$mimetypes as $ext => $mime) | |||
{ | |||
if (preg_match ('/'. $ext .'$/', $file)) { | |||
$this->format [$ext] = $file; | |||
if (preg_match ('/^\//', $config['calibre_directory'])) | |||
{ | |||
array_push ($linkArray, new Link ("fetch.php?id=$this->id&type=" . $ext, $mime, "http://opds-spec.org/acquisition", "Download")); | |||
} | |||
else | |||
{ | |||
array_push ($linkArray, new Link (rawurlencode ($this->path."/".$file), $mime, "http://opds-spec.org/acquisition", "Download")); | |||
array_push ($linkArray, new Link (str_replace('%2F','/',rawurlencode ($this->path."/".$file)), $mime, "http://opds-spec.org/acquisition", "Download")); | |||
} | |||
} | |||
} | |||
@@ -171,12 +201,12 @@ class Book extends Base { | |||
$entry = new Entry ("Books", | |||
self::ALL_BOOKS_ID, | |||
"Alphabetical index of the $nBooks books", "text", | |||
array ( new LinkNavigation ("feed.php?page=".parent::PAGE_ALL_BOOKS))); | |||
array ( new LinkNavigation ("?page=".parent::PAGE_ALL_BOOKS))); | |||
array_push ($result, $entry); | |||
$entry = new Entry ("Recents books", | |||
self::ALL_RECENT_BOOKS_ID, | |||
"Alphabetical index of the " . $config['cops_recentbooks_limit'] . " most recent books", "text", | |||
array ( new LinkNavigation ("feed.php?page=".parent::PAGE_ALL_RECENT_BOOKS))); | |||
array ( new LinkNavigation ("?page=".parent::PAGE_ALL_RECENT_BOOKS))); | |||
array_push ($result, $entry); | |||
return $result; | |||
} | |||
@@ -253,7 +283,7 @@ order by substr (upper (sort), 1, 1)"); | |||
{ | |||
array_push ($entryArray, 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)))); | |||
array ( new LinkNavigation ("?page=".parent::PAGE_ALL_BOOKS_LETTER."&id=".$post->title)))); | |||
} | |||
return $entryArray; | |||
} | |||
@@ -6,13 +6,15 @@ | |||
* @author Sébastien Lucas <sebastien@slucas.fr> | |||
*/ | |||
$config = array(); | |||
if (!isset($config)) | |||
$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 | |||
* and the calibre_internal_directory has to be set properly | |||
* BEWARE : it has to end with a / | |||
*/ | |||
$config['calibre_directory'] = './'; | |||
@@ -23,7 +25,14 @@ | |||
$config['calibre_internal_directory'] = '/Calibre/'; | |||
/* | |||
* Number of books | |||
* Full URL prefix (with trailing /) | |||
* usefull especially for Opensearch where a full URL is sometimes required | |||
* For example Mantano requires it. | |||
*/ | |||
$config['cops_full_url'] = ''; | |||
/* | |||
* Number of recent books to show | |||
*/ | |||
$config['cops_recentbooks_limit'] = '50'; | |||
@@ -39,5 +48,5 @@ | |||
* X-Accel-Redirect : For Nginx | |||
* X-Sendfile : For Lightttpd or Apache (with mod_xsendfile) | |||
*/ | |||
$config['cops_x_accel_redirect'] = "X-Accel-Redirect"; | |||
$config['cops_x_accel_redirect'] = "X-Accel-Redirect"; | |||
?> |
@@ -17,6 +17,8 @@ | |||
header ("Content-Type:application/xml"); | |||
$page = getURLParam ("page", Base::PAGE_INDEX); | |||
$query = getURLParam ("query"); | |||
if ($query) | |||
$page = Base::PAGE_OPENSEARCH_QUERY; | |||
$qid = getURLParam ("id"); | |||
$OPDSRender = new OPDSRenderer (); | |||
@@ -10,6 +10,10 @@ | |||
require_once ("book.php"); | |||
global $config; | |||
$expires = 60*60*24*14; | |||
header("Pragma: public"); | |||
header("Cache-Control: maxage=".$expires); | |||
header('Expires: ' . gmdate('D, d M Y H:i:s', time()+$expires) . ' GMT'); | |||
$bookId = $_GET["id"]; | |||
$book = Book::getBookById($bookId); | |||
$type = getURLParam ("type", "jpg"); | |||
@@ -43,6 +47,31 @@ | |||
imagedestroy($dst_img); | |||
return; | |||
} | |||
if (isset($_GET["height"])) | |||
{ | |||
$file = $book->getFilePath ($type); | |||
// get image size | |||
if($size = GetImageSize($file)){ | |||
$w = $size[0]; | |||
$h = $size[1]; | |||
//set new size | |||
$nh = $_GET["height"]; | |||
$nw = ($nh*$w)/$h; | |||
} | |||
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; | |||
default: | |||
header("Content-type: " . Book::$mimetypes[$type]); | |||
@@ -20,14 +20,14 @@ class Serie extends Base { | |||
} | |||
public function getUri () { | |||
return "feed.php?page=".parent::PAGE_SERIE_DETAIL."&id=$this->id"; | |||
return "?page=".parent::PAGE_SERIE_DETAIL."&id=$this->id"; | |||
} | |||
public static function getCount() { | |||
$nSeries = parent::getDb ()->query('select count(*) from series')->fetchColumn(); | |||
$entry = new Entry ("Series", self::ALL_SERIES_ID, | |||
"Alphabetical index of the $nSeries series", "text", | |||
array ( new LinkNavigation ("feed.php?page=".parent::PAGE_ALL_SERIES))); | |||
array ( new LinkNavigation ("?page=".parent::PAGE_ALL_SERIES))); | |||
return $entry; | |||
} | |||
@@ -62,7 +62,7 @@ order by series.sort'); | |||
{ | |||
array_push ($entryArray, 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")))); | |||
array ( new LinkNavigation ("?page=".parent::PAGE_SERIE_DETAIL."&id=$post->id")))); | |||
} | |||
return $entryArray; | |||
} | |||