From 51018ff76f9f9345dd12a92fdbbf767e4303dc2e Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 15 Jul 2013 13:34:43 +0530 Subject: [PATCH] tags_older_than() --- src/calibre/db/__init__.py | 6 ++--- src/calibre/db/cache.py | 45 ++++++++++++++++++++++++++++++++++ src/calibre/db/legacy.py | 4 +++ src/calibre/db/tests/legacy.py | 20 +++++++++------ 4 files changed, 64 insertions(+), 11 deletions(-) diff --git a/src/calibre/db/__init__.py b/src/calibre/db/__init__.py index 8f83a6bb81..3a14552281 100644 --- a/src/calibre/db/__init__.py +++ b/src/calibre/db/__init__.py @@ -107,12 +107,10 @@ Various things that require other things before they can be migrated: 1. From initialize_dynamic(): set_saved_searches, load_user_template_functions. Also add custom columns/categories/searches info into - self.field_metadata. Finally, implement metadata dirtied - functionality. + self.field_metadata. 2. Catching DatabaseException and sqlite.Error when creating new libraries/switching/on calibre startup. - 3. From refresh in the legacy interface: Rember to flush the composite - column template cache. + 3. Port library/restore.py 4. Replace the metadatabackup thread with the new implementation when using the new backend. 5. In the new API refresh() does not re-read from disk. That might break a few things, for example content server reloading on db change as well as diff --git a/src/calibre/db/cache.py b/src/calibre/db/cache.py index d278e880b7..28fff9268b 100644 --- a/src/calibre/db/cache.py +++ b/src/calibre/db/cache.py @@ -1280,6 +1280,51 @@ class Cache(object): def refresh_ondevice(self): self.fields['ondevice'].clear_caches() + @read_api + def tags_older_than(self, tag, delta=None, must_have_tag=None, must_have_authors=None): + ''' + Return the ids of all books having the tag ``tag`` that are older than + than the specified time. tag comparison is case insensitive. + + :param delta: A timedelta object or None. If None, then all ids with + the tag are returned. + :param must_have_tag: If not None the list of matches will be + restricted to books that have this tag + :param must_have_authors: A list of authors. If not None the list of + matches will be restricted to books that have these authors (case + insensitive). + ''' + tag_map = {icu_lower(v):k for k, v in self._get_id_map('tags').iteritems()} + tag = icu_lower(tag.strip()) + mht = icu_lower(must_have_tag.strip()) if must_have_tag else None + tag_id, mht_id = tag_map.get(tag, None), tag_map.get(mht, None) + ans = set() + if mht_id is None and mht: + return ans + if tag_id is not None: + tagged_books = self._books_for_field('tags', tag_id) + if mht_id is not None and tagged_books: + tagged_books = tagged_books.intersection(self._books_for_field('tags', mht_id)) + if tagged_books: + if must_have_authors is not None: + amap = {icu_lower(v):k for k, v in self._get_id_map('authors').iteritems()} + books = None + for author in must_have_authors: + abooks = self._books_for_field('authors', amap.get(icu_lower(author), None)) + books = abooks if books is None else books.intersection(abooks) + if not books: + break + tagged_books = tagged_books.intersection(books or set()) + if delta is None: + ans = tagged_books + else: + now = nowf() + for book_id in tagged_books: + ts = self._field_for('timestamp', book_id) + if (now - ts) > delta: + ans.add(book_id) + return ans + # }}} class SortKey(object): # {{{ diff --git a/src/calibre/db/legacy.py b/src/calibre/db/legacy.py index b2ad232f24..62cfe08838 100644 --- a/src/calibre/db/legacy.py +++ b/src/calibre/db/legacy.py @@ -472,6 +472,10 @@ class LibraryDatabase(object): def refresh_ondevice(self): self.new_api.refresh_ondevice() + def tags_older_than(self, tag, delta, must_have_tag=None, must_have_authors=None): + for book_id in sorted(self.new_api.tags_older_than(tag, delta=delta, must_have_tag=must_have_tag, must_have_authors=must_have_authors)): + yield book_id + # Private interface {{{ def __iter__(self): for row in self.data.iterall(): diff --git a/src/calibre/db/tests/legacy.py b/src/calibre/db/tests/legacy.py index 3f2c8729b8..4d25c8798a 100644 --- a/src/calibre/db/tests/legacy.py +++ b/src/calibre/db/tests/legacy.py @@ -153,13 +153,19 @@ class LegacyTest(BaseTest): # }}} def test_legacy_direct(self): # {{{ - 'Test methods that are directly equivalent in the old and new interface' + 'Test read-only methods that are directly equivalent in the old and new interface' from calibre.ebooks.metadata.book.base import Metadata + from datetime import timedelta ndb = self.init_legacy(self.cloned_library) db = self.init_old() for meth, args in { 'get_next_series_num_for': [('A Series One',)], + '@tags_older_than': [ + ('News', None), ('Tag One', None), ('xxxx', None), ('Tag One', None, 'News'), ('News', None, 'xxxx'), + ('News', None, None, ['xxxxxxx']), ('News', None, 'Tag One', ['Author Two', 'Author One']), + ('News', timedelta(0), None, None), ('News', timedelta(100000)), + ], 'format':[(1, 'FMT1', True), (2, 'FMT1', True), (0, 'xxxxxx')], 'has_format':[(1, 'FMT1', True), (2, 'FMT1', True), (0, 'xxxxxx')], '@format_files':[(0,),(1,),(2,)], @@ -208,13 +214,13 @@ class LegacyTest(BaseTest): 'books_in_series_of':[(0,), (1,), (2,)], 'books_with_same_title':[(Metadata(db.title(0)),), (Metadata(db.title(1)),), (Metadata('1234'),)], }.iteritems(): + fmt = lambda x: x + if meth[0] in {'!', '@'}: + fmt = {'!':dict, '@':frozenset}[meth[0]] + meth = meth[1:] + elif meth == 'get_authors_with_ids': + fmt = lambda val:{x[0]:tuple(x[1:]) for x in val} for a in args: - fmt = lambda x: x - if meth[0] in {'!', '@'}: - fmt = {'!':dict, '@':frozenset}[meth[0]] - meth = meth[1:] - elif meth == 'get_authors_with_ids': - fmt = lambda val:{x[0]:tuple(x[1:]) for x in val} self.assertEqual(fmt(getattr(db, meth)(*a)), fmt(getattr(ndb, meth)(*a)), 'The method: %s() returned different results for argument %s' % (meth, a)) db.close()