diff --git a/files_opds/admin.php b/files_opds/admin.php new file mode 100644 index 0000000..4bfc869 --- /dev/null +++ b/files_opds/admin.php @@ -0,0 +1,30 @@ + Config::getPreview('OC\Preview\Epub') ? 1 : 0 ], + ["pdf" => Config::getPreview('OC\Preview\PDF') ? 1 : 0], + ["openoffice" => Config::getPreview('OC\Preview\OpenOffice') ? 1 : 0], + ["msoffice" => Config::getPreview('OC\Preview\MSOfficeDoc') ? 1 : 0] + ); + +$tmpl = new \OCP\Template('files_opds', 'admin'); +$tmpl->assign('previewFormats', $formats); +$tmpl->assign('cover-x', Config::getApp('cover-x', '200')); +$tmpl->assign('cover-y', Config::getApp('cover-y', '200')); +$tmpl->assign('thumb-x', Config::getApp('thumb-x', '36')); +$tmpl->assign('thumb-y', Config::getApp('thumb-y', '36')); + +return $tmpl->fetchPage(); diff --git a/files_opds/ajax/admin.php b/files_opds/ajax/admin.php new file mode 100644 index 0000000..caaadf1 --- /dev/null +++ b/files_opds/ajax/admin.php @@ -0,0 +1,54 @@ + array('message'=> $l->t('Settings updated successfully.')) + ) +); + +exit(); + diff --git a/files_opds/appinfo/app.php b/files_opds/appinfo/app.php index 2ba7431..a157b70 100644 --- a/files_opds/appinfo/app.php +++ b/files_opds/appinfo/app.php @@ -2,6 +2,14 @@ $l = OC_L10N::get('files_opds'); +require 'files_opds/lib/epub-preview.php'; + \OCP\App::registerPersonal('files_opds', 'personal'); +\OCP\App::registerAdmin('files_opds', 'admin'); +/* enable preview providers... */ +// \OCA\Files_Opds\Config::setPreview('OC\Preview\Epub',true); + +/* ..and register preview provider */ +\OC\Preview::registerProvider('OC\Preview\Epub'); diff --git a/files_opds/appinfo/info.xml b/files_opds/appinfo/info.xml index df6b1ae..c82b8d8 100644 --- a/files_opds/appinfo/info.xml +++ b/files_opds/appinfo/info.xml @@ -4,7 +4,7 @@ OPDS catalog Personal OPDS catalog AGPL - 0.1.2 + 0.2 Frank de Lange 7.0 true diff --git a/files_opds/css/settings.css b/files_opds/css/settings.css new file mode 100644 index 0000000..54893a9 --- /dev/null +++ b/files_opds/css/settings.css @@ -0,0 +1,7 @@ +#opds .indent { + padding-left: 1em; +} + +#opds .double-indent { + padding-left: 2em; +} diff --git a/files_opds/js/admin.js b/files_opds/js/admin.js new file mode 100644 index 0000000..3aaa0aa --- /dev/null +++ b/files_opds/js/admin.js @@ -0,0 +1,53 @@ +$(document).ready(function(){ + // save settings + var opdsAdminSettings = { + save : function() { + var epub = document.getElementById('opds-preview-epub').checked ? 'true' : 'false'; + var pdf = document.getElementById('opds-preview-pdf').checked ? 'true' : 'false'; + var openoffice = document.getElementById('opds-preview-openoffice').checked ? 'true' : 'false'; + var msoffice = document.getElementById('opds-preview-msoffice').checked ? 'true' : 'false'; + var data = { + opdsPreviewEpub : epub, + opdsPreviewPdf : pdf, + opdsPreviewOpenOffice : openoffice, + opdsPreviewMsOffice : msoffice + }; + OC.msg.startSaving('#opds-admin .msg'); + $.post(OC.filePath('files_opds', 'ajax', 'admin.php'), data, opdsAdminSettings.afterSave); + }, + afterSave : function(data){ + OC.msg.finishedSaving('#opds .msg', data); + } + }; + + var opdsAdminCoverSettings = { + save : function() { + var data = { + opdsCoverX : $('#opds-cover-x').val(), + opdsCoverY : $('#opds-cover-y').val(), + opdsThumbX : $('#opds-thumb-x').val(), + opdsThumbY : $('#opds-thumb-y').val() + }; + OC.msg.startSaving('#opds-admin .msg'); + $.post(OC.filePath('files_opds', 'ajax', 'admin.php'), data, opdsAdminCoverSettings.afterSave); + }, + afterSave : function(data){ + OC.msg.finishedSaving('#opds .msg', data); + } + }; + + $('#opds-preview-epub').on("change", opdsAdminSettings.save); + $('#opds-preview-pdf').on("change", opdsAdminSettings.save); + $('#opds-preview-openoffice').on("change", opdsAdminSettings.save); + $('#opds-preview-msoffice').on("change", opdsAdminSettings.save); + + $('#opds-cover-x,#opds-cover-y,#opds-thumb-x,#opds-thumb-y').blur(opdsAdminCoverSettings.save); + $('#opds-cover-x,#opds-cover-y,#opds-thumb-x,#opds-thumb-y').keypress(function( event ) { + if (event.which == 13) { + event.preventDefault(); + opdsAdminCoverSettings.save(); + } + }); + +}); + diff --git a/files_opds/lib/config.php b/files_opds/lib/config.php index 8b1a716..6e84a54 100644 --- a/files_opds/lib/config.php +++ b/files_opds/lib/config.php @@ -38,4 +38,74 @@ class Config public static function set($key, $value) { return \OCP\Config::setUserValue(\OCP\User::getUser(), 'files_opds', $key, $value); } + + /** + * @brief get app config value + * + * @param string $key value to retrieve + * @param string $default default value to use + * @return string retrieved value or default + */ + public static function getApp($key, $default) { + return \OCP\Config::getAppValue('files_opds', $key, $default); + } + + /** + * @brief set app config value + * + * @param string $key key for value to change + * @param string $value value to use + * @return bool success + */ + public static function setApp($key, $value) { + return \OCP\Config::setAppValue('files_opds', $key, $value); + } + + /** + * @brief get preview status + * + * @param string format + * @return bool (true = enabled, false = disabled) + */ + public static function getPreview($format) { + $enablePreviewProviders = \OCP\Config::getSystemValue('enabledPreviewProviders', null); + if (!($enablePreviewProviders === null)) { + return in_array($format, $enablePreviewProviders); + } + return false; + } + + /** + * @brief enable/disable preview for selected format + * + * @param string format + * @param bool enable (true = enable, false = disable, default = false) + * @return bool + */ + public static function setPreview($format, $enable = 'false') { + $enablePreviewProviders = \OCP\Config::getSystemValue('enabledPreviewProviders', null); + if ($enable == 'true') { + if ($enablePreviewProviders === null) { + // set up default providers + $enablePreviewProviders = array(); + array_push($enablePreviewProviders, + 'OC\Preview\Image', + 'OC\Preview\MP3', + 'OC\Preview\TXT', + 'OC\Preview\MarkDown'); + } + if (!(in_array($format,$enablePreviewProviders))) { + array_push($enablePreviewProviders, $format); + } + } else { + if (!($enablePreviewProviders == null)) { + $enablePreviewProviders = array_diff($enablePreviewProviders, array($format)); + } + } + + if (!(\OCP\Config::setSystemValue('enabledPreviewProviders', $enablePreviewProviders))) { + logWarn("Failed to enable " . $format . " preview provider (config.php readonly?)"); + return true; + } + } } diff --git a/files_opds/lib/epub-preview.php b/files_opds/lib/epub-preview.php new file mode 100644 index 0000000..1bd0164 --- /dev/null +++ b/files_opds/lib/epub-preview.php @@ -0,0 +1,48 @@ +getFileInfo($path); + if(!$fileInfo) { + return false; + } + + $absPath = $fileview->toTmpFile($path); + + $epub = new \OCA\Files_Opds\Epub($absPath); + + $cover = $epub->Cover(); + + if ($cover) { + + $image = new \OC_Image(); + + $image->loadFromData($cover['data']); + } + + return (($cover !== null) && $image->valid()) ? $image : false; + } + +} + diff --git a/files_opds/lib/epub.php b/files_opds/lib/epub.php new file mode 100644 index 0000000..afb099b --- /dev/null +++ b/files_opds/lib/epub.php @@ -0,0 +1,332 @@ +file = $file; + $zip = new \ZipArchive(); + if(!($zip->open($this->file))){ + \OC_Log::write('epub', "Failed to read epub file", \OC_Log::ERROR); + return false; + } + + // read container data + $data = $zip->getFromName('META-INF/container.xml'); + if($data == false){ + \OC_Log::write('epub', "Failed to access epub container data", \OC_Log::ERROR); + return false; + } + + $xml = new DOMDocument(); + $xml->registerNodeClass('DOMElement','\OCA\Files_Opds\EPubDOMElement'); + $xml->loadXML($data); + $xpath = new EPubDOMXPath($xml); + $nodes = $xpath->query('//n:rootfiles/n:rootfile[@media-type="application/oebps-package+xml"]'); + $this->meta = $nodes->item(0)->attr('full-path'); + + // load metadata + $data = $zip->getFromName($this->meta); + if(!$data){ + \OC_Log::write('epub', 'Failed to access epub metadata', \OC_Log::ERROR); + return false; + } + + $this->xml = new \DOMDocument(); + $this->xml->registerNodeClass('DOMElement','\OCA\Files_Opds\EPubDOMElement'); + $this->xml->loadXML($data); + $this->xml->formatOutput = true; + $this->xpath = new EPubDOMXPath($this->xml); + + $zip->close(); + } + + /** + * @brief file name getter + * @return string filename + */ + public static function file() { + return $this->file; + } + + /** + * @brief get author(s) + * + * @return array $authors + */ + public static function Authors() { + // read current data + $rolefix = false; + $authors = array(); + $nodes = $this->xpath->query('//opf:metadata/dc:creator[@opf:role="aut"]'); + if($nodes->length == 0){ + // no nodes where found, let's try again without role + $nodes = $this->xpath->query('//opf:metadata/dc:creator'); + $rolefix = true; + } + foreach($nodes as $node){ + $name = $node->nodeValue; + $as = $node->attr('opf:file-as'); + if(!$as){ + $as = $name; + $node->attr('opf:file-as',$as); + } + if($rolefix){ + $node->attr('opf:role','aut'); + } + $authors[$as] = $name; + } + return $authors; + } + + /** + * @brief get book title + * + * @param string $title + */ + public function Title(){ + return $this->get('dc:title'); + } + + /** + * @brief get language + * + * @param string $lang + */ + public function Language(){ + return $this->get('dc:language'); + } + + /** + * @brief get publisher info + * + * @return string $publisher + */ + public function Publisher(){ + return $this->get('dc:publisher'); + } + + /** + * @brief get copyright info + * + * @return string $rights + */ + public function Copyright(){ + return $this->get('dc:rights'); + } + + /** + * @brief get description + * + * @return string $description + */ + public function Description(){ + return $this->get('dc:description'); + } + + /** + * @brief get ISBN number + * + * @return string $isbn + */ + public function ISBN(){ + return $this->get('dc:identifier','opf:scheme','ISBN'); + } + + /** + * @brief get Google Books ID + * + * @return string $google + */ + public function Google(){ + return $this->get('dc:identifier','opf:scheme','GOOGLE'); + } + + /** + * @brief get Amazon ID + * + * @return string $amazon + */ + public function Amazon(){ + return $this->get('dc:identifier','opf:scheme','AMAZON'); + } + + /** + * @brief get subjects (aka. tags) + * + * @return array $subjects + */ + public function Subjects(){ + $subjects = array(); + $nodes = $this->xpath->query('//opf:metadata/dc:subject'); + foreach($nodes as $node){ + $subjects[] = $node->nodeValue; + } + return $subjects; + } + + /** + * @brief get cover data + * + * Returns an associative array with the following keys: + * + * mime - filetype (usually image/jpeg) + * data - binary image data + * found - internal path, or false if no image is set in epub + * + * @return array or null + */ + public function Cover(){ + $nodes = $this->xpath->query('//opf:metadata/opf:meta[@name="cover"]'); + if($nodes->length) { + $coverid = (String) $nodes->item(0)->attr('opf:content'); + if ($coverid) { + $nodes = $this->xpath->query('//opf:manifest/opf:item[@id="'.$coverid.'"]'); + if ($nodes->length) { + $mime = $nodes->item(0)->attr('opf:media-type'); + $path = $nodes->item(0)->attr('opf:href'); + $path = dirname('/'.$this->meta).'/'.$path; // image path is relative to meta file + $path = ltrim($path,'/'); + $zip = new \ZipArchive(); + if($zip->open($this->file)){ + $data = $zip->getFromName($path); + return array( + 'mime' => $mime, + 'data' => $data, + 'found' => $path + ); + } + } + } + } else { + return null; + } + } + + /** + * @brief simple getter for simple meta attributes + * + * It should only be used for attributes that are expected to be unique + * + * @param string $item XML node to get + * @param string $att Attribute name + * @param string $aval Attribute value + * @return string node value + */ + protected function get($item, $att=false, $aval=false){ + $xpath = '//opf:metadata/'.$item; + if ($att) { + $xpath .= "[@$att=\"$aval\"]"; + } + + $nodes = $this->xpath->query($xpath); + if($nodes->length){ + return $nodes->item(0)->nodeValue; + }else{ + return ''; + } + } +} + +class EPubDOMXPath extends DOMXPath { + public function __construct(DOMDocument $doc){ + parent::__construct($doc); + + if(is_a($doc->documentElement, '\OCA\Files_Opds\EPubDOMElement')){ + foreach($doc->documentElement->namespaces as $ns => $url){ + $this->registerNamespace($ns,$url); + } + } + } +} + +class EPubDOMElement extends DOMElement { + public $namespaces = array( + 'n' => 'urn:oasis:names:tc:opendocument:xmlns:container', + 'opf' => 'http://www.idpf.org/2007/opf', + 'dc' => 'http://purl.org/dc/elements/1.1/' + ); + + + public function __construct($name, $value='', $namespaceURI=''){ + list($ns,$name) = $this->splitns($name); + $value = htmlspecialchars($value); + if(!$namespaceURI && $ns){ + $namespaceURI = $this->namespaces[$ns]; + } + parent::__construct($name, $value, $namespaceURI); + } + + /** + * @brief split given name in namespace prefix and local part + * + * @param string $name + * @return array (namespace, name) + */ + public function splitns($name){ + $list = explode(':',$name,2); + if(count($list) < 2) array_unshift($list,''); + return $list; + } + + /** + * @brief simple EPub namespace aware attribute getter + * + * @param string attribute + * @return string attribute value + */ + public function attr($attr){ + list($ns,$attr) = $this->splitns($attr); + + $nsuri = ''; + if($ns){ + $nsuri = $this->namespaces[$ns]; + if(!$this->namespaceURI){ + if($this->isDefaultNamespace($nsuri)){ + $nsuri = ''; + } + }elseif($this->namespaceURI == $nsuri){ + $nsuri = ''; + } + } + + if($nsuri){ + return $this->getAttributeNS($nsuri,$attr); + }else{ + return $this->getAttribute($attr); + } + } +} + + + diff --git a/files_opds/lib/files.php b/files_opds/lib/files.php index dbae3bf..cfa57ff 100644 --- a/files_opds/lib/files.php +++ b/files_opds/lib/files.php @@ -18,7 +18,7 @@ namespace OCA\Files_Opds; class Files extends \OCA\Files\Helper { /** - * Formats the file info to be returned as OPDS to the client. + * Formats the file info to be returned as OPDS to the client * * @param \OCP\Files\FileInfo $i * @return array formatted file info @@ -28,11 +28,16 @@ class Files extends \OCA\Files\Helper $entry['id'] = $i['fileid']; $entry['mtime'] = $i['mtime'] * 1000; - $entry['icon'] = self::determineIcon($i); $entry['name'] = $i->getName(); - $entry['mimetype'] = $i['mimetype']; - $entry['size'] = $i['size']; $entry['type'] = $i['type']; + if ($i['type'] === 'file') { + $entry['mimetype'] = $i['mimetype']; + $entry['preview'] = self::getPreview($i); + $entry['thumbnail'] = self::getThumbnail($i); + $entry['humansize'] = \OC_Helper::humanFileSize($i['size']); + } else { + $entry['icon'] = self::determineIcon($i); + } return $entry; } @@ -54,6 +59,33 @@ class Files extends \OCA\Files\Helper return $files; } + /** + * @brief get preview for file + * @param \OCP\Files\FileInfo $i + * @return string preview URL + */ + public static function getPreview($i) { + if (\OC::$server->getPreviewManager()->isMimeSupported($i['mimetype'])) { + return \OC_Helper::linkToRoute( 'core_ajax_preview', array('x' => Config::getApp('cover-x', '200'), 'y' => Config::getApp('cover-y', '200'), 'file' => \OC\Files\Filesystem::normalizePath(\OC\Files\Filesystem::getPath($i['fileid'])))); + } else { + return self::determineIcon($i); + } + } + + + /** + * @brief get thumbnail for file + * @param \OCP\Files\FileInfo $i + * @return string preview URL + */ + public static function getThumbnail($i) { + if (\OC::$server->getPreviewManager()->isMimeSupported($i['mimetype'])) { + return \OC_Helper::linkToRoute( 'core_ajax_preview', array('x' => Config::getApp('thumb-x', '36'), 'y' => Config::getApp('thumb-y', '36'), 'file' => \OC\Files\Filesystem::normalizePath(\OC\Files\Filesystem::getPath($i['fileid'])))); + } else { + return self::determineIcon($i); + } + } + /* * @brief check if $child is a subdirectory of $parent * diff --git a/files_opds/templates/admin.php b/files_opds/templates/admin.php new file mode 100644 index 0000000..0ac3a99 --- /dev/null +++ b/files_opds/templates/admin.php @@ -0,0 +1,48 @@ + $enabled) { + echo '' : '>'); + echo ''; + } +} + +?> + +
+

t('OPDS')); ?>

+

Enable preview for:

+
+ +
+ +
+ +
+
+
+

Cover size

+ + " value="" /> + + " value="" /> +
+
+

Cover thumbnail size

+ + " value="" /> + + " value="" /> +
+
+ diff --git a/files_opds/templates/feed.php b/files_opds/templates/feed.php index d2f0e6b..265619f 100644 --- a/files_opds/templates/feed.php +++ b/files_opds/templates/feed.php @@ -10,11 +10,19 @@ * later. */ + +function formatMetadata($humansize,$mimetype,$name) { + return "Size: " . $humansize . "\n" + . "Type: " . $mimetype . "\n" + . "Filename: " . $name; +} + echo ''; ?> id: <?php p($l->t("%s's library", array($_['user']))); ?> @@ -54,19 +62,20 @@ echo ''; <?php p($file['name']); ?> id: + - - - + @@ -89,19 +98,20 @@ echo ''; <?php p($file['name']); ?> id: + - - - + diff --git a/files_opds/templates/personal.php b/files_opds/templates/personal.php index 22fd980..ef92218 100644 --- a/files_opds/templates/personal.php +++ b/files_opds/templates/personal.php @@ -12,7 +12,7 @@ ?> -
+

t('OPDS')); ?>

type="checkbox">