mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
KG updates
This commit is contained in:
commit
ba76de5a9a
@ -1,6 +1,6 @@
|
|||||||
|
|
||||||
__license__ = 'GPL v3'
|
__license__ = 'GPL v3'
|
||||||
__copyright__ = '2008-2010, Darko Miletic <darko.miletic at gmail.com>'
|
__copyright__ = '2008-2011, Darko Miletic <darko.miletic at gmail.com>'
|
||||||
'''
|
'''
|
||||||
clarin.com
|
clarin.com
|
||||||
'''
|
'''
|
||||||
@ -18,11 +18,18 @@ class Clarin(BasicNewsRecipe):
|
|||||||
use_embedded_content = False
|
use_embedded_content = False
|
||||||
no_stylesheets = True
|
no_stylesheets = True
|
||||||
encoding = 'utf8'
|
encoding = 'utf8'
|
||||||
|
delay = 1
|
||||||
language = 'es_AR'
|
language = 'es_AR'
|
||||||
publication_type = 'newspaper'
|
publication_type = 'newspaper'
|
||||||
INDEX = 'http://www.clarin.com'
|
INDEX = 'http://www.clarin.com'
|
||||||
masthead_url = 'http://www.clarin.com/static/CLAClarin/images/logo-clarin-print.jpg'
|
masthead_url = 'http://www.clarin.com/static/CLAClarin/images/logo-clarin-print.jpg'
|
||||||
extra_css = ' body{font-family: Arial,Helvetica,sans-serif} h2{font-family: Georgia,serif; font-size: xx-large} .hora{font-weight:bold} .hd p{font-size: small} .nombre-autor{color: #0F325A} '
|
extra_css = """
|
||||||
|
body{font-family: Arial,Helvetica,sans-serif}
|
||||||
|
h2{font-family: Georgia,serif; font-size: xx-large}
|
||||||
|
.hora{font-weight:bold}
|
||||||
|
.hd p{font-size: small}
|
||||||
|
.nombre-autor{color: #0F325A}
|
||||||
|
"""
|
||||||
|
|
||||||
conversion_options = {
|
conversion_options = {
|
||||||
'comment' : description
|
'comment' : description
|
||||||
@ -32,6 +39,8 @@ class Clarin(BasicNewsRecipe):
|
|||||||
}
|
}
|
||||||
|
|
||||||
keep_only_tags = [dict(attrs={'class':['hd','mt']})]
|
keep_only_tags = [dict(attrs={'class':['hd','mt']})]
|
||||||
|
remove_tags = [dict(name=['meta','base','link'])]
|
||||||
|
remove_attributes = ['lang','_mce_bogus']
|
||||||
|
|
||||||
feeds = [
|
feeds = [
|
||||||
(u'Pagina principal', u'http://www.clarin.com/rss/' )
|
(u'Pagina principal', u'http://www.clarin.com/rss/' )
|
||||||
@ -47,6 +56,10 @@ class Clarin(BasicNewsRecipe):
|
|||||||
,(u'Ciudades' , u'http://www.clarin.com/rss/ciudades/' )
|
,(u'Ciudades' , u'http://www.clarin.com/rss/ciudades/' )
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def get_article_url(self, article):
|
||||||
|
return article.get('guid', None)
|
||||||
|
|
||||||
def print_version(self, url):
|
def print_version(self, url):
|
||||||
return url + '?print=1'
|
return url + '?print=1'
|
||||||
|
|
||||||
|
@ -53,6 +53,7 @@ class FinancialTimes(BasicNewsRecipe):
|
|||||||
feeds = [
|
feeds = [
|
||||||
(u'UK' , u'http://www.ft.com/rss/home/uk' )
|
(u'UK' , u'http://www.ft.com/rss/home/uk' )
|
||||||
,(u'US' , u'http://www.ft.com/rss/home/us' )
|
,(u'US' , u'http://www.ft.com/rss/home/us' )
|
||||||
|
,(u'Europe' , u'http://www.ft.com/rss/home/europe' )
|
||||||
,(u'Asia' , u'http://www.ft.com/rss/home/asia' )
|
,(u'Asia' , u'http://www.ft.com/rss/home/asia' )
|
||||||
,(u'Middle East', u'http://www.ft.com/rss/home/middleeast')
|
,(u'Middle East', u'http://www.ft.com/rss/home/middleeast')
|
||||||
]
|
]
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
__license__ = 'GPL v3'
|
__license__ = 'GPL v3'
|
||||||
__copyright__ = '2009-2010, Darko Miletic <darko.miletic at gmail.com>'
|
__copyright__ = '2009-2011, Darko Miletic <darko.miletic at gmail.com>'
|
||||||
'''
|
'''
|
||||||
staradvertiser.com
|
staradvertiser.com
|
||||||
'''
|
'''
|
||||||
@ -9,7 +9,7 @@ from calibre.web.feeds.news import BasicNewsRecipe
|
|||||||
class Starbulletin(BasicNewsRecipe):
|
class Starbulletin(BasicNewsRecipe):
|
||||||
title = 'Honolulu Star Advertiser'
|
title = 'Honolulu Star Advertiser'
|
||||||
__author__ = 'Darko Miletic'
|
__author__ = 'Darko Miletic'
|
||||||
description = "Latest national and local Hawaii sports news"
|
description = 'Latest national and local Hawaii sports news'
|
||||||
publisher = 'Honolulu Star-Advertiser'
|
publisher = 'Honolulu Star-Advertiser'
|
||||||
category = 'news, Honolulu, Hawaii'
|
category = 'news, Honolulu, Hawaii'
|
||||||
oldest_article = 2
|
oldest_article = 2
|
||||||
@ -19,7 +19,13 @@ class Starbulletin(BasicNewsRecipe):
|
|||||||
use_embedded_content = False
|
use_embedded_content = False
|
||||||
encoding = 'utf8'
|
encoding = 'utf8'
|
||||||
publication_type = 'newspaper'
|
publication_type = 'newspaper'
|
||||||
extra_css = ' body{font-family: Verdana,Arial,Helvetica,sans-serif} h1,.brown,.postCredit{color: #663300} .storyDeck{font-size: 1.2em; font-weight: bold} '
|
masthead_url = 'http://media.staradvertiser.com/designimages/star-advertiser-logo-small.gif'
|
||||||
|
extra_css = """
|
||||||
|
body{font-family: Verdana,Arial,Helvetica,sans-serif}
|
||||||
|
h1,.brown,.postCredit{color: #663300}
|
||||||
|
.storyDeck{font-size: 1.2em; font-weight: bold}
|
||||||
|
img{display: block}
|
||||||
|
"""
|
||||||
|
|
||||||
conversion_options = {
|
conversion_options = {
|
||||||
'comment' : description
|
'comment' : description
|
||||||
@ -28,15 +34,17 @@ class Starbulletin(BasicNewsRecipe):
|
|||||||
, 'language' : language
|
, 'language' : language
|
||||||
, 'linearize_tables' : True
|
, 'linearize_tables' : True
|
||||||
}
|
}
|
||||||
|
keep_only_tags = [
|
||||||
remove_tags_before = dict(attrs={'id':'storyTitle'})
|
dict(attrs={'id':'storyTitle'})
|
||||||
remove_tags_after = dict(name='div',attrs={'class':'storytext'})
|
,dict(attrs={'class':['storyDeck','postCredit']})
|
||||||
remove_tags = [
|
,dict(name='span',attrs={'class':'brown'})
|
||||||
dict(name=['object','link','script','span'])
|
|
||||||
,dict(attrs={'class':'insideStoryImage'})
|
|
||||||
,dict(attrs={'name':'fb_share'})
|
|
||||||
,dict(name='div',attrs={'class':'storytext'})
|
,dict(name='div',attrs={'class':'storytext'})
|
||||||
]
|
]
|
||||||
|
remove_tags = [
|
||||||
|
dict(name=['object','link','script','span','meta','base','iframe'])
|
||||||
|
,dict(attrs={'class':['insideStoryImage','insideStoryAd']})
|
||||||
|
,dict(attrs={'name':'fb_share'})
|
||||||
|
]
|
||||||
|
|
||||||
feeds = [
|
feeds = [
|
||||||
(u'Headlines' , u'http://www.staradvertiser.com/staradvertiser_headlines.rss' )
|
(u'Headlines' , u'http://www.staradvertiser.com/staradvertiser_headlines.rss' )
|
||||||
@ -47,3 +55,24 @@ class Starbulletin(BasicNewsRecipe):
|
|||||||
,(u'Business' , u'http://www.staradvertiser.com/business/index.rss' )
|
,(u'Business' , u'http://www.staradvertiser.com/business/index.rss' )
|
||||||
,(u'Travel' , u'http://www.staradvertiser.com/travel/index.rss' )
|
,(u'Travel' , u'http://www.staradvertiser.com/travel/index.rss' )
|
||||||
]
|
]
|
||||||
|
|
||||||
|
def preprocess_html(self, soup):
|
||||||
|
for item in soup.findAll(style=True):
|
||||||
|
del item['style']
|
||||||
|
for item in soup.findAll('a'):
|
||||||
|
limg = item.find('img')
|
||||||
|
if item.string is not None:
|
||||||
|
str = item.string
|
||||||
|
item.replaceWith(str)
|
||||||
|
else:
|
||||||
|
if limg:
|
||||||
|
item.name = 'div'
|
||||||
|
item.attrs = []
|
||||||
|
else:
|
||||||
|
str = self.tag_to_string(item)
|
||||||
|
item.replaceWith(str)
|
||||||
|
for item in soup.findAll('img'):
|
||||||
|
if not item.has_key('alt'):
|
||||||
|
item['alt'] = 'image'
|
||||||
|
return soup
|
||||||
|
|
@ -266,26 +266,6 @@ max_content_server_tags_shown=5
|
|||||||
content_server_will_display = ['*']
|
content_server_will_display = ['*']
|
||||||
content_server_wont_display = []
|
content_server_wont_display = []
|
||||||
|
|
||||||
#: Set custom metadata fields that the book details panel will or will not display.
|
|
||||||
# book_details_will_display is a list of custom fields to be displayed.
|
|
||||||
# book_details_wont_display is a list of custom fields not to be displayed.
|
|
||||||
# wont_display has priority over will_display.
|
|
||||||
# The special value '*' means all custom fields. The value [] means no entries.
|
|
||||||
# Defaults:
|
|
||||||
# book_details_will_display = ['*']
|
|
||||||
# book_details_wont_display = []
|
|
||||||
# Examples:
|
|
||||||
# To display only the custom fields #mytags and #genre:
|
|
||||||
# book_details_will_display = ['#mytags', '#genre']
|
|
||||||
# book_details_wont_display = []
|
|
||||||
# To display all fields except #mycomments:
|
|
||||||
# book_details_will_display = ['*']
|
|
||||||
# book_details_wont_display['#mycomments']
|
|
||||||
# As above, this tweak affects only display of custom fields. The standard
|
|
||||||
# fields are not affected
|
|
||||||
book_details_will_display = ['*']
|
|
||||||
book_details_wont_display = []
|
|
||||||
|
|
||||||
#: Set the maximum number of sort 'levels'
|
#: Set the maximum number of sort 'levels'
|
||||||
# Set the maximum number of sort 'levels' that calibre will use to resort the
|
# Set the maximum number of sort 'levels' that calibre will use to resort the
|
||||||
# library after certain operations such as searches or device insertion. Each
|
# library after certain operations such as searches or device insertion. Each
|
||||||
|
Binary file not shown.
41
resources/templates/book_details.css
Normal file
41
resources/templates/book_details.css
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
a {
|
||||||
|
text-decoration: none;
|
||||||
|
color: blue
|
||||||
|
}
|
||||||
|
.comments {
|
||||||
|
margin-top: 0;
|
||||||
|
padding-top: 0;
|
||||||
|
text-indent: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
table.fields {
|
||||||
|
margin-bottom: 0;
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
table.fields td {
|
||||||
|
vertical-align: top
|
||||||
|
}
|
||||||
|
|
||||||
|
table.fields td.title {
|
||||||
|
font-weight: bold
|
||||||
|
}
|
||||||
|
|
||||||
|
.series_name {
|
||||||
|
font-style: italic
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
The HTML that this styleshhet applies to looks like this:
|
||||||
|
|
||||||
|
<table class="fields">
|
||||||
|
<tr id="formats" class="datatype_text"><td class="title">Formats:</td><td><a href="format:572:EPUB">EPUB</a>, <a href="format:572:LIT">LIT</a></td></tr>
|
||||||
|
<tr id="series" class="datatype_series"><td class="title">Series:</td><td>Book II of <span class="series_name">The Sea Beggars</span></td></tr>
|
||||||
|
<tr id="tags" class="datatype_text"><td class="title">Tags:</td><td>Fantasy, Fiction</td></tr>
|
||||||
|
<tr id="path" class="datatype_text"><td class="title">Path:</td><td><a href="path:572" title="/home/kovid/test library/Paul Kearney/This Forsaken Earth (572)">Click to open</a></td></tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<div id="comments" class="comments"><h3>From Publishers Weekly</h3><p>At the start of Kearney's rousing sequel to <em>The Mark of Ran</em> (2005), Rol Cortishane, the youthful captain of the privateer <em>Revenant</em>, captures a slaver and frees its chained slaves. Back in the harbor of Ganesh Ka in the land of Umer, Rol encounters an untrustworthy acquaintance he hasn't seen in years, Canker, a former king of thieves, who urges Rol to join in the fight to save Rowen, a darkly beautiful queen, whose throne is at risk in mountainous Bionar. That Rowen is Rol's half-sister for whom he has lusted in the past doesn't make Rol's decision to help an easy one. If as in <em>The Mark of Ran</em> the action is more lively at sea than on land, Kearney's solid storytelling and nautical detail worthy of C.S. Forester or Patrick O'Brian will keep readers turning the pages. <em>(Dec.)</em> <br />Copyright © Reed Business Information, a division of Reed Elsevier Inc. All rights reserved. </p><h3>From</h3><p>The sequel to <em>The Mark of Ran</em> (2005) finds heroic young Rol Cortishane grown to be a much-feared sea captain. Deciding to ignore his mysterious past, he spends his energy on ship and crew. He is still an outlaw, however, and the only port he can call home is Ganesh Ka, the endangered city of exiles. When word comes from Rowan, his half-sister, asking him to fight on her behalf, he must weigh the safety of Ganesh Ka against Rowan's treachery in the past. Finally persuaded to aid Rowan, he learns more of betrayal and his heritage in the ensuing battles than he had wanted to know. Kearney's characters are much better developed here than they were in <em>The Mark of Ran</em>, and since the book tells a single story, the plot is tighter. Moreover, because almost all the action transpires in the here and now, the sequel can be read without reference to the predecessor. Since it ends hanging on a particularly bloody cliff, expect to see more of Kearney's excellent maritime fantasy. <em>Frieda Murray</em><br /><em>Copyright © American Library Association. All rights reserved</em></p>
|
||||||
|
</div>
|
||||||
|
*/
|
||||||
|
|
@ -69,7 +69,24 @@ nmake -f ms\ntdll.mak install
|
|||||||
Qt
|
Qt
|
||||||
--------
|
--------
|
||||||
|
|
||||||
Extract Qt sourcecode to C:\Qt\4.x.x. Run configure and make::
|
Extract Qt sourcecode to C:\Qt\4.x.x.
|
||||||
|
|
||||||
|
Qt uses its own routine to locate and load "system libraries" including the openssl libraries needed for "Get Books". This means that we have to apply the following patch to have Qt load the openssl libraries bundled with calibre:
|
||||||
|
|
||||||
|
|
||||||
|
--- src/corelib/plugin/qsystemlibrary.cpp 2011-02-22 05:04:00.000000000 -0700
|
||||||
|
+++ src/corelib/plugin/qsystemlibrary.cpp 2011-04-25 20:53:13.635247466 -0600
|
||||||
|
@@ -110,7 +110,7 @@ HINSTANCE QSystemLibrary::load(const wch
|
||||||
|
|
||||||
|
#if !defined(QT_BOOTSTRAPPED)
|
||||||
|
if (!onlySystemDirectory)
|
||||||
|
- searchOrder << QFileInfo(qAppFileName()).path();
|
||||||
|
+ searchOrder << (QFileInfo(qAppFileName()).path().replace(QLatin1Char('/'), QLatin1Char('\\')) + QString::fromLatin1("\\DLLs\\"));
|
||||||
|
#endif
|
||||||
|
searchOrder << qSystemDirectory();
|
||||||
|
|
||||||
|
|
||||||
|
Now, run configure and make::
|
||||||
|
|
||||||
configure -opensource -release -qt-zlib -qt-gif -qt-libmng -qt-libpng -qt-libtiff -qt-libjpeg -release -platform win32-msvc2008 -no-qt3support -webkit -xmlpatterns -no-phonon -no-style-plastique -no-style-cleanlooks -no-style-motif -no-style-cde -no-declarative -no-scripttools -no-audio-backend -no-multimedia -no-dbus -no-openvg -no-opengl -no-qt3support -confirm-license -nomake examples -nomake demos -nomake docs -openssl -I Q:\openssl\include -L Q:\openssl\lib && nmake
|
configure -opensource -release -qt-zlib -qt-gif -qt-libmng -qt-libpng -qt-libtiff -qt-libjpeg -release -platform win32-msvc2008 -no-qt3support -webkit -xmlpatterns -no-phonon -no-style-plastique -no-style-cleanlooks -no-style-motif -no-style-cde -no-declarative -no-scripttools -no-audio-backend -no-multimedia -no-dbus -no-openvg -no-opengl -no-qt3support -confirm-license -nomake examples -nomake demos -nomake docs -openssl -I Q:\openssl\include -L Q:\openssl\lib && nmake
|
||||||
|
|
||||||
|
@ -11,6 +11,9 @@
|
|||||||
SummaryCodepage='1252' />
|
SummaryCodepage='1252' />
|
||||||
|
|
||||||
<Media Id="1" Cabinet="{app}.cab" CompressionLevel="{compression}" EmbedCab="yes" />
|
<Media Id="1" Cabinet="{app}.cab" CompressionLevel="{compression}" EmbedCab="yes" />
|
||||||
|
<!-- The following line is needed because of the patch to QtCore4.dll. You can remove this line
|
||||||
|
after you update Qt beyond 4.7.2. 'emus' means re-install even if version is the same not just if it is older. -->
|
||||||
|
<Property Id='REINSTALLMODE' Value='emus'/>
|
||||||
|
|
||||||
<Upgrade Id="{upgrade_code}">
|
<Upgrade Id="{upgrade_code}">
|
||||||
<UpgradeVersion Maximum="{version}"
|
<UpgradeVersion Maximum="{version}"
|
||||||
|
@ -347,9 +347,10 @@ class UploadUserManual(Command): # {{{
|
|||||||
with NamedTemporaryFile(suffix='.zip') as f:
|
with NamedTemporaryFile(suffix='.zip') as f:
|
||||||
os.fchmod(f.fileno(),
|
os.fchmod(f.fileno(),
|
||||||
stat.S_IRUSR|stat.S_IRGRP|stat.S_IROTH|stat.S_IWRITE)
|
stat.S_IRUSR|stat.S_IRGRP|stat.S_IROTH|stat.S_IWRITE)
|
||||||
with CurrentDir(self.d(path)):
|
with CurrentDir(path):
|
||||||
with ZipFile(f, 'w') as zf:
|
with ZipFile(f, 'w') as zf:
|
||||||
for x in os.listdir('.'):
|
for x in os.listdir('.'):
|
||||||
|
if x.endswith('.swp'): continue
|
||||||
zf.write(x)
|
zf.write(x)
|
||||||
if os.path.isdir(x):
|
if os.path.isdir(x):
|
||||||
for y in os.listdir(x):
|
for y in os.listdir(x):
|
||||||
|
@ -388,7 +388,11 @@ class CurrentDir(object):
|
|||||||
return self.cwd
|
return self.cwd
|
||||||
|
|
||||||
def __exit__(self, *args):
|
def __exit__(self, *args):
|
||||||
|
try:
|
||||||
os.chdir(self.cwd)
|
os.chdir(self.cwd)
|
||||||
|
except:
|
||||||
|
# The previous CWD no longer exists
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class StreamReadWrapper(object):
|
class StreamReadWrapper(object):
|
||||||
|
@ -1162,7 +1162,7 @@ class StoreManyBooksStore(StoreBase):
|
|||||||
class StoreMobileReadStore(StoreBase):
|
class StoreMobileReadStore(StoreBase):
|
||||||
name = 'MobileRead'
|
name = 'MobileRead'
|
||||||
description = _('Ebooks handcrafted with the utmost care')
|
description = _('Ebooks handcrafted with the utmost care')
|
||||||
actual_plugin = 'calibre.gui2.store.mobileread_plugin:MobileReadStore'
|
actual_plugin = 'calibre.gui2.store.mobileread.mobileread_plugin:MobileReadStore'
|
||||||
|
|
||||||
class StoreOpenLibraryStore(StoreBase):
|
class StoreOpenLibraryStore(StoreBase):
|
||||||
name = 'Open Library'
|
name = 'Open Library'
|
||||||
|
@ -26,6 +26,7 @@ class ANDROID(USBMS):
|
|||||||
0xc92 : [0x100],
|
0xc92 : [0x100],
|
||||||
0xc97 : [0x226],
|
0xc97 : [0x226],
|
||||||
0xc99 : [0x0100],
|
0xc99 : [0x0100],
|
||||||
|
0xca2 : [0x226],
|
||||||
0xca3 : [0x100],
|
0xca3 : [0x100],
|
||||||
0xca4 : [0x226],
|
0xca4 : [0x226],
|
||||||
},
|
},
|
||||||
|
@ -402,7 +402,7 @@ class HTMLPreProcessor(object):
|
|||||||
(re.compile(r'((?<=</a>)\s*file:/{2,4}[A-Z].*<br>|file:////?[A-Z].*<br>(?=\s*<hr>))', re.IGNORECASE), lambda match: ''),
|
(re.compile(r'((?<=</a>)\s*file:/{2,4}[A-Z].*<br>|file:////?[A-Z].*<br>(?=\s*<hr>))', re.IGNORECASE), lambda match: ''),
|
||||||
|
|
||||||
# Center separator lines
|
# Center separator lines
|
||||||
(re.compile(u'<br>\s*(?P<break>([*#•✦=]+\s*)+)\s*<br>'), lambda match: '<p>\n<p style="text-align:center">' + match.group(1) + '</p>'),
|
(re.compile(u'<br>\s*(?P<break>([*#•✦=] *){3,})\s*<br>'), lambda match: '<p>\n<p style="text-align:center">' + match.group('break') + '</p>'),
|
||||||
|
|
||||||
# Remove page links
|
# Remove page links
|
||||||
(re.compile(r'<a name=\d+></a>', re.IGNORECASE), lambda match: ''),
|
(re.compile(r'<a name=\d+></a>', re.IGNORECASE), lambda match: ''),
|
||||||
|
@ -19,6 +19,9 @@ from calibre.utils.date import isoformat, format_date
|
|||||||
from calibre.utils.icu import sort_key
|
from calibre.utils.icu import sort_key
|
||||||
from calibre.utils.formatter import TemplateFormatter
|
from calibre.utils.formatter import TemplateFormatter
|
||||||
|
|
||||||
|
def human_readable(size, precision=2):
|
||||||
|
""" Convert a size in bytes into megabytes """
|
||||||
|
return ('%.'+str(precision)+'f'+ 'MB') % ((size/(1024.*1024.)),)
|
||||||
|
|
||||||
NULL_VALUES = {
|
NULL_VALUES = {
|
||||||
'user_metadata': {},
|
'user_metadata': {},
|
||||||
@ -117,7 +120,11 @@ class Metadata(object):
|
|||||||
_('TEMPLATE ERROR'),
|
_('TEMPLATE ERROR'),
|
||||||
self).strip()
|
self).strip()
|
||||||
return val
|
return val
|
||||||
|
if field.startswith('#') and field.endswith('_index'):
|
||||||
|
try:
|
||||||
|
return self.get_extra(field[:-6])
|
||||||
|
except:
|
||||||
|
pass
|
||||||
raise AttributeError(
|
raise AttributeError(
|
||||||
'Metadata object has no attribute named: '+ repr(field))
|
'Metadata object has no attribute named: '+ repr(field))
|
||||||
|
|
||||||
@ -167,11 +174,6 @@ class Metadata(object):
|
|||||||
try:
|
try:
|
||||||
return self.__getattribute__(field)
|
return self.__getattribute__(field)
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
if field.startswith('#') and field.endswith('_index'):
|
|
||||||
try:
|
|
||||||
return self.get_extra(field[:-6])
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
return default
|
return default
|
||||||
|
|
||||||
def get_extra(self, field, default=None):
|
def get_extra(self, field, default=None):
|
||||||
@ -551,7 +553,8 @@ class Metadata(object):
|
|||||||
def format_field_extended(self, key, series_with_index=True):
|
def format_field_extended(self, key, series_with_index=True):
|
||||||
from calibre.ebooks.metadata import authors_to_string
|
from calibre.ebooks.metadata import authors_to_string
|
||||||
'''
|
'''
|
||||||
returns the tuple (field_name, formatted_value)
|
returns the tuple (field_name, formatted_value, original_value,
|
||||||
|
field_metadata)
|
||||||
'''
|
'''
|
||||||
|
|
||||||
# Handle custom series index
|
# Handle custom series index
|
||||||
@ -627,6 +630,8 @@ class Metadata(object):
|
|||||||
res = format_date(res, fmeta['display'].get('date_format','dd MMM yyyy'))
|
res = format_date(res, fmeta['display'].get('date_format','dd MMM yyyy'))
|
||||||
elif datatype == 'rating':
|
elif datatype == 'rating':
|
||||||
res = res/2.0
|
res = res/2.0
|
||||||
|
elif key == 'size':
|
||||||
|
res = human_readable(res)
|
||||||
return (name, unicode(res), orig_res, fmeta)
|
return (name, unicode(res), orig_res, fmeta)
|
||||||
|
|
||||||
return (None, None, None, None)
|
return (None, None, None, None)
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
'''
|
'''
|
||||||
Read meta information from eReader pdb files.
|
Read meta information from pdb files.
|
||||||
'''
|
'''
|
||||||
|
|
||||||
__license__ = 'GPL v3'
|
__license__ = 'GPL v3'
|
||||||
@ -13,10 +13,12 @@ import re
|
|||||||
from calibre.ebooks.metadata import MetaInformation
|
from calibre.ebooks.metadata import MetaInformation
|
||||||
from calibre.ebooks.pdb.header import PdbHeaderReader
|
from calibre.ebooks.pdb.header import PdbHeaderReader
|
||||||
from calibre.ebooks.metadata.ereader import get_metadata as get_eReader
|
from calibre.ebooks.metadata.ereader import get_metadata as get_eReader
|
||||||
|
from calibre.ebooks.metadata.plucker import get_metadata as get_plucker
|
||||||
|
|
||||||
MREADER = {
|
MREADER = {
|
||||||
'PNPdPPrs' : get_eReader,
|
'PNPdPPrs' : get_eReader,
|
||||||
'PNRdPPrs' : get_eReader,
|
'PNRdPPrs' : get_eReader,
|
||||||
|
'DataPlkr' : get_plucker,
|
||||||
}
|
}
|
||||||
|
|
||||||
from calibre.ebooks.metadata.ereader import set_metadata as set_eReader
|
from calibre.ebooks.metadata.ereader import set_metadata as set_eReader
|
||||||
|
73
src/calibre/ebooks/metadata/plucker.py
Normal file
73
src/calibre/ebooks/metadata/plucker.py
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from __future__ import (unicode_literals, division, absolute_import, print_function)
|
||||||
|
|
||||||
|
'''
|
||||||
|
Read meta information from Plucker pdb files.
|
||||||
|
'''
|
||||||
|
|
||||||
|
__license__ = 'GPL v3'
|
||||||
|
__copyright__ = '2009, John Schember <john@nachtimwald.com>'
|
||||||
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
|
import struct
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from calibre.ebooks.metadata import MetaInformation
|
||||||
|
from calibre.ebooks.pdb.header import PdbHeaderReader
|
||||||
|
from calibre.ebooks.pdb.plucker.reader import SectionHeader, DATATYPE_METADATA, \
|
||||||
|
MIBNUM_TO_NAME
|
||||||
|
|
||||||
|
def get_metadata(stream, extract_cover=True):
|
||||||
|
'''
|
||||||
|
Return metadata as a L{MetaInfo} object
|
||||||
|
'''
|
||||||
|
mi = MetaInformation(_('Unknown'), [_('Unknown')])
|
||||||
|
stream.seek(0)
|
||||||
|
|
||||||
|
pheader = PdbHeaderReader(stream)
|
||||||
|
section_data = None
|
||||||
|
for i in range(1, pheader.num_sections):
|
||||||
|
raw_data = pheader.section_data(i)
|
||||||
|
section_header = SectionHeader(raw_data)
|
||||||
|
if section_header.type == DATATYPE_METADATA:
|
||||||
|
section_data = raw_data[8:]
|
||||||
|
break
|
||||||
|
|
||||||
|
if not section_data:
|
||||||
|
return mi
|
||||||
|
|
||||||
|
default_encoding = 'latin-1'
|
||||||
|
record_count, = struct.unpack('>H', section_data[0:2])
|
||||||
|
adv = 0
|
||||||
|
title = None
|
||||||
|
author = None
|
||||||
|
pubdate = 0
|
||||||
|
for i in xrange(record_count):
|
||||||
|
type, = struct.unpack('>H', section_data[2+adv:4+adv])
|
||||||
|
length, = struct.unpack('>H', section_data[4+adv:6+adv])
|
||||||
|
|
||||||
|
# CharSet
|
||||||
|
if type == 1:
|
||||||
|
val, = struct.unpack('>H', section_data[6+adv:8+adv])
|
||||||
|
default_encoding = MIBNUM_TO_NAME.get(val, 'latin-1')
|
||||||
|
# Author
|
||||||
|
elif type == 4:
|
||||||
|
author = section_data[6+adv+(2*length)]
|
||||||
|
# Title
|
||||||
|
elif type == 5:
|
||||||
|
title = section_data[6+adv+(2*length)]
|
||||||
|
# Publication Date
|
||||||
|
elif type == 6:
|
||||||
|
pubdate, = struct.unpack('>I', section_data[6+adv:6+adv+4])
|
||||||
|
|
||||||
|
adv += 2*length
|
||||||
|
|
||||||
|
if title:
|
||||||
|
mi.title = title.replace('\0', '').decode(default_encoding, 'replace')
|
||||||
|
if author:
|
||||||
|
author = author.replace('\0', '').decode(default_encoding, 'replace')
|
||||||
|
mi.author = author.split(',')
|
||||||
|
mi.pubdate = datetime.fromtimestamp(pubdate)
|
||||||
|
|
||||||
|
return mi
|
@ -301,7 +301,7 @@ class Amazon(Source):
|
|||||||
if asin is None:
|
if asin is None:
|
||||||
asin = identifiers.get('asin', None)
|
asin = identifiers.get('asin', None)
|
||||||
if asin:
|
if asin:
|
||||||
return 'http://amzn.com/%s'%asin
|
return ('amazon', asin, 'http://amzn.com/%s'%asin)
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
def create_query(self, log, title=None, authors=None, identifiers={}): # {{{
|
def create_query(self, log, title=None, authors=None, identifiers={}): # {{{
|
||||||
|
@ -56,7 +56,8 @@ class InternalMetadataCompareKeyGen(object):
|
|||||||
|
|
||||||
'''
|
'''
|
||||||
Generate a sort key for comparison of the relevance of Metadata objects,
|
Generate a sort key for comparison of the relevance of Metadata objects,
|
||||||
given a search query.
|
given a search query. This is used only to compare results from the same
|
||||||
|
metadata source, not across different sources.
|
||||||
|
|
||||||
The sort key ensures that an ascending order sort is a sort by order of
|
The sort key ensures that an ascending order sort is a sort by order of
|
||||||
decreasing relevance.
|
decreasing relevance.
|
||||||
@ -374,7 +375,11 @@ class Source(Plugin):
|
|||||||
|
|
||||||
def get_book_url(self, identifiers):
|
def get_book_url(self, identifiers):
|
||||||
'''
|
'''
|
||||||
Return the URL for the book identified by identifiers at this source.
|
Return a 3-tuple or None. The 3-tuple is of the form:
|
||||||
|
(identifier_type, identifier_value, URL).
|
||||||
|
The URL is the URL for the book identified by identifiers at this
|
||||||
|
source. identifier_type, identifier_value specify the identifier
|
||||||
|
corresponding to the URL.
|
||||||
This URL must be browseable to by a human using a browser. It is meant
|
This URL must be browseable to by a human using a browser. It is meant
|
||||||
to provide a clickable link for the user to easily visit the books page
|
to provide a clickable link for the user to easily visit the books page
|
||||||
at this source.
|
at this source.
|
||||||
|
@ -173,7 +173,7 @@ class GoogleBooks(Source):
|
|||||||
def get_book_url(self, identifiers): # {{{
|
def get_book_url(self, identifiers): # {{{
|
||||||
goog = identifiers.get('google', None)
|
goog = identifiers.get('google', None)
|
||||||
if goog is not None:
|
if goog is not None:
|
||||||
return 'http://books.google.com/books?id=%s'%goog
|
return ('google', goog, 'http://books.google.com/books?id=%s'%goog)
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
def create_query(self, log, title=None, authors=None, identifiers={}): # {{{
|
def create_query(self, log, title=None, authors=None, identifiers={}): # {{{
|
||||||
|
@ -412,7 +412,7 @@ def identify(log, abort, # {{{
|
|||||||
|
|
||||||
if msprefs['txt_comments']:
|
if msprefs['txt_comments']:
|
||||||
for r in results:
|
for r in results:
|
||||||
if r.plugin.has_html_comments and r.comments:
|
if r.identify_plugin.has_html_comments and r.comments:
|
||||||
r.comments = html2text(r.comments)
|
r.comments = html2text(r.comments)
|
||||||
|
|
||||||
max_tags = msprefs['max_tags']
|
max_tags = msprefs['max_tags']
|
||||||
@ -435,18 +435,30 @@ def identify(log, abort, # {{{
|
|||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
def urls_from_identifiers(identifiers): # {{{
|
def urls_from_identifiers(identifiers): # {{{
|
||||||
|
identifiers = dict([(k.lower(), v) for k, v in identifiers.iteritems()])
|
||||||
ans = []
|
ans = []
|
||||||
for plugin in all_metadata_plugins():
|
for plugin in all_metadata_plugins():
|
||||||
try:
|
try:
|
||||||
url = plugin.get_book_url(identifiers)
|
id_type, id_val, url = plugin.get_book_url(identifiers)
|
||||||
if url is not None:
|
ans.append((plugin.name, id_type, id_val, url))
|
||||||
ans.append((plugin.name, url))
|
|
||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
isbn = identifiers.get('isbn', None)
|
isbn = identifiers.get('isbn', None)
|
||||||
if isbn:
|
if isbn:
|
||||||
ans.append((isbn,
|
ans.append((isbn, 'isbn', isbn,
|
||||||
'http://www.worldcat.org/search?q=bn%%3A%s&qt=advanced'%isbn))
|
'http://www.worldcat.org/isbn/'+isbn))
|
||||||
|
doi = identifiers.get('doi', None)
|
||||||
|
if doi:
|
||||||
|
ans.append(('DOI', 'doi', doi,
|
||||||
|
'http://dx.doi.org/'+doi))
|
||||||
|
arxiv = identifiers.get('arxiv', None)
|
||||||
|
if arxiv:
|
||||||
|
ans.append(('arXiv', 'arxiv', arxiv,
|
||||||
|
'http://arxiv.org/abs/'+arxiv))
|
||||||
|
oclc = identifiers.get('oclc', None)
|
||||||
|
if oclc:
|
||||||
|
ans.append(('OCLC', 'oclc', oclc,
|
||||||
|
'http://www.worldcat.org/oclc/'+oclc))
|
||||||
return ans
|
return ans
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
|
@ -206,6 +206,7 @@ class OverDrive(Source):
|
|||||||
xref_q = '+'.join(title_tokens)
|
xref_q = '+'.join(title_tokens)
|
||||||
#log.error('Initial query is %s'%initial_q)
|
#log.error('Initial query is %s'%initial_q)
|
||||||
#log.error('Cross reference query is %s'%xref_q)
|
#log.error('Cross reference query is %s'%xref_q)
|
||||||
|
|
||||||
q_xref = q+'SearchResults.svc/GetResults?iDisplayLength=50&sSearch='+xref_q
|
q_xref = q+'SearchResults.svc/GetResults?iDisplayLength=50&sSearch='+xref_q
|
||||||
query = '{"szKeyword":"'+initial_q+'"}'
|
query = '{"szKeyword":"'+initial_q+'"}'
|
||||||
|
|
||||||
@ -229,34 +230,42 @@ class OverDrive(Source):
|
|||||||
if int(m.group('displayrecords')) >= 1:
|
if int(m.group('displayrecords')) >= 1:
|
||||||
results = True
|
results = True
|
||||||
elif int(m.group('totalrecords')) >= 1:
|
elif int(m.group('totalrecords')) >= 1:
|
||||||
|
if int(m.group('totalrecords')) >= 100:
|
||||||
|
if xref_q.find('+') != -1:
|
||||||
|
xref_tokens = xref_q.split('+')
|
||||||
|
xref_q = xref_tokens[0]
|
||||||
|
#log.error('xref_q is '+xref_q)
|
||||||
|
else:
|
||||||
|
xref_q = ''
|
||||||
xref_q = ''
|
xref_q = ''
|
||||||
q_xref = q+'SearchResults.svc/GetResults?iDisplayLength=50&sSearch='+xref_q
|
q_xref = q+'SearchResults.svc/GetResults?iDisplayLength=50&sSearch='+xref_q
|
||||||
elif int(m.group('totalrecords')) == 0:
|
elif int(m.group('totalrecords')) == 0:
|
||||||
return ''
|
return ''
|
||||||
|
|
||||||
return self.sort_ovrdrv_results(raw, title, title_tokens, author, author_tokens)
|
return self.sort_ovrdrv_results(raw, log, title, title_tokens, author, author_tokens)
|
||||||
|
|
||||||
|
|
||||||
def sort_ovrdrv_results(self, raw, title=None, title_tokens=None, author=None, author_tokens=None, ovrdrv_id=None):
|
def sort_ovrdrv_results(self, raw, log, title=None, title_tokens=None, author=None, author_tokens=None, ovrdrv_id=None):
|
||||||
close_matches = []
|
close_matches = []
|
||||||
raw = re.sub('.*?\[\[(?P<content>.*?)\]\].*', '[[\g<content>]]', raw)
|
raw = re.sub('.*?\[\[(?P<content>.*?)\]\].*', '[[\g<content>]]', raw)
|
||||||
results = json.loads(raw)
|
results = json.loads(raw)
|
||||||
#print results
|
#log.error('raw results are:'+str(results))
|
||||||
# The search results are either from a keyword search or a multi-format list from a single ID,
|
# The search results are either from a keyword search or a multi-format list from a single ID,
|
||||||
# sort through the results for closest match/format
|
# sort through the results for closest match/format
|
||||||
if results:
|
if results:
|
||||||
for reserveid, od_title, subtitle, edition, series, publisher, format, formatid, creators, \
|
for reserveid, od_title, subtitle, edition, series, publisher, format, formatid, creators, \
|
||||||
thumbimage, shortdescription, worldcatlink, excerptlink, creatorfile, sorttitle, \
|
thumbimage, shortdescription, worldcatlink, excerptlink, creatorfile, sorttitle, \
|
||||||
availabletolibrary, availabletoretailer, relevancyrank, unknown1, unknown2, unknown3 in results:
|
availabletolibrary, availabletoretailer, relevancyrank, unknown1, unknown2, unknown3 in results:
|
||||||
#print "this record's title is "+od_title+", subtitle is "+subtitle+", author[s] are "+creators+", series is "+series
|
#log.error("this record's title is "+od_title+", subtitle is "+subtitle+", author[s] are "+creators+", series is "+series)
|
||||||
if ovrdrv_id is not None and int(formatid) in [1, 50, 410, 900]:
|
if ovrdrv_id is not None and int(formatid) in [1, 50, 410, 900]:
|
||||||
#print "overdrive id is not None, searching based on format type priority"
|
#log.error('overdrive id is not None, searching based on format type priority')
|
||||||
return self.format_results(reserveid, od_title, subtitle, series, publisher,
|
return self.format_results(reserveid, od_title, subtitle, series, publisher,
|
||||||
creators, thumbimage, worldcatlink, formatid)
|
creators, thumbimage, worldcatlink, formatid)
|
||||||
else:
|
else:
|
||||||
|
if creators:
|
||||||
creators = creators.split(', ')
|
creators = creators.split(', ')
|
||||||
# if an exact match in a preferred format occurs
|
# if an exact match in a preferred format occurs
|
||||||
if (author and creators[0] == author[0]) and od_title == title and int(formatid) in [1, 50, 410, 900] and thumbimage:
|
if ((author and creators and creators[0] == author[0]) or (not author and not creators)) and od_title.lower() == title.lower() and int(formatid) in [1, 50, 410, 900] and thumbimage:
|
||||||
return self.format_results(reserveid, od_title, subtitle, series, publisher,
|
return self.format_results(reserveid, od_title, subtitle, series, publisher,
|
||||||
creators, thumbimage, worldcatlink, formatid)
|
creators, thumbimage, worldcatlink, formatid)
|
||||||
else:
|
else:
|
||||||
@ -282,6 +291,10 @@ class OverDrive(Source):
|
|||||||
close_matches.insert(0, self.format_results(reserveid, od_title, subtitle, series, publisher, creators, thumbimage, worldcatlink, formatid))
|
close_matches.insert(0, self.format_results(reserveid, od_title, subtitle, series, publisher, creators, thumbimage, worldcatlink, formatid))
|
||||||
else:
|
else:
|
||||||
close_matches.append(self.format_results(reserveid, od_title, subtitle, series, publisher, creators, thumbimage, worldcatlink, formatid))
|
close_matches.append(self.format_results(reserveid, od_title, subtitle, series, publisher, creators, thumbimage, worldcatlink, formatid))
|
||||||
|
|
||||||
|
elif close_title_match and close_author_match and int(formatid) in [1, 50, 410, 900]:
|
||||||
|
close_matches.append(self.format_results(reserveid, od_title, subtitle, series, publisher, creators, thumbimage, worldcatlink, formatid))
|
||||||
|
|
||||||
if close_matches:
|
if close_matches:
|
||||||
return close_matches[0]
|
return close_matches[0]
|
||||||
else:
|
else:
|
||||||
@ -289,7 +302,7 @@ class OverDrive(Source):
|
|||||||
else:
|
else:
|
||||||
return ''
|
return ''
|
||||||
|
|
||||||
def overdrive_get_record(self, br, q, ovrdrv_id):
|
def overdrive_get_record(self, br, log, q, ovrdrv_id):
|
||||||
search_url = q+'SearchResults.aspx?ReserveID={'+ovrdrv_id+'}'
|
search_url = q+'SearchResults.aspx?ReserveID={'+ovrdrv_id+'}'
|
||||||
results_url = q+'SearchResults.svc/GetResults?sEcho=1&iColumns=18&sColumns=ReserveID%2CTitle%2CSubtitle%2CEdition%2CSeries%2CPublisher%2CFormat%2CFormatID%2CCreators%2CThumbImage%2CShortDescription%2CWorldCatLink%2CExcerptLink%2CCreatorFile%2CSortTitle%2CAvailableToLibrary%2CAvailableToRetailer%2CRelevancyRank&iDisplayStart=0&iDisplayLength=10&sSearch=&bEscapeRegex=true&iSortingCols=1&iSortCol_0=17&sSortDir_0=asc'
|
results_url = q+'SearchResults.svc/GetResults?sEcho=1&iColumns=18&sColumns=ReserveID%2CTitle%2CSubtitle%2CEdition%2CSeries%2CPublisher%2CFormat%2CFormatID%2CCreators%2CThumbImage%2CShortDescription%2CWorldCatLink%2CExcerptLink%2CCreatorFile%2CSortTitle%2CAvailableToLibrary%2CAvailableToRetailer%2CRelevancyRank&iDisplayStart=0&iDisplayLength=10&sSearch=&bEscapeRegex=true&iSortingCols=1&iSortCol_0=17&sSortDir_0=asc'
|
||||||
|
|
||||||
@ -311,7 +324,7 @@ class OverDrive(Source):
|
|||||||
raw = str(list(raw))
|
raw = str(list(raw))
|
||||||
clean_cj = mechanize.CookieJar()
|
clean_cj = mechanize.CookieJar()
|
||||||
br.set_cookiejar(clean_cj)
|
br.set_cookiejar(clean_cj)
|
||||||
return self.sort_ovrdrv_results(raw, None, None, None, ovrdrv_id)
|
return self.sort_ovrdrv_results(raw, log, None, None, None, ovrdrv_id)
|
||||||
|
|
||||||
|
|
||||||
def find_ovrdrv_data(self, br, log, title, author, isbn, ovrdrv_id=None):
|
def find_ovrdrv_data(self, br, log, title, author, isbn, ovrdrv_id=None):
|
||||||
@ -319,7 +332,7 @@ class OverDrive(Source):
|
|||||||
if ovrdrv_id is None:
|
if ovrdrv_id is None:
|
||||||
return self.overdrive_search(br, log, q, title, author)
|
return self.overdrive_search(br, log, q, title, author)
|
||||||
else:
|
else:
|
||||||
return self.overdrive_get_record(br, q, ovrdrv_id)
|
return self.overdrive_get_record(br, log, q, ovrdrv_id)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,75 +0,0 @@
|
|||||||
'''
|
|
||||||
Device profiles.
|
|
||||||
'''
|
|
||||||
|
|
||||||
__license__ = 'GPL v3'
|
|
||||||
__copyright__ = '2008, Marshall T. Vandegrift <llasram@gmail.com>'
|
|
||||||
|
|
||||||
from itertools import izip
|
|
||||||
|
|
||||||
FONT_SIZES = [('xx-small', 1),
|
|
||||||
('x-small', None),
|
|
||||||
('small', 2),
|
|
||||||
('medium', 3),
|
|
||||||
('large', 4),
|
|
||||||
('x-large', 5),
|
|
||||||
('xx-large', 6),
|
|
||||||
(None, 7)]
|
|
||||||
|
|
||||||
|
|
||||||
class Profile(object):
|
|
||||||
def __init__(self, width, height, dpi, fbase, fsizes):
|
|
||||||
self.width = (float(width) / dpi) * 72.
|
|
||||||
self.height = (float(height) / dpi) * 72.
|
|
||||||
self.dpi = float(dpi)
|
|
||||||
self.fbase = float(fbase)
|
|
||||||
self.fsizes = []
|
|
||||||
for (name, num), size in izip(FONT_SIZES, fsizes):
|
|
||||||
self.fsizes.append((name, num, float(size)))
|
|
||||||
self.fnames = dict((name, sz) for name, _, sz in self.fsizes if name)
|
|
||||||
self.fnums = dict((num, sz) for _, num, sz in self.fsizes if num)
|
|
||||||
|
|
||||||
|
|
||||||
PROFILES = {
|
|
||||||
'PRS505':
|
|
||||||
Profile(width=584, height=754, dpi=168.451, fbase=12,
|
|
||||||
fsizes=[7.5, 9, 10, 12, 15.5, 20, 22, 24]),
|
|
||||||
|
|
||||||
'MSReader':
|
|
||||||
Profile(width=480, height=652, dpi=96, fbase=13,
|
|
||||||
fsizes=[10, 11, 13, 16, 18, 20, 22, 26]),
|
|
||||||
|
|
||||||
# Not really, but let's pretend
|
|
||||||
'Mobipocket':
|
|
||||||
Profile(width=600, height=800, dpi=96, fbase=18,
|
|
||||||
fsizes=[14, 14, 16, 18, 20, 22, 24, 26]),
|
|
||||||
|
|
||||||
# No clue on usable screen size; DPI should be good
|
|
||||||
'HanlinV3':
|
|
||||||
Profile(width=584, height=754, dpi=168.451, fbase=16,
|
|
||||||
fsizes=[12, 12, 14, 16, 18, 20, 22, 24]),
|
|
||||||
|
|
||||||
'CybookG3':
|
|
||||||
Profile(width=600, height=800, dpi=168.451, fbase=16,
|
|
||||||
fsizes=[12, 12, 14, 16, 18, 20, 22, 24]),
|
|
||||||
|
|
||||||
'Kindle':
|
|
||||||
Profile(width=525, height=640, dpi=168.451, fbase=16,
|
|
||||||
fsizes=[12, 12, 14, 16, 18, 20, 22, 24]),
|
|
||||||
|
|
||||||
'Browser':
|
|
||||||
Profile(width=800, height=600, dpi=100.0, fbase=12,
|
|
||||||
fsizes=[5, 7, 9, 12, 13.5, 17, 20, 22, 24])
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class Context(object):
|
|
||||||
PROFILES = PROFILES
|
|
||||||
|
|
||||||
def __init__(self, source, dest):
|
|
||||||
if source in PROFILES:
|
|
||||||
source = PROFILES[source]
|
|
||||||
if dest in PROFILES:
|
|
||||||
dest = PROFILES[dest]
|
|
||||||
self.source = source
|
|
||||||
self.dest = dest
|
|
@ -21,7 +21,6 @@ from calibre import force_unicode
|
|||||||
from calibre.ebooks import unit_convert
|
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
|
|
||||||
|
|
||||||
cssutils.log.setLevel(logging.WARN)
|
cssutils.log.setLevel(logging.WARN)
|
||||||
|
|
||||||
@ -123,10 +122,10 @@ class CSSSelector(etree.XPath):
|
|||||||
class Stylizer(object):
|
class Stylizer(object):
|
||||||
STYLESHEETS = WeakKeyDictionary()
|
STYLESHEETS = WeakKeyDictionary()
|
||||||
|
|
||||||
def __init__(self, tree, path, oeb, opts, profile=PROFILES['PRS505'],
|
def __init__(self, tree, path, oeb, opts, profile=None,
|
||||||
extra_css='', user_css=''):
|
extra_css='', user_css=''):
|
||||||
self.oeb, self.opts = oeb, opts
|
self.oeb, self.opts = oeb, opts
|
||||||
self.profile = profile
|
self.profile = opts.input_profile
|
||||||
self.logger = oeb.logger
|
self.logger = oeb.logger
|
||||||
item = oeb.manifest.hrefs[path]
|
item = oeb.manifest.hrefs[path]
|
||||||
basename = os.path.basename(path)
|
basename = os.path.basename(path)
|
||||||
|
@ -12,6 +12,7 @@ from calibre.ebooks.pdb.ereader.reader import Reader as ereader_reader
|
|||||||
from calibre.ebooks.pdb.palmdoc.reader import Reader as palmdoc_reader
|
from calibre.ebooks.pdb.palmdoc.reader import Reader as palmdoc_reader
|
||||||
from calibre.ebooks.pdb.ztxt.reader import Reader as ztxt_reader
|
from calibre.ebooks.pdb.ztxt.reader import Reader as ztxt_reader
|
||||||
from calibre.ebooks.pdb.pdf.reader import Reader as pdf_reader
|
from calibre.ebooks.pdb.pdf.reader import Reader as pdf_reader
|
||||||
|
from calibre.ebooks.pdb.plucker.reader import Reader as plucker_reader
|
||||||
|
|
||||||
FORMAT_READERS = {
|
FORMAT_READERS = {
|
||||||
'PNPdPPrs': ereader_reader,
|
'PNPdPPrs': ereader_reader,
|
||||||
@ -19,6 +20,7 @@ FORMAT_READERS = {
|
|||||||
'zTXTGPlm': ztxt_reader,
|
'zTXTGPlm': ztxt_reader,
|
||||||
'TEXtREAd': palmdoc_reader,
|
'TEXtREAd': palmdoc_reader,
|
||||||
'.pdfADBE': pdf_reader,
|
'.pdfADBE': pdf_reader,
|
||||||
|
'DataPlkr': plucker_reader,
|
||||||
}
|
}
|
||||||
|
|
||||||
from calibre.ebooks.pdb.palmdoc.writer import Writer as palmdoc_writer
|
from calibre.ebooks.pdb.palmdoc.writer import Writer as palmdoc_writer
|
||||||
@ -37,6 +39,7 @@ IDENTITY_TO_NAME = {
|
|||||||
'zTXTGPlm': 'zTXT',
|
'zTXTGPlm': 'zTXT',
|
||||||
'TEXtREAd': 'PalmDOC',
|
'TEXtREAd': 'PalmDOC',
|
||||||
'.pdfADBE': 'Adobe Reader',
|
'.pdfADBE': 'Adobe Reader',
|
||||||
|
'DataPlkr': 'Plucker',
|
||||||
|
|
||||||
'BVokBDIC': 'BDicty',
|
'BVokBDIC': 'BDicty',
|
||||||
'DB99DBOS': 'DB (Database program)',
|
'DB99DBOS': 'DB (Database program)',
|
||||||
@ -50,7 +53,6 @@ IDENTITY_TO_NAME = {
|
|||||||
'DATALSdb': 'LIST',
|
'DATALSdb': 'LIST',
|
||||||
'Mdb1Mdb1': 'MobileDB',
|
'Mdb1Mdb1': 'MobileDB',
|
||||||
'BOOKMOBI': 'MobiPocket',
|
'BOOKMOBI': 'MobiPocket',
|
||||||
'DataPlkr': 'Plucker',
|
|
||||||
'DataSprd': 'QuickSheet',
|
'DataSprd': 'QuickSheet',
|
||||||
'SM01SMem': 'SuperMemo',
|
'SM01SMem': 'SuperMemo',
|
||||||
'TEXtTlDc': 'TealDoc',
|
'TEXtTlDc': 'TealDoc',
|
||||||
|
@ -129,14 +129,22 @@ class Reader132(FormatReader):
|
|||||||
footnoteids = re.findall('\w+(?=\x00)', self.section_data(self.header_record.footnote_offset).decode('cp1252' if self.encoding is None else self.encoding))
|
footnoteids = re.findall('\w+(?=\x00)', self.section_data(self.header_record.footnote_offset).decode('cp1252' if self.encoding is None else self.encoding))
|
||||||
for fid, i in enumerate(range(self.header_record.footnote_offset + 1, self.header_record.footnote_offset + self.header_record.footnote_count)):
|
for fid, i in enumerate(range(self.header_record.footnote_offset + 1, self.header_record.footnote_offset + self.header_record.footnote_count)):
|
||||||
self.log.debug('Extracting footnote page %i' % i)
|
self.log.debug('Extracting footnote page %i' % i)
|
||||||
html += footnote_to_html(footnoteids[fid], self.decompress_text(i))
|
if fid < len(footnoteids):
|
||||||
|
fid = footnoteids[fid]
|
||||||
|
else:
|
||||||
|
fid = ''
|
||||||
|
html += footnote_to_html(fid, self.decompress_text(i))
|
||||||
|
|
||||||
if self.header_record.sidebar_count > 0:
|
if self.header_record.sidebar_count > 0:
|
||||||
html += '<br /><h1>%s</h1>' % _('Sidebar')
|
html += '<br /><h1>%s</h1>' % _('Sidebar')
|
||||||
sidebarids = re.findall('\w+(?=\x00)', self.section_data(self.header_record.sidebar_offset).decode('cp1252' if self.encoding is None else self.encoding))
|
sidebarids = re.findall('\w+(?=\x00)', self.section_data(self.header_record.sidebar_offset).decode('cp1252' if self.encoding is None else self.encoding))
|
||||||
for sid, i in enumerate(range(self.header_record.sidebar_offset + 1, self.header_record.sidebar_offset + self.header_record.sidebar_count)):
|
for sid, i in enumerate(range(self.header_record.sidebar_offset + 1, self.header_record.sidebar_offset + self.header_record.sidebar_count)):
|
||||||
self.log.debug('Extracting sidebar page %i' % i)
|
self.log.debug('Extracting sidebar page %i' % i)
|
||||||
html += sidebar_to_html(sidebarids[sid], self.decompress_text(i))
|
if sid < len(sidebarids):
|
||||||
|
sid = sidebarids[sid]
|
||||||
|
else:
|
||||||
|
sid = ''
|
||||||
|
html += sidebar_to_html(sid, self.decompress_text(i))
|
||||||
|
|
||||||
html += '</body></html>'
|
html += '</body></html>'
|
||||||
|
|
||||||
|
0
src/calibre/ebooks/pdb/plucker/__init__.py
Normal file
0
src/calibre/ebooks/pdb/plucker/__init__.py
Normal file
739
src/calibre/ebooks/pdb/plucker/reader.py
Normal file
739
src/calibre/ebooks/pdb/plucker/reader.py
Normal file
@ -0,0 +1,739 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
#from __future__ import (unicode_literals, division, absolute_import, print_function)
|
||||||
|
|
||||||
|
__license__ = 'GPL v3'
|
||||||
|
__copyright__ = '20011, John Schember <john@nachtimwald.com>'
|
||||||
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
|
import os
|
||||||
|
import struct
|
||||||
|
import zlib
|
||||||
|
|
||||||
|
from collections import OrderedDict
|
||||||
|
|
||||||
|
from calibre import CurrentDir
|
||||||
|
from calibre.ebooks.pdb.formatreader import FormatReader
|
||||||
|
from calibre.ptempfile import TemporaryFile
|
||||||
|
from calibre.utils.magick import Image, create_canvas
|
||||||
|
from calibre.ebooks.compression.palmdoc import decompress_doc
|
||||||
|
|
||||||
|
DATATYPE_PHTML = 0
|
||||||
|
DATATYPE_PHTML_COMPRESSED = 1
|
||||||
|
DATATYPE_TBMP = 2
|
||||||
|
DATATYPE_TBMP_COMPRESSED = 3
|
||||||
|
DATATYPE_MAILTO = 4
|
||||||
|
DATATYPE_LINK_INDEX = 5
|
||||||
|
DATATYPE_LINKS = 6
|
||||||
|
DATATYPE_LINKS_COMPRESSED = 7
|
||||||
|
DATATYPE_BOOKMARKS = 8
|
||||||
|
DATATYPE_CATEGORY = 9
|
||||||
|
DATATYPE_METADATA = 10
|
||||||
|
DATATYPE_STYLE_SHEET = 11
|
||||||
|
DATATYPE_FONT_PAGE = 12
|
||||||
|
DATATYPE_TABLE = 13
|
||||||
|
DATATYPE_TABLE_COMPRESSED = 14
|
||||||
|
DATATYPE_COMPOSITE_IMAGE = 15
|
||||||
|
DATATYPE_PAGELIST_METADATA = 16
|
||||||
|
DATATYPE_SORTED_URL_INDEX = 17
|
||||||
|
DATATYPE_SORTED_URL = 18
|
||||||
|
DATATYPE_SORTED_URL_COMPRESSED = 19
|
||||||
|
DATATYPE_EXT_ANCHOR_INDEX = 20
|
||||||
|
DATATYPE_EXT_ANCHOR = 21
|
||||||
|
DATATYPE_EXT_ANCHOR_COMPRESSED = 22
|
||||||
|
|
||||||
|
# IETF IANA MIBenum value for the character set.
|
||||||
|
# See the http://www.iana.org/assignments/character-sets for valid values.
|
||||||
|
# Not all character sets are handled by Python. This is a small subset that
|
||||||
|
# the MIBenum maps to Python standard encodings
|
||||||
|
# from http://docs.python.org/library/codecs.html#standard-encodings
|
||||||
|
MIBNUM_TO_NAME = {
|
||||||
|
3: 'ascii',
|
||||||
|
4: 'latin_1',
|
||||||
|
5: 'iso8859_2',
|
||||||
|
6: 'iso8859_3',
|
||||||
|
7: 'iso8859_4',
|
||||||
|
8: 'iso8859_5',
|
||||||
|
9: 'iso8859_6',
|
||||||
|
10: 'iso8859_7',
|
||||||
|
11: 'iso8859_8',
|
||||||
|
12: 'iso8859_9',
|
||||||
|
13: 'iso8859_10',
|
||||||
|
17: 'shift_jis',
|
||||||
|
18: 'euc_jp',
|
||||||
|
27: 'utf_7',
|
||||||
|
36: 'euc_kr',
|
||||||
|
37: 'iso2022_kr',
|
||||||
|
38: 'euc_kr',
|
||||||
|
39: 'iso2022_jp',
|
||||||
|
40: 'iso2022_jp_2',
|
||||||
|
106: 'utf-8',
|
||||||
|
109: 'iso8859_13',
|
||||||
|
110: 'iso8859_14',
|
||||||
|
111: 'iso8859_15',
|
||||||
|
112: 'iso8859_16',
|
||||||
|
1013: 'utf_16_be',
|
||||||
|
1014: 'utf_16_le',
|
||||||
|
1015: 'utf_16',
|
||||||
|
2009: 'cp850',
|
||||||
|
2010: 'cp852',
|
||||||
|
2011: 'cp437',
|
||||||
|
2013: 'cp862',
|
||||||
|
2025: 'gb2312',
|
||||||
|
2026: 'big5',
|
||||||
|
2028: 'cp037',
|
||||||
|
2043: 'cp424',
|
||||||
|
2044: 'cp500',
|
||||||
|
2046: 'cp855',
|
||||||
|
2047: 'cp857',
|
||||||
|
2048: 'cp860',
|
||||||
|
2049: 'cp861',
|
||||||
|
2050: 'cp863',
|
||||||
|
2051: 'cp864',
|
||||||
|
2052: 'cp865',
|
||||||
|
2054: 'cp869',
|
||||||
|
2063: 'cp1026',
|
||||||
|
2085: 'hz',
|
||||||
|
2086: 'cp866',
|
||||||
|
2087: 'cp775',
|
||||||
|
2089: 'cp858',
|
||||||
|
2091: 'cp1140',
|
||||||
|
2102: 'big5hkscs',
|
||||||
|
2250: 'cp1250',
|
||||||
|
2251: 'cp1251',
|
||||||
|
2252: 'cp1252',
|
||||||
|
2253: 'cp1253',
|
||||||
|
2254: 'cp1254',
|
||||||
|
2255: 'cp1255',
|
||||||
|
2256: 'cp1256',
|
||||||
|
2257: 'cp1257',
|
||||||
|
2258: 'cp1258',
|
||||||
|
}
|
||||||
|
|
||||||
|
class HeaderRecord(object):
|
||||||
|
'''
|
||||||
|
Plucker header. PDB record 0.
|
||||||
|
'''
|
||||||
|
|
||||||
|
def __init__(self, raw):
|
||||||
|
self.uid, = struct.unpack('>H', raw[0:2])
|
||||||
|
# This is labled version in the spec.
|
||||||
|
# 2 is ZLIB compressed,
|
||||||
|
# 1 is DOC compressed
|
||||||
|
self.compression, = struct.unpack('>H', raw[2:4])
|
||||||
|
self.records, = struct.unpack('>H', raw[4:6])
|
||||||
|
# uid of the first html file. This should link
|
||||||
|
# to other files which in turn may link to others.
|
||||||
|
self.home_html = None
|
||||||
|
|
||||||
|
self.reserved = {}
|
||||||
|
for i in xrange(self.records):
|
||||||
|
adv = 4*i
|
||||||
|
name, = struct.unpack('>H', raw[6+adv:8+adv])
|
||||||
|
id, = struct.unpack('>H', raw[8+adv:10+adv])
|
||||||
|
self.reserved[id] = name
|
||||||
|
if name == 0:
|
||||||
|
self.home_html = id
|
||||||
|
|
||||||
|
|
||||||
|
class SectionHeader(object):
|
||||||
|
'''
|
||||||
|
Every sections (record) has this header. It gives
|
||||||
|
details about the section such as it's uid.
|
||||||
|
'''
|
||||||
|
|
||||||
|
def __init__(self, raw):
|
||||||
|
self.uid, = struct.unpack('>H', raw[0:2])
|
||||||
|
self.paragraphs, = struct.unpack('>H', raw[2:4])
|
||||||
|
self.size, = struct.unpack('>H', raw[4:6])
|
||||||
|
self.type, = struct.unpack('>B', raw[6])
|
||||||
|
self.flags, = struct.unpack('>B', raw[7])
|
||||||
|
|
||||||
|
|
||||||
|
class SectionHeaderText(object):
|
||||||
|
'''
|
||||||
|
Sub header for text records.
|
||||||
|
'''
|
||||||
|
|
||||||
|
def __init__(self, section_header, raw):
|
||||||
|
# The uncompressed size of each paragraph.
|
||||||
|
self.sizes = []
|
||||||
|
# uncompressed offset of each paragraph starting
|
||||||
|
# at the beginning of the PHTML.
|
||||||
|
self.paragraph_offsets = []
|
||||||
|
# Paragraph attributes.
|
||||||
|
self.attributes = []
|
||||||
|
|
||||||
|
for i in xrange(section_header.paragraphs):
|
||||||
|
adv = 4*i
|
||||||
|
self.sizes.append(struct.unpack('>H', raw[adv:2+adv])[0])
|
||||||
|
self.attributes.append(struct.unpack('>H', raw[2+adv:4+adv])[0])
|
||||||
|
|
||||||
|
running_offset = 0
|
||||||
|
for size in self.sizes:
|
||||||
|
running_offset += size
|
||||||
|
self.paragraph_offsets.append(running_offset)
|
||||||
|
|
||||||
|
|
||||||
|
class SectionMetadata(object):
|
||||||
|
'''
|
||||||
|
Metadata.
|
||||||
|
|
||||||
|
This does not store metadata such as title, or author.
|
||||||
|
That metadata would be best retrieved with the PDB (plucker)
|
||||||
|
metdata reader.
|
||||||
|
|
||||||
|
This stores document specific information such as the
|
||||||
|
text encoding.
|
||||||
|
|
||||||
|
Note: There is a default encoding but each text section
|
||||||
|
can be assigned a different encoding.
|
||||||
|
'''
|
||||||
|
|
||||||
|
def __init__(self, raw):
|
||||||
|
self.default_encoding = 'latin-1'
|
||||||
|
self.exceptional_uid_encodings = {}
|
||||||
|
self.owner_id = None
|
||||||
|
|
||||||
|
record_count, = struct.unpack('>H', raw[0:2])
|
||||||
|
|
||||||
|
adv = 0
|
||||||
|
for i in xrange(record_count):
|
||||||
|
type, = struct.unpack('>H', raw[2+adv:4+adv])
|
||||||
|
length, = struct.unpack('>H', raw[4+adv:6+adv])
|
||||||
|
|
||||||
|
# CharSet
|
||||||
|
if type == 1:
|
||||||
|
val, = struct.unpack('>H', raw[6+adv:8+adv])
|
||||||
|
self.default_encoding = MIBNUM_TO_NAME.get(val, 'latin-1')
|
||||||
|
# ExceptionalCharSets
|
||||||
|
elif type == 2:
|
||||||
|
ii_adv = 0
|
||||||
|
for ii in xrange(length / 2):
|
||||||
|
uid, = struct.unpack('>H', raw[6+adv+ii_adv:8+adv+ii_adv])
|
||||||
|
mib, = struct.unpack('>H', raw[8+adv+ii_adv:10+adv+ii_adv])
|
||||||
|
self.exceptional_uid_encodings[uid] = MIBNUM_TO_NAME.get(mib, 'latin-1')
|
||||||
|
ii_adv += 4
|
||||||
|
# OwnerID
|
||||||
|
elif type == 3:
|
||||||
|
self.owner_id = struct.unpack('>I', raw[6+adv:10+adv])
|
||||||
|
# Author, Title, PubDate
|
||||||
|
# Ignored here. The metadata reader plugin
|
||||||
|
# will get this info because if it's missing
|
||||||
|
# the metadata reader plugin will use fall
|
||||||
|
# back data from elsewhere in the file.
|
||||||
|
elif type in (4, 5, 6):
|
||||||
|
pass
|
||||||
|
# Linked Documents
|
||||||
|
elif type == 7:
|
||||||
|
pass
|
||||||
|
|
||||||
|
adv += 2*length
|
||||||
|
|
||||||
|
|
||||||
|
class SectionText(object):
|
||||||
|
'''
|
||||||
|
Text data. Stores a text section header and the PHTML.
|
||||||
|
'''
|
||||||
|
|
||||||
|
def __init__(self, section_header, raw):
|
||||||
|
self.header = SectionHeaderText(section_header, raw)
|
||||||
|
self.data = raw[section_header.paragraphs * 4:]
|
||||||
|
|
||||||
|
|
||||||
|
class SectionCompositeImage(object):
|
||||||
|
'''
|
||||||
|
A composite image consists of a a 2D array
|
||||||
|
of rows and columns. The entries in the array
|
||||||
|
are uid's.
|
||||||
|
'''
|
||||||
|
|
||||||
|
def __init__(self, raw):
|
||||||
|
self.columns, = struct.unpack('>H', raw[0:2])
|
||||||
|
self.rows, = struct.unpack('>H', raw[2:4])
|
||||||
|
|
||||||
|
# [
|
||||||
|
# [uid, uid, uid, ...],
|
||||||
|
# [uid, uid, uid, ...],
|
||||||
|
# ...
|
||||||
|
# ]
|
||||||
|
#
|
||||||
|
# Each item in the layout is in it's
|
||||||
|
# correct position in the final
|
||||||
|
# composite.
|
||||||
|
#
|
||||||
|
# Each item in the layout is a uid
|
||||||
|
# to an image record.
|
||||||
|
self.layout = []
|
||||||
|
offset = 4
|
||||||
|
for i in xrange(self.rows):
|
||||||
|
col = []
|
||||||
|
for j in xrange(self.columns):
|
||||||
|
col.append(struct.unpack('>H', raw[offset:offset+2])[0])
|
||||||
|
offset += 2
|
||||||
|
self.layout.append(col)
|
||||||
|
|
||||||
|
|
||||||
|
class Reader(FormatReader):
|
||||||
|
'''
|
||||||
|
Convert a plucker archive into HTML.
|
||||||
|
|
||||||
|
TODO:
|
||||||
|
* UTF 16 and 32 characters.
|
||||||
|
* Margins.
|
||||||
|
* Alignment.
|
||||||
|
* Font color.
|
||||||
|
* DATATYPE_MAILTO
|
||||||
|
* DATATYPE_TABLE(_COMPRESSED)
|
||||||
|
* DATATYPE_EXT_ANCHOR_INDEX
|
||||||
|
* DATATYPE_EXT_ANCHOR(_COMPRESSED)
|
||||||
|
'''
|
||||||
|
|
||||||
|
def __init__(self, header, stream, log, options):
|
||||||
|
self.stream = stream
|
||||||
|
self.log = log
|
||||||
|
self.options = options
|
||||||
|
|
||||||
|
# Mapping of section uid to our internal
|
||||||
|
# list of sections.
|
||||||
|
self.uid_section_number = OrderedDict()
|
||||||
|
self.uid_text_secion_number = OrderedDict()
|
||||||
|
self.uid_text_secion_encoding = {}
|
||||||
|
self.uid_image_section_number = {}
|
||||||
|
self.uid_composite_image_section_number = {}
|
||||||
|
self.metadata_section_number = None
|
||||||
|
self.default_encoding = 'latin-1'
|
||||||
|
self.owner_id = None
|
||||||
|
self.sections = []
|
||||||
|
|
||||||
|
# The Plucker record0 header
|
||||||
|
self.header_record = HeaderRecord(header.section_data(0))
|
||||||
|
|
||||||
|
for i in range(1, header.num_sections):
|
||||||
|
section_number = len(self.sections)
|
||||||
|
# The length of the section header.
|
||||||
|
# Where the actual data in the section starts.
|
||||||
|
start = 8
|
||||||
|
section = None
|
||||||
|
|
||||||
|
raw_data = header.section_data(i)
|
||||||
|
# Every sections has a section header.
|
||||||
|
section_header = SectionHeader(raw_data)
|
||||||
|
|
||||||
|
# Store sections we care able.
|
||||||
|
if section_header.type in (DATATYPE_PHTML, DATATYPE_PHTML_COMPRESSED):
|
||||||
|
self.uid_text_secion_number[section_header.uid] = section_number
|
||||||
|
section = SectionText(section_header, raw_data[start:])
|
||||||
|
elif section_header.type in (DATATYPE_TBMP, DATATYPE_TBMP_COMPRESSED):
|
||||||
|
self.uid_image_section_number[section_header.uid] = section_number
|
||||||
|
section = raw_data[start:]
|
||||||
|
elif section_header.type == DATATYPE_METADATA:
|
||||||
|
self.metadata_section_number = section_number
|
||||||
|
section = SectionMetadata(raw_data[start:])
|
||||||
|
elif section_header.type == DATATYPE_COMPOSITE_IMAGE:
|
||||||
|
self.uid_composite_image_section_number[section_header.uid] = section_number
|
||||||
|
section = SectionCompositeImage(raw_data[start:])
|
||||||
|
|
||||||
|
# Store the section.
|
||||||
|
if section:
|
||||||
|
self.uid_section_number[section_header.uid] = section_number
|
||||||
|
self.sections.append((section_header, section))
|
||||||
|
|
||||||
|
# Store useful information from the metadata section locally
|
||||||
|
# to make access easier.
|
||||||
|
if self.metadata_section_number:
|
||||||
|
mdata_section = self.sections[self.metadata_section_number][1]
|
||||||
|
for k, v in mdata_section.exceptional_uid_encodings.items():
|
||||||
|
self.uid_text_secion_encoding[k] = v
|
||||||
|
self.default_encoding = mdata_section.default_encoding
|
||||||
|
self.owner_id = mdata_section.owner_id
|
||||||
|
|
||||||
|
# Get the metadata (tile, author, ...) with the metadata reader.
|
||||||
|
from calibre.ebooks.metadata.pdb import get_metadata
|
||||||
|
self.mi = get_metadata(stream, False)
|
||||||
|
|
||||||
|
def extract_content(self, output_dir):
|
||||||
|
# Each text record is independent (unless the continuation
|
||||||
|
# value is set in the previous record). Put each converted
|
||||||
|
# text recored into a separate file. We will reference the
|
||||||
|
# home.html file as the first file and let the HTML input
|
||||||
|
# plugin assemble the order based on hyperlinks.
|
||||||
|
with CurrentDir(output_dir):
|
||||||
|
for uid, num in self.uid_text_secion_number.items():
|
||||||
|
self.log.debug(_('Writing record with uid: %s as %s.html' % (uid, uid)))
|
||||||
|
with open('%s.html' % uid, 'wb') as htmlf:
|
||||||
|
html = u'<html><body>'
|
||||||
|
section_header, section_data = self.sections[num]
|
||||||
|
if section_header.type == DATATYPE_PHTML:
|
||||||
|
html += self.process_phtml(section_data.data, section_data.header.paragraph_offsets)
|
||||||
|
elif section_header.type == DATATYPE_PHTML_COMPRESSED:
|
||||||
|
d = self.decompress_phtml(section_data.data)
|
||||||
|
html += self.process_phtml(d, section_data.header.paragraph_offsets).decode(self.get_text_uid_encoding(section_header.uid), 'replace')
|
||||||
|
html += '</body></html>'
|
||||||
|
htmlf.write(html.encode('utf-8'))
|
||||||
|
|
||||||
|
# Images.
|
||||||
|
# Cache the image sizes in case they are used by a composite image.
|
||||||
|
image_sizes = {}
|
||||||
|
if not os.path.exists(os.path.join(output_dir, 'images/')):
|
||||||
|
os.makedirs(os.path.join(output_dir, 'images/'))
|
||||||
|
with CurrentDir(os.path.join(output_dir, 'images/')):
|
||||||
|
# Single images.
|
||||||
|
for uid, num in self.uid_image_section_number.items():
|
||||||
|
section_header, section_data = self.sections[num]
|
||||||
|
if section_data:
|
||||||
|
idata = None
|
||||||
|
if section_header.type == DATATYPE_TBMP:
|
||||||
|
idata = section_data
|
||||||
|
elif section_header.type == DATATYPE_TBMP_COMPRESSED:
|
||||||
|
if self.header_record.compression == 1:
|
||||||
|
idata = decompress_doc(section_data)
|
||||||
|
elif self.header_record.compression == 2:
|
||||||
|
idata = zlib.decompress(section_data)
|
||||||
|
try:
|
||||||
|
with TemporaryFile(suffix='.palm') as itn:
|
||||||
|
with open(itn, 'wb') as itf:
|
||||||
|
itf.write(idata)
|
||||||
|
im = Image()
|
||||||
|
im.read(itn)
|
||||||
|
image_sizes[uid] = im.size
|
||||||
|
im.set_compression_quality(70)
|
||||||
|
im.save('%s.jpg' % uid)
|
||||||
|
self.log.debug('Wrote image with uid %s to images/%s.jpg' % (uid, uid))
|
||||||
|
except Exception as e:
|
||||||
|
self.log.error('Failed to write image with uid %s: %s' % (uid, e))
|
||||||
|
else:
|
||||||
|
self.log.error('Failed to write image with uid %s: No data.' % uid)
|
||||||
|
# Composite images.
|
||||||
|
# We're going to use the already compressed .jpg images here.
|
||||||
|
for uid, num in self.uid_composite_image_section_number.items():
|
||||||
|
try:
|
||||||
|
section_header, section_data = self.sections[num]
|
||||||
|
# Get the final width and height.
|
||||||
|
width = 0
|
||||||
|
height = 0
|
||||||
|
for row in section_data.layout:
|
||||||
|
row_width = 0
|
||||||
|
col_height = 0
|
||||||
|
for col in row:
|
||||||
|
if col not in image_sizes:
|
||||||
|
raise Exception('Image with uid: %s missing.' % col)
|
||||||
|
im = Image()
|
||||||
|
im.read('%s.jpg' % col)
|
||||||
|
w, h = im.size
|
||||||
|
row_width += w
|
||||||
|
if col_height < h:
|
||||||
|
col_height = h
|
||||||
|
if width < row_width:
|
||||||
|
width = row_width
|
||||||
|
height += col_height
|
||||||
|
# Create a new image the total size of all image
|
||||||
|
# parts. Put the parts into the new image.
|
||||||
|
canvas = create_canvas(width, height)
|
||||||
|
y_off = 0
|
||||||
|
for row in section_data.layout:
|
||||||
|
x_off = 0
|
||||||
|
largest_height = 0
|
||||||
|
for col in row:
|
||||||
|
im = Image()
|
||||||
|
im.read('%s.jpg' % col)
|
||||||
|
canvas.compose(im, x_off, y_off)
|
||||||
|
w, h = im.size
|
||||||
|
x_off += w
|
||||||
|
if largest_height < h:
|
||||||
|
largest_height = h
|
||||||
|
y_off += largest_height
|
||||||
|
canvas.set_compression_quality(70)
|
||||||
|
canvas.save('%s.jpg' % uid)
|
||||||
|
self.log.debug('Wrote composite image with uid %s to images/%s.jpg' % (uid, uid))
|
||||||
|
except Exception as e:
|
||||||
|
self.log.error('Failed to write composite image with uid %s: %s' % (uid, e))
|
||||||
|
|
||||||
|
# Run the HTML through the html processing plugin.
|
||||||
|
from calibre.customize.ui import plugin_for_input_format
|
||||||
|
html_input = plugin_for_input_format('html')
|
||||||
|
for opt in html_input.options:
|
||||||
|
setattr(self.options, opt.option.name, opt.recommended_value)
|
||||||
|
self.options.input_encoding = 'utf-8'
|
||||||
|
odi = self.options.debug_pipeline
|
||||||
|
self.options.debug_pipeline = None
|
||||||
|
# Determine the home.html record uid. This should be set in the
|
||||||
|
# reserved values in the metadata recored. home.html is the first
|
||||||
|
# text record (should have hyper link references to other records)
|
||||||
|
# in the document.
|
||||||
|
try:
|
||||||
|
home_html = self.header_record.home_html
|
||||||
|
if not home_html:
|
||||||
|
home_html = self.uid_text_secion_number.items()[0][0]
|
||||||
|
except:
|
||||||
|
raise Exception(_('Could not determine home.html'))
|
||||||
|
# Generate oeb from html conversion.
|
||||||
|
oeb = html_input.convert(open('%s.html' % home_html, 'rb'), self.options, 'html', self.log, {})
|
||||||
|
self.options.debug_pipeline = odi
|
||||||
|
|
||||||
|
return oeb
|
||||||
|
|
||||||
|
def decompress_phtml(self, data):
|
||||||
|
if self.header_record.compression == 2:
|
||||||
|
if self.owner_id:
|
||||||
|
raise NotImplementedError
|
||||||
|
return zlib.decompress(data)
|
||||||
|
elif self.header_record.compression == 1:
|
||||||
|
from calibre.ebooks.compression.palmdoc import decompress_doc
|
||||||
|
return decompress_doc(data)
|
||||||
|
|
||||||
|
def process_phtml(self, d, paragraph_offsets=[]):
|
||||||
|
html = u'<p id="p0">'
|
||||||
|
offset = 0
|
||||||
|
paragraph_open = True
|
||||||
|
link_open = False
|
||||||
|
need_set_p_id = False
|
||||||
|
p_num = 1
|
||||||
|
font_specifier_close = ''
|
||||||
|
|
||||||
|
while offset < len(d):
|
||||||
|
if not paragraph_open:
|
||||||
|
if need_set_p_id:
|
||||||
|
html += u'<p id="p%s">' % p_num
|
||||||
|
p_num += 1
|
||||||
|
need_set_p_id = False
|
||||||
|
else:
|
||||||
|
html += u'<p>'
|
||||||
|
paragraph_open = True
|
||||||
|
|
||||||
|
c = ord(d[offset])
|
||||||
|
# PHTML "functions"
|
||||||
|
if c == 0x0:
|
||||||
|
offset += 1
|
||||||
|
c = ord(d[offset])
|
||||||
|
# Page link begins
|
||||||
|
# 2 Bytes
|
||||||
|
# record ID
|
||||||
|
if c == 0x0a:
|
||||||
|
offset += 1
|
||||||
|
id = struct.unpack('>H', d[offset:offset+2])[0]
|
||||||
|
if id in self.uid_text_secion_number:
|
||||||
|
html += '<a href="%s.html">' % id
|
||||||
|
link_open = True
|
||||||
|
offset += 1
|
||||||
|
# Targeted page link begins
|
||||||
|
# 3 Bytes
|
||||||
|
# record ID, target
|
||||||
|
elif c == 0x0b:
|
||||||
|
offset += 3
|
||||||
|
# Paragraph link begins
|
||||||
|
# 4 Bytes
|
||||||
|
# record ID, paragraph number
|
||||||
|
elif c == 0x0c:
|
||||||
|
offset += 1
|
||||||
|
id = struct.unpack('>H', d[offset:offset+2])[0]
|
||||||
|
offset += 2
|
||||||
|
pid = struct.unpack('>H', d[offset:offset+2])[0]
|
||||||
|
if id in self.uid_text_secion_number:
|
||||||
|
html += '<a href="%s.html#p%s">' % (id, pid)
|
||||||
|
link_open = True
|
||||||
|
offset += 1
|
||||||
|
# Targeted paragraph link begins
|
||||||
|
# 5 Bytes
|
||||||
|
# record ID, paragraph number, target
|
||||||
|
elif c == 0x0d:
|
||||||
|
offset += 5
|
||||||
|
# Link ends
|
||||||
|
# 0 Bytes
|
||||||
|
elif c == 0x08:
|
||||||
|
if link_open:
|
||||||
|
html += '</a>'
|
||||||
|
link_open = False
|
||||||
|
# Set font
|
||||||
|
# 1 Bytes
|
||||||
|
# font specifier
|
||||||
|
elif c == 0x11:
|
||||||
|
offset += 1
|
||||||
|
specifier = d[offset]
|
||||||
|
html += font_specifier_close
|
||||||
|
# Regular text
|
||||||
|
if specifier == 0:
|
||||||
|
font_specifier_close = ''
|
||||||
|
# h1
|
||||||
|
elif specifier == 1:
|
||||||
|
html += '<h1>'
|
||||||
|
font_specifier_close = '</h1>'
|
||||||
|
# h2
|
||||||
|
elif specifier == 2:
|
||||||
|
html += '<h2>'
|
||||||
|
font_specifier_close = '</h2>'
|
||||||
|
# h3
|
||||||
|
elif specifier == 3:
|
||||||
|
html += '<h13>'
|
||||||
|
font_specifier_close = '</h3>'
|
||||||
|
# h4
|
||||||
|
elif specifier == 4:
|
||||||
|
html += '<h4>'
|
||||||
|
font_specifier_close = '</h4>'
|
||||||
|
# h5
|
||||||
|
elif specifier == 5:
|
||||||
|
html += '<h5>'
|
||||||
|
font_specifier_close = '</h5>'
|
||||||
|
# h6
|
||||||
|
elif specifier == 6:
|
||||||
|
html += '<h6>'
|
||||||
|
font_specifier_close = '</h6>'
|
||||||
|
# Bold
|
||||||
|
elif specifier == 7:
|
||||||
|
html += '<b>'
|
||||||
|
font_specifier_close = '</b>'
|
||||||
|
# Fixed-width
|
||||||
|
elif specifier == 8:
|
||||||
|
html += '<tt>'
|
||||||
|
font_specifier_close = '</tt>'
|
||||||
|
# Small
|
||||||
|
elif specifier == 9:
|
||||||
|
html += '<small>'
|
||||||
|
font_specifier_close = '</small>'
|
||||||
|
# Subscript
|
||||||
|
elif specifier == 10:
|
||||||
|
html += '<sub>'
|
||||||
|
font_specifier_close = '</sub>'
|
||||||
|
# Superscript
|
||||||
|
elif specifier == 11:
|
||||||
|
html += '<sup>'
|
||||||
|
font_specifier_close = '</sup>'
|
||||||
|
# Embedded image
|
||||||
|
# 2 Bytes
|
||||||
|
# image record ID
|
||||||
|
elif c == 0x1a:
|
||||||
|
offset += 1
|
||||||
|
uid = struct.unpack('>H', d[offset:offset+2])[0]
|
||||||
|
html += '<img src="images/%s.jpg" />' % uid
|
||||||
|
offset += 1
|
||||||
|
# Set margin
|
||||||
|
# 2 Bytes
|
||||||
|
# left margin, right margin
|
||||||
|
elif c == 0x22:
|
||||||
|
offset += 2
|
||||||
|
# Alignment of text
|
||||||
|
# 1 Bytes
|
||||||
|
# alignment
|
||||||
|
elif c == 0x29:
|
||||||
|
offset += 1
|
||||||
|
# Horizontal rule
|
||||||
|
# 3 Bytes
|
||||||
|
# 8-bit height, 8-bit width (pixels), 8-bit width (%, 1-100)
|
||||||
|
elif c == 0x33:
|
||||||
|
offset += 3
|
||||||
|
if paragraph_open:
|
||||||
|
html += u'</p>'
|
||||||
|
paragraph_open = False
|
||||||
|
html += u'<hr />'
|
||||||
|
# New line
|
||||||
|
# 0 Bytes
|
||||||
|
elif c == 0x38:
|
||||||
|
if paragraph_open:
|
||||||
|
html += u'</p>\n'
|
||||||
|
paragraph_open = False
|
||||||
|
# Italic text begins
|
||||||
|
# 0 Bytes
|
||||||
|
elif c == 0x40:
|
||||||
|
html += u'<i>'
|
||||||
|
# Italic text ends
|
||||||
|
# 0 Bytes
|
||||||
|
elif c == 0x48:
|
||||||
|
html += u'</i>'
|
||||||
|
# Set text color
|
||||||
|
# 3 Bytes
|
||||||
|
# 8-bit red, 8-bit green, 8-bit blue
|
||||||
|
elif c == 0x53:
|
||||||
|
offset += 3
|
||||||
|
# Multiple embedded image
|
||||||
|
# 4 Bytes
|
||||||
|
# alternate image record ID, image record ID
|
||||||
|
elif c == 0x5c:
|
||||||
|
offset += 3
|
||||||
|
uid = struct.unpack('>H', d[offset:offset+2])[0]
|
||||||
|
html += '<img src="images/%s.jpg" />' % uid
|
||||||
|
offset += 1
|
||||||
|
# Underline text begins
|
||||||
|
# 0 Bytes
|
||||||
|
elif c == 0x60:
|
||||||
|
html += u'<u>'
|
||||||
|
# Underline text ends
|
||||||
|
# 0 Bytes
|
||||||
|
elif c == 0x68:
|
||||||
|
html += u'</u>'
|
||||||
|
# Strike-through text begins
|
||||||
|
# 0 Bytes
|
||||||
|
elif c == 0x70:
|
||||||
|
html += u'<s>'
|
||||||
|
# Strike-through text ends
|
||||||
|
# 0 Bytes
|
||||||
|
elif c == 0x78:
|
||||||
|
html += u'</s>'
|
||||||
|
# 16-bit Unicode character
|
||||||
|
# 3 Bytes
|
||||||
|
# alternate text length, 16-bit unicode character
|
||||||
|
elif c == 0x83:
|
||||||
|
offset += 3
|
||||||
|
# 32-bit Unicode character
|
||||||
|
# 5 Bytes
|
||||||
|
# alternate text length, 32-bit unicode character
|
||||||
|
elif c == 0x85:
|
||||||
|
offset += 5
|
||||||
|
# Begin custom font span
|
||||||
|
# 6 Bytes
|
||||||
|
# font page record ID, X page position, Y page position
|
||||||
|
elif c == 0x8e:
|
||||||
|
offset += 6
|
||||||
|
# Adjust custom font glyph position
|
||||||
|
# 4 Bytes
|
||||||
|
# X page position, Y page position
|
||||||
|
elif c == 0x8c:
|
||||||
|
offset += 4
|
||||||
|
# Change font page
|
||||||
|
# 2 Bytes
|
||||||
|
# font record ID
|
||||||
|
elif c == 0x8a:
|
||||||
|
offset += 2
|
||||||
|
# End custom font span
|
||||||
|
# 0 Bytes
|
||||||
|
elif c == 0x88:
|
||||||
|
pass
|
||||||
|
# Begin new table row
|
||||||
|
# 0 Bytes
|
||||||
|
elif c == 0x90:
|
||||||
|
pass
|
||||||
|
# Insert table (or table link)
|
||||||
|
# 2 Bytes
|
||||||
|
# table record ID
|
||||||
|
elif c == 0x92:
|
||||||
|
offset += 2
|
||||||
|
# Table cell data
|
||||||
|
# 7 Bytes
|
||||||
|
# 8-bit alignment, 16-bit image record ID, 8-bit columns, 8-bit rows, 16-bit text length
|
||||||
|
elif c == 0x97:
|
||||||
|
offset += 7
|
||||||
|
# Exact link modifier
|
||||||
|
# 2 Bytes
|
||||||
|
# Paragraph Offset (The Exact Link Modifier modifies a Paragraph Link or Targeted Paragraph Link function to specify an exact byte offset within the paragraph. This function must be followed immediately by the function it modifies).
|
||||||
|
elif c == 0x9a:
|
||||||
|
offset += 2
|
||||||
|
elif c == 0xa0:
|
||||||
|
html += ' '
|
||||||
|
else:
|
||||||
|
html += unichr(c)
|
||||||
|
offset += 1
|
||||||
|
if offset in paragraph_offsets:
|
||||||
|
need_set_p_id = True
|
||||||
|
if paragraph_open:
|
||||||
|
html += u'</p>\n'
|
||||||
|
paragraph_open = False
|
||||||
|
|
||||||
|
if paragraph_open:
|
||||||
|
html += u'</p>'
|
||||||
|
|
||||||
|
return html
|
||||||
|
|
||||||
|
def get_text_uid_encoding(self, uid):
|
||||||
|
# Return the user sepcified input encoding,
|
||||||
|
# otherwise return the alternate encoding specified for the uid,
|
||||||
|
# otherwise retur the default encoding for the document.
|
||||||
|
return self.options.input_encoding if self.options.input_encoding else self.uid_text_secion_encoding.get(uid, self.default_encoding)
|
@ -749,7 +749,10 @@ def pml_to_html(pml):
|
|||||||
|
|
||||||
def footnote_sidebar_to_html(pre_id, id, pml):
|
def footnote_sidebar_to_html(pre_id, id, pml):
|
||||||
id = id.strip('\x01')
|
id = id.strip('\x01')
|
||||||
html = '<br /><br style="page-break-after: always;" /><div id="%s-%s"><p>%s</p><small><a href="#r%s-%s">return</a></small></div>' % (pre_id, id, pml_to_html(pml), pre_id, id)
|
if id.strip():
|
||||||
|
html = '<br /><br style="page-break-after: always;" /><div id="%s-%s">%s<small><a href="#r%s-%s">return</a></small></div>' % (pre_id, id, pml_to_html(pml), pre_id, id)
|
||||||
|
else:
|
||||||
|
html = '<br /><br style="page-break-after: always;" /><div>%s</div>' % pml_to_html(pml)
|
||||||
return html
|
return html
|
||||||
|
|
||||||
def footnote_to_html(id, pml):
|
def footnote_to_html(id, pml):
|
||||||
|
@ -80,6 +80,14 @@ gprefs.defaults['font'] = None
|
|||||||
gprefs.defaults['tags_browser_partition_method'] = 'first letter'
|
gprefs.defaults['tags_browser_partition_method'] = 'first letter'
|
||||||
gprefs.defaults['tags_browser_collapse_at'] = 100
|
gprefs.defaults['tags_browser_collapse_at'] = 100
|
||||||
gprefs.defaults['edit_metadata_single_layout'] = 'default'
|
gprefs.defaults['edit_metadata_single_layout'] = 'default'
|
||||||
|
gprefs.defaults['book_display_fields'] = [
|
||||||
|
('title', False), ('authors', False), ('formats', True),
|
||||||
|
('series', True), ('identifiers', True), ('tags', True),
|
||||||
|
('path', True), ('publisher', False), ('rating', False),
|
||||||
|
('author_sort', False), ('sort', False), ('timestamp', False),
|
||||||
|
('uuid', False), ('comments', True), ('id', False), ('pubdate', False),
|
||||||
|
('last_modified', False), ('size', False),
|
||||||
|
]
|
||||||
|
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
@ -89,7 +97,7 @@ UNDEFINED_QDATE = QDate(UNDEFINED_DATE)
|
|||||||
ALL_COLUMNS = ['title', 'ondevice', 'authors', 'size', 'timestamp', 'rating', 'publisher',
|
ALL_COLUMNS = ['title', 'ondevice', 'authors', 'size', 'timestamp', 'rating', 'publisher',
|
||||||
'tags', 'series', 'pubdate']
|
'tags', 'series', 'pubdate']
|
||||||
|
|
||||||
def _config():
|
def _config(): # {{{
|
||||||
c = Config('gui', 'preferences for the calibre GUI')
|
c = Config('gui', 'preferences for the calibre GUI')
|
||||||
c.add_opt('send_to_storage_card_by_default', default=False,
|
c.add_opt('send_to_storage_card_by_default', default=False,
|
||||||
help=_('Send file to storage card instead of main memory by default'))
|
help=_('Send file to storage card instead of main memory by default'))
|
||||||
@ -181,6 +189,8 @@ def _config():
|
|||||||
return ConfigProxy(c)
|
return ConfigProxy(c)
|
||||||
|
|
||||||
config = _config()
|
config = _config()
|
||||||
|
# }}}
|
||||||
|
|
||||||
# Turn off DeprecationWarnings in windows GUI
|
# Turn off DeprecationWarnings in windows GUI
|
||||||
if iswindows:
|
if iswindows:
|
||||||
import warnings
|
import warnings
|
||||||
@ -729,12 +739,6 @@ def build_forms(srcdir, info=None):
|
|||||||
dat = dat.replace('from QtWebKit.QWebView import QWebView',
|
dat = dat.replace('from QtWebKit.QWebView import QWebView',
|
||||||
'from PyQt4 import QtWebKit\nfrom PyQt4.QtWebKit import QWebView')
|
'from PyQt4 import QtWebKit\nfrom PyQt4.QtWebKit import QWebView')
|
||||||
|
|
||||||
if form.endswith('viewer%smain.ui'%os.sep):
|
|
||||||
info('\t\tPromoting WebView')
|
|
||||||
dat = dat.replace('self.view = QtWebKit.QWebView(', 'self.view = DocumentView(')
|
|
||||||
dat = dat.replace('self.view = QWebView(', 'self.view = DocumentView(')
|
|
||||||
dat += '\n\nfrom calibre.gui2.viewer.documentview import DocumentView'
|
|
||||||
|
|
||||||
open(compiled_form, 'wb').write(dat)
|
open(compiled_form, 'wb').write(dat)
|
||||||
|
|
||||||
_df = os.environ.get('CALIBRE_DEVELOP_FROM', None)
|
_df = os.environ.get('CALIBRE_DEVELOP_FROM', None)
|
||||||
|
@ -117,11 +117,11 @@ class EditMetadataAction(InterfaceAction):
|
|||||||
|
|
||||||
payload = (id_map, failed_ids, failed_covers)
|
payload = (id_map, failed_ids, failed_covers)
|
||||||
from calibre.gui2.dialogs.message_box import ProceedNotification
|
from calibre.gui2.dialogs.message_box import ProceedNotification
|
||||||
p = ProceedNotification(payload, job.html_details,
|
p = ProceedNotification(self.apply_downloaded_metadata,
|
||||||
|
payload, job.html_details,
|
||||||
_('Download log'), _('Download complete'), msg,
|
_('Download log'), _('Download complete'), msg,
|
||||||
det_msg=det_msg, show_copy_button=show_copy_button,
|
det_msg=det_msg, show_copy_button=show_copy_button,
|
||||||
parent=self.gui)
|
parent=self.gui)
|
||||||
p.proceed.connect(self.apply_downloaded_metadata)
|
|
||||||
p.show()
|
p.show()
|
||||||
|
|
||||||
def apply_downloaded_metadata(self, payload):
|
def apply_downloaded_metadata(self, payload):
|
||||||
@ -529,13 +529,17 @@ class EditMetadataAction(InterfaceAction):
|
|||||||
view.reset()
|
view.reset()
|
||||||
|
|
||||||
# Apply bulk metadata changes {{{
|
# Apply bulk metadata changes {{{
|
||||||
def apply_metadata_changes(self, id_map, title=None, msg=''):
|
def apply_metadata_changes(self, id_map, title=None, msg='', callback=None):
|
||||||
'''
|
'''
|
||||||
Apply the metadata changes in id_map to the database synchronously
|
Apply the metadata changes in id_map to the database synchronously
|
||||||
id_map must be a mapping of ids to Metadata objects. Set any fields you
|
id_map must be a mapping of ids to Metadata objects. Set any fields you
|
||||||
do not want updated in the Metadata object to null. An easy way to do
|
do not want updated in the Metadata object to null. An easy way to do
|
||||||
that is to create a metadata object as Metadata(_('Unknown')) and then
|
that is to create a metadata object as Metadata(_('Unknown')) and then
|
||||||
only set the fields you want changed on this object.
|
only set the fields you want changed on this object.
|
||||||
|
|
||||||
|
callback can be either None or a function accepting a single argument,
|
||||||
|
in which case it is called after applying is complete with the list of
|
||||||
|
changed ids.
|
||||||
'''
|
'''
|
||||||
if title is None:
|
if title is None:
|
||||||
title = _('Applying changed metadata')
|
title = _('Applying changed metadata')
|
||||||
@ -544,6 +548,7 @@ class EditMetadataAction(InterfaceAction):
|
|||||||
self.apply_failures = []
|
self.apply_failures = []
|
||||||
self.applied_ids = []
|
self.applied_ids = []
|
||||||
self.apply_pd = None
|
self.apply_pd = None
|
||||||
|
self.apply_callback = callback
|
||||||
if len(self.apply_id_map) > 1:
|
if len(self.apply_id_map) > 1:
|
||||||
from calibre.gui2.dialogs.progress import ProgressDialog
|
from calibre.gui2.dialogs.progress import ProgressDialog
|
||||||
self.apply_pd = ProgressDialog(title, msg, min=0,
|
self.apply_pd = ProgressDialog(title, msg, min=0,
|
||||||
@ -611,6 +616,11 @@ class EditMetadataAction(InterfaceAction):
|
|||||||
|
|
||||||
self.apply_id_map = []
|
self.apply_id_map = []
|
||||||
self.apply_pd = None
|
self.apply_pd = None
|
||||||
|
try:
|
||||||
|
if callable(self.apply_callback):
|
||||||
|
self.apply_callback(self.applied_ids)
|
||||||
|
finally:
|
||||||
|
self.apply_callback = None
|
||||||
|
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
|
@ -30,5 +30,5 @@ class ShowBookDetailsAction(InterfaceAction):
|
|||||||
index = self.gui.library_view.currentIndex()
|
index = self.gui.library_view.currentIndex()
|
||||||
if index.isValid():
|
if index.isValid():
|
||||||
BookInfo(self.gui, self.gui.library_view, index,
|
BookInfo(self.gui, self.gui.library_view, index,
|
||||||
self.gui.iactions['View'].view_format_by_id).show()
|
self.gui.book_details.handle_click).show()
|
||||||
|
|
||||||
|
@ -5,67 +5,154 @@ __license__ = 'GPL v3'
|
|||||||
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||||
__docformat__ = 'restructuredtext en'
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
import collections, sys
|
|
||||||
from Queue import Queue
|
|
||||||
|
|
||||||
from PyQt4.Qt import QPixmap, QSize, QWidget, Qt, pyqtSignal, QUrl, \
|
from PyQt4.Qt import (QPixmap, QSize, QWidget, Qt, pyqtSignal, QUrl,
|
||||||
QPropertyAnimation, QEasingCurve, QThread, QApplication, QFontInfo, \
|
QPropertyAnimation, QEasingCurve, QApplication, QFontInfo,
|
||||||
QSizePolicy, QPainter, QRect, pyqtProperty, QLayout, QPalette, QMenu
|
QSizePolicy, QPainter, QRect, pyqtProperty, QLayout, QPalette, QMenu)
|
||||||
from PyQt4.QtWebKit import QWebView
|
from PyQt4.QtWebKit import QWebView
|
||||||
|
|
||||||
from calibre import fit_image, prepare_string_for_xml
|
from calibre import fit_image, force_unicode, prepare_string_for_xml
|
||||||
from calibre.gui2.dnd import dnd_has_image, dnd_get_image, dnd_get_files, \
|
from calibre.gui2.dnd import (dnd_has_image, dnd_get_image, dnd_get_files,
|
||||||
IMAGE_EXTENSIONS, dnd_has_extension
|
IMAGE_EXTENSIONS, dnd_has_extension)
|
||||||
from calibre.ebooks import BOOK_EXTENSIONS
|
from calibre.ebooks import BOOK_EXTENSIONS
|
||||||
from calibre.constants import preferred_encoding
|
from calibre.ebooks.metadata.book.base import (field_metadata, Metadata)
|
||||||
|
from calibre.ebooks.metadata import fmt_sidx
|
||||||
|
from calibre.ebooks.metadata.sources.identify import urls_from_identifiers
|
||||||
|
from calibre.constants import filesystem_encoding
|
||||||
from calibre.library.comments import comments_to_html
|
from calibre.library.comments import comments_to_html
|
||||||
from calibre.gui2 import config, open_local_file, open_url, pixmap_to_data
|
from calibre.gui2 import (config, open_local_file, open_url, pixmap_to_data,
|
||||||
|
gprefs)
|
||||||
from calibre.utils.icu import sort_key
|
from calibre.utils.icu import sort_key
|
||||||
|
|
||||||
# render_rows(data) {{{
|
def render_html(mi, css, vertical, widget, all_fields=False): # {{{
|
||||||
WEIGHTS = collections.defaultdict(lambda : 100)
|
table = render_data(mi, all_fields=all_fields,
|
||||||
WEIGHTS[_('Path')] = 5
|
use_roman_numbers=config['use_roman_numerals_for_series_number'])
|
||||||
WEIGHTS[_('Formats')] = 1
|
|
||||||
WEIGHTS[_('Collections')] = 2
|
|
||||||
WEIGHTS[_('Series')] = 3
|
|
||||||
WEIGHTS[_('Tags')] = 4
|
|
||||||
|
|
||||||
def render_rows(data):
|
def color_to_string(col):
|
||||||
keys = data.keys()
|
ans = '#000000'
|
||||||
# First sort by name. The WEIGHTS sort will preserve this sub-order
|
if col.isValid():
|
||||||
keys.sort(key=sort_key)
|
col = col.toRgb()
|
||||||
keys.sort(key=lambda x: WEIGHTS[x])
|
if col.isValid():
|
||||||
rows = []
|
ans = unicode(col.name())
|
||||||
for key in keys:
|
return ans
|
||||||
txt = data[key]
|
|
||||||
if key in ('id', _('Comments')) or not hasattr(txt, 'strip') or not txt.strip() or \
|
f = QFontInfo(QApplication.font(widget)).pixelSize()
|
||||||
txt == 'None':
|
c = color_to_string(QApplication.palette().color(QPalette.Normal,
|
||||||
continue
|
QPalette.WindowText))
|
||||||
if isinstance(key, str):
|
templ = u'''\
|
||||||
key = key.decode(preferred_encoding, 'replace')
|
<html>
|
||||||
if isinstance(txt, str):
|
<head>
|
||||||
txt = txt.decode(preferred_encoding, 'replace')
|
<style type="text/css">
|
||||||
if key.endswith(u':html'):
|
body, td {background-color: transparent; font-size: %dpx; color: %s }
|
||||||
key = key[:-5]
|
</style>
|
||||||
txt = comments_to_html(txt)
|
<style type="text/css">
|
||||||
elif '</font>' not in txt:
|
%s
|
||||||
txt = prepare_string_for_xml(txt)
|
</style>
|
||||||
if 'id' in data:
|
</head>
|
||||||
if key == _('Path'):
|
<body>
|
||||||
txt = u'<a href="path:%s" title="%s">%s</a>'%(data['id'],
|
%%s
|
||||||
txt, _('Click to open'))
|
</body>
|
||||||
if key == _('Formats') and txt and txt != _('None'):
|
<html>
|
||||||
fmts = [x.strip() for x in txt.split(',')]
|
'''%(f, c, css)
|
||||||
fmts = [u'<a href="format:%s:%s">%s</a>' % (data['id'], x, x) for x
|
comments = u''
|
||||||
in fmts]
|
if mi.comments:
|
||||||
txt = ', '.join(fmts)
|
comments = comments_to_html(force_unicode(mi.comments))
|
||||||
|
right_pane = u'<div id="comments" class="comments">%s</div>'%comments
|
||||||
|
|
||||||
|
if vertical:
|
||||||
|
ans = templ%(table+right_pane)
|
||||||
else:
|
else:
|
||||||
if key == _('Path'):
|
ans = templ%(u'<table><tr><td valign="top" '
|
||||||
txt = u'<a href="devpath:%s">%s</a>'%(txt,
|
'style="padding-right:2em; width:40%%">%s</td><td valign="top">%s</td></tr></table>'
|
||||||
_('Click to open'))
|
% (table, right_pane))
|
||||||
|
return ans
|
||||||
|
|
||||||
rows.append((key, txt))
|
def get_field_list(fm, use_defaults=False):
|
||||||
return rows
|
src = gprefs.defaults if use_defaults else gprefs
|
||||||
|
fieldlist = list(src['book_display_fields'])
|
||||||
|
names = frozenset([x[0] for x in fieldlist])
|
||||||
|
for field in fm.displayable_field_keys():
|
||||||
|
if field not in names:
|
||||||
|
fieldlist.append((field, True))
|
||||||
|
return fieldlist
|
||||||
|
|
||||||
|
def render_data(mi, use_roman_numbers=True, all_fields=False):
|
||||||
|
ans = []
|
||||||
|
isdevice = not hasattr(mi, 'id')
|
||||||
|
fm = getattr(mi, 'field_metadata', field_metadata)
|
||||||
|
|
||||||
|
for field, display in get_field_list(fm):
|
||||||
|
metadata = fm.get(field, None)
|
||||||
|
if all_fields:
|
||||||
|
display = True
|
||||||
|
if (not display or not metadata or mi.is_null(field) or
|
||||||
|
field == 'comments'):
|
||||||
|
continue
|
||||||
|
name = metadata['name']
|
||||||
|
if not name:
|
||||||
|
name = field
|
||||||
|
name += ':'
|
||||||
|
if metadata['datatype'] == 'comments':
|
||||||
|
val = getattr(mi, field)
|
||||||
|
if val:
|
||||||
|
val = force_unicode(val)
|
||||||
|
ans.append((field,
|
||||||
|
u'<td class="comments" colspan="2">%s</td>'%comments_to_html(val)))
|
||||||
|
elif field == 'path':
|
||||||
|
if mi.path:
|
||||||
|
path = force_unicode(mi.path, filesystem_encoding)
|
||||||
|
scheme = u'devpath' if isdevice else u'path'
|
||||||
|
url = prepare_string_for_xml(path if isdevice else
|
||||||
|
unicode(mi.id), True)
|
||||||
|
link = u'<a href="%s:%s" title="%s">%s</a>' % (scheme, url,
|
||||||
|
prepare_string_for_xml(path, True), _('Click to open'))
|
||||||
|
ans.append((field, u'<td class="title">%s</td><td>%s</td>'%(name, link)))
|
||||||
|
elif field == 'formats':
|
||||||
|
if isdevice: continue
|
||||||
|
fmts = [u'<a href="format:%s:%s">%s</a>' % (mi.id, x, x) for x
|
||||||
|
in mi.formats]
|
||||||
|
ans.append((field, u'<td class="title">%s</td><td>%s</td>'%(name,
|
||||||
|
u', '.join(fmts))))
|
||||||
|
elif field == 'identifiers':
|
||||||
|
urls = urls_from_identifiers(mi.identifiers)
|
||||||
|
links = [u'<a href="%s" title="%s:%s">%s</a>' % (url, id_typ, id_val, name)
|
||||||
|
for name, id_typ, id_val, url in urls]
|
||||||
|
links = u', '.join(links)
|
||||||
|
ans.append((field, u'<td class="title">%s</td><td>%s</td>'%(
|
||||||
|
_('Ids')+':', links)))
|
||||||
|
else:
|
||||||
|
val = mi.format_field(field)[-1]
|
||||||
|
if val is None:
|
||||||
|
continue
|
||||||
|
val = prepare_string_for_xml(val)
|
||||||
|
if metadata['datatype'] == 'series':
|
||||||
|
sidx = mi.get(field+'_index')
|
||||||
|
if sidx is None:
|
||||||
|
sidx = 1.0
|
||||||
|
val = _('Book %s of <span class="series_name">%s</span>')%(fmt_sidx(sidx,
|
||||||
|
use_roman=use_roman_numbers),
|
||||||
|
prepare_string_for_xml(getattr(mi, field)))
|
||||||
|
|
||||||
|
ans.append((field, u'<td class="title">%s</td><td>%s</td>'%(name, val)))
|
||||||
|
|
||||||
|
dc = getattr(mi, 'device_collections', [])
|
||||||
|
if dc:
|
||||||
|
dc = u', '.join(sorted(dc, key=sort_key))
|
||||||
|
ans.append(('device_collections',
|
||||||
|
u'<td class="title">%s</td><td>%s</td>'%(
|
||||||
|
_('Collections')+':', dc)))
|
||||||
|
|
||||||
|
def classname(field):
|
||||||
|
try:
|
||||||
|
dt = fm[field]['datatype']
|
||||||
|
except:
|
||||||
|
dt = 'text'
|
||||||
|
return 'datatype_%s'%dt
|
||||||
|
|
||||||
|
ans = [u'<tr id="%s" class="%s">%s</tr>'%(field.replace('#', '_'),
|
||||||
|
classname(field), html) for field, html in ans]
|
||||||
|
# print '\n'.join(ans)
|
||||||
|
return u'<table class="fields">%s</table>'%(u'\n'.join(ans))
|
||||||
|
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
@ -117,10 +204,10 @@ class CoverView(QWidget): # {{{
|
|||||||
|
|
||||||
def show_data(self, data):
|
def show_data(self, data):
|
||||||
self.animation.stop()
|
self.animation.stop()
|
||||||
same_item = data.get('id', True) == self.data.get('id', False)
|
same_item = getattr(data, 'id', True) == self.data.get('id', False)
|
||||||
self.data = {'id':data.get('id', None)}
|
self.data = {'id':data.get('id', None)}
|
||||||
if data.has_key('cover'):
|
if data.cover_data[1]:
|
||||||
self.pixmap = QPixmap.fromImage(data.pop('cover'))
|
self.pixmap = QPixmap.fromImage(data.cover_data[1])
|
||||||
if self.pixmap.isNull() or self.pixmap.width() < 5 or \
|
if self.pixmap.isNull() or self.pixmap.width() < 5 or \
|
||||||
self.pixmap.height() < 5:
|
self.pixmap.height() < 5:
|
||||||
self.pixmap = self.default_pixmap
|
self.pixmap = self.default_pixmap
|
||||||
@ -188,32 +275,6 @@ class CoverView(QWidget): # {{{
|
|||||||
|
|
||||||
# Book Info {{{
|
# Book Info {{{
|
||||||
|
|
||||||
class RenderComments(QThread):
|
|
||||||
|
|
||||||
rdone = pyqtSignal(object, object)
|
|
||||||
|
|
||||||
def __init__(self, parent):
|
|
||||||
QThread.__init__(self, parent)
|
|
||||||
self.queue = Queue()
|
|
||||||
self.start()
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
while True:
|
|
||||||
try:
|
|
||||||
rows, comments = self.queue.get()
|
|
||||||
except:
|
|
||||||
break
|
|
||||||
import time
|
|
||||||
time.sleep(0.001)
|
|
||||||
oint = sys.getcheckinterval()
|
|
||||||
sys.setcheckinterval(5)
|
|
||||||
try:
|
|
||||||
self.rdone.emit(rows, comments_to_html(comments))
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
sys.setcheckinterval(oint)
|
|
||||||
|
|
||||||
|
|
||||||
class BookInfo(QWebView):
|
class BookInfo(QWebView):
|
||||||
|
|
||||||
link_clicked = pyqtSignal(object)
|
link_clicked = pyqtSignal(object)
|
||||||
@ -221,8 +282,6 @@ class BookInfo(QWebView):
|
|||||||
def __init__(self, vertical, parent=None):
|
def __init__(self, vertical, parent=None):
|
||||||
QWebView.__init__(self, parent)
|
QWebView.__init__(self, parent)
|
||||||
self.vertical = vertical
|
self.vertical = vertical
|
||||||
self.renderer = RenderComments(self)
|
|
||||||
self.renderer.rdone.connect(self._show_data, type=Qt.QueuedConnection)
|
|
||||||
self.page().setLinkDelegationPolicy(self.page().DelegateAllLinks)
|
self.page().setLinkDelegationPolicy(self.page().DelegateAllLinks)
|
||||||
self.linkClicked.connect(self.link_activated)
|
self.linkClicked.connect(self.link_activated)
|
||||||
self._link_clicked = False
|
self._link_clicked = False
|
||||||
@ -231,65 +290,21 @@ class BookInfo(QWebView):
|
|||||||
self.setAcceptDrops(False)
|
self.setAcceptDrops(False)
|
||||||
palette.setBrush(QPalette.Base, Qt.transparent)
|
palette.setBrush(QPalette.Base, Qt.transparent)
|
||||||
self.page().setPalette(palette)
|
self.page().setPalette(palette)
|
||||||
|
self.css = P('templates/book_details.css', data=True).decode('utf-8')
|
||||||
|
|
||||||
def link_activated(self, link):
|
def link_activated(self, link):
|
||||||
self._link_clicked = True
|
self._link_clicked = True
|
||||||
|
if unicode(link.scheme()) in ('http', 'https'):
|
||||||
|
return open_url(link)
|
||||||
link = unicode(link.toString())
|
link = unicode(link.toString())
|
||||||
self.link_clicked.emit(link)
|
self.link_clicked.emit(link)
|
||||||
|
|
||||||
def turnoff_scrollbar(self, *args):
|
def turnoff_scrollbar(self, *args):
|
||||||
self.page().mainFrame().setScrollBarPolicy(Qt.Horizontal, Qt.ScrollBarAlwaysOff)
|
self.page().mainFrame().setScrollBarPolicy(Qt.Horizontal, Qt.ScrollBarAlwaysOff)
|
||||||
|
|
||||||
def show_data(self, data):
|
def show_data(self, mi):
|
||||||
rows = render_rows(data)
|
html = render_html(mi, self.css, self.vertical, self.parent())
|
||||||
rows = u'\n'.join([u'<tr><td valign="top"><b>%s:</b></td><td valign="top">%s</td></tr>'%(k,t) for
|
self.setHtml(html)
|
||||||
k, t in rows])
|
|
||||||
comments = data.get(_('Comments'), '')
|
|
||||||
if not comments or comments == u'None':
|
|
||||||
comments = ''
|
|
||||||
self.renderer.queue.put((rows, comments))
|
|
||||||
self._show_data(rows, '')
|
|
||||||
|
|
||||||
|
|
||||||
def _show_data(self, rows, comments):
|
|
||||||
|
|
||||||
def color_to_string(col):
|
|
||||||
ans = '#000000'
|
|
||||||
if col.isValid():
|
|
||||||
col = col.toRgb()
|
|
||||||
if col.isValid():
|
|
||||||
ans = unicode(col.name())
|
|
||||||
return ans
|
|
||||||
|
|
||||||
f = QFontInfo(QApplication.font(self.parent())).pixelSize()
|
|
||||||
c = color_to_string(QApplication.palette().color(QPalette.Normal,
|
|
||||||
QPalette.WindowText))
|
|
||||||
templ = u'''\
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<style type="text/css">
|
|
||||||
body, td {background-color: transparent; font-size: %dpx; color: %s }
|
|
||||||
a { text-decoration: none; color: blue }
|
|
||||||
div.description { margin-top: 0; padding-top: 0; text-indent: 0 }
|
|
||||||
table { margin-bottom: 0; padding-bottom: 0; }
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
%%s
|
|
||||||
</body>
|
|
||||||
<html>
|
|
||||||
'''%(f, c)
|
|
||||||
if self.vertical:
|
|
||||||
extra = ''
|
|
||||||
if comments:
|
|
||||||
extra = u'<div class="description">%s</div>'%comments
|
|
||||||
self.setHtml(templ%(u'<table>%s</table>%s'%(rows, extra)))
|
|
||||||
else:
|
|
||||||
left_pane = u'<table>%s</table>'%rows
|
|
||||||
right_pane = u'<div>%s</div>'%comments
|
|
||||||
self.setHtml(templ%(u'<table><tr><td valign="top" '
|
|
||||||
'style="padding-right:2em; width:40%%">%s</td><td valign="top">%s</td></tr></table>'
|
|
||||||
% (left_pane, right_pane)))
|
|
||||||
|
|
||||||
def mouseDoubleClickEvent(self, ev):
|
def mouseDoubleClickEvent(self, ev):
|
||||||
swidth = self.page().mainFrame().scrollBarGeometry(Qt.Vertical).width()
|
swidth = self.page().mainFrame().scrollBarGeometry(Qt.Vertical).width()
|
||||||
@ -457,10 +472,10 @@ class BookDetails(QWidget): # {{{
|
|||||||
self._layout.addWidget(self.cover_view)
|
self._layout.addWidget(self.cover_view)
|
||||||
self.book_info = BookInfo(vertical, self)
|
self.book_info = BookInfo(vertical, self)
|
||||||
self._layout.addWidget(self.book_info)
|
self._layout.addWidget(self.book_info)
|
||||||
self.book_info.link_clicked.connect(self._link_clicked)
|
self.book_info.link_clicked.connect(self.handle_click)
|
||||||
self.setCursor(Qt.PointingHandCursor)
|
self.setCursor(Qt.PointingHandCursor)
|
||||||
|
|
||||||
def _link_clicked(self, link):
|
def handle_click(self, link):
|
||||||
typ, _, val = link.partition(':')
|
typ, _, val = link.partition(':')
|
||||||
if typ == 'path':
|
if typ == 'path':
|
||||||
self.open_containing_folder.emit(int(val))
|
self.open_containing_folder.emit(int(val))
|
||||||
@ -484,7 +499,7 @@ 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.current_path = getattr(data, u'path', u'')
|
||||||
self.update_layout()
|
self.update_layout()
|
||||||
|
|
||||||
def update_layout(self):
|
def update_layout(self):
|
||||||
@ -500,7 +515,7 @@ class BookDetails(QWidget): # {{{
|
|||||||
)
|
)
|
||||||
|
|
||||||
def reset_info(self):
|
def reset_info(self):
|
||||||
self.show_data({})
|
self.show_data(Metadata(_('Unknown')))
|
||||||
|
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
|
@ -3,30 +3,33 @@ __license__ = 'GPL v3'
|
|||||||
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
|
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
|
||||||
__docformat__ = 'restructuredtext en'
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
import textwrap, os, re
|
|
||||||
|
|
||||||
from PyQt4.Qt import QCoreApplication, SIGNAL, QModelIndex, QTimer, Qt, \
|
from PyQt4.Qt import (QCoreApplication, SIGNAL, QModelIndex, QTimer, Qt,
|
||||||
QDialog, QPixmap, QIcon, QSize
|
QDialog, QPixmap, QIcon, QSize, QPalette)
|
||||||
|
|
||||||
from calibre.gui2.dialogs.book_info_ui import Ui_BookInfo
|
from calibre.gui2.dialogs.book_info_ui import Ui_BookInfo
|
||||||
from calibre.gui2 import dynamic, open_local_file, open_url
|
from calibre.gui2 import dynamic
|
||||||
from calibre import fit_image
|
from calibre import fit_image
|
||||||
from calibre.library.comments import comments_to_html
|
from calibre.gui2.book_details import render_html
|
||||||
from calibre.utils.icu import sort_key
|
|
||||||
|
|
||||||
|
|
||||||
class BookInfo(QDialog, Ui_BookInfo):
|
class BookInfo(QDialog, Ui_BookInfo):
|
||||||
|
|
||||||
def __init__(self, parent, view, row, view_func):
|
def __init__(self, parent, view, row, link_delegate):
|
||||||
QDialog.__init__(self, parent)
|
QDialog.__init__(self, parent)
|
||||||
Ui_BookInfo.__init__(self)
|
Ui_BookInfo.__init__(self)
|
||||||
self.setupUi(self)
|
self.setupUi(self)
|
||||||
self.gui = parent
|
self.gui = parent
|
||||||
self.cover_pixmap = None
|
self.cover_pixmap = None
|
||||||
self.comments.sizeHint = self.comments_size_hint
|
self.details.sizeHint = self.details_size_hint
|
||||||
self.comments.page().setLinkDelegationPolicy(self.comments.page().DelegateAllLinks)
|
self.details.page().setLinkDelegationPolicy(self.details.page().DelegateAllLinks)
|
||||||
self.comments.linkClicked.connect(self.link_clicked)
|
self.details.linkClicked.connect(self.link_clicked)
|
||||||
self.view_func = view_func
|
self.css = P('templates/book_details.css', data=True).decode('utf-8')
|
||||||
|
self.link_delegate = link_delegate
|
||||||
|
self.details.setAttribute(Qt.WA_OpaquePaintEvent, False)
|
||||||
|
palette = self.details.palette()
|
||||||
|
self.details.setAcceptDrops(False)
|
||||||
|
palette.setBrush(QPalette.Base, Qt.transparent)
|
||||||
|
self.details.page().setPalette(palette)
|
||||||
|
|
||||||
|
|
||||||
self.view = view
|
self.view = view
|
||||||
@ -37,7 +40,6 @@ class BookInfo(QDialog, Ui_BookInfo):
|
|||||||
self.connect(self.view.selectionModel(), SIGNAL('currentChanged(QModelIndex,QModelIndex)'), self.slave)
|
self.connect(self.view.selectionModel(), SIGNAL('currentChanged(QModelIndex,QModelIndex)'), self.slave)
|
||||||
self.connect(self.next_button, SIGNAL('clicked()'), self.next)
|
self.connect(self.next_button, SIGNAL('clicked()'), self.next)
|
||||||
self.connect(self.previous_button, SIGNAL('clicked()'), self.previous)
|
self.connect(self.previous_button, SIGNAL('clicked()'), self.previous)
|
||||||
self.connect(self.text, SIGNAL('linkActivated(QString)'), self.open_book_path)
|
|
||||||
self.fit_cover.stateChanged.connect(self.toggle_cover_fit)
|
self.fit_cover.stateChanged.connect(self.toggle_cover_fit)
|
||||||
self.cover.resizeEvent = self.cover_view_resized
|
self.cover.resizeEvent = self.cover_view_resized
|
||||||
self.cover.cover_changed.connect(self.cover_changed)
|
self.cover.cover_changed.connect(self.cover_changed)
|
||||||
@ -46,6 +48,10 @@ class BookInfo(QDialog, Ui_BookInfo):
|
|||||||
screen_height = desktop.availableGeometry().height() - 100
|
screen_height = desktop.availableGeometry().height() - 100
|
||||||
self.resize(self.size().width(), screen_height)
|
self.resize(self.size().width(), screen_height)
|
||||||
|
|
||||||
|
def link_clicked(self, qurl):
|
||||||
|
link = unicode(qurl.toString())
|
||||||
|
self.link_delegate(link)
|
||||||
|
|
||||||
def cover_changed(self, data):
|
def cover_changed(self, data):
|
||||||
if self.current_row is not None:
|
if self.current_row is not None:
|
||||||
id_ = self.view.model().id(self.current_row)
|
id_ = self.view.model().id(self.current_row)
|
||||||
@ -60,11 +66,8 @@ class BookInfo(QDialog, Ui_BookInfo):
|
|||||||
if self.fit_cover.isChecked():
|
if self.fit_cover.isChecked():
|
||||||
self.resize_cover()
|
self.resize_cover()
|
||||||
|
|
||||||
def link_clicked(self, url):
|
def details_size_hint(self):
|
||||||
open_url(url)
|
return QSize(350, 550)
|
||||||
|
|
||||||
def comments_size_hint(self):
|
|
||||||
return QSize(350, 250)
|
|
||||||
|
|
||||||
def toggle_cover_fit(self, state):
|
def toggle_cover_fit(self, state):
|
||||||
dynamic.set('book_info_dialog_fit_cover', self.fit_cover.isChecked())
|
dynamic.set('book_info_dialog_fit_cover', self.fit_cover.isChecked())
|
||||||
@ -77,13 +80,6 @@ class BookInfo(QDialog, Ui_BookInfo):
|
|||||||
row = current.row()
|
row = current.row()
|
||||||
self.refresh(row)
|
self.refresh(row)
|
||||||
|
|
||||||
def open_book_path(self, path):
|
|
||||||
path = unicode(path)
|
|
||||||
if os.sep in path:
|
|
||||||
open_local_file(path)
|
|
||||||
else:
|
|
||||||
self.view_func(self.view.model().id(self.current_row), path)
|
|
||||||
|
|
||||||
def next(self):
|
def next(self):
|
||||||
row = self.view.currentIndex().row()
|
row = self.view.currentIndex().row()
|
||||||
ni = self.view.model().index(row+1, 0)
|
ni = self.view.model().index(row+1, 0)
|
||||||
@ -117,8 +113,8 @@ class BookInfo(QDialog, Ui_BookInfo):
|
|||||||
row = row.row()
|
row = row.row()
|
||||||
if row == self.current_row:
|
if row == self.current_row:
|
||||||
return
|
return
|
||||||
info = self.view.model().get_book_info(row)
|
mi = self.view.model().get_book_display_info(row)
|
||||||
if info is None:
|
if mi is None:
|
||||||
# Indicates books was deleted from library, or row numbers have
|
# Indicates books was deleted from library, or row numbers have
|
||||||
# changed
|
# changed
|
||||||
return
|
return
|
||||||
@ -126,40 +122,11 @@ class BookInfo(QDialog, Ui_BookInfo):
|
|||||||
self.previous_button.setEnabled(False if row == 0 else True)
|
self.previous_button.setEnabled(False if row == 0 else True)
|
||||||
self.next_button.setEnabled(False if row == self.view.model().rowCount(QModelIndex())-1 else True)
|
self.next_button.setEnabled(False if row == self.view.model().rowCount(QModelIndex())-1 else True)
|
||||||
self.current_row = row
|
self.current_row = row
|
||||||
self.setWindowTitle(info[_('Title')])
|
self.setWindowTitle(mi.title)
|
||||||
self.title.setText('<b>'+info.pop(_('Title')))
|
self.title.setText('<b>'+mi.title)
|
||||||
comments = info.pop(_('Comments'), '')
|
mi.title = _('Unknown')
|
||||||
if comments:
|
self.cover_pixmap = QPixmap.fromImage(mi.cover_data[1])
|
||||||
comments = comments_to_html(comments)
|
|
||||||
if re.search(r'<[a-zA-Z]+>', comments) is None:
|
|
||||||
lines = comments.splitlines()
|
|
||||||
lines = [x if x.strip() else '<br><br>' for x in lines]
|
|
||||||
comments = '\n'.join(lines)
|
|
||||||
self.comments.setHtml('<div>%s</div>' % comments)
|
|
||||||
self.comments.page().setLinkDelegationPolicy(self.comments.page().DelegateAllLinks)
|
|
||||||
cdata = info.pop('cover', '')
|
|
||||||
self.cover_pixmap = QPixmap.fromImage(cdata)
|
|
||||||
self.resize_cover()
|
self.resize_cover()
|
||||||
|
html = render_html(mi, self.css, True, self, all_fields=True)
|
||||||
|
self.details.setHtml(html)
|
||||||
|
|
||||||
rows = u''
|
|
||||||
self.text.setText('')
|
|
||||||
self.data = info
|
|
||||||
if _('Path') in info.keys():
|
|
||||||
p = info[_('Path')]
|
|
||||||
info[_('Path')] = '<a href="%s">%s</a>'%(p, p)
|
|
||||||
if _('Formats') in info.keys():
|
|
||||||
formats = info[_('Formats')].split(',')
|
|
||||||
info[_('Formats')] = ''
|
|
||||||
for f in formats:
|
|
||||||
f = f.strip()
|
|
||||||
info[_('Formats')] += '<a href="%s">%s</a>, '%(f,f)
|
|
||||||
for key in sorted(info.keys(), key=sort_key):
|
|
||||||
if key == 'id': continue
|
|
||||||
txt = info[key]
|
|
||||||
if key.endswith(':html'):
|
|
||||||
key = key[:-5]
|
|
||||||
txt = comments_to_html(txt)
|
|
||||||
if key != _('Path'):
|
|
||||||
txt = u'<br />\n'.join(textwrap.wrap(txt, 120))
|
|
||||||
rows += u'<tr><td><b>%s:</b></td><td>%s</td></tr>'%(key, txt)
|
|
||||||
self.text.setText(u'<table>'+rows+'</table>')
|
|
||||||
|
@ -20,6 +20,12 @@
|
|||||||
<layout class="QGridLayout" name="gridLayout">
|
<layout class="QGridLayout" name="gridLayout">
|
||||||
<item row="0" column="0" colspan="2">
|
<item row="0" column="0" colspan="2">
|
||||||
<widget class="QLabel" name="title">
|
<widget class="QLabel" name="title">
|
||||||
|
<property name="sizePolicy">
|
||||||
|
<sizepolicy hsizetype="Preferred" vsizetype="Maximum">
|
||||||
|
<horstretch>0</horstretch>
|
||||||
|
<verstretch>0</verstretch>
|
||||||
|
</sizepolicy>
|
||||||
|
</property>
|
||||||
<property name="font">
|
<property name="font">
|
||||||
<font>
|
<font>
|
||||||
<weight>75</weight>
|
<weight>75</weight>
|
||||||
@ -34,82 +40,17 @@
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="1" column="0" rowspan="3">
|
<item row="2" column="0" rowspan="3">
|
||||||
<widget class="CoverView" name="cover"/>
|
<widget class="CoverView" name="cover"/>
|
||||||
</item>
|
</item>
|
||||||
<item row="1" column="1">
|
<item row="3" column="1">
|
||||||
<widget class="QScrollArea" name="scrollArea">
|
|
||||||
<property name="frameShape">
|
|
||||||
<enum>QFrame::NoFrame</enum>
|
|
||||||
</property>
|
|
||||||
<property name="widgetResizable">
|
|
||||||
<bool>true</bool>
|
|
||||||
</property>
|
|
||||||
<widget class="QWidget" name="scrollAreaWidgetContents">
|
|
||||||
<property name="geometry">
|
|
||||||
<rect>
|
|
||||||
<x>0</x>
|
|
||||||
<y>0</y>
|
|
||||||
<width>435</width>
|
|
||||||
<height>670</height>
|
|
||||||
</rect>
|
|
||||||
</property>
|
|
||||||
<layout class="QVBoxLayout" name="verticalLayout">
|
|
||||||
<item>
|
|
||||||
<widget class="QLabel" name="text">
|
|
||||||
<property name="text">
|
|
||||||
<string>TextLabel</string>
|
|
||||||
</property>
|
|
||||||
<property name="alignment">
|
|
||||||
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
|
|
||||||
</property>
|
|
||||||
<property name="wordWrap">
|
|
||||||
<bool>true</bool>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<widget class="QGroupBox" name="groupBox">
|
|
||||||
<property name="title">
|
|
||||||
<string>Comments</string>
|
|
||||||
</property>
|
|
||||||
<layout class="QVBoxLayout" name="verticalLayout_2">
|
|
||||||
<item>
|
|
||||||
<widget class="QWebView" name="comments">
|
|
||||||
<property name="sizePolicy">
|
|
||||||
<sizepolicy hsizetype="Preferred" vsizetype="Expanding">
|
|
||||||
<horstretch>0</horstretch>
|
|
||||||
<verstretch>0</verstretch>
|
|
||||||
</sizepolicy>
|
|
||||||
</property>
|
|
||||||
<property name="maximumSize">
|
|
||||||
<size>
|
|
||||||
<width>350</width>
|
|
||||||
<height>16777215</height>
|
|
||||||
</size>
|
|
||||||
</property>
|
|
||||||
<property name="url">
|
|
||||||
<url>
|
|
||||||
<string>about:blank</string>
|
|
||||||
</url>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
</layout>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
</layout>
|
|
||||||
</widget>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="2" column="1">
|
|
||||||
<widget class="QCheckBox" name="fit_cover">
|
<widget class="QCheckBox" name="fit_cover">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Fit &cover within view</string>
|
<string>Fit &cover within view</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="3" column="1">
|
<item row="4" column="1">
|
||||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||||
<item>
|
<item>
|
||||||
<widget class="QPushButton" name="previous_button">
|
<widget class="QPushButton" name="previous_button">
|
||||||
@ -135,6 +76,15 @@
|
|||||||
</item>
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</item>
|
</item>
|
||||||
|
<item row="2" column="1">
|
||||||
|
<widget class="QWebView" name="details">
|
||||||
|
<property name="url">
|
||||||
|
<url>
|
||||||
|
<string>about:blank</string>
|
||||||
|
</url>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
<customwidgets>
|
<customwidgets>
|
||||||
|
@ -22,6 +22,12 @@
|
|||||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||||
<item>
|
<item>
|
||||||
<widget class="QLabel" name="label">
|
<widget class="QLabel" name="label">
|
||||||
|
<property name="sizePolicy">
|
||||||
|
<sizepolicy hsizetype="Maximum" vsizetype="Preferred">
|
||||||
|
<horstretch>0</horstretch>
|
||||||
|
<verstretch>0</verstretch>
|
||||||
|
</sizepolicy>
|
||||||
|
</property>
|
||||||
<property name="pixmap">
|
<property name="pixmap">
|
||||||
<pixmap resource="../../../../resources/images.qrc">:/images/dialog_warning.png</pixmap>
|
<pixmap resource="../../../../resources/images.qrc">:/images/dialog_warning.png</pixmap>
|
||||||
</property>
|
</property>
|
||||||
@ -46,6 +52,10 @@
|
|||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Library</string>
|
<string>Library</string>
|
||||||
</property>
|
</property>
|
||||||
|
<property name="icon">
|
||||||
|
<iconset resource="../../../../resources/images.qrc">
|
||||||
|
<normaloff>:/images/library.png</normaloff>:/images/library.png</iconset>
|
||||||
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
@ -53,6 +63,10 @@
|
|||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Device</string>
|
<string>Device</string>
|
||||||
</property>
|
</property>
|
||||||
|
<property name="icon">
|
||||||
|
<iconset resource="../../../../resources/images.qrc">
|
||||||
|
<normaloff>:/images/reader.png</normaloff>:/images/reader.png</iconset>
|
||||||
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
@ -60,6 +74,10 @@
|
|||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Library and Device</string>
|
<string>Library and Device</string>
|
||||||
</property>
|
</property>
|
||||||
|
<property name="icon">
|
||||||
|
<iconset resource="../../../../resources/images.qrc">
|
||||||
|
<normaloff>:/images/trash.png</normaloff>:/images/trash.png</iconset>
|
||||||
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
|
@ -7,7 +7,7 @@ __docformat__ = 'restructuredtext en'
|
|||||||
|
|
||||||
|
|
||||||
from PyQt4.Qt import (QDialog, QIcon, QApplication, QSize, QKeySequence,
|
from PyQt4.Qt import (QDialog, QIcon, QApplication, QSize, QKeySequence,
|
||||||
QAction, Qt, pyqtSignal, QTextBrowser, QDialogButtonBox, QVBoxLayout)
|
QAction, Qt, QTextBrowser, QDialogButtonBox, QVBoxLayout)
|
||||||
|
|
||||||
from calibre.constants import __version__
|
from calibre.constants import __version__
|
||||||
from calibre.gui2.dialogs.message_box_ui import Ui_Dialog
|
from calibre.gui2.dialogs.message_box_ui import Ui_Dialog
|
||||||
@ -143,17 +143,20 @@ class ViewLog(QDialog): # {{{
|
|||||||
QApplication.clipboard().setText(txt)
|
QApplication.clipboard().setText(txt)
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
|
|
||||||
|
_proceed_memory = []
|
||||||
|
|
||||||
class ProceedNotification(MessageBox): # {{{
|
class ProceedNotification(MessageBox): # {{{
|
||||||
|
|
||||||
proceed = pyqtSignal(object)
|
def __init__(self, callback, payload, html_log, log_viewer_title, title, msg,
|
||||||
|
det_msg='', show_copy_button=False, parent=None):
|
||||||
def __init__(self, payload, html_log, log_viewer_title, title, msg, det_msg='', show_copy_button=False, parent=None):
|
|
||||||
'''
|
'''
|
||||||
A non modal popup that notifies the user that a background task has
|
A non modal popup that notifies the user that a background task has
|
||||||
been completed. If they user clicks yes, the proceed signal is emitted
|
been completed.
|
||||||
with payload as its argument.
|
|
||||||
|
|
||||||
:param payload: Arbitrary object, emitted in the proceed signal
|
:param callback: A callable that is called with payload if the user
|
||||||
|
asks to proceed. Note that this is always called in the GUI thread
|
||||||
|
:param payload: Arbitrary object, passed to callback
|
||||||
:param html_log: An HTML or plain text log
|
:param html_log: An HTML or plain text log
|
||||||
:param log_viewer_title: The title for the log viewer window
|
:param log_viewer_title: The title for the log viewer window
|
||||||
:param title: The title fo rthis popup
|
:param title: The title fo rthis popup
|
||||||
@ -166,25 +169,31 @@ class ProceedNotification(MessageBox): # {{{
|
|||||||
self.payload = payload
|
self.payload = payload
|
||||||
self.html_log = html_log
|
self.html_log = html_log
|
||||||
self.log_viewer_title = log_viewer_title
|
self.log_viewer_title = log_viewer_title
|
||||||
self.finished.connect(self.do_proceed)
|
self.finished.connect(self.do_proceed, type=Qt.QueuedConnection)
|
||||||
|
|
||||||
self.vlb = self.bb.addButton(_('View log'), self.bb.ActionRole)
|
self.vlb = self.bb.addButton(_('View log'), self.bb.ActionRole)
|
||||||
self.vlb.setIcon(QIcon(I('debug.png')))
|
self.vlb.setIcon(QIcon(I('debug.png')))
|
||||||
self.vlb.clicked.connect(self.show_log)
|
self.vlb.clicked.connect(self.show_log)
|
||||||
self.det_msg_toggle.setVisible(bool(det_msg))
|
self.det_msg_toggle.setVisible(bool(det_msg))
|
||||||
self.setModal(False)
|
self.setModal(False)
|
||||||
|
self.callback = callback
|
||||||
|
_proceed_memory.append(self)
|
||||||
|
|
||||||
def show_log(self):
|
def show_log(self):
|
||||||
self.log_viewer = ViewLog(self.log_viewer_title, self.html_log,
|
self.log_viewer = ViewLog(self.log_viewer_title, self.html_log,
|
||||||
parent=self)
|
parent=self)
|
||||||
|
|
||||||
def do_proceed(self, result):
|
def do_proceed(self, result):
|
||||||
if result == self.Accepted:
|
|
||||||
self.proceed.emit(self.payload)
|
|
||||||
try:
|
try:
|
||||||
self.proceed.disconnect()
|
if result == self.Accepted:
|
||||||
except:
|
self.callback(self.payload)
|
||||||
pass
|
finally:
|
||||||
|
# Ensure this notification is garbage collected
|
||||||
|
self.callback = None
|
||||||
|
self.setParent(None)
|
||||||
|
self.finished.disconnect()
|
||||||
|
self.vlb.clicked.disconnect()
|
||||||
|
_proceed_memory.remove(self)
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
@ -214,7 +214,6 @@ class SearchBar(QWidget): # {{{
|
|||||||
x.setIcon(QIcon(I("search_add_saved.png")))
|
x.setIcon(QIcon(I("search_add_saved.png")))
|
||||||
x.setObjectName("save_search_button")
|
x.setObjectName("save_search_button")
|
||||||
l.addWidget(x)
|
l.addWidget(x)
|
||||||
x.setToolTip(_("Save current search under the name shown in the box"))
|
|
||||||
|
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
|
@ -8,20 +8,20 @@ __docformat__ = 'restructuredtext en'
|
|||||||
import shutil, functools, re, os, traceback
|
import shutil, functools, re, os, traceback
|
||||||
from contextlib import closing
|
from contextlib import closing
|
||||||
|
|
||||||
from PyQt4.Qt import QAbstractTableModel, Qt, pyqtSignal, QIcon, QImage, \
|
from PyQt4.Qt import (QAbstractTableModel, Qt, pyqtSignal, QIcon, QImage,
|
||||||
QModelIndex, QVariant, QDate, QColor
|
QModelIndex, QVariant, QDate, QColor)
|
||||||
|
|
||||||
from calibre.gui2 import NONE, config, UNDEFINED_QDATE
|
from calibre.gui2 import NONE, UNDEFINED_QDATE
|
||||||
from calibre.utils.pyparsing import ParseException
|
from calibre.utils.pyparsing import ParseException
|
||||||
from calibre.ebooks.metadata import fmt_sidx, authors_to_string, string_to_authors
|
from calibre.ebooks.metadata import fmt_sidx, authors_to_string, string_to_authors
|
||||||
from calibre.ptempfile import PersistentTemporaryFile
|
from calibre.ptempfile import PersistentTemporaryFile
|
||||||
from calibre.utils.config import tweaks, prefs
|
from calibre.utils.config import tweaks, prefs
|
||||||
from calibre.utils.date import dt_factory, qt_to_dt, isoformat
|
from calibre.utils.date import dt_factory, qt_to_dt
|
||||||
from calibre.utils.icu import sort_key
|
from calibre.utils.icu import sort_key
|
||||||
from calibre.utils.search_query_parser import SearchQueryParser
|
from calibre.utils.search_query_parser import SearchQueryParser
|
||||||
from calibre.library.caches import _match, CONTAINS_MATCH, EQUALS_MATCH, \
|
from calibre.library.caches import (_match, CONTAINS_MATCH, EQUALS_MATCH,
|
||||||
REGEXP_MATCH, MetadataBackup, force_to_bool
|
REGEXP_MATCH, MetadataBackup, force_to_bool)
|
||||||
from calibre import strftime, isbytestring, prepare_string_for_xml
|
from calibre import strftime, isbytestring
|
||||||
from calibre.constants import filesystem_encoding, DEBUG
|
from calibre.constants import filesystem_encoding, DEBUG
|
||||||
from calibre.gui2.library import DEFAULT_SORT
|
from calibre.gui2.library import DEFAULT_SORT
|
||||||
|
|
||||||
@ -114,7 +114,7 @@ class BooksModel(QAbstractTableModel): # {{{
|
|||||||
return cc_label in self.custom_columns
|
return cc_label in self.custom_columns
|
||||||
|
|
||||||
def read_config(self):
|
def read_config(self):
|
||||||
self.use_roman_numbers = config['use_roman_numerals_for_series_number']
|
pass
|
||||||
|
|
||||||
def set_device_connected(self, is_connected):
|
def set_device_connected(self, is_connected):
|
||||||
self.device_connected = is_connected
|
self.device_connected = is_connected
|
||||||
@ -355,63 +355,13 @@ class BooksModel(QAbstractTableModel): # {{{
|
|||||||
return self.rowCount(None)
|
return self.rowCount(None)
|
||||||
|
|
||||||
def get_book_display_info(self, idx):
|
def get_book_display_info(self, idx):
|
||||||
def custom_keys_to_display():
|
|
||||||
ans = getattr(self, '_custom_fields_in_book_info', None)
|
|
||||||
if ans is None:
|
|
||||||
cfkeys = set(self.db.custom_field_keys())
|
|
||||||
yes_fields = set(tweaks['book_details_will_display'])
|
|
||||||
no_fields = set(tweaks['book_details_wont_display'])
|
|
||||||
if '*' in yes_fields:
|
|
||||||
yes_fields = cfkeys
|
|
||||||
if '*' in no_fields:
|
|
||||||
no_fields = cfkeys
|
|
||||||
ans = frozenset(yes_fields - no_fields)
|
|
||||||
setattr(self, '_custom_fields_in_book_info', ans)
|
|
||||||
return ans
|
|
||||||
|
|
||||||
data = {}
|
|
||||||
cdata = self.cover(idx)
|
|
||||||
if cdata:
|
|
||||||
data['cover'] = cdata
|
|
||||||
tags = list(self.db.get_tags(self.db.id(idx)))
|
|
||||||
if tags:
|
|
||||||
tags.sort(key=sort_key)
|
|
||||||
tags = ', '.join(tags)
|
|
||||||
else:
|
|
||||||
tags = _('None')
|
|
||||||
data[_('Tags')] = tags
|
|
||||||
formats = self.db.formats(idx)
|
|
||||||
if formats:
|
|
||||||
formats = formats.replace(',', ', ')
|
|
||||||
else:
|
|
||||||
formats = _('None')
|
|
||||||
data[_('Formats')] = formats
|
|
||||||
data[_('Path')] = self.db.abspath(idx)
|
|
||||||
data['id'] = self.id(idx)
|
|
||||||
comments = self.db.comments(idx)
|
|
||||||
if not comments:
|
|
||||||
comments = _('None')
|
|
||||||
data[_('Comments')] = comments
|
|
||||||
series = self.db.series(idx)
|
|
||||||
if series:
|
|
||||||
sidx = self.db.series_index(idx)
|
|
||||||
sidx = fmt_sidx(sidx, use_roman = self.use_roman_numbers)
|
|
||||||
data[_('Series')] = \
|
|
||||||
_('Book %s of %s.')%\
|
|
||||||
(sidx, prepare_string_for_xml(series))
|
|
||||||
mi = self.db.get_metadata(idx)
|
mi = self.db.get_metadata(idx)
|
||||||
cf_to_display = custom_keys_to_display()
|
mi.size = mi.book_size
|
||||||
for key in mi.custom_field_keys():
|
mi.cover_data = ('jpg', self.cover(idx))
|
||||||
if key not in cf_to_display:
|
mi.id = self.db.id(idx)
|
||||||
continue
|
mi.field_metadata = self.db.field_metadata
|
||||||
name, val = mi.format_field(key)
|
mi.path = self.db.abspath(idx, create_dirs=False)
|
||||||
if mi.metadata_for_field(key)['datatype'] == 'comments':
|
return mi
|
||||||
name += ':html'
|
|
||||||
if val and name not in data:
|
|
||||||
data[name] = val
|
|
||||||
|
|
||||||
return data
|
|
||||||
|
|
||||||
|
|
||||||
def current_changed(self, current, previous, emit_signal=True):
|
def current_changed(self, current, previous, emit_signal=True):
|
||||||
if current.isValid():
|
if current.isValid():
|
||||||
@ -425,16 +375,8 @@ class BooksModel(QAbstractTableModel): # {{{
|
|||||||
def get_book_info(self, index):
|
def get_book_info(self, index):
|
||||||
if isinstance(index, int):
|
if isinstance(index, int):
|
||||||
index = self.index(index, 0)
|
index = self.index(index, 0)
|
||||||
|
# If index is not valid returns None
|
||||||
data = self.current_changed(index, None, False)
|
data = self.current_changed(index, None, False)
|
||||||
if data is None:
|
|
||||||
return data
|
|
||||||
row = index.row()
|
|
||||||
data[_('Title')] = self.db.title(row)
|
|
||||||
au = self.db.authors(row)
|
|
||||||
if not au:
|
|
||||||
au = _('Unknown')
|
|
||||||
au = authors_to_string([a.strip().replace('|', ',') for a in au.split(',')])
|
|
||||||
data[_('Author(s)')] = au
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def metadata_for(self, ids):
|
def metadata_for(self, ids):
|
||||||
@ -1189,39 +1131,46 @@ class DeviceBooksModel(BooksModel): # {{{
|
|||||||
img = self.default_image
|
img = self.default_image
|
||||||
return img
|
return img
|
||||||
|
|
||||||
def current_changed(self, current, previous):
|
def get_book_display_info(self, idx):
|
||||||
data = {}
|
from calibre.ebooks.metadata.book.base import Metadata
|
||||||
item = self.db[self.map[current.row()]]
|
item = self.db[self.map[idx]]
|
||||||
cover = self.cover(current.row())
|
cover = self.cover(idx)
|
||||||
if cover is not self.default_image:
|
if cover is self.default_image:
|
||||||
data['cover'] = cover
|
cover = None
|
||||||
type = _('Unknown')
|
title = item.title
|
||||||
|
if not title:
|
||||||
|
title = _('Unknown')
|
||||||
|
au = item.authors
|
||||||
|
if not au:
|
||||||
|
au = [_('Unknown')]
|
||||||
|
mi = Metadata(title, au)
|
||||||
|
mi.cover_data = ('jpg', cover)
|
||||||
|
fmt = _('Unknown')
|
||||||
ext = os.path.splitext(item.path)[1]
|
ext = os.path.splitext(item.path)[1]
|
||||||
if ext:
|
if ext:
|
||||||
type = ext[1:].lower()
|
fmt = ext[1:].lower()
|
||||||
data[_('Format')] = type
|
mi.formats = [fmt]
|
||||||
data[_('Path')] = item.path
|
mi.path = (item.path if item.path else None)
|
||||||
dt = dt_factory(item.datetime, assume_utc=True)
|
dt = dt_factory(item.datetime, assume_utc=True)
|
||||||
data[_('Timestamp')] = isoformat(dt, sep=' ', as_utc=False)
|
mi.timestamp = dt
|
||||||
data[_('Collections')] = ', '.join(item.device_collections)
|
mi.device_collections = list(item.device_collections)
|
||||||
|
mi.tags = list(getattr(item, 'tags', []))
|
||||||
tags = getattr(item, 'tags', None)
|
mi.comments = getattr(item, 'comments', None)
|
||||||
if tags:
|
|
||||||
tags = u', '.join(tags)
|
|
||||||
else:
|
|
||||||
tags = _('None')
|
|
||||||
data[_('Tags')] = tags
|
|
||||||
comments = getattr(item, 'comments', None)
|
|
||||||
if not comments:
|
|
||||||
comments = _('None')
|
|
||||||
data[_('Comments')] = comments
|
|
||||||
series = getattr(item, 'series', None)
|
series = getattr(item, 'series', None)
|
||||||
if series:
|
if series:
|
||||||
sidx = getattr(item, 'series_index', 0)
|
sidx = getattr(item, 'series_index', 0)
|
||||||
sidx = fmt_sidx(sidx, use_roman = self.use_roman_numbers)
|
mi.series = series
|
||||||
data[_('Series')] = _('Book <font face="serif">%s</font> of %s.')%(sidx, series)
|
mi.series_index = sidx
|
||||||
|
return mi
|
||||||
|
|
||||||
|
def current_changed(self, current, previous, emit_signal=True):
|
||||||
|
if current.isValid():
|
||||||
|
idx = current.row()
|
||||||
|
data = self.get_book_display_info(idx)
|
||||||
|
if emit_signal:
|
||||||
self.new_bookdisplay_data.emit(data)
|
self.new_bookdisplay_data.emit(data)
|
||||||
|
else:
|
||||||
|
return data
|
||||||
|
|
||||||
def paths(self, rows):
|
def paths(self, rows):
|
||||||
return [self.db[self.map[r.row()]].path for r in rows ]
|
return [self.db[self.map[r.row()]].path for r in rows ]
|
||||||
@ -1281,7 +1230,7 @@ class DeviceBooksModel(BooksModel): # {{{
|
|||||||
elif cname == 'authors':
|
elif cname == 'authors':
|
||||||
au = self.db[self.map[row]].authors
|
au = self.db[self.map[row]].authors
|
||||||
if not au:
|
if not au:
|
||||||
au = self.unknown
|
au = [_('Unknown')]
|
||||||
return QVariant(authors_to_string(au))
|
return QVariant(authors_to_string(au))
|
||||||
elif cname == 'size':
|
elif cname == 'size':
|
||||||
size = self.db[self.map[row]].size
|
size = self.db[self.map[row]].size
|
||||||
|
@ -650,6 +650,11 @@ class BooksView(QTableView): # {{{
|
|||||||
def column_map(self):
|
def column_map(self):
|
||||||
return self._model.column_map
|
return self._model.column_map
|
||||||
|
|
||||||
|
def refresh_book_details(self):
|
||||||
|
idx = self.currentIndex()
|
||||||
|
if idx.isValid():
|
||||||
|
self._model.current_changed(idx, idx)
|
||||||
|
|
||||||
def scrollContentsBy(self, dx, dy):
|
def scrollContentsBy(self, dx, dy):
|
||||||
# Needed as Qt bug causes headerview to not always update when scrolling
|
# Needed as Qt bug causes headerview to not always update when scrolling
|
||||||
QTableView.scrollContentsBy(self, dx, dy)
|
QTableView.scrollContentsBy(self, dx, dy)
|
||||||
|
@ -18,11 +18,11 @@ from calibre.gui2.widgets import EnLineEdit, FormatList, ImageView
|
|||||||
from calibre.gui2.complete import MultiCompleteLineEdit, MultiCompleteComboBox
|
from calibre.gui2.complete import MultiCompleteLineEdit, MultiCompleteComboBox
|
||||||
from calibre.utils.icu import sort_key
|
from calibre.utils.icu import sort_key
|
||||||
from calibre.utils.config import tweaks, prefs
|
from calibre.utils.config import tweaks, prefs
|
||||||
from calibre.ebooks.metadata import title_sort, authors_to_string, \
|
from calibre.ebooks.metadata import (title_sort, authors_to_string,
|
||||||
string_to_authors, check_isbn
|
string_to_authors, check_isbn)
|
||||||
from calibre.ebooks.metadata.meta import get_metadata
|
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
|
from calibre import strftime
|
||||||
from calibre.ebooks import BOOK_EXTENSIONS
|
from calibre.ebooks import BOOK_EXTENSIONS
|
||||||
@ -805,6 +805,7 @@ class CommentsEdit(Editor): # {{{
|
|||||||
else:
|
else:
|
||||||
val = comments_to_html(val)
|
val = comments_to_html(val)
|
||||||
self.html = val
|
self.html = val
|
||||||
|
self.wyswyg_dirtied()
|
||||||
return property(fget=fget, fset=fset)
|
return property(fget=fget, fset=fset)
|
||||||
|
|
||||||
def initialize(self, db, id_):
|
def initialize(self, db, id_):
|
||||||
|
@ -253,7 +253,7 @@ class ResultsView(QTableView): # {{{
|
|||||||
parts.append('</center>')
|
parts.append('</center>')
|
||||||
if book.identifiers:
|
if book.identifiers:
|
||||||
urls = urls_from_identifiers(book.identifiers)
|
urls = urls_from_identifiers(book.identifiers)
|
||||||
ids = ['<a href="%s">%s</a>'%(url, name) for name, url in urls]
|
ids = ['<a href="%s">%s</a>'%(url, name) for name, ign, ign, url in urls]
|
||||||
if ids:
|
if ids:
|
||||||
parts.append('<div><b>%s:</b> %s</div><br>'%(_('See at'), ', '.join(ids)))
|
parts.append('<div><b>%s:</b> %s</div><br>'%(_('See at'), ', '.join(ids)))
|
||||||
if book.tags:
|
if book.tags:
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
<rect>
|
<rect>
|
||||||
<x>0</x>
|
<x>0</x>
|
||||||
<y>0</y>
|
<y>0</y>
|
||||||
<width>672</width>
|
<width>941</width>
|
||||||
<height>563</height>
|
<height>563</height>
|
||||||
</rect>
|
</rect>
|
||||||
</property>
|
</property>
|
||||||
@ -22,7 +22,7 @@
|
|||||||
<property name="sizeHint" stdset="0">
|
<property name="sizeHint" stdset="0">
|
||||||
<size>
|
<size>
|
||||||
<width>10</width>
|
<width>10</width>
|
||||||
<height>00</height>
|
<height>0</height>
|
||||||
</size>
|
</size>
|
||||||
</property>
|
</property>
|
||||||
</spacer>
|
</spacer>
|
||||||
@ -50,13 +50,13 @@
|
|||||||
</item>
|
</item>
|
||||||
<item row="2" column="2">
|
<item row="2" column="2">
|
||||||
<widget class="QCheckBox" name="opt_bools_are_tristate">
|
<widget class="QCheckBox" name="opt_bools_are_tristate">
|
||||||
<property name="text">
|
|
||||||
<string>Yes/No columns have three values (Requires restart)</string>
|
|
||||||
</property>
|
|
||||||
<property name="toolTip">
|
<property name="toolTip">
|
||||||
<string>If checked, Yes/No custom columns values can be Yes, No, or Unknown.
|
<string>If checked, Yes/No custom columns values can be Yes, No, or Unknown.
|
||||||
If not checked, the values can be Yes or No.</string>
|
If not checked, the values can be Yes or No.</string>
|
||||||
</property>
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>Yes/No columns have three values (Requires restart)</string>
|
||||||
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="4" column="0">
|
<item row="4" column="0">
|
||||||
@ -304,7 +304,7 @@ If not checked, the values can be Yes or No.</string>
|
|||||||
</layout>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="30" column="0" colspan="3">
|
<item row="9" column="2">
|
||||||
<widget class="QPushButton" name="reset_confirmation_button">
|
<widget class="QPushButton" name="reset_confirmation_button">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Reset all disabled &confirmation dialogs</string>
|
<string>Reset all disabled &confirmation dialogs</string>
|
||||||
|
@ -158,7 +158,7 @@ class CreateCustomColumn(QDialog, Ui_QCreateCustomColumn):
|
|||||||
{
|
{
|
||||||
'isbn': '{identifiers:select(isbn)}',
|
'isbn': '{identifiers:select(isbn)}',
|
||||||
'formats': '{formats}',
|
'formats': '{formats}',
|
||||||
'last_modified':'''{last_modified:'format_date($, "dd MMM yy")'}'''
|
'last_modified':'''{last_modified:'format_date($, "dd MMM yyyy")'}'''
|
||||||
}[which])
|
}[which])
|
||||||
self.composite_sort_by.setCurrentIndex(2 if which == 'last_modified' else 0)
|
self.composite_sort_by.setCurrentIndex(2 if which == 'last_modified' else 0)
|
||||||
|
|
||||||
|
@ -5,15 +5,91 @@ __license__ = 'GPL v3'
|
|||||||
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||||
__docformat__ = 'restructuredtext en'
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
from PyQt4.Qt import QApplication, QFont, QFontInfo, QFontDialog
|
from PyQt4.Qt import (QApplication, QFont, QFontInfo, QFontDialog,
|
||||||
|
QAbstractListModel, Qt)
|
||||||
|
|
||||||
from calibre.gui2.preferences import ConfigWidgetBase, test_widget, CommaSeparatedList
|
from calibre.gui2.preferences import ConfigWidgetBase, test_widget, CommaSeparatedList
|
||||||
from calibre.gui2.preferences.look_feel_ui import Ui_Form
|
from calibre.gui2.preferences.look_feel_ui import Ui_Form
|
||||||
from calibre.gui2 import config, gprefs, qt_app
|
from calibre.gui2 import config, gprefs, qt_app
|
||||||
from calibre.utils.localization import available_translations, \
|
from calibre.utils.localization import (available_translations,
|
||||||
get_language, get_lang
|
get_language, get_lang)
|
||||||
from calibre.utils.config import prefs
|
from calibre.utils.config import prefs
|
||||||
from calibre.utils.icu import sort_key
|
from calibre.utils.icu import sort_key
|
||||||
|
from calibre.gui2 import NONE
|
||||||
|
from calibre.gui2.book_details import get_field_list
|
||||||
|
|
||||||
|
class DisplayedFields(QAbstractListModel): # {{{
|
||||||
|
|
||||||
|
def __init__(self, db, parent=None):
|
||||||
|
QAbstractListModel.__init__(self, parent)
|
||||||
|
|
||||||
|
self.fields = []
|
||||||
|
self.db = db
|
||||||
|
self.changed = False
|
||||||
|
|
||||||
|
def initialize(self, use_defaults=False):
|
||||||
|
self.fields = [[x[0], x[1]] for x in
|
||||||
|
get_field_list(self.db.field_metadata,
|
||||||
|
use_defaults=use_defaults)]
|
||||||
|
self.reset()
|
||||||
|
self.changed = True
|
||||||
|
|
||||||
|
def rowCount(self, *args):
|
||||||
|
return len(self.fields)
|
||||||
|
|
||||||
|
def data(self, index, role):
|
||||||
|
try:
|
||||||
|
field, visible = self.fields[index.row()]
|
||||||
|
except:
|
||||||
|
return NONE
|
||||||
|
if role == Qt.DisplayRole:
|
||||||
|
name = field
|
||||||
|
try:
|
||||||
|
name = self.db.field_metadata[field]['name']
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
if not name:
|
||||||
|
name = field
|
||||||
|
return name
|
||||||
|
if role == Qt.CheckStateRole:
|
||||||
|
return Qt.Checked if visible else Qt.Unchecked
|
||||||
|
return NONE
|
||||||
|
|
||||||
|
def flags(self, index):
|
||||||
|
ans = QAbstractListModel.flags(self, index)
|
||||||
|
return ans | Qt.ItemIsUserCheckable
|
||||||
|
|
||||||
|
def setData(self, index, val, role):
|
||||||
|
ret = False
|
||||||
|
if role == Qt.CheckStateRole:
|
||||||
|
val, ok = val.toInt()
|
||||||
|
if ok:
|
||||||
|
self.fields[index.row()][1] = bool(val)
|
||||||
|
self.changed = True
|
||||||
|
ret = True
|
||||||
|
self.dataChanged.emit(index, index)
|
||||||
|
return ret
|
||||||
|
|
||||||
|
def restore_defaults(self):
|
||||||
|
self.initialize(use_defaults=True)
|
||||||
|
|
||||||
|
def commit(self):
|
||||||
|
if self.changed:
|
||||||
|
gprefs['book_display_fields'] = self.fields
|
||||||
|
|
||||||
|
def move(self, idx, delta):
|
||||||
|
row = idx.row() + delta
|
||||||
|
if row >= 0 and row < len(self.fields):
|
||||||
|
t = self.fields[row]
|
||||||
|
self.fields[row] = self.fields[row-delta]
|
||||||
|
self.fields[row-delta] = t
|
||||||
|
self.dataChanged.emit(idx, idx)
|
||||||
|
idx = self.index(row)
|
||||||
|
self.dataChanged.emit(idx, idx)
|
||||||
|
self.changed = True
|
||||||
|
return idx
|
||||||
|
|
||||||
|
# }}}
|
||||||
|
|
||||||
class ConfigWidget(ConfigWidgetBase, Ui_Form):
|
class ConfigWidget(ConfigWidgetBase, Ui_Form):
|
||||||
|
|
||||||
@ -76,11 +152,18 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
|
|||||||
self.current_font = self.initial_font = None
|
self.current_font = self.initial_font = None
|
||||||
self.change_font_button.clicked.connect(self.change_font)
|
self.change_font_button.clicked.connect(self.change_font)
|
||||||
|
|
||||||
|
self.display_model = DisplayedFields(self.gui.current_db,
|
||||||
|
self.field_display_order)
|
||||||
|
self.display_model.dataChanged.connect(self.changed_signal)
|
||||||
|
self.field_display_order.setModel(self.display_model)
|
||||||
|
self.df_up_button.clicked.connect(self.move_df_up)
|
||||||
|
self.df_down_button.clicked.connect(self.move_df_down)
|
||||||
|
|
||||||
def initialize(self):
|
def initialize(self):
|
||||||
ConfigWidgetBase.initialize(self)
|
ConfigWidgetBase.initialize(self)
|
||||||
self.current_font = self.initial_font = gprefs['font']
|
self.current_font = self.initial_font = gprefs['font']
|
||||||
self.update_font_display()
|
self.update_font_display()
|
||||||
|
self.display_model.initialize()
|
||||||
|
|
||||||
def restore_defaults(self):
|
def restore_defaults(self):
|
||||||
ConfigWidgetBase.restore_defaults(self)
|
ConfigWidgetBase.restore_defaults(self)
|
||||||
@ -89,6 +172,8 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
|
|||||||
if ofont is not None:
|
if ofont is not None:
|
||||||
self.changed_signal.emit()
|
self.changed_signal.emit()
|
||||||
self.update_font_display()
|
self.update_font_display()
|
||||||
|
self.display_model.restore_defaults()
|
||||||
|
self.changed_signal.emit()
|
||||||
|
|
||||||
def build_font_obj(self):
|
def build_font_obj(self):
|
||||||
font_info = self.current_font
|
font_info = self.current_font
|
||||||
@ -107,6 +192,24 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
|
|||||||
self.font_display.setText(name +
|
self.font_display.setText(name +
|
||||||
' [%dpt]'%fi.pointSize())
|
' [%dpt]'%fi.pointSize())
|
||||||
|
|
||||||
|
def move_df_up(self):
|
||||||
|
idx = self.field_display_order.currentIndex()
|
||||||
|
if idx.isValid():
|
||||||
|
idx = self.display_model.move(idx, -1)
|
||||||
|
if idx is not None:
|
||||||
|
sm = self.field_display_order.selectionModel()
|
||||||
|
sm.select(idx, sm.ClearAndSelect)
|
||||||
|
self.field_display_order.setCurrentIndex(idx)
|
||||||
|
|
||||||
|
def move_df_down(self):
|
||||||
|
idx = self.field_display_order.currentIndex()
|
||||||
|
if idx.isValid():
|
||||||
|
idx = self.display_model.move(idx, 1)
|
||||||
|
if idx is not None:
|
||||||
|
sm = self.field_display_order.selectionModel()
|
||||||
|
sm.select(idx, sm.ClearAndSelect)
|
||||||
|
self.field_display_order.setCurrentIndex(idx)
|
||||||
|
|
||||||
def change_font(self, *args):
|
def change_font(self, *args):
|
||||||
fd = QFontDialog(self.build_font_obj(), self)
|
fd = QFontDialog(self.build_font_obj(), self)
|
||||||
if fd.exec_() == fd.Accepted:
|
if fd.exec_() == fd.Accepted:
|
||||||
@ -123,14 +226,16 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
|
|||||||
gprefs['font'] = self.current_font
|
gprefs['font'] = self.current_font
|
||||||
QApplication.setFont(self.font_display.font())
|
QApplication.setFont(self.font_display.font())
|
||||||
rr = True
|
rr = True
|
||||||
|
self.display_model.commit()
|
||||||
return rr
|
return rr
|
||||||
|
|
||||||
|
|
||||||
def refresh_gui(self, gui):
|
def refresh_gui(self, gui):
|
||||||
self.update_font_display()
|
self.update_font_display()
|
||||||
gui.tags_view.reread_collapse_parameters()
|
gui.tags_view.reread_collapse_parameters()
|
||||||
|
gui.library_view.refresh_book_details()
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
app = QApplication([])
|
from calibre.gui2 import Application
|
||||||
|
app = Application([])
|
||||||
test_widget('Interface', 'Look & Feel')
|
test_widget('Interface', 'Look & Feel')
|
||||||
|
|
||||||
|
@ -7,13 +7,27 @@
|
|||||||
<x>0</x>
|
<x>0</x>
|
||||||
<y>0</y>
|
<y>0</y>
|
||||||
<width>717</width>
|
<width>717</width>
|
||||||
<height>444</height>
|
<height>390</height>
|
||||||
</rect>
|
</rect>
|
||||||
</property>
|
</property>
|
||||||
<property name="windowTitle">
|
<property name="windowTitle">
|
||||||
<string>Form</string>
|
<string>Form</string>
|
||||||
</property>
|
</property>
|
||||||
<layout class="QGridLayout" name="gridLayout_2">
|
<layout class="QGridLayout" name="gridLayout_2">
|
||||||
|
<item row="0" column="0">
|
||||||
|
<widget class="QTabWidget" name="tabWidget">
|
||||||
|
<property name="currentIndex">
|
||||||
|
<number>0</number>
|
||||||
|
</property>
|
||||||
|
<widget class="QWidget" name="tab">
|
||||||
|
<attribute name="icon">
|
||||||
|
<iconset resource="../../../../resources/images.qrc">
|
||||||
|
<normaloff>:/images/lt.png</normaloff>:/images/lt.png</iconset>
|
||||||
|
</attribute>
|
||||||
|
<attribute name="title">
|
||||||
|
<string>Main Interface</string>
|
||||||
|
</attribute>
|
||||||
|
<layout class="QGridLayout" name="gridLayout_9">
|
||||||
<item row="0" column="0">
|
<item row="0" column="0">
|
||||||
<widget class="QLabel" name="label_17">
|
<widget class="QLabel" name="label_17">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
@ -41,19 +55,6 @@
|
|||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="1" column="0">
|
<item row="1" column="0">
|
||||||
<widget class="QLabel" name="label_6">
|
|
||||||
<property name="text">
|
|
||||||
<string>&Number of covers to show in browse mode (needs restart):</string>
|
|
||||||
</property>
|
|
||||||
<property name="buddy">
|
|
||||||
<cstring>opt_cover_flow_queue_length</cstring>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="1" column="1">
|
|
||||||
<widget class="QSpinBox" name="opt_cover_flow_queue_length"/>
|
|
||||||
</item>
|
|
||||||
<item row="2" column="0">
|
|
||||||
<widget class="QLabel" name="label_7">
|
<widget class="QLabel" name="label_7">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Choose &language (requires restart):</string>
|
<string>Choose &language (requires restart):</string>
|
||||||
@ -63,7 +64,7 @@
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="2" column="1">
|
<item row="1" column="1">
|
||||||
<widget class="QComboBox" name="opt_language">
|
<widget class="QComboBox" name="opt_language">
|
||||||
<property name="sizeAdjustPolicy">
|
<property name="sizeAdjustPolicy">
|
||||||
<enum>QComboBox::AdjustToMinimumContentsLengthWithIcon</enum>
|
<enum>QComboBox::AdjustToMinimumContentsLengthWithIcon</enum>
|
||||||
@ -73,17 +74,14 @@
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="3" column="0">
|
<item row="2" column="0">
|
||||||
<widget class="QCheckBox" name="opt_show_avg_rating">
|
<widget class="QCheckBox" name="opt_systray_icon">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Show &average ratings in the tags browser</string>
|
<string>Enable system &tray icon (needs restart)</string>
|
||||||
</property>
|
|
||||||
<property name="checked">
|
|
||||||
<bool>true</bool>
|
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="3" column="1">
|
<item row="2" column="1">
|
||||||
<widget class="QCheckBox" name="opt_disable_animations">
|
<widget class="QCheckBox" name="opt_disable_animations">
|
||||||
<property name="toolTip">
|
<property name="toolTip">
|
||||||
<string>Disable all animations. Useful if you have a slow/old computer.</string>
|
<string>Disable all animations. Useful if you have a slow/old computer.</string>
|
||||||
@ -93,136 +91,31 @@
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="4" column="0">
|
<item row="3" column="0">
|
||||||
<widget class="QCheckBox" name="opt_systray_icon">
|
|
||||||
<property name="text">
|
|
||||||
<string>Enable system &tray icon (needs restart)</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="4" column="1">
|
|
||||||
<widget class="QCheckBox" name="opt_show_splash_screen">
|
|
||||||
<property name="text">
|
|
||||||
<string>Show &splash screen at startup</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="5" column="0">
|
|
||||||
<widget class="QCheckBox" name="opt_disable_tray_notification">
|
<widget class="QCheckBox" name="opt_disable_tray_notification">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Disable &notifications in system tray</string>
|
<string>Disable &notifications in system tray</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="5" column="1">
|
<item row="3" column="1">
|
||||||
<widget class="QCheckBox" name="opt_use_roman_numerals_for_series_number">
|
<widget class="QCheckBox" name="opt_show_splash_screen">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Use &Roman numerals for series</string>
|
<string>Show &splash screen at startup</string>
|
||||||
</property>
|
|
||||||
<property name="checked">
|
|
||||||
<bool>true</bool>
|
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="6" column="0" colspan="2">
|
<item row="5" column="0" colspan="2">
|
||||||
<widget class="QCheckBox" name="opt_separate_cover_flow">
|
|
||||||
<property name="text">
|
|
||||||
<string>Show cover &browser in a separate window (needs restart)</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="7" column="0" colspan="2">
|
|
||||||
<layout class="QHBoxLayout">
|
|
||||||
<item>
|
|
||||||
<widget class="QLabel" name="label_6">
|
|
||||||
<property name="text">
|
|
||||||
<string>Tags browser category &partitioning method:</string>
|
|
||||||
</property>
|
|
||||||
<property name="buddy">
|
|
||||||
<cstring>opt_tags_browser_partition_method</cstring>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<widget class="QComboBox" name="opt_tags_browser_partition_method">
|
|
||||||
<property name="toolTip">
|
|
||||||
<string>Choose how tag browser subcategories are displayed when
|
|
||||||
there are more items than the limit. Select by first
|
|
||||||
letter to see an A, B, C list. Choose partitioned to
|
|
||||||
have a list of fixed-sized groups. Set to disabled
|
|
||||||
if you never want subcategories</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<widget class="QLabel" name="label_6">
|
|
||||||
<property name="text">
|
|
||||||
<string>&Collapse when more items than:</string>
|
|
||||||
</property>
|
|
||||||
<property name="buddy">
|
|
||||||
<cstring>opt_tags_browser_collapse_at</cstring>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<widget class="QSpinBox" name="opt_tags_browser_collapse_at">
|
|
||||||
<property name="toolTip">
|
|
||||||
<string>If a Tag Browser category has more than this number of items, it is divided
|
|
||||||
up into sub-categories. If the partition method is set to disable, this value is ignored.</string>
|
|
||||||
</property>
|
|
||||||
<property name="maximum">
|
|
||||||
<number>10000</number>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item>
|
|
||||||
<spacer name="verticalSpacer">
|
|
||||||
<property name="orientation">
|
|
||||||
<enum>Qt::Horizontal</enum>
|
|
||||||
</property>
|
|
||||||
<property name="sizeHint" stdset="0">
|
|
||||||
<size>
|
|
||||||
<width>20</width>
|
|
||||||
<height>5</height>
|
|
||||||
</size>
|
|
||||||
</property>
|
|
||||||
</spacer>
|
|
||||||
</item>
|
|
||||||
</layout>
|
|
||||||
</item>
|
|
||||||
<item row="8" column="0">
|
|
||||||
<widget class="QLabel" name="label_81">
|
|
||||||
<property name="text">
|
|
||||||
<string>Categories with &hierarchical items:</string>
|
|
||||||
</property>
|
|
||||||
<property name="buddy">
|
|
||||||
<cstring>opt_categories_using_hierarchy</cstring>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="8" column="1">
|
|
||||||
<widget class="MultiCompleteLineEdit" name="opt_categories_using_hierarchy">
|
|
||||||
<property name="toolTip">
|
|
||||||
<string>A comma-separated list of columns in which items containing
|
|
||||||
periods are displayed in the tag browser trees. For example, if
|
|
||||||
this box contains 'tags' then tags of the form 'Mystery.English'
|
|
||||||
and 'Mystery.Thriller' will be displayed with English and Thriller
|
|
||||||
both under 'Mystery'. If 'tags' is not in this box,
|
|
||||||
then the tags will be displayed each on their own line.</string>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
<item row="15" column="0" colspan="2">
|
|
||||||
<widget class="QGroupBox" name="groupBox_2">
|
<widget class="QGroupBox" name="groupBox_2">
|
||||||
<property name="title">
|
<property name="title">
|
||||||
<string>&Toolbar</string>
|
<string>&Toolbar</string>
|
||||||
</property>
|
</property>
|
||||||
<layout class="QGridLayout" name="gridLayout">
|
<layout class="QGridLayout" name="gridLayout_8">
|
||||||
<item row="0" column="1">
|
<item row="0" column="1">
|
||||||
<widget class="QComboBox" name="opt_toolbar_icon_size"/>
|
<widget class="QComboBox" name="opt_toolbar_icon_size"/>
|
||||||
</item>
|
</item>
|
||||||
<item row="0" column="0">
|
<item row="0" column="0">
|
||||||
<widget class="QLabel" name="label">
|
<widget class="QLabel" name="label_5">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>&Icon size:</string>
|
<string>&Icon size:</string>
|
||||||
</property>
|
</property>
|
||||||
@ -235,7 +128,7 @@ then the tags will be displayed each on their own line.</string>
|
|||||||
<widget class="QComboBox" name="opt_toolbar_text"/>
|
<widget class="QComboBox" name="opt_toolbar_text"/>
|
||||||
</item>
|
</item>
|
||||||
<item row="1" column="0">
|
<item row="1" column="0">
|
||||||
<widget class="QLabel" name="label_4">
|
<widget class="QLabel" name="label_8">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Show &text under icons:</string>
|
<string>Show &text under icons:</string>
|
||||||
</property>
|
</property>
|
||||||
@ -247,7 +140,7 @@ then the tags will be displayed each on their own line.</string>
|
|||||||
</layout>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="16" column="0">
|
<item row="4" column="0">
|
||||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||||
<item>
|
<item>
|
||||||
<widget class="QLabel" name="label_2">
|
<widget class="QLabel" name="label_2">
|
||||||
@ -268,15 +161,15 @@ then the tags will be displayed each on their own line.</string>
|
|||||||
</item>
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</item>
|
</item>
|
||||||
<item row="16" column="1">
|
<item row="4" column="1">
|
||||||
<widget class="QPushButton" name="change_font_button">
|
<widget class="QPushButton" name="change_font_button">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Change &font (needs restart)</string>
|
<string>Change &font (needs restart)</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="17" column="0" colspan="2">
|
<item row="6" column="0">
|
||||||
<spacer name="verticalSpacer">
|
<spacer name="verticalSpacer_3">
|
||||||
<property name="orientation">
|
<property name="orientation">
|
||||||
<enum>Qt::Vertical</enum>
|
<enum>Qt::Vertical</enum>
|
||||||
</property>
|
</property>
|
||||||
@ -290,6 +183,234 @@ then the tags will be displayed each on their own line.</string>
|
|||||||
</item>
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
|
<widget class="QWidget" name="tab_4">
|
||||||
|
<attribute name="icon">
|
||||||
|
<iconset resource="../../../../resources/images.qrc">
|
||||||
|
<normaloff>:/images/book.png</normaloff>:/images/book.png</iconset>
|
||||||
|
</attribute>
|
||||||
|
<attribute name="title">
|
||||||
|
<string>Book Details</string>
|
||||||
|
</attribute>
|
||||||
|
<layout class="QGridLayout" name="gridLayout_12">
|
||||||
|
<item row="0" column="0" rowspan="2">
|
||||||
|
<widget class="QGroupBox" name="groupBox">
|
||||||
|
<property name="title">
|
||||||
|
<string>Select displayed metadata</string>
|
||||||
|
</property>
|
||||||
|
<layout class="QGridLayout" name="gridLayout_3">
|
||||||
|
<item row="0" column="0" rowspan="3">
|
||||||
|
<widget class="QListView" name="field_display_order">
|
||||||
|
<property name="alternatingRowColors">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="0" column="1">
|
||||||
|
<widget class="QToolButton" name="df_up_button">
|
||||||
|
<property name="toolTip">
|
||||||
|
<string>Move up</string>
|
||||||
|
</property>
|
||||||
|
<property name="icon">
|
||||||
|
<iconset resource="../../../../resources/images.qrc">
|
||||||
|
<normaloff>:/images/arrow-up.png</normaloff>:/images/arrow-up.png</iconset>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="2" column="1">
|
||||||
|
<widget class="QToolButton" name="df_down_button">
|
||||||
|
<property name="toolTip">
|
||||||
|
<string>Move down</string>
|
||||||
|
</property>
|
||||||
|
<property name="icon">
|
||||||
|
<iconset resource="../../../../resources/images.qrc">
|
||||||
|
<normaloff>:/images/arrow-down.png</normaloff>:/images/arrow-down.png</iconset>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="1" column="1">
|
||||||
|
<spacer name="verticalSpacer_5">
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Vertical</enum>
|
||||||
|
</property>
|
||||||
|
<property name="sizeHint" stdset="0">
|
||||||
|
<size>
|
||||||
|
<width>20</width>
|
||||||
|
<height>40</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
</spacer>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="0" column="1">
|
||||||
|
<widget class="QCheckBox" name="opt_use_roman_numerals_for_series_number">
|
||||||
|
<property name="text">
|
||||||
|
<string>Use &Roman numerals for series</string>
|
||||||
|
</property>
|
||||||
|
<property name="checked">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="1" column="1">
|
||||||
|
<widget class="QLabel" name="label_3">
|
||||||
|
<property name="text">
|
||||||
|
<string>Note that <b>comments</b> will always be displayed at the end, regardless of the position you assign here.</string>
|
||||||
|
</property>
|
||||||
|
<property name="wordWrap">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
<widget class="QWidget" name="tab_2">
|
||||||
|
<attribute name="icon">
|
||||||
|
<iconset resource="../../../../resources/images.qrc">
|
||||||
|
<normaloff>:/images/tags.png</normaloff>:/images/tags.png</iconset>
|
||||||
|
</attribute>
|
||||||
|
<attribute name="title">
|
||||||
|
<string>Tag Browser</string>
|
||||||
|
</attribute>
|
||||||
|
<layout class="QGridLayout" name="gridLayout_10">
|
||||||
|
<item row="0" column="0" colspan="2">
|
||||||
|
<widget class="QLabel" name="label_9">
|
||||||
|
<property name="text">
|
||||||
|
<string>Tags browser category &partitioning method:</string>
|
||||||
|
</property>
|
||||||
|
<property name="buddy">
|
||||||
|
<cstring>opt_tags_browser_partition_method</cstring>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="0" column="2">
|
||||||
|
<widget class="QComboBox" name="opt_tags_browser_partition_method">
|
||||||
|
<property name="toolTip">
|
||||||
|
<string>Choose how tag browser subcategories are displayed when
|
||||||
|
there are more items than the limit. Select by first
|
||||||
|
letter to see an A, B, C list. Choose partitioned to
|
||||||
|
have a list of fixed-sized groups. Set to disabled
|
||||||
|
if you never want subcategories</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="0" column="3">
|
||||||
|
<widget class="QLabel" name="label_10">
|
||||||
|
<property name="text">
|
||||||
|
<string>&Collapse when more items than:</string>
|
||||||
|
</property>
|
||||||
|
<property name="buddy">
|
||||||
|
<cstring>opt_tags_browser_collapse_at</cstring>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="0" column="4">
|
||||||
|
<widget class="QSpinBox" name="opt_tags_browser_collapse_at">
|
||||||
|
<property name="toolTip">
|
||||||
|
<string>If a Tag Browser category has more than this number of items, it is divided
|
||||||
|
up into sub-categories. If the partition method is set to disable, this value is ignored.</string>
|
||||||
|
</property>
|
||||||
|
<property name="maximum">
|
||||||
|
<number>10000</number>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="1" column="0" colspan="5">
|
||||||
|
<widget class="QCheckBox" name="opt_show_avg_rating">
|
||||||
|
<property name="text">
|
||||||
|
<string>Show &average ratings in the tags browser</string>
|
||||||
|
</property>
|
||||||
|
<property name="checked">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="2" column="0">
|
||||||
|
<widget class="QLabel" name="label_81">
|
||||||
|
<property name="text">
|
||||||
|
<string>Categories with &hierarchical items:</string>
|
||||||
|
</property>
|
||||||
|
<property name="buddy">
|
||||||
|
<cstring>opt_categories_using_hierarchy</cstring>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="3" column="0" colspan="5">
|
||||||
|
<spacer name="verticalSpacer_2">
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Vertical</enum>
|
||||||
|
</property>
|
||||||
|
<property name="sizeHint" stdset="0">
|
||||||
|
<size>
|
||||||
|
<width>690</width>
|
||||||
|
<height>252</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
</spacer>
|
||||||
|
</item>
|
||||||
|
<item row="2" column="2" colspan="3">
|
||||||
|
<widget class="MultiCompleteLineEdit" name="opt_categories_using_hierarchy">
|
||||||
|
<property name="toolTip">
|
||||||
|
<string>A comma-separated list of columns in which items containing
|
||||||
|
periods are displayed in the tag browser trees. For example, if
|
||||||
|
this box contains 'tags' then tags of the form 'Mystery.English'
|
||||||
|
and 'Mystery.Thriller' will be displayed with English and Thriller
|
||||||
|
both under 'Mystery'. If 'tags' is not in this box,
|
||||||
|
then the tags will be displayed each on their own line.</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
<widget class="QWidget" name="tab_3">
|
||||||
|
<attribute name="icon">
|
||||||
|
<iconset resource="../../../../resources/images.qrc">
|
||||||
|
<normaloff>:/images/cover_flow.png</normaloff>:/images/cover_flow.png</iconset>
|
||||||
|
</attribute>
|
||||||
|
<attribute name="title">
|
||||||
|
<string>Cover Browser</string>
|
||||||
|
</attribute>
|
||||||
|
<layout class="QGridLayout" name="gridLayout_11">
|
||||||
|
<item row="0" column="0" colspan="2">
|
||||||
|
<widget class="QCheckBox" name="opt_separate_cover_flow">
|
||||||
|
<property name="text">
|
||||||
|
<string>Show cover &browser in a separate window (needs restart)</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="1" column="0">
|
||||||
|
<widget class="QLabel" name="label_6">
|
||||||
|
<property name="text">
|
||||||
|
<string>&Number of covers to show in browse mode (needs restart):</string>
|
||||||
|
</property>
|
||||||
|
<property name="buddy">
|
||||||
|
<cstring>opt_cover_flow_queue_length</cstring>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="1" column="1">
|
||||||
|
<widget class="QSpinBox" name="opt_cover_flow_queue_length"/>
|
||||||
|
</item>
|
||||||
|
<item row="2" column="0" colspan="2">
|
||||||
|
<spacer name="verticalSpacer_4">
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Vertical</enum>
|
||||||
|
</property>
|
||||||
|
<property name="sizeHint" stdset="0">
|
||||||
|
<size>
|
||||||
|
<width>690</width>
|
||||||
|
<height>283</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
</spacer>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
<customwidgets>
|
<customwidgets>
|
||||||
<customwidget>
|
<customwidget>
|
||||||
<class>MultiCompleteLineEdit</class>
|
<class>MultiCompleteLineEdit</class>
|
||||||
@ -297,6 +418,8 @@ then the tags will be displayed each on their own line.</string>
|
|||||||
<header>calibre/gui2/complete.h</header>
|
<header>calibre/gui2/complete.h</header>
|
||||||
</customwidget>
|
</customwidget>
|
||||||
</customwidgets>
|
</customwidgets>
|
||||||
<resources/>
|
<resources>
|
||||||
|
<include location="../../../../resources/images.qrc"/>
|
||||||
|
</resources>
|
||||||
<connections/>
|
<connections/>
|
||||||
</ui>
|
</ui>
|
||||||
|
@ -87,7 +87,9 @@ class Category(QWidget): # {{{
|
|||||||
self.plugins = plugins
|
self.plugins = plugins
|
||||||
|
|
||||||
self.bar = QToolBar(self)
|
self.bar = QToolBar(self)
|
||||||
self.bar.setIconSize(QSize(48, 48))
|
self.bar.setStyleSheet(
|
||||||
|
'QToolBar { border: none; background: none }')
|
||||||
|
self.bar.setIconSize(QSize(32, 32))
|
||||||
self.bar.setMovable(False)
|
self.bar.setMovable(False)
|
||||||
self.bar.setFloatable(False)
|
self.bar.setFloatable(False)
|
||||||
self.bar.setToolButtonStyle(Qt.ToolButtonTextUnderIcon)
|
self.bar.setToolButtonStyle(Qt.ToolButtonTextUnderIcon)
|
||||||
|
@ -7,12 +7,15 @@ __copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
|
|||||||
__docformat__ = 'restructuredtext en'
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
import re
|
import re
|
||||||
|
from functools import partial
|
||||||
|
|
||||||
|
|
||||||
from PyQt4.Qt import QComboBox, Qt, QLineEdit, QStringList, pyqtSlot, QDialog, \
|
from PyQt4.Qt import QComboBox, Qt, QLineEdit, QStringList, pyqtSlot, QDialog, \
|
||||||
pyqtSignal, QCompleter, QAction, QKeySequence, QTimer, \
|
pyqtSignal, QCompleter, QAction, QKeySequence, QTimer, \
|
||||||
QString, QIcon
|
QString, QIcon, QMenu
|
||||||
|
|
||||||
from calibre.gui2 import config
|
from calibre.gui2 import config, error_dialog
|
||||||
|
from calibre.gui2.dialogs.confirm_delete import confirm
|
||||||
from calibre.gui2.dialogs.saved_search_editor import SavedSearchEditor
|
from calibre.gui2.dialogs.saved_search_editor import SavedSearchEditor
|
||||||
from calibre.gui2.dialogs.search import SearchDialog
|
from calibre.gui2.dialogs.search import SearchDialog
|
||||||
from calibre.utils.search_query_parser import saved_searches
|
from calibre.utils.search_query_parser import saved_searches
|
||||||
@ -330,6 +333,24 @@ class SavedSearchBox(QComboBox): # {{{
|
|||||||
self.saved_search_selected (name)
|
self.saved_search_selected (name)
|
||||||
self.changed.emit()
|
self.changed.emit()
|
||||||
|
|
||||||
|
def delete_current_search(self):
|
||||||
|
idx = self.currentIndex()
|
||||||
|
if idx <= 0:
|
||||||
|
error_dialog(self, _('Delete current search'),
|
||||||
|
_('No search is selected'), show=True)
|
||||||
|
return
|
||||||
|
if not confirm('<p>'+_('The selected search will be '
|
||||||
|
'<b>permanently deleted</b>. Are you sure?')
|
||||||
|
+'</p>', 'saved_search_delete', self):
|
||||||
|
return
|
||||||
|
ss = saved_searches().lookup(unicode(self.currentText()))
|
||||||
|
if ss is None:
|
||||||
|
return
|
||||||
|
saved_searches().delete(unicode(self.currentText()))
|
||||||
|
self.clear()
|
||||||
|
self.search_box.clear()
|
||||||
|
self.changed.emit()
|
||||||
|
|
||||||
# SIGNALed from the main UI
|
# SIGNALed from the main UI
|
||||||
def copy_search_button_clicked (self):
|
def copy_search_button_clicked (self):
|
||||||
idx = self.currentIndex();
|
idx = self.currentIndex();
|
||||||
@ -428,6 +449,22 @@ class SavedSearchBoxMixin(object): # {{{
|
|||||||
for x in ('copy', 'save'):
|
for x in ('copy', 'save'):
|
||||||
b = getattr(self, x+'_search_button')
|
b = getattr(self, x+'_search_button')
|
||||||
b.setStatusTip(b.toolTip())
|
b.setStatusTip(b.toolTip())
|
||||||
|
self.save_search_button.setToolTip('<p>' +
|
||||||
|
_("Save current search under the name shown in the box. "
|
||||||
|
"Press and hold for a pop-up options menu.") + '</p>')
|
||||||
|
self.save_search_button.setMenu(QMenu())
|
||||||
|
self.save_search_button.menu().addAction(
|
||||||
|
QIcon(I('plus.png')),
|
||||||
|
_('Create saved search'),
|
||||||
|
self.saved_search.save_search_button_clicked)
|
||||||
|
self.save_search_button.menu().addAction(
|
||||||
|
QIcon(I('trash.png')),
|
||||||
|
_('Delete saved search'),
|
||||||
|
self.saved_search.delete_current_search)
|
||||||
|
self.save_search_button.menu().addAction(
|
||||||
|
QIcon(I('search.png')),
|
||||||
|
_('Manage saved searches'),
|
||||||
|
partial(self.do_saved_search_edit, None))
|
||||||
|
|
||||||
def saved_searches_changed(self, set_restriction=None, recount=True):
|
def saved_searches_changed(self, set_restriction=None, recount=True):
|
||||||
p = sorted(saved_searches().names(), key=sort_key)
|
p = sorted(saved_searches().names(), key=sort_key)
|
||||||
|
@ -127,17 +127,39 @@ class StorePlugin(object): # {{{
|
|||||||
'''
|
'''
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def get_settings(self):
|
def update_cache(self, parent=None, timeout=60, force=False, suppress_progress=False):
|
||||||
'''
|
'''
|
||||||
This is only useful for plugins that implement
|
Some plugins need to keep an local cache of available books. This function
|
||||||
:attr:`config_widget` that is the only way to save
|
is called to update the caches. It is recommended to call this function
|
||||||
settings. This is used by plugins to get the saved
|
from :meth:`open`. Especially if :meth:`open` does anything other than
|
||||||
settings and apply when necessary.
|
open a web page.
|
||||||
|
|
||||||
:return: A dictionary filled with the settings used
|
This function can be called at any time. It is up to the plugin to determine
|
||||||
by this plugin.
|
if the cache really does need updating. Unless :param:`force` is True, then
|
||||||
|
the plugin must update the cache. The only time force should be True is if
|
||||||
|
this function is called by the plugin's configuration dialog.
|
||||||
|
|
||||||
|
if :param:`suppress_progress` is False it is safe to assume that this function
|
||||||
|
is being called from the main GUI thread so it is safe and recommended to use
|
||||||
|
a QProgressDialog to display what is happening and allow the user to cancel
|
||||||
|
the operation. if :param:`suppress_progress` is True then run the update
|
||||||
|
silently. In this case there is no guarantee what thread is calling this
|
||||||
|
function so no Qt related functionality that requires being run in the main
|
||||||
|
GUI thread should be run. E.G. Open a QProgressDialog.
|
||||||
|
|
||||||
|
:param parent: The parent object to be used by an GUI dialogs.
|
||||||
|
|
||||||
|
:param timeout: The maximum amount of time that should be spent in
|
||||||
|
any given network connection.
|
||||||
|
|
||||||
|
:param force: Force updating the cache even if the plugin has determined
|
||||||
|
it is not necessary.
|
||||||
|
|
||||||
|
:param suppress_progress: Should a progress indicator be shown.
|
||||||
|
|
||||||
|
:return: True if the cache was updated, False otherwise.
|
||||||
'''
|
'''
|
||||||
raise NotImplementedError()
|
return False
|
||||||
|
|
||||||
def do_genesis(self):
|
def do_genesis(self):
|
||||||
self.genesis()
|
self.genesis()
|
||||||
|
@ -24,10 +24,9 @@ from calibre.gui2.store.web_store_dialog import WebStoreDialog
|
|||||||
class BaenWebScriptionStore(BasicStoreConfig, StorePlugin):
|
class BaenWebScriptionStore(BasicStoreConfig, StorePlugin):
|
||||||
|
|
||||||
def open(self, parent=None, detail_item=None, external=False):
|
def open(self, parent=None, detail_item=None, external=False):
|
||||||
settings = self.get_settings()
|
|
||||||
url = 'http://www.webscription.net/'
|
url = 'http://www.webscription.net/'
|
||||||
|
|
||||||
if external or settings.get(self.name + '_open_external', False):
|
if external or self.config.get('open_external', False):
|
||||||
if detail_item:
|
if detail_item:
|
||||||
url = url + detail_item
|
url = url + detail_item
|
||||||
open_url(QUrl(url_slash_cleaner(url)))
|
open_url(QUrl(url_slash_cleaner(url)))
|
||||||
@ -37,7 +36,7 @@ class BaenWebScriptionStore(BasicStoreConfig, StorePlugin):
|
|||||||
detail_url = url + detail_item
|
detail_url = url + detail_item
|
||||||
d = WebStoreDialog(self.gui, url, parent, detail_url)
|
d = WebStoreDialog(self.gui, url, parent, detail_url)
|
||||||
d.setWindowTitle(self.name)
|
d.setWindowTitle(self.name)
|
||||||
d.set_tags(settings.get(self.name + '_tags', ''))
|
d.set_tags(self.config.get('tags', ''))
|
||||||
d.exec_()
|
d.exec_()
|
||||||
|
|
||||||
def search(self, query, max_results=10, timeout=60):
|
def search(self, query, max_results=10, timeout=60):
|
||||||
|
0
src/calibre/gui2/store/mobileread/__init__.py
Normal file
0
src/calibre/gui2/store/mobileread/__init__.py
Normal file
62
src/calibre/gui2/store/mobileread/cache_progress_dialog.py
Normal file
62
src/calibre/gui2/store/mobileread/cache_progress_dialog.py
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from __future__ import (unicode_literals, division, absolute_import, print_function)
|
||||||
|
|
||||||
|
__license__ = 'GPL 3'
|
||||||
|
__copyright__ = '2011, John Schember <john@nachtimwald.com>'
|
||||||
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
|
from PyQt4.Qt import QDialog
|
||||||
|
|
||||||
|
from calibre.gui2.store.mobileread.cache_progress_dialog_ui import Ui_Dialog
|
||||||
|
|
||||||
|
class CacheProgressDialog(QDialog, Ui_Dialog):
|
||||||
|
|
||||||
|
def __init__(self, parent=None, total=None):
|
||||||
|
QDialog.__init__(self, parent)
|
||||||
|
self.setupUi(self)
|
||||||
|
|
||||||
|
self.completed = 0
|
||||||
|
self.canceled = False
|
||||||
|
|
||||||
|
self.progress.setValue(0)
|
||||||
|
self.progress.setMinimum(0)
|
||||||
|
self.progress.setMaximum(total if total else 0)
|
||||||
|
|
||||||
|
def exec_(self):
|
||||||
|
self.completed = 0
|
||||||
|
self.canceled = False
|
||||||
|
QDialog.exec_(self)
|
||||||
|
|
||||||
|
def open(self):
|
||||||
|
self.completed = 0
|
||||||
|
self.canceled = False
|
||||||
|
QDialog.open(self)
|
||||||
|
|
||||||
|
def reject(self):
|
||||||
|
self.canceled = True
|
||||||
|
QDialog.reject(self)
|
||||||
|
|
||||||
|
def update_progress(self):
|
||||||
|
'''
|
||||||
|
completed is an int from 0 to total representing the number
|
||||||
|
records that have bee completed.
|
||||||
|
'''
|
||||||
|
self.set_progress(self.completed + 1)
|
||||||
|
|
||||||
|
def set_message(self, msg):
|
||||||
|
self.message.setText(msg)
|
||||||
|
|
||||||
|
def set_details(self, msg):
|
||||||
|
self.details.setText(msg)
|
||||||
|
|
||||||
|
def set_progress(self, completed):
|
||||||
|
'''
|
||||||
|
completed is an int from 0 to total representing the number
|
||||||
|
records that have bee completed.
|
||||||
|
'''
|
||||||
|
self.completed = completed
|
||||||
|
self.progress.setValue(self.completed)
|
||||||
|
|
||||||
|
def set_total(self, total):
|
||||||
|
self.progress.setMaximum(total)
|
104
src/calibre/gui2/store/mobileread/cache_progress_dialog.ui
Normal file
104
src/calibre/gui2/store/mobileread/cache_progress_dialog.ui
Normal file
@ -0,0 +1,104 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<ui version="4.0">
|
||||||
|
<class>Dialog</class>
|
||||||
|
<widget class="QDialog" name="Dialog">
|
||||||
|
<property name="geometry">
|
||||||
|
<rect>
|
||||||
|
<x>0</x>
|
||||||
|
<y>0</y>
|
||||||
|
<width>402</width>
|
||||||
|
<height>138</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
<property name="windowTitle">
|
||||||
|
<string>Dialog</string>
|
||||||
|
</property>
|
||||||
|
<layout class="QVBoxLayout" name="verticalLayout">
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="message">
|
||||||
|
<property name="text">
|
||||||
|
<string>Updating book cache</string>
|
||||||
|
</property>
|
||||||
|
<property name="alignment">
|
||||||
|
<set>Qt::AlignCenter</set>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QProgressBar" name="progress">
|
||||||
|
<property name="value">
|
||||||
|
<number>24</number>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="details">
|
||||||
|
<property name="text">
|
||||||
|
<string/>
|
||||||
|
</property>
|
||||||
|
<property name="alignment">
|
||||||
|
<set>Qt::AlignCenter</set>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<spacer name="verticalSpacer">
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Vertical</enum>
|
||||||
|
</property>
|
||||||
|
<property name="sizeHint" stdset="0">
|
||||||
|
<size>
|
||||||
|
<width>20</width>
|
||||||
|
<height>40</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
</spacer>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QDialogButtonBox" name="buttonBox">
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Horizontal</enum>
|
||||||
|
</property>
|
||||||
|
<property name="standardButtons">
|
||||||
|
<set>QDialogButtonBox::Cancel</set>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
<resources/>
|
||||||
|
<connections>
|
||||||
|
<connection>
|
||||||
|
<sender>buttonBox</sender>
|
||||||
|
<signal>accepted()</signal>
|
||||||
|
<receiver>Dialog</receiver>
|
||||||
|
<slot>accept()</slot>
|
||||||
|
<hints>
|
||||||
|
<hint type="sourcelabel">
|
||||||
|
<x>248</x>
|
||||||
|
<y>254</y>
|
||||||
|
</hint>
|
||||||
|
<hint type="destinationlabel">
|
||||||
|
<x>157</x>
|
||||||
|
<y>274</y>
|
||||||
|
</hint>
|
||||||
|
</hints>
|
||||||
|
</connection>
|
||||||
|
<connection>
|
||||||
|
<sender>buttonBox</sender>
|
||||||
|
<signal>rejected()</signal>
|
||||||
|
<receiver>Dialog</receiver>
|
||||||
|
<slot>reject()</slot>
|
||||||
|
<hints>
|
||||||
|
<hint type="sourcelabel">
|
||||||
|
<x>316</x>
|
||||||
|
<y>260</y>
|
||||||
|
</hint>
|
||||||
|
<hint type="destinationlabel">
|
||||||
|
<x>286</x>
|
||||||
|
<y>274</y>
|
||||||
|
</hint>
|
||||||
|
</hints>
|
||||||
|
</connection>
|
||||||
|
</connections>
|
||||||
|
</ui>
|
94
src/calibre/gui2/store/mobileread/cache_update_thread.py
Normal file
94
src/calibre/gui2/store/mobileread/cache_update_thread.py
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from __future__ import (unicode_literals, division, absolute_import, print_function)
|
||||||
|
|
||||||
|
__license__ = 'GPL 3'
|
||||||
|
__copyright__ = '2011, John Schember <john@nachtimwald.com>'
|
||||||
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
|
import time
|
||||||
|
from contextlib import closing
|
||||||
|
from threading import Thread
|
||||||
|
|
||||||
|
from lxml import html
|
||||||
|
|
||||||
|
from PyQt4.Qt import (pyqtSignal, QObject)
|
||||||
|
|
||||||
|
from calibre import browser
|
||||||
|
from calibre.gui2.store.search_result import SearchResult
|
||||||
|
|
||||||
|
class CacheUpdateThread(Thread, QObject):
|
||||||
|
|
||||||
|
total_changed = pyqtSignal(int)
|
||||||
|
update_progress = pyqtSignal(int)
|
||||||
|
update_details = pyqtSignal(unicode)
|
||||||
|
|
||||||
|
def __init__(self, config, seralize_books_function, timeout):
|
||||||
|
Thread.__init__(self)
|
||||||
|
QObject.__init__(self)
|
||||||
|
|
||||||
|
self.daemon = True
|
||||||
|
self.config = config
|
||||||
|
self.seralize_books = seralize_books_function
|
||||||
|
self.timeout = timeout
|
||||||
|
self._run = True
|
||||||
|
|
||||||
|
def abort(self):
|
||||||
|
self._run = False
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
url = 'http://www.mobileread.com/forums/ebooks.php?do=getlist&type=html'
|
||||||
|
|
||||||
|
self.update_details.emit(_('Checking last download date.'))
|
||||||
|
last_download = self.config.get('last_download', None)
|
||||||
|
# Don't update the book list if our cache is less than one week old.
|
||||||
|
if last_download and (time.time() - last_download) < 604800:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.update_details.emit(_('Downloading book list from MobileRead.'))
|
||||||
|
# Download the book list HTML file from MobileRead.
|
||||||
|
br = browser()
|
||||||
|
raw_data = None
|
||||||
|
try:
|
||||||
|
with closing(br.open(url, timeout=self.timeout)) as f:
|
||||||
|
raw_data = f.read()
|
||||||
|
except:
|
||||||
|
return
|
||||||
|
|
||||||
|
if not raw_data or not self._run:
|
||||||
|
return
|
||||||
|
|
||||||
|
self.update_details.emit(_('Processing books.'))
|
||||||
|
# Turn books listed in the HTML file into SearchResults's.
|
||||||
|
books = []
|
||||||
|
try:
|
||||||
|
data = html.fromstring(raw_data)
|
||||||
|
raw_books = data.xpath('//ul/li')
|
||||||
|
self.total_changed.emit(len(raw_books))
|
||||||
|
|
||||||
|
for i, book_data in enumerate(raw_books):
|
||||||
|
self.update_details.emit(_('%s of %s books processed.') % (i, len(raw_books)))
|
||||||
|
book = SearchResult()
|
||||||
|
book.detail_item = ''.join(book_data.xpath('.//a/@href'))
|
||||||
|
book.formats = ''.join(book_data.xpath('.//i/text()'))
|
||||||
|
book.formats = book.formats.strip()
|
||||||
|
|
||||||
|
text = ''.join(book_data.xpath('.//a/text()'))
|
||||||
|
if ':' in text:
|
||||||
|
book.author, q, text = text.partition(':')
|
||||||
|
book.author = book.author.strip()
|
||||||
|
book.title = text.strip()
|
||||||
|
books.append(book)
|
||||||
|
|
||||||
|
if not self._run:
|
||||||
|
books = []
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
self.update_progress.emit(i)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Save the book list and it's create time.
|
||||||
|
if books:
|
||||||
|
self.config['book_list'] = self.seralize_books(books)
|
||||||
|
self.config['last_download'] = time.time()
|
105
src/calibre/gui2/store/mobileread/mobileread_plugin.py
Normal file
105
src/calibre/gui2/store/mobileread/mobileread_plugin.py
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from __future__ import (unicode_literals, division, absolute_import, print_function)
|
||||||
|
|
||||||
|
__license__ = 'GPL 3'
|
||||||
|
__copyright__ = '2011, John Schember <john@nachtimwald.com>'
|
||||||
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
|
from threading import Lock
|
||||||
|
|
||||||
|
from PyQt4.Qt import (QUrl, QCoreApplication)
|
||||||
|
|
||||||
|
from calibre.gui2 import open_url
|
||||||
|
from calibre.gui2.store import StorePlugin
|
||||||
|
from calibre.gui2.store.basic_config import BasicStoreConfig
|
||||||
|
from calibre.gui2.store.search_result import SearchResult
|
||||||
|
from calibre.gui2.store.web_store_dialog import WebStoreDialog
|
||||||
|
from calibre.gui2.store.mobileread.models import SearchFilter
|
||||||
|
from calibre.gui2.store.mobileread.cache_progress_dialog import CacheProgressDialog
|
||||||
|
from calibre.gui2.store.mobileread.cache_update_thread import CacheUpdateThread
|
||||||
|
from calibre.gui2.store.mobileread.store_dialog import MobeReadStoreDialog
|
||||||
|
|
||||||
|
class MobileReadStore(BasicStoreConfig, StorePlugin):
|
||||||
|
|
||||||
|
def genesis(self):
|
||||||
|
self.lock = Lock()
|
||||||
|
|
||||||
|
def open(self, parent=None, detail_item=None, external=False):
|
||||||
|
url = 'http://www.mobileread.com/'
|
||||||
|
|
||||||
|
if external or self.config.get('open_external', False):
|
||||||
|
open_url(QUrl(detail_item if detail_item else url))
|
||||||
|
else:
|
||||||
|
if detail_item:
|
||||||
|
d = WebStoreDialog(self.gui, url, parent, detail_item)
|
||||||
|
d.setWindowTitle(self.name)
|
||||||
|
d.set_tags(self.config.get('tags', ''))
|
||||||
|
d.exec_()
|
||||||
|
else:
|
||||||
|
self.update_cache(parent, 30)
|
||||||
|
d = MobeReadStoreDialog(self, parent)
|
||||||
|
d.setWindowTitle(self.name)
|
||||||
|
d.exec_()
|
||||||
|
|
||||||
|
def search(self, query, max_results=10, timeout=60):
|
||||||
|
books = self.get_book_list()
|
||||||
|
|
||||||
|
sf = SearchFilter(books)
|
||||||
|
matches = sf.parse(query)
|
||||||
|
|
||||||
|
for book in matches:
|
||||||
|
book.price = '$0.00'
|
||||||
|
book.drm = SearchResult.DRM_UNLOCKED
|
||||||
|
yield book
|
||||||
|
|
||||||
|
def update_cache(self, parent=None, timeout=10, force=False, suppress_progress=False):
|
||||||
|
if self.lock.acquire(False):
|
||||||
|
try:
|
||||||
|
update_thread = CacheUpdateThread(self.config, self.seralize_books, timeout)
|
||||||
|
if not suppress_progress:
|
||||||
|
progress = CacheProgressDialog(parent)
|
||||||
|
progress.set_message(_('Updating MobileRead book cache...'))
|
||||||
|
|
||||||
|
update_thread.total_changed.connect(progress.set_total)
|
||||||
|
update_thread.update_progress.connect(progress.set_progress)
|
||||||
|
update_thread.update_details.connect(progress.set_details)
|
||||||
|
progress.rejected.connect(update_thread.abort)
|
||||||
|
|
||||||
|
progress.open()
|
||||||
|
update_thread.start()
|
||||||
|
while update_thread.is_alive() and not progress.canceled:
|
||||||
|
QCoreApplication.processEvents()
|
||||||
|
|
||||||
|
if progress.isVisible():
|
||||||
|
progress.accept()
|
||||||
|
return not progress.canceled
|
||||||
|
else:
|
||||||
|
update_thread.start()
|
||||||
|
finally:
|
||||||
|
self.lock.release()
|
||||||
|
|
||||||
|
def get_book_list(self):
|
||||||
|
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
|
||||||
|
|
||||||
|
def deseralize_books(self, sbooks):
|
||||||
|
books = []
|
||||||
|
for s in sbooks:
|
||||||
|
b = SearchResult()
|
||||||
|
b.author = s.get('author', '')
|
||||||
|
b.title = s.get('title', '')
|
||||||
|
b.detail_item = s.get('detail_item', '')
|
||||||
|
b.formats = s.get('formats', '')
|
||||||
|
books.append(b)
|
||||||
|
return books
|
190
src/calibre/gui2/store/mobileread/models.py
Normal file
190
src/calibre/gui2/store/mobileread/models.py
Normal file
@ -0,0 +1,190 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from __future__ import (unicode_literals, division, absolute_import, print_function)
|
||||||
|
|
||||||
|
__license__ = 'GPL 3'
|
||||||
|
__copyright__ = '2011, John Schember <john@nachtimwald.com>'
|
||||||
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
|
from operator import attrgetter
|
||||||
|
|
||||||
|
from PyQt4.Qt import (Qt, QAbstractItemModel, QModelIndex, QVariant, pyqtSignal)
|
||||||
|
|
||||||
|
from calibre.gui2 import NONE
|
||||||
|
from calibre.library.caches import _match, CONTAINS_MATCH, EQUALS_MATCH, \
|
||||||
|
REGEXP_MATCH
|
||||||
|
from calibre.utils.icu import sort_key
|
||||||
|
from calibre.utils.search_query_parser import SearchQueryParser
|
||||||
|
|
||||||
|
class BooksModel(QAbstractItemModel):
|
||||||
|
|
||||||
|
total_changed = pyqtSignal(int)
|
||||||
|
|
||||||
|
HEADERS = [_('Title'), _('Author(s)'), _('Format')]
|
||||||
|
|
||||||
|
def __init__(self, all_books):
|
||||||
|
QAbstractItemModel.__init__(self)
|
||||||
|
self.books = all_books
|
||||||
|
self.all_books = all_books
|
||||||
|
self.filter = ''
|
||||||
|
self.search_filter = SearchFilter(all_books)
|
||||||
|
self.sort_col = 0
|
||||||
|
self.sort_order = Qt.AscendingOrder
|
||||||
|
|
||||||
|
def get_book(self, index):
|
||||||
|
row = index.row()
|
||||||
|
if row < len(self.books):
|
||||||
|
return self.books[row]
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def search(self, filter):
|
||||||
|
self.filter = filter.strip()
|
||||||
|
if not self.filter:
|
||||||
|
self.books = self.all_books
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
self.books = list(self.search_filter.parse(self.filter))
|
||||||
|
except:
|
||||||
|
self.books = self.all_books
|
||||||
|
self.sort(self.sort_col, self.sort_order)
|
||||||
|
self.total_changed.emit(self.rowCount())
|
||||||
|
|
||||||
|
def index(self, row, column, parent=QModelIndex()):
|
||||||
|
return self.createIndex(row, column)
|
||||||
|
|
||||||
|
def parent(self, index):
|
||||||
|
if not index.isValid() or index.internalId() == 0:
|
||||||
|
return QModelIndex()
|
||||||
|
return self.createIndex(0, 0)
|
||||||
|
|
||||||
|
def rowCount(self, *args):
|
||||||
|
return len(self.books)
|
||||||
|
|
||||||
|
def columnCount(self, *args):
|
||||||
|
return len(self.HEADERS)
|
||||||
|
|
||||||
|
def headerData(self, section, orientation, role):
|
||||||
|
if role != Qt.DisplayRole:
|
||||||
|
return NONE
|
||||||
|
text = ''
|
||||||
|
if orientation == Qt.Horizontal:
|
||||||
|
if section < len(self.HEADERS):
|
||||||
|
text = self.HEADERS[section]
|
||||||
|
return QVariant(text)
|
||||||
|
else:
|
||||||
|
return QVariant(section+1)
|
||||||
|
|
||||||
|
def data(self, index, role):
|
||||||
|
row, col = index.row(), index.column()
|
||||||
|
result = self.books[row]
|
||||||
|
if role == Qt.DisplayRole:
|
||||||
|
if col == 0:
|
||||||
|
return QVariant(result.title)
|
||||||
|
elif col == 1:
|
||||||
|
return QVariant(result.author)
|
||||||
|
elif col == 2:
|
||||||
|
return QVariant(result.formats)
|
||||||
|
return NONE
|
||||||
|
|
||||||
|
def data_as_text(self, result, col):
|
||||||
|
text = ''
|
||||||
|
if col == 0:
|
||||||
|
text = result.title
|
||||||
|
elif col == 1:
|
||||||
|
text = result.author
|
||||||
|
elif col == 2:
|
||||||
|
text = result.formats
|
||||||
|
return text
|
||||||
|
|
||||||
|
def sort(self, col, order, reset=True):
|
||||||
|
self.sort_col = col
|
||||||
|
self.sort_order = order
|
||||||
|
if not self.books:
|
||||||
|
return
|
||||||
|
descending = order == Qt.DescendingOrder
|
||||||
|
self.books.sort(None,
|
||||||
|
lambda x: sort_key(unicode(self.data_as_text(x, col))),
|
||||||
|
descending)
|
||||||
|
if reset:
|
||||||
|
self.reset()
|
||||||
|
|
||||||
|
|
||||||
|
class SearchFilter(SearchQueryParser):
|
||||||
|
|
||||||
|
USABLE_LOCATIONS = [
|
||||||
|
'all',
|
||||||
|
'author',
|
||||||
|
'authors',
|
||||||
|
'format',
|
||||||
|
'formats',
|
||||||
|
'title',
|
||||||
|
]
|
||||||
|
|
||||||
|
def __init__(self, all_books=[]):
|
||||||
|
SearchQueryParser.__init__(self, locations=self.USABLE_LOCATIONS)
|
||||||
|
self.srs = set(all_books)
|
||||||
|
|
||||||
|
def universal_set(self):
|
||||||
|
return self.srs
|
||||||
|
|
||||||
|
def get_matches(self, location, query):
|
||||||
|
location = location.lower().strip()
|
||||||
|
if location == 'authors':
|
||||||
|
location = 'author'
|
||||||
|
elif location == 'formats':
|
||||||
|
location = 'format'
|
||||||
|
|
||||||
|
matchkind = CONTAINS_MATCH
|
||||||
|
if len(query) > 1:
|
||||||
|
if query.startswith('\\'):
|
||||||
|
query = query[1:]
|
||||||
|
elif query.startswith('='):
|
||||||
|
matchkind = EQUALS_MATCH
|
||||||
|
query = query[1:]
|
||||||
|
elif query.startswith('~'):
|
||||||
|
matchkind = REGEXP_MATCH
|
||||||
|
query = query[1:]
|
||||||
|
if matchkind != REGEXP_MATCH: ### leave case in regexps because it can be significant e.g. \S \W \D
|
||||||
|
query = query.lower()
|
||||||
|
|
||||||
|
if location not in self.USABLE_LOCATIONS:
|
||||||
|
return set([])
|
||||||
|
matches = set([])
|
||||||
|
all_locs = set(self.USABLE_LOCATIONS) - set(['all'])
|
||||||
|
locations = all_locs if location == 'all' else [location]
|
||||||
|
q = {
|
||||||
|
'author': lambda x: x.author.lower(),
|
||||||
|
'format': attrgetter('formats'),
|
||||||
|
'title': lambda x: x.title.lower(),
|
||||||
|
}
|
||||||
|
for x in ('author', 'format'):
|
||||||
|
q[x+'s'] = q[x]
|
||||||
|
for sr in self.srs:
|
||||||
|
for locvalue in locations:
|
||||||
|
accessor = q[locvalue]
|
||||||
|
if query == 'true':
|
||||||
|
if accessor(sr) is not None:
|
||||||
|
matches.add(sr)
|
||||||
|
continue
|
||||||
|
if query == 'false':
|
||||||
|
if accessor(sr) is None:
|
||||||
|
matches.add(sr)
|
||||||
|
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 = [accessor(sr)]
|
||||||
|
if _match(query, vals, m):
|
||||||
|
matches.add(sr)
|
||||||
|
break
|
||||||
|
except ValueError: # Unicode errors
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
return matches
|
83
src/calibre/gui2/store/mobileread/store_dialog.py
Normal file
83
src/calibre/gui2/store/mobileread/store_dialog.py
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from __future__ import (unicode_literals, division, absolute_import, print_function)
|
||||||
|
|
||||||
|
__license__ = 'GPL 3'
|
||||||
|
__copyright__ = '2011, John Schember <john@nachtimwald.com>'
|
||||||
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
|
|
||||||
|
from PyQt4.Qt import (Qt, QDialog, QIcon)
|
||||||
|
|
||||||
|
from calibre.gui2.store.search.adv_search_builder import AdvSearchBuilderDialog
|
||||||
|
from calibre.gui2.store.mobileread.models import BooksModel
|
||||||
|
from calibre.gui2.store.mobileread.store_dialog_ui import Ui_Dialog
|
||||||
|
|
||||||
|
class MobeReadStoreDialog(QDialog, Ui_Dialog):
|
||||||
|
|
||||||
|
def __init__(self, plugin, *args):
|
||||||
|
QDialog.__init__(self, *args)
|
||||||
|
self.setupUi(self)
|
||||||
|
|
||||||
|
self.plugin = plugin
|
||||||
|
|
||||||
|
self.adv_search_button.setIcon(QIcon(I('search.png')))
|
||||||
|
|
||||||
|
self._model = BooksModel(self.plugin.get_book_list())
|
||||||
|
self.results_view.setModel(self._model)
|
||||||
|
self.total.setText('%s' % self.results_view.model().rowCount())
|
||||||
|
|
||||||
|
self.search_button.clicked.connect(self.do_search)
|
||||||
|
self.adv_search_button.clicked.connect(self.build_adv_search)
|
||||||
|
self.results_view.activated.connect(self.open_store)
|
||||||
|
self.results_view.model().total_changed.connect(self.update_book_total)
|
||||||
|
self.finished.connect(self.dialog_closed)
|
||||||
|
|
||||||
|
self.restore_state()
|
||||||
|
|
||||||
|
def do_search(self):
|
||||||
|
self.results_view.model().search(unicode(self.search_query.text()))
|
||||||
|
|
||||||
|
def open_store(self, index):
|
||||||
|
result = self.results_view.model().get_book(index)
|
||||||
|
if result:
|
||||||
|
self.plugin.open(self, result.detail_item)
|
||||||
|
|
||||||
|
def update_book_total(self, total):
|
||||||
|
self.total.setText('%s' % total)
|
||||||
|
|
||||||
|
def build_adv_search(self):
|
||||||
|
adv = AdvSearchBuilderDialog(self)
|
||||||
|
adv.price_label.hide()
|
||||||
|
adv.price_box.hide()
|
||||||
|
if adv.exec_() == QDialog.Accepted:
|
||||||
|
self.search_query.setText(adv.search_string())
|
||||||
|
|
||||||
|
def restore_state(self):
|
||||||
|
geometry = self.plugin.config.get('dialog_geometry', None)
|
||||||
|
if geometry:
|
||||||
|
self.restoreGeometry(geometry)
|
||||||
|
|
||||||
|
results_cwidth = self.plugin.config.get('dialog_results_view_column_width')
|
||||||
|
if results_cwidth:
|
||||||
|
for i, x in enumerate(results_cwidth):
|
||||||
|
if i >= self.results_view.model().columnCount():
|
||||||
|
break
|
||||||
|
self.results_view.setColumnWidth(i, x)
|
||||||
|
else:
|
||||||
|
for i in xrange(self.results_view.model().columnCount()):
|
||||||
|
self.results_view.resizeColumnToContents(i)
|
||||||
|
|
||||||
|
self.results_view.model().sort_col = self.plugin.config.get('dialog_sort_col', 0)
|
||||||
|
self.results_view.model().sort_order = self.plugin.config.get('dialog_sort_order', Qt.AscendingOrder)
|
||||||
|
self.results_view.model().sort(self.results_view.model().sort_col, self.results_view.model().sort_order)
|
||||||
|
self.results_view.header().setSortIndicator(self.results_view.model().sort_col, self.results_view.model().sort_order)
|
||||||
|
|
||||||
|
def save_state(self):
|
||||||
|
self.plugin.config['dialog_geometry'] = bytearray(self.saveGeometry())
|
||||||
|
self.plugin.config['dialog_results_view_column_width'] = [self.results_view.columnWidth(i) for i in range(self.results_view.model().columnCount())]
|
||||||
|
self.plugin.config['dialog_sort_col'] = self.results_view.model().sort_col
|
||||||
|
self.plugin.config['dialog_sort_order'] = self.results_view.model().sort_order
|
||||||
|
|
||||||
|
def dialog_closed(self, result):
|
||||||
|
self.save_state()
|
@ -19,13 +19,30 @@
|
|||||||
<item>
|
<item>
|
||||||
<widget class="QLabel" name="label">
|
<widget class="QLabel" name="label">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Search:</string>
|
<string>&Query:</string>
|
||||||
|
</property>
|
||||||
|
<property name="buddy">
|
||||||
|
<cstring>search_query</cstring>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QToolButton" name="adv_search_button">
|
||||||
|
<property name="text">
|
||||||
|
<string>...</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<widget class="QLineEdit" name="search_query"/>
|
<widget class="QLineEdit" name="search_query"/>
|
||||||
</item>
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QPushButton" name="search_button">
|
||||||
|
<property name="text">
|
||||||
|
<string>Search</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
@ -1,316 +0,0 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
|
|
||||||
from __future__ import (unicode_literals, division, absolute_import, print_function)
|
|
||||||
|
|
||||||
__license__ = 'GPL 3'
|
|
||||||
__copyright__ = '2011, John Schember <john@nachtimwald.com>'
|
|
||||||
__docformat__ = 'restructuredtext en'
|
|
||||||
|
|
||||||
import difflib
|
|
||||||
import heapq
|
|
||||||
import time
|
|
||||||
from contextlib import closing
|
|
||||||
from threading import RLock
|
|
||||||
|
|
||||||
from lxml import html
|
|
||||||
|
|
||||||
from PyQt4.Qt import Qt, QUrl, QDialog, QAbstractItemModel, QModelIndex, QVariant, \
|
|
||||||
pyqtSignal
|
|
||||||
|
|
||||||
from calibre import browser
|
|
||||||
from calibre.gui2 import open_url, NONE
|
|
||||||
from calibre.gui2.store import StorePlugin
|
|
||||||
from calibre.gui2.store.basic_config import BasicStoreConfig
|
|
||||||
from calibre.gui2.store.mobileread_store_dialog_ui import Ui_Dialog
|
|
||||||
from calibre.gui2.store.search_result import SearchResult
|
|
||||||
from calibre.gui2.store.web_store_dialog import WebStoreDialog
|
|
||||||
from calibre.utils.icu import sort_key
|
|
||||||
|
|
||||||
class MobileReadStore(BasicStoreConfig, StorePlugin):
|
|
||||||
|
|
||||||
def genesis(self):
|
|
||||||
self.rlock = RLock()
|
|
||||||
|
|
||||||
def open(self, parent=None, detail_item=None, external=False):
|
|
||||||
url = 'http://www.mobileread.com/'
|
|
||||||
|
|
||||||
if external or self.config.get('open_external', False):
|
|
||||||
open_url(QUrl(detail_item if detail_item else url))
|
|
||||||
else:
|
|
||||||
if detail_item:
|
|
||||||
d = WebStoreDialog(self.gui, url, parent, detail_item)
|
|
||||||
d.setWindowTitle(self.name)
|
|
||||||
d.set_tags(self.config.get('tags', ''))
|
|
||||||
d.exec_()
|
|
||||||
else:
|
|
||||||
d = MobeReadStoreDialog(self, parent)
|
|
||||||
d.setWindowTitle(self.name)
|
|
||||||
d.exec_()
|
|
||||||
|
|
||||||
def search(self, query, max_results=10, timeout=60):
|
|
||||||
books = self.get_book_list(timeout=timeout)
|
|
||||||
|
|
||||||
query = query.lower()
|
|
||||||
query_parts = query.split(' ')
|
|
||||||
matches = []
|
|
||||||
s = difflib.SequenceMatcher()
|
|
||||||
for x in books:
|
|
||||||
ratio = 0
|
|
||||||
t_string = '%s %s' % (x.author.lower(), x.title.lower())
|
|
||||||
query_matches = []
|
|
||||||
for q in query_parts:
|
|
||||||
if q in t_string:
|
|
||||||
query_matches.append(q)
|
|
||||||
for q in query_matches:
|
|
||||||
s.set_seq2(q)
|
|
||||||
for p in t_string.split(' '):
|
|
||||||
s.set_seq1(p)
|
|
||||||
ratio += s.ratio()
|
|
||||||
if ratio > 0:
|
|
||||||
matches.append((ratio, x))
|
|
||||||
|
|
||||||
# Move the best scorers to head of list.
|
|
||||||
matches = heapq.nlargest(max_results, matches)
|
|
||||||
for score, book in matches:
|
|
||||||
book.price = '$0.00'
|
|
||||||
book.drm = SearchResult.DRM_UNLOCKED
|
|
||||||
yield book
|
|
||||||
|
|
||||||
def update_book_list(self, timeout=10):
|
|
||||||
with self.rlock:
|
|
||||||
url = 'http://www.mobileread.com/forums/ebooks.php?do=getlist&type=html'
|
|
||||||
|
|
||||||
last_download = self.config.get('last_download', None)
|
|
||||||
# Don't update the book list if our cache is less than one week old.
|
|
||||||
if last_download and (time.time() - last_download) < 604800:
|
|
||||||
return
|
|
||||||
|
|
||||||
# Download the book list HTML file from MobileRead.
|
|
||||||
br = browser()
|
|
||||||
raw_data = None
|
|
||||||
with closing(br.open(url, timeout=timeout)) as f:
|
|
||||||
raw_data = f.read()
|
|
||||||
|
|
||||||
if not raw_data:
|
|
||||||
return
|
|
||||||
|
|
||||||
# Turn books listed in the HTML file into SearchResults's.
|
|
||||||
books = []
|
|
||||||
try:
|
|
||||||
data = html.fromstring(raw_data)
|
|
||||||
for book_data in data.xpath('//ul/li'):
|
|
||||||
book = SearchResult()
|
|
||||||
book.detail_item = ''.join(book_data.xpath('.//a/@href'))
|
|
||||||
book.formats = ''.join(book_data.xpath('.//i/text()'))
|
|
||||||
book.formats = book.formats.strip()
|
|
||||||
|
|
||||||
text = ''.join(book_data.xpath('.//a/text()'))
|
|
||||||
if ':' in text:
|
|
||||||
book.author, q, text = text.partition(':')
|
|
||||||
book.author = book.author.strip()
|
|
||||||
book.title = text.strip()
|
|
||||||
books.append(book)
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Save the book list and it's create time.
|
|
||||||
if books:
|
|
||||||
self.config['last_download'] = time.time()
|
|
||||||
self.config['book_list'] = self.seralize_books(books)
|
|
||||||
|
|
||||||
def get_book_list(self, timeout=10):
|
|
||||||
self.update_book_list(timeout=timeout)
|
|
||||||
return self.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
|
|
||||||
|
|
||||||
def deseralize_books(self, sbooks):
|
|
||||||
books = []
|
|
||||||
for s in sbooks:
|
|
||||||
b = SearchResult()
|
|
||||||
b.author = s.get('author', '')
|
|
||||||
b.title = s.get('title', '')
|
|
||||||
b.detail_item = s.get('detail_item', '')
|
|
||||||
b.formats = s.get('formats', '')
|
|
||||||
books.append(b)
|
|
||||||
return books
|
|
||||||
|
|
||||||
|
|
||||||
class MobeReadStoreDialog(QDialog, Ui_Dialog):
|
|
||||||
|
|
||||||
def __init__(self, plugin, *args):
|
|
||||||
QDialog.__init__(self, *args)
|
|
||||||
self.setupUi(self)
|
|
||||||
|
|
||||||
self.plugin = plugin
|
|
||||||
|
|
||||||
self.model = BooksModel()
|
|
||||||
self.results_view.setModel(self.model)
|
|
||||||
self.results_view.model().set_books(self.plugin.get_book_list())
|
|
||||||
self.total.setText('%s' % self.model.rowCount())
|
|
||||||
|
|
||||||
self.results_view.activated.connect(self.open_store)
|
|
||||||
self.search_query.textChanged.connect(self.model.set_filter)
|
|
||||||
self.results_view.model().total_changed.connect(self.total.setText)
|
|
||||||
self.finished.connect(self.dialog_closed)
|
|
||||||
|
|
||||||
self.restore_state()
|
|
||||||
|
|
||||||
def open_store(self, index):
|
|
||||||
result = self.results_view.model().get_book(index)
|
|
||||||
if result:
|
|
||||||
self.plugin.open(self, result.detail_item)
|
|
||||||
|
|
||||||
def restore_state(self):
|
|
||||||
geometry = self.plugin.config.get('dialog_geometry', None)
|
|
||||||
if geometry:
|
|
||||||
self.restoreGeometry(geometry)
|
|
||||||
|
|
||||||
results_cwidth = self.plugin.config.get('dialog_results_view_column_width')
|
|
||||||
if results_cwidth:
|
|
||||||
for i, x in enumerate(results_cwidth):
|
|
||||||
if i >= self.results_view.model().columnCount():
|
|
||||||
break
|
|
||||||
self.results_view.setColumnWidth(i, x)
|
|
||||||
else:
|
|
||||||
for i in xrange(self.results_view.model().columnCount()):
|
|
||||||
self.results_view.resizeColumnToContents(i)
|
|
||||||
|
|
||||||
self.results_view.model().sort_col = self.plugin.config.get('dialog_sort_col', 0)
|
|
||||||
self.results_view.model().sort_order = self.plugin.config.get('dialog_sort_order', Qt.AscendingOrder)
|
|
||||||
self.results_view.model().sort(self.results_view.model().sort_col, self.results_view.model().sort_order)
|
|
||||||
self.results_view.header().setSortIndicator(self.results_view.model().sort_col, self.results_view.model().sort_order)
|
|
||||||
|
|
||||||
def save_state(self):
|
|
||||||
self.plugin.config['dialog_geometry'] = bytearray(self.saveGeometry())
|
|
||||||
self.plugin.config['dialog_results_view_column_width'] = [self.results_view.columnWidth(i) for i in range(self.model.columnCount())]
|
|
||||||
self.plugin.config['dialog_sort_col'] = self.results_view.model().sort_col
|
|
||||||
self.plugin.config['dialog_sort_order'] = self.results_view.model().sort_order
|
|
||||||
|
|
||||||
def dialog_closed(self, result):
|
|
||||||
self.save_state()
|
|
||||||
|
|
||||||
|
|
||||||
class BooksModel(QAbstractItemModel):
|
|
||||||
|
|
||||||
total_changed = pyqtSignal(unicode)
|
|
||||||
|
|
||||||
HEADERS = [_('Title'), _('Author(s)'), _('Format')]
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
QAbstractItemModel.__init__(self)
|
|
||||||
self.books = []
|
|
||||||
self.all_books = []
|
|
||||||
self.filter = ''
|
|
||||||
self.sort_col = 0
|
|
||||||
self.sort_order = Qt.AscendingOrder
|
|
||||||
|
|
||||||
def set_books(self, books):
|
|
||||||
self.books = books
|
|
||||||
self.all_books = books
|
|
||||||
|
|
||||||
self.sort(self.sort_col, self.sort_order)
|
|
||||||
|
|
||||||
def get_book(self, index):
|
|
||||||
row = index.row()
|
|
||||||
if row < len(self.books):
|
|
||||||
return self.books[row]
|
|
||||||
else:
|
|
||||||
return None
|
|
||||||
|
|
||||||
def set_filter(self, filter):
|
|
||||||
#self.layoutAboutToBeChanged.emit()
|
|
||||||
self.beginResetModel()
|
|
||||||
|
|
||||||
self.filter = unicode(filter)
|
|
||||||
self.books = []
|
|
||||||
if self.filter:
|
|
||||||
for b in self.all_books:
|
|
||||||
test = '%s %s %s' % (b.title, b.author, b.formats)
|
|
||||||
test = test.lower()
|
|
||||||
include = True
|
|
||||||
for item in self.filter.split(' '):
|
|
||||||
item = item.lower()
|
|
||||||
if item not in test:
|
|
||||||
include = False
|
|
||||||
break
|
|
||||||
if include:
|
|
||||||
self.books.append(b)
|
|
||||||
else:
|
|
||||||
self.books = self.all_books
|
|
||||||
|
|
||||||
self.sort(self.sort_col, self.sort_order, reset=False)
|
|
||||||
self.total_changed.emit('%s' % self.rowCount())
|
|
||||||
|
|
||||||
self.endResetModel()
|
|
||||||
#self.layoutChanged.emit()
|
|
||||||
|
|
||||||
def index(self, row, column, parent=QModelIndex()):
|
|
||||||
return self.createIndex(row, column)
|
|
||||||
|
|
||||||
def parent(self, index):
|
|
||||||
if not index.isValid() or index.internalId() == 0:
|
|
||||||
return QModelIndex()
|
|
||||||
return self.createIndex(0, 0)
|
|
||||||
|
|
||||||
def rowCount(self, *args):
|
|
||||||
return len(self.books)
|
|
||||||
|
|
||||||
def columnCount(self, *args):
|
|
||||||
return len(self.HEADERS)
|
|
||||||
|
|
||||||
def headerData(self, section, orientation, role):
|
|
||||||
if role != Qt.DisplayRole:
|
|
||||||
return NONE
|
|
||||||
text = ''
|
|
||||||
if orientation == Qt.Horizontal:
|
|
||||||
if section < len(self.HEADERS):
|
|
||||||
text = self.HEADERS[section]
|
|
||||||
return QVariant(text)
|
|
||||||
else:
|
|
||||||
return QVariant(section+1)
|
|
||||||
|
|
||||||
def data(self, index, role):
|
|
||||||
row, col = index.row(), index.column()
|
|
||||||
result = self.books[row]
|
|
||||||
if role == Qt.DisplayRole:
|
|
||||||
if col == 0:
|
|
||||||
return QVariant(result.title)
|
|
||||||
elif col == 1:
|
|
||||||
return QVariant(result.author)
|
|
||||||
elif col == 2:
|
|
||||||
return QVariant(result.formats)
|
|
||||||
return NONE
|
|
||||||
|
|
||||||
def data_as_text(self, result, col):
|
|
||||||
text = ''
|
|
||||||
if col == 0:
|
|
||||||
text = result.title
|
|
||||||
elif col == 1:
|
|
||||||
text = result.author
|
|
||||||
elif col == 2:
|
|
||||||
text = result.formats
|
|
||||||
return text
|
|
||||||
|
|
||||||
def sort(self, col, order, reset=True):
|
|
||||||
self.sort_col = col
|
|
||||||
self.sort_order = order
|
|
||||||
|
|
||||||
if not self.books:
|
|
||||||
return
|
|
||||||
descending = order == Qt.DescendingOrder
|
|
||||||
self.books.sort(None,
|
|
||||||
lambda x: sort_key(unicode(self.data_as_text(x, col))),
|
|
||||||
descending)
|
|
||||||
if reset:
|
|
||||||
self.reset()
|
|
||||||
|
|
@ -50,6 +50,9 @@ class OpenLibraryStore(BasicStoreConfig, StorePlugin):
|
|||||||
if counter <= 0:
|
if counter <= 0:
|
||||||
break
|
break
|
||||||
|
|
||||||
|
# Don't include books that don't have downloadable files.
|
||||||
|
if not data.xpath('boolean(./span[@class="actions"]//span[@class="label" and contains(text(), "Read")])'):
|
||||||
|
continue
|
||||||
id = ''.join(data.xpath('./span[@class="bookcover"]/a/@href'))
|
id = ''.join(data.xpath('./span[@class="bookcover"]/a/@href'))
|
||||||
if not id:
|
if not id:
|
||||||
continue
|
continue
|
||||||
@ -67,7 +70,7 @@ 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
|
s.drm = SearchResult.DRM_UNLOCKED
|
||||||
|
|
||||||
yield s
|
yield s
|
||||||
|
|
||||||
|
123
src/calibre/gui2/store/search/adv_search_builder.py
Normal file
123
src/calibre/gui2/store/search/adv_search_builder.py
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from __future__ import (unicode_literals, division, absolute_import, print_function)
|
||||||
|
|
||||||
|
__license__ = 'GPL 3'
|
||||||
|
__copyright__ = '2011, John Schember <john@nachtimwald.com>'
|
||||||
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
|
import re
|
||||||
|
|
||||||
|
from PyQt4.Qt import (QDialog, QDialogButtonBox)
|
||||||
|
|
||||||
|
from calibre.gui2.store.search.adv_search_builder_ui import Ui_Dialog
|
||||||
|
from calibre.library.caches import CONTAINS_MATCH, EQUALS_MATCH
|
||||||
|
|
||||||
|
class AdvSearchBuilderDialog(QDialog, Ui_Dialog):
|
||||||
|
|
||||||
|
def __init__(self, parent):
|
||||||
|
QDialog.__init__(self, parent)
|
||||||
|
self.setupUi(self)
|
||||||
|
|
||||||
|
self.buttonBox.accepted.connect(self.advanced_search_button_pushed)
|
||||||
|
self.tab_2_button_box.accepted.connect(self.accept)
|
||||||
|
self.tab_2_button_box.rejected.connect(self.reject)
|
||||||
|
self.clear_button.clicked.connect(self.clear_button_pushed)
|
||||||
|
self.adv_search_used = False
|
||||||
|
self.mc = ''
|
||||||
|
|
||||||
|
self.tabWidget.setCurrentIndex(0)
|
||||||
|
self.tabWidget.currentChanged[int].connect(self.tab_changed)
|
||||||
|
self.tab_changed(0)
|
||||||
|
|
||||||
|
def tab_changed(self, idx):
|
||||||
|
if idx == 1:
|
||||||
|
self.tab_2_button_box.button(QDialogButtonBox.Ok).setDefault(True)
|
||||||
|
else:
|
||||||
|
self.buttonBox.button(QDialogButtonBox.Ok).setDefault(True)
|
||||||
|
|
||||||
|
def advanced_search_button_pushed(self):
|
||||||
|
self.adv_search_used = True
|
||||||
|
self.accept()
|
||||||
|
|
||||||
|
def clear_button_pushed(self):
|
||||||
|
self.title_box.setText('')
|
||||||
|
self.author_box.setText('')
|
||||||
|
self.price_box.setText('')
|
||||||
|
self.format_box.setText('')
|
||||||
|
|
||||||
|
def tokens(self, raw):
|
||||||
|
phrases = re.findall(r'\s*".*?"\s*', raw)
|
||||||
|
for f in phrases:
|
||||||
|
raw = raw.replace(f, ' ')
|
||||||
|
phrases = [t.strip('" ') for t in phrases]
|
||||||
|
return ['"' + self.mc + t + '"' for t in phrases + [r.strip() for r in raw.split()]]
|
||||||
|
|
||||||
|
def search_string(self):
|
||||||
|
if self.adv_search_used:
|
||||||
|
return self.adv_search_string()
|
||||||
|
else:
|
||||||
|
return self.box_search_string()
|
||||||
|
|
||||||
|
def adv_search_string(self):
|
||||||
|
mk = self.matchkind.currentIndex()
|
||||||
|
if mk == CONTAINS_MATCH:
|
||||||
|
self.mc = ''
|
||||||
|
elif mk == EQUALS_MATCH:
|
||||||
|
self.mc = '='
|
||||||
|
else:
|
||||||
|
self.mc = '~'
|
||||||
|
all, any, phrase, none = map(lambda x: unicode(x.text()),
|
||||||
|
(self.all, self.any, self.phrase, self.none))
|
||||||
|
all, any, none = map(self.tokens, (all, any, none))
|
||||||
|
phrase = phrase.strip()
|
||||||
|
all = ' and '.join(all)
|
||||||
|
any = ' or '.join(any)
|
||||||
|
none = ' and not '.join(none)
|
||||||
|
ans = ''
|
||||||
|
if phrase:
|
||||||
|
ans += '"%s"'%phrase
|
||||||
|
if all:
|
||||||
|
ans += (' and ' if ans else '') + all
|
||||||
|
if none:
|
||||||
|
ans += (' and not ' if ans else 'not ') + none
|
||||||
|
if any:
|
||||||
|
ans += (' or ' if ans else '') + any
|
||||||
|
return ans
|
||||||
|
|
||||||
|
def token(self):
|
||||||
|
txt = unicode(self.text.text()).strip()
|
||||||
|
if txt:
|
||||||
|
if self.negate.isChecked():
|
||||||
|
txt = '!'+txt
|
||||||
|
tok = self.FIELDS[unicode(self.field.currentText())]+txt
|
||||||
|
if re.search(r'\s', tok):
|
||||||
|
tok = '"%s"'%tok
|
||||||
|
return tok
|
||||||
|
|
||||||
|
def box_search_string(self):
|
||||||
|
mk = self.matchkind.currentIndex()
|
||||||
|
if mk == CONTAINS_MATCH:
|
||||||
|
self.mc = ''
|
||||||
|
elif mk == EQUALS_MATCH:
|
||||||
|
self.mc = '='
|
||||||
|
else:
|
||||||
|
self.mc = '~'
|
||||||
|
|
||||||
|
ans = []
|
||||||
|
self.box_last_values = {}
|
||||||
|
title = unicode(self.title_box.text()).strip()
|
||||||
|
if title:
|
||||||
|
ans.append('title:"' + self.mc + title + '"')
|
||||||
|
author = unicode(self.author_box.text()).strip()
|
||||||
|
if author:
|
||||||
|
ans.append('author:"' + self.mc + author + '"')
|
||||||
|
price = unicode(self.price_box.text()).strip()
|
||||||
|
if price:
|
||||||
|
ans.append('price:"' + self.mc + price + '"')
|
||||||
|
format = unicode(self.format_box.text()).strip()
|
||||||
|
if author:
|
||||||
|
ans.append('format:"' + self.mc + format + '"')
|
||||||
|
if ans:
|
||||||
|
return ' and '.join(ans)
|
||||||
|
return ''
|
364
src/calibre/gui2/store/search/adv_search_builder.ui
Normal file
364
src/calibre/gui2/store/search/adv_search_builder.ui
Normal file
@ -0,0 +1,364 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<ui version="4.0">
|
||||||
|
<class>Dialog</class>
|
||||||
|
<widget class="QDialog" name="Dialog">
|
||||||
|
<property name="geometry">
|
||||||
|
<rect>
|
||||||
|
<x>0</x>
|
||||||
|
<y>0</y>
|
||||||
|
<width>752</width>
|
||||||
|
<height>472</height>
|
||||||
|
</rect>
|
||||||
|
</property>
|
||||||
|
<property name="windowTitle">
|
||||||
|
<string>Advanced Search</string>
|
||||||
|
</property>
|
||||||
|
<property name="windowIcon">
|
||||||
|
<iconset>
|
||||||
|
<normaloff>:/images/search.png</normaloff>:/images/search.png</iconset>
|
||||||
|
</property>
|
||||||
|
<layout class="QGridLayout" name="gridLayout_2">
|
||||||
|
<item row="0" column="0">
|
||||||
|
<widget class="QLabel" name="label_5">
|
||||||
|
<property name="text">
|
||||||
|
<string>&What kind of match to use:</string>
|
||||||
|
</property>
|
||||||
|
<property name="buddy">
|
||||||
|
<cstring>matchkind</cstring>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="0" column="1">
|
||||||
|
<widget class="QComboBox" name="matchkind">
|
||||||
|
<item>
|
||||||
|
<property name="text">
|
||||||
|
<string>Contains: the word or phrase matches anywhere in the metadata field</string>
|
||||||
|
</property>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<property name="text">
|
||||||
|
<string>Equals: the word or phrase must match the entire metadata field</string>
|
||||||
|
</property>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<property name="text">
|
||||||
|
<string>Regular expression: the expression must match anywhere in the metadata field</string>
|
||||||
|
</property>
|
||||||
|
</item>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="2" column="0" colspan="2">
|
||||||
|
<widget class="QTabWidget" name="tabWidget">
|
||||||
|
<property name="currentIndex">
|
||||||
|
<number>0</number>
|
||||||
|
</property>
|
||||||
|
<widget class="QWidget" name="tab">
|
||||||
|
<attribute name="title">
|
||||||
|
<string>A&dvanced Search</string>
|
||||||
|
</attribute>
|
||||||
|
<layout class="QGridLayout" name="gridLayout_3">
|
||||||
|
<item row="0" column="0">
|
||||||
|
<widget class="QGroupBox" name="groupBox">
|
||||||
|
<property name="title">
|
||||||
|
<string>Find entries that have...</string>
|
||||||
|
</property>
|
||||||
|
<layout class="QVBoxLayout" name="verticalLayout">
|
||||||
|
<item>
|
||||||
|
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="label">
|
||||||
|
<property name="text">
|
||||||
|
<string>&All these words:</string>
|
||||||
|
</property>
|
||||||
|
<property name="buddy">
|
||||||
|
<cstring>all</cstring>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QLineEdit" name="all"/>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<layout class="QHBoxLayout" name="horizontalLayout_2">
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="label_2">
|
||||||
|
<property name="text">
|
||||||
|
<string>This exact &phrase:</string>
|
||||||
|
</property>
|
||||||
|
<property name="buddy">
|
||||||
|
<cstring>all</cstring>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QLineEdit" name="phrase"/>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<layout class="QHBoxLayout" name="horizontalLayout_3">
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="label_3">
|
||||||
|
<property name="text">
|
||||||
|
<string>&One or more of these words:</string>
|
||||||
|
</property>
|
||||||
|
<property name="buddy">
|
||||||
|
<cstring>all</cstring>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QLineEdit" name="any"/>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="1" column="0">
|
||||||
|
<widget class="QGroupBox" name="groupBox_2">
|
||||||
|
<property name="title">
|
||||||
|
<string>But dont show entries that have...</string>
|
||||||
|
</property>
|
||||||
|
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||||
|
<item>
|
||||||
|
<layout class="QHBoxLayout" name="horizontalLayout_4">
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="label_4">
|
||||||
|
<property name="text">
|
||||||
|
<string>Any of these &unwanted words:</string>
|
||||||
|
</property>
|
||||||
|
<property name="buddy">
|
||||||
|
<cstring>all</cstring>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QLineEdit" name="none"/>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QLabel" name="label_6">
|
||||||
|
<property name="maximumSize">
|
||||||
|
<size>
|
||||||
|
<width>16777215</width>
|
||||||
|
<height>30</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>See the <a href="http://calibre-ebook.com/user_manual/gui.html#the-search-interface">User Manual</a> for more help</string>
|
||||||
|
</property>
|
||||||
|
<property name="openExternalLinks">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="2" column="0">
|
||||||
|
<spacer name="verticalSpacer_2">
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Vertical</enum>
|
||||||
|
</property>
|
||||||
|
<property name="sizeHint" stdset="0">
|
||||||
|
<size>
|
||||||
|
<width>20</width>
|
||||||
|
<height>40</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
</spacer>
|
||||||
|
</item>
|
||||||
|
<item row="3" column="0">
|
||||||
|
<widget class="QDialogButtonBox" name="buttonBox">
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Horizontal</enum>
|
||||||
|
</property>
|
||||||
|
<property name="standardButtons">
|
||||||
|
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
<widget class="QWidget" name="tab_2">
|
||||||
|
<attribute name="title">
|
||||||
|
<string>Titl&e/Author/Price ...</string>
|
||||||
|
</attribute>
|
||||||
|
<layout class="QGridLayout" name="gridLayout">
|
||||||
|
<item row="1" column="0">
|
||||||
|
<widget class="QLabel" name="label_7">
|
||||||
|
<property name="text">
|
||||||
|
<string>&Title:</string>
|
||||||
|
</property>
|
||||||
|
<property name="buddy">
|
||||||
|
<cstring>title_box</cstring>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="1" column="1">
|
||||||
|
<widget class="EnLineEdit" name="title_box">
|
||||||
|
<property name="toolTip">
|
||||||
|
<string>Enter the title.</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="2" column="0">
|
||||||
|
<widget class="QLabel" name="label_8">
|
||||||
|
<property name="text">
|
||||||
|
<string>&Author:</string>
|
||||||
|
</property>
|
||||||
|
<property name="buddy">
|
||||||
|
<cstring>author_box</cstring>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="3" column="0">
|
||||||
|
<widget class="QLabel" name="price_label">
|
||||||
|
<property name="text">
|
||||||
|
<string>&Price:</string>
|
||||||
|
</property>
|
||||||
|
<property name="buddy">
|
||||||
|
<cstring>price_box</cstring>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="6" column="0" colspan="2">
|
||||||
|
<layout class="QHBoxLayout" name="horizontalLayout_6">
|
||||||
|
<item>
|
||||||
|
<widget class="QPushButton" name="clear_button">
|
||||||
|
<property name="text">
|
||||||
|
<string>&Clear</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QDialogButtonBox" name="tab_2_button_box">
|
||||||
|
<property name="standardButtons">
|
||||||
|
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</item>
|
||||||
|
<item row="5" column="1">
|
||||||
|
<spacer name="verticalSpacer">
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Vertical</enum>
|
||||||
|
</property>
|
||||||
|
<property name="sizeHint" stdset="0">
|
||||||
|
<size>
|
||||||
|
<width>20</width>
|
||||||
|
<height>40</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
</spacer>
|
||||||
|
</item>
|
||||||
|
<item row="0" column="0" colspan="2">
|
||||||
|
<widget class="QLabel" name="label_11">
|
||||||
|
<property name="text">
|
||||||
|
<string>Search only in specific fields:</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="2" column="1">
|
||||||
|
<widget class="EnLineEdit" name="author_box"/>
|
||||||
|
</item>
|
||||||
|
<item row="4" column="1">
|
||||||
|
<widget class="QLineEdit" name="format_box"/>
|
||||||
|
</item>
|
||||||
|
<item row="4" column="0">
|
||||||
|
<widget class="QLabel" name="label_10">
|
||||||
|
<property name="text">
|
||||||
|
<string>&Format:</string>
|
||||||
|
</property>
|
||||||
|
<property name="buddy">
|
||||||
|
<cstring>format_box</cstring>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="3" column="1">
|
||||||
|
<widget class="EnLineEdit" name="price_box"/>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="1" column="1">
|
||||||
|
<spacer name="verticalSpacer_3">
|
||||||
|
<property name="orientation">
|
||||||
|
<enum>Qt::Vertical</enum>
|
||||||
|
</property>
|
||||||
|
<property name="sizeHint" stdset="0">
|
||||||
|
<size>
|
||||||
|
<width>20</width>
|
||||||
|
<height>40</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
</spacer>
|
||||||
|
</item>
|
||||||
|
</layout>
|
||||||
|
</widget>
|
||||||
|
<customwidgets>
|
||||||
|
<customwidget>
|
||||||
|
<class>EnLineEdit</class>
|
||||||
|
<extends>QLineEdit</extends>
|
||||||
|
<header>widgets.h</header>
|
||||||
|
</customwidget>
|
||||||
|
</customwidgets>
|
||||||
|
<tabstops>
|
||||||
|
<tabstop>all</tabstop>
|
||||||
|
<tabstop>phrase</tabstop>
|
||||||
|
<tabstop>any</tabstop>
|
||||||
|
<tabstop>none</tabstop>
|
||||||
|
<tabstop>buttonBox</tabstop>
|
||||||
|
<tabstop>title_box</tabstop>
|
||||||
|
<tabstop>author_box</tabstop>
|
||||||
|
<tabstop>price_box</tabstop>
|
||||||
|
<tabstop>format_box</tabstop>
|
||||||
|
<tabstop>clear_button</tabstop>
|
||||||
|
<tabstop>tab_2_button_box</tabstop>
|
||||||
|
<tabstop>tabWidget</tabstop>
|
||||||
|
<tabstop>matchkind</tabstop>
|
||||||
|
</tabstops>
|
||||||
|
<resources>
|
||||||
|
<include location="../../../../resources/images.qrc"/>
|
||||||
|
</resources>
|
||||||
|
<connections>
|
||||||
|
<connection>
|
||||||
|
<sender>buttonBox</sender>
|
||||||
|
<signal>accepted()</signal>
|
||||||
|
<receiver>Dialog</receiver>
|
||||||
|
<slot>accept()</slot>
|
||||||
|
<hints>
|
||||||
|
<hint type="sourcelabel">
|
||||||
|
<x>248</x>
|
||||||
|
<y>254</y>
|
||||||
|
</hint>
|
||||||
|
<hint type="destinationlabel">
|
||||||
|
<x>157</x>
|
||||||
|
<y>274</y>
|
||||||
|
</hint>
|
||||||
|
</hints>
|
||||||
|
</connection>
|
||||||
|
<connection>
|
||||||
|
<sender>buttonBox</sender>
|
||||||
|
<signal>rejected()</signal>
|
||||||
|
<receiver>Dialog</receiver>
|
||||||
|
<slot>reject()</slot>
|
||||||
|
<hints>
|
||||||
|
<hint type="sourcelabel">
|
||||||
|
<x>316</x>
|
||||||
|
<y>260</y>
|
||||||
|
</hint>
|
||||||
|
<hint type="destinationlabel">
|
||||||
|
<x>286</x>
|
||||||
|
<y>274</y>
|
||||||
|
</hint>
|
||||||
|
</hints>
|
||||||
|
</connection>
|
||||||
|
</connections>
|
||||||
|
</ui>
|
@ -6,7 +6,6 @@ __license__ = 'GPL 3'
|
|||||||
__copyright__ = '2011, John Schember <john@nachtimwald.com>'
|
__copyright__ = '2011, John Schember <john@nachtimwald.com>'
|
||||||
__docformat__ = 'restructuredtext en'
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
import time
|
|
||||||
import traceback
|
import traceback
|
||||||
from contextlib import closing
|
from contextlib import closing
|
||||||
from threading import Thread
|
from threading import Thread
|
||||||
@ -17,7 +16,9 @@ from calibre.utils.magick.draw import thumbnail
|
|||||||
|
|
||||||
class GenericDownloadThreadPool(object):
|
class GenericDownloadThreadPool(object):
|
||||||
'''
|
'''
|
||||||
add_task must be implemented in a subclass.
|
add_task must be implemented in a subclass and must
|
||||||
|
GenericDownloadThreadPool.add_task must be called
|
||||||
|
at the end of the function.
|
||||||
'''
|
'''
|
||||||
|
|
||||||
def __init__(self, thread_type, thread_count):
|
def __init__(self, thread_type, thread_count):
|
||||||
@ -29,10 +30,16 @@ class GenericDownloadThreadPool(object):
|
|||||||
self.threads = []
|
self.threads = []
|
||||||
|
|
||||||
def add_task(self):
|
def add_task(self):
|
||||||
raise NotImplementedError()
|
'''
|
||||||
|
This must be implemented in a sub class and this function
|
||||||
|
must be called at the end of the add_task function in
|
||||||
|
the sub class.
|
||||||
|
|
||||||
def start_threads(self):
|
The implementation of this function (in this base class)
|
||||||
for i in range(self.thread_count):
|
starts any threads necessary to fill the pool if it is
|
||||||
|
not already full.
|
||||||
|
'''
|
||||||
|
for i in xrange(self.thread_count - self.running_threads_count()):
|
||||||
t = self.thread_type(self.tasks, self.results)
|
t = self.thread_type(self.tasks, self.results)
|
||||||
self.threads.append(t)
|
self.threads.append(t)
|
||||||
t.start()
|
t.start()
|
||||||
@ -60,10 +67,14 @@ class GenericDownloadThreadPool(object):
|
|||||||
return not self.results.empty()
|
return not self.results.empty()
|
||||||
|
|
||||||
def threads_running(self):
|
def threads_running(self):
|
||||||
|
return self.running_threads_count() > 0
|
||||||
|
|
||||||
|
def running_threads_count(self):
|
||||||
|
count = 0
|
||||||
for t in self.threads:
|
for t in self.threads:
|
||||||
if t.is_alive():
|
if t.is_alive():
|
||||||
return True
|
count += 1
|
||||||
return False
|
return count
|
||||||
|
|
||||||
|
|
||||||
class SearchThreadPool(GenericDownloadThreadPool):
|
class SearchThreadPool(GenericDownloadThreadPool):
|
||||||
@ -73,17 +84,16 @@ class SearchThreadPool(GenericDownloadThreadPool):
|
|||||||
using start_threads(). Reset by calling abort().
|
using start_threads(). Reset by calling abort().
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
sp = SearchThreadPool(SearchThread, 3)
|
sp = SearchThreadPool(3)
|
||||||
add tasks using add_task(...)
|
sp.add_task(...)
|
||||||
sp.start_threads()
|
|
||||||
all threads have finished.
|
|
||||||
sp.abort()
|
|
||||||
add tasks using add_task(...)
|
|
||||||
sp.start_threads()
|
|
||||||
'''
|
'''
|
||||||
|
|
||||||
|
def __init__(self, thread_count):
|
||||||
|
GenericDownloadThreadPool.__init__(self, SearchThread, thread_count)
|
||||||
|
|
||||||
def add_task(self, query, store_name, store_plugin, timeout):
|
def add_task(self, query, store_name, store_plugin, timeout):
|
||||||
self.tasks.put((query, store_name, store_plugin, timeout))
|
self.tasks.put((query, store_name, store_plugin, timeout))
|
||||||
|
GenericDownloadThreadPool.add_task(self)
|
||||||
|
|
||||||
|
|
||||||
class SearchThread(Thread):
|
class SearchThread(Thread):
|
||||||
@ -113,12 +123,13 @@ class SearchThread(Thread):
|
|||||||
|
|
||||||
|
|
||||||
class CoverThreadPool(GenericDownloadThreadPool):
|
class CoverThreadPool(GenericDownloadThreadPool):
|
||||||
'''
|
|
||||||
Once started all threads run until abort is called.
|
def __init__(self, thread_count):
|
||||||
'''
|
GenericDownloadThreadPool.__init__(self, CoverThread, thread_count)
|
||||||
|
|
||||||
def add_task(self, search_result, update_callback, timeout=5):
|
def add_task(self, search_result, update_callback, timeout=5):
|
||||||
self.tasks.put((search_result, update_callback, timeout))
|
self.tasks.put((search_result, update_callback, timeout))
|
||||||
|
GenericDownloadThreadPool.add_task(self)
|
||||||
|
|
||||||
|
|
||||||
class CoverThread(Thread):
|
class CoverThread(Thread):
|
||||||
@ -136,12 +147,8 @@ class CoverThread(Thread):
|
|||||||
self._run = False
|
self._run = False
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
while self._run:
|
while self._run and not self.tasks.empty():
|
||||||
try:
|
try:
|
||||||
time.sleep(.1)
|
|
||||||
while not self.tasks.empty():
|
|
||||||
if not self._run:
|
|
||||||
break
|
|
||||||
result, callback, timeout = self.tasks.get()
|
result, callback, timeout = self.tasks.get()
|
||||||
if result and result.cover_url:
|
if result and result.cover_url:
|
||||||
with closing(self.br.open(result.cover_url, timeout=timeout)) as f:
|
with closing(self.br.open(result.cover_url, timeout=timeout)) as f:
|
||||||
@ -154,12 +161,13 @@ class CoverThread(Thread):
|
|||||||
|
|
||||||
|
|
||||||
class DetailsThreadPool(GenericDownloadThreadPool):
|
class DetailsThreadPool(GenericDownloadThreadPool):
|
||||||
'''
|
|
||||||
Once started all threads run until abort is called.
|
def __init__(self, thread_count):
|
||||||
'''
|
GenericDownloadThreadPool.__init__(self, DetailsThread, thread_count)
|
||||||
|
|
||||||
def add_task(self, search_result, store_plugin, update_callback, timeout=10):
|
def add_task(self, search_result, store_plugin, update_callback, timeout=10):
|
||||||
self.tasks.put((search_result, store_plugin, update_callback, timeout))
|
self.tasks.put((search_result, store_plugin, update_callback, timeout))
|
||||||
|
GenericDownloadThreadPool.add_task(self)
|
||||||
|
|
||||||
|
|
||||||
class DetailsThread(Thread):
|
class DetailsThread(Thread):
|
||||||
@ -175,12 +183,8 @@ class DetailsThread(Thread):
|
|||||||
self._run = False
|
self._run = False
|
||||||
|
|
||||||
def run(self):
|
def run(self):
|
||||||
while self._run:
|
while self._run and not self.tasks.empty():
|
||||||
try:
|
try:
|
||||||
time.sleep(.1)
|
|
||||||
while not self.tasks.empty():
|
|
||||||
if not self._run:
|
|
||||||
break
|
|
||||||
result, store_plugin, callback, timeout = self.tasks.get()
|
result, store_plugin, callback, timeout = self.tasks.get()
|
||||||
if result:
|
if result:
|
||||||
store_plugin.get_details(result, timeout)
|
store_plugin.get_details(result, timeout)
|
||||||
@ -188,3 +192,33 @@ class DetailsThread(Thread):
|
|||||||
self.tasks.task_done()
|
self.tasks.task_done()
|
||||||
except:
|
except:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
|
||||||
|
class CacheUpdateThreadPool(GenericDownloadThreadPool):
|
||||||
|
|
||||||
|
def __init__(self, thread_count):
|
||||||
|
GenericDownloadThreadPool.__init__(self, CacheUpdateThread, thread_count)
|
||||||
|
|
||||||
|
def add_task(self, store_plugin, timeout=10):
|
||||||
|
self.tasks.put((store_plugin, timeout))
|
||||||
|
GenericDownloadThreadPool.add_task(self)
|
||||||
|
|
||||||
|
|
||||||
|
class CacheUpdateThread(Thread):
|
||||||
|
|
||||||
|
def __init__(self, tasks, results):
|
||||||
|
Thread.__init__(self)
|
||||||
|
self.daemon = True
|
||||||
|
self.tasks = tasks
|
||||||
|
self._run = True
|
||||||
|
|
||||||
|
def abort(self):
|
||||||
|
self._run = False
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
while self._run and not self.tasks.empty():
|
||||||
|
try:
|
||||||
|
store_plugin, timeout = self.tasks.get()
|
||||||
|
store_plugin.update_cache(timeout=timeout, suppress_progress=True)
|
||||||
|
except:
|
||||||
|
traceback.print_exc()
|
||||||
|
@ -14,7 +14,7 @@ from PyQt4.Qt import (Qt, QAbstractItemModel, QVariant, QPixmap, QModelIndex, QS
|
|||||||
from calibre.gui2 import NONE
|
from calibre.gui2 import NONE
|
||||||
from calibre.gui2.store.search_result import SearchResult
|
from calibre.gui2.store.search_result import SearchResult
|
||||||
from calibre.gui2.store.search.download_thread import DetailsThreadPool, \
|
from calibre.gui2.store.search.download_thread import DetailsThreadPool, \
|
||||||
DetailsThread, CoverThreadPool, CoverThread
|
CoverThreadPool
|
||||||
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.icu import sort_key
|
from calibre.utils.icu import sort_key
|
||||||
@ -51,10 +51,8 @@ class Matches(QAbstractItemModel):
|
|||||||
self.matches = []
|
self.matches = []
|
||||||
self.query = ''
|
self.query = ''
|
||||||
self.search_filter = SearchFilter()
|
self.search_filter = SearchFilter()
|
||||||
self.cover_pool = CoverThreadPool(CoverThread, 2)
|
self.cover_pool = CoverThreadPool(2)
|
||||||
self.cover_pool.start_threads()
|
self.details_pool = DetailsThreadPool(4)
|
||||||
self.details_pool = DetailsThreadPool(DetailsThread, 4)
|
|
||||||
self.details_pool.start_threads()
|
|
||||||
|
|
||||||
self.sort_col = 2
|
self.sort_col = 2
|
||||||
self.sort_order = Qt.AscendingOrder
|
self.sort_order = Qt.AscendingOrder
|
||||||
@ -70,9 +68,7 @@ class Matches(QAbstractItemModel):
|
|||||||
self.search_filter.clear_search_results()
|
self.search_filter.clear_search_results()
|
||||||
self.query = ''
|
self.query = ''
|
||||||
self.cover_pool.abort()
|
self.cover_pool.abort()
|
||||||
self.cover_pool.start_threads()
|
|
||||||
self.details_pool.abort()
|
self.details_pool.abort()
|
||||||
self.details_pool.start_threads()
|
|
||||||
self.reset()
|
self.reset()
|
||||||
|
|
||||||
def add_result(self, result, store_plugin):
|
def add_result(self, result, store_plugin):
|
||||||
|
@ -9,17 +9,17 @@ __docformat__ = 'restructuredtext en'
|
|||||||
import re
|
import re
|
||||||
from random import shuffle
|
from random import shuffle
|
||||||
|
|
||||||
from PyQt4.Qt import (Qt, QDialog, QTimer, QCheckBox, QVBoxLayout)
|
from PyQt4.Qt import (Qt, QDialog, QTimer, QCheckBox, QVBoxLayout, QIcon)
|
||||||
|
|
||||||
from calibre.gui2 import JSONConfig, info_dialog
|
from calibre.gui2 import JSONConfig, info_dialog
|
||||||
from calibre.gui2.progress_indicator import ProgressIndicator
|
from calibre.gui2.progress_indicator import ProgressIndicator
|
||||||
from calibre.gui2.store.search.download_thread import SearchThreadPool, SearchThread
|
from calibre.gui2.store.search.adv_search_builder import AdvSearchBuilderDialog
|
||||||
|
from calibre.gui2.store.search.download_thread import SearchThreadPool, \
|
||||||
|
CacheUpdateThreadPool
|
||||||
from calibre.gui2.store.search.search_ui import Ui_Dialog
|
from calibre.gui2.store.search.search_ui import Ui_Dialog
|
||||||
|
|
||||||
HANG_TIME = 75000 # milliseconds seconds
|
HANG_TIME = 75000 # milliseconds seconds
|
||||||
TIMEOUT = 75 # seconds
|
TIMEOUT = 75 # seconds
|
||||||
SEARCH_THREAD_TOTAL = 4
|
|
||||||
COVER_DOWNLOAD_THREAD_TOTAL = 2
|
|
||||||
|
|
||||||
class SearchDialog(QDialog, Ui_Dialog):
|
class SearchDialog(QDialog, Ui_Dialog):
|
||||||
|
|
||||||
@ -31,11 +31,17 @@ class SearchDialog(QDialog, Ui_Dialog):
|
|||||||
|
|
||||||
# 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
|
||||||
self.search_pool = SearchThreadPool(SearchThread, SEARCH_THREAD_TOTAL)
|
self.search_pool = SearchThreadPool(4)
|
||||||
|
self.cache_pool = CacheUpdateThreadPool(2)
|
||||||
# Check for results and hung threads.
|
# Check for results and hung threads.
|
||||||
self.checker = QTimer()
|
self.checker = QTimer()
|
||||||
|
self.progress_checker = QTimer()
|
||||||
self.hang_check = 0
|
self.hang_check = 0
|
||||||
|
|
||||||
|
# Update store caches silently.
|
||||||
|
for p in self.store_plugins.values():
|
||||||
|
self.cache_pool.add_task(p, 30)
|
||||||
|
|
||||||
# Add check boxes for each store so the user
|
# Add check boxes for each store so the user
|
||||||
# can disable searching specific stores on a
|
# can disable searching specific stores on a
|
||||||
# per search basis.
|
# per search basis.
|
||||||
@ -52,16 +58,27 @@ class SearchDialog(QDialog, Ui_Dialog):
|
|||||||
self.pi = ProgressIndicator(self, 24)
|
self.pi = ProgressIndicator(self, 24)
|
||||||
self.top_layout.addWidget(self.pi)
|
self.top_layout.addWidget(self.pi)
|
||||||
|
|
||||||
|
self.adv_search_button.setIcon(QIcon(I('search.png')))
|
||||||
|
|
||||||
|
self.adv_search_button.clicked.connect(self.build_adv_search)
|
||||||
self.search.clicked.connect(self.do_search)
|
self.search.clicked.connect(self.do_search)
|
||||||
self.checker.timeout.connect(self.get_results)
|
self.checker.timeout.connect(self.get_results)
|
||||||
|
self.progress_checker.timeout.connect(self.check_progress)
|
||||||
self.results_view.activated.connect(self.open_store)
|
self.results_view.activated.connect(self.open_store)
|
||||||
self.select_all_stores.clicked.connect(self.stores_select_all)
|
self.select_all_stores.clicked.connect(self.stores_select_all)
|
||||||
self.select_invert_stores.clicked.connect(self.stores_select_invert)
|
self.select_invert_stores.clicked.connect(self.stores_select_invert)
|
||||||
self.select_none_stores.clicked.connect(self.stores_select_none)
|
self.select_none_stores.clicked.connect(self.stores_select_none)
|
||||||
self.finished.connect(self.dialog_closed)
|
self.finished.connect(self.dialog_closed)
|
||||||
|
|
||||||
|
self.progress_checker.start(100)
|
||||||
|
|
||||||
self.restore_state()
|
self.restore_state()
|
||||||
|
|
||||||
|
def build_adv_search(self):
|
||||||
|
adv = AdvSearchBuilderDialog(self)
|
||||||
|
if adv.exec_() == QDialog.Accepted:
|
||||||
|
self.search_edit.setText(adv.search_string())
|
||||||
|
|
||||||
def resize_columns(self):
|
def resize_columns(self):
|
||||||
total = 600
|
total = 600
|
||||||
# Cover
|
# Cover
|
||||||
@ -105,10 +122,8 @@ class SearchDialog(QDialog, Ui_Dialog):
|
|||||||
for n in store_names:
|
for n in store_names:
|
||||||
if getattr(self, 'store_check_' + n).isChecked():
|
if getattr(self, 'store_check_' + n).isChecked():
|
||||||
self.search_pool.add_task(query, n, self.store_plugins[n], TIMEOUT)
|
self.search_pool.add_task(query, n, self.store_plugins[n], TIMEOUT)
|
||||||
if self.search_pool.has_tasks():
|
|
||||||
self.hang_check = 0
|
self.hang_check = 0
|
||||||
self.checker.start(100)
|
self.checker.start(100)
|
||||||
self.search_pool.start_threads()
|
|
||||||
self.pi.startAnimation()
|
self.pi.startAnimation()
|
||||||
|
|
||||||
def clean_query(self, query):
|
def clean_query(self, query):
|
||||||
@ -181,20 +196,17 @@ class SearchDialog(QDialog, Ui_Dialog):
|
|||||||
if self.hang_check >= HANG_TIME:
|
if self.hang_check >= HANG_TIME:
|
||||||
self.search_pool.abort()
|
self.search_pool.abort()
|
||||||
self.checker.stop()
|
self.checker.stop()
|
||||||
self.pi.stopAnimation()
|
|
||||||
else:
|
else:
|
||||||
# Stop the checker if not threads are running.
|
# Stop the checker if not threads are running.
|
||||||
if not self.search_pool.threads_running() and not self.search_pool.has_tasks():
|
if not self.search_pool.threads_running() and not self.search_pool.has_tasks():
|
||||||
self.checker.stop()
|
self.checker.stop()
|
||||||
self.pi.stopAnimation()
|
|
||||||
|
|
||||||
while self.search_pool.has_results():
|
while self.search_pool.has_results():
|
||||||
res, store_plugin = self.search_pool.get_result()
|
res, store_plugin = self.search_pool.get_result()
|
||||||
if res:
|
if res:
|
||||||
self.results_view.model().add_result(res, store_plugin)
|
self.results_view.model().add_result(res, store_plugin)
|
||||||
|
|
||||||
if not self.checker.isActive():
|
if not self.search_pool.threads_running() and not self.results_view.model().has_results():
|
||||||
if not self.results_view.model().has_results():
|
|
||||||
info_dialog(self, _('No matches'), _('Couldn\'t find any books matching your query.'), show=True, show_copy_button=False)
|
info_dialog(self, _('No matches'), _('Couldn\'t find any books matching your query.'), show=True, show_copy_button=False)
|
||||||
|
|
||||||
|
|
||||||
@ -202,6 +214,13 @@ class SearchDialog(QDialog, Ui_Dialog):
|
|||||||
result = self.results_view.model().get_result(index)
|
result = self.results_view.model().get_result(index)
|
||||||
self.store_plugins[result.store_name].open(self, result.detail_item)
|
self.store_plugins[result.store_name].open(self, result.detail_item)
|
||||||
|
|
||||||
|
def check_progress(self):
|
||||||
|
if not self.search_pool.threads_running() and not self.results_view.model().cover_pool.threads_running() and not self.results_view.model().details_pool.threads_running():
|
||||||
|
self.pi.stopAnimation()
|
||||||
|
else:
|
||||||
|
if not self.pi.isAnimated():
|
||||||
|
self.pi.startAnimation()
|
||||||
|
|
||||||
def get_store_checks(self):
|
def get_store_checks(self):
|
||||||
'''
|
'''
|
||||||
Returns a list of QCheckBox's for each store.
|
Returns a list of QCheckBox's for each store.
|
||||||
@ -228,5 +247,6 @@ class SearchDialog(QDialog, Ui_Dialog):
|
|||||||
def dialog_closed(self, result):
|
def dialog_closed(self, result):
|
||||||
self.results_view.model().closing()
|
self.results_view.model().closing()
|
||||||
self.search_pool.abort()
|
self.search_pool.abort()
|
||||||
|
self.cache_pool.abort()
|
||||||
self.save_state()
|
self.save_state()
|
||||||
|
|
||||||
|
@ -30,6 +30,13 @@
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
<item>
|
||||||
|
<widget class="QToolButton" name="adv_search_button">
|
||||||
|
<property name="text">
|
||||||
|
<string>...</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<widget class="QLineEdit" name="search_edit"/>
|
<widget class="QLineEdit" name="search_edit"/>
|
||||||
</item>
|
</item>
|
||||||
|
@ -534,6 +534,7 @@ class DocumentView(QWebView): # {{{
|
|||||||
_('&Lookup in dictionary'), self)
|
_('&Lookup in dictionary'), self)
|
||||||
self.dictionary_action.setShortcut(Qt.CTRL+Qt.Key_L)
|
self.dictionary_action.setShortcut(Qt.CTRL+Qt.Key_L)
|
||||||
self.dictionary_action.triggered.connect(self.lookup)
|
self.dictionary_action.triggered.connect(self.lookup)
|
||||||
|
self.addAction(self.dictionary_action)
|
||||||
self.goto_location_action = QAction(_('Go to...'), self)
|
self.goto_location_action = QAction(_('Go to...'), self)
|
||||||
self.goto_location_menu = m = QMenu(self)
|
self.goto_location_menu = m = QMenu(self)
|
||||||
self.goto_location_actions = a = {
|
self.goto_location_actions = a = {
|
||||||
|
@ -33,24 +33,21 @@
|
|||||||
<enum>QFrame::Raised</enum>
|
<enum>QFrame::Raised</enum>
|
||||||
</property>
|
</property>
|
||||||
<layout class="QGridLayout" name="gridLayout">
|
<layout class="QGridLayout" name="gridLayout">
|
||||||
<item row="0" column="0">
|
<item row="1" column="1">
|
||||||
<widget class="QWebView" name="view"/>
|
|
||||||
</item>
|
|
||||||
<item row="0" column="1">
|
|
||||||
<widget class="QScrollBar" name="vertical_scrollbar">
|
<widget class="QScrollBar" name="vertical_scrollbar">
|
||||||
<property name="orientation">
|
<property name="orientation">
|
||||||
<enum>Qt::Vertical</enum>
|
<enum>Qt::Vertical</enum>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="1" column="0">
|
<item row="2" column="0">
|
||||||
<widget class="QScrollBar" name="horizontal_scrollbar">
|
<widget class="QScrollBar" name="horizontal_scrollbar">
|
||||||
<property name="orientation">
|
<property name="orientation">
|
||||||
<enum>Qt::Horizontal</enum>
|
<enum>Qt::Horizontal</enum>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="2" column="0" colspan="2">
|
<item row="3" column="0" colspan="2">
|
||||||
<widget class="QFrame" name="dictionary_box">
|
<widget class="QFrame" name="dictionary_box">
|
||||||
<property name="frameShape">
|
<property name="frameShape">
|
||||||
<enum>QFrame::StyledPanel</enum>
|
<enum>QFrame::StyledPanel</enum>
|
||||||
@ -91,6 +88,9 @@
|
|||||||
</layout>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
<item row="1" column="0">
|
||||||
|
<widget class="DocumentView" name="view" native="true"/>
|
||||||
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
</widget>
|
</widget>
|
||||||
@ -108,7 +108,7 @@
|
|||||||
</size>
|
</size>
|
||||||
</property>
|
</property>
|
||||||
<attribute name="toolBarArea">
|
<attribute name="toolBarArea">
|
||||||
<enum>Qt::LeftToolBarArea</enum>
|
<enum>LeftToolBarArea</enum>
|
||||||
</attribute>
|
</attribute>
|
||||||
<attribute name="toolBarBreak">
|
<attribute name="toolBarBreak">
|
||||||
<bool>false</bool>
|
<bool>false</bool>
|
||||||
@ -121,7 +121,7 @@
|
|||||||
<addaction name="action_font_size_larger"/>
|
<addaction name="action_font_size_larger"/>
|
||||||
<addaction name="action_font_size_smaller"/>
|
<addaction name="action_font_size_smaller"/>
|
||||||
<addaction name="action_table_of_contents"/>
|
<addaction name="action_table_of_contents"/>
|
||||||
<addaction name="action_metadata"/>
|
<addaction name="action_full_screen"/>
|
||||||
<addaction name="separator"/>
|
<addaction name="separator"/>
|
||||||
<addaction name="action_previous_page"/>
|
<addaction name="action_previous_page"/>
|
||||||
<addaction name="action_next_page"/>
|
<addaction name="action_next_page"/>
|
||||||
@ -130,13 +130,13 @@
|
|||||||
<addaction name="action_reference_mode"/>
|
<addaction name="action_reference_mode"/>
|
||||||
<addaction name="separator"/>
|
<addaction name="separator"/>
|
||||||
<addaction name="action_preferences"/>
|
<addaction name="action_preferences"/>
|
||||||
<addaction name="action_full_screen"/>
|
<addaction name="action_metadata"/>
|
||||||
<addaction name="separator"/>
|
<addaction name="separator"/>
|
||||||
<addaction name="action_print"/>
|
<addaction name="action_print"/>
|
||||||
</widget>
|
</widget>
|
||||||
<widget class="QToolBar" name="tool_bar2">
|
<widget class="QToolBar" name="tool_bar2">
|
||||||
<attribute name="toolBarArea">
|
<attribute name="toolBarArea">
|
||||||
<enum>Qt::TopToolBarArea</enum>
|
<enum>TopToolBarArea</enum>
|
||||||
</attribute>
|
</attribute>
|
||||||
<attribute name="toolBarBreak">
|
<attribute name="toolBarBreak">
|
||||||
<bool>false</bool>
|
<bool>false</bool>
|
||||||
@ -316,6 +316,12 @@
|
|||||||
<extends>QWidget</extends>
|
<extends>QWidget</extends>
|
||||||
<header>QtWebKit/QWebView</header>
|
<header>QtWebKit/QWebView</header>
|
||||||
</customwidget>
|
</customwidget>
|
||||||
|
<customwidget>
|
||||||
|
<class>DocumentView</class>
|
||||||
|
<extends>QWidget</extends>
|
||||||
|
<header>calibre/gui2/viewer/documentview.h</header>
|
||||||
|
<container>1</container>
|
||||||
|
</customwidget>
|
||||||
</customwidgets>
|
</customwidgets>
|
||||||
<resources>
|
<resources>
|
||||||
<include location="../../../../resources/images.qrc"/>
|
<include location="../../../../resources/images.qrc"/>
|
||||||
|
@ -406,11 +406,9 @@ class ResultCache(SearchQueryParser): # {{{
|
|||||||
if val_func is None:
|
if val_func is None:
|
||||||
loc = self.field_metadata[location]['rec_index']
|
loc = self.field_metadata[location]['rec_index']
|
||||||
val_func = lambda item, loc=loc: item[loc]
|
val_func = lambda item, loc=loc: item[loc]
|
||||||
dt = self.field_metadata[location]['datatype']
|
|
||||||
|
|
||||||
q = ''
|
q = ''
|
||||||
val_func = lambda item, loc=loc: item[loc]
|
|
||||||
cast = adjust = lambda x: x
|
cast = adjust = lambda x: x
|
||||||
|
dt = self.field_metadata[location]['datatype']
|
||||||
|
|
||||||
if query == 'false':
|
if query == 'false':
|
||||||
if dt == 'rating' or location == 'cover':
|
if dt == 'rating' or location == 'cover':
|
||||||
@ -556,10 +554,14 @@ class ResultCache(SearchQueryParser): # {{{
|
|||||||
return matchkind, query
|
return matchkind, query
|
||||||
|
|
||||||
def get_bool_matches(self, location, query, candidates):
|
def get_bool_matches(self, location, query, candidates):
|
||||||
bools_are_tristate = not self.db_prefs.get('bools_are_tristate')
|
bools_are_tristate = self.db_prefs.get('bools_are_tristate')
|
||||||
loc = self.field_metadata[location]['rec_index']
|
loc = self.field_metadata[location]['rec_index']
|
||||||
matches = set()
|
matches = set()
|
||||||
query = icu_lower(query)
|
query = icu_lower(query)
|
||||||
|
if query not in (_('no'), _('unchecked'), '_no', 'false',
|
||||||
|
_('yes'), _('checked'), '_yes', 'true',
|
||||||
|
_('empty'), _('blank'), '_empty'):
|
||||||
|
raise ParseException(_('Invalid boolean query "{0}"').format(query))
|
||||||
for id_ in candidates:
|
for id_ in candidates:
|
||||||
item = self._data[id_]
|
item = self._data[id_]
|
||||||
if item is None:
|
if item is None:
|
||||||
@ -630,8 +632,11 @@ class ResultCache(SearchQueryParser): # {{{
|
|||||||
terms.add(l)
|
terms.add(l)
|
||||||
if terms:
|
if terms:
|
||||||
for l in terms:
|
for l in terms:
|
||||||
|
try:
|
||||||
matches |= self.get_matches(l, query,
|
matches |= self.get_matches(l, query,
|
||||||
candidates=candidates, allow_recursion=allow_recursion)
|
candidates=candidates, allow_recursion=allow_recursion)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
return matches
|
return matches
|
||||||
|
|
||||||
if location in self.field_metadata:
|
if location in self.field_metadata:
|
||||||
@ -1005,9 +1010,9 @@ class SortKeyGenerator(object):
|
|||||||
if sb == 'date':
|
if sb == 'date':
|
||||||
try:
|
try:
|
||||||
val = parse_date(val)
|
val = parse_date(val)
|
||||||
dt = 'datetime'
|
|
||||||
except:
|
except:
|
||||||
pass
|
val = UNDEFINED_DATE
|
||||||
|
dt = 'datetime'
|
||||||
elif sb == 'number':
|
elif sb == 'number':
|
||||||
try:
|
try:
|
||||||
val = float(val)
|
val = float(val)
|
||||||
|
@ -1,9 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
|
|
||||||
|
|
||||||
__license__ = 'GPL v3'
|
|
||||||
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
|
||||||
__docformat__ = 'restructuredtext en'
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1,37 +0,0 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
|
|
||||||
|
|
||||||
__license__ = 'GPL v3'
|
|
||||||
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
|
||||||
__docformat__ = 'restructuredtext en'
|
|
||||||
|
|
||||||
|
|
||||||
''' Design documentation {{{
|
|
||||||
|
|
||||||
Storage paradigm {{{
|
|
||||||
* Agnostic to storage paradigm (i.e. no book per folder assumptions)
|
|
||||||
* Two separate concepts: A store and collection
|
|
||||||
A store is a backend, like a sqlite database associated with a path on
|
|
||||||
the local filesystem, or a cloud based storage solution.
|
|
||||||
A collection is a user defined group of stores. Most of the logic for
|
|
||||||
data manipulation sorting/searching/restrictions should be in the collection
|
|
||||||
class. The collection class should transparently handle the
|
|
||||||
conversion from store name + id to row number in the collection.
|
|
||||||
* Not sure how feasible it is to allow many-many maps between stores
|
|
||||||
and collections.
|
|
||||||
}}}
|
|
||||||
|
|
||||||
Event system {{{
|
|
||||||
* Comprehensive event system that other components can subscribe to
|
|
||||||
* Subscribers should be able to temporarily block receiving events
|
|
||||||
* Should event dispatch be asynchronous?
|
|
||||||
* Track last modified time for metadata and each format
|
|
||||||
}}}
|
|
||||||
}}}'''
|
|
||||||
|
|
||||||
# Imports {{{
|
|
||||||
# }}}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -188,7 +188,7 @@ class FieldMetadata(dict):
|
|||||||
'datatype':'text',
|
'datatype':'text',
|
||||||
'is_multiple':None,
|
'is_multiple':None,
|
||||||
'kind':'field',
|
'kind':'field',
|
||||||
'name':None,
|
'name':_('Author Sort'),
|
||||||
'search_terms':['author_sort'],
|
'search_terms':['author_sort'],
|
||||||
'is_custom':False,
|
'is_custom':False,
|
||||||
'is_category':False,
|
'is_category':False,
|
||||||
@ -238,7 +238,7 @@ class FieldMetadata(dict):
|
|||||||
'datatype':'datetime',
|
'datatype':'datetime',
|
||||||
'is_multiple':None,
|
'is_multiple':None,
|
||||||
'kind':'field',
|
'kind':'field',
|
||||||
'name':_('Date'),
|
'name':_('Modified'),
|
||||||
'search_terms':['last_modified'],
|
'search_terms':['last_modified'],
|
||||||
'is_custom':False,
|
'is_custom':False,
|
||||||
'is_category':False,
|
'is_category':False,
|
||||||
@ -258,7 +258,7 @@ class FieldMetadata(dict):
|
|||||||
'datatype':'text',
|
'datatype':'text',
|
||||||
'is_multiple':None,
|
'is_multiple':None,
|
||||||
'kind':'field',
|
'kind':'field',
|
||||||
'name':None,
|
'name':_('Path'),
|
||||||
'search_terms':[],
|
'search_terms':[],
|
||||||
'is_custom':False,
|
'is_custom':False,
|
||||||
'is_category':False,
|
'is_category':False,
|
||||||
@ -308,7 +308,7 @@ class FieldMetadata(dict):
|
|||||||
'datatype':'float',
|
'datatype':'float',
|
||||||
'is_multiple':None,
|
'is_multiple':None,
|
||||||
'kind':'field',
|
'kind':'field',
|
||||||
'name':_('Size (MB)'),
|
'name':_('Size'),
|
||||||
'search_terms':['size'],
|
'search_terms':['size'],
|
||||||
'is_custom':False,
|
'is_custom':False,
|
||||||
'is_category':False,
|
'is_category':False,
|
||||||
@ -399,6 +399,13 @@ class FieldMetadata(dict):
|
|||||||
if self._tb_cats[k]['kind']=='field' and
|
if self._tb_cats[k]['kind']=='field' and
|
||||||
self._tb_cats[k]['datatype'] is not None]
|
self._tb_cats[k]['datatype'] is not None]
|
||||||
|
|
||||||
|
def displayable_field_keys(self):
|
||||||
|
return [k for k in self._tb_cats.keys()
|
||||||
|
if self._tb_cats[k]['kind']=='field' and
|
||||||
|
self._tb_cats[k]['datatype'] is not None and
|
||||||
|
k not in ('au_map', 'marked', 'ondevice', 'cover') and
|
||||||
|
not self.is_series_index(k)]
|
||||||
|
|
||||||
def standard_field_keys(self):
|
def standard_field_keys(self):
|
||||||
return [k for k in self._tb_cats.keys()
|
return [k for k in self._tb_cats.keys()
|
||||||
if self._tb_cats[k]['kind']=='field' and
|
if self._tb_cats[k]['kind']=='field' and
|
||||||
@ -442,6 +449,11 @@ class FieldMetadata(dict):
|
|||||||
def is_custom_field(self, key):
|
def is_custom_field(self, key):
|
||||||
return key.startswith(self.custom_field_prefix)
|
return key.startswith(self.custom_field_prefix)
|
||||||
|
|
||||||
|
def is_series_index(self, key):
|
||||||
|
m = self[key]
|
||||||
|
return (m['datatype'] == 'float' and key.endswith('_index') and
|
||||||
|
key[:-6] in self)
|
||||||
|
|
||||||
def key_to_label(self, key):
|
def key_to_label(self, key):
|
||||||
if 'label' not in self._tb_cats[key]:
|
if 'label' not in self._tb_cats[key]:
|
||||||
return key
|
return key
|
||||||
|
@ -149,7 +149,8 @@ class PostInstall:
|
|||||||
if islinux or isfreebsd:
|
if islinux or isfreebsd:
|
||||||
for f in os.listdir('.'):
|
for f in os.listdir('.'):
|
||||||
if os.stat(f).st_uid == 0:
|
if os.stat(f).st_uid == 0:
|
||||||
os.rmdir(f) if os.path.isdir(f) else os.unlink(f)
|
import shutil
|
||||||
|
shutil.rmtree(f) if os.path.isdir(f) else os.unlink(f)
|
||||||
if os.stat(config_dir).st_uid == 0:
|
if os.stat(config_dir).st_uid == 0:
|
||||||
os.rmdir(config_dir)
|
os.rmdir(config_dir)
|
||||||
|
|
||||||
|
@ -20,11 +20,14 @@ What formats does |app| support conversion to/from?
|
|||||||
|app| supports the conversion of many input formats to many output formats.
|
|app| supports the conversion of many input formats to many output formats.
|
||||||
It can convert every input format in the following list, to every output format.
|
It can convert every input format in the following list, to every output format.
|
||||||
|
|
||||||
*Input Formats:* CBZ, CBR, CBC, CHM, EPUB, FB2, HTML, HTMLZ, LIT, LRF, MOBI, ODT, PDF, PRC**, PDB, PML, RB, RTF, SNB, TCR, TXT, TXTZ
|
*Input Formats:* CBZ, CBR, CBC, CHM, EPUB, FB2, HTML, HTMLZ, LIT, LRF, MOBI, ODT, PDF, PRC, PDB, PML, RB, RTF, SNB, TCR, TXT, TXTZ
|
||||||
|
|
||||||
*Output Formats:* EPUB, FB2, OEB, LIT, LRF, MOBI, HTMLZ, PDB, PML, RB, PDF, SNB, TCR, TXT, TXTZ
|
*Output Formats:* EPUB, FB2, OEB, LIT, LRF, MOBI, HTMLZ, PDB, PML, RB, PDF, SNB, TCR, TXT, TXTZ
|
||||||
|
|
||||||
** PRC is a generic format, |app| supports PRC files with TextRead and MOBIBook headers
|
.. note ::
|
||||||
|
|
||||||
|
PRC is a generic format, |app| supports PRC files with TextRead and MOBIBook headers.
|
||||||
|
PDB is also a generic format. |app| supports eReder, Plucker, PML and zTxt PDB files.
|
||||||
|
|
||||||
.. _best-source-formats:
|
.. _best-source-formats:
|
||||||
|
|
||||||
|
@ -365,6 +365,8 @@ Dates and numeric fields support the relational operators ``=`` (equals), ``>``
|
|||||||
Rating fields are considered to be numeric. For example, the search ``rating:>=3`` will find all books rated 3
|
Rating fields are considered to be numeric. For example, the search ``rating:>=3`` will find all books rated 3
|
||||||
or higher.
|
or higher.
|
||||||
|
|
||||||
|
You can search for the number of items in multiple-valued fields such as tags). These searches begin with the character ``#``, then use the same syntax as numeric fields. For example, to find all books with more than 4 tags, use ``tags:#>4``. To find all books with exactly 10 tags, use ``tags:#=10``.
|
||||||
|
|
||||||
Series indices are searchable. For the standard series, the search name is 'series_index'. For
|
Series indices are searchable. For the standard series, the search name is 'series_index'. For
|
||||||
custom series columns, use the column search name followed by _index. For example, to search the indices for a
|
custom series columns, use the column search name followed by _index. For example, to search the indices for a
|
||||||
custom series column named ``#my_series``, you would use the search name ``#my_series_index``.
|
custom series column named ``#my_series``, you would use the search name ``#my_series_index``.
|
||||||
|
@ -65,17 +65,14 @@ Catalog plugins
|
|||||||
Metadata download plugins
|
Metadata download plugins
|
||||||
--------------------------
|
--------------------------
|
||||||
|
|
||||||
.. module:: calibre.ebooks.metadata.fetch
|
.. module:: calibre.ebooks.metadata.sources.base
|
||||||
|
|
||||||
.. autoclass:: MetadataSource
|
.. autoclass:: Source
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
:members:
|
:members:
|
||||||
:member-order: bysource
|
:member-order: bysource
|
||||||
|
|
||||||
.. autoclass:: calibre.ebooks.metadata.covers.CoverDownload
|
.. autoclass:: InternalMetadataCompareKeyGen
|
||||||
:show-inheritance:
|
|
||||||
:members:
|
|
||||||
:member-order: bysource
|
|
||||||
|
|
||||||
Conversion plugins
|
Conversion plugins
|
||||||
--------------------
|
--------------------
|
||||||
|
Loading…
x
Reference in New Issue
Block a user