diff --git a/src/calibre/customize/builtins.py b/src/calibre/customize/builtins.py index 4e47c70bb0..89c800afb2 100644 --- a/src/calibre/customize/builtins.py +++ b/src/calibre/customize/builtins.py @@ -796,6 +796,17 @@ class Sending(PreferencesPlugin): description = _('Control how calibre transfers files to your ' 'ebook reader') +class Plugboard(PreferencesPlugin): + name = 'Plugboard' + icon = I('plugboard.png') + gui_name = _('Metadata plugboard') + category = 'Import/Export' + gui_category = _('Import/Export') + category_order = 3 + name_order = 4 + config_widget = 'calibre.gui2.preferences.plugboard' + description = _('Change metadata fields before saving/sending') + class Email(PreferencesPlugin): name = 'Email' icon = I('mail.png') @@ -856,8 +867,8 @@ class Misc(PreferencesPlugin): description = _('Miscellaneous advanced configuration') plugins += [LookAndFeel, Behavior, Columns, Toolbar, InputOptions, - CommonOptions, OutputOptions, Adding, Saving, Sending, Email, Server, - Plugins, Tweaks, Misc] + CommonOptions, OutputOptions, Adding, Saving, Sending, Plugboard, + Email, Server, Plugins, Tweaks, Misc] #}}} diff --git a/src/calibre/ebooks/metadata/book/base.py b/src/calibre/ebooks/metadata/book/base.py index bf95e989e8..aaa7c78e9a 100644 --- a/src/calibre/ebooks/metadata/book/base.py +++ b/src/calibre/ebooks/metadata/book/base.py @@ -182,7 +182,7 @@ class Metadata(object): return metadata describing a standard or custom field. ''' if key not in self.custom_field_keys(): - return self.get_standard_metadata(self, key, make_copy=False) + return self.get_standard_metadata(key, make_copy=False) return self.get_user_metadata(key, make_copy=False) def all_non_none_fields(self): @@ -294,6 +294,33 @@ class Metadata(object): _data = object.__getattribute__(self, '_data') _data['user_metadata'][field] = metadata + def copy_specific_attributes(self, other, attrs): + ''' + Takes a dict {src:dest, src:dest} and copys other[src] to self[dest]. + This is on a best-efforts basis. Some assignments can make no sense. + ''' + if not attrs: + return + for src in attrs: + try: + print src + sfm = other.metadata_for_field(src) + dfm = self.metadata_for_field(attrs[src]) + if dfm['is_multiple']: + if sfm['is_multiple']: + self.set(attrs[src], other.get(src)) + else: + self.set(attrs[src], + [f.strip() for f in other.get(src).split(',') + if f.strip()]) + elif sfm['is_multiple']: + self.set(attrs[src], ','.join(other.get(src))) + else: + self.set(attrs[src], other.get(src)) + except: + traceback.print_exc() + pass + # Old Metadata API {{{ def print_all_attributes(self): for x in STANDARD_METADATA_FIELDS: diff --git a/src/calibre/gui2/device.py b/src/calibre/gui2/device.py index 58c5e5d9ad..eb1716f782 100644 --- a/src/calibre/gui2/device.py +++ b/src/calibre/gui2/device.py @@ -317,19 +317,40 @@ class DeviceManager(Thread): # {{{ args=[booklist, on_card], description=_('Send collections to device')) - def _upload_books(self, files, names, on_card=None, metadata=None): + def _upload_books(self, files, names, on_card=None, metadata=None, plugboards=None): '''Upload books to device: ''' if metadata and files and len(metadata) == len(files): for f, mi in zip(files, metadata): if isinstance(f, unicode): ext = f.rpartition('.')[-1].lower() + dev_name = self.connected_device.name + cpb = None + if ext in plugboards: + cpb = plugboards[ext] + elif ' any' in plugboards: + cpb = plugboards[' any'] + if cpb is not None: + if dev_name in cpb: + cpb = cpb[dev_name] + elif ' any' in plugboards[ext]: + cpb = cpb[' any'] + else: + cpb = None + + if DEBUG: + prints('Using plugboard', cpb) if ext: try: if DEBUG: prints('Setting metadata in:', mi.title, 'at:', f, file=sys.__stdout__) with open(f, 'r+b') as stream: - set_metadata(stream, mi, stream_type=ext) + if cpb: + newmi = mi.deepcopy() + newmi.copy_specific_attributes(mi, cpb) + else: + newmi = mi + set_metadata(stream, newmi, stream_type=ext) except: if DEBUG: prints(traceback.format_exc(), file=sys.__stdout__) @@ -338,12 +359,12 @@ class DeviceManager(Thread): # {{{ metadata=metadata, end_session=False) def upload_books(self, done, files, names, on_card=None, titles=None, - metadata=None): + metadata=None, plugboards=None): desc = _('Upload %d books to device')%len(names) if titles: desc += u':' + u', '.join(titles) return self.create_job(self._upload_books, done, args=[files, names], - kwargs={'on_card':on_card,'metadata':metadata}, description=desc) + kwargs={'on_card':on_card,'metadata':metadata,'plugboards':plugboards}, description=desc) def add_books_to_metadata(self, locations, metadata, booklists): self.device.add_books_to_metadata(locations, metadata, booklists) @@ -1257,10 +1278,11 @@ class DeviceMixin(object): # {{{ :param files: List of either paths to files or file like objects ''' titles = [i.title for i in metadata] + plugboards = self.library_view.model().db.prefs.get('plugboards', None) job = self.device_manager.upload_books( Dispatcher(self.books_uploaded), files, names, on_card=on_card, - metadata=metadata, titles=titles + metadata=metadata, titles=titles, plugboards=plugboards ) self.upload_memory[job] = (metadata, on_card, memory, files) diff --git a/src/calibre/gui2/preferences/plugboard.py b/src/calibre/gui2/preferences/plugboard.py new file mode 100644 index 0000000000..5691120cef --- /dev/null +++ b/src/calibre/gui2/preferences/plugboard.py @@ -0,0 +1,257 @@ +#!/usr/bin/env python +# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai + +__license__ = 'GPL v3' +__copyright__ = '2010, Kovid Goyal ' +__docformat__ = 'restructuredtext en' + +from PyQt4 import QtGui + +from calibre.gui2 import error_dialog +from calibre.gui2.preferences import ConfigWidgetBase, test_widget, \ + AbortCommit +from calibre.gui2.preferences.plugboard_ui import Ui_Form +from calibre.customize.ui import metadata_writers, device_plugins + + +class ConfigWidget(ConfigWidgetBase, Ui_Form): + + def genesis(self, gui): + self.gui = gui + self.db = gui.library_view.model().db + self.current_plugboards = self.db.prefs.get('plugboards', {'epub': {' any': {'title':'authors', 'authors':'tags'}}}) + self.current_device = None + self.current_format = None +# self.proxy = ConfigProxy(config()) +# +# r = self.register +# +# for x in ('asciiize', 'update_metadata', 'save_cover', 'write_opf', +# 'replace_whitespace', 'to_lowercase', 'formats', 'timefmt'): +# r(x, self.proxy) +# +# self.save_template.changed_signal.connect(self.changed_signal.emit) + + def clear_fields(self, edit_boxes=False, new_boxes=False): + self.ok_button.setEnabled(False) + for w in self.source_widgets: + w.clear() + for w in self.dest_widgets: + w.clear() + if edit_boxes: + self.edit_device.setCurrentIndex(0) + self.edit_format.setCurrentIndex(0) + if new_boxes: + self.new_device.setCurrentIndex(0) + self.new_format.setCurrentIndex(0) + + def set_fields(self): + self.ok_button.setEnabled(True) + for w in self.source_widgets: + w.addItems(self.fields) + for w in self.dest_widgets: + w.addItems(self.fields) + + def set_field(self, i, src, dst): + print i, src, dst + idx = self.fields.index(src) + self.source_widgets[i].setCurrentIndex(idx) + idx = self.fields.index(dst) + self.dest_widgets[i].setCurrentIndex(idx) + + def edit_device_changed(self, txt): + if txt == '': + self.current_device = None + return + print 'edit device changed' + self.clear_fields(new_boxes=True) + self.current_device = unicode(txt) + fpb = self.current_plugboards.get(self.current_format, None) + if fpb is None: + print 'None format!' + return + dpb = fpb.get(self.current_device, None) + if dpb is None: + print 'none device!' + return + self.set_fields() + for i,src in enumerate(dpb): + self.set_field(i, src, dpb[src]) + self.ok_button.setEnabled(True) + + def edit_format_changed(self, txt): + if txt == '': + self.edit_device.setCurrentIndex(0) + self.current_format = None + self.current_device = None + return + print 'edit_format_changed' + self.clear_fields(new_boxes=True) + txt = unicode(txt) + fpb = self.current_plugboards.get(txt, None) + if fpb is None: + print 'None editable format!' + return + self.current_format = txt + devices = [''] + for d in fpb: + devices.append(d) + self.edit_device.clear() + self.edit_device.addItems(devices) + self.edit_device.setCurrentIndex(0) + + def new_device_changed(self, txt): + if txt == '': + self.current_device = None + return + print 'new_device_changed' + self.clear_fields(edit_boxes=True) + self.current_device = unicode(txt) + error = False + if self.current_format == ' any': + for f in self.current_plugboards: + if self.current_device == ' any' and len(self.current_plugboards[f]): + error = True + break + if self.current_device in self.current_plugboards[f]: + error = True + break + if ' any' in self.current_plugboards[f]: + error = True + break + else: + fpb = self.current_plugboards.get(self.current_format, None) + if fpb is not None: + if ' any' in fpb: + error = True + else: + dpb = fpb.get(self.current_device, None) + if dpb is not None: + error = True + + if error: + error_dialog(self, '', + _('That format and device already has a plugboard'), + show=True) + self.new_device.setCurrentIndex(0) + return + self.set_fields() + + def new_format_changed(self, txt): + if txt == '': + self.current_format = None + self.current_device = None + return + print 'new_format_changed' + self.clear_fields(edit_boxes=True) + self.current_format = unicode(txt) + self.new_device.setCurrentIndex(0) + + def ok_clicked(self): + pb = {} + print self.current_format, self.current_device + for i in range(0, len(self.source_widgets)): + s = self.source_widgets[i].currentIndex() + if s != 0: + d = self.dest_widgets[i].currentIndex() + if d != 0: + pb[self.fields[s]] = self.fields[d] + if len(pb) == 0: + if self.current_format in self.current_plugboards: + fpb = self.current_plugboards[self.current_format] + if self.current_device in fpb: + del fpb[self.current_device] + if len(fpb) == 0: + del self.current_plugboards[self.current_format] + else: + if self.current_format not in self.current_plugboards: + self.current_plugboards[self.current_format] = {} + fpb = self.current_plugboards[self.current_format] + fpb[self.current_device] = pb + self.changed_signal.emit() + self.refill_all_boxes() + + def refill_all_boxes(self): + self.current_device = None + self.current_format = None + self.clear_fields(new_boxes=True) + self.edit_format.clear() + self.edit_format.addItem('') + for format in self.current_plugboards: + self.edit_format.addItem(format) + self.edit_format.setCurrentIndex(0) + self.edit_device.clear() + self.ok_button.setEnabled(False) + + def initialize(self): + def field_cmp(x, y): + if x.startswith('#'): + if y.startswith('#'): + return cmp(x.lower(), y.lower()) + else: + return 1 + elif y.startswith('#'): + return -1 + else: + return cmp(x.lower(), y.lower()) + + ConfigWidgetBase.initialize(self) + + self.devices = ['', ' any', 'save to disk'] + for device in device_plugins(): + self.devices.append(device.name) + self.devices.sort(cmp=lambda x, y: cmp(x.lower(), y.lower())) + self.new_device.addItems(self.devices) + + self.formats = ['', ' any'] + for w in metadata_writers(): + for f in w.file_types: + self.formats.append(f) + self.formats.sort() + self.new_format.addItems(self.formats) + + self.fields = [''] + for f in self.db.all_field_keys(): + if self.db.field_metadata[f].get('rec_index', None) is not None and\ + self.db.field_metadata[f]['datatype'] is not None and \ + self.db.field_metadata[f]['search_terms']: + self.fields.append(f) + self.fields.sort(cmp=field_cmp) + + self.source_widgets = [] + self.dest_widgets = [] + for i in range(0, 10): + w = QtGui.QComboBox(self) + self.source_widgets.append(w) + self.fields_layout.addWidget(w, 5+i, 0, 1, 1) + w = QtGui.QComboBox(self) + self.dest_widgets.append(w) + self.fields_layout.addWidget(w, 5+i, 1, 1, 1) + + self.edit_device.currentIndexChanged[str].connect(self.edit_device_changed) + self.edit_format.currentIndexChanged[str].connect(self.edit_format_changed) + self.new_device.currentIndexChanged[str].connect(self.new_device_changed) + self.new_format.currentIndexChanged[str].connect(self.new_format_changed) + self.ok_button.clicked.connect(self.ok_clicked) + + self.refill_all_boxes() + + def restore_defaults(self): + ConfigWidgetBase.restore_defaults(self) + self.current_plugboards = {} + self.refill_all_boxes() + self.changed_signal.emit() + + def commit(self): + self.db.prefs.set('plugboards', self.current_plugboards) + return ConfigWidgetBase.commit(self) + + def refresh_gui(self, gui): + pass + + +if __name__ == '__main__': + from PyQt4.Qt import QApplication + app = QApplication([]) + test_widget('Import/Export', 'plugboards') + diff --git a/src/calibre/gui2/preferences/plugboard.ui b/src/calibre/gui2/preferences/plugboard.ui new file mode 100644 index 0000000000..ad72ec359f --- /dev/null +++ b/src/calibre/gui2/preferences/plugboard.ui @@ -0,0 +1,138 @@ + + + Form + + + + 0 + 0 + 707 + 340 + + + + Form + + + + + + Here you can control what metadata calibre uses when saving or sending books: + + + true + + + + + + + + + Add new plugboard + + + + + + + Edit existing plugboard + + + + + + + + + + + + + + + + + + + Format (choose first) + + + Qt::AlignCenter + + + + + + + Device (choose second) + + + Qt::AlignCenter + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + Source field + + + Qt::AlignCenter + + + + + + + Destination field + + + Qt::AlignCenter + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + Done + + + + + + + + + + diff --git a/src/calibre/library/save_to_disk.py b/src/calibre/library/save_to_disk.py index e479d27121..54671da4b4 100644 --- a/src/calibre/library/save_to_disk.py +++ b/src/calibre/library/save_to_disk.py @@ -232,6 +232,21 @@ def save_book_to_disk(id, db, root, opts, length): written = False for fmt in formats: + dev_name = 'save to disk' + plugboards = db.prefs.get('plugboards', None) + cpb = None + if fmt in plugboards: + cpb = plugboards[fmt] + elif ' any' in plugboards: + cpb = plugboards[' any'] + if cpb is not None: + if dev_name in cpb: + cpb = cpb[dev_name] + elif ' any' in plugboards[fmt]: + cpb = cpb[' any'] + else: + cpb = None + data = db.format(id, fmt, index_is_id=True) if data is None: continue @@ -242,7 +257,12 @@ def save_book_to_disk(id, db, root, opts, length): stream.write(data) stream.seek(0) try: - set_metadata(stream, mi, fmt) + if cpb: + newmi = mi.deepcopy() + newmi.copy_specific_attributes(mi, cpb) + else: + newmi = mi + set_metadata(stream, newmi, fmt) except: traceback.print_exc() stream.seek(0)