mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-08 10:44:09 -04:00
Drag and drop to Tag Browser. Fix #7078 (Book Details missing). Various minor bug fixes
This commit is contained in:
commit
abe8bd9a25
@ -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
|
||||||
|
|
||||||
|
@ -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):
|
||||||
|
@ -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
|
|
||||||
|
@ -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) = \
|
||||||
|
@ -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)
|
||||||
|
@ -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')
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
@ -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:
|
||||||
res = res + \
|
if self.get_extra(key) is not None:
|
||||||
' [%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':
|
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)
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
|
||||||
|
@ -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,12 +46,11 @@ 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)
|
||||||
self.devices.insert(2, plugboard_any_device_value)
|
self.devices.insert(2, plugboard_any_device_value)
|
||||||
|
@ -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,10 +124,12 @@ 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\
|
||||||
allowed = True
|
(fm['is_custom'] and \
|
||||||
|
fm['datatype'] in ['text', 'rating', 'series']):
|
||||||
|
allowed = True
|
||||||
if allowed:
|
if allowed:
|
||||||
event.acceptProposedAction()
|
event.acceptProposedAction()
|
||||||
else:
|
else:
|
||||||
@ -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 \
|
||||||
child = m.data(idx, Qt.UserRole)
|
('tags', 'series', 'authors', 'rating', 'publisher') or\
|
||||||
md = event.mimeData()
|
(fm['is_custom'] and \
|
||||||
mime = 'application/calibre+from_library'
|
fm['datatype'] in ['text', 'rating', 'series']):
|
||||||
ids = list(map(int, str(md.data(mime)).split()))
|
child = m.data(idx, Qt.UserRole)
|
||||||
self.handle_drop(item, child, ids)
|
md = event.mimeData()
|
||||||
event.accept()
|
mime = 'application/calibre+from_library'
|
||||||
|
ids = list(map(int, str(md.data(mime)).split()))
|
||||||
|
self.handle_drop(item, child, ids)
|
||||||
|
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): # {{{
|
||||||
|
@ -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()
|
||||||
|
@ -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,14 +1260,15 @@ 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 not mi.authors:
|
if set_authors:
|
||||||
mi.authors = [_('Unknown')]
|
if not mi.authors:
|
||||||
authors = []
|
mi.authors = [_('Unknown')]
|
||||||
for a in mi.authors:
|
authors = []
|
||||||
authors += string_to_authors(a)
|
for a in mi.authors:
|
||||||
self.set_authors(id, authors, notify=False, commit=False)
|
authors += string_to_authors(a)
|
||||||
|
self.set_authors(id, authors, notify=False, commit=False)
|
||||||
if mi.author_sort:
|
if mi.author_sort:
|
||||||
doit(self.set_author_sort, id, mi.author_sort, notify=False,
|
doit(self.set_author_sort, id, mi.author_sort, notify=False,
|
||||||
commit=False)
|
commit=False)
|
||||||
@ -1304,7 +1306,8 @@ 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)
|
||||||
self.conn.commit()
|
if commit:
|
||||||
|
self.conn.commit()
|
||||||
self.notify('metadata', [id])
|
self.notify('metadata', [id])
|
||||||
|
|
||||||
def authors_sort_strings(self, id, index_is_id=False):
|
def authors_sort_strings(self, id, index_is_id=False):
|
||||||
|
@ -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,
|
||||||
|
@ -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
|
||||||
|
@ -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::
|
||||||
|
|
||||||
|
@ -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])
|
||||||
|
Loading…
x
Reference in New Issue
Block a user