diff --git a/src/calibre/ebooks/metadata/isbndb.py b/src/calibre/ebooks/metadata/isbndb.py index 1f0dde3696..3cf5f92eaf 100644 --- a/src/calibre/ebooks/metadata/isbndb.py +++ b/src/calibre/ebooks/metadata/isbndb.py @@ -9,7 +9,7 @@ from urllib import urlopen, quote from calibre import setup_cli_handlers from calibre.utils.config import OptionParser -from calibre.ebooks.metadata import MetaInformation +from calibre.ebooks.metadata import MetaInformation, authors_to_sort_string from calibre.ebooks.BeautifulSoup import BeautifulStoneSoup BASE_URL = 'http://isbndb.com/api/books.xml?access_key=%(key)s&page_number=1&results=subjects,authors,texts&' @@ -64,7 +64,8 @@ class ISBNDBMetadata(MetaInformation): try: self.author_sort = book.find('authors').find('person').string except: - pass + if self.authors: + self.author_sort = authors_to_sort_string(self.authors) self.publisher = book.find('publishertext').string summ = book.find('summary') diff --git a/src/calibre/ebooks/mobi/reader.py b/src/calibre/ebooks/mobi/reader.py index 1317ab92c2..6cc283be95 100644 --- a/src/calibre/ebooks/mobi/reader.py +++ b/src/calibre/ebooks/mobi/reader.py @@ -411,13 +411,12 @@ def get_metadata(stream): if mr.book_header.exth is None: mi = MetaInformation(mr.name, ['Unknown']) else: - tdir = tempfile.mkdtemp('mobi-meta', __appname__) + tdir = tempfile.mkdtemp('_mobi_meta', __appname__) atexit.register(shutil.rmtree, tdir) mr.extract_images([], tdir) mi = mr.create_opf('dummy.html') if mi.cover: cover = os.path.join(tdir, mi.cover) - print cover if os.access(cover, os.R_OK): mi.cover_data = ('JPEG', open(os.path.join(tdir, mi.cover), 'rb').read()) return mi diff --git a/src/calibre/gui2/main.py b/src/calibre/gui2/main.py index 82cb9e7002..cd82710197 100644 --- a/src/calibre/gui2/main.py +++ b/src/calibre/gui2/main.py @@ -1178,6 +1178,7 @@ in which you want to store your books files. Any existing books will be automati try: self.olddb = LibraryDatabase(self.database_path) except: + traceback.print_exc() self.olddb = None diff --git a/src/calibre/library/cli.py b/src/calibre/library/cli.py index 532e8626dc..67f677e68a 100644 --- a/src/calibre/library/cli.py +++ b/src/calibre/library/cli.py @@ -51,7 +51,7 @@ XML_TEMPLATE = '''\ ${record['cover']} - $path + ${path} diff --git a/src/calibre/library/database.py b/src/calibre/library/database.py index 59f1a65429..1d15c9f838 100644 --- a/src/calibre/library/database.py +++ b/src/calibre/library/database.py @@ -30,11 +30,21 @@ class Concatenate(object): if self.sep: return self.ans[:-len(self.sep)] return self.ans +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, unicode): path = path.encode('utf-8') - conn = sqlite.connect(path, detect_types=sqlite.PARSE_DECLTYPES|sqlite.PARSE_COLNAMES) + 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('^(A|The|An)\s+', re.IGNORECASE) @@ -812,11 +822,11 @@ ALTER TABLE books ADD COLUMN isbn TEXT DEFAULT "" COLLATE NOCASE; def user_version(): doc = 'The user version of this database' def fget(self): - return self.conn.execute('pragma user_version;').next()[0] + return self.conn.get('pragma user_version;', all=False) return property(doc=doc, fget=fget) def is_empty(self): - return not self.conn.execute('SELECT id FROM books LIMIT 1').fetchone() + return not self.conn.get('SELECT id FROM books LIMIT 1', all=False) def refresh(self, sort_field, ascending): ''' @@ -846,14 +856,14 @@ ALTER TABLE books ADD COLUMN isbn TEXT DEFAULT "" COLLATE NOCASE; else: sort += ',title '+order - self.cache = self.conn.execute('SELECT * from meta ORDER BY '+sort).fetchall() + self.cache = self.conn.get('SELECT * from meta ORDER BY '+sort) self.data = self.cache self.conn.commit() def refresh_ids(self, ids): indices = map(self.index, ids) for id, idx in zip(ids, indices): - row = self.conn.execute('SELECT * from meta WHERE id=?', (id,)).fetchone() + row = self.conn.get('SELECT * from meta WHERE id=?', (id,), all=False) self.data[idx] = row return indices @@ -905,7 +915,7 @@ ALTER TABLE books ADD COLUMN isbn TEXT DEFAULT "" COLLATE NOCASE; if not index_is_id: return self.data[index][1] try: - return self.conn.execute('SELECT title FROM meta WHERE id=?',(index,)).fetchone()[0] + return self.conn.get('SELECT title FROM meta WHERE id=?',(index,), all=False) except: return _('Unknown') @@ -917,73 +927,69 @@ ALTER TABLE books ADD COLUMN isbn TEXT DEFAULT "" COLLATE NOCASE; if not index_is_id: return self.data[index][2] try: - return self.conn.execute('SELECT authors FROM meta WHERE id=?',(index,)).fetchone()[0] + return self.conn.get('SELECT authors FROM meta WHERE id=?',(index,), all=False) except: pass def isbn(self, idx, index_is_id=False): id = idx if index_is_id else self.id(idx) - return self.conn.execute('SELECT isbn FROM books WHERE id=?',(id,)).fetchone()[0] + 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.execute('SELECT author_sort FROM books WHERE id=?', (id,)).fetchone()[0] + 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.execute('SELECT publisher FROM meta WHERE id=?', (index,)).fetchone()[0] + return self.conn.get('SELECT publisher FROM meta WHERE id=?', (index,), all=False) return self.data[index][3] def rating(self, index, index_is_id=False): if index_is_id: - return self.conn.execute('SELECT rating FROM meta WHERE id=?', (index,)).fetchone()[0] + 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.execute('SELECT timestamp FROM meta WHERE id=?', (index,)).fetchone()[0] + 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.execute('SELECT size FROM meta WHERE id=?', (index,)).fetchone()[0] + return self.conn.get('SELECT size FROM meta WHERE id=?', (index,), all=False) return self.data[index][6] 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.execute('SELECT data FROM covers WHERE book=?', (id,)).fetchone() - if not data or not data[0]: + data = self.conn.get('SELECT data FROM covers WHERE book=?', (id,), all=False) + if not data: return None - return(decompress(data[0])) + 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.execute('SELECT concat(name) FROM tags WHERE tags.id IN (SELECT tag from books_tags_link WHERE book=?)', (id,)).fetchall() + 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) - ans= self.conn.execute('SELECT series from books_series_link WHERE book=?', (id,)).fetchone() - if ans: - return ans[0] + 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) - ans = self.conn.execute('SELECT name from series WHERE id=?', (id,)).fetchone() - if ans: - return ans[0] - + 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.execute('SELECT series_index FROM books WHERE id=?', (index,)).fetchone()[0] + ans = self.conn.get('SELECT series_index FROM books WHERE id=?', (index,), all=False) try: return int(ans) except: @@ -994,8 +1000,8 @@ ALTER TABLE books ADD COLUMN isbn TEXT DEFAULT "" COLLATE NOCASE; 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() + 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] @@ -1014,41 +1020,35 @@ ALTER TABLE books ADD COLUMN isbn TEXT DEFAULT "" COLLATE NOCASE; def comments(self, index, index_is_id=False): '''Comments as string or None''' id = index if index_is_id else self.id(index) - matches = self.conn.execute('SELECT text FROM comments WHERE book=?', (id,)).fetchall() - if not matches: - return None - return matches[0][0] - + 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) - matches = self.conn.execute('SELECT concat(format) FROM data WHERE data.book=?', (id,)).fetchall() - if not matches: - return None - return matches[0][0] + 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.execute('SELECT uncompressed_size FROM data WHERE data.book=? AND data.format=?', (id, format)).fetchone()[0] + 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.execute('SELECT data FROM data WHERE book=? AND format=?', (id, format)).fetchone()[0]) + 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.execute('SELECT id, name FROM series').fetchall()] + self.conn.get('SELECT id, name FROM series')] def all_tags(self): - return [i[0].strip() for i in self.conn.execute('SELECT name FROM tags').fetchall() if i[0].strip()] + return [i[0].strip() for i in self.conn.get('SELECT name FROM tags') if i[0].strip()] def conversion_options(self, id, format): - data = self.conn.execute('SELECT data FROM conversion_options WHERE book=? AND format=?', (id, format.upper())).fetchone() + data = self.conn.get('SELECT data FROM conversion_options WHERE book=? AND format=?', (id, format.upper()), all=False) if data: - return cPickle.loads(str(data[0])) + return cPickle.loads(str(data)) return None @@ -1108,9 +1108,9 @@ ALTER TABLE books ADD COLUMN isbn TEXT DEFAULT "" COLLATE NOCASE; def set_conversion_options(self, id, format, options): data = sqlite.Binary(cPickle.dumps(options, -1)) - oid = self.conn.execute('SELECT id FROM conversion_options WHERE book=? AND format=?', (id, format.upper())).fetchone() + 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[0])) + 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() @@ -1125,9 +1125,9 @@ ALTER TABLE books ADD COLUMN isbn TEXT DEFAULT "" COLLATE NOCASE; if not a: continue a = a.strip() - author = self.conn.execute('SELECT id from authors WHERE name=?', (a,)).fetchone() + author = self.conn.get('SELECT id from authors WHERE name=?', (a,), all=False) if author: - aid = author[0] + aid = author # Handle change of case self.conn.execute('UPDATE authors SET name=? WHERE id=?', (a, aid)) else: @@ -1155,9 +1155,9 @@ ALTER TABLE books ADD COLUMN isbn TEXT DEFAULT "" COLLATE NOCASE; 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() + pub = self.conn.get('SELECT id from publishers WHERE name=?', (publisher,), all=False) if pub: - aid = pub[0] + 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)) @@ -1169,15 +1169,14 @@ ALTER TABLE books ADD COLUMN isbn TEXT DEFAULT "" COLLATE NOCASE; self.conn.commit() def is_tag_used(self, tag): - id = self.conn.execute('SELECT id FROM tags WHERE name=?', (tag,)).fetchone() + id = self.conn.get('SELECT id FROM tags WHERE name=?', (tag,), all=False) if not id: return False - return bool(self.conn.execute('SELECT tag FROM books_tags_link WHERE tag=?',(id[0],)).fetchone()) + return bool(self.conn.get('SELECT tag FROM books_tags_link WHERE tag=?',(id,), all=False)) def delete_tag(self, tag): - id = self.conn.execute('SELECT id FROM tags WHERE name=?', (tag,)).fetchone() + id = self.conn.get('SELECT id FROM tags WHERE name=?', (tag,), all=False) if id: - id = id[0] self.conn.execute('DELETE FROM books_tags_link WHERE tag=?', (id,)) self.conn.execute('DELETE FROM tags WHERE id=?', (id,)) self.conn.commit() @@ -1188,9 +1187,9 @@ ALTER TABLE books ADD COLUMN isbn TEXT DEFAULT "" COLLATE NOCASE; def unapply_tags(self, book_id, tags): for tag in tags: - id = self.conn.execute('SELECT id FROM tags WHERE name=?', (tag,)).fetchone() + 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[0], book_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): @@ -1204,14 +1203,14 @@ ALTER TABLE books ADD COLUMN isbn TEXT DEFAULT "" COLLATE NOCASE; tag = tag.lower().strip() if not tag: continue - t = self.conn.execute('SELECT id FROM tags WHERE name=?', (tag,)).fetchone() + t = self.conn.get('SELECT id FROM tags WHERE name=?', (tag,), all=False) if t: - tid = t[0] + tid = t 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(): + 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() @@ -1220,9 +1219,9 @@ ALTER TABLE books ADD COLUMN isbn TEXT DEFAULT "" COLLATE NOCASE; 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() + s = self.conn.get('SELECT id from series WHERE name=?', (series,), all=False) if s: - aid = s[0] + 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)) @@ -1232,8 +1231,8 @@ ALTER TABLE books ADD COLUMN isbn TEXT DEFAULT "" COLLATE NOCASE; self.data[row][9] = series def remove_unused_series(self): - for id, in self.conn.execute('SELECT id FROM series').fetchall(): - if not self.conn.execute('SELECT id from books_series_link WHERE series=?', (id,)).fetchone(): + 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() @@ -1248,8 +1247,8 @@ ALTER TABLE books ADD COLUMN isbn TEXT DEFAULT "" COLLATE NOCASE; 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 + 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() @@ -1336,7 +1335,7 @@ ALTER TABLE books ADD COLUMN isbn TEXT DEFAULT "" COLLATE NOCASE; return i def get_feeds(self): - feeds = self.conn.execute('SELECT title, script FROM feeds').fetchall() + feeds = self.conn.get('SELECT title, script FROM feeds') for title, script in feeds: yield title, script @@ -1386,7 +1385,7 @@ ALTER TABLE books ADD COLUMN isbn TEXT DEFAULT "" COLLATE NOCASE; self.conn.commit() def all_ids(self): - return [i[0] for i in self.conn.execute('SELECT id FROM books').fetchall()] + return [i[0] for i in self.conn.get('SELECT id FROM books')] def export_to_dir(self, dir, indices, byauthor=False, single_dir=False, index_is_id=False): @@ -1395,8 +1394,8 @@ ALTER TABLE books ADD COLUMN isbn TEXT DEFAULT "" COLLATE NOCASE; by_author = {} for index in indices: id = index if index_is_id else self.id(index) - au = self.conn.execute('SELECT author_sort FROM books WHERE id=?', - (id,)).fetchone()[0] + au = self.conn.get('SELECT author_sort FROM books WHERE id=?', + (id,), all=False) if not au: au = self.authors(index, index_is_id=index_is_id) if not au: @@ -1540,10 +1539,10 @@ ALTER TABLE books ADD COLUMN isbn TEXT DEFAULT "" COLLATE NOCASE; def has_book(self, mi): - return bool(self.conn.execute('SELECT id FROM books where title=?', (mi.title,)).fetchone()) + return bool(self.conn.get('SELECT id FROM books where title=?', (mi.title,), all=False)) def has_id(self, id): - return self.conn.execute('SELECT id FROM books where id=?', (id,)).fetchone() is not None + return self.conn.get('SELECT id FROM books where id=?', (id,), all=False) is not None def recursive_import(self, root, single_book_per_directory=True): root = os.path.abspath(root) diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py index 0267691275..ad203ecf4a 100644 --- a/src/calibre/library/database2.py +++ b/src/calibre/library/database2.py @@ -8,7 +8,6 @@ The database used to store ebook metadata ''' import os, re, sys, shutil, cStringIO, glob, collections, textwrap, \ operator, itertools, functools, traceback -import sqlite3 as sqlite from itertools import repeat from PyQt4.QtCore import QCoreApplication, QThread, QReadWriteLock @@ -16,9 +15,11 @@ from PyQt4.QtGui import QApplication, QPixmap, QImage __app = None from calibre.library.database import LibraryDatabase +from calibre.library.sqlite import connect, IntegrityError from calibre.ebooks.metadata import string_to_authors, authors_to_string from calibre.constants import preferred_encoding, iswindows, isosx + copyfile = os.link if hasattr(os, 'link') else shutil.copyfile filesystem_encoding = sys.getfilesystemencoding() if filesystem_encoding is None: filesystem_encoding = 'utf-8' @@ -157,23 +158,6 @@ class CoverCache(QThread): self.load_queue.appendleft(id) self.load_queue_lock.unlock() -class Concatenate(object): - '''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): - if not self.ans: - return None - if self.sep: - return self.ans[:-len(self.sep)] - return self.ans - class ResultCache(object): ''' @@ -226,7 +210,7 @@ class ResultCache(object): def refresh_ids(self, conn, ids): for id in ids: - self._data[id] = conn.execute('SELECT * from meta WHERE id=?', (id,)).fetchone() + self._data[id] = conn.get('SELECT * from meta WHERE id=?', (id,))[0] return map(self.row, ids) def books_added(self, ids, conn): @@ -234,7 +218,7 @@ class ResultCache(object): return self._data.extend(repeat(None, max(ids)-len(self._data)+2)) for id in ids: - self._data[id] = conn.execute('SELECT * from meta WHERE id=?', (id,)).fetchone() + self._data[id] = conn.get('SELECT * from meta WHERE id=?', (id,))[0] self._map[0:0] = ids self._map_filtered[0:0] = ids @@ -246,7 +230,7 @@ class ResultCache(object): # Fast mapping from sorted, filtered row numbers to ids # At the moment it is the same as self._map self._map_filtered = list(self._map) - temp = db.conn.execute('SELECT * FROM meta').fetchall() + temp = db.conn.get('SELECT * FROM meta') # Fast mapping from ids to data. # Can be None for ids that dont exist (i.e. have been deleted) self._data = list(itertools.repeat(None, temp[-1][0]+2)) if temp else [] @@ -285,53 +269,53 @@ class ResultCache(object): self._map_filtered.remove(id) def sort_on_title(self, order, db): - return db.conn.execute('SELECT id FROM books ORDER BY sort ' + order).fetchall() + return db.conn.get('SELECT id FROM books ORDER BY sort ' + order) def sort_on_author_sort(self, order, db): - return db.conn.execute('SELECT id FROM books ORDER BY author_sort,sort ' + order).fetchall() + return db.conn.get('SELECT id FROM books ORDER BY author_sort,sort ' + order) def sort_on_timestamp(self, order, db): - return db.conn.execute('SELECT id FROM books ORDER BY id ' + order).fetchall() + return db.conn.get('SELECT id FROM books ORDER BY id ' + order) def sort_on_publisher(self, order, db): - no_publisher = db.conn.execute('SELECT id FROM books WHERE books.id NOT IN (SELECT book FROM books_publishers_link) ORDER BY books.sort').fetchall() + no_publisher = db.conn.get('SELECT id FROM books WHERE books.id NOT IN (SELECT book FROM books_publishers_link) ORDER BY books.sort') ans = [] - for r in db.conn.execute('SELECT id FROM publishers ORDER BY name '+order).fetchall(): + for r in db.conn.get('SELECT id FROM publishers ORDER BY name '+order): publishers_id = r[0] - ans += db.conn.execute('SELECT id FROM books WHERE books.id IN (SELECT book FROM books_publishers_link WHERE publisher=?) ORDER BY books.sort '+order, (publishers_id,)).fetchall() + ans += db.conn.get('SELECT id FROM books WHERE books.id IN (SELECT book FROM books_publishers_link WHERE publisher=?) ORDER BY books.sort '+order, (publishers_id,)) ans = (no_publisher + ans) if order == 'ASC' else (ans + no_publisher) return ans def sort_on_size(self, order, db): - return db.conn.execute('SELECT id FROM meta ORDER BY size ' + order).fetchall() + return db.conn.get('SELECT id FROM meta ORDER BY size ' + order) def sort_on_rating(self, order, db): - no_rating = db.conn.execute('SELECT id FROM books WHERE books.id NOT IN (SELECT book FROM books_ratings_link) ORDER BY books.sort').fetchall() + no_rating = db.conn.get('SELECT id FROM books WHERE books.id NOT IN (SELECT book FROM books_ratings_link) ORDER BY books.sort') ans = [] - for r in db.conn.execute('SELECT id FROM ratings ORDER BY rating '+order).fetchall(): + for r in db.conn.get('SELECT id FROM ratings ORDER BY rating '+order): ratings_id = r[0] - ans += db.conn.execute('SELECT id FROM books WHERE books.id IN (SELECT book FROM books_ratings_link WHERE rating=?) ORDER BY books.sort', (ratings_id,)).fetchall() + ans += db.conn.get('SELECT id FROM books WHERE books.id IN (SELECT book FROM books_ratings_link WHERE rating=?) ORDER BY books.sort', (ratings_id,)) ans = (no_rating + ans) if order == 'ASC' else (ans + no_rating) return ans def sort_on_series(self, order, db): - no_series = db.conn.execute('SELECT id FROM books WHERE books.id NOT IN (SELECT book FROM books_series_link) ORDER BY books.sort').fetchall() + no_series = db.conn.get('SELECT id FROM books WHERE books.id NOT IN (SELECT book FROM books_series_link) ORDER BY books.sort') ans = [] - for r in db.conn.execute('SELECT id FROM series ORDER BY name '+order).fetchall(): + for r in db.conn.get('SELECT id FROM series ORDER BY name '+order): series_id = r[0] - ans += db.conn.execute('SELECT id FROM books WHERE books.id IN (SELECT book FROM books_series_link WHERE series=?) ORDER BY books.series_index,books.id '+order, (series_id,)).fetchall() + ans += db.conn.get('SELECT id FROM books WHERE books.id IN (SELECT book FROM books_series_link WHERE series=?) ORDER BY books.series_index,books.id '+order, (series_id,)) ans = (no_series + ans) if order == 'ASC' else (ans + no_series) return ans def sort_on_tags(self, order, db): - no_tags = db.conn.execute('SELECT id FROM books WHERE books.id NOT IN (SELECT book FROM books_tags_link) ORDER BY books.sort').fetchall() + no_tags = db.conn.get('SELECT id FROM books WHERE books.id NOT IN (SELECT book FROM books_tags_link) ORDER BY books.sort') ans = [] - for r in db.conn.execute('SELECT id FROM tags ORDER BY name '+order).fetchall(): + for r in db.conn.get('SELECT id FROM tags ORDER BY name '+order): tag_id = r[0] - ans += db.conn.execute('SELECT id FROM books WHERE books.id IN (SELECT book FROM books_tags_link WHERE tag=?) ORDER BY books.sort '+order, (tag_id,)).fetchall() + ans += db.conn.get('SELECT id FROM books WHERE books.id IN (SELECT book FROM books_tags_link WHERE tag=?) ORDER BY books.sort '+order, (tag_id,)) ans = (no_tags + ans) if order == 'ASC' else (ans + no_tags) return ans @@ -353,36 +337,25 @@ class LibraryDatabase2(LibraryDatabase): @apply def user_version(): doc = 'The user version of this database' + def fget(self): - return self.conn.execute('pragma user_version;').next()[0] + return self.conn.get('pragma user_version;', all=False) + def fset(self, val): self.conn.execute('pragma user_version=%d'%int(val)) self.conn.commit() + return property(doc=doc, fget=fget, fset=fset) def connect(self): if 'win32' in sys.platform and len(self.library_path) + 4*self.PATH_LIMIT + 10 > 259: raise ValueError('Path to library too long. Must be less than %d characters.'%(259-4*self.PATH_LIMIT-10)) exists = os.path.exists(self.dbpath) - self.conn = sqlite.connect(self.dbpath, - detect_types=sqlite.PARSE_DECLTYPES|sqlite.PARSE_COLNAMES) + self.conn = connect(self.dbpath, self.row_factory) if exists and self.user_version == 0: self.conn.close() os.remove(self.dbpath) - self.conn = sqlite.connect(self.dbpath, - detect_types=sqlite.PARSE_DECLTYPES|sqlite.PARSE_COLNAMES) - self.conn.row_factory = sqlite.Row if self.row_factory else lambda cursor, row : list(row) - self.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() - - self.conn.create_function('title_sort', 1, title_sort) + self.conn = connect(self.dbpath, self.row_factory) if self.user_version == 0: self.initialize_database() @@ -453,7 +426,7 @@ class LibraryDatabase2(LibraryDatabase): def path(self, index, index_is_id=False): 'Return the relative path to the directory containing this books files as a unicode string.' id = index if index_is_id else self.id(index) - path = self.conn.execute('SELECT path FROM books WHERE id=?', (id,)).fetchone()[0].replace('/', os.sep) + path = self.conn.get('SELECT path FROM books WHERE id=?', (id,), all=False).replace('/', os.sep) return path def abspath(self, index, index_is_id=False): @@ -503,7 +476,7 @@ class LibraryDatabase2(LibraryDatabase): fname = self.construct_file_name(id) changed = False for format in formats: - name = self.conn.execute('SELECT name FROM data WHERE book=? AND format=?', (id, format)).fetchone()[0] + name = self.conn.get('SELECT name FROM data WHERE book=? AND format=?', (id, format), all=False) if name and name != fname: changed = True break @@ -594,7 +567,7 @@ class LibraryDatabase2(LibraryDatabase): p.save(path) def all_formats(self): - formats = self.conn.execute('SELECT format from data').fetchall() + formats = self.conn.get('SELECT format from data') if not formats: return set([]) return set([f[0] for f in formats]) @@ -604,8 +577,8 @@ class LibraryDatabase2(LibraryDatabase): id = index if index_is_id else self.id(index) path = os.path.join(self.library_path, self.path(id, index_is_id=True)) try: - formats = self.conn.execute('SELECT format FROM data WHERE book=?', (id,)).fetchall() - name = self.conn.execute('SELECT name FROM data WHERE book=?', (id,)).fetchone()[0] + formats = self.conn.get('SELECT format FROM data WHERE book=?', (id,)) + name = self.conn.get('SELECT name FROM data WHERE book=?', (id,), all=False) formats = map(lambda x:x[0], formats) except: return None @@ -621,7 +594,7 @@ class LibraryDatabase2(LibraryDatabase): 'Return absolute path to the ebook file of format `format`' id = index if index_is_id else self.id(index) path = os.path.join(self.library_path, self.path(id, index_is_id=True)) - name = self.conn.execute('SELECT name FROM data WHERE book=? AND format=?', (id, format)).fetchone()[0] + name = self.conn.get('SELECT name FROM data WHERE book=? AND format=?', (id, format), all=False) if name: format = ('.' + format.lower()) if format else '' path = os.path.join(path, name+format) @@ -645,7 +618,7 @@ class LibraryDatabase2(LibraryDatabase): id = index if index_is_id else self.id(index) if path is None: path = os.path.join(self.library_path, self.path(id, index_is_id=True)) - name = self.conn.execute('SELECT name FROM data WHERE book=? AND format=?', (id, format)).fetchone() + name = self.conn.get('SELECT name FROM data WHERE book=? AND format=?', (id, format), all=False) if name: self.conn.execute('DELETE FROM data WHERE book=? AND format=?', (id, format)) name = self.construct_file_name(id) @@ -682,7 +655,7 @@ class LibraryDatabase2(LibraryDatabase): def remove_format(self, index, format, index_is_id=False): id = index if index_is_id else self.id(index) path = os.path.join(self.library_path, self.path(id, index_is_id=True)) - name = self.conn.execute('SELECT name FROM data WHERE book=? AND format=?', (id, format)).fetchone() + name = self.conn.get('SELECT name FROM data WHERE book=? AND format=?', (id, format), all=False) name = name[0] if name else False if name: ext = ('.' + format.lower()) if format else '' @@ -709,7 +682,7 @@ class LibraryDatabase2(LibraryDatabase): def get_categories(self, sort_on_count=False): categories = {} def get(name, category, field='name'): - ans = self.conn.execute('SELECT DISTINCT %s FROM %s'%(field, name)).fetchall() + ans = self.conn.get('SELECT DISTINCT %s FROM %s'%(field, name)) ans = [x[0].strip() for x in ans] try: ans.remove('') @@ -718,16 +691,14 @@ class LibraryDatabase2(LibraryDatabase): tags = categories[category] if name != 'data': for tag in tags: - id = self.conn.execute('SELECT id FROM %s WHERE %s=?'%(name, field), (tag,)).fetchone() - if id: - id = id[0] + id = self.conn.get('SELECT id FROM %s WHERE %s=?'%(name, field), (tag,), all=False) tag.id = id for tag in tags: if tag.id is not None: - tag.count = self.conn.execute('SELECT COUNT(id) FROM books_%s_link WHERE %s=?'%(name, category), (tag.id,)).fetchone()[0] + tag.count = self.conn.get('SELECT COUNT(id) FROM books_%s_link WHERE %s=?'%(name, category), (tag.id,), all=False) else: for tag in tags: - tag.count = self.conn.execute('SELECT COUNT(format) FROM data WHERE format=?', (tag,)).fetchone()[0] + tag.count = self.conn.get('SELECT COUNT(format) FROM data WHERE format=?', (tag,), all=False) tags.sort(reverse=sort_on_count, cmp=(lambda x,y:cmp(x.count,y.count)) if sort_on_count else cmp) for x in (('authors', 'author'), ('tags', 'tag'), ('publishers', 'publisher'), ('series', 'series')): @@ -785,6 +756,8 @@ class LibraryDatabase2(LibraryDatabase): self.set_tags(id, mi.tags, notify=False) if mi.comments: self.set_comment(id, mi.comments) + if mi.isbn and mi.isbn.strip(): + self.set_isbn(id, mi.isbn) self.set_path(id, True) self.notify('metadata', [id]) @@ -800,16 +773,16 @@ class LibraryDatabase2(LibraryDatabase): a = a.strip().replace(',', '|') if not isinstance(a, unicode): a = a.decode(preferred_encoding, 'replace') - author = self.conn.execute('SELECT id from authors WHERE name=?', (a,)).fetchone() + author = self.conn.get('SELECT id from authors WHERE name=?', (a,), all=False) if author: - aid = author[0] + 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 + except IntegrityError: # Sometimes books specify the same author twice in their metadata pass self.set_path(id, True) self.notify('metadata', [id]) @@ -829,9 +802,9 @@ class LibraryDatabase2(LibraryDatabase): if publisher: if not isinstance(publisher, unicode): publisher = publisher.decode(preferred_encoding, 'replace') - pub = self.conn.execute('SELECT id from publishers WHERE name=?', (publisher,)).fetchone() + pub = self.conn.get('SELECT id from publishers WHERE name=?', (publisher,), all=False) if pub: - aid = pub[0] + 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)) @@ -852,14 +825,14 @@ class LibraryDatabase2(LibraryDatabase): continue if not isinstance(tag, unicode): tag = tag.decode(preferred_encoding, 'replace') - t = self.conn.execute('SELECT id FROM tags WHERE name=?', (tag,)).fetchone() + t = self.conn.get('SELECT id FROM tags WHERE name=?', (tag,), all=False) if t: - tid = t[0] + tid = t 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(): + 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() @@ -872,9 +845,9 @@ class LibraryDatabase2(LibraryDatabase): if series: if not isinstance(series, unicode): series = series.decode(preferred_encoding, 'replace') - s = self.conn.execute('SELECT id from series WHERE name=?', (series,)).fetchone() + s = self.conn.get('SELECT id from series WHERE name=?', (series,), all=False) if s: - aid = s[0] + 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)) @@ -904,7 +877,7 @@ class LibraryDatabase2(LibraryDatabase): def add_books(self, paths, formats, metadata, uris=[], add_duplicates=True): ''' Add a book to the database. The result cache is not updated. - @param paths: List of paths to book files of file-like objects + @param paths: List of paths to book files or file-like objects ''' formats, metadata, uris = iter(formats), iter(metadata), iter(uris) duplicates = [] @@ -965,7 +938,7 @@ class LibraryDatabase2(LibraryDatabase): def move_library_to(self, newloc, progress=None): header = _(u'

Copying books to %s

')%newloc - books = self.conn.execute('SELECT id, path, title FROM books').fetchall() + books = self.conn.get('SELECT id, path, title FROM books') if progress is not None: progress.setValue(0) progress.setLabelText(header) @@ -1047,6 +1020,7 @@ class LibraryDatabase2(LibraryDatabase): x['formats'].append(path%fmt.lower()) x['fmt_'+fmt.lower()] = path%fmt.lower() x['available_formats'] = [i.upper() for i in formats.split(',')] + return data def migrate_old(self, db, progress): @@ -1056,7 +1030,7 @@ class LibraryDatabase2(LibraryDatabase): QCoreApplication.processEvents() db.conn.row_factory = lambda cursor, row : tuple(row) db.conn.text_factory = lambda x : unicode(x, 'utf-8', 'replace') - books = db.conn.execute('SELECT id, title, sort, timestamp, uri, series_index, author_sort, isbn FROM books ORDER BY id ASC').fetchall() + books = db.conn.get('SELECT id, title, sort, timestamp, uri, series_index, author_sort, isbn FROM books ORDER BY id ASC') progress.setAutoReset(False) progress.setRange(0, len(books)) @@ -1072,7 +1046,7 @@ books_ratings_link books_series_link feeds '''.split() for table in tables: - rows = db.conn.execute('SELECT * FROM %s ORDER BY id ASC'%table).fetchall() + rows = db.conn.get('SELECT * FROM %s ORDER BY id ASC'%table) for row in rows: self.conn.execute('INSERT INTO %s VALUES(%s)'%(table, ','.join(repeat('?', len(row)))), row) diff --git a/src/calibre/library/server.py b/src/calibre/library/server.py new file mode 100644 index 0000000000..d1d48145f7 --- /dev/null +++ b/src/calibre/library/server.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python +__license__ = 'GPL v3' +__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net' +__docformat__ = 'restructuredtext en' + +''' +HTTP server for remote access to the calibre database. +''' + +import sys +from BaseHTTPServer import HTTPServer, BaseHTTPRequestHandler + +from calibre.constants import __version__ + +class Server(HTTPServer): + pass + +class DBHandler(BaseHTTPRequestHandler): + + server_version = 'calibre/'+__version__ + + def set_db(self, db): + self.db = db + + +def server(db, port=80): + server = Server(('', port), DBHandler) + +def main(args=sys.argv): + from calibre.utils.config import prefs + from calibre.library.database2 import LibraryDatabase2 + db = LibraryDatabase2(prefs['library_path']) + try: + print 'Starting server...' + s = server() + s.server_forever() + except KeyboardInterrupt: + print 'Server interrupted' + s.socket.close() + return 0 + +if __name__ == '__main__': + sys.exit(main()) \ No newline at end of file diff --git a/src/calibre/library/sqlite.py b/src/calibre/library/sqlite.py new file mode 100644 index 0000000000..15d85dd362 --- /dev/null +++ b/src/calibre/library/sqlite.py @@ -0,0 +1,156 @@ +from __future__ import with_statement +__license__ = 'GPL v3' +__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net' +__docformat__ = 'restructuredtext en' + +''' +Wrapper for multi-threaded access to a single sqlite database connection. Serializes +all calls. +''' +import sqlite3 as sqlite, traceback, re, time +from sqlite3 import IntegrityError +from threading import Thread +from Queue import Queue + + +class Concatenate(object): + '''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): + if not self.ans: + return None + if self.sep: + return self.ans[:-len(self.sep)] + return self.ans + +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() + + +class DBThread(Thread): + + CLOSE = '-------close---------' + + def __init__(self, path, row_factory): + Thread.__init__(self) + self.setDaemon(True) + self.path = path + self.unhandled_error = (None, '') + self.row_factory = row_factory + self.requests = Queue(1) + self.results = Queue(1) + self.conn = None + + def connect(self): + self.conn = sqlite.connect(self.path, factory=Connection, + detect_types=sqlite.PARSE_DECLTYPES|sqlite.PARSE_COLNAMES) + self.conn.row_factory = sqlite.Row if self.row_factory else lambda cursor, row : list(row) + self.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() + + self.conn.create_function('title_sort', 1, title_sort) + + def run(self): + try: + self.connect() + while True: + func, args, kwargs = self.requests.get() + if func == self.CLOSE: + self.conn.close() + break + func = getattr(self.conn, func) + try: + ok, res = True, func(*args, **kwargs) + except Exception, err: + ok, res = False, (err, traceback.format_exc()) + self.results.put((ok, res)) + except Exception, err: + self.unhandled_error = (err, traceback.format_exc()) + +class DatabaseException(Exception): + + def __init__(self, err, tb): + tb = '\n\t'.join(('\tRemote'+tb).splitlines()) + msg = unicode(err) +'\n' + tb + Exception.__init__(self, msg) + self.orig_err = err + self.orig_tb = tb + +def proxy(fn): + ''' Decorator to call methods on the database connection in the proxy thread ''' + def run(self, *args, **kwargs): + if self.proxy.unhandled_error[0] is not None: + raise DatabaseException(*self.proxy.unhandled_error) + self.proxy.requests.put((fn.__name__, args, kwargs)) + ok, res = self.proxy.results.get() + if not ok: + if isinstance(res[0], IntegrityError): + raise IntegrityError(unicode(res[0])) + raise DatabaseException(*res) + return res + return run + + +class ConnectionProxy(object): + + def __init__(self, proxy): + self.proxy = proxy + + def close(self): + if self.proxy.unhandled_error is None: + self.proxy.requests.put((self.proxy.CLOSE, [], {})) + + @proxy + def get(self, query, all=True): pass + + @proxy + def commit(self): pass + + @proxy + def execute(self): pass + + @proxy + def executemany(self): pass + + @proxy + def executescript(self): pass + + @proxy + def create_aggregate(self): pass + + @proxy + def create_function(self): pass + + @proxy + def cursor(self): pass + +def connect(dbpath, row_factory=None): + conn = ConnectionProxy(DBThread(dbpath, row_factory)) + conn.proxy.start() + while conn.proxy.unhandled_error[0] is None and conn.proxy.conn is None: + time.sleep(0.01) + if conn.proxy.unhandled_error[0] is not None: + raise DatabaseException(*conn.proxy.unhandled_error) + return conn \ No newline at end of file diff --git a/src/calibre/library/test.py b/src/calibre/library/test.py new file mode 100644 index 0000000000..3065e4eec0 --- /dev/null +++ b/src/calibre/library/test.py @@ -0,0 +1,90 @@ +#!/usr/bin/env python +__license__ = 'GPL v3' +__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net' +__docformat__ = 'restructuredtext en' + +''' +Unit tests for database layer. +''' + +import sys, unittest, os +from itertools import repeat + +from calibre.ptempfile import PersistentTemporaryDirectory +from calibre.library.database2 import LibraryDatabase2 +from calibre.ebooks.metadata import MetaInformation + +class DBTest(unittest.TestCase): + + img = '\xff\xd8\xff\xe0\x00\x10JFIF\x00\x01\x01\x01\x00d\x00d\x00\x00\xff\xdb\x00C\x00\x05\x03\x04\x04\x04\x03\x05\x04\x04\x04\x05\x05\x05\x06\x07\x0c\x08\x07\x07\x07\x07\x0f\x0b\x0b\t\x0c\x11\x0f\x12\x12\x11\x0f\x11\x11\x13\x16\x1c\x17\x13\x14\x1a\x15\x11\x11\x18!\x18\x1a\x1d\x1d\x1f\x1f\x1f\x13\x17"$"\x1e$\x1c\x1e\x1f\x1e\xff\xdb\x00C\x01\x05\x05\x05\x07\x06\x07\x0e\x08\x08\x0e\x1e\x14\x11\x14\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\x1e\xff\xc0\x00\x11\x08\x00\x01\x00\x01\x03\x01\x11\x00\x02\x11\x01\x03\x11\x01\xff\xc4\x00\x14\x00\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x04\xff\xc4\x00\x14\x10\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xc4\x00\x14\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x06\xff\xc4\x00\x14\x11\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\xff\xda\x00\x0c\x03\x01\x00\x02\x11\x03\x11\x00?\x00p\xf9+\xff\xd9' + + def setUp(self): + self.tdir = PersistentTemporaryDirectory('_calibre_dbtest') + self.db = LibraryDatabase2(self.tdir) + f = open(os.path.join(self.tdir, 'test.txt'), 'w+b') + f.write('test') + paths = list(repeat(f, 3)) + formats = list(repeat('txt', 3)) + m1 = MetaInformation('Test Ebook 1', ['Test Author 1']) + m1.tags = ['tag1', 'tag2'] + m1.publisher = 'Test Publisher 1' + m1.rating = 2 + m1.series = 'Test Series 1' + m1.series_index = 3 + m1.author_sort = 'as1' + m1.isbn = 'isbn1' + m1.cover_data = ('jpg', self.img) + m2 = MetaInformation('Test Ebook 2', ['Test Author 2']) + m2.tags = ['tag3', 'tag4'] + m2.publisher = 'Test Publisher 2' + m2.rating = 3 + m2.series = 'Test Series 2' + m2.series_index = 1 + m2.author_sort = 'as1' + m2.isbn = 'isbn1' + self.db.add_books(paths, formats, [m1, m2, m2], add_duplicates=True) + self.m1, self.m2 = m1, m2 + + def testAdding(self): + m1, m2 = self.db.get_metadata(1, True), self.db.get_metadata(2, True) + for p in ('title', 'authors', 'publisher', 'rating', 'series', + 'series_index', 'author_sort', 'isbn', 'tags'): + + def ga(mi, p): + val = getattr(mi, p) + if isinstance(val, list): + val = set(val) + return val + + self.assertEqual(ga(self.m1, p), ga(m1, p)) + self.assertEqual(ga(self.m2, p), ga(m2, p)) + + self.assertEqual(self.db.format(1, 'txt', index_is_id=True), 'test') + self.assertNotEqual(self.db.cover(1, index_is_id=True), None) + self.assertEqual(self.db.cover(2, index_is_id=True), None) + + def testMetadata(self): + self.db.refresh('timestamp', True) + for x in ('title', 'author_sort', 'series', 'publisher', 'isbn', 'series_index', 'rating'): + val = 3 if x in ['rating', 'series_index'] else 'dummy' + getattr(self.db, 'set_'+x)(3, val) + self.db.refresh_ids([3]) + self.assertEqual(getattr(self.db, x)(2), val) + + self.db.set_authors(3, ['new auth']) + self.assertEqual(self.db.format(3, 'txt', index_is_id=True), 'test') + + +def suite(): + return unittest.TestLoader().loadTestsFromTestCase(DBTest) + +def test(): + unittest.TextTestRunner(verbosity=2).run(suite()) + + +def main(args=sys.argv): + test() + return 0 + +if __name__ == '__main__': + sys.exit(main()) \ No newline at end of file