Pull from Trunk
@ -4,6 +4,61 @@
|
|||||||
# for important features/bug fixes.
|
# for important features/bug fixes.
|
||||||
# Also, each release can have new and improved recipes.
|
# Also, each release can have new and improved recipes.
|
||||||
|
|
||||||
|
- version: 0.7.7
|
||||||
|
date: 2010-07-02
|
||||||
|
|
||||||
|
new features:
|
||||||
|
- title: "Support for the Nokia E52"
|
||||||
|
|
||||||
|
- title: "Searching on the size column"
|
||||||
|
|
||||||
|
- title: "iTunes driver: Add option to disable cover fetching for speeding up the fetching of large book collections"
|
||||||
|
|
||||||
|
bug fixes:
|
||||||
|
- title: "SONY driver: Only update metadata when books are sent to device."
|
||||||
|
|
||||||
|
- title: "TXT Input: Ensure the generated html is splittable"
|
||||||
|
tickets: [5904]
|
||||||
|
|
||||||
|
- title: "Fix infinite loop in default cover generation."
|
||||||
|
tickets: [6061]
|
||||||
|
|
||||||
|
- title: "HTML Input: Fix a parsing bug that was triggered in rare conditions"
|
||||||
|
tickets: [6064]
|
||||||
|
|
||||||
|
- title: "HTML2Zip plugin: Do not replace ligatures"
|
||||||
|
tickets: [6019]
|
||||||
|
|
||||||
|
- title: "iTunes driver: Fix transmission of non integral series numbers"
|
||||||
|
tickets: [6046]
|
||||||
|
|
||||||
|
- title: "Simplify implementation of cover caching and ensure cover browser is updated when covers are changed"
|
||||||
|
|
||||||
|
- title: "PDF metadata: Fix last character corrupted when setting metadata in encrypted files."
|
||||||
|
|
||||||
|
- title: "PDF metadata: Update the version of PoDoFo used to set metadata to 0.8.1. Hopefully that means more PDF files will work"
|
||||||
|
|
||||||
|
- title: "Device drivers: Speedup for dumping metadata cache to devices on Windows XP"
|
||||||
|
|
||||||
|
- title: "EPUB Output: Ensure that language setting is conformant to the specs"
|
||||||
|
|
||||||
|
- title: "MOBI Output: Fix a memory leak and a crash in the palmdoc compression routine"
|
||||||
|
|
||||||
|
- title: "Metadata download: Fix a regression that resulted in a failed download for some books"
|
||||||
|
|
||||||
|
new recipes:
|
||||||
|
- title: "Foreign Policy and Alo!"
|
||||||
|
author: Darko Miletic
|
||||||
|
|
||||||
|
- title: Statesman and ifzm
|
||||||
|
author: rty
|
||||||
|
|
||||||
|
improved recipes:
|
||||||
|
- Akter
|
||||||
|
- The Old New Thing
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
- version: 0.7.6
|
- version: 0.7.6
|
||||||
date: 2010-06-28
|
date: 2010-06-28
|
||||||
|
|
||||||
|
Before Width: | Height: | Size: 89 KiB After Width: | Height: | Size: 3.8 KiB |
Before Width: | Height: | Size: 75 KiB After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 117 KiB After Width: | Height: | Size: 5.0 KiB |
@ -1752,7 +1752,7 @@
|
|||||||
sodipodi:cy="93.331604"
|
sodipodi:cy="93.331604"
|
||||||
sodipodi:cx="-166.53223"
|
sodipodi:cx="-166.53223"
|
||||||
id="path6082"
|
id="path6082"
|
||||||
style="opacity:1;fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:1.08779998;stroke-opacity:1;filter:url(#filter6074)"
|
style="opacity:1;fill:url(#radialGradient6084);fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:1.08779998;stroke-opacity:1;filter:url(#filter6074)"
|
||||||
sodipodi:type="arc" /></clipPath><radialGradient
|
sodipodi:type="arc" /></clipPath><radialGradient
|
||||||
inkscape:collect="always"
|
inkscape:collect="always"
|
||||||
xlink:href="#linearGradient5990"
|
xlink:href="#linearGradient5990"
|
||||||
@ -2513,7 +2513,7 @@
|
|||||||
transform="matrix(-1.7332269,0,0,1.7332269,-228.13814,-101.76485)"
|
transform="matrix(-1.7332269,0,0,1.7332269,-228.13814,-101.76485)"
|
||||||
clip-path="none" /><path
|
clip-path="none" /><path
|
||||||
sodipodi:type="arc"
|
sodipodi:type="arc"
|
||||||
style="opacity:1;fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:1.08779998;stroke-opacity:1;filter:url(#filter6074)"
|
style="opacity:1;fill:url(#radialGradient6084);fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:1.08779998;stroke-opacity:1;filter:url(#filter6074)"
|
||||||
id="path3915"
|
id="path3915"
|
||||||
sodipodi:cx="-166.53223"
|
sodipodi:cx="-166.53223"
|
||||||
sodipodi:cy="93.331604"
|
sodipodi:cy="93.331604"
|
||||||
@ -2901,22 +2901,8 @@
|
|||||||
id="g133">
|
id="g133">
|
||||||
<defs
|
<defs
|
||||||
id="defs135" />
|
id="defs135" />
|
||||||
<use
|
|
||||||
|
|
||||||
id="use138"
|
|
||||||
x="0"
|
|
||||||
y="0"
|
|
||||||
width="121"
|
|
||||||
height="120" />
|
|
||||||
<clipPath
|
<clipPath
|
||||||
id="XMLID_215_">
|
id="XMLID_215_">
|
||||||
<use
|
|
||||||
|
|
||||||
id="use141"
|
|
||||||
x="0"
|
|
||||||
y="0"
|
|
||||||
width="121"
|
|
||||||
height="120" />
|
|
||||||
</clipPath>
|
</clipPath>
|
||||||
<g
|
<g
|
||||||
clip-path="url(#XMLID_215_)"
|
clip-path="url(#XMLID_215_)"
|
||||||
|
Before Width: | Height: | Size: 116 KiB After Width: | Height: | Size: 116 KiB |
4298
resources/images/edit_copy.svg
Normal file
After Width: | Height: | Size: 133 KiB |
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 3.1 KiB |
269
resources/images/help.svg
Normal file
@ -0,0 +1,269 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Generator: Adobe Illustrator 12.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 51448) -->
|
||||||
|
<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"
|
||||||
|
version="1.0"
|
||||||
|
id="Livello_1"
|
||||||
|
width="128"
|
||||||
|
height="128"
|
||||||
|
viewBox="0 0 139 139"
|
||||||
|
overflow="visible"
|
||||||
|
enable-background="new 0 0 139 139"
|
||||||
|
xml:space="preserve"
|
||||||
|
sodipodi:version="0.32"
|
||||||
|
inkscape:version="0.45+devel"
|
||||||
|
sodipodi:docname="system-help.svgz"
|
||||||
|
inkscape:output_extension="org.inkscape.output.svgz.inkscape"
|
||||||
|
style="overflow:visible"><metadata
|
||||||
|
id="metadata3164"><rdf:RDF><cc:Work
|
||||||
|
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
|
||||||
|
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /></cc:Work></rdf:RDF></metadata><defs
|
||||||
|
id="defs3162"><filter
|
||||||
|
inkscape:collect="always"
|
||||||
|
x="-0.132641"
|
||||||
|
width="1.265282"
|
||||||
|
y="-0.34752154"
|
||||||
|
height="1.6950431"
|
||||||
|
id="filter3547"><feGaussianBlur
|
||||||
|
inkscape:collect="always"
|
||||||
|
stdDeviation="2.7512044"
|
||||||
|
id="feGaussianBlur3549" /></filter><filter
|
||||||
|
inkscape:collect="always"
|
||||||
|
id="filter5097"><feGaussianBlur
|
||||||
|
inkscape:collect="always"
|
||||||
|
stdDeviation="2.32"
|
||||||
|
id="feGaussianBlur5099" /></filter><filter
|
||||||
|
inkscape:collect="always"
|
||||||
|
x="-0.143268"
|
||||||
|
width="1.286536"
|
||||||
|
y="-0.072184406"
|
||||||
|
height="1.1443688"
|
||||||
|
id="filter5125"><feGaussianBlur
|
||||||
|
inkscape:collect="always"
|
||||||
|
stdDeviation="1.91024"
|
||||||
|
id="feGaussianBlur5127" /></filter></defs><sodipodi:namedview
|
||||||
|
inkscape:window-height="697"
|
||||||
|
inkscape:window-width="1024"
|
||||||
|
inkscape:pageshadow="2"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
guidetolerance="10.0"
|
||||||
|
gridtolerance="10.0"
|
||||||
|
objecttolerance="10.0"
|
||||||
|
borderopacity="1.0"
|
||||||
|
bordercolor="#666666"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
id="base"
|
||||||
|
inkscape:zoom="2.9352518"
|
||||||
|
inkscape:cx="99.496726"
|
||||||
|
inkscape:cy="69.329657"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="0"
|
||||||
|
inkscape:current-layer="Livello_1"
|
||||||
|
height="128px"
|
||||||
|
width="128px" />
|
||||||
|
<filter
|
||||||
|
id="AI_Sfocatura_4">
|
||||||
|
<feGaussianBlur
|
||||||
|
stdDeviation="4"
|
||||||
|
id="feGaussianBlur3096" />
|
||||||
|
</filter>
|
||||||
|
<filter
|
||||||
|
id="AI_Sfocatura_2">
|
||||||
|
<feGaussianBlur
|
||||||
|
stdDeviation="2"
|
||||||
|
id="feGaussianBlur3099" />
|
||||||
|
</filter>
|
||||||
|
<radialGradient
|
||||||
|
id="XMLID_12_"
|
||||||
|
cx="69.600098"
|
||||||
|
cy="69.576698"
|
||||||
|
r="58"
|
||||||
|
gradientTransform="matrix(1,0,0,-0.1823,0,134.8566)"
|
||||||
|
gradientUnits="userSpaceOnUse">
|
||||||
|
<stop
|
||||||
|
offset="0"
|
||||||
|
style="stop-color:#000000"
|
||||||
|
id="stop3102" />
|
||||||
|
<stop
|
||||||
|
offset="1"
|
||||||
|
style="stop-color:#000000;stop-opacity:0;"
|
||||||
|
id="stop3104" />
|
||||||
|
</radialGradient>
|
||||||
|
<circle
|
||||||
|
sodipodi:ry="58"
|
||||||
|
sodipodi:rx="58"
|
||||||
|
sodipodi:cy="69.599998"
|
||||||
|
sodipodi:cx="69.599998"
|
||||||
|
style="opacity:0.7;fill:#000000;fill-opacity:1;stroke:none;filter:url(#filter5097)"
|
||||||
|
id="circle5091"
|
||||||
|
r="58"
|
||||||
|
cy="69.599998"
|
||||||
|
cx="69.599998"
|
||||||
|
transform="matrix(1.0859375,0,0,1.0859375,-3.9093733,-8.2531233)" /><ellipse
|
||||||
|
cx="69.599998"
|
||||||
|
cy="122.173"
|
||||||
|
rx="58"
|
||||||
|
ry="10.573"
|
||||||
|
id="ellipse3106"
|
||||||
|
style="opacity:0.6;fill:url(#XMLID_12_)"
|
||||||
|
sodipodi:cx="69.599998"
|
||||||
|
sodipodi:cy="122.173"
|
||||||
|
sodipodi:rx="58"
|
||||||
|
sodipodi:ry="10.573"
|
||||||
|
transform="translate(-9.9998474e-2,1.9102535)" />
|
||||||
|
|
||||||
|
<radialGradient
|
||||||
|
id="XMLID_13_"
|
||||||
|
cx="69.600098"
|
||||||
|
cy="69.600098"
|
||||||
|
r="58"
|
||||||
|
gradientUnits="userSpaceOnUse">
|
||||||
|
<stop
|
||||||
|
offset="0.6154"
|
||||||
|
style="stop-color:#EEEEEE"
|
||||||
|
id="stop3113" />
|
||||||
|
<stop
|
||||||
|
offset="0.8225"
|
||||||
|
style="stop-color:#DDDDDD"
|
||||||
|
id="stop3115" />
|
||||||
|
<stop
|
||||||
|
offset="1"
|
||||||
|
style="stop-color:#FFFFFF"
|
||||||
|
id="stop3117" />
|
||||||
|
</radialGradient>
|
||||||
|
<circle
|
||||||
|
cx="69.599998"
|
||||||
|
cy="69.599998"
|
||||||
|
r="58"
|
||||||
|
id="circle3119"
|
||||||
|
style="fill:url(#XMLID_13_)"
|
||||||
|
sodipodi:cx="69.599998"
|
||||||
|
sodipodi:cy="69.599998"
|
||||||
|
sodipodi:rx="58"
|
||||||
|
sodipodi:ry="58"
|
||||||
|
transform="matrix(1.0859375,0,0,1.0859375,-3.9093733,-8.2531233)" />
|
||||||
|
<linearGradient
|
||||||
|
id="XMLID_14_"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
x1="27.6001"
|
||||||
|
y1="69.600098"
|
||||||
|
x2="111.6001"
|
||||||
|
y2="69.600098"
|
||||||
|
gradientTransform="matrix(1.0859375,0,0,1.0859375,-3.9093733,-8.2531233)">
|
||||||
|
<stop
|
||||||
|
offset="0"
|
||||||
|
style="stop-color:#2A94EC"
|
||||||
|
id="stop3122" />
|
||||||
|
<stop
|
||||||
|
offset="1"
|
||||||
|
style="stop-color:#0057AE"
|
||||||
|
id="stop3124" />
|
||||||
|
</linearGradient>
|
||||||
|
<path
|
||||||
|
d="M 26.062502,67.328127 C 26.062502,92.477355 46.522651,112.9375 71.671877,112.9375 C 96.821104,112.9375 117.28125,92.477355 117.28125,67.328127 C 117.28125,42.178901 96.821104,21.718753 71.671877,21.718753 C 46.522651,21.718753 26.062502,42.178901 26.062502,67.328127 z"
|
||||||
|
id="path3126"
|
||||||
|
style="fill:url(#XMLID_14_)" />
|
||||||
|
<g
|
||||||
|
id="circle22111"
|
||||||
|
cy="92"
|
||||||
|
rx="36"
|
||||||
|
ry="36"
|
||||||
|
cx="343.99899"
|
||||||
|
enable-background="new "
|
||||||
|
style="opacity:0.3;filter:url(#filter3547)"
|
||||||
|
transform="matrix(1.0859375,0,0,1.0859375,-3.9093733,-8.2531233)">
|
||||||
|
<path
|
||||||
|
d="M 77.041,104.759 C 63.767,106.115 50.122,103.11 46.565,98.042 C 43.007,92.976 50.885,87.768 64.16,86.41 C 77.434,85.054 91.079,88.058 94.637,93.126 C 98.193,98.194 90.315,103.401 77.041,104.759 z"
|
||||||
|
id="path3129"
|
||||||
|
style="fill:#a8dde0" />
|
||||||
|
</g>
|
||||||
|
<linearGradient
|
||||||
|
id="circle16776_1_"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
x1="135.5601"
|
||||||
|
y1="417.66461"
|
||||||
|
x2="161.87621"
|
||||||
|
y2="417.66461"
|
||||||
|
gradientTransform="matrix(0,1.7280523,1.7280523,0,-650.07477,-218.71693)">
|
||||||
|
<stop
|
||||||
|
offset="0"
|
||||||
|
style="stop-color:#FFFFFF"
|
||||||
|
id="stop3132" />
|
||||||
|
<stop
|
||||||
|
offset="1"
|
||||||
|
style="stop-color:#ffffff;stop-opacity:0;"
|
||||||
|
id="stop3134" />
|
||||||
|
</linearGradient>
|
||||||
|
<path
|
||||||
|
id="circle16776"
|
||||||
|
enable-background="new "
|
||||||
|
d="M 71.671877,24.06655 C 50.288682,24.06655 32.41958,38.77123 28.113838,58.349597 C 36.698174,66.142284 52.986151,54.358777 71.671877,54.358777 C 90.357604,54.358777 106.64666,66.142284 115.22991,58.349597 C 110.92417,38.77123 93.056158,24.06655 71.671877,24.06655 z"
|
||||||
|
style="opacity:0.8;fill:url(#circle16776_1_)" />
|
||||||
|
<g
|
||||||
|
id="g3137"
|
||||||
|
transform="matrix(1.0859375,0,0,1.0859375,-3.9093733,-8.2531233)">
|
||||||
|
<defs
|
||||||
|
id="defs3139"><path
|
||||||
|
id="XMLID_10_"
|
||||||
|
d="M 27.6,69.6 C 27.6,92.759 46.441,111.6 69.6,111.6 C 92.759,111.6 111.6,92.759 111.6,69.6 C 111.6,46.441 92.759,27.6 69.6,27.6 C 46.441,27.6 27.6,46.441 27.6,69.6 z" /></defs>
|
||||||
|
<clipPath
|
||||||
|
id="XMLID_6_">
|
||||||
|
<use
|
||||||
|
xlink:href="#XMLID_10_"
|
||||||
|
id="use3143"
|
||||||
|
x="0"
|
||||||
|
y="0"
|
||||||
|
width="139"
|
||||||
|
height="139" />
|
||||||
|
</clipPath>
|
||||||
|
<g
|
||||||
|
clip-path="url(#XMLID_6_)"
|
||||||
|
id="g3145"
|
||||||
|
style="filter:url(#AI_Sfocatura_2)">
|
||||||
|
<path
|
||||||
|
d="M 27.6,69.6 C 27.6,92.759 46.441,111.6 69.6,111.6 C 92.759,111.6 111.6,92.759 111.6,69.6 C 111.6,46.441 92.759,27.6 69.6,27.6 C 46.441,27.6 27.6,46.441 27.6,69.6 z"
|
||||||
|
id="path3147"
|
||||||
|
style="fill:none;stroke:#00316e;stroke-width:2" />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<g
|
||||||
|
transform="matrix(1.0859375,0,0,1.1113796,-3.201342,-9.3177223)"
|
||||||
|
id="g5119"
|
||||||
|
style="fill:#00316e;filter:url(#filter5125)"><path
|
||||||
|
style="fill:#00316e"
|
||||||
|
d="M 63.37,80.089 L 63.192,77.746 C 63.012,73.148 64.44,68.462 68.451,63.684 C 71.304,60.26 73.62,57.286 73.62,54.221 C 73.62,51.157 71.571,48.994 67.202,48.903 C 64.173,48.903 60.696,49.895 58.289,51.517 L 55.348,41.784 C 58.556,39.89 63.815,38.088 70.233,38.088 C 81.91,38.088 87.348,44.668 87.348,52.058 C 87.348,58.997 83.069,63.415 79.681,67.289 C 76.472,70.894 75.046,74.41 75.135,78.466 L 75.135,80.088 L 63.37,80.088 L 63.37,80.089 z"
|
||||||
|
id="path5121" /><circle
|
||||||
|
style="fill:#00316e"
|
||||||
|
sodipodi:ry="8"
|
||||||
|
sodipodi:rx="8"
|
||||||
|
sodipodi:cy="93.599998"
|
||||||
|
sodipodi:cx="69.599998"
|
||||||
|
cx="69.599998"
|
||||||
|
cy="93.599998"
|
||||||
|
r="8"
|
||||||
|
id="circle5123" /></g><g
|
||||||
|
id="g5101"
|
||||||
|
transform="matrix(1.0859375,0,0,1.0859375,-3.201342,-8.2531233)"><path
|
||||||
|
id="path3157"
|
||||||
|
d="M 63.37,80.089 L 63.192,77.746 C 63.012,73.148 64.44,68.462 68.451,63.684 C 71.304,60.26 73.62,57.286 73.62,54.221 C 73.62,51.157 71.571,48.994 67.202,48.903 C 64.173,48.903 60.696,49.895 58.289,51.517 L 55.348,41.784 C 58.556,39.89 63.815,38.088 70.233,38.088 C 81.91,38.088 87.348,44.668 87.348,52.058 C 87.348,58.997 83.069,63.415 79.681,67.289 C 76.472,70.894 75.046,74.41 75.135,78.466 L 75.135,80.088 L 63.37,80.088 L 63.37,80.089 z"
|
||||||
|
style="fill:#ffffff" /><circle
|
||||||
|
id="circle3159"
|
||||||
|
r="8"
|
||||||
|
cy="93.599998"
|
||||||
|
cx="69.599998"
|
||||||
|
sodipodi:cx="69.599998"
|
||||||
|
sodipodi:cy="93.599998"
|
||||||
|
sodipodi:rx="8"
|
||||||
|
sodipodi:ry="8"
|
||||||
|
style="fill:#ffffff" /></g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 8.4 KiB |
Before Width: | Height: | Size: 109 KiB After Width: | Height: | Size: 4.1 KiB |
BIN
resources/images/news/alo_novine.png
Normal file
After Width: | Height: | Size: 753 B |
BIN
resources/images/news/haaretz.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 58 KiB After Width: | Height: | Size: 6.3 KiB |
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 3.6 KiB |
Before Width: | Height: | Size: 69 KiB After Width: | Height: | Size: 7.2 KiB |
Before Width: | Height: | Size: 73 KiB After Width: | Height: | Size: 2.2 KiB |
@ -15,7 +15,7 @@ class Akter(BasicNewsRecipe):
|
|||||||
category = 'vesti, online vesti, najnovije vesti, politika, sport, ekonomija, biznis, finansije, berza, kultura, zivot, putovanja, auto, automobili, tehnologija, politicki magazin, dogadjaji, desavanja, lifestyle, zdravlje, zdravstvo, vest, novine, nedeljnik, srbija, novi sad, vojvodina, svet, drustvo, zabava, republika srpska, beograd, intervju, komentar, reportaza, arhiva vesti, news, serbia, politics'
|
category = 'vesti, online vesti, najnovije vesti, politika, sport, ekonomija, biznis, finansije, berza, kultura, zivot, putovanja, auto, automobili, tehnologija, politicki magazin, dogadjaji, desavanja, lifestyle, zdravlje, zdravstvo, vest, novine, nedeljnik, srbija, novi sad, vojvodina, svet, drustvo, zabava, republika srpska, beograd, intervju, komentar, reportaza, arhiva vesti, news, serbia, politics'
|
||||||
oldest_article = 8
|
oldest_article = 8
|
||||||
max_articles_per_feed = 100
|
max_articles_per_feed = 100
|
||||||
no_stylesheets = False
|
no_stylesheets = True
|
||||||
use_embedded_content = False
|
use_embedded_content = False
|
||||||
encoding = 'utf-8'
|
encoding = 'utf-8'
|
||||||
masthead_url = 'http://www.akter.co.rs/templates/gk_thenews2/images/style2/logo.png'
|
masthead_url = 'http://www.akter.co.rs/templates/gk_thenews2/images/style2/logo.png'
|
||||||
@ -23,9 +23,9 @@ class Akter(BasicNewsRecipe):
|
|||||||
publication_type = 'magazine'
|
publication_type = 'magazine'
|
||||||
remove_empty_feeds = True
|
remove_empty_feeds = True
|
||||||
PREFIX = 'http://www.akter.co.rs'
|
PREFIX = 'http://www.akter.co.rs'
|
||||||
extra_css = """ @font-face {font-family: "serif1";src:url(res:///opt/sony/ebook/FONT/tt0011m_.ttf)}
|
extra_css = """
|
||||||
@font-face {font-family: "sans1";src:url(res:///opt/sony/ebook/FONT/tt0003m_.ttf)}
|
@font-face {font-family: "sans1";src:url(res:///opt/sony/ebook/FONT/tt0003m_.ttf)}
|
||||||
.article_description,body,.lokacija{font-family: Arial,Helvetica,sans1,sans-serif}
|
.article_description,body{font-family: Arial,Helvetica,sans1,sans-serif}
|
||||||
.color-2{display:block; margin-bottom: 10px; padding: 5px, 10px;
|
.color-2{display:block; margin-bottom: 10px; padding: 5px, 10px;
|
||||||
border-left: 1px solid #D00000; color: #D00000}
|
border-left: 1px solid #D00000; color: #D00000}
|
||||||
img{margin-bottom: 0.8em} """
|
img{margin-bottom: 0.8em} """
|
||||||
|
65
resources/recipes/alo_novine.recipe
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
__license__ = 'GPL v3'
|
||||||
|
__copyright__ = '2010, Darko Miletic <darko.miletic at gmail.com>'
|
||||||
|
|
||||||
|
'''
|
||||||
|
www.alo.rs
|
||||||
|
'''
|
||||||
|
|
||||||
|
import re
|
||||||
|
from calibre.web.feeds.recipes import BasicNewsRecipe
|
||||||
|
|
||||||
|
class Alo_Novine(BasicNewsRecipe):
|
||||||
|
title = 'Alo!'
|
||||||
|
__author__ = 'Darko Miletic'
|
||||||
|
description = "News Portal from Serbia"
|
||||||
|
publisher = 'Alo novine d.o.o.'
|
||||||
|
category = 'news, politics, Serbia'
|
||||||
|
oldest_article = 2
|
||||||
|
max_articles_per_feed = 100
|
||||||
|
delay = 4
|
||||||
|
no_stylesheets = True
|
||||||
|
encoding = 'utf-8'
|
||||||
|
use_embedded_content = False
|
||||||
|
language = 'sr'
|
||||||
|
extra_css = """
|
||||||
|
@font-face {font-family: "sans1";src:url(res:///opt/sony/ebook/FONT/tt0003m_.ttf)}
|
||||||
|
.article_description,body{font-family: Arial,Helvetica,sans1,sans-serif}
|
||||||
|
.lead {font-size: 1.3em}
|
||||||
|
h1{color: #DB0700}
|
||||||
|
.article_uvod{font-style: italic; font-size: 1.2em}
|
||||||
|
img{margin-bottom: 0.8em} """
|
||||||
|
|
||||||
|
conversion_options = {
|
||||||
|
'comment' : description
|
||||||
|
, 'tags' : category
|
||||||
|
, 'publisher': publisher
|
||||||
|
, 'language' : language
|
||||||
|
}
|
||||||
|
|
||||||
|
preprocess_regexps = [(re.compile(u'\u0110'), lambda match: u'\u00D0')]
|
||||||
|
|
||||||
|
remove_tags = [dict(name=['object','link','embed'])]
|
||||||
|
remove_attributes = ['height','width']
|
||||||
|
|
||||||
|
feeds = [
|
||||||
|
(u'Najnovije Vijesti', u'http://www.alo.rs/rss/danasnje_vesti')
|
||||||
|
,(u'Politika' , u'http://www.alo.rs/rss/politika')
|
||||||
|
,(u'Vesti' , u'http://www.alo.rs/rss/vesti')
|
||||||
|
,(u'Sport' , u'http://www.alo.rs/rss/sport')
|
||||||
|
,(u'Ljudi' , u'http://www.alo.rs/rss/ljudi')
|
||||||
|
,(u'Saveti' , u'http://www.alo.rs/rss/saveti')
|
||||||
|
]
|
||||||
|
|
||||||
|
def preprocess_html(self, soup):
|
||||||
|
for item in soup.findAll(style=True):
|
||||||
|
del item['style']
|
||||||
|
return soup
|
||||||
|
|
||||||
|
def print_version(self, url):
|
||||||
|
artl = url.rpartition('/')[0]
|
||||||
|
artid = artl.rpartition('/')[2]
|
||||||
|
return 'http://www.alo.rs/resources/templates/tools/print.php?id=' + artid
|
||||||
|
|
||||||
|
def image_url_processor(self, baseurl, url):
|
||||||
|
return url.replace('alo.rs//','alo.rs/')
|
||||||
|
|
40
resources/recipes/anchorage_daily.recipe
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
from calibre.web.feeds.news import BasicNewsRecipe
|
||||||
|
|
||||||
|
class AdvancedUserRecipe1278347258(BasicNewsRecipe):
|
||||||
|
title = u'Anchorage Daily News'
|
||||||
|
__author__ = 'rty'
|
||||||
|
oldest_article = 7
|
||||||
|
max_articles_per_feed = 100
|
||||||
|
|
||||||
|
feeds = [(u'Alaska News', u'http://www.adn.com/news/alaska/index.xml'),
|
||||||
|
(u'Business', u'http://www.adn.com/money/index.xml'),
|
||||||
|
(u'Sports', u'http://www.adn.com/sports/index.xml'),
|
||||||
|
(u'Politics', u'http://www.adn.com/politics/index.xml'),
|
||||||
|
(u'Lifestyles', u'http://www.adn.com/life/index.xml'),
|
||||||
|
(u'Iditarod', u'http://www.adn.com/iditarod/index.xml')
|
||||||
|
]
|
||||||
|
description = ''''Alaska's Newspaper'''
|
||||||
|
publisher = 'http://www.adn.com'
|
||||||
|
category = 'news, Alaska, Anchorage'
|
||||||
|
language = 'en'
|
||||||
|
extra_css = '''
|
||||||
|
p{font-weight: normal;text-align: justify}
|
||||||
|
'''
|
||||||
|
remove_javascript = True
|
||||||
|
use_embedded_content = False
|
||||||
|
no_stylesheets = True
|
||||||
|
language = 'en'
|
||||||
|
encoding = 'latin-1'
|
||||||
|
conversion_options = {'linearize_tables':True}
|
||||||
|
masthead_url = 'http://media.adn.com/includes/assets/images/adn_logo.2.gif'
|
||||||
|
|
||||||
|
keep_only_tags = [
|
||||||
|
dict(name='div', attrs={'class':'left_col story_mainbar'}),
|
||||||
|
]
|
||||||
|
remove_tags = [
|
||||||
|
dict(name='div', attrs={'class':'story_tools'}),
|
||||||
|
dict(name='p', attrs={'class':'ad_label'}),
|
||||||
|
]
|
||||||
|
remove_tags_after = [
|
||||||
|
dict(name='div', attrs={'class':'advertisement'}),
|
||||||
|
]
|
39
resources/recipes/bbc_chinese.recipe
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
from calibre.web.feeds.news import BasicNewsRecipe
|
||||||
|
|
||||||
|
class AdvancedUserRecipe1277443634(BasicNewsRecipe):
|
||||||
|
title = u'BBC Chinese'
|
||||||
|
oldest_article = 7
|
||||||
|
max_articles_per_feed = 100
|
||||||
|
|
||||||
|
feeds = [
|
||||||
|
(u'\u4e3b\u9875', u'http://www.bbc.co.uk/zhongwen/simp/index.xml'),
|
||||||
|
(u'\u56fd\u9645\u65b0\u95fb', u'http://www.bbc.co.uk/zhongwen/simp/world/index.xml'),
|
||||||
|
(u'\u4e24\u5cb8\u4e09\u5730', u'http://www.bbc.co.uk/zhongwen/simp/china/index.xml'),
|
||||||
|
(u'\u91d1\u878d\u8d22\u7ecf', u'http://www.bbc.co.uk/zhongwen/simp/business/index.xml'),
|
||||||
|
(u'\u7f51\u4e0a\u4e92\u52a8', u'http://www.bbc.co.uk/zhongwen/simp/interactive/index.xml'),
|
||||||
|
(u'\u97f3\u89c6\u56fe\u7247', u'http://www.bbc.co.uk/zhongwen/simp/multimedia/index.xml'),
|
||||||
|
(u'\u5206\u6790\u8bc4\u8bba', u'http://www.bbc.co.uk/zhongwen/simp/indepth/index.xml')
|
||||||
|
]
|
||||||
|
extra_css = '''
|
||||||
|
@font-face {font-family: "DroidFont", serif, sans-serif; src: url(res:///system/fonts/DroidSansFallback.ttf); }\n
|
||||||
|
body {margin-right: 8pt; font-family: 'DroidFont', serif;}\n
|
||||||
|
h1 {font-family: 'DroidFont', serif;}\n
|
||||||
|
.articledescription {font-family: 'DroidFont', serif;}
|
||||||
|
'''
|
||||||
|
__author__ = 'rty'
|
||||||
|
__version__ = '1.0'
|
||||||
|
language = 'zh'
|
||||||
|
pubisher = 'British Broadcasting Corporation'
|
||||||
|
description = 'BBC news in Chinese'
|
||||||
|
category = 'News, Chinese'
|
||||||
|
remove_javascript = True
|
||||||
|
use_embedded_content = False
|
||||||
|
no_stylesheets = True
|
||||||
|
encoding = 'UTF-8'
|
||||||
|
conversion_options = {'linearize_tables':True}
|
||||||
|
masthead_url = 'http://wscdn.bbc.co.uk/zhongwen/simp/images/1024/brand.jpg'
|
||||||
|
keep_only_tags = [
|
||||||
|
dict(name='h1'),
|
||||||
|
dict(name='p', attrs={'class':['primary-topic','summary']}),
|
||||||
|
dict(name='div', attrs={'class':['bodytext','datestamp']}),
|
||||||
|
]
|
64
resources/recipes/big_oven.recipe
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
from calibre.web.feeds.news import BasicNewsRecipe
|
||||||
|
|
||||||
|
class BigOven(BasicNewsRecipe):
|
||||||
|
title = 'BigOven'
|
||||||
|
__author__ = 'Starson17'
|
||||||
|
description = 'Recipes for the Foodie in us all. Registration is free. A fake username and password just gives smaller photos.'
|
||||||
|
language = 'en'
|
||||||
|
category = 'news, food, recipes, gourmet'
|
||||||
|
publisher = 'Starson17'
|
||||||
|
use_embedded_content= False
|
||||||
|
no_stylesheets = True
|
||||||
|
oldest_article = 24
|
||||||
|
remove_javascript = True
|
||||||
|
remove_empty_feeds = True
|
||||||
|
cover_url = 'http://www.software.com/images/products/BigOven%20Logo_177_216.JPG'
|
||||||
|
max_articles_per_feed = 30
|
||||||
|
needs_subscription = True
|
||||||
|
|
||||||
|
conversion_options = {'linearize_tables' : True
|
||||||
|
, 'comment' : description
|
||||||
|
, 'tags' : category
|
||||||
|
, 'publisher' : publisher
|
||||||
|
, 'language' : language
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_browser(self):
|
||||||
|
br = BasicNewsRecipe.get_browser()
|
||||||
|
if self.username is not None and self.password is not None:
|
||||||
|
br.open('http://www.bigoven.com/')
|
||||||
|
br.select_form(name='form1')
|
||||||
|
br['TopMenu_bo1$email'] = self.username
|
||||||
|
br['TopMenu_bo1$password'] = self.password
|
||||||
|
br.submit()
|
||||||
|
return br
|
||||||
|
|
||||||
|
remove_attributes = ['style', 'font']
|
||||||
|
|
||||||
|
keep_only_tags = [dict(name='h1')
|
||||||
|
,dict(name='div', attrs={'class':'img'})
|
||||||
|
,dict(name='div', attrs={'id':'intro'})
|
||||||
|
]
|
||||||
|
|
||||||
|
remove_tags = [dict(name='div', attrs={'style':["overflow: visible;"]})
|
||||||
|
,dict(name='div', attrs={'class':['ctas']})
|
||||||
|
#,dict(name='a', attrs={'class':['edit']})
|
||||||
|
,dict(name='p', attrs={'class':['byline']})
|
||||||
|
]
|
||||||
|
|
||||||
|
feeds = [(u'4 & 5 Star Rated Recipes', u'http://feeds.feedburner.com/Bigovencom-RecipeRaves?format=xml')]
|
||||||
|
|
||||||
|
def preprocess_html(self, soup):
|
||||||
|
for tag in soup.findAll(name='a', attrs={'class':['edit']}):
|
||||||
|
tag.parent.extract()
|
||||||
|
for tag in soup.findAll(name='a', attrs={'class':['deflink']}):
|
||||||
|
tag.replaceWith(tag.string)
|
||||||
|
return soup
|
||||||
|
|
||||||
|
extra_css = '''
|
||||||
|
h1{font-family:Arial,Helvetica,sans-serif; font-weight:bold;font-size:large;}
|
||||||
|
h2{font-family:Arial,Helvetica,sans-serif; font-weight:normal;font-size:medium;}
|
||||||
|
p{font-family:Arial,Helvetica,sans-serif;font-size:small;}
|
||||||
|
body{font-family:Helvetica,Arial,sans-serif;font-size:small;}
|
||||||
|
'''
|
||||||
|
|
39
resources/recipes/china_economic_net.recipe
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
from calibre.web.feeds.news import BasicNewsRecipe
|
||||||
|
|
||||||
|
class AdvancedUserRecipe1278162597(BasicNewsRecipe):
|
||||||
|
__author__ = 'rty'
|
||||||
|
title = u'China Economic Net'
|
||||||
|
oldest_article = 7
|
||||||
|
max_articles_per_feed = 100
|
||||||
|
|
||||||
|
pubisher = 'www.ce.cn - China Economic net - Beijing'
|
||||||
|
description = 'China Economic Net Magazine'
|
||||||
|
category = 'Economic News Magazine, Chinese, China'
|
||||||
|
feeds = [
|
||||||
|
(u'Stock Market 股市', u'http://finance.ce.cn/stock/index_6304.xml'),
|
||||||
|
(u'Money 理财', u'http://finance.ce.cn/money/index_6301.xml'),
|
||||||
|
(u'Health 健康', u'http://www.ce.cn/health/index_6294.xml'),
|
||||||
|
(u'Technology 科技', u'http://sci.ce.cn/mainpage/index_6307.xml'),
|
||||||
|
(u'Domestic Politics 国内时政', u'http://www.ce.cn/xwzx/gnsz/index_6273.xml')
|
||||||
|
]
|
||||||
|
masthead_url = 'http://finance.ce.cn/images/08mdy_logo.gif'
|
||||||
|
extra_css = '''
|
||||||
|
@font-face {font-family: "DroidFont", serif, sans-serif; src: url(res:///system/fonts/DroidSansFallback.ttf); }\n
|
||||||
|
body {margin-right: 8pt; font-family: 'DroidFont', serif;}\n
|
||||||
|
h1 {font-family: 'DroidFont', serif;}\n
|
||||||
|
.articledescription {font-family: 'DroidFont', serif;}
|
||||||
|
'''
|
||||||
|
remove_javascript = True
|
||||||
|
use_embedded_content = False
|
||||||
|
no_stylesheets = True
|
||||||
|
language = 'zh-cn'
|
||||||
|
encoding = 'gb2312'
|
||||||
|
conversion_options = {'linearize_tables':True}
|
||||||
|
|
||||||
|
|
||||||
|
keep_only_tags = [
|
||||||
|
|
||||||
|
dict(name='h1', attrs={'id':'articleTitle'}),
|
||||||
|
dict(name='div', attrs={'class':'laiyuan'}),
|
||||||
|
dict(name='div', attrs={'id':'articleText'}),
|
||||||
|
]
|
@ -7,7 +7,7 @@ class AdvancedUserRecipe1277228948(BasicNewsRecipe):
|
|||||||
|
|
||||||
__author__ = 'rty'
|
__author__ = 'rty'
|
||||||
__version__ = '1.0'
|
__version__ = '1.0'
|
||||||
language = 'zh_CN'
|
language = 'zh'
|
||||||
pubisher = 'www.chinapressusa.com'
|
pubisher = 'www.chinapressusa.com'
|
||||||
description = 'Overseas Chinese Network Newspaper in the USA'
|
description = 'Overseas Chinese Network Newspaper in the USA'
|
||||||
category = 'News in Chinese, USA'
|
category = 'News in Chinese, USA'
|
||||||
|
@ -1,14 +1,29 @@
|
|||||||
import re
|
#!/usr/bin/env python
|
||||||
|
__license__ = 'GPL v3'
|
||||||
|
__copyright__ = '2010 elsuave'
|
||||||
|
|
||||||
from calibre.web.feeds.news import BasicNewsRecipe
|
from calibre.web.feeds.news import BasicNewsRecipe
|
||||||
class EandP(BasicNewsRecipe):
|
class EandP(BasicNewsRecipe):
|
||||||
title = u'Editor and Publisher'
|
title = u'Editor and Publisher'
|
||||||
__author__ = u'Xanthan Gum'
|
__author__ = u'elsuave (modified from Xanthan Gum)'
|
||||||
description = 'News about newspapers and journalism.'
|
description = 'News about newspapers and journalism.'
|
||||||
|
publisher = 'Editor and Publisher'
|
||||||
|
category = 'news, journalism, industry'
|
||||||
language = 'en'
|
language = 'en'
|
||||||
no_stylesheets = True
|
max_articles_per_feed = 25
|
||||||
|
no_stylesheets = True
|
||||||
|
use_embedded_content = False
|
||||||
|
encoding = 'utf8'
|
||||||
|
cover_url = 'http://www.editorandpublisher.com/images/EP_main_logo.gif'
|
||||||
|
remove_javascript = True
|
||||||
|
|
||||||
oldest_article = 7
|
html2lrf_options = [
|
||||||
max_articles_per_feed = 100
|
'--comment', description
|
||||||
|
, '--category', category
|
||||||
|
, '--publisher', publisher
|
||||||
|
]
|
||||||
|
|
||||||
|
html2epub_options = 'publisher="' + publisher + '"\ncomments="' + description + '"\ntags="' + category + '"'
|
||||||
|
|
||||||
# Font formatting code borrowed from kwetal
|
# Font formatting code borrowed from kwetal
|
||||||
|
|
||||||
@ -18,17 +33,21 @@ class EandP(BasicNewsRecipe):
|
|||||||
h2{font-size: large;}
|
h2{font-size: large;}
|
||||||
'''
|
'''
|
||||||
|
|
||||||
# Delete everything before the article
|
# Keep only div:itemmgap
|
||||||
|
|
||||||
remove_tags_before = dict(name='font', attrs={'class':'titlebar_black'})
|
keep_only_tags = [
|
||||||
|
dict(name='div', attrs={'class':'itemmgap'})
|
||||||
|
]
|
||||||
|
|
||||||
# Delete everything after the article
|
# Remove commenting/social media lins
|
||||||
|
|
||||||
preprocess_regexps = [(re.compile(r'<!--endclickprintinclude-->.*</body>', re.DOTALL|re.IGNORECASE),
|
remove_tags_after = [dict(name='div', attrs={'class':'clear'})]
|
||||||
lambda match: '</body>'),]
|
|
||||||
|
|
||||||
|
feeds = [(u'Breaking News', u'http://www.editorandpublisher.com/GenerateRssFeed.aspx'),
|
||||||
|
(u'Business News', u'http://www.editorandpublisher.com/GenerateRssFeed.aspx?CategoryId=2'),
|
||||||
|
(u'Ad/Circ News', u'http://www.editorandpublisher.com/GenerateRssFeed.aspx?CategoryId=3'),
|
||||||
|
(u'Newsroom', u'http://www.editorandpublisher.com/GenerateRssFeed.aspx?CategoryId=4'),
|
||||||
|
(u'Technology News', u'http://www.editorandpublisher.com/GenerateRssFeed.aspx?CategoryId=5'),
|
||||||
|
(u'Syndicates News', u'http://www.editorandpublisher.com/GenerateRssFeed.aspx?CategoryId=7')]
|
||||||
|
|
||||||
feeds = [(u'Breaking News', u'http://feeds.feedburner.com/EditorAndPublisher-BreakingNews'),
|
|
||||||
(u'Business News', u'http://feeds.feedburner.com/EditorAndPublisher-BusinessNews'),
|
|
||||||
(u'Newsroom', u'http://feeds.feedburner.com/EditorAndPublisher-Newsroom'),
|
|
||||||
(u'Technology News', u'http://feeds.feedburner.com/EditorAndPublisher-Technology'),
|
|
||||||
(u'Syndicates News', u'http://feeds.feedburner.com/EditorAndPublisher-Syndicates')]
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
|
|
||||||
__license__ = 'GPL v3'
|
__license__ = 'GPL v3'
|
||||||
__copyright__ = '2009, Darko Miletic <darko.miletic at gmail.com>'
|
__copyright__ = '2010, elsuave'
|
||||||
'''
|
'''
|
||||||
estadao.com.br
|
estadao.com.br
|
||||||
'''
|
'''
|
||||||
@ -10,12 +10,12 @@ from calibre.web.feeds.news import BasicNewsRecipe
|
|||||||
|
|
||||||
class Estadao(BasicNewsRecipe):
|
class Estadao(BasicNewsRecipe):
|
||||||
title = 'O Estado de S. Paulo'
|
title = 'O Estado de S. Paulo'
|
||||||
__author__ = 'Darko Miletic'
|
__author__ = 'elsuave (modified from Darko Miletic)'
|
||||||
description = 'News from Brasil in Portuguese'
|
description = 'News from Brasil in Portuguese'
|
||||||
publisher = 'O Estado de S. Paulo'
|
publisher = 'O Estado de S. Paulo'
|
||||||
category = 'news, politics, Brasil'
|
category = 'news, politics, Brasil'
|
||||||
oldest_article = 2
|
oldest_article = 2
|
||||||
max_articles_per_feed = 100
|
max_articles_per_feed = 25
|
||||||
no_stylesheets = True
|
no_stylesheets = True
|
||||||
use_embedded_content = False
|
use_embedded_content = False
|
||||||
encoding = 'utf8'
|
encoding = 'utf8'
|
||||||
@ -30,13 +30,14 @@ class Estadao(BasicNewsRecipe):
|
|||||||
|
|
||||||
html2epub_options = 'publisher="' + publisher + '"\ncomments="' + description + '"\ntags="' + category + '"'
|
html2epub_options = 'publisher="' + publisher + '"\ncomments="' + description + '"\ntags="' + category + '"'
|
||||||
|
|
||||||
keep_only_tags = [dict(name='div', attrs={'id':'c1'})]
|
keep_only_tags = [
|
||||||
|
dict(name='div', attrs={'class':['bb-md-noticia','c5']})
|
||||||
|
]
|
||||||
|
|
||||||
remove_tags = [
|
remove_tags = [
|
||||||
dict(name=['script','object','form','ul'])
|
dict(name=['script','object','form','ul'])
|
||||||
,dict(name='div', attrs={'id':['votacao','estadaohoje']})
|
,dict(name='div', attrs={'class':['fnt2 Color_04 bold','right fnt2 innerTop15 dvTmFont','™_01 right outerLeft15','tituloBox','tags']})
|
||||||
,dict(name='p', attrs={'id':'ctrl_texto'})
|
,dict(name='div', attrs={'id':['bb-md-noticia-subcom']})
|
||||||
,dict(name='p', attrs={'class':'texto'})
|
|
||||||
]
|
]
|
||||||
|
|
||||||
feeds = [
|
feeds = [
|
||||||
@ -51,13 +52,12 @@ class Estadao(BasicNewsRecipe):
|
|||||||
,(u'Vida &', u'http://www.estadao.com.br/rss/vidae.xml')
|
,(u'Vida &', u'http://www.estadao.com.br/rss/vidae.xml')
|
||||||
]
|
]
|
||||||
|
|
||||||
def preprocess_html(self, soup):
|
|
||||||
ifr = soup.find('iframe')
|
|
||||||
if ifr:
|
|
||||||
ifr.extract()
|
|
||||||
for item in soup.findAll(style=True):
|
|
||||||
del item['style']
|
|
||||||
return soup
|
|
||||||
|
|
||||||
language = 'pt'
|
language = 'pt'
|
||||||
|
|
||||||
|
def get_article_url(self, article):
|
||||||
|
url = BasicNewsRecipe.get_article_url(self, article)
|
||||||
|
if '/Multimidia/' not in url:
|
||||||
|
return url
|
||||||
|
|
||||||
|
45
resources/recipes/foreign_policy.recipe
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
__license__ = 'GPL v3'
|
||||||
|
__copyright__ = '2010, Darko Miletic <darko.miletic at gmail.com>'
|
||||||
|
'''
|
||||||
|
www.foreignpolicy.com
|
||||||
|
'''
|
||||||
|
|
||||||
|
from calibre.web.feeds.news import BasicNewsRecipe
|
||||||
|
|
||||||
|
class ForeignPolicy(BasicNewsRecipe):
|
||||||
|
title = 'Foreign Policy'
|
||||||
|
__author__ = 'Darko Miletic'
|
||||||
|
description = 'International News'
|
||||||
|
publisher = 'Washingtonpost.Newsweek Interactive, LLC'
|
||||||
|
category = 'news, politics, USA'
|
||||||
|
oldest_article = 31
|
||||||
|
max_articles_per_feed = 200
|
||||||
|
no_stylesheets = True
|
||||||
|
encoding = 'utf8'
|
||||||
|
use_embedded_content = False
|
||||||
|
language = 'en'
|
||||||
|
remove_empty_feeds = True
|
||||||
|
extra_css = ' body{font-family: Georgia,"Times New Roman",Times,serif } img{margin-bottom: 0.4em} h1,h2,h3,h4,h5,h6{font-family: Arial,Helvetica,sans-serif} '
|
||||||
|
|
||||||
|
conversion_options = {
|
||||||
|
'comment' : description
|
||||||
|
, 'tags' : category
|
||||||
|
, 'publisher' : publisher
|
||||||
|
, 'language' : language
|
||||||
|
}
|
||||||
|
|
||||||
|
keep_only_tags = [dict(attrs={'id':['art-mast','art-body','auth-bio']})]
|
||||||
|
remove_tags = [dict(name='iframe'),dict(attrs={'id':['share-box','base-ad']})]
|
||||||
|
remove_attributes = ['height','width']
|
||||||
|
|
||||||
|
|
||||||
|
feeds = [(u'Articles', u'http://www.foreignpolicy.com/node/feed')]
|
||||||
|
|
||||||
|
def print_version(self, url):
|
||||||
|
return url + '?print=yes&page=full'
|
||||||
|
|
||||||
|
def preprocess_html(self, soup):
|
||||||
|
for item in soup.findAll(style=True):
|
||||||
|
del item['style']
|
||||||
|
return soup
|
||||||
|
|
@ -1,56 +1,95 @@
|
|||||||
__license__ = 'GPL v3'
|
__license__ = 'GPL v3'
|
||||||
__copyright__ = '2010, Darko Miletic <darko.miletic at gmail.com>'
|
__copyright__ = '2010, Darko Miletic <darko.miletic at gmail.com>'
|
||||||
'''
|
'''
|
||||||
haaretz.com
|
www.haaretz.com
|
||||||
'''
|
'''
|
||||||
|
|
||||||
|
import re
|
||||||
|
from calibre import strftime
|
||||||
|
from time import gmtime
|
||||||
from calibre.web.feeds.news import BasicNewsRecipe
|
from calibre.web.feeds.news import BasicNewsRecipe
|
||||||
|
|
||||||
class Haaretz_en(BasicNewsRecipe):
|
class HaaretzPrint_en(BasicNewsRecipe):
|
||||||
title = 'Haaretz in English'
|
title = 'Haaretz - print edition'
|
||||||
__author__ = 'Darko Miletic'
|
__author__ = 'Darko Miletic'
|
||||||
description = 'Haaretz.com, the online edition of Haaretz Newspaper in Israel, and analysis from Israel and the Middle East. Haaretz.com provides extensive and in-depth coverage of Israel, the Jewish World and the Middle East, including defense, diplomacy, the Arab-Israeli conflict, the peace process, Israeli politics, Jerusalem affairs, international relations, Iran, Iraq, Syria, Lebanon, the Palestinian Authority, the West Bank and the Gaza Strip, the Israeli business world and Jewish life in Israel and the Diaspora. '
|
description = "Haaretz.com is the world's leading English-language Website for real-time news and analysis of Israel and the Middle East."
|
||||||
publisher = 'haaretz.com'
|
publisher = 'Haaretz'
|
||||||
category = 'news, politics, Israel'
|
category = "news, Haaretz, Israel news, Israel newspapers, Israel business news, Israel financial news, Israeli news,Israeli newspaper, Israeli newspapers, news from Israel, news in Israel, news Israel, news on Israel, newspaper Israel, Israel sports news, Israel diplomacy news"
|
||||||
oldest_article = 2
|
oldest_article = 2
|
||||||
max_articles_per_feed = 200
|
max_articles_per_feed = 200
|
||||||
no_stylesheets = True
|
no_stylesheets = True
|
||||||
encoding = 'cp1252'
|
encoding = 'utf8'
|
||||||
use_embedded_content = False
|
use_embedded_content = False
|
||||||
language = 'en_IL'
|
language = 'en_IL'
|
||||||
publication_type = 'newspaper'
|
publication_type = 'newspaper'
|
||||||
remove_empty_feeds = True
|
PREFIX = 'http://www.haaretz.com'
|
||||||
masthead_url = 'http://www.haaretz.com/images/logos/logoGrey.gif'
|
masthead_url = PREFIX + '/images/logos/logoGrey.gif'
|
||||||
extra_css = ' body{font-family: Verdana,Arial,Helvetica,sans-serif } '
|
extra_css = ' body{font-family: Verdana,Arial,Helvetica,sans-serif } '
|
||||||
|
|
||||||
|
preprocess_regexps = [(re.compile(r'</body>.*?</html>', re.DOTALL|re.IGNORECASE),lambda match: '</body></html>')]
|
||||||
|
|
||||||
conversion_options = {
|
conversion_options = {
|
||||||
'comment' : description
|
'comment' : description
|
||||||
, 'tags' : category
|
, 'tags' : category
|
||||||
, 'publisher' : publisher
|
, 'publisher': publisher
|
||||||
, 'language' : language
|
, 'language' : language
|
||||||
}
|
}
|
||||||
|
|
||||||
remove_tags = [dict(name='div', attrs={'class':['rightcol']}),dict(name='table')]
|
keep_only_tags = [dict(attrs={'id':'threecolumns'})]
|
||||||
remove_tags_before = dict(name='h1')
|
remove_attributes = ['width','height']
|
||||||
remove_tags_after = dict(attrs={'id':'innerArticle'})
|
remove_tags = [
|
||||||
keep_only_tags = [dict(attrs={'id':'content'})]
|
dict(name=['iframe','link','object','embed'])
|
||||||
|
,dict(name='div',attrs={'class':'rightcol'})
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
feeds = [
|
feeds = [
|
||||||
(u'Opinion' , u'http://www.haaretz.com/cmlink/opinion-rss-1.209234?localLinksEnabled=false' )
|
(u'News' , PREFIX + u'/print-edition/news' )
|
||||||
,(u'Defense and diplomacy' , u'http://www.haaretz.com/cmlink/defense-and-diplomacy-rss-1.208894?localLinksEnabled=false')
|
,(u'Opinion' , PREFIX + u'/print-edition/opinion' )
|
||||||
,(u'National' , u'http://www.haaretz.com/cmlink/national-rss-1.208896?localLinksEnabled=false' )
|
,(u'Business' , PREFIX + u'/print-edition/business' )
|
||||||
,(u'International' , u'http://www.haaretz.com/cmlink/international-rss-1.208898?localLinksEnabled=false' )
|
,(u'Real estate' , PREFIX + u'/print-edition/real-estate' )
|
||||||
,(u'Jewish World' , u'http://www.haaretz.com/cmlink/jewish-world-rss-1.209085?localLinksEnabled=false' )
|
,(u'Sports' , PREFIX + u'/print-edition/sports' )
|
||||||
,(u'Business' , u'http://www.haaretz.com/cmlink/business-print-rss-1.264904?localLinksEnabled=false' )
|
,(u'Travel' , PREFIX + u'/print-edition/travel' )
|
||||||
,(u'Real Estate' , u'http://www.haaretz.com/cmlink/real-estate-print-rss-1.264977?localLinksEnabled=false' )
|
,(u'Books' , PREFIX + u'/print-edition/books' )
|
||||||
,(u'Features' , u'http://www.haaretz.com/cmlink/features-print-rss-1.264912?localLinksEnabled=false' )
|
,(u'Food & Wine' , PREFIX + u'/print-edition/food-wine' )
|
||||||
,(u'Arts and leisure' , u'http://www.haaretz.com/cmlink/arts-and-leisure-rss-1.286090?localLinksEnabled=false' )
|
,(u'Arts & Leisure', PREFIX + u'/print-edition/arts-leisure' )
|
||||||
,(u'Books' , u'http://www.haaretz.com/cmlink/books-rss-1.264947?localLinksEnabled=false' )
|
,(u'Features' , PREFIX + u'/print-edition/features' )
|
||||||
,(u'Food and Wine' , u'http://www.haaretz.com/cmlink/food-and-wine-print-rss-1.265034?localLinksEnabled=false' )
|
|
||||||
,(u'Sports' , u'http://www.haaretz.com/cmlink/sports-rss-1.286092?localLinksEnabled=false' )
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def print_version(self, url):
|
||||||
|
article = url.rpartition('/')[2]
|
||||||
|
return 'http://www.haaretz.com/misc/article-print-page/' + article
|
||||||
|
|
||||||
|
def parse_index(self):
|
||||||
|
totalfeeds = []
|
||||||
|
lfeeds = self.get_feeds()
|
||||||
|
for feedobj in lfeeds:
|
||||||
|
feedtitle, feedurl = feedobj
|
||||||
|
self.report_progress(0, _('Fetching feed')+' %s...'%(feedtitle if feedtitle else feedurl))
|
||||||
|
articles = []
|
||||||
|
soup = self.index_to_soup(feedurl)
|
||||||
|
for item in soup.findAll(attrs={'class':'text'}):
|
||||||
|
sp = item.find('span',attrs={'class':'h3 font-weight-normal'})
|
||||||
|
desc = item.find('p')
|
||||||
|
description = ''
|
||||||
|
if sp:
|
||||||
|
if desc:
|
||||||
|
description = self.tag_to_string(desc)
|
||||||
|
link = sp.a
|
||||||
|
url = self.PREFIX + link['href']
|
||||||
|
title = self.tag_to_string(link)
|
||||||
|
times = strftime('%a, %d %b %Y %H:%M:%S +0000',gmtime())
|
||||||
|
articles.append({
|
||||||
|
'title' :title
|
||||||
|
,'date' :times
|
||||||
|
,'url' :url
|
||||||
|
,'description':description
|
||||||
|
})
|
||||||
|
totalfeeds.append((feedtitle, articles))
|
||||||
|
return totalfeeds
|
||||||
|
|
||||||
|
|
||||||
def preprocess_html(self, soup):
|
def preprocess_html(self, soup):
|
||||||
for item in soup.findAll(style=True):
|
for item in soup.findAll(style=True):
|
||||||
del item['style']
|
del item['style']
|
||||||
|
50
resources/recipes/ifzm.recipe
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
from calibre.web.feeds.news import BasicNewsRecipe
|
||||||
|
|
||||||
|
class AdvancedUserRecipe1277305250(BasicNewsRecipe):
|
||||||
|
title = u'infzm - China Southern Weekly'
|
||||||
|
oldest_article = 14
|
||||||
|
max_articles_per_feed = 100
|
||||||
|
|
||||||
|
feeds = [(u'\u5357\u65b9\u5468\u672b-\u70ed\u70b9\u65b0\u95fb', u'http://www.infzm.com/rss/home/rss2.0.xml'),
|
||||||
|
(u'\u5357\u65b9\u5468\u672b-\u7ecf\u6d4e\u65b0\u95fb', u'http://www.infzm.com/rss/economic.xml'),
|
||||||
|
(u'\u5357\u65b9\u5468\u672b-\u6587\u5316\u65b0\u95fb', u'http://www.infzm.com/rss/culture.xml'),
|
||||||
|
(u'\u5357\u65b9\u5468\u672b-\u751f\u6d3b\u65f6\u5c1a', u'http://www.infzm.com/rss/lifestyle.xml'),
|
||||||
|
(u'\u5357\u65b9\u5468\u672b-\u89c2\u70b9', u'http://www.infzm.com/rss/opinion.xml')
|
||||||
|
]
|
||||||
|
__author__ = 'rty'
|
||||||
|
__version__ = '1.0'
|
||||||
|
language = 'zh'
|
||||||
|
pubisher = 'http://www.infzm.com'
|
||||||
|
description = 'Chinese Weekly Tabloid'
|
||||||
|
category = 'News, China'
|
||||||
|
remove_javascript = True
|
||||||
|
use_embedded_content = False
|
||||||
|
no_stylesheets = True
|
||||||
|
#encoding = 'GB2312'
|
||||||
|
encoding = 'UTF-8'
|
||||||
|
conversion_options = {'linearize_tables':True}
|
||||||
|
masthead_url = 'http://i50.tinypic.com/2qmfb7l.jpg'
|
||||||
|
|
||||||
|
extra_css = '''
|
||||||
|
@font-face { font-family: "DroidFont", serif, sans-serif; src: url(res:///system/fonts/DroidSansFallback.ttf); }\n
|
||||||
|
body {
|
||||||
|
margin-right: 8pt;
|
||||||
|
font-family: 'DroidFont', serif;}
|
||||||
|
.detailContent {font-family: 'DroidFont', serif, sans-serif}
|
||||||
|
'''
|
||||||
|
|
||||||
|
keep_only_tags = [
|
||||||
|
dict(name='div', attrs={'id':'detailContent'}),
|
||||||
|
]
|
||||||
|
remove_tags = [
|
||||||
|
dict(name='div', attrs={'id':['detailTools', 'detailSideL', 'pageNum']}),
|
||||||
|
]
|
||||||
|
remove_tags_after = [
|
||||||
|
dict(name='div', attrs={'id':'pageNum'}),
|
||||||
|
]
|
||||||
|
def preprocess_html(self, soup):
|
||||||
|
for item in soup.findAll(color=True):
|
||||||
|
del item['font']
|
||||||
|
for item in soup.findAll(style=True):
|
||||||
|
del item['style']
|
||||||
|
return soup
|
@ -89,6 +89,7 @@ class NYTimes(BasicNewsRecipe):
|
|||||||
'relatedSearchesModule',
|
'relatedSearchesModule',
|
||||||
'side_tool',
|
'side_tool',
|
||||||
'singleAd',
|
'singleAd',
|
||||||
|
'subNavigation clearfix',
|
||||||
'subNavigation tabContent active',
|
'subNavigation tabContent active',
|
||||||
'subNavigation tabContent active clearfix',
|
'subNavigation tabContent active clearfix',
|
||||||
]}),
|
]}),
|
||||||
@ -460,8 +461,10 @@ class NYTimes(BasicNewsRecipe):
|
|||||||
if mp_off >= 0:
|
if mp_off >= 0:
|
||||||
c = c[:mp_off]
|
c = c[:mp_off]
|
||||||
emTag.insert(0, c)
|
emTag.insert(0, c)
|
||||||
hrTag = Tag(soup, 'hr')
|
#hrTag = Tag(soup, 'hr')
|
||||||
#hrTag['style'] = "margin-top:0em;margin-bottom:0em"
|
#hrTag['class'] = 'caption_divider'
|
||||||
|
hrTag = Tag(soup, 'div')
|
||||||
|
hrTag['class'] = 'divider'
|
||||||
emTag.insert(1, hrTag)
|
emTag.insert(1, hrTag)
|
||||||
caption.replaceWith(emTag)
|
caption.replaceWith(emTag)
|
||||||
|
|
||||||
|
@ -76,6 +76,7 @@ class NYTimes(BasicNewsRecipe):
|
|||||||
'relatedSearchesModule',
|
'relatedSearchesModule',
|
||||||
'side_tool',
|
'side_tool',
|
||||||
'singleAd',
|
'singleAd',
|
||||||
|
'subNavigation clearfix',
|
||||||
'subNavigation tabContent active',
|
'subNavigation tabContent active',
|
||||||
'subNavigation tabContent active clearfix',
|
'subNavigation tabContent active clearfix',
|
||||||
]}),
|
]}),
|
||||||
@ -335,7 +336,7 @@ class NYTimes(BasicNewsRecipe):
|
|||||||
self.log(">>> No class:'columnGroup first' found <<<")
|
self.log(">>> No class:'columnGroup first' found <<<")
|
||||||
# Change class="kicker" to <h3>
|
# Change class="kicker" to <h3>
|
||||||
kicker = soup.find(True, {'class':'kicker'})
|
kicker = soup.find(True, {'class':'kicker'})
|
||||||
if kicker and kicker.contents[0]:
|
if kicker and kicker.contents and kicker.contents[0]:
|
||||||
h3Tag = Tag(soup, "h3")
|
h3Tag = Tag(soup, "h3")
|
||||||
h3Tag.insert(0, self.fixChars(self.tag_to_string(kicker,
|
h3Tag.insert(0, self.fixChars(self.tag_to_string(kicker,
|
||||||
use_alt=False)))
|
use_alt=False)))
|
||||||
@ -350,8 +351,10 @@ class NYTimes(BasicNewsRecipe):
|
|||||||
if mp_off >= 0:
|
if mp_off >= 0:
|
||||||
c = c[:mp_off]
|
c = c[:mp_off]
|
||||||
emTag.insert(0, c)
|
emTag.insert(0, c)
|
||||||
hrTag = Tag(soup, 'hr')
|
#hrTag = Tag(soup, 'hr')
|
||||||
#hrTag['style'] = "margin-top:0em;margin-bottom:0em"
|
#hrTag['class'] = 'caption_divider'
|
||||||
|
hrTag = Tag(soup, 'div')
|
||||||
|
hrTag['class'] = 'divider'
|
||||||
emTag.insert(1, hrTag)
|
emTag.insert(1, hrTag)
|
||||||
caption.replaceWith(emTag)
|
caption.replaceWith(emTag)
|
||||||
|
|
||||||
@ -457,8 +460,10 @@ class NYTimes(BasicNewsRecipe):
|
|||||||
return self.massageNCXText(self.tag_to_string(p,use_alt=False))
|
return self.massageNCXText(self.tag_to_string(p,use_alt=False))
|
||||||
return None
|
return None
|
||||||
|
|
||||||
article.author = extract_author(soup)
|
if not article.author:
|
||||||
article.summary = article.text_summary = extract_description(soup)
|
article.author = extract_author(soup)
|
||||||
|
if not article.summary:
|
||||||
|
article.summary = article.text_summary = extract_description(soup)
|
||||||
|
|
||||||
def strip_anchors(self,soup):
|
def strip_anchors(self,soup):
|
||||||
paras = soup.findAll(True)
|
paras = soup.findAll(True)
|
||||||
|
79
resources/recipes/singtao_daily.recipe
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
from calibre.web.feeds.recipes import BasicNewsRecipe
|
||||||
|
|
||||||
|
class AdvancedUserRecipe1278063072(BasicNewsRecipe):
|
||||||
|
title = u'Singtao Daily - Canada'
|
||||||
|
oldest_article = 7
|
||||||
|
max_articles_per_feed = 100
|
||||||
|
__author__ = 'rty'
|
||||||
|
description = 'Toronto Canada Chinese Newspaper'
|
||||||
|
publisher = 'news.singtao.ca'
|
||||||
|
category = 'Chinese, News, Canada'
|
||||||
|
remove_javascript = True
|
||||||
|
use_embedded_content = False
|
||||||
|
no_stylesheets = True
|
||||||
|
language = 'zh'
|
||||||
|
conversion_options = {'linearize_tables':True}
|
||||||
|
masthead_url = 'http://news.singtao.ca/i/site_2009/logo.jpg'
|
||||||
|
extra_css = '''
|
||||||
|
@font-face {font-family: "DroidFont", serif, sans-serif; src: url(res:///system/fonts/DroidSansFallback.ttf); }\
|
||||||
|
|
||||||
|
body {text-align: justify; margin-right: 8pt; font-family: 'DroidFont', serif;}\
|
||||||
|
|
||||||
|
h1 {font-family: 'DroidFont', serif;}\
|
||||||
|
|
||||||
|
.articledescription {font-family: 'DroidFont', serif;}
|
||||||
|
'''
|
||||||
|
keep_only_tags = [
|
||||||
|
dict(name='div', attrs={'id':['title','storybody']}),
|
||||||
|
dict(name='div', attrs={'class':'content'})
|
||||||
|
]
|
||||||
|
|
||||||
|
def parse_index(self):
|
||||||
|
feeds = []
|
||||||
|
for title, url in [
|
||||||
|
('Editorial',
|
||||||
|
'http://news.singtao.ca/toronto/editorial.html'),
|
||||||
|
('Toronto \xe5\x9f\x8e\xe5\xb8\x82/\xe7\xa4\xbe\xe5\x8d\x80'.decode('utf-8'),
|
||||||
|
'http://news.singtao.ca/toronto/city.html'),
|
||||||
|
('Canada \xe5\x8a\xa0\xe5\x9c\x8b'.decode('utf-8'),
|
||||||
|
'http://news.singtao.ca/toronto/canada.html'),
|
||||||
|
('Entertainment',
|
||||||
|
'http://news.singtao.ca/toronto/entertainment.html'),
|
||||||
|
('World',
|
||||||
|
'http://news.singtao.ca/toronto/world.html'),
|
||||||
|
('Finance \xe5\x9c\x8b\xe9\x9a\x9b\xe8\xb2\xa1\xe7\xb6\x93'.decode('utf-8'),
|
||||||
|
'http://news.singtao.ca/toronto/finance.html'),
|
||||||
|
('Sports', 'http://news.singtao.ca/toronto/sports.html'),
|
||||||
|
]:
|
||||||
|
articles = self.parse_section(url)
|
||||||
|
if articles:
|
||||||
|
feeds.append((title, articles))
|
||||||
|
return feeds
|
||||||
|
|
||||||
|
def parse_section(self, url):
|
||||||
|
soup = self.index_to_soup(url)
|
||||||
|
div = soup.find(attrs={'class': ['newslist paddingL10T10','newslist3 paddingL10T10']})
|
||||||
|
#date = div.find(attrs={'class': 'underlineBLK'})
|
||||||
|
current_articles = []
|
||||||
|
for li in div.findAll('li'):
|
||||||
|
a = li.find('a', href = True)
|
||||||
|
if a is None:
|
||||||
|
continue
|
||||||
|
title = self.tag_to_string(a)
|
||||||
|
url = a.get('href', False)
|
||||||
|
if not url or not title:
|
||||||
|
continue
|
||||||
|
if url.startswith('/'):
|
||||||
|
url = 'http://news.singtao.ca'+url
|
||||||
|
# self.log('\ \ Found article:', title)
|
||||||
|
# self.log('\ \ \ ', url)
|
||||||
|
current_articles.append({'title': title, 'url': url, 'description':''})
|
||||||
|
|
||||||
|
return current_articles
|
||||||
|
|
||||||
|
def preprocess_html(self, soup):
|
||||||
|
for item in soup.findAll(style=True):
|
||||||
|
del item['style']
|
||||||
|
for item in soup.findAll(width=True):
|
||||||
|
del item['width']
|
||||||
|
return soup
|
35
resources/recipes/statesman.recipe
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
from calibre.web.feeds.news import BasicNewsRecipe
|
||||||
|
|
||||||
|
class AdvancedUserRecipe1278049615(BasicNewsRecipe):
|
||||||
|
title = u'Statesman'
|
||||||
|
pubisher = 'http://www.statesman.com/'
|
||||||
|
description = 'Austin Texas Daily Newspaper'
|
||||||
|
category = 'News, Austin, Texas'
|
||||||
|
__author__ = 'rty'
|
||||||
|
oldest_article = 3
|
||||||
|
|
||||||
|
max_articles_per_feed = 100
|
||||||
|
|
||||||
|
feeds = [(u'News', u'http://www.statesman.com/section-rss.do?source=news&includeSubSections=true'),
|
||||||
|
(u'Business', u'http://www.statesman.com/section-rss.do?source=business&includeSubSections=true'),
|
||||||
|
(u'Life', u'http://www.statesman.com/section-rss.do?source=life&includesubsection=true'),
|
||||||
|
(u'Editorial', u'http://www.statesman.com/section-rss.do?source=opinion&includesubsections=true'),
|
||||||
|
(u'Sports', u'http://www.statesman.com/section-rss.do?source=sports&includeSubSections=true')
|
||||||
|
]
|
||||||
|
masthead_url = "http://www.statesman.com/images/cmg-logo.gif"
|
||||||
|
#temp_files = []
|
||||||
|
#articles_are_obfuscated = True
|
||||||
|
|
||||||
|
remove_javascript = True
|
||||||
|
use_embedded_content = False
|
||||||
|
no_stylesheets = True
|
||||||
|
language = 'en'
|
||||||
|
encoding = 'utf-8'
|
||||||
|
conversion_options = {'linearize_tables':True}
|
||||||
|
remove_tags = [
|
||||||
|
dict(name='div', attrs={'id':'cxArticleOptions'}),
|
||||||
|
]
|
||||||
|
keep_only_tags = [
|
||||||
|
dict(name='div', attrs={'class':'cxArticleHeader'}),
|
||||||
|
dict(name='div', attrs={'id':'cxArticleBodyText'}),
|
||||||
|
]
|
@ -40,13 +40,14 @@ class LinuxFreeze(Command):
|
|||||||
'/usr/bin/pdftohtml',
|
'/usr/bin/pdftohtml',
|
||||||
'/usr/lib/libwmflite-0.2.so.7',
|
'/usr/lib/libwmflite-0.2.so.7',
|
||||||
'/usr/lib/liblcms.so.1',
|
'/usr/lib/liblcms.so.1',
|
||||||
|
'/usr/lib/libstlport.so.5.1',
|
||||||
'/tmp/calibre-mount-helper',
|
'/tmp/calibre-mount-helper',
|
||||||
'/usr/lib/libunrar.so',
|
'/usr/lib/libunrar.so',
|
||||||
'/usr/lib/libchm.so.0',
|
'/usr/lib/libchm.so.0',
|
||||||
'/usr/lib/libsqlite3.so.0',
|
'/usr/lib/libsqlite3.so.0',
|
||||||
'/usr/lib/libsqlite3.so.0',
|
'/usr/lib/libsqlite3.so.0',
|
||||||
'/usr/lib/libmng.so.1',
|
'/usr/lib/libmng.so.1',
|
||||||
'/usr/lib/libpodofo.so.0.6.99',
|
'/usr/lib/libpodofo.so.0.8.1',
|
||||||
'/lib/libz.so.1',
|
'/lib/libz.so.1',
|
||||||
'/lib/libuuid.so.1',
|
'/lib/libuuid.so.1',
|
||||||
'/usr/lib/libtiff.so.3',
|
'/usr/lib/libtiff.so.3',
|
||||||
|
@ -265,6 +265,9 @@ class Py2App(object):
|
|||||||
@flush
|
@flush
|
||||||
def get_local_dependencies(self, path_to_lib):
|
def get_local_dependencies(self, path_to_lib):
|
||||||
for x in self.get_dependencies(path_to_lib):
|
for x in self.get_dependencies(path_to_lib):
|
||||||
|
if x.startswith('libpodofo'):
|
||||||
|
yield x, x
|
||||||
|
continue
|
||||||
for y in (SW+'/lib/', '/usr/local/lib/', SW+'/qt/lib/',
|
for y in (SW+'/lib/', '/usr/local/lib/', SW+'/qt/lib/',
|
||||||
'/opt/local/lib/',
|
'/opt/local/lib/',
|
||||||
'/Library/Frameworks/Python.framework/', SW+'/freetype/lib/'):
|
'/Library/Frameworks/Python.framework/', SW+'/freetype/lib/'):
|
||||||
@ -397,7 +400,7 @@ class Py2App(object):
|
|||||||
@flush
|
@flush
|
||||||
def add_podofo(self):
|
def add_podofo(self):
|
||||||
info('\nAdding PoDoFo')
|
info('\nAdding PoDoFo')
|
||||||
pdf = join(SW, 'lib', 'libpodofo.0.6.99.dylib')
|
pdf = join(SW, 'lib', 'libpodofo.0.8.1.dylib')
|
||||||
self.install_dylib(pdf)
|
self.install_dylib(pdf)
|
||||||
|
|
||||||
@flush
|
@flush
|
||||||
|
@ -162,9 +162,50 @@ SET(WANT_LIB64 FALSE)
|
|||||||
SET(PODOFO_BUILD_SHARED TRUE)
|
SET(PODOFO_BUILD_SHARED TRUE)
|
||||||
SET(PODOFO_BUILD_STATIC FALSE)
|
SET(PODOFO_BUILD_STATIC FALSE)
|
||||||
|
|
||||||
cp build/podofo-0.7.0/build/src/Release/podofo.dll bin/
|
cp build/podofo/build/src/Release/podofo.dll bin/
|
||||||
cp build/podofo-0.7.0/build/src/Release/podofo.lib lib/
|
cp build/podofo/build/src/Release/podofo.lib lib/
|
||||||
cp build/podofo-0.7.0/build/src/Release/podofo.exp lib/
|
cp build/podofo/build/src/Release/podofo.exp lib/
|
||||||
|
|
||||||
|
cp build/podofo/build/podofo_config.h include/podofo/
|
||||||
|
cp -r build/podofo/src/* include/podofo/
|
||||||
|
|
||||||
|
The following patch was required to get it to compile:
|
||||||
|
|
||||||
|
Index: src/PdfImage.cpp
|
||||||
|
===================================================================
|
||||||
|
--- src/PdfImage.cpp (revision 1261)
|
||||||
|
+++ src/PdfImage.cpp (working copy)
|
||||||
|
@@ -627,7 +627,7 @@
|
||||||
|
|
||||||
|
long lLen = static_cast<long>(pInfo->rowbytes * height);
|
||||||
|
char* pBuffer = static_cast<char*>(malloc(sizeof(char) * lLen));
|
||||||
|
- png_bytep pRows[height];
|
||||||
|
+ png_bytepp pRows = static_cast<png_bytepp>(malloc(sizeof(png_bytep)*height));
|
||||||
|
for(int y=0; y<height; y++)
|
||||||
|
{
|
||||||
|
pRows[y] = reinterpret_cast<png_bytep>(pBuffer + (y * pInfo->rowbytes));
|
||||||
|
@@ -672,6 +672,7 @@
|
||||||
|
this->SetImageData( width, height, pInfo->bit_depth, &stream );
|
||||||
|
|
||||||
|
free(pBuffer);
|
||||||
|
+ free(pRows);
|
||||||
|
}
|
||||||
|
#endif // PODOFO_HAVE_PNG_LIB
|
||||||
|
|
||||||
|
Index: src/PdfFiltersPrivate.cpp
|
||||||
|
===================================================================
|
||||||
|
--- src/PdfFiltersPrivate.cpp (revision 1261)
|
||||||
|
+++ src/PdfFiltersPrivate.cpp (working copy)
|
||||||
|
@@ -1019,7 +1019,7 @@
|
||||||
|
/*
|
||||||
|
* Prepare for input from a memory buffer.
|
||||||
|
*/
|
||||||
|
-GLOBAL(void)
|
||||||
|
+void
|
||||||
|
jpeg_memory_src (j_decompress_ptr cinfo, const JOCTET * buffer, size_t bufsize)
|
||||||
|
{
|
||||||
|
my_src_ptr src;
|
||||||
|
|
||||||
|
|
||||||
ImageMagick
|
ImageMagick
|
||||||
--------------
|
--------------
|
||||||
|
@ -153,18 +153,10 @@
|
|||||||
<Property Id="WixShellExecTarget" Value="[#{exe_map[calibre]}]" />
|
<Property Id="WixShellExecTarget" Value="[#{exe_map[calibre]}]" />
|
||||||
<CustomAction Id="LaunchApplication" BinaryKey="WixCA"
|
<CustomAction Id="LaunchApplication" BinaryKey="WixCA"
|
||||||
DllEntry="WixShellExec" Impersonate="yes"/>
|
DllEntry="WixShellExec" Impersonate="yes"/>
|
||||||
<InstallExecuteSequence>
|
|
||||||
<FileCost Suppress="yes" />
|
|
||||||
</InstallExecuteSequence>
|
|
||||||
<InstallUISequence>
|
<InstallUISequence>
|
||||||
<FileCost Suppress="yes" />
|
<FileCost Suppress="yes" />
|
||||||
</InstallUISequence>
|
</InstallUISequence>
|
||||||
<AdminExecuteSequence>
|
|
||||||
<FileCost Suppress="yes" />
|
|
||||||
</AdminExecuteSequence>
|
|
||||||
<AdminUISequence>
|
|
||||||
<FileCost Suppress="yes" />
|
|
||||||
</AdminUISequence>
|
|
||||||
|
|
||||||
</Product>
|
</Product>
|
||||||
</Wix>
|
</Wix>
|
||||||
|
@ -342,13 +342,6 @@ def detect_ncpus():
|
|||||||
return ans
|
return ans
|
||||||
|
|
||||||
|
|
||||||
def launch(path_or_url):
|
|
||||||
from PyQt4.QtCore import QUrl
|
|
||||||
from PyQt4.QtGui import QDesktopServices
|
|
||||||
if os.path.exists(path_or_url):
|
|
||||||
path_or_url = 'file:'+path_or_url
|
|
||||||
QDesktopServices.openUrl(QUrl(path_or_url))
|
|
||||||
|
|
||||||
relpath = os.path.relpath
|
relpath = os.path.relpath
|
||||||
_spat = re.compile(r'^the\s+|^a\s+|^an\s+', re.IGNORECASE)
|
_spat = re.compile(r'^the\s+|^a\s+|^an\s+', re.IGNORECASE)
|
||||||
def english_sort(x, y):
|
def english_sort(x, y):
|
||||||
|
@ -2,7 +2,7 @@ __license__ = 'GPL v3'
|
|||||||
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
|
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
|
||||||
__docformat__ = 'restructuredtext en'
|
__docformat__ = 'restructuredtext en'
|
||||||
__appname__ = 'calibre'
|
__appname__ = 'calibre'
|
||||||
__version__ = '0.7.6'
|
__version__ = '0.7.7'
|
||||||
__author__ = "Kovid Goyal <kovid@kovidgoyal.net>"
|
__author__ = "Kovid Goyal <kovid@kovidgoyal.net>"
|
||||||
|
|
||||||
import re
|
import re
|
||||||
|
@ -30,6 +30,7 @@ every time you add an HTML file to the library.\
|
|||||||
|
|
||||||
with TemporaryDirectory('_plugin_html2zip') as tdir:
|
with TemporaryDirectory('_plugin_html2zip') as tdir:
|
||||||
recs =[('debug_pipeline', tdir, OptionRecommendation.HIGH)]
|
recs =[('debug_pipeline', tdir, OptionRecommendation.HIGH)]
|
||||||
|
recs.append(['keep_ligatures', True, OptionRecommendation.HIGH])
|
||||||
if self.site_customization and self.site_customization.strip():
|
if self.site_customization and self.site_customization.strip():
|
||||||
recs.append(['input_encoding', self.site_customization.strip(),
|
recs.append(['input_encoding', self.site_customization.strip(),
|
||||||
OptionRecommendation.HIGH])
|
OptionRecommendation.HIGH])
|
||||||
@ -81,7 +82,7 @@ class PML2PMLZ(FileTypePlugin):
|
|||||||
|
|
||||||
return of.name
|
return of.name
|
||||||
|
|
||||||
|
# Metadata reader plugins {{{
|
||||||
class ComicMetadataReader(MetadataReaderPlugin):
|
class ComicMetadataReader(MetadataReaderPlugin):
|
||||||
|
|
||||||
name = 'Read comic metadata'
|
name = 'Read comic metadata'
|
||||||
@ -319,7 +320,9 @@ class ZipMetadataReader(MetadataReaderPlugin):
|
|||||||
def get_metadata(self, stream, ftype):
|
def get_metadata(self, stream, ftype):
|
||||||
from calibre.ebooks.metadata.zip import get_metadata
|
from calibre.ebooks.metadata.zip import get_metadata
|
||||||
return get_metadata(stream)
|
return get_metadata(stream)
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
# Metadata writer plugins {{{
|
||||||
|
|
||||||
class EPUBMetadataWriter(MetadataWriterPlugin):
|
class EPUBMetadataWriter(MetadataWriterPlugin):
|
||||||
|
|
||||||
@ -395,6 +398,7 @@ class TOPAZMetadataWriter(MetadataWriterPlugin):
|
|||||||
from calibre.ebooks.metadata.topaz import set_metadata
|
from calibre.ebooks.metadata.topaz import set_metadata
|
||||||
set_metadata(stream, mi)
|
set_metadata(stream, mi)
|
||||||
|
|
||||||
|
# }}}
|
||||||
|
|
||||||
from calibre.ebooks.comic.input import ComicInput
|
from calibre.ebooks.comic.input import ComicInput
|
||||||
from calibre.ebooks.epub.input import EPUBInput
|
from calibre.ebooks.epub.input import EPUBInput
|
||||||
@ -444,7 +448,7 @@ from calibre.devices.kindle.driver import KINDLE, KINDLE2, KINDLE_DX
|
|||||||
from calibre.devices.nook.driver import NOOK
|
from calibre.devices.nook.driver import NOOK
|
||||||
from calibre.devices.prs505.driver import PRS505
|
from calibre.devices.prs505.driver import PRS505
|
||||||
from calibre.devices.android.driver import ANDROID, S60
|
from calibre.devices.android.driver import ANDROID, S60
|
||||||
from calibre.devices.nokia.driver import N770, N810, E71X
|
from calibre.devices.nokia.driver import N770, N810, E71X, E52
|
||||||
from calibre.devices.eslick.driver import ESLICK, EBK52
|
from calibre.devices.eslick.driver import ESLICK, EBK52
|
||||||
from calibre.devices.nuut2.driver import NUUT2
|
from calibre.devices.nuut2.driver import NUUT2
|
||||||
from calibre.devices.iriver.driver import IRIVER_STORY
|
from calibre.devices.iriver.driver import IRIVER_STORY
|
||||||
@ -519,6 +523,7 @@ plugins += [
|
|||||||
S60,
|
S60,
|
||||||
N770,
|
N770,
|
||||||
E71X,
|
E71X,
|
||||||
|
E52,
|
||||||
N810,
|
N810,
|
||||||
COOL_ER,
|
COOL_ER,
|
||||||
ESLICK,
|
ESLICK,
|
||||||
|
@ -275,13 +275,44 @@ class iPadOutput(OutputProfile):
|
|||||||
# touchscreen_news_css {{{
|
# touchscreen_news_css {{{
|
||||||
touchscreen_news_css = u'''
|
touchscreen_news_css = u'''
|
||||||
/* hr used in articles */
|
/* hr used in articles */
|
||||||
|
.article_articles_list {
|
||||||
|
width:18%;
|
||||||
|
}
|
||||||
|
.article_link {
|
||||||
|
color: #593f29;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
.article_next {
|
||||||
|
-webkit-border-top-right-radius:4px;
|
||||||
|
-webkit-border-bottom-right-radius:4px;
|
||||||
|
font-style: italic;
|
||||||
|
width:32%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.article_prev {
|
||||||
|
-webkit-border-top-left-radius:4px;
|
||||||
|
-webkit-border-bottom-left-radius:4px;
|
||||||
|
font-style: italic;
|
||||||
|
width:32%;
|
||||||
|
}
|
||||||
|
.article_sections_list {
|
||||||
|
width:18%;
|
||||||
|
}
|
||||||
|
.articles_link {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.sections_link {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
.caption_divider {
|
.caption_divider {
|
||||||
border:#ccc 1px solid;
|
border:#ccc 1px solid;
|
||||||
}
|
}
|
||||||
|
|
||||||
.touchscreen_navbar {
|
.touchscreen_navbar {
|
||||||
background:#ccc;
|
background:#c3bab2;
|
||||||
border:#ccc 1px solid;
|
border:#ccc 0px solid;
|
||||||
border-collapse:separate;
|
border-collapse:separate;
|
||||||
border-spacing:1px;
|
border-spacing:1px;
|
||||||
margin-left: 5%;
|
margin-left: 5%;
|
||||||
@ -292,22 +323,16 @@ class iPadOutput(OutputProfile):
|
|||||||
.touchscreen_navbar td {
|
.touchscreen_navbar td {
|
||||||
background:#fff;
|
background:#fff;
|
||||||
font-family:Helvetica;
|
font-family:Helvetica;
|
||||||
font-size:90%;
|
font-size:80%;
|
||||||
padding: 5px;
|
/* UI touchboxes use 8px padding */
|
||||||
|
padding: 6px;
|
||||||
text-align:center;
|
text-align:center;
|
||||||
}
|
}
|
||||||
.touchscreen_navbar td:first-child {
|
|
||||||
-webkit-border-top-left-radius:4px;
|
|
||||||
-webkit-border-bottom-left-radius:4px;
|
|
||||||
}
|
|
||||||
.touchscreen_navbar td:last-child {
|
|
||||||
-webkit-border-top-right-radius:4px;
|
|
||||||
-webkit-border-bottom-right-radius:4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.feed_link {
|
.touchscreen_navbar td a:link {
|
||||||
font-style: italic;
|
color: #593f29;
|
||||||
}
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
/* Index formatting */
|
/* Index formatting */
|
||||||
.publish_date {
|
.publish_date {
|
||||||
@ -318,12 +343,50 @@ class iPadOutput(OutputProfile):
|
|||||||
border-top:1px solid gray;
|
border-top:1px solid gray;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hr.caption_divider {
|
||||||
|
border-color:black;
|
||||||
|
border-style:solid;
|
||||||
|
border-width:1px;
|
||||||
|
}
|
||||||
|
|
||||||
/* Feed summary formatting */
|
/* Feed summary formatting */
|
||||||
|
.article_summary {
|
||||||
|
display:inline-block;
|
||||||
|
}
|
||||||
|
.feed {
|
||||||
|
font-family:sans-serif;
|
||||||
|
font-weight:bold;
|
||||||
|
font-size:larger;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feed_link {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feed_next {
|
||||||
|
-webkit-border-top-right-radius:4px;
|
||||||
|
-webkit-border-bottom-right-radius:4px;
|
||||||
|
font-style: italic;
|
||||||
|
width:40%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feed_prev {
|
||||||
|
-webkit-border-top-left-radius:4px;
|
||||||
|
-webkit-border-bottom-left-radius:4px;
|
||||||
|
font-style: italic;
|
||||||
|
width:40%;
|
||||||
|
}
|
||||||
|
|
||||||
.feed_title {
|
.feed_title {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: 160%;
|
font-size: 160%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.feed_up {
|
||||||
|
font-weight: bold;
|
||||||
|
width:20%;
|
||||||
|
}
|
||||||
|
|
||||||
.summary_headline {
|
.summary_headline {
|
||||||
font-weight:bold;
|
font-weight:bold;
|
||||||
text-align:left;
|
text-align:left;
|
||||||
@ -338,12 +401,6 @@ class iPadOutput(OutputProfile):
|
|||||||
text-align:left;
|
text-align:left;
|
||||||
}
|
}
|
||||||
|
|
||||||
.feed {
|
|
||||||
font-family:sans-serif;
|
|
||||||
font-weight:bold;
|
|
||||||
font-size:larger;
|
|
||||||
}
|
|
||||||
|
|
||||||
'''
|
'''
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
|
@ -106,9 +106,11 @@ class BOOX(HANLINV3):
|
|||||||
description = _('Communicate with the BOOX eBook reader.')
|
description = _('Communicate with the BOOX eBook reader.')
|
||||||
author = 'Jesus Manuel Marinho Valcarce'
|
author = 'Jesus Manuel Marinho Valcarce'
|
||||||
supported_platforms = ['windows', 'osx', 'linux']
|
supported_platforms = ['windows', 'osx', 'linux']
|
||||||
|
METADATA_CACHE = '.metadata.calibre'
|
||||||
|
|
||||||
# Ordered list of supported formats
|
# Ordered list of supported formats
|
||||||
FORMATS = ['epub', 'fb2', 'djvu', 'pdf', 'html', 'txt', 'rtf', 'mobi', 'prc', 'chm']
|
FORMATS = ['epub', 'fb2', 'djvu', 'pdf', 'html', 'txt', 'rtf', 'mobi',
|
||||||
|
'prc', 'chm', 'doc']
|
||||||
|
|
||||||
VENDOR_ID = [0x0525]
|
VENDOR_ID = [0x0525]
|
||||||
PRODUCT_ID = [0xa4a5]
|
PRODUCT_ID = [0xa4a5]
|
||||||
|
@ -67,3 +67,24 @@ class E71X(USBMS):
|
|||||||
VENDOR_NAME = 'NOKIA'
|
VENDOR_NAME = 'NOKIA'
|
||||||
WINDOWS_MAIN_MEM = 'S60'
|
WINDOWS_MAIN_MEM = 'S60'
|
||||||
|
|
||||||
|
class E52(USBMS):
|
||||||
|
|
||||||
|
name = 'Nokia E52 device interface'
|
||||||
|
gui_name = 'Nokia E52'
|
||||||
|
description = _('Communicate with the Nokia E52')
|
||||||
|
author = 'David Ignjic'
|
||||||
|
supported_platforms = ['windows', 'linux', 'osx']
|
||||||
|
|
||||||
|
VENDOR_ID = [0x421]
|
||||||
|
PRODUCT_ID = [0x1CD]
|
||||||
|
BCD = [0x100]
|
||||||
|
|
||||||
|
|
||||||
|
FORMATS = ['mobi', 'prc']
|
||||||
|
|
||||||
|
EBOOK_DIR_MAIN = 'eBooks'
|
||||||
|
SUPPORTS_SUB_DIRS = True
|
||||||
|
|
||||||
|
VENDOR_NAME = 'NOKIA'
|
||||||
|
WINDOWS_MAIN_MEM = 'S60'
|
||||||
|
|
||||||
|
@ -144,52 +144,73 @@ class XMLCache(object):
|
|||||||
if title+str(i) not in seen:
|
if title+str(i) not in seen:
|
||||||
title = title+str(i)
|
title = title+str(i)
|
||||||
playlist.set('title', title)
|
playlist.set('title', title)
|
||||||
|
seen.add(title)
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
seen.add(title)
|
seen.add(title)
|
||||||
|
|
||||||
def build_playlist_id_map(self):
|
|
||||||
debug_print('Start build_playlist_id_map')
|
|
||||||
ans = {}
|
|
||||||
self.ensure_unique_playlist_titles()
|
|
||||||
debug_print('after ensure_unique_playlist_titles')
|
|
||||||
self.prune_empty_playlists()
|
|
||||||
for i, root in self.record_roots.items():
|
|
||||||
debug_print('build_playlist_id_map loop', i)
|
|
||||||
id_map = self.build_id_map(root)
|
|
||||||
ans[i] = []
|
|
||||||
for playlist in root.xpath('//*[local-name()="playlist"]'):
|
|
||||||
items = []
|
|
||||||
for item in playlist:
|
|
||||||
id_ = item.get('id', None)
|
|
||||||
record = id_map.get(id_, None)
|
|
||||||
if record is not None:
|
|
||||||
items.append(record)
|
|
||||||
ans[i].append((playlist.get('title'), items))
|
|
||||||
debug_print('end build_playlist_id_map')
|
|
||||||
return ans
|
|
||||||
|
|
||||||
def build_id_playlist_map(self, bl_index):
|
def build_id_playlist_map(self, bl_index):
|
||||||
|
'''
|
||||||
|
Return a map of the collections in books: {lpaths: [collection names]}
|
||||||
|
'''
|
||||||
debug_print('Start build_id_playlist_map')
|
debug_print('Start build_id_playlist_map')
|
||||||
pmap = self.build_playlist_id_map()[bl_index]
|
self.ensure_unique_playlist_titles()
|
||||||
|
self.prune_empty_playlists()
|
||||||
|
debug_print('after cleaning playlists')
|
||||||
|
root = self.record_roots[bl_index]
|
||||||
|
if root is None:
|
||||||
|
return
|
||||||
|
id_map = self.build_id_map(root)
|
||||||
playlist_map = {}
|
playlist_map = {}
|
||||||
for title, records in pmap:
|
# foreach playlist, get the lpaths for the ids in it, then add to dict
|
||||||
for record in records:
|
for playlist in root.xpath('//*[local-name()="playlist"]'):
|
||||||
path = record.get('path', None)
|
name = playlist.get('title')
|
||||||
if path:
|
if name is None:
|
||||||
if path not in playlist_map:
|
debug_print('build_id_playlist_map: unnamed playlist!')
|
||||||
playlist_map[path] = []
|
continue
|
||||||
playlist_map[path].append(title)
|
for item in playlist:
|
||||||
|
# translate each id into its lpath
|
||||||
|
id_ = item.get('id', None)
|
||||||
|
if id_ is None:
|
||||||
|
debug_print('build_id_playlist_map: id_ is None!')
|
||||||
|
continue
|
||||||
|
bk = id_map.get(id_, None)
|
||||||
|
if bk is None:
|
||||||
|
debug_print('build_id_playlist_map: book is None!', id_)
|
||||||
|
continue
|
||||||
|
lpath = bk.get('path', None)
|
||||||
|
if lpath is None:
|
||||||
|
debug_print('build_id_playlist_map: lpath is None!', id_)
|
||||||
|
continue
|
||||||
|
if lpath not in playlist_map:
|
||||||
|
playlist_map[lpath] = []
|
||||||
|
playlist_map[lpath].append(name)
|
||||||
debug_print('Finish build_id_playlist_map. Found', len(playlist_map))
|
debug_print('Finish build_id_playlist_map. Found', len(playlist_map))
|
||||||
return playlist_map
|
return playlist_map
|
||||||
|
|
||||||
|
def reset_existing_playlists_map(self):
|
||||||
|
'''
|
||||||
|
Call this method before calling get_or_create_playlist in the context of
|
||||||
|
a given job. Call it again after deleting any playlists. The current
|
||||||
|
implementation adds all new playlists before deleting any, so that
|
||||||
|
constraint is respected.
|
||||||
|
'''
|
||||||
|
self._playlist_to_playlist_id_map = {}
|
||||||
|
|
||||||
def get_or_create_playlist(self, bl_idx, title):
|
def get_or_create_playlist(self, bl_idx, title):
|
||||||
|
# maintain a private map of playlists to their ids. Don't check if it
|
||||||
|
# exists, because reset_existing_playlist_map must be called before it
|
||||||
|
# is used to ensure that deleted playlists are taken into account
|
||||||
root = self.record_roots[bl_idx]
|
root = self.record_roots[bl_idx]
|
||||||
for playlist in root.xpath('//*[local-name()="playlist"]'):
|
if bl_idx not in self._playlist_to_playlist_id_map:
|
||||||
if playlist.get('title', None) == title:
|
self._playlist_to_playlist_id_map[bl_idx] = {}
|
||||||
return playlist
|
for playlist in root.xpath('//*[local-name()="playlist"]'):
|
||||||
if DEBUG:
|
pl_title = playlist.get('title', None)
|
||||||
debug_print('Creating playlist:', title)
|
if pl_title is not None:
|
||||||
|
self._playlist_to_playlist_id_map[bl_idx][pl_title] = playlist
|
||||||
|
if title in self._playlist_to_playlist_id_map[bl_idx]:
|
||||||
|
return self._playlist_to_playlist_id_map[bl_idx][title]
|
||||||
|
debug_print('Creating playlist:', title)
|
||||||
ans = root.makeelement('{%s}playlist'%self.namespaces[bl_idx],
|
ans = root.makeelement('{%s}playlist'%self.namespaces[bl_idx],
|
||||||
nsmap=root.nsmap, attrib={
|
nsmap=root.nsmap, attrib={
|
||||||
'uuid' : uuid(),
|
'uuid' : uuid(),
|
||||||
@ -198,6 +219,7 @@ class XMLCache(object):
|
|||||||
'sourceid': '1'
|
'sourceid': '1'
|
||||||
})
|
})
|
||||||
root.append(ans)
|
root.append(ans)
|
||||||
|
self._playlist_to_playlist_id_map[bl_idx][title] = ans
|
||||||
return ans
|
return ans
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
@ -260,7 +282,9 @@ class XMLCache(object):
|
|||||||
ensure_media_xml_base_ids(root)
|
ensure_media_xml_base_ids(root)
|
||||||
|
|
||||||
idmap = ensure_numeric_ids(root)
|
idmap = ensure_numeric_ids(root)
|
||||||
remap_playlist_references(root, idmap)
|
if len(idmap) > 0:
|
||||||
|
debug_print('fix_ids: found some non-numeric ids')
|
||||||
|
remap_playlist_references(root, idmap)
|
||||||
if i == 0:
|
if i == 0:
|
||||||
sourceid, playlist_sid = 1, 0
|
sourceid, playlist_sid = 1, 0
|
||||||
base = 0
|
base = 0
|
||||||
@ -321,17 +345,20 @@ class XMLCache(object):
|
|||||||
debug_print('Updating XML Cache:', i)
|
debug_print('Updating XML Cache:', i)
|
||||||
root = self.record_roots[i]
|
root = self.record_roots[i]
|
||||||
lpath_map = self.build_lpath_map(root)
|
lpath_map = self.build_lpath_map(root)
|
||||||
|
gtz_count = ltz_count = 0
|
||||||
for book in booklist:
|
for book in booklist:
|
||||||
path = os.path.join(self.prefixes[i], *(book.lpath.split('/')))
|
path = os.path.join(self.prefixes[i], *(book.lpath.split('/')))
|
||||||
record = lpath_map.get(book.lpath, None)
|
record = lpath_map.get(book.lpath, None)
|
||||||
if record is None:
|
if record is None:
|
||||||
record = self.create_text_record(root, i, book.lpath)
|
record = self.create_text_record(root, i, book.lpath)
|
||||||
self.update_text_record(record, book, path, i)
|
(gtz_count, ltz_count) = self.update_text_record(record, book,
|
||||||
|
path, i, gtz_count, ltz_count)
|
||||||
# Ensure the collections in the XML database are recorded for
|
# Ensure the collections in the XML database are recorded for
|
||||||
# this book
|
# this book
|
||||||
if book.device_collections is None:
|
if book.device_collections is None:
|
||||||
book.device_collections = []
|
book.device_collections = []
|
||||||
book.device_collections = playlist_map.get(book.lpath, [])
|
book.device_collections = playlist_map.get(book.lpath, [])
|
||||||
|
debug_print('Timezone votes: %d GMT, %d LTZ'%(gtz_count, ltz_count))
|
||||||
self.update_playlists(i, root, booklist, collections_attributes)
|
self.update_playlists(i, root, booklist, collections_attributes)
|
||||||
# Update the device collections because update playlist could have added
|
# Update the device collections because update playlist could have added
|
||||||
# some new ones.
|
# some new ones.
|
||||||
@ -352,8 +379,10 @@ class XMLCache(object):
|
|||||||
|
|
||||||
def update_playlists(self, bl_index, root, booklist, collections_attributes):
|
def update_playlists(self, bl_index, root, booklist, collections_attributes):
|
||||||
debug_print('Starting update_playlists', collections_attributes, bl_index)
|
debug_print('Starting update_playlists', collections_attributes, bl_index)
|
||||||
|
self.reset_existing_playlists_map()
|
||||||
collections = booklist.get_collections(collections_attributes)
|
collections = booklist.get_collections(collections_attributes)
|
||||||
lpath_map = self.build_lpath_map(root)
|
lpath_map = self.build_lpath_map(root)
|
||||||
|
debug_print('update_playlists: finished building maps')
|
||||||
for category, books in collections.items():
|
for category, books in collections.items():
|
||||||
records = [lpath_map.get(b.lpath, None) for b in books]
|
records = [lpath_map.get(b.lpath, None) for b in books]
|
||||||
# Remove any books that were not found, although this
|
# Remove any books that were not found, although this
|
||||||
@ -362,23 +391,34 @@ class XMLCache(object):
|
|||||||
debug_print('WARNING: Some elements in the JSON cache were not'
|
debug_print('WARNING: Some elements in the JSON cache were not'
|
||||||
' found in the XML cache')
|
' found in the XML cache')
|
||||||
records = [x for x in records if x is not None]
|
records = [x for x in records if x is not None]
|
||||||
|
# Ensure each book has an ID.
|
||||||
for rec in records:
|
for rec in records:
|
||||||
if rec.get('id', None) is None:
|
if rec.get('id', None) is None:
|
||||||
rec.set('id', str(self.max_id(root)+1))
|
rec.set('id', str(self.max_id(root)+1))
|
||||||
ids = [x.get('id', None) for x in records]
|
ids = [x.get('id', None) for x in records]
|
||||||
|
# Given that we set the ids, there shouldn't be any None's. But
|
||||||
|
# better to be safe...
|
||||||
if None in ids:
|
if None in ids:
|
||||||
debug_print('WARNING: Some <text> elements do not have ids')
|
debug_print('WARNING: Some <text> elements do not have ids')
|
||||||
ids = [x for x in ids if x is not None]
|
ids = [x for x in ids if x is not None]
|
||||||
|
|
||||||
playlist = self.get_or_create_playlist(bl_index, category)
|
playlist = self.get_or_create_playlist(bl_index, category)
|
||||||
|
# Get the books currently in the playlist. We will need them to be
|
||||||
|
# sure to put back any books that were manually added.
|
||||||
playlist_ids = []
|
playlist_ids = []
|
||||||
for item in playlist:
|
for item in playlist:
|
||||||
id_ = item.get('id', None)
|
id_ = item.get('id', None)
|
||||||
if id_ is not None:
|
if id_ is not None:
|
||||||
playlist_ids.append(id_)
|
playlist_ids.append(id_)
|
||||||
|
# Empty the playlist. We do this so that the playlist will have the
|
||||||
|
# order specified by get_collections
|
||||||
for item in list(playlist):
|
for item in list(playlist):
|
||||||
playlist.remove(item)
|
playlist.remove(item)
|
||||||
|
|
||||||
|
# Get a list of ids not known by get_collections
|
||||||
extra_ids = [x for x in playlist_ids if x not in ids]
|
extra_ids = [x for x in playlist_ids if x not in ids]
|
||||||
|
# Rebuild the collection in the order specified by get_collections. Then
|
||||||
|
# add the ids that get_collections didn't know about.
|
||||||
for id_ in ids + extra_ids:
|
for id_ in ids + extra_ids:
|
||||||
item = playlist.makeelement(
|
item = playlist.makeelement(
|
||||||
'{%s}item'%self.namespaces[bl_index],
|
'{%s}item'%self.namespaces[bl_index],
|
||||||
@ -416,11 +456,38 @@ class XMLCache(object):
|
|||||||
root.append(ans)
|
root.append(ans)
|
||||||
return ans
|
return ans
|
||||||
|
|
||||||
def update_text_record(self, record, book, path, bl_index):
|
def update_text_record(self, record, book, path, bl_index, gtz_count, ltz_count):
|
||||||
|
'''
|
||||||
|
Update the Sony database from the book. This is done if the timestamp in
|
||||||
|
the db differs from the timestamp on the file.
|
||||||
|
'''
|
||||||
|
|
||||||
|
# It seems that a Sony device can sometimes know what timezone it is in,
|
||||||
|
# and apparently converts the dates to GMT when it writes them to the
|
||||||
|
# db. Unfortunately, we can't tell when it does this, so we use a
|
||||||
|
# horrible heuristic. First, set dates only for new books, trying to
|
||||||
|
# avoid upsetting the sony. Use the timezone determined through the
|
||||||
|
# voting described next. Second, voting: if a book is not new, compare
|
||||||
|
# its Sony DB date against localtime and gmtime. Count the matches. When
|
||||||
|
# we must set a date, use the one with the most matches. Use localtime
|
||||||
|
# if the case of a tie, and hope it is right.
|
||||||
timestamp = os.path.getmtime(path)
|
timestamp = os.path.getmtime(path)
|
||||||
date = strftime(timestamp)
|
rec_date = record.get('date', None)
|
||||||
if date != record.get('date', None):
|
if not getattr(book, '_new_book', False): # book is not new
|
||||||
|
if strftime(timestamp, zone=time.gmtime) == rec_date:
|
||||||
|
gtz_count += 1
|
||||||
|
elif strftime(timestamp, zone=time.localtime) == rec_date:
|
||||||
|
ltz_count += 1
|
||||||
|
else: # book is new. Set the time using the current votes
|
||||||
|
if ltz_count >= gtz_count:
|
||||||
|
tz = time.localtime
|
||||||
|
debug_print("Using localtime TZ for new book", book.lpath)
|
||||||
|
else:
|
||||||
|
tz = time.gmtime
|
||||||
|
debug_print("Using GMT TZ for new book", book.lpath)
|
||||||
|
date = strftime(timestamp, zone=tz)
|
||||||
record.set('date', date)
|
record.set('date', date)
|
||||||
|
|
||||||
record.set('size', str(os.stat(path).st_size))
|
record.set('size', str(os.stat(path).st_size))
|
||||||
title = book.title if book.title else _('Unknown')
|
title = book.title if book.title else _('Unknown')
|
||||||
record.set('title', title)
|
record.set('title', title)
|
||||||
@ -445,6 +512,7 @@ class XMLCache(object):
|
|||||||
if 'id' not in record.attrib:
|
if 'id' not in record.attrib:
|
||||||
num = self.max_id(record.getroottree().getroot())
|
num = self.max_id(record.getroottree().getroot())
|
||||||
record.set('id', str(num+1))
|
record.set('id', str(num+1))
|
||||||
|
return (gtz_count, ltz_count)
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
# Writing the XML files {{{
|
# Writing the XML files {{{
|
||||||
|
@ -134,9 +134,16 @@ class CollectionsBookList(BookList):
|
|||||||
def get_collections(self, collection_attributes):
|
def get_collections(self, collection_attributes):
|
||||||
collections = {}
|
collections = {}
|
||||||
series_categories = set([])
|
series_categories = set([])
|
||||||
|
# This map of sets is used to avoid linear searches when testing for
|
||||||
|
# book equality
|
||||||
|
collections_lpaths = {}
|
||||||
for book in self:
|
for book in self:
|
||||||
# The default: leave the book in all existing collections. Do not
|
# Make sure we can identify this book via the lpath
|
||||||
# add any new ones.
|
lpath = getattr(book, 'lpath', None)
|
||||||
|
if lpath is None:
|
||||||
|
continue
|
||||||
|
# Decide how we will build the collections. The default: leave the
|
||||||
|
# book in all existing collections. Do not add any new ones.
|
||||||
attrs = ['device_collections']
|
attrs = ['device_collections']
|
||||||
if getattr(book, '_new_book', False):
|
if getattr(book, '_new_book', False):
|
||||||
if prefs['preserve_user_collections']:
|
if prefs['preserve_user_collections']:
|
||||||
@ -163,11 +170,12 @@ class CollectionsBookList(BookList):
|
|||||||
continue
|
continue
|
||||||
if category not in collections:
|
if category not in collections:
|
||||||
collections[category] = []
|
collections[category] = []
|
||||||
if book not in collections[category]:
|
collections_lpaths[category] = set()
|
||||||
|
if lpath not in collections_lpaths[category]:
|
||||||
|
collections_lpaths[category].add(lpath)
|
||||||
collections[category].append(book)
|
collections[category].append(book)
|
||||||
if attr == 'series':
|
if attr == 'series':
|
||||||
series_categories.add(category)
|
series_categories.add(category)
|
||||||
|
|
||||||
# Sort collections
|
# Sort collections
|
||||||
for category, books in collections.items():
|
for category, books in collections.items():
|
||||||
def tgetter(x):
|
def tgetter(x):
|
||||||
|
@ -290,7 +290,7 @@ class USBMS(CLI, Device):
|
|||||||
js = [item.to_json() for item in booklists[listid] if
|
js = [item.to_json() for item in booklists[listid] if
|
||||||
hasattr(item, 'to_json')]
|
hasattr(item, 'to_json')]
|
||||||
with open(self.normalize_path(os.path.join(prefix, self.METADATA_CACHE)), 'wb') as f:
|
with open(self.normalize_path(os.path.join(prefix, self.METADATA_CACHE)), 'wb') as f:
|
||||||
json.dump(js, f, indent=2, encoding='utf-8')
|
f.write(json.dumps(js, indent=2, encoding='utf-8'))
|
||||||
write_prefix(self._main_prefix, 0)
|
write_prefix(self._main_prefix, 0)
|
||||||
write_prefix(self._card_a_prefix, 1)
|
write_prefix(self._card_a_prefix, 1)
|
||||||
write_prefix(self._card_b_prefix, 2)
|
write_prefix(self._card_b_prefix, 2)
|
||||||
|
@ -92,7 +92,7 @@ class CHMInput(InputFormatPlugin):
|
|||||||
metadata.add('identifier', mi.isbn, attrib={'scheme':'ISBN'})
|
metadata.add('identifier', mi.isbn, attrib={'scheme':'ISBN'})
|
||||||
if not metadata.language:
|
if not metadata.language:
|
||||||
oeb.logger.warn(u'Language not specified')
|
oeb.logger.warn(u'Language not specified')
|
||||||
metadata.add('language', get_lang())
|
metadata.add('language', get_lang().replace('_', '-'))
|
||||||
if not metadata.creator:
|
if not metadata.creator:
|
||||||
oeb.logger.warn('Creator not specified')
|
oeb.logger.warn('Creator not specified')
|
||||||
metadata.add('creator', _('Unknown'))
|
metadata.add('creator', _('Unknown'))
|
||||||
|
@ -329,7 +329,7 @@ class HTMLInput(InputFormatPlugin):
|
|||||||
metadata.add('identifier', mi.isbn, attrib={'scheme':'ISBN'})
|
metadata.add('identifier', mi.isbn, attrib={'scheme':'ISBN'})
|
||||||
if not metadata.language:
|
if not metadata.language:
|
||||||
oeb.logger.warn(u'Language not specified')
|
oeb.logger.warn(u'Language not specified')
|
||||||
metadata.add('language', get_lang())
|
metadata.add('language', get_lang().replace('_', '-'))
|
||||||
if not metadata.creator:
|
if not metadata.creator:
|
||||||
oeb.logger.warn('Creator not specified')
|
oeb.logger.warn('Creator not specified')
|
||||||
metadata.add('creator', self.oeb.translate(__('Unknown')))
|
metadata.add('creator', self.oeb.translate(__('Unknown')))
|
||||||
|
15
src/calibre/ebooks/metadata/covers.py
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
|
||||||
|
|
||||||
|
__license__ = 'GPL v3'
|
||||||
|
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||||
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
|
|
||||||
|
from calibre.customize import Plugin
|
||||||
|
|
||||||
|
class CoverDownload(Plugin):
|
||||||
|
|
||||||
|
supported_platforms = ['windows', 'osx', 'linux']
|
||||||
|
author = 'Kovid Goyal'
|
||||||
|
type = _('Cover download')
|
@ -15,7 +15,6 @@ from calibre.utils.config import OptionParser
|
|||||||
from calibre.ebooks.metadata.fetch import MetadataSource
|
from calibre.ebooks.metadata.fetch import MetadataSource
|
||||||
from calibre.utils.date import parse_date, utcnow
|
from calibre.utils.date import parse_date, utcnow
|
||||||
|
|
||||||
DOUBAN_API_KEY = None
|
|
||||||
NAMESPACES = {
|
NAMESPACES = {
|
||||||
'openSearch':'http://a9.com/-/spec/opensearchrss/1.0/',
|
'openSearch':'http://a9.com/-/spec/opensearchrss/1.0/',
|
||||||
'atom' : 'http://www.w3.org/2005/Atom',
|
'atom' : 'http://www.w3.org/2005/Atom',
|
||||||
@ -35,13 +34,15 @@ date = XPath("descendant::db:attribute[@name='pubdate']")
|
|||||||
creator = XPath("descendant::db:attribute[@name='author']")
|
creator = XPath("descendant::db:attribute[@name='author']")
|
||||||
tag = XPath("descendant::db:tag")
|
tag = XPath("descendant::db:tag")
|
||||||
|
|
||||||
|
CALIBRE_DOUBAN_API_KEY = '0bd1672394eb1ebf2374356abec15c3d'
|
||||||
|
|
||||||
class DoubanBooks(MetadataSource):
|
class DoubanBooks(MetadataSource):
|
||||||
|
|
||||||
name = 'Douban Books'
|
name = 'Douban Books'
|
||||||
description = _('Downloads metadata from Douban.com')
|
description = _('Downloads metadata from Douban.com')
|
||||||
supported_platforms = ['windows', 'osx', 'linux'] # Platforms this plugin will run on
|
supported_platforms = ['windows', 'osx', 'linux'] # Platforms this plugin will run on
|
||||||
author = 'Li Fanxi <lifanxi@freemindworld.com>' # The author of this plugin
|
author = 'Li Fanxi <lifanxi@freemindworld.com>' # The author of this plugin
|
||||||
version = (1, 0, 0) # The version number of this plugin
|
version = (1, 0, 1) # The version number of this plugin
|
||||||
|
|
||||||
def fetch(self):
|
def fetch(self):
|
||||||
try:
|
try:
|
||||||
@ -65,7 +66,7 @@ class Query(object):
|
|||||||
type = "search"
|
type = "search"
|
||||||
|
|
||||||
def __init__(self, title=None, author=None, publisher=None, isbn=None,
|
def __init__(self, title=None, author=None, publisher=None, isbn=None,
|
||||||
max_results=20, start_index=1):
|
max_results=20, start_index=1, api_key=''):
|
||||||
assert not(title is None and author is None and publisher is None and \
|
assert not(title is None and author is None and publisher is None and \
|
||||||
isbn is None)
|
isbn is None)
|
||||||
assert (int(max_results) < 21)
|
assert (int(max_results) < 21)
|
||||||
@ -89,16 +90,16 @@ class Query(object):
|
|||||||
|
|
||||||
if self.type == "isbn":
|
if self.type == "isbn":
|
||||||
self.url = self.ISBN_URL + q
|
self.url = self.ISBN_URL + q
|
||||||
if DOUBAN_API_KEY is not None:
|
if api_key != '':
|
||||||
self.url = self.url + "?apikey=" + DOUBAN_API_KEY
|
self.url = self.url + "?apikey=" + api_key
|
||||||
else:
|
else:
|
||||||
self.url = self.SEARCH_URL+urlencode({
|
self.url = self.SEARCH_URL+urlencode({
|
||||||
'q':q,
|
'q':q,
|
||||||
'max-results':max_results,
|
'max-results':max_results,
|
||||||
'start-index':start_index,
|
'start-index':start_index,
|
||||||
})
|
})
|
||||||
if DOUBAN_API_KEY is not None:
|
if api_key != '':
|
||||||
self.url = self.url + "&apikey=" + DOUBAN_API_KEY
|
self.url = self.url + "&apikey=" + api_key
|
||||||
|
|
||||||
def __call__(self, browser, verbose):
|
def __call__(self, browser, verbose):
|
||||||
if verbose:
|
if verbose:
|
||||||
@ -177,7 +178,7 @@ class ResultList(list):
|
|||||||
d = None
|
d = None
|
||||||
return d
|
return d
|
||||||
|
|
||||||
def populate(self, entries, browser, verbose=False):
|
def populate(self, entries, browser, verbose=False, api_key=''):
|
||||||
for x in entries:
|
for x in entries:
|
||||||
try:
|
try:
|
||||||
id_url = entry_id(x)[0].text
|
id_url = entry_id(x)[0].text
|
||||||
@ -186,8 +187,8 @@ class ResultList(list):
|
|||||||
report(verbose)
|
report(verbose)
|
||||||
mi = MetaInformation(title, self.get_authors(x))
|
mi = MetaInformation(title, self.get_authors(x))
|
||||||
try:
|
try:
|
||||||
if DOUBAN_API_KEY is not None:
|
if api_key != '':
|
||||||
id_url = id_url + "?apikey=" + DOUBAN_API_KEY
|
id_url = id_url + "?apikey=" + api_key
|
||||||
raw = browser.open(id_url).read()
|
raw = browser.open(id_url).read()
|
||||||
feed = etree.fromstring(raw)
|
feed = etree.fromstring(raw)
|
||||||
x = entry(feed)[0]
|
x = entry(feed)[0]
|
||||||
@ -203,12 +204,16 @@ class ResultList(list):
|
|||||||
self.append(mi)
|
self.append(mi)
|
||||||
|
|
||||||
def search(title=None, author=None, publisher=None, isbn=None,
|
def search(title=None, author=None, publisher=None, isbn=None,
|
||||||
verbose=False, max_results=40):
|
verbose=False, max_results=40, api_key=None):
|
||||||
br = browser()
|
br = browser()
|
||||||
start, entries = 1, []
|
start, entries = 1, []
|
||||||
|
|
||||||
|
if api_key is None:
|
||||||
|
api_key = CALIBRE_DOUBAN_API_KEY
|
||||||
|
|
||||||
while start > 0 and len(entries) <= max_results:
|
while start > 0 and len(entries) <= max_results:
|
||||||
new, start = Query(title=title, author=author, publisher=publisher,
|
new, start = Query(title=title, author=author, publisher=publisher,
|
||||||
isbn=isbn, max_results=max_results, start_index=start)(br, verbose)
|
isbn=isbn, max_results=max_results, start_index=start, api_key=api_key)(br, verbose)
|
||||||
if not new:
|
if not new:
|
||||||
break
|
break
|
||||||
entries.extend(new)
|
entries.extend(new)
|
||||||
@ -216,7 +221,7 @@ def search(title=None, author=None, publisher=None, isbn=None,
|
|||||||
entries = entries[:max_results]
|
entries = entries[:max_results]
|
||||||
|
|
||||||
ans = ResultList()
|
ans = ResultList()
|
||||||
ans.populate(entries, br, verbose)
|
ans.populate(entries, br, verbose, api_key)
|
||||||
return ans
|
return ans
|
||||||
|
|
||||||
def option_parser():
|
def option_parser():
|
||||||
|
@ -351,9 +351,13 @@ def search(title=None, author=None, publisher=None, isbn=None, isbndb_key=None,
|
|||||||
if len(results) > 1:
|
if len(results) > 1:
|
||||||
if not results[0].comments or len(results[0].comments) == 0:
|
if not results[0].comments or len(results[0].comments) == 0:
|
||||||
for r in results[1:]:
|
for r in results[1:]:
|
||||||
if title.lower() == r.title[:len(title)].lower() and r.comments and len(r.comments):
|
try:
|
||||||
results[0].comments = r.comments
|
if title and title.lower() == r.title[:len(title)].lower() \
|
||||||
break
|
and r.comments and len(r.comments):
|
||||||
|
results[0].comments = r.comments
|
||||||
|
break
|
||||||
|
except:
|
||||||
|
pass
|
||||||
# Find a pubdate
|
# Find a pubdate
|
||||||
pubdate = None
|
pubdate = None
|
||||||
for r in results:
|
for r in results:
|
||||||
|
@ -1069,7 +1069,10 @@ class OPFCreator(MetaInformation):
|
|||||||
dc_attrs={'id':__appname__+'_id'}))
|
dc_attrs={'id':__appname__+'_id'}))
|
||||||
if getattr(self, 'pubdate', None) is not None:
|
if getattr(self, 'pubdate', None) is not None:
|
||||||
a(DC_ELEM('date', self.pubdate.isoformat()))
|
a(DC_ELEM('date', self.pubdate.isoformat()))
|
||||||
a(DC_ELEM('language', self.language if self.language else get_lang()))
|
lang = self.language
|
||||||
|
if not lang or lang.lower() == 'und':
|
||||||
|
lang = get_lang().replace('_', '-')
|
||||||
|
a(DC_ELEM('language', lang))
|
||||||
if self.comments:
|
if self.comments:
|
||||||
a(DC_ELEM('description', self.comments))
|
a(DC_ELEM('description', self.comments))
|
||||||
if self.publisher:
|
if self.publisher:
|
||||||
@ -1194,7 +1197,8 @@ def metadata_to_opf(mi, as_string=True):
|
|||||||
factory(DC('identifier'), mi.isbn, scheme='ISBN')
|
factory(DC('identifier'), mi.isbn, scheme='ISBN')
|
||||||
if mi.rights:
|
if mi.rights:
|
||||||
factory(DC('rights'), mi.rights)
|
factory(DC('rights'), mi.rights)
|
||||||
factory(DC('language'), mi.language if mi.language and mi.language.lower() != 'und' else get_lang())
|
factory(DC('language'), mi.language if mi.language and mi.language.lower()
|
||||||
|
!= 'und' else get_lang().replace('_', '-'))
|
||||||
if mi.tags:
|
if mi.tags:
|
||||||
for tag in mi.tags:
|
for tag in mi.tags:
|
||||||
factory(DC('subject'), tag)
|
factory(DC('subject'), tag)
|
||||||
|
@ -8,7 +8,7 @@ from functools import partial
|
|||||||
|
|
||||||
from calibre import prints
|
from calibre import prints
|
||||||
from calibre.constants import plugins
|
from calibre.constants import plugins
|
||||||
from calibre.ebooks.metadata import MetaInformation, string_to_authors, authors_to_string
|
from calibre.ebooks.metadata import MetaInformation, string_to_authors
|
||||||
|
|
||||||
pdfreflow, pdfreflow_error = plugins['pdfreflow']
|
pdfreflow, pdfreflow_error = plugins['pdfreflow']
|
||||||
|
|
||||||
@ -56,66 +56,10 @@ def get_metadata(stream, cover=True):
|
|||||||
|
|
||||||
get_quick_metadata = partial(get_metadata, cover=False)
|
get_quick_metadata = partial(get_metadata, cover=False)
|
||||||
|
|
||||||
import cStringIO
|
from calibre.utils.podofo import set_metadata as podofo_set_metadata
|
||||||
from threading import Thread
|
|
||||||
|
|
||||||
from calibre.utils.pdftk import set_metadata as pdftk_set_metadata
|
|
||||||
from calibre.utils.podofo import set_metadata as podofo_set_metadata, Unavailable
|
|
||||||
|
|
||||||
def set_metadata(stream, mi):
|
def set_metadata(stream, mi):
|
||||||
stream.seek(0)
|
stream.seek(0)
|
||||||
try:
|
return podofo_set_metadata(stream, mi)
|
||||||
return podofo_set_metadata(stream, mi)
|
|
||||||
except Unavailable:
|
|
||||||
pass
|
|
||||||
try:
|
|
||||||
return pdftk_set_metadata(stream, mi)
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
set_metadata_pypdf(stream, mi)
|
|
||||||
|
|
||||||
|
|
||||||
class MetadataWriter(Thread):
|
|
||||||
|
|
||||||
def __init__(self, out_pdf, buf):
|
|
||||||
self.out_pdf = out_pdf
|
|
||||||
self.buf = buf
|
|
||||||
Thread.__init__(self)
|
|
||||||
self.daemon = True
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
try:
|
|
||||||
self.out_pdf.write(self.buf)
|
|
||||||
except RuntimeError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def set_metadata_pypdf(stream, mi):
|
|
||||||
# Use a StringIO object for the pdf because we will want to over
|
|
||||||
# write it later and if we are working on the stream directly it
|
|
||||||
# could cause some issues.
|
|
||||||
|
|
||||||
from pyPdf import PdfFileReader, PdfFileWriter
|
|
||||||
raw = cStringIO.StringIO(stream.read())
|
|
||||||
orig_pdf = PdfFileReader(raw)
|
|
||||||
title = mi.title if mi.title else orig_pdf.documentInfo.title
|
|
||||||
author = authors_to_string(mi.authors) if mi.authors else orig_pdf.documentInfo.author
|
|
||||||
out_pdf = PdfFileWriter(title=title, author=author)
|
|
||||||
out_str = cStringIO.StringIO()
|
|
||||||
writer = MetadataWriter(out_pdf, out_str)
|
|
||||||
for page in orig_pdf.pages:
|
|
||||||
out_pdf.addPage(page)
|
|
||||||
writer.start()
|
|
||||||
writer.join(10) # Wait 10 secs for writing to complete
|
|
||||||
out_pdf.killed = True
|
|
||||||
writer.join()
|
|
||||||
if out_pdf.killed:
|
|
||||||
print 'Failed to set metadata: took too long'
|
|
||||||
return
|
|
||||||
|
|
||||||
stream.seek(0)
|
|
||||||
stream.truncate()
|
|
||||||
out_str.seek(0)
|
|
||||||
stream.write(out_str.read())
|
|
||||||
stream.seek(0)
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -808,7 +808,8 @@ class Manifest(object):
|
|||||||
pat = re.compile(r'&(%s);'%('|'.join(user_entities.keys())))
|
pat = re.compile(r'&(%s);'%('|'.join(user_entities.keys())))
|
||||||
data = pat.sub(lambda m:user_entities[m.group(1)], data)
|
data = pat.sub(lambda m:user_entities[m.group(1)], data)
|
||||||
|
|
||||||
parser = etree.XMLParser(no_network=True, huge_tree=True)
|
# Setting huge_tree=True causes crashes in windows with large files
|
||||||
|
parser = etree.XMLParser(no_network=True)
|
||||||
# Try with more & more drastic measures to parse
|
# Try with more & more drastic measures to parse
|
||||||
def first_pass(data):
|
def first_pass(data):
|
||||||
try:
|
try:
|
||||||
@ -844,7 +845,7 @@ class Manifest(object):
|
|||||||
nroot = etree.fromstring('<html></html>')
|
nroot = etree.fromstring('<html></html>')
|
||||||
has_body = False
|
has_body = False
|
||||||
for child in list(data):
|
for child in list(data):
|
||||||
if barename(child.tag) == 'body':
|
if isinstance(child.tag, (unicode, str)) and barename(child.tag) == 'body':
|
||||||
has_body = True
|
has_body = True
|
||||||
break
|
break
|
||||||
parent = nroot
|
parent = nroot
|
||||||
|
@ -131,7 +131,7 @@ class OEBReader(object):
|
|||||||
stream = cStringIO.StringIO(etree.tostring(opf))
|
stream = cStringIO.StringIO(etree.tostring(opf))
|
||||||
mi = MetaInformation(OPF(stream))
|
mi = MetaInformation(OPF(stream))
|
||||||
if not mi.language:
|
if not mi.language:
|
||||||
mi.language = get_lang()
|
mi.language = get_lang().replace('_', '-')
|
||||||
self.oeb.metadata.add('language', mi.language)
|
self.oeb.metadata.add('language', mi.language)
|
||||||
if not mi.title:
|
if not mi.title:
|
||||||
mi.title = self.oeb.translate(__('Unknown'))
|
mi.title = self.oeb.translate(__('Unknown'))
|
||||||
|
@ -25,10 +25,17 @@ from calibre.ebooks.oeb.base import XHTML, XHTML_NS, CSS_MIME, OEB_STYLES
|
|||||||
from calibre.ebooks.oeb.base import XPNSMAP, xpath, urlnormalize
|
from calibre.ebooks.oeb.base import XPNSMAP, xpath, urlnormalize
|
||||||
from calibre.ebooks.oeb.profile import PROFILES
|
from calibre.ebooks.oeb.profile import PROFILES
|
||||||
|
|
||||||
html_css = open(P('templates/html.css'), 'rb').read()
|
_html_css_stylesheet = None
|
||||||
|
|
||||||
|
def html_css_stylesheet():
|
||||||
|
global _html_css_stylesheet
|
||||||
|
if _html_css_stylesheet is None:
|
||||||
|
html_css = open(P('templates/html.css'), 'rb').read()
|
||||||
|
_html_css_stylesheet = cssutils.parseString(html_css)
|
||||||
|
_html_css_stylesheet.namespaces['h'] = XHTML_NS
|
||||||
|
return _html_css_stylesheet
|
||||||
|
|
||||||
XHTML_CSS_NAMESPACE = '@namespace "%s";\n' % XHTML_NS
|
XHTML_CSS_NAMESPACE = '@namespace "%s";\n' % XHTML_NS
|
||||||
HTML_CSS_STYLESHEET = cssutils.parseString(html_css)
|
|
||||||
HTML_CSS_STYLESHEET.namespaces['h'] = XHTML_NS
|
|
||||||
|
|
||||||
INHERITED = set(['azimuth', 'border-collapse', 'border-spacing',
|
INHERITED = set(['azimuth', 'border-collapse', 'border-spacing',
|
||||||
'caption-side', 'color', 'cursor', 'direction', 'elevation',
|
'caption-side', 'color', 'cursor', 'direction', 'elevation',
|
||||||
@ -120,7 +127,7 @@ class Stylizer(object):
|
|||||||
item = oeb.manifest.hrefs[path]
|
item = oeb.manifest.hrefs[path]
|
||||||
basename = os.path.basename(path)
|
basename = os.path.basename(path)
|
||||||
cssname = os.path.splitext(basename)[0] + '.css'
|
cssname = os.path.splitext(basename)[0] + '.css'
|
||||||
stylesheets = [HTML_CSS_STYLESHEET]
|
stylesheets = [html_css_stylesheet()]
|
||||||
head = xpath(tree, '/h:html/h:head')
|
head = xpath(tree, '/h:html/h:head')
|
||||||
if head:
|
if head:
|
||||||
head = head[0]
|
head = head[0]
|
||||||
|
@ -63,7 +63,8 @@ class TXTInput(InputFormatPlugin):
|
|||||||
raise ValueError('This txt file has malformed markup, it cannot be'
|
raise ValueError('This txt file has malformed markup, it cannot be'
|
||||||
' converted by calibre. See http://daringfireball.net/projects/markdown/syntax')
|
' converted by calibre. See http://daringfireball.net/projects/markdown/syntax')
|
||||||
else:
|
else:
|
||||||
html = convert_basic(txt)
|
flow_size = getattr(options, 'flow_size', 0)
|
||||||
|
html = convert_basic(txt, epub_split_size_kb=flow_size)
|
||||||
|
|
||||||
from calibre.customize.ui import plugin_for_input_format
|
from calibre.customize.ui import plugin_for_input_format
|
||||||
html_input = plugin_for_input_format('html')
|
html_input = plugin_for_input_format('html')
|
||||||
|
@ -17,14 +17,11 @@ __docformat__ = 'restructuredtext en'
|
|||||||
|
|
||||||
HTML_TEMPLATE = u'<html><head><meta http-equiv="Content-Type" content="text/html; charset=utf-8"/><title>%s</title></head><body>\n%s\n</body></html>'
|
HTML_TEMPLATE = u'<html><head><meta http-equiv="Content-Type" content="text/html; charset=utf-8"/><title>%s</title></head><body>\n%s\n</body></html>'
|
||||||
|
|
||||||
def convert_basic(txt, title=''):
|
def convert_basic(txt, title='', epub_split_size_kb=0):
|
||||||
lines = []
|
|
||||||
# Strip whitespace from the beginning and end of the line. Also replace
|
# Strip whitespace from the beginning and end of the line. Also replace
|
||||||
# all line breaks with \n.
|
# all line breaks with \n.
|
||||||
for line in txt.splitlines():
|
txt = '\n'.join([line.strip() for line in txt.splitlines()])
|
||||||
lines.append(line.strip())
|
|
||||||
txt = '\n'.join(lines)
|
|
||||||
|
|
||||||
# Condense redundant spaces
|
# Condense redundant spaces
|
||||||
txt = re.sub('[ ]{2,}', ' ', txt)
|
txt = re.sub('[ ]{2,}', ' ', txt)
|
||||||
|
|
||||||
@ -34,6 +31,15 @@ def convert_basic(txt, title=''):
|
|||||||
# Remove excessive line breaks.
|
# Remove excessive line breaks.
|
||||||
txt = re.sub('\n{3,}', '\n\n', txt)
|
txt = re.sub('\n{3,}', '\n\n', txt)
|
||||||
|
|
||||||
|
#Takes care if there is no point to split
|
||||||
|
if epub_split_size_kb > 0:
|
||||||
|
length_byte = len(txt.encode('utf-8'))
|
||||||
|
#Calculating the average chunk value for easy splitting as EPUB (+2 as a safe margin)
|
||||||
|
chunk_size = long(length_byte / (int(length_byte / (epub_split_size_kb * 1024) ) + 2 ))
|
||||||
|
#if there are chunks with a superior size then go and break
|
||||||
|
if (len(filter(lambda x: len(x.encode('utf-8')) > chunk_size, txt.split('\n\n')))) :
|
||||||
|
txt = u'\n\n'.join([split_string_separator(line, chunk_size) for line in txt.split('\n\n')])
|
||||||
|
|
||||||
lines = []
|
lines = []
|
||||||
# Split into paragraphs based on having a blank line between text.
|
# Split into paragraphs based on having a blank line between text.
|
||||||
for line in txt.split('\n\n'):
|
for line in txt.split('\n\n'):
|
||||||
@ -71,3 +77,10 @@ def opf_writer(path, opf_name, manifest, spine, mi):
|
|||||||
with open(os.path.join(path, opf_name), 'wb') as opffile:
|
with open(os.path.join(path, opf_name), 'wb') as opffile:
|
||||||
opf.render(opffile)
|
opf.render(opffile)
|
||||||
|
|
||||||
|
def split_string_separator(txt, size) :
|
||||||
|
if len(txt.encode('utf-8')) > size:
|
||||||
|
txt = u''.join([re.sub(u'\.(?P<ends>[^.]*)$', u'.\n\n\g<ends>',
|
||||||
|
txt[i:i+size], 1) for i in
|
||||||
|
xrange(0, len(txt.encode('utf-8')), size)])
|
||||||
|
return txt
|
||||||
|
|
||||||
|
@ -1,18 +1,18 @@
|
|||||||
__license__ = 'GPL v3'
|
__license__ = 'GPL v3'
|
||||||
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
|
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||||
""" The GUI """
|
""" The GUI """
|
||||||
import os
|
import os, sys
|
||||||
from threading import RLock
|
from threading import RLock
|
||||||
|
|
||||||
from PyQt4.QtCore import QVariant, QFileInfo, QObject, SIGNAL, QBuffer, Qt, QSize, \
|
from PyQt4.Qt import QVariant, QFileInfo, QObject, SIGNAL, QBuffer, Qt, QSize, \
|
||||||
QByteArray, QTranslator, QCoreApplication, QThread, \
|
QByteArray, QTranslator, QCoreApplication, QThread, \
|
||||||
QEvent, QTimer, pyqtSignal, QDate
|
QEvent, QTimer, pyqtSignal, QDate, QDesktopServices, \
|
||||||
from PyQt4.QtGui import QFileDialog, QMessageBox, QPixmap, QFileIconProvider, \
|
QFileDialog, QMessageBox, QPixmap, QFileIconProvider, \
|
||||||
QIcon, QApplication, QDialog, QPushButton
|
QIcon, QApplication, QDialog, QPushButton, QUrl
|
||||||
|
|
||||||
ORG_NAME = 'KovidsBrain'
|
ORG_NAME = 'KovidsBrain'
|
||||||
APP_UID = 'libprs500'
|
APP_UID = 'libprs500'
|
||||||
from calibre import islinux, iswindows, isosx, isfreebsd
|
from calibre.constants import islinux, iswindows, isosx, isfreebsd, isfrozen
|
||||||
from calibre.utils.config import Config, ConfigProxy, dynamic, JSONConfig
|
from calibre.utils.config import Config, ConfigProxy, dynamic, JSONConfig
|
||||||
from calibre.utils.localization import set_qt_translator
|
from calibre.utils.localization import set_qt_translator
|
||||||
from calibre.ebooks.metadata.meta import get_metadata, metadata_from_formats
|
from calibre.ebooks.metadata.meta import get_metadata, metadata_from_formats
|
||||||
@ -579,6 +579,22 @@ class Application(QApplication):
|
|||||||
|
|
||||||
_store_app = None
|
_store_app = None
|
||||||
|
|
||||||
|
def open_url(qurl):
|
||||||
|
paths = os.environ.get('LD_LIBRARY_PATH',
|
||||||
|
'').split(os.pathsep)
|
||||||
|
paths = [x for x in paths if x]
|
||||||
|
if isfrozen and islinux and paths:
|
||||||
|
npaths = [x for x in paths if x != sys.frozen_path]
|
||||||
|
os.environ['LD_LIBRARY_PATH'] = os.pathsep.join(npaths)
|
||||||
|
QDesktopServices.openUrl(qurl)
|
||||||
|
if isfrozen and islinux and paths:
|
||||||
|
os.environ['LD_LIBRARY_PATH'] = os.pathsep.join(paths)
|
||||||
|
|
||||||
|
|
||||||
|
def open_local_file(path):
|
||||||
|
url = QUrl.fromLocalFile(path)
|
||||||
|
open_url(url)
|
||||||
|
|
||||||
def is_ok_to_use_qt():
|
def is_ok_to_use_qt():
|
||||||
global gui_thread, _store_app
|
global gui_thread, _store_app
|
||||||
if (islinux or isfreebsd) and ':' not in os.environ.get('DISPLAY', ''):
|
if (islinux or isfreebsd) and ':' not in os.environ.get('DISPLAY', ''):
|
||||||
|
@ -5,17 +5,18 @@ __license__ = 'GPL v3'
|
|||||||
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||||
__docformat__ = 'restructuredtext en'
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
import shutil, os, datetime, sys, time
|
import shutil, os, datetime, time
|
||||||
from functools import partial
|
from functools import partial
|
||||||
|
|
||||||
from PyQt4.Qt import QInputDialog, pyqtSignal, QModelIndex, QThread, Qt, \
|
from PyQt4.Qt import QInputDialog, pyqtSignal, QModelIndex, QThread, Qt, \
|
||||||
SIGNAL, QPixmap, QTimer, QDesktopServices, QUrl, QDialog
|
SIGNAL, QPixmap, QTimer, QDialog
|
||||||
|
|
||||||
from calibre import strftime
|
from calibre import strftime
|
||||||
from calibre.ptempfile import PersistentTemporaryFile
|
from calibre.ptempfile import PersistentTemporaryFile
|
||||||
from calibre.utils.config import prefs, dynamic
|
from calibre.utils.config import prefs, dynamic
|
||||||
from calibre.gui2 import error_dialog, Dispatcher, gprefs, choose_files, \
|
from calibre.gui2 import error_dialog, Dispatcher, gprefs, choose_files, \
|
||||||
choose_dir, warning_dialog, info_dialog, question_dialog, config
|
choose_dir, warning_dialog, info_dialog, question_dialog, config, \
|
||||||
|
open_local_file
|
||||||
from calibre.ebooks.BeautifulSoup import BeautifulSoup, Tag, NavigableString
|
from calibre.ebooks.BeautifulSoup import BeautifulSoup, Tag, NavigableString
|
||||||
from calibre.utils.filenames import ascii_filename
|
from calibre.utils.filenames import ascii_filename
|
||||||
from calibre.gui2.widgets import IMAGE_EXTENSIONS
|
from calibre.gui2.widgets import IMAGE_EXTENSIONS
|
||||||
@ -25,7 +26,7 @@ from calibre.gui2.dialogs.tag_list_editor import TagListEditor
|
|||||||
from calibre.gui2.tools import convert_single_ebook, convert_bulk_ebook, \
|
from calibre.gui2.tools import convert_single_ebook, convert_bulk_ebook, \
|
||||||
fetch_scheduled_recipe, generate_catalog
|
fetch_scheduled_recipe, generate_catalog
|
||||||
from calibre.constants import preferred_encoding, filesystem_encoding, \
|
from calibre.constants import preferred_encoding, filesystem_encoding, \
|
||||||
isosx, isfrozen, islinux
|
isosx
|
||||||
from calibre.gui2.dialogs.choose_format import ChooseFormatDialog
|
from calibre.gui2.dialogs.choose_format import ChooseFormatDialog
|
||||||
from calibre.ebooks import BOOK_EXTENSIONS
|
from calibre.ebooks import BOOK_EXTENSIONS
|
||||||
from calibre.gui2.dialogs.confirm_delete import confirm
|
from calibre.gui2.dialogs.confirm_delete import confirm
|
||||||
@ -322,7 +323,6 @@ class AddAction(object): # {{{
|
|||||||
accept = True
|
accept = True
|
||||||
if accept:
|
if accept:
|
||||||
event.accept()
|
event.accept()
|
||||||
self.cover_cache.refresh([cid])
|
|
||||||
self.library_view.model().current_changed(current_idx, current_idx)
|
self.library_view.model().current_changed(current_idx, current_idx)
|
||||||
|
|
||||||
def __add_filesystem_book(self, paths, allow_device=True):
|
def __add_filesystem_book(self, paths, allow_device=True):
|
||||||
@ -645,6 +645,8 @@ class EditMetadataAction(object): # {{{
|
|||||||
if x.exception is None:
|
if x.exception is None:
|
||||||
self.library_view.model().refresh_ids(
|
self.library_view.model().refresh_ids(
|
||||||
x.updated, cr)
|
x.updated, cr)
|
||||||
|
if self.cover_flow:
|
||||||
|
self.cover_flow.dataChanged()
|
||||||
if x.failures:
|
if x.failures:
|
||||||
details = ['%s: %s'%(title, reason) for title,
|
details = ['%s: %s'%(title, reason) for title,
|
||||||
reason in x.failures.values()]
|
reason in x.failures.values()]
|
||||||
@ -689,7 +691,6 @@ class EditMetadataAction(object): # {{{
|
|||||||
if rows:
|
if rows:
|
||||||
current = self.library_view.currentIndex()
|
current = self.library_view.currentIndex()
|
||||||
m = self.library_view.model()
|
m = self.library_view.model()
|
||||||
m.refresh_cover_cache(map(m.id, rows))
|
|
||||||
if self.cover_flow:
|
if self.cover_flow:
|
||||||
self.cover_flow.dataChanged()
|
self.cover_flow.dataChanged()
|
||||||
m.current_changed(current, previous)
|
m.current_changed(current, previous)
|
||||||
@ -711,6 +712,8 @@ class EditMetadataAction(object): # {{{
|
|||||||
self.library_view.model().resort(reset=False)
|
self.library_view.model().resort(reset=False)
|
||||||
self.library_view.model().research()
|
self.library_view.model().research()
|
||||||
self.tags_view.recount()
|
self.tags_view.recount()
|
||||||
|
if self.cover_flow:
|
||||||
|
self.cover_flow.dataChanged()
|
||||||
|
|
||||||
# Merge books {{{
|
# Merge books {{{
|
||||||
def merge_books(self, safe_merge=False):
|
def merge_books(self, safe_merge=False):
|
||||||
@ -917,7 +920,7 @@ class SaveToDiskAction(object): # {{{
|
|||||||
_('Could not save some books') + ', ' +
|
_('Could not save some books') + ', ' +
|
||||||
_('Click the show details button to see which ones.'),
|
_('Click the show details button to see which ones.'),
|
||||||
u'\n\n'.join(failures), show=True)
|
u'\n\n'.join(failures), show=True)
|
||||||
QDesktopServices.openUrl(QUrl.fromLocalFile(path))
|
open_local_file(path)
|
||||||
|
|
||||||
def books_saved(self, job):
|
def books_saved(self, job):
|
||||||
if job.failed:
|
if job.failed:
|
||||||
@ -1183,15 +1186,7 @@ class ViewAction(object): # {{{
|
|||||||
self.job_manager.launch_gui_app(viewer,
|
self.job_manager.launch_gui_app(viewer,
|
||||||
kwargs=dict(args=args))
|
kwargs=dict(args=args))
|
||||||
else:
|
else:
|
||||||
paths = os.environ.get('LD_LIBRARY_PATH',
|
open_local_file(name)
|
||||||
'').split(os.pathsep)
|
|
||||||
paths = [x for x in paths if x]
|
|
||||||
if isfrozen and islinux and paths:
|
|
||||||
npaths = [x for x in paths if x != sys.frozen_path]
|
|
||||||
os.environ['LD_LIBRARY_PATH'] = os.pathsep.join(npaths)
|
|
||||||
QDesktopServices.openUrl(QUrl.fromLocalFile(name))#launch(name)
|
|
||||||
if isfrozen and islinux and paths:
|
|
||||||
os.environ['LD_LIBRARY_PATH'] = os.pathsep.join(paths)
|
|
||||||
time.sleep(2) # User feedback
|
time.sleep(2) # User feedback
|
||||||
finally:
|
finally:
|
||||||
self.unsetCursor()
|
self.unsetCursor()
|
||||||
@ -1237,11 +1232,11 @@ class ViewAction(object): # {{{
|
|||||||
return
|
return
|
||||||
for row in rows:
|
for row in rows:
|
||||||
path = self.library_view.model().db.abspath(row.row())
|
path = self.library_view.model().db.abspath(row.row())
|
||||||
QDesktopServices.openUrl(QUrl.fromLocalFile(path))
|
open_local_file(path)
|
||||||
|
|
||||||
def view_folder_for_id(self, id_):
|
def view_folder_for_id(self, id_):
|
||||||
path = self.library_view.model().db.abspath(id_, index_is_id=True)
|
path = self.library_view.model().db.abspath(id_, index_is_id=True)
|
||||||
QDesktopServices.openUrl(QUrl.fromLocalFile(path))
|
open_local_file(path)
|
||||||
|
|
||||||
def view_book(self, triggered):
|
def view_book(self, triggered):
|
||||||
rows = self.current_view().selectionModel().selectedRows()
|
rows = self.current_view().selectionModel().selectedRows()
|
||||||
|
@ -15,7 +15,7 @@ from calibre.ebooks.metadata import MetaInformation
|
|||||||
from calibre.constants import preferred_encoding, filesystem_encoding
|
from calibre.constants import preferred_encoding, filesystem_encoding
|
||||||
from calibre.utils.config import prefs
|
from calibre.utils.config import prefs
|
||||||
|
|
||||||
class DuplicatesAdder(QThread):
|
class DuplicatesAdder(QThread): # {{{
|
||||||
# Add duplicate books
|
# Add duplicate books
|
||||||
def __init__(self, parent, db, duplicates, db_adder):
|
def __init__(self, parent, db, duplicates, db_adder):
|
||||||
QThread.__init__(self, parent)
|
QThread.__init__(self, parent)
|
||||||
@ -34,9 +34,9 @@ class DuplicatesAdder(QThread):
|
|||||||
self.emit(SIGNAL('added(PyQt_PyObject)'), count)
|
self.emit(SIGNAL('added(PyQt_PyObject)'), count)
|
||||||
count += 1
|
count += 1
|
||||||
self.emit(SIGNAL('adding_done()'))
|
self.emit(SIGNAL('adding_done()'))
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
class RecursiveFind(QThread): # {{{
|
||||||
class RecursiveFind(QThread):
|
|
||||||
|
|
||||||
def __init__(self, parent, db, root, single):
|
def __init__(self, parent, db, root, single):
|
||||||
QThread.__init__(self, parent)
|
QThread.__init__(self, parent)
|
||||||
@ -79,7 +79,9 @@ class RecursiveFind(QThread):
|
|||||||
if not self.canceled:
|
if not self.canceled:
|
||||||
self.emit(SIGNAL('found(PyQt_PyObject)'), self.books)
|
self.emit(SIGNAL('found(PyQt_PyObject)'), self.books)
|
||||||
|
|
||||||
class DBAdder(Thread):
|
# }}}
|
||||||
|
|
||||||
|
class DBAdder(Thread): # {{{
|
||||||
|
|
||||||
def __init__(self, db, ids, nmap):
|
def __init__(self, db, ids, nmap):
|
||||||
self.db, self.ids, self.nmap = db, dict(**ids), dict(**nmap)
|
self.db, self.ids, self.nmap = db, dict(**ids), dict(**nmap)
|
||||||
@ -219,8 +221,9 @@ class DBAdder(Thread):
|
|||||||
self.db.add_format(id, fmt, f, index_is_id=True,
|
self.db.add_format(id, fmt, f, index_is_id=True,
|
||||||
notify=False, replace=replace)
|
notify=False, replace=replace)
|
||||||
|
|
||||||
|
# }}}
|
||||||
|
|
||||||
class Adder(QObject):
|
class Adder(QObject): # {{{
|
||||||
|
|
||||||
ADD_TIMEOUT = 600 # seconds
|
ADD_TIMEOUT = 600 # seconds
|
||||||
|
|
||||||
@ -410,6 +413,7 @@ class Adder(QObject):
|
|||||||
return getattr(getattr(self, 'db_adder', None), 'infos',
|
return getattr(getattr(self, 'db_adder', None), 'infos',
|
||||||
[])
|
[])
|
||||||
|
|
||||||
|
# }}}
|
||||||
|
|
||||||
###############################################################################
|
###############################################################################
|
||||||
############################## END ADDER ######################################
|
############################## END ADDER ######################################
|
||||||
|
@ -9,14 +9,14 @@ import os, collections
|
|||||||
|
|
||||||
from PyQt4.Qt import QLabel, QPixmap, QSize, QWidget, Qt, pyqtSignal, \
|
from PyQt4.Qt import QLabel, QPixmap, QSize, QWidget, Qt, pyqtSignal, \
|
||||||
QVBoxLayout, QScrollArea, QPropertyAnimation, QEasingCurve, \
|
QVBoxLayout, QScrollArea, QPropertyAnimation, QEasingCurve, \
|
||||||
QSizePolicy, QPainter, QRect, pyqtProperty, QDesktopServices, QUrl
|
QSizePolicy, QPainter, QRect, pyqtProperty
|
||||||
|
|
||||||
from calibre import fit_image, prepare_string_for_xml
|
from calibre import fit_image, prepare_string_for_xml
|
||||||
from calibre.gui2.widgets import IMAGE_EXTENSIONS
|
from calibre.gui2.widgets import IMAGE_EXTENSIONS
|
||||||
from calibre.ebooks import BOOK_EXTENSIONS
|
from calibre.ebooks import BOOK_EXTENSIONS
|
||||||
from calibre.constants import preferred_encoding
|
from calibre.constants import preferred_encoding
|
||||||
from calibre.library.comments import comments_to_html
|
from calibre.library.comments import comments_to_html
|
||||||
from calibre.gui2 import config
|
from calibre.gui2 import config, open_local_file
|
||||||
|
|
||||||
# render_rows(data) {{{
|
# render_rows(data) {{{
|
||||||
WEIGHTS = collections.defaultdict(lambda : 100)
|
WEIGHTS = collections.defaultdict(lambda : 100)
|
||||||
@ -294,7 +294,7 @@ class BookDetails(QWidget): # {{{
|
|||||||
id_, fmt = val.split(':')
|
id_, fmt = val.split(':')
|
||||||
self.view_specific_format.emit(int(id_), fmt)
|
self.view_specific_format.emit(int(id_), fmt)
|
||||||
elif typ == 'devpath':
|
elif typ == 'devpath':
|
||||||
QDesktopServices.openUrl(QUrl.fromLocalFile(val))
|
open_local_file(val)
|
||||||
|
|
||||||
|
|
||||||
def mouseReleaseEvent(self, ev):
|
def mouseReleaseEvent(self, ev):
|
||||||
|
@ -67,6 +67,13 @@ if pictureflow is not None:
|
|||||||
ans = ''
|
ans = ''
|
||||||
return ans
|
return ans
|
||||||
|
|
||||||
|
def subtitle(self, index):
|
||||||
|
try:
|
||||||
|
return u'\u2605'*self.model.rating(index)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
return ''
|
||||||
|
|
||||||
def reset(self):
|
def reset(self):
|
||||||
self.dataChanged.emit()
|
self.dataChanged.emit()
|
||||||
|
|
||||||
@ -115,6 +122,7 @@ class CoverFlowMixin(object):
|
|||||||
self.sync_cf_to_listview)
|
self.sync_cf_to_listview)
|
||||||
self.db_images = DatabaseImages(self.library_view.model())
|
self.db_images = DatabaseImages(self.library_view.model())
|
||||||
self.cover_flow.setImages(self.db_images)
|
self.cover_flow.setImages(self.db_images)
|
||||||
|
self.cover_flow.itemActivated.connect(self.view_specific_book)
|
||||||
else:
|
else:
|
||||||
self.cover_flow = QLabel('<p>'+_('Cover browser could not be loaded')
|
self.cover_flow = QLabel('<p>'+_('Cover browser could not be loaded')
|
||||||
+'<br>'+pictureflowerror)
|
+'<br>'+pictureflowerror)
|
||||||
|
@ -31,6 +31,8 @@ from calibre.utils.smtp import compose_mail, sendmail, extract_email_address, \
|
|||||||
config as email_config
|
config as email_config
|
||||||
from calibre.devices.apple.driver import ITUNES_ASYNC
|
from calibre.devices.apple.driver import ITUNES_ASYNC
|
||||||
from calibre.devices.folder_device.driver import FOLDER_DEVICE
|
from calibre.devices.folder_device.driver import FOLDER_DEVICE
|
||||||
|
from calibre.ebooks.metadata.meta import set_metadata
|
||||||
|
from calibre.constants import DEBUG
|
||||||
|
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
@ -304,6 +306,21 @@ class DeviceManager(Thread): # {{{
|
|||||||
|
|
||||||
def _upload_books(self, files, names, on_card=None, metadata=None):
|
def _upload_books(self, files, names, on_card=None, metadata=None):
|
||||||
'''Upload books to device: '''
|
'''Upload books to device: '''
|
||||||
|
if metadata and files and len(metadata) == len(files):
|
||||||
|
for f, mi in zip(files, metadata):
|
||||||
|
if isinstance(f, unicode):
|
||||||
|
ext = f.rpartition('.')[-1].lower()
|
||||||
|
if ext:
|
||||||
|
try:
|
||||||
|
if DEBUG:
|
||||||
|
prints('Setting metadata in:', mi.title, 'at:',
|
||||||
|
f, file=sys.__stdout__)
|
||||||
|
with open(f, 'r+b') as stream:
|
||||||
|
set_metadata(stream, mi, stream_type=ext)
|
||||||
|
except:
|
||||||
|
if DEBUG:
|
||||||
|
prints(traceback.format_exc(), file=sys.__stdout__)
|
||||||
|
|
||||||
return self.device.upload_books(files, names, on_card,
|
return self.device.upload_books(files, names, on_card,
|
||||||
metadata=metadata, end_session=False)
|
metadata=metadata, end_session=False)
|
||||||
|
|
||||||
@ -495,7 +512,7 @@ class DeviceMenu(QMenu): # {{{
|
|||||||
self.connect_to_folder_action = mitem
|
self.connect_to_folder_action = mitem
|
||||||
|
|
||||||
mitem = self.addAction(QIcon(I('devices/itunes.png')),
|
mitem = self.addAction(QIcon(I('devices/itunes.png')),
|
||||||
_('Connect to iTunes (EXPERIMENTAL)'))
|
_('Connect to iTunes'))
|
||||||
mitem.setEnabled(True)
|
mitem.setEnabled(True)
|
||||||
mitem.triggered.connect(lambda x : self.connect_to_itunes.emit())
|
mitem.triggered.connect(lambda x : self.connect_to_itunes.emit())
|
||||||
self.connect_to_itunes_action = mitem
|
self.connect_to_itunes_action = mitem
|
||||||
@ -741,10 +758,8 @@ class DeviceMixin(object): # {{{
|
|||||||
self.refresh_ondevice_info (device_connected = True, reset_only = True)
|
self.refresh_ondevice_info (device_connected = True, reset_only = True)
|
||||||
else:
|
else:
|
||||||
self.device_connected = None
|
self.device_connected = None
|
||||||
|
self.status_bar.device_disconnected()
|
||||||
self.location_view.model().update_devices()
|
self.location_view.model().update_devices()
|
||||||
self.vanity.setText(self.vanity_template%\
|
|
||||||
dict(version=self.latest_version, device=' '))
|
|
||||||
self.device_info = ' '
|
|
||||||
if self.current_view() != self.library_view:
|
if self.current_view() != self.library_view:
|
||||||
self.book_details.reset_info()
|
self.book_details.reset_info()
|
||||||
self.location_view.setCurrentIndex(self.location_view.model().index(0))
|
self.location_view.setCurrentIndex(self.location_view.model().index(0))
|
||||||
@ -758,10 +773,7 @@ class DeviceMixin(object): # {{{
|
|||||||
return self.device_job_exception(job)
|
return self.device_job_exception(job)
|
||||||
info, cp, fs = job.result
|
info, cp, fs = job.result
|
||||||
self.location_view.model().update_devices(cp, fs)
|
self.location_view.model().update_devices(cp, fs)
|
||||||
self.device_info = _('Connected ')+info[0]
|
self.status_bar.device_connected(info[0])
|
||||||
self.vanity.setText(self.vanity_template%\
|
|
||||||
dict(version=self.latest_version, device=self.device_info))
|
|
||||||
|
|
||||||
self.device_manager.books(Dispatcher(self.metadata_downloaded))
|
self.device_manager.books(Dispatcher(self.metadata_downloaded))
|
||||||
|
|
||||||
def metadata_downloaded(self, job):
|
def metadata_downloaded(self, job):
|
||||||
@ -1145,7 +1157,6 @@ class DeviceMixin(object): # {{{
|
|||||||
|
|
||||||
_files, _auto_ids = self.library_view.model().get_preferred_formats_from_ids(ids,
|
_files, _auto_ids = self.library_view.model().get_preferred_formats_from_ids(ids,
|
||||||
settings.format_map,
|
settings.format_map,
|
||||||
set_metadata=True,
|
|
||||||
specific_format=specific_format,
|
specific_format=specific_format,
|
||||||
exclude_auto=do_auto_convert)
|
exclude_auto=do_auto_convert)
|
||||||
if do_auto_convert:
|
if do_auto_convert:
|
||||||
@ -1416,7 +1427,6 @@ class DeviceMixin(object): # {{{
|
|||||||
# the application_id, which is really the db key, but as this can
|
# the application_id, which is really the db key, but as this can
|
||||||
# accidentally match across libraries we also verify the title. The
|
# accidentally match across libraries we also verify the title. The
|
||||||
# db_id exists on Sony devices. Fallback is title and author match
|
# db_id exists on Sony devices. Fallback is title and author match
|
||||||
resend_metadata = False
|
|
||||||
for booklist in booklists:
|
for booklist in booklists:
|
||||||
for book in booklist:
|
for book in booklist:
|
||||||
if getattr(book, 'uuid', None) in self.db_book_uuid_cache:
|
if getattr(book, 'uuid', None) in self.db_book_uuid_cache:
|
||||||
@ -1433,12 +1443,10 @@ class DeviceMixin(object): # {{{
|
|||||||
if getattr(book, 'application_id', None) in d['db_ids']:
|
if getattr(book, 'application_id', None) in d['db_ids']:
|
||||||
book.in_library = True
|
book.in_library = True
|
||||||
book.smart_update(d['db_ids'][book.application_id])
|
book.smart_update(d['db_ids'][book.application_id])
|
||||||
resend_metadata = True
|
|
||||||
continue
|
continue
|
||||||
if book.db_id in d['db_ids']:
|
if book.db_id in d['db_ids']:
|
||||||
book.in_library = True
|
book.in_library = True
|
||||||
book.smart_update(d['db_ids'][book.db_id])
|
book.smart_update(d['db_ids'][book.db_id])
|
||||||
resend_metadata = True
|
|
||||||
continue
|
continue
|
||||||
if book.authors:
|
if book.authors:
|
||||||
# Compare against both author and author sort, because
|
# Compare against both author and author sort, because
|
||||||
@ -1448,21 +1456,13 @@ class DeviceMixin(object): # {{{
|
|||||||
if book_authors in d['authors']:
|
if book_authors in d['authors']:
|
||||||
book.in_library = True
|
book.in_library = True
|
||||||
book.smart_update(d['authors'][book_authors])
|
book.smart_update(d['authors'][book_authors])
|
||||||
resend_metadata = True
|
|
||||||
elif book_authors in d['author_sort']:
|
elif book_authors in d['author_sort']:
|
||||||
book.in_library = True
|
book.in_library = True
|
||||||
book.smart_update(d['author_sort'][book_authors])
|
book.smart_update(d['author_sort'][book_authors])
|
||||||
resend_metadata = True
|
|
||||||
# Set author_sort if it isn't already
|
# Set author_sort if it isn't already
|
||||||
asort = getattr(book, 'author_sort', None)
|
asort = getattr(book, 'author_sort', None)
|
||||||
if not asort and book.authors:
|
if not asort and book.authors:
|
||||||
book.author_sort = self.library_view.model().db.author_sort_from_authors(book.authors)
|
book.author_sort = self.library_view.model().db.author_sort_from_authors(book.authors)
|
||||||
resend_metadata = True
|
|
||||||
|
|
||||||
if resend_metadata:
|
|
||||||
# Correct the metadata cache on device.
|
|
||||||
if self.device_manager.is_device_connected:
|
|
||||||
self.device_manager.sync_booklists(None, booklists)
|
|
||||||
|
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
|
@ -5,11 +5,11 @@ __docformat__ = 'restructuredtext en'
|
|||||||
|
|
||||||
import textwrap, os, re
|
import textwrap, os, re
|
||||||
|
|
||||||
from PyQt4.QtCore import QCoreApplication, SIGNAL, QModelIndex, QUrl, QTimer, Qt
|
from PyQt4.QtCore import QCoreApplication, SIGNAL, QModelIndex, QTimer, Qt
|
||||||
from PyQt4.QtGui import QDialog, QPixmap, QGraphicsScene, QIcon, QDesktopServices
|
from PyQt4.QtGui import QDialog, QPixmap, QGraphicsScene, QIcon
|
||||||
|
|
||||||
from calibre.gui2.dialogs.book_info_ui import Ui_BookInfo
|
from calibre.gui2.dialogs.book_info_ui import Ui_BookInfo
|
||||||
from calibre.gui2 import dynamic
|
from calibre.gui2 import dynamic, open_local_file
|
||||||
from calibre import fit_image
|
from calibre import fit_image
|
||||||
from calibre.library.comments import comments_to_html
|
from calibre.library.comments import comments_to_html
|
||||||
|
|
||||||
@ -49,12 +49,12 @@ class BookInfo(QDialog, Ui_BookInfo):
|
|||||||
|
|
||||||
def open_book_path(self, path):
|
def open_book_path(self, path):
|
||||||
if os.sep in unicode(path):
|
if os.sep in unicode(path):
|
||||||
QDesktopServices.openUrl(QUrl.fromLocalFile(path))
|
open_local_file(path)
|
||||||
else:
|
else:
|
||||||
format = unicode(path)
|
format = unicode(path)
|
||||||
path = self.view.model().db.format_abspath(self.current_row, format)
|
path = self.view.model().db.format_abspath(self.current_row, format)
|
||||||
if path is not None:
|
if path is not None:
|
||||||
QDesktopServices.openUrl(QUrl.fromLocalFile(path))
|
open_local_file(path)
|
||||||
|
|
||||||
|
|
||||||
def next(self):
|
def next(self):
|
||||||
@ -123,6 +123,7 @@ class BookInfo(QDialog, Ui_BookInfo):
|
|||||||
for key in info.keys():
|
for key in info.keys():
|
||||||
if key == 'id': continue
|
if key == 'id': continue
|
||||||
txt = info[key]
|
txt = info[key]
|
||||||
txt = u'<br />\n'.join(textwrap.wrap(txt, 120))
|
if key != _('Path'):
|
||||||
|
txt = u'<br />\n'.join(textwrap.wrap(txt, 120))
|
||||||
rows += u'<tr><td><b>%s:</b></td><td>%s</td></tr>'%(key, txt)
|
rows += u'<tr><td><b>%s:</b></td><td>%s</td></tr>'%(key, txt)
|
||||||
self.text.setText(u'<table>'+rows+'</table>')
|
self.text.setText(u'<table>'+rows+'</table>')
|
||||||
|
@ -4,7 +4,7 @@ __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
|
|||||||
import os, re, time, textwrap, copy, sys
|
import os, re, time, textwrap, copy, sys
|
||||||
|
|
||||||
from PyQt4.Qt import QDialog, QListWidgetItem, QIcon, \
|
from PyQt4.Qt import QDialog, QListWidgetItem, QIcon, \
|
||||||
QDesktopServices, QVBoxLayout, QLabel, QPlainTextEdit, \
|
QVBoxLayout, QLabel, QPlainTextEdit, \
|
||||||
QStringListModel, QAbstractItemModel, QFont, \
|
QStringListModel, QAbstractItemModel, QFont, \
|
||||||
SIGNAL, QThread, Qt, QSize, QVariant, QUrl, \
|
SIGNAL, QThread, Qt, QSize, QVariant, QUrl, \
|
||||||
QModelIndex, QAbstractTableModel, \
|
QModelIndex, QAbstractTableModel, \
|
||||||
@ -15,8 +15,9 @@ from calibre.constants import iswindows, isosx
|
|||||||
from calibre.gui2.dialogs.config.config_ui import Ui_Dialog
|
from calibre.gui2.dialogs.config.config_ui import Ui_Dialog
|
||||||
from calibre.gui2.dialogs.config.create_custom_column import CreateCustomColumn
|
from calibre.gui2.dialogs.config.create_custom_column import CreateCustomColumn
|
||||||
from calibre.gui2 import choose_dir, error_dialog, config, gprefs, \
|
from calibre.gui2 import choose_dir, error_dialog, config, gprefs, \
|
||||||
ALL_COLUMNS, NONE, info_dialog, choose_files, \
|
open_url, open_local_file, \
|
||||||
warning_dialog, ResizableDialog, question_dialog
|
ALL_COLUMNS, NONE, info_dialog, choose_files, \
|
||||||
|
warning_dialog, ResizableDialog, question_dialog
|
||||||
from calibre.utils.config import prefs
|
from calibre.utils.config import prefs
|
||||||
from calibre.ebooks import BOOK_EXTENSIONS
|
from calibre.ebooks import BOOK_EXTENSIONS
|
||||||
from calibre.ebooks.oeb.iterator import is_supported
|
from calibre.ebooks.oeb.iterator import is_supported
|
||||||
@ -494,6 +495,7 @@ class ConfigDialog(ResizableDialog, Ui_Dialog):
|
|||||||
li = i
|
li = i
|
||||||
self.opt_gui_layout.setCurrentIndex(li)
|
self.opt_gui_layout.setCurrentIndex(li)
|
||||||
self.opt_disable_animations.setChecked(config['disable_animations'])
|
self.opt_disable_animations.setChecked(config['disable_animations'])
|
||||||
|
self.opt_show_donate_button.setChecked(config['show_donate_button'])
|
||||||
|
|
||||||
def check_port_value(self, *args):
|
def check_port_value(self, *args):
|
||||||
port = self.port.value()
|
port = self.port.value()
|
||||||
@ -512,7 +514,7 @@ class ConfigDialog(ResizableDialog, Ui_Dialog):
|
|||||||
|
|
||||||
def open_config_dir(self):
|
def open_config_dir(self):
|
||||||
from calibre.utils.config import config_dir
|
from calibre.utils.config import config_dir
|
||||||
QDesktopServices.openUrl(QUrl.fromLocalFile(config_dir))
|
open_local_file(config_dir)
|
||||||
|
|
||||||
def create_symlinks(self):
|
def create_symlinks(self):
|
||||||
from calibre.utils.osx_symlinks import create_symlinks
|
from calibre.utils.osx_symlinks import create_symlinks
|
||||||
@ -805,7 +807,7 @@ class ConfigDialog(ResizableDialog, Ui_Dialog):
|
|||||||
self.stop.setEnabled(False)
|
self.stop.setEnabled(False)
|
||||||
|
|
||||||
def test_server(self):
|
def test_server(self):
|
||||||
QDesktopServices.openUrl(QUrl('http://127.0.0.1:'+str(self.port.value())))
|
open_url(QUrl('http://127.0.0.1:'+str(self.port.value())))
|
||||||
|
|
||||||
def compact(self, toggled):
|
def compact(self, toggled):
|
||||||
d = CheckIntegrity(self.db, self)
|
d = CheckIntegrity(self.db, self)
|
||||||
@ -870,6 +872,7 @@ class ConfigDialog(ResizableDialog, Ui_Dialog):
|
|||||||
config['overwrite_author_title_metadata'] = self.opt_overwrite_author_title_metadata.isChecked()
|
config['overwrite_author_title_metadata'] = self.opt_overwrite_author_title_metadata.isChecked()
|
||||||
config['enforce_cpu_limit'] = bool(self.opt_enforce_cpu_limit.isChecked())
|
config['enforce_cpu_limit'] = bool(self.opt_enforce_cpu_limit.isChecked())
|
||||||
config['disable_animations'] = bool(self.opt_disable_animations.isChecked())
|
config['disable_animations'] = bool(self.opt_disable_animations.isChecked())
|
||||||
|
config['show_donate_button'] = bool(self.opt_show_donate_button.isChecked())
|
||||||
gprefs['show_splash_screen'] = bool(self.show_splash_screen.isChecked())
|
gprefs['show_splash_screen'] = bool(self.show_splash_screen.isChecked())
|
||||||
fmts = []
|
fmts = []
|
||||||
for i in range(self.viewer.count()):
|
for i in range(self.viewer.count()):
|
||||||
|
@ -356,7 +356,7 @@
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="3" column="0" colspan="2">
|
<item row="3" column="0">
|
||||||
<widget class="QCheckBox" name="show_splash_screen">
|
<widget class="QCheckBox" name="show_splash_screen">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Show &splash screen at startup</string>
|
<string>Show &splash screen at startup</string>
|
||||||
@ -665,6 +665,13 @@
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
|
<item row="3" column="1">
|
||||||
|
<widget class="QCheckBox" name="opt_show_donate_button">
|
||||||
|
<property name="text">
|
||||||
|
<string>Show &donate button (restart)</string>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
<widget class="QWidget" name="page_6">
|
<widget class="QWidget" name="page_6">
|
||||||
|
@ -25,6 +25,12 @@ class tableItem(QTableWidgetItem):
|
|||||||
def __lt__(self, other):
|
def __lt__(self, other):
|
||||||
return self.sort < other.sort
|
return self.sort < other.sort
|
||||||
|
|
||||||
|
class centeredTableItem(tableItem):
|
||||||
|
|
||||||
|
def __init__(self, text):
|
||||||
|
tableItem.__init__(self, text)
|
||||||
|
self.setTextAlignment(Qt.AlignCenter)
|
||||||
|
|
||||||
class titleTableItem(tableItem):
|
class titleTableItem(tableItem):
|
||||||
|
|
||||||
def __init__(self, text):
|
def __init__(self, text):
|
||||||
@ -64,10 +70,10 @@ class DeleteMatchingFromDeviceDialog(QDialog, Ui_DeleteMatchingFromDeviceDialog)
|
|||||||
self.buttonBox.accepted.connect(self.accepted)
|
self.buttonBox.accepted.connect(self.accepted)
|
||||||
self.table.cellClicked.connect(self.cell_clicked)
|
self.table.cellClicked.connect(self.cell_clicked)
|
||||||
self.table.setSelectionMode(QAbstractItemView.NoSelection)
|
self.table.setSelectionMode(QAbstractItemView.NoSelection)
|
||||||
self.table.setColumnCount(5)
|
self.table.setColumnCount(7)
|
||||||
self.table.setHorizontalHeaderLabels(
|
self.table.setHorizontalHeaderLabels(
|
||||||
['', _('Location'), _('Title'),
|
['', _('Location'), _('Title'), _('Author'),
|
||||||
_('Author'), _('Date'), _('Format')])
|
_('Date'), _('Format'), _('Path')])
|
||||||
rows = 0
|
rows = 0
|
||||||
for card in items:
|
for card in items:
|
||||||
rows += len(items[card][1])
|
rows += len(items[card][1])
|
||||||
@ -85,7 +91,8 @@ class DeleteMatchingFromDeviceDialog(QDialog, Ui_DeleteMatchingFromDeviceDialog)
|
|||||||
self.table.setItem(row, 2, titleTableItem(book.title))
|
self.table.setItem(row, 2, titleTableItem(book.title))
|
||||||
self.table.setItem(row, 3, authorTableItem(book))
|
self.table.setItem(row, 3, authorTableItem(book))
|
||||||
self.table.setItem(row, 4, dateTableItem(book.datetime))
|
self.table.setItem(row, 4, dateTableItem(book.datetime))
|
||||||
self.table.setItem(row, 5, tableItem(book.path.rpartition('.')[2]))
|
self.table.setItem(row, 5, centeredTableItem(book.path.rpartition('.')[2]))
|
||||||
|
self.table.setItem(row, 6, tableItem(book.path))
|
||||||
row += 1
|
row += 1
|
||||||
self.table.setCurrentCell(0, 1)
|
self.table.setCurrentCell(0, 1)
|
||||||
self.table.resizeColumnsToContents()
|
self.table.resizeColumnsToContents()
|
||||||
|
@ -103,7 +103,7 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
|
|||||||
if _file:
|
if _file:
|
||||||
_file = os.path.abspath(_file)
|
_file = os.path.abspath(_file)
|
||||||
if not os.access(_file, os.R_OK):
|
if not os.access(_file, os.R_OK):
|
||||||
d = error_dialog(self.window, _('Cannot read'),
|
d = error_dialog(self, _('Cannot read'),
|
||||||
_('You do not have permission to read the file: ') + _file)
|
_('You do not have permission to read the file: ') + _file)
|
||||||
d.exec_()
|
d.exec_()
|
||||||
return
|
return
|
||||||
@ -112,14 +112,14 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
|
|||||||
cf = open(_file, "rb")
|
cf = open(_file, "rb")
|
||||||
cover = cf.read()
|
cover = cf.read()
|
||||||
except IOError, e:
|
except IOError, e:
|
||||||
d = error_dialog(self.window, _('Error reading file'),
|
d = error_dialog(self, _('Error reading file'),
|
||||||
_("<p>There was an error reading from file: <br /><b>") + _file + "</b></p><br />"+str(e))
|
_("<p>There was an error reading from file: <br /><b>") + _file + "</b></p><br />"+str(e))
|
||||||
d.exec_()
|
d.exec_()
|
||||||
if cover:
|
if cover:
|
||||||
pix = QPixmap()
|
pix = QPixmap()
|
||||||
pix.loadFromData(cover)
|
pix.loadFromData(cover)
|
||||||
if pix.isNull():
|
if pix.isNull():
|
||||||
d = error_dialog(self.window,
|
d = error_dialog(self,
|
||||||
_("Not a valid picture"),
|
_("Not a valid picture"),
|
||||||
_file + _(" is not a valid picture"))
|
_file + _(" is not a valid picture"))
|
||||||
d.exec_()
|
d.exec_()
|
||||||
@ -162,7 +162,7 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
|
|||||||
self.formats_changed = True
|
self.formats_changed = True
|
||||||
added = True
|
added = True
|
||||||
if bad_perms:
|
if bad_perms:
|
||||||
error_dialog(self.window, _('No permission'),
|
error_dialog(self, _('No permission'),
|
||||||
_('You do not have '
|
_('You do not have '
|
||||||
'permission to read the following files:'),
|
'permission to read the following files:'),
|
||||||
det_msg='\n'.join(bad_perms), show=True)
|
det_msg='\n'.join(bad_perms), show=True)
|
||||||
|
@ -3,13 +3,13 @@ __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
|
|||||||
|
|
||||||
import time, os
|
import time, os
|
||||||
|
|
||||||
from PyQt4.Qt import SIGNAL, QUrl, QDesktopServices, QAbstractListModel, Qt, \
|
from PyQt4.Qt import SIGNAL, QUrl, QAbstractListModel, Qt, \
|
||||||
QVariant, QInputDialog
|
QVariant, QInputDialog
|
||||||
|
|
||||||
from calibre.web.feeds.recipes import compile_recipe
|
from calibre.web.feeds.recipes import compile_recipe
|
||||||
from calibre.web.feeds.news import AutomaticNewsRecipe
|
from calibre.web.feeds.news import AutomaticNewsRecipe
|
||||||
from calibre.gui2.dialogs.user_profiles_ui import Ui_Dialog
|
from calibre.gui2.dialogs.user_profiles_ui import Ui_Dialog
|
||||||
from calibre.gui2 import error_dialog, question_dialog, \
|
from calibre.gui2 import error_dialog, question_dialog, open_url, \
|
||||||
choose_files, ResizableDialog, NONE
|
choose_files, ResizableDialog, NONE
|
||||||
from calibre.gui2.widgets import PythonHighlighter
|
from calibre.gui2.widgets import PythonHighlighter
|
||||||
from calibre.ptempfile import PersistentTemporaryFile
|
from calibre.ptempfile import PersistentTemporaryFile
|
||||||
@ -135,7 +135,7 @@ class UserProfiles(ResizableDialog, Ui_Dialog):
|
|||||||
url.addQueryItem('subject', subject)
|
url.addQueryItem('subject', subject)
|
||||||
url.addQueryItem('body', body)
|
url.addQueryItem('body', body)
|
||||||
url.addQueryItem('attachment', pt.name)
|
url.addQueryItem('attachment', pt.name)
|
||||||
QDesktopServices.openUrl(url)
|
open_url(url)
|
||||||
|
|
||||||
|
|
||||||
def current_changed(self, current, previous):
|
def current_changed(self, current, previous):
|
||||||
|
@ -5,22 +5,22 @@ __license__ = 'GPL v3'
|
|||||||
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||||
__docformat__ = 'restructuredtext en'
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
import functools
|
import functools, sys, os
|
||||||
|
|
||||||
from PyQt4.Qt import QMenu, Qt, pyqtSignal, QToolButton, QIcon, QStackedWidget, \
|
from PyQt4.Qt import QMenu, Qt, pyqtSignal, QToolButton, QIcon, QStackedWidget, \
|
||||||
QSize, QSizePolicy, QStatusBar
|
QSize, QSizePolicy, QStatusBar, QUrl, QLabel, QFont
|
||||||
|
|
||||||
from calibre.utils.config import prefs
|
from calibre.utils.config import prefs
|
||||||
from calibre.ebooks import BOOK_EXTENSIONS
|
from calibre.ebooks import BOOK_EXTENSIONS
|
||||||
from calibre.constants import isosx, __appname__, preferred_encoding
|
from calibre.constants import isosx, __appname__, preferred_encoding, \
|
||||||
from calibre.gui2 import config, is_widescreen
|
__version__
|
||||||
|
from calibre.gui2 import config, is_widescreen, open_url
|
||||||
from calibre.gui2.library.views import BooksView, DeviceBooksView
|
from calibre.gui2.library.views import BooksView, DeviceBooksView
|
||||||
from calibre.gui2.widgets import Splitter
|
from calibre.gui2.widgets import Splitter
|
||||||
from calibre.gui2.tag_view import TagBrowserWidget
|
from calibre.gui2.tag_view import TagBrowserWidget
|
||||||
from calibre.gui2.book_details import BookDetails
|
from calibre.gui2.book_details import BookDetails
|
||||||
from calibre.gui2.notify import get_notifier
|
from calibre.gui2.notify import get_notifier
|
||||||
|
|
||||||
|
|
||||||
_keep_refs = []
|
_keep_refs = []
|
||||||
|
|
||||||
def partial(*args, **kwargs):
|
def partial(*args, **kwargs):
|
||||||
@ -48,6 +48,7 @@ class SaveMenu(QMenu): # {{{
|
|||||||
class ToolbarMixin(object): # {{{
|
class ToolbarMixin(object): # {{{
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
self.action_help.triggered.connect(self.show_help)
|
||||||
md = QMenu()
|
md = QMenu()
|
||||||
md.addAction(_('Edit metadata individually'),
|
md.addAction(_('Edit metadata individually'),
|
||||||
partial(self.edit_metadata, False, bulk=False))
|
partial(self.edit_metadata, False, bulk=False))
|
||||||
@ -182,9 +183,13 @@ class ToolbarMixin(object): # {{{
|
|||||||
for ch in self.tool_bar.children():
|
for ch in self.tool_bar.children():
|
||||||
if isinstance(ch, QToolButton):
|
if isinstance(ch, QToolButton):
|
||||||
ch.setCursor(Qt.PointingHandCursor)
|
ch.setCursor(Qt.PointingHandCursor)
|
||||||
|
ch.setStatusTip(ch.toolTip())
|
||||||
|
|
||||||
self.tool_bar.contextMenuEvent = self.no_op
|
self.tool_bar.contextMenuEvent = self.no_op
|
||||||
|
|
||||||
|
def show_help(self, *args):
|
||||||
|
open_url(QUrl('http://calibre-ebook.com/user_manual'))
|
||||||
|
|
||||||
def read_toolbar_settings(self):
|
def read_toolbar_settings(self):
|
||||||
self.tool_bar.setIconSize(config['toolbar_icon_size'])
|
self.tool_bar.setIconSize(config['toolbar_icon_size'])
|
||||||
self.tool_bar.setToolButtonStyle(
|
self.tool_bar.setToolButtonStyle(
|
||||||
@ -362,12 +367,50 @@ class Stack(QStackedWidget): # {{{
|
|||||||
|
|
||||||
class StatusBar(QStatusBar): # {{{
|
class StatusBar(QStatusBar): # {{{
|
||||||
|
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
QStatusBar.__init__(self, parent)
|
||||||
|
self.default_message = __appname__ + ' ' + _('version') + ' ' + \
|
||||||
|
self.get_version() + ' ' + _('created by Kovid Goyal')
|
||||||
|
self.device_string = ''
|
||||||
|
self.update_label = QLabel('')
|
||||||
|
self.update_label.setOpenExternalLinks(True)
|
||||||
|
self.addPermanentWidget(self.update_label)
|
||||||
|
self.update_label.setVisible(False)
|
||||||
|
self._font = QFont()
|
||||||
|
self._font.setBold(True)
|
||||||
|
self.setFont(self._font)
|
||||||
|
|
||||||
def initialize(self, systray=None):
|
def initialize(self, systray=None):
|
||||||
self.systray = systray
|
self.systray = systray
|
||||||
self.notifier = get_notifier(systray)
|
self.notifier = get_notifier(systray)
|
||||||
|
self.messageChanged.connect(self.message_changed,
|
||||||
|
type=Qt.QueuedConnection)
|
||||||
|
self.message_changed('')
|
||||||
|
|
||||||
|
def device_connected(self, devname):
|
||||||
|
self.device_string = _('Connected ') + devname
|
||||||
|
self.clearMessage()
|
||||||
|
|
||||||
|
def device_disconnected(self):
|
||||||
|
self.device_string = ''
|
||||||
|
self.clearMessage()
|
||||||
|
|
||||||
|
def new_version_available(self, ver, url):
|
||||||
|
msg = (u'<span style="color:red; font-weight: bold">%s: <a href="%s">%s<a></span>') % (
|
||||||
|
_('Update found'), url, ver)
|
||||||
|
self.update_label.setText(msg)
|
||||||
|
self.update_label.setCursor(Qt.PointingHandCursor)
|
||||||
|
self.update_label.setVisible(True)
|
||||||
|
|
||||||
|
def get_version(self):
|
||||||
|
dv = os.environ.get('CALIBRE_DEVELOP_FROM', None)
|
||||||
|
v = __version__
|
||||||
|
if getattr(sys, 'frozen', False) and dv and os.path.abspath(dv) in sys.path:
|
||||||
|
v += '*'
|
||||||
|
return v
|
||||||
|
|
||||||
def show_message(self, msg, timeout=0):
|
def show_message(self, msg, timeout=0):
|
||||||
QStatusBar.showMessage(self, msg, timeout)
|
self.showMessage(msg, timeout)
|
||||||
if self.notifier is not None and not config['disable_tray_notification']:
|
if self.notifier is not None and not config['disable_tray_notification']:
|
||||||
if isosx and isinstance(msg, unicode):
|
if isosx and isinstance(msg, unicode):
|
||||||
try:
|
try:
|
||||||
@ -377,7 +420,15 @@ class StatusBar(QStatusBar): # {{{
|
|||||||
self.notifier(msg)
|
self.notifier(msg)
|
||||||
|
|
||||||
def clear_message(self):
|
def clear_message(self):
|
||||||
QStatusBar.clearMessage(self)
|
self.clearMessage()
|
||||||
|
|
||||||
|
def message_changed(self, msg):
|
||||||
|
if not msg or msg.isEmpty() or msg.isNull():
|
||||||
|
extra = ''
|
||||||
|
if self.device_string:
|
||||||
|
extra = ' ..::.. ' + self.device_string
|
||||||
|
self.showMessage(self.default_message + extra)
|
||||||
|
|
||||||
|
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
|
@ -20,7 +20,8 @@ from calibre.utils.config import tweaks, prefs
|
|||||||
from calibre.utils.date import dt_factory, qt_to_dt, isoformat
|
from calibre.utils.date import dt_factory, qt_to_dt, isoformat
|
||||||
from calibre.ebooks.metadata.meta import set_metadata as _set_metadata
|
from calibre.ebooks.metadata.meta import set_metadata as _set_metadata
|
||||||
from calibre.utils.search_query_parser import SearchQueryParser
|
from calibre.utils.search_query_parser import SearchQueryParser
|
||||||
from calibre.library.caches import _match, CONTAINS_MATCH, EQUALS_MATCH, REGEXP_MATCH
|
from calibre.library.caches import _match, CONTAINS_MATCH, EQUALS_MATCH, \
|
||||||
|
REGEXP_MATCH, CoverCache
|
||||||
from calibre.library.cli import parse_series_string
|
from calibre.library.cli import parse_series_string
|
||||||
from calibre import strftime, isbytestring, prepare_string_for_xml
|
from calibre import strftime, isbytestring, prepare_string_for_xml
|
||||||
from calibre.constants import filesystem_encoding
|
from calibre.constants import filesystem_encoding
|
||||||
@ -149,21 +150,22 @@ class BooksModel(QAbstractTableModel): # {{{
|
|||||||
self.build_data_convertors()
|
self.build_data_convertors()
|
||||||
self.reset()
|
self.reset()
|
||||||
self.database_changed.emit(db)
|
self.database_changed.emit(db)
|
||||||
|
if self.cover_cache is not None:
|
||||||
|
self.cover_cache.stop()
|
||||||
|
self.cover_cache = CoverCache(db)
|
||||||
|
self.cover_cache.start()
|
||||||
|
def refresh_cover(event, ids):
|
||||||
|
if event == 'cover' and self.cover_cache is not None:
|
||||||
|
self.cover_cache.refresh(ids)
|
||||||
|
db.add_listener(refresh_cover)
|
||||||
|
|
||||||
def refresh_ids(self, ids, current_row=-1):
|
def refresh_ids(self, ids, current_row=-1):
|
||||||
rows = self.db.refresh_ids(ids)
|
rows = self.db.refresh_ids(ids)
|
||||||
if rows:
|
if rows:
|
||||||
self.refresh_rows(rows, current_row=current_row)
|
self.refresh_rows(rows, current_row=current_row)
|
||||||
|
|
||||||
def refresh_cover_cache(self, ids):
|
|
||||||
if self.cover_cache:
|
|
||||||
self.cover_cache.refresh(ids)
|
|
||||||
|
|
||||||
def refresh_rows(self, rows, current_row=-1):
|
def refresh_rows(self, rows, current_row=-1):
|
||||||
for row in rows:
|
for row in rows:
|
||||||
if self.cover_cache:
|
|
||||||
id = self.db.id(row)
|
|
||||||
self.cover_cache.refresh([id])
|
|
||||||
if row == current_row:
|
if row == current_row:
|
||||||
self.new_bookdisplay_data.emit(
|
self.new_bookdisplay_data.emit(
|
||||||
self.get_book_display_info(row))
|
self.get_book_display_info(row))
|
||||||
@ -326,7 +328,7 @@ class BooksModel(QAbstractTableModel): # {{{
|
|||||||
|
|
||||||
def set_cache(self, idx):
|
def set_cache(self, idx):
|
||||||
l, r = 0, self.count()-1
|
l, r = 0, self.count()-1
|
||||||
if self.cover_cache:
|
if self.cover_cache is not None:
|
||||||
l = max(l, idx-self.buffer_size)
|
l = max(l, idx-self.buffer_size)
|
||||||
r = min(r, idx+self.buffer_size)
|
r = min(r, idx+self.buffer_size)
|
||||||
k = min(r-idx, idx-l)
|
k = min(r-idx, idx-l)
|
||||||
@ -490,15 +492,18 @@ class BooksModel(QAbstractTableModel): # {{{
|
|||||||
def title(self, row_number):
|
def title(self, row_number):
|
||||||
return self.db.title(row_number)
|
return self.db.title(row_number)
|
||||||
|
|
||||||
|
def rating(self, row_number):
|
||||||
|
ans = self.db.rating(row_number)
|
||||||
|
ans = ans/2 if ans else 0
|
||||||
|
return int(ans)
|
||||||
|
|
||||||
def cover(self, row_number):
|
def cover(self, row_number):
|
||||||
data = None
|
data = None
|
||||||
try:
|
try:
|
||||||
id = self.db.id(row_number)
|
id = self.db.id(row_number)
|
||||||
if self.cover_cache:
|
if self.cover_cache is not None:
|
||||||
img = self.cover_cache.cover(id)
|
img = self.cover_cache.cover(id)
|
||||||
if img:
|
if not img.isNull():
|
||||||
if img.isNull():
|
|
||||||
img = self.default_image
|
|
||||||
return img
|
return img
|
||||||
if not data:
|
if not data:
|
||||||
data = self.db.cover(row_number)
|
data = self.db.cover(row_number)
|
||||||
@ -937,6 +942,7 @@ class DeviceBooksModel(BooksModel): # {{{
|
|||||||
cname = self.column_map[index.column()]
|
cname = self.column_map[index.column()]
|
||||||
if cname in ('title', 'authors') or \
|
if cname in ('title', 'authors') or \
|
||||||
(cname == 'collections' and \
|
(cname == 'collections' and \
|
||||||
|
callable(getattr(self.db, 'supports_collections', None)) and \
|
||||||
self.db.supports_collections() and \
|
self.db.supports_collections() and \
|
||||||
prefs['preserve_user_collections']):
|
prefs['preserve_user_collections']):
|
||||||
flags |= Qt.ItemIsEditable
|
flags |= Qt.ItemIsEditable
|
||||||
|
@ -501,8 +501,9 @@ class DeviceBooksView(BooksView): # {{{
|
|||||||
|
|
||||||
def contextMenuEvent(self, event):
|
def contextMenuEvent(self, event):
|
||||||
self.edit_collections_menu.setVisible(
|
self.edit_collections_menu.setVisible(
|
||||||
self._model.db.supports_collections() and \
|
callable(getattr(self._model.db, 'supports_collections', None)) and \
|
||||||
prefs['preserve_user_collections'])
|
self._model.db.supports_collections() and \
|
||||||
|
prefs['preserve_user_collections'])
|
||||||
self.context_menu.popup(event.globalPos())
|
self.context_menu.popup(event.globalPos())
|
||||||
event.accept()
|
event.accept()
|
||||||
|
|
||||||
|
@ -96,10 +96,7 @@
|
|||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<widget class="QToolButton" name="donate_button">
|
<widget class="ThrobbingButton" name="donate_button">
|
||||||
<property name="cursor">
|
|
||||||
<cursorShape>PointingHandCursor</cursorShape>
|
|
||||||
</property>
|
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>...</string>
|
<string>...</string>
|
||||||
</property>
|
</property>
|
||||||
@ -107,45 +104,13 @@
|
|||||||
<iconset resource="../../../resources/images.qrc">
|
<iconset resource="../../../resources/images.qrc">
|
||||||
<normaloff>:/images/donate.svg</normaloff>:/images/donate.svg</iconset>
|
<normaloff>:/images/donate.svg</normaloff>:/images/donate.svg</iconset>
|
||||||
</property>
|
</property>
|
||||||
<property name="iconSize">
|
|
||||||
<size>
|
|
||||||
<width>64</width>
|
|
||||||
<height>64</height>
|
|
||||||
</size>
|
|
||||||
</property>
|
|
||||||
<property name="autoRaise">
|
<property name="autoRaise">
|
||||||
<bool>true</bool>
|
<bool>true</bool>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item>
|
<item>
|
||||||
<layout class="QVBoxLayout" name="verticalLayout_3">
|
<layout class="QVBoxLayout" name="verticalLayout_3"/>
|
||||||
<item>
|
|
||||||
<widget class="QLabel" name="vanity">
|
|
||||||
<property name="sizePolicy">
|
|
||||||
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
|
|
||||||
<horstretch>0</horstretch>
|
|
||||||
<verstretch>0</verstretch>
|
|
||||||
</sizepolicy>
|
|
||||||
</property>
|
|
||||||
<property name="maximumSize">
|
|
||||||
<size>
|
|
||||||
<width>16777215</width>
|
|
||||||
<height>90</height>
|
|
||||||
</size>
|
|
||||||
</property>
|
|
||||||
<property name="text">
|
|
||||||
<string/>
|
|
||||||
</property>
|
|
||||||
<property name="textFormat">
|
|
||||||
<enum>Qt::RichText</enum>
|
|
||||||
</property>
|
|
||||||
<property name="openExternalLinks">
|
|
||||||
<bool>true</bool>
|
|
||||||
</property>
|
|
||||||
</widget>
|
|
||||||
</item>
|
|
||||||
</layout>
|
|
||||||
</item>
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</item>
|
</item>
|
||||||
@ -259,7 +224,7 @@
|
|||||||
</size>
|
</size>
|
||||||
</property>
|
</property>
|
||||||
<property name="toolTip">
|
<property name="toolTip">
|
||||||
<string>Choose saved search or enter name for new saved search</string>
|
<string/>
|
||||||
</property>
|
</property>
|
||||||
<property name="minimumContentsLength">
|
<property name="minimumContentsLength">
|
||||||
<number>15</number>
|
<number>15</number>
|
||||||
@ -353,6 +318,8 @@
|
|||||||
<addaction name="action_save"/>
|
<addaction name="action_save"/>
|
||||||
<addaction name="action_del"/>
|
<addaction name="action_del"/>
|
||||||
<addaction name="separator"/>
|
<addaction name="separator"/>
|
||||||
|
<addaction name="action_help"/>
|
||||||
|
<addaction name="separator"/>
|
||||||
<addaction name="action_preferences"/>
|
<addaction name="action_preferences"/>
|
||||||
</widget>
|
</widget>
|
||||||
<action name="action_add">
|
<action name="action_add">
|
||||||
@ -544,6 +511,21 @@
|
|||||||
<string>Ctrl+P</string>
|
<string>Ctrl+P</string>
|
||||||
</property>
|
</property>
|
||||||
</action>
|
</action>
|
||||||
|
<action name="action_help">
|
||||||
|
<property name="icon">
|
||||||
|
<iconset resource="../../../resources/images.qrc">
|
||||||
|
<normaloff>:/images/help.svg</normaloff>:/images/help.svg</iconset>
|
||||||
|
</property>
|
||||||
|
<property name="text">
|
||||||
|
<string>Help</string>
|
||||||
|
</property>
|
||||||
|
<property name="toolTip">
|
||||||
|
<string>Browse the calibre User Manual</string>
|
||||||
|
</property>
|
||||||
|
<property name="shortcut">
|
||||||
|
<string>F1</string>
|
||||||
|
</property>
|
||||||
|
</action>
|
||||||
</widget>
|
</widget>
|
||||||
<customwidgets>
|
<customwidgets>
|
||||||
<customwidget>
|
<customwidget>
|
||||||
@ -561,6 +543,11 @@
|
|||||||
<extends>QComboBox</extends>
|
<extends>QComboBox</extends>
|
||||||
<header>calibre.gui2.search_box</header>
|
<header>calibre.gui2.search_box</header>
|
||||||
</customwidget>
|
</customwidget>
|
||||||
|
<customwidget>
|
||||||
|
<class>ThrobbingButton</class>
|
||||||
|
<extends>QToolButton</extends>
|
||||||
|
<header>calibre/gui2/throbber.h</header>
|
||||||
|
</customwidget>
|
||||||
</customwidgets>
|
</customwidgets>
|
||||||
<resources>
|
<resources>
|
||||||
<include location="../../../resources/images.qrc"/>
|
<include location="../../../resources/images.qrc"/>
|
||||||
|
@ -84,6 +84,7 @@ typedef unsigned short QRgb565;
|
|||||||
#define REFLECTION_FACTOR 1.5
|
#define REFLECTION_FACTOR 1.5
|
||||||
|
|
||||||
#define MAX(x, y) ((x > y) ? x : y)
|
#define MAX(x, y) ((x > y) ? x : y)
|
||||||
|
#define MIN(x, y) ((x < y) ? x : y)
|
||||||
|
|
||||||
#define RGB565_RED_MASK 0xF800
|
#define RGB565_RED_MASK 0xF800
|
||||||
#define RGB565_GREEN_MASK 0x07E0
|
#define RGB565_GREEN_MASK 0x07E0
|
||||||
@ -578,12 +579,10 @@ void PictureFlowPrivate::resetSlides()
|
|||||||
|
|
||||||
static QImage prepareSurface(QImage img, int w, int h)
|
static QImage prepareSurface(QImage img, int w, int h)
|
||||||
{
|
{
|
||||||
Qt::TransformationMode mode = Qt::SmoothTransformation;
|
img = img.scaled(w, h, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
|
||||||
img = img.scaled(w, h, Qt::IgnoreAspectRatio, mode);
|
|
||||||
|
|
||||||
// slightly larger, to accommodate for the reflection
|
// slightly larger, to accommodate for the reflection
|
||||||
int hs = int(h * REFLECTION_FACTOR);
|
int hs = int(h * REFLECTION_FACTOR);
|
||||||
int hofs = 0;
|
|
||||||
|
|
||||||
// offscreen buffer: black is sweet
|
// offscreen buffer: black is sweet
|
||||||
QImage result(hs, w, QImage::Format_RGB16);
|
QImage result(hs, w, QImage::Format_RGB16);
|
||||||
@ -594,21 +593,20 @@ static QImage prepareSurface(QImage img, int w, int h)
|
|||||||
// (and much better and faster to work row-wise, i.e in one scanline)
|
// (and much better and faster to work row-wise, i.e in one scanline)
|
||||||
for(int x = 0; x < w; x++)
|
for(int x = 0; x < w; x++)
|
||||||
for(int y = 0; y < h; y++)
|
for(int y = 0; y < h; y++)
|
||||||
result.setPixel(hofs + y, x, img.pixel(x, y));
|
result.setPixel(y, x, img.pixel(x, y));
|
||||||
|
|
||||||
// create the reflection
|
// create the reflection
|
||||||
int ht = hs - h - hofs;
|
int ht = hs - h;
|
||||||
int hte = ht;
|
|
||||||
for(int x = 0; x < w; x++)
|
for(int x = 0; x < w; x++)
|
||||||
for(int y = 0; y < ht; y++)
|
for(int y = 0; y < ht; y++)
|
||||||
{
|
{
|
||||||
QRgb color = img.pixel(x, img.height()-y-1);
|
QRgb color = img.pixel(x, img.height()-y-1);
|
||||||
//QRgb565 color = img.scanLine(img.height()-y-1) + x*sizeof(QRgb565); //img.pixel(x, img.height()-y-1);
|
//QRgb565 color = img.scanLine(img.height()-y-1) + x*sizeof(QRgb565); //img.pixel(x, img.height()-y-1);
|
||||||
int a = qAlpha(color);
|
int a = qAlpha(color);
|
||||||
int r = qRed(color) * a / 256 * (hte - y) / hte * 3/5;
|
int r = qRed(color) * a / 256 * (ht - y) / ht * 3/5;
|
||||||
int g = qGreen(color) * a / 256 * (hte - y) / hte * 3/5;
|
int g = qGreen(color) * a / 256 * (ht - y) / ht * 3/5;
|
||||||
int b = qBlue(color) * a / 256 * (hte - y) / hte * 3/5;
|
int b = qBlue(color) * a / 256 * (ht - y) / ht * 3/5;
|
||||||
result.setPixel(h+hofs+y, x, qRgb(r, g, b));
|
result.setPixel(h+y, x, qRgb(r, g, b));
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
@ -708,9 +706,12 @@ void PictureFlowPrivate::render()
|
|||||||
painter.setPen(Qt::white);
|
painter.setPen(Qt::white);
|
||||||
//painter.setPen(QColor(255,255,255,127));
|
//painter.setPen(QColor(255,255,255,127));
|
||||||
|
|
||||||
if (centerIndex < slideCount() && centerIndex > -1)
|
if (centerIndex < slideCount() && centerIndex > -1) {
|
||||||
painter.drawText( QRect(0,0, buffer.width(), buffer.height()*2-fontSize*3),
|
painter.drawText( QRect(0,0, buffer.width(), buffer.height()*2-fontSize*4),
|
||||||
Qt::AlignCenter, slideImages->caption(centerIndex));
|
Qt::AlignCenter, slideImages->caption(centerIndex));
|
||||||
|
painter.drawText( QRect(0,0, buffer.width(), buffer.height()*2-fontSize*2),
|
||||||
|
Qt::AlignCenter, slideImages->subtitle(centerIndex));
|
||||||
|
}
|
||||||
|
|
||||||
painter.end();
|
painter.end();
|
||||||
|
|
||||||
@ -761,15 +762,22 @@ void PictureFlowPrivate::render()
|
|||||||
int sc = slideCount();
|
int sc = slideCount();
|
||||||
|
|
||||||
painter.setPen(QColor(255,255,255, (255-fade) ));
|
painter.setPen(QColor(255,255,255, (255-fade) ));
|
||||||
if (leftTextIndex < sc && leftTextIndex > -1)
|
if (leftTextIndex < sc && leftTextIndex > -1) {
|
||||||
painter.drawText( QRect(0,0, buffer.width(), buffer.height()*2 - fontSize*3),
|
painter.drawText( QRect(0,0, buffer.width(), buffer.height()*2 - fontSize*4),
|
||||||
Qt::AlignCenter, slideImages->caption(leftTextIndex));
|
Qt::AlignCenter, slideImages->caption(leftTextIndex));
|
||||||
|
painter.drawText( QRect(0,0, buffer.width(), buffer.height()*2 - fontSize*2),
|
||||||
|
Qt::AlignCenter, slideImages->subtitle(leftTextIndex));
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
painter.setPen(QColor(255,255,255, fade));
|
painter.setPen(QColor(255,255,255, fade));
|
||||||
if (leftTextIndex+1 < sc && leftTextIndex > -2)
|
if (leftTextIndex+1 < sc && leftTextIndex > -2) {
|
||||||
painter.drawText( QRect(0,0, buffer.width(), buffer.height()*2 - fontSize*3),
|
painter.drawText( QRect(0,0, buffer.width(), buffer.height()*2 - fontSize*4),
|
||||||
Qt::AlignCenter, slideImages->caption(leftTextIndex+1));
|
Qt::AlignCenter, slideImages->caption(leftTextIndex+1));
|
||||||
|
painter.drawText( QRect(0,0, buffer.width(), buffer.height()*2 - fontSize*2),
|
||||||
|
Qt::AlignCenter, slideImages->subtitle(leftTextIndex+1));
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
painter.end();
|
painter.end();
|
||||||
}
|
}
|
||||||
@ -797,12 +805,20 @@ QRect PictureFlowPrivate::renderCenterSlide(const SlideInfo &slide) {
|
|||||||
int sw = src->height();
|
int sw = src->height();
|
||||||
int sh = src->width();
|
int sh = src->width();
|
||||||
int h = buffer.height();
|
int h = buffer.height();
|
||||||
QRect rect(buffer.width()/2 - sw/2, 0, sw, h-1);
|
int srcoff = 0;
|
||||||
int left = rect.left();
|
int left = buffer.width()/2 - sw/2;
|
||||||
|
if (left < 0) {
|
||||||
|
srcoff = -left;
|
||||||
|
sw += left;
|
||||||
|
left = 0;
|
||||||
|
}
|
||||||
|
QRect rect(left, 0, sw, h-1);
|
||||||
|
int xcon = MIN(h-1, sh-1);
|
||||||
|
int ycon = MIN(sw, buffer.width() - left);
|
||||||
|
|
||||||
for(int x = 0; x < sh-1; x++)
|
for(int x = 0; x < xcon; x++)
|
||||||
for(int y = 0; y < sw; y++)
|
for(int y = 0; y < ycon; y++)
|
||||||
buffer.setPixel(left + y, 1+x, src->pixel(x, y));
|
buffer.setPixel(left + y, 1+x, src->pixel(x, srcoff+y));
|
||||||
|
|
||||||
return rect;
|
return rect;
|
||||||
}
|
}
|
||||||
@ -1366,5 +1382,6 @@ void PictureFlow::emitcurrentChanged(int index) { emit currentChanged(index); }
|
|||||||
int FlowImages::count() { return 0; }
|
int FlowImages::count() { return 0; }
|
||||||
QImage FlowImages::image(int index) { index=0; return QImage(); }
|
QImage FlowImages::image(int index) { index=0; return QImage(); }
|
||||||
QString FlowImages::caption(int index) {index=0; return QString(); }
|
QString FlowImages::caption(int index) {index=0; return QString(); }
|
||||||
|
QString FlowImages::subtitle(int index) {index=0; return QString(); }
|
||||||
|
|
||||||
// }}}
|
// }}}
|
||||||
|
@ -67,6 +67,7 @@ public:
|
|||||||
virtual int count();
|
virtual int count();
|
||||||
virtual QImage image(int index);
|
virtual QImage image(int index);
|
||||||
virtual QString caption(int index);
|
virtual QString caption(int index);
|
||||||
|
virtual QString subtitle(int index);
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void dataChanged();
|
void dataChanged();
|
||||||
|
@ -16,6 +16,7 @@ public:
|
|||||||
virtual int count();
|
virtual int count();
|
||||||
virtual QImage image(int index);
|
virtual QImage image(int index);
|
||||||
virtual QString caption(int index);
|
virtual QString caption(int index);
|
||||||
|
virtual QString subtitle(int index);
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void dataChanged();
|
void dataChanged();
|
||||||
|
@ -6,6 +6,8 @@ __license__ = 'GPL v3'
|
|||||||
__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
|
__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||||
__docformat__ = 'restructuredtext en'
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
|
import re
|
||||||
|
|
||||||
from PyQt4.Qt import QComboBox, Qt, QLineEdit, QStringList, pyqtSlot, \
|
from PyQt4.Qt import QComboBox, Qt, QLineEdit, QStringList, pyqtSlot, \
|
||||||
pyqtSignal, SIGNAL, QObject, QDialog, QCompleter, \
|
pyqtSignal, SIGNAL, QObject, QDialog, QCompleter, \
|
||||||
QAction, QKeySequence
|
QAction, QKeySequence
|
||||||
@ -368,6 +370,10 @@ class SearchBoxMixin(object):
|
|||||||
self.action_focus_search.triggered.connect(lambda x:
|
self.action_focus_search.triggered.connect(lambda x:
|
||||||
self.search.setFocus(Qt.OtherFocusReason))
|
self.search.setFocus(Qt.OtherFocusReason))
|
||||||
self.addAction(self.action_focus_search)
|
self.addAction(self.action_focus_search)
|
||||||
|
self.search.setStatusTip(re.sub(r'<\w+>', ' ',
|
||||||
|
unicode(self.search.toolTip())))
|
||||||
|
self.advanced_search_button.setStatusTip(self.advanced_search_button.toolTip())
|
||||||
|
self.clear_button.setStatusTip(self.clear_button.toolTip())
|
||||||
|
|
||||||
def search_box_cleared(self):
|
def search_box_cleared(self):
|
||||||
self.tags_view.clear()
|
self.tags_view.clear()
|
||||||
@ -396,6 +402,12 @@ class SavedSearchBoxMixin(object):
|
|||||||
self.saved_search.delete_search_button_clicked)
|
self.saved_search.delete_search_button_clicked)
|
||||||
self.connect(self.copy_search_button, SIGNAL('clicked()'),
|
self.connect(self.copy_search_button, SIGNAL('clicked()'),
|
||||||
self.saved_search.copy_search_button_clicked)
|
self.saved_search.copy_search_button_clicked)
|
||||||
|
self.saved_search.setToolTip(
|
||||||
|
_('Choose saved search or enter name for new saved search'))
|
||||||
|
self.saved_search.setStatusTip(self.saved_search.toolTip())
|
||||||
|
for x in ('copy', 'save', 'delete'):
|
||||||
|
b = getattr(self, x+'_search_button')
|
||||||
|
b.setStatusTip(b.toolTip())
|
||||||
|
|
||||||
|
|
||||||
def saved_searches_changed(self):
|
def saved_searches_changed(self):
|
||||||
|
@ -11,6 +11,7 @@ class SearchRestrictionMixin(object):
|
|||||||
self.library_view.model().count_changed_signal.connect(self.restriction_count_changed)
|
self.library_view.model().count_changed_signal.connect(self.restriction_count_changed)
|
||||||
self.search_restriction.setSizeAdjustPolicy(self.search_restriction.AdjustToMinimumContentsLengthWithIcon)
|
self.search_restriction.setSizeAdjustPolicy(self.search_restriction.AdjustToMinimumContentsLengthWithIcon)
|
||||||
self.search_restriction.setMinimumContentsLength(10)
|
self.search_restriction.setMinimumContentsLength(10)
|
||||||
|
self.search_restriction.setStatusTip(self.search_restriction.toolTip())
|
||||||
|
|
||||||
'''
|
'''
|
||||||
Adding and deleting books while restricted creates a complexity. When added,
|
Adding and deleting books while restricted creates a complexity. When added,
|
||||||
|
@ -768,6 +768,9 @@ class TagBrowserWidget(QWidget): # {{{
|
|||||||
for x in (_('Sort by name'), _('Sort by popularity'),
|
for x in (_('Sort by name'), _('Sort by popularity'),
|
||||||
_('Sort by average rating')):
|
_('Sort by average rating')):
|
||||||
parent.sort_by.addItem(x)
|
parent.sort_by.addItem(x)
|
||||||
|
parent.sort_by.setToolTip(
|
||||||
|
_('Set the sort order for entries in the Tag Browser'))
|
||||||
|
parent.sort_by.setStatusTip(parent.sort_by.toolTip())
|
||||||
parent.sort_by.setCurrentIndex(0)
|
parent.sort_by.setCurrentIndex(0)
|
||||||
self._layout.addWidget(parent.sort_by)
|
self._layout.addWidget(parent.sort_by)
|
||||||
|
|
||||||
@ -776,9 +779,16 @@ class TagBrowserWidget(QWidget): # {{{
|
|||||||
parent.tag_match.addItem(x)
|
parent.tag_match.addItem(x)
|
||||||
parent.tag_match.setCurrentIndex(0)
|
parent.tag_match.setCurrentIndex(0)
|
||||||
self._layout.addWidget(parent.tag_match)
|
self._layout.addWidget(parent.tag_match)
|
||||||
|
parent.tag_match.setToolTip(
|
||||||
|
_('When selecting multiple entries in the Tag Browser '
|
||||||
|
'match any or all of them'))
|
||||||
|
parent.tag_match.setStatusTip(parent.tag_match.toolTip())
|
||||||
|
|
||||||
parent.edit_categories = QPushButton(_('Manage &user categories'), parent)
|
parent.edit_categories = QPushButton(_('Manage &user categories'), parent)
|
||||||
self._layout.addWidget(parent.edit_categories)
|
self._layout.addWidget(parent.edit_categories)
|
||||||
|
parent.edit_categories.setToolTip(
|
||||||
|
_('Add your own categories to the Tag Browser'))
|
||||||
|
parent.edit_categories.setStatusTip(parent.edit_categories.toolTip())
|
||||||
|
|
||||||
|
|
||||||
# }}}
|
# }}}
|
||||||
|
70
src/calibre/gui2/throbber.py
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
|
||||||
|
|
||||||
|
__license__ = 'GPL v3'
|
||||||
|
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||||
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
|
|
||||||
|
from PyQt4.Qt import QToolButton, QSize, QPropertyAnimation, Qt, \
|
||||||
|
QMetaObject
|
||||||
|
|
||||||
|
from calibre.gui2 import config
|
||||||
|
|
||||||
|
class ThrobbingButton(QToolButton):
|
||||||
|
|
||||||
|
def __init__(self, *args):
|
||||||
|
QToolButton.__init__(self, *args)
|
||||||
|
self.animation = QPropertyAnimation(self, 'iconSize', self)
|
||||||
|
self.animation.setDuration(60/72.*1000)
|
||||||
|
self.animation.setLoopCount(4)
|
||||||
|
self.normal_icon_size = QSize(64, 64)
|
||||||
|
self.animation.valueChanged.connect(self.value_changed)
|
||||||
|
self.setCursor(Qt.PointingHandCursor)
|
||||||
|
self.animation.finished.connect(self.animation_finished)
|
||||||
|
|
||||||
|
def set_normal_icon_size(self, w, h):
|
||||||
|
self.normal_icon_size = QSize(w, h)
|
||||||
|
self.setIconSize(self.normal_icon_size)
|
||||||
|
self.setMinimumSize(self.sizeHint())
|
||||||
|
|
||||||
|
def animation_finished(self):
|
||||||
|
self.setIconSize(self.normal_icon_size)
|
||||||
|
|
||||||
|
def enterEvent(self, ev):
|
||||||
|
self.start_animation()
|
||||||
|
|
||||||
|
def leaveEvent(self, ev):
|
||||||
|
self.stop_animation()
|
||||||
|
|
||||||
|
def value_changed(self, val):
|
||||||
|
self.update()
|
||||||
|
|
||||||
|
def start_animation(self):
|
||||||
|
if config['disable_animations']: return
|
||||||
|
if self.animation.state() != self.animation.Stopped or not self.isVisible():
|
||||||
|
return
|
||||||
|
size = self.normal_icon_size.width()
|
||||||
|
smaller = int(0.7 * size)
|
||||||
|
self.animation.setStartValue(QSize(smaller, smaller))
|
||||||
|
self.animation.setEndValue(self.normal_icon_size)
|
||||||
|
QMetaObject.invokeMethod(self.animation, 'start', Qt.QueuedConnection)
|
||||||
|
|
||||||
|
def stop_animation(self):
|
||||||
|
self.animation.stop()
|
||||||
|
self.animation_finished()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
from PyQt4.Qt import QApplication, QWidget, QHBoxLayout, QIcon
|
||||||
|
app = QApplication([])
|
||||||
|
w = QWidget()
|
||||||
|
w.setLayout(QHBoxLayout())
|
||||||
|
b = ThrobbingButton()
|
||||||
|
b.setIcon(QIcon(I('donate.svg')))
|
||||||
|
w.layout().addWidget(b)
|
||||||
|
w.show()
|
||||||
|
b.set_normal_icon_size(64, 64)
|
||||||
|
b.start_animation()
|
||||||
|
|
||||||
|
app.exec_()
|
@ -12,18 +12,18 @@ __docformat__ = 'restructuredtext en'
|
|||||||
import collections, os, sys, textwrap, time
|
import collections, os, sys, textwrap, time
|
||||||
from Queue import Queue, Empty
|
from Queue import Queue, Empty
|
||||||
from threading import Thread
|
from threading import Thread
|
||||||
from PyQt4.Qt import Qt, SIGNAL, QObject, QUrl, QTimer, \
|
from PyQt4.Qt import Qt, SIGNAL, QObject, QTimer, \
|
||||||
QPixmap, QMenu, QIcon, pyqtSignal, \
|
QPixmap, QMenu, QIcon, pyqtSignal, \
|
||||||
QDialog, QDesktopServices, \
|
QDialog, \
|
||||||
QSystemTrayIcon, QApplication, QKeySequence, QAction, \
|
QSystemTrayIcon, QApplication, QKeySequence, QAction, \
|
||||||
QMessageBox, QHelpEvent
|
QMessageBox, QHelpEvent
|
||||||
|
|
||||||
from calibre import prints, patheq
|
from calibre import prints, patheq
|
||||||
from calibre.constants import __version__, __appname__, isosx
|
from calibre.constants import __appname__, isosx
|
||||||
from calibre.ptempfile import PersistentTemporaryFile
|
from calibre.ptempfile import PersistentTemporaryFile
|
||||||
from calibre.utils.config import prefs, dynamic
|
from calibre.utils.config import prefs, dynamic
|
||||||
from calibre.utils.ipc.server import Server
|
from calibre.utils.ipc.server import Server
|
||||||
from calibre.gui2 import error_dialog, GetMetadata, \
|
from calibre.gui2 import error_dialog, GetMetadata, open_local_file, \
|
||||||
gprefs, max_available_height, config, info_dialog
|
gprefs, max_available_height, config, info_dialog
|
||||||
from calibre.gui2.cover_flow import CoverFlowMixin
|
from calibre.gui2.cover_flow import CoverFlowMixin
|
||||||
from calibre.gui2.widgets import ProgressIndicator
|
from calibre.gui2.widgets import ProgressIndicator
|
||||||
@ -38,7 +38,6 @@ from calibre.gui2.dialogs.config import ConfigDialog
|
|||||||
|
|
||||||
from calibre.gui2.dialogs.book_info import BookInfo
|
from calibre.gui2.dialogs.book_info import BookInfo
|
||||||
from calibre.library.database2 import LibraryDatabase2
|
from calibre.library.database2 import LibraryDatabase2
|
||||||
from calibre.library.caches import CoverCache
|
|
||||||
from calibre.gui2.init import ToolbarMixin, LibraryViewMixin, LayoutMixin
|
from calibre.gui2.init import ToolbarMixin, LibraryViewMixin, LayoutMixin
|
||||||
from calibre.gui2.search_box import SearchBoxMixin, SavedSearchBoxMixin
|
from calibre.gui2.search_box import SearchBoxMixin, SavedSearchBoxMixin
|
||||||
from calibre.gui2.search_restriction_mixin import SearchRestrictionMixin
|
from calibre.gui2.search_restriction_mixin import SearchRestrictionMixin
|
||||||
@ -138,6 +137,7 @@ class Main(MainWindow, Ui_MainWindow, DeviceMixin, ToolbarMixin, # {{{
|
|||||||
self.restriction_in_effect = False
|
self.restriction_in_effect = False
|
||||||
|
|
||||||
self.progress_indicator = ProgressIndicator(self)
|
self.progress_indicator = ProgressIndicator(self)
|
||||||
|
self.progress_indicator.pos = (0, 20)
|
||||||
self.verbose = opts.verbose
|
self.verbose = opts.verbose
|
||||||
self.get_metadata = GetMetadata()
|
self.get_metadata = GetMetadata()
|
||||||
self.upload_memory = {}
|
self.upload_memory = {}
|
||||||
@ -163,6 +163,7 @@ class Main(MainWindow, Ui_MainWindow, DeviceMixin, ToolbarMixin, # {{{
|
|||||||
self.donate_action = self.system_tray_menu.addAction(
|
self.donate_action = self.system_tray_menu.addAction(
|
||||||
QIcon(I('donate.svg')), _('&Donate to support calibre'))
|
QIcon(I('donate.svg')), _('&Donate to support calibre'))
|
||||||
self.donate_button.setDefaultAction(self.donate_action)
|
self.donate_button.setDefaultAction(self.donate_action)
|
||||||
|
self.donate_button.setStatusTip(self.donate_button.toolTip())
|
||||||
self.eject_action = self.system_tray_menu.addAction(
|
self.eject_action = self.system_tray_menu.addAction(
|
||||||
QIcon(I('eject.svg')), _('&Eject connected device'))
|
QIcon(I('eject.svg')), _('&Eject connected device'))
|
||||||
self.eject_action.setEnabled(False)
|
self.eject_action.setEnabled(False)
|
||||||
@ -202,18 +203,7 @@ class Main(MainWindow, Ui_MainWindow, DeviceMixin, ToolbarMixin, # {{{
|
|||||||
self.device_manager.umount_device)
|
self.device_manager.umount_device)
|
||||||
self.eject_action.triggered.connect(self.device_manager.umount_device)
|
self.eject_action.triggered.connect(self.device_manager.umount_device)
|
||||||
|
|
||||||
####################### Vanity ########################
|
#################### Update notification ###################
|
||||||
self.vanity_template = _('<p>For help see the: <a href="%s">User Manual</a>'
|
|
||||||
'<br>')%'http://calibre-ebook.com/user_manual'
|
|
||||||
dv = os.environ.get('CALIBRE_DEVELOP_FROM', None)
|
|
||||||
v = __version__
|
|
||||||
if getattr(sys, 'frozen', False) and dv and os.path.abspath(dv) in sys.path:
|
|
||||||
v += '*'
|
|
||||||
self.vanity_template += _('<b>%s</b>: %s by <b>Kovid Goyal '
|
|
||||||
'%%(version)s</b><br>%%(device)s</p>')%(__appname__, v)
|
|
||||||
self.latest_version = ' '
|
|
||||||
self.vanity.setText(self.vanity_template%dict(version=' ', device=' '))
|
|
||||||
self.device_info = ' '
|
|
||||||
UpdateMixin.__init__(self, opts)
|
UpdateMixin.__init__(self, opts)
|
||||||
|
|
||||||
####################### Setup Toolbar #####################
|
####################### Setup Toolbar #####################
|
||||||
@ -230,9 +220,6 @@ class Main(MainWindow, Ui_MainWindow, DeviceMixin, ToolbarMixin, # {{{
|
|||||||
|
|
||||||
if self.system_tray_icon.isVisible() and opts.start_in_tray:
|
if self.system_tray_icon.isVisible() and opts.start_in_tray:
|
||||||
self.hide_windows()
|
self.hide_windows()
|
||||||
self.cover_cache = CoverCache(self.library_path)
|
|
||||||
self.cover_cache.start()
|
|
||||||
self.library_view.model().cover_cache = self.cover_cache
|
|
||||||
self.library_view.model().count_changed_signal.connect \
|
self.library_view.model().count_changed_signal.connect \
|
||||||
(self.location_view.count_changed)
|
(self.location_view.count_changed)
|
||||||
if not gprefs.get('quick_start_guide_added', False):
|
if not gprefs.get('quick_start_guide_added', False):
|
||||||
@ -294,6 +281,8 @@ class Main(MainWindow, Ui_MainWindow, DeviceMixin, ToolbarMixin, # {{{
|
|||||||
|
|
||||||
self.read_settings()
|
self.read_settings()
|
||||||
self.finalize_layout()
|
self.finalize_layout()
|
||||||
|
self.donate_button.set_normal_icon_size(64, 64)
|
||||||
|
self.donate_button.start_animation()
|
||||||
|
|
||||||
def resizeEvent(self, ev):
|
def resizeEvent(self, ev):
|
||||||
MainWindow.resizeEvent(self, ev)
|
MainWindow.resizeEvent(self, ev)
|
||||||
@ -557,7 +546,8 @@ class Main(MainWindow, Ui_MainWindow, DeviceMixin, ToolbarMixin, # {{{
|
|||||||
'''
|
'''
|
||||||
MSG = _('is the result of the efforts of many volunteers from all '
|
MSG = _('is the result of the efforts of many volunteers from all '
|
||||||
'over the world. If you find it useful, please consider '
|
'over the world. If you find it useful, please consider '
|
||||||
'donating to support its development.')
|
'donating to support its development. Your donation helps '
|
||||||
|
'keep calibre development going.')
|
||||||
HTML = u'''
|
HTML = u'''
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
@ -575,7 +565,7 @@ class Main(MainWindow, Ui_MainWindow, DeviceMixin, ToolbarMixin, # {{{
|
|||||||
pt = PersistentTemporaryFile('_donate.htm')
|
pt = PersistentTemporaryFile('_donate.htm')
|
||||||
pt.write(HTML.encode('utf-8'))
|
pt.write(HTML.encode('utf-8'))
|
||||||
pt.close()
|
pt.close()
|
||||||
QDesktopServices.openUrl(QUrl.fromLocalFile(pt.name))
|
open_local_file(pt.name)
|
||||||
|
|
||||||
|
|
||||||
def confirm_quit(self):
|
def confirm_quit(self):
|
||||||
@ -606,9 +596,10 @@ class Main(MainWindow, Ui_MainWindow, DeviceMixin, ToolbarMixin, # {{{
|
|||||||
while self.spare_servers:
|
while self.spare_servers:
|
||||||
self.spare_servers.pop().close()
|
self.spare_servers.pop().close()
|
||||||
self.device_manager.keep_going = False
|
self.device_manager.keep_going = False
|
||||||
self.cover_cache.stop()
|
cc = self.library_view.model().cover_cache
|
||||||
|
if cc is not None:
|
||||||
|
cc.stop()
|
||||||
self.hide_windows()
|
self.hide_windows()
|
||||||
self.cover_cache.terminate()
|
|
||||||
self.emailer.stop()
|
self.emailer.stop()
|
||||||
try:
|
try:
|
||||||
try:
|
try:
|
||||||
|
@ -3,13 +3,13 @@ __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
|
|||||||
|
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
from PyQt4.Qt import QThread, pyqtSignal, QDesktopServices, QUrl, Qt
|
from PyQt4.Qt import QThread, pyqtSignal, Qt, QUrl
|
||||||
import mechanize
|
import mechanize
|
||||||
|
|
||||||
from calibre.constants import __appname__, __version__, iswindows, isosx
|
from calibre.constants import __appname__, __version__, iswindows, isosx
|
||||||
from calibre import browser
|
from calibre import browser
|
||||||
from calibre.utils.config import prefs
|
from calibre.utils.config import prefs
|
||||||
from calibre.gui2 import config, dynamic, question_dialog
|
from calibre.gui2 import config, dynamic, question_dialog, open_url
|
||||||
|
|
||||||
URL = 'http://status.calibre-ebook.com/latest'
|
URL = 'http://status.calibre-ebook.com/latest'
|
||||||
|
|
||||||
@ -49,12 +49,8 @@ class UpdateMixin(object):
|
|||||||
def update_found(self, version):
|
def update_found(self, version):
|
||||||
os = 'windows' if iswindows else 'osx' if isosx else 'linux'
|
os = 'windows' if iswindows else 'osx' if isosx else 'linux'
|
||||||
url = 'http://calibre-ebook.com/download_%s'%os
|
url = 'http://calibre-ebook.com/download_%s'%os
|
||||||
self.latest_version = '<br>' + _('<span style="color:red; font-weight:bold">'
|
self.status_bar.new_version_available(version, url)
|
||||||
'Latest version: <a href="%s">%s</a></span>')%(url, version)
|
|
||||||
self.vanity.setText(self.vanity_template%\
|
|
||||||
(dict(version=self.latest_version,
|
|
||||||
device=self.device_info)))
|
|
||||||
self.vanity.update()
|
|
||||||
if config.get('new_version_notification') and \
|
if config.get('new_version_notification') and \
|
||||||
dynamic.get('update to version %s'%version, True):
|
dynamic.get('update to version %s'%version, True):
|
||||||
if question_dialog(self, _('Update available'),
|
if question_dialog(self, _('Update available'),
|
||||||
@ -64,7 +60,7 @@ class UpdateMixin(object):
|
|||||||
'ge?')%(__appname__, version)):
|
'ge?')%(__appname__, version)):
|
||||||
url = 'http://calibre-ebook.com/download_'+\
|
url = 'http://calibre-ebook.com/download_'+\
|
||||||
('windows' if iswindows else 'osx' if isosx else 'linux')
|
('windows' if iswindows else 'osx' if isosx else 'linux')
|
||||||
QDesktopServices.openUrl(QUrl(url))
|
open_url(QUrl(url))
|
||||||
dynamic.set('update to version %s'%version, False)
|
dynamic.set('update to version %s'%version, False)
|
||||||
|
|
||||||
|
|
||||||
|
@ -6,7 +6,7 @@ from functools import partial
|
|||||||
from threading import Thread
|
from threading import Thread
|
||||||
|
|
||||||
from PyQt4.Qt import QApplication, Qt, QIcon, QTimer, SIGNAL, QByteArray, \
|
from PyQt4.Qt import QApplication, Qt, QIcon, QTimer, SIGNAL, QByteArray, \
|
||||||
QDesktopServices, QDoubleSpinBox, QLabel, QTextBrowser, \
|
QDoubleSpinBox, QLabel, QTextBrowser, \
|
||||||
QPainter, QBrush, QColor, QStandardItemModel, QPalette, \
|
QPainter, QBrush, QColor, QStandardItemModel, QPalette, \
|
||||||
QStandardItem, QUrl, QRegExpValidator, QRegExp, QLineEdit, \
|
QStandardItem, QUrl, QRegExpValidator, QRegExp, QLineEdit, \
|
||||||
QToolButton, QMenu, QInputDialog, QAction, QKeySequence
|
QToolButton, QMenu, QInputDialog, QAction, QKeySequence
|
||||||
@ -17,7 +17,7 @@ from calibre.gui2.viewer.bookmarkmanager import BookmarkManager
|
|||||||
from calibre.gui2.widgets import ProgressIndicator
|
from calibre.gui2.widgets import ProgressIndicator
|
||||||
from calibre.gui2.main_window import MainWindow
|
from calibre.gui2.main_window import MainWindow
|
||||||
from calibre.gui2 import Application, ORG_NAME, APP_UID, choose_files, \
|
from calibre.gui2 import Application, ORG_NAME, APP_UID, choose_files, \
|
||||||
info_dialog, error_dialog
|
info_dialog, error_dialog, open_url
|
||||||
from calibre.ebooks.oeb.iterator import EbookIterator
|
from calibre.ebooks.oeb.iterator import EbookIterator
|
||||||
from calibre.ebooks import DRMError
|
from calibre.ebooks import DRMError
|
||||||
from calibre.constants import islinux, isfreebsd
|
from calibre.constants import islinux, isfreebsd
|
||||||
@ -472,7 +472,7 @@ class EbookViewer(MainWindow, Ui_EbookViewer):
|
|||||||
elif frag:
|
elif frag:
|
||||||
self.view.scroll_to(frag)
|
self.view.scroll_to(frag)
|
||||||
else:
|
else:
|
||||||
QDesktopServices.openUrl(url)
|
open_url(url)
|
||||||
|
|
||||||
def load_started(self):
|
def load_started(self):
|
||||||
self.open_progress_indicator(_('Loading flow...'))
|
self.open_progress_indicator(_('Loading flow...'))
|
||||||
|
@ -243,7 +243,7 @@
|
|||||||
<action name="action_copy">
|
<action name="action_copy">
|
||||||
<property name="icon">
|
<property name="icon">
|
||||||
<iconset resource="../../../../resources/images.qrc">
|
<iconset resource="../../../../resources/images.qrc">
|
||||||
<normaloff>:/images/convert.svg</normaloff>:/images/convert.svg</iconset>
|
<normaloff>:/images/edit_copy.svg</normaloff>:/images/edit_copy.svg</iconset>
|
||||||
</property>
|
</property>
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Copy to clipboard</string>
|
<string>Copy to clipboard</string>
|
||||||
|
@ -38,12 +38,16 @@ class ProgressIndicator(QWidget):
|
|||||||
self.status.setWordWrap(True)
|
self.status.setWordWrap(True)
|
||||||
self.status.setAlignment(Qt.AlignHCenter|Qt.AlignTop)
|
self.status.setAlignment(Qt.AlignHCenter|Qt.AlignTop)
|
||||||
self.setVisible(False)
|
self.setVisible(False)
|
||||||
|
self.pos = None
|
||||||
|
|
||||||
def start(self, msg=''):
|
def start(self, msg=''):
|
||||||
view = self.parent()
|
view = self.parent()
|
||||||
pwidth, pheight = view.size().width(), view.size().height()
|
pwidth, pheight = view.size().width(), view.size().height()
|
||||||
self.resize(pwidth, min(pheight, 250))
|
self.resize(pwidth, min(pheight, 250))
|
||||||
self.move(0, (pheight-self.size().height())/2.)
|
if self.pos is None:
|
||||||
|
self.move(0, (pheight-self.size().height())/2.)
|
||||||
|
else:
|
||||||
|
self.move(self.pos[0], self.pos[1])
|
||||||
self.pi.resize(self.pi.sizeHint())
|
self.pi.resize(self.pi.sizeHint())
|
||||||
self.pi.move(int((self.size().width()-self.pi.size().width())/2.), 0)
|
self.pi.move(int((self.size().width()-self.pi.size().width())/2.), 0)
|
||||||
self.status.resize(self.size().width(), self.size().height()-self.pi.size().height()-10)
|
self.status.resize(self.size().width(), self.size().height()-self.pi.size().height()-10)
|
||||||
@ -263,10 +267,10 @@ class LocationModel(QAbstractListModel):
|
|||||||
QVariant(QIcon(I('reader.svg'))),
|
QVariant(QIcon(I('reader.svg'))),
|
||||||
QVariant(QIcon(I('sd.svg'))),
|
QVariant(QIcon(I('sd.svg'))),
|
||||||
QVariant(QIcon(I('sd.svg')))]
|
QVariant(QIcon(I('sd.svg')))]
|
||||||
self.text = [_('Library\n%d\nbooks'),
|
self.text = [_('Library\n%d books'),
|
||||||
_('Reader\n%s\navailable'),
|
_('Reader\n%s'),
|
||||||
_('Card A\n%s\navailable'),
|
_('Card A\n%s'),
|
||||||
_('Card B\n%s\navailable')]
|
_('Card B\n%s')]
|
||||||
self.free = [-1, -1, -1]
|
self.free = [-1, -1, -1]
|
||||||
self.count = 0
|
self.count = 0
|
||||||
self.highlight_row = 0
|
self.highlight_row = 0
|
||||||
@ -294,6 +298,14 @@ class LocationModel(QAbstractListModel):
|
|||||||
row = 3
|
row = 3
|
||||||
return row
|
return row
|
||||||
|
|
||||||
|
def get_tooltip(self, row, drow):
|
||||||
|
ans = self.tooltips[row]
|
||||||
|
if row > 0:
|
||||||
|
fs = self.free[drow-1]
|
||||||
|
if fs > -1:
|
||||||
|
ans += '\n\n%s '%(human_readable(fs)) + _('free')
|
||||||
|
return ans
|
||||||
|
|
||||||
def data(self, index, role):
|
def data(self, index, role):
|
||||||
row = index.row()
|
row = index.row()
|
||||||
drow = self.get_device_row(row)
|
drow = self.get_device_row(row)
|
||||||
@ -304,8 +316,9 @@ class LocationModel(QAbstractListModel):
|
|||||||
data = QVariant(text)
|
data = QVariant(text)
|
||||||
elif role == Qt.DecorationRole:
|
elif role == Qt.DecorationRole:
|
||||||
data = self.icons[drow]
|
data = self.icons[drow]
|
||||||
elif role == Qt.ToolTipRole:
|
elif role in (Qt.ToolTipRole, Qt.StatusTipRole):
|
||||||
data = QVariant(self.tooltips[drow])
|
ans = self.get_tooltip(row, drow)
|
||||||
|
data = QVariant(ans)
|
||||||
elif role == Qt.SizeHintRole:
|
elif role == Qt.SizeHintRole:
|
||||||
data = QVariant(QSize(155, 90))
|
data = QVariant(QSize(155, 90))
|
||||||
elif role == Qt.FontRole:
|
elif role == Qt.FontRole:
|
||||||
@ -1002,12 +1015,14 @@ class LayoutButton(QToolButton):
|
|||||||
label =_('Show')
|
label =_('Show')
|
||||||
self.setText(label + ' ' + self.label)
|
self.setText(label + ' ' + self.label)
|
||||||
self.setToolTip(self.text())
|
self.setToolTip(self.text())
|
||||||
|
self.setStatusTip(self.text())
|
||||||
|
|
||||||
def set_state_to_hide(self, *args):
|
def set_state_to_hide(self, *args):
|
||||||
self.setChecked(True)
|
self.setChecked(True)
|
||||||
label = _('Hide')
|
label = _('Hide')
|
||||||
self.setText(label + ' ' + self.label)
|
self.setText(label + ' ' + self.label)
|
||||||
self.setToolTip(self.text())
|
self.setToolTip(self.text())
|
||||||
|
self.setStatusTip(self.text())
|
||||||
|
|
||||||
def update_state(self, *args):
|
def update_state(self, *args):
|
||||||
if self.splitter.is_side_index_hidden:
|
if self.splitter.is_side_index_hidden:
|
||||||
|
@ -6,11 +6,13 @@ __license__ = 'GPL v3'
|
|||||||
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||||
__docformat__ = 'restructuredtext en'
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
import collections, glob, os, re, itertools, functools
|
import re, itertools, functools
|
||||||
from itertools import repeat
|
from itertools import repeat
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
from threading import Thread, RLock
|
||||||
|
from Queue import Queue, Empty
|
||||||
|
|
||||||
from PyQt4.Qt import QThread, QReadWriteLock, QImage, Qt
|
from PyQt4.Qt import QImage, Qt
|
||||||
|
|
||||||
from calibre.utils.config import tweaks
|
from calibre.utils.config import tweaks
|
||||||
from calibre.utils.date import parse_date, now, UNDEFINED_DATE
|
from calibre.utils.date import parse_date, now, UNDEFINED_DATE
|
||||||
@ -19,120 +21,73 @@ from calibre.utils.pyparsing import ParseException
|
|||||||
from calibre.ebooks.metadata import title_sort
|
from calibre.ebooks.metadata import title_sort
|
||||||
from calibre import fit_image
|
from calibre import fit_image
|
||||||
|
|
||||||
class CoverCache(QThread):
|
class CoverCache(Thread):
|
||||||
|
|
||||||
def __init__(self, library_path, parent=None):
|
def __init__(self, db):
|
||||||
QThread.__init__(self, parent)
|
Thread.__init__(self)
|
||||||
self.library_path = library_path
|
self.daemon = True
|
||||||
self.id_map = None
|
self.db = db
|
||||||
self.id_map_lock = QReadWriteLock()
|
self.load_queue = Queue()
|
||||||
self.load_queue = collections.deque()
|
|
||||||
self.load_queue_lock = QReadWriteLock(QReadWriteLock.Recursive)
|
|
||||||
self.cache = {}
|
|
||||||
self.cache_lock = QReadWriteLock()
|
|
||||||
self.id_map_stale = True
|
|
||||||
self.keep_running = True
|
self.keep_running = True
|
||||||
|
self.cache = {}
|
||||||
def build_id_map(self):
|
self.lock = RLock()
|
||||||
self.id_map_lock.lockForWrite()
|
self.null_image = QImage()
|
||||||
self.id_map = {}
|
|
||||||
for f in glob.glob(os.path.join(self.library_path, '*', '* (*)', 'cover.jpg')):
|
|
||||||
c = os.path.basename(os.path.dirname(f))
|
|
||||||
try:
|
|
||||||
id = int(re.search(r'\((\d+)\)', c[c.rindex('('):]).group(1))
|
|
||||||
self.id_map[id] = f
|
|
||||||
except:
|
|
||||||
continue
|
|
||||||
self.id_map_lock.unlock()
|
|
||||||
self.id_map_stale = False
|
|
||||||
|
|
||||||
|
|
||||||
def set_cache(self, ids):
|
|
||||||
self.cache_lock.lockForWrite()
|
|
||||||
already_loaded = set([])
|
|
||||||
for id in self.cache.keys():
|
|
||||||
if id in ids:
|
|
||||||
already_loaded.add(id)
|
|
||||||
else:
|
|
||||||
self.cache.pop(id)
|
|
||||||
self.cache_lock.unlock()
|
|
||||||
ids = [i for i in ids if i not in already_loaded]
|
|
||||||
self.load_queue_lock.lockForWrite()
|
|
||||||
self.load_queue = collections.deque(ids)
|
|
||||||
self.load_queue_lock.unlock()
|
|
||||||
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
while self.keep_running:
|
|
||||||
if self.id_map is None or self.id_map_stale:
|
|
||||||
self.build_id_map()
|
|
||||||
while True: # Load images from the load queue
|
|
||||||
self.load_queue_lock.lockForWrite()
|
|
||||||
try:
|
|
||||||
id = self.load_queue.popleft()
|
|
||||||
except IndexError:
|
|
||||||
break
|
|
||||||
finally:
|
|
||||||
self.load_queue_lock.unlock()
|
|
||||||
|
|
||||||
self.cache_lock.lockForRead()
|
|
||||||
need = True
|
|
||||||
if id in self.cache.keys():
|
|
||||||
need = False
|
|
||||||
self.cache_lock.unlock()
|
|
||||||
if not need:
|
|
||||||
continue
|
|
||||||
path = None
|
|
||||||
self.id_map_lock.lockForRead()
|
|
||||||
if id in self.id_map.keys():
|
|
||||||
path = self.id_map[id]
|
|
||||||
else:
|
|
||||||
self.id_map_stale = True
|
|
||||||
self.id_map_lock.unlock()
|
|
||||||
if path and os.access(path, os.R_OK):
|
|
||||||
try:
|
|
||||||
img = QImage()
|
|
||||||
data = open(path, 'rb').read()
|
|
||||||
img.loadFromData(data)
|
|
||||||
if img.isNull():
|
|
||||||
continue
|
|
||||||
scaled, nwidth, nheight = fit_image(img.width(),
|
|
||||||
img.height(), 600, 800)
|
|
||||||
if scaled:
|
|
||||||
img = img.scaled(nwidth, nheight, Qt.KeepAspectRatio,
|
|
||||||
Qt.SmoothTransformation)
|
|
||||||
except:
|
|
||||||
continue
|
|
||||||
self.cache_lock.lockForWrite()
|
|
||||||
self.cache[id] = img
|
|
||||||
self.cache_lock.unlock()
|
|
||||||
|
|
||||||
self.sleep(1)
|
|
||||||
|
|
||||||
def stop(self):
|
def stop(self):
|
||||||
self.keep_running = False
|
self.keep_running = False
|
||||||
|
|
||||||
def cover(self, id):
|
def _image_for_id(self, id_):
|
||||||
val = None
|
img = self.db.cover(id_, index_is_id=True, as_image=True)
|
||||||
if self.cache_lock.tryLockForRead(50):
|
if img is None:
|
||||||
val = self.cache.get(id, None)
|
img = QImage()
|
||||||
self.cache_lock.unlock()
|
if not img.isNull():
|
||||||
return val
|
scaled, nwidth, nheight = fit_image(img.width(),
|
||||||
|
img.height(), 600, 800)
|
||||||
|
if scaled:
|
||||||
|
img = img.scaled(nwidth, nheight, Qt.KeepAspectRatio,
|
||||||
|
Qt.SmoothTransformation)
|
||||||
|
|
||||||
|
return img
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
while self.keep_running:
|
||||||
|
try:
|
||||||
|
id_ = self.load_queue.get(True, 1)
|
||||||
|
except Empty:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
img = self._image_for_id(id_)
|
||||||
|
except:
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
continue
|
||||||
|
with self.lock:
|
||||||
|
self.cache[id_] = img
|
||||||
|
|
||||||
|
def set_cache(self, ids):
|
||||||
|
with self.lock:
|
||||||
|
already_loaded = set([])
|
||||||
|
for id in self.cache.keys():
|
||||||
|
if id in ids:
|
||||||
|
already_loaded.add(id)
|
||||||
|
else:
|
||||||
|
self.cache.pop(id)
|
||||||
|
for id_ in set(ids) - already_loaded:
|
||||||
|
self.load_queue.put(id_)
|
||||||
|
|
||||||
|
def cover(self, id_):
|
||||||
|
with self.lock:
|
||||||
|
return self.cache.get(id_, self.null_image)
|
||||||
|
|
||||||
def clear_cache(self):
|
def clear_cache(self):
|
||||||
self.cache_lock.lockForWrite()
|
with self.lock:
|
||||||
self.cache = {}
|
self.cache = {}
|
||||||
self.cache_lock.unlock()
|
|
||||||
|
|
||||||
def refresh(self, ids):
|
def refresh(self, ids):
|
||||||
self.cache_lock.lockForWrite()
|
with self.lock:
|
||||||
for id in ids:
|
for id_ in ids:
|
||||||
self.cache.pop(id, None)
|
self.cache.pop(id_, None)
|
||||||
self.cache_lock.unlock()
|
self.load_queue.put(id_)
|
||||||
self.load_queue_lock.lockForWrite()
|
|
||||||
for id in ids:
|
|
||||||
self.load_queue.appendleft(id)
|
|
||||||
self.load_queue_lock.unlock()
|
|
||||||
|
|
||||||
### Global utility function for get_match here and in gui2/library.py
|
### Global utility function for get_match here and in gui2/library.py
|
||||||
CONTAINS_MATCH = 0
|
CONTAINS_MATCH = 0
|
||||||
@ -341,8 +296,15 @@ class ResultCache(SearchQueryParser):
|
|||||||
cast = lambda x : float (x)
|
cast = lambda x : float (x)
|
||||||
adjust = lambda x: x
|
adjust = lambda x: x
|
||||||
|
|
||||||
|
if len(query) > 1:
|
||||||
|
mult = query[-1:].lower()
|
||||||
|
mult = {'k':1024.,'m': 1024.**2, 'g': 1024.**3}.get(mult, 1.0)
|
||||||
|
if mult != 1.0:
|
||||||
|
query = query[:-1]
|
||||||
|
else:
|
||||||
|
mult = 1.0
|
||||||
try:
|
try:
|
||||||
q = cast(query)
|
q = cast(query) * mult
|
||||||
except:
|
except:
|
||||||
return matches
|
return matches
|
||||||
|
|
||||||
|
@ -8,9 +8,14 @@ __docformat__ = 'restructuredtext en'
|
|||||||
import re
|
import re
|
||||||
|
|
||||||
from calibre.constants import preferred_encoding
|
from calibre.constants import preferred_encoding
|
||||||
from calibre.ebooks.BeautifulSoup import BeautifulSoup, Tag, NavigableString
|
from calibre.ebooks.BeautifulSoup import BeautifulSoup, Tag, NavigableString, \
|
||||||
|
CData, Comment, Declaration, ProcessingInstruction
|
||||||
from calibre import prepare_string_for_xml
|
from calibre import prepare_string_for_xml
|
||||||
|
|
||||||
|
# Hackish - ignoring sentences ending or beginning in numbers to avoid
|
||||||
|
# confusion with decimal points.
|
||||||
|
lost_cr_pat = re.compile('([a-z])([\.\?!])([A-Z])')
|
||||||
|
|
||||||
def comments_to_html(comments):
|
def comments_to_html(comments):
|
||||||
'''
|
'''
|
||||||
Convert random comment text to normalized, xml-legal block of <p>s
|
Convert random comment text to normalized, xml-legal block of <p>s
|
||||||
@ -41,36 +46,25 @@ def comments_to_html(comments):
|
|||||||
|
|
||||||
if '<' not in comments:
|
if '<' not in comments:
|
||||||
comments = prepare_string_for_xml(comments)
|
comments = prepare_string_for_xml(comments)
|
||||||
comments = comments.replace(u'\n', u'<br />')
|
parts = [u'<p class="description">%s</p>'%x.replace(u'\n', u'<br />')
|
||||||
return u'<p>%s</p>'%comments
|
for x in comments.split('\n\n')]
|
||||||
|
return '\n'.join(parts)
|
||||||
# Hackish - ignoring sentences ending or beginning in numbers to avoid
|
|
||||||
# confusion with decimal points.
|
|
||||||
|
|
||||||
# Explode lost CRs to \n\n
|
# Explode lost CRs to \n\n
|
||||||
for lost_cr in re.finditer('([a-z])([\.\?!])([A-Z])', comments):
|
for lost_cr in lost_cr_pat.finditer(comments):
|
||||||
comments = comments.replace(lost_cr.group(),
|
comments = comments.replace(lost_cr.group(),
|
||||||
'%s%s\n\n%s' % (lost_cr.group(1),
|
'%s%s\n\n%s' % (lost_cr.group(1),
|
||||||
lost_cr.group(2),
|
lost_cr.group(2),
|
||||||
lost_cr.group(3)))
|
lost_cr.group(3)))
|
||||||
|
|
||||||
|
comments = comments.replace(u'\r', u'')
|
||||||
# Convert \n\n to <p>s
|
# Convert \n\n to <p>s
|
||||||
if re.search('\n\n', comments):
|
comments = comments.replace(u'\n\n', u'<p>')
|
||||||
soup = BeautifulSoup()
|
|
||||||
split_ps = comments.split(u'\n\n')
|
|
||||||
tsc = 0
|
|
||||||
for p in split_ps:
|
|
||||||
pTag = Tag(soup,'p')
|
|
||||||
pTag.insert(0,p)
|
|
||||||
soup.insert(tsc,pTag)
|
|
||||||
tsc += 1
|
|
||||||
comments = soup.renderContents(None)
|
|
||||||
|
|
||||||
# Convert solo returns to <br />
|
# Convert solo returns to <br />
|
||||||
comments = re.sub('[\r\n]','<br />', comments)
|
comments = comments.replace(u'\n', '<br />')
|
||||||
|
|
||||||
# Convert two hyphens to emdash
|
# Convert two hyphens to emdash
|
||||||
comments = re.sub('--', '—', comments)
|
comments = comments.replace('--', '—')
|
||||||
|
|
||||||
soup = BeautifulSoup(comments)
|
soup = BeautifulSoup(comments)
|
||||||
result = BeautifulSoup()
|
result = BeautifulSoup()
|
||||||
rtc = 0
|
rtc = 0
|
||||||
@ -85,35 +79,52 @@ def comments_to_html(comments):
|
|||||||
ptc = 0
|
ptc = 0
|
||||||
pTag.insert(ptc,prepare_string_for_xml(token))
|
pTag.insert(ptc,prepare_string_for_xml(token))
|
||||||
ptc += 1
|
ptc += 1
|
||||||
|
elif type(token) in (CData, Comment, Declaration,
|
||||||
elif token.name in ['br','b','i','em']:
|
ProcessingInstruction):
|
||||||
|
continue
|
||||||
|
elif token.name in ['br', 'b', 'i', 'em', 'strong', 'span', 'font', 'a',
|
||||||
|
'hr']:
|
||||||
if not open_pTag:
|
if not open_pTag:
|
||||||
pTag = Tag(result,'p')
|
pTag = Tag(result,'p')
|
||||||
open_pTag = True
|
open_pTag = True
|
||||||
ptc = 0
|
ptc = 0
|
||||||
pTag.insert(ptc, token)
|
pTag.insert(ptc, token)
|
||||||
ptc += 1
|
ptc += 1
|
||||||
|
|
||||||
else:
|
else:
|
||||||
if open_pTag:
|
if open_pTag:
|
||||||
result.insert(rtc, pTag)
|
result.insert(rtc, pTag)
|
||||||
rtc += 1
|
rtc += 1
|
||||||
open_pTag = False
|
open_pTag = False
|
||||||
ptc = 0
|
ptc = 0
|
||||||
# Clean up NavigableStrings for xml
|
|
||||||
sub_tokens = list(token.contents)
|
|
||||||
for sub_token in sub_tokens:
|
|
||||||
if type(sub_token) is NavigableString:
|
|
||||||
sub_token.replaceWith(prepare_string_for_xml(sub_token))
|
|
||||||
result.insert(rtc, token)
|
result.insert(rtc, token)
|
||||||
rtc += 1
|
rtc += 1
|
||||||
|
|
||||||
if open_pTag:
|
if open_pTag:
|
||||||
result.insert(rtc, pTag)
|
result.insert(rtc, pTag)
|
||||||
|
|
||||||
paras = result.findAll('p')
|
for p in result.findAll('p'):
|
||||||
for p in paras:
|
|
||||||
p['class'] = 'description'
|
p['class'] = 'description'
|
||||||
|
|
||||||
|
for t in result.findAll(text=True):
|
||||||
|
t.replaceWith(prepare_string_for_xml(unicode(t)))
|
||||||
|
|
||||||
return result.renderContents(encoding=None)
|
return result.renderContents(encoding=None)
|
||||||
|
|
||||||
|
def test():
|
||||||
|
for pat, val in [
|
||||||
|
('lineone\n\nlinetwo',
|
||||||
|
'<p class="description">lineone</p>\n<p class="description">linetwo</p>'),
|
||||||
|
('a <b>b&c</b>\nf', '<p class="description">a <b>b&c;</b><br />f</p>'),
|
||||||
|
('a <?xml asd> b\n\ncd', '<p class="description">a b</p><p class="description">cd</p>'),
|
||||||
|
]:
|
||||||
|
print
|
||||||
|
print 'Testing: %r'%pat
|
||||||
|
cval = comments_to_html(pat)
|
||||||
|
print 'Value: %r'%cval
|
||||||
|
if comments_to_html(pat) != val:
|
||||||
|
print 'FAILED'
|
||||||
|
break
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
test()
|
||||||
|
|
||||||
|
@ -6,7 +6,7 @@ __docformat__ = 'restructuredtext en'
|
|||||||
'''
|
'''
|
||||||
The database used to store ebook metadata
|
The database used to store ebook metadata
|
||||||
'''
|
'''
|
||||||
import os, sys, shutil, cStringIO, glob,functools, traceback
|
import os, sys, shutil, cStringIO, glob, time, functools, traceback
|
||||||
from itertools import repeat
|
from itertools import repeat
|
||||||
from math import floor
|
from math import floor
|
||||||
|
|
||||||
@ -440,12 +440,20 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
|||||||
if os.access(path, os.R_OK):
|
if os.access(path, os.R_OK):
|
||||||
if as_path:
|
if as_path:
|
||||||
return path
|
return path
|
||||||
f = open(path, 'rb')
|
try:
|
||||||
|
f = open(path, 'rb')
|
||||||
|
except (IOError, OSError):
|
||||||
|
time.sleep(0.2)
|
||||||
|
f = open(path, 'rb')
|
||||||
if as_image:
|
if as_image:
|
||||||
img = QImage()
|
img = QImage()
|
||||||
img.loadFromData(f.read())
|
img.loadFromData(f.read())
|
||||||
|
f.close()
|
||||||
return img
|
return img
|
||||||
return f if as_file else f.read()
|
ans = f if as_file else f.read()
|
||||||
|
if ans is not f:
|
||||||
|
f.close()
|
||||||
|
return ans
|
||||||
|
|
||||||
def get_metadata(self, idx, index_is_id=False, get_cover=False):
|
def get_metadata(self, idx, index_is_id=False, get_cover=False):
|
||||||
'''
|
'''
|
||||||
@ -492,12 +500,18 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
|||||||
path = os.path.join(self.library_path, self.path(id, index_is_id=True), 'cover.jpg')
|
path = os.path.join(self.library_path, self.path(id, index_is_id=True), 'cover.jpg')
|
||||||
return os.access(path, os.R_OK)
|
return os.access(path, os.R_OK)
|
||||||
|
|
||||||
def remove_cover(self, id):
|
def remove_cover(self, id, notify=True):
|
||||||
path = os.path.join(self.library_path, self.path(id, index_is_id=True), 'cover.jpg')
|
path = os.path.join(self.library_path, self.path(id, index_is_id=True), 'cover.jpg')
|
||||||
if os.path.exists(path):
|
if os.path.exists(path):
|
||||||
os.remove(path)
|
try:
|
||||||
|
os.remove(path)
|
||||||
|
except (IOError, OSError):
|
||||||
|
time.sleep(0.2)
|
||||||
|
os.remove(path)
|
||||||
|
if notify:
|
||||||
|
self.notify('cover', [id])
|
||||||
|
|
||||||
def set_cover(self, id, data):
|
def set_cover(self, id, data, notify=True):
|
||||||
'''
|
'''
|
||||||
Set the cover for this book.
|
Set the cover for this book.
|
||||||
|
|
||||||
@ -509,7 +523,13 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
|||||||
else:
|
else:
|
||||||
if callable(getattr(data, 'read', None)):
|
if callable(getattr(data, 'read', None)):
|
||||||
data = data.read()
|
data = data.read()
|
||||||
save_cover_data_to(data, path)
|
try:
|
||||||
|
save_cover_data_to(data, path)
|
||||||
|
except (IOError, OSError):
|
||||||
|
time.sleep(0.2)
|
||||||
|
save_cover_data_to(data, path)
|
||||||
|
if notify:
|
||||||
|
self.notify('cover', [id])
|
||||||
|
|
||||||
def book_on_device(self, id):
|
def book_on_device(self, id):
|
||||||
if callable(self.book_on_device_func):
|
if callable(self.book_on_device_func):
|
||||||
|
@ -253,7 +253,7 @@ class FieldMetadata(dict):
|
|||||||
'is_multiple':None,
|
'is_multiple':None,
|
||||||
'kind':'field',
|
'kind':'field',
|
||||||
'name':None,
|
'name':None,
|
||||||
'search_terms':[],
|
'search_terms':['size'],
|
||||||
'is_custom':False,
|
'is_custom':False,
|
||||||
'is_category':False}),
|
'is_category':False}),
|
||||||
('timestamp', {'table':None,
|
('timestamp', {'table':None,
|
||||||
|
@ -162,6 +162,9 @@ turned into a collection on the reader. Note that the PRS-500 does not support c
|
|||||||
How do I use |app| with my iPad/iPhone/iTouch?
|
How do I use |app| with my iPad/iPhone/iTouch?
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
Over the air
|
||||||
|
^^^^^^^^^^^^^^
|
||||||
|
|
||||||
The easiest way to browse your |app| collection on your Apple device (iPad/iPhone/iPod) is by using the *free* Stanza app, available from the Apple app store. You need at least Stanza version 3.0. Stanza allows you to access your |app| collection wirelessly, over the air.
|
The easiest way to browse your |app| collection on your Apple device (iPad/iPhone/iPod) is by using the *free* Stanza app, available from the Apple app store. You need at least Stanza version 3.0. Stanza allows you to access your |app| collection wirelessly, over the air.
|
||||||
|
|
||||||
First perform the following steps in |app|
|
First perform the following steps in |app|
|
||||||
@ -181,13 +184,13 @@ Replace ``192.168.1.2`` with the local IP address of the computer running |app|.
|
|||||||
|
|
||||||
If you get timeout errors while browsing the calibre catalog in Stanza, try increasing the connection timeout value in the stanza settings. Go to Info->Settings and increase the value of Download Timeout.
|
If you get timeout errors while browsing the calibre catalog in Stanza, try increasing the connection timeout value in the stanza settings. Go to Info->Settings and increase the value of Download Timeout.
|
||||||
|
|
||||||
Alternative for the iPad
|
With the USB cable
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
As of |app| version 0.7.0, you can plugin your iPad into the computer using its charging cable, and |app| will detect it and show you a list of books on the iPad. You can then use the Send to device button to send books directly to iBooks on the iPad.
|
As of |app| version 0.7.0, you can plug your iDevice into the computer using its charging cable, and |app| will detect it and show you a list of books on the device. You can then use the *Send to device button* to send books directly to iBooks on the device. Note that you must have at least iOS 4 installed on your iPhone/iTouch for this to work.
|
||||||
|
|
||||||
This method only works on Windows XP and higher and OS X 10.5 and higher. Linux is not supported (iTunes is not available in linux) and OS X 10.4 is not supported. For more details, see
|
This method only works on Windows XP and higher and OS X 10.5 and higher. Linux is not supported (iTunes is not available in linux) and OS X 10.4 is not supported.
|
||||||
`this forum post <http://www.mobileread.com/forums/showpost.php?p=944079&postcount=1>`_.
|
For more details on how this works, see `this forum post <http://www.mobileread.com/forums/showpost.php?p=944079&postcount=1>`_.
|
||||||
|
|
||||||
How do I use |app| with my Android phone?
|
How do I use |app| with my Android phone?
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
@ -199,31 +199,59 @@ Searches are by default 'contains'. An item matches if the search string appears
|
|||||||
Two other kinds of searches are available: equality search and search using regular expressions.
|
Two other kinds of searches are available: equality search and search using regular expressions.
|
||||||
|
|
||||||
Equality searches are indicated by prefixing the search string with an equals sign (=). For example, the query
|
Equality searches are indicated by prefixing the search string with an equals sign (=). For example, the query
|
||||||
``tag:"=science"`` will match "science", but not "science fiction". Regular expression searches are
|
``tag:"=science"`` will match "science", but not "science fiction" or "hard science". Regular expression searches are
|
||||||
indicated by prefixing the search string with a tilde (~). Any python-compatible regular expression can
|
indicated by prefixing the search string with a tilde (~). Any python-compatible regular expression can
|
||||||
be used. Regular expression searches are contains searches unless the expression contains anchors.
|
be used. Regular expression searches are contains searches unless the expression contains anchors.
|
||||||
Should you need to search for a string with a leading equals or tilde, prefix the string with a backslash.
|
Should you need to search for a string with a leading equals or tilde, prefix the string with a backslash.
|
||||||
|
|
||||||
|
Enclose search strings with quotes (") if the string contains parenthesis or spaces. For example, to search
|
||||||
|
for the tag ``Science Fiction``, you would need to search for ``tag:"=science fiction"``. If you search for
|
||||||
|
``tag:=science fiction``, you will find all books with the tag 'science' and containing the word 'fiction' in any
|
||||||
|
metadata.
|
||||||
|
|
||||||
You can build advanced search queries easily using the :guilabel:`Advanced Search Dialog`, accessed by
|
You can build advanced search queries easily using the :guilabel:`Advanced Search Dialog`, accessed by
|
||||||
clicking the button |sbi|.
|
clicking the button |sbi|.
|
||||||
|
|
||||||
Available fields for searching are: ``tag, title, author, publisher, series, rating cover, comments, format,
|
Available fields for searching are: ``tag, title, author, publisher, series, rating, cover, comments, format,
|
||||||
isbn, date, pubdate, search``.
|
isbn, date, pubdate, search, size`` and custom columns. If a device is plugged in, the ``ondevice`` field
|
||||||
|
becomes available. To find the search name for a custom column, hover your mouse over the column header.
|
||||||
|
|
||||||
The syntax for searching for dates and publication dates is::
|
The syntax for searching for dates is::
|
||||||
|
|
||||||
pubdate:>2000-1 Will find all books published after Jan, 2000
|
pubdate:>2000-1 Will find all books published after Jan, 2000
|
||||||
date:<=2000-1-3 Will find all books added to calibre beforre 3 Jan, 2000
|
date:<=2000-1-3 Will find all books added to calibre before 3 Jan, 2000
|
||||||
pubdate:=2009 Will find all books published in 2009
|
pubdate:=2009 Will find all books published in 2009
|
||||||
|
|
||||||
|
If the date is ambiguous, the current locale is used for date comparison. For example, in an mm/dd/yyyy
|
||||||
|
locale, 2/1/2009 is interpreted as 1 Feb 2009. In a dd/mm/yyyy locale, it is interpreted as 2 Jan 2009.
|
||||||
|
|
||||||
|
Some special date strings are available. The string ``today`` translates to today's date, whatever it is. The
|
||||||
|
strings `yesterday`` and ``thismonth`` also work. In addition, the string ``daysago`` can be used to compare
|
||||||
|
to a date some number of days ago, for example: date:>10daysago, date:<=45daysago.
|
||||||
|
|
||||||
|
You can search for books that have a format of a certain size like this::
|
||||||
|
|
||||||
|
size:>1.1M Will find books with a format larger than 1.1MB
|
||||||
|
size:<=1K Will find books with a format smaller than 1KB
|
||||||
|
|
||||||
|
Dates and numeric fields support the operators ``=`` (equals), ``>`` (greater than), ``>=`` (greater than or
|
||||||
|
equal to), ``<`` (less than), ``<=`` (less than or equal to), and ``!=`` (not equal to). Rating fields are
|
||||||
|
considered to be numeric. For example, the search ``rating:>=3`` will find all books rated 3 or higher.
|
||||||
|
|
||||||
The special field ``search`` is used for saved searches. So if you save a search with the name
|
The special field ``search`` is used for saved searches. So if you save a search with the name
|
||||||
"My spouse's books" you can enter ``search:"My spouses' books"`` in the search bar to reuse the saved
|
"My spouse's books" you can enter ``search:"My spouse's books"`` in the search bar to reuse the saved
|
||||||
search. More about saving searches, below.
|
search. More about saving searches, below.
|
||||||
|
|
||||||
You can search for the absence or presnce of a filed using the specia "true" and "false" values. For example::
|
You can search for the absence or presence of a field using the special "true" and "false" values. For example::
|
||||||
|
|
||||||
cover:false Will give you all books without a cover
|
cover:false will give you all books without a cover
|
||||||
series:true Will give you all books that belong to a series
|
series:true will give you all books that belong to a series
|
||||||
|
comments:false will give you all books with an empty comment
|
||||||
|
|
||||||
|
Yes/no custom columns are searchable. Searching for ``false``, ``empty``, or ``blank`` will find all books
|
||||||
|
with undefined values in the column. Searching for ``true`` will find all books that do not have undefined
|
||||||
|
values in the column. Searching for ``yes`` or ``checked`` will find all books with ``Yes`` in the column.
|
||||||
|
Searching for ``no`` or ``unchecked`` will find all books with ``No`` in the column.
|
||||||
|
|
||||||
.. |sbi| image:: images/search_button.png
|
.. |sbi| image:: images/search_button.png
|
||||||
:align: middle
|
:align: middle
|
||||||
|