mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-08-11 09:13:57 -04:00
0.9.18
This commit is contained in:
commit
460c8a77ce
@ -19,6 +19,56 @@
|
||||
# new recipes:
|
||||
# - title:
|
||||
|
||||
- version: 0.9.18
|
||||
date: 2013-02-08
|
||||
|
||||
new features:
|
||||
- title: "New metadata source: Edelweiss, a catalog of books that is updated directly by publishers. To enable it, go to Preferences->Metadata download and enable the Edelweiss plugin."
|
||||
tickets: [1091073]
|
||||
|
||||
- title: "Add an option to add extra spacing between rows in the book list. (Preferences->Look & Feel)"
|
||||
tickets: [1117907]
|
||||
|
||||
- title: "Column coloring/icons: Add a 'days ago' condition, useable with columns that store dates to set colors/icons based on the number of days before today"
|
||||
|
||||
- title: "E-book viewer: Add shortcuts Ctrl+= and Ctrl+- to increase/decrease text size."
|
||||
tickets: [ 1117524 ]
|
||||
|
||||
- title: "When showing possible duplicates after adding books, also show the file formats."
|
||||
|
||||
- title: "Driver for Trekstor Ventos Tablet"
|
||||
|
||||
bug fixes:
|
||||
- title: "Conversion: When transliterating unicode characters, handle « and » correctly."
|
||||
tickets: [1117270]
|
||||
|
||||
- title: "Fix adding books from multiple directories with multiple books per directory treating opf files as an ebook"
|
||||
|
||||
- title: "Fix download metadata window not resizable on smaller screens"
|
||||
tickets: [1116849]
|
||||
|
||||
- title: "Tweak Book: When rebuilding azw3 files handle <a> tags that have name but not id attribute, these are apparently produced by kindlegen."
|
||||
tickets: [ 1112934 ]
|
||||
|
||||
- title: "Fix regression in advanced column color rules."
|
||||
tickets: [1118678]
|
||||
|
||||
improved recipes:
|
||||
- El Mundo today
|
||||
- fluter.de
|
||||
- Birmingham Post
|
||||
- Japan Times
|
||||
- The Toronto Star
|
||||
- Le Monde (subscription version)
|
||||
- Globe and Mail
|
||||
|
||||
new recipes:
|
||||
- title: VICE Magazine Deutschland
|
||||
author: Alex
|
||||
|
||||
- title: Libertad Digital
|
||||
author: Darko Miletic
|
||||
|
||||
- version: 0.9.17
|
||||
date: 2013-02-01
|
||||
|
||||
|
366
imgsrc/polish.svg
Normal file
366
imgsrc/polish.svg
Normal 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 |
@ -663,7 +663,7 @@ Post any output you see in a help message on the `Forum <http://www.mobileread.c
|
||||
|app| freezes/crashes occasionally?
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
There are three possible things I know of, that can cause this:
|
||||
There are five possible things I know of, that can cause this:
|
||||
|
||||
* You recently connected an external monitor or TV to your computer. In
|
||||
this case, whenever |app| opens a new window like the edit metadata
|
||||
@ -671,7 +671,7 @@ There are three 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.
|
||||
|
||||
|
@ -14,7 +14,7 @@ class AdvancedUserRecipe1306097511(BasicNewsRecipe):
|
||||
remove_empty_feeds = True
|
||||
remove_javascript = True
|
||||
no_stylesheets = True
|
||||
#auto_cleanup = True
|
||||
auto_cleanup = True
|
||||
language = 'en_GB'
|
||||
|
||||
cover_url = 'http://profile.ak.fbcdn.net/hprofile-ak-snc4/161987_9010212100_2035706408_n.jpg'
|
||||
@ -23,7 +23,7 @@ class AdvancedUserRecipe1306097511(BasicNewsRecipe):
|
||||
def get_cover_url(self):
|
||||
soup = self.index_to_soup('http://www.birminghampost.net')
|
||||
# look for the block containing the sun button and url
|
||||
cov = soup.find(attrs={'height' : re.compile('3'), 'alt' : re.compile('Birmingham Post')})
|
||||
cov = soup.find(attrs={'height' : re.compile('3'), 'alt' : re.compile('Post')})
|
||||
print
|
||||
print '%%%%%%%%%%%%%%%',cov
|
||||
print
|
||||
@ -43,20 +43,7 @@ class AdvancedUserRecipe1306097511(BasicNewsRecipe):
|
||||
return cover_url
|
||||
|
||||
|
||||
keep_only_tags = [
|
||||
dict(attrs={'id' : 'article-header'}),
|
||||
#dict(name='h1',attrs={'id' : 'article-header'}),
|
||||
dict(attrs={'class':['article-meta-author','article-meta-date','article main','art-o art-align-center otm-1 ']}),
|
||||
dict(name='div',attrs={'class' : 'article-image full'}),
|
||||
dict(attrs={'clas' : 'art-o art-align-center otm-1 '}),
|
||||
dict(name='div',attrs={'class' : 'article main'}),
|
||||
#dict(name='p')
|
||||
#dict(attrs={'id' : 'three-col'})
|
||||
]
|
||||
remove_tags = [
|
||||
# dict(name='div',attrs={'class' : 'span-33 last header-links'})
|
||||
|
||||
]
|
||||
feeds = [
|
||||
#(u'News',u'http://www.birminghampost.net/news/rss.xml'),
|
||||
(u'West Mids. News', u'http://www.birminghampost.net/news/west-midlands-news/rss.xml'),
|
||||
@ -65,9 +52,3 @@ class AdvancedUserRecipe1306097511(BasicNewsRecipe):
|
||||
(u'Bloggs & Comments',u'http://www.birminghampost.net/comment/rss.xml')
|
||||
|
||||
]
|
||||
extra_css = '''
|
||||
h1{font-family:Arial,Helvetica,sans-serif; font-weight:bold;font-size:large;text-align:center;}
|
||||
h2{font-family:Arial,Helvetica,sans-serif; font-weight:normal;font-size:small;}
|
||||
p{font-family:Arial,Helvetica,sans-serif;font-size:small;}
|
||||
body{font-family:Helvetica,Arial,sans-serif;font-size:small;}
|
||||
'''
|
||||
|
@ -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
|
||||
|
@ -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'
|
||||
|
||||
|
@ -62,7 +62,8 @@ class HBR(BasicNewsRecipe):
|
||||
|
||||
today = date.today()
|
||||
future = today + timedelta(days=30)
|
||||
for x in [x.strftime('%y%m') for x in (future, today)]:
|
||||
past = today - timedelta(days=30)
|
||||
for x in [x.strftime('%y%m') for x in (future, today, past)]:
|
||||
url = self.INDEX + x
|
||||
soup = self.index_to_soup(url)
|
||||
if (not soup.find(text='Issue Not Found') and not soup.find(
|
||||
|
@ -1,5 +1,5 @@
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2008-2011, Darko Miletic <darko.miletic at gmail.com>'
|
||||
__copyright__ = '2008-2013, Darko Miletic <darko.miletic at gmail.com>'
|
||||
'''
|
||||
japantimes.co.jp
|
||||
'''
|
||||
@ -13,59 +13,41 @@ class JapanTimes(BasicNewsRecipe):
|
||||
language = 'en_JP'
|
||||
category = 'news, politics, japan'
|
||||
publisher = 'The Japan Times'
|
||||
oldest_article = 5
|
||||
oldest_article = 2
|
||||
max_articles_per_feed = 150
|
||||
no_stylesheets = True
|
||||
use_embedded_content = False
|
||||
encoding = 'utf8'
|
||||
publication_type = 'newspaper'
|
||||
masthead_url = 'http://search.japantimes.co.jp/images/header_title.gif'
|
||||
masthead_url = 'http://www.japantimes.co.jp/wp-content/themes/jt_theme/library/img/logo-japan-times.png'
|
||||
extra_css = 'body{font-family: Geneva,Arial,Helvetica,sans-serif}'
|
||||
|
||||
conversion_options = {
|
||||
'comment' : description
|
||||
, 'tags' : category
|
||||
, 'publisher' : publisher
|
||||
, 'language' : language
|
||||
, 'linearize_tables' : True
|
||||
'comment' : description
|
||||
, 'tags' : category
|
||||
, 'publisher' : publisher
|
||||
, 'language' : language
|
||||
}
|
||||
|
||||
|
||||
keep_only_tags = [dict(name='div', attrs={'id':'printresult'})]
|
||||
remove_tags = [
|
||||
dict(name=['iframe','meta','link','embed','object','base'])
|
||||
,dict(attrs={'id':'searchfooter'})
|
||||
]
|
||||
feeds = [(u'The Japan Times', u'http://feeds.feedburner.com/japantimes')]
|
||||
remove_attributes = ['border']
|
||||
remove_tags_after = dict(name='div', attrs={'class':'entry'})
|
||||
keep_only_tags = [dict(name='div', attrs={'class':'padding_block'})]
|
||||
remove_tags = [
|
||||
dict(name=['iframe','embed','object','base'])
|
||||
,dict(attrs={'class':['meta_extras','related_articles']})
|
||||
,dict(attrs={'id':'content_footer_menu'})
|
||||
]
|
||||
feeds = [
|
||||
(u'News' , u'http://www.japantimes.co.jp/news/feed/' )
|
||||
,(u'Opinion' , u'http://www.japantimes.co.jp/opinion/feed/' )
|
||||
,(u'Life' , u'http://www.japantimes.co.jp/opinion/feed/' )
|
||||
,(u'Community', u'http://www.japantimes.co.jp/community/feed/')
|
||||
,(u'Culture' , u'http://www.japantimes.co.jp/culture/feed/' )
|
||||
,(u'Sports' , u'http://www.japantimes.co.jp/sports/feed/' )
|
||||
]
|
||||
|
||||
def get_article_url(self, article):
|
||||
rurl = BasicNewsRecipe.get_article_url(self, article)
|
||||
return rurl.partition('?')[0]
|
||||
|
||||
def print_version(self, url):
|
||||
if '/rss/' in url:
|
||||
return url.replace('.jp/rss/','.jp/print/')
|
||||
if '/text/' in url:
|
||||
return url.replace('.jp/text/','.jp/print/')
|
||||
return url
|
||||
|
||||
def preprocess_html(self, soup):
|
||||
for item in soup.findAll(style=True):
|
||||
del item['style']
|
||||
for item in soup.findAll('img'):
|
||||
if not item.has_key('alt'):
|
||||
item['alt'] = 'image'
|
||||
for item in soup.findAll('photo'):
|
||||
item.name = 'div'
|
||||
for item in soup.head.findAll('paragraph'):
|
||||
item.extract()
|
||||
for item in soup.findAll('wwfilename'):
|
||||
item.extract()
|
||||
for item in soup.findAll('jtcategory'):
|
||||
item.extract()
|
||||
for item in soup.findAll('nomooter'):
|
||||
item.extract()
|
||||
for item in soup.body.findAll('paragraph'):
|
||||
item.name = 'p'
|
||||
return soup
|
||||
def preprocess_raw_html(self, raw, url):
|
||||
return '<html><head>'+raw[raw.find('</head>'):]
|
||||
|
@ -1,15 +1,16 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2012, Rémi Vanicat <vanicat at debian.org>'
|
||||
__copyright__ = '2012, 2013, Rémi Vanicat <vanicat at debian.org>'
|
||||
'''
|
||||
Lemonde.fr: Version abonnée
|
||||
'''
|
||||
|
||||
|
||||
import os, zipfile, re, time
|
||||
from urllib2 import HTTPError
|
||||
from calibre.constants import preferred_encoding
|
||||
|
||||
from calibre import strftime
|
||||
from calibre.web.feeds.news import BasicNewsRecipe
|
||||
from calibre.ebooks.BeautifulSoup import BeautifulSoup
|
||||
from calibre.ptempfile import PersistentTemporaryFile
|
||||
@ -20,28 +21,38 @@ class LeMondeAbonne(BasicNewsRecipe):
|
||||
__author__ = u'Rémi Vanicat'
|
||||
description = u'Actualités'
|
||||
category = u'Actualités, France, Monde'
|
||||
publisher = 'Le Monde'
|
||||
language = 'fr'
|
||||
needs_subscription = True
|
||||
no_stylesheets = True
|
||||
smarten_punctuation = True
|
||||
remove_attributes = [ 'border', 'cellspacing', 'display', 'align', 'cellpadding', 'colspan', 'valign', 'vscape', 'hspace', 'alt', 'width', 'height']
|
||||
extra_css = ''' li{margin:6pt 0}
|
||||
ul{margin:0}
|
||||
|
||||
no_stylesheets = True
|
||||
div.photo img{max-width:100%; border:0px transparent solid;}
|
||||
div.photo{font-family:inherit; color:#333; text-align:center;}
|
||||
div.photo p{text-align:justify;font-size:.9em; line-height:.9em;}
|
||||
|
||||
extra_css = u'''
|
||||
h1{font-size:130%;}
|
||||
.ariane{font-size:xx-small;}
|
||||
.source{font-size:xx-small;}
|
||||
.href{font-size:xx-small;}
|
||||
.LM_caption{color:#666666; font-size:x-small;}
|
||||
.main-article-info{font-family:Arial,Helvetica,sans-serif;}
|
||||
#full-contents{font-size:small; font-family:Arial,Helvetica,sans-serif;font-weight:normal;}
|
||||
#match-stats-summary{font-size:small; font-family:Arial,Helvetica,sans-serif;font-weight:normal;}
|
||||
'''
|
||||
@page{margin:10pt}
|
||||
.ar-txt {color:#000; text-align:justify;}
|
||||
h1{text-align:left; font-size:1.25em;}
|
||||
|
||||
.auteur{text-align:right; font-weight:bold}
|
||||
.feed{text-align:right; font-weight:bold}
|
||||
.po-ti2{font-weight:bold}
|
||||
.fen-tt{font-weight:bold;font-size:1.1em}
|
||||
'''
|
||||
|
||||
zipurl_format = 'http://medias.lemonde.fr/abonnes/editionelectronique/%Y%m%d/html/%y%m%d.zip'
|
||||
coverurl_format = '/img/%y%m%d01.jpg'
|
||||
path_format = "%y%m%d"
|
||||
login_url = 'http://www.lemonde.fr/web/journal_electronique/identification/1,56-0,45-0,0.html'
|
||||
|
||||
keep_only_tags = [ dict(name="div", attrs={ 'class': 'po-prti' }), dict(name=['h1']), dict(name='div', attrs={ 'class': 'photo' }), dict(name='div', attrs={ 'class': 'po-ti2' }), dict(name='div', attrs={ 'class': 'ar-txt' }), dict(name='div', attrs={ 'class': 'po_rtcol' }) ]
|
||||
keep_only_tags = [dict(name=['h1']), dict(name='div', attrs={ 'class': 'photo' }), dict(name='div', attrs={ 'class': 'po-ti2' }), dict(name='div', attrs={ 'class': 'ar-txt' }), dict(name='div', attrs={ 'class': 'po_rtcol' }) ]
|
||||
|
||||
|
||||
remove_tags = [ dict(name='div', attrs={ 'class': 'po-ti' }),dict(name='div', attrs={ 'class': 'po-copy' })]
|
||||
|
||||
article_id_pattern = re.compile("[0-9]+\\.html")
|
||||
article_url_format = 'http://www.lemonde.fr/journalelectronique/donnees/protege/%Y%m%d/html/'
|
||||
@ -67,12 +78,16 @@ class LeMondeAbonne(BasicNewsRecipe):
|
||||
|
||||
second = time.time()
|
||||
second += self.decalage
|
||||
ltime = self.ltime = time.gmtime(second)
|
||||
url = time.strftime(self.zipurl_format, ltime)
|
||||
|
||||
self.timefmt=strftime(" %A %d %B %Y", ltime)
|
||||
|
||||
response = browser.open(url)
|
||||
for i in range(7):
|
||||
self.ltime = time.gmtime(second)
|
||||
self.timefmt=time.strftime(" %A %d %B %Y",self.ltime).decode(preferred_encoding)
|
||||
url = time.strftime(self.zipurl_format,self.ltime)
|
||||
try:
|
||||
response = browser.open(url)
|
||||
continue
|
||||
except HTTPError:
|
||||
second -= 24*60*60
|
||||
|
||||
tmp = PersistentTemporaryFile(suffix='.zip')
|
||||
self.report_progress(0.1,_('downloading zip file'))
|
||||
@ -85,7 +100,7 @@ class LeMondeAbonne(BasicNewsRecipe):
|
||||
zfile.extractall(self.output_dir)
|
||||
zfile.close()
|
||||
|
||||
path = os.path.join(self.output_dir, time.strftime(self.path_format, ltime), "data")
|
||||
path = os.path.join(self.output_dir, time.strftime(self.path_format, self.ltime), "data")
|
||||
|
||||
self.articles_path = path
|
||||
|
||||
@ -95,13 +110,33 @@ class LeMondeAbonne(BasicNewsRecipe):
|
||||
|
||||
flux = []
|
||||
|
||||
article_url = time.strftime(self.article_url_format, ltime)
|
||||
article_url = time.strftime(self.article_url_format, self.ltime)
|
||||
|
||||
for i in range(nb_index_files):
|
||||
filename = os.path.join(path, "selection_%d.html" % (i + 1))
|
||||
tmp = open(filename,'r')
|
||||
soup=BeautifulSoup(tmp)
|
||||
soup=BeautifulSoup(tmp,convertEntities=BeautifulSoup.HTML_ENTITIES)
|
||||
title=soup.find('span').contents[0]
|
||||
if title=="Une":
|
||||
title="À la une"
|
||||
if title=="Evenement":
|
||||
title="L'événement"
|
||||
if title=="Planete":
|
||||
title="Planète"
|
||||
if title=="Economie - Entreprises":
|
||||
title="Économie"
|
||||
if title=="L'Oeil du Monde":
|
||||
title="L'œil du Monde"
|
||||
if title=="Enquete":
|
||||
title="Enquête"
|
||||
if title=="Editorial - Analyses":
|
||||
title="Analyses"
|
||||
if title=="Le Monde Economie":
|
||||
title="Économie"
|
||||
if title=="Le Monde Culture et idées":
|
||||
title="Idées"
|
||||
if title=="Le Monde Géo et politique":
|
||||
title="Géopolitique"
|
||||
tmp.close()
|
||||
|
||||
filename = os.path.join(path, "frame_gauche_%d.html" % (i + 1))
|
||||
@ -114,7 +149,7 @@ class LeMondeAbonne(BasicNewsRecipe):
|
||||
article = {
|
||||
'title': link.contents[0],
|
||||
'url': article_url + article_id,
|
||||
'descripion': '',
|
||||
'description': '',
|
||||
'content': ''
|
||||
}
|
||||
articles.append(article)
|
||||
@ -129,4 +164,3 @@ class LeMondeAbonne(BasicNewsRecipe):
|
||||
# Local Variables:
|
||||
# mode: python
|
||||
# End:
|
||||
|
||||
|
@ -1,7 +1,5 @@
|
||||
#!/usr/bin/env python
|
||||
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2009, Darko Miletic <darko.miletic at gmail.com>'
|
||||
__copyright__ = '2009-2013, Darko Miletic <darko.miletic at gmail.com>'
|
||||
'''
|
||||
www.thestar.com
|
||||
'''
|
||||
@ -11,18 +9,17 @@ from calibre.web.feeds.news import BasicNewsRecipe
|
||||
class TheTorontoStar(BasicNewsRecipe):
|
||||
title = 'The Toronto Star'
|
||||
__author__ = 'Darko Miletic'
|
||||
description = "Canada's largest daily newspaper"
|
||||
description = "Thestar.com is Canada's largest online news site. Stay current with our sports, business entertainment news and more from the Toronto Star and thestar.com"
|
||||
oldest_article = 2
|
||||
language = 'en_CA'
|
||||
max_articles_per_feed = 100
|
||||
no_stylesheets = True
|
||||
#auto_cleanup = True
|
||||
#auto_cleanup_keep = '//div[@class="topsContent topsContentActive"]'
|
||||
use_embedded_content = False
|
||||
delay = 2
|
||||
publisher = 'The Toronto Star'
|
||||
category = "Toronto Star,Canada's largest daily newspaper,breaking news,classifieds,careers,GTA,Toronto Maple Leafs,sports,Toronto,news,editorial,The Star,Ontario,information,columnists,business,entertainment,births,deaths,automotive,rentals,weather,archives,Torstar,technology,Joseph Atkinson"
|
||||
encoding = 'utf-8'
|
||||
masthead_url = 'http://www.thestar.com/etc/designs/thestar/images/general/logoLrg.png'
|
||||
|
||||
conversion_options = {
|
||||
'comments' : description
|
||||
@ -30,23 +27,18 @@ class TheTorontoStar(BasicNewsRecipe):
|
||||
,'publisher' : publisher
|
||||
}
|
||||
|
||||
#keep_only_tags = [dict(name='div', attrs={'class':'ts-article'})]
|
||||
#remove_tags_before = dict(name='div',attrs={'id':'ts-article_header'})
|
||||
remove_tags_before = dict(name='div',attrs={'class':'article-headline'})
|
||||
|
||||
feeds = [
|
||||
(u'News' , u'http://www.thestar.com/rss/?categories=293' )
|
||||
,(u'Opinion' , u'http://www.thestar.com/rss/?categories=303' )
|
||||
,(u'Business' , u'http://www.thestar.com/rss/?categories=294' )
|
||||
,(u'Sports' , u'http://www.thestar.com/rss/?categories=295' )
|
||||
,(u'Entertainment', u'http://www.toronto.com/rss?categories=6298' )
|
||||
,(u'Living' , u'http://www.thestar.com/rss/?categories=297' )
|
||||
,(u'Travel' , u'http://www.thestar.com/rss/list/1042246?' )
|
||||
,(u'Science' , u'http://www.thestar.com/rss?categories=6481')
|
||||
(u'News' , u'http://www.thestar.com/feeds.articles.news.rss' )
|
||||
,(u'Opinion' , u'http://www.thestar.com/feeds.articles.opinion.rss' )
|
||||
,(u'Business' , u'http://www.thestar.com/feeds.articles.business.rss' )
|
||||
,(u'Sports' , u'http://www.thestar.com/feeds.articles.sports.rss' )
|
||||
,(u'Entertainment', u'http://www.thestar.com/feeds.articles.entertainment.rss' )
|
||||
,(u'Living' , u'http://www.thestar.com/feeds.articles.life.rss' )
|
||||
,(u'Travel' , u'http://www.thestar.com/feeds.articles.life.travel.rss' )
|
||||
,(u'Technology' , u'http://www.thestar.com/feeds.articles.life.technology.rss')
|
||||
]
|
||||
|
||||
def print_version(self, url):
|
||||
artl = url.rpartition('--')[0]
|
||||
artid = artl.rpartition('/')[2]
|
||||
return 'http://www.thestar.com/printarticle/' + artid
|
||||
|
||||
|
||||
return url.replace('.html', '.print.html')
|
||||
|
40
recipes/vice_magazine_de.recipe
Normal file
40
recipes/vice_magazine_de.recipe
Normal 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')]
|
Binary file not shown.
BIN
resources/images/polish.png
Normal file
BIN
resources/images/polish.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 11 KiB |
@ -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"
|
||||
|
@ -26,7 +26,7 @@ def get_opts_from_parser(parser):
|
||||
class Coffee(Command): # {{{
|
||||
|
||||
description = 'Compile coffeescript files into javascript'
|
||||
COFFEE_DIRS = ('ebooks/oeb/display',)
|
||||
COFFEE_DIRS = ('ebooks/oeb/display', 'ebooks/oeb/polish')
|
||||
|
||||
def add_options(self, parser):
|
||||
parser.add_option('--watch', '-w', action='store_true', default=False,
|
||||
|
@ -4,7 +4,7 @@ __license__ = 'GPL v3'
|
||||
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
__appname__ = u'calibre'
|
||||
numeric_version = (0, 9, 17)
|
||||
numeric_version = (0, 9, 18)
|
||||
__version__ = u'.'.join(map(unicode, numeric_version))
|
||||
__author__ = u"Kovid Goyal <kovid@kovidgoyal.net>"
|
||||
|
||||
|
@ -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,
|
||||
|
@ -452,6 +452,13 @@ class SamsungGalaxy(TabletOutput):
|
||||
'a resolution of 600x1280')
|
||||
screen_size = comic_screen_size = (600, 1280)
|
||||
|
||||
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 1280x1920')
|
||||
screen_size = comic_screen_size = (1280, 1920)
|
||||
|
||||
class SonyReaderOutput(OutputProfile):
|
||||
|
||||
name = 'Sony Reader'
|
||||
@ -786,7 +793,7 @@ output_profiles = [OutputProfile, SonyReaderOutput, SonyReader300Output,
|
||||
SonyReader900Output, MSReaderOutput, MobipocketOutput, HanlinV3Output,
|
||||
HanlinV5Output, CybookG3Output, CybookOpusOutput, KindleOutput,
|
||||
iPadOutput, iPad3Output, KoboReaderOutput, TabletOutput, SamsungGalaxy,
|
||||
SonyReaderLandscapeOutput, KindleDXOutput, IlliadOutput,
|
||||
SonyReaderLandscapeOutput, KindleDXOutput, IlliadOutput, NookHD,
|
||||
IRexDR1000Output, IRexDR800Output, JetBook5Output, NookOutput,
|
||||
BambookOutput, NookColorOutput, PocketBook900Output, PocketBookPro912Output,
|
||||
GenericEink, GenericEinkLarge, KindleFireOutput, KindlePaperWhiteOutput]
|
||||
|
@ -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):
|
||||
|
@ -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):
|
||||
|
||||
@ -54,6 +54,9 @@ class ANDROID(USBMS):
|
||||
# Eken
|
||||
0x040d : { 0x8510 : [0x0001], 0x0851 : [0x1] },
|
||||
|
||||
# Trekstor
|
||||
0x1e68 : { 0x006a : [0x0231] },
|
||||
|
||||
# Motorola
|
||||
0x22b8 : { 0x41d9 : [0x216], 0x2d61 : [0x100], 0x2d67 : [0x100],
|
||||
0x2de8 : [0x229],
|
||||
@ -215,8 +218,8 @@ class ANDROID(USBMS):
|
||||
'POCKET', 'ONDA_MID', 'ZENITHIN', 'INGENIC', 'PMID701C', 'PD',
|
||||
'PMP5097C', 'MASS', 'NOVO7', 'ZEKI', 'COBY', 'SXZ', 'USB_2.0',
|
||||
'COBY_MID', 'VS', 'AINOL', 'TOPWISE', 'PAD703', 'NEXT8D12',
|
||||
'MEDIATEK', 'KEENHI', 'TECLAST']
|
||||
WINDOWS_MAIN_MEM = ['ANDROID_PHONE', 'A855', 'A853', 'INC.NEXUS_ONE',
|
||||
'MEDIATEK', 'KEENHI', 'TECLAST', 'SURFTAB']
|
||||
WINDOWS_MAIN_MEM = ['ANDROID_PHONE', 'A855', 'A853', 'A953', 'INC.NEXUS_ONE',
|
||||
'__UMS_COMPOSITE', '_MB200', 'MASS_STORAGE', '_-_CARD', 'SGH-I897',
|
||||
'GT-I9000', 'FILE-STOR_GADGET', 'SGH-T959_CARD', 'SGH-T959', 'SAMSUNG_ANDROID',
|
||||
'SCH-I500_CARD', 'SPH-D700_CARD', 'MB810', 'GT-P1000', 'DESIRE',
|
||||
@ -236,7 +239,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', 'ST80208-1']
|
||||
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',
|
||||
@ -247,7 +250,7 @@ class ANDROID(USBMS):
|
||||
'FILE-CD_GADGET', 'GT-I9001_CARD', 'USB_2.0', 'XT875',
|
||||
'UMS_COMPOSITE', 'PRO', '.KOBO_VOX', 'SGH-T989_CARD', 'SGH-I727',
|
||||
'USB_FLASH_DRIVER', 'ANDROID', 'MID7042', '7035', 'VIEWPAD_7E',
|
||||
'NOVO7', 'ADVANCED', 'TABLET_PC', 'F', 'E400_SD_CARD']
|
||||
'NOVO7', 'ADVANCED', 'TABLET_PC', 'F', 'E400_SD_CARD', 'ST80208-1']
|
||||
|
||||
OSX_MAIN_MEM = 'Android Device Main Memory'
|
||||
|
||||
|
@ -54,6 +54,8 @@ def synchronous(tlockname):
|
||||
|
||||
class ConnectionListener (Thread):
|
||||
|
||||
all_ip_addresses = dict()
|
||||
|
||||
NOT_SERVICED_COUNT = 6
|
||||
|
||||
def __init__(self, driver):
|
||||
@ -61,6 +63,7 @@ class ConnectionListener (Thread):
|
||||
self.daemon = True
|
||||
self.driver = driver
|
||||
self.keep_running = True
|
||||
all_ip_addresses = dict()
|
||||
|
||||
def stop(self):
|
||||
self.keep_running = False
|
||||
@ -68,6 +71,8 @@ class ConnectionListener (Thread):
|
||||
def run(self):
|
||||
queue_not_serviced_count = 0
|
||||
device_socket = None
|
||||
get_all_ips(reinitialize=True)
|
||||
|
||||
while self.keep_running:
|
||||
try:
|
||||
time.sleep(1)
|
||||
@ -78,6 +83,11 @@ class ConnectionListener (Thread):
|
||||
if not self.keep_running:
|
||||
break
|
||||
|
||||
if not self.all_ip_addresses:
|
||||
self.all_ip_addresses = get_all_ips()
|
||||
if self.all_ip_addresses:
|
||||
self.driver._debug("All IP addresses", self.all_ip_addresses)
|
||||
|
||||
if not self.driver.connection_queue.empty():
|
||||
queue_not_serviced_count += 1
|
||||
if queue_not_serviced_count >= self.NOT_SERVICED_COUNT:
|
||||
@ -1287,8 +1297,6 @@ class SMART_DEVICE_APP(DeviceConfig, DevicePlugin):
|
||||
self.client_can_stream_metadata = False
|
||||
self.client_wants_uuid_file_names = False
|
||||
|
||||
self._debug("All IP addresses", get_all_ips())
|
||||
|
||||
message = None
|
||||
try:
|
||||
self.listen_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
|
@ -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'<'*3, u'»':u'>'*3})
|
||||
html = mr.mreplace(html)
|
||||
html = unihandecoder.decode(html)
|
||||
|
||||
if getattr(self.extra_opts, 'enable_heuristics', False):
|
||||
|
395
src/calibre/ebooks/metadata/sources/edelweiss.py
Normal file
395
src/calibre/ebooks/metadata/sources/edelweiss.py
Normal 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)
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -241,6 +241,11 @@ class KF8Writer(object):
|
||||
j = 0
|
||||
for tag in root.iterdescendants(etree.Element):
|
||||
id_ = tag.attrib.get('id', None)
|
||||
if id_ is None and tag.tag == XHTML('a'):
|
||||
# Can happen during tweaking
|
||||
id_ = tag.attrib.get('name', None)
|
||||
if id_ is not None:
|
||||
tag.attrib['id'] = id_
|
||||
if id_ is not None or barename(tag.tag).lower() in aid_able_tags:
|
||||
aid = aidbase + j
|
||||
tag.attrib['aid'] = to_base(aid, base=32)
|
||||
|
@ -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
|
||||
|
@ -7,23 +7,26 @@ __license__ = 'GPL v3'
|
||||
__copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
import os, posixpath, logging, sys, hashlib, uuid
|
||||
import os, logging, sys, hashlib, uuid
|
||||
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
|
||||
@ -56,7 +60,7 @@ class Container(object):
|
||||
# Special case if we have stumbled onto the opf
|
||||
if path == opfpath:
|
||||
self.opf_name = name
|
||||
self.opf_dir = posixpath.dirname(path)
|
||||
self.opf_dir = os.path.dirname(path)
|
||||
self.mime_map[name] = guess_type('a.opf')[0]
|
||||
|
||||
# Update mime map with data from the OPF
|
||||
@ -66,13 +70,25 @@ class Container(object):
|
||||
href = item.get('href')
|
||||
self.mime_map[self.href_to_name(href)] = item.get('media-type')
|
||||
|
||||
def abspath_to_name(self, fullpath):
|
||||
return self.relpath(os.path.abspath(fullpath)).replace(os.sep, '/')
|
||||
|
||||
def href_to_name(self, href, base=None):
|
||||
'''
|
||||
Convert an href (relative to base) to a name (i.e. a path
|
||||
relative to self.root with POSIX separators).
|
||||
|
||||
base must be an absolute path with OS separators or None, in which case
|
||||
the href is interpreted relative to the dir containing the OPF.
|
||||
'''
|
||||
if base is None:
|
||||
base = self.opf_dir
|
||||
href = urlunquote(href.partition('#')[0])
|
||||
fullpath = posixpath.abspath(posixpath.join(base, href))
|
||||
return self.relpath(fullpath)
|
||||
fullpath = os.path.join(base, *href.split('/'))
|
||||
return self.abspath_to_name(fullpath)
|
||||
|
||||
def has_name(self, name):
|
||||
return name in self.name_path_map
|
||||
|
||||
def relpath(self, path):
|
||||
return relpath(path, self.root)
|
||||
@ -129,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):
|
||||
@ -177,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
|
||||
|
||||
@ -282,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
|
||||
|
||||
@ -345,10 +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}
|
||||
|
||||
if __name__ == '__main__':
|
||||
f = sys.argv[-1]
|
||||
ebook = (AZW3Container if f.rpartition('.')[-1].lower() in {'azw3', 'mobi'}
|
||||
else EpubContainer)(f, default_log)
|
||||
for s in ebook.spine_items:
|
||||
print (ebook.relpath(s))
|
||||
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
|
||||
|
||||
def test_roundtrip():
|
||||
ebook = get_container(sys.argv[-1])
|
||||
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()
|
||||
|
||||
|
||||
|
72
src/calibre/ebooks/oeb/polish/font_stats.coffee
Normal file
72
src/calibre/ebooks/oeb/polish/font_stats.coffee
Normal file
@ -0,0 +1,72 @@
|
||||
#!/usr/bin/env coffee
|
||||
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
|
||||
|
||||
###
|
||||
Copyright 2013, Kovid Goyal <kovid at kovidgoyal.net>
|
||||
Released under the GPLv3 License
|
||||
###
|
||||
|
||||
|
||||
if window?.calibre_utils
|
||||
log = window.calibre_utils.log
|
||||
|
||||
font_dict = (style, computed=false) ->
|
||||
if computed
|
||||
fams = []
|
||||
family = style.getPropertyCSSValue('font-family')
|
||||
if family.cssValueType == CSSValue.CSS_PRIMITIVE_VALUE
|
||||
fams.push(family.getStringValue())
|
||||
else
|
||||
for f in family
|
||||
fams.push(f.getStringValue())
|
||||
else
|
||||
fams = style.getPropertyValue('font-family')
|
||||
return {
|
||||
'font-family':fams,
|
||||
'font-weight':style.getPropertyValue('font-weight'),
|
||||
'font-style':style.getPropertyValue('font-style'),
|
||||
'font-stretch':style.getPropertyValue('font-stretch'),
|
||||
}
|
||||
|
||||
font_usage = (node) ->
|
||||
style = window.getComputedStyle(node, null)
|
||||
ans = font_dict(style, true)
|
||||
text = []
|
||||
for child in node.childNodes
|
||||
if child.nodeType == Node.TEXT_NODE
|
||||
text.push(child.nodeValue)
|
||||
ans['text'] = text
|
||||
return ans
|
||||
|
||||
class FontStats
|
||||
# This class is a namespace to expose functions via the
|
||||
# window.font_stats object.
|
||||
|
||||
constructor: () ->
|
||||
if not this instanceof arguments.callee
|
||||
throw new Error('FontStats constructor called as function')
|
||||
|
||||
get_font_face_rules: () ->
|
||||
font_faces = []
|
||||
for sheet in document.styleSheets
|
||||
for rule in sheet.cssRules
|
||||
if rule.type == rule.FONT_FACE_RULE
|
||||
fd = font_dict(rule.style)
|
||||
fd['src'] = rule.style.getPropertyValue('src')
|
||||
font_faces.push(fd)
|
||||
py_bridge.value = font_faces
|
||||
|
||||
get_font_usage: () ->
|
||||
ans = []
|
||||
busage = font_usage(document.body)
|
||||
if busage != null
|
||||
ans.push(busage)
|
||||
for node in document.body.getElementsByTagName('*')
|
||||
usage = font_usage(node)
|
||||
if usage != null
|
||||
ans.push(usage)
|
||||
py_bridge.value = ans
|
||||
|
||||
if window?
|
||||
window.font_stats = new FontStats()
|
||||
|
160
src/calibre/ebooks/oeb/polish/main.py
Normal file
160
src/calibre/ebooks/oeb/polish/main.py
Normal 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()
|
||||
|
@ -7,19 +7,107 @@ __license__ = 'GPL v3'
|
||||
__copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
import json
|
||||
import json, sys, os
|
||||
from urllib import unquote
|
||||
|
||||
from PyQt4.Qt import (QWebPage, pyqtProperty, QString, QEventLoop, QWebView,
|
||||
Qt, QSize, QTimer)
|
||||
from cssutils import parseStyle
|
||||
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
|
||||
from calibre.gui2 import must_use_qt
|
||||
|
||||
class Page(QWebPage):
|
||||
def normalize_font_properties(font):
|
||||
w = font.get('font-weight', None)
|
||||
if not w and w != 0:
|
||||
w = 'normal'
|
||||
w = unicode(w)
|
||||
w = {'normal':'400', 'bold':'700'}.get(w, w)
|
||||
if w not in {'100', '200', '300', '400', '500', '600', '700',
|
||||
'800', '900'}:
|
||||
w = '400'
|
||||
font['font-weight'] = w
|
||||
|
||||
val = font.get('font-style', None)
|
||||
if val not in {'normal', 'italic', 'oblique'}:
|
||||
val = 'normal'
|
||||
font['font-style'] = val
|
||||
|
||||
val = font.get('font-stretch', None)
|
||||
if val not in {'normal', 'ultra-condensed', 'extra-condensed', 'condensed',
|
||||
'semi-condensed', 'semi-expanded', 'expanded',
|
||||
'extra-expanded', 'ultra-expanded'}:
|
||||
val = 'normal'
|
||||
font['font-stretch'] = val
|
||||
|
||||
widths = {x:i for i, x in enumerate(( 'ultra-condensed',
|
||||
'extra-condensed', 'condensed', 'semi-condensed', 'normal',
|
||||
'semi-expanded', 'expanded', 'extra-expanded', 'ultra-expanded'
|
||||
))}
|
||||
|
||||
def get_matching_rules(rules, font):
|
||||
normalize_font_properties(font)
|
||||
matches = []
|
||||
|
||||
# Filter on family
|
||||
for rule in reversed(rules):
|
||||
ff = frozenset(icu_lower(x) for x in font.get('font-family', []))
|
||||
if ff.intersection(rule['font-family']):
|
||||
matches.append(rule)
|
||||
if not matches:
|
||||
return []
|
||||
|
||||
# Filter on font stretch
|
||||
width = widths[font.get('font-stretch', 'normal')]
|
||||
|
||||
min_dist = min(abs(width-f['width']) for f in matches)
|
||||
nearest = [f for f in matches if abs(width-f['width']) ==
|
||||
min_dist]
|
||||
if width <= 4:
|
||||
lmatches = [f for f in nearest if f['width'] <= width]
|
||||
else:
|
||||
lmatches = [f for f in nearest if f['width'] >= width]
|
||||
matches = (lmatches or nearest)
|
||||
|
||||
# Filter on font-style
|
||||
fs = font.get('font-style', 'normal')
|
||||
order = {
|
||||
'oblique':['oblique', 'italic', 'normal'],
|
||||
'normal':['normal', 'oblique', 'italic']
|
||||
}.get(fs, ['italic', 'oblique', 'normal'])
|
||||
for q in order:
|
||||
m = [f for f in matches if f.get('font-style', 'normal') == q]
|
||||
if m:
|
||||
matches = m
|
||||
break
|
||||
|
||||
# Filter on font weight
|
||||
fw = int(font.get('font-weight', '400'))
|
||||
if fw == 400:
|
||||
q = [400, 500, 300, 200, 100, 600, 700, 800, 900]
|
||||
elif fw == 500:
|
||||
q = [500, 400, 300, 200, 100, 600, 700, 800, 900]
|
||||
elif fw < 400:
|
||||
q = [fw] + list(xrange(fw-100, -100, -100)) + list(xrange(fw+100,
|
||||
100, 1000))
|
||||
else:
|
||||
q = [fw] + list(xrange(fw+100, 100, 1000)) + list(xrange(fw-100,
|
||||
-100, -100))
|
||||
for wt in q:
|
||||
m = [f for f in matches if f['weight'] == wt]
|
||||
if m:
|
||||
return m
|
||||
return []
|
||||
|
||||
class Page(QWebPage): # {{{
|
||||
|
||||
def __init__(self, log):
|
||||
self.log = log
|
||||
QWebPage.__init__(self)
|
||||
self.js = None
|
||||
self.evaljs = self.mainFrame().evaluateJavaScript
|
||||
self.bridge_value = None
|
||||
|
||||
def javaScriptConsoleMessage(self, msg, lineno, msgid):
|
||||
self.log(u'JS:', unicode(msg))
|
||||
@ -40,6 +128,23 @@ class Page(QWebPage):
|
||||
_pass_json_value = pyqtProperty(QString, fget=_pass_json_value_getter,
|
||||
fset=_pass_json_value_setter)
|
||||
|
||||
def load_js(self):
|
||||
if self.js is None:
|
||||
from calibre.utils.resources import compiled_coffeescript
|
||||
self.js = compiled_coffeescript('ebooks.oeb.display.utils')
|
||||
self.js += compiled_coffeescript('ebooks.oeb.polish.font_stats')
|
||||
self.mainFrame().addToJavaScriptWindowObject("py_bridge", self)
|
||||
self.evaljs(self.js)
|
||||
self.evaljs('''
|
||||
py_bridge.__defineGetter__('value', function() {
|
||||
return JSON.parse(this._pass_json_value);
|
||||
});
|
||||
py_bridge.__defineSetter__('value', function(val) {
|
||||
this._pass_json_value = JSON.stringify(val);
|
||||
});
|
||||
''')
|
||||
# }}}
|
||||
|
||||
class StatsCollector(object):
|
||||
|
||||
def __init__(self, container):
|
||||
@ -85,6 +190,7 @@ class StatsCollector(object):
|
||||
self.loop.exit(1)
|
||||
return
|
||||
try:
|
||||
self.page.load_js()
|
||||
self.collect_font_stats()
|
||||
except:
|
||||
self.log.exception('Failed to collect font stats from: %s'%self.container.relpath(self.current_item))
|
||||
@ -94,6 +200,70 @@ class StatsCollector(object):
|
||||
self.render_book()
|
||||
|
||||
def collect_font_stats(self):
|
||||
pass
|
||||
self.page.evaljs('window.font_stats.get_font_face_rules()')
|
||||
font_face_rules = self.page.bridge_value
|
||||
if not isinstance(font_face_rules, list):
|
||||
raise Exception('Unknown error occurred while reading font-face rules')
|
||||
|
||||
# Weed out invalid font-face rules
|
||||
rules = []
|
||||
for rule in font_face_rules:
|
||||
ff = rule.get('font-family', None)
|
||||
if not ff: continue
|
||||
style = parseStyle('font-family:%s'%ff, validate=False)
|
||||
ff = [x.value for x in
|
||||
style.getProperty('font-family').propertyValue]
|
||||
if not ff or ff[0] == 'inherit':
|
||||
continue
|
||||
rule['font-family'] = frozenset(icu_lower(f) for f in ff)
|
||||
src = rule.get('src', None)
|
||||
if not src: continue
|
||||
style = parseStyle('background-image:%s'%src, validate=False)
|
||||
src = style.getProperty('background-image').propertyValue[0].uri
|
||||
if not src.startswith('file://'):
|
||||
self.log.warn('Unknown URI in @font-face: %r'%src)
|
||||
continue
|
||||
src = src[len('file://'):]
|
||||
if iswindows and src.startswith('/'):
|
||||
src = src[1:]
|
||||
src = src.replace('/', os.sep)
|
||||
src = unquote(src)
|
||||
name = self.container.abspath_to_name(src)
|
||||
if not self.container.has_name(name):
|
||||
self.log.warn('Font %r referenced in @font-face rule not found'
|
||||
%name)
|
||||
continue
|
||||
rule['src'] = name
|
||||
normalize_font_properties(rule)
|
||||
rule['width'] = widths[rule['font-stretch']]
|
||||
rule['weight'] = int(rule['font-weight'])
|
||||
rules.append(rule)
|
||||
|
||||
if not rules:
|
||||
return
|
||||
|
||||
for rule in rules:
|
||||
if rule['src'] not in self.font_stats:
|
||||
self.font_stats[rule['src']] = set()
|
||||
|
||||
self.page.evaljs('window.font_stats.get_font_usage()')
|
||||
font_usage = self.page.bridge_value
|
||||
if not isinstance(font_usage, list):
|
||||
raise Exception('Unknown error occurred while reading font usage')
|
||||
exclude = {'\n', '\r', '\t'}
|
||||
for font in font_usage:
|
||||
text = set()
|
||||
for t in font['text']:
|
||||
text |= frozenset(t)
|
||||
text.difference_update(exclude)
|
||||
if not text: continue
|
||||
for rule in get_matching_rules(rules, font):
|
||||
self.font_stats[rule['src']] |= text
|
||||
|
||||
if __name__ == '__main__':
|
||||
from calibre.ebooks.oeb.polish.container import get_container
|
||||
from calibre.utils.logging import default_log
|
||||
default_log.filter_level = default_log.DEBUG
|
||||
ebook = get_container(sys.argv[-1], default_log)
|
||||
print (StatsCollector(ebook).font_stats)
|
||||
|
||||
|
101
src/calibre/ebooks/oeb/polish/subset.py
Normal file
101
src/calibre/ebooks/oeb/polish/subset.py
Normal 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)
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
# }}}
|
||||
|
||||
|
@ -182,6 +182,7 @@ class ConnectShareAction(InterfaceAction):
|
||||
|
||||
def genesis(self):
|
||||
self.share_conn_menu = ShareConnMenu(self.gui)
|
||||
self.share_conn_menu.aboutToShow.connect(self.set_smartdevice_action_state)
|
||||
self.share_conn_menu.toggle_server.connect(self.toggle_content_server)
|
||||
self.share_conn_menu.control_smartdevice.connect(self.control_smartdevice)
|
||||
self.share_conn_menu.config_email.connect(partial(
|
||||
@ -258,7 +259,10 @@ class ConnectShareAction(InterfaceAction):
|
||||
show_port = True
|
||||
else:
|
||||
all_ips = get_all_ip_addresses()
|
||||
if len(all_ips) > 3:
|
||||
if len(all_ips) == 0:
|
||||
formatted_addresses = _('Still looking for IP addresses')
|
||||
show_port = False
|
||||
elif len(all_ips) > 3:
|
||||
formatted_addresses = _('Many IP addresses. See Start/Stop dialog.')
|
||||
show_port = False
|
||||
else:
|
||||
|
241
src/calibre/gui2/actions/polish.py
Normal file
241
src/calibre/gui2/actions/polish.py
Normal 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_()
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -133,7 +133,7 @@ class TagListEditor(QDialog, Ui_TagListEditor):
|
||||
select_item = item
|
||||
item = CountTableWidgetItem(self.counts[tag])
|
||||
# only the name column can be selected
|
||||
item.setFlags (item.flags() & ~Qt.ItemIsSelectable)
|
||||
item.setFlags (item.flags() & ~(Qt.ItemIsSelectable|Qt.ItemIsEditable))
|
||||
self.table.setItem(row, 1, item)
|
||||
item = QTableWidgetItem('')
|
||||
item.setFlags (item.flags() & ~(Qt.ItemIsSelectable|Qt.ItemIsEditable))
|
||||
|
@ -197,7 +197,8 @@ class TemplateHighlighter(QSyntaxHighlighter):
|
||||
|
||||
class TemplateDialog(QDialog, Ui_TemplateDialog):
|
||||
|
||||
def __init__(self, parent, text, mi=None, fm=None, color_field=None):
|
||||
def __init__(self, parent, text, mi=None, fm=None, color_field=None,
|
||||
rule_kind='color'):
|
||||
QDialog.__init__(self, parent)
|
||||
Ui_TemplateDialog.__init__(self)
|
||||
self.setupUi(self)
|
||||
@ -264,7 +265,8 @@ class TemplateDialog(QDialog, Ui_TemplateDialog):
|
||||
self.function.setCurrentIndex(0)
|
||||
self.function.currentIndexChanged[str].connect(self.function_changed)
|
||||
self.textbox_changed()
|
||||
self.rule = (None, '')
|
||||
self.rule = (rule_kind, None, '')
|
||||
self.rule_kind = rule_kind
|
||||
|
||||
tt = _('Template language tutorial')
|
||||
self.template_tutorial.setText(
|
||||
@ -323,5 +325,5 @@ class TemplateDialog(QDialog, Ui_TemplateDialog):
|
||||
_('The template box cannot be empty'), show=True)
|
||||
return
|
||||
|
||||
self.rule = (unicode(self.colored_field.currentText()), txt)
|
||||
self.rule = (self.rule_kind, unicode(self.colored_field.currentText()), txt)
|
||||
QDialog.accept(self)
|
||||
|
@ -98,7 +98,7 @@ class ColumnIcon(object):
|
||||
icon_cache[id_][dex] = icon_bitmap
|
||||
icon_bitmap_cache[icon] = icon_bitmap
|
||||
self.mi = None
|
||||
return icon
|
||||
return icon_bitmap
|
||||
except:
|
||||
pass
|
||||
|
||||
|
@ -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):
|
||||
|
@ -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):
|
||||
|
@ -52,6 +52,12 @@ class ConditionEditor(QWidget): # {{{
|
||||
(_('is less than'), 'lt'),
|
||||
(_('is greater than'), 'gt')
|
||||
),
|
||||
'datetime' : (
|
||||
(_('is equal to'), 'eq'),
|
||||
(_('is less than'), 'lt'),
|
||||
(_('is greater than'), 'gt'),
|
||||
(_('is not more days ago than'), 'count_days')
|
||||
),
|
||||
'multiple' : (
|
||||
(_('has'), 'has'),
|
||||
(_('does not have'), 'does not have'),
|
||||
@ -70,7 +76,7 @@ class ConditionEditor(QWidget): # {{{
|
||||
),
|
||||
}
|
||||
|
||||
for x in ('float', 'rating', 'datetime'):
|
||||
for x in ('float', 'rating'):
|
||||
ACTION_MAP[x] = ACTION_MAP['int']
|
||||
|
||||
|
||||
@ -232,8 +238,12 @@ class ConditionEditor(QWidget): # {{{
|
||||
v = QIntValidator if dt == 'int' else QDoubleValidator
|
||||
self.value_box.setValidator(v(self.value_box))
|
||||
elif dt == 'datetime':
|
||||
self.value_box.setInputMask('9999-99-99')
|
||||
tt = _('Enter a date in the format YYYY-MM-DD')
|
||||
if action == 'count_days':
|
||||
self.value_box.setValidator(QIntValidator(self.value_box))
|
||||
tt = _('Enter the number of days old the item can be. Zero is today')
|
||||
else:
|
||||
self.value_box.setInputMask('9999-99-99')
|
||||
tt = _('Enter a date in the format YYYY-MM-DD')
|
||||
else:
|
||||
tt = _('Enter a string.')
|
||||
if 'pattern' in action:
|
||||
|
@ -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
|
||||
|
@ -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 &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>&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>&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 &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>&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>&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 &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 &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 &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">
|
||||
|
@ -549,7 +549,8 @@ class TagsView(QTreeView): # {{{
|
||||
# Offer specific editors for tags/series/publishers/saved searches
|
||||
self.context_menu.addSeparator()
|
||||
if key in ['tags', 'publisher', 'series'] or \
|
||||
self.db.field_metadata[key]['is_custom']:
|
||||
(self.db.field_metadata[key]['is_custom'] and
|
||||
self.db.field_metadata[key]['datatype'] != 'composite'):
|
||||
self.context_menu.addAction(_('Manage %s')%category,
|
||||
partial(self.context_menu_handler, action='open_editor',
|
||||
category=tag.original_name if tag else None,
|
||||
|
@ -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)
|
||||
|
||||
|
@ -198,6 +198,7 @@ class NookColor(Nook):
|
||||
class NookTablet(NookColor):
|
||||
id = 'nook_tablet'
|
||||
name = 'Nook Tablet/HD'
|
||||
output_profile = 'nook_hd_plus'
|
||||
|
||||
class CybookG3(Device):
|
||||
|
||||
|
@ -133,13 +133,16 @@ class Rule(object): # {{{
|
||||
return "cmp(field('%s'), %s, '%s', '%s', '%s')" % (col, val, lt, eq, gt)
|
||||
|
||||
def date_condition(self, col, action, val):
|
||||
if action == 'count_days':
|
||||
return (("cmp(add(%s, 1), days_between(today(), format_date(raw_field('%s'), 'yyyy-MM-dd')), '', '1', '1')")
|
||||
%(val, col))
|
||||
lt, eq, gt = {
|
||||
'eq': ('', '1', ''),
|
||||
'lt': ('1', '', ''),
|
||||
'gt': ('', '', '1')
|
||||
}[action]
|
||||
return "strcmp(format_date(raw_field('%s'), 'yyyy-MM-dd'), '%s', '%s', '%s', '%s')" % (col,
|
||||
val, lt, eq, gt)
|
||||
return ("strcmp(format_date(raw_field('%s'), 'yyyy-MM-dd'), '%s', '%s', '%s', '%s')" %
|
||||
(col, val, lt, eq, gt))
|
||||
|
||||
def multiple_condition(self, col, action, val, sep):
|
||||
if not sep or sep == '|':
|
||||
|
@ -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]
|
||||
|
@ -497,6 +497,13 @@ class FieldMetadata(dict):
|
||||
def is_custom_field(self, key):
|
||||
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):
|
||||
return [k for k in self._tb_cats.iterkeys() if self.is_ignorable_field(k)]
|
||||
|
||||
def is_series_index(self, key):
|
||||
m = self[key]
|
||||
return (m['datatype'] == 'float' and key.endswith('_index') and
|
||||
|
@ -51,7 +51,7 @@ def server_config(defaults=None):
|
||||
return c
|
||||
|
||||
def custom_fields_to_display(db):
|
||||
ckeys = db.custom_field_keys()
|
||||
ckeys = db.field_metadata.ignorable_field_keys()
|
||||
yes_fields = set(tweaks['content_server_will_display'])
|
||||
no_fields = set(tweaks['content_server_wont_display'])
|
||||
if '*' in yes_fields:
|
||||
|
@ -300,7 +300,8 @@ class AjaxServer(object):
|
||||
meta = category_meta.get(category, None)
|
||||
if meta is None:
|
||||
continue
|
||||
if meta['is_custom'] and category not in displayed_custom_fields:
|
||||
if category_meta.is_ignorable_field(category) and \
|
||||
category not in displayed_custom_fields:
|
||||
continue
|
||||
display_name = meta['name']
|
||||
if category.startswith('@'):
|
||||
|
@ -269,7 +269,7 @@ class BrowseServer(object):
|
||||
for x in fm.sortable_field_keys():
|
||||
if x in ('ondevice', 'formats', 'sort'):
|
||||
continue
|
||||
if fm[x]['is_custom'] and x not in displayed_custom_fields:
|
||||
if fm.is_ignorable_field(x) and x not in displayed_custom_fields:
|
||||
continue
|
||||
if x == 'comments' or fm[x]['datatype'] == 'comments':
|
||||
continue
|
||||
@ -369,7 +369,8 @@ class BrowseServer(object):
|
||||
meta = category_meta.get(category, None)
|
||||
if meta is None:
|
||||
continue
|
||||
if meta['is_custom'] and category not in displayed_custom_fields:
|
||||
if self.db.field_metadata.is_ignorable_field(category) and \
|
||||
category not in displayed_custom_fields:
|
||||
continue
|
||||
# get the icon files
|
||||
main_cat = (category.partition('.')[0]) if hasattr(category,
|
||||
@ -836,7 +837,8 @@ class BrowseServer(object):
|
||||
displayed_custom_fields = custom_fields_to_display(self.db)
|
||||
for field, m in list(mi.get_all_standard_metadata(False).items()) + \
|
||||
list(mi.get_all_user_metadata(False).items()):
|
||||
if m['is_custom'] and field not in displayed_custom_fields:
|
||||
if self.db.field_metadata.is_ignorable_field(field) and \
|
||||
field not in displayed_custom_fields:
|
||||
continue
|
||||
if m['datatype'] == 'comments' or field == 'comments' or (
|
||||
m['datatype'] == 'composite' and \
|
||||
|
@ -594,7 +594,7 @@ class OPDSServer(object):
|
||||
meta = category_meta.get(category, None)
|
||||
if meta is None:
|
||||
continue
|
||||
if category_meta.is_custom_field(category) and \
|
||||
if category_meta.is_ignorable_field(category) and \
|
||||
category not in custom_fields_to_display(self.db):
|
||||
continue
|
||||
cats.append((meta['name'], meta['name'], 'N'+category))
|
||||
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user