Merge from trunk

This commit is contained in:
Charles Haley 2011-04-18 11:07:32 +01:00
commit a13b6065c6
32 changed files with 827 additions and 458 deletions

View File

@ -19,6 +19,45 @@
# new recipes:
# - title:
- version: 0.7.56
date: 2011-04-17
new features:
- title: "This is primarily a bug fix release that fixes a bug in 0.7.55 that caused calibre to rescan the files on the device every time the device is connected. If you updated to 0.7.55 it is highly recommended you update to 0.7.56"
- title: "Device driver for Coby Kyros"
- title: "Remove the quick access to search options from next to the search bar, as we now have a separate search highlights toggle button"
- title: "MOBI Output: Ensure that MOBI files always have 8KB worth of null bytes at the end of record 0. This appears to be necessary for Amazon to be able to add DRM to calibre generated MOBI files sent to their publishing service."
- title: "Add a tool to inspect MOBI files. To use: calibre-debug -m file.mobi"
bug fixes:
- title: "Fixed regression taht caused calibre to rescan files on the device on every reconnect"
- title: "Fix donate button causing the toolbar to be too large on OS X"
- title: "MOBI Input: Fix detection of Table of Contents for MOBI files that have a page break between the location designated as the Table of Contents and the actual table of contents."
tickets: [763504]
- title: "Comic Input: Fix handling of some CBZ files that have wrongly encoded non ASCII filenames on windows."
tickets: [763280]
- title: "PML Input: Fix multi-line chapter title causing a spurious page break"
tickets: [763238]
- title: "EPUB Input: Speed up processing of files with very large manifest/spines"
- title: "Fix regression that broke cover:False searches in 0.7.55"
improved recipes:
- Suedduetsche Zeitung
- Irish Times
- Big Oven
- NSPM
- version: 0.7.55
date: 2011-04-15

View File

@ -2,7 +2,7 @@ __license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
__docformat__ = 'restructuredtext en'
__appname__ = 'calibre'
__version__ = '0.7.55'
__version__ = '0.7.56'
__author__ = "Kovid Goyal <kovid@kovidgoyal.net>"
import re, importlib

View File

@ -201,8 +201,9 @@ class ITUNES(DriverBase):
# 0x1294 iPhone 3GS
# 0x1297 iPhone 4
# 0x129a iPad
# 0x12a2 iPad2
VENDOR_ID = [0x05ac]
PRODUCT_ID = [0x1292,0x1293,0x1294,0x1297,0x1299,0x129a]
PRODUCT_ID = [0x1292,0x1293,0x1294,0x1297,0x1299,0x129a,0x12a2]
BCD = [0x01]
# Plugboard ID
@ -421,7 +422,7 @@ class ITUNES(DriverBase):
cached_books[this_book.path] = {
'title':book.name(),
'author':[book.artist()],
'author':book.artist().split(' & '),
'lib_book':library_books[this_book.path] if this_book.path in library_books else None,
'dev_book':book,
'uuid': book.composer()
@ -459,7 +460,7 @@ class ITUNES(DriverBase):
cached_books[this_book.path] = {
'title':book.Name,
'author':book.Artist,
'author':book.artist().split(' & '),
'lib_book':library_books[this_book.path] if this_book.path in library_books else None,
'uuid': book.Composer,
'format': 'pdf' if book.KindAsString.startswith('PDF') else 'epub'
@ -1021,7 +1022,9 @@ class ITUNES(DriverBase):
if isosx:
for (i,file) in enumerate(files):
format = file.rpartition('.')[2].lower()
path = self.path_template % (metadata[i].title, metadata[i].author[0],format)
path = self.path_template % (metadata[i].title,
authors_to_string(metadata[i].authors),
format)
self._remove_existing_copy(path, metadata[i])
fpath = self._get_fpath(file, metadata[i], format, update_md=True)
db_added, lb_added = self._add_new_copy(fpath, metadata[i])
@ -1034,9 +1037,11 @@ class ITUNES(DriverBase):
if DEBUG:
self.log.info("ITUNES.upload_books()")
self.log.info(" adding '%s' by '%s' uuid:%s to self.cached_books" %
( metadata[i].title, metadata[i].author, metadata[i].uuid))
(metadata[i].title,
authors_to_string(metadata[i].authors),
metadata[i].uuid))
self.cached_books[this_book.path] = {
'author': metadata[i].author,
'author': authors_to_string(metadata[i].authors),
'dev_book': db_added,
'format': format,
'lib_book': lb_added,
@ -1055,7 +1060,9 @@ class ITUNES(DriverBase):
for (i,file) in enumerate(files):
format = file.rpartition('.')[2].lower()
path = self.path_template % (metadata[i].title, metadata[i].author[0],format)
path = self.path_template % (metadata[i].title,
authors_to_string(metadata[i].authors),
format)
self._remove_existing_copy(path, metadata[i])
fpath = self._get_fpath(file, metadata[i],format, update_md=True)
db_added, lb_added = self._add_new_copy(fpath, metadata[i])
@ -1075,9 +1082,11 @@ class ITUNES(DriverBase):
if DEBUG:
self.log.info("ITUNES.upload_books()")
self.log.info(" adding '%s' by '%s' uuid:%s to self.cached_books" %
( metadata[i].title, metadata[i].author, metadata[i].uuid))
(metadata[i].title,
authors_to_string(metadata[i].authors),
metadata[i].uuid))
self.cached_books[this_book.path] = {
'author': metadata[i].author[0],
'author': authors_to_string(metadata[i].authors),
'dev_book': db_added,
'format': format,
'lib_book': lb_added,
@ -1190,7 +1199,7 @@ class ITUNES(DriverBase):
base_fn = base_fn.rpartition('.')[0]
db_added = self._find_device_book(
{ 'title': base_fn if format == 'pdf' else metadata.title,
'author': metadata.authors[0],
'author': authors_to_string(metadata.authors),
'uuid': metadata.uuid,
'format': format})
return db_added
@ -1255,7 +1264,7 @@ class ITUNES(DriverBase):
base_fn = base_fn.rpartition('.')[0]
added = self._find_library_book(
{ 'title': base_fn if format == 'pdf' else metadata.title,
'author': metadata.author[0],
'author': authors_to_string(metadata.authors),
'uuid': metadata.uuid,
'format': format})
return added
@ -1314,7 +1323,7 @@ class ITUNES(DriverBase):
with open(metadata.cover,'r+b') as cd:
cover_data = cd.read()
except:
self.problem_titles.append("'%s' by %s" % (metadata.title, metadata.author[0]))
self.problem_titles.append("'%s' by %s" % (metadata.title, authors_to_string(metadata.authors)))
self.log.error(" error scaling '%s' for '%s'" % (metadata.cover,metadata.title))
import traceback
@ -1389,7 +1398,7 @@ class ITUNES(DriverBase):
thumb_path = path.rpartition('.')[0] + '.jpg'
zfw.writestr(thumb_path, thumb)
except:
self.problem_titles.append("'%s' by %s" % (metadata.title, metadata.author[0]))
self.problem_titles.append("'%s' by %s" % (metadata.title, authors_to_string(metadata.authors)))
self.log.error(" error converting '%s' to thumb for '%s'" % (metadata.cover,metadata.title))
finally:
try:
@ -1407,7 +1416,7 @@ class ITUNES(DriverBase):
if DEBUG:
self.log.info(" ITUNES._create_new_book()")
this_book = Book(metadata.title, authors_to_string(metadata.author))
this_book = Book(metadata.title, authors_to_string(metadata.authors))
this_book.datetime = time.gmtime()
this_book.db_id = None
this_book.device_collections = []
@ -2451,7 +2460,7 @@ class ITUNES(DriverBase):
for book in self.cached_books:
if self.cached_books[book]['uuid'] == metadata.uuid or \
(self.cached_books[book]['title'] == metadata.title and \
self.cached_books[book]['author'] == metadata.authors[0]):
self.cached_books[book]['author'] == authors_to_string(metadata.authors)):
self.update_list.append(self.cached_books[book])
self._remove_from_device(self.cached_books[book])
if DEBUG:
@ -2470,7 +2479,7 @@ class ITUNES(DriverBase):
for book in self.cached_books:
if self.cached_books[book]['uuid'] == metadata.uuid or \
(self.cached_books[book]['title'] == metadata.title and \
self.cached_books[book]['author'] == metadata.authors[0]):
self.cached_books[book]['author'] == authors_to_string(metadata.authors)):
self.update_list.append(self.cached_books[book])
self._remove_from_iTunes(self.cached_books[book])
if DEBUG:
@ -2939,13 +2948,13 @@ class ITUNES(DriverBase):
def _xform_metadata_via_plugboard(self, book, format):
''' Transform book metadata from plugboard templates '''
if DEBUG:
self.log.info(" ITUNES._xform_metadata_via_plugboard()")
self.log.info(" ITUNES._xform_metadata_via_plugboard()")
if self.plugboard_func:
pb = self.plugboard_func(self.DEVICE_PLUGBOARD_NAME, format, self.plugboards)
newmi = book.deepcopy_metadata()
newmi.template_to_attribute(book, pb)
if DEBUG:
if pb is not None and DEBUG:
self.log.info(" transforming %s using %s:" % (format, pb))
self.log.info(" title: %s %s" % (book.title, ">>> %s" %
newmi.title if book.title != newmi.title else ''))
@ -3062,7 +3071,7 @@ class ITUNES_ASYNC(ITUNES):
cached_books[this_book.path] = {
'title':library_books[book].name(),
'author':[library_books[book].artist()],
'author':library_books[book].artist().split(' & '),
'lib_book':library_books[book],
'dev_book':None,
'uuid': library_books[book].composer(),
@ -3102,7 +3111,7 @@ class ITUNES_ASYNC(ITUNES):
cached_books[this_book.path] = {
'title':library_books[book].Name,
'author':library_books[book].Artist,
'author':library_books[book].Artist.split(' & '),
'lib_book':library_books[book],
'uuid': library_books[book].Composer,
'format': format
@ -3288,7 +3297,7 @@ class Book(Metadata):
See ebooks.metadata.book.base
'''
def __init__(self,title,author):
Metadata.__init__(self, title, authors=[author])
Metadata.__init__(self, title, authors=author.split(' & '))
@property
def title_sorter(self):

View File

@ -52,6 +52,9 @@ class CHMInput(InputFormatPlugin):
metadata = get_metadata_from_reader(self._chm_reader)
self._chm_reader.CloseCHM()
#print tdir
#from calibre import ipython
#ipython()
odi = options.debug_pipeline
options.debug_pipeline = None

View File

@ -147,7 +147,8 @@ class CHMReader(CHMFile):
if self.hhc_path == '.hhc' and self.hhc_path not in files:
from calibre import walk
for x in walk(output_dir):
if os.path.basename(x).lower() in ('index.htm', 'index.html'):
if os.path.basename(x).lower() in ('index.htm', 'index.html',
'contents.htm', 'contents.html'):
self.hhc_path = os.path.relpath(x, output_dir)
break

View File

@ -12,6 +12,7 @@ from Queue import Empty
from calibre.customize.conversion import InputFormatPlugin, OptionRecommendation
from calibre import extract, CurrentDir, prints
from calibre.constants import filesystem_encoding
from calibre.ptempfile import PersistentTemporaryDirectory
from calibre.utils.ipc.server import Server
from calibre.utils.ipc.job import ParallelJob
@ -21,6 +22,10 @@ def extract_comic(path_to_comic_file):
Un-archive the comic file.
'''
tdir = PersistentTemporaryDirectory(suffix='_comic_extract')
if not isinstance(tdir, unicode):
# Needed in case the zip file has wrongly encoded unicode file/dir
# names
tdir = tdir.decode(filesystem_encoding)
extract(path_to_comic_file, tdir)
return tdir

View File

@ -17,6 +17,7 @@
#define BUFFER 6000
#define MIN(x, y) ( ((x) < (y)) ? (x) : (y) )
#define MAX(x, y) ( ((x) > (y)) ? (x) : (y) )
typedef unsigned short int Byte;
typedef struct {
@ -53,7 +54,7 @@ cpalmdoc_decompress(PyObject *self, PyObject *args) {
// Map chars to bytes
for (j = 0; j < input_len; j++)
input[j] = (_input[j] < 0) ? _input[j]+256 : _input[j];
output = (char *)PyMem_Malloc(sizeof(char)*BUFFER);
output = (char *)PyMem_Malloc(sizeof(char)*(MAX(BUFFER, 5*input_len)));
if (output == NULL) return PyErr_NoMemory();
while (i < input_len) {

View File

@ -294,8 +294,24 @@ class Source(Plugin):
Excludes connectives and punctuation.
'''
if title:
pat = re.compile(r'''[-,:;+!@#$%^&*(){}.`~"'\s\[\]/]''')
title = pat.sub(' ', title)
title_patterns = [(re.compile(pat, re.IGNORECASE), repl) for pat, repl in
[
# Remove things like: (2010) (Omnibus) etc.
(r'(?i)[({\[](\d{4}|omnibus|anthology|hardcover|paperback|mass\s*market|edition|ed\.)[\])}]', ''),
# Remove any strings that contain the substring edition inside
# parentheses
(r'(?i)[({\[].*?(edition|ed.).*?[\]})]', ''),
# Remove commas used a separators in numbers
(r'(\d+),(\d+)', r'\1\2'),
# Remove hyphens only if they have whitespace before them
(r'(\s-)', ' '),
# Remove single quotes
(r"'", ''),
# Replace other special chars with a space
(r'''[:,;+!@#$%^&*(){}.`~"\s\[\]/]''', ' ')
]]
for pat, repl in title_patterns:
title = pat.sub(repl, title)
tokens = title.split()
for token in tokens:
token = token.strip()

View File

@ -114,8 +114,12 @@ class ISBNMerge(object):
return self.results
def merge_metadata_results(self):
' Merge results with identical title and authors '
def merge_metadata_results(self, merge_on_identifiers=False):
'''
Merge results with identical title and authors or an identical
identifier
'''
# First title/author
groups = {}
for result in self.results:
title = lower(result.title if result.title else '')
@ -135,6 +139,44 @@ class ISBNMerge(object):
result = rgroup[0]
self.results.append(result)
if merge_on_identifiers:
# Now identifiers
groups, empty = {}, []
for result in self.results:
key = set()
for typ, val in result.identifiers.iteritems():
if typ and val:
key.add((typ, val))
if key:
key = frozenset(key)
match = None
for candidate in list(groups):
if candidate.intersection(key):
# We have at least one identifier in common
match = candidate.union(key)
results = groups.pop(candidate)
results.append(result)
groups[match] = results
break
if match is None:
groups[key] = [result]
else:
empty.append(result)
if len(groups) != len(self.results):
self.results = []
for rgroup in groups.itervalues():
rel = [r.average_source_relevance for r in rgroup]
if len(rgroup) > 1:
result = self.merge(rgroup, None, do_asr=False)
result.average_source_relevance = sum(rel)/len(rel)
elif rgroup:
result = rgroup[0]
self.results.append(result)
if empty:
self.results.extend(empty)
self.results.sort(key=attrgetter('average_source_relevance'))
def merge_isbn_results(self):
@ -408,7 +450,7 @@ if __name__ == '__main__': # tests {{{
{'identifiers':{'isbn': '9780307459671'},
'title':'Invisible Gorilla', 'authors':['Christopher Chabris']},
[title_test('The Invisible Gorilla',
exact=True), authors_test(['Christopher F. Chabris', 'Daniel Simons'])]
exact=True), authors_test(['Christopher Chabris', 'Daniel Simons'])]
),

View File

@ -15,14 +15,17 @@ from calibre.customize.ui import metadata_plugins
from calibre import prints, sanitize_file_name2
from calibre.ebooks.metadata import check_isbn
from calibre.ebooks.metadata.sources.base import (create_log,
get_cached_cover_urls)
get_cached_cover_urls, msprefs)
def isbn_test(isbn):
isbn_ = check_isbn(isbn)
def test(mi):
misbn = check_isbn(mi.isbn)
return misbn and misbn == isbn_
if misbn and misbn == isbn_:
return True
prints('ISBN test failed. Expected: \'%s\' found \'%s\''%(isbn_, misbn))
return False
return test
@ -32,8 +35,11 @@ def title_test(title, exact=False):
def test(mi):
mt = mi.title.lower()
return (exact and mt == title) or \
(not exact and title in mt)
if (exact and mt == title) or \
(not exact and title in mt):
return True
prints('Title test failed. Expected: \'%s\' found \'%s\''%(title, mt))
return False
return test
@ -42,7 +48,22 @@ def authors_test(authors):
def test(mi):
au = set([x.lower() for x in mi.authors])
return au == authors
if msprefs['swap_author_names']:
def revert_to_fn_ln(a):
if ',' not in a:
return a
parts = a.split(',', 1)
t = parts[-1]
parts = parts[:-1]
parts.insert(0, t)
return ' '.join(parts)
au = set([revert_to_fn_ln(x) for x in au])
if au == authors:
return True
prints('Author test failed. Expected: \'%s\' found \'%s\''%(authors, au))
return False
return test

View File

@ -716,6 +716,7 @@ class MobiReader(object):
ent_pat = re.compile(r'&(\S+?);')
if elems:
tocobj = TOC()
found = False
reached = False
for x in root.iter():
if x == elems[-1]:
@ -732,7 +733,8 @@ class MobiReader(object):
text = ent_pat.sub(entity_to_unicode, text)
tocobj.add_item(toc.partition('#')[0], href[1:],
text)
if reached and x.get('class', None) == 'mbp_pagebreak':
found = True
if reached and found and x.get('class', None) == 'mbp_pagebreak':
break
if tocobj is not None:
opf.set_toc(tocobj)

View File

@ -24,7 +24,7 @@ from calibre.translations.dynamic import translate
from calibre.ebooks.chardet import xml_to_unicode
from calibre.ebooks.oeb.entitydefs import ENTITYDEFS
from calibre.ebooks.conversion.preprocess import CSSPreProcessor
from calibre import isbytestring
from calibre import isbytestring, as_unicode
RECOVER_PARSER = etree.XMLParser(recover=True, no_network=True)
@ -643,7 +643,7 @@ class Metadata(object):
return unicode(self.value).encode('ascii', 'xmlcharrefreplace')
def __unicode__(self):
return unicode(self.value)
return as_unicode(self.value)
def to_opf1(self, dcmeta=None, xmeta=None, nsrmap={}):
attrib = {}

View File

@ -648,6 +648,18 @@ def open_url(qurl):
if isfrozen and islinux and paths:
os.environ['LD_LIBRARY_PATH'] = os.pathsep.join(paths)
def get_current_db():
'''
This method will try to return the current database in use by the user as
efficiently as possible, i.e. without constructing duplicate
LibraryDatabase objects.
'''
from calibre.gui2.ui import get_gui
gui = get_gui()
if gui is not None and gui.current_db is not None:
return gui.current_db
from calibre.library import db
return db()
def open_local_file(path):
if iswindows:

View File

@ -17,7 +17,7 @@ from calibre.gui2.actions import InterfaceAction
class GenerateCatalogAction(InterfaceAction):
name = 'Generate Catalog'
action_spec = (_('Create a catalog of the books in your calibre library'), None, None, None)
action_spec = (_('Create a catalog of the books in your calibre library'), 'catalog.png', 'Catalog builder', None)
dont_add_to = frozenset(['menubar-device', 'toolbar-device', 'context-menu-device'])
def generate_catalog(self):

View File

@ -8,20 +8,20 @@ __docformat__ = 'restructuredtext en'
from functools import partial
from PyQt4.Qt import Qt, QMenu, QToolButton, QDialog, QVBoxLayout
from PyQt4.Qt import QMenu
from calibre.gui2.actions import InterfaceAction
class StoreAction(InterfaceAction):
name = 'Store'
action_spec = (_('Store'), 'store.png', None, None)
action_spec = (_('Get books'), 'store.png', None, None)
def genesis(self):
self.qaction.triggered.connect(self.search)
self.store_menu = QMenu()
self.load_menu()
def load_menu(self):
self.store_menu.clear()
self.store_menu.addAction(_('Search'), self.search)
@ -29,11 +29,11 @@ class StoreAction(InterfaceAction):
for n, p in self.gui.istores.items():
self.store_menu.addAction(n, partial(self.open_store, p))
self.qaction.setMenu(self.store_menu)
def search(self):
from calibre.gui2.store.search import SearchDialog
sd = SearchDialog(self.gui.istores, self.gui)
sd.exec_()
def open_store(self, store_plugin):
store_plugin.open(self.gui)

View File

@ -483,8 +483,15 @@ class BookDetails(QWidget): # {{{
self.book_info.show_data(data)
self.cover_view.show_data(data)
self._layout.do_layout(self.rect())
self.setToolTip('<p>'+_('Double-click to open Book Details window') +
'<br><br>' + _('Path') + ': ' + data.get(_('Path'), ''))
try:
sz = self.cover_view.pixmap.size()
except:
sz = QSize(0, 0)
self.setToolTip(
'<p>'+_('Double-click to open Book Details window') +
'<br><br>' + _('Path') + ': ' + data.get(_('Path'), '') +
'<br><br>' + _('Cover size: %dx%d')%(sz.width(), sz.height())
)
def reset_info(self):
self.show_data({})

View File

@ -109,6 +109,8 @@ class BookInfo(QDialog, Ui_BookInfo):
pixmap = pixmap.scaled(new_width, new_height,
Qt.KeepAspectRatio, Qt.SmoothTransformation)
self.cover.set_pixmap(pixmap)
sz = pixmap.size()
self.cover.setToolTip(_('Cover size: %dx%d')%(sz.width(), sz.height()))
def refresh(self, row):
if isinstance(row, QModelIndex):

View File

@ -68,7 +68,7 @@ class DaysOfWeek(Base):
def initialize(self, typ=None, val=None):
if typ is None:
typ = 'day/time'
val = (-1, 9, 0)
val = (-1, 6, 0)
if typ == 'day/time':
val = convert_day_time_schedule(val)
@ -118,7 +118,7 @@ class DaysOfMonth(Base):
def initialize(self, typ=None, val=None):
if val is None:
val = ((1,), 9, 0)
val = ((1,), 6, 0)
days_of_month, hour, minute = val
self.days.setText(', '.join(map(str, map(int, days_of_month))))
self.time.setTime(QTime(hour, minute))
@ -380,7 +380,7 @@ class SchedulerDialog(QDialog, Ui_Dialog):
if d < timedelta(days=366):
ld_text = tm
else:
typ, sch = 'day/time', (-1, 9, 0)
typ, sch = 'day/time', (-1, 6, 0)
sch_widget = {'day/time': 0, 'days_of_week': 0, 'days_of_month':1,
'interval':2}[typ]
rb = getattr(self, list(self.SCHEDULE_TYPES)[sch_widget])

View File

@ -12,6 +12,7 @@ from zipfile import ZipFile, ZIP_DEFLATED, ZIP_STORED
from PyQt4.Qt import QDialog
from calibre.constants import isosx, iswindows
from calibre.gui2 import open_local_file
from calibre.gui2.dialogs.tweak_epub_ui import Ui_Dialog
from calibre.libunzip import extract as zipextract
@ -42,11 +43,19 @@ class TweakEpub(QDialog, Ui_Dialog):
self.move(parent_loc.x(),parent_loc.y())
def cleanup(self):
if isosx:
try:
import appscript
self.finder = appscript.app('Finder')
self.finder.Finder_windows[os.path.basename(self._exploded)].close()
except:
# appscript fails to load on 10.4
pass
# Delete directory containing exploded ePub
if self._exploded is not None:
shutil.rmtree(self._exploded, ignore_errors=True)
def display_exploded(self):
'''
Generic subprocess launch of native file browser

View File

@ -317,6 +317,8 @@ class BaseToolBar(QToolBar): # {{{
QToolBar.resizeEvent(self, ev)
style = self.get_text_style()
self.setToolButtonStyle(style)
if hasattr(self, 'd_widget') and hasattr(self.d_widget, 'filler'):
self.d_widget.filler.setVisible(style != Qt.ToolButtonIconOnly)
def get_text_style(self):
style = Qt.ToolButtonTextUnderIcon
@ -399,7 +401,10 @@ class ToolBar(BaseToolBar): # {{{
self.d_widget.layout().addWidget(self.donate_button)
if isosx:
self.d_widget.setStyleSheet('QWidget, QToolButton {background-color: none; border: none; }')
self.d_widget.layout().addWidget(QLabel(u'\u00a0'))
self.d_widget.layout().setContentsMargins(0,0,0,0)
self.d_widget.setContentsMargins(0,0,0,0)
self.d_widget.filler = QLabel(u'\u00a0')
self.d_widget.layout().addWidget(self.d_widget.filler)
bar.addWidget(self.d_widget)
self.showing_donate = True
elif what in self.gui.iactions:

View File

@ -223,7 +223,7 @@ class AuthorSortEdit(EnLineEdit):
LABEL = _('Author s&ort:')
def __init__(self, parent, authors_edit, autogen_button, db,
copy_as_to_a_action):
copy_a_to_as_action, copy_as_to_a_action):
EnLineEdit.__init__(self, parent)
self.authors_edit = authors_edit
self.db = db
@ -242,6 +242,7 @@ class AuthorSortEdit(EnLineEdit):
self.textChanged.connect(self.update_state)
autogen_button.clicked.connect(self.auto_generate)
copy_a_to_as_action.triggered.connect(self.auto_generate)
copy_as_to_a_action.triggered.connect(self.copy_to_authors)
self.update_state()

View File

@ -109,10 +109,12 @@ class MetadataSingleDialogBase(ResizableDialog):
'Using this button to create author sort will change author sort from'
' red to green.'))
b.m = m = QMenu()
ac = m.addAction(QIcon(I('back.png')), _('Set author from author sort'))
ac = m.addAction(QIcon(I('forward.png')), _('Set author sort from author'))
ac2 = m.addAction(QIcon(I('back.png')), _('Set author from author sort'))
b.setMenu(m)
self.authors = AuthorsEdit(self)
self.author_sort = AuthorSortEdit(self, self.authors, b, self.db, ac)
self.author_sort = AuthorSortEdit(self, self.authors, b, self.db, ac,
ac2)
self.basic_metadata_widgets.extend([self.authors, self.author_sort])
self.swap_title_author_button = QToolButton(self)

View File

@ -319,9 +319,12 @@ def show_config_widget(category, name, gui=None, show_restart_msg=False,
:return: True iff a restart is required for the changes made by the user to
take effect
'''
from calibre.gui2 import gprefs
pl = get_plugin(category, name)
d = ConfigDialog(parent)
d.resize(750, 550)
conf_name = 'config_widget_dialog_geometry_%s_%s'%(category, name)
geom = gprefs.get(conf_name, None)
d.setWindowTitle(_('Configure ') + name)
d.setWindowIcon(QIcon(I('config.png')))
bb = QDialogButtonBox(d)
@ -345,7 +348,11 @@ def show_config_widget(category, name, gui=None, show_restart_msg=False,
mygui = True
w.genesis(gui)
w.initialize()
if geom is not None:
d.restoreGeometry(geom)
d.exec_()
geom = bytearray(d.saveGeometry())
gprefs[conf_name] = geom
rr = getattr(d, 'restart_required', False)
if show_restart_msg and rr:
from calibre.gui2 import warning_dialog

View File

@ -73,13 +73,13 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
choices=sorted(list(choices), key=sort_key))
self.current_font = None
self.current_font = self.initial_font = None
self.change_font_button.clicked.connect(self.change_font)
def initialize(self):
ConfigWidgetBase.initialize(self)
self.current_font = gprefs['font']
self.current_font = self.initial_font = gprefs['font']
self.update_font_display()
def restore_defaults(self):
@ -119,7 +119,7 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
def commit(self, *args):
rr = ConfigWidgetBase.commit(self, *args)
if self.current_font != gprefs['font']:
if self.current_font != self.initial_font:
gprefs['font'] = self.current_font
QApplication.setFont(self.font_display.font())
rr = True

View File

@ -19,27 +19,27 @@ class StorePlugin(object): # {{{
If two :class:`StorePlugin` objects have the same name, the one with higher
priority takes precedence.
Sub-classes must implement :meth:`open`, and :meth:`search`.
Regarding :meth:`open`. Most stores only make themselves available
though a web site thus most store plugins will open using
:class:`calibre.gui2.store.web_store_dialog.WebStoreDialog`. This will
open a modal window and display the store website in a QWebView.
Sub-classes should implement and use the :meth:`genesis` if they require
plugin specific initialization. They should not override or otherwise
reimplement :meth:`__init__`.
Once initialized, this plugin has access to the main calibre GUI via the
:attr:`gui` member. You can access other plugins by name, for example::
self.gui.istores['Amazon Kindle']
Plugin authors can use affiliate programs within their plugin. The
distribution of money earned from a store plugin is 70/30. 70% going
to the pluin author / maintainer and 30% going to the calibre project.
The easiest way to handle affiliate money payouts is to randomly select
between the author's affiliate id and calibre's affiliate id so that
70% of the time the author's id is used.
@ -49,61 +49,61 @@ class StorePlugin(object): # {{{
self.gui = gui
self.name = name
self.base_plugin = None
def open(self, gui, parent=None, detail_item=None, external=False):
'''
Open the store.
:param gui: The main GUI. This will be used to have the job
system start downloading an item from the store.
:param parent: The parent of the store dialog. This is used
to create modal dialogs.
:param detail_item: A plugin specific reference to an item
in the store that the user should be shown.
:param external: When False open an internal dialog with the
store. When True open the users default browser to the store's
web site. :param:`detail_item` should still be respected when external
is True.
'''
raise NotImplementedError()
def search(self, query, max_results=10, timeout=60):
'''
Searches the store for items matching query. This should
return items as a generator.
Don't be lazy with the search! Load as much data as possible in the
:class:`calibre.gui2.store.search_result.SearchResult` object. If you have to parse
multiple pages to get all of the data then do so. However, if data (such as cover_url)
isn't available because the store does not display cover images then it's okay to
ignore it.
Also, by default search results can only include ebooks. A plugin can offer users
an option to include physical books in the search results but this must be
disabled by default.
If a store doesn't provide search on it's own use something like a site specific
google search to get search results for this funtion.
:param query: The string query search with.
:param max_results: The maximum number of results to return.
:param timeout: The maximum amount of time in seconds to spend download the search results.
:return: :class:`calibre.gui2.store.search_result.SearchResult` objects
item_data is plugin specific and is used in :meth:`open` to open to a specifc place in the store.
item_data is plugin specific and is used in :meth:`open` to open to a specifc place in the store.
'''
raise NotImplementedError()
def get_settings(self):
'''
This is only useful for plugins that implement
:attr:`config_widget` that is the only way to save
settings. This is used by plugins to get the saved
settings and apply when necessary.
:return: A dictionary filled with the settings used
by this plugin.
'''
@ -117,23 +117,23 @@ class StorePlugin(object): # {{{
Plugin specific initialization.
'''
pass
def config_widget(self):
'''
See :class:`calibre.customize.Plugin` for details.
'''
raise NotImplementedError()
def save_settings(self, config_widget):
'''
See :class:`calibre.customize.Plugin` for details.
'''
raise NotImplementedError()
def customization_help(self, gui=False):
'''
See :class:`calibre.customize.Plugin` for details.
'''
raise NotImplementedError()
# }}}
# }}}

View File

@ -21,14 +21,14 @@ from calibre.gui2.store import StorePlugin
from calibre.gui2.store.search_result import SearchResult
class AmazonKindleStore(StorePlugin):
def open(self, parent=None, detail_item=None, external=False):
'''
Amazon comes with a number of difficulties.
QWebView has major issues with Amazon.com. The largest of
issues is it simply doesn't work on a number of pages.
When connecting to a number parts of Amazon.com (Kindle library
for instance) QNetworkAccessManager fails to connect with a
NetworkError of 399 - ProtocolFailure. The strange thing is,
@ -37,19 +37,19 @@ class AmazonKindleStore(StorePlugin):
the QNetworkAccessManager decides there was a NetworkError it
does not download the page from Amazon. So I can't even set the
HTML in the QWebView myself.
There is http://bugreports.qt.nokia.com/browse/QTWEBKIT-259 an
open bug about the issue but it is not correct. We can set the
useragent (Arora does) to something else and the above issue
will persist. This http://developer.qt.nokia.com/forums/viewthread/793
gives a bit more information about the issue but as of now (27/Feb/2011)
there is no solution or work around.
We cannot change the The linkDelegationPolicy to allow us to avoid
QNetworkAccessManager because it only works links. Forms aren't
included so the same issue persists on any part of the site (login)
that use a form to load a new page.
Using an aStore was evaluated but I've decided against using it.
There are three major issues with an aStore. Because checkout is
handled by sending the user to Amazon we can't put it in a QWebView.
@ -57,7 +57,7 @@ class AmazonKindleStore(StorePlugin):
nicer. Also, we cannot put the aStore in a QWebView and let it open the
redirection the users default browser because the cookies with the
shopping cart won't transfer.
Another issue with the aStore is how it handles the referral. It only
counts the referral for the items in the shopping card / the item
that directed the user to Amazon. Kindle books do not use the shopping
@ -65,44 +65,44 @@ class AmazonKindleStore(StorePlugin):
instance we would only get referral credit for the one book that the
aStore directs to Amazon that the user buys. Any other purchases we
won't get credit for.
The last issue with the aStore is performance. Even though it's an
Amazon site it's alow. So much slower than Amazon.com that it makes
me not want to browse books using it. The look and feel are lesser
issues. So is the fact that it almost seems like the purchase is
with calibre. This can cause some support issues because we can't
do much for issues with Amazon.com purchase hiccups.
Another option that was evaluated was the Product Advertising API.
The reasons against this are complexity. It would take a lot of work
to basically re-create Amazon.com within calibre. The Product
Advertising API is also designed with being run on a server not
in an app. The signing keys would have to be made avaliable to ever
calibre user which means bad things could be done with our account.
The Product Advertising API also assumes the same browser for easy
shopping cart transfer to Amazon. With QWebView not working and there
not being an easy way to transfer cookies between a QWebView and the
users default browser this won't work well.
We could create our own website on the calibre server and create an
Amazon Product Advertising API store. However, this goes back to the
complexity argument. Why spend the time recreating Amazon.com
The final and largest issue against using the Product Advertising API
is the Efficiency Guidelines:
"Each account used to access the Product Advertising API will be allowed
an initial usage limit of 2,000 requests per hour. Each account will
receive an additional 500 requests per hour (up to a maximum of 25,000
requests per hour) for every $1 of shipped item revenue driven per hour
in a trailing 30-day period. Usage thresholds are recalculated daily based
on revenue performance."
on revenue performance."
With over two million users a limit of 2,000 request per hour could
render our store unusable for no other reason than Amazon rate
limiting our traffic.
The best (I use the term lightly here) solution is to open Amazon.com
in the users default browser and set the affiliate id as part of the url.
'''
@ -119,14 +119,14 @@ class AmazonKindleStore(StorePlugin):
def search(self, query, max_results=10, timeout=60):
url = 'http://www.amazon.com/s/url=search-alias%3Ddigital-text&field-keywords=' + urllib2.quote(query)
br = browser()
counter = max_results
with closing(br.open(url, timeout=timeout)) as f:
doc = html.fromstring(f.read())
for data in doc.xpath('//div[@class="productData"]'):
if counter <= 0:
break
# Even though we are searching digital-text only Amazon will still
# put in results for non Kindle books (author pages). Se we need
# to explicitly check if the item is a Kindle book and ignore it
@ -134,7 +134,7 @@ class AmazonKindleStore(StorePlugin):
type = ''.join(data.xpath('//span[@class="format"]/text()'))
if 'kindle' not in type.lower():
continue
# We must have an asin otherwise we can't easily reference the
# book later.
asin_href = None
@ -148,25 +148,25 @@ class AmazonKindleStore(StorePlugin):
continue
else:
continue
cover_url = ''
if asin_href:
cover_img = data.xpath('//div[@class="productImage"]/a[@href="%s"]/img/@src' % asin_href)
if cover_img:
cover_url = cover_img[0]
title = ''.join(data.xpath('div[@class="productTitle"]/a/text()'))
author = ''.join(data.xpath('div[@class="productTitle"]/span[@class="ptBrand"]/text()'))
author = author.split('by')[-1]
price = ''.join(data.xpath('div[@class="newPrice"]/span/text()'))
counter -= 1
s = SearchResult()
s.cover_url = cover_url
s.title = title.strip()
s.author = author.strip()
s.price = price.strip()
s.detail_item = asin.strip()
yield s

View File

@ -6,7 +6,6 @@ __license__ = 'GPL 3'
__copyright__ = '2011, John Schember <john@nachtimwald.com>'
__docformat__ = 'restructuredtext en'
import re
import urllib2
from contextlib import closing
@ -22,7 +21,7 @@ from calibre.gui2.store.search_result import SearchResult
from calibre.gui2.store.web_store_dialog import WebStoreDialog
class BeWriteStore(BasicStoreConfig, StorePlugin):
def open(self, parent=None, detail_item=None, external=False):
settings = self.get_settings()
url = 'http://www.bewrite.net/mm5/merchant.mvc?Screen=SFNT'
@ -42,9 +41,9 @@ class BeWriteStore(BasicStoreConfig, StorePlugin):
def search(self, query, max_results=10, timeout=60):
url = 'http://www.bewrite.net/mm5/merchant.mvc?Search_Code=B&Screen=SRCH&Search=' + urllib2.quote(query)
br = browser()
counter = max_results
with closing(br.open(url, timeout=timeout)) as f:
doc = html.fromstring(f.read())
@ -55,12 +54,12 @@ class BeWriteStore(BasicStoreConfig, StorePlugin):
id = ''.join(data.xpath('.//a/@href'))
if not id:
continue
heading = ''.join(data.xpath('./td[2]//text()'))
heading = ''.join(data.xpath('./td[2]//text()'))
title, q, author = heading.partition('by ')
cover_url = ''
price = ''
with closing(br.open(id.strip(), timeout=timeout/4)) as nf:
idata = html.fromstring(nf.read())
price = ''.join(idata.xpath('//div[@id="content"]//td[contains(text(), "ePub")]/text()'))
@ -68,14 +67,14 @@ class BeWriteStore(BasicStoreConfig, StorePlugin):
cover_img = idata.xpath('//div[@id="content"]//img[1]/@src')
if cover_img:
cover_url = 'http://www.bewrite.net/mm5/' + cover_img[0]
counter -= 1
s = SearchResult()
s.cover_url = cover_url.strip()
s.title = title.strip()
s.author = author.strip()
s.price = price.strip()
s.detail_item = id.strip()
yield s

View File

@ -13,9 +13,8 @@ from random import shuffle
from threading import Thread
from Queue import Queue
from PyQt4.Qt import Qt, QAbstractItemModel, QDialog, QTimer, QVariant, \
QModelIndex, QPixmap, QSize, QCheckBox, QVBoxLayout, QHBoxLayout, \
QPushButton, QString, QByteArray
from PyQt4.Qt import (Qt, QAbstractItemModel, QDialog, QTimer, QVariant,
QModelIndex, QPixmap, QSize, QCheckBox, QVBoxLayout)
from calibre import browser
from calibre.gui2 import NONE
@ -35,7 +34,7 @@ class SearchDialog(QDialog, Ui_Dialog):
def __init__(self, istores, *args):
QDialog.__init__(self, *args)
self.setupUi(self)
self.config = DynamicConfig('store_search')
# We keep a cache of store plugins and reference them by name.
@ -44,7 +43,7 @@ class SearchDialog(QDialog, Ui_Dialog):
# Check for results and hung threads.
self.checker = QTimer()
self.hang_check = 0
self.model = Matches()
self.results_view.setModel(self.model)
@ -59,7 +58,7 @@ class SearchDialog(QDialog, Ui_Dialog):
stores_group_layout.addWidget(cbox)
setattr(self, 'store_check_' + x, cbox)
stores_group_layout.addStretch()
# Create and add the progress indicator
self.pi = ProgressIndicator(self, 24)
self.bottom_layout.insertWidget(0, self.pi)
@ -71,9 +70,9 @@ class SearchDialog(QDialog, Ui_Dialog):
self.select_invert_stores.clicked.connect(self.stores_select_invert)
self.select_none_stores.clicked.connect(self.stores_select_none)
self.finished.connect(self.dialog_closed)
self.restore_state()
def resize_columns(self):
total = 600
# Cover
@ -87,19 +86,19 @@ class SearchDialog(QDialog, Ui_Dialog):
self.results_view.setColumnWidth(3, int(total*.10))
# Store
self.results_view.setColumnWidth(4, int(total*.20))
def do_search(self, checked=False):
# Stop all running threads.
self.checker.stop()
self.search_pool.abort()
# Clear the visible results.
self.results_view.model().clear_results()
# Don't start a search if there is nothing to search for.
query = unicode(self.search_edit.text())
if not query.strip():
return
# Plugins are in alphebetic order. Randomize the
# order of plugin names. This way plugins closer
# to a don't have an unfair advantage over
@ -117,12 +116,12 @@ class SearchDialog(QDialog, Ui_Dialog):
self.checker.start(100)
self.search_pool.start_threads()
self.pi.startAnimation()
def save_state(self):
self.config['store_search_geometry'] = self.saveGeometry()
self.config['store_search_store_splitter_state'] = self.store_splitter.saveState()
self.config['store_search_results_view_column_width'] = [self.results_view.columnWidth(i) for i in range(self.model.columnCount())]
store_check = {}
for n in self.store_plugins:
store_check[n] = getattr(self, 'store_check_' + n).isChecked()
@ -132,11 +131,11 @@ class SearchDialog(QDialog, Ui_Dialog):
geometry = self.config['store_search_geometry']
if geometry:
self.restoreGeometry(geometry)
splitter_state = self.config['store_search_store_splitter_state']
if splitter_state:
self.store_splitter.restoreState(splitter_state)
results_cwidth = self.config['store_search_results_view_column_width']
if results_cwidth:
for i, x in enumerate(results_cwidth):
@ -145,7 +144,7 @@ class SearchDialog(QDialog, Ui_Dialog):
self.results_view.setColumnWidth(i, x)
else:
self.resize_columns()
store_check = self.config['store_search_store_checked']
if store_check:
for n in store_check:
@ -165,7 +164,7 @@ class SearchDialog(QDialog, Ui_Dialog):
if not self.search_pool.threads_running() and not self.search_pool.has_tasks():
self.checker.stop()
self.pi.stopAnimation()
while self.search_pool.has_results():
res = self.search_pool.get_result()
if res:
@ -189,15 +188,15 @@ class SearchDialog(QDialog, Ui_Dialog):
def stores_select_all(self):
for check in self.get_store_checks():
check.setChecked(True)
def stores_select_invert(self):
for check in self.get_store_checks():
check.setChecked(not check.isChecked())
def stores_select_none(self):
for check in self.get_store_checks():
check.setChecked(False)
def dialog_closed(self, result):
self.model.closing()
self.search_pool.abort()
@ -208,46 +207,46 @@ class GenericDownloadThreadPool(object):
'''
add_task must be implemented in a subclass.
'''
def __init__(self, thread_type, thread_count):
self.thread_type = thread_type
self.thread_count = thread_count
self.tasks = Queue()
self.results = Queue()
self.threads = []
def add_task(self):
raise NotImplementedError()
def start_threads(self):
for i in range(self.thread_count):
t = self.thread_type(self.tasks, self.results)
self.threads.append(t)
t.start()
def abort(self):
self.tasks = Queue()
self.results = Queue()
for t in self.threads:
t.abort()
self.threads = []
def has_tasks(self):
return not self.tasks.empty()
def get_result(self):
return self.results.get()
def get_result_no_wait(self):
return self.results.get_nowait()
def result_count(self):
return len(self.results)
def has_results(self):
return not self.results.empty()
def threads_running(self):
for t in self.threads:
if t.is_alive():
@ -260,7 +259,7 @@ class SearchThreadPool(GenericDownloadThreadPool):
Threads will run until there is no work or
abort is called. Create and start new threads
using start_threads(). Reset by calling abort().
Example:
sp = SearchThreadPool(SearchThread, 3)
add tasks using add_task(...)
@ -270,13 +269,13 @@ class SearchThreadPool(GenericDownloadThreadPool):
add tasks using add_task(...)
sp.start_threads()
'''
def add_task(self, query, store_name, store_plugin, timeout):
self.tasks.put((query, store_name, store_plugin, timeout))
class SearchThread(Thread):
def __init__(self, tasks, results):
Thread.__init__(self)
self.daemon = True
@ -286,7 +285,7 @@ class SearchThread(Thread):
def abort(self):
self._run = False
def run(self):
while self._run and not self.tasks.empty():
try:
@ -305,7 +304,7 @@ class CoverThreadPool(GenericDownloadThreadPool):
'''
Once started all threads run until abort is called.
'''
def add_task(self, search_result, update_callback, timeout=5):
self.tasks.put((search_result, update_callback, timeout))
@ -318,12 +317,12 @@ class CoverThread(Thread):
self.tasks = tasks
self.results = results
self._run = True
self.br = browser()
def abort(self):
self._run = False
def run(self):
while self._run:
try:
@ -354,13 +353,13 @@ class Matches(QAbstractItemModel):
def closing(self):
self.cover_pool.abort()
def clear_results(self):
self.matches = []
self.cover_pool.abort()
self.cover_pool.start_threads()
self.reset()
def add_result(self, result):
self.layoutAboutToBeChanged.emit()
self.matches.append(result)
@ -391,7 +390,7 @@ class Matches(QAbstractItemModel):
def columnCount(self, *args):
return len(self.HEADERS)
def headerData(self, section, orientation, role):
if role != Qt.DisplayRole:
return NONE
@ -434,7 +433,7 @@ class Matches(QAbstractItemModel):
elif col == 3:
text = result.price
if len(text) < 3 or text[-3] not in ('.', ','):
text += '00'
text += '00'
text = re.sub(r'\D', '', text)
text = text.rjust(6, '0')
elif col == 4:
@ -444,7 +443,7 @@ class Matches(QAbstractItemModel):
def sort(self, col, order, reset=True):
if not self.matches:
return
descending = order == Qt.DescendingOrder
descending = order == Qt.DescendingOrder
self.matches.sort(None,
lambda x: sort_key(unicode(self.data_as_text(x, col))),
descending)

View File

@ -9,8 +9,8 @@ __docformat__ = 'restructuredtext en'
import os
from urlparse import urlparse
from PyQt4.Qt import QWebView, QWebPage, QNetworkCookieJar, QNetworkRequest, QString, \
QFileDialog, QNetworkProxy
from PyQt4.Qt import QNetworkCookieJar, QFileDialog, QNetworkProxy
from PyQt4.QtWebKit import QWebView, QWebPage
from calibre import USER_AGENT, get_proxies, get_download_filename
from calibre.ebooks import BOOK_EXTENSIONS
@ -35,13 +35,13 @@ class NPWebView(QWebView):
proxy.setPassword(proxy_parts.password)
proxy.setHostName(proxy_parts.hostname)
proxy.setPort(proxy_parts.port)
self.page().networkAccessManager().setProxy(proxy)
self.page().networkAccessManager().setProxy(proxy)
self.page().setForwardUnsupportedContent(True)
self.page().unsupportedContent.connect(self.start_download)
self.page().downloadRequested.connect(self.start_download)
self.page().networkAccessManager().sslErrors.connect(self.ignore_ssl_errors)
def createWindow(self, type):
if type == QWebPage.WebBrowserWindow:
return self
@ -50,17 +50,17 @@ class NPWebView(QWebView):
def set_gui(self, gui):
self.gui = gui
def set_tags(self, tags):
self.tags = tags
def start_download(self, request):
if not self.gui:
return
url = unicode(request.url().toString())
cf = self.get_cookies()
filename = get_download_filename(url, cf)
ext = os.path.splitext(filename)[1][1:].lower()
if ext not in BOOK_EXTENSIONS:
@ -76,21 +76,21 @@ class NPWebView(QWebView):
def ignore_ssl_errors(self, reply, errors):
reply.ignoreSslErrors(errors)
def get_cookies(self):
'''
Writes QNetworkCookies to Mozilla cookie .txt file.
:return: The file path to the cookie file.
'''
cf = PersistentTemporaryFile(suffix='.txt')
cf.write('# Netscape HTTP Cookie File\n\n')
for c in self.page().networkAccessManager().cookieJar().allCookies():
cookie = []
domain = unicode(c.domain())
cookie.append(domain)
cookie.append('TRUE' if domain.startswith('.') else 'FALSE')
cookie.append(unicode(c.path()))
@ -98,15 +98,15 @@ class NPWebView(QWebView):
cookie.append(unicode(c.expirationDate().toTime_t()))
cookie.append(unicode(c.name()))
cookie.append(unicode(c.value()))
cf.write('\t'.join(cookie))
cf.write('\n')
cf.close()
return cf.name
class NPWebPage(QWebPage):
def userAgentForUrl(self, url):
return USER_AGENT

View File

@ -88,6 +88,11 @@ class SystemTrayIcon(QSystemTrayIcon): # {{{
# }}}
_gui = None
def get_gui():
return _gui
class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
TagBrowserMixin, CoverFlowMixin, LibraryViewMixin, SearchBoxMixin,
SavedSearchBoxMixin, SearchRestrictionMixin, LayoutMixin, UpdateMixin,
@ -97,7 +102,9 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
def __init__(self, opts, parent=None, gui_debug=None):
global _gui
MainWindow.__init__(self, opts, parent=parent, disable_automatic_gc=True)
_gui = self
self.opts = opts
self.device_connected = None
self.gui_debug = gui_debug

View File

@ -426,7 +426,7 @@ def do_show_metadata(db, id, as_opf):
mi = OPFCreator(os.getcwd(), mi)
mi.render(sys.stdout)
else:
print unicode(mi).encode(preferred_encoding)
prints(unicode(mi))
def show_metadata_option_parser():
parser = get_parser(_(

File diff suppressed because it is too large Load Diff