From 6c9a6c1020f0641fa7ee7757c0d151de28298891 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Fri, 12 Jul 2013 10:23:38 +0530 Subject: [PATCH] Implement custom book data API --- src/calibre/db/backend.py | 41 ++++++++++++++++++++++++++++++++++ src/calibre/db/cache.py | 30 +++++++++++++++++++++++++ src/calibre/db/legacy.py | 23 +++++++++++++++++++ src/calibre/db/tests/legacy.py | 31 +++++++++++++++++++++++++ 4 files changed, 125 insertions(+) diff --git a/src/calibre/db/backend.py b/src/calibre/db/backend.py index d75106209f..8ebdc4a154 100644 --- a/src/calibre/db/backend.py +++ b/src/calibre/db/backend.py @@ -1175,5 +1175,46 @@ class DB(object): self.rmtree(parent, permanent=permanent) self.conn.executemany( 'DELETE FROM books WHERE id=?', [(x,) for x in path_map]) + + def add_custom_data(self, name, val_map, delete_first): + if delete_first: + self.conn.execute('DELETE FROM books_plugin_data WHERE name=?', (name, )) + self.conn.executemany( + 'INSERT OR REPLACE INTO books_plugin_data (book, name, val) VALUES (?, ?, ?)', + [(book_id, name, json.dumps(val, default=to_json)) + for book_id, val in val_map.iteritems()]) + + def get_custom_book_data(self, name, book_ids, default=None): + book_ids = frozenset(book_ids) + def safe_load(val): + try: + return json.loads(val, object_hook=from_json) + except: + return default + + if len(book_ids) == 1: + bid = next(iter(book_ids)) + ans = {book_id:safe_load(val) for book_id, val in + self.conn.execute('SELECT book, val FROM books_plugin_data WHERE book=? AND name=?', (bid, name))} + return ans or {bid:default} + + ans = {} + for book_id, val in self.conn.execute( + 'SELECT book, val FROM books_plugin_data WHERE name=?', (name,)): + if not book_ids or book_id in book_ids: + val = safe_load(val) + ans[book_id] = val + return ans + + def delete_custom_book_data(self, name, book_ids): + if book_ids: + self.conn.executemany('DELETE FROM books_plugin_data WHERE book=? AND name=?', + [(book_id, name) for book_id in book_ids]) + else: + self.conn.execute('DELETE FROM books_plugin_data WHERE name=?', (name,)) + + def get_ids_for_custom_book_data(self, name): + return frozenset(r[0] for r in self.conn.execute('SELECT book FROM books_plugin_data WHERE name=?', (name,))) + # }}} diff --git a/src/calibre/db/cache.py b/src/calibre/db/cache.py index b8b9ad7436..49f5bd24ec 100644 --- a/src/calibre/db/cache.py +++ b/src/calibre/db/cache.py @@ -1147,6 +1147,36 @@ class Cache(object): else: table.remove_books(book_ids, self.backend) + @write_api + def add_custom_book_data(self, name, val_map, delete_first=False): + ''' Add data for name where val_map is a map of book_ids to values. If + delete_first is True, all previously stored data for name will be + removed. ''' + missing = frozenset(val_map) - self._all_book_ids() + if missing: + raise ValueError('add_custom_book_data: no such book_ids: %d'%missing) + self.backend.add_custom_data(name, val_map, delete_first) + + @read_api + def get_custom_book_data(self, name, book_ids=(), default=None): + ''' Get data for name. By default returns data for all book_ids, pass + in a list of book ids if you only want some data. Returns a map of + book_id to values. If a particular value could not be decoded, uses + default for it. ''' + return self.backend.get_custom_book_data(name, book_ids, default) + + @write_api + def delete_custom_book_data(self, name, book_ids=()): + ''' Delete data for name. By default deletes all data, if you only want + to delete data for some book ids, pass in a list of book ids. ''' + self.backend.delete_custom_book_data(name, book_ids) + + @read_api + def get_ids_for_custom_book_data(self, name): + ''' Return the set of book ids for which name has data. ''' + return self.backend.get_ids_for_custom_book_data(name) + + # }}} class SortKey(object): # {{{ diff --git a/src/calibre/db/legacy.py b/src/calibre/db/legacy.py index b9f23cc5d7..a12e949b55 100644 --- a/src/calibre/db/legacy.py +++ b/src/calibre/db/legacy.py @@ -216,6 +216,29 @@ class LibraryDatabase(object): # }}} + # Custom data {{{ + def add_custom_book_data(self, book_id, name, val): + self.new_api.add_custom_book_data(name, {book_id:val}) + + def add_multiple_custom_book_data(self, name, val_map, delete_first=False): + self.new_api.add_custom_book_data(name, val_map, delete_first=delete_first) + + def get_custom_book_data(self, book_id, name, default=None): + return self.new_api.get_custom_book_data(name, book_ids={book_id}, default=default).get(book_id, default) + + def get_all_custom_book_data(self, name, default=None): + return self.new_api.get_custom_book_data(name, default=default) + + def delete_custom_book_data(self, book_id, name): + self.new_api.delete_custom_book_data(name, book_ids=(book_id,)) + + def delete_all_custom_book_data(self, name): + self.new_api.delete_custom_book_data(name) + + def get_ids_for_custom_book_data(self, name): + return list(self.new_api.get_ids_for_custom_book_data(name)) + # }}} + # Private interface {{{ def __iter__(self): diff --git a/src/calibre/db/tests/legacy.py b/src/calibre/db/tests/legacy.py index ef47f5d7bf..a94e59d17f 100644 --- a/src/calibre/db/tests/legacy.py +++ b/src/calibre/db/tests/legacy.py @@ -245,3 +245,34 @@ class LegacyTest(BaseTest): # }}} + def test_legacy_custom_data(self): # {{{ + 'Test the API for custom data storage' + legacy, old = self.init_legacy(self.cloned_library), self.init_old(self.cloned_library) + for name in ('name1', 'name2', 'name3'): + T = partial(ET, 'add_custom_book_data', old=old, legacy=legacy) + T((1, name, 'val1'))(self) + T((2, name, 'val2'))(self) + T((3, name, 'val3'))(self) + T = partial(ET, 'get_ids_for_custom_book_data', old=old, legacy=legacy) + T((name,))(self) + T = partial(ET, 'get_custom_book_data', old=old, legacy=legacy) + T((1, name, object())) + T((9, name, object())) + T = partial(ET, 'get_all_custom_book_data', old=old, legacy=legacy) + T((name, object())) + T((name+'!', object())) + T = partial(ET, 'delete_custom_book_data', old=old, legacy=legacy) + T((name, 1)) + T = partial(ET, 'get_all_custom_book_data', old=old, legacy=legacy) + T((name, object())) + T = partial(ET, 'delete_all_custom_book_data', old=old, legacy=legacy) + T((name)) + T = partial(ET, 'get_all_custom_book_data', old=old, legacy=legacy) + T((name, object())) + + T = partial(ET, 'add_multiple_custom_book_data', old=old, legacy=legacy) + T(('n', {1:'val1', 2:'val2'}))(self) + T = partial(ET, 'get_all_custom_book_data', old=old, legacy=legacy) + T(('n', object())) + old.close() + # }}}