404 - ' in raw:
- report(verbose)
- return
- raw = xml_to_unicode(raw, strip_encoding_pats=True,
- resolve_entities=True)[0]
- try:
- feed = soupparser.fromstring(raw)
- except:
- try:
- #remove ASCII invalid chars
- feed = soupparser.fromstring(clean_ascii_chars(raw))
- except:
- return None
-
- # get results
- return feed.xpath("//div[@id='container']")[0]
-
- def populate(self, entries, browser, verbose=False):
- #single entry
- if len(entries) == 1 and not isinstance(entries[0], str):
- try:
- entry = entries[0].xpath("//div[@id='container']")[0]
- entry = entry.find("div[@id='book-info']")
- title = self.get_title(entry)
- authors = self.get_authors(entry)
- except Exception as e:
- if verbose:
- print 'Failed to get all details for an entry'
- print e
- return
- self.append(self.fill_MI(entry, title, authors, verbose))
- else:
- #multiple entries
- for x in entries:
- try:
- entry = self.get_individual_metadata(browser, x, verbose)
- entry = entry.find("div[@id='book-info']")
- title = self.get_title(entry)
- authors = self.get_authors(entry)
- except Exception as e:
- if verbose:
- print 'Failed to get all details for an entry'
- print e
- continue
- self.append(self.fill_MI(entry, title, authors, verbose))
-
-class Covers(object):
-
- def __init__(self, isbn = None):
- assert isbn is not None
- self.urlimg = ''
- self.isbn = isbn
- self.isbnf = False
-
- def __call__(self, entry = None):
- try:
- self.urlimg = entry.xpath("//div[@id='book-picture']/a")[0].get('href')
- except:
- return self
- isbno = entry.get_element_by_id('book-info').find("dl[@title='Informations sur le livre']")
- for x in isbno.getiterator('dt'):
- if x.text == 'ISBN' and check_isbn(x.getnext().text_content()):
- self.isbnf = True
- break
- return self
-
- def check_cover(self):
- return True if self.urlimg else False
-
- def get_cover(self, browser, timeout = 5.):
- try:
- cover, ext = browser.open_novisit(self.urlimg, timeout=timeout).read(), \
- self.urlimg.rpartition('.')[-1]
- return cover, ext if ext else 'jpg'
- except Exception as err:
- if isinstance(getattr(err, 'args', [None])[0], socket.timeout):
- raise NiceBooksError(_('Nicebooks timed out. Try again later.'))
- if not len(self.urlimg):
- if not self.isbnf:
- raise ISBNNotFound(_('ISBN: %s not found.') % self.isbn)
- raise NiceBooksError(_('An errror occured with Nicebooks cover fetcher'))
-
-
-def search(title=None, author=None, publisher=None, isbn=None,
- max_results=5, verbose=False, keywords=None):
- br = browser()
- entries = Query(title=title, author=author, isbn=isbn, publisher=publisher,
- keywords=keywords, max_results=max_results)(br, verbose,timeout = 10.)
-
- if entries is None or len(entries) == 0:
- return None
-
- #List of entry
- ans = ResultList()
- ans.populate(entries, br, verbose)
- return ans
-
-def check_for_cover(isbn):
- br = browser()
- entry = Query(isbn=isbn, max_results=1)(br, False)[0]
- return Covers(isbn)(entry).check_cover()
-
-def cover_from_isbn(isbn, timeout = 5.):
- br = browser()
- entry = Query(isbn=isbn, max_results=1)(br, False, timeout)[0]
- return Covers(isbn)(entry).get_cover(br, timeout)
-
-
-def option_parser():
- parser = OptionParser(textwrap.dedent(\
- _('''\
- %prog [options]
-
- Fetch book metadata from Nicebooks. You must specify one of title, author,
- ISBN, publisher or keywords. Will fetch a maximum of 20 matches,
- so you should make your query as specific as possible.
- It can also get covers if the option is activated.
- ''')
- ))
- parser.add_option('-t', '--title', help=_('Book title'))
- parser.add_option('-a', '--author', help=_('Book author(s)'))
- parser.add_option('-p', '--publisher', help=_('Book publisher'))
- parser.add_option('-i', '--isbn', help=_('Book ISBN'))
- parser.add_option('-k', '--keywords', help=_('Keywords'))
- parser.add_option('-c', '--covers', default=0,
- help=_('Covers: 1-Check/ 2-Download'))
- parser.add_option('-p', '--coverspath', default='',
- help=_('Covers files path'))
- parser.add_option('-m', '--max-results', default=20,
- help=_('Maximum number of results to fetch'))
- parser.add_option('-v', '--verbose', default=0, action='count',
- help=_('Be more verbose about errors'))
- return parser
-
-def main(args=sys.argv):
- import os
- parser = option_parser()
- opts, args = parser.parse_args(args)
- try:
- results = search(opts.title, opts.author, isbn=opts.isbn, publisher=opts.publisher,
- keywords=opts.keywords, verbose=opts.verbose, max_results=opts.max_results)
- except AssertionError:
- report(True)
- parser.print_help()
- return 1
- if results is None or len(results) == 0:
- print _('No result found for this search!')
- return 0
- for result in results:
- print unicode(result).encode(preferred_encoding, 'replace')
- covact = int(opts.covers)
- if covact == 1:
- textcover = _('No cover found!')
- if check_for_cover(result.isbn):
- textcover = _('A cover was found for this book')
- print textcover
- elif covact == 2:
- cover_data, ext = cover_from_isbn(result.isbn)
- cpath = result.isbn
- if len(opts.coverspath):
- cpath = os.path.normpath(opts.coverspath + '/' + result.isbn)
- oname = os.path.abspath(cpath+'.'+ext)
- open(oname, 'wb').write(cover_data)
- print _('Cover saved to file '), oname
- print
-
-if __name__ == '__main__':
- sys.exit(main())
diff --git a/src/calibre/ebooks/metadata/sources/base.py b/src/calibre/ebooks/metadata/sources/base.py
index e67b87efbd..3eff9b11b3 100644
--- a/src/calibre/ebooks/metadata/sources/base.py
+++ b/src/calibre/ebooks/metadata/sources/base.py
@@ -307,7 +307,7 @@ class Source(Plugin):
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\.)[\])}]', ''),
+ (r'(?i)[({\[](\d{4}|omnibus|anthology|hardcover|paperback|turtleback|mass\s*market|edition|ed\.)[\])}]', ''),
# Remove any strings that contain the substring edition inside
# parentheses
(r'(?i)[({\[].*?(edition|ed.).*?[\]})]', ''),
diff --git a/src/calibre/ebooks/metadata/sources/cli.py b/src/calibre/ebooks/metadata/sources/cli.py
index cb422f939d..f8b9c6b7a9 100644
--- a/src/calibre/ebooks/metadata/sources/cli.py
+++ b/src/calibre/ebooks/metadata/sources/cli.py
@@ -19,13 +19,8 @@ from calibre.ebooks.metadata.opf2 import metadata_to_opf
from calibre.ebooks.metadata.sources.base import create_log
from calibre.ebooks.metadata.sources.identify import identify
from calibre.ebooks.metadata.sources.covers import download_cover
-from calibre.utils.config import test_eight_code
def option_parser():
- if not test_eight_code:
- from calibre.ebooks.metadata.fetch import option_parser
- return option_parser()
-
parser = OptionParser(textwrap.dedent(
'''\
%prog [options]
@@ -48,9 +43,6 @@ def option_parser():
return parser
def main(args=sys.argv):
- if not test_eight_code:
- from calibre.ebooks.metadata.fetch import main
- return main(args)
parser = option_parser()
opts, args = parser.parse_args(args)
diff --git a/src/calibre/gui2/actions/add.py b/src/calibre/gui2/actions/add.py
index f8dd0693ea..737bf38a56 100644
--- a/src/calibre/gui2/actions/add.py
+++ b/src/calibre/gui2/actions/add.py
@@ -20,9 +20,8 @@ from calibre.ebooks import BOOK_EXTENSIONS
from calibre.utils.filenames import ascii_filename
from calibre.constants import preferred_encoding, filesystem_encoding
from calibre.gui2.actions import InterfaceAction
-from calibre.gui2 import config, question_dialog
+from calibre.gui2 import question_dialog
from calibre.ebooks.metadata import MetaInformation
-from calibre.utils.config import test_eight_code
from calibre.ebooks.metadata.sources.base import msprefs
def get_filters():
@@ -180,26 +179,17 @@ class AddAction(InterfaceAction):
except IndexError:
self.gui.library_view.model().books_added(self.isbn_add_dialog.value)
self.isbn_add_dialog.accept()
- if test_eight_code:
- orig = msprefs['ignore_fields']
- new = list(orig)
- for x in ('title', 'authors'):
- if x in new:
- new.remove(x)
- msprefs['ignore_fields'] = new
- try:
- self.gui.iactions['Edit Metadata'].download_metadata(
- ids=self.add_by_isbn_ids)
- finally:
- msprefs['ignore_fields'] = orig
- else:
- orig = config['overwrite_author_title_metadata']
- config['overwrite_author_title_metadata'] = True
- try:
- self.gui.iactions['Edit Metadata'].do_download_metadata(
- self.add_by_isbn_ids)
- finally:
- config['overwrite_author_title_metadata'] = orig
+ orig = msprefs['ignore_fields']
+ new = list(orig)
+ for x in ('title', 'authors'):
+ if x in new:
+ new.remove(x)
+ msprefs['ignore_fields'] = new
+ try:
+ self.gui.iactions['Edit Metadata'].download_metadata(
+ ids=self.add_by_isbn_ids)
+ finally:
+ msprefs['ignore_fields'] = orig
return
diff --git a/src/calibre/gui2/actions/edit_metadata.py b/src/calibre/gui2/actions/edit_metadata.py
index 3349be8d80..4ab4950179 100644
--- a/src/calibre/gui2/actions/edit_metadata.py
+++ b/src/calibre/gui2/actions/edit_metadata.py
@@ -10,15 +10,13 @@ from functools import partial
from PyQt4.Qt import Qt, QMenu, QModelIndex, QTimer
-from calibre.gui2 import error_dialog, config, Dispatcher, question_dialog
-from calibre.gui2.dialogs.metadata_single import MetadataSingleDialog
+from calibre.gui2 import error_dialog, Dispatcher, question_dialog
from calibre.gui2.dialogs.metadata_bulk import MetadataBulkDialog
from calibre.gui2.dialogs.confirm_delete import confirm
from calibre.gui2.dialogs.tag_list_editor import TagListEditor
from calibre.gui2.actions import InterfaceAction
from calibre.ebooks.metadata import authors_to_string
from calibre.utils.icu import sort_key
-from calibre.utils.config import test_eight_code
class EditMetadataAction(InterfaceAction):
@@ -36,22 +34,8 @@ class EditMetadataAction(InterfaceAction):
md.addAction(_('Edit metadata in bulk'),
partial(self.edit_metadata, False, bulk=True))
md.addSeparator()
- if test_eight_code:
- dall = self.download_metadata
- else:
- dall = partial(self.download_metadata_old, False, covers=True)
- dident = partial(self.download_metadata_old, False, covers=False)
- dcovers = partial(self.download_metadata_old, False, covers=True,
- set_metadata=False, set_social_metadata=False)
-
- md.addAction(_('Download metadata and covers'), dall,
+ md.addAction(_('Download metadata and covers'), self.download_metadata,
Qt.ControlModifier+Qt.Key_D)
- if not test_eight_code:
- md.addAction(_('Download only metadata'), dident)
- md.addAction(_('Download only covers'), dcovers)
- md.addAction(_('Download only social metadata'),
- partial(self.download_metadata_old, False, covers=False,
- set_metadata=False, set_social_metadata=True))
self.metadata_menu = md
mb = QMenu()
@@ -88,7 +72,7 @@ class EditMetadataAction(InterfaceAction):
_('No books selected'), show=True)
db = self.gui.library_view.model().db
ids = [db.id(row.row()) for row in rows]
- from calibre.gui2.metadata.bulk_download2 import start_download
+ from calibre.gui2.metadata.bulk_download import start_download
start_download(self.gui, ids,
Dispatcher(self.metadata_downloaded))
@@ -96,7 +80,7 @@ class EditMetadataAction(InterfaceAction):
if job.failed:
self.gui.job_exception(job, dialog_title=_('Failed to download metadata'))
return
- from calibre.gui2.metadata.bulk_download2 import get_job_details
+ from calibre.gui2.metadata.bulk_download import get_job_details
id_map, failed_ids, failed_covers, all_failed, det_msg = \
get_job_details(job)
if all_failed:
@@ -112,8 +96,9 @@ class EditMetadataAction(InterfaceAction):
show_copy_button = False
if failed_ids or failed_covers:
show_copy_button = True
+ num = len(failed_ids.union(failed_covers))
msg += ''+_('Could not download metadata and/or covers for %d of the books. Click'
- ' "Show details" to see which books.')%len(failed_ids)
+ ' "Show details" to see which books.')%num
payload = (id_map, failed_ids, failed_covers)
from calibre.gui2.dialogs.message_box import ProceedNotification
@@ -158,49 +143,6 @@ class EditMetadataAction(InterfaceAction):
self.apply_metadata_changes(id_map)
- def download_metadata_old(self, checked, covers=True, set_metadata=True,
- set_social_metadata=None):
- rows = self.gui.library_view.selectionModel().selectedRows()
- if not rows or len(rows) == 0:
- d = error_dialog(self.gui, _('Cannot download metadata'),
- _('No books selected'))
- d.exec_()
- return
- db = self.gui.library_view.model().db
- ids = [db.id(row.row()) for row in rows]
- self.do_download_metadata(ids, covers=covers,
- set_metadata=set_metadata,
- set_social_metadata=set_social_metadata)
-
- def do_download_metadata(self, ids, covers=True, set_metadata=True,
- set_social_metadata=None):
- m = self.gui.library_view.model()
- db = m.db
- if set_social_metadata is None:
- get_social_metadata = config['get_social_metadata']
- else:
- get_social_metadata = set_social_metadata
- from calibre.gui2.metadata.bulk_download import DoDownload
- if set_social_metadata is not None and set_social_metadata:
- x = _('social metadata')
- else:
- x = _('covers') if covers and not set_metadata else _('metadata')
- title = _('Downloading {0} for {1} book(s)').format(x, len(ids))
- self._download_book_metadata = DoDownload(self.gui, title, db, ids,
- get_covers=covers, set_metadata=set_metadata,
- get_social_metadata=get_social_metadata)
- m.stop_metadata_backup()
- try:
- self._download_book_metadata.exec_()
- finally:
- m.start_metadata_backup()
- cr = self.gui.library_view.currentIndex().row()
- x = self._download_book_metadata
- if x.updated:
- self.gui.library_view.model().refresh_ids(
- x.updated, cr)
- if self.gui.cover_flow:
- self.gui.cover_flow.dataChanged()
# }}}
def edit_metadata(self, checked, bulk=None):
@@ -227,9 +169,7 @@ class EditMetadataAction(InterfaceAction):
list(range(self.gui.library_view.model().rowCount(QModelIndex())))
current_row = row_list.index(cr)
- func = (self.do_edit_metadata if test_eight_code else
- self.do_edit_metadata_old)
- changed, rows_to_refresh = func(row_list, current_row)
+ changed, rows_to_refresh = self.do_edit_metadata(row_list, current_row)
m = self.gui.library_view.model()
@@ -244,36 +184,6 @@ class EditMetadataAction(InterfaceAction):
m.current_changed(current, previous)
self.gui.tags_view.recount()
- def do_edit_metadata_old(self, row_list, current_row):
- changed = set([])
- db = self.gui.library_view.model().db
-
- while True:
- prev = next_ = None
- if current_row > 0:
- prev = db.title(row_list[current_row-1])
- if current_row < len(row_list) - 1:
- next_ = db.title(row_list[current_row+1])
-
- d = MetadataSingleDialog(self.gui, row_list[current_row], db,
- prev=prev, next_=next_)
- d.view_format.connect(lambda
- fmt:self.gui.iactions['View'].view_format(row_list[current_row],
- fmt))
- ret = d.exec_()
- d.break_cycles()
- if ret != d.Accepted:
- break
-
- changed.add(d.id)
- self.gui.library_view.model().refresh_ids(list(d.books_to_refresh))
- if d.row_delta == 0:
- break
- current_row += d.row_delta
- self.gui.library_view.set_current_row(current_row)
- self.gui.library_view.scroll_to_row(current_row)
- return changed, set()
-
def do_edit_metadata(self, row_list, current_row):
from calibre.gui2.metadata.single import edit_metadata
db = self.gui.library_view.model().db
diff --git a/src/calibre/gui2/dialogs/fetch_metadata.py b/src/calibre/gui2/dialogs/fetch_metadata.py
deleted file mode 100644
index 426c7b1d60..0000000000
--- a/src/calibre/gui2/dialogs/fetch_metadata.py
+++ /dev/null
@@ -1,271 +0,0 @@
-__license__ = 'GPL v3'
-__copyright__ = '2008, Kovid Goyal '
-'''
-GUI for fetching metadata from servers.
-'''
-
-import time
-from threading import Thread
-
-from PyQt4.QtCore import Qt, QObject, SIGNAL, QVariant, pyqtSignal, \
- QAbstractTableModel, QCoreApplication, QTimer
-from PyQt4.QtGui import QDialog, QItemSelectionModel, QIcon
-
-from calibre.gui2.dialogs.fetch_metadata_ui import Ui_FetchMetadata
-from calibre.gui2 import error_dialog, NONE, info_dialog, config
-from calibre.gui2.widgets import ProgressIndicator
-from calibre import strftime, force_unicode
-from calibre.customize.ui import get_isbndb_key, set_isbndb_key
-from calibre.utils.icu import sort_key
-
-_hung_fetchers = set([])
-
-class Fetcher(Thread):
-
- def __init__(self, title, author, publisher, isbn, key):
- Thread.__init__(self)
- self.daemon = True
- self.title = title
- self.author = author
- self.publisher = publisher
- self.isbn = isbn
- self.key = key
- self.results, self.exceptions = [], []
-
- def run(self):
- from calibre.ebooks.metadata.fetch import search
- self.results, self.exceptions = search(self.title, self.author,
- self.publisher, self.isbn,
- self.key if self.key else None)
-
-
-class Matches(QAbstractTableModel):
-
- def __init__(self, matches):
- self.matches = matches
- self.yes_icon = QVariant(QIcon(I('ok.png')))
- QAbstractTableModel.__init__(self)
-
- def rowCount(self, *args):
- return len(self.matches)
-
- def columnCount(self, *args):
- return 8
-
- def headerData(self, section, orientation, role):
- if role != Qt.DisplayRole:
- return NONE
- text = ""
- if orientation == Qt.Horizontal:
- if section == 0: text = _("Title")
- elif section == 1: text = _("Author(s)")
- elif section == 2: text = _("Author Sort")
- elif section == 3: text = _("Publisher")
- elif section == 4: text = _("ISBN")
- elif section == 5: text = _("Published")
- elif section == 6: text = _("Has Cover")
- elif section == 7: text = _("Has Summary")
-
- return QVariant(text)
- else:
- return QVariant(section+1)
-
- def summary(self, row):
- return self.matches[row].comments
-
- def data_as_text(self, book, col):
- if col == 0 and book.title is not None:
- return book.title
- elif col == 1:
- return ', '.join(book.authors)
- elif col == 2 and book.author_sort is not None:
- return book.author_sort
- elif col == 3 and book.publisher is not None:
- return book.publisher
- elif col == 4 and book.isbn is not None:
- return book.isbn
- elif col == 5 and hasattr(book.pubdate, 'timetuple'):
- return strftime('%b %Y', book.pubdate.timetuple())
- elif col == 6 and book.has_cover:
- return 'y'
- elif col == 7 and book.comments:
- return 'y'
- return ''
-
- def data(self, index, role):
- row, col = index.row(), index.column()
- book = self.matches[row]
- if role == Qt.DisplayRole:
- res = self.data_as_text(book, col)
- if col <= 5 and res:
- return QVariant(res)
- return NONE
- elif role == Qt.DecorationRole:
- if col == 6 and book.has_cover:
- return self.yes_icon
- if col == 7 and book.comments:
- return self.yes_icon
- return NONE
-
- def sort(self, col, order, reset=True):
- if not self.matches:
- return
- descending = order == Qt.DescendingOrder
- self.matches.sort(None,
- lambda x: sort_key(unicode(force_unicode(self.data_as_text(x, col)))),
- descending)
- if reset:
- self.reset()
-
-class FetchMetadata(QDialog, Ui_FetchMetadata):
-
- HANG_TIME = 75 #seconds
-
- queue_reject = pyqtSignal()
-
- def __init__(self, parent, isbn, title, author, publisher, timeout):
- QDialog.__init__(self, parent)
- Ui_FetchMetadata.__init__(self)
- self.setupUi(self)
-
- for fetcher in list(_hung_fetchers):
- if not fetcher.is_alive():
- _hung_fetchers.remove(fetcher)
-
- self.pi = ProgressIndicator(self)
- self.timeout = timeout
- QObject.connect(self.fetch, SIGNAL('clicked()'), self.fetch_metadata)
- self.queue_reject.connect(self.reject, Qt.QueuedConnection)
-
- isbndb_key = get_isbndb_key()
- if not isbndb_key:
- isbndb_key = ''
- self.key.setText(isbndb_key)
-
- self.setWindowTitle(title if title else _('Unknown'))
- self.isbn = isbn
- self.title = title
- self.author = author.strip()
- self.publisher = publisher
- self.previous_row = None
- self.warning.setVisible(False)
- self.connect(self.matches, SIGNAL('activated(QModelIndex)'), self.chosen)
- self.connect(self.matches, SIGNAL('entered(QModelIndex)'),
- self.show_summary)
- self.matches.setMouseTracking(True)
- # Enabling sorting and setting a sort column will not change the initial
- # order of the results, as they are filled in later
- self.matches.setSortingEnabled(True)
- self.matches.horizontalHeader().sectionClicked.connect(self.show_sort_indicator)
- self.matches.horizontalHeader().setSortIndicatorShown(False)
- self.fetch_metadata()
- self.opt_get_social_metadata.setChecked(config['get_social_metadata'])
- self.opt_overwrite_author_title_metadata.setChecked(config['overwrite_author_title_metadata'])
- self.opt_auto_download_cover.setChecked(config['auto_download_cover'])
-
- def show_summary(self, current, *args):
- row = current.row()
- if row != self.previous_row:
- summ = self.model.summary(row)
- self.summary.setText(summ if summ else '')
- self.previous_row = row
-
- def fetch_metadata(self):
- self.warning.setVisible(False)
- key = str(self.key.text())
- if key:
- set_isbndb_key(key)
- else:
- key = None
- title = author = publisher = isbn = None
- if self.isbn:
- isbn = self.isbn
- if self.title:
- title = self.title
- if self.author and not self.author == _('Unknown'):
- author = self.author
- self.fetch.setEnabled(False)
- self.setCursor(Qt.WaitCursor)
- QCoreApplication.instance().processEvents()
- self.fetcher = Fetcher(title, author, publisher, isbn, key)
- self.fetcher.start()
- self.pi.start(_('Finding metadata...'))
- self._hangcheck = QTimer(self)
- self.connect(self._hangcheck, SIGNAL('timeout()'), self.hangcheck,
- Qt.QueuedConnection)
- self.start_time = time.time()
- self._hangcheck.start(100)
-
- def hangcheck(self):
- if self.fetcher.is_alive() and \
- time.time() - self.start_time < self.HANG_TIME:
- return
- self._hangcheck.stop()
- try:
- if self.fetcher.is_alive():
- error_dialog(self, _('Could not find metadata'),
- _('The metadata download seems to have stalled. '
- 'Try again later.')).exec_()
- self.terminate()
- return self.queue_reject.emit()
- self.model = Matches(self.fetcher.results)
- warnings = [(x[0], force_unicode(x[1])) for x in \
- self.fetcher.exceptions if x[1] is not None]
- if warnings:
- warnings='
'.join(['%s: %s'%(name, exc) for name,exc in warnings])
- self.warning.setText(''+ _('Warning')+':'+\
- _('Could not fetch metadata from:')+\
- '
'+warnings+'
')
- self.warning.setVisible(True)
- if self.model.rowCount() < 1:
- info_dialog(self, _('No metadata found'),
- _('No metadata found, try adjusting the title and author '
- 'and/or removing the ISBN.')).exec_()
- self.reject()
- return
-
- self.matches.setModel(self.model)
- QObject.connect(self.matches.selectionModel(),
- SIGNAL('currentRowChanged(QModelIndex, QModelIndex)'),
- self.show_summary)
- self.model.reset()
- self.matches.selectionModel().select(self.model.index(0, 0),
- QItemSelectionModel.Select | QItemSelectionModel.Rows)
- self.matches.setCurrentIndex(self.model.index(0, 0))
- finally:
- self.fetch.setEnabled(True)
- self.unsetCursor()
- self.matches.resizeColumnsToContents()
- self.pi.stop()
-
- def terminate(self):
- if hasattr(self, 'fetcher') and self.fetcher.is_alive():
- _hung_fetchers.add(self.fetcher)
- if hasattr(self, '_hangcheck') and self._hangcheck.isActive():
- self._hangcheck.stop()
- # Save value of auto_download_cover, since this is the only place it can
- # be set. The values of the other options can be set in
- # Preferences->Behavior and should not be set here as they affect bulk
- # downloading as well.
- if self.opt_auto_download_cover.isChecked() != config['auto_download_cover']:
- config.set('auto_download_cover', self.opt_auto_download_cover.isChecked())
-
- def __enter__(self, *args):
- return self
-
- def __exit__(self, *args):
- self.terminate()
-
- def selected_book(self):
- try:
- return self.matches.model().matches[self.matches.currentIndex().row()]
- except:
- return None
-
- def chosen(self, index):
- self.matches.setCurrentIndex(index)
- self.accept()
-
- def show_sort_indicator(self, *args):
- self.matches.horizontalHeader().setSortIndicatorShown(True)
-
diff --git a/src/calibre/gui2/dialogs/fetch_metadata.ui b/src/calibre/gui2/dialogs/fetch_metadata.ui
deleted file mode 100644
index b140fa158d..0000000000
--- a/src/calibre/gui2/dialogs/fetch_metadata.ui
+++ /dev/null
@@ -1,179 +0,0 @@
-
-
- FetchMetadata
-
-
- Qt::WindowModal
-
-
-
- 0
- 0
- 890
- 642
-
-
-
- Fetch metadata
-
-
-
- :/images/metadata.png:/images/metadata.png
-
-
- -
-
-
- <p>calibre can find metadata for your books from two locations: <b>Google Books</b> and <b>isbndb.com</b>. <p>To use isbndb.com you must sign up for a <a href="http://www.isbndb.com">free account</a> and enter your access key below.
-
-
- Qt::AlignCenter
-
-
- true
-
-
- true
-
-
-
- -
-
-
-
-
-
- &Access Key:
-
-
- key
-
-
-
- -
-
-
- -
-
-
- Fetch
-
-
-
-
-
- -
-
-
-
-
-
- true
-
-
-
- -
-
-
- Matches
-
-
-
-
-
-
- Select the book that most closely matches your copy from the list below
-
-
-
- -
-
-
-
- 0
- 1
-
-
-
- true
-
-
- QAbstractItemView::SingleSelection
-
-
- QAbstractItemView::SelectRows
-
-
-
- -
-
-
-
-
-
- -
-
-
- Overwrite author and title with author and title of selected book
-
-
-
- -
-
-
- Download &social metadata (tags/rating/etc.) for the selected book
-
-
-
- -
-
-
- Automatically download the cover, if available
-
-
-
- -
-
-
- QDialogButtonBox::Cancel|QDialogButtonBox::Ok
-
-
-
-
-
-
-
-
-
-
- buttonBox
- accepted()
- FetchMetadata
- accept()
-
-
- 460
- 599
-
-
- 657
- 530
-
-
-
-
- buttonBox
- rejected()
- FetchMetadata
- reject()
-
-
- 417
- 599
-
-
- 0
- 491
-
-
-
-
-
diff --git a/src/calibre/gui2/dialogs/metadata_single.py b/src/calibre/gui2/dialogs/metadata_single.py
deleted file mode 100644
index 4776562c29..0000000000
--- a/src/calibre/gui2/dialogs/metadata_single.py
+++ /dev/null
@@ -1,1031 +0,0 @@
-__license__ = 'GPL v3'
-__copyright__ = '2008, Kovid Goyal '
-
-'''
-The dialog used to edit meta information for a book as well as
-add/remove formats
-'''
-
-import os, re, time, traceback, textwrap
-from functools import partial
-from threading import Thread
-
-from PyQt4.Qt import SIGNAL, QObject, Qt, QTimer, QDate, \
- QPixmap, QListWidgetItem, QDialog, pyqtSignal, QIcon, \
- QPushButton, QKeySequence
-
-from calibre.gui2 import error_dialog, file_icon_provider, dynamic, \
- choose_files, choose_images, ResizableDialog, \
- warning_dialog, question_dialog, UNDEFINED_QDATE
-from calibre.gui2.dialogs.metadata_single_ui import Ui_MetadataSingleDialog
-from calibre.gui2.dialogs.fetch_metadata import FetchMetadata
-from calibre.gui2.dialogs.tag_editor import TagEditor
-from calibre.gui2.widgets import ProgressIndicator
-from calibre.ebooks import BOOK_EXTENSIONS
-from calibre.ebooks.metadata import string_to_authors, \
- authors_to_string, check_isbn, title_sort
-from calibre.ebooks.metadata.covers import download_cover
-from calibre.ebooks.metadata import MetaInformation
-from calibre.utils.config import prefs, tweaks
-from calibre.utils.date import qt_to_dt, local_tz, utcfromtimestamp
-from calibre.utils.icu import sort_key
-from calibre.customize.ui import run_plugins_on_import, get_isbndb_key
-from calibre.gui2.preferences.social import SocialMetadata
-from calibre.gui2.custom_column_widgets import populate_metadata_page
-from calibre import strftime
-from calibre.library.comments import comments_to_html
-
-class CoverFetcher(Thread): # {{{
-
- def __init__(self, username, password, isbn, timeout, title, author):
- Thread.__init__(self)
- self.daemon = True
-
- self.username = username.strip() if username else username
- self.password = password.strip() if password else password
- self.timeout = timeout
- self.isbn = isbn
- self.title = title
- self.needs_isbn = False
- self.author = author
- self.exception = self.traceback = self.cover_data = self.errors = None
-
- def run(self):
- try:
- au = self.author if self.author else None
- mi = MetaInformation(self.title, [au])
- if not self.isbn:
- from calibre.ebooks.metadata.fetch import search
- if not self.title:
- self.needs_isbn = True
- return
- key = get_isbndb_key()
- if not key:
- key = None
- results = search(title=self.title, author=au,
- isbndb_key=key)[0]
- results = sorted([x.isbn for x in results if x.isbn],
- cmp=lambda x,y:cmp(len(x),len(y)), reverse=True)
- if not results:
- self.needs_isbn = True
- return
- self.isbn = results[0]
-
- mi.isbn = self.isbn
-
- self.cover_data, self.errors = download_cover(mi,
- timeout=self.timeout)
- except Exception as e:
- self.exception = e
- self.traceback = traceback.format_exc()
- print self.traceback
-
-# }}}
-
-class Format(QListWidgetItem): # {{{
-
- def __init__(self, parent, ext, size, path=None, timestamp=None):
- self.path = path
- self.ext = ext
- self.size = float(size)/(1024*1024)
- text = '%s (%.2f MB)'%(self.ext.upper(), self.size)
- QListWidgetItem.__init__(self, file_icon_provider().icon_from_ext(ext),
- text, parent, QListWidgetItem.UserType)
- if timestamp is not None:
- ts = timestamp.astimezone(local_tz)
- t = strftime('%a, %d %b %Y [%H:%M:%S]', ts.timetuple())
- text = _('Last modified: %s')%t
- self.setToolTip(text)
- self.setStatusTip(text)
-
-# }}}
-
-
-class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
-
- COVER_FETCH_TIMEOUT = 240 # seconds
- view_format = pyqtSignal(object)
-
- # Cover processing {{{
-
- def set_cover(self):
- mi, ext = self.get_selected_format_metadata()
- if mi is None:
- return
- cdata = None
- if mi.cover and os.access(mi.cover, os.R_OK):
- cdata = open(mi.cover).read()
- elif mi.cover_data[1] is not None:
- cdata = mi.cover_data[1]
- if cdata is None:
- error_dialog(self, _('Could not read cover'),
- _('Could not read cover from %s format')%ext).exec_()
- return
- pix = QPixmap()
- pix.loadFromData(cdata)
- if pix.isNull():
- error_dialog(self, _('Could not read cover'),
- _('The cover in the %s format is invalid')%ext).exec_()
- return
- self.cover.setPixmap(pix)
- self.update_cover_tooltip()
- self.cover_changed = True
- self.cpixmap = pix
- self.cover_data = cdata
-
- def trim_cover(self, *args):
- from calibre.utils.magick import Image
- cdata = self.cover_data
- if not cdata:
- return
- im = Image()
- im.load(cdata)
- im.trim(10)
- cdata = im.export('png')
- pix = QPixmap()
- pix.loadFromData(cdata)
- self.cover.setPixmap(pix)
- self.update_cover_tooltip()
- self.cover_changed = True
- self.cpixmap = pix
- self.cover_data = cdata
-
-
-
- def update_cover_tooltip(self):
- p = self.cover.pixmap()
- self.cover.setToolTip(_('Cover size: %dx%d pixels') %
- (p.width(), p.height()))
-
-
- def do_reset_cover(self, *args):
- pix = QPixmap(I('default_cover.png'))
- self.cover.setPixmap(pix)
- self.update_cover_tooltip()
- self.cover_changed = True
- self.cover_data = None
-
- def select_cover(self, checked):
- files = choose_images(self, 'change cover dialog',
- _('Choose cover for ') + unicode(self.title.text()))
- if not files:
- return
- _file = files[0]
- if _file:
- _file = os.path.abspath(_file)
- if not os.access(_file, os.R_OK):
- d = error_dialog(self, _('Cannot read'),
- _('You do not have permission to read the file: ') + _file)
- d.exec_()
- return
- cf, cover = None, None
- try:
- cf = open(_file, "rb")
- cover = cf.read()
- except IOError as e:
- d = error_dialog(self, _('Error reading file'),
- _("There was an error reading from file:
") + _file + "
"+str(e))
- d.exec_()
- if cover:
- pix = QPixmap()
- pix.loadFromData(cover)
- if pix.isNull():
- d = error_dialog(self,
- _("Not a valid picture"),
- _file + _(" is not a valid picture"))
- d.exec_()
- else:
- self.cover.setPixmap(pix)
- self.update_cover_tooltip()
- self.cover_changed = True
- self.cpixmap = pix
- self.cover_data = cover
-
- def generate_cover(self, *args):
- from calibre.ebooks import calibre_cover
- from calibre.ebooks.metadata import fmt_sidx
- from calibre.gui2 import config
- title = unicode(self.title.text()).strip()
- author = unicode(self.authors.text()).strip()
- if author.endswith('&'):
- author = author[:-1].strip()
- if not title or not author:
- return error_dialog(self, _('Specify title and author'),
- _('You must specify a title and author before generating '
- 'a cover'), show=True)
- series = unicode(self.series.text()).strip()
- series_string = None
- if series:
- series_string = _('Book %s of %s')%(
- fmt_sidx(self.series_index.value(),
- use_roman=config['use_roman_numerals_for_series_number']), series)
- self.cover_data = calibre_cover(title, author,
- series_string=series_string)
- pix = QPixmap()
- pix.loadFromData(self.cover_data)
- self.cover.setPixmap(pix)
- self.update_cover_tooltip()
- self.cover_changed = True
- self.cpixmap = pix
-
- def cover_dropped(self, cover_data):
- self.cover_changed = True
- self.cover_data = cover_data
- self.update_cover_tooltip()
-
- def fetch_cover(self):
- isbn = re.sub(r'[^0-9a-zA-Z]', '', unicode(self.isbn.text())).strip()
- self.fetch_cover_button.setEnabled(False)
- self.setCursor(Qt.WaitCursor)
- title, author = map(unicode, (self.title.text(), self.authors.text()))
- self.cover_fetcher = CoverFetcher(None, None, isbn,
- self.timeout, title, author)
- self.cover_fetcher.start()
- self.cf_start_time = time.time()
- self.pi.start(_('Downloading cover...'))
- QTimer.singleShot(100, self.hangcheck)
-
- def hangcheck(self):
- cf = self.cover_fetcher
- if cf is None:
- # Called after dialog closed
- return
-
- if cf.is_alive() and \
- time.time()-self.cf_start_time < self.COVER_FETCH_TIMEOUT:
- QTimer.singleShot(100, self.hangcheck)
- return
-
- try:
- if cf.is_alive():
- error_dialog(self, _('Cannot fetch cover'),
- _('Could not fetch cover.
')+
- _('The download timed out.')).exec_()
- return
- if cf.needs_isbn:
- error_dialog(self, _('Cannot fetch cover'),
- _('Could not find cover for this book. Try '
- 'specifying the ISBN first.')).exec_()
- return
- if cf.exception is not None:
- err = cf.exception
- error_dialog(self, _('Cannot fetch cover'),
- _('Could not fetch cover.
')+unicode(err)).exec_()
- return
- if cf.errors and cf.cover_data is None:
- details = u'\n\n'.join([e[-1] + ': ' + e[1] for e in cf.errors])
- error_dialog(self, _('Cannot fetch cover'),
- _('Could not fetch cover.
') +
- _('For the error message from each cover source, '
- 'click Show details below.'), det_msg=details, show=True)
- return
-
- pix = QPixmap()
- pix.loadFromData(cf.cover_data)
- if pix.isNull():
- error_dialog(self, _('Bad cover'),
- _('The cover is not a valid picture')).exec_()
- else:
- self.cover.setPixmap(pix)
- self.update_cover_tooltip()
- self.cover_changed = True
- self.cpixmap = pix
- self.cover_data = cf.cover_data
- finally:
- self.fetch_cover_button.setEnabled(True)
- self.unsetCursor()
- if self.pi is not None:
- self.pi.stop()
-
-
- # }}}
-
- # Formats processing {{{
- def add_format(self, x):
- files = choose_files(self, 'add formats dialog',
- _("Choose formats for ") + unicode((self.title.text())),
- [(_('Books'), BOOK_EXTENSIONS)])
- self._add_formats(files)
-
- def _add_formats(self, paths):
- added = False
- if not paths:
- return added
- bad_perms = []
- for _file in paths:
- _file = os.path.abspath(_file)
- if not os.access(_file, os.R_OK):
- bad_perms.append(_file)
- continue
-
- nfile = run_plugins_on_import(_file)
- if nfile is not None:
- _file = nfile
- stat = os.stat(_file)
- size = stat.st_size
- ext = os.path.splitext(_file)[1].lower().replace('.', '')
- timestamp = utcfromtimestamp(stat.st_mtime)
- for row in range(self.formats.count()):
- fmt = self.formats.item(row)
- if fmt.ext.lower() == ext:
- self.formats.takeItem(row)
- break
- Format(self.formats, ext, size, path=_file, timestamp=timestamp)
- self.formats_changed = True
- added = True
- if bad_perms:
- error_dialog(self, _('No permission'),
- _('You do not have '
- 'permission to read the following files:'),
- det_msg='\n'.join(bad_perms), show=True)
-
- return added
-
- def formats_dropped(self, event, paths):
- if self._add_formats(paths):
- event.accept()
-
- def remove_format(self, *args):
- rows = self.formats.selectionModel().selectedRows(0)
- for row in rows:
- self.formats.takeItem(row.row())
- self.formats_changed = True
-
- def get_selected_format_metadata(self):
- from calibre.ebooks.metadata.meta import get_metadata
- old = prefs['read_file_metadata']
- if not old:
- prefs['read_file_metadata'] = True
- try:
- row = self.formats.currentRow()
- fmt = self.formats.item(row)
- if fmt is None:
- if self.formats.count() == 1:
- fmt = self.formats.item(0)
- if fmt is None:
- error_dialog(self, _('No format selected'),
- _('No format selected')).exec_()
- return None, None
- ext = fmt.ext.lower()
- if fmt.path is None:
- stream = self.db.format(self.row, ext, as_file=True)
- else:
- stream = open(fmt.path, 'r+b')
- try:
- mi = get_metadata(stream, ext)
- return mi, ext
- except:
- error_dialog(self, _('Could not read metadata'),
- _('Could not read metadata from %s format')%ext).exec_()
- return None, None
- finally:
- if old != prefs['read_file_metadata']:
- prefs['read_file_metadata'] = old
-
- def set_metadata_from_format(self):
- mi, ext = self.get_selected_format_metadata()
- if mi is None:
- return
- if mi.title:
- self.title.setText(mi.title)
- if mi.authors:
- self.authors.setEditText(authors_to_string(mi.authors))
- if mi.author_sort:
- self.author_sort.setText(mi.author_sort)
- if mi.rating is not None:
- try:
- self.rating.setValue(mi.rating)
- except:
- pass
- if mi.publisher:
- self.publisher.setEditText(mi.publisher)
- if mi.tags:
- self.tags.setText(', '.join(mi.tags))
- if mi.isbn:
- self.isbn.setText(mi.isbn)
- if mi.pubdate:
- self.pubdate.setDate(QDate(mi.pubdate.year, mi.pubdate.month,
- mi.pubdate.day))
- if mi.series and mi.series.strip():
- self.series.setEditText(mi.series)
- if mi.series_index is not None:
- self.series_index.setValue(float(mi.series_index))
- if mi.comments and mi.comments.strip():
- comments = comments_to_html(mi.comments)
- self.comments.html = comments
-
-
- def sync_formats(self):
- old_extensions, new_extensions, paths = set(), set(), {}
- for row in range(self.formats.count()):
- fmt = self.formats.item(row)
- ext, path = fmt.ext.lower(), fmt.path
- if 'unknown' in ext.lower():
- ext = None
- if path:
- new_extensions.add(ext)
- paths[ext] = path
- else:
- old_extensions.add(ext)
- for ext in new_extensions:
- self.db.add_format(self.row, ext, open(paths[ext], 'rb'), notify=False)
- dbfmts = self.db.formats(self.row)
- db_extensions = set([f.lower() for f in (dbfmts.split(',') if dbfmts
- else [])])
- extensions = new_extensions.union(old_extensions)
- for ext in db_extensions:
- if ext not in extensions and ext in self.original_formats:
- self.db.remove_format(self.row, ext, notify=False)
-
- def show_format(self, item, *args):
- fmt = item.ext
- self.view_format.emit(fmt)
-
- # }}}
-
- def __init__(self, window, row, db, prev=None,
- next_=None):
- ResizableDialog.__init__(self, window)
- self.cover_fetcher = None
- self.bc_box.layout().setAlignment(self.cover, Qt.AlignCenter|Qt.AlignHCenter)
- base = unicode(self.author_sort.toolTip())
- ok_tooltip = '' + textwrap.fill(base+'
'+
- _(' The green color indicates that the current '
- 'author sort matches the current author'))
- bad_tooltip = '
'+textwrap.fill(base + '
'+
- _(' The red color indicates that the current '
- 'author sort does not match the current author. '
- 'No action is required if this is what you want.'))
- self.aus_tooltips = (ok_tooltip, bad_tooltip)
-
- base = unicode(self.title_sort.toolTip())
- ok_tooltip = '
' + textwrap.fill(base+'
'+
- _(' The green color indicates that the current '
- 'title sort matches the current title'))
- bad_tooltip = '
'+textwrap.fill(base + '
'+
- _(' The red color warns that the current '
- 'title sort does not match the current title. '
- 'No action is required if this is what you want.'))
- self.ts_tooltips = (ok_tooltip, bad_tooltip)
- self.row_delta = 0
- if prev:
- self.prev_button = QPushButton(QIcon(I('back.png')), _('Previous'),
- self)
- self.button_box.addButton(self.prev_button, self.button_box.ActionRole)
- tip = (_('Save changes and edit the metadata of %s')+' [Alt+Left]')%prev
- self.prev_button.setToolTip(tip)
- self.prev_button.clicked.connect(partial(self.next_triggered,
- -1))
- self.prev_button.setShortcut(QKeySequence('Alt+Left'))
- if next_:
- self.next_button = QPushButton(QIcon(I('forward.png')), _('Next'),
- self)
- self.button_box.addButton(self.next_button, self.button_box.ActionRole)
- tip = (_('Save changes and edit the metadata of %s')+' [Alt+Right]')%next_
- self.next_button.setToolTip(tip)
- self.next_button.clicked.connect(partial(self.next_triggered, 1))
- self.next_button.setShortcut(QKeySequence('Alt+Right'))
-
- self.splitter.setStretchFactor(100, 1)
- self.read_state()
- self.db = db
- self.pi = ProgressIndicator(self)
- self.id = db.id(row)
- self.row = row
- self.cover_data = None
- self.formats_changed = False
- self.formats.setAcceptDrops(True)
- self.cover_changed = False
- self.cpixmap = None
- self.pubdate.setMinimumDate(UNDEFINED_QDATE)
- pubdate_format = tweaks['gui_pubdate_display_format']
- if pubdate_format is not None:
- self.pubdate.setDisplayFormat(pubdate_format)
- self.date.setMinimumDate(UNDEFINED_QDATE)
- self.pubdate.setSpecialValueText(_('Undefined'))
- self.date.setSpecialValueText(_('Undefined'))
- self.clear_pubdate_button.clicked.connect(self.clear_pubdate)
-
-
- self.connect(self.cover, SIGNAL('cover_changed(PyQt_PyObject)'), self.cover_dropped)
- QObject.connect(self.cover_button, SIGNAL("clicked(bool)"), \
- self.select_cover)
- QObject.connect(self.add_format_button, SIGNAL("clicked(bool)"), \
- self.add_format)
- self.connect(self.formats,
- SIGNAL('formats_dropped(PyQt_PyObject,PyQt_PyObject)'),
- self.formats_dropped)
- QObject.connect(self.remove_format_button, SIGNAL("clicked(bool)"), \
- self.remove_format)
- QObject.connect(self.fetch_metadata_button, SIGNAL('clicked()'),
- self.fetch_metadata)
-
- QObject.connect(self.fetch_cover_button, SIGNAL('clicked()'),
- self.fetch_cover)
- QObject.connect(self.tag_editor_button, SIGNAL('clicked()'),
- self.edit_tags)
- QObject.connect(self.remove_series_button, SIGNAL('clicked()'),
- self.remove_unused_series)
- QObject.connect(self.auto_author_sort, SIGNAL('clicked()'),
- self.deduce_author_sort)
- QObject.connect(self.auto_title_sort, SIGNAL('clicked()'),
- self.deduce_title_sort)
- self.trim_cover_button.clicked.connect(self.trim_cover)
- self.connect(self.title_sort, SIGNAL('textChanged(const QString&)'),
- self.title_sort_box_changed)
- self.connect(self.title, SIGNAL('textChanged(const QString&)'),
- self.title_box_changed)
- self.connect(self.author_sort, SIGNAL('textChanged(const QString&)'),
- self.author_sort_box_changed)
- self.connect(self.authors, SIGNAL('editTextChanged(const QString&)'),
- self.authors_box_changed)
- self.connect(self.formats, SIGNAL('itemDoubleClicked(QListWidgetItem*)'),
- self.show_format)
- self.connect(self.formats, SIGNAL('delete_format()'), self.remove_format)
- self.connect(self.button_set_cover, SIGNAL('clicked()'), self.set_cover)
- self.connect(self.button_set_metadata, SIGNAL('clicked()'),
- self.set_metadata_from_format)
- self.connect(self.reset_cover, SIGNAL('clicked()'), self.do_reset_cover)
- self.connect(self.swap_button, SIGNAL('clicked()'), self.swap_title_author)
- self.timeout = float(prefs['network_timeout'])
-
-
- self.title.setText(db.title(row))
- self.title_sort.setText(db.title_sort(row))
- isbn = db.isbn(self.id, index_is_id=True)
- if not isbn:
- isbn = ''
- self.isbn.textChanged.connect(self.validate_isbn)
- self.isbn.setText(isbn)
- aus = self.db.author_sort(row)
- self.author_sort.setText(aus if aus else '')
- tags = self.db.tags(row)
- self.original_tags = ', '.join(tags.split(',')) if tags else ''
- self.tags.setText(self.original_tags)
- self.tags.update_items_cache(self.db.all_tags())
- rating = self.db.rating(row)
- if rating > 0:
- self.rating.setValue(int(rating/2.))
- comments = self.db.comments(row)
- if comments and comments.strip():
- comments = comments_to_html(comments)
- self.comments.html = comments
- cover = self.db.cover(row)
- pubdate = db.pubdate(self.id, index_is_id=True)
- self.pubdate.setDate(QDate(pubdate.year, pubdate.month,
- pubdate.day))
- timestamp = db.timestamp(self.id, index_is_id=True)
- self.date.setDate(QDate(timestamp.year, timestamp.month,
- timestamp.day))
- self.orig_date = qt_to_dt(self.date.date())
-
- exts = self.db.formats(row)
- self.original_formats = []
- if exts:
- exts = exts.split(',')
- for ext in exts:
- if not ext:
- ext = ''
- size = self.db.sizeof_format(row, ext)
- timestamp = self.db.format_last_modified(self.id, ext)
- if size is None:
- continue
- Format(self.formats, ext, size, timestamp=timestamp)
- self.original_formats.append(ext.lower())
-
-
- self.initialize_combos()
- si = self.db.series_index(row)
- if si is None:
- si = 1.0
- try:
- self.series_index.setValue(float(si))
- except:
- self.series_index.setValue(1.0)
- QObject.connect(self.series, SIGNAL('currentIndexChanged(int)'), self.enable_series_index)
- QObject.connect(self.series, SIGNAL('editTextChanged(QString)'), self.enable_series_index)
- self.series.lineEdit().editingFinished.connect(self.increment_series_index)
-
- pm = QPixmap()
- if cover:
- pm.loadFromData(cover)
- if pm.isNull():
- pm = QPixmap(I('default_cover.png'))
- else:
- self.cover_data = cover
- self.cover.setPixmap(pm)
- self.update_cover_tooltip()
- self.original_series_name = unicode(self.series.text()).strip()
- if len(db.custom_column_label_map) == 0:
- self.central_widget.tabBar().setVisible(False)
- self.central_widget.setTabEnabled(1, False)
- else:
- self.create_custom_column_editors()
- self.generate_cover_button.clicked.connect(self.generate_cover)
-
- self.original_author = unicode(self.authors.text()).strip()
- self.original_title = unicode(self.title.text()).strip()
- self.books_to_refresh = set()
-
- self.show()
-
- def clear_pubdate(self, *args):
- self.pubdate.setDate(UNDEFINED_QDATE)
-
- def create_custom_column_editors(self):
- w = self.central_widget.widget(1)
- layout = w.layout()
- self.custom_column_widgets, self.__cc_spacers = \
- populate_metadata_page(layout, self.db, self.id, parent=w, bulk=False,
- two_column=tweaks['metadata_single_use_2_cols_for_custom_fields'])
- self.__custom_col_layouts = [layout]
- ans = self.custom_column_widgets
- for i in range(len(ans)-1):
- if len(ans[i+1].widgets) == 2:
- w.setTabOrder(ans[i].widgets[-1], ans[i+1].widgets[1])
- else:
- w.setTabOrder(ans[i].widgets[-1], ans[i+1].widgets[0])
- for c in range(2, len(ans[i].widgets), 2):
- w.setTabOrder(ans[i].widgets[c-1], ans[i].widgets[c+1])
-
- def title_box_changed(self, txt):
- ts = unicode(txt)
- ts = title_sort(ts)
- self.mark_box_as_ok(control = self.title_sort, tt=self.ts_tooltips,
- normal=(unicode(self.title_sort.text()) == ts))
-
- def title_sort_box_changed(self, txt):
- ts = unicode(txt)
- self.mark_box_as_ok(control = self.title_sort, tt=self.ts_tooltips,
- normal=(title_sort(unicode(self.title.text())) == ts))
-
- def authors_box_changed(self, txt):
- aus = unicode(txt)
- aus = re.sub(r'\s+et al\.$', '', aus)
- aus = self.db.author_sort_from_authors(string_to_authors(aus))
- self.mark_box_as_ok(control = self.author_sort, tt=self.aus_tooltips,
- normal=(unicode(self.author_sort.text()) == aus))
-
- def author_sort_box_changed(self, txt):
- au = unicode(self.authors.text())
- au = re.sub(r'\s+et al\.$', '', au)
- au = self.db.author_sort_from_authors(string_to_authors(au))
- self.mark_box_as_ok(control = self.author_sort, tt=self.aus_tooltips,
- normal=(au == txt))
-
- def mark_box_as_ok(self, control, tt, normal=True):
- if normal:
- col = 'rgb(0, 255, 0, 20%)'
- else:
- col = 'rgb(255, 0, 0, 20%)'
- control.setStyleSheet('QLineEdit { color: black; '
- 'background-color: %s; }'%col)
- tt = tt[0] if normal else tt[1]
- control.setToolTip(tt)
-
- def validate_isbn(self, isbn):
- isbn = unicode(isbn).strip()
- if not isbn:
- self.isbn.setStyleSheet('QLineEdit { background-color: rgba(0,255,0,0%) }')
- self.isbn.setToolTip(_('This ISBN number is valid'))
- return
-
- if check_isbn(isbn):
- self.isbn.setStyleSheet('QLineEdit { background-color: rgba(0,255,0,20%) }')
- self.isbn.setToolTip(_('This ISBN number is valid'))
- else:
- self.isbn.setStyleSheet('QLineEdit { background-color: rgba(255,0,0,20%) }')
- self.isbn.setToolTip(_('This ISBN number is invalid'))
-
- def deduce_author_sort(self):
- au = unicode(self.authors.text())
- au = re.sub(r'\s+et al\.$', '', au)
- authors = string_to_authors(au)
- self.author_sort.setText(self.db.author_sort_from_authors(authors))
-
- def deduce_title_sort(self):
- ts = unicode(self.title.text())
- self.title_sort.setText(title_sort(ts))
-
- def swap_title_author(self):
- title = self.title.text()
- self.title.setText(self.authors.text())
- self.authors.setText(title)
- self.deduce_author_sort()
- self.deduce_title_sort()
-
- def initialize_combos(self):
- self.initalize_authors()
- self.initialize_series()
- self.initialize_publisher()
-
- self.layout().activate()
-
- def initalize_authors(self):
- all_authors = self.db.all_authors()
- all_authors.sort(key=lambda x : sort_key(x[1]))
- for i in all_authors:
- id, name = i
- name = [name.strip().replace('|', ',') for n in name.split(',')]
- self.authors.addItem(authors_to_string(name))
-
- au = self.db.authors(self.row)
- if not au:
- au = _('Unknown')
- au = ' & '.join([a.strip().replace('|', ',') for a in au.split(',')])
- self.authors.setEditText(au)
-
- self.authors.set_separator('&')
- self.authors.set_space_before_sep(True)
- self.authors.set_add_separator(tweaks['authors_completer_append_separator'])
- self.authors.update_items_cache(self.db.all_author_names())
-
- def initialize_series(self):
- self.series.setSizeAdjustPolicy(self.series.AdjustToContentsOnFirstShow)
- all_series = self.db.all_series()
- all_series.sort(key=lambda x : sort_key(x[1]))
- self.series.set_separator(None)
- self.series.update_items_cache([x[1] for x in all_series])
- series_id = self.db.series_id(self.row)
- idx, c = None, 0
- for i in all_series:
- id, name = i
- if id == series_id:
- idx = c
- self.series.addItem(name)
- c += 1
-
- self.series.lineEdit().setText('')
- if idx is not None:
- self.series.setCurrentIndex(idx)
- self.enable_series_index()
-
- def initialize_publisher(self):
- all_publishers = self.db.all_publishers()
- all_publishers.sort(key=lambda x : sort_key(x[1]))
- self.publisher.set_separator(None)
- self.publisher.update_items_cache([x[1] for x in all_publishers])
- publisher_id = self.db.publisher_id(self.row)
- idx, c = None, 0
- for i in all_publishers:
- id, name = i
- if id == publisher_id:
- idx = c
- self.publisher.addItem(name)
- c += 1
-
- self.publisher.setEditText('')
- if idx is not None:
- self.publisher.setCurrentIndex(idx)
-
- def edit_tags(self):
- if self.tags.text() != self.original_tags:
- if question_dialog(self, _('Tags changed'),
- _('You have changed the tags. In order to use the tags'
- ' editor, you must either discard or apply these '
- 'changes. Apply changes?'), show_copy_button=False):
- self.books_to_refresh |= self.apply_tags(commit=True,
- notify=True)
- self.original_tags = unicode(self.tags.text())
- else:
- self.tags.setText(self.original_tags)
- d = TagEditor(self, self.db, self.id)
- d.exec_()
- if d.result() == QDialog.Accepted:
- tag_string = ', '.join(d.tags)
- self.tags.setText(tag_string)
- self.tags.update_items_cache(self.db.all_tags())
-
-
- def fetch_metadata(self):
- isbn = re.sub(r'[^0-9a-zA-Z]', '', unicode(self.isbn.text()))
- title = unicode(self.title.text())
- try:
- author = string_to_authors(unicode(self.authors.text()))[0]
- except:
- author = ''
- publisher = unicode(self.publisher.currentText())
- if isbn or title or author or publisher:
- d = FetchMetadata(self, isbn, title, author, publisher, self.timeout)
- self._fetch_metadata_scope = d
- with d:
- if d.exec_() == QDialog.Accepted:
- book = d.selected_book()
- if book:
- if d.opt_get_social_metadata.isChecked():
- d2 = SocialMetadata(book, self)
- d2.exec_()
- if d2.timed_out:
- warning_dialog(self, _('Timed out'),
- _('The download of social'
- ' metadata timed out, the servers are'
- ' probably busy. Try again later.'),
- show=True)
- elif d2.exceptions:
- det = '\n'.join([x[0]+'\n\n'+x[-1]+'\n\n\n' for
- x in d2.exceptions])
- warning_dialog(self, _('There were errors'),
- _('There were errors downloading social metadata'),
- det_msg=det, show=True)
- else:
- book.tags = []
- if d.opt_overwrite_author_title_metadata.isChecked():
- self.title.setText(book.title)
- self.authors.setText(authors_to_string(book.authors))
- if book.author_sort: self.author_sort.setText(book.author_sort)
- if book.publisher: self.publisher.setEditText(book.publisher)
- if book.isbn: self.isbn.setText(book.isbn)
- if book.pubdate:
- dt = book.pubdate
- self.pubdate.setDate(QDate(dt.year, dt.month, dt.day))
- summ = book.comments
- if summ:
- prefix = self.comments.html
- if prefix:
- prefix += '\n'
- self.comments.html = prefix + comments_to_html(summ)
- if book.rating is not None:
- self.rating.setValue(int(book.rating))
- if book.tags:
- self.tags.setText(', '.join(book.tags))
- if book.series is not None:
- if self.series.text() is None or self.series.text() == '':
- self.series.setText(book.series)
- if book.series_index is not None:
- self.series_index.setValue(book.series_index)
- if book.has_cover:
- if d.opt_auto_download_cover.isChecked():
- self.fetch_cover()
- else:
- self.fetch_cover_button.setFocus(Qt.OtherFocusReason)
- else:
- error_dialog(self, _('Cannot fetch metadata'),
- _('You must specify at least one of ISBN, Title, '
- 'Authors or Publisher'), show=True)
- self.title.setFocus(Qt.OtherFocusReason)
-
- def enable_series_index(self, *args):
- self.series_index.setEnabled(True)
-
- def increment_series_index(self):
- if self.db is not None:
- try:
- series = unicode(self.series.text()).strip()
- if series and series != self.original_series_name:
- ns = 1
- if tweaks['series_index_auto_increment'] != 'const':
- ns = self.db.get_next_series_num_for(series)
- self.series_index.setValue(ns)
- self.original_series_name = series
- except:
- traceback.print_exc()
-
- def remove_unused_series(self):
- self.db.remove_unused_series()
- idx = unicode(self.series.currentText())
- self.series.clear()
- self.initialize_series()
- if idx:
- for i in range(self.series.count()):
- if unicode(self.series.itemText(i)) == idx:
- self.series.setCurrentIndex(i)
- break
-
- def apply_tags(self, commit=False, notify=False):
- return self.db.set_tags(self.id, [x.strip() for x in
- unicode(self.tags.text()).split(',')],
- notify=notify, commit=commit, allow_case_change=True)
-
- def next_triggered(self, row_delta, *args):
- self.row_delta = row_delta
- self.accept()
-
- def accept(self):
- try:
- if self.formats_changed:
- self.sync_formats()
- title = unicode(self.title.text()).strip()
- if title != self.original_title:
- self.db.set_title(self.id, title, notify=False)
- # This must be after setting the title because of the DB update trigger
- ts = unicode(self.title_sort.text()).strip()
- if ts:
- self.db.set_title_sort(self.id, ts, notify=False, commit=False)
- au = unicode(self.authors.text()).strip()
- if au and au != self.original_author:
- self.books_to_refresh |= self.db.set_authors(self.id,
- string_to_authors(au),
- notify=False,
- allow_case_change=True)
- aus = unicode(self.author_sort.text()).strip()
- if aus:
- self.db.set_author_sort(self.id, aus, notify=False, commit=False)
- self.db.set_isbn(self.id,
- re.sub(r'[^0-9a-zA-Z]', '',
- unicode(self.isbn.text()).strip()),
- notify=False, commit=False)
- self.db.set_rating(self.id, 2*self.rating.value(), notify=False,
- commit=False)
- self.books_to_refresh |= self.apply_tags()
- self.books_to_refresh |= self.db.set_publisher(self.id,
- unicode(self.publisher.currentText()).strip(),
- notify=False, commit=False, allow_case_change=True)
- self.books_to_refresh |= self.db.set_series(self.id,
- unicode(self.series.currentText()).strip(), notify=False,
- commit=False, allow_case_change=True)
- self.db.set_series_index(self.id, self.series_index.value(),
- notify=False, commit=False)
- self.db.set_comment(self.id,
- self.comments.html,
- notify=False, commit=False)
- d = self.pubdate.date()
- d = qt_to_dt(d)
- self.db.set_pubdate(self.id, d, notify=False, commit=False)
- d = self.date.date()
- d = qt_to_dt(d)
- if d != self.orig_date:
- self.db.set_timestamp(self.id, d, notify=False, commit=False)
- self.db.commit()
-
- if self.cover_changed:
- if self.cover_data is not None:
- self.db.set_cover(self.id, self.cover_data)
- else:
- self.db.remove_cover(self.id)
- for w in getattr(self, 'custom_column_widgets', []):
- self.books_to_refresh |= w.commit(self.id)
- self.db.commit()
- except (IOError, OSError) as err:
- if getattr(err, 'errno', -1) == 13: # Permission denied
- fname = err.filename if err.filename else 'file'
- return error_dialog(self, _('Permission denied'),
- _('Could not open %s. Is it being used by another'
- ' program?')%fname, det_msg=traceback.format_exc(),
- show=True)
- raise
- self.save_state()
- self.cover_fetcher = None
- QDialog.accept(self)
-
- def reject(self, *args):
- self.save_state()
- self.cover_fetcher = None
- QDialog.reject(self, *args)
-
- def read_state(self):
- wg = dynamic.get('metasingle_window_geometry2', None)
- ss = dynamic.get('metasingle_splitter_state2', None)
- if wg is not None:
- self.restoreGeometry(wg)
- if ss is not None:
- self.splitter.restoreState(ss)
-
- def save_state(self):
- dynamic.set('metasingle_window_geometry2', bytes(self.saveGeometry()))
- dynamic.set('metasingle_splitter_state2',
- bytes(self.splitter.saveState()))
-
- def break_cycles(self):
- # Break any reference cycles that could prevent python
- # from garbage collecting this dialog
- def disconnect(signal):
- try:
- signal.disconnect()
- except:
- pass # Fails if view format was never connected
- disconnect(self.view_format)
- for b in ('next_button', 'prev_button'):
- x = getattr(self, b, None)
- if x is not None:
- disconnect(x.clicked)
-
-if __name__ == '__main__':
- from calibre.library import db
- from PyQt4.Qt import QApplication
- from calibre.utils.mem import memory
- import gc
-
-
- app = QApplication([])
- db = db()
-
- # Initialize all Qt Objects once
- d = MetadataSingleDialog(None, 4, db)
- d.break_cycles()
- d.reject()
- del d
-
- for i in range(5):
- gc.collect()
- before = memory()
-
- d = MetadataSingleDialog(None, 4, db)
- d.reject()
- d.break_cycles()
- del d
-
- for i in range(5):
- gc.collect()
- print 'Used memory:', memory(before)/1024.**2, 'MB'
-
-
diff --git a/src/calibre/gui2/dialogs/metadata_single.ui b/src/calibre/gui2/dialogs/metadata_single.ui
deleted file mode 100644
index ced5030f94..0000000000
--- a/src/calibre/gui2/dialogs/metadata_single.ui
+++ /dev/null
@@ -1,937 +0,0 @@
-
-
- MetadataSingleDialog
-
-
-
- 0
- 0
- 994
- 716
-
-
-
-
- 0
- 0
-
-
-
- Edit Meta Information
-
-
-
- :/images/edit_input.png:/images/edit_input.png
-
-
- true
-
-
- true
-
-
- -
-
-
- QFrame::NoFrame
-
-
- true
-
-
-
-
- 0
- 0
- 986
- 677
-
-
-
-
- 0
-
-
-
-
-
-
- 800
- 665
-
-
-
- 0
-
-
-
- &Basic metadata
-
-
-
-
-
-
- Qt::Horizontal
-
-
-
-
-
-
-
- Meta information
-
-
-
-
-
-
- &Title:
-
-
- Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
-
-
- title
-
-
-
- -
-
-
- Change the title of this book
-
-
-
- -
-
-
-
-
-
- Qt::Vertical
-
-
-
- 20
- 40
-
-
-
-
- -
-
-
- Automatically create the title sort entry based on the current title entry.
-Using this button to create title sort will change title sort from red to green.
-
-
- ...
-
-
-
- :/images/auto_author_sort.png:/images/auto_author_sort.png
-
-
-
- -
-
-
- Qt::Vertical
-
-
-
- 20
- 40
-
-
-
-
- -
-
-
- Swap the author and title
-
-
- ...
-
-
-
- :/images/swap.png:/images/swap.png
-
-
-
- 16
- 16
-
-
-
-
- -
-
-
- Qt::Vertical
-
-
-
- 20
- 40
-
-
-
-
- -
-
-
- Automatically create the author sort entry based on the current author entry.
-Using this button to create author sort will change author sort from red to green.
-
-
- ...
-
-
-
- :/images/auto_author_sort.png:/images/auto_author_sort.png
-
-
-
- -
-
-
- Qt::Vertical
-
-
-
- 20
- 40
-
-
-
-
-
-
- -
-
-
- Title &sort:
-
-
- Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
-
-
- title_sort
-
-
-
- -
-
-
- Specify how this book should be sorted when by title. For example, The Exorcist might be sorted as Exorcist, The.
-
-
-
- -
-
-
- &Author(s):
-
-
- Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
-
-
- authors
-
-
-
- -
-
-
- true
-
-
-
- -
-
-
- Author S&ort:
-
-
- Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
-
-
- author_sort
-
-
-
- -
-
-
- Specify how the author(s) of this book should be sorted. For example Charles Dickens should be sorted as Dickens, Charles.
-If the box is colored green, then text matches the individual author's sort strings. If it is colored red, then the authors and this text do not match.
-
-
-
- -
-
-
- &Rating:
-
-
- Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
-
-
- rating
-
-
-
- -
-
-
- Rating of this book. 0-5 stars
-
-
- Rating of this book. 0-5 stars
-
-
- QAbstractSpinBox::PlusMinus
-
-
- stars
-
-
- 5
-
-
-
- -
-
-
- &Publisher:
-
-
- Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
-
-
- publisher
-
-
-
- -
-
-
- true
-
-
-
- -
-
-
- Ta&gs:
-
-
- Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
-
-
- tags
-
-
-
- -
-
-
-
-
-
- Tags categorize the book. This is particularly useful while searching. <br><br>They can be any words or phrases, separated by commas.
-
-
-
-
-
- -
-
-
- Open Tag Editor
-
-
- Open Tag Editor
-
-
-
- :/images/chapters.png:/images/chapters.png
-
-
-
- -
-
-
- &Series:
-
-
- Qt::PlainText
-
-
- Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
-
-
- series
-
-
-
- -
-
-
- 5
-
-
-
-
-
- List of known series. You can add new series.
-
-
- List of known series. You can add new series.
-
-
- true
-
-
- QComboBox::InsertAlphabetically
-
-
-
-
-
- -
-
-
- Remove unused series (Series that have no books)
-
-
- ...
-
-
-
- :/images/trash.png:/images/trash.png
-
-
-
- -
-
-
- false
-
-
- Book
-
-
- 99999999.989999994635582
-
-
-
- -
-
-
- IS&BN:
-
-
- Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
-
-
- isbn
-
-
-
- -
-
-
- -
-
-
- &Date:
-
-
- Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
-
-
- date
-
-
-
- -
-
-
- dd MMM yyyy
-
-
- true
-
-
-
- -
-
-
- Publishe&d:
-
-
- Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter
-
-
- pubdate
-
-
-
- -
-
-
- MMM yyyy
-
-
- true
-
-
-
- -
-
-
- Clear published date
-
-
-
- :/images/trash.png:/images/trash.png
-
-
-
-
-
-
- -
-
-
- &Fetch metadata from server
-
-
-
- -
-
-
- Qt::Vertical
-
-
- QSizePolicy::Fixed
-
-
-
- 20
- 40
-
-
-
-
-
-
-
-
- -
-
-
-
- 0
- 10
-
-
-
- Book Cover
-
-
-
-
-
-
-
- 0
- 100
-
-
-
-
- -
-
-
- 6
-
-
- QLayout::SetMaximumSize
-
-
- 0
-
-
-
-
-
- Change &cover image:
-
-
- cover_button
-
-
-
- -
-
-
- 6
-
-
- 0
-
-
-
-
-
- &Browse
-
-
-
- :/images/document_open.png:/images/document_open.png
-
-
-
- -
-
-
- Remove border (if any) from cover
-
-
- T&rim
-
-
-
- :/images/trim.png:/images/trim.png
-
-
-
- -
-
-
- Reset cover to default
-
-
- &Remove
-
-
-
- :/images/trash.png:/images/trash.png
-
-
-
-
-
-
-
- -
-
-
-
-
-
- Download co&ver
-
-
-
- -
-
-
- Generate a default cover based on the title and author
-
-
- &Generate cover
-
-
-
-
-
-
-
-
-
-
-
-
- -
-
-
-
- 0
- 0
-
-
-
- Available Formats
-
-
-
-
-
-
-
-
-
-
- 0
- 0
-
-
-
-
- 16777215
- 140
-
-
-
-
- 100
- 0
-
-
-
- QAbstractItemView::DropOnly
-
-
-
- 64
- 64
-
-
-
-
- -
-
-
- Add a new format for this book to the database
-
-
- ...
-
-
-
- :/images/add_book.png:/images/add_book.png
-
-
-
- 32
- 32
-
-
-
-
- -
-
-
- Remove the selected formats for this book from the database.
-
-
- ...
-
-
-
- :/images/trash.png:/images/trash.png
-
-
-
- 32
- 32
-
-
-
-
- -
-
-
- Set the cover for the book from the selected format
-
-
- ...
-
-
-
- :/images/book.png:/images/book.png
-
-
-
- 32
- 32
-
-
-
-
- -
-
-
- Update metadata from the metadata in the selected format
-
-
-
-
-
-
- :/images/edit_input.png:/images/edit_input.png
-
-
-
- 32
- 32
-
-
-
-
-
-
-
-
-
-
- -
-
-
-
- 0
- 10
-
-
-
- &Comments
-
-
-
- 0
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- &Custom metadata
-
-
-
-
-
-
-
-
-
- -
-
-
- Qt::Horizontal
-
-
- QDialogButtonBox::Cancel|QDialogButtonBox::Ok
-
-
-
-
-
-
-
- EnLineEdit
- QLineEdit
-
-
-
- MultiCompleteLineEdit
- QLineEdit
-
-
-
- MultiCompleteComboBox
- QComboBox
-
-
-
- FormatList
- QListWidget
-
-
-
- ImageView
- QWidget
-
- 1
-
-
- Editor
- QWidget
- calibre/gui2/comments_editor.h
- 1
-
-
-
- title
- auto_title_sort
- title_sort
- swap_button
- authors
- auto_author_sort
- author_sort
- rating
- publisher
- tags
- tag_editor_button
- series
- remove_series_button
- series_index
- isbn
- date
- pubdate
- fetch_metadata_button
- button_set_cover
- button_set_metadata
- formats
- add_format_button
- remove_format_button
- cover_button
- trim_cover_button
- reset_cover
- fetch_cover_button
- generate_cover_button
- button_box
- scrollArea
- central_widget
-
-
-
-
-
-
- button_box
- accepted()
- MetadataSingleDialog
- accept()
-
-
- 261
- 710
-
-
- 157
- 274
-
-
-
-
- button_box
- rejected()
- MetadataSingleDialog
- reject()
-
-
- 329
- 710
-
-
- 286
- 274
-
-
-
-
-
diff --git a/src/calibre/gui2/metadata/bulk_download.py b/src/calibre/gui2/metadata/bulk_download.py
index 7a7f49dabf..2a307fc902 100644
--- a/src/calibre/gui2/metadata/bulk_download.py
+++ b/src/calibre/gui2/metadata/bulk_download.py
@@ -1,308 +1,195 @@
#!/usr/bin/env python
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
-from __future__ import with_statement
+from __future__ import (unicode_literals, division, absolute_import,
+ print_function)
__license__ = 'GPL v3'
-__copyright__ = '2009, Kovid Goyal '
+__copyright__ = '2011, Kovid Goyal '
__docformat__ = 'restructuredtext en'
-import traceback
-from threading import Thread
-from Queue import Queue, Empty
from functools import partial
+from itertools import izip
+from threading import Event
-from PyQt4.Qt import QObject, QTimer, QDialog, \
- QVBoxLayout, QTextBrowser, QLabel, QGroupBox, QDialogButtonBox
+from PyQt4.Qt import (QIcon, QDialog,
+ QDialogButtonBox, QLabel, QGridLayout, QPixmap, Qt)
-from calibre.ebooks.metadata.fetch import search, get_social_metadata
-from calibre.gui2 import config, error_dialog
-from calibre.gui2.dialogs.progress import ProgressDialog
-from calibre.ebooks.metadata.covers import download_cover
-from calibre.customize.ui import get_isbndb_key
+from calibre.gui2.threaded_jobs import ThreadedJob
+from calibre.ebooks.metadata.sources.identify import identify, msprefs
+from calibre.ebooks.metadata.sources.covers import download_cover
+from calibre.ebooks.metadata.book.base import Metadata
+from calibre.customize.ui import metadata_plugins
+from calibre.ptempfile import PersistentTemporaryFile
-class Worker(Thread):
- 'Cover downloader'
+# Start download {{{
+def show_config(gui, parent):
+ from calibre.gui2.preferences import show_config_widget
+ show_config_widget('Sharing', 'Metadata download', parent=parent,
+ gui=gui, never_shutdown=True)
- def __init__(self):
- Thread.__init__(self)
- self.daemon = True
- self.jobs = Queue()
- self.results = Queue()
+class ConfirmDialog(QDialog):
- def run(self):
- while True:
- id, mi = self.jobs.get()
- if not getattr(mi, 'isbn', False):
- break
+ def __init__(self, ids, parent):
+ QDialog.__init__(self, parent)
+ self.setWindowTitle(_('Schedule download?'))
+ self.setWindowIcon(QIcon(I('dialog_question.png')))
+
+ l = self.l = QGridLayout()
+ self.setLayout(l)
+
+ i = QLabel(self)
+ i.setPixmap(QPixmap(I('dialog_question.png')))
+ l.addWidget(i, 0, 0)
+
+ t = QLabel(
+ ''+_('The download of metadata for the %d selected book(s) will'
+ ' run in the background. Proceed?')%len(ids) +
+ '
'+_('You can monitor the progress of the download '
+ 'by clicking the rotating spinner in the bottom right '
+ 'corner.') +
+ '
'+_('When the download completes you will be asked for'
+ ' confirmation before calibre applies the downloaded metadata.')
+ )
+ t.setWordWrap(True)
+ l.addWidget(t, 0, 1)
+ l.setColumnStretch(0, 1)
+ l.setColumnStretch(1, 100)
+
+ self.identify = self.covers = True
+ self.bb = QDialogButtonBox(QDialogButtonBox.Cancel)
+ self.bb.rejected.connect(self.reject)
+ b = self.bb.addButton(_('Download only &metadata'),
+ self.bb.AcceptRole)
+ b.clicked.connect(self.only_metadata)
+ b.setIcon(QIcon(I('edit_input.png')))
+ b = self.bb.addButton(_('Download only &covers'),
+ self.bb.AcceptRole)
+ b.clicked.connect(self.only_covers)
+ b.setIcon(QIcon(I('default_cover.png')))
+ b = self.b = self.bb.addButton(_('&Configure download'), self.bb.ActionRole)
+ b.setIcon(QIcon(I('config.png')))
+ b.clicked.connect(partial(show_config, parent, self))
+ l.addWidget(self.bb, 1, 0, 1, 2)
+ b = self.bb.addButton(_('Download &both'),
+ self.bb.AcceptRole)
+ b.clicked.connect(self.accept)
+ b.setDefault(True)
+ b.setAutoDefault(True)
+ b.setIcon(QIcon(I('ok.png')))
+
+ self.resize(self.sizeHint())
+ b.setFocus(Qt.OtherFocusReason)
+
+ def only_metadata(self):
+ self.covers = False
+ self.accept()
+
+ def only_covers(self):
+ self.identify = False
+ self.accept()
+
+def start_download(gui, ids, callback):
+ d = ConfirmDialog(ids, gui)
+ ret = d.exec_()
+ d.b.clicked.disconnect()
+ if ret != d.Accepted:
+ return
+
+ job = ThreadedJob('metadata bulk download',
+ _('Download metadata for %d books')%len(ids),
+ download, (ids, gui.current_db, d.identify, d.covers), {}, callback)
+ gui.job_manager.run_threaded_job(job)
+ gui.status_bar.show_message(_('Metadata download started'), 3000)
+# }}}
+
+def get_job_details(job):
+ id_map, failed_ids, failed_covers, title_map, all_failed = job.result
+ det_msg = []
+ for i in failed_ids | failed_covers:
+ title = title_map[i]
+ if i in failed_ids:
+ title += (' ' + _('(Failed metadata)'))
+ if i in failed_covers:
+ title += (' ' + _('(Failed cover)'))
+ det_msg.append(title)
+ det_msg = '\n'.join(det_msg)
+ return id_map, failed_ids, failed_covers, all_failed, det_msg
+
+def merge_result(oldmi, newmi):
+ dummy = Metadata(_('Unknown'))
+ for f in msprefs['ignore_fields']:
+ if ':' not in f:
+ setattr(newmi, f, getattr(dummy, f))
+ fields = set()
+ for plugin in metadata_plugins(['identify']):
+ fields |= plugin.touched_fields
+
+ for f in fields:
+ # Optimize so that set_metadata does not have to do extra work later
+ if not f.startswith('identifier:'):
+ if (not newmi.is_null(f) and getattr(newmi, f) == getattr(oldmi, f)):
+ setattr(newmi, f, getattr(dummy, f))
+
+ newmi.last_modified = oldmi.last_modified
+
+ return newmi
+
+def download(ids, db, do_identify, covers,
+ log=None, abort=None, notifications=None):
+ ids = list(ids)
+ metadata = [db.get_metadata(i, index_is_id=True, get_user_categories=False)
+ for i in ids]
+ failed_ids = set()
+ failed_covers = set()
+ title_map = {}
+ ans = {}
+ count = 0
+ all_failed = True
+ '''
+ # Test apply dialog
+ all_failed = do_identify = covers = False
+ '''
+ for i, mi in izip(ids, metadata):
+ if abort.is_set():
+ log.error('Aborting...')
+ break
+ title, authors, identifiers = mi.title, mi.authors, mi.identifiers
+ title_map[i] = title
+ if do_identify:
+ results = []
try:
- cdata, errors = download_cover(mi)
- if cdata:
- self.results.put((id, mi, True, cdata))
- else:
- msg = []
- for e in errors:
- if not e[0]:
- msg.append(e[-1] + ' - ' + e[1])
- self.results.put((id, mi, False, '\n'.join(msg)))
+ results = identify(log, Event(), title=title, authors=authors,
+ identifiers=identifiers)
except:
- self.results.put((id, mi, False, traceback.format_exc()))
-
- def __enter__(self):
- self.start()
- return self
-
- def __exit__(self, *args):
- self.jobs.put((False, False))
-
-
-class DownloadMetadata(Thread):
- 'Metadata downloader'
-
- def __init__(self, db, ids, get_covers, set_metadata=True,
- get_social_metadata=True):
- Thread.__init__(self)
- self.daemon = True
- self.metadata = {}
- self.covers = {}
- self.set_metadata = set_metadata
- self.get_social_metadata = get_social_metadata
- self.social_metadata_exceptions = []
- self.db = db
- self.updated = set([])
- self.get_covers = get_covers
- self.worker = Worker()
- self.results = Queue()
- self.keep_going = True
- for id in ids:
- self.metadata[id] = db.get_metadata(id, index_is_id=True)
- self.metadata[id].rating = None
- self.total = len(ids)
- if self.get_covers:
- self.total += len(ids)
- self.fetched_metadata = {}
- self.fetched_covers = {}
- self.failures = {}
- self.cover_failures = {}
- self.exception = self.tb = None
-
- def run(self):
- try:
- self._run()
- except Exception as e:
- self.exception = e
- self.tb = traceback.format_exc()
-
- def _run(self):
- self.key = get_isbndb_key()
- if not self.key:
- self.key = None
- with self.worker:
- for id, mi in self.metadata.items():
- if not self.keep_going:
- break
- args = {}
- if mi.isbn:
- args['isbn'] = mi.isbn
- else:
- if mi.is_null('title'):
- self.failures[id] = \
- _('Book has neither title nor ISBN')
- continue
- args['title'] = mi.title
- if mi.authors and mi.authors[0] != _('Unknown'):
- args['author'] = mi.authors[0]
- if self.key:
- args['isbndb_key'] = self.key
- results, exceptions = search(**args)
- if results:
- fmi = results[0]
- self.fetched_metadata[id] = fmi
- if self.get_covers:
- if fmi.isbn:
- self.worker.jobs.put((id, fmi))
- else:
- self.results.put((id, 'cover', False, mi.title))
- if (not config['overwrite_author_title_metadata']):
- fmi.authors = mi.authors
- fmi.author_sort = mi.author_sort
- fmi.title = mi.title
- mi.smart_update(fmi)
- if mi.isbn and self.get_social_metadata:
- self.social_metadata_exceptions = get_social_metadata(mi)
- if mi.rating:
- mi.rating *= 2
- if not self.get_social_metadata:
- mi.tags = []
- self.results.put((id, 'metadata', True, mi.title))
- else:
- self.failures[id] = _('No matches found for this book')
- self.results.put((id, 'metadata', False, mi.title))
- self.results.put((id, 'cover', False, mi.title))
- self.commit_covers()
-
- self.commit_covers(True)
-
- def commit_covers(self, all=False):
- if all:
- self.worker.jobs.put((False, False))
- while True:
- try:
- id, fmi, ok, cdata = self.worker.results.get_nowait()
- if ok:
- self.fetched_covers[id] = cdata
- self.results.put((id, 'cover', ok, fmi.title))
- else:
- self.results.put((id, 'cover', ok, fmi.title))
- try:
- self.cover_failures[id] = unicode(cdata)
- except:
- self.cover_failures[id] = repr(cdata)
- except Empty:
- if not all or not self.worker.is_alive():
- return
-
-class DoDownload(QObject):
-
- def __init__(self, parent, title, db, ids, get_covers, set_metadata=True,
- get_social_metadata=True):
- QObject.__init__(self, parent)
- self.pd = ProgressDialog(title, min=0, max=0, parent=parent)
- self.pd.canceled_signal.connect(self.cancel)
- self.downloader = None
- self.create = partial(DownloadMetadata, db, ids, get_covers,
- set_metadata=set_metadata,
- get_social_metadata=get_social_metadata)
- self.get_covers = get_covers
- self.db = db
- self.updated = set([])
- self.total = len(ids)
- self.keep_going = True
-
- def exec_(self):
- QTimer.singleShot(50, self.do_one)
- ret = self.pd.exec_()
- if getattr(self.downloader, 'exception', None) is not None and \
- ret == self.pd.Accepted:
- error_dialog(self.parent(), _('Failed'),
- _('Failed to download metadata'), show=True)
- else:
- self.show_report()
- return ret
-
- def cancel(self, *args):
- self.keep_going = False
- self.downloader.keep_going = False
- self.pd.reject()
-
- def do_one(self):
- try:
- if not self.keep_going:
- return
- if self.downloader is None:
- self.downloader = self.create()
- self.downloader.start()
- self.pd.set_min(0)
- self.pd.set_max(self.downloader.total)
- try:
- r = self.downloader.results.get_nowait()
- self.handle_result(r)
- except Empty:
pass
- if not self.downloader.is_alive():
- while True:
- try:
- r = self.downloader.results.get_nowait()
- self.handle_result(r)
- except Empty:
- break
- self.pd.accept()
- return
- except:
- self.cancel()
- raise
- QTimer.singleShot(50, self.do_one)
-
- def handle_result(self, r):
- id_, typ, ok, title = r
- what = _('cover') if typ == 'cover' else _('metadata')
- which = _('Downloaded') if ok else _('Failed to get')
- if self.get_covers or typ != 'cover' or ok:
- # Do not show message when cover fetch fails if user didn't ask to
- # download covers
- self.pd.set_msg(_('%s %s for: %s') % (which, what, title))
- self.pd.value += 1
- if ok:
- self.updated.add(id_)
- if typ == 'cover':
- try:
- self.db.set_cover(id_,
- self.downloader.fetched_covers.pop(id_))
- except:
- self.downloader.cover_failures[id_] = \
- traceback.format_exc()
+ if results:
+ all_failed = False
+ mi = merge_result(mi, results[0])
+ identifiers = mi.identifiers
+ if not mi.is_null('rating'):
+ # set_metadata expects a rating out of 10
+ mi.rating *= 2
else:
- try:
- self.set_metadata(id_)
- except:
- self.downloader.failures[id_] = \
- traceback.format_exc()
-
- def set_metadata(self, id_):
- mi = self.downloader.metadata[id_]
- if self.downloader.set_metadata:
- self.db.set_metadata(id_, mi)
- if not self.downloader.set_metadata and self.downloader.get_social_metadata:
- if mi.rating:
- self.db.set_rating(id_, mi.rating)
- if mi.tags:
- self.db.set_tags(id_, mi.tags)
- if mi.comments:
- self.db.set_comment(id_, mi.comments)
- if mi.series:
- self.db.set_series(id_, mi.series)
- if mi.series_index is not None:
- self.db.set_series_index(id_, mi.series_index)
-
- def show_report(self):
- f, cf = self.downloader.failures, self.downloader.cover_failures
- report = []
- if f:
- report.append(
- '
Failed to download metadata for the following:
')
- for id_, err in f.items():
- mi = self.downloader.metadata[id_]
- report.append('- %s
%s
' % (mi.title,
- unicode(err)))
- report.append('
')
- if cf:
- report.append(
- 'Failed to download cover for the following:
')
- for id_, err in cf.items():
- mi = self.downloader.metadata[id_]
- report.append('- %s
%s
' % (mi.title,
- unicode(err)))
- report.append('
')
-
- if len(self.updated) != self.total or report:
- d = QDialog(self.parent())
- bb = QDialogButtonBox(QDialogButtonBox.Ok, parent=d)
- v1 = QVBoxLayout()
- d.setLayout(v1)
- d.setWindowTitle(_('Done'))
- v1.addWidget(QLabel(_('Successfully downloaded metadata for %d out of %d books') %
- (len(self.updated), self.total)))
- gb = QGroupBox(_('Details'), self.parent())
- v2 = QVBoxLayout()
- gb.setLayout(v2)
- b = QTextBrowser(self.parent())
- v2.addWidget(b)
- b.setHtml('\n'.join(report))
- v1.addWidget(gb)
- v1.addWidget(bb)
- bb.accepted.connect(d.accept)
- d.resize(800, 600)
- d.exec_()
-
+ log.error('Failed to download metadata for', title)
+ failed_ids.add(i)
+ # We don't want set_metadata operating on anything but covers
+ mi = merge_result(mi, mi)
+ if covers:
+ cdata = download_cover(log, title=title, authors=authors,
+ identifiers=identifiers)
+ if cdata is not None:
+ with PersistentTemporaryFile('.jpg', 'downloaded-cover-') as f:
+ f.write(cdata[-1])
+ mi.cover = f.name
+ all_failed = False
+ else:
+ failed_covers.add(i)
+ ans[i] = mi
+ count += 1
+ notifications.put((count/len(ids),
+ _('Downloaded %d of %d')%(count, len(ids))))
+ log('Download complete, with %d failures'%len(failed_ids))
+ return (ans, failed_ids, failed_covers, title_map, all_failed)
diff --git a/src/calibre/gui2/metadata/bulk_download2.py b/src/calibre/gui2/metadata/bulk_download2.py
deleted file mode 100644
index 2a307fc902..0000000000
--- a/src/calibre/gui2/metadata/bulk_download2.py
+++ /dev/null
@@ -1,195 +0,0 @@
-#!/usr/bin/env python
-# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
-from __future__ import (unicode_literals, division, absolute_import,
- print_function)
-
-__license__ = 'GPL v3'
-__copyright__ = '2011, Kovid Goyal '
-__docformat__ = 'restructuredtext en'
-
-from functools import partial
-from itertools import izip
-from threading import Event
-
-from PyQt4.Qt import (QIcon, QDialog,
- QDialogButtonBox, QLabel, QGridLayout, QPixmap, Qt)
-
-from calibre.gui2.threaded_jobs import ThreadedJob
-from calibre.ebooks.metadata.sources.identify import identify, msprefs
-from calibre.ebooks.metadata.sources.covers import download_cover
-from calibre.ebooks.metadata.book.base import Metadata
-from calibre.customize.ui import metadata_plugins
-from calibre.ptempfile import PersistentTemporaryFile
-
-# Start download {{{
-def show_config(gui, parent):
- from calibre.gui2.preferences import show_config_widget
- show_config_widget('Sharing', 'Metadata download', parent=parent,
- gui=gui, never_shutdown=True)
-
-class ConfirmDialog(QDialog):
-
- def __init__(self, ids, parent):
- QDialog.__init__(self, parent)
- self.setWindowTitle(_('Schedule download?'))
- self.setWindowIcon(QIcon(I('dialog_question.png')))
-
- l = self.l = QGridLayout()
- self.setLayout(l)
-
- i = QLabel(self)
- i.setPixmap(QPixmap(I('dialog_question.png')))
- l.addWidget(i, 0, 0)
-
- t = QLabel(
- ''+_('The download of metadata for the %d selected book(s) will'
- ' run in the background. Proceed?')%len(ids) +
- '
'+_('You can monitor the progress of the download '
- 'by clicking the rotating spinner in the bottom right '
- 'corner.') +
- '
'+_('When the download completes you will be asked for'
- ' confirmation before calibre applies the downloaded metadata.')
- )
- t.setWordWrap(True)
- l.addWidget(t, 0, 1)
- l.setColumnStretch(0, 1)
- l.setColumnStretch(1, 100)
-
- self.identify = self.covers = True
- self.bb = QDialogButtonBox(QDialogButtonBox.Cancel)
- self.bb.rejected.connect(self.reject)
- b = self.bb.addButton(_('Download only &metadata'),
- self.bb.AcceptRole)
- b.clicked.connect(self.only_metadata)
- b.setIcon(QIcon(I('edit_input.png')))
- b = self.bb.addButton(_('Download only &covers'),
- self.bb.AcceptRole)
- b.clicked.connect(self.only_covers)
- b.setIcon(QIcon(I('default_cover.png')))
- b = self.b = self.bb.addButton(_('&Configure download'), self.bb.ActionRole)
- b.setIcon(QIcon(I('config.png')))
- b.clicked.connect(partial(show_config, parent, self))
- l.addWidget(self.bb, 1, 0, 1, 2)
- b = self.bb.addButton(_('Download &both'),
- self.bb.AcceptRole)
- b.clicked.connect(self.accept)
- b.setDefault(True)
- b.setAutoDefault(True)
- b.setIcon(QIcon(I('ok.png')))
-
- self.resize(self.sizeHint())
- b.setFocus(Qt.OtherFocusReason)
-
- def only_metadata(self):
- self.covers = False
- self.accept()
-
- def only_covers(self):
- self.identify = False
- self.accept()
-
-def start_download(gui, ids, callback):
- d = ConfirmDialog(ids, gui)
- ret = d.exec_()
- d.b.clicked.disconnect()
- if ret != d.Accepted:
- return
-
- job = ThreadedJob('metadata bulk download',
- _('Download metadata for %d books')%len(ids),
- download, (ids, gui.current_db, d.identify, d.covers), {}, callback)
- gui.job_manager.run_threaded_job(job)
- gui.status_bar.show_message(_('Metadata download started'), 3000)
-# }}}
-
-def get_job_details(job):
- id_map, failed_ids, failed_covers, title_map, all_failed = job.result
- det_msg = []
- for i in failed_ids | failed_covers:
- title = title_map[i]
- if i in failed_ids:
- title += (' ' + _('(Failed metadata)'))
- if i in failed_covers:
- title += (' ' + _('(Failed cover)'))
- det_msg.append(title)
- det_msg = '\n'.join(det_msg)
- return id_map, failed_ids, failed_covers, all_failed, det_msg
-
-def merge_result(oldmi, newmi):
- dummy = Metadata(_('Unknown'))
- for f in msprefs['ignore_fields']:
- if ':' not in f:
- setattr(newmi, f, getattr(dummy, f))
- fields = set()
- for plugin in metadata_plugins(['identify']):
- fields |= plugin.touched_fields
-
- for f in fields:
- # Optimize so that set_metadata does not have to do extra work later
- if not f.startswith('identifier:'):
- if (not newmi.is_null(f) and getattr(newmi, f) == getattr(oldmi, f)):
- setattr(newmi, f, getattr(dummy, f))
-
- newmi.last_modified = oldmi.last_modified
-
- return newmi
-
-def download(ids, db, do_identify, covers,
- log=None, abort=None, notifications=None):
- ids = list(ids)
- metadata = [db.get_metadata(i, index_is_id=True, get_user_categories=False)
- for i in ids]
- failed_ids = set()
- failed_covers = set()
- title_map = {}
- ans = {}
- count = 0
- all_failed = True
- '''
- # Test apply dialog
- all_failed = do_identify = covers = False
- '''
- for i, mi in izip(ids, metadata):
- if abort.is_set():
- log.error('Aborting...')
- break
- title, authors, identifiers = mi.title, mi.authors, mi.identifiers
- title_map[i] = title
- if do_identify:
- results = []
- try:
- results = identify(log, Event(), title=title, authors=authors,
- identifiers=identifiers)
- except:
- pass
- if results:
- all_failed = False
- mi = merge_result(mi, results[0])
- identifiers = mi.identifiers
- if not mi.is_null('rating'):
- # set_metadata expects a rating out of 10
- mi.rating *= 2
- else:
- log.error('Failed to download metadata for', title)
- failed_ids.add(i)
- # We don't want set_metadata operating on anything but covers
- mi = merge_result(mi, mi)
- if covers:
- cdata = download_cover(log, title=title, authors=authors,
- identifiers=identifiers)
- if cdata is not None:
- with PersistentTemporaryFile('.jpg', 'downloaded-cover-') as f:
- f.write(cdata[-1])
- mi.cover = f.name
- all_failed = False
- else:
- failed_covers.add(i)
- ans[i] = mi
- count += 1
- notifications.put((count/len(ids),
- _('Downloaded %d of %d')%(count, len(ids))))
- log('Download complete, with %d failures'%len(failed_ids))
- return (ans, failed_ids, failed_covers, title_map, all_failed)
-
-
-
diff --git a/src/calibre/gui2/preferences/behavior.py b/src/calibre/gui2/preferences/behavior.py
index b376d067bc..e062ae2662 100644
--- a/src/calibre/gui2/preferences/behavior.py
+++ b/src/calibre/gui2/preferences/behavior.py
@@ -19,7 +19,6 @@ from calibre.ebooks import BOOK_EXTENSIONS
from calibre.ebooks.oeb.iterator import is_supported
from calibre.constants import iswindows
from calibre.utils.icu import sort_key
-from calibre.utils.config import test_eight_code
class OutputFormatSetting(Setting):
@@ -40,12 +39,6 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
r('network_timeout', prefs)
-
- r('overwrite_author_title_metadata', config)
- r('get_social_metadata', config)
- if test_eight_code:
- self.opt_overwrite_author_title_metadata.setVisible(False)
- self.opt_get_social_metadata.setVisible(False)
r('new_version_notification', config)
r('upload_news_to_device', config)
r('delete_news_from_library_on_upload', config)
@@ -67,13 +60,9 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
signal.connect(self.internally_viewed_formats_changed)
r('bools_are_tristate', db.prefs, restart_required=True)
- if test_eight_code:
- r = self.register
- choices = [(_('Default'), 'default'), (_('Compact Metadata'), 'alt1')]
- r('edit_metadata_single_layout', gprefs, choices=choices)
- else:
- self.opt_edit_metadata_single_layout.setVisible(False)
- self.edit_metadata_single_label.setVisible(False)
+ r = self.register
+ choices = [(_('Default'), 'default'), (_('Compact Metadata'), 'alt1')]
+ r('edit_metadata_single_layout', gprefs, choices=choices)
def initialize(self):
ConfigWidgetBase.initialize(self)
diff --git a/src/calibre/gui2/preferences/behavior.ui b/src/calibre/gui2/preferences/behavior.ui
index 69ebce6acf..ffd59d72bb 100644
--- a/src/calibre/gui2/preferences/behavior.ui
+++ b/src/calibre/gui2/preferences/behavior.ui
@@ -14,41 +14,14 @@
Form
- -
-
-
- Qt::Horizontal
-
-
-
- 10
- 0
-
-
-
-
- -
-
-
- &Overwrite author and title by default when fetching metadata
-
-
-
- -
-
-
- Download &social metadata (tags/ratings/etc.) by default
-
-
-
- -
+
-
Show notification when &new version is available
- -
+
-
If checked, Yes/No custom columns values can be Yes, No, or Unknown.
@@ -59,21 +32,21 @@ If not checked, the values can be Yes or No.
- -
+
-
Automatically send downloaded &news to ebook reader
- -
+
-
&Delete news from library when it is automatically sent to reader
- -
+
-
-
@@ -97,7 +70,7 @@ If not checked, the values can be Yes or No.
- -
+
-
-
@@ -130,7 +103,7 @@ If not checked, the values can be Yes or No.
- -
+
-
-
@@ -169,7 +142,7 @@ If not checked, the values can be Yes or No.
- -
+
-
-
@@ -202,7 +175,7 @@ If not checked, the values can be Yes or No.
- -
+
-
-
@@ -223,7 +196,7 @@ If not checked, the values can be Yes or No.
- -
+
-
Preferred &input format order:
@@ -285,7 +258,7 @@ If not checked, the values can be Yes or No.
- -
+
-
Use internal &viewer for:
@@ -304,7 +277,7 @@ If not checked, the values can be Yes or No.
- -
+
-
Reset all disabled &confirmation dialogs
diff --git a/src/calibre/gui2/preferences/social.py b/src/calibre/gui2/preferences/social.py
deleted file mode 100644
index a22bcce091..0000000000
--- a/src/calibre/gui2/preferences/social.py
+++ /dev/null
@@ -1,79 +0,0 @@
-#!/usr/bin/env python
-# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
-from __future__ import with_statement
-
-__license__ = 'GPL v3'
-__copyright__ = '2009, Kovid Goyal '
-__docformat__ = 'restructuredtext en'
-
-import time
-from threading import Thread
-
-from PyQt4.Qt import QDialog, QDialogButtonBox, Qt, QLabel, QVBoxLayout, \
- QTimer
-
-from calibre.ebooks.metadata import MetaInformation
-
-class Worker(Thread):
-
- def __init__(self, mi):
- Thread.__init__(self)
- self.daemon = True
- self.mi = MetaInformation(mi)
- self.exceptions = []
-
- def run(self):
- from calibre.ebooks.metadata.fetch import get_social_metadata
- self.exceptions = get_social_metadata(self.mi)
-
-class SocialMetadata(QDialog):
-
- TIMEOUT = 300 # seconds
-
- def __init__(self, mi, parent):
- QDialog.__init__(self, parent)
-
- self.bbox = QDialogButtonBox(QDialogButtonBox.Cancel, Qt.Horizontal, self)
- self.mi = mi
- self.layout = QVBoxLayout(self)
- self.label = QLabel(_('Downloading social metadata, please wait...'), self)
- self.label.setWordWrap(True)
- self.layout.addWidget(self.label)
- self.layout.addWidget(self.bbox)
-
- self.worker = Worker(mi)
- self.bbox.rejected.connect(self.reject)
- self.worker.start()
- self.start_time = time.time()
- self.timed_out = False
- self.rejected = False
- QTimer.singleShot(50, self.update)
-
- def reject(self):
- self.rejected = True
- QDialog.reject(self)
-
- def update(self):
- if self.rejected:
- return
- if time.time() - self.start_time > self.TIMEOUT:
- self.timed_out = True
- self.reject()
- return
- if not self.worker.is_alive():
- self.accept()
- return
- QTimer.singleShot(50, self.update)
-
- def accept(self):
- self.mi.tags = self.worker.mi.tags
- self.mi.rating = self.worker.mi.rating
- self.mi.comments = self.worker.mi.comments
- if self.worker.mi.series:
- self.mi.series = self.worker.mi.series
- self.mi.series_index = self.worker.mi.series_index
- QDialog.accept(self)
-
- @property
- def exceptions(self):
- return self.worker.exceptions
diff --git a/src/calibre/utils/config.py b/src/calibre/utils/config.py
index 8b23cf3071..0fcd047619 100644
--- a/src/calibre/utils/config.py
+++ b/src/calibre/utils/config.py
@@ -24,8 +24,6 @@ if False:
OptionSet, ConfigInterface, read_tweaks, write_tweaks
read_raw_tweaks, tweaks, plugin_dir
-test_eight_code = tweaks.get('test_eight_code', False)
-
def check_config_write_access():
return os.access(config_dir, os.W_OK) and os.access(config_dir, os.X_OK)
From 75a43d9c1a5a6ac84fae363734e17952b327f364 Mon Sep 17 00:00:00 2001
From: Kovid Goyal
Date: Sat, 30 Apr 2011 13:20:38 -0600
Subject: [PATCH 02/39] ISBN checking now correctly flags ISBNs with all same
digits as invalid
---
src/calibre/ebooks/metadata/__init__.py | 3 +++
1 file changed, 3 insertions(+)
diff --git a/src/calibre/ebooks/metadata/__init__.py b/src/calibre/ebooks/metadata/__init__.py
index 2ae5f3ade5..9c7838cb2c 100644
--- a/src/calibre/ebooks/metadata/__init__.py
+++ b/src/calibre/ebooks/metadata/__init__.py
@@ -274,6 +274,9 @@ def check_isbn(isbn):
if not isbn:
return None
isbn = re.sub(r'[^0-9X]', '', isbn.upper())
+ all_same = re.match(r'(\d)\1{9,12}$', isbn)
+ if all_same is not None:
+ return None
if len(isbn) == 10:
return check_isbn10(isbn)
if len(isbn) == 13:
From 8862bc694e2424c36dd2e39060c1cc8485318b87 Mon Sep 17 00:00:00 2001
From: Kovid Goyal
Date: Sat, 30 Apr 2011 16:32:48 -0600
Subject: [PATCH 03/39] Fix #774457 (Downloading metadata gives error)
---
src/calibre/ebooks/metadata/sources/identify.py | 7 +++----
1 file changed, 3 insertions(+), 4 deletions(-)
diff --git a/src/calibre/ebooks/metadata/sources/identify.py b/src/calibre/ebooks/metadata/sources/identify.py
index 9a9e5aa164..3d4807ac02 100644
--- a/src/calibre/ebooks/metadata/sources/identify.py
+++ b/src/calibre/ebooks/metadata/sources/identify.py
@@ -400,6 +400,9 @@ def identify(log, abort, # {{{
and plugin.get_cached_cover_url(result.identifiers) is not
None)
result.identify_plugin = plugin
+ if msprefs['txt_comments']:
+ if plugin.has_html_comments and result.comments:
+ result.comments = html2text(r.comments)
log('The identify phase took %.2f seconds'%(time.time() - start_time))
log('The longest time (%f) was taken by:'%longest, lp)
@@ -410,10 +413,6 @@ def identify(log, abort, # {{{
log('We have %d merged results, merging took: %.2f seconds' %
(len(results), time.time() - start_time))
- if msprefs['txt_comments']:
- for r in results:
- if r.identify_plugin.has_html_comments and r.comments:
- r.comments = html2text(r.comments)
max_tags = msprefs['max_tags']
for r in results:
From 3cfbf1cccd705dc49dbeeaf26e9d35821fe195cd Mon Sep 17 00:00:00 2001
From: Kovid Goyal
Date: Sat, 30 Apr 2011 16:42:11 -0600
Subject: [PATCH 04/39] ...
---
src/calibre/devices/misc.py | 2 +-
src/calibre/manual/faq.rst | 12 ++++++++++++
2 files changed, 13 insertions(+), 1 deletion(-)
diff --git a/src/calibre/devices/misc.py b/src/calibre/devices/misc.py
index b9710d1958..a0cb819cb9 100644
--- a/src/calibre/devices/misc.py
+++ b/src/calibre/devices/misc.py
@@ -187,7 +187,7 @@ class LUMIREAD(USBMS):
cfilepath = cfilepath.replace(os.sep+'books'+os.sep,
os.sep+'covers'+os.sep, 1)
pdir = os.path.dirname(cfilepath)
- if not os.exists(pdir):
+ if not os.path.exists(pdir):
os.makedirs(pdir)
with open(cfilepath+'.jpg', 'wb') as f:
f.write(metadata.thumbnail[-1])
diff --git a/src/calibre/manual/faq.rst b/src/calibre/manual/faq.rst
index 56d1832440..816ce7c496 100644
--- a/src/calibre/manual/faq.rst
+++ b/src/calibre/manual/faq.rst
@@ -468,6 +468,18 @@ If it still wont launch, start a command prompt (press the windows key and R; th
Post any output you see in a help message on the `Forum `_.
+|app| freeze when I click on anything?
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+There are three possible things I know of, that can cause this:
+
+ * You recently connected an external monitor or TV to your computer. In this case, whenever |app| opens a new window like the edit metadata window or the conversion dialog, it appears on the second monitor where you dont notice it and so you think |app| has frozen. Disconnect your second monitor and restart calibre.
+
+ * You are using a Wacom branded mouse. There is an incompatibility between Wacom mice and the graphics toolkit |app| uses. Try using a non-Wacom mouse.
+
+ * You have invalid files in your fonts folder. If this is the case, start |app| in debug mode as desribed in the previous answer and you will get messages about invalid files in :file:`C:\\Windows\\fonts`. Delete these files and you will be fine.
+
+
|app| is not starting on OS X?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
From 91350fc1284bb3207a62be4552cdf5e0bdde8408 Mon Sep 17 00:00:00 2001
From: Charles Haley <>
Date: Sun, 1 May 2011 16:21:13 +0100
Subject: [PATCH 05/39] Add another Samsung card ID (SGH-T849_CARD) to
WINDOWS_CARD_A_MEM.
---
src/calibre/devices/android/driver.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/calibre/devices/android/driver.py b/src/calibre/devices/android/driver.py
index 359dae89fe..f500560f97 100644
--- a/src/calibre/devices/android/driver.py
+++ b/src/calibre/devices/android/driver.py
@@ -112,7 +112,7 @@ class ANDROID(USBMS):
'MB860', 'MULTI-CARD', 'MID7015A', 'INCREDIBLE', 'A7EB']
WINDOWS_CARD_A_MEM = ['ANDROID_PHONE', 'GT-I9000_CARD', 'SGH-I897',
'FILE-STOR_GADGET', 'SGH-T959', 'SAMSUNG_ANDROID', 'GT-P1000_CARD',
- 'A70S', 'A101IT', '7', 'INCREDIBLE', 'A7EB']
+ 'A70S', 'A101IT', '7', 'INCREDIBLE', 'A7EB', 'SGH-T849_CARD']
OSX_MAIN_MEM = 'Android Device Main Memory'
From bab502e6543b2997158b23a72d6f41e119e15136 Mon Sep 17 00:00:00 2001
From: Kovid Goyal
Date: Sun, 1 May 2011 09:58:55 -0600
Subject: [PATCH 06/39] Fix Brand Eins
---
recipes/brand_eins.recipe | 13 ++++++++-----
src/calibre/manual/faq.rst | 2 +-
2 files changed, 9 insertions(+), 6 deletions(-)
diff --git a/recipes/brand_eins.recipe b/recipes/brand_eins.recipe
index 15e1d3ccca..e6fe57b334 100644
--- a/recipes/brand_eins.recipe
+++ b/recipes/brand_eins.recipe
@@ -3,7 +3,8 @@
__license__ = 'GPL v3'
__copyright__ = '2010, Constantin Hofstetter , Steffen Siebert '
-__version__ = '0.98' # 2011-04-10
+__version__ = '0.98'
+
''' http://brandeins.de - Wirtschaftsmagazin '''
import re
import string
@@ -13,8 +14,8 @@ from calibre.web.feeds.recipes import BasicNewsRecipe
class BrandEins(BasicNewsRecipe):
title = u'brand eins'
- __author__ = 'Constantin Hofstetter; Steffen Siebert'
- description = u'Wirtschaftsmagazin: Gets the last full issue on default. Set a integer value for the username-field to get older issues: 1 -> the newest (but not complete) issue, 2 -> the last complete issue (default), 3 -> the issue before 2 etc.'
+ __author__ = 'Constantin Hofstetter'
+ description = u'Wirtschaftsmagazin'
publisher ='brandeins.de'
category = 'politics, business, wirtschaft, Germany'
use_embedded_content = False
@@ -105,10 +106,11 @@ class BrandEins(BasicNewsRecipe):
keys = issue_map.keys()
keys.sort()
keys.reverse()
- selected_issue = issue_map[keys[issue-1]]
+ selected_issue_key = keys[issue - 1]
+ selected_issue = issue_map[selected_issue_key]
url = selected_issue.get('href', False)
# Get the title for the magazin - build it out of the title of the cover - take the issue and year;
- self.title = "brand eins "+ re.search(r"(?P\d\d\/\d\d\d\d)", selected_issue.find('img').get('title', False)).group('date')
+ self.title = "brand eins " + selected_issue_key[4:] + "/" + selected_issue_key[0:4]
url = 'http://brandeins.de/'+url
# url = "http://www.brandeins.de/archiv/magazin/tierisch.html"
@@ -161,3 +163,4 @@ class BrandEins(BasicNewsRecipe):
current_articles.append({'title': title, 'url': url, 'description': description, 'date':''})
titles_and_articles.append([chapter_title, current_articles])
return titles_and_articles
+
diff --git a/src/calibre/manual/faq.rst b/src/calibre/manual/faq.rst
index 816ce7c496..08ebb6506b 100644
--- a/src/calibre/manual/faq.rst
+++ b/src/calibre/manual/faq.rst
@@ -468,7 +468,7 @@ If it still wont launch, start a command prompt (press the windows key and R; th
Post any output you see in a help message on the `Forum `_.
-|app| freeze when I click on anything?
+|app| freezes when I click on anything?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
There are three possible things I know of, that can cause this:
From 186a47da8ce95287db54d0706ff556a4363d0860 Mon Sep 17 00:00:00 2001
From: Kovid Goyal
Date: Sun, 1 May 2011 10:33:10 -0600
Subject: [PATCH 07/39] Fix #774743 (Calibre Crash on read [open by (v) from
edit-screen])
---
src/calibre/gui2/actions/view.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/calibre/gui2/actions/view.py b/src/calibre/gui2/actions/view.py
index c93e69f0fc..6cf5c5d5af 100644
--- a/src/calibre/gui2/actions/view.py
+++ b/src/calibre/gui2/actions/view.py
@@ -60,7 +60,7 @@ class ViewAction(InterfaceAction):
def build_menus(self, db):
self.view_menu.clear()
- self.view_menu.addAction(self.qaction)
+ self.view_menu.addAction(self.view_action)
self.view_menu.addAction(self.view_specific_action)
self.view_menu.addSeparator()
self.view_menu.addAction(self.action_pick_random)
From 9a32f09a71ba49f727f1c298bf79915bd18f6e98 Mon Sep 17 00:00:00 2001
From: Kovid Goyal
Date: Sun, 1 May 2011 10:46:42 -0600
Subject: [PATCH 08/39] MOBI Input: Handle MOBI files with empty tags
correctly. Fixes #774785 (Private bug)
---
src/calibre/ebooks/mobi/reader.py | 5 +++++
src/calibre/ebooks/oeb/base.py | 4 ++--
2 files changed, 7 insertions(+), 2 deletions(-)
diff --git a/src/calibre/ebooks/mobi/reader.py b/src/calibre/ebooks/mobi/reader.py
index d9c6853795..3d858864a8 100644
--- a/src/calibre/ebooks/mobi/reader.py
+++ b/src/calibre/ebooks/mobi/reader.py
@@ -253,6 +253,8 @@ class MobiReader(object):
.italic { font-style: italic }
+ .underline { text-decoration: underline }
+
.mbp_pagebreak {
page-break-after: always; margin: 0; display: block
}
@@ -601,6 +603,9 @@ class MobiReader(object):
elif tag.tag == 'i':
tag.tag = 'span'
tag.attrib['class'] = 'italic'
+ elif tag.tag == 'u':
+ tag.tag = 'span'
+ tag.attrib['class'] = 'underline'
elif tag.tag == 'b':
tag.tag = 'span'
tag.attrib['class'] = 'bold'
diff --git a/src/calibre/ebooks/oeb/base.py b/src/calibre/ebooks/oeb/base.py
index 1f71e32548..db83fca496 100644
--- a/src/calibre/ebooks/oeb/base.py
+++ b/src/calibre/ebooks/oeb/base.py
@@ -1049,8 +1049,8 @@ class Manifest(object):
# Remove hyperlinks with no content as they cause rendering
# artifacts in browser based renderers
- # Also remove empty and tags
- for a in xpath(data, '//h:a[@href]|//h:i|//h:b'):
+ # Also remove empty , and tags
+ for a in xpath(data, '//h:a[@href]|//h:i|//h:b|//h:u'):
if a.get('id', None) is None and a.get('name', None) is None \
and len(a) == 0 and not a.text:
remove_elem(a)
From 7bfa82b98308f18e7d53a590417d0dd378893416 Mon Sep 17 00:00:00 2001
From: Kovid Goyal
Date: Sun, 1 May 2011 13:50:24 -0600
Subject: [PATCH 09/39] Fix #775048 (spelling mistake on website)
---
src/calibre/manual/faq.rst | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/calibre/manual/faq.rst b/src/calibre/manual/faq.rst
index 08ebb6506b..2e2a8e5ae6 100644
--- a/src/calibre/manual/faq.rst
+++ b/src/calibre/manual/faq.rst
@@ -557,7 +557,7 @@ You have two choices:
How is |app| licensed?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-|app| is licensed under the GNU General Public License v3 (an open source license). This means that you are free to redistribute |app| as long as you make the source code available. So if you want to put |app| on a CD with your product, you must also put the |app| source code on the CD. The source code is available for download `from googlecode `_. You are free to use the results of conversions from |app| however you want. You cannot use code, libraries from |app| in your software without maing your software open source. For details, see `The GNU GPL v3 `_.
+|app| is licensed under the GNU General Public License v3 (an open source license). This means that you are free to redistribute |app| as long as you make the source code available. So if you want to put |app| on a CD with your product, you must also put the |app| source code on the CD. The source code is available for download `from googlecode `_. You are free to use the results of conversions from |app| however you want. You cannot use code, libraries from |app| in your software without making your software open source. For details, see `The GNU GPL v3 `_.
How do I run calibre from my USB stick?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
From 255fe03b289000c46f83eb4a2c93362e8179be53 Mon Sep 17 00:00:00 2001
From: Charles Haley <>
Date: Mon, 2 May 2011 12:25:45 +0100
Subject: [PATCH 10/39] First attempt at a user_defined device
---
src/calibre/customize/builtins.py | 2 +
src/calibre/devices/__init__.py | 52 ++++++++++
src/calibre/devices/user_defined/__init__.py | 0
src/calibre/devices/user_defined/driver.py | 86 +++++++++++++++++
.../gui2/preferences/device_user_defined.py | 94 +++++++++++++++++++
src/calibre/gui2/preferences/misc.py | 6 ++
src/calibre/gui2/preferences/misc.ui | 9 +-
7 files changed, 248 insertions(+), 1 deletion(-)
create mode 100644 src/calibre/devices/user_defined/__init__.py
create mode 100644 src/calibre/devices/user_defined/driver.py
create mode 100644 src/calibre/gui2/preferences/device_user_defined.py
diff --git a/src/calibre/customize/builtins.py b/src/calibre/customize/builtins.py
index 36bcbdbfe2..776b04d5f6 100644
--- a/src/calibre/customize/builtins.py
+++ b/src/calibre/customize/builtins.py
@@ -595,6 +595,7 @@ from calibre.devices.jetbook.driver import JETBOOK, MIBUK, JETBOOK_MINI
from calibre.devices.kindle.driver import KINDLE, KINDLE2, KINDLE_DX
from calibre.devices.nook.driver import NOOK, NOOK_COLOR
from calibre.devices.prs505.driver import PRS505
+from calibre.devices.user_defined.driver import USER_DEFINED
from calibre.devices.android.driver import ANDROID, S60
from calibre.devices.nokia.driver import N770, N810, E71X, E52
from calibre.devices.eslick.driver import ESLICK, EBK52
@@ -742,6 +743,7 @@ plugins += [
EEEREADER,
NEXTBOOK,
ITUNES,
+ USER_DEFINED,
]
plugins += [x for x in list(locals().values()) if isinstance(x, type) and \
x.__name__.endswith('MetadataReader')]
diff --git a/src/calibre/devices/__init__.py b/src/calibre/devices/__init__.py
index 63b0b89a17..d151ae1844 100644
--- a/src/calibre/devices/__init__.py
+++ b/src/calibre/devices/__init__.py
@@ -156,3 +156,55 @@ def debug(ioreg_to_tmp=False, buf=None):
sys.stdout = oldo
sys.stderr = olde
+def device_info(ioreg_to_tmp=False, buf=None):
+ from calibre.devices.scanner import DeviceScanner, win_pnp_drives
+ from calibre.constants import iswindows
+ from calibre import prints
+ import re
+
+ res = {}
+ if not iswindows:
+ return None
+ try:
+ s = DeviceScanner()
+ s.scan()
+ devices = (s.devices)
+ device_details = {}
+ device_set = set()
+ for dev in devices:
+ vid = re.search('vid_([0-9a-f]*)&', dev)
+ if vid:
+ vid = vid.group(1)
+ pid = re.search('pid_([0-9a-f]*)&', dev)
+ if pid:
+ pid = pid.group(1)
+ rev = re.search('rev_([0-9a-f]*)$', dev)
+ if rev:
+ rev = rev.group(1)
+ d = vid+pid+rev
+ prints(d)
+ device_set.add(d)
+ device_details[d] = (vid, pid, rev)
+ res['device_set'] = device_set
+ res['device_details'] = device_details
+ drives = win_pnp_drives(debug=False)
+ drive_details = {}
+ print drives
+ drive_set = set()
+ for drive,details in drives.iteritems():
+ order = 'ORD_' + str(drive.order)
+ ven = re.search('VEN_([^&]*)&', details)
+ if ven:
+ ven = ven.group(1)
+ prod = re.search('PROD_([^&]*)&', details)
+ if prod:
+ prod = prod.group(1)
+ d = (order, ven, prod)
+ print d
+ drive_details[drive] = d
+ drive_set.add(drive)
+ res['drive_details'] = drive_details
+ res['drive_set'] = drive_set
+ finally:
+ pass
+ return res
\ No newline at end of file
diff --git a/src/calibre/devices/user_defined/__init__.py b/src/calibre/devices/user_defined/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/src/calibre/devices/user_defined/driver.py b/src/calibre/devices/user_defined/driver.py
new file mode 100644
index 0000000000..682ed1712e
--- /dev/null
+++ b/src/calibre/devices/user_defined/driver.py
@@ -0,0 +1,86 @@
+# -*- coding: utf-8 -*-
+
+__license__ = 'GPL v3'
+__copyright__ = '2009, Kovid Goyal '
+__docformat__ = 'restructuredtext en'
+
+from calibre.devices.usbms.driver import USBMS
+from calibre.ebooks import BOOK_EXTENSIONS
+
+class USER_DEFINED(USBMS):
+
+ name = 'User Defined USB driver'
+ gui_name = 'User Defined phone'
+ author = 'Kovid Goyal'
+ supported_platforms = ['windows', 'osx', 'linux']
+
+ # Ordered list of supported formats
+ FORMATS = BOOK_EXTENSIONS
+
+ VENDOR_ID = 0xFFFF
+ PRODUCT_ID = 0xFFFF
+ BCD = None
+
+ EBOOK_DIR_MAIN = ''
+ EBOOK_DIR_CARD_A = ''
+
+ VENDOR_NAME = []
+ WINDOWS_MAIN_MEM = ''
+ WINDOWS_CARD_A_MEM = ''
+
+ OSX_MAIN_MEM = 'Device Main Memory'
+
+ MAIN_MEMORY_VOLUME_LABEL = 'Device Main Memory'
+
+ SUPPORTS_SUB_DIRS = True
+
+ EXTRA_CUSTOMIZATION_MESSAGE = [
+ _('USB Vendor ID (in hex)'),
+ _('USB Product ID (in hex)'),
+ _('USB Revision ID (in hex)'),
+ _('Windows main memory vendor string'),
+ _('Windows main memory ID string'),
+ _('Windows card A vendor string'),
+ _('Windows card A ID string'),
+ _('Main memory folder'),
+ _('Card A folder'),
+ ]
+ EXTRA_CUSTOMIZATION_DEFAULT = [
+ '0x0000',
+ '0x0000',
+ '0x0000',
+ '',
+ '',
+ '',
+ '',
+ '',
+ '',
+ ]
+ OPT_USB_VENDOR_ID = 0
+ OPT_USB_PRODUCT_ID = 1
+ OPT_USB_REVISION_ID = 2
+ OPT_USB_WINDOWS_MM_VEN_ID = 3
+ OPT_USB_WINDOWS_MM_ID = 4
+ OPT_USB_WINDOWS_CA_VEN_ID = 5
+ OPT_USB_WINDOWS_CA_ID = 6
+ OPT_MAIN_MEM_FOLDER = 7
+ OPT_CARD_A_FOLDER = 8
+
+ def __init__(self, *args):
+ USBMS.__init__(self, args)
+ try:
+ e = self.settings().extra_customization
+ self.VENDOR_ID = int(e[self.OPT_USB_VENDOR_ID], 16)
+ self.PRODUCT_ID = int(e[self.OPT_USB_PRODUCT_ID], 16)
+ self.BCD = [int(e[self.OPT_USB_REVISION_ID], 16)]
+ print '%x, %x, %s' %(self.VENDOR_ID, self.PRODUCT_ID, str(self.BCD))
+ if e[self.OPT_USB_WINDOWS_MM_VEN_ID]:
+ self.VENDOR_NAME.append(e[self.OPT_USB_WINDOWS_MM_VEN_ID])
+ if e[self.OPT_USB_WINDOWS_CA_VEN_ID]:
+ self.VENDOR_NAME.append(e[self.OPT_USB_WINDOWS_CA_VEN_ID])
+ self.WINDOWS_MAIN_MEM = e[self.OPT_USB_WINDOWS_MM_ID]
+ self.WINDOWS_CARD_A_MEM = e[self.OPT_USB_WINDOWS_CA_ID]
+ self.EBOOK_DIR_MAIN = e[self.OPT_MAIN_MEM_FOLDER]
+ self.EBOOK_DIR_CARD_A = e[self.OPT_CARD_A_FOLDER]
+ except:
+ pass
\ No newline at end of file
diff --git a/src/calibre/gui2/preferences/device_user_defined.py b/src/calibre/gui2/preferences/device_user_defined.py
new file mode 100644
index 0000000000..0b820bd742
--- /dev/null
+++ b/src/calibre/gui2/preferences/device_user_defined.py
@@ -0,0 +1,94 @@
+#!/usr/bin/env python
+# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
+from __future__ import with_statement
+
+__license__ = 'GPL v3'
+__copyright__ = '2009, Kovid Goyal '
+__docformat__ = 'restructuredtext en'
+
+
+from PyQt4.Qt import QDialog, QVBoxLayout, QPlainTextEdit, QTimer, \
+ QDialogButtonBox, QPushButton, QApplication, QIcon, QMessageBox
+
+def step_dialog(parent, title, msg, det_msg=''):
+ d = QMessageBox(parent)
+ d.setWindowTitle(title)
+ d.setText(msg)
+ d.setStandardButtons(QMessageBox.Ok | QMessageBox.Cancel)
+ return d.exec_() & QMessageBox.Cancel
+
+
+class UserDefinedDevice(QDialog):
+
+ def __init__(self, parent=None):
+ QDialog.__init__(self, parent)
+ self._layout = QVBoxLayout(self)
+ self.setLayout(self._layout)
+ self.log = QPlainTextEdit(self)
+ self._layout.addWidget(self.log)
+ self.log.setPlainText(_('Getting debug information')+'...')
+ self.copy = QPushButton(_('Copy to &clipboard'))
+ self.copy.setDefault(True)
+ self.setWindowTitle(_('Debug device detection'))
+ self.setWindowIcon(QIcon(I('debug.png')))
+ self.copy.clicked.connect(self.copy_to_clipboard)
+ self.ok = QPushButton('&OK')
+ self.ok.setAutoDefault(False)
+ self.ok.clicked.connect(self.accept)
+ self.bbox = QDialogButtonBox(self)
+ self.bbox.addButton(self.copy, QDialogButtonBox.ActionRole)
+ self.bbox.addButton(self.ok, QDialogButtonBox.AcceptRole)
+ self._layout.addWidget(self.bbox)
+ self.resize(750, 500)
+ self.bbox.setEnabled(False)
+ QTimer.singleShot(1000, self.device_info)
+
+ def device_info(self):
+ try:
+ from calibre.devices import device_info
+ r = step_dialog(self.parent(), _('Device Detection'),
+ _('Ensure your device is disconnected, then press OK'))
+ if r:
+ return
+ before = device_info()
+ r = step_dialog(self.parent(), _('Device Detection'),
+ _('Ensure your device is connected, then press OK'))
+ if r:
+ return
+ after = device_info()
+ new_drives = after['drive_set'] - before['drive_set']
+ new_devices = after['device_set'] - before['device_set']
+ res = ''
+ if len(new_drives) and len(new_devices) == 1:
+ for d in new_devices:
+ res = _('USB Vendor ID (in hex)') + ': 0x' + \
+ after['device_details'][d][0] + '\n'
+ res += _('USB Product ID (in hex)') + ': 0x' + \
+ after['device_details'][d][1] + '\n'
+ res += _('USB Revision ID (in hex)') + ': 0x' + \
+ after['device_details'][d][2] + '\n'
+ # sort the drives by the order number
+ for i,d in enumerate(sorted(new_drives,
+ key=lambda x: after['drive_details'][x][0])):
+ if i == 0:
+ res += _('Windows main memory ID string') + ': ' + \
+ after['drive_details'][d][1] + '\n'
+ res += _('Windows main memory ID string') + ': ' + \
+ after['drive_details'][d][2] + '\n'
+ else:
+ res += _('Windows card A vendor string') + ': ' + \
+ after['drive_details'][d][1] + '\n'
+ res += _('Windows card A ID string') + ': ' + \
+ after['drive_details'][d][2] + '\n'
+
+ self.log.setPlainText(res)
+ finally:
+ self.bbox.setEnabled(True)
+
+ def copy_to_clipboard(self):
+ QApplication.clipboard().setText(self.log.toPlainText())
+
+if __name__ == '__main__':
+ app = QApplication([])
+ d = UserDefinedDevice()
+ d.exec_()
diff --git a/src/calibre/gui2/preferences/misc.py b/src/calibre/gui2/preferences/misc.py
index ead5da4ce4..80bfdffcd8 100644
--- a/src/calibre/gui2/preferences/misc.py
+++ b/src/calibre/gui2/preferences/misc.py
@@ -30,6 +30,7 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
r('enforce_cpu_limit', config, restart_required=True)
self.device_detection_button.clicked.connect(self.debug_device_detection)
self.button_open_config_dir.clicked.connect(self.open_config_dir)
+ self.user_defined_device_button.clicked.connect(self.user_defined_device)
self.button_osx_symlinks.clicked.connect(self.create_symlinks)
self.button_osx_symlinks.setVisible(isosx)
@@ -38,6 +39,11 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
d = DebugDevice(self)
d.exec_()
+ def user_defined_device(self, *args):
+ from calibre.gui2.preferences.device_user_defined import UserDefinedDevice
+ d = UserDefinedDevice(self)
+ d.exec_()
+
def open_config_dir(self, *args):
from calibre.utils.config import config_dir
open_local_file(config_dir)
diff --git a/src/calibre/gui2/preferences/misc.ui b/src/calibre/gui2/preferences/misc.ui
index 8b0189b0a1..cce14f5ade 100644
--- a/src/calibre/gui2/preferences/misc.ui
+++ b/src/calibre/gui2/preferences/misc.ui
@@ -58,7 +58,14 @@
- -
+
-
+
+
+ Setup the &user defined device
+
+
+
+ -
Qt::Vertical
From f74bed638111653a7d302c17da8938e34d962f16 Mon Sep 17 00:00:00 2001
From: Charles Haley <>
Date: Mon, 2 May 2011 13:29:08 +0100
Subject: [PATCH 11/39] ...
---
src/calibre/devices/__init__.py | 4 ----
src/calibre/devices/user_defined/driver.py | 17 +++++++++--------
.../gui2/preferences/device_user_defined.py | 7 ++++++-
src/calibre/gui2/preferences/misc.py | 3 ++-
src/calibre/gui2/preferences/misc.ui | 2 +-
5 files changed, 18 insertions(+), 15 deletions(-)
diff --git a/src/calibre/devices/__init__.py b/src/calibre/devices/__init__.py
index d151ae1844..02c42a1d6e 100644
--- a/src/calibre/devices/__init__.py
+++ b/src/calibre/devices/__init__.py
@@ -159,7 +159,6 @@ def debug(ioreg_to_tmp=False, buf=None):
def device_info(ioreg_to_tmp=False, buf=None):
from calibre.devices.scanner import DeviceScanner, win_pnp_drives
from calibre.constants import iswindows
- from calibre import prints
import re
res = {}
@@ -182,14 +181,12 @@ def device_info(ioreg_to_tmp=False, buf=None):
if rev:
rev = rev.group(1)
d = vid+pid+rev
- prints(d)
device_set.add(d)
device_details[d] = (vid, pid, rev)
res['device_set'] = device_set
res['device_details'] = device_details
drives = win_pnp_drives(debug=False)
drive_details = {}
- print drives
drive_set = set()
for drive,details in drives.iteritems():
order = 'ORD_' + str(drive.order)
@@ -200,7 +197,6 @@ def device_info(ioreg_to_tmp=False, buf=None):
if prod:
prod = prod.group(1)
d = (order, ven, prod)
- print d
drive_details[drive] = d
drive_set.add(drive)
res['drive_details'] = drive_details
diff --git a/src/calibre/devices/user_defined/driver.py b/src/calibre/devices/user_defined/driver.py
index 682ed1712e..03ed7dee94 100644
--- a/src/calibre/devices/user_defined/driver.py
+++ b/src/calibre/devices/user_defined/driver.py
@@ -10,7 +10,7 @@ from calibre.ebooks import BOOK_EXTENSIONS
class USER_DEFINED(USBMS):
name = 'User Defined USB driver'
- gui_name = 'User Defined phone'
+ gui_name = 'User Defined USB Device'
author = 'Kovid Goyal'
supported_platforms = ['windows', 'osx', 'linux']
@@ -66,21 +66,22 @@ class USER_DEFINED(USBMS):
OPT_MAIN_MEM_FOLDER = 7
OPT_CARD_A_FOLDER = 8
- def __init__(self, *args):
- USBMS.__init__(self, args)
+ def initialize(self):
try:
e = self.settings().extra_customization
self.VENDOR_ID = int(e[self.OPT_USB_VENDOR_ID], 16)
self.PRODUCT_ID = int(e[self.OPT_USB_PRODUCT_ID], 16)
self.BCD = [int(e[self.OPT_USB_REVISION_ID], 16)]
- print '%x, %x, %s' %(self.VENDOR_ID, self.PRODUCT_ID, str(self.BCD))
if e[self.OPT_USB_WINDOWS_MM_VEN_ID]:
self.VENDOR_NAME.append(e[self.OPT_USB_WINDOWS_MM_VEN_ID])
- if e[self.OPT_USB_WINDOWS_CA_VEN_ID]:
+ if e[self.OPT_USB_WINDOWS_CA_VEN_ID] and \
+ e[self.OPT_USB_WINDOWS_CA_VEN_ID] not in self.VENDOR_NAME:
self.VENDOR_NAME.append(e[self.OPT_USB_WINDOWS_CA_VEN_ID])
- self.WINDOWS_MAIN_MEM = e[self.OPT_USB_WINDOWS_MM_ID]
- self.WINDOWS_CARD_A_MEM = e[self.OPT_USB_WINDOWS_CA_ID]
+ self.WINDOWS_MAIN_MEM = e[self.OPT_USB_WINDOWS_MM_ID] + '&'
+ self.WINDOWS_CARD_A_MEM = e[self.OPT_USB_WINDOWS_CA_ID] + '&'
self.EBOOK_DIR_MAIN = e[self.OPT_MAIN_MEM_FOLDER]
self.EBOOK_DIR_CARD_A = e[self.OPT_CARD_A_FOLDER]
except:
- pass
\ No newline at end of file
+ import traceback
+ traceback.print_exc()
+ USBMS.initialize(self)
\ No newline at end of file
diff --git a/src/calibre/gui2/preferences/device_user_defined.py b/src/calibre/gui2/preferences/device_user_defined.py
index 0b820bd742..914e2b5666 100644
--- a/src/calibre/gui2/preferences/device_user_defined.py
+++ b/src/calibre/gui2/preferences/device_user_defined.py
@@ -81,7 +81,12 @@ class UserDefinedDevice(QDialog):
res += _('Windows card A ID string') + ': ' + \
after['drive_details'][d][2] + '\n'
- self.log.setPlainText(res)
+ trailer = _('Enter the above values into the USER_DEVICE by '
+ 'customizing the device plugin. Be sure to also '
+ 'enter the folders where you want the books to '
+ 'be put. You must restart calibre for your changes '
+ 'to take effect.\n')
+ self.log.setPlainText(res + '\n\n' + trailer)
finally:
self.bbox.setEnabled(True)
diff --git a/src/calibre/gui2/preferences/misc.py b/src/calibre/gui2/preferences/misc.py
index 80bfdffcd8..179e8a995d 100644
--- a/src/calibre/gui2/preferences/misc.py
+++ b/src/calibre/gui2/preferences/misc.py
@@ -9,7 +9,7 @@ __docformat__ = 'restructuredtext en'
from calibre.gui2.preferences import ConfigWidgetBase, test_widget, Setting
from calibre.gui2.preferences.misc_ui import Ui_Form
from calibre.gui2 import error_dialog, config, open_local_file, info_dialog
-from calibre.constants import isosx
+from calibre.constants import isosx, iswindows
class WorkersSetting(Setting):
@@ -33,6 +33,7 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
self.user_defined_device_button.clicked.connect(self.user_defined_device)
self.button_osx_symlinks.clicked.connect(self.create_symlinks)
self.button_osx_symlinks.setVisible(isosx)
+ self.user_defined_device_button.setVisible(iswindows)
def debug_device_detection(self, *args):
from calibre.gui2.preferences.device_debug import DebugDevice
diff --git a/src/calibre/gui2/preferences/misc.ui b/src/calibre/gui2/preferences/misc.ui
index cce14f5ade..df530bbe9a 100644
--- a/src/calibre/gui2/preferences/misc.ui
+++ b/src/calibre/gui2/preferences/misc.ui
@@ -61,7 +61,7 @@
-
- Setup the &user defined device
+ Get information to setup the &user defined device (Windows only)
From 0962ebb3ba0016b223d603a819e0b59b5bfe6984 Mon Sep 17 00:00:00 2001
From: Charles Haley <>
Date: Mon, 2 May 2011 14:58:26 +0100
Subject: [PATCH 12/39] Make user_defined_device information work on linux
---
src/calibre/devices/__init__.py | 81 ++++++++++---------
.../gui2/preferences/device_user_defined.py | 36 +++++----
src/calibre/gui2/preferences/misc.py | 3 +-
src/calibre/gui2/preferences/misc.ui | 2 +-
4 files changed, 66 insertions(+), 56 deletions(-)
diff --git a/src/calibre/devices/__init__.py b/src/calibre/devices/__init__.py
index 02c42a1d6e..e47cd82b50 100644
--- a/src/calibre/devices/__init__.py
+++ b/src/calibre/devices/__init__.py
@@ -162,45 +162,54 @@ def device_info(ioreg_to_tmp=False, buf=None):
import re
res = {}
- if not iswindows:
- return None
+ device_details = {}
+ device_set = set()
+ drive_details = {}
+ drive_set = set()
+ res['device_set'] = device_set
+ res['device_details'] = device_details
+ res['drive_details'] = drive_details
+ res['drive_set'] = drive_set
+
try:
s = DeviceScanner()
s.scan()
devices = (s.devices)
- device_details = {}
- device_set = set()
- for dev in devices:
- vid = re.search('vid_([0-9a-f]*)&', dev)
- if vid:
- vid = vid.group(1)
- pid = re.search('pid_([0-9a-f]*)&', dev)
- if pid:
- pid = pid.group(1)
- rev = re.search('rev_([0-9a-f]*)$', dev)
- if rev:
- rev = rev.group(1)
- d = vid+pid+rev
- device_set.add(d)
- device_details[d] = (vid, pid, rev)
- res['device_set'] = device_set
- res['device_details'] = device_details
- drives = win_pnp_drives(debug=False)
- drive_details = {}
- drive_set = set()
- for drive,details in drives.iteritems():
- order = 'ORD_' + str(drive.order)
- ven = re.search('VEN_([^&]*)&', details)
- if ven:
- ven = ven.group(1)
- prod = re.search('PROD_([^&]*)&', details)
- if prod:
- prod = prod.group(1)
- d = (order, ven, prod)
- drive_details[drive] = d
- drive_set.add(drive)
- res['drive_details'] = drive_details
- res['drive_set'] = drive_set
+ if not iswindows:
+ devices = [list(x) for x in devices]
+ for dev in devices:
+ for i in range(3):
+ dev[i] = hex(dev[i])
+ d = dev[0] + dev[1] + dev[2]
+ device_set.add(d)
+ device_details[d] = dev[0:3]
+ else:
+ for dev in devices:
+ vid = re.search('vid_([0-9a-f]*)&', dev)
+ if vid:
+ vid = vid.group(1)
+ pid = re.search('pid_([0-9a-f]*)&', dev)
+ if pid:
+ pid = pid.group(1)
+ rev = re.search('rev_([0-9a-f]*)$', dev)
+ if rev:
+ rev = rev.group(1)
+ d = vid+pid+rev
+ device_set.add(d)
+ device_details[d] = (vid, pid, rev)
+
+ drives = win_pnp_drives(debug=False)
+ for drive,details in drives.iteritems():
+ order = 'ORD_' + str(drive.order)
+ ven = re.search('VEN_([^&]*)&', details)
+ if ven:
+ ven = ven.group(1)
+ prod = re.search('PROD_([^&]*)&', details)
+ if prod:
+ prod = prod.group(1)
+ d = (order, ven, prod)
+ drive_details[drive] = d
+ drive_set.add(drive)
finally:
pass
- return res
\ No newline at end of file
+ return res
diff --git a/src/calibre/gui2/preferences/device_user_defined.py b/src/calibre/gui2/preferences/device_user_defined.py
index 914e2b5666..c2a27d3937 100644
--- a/src/calibre/gui2/preferences/device_user_defined.py
+++ b/src/calibre/gui2/preferences/device_user_defined.py
@@ -10,6 +10,8 @@ __docformat__ = 'restructuredtext en'
from PyQt4.Qt import QDialog, QVBoxLayout, QPlainTextEdit, QTimer, \
QDialogButtonBox, QPushButton, QApplication, QIcon, QMessageBox
+from calibre.constants import iswindows
+
def step_dialog(parent, title, msg, det_msg=''):
d = QMessageBox(parent)
d.setWindowTitle(title)
@@ -26,10 +28,10 @@ class UserDefinedDevice(QDialog):
self.setLayout(self._layout)
self.log = QPlainTextEdit(self)
self._layout.addWidget(self.log)
- self.log.setPlainText(_('Getting debug information')+'...')
+ self.log.setPlainText(_('Getting device information')+'...')
self.copy = QPushButton(_('Copy to &clipboard'))
self.copy.setDefault(True)
- self.setWindowTitle(_('Debug device detection'))
+ self.setWindowTitle(_('User-defined device information'))
self.setWindowIcon(QIcon(I('debug.png')))
self.copy.clicked.connect(self.copy_to_clipboard)
self.ok = QPushButton('&OK')
@@ -59,7 +61,7 @@ class UserDefinedDevice(QDialog):
new_drives = after['drive_set'] - before['drive_set']
new_devices = after['device_set'] - before['device_set']
res = ''
- if len(new_drives) and len(new_devices) == 1:
+ if (not iswindows or len(new_drives)) and len(new_devices) == 1:
for d in new_devices:
res = _('USB Vendor ID (in hex)') + ': 0x' + \
after['device_details'][d][0] + '\n'
@@ -67,20 +69,20 @@ class UserDefinedDevice(QDialog):
after['device_details'][d][1] + '\n'
res += _('USB Revision ID (in hex)') + ': 0x' + \
after['device_details'][d][2] + '\n'
- # sort the drives by the order number
- for i,d in enumerate(sorted(new_drives,
- key=lambda x: after['drive_details'][x][0])):
- if i == 0:
- res += _('Windows main memory ID string') + ': ' + \
- after['drive_details'][d][1] + '\n'
- res += _('Windows main memory ID string') + ': ' + \
- after['drive_details'][d][2] + '\n'
- else:
- res += _('Windows card A vendor string') + ': ' + \
- after['drive_details'][d][1] + '\n'
- res += _('Windows card A ID string') + ': ' + \
- after['drive_details'][d][2] + '\n'
-
+ if iswindows:
+ # sort the drives by the order number
+ for i,d in enumerate(sorted(new_drives,
+ key=lambda x: after['drive_details'][x][0])):
+ if i == 0:
+ res += _('Windows main memory ID string') + ': ' + \
+ after['drive_details'][d][1] + '\n'
+ res += _('Windows main memory ID string') + ': ' + \
+ after['drive_details'][d][2] + '\n'
+ else:
+ res += _('Windows card A vendor string') + ': ' + \
+ after['drive_details'][d][1] + '\n'
+ res += _('Windows card A ID string') + ': ' + \
+ after['drive_details'][d][2] + '\n'
trailer = _('Enter the above values into the USER_DEVICE by '
'customizing the device plugin. Be sure to also '
'enter the folders where you want the books to '
diff --git a/src/calibre/gui2/preferences/misc.py b/src/calibre/gui2/preferences/misc.py
index 179e8a995d..80bfdffcd8 100644
--- a/src/calibre/gui2/preferences/misc.py
+++ b/src/calibre/gui2/preferences/misc.py
@@ -9,7 +9,7 @@ __docformat__ = 'restructuredtext en'
from calibre.gui2.preferences import ConfigWidgetBase, test_widget, Setting
from calibre.gui2.preferences.misc_ui import Ui_Form
from calibre.gui2 import error_dialog, config, open_local_file, info_dialog
-from calibre.constants import isosx, iswindows
+from calibre.constants import isosx
class WorkersSetting(Setting):
@@ -33,7 +33,6 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
self.user_defined_device_button.clicked.connect(self.user_defined_device)
self.button_osx_symlinks.clicked.connect(self.create_symlinks)
self.button_osx_symlinks.setVisible(isosx)
- self.user_defined_device_button.setVisible(iswindows)
def debug_device_detection(self, *args):
from calibre.gui2.preferences.device_debug import DebugDevice
diff --git a/src/calibre/gui2/preferences/misc.ui b/src/calibre/gui2/preferences/misc.ui
index df530bbe9a..843f0f01b7 100644
--- a/src/calibre/gui2/preferences/misc.ui
+++ b/src/calibre/gui2/preferences/misc.ui
@@ -61,7 +61,7 @@
-
- Get information to setup the &user defined device (Windows only)
+ Get information to setup the &user defined device
From ffdfb6ebe552f26a8b9e3fceb6494edab328dfb6 Mon Sep 17 00:00:00 2001
From: Charles Haley <>
Date: Mon, 2 May 2011 15:21:51 +0100
Subject: [PATCH 13/39] ...
---
src/calibre/gui2/preferences/device_user_defined.py | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/src/calibre/gui2/preferences/device_user_defined.py b/src/calibre/gui2/preferences/device_user_defined.py
index c2a27d3937..198ae0d7a9 100644
--- a/src/calibre/gui2/preferences/device_user_defined.py
+++ b/src/calibre/gui2/preferences/device_user_defined.py
@@ -51,11 +51,13 @@ class UserDefinedDevice(QDialog):
r = step_dialog(self.parent(), _('Device Detection'),
_('Ensure your device is disconnected, then press OK'))
if r:
+ self.close()
return
before = device_info()
r = step_dialog(self.parent(), _('Device Detection'),
_('Ensure your device is connected, then press OK'))
if r:
+ self.close()
return
after = device_info()
new_drives = after['drive_set'] - before['drive_set']
@@ -83,7 +85,8 @@ class UserDefinedDevice(QDialog):
after['drive_details'][d][1] + '\n'
res += _('Windows card A ID string') + ': ' + \
after['drive_details'][d][2] + '\n'
- trailer = _('Enter the above values into the USER_DEVICE by '
+ trailer = _('Copy these values to the clipboard, paste them into an '
+ 'editor, then enter them into the USER_DEVICE by '
'customizing the device plugin. Be sure to also '
'enter the folders where you want the books to '
'be put. You must restart calibre for your changes '
From 106396f76806aa2647b85268c9ab18e7688301ed Mon Sep 17 00:00:00 2001
From: Charles Haley <>
Date: Mon, 2 May 2011 15:36:25 +0100
Subject: [PATCH 14/39] ...
---
src/calibre/devices/user_defined/driver.py | 40 +++++++++++++++++-----
1 file changed, 31 insertions(+), 9 deletions(-)
diff --git a/src/calibre/devices/user_defined/driver.py b/src/calibre/devices/user_defined/driver.py
index 03ed7dee94..3211bd19ef 100644
--- a/src/calibre/devices/user_defined/driver.py
+++ b/src/calibre/devices/user_defined/driver.py
@@ -35,15 +35,37 @@ class USER_DEFINED(USBMS):
SUPPORTS_SUB_DIRS = True
EXTRA_CUSTOMIZATION_MESSAGE = [
- _('USB Vendor ID (in hex)'),
- _('USB Product ID (in hex)'),
- _('USB Revision ID (in hex)'),
- _('Windows main memory vendor string'),
- _('Windows main memory ID string'),
- _('Windows card A vendor string'),
- _('Windows card A ID string'),
- _('Main memory folder'),
- _('Card A folder'),
+ _('USB Vendor ID (in hex)') + ':::' +
+ _('Get this ID using Preferences -> Misc -> Get information to '
+ 'set up the user-defined device'),
+ _('USB Product ID (in hex)')+ ':::' +
+ _('Get this ID using Preferences -> Misc -> Get information to '
+ 'set up the user-defined device'),
+ _('USB Revision ID (in hex)')+ ':::' +
+ _('Get this ID using Preferences -> Misc -> Get information to '
+ 'set up the user-defined device'),
+ _('Windows main memory vendor string') + ':::' +
+ _('This field is used only on windows. '
+ 'Get this ID using Preferences -> Misc -> Get information to '
+ 'set up the user-defined device'),
+ _('Windows main memory ID string') + ':::' +
+ _('This field is used only on windows. '
+ 'Get this ID using Preferences -> Misc -> Get information to '
+ 'set up the user-defined device'),
+ _('Windows card A vendor string') + ':::' +
+ _('This field is used only on windows. '
+ 'Get this ID using Preferences -> Misc -> Get information to '
+ 'set up the user-defined device'),
+ _('Windows card A ID string') + ':::' +
+ _('This field is used only on windows. '
+ 'Get this ID using Preferences -> Misc -> Get information to '
+ 'set up the user-defined device'),
+ _('Main memory folder') + ':::' +
+ _('Enter the folder where the books are to be stored. This folder '
+ 'is prepended to any send_to_device template'),
+ _('Card A folder') + ':::' +
+ _('Enter the folder where the books are to be stored. This folder '
+ 'is prepended to any send_to_device template'),
]
EXTRA_CUSTOMIZATION_DEFAULT = [
'0x0000',
From c64d180d1d470823486b871a07617c0886fbdc3e Mon Sep 17 00:00:00 2001
From: Charles Haley <>
Date: Mon, 2 May 2011 18:07:13 +0100
Subject: [PATCH 15/39] Switch device plugin preferences to double-column if
more than 6 preferences
---
src/calibre/devices/usbms/deviceconfig.py | 3 ++
src/calibre/devices/user_defined/driver.py | 52 ++++++++++---------
.../gui2/device_drivers/configwidget.py | 19 +++++--
.../gui2/device_drivers/configwidget.ui | 2 +-
4 files changed, 46 insertions(+), 30 deletions(-)
diff --git a/src/calibre/devices/usbms/deviceconfig.py b/src/calibre/devices/usbms/deviceconfig.py
index 3c79652463..3f669f1e24 100644
--- a/src/calibre/devices/usbms/deviceconfig.py
+++ b/src/calibre/devices/usbms/deviceconfig.py
@@ -94,6 +94,9 @@ class DeviceConfig(object):
if isinstance(cls.EXTRA_CUSTOMIZATION_MESSAGE, list):
ec = []
for i in range(0, len(cls.EXTRA_CUSTOMIZATION_MESSAGE)):
+ if config_widget.opt_extra_customization[i] is None:
+ ec.append(None)
+ continue
if hasattr(config_widget.opt_extra_customization[i], 'isChecked'):
ec.append(config_widget.opt_extra_customization[i].isChecked())
else:
diff --git a/src/calibre/devices/user_defined/driver.py b/src/calibre/devices/user_defined/driver.py
index 3211bd19ef..f57f61fe7c 100644
--- a/src/calibre/devices/user_defined/driver.py
+++ b/src/calibre/devices/user_defined/driver.py
@@ -15,7 +15,7 @@ class USER_DEFINED(USBMS):
supported_platforms = ['windows', 'osx', 'linux']
# Ordered list of supported formats
- FORMATS = BOOK_EXTENSIONS
+ FORMATS = ['epub', 'mobi', 'pdf']
VENDOR_ID = 0xFFFF
PRODUCT_ID = 0xFFFF
@@ -35,42 +35,44 @@ class USER_DEFINED(USBMS):
SUPPORTS_SUB_DIRS = True
EXTRA_CUSTOMIZATION_MESSAGE = [
- _('USB Vendor ID (in hex)') + ':::' +
+ _('USB Vendor ID (in hex)') + ':::' +
_('Get this ID using Preferences -> Misc -> Get information to '
- 'set up the user-defined device'),
- _('USB Product ID (in hex)')+ ':::' +
+ 'set up the user-defined device') + '
',
+ _('USB Product ID (in hex)')+ ':::' +
_('Get this ID using Preferences -> Misc -> Get information to '
- 'set up the user-defined device'),
- _('USB Revision ID (in hex)')+ ':::' +
+ 'set up the user-defined device') + '
',
+ _('USB Revision ID (in hex)')+ ':::' +
_('Get this ID using Preferences -> Misc -> Get information to '
- 'set up the user-defined device'),
- _('Windows main memory vendor string') + ':::' +
+ 'set up the user-defined device') + '
',
+ '',
+ _('Windows main memory vendor string') + ':::' +
_('This field is used only on windows. '
'Get this ID using Preferences -> Misc -> Get information to '
- 'set up the user-defined device'),
- _('Windows main memory ID string') + ':::' +
+ 'set up the user-defined device') + '
',
+ _('Windows main memory ID string') + ':::' +
_('This field is used only on windows. '
'Get this ID using Preferences -> Misc -> Get information to '
- 'set up the user-defined device'),
- _('Windows card A vendor string') + ':::' +
+ 'set up the user-defined device') + '
',
+ _('Windows card A vendor string') + ':::' +
_('This field is used only on windows. '
'Get this ID using Preferences -> Misc -> Get information to '
- 'set up the user-defined device'),
- _('Windows card A ID string') + ':::' +
+ 'set up the user-defined device') + '
',
+ _('Windows card A ID string') + ':::' +
_('This field is used only on windows. '
'Get this ID using Preferences -> Misc -> Get information to '
- 'set up the user-defined device'),
- _('Main memory folder') + ':::' +
+ 'set up the user-defined device') + '
',
+ _('Main memory folder') + ':::' +
_('Enter the folder where the books are to be stored. This folder '
- 'is prepended to any send_to_device template'),
- _('Card A folder') + ':::' +
+ 'is prepended to any send_to_device template') + '
',
+ _('Card A folder') + ':::' +
_('Enter the folder where the books are to be stored. This folder '
- 'is prepended to any send_to_device template'),
+ 'is prepended to any send_to_device template') + '
',
]
EXTRA_CUSTOMIZATION_DEFAULT = [
'0x0000',
'0x0000',
'0x0000',
+ None,
'',
'',
'',
@@ -81,12 +83,12 @@ class USER_DEFINED(USBMS):
OPT_USB_VENDOR_ID = 0
OPT_USB_PRODUCT_ID = 1
OPT_USB_REVISION_ID = 2
- OPT_USB_WINDOWS_MM_VEN_ID = 3
- OPT_USB_WINDOWS_MM_ID = 4
- OPT_USB_WINDOWS_CA_VEN_ID = 5
- OPT_USB_WINDOWS_CA_ID = 6
- OPT_MAIN_MEM_FOLDER = 7
- OPT_CARD_A_FOLDER = 8
+ OPT_USB_WINDOWS_MM_VEN_ID = 4
+ OPT_USB_WINDOWS_MM_ID = 5
+ OPT_USB_WINDOWS_CA_VEN_ID = 6
+ OPT_USB_WINDOWS_CA_ID = 7
+ OPT_MAIN_MEM_FOLDER = 8
+ OPT_CARD_A_FOLDER = 9
def initialize(self):
try:
diff --git a/src/calibre/gui2/device_drivers/configwidget.py b/src/calibre/gui2/device_drivers/configwidget.py
index fc7e16e639..e55a0c6dda 100644
--- a/src/calibre/gui2/device_drivers/configwidget.py
+++ b/src/calibre/gui2/device_drivers/configwidget.py
@@ -62,8 +62,18 @@ class ConfigWidget(QWidget, Ui_ConfigWidget):
if isinstance(extra_customization_message, list):
self.opt_extra_customization = []
+ if len(extra_customization_message) > 6:
+ row_func = lambda x, y: ((x/2) * 2) + y
+ col_func = lambda x: x%2
+ else:
+ row_func = lambda x, y: x*2 + y
+ col_func = lambda x: 0
+
for i, m in enumerate(extra_customization_message):
label_text, tt = parse_msg(m)
+ if not label_text:
+ self.opt_extra_customization.append(None)
+ continue
if isinstance(settings.extra_customization[i], bool):
self.opt_extra_customization.append(QCheckBox(label_text))
self.opt_extra_customization[-1].setToolTip(tt)
@@ -75,8 +85,9 @@ class ConfigWidget(QWidget, Ui_ConfigWidget):
l.setBuddy(self.opt_extra_customization[i])
l.setWordWrap(True)
self.opt_extra_customization[i].setText(settings.extra_customization[i])
- self.extra_layout.addWidget(l)
- self.extra_layout.addWidget(self.opt_extra_customization[i])
+ self.extra_layout.addWidget(l, row_func(i, 0), col_func(i))
+ self.extra_layout.addWidget(self.opt_extra_customization[i],
+ row_func(i, 1), col_func(i))
else:
self.opt_extra_customization = QLineEdit()
label_text, tt = parse_msg(extra_customization_message)
@@ -86,8 +97,8 @@ class ConfigWidget(QWidget, Ui_ConfigWidget):
l.setWordWrap(True)
if settings.extra_customization:
self.opt_extra_customization.setText(settings.extra_customization)
- self.extra_layout.addWidget(l)
- self.extra_layout.addWidget(self.opt_extra_customization)
+ self.extra_layout.addWidget(l, 0, 0)
+ self.extra_layout.addWidget(self.opt_extra_customization, 1, 0)
self.opt_save_template.setText(settings.save_template)
diff --git a/src/calibre/gui2/device_drivers/configwidget.ui b/src/calibre/gui2/device_drivers/configwidget.ui
index 619d7052e8..92324fd1a7 100644
--- a/src/calibre/gui2/device_drivers/configwidget.ui
+++ b/src/calibre/gui2/device_drivers/configwidget.ui
@@ -101,7 +101,7 @@
-
-
+
-
From 5d3455b184a6563fd3a43d93fea6de87f638d649 Mon Sep 17 00:00:00 2001
From: Kovid Goyal
Date: Mon, 2 May 2011 11:52:07 -0600
Subject: [PATCH 16/39] Print out plugin load failure traceback only in DEBUG
mode
---
src/calibre/customize/ui.py | 6 ++++--
1 file changed, 4 insertions(+), 2 deletions(-)
diff --git a/src/calibre/customize/ui.py b/src/calibre/customize/ui.py
index 151235cef9..3a2d638aab 100644
--- a/src/calibre/customize/ui.py
+++ b/src/calibre/customize/ui.py
@@ -19,6 +19,7 @@ from calibre.utils.config import (make_config_dir, Config, ConfigProxy,
plugin_dir, OptionParser)
from calibre.ebooks.epub.fix import ePubFixer
from calibre.ebooks.metadata.sources.base import Source
+from calibre.constants import DEBUG
builtin_names = frozenset([p.name for p in builtin_plugins])
@@ -487,8 +488,9 @@ def initialize_plugins():
plugin = initialize_plugin(plugin, None if isinstance(zfp, type) else zfp)
_initialized_plugins.append(plugin)
except:
- print 'Failed to initialize plugin...'
- traceback.print_exc()
+ print 'Failed to initialize plugin:', repr(zfp)
+ if DEBUG:
+ traceback.print_exc()
_initialized_plugins.sort(cmp=lambda x,y:cmp(x.priority, y.priority), reverse=True)
reread_filetype_plugins()
reread_metadata_plugins()
From 58c6733d3f9af650c5b7d7dc004e551d21d0d79e Mon Sep 17 00:00:00 2001
From: Charles Haley <>
Date: Mon, 2 May 2011 20:27:41 +0100
Subject: [PATCH 17/39] Add some documentation for the new User Defined device.
---
src/calibre/manual/faq.rst | 9 ++++++++-
1 file changed, 8 insertions(+), 1 deletion(-)
diff --git a/src/calibre/manual/faq.rst b/src/calibre/manual/faq.rst
index 2e2a8e5ae6..0e964516c4 100644
--- a/src/calibre/manual/faq.rst
+++ b/src/calibre/manual/faq.rst
@@ -100,7 +100,9 @@ Device Integration
What devices does |app| support?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-At the moment |app| has full support for the SONY PRS line, Barnes & Noble Nook line, Cybook Gen 3/Opus, Amazon Kindle line, Entourage Edge, Longshine ShineBook, Ectaco Jetbook, BeBook/BeBook Mini, Irex Illiad/DR1000, Foxit eSlick, PocketBook line, Italica, eClicto, Iriver Story, Airis dBook, Hanvon N515, Binatone Readme, Teclast K3 and clones, SpringDesign Alex, Kobo Reader, various Android phones and the iPhone/iPad. In addition, using the :guilabel:`Connect to folder` function you can use it with any ebook reader that exports itself as a USB disk.
+At the moment |app| has full support for the SONY PRS line, Barnes & Noble Nook line, Cybook Gen 3/Opus, Amazon Kindle line, Entourage Edge, Longshine ShineBook, Ectaco Jetbook, BeBook/BeBook Mini, Irex Illiad/DR1000, Foxit eSlick, PocketBook line, Italica, eClicto, Iriver Story, Airis dBook, Hanvon N515, Binatone Readme, Teclast K3 and clones, SpringDesign Alex, Kobo Reader, various Android phones and the iPhone/iPad. In addition, using the :guilabel:`Connect to folder` function you can use it with any ebook reader that exports itself as a USB disk.
+
+There is also a special ``User Defined`` device plugin that can be used to connect to arbitrary devices that present their memory as disk drives. See the device plugin ``Preferences -> Plugins -> Device Plugins -> User Defined`` and ``Preferences -> Miscelleaneous -> Get information to setup the user defined device`` for more information.
How can I help get my device supported in |app|?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -133,6 +135,11 @@ Follow these steps to find the problem:
* In calibre, go to Preferences->Plugins->Device Interface plugin and make sure the plugin for your device is enabled, the plugin icon next to it should be green when it is enabled.
* If all the above steps fail, go to Preferences->Miscellaneous and click debug device detection with your device attached and post the output as a ticket on `the calibre bug tracker `_.
+My device is non-standard or unusual. What can I do to connect to it?
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+In addition to the ``Connect to Folder`` function found under the Connect/Share menu (see :guilabel:`Connect to folder`), |app| provides a ``User Defined`` device plugin that can be used to connect to any USB device that presents its memory as disk drives. See the device plugin ``Preferences -> Plugins -> Device Plugins -> User Defined`` and ``Preferences -> Miscelleaneous -> Get information to setup the user defined device`` for more information.
+
How does |app| manage collections on my SONY reader?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
From fb87d6b9ad88ef54fe2bbf008a27430b6b954387 Mon Sep 17 00:00:00 2001
From: Kovid Goyal
Date: Mon, 2 May 2011 13:46:19 -0600
Subject: [PATCH 18/39] Fix #775825 (titlecase error on the word (part) macht)
---
src/calibre/utils/titlecase.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/calibre/utils/titlecase.py b/src/calibre/utils/titlecase.py
index bf2f9a78d4..1f153dd5fe 100755
--- a/src/calibre/utils/titlecase.py
+++ b/src/calibre/utils/titlecase.py
@@ -68,7 +68,7 @@ def titlecase(text):
continue
match = MAC_MC.match(word)
- if match and not match.group(2).startswith('hin'):
+ if match and not match.group(2)[:3] in ('hin', 'ht'):
line.append("%s%s" % (capitalize(match.group(1)),
capitalize(match.group(2))))
continue
From 375529f7cba71779bc74b264ba645bcf0daa9b80 Mon Sep 17 00:00:00 2001
From: Kovid Goyal
Date: Mon, 2 May 2011 16:41:53 -0600
Subject: [PATCH 19/39] Fix #775952 (Calibre not seeing dell streak 5 due to
new USB identifier)
---
src/calibre/devices/android/driver.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/calibre/devices/android/driver.py b/src/calibre/devices/android/driver.py
index f500560f97..ca84271778 100644
--- a/src/calibre/devices/android/driver.py
+++ b/src/calibre/devices/android/driver.py
@@ -62,7 +62,7 @@ class ANDROID(USBMS):
0x502 : { 0x3203 : [0x0100]},
# Dell
- 0x413c : { 0xb007 : [0x0100, 0x0224]},
+ 0x413c : { 0xb007 : [0x0100, 0x0224, 0x0226]},
# LG
0x1004 : { 0x61cc : [0x100], 0x61ce : [0x100], 0x618e : [0x226] },
From 5bf153338b11da59b4f24cdd1e68611513ac7daf Mon Sep 17 00:00:00 2001
From: Kovid Goyal
Date: Mon, 2 May 2011 18:28:38 -0600
Subject: [PATCH 20/39] BibTeX catalog: Convert all HTML comments to plain
text. Fixes #775051 (catalog BIB)
---
src/calibre/devices/user_defined/driver.py | 3 +--
src/calibre/library/catalog.py | 17 ++++++++++-------
src/calibre/utils/bibtex.py | 2 +-
3 files changed, 12 insertions(+), 10 deletions(-)
diff --git a/src/calibre/devices/user_defined/driver.py b/src/calibre/devices/user_defined/driver.py
index f57f61fe7c..c496422255 100644
--- a/src/calibre/devices/user_defined/driver.py
+++ b/src/calibre/devices/user_defined/driver.py
@@ -5,7 +5,6 @@ __copyright__ = '2009, Kovid Goyal '
__docformat__ = 'restructuredtext en'
from calibre.devices.usbms.driver import USBMS
-from calibre.ebooks import BOOK_EXTENSIONS
class USER_DEFINED(USBMS):
@@ -108,4 +107,4 @@ class USER_DEFINED(USBMS):
except:
import traceback
traceback.print_exc()
- USBMS.initialize(self)
\ No newline at end of file
+ USBMS.initialize(self)
diff --git a/src/calibre/library/catalog.py b/src/calibre/library/catalog.py
index 717e8e2c6b..aeecc3cfca 100644
--- a/src/calibre/library/catalog.py
+++ b/src/calibre/library/catalog.py
@@ -8,6 +8,7 @@ from collections import namedtuple
from copy import deepcopy
from xml.sax.saxutils import escape
from lxml import etree
+from types import StringType, UnicodeType
from calibre import prints, prepare_string_for_xml, strftime
from calibre.constants import preferred_encoding, DEBUG
@@ -15,13 +16,16 @@ from calibre.customize import CatalogPlugin
from calibre.customize.conversion import OptionRecommendation, DummyReporter
from calibre.ebooks.BeautifulSoup import BeautifulSoup, BeautifulStoneSoup, Tag, NavigableString
from calibre.ebooks.chardet import substitute_entites
+from calibre.library.save_to_disk import preprocess_template
from calibre.ptempfile import PersistentTemporaryDirectory
+from calibre.utils.bibtex import BibTeX
from calibre.utils.config import config_dir
from calibre.utils.date import format_date, isoformat, is_date_undefined, now as nowf
+from calibre.utils.html2text import html2text
from calibre.utils.icu import capitalize
from calibre.utils.logging import default_log as log
-from calibre.utils.zipfile import ZipFile, ZipInfo
from calibre.utils.magick.draw import thumbnail
+from calibre.utils.zipfile import ZipFile, ZipInfo
FIELDS = ['all', 'title', 'author_sort', 'authors', 'comments',
'cover', 'formats','id', 'isbn', 'ondevice', 'pubdate', 'publisher',
@@ -303,12 +307,6 @@ class BIBTEX(CatalogPlugin): # {{{
def run(self, path_to_output, opts, db, notification=DummyReporter()):
- from types import StringType, UnicodeType
-
- from calibre.library.save_to_disk import preprocess_template
- #Bibtex functions
- from calibre.utils.bibtex import BibTeX
-
def create_bibtex_entry(entry, fields, mode, template_citation,
bibtexdict, citation_bibtex=True, calibre_files=True):
@@ -365,6 +363,11 @@ class BIBTEX(CatalogPlugin): # {{{
#\n removal
item = item.replace(u'\r\n',u' ')
item = item.replace(u'\n',u' ')
+ #html to text
+ try:
+ item = html2text(item)
+ except:
+ log.warn("Failed to convert comments to text")
bibtex_entry.append(u'note = "%s"' % bibtexdict.utf8ToBibtex(item))
elif field == 'isbn' :
diff --git a/src/calibre/utils/bibtex.py b/src/calibre/utils/bibtex.py
index d19a6b05fe..518ec96611 100644
--- a/src/calibre/utils/bibtex.py
+++ b/src/calibre/utils/bibtex.py
@@ -2905,4 +2905,4 @@ class BibTeX:
def bibtex_author_format(self, item):
#Format authors for Bibtex compliance (get a list as input)
- return self.utf8ToBibtex(u' and'.join([author for author in item]))
+ return self.utf8ToBibtex(u' and '.join([author for author in item]))
From 25a4310fb90ccd8639f89e99cdda2387a302e2f0 Mon Sep 17 00:00:00 2001
From: "ken@szboeye.com" <>
Date: Tue, 3 May 2011 14:24:21 +0800
Subject: [PATCH 21/39] Add support for the Boeye Digital Reader .
---
src/calibre/customize/builtins.py | 3 +
src/calibre/customize/profiles.py | 89 ++++++++++++++++++++++++++-
src/calibre/devices/boeye/__init__.py | 0
src/calibre/devices/boeye/driver.py | 57 +++++++++++++++++
4 files changed, 147 insertions(+), 2 deletions(-)
create mode 100644 src/calibre/devices/boeye/__init__.py
create mode 100644 src/calibre/devices/boeye/driver.py
diff --git a/src/calibre/customize/builtins.py b/src/calibre/customize/builtins.py
index 776b04d5f6..c1da8391e0 100644
--- a/src/calibre/customize/builtins.py
+++ b/src/calibre/customize/builtins.py
@@ -613,6 +613,7 @@ from calibre.devices.misc import PALMPRE, AVANT, SWEEX, PDNOVEL, \
from calibre.devices.folder_device.driver import FOLDER_DEVICE_FOR_CONFIG
from calibre.devices.kobo.driver import KOBO
from calibre.devices.bambook.driver import BAMBOOK
+from calibre.devices.boeye.driver import BOEYE_BEX, BOEYE_BDX
from calibre.library.catalog import CSV_XML, EPUB_MOBI, BIBTEX
from calibre.ebooks.epub.fix.unmanifested import Unmanifested
@@ -743,6 +744,8 @@ plugins += [
EEEREADER,
NEXTBOOK,
ITUNES,
+ BOEYE_BEX,
+ BOEYE_BDX,
USER_DEFINED,
]
plugins += [x for x in list(locals().values()) if isinstance(x, type) and \
diff --git a/src/calibre/customize/profiles.py b/src/calibre/customize/profiles.py
index 5c29f1e79b..e771a36c2e 100644
--- a/src/calibre/customize/profiles.py
+++ b/src/calibre/customize/profiles.py
@@ -214,11 +214,51 @@ class NookInput(InputProfile):
dpi = 167
fbase = 16
fsizes = [12, 12, 14, 16, 18, 20, 22, 24]
+
+class BoeyeG5Input(InputProfile):
+
+ author = 'Ken'
+ name = 'Boeye Digital Reader G5'
+ short_name = 'boeyeg5'
+ description = _('This profile is intended for the Boeye G5.')
+
+ # Screen size is a best guess
+ screen_size = (600, 800)
+ dpi = 200
+ fbase = 16
+ fsizes = [12, 14, 16, 18, 20, 22, 24]
+
+class BoeyeG6Input(InputProfile):
+
+ author = 'Ken'
+ name = 'Boeye Digital Reader G6'
+ short_name = 'boeyeg6'
+ description = _('This profile is intended for the Boeye G6.')
+
+ # Screen size is a best guess
+ screen_size = (600, 800)
+ dpi = 166.66
+ fbase = 16
+ fsizes = [12, 14, 16, 18, 20, 22, 24]
+
+class BoeyeG10Input(InputProfile):
+
+ author = 'Ken'
+ name = 'Boeye Digital Reader G10'
+ short_name = 'boeyeg10'
+ description = _('This profile is intended for the Boeye G10.')
+
+ # Screen size is a best guess
+ screen_size = (825, 1200)
+ dpi = 150
+ fbase = 16
+ fsizes = [12, 14, 16, 18, 20, 22, 24]
+
input_profiles = [InputProfile, SonyReaderInput, SonyReader300Input,
SonyReader900Input, MSReaderInput, MobipocketInput, HanlinV3Input,
HanlinV5Input, CybookG3Input, CybookOpusInput, KindleInput, IlliadInput,
- IRexDR1000Input, IRexDR800Input, NookInput]
+ IRexDR1000Input, IRexDR800Input, NookInput, BoeyeG5Input, BoeyeG6Input, BoeyeG10Input]
input_profiles.sort(cmp=lambda x,y:cmp(x.name.lower(), y.name.lower()))
@@ -730,6 +770,50 @@ class BambookOutput(OutputProfile):
dpi = 168.451
fbase = 12
fsizes = [10, 12, 14, 16]
+
+class BoeyeG5Output(OutputProfile):
+
+ author = 'Ken'
+ name = 'Boeye Digital Reader G5'
+ short_name = 'boeyeg5'
+ description = _('This profile is intended for the Boeye Digital Reader G5.')
+
+ # Screen size is a best guess
+ screen_size = (600, 800)
+ comic_screen_size = (600, 740)
+ dpi = 200
+ fbase = 16
+ fsizes = [12, 14, 16, 18, 20, 22, 24]
+
+
+class BoeyeG6Output(OutputProfile):
+
+ author = 'Ken'
+ name = 'Boeye Digital Reader G6'
+ short_name = 'boeyeg6'
+ description = _('This profile is intended for the Boeye Digital Reader G6.')
+
+ # Screen size is a best guess
+ screen_size = (600, 800)
+ comic_screen_size = (600, 740)
+ dpi = 160
+ fbase = 16
+ fsizes = [12, 14, 16, 18, 20, 22, 24]
+
+class BoeyeG10Output(OutputProfile):
+
+ author = 'Ken'
+ name = 'Boeye Digital Reader G10'
+ short_name = 'boeyeg10'
+ description = _('This profile is intended for the Boeye Digital Reader G10.')
+
+ # Screen size is a best guess
+ screen_size = (825, 1200)
+ comic_screen_size = (824, 1140)
+ dpi = 150
+ fbase = 16
+ fsizes = [12, 14, 16, 18, 20, 22, 24]
+
output_profiles = [OutputProfile, SonyReaderOutput, SonyReader300Output,
SonyReader900Output, MSReaderOutput, MobipocketOutput, HanlinV3Output,
@@ -737,6 +821,7 @@ output_profiles = [OutputProfile, SonyReaderOutput, SonyReader300Output,
iPadOutput, KoboReaderOutput, TabletOutput, SamsungGalaxy,
SonyReaderLandscapeOutput, KindleDXOutput, IlliadOutput,
IRexDR1000Output, IRexDR800Output, JetBook5Output, NookOutput,
- BambookOutput, NookColorOutput, GenericEink, GenericEinkLarge]
+ BambookOutput, NookColorOutput, BoeyeG5Output, BoeyeG6Output, BoeyeG10Output,
+ GenericEink, GenericEinkLarge]
output_profiles.sort(cmp=lambda x,y:cmp(x.name.lower(), y.name.lower()))
diff --git a/src/calibre/devices/boeye/__init__.py b/src/calibre/devices/boeye/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/src/calibre/devices/boeye/driver.py b/src/calibre/devices/boeye/driver.py
new file mode 100644
index 0000000000..fcde1653d1
--- /dev/null
+++ b/src/calibre/devices/boeye/driver.py
@@ -0,0 +1,57 @@
+__license__ = 'GPL v3'
+__copyright__ = '2011, Ken '
+__docformat__ = 'restructuredtext en'
+
+'''
+Device driver for BOEYE serial readers
+'''
+
+import re
+from calibre.devices.usbms.driver import USBMS
+
+class BOEYE_BEX(USBMS):
+ name = 'BOEYE BEX reader driver'
+ gui_name = 'BOEYE BEX'
+ description = _('Communicate with BOEYE BEX Serial eBook readers.')
+ author = 'szboeye'
+ supported_platforms = ['windows', 'osx', 'linux']
+
+ FORMATS = ['epub', 'mobi', 'fb2', 'lit', 'prc', 'pdf', 'rtf', 'txt', 'djvu', 'doc', 'chm', 'html', 'zip', 'pdb']
+
+ VENDOR_ID = [0x0085]
+ PRODUCT_ID = [0x600]
+
+ VENDOR_NAME = 'LINUX'
+ WINDOWS_MAIN_MEM = 'FILE-STOR_GADGET'
+ OSX_MAIN_MEM = 'Linux File-Stor Gadget Media'
+
+ MAIN_MEMORY_VOLUME_LABEL = 'BOEYE BEX Storage Card'
+
+ EBOOK_DIR_MAIN = 'Documents'
+ SUPPORTS_SUB_DIRS = True
+
+class BOEYE_BDX(USBMS):
+ name = 'BOEYE BDX reader driver'
+ gui_name = 'BOEYE BDX'
+ description = _('Communicate with BOEYE BDX serial eBook readers.')
+ author = 'szboeye'
+ supported_platforms = ['windows', 'osx', 'linux']
+
+ FORMATS = ['epub', 'mobi', 'fb2', 'lit', 'prc', 'pdf', 'rtf', 'txt', 'djvu', 'doc', 'chm', 'html', 'zip', 'pdb']
+
+ VENDOR_ID = [0x0085]
+ PRODUCT_ID = [0x800]
+
+ VENDOR_NAME = 'LINUX'
+ WINDOWS_MAIN_MEM = 'FILE-STOR_GADGET'
+ WINDOWS_CARD_A_MEM = 'FILE-STOR_GADGET'
+
+ OSX_MAIN_MEM = 'Linux File-Stor Gadget Media'
+ OSX_CARD_A_MEM = 'Linux File-Stor Gadget Media'
+
+ MAIN_MEMORY_VOLUME_LABEL = 'BOEYE BDX Internal Memory'
+ STORAGE_CARD_VOLUME_LABEL = 'BOEYE BDX Storage Card'
+
+ EBOOK_DIR_MAIN = 'Documents'
+ EBOOK_DIR_CARD_A = 'Documents'
+ SUPPORTS_SUB_DIRS = True
From 731ff8eac4aef3bf27472f27f7d9d97c8d7d34cf Mon Sep 17 00:00:00 2001
From: Kovid Goyal
Date: Tue, 3 May 2011 09:39:41 -0600
Subject: [PATCH 22/39] ...
---
recipes/f_secure.recipe | 1 -
1 file changed, 1 deletion(-)
diff --git a/recipes/f_secure.recipe b/recipes/f_secure.recipe
index f276a4961a..5a03f01ac8 100644
--- a/recipes/f_secure.recipe
+++ b/recipes/f_secure.recipe
@@ -12,7 +12,6 @@ class AdvancedUserRecipe1301860159(BasicNewsRecipe):
max_articles_per_feed = 100
no_stylesheets = True
use_embedded_content = False
- language = 'en_EN'
remove_javascript = True
keep_only_tags = [dict(name='div', attrs={'class':'modSectionTd2'})]
remove_tags = [dict(name='a'),dict(name='hr')]
From 72120ed403490d4a9938b67f476639d92f49fd13 Mon Sep 17 00:00:00 2001
From: Kovid Goyal
Date: Tue, 3 May 2011 10:15:46 -0600
Subject: [PATCH 23/39] Ensure the Preferences menu in OSX has an action to
launch the preferences
---
src/calibre/gui2/actions/preferences.py | 5 +++--
1 file changed, 3 insertions(+), 2 deletions(-)
diff --git a/src/calibre/gui2/actions/preferences.py b/src/calibre/gui2/actions/preferences.py
index 24d20b23f9..b65967e994 100644
--- a/src/calibre/gui2/actions/preferences.py
+++ b/src/calibre/gui2/actions/preferences.py
@@ -10,7 +10,7 @@ from PyQt4.Qt import QIcon, QMenu, Qt
from calibre.gui2.actions import InterfaceAction
from calibre.gui2.preferences.main import Preferences
from calibre.gui2 import error_dialog
-from calibre.constants import DEBUG
+from calibre.constants import DEBUG, isosx
class PreferencesAction(InterfaceAction):
@@ -19,7 +19,8 @@ class PreferencesAction(InterfaceAction):
def genesis(self):
pm = QMenu()
- pm.addAction(QIcon(I('config.png')), _('Preferences'), self.do_config)
+ acname = _('Change calibre behavior') if isosx else _('Preferences')
+ pm.addAction(QIcon(I('config.png')), acname, self.do_config)
pm.addAction(QIcon(I('wizard.png')), _('Run welcome wizard'),
self.gui.run_wizard)
if not DEBUG:
From 62892d7161d414f18b62bab766f15ad5651ce47f Mon Sep 17 00:00:00 2001
From: Kovid Goyal
Date: Tue, 3 May 2011 14:10:35 -0600
Subject: [PATCH 24/39] Add Get Books action to main toolbar by default
---
src/calibre/gui2/__init__.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/calibre/gui2/__init__.py b/src/calibre/gui2/__init__.py
index 60d2a0a7dd..1dfe1d8d14 100644
--- a/src/calibre/gui2/__init__.py
+++ b/src/calibre/gui2/__init__.py
@@ -34,7 +34,7 @@ if isosx:
)
gprefs.defaults['action-layout-toolbar'] = (
'Add Books', 'Edit Metadata', None, 'Convert Books', 'View', None,
- 'Choose Library', 'Donate', None, 'Fetch News', 'Save To Disk',
+ 'Choose Library', 'Donate', None, 'Fetch News', 'Store', 'Save To Disk',
'Connect Share', None, 'Remove Books',
)
gprefs.defaults['action-layout-toolbar-device'] = (
@@ -48,7 +48,7 @@ else:
gprefs.defaults['action-layout-menubar-device'] = ()
gprefs.defaults['action-layout-toolbar'] = (
'Add Books', 'Edit Metadata', None, 'Convert Books', 'View', None,
- 'Choose Library', 'Donate', None, 'Fetch News', 'Save To Disk',
+ 'Choose Library', 'Donate', None, 'Fetch News', 'Store', 'Save To Disk',
'Connect Share', None, 'Remove Books', None, 'Help', 'Preferences',
)
gprefs.defaults['action-layout-toolbar-device'] = (
From b9973c45404f0457684bbbfa1daf0f143bd492a8 Mon Sep 17 00:00:00 2001
From: Charles Haley <>
Date: Tue, 3 May 2011 21:19:47 +0100
Subject: [PATCH 25/39] Add an 'all metadata' edit metadata variant
---
src/calibre/gui2/metadata/single.py | 136 ++++++++++++++++++++++-
src/calibre/gui2/preferences/behavior.py | 3 +-
2 files changed, 134 insertions(+), 5 deletions(-)
diff --git a/src/calibre/gui2/metadata/single.py b/src/calibre/gui2/metadata/single.py
index 63d4499966..e85a0adc13 100644
--- a/src/calibre/gui2/metadata/single.py
+++ b/src/calibre/gui2/metadata/single.py
@@ -13,7 +13,7 @@ from functools import partial
from PyQt4.Qt import (Qt, QVBoxLayout, QHBoxLayout, QWidget, QPushButton,
QGridLayout, pyqtSignal, QDialogButtonBox, QScrollArea, QFont,
QTabWidget, QIcon, QToolButton, QSplitter, QGroupBox, QSpacerItem,
- QSizePolicy, QPalette, QFrame, QSize, QKeySequence, QMenu)
+ QSizePolicy, QPalette, QFrame, QSize, QKeySequence, QMenu, QLabel)
from calibre.ebooks.metadata import authors_to_string, string_to_authors
from calibre.gui2 import ResizableDialog, error_dialog, gprefs, pixmap_to_data
@@ -198,7 +198,7 @@ class MetadataSingleDialogBase(ResizableDialog):
ans = self.custom_metadata_widgets
for i in range(len(ans)-1):
if before is not None and i == 0:
- pass# Do something
+ pass
if len(ans[i+1].widgets) == 2:
sto(ans[i].widgets[-1], ans[i+1].widgets[1])
else:
@@ -206,7 +206,7 @@ class MetadataSingleDialogBase(ResizableDialog):
for c in range(2, len(ans[i].widgets), 2):
sto(ans[i].widgets[c-1], ans[i].widgets[c+1])
if after is not None:
- pass # Do something
+ pass
# }}}
def do_view_format(self, path, fmt):
@@ -728,7 +728,135 @@ class MetadataSingleDialogAlt1(MetadataSingleDialogBase): # {{{
# }}}
-editors = {'default': MetadataSingleDialog, 'alt1': MetadataSingleDialogAlt1}
+class MetadataSingleDialogAlt2(MetadataSingleDialogBase): # {{{
+
+ cc_two_column = False
+ one_line_comments_toolbar = True
+
+ def do_layout(self):
+ self.central_widget.clear()
+ self.labels = []
+ sto = QWidget.setTabOrder
+
+ self.central_widget.tabBar().setVisible(False)
+ tab0 = QWidget(self)
+ self.central_widget.addTab(tab0, _("&Metadata"))
+ l = QGridLayout()
+ tab0.setLayout(l)
+
+ # Basic metadata in col 0
+ tl = QGridLayout()
+ gb = QGroupBox(_('Basic metadata'), tab0)
+ l.addWidget(gb, 0, 0, 1, 1)
+ gb.setLayout(tl)
+
+ self.button_box.addButton(self.fetch_metadata_button,
+ QDialogButtonBox.ActionRole)
+ self.config_metadata_button.setToolButtonStyle(Qt.ToolButtonTextOnly)
+ self.config_metadata_button.setText(_('Configure metadata downloading'))
+ self.button_box.addButton(self.config_metadata_button,
+ QDialogButtonBox.ActionRole)
+ sto(self.button_box, self.title)
+
+ def create_row(row, widget, tab_to, button=None, icon=None, span=1):
+ ql = BuddyLabel(widget)
+ tl.addWidget(ql, row, 1, 1, 1)
+ tl.addWidget(widget, row, 2, 1, 1)
+ if button is not None:
+ tl.addWidget(button, row, 3, span, 1)
+ if icon is not None:
+ button.setIcon(QIcon(I(icon)))
+ if tab_to is not None:
+ if button is not None:
+ sto(widget, button)
+ sto(button, tab_to)
+ else:
+ sto(widget, tab_to)
+
+ tl.addWidget(self.swap_title_author_button, 0, 0, 2, 1)
+
+ create_row(0, self.title, self.title_sort,
+ button=self.deduce_title_sort_button, span=2,
+ icon='auto_author_sort.png')
+ create_row(1, self.title_sort, self.authors)
+ create_row(2, self.authors, self.author_sort,
+ button=self.deduce_author_sort_button,
+ span=2, icon='auto_author_sort.png')
+ create_row(3, self.author_sort, self.series)
+ create_row(4, self.series, self.series_index,
+ button=self.remove_unused_series_button, icon='trash.png')
+ create_row(5, self.series_index, self.tags)
+ create_row(6, self.tags, self.rating, button=self.tags_editor_button)
+ create_row(7, self.rating, self.pubdate)
+ create_row(8, self.pubdate, self.publisher,
+ button=self.pubdate.clear_button, icon='trash.png')
+ create_row(9, self.publisher, self.timestamp)
+ create_row(10, self.timestamp, self.identifiers,
+ button=self.timestamp.clear_button, icon='trash.png')
+ create_row(11, self.identifiers, self.comments,
+ button=self.clear_identifiers_button, icon='trash.png')
+ tl.addItem(QSpacerItem(1, 1, QSizePolicy.Fixed, QSizePolicy.Expanding),
+ 12, 1, 1 ,1)
+
+ # Custom metadata in col 1
+ w = getattr(self, 'custom_metadata_widgets_parent', None)
+ if w is not None:
+ gb = QGroupBox(_('Custom metadata'), tab0)
+ gbl = QVBoxLayout()
+ gb.setLayout(gbl)
+ sr = QScrollArea(gb)
+ sr.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
+ sr.setWidgetResizable(True)
+ sr.setBackgroundRole(QPalette.Base)
+ sr.setFrameStyle(QFrame.NoFrame)
+ sr.setWidget(w)
+ gbl.addWidget(sr)
+ l.addWidget(gb, 0, 1, 1, 1)
+ sp = QSizePolicy()
+ sp.setVerticalStretch(10)
+ sp.setHorizontalPolicy(QSizePolicy.Fixed)
+ sp.setVerticalPolicy(QSizePolicy.Expanding)
+ gb.setSizePolicy(sp)
+ self.set_custom_metadata_tab_order()
+
+ # comments span col 0 & 1
+ w = QGroupBox(_('Comments'), tab0)
+ sp = QSizePolicy()
+ sp.setVerticalStretch(10)
+ sp.setHorizontalPolicy(QSizePolicy.Expanding)
+ sp.setVerticalPolicy(QSizePolicy.Expanding)
+ w.setSizePolicy(sp)
+ lb = QHBoxLayout()
+ w.setLayout(lb)
+ lb.addWidget(self.comments)
+ l.addWidget(w, 1, 0, 1, 2)
+
+ # Cover & formats in col 3
+ gb = QGroupBox(_('Cover'), tab0)
+ lb = QGridLayout()
+ gb.setLayout(lb)
+ lb.addWidget(self.cover, 0, 0, 1, 3, alignment=Qt.AlignCenter)
+ sto(self.clear_identifiers_button, self.cover.buttons[0])
+ for i, b in enumerate(self.cover.buttons[:3]):
+ lb.addWidget(b, 1, i, 1, 1)
+ sto(b, self.cover.buttons[i+1])
+ hl = QHBoxLayout()
+ for b in self.cover.buttons[3:]:
+ hl.addWidget(b)
+ sto(self.cover.buttons[-2], self.cover.buttons[-1])
+ lb.addLayout(hl, 2, 0, 1, 3)
+ l.addWidget(gb, 0, 2, 1, 1)
+ l.addWidget(self.formats_manager, 1, 2, 1, 1)
+ sto(self.cover.buttons[-1], self.formats_manager)
+
+ self.formats_manager.formats.setMaximumWidth(10000)
+ self.formats_manager.formats.setIconSize(QSize(32, 32))
+
+# }}}
+
+
+editors = {'default': MetadataSingleDialog, 'alt1': MetadataSingleDialogAlt1,
+ 'alt2': MetadataSingleDialogAlt2}
def edit_metadata(db, row_list, current_row, parent=None, view_slot=None,
set_current_callback=None):
diff --git a/src/calibre/gui2/preferences/behavior.py b/src/calibre/gui2/preferences/behavior.py
index e062ae2662..1247c54ec9 100644
--- a/src/calibre/gui2/preferences/behavior.py
+++ b/src/calibre/gui2/preferences/behavior.py
@@ -61,7 +61,8 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
r('bools_are_tristate', db.prefs, restart_required=True)
r = self.register
- choices = [(_('Default'), 'default'), (_('Compact Metadata'), 'alt1')]
+ choices = [(_('Default'), 'default'), (_('Compact Metadata'), 'alt1'),
+ (_('All on 1 tab'), 'alt2')]
r('edit_metadata_single_layout', gprefs, choices=choices)
def initialize(self):
From 3ac69128e7899d8c24bb5c1cfbac1a91c9c26757 Mon Sep 17 00:00:00 2001
From: Kovid Goyal
Date: Tue, 3 May 2011 16:04:59 -0600
Subject: [PATCH 26/39] Update FrazPC
---
recipes/frazpc.recipe | 27 +++++++++++++++------------
1 file changed, 15 insertions(+), 12 deletions(-)
diff --git a/recipes/frazpc.recipe b/recipes/frazpc.recipe
index 56e45076ac..2d0d54d10c 100644
--- a/recipes/frazpc.recipe
+++ b/recipes/frazpc.recipe
@@ -1,7 +1,7 @@
#!/usr/bin/env python
__license__ = 'GPL v3'
-__copyright__ = u'2010, Tomasz Dlugosz '
+__copyright__ = u'2010-2011, Tomasz Dlugosz '
'''
frazpc.pl
'''
@@ -19,17 +19,20 @@ class FrazPC(BasicNewsRecipe):
use_embedded_content = False
no_stylesheets = True
- feeds = [(u'Aktualno\u015bci', u'http://www.frazpc.pl/feed'), (u'Recenzje', u'http://www.frazpc.pl/kat/recenzje-2/feed') ]
-
- keep_only_tags = [dict(name='div', attrs={'id':'FRAZ_CONTENT'})]
-
- remove_tags = [dict(name='p', attrs={'class':'gray tagsP fs11'})]
-
- preprocess_regexps = [
- (re.compile(i[0], re.IGNORECASE | re.DOTALL), i[1]) for i in
- [(r'
(Skomentuj|Komentarz(e)?\([0-9]*\)) \|', lambda match: '')]
+ feeds = [
+ (u'Aktualno\u015bci', u'http://www.frazpc.pl/feed/aktualnosci'),
+ (u'Artyku\u0142y', u'http://www.frazpc.pl/feed/artykuly')
]
+ keep_only_tags = [dict(name='div', attrs={'class':'article'})]
+
+ remove_tags = [
+ dict(name='div', attrs={'class':'title-wrapper'}),
+ dict(name='p', attrs={'class':'tags'}),
+ dict(name='p', attrs={'class':'article-links'}),
+ dict(name='div', attrs={'class':'comments_box'})
+ ]
+
+ preprocess_regexps = [(re.compile(r'\|
Komentarze \([0-9]*\)'), lambda match: '')]
+
remove_attributes = [ 'width', 'height' ]
From 0168ce58667ce362d62bbc4a1f524e5882f39184 Mon Sep 17 00:00:00 2001
From: John Schember
Date: Tue, 3 May 2011 19:13:57 -0400
Subject: [PATCH 27/39] GUI: OS X fix, show books in library when device
connected as menu item so user can go to their library.
---
src/calibre/gui2/layout.py | 22 +++++++++++++++-------
1 file changed, 15 insertions(+), 7 deletions(-)
diff --git a/src/calibre/gui2/layout.py b/src/calibre/gui2/layout.py
index b5cc0163ed..b3c9bd3a02 100644
--- a/src/calibre/gui2/layout.py
+++ b/src/calibre/gui2/layout.py
@@ -44,18 +44,19 @@ class LocationManager(QObject): # {{{
receiver = partial(self._location_selected, name)
ac.triggered.connect(receiver)
self.tooltips[name] = tooltip
+
+ m = QMenu(parent)
+ self._mem.append(m)
+ a = m.addAction(icon, tooltip)
+ a.triggered.connect(receiver)
if name != 'library':
- m = QMenu(parent)
- self._mem.append(m)
- a = m.addAction(icon, tooltip)
- a.triggered.connect(receiver)
self._mem.append(a)
a = m.addAction(QIcon(I('eject.png')), _('Eject this device'))
a.triggered.connect(self._eject_requested)
- ac.setMenu(m)
self._mem.append(a)
else:
ac.setToolTip(tooltip)
+ ac.setMenu(m)
ac.calibre_name = name
return ac
@@ -71,7 +72,12 @@ class LocationManager(QObject): # {{{
def set_switch_actions(self, quick_actions, rename_actions, delete_actions,
switch_actions, choose_action):
- self.switch_menu = QMenu()
+ self.switch_menu = self.library_action.menu()
+ if self.switch_menu:
+ self.switch_menu.addSeparator()
+ else:
+ self.switch_menu = QMenu()
+
self.switch_menu.addAction(choose_action)
self.cs_menus = []
for t, acs in [(_('Quick switch'), quick_actions),
@@ -85,7 +91,9 @@ class LocationManager(QObject): # {{{
self.switch_menu.addSeparator()
for ac in switch_actions:
self.switch_menu.addAction(ac)
- self.library_action.setMenu(self.switch_menu)
+
+ if self.switch_menu != self.library_action.menu():
+ self.library_action.setMenu(self.switch_menu)
def _location_selected(self, location, *args):
if location != self.current_location and hasattr(self,
From ce28d66991ebd825ce41ecedb68503be2c243267 Mon Sep 17 00:00:00 2001
From: John Schember
Date: Tue, 3 May 2011 19:37:27 -0400
Subject: [PATCH 28/39] Store: Open external quick check in search dialog.
---
src/calibre/gui2/store/search/search.py | 5 ++++-
src/calibre/gui2/store/search/search.ui | 12 +++++++++++-
2 files changed, 15 insertions(+), 2 deletions(-)
diff --git a/src/calibre/gui2/store/search/search.py b/src/calibre/gui2/store/search/search.py
index 5654df8ffc..07d4afca54 100644
--- a/src/calibre/gui2/store/search/search.py
+++ b/src/calibre/gui2/store/search/search.py
@@ -155,6 +155,7 @@ class SearchDialog(QDialog, Ui_Dialog):
self.config['results_view_column_width'] = [self.results_view.columnWidth(i) for i in range(self.results_view.model().columnCount())]
self.config['sort_col'] = self.results_view.model().sort_col
self.config['sort_order'] = self.results_view.model().sort_order
+ self.config['open_external'] = self.open_external.isChecked()
store_check = {}
for n in self.store_plugins:
@@ -179,6 +180,8 @@ class SearchDialog(QDialog, Ui_Dialog):
else:
self.resize_columns()
+ self.open_external.setChecked(self.config.get('open_external', False))
+
store_check = self.config.get('store_checked', None)
if store_check:
for n in store_check:
@@ -212,7 +215,7 @@ class SearchDialog(QDialog, Ui_Dialog):
def open_store(self, index):
result = self.results_view.model().get_result(index)
- self.store_plugins[result.store_name].open(self, result.detail_item)
+ self.store_plugins[result.store_name].open(self, result.detail_item, self.open_external.isChecked())
def check_progress(self):
if not self.search_pool.threads_running() and not self.results_view.model().cover_pool.threads_running() and not self.results_view.model().details_pool.threads_running():
diff --git a/src/calibre/gui2/store/search/search.ui b/src/calibre/gui2/store/search/search.ui
index 0d39a70a29..7e8dd36284 100644
--- a/src/calibre/gui2/store/search/search.ui
+++ b/src/calibre/gui2/store/search/search.ui
@@ -70,7 +70,7 @@
0
0
215
- 116
+ 93
@@ -101,6 +101,16 @@
+ -
+
+
+ Open a selected book in the system's web browser
+
+
+ Open external
+
+
+
From bb0e6a60e749ad5fe9768fcee11a45b3d4ded7de Mon Sep 17 00:00:00 2001
From: Kovid Goyal
Date: Wed, 4 May 2011 10:24:23 -0600
Subject: [PATCH 29/39] ...
---
recipes/telepolis.recipe | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/recipes/telepolis.recipe b/recipes/telepolis.recipe
index 4ca57f8275..8109e3e39a 100644
--- a/recipes/telepolis.recipe
+++ b/recipes/telepolis.recipe
@@ -18,7 +18,7 @@ class TelepolisNews(BasicNewsRecipe):
recursion = 0
no_stylesheets = True
encoding = "utf-8"
- language = 'de_AT'
+ language = 'de'
use_embedded_content =False
remove_empty_feeds = True
From bfbd42dd6d0bc5494162cea64c981e46ab8ab8be Mon Sep 17 00:00:00 2001
From: Kovid Goyal
Date: Wed, 4 May 2011 10:39:20 -0600
Subject: [PATCH 30/39] Fix USA Today
---
recipes/usatoday.recipe | 397 ++--------------------------------------
1 file changed, 20 insertions(+), 377 deletions(-)
diff --git a/recipes/usatoday.recipe b/recipes/usatoday.recipe
index bd47262563..a4899b7187 100644
--- a/recipes/usatoday.recipe
+++ b/recipes/usatoday.recipe
@@ -7,13 +7,11 @@ usatoday.com
'''
from calibre.web.feeds.news import BasicNewsRecipe
-from calibre.ebooks.BeautifulSoup import BeautifulSoup, BeautifulStoneSoup, NavigableString, Tag
-import re
class USAToday(BasicNewsRecipe):
title = 'USA Today'
- __author__ = 'GRiker'
+ __author__ = 'Kovid Goyal'
oldest_article = 1
timefmt = ''
max_articles_per_feed = 20
@@ -31,7 +29,6 @@ class USAToday(BasicNewsRecipe):
margin-bottom: 0em; \
font-size: smaller;}\n \
.articleBody {text-align: left;}\n '
- conversion_options = { 'linearize_tables' : True }
#simultaneous_downloads = 1
feeds = [
('Top Headlines', 'http://rssfeeds.usatoday.com/usatoday-NewsTopStories'),
@@ -47,63 +44,26 @@ class USAToday(BasicNewsRecipe):
('Most Popular', 'http://rssfeeds.usatoday.com/Usatoday-MostViewedArticles'),
('Offbeat News', 'http://rssfeeds.usatoday.com/UsatodaycomOffbeat-TopStories'),
]
- keep_only_tags = [dict(attrs={'class':[
- 'byLine',
- 'inside-copy',
- 'inside-head',
- 'inside-head2',
- 'item',
- 'item-block',
- 'photo-container',
- ]}),
- dict(id=[
- 'applyMainStoryPhoto',
- 'permalink',
- ])]
+ keep_only_tags = [dict(attrs={'class':'story'})]
+ remove_tags = [
+ dict(attrs={'class':[
+ 'share',
+ 'reprints',
+ 'inline-h3',
+ 'info-extras',
+ 'ppy-outer',
+ 'ppy-caption',
+ 'comments',
+ 'jump',
+ 'pagetools',
+ 'post-attributes',
+ 'tags',
+ 'bottom-tools',
+ 'sponsoredlinks',
+ ]}),
+ dict(id=['pluck']),
+ ]
- remove_tags = [dict(attrs={'class':[
- 'comments',
- 'jump',
- 'pagetools',
- 'post-attributes',
- 'tags',
- ]}),
- dict(id=[])]
-
- #feeds = [('Most Popular', 'http://rssfeeds.usatoday.com/Usatoday-MostViewedArticles')]
-
- def dump_hex(self, src, length=16):
- ''' Diagnostic '''
- FILTER=''.join([(len(repr(chr(x)))==3) and chr(x) or '.' for x in range(256)])
- N=0; result=''
- while src:
- s,src = src[:length],src[length:]
- hexa = ' '.join(["%02X"%ord(x) for x in s])
- s = s.translate(FILTER)
- result += "%04X %-*s %s\n" % (N, length*3, hexa, s)
- N+=length
- print result
-
- def fixChars(self,string):
- # Replace lsquo (\x91)
- fixed = re.sub("\x91","‘",string)
-
- # Replace rsquo (\x92)
- fixed = re.sub("\x92","’",fixed)
-
- # Replace ldquo (\x93)
- fixed = re.sub("\x93","“",fixed)
-
- # Replace rdquo (\x94)
- fixed = re.sub("\x94","”",fixed)
-
- # Replace ndash (\x96)
- fixed = re.sub("\x96","–",fixed)
-
- # Replace mdash (\x97)
- fixed = re.sub("\x97","—",fixed)
-
- return fixed
def get_masthead_url(self):
masthead = 'http://i.usatoday.net/mobile/_common/_images/565x73_usat_mobile.gif'
@@ -115,321 +75,4 @@ class USAToday(BasicNewsRecipe):
masthead = None
return masthead
- def massageNCXText(self, description):
- # Kindle TOC descriptions won't render certain characters
- if description:
- massaged = unicode(BeautifulStoneSoup(description, convertEntities=BeautifulStoneSoup.HTML_ENTITIES))
- # Replace '&' with '&'
- massaged = re.sub("&","&", massaged)
- return self.fixChars(massaged)
- else:
- return description
- def parse_feeds(self, *args, **kwargs):
- parsed_feeds = BasicNewsRecipe.parse_feeds(self, *args, **kwargs)
- # Count articles for progress dialog
- article_count = 0
- for feed in parsed_feeds:
- article_count += len(feed)
- self.log( "Queued %d articles" % article_count)
- return parsed_feeds
-
- def preprocess_html(self, soup):
- soup = self.strip_anchors(soup)
- return soup
-
- def postprocess_html(self, soup, first_fetch):
-
- # Remove navLinks
- navLinks = soup.find(True,{'style':'padding-bottom:3px'})
- if navLinks:
- navLinks.extract()
-
- # Remove
- gibberish = soup.find(True,{'style':'margin-bottom:10px'})
- if gibberish:
- gibberish.extract()
-
- # Change
to
- headline = soup.find(True, {'class':['inside-head','inside-head2']})
- if not headline:
- headline = soup.find('h3')
- if headline:
- tag = Tag(soup, "h2")
- tag['class'] = "headline"
- tag.insert(0, headline.contents[0])
- headline.replaceWith(tag)
- else:
- print "unable to find headline:\n%s\n" % soup
-
- # Change byLine to byline, change commas to middot
- # Kindle renders commas in byline as '&'
- byline = soup.find(True, {'class':'byLine'})
- if byline:
- byline['class'] = 'byline'
- # Replace comma with middot
- byline.contents[0].replaceWith(re.sub(","," ·", byline.renderContents()))
-
- jumpout_punc_list = [':','?']
- # Remove the inline jumpouts in
- paras = soup.findAll(True, {'class':'inside-copy'})
- for para in paras:
- if re.match("
[\w\W]+ ",para.renderContents()):
- p = para.find('b')
- for punc in jumpout_punc_list:
- punc_offset = p.contents[0].find(punc)
- if punc_offset == -1:
- continue
- if punc_offset > 1:
- if p.contents[0][:punc_offset] == p.contents[0][:punc_offset].upper():
- #print "extracting \n%s\n" % para.prettify()
- para.extract()
-
- # Reset class for remaining
- paras = soup.findAll(True, {'class':'inside-copy'})
- for para in paras:
- para['class'] = 'articleBody'
-
- # Remove inline jumpouts in
- paras = soup.findAll(['p'])
- for p in paras:
- if hasattr(p,'contents') and len(p.contents):
- for punc in jumpout_punc_list:
- punc_offset = p.contents[0].find(punc)
- if punc_offset == -1:
- continue
- if punc_offset > 2 and hasattr(p,'a') and len(p.contents):
- #print "evaluating %s\n" % p.contents[0][:punc_offset+1]
- if p.contents[0][:punc_offset] == p.contents[0][:punc_offset].upper():
- #print "extracting \n%s\n" % p.prettify()
- p.extract()
-
- # Capture the first img, insert after headline
- imgs = soup.findAll('img')
- print "postprocess_html(): %d images" % len(imgs)
- if imgs:
- divTag = Tag(soup, 'div')
- divTag['class'] = 'image'
- body = soup.find('body')
- img = imgs[0]
- #print "img: \n%s\n" % img.prettify()
-
- # Table for photo and credit
- tableTag = Tag(soup,'table')
-
- # Photo
- trimgTag = Tag(soup, 'tr')
- tdimgTag = Tag(soup, 'td')
- tdimgTag.insert(0,img)
- trimgTag.insert(0,tdimgTag)
- tableTag.insert(0,trimgTag)
-
- # Credit
- trcreditTag = Tag(soup, 'tr')
-
- tdcreditTag = Tag(soup, 'td')
- tdcreditTag['class'] = 'credit'
- credit = soup.find('td',{'class':'photoCredit'})
- if credit:
- tdcreditTag.insert(0,NavigableString(credit.renderContents()))
- else:
- credit = img['credit']
- if credit:
- tdcreditTag.insert(0,NavigableString(credit))
- else:
- tdcreditTag.insert(0,NavigableString(''))
-
- trcreditTag.insert(0,tdcreditTag)
- tableTag.insert(1,trcreditTag)
- dtc = 0
- divTag.insert(dtc,tableTag)
- dtc += 1
-
- if False:
- # Add the caption in the table
- tableCaptionTag = Tag(soup,'caption')
- tableCaptionTag.insert(0,soup.find('td',{'class':'photoCredit'}).renderContents())
- tableTag.insert(1,tableCaptionTag)
- divTag.insert(dtc,tableTag)
- dtc += 1
- body.insert(1,divTag)
- else:
- # Add the caption below the table
- #print "Looking for caption in this soup:\n%s" % img.prettify()
- captionTag = Tag(soup,'p')
- captionTag['class'] = 'caption'
- if hasattr(img,'alt') and img['alt']:
- captionTag.insert(0,NavigableString('
%s
' % img['alt']))
- divTag.insert(dtc, captionTag)
- dtc += 1
- else:
- try:
- captionTag.insert(0,NavigableString('
%s
' % img['cutline']))
- divTag.insert(dtc, captionTag)
- dtc += 1
- except:
- pass
-
- hrTag = Tag(soup, 'hr')
- divTag.insert(dtc, hrTag)
- dtc += 1
-
- # Delete
- restructure
- tag = body.find(True)
- while True:
- insertLoc += 1
- try:
- if hasattr(tag,'class') and tag['class'] == 'headline':
- headline_found = True
- tag.insert(insertLoc,divTag)
- break
- except:
- pass
- tag = tag.next
- if not tag:
- break
-
- # Yank out headline, img and caption
- headline = body.find('h2','headline')
- img = body.find('div','image')
- caption = body.find('p''class')
-
- # body(0) is calibre_navbar
- # body(1) is
-
- btc = 1
- headline.extract()
- body.insert(1, headline)
- btc += 1
- if img:
- img.extract()
- body.insert(btc, img)
- btc += 1
- if caption:
- caption.extract()
- body.insert(btc, caption)
- btc += 1
-
- if len(imgs) > 1:
- if True:
- [img.extract() for img in imgs[1:]]
- else:
- # Format the remaining images
- # This doesn't work yet
- for img in imgs[1:]:
- print "img:\n%s\n" % img.prettify()
- divTag = Tag(soup, 'div')
- divTag['class'] = 'image'
-
- # Table for photo and credit
- tableTag = Tag(soup,'table')
-
- # Photo
- trimgTag = Tag(soup, 'tr')
- tdimgTag = Tag(soup, 'td')
- tdimgTag.insert(0,img)
- trimgTag.insert(0,tdimgTag)
- tableTag.insert(0,trimgTag)
-
- # Credit
- trcreditTag = Tag(soup, 'tr')
-
- tdcreditTag = Tag(soup, 'td')
- tdcreditTag['class'] = 'credit'
- try:
- tdcreditTag.insert(0,NavigableString(img['credit']))
- except:
- tdcreditTag.insert(0,NavigableString(''))
- trcreditTag.insert(0,tdcreditTag)
- tableTag.insert(1,trcreditTag)
- divTag.insert(0,tableTag)
- soup.img.replaceWith(divTag)
-
- return soup
-
- def postprocess_book(self, oeb, opts, log) :
-
- def extract_byline(href) :
- #
'' :
- return self.massageNCXText(self.tag_to_string(p,use_alt=False))
- else:
- print "Didn't find
in this soup:\n%s" % soup.prettify()
- return None
-
- # Method entry point here
- # Single section toc looks different than multi-section tocs
- if oeb.toc.depth() == 2 :
- for article in oeb.toc :
- if article.author is None :
- article.author = extract_byline(article.href)
- if article.description is None :
- article.description = extract_description(article.href)
- elif oeb.toc.depth() == 3 :
- for section in oeb.toc :
- for article in section :
- article.author = extract_byline(article.href)
- '''
- if article.author is None :
- article.author = self.massageNCXText(extract_byline(article.href))
- else:
- article.author = self.massageNCXText(article.author)
- '''
- if article.description is None :
- article.description = extract_description(article.href)
-
- def strip_anchors(self,soup):
- paras = soup.findAll(True)
- for para in paras:
- aTags = para.findAll('a')
- for a in aTags:
- if a.img is None:
- a.replaceWith(a.renderContents().decode('cp1252','replace'))
- return soup
From 1400f54107185d80ea880bc786de78910a382e48 Mon Sep 17 00:00:00 2001
From: Kovid Goyal
Date: Wed, 4 May 2011 11:02:56 -0600
Subject: [PATCH 31/39] EPUB metadata: When extracting covers from epub files
handle invalid epubs that specify their content as a raster cover. Apparently
PG produces these.
---
src/calibre/ebooks/metadata/opf2.py | 4 +++-
src/calibre/gui2/actions/choose_library.py | 2 +-
2 files changed, 4 insertions(+), 2 deletions(-)
diff --git a/src/calibre/ebooks/metadata/opf2.py b/src/calibre/ebooks/metadata/opf2.py
index 58c887bfdb..1d91236757 100644
--- a/src/calibre/ebooks/metadata/opf2.py
+++ b/src/calibre/ebooks/metadata/opf2.py
@@ -966,7 +966,9 @@ class OPF(object): # {{{
cover_id = covers[0].get('content')
for item in self.itermanifest():
if item.get('id', None) == cover_id:
- return item.get('href', None)
+ mt = item.get('media-type', '')
+ if 'xml' not in mt:
+ return item.get('href', None)
@dynamic_property
def cover(self):
diff --git a/src/calibre/gui2/actions/choose_library.py b/src/calibre/gui2/actions/choose_library.py
index 4b262ad9dd..a663f288af 100644
--- a/src/calibre/gui2/actions/choose_library.py
+++ b/src/calibre/gui2/actions/choose_library.py
@@ -246,7 +246,7 @@ class ChooseLibraryAction(InterfaceAction):
def delete_requested(self, name, location):
loc = location.replace('/', os.sep)
if not question_dialog(self.gui, _('Are you sure?'), ''+
- _('All files from %s will be '
+ _('All files from %s will be '
'permanently deleted. Are you sure?') % loc,
show_copy_button=False):
return
From fe90a1b04fa8fecd5abfacbc795ca8b526897b11 Mon Sep 17 00:00:00 2001
From: Kovid Goyal
Date: Wed, 4 May 2011 13:13:38 -0600
Subject: [PATCH 32/39] Implement #777001 (Add Preview ePub button to Tweak
ePub dialog)
---
src/calibre/gui2/dialogs/tweak_epub.py | 38 ++++++++++++++---
src/calibre/gui2/dialogs/tweak_epub.ui | 57 +++++++++++++++-----------
2 files changed, 66 insertions(+), 29 deletions(-)
diff --git a/src/calibre/gui2/dialogs/tweak_epub.py b/src/calibre/gui2/dialogs/tweak_epub.py
index edc274c9b2..732d74b77d 100755
--- a/src/calibre/gui2/dialogs/tweak_epub.py
+++ b/src/calibre/gui2/dialogs/tweak_epub.py
@@ -7,16 +7,16 @@ __copyright__ = '2010, Kovid Goyal '
__docformat__ = 'restructuredtext en'
import os, shutil
-from contextlib import closing
from zipfile import ZipFile, ZIP_DEFLATED, ZIP_STORED
from PyQt4.Qt import QDialog
from calibre.constants import isosx
-from calibre.gui2 import open_local_file
+from calibre.gui2 import open_local_file, error_dialog
from calibre.gui2.dialogs.tweak_epub_ui import Ui_Dialog
from calibre.libunzip import extract as zipextract
-from calibre.ptempfile import PersistentTemporaryDirectory
+from calibre.ptempfile import (PersistentTemporaryDirectory,
+ PersistentTemporaryFile)
class TweakEpub(QDialog, Ui_Dialog):
'''
@@ -37,11 +37,15 @@ class TweakEpub(QDialog, Ui_Dialog):
self.cancel_button.clicked.connect(self.reject)
self.explode_button.clicked.connect(self.explode)
self.rebuild_button.clicked.connect(self.rebuild)
+ self.preview_button.clicked.connect(self.preview)
# Position update dialog overlaying top left of app window
parent_loc = parent.pos()
self.move(parent_loc.x(),parent_loc.y())
+ self.gui = parent
+ self._preview_files = []
+
def cleanup(self):
if isosx:
try:
@@ -55,6 +59,11 @@ class TweakEpub(QDialog, Ui_Dialog):
# Delete directory containing exploded ePub
if self._exploded is not None:
shutil.rmtree(self._exploded, ignore_errors=True)
+ for x in self._preview_files:
+ try:
+ os.remove(x)
+ except:
+ pass
def display_exploded(self):
'''
@@ -71,9 +80,8 @@ class TweakEpub(QDialog, Ui_Dialog):
self.rebuild_button.setEnabled(True)
self.explode_button.setEnabled(False)
- def rebuild(self, *args):
- self._output = os.path.join(self._exploded, 'rebuilt.epub')
- with closing(ZipFile(self._output, 'w', compression=ZIP_DEFLATED)) as zf:
+ def do_rebuild(self, src):
+ with ZipFile(src, 'w', compression=ZIP_DEFLATED) as zf:
# Write mimetype
zf.write(os.path.join(self._exploded,'mimetype'), 'mimetype', compress_type=ZIP_STORED)
# Write everything else
@@ -86,5 +94,23 @@ class TweakEpub(QDialog, Ui_Dialog):
zfn = os.path.relpath(absfn,
self._exploded).replace(os.sep, '/')
zf.write(absfn, zfn)
+
+ def preview(self):
+ if not self._exploded:
+ return error_dialog(self, _('Cannot preview'),
+ _('You must first explode the epub before previewing.'),
+ show=True)
+
+ tf = PersistentTemporaryFile('.epub')
+ tf.close()
+ self._preview_files.append(tf.name)
+
+ self.do_rebuild(tf.name)
+
+ self.gui.iactions['View']._view_file(tf.name)
+
+ def rebuild(self, *args):
+ self._output = os.path.join(self._exploded, 'rebuilt.epub')
+ self.do_rebuild(self._output)
return QDialog.accept(self)
diff --git a/src/calibre/gui2/dialogs/tweak_epub.ui b/src/calibre/gui2/dialogs/tweak_epub.ui
index fc6f24675f..a59af4fde1 100644
--- a/src/calibre/gui2/dialogs/tweak_epub.ui
+++ b/src/calibre/gui2/dialogs/tweak_epub.ui
@@ -23,6 +23,16 @@
false
+ -
+
+
+ <p>Explode the ePub to display contents in a file browser window. To tweak individual files, right-click, then 'Open with...' your editor of choice. When tweaks are complete, close the file browser window <b>and the editor windows you used to edit files in the epub</b>.</p><p>Rebuild the ePub, updating your calibre library.</p>
+
+
+ true
+
+
+
-
@@ -37,23 +47,6 @@
- -
-
-
- false
-
-
- Rebuild ePub from exploded contents
-
-
- &Rebuild ePub
-
-
-
- :/images/exec.png:/images/exec.png
-
-
-
-
@@ -68,13 +61,31 @@
- -
-
-
- <p>Explode the ePub to display contents in a file browser window. To tweak individual files, right-click, then 'Open with...' your editor of choice. When tweaks are complete, close the file browser window <b>and the editor windows you used to edit files in the epub</b>.</p><p>Rebuild the ePub, updating your calibre library.</p>
+
-
+
+
+ false
-
- true
+
+ Rebuild ePub from exploded contents
+
+
+ &Rebuild ePub
+
+
+
+ :/images/exec.png:/images/exec.png
+
+
+
+ -
+
+
+ &Preview ePub
+
+
+
+ :/images/view.png:/images/view.png
From c84c5f297bcfcd30f99f503768acd86235f1da43 Mon Sep 17 00:00:00 2001
From: Kovid Goyal
Date: Wed, 4 May 2011 15:18:37 -0600
Subject: [PATCH 33/39] Add select all/none buttons to the metadata download
prefs
---
.../gui2/preferences/metadata_sources.py | 13 ++++++++++++-
.../gui2/preferences/metadata_sources.ui | 18 ++++++++++++++++--
2 files changed, 28 insertions(+), 3 deletions(-)
diff --git a/src/calibre/gui2/preferences/metadata_sources.py b/src/calibre/gui2/preferences/metadata_sources.py
index f487051d07..05ff23987d 100644
--- a/src/calibre/gui2/preferences/metadata_sources.py
+++ b/src/calibre/gui2/preferences/metadata_sources.py
@@ -190,7 +190,15 @@ class FieldsModel(QAbstractListModel): # {{{
return ans | Qt.ItemIsUserCheckable
def restore_defaults(self):
- self.overrides = dict([(f, self.state(f, True)) for f in self.fields])
+ self.overrides = dict([(f, self.state(f, Qt.Checked)) for f in self.fields])
+ self.reset()
+
+ def select_all(self):
+ self.overrides = dict([(f, Qt.Checked) for f in self.fields])
+ self.reset()
+
+ def clear_all(self):
+ self.overrides = dict([(f, Qt.Unchecked) for f in self.fields])
self.reset()
def setData(self, index, val, role):
@@ -273,6 +281,9 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
self.fields_view.setModel(self.fields_model)
self.fields_model.dataChanged.connect(self.changed_signal)
+ self.select_all_button.clicked.connect(self.fields_model.select_all)
+ self.clear_all_button.clicked.connect(self.fields_model.clear_all)
+
def configure_plugin(self):
for index in self.sources_view.selectionModel().selectedRows():
plugin = self.sources_model.data(index, Qt.UserRole)
diff --git a/src/calibre/gui2/preferences/metadata_sources.ui b/src/calibre/gui2/preferences/metadata_sources.ui
index e46069b036..ff161654dd 100644
--- a/src/calibre/gui2/preferences/metadata_sources.ui
+++ b/src/calibre/gui2/preferences/metadata_sources.ui
@@ -77,8 +77,8 @@
Downloaded metadata fields
-
- -
+
+
-
If you uncheck any fields, metadata for those fields will not be downloaded
@@ -88,6 +88,20 @@
+ -
+
+
+ &Select all
+
+
+
+ -
+
+
+ &Clear all
+
+
+
From 45ec80f4bb580b88d6c957906f5201648c2b9edf Mon Sep 17 00:00:00 2001
From: Kovid Goyal
Date: Wed, 4 May 2011 15:49:34 -0600
Subject: [PATCH 34/39] For the lazy among us
---
src/calibre/gui2/actions/choose_library.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/calibre/gui2/actions/choose_library.py b/src/calibre/gui2/actions/choose_library.py
index a663f288af..f6b19fc4aa 100644
--- a/src/calibre/gui2/actions/choose_library.py
+++ b/src/calibre/gui2/actions/choose_library.py
@@ -246,7 +246,7 @@ class ChooseLibraryAction(InterfaceAction):
def delete_requested(self, name, location):
loc = location.replace('/', os.sep)
if not question_dialog(self.gui, _('Are you sure?'), ''+
- _('All files from %s will be '
+ _('All files from
%s
will be '
'permanently deleted. Are you sure?') % loc,
show_copy_button=False):
return
From a34ea87a940feaf743ade8eddb893ce353aef6be Mon Sep 17 00:00:00 2001
From: John Schember
Date: Wed, 4 May 2011 18:56:30 -0400
Subject: [PATCH 35/39] Fix Bug #763105: RTF output displays some unicode
characters incorrectly.
---
src/calibre/ebooks/rtf/rtfml.py | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/src/calibre/ebooks/rtf/rtfml.py b/src/calibre/ebooks/rtf/rtfml.py
index 97fa175d1a..cd877c63c2 100644
--- a/src/calibre/ebooks/rtf/rtfml.py
+++ b/src/calibre/ebooks/rtf/rtfml.py
@@ -79,8 +79,7 @@ def txt2rtf(text):
elif val <= 127:
buf.write(x)
else:
- repl = ascii_text(x)
- c = r'\uc{2}\u{0:d}{1}'.format(val, repl, len(repl))
+ c = r'\u{0:d}?'.format(val)
buf.write(c)
return buf.getvalue()
From e15ee70a1ded03bf4b84f5a30f8fce89aeefa56e Mon Sep 17 00:00:00 2001
From: Kovid Goyal
Date: Wed, 4 May 2011 18:56:07 -0600
Subject: [PATCH 36/39] ODT Input: Speed up conversion of ODT files that define
huge amounts of redundant style information. Fixes #777468 (Conversion from
ODT to EPUB extremely slow)
---
src/calibre/ebooks/odt/input.py | 55 ++++++++++++++++++++++++++++++---
src/odf/odf2xhtml.py | 14 +++++++--
2 files changed, 62 insertions(+), 7 deletions(-)
diff --git a/src/calibre/ebooks/odt/input.py b/src/calibre/ebooks/odt/input.py
index 1184148e80..10553dac2b 100644
--- a/src/calibre/ebooks/odt/input.py
+++ b/src/calibre/ebooks/odt/input.py
@@ -7,6 +7,8 @@ __docformat__ = 'restructuredtext en'
Convert an ODT file into a Open Ebook
'''
import os
+
+from lxml import etree
from odf.odf2xhtml import ODF2XHTML
from calibre import CurrentDir, walk
@@ -23,7 +25,48 @@ class Extract(ODF2XHTML):
with open(name, 'wb') as f:
f.write(data)
- def __call__(self, stream, odir):
+ def filter_css(self, html, log):
+ root = etree.fromstring(html)
+ style = root.xpath('//*[local-name() = "style" and @type="text/css"]')
+ if style:
+ style = style[0]
+ css = style.text
+ if css:
+ style.text, sel_map = self.do_filter_css(css)
+ for x in root.xpath('//*[@class]'):
+ extra = []
+ orig = x.get('class')
+ for cls in orig.split():
+ extra.extend(sel_map.get(cls, []))
+ if extra:
+ x.set('class', orig + ' ' + ' '.join(extra))
+ html = etree.tostring(root, encoding='utf-8',
+ xml_declaration=True)
+ return html
+
+ def do_filter_css(self, css):
+ from cssutils import parseString
+ from cssutils.css import CSSRule
+ sheet = parseString(css)
+ rules = list(sheet.cssRules.rulesOfType(CSSRule.STYLE_RULE))
+ sel_map = {}
+ count = 0
+ for r in rules:
+ # Check if we have only class selectors for this rule
+ nc = [x for x in r.selectorList if not
+ x.selectorText.startswith('.')]
+ if len(r.selectorList) > 1 and not nc:
+ replace_name = 'c_odt%d'%count
+ count += 1
+ for sel in r.selectorList:
+ s = sel.selectorText[1:]
+ if s not in sel_map:
+ sel_map[s] = []
+ sel_map[s].append(replace_name)
+ r.selectorText = '.'+replace_name
+ return sheet.cssText, sel_map
+
+ def __call__(self, stream, odir, log):
from calibre.utils.zipfile import ZipFile
from calibre.ebooks.metadata.meta import get_metadata
from calibre.ebooks.metadata.opf2 import OPFCreator
@@ -32,13 +75,17 @@ class Extract(ODF2XHTML):
if not os.path.exists(odir):
os.makedirs(odir)
with CurrentDir(odir):
- print 'Extracting ODT file...'
+ log('Extracting ODT file...')
html = self.odf2xhtml(stream)
# A blanket img specification like this causes problems
- # with EPUB output as the contaiing element often has
+ # with EPUB output as the containing element often has
# an absolute height and width set that is larger than
# the available screen real estate
html = html.replace('img { width: 100%; height: 100%; }', '')
+ try:
+ html = self.filter_css(html, log)
+ except:
+ log.exception('Failed to filter CSS, conversion may be slow')
with open('index.xhtml', 'wb') as f:
f.write(html.encode('utf-8'))
zf = ZipFile(stream, 'r')
@@ -67,7 +114,7 @@ class ODTInput(InputFormatPlugin):
def convert(self, stream, options, file_ext, log,
accelerators):
- return Extract()(stream, '.')
+ return Extract()(stream, '.', log)
def postprocess_book(self, oeb, opts, log):
# Fix constructs as the asinine epubchecker complains
diff --git a/src/odf/odf2xhtml.py b/src/odf/odf2xhtml.py
index 26da9d9905..a04aa48bf7 100644
--- a/src/odf/odf2xhtml.py
+++ b/src/odf/odf2xhtml.py
@@ -841,11 +841,19 @@ ol, ul { padding-left: 2em; }
self.styledict[name] = styles
# Write the styles to HTML
self.writeout(self.default_styles)
+ # Changed by Kovid to not write out endless copies of the same style
+ css_styles = {}
for name in self.stylestack:
styles = self.styledict.get(name)
- css2 = self.cs.convert_styles(styles)
- self.writeout("%s {\n" % name)
- for style, val in css2.items():
+ css2 = tuple(self.cs.convert_styles(styles).iteritems())
+ if css2 in css_styles:
+ css_styles[css2].append(name)
+ else:
+ css_styles[css2] = [name]
+
+ for css2, names in css_styles.iteritems():
+ self.writeout("%s {\n" % ', '.join(names))
+ for style, val in css2:
self.writeout("\t%s: %s;\n" % (style, val) )
self.writeout("}\n")
From 07da7b239c377a856fbc43f16aa2f01042964e3d Mon Sep 17 00:00:00 2001
From: John Schember
Date: Wed, 4 May 2011 21:48:44 -0400
Subject: [PATCH 37/39] Fix Bug #775669: HTMLZ OPF sets cover, adds cover
properly when updating metadata, reads cover form guide section of opf in
archive.
---
src/calibre/ebooks/htmlz/input.py | 25 +++++++++++++++++--
src/calibre/ebooks/htmlz/output.py | 25 ++++++++++++++++++-
src/calibre/ebooks/metadata/extz.py | 24 ++++++++++--------
src/calibre/ebooks/oeb/transforms/metadata.py | 2 +-
4 files changed, 61 insertions(+), 15 deletions(-)
diff --git a/src/calibre/ebooks/htmlz/input.py b/src/calibre/ebooks/htmlz/input.py
index dcf2ed0ed3..d083fcc4ab 100644
--- a/src/calibre/ebooks/htmlz/input.py
+++ b/src/calibre/ebooks/htmlz/input.py
@@ -7,10 +7,12 @@ __copyright__ = '2011, John Schember '
__docformat__ = 'restructuredtext en'
import os
+import posixpath
-from calibre import walk
+from calibre import guess_type, walk
from calibre.customize.conversion import InputFormatPlugin
from calibre.ebooks.chardet import xml_to_unicode
+from calibre.ebooks.metadata.opf2 import OPF
from calibre.utils.zipfile import ZipFile
class HTMLZInput(InputFormatPlugin):
@@ -27,7 +29,7 @@ class HTMLZInput(InputFormatPlugin):
# Extract content from zip archive.
zf = ZipFile(stream)
- zf.extractall('.')
+ zf.extractall()
for x in walk('.'):
if os.path.splitext(x)[1].lower() in ('.html', '.xhtml', '.htm'):
@@ -70,5 +72,24 @@ class HTMLZInput(InputFormatPlugin):
from calibre.ebooks.oeb.transforms.metadata import meta_info_to_oeb_metadata
mi = get_file_type_metadata(stream, file_ext)
meta_info_to_oeb_metadata(mi, oeb.metadata, log)
+
+ # Get the cover path from the OPF.
+ cover_href = None
+ opf = None
+ for x in walk('.'):
+ if os.path.splitext(x)[1].lower() in ('.opf'):
+ opf = x
+ break
+ if opf:
+ opf = OPF(opf)
+ cover_href = posixpath.relpath(opf.cover, os.path.dirname(stream.name))
+ # Set the cover.
+ if cover_href:
+ cdata = None
+ with open(cover_href, 'rb') as cf:
+ cdata = cf.read()
+ id, href = oeb.manifest.generate('cover', cover_href)
+ oeb.manifest.add(id, href, guess_type(cover_href)[0], data=cdata)
+ oeb.guide.add('cover', 'Cover', href)
return oeb
diff --git a/src/calibre/ebooks/htmlz/output.py b/src/calibre/ebooks/htmlz/output.py
index 6d2ad54a12..a1ef57af2c 100644
--- a/src/calibre/ebooks/htmlz/output.py
+++ b/src/calibre/ebooks/htmlz/output.py
@@ -7,11 +7,13 @@ __copyright__ = '2011, John Schember '
__docformat__ = 'restructuredtext en'
import os
+from cStringIO import StringIO
from lxml import etree
from calibre.customize.conversion import OutputFormatPlugin, \
OptionRecommendation
+from calibre.ebooks.metadata.opf2 import OPF, metadata_to_opf
from calibre.ptempfile import TemporaryDirectory
from calibre.utils.zipfile import ZipFile
@@ -79,10 +81,31 @@ class HTMLZOutput(OutputFormatPlugin):
fname = os.path.join(tdir, 'images', images[item.href])
with open(fname, 'wb') as img:
img.write(data)
+
+ # Cover
+ cover_path = None
+ try:
+ cover_data = None
+ if oeb_book.metadata.cover:
+ term = oeb_book.metadata.cover[0].term
+ cover_data = oeb_book.guide[term].item.data
+ if cover_data:
+ from calibre.utils.magick.draw import save_cover_data_to
+ cover_path = os.path.join(tdir, 'cover.jpg')
+ with open(cover_path, 'w') as cf:
+ cf.write('')
+ save_cover_data_to(cover_data, cover_path)
+ except:
+ import traceback
+ traceback.print_exc()
# Metadata
with open(os.path.join(tdir, 'metadata.opf'), 'wb') as mdataf:
- mdataf.write(etree.tostring(oeb_book.metadata.to_opf1()))
+ opf = OPF(StringIO(etree.tostring(oeb_book.metadata.to_opf1())))
+ mi = opf.to_book_metadata()
+ if cover_path:
+ mi.cover = 'cover.jpg'
+ mdataf.write(metadata_to_opf(mi))
htmlz = ZipFile(output_path, 'w')
htmlz.add_dir(tdir)
diff --git a/src/calibre/ebooks/metadata/extz.py b/src/calibre/ebooks/metadata/extz.py
index 18069b2a6a..21c10278e1 100644
--- a/src/calibre/ebooks/metadata/extz.py
+++ b/src/calibre/ebooks/metadata/extz.py
@@ -13,7 +13,7 @@ import posixpath
from cStringIO import StringIO
from calibre.ebooks.metadata import MetaInformation
-from calibre.ebooks.metadata.opf2 import OPF
+from calibre.ebooks.metadata.opf2 import OPF, metadata_to_opf
from calibre.ptempfile import PersistentTemporaryFile
from calibre.utils.zipfile import ZipFile, safe_replace
@@ -31,9 +31,9 @@ def get_metadata(stream, extract_cover=True):
opf = OPF(opf_stream)
mi = opf.to_book_metadata()
if extract_cover:
- cover_name = opf.raster_cover
- if cover_name:
- mi.cover_data = ('jpg', zf.read(cover_name))
+ cover_href = posixpath.relpath(opf.cover, os.path.dirname(stream.name))
+ if cover_href:
+ mi.cover_data = ('jpg', zf.read(cover_href))
except:
return mi
return mi
@@ -59,17 +59,19 @@ def set_metadata(stream, mi):
except:
pass
if new_cdata:
- raster_cover = opf.raster_cover
- if not raster_cover:
- raster_cover = 'cover.jpg'
- cpath = posixpath.join(posixpath.dirname(opf_path), raster_cover)
+ cover = opf.cover
+ if not cover:
+ cover = 'cover.jpg'
+ cpath = posixpath.join(posixpath.dirname(opf_path), cover)
new_cover = _write_new_cover(new_cdata, cpath)
replacements[cpath] = open(new_cover.name, 'rb')
+ mi.cover = cover
# Update the metadata.
- opf.smart_update(mi, replace_metadata=True)
- newopf = StringIO(opf.render())
- safe_replace(stream, opf_path, newopf, extra_replacements=replacements)
+ old_mi = opf.to_book_metadata()
+ old_mi.smart_update(mi)
+ newopf = StringIO(metadata_to_opf(old_mi))
+ safe_replace(stream, opf_path, newopf, extra_replacements=replacements, add_missing=True)
# Cleanup temporary files.
try:
diff --git a/src/calibre/ebooks/oeb/transforms/metadata.py b/src/calibre/ebooks/oeb/transforms/metadata.py
index 19c209b74d..f719ee3eb5 100644
--- a/src/calibre/ebooks/oeb/transforms/metadata.py
+++ b/src/calibre/ebooks/oeb/transforms/metadata.py
@@ -36,7 +36,7 @@ def meta_info_to_oeb_metadata(mi, m, log, override_input_metadata=False):
m.clear('description')
m.add('description', mi.comments)
elif override_input_metadata:
- m.clear('description')
+ m.clear('description')
if not mi.is_null('publisher'):
m.clear('publisher')
m.add('publisher', mi.publisher)
From 6fbb996d2a30afd36da0e3566cc8e22e23a912ab Mon Sep 17 00:00:00 2001
From: John Schember
Date: Wed, 4 May 2011 21:50:08 -0400
Subject: [PATCH 38/39] HTMLZ: Keep existing non-metadata sections when
updating.
---
src/calibre/ebooks/metadata/extz.py | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/src/calibre/ebooks/metadata/extz.py b/src/calibre/ebooks/metadata/extz.py
index 21c10278e1..1bda263015 100644
--- a/src/calibre/ebooks/metadata/extz.py
+++ b/src/calibre/ebooks/metadata/extz.py
@@ -70,7 +70,8 @@ def set_metadata(stream, mi):
# Update the metadata.
old_mi = opf.to_book_metadata()
old_mi.smart_update(mi)
- newopf = StringIO(metadata_to_opf(old_mi))
+ opf.smart_update(metadata_to_opf(old_mi))
+ newopf = StringIO(opf.render())
safe_replace(stream, opf_path, newopf, extra_replacements=replacements, add_missing=True)
# Cleanup temporary files.
From b461f5bc26030f3f8ce43d02facc3914ade45c5e Mon Sep 17 00:00:00 2001
From: Kovid Goyal
Date: Wed, 4 May 2011 20:30:34 -0600
Subject: [PATCH 39/39] ...
---
src/calibre/ebooks/odt/input.py | 3 +++
1 file changed, 3 insertions(+)
diff --git a/src/calibre/ebooks/odt/input.py b/src/calibre/ebooks/odt/input.py
index 10553dac2b..e724acb981 100644
--- a/src/calibre/ebooks/odt/input.py
+++ b/src/calibre/ebooks/odt/input.py
@@ -56,6 +56,9 @@ class Extract(ODF2XHTML):
nc = [x for x in r.selectorList if not
x.selectorText.startswith('.')]
if len(r.selectorList) > 1 and not nc:
+ # Replace all the class selectors with a single class selector
+ # This will be added to the class attribute of all elements
+ # that have one of these selectors.
replace_name = 'c_odt%d'%count
count += 1
for sel in r.selectorList: