0.7.22 update

This commit is contained in:
GRiker 2010-10-03 16:11:32 -07:00
commit b57222a01c
58 changed files with 45676 additions and 31403 deletions

View File

@ -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

View File

@ -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

View File

@ -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 = [

View File

@ -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

View File

@ -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 \

View File

@ -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'

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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):

View File

@ -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)

View File

@ -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):

View File

@ -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

View File

@ -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)

View File

@ -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):

View File

@ -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)

View File

@ -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())

View File

@ -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()

View File

@ -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
# }}}

View File

@ -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): # {{{

View File

@ -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
#}}}

View File

@ -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:

View File

@ -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 \

View File

@ -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)

View File

@ -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">

View File

@ -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 = []

View File

@ -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,))

View File

@ -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)

View File

@ -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

View File

@ -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