Drag and drop to Tag Browser. Fix #7078 (Book Details missing). Various minor bug fixes

This commit is contained in:
Kovid Goyal 2010-10-06 11:04:41 -06:00
commit abe8bd9a25
18 changed files with 220 additions and 90 deletions

View File

@ -21,6 +21,7 @@ class FOLDER_DEVICE_FOR_CONFIG(USBMS):
VENDOR_ID = 0xffff VENDOR_ID = 0xffff
PRODUCT_ID = 0xffff PRODUCT_ID = 0xffff
BCD = 0xffff BCD = 0xffff
DEVICE_PLUGBOARD_NAME = 'FOLDER_DEVICE'
class FOLDER_DEVICE(USBMS): class FOLDER_DEVICE(USBMS):
@ -36,6 +37,7 @@ class FOLDER_DEVICE(USBMS):
VENDOR_ID = 0xffff VENDOR_ID = 0xffff
PRODUCT_ID = 0xffff PRODUCT_ID = 0xffff
BCD = 0xffff BCD = 0xffff
DEVICE_PLUGBOARD_NAME = 'FOLDER_DEVICE'
THUMBNAIL_HEIGHT = 68 # Height for thumbnails on device THUMBNAIL_HEIGHT = 68 # Height for thumbnails on device

View File

@ -325,8 +325,9 @@ class KOBO(USBMS):
book = Book(prefix, lpath, '', '', '', '', '', '', other=info) book = Book(prefix, lpath, '', '', '', '', '', '', other=info)
if book.size is None: if book.size is None:
book.size = os.stat(self.normalize_path(path)).st_size book.size = os.stat(self.normalize_path(path)).st_size
book._new_book = True # Must be before add_book b = booklists[blist].add_book(book, replace_metadata=True)
booklists[blist].add_book(book, replace_metadata=True) if b:
b._new_book = True
self.report_progress(1.0, _('Adding books to device metadata listing...')) self.report_progress(1.0, _('Adding books to device metadata listing...'))
def contentid_from_path(self, path, ContentType): def contentid_from_path(self, path, ContentType):

View File

@ -64,6 +64,7 @@ class PRS505(USBMS):
EXTRA_CUSTOMIZATION_DEFAULT = ', '.join(['series', 'tags']) EXTRA_CUSTOMIZATION_DEFAULT = ', '.join(['series', 'tags'])
plugboard = None plugboard = None
plugboard_func = None
def windows_filter_pnp_id(self, pnp_id): def windows_filter_pnp_id(self, pnp_id):
return '_LAUNCHER' in pnp_id return '_LAUNCHER' in pnp_id
@ -152,7 +153,12 @@ class PRS505(USBMS):
else: else:
collections = [] collections = []
debug_print('PRS505: collection fields:', collections) debug_print('PRS505: collection fields:', collections)
c.update(blists, collections, self.plugboard) pb = None
if self.plugboard_func:
pb = self.plugboard_func(self.__class__.__name__,
'device_db', self.plugboards)
debug_print('PRS505: use plugboards', pb)
c.update(blists, collections, pb)
c.write() c.write()
USBMS.sync_booklists(self, booklists, end_session=end_session) USBMS.sync_booklists(self, booklists, end_session=end_session)
@ -165,9 +171,6 @@ class PRS505(USBMS):
c.write() c.write()
debug_print('PRS505: finished rebuild_collections') debug_print('PRS505: finished rebuild_collections')
def use_plugboard_ext(self): def set_plugboards(self, plugboards, pb_func):
return 'device_db' self.plugboards = plugboards
self.plugboard_func = pb_func
def set_plugboard(self, pb):
debug_print('PRS505: use plugboard', pb)
self.plugboard = pb

View File

@ -360,8 +360,9 @@ class XMLCache(object):
if record is None: if record is None:
record = self.create_text_record(root, i, book.lpath) record = self.create_text_record(root, i, book.lpath)
if plugboard is not None: if plugboard is not None:
newmi = book.deepcopy() newmi = book.deepcopy_metadata()
newmi.template_to_attribute(book, plugboard) newmi.template_to_attribute(book, plugboard)
newmi.set('_new_book', getattr(book, '_new_book', False))
else: else:
newmi = book newmi = book
(gtz_count, ltz_count, use_tz_var) = \ (gtz_count, ltz_count, use_tz_var) = \

View File

@ -71,17 +71,21 @@ class BookList(_BookList):
return False return False
def add_book(self, book, replace_metadata): def add_book(self, book, replace_metadata):
'''
Add the book to the booklist, if needed. Return None if the book is
already there and not updated, otherwise return the book.
'''
try: try:
b = self.index(book) b = self.index(book)
except (ValueError, IndexError): except (ValueError, IndexError):
b = None b = None
if b is None: if b is None:
self.append(book) self.append(book)
return True return book
if replace_metadata: if replace_metadata:
self[b].smart_update(book, replace_metadata=True) self[b].smart_update(book, replace_metadata=True)
return True return self[b]
return False return None
def remove_book(self, book): def remove_book(self, book):
self.remove(book) self.remove(book)

View File

@ -242,8 +242,9 @@ class USBMS(CLI, Device):
book = self.book_class(prefix, lpath, other=info) book = self.book_class(prefix, lpath, other=info)
if book.size is None: if book.size is None:
book.size = os.stat(self.normalize_path(path)).st_size book.size = os.stat(self.normalize_path(path)).st_size
book._new_book = True # Must be before add_book b = booklists[blist].add_book(book, replace_metadata=True)
booklists[blist].add_book(book, replace_metadata=True) if b:
b._new_book = True
self.report_progress(1.0, _('Adding books to device metadata listing...')) self.report_progress(1.0, _('Adding books to device metadata listing...'))
debug_print('USBMS: finished adding metadata') debug_print('USBMS: finished adding metadata')

View File

@ -104,7 +104,8 @@ STANDARD_METADATA_FIELDS = SOCIAL_METADATA_FIELDS.union(
SC_FIELDS_NOT_COPIED = frozenset(['title', 'title_sort', 'authors', SC_FIELDS_NOT_COPIED = frozenset(['title', 'title_sort', 'authors',
'author_sort', 'author_sort_map', 'author_sort', 'author_sort_map',
'cover_data', 'tags', 'language']) 'cover_data', 'tags', 'language',
'classifiers'])
# Metadata fields that smart update should copy only if the source is not None # Metadata fields that smart update should copy only if the source is not None
SC_FIELDS_COPY_NOT_NULL = frozenset(['lpath', 'size', 'comments', 'thumbnail']) SC_FIELDS_COPY_NOT_NULL = frozenset(['lpath', 'size', 'comments', 'thumbnail'])
@ -114,8 +115,7 @@ SC_COPYABLE_FIELDS = SOCIAL_METADATA_FIELDS.union(
PUBLICATION_METADATA_FIELDS).union( PUBLICATION_METADATA_FIELDS).union(
BOOK_STRUCTURE_FIELDS).union( BOOK_STRUCTURE_FIELDS).union(
DEVICE_METADATA_FIELDS).union( DEVICE_METADATA_FIELDS).union(
CALIBRE_METADATA_FIELDS).union( CALIBRE_METADATA_FIELDS) - \
TOP_LEVEL_CLASSIFIERS) - \
SC_FIELDS_NOT_COPIED.union( SC_FIELDS_NOT_COPIED.union(
SC_FIELDS_COPY_NOT_NULL) SC_FIELDS_COPY_NOT_NULL)

View File

@ -148,6 +148,11 @@ class Metadata(object):
object.__setattr__(m, '_data', copy.deepcopy(object.__getattribute__(self, '_data'))) object.__setattr__(m, '_data', copy.deepcopy(object.__getattribute__(self, '_data')))
return m return m
def deepcopy_metadata(self):
m = Metadata(None)
object.__setattr__(m, '_data', copy.deepcopy(object.__getattribute__(self, '_data')))
return m
def get(self, field, default=None): def get(self, field, default=None):
try: try:
return self.__getattribute__(field) return self.__getattribute__(field)
@ -164,6 +169,18 @@ class Metadata(object):
def set(self, field, val, extra=None): def set(self, field, val, extra=None):
self.__setattr__(field, val, extra) self.__setattr__(field, val, extra)
def get_classifiers(self):
'''
Return a copy of the classifiers dictionary.
The dict is small, and the penalty for using a reference where a copy is
needed is large. Also, we don't want any manipulations of the returned
dict to show up in the book.
'''
return copy.deepcopy(object.__getattribute__(self, '_data')['classifiers'])
def set_classifiers(self, classifiers):
object.__getattribute__(self, '_data')['classifiers'] = classifiers
# field-oriented interface. Intended to be the same as in LibraryDatabase # field-oriented interface. Intended to be the same as in LibraryDatabase
def standard_field_keys(self): def standard_field_keys(self):
@ -369,6 +386,8 @@ class Metadata(object):
self.set_all_user_metadata(other.get_all_user_metadata(make_copy=True)) self.set_all_user_metadata(other.get_all_user_metadata(make_copy=True))
for x in SC_FIELDS_COPY_NOT_NULL: for x in SC_FIELDS_COPY_NOT_NULL:
copy_not_none(self, other, x) copy_not_none(self, other, x)
if callable(getattr(other, 'get_classifiers', None)):
self.set_classifiers(other.get_classifiers())
# language is handled below # language is handled below
else: else:
for attr in SC_COPYABLE_FIELDS: for attr in SC_COPYABLE_FIELDS:
@ -423,6 +442,17 @@ class Metadata(object):
if len(other_comments.strip()) > len(my_comments.strip()): if len(other_comments.strip()) > len(my_comments.strip()):
self.comments = other_comments self.comments = other_comments
# Copy all the non-none classifiers
if callable(getattr(other, 'get_classifiers', None)):
d = self.get_classifiers()
s = other.get_classifiers()
d.update([v for v in s.iteritems() if v[1] is not None])
self.set_classifiers(d)
else:
# other structure not Metadata. Copy the top-level classifiers
for attr in TOP_LEVEL_CLASSIFIERS:
copy_not_none(self, other, attr)
other_lang = getattr(other, 'language', None) other_lang = getattr(other, 'language', None)
if other_lang and other_lang.lower() != 'und': if other_lang and other_lang.lower() != 'und':
self.language = other_lang self.language = other_lang
@ -432,7 +462,7 @@ class Metadata(object):
v = self.series_index if val is None else val v = self.series_index if val is None else val
try: try:
x = float(v) x = float(v)
except ValueError: except (ValueError, TypeError):
x = 1 x = 1
return fmt_sidx(x) return fmt_sidx(x)
@ -459,6 +489,19 @@ class Metadata(object):
''' '''
returns the tuple (field_name, formatted_value) returns the tuple (field_name, formatted_value)
''' '''
# Handle custom series index
if key.startswith('#') and key.endswith('_index'):
tkey = key[:-6] # strip the _index
cmeta = self.get_user_metadata(tkey, make_copy=False)
if cmeta['datatype'] == 'series':
if self.get(tkey):
res = self.get_extra(tkey)
return (unicode(cmeta['name']+'_index'),
self.format_series_index(res), res, cmeta)
else:
return (unicode(cmeta['name']+'_index'), '', '', cmeta)
if key in self.custom_field_keys(): if key in self.custom_field_keys():
res = self.get(key, None) res = self.get(key, None)
cmeta = self.get_user_metadata(key, make_copy=False) cmeta = self.get_user_metadata(key, make_copy=False)
@ -474,19 +517,21 @@ class Metadata(object):
if datatype == 'text' and cmeta['is_multiple']: if datatype == 'text' and cmeta['is_multiple']:
res = u', '.join(res) res = u', '.join(res)
elif datatype == 'series' and series_with_index: elif datatype == 'series' and series_with_index:
if self.get_extra(key) is not None:
res = res + \ res = res + \
' [%s]'%self.format_series_index(val=self.get_extra(key)) ' [%s]'%self.format_series_index(val=self.get_extra(key))
elif datatype == 'datetime': elif datatype == 'datetime':
res = format_date(res, cmeta['display'].get('date_format','dd MMM yyyy')) res = format_date(res, cmeta['display'].get('date_format','dd MMM yyyy'))
elif datatype == 'bool': elif datatype == 'bool':
res = _('Yes') if res else _('No') res = _('Yes') if res else _('No')
elif datatype == 'float' and key.endswith('_index'):
res = self.format_series_index(res)
return (name, unicode(res), orig_res, cmeta) return (name, unicode(res), orig_res, cmeta)
if key in field_metadata and field_metadata[key]['kind'] == 'field': # Translate aliases into the standard field name
fmkey = field_metadata.search_term_to_field_key(key)
if fmkey in field_metadata and field_metadata[fmkey]['kind'] == 'field':
res = self.get(key, None) res = self.get(key, None)
fmeta = field_metadata[key] fmeta = field_metadata[fmkey]
name = unicode(fmeta['name']) name = unicode(fmeta['name'])
if res is None or res == '': if res is None or res == '':
return (name, res, None, None) return (name, res, None, None)

View File

@ -104,6 +104,28 @@ class DeviceJob(BaseJob): # {{{
# }}} # }}}
def find_plugboard(device_name, format, plugboards):
cpb = None
if format in plugboards:
cpb = plugboards[format]
elif plugboard_any_format_value in plugboards:
cpb = plugboards[plugboard_any_format_value]
if cpb is not None:
if device_name in cpb:
cpb = cpb[device_name]
elif plugboard_any_device_value in cpb:
cpb = cpb[plugboard_any_device_value]
else:
cpb = None
if DEBUG:
prints('Device using plugboard', format, device_name, cpb)
return cpb
def device_name_for_plugboards(device_class):
if hasattr(device_class, 'DEVICE_PLUGBOARD_NAME'):
return device_class.DEVICE_PLUGBOARD_NAME
return device_class.__class__.__name__
class DeviceManager(Thread): # {{{ class DeviceManager(Thread): # {{{
def __init__(self, connected_slot, job_manager, open_feedback_slot, sleep_time=2): def __init__(self, connected_slot, job_manager, open_feedback_slot, sleep_time=2):
@ -311,12 +333,9 @@ class DeviceManager(Thread): # {{{
return self.device.card_prefix(end_session=False), self.device.free_space() return self.device.card_prefix(end_session=False), self.device.free_space()
def sync_booklists(self, done, booklists, plugboards): def sync_booklists(self, done, booklists, plugboards):
if hasattr(self.connected_device, 'use_plugboard_ext') and \ if hasattr(self.connected_device, 'set_plugboards') and \
callable(self.connected_device.use_plugboard_ext): callable(self.connected_device.set_plugboards):
ext = self.connected_device.use_plugboard_ext() self.connected_device.set_plugboards(plugboards, find_plugboard)
if ext is not None:
self.connected_device.set_plugboard(
self.find_plugboard(ext, plugboards))
return self.create_job(self._sync_booklists, done, args=[booklists], return self.create_job(self._sync_booklists, done, args=[booklists],
description=_('Send metadata to device')) description=_('Send metadata to device'))
@ -325,31 +344,18 @@ class DeviceManager(Thread): # {{{
args=[booklist, on_card], args=[booklist, on_card],
description=_('Send collections to device')) description=_('Send collections to device'))
def find_plugboard(self, ext, plugboards):
dev_name = self.connected_device.__class__.__name__
cpb = None
if ext in plugboards:
cpb = plugboards[ext]
elif plugboard_any_format_value in plugboards:
cpb = plugboards[plugboard_any_format_value]
if cpb is not None:
if dev_name in cpb:
cpb = cpb[dev_name]
elif plugboard_any_device_value in cpb:
cpb = cpb[plugboard_any_device_value]
else:
cpb = None
if DEBUG:
prints('Device using plugboard', ext, dev_name, cpb)
return cpb
def _upload_books(self, files, names, on_card=None, metadata=None, plugboards=None): def _upload_books(self, files, names, on_card=None, metadata=None, plugboards=None):
'''Upload books to device: ''' '''Upload books to device: '''
if hasattr(self.connected_device, 'set_plugboards') and \
callable(self.connected_device.set_plugboards):
self.connected_device.set_plugboards(plugboards, find_plugboard)
if metadata and files and len(metadata) == len(files): if metadata and files and len(metadata) == len(files):
for f, mi in zip(files, metadata): for f, mi in zip(files, metadata):
if isinstance(f, unicode): if isinstance(f, unicode):
ext = f.rpartition('.')[-1].lower() ext = f.rpartition('.')[-1].lower()
cpb = self.find_plugboard(ext, plugboards) cpb = find_plugboard(
device_name_for_plugboards(self.connected_device),
ext, plugboards)
if ext: if ext:
try: try:
if DEBUG: if DEBUG:
@ -357,7 +363,7 @@ class DeviceManager(Thread): # {{{
f, file=sys.__stdout__) f, file=sys.__stdout__)
with open(f, 'r+b') as stream: with open(f, 'r+b') as stream:
if cpb: if cpb:
newmi = mi.deepcopy() newmi = mi.deepcopy_metadata()
newmi.template_to_attribute(mi, cpb) newmi.template_to_attribute(mi, cpb)
else: else:
newmi = mi newmi = mi

View File

@ -799,7 +799,7 @@ class BooksModel(QAbstractTableModel): # {{{
self.db.set_pubdate(id, qt_to_dt(val, as_utc=False)) self.db.set_pubdate(id, qt_to_dt(val, as_utc=False))
else: else:
self.db.set(row, column, val) self.db.set(row, column, val)
self.refresh_rows([row], row) self.refresh_ids([id], row)
self.dataChanged.emit(index, index) self.dataChanged.emit(index, index)
return True return True

View File

@ -9,6 +9,7 @@ from PyQt4 import QtGui
from PyQt4.Qt import Qt from PyQt4.Qt import Qt
from calibre.gui2 import error_dialog from calibre.gui2 import error_dialog
from calibre.gui2.device import device_name_for_plugboards
from calibre.gui2.preferences import ConfigWidgetBase, test_widget from calibre.gui2.preferences import ConfigWidgetBase, test_widget
from calibre.gui2.preferences.plugboard_ui import Ui_Form from calibre.gui2.preferences.plugboard_ui import Ui_Form
from calibre.customize.ui import metadata_writers, device_plugins from calibre.customize.ui import metadata_writers, device_plugins
@ -45,11 +46,10 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
else: else:
self.device_label.setText(_('Device currently connected: None')) self.device_label.setText(_('Device currently connected: None'))
self.devices = [''] self.devices = ['', 'APPLE', 'FOLDER_DEVICE']
for device in device_plugins(): for device in device_plugins():
n = device.__class__.__name__ n = device_name_for_plugboards(device)
if n.startswith('FOLDER_DEVICE'): if n not in self.devices:
n = 'FOLDER_DEVICE'
self.devices.append(n) self.devices.append(n)
self.devices.sort(cmp=lambda x, y: cmp(x.lower(), y.lower())) self.devices.sort(cmp=lambda x, y: cmp(x.lower(), y.lower()))
self.devices.insert(1, plugboard_save_to_disk_value) self.devices.insert(1, plugboard_save_to_disk_value)

View File

@ -20,6 +20,7 @@ from calibre.gui2 import config, NONE
from calibre.library.field_metadata import TagsIcons from calibre.library.field_metadata import TagsIcons
from calibre.utils.search_query_parser import saved_searches from calibre.utils.search_query_parser import saved_searches
from calibre.gui2 import error_dialog from calibre.gui2 import error_dialog
from calibre.gui2.dialogs.confirm_delete import confirm
from calibre.gui2.dialogs.tag_categories import TagCategories from calibre.gui2.dialogs.tag_categories import TagCategories
from calibre.gui2.dialogs.tag_list_editor import TagListEditor from calibre.gui2.dialogs.tag_list_editor import TagListEditor
from calibre.gui2.dialogs.edit_authors_dialog import EditAuthorsDialog from calibre.gui2.dialogs.edit_authors_dialog import EditAuthorsDialog
@ -66,6 +67,7 @@ class TagsView(QTreeView): # {{{
author_sort_edit = pyqtSignal(object, object) author_sort_edit = pyqtSignal(object, object)
tag_item_renamed = pyqtSignal() tag_item_renamed = pyqtSignal()
search_item_renamed = pyqtSignal() search_item_renamed = pyqtSignal()
drag_drop_finished = pyqtSignal(object)
def __init__(self, parent=None): def __init__(self, parent=None):
QTreeView.__init__(self, parent=None) QTreeView.__init__(self, parent=None)
@ -122,9 +124,11 @@ class TagsView(QTreeView): # {{{
p = m.parent(idx) p = m.parent(idx)
if idx.isValid() and p.isValid(): if idx.isValid() and p.isValid():
item = m.data(p, Qt.UserRole) item = m.data(p, Qt.UserRole)
if item.type == TagTreeItem.CATEGORY and \ fm = self.db.metadata_for_field(item.category_key)
item.category_key in \ if item.category_key in \
('tags', 'series', 'authors', 'rating', 'publisher'): ('tags', 'series', 'authors', 'rating', 'publisher') or\
(fm['is_custom'] and \
fm['datatype'] in ['text', 'rating', 'series']):
allowed = True allowed = True
if allowed: if allowed:
event.acceptProposedAction() event.acceptProposedAction()
@ -137,18 +141,70 @@ class TagsView(QTreeView): # {{{
p = m.parent(idx) p = m.parent(idx)
if idx.isValid() and p.isValid(): if idx.isValid() and p.isValid():
item = m.data(p, Qt.UserRole) item = m.data(p, Qt.UserRole)
if item.type == TagTreeItem.CATEGORY and \ if item.type == TagTreeItem.CATEGORY:
item.category_key in \ fm = self.db.metadata_for_field(item.category_key)
('tags', 'series', 'authors', 'rating', 'publisher'): if item.category_key in \
('tags', 'series', 'authors', 'rating', 'publisher') or\
(fm['is_custom'] and \
fm['datatype'] in ['text', 'rating', 'series']):
child = m.data(idx, Qt.UserRole) child = m.data(idx, Qt.UserRole)
md = event.mimeData() md = event.mimeData()
mime = 'application/calibre+from_library' mime = 'application/calibre+from_library'
ids = list(map(int, str(md.data(mime)).split())) ids = list(map(int, str(md.data(mime)).split()))
self.handle_drop(item, child, ids) self.handle_drop(item, child, ids)
event.accept() event.accept()
return
event.ignore()
def handle_drop(self, parent, child, ids): def handle_drop(self, parent, child, ids):
print 'Dropped ids:', ids # print 'Dropped ids:', ids, parent.category_key, child.tag.name
key = parent.category_key
if (key == 'authors' and len(ids) >= 5):
if not confirm('<p>'+_('Changing the authors for several books can '
'take a while. Are you sure?')
+'</p>', 'tag_browser_drop_authors', self):
return
elif len(ids) > 15:
if not confirm('<p>'+_('Changing the metadata for that many books '
'can take a while. Are you sure?')
+'</p>', 'tag_browser_many_changes', self):
return
fm = self.db.metadata_for_field(key)
is_multiple = fm['is_multiple']
val = child.tag.name
for id in ids:
mi = self.db.get_metadata(id, index_is_id=True)
# Prepare to ignore the author, unless it is changed. Title is
# always ignored -- see the call to set_metadata
set_authors = False
# Author_sort cannot change explicitly. Changing the author might
# change it.
mi.author_sort = None # Never will change by itself.
if key == 'authors':
mi.authors = [val]
set_authors=True
elif fm['datatype'] == 'rating':
mi.set(key, len(val) * 2)
elif fm['is_custom'] and fm['datatype'] == 'series':
mi.set(key, val, extra=1.0)
elif is_multiple:
new_val = mi.get(key, [])
if val in new_val:
# Fortunately, only one field can change, so the continue
# won't break anything
continue
new_val.append(val)
mi.set(key, new_val)
else:
mi.set(key, val)
self.db.set_metadata(id, mi, set_title=False,
set_authors=set_authors, commit=False)
self.db.commit()
self.drag_drop_finished.emit(ids)
@property @property
def match_all(self): def match_all(self):
@ -730,6 +786,7 @@ class TagBrowserMixin(object): # {{{
self.tags_view.author_sort_edit.connect(self.do_author_sort_edit) self.tags_view.author_sort_edit.connect(self.do_author_sort_edit)
self.tags_view.tag_item_renamed.connect(self.do_tag_item_renamed) self.tags_view.tag_item_renamed.connect(self.do_tag_item_renamed)
self.tags_view.search_item_renamed.connect(self.saved_searches_changed) self.tags_view.search_item_renamed.connect(self.saved_searches_changed)
self.tags_view.drag_drop_finished.connect(self.drag_drop_finished)
self.edit_categories.clicked.connect(lambda x: self.edit_categories.clicked.connect(lambda x:
self.do_user_categories_edit()) self.do_user_categories_edit())
@ -811,6 +868,9 @@ class TagBrowserMixin(object): # {{{
self.library_view.model().refresh() self.library_view.model().refresh()
self.tags_view.recount() self.tags_view.recount()
def drag_drop_finished(self, ids):
self.library_view.model().refresh_ids(ids)
# }}} # }}}
class TagBrowserWidget(QWidget): # {{{ class TagBrowserWidget(QWidget): # {{{

View File

@ -971,14 +971,14 @@ def restore_database_option_parser():
files in each directory of the calibre library. This is files in each directory of the calibre library. This is
useful if your metadata.db file has been corrupted. useful if your metadata.db file has been corrupted.
WARNING: This completely regenerates your datbase. You will WARNING: This completely regenerates your database. You will
lose stored per-book conversion settings and custom recipes. lose stored per-book conversion settings and custom recipes.
''')) '''))
return parser return parser
def command_restore_database(args, dbpath): def command_restore_database(args, dbpath):
from calibre.library.restore import Restore from calibre.library.restore import Restore
parser = saved_searches_option_parser() parser = restore_database_option_parser()
opts, args = parser.parse_args(args) opts, args = parser.parse_args(args)
if len(args) != 0: if len(args) != 0:
parser.print_help() parser.print_help()

View File

@ -1247,7 +1247,8 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
self.set_path(id, True) self.set_path(id, True)
self.notify('metadata', [id]) self.notify('metadata', [id])
def set_metadata(self, id, mi, ignore_errors=False): def set_metadata(self, id, mi, ignore_errors=False,
set_title=True, set_authors=True, commit=True):
''' '''
Set metadata for the book `id` from the `Metadata` object `mi` Set metadata for the book `id` from the `Metadata` object `mi`
''' '''
@ -1259,8 +1260,9 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
traceback.print_exc() traceback.print_exc()
else: else:
raise raise
if mi.title: if set_title and mi.title:
self.set_title(id, mi.title, commit=False) self.set_title(id, mi.title, commit=False)
if set_authors:
if not mi.authors: if not mi.authors:
mi.authors = [_('Unknown')] mi.authors = [_('Unknown')]
authors = [] authors = []
@ -1304,6 +1306,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
val=mi.get(key), val=mi.get(key),
extra=mi.get_extra(key), extra=mi.get_extra(key),
label=user_mi[key]['label'], commit=False) label=user_mi[key]['label'], commit=False)
if commit:
self.conn.commit() self.conn.commit()
self.notify('metadata', [id]) self.notify('metadata', [id])

View File

@ -259,8 +259,8 @@ class FieldMetadata(dict):
'datatype':'text', 'datatype':'text',
'is_multiple':None, 'is_multiple':None,
'kind':'field', 'kind':'field',
'name':None, 'name':_('Title Sort'),
'search_terms':[], 'search_terms':['title_sort'],
'is_custom':False, 'is_custom':False,
'is_category':False}), 'is_category':False}),
('size', {'table':None, ('size', {'table':None,

View File

@ -268,7 +268,8 @@ def save_book_to_disk(id, db, root, opts, length):
cpb = cpb[dev_name] cpb = cpb[dev_name]
else: else:
cpb = None cpb = None
if DEBUG: # Leave this here for a while, in case problems arise.
if cpb is not None:
prints('Save-to-disk using plugboard:', fmt, cpb) prints('Save-to-disk using plugboard:', fmt, cpb)
data = db.format(id, fmt, index_is_id=True) data = db.format(id, fmt, index_is_id=True)
if data is None: if data is None:
@ -281,7 +282,7 @@ def save_book_to_disk(id, db, root, opts, length):
stream.seek(0) stream.seek(0)
try: try:
if cpb: if cpb:
newmi = mi.deepcopy() newmi = mi.deepcopy_metadata()
newmi.template_to_attribute(mi, cpb) newmi.template_to_attribute(mi, cpb)
else: else:
newmi = mi newmi = mi

View File

@ -38,11 +38,11 @@ If a particular book does not have a particular piece of metadata, the field in
If a book has a series, the template will produce:: If a book has a series, the template will produce::
{Asimov, Isaac}/Foundation/Second Foundation - 3 Asimov, Isaac/Foundation/Second Foundation 3
and if a book does not have a series:: and if a book does not have a series::
{Asimov, Isaac}/Second Foundation Asimov, Isaac/Second Foundation
(|app| automatically removes multiple slashes and leading or trailing spaces). (|app| automatically removes multiple slashes and leading or trailing spaces).
@ -119,10 +119,11 @@ The functions available are:
* ``ifempty(text)`` -- if the field is not empty, return the value of the field. Otherwise return `text`. * ``ifempty(text)`` -- if the field is not empty, return the value of the field. Otherwise return `text`.
* ``test(text if not empty, text if empty)`` -- return `text if not empty` if the field is not empty, otherwise return `text if empty`. * ``test(text if not empty, text if empty)`` -- return `text if not empty` if the field is not empty, otherwise return `text if empty`.
* ``contains(pattern, text if match, text if not match`` -- checks if field contains matches for the regular expression `pattern`. Returns `text if match` if matches are found, otherwise it returns `text if no match`. * ``contains(pattern, text if match, text if not match`` -- checks if field contains matches for the regular expression `pattern`. Returns `text if match` if matches are found, otherwise it returns `text if no match`.
* ``switch(pattern, value, pattern, value, ..., else_value)`` -- for each ``pattern, value`` pair, checks if the field matches the ``pattern`` and if so, returns that ``value``. If no ``pattern`` matches, then ``else_value`` is returned. You can have as many ``pattern, value`` pairs as you want. * ``switch(pattern, value, pattern, value, ..., else_value)`` -- for each ``pattern, value`` pair, checks if the field matches the regular expression ``pattern`` and if so, returns that ``value``. If no ``pattern`` matches, then ``else_value`` is returned. You can have as many ``pattern, value`` pairs as you want.
* ``re(pattern, replacement)`` -- return the field after applying the regular expression. All instances of `pattern` are replaced with `replacement`. As in all of |app|, these are python-compatible regular expressions.
* ``shorten(left chars, middle text, right chars)`` -- Return a shortened version of the field, consisting of `left chars` characters from the beginning of the field, followed by `middle text`, followed by `right chars` characters from the end of the string. `Left chars` and `right chars` must be integers. For example, assume the title of the book is `Ancient English Laws in the Times of Ivanhoe`, and you want it to fit in a space of at most 15 characters. If you use ``{title:shorten(9,-,5)}``, the result will be `Ancient E-nhoe`. If the field's length is less than ``left chars`` + ``right chars`` + the length of ``middle text``, then the field will be used intact. For example, the title `The Dome` would not be changed. * ``shorten(left chars, middle text, right chars)`` -- Return a shortened version of the field, consisting of `left chars` characters from the beginning of the field, followed by `middle text`, followed by `right chars` characters from the end of the string. `Left chars` and `right chars` must be integers. For example, assume the title of the book is `Ancient English Laws in the Times of Ivanhoe`, and you want it to fit in a space of at most 15 characters. If you use ``{title:shorten(9,-,5)}``, the result will be `Ancient E-nhoe`. If the field's length is less than ``left chars`` + ``right chars`` + the length of ``middle text``, then the field will be used intact. For example, the title `The Dome` would not be changed.
* ``lookup(field if not empty, field if empty)`` -- like test, except the arguments are field (metadata) names, not text. The value of the appropriate field will be fetched and used. Note that because composite columns are fields, you can use this function in one composite field to use the value of some other composite field. This is extremely useful when constructing variable save paths (more later). * ``lookup(field if not empty, field if empty)`` -- like test, except the arguments are field (metadata) names, not text. The value of the appropriate field will be fetched and used. Note that because composite columns are fields, you can use this function in one composite field to use the value of some other composite field. This is extremely useful when constructing variable save paths (more later).
* ``re(pattern, replacement)`` -- return the field after applying the regular expression. All instances of `pattern` are replaced with `replacement`. As in all of |app|, these are python-compatible regular expressions.
Now, about using functions and formatting in the same field. Suppose you have an integer custom column called ``#myint`` that you want to see with leading zeros, as in ``003``. To do this, you would use a format of ``0>3s``. However, by default, if a number (integer or float) equals zero then the field produces the empty value, so zero values will produce nothing, not ``000``. If you really want to see ``000`` values, then you use both the format string and the ``ifempty`` function to change the empty value back to a zero. The field reference would be:: Now, about using functions and formatting in the same field. Suppose you have an integer custom column called ``#myint`` that you want to see with leading zeros, as in ``003``. To do this, you would use a format of ``0>3s``. However, by default, if a number (integer or float) equals zero then the field produces the empty value, so zero values will produce nothing, not ``000``. If you really want to see ``000`` values, then you use both the format string and the ``ifempty`` function to change the empty value back to a zero. The field reference would be::

View File

@ -82,6 +82,7 @@ class TemplateFormatter(string.Formatter):
format_string_re = re.compile(r'^(.*)\|(.*)\|(.*)$') format_string_re = re.compile(r'^(.*)\|(.*)\|(.*)$')
compress_spaces = re.compile(r'\s+') compress_spaces = re.compile(r'\s+')
backslash_comma_to_comma = re.compile(r'\\,')
arg_parser = re.Scanner([ arg_parser = re.Scanner([
(r',', lambda x,t: ''), (r',', lambda x,t: ''),
@ -123,6 +124,7 @@ class TemplateFormatter(string.Formatter):
field = fmt[colon:p] field = fmt[colon:p]
func = self.functions[field] func = self.functions[field]
args = self.arg_parser.scan(fmt[p+1:])[0] args = self.arg_parser.scan(fmt[p+1:])[0]
args = [self.backslash_comma_to_comma.sub(',', a) for a in args]
if (func[0] == 0 and (len(args) != 1 or args[0])) or \ if (func[0] == 0 and (len(args) != 1 or args[0])) or \
(func[0] > 0 and func[0] != len(args)): (func[0] > 0 and func[0] != len(args)):
raise ValueError('Incorrect number of arguments for function '+ fmt[0:p]) raise ValueError('Incorrect number of arguments for function '+ fmt[0:p])