mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
merge from trunk
This commit is contained in:
commit
bce7efa49e
59
recipes/babyonline.recipe
Normal file
59
recipes/babyonline.recipe
Normal 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)
|
@ -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'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'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'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 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'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'),
|
(u'Confidential News', u'http://feeds.news.com.au/public/rss/2.0/dtele_entertainment_confidential_252.xml'),
|
||||||
|
@ -14,14 +14,14 @@ class EcuisineRo(BasicNewsRecipe):
|
|||||||
__author__ = u'Silviu Cotoar\u0103'
|
__author__ = u'Silviu Cotoar\u0103'
|
||||||
description = u'Reinventeaz\u0103 pl\u0103cerea de a g\u0103ti'
|
description = u'Reinventeaz\u0103 pl\u0103cerea de a g\u0103ti'
|
||||||
publisher = 'eCuisine'
|
publisher = 'eCuisine'
|
||||||
oldest_article = 5
|
oldest_article = 50
|
||||||
language = 'ro'
|
language = 'ro'
|
||||||
max_articles_per_feed = 100
|
max_articles_per_feed = 100
|
||||||
no_stylesheets = True
|
no_stylesheets = True
|
||||||
use_embedded_content = False
|
use_embedded_content = False
|
||||||
category = 'Ziare,Retete,Bucatarie'
|
category = 'Ziare,Retete,Bucatarie'
|
||||||
encoding = 'utf-8'
|
encoding = 'utf-8'
|
||||||
cover_url = ''
|
cover_url = 'http://www.ecuisine.ro/sites/all/themes/ecuisine/images/logo.gif'
|
||||||
|
|
||||||
conversion_options = {
|
conversion_options = {
|
||||||
'comments' : description
|
'comments' : description
|
||||||
@ -31,8 +31,8 @@ class EcuisineRo(BasicNewsRecipe):
|
|||||||
}
|
}
|
||||||
|
|
||||||
keep_only_tags = [
|
keep_only_tags = [
|
||||||
dict(name='div', attrs={'class':'page-title'})
|
dict(name='h1', attrs={'id':'page-title'})
|
||||||
, dict(name='div', attrs={'class':'content clearfix'})
|
, dict(name='div', attrs={'class':'field-item even'})
|
||||||
]
|
]
|
||||||
|
|
||||||
remove_tags = [
|
remove_tags = [
|
||||||
|
@ -31,8 +31,8 @@ class EgirlRo(BasicNewsRecipe):
|
|||||||
}
|
}
|
||||||
|
|
||||||
keep_only_tags = [
|
keep_only_tags = [
|
||||||
dict(name='div', attrs={'id':'title_art'})
|
dict(name='div', attrs={'id':'content_art'})
|
||||||
, dict(name='div', attrs={'class':'content_style'})
|
, dict(name='div', attrs={'class':'content_articol'})
|
||||||
]
|
]
|
||||||
|
|
||||||
feeds = [
|
feeds = [
|
||||||
|
BIN
recipes/icons/babyonline.png
Normal file
BIN
recipes/icons/babyonline.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 256 B |
@ -37,10 +37,12 @@ class TabuRo(BasicNewsRecipe):
|
|||||||
]
|
]
|
||||||
|
|
||||||
remove_tags = [
|
remove_tags = [
|
||||||
dict(name='div', attrs={'class':'asemanatoare'})
|
dict(name='div', attrs={'class':'asemanatoare'}),
|
||||||
|
dict(name='div', attrs={'class':'social'})
|
||||||
]
|
]
|
||||||
|
|
||||||
remove_tags_after = [
|
remove_tags_after = [
|
||||||
|
dict(name='div', attrs={'class':'social'}),
|
||||||
dict(name='div', attrs={'id':'comments'}),
|
dict(name='div', attrs={'id':'comments'}),
|
||||||
dict(name='div', attrs={'class':'asemanatoare'})
|
dict(name='div', attrs={'class':'asemanatoare'})
|
||||||
]
|
]
|
||||||
|
BIN
resources/images/drm-locked.png
Normal file
BIN
resources/images/drm-locked.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.6 KiB |
BIN
resources/images/drm-unlocked.png
Normal file
BIN
resources/images/drm-unlocked.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.6 KiB |
Binary file not shown.
Before Width: | Height: | Size: 6.3 KiB |
BIN
resources/images/identifiers.png
Normal file
BIN
resources/images/identifiers.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 705 B |
4
setup.py
4
setup.py
@ -15,9 +15,9 @@ from setup import prints, get_warnings
|
|||||||
|
|
||||||
def check_version_info():
|
def check_version_info():
|
||||||
vi = sys.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 None
|
||||||
return 'calibre requires python >= 2.6'
|
return 'calibre requires python >= 2.7 and < 3'
|
||||||
|
|
||||||
def option_parser():
|
def option_parser():
|
||||||
parser = optparse.OptionParser()
|
parser = optparse.OptionParser()
|
||||||
|
@ -45,6 +45,7 @@ fcntl = None if iswindows else importlib.import_module('fcntl')
|
|||||||
filesystem_encoding = sys.getfilesystemencoding()
|
filesystem_encoding = sys.getfilesystemencoding()
|
||||||
if filesystem_encoding is None: filesystem_encoding = 'utf-8'
|
if filesystem_encoding is None: filesystem_encoding = 'utf-8'
|
||||||
|
|
||||||
|
|
||||||
DEBUG = False
|
DEBUG = False
|
||||||
|
|
||||||
def debug():
|
def debug():
|
||||||
|
@ -22,6 +22,11 @@ from calibre.utils.config import make_config_dir, Config, ConfigProxy, \
|
|||||||
from calibre.ebooks.epub.fix import ePubFixer
|
from calibre.ebooks.epub.fix import ePubFixer
|
||||||
from calibre.ebooks.metadata.sources.base import Source
|
from calibre.ebooks.metadata.sources.base import Source
|
||||||
|
|
||||||
|
builtin_names = frozenset([p.name for p in builtin_plugins])
|
||||||
|
|
||||||
|
class NameConflict(ValueError):
|
||||||
|
pass
|
||||||
|
|
||||||
def _config():
|
def _config():
|
||||||
c = Config('customize')
|
c = Config('customize')
|
||||||
c.add_opt('plugins', default={}, help=_('Installed plugins'))
|
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):
|
def add_plugin(path_to_zip_file):
|
||||||
make_config_dir()
|
make_config_dir()
|
||||||
plugin = load_plugin(path_to_zip_file)
|
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)
|
plugin = initialize_plugin(plugin, path_to_zip_file)
|
||||||
plugins = config['plugins']
|
plugins = config['plugins']
|
||||||
zfp = os.path.join(plugin_dir, plugin.name+'.zip')
|
zfp = os.path.join(plugin_dir, plugin.name+'.zip')
|
||||||
@ -506,7 +514,11 @@ def initialize_plugin(plugin, path_to_zip_file):
|
|||||||
def initialize_plugins():
|
def initialize_plugins():
|
||||||
global _initialized_plugins
|
global _initialized_plugins
|
||||||
_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:
|
||||||
try:
|
try:
|
||||||
plugin = load_plugin(zfp) if not isinstance(zfp, type) else zfp
|
plugin = load_plugin(zfp) if not isinstance(zfp, type) else zfp
|
||||||
|
@ -164,7 +164,7 @@ class APNXBuilder(object):
|
|||||||
if c == '/':
|
if c == '/':
|
||||||
closing = True
|
closing = True
|
||||||
continue
|
continue
|
||||||
elif c == 'p':
|
elif c in ('d', 'p'):
|
||||||
if closing:
|
if closing:
|
||||||
in_p = False
|
in_p = False
|
||||||
else:
|
else:
|
||||||
|
@ -7,7 +7,7 @@ Code for the conversion of ebook formats and the reading of metadata
|
|||||||
from various formats.
|
from various formats.
|
||||||
'''
|
'''
|
||||||
|
|
||||||
import traceback, os
|
import traceback, os, re
|
||||||
from calibre import CurrentDir
|
from calibre import CurrentDir
|
||||||
|
|
||||||
class ConversionError(Exception):
|
class ConversionError(Exception):
|
||||||
@ -169,3 +169,42 @@ def calibre_cover(title, author_string, series_string=None,
|
|||||||
lines.append(TextLine(series_string, author_size))
|
lines.append(TextLine(series_string, author_size))
|
||||||
return create_cover_page(lines, I('library.png'), output_format='jpg')
|
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
|
||||||
|
|
||||||
|
|
||||||
|
@ -25,6 +25,7 @@ msprefs.defaults['max_tags'] = 20
|
|||||||
msprefs.defaults['wait_after_first_identify_result'] = 30 # seconds
|
msprefs.defaults['wait_after_first_identify_result'] = 30 # seconds
|
||||||
msprefs.defaults['wait_after_first_cover_result'] = 60 # seconds
|
msprefs.defaults['wait_after_first_cover_result'] = 60 # seconds
|
||||||
msprefs.defaults['swap_author_names'] = False
|
msprefs.defaults['swap_author_names'] = False
|
||||||
|
msprefs.defaults['fewer_tags'] = True
|
||||||
|
|
||||||
# Google covers are often poor quality (scans/errors) but they have high
|
# Google covers are often poor quality (scans/errors) but they have high
|
||||||
# resolution, so they trump covers from better sources. So make sure they
|
# resolution, so they trump covers from better sources. So make sure they
|
||||||
|
@ -216,7 +216,7 @@ class ISBNMerge(object):
|
|||||||
|
|
||||||
# We assume the smallest set of tags has the least cruft in it
|
# We assume the smallest set of tags has the least cruft in it
|
||||||
ans.tags = self.length_merge('tags', results,
|
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
|
# We assume the longest series has the most info in it
|
||||||
ans.series = self.length_merge('series', results,
|
ans.series = self.length_merge('series', results,
|
||||||
|
@ -40,24 +40,17 @@ class OverDrive(Source):
|
|||||||
supports_gzip_transfer_encoding = False
|
supports_gzip_transfer_encoding = False
|
||||||
cached_cover_url_is_reliable = True
|
cached_cover_url_is_reliable = True
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
Source.__init__(self, *args, **kwargs)
|
|
||||||
|
|
||||||
options = (
|
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.')),
|
_('Enable this option to gather all metadata available from Overdrive.')),
|
||||||
)
|
)
|
||||||
|
|
||||||
config_help_message = '<p>'+_('Additional metadata can be taken from Overdrive\'s book detail'
|
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,'
|
' 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'
|
' and the ebook ISBN. Collecting this data is disabled by default due to the extra'
|
||||||
' time required.')
|
' time required. Check the download all metadata option below to'
|
||||||
|
' enable downloading this data.')
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
Source.__init__(self, *args, **kwargs)
|
|
||||||
|
|
||||||
prefs = self.prefs
|
|
||||||
prefs.defaults['get_full_metadata'] = False
|
|
||||||
|
|
||||||
def identify(self, log, result_queue, abort, title=None, authors=None, # {{{
|
def identify(self, log, result_queue, abort, title=None, authors=None, # {{{
|
||||||
identifiers={}, timeout=30):
|
identifiers={}, timeout=30):
|
||||||
|
@ -20,7 +20,7 @@ from calibre.utils.filenames import ascii_filename
|
|||||||
from calibre.utils.date import parse_date
|
from calibre.utils.date import parse_date
|
||||||
from calibre.utils.cleantext import clean_ascii_chars
|
from calibre.utils.cleantext import clean_ascii_chars
|
||||||
from calibre.ptempfile import TemporaryDirectory
|
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.chardet import ENCODING_PATS
|
||||||
from calibre.ebooks.mobi import MobiError
|
from calibre.ebooks.mobi import MobiError
|
||||||
from calibre.ebooks.mobi.huffcdic import HuffReader
|
from calibre.ebooks.mobi.huffcdic import HuffReader
|
||||||
@ -258,6 +258,8 @@ class MobiReader(object):
|
|||||||
}
|
}
|
||||||
''')
|
''')
|
||||||
self.tag_css_rules = {}
|
self.tag_css_rules = {}
|
||||||
|
self.left_margins = {}
|
||||||
|
self.text_indents = {}
|
||||||
|
|
||||||
if hasattr(filename_or_stream, 'read'):
|
if hasattr(filename_or_stream, 'read'):
|
||||||
stream = filename_or_stream
|
stream = filename_or_stream
|
||||||
@ -567,9 +569,21 @@ class MobiReader(object):
|
|||||||
elif tag.tag == 'img':
|
elif tag.tag == 'img':
|
||||||
tag.set('width', width)
|
tag.set('width', width)
|
||||||
else:
|
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('-'):
|
if width.startswith('-'):
|
||||||
styles.append('margin-left: %s' % self.ensure_unit(width[1:]))
|
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'):
|
if attrib.has_key('align'):
|
||||||
align = attrib.pop('align').strip()
|
align = attrib.pop('align').strip()
|
||||||
if align:
|
if align:
|
||||||
@ -661,6 +675,26 @@ class MobiReader(object):
|
|||||||
if hasattr(parent, 'remove'):
|
if hasattr(parent, 'remove'):
|
||||||
parent.remove(tag)
|
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):
|
def create_opf(self, htmlfile, guide=None, root=None):
|
||||||
mi = getattr(self.book_header.exth, 'mi', self.embedded_mi)
|
mi = getattr(self.book_header.exth, 'mi', self.embedded_mi)
|
||||||
if mi is None:
|
if mi is None:
|
||||||
@ -731,16 +765,45 @@ class MobiReader(object):
|
|||||||
except:
|
except:
|
||||||
text = ''
|
text = ''
|
||||||
text = ent_pat.sub(entity_to_unicode, 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)
|
text)
|
||||||
|
item.left_space = int(self.get_left_whitespace(x))
|
||||||
found = True
|
found = True
|
||||||
if reached and found and x.get('class', None) == 'mbp_pagebreak':
|
if reached and found and x.get('class', None) == 'mbp_pagebreak':
|
||||||
break
|
break
|
||||||
if tocobj is not None:
|
if tocobj is not None:
|
||||||
|
tocobj = self.structure_toc(tocobj)
|
||||||
opf.set_toc(tocobj)
|
opf.set_toc(tocobj)
|
||||||
|
|
||||||
return opf, ncx_manifest_entry
|
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_entries(self, data):
|
||||||
def sizeof_trailing_entry(ptr, psize):
|
def sizeof_trailing_entry(ptr, psize):
|
||||||
|
@ -18,6 +18,7 @@ from cssutils import profile as cssprofiles
|
|||||||
from lxml import etree
|
from lxml import etree
|
||||||
from lxml.cssselect import css_to_xpath, ExpressionError, SelectorSyntaxError
|
from lxml.cssselect import css_to_xpath, ExpressionError, SelectorSyntaxError
|
||||||
from calibre import force_unicode
|
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 XHTML, XHTML_NS, CSS_MIME, OEB_STYLES
|
||||||
from calibre.ebooks.oeb.base import XPNSMAP, xpath, urlnormalize
|
from calibre.ebooks.oeb.base import XPNSMAP, xpath, urlnormalize
|
||||||
from calibre.ebooks.oeb.profile import PROFILES
|
from calibre.ebooks.oeb.profile import PROFILES
|
||||||
@ -444,7 +445,6 @@ class Stylizer(object):
|
|||||||
|
|
||||||
|
|
||||||
class Style(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)')
|
MS_PAT = re.compile(r'^\s*(mso-|panose-|text-underline|tab-interval)')
|
||||||
|
|
||||||
def __init__(self, element, stylizer):
|
def __init__(self, element, stylizer):
|
||||||
@ -507,43 +507,11 @@ class Style(object):
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
def _unit_convert(self, value, base=None, font=None):
|
def _unit_convert(self, value, base=None, font=None):
|
||||||
' Return value in pts'
|
'Return value in pts'
|
||||||
if isinstance(value, (int, long, float)):
|
if base is None:
|
||||||
return value
|
base = self.width
|
||||||
try:
|
font = font or self.fontSize
|
||||||
return float(value) * 72.0 / self._profile.dpi
|
return unit_convert(value, base, font, 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 == '%':
|
|
||||||
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
|
|
||||||
|
|
||||||
def pt_to_px(self, value):
|
def pt_to_px(self, value):
|
||||||
return (self._profile.dpi / 72.0) * value
|
return (self._profile.dpi / 72.0) * value
|
||||||
|
@ -11,6 +11,7 @@ from functools import partial
|
|||||||
from PyQt4.Qt import QMenu
|
from PyQt4.Qt import QMenu
|
||||||
|
|
||||||
from calibre.gui2.actions import InterfaceAction
|
from calibre.gui2.actions import InterfaceAction
|
||||||
|
from calibre.gui2.dialogs.confirm_delete import confirm
|
||||||
|
|
||||||
class StoreAction(InterfaceAction):
|
class StoreAction(InterfaceAction):
|
||||||
|
|
||||||
@ -31,9 +32,35 @@ class StoreAction(InterfaceAction):
|
|||||||
self.qaction.setMenu(self.store_menu)
|
self.qaction.setMenu(self.store_menu)
|
||||||
|
|
||||||
def search(self):
|
def search(self):
|
||||||
|
self.show_disclaimer()
|
||||||
from calibre.gui2.store.search import SearchDialog
|
from calibre.gui2.store.search import SearchDialog
|
||||||
sd = SearchDialog(self.gui.istores, self.gui)
|
sd = SearchDialog(self.gui.istores, self.gui)
|
||||||
sd.exec_()
|
sd.exec_()
|
||||||
|
|
||||||
def open_store(self, store_plugin):
|
def open_store(self, store_plugin):
|
||||||
|
self.show_disclaimer()
|
||||||
store_plugin.open(self.gui)
|
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'))
|
||||||
|
|
||||||
|
@ -418,6 +418,7 @@ class BookDetails(QWidget): # {{{
|
|||||||
if y is None:
|
if y is None:
|
||||||
# Local image
|
# Local image
|
||||||
self.cover_view.paste_from_clipboard(x)
|
self.cover_view.paste_from_clipboard(x)
|
||||||
|
self.update_layout()
|
||||||
else:
|
else:
|
||||||
self.remote_file_dropped.emit(x, y)
|
self.remote_file_dropped.emit(x, y)
|
||||||
# We do not support setting cover *and* adding formats for
|
# We do not support setting cover *and* adding formats for
|
||||||
@ -449,6 +450,7 @@ class BookDetails(QWidget): # {{{
|
|||||||
self.setAcceptDrops(True)
|
self.setAcceptDrops(True)
|
||||||
self._layout = DetailsLayout(vertical, self)
|
self._layout = DetailsLayout(vertical, self)
|
||||||
self.setLayout(self._layout)
|
self.setLayout(self._layout)
|
||||||
|
self.current_path = ''
|
||||||
|
|
||||||
self.cover_view = CoverView(vertical, self)
|
self.cover_view = CoverView(vertical, self)
|
||||||
self.cover_view.cover_changed.connect(self.cover_changed.emit)
|
self.cover_view.cover_changed.connect(self.cover_changed.emit)
|
||||||
@ -482,15 +484,19 @@ class BookDetails(QWidget): # {{{
|
|||||||
def show_data(self, data):
|
def show_data(self, data):
|
||||||
self.book_info.show_data(data)
|
self.book_info.show_data(data)
|
||||||
self.cover_view.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())
|
self._layout.do_layout(self.rect())
|
||||||
try:
|
try:
|
||||||
sz = self.cover_view.pixmap.size()
|
sz = self.cover_view.pixmap.size()
|
||||||
except:
|
except:
|
||||||
sz = QSize(0, 0)
|
sz = QSize(0, 0)
|
||||||
self.setToolTip(
|
self.setToolTip(
|
||||||
'<p>'+_('Double-click to open Book Details window') +
|
'<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())
|
'<br><br>' + _('Cover size: %dx%d')%(sz.width(), sz.height())
|
||||||
)
|
)
|
||||||
|
|
||||||
def reset_info(self):
|
def reset_info(self):
|
||||||
|
@ -24,11 +24,18 @@ class Dialog(QDialog, Ui_Dialog):
|
|||||||
dynamic[confirm_config_name(self.name)] = self.again.isChecked()
|
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):
|
if not dynamic.get(confirm_config_name(name), True):
|
||||||
return True
|
return True
|
||||||
d = Dialog(msg, name, parent)
|
d = Dialog(msg, name, parent)
|
||||||
d.label.setPixmap(QPixmap(I(pixmap)))
|
d.label.setPixmap(QPixmap(I(pixmap)))
|
||||||
d.setWindowIcon(QIcon(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())
|
d.resize(d.sizeHint())
|
||||||
return d.exec_() == d.Accepted
|
return d.exec_() == d.Accepted
|
||||||
|
@ -24,7 +24,7 @@ from calibre.ebooks.metadata.meta import get_metadata
|
|||||||
from calibre.gui2 import file_icon_provider, UNDEFINED_QDATE, UNDEFINED_DATE, \
|
from calibre.gui2 import file_icon_provider, UNDEFINED_QDATE, UNDEFINED_DATE, \
|
||||||
choose_files, error_dialog, choose_images, question_dialog
|
choose_files, error_dialog, choose_images, question_dialog
|
||||||
from calibre.utils.date import local_tz, qt_to_dt
|
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.ebooks import BOOK_EXTENSIONS
|
||||||
from calibre.customize.ui import run_plugins_on_import
|
from calibre.customize.ui import run_plugins_on_import
|
||||||
from calibre.utils.date import utcfromtimestamp
|
from calibre.utils.date import utcfromtimestamp
|
||||||
@ -672,12 +672,7 @@ class Cover(ImageView): # {{{
|
|||||||
self.frame_size = (sz.width()//3, sz.height())
|
self.frame_size = (sz.width()//3, sz.height())
|
||||||
|
|
||||||
def sizeHint(self):
|
def sizeHint(self):
|
||||||
sz = ImageView.sizeHint(self)
|
sz = QSize(self.frame_size[0], self.frame_size[1])
|
||||||
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)
|
|
||||||
return sz
|
return sz
|
||||||
|
|
||||||
def select_cover(self, *args):
|
def select_cover(self, *args):
|
||||||
|
@ -295,7 +295,7 @@ def proceed(gui, job):
|
|||||||
_('Failed to download metadata or covers for any of the %d'
|
_('Failed to download metadata or covers for any of the %d'
|
||||||
' book(s).') % len(id_map), det_msg=det_msg)
|
' book(s).') % len(id_map), det_msg=det_msg)
|
||||||
else:
|
else:
|
||||||
fmsg = det_msg = ''
|
fmsg = ''
|
||||||
if failed_ids or failed_covers:
|
if failed_ids or failed_covers:
|
||||||
fmsg = '<p>'+_('Could not download metadata and/or covers for %d of the books. Click'
|
fmsg = '<p>'+_('Could not download metadata and/or covers for %d of the books. Click'
|
||||||
' "Show details" to see which books.')%len(failed_ids)
|
' "Show details" to see which books.')%len(failed_ids)
|
||||||
|
@ -259,6 +259,7 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
|
|||||||
r('wait_after_first_identify_result', msprefs)
|
r('wait_after_first_identify_result', msprefs)
|
||||||
r('wait_after_first_cover_result', msprefs)
|
r('wait_after_first_cover_result', msprefs)
|
||||||
r('swap_author_names', msprefs)
|
r('swap_author_names', msprefs)
|
||||||
|
r('fewer_tags', msprefs)
|
||||||
|
|
||||||
self.configure_plugin_button.clicked.connect(self.configure_plugin)
|
self.configure_plugin_button.clicked.connect(self.configure_plugin)
|
||||||
self.sources_model = SourcesModel(self)
|
self.sources_model = SourcesModel(self)
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
<x>0</x>
|
<x>0</x>
|
||||||
<y>0</y>
|
<y>0</y>
|
||||||
<width>781</width>
|
<width>781</width>
|
||||||
<height>300</height>
|
<height>394</height>
|
||||||
</rect>
|
</rect>
|
||||||
</property>
|
</property>
|
||||||
<property name="windowTitle">
|
<property name="windowTitle">
|
||||||
@ -21,7 +21,7 @@
|
|||||||
<widget class="QStackedWidget" name="stack">
|
<widget class="QStackedWidget" name="stack">
|
||||||
<widget class="QWidget" name="page">
|
<widget class="QWidget" name="page">
|
||||||
<layout class="QGridLayout" name="gridLayout">
|
<layout class="QGridLayout" name="gridLayout">
|
||||||
<item row="0" column="0" rowspan="6">
|
<item row="0" column="0" rowspan="7">
|
||||||
<widget class="QGroupBox" name="groupBox">
|
<widget class="QGroupBox" name="groupBox">
|
||||||
<property name="title">
|
<property name="title">
|
||||||
<string>Metadata sources</string>
|
<string>Metadata sources</string>
|
||||||
@ -105,7 +105,7 @@
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="3" column="1">
|
<item row="4" column="1">
|
||||||
<widget class="QLabel" name="label_2">
|
<widget class="QLabel" name="label_2">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Max. number of &tags to download:</string>
|
<string>Max. number of &tags to download:</string>
|
||||||
@ -115,10 +115,10 @@
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="3" column="2">
|
<item row="4" column="2">
|
||||||
<widget class="QSpinBox" name="opt_max_tags"/>
|
<widget class="QSpinBox" name="opt_max_tags"/>
|
||||||
</item>
|
</item>
|
||||||
<item row="4" column="1">
|
<item row="5" column="1">
|
||||||
<widget class="QLabel" name="label_3">
|
<widget class="QLabel" name="label_3">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Max. &time to wait after first match is found:</string>
|
<string>Max. &time to wait after first match is found:</string>
|
||||||
@ -128,14 +128,14 @@
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="4" column="2">
|
<item row="5" column="2">
|
||||||
<widget class="QSpinBox" name="opt_wait_after_first_identify_result">
|
<widget class="QSpinBox" name="opt_wait_after_first_identify_result">
|
||||||
<property name="suffix">
|
<property name="suffix">
|
||||||
<string> secs</string>
|
<string> secs</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="5" column="1">
|
<item row="6" column="1">
|
||||||
<widget class="QLabel" name="label_4">
|
<widget class="QLabel" name="label_4">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Max. time to wait after first &cover is found:</string>
|
<string>Max. time to wait after first &cover is found:</string>
|
||||||
@ -145,13 +145,24 @@
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="5" column="2">
|
<item row="6" column="2">
|
||||||
<widget class="QSpinBox" name="opt_wait_after_first_cover_result">
|
<widget class="QSpinBox" name="opt_wait_after_first_cover_result">
|
||||||
<property name="suffix">
|
<property name="suffix">
|
||||||
<string> secs</string>
|
<string> secs</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
<item row="3" column="1" colspan="2">
|
||||||
|
<widget class="QCheckBox" name="opt_fewer_tags">
|
||||||
|
<property name="toolTip">
|
||||||
|
<string><p>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.
|
||||||
|
<p>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 &fewer tags</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
<widget class="QWidget" name="page_2"/>
|
<widget class="QWidget" name="page_2"/>
|
||||||
|
@ -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 import ConfigWidgetBase, test_widget
|
||||||
from calibre.gui2.preferences.plugins_ui import Ui_Form
|
from calibre.gui2.preferences.plugins_ui import Ui_Form
|
||||||
from calibre.customize.ui import initialized_plugins, is_disabled, enable_plugin, \
|
from calibre.customize.ui import (initialized_plugins, is_disabled, enable_plugin,
|
||||||
disable_plugin, plugin_customization, add_plugin, \
|
disable_plugin, plugin_customization, add_plugin,
|
||||||
remove_plugin
|
remove_plugin, NameConflict)
|
||||||
from calibre.gui2 import NONE, error_dialog, info_dialog, choose_files, \
|
from calibre.gui2 import NONE, error_dialog, info_dialog, choose_files, \
|
||||||
question_dialog, gprefs
|
question_dialog, gprefs
|
||||||
from calibre.utils.search_query_parser import SearchQueryParser
|
from calibre.utils.search_query_parser import SearchQueryParser
|
||||||
@ -279,7 +279,11 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
|
|||||||
' Are you sure you want to proceed?'),
|
' Are you sure you want to proceed?'),
|
||||||
show_copy_button=False):
|
show_copy_button=False):
|
||||||
return
|
return
|
||||||
plugin = add_plugin(path)
|
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.populate()
|
||||||
self._plugin_model.reset()
|
self._plugin_model.reset()
|
||||||
self.changed_signal.emit()
|
self.changed_signal.emit()
|
||||||
|
@ -76,11 +76,17 @@ class StorePlugin(object): # {{{
|
|||||||
return items as a generator.
|
return items as a generator.
|
||||||
|
|
||||||
Don't be lazy with the search! Load as much data as possible in the
|
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
|
:class:`calibre.gui2.store.search_result.SearchResult` object.
|
||||||
multiple pages to get all of the data then do so. However, if data (such as cover_url)
|
However, if data (such as cover_url)
|
||||||
isn't available because the store does not display cover images then it's okay to
|
isn't available because the store does not display cover images then it's okay to
|
||||||
ignore it.
|
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
|
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
|
an option to include physical books in the search results but this must be
|
||||||
disabled by default.
|
disabled by default.
|
||||||
@ -90,13 +96,34 @@ class StorePlugin(object): # {{{
|
|||||||
|
|
||||||
:param query: The string query search with.
|
:param query: The string query search with.
|
||||||
:param max_results: The maximum number of results to return.
|
: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
|
:return: :class:`calibre.gui2.store.search_result.SearchResult` objects
|
||||||
item_data is plugin specific and is used in :meth:`open` to open to a specifc place in the store.
|
item_data is plugin specific and is used in :meth:`open` to open to a specifc place in the store.
|
||||||
'''
|
'''
|
||||||
raise NotImplementedError()
|
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):
|
def get_settings(self):
|
||||||
'''
|
'''
|
||||||
This is only useful for plugins that implement
|
This is only useful for plugins that implement
|
||||||
|
@ -154,6 +154,13 @@ class AmazonKindleStore(StorePlugin):
|
|||||||
cover_img = data.xpath('//div[@class="productImage"]/a[@href="%s"]/img/@src' % asin_href)
|
cover_img = data.xpath('//div[@class="productImage"]/a[@href="%s"]/img/@src' % asin_href)
|
||||||
if cover_img:
|
if cover_img:
|
||||||
cover_url = cover_img[0]
|
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()'))
|
title = ''.join(data.xpath('div[@class="productTitle"]/a/text()'))
|
||||||
author = ''.join(data.xpath('div[@class="productTitle"]/span[@class="ptBrand"]/text()'))
|
author = ''.join(data.xpath('div[@class="productTitle"]/span[@class="ptBrand"]/text()'))
|
||||||
@ -168,5 +175,23 @@ class AmazonKindleStore(StorePlugin):
|
|||||||
s.author = author.strip()
|
s.author = author.strip()
|
||||||
s.price = price.strip()
|
s.price = price.strip()
|
||||||
s.detail_item = asin.strip()
|
s.detail_item = asin.strip()
|
||||||
|
s.formats = 'Kindle'
|
||||||
|
|
||||||
yield s
|
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
|
||||||
|
|
||||||
|
|
||||||
|
@ -85,5 +85,7 @@ class BaenWebScriptionStore(BasicStoreConfig, StorePlugin):
|
|||||||
s.author = author.strip()
|
s.author = author.strip()
|
||||||
s.price = price
|
s.price = price
|
||||||
s.detail_item = id.strip()
|
s.detail_item = id.strip()
|
||||||
|
s.drm = SearchResult.DRM_UNLOCKED
|
||||||
|
s.formats = 'RB, MOBI, EPUB, LIT, LRF, RTF, HTML'
|
||||||
|
|
||||||
yield s
|
yield s
|
||||||
|
@ -60,14 +60,6 @@ class BeWriteStore(BasicStoreConfig, StorePlugin):
|
|||||||
cover_url = ''
|
cover_url = ''
|
||||||
price = ''
|
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
|
counter -= 1
|
||||||
|
|
||||||
s = SearchResult()
|
s = SearchResult()
|
||||||
@ -76,5 +68,36 @@ class BeWriteStore(BasicStoreConfig, StorePlugin):
|
|||||||
s.author = author.strip()
|
s.author = author.strip()
|
||||||
s.price = price.strip()
|
s.price = price.strip()
|
||||||
s.detail_item = id.strip()
|
s.detail_item = id.strip()
|
||||||
|
s.drm = SearchResult.DRM_UNLOCKED
|
||||||
|
|
||||||
yield s
|
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
|
||||||
|
@ -78,5 +78,7 @@ class BNStore(BasicStoreConfig, StorePlugin):
|
|||||||
s.author = author.strip()
|
s.author = author.strip()
|
||||||
s.price = price
|
s.price = price
|
||||||
s.detail_item = id.strip()
|
s.detail_item = id.strip()
|
||||||
|
s.drm = SearchResult.DRM_UNKNOWN
|
||||||
|
s.formats = 'Nook'
|
||||||
|
|
||||||
yield s
|
yield s
|
||||||
|
@ -75,6 +75,8 @@ class DieselEbooksStore(BasicStoreConfig, StorePlugin):
|
|||||||
if price_elem:
|
if price_elem:
|
||||||
price = price_elem[0]
|
price = price_elem[0]
|
||||||
|
|
||||||
|
formats = ', '.join(data.xpath('.//td[@class="format"]/text()'))
|
||||||
|
|
||||||
counter -= 1
|
counter -= 1
|
||||||
|
|
||||||
s = SearchResult()
|
s = SearchResult()
|
||||||
@ -83,5 +85,18 @@ class DieselEbooksStore(BasicStoreConfig, StorePlugin):
|
|||||||
s.author = author.strip()
|
s.author = author.strip()
|
||||||
s.price = price.strip()
|
s.price = price.strip()
|
||||||
s.detail_item = '/item/' + id.strip()
|
s.detail_item = '/item/' + id.strip()
|
||||||
|
s.formats = formats
|
||||||
|
|
||||||
yield s
|
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
|
||||||
|
@ -7,6 +7,7 @@ __copyright__ = '2011, John Schember <john@nachtimwald.com>'
|
|||||||
__docformat__ = 'restructuredtext en'
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
import random
|
import random
|
||||||
|
import re
|
||||||
import urllib2
|
import urllib2
|
||||||
from contextlib import closing
|
from contextlib import closing
|
||||||
|
|
||||||
@ -64,15 +65,6 @@ class EbookscomStore(BasicStoreConfig, StorePlugin):
|
|||||||
if not id:
|
if not id:
|
||||||
continue
|
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'))
|
cover_url = ''.join(data.xpath('.//img[1]/@src'))
|
||||||
|
|
||||||
title = ''
|
title = ''
|
||||||
@ -89,7 +81,40 @@ class EbookscomStore(BasicStoreConfig, StorePlugin):
|
|||||||
s.cover_url = cover_url
|
s.cover_url = cover_url
|
||||||
s.title = title.strip()
|
s.title = title.strip()
|
||||||
s.author = author.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()
|
s.detail_item = '?url=http://www.ebooks.com/cj.asp?IID=' + id.strip() + '&cjsku=' + id.strip()
|
||||||
|
|
||||||
yield s
|
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
|
||||||
|
@ -7,6 +7,7 @@ __copyright__ = '2011, John Schember <john@nachtimwald.com>'
|
|||||||
__docformat__ = 'restructuredtext en'
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
import random
|
import random
|
||||||
|
import re
|
||||||
import urllib2
|
import urllib2
|
||||||
from contextlib import closing
|
from contextlib import closing
|
||||||
|
|
||||||
@ -76,5 +77,28 @@ class EHarlequinStore(BasicStoreConfig, StorePlugin):
|
|||||||
s.author = author.strip()
|
s.author = author.strip()
|
||||||
s.price = price.strip()
|
s.price = price.strip()
|
||||||
s.detail_item = '?url=http://ebooks.eharlequin.com/' + id.strip()
|
s.detail_item = '?url=http://ebooks.eharlequin.com/' + id.strip()
|
||||||
|
s.formats = 'EPUB'
|
||||||
|
|
||||||
yield s
|
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
|
||||||
|
@ -72,8 +72,10 @@ class FeedbooksStore(BasicStoreConfig, StorePlugin):
|
|||||||
title = ''.join(data.xpath('//h5//a/text()'))
|
title = ''.join(data.xpath('//h5//a/text()'))
|
||||||
author = ''.join(data.xpath('//h6//a/text()'))
|
author = ''.join(data.xpath('//h6//a/text()'))
|
||||||
price = ''.join(data.xpath('//a[@class="buy"]/text()'))
|
price = ''.join(data.xpath('//a[@class="buy"]/text()'))
|
||||||
|
formats = 'EPUB'
|
||||||
if not price:
|
if not price:
|
||||||
price = '$0.00'
|
price = '$0.00'
|
||||||
|
formats = 'EPUB, MOBI, PDF'
|
||||||
cover_url = ''
|
cover_url = ''
|
||||||
cover_url_img = data.xpath('//img')
|
cover_url_img = data.xpath('//img')
|
||||||
if cover_url_img:
|
if cover_url_img:
|
||||||
@ -88,5 +90,18 @@ class FeedbooksStore(BasicStoreConfig, StorePlugin):
|
|||||||
s.author = author.strip()
|
s.author = author.strip()
|
||||||
s.price = price.replace(' ', '').strip()
|
s.price = price.replace(' ', '').strip()
|
||||||
s.detail_item = id.strip()
|
s.detail_item = id.strip()
|
||||||
|
s.formats = formats
|
||||||
|
|
||||||
yield s
|
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
|
||||||
|
@ -79,5 +79,15 @@ class GutenbergStore(BasicStoreConfig, StorePlugin):
|
|||||||
s.author = author.strip()
|
s.author = author.strip()
|
||||||
s.price = price.strip()
|
s.price = price.strip()
|
||||||
s.detail_item = '/ebooks/' + id.strip()
|
s.detail_item = '/ebooks/' + id.strip()
|
||||||
|
s.drm = SearchResult.DRM_UNLOCKED
|
||||||
|
|
||||||
yield s
|
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
|
@ -63,7 +63,7 @@ class KoboStore(BasicStoreConfig, StorePlugin):
|
|||||||
if not id:
|
if not id:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
price = ''.join(data.xpath('.//span[@class="SCOurPrice"]/strong/text()'))
|
price = ''.join(data.xpath('.//li[@class="OurPrice"]/strong/text()'))
|
||||||
if not price:
|
if not price:
|
||||||
price = '$0.00'
|
price = '$0.00'
|
||||||
|
|
||||||
@ -71,6 +71,7 @@ class KoboStore(BasicStoreConfig, StorePlugin):
|
|||||||
|
|
||||||
title = ''.join(data.xpath('.//div[@class="SCItemHeader"]/h1/a[1]/text()'))
|
title = ''.join(data.xpath('.//div[@class="SCItemHeader"]/h1/a[1]/text()'))
|
||||||
author = ''.join(data.xpath('.//div[@class="SCItemSummary"]/span/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
|
counter -= 1
|
||||||
|
|
||||||
@ -80,5 +81,7 @@ class KoboStore(BasicStoreConfig, StorePlugin):
|
|||||||
s.author = author.strip()
|
s.author = author.strip()
|
||||||
s.price = price.strip()
|
s.price = price.strip()
|
||||||
s.detail_item = '?url=http://www.kobobooks.com/' + id.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
|
yield s
|
||||||
|
@ -89,5 +89,7 @@ class ManyBooksStore(BasicStoreConfig, StorePlugin):
|
|||||||
s.author = author.strip()
|
s.author = author.strip()
|
||||||
s.price = price.strip()
|
s.price = price.strip()
|
||||||
s.detail_item = '/titles/' + id
|
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
|
yield s
|
||||||
|
@ -18,19 +18,18 @@ from PyQt4.Qt import Qt, QUrl, QDialog, QAbstractItemModel, QModelIndex, QVarian
|
|||||||
pyqtSignal
|
pyqtSignal
|
||||||
|
|
||||||
from calibre import browser
|
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 import StorePlugin
|
||||||
from calibre.gui2.store.basic_config import BasicStoreConfig
|
from calibre.gui2.store.basic_config import BasicStoreConfig
|
||||||
from calibre.gui2.store.mobileread_store_dialog_ui import Ui_Dialog
|
from calibre.gui2.store.mobileread_store_dialog_ui import Ui_Dialog
|
||||||
from calibre.gui2.store.search_result import SearchResult
|
from calibre.gui2.store.search_result import SearchResult
|
||||||
from calibre.gui2.store.web_store_dialog import WebStoreDialog
|
from calibre.gui2.store.web_store_dialog import WebStoreDialog
|
||||||
from calibre.utils.config import DynamicConfig
|
|
||||||
from calibre.utils.icu import sort_key
|
from calibre.utils.icu import sort_key
|
||||||
|
|
||||||
class MobileReadStore(BasicStoreConfig, StorePlugin):
|
class MobileReadStore(BasicStoreConfig, StorePlugin):
|
||||||
|
|
||||||
def genesis(self):
|
def genesis(self):
|
||||||
self.config = DynamicConfig('store_' + self.name)
|
self.config = JSONConfig('store/store/' + self.name)
|
||||||
self.rlock = RLock()
|
self.rlock = RLock()
|
||||||
|
|
||||||
def open(self, parent=None, detail_item=None, external=False):
|
def open(self, parent=None, detail_item=None, external=False):
|
||||||
@ -76,13 +75,14 @@ class MobileReadStore(BasicStoreConfig, StorePlugin):
|
|||||||
matches = heapq.nlargest(max_results, matches)
|
matches = heapq.nlargest(max_results, matches)
|
||||||
for score, book in matches:
|
for score, book in matches:
|
||||||
book.price = '$0.00'
|
book.price = '$0.00'
|
||||||
|
book.drm = SearchResult.DRM_UNLOCKED
|
||||||
yield book
|
yield book
|
||||||
|
|
||||||
def update_book_list(self, timeout=10):
|
def update_book_list(self, timeout=10):
|
||||||
with self.rlock:
|
with self.rlock:
|
||||||
url = 'http://www.mobileread.com/forums/ebooks.php?do=getlist&type=html'
|
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.
|
# Don't update the book list if our cache is less than one week old.
|
||||||
if last_download and (time.time() - last_download) < 604800:
|
if last_download and (time.time() - last_download) < 604800:
|
||||||
return
|
return
|
||||||
@ -96,15 +96,15 @@ class MobileReadStore(BasicStoreConfig, StorePlugin):
|
|||||||
if not raw_data:
|
if not raw_data:
|
||||||
return
|
return
|
||||||
|
|
||||||
# Turn books listed in the HTML file into BookRef's.
|
# Turn books listed in the HTML file into SearchResults's.
|
||||||
books = []
|
books = []
|
||||||
try:
|
try:
|
||||||
data = html.fromstring(raw_data)
|
data = html.fromstring(raw_data)
|
||||||
for book_data in data.xpath('//ul/li'):
|
for book_data in data.xpath('//ul/li'):
|
||||||
book = BookRef()
|
book = SearchResult()
|
||||||
book.detail_item = ''.join(book_data.xpath('.//a/@href'))
|
book.detail_item = ''.join(book_data.xpath('.//a/@href'))
|
||||||
book.format = ''.join(book_data.xpath('.//i/text()'))
|
book.formats = ''.join(book_data.xpath('.//i/text()'))
|
||||||
book.format = book.format.strip()
|
book.formats = book.formats.strip()
|
||||||
|
|
||||||
text = ''.join(book_data.xpath('.//a/text()'))
|
text = ''.join(book_data.xpath('.//a/text()'))
|
||||||
if ':' in text:
|
if ':' in text:
|
||||||
@ -117,20 +117,34 @@ class MobileReadStore(BasicStoreConfig, StorePlugin):
|
|||||||
|
|
||||||
# Save the book list and it's create time.
|
# Save the book list and it's create time.
|
||||||
if books:
|
if books:
|
||||||
self.config[self.name + '_last_download'] = time.time()
|
self.config['last_download'] = time.time()
|
||||||
self.config[self.name + '_book_list'] = books
|
self.config['book_list'] = self.seralize_books(books)
|
||||||
|
|
||||||
def get_book_list(self, timeout=10):
|
def get_book_list(self, timeout=10):
|
||||||
self.update_book_list(timeout=timeout)
|
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 deseralize_books(self, sbooks):
|
||||||
|
books = []
|
||||||
def __init__(self):
|
for s in sbooks:
|
||||||
SearchResult.__init__(self)
|
b = SearchResult()
|
||||||
|
b.author = s.get('author', '')
|
||||||
self.format = ''
|
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):
|
class MobeReadStoreDialog(QDialog, Ui_Dialog):
|
||||||
@ -159,11 +173,11 @@ class MobeReadStoreDialog(QDialog, Ui_Dialog):
|
|||||||
self.plugin.open(self, result.detail_item)
|
self.plugin.open(self, result.detail_item)
|
||||||
|
|
||||||
def restore_state(self):
|
def restore_state(self):
|
||||||
geometry = self.plugin.config['store_mobileread_dialog_geometry']
|
geometry = self.plugin.config.get('dialog_geometry', None)
|
||||||
if geometry:
|
if geometry:
|
||||||
self.restoreGeometry(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:
|
if results_cwidth:
|
||||||
for i, x in enumerate(results_cwidth):
|
for i, x in enumerate(results_cwidth):
|
||||||
if i >= self.results_view.model().columnCount():
|
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()):
|
for i in xrange(self.results_view.model().columnCount()):
|
||||||
self.results_view.resizeColumnToContents(i)
|
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_col = self.plugin.config.get('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_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.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)
|
self.results_view.header().setSortIndicator(self.results_view.model().sort_col, self.results_view.model().sort_order)
|
||||||
|
|
||||||
def save_state(self):
|
def save_state(self):
|
||||||
self.plugin.config['store_mobileread_dialog_geometry'] = self.saveGeometry()
|
self.plugin.config['dialog_geometry'] = bytearray(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['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['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_sort_order'] = self.results_view.model().sort_order
|
||||||
|
|
||||||
def dialog_closed(self, result):
|
def dialog_closed(self, result):
|
||||||
self.save_state()
|
self.save_state()
|
||||||
@ -223,7 +237,7 @@ class BooksModel(QAbstractItemModel):
|
|||||||
self.books = []
|
self.books = []
|
||||||
if self.filter:
|
if self.filter:
|
||||||
for b in self.all_books:
|
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()
|
test = test.lower()
|
||||||
include = True
|
include = True
|
||||||
for item in self.filter.split(' '):
|
for item in self.filter.split(' '):
|
||||||
@ -276,7 +290,7 @@ class BooksModel(QAbstractItemModel):
|
|||||||
elif col == 1:
|
elif col == 1:
|
||||||
return QVariant(result.author)
|
return QVariant(result.author)
|
||||||
elif col == 2:
|
elif col == 2:
|
||||||
return QVariant(result.format)
|
return QVariant(result.formats)
|
||||||
return NONE
|
return NONE
|
||||||
|
|
||||||
def data_as_text(self, result, col):
|
def data_as_text(self, result, col):
|
||||||
@ -286,7 +300,7 @@ class BooksModel(QAbstractItemModel):
|
|||||||
elif col == 1:
|
elif col == 1:
|
||||||
text = result.author
|
text = result.author
|
||||||
elif col == 2:
|
elif col == 2:
|
||||||
text = result.format
|
text = result.formats
|
||||||
return text
|
return text
|
||||||
|
|
||||||
def sort(self, col, order, reset=True):
|
def sort(self, col, order, reset=True):
|
||||||
|
@ -68,5 +68,15 @@ class OpenLibraryStore(BasicStoreConfig, StorePlugin):
|
|||||||
s.author = author.strip()
|
s.author = author.strip()
|
||||||
s.price = price
|
s.price = price
|
||||||
s.detail_item = id.strip()
|
s.detail_item = id.strip()
|
||||||
|
s.drm = SearchResult.DRM_UNKNOWN
|
||||||
|
|
||||||
yield s
|
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
|
||||||
|
@ -10,6 +10,7 @@ import re
|
|||||||
import time
|
import time
|
||||||
import traceback
|
import traceback
|
||||||
from contextlib import closing
|
from contextlib import closing
|
||||||
|
from operator import attrgetter
|
||||||
from random import shuffle
|
from random import shuffle
|
||||||
from threading import Thread
|
from threading import Thread
|
||||||
from Queue import Queue
|
from Queue import Queue
|
||||||
@ -18,12 +19,12 @@ from PyQt4.Qt import (Qt, QAbstractItemModel, QDialog, QTimer, QVariant,
|
|||||||
QModelIndex, QPixmap, QSize, QCheckBox, QVBoxLayout)
|
QModelIndex, QPixmap, QSize, QCheckBox, QVBoxLayout)
|
||||||
|
|
||||||
from calibre import browser
|
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.progress_indicator import ProgressIndicator
|
||||||
from calibre.gui2.store.search_ui import Ui_Dialog
|
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, \
|
from calibre.library.caches import _match, CONTAINS_MATCH, EQUALS_MATCH, \
|
||||||
REGEXP_MATCH
|
REGEXP_MATCH
|
||||||
from calibre.utils.config import DynamicConfig
|
|
||||||
from calibre.utils.icu import sort_key
|
from calibre.utils.icu import sort_key
|
||||||
from calibre.utils.magick.draw import thumbnail
|
from calibre.utils.magick.draw import thumbnail
|
||||||
from calibre.utils.search_query_parser import SearchQueryParser
|
from calibre.utils.search_query_parser import SearchQueryParser
|
||||||
@ -33,13 +34,21 @@ TIMEOUT = 75 # seconds
|
|||||||
SEARCH_THREAD_TOTAL = 4
|
SEARCH_THREAD_TOTAL = 4
|
||||||
COVER_DOWNLOAD_THREAD_TOTAL = 2
|
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):
|
class SearchDialog(QDialog, Ui_Dialog):
|
||||||
|
|
||||||
def __init__(self, istores, *args):
|
def __init__(self, istores, *args):
|
||||||
QDialog.__init__(self, *args)
|
QDialog.__init__(self, *args)
|
||||||
self.setupUi(self)
|
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.
|
# We keep a cache of store plugins and reference them by name.
|
||||||
self.store_plugins = istores
|
self.store_plugins = istores
|
||||||
@ -87,9 +96,13 @@ class SearchDialog(QDialog, Ui_Dialog):
|
|||||||
# Author
|
# Author
|
||||||
self.results_view.setColumnWidth(2,int(total*.35))
|
self.results_view.setColumnWidth(2,int(total*.35))
|
||||||
# Price
|
# 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
|
# 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):
|
def do_search(self, checked=False):
|
||||||
# Stop all running threads.
|
# Stop all running threads.
|
||||||
@ -102,6 +115,9 @@ class SearchDialog(QDialog, Ui_Dialog):
|
|||||||
query = unicode(self.search_edit.text())
|
query = unicode(self.search_edit.text())
|
||||||
if not query.strip():
|
if not query.strip():
|
||||||
return
|
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
|
# Plugins are in alphebetic order. Randomize the
|
||||||
# order of plugin names. This way plugins closer
|
# order of plugin names. This way plugins closer
|
||||||
@ -110,6 +126,8 @@ class SearchDialog(QDialog, Ui_Dialog):
|
|||||||
store_names = self.store_plugins.keys()
|
store_names = self.store_plugins.keys()
|
||||||
if not store_names:
|
if not store_names:
|
||||||
return
|
return
|
||||||
|
# Remove all of our internal filtering logic from the query.
|
||||||
|
query = self.clean_query(query)
|
||||||
shuffle(store_names)
|
shuffle(store_names)
|
||||||
# Add plugins that the user has checked to the search pool's work queue.
|
# Add plugins that the user has checked to the search pool's work queue.
|
||||||
for n in store_names:
|
for n in store_names:
|
||||||
@ -121,9 +139,32 @@ class SearchDialog(QDialog, Ui_Dialog):
|
|||||||
self.search_pool.start_threads()
|
self.search_pool.start_threads()
|
||||||
self.pi.startAnimation()
|
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):
|
def save_state(self):
|
||||||
self.config['store_search_geometry'] = self.saveGeometry()
|
self.config['store_search_geometry'] = bytearray(self.saveGeometry())
|
||||||
self.config['store_search_store_splitter_state'] = self.store_splitter.saveState()
|
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())]
|
self.config['store_search_results_view_column_width'] = [self.results_view.columnWidth(i) for i in range(self.model.columnCount())]
|
||||||
|
|
||||||
store_check = {}
|
store_check = {}
|
||||||
@ -132,15 +173,15 @@ class SearchDialog(QDialog, Ui_Dialog):
|
|||||||
self.config['store_search_store_checked'] = store_check
|
self.config['store_search_store_checked'] = store_check
|
||||||
|
|
||||||
def restore_state(self):
|
def restore_state(self):
|
||||||
geometry = self.config['store_search_geometry']
|
geometry = self.config.get('store_search_geometry', None)
|
||||||
if geometry:
|
if geometry:
|
||||||
self.restoreGeometry(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:
|
if splitter_state:
|
||||||
self.store_splitter.restoreState(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:
|
if results_cwidth:
|
||||||
for i, x in enumerate(results_cwidth):
|
for i, x in enumerate(results_cwidth):
|
||||||
if i >= self.model.columnCount():
|
if i >= self.model.columnCount():
|
||||||
@ -149,7 +190,7 @@ class SearchDialog(QDialog, Ui_Dialog):
|
|||||||
else:
|
else:
|
||||||
self.resize_columns()
|
self.resize_columns()
|
||||||
|
|
||||||
store_check = self.config['store_search_store_checked']
|
store_check = self.config.get('store_search_store_checked', None)
|
||||||
if store_check:
|
if store_check:
|
||||||
for n in store_check:
|
for n in store_check:
|
||||||
if hasattr(self, 'store_check_' + n):
|
if hasattr(self, 'store_check_' + n):
|
||||||
@ -170,9 +211,9 @@ class SearchDialog(QDialog, Ui_Dialog):
|
|||||||
self.pi.stopAnimation()
|
self.pi.stopAnimation()
|
||||||
|
|
||||||
while self.search_pool.has_results():
|
while self.search_pool.has_results():
|
||||||
res = self.search_pool.get_result()
|
res, store_plugin = self.search_pool.get_result()
|
||||||
if res:
|
if res:
|
||||||
self.results_view.model().add_result(res)
|
self.results_view.model().add_result(res, store_plugin)
|
||||||
|
|
||||||
def open_store(self, index):
|
def open_store(self, index):
|
||||||
result = self.results_view.model().get_result(index)
|
result = self.results_view.model().get_result(index)
|
||||||
@ -294,18 +335,14 @@ class SearchThread(Thread):
|
|||||||
while self._run and not self.tasks.empty():
|
while self._run and not self.tasks.empty():
|
||||||
try:
|
try:
|
||||||
query, store_name, store_plugin, timeout = self.tasks.get()
|
query, store_name, store_plugin, timeout = self.tasks.get()
|
||||||
squery = query
|
for res in store_plugin.search(query, timeout=timeout):
|
||||||
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):
|
|
||||||
if not self._run:
|
if not self._run:
|
||||||
return
|
return
|
||||||
res.store_name = store_name
|
res.store_name = store_name
|
||||||
if SearchFilter(res).parse(query):
|
self.results.put((res, store_plugin))
|
||||||
self.results.put(res)
|
|
||||||
self.tasks.task_done()
|
self.tasks.task_done()
|
||||||
except:
|
except:
|
||||||
pass
|
traceback.print_exc()
|
||||||
|
|
||||||
|
|
||||||
class CoverThreadPool(GenericDownloadThreadPool):
|
class CoverThreadPool(GenericDownloadThreadPool):
|
||||||
@ -349,30 +386,98 @@ class CoverThread(Thread):
|
|||||||
continue
|
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):
|
class Matches(QAbstractItemModel):
|
||||||
|
|
||||||
HEADERS = [_('Cover'), _('Title'), _('Author(s)'), _('Price'), _('Store')]
|
HEADERS = [_('Cover'), _('Title'), _('Author(s)'), _('Price'), _('DRM'), _('Store'), _('Formats')]
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
QAbstractItemModel.__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.matches = []
|
||||||
|
self.query = ''
|
||||||
|
self.search_filter = SearchFilter()
|
||||||
self.cover_pool = CoverThreadPool(CoverThread, 2)
|
self.cover_pool = CoverThreadPool(CoverThread, 2)
|
||||||
self.cover_pool.start_threads()
|
self.cover_pool.start_threads()
|
||||||
|
self.details_pool = DetailsThreadPool(DetailsThread, 4)
|
||||||
|
self.details_pool.start_threads()
|
||||||
|
|
||||||
def closing(self):
|
def closing(self):
|
||||||
self.cover_pool.abort()
|
self.cover_pool.abort()
|
||||||
|
self.details_pool.abort()
|
||||||
|
|
||||||
def clear_results(self):
|
def clear_results(self):
|
||||||
|
self.all_matches = []
|
||||||
self.matches = []
|
self.matches = []
|
||||||
|
self.all_matches = []
|
||||||
|
self.search_filter.clear_search_results()
|
||||||
|
self.query = ''
|
||||||
self.cover_pool.abort()
|
self.cover_pool.abort()
|
||||||
self.cover_pool.start_threads()
|
self.cover_pool.start_threads()
|
||||||
|
self.details_pool.abort()
|
||||||
|
self.details_pool.start_threads()
|
||||||
self.reset()
|
self.reset()
|
||||||
|
|
||||||
def add_result(self, result):
|
def add_result(self, result, store_plugin):
|
||||||
self.layoutAboutToBeChanged.emit()
|
if result not in self.all_matches:
|
||||||
self.matches.append(result)
|
self.layoutAboutToBeChanged.emit()
|
||||||
self.cover_pool.add_task(result, self.update_result)
|
self.all_matches.append(result)
|
||||||
self.layoutChanged.emit()
|
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):
|
def get_result(self, index):
|
||||||
row = index.row()
|
row = index.row()
|
||||||
@ -381,10 +486,29 @@ class Matches(QAbstractItemModel):
|
|||||||
else:
|
else:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def update_result(self):
|
def filter_results(self):
|
||||||
self.layoutAboutToBeChanged.emit()
|
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()
|
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()):
|
def index(self, row, column, parent=QModelIndex()):
|
||||||
return self.createIndex(row, column)
|
return self.createIndex(row, column)
|
||||||
|
|
||||||
@ -420,14 +544,41 @@ class Matches(QAbstractItemModel):
|
|||||||
return QVariant(result.author)
|
return QVariant(result.author)
|
||||||
elif col == 3:
|
elif col == 3:
|
||||||
return QVariant(result.price)
|
return QVariant(result.price)
|
||||||
elif col == 4:
|
elif col == 5:
|
||||||
return QVariant(result.store_name)
|
return QVariant(result.store_name)
|
||||||
|
elif col == 6:
|
||||||
|
return QVariant(result.formats)
|
||||||
return NONE
|
return NONE
|
||||||
elif role == Qt.DecorationRole:
|
elif role == Qt.DecorationRole:
|
||||||
if col == 0 and result.cover_data:
|
if col == 0 and result.cover_data:
|
||||||
p = QPixmap()
|
p = QPixmap()
|
||||||
p.loadFromData(result.cover_data)
|
p.loadFromData(result.cover_data)
|
||||||
return QVariant(p)
|
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:
|
elif role == Qt.SizeHintRole:
|
||||||
return QSize(64, 64)
|
return QSize(64, 64)
|
||||||
return NONE
|
return NONE
|
||||||
@ -439,25 +590,34 @@ class Matches(QAbstractItemModel):
|
|||||||
elif col == 2:
|
elif col == 2:
|
||||||
text = result.author
|
text = result.author
|
||||||
elif col == 3:
|
elif col == 3:
|
||||||
text = result.price
|
text = comparable_price(result.price)
|
||||||
if len(text) < 3 or text[-3] not in ('.', ','):
|
|
||||||
text += '00'
|
|
||||||
text = re.sub(r'\D', '', text)
|
|
||||||
text = text.rjust(6, '0')
|
|
||||||
elif col == 4:
|
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
|
text = result.store_name
|
||||||
|
elif col == 6:
|
||||||
|
text = ', '.join(sorted(result.formats.split(',')))
|
||||||
return text
|
return text
|
||||||
|
|
||||||
def sort(self, col, order, reset=True):
|
def sort(self, col, order, reset=True):
|
||||||
if not self.matches:
|
if not self.matches:
|
||||||
return
|
return
|
||||||
descending = order == Qt.DescendingOrder
|
descending = order == Qt.DescendingOrder
|
||||||
self.matches.sort(None,
|
self.all_matches.sort(None,
|
||||||
lambda x: sort_key(unicode(self.data_as_text(x, col))),
|
lambda x: sort_key(unicode(self.data_as_text(x, col))),
|
||||||
descending)
|
descending)
|
||||||
|
self.reorder_matches()
|
||||||
if reset:
|
if reset:
|
||||||
self.reset()
|
self.reset()
|
||||||
|
|
||||||
|
def reorder_matches(self):
|
||||||
|
self.matches = sorted(self.matches, key=lambda x: self.all_matches.index(x))
|
||||||
|
|
||||||
|
|
||||||
class SearchFilter(SearchQueryParser):
|
class SearchFilter(SearchQueryParser):
|
||||||
|
|
||||||
@ -466,22 +626,33 @@ class SearchFilter(SearchQueryParser):
|
|||||||
'author',
|
'author',
|
||||||
'authors',
|
'authors',
|
||||||
'cover',
|
'cover',
|
||||||
|
'drm',
|
||||||
|
'format',
|
||||||
|
'formats',
|
||||||
'price',
|
'price',
|
||||||
'title',
|
'title',
|
||||||
'store',
|
'store',
|
||||||
]
|
]
|
||||||
|
|
||||||
def __init__(self, search_result):
|
def __init__(self):
|
||||||
SearchQueryParser.__init__(self, locations=self.USABLE_LOCATIONS)
|
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):
|
def universal_set(self):
|
||||||
return set([self.search_result])
|
return self.srs
|
||||||
|
|
||||||
def get_matches(self, location, query):
|
def get_matches(self, location, query):
|
||||||
location = location.lower().strip()
|
location = location.lower().strip()
|
||||||
if location == 'authors':
|
if location == 'authors':
|
||||||
location = 'author'
|
location = 'author'
|
||||||
|
elif location == 'formats':
|
||||||
|
location = 'format'
|
||||||
|
|
||||||
matchkind = CONTAINS_MATCH
|
matchkind = CONTAINS_MATCH
|
||||||
if len(query) > 1:
|
if len(query) > 1:
|
||||||
@ -502,38 +673,54 @@ class SearchFilter(SearchQueryParser):
|
|||||||
all_locs = set(self.USABLE_LOCATIONS) - set(['all'])
|
all_locs = set(self.USABLE_LOCATIONS) - set(['all'])
|
||||||
locations = all_locs if location == 'all' else [location]
|
locations = all_locs if location == 'all' else [location]
|
||||||
q = {
|
q = {
|
||||||
'author': self.search_result.author.lower(),
|
'author': lambda x: x.author.lower(),
|
||||||
'cover': self.search_result.cover_url,
|
'cover': attrgetter('cover_url'),
|
||||||
'format': '',
|
'drm': attrgetter('drm'),
|
||||||
'price': self.search_result.price,
|
'format': attrgetter('formats'),
|
||||||
'store': self.search_result.store_name.lower(),
|
'price': lambda x: comparable_price(x.price),
|
||||||
'title': self.search_result.title.lower(),
|
'store': lambda x: x.store_name.lower(),
|
||||||
|
'title': lambda x: x.title.lower(),
|
||||||
}
|
}
|
||||||
for x in ('author', 'format'):
|
for x in ('author', 'format'):
|
||||||
q[x+'s'] = q[x]
|
q[x+'s'] = q[x]
|
||||||
for locvalue in locations:
|
for sr in self.srs:
|
||||||
ac_val = q[locvalue]
|
for locvalue in locations:
|
||||||
if query == 'true':
|
accessor = q[locvalue]
|
||||||
if ac_val is not None:
|
if query == 'true':
|
||||||
matches.add(self.search_result)
|
if locvalue == 'drm':
|
||||||
continue
|
if accessor(sr) == SearchResult.DRM_LOCKED:
|
||||||
if query == 'false':
|
matches.add(sr)
|
||||||
if ac_val is None:
|
else:
|
||||||
matches.add(self.search_result)
|
if accessor(sr) is not None:
|
||||||
continue
|
matches.add(sr)
|
||||||
try:
|
continue
|
||||||
### Can't separate authors because comma is used for name sep and author sep
|
if query == 'false':
|
||||||
### Exact match might not get what you want. For that reason, turn author
|
if locvalue == 'drm':
|
||||||
### exactmatch searches into contains searches.
|
if accessor(sr) == SearchResult.DRM_UNLOCKED:
|
||||||
if locvalue == 'author' and matchkind == EQUALS_MATCH:
|
matches.add(sr)
|
||||||
m = CONTAINS_MATCH
|
else:
|
||||||
else:
|
if accessor(sr) is None:
|
||||||
m = matchkind
|
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
|
||||||
|
|
||||||
vals = [ac_val]
|
if locvalue == 'format':
|
||||||
if _match(query, vals, m):
|
vals = accessor(sr).split(',')
|
||||||
matches.add(self.search_result)
|
else:
|
||||||
break
|
vals = [accessor(sr)]
|
||||||
except ValueError: # Unicode errors
|
if _match(query, vals, m):
|
||||||
traceback.print_exc()
|
matches.add(sr)
|
||||||
|
break
|
||||||
|
except ValueError: # Unicode errors
|
||||||
|
traceback.print_exc()
|
||||||
return matches
|
return matches
|
||||||
|
@ -11,7 +11,11 @@
|
|||||||
</rect>
|
</rect>
|
||||||
</property>
|
</property>
|
||||||
<property name="windowTitle">
|
<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>
|
||||||
<property name="sizeGripEnabled">
|
<property name="sizeGripEnabled">
|
||||||
<bool>true</bool>
|
<bool>true</bool>
|
||||||
@ -58,8 +62,8 @@
|
|||||||
<rect>
|
<rect>
|
||||||
<x>0</x>
|
<x>0</x>
|
||||||
<y>0</y>
|
<y>0</y>
|
||||||
<width>215</width>
|
<width>170</width>
|
||||||
<height>116</height>
|
<height>138</height>
|
||||||
</rect>
|
</rect>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
@ -174,7 +178,9 @@
|
|||||||
</item>
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
<resources/>
|
<resources>
|
||||||
|
<include location="../../../../resources/images.qrc"/>
|
||||||
|
</resources>
|
||||||
<connections>
|
<connections>
|
||||||
<connection>
|
<connection>
|
||||||
<sender>close</sender>
|
<sender>close</sender>
|
||||||
|
@ -8,6 +8,10 @@ __docformat__ = 'restructuredtext en'
|
|||||||
|
|
||||||
class SearchResult(object):
|
class SearchResult(object):
|
||||||
|
|
||||||
|
DRM_LOCKED = 1
|
||||||
|
DRM_UNLOCKED = 2
|
||||||
|
DRM_UNKNOWN = 3
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.store_name = ''
|
self.store_name = ''
|
||||||
self.cover_url = ''
|
self.cover_url = ''
|
||||||
@ -16,3 +20,8 @@ class SearchResult(object):
|
|||||||
self.author = ''
|
self.author = ''
|
||||||
self.price = ''
|
self.price = ''
|
||||||
self.detail_item = ''
|
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
|
||||||
|
@ -90,5 +90,15 @@ class SmashwordsStore(BasicStoreConfig, StorePlugin):
|
|||||||
s.author = author.strip()
|
s.author = author.strip()
|
||||||
s.price = price.strip()
|
s.price = price.strip()
|
||||||
s.detail_item = '/books/view/' + id.strip()
|
s.detail_item = '/books/view/' + id.strip()
|
||||||
|
s.drm = SearchResult.DRM_UNLOCKED
|
||||||
|
|
||||||
yield s
|
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
|
||||||
|
@ -68,6 +68,19 @@ class NPWebView(QWebView):
|
|||||||
filename = get_download_filename(url, cf)
|
filename = get_download_filename(url, cf)
|
||||||
ext = os.path.splitext(filename)[1][1:].lower()
|
ext = os.path.splitext(filename)[1][1:].lower()
|
||||||
if ext not in BOOK_EXTENSIONS:
|
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('~')
|
home = os.path.expanduser('~')
|
||||||
name = QFileDialog.getSaveFileName(self,
|
name = QFileDialog.getSaveFileName(self,
|
||||||
_('File is not a supported ebook type. Save to disk?'),
|
_('File is not a supported ebook type. Save to disk?'),
|
||||||
|
@ -35,7 +35,7 @@ category_icon_map = {
|
|||||||
'custom:' : 'column.png',
|
'custom:' : 'column.png',
|
||||||
'user:' : 'tb_folder.png',
|
'user:' : 'tb_folder.png',
|
||||||
'search' : 'search.png',
|
'search' : 'search.png',
|
||||||
'identifiers': 'id_card.png'
|
'identifiers': 'identifiers.png'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -61,6 +61,12 @@ if not _run_once:
|
|||||||
|
|
||||||
################################################################################
|
################################################################################
|
||||||
# Initialize locale
|
# 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:
|
try:
|
||||||
locale.setlocale(locale.LC_ALL, '')
|
locale.setlocale(locale.LC_ALL, '')
|
||||||
except:
|
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
Loading…
x
Reference in New Issue
Block a user