Merge from trunk

This commit is contained in:
Charles Haley 2013-02-07 22:02:16 +01:00
commit 047fb49c2d
116 changed files with 44629 additions and 32114 deletions

366
imgsrc/polish.svg Normal file
View File

@ -0,0 +1,366 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://web.resource.org/cc/"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="48"
height="48"
id="svg2"
sodipodi:version="0.32"
inkscape:version="0.45"
version="1.0"
sodipodi:docname="edit-clear.svg"
inkscape:output_extension="org.inkscape.output.svg.inkscape"
sodipodi:docbase="/home/dobey/Projects/gnome-icon-theme/scalable/actions">
<defs
id="defs4">
<linearGradient
inkscape:collect="always"
id="linearGradient6019">
<stop
style="stop-color:#000000;stop-opacity:1;"
offset="0"
id="stop6021" />
<stop
style="stop-color:#000000;stop-opacity:0;"
offset="1"
id="stop6023" />
</linearGradient>
<linearGradient
inkscape:collect="always"
id="linearGradient5999">
<stop
style="stop-color:#c4a000;stop-opacity:1;"
offset="0"
id="stop6001" />
<stop
style="stop-color:#c4a000;stop-opacity:0;"
offset="1"
id="stop6003" />
</linearGradient>
<linearGradient
inkscape:collect="always"
id="linearGradient5987">
<stop
style="stop-color:#d7c20f;stop-opacity:1"
offset="0"
id="stop5989" />
<stop
style="stop-color:#b6970d;stop-opacity:1"
offset="1"
id="stop5991" />
</linearGradient>
<linearGradient
id="linearGradient5981"
inkscape:collect="always">
<stop
id="stop5983"
offset="0"
style="stop-color:#ffffff;stop-opacity:1;" />
<stop
id="stop5985"
offset="1"
style="stop-color:#ffffff;stop-opacity:0.69411765" />
</linearGradient>
<linearGradient
inkscape:collect="always"
id="linearGradient5996">
<stop
style="stop-color:#8f5902;stop-opacity:1;"
offset="0"
id="stop5998" />
<stop
style="stop-color:#73521e;stop-opacity:1"
offset="1"
id="stop6000" />
</linearGradient>
<linearGradient
inkscape:collect="always"
id="linearGradient5984">
<stop
style="stop-color:#ffffff;stop-opacity:1;"
offset="0"
id="stop5986" />
<stop
style="stop-color:#ffffff;stop-opacity:0.13438736"
offset="1"
id="stop5988" />
</linearGradient>
<linearGradient
inkscape:collect="always"
id="linearGradient5974">
<stop
style="stop-color:#ad7fa8;stop-opacity:1;"
offset="0"
id="stop5976" />
<stop
style="stop-color:#dac6d8;stop-opacity:1"
offset="1"
id="stop5978" />
</linearGradient>
<linearGradient
inkscape:collect="always"
id="linearGradient5966">
<stop
style="stop-color:#fdef72;stop-opacity:1"
offset="0"
id="stop5968" />
<stop
style="stop-color:#e2cb0b;stop-opacity:1"
offset="1"
id="stop5970" />
</linearGradient>
<linearGradient
inkscape:collect="always"
id="linearGradient5958">
<stop
style="stop-color:#c17d11;stop-opacity:1;"
offset="0"
id="stop5960" />
<stop
style="stop-color:#e9b96e;stop-opacity:1"
offset="1"
id="stop5962" />
</linearGradient>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient5958"
id="linearGradient5964"
x1="28"
y1="16"
x2="26"
y2="8"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(-2,0)" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient5966"
id="linearGradient5972"
x1="20.933708"
y1="25.060659"
x2="30.208115"
y2="30.742676"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(-2,0)" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient5974"
id="linearGradient5980"
x1="27.651777"
y1="23.145937"
x2="21.59099"
y2="20.618719"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0.9768193,0,0,1,-1.3746633,0)" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient5984"
id="linearGradient5994"
gradientUnits="userSpaceOnUse"
x1="28"
y1="8"
x2="33.447109"
y2="16.685888" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient5996"
id="linearGradient6002"
x1="30.324829"
y1="9.2407961"
x2="34"
y2="18"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(-2,0)" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient5981"
id="linearGradient5973"
gradientUnits="userSpaceOnUse"
x1="22.319767"
y1="41.955986"
x2="18.985712"
y2="37.029255"
gradientTransform="translate(-2,0)" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient5987"
id="linearGradient5993"
x1="17.032078"
y1="27.446827"
x2="29.494455"
y2="37.845814"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(-2,0)" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient5999"
id="linearGradient6005"
x1="27.354809"
y1="36.218422"
x2="23.489431"
y2="34.728424"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(-2,0)" />
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient6019"
id="radialGradient6025"
cx="38"
cy="69"
fx="28.603323"
fy="69"
r="20"
gradientTransform="matrix(1,0,0,0.45,0,37.95)"
gradientUnits="userSpaceOnUse" />
</defs>
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#f6aaaa"
borderopacity="1"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="1"
inkscape:cx="37.14966"
inkscape:cy="21.336383"
inkscape:document-units="px"
inkscape:current-layer="layer1"
width="48px"
height="48px"
inkscape:showpageshadow="false"
showgrid="false"
gridspacingx="0.5px"
gridspacingy="0.5px"
gridempspacing="2"
inkscape:grid-points="true"
inkscape:window-width="862"
inkscape:window-height="875"
inkscape:window-x="12"
inkscape:window-y="50"
inkscape:object-paths="true"
inkscape:object-nodes="true"
objecttolerance="6"
gridtolerance="6"
guidetolerance="6"
showborder="false">
<inkscape:grid
type="xygrid"
id="grid5333"
spacingx="0.5px"
spacingy="0.5px"
empspacing="2" />
</sodipodi:namedview>
<metadata
id="metadata7">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:creator>
<cc:Agent>
<dc:title>Ulisse Perusin</dc:title>
</cc:Agent>
</dc:creator>
<cc:license
rdf:resource="http://creativecommons.org/licenses/GPL/2.0/" />
<dc:source>uli.peru@gmail.com</dc:source>
<dc:title>edit-clear</dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Livello 1"
inkscape:groupmode="layer"
id="layer1">
<path
sodipodi:type="arc"
style="opacity:0.25;fill:url(#radialGradient6025);fill-opacity:1;stroke:none;stroke-width:0.99999994;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:20;stroke-dasharray:none;stroke-opacity:1"
id="path6017"
sodipodi:cx="38"
sodipodi:cy="69"
sodipodi:rx="20"
sodipodi:ry="9"
d="M 58,69 A 20,9 0 1 1 18,69 A 20,9 0 1 1 58,69 z"
transform="matrix(1,0,0,0.6666668,-13.999999,-5.0000087)" />
<path
style="fill:url(#linearGradient5964);fill-opacity:1;fill-rule:evenodd;stroke:url(#linearGradient6002);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="M 34.59375,2.46875 C 26.390533,2.5744003 25.19695,18.716276 22.84375,21.625 L 26.84375,23.0625 C 29.475623,18.689953 42.599746,4.1545034 35.40625,2.5 C 35.12676,2.4690309 34.85837,2.4653419 34.59375,2.46875 z M 33.5625,4.53125 C 33.756063,4.5125114 33.930486,4.5369694 34.09375,4.625 C 34.746806,4.9771226 34.817405,6.1198771 34.25,7.15625 C 33.682595,8.1926229 32.684304,8.7583725 32.03125,8.40625 C 31.378197,8.0541272 31.307595,6.9113729 31.875,5.875 C 32.300554,5.0977202 32.981812,4.5874659 33.5625,4.53125 z"
id="path5371" />
<path
sodipodi:type="inkscape:offset"
inkscape:radius="-1.0049498"
inkscape:original="M 36.59375 2.46875 C 28.390533 2.5744003 27.19695 18.716276 24.84375 21.625 L 28.84375 23.0625 C 31.475623 18.689953 44.599746 4.1545034 37.40625 2.5 C 37.12676 2.4690309 36.85837 2.4653419 36.59375 2.46875 z "
style="opacity:0.26666667;fill:none;fill-opacity:1;fill-rule:evenodd;stroke:url(#linearGradient5994);stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
id="path5992"
d="M 36.59375,3.46875 C 34.872132,3.4909229 33.585825,4.3246243 32.40625,5.75 C 31.226675,7.1753757 30.257916,9.1916354 29.46875,11.34375 C 28.679584,13.495865 28.04471,15.77802 27.46875,17.71875 C 27.068859,19.066206 26.698893,20.125198 26.25,21.0625 L 28.4375,21.84375 C 30.056094,19.348126 33.476298,15.252572 35.96875,11.21875 C 37.294589,9.0729934 38.25245,7.0407089 38.46875,5.65625 C 38.5769,4.9640206 38.513818,4.4833206 38.34375,4.1875 C 38.179059,3.9010309 37.880274,3.6629145 37.21875,3.5 C 37.019255,3.4812162 36.817917,3.4658629 36.59375,3.46875 z"
transform="translate(-2,0)" />
<path
style="fill:url(#linearGradient5972);fill-opacity:1;fill-rule:evenodd;stroke:url(#linearGradient5993);stroke-width:0.99999994;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:20;stroke-dasharray:none;stroke-opacity:1"
d="M 20.5,16.5 C 21.209506,18.503007 19.958612,20.237831 19.5,21.5 C 14.308433,23.045674 10.713199,31.203726 5.3674175,35.453585 C 6.0352055,36.150983 6.819644,36.897763 7.5,37.5 L 11.5625,33.96875 L 8.494944,38.493399 C 10.704181,40.284382 13,41.5 14.5,42 L 17.25,38.34375 L 15.5,42.5 C 16.951994,43.088882 20.485286,43.982025 22.5,44 L 24.50389,40.597503 L 23.990721,44.0625 C 24.820284,44.220859 26.428886,44.436716 27.5,44.46875 C 30.862186,38.96875 31.5,30 29.5,26 C 29,24 31,21.5 32.5,20.5 C 30,18.5 24.294411,16.196274 20.5,16.5 z"
id="path5367"
sodipodi:nodetypes="cccccccccccccccc" />
<path
style="opacity:0.26666667;fill:#c4a000;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="M 9,38.5 C 13.816495,33.489105 13.465023,31.296074 19.116117,26.972272 C 16.133675,31.800703 15.650278,34.31233 12,40.5 L 9,38.5 z"
id="path5975"
sodipodi:nodetypes="cccc" />
<path
style="opacity:0.41568627;fill:#c4a000;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="M 16.149808,42.202452 L 20.495835,32.362305 C 22.160348,29.378578 23.355507,26.392253 25.024808,24.014952 C 23.422854,29.432989 20.134118,36.136745 17.493558,42.639952 L 16.149808,42.202452 z"
id="path5979"
sodipodi:nodetypes="ccccc" />
<path
style="opacity:0.47843137;fill:none;fill-opacity:1;fill-rule:evenodd;stroke:url(#linearGradient5973);stroke-width:0.99999994px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="M 21.625,17.5 C 21.721738,19.415146 20.702057,21.029645 20.40625,21.84375 C 20.302147,22.128459 20.07092,22.348124 19.78125,22.4375 C 14.327852,24.672144 12.151447,31.011538 6.7866116,35.523667 C 6.988254,35.723521 7.2786424,35.940971 7.4811412,36.131898 L 16.5,28.5 L 9.923385,38.310313 C 11.193418,39.337926 12.645586,40.194857 14.150041,40.799478 L 21.144394,31.5 L 16.869501,41.911612 C 18.46507,42.437269 19.967804,42.738908 21.81451,43 L 26.43324,35.3125 L 25.0625,43.219317 L 26.9375,43.445312 C 28.370713,40.909818 29.069882,37.778782 29.46875,34.65625 C 29.892695,31.337404 29.463786,28.115072 28.625,26.4375 C 28.597837,26.377291 28.576895,26.314465 28.5625,26.25 C 28.215642,24.862569 28.731642,23.504373 29.4375,22.375 C 29.864393,21.691971 30.367872,21.084221 30.902459,20.573223 C 29.730977,19.790532 28.315762,19.113157 26.53125,18.46875 C 24.769173,17.832444 23.033252,17.518725 21.625,17.5 z"
id="path6014"
sodipodi:nodetypes="csccccccccccccsssscsc" />
<path
style="opacity:0.24705882;fill:url(#linearGradient6005);fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="M 18.96875,43 C 21.146316,37.248129 25.364666,32.931057 26.985663,27.064588 C 27.037206,30.726661 27.235383,37.268314 25.09375,43.78125 C 24.773984,43.783025 24.919823,43.670441 24.62387,43.662697 L 25.424662,37.93818 L 22.143176,43.492564 C 19.952368,43.33624 20.848565,43.525163 18.96875,43 z"
id="path5977"
sodipodi:nodetypes="ccccccc" />
<path
style="opacity:0.48235294;fill:#c4a000;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="M 21.003067,22.610447 C 19.751072,23.226826 18.940858,24.137725 18.019961,24.974835 C 19.246448,24.266192 20.398947,23.525841 22.019534,22.986097 L 21.003067,22.610447 z"
id="path5995"
sodipodi:nodetypes="cccc" />
<path
style="opacity:0.48235294;fill:#c4a000;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="M 23.008698,23.061049 L 21.992233,25.049787 L 24.972946,23.461537 L 23.008698,23.061049 z"
id="path5997"
sodipodi:nodetypes="cccc" />
<path
style="opacity:0.48235294;fill:#c4a000;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="M 22.939805,17.961399 L 22.044612,19.668421 L 23.610339,20.170505 L 22.939805,17.961399 z"
id="path6007"
sodipodi:nodetypes="cccc" />
<path
style="opacity:0.48235294;fill:#c4a000;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="M 29.923254,19.88537 L 27.463006,21.720817 L 29.028733,22.222901 L 29.923254,19.88537 z"
id="path6009"
sodipodi:nodetypes="cccc" />
<path
style="opacity:0.48235294;fill:#c4a000;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
d="M 26.012925,17.938566 L 24.499014,20.46318 L 26.064741,20.965264 L 26.012925,17.938566 z"
id="path6011"
sodipodi:nodetypes="cccc" />
<path
style="opacity:0.2;fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1"
d="M 18.967726,22.024699 C 18.400946,19.059215 28.300561,24.177602 30.004548,25.019068 C 29.998066,26.098136 30.004548,27.019068 29.027729,27.019068 C 26.460191,25.626088 22.492474,23.413925 18.967726,22.024699 z"
id="path6013"
sodipodi:nodetypes="cccc" />
<path
style="fill:url(#linearGradient5980);fill-opacity:1;fill-rule:evenodd;stroke:#5c3566;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:round;stroke-opacity:1"
d="M 18.650133,21.5 C 18.161723,20.5 18.650133,19.5 19.626953,19.5 C 23.618393,20.475417 26.951828,21.706232 30.371965,23.5 C 30.860375,24.5 30.371965,25.5 29.395146,25.5 C 25.861203,23.63558 22.528435,22.425187 18.650133,21.5 z"
id="path5373"
sodipodi:nodetypes="ccccc" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -671,7 +671,7 @@ There are five possible things I know of, that can cause this:
you dont notice it and so you think |app| has frozen. Disconnect your
second monitor and restart calibre.
* You are using a Wacom branded mouse. There is an incompatibility between
* You are using a Wacom branded USB mouse. There is an incompatibility between
Wacom mice and the graphics toolkit |app| uses. Try using a non-Wacom
mouse.

View File

@ -3,29 +3,34 @@ from calibre.web.feeds.news import BasicNewsRecipe
class ElMundoTodayRecipe(BasicNewsRecipe):
title = 'El Mundo Today'
__author__ = 'atordo'
description = u'La actualidad del mañana'
description = u'La actualidad del ma\u00f1ana'
category = 'Noticias, humor'
cover_url = 'http://www.elmundotoday.com/wp-content/themes/EarthlyTouch/images/logo.png'
oldest_article = 30
oldest_article = 15
max_articles_per_feed = 60
auto_cleanup = False
no_stylesheets = True
remove_javascript = True
language = 'es'
use_embedded_content = False
publication_type = 'blog'
preprocess_regexps = [
(re.compile(r'</title>.*<!--Begin Article Single-->', re.DOTALL),
lambda match: '</title><body>'),
#(re.compile(r'^\t{5}<a href.*Permanent Link to ">$'), lambda match: ''),
#(re.compile(r'\t{5}</a>$'), lambda match: ''),
(re.compile(r'<div class="social4i".*</body>', re.DOTALL),
lambda match: '</body>'),
lambda match: '</title></head><body>'),
(re.compile(r'<img alt="" src="http://www.elmundotoday.com/wp-content/themes/emt/images/otrassecciones-line.gif">'),
lambda match: ''),
(re.compile(r'<div style="clear: both;"></div>.*</body>', re.DOTALL),
lambda match: '</body>')
]
keep_only_tags = [
dict(name='div', attrs={'class':'post-wrapper'})
dict(name='div', attrs={'class':'post-wrapper '})
]
remove_tags = [
dict(name='div', attrs={'class':'social4i'}),
dict(name='span', attrs={'class':'num-comentarios'})
]
remove_attributes = [ 'href', 'title', 'alt' ]
@ -36,8 +41,3 @@ class ElMundoTodayRecipe(BasicNewsRecipe):
'''
feeds = [('El Mundo Today', 'http://www.elmundotoday.com/feed/')]
def get_broser(self):
br = BasicNewsRecipe.get_browser(self)
br.set_handle_gzip(True)
return br

View File

@ -14,26 +14,17 @@ class AdvancedUserRecipe1313693926(BasicNewsRecipe):
language = 'de'
encoding = 'UTF-8'
__author__ = 'Armin Geller' # 2011-08-19
__author__ = 'Armin Geller' # 2013-02-05 V3
oldest_article = 7
max_articles_per_feed = 50
remove_tags = [
dict(name='div', attrs={'id':["comments"]}),
dict(attrs={'class':['commentlink']}),
]
keep_only_tags = [
dict(name='div', attrs={'class':["grid_8 articleText"]}),
dict(name='div', attrs={'class':["articleTextInnerText"]}),
]
feeds = [
(u'Inhalt:', u'http://www.fluter.de/de/?tpl=907'),
]
extra_css = '.cs_img {margin-right: 10pt;}'
def print_version(self, url):
return url + '?tpl=1260'

View File

@ -0,0 +1,40 @@
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
import re
from calibre.web.feeds.news import BasicNewsRecipe
class ViceDERecipe(BasicNewsRecipe):
title = u'Vice Magazin Deutschland'
__author__ = 'atordo;alex'
description = u'Die offizielle Website des Vice Magazins Deutschland'
category = u'Nachrichten, Fotografie, Blogs, Mode, Kunst, Film, Musik, Literatur, Technik'
cover_url = 'http://www.seeklogo.com/images/V/Vice-logo-668578AC94-seeklogo.com.gif'
oldest_article = 14
max_articles_per_feed = 100
auto_cleanup = False
no_stylesheets = True
language = 'de'
use_embedded_content = False
remove_javascript = True
publication_type = 'magazine'
recursions=10
match_regexps = [r'/read/.*\?Contentpage=[2-9]$']
keep_only_tags = [
dict(attrs={'class':['article_title','article_content','next']})
]
remove_tags = [
dict(attrs={'class':['social_buttons','search','tweet','like','inline_socials'
,'stumblebadge','plusone']})
]
extra_css = '''
.author{font-size:small}
img{margin-bottom: 0.4em; display:block; margin-left:auto; margin-right: auto}
'''
preprocess_regexps = [
(re.compile(r'<img src="http://.*\.scorecardresearch\.com/'), lambda m: '')
]
feeds = [('Vice', 'http://www.vice.com/de/rss')]

BIN
resources/images/polish.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

View File

@ -12,14 +12,14 @@ msgstr ""
"Report-Msgid-Bugs-To: Debian iso-codes team <pkg-isocodes-"
"devel@lists.alioth.debian.org>\n"
"POT-Creation-Date: 2011-11-25 14:01+0000\n"
"PO-Revision-Date: 2012-10-24 18:16+0000\n"
"PO-Revision-Date: 2013-02-04 07:01+0000\n"
"Last-Translator: drMerry <Unknown>\n"
"Language-Team: Dutch <vertaling@vrijschrift.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2012-10-25 05:35+0000\n"
"X-Generator: Launchpad (build 16179)\n"
"X-Launchpad-Export-Date: 2013-02-05 04:44+0000\n"
"X-Generator: Launchpad (build 16468)\n"
"Language: nl\n"
#. name for aaa
@ -728,7 +728,7 @@ msgstr "Aimol"
#. name for ain
msgid "Ainu (Japan)"
msgstr ""
msgstr "Ainu (Japan)"
#. name for aio
msgid "Aiton"
@ -2496,7 +2496,7 @@ msgstr "Wit-Russisch; Belarussisch"
#. name for bem
msgid "Bemba (Zambia)"
msgstr ""
msgstr "Bemba (Zambia)"
#. name for ben
msgid "Bengali"
@ -2892,7 +2892,7 @@ msgstr "Bimoba"
#. name for bin
msgid "Bini"
msgstr "Bini"
msgstr "Bini; Edo"
#. name for bio
msgid "Nai"
@ -4684,7 +4684,7 @@ msgstr ""
#. name for car
msgid "Carib; Galibi"
msgstr ""
msgstr "Caribische talen"
#. name for cas
msgid "Tsimané"
@ -6360,7 +6360,7 @@ msgstr ""
#. name for div
msgid "Dhivehi"
msgstr ""
msgstr "Divehi"
#. name for diw
msgid "Dinka; Northwestern"
@ -6604,7 +6604,7 @@ msgstr "Vietnamese dong"
#. name for doi
msgid "Dogri (macrolanguage)"
msgstr ""
msgstr "Dogri"
#. name for dok
msgid "Dondo"
@ -7508,7 +7508,7 @@ msgstr ""
#. name for fan
msgid "Fang (Equatorial Guinea)"
msgstr ""
msgstr "Fang"
#. name for fao
msgid "Faroese"
@ -7584,7 +7584,7 @@ msgstr "Fijisch"
#. name for fil
msgid "Filipino"
msgstr ""
msgstr "Filipijns"
#. name for fin
msgid "Finnish"
@ -8192,7 +8192,7 @@ msgstr ""
#. name for gez
msgid "Geez"
msgstr "Geez"
msgstr "Ge'ez"
#. name for gfk
msgid "Patpatar"
@ -9016,7 +9016,7 @@ msgstr ""
#. name for gwi
msgid "Gwichʼin"
msgstr ""
msgstr "Gwichʼin"
#. name for gwj
msgid "/Gwi"
@ -10076,7 +10076,7 @@ msgstr ""
#. name for iii
msgid "Yi; Sichuan"
msgstr ""
msgstr "Yi; Sichuan - Nuosu"
#. name for ijc
msgid "Izon"
@ -11076,11 +11076,11 @@ msgstr ""
#. name for kal
msgid "Kalaallisut"
msgstr ""
msgstr "Groenlands"
#. name for kam
msgid "Kamba (Kenya)"
msgstr ""
msgstr "Kamba (Kenya)"
#. name for kan
msgid "Kannada"
@ -11784,7 +11784,7 @@ msgstr ""
#. name for khm
msgid "Khmer; Central"
msgstr ""
msgstr "Khmer, Cambodjaans"
#. name for khn
msgid "Khandesi"
@ -12488,7 +12488,7 @@ msgstr ""
#. name for kok
msgid "Konkani (macrolanguage)"
msgstr ""
msgstr "Konkani"
#. name for kol
msgid "Kol (Papua New Guinea)"
@ -12500,7 +12500,7 @@ msgstr "Komi"
#. name for kon
msgid "Kongo"
msgstr "Kongo"
msgstr "Kikongo"
#. name for koo
msgid "Konzo"
@ -13736,7 +13736,7 @@ msgstr ""
#. name for lao
msgid "Lao"
msgstr "Lao"
msgstr "Laotiaans"
#. name for lap
msgid "Laka (Chad)"
@ -14872,7 +14872,7 @@ msgstr ""
#. name for lug
msgid "Ganda"
msgstr "Ganda"
msgstr "Luganda"
#. name for lui
msgid "Luiseno"
@ -15468,7 +15468,7 @@ msgstr ""
#. name for men
msgid "Mende (Sierra Leone)"
msgstr ""
msgstr "Mende"
#. name for meo
msgid "Malay; Kedah"
@ -15832,7 +15832,7 @@ msgstr ""
#. name for mic
msgid "Mi'kmaq"
msgstr ""
msgstr "Mi'kmaq; Micmac"
#. name for mid
msgid "Mandaic"
@ -16812,7 +16812,7 @@ msgstr ""
#. name for msa
msgid "Malay (macrolanguage)"
msgstr ""
msgstr "Maleis"
#. name for msb
msgid "Masbatenyo"
@ -17680,7 +17680,7 @@ msgstr "Nauruaans"
#. name for nav
msgid "Navajo"
msgstr ""
msgstr "Navajo"
#. name for naw
msgid "Nawuri"
@ -18068,7 +18068,7 @@ msgstr ""
#. name for new
msgid "Bhasa; Nepal"
msgstr ""
msgstr "Newari; Nepal"
#. name for nex
msgid "Neme"
@ -18296,7 +18296,7 @@ msgstr ""
#. name for nia
msgid "Nias"
msgstr "Niaas"
msgstr "Nias"
#. name for nib
msgid "Nakame"
@ -18972,7 +18972,7 @@ msgstr ""
#. name for nqo
msgid "N'Ko"
msgstr "Nko"
msgstr "N'Ko"
#. name for nra
msgid "Ngom"
@ -19080,7 +19080,7 @@ msgstr ""
#. name for nso
msgid "Sotho; Northern"
msgstr ""
msgstr "Pedi; Sepedi; Noord-Sothotisch"
#. name for nsp
msgid "Nepalese Sign Language"
@ -19296,7 +19296,7 @@ msgstr ""
#. name for nwc
msgid "Newari; Old"
msgstr ""
msgstr "Newari; Klassiek Nepal"
#. name for nwe
msgid "Ngwe"
@ -19372,7 +19372,7 @@ msgstr ""
#. name for nya
msgid "Nyanja"
msgstr ""
msgstr "Nyanja"
#. name for nyb
msgid "Nyangbo"
@ -20028,7 +20028,7 @@ msgstr "Spaans; oud"
#. name for oss
msgid "Ossetian"
msgstr ""
msgstr "Ossetisch"
#. name for ost
msgid "Osatu"
@ -20212,7 +20212,7 @@ msgstr "Pampanga"
#. name for pan
msgid "Panjabi"
msgstr ""
msgstr "Punjabi"
#. name for pao
msgid "Paiute; Northern"
@ -21848,7 +21848,7 @@ msgstr ""
#. name for rar
msgid "Maori; Cook Islands"
msgstr ""
msgstr "Rarotongan; Cookeilanden Maori"
#. name for ras
msgid "Tegali"
@ -22220,7 +22220,7 @@ msgstr ""
#. name for rom
msgid "Romany"
msgstr "Roma"
msgstr "Romani"
#. name for ron
msgid "Romanian"
@ -23380,7 +23380,7 @@ msgstr ""
#. name for sma
msgid "Sami; Southern"
msgstr ""
msgstr "Samisch; zuid, Laps; zuid"
#. name for smb
msgid "Simbari"
@ -23396,7 +23396,7 @@ msgstr ""
#. name for sme
msgid "Sami; Northern"
msgstr ""
msgstr "Samisch; noord, Laps; noord"
#. name for smf
msgid "Auwe"
@ -23428,7 +23428,7 @@ msgstr ""
#. name for smn
msgid "Sami; Inari"
msgstr ""
msgstr "Sami; Inari, Laps; Inari"
#. name for smo
msgid "Samoan"
@ -23448,7 +23448,7 @@ msgstr ""
#. name for sms
msgid "Sami; Skolt"
msgstr ""
msgstr "Sami; Skolt, Laps; Skolt"
#. name for smt
msgid "Simte"
@ -24188,7 +24188,7 @@ msgstr ""
#. name for swa
msgid "Swahili (macrolanguage)"
msgstr ""
msgstr "Swahili"
#. name for swb
msgid "Comorian; Maore"
@ -24344,7 +24344,7 @@ msgstr ""
#. name for syc
msgid "Syriac; Classical"
msgstr ""
msgstr "Syriac; Klassiek"
#. name for syi
msgid "Seki"
@ -25332,7 +25332,7 @@ msgstr ""
#. name for tlh
msgid "Klingon"
msgstr ""
msgstr "Klingon; tlhIngan-Hol"
#. name for tli
msgid "Tlingit"
@ -27412,7 +27412,7 @@ msgstr ""
#. name for wal
msgid "Wolaytta"
msgstr ""
msgstr "Walamo"
#. name for wam
msgid "Wampanoag"
@ -27436,7 +27436,7 @@ msgstr ""
#. name for war
msgid "Waray (Philippines)"
msgstr ""
msgstr "Waray (Filipijns)"
#. name for was
msgid "Washo"
@ -28240,7 +28240,7 @@ msgstr ""
#. name for xal
msgid "Kalmyk"
msgstr ""
msgstr "Kalmyk"
#. name for xam
msgid "/Xam"
@ -30144,7 +30144,7 @@ msgstr ""
#. name for zab
msgid "Zapotec; San Juan Guelavía"
msgstr ""
msgstr "Zapotec"
#. name for zac
msgid "Zapotec; Ocotlán"
@ -30308,7 +30308,7 @@ msgstr ""
#. name for zha
msgid "Zhuang"
msgstr ""
msgstr "Zhuang, Tsjoeang"
#. name for zhb
msgid "Zhaba"

View File

@ -757,13 +757,14 @@ plugins += [
# New metadata download plugins {{{
from calibre.ebooks.metadata.sources.google import GoogleBooks
from calibre.ebooks.metadata.sources.amazon import Amazon
from calibre.ebooks.metadata.sources.edelweiss import Edelweiss
from calibre.ebooks.metadata.sources.openlibrary import OpenLibrary
from calibre.ebooks.metadata.sources.isbndb import ISBNDB
from calibre.ebooks.metadata.sources.overdrive import OverDrive
from calibre.ebooks.metadata.sources.douban import Douban
from calibre.ebooks.metadata.sources.ozon import Ozon
plugins += [GoogleBooks, Amazon, OpenLibrary, ISBNDB, OverDrive, Douban, Ozon]
plugins += [GoogleBooks, Amazon, Edelweiss, OpenLibrary, ISBNDB, OverDrive, Douban, Ozon]
# }}}
@ -789,6 +790,11 @@ class ActionConvert(InterfaceActionBase):
actual_plugin = 'calibre.gui2.actions.convert:ConvertAction'
description = _('Convert books to various ebook formats')
# class ActionPolish(InterfaceActionBase):
# name = 'Polish Books'
# actual_plugin = 'calibre.gui2.actions.polish:PolishAction'
# description = _('Fine tune your ebooks')
#
class ActionDelete(InterfaceActionBase):
name = 'Remove Books'
actual_plugin = 'calibre.gui2.actions.delete:DeleteAction'
@ -924,7 +930,7 @@ class ActionPluginUpdater(InterfaceActionBase):
plugins += [ActionAdd, ActionFetchAnnotations, ActionGenerateCatalog,
ActionConvert, ActionDelete, ActionEditMetadata, ActionView,
ActionFetchNews, ActionSaveToDisk, ActionQuickview,
ActionFetchNews, ActionSaveToDisk, ActionQuickview, #ActionPolish,
ActionShowBookDetails,ActionRestart, ActionOpenFolder, ActionConnectShare,
ActionSendToDevice, ActionHelp, ActionPreferences, ActionSimilarBooks,
ActionAddToLibrary, ActionEditCollections, ActionChooseLibrary,

View File

@ -456,8 +456,8 @@ class NookHD(TabletOutput):
name = 'Nook HD+'
short_name = 'nook_hd_plus'
description = _('Intended for the Nook HD+ and similar tablet devices with '
'a resolution of 1080x1920')
screen_size = comic_screen_size = (1080, 1920)
'a resolution of 1280x1920')
screen_size = comic_screen_size = (1280, 1920)
class SonyReaderOutput(OutputProfile):

View File

@ -92,7 +92,7 @@ def restore_plugin_state_to_default(plugin_or_name):
config['enabled_plugins'] = ep
default_disabled_plugins = set([
'Overdrive', 'Douban Books', 'OZON.ru',
'Overdrive', 'Douban Books', 'OZON.ru', 'Edelweiss',
])
def is_disabled(plugin):

View File

@ -10,7 +10,7 @@ import cStringIO
from calibre.devices.usbms.driver import USBMS
HTC_BCDS = [0x100, 0x0222, 0x0226, 0x227, 0x228, 0x229, 0x0231, 0x9999]
HTC_BCDS = [0x100, 0x0222, 0x0224, 0x0226, 0x227, 0x228, 0x229, 0x0231, 0x9999]
class ANDROID(USBMS):
@ -236,7 +236,7 @@ class ANDROID(USBMS):
'ADVANCED', 'SGH-I727', 'USB_FLASH_DRIVER', 'ANDROID',
'S5830I_CARD', 'MID7042', 'LINK-CREATE', '7035', 'VIEWPAD_7E',
'NOVO7', 'MB526', '_USB#WYK7MSF8KE', 'TABLET_PC', 'F', 'MT65XX_MS',
'ICS', 'E400']
'ICS', 'E400', '__FILE-STOR_GADG']
WINDOWS_CARD_A_MEM = ['ANDROID_PHONE', 'GT-I9000_CARD', 'SGH-I897',
'FILE-STOR_GADGET', 'SGH-T959_CARD', 'SGH-T959', 'SAMSUNG_ANDROID', 'GT-P1000_CARD',
'A70S', 'A101IT', '7', 'INCREDIBLE', 'A7EB', 'SGH-T849_CARD',

View File

@ -626,7 +626,10 @@ class HTMLPreProcessor(object):
if getattr(self.extra_opts, 'asciiize', False):
from calibre.utils.localization import get_udc
from calibre.utils.mreplace import MReplace
unihandecoder = get_udc()
mr = MReplace(data={u'«':u'&lt;'*3, u'»':u'&gt;'*3})
html = mr.mreplace(html)
html = unihandecoder.decode(html)
if getattr(self.extra_opts, 'enable_heuristics', False):

View File

@ -0,0 +1,395 @@
#!/usr/bin/env python
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:fdm=marker:ai
from __future__ import (unicode_literals, division, absolute_import,
print_function)
__license__ = 'GPL v3'
__copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
import time, re
from threading import Thread
from Queue import Queue, Empty
from calibre import as_unicode, random_user_agent
from calibre.ebooks.metadata import check_isbn
from calibre.ebooks.metadata.sources.base import Source
def parse_html(raw):
import html5lib
from calibre.ebooks.chardet import xml_to_unicode
from calibre.utils.cleantext import clean_ascii_chars
raw = clean_ascii_chars(xml_to_unicode(raw, strip_encoding_pats=True,
resolve_entities=True, assume_utf8=True)[0])
return html5lib.parse(raw, treebuilder='lxml',
namespaceHTMLElements=False).getroot()
def CSSSelect(expr):
from cssselect import HTMLTranslator
from lxml.etree import XPath
return XPath(HTMLTranslator().css_to_xpath(expr))
def astext(node):
from lxml import etree
return etree.tostring(node, method='text', encoding=unicode,
with_tail=False).strip()
class Worker(Thread): # {{{
def __init__(self, sku, url, relevance, result_queue, br, timeout, log, plugin):
Thread.__init__(self)
self.daemon = True
self.url, self.br, self.log, self.timeout = url, br, log, timeout
self.result_queue, self.plugin, self.sku = result_queue, plugin, sku
self.relevance = relevance
def run(self):
try:
raw = self.br.open_novisit(self.url, timeout=self.timeout).read()
except:
self.log.exception('Failed to load details page: %r'%self.url)
return
try:
mi = self.parse(raw)
mi.source_relevance = self.relevance
self.plugin.clean_downloaded_metadata(mi)
self.result_queue.put(mi)
except:
self.log.exception('Failed to parse details page: %r'%self.url)
def parse(self, raw):
from calibre.ebooks.metadata.book.base import Metadata
from calibre.utils.date import parse_only_date, UNDEFINED_DATE
root = parse_html(raw)
sku = CSSSelect('div.sku.attGroup')(root)[0]
info = sku.getparent()
top = info.getparent().getparent()
banner = top.find('div')
spans = banner.findall('span')
title = ''
for i, span in enumerate(spans):
if i == 0 or '12pt' in span.get('style', ''):
title += astext(span)
else:
break
authors = [re.sub(r'\(.*\)', '', x).strip() for x in astext(spans[-1]).split(',')]
mi = Metadata(title.strip(), authors)
# Identifiers
isbns = [check_isbn(x.strip()) for x in astext(sku).split(',')]
for isbn in isbns:
if isbn:
self.plugin.cache_isbn_to_identifier(isbn, self.sku)
isbns = sorted(isbns, key=lambda x:len(x) if x else 0, reverse=True)
if isbns and isbns[0]:
mi.isbn = isbns[0]
mi.set_identifier('edelweiss', self.sku)
# Tags
bisac = CSSSelect('div.bisac.attGroup')(root)
if bisac:
bisac = astext(bisac[0])
mi.tags = [x.strip() for x in bisac.split(',')]
mi.tags = [t[1:].strip() if t.startswith('&') else t for t in mi.tags]
# Publisher
pub = CSSSelect('div.supplier.attGroup')(root)
if pub:
pub = astext(pub[0])
mi.publisher = pub
# Pubdate
pub = CSSSelect('div.shipDate.attGroupItem')(root)
if pub:
pub = astext(pub[0])
parts = pub.partition(':')[0::2]
pub = parts[1] or parts[0]
try:
q = parse_only_date(pub, assume_utc=True)
if q.year != UNDEFINED_DATE:
mi.pubdate = q
except:
self.log.exception('Error parsing published date: %r'%pub)
# Comments
comm = ''
general = CSSSelect('div#pd-general-overview-content')(root)
if general:
q = self.render_comments(general[0])
if q != '<p>No title summary available. </p>':
comm += q
general = CSSSelect('div#pd-general-contributor-content')(root)
if general:
comm += self.render_comments(general[0])
general = CSSSelect('div#pd-general-quotes-content')(root)
if general:
comm += self.render_comments(general[0])
if comm:
mi.comments = comm
# Cover
img = CSSSelect('img.title-image[src]')(root)
if img:
href = img[0].get('src').replace('jacket_covers/medium/',
'jacket_covers/flyout/')
self.plugin.cache_identifier_to_cover_url(self.sku, href)
mi.has_cover = self.plugin.cached_identifier_to_cover_url(self.sku) is not None
return mi
def render_comments(self, desc):
from lxml import etree
from calibre.library.comments import sanitize_comments_html
for c in desc.xpath('descendant::noscript'):
c.getparent().remove(c)
for a in desc.xpath('descendant::a[@href]'):
del a.attrib['href']
a.tag = 'span'
desc = etree.tostring(desc, method='html', encoding=unicode).strip()
# remove all attributes from tags
desc = re.sub(r'<([a-zA-Z0-9]+)\s[^>]+>', r'<\1>', desc)
# Collapse whitespace
#desc = re.sub('\n+', '\n', desc)
#desc = re.sub(' +', ' ', desc)
# Remove comments
desc = re.sub(r'(?s)<!--.*?-->', '', desc)
return sanitize_comments_html(desc)
# }}}
class Edelweiss(Source):
name = 'Edelweiss'
description = _('Downloads metadata and covers from Edelweiss - A catalog updated by book publishers')
capabilities = frozenset(['identify', 'cover'])
touched_fields = frozenset([
'title', 'authors', 'tags', 'pubdate', 'comments', 'publisher',
'identifier:isbn', 'identifier:edelweiss'])
supports_gzip_transfer_encoding = True
has_html_comments = True
@property
def user_agent(self):
# Pass in an index to random_user_agent() to test with a particular
# user agent
return random_user_agent()
def _get_book_url(self, sku):
if sku:
return 'http://edelweiss.abovethetreeline.com/ProductDetailPage.aspx?sku=%s'%sku
def get_book_url(self, identifiers): # {{{
sku = identifiers.get('edelweiss', None)
if sku:
return 'edelweiss', sku, self._get_book_url(sku)
# }}}
def get_cached_cover_url(self, identifiers): # {{{
sku = identifiers.get('edelweiss', None)
if not sku:
isbn = identifiers.get('isbn', None)
if isbn is not None:
sku = self.cached_isbn_to_identifier(isbn)
return self.cached_identifier_to_cover_url(sku)
# }}}
def create_query(self, log, title=None, authors=None, identifiers={}): # {{{
from urllib import urlencode
BASE_URL = 'http://edelweiss.abovethetreeline.com/CatalogOverview.aspx?'
params = {
'group':'search',
'searchType':999,
'searchOrgID':'',
'dateRange':0,
'isbn':'',
}
for num in (0, 1, 2, 3, 4, 5, 6, 200, 201, 202, 204):
params['condition%d'%num] = 1
params['keywords%d'%num] = ''
title_key, author_key = 'keywords200', 'keywords201'
isbn = check_isbn(identifiers.get('isbn', None))
found = False
if isbn is not None:
params['isbn'] = isbn
found = True
elif title or authors:
title_tokens = list(self.get_title_tokens(title))
if title_tokens:
params[title_key] = ' '.join(title_tokens)
found = True
author_tokens = self.get_author_tokens(authors,
only_first_author=True)
if author_tokens:
params[author_key] = ' '.join(author_tokens)
found = True
if not found:
return None
for k in (title_key, author_key, 'isbn'):
v = params[k]
if isinstance(v, unicode):
params[k] = v.encode('utf-8')
return BASE_URL+urlencode(params)
# }}}
def identify(self, log, result_queue, abort, title=None, authors=None, # {{{
identifiers={}, timeout=30):
from urlparse import parse_qs
book_url = self._get_book_url(identifiers.get('edelweiss', None))
br = self.browser
if book_url:
entries = [(book_url, identifiers['edelweiss'])]
else:
entries = []
query = self.create_query(log, title=title, authors=authors,
identifiers=identifiers)
if not query:
log.error('Insufficient metadata to construct query')
return
try:
raw = br.open_novisit(query, timeout=timeout).read()
except Exception as e:
log.exception('Failed to make identify query: %r'%query)
return as_unicode(e)
try:
root = parse_html(raw)
except Exception as e:
log.exception('Failed to parse identify results')
return as_unicode(e)
for entry in CSSSelect('div.listRow div.listRowMain')(root):
a = entry.xpath('descendant::a[contains(@href, "sku=") and contains(@href, "ProductDetailPage.aspx")]')
if not a: continue
href = a[0].get('href')
prefix, qs = href.partition('?')[0::2]
sku = parse_qs(qs).get('sku', None)
if sku and sku[0]:
sku = sku[0]
div = CSSSelect('div.sku.attGroup')(entry)
if div:
text = astext(div[0])
isbns = [check_isbn(x.strip()) for x in text.split(',')]
for isbn in isbns:
if isbn:
self.cache_isbn_to_identifier(isbn, sku)
for img in entry.xpath('descendant::img[contains(@src, "/jacket_covers/thumbnail/")]'):
self.cache_identifier_to_cover_url(sku, img.get('src').replace('/thumbnail/', '/flyout/'))
div = CSSSelect('div.format.attGroup')(entry)
text = astext(div[0]).lower()
if 'audio' in text or 'mp3' in text: # Audio-book, ignore
continue
entries.append((self._get_book_url(sku), sku))
if (not entries and identifiers and title and authors and
not abort.is_set()):
return self.identify(log, result_queue, abort, title=title,
authors=authors, timeout=timeout)
if not entries:
return
workers = [Worker(sku, url, i, result_queue, br.clone_browser(), timeout, log, self)
for i, (url, sku) in enumerate(entries[:5])]
for w in workers:
w.start()
# Don't send all requests at the same time
time.sleep(0.1)
while not abort.is_set():
a_worker_is_alive = False
for w in workers:
w.join(0.2)
if abort.is_set():
break
if w.is_alive():
a_worker_is_alive = True
if not a_worker_is_alive:
break
# }}}
def download_cover(self, log, result_queue, abort, # {{{
title=None, authors=None, identifiers={}, timeout=30):
cached_url = self.get_cached_cover_url(identifiers)
if cached_url is None:
log.info('No cached cover found, running identify')
rq = Queue()
self.identify(log, rq, abort, title=title, authors=authors,
identifiers=identifiers)
if abort.is_set():
return
results = []
while True:
try:
results.append(rq.get_nowait())
except Empty:
break
results.sort(key=self.identify_results_keygen(
title=title, authors=authors, identifiers=identifiers))
for mi in results:
cached_url = self.get_cached_cover_url(mi.identifiers)
if cached_url is not None:
break
if cached_url is None:
log.info('No cover found')
return
if abort.is_set():
return
br = self.browser
log('Downloading cover from:', cached_url)
try:
cdata = br.open_novisit(cached_url, timeout=timeout).read()
result_queue.put((self, cdata))
except:
log.exception('Failed to download cover from:', cached_url)
# }}}
if __name__ == '__main__':
from calibre.ebooks.metadata.sources.test import (
test_identify_plugin, title_test, authors_test, comments_test, pubdate_test)
tests = [
# Multiple authors and two part title and no general description
({'identifiers':{'edelweiss':'0321180607'}},
[title_test(
"XQuery from the Experts: A Guide to the W3C XML Query Language"
, exact=True), authors_test([
'Howard Katz', 'Don Chamberlin', 'Denise Draper', 'Mary Fernandez',
'Michael Kay', 'Jonathan Robie', 'Michael Rys', 'Jerome Simeon',
'Jim Tivy', 'Philip Wadler']), pubdate_test(2003, 8, 22),
comments_test('Jérôme Siméon'), lambda mi: bool(mi.comments and 'No title summary' not in mi.comments)
]),
( # An isbn not present in edelweiss
{'identifiers':{'isbn': '9780316044981'}, 'title':'The Heroes',
'authors':['Joe Abercrombie']},
[title_test('The Heroes', exact=True),
authors_test(['Joe Abercrombie'])]
),
( # Pubdate
{'title':'The Great Gatsby', 'authors':['F. Scott Fitzgerald']},
[title_test('The great gatsby', exact=True),
authors_test(['F. Scott Fitzgerald']), pubdate_test(2004, 9, 29)]
),
]
start, stop = 0, len(tests)
tests = tests[start:stop]
test_identify_plugin(Edelweiss.name, tests)

View File

@ -11,7 +11,7 @@ import os, tempfile, time
from Queue import Queue, Empty
from threading import Event
from calibre.customize.ui import metadata_plugins
from calibre.customize.ui import all_metadata_plugins
from calibre import prints, sanitize_file_name2
from calibre.ebooks.metadata import check_isbn
from calibre.ebooks.metadata.sources.base import (create_log,
@ -94,6 +94,16 @@ def comments_test(sentinel):
return False
return test
def pubdate_test(year, month, day):
def test(mi):
p = mi.pubdate
if p is not None and p.year == year and p.month == month and p.day == day:
return True
return False
return test
def init_test(tdir_name):
tdir = tempfile.gettempdir()
lf = os.path.join(tdir, tdir_name.replace(' ', '')+'_identify_test.txt')
@ -178,8 +188,8 @@ def test_identify_plugin(name, tests, modify_plugin=lambda plugin:None,
test.
'''
plugin = None
for x in metadata_plugins(['identify']):
if x.name == name:
for x in all_metadata_plugins():
if x.name == name and 'identify' in x.capabilities:
plugin = x
break
modify_plugin(plugin)

View File

@ -5,7 +5,7 @@ __copyright__ = '2010, Kovid Goyal <kovid at kovidgoyal.net>'
import os, glob, re, functools
from urlparse import urlparse
from urllib import unquote
from uuid import uuid4
from collections import Counter
from lxml import etree
from lxml.builder import ElementMaker
@ -249,16 +249,19 @@ class TOC(list):
navmap = E.navMap()
root.append(navmap)
root.set('{http://www.w3.org/XML/1998/namespace}lang', 'en')
c = Counter()
def navpoint(parent, np):
text = np.text
if not text:
text = ''
c[1] += 1
item_id = 'num_%d'%c[1]
elem = E.navPoint(
E.navLabel(E.text(re.sub(r'\s+', ' ', text))),
E.content(src=unicode(np.href)+(('#' + unicode(np.fragment))
if np.fragment else '')),
id=str(uuid4()),
id=item_id,
playOrder=str(np.play_order)
)
au = getattr(np, 'author', None)

View File

@ -123,6 +123,12 @@ class EXTHHeader(object): # {{{
raw = check_isbn(content[len(isig):])
if raw and not self.mi.isbn:
self.mi.isbn = raw
elif content.startswith('calibre:'):
# calibre book uuid is stored here by recent calibre
# releases
cid = content[len('calibre:'):]
if cid:
self.mi.application_id = self.mi.uuid = cid
except:
pass
elif idx == 113: # ASIN or other id

View File

@ -337,6 +337,24 @@ def xml2unicode(root, pretty_print=False):
def xml2text(elem):
return etree.tostring(elem, method='text', encoding=unicode, with_tail=False)
def serialize(data, media_type, pretty_print=False):
if isinstance(data, etree._Element):
ans = xml2str(data, pretty_print=pretty_print)
if media_type in OEB_DOCS:
# Convert self closing div|span|a|video|audio|iframe|etc tags
# to normally closed ones, as they are interpreted
# incorrectly by some browser based renderers
ans = close_self_closing_tags(ans)
return ans
if isinstance(data, unicode):
return data.encode('utf-8')
if hasattr(data, 'cssText'):
data = data.cssText
if isinstance(data, unicode):
data = data.encode('utf-8')
return data + b'\n'
return bytes(data)
ASCII_CHARS = set(chr(x) for x in xrange(128))
UNIBYTE_CHARS = set(chr(x) for x in xrange(256))
URL_SAFE = set('ABCDEFGHIJKLMNOPQRSTUVWXYZ'
@ -960,23 +978,7 @@ class Manifest(object):
self._data = None
def __str__(self):
data = self.data
if isinstance(data, etree._Element):
ans = xml2str(data, pretty_print=self.oeb.pretty_print)
if self.media_type in OEB_DOCS:
# Convert self closing div|span|a|video|audio|iframe|etc tags
# to normally closed ones, as they are interpreted
# incorrectly by some browser based renderers
ans = close_self_closing_tags(ans)
return ans
if isinstance(data, unicode):
return data.encode('utf-8')
if hasattr(data, 'cssText'):
data = data.cssText
if isinstance(data, unicode):
data = data.encode('utf-8')
return data + b'\n'
return str(data)
return serialize(self.data, self.media_type, pretty_print=self.oeb.pretty_print)
def __unicode__(self):
data = self.data

View File

@ -13,17 +13,20 @@ from urllib import unquote as urlunquote
from lxml import etree
from calibre import guess_type, CurrentDir
from calibre.customize.ui import (plugin_for_input_format,
plugin_for_output_format)
from calibre.ebooks.chardet import xml_to_unicode
from calibre.ebooks.conversion.plugins.epub_input import (
ADOBE_OBFUSCATION, IDPF_OBFUSCATION, decrypt_font)
from calibre.ebooks.conversion.preprocess import HTMLPreProcessor, CSSPreProcessor
from calibre.ebooks.mobi import MobiError
from calibre.ebooks.mobi.reader.headers import MetadataHeader
from calibre.ebooks.oeb.base import OEB_DOCS, _css_logger, OEB_STYLES, OPF2_NS
from calibre.ebooks.mobi.tweak import set_cover
from calibre.ebooks.oeb.base import (serialize, OEB_DOCS, _css_logger,
OEB_STYLES, OPF2_NS)
from calibre.ebooks.oeb.polish.errors import InvalidBook, DRMError
from calibre.ebooks.oeb.parse_utils import NotHTML, parse_html, RECOVER_PARSER
from calibre.ptempfile import PersistentTemporaryDirectory
from calibre.utils.fonts.sfnt.container import Sfnt
from calibre.ptempfile import PersistentTemporaryDirectory, PersistentTemporaryFile
from calibre.utils.ipc.simple_worker import fork_job, WorkerError
from calibre.utils.logging import default_log
from calibre.utils.zipfile import ZipFile
@ -43,6 +46,7 @@ class Container(object):
self.parsed_cache = {}
self.mime_map = {}
self.name_path_map = {}
self.dirtied = set()
# Map of relative paths with '/' separators from root of unzipped ePub
# to absolute paths on filesystem with os-specific separators
@ -141,8 +145,6 @@ class Container(object):
data = self.parse_xml(data)
elif mime in OEB_STYLES:
data = self.parse_css(data, self.relpath(path))
elif mime in OEB_FONTS or path.rpartition('.')[-1].lower() in {'ttf', 'otf'}:
data = Sfnt(data)
return data
def parse_css(self, data, fname):
@ -189,6 +191,65 @@ class Container(object):
for path in non_linear:
yield path
def remove_item(self, name):
'''
Remove the item identified by name from this container. This removes all
references to the item in the OPF manifest, guide and spine as well as from
any internal caches.
'''
removed = set()
for elem in self.opf.xpath('//opf:manifest/opf:item[@href]',
namespaces={'opf':OPF2_NS}):
if self.href_to_name(elem.get('href')) == name:
id_ = elem.get('id', None)
if id_ is not None:
removed.add(id_)
elem.getparent().remove(elem)
self.dirty(self.opf_name)
if removed:
for item in self.opf.xpath('//opf:spine/opf:itemref[@idref]',
namespaces={'opf':OPF2_NS}):
idref = item.get('idref')
if idref in removed:
item.getparent().remove(item)
self.dirty(self.opf_name)
for item in self.opf.xpath('//opf:guide/opf:reference[@href]',
namespaces={'opf':OPF2_NS}):
if self.href_to_name(item.get('href')) == name:
item.getparent().remove(item)
self.dirty(self.opf_name)
path = self.name_path_map.pop(name)
if os.path.exists(path):
os.remove(path)
self.mime_map.pop(name, None)
self.parsed_cache.pop(name, None)
self.dirtied.discard(name)
def dirty(self, name):
self.dirtied.add(name)
def commit(self, outpath=None):
for name in tuple(self.dirtied):
self.dirtied.remove(name)
data = self.parsed_cache.pop(name)
data = serialize(data, self.mime_map[name])
with open(self.name_path_map[name], 'wb') as f:
f.write(data)
def compare_to(self, other):
if set(self.name_path_map) != set(other.name_path_map):
return 'Set of files is not the same'
mismatches = []
for name, path in self.name_path_map.iteritems():
opath = other.name_path_map[name]
with open(path, 'rb') as f1, open(opath, 'rb') as f2:
if f1.read() != f2.read():
mismatches.append('The file %s is not the same'%name)
return '\n'.join(mismatches)
# EPUB {{{
class InvalidEpub(InvalidBook):
pass
@ -294,8 +355,26 @@ class EpubContainer(Container):
if not tkey:
raise InvalidBook('Failed to find obfuscation key')
decrypt_font(tkey, path, alg)
self.obfuscated_fonts[name] = (alg, tkey)
self.obfuscated_fonts[font] = (alg, tkey)
def commit(self, outpath=None):
super(EpubContainer, self).commit()
for name in self.obfuscated_fonts:
if name not in self.name_path_map:
continue
alg, key = self.obfuscated_fonts[name]
# Decrypting and encrypting are the same operation (XOR with key)
decrypt_font(key, self.name_path_map[name], alg)
if outpath is None:
outpath = self.pathtoepub
from calibre.ebooks.tweak import zip_rebuilder
with open(join(self.root, 'mimetype'), 'wb') as f:
f.write(guess_type('a.epub')[0])
zip_rebuilder(self.root, outpath)
# }}}
# AZW3 {{{
class InvalidMobi(InvalidBook):
pass
@ -357,14 +436,40 @@ class AZW3Container(Container):
super(AZW3Container, self).__init__(tdir, opf_path, log)
self.obfuscated_fonts = {x.replace(os.sep, '/') for x in obfuscated_fonts}
def commit(self, outpath=None):
super(AZW3Container, self).commit()
if outpath is None:
outpath = self.pathtoazw3
from calibre.ebooks.conversion.plumber import Plumber, create_oebbook
opf = self.name_path_map[self.opf_name]
plumber = Plumber(opf, outpath, self.log)
plumber.setup_options()
inp = plugin_for_input_format('azw3')
outp = plugin_for_output_format('azw3')
plumber.opts.mobi_passthrough = True
oeb = create_oebbook(default_log, opf, plumber.opts)
set_cover(oeb)
outp.convert(oeb, outpath, inp, plumber.opts, default_log)
# }}}
def get_container(path, log=None):
if log is None: log = default_log
ebook = (AZW3Container if path.rpartition('.')[-1].lower() in {'azw3', 'mobi'}
else EpubContainer)(path, log)
return ebook
if __name__ == '__main__':
def test_roundtrip():
ebook = get_container(sys.argv[-1])
for s in ebook.spine_items:
print (ebook.relpath(s))
p = PersistentTemporaryFile(suffix='.'+sys.argv[-1].rpartition('.')[-1])
p.close()
ebook.commit(outpath=p.name)
ebook2 = get_container(p.name)
ebook3 = get_container(p.name)
diff = ebook3.compare_to(ebook2)
if diff is not None:
print (diff)
if __name__ == '__main__':
test_roundtrip()

View File

@ -0,0 +1,160 @@
#!/usr/bin/env python
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:fdm=marker:ai
from __future__ import (unicode_literals, division, absolute_import,
print_function)
__license__ = 'GPL v3'
__copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
import re
from collections import namedtuple
from functools import partial
from calibre.ebooks.oeb.polish.container import get_container
from calibre.ebooks.oeb.polish.stats import StatsCollector
from calibre.ebooks.oeb.polish.subset import subset_all_fonts
from calibre.utils.logging import Log
ALL_OPTS = {
'subset': False,
'opf': None,
'cover': None,
}
SUPPORTED = {'EPUB', 'AZW3'}
# Help {{{
HELP = {'about': _(
'''\
<p><i>Polishing books</i> is all about putting the shine of perfection onto
your carefully crafted ebooks.</p>
<p>Polishing tries to minimize the changes to the internal code of your ebook.
Unlike conversion, it <i>does not</i> flatten CSS, rename files, change font
sizes, adjust margins, etc. Every action performs only the minimum set of
changes needed for the desired effect.</p>
<p>You should use this tool as the last step in your ebook creation process.</p>
<p>Note that polishing only works on files in the <b>%s</b> formats.</p>
''')%_(' or ').join(SUPPORTED),
'subset': _('''\
<p>Subsetting fonts means reducing an embedded font to contain
only the characters used from that font in the book. This
greatly reduces the size of the font files (halving the font
file sizes is common).</p>
<p>For example, if the book uses a specific font for headers,
then subsetting will reduce that font to contain only the
characters present in the actual headers in the book. Or if the
book embeds the bold and italic versions of a font, but bold
and italic text is relatively rare, or absent altogether, then
the bold and italic fonts can either be reduced to only a few
characters or completely removed.</p>
<p>The only downside to subsetting fonts is that if, at a later
date you decide to add more text to your books, the newly added
text might not be covered by the subset font.</p>
'''),
}
def hfix(name, raw):
if name == 'about':
return raw
raw = raw.replace('\n\n', '__XX__')
raw = raw.replace('\n', ' ')
raw = raw.replace('__XX__', '\n')
return raw
CLI_HELP = {x:hfix(x, re.sub('<.*?>', '', y)) for x, y in HELP.iteritems()}
# }}}
def polish(file_map, opts, log, report):
for inbook, outbook in file_map.iteritems():
report('Polishing: %s'%(inbook.rpartition('.')[-1].upper()))
ebook = get_container(inbook, log)
if opts.subset:
stats = StatsCollector(ebook)
if opts.subset:
report('\n### Subsetting embedded fonts')
subset_all_fonts(ebook, stats.font_stats, report)
report('')
ebook.commit(outbook)
def gui_polish(data):
files = data.pop('files')
file_map = {x:x for x in files}
opts = ALL_OPTS.copy()
opts.update(data)
O = namedtuple('Options', ' '.join(data.iterkeys()))
opts = O(**opts)
log = Log(level=Log.DEBUG)
report = []
polish(file_map, opts, log, report.append)
log('\n', '-'*30, ' REPORT ', '-'*30)
for msg in report:
log(msg)
def option_parser():
from calibre.utils.config import OptionParser
USAGE = '%prog [options] input_file [output_file]\n\n' + re.sub(
r'<.*?>', '', CLI_HELP['about'])
parser = OptionParser(usage=USAGE)
o = partial(parser.add_option, default=False, action='store_true')
o('--subset-fonts', '-f', dest='subset', help=CLI_HELP['subset'])
o('--verbose', help=_('Produce more verbose output, useful for debugging.'))
return parser
def cli_polish():
parser = option_parser()
opts, args = parser.parse_args()
log = Log(level=Log.DEBUG if opts.verbose else Log.INFO)
if not args:
parser.print_help()
log.error(_('You must provide the input file to polish'))
raise SystemExit(1)
if len(args) > 2:
parser.print_help()
log.error(_('Unknown extra arguments'))
raise SystemExit(1)
if len(args) == 1:
inbook = args[0]
base, ext = inbook.rpartition('.')[0::2]
outbook = base + '_polished.' + ext
else:
inbook, outbook = args
popts = ALL_OPTS.copy()
for k, v in popts.iteritems():
popts[k] = getattr(opts, k, None)
O = namedtuple('Options', ' '.join(popts.iterkeys()))
popts = O(**popts)
report = []
something = False
for name in ALL_OPTS:
if name not in {'opf', 'cover'}:
if getattr(popts, name):
something = True
if not something:
parser.print_help()
log.error(_('You must specify at least one action to perform'))
raise SystemExit(1)
polish({inbook:outbook}, popts, log, report.append)
log('\n', '-'*30, ' REPORT ', '-'*30)
for msg in report:
log(msg)
log('Output written to:', outbook)
if __name__ == '__main__':
cli_polish()

View File

@ -11,8 +11,8 @@ import json, sys, os
from urllib import unquote
from cssutils import parseStyle
from PyQt4.Qt import (QWebPage, pyqtProperty, QString, QEventLoop, QWebView,
Qt, QSize, QTimer)
from PyQt4.Qt import (pyqtProperty, QString, QEventLoop, Qt, QSize, QTimer)
from PyQt4.QtWebKit import QWebPage, QWebView
from calibre.constants import iswindows
from calibre.ebooks.oeb.display.webview import load_html

View File

@ -0,0 +1,101 @@
#!/usr/bin/env python
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:fdm=marker:ai
from __future__ import (unicode_literals, division, absolute_import,
print_function)
__license__ = 'GPL v3'
__copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
import os, sys
from calibre import prints
from calibre.ebooks.oeb.base import OEB_STYLES, OEB_DOCS, XPath
from calibre.ebooks.oeb.polish.container import OEB_FONTS
from calibre.utils.fonts.sfnt.subset import subset
from calibre.utils.fonts.utils import get_font_names
def remove_font_face_rules(container, sheet, remove_names):
changed = False
for rule in tuple(sheet.cssRules):
if rule.type != rule.FONT_FACE_RULE:
continue
try:
uri = rule.style.getProperty('src').propertyValue[0].uri
except (IndexError, KeyError, AttributeError, TypeError, ValueError):
continue
name = container.href_to_name(uri)
if name in remove_names:
sheet.deleteRule(rule)
changed = True
return changed
def subset_all_fonts(container, font_stats, report):
remove = set()
total_old = total_new = 0
for name, mt in container.mime_map.iteritems():
if mt in OEB_FONTS or name.rpartition('.')[-1].lower() in {'otf', 'ttf'}:
chars = font_stats.get(name, set())
path = container.name_path_map[name]
total_old += os.path.getsize(path)
if not chars:
remove.add(name)
report('Removed unused font: %s'%name)
continue
with open(path, 'r+b') as f:
raw = f.read()
font_name = get_font_names(raw)[-1]
warnings = []
container.log('Subsetting font: %s'%font_name)
nraw, old_sizes, new_sizes = subset(raw, chars,
warnings=warnings)
for w in warnings:
container.log.warn(w)
olen = sum(old_sizes.itervalues())
nlen = sum(new_sizes.itervalues())
total_new += len(nraw)
report('Decreased the font %s to %.1f%% of its original size'%
(font_name, nlen/olen * 100))
f.seek(0), f.truncate(), f.write(nraw)
for name in remove:
container.remove_item(name)
if remove:
for name, mt in container.mime_map.iteritems():
if mt in OEB_STYLES:
sheet = container.parsed(name)
if remove_font_face_rules(container, sheet, remove):
container.dirty(name)
elif mt in OEB_DOCS:
for style in XPath('//h:style')(container.parsed(name)):
if style.get('type', 'text/css') == 'text/css' and style.text:
sheet = container.parse_css(style.text, name)
if remove_font_face_rules(container, sheet, remove):
style.text = sheet.cssText
container.dirty(name)
if total_old > 0:
report('Reduced total font size to %.1f%% of original'%(
total_new/total_old*100))
else:
report('No embedded fonts found')
if __name__ == '__main__':
from calibre.ebooks.oeb.polish.container import get_container
from calibre.ebooks.oeb.polish.stats import StatsCollector
from calibre.utils.logging import default_log
default_log.filter_level = default_log.DEBUG
inbook = sys.argv[-1]
ebook = get_container(inbook, default_log)
report = []
stats = StatsCollector(ebook).font_stats
subset_all_fonts(ebook, stats, report.append)
outbook, ext = inbook.rpartition('.')[0::2]
outbook += '_subset.'+ext
ebook.commit(outbook)
prints('\nReport:')
for msg in report:
prints(msg)
print()
prints('Output written to:', outbook)

View File

@ -209,7 +209,7 @@ class SubsetFonts(object):
no match is found ( can happen if not family matches).
'''
ff = style.get('font-family', [])
lnames = {x.lower() for x in ff}
lnames = {unicode(x).lower() for x in ff}
matching_set = []
# Filter on font-family

View File

@ -109,6 +109,7 @@ defs['bd_show_cover'] = True
defs['bd_overlay_cover_size'] = False
defs['tags_browser_category_icons'] = {}
defs['cover_browser_reflections'] = True
defs['extra_row_spacing'] = 0
del defs
# }}}

View File

@ -0,0 +1,241 @@
#!/usr/bin/env python
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:fdm=marker:ai
from __future__ import (unicode_literals, division, absolute_import,
print_function)
__license__ = 'GPL v3'
__copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
import os, weakref, shutil
from collections import OrderedDict
from PyQt4.Qt import (QDialog, QGridLayout, QIcon, QCheckBox, QLabel, QFrame,
QApplication, QDialogButtonBox, Qt, QSize, QSpacerItem,
QSizePolicy, QTimer)
from calibre.gui2 import error_dialog, Dispatcher
from calibre.gui2.actions import InterfaceAction
from calibre.gui2.convert.metadata import create_opf_file
from calibre.gui2.dialogs.progress import ProgressDialog
from calibre.ptempfile import PersistentTemporaryDirectory
from calibre.utils.config_base import tweaks
class Polish(QDialog):
def __init__(self, db, book_id_map, parent=None):
from calibre.ebooks.oeb.polish.main import HELP
QDialog.__init__(self, parent)
self.db, self.book_id_map = weakref.ref(db), book_id_map
self.setWindowIcon(QIcon(I('polish.png')))
self.setWindowTitle(ngettext(
'Polish book', _('Polish %d books')%len(book_id_map), len(book_id_map)))
self.help_text = {
'polish': _('<h3>About Polishing books</h3>%s')%HELP['about'],
'subset':_('<h3>Subsetting fonts</h3>%s')%HELP['subset'],
}
self.l = l = QGridLayout()
self.setLayout(l)
self.la = la = QLabel('<b>'+_('Select actions to perform:'))
l.addWidget(la, 0, 0, 1, 2)
count = 0
self.actions = OrderedDict([
('subset', _('Subset all embedded fonts')),
])
for name, text in self.actions.iteritems():
count += 1
x = QCheckBox(text, self)
l.addWidget(x, count, 0, 1, 1)
setattr(self, 'opt_'+name, x)
la = QLabel(' <a href="#%s">%s</a>'%(name, _('About')))
setattr(self, 'label_'+name, x)
la.linkActivated.connect(self.help_link_activated)
l.addWidget(la, count, 1, 1, 1)
count += 1
l.addItem(QSpacerItem(10, 10, vPolicy=QSizePolicy.Expanding), count, 1, 1, 2)
la = self.help_label = QLabel('')
self.help_link_activated('#polish')
la.setWordWrap(True)
la.setTextFormat(Qt.RichText)
la.setFrameShape(QFrame.StyledPanel)
la.setAlignment(Qt.AlignLeft|Qt.AlignTop)
la.setLineWidth(2)
la.setStyleSheet('QLabel { margin-left: 75px }')
l.addWidget(la, 0, 2, count+1, 1)
l.setColumnStretch(2, 1)
self.bb = bb = QDialogButtonBox(QDialogButtonBox.Ok|QDialogButtonBox.Cancel)
bb.accepted.connect(self.accept)
bb.rejected.connect(self.reject)
l.addWidget(bb, count+1, 0, 1, -1)
self.resize(QSize(800, 600))
def help_link_activated(self, link):
link = unicode(link)[1:]
self.help_label.setText(self.help_text[link])
def accept(self):
self.actions = ac = {}
something = False
for action in self.actions:
ac[action] = bool(getattr(self, 'opt_'+action).isChecked())
if ac[action]:
something = True
if not something:
return error_dialog(self, _('No actions selected'),
_('You must select at least one action, or click Cancel.'),
show=True)
self.queue_files()
return super(Polish, self).accept()
def queue_files(self):
self.tdir = PersistentTemporaryDirectory('_queue_polish')
self.jobs = []
if len(self.book_id_map) <= 5:
for i, (book_id, formats) in enumerate(self.book_id_map.iteritems()):
self.do_book(i+1, book_id, formats)
else:
self.queue = [(i+1, id_) for i, id_ in enumerate(self.book_id_map)]
self.pd = ProgressDialog(_('Queueing books for polishing'),
max=len(self.queue), parent=self)
QTimer.singleShot(0, self.do_one)
self.pd.exec_()
def do_one(self):
if not self.queue:
self.pd.accept()
return
if self.pd.canceled:
self.jobs = []
self.pd.reject()
return
num, book_id = self.queue.pop()
try:
self.do_book(num, book_id, self.book_id_map[book_id])
except:
self.pd.reject()
else:
self.pd.set_value(num)
QTimer.singleShot(0, self.do_one)
def do_book(self, num, book_id, formats):
base = os.path.join(self.tdir, unicode(book_id))
os.mkdir(base)
db = self.db()
opf = os.path.join(base, 'metadata.opf')
with open(opf, 'wb') as opf_file:
mi = create_opf_file(db, book_id, opf_file=opf_file)[0]
data = {'opf':opf, 'files':[]}
for action in self.actions:
data[action] = bool(getattr(self, 'opt_'+action).isChecked())
cover = os.path.join(base, 'cover.jpg')
if db.copy_cover_to(book_id, cover, index_is_id=True):
data['cover'] = cover
for fmt in formats:
ext = fmt.replace('ORIGINAL_', '').lower()
with open(os.path.join(base, '%s.%s'%(book_id, ext)), 'wb') as f:
db.copy_format_to(book_id, fmt, f, index_is_id=True)
data['files'].append(f.name)
desc = ngettext(_('Polish %s')%mi.title,
_('Polish book %(nums)s of %(tot)s (%(title)s)')%dict(
num=num, tot=len(self.book_id_map),
title=mi.title), len(self.book_id_map))
if hasattr(self, 'pd'):
self.pd.set_msg(_('Queueing book %(nums)s of %(tot)s (%(title)s)')%dict(
num=num, tot=len(self.book_id_map), title=mi.title))
self.jobs.append((desc, data, book_id, base))
class PolishAction(InterfaceAction):
name = 'Polish Books'
action_spec = (_('Polish books'), 'polish.png', None, _('P'))
dont_add_to = frozenset(['context-menu-device'])
action_type = 'current'
def genesis(self):
self.qaction.triggered.connect(self.polish_books)
def location_selected(self, loc):
enabled = loc == 'library'
self.qaction.setEnabled(enabled)
def get_books_for_polishing(self):
from calibre.ebooks.oeb.polish.main import SUPPORTED
rows = [r.row() for r in
self.gui.library_view.selectionModel().selectedRows()]
if not rows or len(rows) == 0:
d = error_dialog(self.gui, _('Cannot polish'),
_('No books selected'))
d.exec_()
return None
db = self.gui.library_view.model().db
ans = (db.id(r) for r in rows)
supported = set(SUPPORTED)
for x in SUPPORTED:
supported.add('ORIGINAL_'+x)
ans = [(x, set( (db.formats(x, index_is_id=True) or '').split(',') )
.intersection(supported)) for x in ans]
ans = [x for x in ans if x[1]]
if not ans:
error_dialog(self.gui, _('Cannot polish'),
_('Polishing is only supported for books in the %s'
' formats. Convert to one of those formats before polishing.')
%_(' or ').join(sorted(SUPPORTED)), show=True)
ans = OrderedDict(ans)
for fmts in ans.itervalues():
for x in SUPPORTED:
if ('ORIGINAL_'+x) in fmts:
fmts.discard(x)
return ans
def polish_books(self):
book_id_map = self.get_books_for_polishing()
if not book_id_map:
return
d = Polish(self.gui.library_view.model().db, book_id_map, parent=self.gui)
if d.exec_() == d.Accepted and d.jobs:
for desc, data, book_id, base, files in reversed(d.jobs):
job = self.gui.job_manager.run_job(
Dispatcher(self.book_polished), 'gui_polish', args=(data,),
description=desc)
job.polish_args = (book_id, base, data['files'])
def book_polished(self, job):
if job.failed:
self.gui.job_exception(job)
return
db = self.gui.current_db
book_id, base, files = job.polish_args
for path in files:
fmt = path.rpartition('.')[-1].upper()
if tweaks['save_original_format']:
db.save_original_format(book_id, fmt, notify=False)
with open(path, 'rb') as f:
db.add_format(book_id, fmt, f, index_is_id=True)
self.gui.status_bar.show_message(job.description + \
(' completed'), 2000)
try:
shutil.rmtree(base)
parent = os.path.dirname(base)
os.rmdir(parent)
except:
pass
if __name__ == '__main__':
app = QApplication([])
app
from calibre.library import db
d = Polish(db(), {1:{'EPUB'}, 2:{'AZW3'}})
d.exec_()

View File

@ -6,7 +6,7 @@ __license__ = 'GPL v3'
__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
import os, uuid, re
import os, re
from PyQt4.Qt import QPixmap, SIGNAL
@ -21,15 +21,15 @@ from calibre.utils.icu import sort_key
from calibre.library.comments import comments_to_html
from calibre.utils.config import tweaks
def create_opf_file(db, book_id):
def create_opf_file(db, book_id, opf_file=None):
mi = db.get_metadata(book_id, index_is_id=True)
mi.application_id = uuid.uuid4()
old_cover = mi.cover
mi.cover = None
mi.application_id = mi.uuid
raw = metadata_to_opf(mi)
mi.cover = old_cover
opf_file = PersistentTemporaryFile('.opf')
if opf_file is None:
opf_file = PersistentTemporaryFile('.opf')
opf_file.write(raw)
opf_file.close()
return mi, opf_file

View File

@ -7,6 +7,8 @@ __license__ = 'GPL v3'
__copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
import os.path
from PyQt4.Qt import (QDialog, QGridLayout, QIcon, QLabel, QTreeWidget,
QTreeWidgetItem, Qt, QFont, QDialogButtonBox)
@ -65,15 +67,20 @@ class DuplicatesQuestion(QDialog):
QDialog.reject(self)
def process_duplicates(self, db, duplicates):
ta = _('%(title)s by %(author)s')
ta = _('%(title)s by %(author)s [%(formats)s]')
bf = QFont(self.dup_list.font())
bf.setBold(True)
itf = QFont(self.dup_list.font())
itf.setItalic(True)
for mi, cover, formats in duplicates:
# formats is a list of file paths
# Grab just the extension and display to the user
# Based only off the file name, no file type tests are done.
incoming_formats = ', '.join(os.path.splitext(path)[-1].replace('.', '').upper() for path in formats)
item = QTreeWidgetItem([ta%dict(
title=mi.title, author=mi.format_field('authors')[1])] , 0)
title=mi.title, author=mi.format_field('authors')[1],
formats=incoming_formats)] , 0)
item.setCheckState(0, Qt.Checked)
item.setFlags(Qt.ItemIsEnabled|Qt.ItemIsUserCheckable)
item.setData(0, Qt.FontRole, bf)
@ -93,7 +100,9 @@ class DuplicatesQuestion(QDialog):
index_is_id=True) or '').split(',')]
add_child(ta%dict(
title=db.title(book_id, index_is_id=True),
author=authors_to_string(aut)))
author=authors_to_string(aut),
formats=db.formats(book_id, index_is_id=True,
verify_formats=False)))
add_child('')
yield item

View File

@ -528,11 +528,16 @@ class BooksView(QTableView): # {{{
self.was_restored = True
def refresh_row_sizing(self):
self.row_sizing_done = False
self.do_row_sizing()
def do_row_sizing(self):
# Resize all rows to have the correct height
if not self.row_sizing_done and self.model().rowCount(QModelIndex()) > 0:
self.resizeRowToContents(0)
self.verticalHeader().setDefaultSectionSize(self.rowHeight(0))
self.verticalHeader().setDefaultSectionSize(self.rowHeight(0) +
gprefs['extra_row_spacing'])
self.row_sizing_done = True
def resize_column_to_fit(self, column):

View File

@ -376,6 +376,13 @@ class Comments(QWebView): # {{{
<html>
'''%(fam, f, c)
self.setHtml(templ%html)
def sizeHint(self):
# This is needed, because on windows the dialog cannot be resized to
# so that this widgets height become < sizeHint().height(). Qt sets the
# sizeHint to (800, 600), which makes the dialog unusable on smaller
# screens.
return QSize(800, 300)
# }}}
class IdentifyWorker(Thread): # {{{
@ -977,12 +984,6 @@ class FullFetch(QDialog): # {{{
if geom is not None and geom:
self.restoreGeometry(geom)
# Workaround for Qt 4.8.0 bug that causes the frame of the window to go
# off the top of the screen if a max height is not set for the
# QWebView. Seems to only happen on windows, but keep it for all
# platforms just in case.
self.identify_widget.comments_view.setMaximumHeight(self.height()-100)
self.finished.connect(self.cleanup)
def view_log(self):

View File

@ -111,6 +111,7 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
r('cover_flow_queue_length', config, restart_required=True)
r('cover_browser_reflections', gprefs)
r('extra_row_spacing', gprefs)
def get_esc_lang(l):
if l == 'en':
@ -292,6 +293,7 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
gui.library_view.refresh_book_details()
if hasattr(gui.cover_flow, 'setShowReflections'):
gui.cover_flow.setShowReflections(gprefs['cover_browser_reflections'])
gui.library_view.refresh_row_sizing()
if __name__ == '__main__':
from calibre.gui2 import Application

View File

@ -28,16 +28,6 @@
<string>Main Interface</string>
</attribute>
<layout class="QGridLayout" name="gridLayout_9">
<item row="2" column="0">
<widget class="QLabel" name="label_7">
<property name="text">
<string>Choose &amp;language (requires restart):</string>
</property>
<property name="buddy">
<cstring>opt_language</cstring>
</property>
</widget>
</item>
<item row="2" column="1">
<widget class="QComboBox" name="opt_language">
<property name="sizeAdjustPolicy">
@ -55,6 +45,19 @@
</property>
</widget>
</item>
<item row="9" column="0">
<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>
<item row="1" column="0">
<widget class="QLabel" name="label_17">
<property name="text">
@ -105,54 +108,6 @@
</property>
</widget>
</item>
<item row="7" column="0" colspan="2">
<widget class="QGroupBox" name="groupBox_2">
<property name="title">
<string>&amp;Toolbar</string>
</property>
<layout class="QGridLayout" name="gridLayout_8">
<item row="0" column="1">
<widget class="QComboBox" name="opt_toolbar_icon_size"/>
</item>
<item row="0" column="0">
<widget class="QLabel" name="label_5">
<property name="text">
<string>&amp;Icon size:</string>
</property>
<property name="buddy">
<cstring>opt_toolbar_icon_size</cstring>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QComboBox" name="opt_toolbar_text"/>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_8">
<property name="text">
<string>Show &amp;text under icons:</string>
</property>
<property name="buddy">
<cstring>opt_toolbar_text</cstring>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item row="8" column="0">
<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>
<item row="6" column="0">
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
@ -191,6 +146,51 @@
</property>
</widget>
</item>
<item row="7" column="0" colspan="2">
<widget class="QGroupBox" name="groupBox_2">
<property name="title">
<string>&amp;Toolbar</string>
</property>
<layout class="QGridLayout" name="gridLayout_8">
<item row="0" column="1">
<widget class="QComboBox" name="opt_toolbar_icon_size"/>
</item>
<item row="0" column="0">
<widget class="QLabel" name="label_5">
<property name="text">
<string>&amp;Icon size:</string>
</property>
<property name="buddy">
<cstring>opt_toolbar_icon_size</cstring>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QComboBox" name="opt_toolbar_text"/>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_8">
<property name="text">
<string>Show &amp;text under icons:</string>
</property>
<property name="buddy">
<cstring>opt_toolbar_text</cstring>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_7">
<property name="text">
<string>Choose &amp;language (requires restart):</string>
</property>
<property name="buddy">
<cstring>opt_language</cstring>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="opt_ui_style"/>
</item>
@ -201,6 +201,26 @@
</property>
</widget>
</item>
<item row="8" column="0">
<widget class="QLabel" name="label_4">
<property name="text">
<string>Extra &amp;spacing to add between rows in the book list:</string>
</property>
<property name="buddy">
<cstring>opt_extra_row_spacing</cstring>
</property>
</widget>
</item>
<item row="8" column="1">
<widget class="QSpinBox" name="opt_extra_row_spacing">
<property name="specialValueText">
<string>None</string>
</property>
<property name="suffix">
<string> px</string>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="tab_4">

View File

@ -236,6 +236,8 @@ class EbookViewer(MainWindow, Ui_EbookViewer):
self.action_copy.triggered[bool].connect(self.copy)
self.action_font_size_larger.triggered.connect(self.font_size_larger)
self.action_font_size_smaller.triggered.connect(self.font_size_smaller)
self.action_font_size_larger.setShortcut(Qt.CTRL+Qt.Key_Equal)
self.action_font_size_smaller.setShortcut(Qt.CTRL+Qt.Key_Minus)
self.action_open_ebook.triggered[bool].connect(self.open_ebook)
self.action_next_page.triggered.connect(self.view.next_page)
self.action_previous_page.triggered.connect(self.view.previous_page)
@ -705,11 +707,13 @@ class EbookViewer(MainWindow, Ui_EbookViewer):
self.view.shrink_fonts()
def magnification_changed(self, val):
tt = _('%(which)s font size\nCurrent magnification: %(mag).1f')
tt = _('%(which)s font size [%(sc)s]\nCurrent magnification: %(mag).1f')
sc = unicode(self.action_font_size_larger.shortcut().toString())
self.action_font_size_larger.setToolTip(
tt %dict(which=_('Increase'), mag=val))
tt %dict(which=_('Increase'), mag=val, sc=sc))
sc = unicode(self.action_font_size_smaller.shortcut().toString())
self.action_font_size_smaller.setToolTip(
tt %dict(which=_('Decrease'), mag=val))
tt %dict(which=_('Decrease'), mag=val, sc=sc))
self.action_font_size_larger.setEnabled(self.view.multiplier < 3)
self.action_font_size_smaller.setEnabled(self.view.multiplier > 0.2)

View File

@ -3739,7 +3739,7 @@ books_series_link feeds
if not ext:
continue
ext = ext[1:].lower()
if ext not in BOOK_EXTENSIONS and ext != 'opf':
if ext not in BOOK_EXTENSIONS:
continue
key = os.path.splitext(path)[0]

View File

@ -498,6 +498,7 @@ class FieldMetadata(dict):
return key.startswith(self.custom_field_prefix)
def is_ignorable_field(self, key):
'Custom fields and user categories are ignorable'
return self.is_custom_field(key) or key.startswith('@')
def ignorable_field_keys(self):

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

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