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

This commit is contained in:
Kovid Goyal 2010-05-19 17:52:37 -06:00
parent ec18604949
commit b97141b208
4 changed files with 67 additions and 58 deletions

View File

@ -396,14 +396,6 @@ class BookList(list):
''' Return True if the the device supports tags (collections) for this book list. ''' ''' Return True if the the device supports tags (collections) for this book list. '''
raise NotImplementedError() 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): def add_book(self, book, replace_metadata):
''' '''
Add the book to the booklist. Intent is to maintain any device-internal Add the book to the booklist. Intent is to maintain any device-internal

View File

@ -101,16 +101,25 @@ class XMLCache(object):
# Playlist management {{{ # Playlist management {{{
def purge_broken_playlist_items(self, root): def purge_broken_playlist_items(self, root):
for item in root.xpath( for pl in root.xpath('//*[local-name()="playlist"]'):
'//*[local-name()="playlist"]/*[local-name()="item"]'): seen = set([])
id_ = item.get('id', None) for item in list(pl):
if id_ is None or not root.xpath( id_ = item.get('id', None)
'//*[local-name()!="item" and @id="%s"]'%id_): if id_ is None or id_ in seen or not root.xpath(
if DEBUG: '//*[local-name()!="item" and @id="%s"]'%id_):
prints('Purging broken playlist item:', if DEBUG:
etree.tostring(item, with_tail=False)) if id_ is None:
item.getparent().remove(item) 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): def prune_empty_playlists(self):
for i, root in self.record_roots.items(): for i, root in self.record_roots.items():
@ -175,6 +184,8 @@ class XMLCache(object):
# }}} # }}}
def fix_ids(self): # {{{ def fix_ids(self): # {{{
if DEBUG:
prints('Running fix_ids()')
def ensure_numeric_ids(root): def ensure_numeric_ids(root):
idmap = {} idmap = {}
@ -294,13 +305,8 @@ class XMLCache(object):
break break
if book.lpath in playlist_map: if book.lpath in playlist_map:
tags = playlist_map[book.lpath] tags = playlist_map[book.lpath]
if tags: book.device_collections = tags
if DEBUG:
prints('Adding tags:', tags, 'to', book.title)
if not book.tags:
book.tags = []
book.tags = list(book.tags)
book.tags += tags
# }}} # }}}
# Update XML from JSON {{{ # Update XML from JSON {{{
@ -359,7 +365,25 @@ class XMLCache(object):
nsmap=playlist.nsmap, attrib={'id':id_}) nsmap=playlist.nsmap, attrib={'id':id_})
playlist.append(item) 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): def create_text_record(self, root, bl_id, lpath):
namespace = self.namespaces[bl_id] namespace = self.namespaces[bl_id]
@ -414,8 +438,19 @@ class XMLCache(object):
child.iterchildren(reversed=True).next().tail = '\n'+'\t'*level child.iterchildren(reversed=True).next().tail = '\n'+'\t'*level
root.iterchildren(reversed=True).next().tail = '\n'+'\t'*(level-1) 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): def write(self):
for i, path in self.paths.items(): for i, path in self.paths.items():
self.move_playlists_to_bottom()
self.cleanup_whitespace(i) self.cleanup_whitespace(i)
raw = etree.tostring(self.roots[i], encoding='UTF-8', raw = etree.tostring(self.roots[i], encoding='UTF-8',
xml_declaration=True) xml_declaration=True)

View File

@ -14,14 +14,14 @@ from calibre import isbytestring
class Book(MetaInformation): class Book(MetaInformation):
BOOK_ATTRS = ['lpath', 'size', 'mime'] BOOK_ATTRS = ['lpath', 'size', 'mime', 'device_collections']
JSON_ATTRS = [ JSON_ATTRS = [
'lpath', 'title', 'authors', 'mime', 'size', 'tags', 'author_sort', 'lpath', 'title', 'authors', 'mime', 'size', 'tags', 'author_sort',
'title_sort', 'comments', 'category', 'publisher', 'series', 'title_sort', 'comments', 'category', 'publisher', 'series',
'series_index', 'rating', 'isbn', 'language', 'application_id', 'series_index', 'rating', 'isbn', 'language', 'application_id',
'book_producer', 'lccn', 'lcc', 'ddc', 'rights', 'publication_type', 'book_producer', 'lccn', 'lcc', 'ddc', 'rights', 'publication_type',
'uuid' 'uuid',
] ]
def __init__(self, prefix, lpath, size=None, other=None): def __init__(self, prefix, lpath, size=None, other=None):
@ -29,6 +29,7 @@ class Book(MetaInformation):
MetaInformation.__init__(self, '') MetaInformation.__init__(self, '')
self.device_collections = []
self.path = os.path.join(prefix, lpath) self.path = os.path.join(prefix, lpath)
if os.sep == '\\': if os.sep == '\\':
self.path = self.path.replace('/', '\\') self.path = self.path.replace('/', '\\')
@ -45,27 +46,7 @@ class Book(MetaInformation):
self.smart_update(other) self.smart_update(other)
def __eq__(self, other): def __eq__(self, other):
spath = self.path return self.path == getattr(other, 'path', None)
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
@dynamic_property @dynamic_property
def db_id(self): def db_id(self):
@ -119,9 +100,6 @@ class BookList(_BookList):
def supports_tags(self): def supports_tags(self):
return True return True
def set_tags(self, book, tags):
book.tags = tags
def add_book(self, book, replace_metadata): def add_book(self, book, replace_metadata):
if book not in self: if book not in self:
self.append(book) self.append(book)
@ -134,6 +112,7 @@ class BookList(_BookList):
def get_collections(self, collection_attributes): def get_collections(self, collection_attributes):
collections = {} collections = {}
series_categories = set([]) series_categories = set([])
collection_attributes = list(collection_attributes)+['device_collections']
for attr in collection_attributes: for attr in collection_attributes:
for book in self: for book in self:
val = getattr(book, attr, None) val = getattr(book, attr, None)
@ -147,9 +126,12 @@ class BookList(_BookList):
for category in val: for category in val:
if category not in collections: if category not in collections:
collections[category] = [] collections[category] = []
collections[category].append(book) if book not in collections[category]:
if attr == 'series': collections[category].append(book)
series_categories.add(category) if attr == 'series':
series_categories.add(category)
# Sort collections
for category, books in collections.items(): for category, books in collections.items():
def tgetter(x): def tgetter(x):
return getattr(x, 'title_sort', 'zzzz') return getattr(x, 'title_sort', 'zzzz')

View File

@ -807,7 +807,7 @@ class DeviceBooksModel(BooksModel): # {{{
'authors' : _('Author(s)'), 'authors' : _('Author(s)'),
'timestamp' : _('Date'), 'timestamp' : _('Date'),
'size' : _('Size'), 'size' : _('Size'),
'tags' : _('Tags') 'tags' : _('Collections')
} }
self.marked_for_deletion = {} self.marked_for_deletion = {}
self.search_engine = OnDeviceSearch(self) self.search_engine = OnDeviceSearch(self)
@ -1000,7 +1000,7 @@ class DeviceBooksModel(BooksModel): # {{{
dt = dt_factory(dt, assume_utc=True, as_utc=False) dt = dt_factory(dt, assume_utc=True, as_utc=False)
return QVariant(strftime(TIME_FMT, dt.timetuple())) return QVariant(strftime(TIME_FMT, dt.timetuple()))
elif cname == 'tags': elif cname == 'tags':
tags = self.db[self.map[row]].tags tags = self.db[self.map[row]].device_collections
if tags: if tags:
return QVariant(', '.join(tags)) return QVariant(', '.join(tags))
elif role == Qt.ToolTipRole and index.isValid(): elif role == Qt.ToolTipRole and index.isValid():
@ -1047,7 +1047,7 @@ class DeviceBooksModel(BooksModel): # {{{
elif cname == 'tags': elif cname == 'tags':
tags = [i.strip() for i in val.split(',')] tags = [i.strip() for i in val.split(',')]
tags = [t for t in tags if t] 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.dataChanged.emit(index, index)
self.booklist_dirtied.emit() self.booklist_dirtied.emit()
done = True done = True