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. '''
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

View File

@ -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"]'):
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 not root.xpath(
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:',
etree.tostring(item, with_tail=False))
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)

View File

@ -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] = []
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')

View File

@ -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