From b97141b2080bb5d04c0ec13bf1e0306ad325e0e5 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 19 May 2010 17:52:37 -0600 Subject: [PATCH] Change the tags column in the device view to a Collections column that allows the user to directly edit collections on the device. Note that if the user deletes a collection taht corresponds to some data in the calibre library that would be turned intoa coleection, then the deletion has no effect, on device reconnect --- src/calibre/devices/interface.py | 8 --- src/calibre/devices/prs505/sony_cache.py | 71 ++++++++++++++++++------ src/calibre/devices/usbms/books.py | 40 ++++--------- src/calibre/gui2/library/models.py | 6 +- 4 files changed, 67 insertions(+), 58 deletions(-) diff --git a/src/calibre/devices/interface.py b/src/calibre/devices/interface.py index be58bc9b0c..df2d5500e4 100644 --- a/src/calibre/devices/interface.py +++ b/src/calibre/devices/interface.py @@ -396,14 +396,6 @@ class BookList(list): ''' Return True if the the device supports tags (collections) for this book list. ''' raise NotImplementedError() - def set_tags(self, book, tags): - ''' - Set the tags for C{book} to C{tags}. - @param tags: A list of strings. Can be empty. - @param book: A book object that is in this BookList. - ''' - raise NotImplementedError() - def add_book(self, book, replace_metadata): ''' Add the book to the booklist. Intent is to maintain any device-internal diff --git a/src/calibre/devices/prs505/sony_cache.py b/src/calibre/devices/prs505/sony_cache.py index 14ac03c777..ec4c263cf9 100644 --- a/src/calibre/devices/prs505/sony_cache.py +++ b/src/calibre/devices/prs505/sony_cache.py @@ -101,16 +101,25 @@ class XMLCache(object): # Playlist management {{{ def purge_broken_playlist_items(self, root): - for item in root.xpath( - '//*[local-name()="playlist"]/*[local-name()="item"]'): - id_ = item.get('id', None) - if id_ is None or not root.xpath( - '//*[local-name()!="item" and @id="%s"]'%id_): - if DEBUG: - prints('Purging broken playlist item:', - etree.tostring(item, with_tail=False)) - item.getparent().remove(item) - + for pl in root.xpath('//*[local-name()="playlist"]'): + seen = set([]) + for item in list(pl): + id_ = item.get('id', None) + if id_ is None or id_ in seen or not root.xpath( + '//*[local-name()!="item" and @id="%s"]'%id_): + if DEBUG: + if id_ is None: + cause = 'invalid id' + elif id_ in seen: + cause = 'duplicate item' + else: + cause = 'id not found' + prints('Purging broken playlist item:', + id_, 'from playlist:', pl.get('title', None), + 'because:', cause) + item.getparent().remove(item) + continue + seen.add(id_) def prune_empty_playlists(self): for i, root in self.record_roots.items(): @@ -175,6 +184,8 @@ class XMLCache(object): # }}} def fix_ids(self): # {{{ + if DEBUG: + prints('Running fix_ids()') def ensure_numeric_ids(root): idmap = {} @@ -294,13 +305,8 @@ class XMLCache(object): break if book.lpath in playlist_map: tags = playlist_map[book.lpath] - if tags: - if DEBUG: - prints('Adding tags:', tags, 'to', book.title) - if not book.tags: - book.tags = [] - book.tags = list(book.tags) - book.tags += tags + book.device_collections = tags + # }}} # Update XML from JSON {{{ @@ -359,7 +365,25 @@ class XMLCache(object): nsmap=playlist.nsmap, attrib={'id':id_}) playlist.append(item) - + # Delete playlist entries not in collections + for playlist in root.xpath('//*[local-name()="playlist"]'): + title = playlist.get('title', None) + if title not in collections: + if DEBUG: + prints('Deleting playlist:', playlist.get('title', '')) + playlist.getparent().remove(playlist) + continue + books = collections[title] + records = [self.book_by_lpath(b.lpath, root) for b in books] + records = [x for x in records if x is not None] + ids = [x.get('id', None) for x in records] + ids = [x for x in ids if x is not None] + for item in list(playlist): + if item.get('id', None) not in ids: + if DEBUG: + prints('Deleting item:', item.get('id', ''), + 'from playlist:', playlist.get('title', '')) + playlist.remove(item) def create_text_record(self, root, bl_id, lpath): namespace = self.namespaces[bl_id] @@ -414,8 +438,19 @@ class XMLCache(object): child.iterchildren(reversed=True).next().tail = '\n'+'\t'*level root.iterchildren(reversed=True).next().tail = '\n'+'\t'*(level-1) + def move_playlists_to_bottom(self): + for root in self.record_roots.values(): + seen = [] + for pl in root.xpath('//*[local-name()="playlist"]'): + pl.getparent().remove(pl) + seen.append(pl) + for pl in seen: + root.append(pl) + + def write(self): for i, path in self.paths.items(): + self.move_playlists_to_bottom() self.cleanup_whitespace(i) raw = etree.tostring(self.roots[i], encoding='UTF-8', xml_declaration=True) diff --git a/src/calibre/devices/usbms/books.py b/src/calibre/devices/usbms/books.py index 97c911283b..a0e3dd01d2 100644 --- a/src/calibre/devices/usbms/books.py +++ b/src/calibre/devices/usbms/books.py @@ -14,14 +14,14 @@ from calibre import isbytestring class Book(MetaInformation): - BOOK_ATTRS = ['lpath', 'size', 'mime'] + BOOK_ATTRS = ['lpath', 'size', 'mime', 'device_collections'] JSON_ATTRS = [ 'lpath', 'title', 'authors', 'mime', 'size', 'tags', 'author_sort', 'title_sort', 'comments', 'category', 'publisher', 'series', 'series_index', 'rating', 'isbn', 'language', 'application_id', 'book_producer', 'lccn', 'lcc', 'ddc', 'rights', 'publication_type', - 'uuid' + 'uuid', ] def __init__(self, prefix, lpath, size=None, other=None): @@ -29,6 +29,7 @@ class Book(MetaInformation): MetaInformation.__init__(self, '') + self.device_collections = [] self.path = os.path.join(prefix, lpath) if os.sep == '\\': self.path = self.path.replace('/', '\\') @@ -45,27 +46,7 @@ class Book(MetaInformation): self.smart_update(other) def __eq__(self, other): - spath = self.path - opath = other.path - - if not isinstance(self.path, unicode): - try: - spath = unicode(self.path) - except: - try: - spath = self.path.decode(filesystem_encoding) - except: - spath = self.path - if not isinstance(other.path, unicode): - try: - opath = unicode(other.path) - except: - try: - opath = other.path.decode(filesystem_encoding) - except: - opath = other.path - - return spath == opath + return self.path == getattr(other, 'path', None) @dynamic_property def db_id(self): @@ -119,9 +100,6 @@ class BookList(_BookList): def supports_tags(self): return True - def set_tags(self, book, tags): - book.tags = tags - def add_book(self, book, replace_metadata): if book not in self: self.append(book) @@ -134,6 +112,7 @@ class BookList(_BookList): def get_collections(self, collection_attributes): collections = {} series_categories = set([]) + collection_attributes = list(collection_attributes)+['device_collections'] for attr in collection_attributes: for book in self: val = getattr(book, attr, None) @@ -147,9 +126,12 @@ class BookList(_BookList): for category in val: if category not in collections: collections[category] = [] - collections[category].append(book) - if attr == 'series': - series_categories.add(category) + if book not in collections[category]: + collections[category].append(book) + if attr == 'series': + series_categories.add(category) + + # Sort collections for category, books in collections.items(): def tgetter(x): return getattr(x, 'title_sort', 'zzzz') diff --git a/src/calibre/gui2/library/models.py b/src/calibre/gui2/library/models.py index bd8fb20741..43816f3ea0 100644 --- a/src/calibre/gui2/library/models.py +++ b/src/calibre/gui2/library/models.py @@ -807,7 +807,7 @@ class DeviceBooksModel(BooksModel): # {{{ 'authors' : _('Author(s)'), 'timestamp' : _('Date'), 'size' : _('Size'), - 'tags' : _('Tags') + 'tags' : _('Collections') } self.marked_for_deletion = {} self.search_engine = OnDeviceSearch(self) @@ -1000,7 +1000,7 @@ class DeviceBooksModel(BooksModel): # {{{ dt = dt_factory(dt, assume_utc=True, as_utc=False) return QVariant(strftime(TIME_FMT, dt.timetuple())) elif cname == 'tags': - tags = self.db[self.map[row]].tags + tags = self.db[self.map[row]].device_collections if tags: return QVariant(', '.join(tags)) elif role == Qt.ToolTipRole and index.isValid(): @@ -1047,7 +1047,7 @@ class DeviceBooksModel(BooksModel): # {{{ elif cname == 'tags': tags = [i.strip() for i in val.split(',')] tags = [t for t in tags if t] - self.db.set_tags(self.db[idx], tags) + self.db[idx].device_collections = tags self.dataChanged.emit(index, index) self.booklist_dirtied.emit() done = True