From 56023722709d35816424cd3c77f5afe972103d0c Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Thu, 16 Sep 2010 12:24:56 +0100 Subject: [PATCH 1/3] Minor changes to OPF testing --- src/calibre/ebooks/metadata/opf2.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/calibre/ebooks/metadata/opf2.py b/src/calibre/ebooks/metadata/opf2.py index ecbef3194d..8a4ff6a5bd 100644 --- a/src/calibre/ebooks/metadata/opf2.py +++ b/src/calibre/ebooks/metadata/opf2.py @@ -1415,9 +1415,11 @@ def test_user_metadata(): mi = Metadata('Test title', ['test author1', 'test author2']) um = { '#myseries': { '#value#': u'test series\xe4', 'datatype':'text', - 'is_multiple': False, 'name': u'My Series'}, + 'is_multiple': None, 'name': u'My Series'}, '#myseries_index': { '#value#': 2.45, 'datatype': 'float', - 'is_multiple': False} + 'is_multiple': None}, + '#mytags': {'#value#':['t1','t2','t3'], 'datatype':'text', + 'is_multiple': '|', 'name': u'My Tags'} } mi.set_all_user_metadata(um) raw = metadata_to_opf(mi) From e1dd08acef1b14c73a3da542f974acd9a10a1f79 Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Thu, 16 Sep 2010 14:02:49 +0100 Subject: [PATCH 2/3] Several changes: 1) Add an option to specify the time format when sending to device. This is the analog of the same option that already exists for save to disk. 2) refactor the format_field code. Remove the special parameters on format_field. Add format_field_extended that returns a 4-element tuple (name, formatted val, original val, field metadata). 3) change (simplify) usbms collections management to use new format_field_extended method. 4) change device.py to not call sync_booklists twice. 5) add the fix for gui-not-updating, in hopes that we can avoid merge conflicts --- src/calibre/devices/usbms/books.py | 21 +++++++-------- src/calibre/devices/usbms/device.py | 4 ++- src/calibre/ebooks/metadata/book/base.py | 33 ++++++++++++------------ src/calibre/gui2/actions/add.py | 2 +- src/calibre/gui2/device.py | 28 ++++++++++++-------- src/calibre/gui2/preferences/sending.py | 3 +++ src/calibre/gui2/preferences/sending.ui | 15 ++++++++++- src/calibre/library/save_to_disk.py | 3 +++ 8 files changed, 68 insertions(+), 41 deletions(-) diff --git a/src/calibre/devices/usbms/books.py b/src/calibre/devices/usbms/books.py index d25787fc89..eab625f7be 100644 --- a/src/calibre/devices/usbms/books.py +++ b/src/calibre/devices/usbms/books.py @@ -94,12 +94,12 @@ class CollectionsBookList(BookList): def supports_collections(self): return True - def compute_category_name(self, attr, category, cust_field_meta): + def compute_category_name(self, attr, category, field_meta): renames = tweaks['sony_collection_renaming_rules'] attr_name = renames.get(attr, None) if attr_name is None: - if attr in cust_field_meta: - attr_name = '(%s)'%cust_field_meta[attr]['name'] + if field_meta['is_custom']: + attr_name = '(%s)'%field_meta['name'] else: attr_name = '' elif attr_name != '': @@ -138,23 +138,23 @@ class CollectionsBookList(BookList): # specified 'on_connect' attrs = collection_attributes meta_vals = book.get_all_non_none_attributes() - cust_field_meta = book.get_all_user_metadata(make_copy=False) for attr in attrs: attr = attr.strip() - ign, val = book.format_field(attr, - ignore_series_index=True, - return_multiples_as_list=True) + ign, val, orig_val, fm = book.format_field_extended(attr) if not val: continue if isbytestring(val): val = val.decode(preferred_encoding, 'replace') if isinstance(val, (list, tuple)): val = list(val) + elif fm['datatype'] == 'series': + val = [orig_val] + elif fm['datatype'] == 'text' and fm['is_multiple']: + val = orig_val else: val = [val] for category in val: is_series = False - if attr in cust_field_meta: # is a custom field - fm = cust_field_meta[attr] + if fm['is_custom']: # is a custom field if fm['datatype'] == 'text' and len(category) > 1 and \ category[0] == '[' and category[-1] == ']': continue @@ -168,8 +168,7 @@ class CollectionsBookList(BookList): ('series' in collection_attributes and meta_vals.get('series', None) == category): is_series = True - cat_name = self.compute_category_name(attr, category, - cust_field_meta) + cat_name = self.compute_category_name(attr, category, fm) if cat_name not in collections: collections[cat_name] = [] collections_lpaths[cat_name] = set() diff --git a/src/calibre/devices/usbms/device.py b/src/calibre/devices/usbms/device.py index b954911242..928d00ad4a 100644 --- a/src/calibre/devices/usbms/device.py +++ b/src/calibre/devices/usbms/device.py @@ -829,12 +829,14 @@ class Device(DeviceConfig, DevicePlugin): ext = os.path.splitext(fname)[1] from calibre.library.save_to_disk import get_components + from calibre.library.save_to_disk import config + opts = config().parse() if not isinstance(template, unicode): template = template.decode('utf-8') app_id = str(getattr(mdata, 'application_id', '')) # The db id will be in the created filename extra_components = get_components(template, mdata, fname, - length=250-len(app_id)-1) + timefmt=opts.send_timefmt, length=250-len(app_id)-1) if not extra_components: extra_components.append(sanitize(self.filename_callback(fname, mdata))) diff --git a/src/calibre/ebooks/metadata/book/base.py b/src/calibre/ebooks/metadata/book/base.py index 7405f20a7c..b252f518da 100644 --- a/src/calibre/ebooks/metadata/book/base.py +++ b/src/calibre/ebooks/metadata/book/base.py @@ -343,8 +343,11 @@ class Metadata(object): def format_rating(self): return unicode(self.rating) - def format_field(self, key, ignore_series_index=False, - return_multiples_as_list=False): + def format_field(self, key): + name, val, ign, ign = self.format_field_extended(key) + return (name, val) + + def format_field_extended(self, key): from calibre.ebooks.metadata import authors_to_string ''' returns the tuple (field_name, formatted_value) @@ -352,43 +355,41 @@ class Metadata(object): if key in self.user_metadata_keys: res = self.get(key, None) if res is None or res == '': - return (None, None) + return (None, None, None, None) + orig_res = res cmeta = self.get_user_metadata(key, make_copy=False) name = unicode(cmeta['name']) datatype = cmeta['datatype'] if datatype == 'text' and cmeta['is_multiple']: - if not return_multiples_as_list: - res = u', '.join(res) + res = u', '.join(res) elif datatype == 'series': - if not ignore_series_index: - res = res + \ - ' [%s]'%self.format_series_index(val=self.get_extra(key)) + res = res + \ + ' [%s]'%self.format_series_index(val=self.get_extra(key)) elif datatype == 'datetime': res = format_date(res, cmeta['display'].get('date_format','dd MMM yyyy')) elif datatype == 'bool': res = _('Yes') if res else _('No') - return (name, res) + return (name, res, orig_res, cmeta) if key in field_metadata and field_metadata[key]['kind'] == 'field': res = self.get(key, None) if res is None or res == '': - return (None, None) + return (None, None, None, None) + orig_res = res fmeta = field_metadata[key] name = unicode(fmeta['name']) datatype = fmeta['datatype'] if key == 'authors': res = authors_to_string(res) elif datatype == 'text' and fmeta['is_multiple']: - if not return_multiples_as_list: - res = u', '.join(res) + res = u', '.join(res) elif datatype == 'series': - if not ignore_series_index: - res = res + ' [%s]'%self.format_series_index() + res = res + ' [%s]'%self.format_series_index() elif datatype == 'datetime': res = format_date(res, fmeta['display'].get('date_format','dd MMM yyyy')) - return (name, res) + return (name, res, orig_res, fmeta) - return (None, None) + return (None, None, None, None) def __unicode__(self): from calibre.ebooks.metadata import authors_to_string diff --git a/src/calibre/gui2/actions/add.py b/src/calibre/gui2/actions/add.py index add7bf1d5b..aa20b8bc16 100644 --- a/src/calibre/gui2/actions/add.py +++ b/src/calibre/gui2/actions/add.py @@ -230,7 +230,7 @@ class AddAction(InterfaceAction): self._files_added(paths, names, infos, on_card=on_card) # set the in-library flags, and as a consequence send the library's # metadata for this book to the device. This sets the uuid to the - # correct value. + # correct value. Note that set_books_in_library might sync_booklists self.gui.set_books_in_library(booklists=[model.db], reset=True) model.reset() diff --git a/src/calibre/gui2/device.py b/src/calibre/gui2/device.py index f839e1d519..196e97f2a3 100644 --- a/src/calibre/gui2/device.py +++ b/src/calibre/gui2/device.py @@ -745,6 +745,7 @@ class DeviceMixin(object): # {{{ if job.failed: self.device_job_exception(job) return + # set_books_in_library might schedule a sync_booklists job self.set_books_in_library(job.result, reset=True) mainlist, cardalist, cardblist = job.result self.memory_view.set_database(mainlist) @@ -789,11 +790,12 @@ class DeviceMixin(object): # {{{ self.device_manager.remove_books_from_metadata(paths, self.booklists()) model.paths_deleted(paths) - self.upload_booklists() # Force recomputation the library's ondevice info. We need to call # set_books_in_library even though books were not added because - # the deleted book might have been an exact match. - self.set_books_in_library(self.booklists(), reset=True) + # the deleted book might have been an exact match. Upload the booklists + # if set_books_in_library did not. + if not self.set_books_in_library(self.booklists(), reset=True): + self.upload_booklists() self.book_on_device(None, None, reset=True) # We need to reset the ondevice flags in the library. Use a big hammer, # so we don't need to worry about whether some succeeded or not. @@ -1280,8 +1282,6 @@ class DeviceMixin(object): # {{{ self.device_manager.add_books_to_metadata(job.result, metadata, self.booklists()) - self.upload_booklists() - books_to_be_deleted = [] if memory and memory[1]: books_to_be_deleted = memory[1] @@ -1291,12 +1291,15 @@ class DeviceMixin(object): # {{{ # book already there with a different book. This happens frequently in # news. When this happens, the book match indication will be wrong # because the UUID changed. Force both the device and the library view - # to refresh the flags. - self.set_books_in_library(self.booklists(), reset=True) + # to refresh the flags. Set_books_in_library could upload the booklists. + # If it does not, then do it here. + if not self.set_books_in_library(self.booklists(), reset=True): + self.upload_booklists() self.book_on_device(None, reset=True) self.refresh_ondevice_info(device_connected = True) - view = self.card_a_view if on_card == 'carda' else self.card_b_view if on_card == 'cardb' else self.memory_view + view = self.card_a_view if on_card == 'carda' else \ + self.card_b_view if on_card == 'cardb' else self.memory_view view.model().resort(reset=False) view.model().research() for f in files: @@ -1371,7 +1374,7 @@ class DeviceMixin(object): # {{{ try: db = self.library_view.model().db except: - return + return False # Build a cache (map) of the library, so the search isn't On**2 self.db_book_title_cache = {} self.db_book_uuid_cache = {} @@ -1466,10 +1469,13 @@ class DeviceMixin(object): # {{{ # Set author_sort if it isn't already asort = getattr(book, 'author_sort', None) if not asort and book.authors: - book.author_sort = self.library_view.model().db.author_sort_from_authors(book.authors) + book.author_sort = self.library_view.model().db.\ + author_sort_from_authors(book.authors) if update_metadata: if self.device_manager.is_device_connected: - self.device_manager.sync_booklists(None, booklists) + self.device_manager.sync_booklists( + Dispatcher(self.metadata_synced), booklists) + return update_metadata # }}} diff --git a/src/calibre/gui2/preferences/sending.py b/src/calibre/gui2/preferences/sending.py index 748c6b2a2d..ac4abbcf41 100644 --- a/src/calibre/gui2/preferences/sending.py +++ b/src/calibre/gui2/preferences/sending.py @@ -22,6 +22,9 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form): r = self.register + for x in ('send_timefmt',): + r(x, self.proxy) + choices = [(_('Manual management'), 'manual'), (_('Only on send'), 'on_send'), (_('Automatic management'), 'on_connect')] diff --git a/src/calibre/gui2/preferences/sending.ui b/src/calibre/gui2/preferences/sending.ui index e064646afd..b9d1d1e1d2 100644 --- a/src/calibre/gui2/preferences/sending.ui +++ b/src/calibre/gui2/preferences/sending.ui @@ -80,7 +80,20 @@ - + + + + Format &dates as: + + + opt_send_timefmt + + + + + + + Here you can control how calibre will save your books when you click the Send to Device button. This setting can be overriden for individual devices by customizing the device interface plugins in Preferences->Advanced->Plugins diff --git a/src/calibre/library/save_to_disk.py b/src/calibre/library/save_to_disk.py index 3fa40c68b2..71850abcd5 100644 --- a/src/calibre/library/save_to_disk.py +++ b/src/calibre/library/save_to_disk.py @@ -84,6 +84,9 @@ def config(defaults=None): x('timefmt', default='%b, %Y', help=_('The format in which to display dates. %d - day, %b - month, ' '%Y - year. Default is: %b, %Y')) + x('send_timefmt', default='%b, %Y', + help=_('The format in which to display dates. %d - day, %b - month, ' + '%Y - year. Default is: %b, %Y')) x('to_lowercase', default=False, help=_('Convert paths to lowercase.')) x('replace_whitespace', default=False, From 4645138a67193537ba3e91fc3e4942017d72e1de Mon Sep 17 00:00:00 2001 From: Charles Haley <> Date: Thu, 16 Sep 2010 16:03:07 +0100 Subject: [PATCH 3/3] 1) Re-enable syntactic validation of save templates. 2) fix row numbering on send_to_device preferences ui template. --- src/calibre/gui2/preferences/save_template.py | 37 +++++++++++-------- src/calibre/gui2/preferences/sending.ui | 2 +- 2 files changed, 22 insertions(+), 17 deletions(-) diff --git a/src/calibre/gui2/preferences/save_template.py b/src/calibre/gui2/preferences/save_template.py index 26dc02f259..0dbee5bf21 100644 --- a/src/calibre/gui2/preferences/save_template.py +++ b/src/calibre/gui2/preferences/save_template.py @@ -8,8 +8,10 @@ __docformat__ = 'restructuredtext en' from PyQt4.Qt import QWidget, pyqtSignal +from calibre.gui2 import error_dialog from calibre.gui2.preferences.save_template_ui import Ui_Form -from calibre.library.save_to_disk import FORMAT_ARG_DESCS +from calibre.library.save_to_disk import FORMAT_ARG_DESCS, preprocess_template,\ + safe_format class SaveTemplate(QWidget, Ui_Form): @@ -24,8 +26,11 @@ class SaveTemplate(QWidget, Ui_Form): variables = sorted(FORMAT_ARG_DESCS.keys()) rows = [] for var in variables: - rows.append(u'%s%s'% + rows.append(u'%s %s'% (var, FORMAT_ARG_DESCS[var])) + rows.append(u'%s  %s'%( + _('Any custom field'), + _('The lookup name of any custom field. These names begin with "#")'))) table = u'%s
'%(u'\n'.join(rows)) self.template_variables.setText(table) @@ -39,21 +44,21 @@ class SaveTemplate(QWidget, Ui_Form): self.changed_signal.emit() def validate(self): - # TODO: NEWMETA: I haven't figured out how to get the custom columns - # into here, so for the moment make all templates valid. + ''' + Do a syntax check on the format string. Doing a semantic check + (verifying that the fields exist) is not useful in the presence of + custom fields, because they may or may not exist. + ''' + tmpl = preprocess_template(self.opt_template.text()) + fa = {} + try: + safe_format(tmpl, fa) + except Exception, err: + error_dialog(self, _('Invalid template'), + '

'+_('The template %s is invalid:')%tmpl + \ + '
'+str(err), show=True) + return False return True -# tmpl = preprocess_template(self.opt_template.text()) -# fa = {} -# for x in FORMAT_ARG_DESCS.keys(): -# fa[x]='random long string' -# try: -# tmpl.format(**fa) -# except Exception, err: -# error_dialog(self, _('Invalid template'), -# '

'+_('The template %s is invalid:')%tmpl + \ -# '
'+str(err), show=True) -# return False -# return True def set_value(self, val): self.opt_template.set_value(val) diff --git a/src/calibre/gui2/preferences/sending.ui b/src/calibre/gui2/preferences/sending.ui index b9d1d1e1d2..75b1899a3a 100644 --- a/src/calibre/gui2/preferences/sending.ui +++ b/src/calibre/gui2/preferences/sending.ui @@ -103,7 +103,7 @@ - +