mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Merge from trunk
This commit is contained in:
commit
047fb49c2d
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 |
@ -671,7 +671,7 @@ There are five possible things I know of, that can cause this:
|
||||
you dont notice it and so you think |app| has frozen. Disconnect your
|
||||
second monitor and restart calibre.
|
||||
|
||||
* You are using a Wacom branded mouse. There is an incompatibility between
|
||||
* You are using a Wacom branded USB mouse. There is an incompatibility between
|
||||
Wacom mice and the graphics toolkit |app| uses. Try using a non-Wacom
|
||||
mouse.
|
||||
|
||||
|
@ -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'
|
||||
|
||||
|
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')]
|
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"
|
||||
|
@ -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,
|
||||
|
@ -456,8 +456,8 @@ class NookHD(TabletOutput):
|
||||
name = 'Nook HD+'
|
||||
short_name = 'nook_hd_plus'
|
||||
description = _('Intended for the Nook HD+ and similar tablet devices with '
|
||||
'a resolution of 1080x1920')
|
||||
screen_size = comic_screen_size = (1080, 1920)
|
||||
'a resolution of 1280x1920')
|
||||
screen_size = comic_screen_size = (1280, 1920)
|
||||
|
||||
class SonyReaderOutput(OutputProfile):
|
||||
|
||||
|
@ -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):
|
||||
|
||||
@ -236,7 +236,7 @@ class ANDROID(USBMS):
|
||||
'ADVANCED', 'SGH-I727', 'USB_FLASH_DRIVER', 'ANDROID',
|
||||
'S5830I_CARD', 'MID7042', 'LINK-CREATE', '7035', 'VIEWPAD_7E',
|
||||
'NOVO7', 'MB526', '_USB#WYK7MSF8KE', 'TABLET_PC', 'F', 'MT65XX_MS',
|
||||
'ICS', 'E400']
|
||||
'ICS', 'E400', '__FILE-STOR_GADG']
|
||||
WINDOWS_CARD_A_MEM = ['ANDROID_PHONE', 'GT-I9000_CARD', 'SGH-I897',
|
||||
'FILE-STOR_GADGET', 'SGH-T959_CARD', 'SGH-T959', 'SAMSUNG_ANDROID', 'GT-P1000_CARD',
|
||||
'A70S', 'A101IT', '7', 'INCREDIBLE', 'A7EB', 'SGH-T849_CARD',
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -13,17 +13,20 @@ from urllib import unquote as urlunquote
|
||||
from lxml import etree
|
||||
|
||||
from calibre import guess_type, CurrentDir
|
||||
from calibre.customize.ui import (plugin_for_input_format,
|
||||
plugin_for_output_format)
|
||||
from calibre.ebooks.chardet import xml_to_unicode
|
||||
from calibre.ebooks.conversion.plugins.epub_input import (
|
||||
ADOBE_OBFUSCATION, IDPF_OBFUSCATION, decrypt_font)
|
||||
from calibre.ebooks.conversion.preprocess import HTMLPreProcessor, CSSPreProcessor
|
||||
from calibre.ebooks.mobi import MobiError
|
||||
from calibre.ebooks.mobi.reader.headers import MetadataHeader
|
||||
from calibre.ebooks.oeb.base import OEB_DOCS, _css_logger, OEB_STYLES, OPF2_NS
|
||||
from calibre.ebooks.mobi.tweak import set_cover
|
||||
from calibre.ebooks.oeb.base import (serialize, OEB_DOCS, _css_logger,
|
||||
OEB_STYLES, OPF2_NS)
|
||||
from calibre.ebooks.oeb.polish.errors import InvalidBook, DRMError
|
||||
from calibre.ebooks.oeb.parse_utils import NotHTML, parse_html, RECOVER_PARSER
|
||||
from calibre.ptempfile import PersistentTemporaryDirectory
|
||||
from calibre.utils.fonts.sfnt.container import Sfnt
|
||||
from calibre.ptempfile import PersistentTemporaryDirectory, PersistentTemporaryFile
|
||||
from calibre.utils.ipc.simple_worker import fork_job, WorkerError
|
||||
from calibre.utils.logging import default_log
|
||||
from calibre.utils.zipfile import ZipFile
|
||||
@ -43,6 +46,7 @@ class Container(object):
|
||||
self.parsed_cache = {}
|
||||
self.mime_map = {}
|
||||
self.name_path_map = {}
|
||||
self.dirtied = set()
|
||||
|
||||
# Map of relative paths with '/' separators from root of unzipped ePub
|
||||
# to absolute paths on filesystem with os-specific separators
|
||||
@ -141,8 +145,6 @@ class Container(object):
|
||||
data = self.parse_xml(data)
|
||||
elif mime in OEB_STYLES:
|
||||
data = self.parse_css(data, self.relpath(path))
|
||||
elif mime in OEB_FONTS or path.rpartition('.')[-1].lower() in {'ttf', 'otf'}:
|
||||
data = Sfnt(data)
|
||||
return data
|
||||
|
||||
def parse_css(self, data, fname):
|
||||
@ -189,6 +191,65 @@ class Container(object):
|
||||
for path in non_linear:
|
||||
yield path
|
||||
|
||||
def remove_item(self, name):
|
||||
'''
|
||||
Remove the item identified by name from this container. This removes all
|
||||
references to the item in the OPF manifest, guide and spine as well as from
|
||||
any internal caches.
|
||||
'''
|
||||
removed = set()
|
||||
for elem in self.opf.xpath('//opf:manifest/opf:item[@href]',
|
||||
namespaces={'opf':OPF2_NS}):
|
||||
if self.href_to_name(elem.get('href')) == name:
|
||||
id_ = elem.get('id', None)
|
||||
if id_ is not None:
|
||||
removed.add(id_)
|
||||
elem.getparent().remove(elem)
|
||||
self.dirty(self.opf_name)
|
||||
if removed:
|
||||
for item in self.opf.xpath('//opf:spine/opf:itemref[@idref]',
|
||||
namespaces={'opf':OPF2_NS}):
|
||||
idref = item.get('idref')
|
||||
if idref in removed:
|
||||
item.getparent().remove(item)
|
||||
self.dirty(self.opf_name)
|
||||
|
||||
for item in self.opf.xpath('//opf:guide/opf:reference[@href]',
|
||||
namespaces={'opf':OPF2_NS}):
|
||||
if self.href_to_name(item.get('href')) == name:
|
||||
item.getparent().remove(item)
|
||||
self.dirty(self.opf_name)
|
||||
|
||||
path = self.name_path_map.pop(name)
|
||||
if os.path.exists(path):
|
||||
os.remove(path)
|
||||
self.mime_map.pop(name, None)
|
||||
self.parsed_cache.pop(name, None)
|
||||
self.dirtied.discard(name)
|
||||
|
||||
def dirty(self, name):
|
||||
self.dirtied.add(name)
|
||||
|
||||
def commit(self, outpath=None):
|
||||
for name in tuple(self.dirtied):
|
||||
self.dirtied.remove(name)
|
||||
data = self.parsed_cache.pop(name)
|
||||
data = serialize(data, self.mime_map[name])
|
||||
with open(self.name_path_map[name], 'wb') as f:
|
||||
f.write(data)
|
||||
|
||||
def compare_to(self, other):
|
||||
if set(self.name_path_map) != set(other.name_path_map):
|
||||
return 'Set of files is not the same'
|
||||
mismatches = []
|
||||
for name, path in self.name_path_map.iteritems():
|
||||
opath = other.name_path_map[name]
|
||||
with open(path, 'rb') as f1, open(opath, 'rb') as f2:
|
||||
if f1.read() != f2.read():
|
||||
mismatches.append('The file %s is not the same'%name)
|
||||
return '\n'.join(mismatches)
|
||||
|
||||
# EPUB {{{
|
||||
class InvalidEpub(InvalidBook):
|
||||
pass
|
||||
|
||||
@ -294,8 +355,26 @@ class EpubContainer(Container):
|
||||
if not tkey:
|
||||
raise InvalidBook('Failed to find obfuscation key')
|
||||
decrypt_font(tkey, path, alg)
|
||||
self.obfuscated_fonts[name] = (alg, tkey)
|
||||
self.obfuscated_fonts[font] = (alg, tkey)
|
||||
|
||||
def commit(self, outpath=None):
|
||||
super(EpubContainer, self).commit()
|
||||
for name in self.obfuscated_fonts:
|
||||
if name not in self.name_path_map:
|
||||
continue
|
||||
alg, key = self.obfuscated_fonts[name]
|
||||
# Decrypting and encrypting are the same operation (XOR with key)
|
||||
decrypt_font(key, self.name_path_map[name], alg)
|
||||
if outpath is None:
|
||||
outpath = self.pathtoepub
|
||||
from calibre.ebooks.tweak import zip_rebuilder
|
||||
with open(join(self.root, 'mimetype'), 'wb') as f:
|
||||
f.write(guess_type('a.epub')[0])
|
||||
zip_rebuilder(self.root, outpath)
|
||||
|
||||
# }}}
|
||||
|
||||
# AZW3 {{{
|
||||
class InvalidMobi(InvalidBook):
|
||||
pass
|
||||
|
||||
@ -357,14 +436,40 @@ class AZW3Container(Container):
|
||||
super(AZW3Container, self).__init__(tdir, opf_path, log)
|
||||
self.obfuscated_fonts = {x.replace(os.sep, '/') for x in obfuscated_fonts}
|
||||
|
||||
def commit(self, outpath=None):
|
||||
super(AZW3Container, self).commit()
|
||||
if outpath is None:
|
||||
outpath = self.pathtoazw3
|
||||
from calibre.ebooks.conversion.plumber import Plumber, create_oebbook
|
||||
opf = self.name_path_map[self.opf_name]
|
||||
plumber = Plumber(opf, outpath, self.log)
|
||||
plumber.setup_options()
|
||||
inp = plugin_for_input_format('azw3')
|
||||
outp = plugin_for_output_format('azw3')
|
||||
plumber.opts.mobi_passthrough = True
|
||||
oeb = create_oebbook(default_log, opf, plumber.opts)
|
||||
set_cover(oeb)
|
||||
outp.convert(oeb, outpath, inp, plumber.opts, default_log)
|
||||
# }}}
|
||||
|
||||
def get_container(path, log=None):
|
||||
if log is None: log = default_log
|
||||
ebook = (AZW3Container if path.rpartition('.')[-1].lower() in {'azw3', 'mobi'}
|
||||
else EpubContainer)(path, log)
|
||||
return ebook
|
||||
|
||||
if __name__ == '__main__':
|
||||
def test_roundtrip():
|
||||
ebook = get_container(sys.argv[-1])
|
||||
for s in ebook.spine_items:
|
||||
print (ebook.relpath(s))
|
||||
p = PersistentTemporaryFile(suffix='.'+sys.argv[-1].rpartition('.')[-1])
|
||||
p.close()
|
||||
ebook.commit(outpath=p.name)
|
||||
ebook2 = get_container(p.name)
|
||||
ebook3 = get_container(p.name)
|
||||
diff = ebook3.compare_to(ebook2)
|
||||
if diff is not None:
|
||||
print (diff)
|
||||
|
||||
if __name__ == '__main__':
|
||||
test_roundtrip()
|
||||
|
||||
|
||||
|
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()
|
||||
|
@ -11,8 +11,8 @@ import json, sys, os
|
||||
from urllib import unquote
|
||||
|
||||
from cssutils import parseStyle
|
||||
from PyQt4.Qt import (QWebPage, pyqtProperty, QString, QEventLoop, QWebView,
|
||||
Qt, QSize, QTimer)
|
||||
from PyQt4.Qt import (pyqtProperty, QString, QEventLoop, Qt, QSize, QTimer)
|
||||
from PyQt4.QtWebKit import QWebPage, QWebView
|
||||
|
||||
from calibre.constants import iswindows
|
||||
from calibre.ebooks.oeb.display.webview import load_html
|
||||
|
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
|
||||
# }}}
|
||||
|
||||
|
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
|
||||
|
@ -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):
|
||||
|
@ -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">
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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]
|
||||
|
@ -498,6 +498,7 @@ class FieldMetadata(dict):
|
||||
return key.startswith(self.custom_field_prefix)
|
||||
|
||||
def is_ignorable_field(self, key):
|
||||
'Custom fields and user categories are ignorable'
|
||||
return self.is_custom_field(key) or key.startswith('@')
|
||||
|
||||
def ignorable_field_keys(self):
|
||||
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user