diff --git a/resources/epub-loader/BaseExport.class.php b/resources/epub-loader/BaseExport.class.php new file mode 100644 index 0000000..95915b4 --- /dev/null +++ b/resources/epub-loader/BaseExport.class.php @@ -0,0 +1,209 @@ + + */ + +class BaseExport +{ + protected $mProperties = null; + protected $mFileName = ''; + protected $mSearch = null; + protected $mReplace = null; + + public $mFormatProperty = true; + + /** + * Open an export file (or create if file does not exist) + * + * @param string Export file name + * @param boolean Force file creation + */ + public function __construct($inFileName, $inCreate = false) + { + if ($inCreate && file_exists($inFileName)) { + if (!unlink($inFileName)) { + $error = sprintf('Cannot remove file: %s', $inFileName); + throw new Exception($error); + } + } + + $this->mFileName = $inFileName; + + $this->mProperties = array(); + } + + public function ClearProperties() + { + $this->mProperties = array(); + } + + public function SetProperty($inKey, $inValue) + { + // Don't store empty keys + if (empty($inKey)) { + return; + } + + if ($this->mFormatProperty) { + $inValue = $this->FormatProperty($inValue); + } + + $this->mProperties[$inKey] = $inValue; + } + + /** + * Format a property + * + * @param string or array of strings to format + * @return string or array of strings formated + */ + protected function FormatProperty($inValue) + { + if (!isset($inValue)) { + return ''; + } + if (is_numeric($inValue)) { + return (string)$inValue; + } + if (is_array($inValue)) { + // Recursive call for arrays + foreach ($inValue as $key => $value) { + $inValue[$key] = $this->FormatProperty($value); + } + return $inValue; + } + if (!is_string($inValue) || empty($inValue)) { + return ''; + } + + // Replace html entities with normal characters + $str = html_entity_decode($inValue, ENT_COMPAT, 'UTF-8'); + // Replace characters + if (isset($this->mSearch)) { + $str = str_replace($this->mSearch, $this->mReplace, $str); + } + + // Strip double spaces + while (strpos($str, ' ') !== false) { + $str = str_replace(' ', ' ', $str); + } + + // Trim + $str = trim($str); + + return $str; + } + + /** + * Save data to file + * + * @throws Exception if error + */ + public function SaveToFile() + { + // Write the file + $content = $this->GetContent(); + if (!file_put_contents($this->mFileName, $content)) { + $error = sprintf('Cannot save export to file: %s', $this->mFileName); + throw new Exception($error); + } + } + + /** + * Send download http headers + * + * @param string $inFileName Download file name to display in the browser + * @param int $inFileSize Download file size + * @param string $inCodeSet Charset + * @throws exception if http headers have been already sent + * + * @return void + */ + private function SendDownloadHeaders($inFileName, $inFileSize = null, $inCodeSet = 'utf-8') + { + // Throws excemtion if http headers have been already sent + $filename = ''; + $linenum = 0; + if (headers_sent($filename, $linenum)) { + $error = sprintf('Http headers already sent by file: %s ligne %d', $filename, $linenum); + throw new Exception($error); + } + + $inFileName = str_replace(' ', '', basename($inFileName)); // Cleanup file name + $ext = strtolower(substr(strrchr($inFileName, '.'), 1)); + + switch ($ext) { + case 'pdf': + $contentType = 'application/pdf'; + break; + case 'zip': + $contentType = 'application/zip'; + break; + case 'xml': + $contentType = 'text/xml'; + if (!empty($inCodeSet)) { + $contentType .= '; charset=' . $inCodeSet . '"'; + } + break; + case 'txt': + $contentType = 'text/plain'; + if (!empty($inCodeSet)) { + $contentType .= '; charset=' . $inCodeSet . '"'; + } + break; + case 'csv': + $contentType = 'text/csv'; + if (!empty($inCodeSet)) { + $contentType .= '; charset=' . $inCodeSet . '"'; + } + break; + case 'html': + $contentType = 'text/html'; + if (!empty($inCodeSet)) { + $contentType .= '; charset=' . $inCodeSet . '"'; + } + break; + default: + $contentType = 'application/force-download'; + break; + } + + // Send http headers for download + header('Content-disposition: attachment; filename="' . $inFileName . '"'); + Header('Content-Type: ' . $contentType); + //header('Content-Transfer-Encoding: binary'); + if (isset($inFileSize)) { + header('Content-Length: ' . $inFileSize); + } + + // Send http headers to remove the browser cache + header('Expires: Mon, 26 Jul 1997 05:00:00 GMT'); + header('Last-Modified: ' . gmdate("D, d M Y H:i:s") . ' GMT'); + header('Cache-Control: no-store, no-cache, must-revalidate'); + header('Cache-Control: post-check=0, pre-check=0', false); + header('Pragma: no-cache'); + } + + /** + * Download export and stop further script execution + */ + public function Download() + { + $content = $this->GetContent(); + + // Send http download headers + $size = strlen($content); + $this->SendDownloadHeaders($this->mFileName, $size); + + // Send file content to download + echo $content; + + exit; + } + +} + +?> diff --git a/resources/epub-loader/BookExport.class.php b/resources/epub-loader/BookExport.class.php new file mode 100644 index 0000000..20eb829 --- /dev/null +++ b/resources/epub-loader/BookExport.class.php @@ -0,0 +1,122 @@ + + */ + +require_once(realpath(dirname(__FILE__)) . '/CsvExport.class.php'); + +class BookExport +{ + private $mExport = null; + private $mNbBook = 0; + + const eExportTypeCsv = 1; + const CsvSeparator = "\t"; + + /** + * Open an export file (or create if file does not exist) + * + * @param string Export file name + * @param enum Export type + * @param boolean Force file creation + * @throws Exception if error + */ + public function __construct($inFileName, $inExportType, $inCreate = false) + { + switch ($inExportType) { + case self::eExportTypeCsv: + $this->mExport = new CsvExport($inFileName); + break; + default: + $error = sprintf('Incorrect export type: %d', $inExportType); + throw new Exception($error); + } + } + + /** + * Add an epub to the export + * + * @param string Epub file name + * @throws Exception if error + * + * @return void + */ + public function AddEpub($inFileName) + { + // Load the book infos + $bookInfos = new BookInfos(); + $bookInfos->LoadFromEpub($inFileName); + // Add the book + $this->AddBook($bookInfos); + } + + /** + * Add a new book to the export + * + * @param object BookInfo object + * @throws Exception if error + * + * @return void + */ + private function AddBook($inBookInfo) + { + // Add export header + if ($this->mNbBook++ == 0) { + $i = 1; + $this->mExport->SetProperty($i++, 'Format'); + $this->mExport->SetProperty($i++, 'Path'); + $this->mExport->SetProperty($i++, 'Name'); + $this->mExport->SetProperty($i++, 'Uuid'); + $this->mExport->SetProperty($i++, 'Uri'); + $this->mExport->SetProperty($i++, 'Title'); + $this->mExport->SetProperty($i++, 'Authors'); + $this->mExport->SetProperty($i++, 'Language'); + $this->mExport->SetProperty($i++, 'Description'); + $this->mExport->SetProperty($i++, 'Subjects'); + $this->mExport->SetProperty($i++, 'Cover'); + $this->mExport->SetProperty($i++, 'Serie'); + $this->mExport->SetProperty($i++, 'SerieIndex'); + $this->mExport->AddContent(); + } + + // Add book infos to the export + $i = 1; + $this->mExport->SetProperty($i++, $inBookInfo->mFormat); + $this->mExport->SetProperty($i++, $inBookInfo->mPath); + $this->mExport->SetProperty($i++, $inBookInfo->mName); + $this->mExport->SetProperty($i++, $inBookInfo->mUuid); + $this->mExport->SetProperty($i++, $inBookInfo->mUri); + $this->mExport->SetProperty($i++, $inBookInfo->mTitle); + $this->mExport->SetProperty($i++, implode(' - ', $inBookInfo->mAuthors)); + $this->mExport->SetProperty($i++, $inBookInfo->mLanguage); + $this->mExport->SetProperty($i++, $inBookInfo->mDescription); + $this->mExport->SetProperty($i++, implode(' - ', $inBookInfo->mSubjects)); + $this->mExport->SetProperty($i++, $inBookInfo->mCover); + $this->mExport->SetProperty($i++, $inBookInfo->mSerie); + $this->mExport->SetProperty($i++, $inBookInfo->mSerieIndex); + + $this->mExport->AddContent(); + } + + /** + * Download export and stop further script execution + */ + public function Download() + { + $this->mExport->Download(); + } + + /** + * Save export to file + */ + public function SaveToFile() + { + $this->mExport->SaveToFile(); + } + +} + +?> diff --git a/resources/epub-loader/BookInfos.class.php b/resources/epub-loader/BookInfos.class.php new file mode 100644 index 0000000..a9076eb --- /dev/null +++ b/resources/epub-loader/BookInfos.class.php @@ -0,0 +1,65 @@ + + */ + +require_once(realpath(dirname(__FILE__)) . '/ZipFile.class.php'); +require_once(realpath(dirname(dirname(__FILE__))) . '/php-epub-meta/epub.php'); + +/** + * BookInfos class contains informations about a book, + * and methods to load this informations from multiple sources (eg epub file) + */ +class BookInfos +{ + public $mPath = ''; + public $mName = ''; + public $mFormat = ''; + public $mUuid = ''; + public $mUri = ''; + public $mTitle = ''; + public $mTitleSort = ''; + public $mAuthors = null; + public $mLanguage = ''; + public $mDescription = ''; + public $mSubjects = null; + public $mCover = ''; + public $mSerie = ''; + public $mSerieIndex = ''; + + /** + * Loads book infos from an epub file + * + * @param string Epub full file name + * @throws Exception if error + * + * @return void + */ + public function LoadFromEpub($inFileName) + { + // Load the epub file + $epub = new EPub($inFileName, 'ZipFile'); + + // Get the epub infos + $this->mFormat = 'epub'; + $this->mPath = pathinfo($inFileName, PATHINFO_DIRNAME); + $this->mName = pathinfo($inFileName, PATHINFO_FILENAME); + $this->mUuid = $epub->Uuid(); + $this->mUri = $epub->Uri(); + $this->mTitle = $epub->Title(); + $this->mTitleSort = $this->mTitle; // Use title for now + $this->mAuthors = $epub->Authors(); + $this->mLanguage = $epub->Language(); + $this->mDescription = $epub->Description(); + $this->mSubjects = $epub->Subjects(); + $this->mCover = $epub->getCoverItem(); + $this->mSerie = $epub->Serie(); + $this->mSerieIndex = $epub->SerieIndex(); + } + +} + +?> \ No newline at end of file diff --git a/resources/epub-loader/CalibreDbLoader.class.php b/resources/epub-loader/CalibreDbLoader.class.php new file mode 100644 index 0000000..a391233 --- /dev/null +++ b/resources/epub-loader/CalibreDbLoader.class.php @@ -0,0 +1,254 @@ + + */ + +require_once(realpath(dirname(__FILE__)) . '/BookInfos.class.php'); + +/** + * Calibre database sql file that comes unmodified from Calibre project: + * /calibre/resources/metadata_sqlite.sql + */ +define('CalibreCreateDbSql', realpath(dirname(__FILE__)) . '/metadata_sqlite.sql'); + +/** + * CalibreDbLoader class allows to open or create a new Calibre database, + * and then add BookInfos objects into the database + */ +class CalibreDbLoader +{ + private $mDb = null; + + /** + * Open a Calibre database (or create if database does not exist) + * + * @param string Calibre database file name + * @param boolean Force database creation + */ + public function __construct($inDbFileName, $inCreate = false) + { + if ($inCreate || !file_exists($inDbFileName)) { + $this->CreateDatabase($inDbFileName); + } + else { + $this->OpenDatabase($inDbFileName); + } + } + + /** + * Create an sqlite database + * + * @param string Database file name + * @throws Exception if error + * + * @return void + */ + private function CreateDatabase($inDbFileName) + { + // Read the sql file + $content = file_get_contents(CalibreCreateDbSql); + if ($content === false) { + $error = sprintf('Cannot read sql file: %s', $inDbFileName); + throw new Exception($error); + } + + // Remove the database file + if (file_exists($inDbFileName) && !unlink($inDbFileName)) { + $error = sprintf('Cannot remove database file: %s', $inDbFileName); + throw new Exception($error); + } + + // Create the new database file + $this->OpenDatabase($inDbFileName); + + // Create the database tables + try { + $sqlArray = explode('CREATE ', $content); + foreach ($sqlArray as $sql) { + $sql = trim($sql); + if (empty($sql)) { + continue; + } + $sql = 'CREATE ' . $sql; + $str = strtolower($sql); + if (strpos($str, 'create view') !== false) { + continue; + } + if (strpos($str, 'title_sort') !== false) { + continue; + } + $stmt = $this->mDb->prepare($sql); + $stmt->execute(); + } + } + catch (Exception $e) { + $error = sprintf('Cannot create database: %s', $e->getMessage()); + throw new Exception($error); + } + } + + /** + * Open an sqlite database + * + * @param string Database file name + * @throws Exception if error + * + * @return void + */ + private function OpenDatabase($inDbFileName) + { + try { + // Init the Data Source Name + $dsn = 'sqlite:' . $inDbFileName; + // Open the database + $this->mDb = new PDO($dsn); // Send an exception if error + $this->mDb->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); + //echo sprintf('Init database ok for: %s%s', $dsn, '
'); + } + catch (Exception $e) { + $error = sprintf('Cannot open database [%s]: %s', $dsn, $e->getMessage()); + throw new Exception($error); + } + } + + /** + * Add an epub to the db + * + * @param string Epub file name + * @throws Exception if error + * + * @return void + */ + public function AddEpub($inFileName) + { + // Load the book infos + $bookInfos = new BookInfos(); + $bookInfos->LoadFromEpub($inFileName); + // Add the book + $this->AddBook($bookInfos); + } + + /** + * Add a new book into the db + * + * @param object BookInfo object + * @throws Exception if error + * + * @return void + */ + private function AddBook($inBookInfo) + { + $sql = 'insert into books(title, sort, uuid, path) values(:title, :sort, :uuid, :path)'; + $stmt = $this->mDb->prepare($sql); + $stmt->bindParam(':title', $inBookInfo->mTitle); + $stmt->bindParam(':sort', $inBookInfo->mTitleSort); + $stmt->bindParam(':uuid', $inBookInfo->mUuid); + $stmt->bindParam(':path', $inBookInfo->mPath); + $stmt->execute(); + // Get the book id + $sql = 'select id, title from books where uuid=:uuid'; + $stmt = $this->mDb->prepare($sql); + $stmt->bindParam(':uuid', $inBookInfo->mUuid); + $stmt->execute(); + $idBook = null; + while ($post = $stmt->fetchObject()) { + if (!isset($idBook)) { + $idBook = $post->id; + } + else { + $error = sprintf('Multiple book id for uuid: %s (already in title "%s")', $inBookInfo->mUuid, $post->title); + throw new Exception($error); + } + } + if (!isset($idBook)) { + $error = sprintf('Cannot find book id for uuid: %s', $inBookInfo->mUuid); + throw new Exception($error); + } + // Add the book formats + $sql = 'insert into data(book, format, name, uncompressed_size) values(:idBook, :format, :name, 0)'; + $stmt = $this->mDb->prepare($sql); + $stmt->bindParam(':idBook', $idBook, PDO::PARAM_INT); + $stmt->bindParam(':format', $inBookInfo->mFormat); + $stmt->bindParam(':name', $inBookInfo->mName); + $stmt->execute(); + // Add the book identifiers + if (!empty($inBookInfo->mUri)) { + $sql = 'insert into identifiers(book, type, val) values(:idBook, :type, :value)'; + $stmt = $this->mDb->prepare($sql); + $type = 'URI'; + $stmt->bindParam(':idBook', $idBook, PDO::PARAM_INT); + $stmt->bindParam(':type', $type); + $stmt->bindParam(':value', $inBookInfo->mUri); + $stmt->execute(); + } + // Add the authors in the db + foreach ($inBookInfo->mAuthors as $author) { + // Get the author id + $sql = 'select id from authors where name=:author'; + $stmt = $this->mDb->prepare($sql); + $stmt->bindParam(':author', $author); + $stmt->execute(); + $post = $stmt->fetchObject(); + if ($post) { + $idAuthor = $post->id; + } + else { + // Add a new author + $sql = 'insert into authors(name, sort) values(:author, :sort)'; + $stmt = $this->mDb->prepare($sql); + $stmt->bindParam(':author', $author); + $stmt->bindParam(':sort', $author); + $stmt->execute(); + // Get the author id + $sql = 'select id from authors where name=:author'; + $stmt = $this->mDb->prepare($sql); + $stmt->bindParam(':author', $author); + $stmt->execute(); + $idAuthor = null; + while ($post = $stmt->fetchObject()) { + if (!isset($idAuthor)) { + $idAuthor = $post->id; + } + else { + $error = sprintf('Multiple authors for name: %s', $author); + throw new Exception($error); + } + } + if (!isset($idAuthor)) { + $error = sprintf('Cannot find author id for name: %s', $author); + throw new Exception($error); + } + // Add the book author link + $sql = 'insert into books_authors_link(book, author) values(:idBook, :idAuthor)'; + $stmt = $this->mDb->prepare($sql); + $stmt->bindParam(':idBook', $idBook, PDO::PARAM_INT); + $stmt->bindParam(':idAuthor', $idAuthor, PDO::PARAM_INT); + $stmt->execute(); + } + } + } + + /** + * Check database for debug + * + * @return void + */ + private function CheckDatabase() + { + // Retrieve some infos for check only + $sql = 'select id, title, sort from books'; + $stmt = $this->mDb->prepare($sql); + $stmt->execute(); + while ($post = $stmt->fetchObject()) { + $id = $post->id; + $title = $post->title; + $sort = $post->sort; + } + } + +} + +?> diff --git a/resources/epub-loader/CsvExport.class.php b/resources/epub-loader/CsvExport.class.php new file mode 100644 index 0000000..20b58c4 --- /dev/null +++ b/resources/epub-loader/CsvExport.class.php @@ -0,0 +1,69 @@ + + */ + +require_once(realpath(dirname(__FILE__)) . '/BaseExport.class.php'); + +class CsvExport extends BaseExport +{ + private $mLines = null; + + const CsvSeparator = "\t"; + + /** + * Open an export file (or create if file does not exist) + * + * @param string Export file name + * @param boolean Force file creation + */ + public function __construct($inFileName) + { + $this->mSearch = array("\r", "\n", self::CsvSeparator); + $this->mReplace = array('', '
', ''); + + // Init container + $this->mLines = array(); + + parent::__construct($inFileName); + } + + /** + * Add the current properties into the export content + * and reset the properties + */ + public function AddContent() + { + $text = ''; + foreach ($this->mProperties as $key => $value) { + $info = ''; + if (is_array($value)) { + foreach ($value as $value1) { + $text .= $value1 . self::CsvSeparator; + } + continue; + } + else { + $info = $value; + } + $text .= $info . self::CsvSeparator; + } + + $this->mLines[] = $text; + + $this->ClearProperties(); + } + + protected function GetContent() + { + $text = implode("\n", $this->mLines) . "\n"; + + return $text; + } + +} + +?> diff --git a/resources/epub-loader/ZipFile.class.php b/resources/epub-loader/ZipFile.class.php new file mode 100644 index 0000000..abcbd82 --- /dev/null +++ b/resources/epub-loader/ZipFile.class.php @@ -0,0 +1,119 @@ + + */ + +/** + * ZipFile class allows to open files inside a zip file with the standard php zip functions + */ +class ZipFile +{ + private $mZip; + private $mEntries; + + public function __construct() + { + $this->mZip = null; + $this->mEntries = null; + } + + /** + * Destructor + */ + public function __destruct() + { + $this->Close(); + } + + /** + * Open a zip file and read it's entries + * + * @param string $inFileName + * @return boolean True if zip file has been correctly opended, else false + */ + public function Open($inFileName) + { + $this->Close(); + + $this->mZip = zip_open($inFileName); + if (!$this->mZip) { + return false; + } + + $this->mEntries = array(); + + while ($entry = zip_read($this->mZip)) { + $fileName = zip_entry_name($entry); + $this->mEntries[$fileName] = $entry; + } + + return true; + } + + /** + * Check if a file exist in the zip entries + * + * @param string $inFileName File to search + * + * @return boolean True if the file exist, else false + */ + public function FileExists($inFileName) + { + if (!isset($this->mZip)) { + return false; + } + + if (!isset($this->mEntries[$inFileName])) { + return false; + } + + return true; + } + + /** + * Read the content of a file in the zip entries + * + * @param string $inFileName File to search + * + * @return mixed File content the file exist, else false + */ + public function FileRead($inFileName) + { + if (!isset($this->mZip)) { + return false; + } + + if (!isset($this->mEntries[$inFileName])) { + return false; + } + + $entry = $this->mEntries[$inFileName]; + if (!zip_entry_open($this->mZip, $entry)) { + return false; + } + $data = zip_entry_read($entry, zip_entry_filesize($entry)); + zip_entry_close($entry); + + return $data; + } + + /** + * Close the zip file + * + * @return void + */ + public function Close() + { + if (!isset($this->mZip)) { + return false; + } + + zip_close($this->mZip); + } + +} + +?> \ No newline at end of file diff --git a/resources/epub-loader/metadata_sqlite.sql b/resources/epub-loader/metadata_sqlite.sql new file mode 100644 index 0000000..83f55c2 --- /dev/null +++ b/resources/epub-loader/metadata_sqlite.sql @@ -0,0 +1,549 @@ +CREATE TABLE authors ( id INTEGER PRIMARY KEY, + name TEXT NOT NULL COLLATE NOCASE, + sort TEXT COLLATE NOCASE, + link TEXT NOT NULL DEFAULT "", + UNIQUE(name) + ); +CREATE TABLE books ( id INTEGER PRIMARY KEY AUTOINCREMENT, + title TEXT NOT NULL DEFAULT 'Unknown' COLLATE NOCASE, + sort TEXT COLLATE NOCASE, + timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + pubdate TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + series_index REAL NOT NULL DEFAULT 1.0, + author_sort TEXT COLLATE NOCASE, + isbn TEXT DEFAULT "" COLLATE NOCASE, + lccn TEXT DEFAULT "" COLLATE NOCASE, + path TEXT NOT NULL DEFAULT "", + flags INTEGER NOT NULL DEFAULT 1, + uuid TEXT, + has_cover BOOL DEFAULT 0, + last_modified TIMESTAMP NOT NULL DEFAULT "2000-01-01 00:00:00+00:00"); +CREATE TABLE books_authors_link ( id INTEGER PRIMARY KEY, + book INTEGER NOT NULL, + author INTEGER NOT NULL, + UNIQUE(book, author) + ); +CREATE TABLE books_languages_link ( id INTEGER PRIMARY KEY, + book INTEGER NOT NULL, + lang_code INTEGER NOT NULL, + item_order INTEGER NOT NULL DEFAULT 0, + UNIQUE(book, lang_code) + ); +CREATE TABLE books_plugin_data(id INTEGER PRIMARY KEY, + book INTEGER NON NULL, + name TEXT NON NULL, + val TEXT NON NULL, + UNIQUE(book,name)); +CREATE TABLE books_publishers_link ( id INTEGER PRIMARY KEY, + book INTEGER NOT NULL, + publisher INTEGER NOT NULL, + UNIQUE(book) + ); +CREATE TABLE books_ratings_link ( id INTEGER PRIMARY KEY, + book INTEGER NOT NULL, + rating INTEGER NOT NULL, + UNIQUE(book, rating) + ); +CREATE TABLE books_series_link ( id INTEGER PRIMARY KEY, + book INTEGER NOT NULL, + series INTEGER NOT NULL, + UNIQUE(book) + ); +CREATE TABLE books_tags_link ( id INTEGER PRIMARY KEY, + book INTEGER NOT NULL, + tag INTEGER NOT NULL, + UNIQUE(book, tag) + ); +CREATE TABLE comments ( id INTEGER PRIMARY KEY, + book INTEGER NON NULL, + text TEXT NON NULL COLLATE NOCASE, + UNIQUE(book) + ); +CREATE TABLE conversion_options ( id INTEGER PRIMARY KEY, + format TEXT NOT NULL COLLATE NOCASE, + book INTEGER, + data BLOB NOT NULL, + UNIQUE(format,book) + ); +CREATE TABLE custom_columns ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + label TEXT NOT NULL, + name TEXT NOT NULL, + datatype TEXT NOT NULL, + mark_for_delete BOOL DEFAULT 0 NOT NULL, + editable BOOL DEFAULT 1 NOT NULL, + display TEXT DEFAULT "{}" NOT NULL, + is_multiple BOOL DEFAULT 0 NOT NULL, + normalized BOOL NOT NULL, + UNIQUE(label) + ); +CREATE TABLE data ( id INTEGER PRIMARY KEY, + book INTEGER NON NULL, + format TEXT NON NULL COLLATE NOCASE, + uncompressed_size INTEGER NON NULL, + name TEXT NON NULL, + UNIQUE(book, format) +); +CREATE TABLE feeds ( id INTEGER PRIMARY KEY, + title TEXT NOT NULL, + script TEXT NOT NULL, + UNIQUE(title) + ); +CREATE TABLE identifiers ( id INTEGER PRIMARY KEY, + book INTEGER NON NULL, + type TEXT NON NULL DEFAULT "isbn" COLLATE NOCASE, + val TEXT NON NULL COLLATE NOCASE, + UNIQUE(book, type) + ); +CREATE TABLE languages ( id INTEGER PRIMARY KEY, + lang_code TEXT NON NULL COLLATE NOCASE, + UNIQUE(lang_code) + ); +CREATE TABLE library_id ( id INTEGER PRIMARY KEY, + uuid TEXT NOT NULL, + UNIQUE(uuid) + ); +CREATE TABLE metadata_dirtied(id INTEGER PRIMARY KEY, + book INTEGER NOT NULL, + UNIQUE(book)); +CREATE TABLE preferences(id INTEGER PRIMARY KEY, + key TEXT NON NULL, + val TEXT NON NULL, + UNIQUE(key)); +CREATE TABLE publishers ( id INTEGER PRIMARY KEY, + name TEXT NOT NULL COLLATE NOCASE, + sort TEXT COLLATE NOCASE, + UNIQUE(name) + ); +CREATE TABLE ratings ( id INTEGER PRIMARY KEY, + rating INTEGER CHECK(rating > -1 AND rating < 11), + UNIQUE (rating) + ); +CREATE TABLE series ( id INTEGER PRIMARY KEY, + name TEXT NOT NULL COLLATE NOCASE, + sort TEXT COLLATE NOCASE, + UNIQUE (name) + ); +CREATE TABLE tags ( id INTEGER PRIMARY KEY, + name TEXT NOT NULL COLLATE NOCASE, + UNIQUE (name) + ); +CREATE VIEW meta AS + SELECT id, title, + (SELECT sortconcat(bal.id, name) FROM books_authors_link AS bal JOIN authors ON(author = authors.id) WHERE book = books.id) authors, + (SELECT name FROM publishers WHERE publishers.id IN (SELECT publisher from books_publishers_link WHERE book=books.id)) publisher, + (SELECT rating FROM ratings WHERE ratings.id IN (SELECT rating from books_ratings_link WHERE book=books.id)) rating, + timestamp, + (SELECT MAX(uncompressed_size) FROM data WHERE book=books.id) size, + (SELECT concat(name) FROM tags WHERE tags.id IN (SELECT tag from books_tags_link WHERE book=books.id)) tags, + (SELECT text FROM comments WHERE book=books.id) comments, + (SELECT name FROM series WHERE series.id IN (SELECT series FROM books_series_link WHERE book=books.id)) series, + series_index, + sort, + author_sort, + (SELECT concat(format) FROM data WHERE data.book=books.id) formats, + isbn, + path, + lccn, + pubdate, + flags, + uuid + FROM books; +CREATE VIEW tag_browser_authors AS SELECT + id, + name, + (SELECT COUNT(id) FROM books_authors_link WHERE author=authors.id) count, + (SELECT AVG(ratings.rating) + FROM books_authors_link AS tl, books_ratings_link AS bl, ratings + WHERE tl.author=authors.id AND bl.book=tl.book AND + ratings.id = bl.rating AND ratings.rating <> 0) avg_rating, + sort AS sort + FROM authors; +CREATE VIEW tag_browser_filtered_authors AS SELECT + id, + name, + (SELECT COUNT(books_authors_link.id) FROM books_authors_link WHERE + author=authors.id AND books_list_filter(book)) count, + (SELECT AVG(ratings.rating) + FROM books_authors_link AS tl, books_ratings_link AS bl, ratings + WHERE tl.author=authors.id AND bl.book=tl.book AND + ratings.id = bl.rating AND ratings.rating <> 0 AND + books_list_filter(bl.book)) avg_rating, + sort AS sort + FROM authors; +CREATE VIEW tag_browser_filtered_publishers AS SELECT + id, + name, + (SELECT COUNT(books_publishers_link.id) FROM books_publishers_link WHERE + publisher=publishers.id AND books_list_filter(book)) count, + (SELECT AVG(ratings.rating) + FROM books_publishers_link AS tl, books_ratings_link AS bl, ratings + WHERE tl.publisher=publishers.id AND bl.book=tl.book AND + ratings.id = bl.rating AND ratings.rating <> 0 AND + books_list_filter(bl.book)) avg_rating, + name AS sort + FROM publishers; +CREATE VIEW tag_browser_filtered_ratings AS SELECT + id, + rating, + (SELECT COUNT(books_ratings_link.id) FROM books_ratings_link WHERE + rating=ratings.id AND books_list_filter(book)) count, + (SELECT AVG(ratings.rating) + FROM books_ratings_link AS tl, books_ratings_link AS bl, ratings + WHERE tl.rating=ratings.id AND bl.book=tl.book AND + ratings.id = bl.rating AND ratings.rating <> 0 AND + books_list_filter(bl.book)) avg_rating, + rating AS sort + FROM ratings; +CREATE VIEW tag_browser_filtered_series AS SELECT + id, + name, + (SELECT COUNT(books_series_link.id) FROM books_series_link WHERE + series=series.id AND books_list_filter(book)) count, + (SELECT AVG(ratings.rating) + FROM books_series_link AS tl, books_ratings_link AS bl, ratings + WHERE tl.series=series.id AND bl.book=tl.book AND + ratings.id = bl.rating AND ratings.rating <> 0 AND + books_list_filter(bl.book)) avg_rating, + (title_sort(name)) AS sort + FROM series; +CREATE VIEW tag_browser_filtered_tags AS SELECT + id, + name, + (SELECT COUNT(books_tags_link.id) FROM books_tags_link WHERE + tag=tags.id AND books_list_filter(book)) count, + (SELECT AVG(ratings.rating) + FROM books_tags_link AS tl, books_ratings_link AS bl, ratings + WHERE tl.tag=tags.id AND bl.book=tl.book AND + ratings.id = bl.rating AND ratings.rating <> 0 AND + books_list_filter(bl.book)) avg_rating, + name AS sort + FROM tags; +CREATE VIEW tag_browser_publishers AS SELECT + id, + name, + (SELECT COUNT(id) FROM books_publishers_link WHERE publisher=publishers.id) count, + (SELECT AVG(ratings.rating) + FROM books_publishers_link AS tl, books_ratings_link AS bl, ratings + WHERE tl.publisher=publishers.id AND bl.book=tl.book AND + ratings.id = bl.rating AND ratings.rating <> 0) avg_rating, + name AS sort + FROM publishers; +CREATE VIEW tag_browser_ratings AS SELECT + id, + rating, + (SELECT COUNT(id) FROM books_ratings_link WHERE rating=ratings.id) count, + (SELECT AVG(ratings.rating) + FROM books_ratings_link AS tl, books_ratings_link AS bl, ratings + WHERE tl.rating=ratings.id AND bl.book=tl.book AND + ratings.id = bl.rating AND ratings.rating <> 0) avg_rating, + rating AS sort + FROM ratings; +CREATE VIEW tag_browser_series AS SELECT + id, + name, + (SELECT COUNT(id) FROM books_series_link WHERE series=series.id) count, + (SELECT AVG(ratings.rating) + FROM books_series_link AS tl, books_ratings_link AS bl, ratings + WHERE tl.series=series.id AND bl.book=tl.book AND + ratings.id = bl.rating AND ratings.rating <> 0) avg_rating, + (title_sort(name)) AS sort + FROM series; +CREATE VIEW tag_browser_tags AS SELECT + id, + name, + (SELECT COUNT(id) FROM books_tags_link WHERE tag=tags.id) count, + (SELECT AVG(ratings.rating) + FROM books_tags_link AS tl, books_ratings_link AS bl, ratings + WHERE tl.tag=tags.id AND bl.book=tl.book AND + ratings.id = bl.rating AND ratings.rating <> 0) avg_rating, + name AS sort + FROM tags; +CREATE INDEX authors_idx ON books (author_sort COLLATE NOCASE); +CREATE INDEX books_authors_link_aidx ON books_authors_link (author); +CREATE INDEX books_authors_link_bidx ON books_authors_link (book); +CREATE INDEX books_idx ON books (sort COLLATE NOCASE); +CREATE INDEX books_languages_link_aidx ON books_languages_link (lang_code); +CREATE INDEX books_languages_link_bidx ON books_languages_link (book); +CREATE INDEX books_publishers_link_aidx ON books_publishers_link (publisher); +CREATE INDEX books_publishers_link_bidx ON books_publishers_link (book); +CREATE INDEX books_ratings_link_aidx ON books_ratings_link (rating); +CREATE INDEX books_ratings_link_bidx ON books_ratings_link (book); +CREATE INDEX books_series_link_aidx ON books_series_link (series); +CREATE INDEX books_series_link_bidx ON books_series_link (book); +CREATE INDEX books_tags_link_aidx ON books_tags_link (tag); +CREATE INDEX books_tags_link_bidx ON books_tags_link (book); +CREATE INDEX comments_idx ON comments (book); +CREATE INDEX conversion_options_idx_a ON conversion_options (format COLLATE NOCASE); +CREATE INDEX conversion_options_idx_b ON conversion_options (book); +CREATE INDEX custom_columns_idx ON custom_columns (label); +CREATE INDEX data_idx ON data (book); +CREATE INDEX formats_idx ON data (format); +CREATE INDEX languages_idx ON languages (lang_code COLLATE NOCASE); +CREATE INDEX publishers_idx ON publishers (name COLLATE NOCASE); +CREATE INDEX series_idx ON series (name COLLATE NOCASE); +CREATE INDEX tags_idx ON tags (name COLLATE NOCASE); +CREATE TRIGGER books_delete_trg + AFTER DELETE ON books + BEGIN + DELETE FROM books_authors_link WHERE book=OLD.id; + DELETE FROM books_publishers_link WHERE book=OLD.id; + DELETE FROM books_ratings_link WHERE book=OLD.id; + DELETE FROM books_series_link WHERE book=OLD.id; + DELETE FROM books_tags_link WHERE book=OLD.id; + DELETE FROM books_languages_link WHERE book=OLD.id; + DELETE FROM data WHERE book=OLD.id; + DELETE FROM comments WHERE book=OLD.id; + DELETE FROM conversion_options WHERE book=OLD.id; + DELETE FROM books_plugin_data WHERE book=OLD.id; + DELETE FROM identifiers WHERE book=OLD.id; + END; +CREATE TRIGGER books_insert_trg AFTER INSERT ON books + BEGIN + UPDATE books SET sort=title_sort(NEW.title),uuid=uuid4() WHERE id=NEW.id; + END; +CREATE TRIGGER books_update_trg + AFTER UPDATE ON books + BEGIN + UPDATE books SET sort=title_sort(NEW.title) + WHERE id=NEW.id AND OLD.title <> NEW.title; + END; +CREATE TRIGGER fkc_comments_insert + BEFORE INSERT ON comments + BEGIN + SELECT CASE + WHEN (SELECT id from books WHERE id=NEW.book) IS NULL + THEN RAISE(ABORT, 'Foreign key violation: book not in books') + END; + END; +CREATE TRIGGER fkc_comments_update + BEFORE UPDATE OF book ON comments + BEGIN + SELECT CASE + WHEN (SELECT id from books WHERE id=NEW.book) IS NULL + THEN RAISE(ABORT, 'Foreign key violation: book not in books') + END; + END; +CREATE TRIGGER fkc_data_insert + BEFORE INSERT ON data + BEGIN + SELECT CASE + WHEN (SELECT id from books WHERE id=NEW.book) IS NULL + THEN RAISE(ABORT, 'Foreign key violation: book not in books') + END; + END; +CREATE TRIGGER fkc_data_update + BEFORE UPDATE OF book ON data + BEGIN + SELECT CASE + WHEN (SELECT id from books WHERE id=NEW.book) IS NULL + THEN RAISE(ABORT, 'Foreign key violation: book not in books') + END; + END; +CREATE TRIGGER fkc_delete_on_authors + BEFORE DELETE ON authors + BEGIN + SELECT CASE + WHEN (SELECT COUNT(id) FROM books_authors_link WHERE author=OLD.id) > 0 + THEN RAISE(ABORT, 'Foreign key violation: authors is still referenced') + END; + END; +CREATE TRIGGER fkc_delete_on_languages + BEFORE DELETE ON languages + BEGIN + SELECT CASE + WHEN (SELECT COUNT(id) FROM books_languages_link WHERE lang_code=OLD.id) > 0 + THEN RAISE(ABORT, 'Foreign key violation: language is still referenced') + END; + END; +CREATE TRIGGER fkc_delete_on_languages_link + BEFORE INSERT ON books_languages_link + BEGIN + SELECT CASE + WHEN (SELECT id from books WHERE id=NEW.book) IS NULL + THEN RAISE(ABORT, 'Foreign key violation: book not in books') + WHEN (SELECT id from languages WHERE id=NEW.lang_code) IS NULL + THEN RAISE(ABORT, 'Foreign key violation: lang_code not in languages') + END; + END; +CREATE TRIGGER fkc_delete_on_publishers + BEFORE DELETE ON publishers + BEGIN + SELECT CASE + WHEN (SELECT COUNT(id) FROM books_publishers_link WHERE publisher=OLD.id) > 0 + THEN RAISE(ABORT, 'Foreign key violation: publishers is still referenced') + END; + END; +CREATE TRIGGER fkc_delete_on_series + BEFORE DELETE ON series + BEGIN + SELECT CASE + WHEN (SELECT COUNT(id) FROM books_series_link WHERE series=OLD.id) > 0 + THEN RAISE(ABORT, 'Foreign key violation: series is still referenced') + END; + END; +CREATE TRIGGER fkc_delete_on_tags + BEFORE DELETE ON tags + BEGIN + SELECT CASE + WHEN (SELECT COUNT(id) FROM books_tags_link WHERE tag=OLD.id) > 0 + THEN RAISE(ABORT, 'Foreign key violation: tags is still referenced') + END; + END; +CREATE TRIGGER fkc_insert_books_authors_link + BEFORE INSERT ON books_authors_link + BEGIN + SELECT CASE + WHEN (SELECT id from books WHERE id=NEW.book) IS NULL + THEN RAISE(ABORT, 'Foreign key violation: book not in books') + WHEN (SELECT id from authors WHERE id=NEW.author) IS NULL + THEN RAISE(ABORT, 'Foreign key violation: author not in authors') + END; + END; +CREATE TRIGGER fkc_insert_books_publishers_link + BEFORE INSERT ON books_publishers_link + BEGIN + SELECT CASE + WHEN (SELECT id from books WHERE id=NEW.book) IS NULL + THEN RAISE(ABORT, 'Foreign key violation: book not in books') + WHEN (SELECT id from publishers WHERE id=NEW.publisher) IS NULL + THEN RAISE(ABORT, 'Foreign key violation: publisher not in publishers') + END; + END; +CREATE TRIGGER fkc_insert_books_ratings_link + BEFORE INSERT ON books_ratings_link + BEGIN + SELECT CASE + WHEN (SELECT id from books WHERE id=NEW.book) IS NULL + THEN RAISE(ABORT, 'Foreign key violation: book not in books') + WHEN (SELECT id from ratings WHERE id=NEW.rating) IS NULL + THEN RAISE(ABORT, 'Foreign key violation: rating not in ratings') + END; + END; +CREATE TRIGGER fkc_insert_books_series_link + BEFORE INSERT ON books_series_link + BEGIN + SELECT CASE + WHEN (SELECT id from books WHERE id=NEW.book) IS NULL + THEN RAISE(ABORT, 'Foreign key violation: book not in books') + WHEN (SELECT id from series WHERE id=NEW.series) IS NULL + THEN RAISE(ABORT, 'Foreign key violation: series not in series') + END; + END; +CREATE TRIGGER fkc_insert_books_tags_link + BEFORE INSERT ON books_tags_link + BEGIN + SELECT CASE + WHEN (SELECT id from books WHERE id=NEW.book) IS NULL + THEN RAISE(ABORT, 'Foreign key violation: book not in books') + WHEN (SELECT id from tags WHERE id=NEW.tag) IS NULL + THEN RAISE(ABORT, 'Foreign key violation: tag not in tags') + END; + END; +CREATE TRIGGER fkc_update_books_authors_link_a + BEFORE UPDATE OF book ON books_authors_link + BEGIN + SELECT CASE + WHEN (SELECT id from books WHERE id=NEW.book) IS NULL + THEN RAISE(ABORT, 'Foreign key violation: book not in books') + END; + END; +CREATE TRIGGER fkc_update_books_authors_link_b + BEFORE UPDATE OF author ON books_authors_link + BEGIN + SELECT CASE + WHEN (SELECT id from authors WHERE id=NEW.author) IS NULL + THEN RAISE(ABORT, 'Foreign key violation: author not in authors') + END; + END; +CREATE TRIGGER fkc_update_books_languages_link_a + BEFORE UPDATE OF book ON books_languages_link + BEGIN + SELECT CASE + WHEN (SELECT id from books WHERE id=NEW.book) IS NULL + THEN RAISE(ABORT, 'Foreign key violation: book not in books') + END; + END; +CREATE TRIGGER fkc_update_books_languages_link_b + BEFORE UPDATE OF lang_code ON books_languages_link + BEGIN + SELECT CASE + WHEN (SELECT id from languages WHERE id=NEW.lang_code) IS NULL + THEN RAISE(ABORT, 'Foreign key violation: lang_code not in languages') + END; + END; +CREATE TRIGGER fkc_update_books_publishers_link_a + BEFORE UPDATE OF book ON books_publishers_link + BEGIN + SELECT CASE + WHEN (SELECT id from books WHERE id=NEW.book) IS NULL + THEN RAISE(ABORT, 'Foreign key violation: book not in books') + END; + END; +CREATE TRIGGER fkc_update_books_publishers_link_b + BEFORE UPDATE OF publisher ON books_publishers_link + BEGIN + SELECT CASE + WHEN (SELECT id from publishers WHERE id=NEW.publisher) IS NULL + THEN RAISE(ABORT, 'Foreign key violation: publisher not in publishers') + END; + END; +CREATE TRIGGER fkc_update_books_ratings_link_a + BEFORE UPDATE OF book ON books_ratings_link + BEGIN + SELECT CASE + WHEN (SELECT id from books WHERE id=NEW.book) IS NULL + THEN RAISE(ABORT, 'Foreign key violation: book not in books') + END; + END; +CREATE TRIGGER fkc_update_books_ratings_link_b + BEFORE UPDATE OF rating ON books_ratings_link + BEGIN + SELECT CASE + WHEN (SELECT id from ratings WHERE id=NEW.rating) IS NULL + THEN RAISE(ABORT, 'Foreign key violation: rating not in ratings') + END; + END; +CREATE TRIGGER fkc_update_books_series_link_a + BEFORE UPDATE OF book ON books_series_link + BEGIN + SELECT CASE + WHEN (SELECT id from books WHERE id=NEW.book) IS NULL + THEN RAISE(ABORT, 'Foreign key violation: book not in books') + END; + END; +CREATE TRIGGER fkc_update_books_series_link_b + BEFORE UPDATE OF series ON books_series_link + BEGIN + SELECT CASE + WHEN (SELECT id from series WHERE id=NEW.series) IS NULL + THEN RAISE(ABORT, 'Foreign key violation: series not in series') + END; + END; +CREATE TRIGGER fkc_update_books_tags_link_a + BEFORE UPDATE OF book ON books_tags_link + BEGIN + SELECT CASE + WHEN (SELECT id from books WHERE id=NEW.book) IS NULL + THEN RAISE(ABORT, 'Foreign key violation: book not in books') + END; + END; +CREATE TRIGGER fkc_update_books_tags_link_b + BEFORE UPDATE OF tag ON books_tags_link + BEGIN + SELECT CASE + WHEN (SELECT id from tags WHERE id=NEW.tag) IS NULL + THEN RAISE(ABORT, 'Foreign key violation: tag not in tags') + END; + END; +CREATE TRIGGER series_insert_trg + AFTER INSERT ON series + BEGIN + UPDATE series SET sort=NEW.name WHERE id=NEW.id; + END; +CREATE TRIGGER series_update_trg + AFTER UPDATE ON series + BEGIN + UPDATE series SET sort=NEW.name WHERE id=NEW.id; + END; +pragma user_version=21;