%PDF- %PDF-
Direktori : /lib/calibre/calibre/library/ |
Current File : //lib/calibre/calibre/library/database.py |
__license__ = 'GPL v3' __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>' ''' Backend that implements storage of ebooks in an sqlite database. ''' import sqlite3 as sqlite import datetime, re, sre_constants from zlib import compress, decompress from calibre.ebooks.metadata import MetaInformation from calibre.ebooks.metadata import string_to_authors from calibre.utils.serialize import pickle_loads, pickle_dumps from calibre import isbytestring class Concatenate: '''String concatenation aggregator for sqlite''' def __init__(self, sep=','): self.sep = sep self.ans = '' def step(self, value): if value is not None: self.ans += value + self.sep def finalize(self): try: if not self.ans: return None if self.sep: return self.ans[:-len(self.sep)] return self.ans except Exception: import traceback traceback.print_exc() raise class Connection(sqlite.Connection): def get(self, *args, **kw): ans = self.execute(*args) if not kw.get('all', True): ans = ans.fetchone() if not ans: ans = [None] return ans[0] return ans.fetchall() def _connect(path): if isinstance(path, str): path = path.encode('utf-8') conn = sqlite.connect(path, factory=Connection, 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(r'^(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: @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 = string_to_authors(authors) 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 tag=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 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_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() @staticmethod def upgrade_version3(conn): conn.executescript( ''' /***** Add series_index column to meta view ******/ 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, series_index, sort, author_sort FROM books; ''') conn.execute('pragma user_version=4') conn.commit() @staticmethod def upgrade_version4(conn): conn.executescript( ''' /***** Add formats column to meta view ******/ 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, series_index, sort, author_sort, (SELECT concat(format) FROM data WHERE data.book=books.id) formats FROM books; ''') conn.execute('pragma user_version=5') conn.commit() @staticmethod def upgrade_version5(conn): conn.executescript( ''' DROP TRIGGER fkc_delete_books_tags_link; CREATE TRIGGER fkc_delete_books_tags_link 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: tag is still referenced') END; END; ''') conn.execute('pragma user_version=6') conn.commit() @staticmethod def upgrade_version6(conn): conn.executescript('''CREATE TABLE feeds ( id INTEGER PRIMARY KEY, title TEXT NOT NULL, script TEXT NOT NULL, UNIQUE(title) );''') conn.execute('pragma user_version=7') conn.commit() @staticmethod def upgrade_version7(conn): conn.executescript('''\ DROP TRIGGER fkc_update_books_series_link_b; 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; DROP TRIGGER fkc_delete_books_series_link; CREATE TRIGGER fkc_delete_books_series_link 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; ''' ) conn.execute('pragma user_version=8') conn.commit() @staticmethod def upgrade_version8(conn): conn.execute('DELETE FROM feeds') conn.execute('pragma user_version=9') conn.commit() @staticmethod def upgrade_version9(conn): for id, title in conn.execute('SELECT id, title FROM books').fetchall(): conn.execute('UPDATE books SET title=? WHERE id=?', (title, id)) conn.execute('pragma user_version=10') conn.commit() @staticmethod def upgrade_version10(conn): for id, author_sort in conn.execute('SELECT id, author_sort FROM books').fetchall(): if not author_sort: aus = conn.execute('SELECT authors FROM meta WHERE id=?',(id,)).fetchone()[0] conn.execute('UPDATE books SET author_sort=? WHERE id=?', (aus, id)) conn.execute('pragma user_version=11') conn.commit() @staticmethod def upgrade_version11(conn): conn.executescript( ''' /***** Add isbn column to meta view ******/ 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, series_index, sort, author_sort, (SELECT concat(format) FROM data WHERE data.book=books.id) formats, isbn FROM books; ''') conn.execute('pragma user_version=12') conn.commit() def __init__(self, dbpath, row_factory=False): self.dbpath = dbpath self.conn = _connect(dbpath) if row_factory: self.conn.row_factory = sqlite.Row self.cache = [] self.data = [] if self.user_version == 0: # No tables have been created LibraryDatabase.create_version1(self.conn) i = 0 while True: i += 1 func = getattr(LibraryDatabase, 'upgrade_version%d'%i, None) if func is None: break if self.user_version == i: print('Upgrading database from version: %d'%i) func(self.conn) def close(self): # global _lock_file # _lock_file.close() # os.unlink(_lock_file.name) # _lock_file = None self.conn.close() @property def user_version(self): 'The user version of this database' return self.conn.get('pragma user_version;', all=False) def is_empty(self): return not self.conn.get('SELECT id FROM books LIMIT 1', all=False) 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', 'timestamp' : 'timestamp', 'formats' : 'formats', 'rating' : 'rating', 'tags' : 'tags', 'series' : 'series', } field = FIELDS[sort_field] order = 'ASC' if not ascending: order = 'DESC' sort = field + ' ' + order if field == 'series': sort += ',series_index '+order elif field == 'title': sort += ',author_sort ' + order else: sort += ',title '+order self.cache = self.conn.get('SELECT * from meta ORDER BY '+sort) self.data = self.cache self.conn.commit() def refresh_ids(self, ids): indices = list(map(self.index, ids)) for id, idx in zip(ids, indices): row = self.conn.get('SELECT * from meta WHERE id=?', (id,), all=False) self.data[idx] = row return indices def filter(self, filters, refilter=False, OR=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 SearchToken objects @param refilter: If True filters are applied to the results of the previous filtering. @param OR: If True, keeps a match if any one of the filters matches. If False, keeps a match only if all the filters match ''' if not filters: self.data = self.data if refilter else self.cache else: matches = [] for item in self.data if refilter else self.cache: if OR: keep = False for token in filters: if token.match(item): keep = True break if keep: matches.append(item) else: keep = True for token in filters: if not token.match(item): 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 row(self, id): for r, record in enumerate(self.data): if record[0] == id: return r def title(self, index, index_is_id=False): if not index_is_id: return self.data[index][1] try: return self.conn.get('SELECT title FROM meta WHERE id=?',(index,), all=False) except: return _('Unknown') def authors(self, index, index_is_id=False): ''' Authors as a comma separated list or None. In the comma separated list, commas in author names are replaced by | symbols ''' if not index_is_id: return self.data[index][2] try: return self.conn.get('SELECT authors FROM meta WHERE id=?',(index,), all=False) except: pass def author_id(self, index, index_is_id=False): id = index if index_is_id else self.id(index) return self.conn.get('SELECT author from books_authors_link WHERE book=?', (id,), all=False) def isbn(self, idx, index_is_id=False): id = idx if index_is_id else self.id(idx) return self.conn.get('SELECT isbn FROM books WHERE id=?',(id,), all=False) def author_sort(self, index, index_is_id=False): id = index if index_is_id else self.id(index) return self.conn.get('SELECT author_sort FROM books WHERE id=?', (id,), all=False) def publisher(self, index, index_is_id=False): if index_is_id: return self.conn.get('SELECT publisher FROM meta WHERE id=?', (index,), all=False) return self.data[index][3] def publisher_id(self, index, index_is_id=False): id = index if index_is_id else self.id(index) return self.conn.get('SELECT publisher from books_publishers_link WHERE book=?', (id,), all=False) def rating(self, index, index_is_id=False): if index_is_id: return self.conn.get('SELECT rating FROM meta WHERE id=?', (index,), all=False) return self.data[index][4] def timestamp(self, index, index_is_id=False): if index_is_id: return self.conn.get('SELECT timestamp FROM meta WHERE id=?', (index,), all=False) return self.data[index][5] def max_size(self, index, index_is_id=False): if index_is_id: return self.conn.get('SELECT size FROM meta WHERE id=?', (index,), all=False) return self.data[index][4] def cover(self, index, index_is_id=False): '''Cover as a data string or None''' id = index if index_is_id else self.id(index) data = self.conn.get('SELECT data FROM covers WHERE book=?', (id,), all=False) if not data: return None return(decompress(data)) def tags(self, index, index_is_id=False): '''tags as a comma separated list or None''' id = index if index_is_id else self.id(index) matches = self.conn.get('SELECT concat(name) FROM tags WHERE tags.id IN (SELECT tag from books_tags_link WHERE book=?)', (id,)) if not matches or not matches[0][0]: return None matches = [t.lower().strip() for t in matches[0][0].split(',')] return ','.join(matches) def series_id(self, index, index_is_id=False): id = index if index_is_id else self.id(index) return self.conn.get('SELECT series from books_series_link WHERE book=?', (id,), all=False) def series(self, index, index_is_id=False): id = self.series_id(index, index_is_id) return self.conn.get('SELECT name from series WHERE id=?', (id,), all=False) def series_index(self, index, index_is_id=False): ans = None if not index_is_id: ans = self.data[index][10] else: ans = self.conn.get('SELECT series_index FROM books WHERE id=?', (index,), all=False) try: return float(ans) except: return 1.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.get('SELECT book from books_series_link WHERE series=?', (series_id,)) if not ans: return [] ans = [id[0] for id in ans] ans.sort(key=lambda x: self.series_index(x, True)) return ans def books_in_series_of(self, index, index_is_id=False): ''' Return an ordered list of all books in the series that the book identified 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, index_is_id=index_is_id) return self.books_in_series(series_id) def comments(self, index, index_is_id=False): '''Comments as string or None''' id = index if index_is_id else self.id(index) return self.conn.get('SELECT text FROM comments WHERE book=?', (id,), all=False) 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) return self.conn.get('SELECT concat(format) FROM data WHERE data.book=?', (id,), all=False) def sizeof_format(self, index, format, index_is_id=False): ''' Return size of C{format} for book C{index} in bytes''' id = index if index_is_id else self.id(index) format = format.upper() return self.conn.get('SELECT uncompressed_size FROM data WHERE data.book=? AND data.format=?', (id, format), all=False) def format(self, index, format, index_is_id=False): id = index if index_is_id else self.id(index) return decompress(self.conn.get('SELECT data FROM data WHERE book=? AND format=?', (id, format), all=False)) def all_series(self): return [(i[0], i[1]) for i in self.conn.get('SELECT id, name FROM series')] def series_name(self, series_id): return self.conn.get('SELECT name FROM series WHERE id=%d'%series_id, all=False) def author_name(self, author_id): return self.conn.get('SELECT name FROM authors WHERE id=%d'%author_id, all=False) def tag_name(self, tag_id): return self.conn.get('SELECT name FROM tags WHERE id=%d'%tag_id, all=False) def all_authors(self): return [(i[0], i[1]) for i in self.conn.get('SELECT id, name FROM authors')] def all_author_names(self): return list(filter(None, [i[0].strip().replace('|', ',') for i in self.conn.get( 'SELECT name FROM authors')])) def all_publishers(self): return [(i[0], i[1]) for i in self.conn.get('SELECT id, name FROM publishers')] def all_tags(self): return [i[0].strip() for i in self.conn.get('SELECT name FROM tags') if i[0].strip()] def all_tags2(self): return [(i[0], i[1]) for i in self.conn.get('SELECT id, name FROM tags')] def all_titles(self): return [(i[0], i[1]) for i in self.conn.get('SELECT id, title FROM books')] def conversion_options(self, id, format): data = self.conn.get('SELECT data FROM conversion_options WHERE book=? AND format=?', (id, format.upper()), all=False) if data: return pickle_loads(bytes(data)) return None def has_conversion_options(self, ids, format='PIPE'): ids = tuple(ids) if len(ids) > 50000: return True if len(ids) == 1: ids = '(%d)'%ids[0] else: ids = repr(ids) return self.conn.get(''' SELECT data FROM conversion_options WHERE book IN %s AND format=? LIMIT 1'''%(ids,), (format,), all=False) is not None def delete_conversion_options(self, id, format, commit=True): self.conn.execute('DELETE FROM conversion_options WHERE book=? AND format=?', (id, format.upper())) if commit: self.conn.commit() 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, index_is_id=False): id = index if index_is_id else 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] col = {'title':1, 'authors':2, 'publisher':3, 'rating':4, 'tags':7}[column] self.data[row][col] = val for item in self.cache: if item[0] == id: item[col] = val break if column == 'authors': val = string_to_authors(val) 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) elif column == 'tags': self.set_tags(id, val.split(','), append=False) def set_conversion_options(self, id, format, options): data = sqlite.Binary(pickle_dumps(options)) oid = self.conn.get('SELECT id FROM conversion_options WHERE book=? AND format=?', (id, format.upper()), all=False) if oid: self.conn.execute('UPDATE conversion_options SET data=? WHERE id=?', (data, oid)) 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.get('SELECT id from authors WHERE name=?', (a,), all=False) if author: aid = author # Handle change of case self.conn.execute('UPDATE authors SET name=? WHERE id=?', (a, aid)) else: aid = self.conn.execute('INSERT INTO authors(name) VALUES (?)', (a,)).lastrowid try: self.conn.execute('INSERT INTO books_authors_link(book, author) VALUES (?,?)', (id, aid)) except sqlite.IntegrityError: # Sometimes books specify the same author twice in their metadata pass 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.get('SELECT id from publishers WHERE name=?', (publisher,), all=False) if pub: aid = pub 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 delete_tags(self, tags): for tag in tags: self.delete_tag(tag) def unapply_tags(self, book_id, tags): for tag in tags: id = self.conn.get('SELECT id FROM tags WHERE name=?', (tag,), all=False) if id: self.conn.execute('DELETE FROM books_tags_link WHERE tag=? AND book=?', (id, book_id)) 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.lower().strip() if not tag: continue t = self.conn.get('SELECT id FROM tags WHERE name=?', (tag,), all=False) if t: tid = t else: tid = self.conn.execute('INSERT INTO tags(name) VALUES(?)', (tag,)).lastrowid if not self.conn.get('SELECT book FROM books_tags_link WHERE book=? AND tag=?', (id, tid), all=False): 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.get('SELECT id from series WHERE name=?', (series,), all=False) if s: aid = s 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() row = self.row(id) if row is not None: self.data[row][9] = series def remove_unused_series(self): for id, in self.conn.get('SELECT id FROM series'): if not self.conn.get('SELECT id from books_series_link WHERE series=?', (id,)): self.conn.execute('DELETE FROM series WHERE id=?', (id,)) self.conn.commit() def set_series_index(self, id, idx): idx = int(idx) self.conn.execute('UPDATE books SET series_index=? WHERE id=?', (int(idx), id)) self.conn.commit() row = self.row(id) if row is not None: self.data[row][10] = idx def set_rating(self, id, rating): rating = int(rating) self.conn.execute('DELETE FROM books_ratings_link WHERE book=?',(id,)) rat = self.conn.get('SELECT id FROM ratings WHERE rating=?', (rating,), all=False) rat = rat 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 set_metadata(self, id, mi): ''' Set metadata for the book C{id} from the L{MetaInformation} object C{mi} ''' if mi.title: self.set_title(id, mi.title) if not mi.authors: mi.authors = ['Unknown'] authors = [] for a in mi.authors: authors += string_to_authors(a) 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) if mi.cover_data[1] is not None: self.set_cover(id, mi.cover_data[1]) def add_books(self, paths, formats, metadata, uris=[], add_duplicates=True): ''' Add a book to the database. self.data and self.cache are not updated. @param paths: List of paths to book files of file-like objects ''' formats, metadata, uris = iter(formats), iter(metadata), iter(uris) duplicates = [] for path in paths: mi = next(metadata) format = next(formats) try: uri = next(uris) except StopIteration: uri = None if not add_duplicates and self.has_book(mi): duplicates.append((path, format, mi, uri)) continue series_index = 1 if mi.series_index is None else mi.series_index aus = mi.author_sort if mi.author_sort else ', '.join(mi.authors) obj = self.conn.execute('INSERT INTO books(title, uri, series_index, author_sort) VALUES (?, ?, ?, ?)', (mi.title, uri, series_index, aus)) id = obj.lastrowid self.conn.commit() self.set_metadata(id, mi) stream = path if hasattr(path, 'read') else lopen(path, 'rb') stream.seek(0, 2) usize = stream.tell() stream.seek(0) self.conn.execute('INSERT INTO data(book, format, uncompressed_size, data) VALUES (?,?,?,?)', (id, format, usize, sqlite.Binary(compress(stream.read())))) if not hasattr(path, 'read'): stream.close() self.conn.commit() if duplicates: paths = tuple(duplicate[0] for duplicate in duplicates) formats = tuple(duplicate[1] for duplicate in duplicates) metadata = tuple(duplicate[2] for duplicate in duplicates) uris = tuple(duplicate[3] for duplicate in duplicates) return (paths, formats, metadata, uris) return None 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 get_feeds(self): feeds = self.conn.get('SELECT title, script FROM feeds') yield from feeds def get_feed(self, id): return self.conn.get('SELECT script FROM feeds WHERE id=%d'%id, all=False) def update_feed(self, id, script, title): self.conn.execute('UPDATE feeds set title=? WHERE id=?', (title, id)) self.conn.execute('UPDATE feeds set script=? WHERE id=?', (script, id)) self.conn.commit() def remove_feeds(self, ids): for x in ids: self.conn.execute('DELETE FROM feeds WHERE id=?', (x,)) self.conn.commit() def add_feed(self, title, script): if isbytestring(title): title = title.decode('utf-8') if isbytestring(script): script = script.decode('utf-8') self.conn.execute('INSERT INTO feeds(title, script) VALUES (?, ?)', (title, script)) self.conn.commit() def set_feeds(self, feeds): self.conn.execute('DELETE FROM feeds') for title, script in feeds: self.conn.execute('INSERT INTO feeds(title, script) VALUES (?, ?)', (title, script)) self.conn.commit() 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 get_metadata(self, idx, index_is_id=False): ''' Convenience method to return metadata as a L{MetaInformation} object. ''' aum = self.authors(idx, index_is_id=index_is_id) if aum: aum = [a.strip().replace('|', ',') for a in aum.split(',')] mi = MetaInformation(self.title(idx, index_is_id=index_is_id), aum) mi.author_sort = self.author_sort(idx, index_is_id=index_is_id) mi.comments = self.comments(idx, index_is_id=index_is_id) mi.publisher = self.publisher(idx, index_is_id=index_is_id) tags = self.tags(idx, index_is_id=index_is_id) if tags: mi.tags = [i.strip() for i in tags.split(',')] mi.series = self.series(idx, index_is_id=index_is_id) if mi.series: mi.series_index = self.series_index(idx, index_is_id=index_is_id) mi.rating = self.rating(idx, index_is_id=index_is_id) mi.isbn = self.isbn(idx, index_is_id=index_is_id) id = idx if index_is_id else self.id(idx) mi.application_id = id return mi def vacuum(self): self.conn.execute('VACUUM;') self.conn.commit() def all_ids(self): return [i[0] for i in self.conn.get('SELECT id FROM books')] def has_id(self, id): return self.conn.get('SELECT id FROM books where id=?', (id,), all=False) is not None class SearchToken: FIELD_MAP = {'title' : 1, 'author' : 2, 'publisher' : 3, 'tag' : 7, 'comments' : 8, 'series' : 9, 'format' : 13, } def __init__(self, text_token): self.index = -1 text_token = text_token.strip() for field in self.FIELD_MAP.keys(): if text_token.lower().startswith(field+':'): text_token = text_token[len(field)+1:] self.index = self.FIELD_MAP[field] break self.negate = False if text_token.startswith('!'): self.negate = True text_token = text_token[1:] self.pattern = re.compile(text_token, re.IGNORECASE) def match(self, item): if self.index >= 0: text = item[self.index] if not text: text = '' else: text = ' '.join([item[i] if item[i] else '' for i in self.FIELD_MAP.values()]) return bool(self.pattern.search(text)) ^ self.negate def text_to_tokens(text): OR = False match = re.match(r'\[(.*)\]', text) if match: text = match.group(1) OR = True tokens = [] quot = re.search('"(.*?)"', text) while quot: tokens.append(quot.group(1)) text = text.replace('"'+quot.group(1)+'"', '') quot = re.search('"(.*?)"', text) tokens += text.split(' ') ans = [] for i in tokens: try: ans.append(SearchToken(i)) except sre_constants.error: continue return ans, OR if __name__ == '__main__': sqlite.enable_callback_tracebacks(True) db = LibraryDatabase('/home/kovid/temp/library1.db.orig')