## Copyright (C) 2006 Kovid Goyal kovid@kovidgoyal.net ## 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., ## 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. ''' Backend that implements storage of ebooks in an sqlite database. ''' import sqlite3 as sqlite import datetime, re, os, cPickle, traceback from zlib import compress, decompress from libprs500.ebooks.metadata.meta import set_metadata from libprs500.ebooks.metadata import MetaInformation class Concatenate(object): '''String concatenation aggregator for sqlite''' def __init__(self, sep=','): self.sep = sep self.ans = '' def step(self, value): self.ans += value + self.sep def finalize(self): if not self.ans: return None if self.sep: return self.ans[:-len(self.sep)] return self.ans _lock_file = None class DatabaseLocked(Exception): pass def _lock(path): path = os.path.join(os.path.dirname(path), '.'+os.path.basename(path)+'.lock') global _lock_file if _lock_file is not None: raise DatabaseLocked('Database already locked in this instance.') try: _lock_file = open(path, 'wb') except IOError: raise DatabaseLocked('Database in use by another instance') try: import fcntl, errno try: fcntl.lockf(_lock_file.fileno(), fcntl.LOCK_EX|fcntl.LOCK_NB) except IOError, err: _lock_file = None if err.errno in (errno.EACCES, errno.EAGAIN): raise DatabaseLocked('Database in use by another instance') except ImportError: try: import msvcrt try: msvcrt.locking(_lock_file.fileno(), msvcrt.LK_NBLCK, 1) except IOError: _lock_file = None raise DatabaseLocked('Database in use by another instance') except ImportError: pass def _connect(path): if isinstance(path, unicode): path = path.encode('utf-8') _lock(path) conn = sqlite.connect(path, detect_types=sqlite.PARSE_DECLTYPES|sqlite.PARSE_COLNAMES) conn.row_factory = lambda cursor, row : list(row) conn.create_aggregate('concat', 1, Concatenate) title_pat = re.compile('^(A|The|An\s+)', re.IGNORECASE) def title_sort(title): match = title_pat.search(title) if match: prep = match.group(1) title = title.replace(prep, '') + ', ' + prep return title.strip() conn.create_function('title_sort', 1, title_sort) return conn class LibraryDatabase(object): @staticmethod def books_in_old_database(path): ''' Iterator over the books in the old pre 0.4.0 database. ''' conn = sqlite.connect(path) cur = conn.execute('select * from books_meta order by id;') book = cur.fetchone() while book: id = book[0] meta = { 'title':book[1], 'authors':book[2], 'publisher':book[3], 'tags':book[5], 'comments':book[7], 'rating':book[8], 'timestamp':datetime.datetime.strptime(book[6], '%Y-%m-%d %H:%M:%S'), } cover = {} query = conn.execute('select uncompressed_size, data from books_cover where id=?', (id,)).fetchone() if query: cover = {'uncompressed_size': query[0], 'data': query[1]} query = conn.execute('select extension, uncompressed_size, data from books_data where id=?', (id,)).fetchall() formats = {} for row in query: formats[row[0]] = {'uncompressed_size':row[1], 'data':row[2]} yield meta, cover, formats book = cur.fetchone() @staticmethod def sizeof_old_database(path): conn = sqlite.connect(path) ans = conn.execute('SELECT COUNT(id) from books_meta').fetchone()[0] conn.close() return ans @staticmethod def import_old_database(path, conn, progress=None): count = 0 for book, cover, formats in LibraryDatabase.books_in_old_database(path): authors = book['authors'] if not authors: authors = 'Unknown' obj = conn.execute('INSERT INTO books(title, timestamp, author_sort) VALUES (?,?,?)', (book['title'], book['timestamp'], authors)) id = obj.lastrowid authors = authors.split('&') for a in authors: author = conn.execute('SELECT id from authors WHERE name=?', (a,)).fetchone() if author: aid = author[0] else: aid = conn.execute('INSERT INTO authors(name) VALUES (?)', (a,)).lastrowid conn.execute('INSERT INTO books_authors_link(book, author) VALUES (?,?)', (id, aid)) if book['publisher']: candidate = conn.execute('SELECT id from publishers WHERE name=?', (book['publisher'],)).fetchone() pid = candidate[0] if candidate else conn.execute('INSERT INTO publishers(name) VALUES (?)', (book['publisher'],)).lastrowid conn.execute('INSERT INTO books_publishers_link(book, publisher) VALUES (?,?)', (id, pid)) if book['rating']: candidate = conn.execute('SELECT id from ratings WHERE rating=?', (2*book['rating'],)).fetchone() rid = candidate[0] if candidate else conn.execute('INSERT INTO ratings(rating) VALUES (?)', (2*book['rating'],)).lastrowid conn.execute('INSERT INTO books_ratings_link(book, rating) VALUES (?,?)', (id, rid)) tags = book['tags'] if tags: tags = tags.split(',') else: tags = [] for a in tags: a = a.strip() if not a: continue tag = conn.execute('SELECT id from tags WHERE name=?', (a,)).fetchone() if tag: tid = tag[0] else: tid = conn.execute('INSERT INTO tags(name) VALUES (?)', (a,)).lastrowid conn.execute('INSERT INTO books_tags_link(book, tag) VALUES (?,?)', (id, tid)) comments = book['comments'] if comments: conn.execute('INSERT INTO comments(book, text) VALUES (?, ?)', (id, comments)) if cover: conn.execute('INSERT INTO covers(book, uncompressed_size, data) VALUES (?, ?, ?)', (id, cover['uncompressed_size'], cover['data'])) for format in formats.keys(): conn.execute('INSERT INTO data(book, format, uncompressed_size, data) VALUES (?, ?, ?, ?)', (id, format, formats[format]['uncompressed_size'], formats[format]['data'])) conn.commit() count += 1 if progress: progress(count) @staticmethod def create_version1(conn): conn.executescript(\ ''' /**** books table *****/ 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, uri TEXT, series_index INTEGER NOT NULL DEFAULT 1 ); CREATE INDEX books_idx ON books (sort COLLATE NOCASE); CREATE TRIGGER books_insert_trg AFTER INSERT ON books BEGIN UPDATE books SET sort=title_sort(NEW.title) 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; END; /***** authors table *****/ CREATE TABLE authors ( id INTEGER PRIMARY KEY, name TEXT NOT NULL COLLATE NOCASE, sort TEXT COLLATE NOCASE, UNIQUE(name) ); CREATE INDEX authors_idx ON authors (sort COLLATE NOCASE); CREATE TRIGGER authors_insert_trg AFTER INSERT ON authors BEGIN UPDATE authors SET sort=NEW.name WHERE id=NEW.id; END; CREATE TRIGGER authors_update_trg AFTER UPDATE ON authors BEGIN UPDATE authors SET sort=NEW.name WHERE id=NEW.id; END; CREATE TABLE books_authors_link ( id INTEGER PRIMARY KEY, book INTEGER NOT NULL, author INTEGER NOT NULL, UNIQUE(book, author) ); CREATE INDEX books_authors_link_bidx ON books_authors_link (book); CREATE INDEX books_authors_link_aidx ON books_authors_link (author); 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_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_delete_books_authors_link BEFORE DELETE ON authors BEGIN SELECT CASE WHEN (SELECT COUNT(id) FROM books_authors_link WHERE book=OLD.book) > 0 THEN RAISE(ABORT, 'Foreign key violation: author is still referenced') END; END; /***** publishers table *****/ CREATE TABLE publishers ( id INTEGER PRIMARY KEY, name TEXT NOT NULL COLLATE NOCASE, sort TEXT COLLATE NOCASE, UNIQUE(name) ); CREATE INDEX publishers_idx ON publishers (sort COLLATE NOCASE); CREATE TRIGGER publishers_insert_trg AFTER INSERT ON publishers BEGIN UPDATE publishers SET sort=NEW.name WHERE id=NEW.id; END; CREATE TRIGGER publishers_update_trg AFTER UPDATE ON publishers BEGIN UPDATE publishers SET sort=NEW.name WHERE id=NEW.id; END; CREATE TABLE books_publishers_link ( id INTEGER PRIMARY KEY, book INTEGER NOT NULL, publisher INTEGER NOT NULL, UNIQUE(book) ); CREATE INDEX books_publishers_link_bidx ON books_publishers_link (book); CREATE INDEX books_publishers_link_aidx ON books_publishers_link (publisher); 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_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_delete_books_publishers_link BEFORE DELETE ON publishers BEGIN SELECT CASE WHEN (SELECT COUNT(id) FROM books_publishers_link WHERE book=OLD.book) > 0 THEN RAISE(ABORT, 'Foreign key violation: publisher is still referenced') END; END; /***** tags table *****/ CREATE TABLE tags ( id INTEGER PRIMARY KEY, name TEXT NOT NULL COLLATE NOCASE, UNIQUE (name) ); CREATE INDEX tags_idx ON tags (name COLLATE NOCASE); CREATE TABLE books_tags_link ( id INTEGER PRIMARY KEY, book INTEGER NOT NULL, tag INTEGER NOT NULL, UNIQUE(book, tag) ); CREATE INDEX books_tags_link_bidx ON books_tags_link (book); CREATE INDEX books_tags_link_aidx ON books_tags_link (tag); 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_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 fkc_delete_books_tags_link BEFORE DELETE ON tags BEGIN SELECT CASE WHEN (SELECT COUNT(id) FROM books_tags_link WHERE book=OLD.book) > 0 THEN RAISE(ABORT, 'Foreign key violation: tag is still referenced') END; END; /***** series table *****/ CREATE TABLE series ( id INTEGER PRIMARY KEY, name TEXT NOT NULL COLLATE NOCASE, sort TEXT COLLATE NOCASE, UNIQUE (name) ); CREATE INDEX series_idx ON series (sort COLLATE NOCASE); 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; CREATE TABLE books_series_link ( id INTEGER PRIMARY KEY, book INTEGER NOT NULL, series INTEGER NOT NULL, UNIQUE(book) ); CREATE INDEX books_series_link_bidx ON books_series_link (book); CREATE INDEX books_series_link_aidx ON books_series_link (series); 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_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 serie 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_delete_books_series_link BEFORE DELETE ON series BEGIN SELECT CASE WHEN (SELECT COUNT(id) FROM books_series_link WHERE book=OLD.book) > 0 THEN RAISE(ABORT, 'Foreign key violation: series is still referenced') END; END; /**** ratings table ****/ CREATE TABLE ratings ( id INTEGER PRIMARY KEY, rating INTEGER CHECK(rating > -1 AND rating < 11), UNIQUE (rating) ); INSERT INTO ratings (rating) VALUES (0); INSERT INTO ratings (rating) VALUES (1); INSERT INTO ratings (rating) VALUES (2); INSERT INTO ratings (rating) VALUES (3); INSERT INTO ratings (rating) VALUES (4); INSERT INTO ratings (rating) VALUES (5); INSERT INTO ratings (rating) VALUES (6); INSERT INTO ratings (rating) VALUES (7); INSERT INTO ratings (rating) VALUES (8); INSERT INTO ratings (rating) VALUES (9); INSERT INTO ratings (rating) VALUES (10); CREATE TABLE books_ratings_link ( id INTEGER PRIMARY KEY, book INTEGER NOT NULL, rating INTEGER NOT NULL, UNIQUE(book, rating) ); CREATE INDEX books_ratings_link_bidx ON books_ratings_link (book); CREATE INDEX books_ratings_link_aidx ON books_ratings_link (rating); 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_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; /**** data table ****/ CREATE TABLE data ( id INTEGER PRIMARY KEY, book INTEGER NON NULL, format TEXT NON NULL COLLATE NOCASE, uncompressed_size INTEGER NON NULL, data BLOB NON NULL, UNIQUE(book, format) ); CREATE INDEX data_idx ON data (book); 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; /**** covers table ****/ CREATE TABLE covers ( id INTEGER PRIMARY KEY, book INTEGER NON NULL, uncompressed_size INTEGER NON NULL, data BLOB NON NULL, UNIQUE(book) ); CREATE INDEX covers_idx ON covers (book); CREATE TRIGGER fkc_covers_insert BEFORE INSERT ON covers 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_covers_update BEFORE UPDATE OF book ON covers 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; /**** comments table ****/ CREATE TABLE comments ( id INTEGER PRIMARY KEY, book INTEGER NON NULL, text TEXT NON NULL COLLATE NOCASE, UNIQUE(book) ); CREATE INDEX comments_idx ON comments (book); 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; /**** Handle deletion of book ****/ 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 data WHERE book=OLD.id; DELETE FROM covers WHERE book=OLD.id; DELETE FROM comments WHERE book=OLD.id; END; /**** Views ****/ CREATE VIEW meta AS SELECT id, title, (SELECT concat(name) FROM authors WHERE authors.id IN (SELECT author from books_authors_link 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, sort, (SELECT sort FROM authors WHERE authors.id IN (SELECT author from books_authors_link WHERE book=books.id)) authors_sort, (SELECT sort FROM publishers WHERE publishers.id IN (SELECT publisher from books_publishers_link WHERE book=books.id)) publisher_sort FROM books; '''\ ) conn.execute('pragma user_version=1') conn.commit() @staticmethod def upgrade_version1(conn): conn.executescript( ''' /***** authors_sort table *****/ ALTER TABLE books ADD COLUMN author_sort TEXT COLLATE NOCASE; UPDATE books SET author_sort=(SELECT name FROM authors WHERE id=(SELECT author FROM books_authors_link WHERE book=books.id)) WHERE id IN (SELECT id FROM books ORDER BY id); DROP INDEX authors_idx; DROP TRIGGER authors_insert_trg; DROP TRIGGER authors_update_trg; CREATE INDEX authors_idx ON books (author_sort COLLATE NOCASE); CREATE TABLE conversion_options ( id INTEGER PRIMARY KEY, format TEXT NOT NULL COLLATE NOCASE, book INTEGER, data BLOB NOT NULL, UNIQUE(format,book) ); CREATE INDEX conversion_options_idx_a ON conversion_options (format COLLATE NOCASE); CREATE INDEX conversion_options_idx_b ON conversion_options (book); DROP TRIGGER books_delete_trg; 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 data WHERE book=OLD.id; DELETE FROM covers WHERE book=OLD.id; DELETE FROM comments WHERE book=OLD.id; DELETE FROM conversion_options WHERE book=OLD.id; END; DROP VIEW meta; CREATE VIEW meta AS SELECT id, title, (SELECT concat(name) FROM authors WHERE authors.id IN (SELECT author from books_authors_link 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, sort, author_sort FROM books; DROP INDEX publishers_idx; CREATE INDEX publishers_idx ON publishers (name COLLATE NOCASE); DROP TRIGGER publishers_insert_trg; DROP TRIGGER publishers_update_trg; ''' ) conn.execute('pragma user_version=2') conn.commit() @staticmethod def upgrade_version2(conn): conn.executescript( ''' /***** Add ISBN column ******/ ALTER TABLE books ADD COLUMN isbn TEXT DEFAULT "" COLLATE NOCASE; ''') conn.execute('pragma user_version=3') conn.commit() def __del__(self): global _lock_file import os if _lock_file is not None and os.path.exists(_lock_file): os.unlink(_lock_file) def __init__(self, dbpath): self.dbpath = dbpath self.conn = _connect(dbpath) self.cache = [] self.data = [] if self.user_version == 0: # No tables have been created LibraryDatabase.create_version1(self.conn) if self.user_version == 1: # Upgrade to 2 LibraryDatabase.upgrade_version1(self.conn) if self.user_version == 2: # Upgrade to 3 LibraryDatabase.upgrade_version2(self.conn) @apply def user_version(): doc = 'The user version of this database' def fget(self): return self.conn.execute('pragma user_version;').next()[0] return property(doc=doc, fget=fget) def is_empty(self): return not self.conn.execute('SELECT id FROM books LIMIT 1').fetchone() def refresh(self, sort_field, ascending): ''' Rebuild self.data and self.cache. Filter results are lost. ''' FIELDS = {'title' : 'sort', 'authors': 'author_sort', 'publisher': 'publisher', 'size': 'size', 'date': 'timestamp', 'rating': 'rating' } field = FIELDS[sort_field] order = 'ASC' if not ascending: order = 'DESC' self.cache = self.conn.execute('SELECT * from meta ORDER BY '+field+' ' +order).fetchall() self.data = self.cache self.conn.commit() def filter(self, filters, refilter=False): ''' Filter data based on filters. All the filters must match for an item to be accepted. Matching is case independent regexp matching. @param filters: A list of compiled regexps @param refilter: If True filters are applied to the results of the previous filtering. ''' if not filters: self.data = self.data if refilter else self.cache else: matches = [] for item in self.data if refilter else self.cache: keep = True test = ' '.join([item[i] if item[i] else '' for i in (1,2,3,7,8,9)]) for filter in filters: if not filter.search(test): keep = False break if keep: matches.append(item) self.data = matches def rows(self): return len(self.data) if self.data else 0 def id(self, index): return self.data[index][0] def title(self, index): return self.data[index][1] def authors(self, index, index_is_id=False): ''' Authors as a comman separated list or None''' if not index_is_id: return self.data[index][2] return self.conn.execute('SELECT authors FROM meta WHERE id=?',(index,)).fetchone()[0] def isbn(self, id): return self.conn.execute('SELECT isbn FROM books WHERE id=?',(id,)).fetchone()[0] def author_sort(self, index): id = self.id(index) return self.conn.execute('SELECT author_sort FROM books WHERE id=?', (id,)).fetchone()[0] def publisher(self, index): return self.data[index][3] def rating(self, index): return self.data[index][4] def timestamp(self, index): return self.data[index][5] def max_size(self, index): return self.data[index][6] def cover(self, index): '''Cover as a data string or None''' id = self.id(index) data = self.conn.execute('SELECT data FROM covers WHERE book=?', (id,)).fetchone() if not data or not data[0]: return None return(decompress(data[0])) def tags(self, index): '''tags as a comman separated list or None''' id = self.id(index) matches = self.conn.execute('SELECT concat(name) FROM tags WHERE tags.id IN (SELECT tag from books_tags_link WHERE book=?)', (id,)).fetchall() if not matches: return None return matches[0][0] def series_id(self, index): id = self.id(index) ans= self.conn.execute('SELECT series from books_series_link WHERE book=?', (id,)).fetchone() if ans: return ans[0] def series(self, index): id = self.series_id(index) ans = self.conn.execute('SELECT name from series WHERE id=?', (id,)).fetchone() if ans: return ans[0] def series_index(self, index, index_is_id=False): if not index_is_id: index = self.id(index) return self.conn.execute('SELECT series_index FROM books WHERE id=?', (index,)).fetchone()[0] def books_in_series(self, series_id): ''' Return an ordered list of all books in the series. The list contains book ids. ''' ans = self.conn.execute('SELECT book from books_series_link WHERE series=?', (series_id,)).fetchall() if not ans: return [] ans = [id[0] for id in ans] ans.sort(cmp = lambda x, y: cmp(self.series_index(x, True), self.series_index(y, True))) return ans def books_in_series_of(self, index): ''' Return an ordered list of all books in the series that the book indetified by index belongs to. If the book does not belong to a series return an empty list. The list contains book ids. ''' series_id = self.series_id(index) return self.books_in_series(series_id) def comments(self, index): '''Comments as string or None''' id = self.id(index) matches = self.conn.execute('SELECT text FROM comments WHERE book=?', (id,)).fetchall() if not matches: return None return matches[0][0] def formats(self, index, index_is_id=False): ''' Return available formats as a comma separated list ''' id = index if index_is_id else self.id(index) matches = self.conn.execute('SELECT concat(format) FROM data WHERE data.book=?', (id,)).fetchall() if not matches: return None return matches[0][0] def format(self, index, format): id = self.id(index) return decompress(self.conn.execute('SELECT data FROM data WHERE book=? AND format=?', (id, format)).fetchone()[0]) def all_series(self): return [ (i[0], i[1]) for i in \ self.conn.execute('SELECT id, name FROM series').fetchall()] def conversion_options(self, id, format): data = self.conn.execute('SELECT data FROM conversion_options WHERE book=? AND format=?', (id, format.upper())).fetchone() if data: return cPickle.loads(str(data[0])) return None def add_format(self, index, ext, stream, index_is_id=False): ''' Add the format specified by ext. If it already exists it is replaced. ''' id = index if index_is_id else self.id(index) stream.seek(0, 2) usize = stream.tell() stream.seek(0) data = sqlite.Binary(compress(stream.read())) exts = self.formats(index, index_is_id=index_is_id) if not exts: exts = [] if not ext: ext = '' ext = ext.lower() if ext in exts: self.conn.execute('UPDATE data SET data=? WHERE format=? AND book=?', (data, ext, id)) self.conn.execute('UPDATE data SET uncompressed_size=? WHERE format=? AND book=?', (usize, ext, id)) else: self.conn.execute('INSERT INTO data(book, format, uncompressed_size, data) VALUES (?, ?, ?, ?)', (id, ext, usize, data)) self.conn.commit() def remove_format(self, index, ext): id = self.id(index) self.conn.execute('DELETE FROM data WHERE book=? AND format=?', (id, ext.lower())) self.conn.commit() def set(self, row, column, val): ''' Convenience method for setting the title, authors, publisher or rating ''' id = self.data[row][0] cols = {'title' : 1, 'authors': 2, 'publisher': 3, 'rating':4} col = cols[column] self.data[row][col] = val for item in self.cache: if item[0] == id: item[col] = val break if column == 'authors': val = val.split('&,') self.set_authors(id, val) elif column == 'title': self.set_title(id, val) elif column == 'publisher': self.set_publisher(id, val) elif column == 'rating': self.set_rating(id, val) def set_conversion_options(self, id, format, options): data = sqlite.Binary(cPickle.dumps(options)) oid = self.conn.execute('SELECT id FROM conversion_options WHERE book=? AND format=?', (id, format.upper())).fetchone() if oid: self.conn.execute('UPDATE conversion_options SET data=? WHERE id=?', (data, oid[0])) else: self.conn.execute('INSERT INTO conversion_options(book,format,data) VALUES (?,?,?)', (id,format.upper(),data)) self.conn.commit() def set_authors(self, id, authors): ''' @param authors: A list of authors. ''' self.conn.execute('DELETE FROM books_authors_link WHERE book=?',(id,)) for a in authors: if not a: continue a = a.strip() author = self.conn.execute('SELECT id from authors WHERE name=?', (a,)).fetchone() if author: aid = author[0] else: aid = self.conn.execute('INSERT INTO authors(name) VALUES (?)', (a,)).lastrowid self.conn.execute('INSERT INTO books_authors_link(book, author) VALUES (?,?)', (id, aid)) self.conn.commit() def set_author_sort(self, id, sort): self.conn.execute('UPDATE books SET author_sort=? WHERE id=?', (sort, id)) self.conn.commit() def set_title(self, id, title): if not title: return self.conn.execute('UPDATE books SET title=? WHERE id=?', (title, id)) self.conn.commit() def set_isbn(self, id, isbn): self.conn.execute('UPDATE books SET isbn=? WHERE id=?', (isbn, id)) self.conn.commit() def set_publisher(self, id, publisher): self.conn.execute('DELETE FROM books_publishers_link WHERE book=?',(id,)) if publisher: pub = self.conn.execute('SELECT id from publishers WHERE name=?', (publisher,)).fetchone() if pub: aid = pub[0] else: aid = self.conn.execute('INSERT INTO publishers(name) VALUES (?)', (publisher,)).lastrowid self.conn.execute('INSERT INTO books_publishers_link(book, publisher) VALUES (?,?)', (id, aid)) self.conn.commit() def set_comment(self, id, text): self.conn.execute('DELETE FROM comments WHERE book=?', (id,)) self.conn.execute('INSERT INTO comments(book,text) VALUES (?,?)', (id, text)) self.conn.commit() def set_tags(self, id, tags, append=False): ''' @param tags: list of strings @param append: If True existing tags are not removed ''' if not append: self.conn.execute('DELETE FROM books_tags_link WHERE book=?', (id,)) for tag in set(tags): tag = tag.strip() if not tag: continue t = self.conn.execute('SELECT id FROM tags WHERE name=?', (tag,)).fetchone() if t: tid = t[0] else: tid = self.conn.execute('INSERT INTO tags(name) VALUES(?)', (tag,)).lastrowid if not self.conn.execute('SELECT book FROM books_tags_link WHERE book=? AND tag=?', (id, tid)).fetchone(): self.conn.execute('INSERT INTO books_tags_link(book, tag) VALUES (?,?)', (id, tid)) self.conn.commit() def set_series(self, id, series): self.conn.execute('DELETE FROM books_series_link WHERE book=?',(id,)) if series: s = self.conn.execute('SELECT id from series WHERE name=?', (series,)).fetchone() if s: aid = s[0] else: aid = self.conn.execute('INSERT INTO series(name) VALUES (?)', (series,)).lastrowid self.conn.execute('INSERT INTO books_series_link(book, series) VALUES (?,?)', (id, aid)) self.conn.commit() def set_series_index(self, id, idx): self.conn.execute('UPDATE books SET series_index=? WHERE id=?', (int(idx), id)) self.conn.commit() def set_rating(self, id, rating): rating = int(rating) self.conn.execute('DELETE FROM books_ratings_link WHERE book=?',(id,)) rat = self.conn.execute('SELECT id FROM ratings WHERE rating=?', (rating,)).fetchone() rat = rat[0] if rat else self.conn.execute('INSERT INTO ratings(rating) VALUES (?)', (rating,)).lastrowid self.conn.execute('INSERT INTO books_ratings_link(book, rating) VALUES (?,?)', (id, rat)) self.conn.commit() def set_cover(self, id, data): self.conn.execute('DELETE FROM covers where book=?', (id,)) if data: usize = len(data) data = compress(data) self.conn.execute('INSERT INTO covers(book, uncompressed_size, data) VALUES (?,?,?)', (id, usize, sqlite.Binary(data))) self.conn.commit() def add_books(self, paths, formats, metadata, uris=[]): ''' Add a book to the database. self.data and self.cache are not updated. ''' formats, metadata, uris = iter(formats), iter(metadata), iter(uris) for path in paths: mi = metadata.next() try: uri = uris.next() except StopIteration: uri = None if mi.series_index is None: mi.series_index = 1 obj = self.conn.execute('INSERT INTO books(title, uri, series_index) VALUES (?, ?, ?)', (mi.title, uri, mi.series_index)) id = obj.lastrowid self.conn.commit() if not mi.authors: mi.authors = ['Unknown'] authors = [] for a in mi.authors: authors += a.split('&') self.set_authors(id, authors) if mi.author_sort: self.set_author_sort(id, mi.author_sort) if mi.publisher: self.set_publisher(id, mi.publisher) if mi.rating: self.set_rating(id, mi.rating) if mi.series: self.set_series(id, mi.series) stream = open(path, 'rb') stream.seek(0, 2) usize = stream.tell() stream.seek(0) format = formats.next() self.conn.execute('INSERT INTO data(book, format, uncompressed_size, data) VALUES (?,?,?,?)', (id, format, usize, sqlite.Binary(compress(stream.read())))) stream.close() self.conn.commit() def index(self, id, cache=False): data = self.cache if cache else self.data for i in range(len(data)): if data[i][0] == id: return i def delete_book(self, id): ''' Removes book from self.cache, self.data and underlying database. ''' try: self.cache.pop(self.index(id, cache=True)) self.data.pop(self.index(id, cache=False)) except TypeError: #If data and cache are the same object pass self.conn.execute('DELETE FROM books WHERE id=?', (id,)) self.conn.commit() def export_to_dir(self, dir, indices, byauthor=False): if not os.path.exists(dir): raise IOError('Target directory does not exist: '+dir) by_author = {} for index in indices: id = self.id(index) au = self.conn.execute('SELECT author_sort FROM books WHERE id=?', (id,)).fetchone()[0] if not au: au = self.authors(index) if not au: au = 'Unknown' au = au.split(',')[0] if not by_author.has_key(au): by_author[au] = [] by_author[au].append(index) for au in by_author.keys(): apath = os.path.join(dir, au.replace(os.sep, '_').strip()) if not os.path.exists(apath): os.mkdir(apath) for idx in by_author[au]: title = re.sub(r'\s', ' ', self.title(idx)) tpath = os.path.join(apath, title.replace(os.sep, '_').strip()) id = str(self.id(idx)) if not os.path.exists(tpath): os.mkdir(tpath) for fmt in self.formats(idx).split(','): data = self.format(idx, fmt) name = au + ' - ' + title if byauthor else title + ' - ' + au fname = name +'_'+id+'.'+fmt.lower() f = open(os.path.join(tpath, fname.replace(os.sep, '_').strip()), 'w+b') f.write(data) f.flush() aum = self.authors(idx) if aum: aum = aum.split(',') mi = MetaInformation(self.title(idx), aum) mi.author_sort = self.author_sort(idx) try: set_metadata(f, mi, fmt.lower()) except: print 'Error setting metadata for book:', mi.title traceback.print_exc() if __name__ == '__main__': db = LibraryDatabase('/home/kovid/library1.db')