merge from trunk

This commit is contained in:
Lee 2011-04-22 12:26:38 +08:00
commit bce7efa49e
113 changed files with 67545 additions and 41772 deletions

59
recipes/babyonline.recipe Normal file
View File

@ -0,0 +1,59 @@
# -*- coding: utf-8 -*-
#!/usr/bin/env python
__license__ = 'GPL v3'
__copyright__ = u'2011, Silviu Cotoar\u0103'
'''
babyonline.ro
'''
from calibre.web.feeds.news import BasicNewsRecipe
class BabyOnline(BasicNewsRecipe):
title = u'Baby Online'
__author__ = u'Silviu Cotoar\u0103'
description = u'De la p\u0103rinte la p\u0103rinte'
publisher = u'Baby Online'
oldest_article = 50
language = 'ro'
max_articles_per_feed = 100
no_stylesheets = True
use_embedded_content = False
category = 'Ziare,Reviste,Copii,Mame'
encoding = 'utf-8'
cover_url = 'http://www.babyonline.ro/images/default/logo.gif'
conversion_options = {
'comments' : description
,'tags' : category
,'language' : language
,'publisher' : publisher
}
keep_only_tags = [
dict(name='div', attrs={'id':'article_container'})
]
remove_tags = [
dict(name='div', attrs={'id':'bar_nav'}),
dict(name='div', attrs={'id':'service_send'}),
dict(name='div', attrs={'id':'other_videos'}),
dict(name='div', attrs={'class':'dot_line_yellow'}),
dict(name='a', attrs={'class':'print'}),
dict(name='a', attrs={'class':'email'}),
dict(name='a', attrs={'class':'YM'}),
dict(name='a', attrs={'class':'comment'}),
dict(name='div', attrs={'class':'tombstone_cross'}),
dict(name='span', attrs={'class':'liketext'})
]
remove_tags_after = [
dict(name='div', attrs={'id':'service_send'})
]
feeds = [
(u'Feeds', u'http://www.babyonline.ro/rss_homepage.xml')
]
def preprocess_html(self, soup):
return self.adeify_images(soup)

View File

@ -61,6 +61,12 @@ class DailyTelegraph(BasicNewsRecipe):
(u'Entertainment News', u'http://feeds.news.com.au/public/rss/2.0/dtele_entertainment_news_201.xml'),
(u'Lifestyle News', u'http://feeds.news.com.au/public/rss/2.0/dtele_lifestyle_227.xml'),
(u'Music', u'http://feeds.news.com.au/public/rss/2.0/dtele_music_441.xml'),
(u'Sport',
u'http://feeds.news.com.au/public/rss/2.0/dtele_sport_203.xml'),
(u'Soccer',
u'http://feeds.news.com.au/public/rss/2.0/dtele_sports_soccer_344.xml'),
(u'Rugby Union',
u'http://feeds.news.com.au/public/rss/2.0/dtele_sports_rugby_union_342.xml'),
(u'Property Confidential', u'http://feeds.news.com.au/public/rss/2.0/dtele_property_confidential_463.xml'),
(u'Property - Your Space', u'http://feeds.news.com.au/public/rss/2.0/dtele_property_yourspace_462.xml'),
(u'Confidential News', u'http://feeds.news.com.au/public/rss/2.0/dtele_entertainment_confidential_252.xml'),

View File

@ -14,14 +14,14 @@ class EcuisineRo(BasicNewsRecipe):
__author__ = u'Silviu Cotoar\u0103'
description = u'Reinventeaz\u0103 pl\u0103cerea de a g\u0103ti'
publisher = 'eCuisine'
oldest_article = 5
oldest_article = 50
language = 'ro'
max_articles_per_feed = 100
no_stylesheets = True
use_embedded_content = False
category = 'Ziare,Retete,Bucatarie'
encoding = 'utf-8'
cover_url = ''
cover_url = 'http://www.ecuisine.ro/sites/all/themes/ecuisine/images/logo.gif'
conversion_options = {
'comments' : description
@ -31,8 +31,8 @@ class EcuisineRo(BasicNewsRecipe):
}
keep_only_tags = [
dict(name='div', attrs={'class':'page-title'})
, dict(name='div', attrs={'class':'content clearfix'})
dict(name='h1', attrs={'id':'page-title'})
, dict(name='div', attrs={'class':'field-item even'})
]
remove_tags = [

View File

@ -31,8 +31,8 @@ class EgirlRo(BasicNewsRecipe):
}
keep_only_tags = [
dict(name='div', attrs={'id':'title_art'})
, dict(name='div', attrs={'class':'content_style'})
dict(name='div', attrs={'id':'content_art'})
, dict(name='div', attrs={'class':'content_articol'})
]
feeds = [

Binary file not shown.

After

Width:  |  Height:  |  Size: 256 B

View File

@ -37,10 +37,12 @@ class TabuRo(BasicNewsRecipe):
]
remove_tags = [
dict(name='div', attrs={'class':'asemanatoare'})
dict(name='div', attrs={'class':'asemanatoare'}),
dict(name='div', attrs={'class':'social'})
]
remove_tags_after = [
dict(name='div', attrs={'class':'social'}),
dict(name='div', attrs={'id':'comments'}),
dict(name='div', attrs={'class':'asemanatoare'})
]

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 705 B

View File

@ -15,9 +15,9 @@ from setup import prints, get_warnings
def check_version_info():
vi = sys.version_info
if vi[0] == 2 and vi[1] > 5:
if vi[0] == 2 and vi[1] > 6:
return None
return 'calibre requires python >= 2.6'
return 'calibre requires python >= 2.7 and < 3'
def option_parser():
parser = optparse.OptionParser()

View File

@ -45,6 +45,7 @@ fcntl = None if iswindows else importlib.import_module('fcntl')
filesystem_encoding = sys.getfilesystemencoding()
if filesystem_encoding is None: filesystem_encoding = 'utf-8'
DEBUG = False
def debug():

View File

@ -22,6 +22,11 @@ from calibre.utils.config import make_config_dir, Config, ConfigProxy, \
from calibre.ebooks.epub.fix import ePubFixer
from calibre.ebooks.metadata.sources.base import Source
builtin_names = frozenset([p.name for p in builtin_plugins])
class NameConflict(ValueError):
pass
def _config():
c = Config('customize')
c.add_opt('plugins', default={}, help=_('Installed plugins'))
@ -355,6 +360,9 @@ def set_file_type_metadata(stream, mi, ftype):
def add_plugin(path_to_zip_file):
make_config_dir()
plugin = load_plugin(path_to_zip_file)
if plugin.name in builtin_names:
raise NameConflict(
'A builtin plugin with the name %r already exists' % plugin.name)
plugin = initialize_plugin(plugin, path_to_zip_file)
plugins = config['plugins']
zfp = os.path.join(plugin_dir, plugin.name+'.zip')
@ -506,7 +514,11 @@ def initialize_plugin(plugin, path_to_zip_file):
def initialize_plugins():
global _initialized_plugins
_initialized_plugins = []
for zfp in list(config['plugins'].values()) + builtin_plugins:
conflicts = [name for name in config['plugins'] if name in
builtin_names]
for p in conflicts:
remove_plugin(p)
for zfp in list(config['plugins'].itervalues()) + builtin_plugins:
try:
try:
plugin = load_plugin(zfp) if not isinstance(zfp, type) else zfp

View File

@ -164,7 +164,7 @@ class APNXBuilder(object):
if c == '/':
closing = True
continue
elif c == 'p':
elif c in ('d', 'p'):
if closing:
in_p = False
else:

View File

@ -7,7 +7,7 @@ Code for the conversion of ebook formats and the reading of metadata
from various formats.
'''
import traceback, os
import traceback, os, re
from calibre import CurrentDir
class ConversionError(Exception):
@ -169,3 +169,42 @@ def calibre_cover(title, author_string, series_string=None,
lines.append(TextLine(series_string, author_size))
return create_cover_page(lines, I('library.png'), output_format='jpg')
UNIT_RE = re.compile(r'^(-*[0-9]*[.]?[0-9]*)\s*(%|em|ex|en|px|mm|cm|in|pt|pc)$')
def unit_convert(value, base, font, dpi):
' Return value in pts'
if isinstance(value, (int, long, float)):
return value
try:
return float(value) * 72.0 / dpi
except:
pass
result = value
m = UNIT_RE.match(value)
if m is not None and m.group(1):
value = float(m.group(1))
unit = m.group(2)
if unit == '%':
result = (value / 100.0) * base
elif unit == 'px':
result = value * 72.0 / dpi
elif unit == 'in':
result = value * 72.0
elif unit == 'pt':
result = value
elif unit == 'em':
result = value * font
elif unit in ('ex', 'en'):
# This is a hack for ex since we have no way to know
# the x-height of the font
font = font
result = value * font * 0.5
elif unit == 'pc':
result = value * 12.0
elif unit == 'mm':
result = value * 0.04
elif unit == 'cm':
result = value * 0.40
return result

View File

@ -25,6 +25,7 @@ msprefs.defaults['max_tags'] = 20
msprefs.defaults['wait_after_first_identify_result'] = 30 # seconds
msprefs.defaults['wait_after_first_cover_result'] = 60 # seconds
msprefs.defaults['swap_author_names'] = False
msprefs.defaults['fewer_tags'] = True
# Google covers are often poor quality (scans/errors) but they have high
# resolution, so they trump covers from better sources. So make sure they

View File

@ -216,7 +216,7 @@ class ISBNMerge(object):
# We assume the smallest set of tags has the least cruft in it
ans.tags = self.length_merge('tags', results,
null_value=ans.tags)
null_value=ans.tags, shortest=msprefs['fewer_tags'])
# We assume the longest series has the most info in it
ans.series = self.length_merge('series', results,

View File

@ -40,24 +40,17 @@ class OverDrive(Source):
supports_gzip_transfer_encoding = False
cached_cover_url_is_reliable = True
def __init__(self, *args, **kwargs):
Source.__init__(self, *args, **kwargs)
options = (
Option('get_full_metadata', 'bool', None, _('Gather all Metadata:'),
Option('get_full_metadata', 'bool', False,
_('Download all metadata (slow)'),
_('Enable this option to gather all metadata available from Overdrive.')),
)
config_help_message = '<p>'+_('Additional metadata can be taken from Overdrive\'s book detail'
' page. This includes a limited set of tags used by libraries, comments, language,'
' and the ebook ISBN. Collecting this data is disabled by default due to the extra'
' time required.')
def __init__(self, *args, **kwargs):
Source.__init__(self, *args, **kwargs)
prefs = self.prefs
prefs.defaults['get_full_metadata'] = False
' time required. Check the download all metadata option below to'
' enable downloading this data.')
def identify(self, log, result_queue, abort, title=None, authors=None, # {{{
identifiers={}, timeout=30):

View File

@ -20,7 +20,7 @@ from calibre.utils.filenames import ascii_filename
from calibre.utils.date import parse_date
from calibre.utils.cleantext import clean_ascii_chars
from calibre.ptempfile import TemporaryDirectory
from calibre.ebooks import DRMError
from calibre.ebooks import DRMError, unit_convert
from calibre.ebooks.chardet import ENCODING_PATS
from calibre.ebooks.mobi import MobiError
from calibre.ebooks.mobi.huffcdic import HuffReader
@ -258,6 +258,8 @@ class MobiReader(object):
}
''')
self.tag_css_rules = {}
self.left_margins = {}
self.text_indents = {}
if hasattr(filename_or_stream, 'read'):
stream = filename_or_stream
@ -567,9 +569,21 @@ class MobiReader(object):
elif tag.tag == 'img':
tag.set('width', width)
else:
styles.append('text-indent: %s' % self.ensure_unit(width))
ewidth = self.ensure_unit(width)
styles.append('text-indent: %s' % ewidth)
try:
ewidth_val = unit_convert(ewidth, 12, 500, 166)
self.text_indents[tag] = ewidth_val
except:
pass
if width.startswith('-'):
styles.append('margin-left: %s' % self.ensure_unit(width[1:]))
try:
ewidth_val = unit_convert(ewidth[1:], 12, 500, 166)
self.left_margins[tag] = ewidth_val
except:
pass
if attrib.has_key('align'):
align = attrib.pop('align').strip()
if align:
@ -661,6 +675,26 @@ class MobiReader(object):
if hasattr(parent, 'remove'):
parent.remove(tag)
def get_left_whitespace(self, tag):
def whitespace(tag):
lm = ti = 0.0
if tag.tag == 'p':
ti = unit_convert('1.5em', 12, 500, 166)
if tag.tag == 'blockquote':
lm = unit_convert('2em', 12, 500, 166)
lm = self.left_margins.get(tag, lm)
ti = self.text_indents.get(tag, ti)
return lm + ti
parent = tag
ans = 0.0
while parent is not None:
ans += whitespace(parent)
parent = parent.getparent()
return ans
def create_opf(self, htmlfile, guide=None, root=None):
mi = getattr(self.book_header.exth, 'mi', self.embedded_mi)
if mi is None:
@ -731,16 +765,45 @@ class MobiReader(object):
except:
text = ''
text = ent_pat.sub(entity_to_unicode, text)
tocobj.add_item(toc.partition('#')[0], href[1:],
item = tocobj.add_item(toc.partition('#')[0], href[1:],
text)
item.left_space = int(self.get_left_whitespace(x))
found = True
if reached and found and x.get('class', None) == 'mbp_pagebreak':
break
if tocobj is not None:
tocobj = self.structure_toc(tocobj)
opf.set_toc(tocobj)
return opf, ncx_manifest_entry
def structure_toc(self, toc):
indent_vals = set()
for item in toc:
indent_vals.add(item.left_space)
if len(indent_vals) > 6 or len(indent_vals) < 2:
# Too many or too few levels, give up
return toc
indent_vals = sorted(indent_vals)
last_found = [None for i in indent_vals]
newtoc = TOC()
def find_parent(level):
candidates = last_found[:level]
for x in reversed(candidates):
if x is not None:
return x
return newtoc
for item in toc:
level = indent_vals.index(item.left_space)
parent = find_parent(level)
last_found[level] = parent.add_item(item.href, item.fragment,
item.text)
return newtoc
def sizeof_trailing_entries(self, data):
def sizeof_trailing_entry(ptr, psize):

View File

@ -18,6 +18,7 @@ from cssutils import profile as cssprofiles
from lxml import etree
from lxml.cssselect import css_to_xpath, ExpressionError, SelectorSyntaxError
from calibre import force_unicode
from calibre.ebooks import unit_convert
from calibre.ebooks.oeb.base import XHTML, XHTML_NS, CSS_MIME, OEB_STYLES
from calibre.ebooks.oeb.base import XPNSMAP, xpath, urlnormalize
from calibre.ebooks.oeb.profile import PROFILES
@ -444,7 +445,6 @@ class Stylizer(object):
class Style(object):
UNIT_RE = re.compile(r'^(-*[0-9]*[.]?[0-9]*)\s*(%|em|ex|en|px|mm|cm|in|pt|pc)$')
MS_PAT = re.compile(r'^\s*(mso-|panose-|text-underline|tab-interval)')
def __init__(self, element, stylizer):
@ -507,43 +507,11 @@ class Style(object):
return result
def _unit_convert(self, value, base=None, font=None):
' Return value in pts'
if isinstance(value, (int, long, float)):
return value
try:
return float(value) * 72.0 / self._profile.dpi
except:
pass
result = value
m = self.UNIT_RE.match(value)
if m is not None and m.group(1):
value = float(m.group(1))
unit = m.group(2)
if unit == '%':
'Return value in pts'
if base is None:
base = self.width
result = (value / 100.0) * base
elif unit == 'px':
result = value * 72.0 / self._profile.dpi
elif unit == 'in':
result = value * 72.0
elif unit == 'pt':
result = value
elif unit == 'em':
font = font or self.fontSize
result = value * font
elif unit in ('ex', 'en'):
# This is a hack for ex since we have no way to know
# the x-height of the font
font = font or self.fontSize
result = value * font * 0.5
elif unit == 'pc':
result = value * 12.0
elif unit == 'mm':
result = value * 0.04
elif unit == 'cm':
result = value * 0.40
return result
return unit_convert(value, base, font, self._profile.dpi)
def pt_to_px(self, value):
return (self._profile.dpi / 72.0) * value

View File

@ -11,6 +11,7 @@ from functools import partial
from PyQt4.Qt import QMenu
from calibre.gui2.actions import InterfaceAction
from calibre.gui2.dialogs.confirm_delete import confirm
class StoreAction(InterfaceAction):
@ -31,9 +32,35 @@ class StoreAction(InterfaceAction):
self.qaction.setMenu(self.store_menu)
def search(self):
self.show_disclaimer()
from calibre.gui2.store.search import SearchDialog
sd = SearchDialog(self.gui.istores, self.gui)
sd.exec_()
def open_store(self, store_plugin):
self.show_disclaimer()
store_plugin.open(self.gui)
def show_disclaimer(self):
confirm(('<p>' +
_('Calibre helps you find the ebooks you want by searching '
'the websites of various commercial and public domain '
'book sources for you.') +
'<p>' +
_('Using the integrated search you can easily find which '
'store has the book you are looking for, at the best price. '
'You also get DRM status and other useful information.')
+ '<p>' +
_('All transactions (paid or otherwise) are handled between '
'you and the book seller. '
'Calibre is not part of this process and any issues related '
'to a purchase should be directed to the website you are '
'buying from. Be sure to double check that any books you get '
'will work with your e-book reader, especially if the book you '
'are buying has '
'<a href="http://drmfree.calibre-ebook.com/about#drm">DRM</a>.'
)), 'about_get_books_msg',
parent=self.gui, show_cancel_button=False,
confirm_msg=_('Show this message again'),
pixmap='dialog_information.png', title=_('About Get Books'))

View File

@ -418,6 +418,7 @@ class BookDetails(QWidget): # {{{
if y is None:
# Local image
self.cover_view.paste_from_clipboard(x)
self.update_layout()
else:
self.remote_file_dropped.emit(x, y)
# We do not support setting cover *and* adding formats for
@ -449,6 +450,7 @@ class BookDetails(QWidget): # {{{
self.setAcceptDrops(True)
self._layout = DetailsLayout(vertical, self)
self.setLayout(self._layout)
self.current_path = ''
self.cover_view = CoverView(vertical, self)
self.cover_view.cover_changed.connect(self.cover_changed.emit)
@ -482,6 +484,10 @@ class BookDetails(QWidget): # {{{
def show_data(self, data):
self.book_info.show_data(data)
self.cover_view.show_data(data)
self.current_path = data.get(_('Path'), '')
self.update_layout()
def update_layout(self):
self._layout.do_layout(self.rect())
try:
sz = self.cover_view.pixmap.size()
@ -489,7 +495,7 @@ class BookDetails(QWidget): # {{{
sz = QSize(0, 0)
self.setToolTip(
'<p>'+_('Double-click to open Book Details window') +
'<br><br>' + _('Path') + ': ' + data.get(_('Path'), '') +
'<br><br>' + _('Path') + ': ' + self.current_path +
'<br><br>' + _('Cover size: %dx%d')%(sz.width(), sz.height())
)

View File

@ -24,11 +24,18 @@ class Dialog(QDialog, Ui_Dialog):
dynamic[confirm_config_name(self.name)] = self.again.isChecked()
def confirm(msg, name, parent=None, pixmap='dialog_warning.png'):
def confirm(msg, name, parent=None, pixmap='dialog_warning.png', title=None,
show_cancel_button=True, confirm_msg=None):
if not dynamic.get(confirm_config_name(name), True):
return True
d = Dialog(msg, name, parent)
d.label.setPixmap(QPixmap(I(pixmap)))
d.setWindowIcon(QIcon(I(pixmap)))
if title is not None:
d.setWindowTitle(title)
if not show_cancel_button:
d.buttonBox.button(d.buttonBox.Cancel).setVisible(False)
if confirm_msg is not None:
d.again.setText(confirm_msg)
d.resize(d.sizeHint())
return d.exec_() == d.Accepted

View File

@ -24,7 +24,7 @@ from calibre.ebooks.metadata.meta import get_metadata
from calibre.gui2 import file_icon_provider, UNDEFINED_QDATE, UNDEFINED_DATE, \
choose_files, error_dialog, choose_images, question_dialog
from calibre.utils.date import local_tz, qt_to_dt
from calibre import strftime, fit_image
from calibre import strftime
from calibre.ebooks import BOOK_EXTENSIONS
from calibre.customize.ui import run_plugins_on_import
from calibre.utils.date import utcfromtimestamp
@ -672,12 +672,7 @@ class Cover(ImageView): # {{{
self.frame_size = (sz.width()//3, sz.height())
def sizeHint(self):
sz = ImageView.sizeHint(self)
w, h = sz.width(), sz.height()
resized, nw, nh = fit_image(w, h, self.frame_size[0],
self.frame_size[1])
if resized:
sz = QSize(nw, nh)
sz = QSize(self.frame_size[0], self.frame_size[1])
return sz
def select_cover(self, *args):

View File

@ -295,7 +295,7 @@ def proceed(gui, job):
_('Failed to download metadata or covers for any of the %d'
' book(s).') % len(id_map), det_msg=det_msg)
else:
fmsg = det_msg = ''
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)

View File

@ -259,6 +259,7 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
r('wait_after_first_identify_result', msprefs)
r('wait_after_first_cover_result', msprefs)
r('swap_author_names', msprefs)
r('fewer_tags', msprefs)
self.configure_plugin_button.clicked.connect(self.configure_plugin)
self.sources_model = SourcesModel(self)

View File

@ -7,7 +7,7 @@
<x>0</x>
<y>0</y>
<width>781</width>
<height>300</height>
<height>394</height>
</rect>
</property>
<property name="windowTitle">
@ -21,7 +21,7 @@
<widget class="QStackedWidget" name="stack">
<widget class="QWidget" name="page">
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0" rowspan="6">
<item row="0" column="0" rowspan="7">
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>Metadata sources</string>
@ -105,7 +105,7 @@
</property>
</widget>
</item>
<item row="3" column="1">
<item row="4" column="1">
<widget class="QLabel" name="label_2">
<property name="text">
<string>Max. number of &amp;tags to download:</string>
@ -115,10 +115,10 @@
</property>
</widget>
</item>
<item row="3" column="2">
<item row="4" column="2">
<widget class="QSpinBox" name="opt_max_tags"/>
</item>
<item row="4" column="1">
<item row="5" column="1">
<widget class="QLabel" name="label_3">
<property name="text">
<string>Max. &amp;time to wait after first match is found:</string>
@ -128,14 +128,14 @@
</property>
</widget>
</item>
<item row="4" column="2">
<item row="5" column="2">
<widget class="QSpinBox" name="opt_wait_after_first_identify_result">
<property name="suffix">
<string> secs</string>
</property>
</widget>
</item>
<item row="5" column="1">
<item row="6" column="1">
<widget class="QLabel" name="label_4">
<property name="text">
<string>Max. time to wait after first &amp;cover is found:</string>
@ -145,13 +145,24 @@
</property>
</widget>
</item>
<item row="5" column="2">
<item row="6" column="2">
<widget class="QSpinBox" name="opt_wait_after_first_cover_result">
<property name="suffix">
<string> secs</string>
</property>
</widget>
</item>
<item row="3" column="1" colspan="2">
<widget class="QCheckBox" name="opt_fewer_tags">
<property name="toolTip">
<string>&lt;p&gt;Different metadata sources have different sets of tags for the same book. If this option is checked, then calibre will use the smaller tag sets. These tend to be more like genres, while the larger tag sets tend to describe the books content.
&lt;p&gt;Note that this option will only make a practical difference if one of the metadata sources has a genre like tag set for the book you are searching for. Most often, they all have large tag sets.</string>
</property>
<property name="text">
<string>Prefer &amp;fewer tags</string>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="page_2"/>

View File

@ -13,9 +13,9 @@ from PyQt4.Qt import Qt, QModelIndex, QAbstractItemModel, QVariant, QIcon, \
from calibre.gui2.preferences import ConfigWidgetBase, test_widget
from calibre.gui2.preferences.plugins_ui import Ui_Form
from calibre.customize.ui import initialized_plugins, is_disabled, enable_plugin, \
disable_plugin, plugin_customization, add_plugin, \
remove_plugin
from calibre.customize.ui import (initialized_plugins, is_disabled, enable_plugin,
disable_plugin, plugin_customization, add_plugin,
remove_plugin, NameConflict)
from calibre.gui2 import NONE, error_dialog, info_dialog, choose_files, \
question_dialog, gprefs
from calibre.utils.search_query_parser import SearchQueryParser
@ -279,7 +279,11 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
' Are you sure you want to proceed?'),
show_copy_button=False):
return
try:
plugin = add_plugin(path)
except NameConflict as e:
return error_dialog(self, _('Already exists'),
unicode(e), show=True)
self._plugin_model.populate()
self._plugin_model.reset()
self.changed_signal.emit()

View File

@ -76,11 +76,17 @@ class StorePlugin(object): # {{{
return items as a generator.
Don't be lazy with the search! Load as much data as possible in the
:class:`calibre.gui2.store.search_result.SearchResult` object. If you have to parse
multiple pages to get all of the data then do so. However, if data (such as cover_url)
:class:`calibre.gui2.store.search_result.SearchResult` object.
However, if data (such as cover_url)
isn't available because the store does not display cover images then it's okay to
ignore it.
At the very least a :class:`calibre.gui2.store.search_result.SearchResult`
returned by this function must have the title, author and id.
If you have to parse multiple pages to get all of the data then implement
:meth:`get_deatils` for retrieving additional information.
Also, by default search results can only include ebooks. A plugin can offer users
an option to include physical books in the search results but this must be
disabled by default.
@ -90,13 +96,34 @@ class StorePlugin(object): # {{{
:param query: The string query search with.
:param max_results: The maximum number of results to return.
:param timeout: The maximum amount of time in seconds to spend download the search results.
:param timeout: The maximum amount of time in seconds to spend downloading data for search results.
:return: :class:`calibre.gui2.store.search_result.SearchResult` objects
item_data is plugin specific and is used in :meth:`open` to open to a specifc place in the store.
'''
raise NotImplementedError()
def get_details(self, search_result, timeout=60):
'''
Delayed search for information about specific search items.
Typically, this will be used when certain information such as
formats, drm status, cover url are not part of the main search
results and the information is on another web page.
Using this function allows for the main information (title, author)
to be displayed in the search results while other information can
take extra time to load. Splitting retrieving data that takes longer
to load into a separate function will give the illusion of the search
being faster.
:param search_result: A search result that need details set.
:param timeout: The maximum amount of time in seconds to spend downloading details.
:return: True if the search_result was modified otherwise False
'''
return False
def get_settings(self):
'''
This is only useful for plugins that implement

View File

@ -154,6 +154,13 @@ class AmazonKindleStore(StorePlugin):
cover_img = data.xpath('//div[@class="productImage"]/a[@href="%s"]/img/@src' % asin_href)
if cover_img:
cover_url = cover_img[0]
parts = cover_url.split('/')
bn = parts[-1]
f, _, ext = bn.rpartition('.')
if '_' in f:
bn = f.partition('_')[0]+'_SL160_.'+ext
parts[-1] = bn
cover_url = '/'.join(parts)
title = ''.join(data.xpath('div[@class="productTitle"]/a/text()'))
author = ''.join(data.xpath('div[@class="productTitle"]/span[@class="ptBrand"]/text()'))
@ -168,5 +175,23 @@ class AmazonKindleStore(StorePlugin):
s.author = author.strip()
s.price = price.strip()
s.detail_item = asin.strip()
s.formats = 'Kindle'
yield s
def get_details(self, search_result, timeout):
url = 'http://amazon.com/dp/'
br = browser()
with closing(br.open(url + search_result.detail_item, timeout=timeout)) as nf:
idata = html.fromstring(nf.read())
if idata.xpath('boolean(//div[@class="content"]//li/b[contains(text(), "Simultaneous Device Usage")])'):
if idata.xpath('boolean(//div[@class="content"]//li[contains(., "Unlimited") and contains(b, "Simultaneous Device Usage")])'):
search_result.drm = SearchResult.DRM_UNLOCKED
else:
search_result.drm = SearchResult.DRM_UNKNOWN
else:
search_result.drm = SearchResult.DRM_LOCKED
return True

View File

@ -85,5 +85,7 @@ class BaenWebScriptionStore(BasicStoreConfig, StorePlugin):
s.author = author.strip()
s.price = price
s.detail_item = id.strip()
s.drm = SearchResult.DRM_UNLOCKED
s.formats = 'RB, MOBI, EPUB, LIT, LRF, RTF, HTML'
yield s

View File

@ -60,14 +60,6 @@ class BeWriteStore(BasicStoreConfig, StorePlugin):
cover_url = ''
price = ''
with closing(br.open(id.strip(), timeout=timeout/4)) as nf:
idata = html.fromstring(nf.read())
price = ''.join(idata.xpath('//div[@id="content"]//td[contains(text(), "ePub")]/text()'))
price = '$' + price.split('$')[-1]
cover_img = idata.xpath('//div[@id="content"]//img[1]/@src')
if cover_img:
cover_url = 'http://www.bewrite.net/mm5/' + cover_img[0]
counter -= 1
s = SearchResult()
@ -76,5 +68,36 @@ class BeWriteStore(BasicStoreConfig, StorePlugin):
s.author = author.strip()
s.price = price.strip()
s.detail_item = id.strip()
s.drm = SearchResult.DRM_UNLOCKED
yield s
def get_details(self, search_result, timeout):
br = browser()
with closing(br.open(search_result.detail_item, timeout=timeout)) as nf:
idata = html.fromstring(nf.read())
price = ''.join(idata.xpath('//div[@id="content"]//td[contains(text(), "ePub")]/text()'))
if not price:
price = ''.join(idata.xpath('//div[@id="content"]//td[contains(text(), "MOBI")]/text()'))
if not price:
price = ''.join(idata.xpath('//div[@id="content"]//td[contains(text(), "PDF")]/text()'))
price = '$' + price.split('$')[-1]
search_result.price = price.strip()
cover_img = idata.xpath('//div[@id="content"]//img[1]/@src')
if cover_img:
cover_url = 'http://www.bewrite.net/mm5/' + cover_img[0]
search_result.cover_url = cover_url.strip()
formats = set([])
if idata.xpath('boolean(//div[@id="content"]//td[contains(text(), "ePub")])'):
formats.add('EPUB')
if idata.xpath('boolean(//div[@id="content"]//td[contains(text(), "PDF")])'):
formats.add('PDF')
if idata.xpath('boolean(//div[@id="content"]//td[contains(text(), "MOBI")])'):
formats.add('MOBI')
search_result.formats = ', '.join(list(formats))
return True

View File

@ -78,5 +78,7 @@ class BNStore(BasicStoreConfig, StorePlugin):
s.author = author.strip()
s.price = price
s.detail_item = id.strip()
s.drm = SearchResult.DRM_UNKNOWN
s.formats = 'Nook'
yield s

View File

@ -75,6 +75,8 @@ class DieselEbooksStore(BasicStoreConfig, StorePlugin):
if price_elem:
price = price_elem[0]
formats = ', '.join(data.xpath('.//td[@class="format"]/text()'))
counter -= 1
s = SearchResult()
@ -83,5 +85,18 @@ class DieselEbooksStore(BasicStoreConfig, StorePlugin):
s.author = author.strip()
s.price = price.strip()
s.detail_item = '/item/' + id.strip()
s.formats = formats
yield s
def get_details(self, search_result, timeout):
url = 'http://www.diesel-ebooks.com/item/'
br = browser()
with closing(br.open(url + search_result.detail_item, timeout=timeout)) as nf:
idata = html.fromstring(nf.read())
if idata.xpath('boolean(//table[@class="format-info"]//tr[contains(th, "DRM") and contains(td, "No")])'):
search_result.drm = SearchResult.DRM_UNLOCKED
else:
search_result.drm = SearchResult.DRM_LOCKED
return True

View File

@ -7,6 +7,7 @@ __copyright__ = '2011, John Schember <john@nachtimwald.com>'
__docformat__ = 'restructuredtext en'
import random
import re
import urllib2
from contextlib import closing
@ -64,15 +65,6 @@ class EbookscomStore(BasicStoreConfig, StorePlugin):
if not id:
continue
price = ''
with closing(br.open('http://www.ebooks.com/ebooks/book_display.asp?IID=' + id.strip(), timeout=timeout)) as fp:
pdoc = html.fromstring(fp.read())
pdata = pdoc.xpath('//table[@class="price"]/tr/td/text()')
if len(pdata) >= 2:
price = pdata[1]
if not price:
continue
cover_url = ''.join(data.xpath('.//img[1]/@src'))
title = ''
@ -89,7 +81,40 @@ class EbookscomStore(BasicStoreConfig, StorePlugin):
s.cover_url = cover_url
s.title = title.strip()
s.author = author.strip()
s.price = price.strip()
s.detail_item = '?url=http://www.ebooks.com/cj.asp?IID=' + id.strip() + '&cjsku=' + id.strip()
yield s
def get_details(self, search_result, timeout):
url = 'http://www.ebooks.com/ebooks/book_display.asp?IID='
mo = re.search(r'\?IID=(?P<id>\d+)', search_result.detail_item)
if mo:
id = mo.group('id')
if not id:
return
price = _('Not Available')
br = browser()
with closing(br.open(url + id, timeout=timeout)) as nf:
pdoc = html.fromstring(nf.read())
pdata = pdoc.xpath('//table[@class="price"]/tr/td/text()')
if len(pdata) >= 2:
price = pdata[1]
search_result.drm = SearchResult.DRM_UNLOCKED
for sec in ('Printing', 'Copying', 'Lending'):
if pdoc.xpath('boolean(//div[@class="formatTableInner"]//table//tr[contains(th, "%s") and contains(td, "Off")])' % sec):
search_result.drm = SearchResult.DRM_LOCKED
break
fdata = ', '.join(pdoc.xpath('//table[@class="price"]//tr//td[1]/text()'))
fdata = fdata.replace(':', '')
fdata = re.sub(r'\s{2,}', ' ', fdata)
fdata = fdata.replace(' ,', ',')
fdata = fdata.strip()
search_result.formats = fdata
search_result.price = price.strip()
return True

View File

@ -7,6 +7,7 @@ __copyright__ = '2011, John Schember <john@nachtimwald.com>'
__docformat__ = 'restructuredtext en'
import random
import re
import urllib2
from contextlib import closing
@ -76,5 +77,28 @@ class EHarlequinStore(BasicStoreConfig, StorePlugin):
s.author = author.strip()
s.price = price.strip()
s.detail_item = '?url=http://ebooks.eharlequin.com/' + id.strip()
s.formats = 'EPUB'
yield s
def get_details(self, search_result, timeout):
url = 'http://ebooks.eharlequin.com/en/ContentDetails.htm?ID='
mo = re.search(r'\?ID=(?P<id>.+)', search_result.detail_item)
if mo:
id = mo.group('id')
if not id:
return
br = browser()
with closing(br.open(url + id, timeout=timeout)) as nf:
idata = html.fromstring(nf.read())
drm = SearchResult.DRM_UNKNOWN
if idata.xpath('boolean(//div[@class="drm_head"])'):
if idata.xpath('boolean(//td[contains(., "Copy") and contains(., "not")])'):
drm = SearchResult.DRM_LOCKED
else:
drm = SearchResult.DRM_UNLOCKED
search_result.drm = drm
return True

View File

@ -72,8 +72,10 @@ class FeedbooksStore(BasicStoreConfig, StorePlugin):
title = ''.join(data.xpath('//h5//a/text()'))
author = ''.join(data.xpath('//h6//a/text()'))
price = ''.join(data.xpath('//a[@class="buy"]/text()'))
formats = 'EPUB'
if not price:
price = '$0.00'
formats = 'EPUB, MOBI, PDF'
cover_url = ''
cover_url_img = data.xpath('//img')
if cover_url_img:
@ -88,5 +90,18 @@ class FeedbooksStore(BasicStoreConfig, StorePlugin):
s.author = author.strip()
s.price = price.replace(' ', '').strip()
s.detail_item = id.strip()
s.formats = formats
yield s
def get_details(self, search_result, timeout):
url = 'http://m.feedbooks.com/'
br = browser()
with closing(br.open(url_slash_cleaner(url + search_result.detail_item), timeout=timeout)) as nf:
idata = html.fromstring(nf.read())
if idata.xpath('boolean(//div[contains(@class, "m-description-long")]//p[contains(., "DRM") or contains(b, "Protection")])'):
search_result.drm = SearchResult.DRM_LOCKED
else:
search_result.drm = SearchResult.DRM_UNLOCKED
return True

View File

@ -79,5 +79,15 @@ class GutenbergStore(BasicStoreConfig, StorePlugin):
s.author = author.strip()
s.price = price.strip()
s.detail_item = '/ebooks/' + id.strip()
s.drm = SearchResult.DRM_UNLOCKED
yield s
def get_details(self, search_result, timeout):
url = 'http://m.gutenberg.org/'
br = browser()
with closing(br.open(url + search_result.detail_item, timeout=timeout)) as nf:
idata = html.fromstring(nf.read())
search_result.formats = ', '.join(idata.xpath('//a[@type!="application/atom+xml"]//span[@class="title"]/text()'))
return True

View File

@ -63,7 +63,7 @@ class KoboStore(BasicStoreConfig, StorePlugin):
if not id:
continue
price = ''.join(data.xpath('.//span[@class="SCOurPrice"]/strong/text()'))
price = ''.join(data.xpath('.//li[@class="OurPrice"]/strong/text()'))
if not price:
price = '$0.00'
@ -71,6 +71,7 @@ class KoboStore(BasicStoreConfig, StorePlugin):
title = ''.join(data.xpath('.//div[@class="SCItemHeader"]/h1/a[1]/text()'))
author = ''.join(data.xpath('.//div[@class="SCItemSummary"]/span/a[1]/text()'))
drm = data.xpath('boolean(.//span[@class="SCAvailibilityFormatsText" and contains(text(), "DRM")])')
counter -= 1
@ -80,5 +81,7 @@ class KoboStore(BasicStoreConfig, StorePlugin):
s.author = author.strip()
s.price = price.strip()
s.detail_item = '?url=http://www.kobobooks.com/' + id.strip()
s.drm = SearchResult.DRM_LOCKED if drm else SearchResult.DRM_UNLOCKED
s.formats = 'EPUB'
yield s

View File

@ -89,5 +89,7 @@ class ManyBooksStore(BasicStoreConfig, StorePlugin):
s.author = author.strip()
s.price = price.strip()
s.detail_item = '/titles/' + id
s.drm = SearchResult.DRM_UNLOCKED
s.formts = 'EPUB, PDB (eReader, PalmDoc, zTXT, Plucker, iSilo), FB2, ZIP, AZW, MOBI, PRC, LIT, PKG, PDF, TXT, RB, RTF, LRF, TCR, JAR'
yield s

View File

@ -18,19 +18,18 @@ from PyQt4.Qt import Qt, QUrl, QDialog, QAbstractItemModel, QModelIndex, QVarian
pyqtSignal
from calibre import browser
from calibre.gui2 import open_url, NONE
from calibre.gui2 import open_url, NONE, JSONConfig
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
from calibre.gui2.store.search_result import SearchResult
from calibre.gui2.store.web_store_dialog import WebStoreDialog
from calibre.utils.config import DynamicConfig
from calibre.utils.icu import sort_key
class MobileReadStore(BasicStoreConfig, StorePlugin):
def genesis(self):
self.config = DynamicConfig('store_' + self.name)
self.config = JSONConfig('store/store/' + self.name)
self.rlock = RLock()
def open(self, parent=None, detail_item=None, external=False):
@ -76,13 +75,14 @@ class MobileReadStore(BasicStoreConfig, StorePlugin):
matches = heapq.nlargest(max_results, matches)
for score, book in matches:
book.price = '$0.00'
book.drm = SearchResult.DRM_UNLOCKED
yield book
def update_book_list(self, timeout=10):
with self.rlock:
url = 'http://www.mobileread.com/forums/ebooks.php?do=getlist&type=html'
last_download = self.config.get(self.name + '_last_download', None)
last_download = self.config.get('last_download', None)
# Don't update the book list if our cache is less than one week old.
if last_download and (time.time() - last_download) < 604800:
return
@ -96,15 +96,15 @@ class MobileReadStore(BasicStoreConfig, StorePlugin):
if not raw_data:
return
# Turn books listed in the HTML file into BookRef's.
# Turn books listed in the HTML file into SearchResults's.
books = []
try:
data = html.fromstring(raw_data)
for book_data in data.xpath('//ul/li'):
book = BookRef()
book = SearchResult()
book.detail_item = ''.join(book_data.xpath('.//a/@href'))
book.format = ''.join(book_data.xpath('.//i/text()'))
book.format = book.format.strip()
book.formats = ''.join(book_data.xpath('.//i/text()'))
book.formats = book.formats.strip()
text = ''.join(book_data.xpath('.//a/text()'))
if ':' in text:
@ -117,20 +117,34 @@ class MobileReadStore(BasicStoreConfig, StorePlugin):
# Save the book list and it's create time.
if books:
self.config[self.name + '_last_download'] = time.time()
self.config[self.name + '_book_list'] = books
self.config['last_download'] = time.time()
self.config['book_list'] = self.seralize_books(books)
def get_book_list(self, timeout=10):
self.update_book_list(timeout=timeout)
return self.config.get(self.name + '_book_list', [])
return self.deseralize_books(self.config.get('book_list', []))
def seralize_books(self, books):
sbooks = []
for b in books:
data = {}
data['author'] = b.author
data['title'] = b.title
data['detail_item'] = b.detail_item
data['formats'] = b.formats
sbooks.append(data)
return sbooks
class BookRef(SearchResult):
def __init__(self):
SearchResult.__init__(self)
self.format = ''
def deseralize_books(self, sbooks):
books = []
for s in sbooks:
b = SearchResult()
b.author = s.get('author', '')
b.title = s.get('title', '')
b.detail_item = s.get('detail_item', '')
b.formats = s.get('formats', '')
books.append(b)
return books
class MobeReadStoreDialog(QDialog, Ui_Dialog):
@ -159,11 +173,11 @@ class MobeReadStoreDialog(QDialog, Ui_Dialog):
self.plugin.open(self, result.detail_item)
def restore_state(self):
geometry = self.plugin.config['store_mobileread_dialog_geometry']
geometry = self.plugin.config.get('dialog_geometry', None)
if geometry:
self.restoreGeometry(geometry)
results_cwidth = self.plugin.config['store_mobileread_dialog_results_view_column_width']
results_cwidth = self.plugin.config.get('dialog_results_view_column_width')
if results_cwidth:
for i, x in enumerate(results_cwidth):
if i >= self.results_view.model().columnCount():
@ -173,16 +187,16 @@ class MobeReadStoreDialog(QDialog, Ui_Dialog):
for i in xrange(self.results_view.model().columnCount()):
self.results_view.resizeColumnToContents(i)
self.results_view.model().sort_col = self.plugin.config.get('store_mobileread_dialog_sort_col', 0)
self.results_view.model().sort_order = self.plugin.config.get('store_mobileread_dialog_sort_order', Qt.AscendingOrder)
self.results_view.model().sort_col = self.plugin.config.get('dialog_sort_col', 0)
self.results_view.model().sort_order = self.plugin.config.get('dialog_sort_order', Qt.AscendingOrder)
self.results_view.model().sort(self.results_view.model().sort_col, self.results_view.model().sort_order)
self.results_view.header().setSortIndicator(self.results_view.model().sort_col, self.results_view.model().sort_order)
def save_state(self):
self.plugin.config['store_mobileread_dialog_geometry'] = self.saveGeometry()
self.plugin.config['store_mobileread_dialog_results_view_column_width'] = [self.results_view.columnWidth(i) for i in range(self.model.columnCount())]
self.plugin.config['store_mobileread_dialog_sort_col'] = self.results_view.model().sort_col
self.plugin.config['store_mobileread_dialog_sort_order'] = self.results_view.model().sort_order
self.plugin.config['dialog_geometry'] = bytearray(self.saveGeometry())
self.plugin.config['dialog_results_view_column_width'] = [self.results_view.columnWidth(i) for i in range(self.model.columnCount())]
self.plugin.config['dialog_sort_col'] = self.results_view.model().sort_col
self.plugin.config['dialog_sort_order'] = self.results_view.model().sort_order
def dialog_closed(self, result):
self.save_state()
@ -223,7 +237,7 @@ class BooksModel(QAbstractItemModel):
self.books = []
if self.filter:
for b in self.all_books:
test = '%s %s %s' % (b.title, b.author, b.format)
test = '%s %s %s' % (b.title, b.author, b.formats)
test = test.lower()
include = True
for item in self.filter.split(' '):
@ -276,7 +290,7 @@ class BooksModel(QAbstractItemModel):
elif col == 1:
return QVariant(result.author)
elif col == 2:
return QVariant(result.format)
return QVariant(result.formats)
return NONE
def data_as_text(self, result, col):
@ -286,7 +300,7 @@ class BooksModel(QAbstractItemModel):
elif col == 1:
text = result.author
elif col == 2:
text = result.format
text = result.formats
return text
def sort(self, col, order, reset=True):

View File

@ -68,5 +68,15 @@ class OpenLibraryStore(BasicStoreConfig, StorePlugin):
s.author = author.strip()
s.price = price
s.detail_item = id.strip()
s.drm = SearchResult.DRM_UNKNOWN
yield s
def get_details(self, search_result, timeout):
url = 'http://openlibrary.org/'
br = browser()
with closing(br.open(url_slash_cleaner(url + search_result.detail_item), timeout=timeout)) as nf:
idata = html.fromstring(nf.read())
search_result.formats = ', '.join(list(set(idata.xpath('//a[contains(@title, "Download")]/text()'))))
return True

View File

@ -10,6 +10,7 @@ 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
@ -18,12 +19,12 @@ from PyQt4.Qt import (Qt, QAbstractItemModel, QDialog, QTimer, QVariant,
QModelIndex, QPixmap, QSize, QCheckBox, QVBoxLayout)
from calibre import browser
from calibre.gui2 import NONE
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.config import DynamicConfig
from calibre.utils.icu import sort_key
from calibre.utils.magick.draw import thumbnail
from calibre.utils.search_query_parser import SearchQueryParser
@ -33,13 +34,21 @@ 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 = DynamicConfig('store_search')
self.config = JSONConfig('store/search')
# We keep a cache of store plugins and reference them by name.
self.store_plugins = istores
@ -87,9 +96,13 @@ class SearchDialog(QDialog, Ui_Dialog):
# Author
self.results_view.setColumnWidth(2,int(total*.35))
# Price
self.results_view.setColumnWidth(3, int(total*.10))
self.results_view.setColumnWidth(3, int(total*.5))
# DRM
self.results_view.setColumnWidth(4, int(total*.5))
# Store
self.results_view.setColumnWidth(4, int(total*.20))
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.
@ -102,6 +115,9 @@ class SearchDialog(QDialog, Ui_Dialog):
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
@ -110,6 +126,8 @@ class SearchDialog(QDialog, Ui_Dialog):
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:
@ -121,9 +139,32 @@ class SearchDialog(QDialog, Ui_Dialog):
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'] = self.saveGeometry()
self.config['store_search_store_splitter_state'] = self.store_splitter.saveState()
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 = {}
@ -132,15 +173,15 @@ class SearchDialog(QDialog, Ui_Dialog):
self.config['store_search_store_checked'] = store_check
def restore_state(self):
geometry = self.config['store_search_geometry']
geometry = self.config.get('store_search_geometry', None)
if geometry:
self.restoreGeometry(geometry)
splitter_state = self.config['store_search_store_splitter_state']
splitter_state = self.config.get('store_search_store_splitter_state', None)
if splitter_state:
self.store_splitter.restoreState(splitter_state)
results_cwidth = self.config['store_search_results_view_column_width']
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():
@ -149,7 +190,7 @@ class SearchDialog(QDialog, Ui_Dialog):
else:
self.resize_columns()
store_check = self.config['store_search_store_checked']
store_check = self.config.get('store_search_store_checked', None)
if store_check:
for n in store_check:
if hasattr(self, 'store_check_' + n):
@ -170,9 +211,9 @@ class SearchDialog(QDialog, Ui_Dialog):
self.pi.stopAnimation()
while self.search_pool.has_results():
res = self.search_pool.get_result()
res, store_plugin = self.search_pool.get_result()
if res:
self.results_view.model().add_result(res)
self.results_view.model().add_result(res, store_plugin)
def open_store(self, index):
result = self.results_view.model().get_result(index)
@ -294,18 +335,14 @@ class SearchThread(Thread):
while self._run and not self.tasks.empty():
try:
query, store_name, store_plugin, timeout = self.tasks.get()
squery = query
for loc in SearchFilter.USABLE_LOCATIONS:
squery = re.sub(r'%s:"?(?P<a>[^\s"]+)"?' % loc, '\g<a>', squery)
for res in store_plugin.search(squery, timeout=timeout):
for res in store_plugin.search(query, timeout=timeout):
if not self._run:
return
res.store_name = store_name
if SearchFilter(res).parse(query):
self.results.put(res)
self.results.put((res, store_plugin))
self.tasks.task_done()
except:
pass
traceback.print_exc()
class CoverThreadPool(GenericDownloadThreadPool):
@ -349,29 +386,97 @@ class CoverThread(Thread):
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'), _('Store')]
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):
def add_result(self, result, store_plugin):
if result not in self.all_matches:
self.layoutAboutToBeChanged.emit()
self.matches.append(result)
self.cover_pool.add_task(result, self.update_result)
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):
@ -381,10 +486,29 @@ class Matches(QAbstractItemModel):
else:
return None
def update_result(self):
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)
@ -420,14 +544,41 @@ class Matches(QAbstractItemModel):
return QVariant(result.author)
elif col == 3:
return QVariant(result.price)
elif col == 4:
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
@ -439,25 +590,34 @@ class Matches(QAbstractItemModel):
elif col == 2:
text = result.author
elif col == 3:
text = result.price
if len(text) < 3 or text[-3] not in ('.', ','):
text += '00'
text = re.sub(r'\D', '', text)
text = text.rjust(6, '0')
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.matches.sort(None,
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):
@ -466,22 +626,33 @@ class SearchFilter(SearchQueryParser):
'author',
'authors',
'cover',
'drm',
'format',
'formats',
'price',
'title',
'store',
]
def __init__(self, search_result):
def __init__(self):
SearchQueryParser.__init__(self, locations=self.USABLE_LOCATIONS)
self.search_result = search_result
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 set([self.search_result])
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:
@ -502,24 +673,37 @@ class SearchFilter(SearchQueryParser):
all_locs = set(self.USABLE_LOCATIONS) - set(['all'])
locations = all_locs if location == 'all' else [location]
q = {
'author': self.search_result.author.lower(),
'cover': self.search_result.cover_url,
'format': '',
'price': self.search_result.price,
'store': self.search_result.store_name.lower(),
'title': self.search_result.title.lower(),
'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:
ac_val = q[locvalue]
accessor = q[locvalue]
if query == 'true':
if ac_val is not None:
matches.add(self.search_result)
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 ac_val is None:
matches.add(self.search_result)
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
@ -530,9 +714,12 @@ class SearchFilter(SearchQueryParser):
else:
m = matchkind
vals = [ac_val]
if locvalue == 'format':
vals = accessor(sr).split(',')
else:
vals = [accessor(sr)]
if _match(query, vals, m):
matches.add(self.search_result)
matches.add(sr)
break
except ValueError: # Unicode errors
traceback.print_exc()

View File

@ -11,7 +11,11 @@
</rect>
</property>
<property name="windowTitle">
<string>calibre Store Search</string>
<string>Get Books</string>
</property>
<property name="windowIcon">
<iconset resource="../../../../resources/images.qrc">
<normaloff>:/images/store.png</normaloff>:/images/store.png</iconset>
</property>
<property name="sizeGripEnabled">
<bool>true</bool>
@ -58,8 +62,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>215</width>
<height>116</height>
<width>170</width>
<height>138</height>
</rect>
</property>
</widget>
@ -174,7 +178,9 @@
</item>
</layout>
</widget>
<resources/>
<resources>
<include location="../../../../resources/images.qrc"/>
</resources>
<connections>
<connection>
<sender>close</sender>

View File

@ -8,6 +8,10 @@ __docformat__ = 'restructuredtext en'
class SearchResult(object):
DRM_LOCKED = 1
DRM_UNLOCKED = 2
DRM_UNKNOWN = 3
def __init__(self):
self.store_name = ''
self.cover_url = ''
@ -16,3 +20,8 @@ class SearchResult(object):
self.author = ''
self.price = ''
self.detail_item = ''
self.drm = None
self.formats = ''
def __eq__(self, other):
return self.title == other.title and self.author == other.author and self.store_name == other.store_name

View File

@ -90,5 +90,15 @@ class SmashwordsStore(BasicStoreConfig, StorePlugin):
s.author = author.strip()
s.price = price.strip()
s.detail_item = '/books/view/' + id.strip()
s.drm = SearchResult.DRM_UNLOCKED
yield s
def get_details(self, search_result, timeout):
url = 'http://www.smashwords.com/'
br = browser()
with closing(br.open(url + search_result.detail_item, timeout=timeout)) as nf:
idata = html.fromstring(nf.read())
search_result.formats = ', '.join(list(set(idata.xpath('//td//b//text()'))))
return True

View File

@ -68,6 +68,19 @@ class NPWebView(QWebView):
filename = get_download_filename(url, cf)
ext = os.path.splitext(filename)[1][1:].lower()
if ext not in BOOK_EXTENSIONS:
if ext == 'acsm':
from calibre.gui2.dialogs.confirm_delete import confirm
if not confirm('<p>' + _('This ebook is a DRMed EPUB file. '
'You will be prompted to save this file to your '
'computer. Once it is saved, open it with '
'<a href="http://www.adobe.com/products/digitaleditions/">'
'Adobe Digital Editions</a> (ADE).<p>ADE, in turn '
'will download the actual ebook, which will be a '
'.epub file. You can add this book to calibre '
'using "Add Books" and selecting the file from '
'the ADE library folder.'),
'acsm_download', self):
return
home = os.path.expanduser('~')
name = QFileDialog.getSaveFileName(self,
_('File is not a supported ebook type. Save to disk?'),

View File

@ -35,7 +35,7 @@ category_icon_map = {
'custom:' : 'column.png',
'user:' : 'tb_folder.png',
'search' : 'search.png',
'identifiers': 'id_card.png'
'identifiers': 'identifiers.png'
}

View File

@ -61,6 +61,12 @@ if not _run_once:
################################################################################
# Initialize locale
# Import string as we do not want locale specific
# string.whitespace/printable, on windows especially, this causes problems.
# Before the delay load optimizations, string was loaded before this point
# anyway, so we preserve the old behavior explicitly.
import string
string
try:
locale.setlocale(locale.LC_ALL, '')
except:

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Some files were not shown because too many files have changed in this diff Show More