mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
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:
parent
ec18604949
commit
b97141b208
@ -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
|
||||||
|
@ -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)
|
||||||
|
@ -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')
|
||||||
|
@ -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
|
||||||
|
Loading…
x
Reference in New Issue
Block a user