merge from trunk

This commit is contained in:
Lee 2011-04-23 11:07:10 +08:00
commit eb73df9609
37 changed files with 2511 additions and 2014 deletions

View File

@ -19,6 +19,60 @@
# new recipes:
# - title:
- version: 0.7.57
date: 2011-04-22
new features:
- title: "Launch worker processes on demand instead of keeping a pool of them in memory. Reduces memory footprint."
- title: "Use the visual formatting of the Table of Contents to try to automatically create a multi-level TOC when converting/viewing MOBI files."
tickets: [763681]
- title: "Add a new function booksize() to the template language to get the value of the size column in calibre."
- title: "Add support for using metadata plugboards with the content server (only with the epub format)"
- title: "Change default algorithm for automatically computing author sort to be more intelligent and handle the case when the author name has a comma in it"
- title: "Show cover size in the tooltips of the book details panel and book details popup window"
bug fixes:
- title: "Dragging and dropping a cover onto the book details panel did not change the cover size"
tickets: [768332]
- title: "Fix non-escaped '|' when searching for commas in authors using REGEXP_MATCH"
- title: "Fix ratings in templates being multiplied by 2"
- title: "Fix adding a comma to custom series values when using completion."
tickets: [763788]
- title: "CHM Input: Another workaround for a Microsoft mess."
tickets: [763336]
- title: "Fix job count in the spinner not always being updated when a job completes"
- title: "Changing case only of a title does not update title sort"
tickets: [768904]
improved recipes:
- ecuisine.ro, egirl.ro and tabu.ro
- Daily Telegraph
- Handelsblatt
- Il Sole 24 Ore
- Newsweek
- Arcamax
new recipes:
- title: BabyOnline.ro
author: Silviu Cotoara
- title: "The Journal.ie"
author: Phil Burns
- title: "Der Spiegel"
author: Nikolas Mangold
- version: 0.7.56
date: 2011-04-17

View File

@ -4,7 +4,7 @@ __license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
__docformat__ = 'restructuredtext en'
__appname__ = u'calibre'
numeric_version = (0, 7, 56)
numeric_version = (0, 7, 57)
__version__ = u'.'.join(map(unicode, numeric_version))
__author__ = u"Kovid Goyal <kovid@kovidgoyal.net>"

View File

@ -42,6 +42,10 @@ class Worker(Thread):
self.log.exception('Plugin', self.plugin.name, 'failed')
self.plugin.dl_time_spent = time.time() - start
@property
def name(self):
return self.plugin.name
def is_worker_alive(workers):
for w in workers:
if w.is_alive():
@ -348,7 +352,11 @@ def identify(log, abort, # {{{
if (first_result_at is not None and time.time() - first_result_at >
wait_time):
log('Not waiting any longer for more results')
log.warn('Not waiting any longer for more results. Still running'
' sources:')
for worker in workers:
if worker.is_alive():
log.debug('\t' + worker.name)
abort.set()
break

View File

@ -204,8 +204,8 @@ class OverDrive(Source):
else:
initial_q = ' '.join(author_tokens)
xref_q = '+'.join(title_tokens)
log.error('Initial query is %s'%initial_q)
log.error('Cross reference query is %s'%xref_q)
#log.error('Initial query is %s'%initial_q)
#log.error('Cross reference query is %s'%xref_q)
q_xref = q+'SearchResults.svc/GetResults?iDisplayLength=50&sSearch='+xref_q
query = '{"szKeyword":"'+initial_q+'"}'

View File

@ -8,14 +8,15 @@ __docformat__ = 'restructuredtext en'
import os
from functools import partial
from PyQt4.Qt import Qt, QMenu, QModelIndex
from PyQt4.Qt import Qt, QMenu, QModelIndex, QTimer
from calibre.gui2 import error_dialog, config, Dispatcher
from calibre.gui2 import error_dialog, config, Dispatcher, question_dialog
from calibre.gui2.dialogs.metadata_single import MetadataSingleDialog
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
@ -78,6 +79,7 @@ class EditMetadataAction(InterfaceAction):
self.qaction.setEnabled(enabled)
self.action_merge.setEnabled(enabled)
# Download metadata {{{
def download_metadata(self, ids=None):
if ids is None:
rows = self.gui.library_view.selectionModel().selectedRows()
@ -88,14 +90,73 @@ class EditMetadataAction(InterfaceAction):
ids = [db.id(row.row()) for row in rows]
from calibre.gui2.metadata.bulk_download2 import start_download
start_download(self.gui, ids,
Dispatcher(self.bulk_metadata_downloaded))
Dispatcher(self.metadata_downloaded))
def bulk_metadata_downloaded(self, job):
def metadata_downloaded(self, job):
if job.failed:
self.gui.job_exception(job, dialog_title=_('Failed to download metadata'))
return
from calibre.gui2.metadata.bulk_download2 import proceed
proceed(self.gui, job)
from calibre.gui2.metadata.bulk_download2 import get_job_details
id_map, failed_ids, failed_covers, all_failed, det_msg = \
get_job_details(job)
if all_failed:
return error_dialog(self.gui, _('Download failed'),
_('Failed to download metadata or covers for any of the %d'
' book(s).') % len(id_map), det_msg=det_msg, show=True)
self.gui.status_bar.show_message(_('Metadata download completed'), 3000)
msg = '<p>' + _('Finished downloading metadata for <b>%d book(s)</b>. '
'Proceed with updating the metadata in your library?')%len(id_map)
show_copy_button = False
if failed_ids or failed_covers:
show_copy_button = True
msg += '<p>'+_('Could not download metadata and/or covers for %d of the books. Click'
' "Show details" to see which books.')%len(failed_ids)
payload = (id_map, failed_ids, failed_covers)
from calibre.gui2.dialogs.message_box import ProceedNotification
p = ProceedNotification(payload, job.html_details,
_('Download log'), _('Download complete'), msg,
det_msg=det_msg, show_copy_button=show_copy_button,
parent=self.gui)
p.proceed.connect(self.apply_downloaded_metadata)
p.show()
def apply_downloaded_metadata(self, payload):
id_map, failed_ids, failed_covers = payload
id_map = dict([(k, v) for k, v in id_map.iteritems() if k not in
failed_ids])
if not id_map:
return
modified = set()
db = self.gui.current_db
for i, mi in id_map.iteritems():
lm = db.metadata_last_modified(i, index_is_id=True)
if lm > mi.last_modified:
title = db.title(i, index_is_id=True)
authors = db.authors(i, index_is_id=True)
if authors:
authors = [x.replace('|', ',') for x in authors.split(',')]
title += ' - ' + authors_to_string(authors)
modified.add(title)
if modified:
from calibre.utils.icu import lower
modified = sorted(modified, key=lower)
if not question_dialog(self.gui, _('Some books changed'), '<p>'+
_('The metadata for some books in your library has'
' changed since you started the download. If you'
' proceed, some of those changes may be overwritten. '
'Click "Show details" to see the list of changed books. '
'Do you want to proceed?'), det_msg='\n'.join(modified)):
return
self.apply_metadata_changes(id_map)
def download_metadata_old(self, checked, covers=True, set_metadata=True,
set_social_metadata=None):
@ -140,6 +201,7 @@ class EditMetadataAction(InterfaceAction):
x.updated, cr)
if self.gui.cover_flow:
self.gui.cover_flow.dataChanged()
# }}}
def edit_metadata(self, checked, bulk=None):
'''
@ -466,4 +528,89 @@ class EditMetadataAction(InterfaceAction):
self.gui.upload_collections(model.db, view=view, oncard=oncard)
view.reset()
# Apply bulk metadata changes {{{
def apply_metadata_changes(self, id_map, title=None, msg=''):
'''
Apply the metadata changes in id_map to the database synchronously
id_map must be a mapping of ids to Metadata objects. Set any fields you
do not want updated in the Metadata object to null. An easy way to do
that is to create a metadata object as Metadata(_('Unknown')) and then
only set the fields you want changed on this object.
'''
if title is None:
title = _('Applying changed metadata')
self.apply_id_map = list(id_map.iteritems())
self.apply_current_idx = 0
self.apply_failures = []
self.applied_ids = []
self.apply_pd = None
if len(self.apply_id_map) > 1:
from calibre.gui2.dialogs.progress import ProgressDialog
self.apply_pd = ProgressDialog(title, msg, min=0,
max=len(self.apply_id_map)-1, parent=self.gui,
cancelable=False)
self.apply_pd.setModal(True)
self.apply_pd.show()
self.do_one_apply()
def do_one_apply(self):
if self.apply_current_idx >= len(self.apply_id_map):
return self.finalize_apply()
i, mi = self.apply_id_map[self.apply_current_idx]
db = self.gui.current_db
try:
set_title = not mi.is_null('title')
set_authors = not mi.is_null('authors')
db.set_metadata(i, mi, commit=False, set_title=set_title,
set_authors=set_authors, notify=False)
self.applied_ids.append(i)
except:
import traceback
self.apply_failures.append((i, traceback.format_exc()))
try:
if mi.cover:
os.remove(mi.cover)
except:
pass
self.apply_current_idx += 1
if self.apply_pd is not None:
self.apply_pd.value += 1
QTimer.singleShot(50, self.do_one_apply)
def finalize_apply(self):
db = self.gui.current_db
db.commit()
if self.apply_pd is not None:
self.apply_pd.hide()
if self.apply_failures:
msg = []
for i, tb in self.apply_failures:
title = db.title(i, index_is_id=True)
authors = db.authors(i, index_is_id=True)
if authors:
authors = [x.replace('|', ',') for x in authors.split(',')]
title += ' - ' + authors_to_string(authors)
msg.append(title+'\n\n'+tb+'\n'+('*'*80))
error_dialog(self.gui, _('Some failures'),
_('Failed to apply updated metadata for some books'
' in your library. Click "Show Details" to see '
'details.'), det_msg='\n\n'.join(msg), show=True)
if self.applied_ids:
cr = self.gui.library_view.currentIndex().row()
self.gui.library_view.model().refresh_ids(
self.applied_ids, cr)
if self.gui.cover_flow:
self.gui.cover_flow.dataChanged()
self.apply_id_map = []
self.apply_pd = None
# }}}

View File

@ -33,7 +33,7 @@ class StoreAction(InterfaceAction):
def search(self):
self.show_disclaimer()
from calibre.gui2.store.search import SearchDialog
from calibre.gui2.store.search.search import SearchDialog
sd = SearchDialog(self.gui.istores, self.gui)
sd.exec_()

View File

@ -6,13 +6,13 @@ __copyright__ = '2011, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
from PyQt4.Qt import QDialog, QIcon, QApplication, QSize, QKeySequence, \
QAction, Qt
from PyQt4.Qt import (QDialog, QIcon, QApplication, QSize, QKeySequence,
QAction, Qt, pyqtSignal, QTextBrowser, QDialogButtonBox, QVBoxLayout)
from calibre.constants import __version__
from calibre.gui2.dialogs.message_box_ui import Ui_Dialog
class MessageBox(QDialog, Ui_Dialog):
class MessageBox(QDialog, Ui_Dialog): # {{{
ERROR = 0
WARNING = 1
@ -111,6 +111,81 @@ class MessageBox(QDialog, Ui_Dialog):
self.det_msg_toggle.setVisible(bool(msg))
self.det_msg.setVisible(False)
self.do_resize()
# }}}
class ViewLog(QDialog): # {{{
def __init__(self, title, html, parent=None):
QDialog.__init__(self, parent)
self.l = l = QVBoxLayout()
self.setLayout(l)
self.tb = QTextBrowser(self)
self.tb.setHtml('<pre style="font-family: monospace">%s</pre>' % html)
l.addWidget(self.tb)
self.bb = QDialogButtonBox(QDialogButtonBox.Ok)
self.bb.accepted.connect(self.accept)
self.bb.rejected.connect(self.reject)
self.copy_button = self.bb.addButton(_('Copy to clipboard'),
self.bb.ActionRole)
self.copy_button.setIcon(QIcon(I('edit-copy.png')))
self.copy_button.clicked.connect(self.copy_to_clipboard)
l.addWidget(self.bb)
self.setModal(False)
self.resize(QSize(700, 500))
self.setWindowTitle(title)
self.setWindowIcon(QIcon(I('debug.png')))
self.show()
def copy_to_clipboard(self):
txt = self.tb.toPlainText()
QApplication.clipboard().setText(txt)
# }}}
class ProceedNotification(MessageBox): # {{{
proceed = pyqtSignal(object)
def __init__(self, payload, html_log, log_viewer_title, title, msg, det_msg='', show_copy_button=False, parent=None):
'''
A non modal popup that notifies the user that a background task has
been completed. If they user clicks yes, the proceed signal is emitted
with payload as its argument.
:param payload: Arbitrary object, emitted in the proceed signal
:param html_log: An HTML or plain text log
:param log_viewer_title: The title for the log viewer window
:param title: The title fo rthis popup
:param msg: The msg to display
:param det_msg: Detailed message
'''
MessageBox.__init__(self, MessageBox.QUESTION, title, msg,
det_msg=det_msg, show_copy_button=show_copy_button,
parent=parent)
self.payload = payload
self.html_log = html_log
self.log_viewer_title = log_viewer_title
self.finished.connect(self.do_proceed)
self.vlb = self.bb.addButton(_('View log'), self.bb.ActionRole)
self.vlb.setIcon(QIcon(I('debug.png')))
self.vlb.clicked.connect(self.show_log)
self.det_msg_toggle.setVisible(bool(det_msg))
self.setModal(False)
def show_log(self):
self.log_viewer = ViewLog(self.log_viewer_title, self.html_log,
parent=self)
def do_proceed(self, result):
if result == self.Accepted:
self.proceed.emit(self.payload)
try:
self.proceed.disconnect()
except:
pass
# }}}
if __name__ == '__main__':
app = QApplication([])

View File

@ -169,11 +169,11 @@ class JobManager(QAbstractTableModel): # {{{
job.update()
if orig_state != job.run_state:
needs_reset = True
if job.is_finished:
self.job_done.emit(len(self.unfinished_jobs()))
if needs_reset:
self.jobs.sort()
self.reset()
if job.is_finished:
self.job_done.emit(len(self.unfinished_jobs()))
else:
for job in jobs:
idx = self.jobs.index(job)

View File

@ -7,19 +7,14 @@ __license__ = 'GPL v3'
__copyright__ = '2011, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
import os
from functools import partial
from itertools import izip
from threading import Event
from PyQt4.Qt import (QIcon, QDialog, QVBoxLayout, QTextBrowser, QSize,
QDialogButtonBox, QApplication, QTimer, QLabel, QProgressBar,
QGridLayout, QPixmap, Qt)
from PyQt4.Qt import (QIcon, QDialog,
QDialogButtonBox, QLabel, QGridLayout, QPixmap, Qt)
from calibre.gui2.dialogs.message_box import MessageBox
from calibre.gui2.threaded_jobs import ThreadedJob
from calibre.utils.icu import lower
from calibre.ebooks.metadata import authors_to_string
from calibre.gui2 import question_dialog, error_dialog
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
@ -107,178 +102,7 @@ def start_download(gui, ids, callback):
gui.status_bar.show_message(_('Metadata download started'), 3000)
# }}}
class ViewLog(QDialog): # {{{
def __init__(self, html, parent=None):
QDialog.__init__(self, parent)
self.l = l = QVBoxLayout()
self.setLayout(l)
self.tb = QTextBrowser(self)
self.tb.setHtml('<pre style="font-family: monospace">%s</pre>' % html)
l.addWidget(self.tb)
self.bb = QDialogButtonBox(QDialogButtonBox.Ok)
self.bb.accepted.connect(self.accept)
self.bb.rejected.connect(self.reject)
self.copy_button = self.bb.addButton(_('Copy to clipboard'),
self.bb.ActionRole)
self.copy_button.setIcon(QIcon(I('edit-copy.png')))
self.copy_button.clicked.connect(self.copy_to_clipboard)
l.addWidget(self.bb)
self.setModal(False)
self.resize(QSize(700, 500))
self.setWindowTitle(_('Download log'))
self.setWindowIcon(QIcon(I('debug.png')))
self.show()
def copy_to_clipboard(self):
txt = self.tb.toPlainText()
QApplication.clipboard().setText(txt)
_vl = None
def view_log(job, parent):
global _vl
_vl = ViewLog(job.html_details, parent)
# }}}
# Apply downloaded metadata {{{
class ApplyDialog(QDialog):
def __init__(self, gui):
QDialog.__init__(self, gui)
self.l = l = QVBoxLayout()
self.setLayout(l)
l.addWidget(QLabel(_('Applying downloaded metadata to your library')))
self.pb = QProgressBar(self)
l.addWidget(self.pb)
self.bb = QDialogButtonBox(QDialogButtonBox.Cancel)
self.bb.rejected.connect(self.reject)
l.addWidget(self.bb)
self.gui = gui
self.timer = QTimer(self)
self.timer.timeout.connect(self.do_one)
def start(self, id_map):
self.id_map = list(id_map.iteritems())
self.current_idx = 0
self.failures = []
self.ids = []
self.canceled = False
self.pb.setMinimum(0)
self.pb.setMaximum(len(id_map))
self.timer.start(50)
def do_one(self):
if self.canceled:
return
if self.current_idx >= len(self.id_map):
self.timer.stop()
self.finalize()
return
i, mi = self.id_map[self.current_idx]
db = self.gui.current_db
try:
set_title = not mi.is_null('title')
set_authors = not mi.is_null('authors')
db.set_metadata(i, mi, commit=False, set_title=set_title,
set_authors=set_authors)
self.ids.append(i)
except:
import traceback
self.failures.append((i, traceback.format_exc()))
try:
if mi.cover:
os.remove(mi.cover)
except:
pass
self.pb.setValue(self.pb.value()+1)
self.current_idx += 1
def reject(self):
self.canceled = True
self.timer.stop()
QDialog.reject(self)
def finalize(self):
if self.canceled:
return
if self.failures:
msg = []
db = self.gui.current_db
for i, tb in self.failures:
title = db.title(i, index_is_id=True)
authors = db.authors(i, index_is_id=True)
if authors:
authors = [x.replace('|', ',') for x in authors.split(',')]
title += ' - ' + authors_to_string(authors)
msg.append(title+'\n\n'+tb+'\n'+('*'*80))
parent = self if self.isVisible() else self.parent()
error_dialog(parent, _('Some failures'),
_('Failed to apply updated metadata for some books'
' in your library. Click "Show Details" to see '
'details.'), det_msg='\n\n'.join(msg), show=True)
if self.ids:
cr = self.gui.library_view.currentIndex().row()
self.gui.library_view.model().refresh_ids(
self.ids, cr)
if self.gui.cover_flow:
self.gui.cover_flow.dataChanged()
self.accept()
_amd = None
def apply_metadata(job, gui, q, result):
global _amd
q.vlb.clicked.disconnect()
q.finished.disconnect()
if result != q.Accepted:
return
id_map, failed_ids, failed_covers, title_map, all_failed = job.result
id_map = dict([(k, v) for k, v in id_map.iteritems() if k not in
failed_ids])
if not id_map:
return
modified = set()
db = gui.current_db
for i, mi in id_map.iteritems():
lm = db.metadata_last_modified(i, index_is_id=True)
if lm > mi.last_modified:
title = db.title(i, index_is_id=True)
authors = db.authors(i, index_is_id=True)
if authors:
authors = [x.replace('|', ',') for x in authors.split(',')]
title += ' - ' + authors_to_string(authors)
modified.add(title)
if modified:
modified = sorted(modified, key=lower)
if not question_dialog(gui, _('Some books changed'), '<p>'+
_('The metadata for some books in your library has'
' changed since you started the download. If you'
' proceed, some of those changes may be overwritten. '
'Click "Show details" to see the list of changed books. '
'Do you want to proceed?'), det_msg='\n'.join(modified)):
return
if _amd is None:
_amd = ApplyDialog(gui)
_amd.start(id_map)
if len(id_map) > 3:
_amd.exec_()
def proceed(gui, job):
gui.status_bar.show_message(_('Metadata download completed'), 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:
@ -289,31 +113,7 @@ def proceed(gui, job):
title += (' ' + _('(Failed cover)'))
det_msg.append(title)
det_msg = '\n'.join(det_msg)
if all_failed:
q = error_dialog(gui, _('Download failed'),
_('Failed to download metadata or covers for any of the %d'
' book(s).') % len(id_map), det_msg=det_msg)
else:
fmsg = ''
if failed_ids or failed_covers:
fmsg = '<p>'+_('Could not download metadata and/or covers for %d of the books. Click'
' "Show details" to see which books.')%len(failed_ids)
msg = '<p>' + _('Finished downloading metadata for <b>%d book(s)</b>. '
'Proceed with updating the metadata in your library?')%len(id_map)
q = MessageBox(MessageBox.QUESTION, _('Download complete'),
msg + fmsg, det_msg=det_msg, show_copy_button=bool(failed_ids),
parent=gui)
q.finished.connect(partial(apply_metadata, job, gui, q))
q.vlb = q.bb.addButton(_('View log'), q.bb.ActionRole)
q.vlb.setIcon(QIcon(I('debug.png')))
q.vlb.clicked.connect(partial(view_log, job, q))
q.det_msg_toggle.setVisible(bool(failed_ids | failed_covers))
q.setModal(False)
q.show()
# }}}
return id_map, failed_ids, failed_covers, all_failed, det_msg
def merge_result(oldmi, newmi):
dummy = Metadata(_('Unknown'))
@ -345,6 +145,10 @@ def download(ids, db, do_identify, covers,
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...')
@ -354,7 +158,7 @@ def download(ids, db, do_identify, covers,
if do_identify:
results = []
try:
results = identify(log, abort, title=title, authors=authors,
results = identify(log, Event(), title=title, authors=authors,
identifiers=identifiers)
except:
pass

View File

@ -15,10 +15,10 @@ from operator import attrgetter
from Queue import Queue, Empty
from PyQt4.Qt import (QStyledItemDelegate, QTextDocument, QRectF, QIcon, Qt,
QStyle, QApplication, QDialog, QVBoxLayout, QLabel, QDialogButtonBox,
QApplication, QDialog, QVBoxLayout, QLabel, QDialogButtonBox,
QStackedWidget, QWidget, QTableView, QGridLayout, QFontInfo, QPalette,
QTimer, pyqtSignal, QAbstractTableModel, QVariant, QSize, QListView,
QPixmap, QAbstractListModel, QColor, QRect, QTextBrowser)
QPixmap, QAbstractListModel, QColor, QRect, QTextBrowser, QModelIndex)
from PyQt4.QtWebKit import QWebView
from calibre.customize.ui import metadata_plugins
@ -52,12 +52,9 @@ class RichTextDelegate(QStyledItemDelegate): # {{{
return ans
def paint(self, painter, option, index):
QStyledItemDelegate.paint(self, painter, option, QModelIndex())
painter.save()
painter.setClipRect(QRectF(option.rect))
if hasattr(QStyle, 'CE_ItemViewItem'):
QApplication.style().drawControl(QStyle.CE_ItemViewItem, option, painter)
elif option.state & QStyle.State_Selected:
painter.fillRect(option.rect, option.palette.highlight())
painter.translate(option.rect.topLeft())
self.to_doc(index).drawContents(painter)
painter.restore()
@ -116,14 +113,17 @@ class CoverDelegate(QStyledItemDelegate): # {{{
def paint(self, painter, option, index):
QStyledItemDelegate.paint(self, painter, option, index)
# Ensure the cover is rendered over any selection rect
style = QApplication.style()
style.drawItemPixmap(painter, option.rect, Qt.AlignTop|Qt.AlignHCenter,
QPixmap(index.data(Qt.DecorationRole)))
if self.timer.isActive() and index.data(Qt.UserRole).toBool():
waiting = self.timer.isActive() and index.data(Qt.UserRole).toBool()
if waiting:
rect = QRect(0, 0, self.spinner_width, self.spinner_width)
rect.moveCenter(option.rect.center())
self.draw_spinner(painter, rect)
else:
# Ensure the cover is rendered over any selection rect
style.drawItemPixmap(painter, option.rect, Qt.AlignTop|Qt.AlignHCenter,
QPixmap(index.data(Qt.DecorationRole)))
# }}}
class ResultsModel(QAbstractTableModel): # {{{
@ -949,7 +949,7 @@ class CoverFetch(QDialog): # {{{
# }}}
if __name__ == '__main__':
#DEBUG_DIALOG = True
DEBUG_DIALOG = True
app = QApplication([])
d = FullFetch()
d.start(title='great gatsby', authors=['fitzgerald'])

View File

@ -46,9 +46,12 @@ class StorePlugin(object): # {{{
'''
def __init__(self, gui, name):
from calibre.gui2 import JSONConfig
self.gui = gui
self.name = name
self.base_plugin = None
self.config = JSONConfig('store/stores/' + self.name)
def open(self, gui, parent=None, detail_item=None, external=False):
'''

View File

@ -8,14 +8,8 @@ __docformat__ = 'restructuredtext en'
from PyQt4.Qt import QWidget
from calibre.gui2 import gprefs
from calibre.gui2.store.basic_config_widget_ui import Ui_Form
def save_settings(config_widget):
gprefs[config_widget.store.name + '_open_external'] = config_widget.open_external.isChecked()
tags = unicode(config_widget.tags.text())
gprefs[config_widget.store.name + '_tags'] = tags
class BasicStoreConfigWidget(QWidget, Ui_Form):
def __init__(self, store):
@ -27,10 +21,10 @@ class BasicStoreConfigWidget(QWidget, Ui_Form):
self.load_setings()
def load_setings(self):
settings = self.store.get_settings()
config = self.store.config
self.open_external.setChecked(settings.get(self.store.name + '_open_external'))
self.tags.setText(settings.get(self.store.name + '_tags', ''))
self.open_external.setChecked(config.get('open_external', False))
self.tags.setText(config.get('tags', ''))
class BasicStoreConfig(object):
@ -41,12 +35,6 @@ class BasicStoreConfig(object):
return BasicStoreConfigWidget(self)
def save_settings(self, config_widget):
save_settings(config_widget)
def get_settings(self):
settings = {}
settings[self.name + '_open_external'] = gprefs.get(self.name + '_open_external', False)
settings[self.name + '_tags'] = gprefs.get(self.name + '_tags', self.name + ', store, download')
return settings
self.config['open_external'] = config_widget.open_external.isChecked()
tags = unicode(config_widget.tags.text())
self.config['tags'] = tags

View File

@ -23,10 +23,9 @@ from calibre.gui2.store.web_store_dialog import WebStoreDialog
class BeWriteStore(BasicStoreConfig, StorePlugin):
def open(self, parent=None, detail_item=None, external=False):
settings = self.get_settings()
url = 'http://www.bewrite.net/mm5/merchant.mvc?Screen=SFNT'
if external or settings.get(self.name + '_open_external', False):
if external or self.config.get('open_external', False):
if detail_item:
url = url + detail_item
open_url(QUrl(url_slash_cleaner(url)))
@ -36,7 +35,7 @@ class BeWriteStore(BasicStoreConfig, StorePlugin):
detail_url = url + detail_item
d = WebStoreDialog(self.gui, url, parent, detail_url)
d.setWindowTitle(self.name)
d.set_tags(settings.get(self.name + '_tags', ''))
d.set_tags(self.config.get('tags', ''))
d.exec_()
def search(self, query, max_results=10, timeout=60):

View File

@ -25,8 +25,6 @@ from calibre.gui2.store.web_store_dialog import WebStoreDialog
class BNStore(BasicStoreConfig, StorePlugin):
def open(self, parent=None, detail_item=None, external=False):
settings = self.get_settings()
pub_id = '21000000000352219'
# Use Kovid's affiliate id 30% of the time.
if random.randint(1, 10) in (1, 2, 3):
@ -40,12 +38,12 @@ class BNStore(BasicStoreConfig, StorePlugin):
isbn = mo.group('isbn')
detail_item = 'http://gan.doubleclick.net/gan_click?lid=41000000012871747&pid=' + isbn + '&adurl=' + detail_item + '&pubid=' + pub_id
if external or settings.get(self.name + '_open_external', False):
if external or self.config.get('open_external', False):
open_url(QUrl(url_slash_cleaner(detail_item if detail_item else url)))
else:
d = WebStoreDialog(self.gui, url, parent, detail_item)
d.setWindowTitle(self.name)
d.set_tags(settings.get(self.name + '_tags', ''))
d.set_tags(self.config.get('tags', ''))
d.exec_()
def search(self, query, max_results=10, timeout=60):

View File

@ -24,7 +24,6 @@ from calibre.gui2.store.web_store_dialog import WebStoreDialog
class DieselEbooksStore(BasicStoreConfig, StorePlugin):
def open(self, parent=None, detail_item=None, external=False):
settings = self.get_settings()
url = 'http://www.diesel-ebooks.com/'
aff_id = '?aid=2049'
@ -37,12 +36,12 @@ class DieselEbooksStore(BasicStoreConfig, StorePlugin):
detail_url = url + detail_item + aff_id
url = url + aff_id
if external or settings.get(self.name + '_open_external', False):
if external or self.config.get('open_external', False):
open_url(QUrl(url_slash_cleaner(detail_url if detail_url else url)))
else:
d = WebStoreDialog(self.gui, url, parent, detail_url)
d.setWindowTitle(self.name)
d.set_tags(settings.get(self.name + '_tags', ''))
d.set_tags(self.config.get('tags', ''))
d.exec_()
def search(self, query, max_results=10, timeout=60):

View File

@ -25,8 +25,6 @@ from calibre.gui2.store.web_store_dialog import WebStoreDialog
class EbookscomStore(BasicStoreConfig, StorePlugin):
def open(self, parent=None, detail_item=None, external=False):
settings = self.get_settings()
m_url = 'http://www.dpbolvw.net/'
h_click = 'click-4879827-10364500'
d_click = 'click-4879827-10281551'
@ -40,12 +38,12 @@ class EbookscomStore(BasicStoreConfig, StorePlugin):
if detail_item:
detail_url = m_url + d_click + detail_item
if external or settings.get(self.name + '_open_external', False):
if external or self.config.get('open_external', False):
open_url(QUrl(url_slash_cleaner(detail_url if detail_url else url)))
else:
d = WebStoreDialog(self.gui, url, parent, detail_url)
d.setWindowTitle(self.name)
d.set_tags(settings.get(self.name + '_tags', ''))
d.set_tags(self.config.get('tags', ''))
d.exec_()
def search(self, query, max_results=10, timeout=60):

View File

@ -25,8 +25,6 @@ from calibre.gui2.store.web_store_dialog import WebStoreDialog
class EHarlequinStore(BasicStoreConfig, StorePlugin):
def open(self, parent=None, detail_item=None, external=False):
settings = self.get_settings()
m_url = 'http://www.dpbolvw.net/'
h_click = 'click-4879827-534091'
d_click = 'click-4879827-10375439'
@ -40,12 +38,12 @@ class EHarlequinStore(BasicStoreConfig, StorePlugin):
if detail_item:
detail_url = m_url + d_click + detail_item
if external or settings.get(self.name + '_open_external', False):
if external or self.config.get('open_external', False):
open_url(QUrl(url_slash_cleaner(detail_url if detail_url else url)))
else:
d = WebStoreDialog(self.gui, url, parent, detail_url)
d.setWindowTitle(self.name)
d.set_tags(settings.get(self.name + '_tags', ''))
d.set_tags(self.config.get('tags', ''))
d.exec_()
def search(self, query, max_results=10, timeout=60):

View File

@ -23,11 +23,10 @@ from calibre.gui2.store.web_store_dialog import WebStoreDialog
class FeedbooksStore(BasicStoreConfig, StorePlugin):
def open(self, parent=None, detail_item=None, external=False):
settings = self.get_settings()
url = 'http://m.feedbooks.com/'
ext_url = 'http://feedbooks.com/'
if external or settings.get(self.name + '_open_external', False):
if external or self.config.get('open_external', False):
if detail_item:
ext_url = ext_url + detail_item
open_url(QUrl(url_slash_cleaner(ext_url)))
@ -37,7 +36,7 @@ class FeedbooksStore(BasicStoreConfig, StorePlugin):
detail_url = url + detail_item
d = WebStoreDialog(self.gui, url, parent, detail_url)
d.setWindowTitle(self.name)
d.set_tags(settings.get(self.name + '_tags', ''))
d.set_tags(self.config.get('tags', ''))
d.exec_()
def search(self, query, max_results=10, timeout=60):

View File

@ -23,11 +23,10 @@ from calibre.gui2.store.web_store_dialog import WebStoreDialog
class GutenbergStore(BasicStoreConfig, StorePlugin):
def open(self, parent=None, detail_item=None, external=False):
settings = self.get_settings()
url = 'http://m.gutenberg.org/'
ext_url = 'http://gutenberg.org/'
if external or settings.get(self.name + '_open_external', False):
if external or self.config.get('open_external', False):
if detail_item:
ext_url = ext_url + detail_item
open_url(QUrl(url_slash_cleaner(ext_url)))
@ -37,7 +36,7 @@ class GutenbergStore(BasicStoreConfig, StorePlugin):
detail_url = url + detail_item
d = WebStoreDialog(self.gui, url, parent, detail_url)
d.setWindowTitle(self.name)
d.set_tags(settings.get(self.name + '_tags', ''))
d.set_tags(self.config.get('tags', ''))
d.exec_()
def search(self, query, max_results=10, timeout=60):

View File

@ -24,8 +24,6 @@ from calibre.gui2.store.web_store_dialog import WebStoreDialog
class KoboStore(BasicStoreConfig, StorePlugin):
def open(self, parent=None, detail_item=None, external=False):
settings = self.get_settings()
m_url = 'http://www.dpbolvw.net/'
h_click = 'click-4879827-10762497'
d_click = 'click-4879827-10772898'
@ -39,12 +37,12 @@ class KoboStore(BasicStoreConfig, StorePlugin):
if detail_item:
detail_url = m_url + d_click + detail_item
if external or settings.get(self.name + '_open_external', False):
if external or self.config.get('open_external', False):
open_url(QUrl(url_slash_cleaner(detail_url if detail_url else url)))
else:
d = WebStoreDialog(self.gui, url, parent, detail_url)
d.setWindowTitle(self.name)
d.set_tags(settings.get(self.name + '_tags', ''))
d.set_tags(self.config.get('tags', ''))
d.exec_()
def search(self, query, max_results=10, timeout=60):

View File

@ -24,19 +24,18 @@ from calibre.gui2.store.web_store_dialog import WebStoreDialog
class ManyBooksStore(BasicStoreConfig, StorePlugin):
def open(self, parent=None, detail_item=None, external=False):
settings = self.get_settings()
url = 'http://manybooks.net/'
detail_url = None
if detail_item:
detail_url = url + detail_item
if external or settings.get(self.name + '_open_external', False):
if external or self.config.get('open_external', False):
open_url(QUrl(url_slash_cleaner(detail_url if detail_url else url)))
else:
d = WebStoreDialog(self.gui, url, parent, detail_url)
d.setWindowTitle(self.name)
d.set_tags(settings.get(self.name + '_tags', ''))
d.set_tags(self.config.get('tags', ''))
d.exec_()
def search(self, query, max_results=10, timeout=60):

View File

@ -18,7 +18,7 @@ from PyQt4.Qt import Qt, QUrl, QDialog, QAbstractItemModel, QModelIndex, QVarian
pyqtSignal
from calibre import browser
from calibre.gui2 import open_url, NONE, JSONConfig
from calibre.gui2 import open_url, NONE
from calibre.gui2.store import StorePlugin
from calibre.gui2.store.basic_config import BasicStoreConfig
from calibre.gui2.store.mobileread_store_dialog_ui import Ui_Dialog
@ -29,20 +29,18 @@ from calibre.utils.icu import sort_key
class MobileReadStore(BasicStoreConfig, StorePlugin):
def genesis(self):
self.config = JSONConfig('store/store/' + self.name)
self.rlock = RLock()
def open(self, parent=None, detail_item=None, external=False):
settings = self.get_settings()
url = 'http://www.mobileread.com/'
if external or settings.get(self.name + '_open_external', False):
if external or self.config.get('open_external', False):
open_url(QUrl(detail_item if detail_item else url))
else:
if detail_item:
d = WebStoreDialog(self.gui, url, parent, detail_item)
d.setWindowTitle(self.name)
d.set_tags(settings.get(self.name + '_tags', ''))
d.set_tags(self.config.get('tags', ''))
d.exec_()
else:
d = MobeReadStoreDialog(self, parent)

View File

@ -23,10 +23,9 @@ from calibre.gui2.store.web_store_dialog import WebStoreDialog
class OpenLibraryStore(BasicStoreConfig, StorePlugin):
def open(self, parent=None, detail_item=None, external=False):
settings = self.get_settings()
url = 'http://openlibrary.org/'
if external or settings.get(self.name + '_open_external', False):
if external or self.config.get('open_external', False):
if detail_item:
url = url + detail_item
open_url(QUrl(url_slash_cleaner(url)))
@ -36,7 +35,7 @@ class OpenLibraryStore(BasicStoreConfig, StorePlugin):
detail_url = url + detail_item
d = WebStoreDialog(self.gui, url, parent, detail_url)
d.setWindowTitle(self.name)
d.set_tags(settings.get(self.name + '_tags', ''))
d.set_tags(self.config.get('tags', ''))
d.exec_()
def search(self, query, max_results=10, timeout=60):

View File

@ -1,726 +0,0 @@
# -*- coding: utf-8 -*-
from __future__ import (unicode_literals, division, absolute_import, print_function)
__license__ = 'GPL 3'
__copyright__ = '2011, John Schember <john@nachtimwald.com>'
__docformat__ = 'restructuredtext en'
import re
import time
import traceback
from contextlib import closing
from operator import attrgetter
from random import shuffle
from threading import Thread
from Queue import Queue
from PyQt4.Qt import (Qt, QAbstractItemModel, QDialog, QTimer, QVariant,
QModelIndex, QPixmap, QSize, QCheckBox, QVBoxLayout)
from calibre import browser
from calibre.gui2 import NONE, JSONConfig
from calibre.gui2.progress_indicator import ProgressIndicator
from calibre.gui2.store.search_ui import Ui_Dialog
from calibre.gui2.store.search_result import SearchResult
from calibre.library.caches import _match, CONTAINS_MATCH, EQUALS_MATCH, \
REGEXP_MATCH
from calibre.utils.icu import sort_key
from calibre.utils.magick.draw import thumbnail
from calibre.utils.search_query_parser import SearchQueryParser
HANG_TIME = 75000 # milliseconds seconds
TIMEOUT = 75 # seconds
SEARCH_THREAD_TOTAL = 4
COVER_DOWNLOAD_THREAD_TOTAL = 2
def comparable_price(text):
if len(text) < 3 or text[-3] not in ('.', ','):
text += '00'
text = re.sub(r'\D', '', text)
text = text.rjust(6, '0')
return text
class SearchDialog(QDialog, Ui_Dialog):
def __init__(self, istores, *args):
QDialog.__init__(self, *args)
self.setupUi(self)
self.config = JSONConfig('store/search')
# We keep a cache of store plugins and reference them by name.
self.store_plugins = istores
self.search_pool = SearchThreadPool(SearchThread, SEARCH_THREAD_TOTAL)
# Check for results and hung threads.
self.checker = QTimer()
self.hang_check = 0
self.model = Matches()
self.results_view.setModel(self.model)
# Add check boxes for each store so the user
# can disable searching specific stores on a
# per search basis.
stores_group_layout = QVBoxLayout()
self.stores_group.setLayout(stores_group_layout)
for x in self.store_plugins:
cbox = QCheckBox(x)
cbox.setChecked(True)
stores_group_layout.addWidget(cbox)
setattr(self, 'store_check_' + x, cbox)
stores_group_layout.addStretch()
# Create and add the progress indicator
self.pi = ProgressIndicator(self, 24)
self.bottom_layout.insertWidget(0, self.pi)
self.search.clicked.connect(self.do_search)
self.checker.timeout.connect(self.get_results)
self.results_view.activated.connect(self.open_store)
self.select_all_stores.clicked.connect(self.stores_select_all)
self.select_invert_stores.clicked.connect(self.stores_select_invert)
self.select_none_stores.clicked.connect(self.stores_select_none)
self.finished.connect(self.dialog_closed)
self.restore_state()
def resize_columns(self):
total = 600
# Cover
self.results_view.setColumnWidth(0, 85)
total = total - 85
# Title
self.results_view.setColumnWidth(1,int(total*.35))
# Author
self.results_view.setColumnWidth(2,int(total*.35))
# Price
self.results_view.setColumnWidth(3, int(total*.5))
# DRM
self.results_view.setColumnWidth(4, int(total*.5))
# Store
self.results_view.setColumnWidth(5, int(total*.15))
# Formats
self.results_view.setColumnWidth(6, int(total*.5))
def do_search(self, checked=False):
# Stop all running threads.
self.checker.stop()
self.search_pool.abort()
# Clear the visible results.
self.results_view.model().clear_results()
# Don't start a search if there is nothing to search for.
query = unicode(self.search_edit.text())
if not query.strip():
return
# Give the query to the results model so it can do
# futher filtering.
self.results_view.model().set_query(query)
# Plugins are in alphebetic order. Randomize the
# order of plugin names. This way plugins closer
# to a don't have an unfair advantage over
# plugins further from a.
store_names = self.store_plugins.keys()
if not store_names:
return
# Remove all of our internal filtering logic from the query.
query = self.clean_query(query)
shuffle(store_names)
# Add plugins that the user has checked to the search pool's work queue.
for n in store_names:
if getattr(self, 'store_check_' + n).isChecked():
self.search_pool.add_task(query, n, self.store_plugins[n], TIMEOUT)
if self.search_pool.has_tasks():
self.hang_check = 0
self.checker.start(100)
self.search_pool.start_threads()
self.pi.startAnimation()
def clean_query(self, query):
query = query.lower()
# Remove control modifiers.
query = query.replace('\\', '')
query = query.replace('!', '')
query = query.replace('=', '')
query = query.replace('~', '')
query = query.replace('>', '')
query = query.replace('<', '')
# Remove the prefix.
for loc in ( 'all', 'author', 'authors', 'title'):
query = re.sub(r'%s:"?(?P<a>[^\s"]+)"?' % loc, '\g<a>', query)
# Remove the prefix and search text.
for loc in ('cover', 'drm', 'format', 'formats', 'price', 'store'):
query = re.sub(r'%s:"[^"]"' % loc, '', query)
query = re.sub(r'%s:[^\s]*' % loc, '', query)
# Remove logic.
query = re.sub(r'(^|\s)(and|not|or)(\s|$)', ' ', query)
# Remove excess whitespace.
query = re.sub(r'\s{2,}', ' ', query)
query = query.strip()
return query
def save_state(self):
self.config['store_search_geometry'] = bytearray(self.saveGeometry())
self.config['store_search_store_splitter_state'] = bytearray(self.store_splitter.saveState())
self.config['store_search_results_view_column_width'] = [self.results_view.columnWidth(i) for i in range(self.model.columnCount())]
store_check = {}
for n in self.store_plugins:
store_check[n] = getattr(self, 'store_check_' + n).isChecked()
self.config['store_search_store_checked'] = store_check
def restore_state(self):
geometry = self.config.get('store_search_geometry', None)
if geometry:
self.restoreGeometry(geometry)
splitter_state = self.config.get('store_search_store_splitter_state', None)
if splitter_state:
self.store_splitter.restoreState(splitter_state)
results_cwidth = self.config.get('store_search_results_view_column_width', None)
if results_cwidth:
for i, x in enumerate(results_cwidth):
if i >= self.model.columnCount():
break
self.results_view.setColumnWidth(i, x)
else:
self.resize_columns()
store_check = self.config.get('store_search_store_checked', None)
if store_check:
for n in store_check:
if hasattr(self, 'store_check_' + n):
getattr(self, 'store_check_' + n).setChecked(store_check[n])
def get_results(self):
# We only want the search plugins to run
# a maximum set amount of time before giving up.
self.hang_check += 1
if self.hang_check >= HANG_TIME:
self.search_pool.abort()
self.checker.stop()
self.pi.stopAnimation()
else:
# Stop the checker if not threads are running.
if not self.search_pool.threads_running() and not self.search_pool.has_tasks():
self.checker.stop()
self.pi.stopAnimation()
while self.search_pool.has_results():
res, store_plugin = self.search_pool.get_result()
if res:
self.results_view.model().add_result(res, store_plugin)
def open_store(self, index):
result = self.results_view.model().get_result(index)
self.store_plugins[result.store_name].open(self, result.detail_item)
def get_store_checks(self):
'''
Returns a list of QCheckBox's for each store.
'''
checks = []
for x in self.store_plugins:
check = getattr(self, 'store_check_' + x, None)
if check:
checks.append(check)
return checks
def stores_select_all(self):
for check in self.get_store_checks():
check.setChecked(True)
def stores_select_invert(self):
for check in self.get_store_checks():
check.setChecked(not check.isChecked())
def stores_select_none(self):
for check in self.get_store_checks():
check.setChecked(False)
def dialog_closed(self, result):
self.model.closing()
self.search_pool.abort()
self.save_state()
class GenericDownloadThreadPool(object):
'''
add_task must be implemented in a subclass.
'''
def __init__(self, thread_type, thread_count):
self.thread_type = thread_type
self.thread_count = thread_count
self.tasks = Queue()
self.results = Queue()
self.threads = []
def add_task(self):
raise NotImplementedError()
def start_threads(self):
for i in range(self.thread_count):
t = self.thread_type(self.tasks, self.results)
self.threads.append(t)
t.start()
def abort(self):
self.tasks = Queue()
self.results = Queue()
for t in self.threads:
t.abort()
self.threads = []
def has_tasks(self):
return not self.tasks.empty()
def get_result(self):
return self.results.get()
def get_result_no_wait(self):
return self.results.get_nowait()
def result_count(self):
return len(self.results)
def has_results(self):
return not self.results.empty()
def threads_running(self):
for t in self.threads:
if t.is_alive():
return True
return False
class SearchThreadPool(GenericDownloadThreadPool):
'''
Threads will run until there is no work or
abort is called. Create and start new threads
using start_threads(). Reset by calling abort().
Example:
sp = SearchThreadPool(SearchThread, 3)
add tasks using add_task(...)
sp.start_threads()
all threads have finished.
sp.abort()
add tasks using add_task(...)
sp.start_threads()
'''
def add_task(self, query, store_name, store_plugin, timeout):
self.tasks.put((query, store_name, store_plugin, timeout))
class SearchThread(Thread):
def __init__(self, tasks, results):
Thread.__init__(self)
self.daemon = True
self.tasks = tasks
self.results = results
self._run = True
def abort(self):
self._run = False
def run(self):
while self._run and not self.tasks.empty():
try:
query, store_name, store_plugin, timeout = self.tasks.get()
for res in store_plugin.search(query, timeout=timeout):
if not self._run:
return
res.store_name = store_name
self.results.put((res, store_plugin))
self.tasks.task_done()
except:
traceback.print_exc()
class CoverThreadPool(GenericDownloadThreadPool):
'''
Once started all threads run until abort is called.
'''
def add_task(self, search_result, update_callback, timeout=5):
self.tasks.put((search_result, update_callback, timeout))
class CoverThread(Thread):
def __init__(self, tasks, results):
Thread.__init__(self)
self.daemon = True
self.tasks = tasks
self.results = results
self._run = True
self.br = browser()
def abort(self):
self._run = False
def run(self):
while self._run:
try:
time.sleep(.1)
while not self.tasks.empty():
if not self._run:
break
result, callback, timeout = self.tasks.get()
if result and result.cover_url:
with closing(self.br.open(result.cover_url, timeout=timeout)) as f:
result.cover_data = f.read()
result.cover_data = thumbnail(result.cover_data, 64, 64)[2]
callback()
self.tasks.task_done()
except:
continue
class DetailsThreadPool(GenericDownloadThreadPool):
'''
Once started all threads run until abort is called.
'''
def add_task(self, search_result, store_plugin, update_callback, timeout=10):
self.tasks.put((search_result, store_plugin, update_callback, timeout))
class DetailsThread(Thread):
def __init__(self, tasks, results):
Thread.__init__(self)
self.daemon = True
self.tasks = tasks
self.results = results
self._run = True
def abort(self):
self._run = False
def run(self):
while self._run:
try:
time.sleep(.1)
while not self.tasks.empty():
if not self._run:
break
result, store_plugin, callback, timeout = self.tasks.get()
if result:
store_plugin.get_details(result, timeout)
callback(result)
self.tasks.task_done()
except:
continue
class Matches(QAbstractItemModel):
HEADERS = [_('Cover'), _('Title'), _('Author(s)'), _('Price'), _('DRM'), _('Store'), _('Formats')]
def __init__(self):
QAbstractItemModel.__init__(self)
self.DRM_LOCKED_ICON = QPixmap(I('drm-locked.png')).scaledToHeight(64,
Qt.SmoothTransformation)
self.DRM_UNLOCKED_ICON = QPixmap(I('drm-unlocked.png')).scaledToHeight(64,
Qt.SmoothTransformation)
self.DRM_UNKNOWN_ICON = QPixmap(I('dialog_question.png')).scaledToHeight(64,
Qt.SmoothTransformation)
# All matches. Used to determine the order to display
# self.matches because the SearchFilter returns
# matches unordered.
self.all_matches = []
# Only the showing matches.
self.matches = []
self.query = ''
self.search_filter = SearchFilter()
self.cover_pool = CoverThreadPool(CoverThread, 2)
self.cover_pool.start_threads()
self.details_pool = DetailsThreadPool(DetailsThread, 4)
self.details_pool.start_threads()
def closing(self):
self.cover_pool.abort()
self.details_pool.abort()
def clear_results(self):
self.all_matches = []
self.matches = []
self.all_matches = []
self.search_filter.clear_search_results()
self.query = ''
self.cover_pool.abort()
self.cover_pool.start_threads()
self.details_pool.abort()
self.details_pool.start_threads()
self.reset()
def add_result(self, result, store_plugin):
if result not in self.all_matches:
self.layoutAboutToBeChanged.emit()
self.all_matches.append(result)
self.search_filter.add_search_result(result)
if result.cover_url:
result.cover_queued = True
self.cover_pool.add_task(result, self.filter_results)
else:
result.cover_queued = False
self.details_pool.add_task(result, store_plugin, self.got_result_details)
self.filter_results()
self.layoutChanged.emit()
def get_result(self, index):
row = index.row()
if row < len(self.matches):
return self.matches[row]
else:
return None
def filter_results(self):
self.layoutAboutToBeChanged.emit()
if self.query:
self.matches = list(self.search_filter.parse(self.query))
else:
self.matches = list(self.search_filter.universal_set())
self.reorder_matches()
self.layoutChanged.emit()
def got_result_details(self, result):
if not result.cover_queued and result.cover_url:
result.cover_queued = True
self.cover_pool.add_task(result, self.filter_results)
if result in self.matches:
row = self.matches.index(result)
self.dataChanged.emit(self.index(row, 0), self.index(row, self.columnCount() - 1))
if result.drm not in (SearchResult.DRM_LOCKED, SearchResult.DRM_UNLOCKED, SearchResult.DRM_UNKNOWN):
result.drm = SearchResult.DRM_UNKNOWN
self.filter_results()
def set_query(self, query):
self.query = query
def index(self, row, column, parent=QModelIndex()):
return self.createIndex(row, column)
def parent(self, index):
if not index.isValid() or index.internalId() == 0:
return QModelIndex()
return self.createIndex(0, 0)
def rowCount(self, *args):
return len(self.matches)
def columnCount(self, *args):
return len(self.HEADERS)
def headerData(self, section, orientation, role):
if role != Qt.DisplayRole:
return NONE
text = ''
if orientation == Qt.Horizontal:
if section < len(self.HEADERS):
text = self.HEADERS[section]
return QVariant(text)
else:
return QVariant(section+1)
def data(self, index, role):
row, col = index.row(), index.column()
result = self.matches[row]
if role == Qt.DisplayRole:
if col == 1:
return QVariant(result.title)
elif col == 2:
return QVariant(result.author)
elif col == 3:
return QVariant(result.price)
elif col == 5:
return QVariant(result.store_name)
elif col == 6:
return QVariant(result.formats)
return NONE
elif role == Qt.DecorationRole:
if col == 0 and result.cover_data:
p = QPixmap()
p.loadFromData(result.cover_data)
return QVariant(p)
if col == 4:
if result.drm == SearchResult.DRM_LOCKED:
return QVariant(self.DRM_LOCKED_ICON)
elif result.drm == SearchResult.DRM_UNLOCKED:
return QVariant(self.DRM_UNLOCKED_ICON)
elif result.drm == SearchResult.DRM_UNKNOWN:
return QVariant(self.DRM_UNKNOWN_ICON)
elif role == Qt.ToolTipRole:
if col == 1:
return QVariant('<p>%s</p>' % result.title)
elif col == 2:
return QVariant('<p>%s</p>' % result.author)
elif col == 3:
return QVariant('<p>' + _('Detected price as: %s. Check with the store before making a purchase to verify this price is correct. This price often does not include promotions the store may be running.') % result.price + '</p>')
elif col == 4:
if result.drm == SearchResult.DRM_LOCKED:
return QVariant('<p>' + _('This book as been detected as having DRM restrictions. This book may not work with your reader and you will have limitations placed upon you as to what you can do with this book. Check with the store before making any purchases to ensure you can actually read this book.') + '</p>')
elif result.drm == SearchResult.DRM_UNLOCKED:
return QVariant('<p>' + _('This book has been detected as being DRM Free. You should be able to use this book on any device provided it is in a format calibre supports for conversion. However, before making a purchase double check the DRM status with the store. The store may not be disclosing the use of DRM.') + '</p>')
else:
return QVariant('<p>' + _('The DRM status of this book could not be determined. There is a very high likelihood that this book is actually DRM restricted.') + '</p>')
elif col == 5:
return QVariant('<p>%s</p>' % result.store_name)
elif col == 6:
return QVariant('<p>%s</p>' % result.formats)
elif role == Qt.SizeHintRole:
return QSize(64, 64)
return NONE
def data_as_text(self, result, col):
text = ''
if col == 1:
text = result.title
elif col == 2:
text = result.author
elif col == 3:
text = comparable_price(result.price)
elif col == 4:
if result.drm == SearchResult.DRM_UNLOCKED:
text = 'a'
elif result.drm == SearchResult.DRM_LOCKED:
text = 'b'
else:
text = 'c'
elif col == 5:
text = result.store_name
elif col == 6:
text = ', '.join(sorted(result.formats.split(',')))
return text
def sort(self, col, order, reset=True):
if not self.matches:
return
descending = order == Qt.DescendingOrder
self.all_matches.sort(None,
lambda x: sort_key(unicode(self.data_as_text(x, col))),
descending)
self.reorder_matches()
if reset:
self.reset()
def reorder_matches(self):
self.matches = sorted(self.matches, key=lambda x: self.all_matches.index(x))
class SearchFilter(SearchQueryParser):
USABLE_LOCATIONS = [
'all',
'author',
'authors',
'cover',
'drm',
'format',
'formats',
'price',
'title',
'store',
]
def __init__(self):
SearchQueryParser.__init__(self, locations=self.USABLE_LOCATIONS)
self.srs = set([])
def add_search_result(self, search_result):
self.srs.add(search_result)
def clear_search_results(self):
self.srs = set([])
def universal_set(self):
return self.srs
def get_matches(self, location, query):
location = location.lower().strip()
if location == 'authors':
location = 'author'
elif location == 'formats':
location = 'format'
matchkind = CONTAINS_MATCH
if len(query) > 1:
if query.startswith('\\'):
query = query[1:]
elif query.startswith('='):
matchkind = EQUALS_MATCH
query = query[1:]
elif query.startswith('~'):
matchkind = REGEXP_MATCH
query = query[1:]
if matchkind != REGEXP_MATCH: ### leave case in regexps because it can be significant e.g. \S \W \D
query = query.lower()
if location not in self.USABLE_LOCATIONS:
return set([])
matches = set([])
all_locs = set(self.USABLE_LOCATIONS) - set(['all'])
locations = all_locs if location == 'all' else [location]
q = {
'author': lambda x: x.author.lower(),
'cover': attrgetter('cover_url'),
'drm': attrgetter('drm'),
'format': attrgetter('formats'),
'price': lambda x: comparable_price(x.price),
'store': lambda x: x.store_name.lower(),
'title': lambda x: x.title.lower(),
}
for x in ('author', 'format'):
q[x+'s'] = q[x]
for sr in self.srs:
for locvalue in locations:
accessor = q[locvalue]
if query == 'true':
if locvalue == 'drm':
if accessor(sr) == SearchResult.DRM_LOCKED:
matches.add(sr)
else:
if accessor(sr) is not None:
matches.add(sr)
continue
if query == 'false':
if locvalue == 'drm':
if accessor(sr) == SearchResult.DRM_UNLOCKED:
matches.add(sr)
else:
if accessor(sr) is None:
matches.add(sr)
continue
# this is bool, so can't match below
if locvalue == 'drm':
continue
try:
### Can't separate authors because comma is used for name sep and author sep
### Exact match might not get what you want. For that reason, turn author
### exactmatch searches into contains searches.
if locvalue == 'author' and matchkind == EQUALS_MATCH:
m = CONTAINS_MATCH
else:
m = matchkind
if locvalue == 'format':
vals = accessor(sr).split(',')
else:
vals = [accessor(sr)]
if _match(query, vals, m):
matches.add(sr)
break
except ValueError: # Unicode errors
traceback.print_exc()
return matches

View File

@ -0,0 +1,190 @@
# -*- coding: utf-8 -*-
from __future__ import (unicode_literals, division, absolute_import, print_function)
__license__ = 'GPL 3'
__copyright__ = '2011, John Schember <john@nachtimwald.com>'
__docformat__ = 'restructuredtext en'
import time
import traceback
from contextlib import closing
from threading import Thread
from Queue import Queue
from calibre import browser
from calibre.utils.magick.draw import thumbnail
class GenericDownloadThreadPool(object):
'''
add_task must be implemented in a subclass.
'''
def __init__(self, thread_type, thread_count):
self.thread_type = thread_type
self.thread_count = thread_count
self.tasks = Queue()
self.results = Queue()
self.threads = []
def add_task(self):
raise NotImplementedError()
def start_threads(self):
for i in range(self.thread_count):
t = self.thread_type(self.tasks, self.results)
self.threads.append(t)
t.start()
def abort(self):
self.tasks = Queue()
self.results = Queue()
for t in self.threads:
t.abort()
self.threads = []
def has_tasks(self):
return not self.tasks.empty()
def get_result(self):
return self.results.get()
def get_result_no_wait(self):
return self.results.get_nowait()
def result_count(self):
return len(self.results)
def has_results(self):
return not self.results.empty()
def threads_running(self):
for t in self.threads:
if t.is_alive():
return True
return False
class SearchThreadPool(GenericDownloadThreadPool):
'''
Threads will run until there is no work or
abort is called. Create and start new threads
using start_threads(). Reset by calling abort().
Example:
sp = SearchThreadPool(SearchThread, 3)
add tasks using add_task(...)
sp.start_threads()
all threads have finished.
sp.abort()
add tasks using add_task(...)
sp.start_threads()
'''
def add_task(self, query, store_name, store_plugin, timeout):
self.tasks.put((query, store_name, store_plugin, timeout))
class SearchThread(Thread):
def __init__(self, tasks, results):
Thread.__init__(self)
self.daemon = True
self.tasks = tasks
self.results = results
self._run = True
def abort(self):
self._run = False
def run(self):
while self._run and not self.tasks.empty():
try:
query, store_name, store_plugin, timeout = self.tasks.get()
for res in store_plugin.search(query, timeout=timeout):
if not self._run:
return
res.store_name = store_name
self.results.put((res, store_plugin))
self.tasks.task_done()
except:
traceback.print_exc()
class CoverThreadPool(GenericDownloadThreadPool):
'''
Once started all threads run until abort is called.
'''
def add_task(self, search_result, update_callback, timeout=5):
self.tasks.put((search_result, update_callback, timeout))
class CoverThread(Thread):
def __init__(self, tasks, results):
Thread.__init__(self)
self.daemon = True
self.tasks = tasks
self.results = results
self._run = True
self.br = browser()
def abort(self):
self._run = False
def run(self):
while self._run:
try:
time.sleep(.1)
while not self.tasks.empty():
if not self._run:
break
result, callback, timeout = self.tasks.get()
if result and result.cover_url:
with closing(self.br.open(result.cover_url, timeout=timeout)) as f:
result.cover_data = f.read()
result.cover_data = thumbnail(result.cover_data, 64, 64)[2]
callback()
self.tasks.task_done()
except:
continue
class DetailsThreadPool(GenericDownloadThreadPool):
'''
Once started all threads run until abort is called.
'''
def add_task(self, search_result, store_plugin, update_callback, timeout=10):
self.tasks.put((search_result, store_plugin, update_callback, timeout))
class DetailsThread(Thread):
def __init__(self, tasks, results):
Thread.__init__(self)
self.daemon = True
self.tasks = tasks
self.results = results
self._run = True
def abort(self):
self._run = False
def run(self):
while self._run:
try:
time.sleep(.1)
while not self.tasks.empty():
if not self._run:
break
result, store_plugin, callback, timeout = self.tasks.get()
if result:
store_plugin.get_details(result, timeout)
callback(result)
self.tasks.task_done()
except:
continue

View File

@ -0,0 +1,337 @@
# -*- coding: utf-8 -*-
from __future__ import (unicode_literals, division, absolute_import, print_function)
__license__ = 'GPL 3'
__copyright__ = '2011, John Schember <john@nachtimwald.com>'
__docformat__ = 'restructuredtext en'
import re
from operator import attrgetter
from PyQt4.Qt import (Qt, QAbstractItemModel, QVariant, QPixmap, QModelIndex, QSize)
from calibre.gui2 import NONE
from calibre.gui2.store.search_result import SearchResult
from calibre.gui2.store.search.download_thread import DetailsThreadPool, \
DetailsThread, CoverThreadPool, CoverThread
from calibre.library.caches import _match, CONTAINS_MATCH, EQUALS_MATCH, \
REGEXP_MATCH
from calibre.utils.icu import sort_key
from calibre.utils.search_query_parser import SearchQueryParser
def comparable_price(text):
if len(text) < 3 or text[-3] not in ('.', ','):
text += '00'
text = re.sub(r'\D', '', text)
text = text.rjust(6, '0')
return text
class Matches(QAbstractItemModel):
HEADERS = [_('Cover'), _('Title'), _('Price'), _('DRM'), _('Store')]
HTML_COLS = (1, 4)
def __init__(self):
QAbstractItemModel.__init__(self)
self.DRM_LOCKED_ICON = QPixmap(I('drm-locked.png')).scaledToHeight(64,
Qt.SmoothTransformation)
self.DRM_UNLOCKED_ICON = QPixmap(I('drm-unlocked.png')).scaledToHeight(64,
Qt.SmoothTransformation)
self.DRM_UNKNOWN_ICON = QPixmap(I('dialog_question.png')).scaledToHeight(64,
Qt.SmoothTransformation)
# All matches. Used to determine the order to display
# self.matches because the SearchFilter returns
# matches unordered.
self.all_matches = []
# Only the showing matches.
self.matches = []
self.query = ''
self.search_filter = SearchFilter()
self.cover_pool = CoverThreadPool(CoverThread, 2)
self.cover_pool.start_threads()
self.details_pool = DetailsThreadPool(DetailsThread, 4)
self.details_pool.start_threads()
self.sort_col = 2
self.sort_order = Qt.AscendingOrder
def closing(self):
self.cover_pool.abort()
self.details_pool.abort()
def clear_results(self):
self.all_matches = []
self.matches = []
self.all_matches = []
self.search_filter.clear_search_results()
self.query = ''
self.cover_pool.abort()
self.cover_pool.start_threads()
self.details_pool.abort()
self.details_pool.start_threads()
self.reset()
def add_result(self, result, store_plugin):
if result not in self.all_matches:
self.layoutAboutToBeChanged.emit()
self.all_matches.append(result)
self.search_filter.add_search_result(result)
if result.cover_url:
result.cover_queued = True
self.cover_pool.add_task(result, self.filter_results)
else:
result.cover_queued = False
self.details_pool.add_task(result, store_plugin, self.got_result_details)
self.filter_results()
self.layoutChanged.emit()
def get_result(self, index):
row = index.row()
if row < len(self.matches):
return self.matches[row]
else:
return None
def has_results(self):
return len(self.matches) > 0
def filter_results(self):
self.layoutAboutToBeChanged.emit()
if self.query:
self.matches = list(self.search_filter.parse(self.query))
else:
self.matches = list(self.search_filter.universal_set())
self.sort(self.sort_col, self.sort_order, False)
self.layoutChanged.emit()
def got_result_details(self, result):
if not result.cover_queued and result.cover_url:
result.cover_queued = True
self.cover_pool.add_task(result, self.filter_results)
if result in self.matches:
row = self.matches.index(result)
self.dataChanged.emit(self.index(row, 0), self.index(row, self.columnCount() - 1))
if result.drm not in (SearchResult.DRM_LOCKED, SearchResult.DRM_UNLOCKED, SearchResult.DRM_UNKNOWN):
result.drm = SearchResult.DRM_UNKNOWN
self.filter_results()
def set_query(self, query):
self.query = query
def index(self, row, column, parent=QModelIndex()):
return self.createIndex(row, column)
def parent(self, index):
if not index.isValid() or index.internalId() == 0:
return QModelIndex()
return self.createIndex(0, 0)
def rowCount(self, *args):
return len(self.matches)
def columnCount(self, *args):
return len(self.HEADERS)
def headerData(self, section, orientation, role):
if role != Qt.DisplayRole:
return NONE
text = ''
if orientation == Qt.Horizontal:
if section < len(self.HEADERS):
text = self.HEADERS[section]
return QVariant(text)
else:
return QVariant(section+1)
def data(self, index, role):
row, col = index.row(), index.column()
result = self.matches[row]
if role == Qt.DisplayRole:
if col == 1:
t = result.title if result.title else _('Unknown')
a = result.author if result.author else ''
return QVariant('<b>%s</b><br><i>%s</i>' % (t, a))
elif col == 2:
return QVariant(result.price)
elif col == 4:
return QVariant('%s<br>%s' % (result.store_name, result.formats))
return NONE
elif role == Qt.DecorationRole:
if col == 0 and result.cover_data:
p = QPixmap()
p.loadFromData(result.cover_data)
return QVariant(p)
if col == 3:
if result.drm == SearchResult.DRM_LOCKED:
return QVariant(self.DRM_LOCKED_ICON)
elif result.drm == SearchResult.DRM_UNLOCKED:
return QVariant(self.DRM_UNLOCKED_ICON)
elif result.drm == SearchResult.DRM_UNKNOWN:
return QVariant(self.DRM_UNKNOWN_ICON)
elif role == Qt.ToolTipRole:
if col == 1:
return QVariant('<p>%s</p>' % result.title)
elif col == 2:
return QVariant('<p>' + _('Detected price as: %s. Check with the store before making a purchase to verify this price is correct. This price often does not include promotions the store may be running.') % result.price + '</p>')
elif col == 3:
if result.drm == SearchResult.DRM_LOCKED:
return QVariant('<p>' + _('This book as been detected as having DRM restrictions. This book may not work with your reader and you will have limitations placed upon you as to what you can do with this book. Check with the store before making any purchases to ensure you can actually read this book.') + '</p>')
elif result.drm == SearchResult.DRM_UNLOCKED:
return QVariant('<p>' + _('This book has been detected as being DRM Free. You should be able to use this book on any device provided it is in a format calibre supports for conversion. However, before making a purchase double check the DRM status with the store. The store may not be disclosing the use of DRM.') + '</p>')
else:
return QVariant('<p>' + _('The DRM status of this book could not be determined. There is a very high likelihood that this book is actually DRM restricted.') + '</p>')
elif col == 4:
return QVariant('<p>%s</p>' % result.formats)
elif role == Qt.SizeHintRole:
return QSize(64, 64)
return NONE
def data_as_text(self, result, col):
text = ''
if col == 1:
text = result.title
elif col == 2:
text = comparable_price(result.price)
elif col == 3:
if result.drm == SearchResult.DRM_UNLOCKED:
text = 'a'
if result.drm == SearchResult.DRM_LOCKED:
text = 'b'
else:
text = 'c'
elif col == 4:
text = result.store_name
return text
def sort(self, col, order, reset=True):
self.sort_col = col
self.sort_order = order
if not self.matches:
return
descending = order == Qt.DescendingOrder
self.all_matches.sort(None,
lambda x: sort_key(unicode(self.data_as_text(x, col))),
descending)
self.reorder_matches()
if reset:
self.reset()
def reorder_matches(self):
def keygen(x):
try:
return self.all_matches.index(x)
except:
return 100000
self.matches = sorted(self.matches, key=keygen)
class SearchFilter(SearchQueryParser):
USABLE_LOCATIONS = [
'all',
'author',
'authors',
'cover',
'drm',
'format',
'formats',
'price',
'title',
'store',
]
def __init__(self):
SearchQueryParser.__init__(self, locations=self.USABLE_LOCATIONS)
self.srs = set([])
def add_search_result(self, search_result):
self.srs.add(search_result)
def clear_search_results(self):
self.srs = set([])
def universal_set(self):
return self.srs
def get_matches(self, location, query):
location = location.lower().strip()
if location == 'authors':
location = 'author'
elif location == 'formats':
location = 'format'
matchkind = CONTAINS_MATCH
if len(query) > 1:
if query.startswith('\\'):
query = query[1:]
elif query.startswith('='):
matchkind = EQUALS_MATCH
query = query[1:]
elif query.startswith('~'):
matchkind = REGEXP_MATCH
query = query[1:]
if matchkind != REGEXP_MATCH: ### leave case in regexps because it can be significant e.g. \S \W \D
query = query.lower()
if location not in self.USABLE_LOCATIONS:
return set([])
matches = set([])
all_locs = set(self.USABLE_LOCATIONS) - set(['all'])
locations = all_locs if location == 'all' else [location]
q = {
'author': lambda x: x.author.lower(),
'cover': attrgetter('cover_url'),
'drm': attrgetter('drm'),
'format': attrgetter('formats'),
'price': lambda x: comparable_price(x.price),
'store': lambda x: x.store_name.lower(),
'title': lambda x: x.title.lower(),
}
for x in ('author', 'format'):
q[x+'s'] = q[x]
for sr in self.srs:
for locvalue in locations:
accessor = q[locvalue]
if query == 'true':
if locvalue == 'drm':
if accessor(sr) == SearchResult.DRM_LOCKED:
matches.add(sr)
else:
if accessor(sr) is not None:
matches.add(sr)
continue
if query == 'false':
if locvalue == 'drm':
if accessor(sr) == SearchResult.DRM_UNLOCKED:
matches.add(sr)
else:
if accessor(sr) is None:
matches.add(sr)
continue
# this is bool, so can't match below
if locvalue == 'drm':
continue
try:
### Can't separate authors because comma is used for name sep and author sep
### Exact match might not get what you want. For that reason, turn author
### exactmatch searches into contains searches.
if locvalue == 'author' and matchkind == EQUALS_MATCH:
m = CONTAINS_MATCH
else:
m = matchkind
if locvalue == 'format':
vals = accessor(sr).split(',')
else:
vals = [accessor(sr)]
if _match(query, vals, m):
matches.add(sr)
break
except ValueError: # Unicode errors
import traceback
traceback.print_exc()
return matches

View File

@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
from __future__ import (unicode_literals, division, absolute_import, print_function)
__license__ = 'GPL 3'
__copyright__ = '2011, John Schember <john@nachtimwald.com>'
__docformat__ = 'restructuredtext en'
from PyQt4.Qt import (QTreeView)
from calibre.gui2.metadata.single_download import RichTextDelegate
from calibre.gui2.store.search.models import Matches
class ResultsView(QTreeView):
def __init__(self, *args):
QTreeView.__init__(self,*args)
self._model = Matches()
self.setModel(self._model)
self.rt_delegate = RichTextDelegate(self)
for i in self._model.HTML_COLS:
self.setItemDelegateForColumn(i, self.rt_delegate)

View File

@ -0,0 +1,232 @@
# -*- coding: utf-8 -*-
from __future__ import (unicode_literals, division, absolute_import, print_function)
__license__ = 'GPL 3'
__copyright__ = '2011, John Schember <john@nachtimwald.com>'
__docformat__ = 'restructuredtext en'
import re
from random import shuffle
from PyQt4.Qt import (Qt, QDialog, QTimer, QCheckBox, QVBoxLayout)
from calibre.gui2 import JSONConfig, info_dialog
from calibre.gui2.progress_indicator import ProgressIndicator
from calibre.gui2.store.search.download_thread import SearchThreadPool, SearchThread
from calibre.gui2.store.search.search_ui import Ui_Dialog
HANG_TIME = 75000 # milliseconds seconds
TIMEOUT = 75 # seconds
SEARCH_THREAD_TOTAL = 4
COVER_DOWNLOAD_THREAD_TOTAL = 2
class SearchDialog(QDialog, Ui_Dialog):
def __init__(self, istores, *args):
QDialog.__init__(self, *args)
self.setupUi(self)
self.config = JSONConfig('store/search')
# We keep a cache of store plugins and reference them by name.
self.store_plugins = istores
self.search_pool = SearchThreadPool(SearchThread, SEARCH_THREAD_TOTAL)
# Check for results and hung threads.
self.checker = QTimer()
self.hang_check = 0
# Add check boxes for each store so the user
# can disable searching specific stores on a
# per search basis.
stores_group_layout = QVBoxLayout()
self.stores_group.setLayout(stores_group_layout)
for x in self.store_plugins:
cbox = QCheckBox(x)
cbox.setChecked(True)
stores_group_layout.addWidget(cbox)
setattr(self, 'store_check_' + x, cbox)
stores_group_layout.addStretch()
# Create and add the progress indicator
self.pi = ProgressIndicator(self, 24)
self.top_layout.addWidget(self.pi)
self.search.clicked.connect(self.do_search)
self.checker.timeout.connect(self.get_results)
self.results_view.activated.connect(self.open_store)
self.select_all_stores.clicked.connect(self.stores_select_all)
self.select_invert_stores.clicked.connect(self.stores_select_invert)
self.select_none_stores.clicked.connect(self.stores_select_none)
self.finished.connect(self.dialog_closed)
self.restore_state()
def resize_columns(self):
total = 600
# Cover
self.results_view.setColumnWidth(0, 85)
total = total - 85
# Title / Author
self.results_view.setColumnWidth(1,int(total*.40))
# Price
self.results_view.setColumnWidth(2,int(total*.20))
# DRM
self.results_view.setColumnWidth(3, int(total*.15))
# Store / Formats
self.results_view.setColumnWidth(4, int(total*.25))
def do_search(self, checked=False):
# Stop all running threads.
self.checker.stop()
self.search_pool.abort()
# Clear the visible results.
self.results_view.model().clear_results()
# Don't start a search if there is nothing to search for.
query = unicode(self.search_edit.text())
if not query.strip():
return
# Give the query to the results model so it can do
# futher filtering.
self.results_view.model().set_query(query)
# Plugins are in alphebetic order. Randomize the
# order of plugin names. This way plugins closer
# to a don't have an unfair advantage over
# plugins further from a.
store_names = self.store_plugins.keys()
if not store_names:
return
# Remove all of our internal filtering logic from the query.
query = self.clean_query(query)
shuffle(store_names)
# Add plugins that the user has checked to the search pool's work queue.
for n in store_names:
if getattr(self, 'store_check_' + n).isChecked():
self.search_pool.add_task(query, n, self.store_plugins[n], TIMEOUT)
if self.search_pool.has_tasks():
self.hang_check = 0
self.checker.start(100)
self.search_pool.start_threads()
self.pi.startAnimation()
def clean_query(self, query):
query = query.lower()
# Remove control modifiers.
query = query.replace('\\', '')
query = query.replace('!', '')
query = query.replace('=', '')
query = query.replace('~', '')
query = query.replace('>', '')
query = query.replace('<', '')
# Remove the prefix.
for loc in ( 'all', 'author', 'authors', 'title'):
query = re.sub(r'%s:"?(?P<a>[^\s"]+)"?' % loc, '\g<a>', query)
# Remove the prefix and search text.
for loc in ('cover', 'drm', 'format', 'formats', 'price', 'store'):
query = re.sub(r'%s:"[^"]"' % loc, '', query)
query = re.sub(r'%s:[^\s]*' % loc, '', query)
# Remove logic.
query = re.sub(r'(^|\s)(and|not|or)(\s|$)', ' ', query)
# Remove excess whitespace.
query = re.sub(r'\s{2,}', ' ', query)
query = query.strip()
return query
def save_state(self):
self.config['geometry'] = bytearray(self.saveGeometry())
self.config['store_splitter_state'] = bytearray(self.store_splitter.saveState())
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
store_check = {}
for n in self.store_plugins:
store_check[n] = getattr(self, 'store_check_' + n).isChecked()
self.config['store_checked'] = store_check
def restore_state(self):
geometry = self.config.get('geometry', None)
if geometry:
self.restoreGeometry(geometry)
splitter_state = self.config.get('store_splitter_state', None)
if splitter_state:
self.store_splitter.restoreState(splitter_state)
results_cwidth = self.config.get('results_view_column_width', None)
if results_cwidth:
for i, x in enumerate(results_cwidth):
if i >= self.results_view.model().columnCount():
break
self.results_view.setColumnWidth(i, x)
else:
self.resize_columns()
store_check = self.config.get('store_checked', None)
if store_check:
for n in store_check:
if hasattr(self, 'store_check_' + n):
getattr(self, 'store_check_' + n).setChecked(store_check[n])
self.results_view.model().sort_col = self.config.get('sort_col', 2)
self.results_view.model().sort_order = self.config.get('sort_order', Qt.AscendingOrder)
self.results_view.header().setSortIndicator(self.results_view.model().sort_col, self.results_view.model().sort_order)
def get_results(self):
# We only want the search plugins to run
# a maximum set amount of time before giving up.
self.hang_check += 1
if self.hang_check >= HANG_TIME:
self.search_pool.abort()
self.checker.stop()
self.pi.stopAnimation()
else:
# Stop the checker if not threads are running.
if not self.search_pool.threads_running() and not self.search_pool.has_tasks():
self.checker.stop()
self.pi.stopAnimation()
while self.search_pool.has_results():
res, store_plugin = self.search_pool.get_result()
if res:
self.results_view.model().add_result(res, store_plugin)
if not self.checker.isActive():
if not self.results_view.model().has_results():
info_dialog(self, _('No matches'), _('Couldn\'t find any books matching your query.'), show=True, show_copy_button=False)
def open_store(self, index):
result = self.results_view.model().get_result(index)
self.store_plugins[result.store_name].open(self, result.detail_item)
def get_store_checks(self):
'''
Returns a list of QCheckBox's for each store.
'''
checks = []
for x in self.store_plugins:
check = getattr(self, 'store_check_' + x, None)
if check:
checks.append(check)
return checks
def stores_select_all(self):
for check in self.get_store_checks():
check.setChecked(True)
def stores_select_invert(self):
for check in self.get_store_checks():
check.setChecked(not check.isChecked())
def stores_select_none(self):
for check in self.get_store_checks():
check.setChecked(False)
def dialog_closed(self, result):
self.results_view.model().closing()
self.search_pool.abort()
self.save_state()

View File

@ -14,7 +14,7 @@
<string>Get Books</string>
</property>
<property name="windowIcon">
<iconset resource="../../../../resources/images.qrc">
<iconset>
<normaloff>:/images/store.png</normaloff>:/images/store.png</iconset>
</property>
<property name="sizeGripEnabled">
@ -22,7 +22,7 @@
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QHBoxLayout" name="horizontalLayout_2">
<layout class="QHBoxLayout" name="top_layout">
<item>
<widget class="QLabel" name="label">
<property name="text">
@ -62,8 +62,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>170</width>
<height>138</height>
<width>215</width>
<height>116</height>
</rect>
</property>
</widget>
@ -110,7 +110,7 @@
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<widget class="QTreeView" name="results_view">
<widget class="ResultsView" name="results_view">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>1</horstretch>
@ -178,6 +178,13 @@
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>ResultsView</class>
<extends>QTreeView</extends>
<header>results_view.h</header>
</customwidget>
</customwidgets>
<resources>
<include location="../../../../resources/images.qrc"/>
</resources>

View File

@ -25,7 +25,6 @@ from calibre.gui2.store.web_store_dialog import WebStoreDialog
class SmashwordsStore(BasicStoreConfig, StorePlugin):
def open(self, parent=None, detail_item=None, external=False):
settings = self.get_settings()
url = 'http://www.smashwords.com/'
aff_id = '?ref=usernone'
@ -38,12 +37,12 @@ class SmashwordsStore(BasicStoreConfig, StorePlugin):
detail_url = url + detail_item + aff_id
url = url + aff_id
if external or settings.get(self.name + '_open_external', False):
if external or self.config.get('open_external', False):
open_url(QUrl(url_slash_cleaner(detail_url if detail_url else url)))
else:
d = WebStoreDialog(self.gui, url, parent, detail_url)
d.setWindowTitle(self.name)
d.set_tags(settings.get(self.name + '_tags', ''))
d.set_tags(self.config.get('tags', ''))
d.exec_()
def search(self, query, max_results=10, timeout=60):

View File

@ -1756,7 +1756,8 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
return books_to_refresh
def set_metadata(self, id, mi, ignore_errors=False, set_title=True,
set_authors=True, commit=True, force_changes=False):
set_authors=True, commit=True, force_changes=False,
notify=True):
'''
Set metadata for the book `id` from the `Metadata` object `mi`
@ -1865,7 +1866,8 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
label=user_mi[key]['label'], commit=False)
if commit:
self.conn.commit()
self.notify('metadata', [id])
if notify:
self.notify('metadata', [id])
def authors_sort_strings(self, id, index_is_id=False):
'''
@ -2002,8 +2004,16 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
return False
if isbytestring(title):
title = title.decode(preferred_encoding, 'replace')
old_title = self.title(id, index_is_id=True)
# We cannot check if old_title == title as previous code might have
# already updated the cache
only_case_change = icu_lower(old_title) == icu_lower(title)
self.conn.execute('UPDATE books SET title=? WHERE id=?', (title, id))
self.data.set(id, self.FIELD_MAP['title'], title, row_is_id=True)
if only_case_change:
# SQLite update trigger will not update sort on a case change
self.conn.execute('UPDATE books SET sort=? WHERE id=?',
(title_sort(title), id))
ts = self.conn.get('SELECT sort FROM books WHERE id=?', (id,),
all=False)
if ts:

View File

@ -11,14 +11,14 @@ msgstr ""
"Project-Id-Version: ca\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2011-04-17 16:54+0000\n"
"PO-Revision-Date: 2011-04-17 19:55+0000\n"
"PO-Revision-Date: 2011-04-21 10:40+0000\n"
"Last-Translator: FerranRius <frius64@hotmail.com>\n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2011-04-18 04:49+0000\n"
"X-Generator: Launchpad (build 12735)\n"
"X-Launchpad-Export-Date: 2011-04-22 04:35+0000\n"
"X-Generator: Launchpad (build 12758)\n"
#: /home/kovid/work/calibre/src/calibre/customize/__init__.py:56
msgid "Does absolutely nothing"
@ -18718,6 +18718,85 @@ msgid ""
"sony_collection_renaming_rules={'series':'Series', 'tags':'Tag'}\n"
"sony_collection_name_template='{category:||: }{value}'"
msgstr ""
"Especifica les regles de canvi de nom per a les col·leccions Sony. Aquest "
"ajustament\n"
"només saplica si sestableix la gestió de metadades a automàtica. Les "
"col·leccions\n"
"Sony sanomenen depenent de si el camp és estàndard o personalitzat. Una\n"
"col·lecció derivada dun camp estàndard rep el nom del valor del camp. Per "
"exemple,\n"
"si la columna estàndard «sèrie» conté el valor «Darkover», en nom de la "
"col·lecció\n"
"serà «Darkover». Una col·lecció derivada dun camp personalitzat tindrà el "
"nom del\n"
"camp afegit al valor. Per exemple, si una columna personalitzada de sèrie "
"anomenada\n"
"«Sèries meves» conté el nom «Darkover», la col·lecció es dirà per defecte "
"«Darkover \n"
"(Sèries meves)». Pel que fa a aquesta documentació, considerem que "
"«Darkover» és\n"
"el valor i «Sèries meves» la categoria. Si dos llibres tenen camps que "
"generen el\n"
"mateix nom de col·lecció, els dos llibres estaran a la col·lecció.\n"
"Aquest conjunt dajustaments us permet especificar com sanomenaran les \n"
"col·leccions per a camps estàndards o personalitzats. Es pot utilitzar per "
"afegir una\n"
"descripció a un camp estàndars, per exemple «Foo (Etiqueta)» en lloc de "
"«Foo».\n"
"També es pot utilitzar per forçar que múltiples camps acabin a una mateixa "
"col·lecció.\n"
"Per exemple, es pot forçar que els valors a «sèries», «#la_meva_sèrie_1» i \n"
"«#la_meva_sèrie_2» apareguin en col·leccions anomenades «algun_valor "
"(Sèrie)»,\n"
"fusionant els camps en un conjunt de col·leccions.\n"
"Hi ha dos ajustaments relacionats. El primer determina el nom de la "
"categoria que\n"
"sutilitza per a un camp de metadades. El segon és una plantilla que "
"sutilitza per\n"
"determinar com es combinen el valor i la categoria per crear el nom de la "
"col·lecció.\n"
"La sintaxi del primer ajustament, «sony_collection_renaming_rules», és:\n"
"{'nom_camp_de_cerca':'nom_categoria_a_utilitzar', 'nom_cerca':'nom', ...}\n"
"El segon ajustament «sony_collection_name_template», és una plantilla. "
"Utilitza el\n"
"mateix llenguatge que els quadres de connexions i les plantilles de desar. "
"Aquest\n"
"ajustament controla com es combinen el valor i la categoria per obtenir el "
"nom de la\n"
"col·lecció. Hi ha dos camps disponibles, {categoria} i {valor}. El camp "
"{valor} no\n"
"està mai buit. El camp {categoria} pot estar buit. Per defecte es posa el "
"valor primer\n"
"i després, si no està buida, la categoria entre parèntesis:\n"
"'{value} {category:|(|)}'\n"
"Exemples: Els primers tres exemples assumeixen que no sha canviat el segon\n"
"ajustament.\n"
"1: Es vol que tres columnes de sèries es fusionin en un conjunt de "
"col·leccions. El\n"
"nom de cerca de les columnes són «sèrie», «#sèrie_2» i «#sèrie_3». No es vol "
"res\n"
"entre parèntesis. El valor per a lajustament serà:\n"
"sony_collection_renaming_rules={'sèrie':'', '#sèrie_1':'', '#sèrie_2':''}\n"
"2: Es vol que la paraula «(Sèrie)» aparegui a les col·leccions obtingudes a "
"partir de\n"
"sèries i que la paraula «(Etiqueta)» aparegui a les col·leccions obtingudes "
"a partir\n"
"detiquetes. Serà:\n"
"sony_collection_renaming_rules={'series':'Sèrie', 'tags':'Etiqueta'}\n"
"3: Es vol fusionar «sèrie» i «#la_meva_sèrie» i que safegeixi al final del "
"nom de la\n"
"col lecció «(Sèrie)». La regla de canvi de nom serà:\n"
"sony_collection_renaming_rules={'series':'Sèrie', "
"'#la_meva_sèrie':'Series'}\n"
"4: Igual que a lexemple 2, però en lloc de tenir el nom de la categoria "
"entre\n"
"parèntesis i afegit al final del valor, es vol abans del valor i separat per "
"dos punts,\n"
"com a «Sèrie: Darkover». Sha de canviar la plantilla utilitzada per donar "
"format al\n"
"nom de categoria. Els dos ajustaments resultants seran:\n"
"sony_collection_renaming_rules={'series':'Sèrie', 'tags':'Etiqueta'}\n"
"sony_collection_name_template='{category:||: }{value}'"
#: /home/kovid/work/calibre/resources/default_tweaks.py:221
msgid "Specify how SONY collections are sorted"

File diff suppressed because it is too large Load Diff

View File

@ -8,13 +8,13 @@ msgstr ""
"Project-Id-Version: de\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2011-04-17 16:54+0000\n"
"PO-Revision-Date: 2011-04-20 15:20+0000\n"
"Last-Translator: Armin Geller <Unknown>\n"
"PO-Revision-Date: 2011-04-21 11:25+0000\n"
"Last-Translator: Christine Emrich <Unknown>\n"
"Language-Team: American English <kde-i18n-doc@lists.kde.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2011-04-21 04:37+0000\n"
"X-Launchpad-Export-Date: 2011-04-22 04:36+0000\n"
"X-Generator: Launchpad (build 12758)\n"
"Generated-By: pygettext.py 1.5\n"
@ -500,7 +500,7 @@ msgstr "Metadaten laden"
#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:1059
msgid "Control how calibre downloads ebook metadata from the net"
msgstr ""
"Kontrolle der calibre-Vorhehensweise beim Herunterladen von E-Book-Metadaten "
"Kontrolle der calibre-Vorhehensweise beim Herunterladen von eBook-Metadaten "
"aus dem Netz"
#: /home/kovid/work/calibre/src/calibre/customize/builtins.py:1064
@ -4973,7 +4973,7 @@ msgstr "Fehlgeschlagen"
#: /home/kovid/work/calibre/src/calibre/gui2/actions/choose_library.py:317
msgid "Database integrity check failed, click Show details for details."
msgstr ""
"Die Überprüfung der Datenbankintegrität hat Fehler gefunden. Clicken Sie auf "
"Die Überprüfung der Datenbankintegrität hat Fehler gefunden. Klicken Sie auf "
"Details anzeigen für weitere Informationen."
#: /home/kovid/work/calibre/src/calibre/gui2/actions/choose_library.py:322
@ -9259,7 +9259,7 @@ msgstr "&Generate standard Cover"
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:579
msgid "&Remove cover"
msgstr "&Remove Cover"
msgstr "&Remove Umschlag"
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:580
msgid "Set from &ebook file(s)"
@ -10490,7 +10490,7 @@ msgstr "Neue Kategorie hinzufügen"
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/tag_categories_ui.py:174
msgid "Rename the current category to the what is in the box"
msgstr ""
msgstr "Derzeitige Kategorie zum Inhalt der Eingabebox umbenennen"
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/tag_categories_ui.py:176
msgid "Category filter: "
@ -11287,7 +11287,7 @@ msgstr "Schnellsuche durchführen (Sie können auch die Eingabetaste drücken)"
#: /home/kovid/work/calibre/src/calibre/gui2/layout.py:197
msgid "Reset Quick Search"
msgstr "Quick Search löschen"
msgstr "Schnellsuche löschen"
#: /home/kovid/work/calibre/src/calibre/gui2/layout.py:213
msgid "Copy current search text (instead of search name)"
@ -11789,10 +11789,13 @@ msgid ""
"Failed to apply updated metadata for some books in your library. Click "
"\"Show Details\" to see details."
msgstr ""
"Die aktualisieren Metadaten konnten für einige Bücher Ihrer Bibliothek nicht "
"übernommen werden. Klicken Sie auf \"Zeige Details\" für weitere "
"Informationen."
#: /home/kovid/work/calibre/src/calibre/gui2/metadata/bulk_download2.py:212
msgid "Some books changed"
msgstr ""
msgstr "Einige Bücher haben sich geändert"
#: /home/kovid/work/calibre/src/calibre/gui2/metadata/bulk_download2.py:213
msgid ""
@ -11801,16 +11804,23 @@ msgid ""
"Click \"Show details\" to see the list of changed books. Do you want to "
"proceed?"
msgstr ""
"Die Metadaten einiger Bücher in Ihrer Bibliothek haben sich geändert seit "
"der Download gestartet wurde. Wenn Sie fortfahren, können diese Änderungen "
"verloren gehen. Klicken Sie auf \"Zeige Details\", um eine Liste aller "
"geändertern Bücher zu sehen. Möchten Sie fortfahren?"
#: /home/kovid/work/calibre/src/calibre/gui2/metadata/bulk_download2.py:224
msgid "Metadata download completed"
msgstr ""
msgstr "Herunterladen der Metadaten abgeschlossen"
#: /home/kovid/work/calibre/src/calibre/gui2/metadata/bulk_download2.py:228
msgid ""
"Could not download metadata and/or covers for %d of the books. Click \"Show "
"details\" to see which books."
msgstr ""
"Für %d der Bücher konnten Metadaten und/oder Umschlagbilder nicht "
"heruntergeladen werden. Klicken Sie auf \"Zeige Details\", um betroffene "
"Bücher anzuzeigen."
#: /home/kovid/work/calibre/src/calibre/gui2/metadata/bulk_download2.py:234
msgid "(Failed metadata)"
@ -11825,30 +11835,32 @@ msgid ""
"Finished downloading metadata for <b>%d book(s)</b>. Proceed with updating "
"the metadata in your library?"
msgstr ""
"Erfolgreich Metadaten für <b>%d Buch/Bücher</b> heruntergeladen. Soll mit "
"dem Aktualisieren der Metadaten in Ihrer Bibliothek fortgefahren werden?"
#: /home/kovid/work/calibre/src/calibre/gui2/metadata/bulk_download2.py:240
msgid "Download complete"
msgstr ""
msgstr "Herunterladen abgeschlossen"
#: /home/kovid/work/calibre/src/calibre/gui2/metadata/bulk_download2.py:243
#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single_download.py:827
#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single_download.py:918
msgid "View log"
msgstr ""
msgstr "Log-Datei anschauen"
#: /home/kovid/work/calibre/src/calibre/gui2/metadata/bulk_download2.py:315
msgid "Downloaded %d of %d"
msgstr ""
msgstr "%d von %d heruntergeladen"
#: /home/kovid/work/calibre/src/calibre/gui2/metadata/config.py:58
#: /home/kovid/work/calibre/src/calibre/gui2/preferences/metadata_sources_ui.py:106
msgid "Downloaded metadata fields"
msgstr ""
msgstr "Metadaten-Felder heruntergeladen"
#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single.py:75
#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single.py:237
msgid "Edit Metadata"
msgstr ""
msgstr "Metadaten bearbeiten"
#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single.py:112
msgid "Set author sort from author"
@ -11860,7 +11872,7 @@ msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single.py:168
msgid "&Download metadata"
msgstr ""
msgstr "Metadaten &Herunterladen"
#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single.py:178
msgid "Change how calibre downloads metadata"
@ -11869,7 +11881,7 @@ msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single.py:508
#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single.py:697
msgid "Change cover"
msgstr ""
msgstr "Umschlag ändern"
#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single.py:557
msgid "Co&mments"
@ -13823,7 +13835,7 @@ msgstr "Suche (Zur erweiterten Suche die Schaltfläche links klicken)"
#: /home/kovid/work/calibre/src/calibre/gui2/search_box.py:369
msgid "Enable or disable search highlighting."
msgstr ""
msgstr "Hervorhebung von Suchergebnissen an- oder ausschalten"
#: /home/kovid/work/calibre/src/calibre/gui2/search_box.py:424
msgid "Saved Searches"
@ -13838,7 +13850,7 @@ msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/search_restriction_mixin.py:34
#: /home/kovid/work/calibre/src/calibre/gui2/search_restriction_mixin.py:42
msgid "*Current search"
msgstr ""
msgstr "*Aktuelle Suche"
#: /home/kovid/work/calibre/src/calibre/gui2/search_restriction_mixin.py:12
msgid "Restrict to"
@ -14104,19 +14116,19 @@ msgstr "Der gespeicherte Such-Name %s wird schon verwendet."
#: /home/kovid/work/calibre/src/calibre/gui2/tag_view.py:1834
msgid "Manage Authors"
msgstr ""
msgstr "Autoren verwalten"
#: /home/kovid/work/calibre/src/calibre/gui2/tag_view.py:1836
msgid "Manage Series"
msgstr ""
msgstr "Reihen verwalten"
#: /home/kovid/work/calibre/src/calibre/gui2/tag_view.py:1838
msgid "Manage Publishers"
msgstr ""
msgstr "Herausgeber verwalten"
#: /home/kovid/work/calibre/src/calibre/gui2/tag_view.py:1840
msgid "Manage Tags"
msgstr ""
msgstr "Etiketten verwalten"
#: /home/kovid/work/calibre/src/calibre/gui2/tag_view.py:1852
msgid "Invalid search restriction"
@ -14161,7 +14173,7 @@ msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/tag_view.py:2089
msgid "Find item in tag browser"
msgstr ""
msgstr "Eintrag im Etiketten-Browser finden"
#: /home/kovid/work/calibre/src/calibre/gui2/tag_view.py:2092
msgid ""
@ -14227,7 +14239,7 @@ msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/tag_view.py:2171
msgid "Manage authors, tags, etc"
msgstr ""
msgstr "Autoren, Etiketten, etc. verwalten"
#: /home/kovid/work/calibre/src/calibre/gui2/tag_view.py:2172
msgid ""

View File

@ -11,13 +11,13 @@ msgstr ""
"Project-Id-Version: es\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2011-04-17 16:54+0000\n"
"PO-Revision-Date: 2011-04-18 09:40+0000\n"
"PO-Revision-Date: 2011-04-21 15:39+0000\n"
"Last-Translator: Jellby <Unknown>\n"
"Language-Team: Spanish\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2011-04-19 04:39+0000\n"
"X-Launchpad-Export-Date: 2011-04-22 04:37+0000\n"
"X-Generator: Launchpad (build 12758)\n"
#: /home/kovid/work/calibre/src/calibre/customize/profiles.py:528
@ -18700,6 +18700,60 @@ msgid ""
"sony_collection_renaming_rules={'series':'Series', 'tags':'Tag'}\n"
"sony_collection_name_template='{category:||: }{value}'"
msgstr ""
"Especifica reglas para cambiar el nombre a las colecciones Sony. Este\n"
"ajuste sólo se aplica si la gestión de metadatos está establecida en\n"
"automática. Las colecciones en los lectores Sony se nombran según el campo\n"
"en que se basan sea predefinido o personalizado. Una colección derivada de\n"
"un campo predefinido se nombra con el valor contenido en dicho campo. Por\n"
"ejemplo, si el campo predefinido «series» contiene el valor «Darkover»,\n"
"entonces el nombre de la colección será «Darkover». Una colección derivada\n"
"de un campo personalizado tendrá el nombre del campo añadido al valor. Por\n"
"ejemplo, si un campo personalizado llamado «Mi serie» contiene el valor\n"
"«Darkover», entonces la colección se nombrará de manera predeterminada\n"
"«Darkover (Mi serie)». En lo que a esta documentación respecta, «Darkover»\n"
"se llamará el valor y «Mi serie» se llamará la categoría. Si dos libros\n"
"tienen campos que generan el mismo nombre de colección, ambos se incluirán\n"
"en la misma colección.\n"
"Este conjunto de ajustes le permite especificar, para un campo predefinido\n"
"o personalizado, cómo se nombrarán las colecciones. Puede usarlo para\n"
"añadir una descripción a una etiqueta, por ejemplo «Bla (etiqueta)» en\n"
"lugar de «Bla». También puede usarlo para hacer que distintos campos acaben\n"
"en la misma colección. Por ejemplo, puede hacer que los valores en\n"
"«series», «#mi_serie_1» y «#mi_serie_2» aparezcan en colecciones llamadas\n"
"«algún_valor (Serie)», uniendo todos los campos en un mismo grupo de\n"
"colecciones.\n"
"Hay dos ajustes relacionados. El primero determina el nombre de categoría\n"
"que se usará para un campo de metadatos. El segundo es una plantilla y se\n"
"usa para determinar cómo se combinarán el valor y la categoría para crear\n"
"el nombre de colección. La sintaxis del primer ajuste,\n"
"«sony_collection_renaming_rules», es:\n"
"{«nombre_de_búsqueda_del_campo»:«nombre_de_categoría»,\n"
"«nombre_de_búsqueda»:«categoría», ...}. El segundo ajuste,\n"
"«sony_collection_name_template», es una plantilla. Emplea el mismo lenguaje\n"
"de plantilla que los controles de metadatos y las plantillas de guardado.\n"
"Los únicos dos campos disponibles son «{category}» y «{value}». El campo\n"
"«{value}» nunca está vacío. El campo «{category}» puede estar vacío. El\n"
"comportamiento predeterminado es poner primero el valor y después la\n"
"categoría entre paréntesis si no está vacía: «{value} {category:|(|)}».\n"
"Ejemplos: Los dos primeros ejemplos suponen que el segundo ajuste no se ha\n"
"cambiado.\n"
"1: Quiero unir tres campos de serie en un solo conjunto de colecciones. Los\n"
"nombres de búsqueda de los campos son «series», «#serie_1» y «#serie_2». No\n"
"quiero nada entre paréntesis. El valor que se usaría para el ajuste es:\n"
"sony_collection_renaming_rules={'series':'', '#serie_1':'', '#serie_2':''}\n"
"2: Quiero que la palabra «(Serie)» aparezca en las colecciones creadas a\n"
"partir de series y la palabra «(Etiqueta)» en las creadas a partir de\n"
"etiquetas. Se usaría:\n"
"sony_collection_renaming_rules={'series':'Serie', 'tags':'Etiqueta'}\n"
"3: Quiero unir «series» y «#miserie» y añadir «(Serie)» al nombre de la\n"
"colección. Se usaría:\n"
"sony_collection_renaming_rules={'series':'Serie', '#myserie':'Serie'}\n"
"4: Como en el ejemplo 2, pero en lugar de añadir el nombre de categoría\n"
"entre paréntesis después del valor, lo quiero delante y separado por dos\n"
"puntos, como en «Serie: Darkover». Tengo que cambiar la plantilla usada\n"
"para dar formato al nombre de colección. Los ajustes resultantes son:\n"
"sony_collection_renaming_rules={'series':'Serie', 'tags':'Etiqueta'}\n"
"sony_collection_name_template='{category:||: }{value}'"
#: /home/kovid/work/calibre/resources/default_tweaks.py:221
msgid "Specify how SONY collections are sorted"
@ -18731,6 +18785,28 @@ msgid ""
"[ ( [list of fields], sort field ) , ( [ list of fields ] , sort field ) ]\n"
"Default: empty (no rules), so no collection attributes are named."
msgstr ""
"Especifica cómo se ordenan las colecciones Sony. Este ajuste sólo se\n"
"aplica si la gestión de metadatos está establecida en automática. Puede\n"
"definir qué campo de metadatos se usara para ordenar cada colección.\n"
"El formato del ajuste es una lista de campos de metadatos a partir de los\n"
"cuales se construyen las colecciones, seguida del nombre del campo que\n"
"contiene el valor de orden.\n"
"Ejemplo: Lo siguiente indica que las colecciones creadas a partir de "
"«pubdate»\n"
"y «tags» se ordenarán según el valor contenido en la columna personalizada\n"
"«#mifecha», que las colecciones creadas a partir de «series» se ordenan\n"
"según «series_index» y que el resto de las colecciones se ordenan por\n"
"título. Si un campo de colecciones no se nombra, entonces la colección\n"
"se ordenará por índice de serie si está basada en series y por título\n"
"en caso contrario.\n"
"[(['pubdate', 'tags'],'#mydate'), (['series'],'series_index'), (['*'], "
"'title')]\n"
"Tenga en cuenta que los corchetes y paréntesis son necesarios.\n"
"La sintaxis es:\n"
"[ ( [lista de campos], campo de orden ) , ( [ lista de campos ] , campo de "
"orden ) ]\n"
"Valor predeterminado: vacío (sin reglas), con lo que no se nombra\n"
"ningún atributo de colecciones."
#: /home/kovid/work/calibre/resources/default_tweaks.py:240
msgid "Control how tags are applied when copying books to another library"
@ -18777,6 +18853,22 @@ msgid ""
"content_server_will_display = ['*']\n"
"content_server_wont_display['#mycomments']"
msgstr ""
"«content_server_will_display» es una lista de campos personalizados que\n"
"se mostrarán, «content_server_wont_display» es una lista de campos\n"
"personalizados que no se mostrarán. El segundo tiene preferencia sobre\n"
"el primero.\n"
"El valor especial «*» indica todos los campos personalizados. El valor «[]»\n"
"indica que no hay entradas.\n"
"Valores predeterminados:\n"
"content_server_will_display = ['*']\n"
"content_server_wont_display = []\n"
"Ejemplos:\n"
"Para mostrar sólo los campos personalizados «#misetiquetas» y «#genero»:\n"
"content_server_will_display = ['#misetiquetas', '#genero']\n"
"content_server_wont_display = []\n"
"Para mostrar todos los campos excepto «#miscomentarios»:\n"
"content_server_will_display = ['*']\n"
"content_server_wont_display = ['#mycomments']"
#: /home/kovid/work/calibre/resources/default_tweaks.py:268
msgid ""
@ -18806,6 +18898,24 @@ msgid ""
"As above, this tweak affects only display of custom fields. The standard\n"
"fields are not affected"
msgstr ""
"«book_details_will_display» es una lista de campos personalizados que\n"
"se mostrarán, «book_details_wont_display» es una lista de campos\n"
"personalizados que no se mostrarán. El segundo tiene preferencia sobre\n"
"el primero.\n"
"El valor especial «*» indica todos los campos personalizados. El valor «[]»\n"
"indica que no hay entradas.\n"
"Valores predeterminados:\n"
"book_details_will_display = ['*']\n"
"book_details_wont_display = []\n"
"Ejemplos:\n"
"Para mostrar sólo los campos personalizados «#misetiquetas» y «#genero»:\n"
"book_details_will_display = ['#misetiquetas', '#genero']\n"
"book_details_wont_display = []\n"
"Para mostrar todos los campos excepto «#miscomentarios»:\n"
"book_details_will_display = ['*']\n"
"book_details_wont_display = ['#mycomments']\n"
"Este ajuste sólo afecta a los campos personalizados. Los campos\n"
"predefinidos no se ven afectados."
#: /home/kovid/work/calibre/resources/default_tweaks.py:288
msgid "Set the maximum number of sort 'levels'"

View File

@ -8,13 +8,13 @@ msgstr ""
"Project-Id-Version: calibre\n"
"Report-Msgid-Bugs-To: FULL NAME <EMAIL@ADDRESS>\n"
"POT-Creation-Date: 2011-04-17 16:54+0000\n"
"PO-Revision-Date: 2011-04-20 09:22+0000\n"
"PO-Revision-Date: 2011-04-21 07:36+0000\n"
"Last-Translator: Anca Stratulat <Unknown>\n"
"Language-Team: Romanian <ro@li.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2011-04-21 04:37+0000\n"
"X-Launchpad-Export-Date: 2011-04-22 04:36+0000\n"
"X-Generator: Launchpad (build 12758)\n"
#: /home/kovid/work/calibre/src/calibre/customize/__init__.py:56
@ -2427,12 +2427,17 @@ msgid ""
"Left aligned scene break markers are center aligned. Replace soft scene "
"breaks that use multiple blank lines withhorizontal rules."
msgstr ""
"Marker-ii separatorilor de text aliniaţi la stanga sunt aliniaţi central. "
"Înlocuiţi separatorii fini ai textului care utilizează multiple linii goale "
"cu reguli orizontale."
#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:547
msgid ""
"Replace scene breaks with the specified text. By default, the text from the "
"input document is used."
msgstr ""
"Înlocuieşte separatorii textului cu textul specificat. Împlicit, textul din "
"documentul iniţial este utilizat."
#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:552
msgid ""
@ -2440,20 +2445,25 @@ msgid ""
"used as a dictionary to determine whether hyphens should be retained or "
"removed."
msgstr ""
"Analizaţi cuvintele despărţite în silabe din întregul document. Documentul "
"în sine este folosit ca un dicţionar pentru a determina dacă cratimele ar "
"trebui să fie păstrate sau eliminate."
#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:558
msgid ""
"Looks for occurrences of sequential <h1> or <h2> tags. The tags are "
"renumbered to prevent splitting in the middle of chapter headings."
msgstr ""
"Caută evenimente secvenţale ale etichetelor <h1> sau <h2>. Etichetele sunt "
"renumerotate pentru a preveni scindarea în mijlocul capitolului."
#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:564
msgid "Search pattern (regular expression) to be replaced with sr1-replace."
msgstr ""
msgstr "Model de cautare (expresie comună) înlocuit cu sr1."
#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:569
msgid "Replacement to replace the text found with sr1-search."
msgstr ""
msgstr "Înlocuieste textul găsit cu o căutare sr1."
#: /home/kovid/work/calibre/src/calibre/ebooks/conversion/plumber.py:573
msgid "Search pattern (regular expression) to be replaced with sr2-replace."