mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-08 10:44:09 -04:00
merge from trunk
This commit is contained in:
commit
eb73df9609
@ -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
|
||||
|
||||
|
@ -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>"
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -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+'"}'
|
||||
|
||||
|
@ -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
|
||||
|
||||
# }}}
|
||||
|
||||
|
@ -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_()
|
||||
|
||||
|
@ -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([])
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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'])
|
||||
|
@ -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):
|
||||
'''
|
||||
|
@ -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
|
||||
|
@ -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):
|
||||
|
@ -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):
|
||||
|
@ -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):
|
||||
|
@ -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):
|
||||
|
@ -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):
|
||||
|
@ -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):
|
||||
|
@ -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):
|
||||
|
@ -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):
|
||||
|
@ -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):
|
||||
|
@ -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)
|
||||
|
@ -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):
|
||||
|
@ -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
|
0
src/calibre/gui2/store/search/__init__.py
Normal file
0
src/calibre/gui2/store/search/__init__.py
Normal file
190
src/calibre/gui2/store/search/download_thread.py
Normal file
190
src/calibre/gui2/store/search/download_thread.py
Normal 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
|
337
src/calibre/gui2/store/search/models.py
Normal file
337
src/calibre/gui2/store/search/models.py
Normal 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
|
26
src/calibre/gui2/store/search/results_view.py
Normal file
26
src/calibre/gui2/store/search/results_view.py
Normal 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)
|
||||
|
232
src/calibre/gui2/store/search/search.py
Normal file
232
src/calibre/gui2/store/search/search.py
Normal 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()
|
||||
|
@ -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>
|
@ -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):
|
||||
|
@ -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:
|
||||
|
@ -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 s’aplica si s’estableix la gestió de metadades a automàtica. Les "
|
||||
"col·leccions\n"
|
||||
"Sony s’anomenen depenent de si el camp és estàndard o personalitzat. Una\n"
|
||||
"col·lecció derivada d’un 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 d’un 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 d’ajustaments us permet especificar com s’anomenaran 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"
|
||||
"s’utilitza per a un camp de metadades. El segon és una plantilla que "
|
||||
"s’utilitza 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 s’ha 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 l’ajustament 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"
|
||||
"d’etiquetes. 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 s’afegeixi 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 l’exemple 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». S’ha 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
@ -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 ""
|
||||
|
@ -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'"
|
||||
|
@ -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."
|
||||
|
Loading…
x
Reference in New Issue
Block a user