diff --git a/src/calibre/db/backend.py b/src/calibre/db/backend.py index f998b91ccb..d75106209f 100644 --- a/src/calibre/db/backend.py +++ b/src/calibre/db/backend.py @@ -29,7 +29,7 @@ from calibre.utils.magick.draw import save_cover_data_to from calibre.utils.recycle_bin import delete_tree, delete_file from calibre.db.tables import (OneToOneTable, ManyToOneTable, ManyToManyTable, SizeTable, FormatsTable, AuthorsTable, IdentifiersTable, PathTable, - CompositeTable, LanguagesTable, UUIDTable) + CompositeTable, UUIDTable) # }}} ''' @@ -711,7 +711,6 @@ class DB(object): 'authors':AuthorsTable, 'formats':FormatsTable, 'identifiers':IdentifiersTable, - 'languages':LanguagesTable, }.get(col, ManyToManyTable) tables[col] = cls(col, self.field_metadata[col].copy()) @@ -1165,5 +1164,16 @@ class DB(object): with lopen(path, 'rb') as f: return f.read() + def remove_books(self, path_map, permanent=False): + for book_id, path in path_map.iteritems(): + if path: + path = os.path.join(self.library_path, path) + if os.path.exists(path): + self.rmtree(path, permanent=permanent) + parent = os.path.dirname(path) + if len(os.listdir(parent)) == 0: + self.rmtree(parent, permanent=permanent) + self.conn.executemany( + 'DELETE FROM books WHERE id=?', [(x,) for x in path_map]) # }}} diff --git a/src/calibre/db/cache.py b/src/calibre/db/cache.py index e6044ce6de..ce0582f893 100644 --- a/src/calibre/db/cache.py +++ b/src/calibre/db/cache.py @@ -1129,6 +1129,19 @@ class Cache(object): self._add_format(book_id, fmt, stream_or_path, dbapi=dbapi) return ids, duplicates + @write_api + def remove_books(self, book_ids, permanent=False): + path_map = {} + for book_id in book_ids: + try: + path = self._field_for('path', book_id).replace('/', os.sep) + except: + path = None + path_map[book_id] = path + self.backend.remove_books(path_map, permanent=permanent) + for field in self.fields.itervalues(): + field.table.remove_books(book_ids, self.backend) + # }}} class SortKey(object): # {{{ diff --git a/src/calibre/db/tables.py b/src/calibre/db/tables.py index 6f3343ba12..19c4ade10c 100644 --- a/src/calibre/db/tables.py +++ b/src/calibre/db/tables.py @@ -20,6 +20,10 @@ _c_speedup = plugins['speedup'][0] ONE_ONE, MANY_ONE, MANY_MANY = xrange(3) +class Null: + pass +null = Null() + def _c_convert_timestamp(val): if not val: return None @@ -55,6 +59,9 @@ class Table(object): self.link_table = (link_table if link_table else 'books_%s_link'%self.metadata['table']) + def remove_books(self, book_ids, db): + return set() + class VirtualTable(Table): ''' @@ -83,6 +90,14 @@ class OneToOneTable(Table): self.metadata['column'], self.metadata['table'])): self.book_col_map[row[0]] = self.unserialize(row[1]) + def remove_books(self, book_ids, db): + clean = set() + for book_id in book_ids: + val = self.book_col_map.pop(book_id, null) + if val is not null: + clean.add(val) + return clean + class PathTable(OneToOneTable): def set_path(self, book_id, path, db): @@ -113,6 +128,15 @@ class UUIDTable(OneToOneTable): self.uuid_to_id_map.pop(self.book_col_map.get(book_id, None), None) # discard old uuid self.uuid_to_id_map[uuid] = book_id + def remove_books(self, book_ids, db): + clean = set() + for book_id in book_ids: + val = self.book_col_map.pop(book_id, null) + if val is not null: + self.uuid_to_id_map.pop(val, None) + clean.add(val) + return clean + class CompositeTable(OneToOneTable): def read(self, db): @@ -124,6 +148,9 @@ class CompositeTable(OneToOneTable): self.composite_sort = d.get('composite_sort', False) self.use_decorations = d.get('use_decorations', False) + def remove_books(self, book_ids, db): + return set() + class ManyToOneTable(Table): ''' @@ -156,6 +183,27 @@ class ManyToOneTable(Table): self.col_book_map[row[1]].add(row[0]) self.book_col_map[row[0]] = row[1] + def remove_books(self, book_ids, db): + clean = set() + for book_id in book_ids: + item_id = self.book_col_map.pop(book_id, None) + if item_id is not None: + try: + self.col_book_map[item_id].discard(book_id) + except KeyError: + if self.id_map.pop(item_id, null) is not null: + clean.add(item_id) + else: + if not self.col_book_map[item_id]: + del self.col_book_map[item_id] + if self.id_map.pop(item_id, null) is not null: + clean.add(item_id) + if clean: + db.conn.executemany( + 'DELETE FROM {0} WHERE id=?'.format(self.metadata['table']), + [(x,) for x in clean]) + return clean + class ManyToManyTable(ManyToOneTable): ''' @@ -166,6 +214,7 @@ class ManyToManyTable(ManyToOneTable): table_type = MANY_MANY selectq = 'SELECT book, {0} FROM {1} ORDER BY id' + do_clean_on_remove = True def read_maps(self, db): for row in db.conn.execute( @@ -180,6 +229,27 @@ class ManyToManyTable(ManyToOneTable): for key in tuple(self.book_col_map.iterkeys()): self.book_col_map[key] = tuple(self.book_col_map[key]) + def remove_books(self, book_ids, db): + clean = set() + for book_id in book_ids: + item_ids = self.book_col_map.pop(book_id, ()) + for item_id in item_ids: + try: + self.col_book_map[item_id].discard(book_id) + except KeyError: + if self.id_map.pop(item_id, null) is not null: + clean.add(item_id) + else: + if not self.col_book_map[item_id]: + del self.col_book_map[item_id] + if self.id_map.pop(item_id, null) is not null: + clean.add(item_id) + if clean and self.do_clean_on_remove: + db.conn.executemany( + 'DELETE FROM {0} WHERE id=?'.format(self.metadata['table']), + [(x,) for x in clean]) + return clean + class AuthorsTable(ManyToManyTable): def read_id_maps(self, db): @@ -197,8 +267,17 @@ class AuthorsTable(ManyToManyTable): db.conn.executemany('UPDATE authors SET sort=? WHERE id=?', [(v, k) for k, v in aus_map.iteritems()]) + def remove_books(self, book_ids, db): + clean = ManyToManyTable.remove_books(self, book_ids, db) + for item_id in clean: + self.alink_map.pop(item_id, None) + self.asort_map.pop(item_id, None) + return clean + class FormatsTable(ManyToManyTable): + do_clean_on_remove = False + def read_id_maps(self, db): pass @@ -220,6 +299,13 @@ class FormatsTable(ManyToManyTable): for key in tuple(self.book_col_map.iterkeys()): self.book_col_map[key] = tuple(sorted(self.book_col_map[key])) + def remove_books(self, book_ids, db): + clean = ManyToManyTable.remove_books(self, book_ids, db) + for book_id in book_ids: + self.fname_map.pop(book_id, None) + self.size_map.pop(book_id, None) + return clean + def set_fname(self, book_id, fmt, fname, db): self.fname_map[book_id][fmt] = fname db.conn.execute('UPDATE data SET name=? WHERE book=? AND format=?', @@ -280,8 +366,19 @@ class IdentifiersTable(ManyToManyTable): self.book_col_map[row[0]] = {} self.book_col_map[row[0]][row[1]] = row[2] -class LanguagesTable(ManyToManyTable): + def remove_books(self, book_ids, db): + clean = set() + for book_id in book_ids: + item_map = self.book_col_map.pop(book_id, {}) + for item_id in item_map: + try: + self.col_book_map[item_id].discard(book_id) + except KeyError: + clean.add(item_id) + else: + if not self.col_book_map[item_id]: + del self.col_book_map[item_id] + clean.add(item_id) + return clean - def read_id_maps(self, db): - ManyToManyTable.read_id_maps(self, db)