mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
0.7.22 update
This commit is contained in:
commit
b57222a01c
@ -4,6 +4,52 @@
|
||||
# for important features/bug fixes.
|
||||
# Also, each release can have new and improved recipes.
|
||||
|
||||
- version: 0.7.22
|
||||
date: 2010-10-03
|
||||
|
||||
new features:
|
||||
- title: "Drag and drop books from your calibre library"
|
||||
type: major
|
||||
description: >
|
||||
"You can now drag and drop books from your calibre library. You can drag them to the desktop or to a file explorer, to copy them to your computer. You can drag them to the
|
||||
device icon in calibre to send them to the device. You can also drag and drop books from the device view in calibre to the calibre library icon or the operating
|
||||
system to copy them from the device."
|
||||
|
||||
- title: "There were many minor bug fixes for various bugs caused by the major changes in 0.7.21. So if you have updated to 0.7.21, it is highly recommended you update to 0.7.22"
|
||||
|
||||
- title: "Driver for the VelocityMicro ebook reader device"
|
||||
|
||||
- title: "Add a tweak to control how articles in titles are processed during sorting"
|
||||
|
||||
- title: "Add a new format type 'device_db' to plugboards to control the metadata displayed in book lists on SONY devices."
|
||||
|
||||
bug fixes:
|
||||
- title: "Fix ISBN not being read from filenames in 0.7.21"
|
||||
tickets: [7054]
|
||||
|
||||
- title: "Fix instant Search for text not found causes unhandled exception when conversion jobs are running"
|
||||
tickets: [7043]
|
||||
|
||||
- title: "Fix removing a publisher causes an error in 0.7.21"
|
||||
tickets: [7046]
|
||||
|
||||
- title: "MOBI Output: Fix some images being distorted in 0.7.21"
|
||||
tickets: [7049]
|
||||
|
||||
- title: "Fix regression that broke bulk conversion of books without covers in 0.7.21"
|
||||
|
||||
- title: "Fix regression that broke add and set_metadata commands in calibredb in 0.7.21"
|
||||
|
||||
- title: "Workaround for Qt bug in file open dialogs in linux that causes multiple file selection to ignore files with two or more spaces in the file name"
|
||||
|
||||
- title: "Conversion pipeline: Fix regression in 0.7.21 that broke conversion of LIT/EPUB documents that specified no title in their OPF files"
|
||||
|
||||
- title: "Fix regression that broke iPad driver in 0.7.21"
|
||||
|
||||
improved recipes:
|
||||
- Washington Post
|
||||
|
||||
|
||||
- version: 0.7.21
|
||||
date: 2010-10-01
|
||||
|
||||
|
@ -83,6 +83,16 @@ title_series_sorting = 'library_order'
|
||||
# strictly_alphabetic, it would remain "The Client".
|
||||
save_template_title_series_sorting = 'library_order'
|
||||
|
||||
# Set the list of words that are to be considered 'articles' when computing the
|
||||
# title sort strings. The list is a regular expression, with the articles
|
||||
# separated by 'or' bars. Comparisons are case insensitive, and that cannot be
|
||||
# changed. Changes to this tweak won't have an effect until the book is modified
|
||||
# in some way. If you enter an invalid pattern, it is silently ignored.
|
||||
# To disable use the expression: '^$'
|
||||
# Default: '^(A|The|An)\s+'
|
||||
title_sort_articles=r'^(A|The|An)\s+'
|
||||
|
||||
|
||||
# Specify a folder that calibre should connect to at startup using
|
||||
# connect_to_folder. This must be a full path to the folder. If the folder does
|
||||
# not exist when calibre starts, it is ignored. If there are '\' characters in
|
||||
|
@ -42,7 +42,7 @@ class RMF24_opinie(BasicNewsRecipe):
|
||||
# thanks to Kovid Goyal
|
||||
def get_article_url(self, article):
|
||||
link = article.get('link')
|
||||
if 'audio' not in link:
|
||||
if '/audio,aId' not in link:
|
||||
return link
|
||||
|
||||
preprocess_regexps = [
|
||||
|
@ -2,7 +2,7 @@ __license__ = 'GPL v3'
|
||||
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
__appname__ = 'calibre'
|
||||
__version__ = '0.7.21'
|
||||
__version__ = '0.7.22'
|
||||
__author__ = "Kovid Goyal <kovid@kovidgoyal.net>"
|
||||
|
||||
import re
|
||||
|
@ -460,7 +460,8 @@ from calibre.devices.hanvon.driver import N516, EB511, ALEX, AZBOOKA, THEBOOK
|
||||
from calibre.devices.edge.driver import EDGE
|
||||
from calibre.devices.teclast.driver import TECLAST_K3, NEWSMY, IPAPYRUS, SOVOS
|
||||
from calibre.devices.sne.driver import SNE
|
||||
from calibre.devices.misc import PALMPRE, AVANT, SWEEX, PDNOVEL, KOGAN, GEMEI
|
||||
from calibre.devices.misc import PALMPRE, AVANT, SWEEX, PDNOVEL, KOGAN, \
|
||||
GEMEI, VELOCITYMICRO
|
||||
from calibre.devices.folder_device.driver import FOLDER_DEVICE_FOR_CONFIG
|
||||
from calibre.devices.kobo.driver import KOBO
|
||||
|
||||
@ -572,6 +573,7 @@ plugins += [
|
||||
PDNOVEL,
|
||||
SPECTRA,
|
||||
GEMEI,
|
||||
VELOCITYMICRO,
|
||||
ITUNES,
|
||||
]
|
||||
plugins += [x for x in list(locals().values()) if isinstance(x, type) and \
|
||||
|
@ -108,6 +108,24 @@ class PDNOVEL(USBMS):
|
||||
with open('%s.jpg' % os.path.join(path, filename), 'wb') as coverfile:
|
||||
coverfile.write(coverdata[2])
|
||||
|
||||
class VELOCITYMICRO(USBMS):
|
||||
name = 'VelocityMicro device interface'
|
||||
gui_name = 'VelocityMicro'
|
||||
description = _('Communicate with the VelocityMicro')
|
||||
author = 'Kovid Goyal'
|
||||
supported_platforms = ['windows', 'linux', 'osx']
|
||||
FORMATS = ['epub', 'pdb', 'txt', 'html', 'pdf']
|
||||
|
||||
VENDOR_ID = [0x18d1]
|
||||
PRODUCT_ID = [0xb015]
|
||||
BCD = [0x224]
|
||||
|
||||
VENDOR_NAME = 'ANDROID'
|
||||
WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = '__UMS_COMPOSITE'
|
||||
|
||||
EBOOK_DIR_MAIN = 'eBooks'
|
||||
SUPPORTS_SUB_DIRS = False
|
||||
|
||||
class GEMEI(USBMS):
|
||||
name = 'Gemei Device Interface'
|
||||
gui_name = 'GM2000'
|
||||
|
@ -63,6 +63,8 @@ class PRS505(USBMS):
|
||||
'series, tags, authors'
|
||||
EXTRA_CUSTOMIZATION_DEFAULT = ', '.join(['series', 'tags'])
|
||||
|
||||
plugboard = None
|
||||
|
||||
def windows_filter_pnp_id(self, pnp_id):
|
||||
return '_LAUNCHER' in pnp_id
|
||||
|
||||
@ -150,7 +152,7 @@ class PRS505(USBMS):
|
||||
else:
|
||||
collections = []
|
||||
debug_print('PRS505: collection fields:', collections)
|
||||
c.update(blists, collections)
|
||||
c.update(blists, collections, self.plugboard)
|
||||
c.write()
|
||||
|
||||
USBMS.sync_booklists(self, booklists, end_session=end_session)
|
||||
@ -163,3 +165,9 @@ class PRS505(USBMS):
|
||||
c.write()
|
||||
debug_print('PRS505: finished rebuild_collections')
|
||||
|
||||
def use_plugboard_ext(self):
|
||||
return 'device_db'
|
||||
|
||||
def set_plugboard(self, pb):
|
||||
debug_print('PRS505: use plugboard', pb)
|
||||
self.plugboard = pb
|
||||
|
@ -325,12 +325,6 @@ class XMLCache(object):
|
||||
for book in bl:
|
||||
record = lpath_map.get(book.lpath, None)
|
||||
if record is not None:
|
||||
title = record.get('title', None)
|
||||
if title is not None and title != book.title:
|
||||
debug_print('Renaming title', book.title, 'to', title)
|
||||
book.title = title
|
||||
# Don't set the author, because the reader strips all but
|
||||
# the first author.
|
||||
for thumbnail in record.xpath(
|
||||
'descendant::*[local-name()="thumbnail"]'):
|
||||
for img in thumbnail.xpath(
|
||||
@ -350,7 +344,7 @@ class XMLCache(object):
|
||||
# }}}
|
||||
|
||||
# Update XML from JSON {{{
|
||||
def update(self, booklists, collections_attributes):
|
||||
def update(self, booklists, collections_attributes, plugboard):
|
||||
debug_print('Starting update', collections_attributes)
|
||||
use_tz_var = False
|
||||
for i, booklist in booklists.items():
|
||||
@ -365,8 +359,13 @@ class XMLCache(object):
|
||||
record = lpath_map.get(book.lpath, None)
|
||||
if record is None:
|
||||
record = self.create_text_record(root, i, book.lpath)
|
||||
if plugboard is not None:
|
||||
newmi = book.deepcopy()
|
||||
newmi.template_to_attribute(book, plugboard)
|
||||
else:
|
||||
newmi = book
|
||||
(gtz_count, ltz_count, use_tz_var) = \
|
||||
self.update_text_record(record, book, path, i,
|
||||
self.update_text_record(record, newmi, path, i,
|
||||
gtz_count, ltz_count, use_tz_var)
|
||||
# Ensure the collections in the XML database are recorded for
|
||||
# this book
|
||||
|
@ -707,7 +707,7 @@ OptionRecommendation(name='timestamp',
|
||||
if mi.cover.startswith('http:') or mi.cover.startswith('https:'):
|
||||
mi.cover = self.download_cover(mi.cover)
|
||||
ext = mi.cover.rpartition('.')[-1].lower().strip()
|
||||
if ext not in ('png', 'jpg', 'jpeg'):
|
||||
if ext not in ('png', 'jpg', 'jpeg', 'gif'):
|
||||
ext = 'jpg'
|
||||
mi.cover_data = (ext, open(mi.cover, 'rb').read())
|
||||
mi.cover = None
|
||||
|
@ -44,7 +44,15 @@ def author_to_author_sort(author):
|
||||
def authors_to_sort_string(authors):
|
||||
return ' & '.join(map(author_to_author_sort, authors))
|
||||
|
||||
_title_pat = re.compile('^(A|The|An)\s+', re.IGNORECASE)
|
||||
try:
|
||||
_title_pat = re.compile(tweaks.get('title_sort_articles',
|
||||
r'^(A|The|An)\s+'), re.IGNORECASE)
|
||||
except:
|
||||
print 'Error in title sort pattern'
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
_title_pat = re.compile('^(A|The|An)\s+', re.IGNORECASE)
|
||||
|
||||
_ignore_starts = u'\'"'+u''.join(unichr(x) for x in range(0x2018, 0x201e)+[0x2032, 0x2033])
|
||||
|
||||
def title_sort(title):
|
||||
|
@ -114,7 +114,8 @@ SC_COPYABLE_FIELDS = SOCIAL_METADATA_FIELDS.union(
|
||||
PUBLICATION_METADATA_FIELDS).union(
|
||||
BOOK_STRUCTURE_FIELDS).union(
|
||||
DEVICE_METADATA_FIELDS).union(
|
||||
CALIBRE_METADATA_FIELDS) - \
|
||||
CALIBRE_METADATA_FIELDS).union(
|
||||
TOP_LEVEL_CLASSIFIERS) - \
|
||||
SC_FIELDS_NOT_COPIED.union(
|
||||
SC_FIELDS_COPY_NOT_NULL)
|
||||
|
||||
|
@ -8,6 +8,7 @@ __docformat__ = 'restructuredtext en'
|
||||
import copy, traceback
|
||||
|
||||
from calibre import prints
|
||||
from calibre.constants import DEBUG
|
||||
from calibre.ebooks.metadata.book import SC_COPYABLE_FIELDS
|
||||
from calibre.ebooks.metadata.book import SC_FIELDS_COPY_NOT_NULL
|
||||
from calibre.ebooks.metadata.book import STANDARD_METADATA_FIELDS
|
||||
@ -50,6 +51,8 @@ class SafeFormat(TemplateFormatter):
|
||||
return ''
|
||||
return v
|
||||
except:
|
||||
if DEBUG:
|
||||
traceback.print_exc()
|
||||
return key
|
||||
|
||||
composite_formatter = SafeFormat()
|
||||
@ -320,8 +323,8 @@ class Metadata(object):
|
||||
else:
|
||||
self.set(dest, val)
|
||||
except:
|
||||
traceback.print_exc()
|
||||
pass
|
||||
if DEBUG:
|
||||
traceback.print_exc()
|
||||
|
||||
# Old Metadata API {{{
|
||||
def print_all_attributes(self):
|
||||
|
@ -108,7 +108,8 @@ def _get_metadata(stream, stream_type, use_libprs_metadata,
|
||||
base = metadata_from_filename(name, pat=pattern)
|
||||
if force_read_metadata or is_recipe(name) or prefs['read_file_metadata']:
|
||||
mi = get_file_type_metadata(stream, stream_type)
|
||||
if base.title == os.path.splitext(name)[0] and base.authors is None:
|
||||
if base.title == os.path.splitext(name)[0] and \
|
||||
base.is_null('authors') and base.is_null('isbn'):
|
||||
# Assume that there was no metadata in the file and the user set pattern
|
||||
# to match meta info from the file name did not match.
|
||||
# The regex is meant to match the standard format filenames are written
|
||||
|
@ -41,24 +41,6 @@ class MOBIOutput(OutputFormatPlugin):
|
||||
),
|
||||
])
|
||||
|
||||
def remove_image_transparencies(self):
|
||||
from calibre.utils.magick.draw import save_cover_data_to
|
||||
for item in self.oeb.manifest:
|
||||
if item.media_type.startswith('image'):
|
||||
raw = item.data
|
||||
ext = item.media_type.split('/')[-1].lower()
|
||||
if ext not in ('png', 'gif') or not raw:
|
||||
continue
|
||||
try:
|
||||
data = save_cover_data_to(raw, 'img.'+ext, return_data=True)
|
||||
except:
|
||||
self.log.exception('Failed to remove transparency from',
|
||||
item.href)
|
||||
data = None
|
||||
if data is not None:
|
||||
item.data = data
|
||||
item.unload_data_from_memory()
|
||||
|
||||
def check_for_periodical(self):
|
||||
if self.oeb.metadata.publication_type and \
|
||||
unicode(self.oeb.metadata.publication_type[0]).startswith('periodical:'):
|
||||
@ -178,7 +160,6 @@ class MOBIOutput(OutputFormatPlugin):
|
||||
from calibre.ebooks.oeb.transforms.rasterize import SVGRasterizer, Unavailable
|
||||
from calibre.ebooks.oeb.transforms.htmltoc import HTMLTOCAdder
|
||||
from calibre.customize.ui import plugin_for_input_format
|
||||
self.remove_image_transparencies()
|
||||
imagemax = PALM_MAX_IMAGE_SIZE if opts.rescale_images else None
|
||||
if not opts.no_inline_toc:
|
||||
tocadder = HTMLTOCAdder(title=opts.toc_title)
|
||||
|
@ -15,7 +15,6 @@ from struct import pack
|
||||
import time
|
||||
from urlparse import urldefrag
|
||||
|
||||
from PIL import Image
|
||||
from cStringIO import StringIO
|
||||
from calibre.ebooks.mobi.langcodes import iana2mobi
|
||||
from calibre.ebooks.mobi.mobiml import MBP_NS
|
||||
@ -28,6 +27,7 @@ from calibre.ebooks.oeb.base import namespace
|
||||
from calibre.ebooks.oeb.base import prefixname
|
||||
from calibre.ebooks.oeb.base import urlnormalize
|
||||
from calibre.ebooks.compression.palmdoc import compress_doc
|
||||
from calibre.utils.magick.draw import Image, save_cover_data_to, thumbnail
|
||||
|
||||
INDEXING = True
|
||||
FCIS_FLIS = True
|
||||
@ -111,46 +111,18 @@ def align_block(raw, multiple=4, pad='\0'):
|
||||
return raw + pad*(multiple - extra)
|
||||
|
||||
def rescale_image(data, maxsizeb, dimen=None):
|
||||
image = Image.open(StringIO(data))
|
||||
format = image.format
|
||||
changed = False
|
||||
if image.format not in ('JPEG', 'GIF'):
|
||||
width, height = image.size
|
||||
area = width * height
|
||||
if area <= 40000:
|
||||
format = 'GIF'
|
||||
else:
|
||||
image = image.convert('RGBA')
|
||||
format = 'JPEG'
|
||||
changed = True
|
||||
if dimen is not None:
|
||||
image.thumbnail(dimen, Image.ANTIALIAS)
|
||||
changed = True
|
||||
if changed:
|
||||
data = StringIO()
|
||||
image.save(data, format)
|
||||
data = data.getvalue()
|
||||
if len(data) <= maxsizeb:
|
||||
return data
|
||||
image = image.convert('RGBA')
|
||||
for quality in xrange(95, -1, -1):
|
||||
data = StringIO()
|
||||
image.save(data, 'JPEG', quality=quality)
|
||||
data = data.getvalue()
|
||||
if len(data) <= maxsizeb:
|
||||
return data
|
||||
width, height = image.size
|
||||
for scale in xrange(99, 0, -1):
|
||||
scale = scale / 100.
|
||||
data = StringIO()
|
||||
scaled = image.copy()
|
||||
size = (int(width * scale), (height * scale))
|
||||
scaled.thumbnail(size, Image.ANTIALIAS)
|
||||
scaled.save(data, 'JPEG', quality=0)
|
||||
data = data.getvalue()
|
||||
if len(data) <= maxsizeb:
|
||||
return data
|
||||
# Well, we tried?
|
||||
return thumbnail(data, width=dimen, height=dimen)[-1]
|
||||
# Replace transparent pixels with white pixels and convert to JPEG
|
||||
data = save_cover_data_to(data, 'img.jpg', return_data=True)
|
||||
scale = 0.9
|
||||
while len(data) >= maxsizeb and scale >= 0.05:
|
||||
img = Image()
|
||||
img.load(data)
|
||||
w, h = img.size
|
||||
img.size = (int(scale*w), int(scale*h))
|
||||
data = img.export('jpg')
|
||||
scale -= 0.05
|
||||
return data
|
||||
|
||||
class Serializer(object):
|
||||
|
@ -234,13 +234,14 @@ class AddAction(InterfaceAction):
|
||||
self.gui.set_books_in_library(booklists=[model.db], reset=True)
|
||||
self.gui.refresh_ondevice()
|
||||
|
||||
def add_books_from_device(self, view):
|
||||
rows = view.selectionModel().selectedRows()
|
||||
if not rows or len(rows) == 0:
|
||||
d = error_dialog(self.gui, _('Add to library'), _('No book selected'))
|
||||
d.exec_()
|
||||
return
|
||||
paths = [p for p in view._model.paths(rows) if p is not None]
|
||||
def add_books_from_device(self, view, paths=None):
|
||||
if paths is None:
|
||||
rows = view.selectionModel().selectedRows()
|
||||
if not rows or len(rows) == 0:
|
||||
d = error_dialog(self.gui, _('Add to library'), _('No book selected'))
|
||||
d.exec_()
|
||||
return
|
||||
paths = [p for p in view.model().paths(rows) if p is not None]
|
||||
ve = self.gui.device_manager.device.VIRTUAL_BOOK_EXTENSIONS
|
||||
def ext(x):
|
||||
ans = os.path.splitext(x)[1]
|
||||
@ -261,7 +262,7 @@ class AddAction(InterfaceAction):
|
||||
return
|
||||
from calibre.gui2.add import Adder
|
||||
self.__adder_func = partial(self._add_from_device_adder, on_card=None,
|
||||
model=view._model)
|
||||
model=view.model())
|
||||
self._adder = Adder(self.gui, self.gui.library_view.model().db,
|
||||
self.Dispatcher(self.__adder_func), spare_server=self.gui.spare_server)
|
||||
self._adder.add(paths)
|
||||
|
@ -181,5 +181,6 @@ class ConvertAction(InterfaceAction):
|
||||
self.gui.tags_view.recount()
|
||||
if self.gui.current_view() is self.gui.library_view:
|
||||
current = self.gui.library_view.currentIndex()
|
||||
self.gui.library_view.model().current_changed(current, QModelIndex())
|
||||
if current.isValid():
|
||||
self.gui.library_view.model().current_changed(current, QModelIndex())
|
||||
|
||||
|
@ -21,7 +21,10 @@ from calibre.gui2.convert import Widget
|
||||
def create_opf_file(db, book_id):
|
||||
mi = db.get_metadata(book_id, index_is_id=True)
|
||||
mi.application_id = uuid.uuid4()
|
||||
old_cover = mi.cover
|
||||
mi.cover = None
|
||||
raw = metadata_to_opf(mi)
|
||||
mi.cover = old_cover
|
||||
opf_file = PersistentTemporaryFile('.opf')
|
||||
opf_file.write(raw)
|
||||
opf_file.close()
|
||||
|
@ -310,7 +310,13 @@ class DeviceManager(Thread): # {{{
|
||||
self.device.sync_booklists(booklists, end_session=False)
|
||||
return self.device.card_prefix(end_session=False), self.device.free_space()
|
||||
|
||||
def sync_booklists(self, done, booklists):
|
||||
def sync_booklists(self, done, booklists, plugboards):
|
||||
if hasattr(self.connected_device, 'use_plugboard_ext') and \
|
||||
callable(self.connected_device.use_plugboard_ext):
|
||||
ext = self.connected_device.use_plugboard_ext()
|
||||
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],
|
||||
description=_('Send metadata to device'))
|
||||
|
||||
@ -319,28 +325,31 @@ class DeviceManager(Thread): # {{{
|
||||
args=[booklist, on_card],
|
||||
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):
|
||||
'''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.__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('Using plugboard', ext, dev_name, cpb)
|
||||
cpb = self.find_plugboard(ext, plugboards)
|
||||
if ext:
|
||||
try:
|
||||
if DEBUG:
|
||||
@ -1247,8 +1256,9 @@ class DeviceMixin(object): # {{{
|
||||
'''
|
||||
Upload metadata to device.
|
||||
'''
|
||||
plugboards = self.library_view.model().db.prefs.get('plugboards', {})
|
||||
self.device_manager.sync_booklists(Dispatcher(self.metadata_synced),
|
||||
self.booklists())
|
||||
self.booklists(), plugboards)
|
||||
|
||||
def metadata_synced(self, job):
|
||||
'''
|
||||
@ -1502,8 +1512,10 @@ class DeviceMixin(object): # {{{
|
||||
|
||||
if update_metadata:
|
||||
if self.device_manager.is_device_connected:
|
||||
plugboards = self.library_view.model().db.prefs.get('plugboards', {})
|
||||
self.device_manager.sync_booklists(
|
||||
Dispatcher(self.metadata_synced), booklists)
|
||||
Dispatcher(self.metadata_synced), booklists,
|
||||
plugboards)
|
||||
return update_metadata
|
||||
# }}}
|
||||
|
||||
|
@ -56,6 +56,7 @@ class LocationManager(QObject): # {{{
|
||||
self._mem.append(a)
|
||||
else:
|
||||
ac.setToolTip(tooltip)
|
||||
ac.calibre_name = name
|
||||
|
||||
return ac
|
||||
|
||||
@ -112,7 +113,6 @@ class LocationManager(QObject): # {{{
|
||||
ac.setWhatsThis(t)
|
||||
ac.setStatusTip(t)
|
||||
|
||||
|
||||
@property
|
||||
def has_device(self):
|
||||
return max(self.free) > -1
|
||||
@ -228,6 +228,7 @@ class ToolBar(QToolBar): # {{{
|
||||
self.added_actions = []
|
||||
self.build_bar()
|
||||
self.preferred_width = self.sizeHint().width()
|
||||
self.setAcceptDrops(True)
|
||||
|
||||
def apply_settings(self):
|
||||
sz = gprefs['toolbar_icon_size']
|
||||
@ -317,6 +318,59 @@ class ToolBar(QToolBar): # {{{
|
||||
def database_changed(self, db):
|
||||
pass
|
||||
|
||||
#support drag&drop from/to library from/to reader/card
|
||||
def dragEnterEvent(self, event):
|
||||
md = event.mimeData()
|
||||
if md.hasFormat("application/calibre+from_library") or \
|
||||
md.hasFormat("application/calibre+from_device"):
|
||||
event.setDropAction(Qt.CopyAction)
|
||||
event.accept()
|
||||
else:
|
||||
event.ignore()
|
||||
|
||||
def dragMoveEvent(self, event):
|
||||
allowed = False
|
||||
md = event.mimeData()
|
||||
#Drop is only allowed in the location manager widget's different from the selected one
|
||||
for ac in self.location_manager.available_actions:
|
||||
w = self.widgetForAction(ac)
|
||||
if w is not None:
|
||||
if ( md.hasFormat("application/calibre+from_library") or \
|
||||
md.hasFormat("application/calibre+from_device") ) and \
|
||||
w.geometry().contains(event.pos()) and \
|
||||
isinstance(w, QToolButton) and not w.isChecked():
|
||||
allowed = True
|
||||
break
|
||||
if allowed:
|
||||
event.acceptProposedAction()
|
||||
else:
|
||||
event.ignore()
|
||||
|
||||
def dropEvent(self, event):
|
||||
data = event.mimeData()
|
||||
|
||||
mime = 'application/calibre+from_library'
|
||||
if data.hasFormat(mime):
|
||||
ids = list(map(int, str(data.data(mime)).split()))
|
||||
tgt = None
|
||||
for ac in self.location_manager.available_actions:
|
||||
w = self.widgetForAction(ac)
|
||||
if w is not None and w.geometry().contains(event.pos()):
|
||||
tgt = ac.calibre_name
|
||||
if tgt is not None:
|
||||
if tgt == 'main':
|
||||
tgt = None
|
||||
self.gui.sync_to_device(tgt, False, send_ids=ids)
|
||||
event.accept()
|
||||
|
||||
mime = 'application/calibre+from_device'
|
||||
if data.hasFormat(mime):
|
||||
paths = [unicode(u.toLocalFile()) for u in data.urls()]
|
||||
if paths:
|
||||
self.gui.iactions['Add Books'].add_books_from_device(
|
||||
self.gui.current_view(), paths=paths)
|
||||
event.accept()
|
||||
|
||||
# }}}
|
||||
|
||||
class MainWindowMixin(object): # {{{
|
||||
|
@ -153,7 +153,7 @@ class TextDelegate(QStyledItemDelegate): # {{{
|
||||
complete_items = [i[1] for i in self.auto_complete_function()]
|
||||
completer = QCompleter(complete_items, self)
|
||||
completer.setCaseSensitivity(Qt.CaseInsensitive)
|
||||
completer.setCompletionMode(QCompleter.InlineCompletion)
|
||||
completer.setCompletionMode(QCompleter.PopupCompletion)
|
||||
editor.setCompleter(completer)
|
||||
return editor
|
||||
#}}}
|
||||
|
@ -361,13 +361,14 @@ class BooksModel(QAbstractTableModel): # {{{
|
||||
self.cover_cache.set_cache(ids)
|
||||
|
||||
def current_changed(self, current, previous, emit_signal=True):
|
||||
idx = current.row()
|
||||
self.set_cache(idx)
|
||||
data = self.get_book_display_info(idx)
|
||||
if emit_signal:
|
||||
self.new_bookdisplay_data.emit(data)
|
||||
else:
|
||||
return data
|
||||
if current.isValid():
|
||||
idx = current.row()
|
||||
self.set_cache(idx)
|
||||
data = self.get_book_display_info(idx)
|
||||
if emit_signal:
|
||||
self.new_bookdisplay_data.emit(data)
|
||||
else:
|
||||
return data
|
||||
|
||||
def get_book_info(self, index):
|
||||
if isinstance(index, int):
|
||||
@ -1081,12 +1082,11 @@ class DeviceBooksModel(BooksModel): # {{{
|
||||
self.db = db
|
||||
self.map = list(range(0, len(db)))
|
||||
|
||||
def current_changed(self, current, previous):
|
||||
data = {}
|
||||
item = self.db[self.map[current.row()]]
|
||||
def cover(self, row):
|
||||
item = self.db[self.map[row]]
|
||||
cdata = item.thumbnail
|
||||
img = QImage()
|
||||
if cdata is not None:
|
||||
img = QImage()
|
||||
if hasattr(cdata, 'image_path'):
|
||||
img.load(cdata.image_path)
|
||||
elif cdata:
|
||||
@ -1094,9 +1094,16 @@ class DeviceBooksModel(BooksModel): # {{{
|
||||
img.loadFromData(cdata[-1])
|
||||
else:
|
||||
img.loadFromData(cdata)
|
||||
if img.isNull():
|
||||
img = self.default_image
|
||||
data['cover'] = img
|
||||
if img.isNull():
|
||||
img = self.default_image
|
||||
return img
|
||||
|
||||
def current_changed(self, current, previous):
|
||||
data = {}
|
||||
item = self.db[self.map[current.row()]]
|
||||
cover = self.cover(current.row())
|
||||
if cover is not self.default_image:
|
||||
data['cover'] = cover
|
||||
type = _('Unknown')
|
||||
ext = os.path.splitext(item.path)[1]
|
||||
if ext:
|
||||
|
@ -9,7 +9,8 @@ import os
|
||||
from functools import partial
|
||||
|
||||
from PyQt4.Qt import QTableView, Qt, QAbstractItemView, QMenu, pyqtSignal, \
|
||||
QModelIndex, QIcon, QItemSelection
|
||||
QModelIndex, QIcon, QItemSelection, QMimeData, QDrag, QApplication, \
|
||||
QPoint, QPixmap, QUrl, QImage, QPainter, QColor, QRect
|
||||
|
||||
from calibre.gui2.library.delegates import RatingDelegate, PubDateDelegate, \
|
||||
TextDelegate, DateDelegate, TagsDelegate, CcTextDelegate, \
|
||||
@ -18,7 +19,8 @@ from calibre.gui2.library.models import BooksModel, DeviceBooksModel
|
||||
from calibre.utils.config import tweaks, prefs
|
||||
from calibre.gui2 import error_dialog, gprefs
|
||||
from calibre.gui2.library import DEFAULT_SORT
|
||||
|
||||
from calibre.constants import filesystem_encoding
|
||||
from calibre import force_unicode
|
||||
|
||||
class BooksView(QTableView): # {{{
|
||||
|
||||
@ -31,6 +33,7 @@ class BooksView(QTableView): # {{{
|
||||
self.setDragEnabled(True)
|
||||
self.setDragDropOverwriteMode(False)
|
||||
self.setDragDropMode(self.DragDrop)
|
||||
self.drag_start_pos = None
|
||||
self.setAlternatingRowColors(True)
|
||||
self.setSelectionBehavior(self.SelectRows)
|
||||
self.setShowGrid(False)
|
||||
@ -422,10 +425,92 @@ class BooksView(QTableView): # {{{
|
||||
Accept a drop event and return a list of paths that can be read from
|
||||
and represent files with extensions.
|
||||
'''
|
||||
if event.mimeData().hasFormat('text/uri-list'):
|
||||
urls = [unicode(u.toLocalFile()) for u in event.mimeData().urls()]
|
||||
md = event.mimeData()
|
||||
if md.hasFormat('text/uri-list') and not \
|
||||
md.hasFormat('application/calibre+from_library'):
|
||||
urls = [unicode(u.toLocalFile()) for u in md.urls()]
|
||||
return [u for u in urls if os.path.splitext(u)[1] and os.access(u, os.R_OK)]
|
||||
|
||||
def drag_icon(self, cover, multiple):
|
||||
cover = cover.scaledToHeight(120, Qt.SmoothTransformation)
|
||||
if multiple:
|
||||
base_width = cover.width()
|
||||
base_height = cover.height()
|
||||
base = QImage(base_width+21, base_height+21,
|
||||
QImage.Format_ARGB32_Premultiplied)
|
||||
base.fill(QColor(255, 255, 255, 0).rgba())
|
||||
p = QPainter(base)
|
||||
rect = QRect(20, 0, base_width, base_height)
|
||||
p.fillRect(rect, QColor('white'))
|
||||
p.drawRect(rect)
|
||||
rect.moveLeft(10)
|
||||
rect.moveTop(10)
|
||||
p.fillRect(rect, QColor('white'))
|
||||
p.drawRect(rect)
|
||||
rect.moveLeft(0)
|
||||
rect.moveTop(20)
|
||||
p.fillRect(rect, QColor('white'))
|
||||
p.save()
|
||||
p.setCompositionMode(p.CompositionMode_SourceAtop)
|
||||
p.drawImage(rect.topLeft(), cover)
|
||||
p.restore()
|
||||
p.drawRect(rect)
|
||||
p.end()
|
||||
cover = base
|
||||
return QPixmap.fromImage(cover)
|
||||
|
||||
def drag_data(self):
|
||||
m = self.model()
|
||||
db = m.db
|
||||
rows = self.selectionModel().selectedRows()
|
||||
selected = map(m.id, rows)
|
||||
ids = ' '.join(map(str, selected))
|
||||
md = QMimeData()
|
||||
md.setData('application/calibre+from_library', ids)
|
||||
fmt = prefs['output_format']
|
||||
|
||||
def url_for_id(i):
|
||||
ans = db.format_abspath(i, fmt, index_is_id=True)
|
||||
if ans is None:
|
||||
fmts = db.formats(i, index_is_id=True)
|
||||
if fmts:
|
||||
fmts = fmts.split(',')
|
||||
else:
|
||||
fmts = []
|
||||
for f in fmts:
|
||||
ans = db.format_abspath(i, f, index_is_id=True)
|
||||
if ans is not None:
|
||||
break
|
||||
if ans is None:
|
||||
ans = db.abspath(i, index_is_id=True)
|
||||
return QUrl.fromLocalFile(ans)
|
||||
|
||||
md.setUrls([url_for_id(i) for i in selected])
|
||||
drag = QDrag(self)
|
||||
drag.setMimeData(md)
|
||||
cover = self.drag_icon(m.cover(self.currentIndex().row()),
|
||||
len(selected) > 1)
|
||||
drag.setHotSpot(QPoint(cover.width()//3, cover.height()//3))
|
||||
drag.setPixmap(cover)
|
||||
return drag
|
||||
|
||||
def mousePressEvent(self, event):
|
||||
if event.button() == Qt.LeftButton:
|
||||
self.drag_start_pos = event.pos()
|
||||
return QTableView.mousePressEvent(self, event)
|
||||
|
||||
def mouseMoveEvent(self, event):
|
||||
if not (event.buttons() & Qt.LeftButton) or self.drag_start_pos is None:
|
||||
return
|
||||
if (event.pos() - self.drag_start_pos).manhattanLength() \
|
||||
< QApplication.startDragDistance():
|
||||
return
|
||||
index = self.indexAt(event.pos())
|
||||
if not index.isValid():
|
||||
return
|
||||
drag = self.drag_data()
|
||||
drag.exec_(Qt.CopyAction)
|
||||
|
||||
def dragEnterEvent(self, event):
|
||||
if int(event.possibleActions() & Qt.CopyAction) + \
|
||||
int(event.possibleActions() & Qt.MoveAction) == 0:
|
||||
@ -547,6 +632,21 @@ class DeviceBooksView(BooksView): # {{{
|
||||
self.setDragDropMode(self.NoDragDrop)
|
||||
self.setAcceptDrops(False)
|
||||
|
||||
def drag_data(self):
|
||||
m = self.model()
|
||||
rows = self.selectionModel().selectedRows()
|
||||
paths = [force_unicode(p, enc=filesystem_encoding) for p in m.paths(rows) if p]
|
||||
md = QMimeData()
|
||||
md.setData('application/calibre+from_device', 'dummy')
|
||||
md.setUrls([QUrl.fromLocalFile(p) for p in paths])
|
||||
drag = QDrag(self)
|
||||
drag.setMimeData(md)
|
||||
cover = self.drag_icon(m.cover(self.currentIndex().row()), len(paths) >
|
||||
1)
|
||||
drag.setHotSpot(QPoint(cover.width()//3, cover.height()//3))
|
||||
drag.setPixmap(cover)
|
||||
return drag
|
||||
|
||||
def contextMenuEvent(self, event):
|
||||
edit_collections = callable(getattr(self._model.db, 'supports_collections', None)) and \
|
||||
self._model.db.supports_collections() and \
|
||||
|
@ -39,6 +39,12 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
|
||||
|
||||
ConfigWidgetBase.initialize(self)
|
||||
|
||||
if self.gui.device_manager.connected_device is not None:
|
||||
self.device_label.setText(_('Device currently connected: ') +
|
||||
self.gui.device_manager.connected_device.__class__.__name__)
|
||||
else:
|
||||
self.device_label.setText(_('Device currently connected: None'))
|
||||
|
||||
self.devices = ['']
|
||||
for device in device_plugins():
|
||||
n = device.__class__.__name__
|
||||
@ -54,6 +60,7 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
|
||||
for w in metadata_writers():
|
||||
for f in w.file_types:
|
||||
self.formats.append(f)
|
||||
self.formats.append('device_db')
|
||||
self.formats.sort()
|
||||
self.formats.insert(1, plugboard_any_format_value)
|
||||
self.new_format.addItems(self.formats)
|
||||
|
@ -40,7 +40,18 @@ One possible use for a plugboard is to alter the title to contain series informa
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<item row="2" column="0" colspan="2">
|
||||
<widget class="QLabel" name="device_label">
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="0" colspan="2">
|
||||
<widget class="Line" name="line">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="4" column="0">
|
||||
<layout class="QGridLayout" name="gridLayout_2">
|
||||
<item row="0" column="1">
|
||||
<widget class="QLabel" name="label_6">
|
||||
@ -123,7 +134,7 @@ One possible use for a plugboard is to alter the title to contain series informa
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item row="2" column="1">
|
||||
<item row="4" column="1">
|
||||
<layout class="QGridLayout" name="fields_layout">
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="label_2">
|
||||
|
@ -79,6 +79,8 @@ class TagsView(QTreeView): # {{{
|
||||
self.setHeaderHidden(True)
|
||||
self.setItemDelegate(TagDelegate(self))
|
||||
self.made_connections = False
|
||||
self.setAcceptDrops(True)
|
||||
self.setDropIndicatorShown(True)
|
||||
|
||||
def set_database(self, db, tag_match, sort_by):
|
||||
self.hidden_categories = config['tag_browser_hidden_categories']
|
||||
@ -104,6 +106,49 @@ class TagsView(QTreeView): # {{{
|
||||
def database_changed(self, event, ids):
|
||||
self.refresh_required.emit()
|
||||
|
||||
def dragEnterEvent(self, event):
|
||||
md = event.mimeData()
|
||||
if md.hasFormat("application/calibre+from_library"):
|
||||
event.setDropAction(Qt.CopyAction)
|
||||
event.accept()
|
||||
else:
|
||||
event.ignore()
|
||||
|
||||
def dragMoveEvent(self, event):
|
||||
allowed = False
|
||||
idx = self.indexAt(event.pos())
|
||||
m = self.model()
|
||||
p = m.parent(idx)
|
||||
if idx.isValid() and p.isValid():
|
||||
item = m.data(p, Qt.UserRole)
|
||||
if item.type == TagTreeItem.CATEGORY and \
|
||||
item.category_key in \
|
||||
('tags', 'series', 'authors', 'rating', 'publisher'):
|
||||
allowed = True
|
||||
if allowed:
|
||||
event.acceptProposedAction()
|
||||
else:
|
||||
event.ignore()
|
||||
|
||||
def dropEvent(self, event):
|
||||
idx = self.indexAt(event.pos())
|
||||
m = self.model()
|
||||
p = m.parent(idx)
|
||||
if idx.isValid() and p.isValid():
|
||||
item = m.data(p, Qt.UserRole)
|
||||
if item.type == TagTreeItem.CATEGORY and \
|
||||
item.category_key in \
|
||||
('tags', 'series', 'authors', 'rating', 'publisher'):
|
||||
child = m.data(idx, Qt.UserRole)
|
||||
md = event.mimeData()
|
||||
mime = 'application/calibre+from_library'
|
||||
ids = list(map(int, str(md.data(mime)).split()))
|
||||
self.handle_drop(item, child, ids)
|
||||
event.accept()
|
||||
|
||||
def handle_drop(self, parent, child, ids):
|
||||
print 'Dropped ids:', ids
|
||||
|
||||
@property
|
||||
def match_all(self):
|
||||
return self.tag_match and self.tag_match.currentIndex() > 0
|
||||
@ -326,6 +371,8 @@ class TagTreeItem(object): # {{{
|
||||
self.children.append(child)
|
||||
|
||||
def data(self, role):
|
||||
if role == Qt.UserRole:
|
||||
return self
|
||||
if self.type == self.TAG:
|
||||
return self.tag_data(role)
|
||||
if self.type == self.CATEGORY:
|
||||
@ -544,8 +591,14 @@ class TagsModel(QAbstractItemModel): # {{{
|
||||
def headerData(self, *args):
|
||||
return NONE
|
||||
|
||||
def flags(self, *args):
|
||||
return Qt.ItemIsEnabled|Qt.ItemIsSelectable|Qt.ItemIsEditable
|
||||
def flags(self, index, *args):
|
||||
ans = Qt.ItemIsEnabled|Qt.ItemIsSelectable|Qt.ItemIsEditable
|
||||
if index.isValid() and self.parent(index).isValid():
|
||||
ans |= Qt.ItemIsDropEnabled
|
||||
return ans
|
||||
|
||||
def supportedDropActions(self):
|
||||
return Qt.CopyAction|Qt.MoveAction
|
||||
|
||||
def path_for_index(self, index):
|
||||
ans = []
|
||||
|
@ -1593,7 +1593,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
||||
self.conn.commit()
|
||||
|
||||
def delete_publisher_using_id(self, old_id):
|
||||
self.dirty_books_referencing('publisher', id, commit=False)
|
||||
self.dirty_books_referencing('publisher', old_id, commit=False)
|
||||
self.conn.execute('''DELETE FROM books_publishers_link
|
||||
WHERE publisher=?''', (old_id,))
|
||||
self.conn.execute('DELETE FROM publishers WHERE id=?', (old_id,))
|
||||
|
@ -8,6 +8,7 @@ __docformat__ = 'restructuredtext en'
|
||||
|
||||
import os, traceback, cStringIO, re
|
||||
|
||||
from calibre.constants import DEBUG
|
||||
from calibre.utils.config import Config, StringConfig, tweaks
|
||||
from calibre.utils.formatter import TemplateFormatter
|
||||
from calibre.utils.filenames import shorten_components_to, supports_long_names, \
|
||||
@ -118,8 +119,8 @@ class SafeFormat(TemplateFormatter):
|
||||
try:
|
||||
b = self.book.get_user_metadata(key, False)
|
||||
except:
|
||||
prints('save_to_disk get value exception')
|
||||
traceback.print_exc()
|
||||
if DEBUG:
|
||||
traceback.print_exc()
|
||||
b = None
|
||||
|
||||
if b is not None and b['datatype'] == 'composite':
|
||||
@ -129,13 +130,13 @@ class SafeFormat(TemplateFormatter):
|
||||
self.composite_values[key] = \
|
||||
self.vformat(b['display']['composite_template'], [], kwargs)
|
||||
return self.composite_values[key]
|
||||
if kwargs[key]:
|
||||
return self.sanitize(kwargs[key])
|
||||
if key in kwargs:
|
||||
return kwargs[key].replace('/', '_').replace('\\', '_')
|
||||
return ''
|
||||
except:
|
||||
print('save_to_disk general exception')
|
||||
traceback.print_exc()
|
||||
return ''
|
||||
if DEBUG:
|
||||
traceback.print_exc()
|
||||
return key
|
||||
|
||||
safe_formatter = SafeFormat()
|
||||
|
||||
@ -182,8 +183,8 @@ def get_components(template, mi, id, timefmt='%b %Y', length=250,
|
||||
elif custom_metadata[key]['datatype'] == 'bool':
|
||||
format_args[key] = _('yes') if format_args[key] else _('no')
|
||||
|
||||
components = safe_formatter.safe_format(template, format_args, '', mi,
|
||||
sanitize=sanitize_func)
|
||||
components = safe_formatter.safe_format(template, format_args,
|
||||
'G_C-EXCEPTION!', mi)
|
||||
components = [x.strip() for x in components.split('/') if x.strip()]
|
||||
components = [sanitize_func(x) for x in components if x]
|
||||
if not components:
|
||||
@ -267,7 +268,8 @@ def save_book_to_disk(id, db, root, opts, length):
|
||||
cpb = cpb[dev_name]
|
||||
else:
|
||||
cpb = None
|
||||
#prints('Using plugboard:', fmt, cpb)
|
||||
if DEBUG:
|
||||
prints('Save-to-disk using plugboard:', fmt, cpb)
|
||||
data = db.format(id, fmt, index_is_id=True)
|
||||
if data is None:
|
||||
continue
|
||||
@ -285,7 +287,8 @@ def save_book_to_disk(id, db, root, opts, length):
|
||||
newmi = mi
|
||||
set_metadata(stream, newmi, fmt)
|
||||
except:
|
||||
traceback.print_exc()
|
||||
if DEBUG:
|
||||
traceback.print_exc()
|
||||
stream.seek(0)
|
||||
data = stream.read()
|
||||
fmt_path = base_path+'.'+str(fmt)
|
||||
|
@ -165,7 +165,7 @@ For tags, the result cut apart whereever |app| finds a comma. For example, if th
|
||||
|
||||
The same thing happens for authors, but using a different character for the cut, a `&` (ampersand) instead of a comma. For example, if the template produces the value ``Blogs, Joe&Posts, Susan``, then the book will end up with two authors, ``Blogs, Joe`` and ``Posts, Susan``. If the template produces the value ``Blogs, Joe;Posts, Susan``, then the book will have one author with a rather strange name.
|
||||
|
||||
Plugboards affect only the metadata written into the book. They do not affect calibre's metadata or the metadata used in ``save to disk`` and ``send to device`` templates. Plugboards also do not affect what is written into a Sony's database, so cannot be used for altering the metadata shown on a Sony's menu.
|
||||
Plugboards affect the metadata written into the book when it is saved to disk or written to the device. Plugboards do not affect the metadata used by ``save to disk`` and ``send to device`` to create the file names. Instead, file names are constructed using the templates entered on the appropriate preferences window.
|
||||
|
||||
Helpful Tips
|
||||
------------
|
||||
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -4,7 +4,9 @@ Created on 23 Sep 2010
|
||||
@author: charles
|
||||
'''
|
||||
|
||||
import re, string
|
||||
import re, string, traceback
|
||||
|
||||
from calibre.constants import DEBUG
|
||||
|
||||
class TemplateFormatter(string.Formatter):
|
||||
'''
|
||||
@ -19,7 +21,6 @@ class TemplateFormatter(string.Formatter):
|
||||
string.Formatter.__init__(self)
|
||||
self.book = None
|
||||
self.kwargs = None
|
||||
self.sanitize = None
|
||||
|
||||
def _lookup(self, val, field_if_set, field_not_set):
|
||||
if val:
|
||||
@ -99,8 +100,8 @@ class TemplateFormatter(string.Formatter):
|
||||
return fmt, '', ''
|
||||
return matches.groups()
|
||||
except:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
if DEBUG:
|
||||
traceback.print_exc()
|
||||
return fmt, '', ''
|
||||
|
||||
def format_field(self, val, fmt):
|
||||
@ -139,14 +140,15 @@ class TemplateFormatter(string.Formatter):
|
||||
ans = string.Formatter.vformat(self, fmt, args, kwargs)
|
||||
return self.compress_spaces.sub(' ', ans).strip()
|
||||
|
||||
def safe_format(self, fmt, kwargs, error_value, book, sanitize=None):
|
||||
def safe_format(self, fmt, kwargs, error_value, book):
|
||||
self.kwargs = kwargs
|
||||
self.book = book
|
||||
self.sanitize = sanitize
|
||||
self.composite_values = {}
|
||||
try:
|
||||
ans = self.vformat(fmt, [], kwargs).strip()
|
||||
except:
|
||||
if DEBUG:
|
||||
traceback.print_exc()
|
||||
ans = error_value
|
||||
return ans
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user