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.
|
# for important features/bug fixes.
|
||||||
# Also, each release can have new and improved recipes.
|
# 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
|
- version: 0.7.21
|
||||||
date: 2010-10-01
|
date: 2010-10-01
|
||||||
|
|
||||||
|
@ -83,6 +83,16 @@ title_series_sorting = 'library_order'
|
|||||||
# strictly_alphabetic, it would remain "The Client".
|
# strictly_alphabetic, it would remain "The Client".
|
||||||
save_template_title_series_sorting = 'library_order'
|
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
|
# 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
|
# 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
|
# 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
|
# thanks to Kovid Goyal
|
||||||
def get_article_url(self, article):
|
def get_article_url(self, article):
|
||||||
link = article.get('link')
|
link = article.get('link')
|
||||||
if 'audio' not in link:
|
if '/audio,aId' not in link:
|
||||||
return link
|
return link
|
||||||
|
|
||||||
preprocess_regexps = [
|
preprocess_regexps = [
|
||||||
|
@ -2,7 +2,7 @@ __license__ = 'GPL v3'
|
|||||||
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
|
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
|
||||||
__docformat__ = 'restructuredtext en'
|
__docformat__ = 'restructuredtext en'
|
||||||
__appname__ = 'calibre'
|
__appname__ = 'calibre'
|
||||||
__version__ = '0.7.21'
|
__version__ = '0.7.22'
|
||||||
__author__ = "Kovid Goyal <kovid@kovidgoyal.net>"
|
__author__ = "Kovid Goyal <kovid@kovidgoyal.net>"
|
||||||
|
|
||||||
import re
|
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.edge.driver import EDGE
|
||||||
from calibre.devices.teclast.driver import TECLAST_K3, NEWSMY, IPAPYRUS, SOVOS
|
from calibre.devices.teclast.driver import TECLAST_K3, NEWSMY, IPAPYRUS, SOVOS
|
||||||
from calibre.devices.sne.driver import SNE
|
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.folder_device.driver import FOLDER_DEVICE_FOR_CONFIG
|
||||||
from calibre.devices.kobo.driver import KOBO
|
from calibre.devices.kobo.driver import KOBO
|
||||||
|
|
||||||
@ -572,6 +573,7 @@ plugins += [
|
|||||||
PDNOVEL,
|
PDNOVEL,
|
||||||
SPECTRA,
|
SPECTRA,
|
||||||
GEMEI,
|
GEMEI,
|
||||||
|
VELOCITYMICRO,
|
||||||
ITUNES,
|
ITUNES,
|
||||||
]
|
]
|
||||||
plugins += [x for x in list(locals().values()) if isinstance(x, type) and \
|
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:
|
with open('%s.jpg' % os.path.join(path, filename), 'wb') as coverfile:
|
||||||
coverfile.write(coverdata[2])
|
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):
|
class GEMEI(USBMS):
|
||||||
name = 'Gemei Device Interface'
|
name = 'Gemei Device Interface'
|
||||||
gui_name = 'GM2000'
|
gui_name = 'GM2000'
|
||||||
|
@ -63,6 +63,8 @@ class PRS505(USBMS):
|
|||||||
'series, tags, authors'
|
'series, tags, authors'
|
||||||
EXTRA_CUSTOMIZATION_DEFAULT = ', '.join(['series', 'tags'])
|
EXTRA_CUSTOMIZATION_DEFAULT = ', '.join(['series', 'tags'])
|
||||||
|
|
||||||
|
plugboard = 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
|
||||||
|
|
||||||
@ -150,7 +152,7 @@ class PRS505(USBMS):
|
|||||||
else:
|
else:
|
||||||
collections = []
|
collections = []
|
||||||
debug_print('PRS505: collection fields:', collections)
|
debug_print('PRS505: collection fields:', collections)
|
||||||
c.update(blists, collections)
|
c.update(blists, collections, self.plugboard)
|
||||||
c.write()
|
c.write()
|
||||||
|
|
||||||
USBMS.sync_booklists(self, booklists, end_session=end_session)
|
USBMS.sync_booklists(self, booklists, end_session=end_session)
|
||||||
@ -163,3 +165,9 @@ class PRS505(USBMS):
|
|||||||
c.write()
|
c.write()
|
||||||
debug_print('PRS505: finished rebuild_collections')
|
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:
|
for book in bl:
|
||||||
record = lpath_map.get(book.lpath, None)
|
record = lpath_map.get(book.lpath, None)
|
||||||
if record is not 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(
|
for thumbnail in record.xpath(
|
||||||
'descendant::*[local-name()="thumbnail"]'):
|
'descendant::*[local-name()="thumbnail"]'):
|
||||||
for img in thumbnail.xpath(
|
for img in thumbnail.xpath(
|
||||||
@ -350,7 +344,7 @@ class XMLCache(object):
|
|||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
# Update XML from JSON {{{
|
# Update XML from JSON {{{
|
||||||
def update(self, booklists, collections_attributes):
|
def update(self, booklists, collections_attributes, plugboard):
|
||||||
debug_print('Starting update', collections_attributes)
|
debug_print('Starting update', collections_attributes)
|
||||||
use_tz_var = False
|
use_tz_var = False
|
||||||
for i, booklist in booklists.items():
|
for i, booklist in booklists.items():
|
||||||
@ -365,8 +359,13 @@ class XMLCache(object):
|
|||||||
record = lpath_map.get(book.lpath, None)
|
record = lpath_map.get(book.lpath, None)
|
||||||
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:
|
||||||
|
newmi = book.deepcopy()
|
||||||
|
newmi.template_to_attribute(book, plugboard)
|
||||||
|
else:
|
||||||
|
newmi = book
|
||||||
(gtz_count, ltz_count, use_tz_var) = \
|
(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)
|
gtz_count, ltz_count, use_tz_var)
|
||||||
# Ensure the collections in the XML database are recorded for
|
# Ensure the collections in the XML database are recorded for
|
||||||
# this book
|
# this book
|
||||||
|
@ -707,7 +707,7 @@ OptionRecommendation(name='timestamp',
|
|||||||
if mi.cover.startswith('http:') or mi.cover.startswith('https:'):
|
if mi.cover.startswith('http:') or mi.cover.startswith('https:'):
|
||||||
mi.cover = self.download_cover(mi.cover)
|
mi.cover = self.download_cover(mi.cover)
|
||||||
ext = mi.cover.rpartition('.')[-1].lower().strip()
|
ext = mi.cover.rpartition('.')[-1].lower().strip()
|
||||||
if ext not in ('png', 'jpg', 'jpeg'):
|
if ext not in ('png', 'jpg', 'jpeg', 'gif'):
|
||||||
ext = 'jpg'
|
ext = 'jpg'
|
||||||
mi.cover_data = (ext, open(mi.cover, 'rb').read())
|
mi.cover_data = (ext, open(mi.cover, 'rb').read())
|
||||||
mi.cover = None
|
mi.cover = None
|
||||||
|
@ -44,7 +44,15 @@ def author_to_author_sort(author):
|
|||||||
def authors_to_sort_string(authors):
|
def authors_to_sort_string(authors):
|
||||||
return ' & '.join(map(author_to_author_sort, 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])
|
_ignore_starts = u'\'"'+u''.join(unichr(x) for x in range(0x2018, 0x201e)+[0x2032, 0x2033])
|
||||||
|
|
||||||
def title_sort(title):
|
def title_sort(title):
|
||||||
|
@ -114,7 +114,8 @@ 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) - \
|
CALIBRE_METADATA_FIELDS).union(
|
||||||
|
TOP_LEVEL_CLASSIFIERS) - \
|
||||||
SC_FIELDS_NOT_COPIED.union(
|
SC_FIELDS_NOT_COPIED.union(
|
||||||
SC_FIELDS_COPY_NOT_NULL)
|
SC_FIELDS_COPY_NOT_NULL)
|
||||||
|
|
||||||
|
@ -8,6 +8,7 @@ __docformat__ = 'restructuredtext en'
|
|||||||
import copy, traceback
|
import copy, traceback
|
||||||
|
|
||||||
from calibre import prints
|
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_COPYABLE_FIELDS
|
||||||
from calibre.ebooks.metadata.book import SC_FIELDS_COPY_NOT_NULL
|
from calibre.ebooks.metadata.book import SC_FIELDS_COPY_NOT_NULL
|
||||||
from calibre.ebooks.metadata.book import STANDARD_METADATA_FIELDS
|
from calibre.ebooks.metadata.book import STANDARD_METADATA_FIELDS
|
||||||
@ -50,6 +51,8 @@ class SafeFormat(TemplateFormatter):
|
|||||||
return ''
|
return ''
|
||||||
return v
|
return v
|
||||||
except:
|
except:
|
||||||
|
if DEBUG:
|
||||||
|
traceback.print_exc()
|
||||||
return key
|
return key
|
||||||
|
|
||||||
composite_formatter = SafeFormat()
|
composite_formatter = SafeFormat()
|
||||||
@ -320,8 +323,8 @@ class Metadata(object):
|
|||||||
else:
|
else:
|
||||||
self.set(dest, val)
|
self.set(dest, val)
|
||||||
except:
|
except:
|
||||||
traceback.print_exc()
|
if DEBUG:
|
||||||
pass
|
traceback.print_exc()
|
||||||
|
|
||||||
# Old Metadata API {{{
|
# Old Metadata API {{{
|
||||||
def print_all_attributes(self):
|
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)
|
base = metadata_from_filename(name, pat=pattern)
|
||||||
if force_read_metadata or is_recipe(name) or prefs['read_file_metadata']:
|
if force_read_metadata or is_recipe(name) or prefs['read_file_metadata']:
|
||||||
mi = get_file_type_metadata(stream, stream_type)
|
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
|
# 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.
|
# to match meta info from the file name did not match.
|
||||||
# The regex is meant to match the standard format filenames are written
|
# 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):
|
def check_for_periodical(self):
|
||||||
if self.oeb.metadata.publication_type and \
|
if self.oeb.metadata.publication_type and \
|
||||||
unicode(self.oeb.metadata.publication_type[0]).startswith('periodical:'):
|
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.rasterize import SVGRasterizer, Unavailable
|
||||||
from calibre.ebooks.oeb.transforms.htmltoc import HTMLTOCAdder
|
from calibre.ebooks.oeb.transforms.htmltoc import HTMLTOCAdder
|
||||||
from calibre.customize.ui import plugin_for_input_format
|
from calibre.customize.ui import plugin_for_input_format
|
||||||
self.remove_image_transparencies()
|
|
||||||
imagemax = PALM_MAX_IMAGE_SIZE if opts.rescale_images else None
|
imagemax = PALM_MAX_IMAGE_SIZE if opts.rescale_images else None
|
||||||
if not opts.no_inline_toc:
|
if not opts.no_inline_toc:
|
||||||
tocadder = HTMLTOCAdder(title=opts.toc_title)
|
tocadder = HTMLTOCAdder(title=opts.toc_title)
|
||||||
|
@ -15,7 +15,6 @@ from struct import pack
|
|||||||
import time
|
import time
|
||||||
from urlparse import urldefrag
|
from urlparse import urldefrag
|
||||||
|
|
||||||
from PIL import Image
|
|
||||||
from cStringIO import StringIO
|
from cStringIO import StringIO
|
||||||
from calibre.ebooks.mobi.langcodes import iana2mobi
|
from calibre.ebooks.mobi.langcodes import iana2mobi
|
||||||
from calibre.ebooks.mobi.mobiml import MBP_NS
|
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 prefixname
|
||||||
from calibre.ebooks.oeb.base import urlnormalize
|
from calibre.ebooks.oeb.base import urlnormalize
|
||||||
from calibre.ebooks.compression.palmdoc import compress_doc
|
from calibre.ebooks.compression.palmdoc import compress_doc
|
||||||
|
from calibre.utils.magick.draw import Image, save_cover_data_to, thumbnail
|
||||||
|
|
||||||
INDEXING = True
|
INDEXING = True
|
||||||
FCIS_FLIS = True
|
FCIS_FLIS = True
|
||||||
@ -111,46 +111,18 @@ def align_block(raw, multiple=4, pad='\0'):
|
|||||||
return raw + pad*(multiple - extra)
|
return raw + pad*(multiple - extra)
|
||||||
|
|
||||||
def rescale_image(data, maxsizeb, dimen=None):
|
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:
|
if dimen is not None:
|
||||||
image.thumbnail(dimen, Image.ANTIALIAS)
|
return thumbnail(data, width=dimen, height=dimen)[-1]
|
||||||
changed = True
|
# Replace transparent pixels with white pixels and convert to JPEG
|
||||||
if changed:
|
data = save_cover_data_to(data, 'img.jpg', return_data=True)
|
||||||
data = StringIO()
|
scale = 0.9
|
||||||
image.save(data, format)
|
while len(data) >= maxsizeb and scale >= 0.05:
|
||||||
data = data.getvalue()
|
img = Image()
|
||||||
if len(data) <= maxsizeb:
|
img.load(data)
|
||||||
return data
|
w, h = img.size
|
||||||
image = image.convert('RGBA')
|
img.size = (int(scale*w), int(scale*h))
|
||||||
for quality in xrange(95, -1, -1):
|
data = img.export('jpg')
|
||||||
data = StringIO()
|
scale -= 0.05
|
||||||
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 data
|
return data
|
||||||
|
|
||||||
class Serializer(object):
|
class Serializer(object):
|
||||||
|
@ -234,13 +234,14 @@ class AddAction(InterfaceAction):
|
|||||||
self.gui.set_books_in_library(booklists=[model.db], reset=True)
|
self.gui.set_books_in_library(booklists=[model.db], reset=True)
|
||||||
self.gui.refresh_ondevice()
|
self.gui.refresh_ondevice()
|
||||||
|
|
||||||
def add_books_from_device(self, view):
|
def add_books_from_device(self, view, paths=None):
|
||||||
rows = view.selectionModel().selectedRows()
|
if paths is None:
|
||||||
if not rows or len(rows) == 0:
|
rows = view.selectionModel().selectedRows()
|
||||||
d = error_dialog(self.gui, _('Add to library'), _('No book selected'))
|
if not rows or len(rows) == 0:
|
||||||
d.exec_()
|
d = error_dialog(self.gui, _('Add to library'), _('No book selected'))
|
||||||
return
|
d.exec_()
|
||||||
paths = [p for p in view._model.paths(rows) if p is not None]
|
return
|
||||||
|
paths = [p for p in view.model().paths(rows) if p is not None]
|
||||||
ve = self.gui.device_manager.device.VIRTUAL_BOOK_EXTENSIONS
|
ve = self.gui.device_manager.device.VIRTUAL_BOOK_EXTENSIONS
|
||||||
def ext(x):
|
def ext(x):
|
||||||
ans = os.path.splitext(x)[1]
|
ans = os.path.splitext(x)[1]
|
||||||
@ -261,7 +262,7 @@ class AddAction(InterfaceAction):
|
|||||||
return
|
return
|
||||||
from calibre.gui2.add import Adder
|
from calibre.gui2.add import Adder
|
||||||
self.__adder_func = partial(self._add_from_device_adder, on_card=None,
|
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._adder = Adder(self.gui, self.gui.library_view.model().db,
|
||||||
self.Dispatcher(self.__adder_func), spare_server=self.gui.spare_server)
|
self.Dispatcher(self.__adder_func), spare_server=self.gui.spare_server)
|
||||||
self._adder.add(paths)
|
self._adder.add(paths)
|
||||||
|
@ -181,5 +181,6 @@ class ConvertAction(InterfaceAction):
|
|||||||
self.gui.tags_view.recount()
|
self.gui.tags_view.recount()
|
||||||
if self.gui.current_view() is self.gui.library_view:
|
if self.gui.current_view() is self.gui.library_view:
|
||||||
current = self.gui.library_view.currentIndex()
|
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):
|
def create_opf_file(db, book_id):
|
||||||
mi = db.get_metadata(book_id, index_is_id=True)
|
mi = db.get_metadata(book_id, index_is_id=True)
|
||||||
mi.application_id = uuid.uuid4()
|
mi.application_id = uuid.uuid4()
|
||||||
|
old_cover = mi.cover
|
||||||
|
mi.cover = None
|
||||||
raw = metadata_to_opf(mi)
|
raw = metadata_to_opf(mi)
|
||||||
|
mi.cover = old_cover
|
||||||
opf_file = PersistentTemporaryFile('.opf')
|
opf_file = PersistentTemporaryFile('.opf')
|
||||||
opf_file.write(raw)
|
opf_file.write(raw)
|
||||||
opf_file.close()
|
opf_file.close()
|
||||||
|
@ -310,7 +310,13 @@ class DeviceManager(Thread): # {{{
|
|||||||
self.device.sync_booklists(booklists, end_session=False)
|
self.device.sync_booklists(booklists, end_session=False)
|
||||||
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):
|
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],
|
return self.create_job(self._sync_booklists, done, args=[booklists],
|
||||||
description=_('Send metadata to device'))
|
description=_('Send metadata to device'))
|
||||||
|
|
||||||
@ -319,28 +325,31 @@ 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 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()
|
||||||
dev_name = self.connected_device.__class__.__name__
|
cpb = self.find_plugboard(ext, plugboards)
|
||||||
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)
|
|
||||||
if ext:
|
if ext:
|
||||||
try:
|
try:
|
||||||
if DEBUG:
|
if DEBUG:
|
||||||
@ -1247,8 +1256,9 @@ class DeviceMixin(object): # {{{
|
|||||||
'''
|
'''
|
||||||
Upload metadata to device.
|
Upload metadata to device.
|
||||||
'''
|
'''
|
||||||
|
plugboards = self.library_view.model().db.prefs.get('plugboards', {})
|
||||||
self.device_manager.sync_booklists(Dispatcher(self.metadata_synced),
|
self.device_manager.sync_booklists(Dispatcher(self.metadata_synced),
|
||||||
self.booklists())
|
self.booklists(), plugboards)
|
||||||
|
|
||||||
def metadata_synced(self, job):
|
def metadata_synced(self, job):
|
||||||
'''
|
'''
|
||||||
@ -1502,8 +1512,10 @@ class DeviceMixin(object): # {{{
|
|||||||
|
|
||||||
if update_metadata:
|
if update_metadata:
|
||||||
if self.device_manager.is_device_connected:
|
if self.device_manager.is_device_connected:
|
||||||
|
plugboards = self.library_view.model().db.prefs.get('plugboards', {})
|
||||||
self.device_manager.sync_booklists(
|
self.device_manager.sync_booklists(
|
||||||
Dispatcher(self.metadata_synced), booklists)
|
Dispatcher(self.metadata_synced), booklists,
|
||||||
|
plugboards)
|
||||||
return update_metadata
|
return update_metadata
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
|
@ -56,6 +56,7 @@ class LocationManager(QObject): # {{{
|
|||||||
self._mem.append(a)
|
self._mem.append(a)
|
||||||
else:
|
else:
|
||||||
ac.setToolTip(tooltip)
|
ac.setToolTip(tooltip)
|
||||||
|
ac.calibre_name = name
|
||||||
|
|
||||||
return ac
|
return ac
|
||||||
|
|
||||||
@ -112,7 +113,6 @@ class LocationManager(QObject): # {{{
|
|||||||
ac.setWhatsThis(t)
|
ac.setWhatsThis(t)
|
||||||
ac.setStatusTip(t)
|
ac.setStatusTip(t)
|
||||||
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def has_device(self):
|
def has_device(self):
|
||||||
return max(self.free) > -1
|
return max(self.free) > -1
|
||||||
@ -228,6 +228,7 @@ class ToolBar(QToolBar): # {{{
|
|||||||
self.added_actions = []
|
self.added_actions = []
|
||||||
self.build_bar()
|
self.build_bar()
|
||||||
self.preferred_width = self.sizeHint().width()
|
self.preferred_width = self.sizeHint().width()
|
||||||
|
self.setAcceptDrops(True)
|
||||||
|
|
||||||
def apply_settings(self):
|
def apply_settings(self):
|
||||||
sz = gprefs['toolbar_icon_size']
|
sz = gprefs['toolbar_icon_size']
|
||||||
@ -317,6 +318,59 @@ class ToolBar(QToolBar): # {{{
|
|||||||
def database_changed(self, db):
|
def database_changed(self, db):
|
||||||
pass
|
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): # {{{
|
class MainWindowMixin(object): # {{{
|
||||||
|
@ -153,7 +153,7 @@ class TextDelegate(QStyledItemDelegate): # {{{
|
|||||||
complete_items = [i[1] for i in self.auto_complete_function()]
|
complete_items = [i[1] for i in self.auto_complete_function()]
|
||||||
completer = QCompleter(complete_items, self)
|
completer = QCompleter(complete_items, self)
|
||||||
completer.setCaseSensitivity(Qt.CaseInsensitive)
|
completer.setCaseSensitivity(Qt.CaseInsensitive)
|
||||||
completer.setCompletionMode(QCompleter.InlineCompletion)
|
completer.setCompletionMode(QCompleter.PopupCompletion)
|
||||||
editor.setCompleter(completer)
|
editor.setCompleter(completer)
|
||||||
return editor
|
return editor
|
||||||
#}}}
|
#}}}
|
||||||
|
@ -361,13 +361,14 @@ class BooksModel(QAbstractTableModel): # {{{
|
|||||||
self.cover_cache.set_cache(ids)
|
self.cover_cache.set_cache(ids)
|
||||||
|
|
||||||
def current_changed(self, current, previous, emit_signal=True):
|
def current_changed(self, current, previous, emit_signal=True):
|
||||||
idx = current.row()
|
if current.isValid():
|
||||||
self.set_cache(idx)
|
idx = current.row()
|
||||||
data = self.get_book_display_info(idx)
|
self.set_cache(idx)
|
||||||
if emit_signal:
|
data = self.get_book_display_info(idx)
|
||||||
self.new_bookdisplay_data.emit(data)
|
if emit_signal:
|
||||||
else:
|
self.new_bookdisplay_data.emit(data)
|
||||||
return data
|
else:
|
||||||
|
return data
|
||||||
|
|
||||||
def get_book_info(self, index):
|
def get_book_info(self, index):
|
||||||
if isinstance(index, int):
|
if isinstance(index, int):
|
||||||
@ -1081,12 +1082,11 @@ class DeviceBooksModel(BooksModel): # {{{
|
|||||||
self.db = db
|
self.db = db
|
||||||
self.map = list(range(0, len(db)))
|
self.map = list(range(0, len(db)))
|
||||||
|
|
||||||
def current_changed(self, current, previous):
|
def cover(self, row):
|
||||||
data = {}
|
item = self.db[self.map[row]]
|
||||||
item = self.db[self.map[current.row()]]
|
|
||||||
cdata = item.thumbnail
|
cdata = item.thumbnail
|
||||||
|
img = QImage()
|
||||||
if cdata is not None:
|
if cdata is not None:
|
||||||
img = QImage()
|
|
||||||
if hasattr(cdata, 'image_path'):
|
if hasattr(cdata, 'image_path'):
|
||||||
img.load(cdata.image_path)
|
img.load(cdata.image_path)
|
||||||
elif cdata:
|
elif cdata:
|
||||||
@ -1094,9 +1094,16 @@ class DeviceBooksModel(BooksModel): # {{{
|
|||||||
img.loadFromData(cdata[-1])
|
img.loadFromData(cdata[-1])
|
||||||
else:
|
else:
|
||||||
img.loadFromData(cdata)
|
img.loadFromData(cdata)
|
||||||
if img.isNull():
|
if img.isNull():
|
||||||
img = self.default_image
|
img = self.default_image
|
||||||
data['cover'] = img
|
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')
|
type = _('Unknown')
|
||||||
ext = os.path.splitext(item.path)[1]
|
ext = os.path.splitext(item.path)[1]
|
||||||
if ext:
|
if ext:
|
||||||
|
@ -9,7 +9,8 @@ import os
|
|||||||
from functools import partial
|
from functools import partial
|
||||||
|
|
||||||
from PyQt4.Qt import QTableView, Qt, QAbstractItemView, QMenu, pyqtSignal, \
|
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, \
|
from calibre.gui2.library.delegates import RatingDelegate, PubDateDelegate, \
|
||||||
TextDelegate, DateDelegate, TagsDelegate, CcTextDelegate, \
|
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.utils.config import tweaks, prefs
|
||||||
from calibre.gui2 import error_dialog, gprefs
|
from calibre.gui2 import error_dialog, gprefs
|
||||||
from calibre.gui2.library import DEFAULT_SORT
|
from calibre.gui2.library import DEFAULT_SORT
|
||||||
|
from calibre.constants import filesystem_encoding
|
||||||
|
from calibre import force_unicode
|
||||||
|
|
||||||
class BooksView(QTableView): # {{{
|
class BooksView(QTableView): # {{{
|
||||||
|
|
||||||
@ -31,6 +33,7 @@ class BooksView(QTableView): # {{{
|
|||||||
self.setDragEnabled(True)
|
self.setDragEnabled(True)
|
||||||
self.setDragDropOverwriteMode(False)
|
self.setDragDropOverwriteMode(False)
|
||||||
self.setDragDropMode(self.DragDrop)
|
self.setDragDropMode(self.DragDrop)
|
||||||
|
self.drag_start_pos = None
|
||||||
self.setAlternatingRowColors(True)
|
self.setAlternatingRowColors(True)
|
||||||
self.setSelectionBehavior(self.SelectRows)
|
self.setSelectionBehavior(self.SelectRows)
|
||||||
self.setShowGrid(False)
|
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
|
Accept a drop event and return a list of paths that can be read from
|
||||||
and represent files with extensions.
|
and represent files with extensions.
|
||||||
'''
|
'''
|
||||||
if event.mimeData().hasFormat('text/uri-list'):
|
md = event.mimeData()
|
||||||
urls = [unicode(u.toLocalFile()) for u in event.mimeData().urls()]
|
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)]
|
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):
|
def dragEnterEvent(self, event):
|
||||||
if int(event.possibleActions() & Qt.CopyAction) + \
|
if int(event.possibleActions() & Qt.CopyAction) + \
|
||||||
int(event.possibleActions() & Qt.MoveAction) == 0:
|
int(event.possibleActions() & Qt.MoveAction) == 0:
|
||||||
@ -547,6 +632,21 @@ class DeviceBooksView(BooksView): # {{{
|
|||||||
self.setDragDropMode(self.NoDragDrop)
|
self.setDragDropMode(self.NoDragDrop)
|
||||||
self.setAcceptDrops(False)
|
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):
|
def contextMenuEvent(self, event):
|
||||||
edit_collections = callable(getattr(self._model.db, 'supports_collections', None)) and \
|
edit_collections = callable(getattr(self._model.db, 'supports_collections', None)) and \
|
||||||
self._model.db.supports_collections() and \
|
self._model.db.supports_collections() and \
|
||||||
|
@ -39,6 +39,12 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
|
|||||||
|
|
||||||
ConfigWidgetBase.initialize(self)
|
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 = ['']
|
self.devices = ['']
|
||||||
for device in device_plugins():
|
for device in device_plugins():
|
||||||
n = device.__class__.__name__
|
n = device.__class__.__name__
|
||||||
@ -54,6 +60,7 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
|
|||||||
for w in metadata_writers():
|
for w in metadata_writers():
|
||||||
for f in w.file_types:
|
for f in w.file_types:
|
||||||
self.formats.append(f)
|
self.formats.append(f)
|
||||||
|
self.formats.append('device_db')
|
||||||
self.formats.sort()
|
self.formats.sort()
|
||||||
self.formats.insert(1, plugboard_any_format_value)
|
self.formats.insert(1, plugboard_any_format_value)
|
||||||
self.new_format.addItems(self.formats)
|
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>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</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">
|
<layout class="QGridLayout" name="gridLayout_2">
|
||||||
<item row="0" column="1">
|
<item row="0" column="1">
|
||||||
<widget class="QLabel" name="label_6">
|
<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>
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</item>
|
</item>
|
||||||
<item row="2" column="1">
|
<item row="4" column="1">
|
||||||
<layout class="QGridLayout" name="fields_layout">
|
<layout class="QGridLayout" name="fields_layout">
|
||||||
<item row="0" column="0">
|
<item row="0" column="0">
|
||||||
<widget class="QLabel" name="label_2">
|
<widget class="QLabel" name="label_2">
|
||||||
|
@ -79,6 +79,8 @@ class TagsView(QTreeView): # {{{
|
|||||||
self.setHeaderHidden(True)
|
self.setHeaderHidden(True)
|
||||||
self.setItemDelegate(TagDelegate(self))
|
self.setItemDelegate(TagDelegate(self))
|
||||||
self.made_connections = False
|
self.made_connections = False
|
||||||
|
self.setAcceptDrops(True)
|
||||||
|
self.setDropIndicatorShown(True)
|
||||||
|
|
||||||
def set_database(self, db, tag_match, sort_by):
|
def set_database(self, db, tag_match, sort_by):
|
||||||
self.hidden_categories = config['tag_browser_hidden_categories']
|
self.hidden_categories = config['tag_browser_hidden_categories']
|
||||||
@ -104,6 +106,49 @@ class TagsView(QTreeView): # {{{
|
|||||||
def database_changed(self, event, ids):
|
def database_changed(self, event, ids):
|
||||||
self.refresh_required.emit()
|
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
|
@property
|
||||||
def match_all(self):
|
def match_all(self):
|
||||||
return self.tag_match and self.tag_match.currentIndex() > 0
|
return self.tag_match and self.tag_match.currentIndex() > 0
|
||||||
@ -326,6 +371,8 @@ class TagTreeItem(object): # {{{
|
|||||||
self.children.append(child)
|
self.children.append(child)
|
||||||
|
|
||||||
def data(self, role):
|
def data(self, role):
|
||||||
|
if role == Qt.UserRole:
|
||||||
|
return self
|
||||||
if self.type == self.TAG:
|
if self.type == self.TAG:
|
||||||
return self.tag_data(role)
|
return self.tag_data(role)
|
||||||
if self.type == self.CATEGORY:
|
if self.type == self.CATEGORY:
|
||||||
@ -544,8 +591,14 @@ class TagsModel(QAbstractItemModel): # {{{
|
|||||||
def headerData(self, *args):
|
def headerData(self, *args):
|
||||||
return NONE
|
return NONE
|
||||||
|
|
||||||
def flags(self, *args):
|
def flags(self, index, *args):
|
||||||
return Qt.ItemIsEnabled|Qt.ItemIsSelectable|Qt.ItemIsEditable
|
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):
|
def path_for_index(self, index):
|
||||||
ans = []
|
ans = []
|
||||||
|
@ -1593,7 +1593,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
|||||||
self.conn.commit()
|
self.conn.commit()
|
||||||
|
|
||||||
def delete_publisher_using_id(self, old_id):
|
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
|
self.conn.execute('''DELETE FROM books_publishers_link
|
||||||
WHERE publisher=?''', (old_id,))
|
WHERE publisher=?''', (old_id,))
|
||||||
self.conn.execute('DELETE FROM publishers WHERE id=?', (old_id,))
|
self.conn.execute('DELETE FROM publishers WHERE id=?', (old_id,))
|
||||||
|
@ -8,6 +8,7 @@ __docformat__ = 'restructuredtext en'
|
|||||||
|
|
||||||
import os, traceback, cStringIO, re
|
import os, traceback, cStringIO, re
|
||||||
|
|
||||||
|
from calibre.constants import DEBUG
|
||||||
from calibre.utils.config import Config, StringConfig, tweaks
|
from calibre.utils.config import Config, StringConfig, tweaks
|
||||||
from calibre.utils.formatter import TemplateFormatter
|
from calibre.utils.formatter import TemplateFormatter
|
||||||
from calibre.utils.filenames import shorten_components_to, supports_long_names, \
|
from calibre.utils.filenames import shorten_components_to, supports_long_names, \
|
||||||
@ -118,8 +119,8 @@ class SafeFormat(TemplateFormatter):
|
|||||||
try:
|
try:
|
||||||
b = self.book.get_user_metadata(key, False)
|
b = self.book.get_user_metadata(key, False)
|
||||||
except:
|
except:
|
||||||
prints('save_to_disk get value exception')
|
if DEBUG:
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
b = None
|
b = None
|
||||||
|
|
||||||
if b is not None and b['datatype'] == 'composite':
|
if b is not None and b['datatype'] == 'composite':
|
||||||
@ -129,13 +130,13 @@ class SafeFormat(TemplateFormatter):
|
|||||||
self.composite_values[key] = \
|
self.composite_values[key] = \
|
||||||
self.vformat(b['display']['composite_template'], [], kwargs)
|
self.vformat(b['display']['composite_template'], [], kwargs)
|
||||||
return self.composite_values[key]
|
return self.composite_values[key]
|
||||||
if kwargs[key]:
|
if key in kwargs:
|
||||||
return self.sanitize(kwargs[key])
|
return kwargs[key].replace('/', '_').replace('\\', '_')
|
||||||
return ''
|
return ''
|
||||||
except:
|
except:
|
||||||
print('save_to_disk general exception')
|
if DEBUG:
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
return ''
|
return key
|
||||||
|
|
||||||
safe_formatter = SafeFormat()
|
safe_formatter = SafeFormat()
|
||||||
|
|
||||||
@ -182,8 +183,8 @@ def get_components(template, mi, id, timefmt='%b %Y', length=250,
|
|||||||
elif custom_metadata[key]['datatype'] == 'bool':
|
elif custom_metadata[key]['datatype'] == 'bool':
|
||||||
format_args[key] = _('yes') if format_args[key] else _('no')
|
format_args[key] = _('yes') if format_args[key] else _('no')
|
||||||
|
|
||||||
components = safe_formatter.safe_format(template, format_args, '', mi,
|
components = safe_formatter.safe_format(template, format_args,
|
||||||
sanitize=sanitize_func)
|
'G_C-EXCEPTION!', mi)
|
||||||
components = [x.strip() for x in components.split('/') if x.strip()]
|
components = [x.strip() for x in components.split('/') if x.strip()]
|
||||||
components = [sanitize_func(x) for x in components if x]
|
components = [sanitize_func(x) for x in components if x]
|
||||||
if not components:
|
if not components:
|
||||||
@ -267,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
|
||||||
#prints('Using plugboard:', fmt, cpb)
|
if DEBUG:
|
||||||
|
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:
|
||||||
continue
|
continue
|
||||||
@ -285,7 +287,8 @@ def save_book_to_disk(id, db, root, opts, length):
|
|||||||
newmi = mi
|
newmi = mi
|
||||||
set_metadata(stream, newmi, fmt)
|
set_metadata(stream, newmi, fmt)
|
||||||
except:
|
except:
|
||||||
traceback.print_exc()
|
if DEBUG:
|
||||||
|
traceback.print_exc()
|
||||||
stream.seek(0)
|
stream.seek(0)
|
||||||
data = stream.read()
|
data = stream.read()
|
||||||
fmt_path = base_path+'.'+str(fmt)
|
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.
|
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
|
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
|
@author: charles
|
||||||
'''
|
'''
|
||||||
|
|
||||||
import re, string
|
import re, string, traceback
|
||||||
|
|
||||||
|
from calibre.constants import DEBUG
|
||||||
|
|
||||||
class TemplateFormatter(string.Formatter):
|
class TemplateFormatter(string.Formatter):
|
||||||
'''
|
'''
|
||||||
@ -19,7 +21,6 @@ class TemplateFormatter(string.Formatter):
|
|||||||
string.Formatter.__init__(self)
|
string.Formatter.__init__(self)
|
||||||
self.book = None
|
self.book = None
|
||||||
self.kwargs = None
|
self.kwargs = None
|
||||||
self.sanitize = None
|
|
||||||
|
|
||||||
def _lookup(self, val, field_if_set, field_not_set):
|
def _lookup(self, val, field_if_set, field_not_set):
|
||||||
if val:
|
if val:
|
||||||
@ -99,8 +100,8 @@ class TemplateFormatter(string.Formatter):
|
|||||||
return fmt, '', ''
|
return fmt, '', ''
|
||||||
return matches.groups()
|
return matches.groups()
|
||||||
except:
|
except:
|
||||||
import traceback
|
if DEBUG:
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
return fmt, '', ''
|
return fmt, '', ''
|
||||||
|
|
||||||
def format_field(self, val, fmt):
|
def format_field(self, val, fmt):
|
||||||
@ -139,14 +140,15 @@ class TemplateFormatter(string.Formatter):
|
|||||||
ans = string.Formatter.vformat(self, fmt, args, kwargs)
|
ans = string.Formatter.vformat(self, fmt, args, kwargs)
|
||||||
return self.compress_spaces.sub(' ', ans).strip()
|
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.kwargs = kwargs
|
||||||
self.book = book
|
self.book = book
|
||||||
self.sanitize = sanitize
|
|
||||||
self.composite_values = {}
|
self.composite_values = {}
|
||||||
try:
|
try:
|
||||||
ans = self.vformat(fmt, [], kwargs).strip()
|
ans = self.vformat(fmt, [], kwargs).strip()
|
||||||
except:
|
except:
|
||||||
|
if DEBUG:
|
||||||
|
traceback.print_exc()
|
||||||
ans = error_value
|
ans = error_value
|
||||||
return ans
|
return ans
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user