Merge from trunk

This commit is contained in:
Charles Haley 2010-06-19 17:08:31 +01:00
commit 1f19aba68a
92 changed files with 46103 additions and 32248 deletions

View File

@ -4,6 +4,117 @@
# for important features/bug fixes.
# Also, each release can have new and improved recipes.
- version: 0.7.3
date: 2010-06-18
new features:
- title: "The Tag Browser now display an average rating for each item"
type: major
description: >
"
The icons of each individual item in the Tag Browser are now partially colored to indicate the average rating of
all books belonging to that category. For example, the icon next to each author is partially colored based on the
averagerating of all books by that author in your calibre library. You can also hover your mouse over the item to
see the average rating in a tooltip. Can be turned off via Preferences->Interface
"
- title: "Editable author sort for each author"
type: major
description: >
"calibre has always allowed you to specify the author sort for each bookin your collection. Now you
can also specify the way the name of each individual author should be sorted. This is used to display the list
of authors in the Tag Browser and OPDS feeds in the Content Server"
- title: "When downloading metadata, also get series information from librarything.com"
type: major
tickets: [5148]
- title: "Redesign of the Book Details pane"
type: major
description: >
"The Book details pane now display covers with animation. Also instead of showing the full path to the book, you now have
clickable links to open the containing folder or individual formats. The path information is still accessible via a tooltip"
- title: "New User Interface layouts"
type: major
description: >
"calibre now has two user interface layouts selectable from Preferences->Interface. The 'wide' layout has the book details pane on the side
and the 'narrow' layout has it on the bottom. The default layout is now wide."
- title: "You can now add books directly from the device to the calibre library by right clicking on the books in the device views"
- title: "iPad driver: Create category from series preferentially, also handle series sorting"
- title: "SONY driver: Add an option to use author_sort instead of author when sending to device"
- title: "Hitting Enter in the search box now causes the search to be re-run"
tickets: [5856]
- title: "Boox driver: Make destination directory for books customizable"
- title: "Add plugin to download metadata from douban.com. Disabled by default."
- title: "OS X/linux driver for PocketBook 301"
- title: "Support for the Samsung Galaxy and Sigmatek EBK52"
- title: "On startup do not focus the search bar. Instead you can acces the search bar easily by pressing the / key or the standard search keyboard shortcut for your operating system"
bug fixes:
- title: "iPad driver: Various bug fixes"
- title: "Kobo Output profile: Adjust the screen dimensions when converting comics"
- title: "Fix using Preferences when a device is connected causes items in device menu to be disabled"
- title: "CHM Input: Skip files whoose names are too long for windows"
- title: "Brighten up calibre icon on dark backgrounds"
- title: "Ignore 'Unknown' in title/autors when downloading metadata"
tickets: [5633]
- title: "Fix regression that broke various entries in the menus - Preferences, Open containing folder and Edit metadata individually"
- title: "EPUB metadata: Handle comma separated entries in <dc:subject> tags correctly"
tickets: [5855]
- title: "MOBI Output: Fix underlines not being rendered"
tickets: [5830]
- title: "EPUB Output: Remove workaround for old versions of Adobe Digital Editions' faulty rendering of links in html. calibre no longer forces links to be blue and underlined"
- title: "Fix a bug that could cause the show pane buttons to not show hidden panes"
- title: "Fix Tag Editor does not reflect recently changed data in Tag Catagory Text Box"
tickets: [5809]
- title: "Content server: Fix sorting of books by authors instead of author_sort in the main and mobile views"
- title: "Cover cache: Resize covers larger than 600x800 in the cover cache to reduce memory consumption in the GUI"
- title: "EPUB Output: Default cover is generated is now generated as a JPEG instead of PNG32, reducing size by an order of magnitude."
tickets: [5810]
- title: "Cover Browser: Scale text size with height of cover browser. Only show a reflection of half the cover. Also restore rendering quality after regression in 0.7.1"
tickets: [5808]
- title: "Book list: Do not let the default layout have any column wider than 350 pixels"
new recipes:
- title: Akter
author: Darko Miletic
- title: Thai Rath and The Nation (Thailand)
author: Anat Ruangrassamee
improved recipes:
- Wall Street Journal
- New York Times
- Slashdot
- Publico
- Danas
- version: 0.7.2
date: 2010-06-11

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 158 KiB

After

Width:  |  Height:  |  Size: 160 KiB

View File

@ -72,9 +72,4 @@ gui_pubdate_display_format = 'MMM yyyy'
# without changing anything is sufficient to change the sort.
title_series_sorting = 'library_order'
# How to render average rating in the tag browser.
# There are two rendering methods available. The first is to show a partial
# star, and the second is to show a partially filled rectangle. The first is
# better looking, but uses more screen space than the second.
# Values are 'star' or 'rectangle'
render_avg_rating_using='star'

View File

@ -1,7 +1,6 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:i="http://ns.adobe.com/AdobeIllustrator/10.0/"
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#"
@ -10,446 +9,238 @@
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="128"
height="128"
width="48"
height="48"
id="svg2"
sodipodi:version="0.32"
inkscape:version="0.44.1"
inkscape:version="0.45"
version="1.0"
sodipodi:docbase="/Users/david/Progetti/oxygen-svn/theme/svg/actions"
sodipodi:docname="bookmark.svg">
sodipodi:docbase="/home/dobey/Projects/gnome-icon-theme/scalable/apps"
sodipodi:docname="accessories-dictionary.svg"
inkscape:export-filename="/home/ulisse/Desktop/accessories-dictionary.png"
inkscape:export-xdpi="90"
inkscape:export-ydpi="90"
inkscape:output_extension="org.inkscape.output.svg.inkscape">
<defs
id="defs4">
<linearGradient
id="linearGradient26907"
gradientUnits="userSpaceOnUse"
x1="-84.002403"
y1="-383.9971"
x2="-12.0029"
y2="-383.9971"
gradientTransform="matrix(0,1,-1,0,-39.9985,140.0029)">
<stop
offset="0"
style="stop-color:#888a85;stop-opacity:1;"
id="stop26909" />
<stop
offset="1"
style="stop-color:#2e3436;stop-opacity:1;"
id="stop26911" />
</linearGradient>
<linearGradient
gradientTransform="matrix(0,1,-1,0,-39.9985,140.0029)"
y2="-383.9975"
x2="-23.516129"
y1="-383.9971"
x1="-84.002403"
gradientUnits="userSpaceOnUse"
id="linearGradient3711">
<stop
id="stop3713"
style="stop-color:white;stop-opacity:1;"
offset="0" />
<stop
id="stop3715"
style="stop-color:white;stop-opacity:0;"
offset="1" />
</linearGradient>
<linearGradient
id="linearGradient3081">
<stop
id="stop3083"
offset="0"
style="stop-color:#28691f;stop-opacity:1;" />
<stop
id="stop3085"
offset="1"
style="stop-color:#00bf00;stop-opacity:1;" />
</linearGradient>
<linearGradient
id="linearGradient3290">
<stop
style="stop-color:yellow;stop-opacity:1;"
offset="0"
id="stop3292" />
<stop
style="stop-color:#ffb66d;stop-opacity:1;"
offset="1"
id="stop3294" />
</linearGradient>
<linearGradient
inkscape:collect="always"
id="linearGradient3765">
id="linearGradient2309">
<stop
style="stop-color:#ffffff;stop-opacity:1;"
offset="0"
id="stop3767" />
id="stop2311" />
<stop
style="stop-color:#ffffff;stop-opacity:0;"
offset="1"
id="stop3769" />
id="stop2313" />
</linearGradient>
<linearGradient
inkscape:collect="always"
id="linearGradient3747">
id="linearGradient2301">
<stop
style="stop-color:#ffffff;stop-opacity:1;"
style="stop-color:#790000;stop-opacity:1"
offset="0"
id="stop3749" />
id="stop2303" />
<stop
style="stop-color:#ffffff;stop-opacity:0;"
style="stop-color:#b03636;stop-opacity:1"
offset="1"
id="stop3751" />
id="stop2305" />
</linearGradient>
<linearGradient
id="linearGradient3638">
inkscape:collect="always"
id="linearGradient2286">
<stop
style="stop-color:#ffffff;stop-opacity:0;"
style="stop-color:#555753"
offset="0"
id="stop3640" />
id="stop2288" />
<stop
id="stop3661"
offset="0.06868132"
style="stop-color:#ffffff;stop-opacity:1;" />
<stop
id="stop3659"
offset="0.5"
style="stop-color:#ffffff;stop-opacity:1;" />
<stop
style="stop-color:#ffffff;stop-opacity:0;"
style="stop-color:#555753;stop-opacity:0"
offset="1"
id="stop3642" />
id="stop2290" />
</linearGradient>
<linearGradient
id="linearGradient1563">
inkscape:collect="always"
id="linearGradient2276">
<stop
id="stop1565"
style="stop-color:#babdb6;stop-opacity:1;"
offset="0"
style="stop-color:#ffffff;stop-opacity:1;" />
id="stop2278" />
<stop
id="stop1567"
style="stop-color:#8f9488;stop-opacity:1"
offset="1"
style="stop-color:white;stop-opacity:0;" />
id="stop2280" />
</linearGradient>
<linearGradient
id="linearGradient3273">
inkscape:collect="always"
id="linearGradient2258">
<stop
id="stop3275"
style="stop-color:#ffa4a4;stop-opacity:1"
offset="0"
style="stop-color:#ffffff;stop-opacity:0.55035973;" />
id="stop2260" />
<stop
id="stop3277"
style="stop-color:#a40000"
offset="1"
style="stop-color:#ffffff;stop-opacity:0;" />
id="stop2262" />
</linearGradient>
<linearGradient
id="linearGradient3291"
inkscape:collect="always"
id="linearGradient2235">
<stop
style="stop-color:#cccccc;stop-opacity:1"
offset="0"
id="stop2237" />
<stop
style="stop-color:#9b9b9b;stop-opacity:1"
offset="1"
id="stop2239" />
</linearGradient>
<linearGradient
inkscape:collect="always"
id="linearGradient2229">
<stop
style="stop-color:#888a85"
offset="0"
id="stop2231" />
<stop
style="stop-color:#d3d7cf;stop-opacity:0;"
offset="1"
id="stop2233" />
</linearGradient>
<linearGradient
id="linearGradient2221"
inkscape:collect="always">
<stop
id="stop3293"
id="stop2223"
offset="0"
style="stop-color:#000000;stop-opacity:1;" />
style="stop-color:#babdb6" />
<stop
id="stop3295"
id="stop2225"
offset="1"
style="stop-color:#000000;stop-opacity:0;" />
style="stop-color:#d3d7cf;stop-opacity:0;" />
</linearGradient>
<linearGradient
id="linearGradient12948">
inkscape:collect="always"
id="linearGradient2184">
<stop
style="stop-color:#ffffff;stop-opacity:1;"
offset="0"
id="stop12950" />
id="stop2186" />
<stop
style="stop-color:#c0c0c0;stop-opacity:0;"
style="stop-color:#e3e3e3;stop-opacity:1"
offset="1"
id="stop12952" />
id="stop2188" />
</linearGradient>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3273"
id="linearGradient3605"
x1="80.100487"
y1="44.807674"
x2="77.714729"
y2="101.4734"
xlink:href="#linearGradient2229"
id="linearGradient2211"
x1="24"
y1="19.505583"
x2="19.982143"
y2="19.550226"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0.959962,0,0,0.959962,2.35549,3.275418)"
spreadMethod="reflect" />
gradientTransform="matrix(-1,0,0,1,48,0)" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3638"
id="linearGradient3644"
x1="57.287113"
y1="1.1597457"
x2="144.2531"
y2="16.876789"
xlink:href="#linearGradient2221"
id="linearGradient2219"
x1="24"
y1="19.996655"
x2="32"
y2="19.90625"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(-1,0,0,1,48,0)" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient2184"
id="linearGradient2245"
gradientUnits="userSpaceOnUse"
x1="15.714286"
y1="16.82852"
x2="36.482143"
y2="20.667807" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient2235"
id="linearGradient2247"
gradientUnits="userSpaceOnUse"
x1="19.940901"
y1="10.918805"
x2="24"
y2="22.750927" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient2258"
id="linearGradient2264"
x1="32.794643"
y1="21.696428"
x2="34.79464"
y2="32.321426"
gradientUnits="userSpaceOnUse" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3638"
id="linearGradient3646"
x1="57.287113"
y1="1.1597457"
x2="144.2531"
y2="16.876789"
gradientUnits="userSpaceOnUse" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3638"
id="linearGradient3648"
x1="57.287113"
y1="1.1597457"
x2="144.2531"
y2="16.876789"
xlink:href="#linearGradient2276"
id="linearGradient2282"
x1="37.535713"
y1="34.196426"
x2="9.9285688"
y2="20.089285"
gradientUnits="userSpaceOnUse" />
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient12948"
id="radialGradient3716"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1,-7.045514e-15,1.946707e-15,0.941176,2.788953e-13,3.492906)"
cx="23.190451"
cy="59.379417"
fx="22.471308"
fy="59.354759"
r="2.1082227" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient1563"
id="linearGradient3732"
x1="98.291809"
y1="-126.7503"
x2="44.242641"
y2="101.45739"
xlink:href="#linearGradient2286"
id="radialGradient2292"
cx="24"
cy="36.75"
fx="24"
fy="36.75"
r="22.5"
gradientTransform="matrix(1,0,0,0.3,-3.16587e-17,25.725)"
gradientUnits="userSpaceOnUse" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient1563"
id="linearGradient3739"
gradientUnits="userSpaceOnUse"
x1="98.291809"
y1="-44.01474"
x2="44.242641"
y2="101.45739" />
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3291"
id="radialGradient3743"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1,0,0,0.197802,0,92.82166)"
cx="63.912209"
cy="115.70919"
fx="63.975182"
fy="116.88514"
r="63.912209" />
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3747"
id="radialGradient3753"
cx="5.7531347"
cy="-45.41592"
fx="74.816956"
fy="-43.169445"
r="124.10334"
gradientTransform="matrix(1,-5.290907e-17,-3.962245e-18,9.492274e-2,9.333694e-14,-41.10492)"
gradientUnits="userSpaceOnUse" />
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3747"
id="radialGradient3757"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1,-1.087455e-16,-5.565153e-18,9.492274e-2,-1.420331e-15,-41.10492)"
cx="5.7531347"
cy="-45.41592"
fx="74.816956"
fy="-43.169445"
r="124.10334" />
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3747"
id="radialGradient3761"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1,-1.302059e-16,-7.897474e-18,9.492274e-2,1.345372e-13,-41.10492)"
cx="5.7531347"
cy="-45.41592"
fx="74.816956"
fy="-43.169445"
r="124.10334" />
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3765"
id="radialGradient3771"
cx="23.662739"
cy="95.898506"
fx="24.26058"
fy="96.778763"
r="2.793914"
gradientTransform="matrix(1.484142,0.129521,-0.489782,5.61225,35.51325,-445.3727)"
xlink:href="#linearGradient2301"
id="linearGradient2307"
x1="23.955357"
y1="10.008928"
x2="29.214285"
y2="30.276785"
gradientUnits="userSpaceOnUse" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3290"
id="linearGradient3106"
x1="84.634949"
y1="116.10083"
x2="89.72541"
y2="-15.33666"
xlink:href="#linearGradient2309"
id="linearGradient2315"
x1="6.7230334"
y1="37.683041"
x2="37.804565"
y2="29.096745"
gradientUnits="userSpaceOnUse" />
<radialGradient
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1,0,0,0.111111,0,138.1081)"
r="64.796692"
fy="177.29686"
fx="80.738739"
cy="155.37218"
cx="80.738739"
id="radialGradient5079"
xlink:href="#linearGradient5073"
inkscape:collect="always" />
<linearGradient
id="linearGradient5073"
inkscape:collect="always">
<stop
id="stop5075"
offset="0"
style="stop-color:#000000;stop-opacity:1;" />
<stop
id="stop5077"
offset="1"
style="stop-color:#000000;stop-opacity:0;" />
</linearGradient>
<foreignObject
id="foreignObject7221"
height="1"
width="1"
y="0"
x="0"
requiredExtensions="http://ns.adobe.com/AdobeIllustrator/10.0/">
<i:pgfRef
xlink:href="#adobe_illustrator_pgf" />
</foreignObject>
<linearGradient
id="XMLID_1_"
gradientUnits="userSpaceOnUse"
x1="95.693398"
y1="141.1738"
x2="32.308601"
y2="77.789001">
<stop
offset="0"
style="stop-color:#75511A"
id="stop7227" />
<stop
offset="0.3988"
style="stop-color:#563A11"
id="stop7229" />
<stop
offset="0.7642"
style="stop-color:#402B0B"
id="stop7231" />
<stop
offset="1"
style="stop-color:#382509"
id="stop7233" />
</linearGradient>
<linearGradient
id="XMLID_3_"
gradientUnits="userSpaceOnUse"
x1="63.9995"
y1="92.865196"
x2="63.9995"
y2="120.8652"
gradientTransform="translate(175.0067,11.74752)">
<stop
offset="0"
style="stop-color:#888A85"
id="stop7261" />
<stop
offset="0.3226"
style="stop-color:#A6A7A3"
id="stop7263" />
<stop
offset="1"
style="stop-color:#EEEEEC"
id="stop7265" />
</linearGradient>
<linearGradient
id="XMLID_4_"
gradientUnits="userSpaceOnUse"
x1="64.000504"
y1="108.8652"
x2="64.000504"
y2="92.865196">
<stop
offset="0"
style="stop-color:#EEEEEC"
id="stop7270" />
<stop
offset="1"
style="stop-color:#FFFFFF"
id="stop7272" />
</linearGradient>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3081"
id="linearGradient2149"
gradientUnits="userSpaceOnUse"
x1="62.112335"
y1="90.513916"
x2="67.887672"
y2="39.095695" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient26907"
id="linearGradient3226"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0,1,-1,0,-39.9985,140.0029)"
x1="-70.002899"
y1="-383.9971"
x2="-11.91648"
y2="-383.9971" />
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient3711"
id="radialGradient3228"
gradientUnits="userSpaceOnUse"
cx="343.99899"
cy="92"
fx="343.99899"
fy="92"
r="36" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3711"
id="linearGradient3230"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0,1.022977,-1.022977,0,111.9686,137.8125)"
x1="-88.058083"
y1="-131.93112"
x2="-45.096584"
y2="-131.93112" />
</defs>
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
bordercolor="#a8a8a8"
borderopacity="1"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="4.8203125"
inkscape:cx="64"
inkscape:cy="64"
inkscape:zoom="7.919596"
inkscape:cx="41.482905"
inkscape:cy="24.425816"
inkscape:document-units="px"
inkscape:current-layer="layer1"
inkscape:window-width="1247"
inkscape:window-height="816"
inkscape:window-x="388"
inkscape:window-y="110"
inkscape:showpageshadow="false"
inkscape:grid-bbox="true"
showgrid="true"
gridspacingx="4px"
gridspacingy="4px"
gridempspacing="0"
inkscape:grid-points="true" />
inkscape:grid-points="true"
gridspacingx="0.5px"
gridspacingy="0.5px"
gridempspacing="2"
inkscape:window-width="872"
inkscape:window-height="694"
inkscape:window-x="0"
inkscape:window-y="25"
fill="#75507b" />
<metadata
id="metadata7">
<rdf:RDF>
@ -458,133 +249,108 @@
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:creator>
<cc:Agent>
<dc:title>Ulisse Perusin</dc:title>
</cc:Agent>
</dc:creator>
<dc:title>Dictionary</dc:title>
<dc:subject>
<rdf:Bag>
<rdf:li>dictionary</rdf:li>
<rdf:li>translation</rdf:li>
</rdf:Bag>
</dc:subject>
<cc:license
rdf:resource="http://creativecommons.org/licenses/GPL/2.0/" />
</cc:Work>
<cc:License
rdf:about="http://creativecommons.org/licenses/GPL/2.0/">
<cc:permits
rdf:resource="http://web.resource.org/cc/Reproduction" />
<cc:permits
rdf:resource="http://web.resource.org/cc/Distribution" />
<cc:requires
rdf:resource="http://web.resource.org/cc/Notice" />
<cc:permits
rdf:resource="http://web.resource.org/cc/DerivativeWorks" />
<cc:requires
rdf:resource="http://web.resource.org/cc/ShareAlike" />
<cc:requires
rdf:resource="http://web.resource.org/cc/SourceCode" />
</cc:License>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:label="Livello 1"
inkscape:groupmode="layer"
id="layer1">
<path
transform="matrix(0.511285,0.187762,-0.187762,0.511285,41.72321,44.08266)"
d="M 153.09403,94.713757 C 144.53658,107.09689 92.616372,93.013297 78.414631,98.001518 C 64.21289,102.98974 32.50348,146.4474 18.082028,142.13539 C 3.6605746,137.82337 1.0106378,84.092245 -8.1220219,72.127031 C -17.254681,60.161818 -68.384124,43.433534 -68.739625,28.385431 C -69.095125,13.337327 -18.812666,-5.7867426 -10.255219,-18.169872 C -1.697772,-30.553002 -1.5880954,-84.349316 12.613645,-89.337536 C 26.815387,-94.325757 60.541592,-52.41396 74.963045,-48.101941 C 89.384498,-43.789923 140.58172,-60.30959 149.71438,-48.344376 C 158.84704,-36.379162 129.40853,8.6478227 129.76403,23.695927 C 130.11953,38.74403 161.65148,82.330628 153.09403,94.713757 z "
inkscape:randomized="0"
inkscape:rounded="0.20136392"
inkscape:flatsided="false"
sodipodi:arg2="1.2330172"
sodipodi:arg1="0.60469864"
sodipodi:r2="76.832565"
sodipodi:r1="121.72647"
sodipodi:cy="25.510532"
sodipodi:cx="52.952892"
sodipodi:sides="5"
id="path3574"
style="opacity:1;fill:#e3ad00;fill-opacity:1.0;fill-rule:nonzero;stroke:none;stroke-width:14.80892919;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:4;stroke-opacity:1"
sodipodi:type="star" />
<path
style="opacity:1;fill:url(#linearGradient3106);fill-opacity:1.0;fill-rule:nonzero;stroke:url(#linearGradient3605);stroke-width:6.803;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:4;stroke-opacity:1"
d="M 64.817613,10.159328 C 64.581604,10.317484 63.312654,10.957094 61.843149,12.708869 C 60.101516,14.785047 58.138879,17.917081 56.177505,21.235666 C 54.216128,24.55425 52.251443,28.092247 50.341888,31.150546 C 48.432331,34.208845 46.806952,36.762169 44.279648,38.544213 C 41.752344,40.326257 38.764915,41.002069 35.242943,41.773631 C 31.720971,42.545192 27.75285,43.193621 23.968308,43.926576 C 20.183765,44.659533 16.5656,45.476237 14.025101,46.419462 C 11.841858,47.230044 10.829167,48.201295 10.625712,48.345781 C 10.626839,48.347738 10.624524,48.371367 10.625712,48.374108 C 10.696321,48.571093 10.870285,49.989399 12.127109,52.000123 C 13.563478,54.298089 15.950898,57.154462 18.50096,60.045339 C 21.051023,62.936217 23.774397,65.867627 26.092925,68.628793 C 28.411454,71.389955 30.363146,73.748045 31.27699,76.702337 C 32.190833,79.656627 31.914822,82.68926 31.560274,86.277278 C 31.205724,89.8653 30.616267,93.839413 30.143862,97.665227 C 30.113483,97.911252 30.08362,98.156728 30.054361,98.401429 C 29.628627,101.96194 29.330856,105.3582 29.435657,107.89172 C 29.533657,110.26089 30.173974,111.54076 30.228847,111.74436 C 30.438123,111.73798 31.837454,111.97838 34.138142,111.40442 C 36.7675,110.74847 40.20401,109.39531 43.741411,107.86339 C 47.278812,106.33148 50.937026,104.62602 54.279513,103.27421 C 57.621999,101.9224 60.450754,100.79418 63.542844,100.838 C 66.634933,100.8818 69.418339,102.08321 72.721189,103.52916 C 76.024038,104.97512 79.653393,106.79843 83.145977,108.42996 C 86.638561,110.06147 90.026215,111.49575 92.635935,112.22595 C 94.919441,112.86485 96.334003,112.66799 96.54523,112.67918 C 96.605258,112.47676 97.286649,111.22034 97.451733,108.85487 C 97.640395,106.1515 97.418963,102.46604 97.055137,98.628384 C 96.691312,94.790732 96.174767,90.780408 95.922008,87.183781 C 95.669251,83.587159 95.491404,80.56438 96.488573,77.637169 C 97.485753,74.709955 99.503438,72.399636 101.89927,69.705264 C 104.29508,67.010894 107.11524,64.16591 109.74619,61.348436 C 112.37711,58.530963 114.78913,55.729544 116.29001,53.47319 C 117.57984,51.534136 117.84976,50.137859 117.93304,49.903833 C 117.93436,49.901119 117.93183,49.877435 117.93304,49.875506 C 117.7318,49.725835 116.72138,48.73631 114.56198,47.864201 C 112.04922,46.849382 108.49434,45.927914 104.73209,45.088035 C 100.96983,44.248155 97.012813,43.466178 93.514108,42.595149 C 90.015409,41.724119 87.038194,40.992031 84.562389,39.139105 C 82.086586,37.286179 80.548923,34.65831 78.726774,31.54714 C 76.904626,28.435971 75.041014,24.863438 73.174441,21.49062 C 71.307869,18.117801 69.417536,14.946869 67.735421,12.822182 C 66.263569,10.963081 64.982527,10.293346 64.817613,10.159328 z "
id="path3580"
sodipodi:nodetypes="cssssssssssssssssscssssssscssssssssssssssssc" />
<path
sodipodi:nodetypes="ccc"
id="path2276"
d="M -106.3852,44.124126 L -106.3852,41.329417 L -106.3852,44.124126 z "
style="fill:#ffffff;fill-opacity:0.75688076;fill-rule:nonzero;stroke:none;stroke-width:0.5;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:4;stroke-opacity:1" />
<path
sodipodi:type="arc"
style="opacity:0.38139535;fill:url(#radialGradient3743);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:4;stroke-opacity:1"
id="path3289"
sodipodi:cx="63.912209"
sodipodi:cy="115.70919"
sodipodi:rx="63.912209"
sodipodi:ry="12.641975"
d="M 127.82442 115.70919 A 63.912209 12.641975 0 1 1 0,115.70919 A 63.912209 12.641975 0 1 1 127.82442 115.70919 z"
transform="matrix(-1.001374,0,0,0.410379,128,75.32738)" />
style="opacity:0.50196078;color:#000000;fill:url(#radialGradient2292);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:17.85;stroke-opacity:1;visibility:visible;display:block;overflow:visible"
id="path2284"
sodipodi:cx="24"
sodipodi:cy="36.75"
sodipodi:rx="22.5"
sodipodi:ry="6.75"
d="M 46.5 36.75 A 22.5 6.75 0 1 1 1.5,36.75 A 22.5 6.75 0 1 1 46.5 36.75 z"
transform="matrix(1.066667,0,0,0.962963,-1.600001,1.111111)" />
<path
style="fill:none;fill-opacity:1.0;fill-rule:evenodd;stroke:url(#linearGradient3648);stroke-width:0.50672567;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0.5813008"
d="M 55.266721,10.739701 C 56.520212,8.5685899 61.220699,1.3579337 65.008418,1.4134271 C 71.889436,1.5172832 83.511202,31.129589 88.946059,34.460427 C 95.635958,38.560436 119.92387,41.46414 124.34296,44.969282"
id="path3632"
sodipodi:nodetypes="csss" />
style="color:#000000;fill:#523856;fill-opacity:1;fill-rule:nonzero;stroke:#3e263b;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:17.85;stroke-opacity:1;visibility:visible;display:block;overflow:visible"
d="M 4.5,11.5 L 43.5,11.5 L 47.5,38.5 L 29,38.5 L 28,37.5 C 26,39 22,39 20,37.5 L 19,38.5 L 0.5,38.5 L 4.5,11.5 z "
id="rect1304"
sodipodi:nodetypes="ccccccccc" />
<path
sodipodi:nodetypes="csss"
id="path3634"
d="M 55.236135,11.274949 C 56.489626,9.1038383 61.236542,1.57297 65.023711,1.6581121 C 71.830955,1.8111507 83.271335,31.483209 88.869595,34.705112 C 95.670099,38.618929 119.98852,41.765855 124.40761,45.270997"
style="fill:none;fill-opacity:1.0;fill-rule:evenodd;stroke:url(#linearGradient3646);stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0.43089432" />
sodipodi:type="inkscape:offset"
inkscape:radius="-0.91809106"
inkscape:original="M 4.5 11.5 L 0.5 38.5 L 19 38.5 L 20 37.5 C 22 39 26 39 28 37.5 L 29 38.5 L 47.5 38.5 L 43.5 11.5 L 4.5 11.5 z "
xlink:href="#rect1304"
style="opacity:0.13333333;color:#000000;fill:none;fill-opacity:1;fill-rule:nonzero;stroke:#ffffff;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:17.85;stroke-opacity:1;visibility:visible;display:block;overflow:visible"
id="path2274"
inkscape:href="#rect1304"
d="M 5.28125,12.40625 L 1.5625,37.59375 L 18.59375,37.59375 L 19.34375,36.84375 C 19.667151,36.507336 20.191452,36.467006 20.5625,36.75 C 21.327469,37.323727 22.653015,37.71875 24,37.71875 C 25.346985,37.71875 26.672531,37.323727 27.4375,36.75 C 27.808548,36.467006 28.332849,36.507336 28.65625,36.84375 L 29.40625,37.59375 L 46.4375,37.59375 L 42.71875,12.40625 L 5.28125,12.40625 z " />
<path
style="fill:none;fill-opacity:1.0;fill-rule:evenodd;stroke:url(#linearGradient3644);stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0.43089432"
d="M 55.205632,12.312054 C 56.459123,10.140944 61.420826,2.0361374 65.023711,2.1461616 C 71.282863,2.3391656 83.42385,32.459308 88.778086,35.193162 C 95.766183,38.761259 121.08663,42.680948 124.71264,46.094581"
id="path3636"
sodipodi:nodetypes="csss" />
style="fill:url(#linearGradient2282);fill-opacity:1.0;fill-rule:evenodd;stroke:#888a85;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 2,36.5 C 7.6666667,36.5 16,35 19,36.5 C 22,34 26,34 29,36.5 C 32,35 41,36.5 46,36.5 L 45.5,34 C 38.5,31.5 29,28.5 24,33 C 19,28.5 9.5,31.5 2.5,34 L 2,36.5 z "
id="path2180"
sodipodi:nodetypes="cccccccc" />
<path
sodipodi:type="arc"
style="opacity:0.70232556;fill:url(#radialGradient3716);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.89999998;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:4;stroke-opacity:1"
id="path11160"
sodipodi:cx="23.190451"
sodipodi:cy="59.379417"
sodipodi:rx="2.1082227"
sodipodi:ry="1.9842097"
d="M 25.298673 59.379417 A 2.1082227 1.9842097 0 1 1 21.082228,59.379417 A 2.1082227 1.9842097 0 1 1 25.298673 59.379417 z"
transform="matrix(-1.742936,-1.063485,-0.470527,1.244278,191.1539,-3.699137)" />
sodipodi:type="inkscape:offset"
inkscape:radius="-1.0582203"
inkscape:original="M 14 30.875 C 10.125 31.375 6 32.75 2.5 34 L 2 36.5 C 7.6666667 36.5 16 35 19 36.5 C 22 34 26 34 29 36.5 C 32 35 41 36.5 46 36.5 L 45.5 34 C 38.5 31.5 29 28.5 24 33 C 21.5 30.75 17.875 30.375 14 30.875 z "
xlink:href="#path2180"
style="opacity:0.30196078;fill:none;fill-opacity:1;fill-rule:evenodd;stroke:url(#linearGradient2315);stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path2266"
inkscape:href="#path2180"
d="M 14.375,31.9375 C 10.963293,32.392394 7.260823,33.622273 3.90625,34.8125 L 3.8125,35.34375 C 6.2979599,35.262594 9.0476285,35.037732 11.6875,34.875 C 14.462294,34.703951 16.881256,34.711661 18.78125,35.40625 C 20.133116,34.409774 21.661646,33.894157 23.21875,33.75 C 21.042747,31.830616 17.941674,31.461944 14.375,31.9375 z M 28.625,31.9375 C 27.145571,32.213473 25.86037,32.798142 24.78125,33.75 C 26.338354,33.894157 27.866884,34.409774 29.21875,35.40625 C 31.163554,34.697135 33.704549,34.703523 36.5625,34.875 C 39.261382,35.036933 41.920385,35.260963 44.1875,35.34375 L 44.09375,34.8125 C 40.739177,33.622273 37.036707,32.392394 33.625,31.9375 C 31.827105,31.697781 30.128781,31.656984 28.625,31.9375 z " />
<path
style="opacity:1;fill:url(#linearGradient3732);fill-opacity:1.0;fill-rule:nonzero;stroke:none;stroke-width:6.803;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:4;stroke-opacity:1"
d="M 64.8125 10.15625 C 64.576492 10.314406 63.313255 10.966975 61.84375 12.71875 C 60.102119 14.794928 58.148874 17.931415 56.1875 21.25 C 54.226123 24.568584 52.253305 28.097951 50.34375 31.15625 C 48.434193 34.21455 46.808554 36.749206 44.28125 38.53125 C 41.753946 40.313295 38.771972 41.009688 35.25 41.78125 C 31.728028 42.55281 27.753292 43.204545 23.96875 43.9375 C 20.184208 44.670458 16.571749 45.463025 14.03125 46.40625 C 11.848007 47.216834 10.828455 48.199264 10.625 48.34375 C 10.62496 48.346283 10.625145 48.371753 10.625 48.375 C 10.695609 48.571986 10.868176 49.989276 12.125 52 C 13.561369 54.297967 15.949938 57.140373 18.5 60.03125 C 20.422509 62.210702 22.440722 64.427292 24.3125 66.5625 C 47.187815 68.967477 71.532076 77.450485 95.75 81.53125 C 95.830132 80.186335 96.067405 78.894893 96.5 77.625 C 97.497182 74.697786 99.510418 72.413122 101.90625 69.71875 C 104.30206 67.024383 107.11905 64.161224 109.75 61.34375 C 112.38092 58.526279 114.78037 55.725104 116.28125 53.46875 C 117.57108 51.529696 117.85422 50.140276 117.9375 49.90625 C 117.93747 49.903618 117.93766 49.878251 117.9375 49.875 C 117.73626 49.725328 116.7219 48.747109 114.5625 47.875 C 112.04974 46.860181 108.481 45.933629 104.71875 45.09375 C 100.95649 44.253869 96.998705 43.464779 93.5 42.59375 C 90.001302 41.722719 87.038305 40.977926 84.5625 39.125 C 82.0867 37.272072 80.540899 34.67367 78.71875 31.5625 C 76.8966 28.451331 75.054073 24.872818 73.1875 21.5 C 71.320931 18.127181 69.432115 14.937187 67.75 12.8125 C 66.278149 10.953399 64.977414 10.290268 64.8125 10.15625 z "
id="path3718" />
style="fill:url(#linearGradient2245);fill-opacity:1;fill-rule:evenodd;stroke:url(#linearGradient2247);stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 2.5,34 C 9,31.5 20,29 24,33 C 28,29 39,31.5 45.5,34 L 42.5,10.5 C 37,8 27.5,6 24,9 C 20,6 12,8 5.5,10.5 L 2.5,34 z "
id="path2182"
sodipodi:nodetypes="ccccccc" />
<path
style="opacity:0.41393443;fill:url(#linearGradient3739);fill-opacity:1.0;fill-rule:nonzero;stroke:none;stroke-width:6.803;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:4;stroke-opacity:1"
d="M 64.8125 10.15625 C 64.576492 10.314406 63.313255 10.966975 61.84375 12.71875 C 60.102119 14.794928 58.148874 17.931415 56.1875 21.25 C 54.226123 24.568584 52.253305 28.097951 50.34375 31.15625 C 48.434193 34.21455 46.808554 36.749206 44.28125 38.53125 C 41.753946 40.313295 38.771972 41.009688 35.25 41.78125 C 31.728028 42.55281 27.753292 43.204545 23.96875 43.9375 C 20.184208 44.670458 16.571749 45.463025 14.03125 46.40625 C 11.848007 47.216834 10.828455 48.199264 10.625 48.34375 C 10.62496 48.346283 10.625145 48.371753 10.625 48.375 C 10.695609 48.571986 10.868176 49.989276 12.125 52 C 13.561369 54.297967 15.949938 57.140373 18.5 60.03125 C 20.422509 62.210702 22.440722 64.427292 24.3125 66.5625 C 25.752576 66.713901 27.204929 66.896788 28.65625 67.09375 C 42.328845 56.623879 60.733777 43.188439 78.53125 31.25 C 76.771363 28.21678 74.98888 24.755017 73.1875 21.5 C 71.320931 18.127181 69.432115 14.937187 67.75 12.8125 C 66.278149 10.953399 64.977414 10.290268 64.8125 10.15625 z M 99.34375 43.90625 C 86.79565 53.381359 75.792347 63.914843 66.25 75.09375 C 76.032927 77.504442 85.901575 79.871772 95.75 81.53125 C 95.830132 80.186335 96.067405 78.894893 96.5 77.625 C 97.497182 74.697786 99.510418 72.413122 101.90625 69.71875 C 104.30206 67.024383 107.11905 64.161224 109.75 61.34375 C 112.38092 58.526279 114.78037 55.725104 116.28125 53.46875 C 117.57108 51.529696 117.85422 50.140276 117.9375 49.90625 C 117.93747 49.903618 117.93766 49.878251 117.9375 49.875 C 117.73626 49.725328 116.7219 48.747109 114.5625 47.875 C 112.04974 46.860181 108.481 45.933629 104.71875 45.09375 C 102.94925 44.698729 101.13165 44.292609 99.34375 43.90625 z "
id="path3736" />
<path
id="path3741"
d="M 64.8125,10.15625 C 64.576492,10.314406 63.313255,10.966975 61.84375,12.71875 C 60.102119,14.794928 58.148874,17.931415 56.1875,21.25 C 54.226123,24.568584 52.253305,28.097951 50.34375,31.15625 C 48.434193,34.21455 46.808554,36.749206 44.28125,38.53125 C 41.753946,40.313295 38.771972,41.009688 35.25,41.78125 C 31.728028,42.55281 27.753292,43.204545 23.96875,43.9375 C 20.184208,44.670458 16.571749,45.463025 14.03125,46.40625 C 11.848007,47.216834 10.828455,48.199264 10.625,48.34375 C 10.62496,48.346283 10.625145,48.371753 10.625,48.375 C 10.695609,48.571986 10.868176,49.989276 12.125,52 C 13.561369,54.297967 60.733777,43.188439 78.53125,31.25 C 76.771363,28.21678 74.98888,24.755017 73.1875,21.5 C 71.320931,18.127181 69.432115,14.937187 67.75,12.8125 C 66.278149,10.953399 64.977414,10.290268 64.8125,10.15625 z M 99.34375,43.90625 C 68.470207,67.487324 85.901575,79.871772 95.75,81.53125 C 95.830132,80.186335 96.067405,78.894893 96.5,77.625 C 97.497182,74.697786 99.510418,72.413122 101.90625,69.71875 C 104.30206,67.024383 107.11905,64.161224 109.75,61.34375 C 112.38092,58.526279 114.78037,55.725104 116.28125,53.46875 C 117.57108,51.529696 117.85422,50.140276 117.9375,49.90625 C 117.93747,49.903618 117.93766,49.878251 117.9375,49.875 C 117.73626,49.725328 116.7219,48.747109 114.5625,47.875 C 112.04974,46.860181 108.481,45.933629 104.71875,45.09375 C 102.94925,44.698729 101.13165,44.292609 99.34375,43.90625 z "
style="opacity:0.34836066;fill:url(#linearGradient3739);fill-opacity:1.0;fill-rule:nonzero;stroke:none;stroke-width:6.803;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:4;stroke-opacity:1"
sodipodi:nodetypes="csssssssssscsscccssssssssc" />
<path
sodipodi:type="arc"
style="opacity:0.35655739;fill:url(#radialGradient3753);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:4;stroke-opacity:0.43089432"
id="path3745"
sodipodi:cx="5.7531347"
sodipodi:cy="-45.41592"
sodipodi:rx="124.10334"
sodipodi:ry="11.780229"
d="M 129.85647 -45.41592 A 124.10334 11.780229 0 1 1 -118.35021,-45.41592 A 124.10334 11.780229 0 1 1 129.85647 -45.41592 z"
transform="matrix(0.126835,-5.623734e-2,-3.870485e-2,-9.211943e-2,44.81196,106.2565)" />
<path
transform="matrix(-0.126834,-5.702883e-2,3.870485e-2,-9.341592e-2,81.95911,106.3126)"
d="M 129.85647 -45.41592 A 124.10334 11.780229 0 1 1 -118.35021,-45.41592 A 124.10334 11.780229 0 1 1 129.85647 -45.41592 z"
sodipodi:ry="11.780229"
sodipodi:rx="124.10334"
sodipodi:cy="-45.41592"
sodipodi:cx="5.7531347"
id="path3755"
style="opacity:0.49590164;fill:url(#radialGradient3757);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:4;stroke-opacity:0.43089432"
sodipodi:type="arc" />
<path
transform="matrix(-6.548665e-3,-0.135343,-9.988208e-2,2.696531e-3,91.9485,98.93228)"
d="M 129.85647 -45.41592 A 124.10334 11.780229 0 1 1 -118.35021,-45.41592 A 124.10334 11.780229 0 1 1 129.85647 -45.41592 z"
sodipodi:ry="11.780229"
sodipodi:rx="124.10334"
sodipodi:cy="-45.41592"
sodipodi:cx="5.7531347"
id="path3759"
style="opacity:0.27459016;fill:url(#radialGradient3761);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:4;stroke-opacity:0.43089432"
sodipodi:type="arc" />
<path
style="opacity:0.17;fill:#2e3436;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:14.80892944;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:4;stroke-opacity:1"
d="M 126.314,45.945281 C 127.57715,54.014451 105.05512,70.888621 102.47025,78.476531 C 99.826558,86.237151 107.76438,114.43949 101.06401,119.16403 C 94.363618,123.88859 70.449248,106.96767 62.251508,106.85153 C 54.053768,106.73539 29.690348,122.98275 23.126508,118.07028 C 22.815738,117.8377 22.558998,117.52665 22.314008,117.19528 C 22.708398,118.50348 23.279848,119.53038 24.126508,120.16403 C 30.690348,125.0765 55.053768,108.82913 63.251508,108.94528 C 71.449248,109.06142 95.363618,126.01358 102.064,121.28903 C 108.76438,116.56449 100.82656,88.362141 103.47025,80.601531 C 106.11395,72.840911 129.61178,55.340191 127.189,47.507781 C 127.02007,46.961651 126.72727,46.430131 126.314,45.945281 z M 1.5015079,49.851531 C 4.5831579,57.838661 18.613888,70.110761 22.845258,77.320281 C 20.896748,71.159061 6.3960679,58.682221 1.5015079,49.851531 z "
id="path3308" />
<path
style="opacity:0.17;fill:#2e3436;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:14.80892944;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:4;stroke-opacity:1"
d="M 127.1265,47.382781 C 127.77543,55.569451 105.80307,72.111371 103.2515,79.601531 C 100.60781,87.362151 108.54563,115.56449 101.84525,120.28903 C 95.144868,125.01357 71.261748,108.06142 63.064008,107.94528 C 54.866278,107.82913 30.471598,124.10775 23.907758,119.19528 C 22.913828,118.45141 22.292008,117.15904 21.907758,115.50778 C 22.250148,117.65334 22.937778,119.30561 24.126508,120.19528 C 30.690348,125.10775 55.053778,108.82913 63.251508,108.94528 C 71.449248,109.06142 95.363638,126.01357 102.064,121.28903 C 108.76438,116.56449 100.82655,88.362151 103.47025,80.601531 C 106.11395,72.840911 129.61177,55.340181 127.189,47.507781 C 127.17552,47.464201 127.14164,47.425951 127.1265,47.382781 z M 0.93900787,47.757781 C 1.9815279,56.300511 21.645828,72.265141 23.876508,79.476531 C 23.903658,79.564291 23.914628,79.665031 23.939008,79.757781 C 23.870988,79.288551 23.775168,78.856101 23.657758,78.476531 C 21.511968,71.539581 3.2557579,56.495971 0.93900787,47.757781 z "
id="path3303" />
<path
style="fill:url(#radialGradient3771);fill-opacity:1.0;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1;opacity:0.55327869"
d="M 25.088207,80.21837 C 25.534474,90.747814 22.054583,105.95279 22.237274,111.46459 L 24.632057,111.69267 C 26.600057,101.69251 28.017156,91.508728 28.167214,80.902594 L 25.088207,80.21837 z "
id="path3763"
style="color:#000000;fill:url(#linearGradient2219);fill-opacity:1.0;fill-rule:nonzero;stroke:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dashoffset:17.85;stroke-opacity:1;visibility:visible;display:block;overflow:visible"
d="M 24,9.5 C 22,8 19.5,7.5 16,8 L 16,30.5 C 18,29.5 22,30.5 24,32.5 L 24,9.5 z "
id="rect2192"
sodipodi:nodetypes="ccccc" />
<path
style="opacity:0.62;fill:#2e3436;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:14.80892944;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:4;stroke-opacity:1"
d="M 0.89521787,47.070281 C 0.89114787,55.417331 21.603498,72.067851 23.895208,79.476531 C 24.122348,80.210811 24.248638,81.111361 24.301458,82.164031 C 24.258118,80.995191 24.141798,79.961201 23.895208,79.164031 C 21.629398,71.839061 1.3489179,55.506151 0.89521787,47.070281 z M 127.36395,48.789031 C 126.76815,57.193121 105.96742,73.013441 103.48895,80.289031 C 103.13757,81.320501 102.96318,82.732101 102.92645,84.382781 C 102.97549,82.859581 103.15849,81.571601 103.48895,80.601531 C 105.99802,73.236121 127.27213,57.108781 127.36395,48.789031 z M 63.270208,108.63278 C 55.072468,108.51664 30.709048,124.79525 24.145208,119.88278 C 22.709368,118.80818 21.987678,116.61813 21.738958,113.78903 C 21.963318,116.76637 22.658218,119.08239 24.145208,120.19528 C 30.709048,125.10775 55.072468,108.82913 63.270208,108.94528 C 71.467938,109.06142 95.382328,126.01357 102.0827,121.28903 C 103.80587,120.07399 104.52313,117.30046 104.73895,113.72653 C 104.49958,117.15646 103.75779,119.79539 102.0827,120.97653 C 95.382328,125.70107 71.467938,108.74892 63.270208,108.63278 z "
id="path3296" />
style="color:#000000;fill:url(#linearGradient2211);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:miter;marker:none;marker-start:none;marker-mid:none;marker-end:none;stroke-miterlimit:4;stroke-dashoffset:17.85;stroke-opacity:1;visibility:visible;display:block;overflow:visible"
d="M 24,9.5 C 25.221264,8.803878 26.327771,7.9069322 28,8 L 29,30.5 C 27.5,30 25.5,31.5 24,32.5 L 24,9.5 z "
id="path2195"
sodipodi:nodetypes="ccccc" />
<path
sodipodi:type="inkscape:offset"
inkscape:radius="-0.92850536"
inkscape:original="M 20.34375 7.625 C 16.101562 7.0390625 10.375 8.625 5.5 10.5 L 2.5 34 C 9 31.5 20 29 24 33 C 28 29 39 31.5 45.5 34 L 42.5 10.5 C 37 8 27.5 6 24 9 C 23 8.25 21.757812 7.8203125 20.34375 7.625 z "
xlink:href="#path2182"
style="opacity:0.65098039;fill:none;fill-opacity:1;fill-rule:evenodd;stroke:#ffffff;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path2243"
inkscape:href="#path2182"
d="M 17.03125,8.375 C 14.611845,8.6563261 11.827815,9.5624782 8.78125,10.71875 L 4.25,32.59375 C 7.5567067,31.338728 11.345145,30.271354 14.90625,29.9375 C 16.969491,29.744071 18.927893,29.768608 20.625,30.125 C 21.963283,30.406039 23.09173,31.003906 24,31.8125 C 24.90827,31.003906 26.036717,30.406039 27.375,30.125 C 29.072107,29.768608 31.030509,29.744071 33.09375,29.9375 C 36.654855,30.271354 40.443293,31.338728 43.75,32.59375 L 39.1875,10.6875 C 36.612085,9.5579242 33.750698,8.6570052 31.15625,8.375 C 28.420939,8.0776836 26.053467,8.4675643 24.59375,9.71875 C 24.262671,9.9972426 23.783138,10.010203 23.4375,9.75 C 21.660341,8.417131 19.571761,8.0795918 17.03125,8.375 z " />
<path
style="fill:url(#linearGradient2264);fill-opacity:1.0;fill-rule:evenodd;stroke:url(#linearGradient2307);stroke-width:1;stroke-linecap:square;stroke-linejoin:miter;stroke-opacity:1;stroke-miterlimit:4;stroke-dasharray:none"
d="M 24.455357,8.7321429 C 24.5,20.5 34,20 33.5,30.5 L 32.5,34.5 L 34,34 L 35,35 L 35.5,31 C 36,20 24.544643,19.089286 24.5,8.5 L 24.455357,8.7321429 z "
id="path2227"
sodipodi:nodetypes="cccccccc" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 15 KiB

File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 224 KiB

After

Width:  |  Height:  |  Size: 396 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 429 B

File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 47 KiB

File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 156 KiB

After

Width:  |  Height:  |  Size: 140 KiB

View File

@ -0,0 +1,78 @@
__license__ = 'GPL v3'
__copyright__ = '2010, Darko Miletic <darko.miletic at gmail.com>'
'''
akter.co.rs
'''
import re
from calibre.web.feeds.news import BasicNewsRecipe
class Akter(BasicNewsRecipe):
title = 'AKTER'
__author__ = 'Darko Miletic'
description = 'AKTER - nedeljni politicki magazin savremene Srbije'
publisher = 'Akter Media Group d.o.o.'
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
max_articles_per_feed = 100
no_stylesheets = False
use_embedded_content = False
encoding = 'utf-8'
masthead_url = 'http://www.akter.co.rs/templates/gk_thenews2/images/style2/logo.png'
language = 'sr'
publication_type = 'magazine'
remove_empty_feeds = True
PREFIX = 'http://www.akter.co.rs'
extra_css = """ @font-face {font-family: "serif1";src:url(res:///opt/sony/ebook/FONT/tt0011m_.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}
.color-2{display:block; margin-bottom: 10px; padding: 5px, 10px;
border-left: 1px solid #D00000; color: #D00000}
img{margin-bottom: 0.8em} """
conversion_options = {
'comment' : description
, 'tags' : category
, 'publisher' : publisher
, 'language' : language
, 'linearize_tables' : True
}
preprocess_regexps = [(re.compile(u'\u0110'), lambda match: u'\u00D0')]
feeds = [
(u'Politika' , u'http://www.akter.co.rs/index.php/politikaprint.html' )
,(u'Ekonomija' , u'http://www.akter.co.rs/index.php/ekonomijaprint.html')
,(u'Life&Style' , u'http://www.akter.co.rs/index.php/lsprint.html' )
,(u'Sport' , u'http://www.akter.co.rs/index.php/sportprint.html' )
]
def preprocess_html(self, soup):
for item in soup.findAll(style=True):
del item['style']
return self.adeify_images(soup)
def print_version(self, url):
return url + '?tmpl=component&print=1&page='
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':['sectiontableentry1','sectiontableentry2']}):
link = item.find('a')
url = self.PREFIX + link['href']
title = self.tag_to_string(link)
articles.append({
'title' :title
,'date' :''
,'url' :url
,'description':''
})
totalfeeds.append((feedtitle, articles))
return totalfeeds

View File

@ -1,4 +1,3 @@
# -*- coding: utf-8 -*-
__license__ = 'GPL v3'
__copyright__ = '2008-2010, Darko Miletic <darko.miletic at gmail.com>'
'''
@ -23,7 +22,14 @@ class Danas(BasicNewsRecipe):
language = 'sr'
publication_type = 'newspaper'
remove_empty_feeds = True
extra_css = '@font-face {font-family: "serif1";src:url(res:///opt/sony/ebook/FONT/tt0011m_.ttf)} @font-face {font-family: "sans1";src:url(res:///opt/sony/ebook/FONT/tt0003m_.ttf)} .article_description,body,.lokacija{font-family: Tahoma,Arial,Helvetica,sans1,sans-serif} .nadNaslov,h1,.preamble{font-family: Georgia,"Times New Roman",Times,serif1,serif} .antrfileText{border-left: 2px solid #999999; margin-left: 0.8em; padding-left: 1.2em; margin-bottom: 0; margin-top: 0} h2,.datum,.lokacija,.autor{font-size: small} .antrfileNaslov{border-left: 2px solid #999999; margin-left: 0.8em; padding-left: 1.2em; font-weight:bold; margin-bottom: 0; margin-top: 0} img{margin-bottom: 0.8em} '
extra_css = """ @font-face {font-family: "serif1";src:url(res:///opt/sony/ebook/FONT/tt0011m_.ttf)}
@font-face {font-family: "sans1";src:url(res:///opt/sony/ebook/FONT/tt0003m_.ttf)}
.article_description,body,.lokacija{font-family: Tahoma,Arial,Helvetica,sans1,sans-serif}
.nadNaslov,h1,.preamble{font-family: Georgia,"Times New Roman",Times,serif1,serif}
.antrfileText{border-left: 2px solid #999999; margin-left: 0.8em; padding-left: 1.2em;
margin-bottom: 0; margin-top: 0} h2,.datum,.lokacija,.autor{font-size: small}
.antrfileNaslov{border-left: 2px solid #999999; margin-left: 0.8em; padding-left: 1.2em;
font-weight:bold; margin-bottom: 0; margin-top: 0} img{margin-bottom: 0.8em} """
conversion_options = {
'comment' : description
@ -42,19 +48,32 @@ class Danas(BasicNewsRecipe):
]
feeds = [
(u'Politika' , u'http://www.danas.rs/rss/rss.asp?column_id=27')
,(u'Hronika' , u'http://www.danas.rs/rss/rss.asp?column_id=2' )
,(u'Dru\xc5\xa1tvo', u'http://www.danas.rs/rss/rss.asp?column_id=24')
,(u'Dijalog' , u'http://www.danas.rs/rss/rss.asp?column_id=1' )
,(u'Ekonomija', u'http://www.danas.rs/rss/rss.asp?column_id=6' )
,(u'Svet' , u'http://www.danas.rs/rss/rss.asp?column_id=25')
,(u'Srbija' , u'http://www.danas.rs/rss/rss.asp?column_id=28')
,(u'Kultura' , u'http://www.danas.rs/rss/rss.asp?column_id=5' )
,(u'Sport' , u'http://www.danas.rs/rss/rss.asp?column_id=13')
,(u'Scena' , u'http://www.danas.rs/rss/rss.asp?column_id=42')
,(u'Feljton' , u'http://www.danas.rs/rss/rss.asp?column_id=19')
,(u'Periskop' , u'http://www.danas.rs/rss/rss.asp?column_id=4' )
,(u'Famozno' , u'http://www.danas.rs/rss/rss.asp?column_id=47')
(u'Politika' , u'http://www.danas.rs/rss/rss.asp?column_id=27')
,(u'Hronika' , u'http://www.danas.rs/rss/rss.asp?column_id=2' )
,(u'Drustvo' , u'http://www.danas.rs/rss/rss.asp?column_id=24')
,(u'Dijalog' , u'http://www.danas.rs/rss/rss.asp?column_id=1' )
,(u'Ekonomija' , u'http://www.danas.rs/rss/rss.asp?column_id=6' )
,(u'Svet' , u'http://www.danas.rs/rss/rss.asp?column_id=25')
,(u'Srbija' , u'http://www.danas.rs/rss/rss.asp?column_id=28')
,(u'Kultura' , u'http://www.danas.rs/rss/rss.asp?column_id=5' )
,(u'Sport' , u'http://www.danas.rs/rss/rss.asp?column_id=13')
,(u'Scena' , u'http://www.danas.rs/rss/rss.asp?column_id=42')
,(u'Feljton' , u'http://www.danas.rs/rss/rss.asp?column_id=19')
,(u'Periskop' , u'http://www.danas.rs/rss/rss.asp?column_id=4' )
,(u'Famozno' , u'http://www.danas.rs/rss/rss.asp?column_id=47')
,(u'Sluzbena beleska' , u'http://www.danas.rs/rss/rss.asp?column_id=48')
,(u'Suocavanja' , u'http://www.danas.rs/rss/rss.asp?column_id=49')
,(u'Moj Izbor' , u'http://www.danas.rs/rss/rss.asp?column_id=50')
,(u'Direktno' , u'http://www.danas.rs/rss/rss.asp?column_id=51')
,(u'I tome slicno' , u'http://www.danas.rs/rss/rss.asp?column_id=52')
,(u'No longer and not yet', u'http://www.danas.rs/rss/rss.asp?column_id=53')
,(u'Resetovanje' , u'http://www.danas.rs/rss/rss.asp?column_id=54')
,(u'Iza scene' , u'http://www.danas.rs/rss/rss.asp?column_id=60')
,(u'Drustvoslovlje' , u'http://www.danas.rs/rss/rss.asp?column_id=55')
,(u'Zvaka u pepeljari' , u'http://www.danas.rs/rss/rss.asp?column_id=56')
,(u'Vostani Serbie' , u'http://www.danas.rs/rss/rss.asp?column_id=57')
,(u'Med&Jad-a' , u'http://www.danas.rs/rss/rss.asp?column_id=58')
,(u'Svetlosti pozornice' , u'http://www.danas.rs/rss/rss.asp?column_id=59')
]
def preprocess_html(self, soup):
@ -65,3 +84,10 @@ class Danas(BasicNewsRecipe):
def print_version(self, url):
return url + '&action=print'
def get_cover_url(self):
cover_url = None
soup = self.index_to_soup('http://www.danas.rs/')
for citem in soup.findAll('img'):
if citem['src'].endswith('naslovna.jpg'):
return 'http://www.danas.rs' + citem['src']
return cover_url

View File

@ -17,6 +17,7 @@ class NYTimes(BasicNewsRecipe):
title = 'New York Times Top Stories'
__author__ = 'GRiker'
language = 'en'
requires_version = (0, 7, 3)
description = 'Top Stories from the New York Times'
# List of sections typically included in Top Stories. Use a keyword from the
@ -64,6 +65,7 @@ class NYTimes(BasicNewsRecipe):
timefmt = ''
needs_subscription = True
masthead_url = 'http://graphics8.nytimes.com/images/misc/nytlogo379x64.gif'
cover_margins = (18,18,'grey99')
remove_tags_before = dict(id='article')
remove_tags_after = dict(id='article')
@ -183,6 +185,16 @@ class NYTimes(BasicNewsRecipe):
self.log("\nFailed to login")
return br
def skip_ad_pages(self, soup):
# Skip ad pages served before actual article
skip_tag = soup.find(True, {'name':'skip'})
if skip_tag is not None:
self.log.warn("Found forwarding link: %s" % skip_tag.parent['href'])
url = 'http://www.nytimes.com' + re.sub(r'\?.*', '', skip_tag.parent['href'])
url += '?pagewanted=all'
self.log.warn("Skipping ad to article at '%s'" % url)
return self.index_to_soup(url, raw=True)
def get_cover_url(self):
cover = None
st = time.localtime()
@ -391,14 +403,6 @@ class NYTimes(BasicNewsRecipe):
return ans
def preprocess_html(self, soup):
# Skip ad pages served before actual article
skip_tag = soup.find(True, {'name':'skip'})
if skip_tag is not None:
self.log.error("Found forwarding link: %s" % skip_tag.parent['href'])
url = 'http://www.nytimes.com' + re.sub(r'\?.*', '', skip_tag.parent['href'])
url += '?pagewanted=all'
self.log.error("Skipping ad to article at '%s'" % url)
soup = self.index_to_soup(url)
return self.strip_anchors(soup)
def postprocess_html(self,soup, True):

View File

@ -20,6 +20,7 @@ class NYTimes(BasicNewsRecipe):
title = 'The New York Times'
__author__ = 'GRiker'
language = 'en'
requires_version = (0, 7, 3)
description = 'Daily news from the New York Times (subscription version)'
allSectionKeywords = ['The Front Page', 'International','National','Obituaries','Editorials',
@ -103,6 +104,7 @@ class NYTimes(BasicNewsRecipe):
]),
dict(name=['script', 'noscript', 'style'])]
masthead_url = 'http://graphics8.nytimes.com/images/misc/nytlogo379x64.gif'
cover_margins = (18,18,'grey99')
no_stylesheets = True
extra_css = '.headline {text-align: left;}\n \
.byline {font-family: monospace; \
@ -158,7 +160,7 @@ class NYTimes(BasicNewsRecipe):
return cover
def get_masthead_title(self):
return 'NYTimes GR Version'
return self.title
def dump_ans(self, ans):
total_article_count = 0
@ -279,15 +281,17 @@ class NYTimes(BasicNewsRecipe):
self.dump_ans(ans)
return ans
def preprocess_html(self, soup):
def skip_ad_pages(self, soup):
# Skip ad pages served before actual article
skip_tag = soup.find(True, {'name':'skip'})
if skip_tag is not None:
self.log.error("Found forwarding link: %s" % skip_tag.parent['href'])
self.log.warn("Found forwarding link: %s" % skip_tag.parent['href'])
url = 'http://www.nytimes.com' + re.sub(r'\?.*', '', skip_tag.parent['href'])
url += '?pagewanted=all'
self.log.error("Skipping ad to article at '%s'" % url)
soup = self.index_to_soup(url)
self.log.warn("Skipping ad to article at '%s'" % url)
return self.index_to_soup(url, raw=True)
def preprocess_html(self, soup):
return self.strip_anchors(soup)
def postprocess_html(self,soup, True):

View File

@ -1,41 +1,43 @@
"""
publico.py - v1.0
#!/usr/bin/env python
__author__ = u'Jordi Balcells'
__license__ = 'GPL v3'
description = u'Jornal portugu\xeas - v1.03 (16 June 2010)'
__docformat__ = 'restructuredtext en'
Copyright (c) 2009, David Rodrigues - http://sixhat.net
All rights reserved.
"""
__license__ = 'GPL 3'
'''
publico.pt
'''
from calibre.web.feeds.news import BasicNewsRecipe
import re
class Publico(BasicNewsRecipe):
title = u'P\xfablico'
__author__ = 'David Rodrigues'
oldest_article = 1
max_articles_per_feed = 30
encoding='utf-8'
no_stylesheets = True
language = 'pt'
class PublicoPT(BasicNewsRecipe):
description = u'Jornal portugu\xeas'
cover_url = 'http://static.publico.pt/files/header/img/publico.gif'
title = u'Publico.PT'
category = 'News, politics, culture, economy, general interest'
oldest_article = 2
no_stylesheets = True
encoding = 'utf8'
use_embedded_content = False
language = 'pt'
remove_empty_feeds = True
extra_css = ' body{font-family: Arial,Helvetica,sans-serif } img{margin-bottom: 0.4em} '
preprocess_regexps = [(re.compile(u"\uFFFD", re.DOTALL|re.IGNORECASE), lambda match: ''),]
keep_only_tags = [dict(attrs={'class':['content-noticia-title','artigoHeader','ECOSFERA_MANCHETE','noticia','textoPrincipal','ECOSFERA_texto_01']})]
remove_tags = [dict(attrs={'class':['options','subcoluna']})]
feeds = [
(u'Geral', u'http://feeds.feedburner.com/PublicoUltimaHora'),
(u'Internacional', u'http://www.publico.clix.pt/rss.ashx?idCanal=11'),
(u'Pol\xedtica', u'http://www.publico.clix.pt/rss.ashx?idCanal=12'),
(u'Ci\xcencias', u'http://www.publico.clix.pt/rss.ashx?idCanal=13'),
(u'Desporto', u'http://desporto.publico.pt/rss.ashx'),
(u'Economia', u'http://www.publico.clix.pt/rss.ashx?idCanal=57'),
(u'Educa\xe7\xe3o', u'http://www.publico.clix.pt/rss.ashx?idCanal=58'),
(u'Local', u'http://www.publico.clix.pt/rss.ashx?idCanal=59'),
(u'Media e Tecnologia', u'http://www.publico.clix.pt/rss.ashx?idCanal=61'),
(u'Sociedade', u'http://www.publico.clix.pt/rss.ashx?idCanal=62')
]
remove_tags = [dict(name='script'), dict(id='linhaTitulosHeader')]
keep_only_tags = [dict(name='div')]
feeds = [
(u'Geral', u'http://feeds.feedburner.com/publicoRSS'),
(u'Mundo', u'http://feeds.feedburner.com/PublicoMundo'),
(u'Pol\xedtica', u'http://feeds.feedburner.com/PublicoPolitica'),
(u'Economia', u'http://feeds.feedburner.com/PublicoEconomia'),
(u'Desporto', u'http://feeds.feedburner.com/PublicoDesporto'),
(u'Sociedade', u'http://feeds.feedburner.com/PublicoSociedade'),
(u'Educa\xe7\xe3o', u'http://feeds.feedburner.com/PublicoEducacao'),
(u'Ci\xeancias', u'http://feeds.feedburner.com/PublicoCiencias'),
(u'Ecosfera', u'http://feeds.feedburner.com/PublicoEcosfera'),
(u'Cultura', u'http://feeds.feedburner.com/PublicoCultura'),
(u'Local', u'http://feeds.feedburner.com/PublicoLocal'),
(u'Tecnologia', u'http://feeds.feedburner.com/PublicoTecnologia')
]
def print_version(self,url):
s=re.findall("id=[0-9]+",url);
return "http://ww2.publico.clix.pt/print.aspx?"+s[0]

View File

@ -10,8 +10,10 @@ from calibre.web.feeds.news import BasicNewsRecipe
class Slashdot(BasicNewsRecipe):
title = u'Slashdot.org'
description = '''Tech news. WARNING: This recipe downloads a lot
of content and can result in your IP being banned from slashdot.org'''
of content and may result in your IP being banned from slashdot.org'''
oldest_article = 7
simultaneous_downloads = 1
delay = 3
max_articles_per_feed = 100
language = 'en'

View File

@ -0,0 +1,58 @@
from calibre.web.feeds.news import BasicNewsRecipe
class AdvancedUserRecipe1271637235(BasicNewsRecipe):
title = u'Thairath'
__author__ = 'Anat R.'
language = 'th'
oldest_article = 7
max_articles_per_feed = 100
no_stylesheets = True
remove_javascript = True
use_embedded_content = False
feeds = [(u'News',
u'http://www.thairath.co.th/rss/news.xml'), (u'Politics',
u'http://www.thairath.co.th/rss/pol.xml'), (u'Economy',
u'http://www.thairath.co.th/rss/eco.xml'), (u'International',
u'http://www.thairath.co.th/rss/oversea.xml'), (u'Sports',
u'http://www.thairath.co.th/rss/sport.xml'), (u'Life',
u'http://www.thairath.co.th/rss/life.xml'), (u'Education',
u'http://www.thairath.co.th/rss/edu.xml'), (u'Tech',
u'http://www.thairath.co..th/rss/tech.xml'), (u'Entertainment',
u'http://www.thairath.co.th/rss/ent.xml')]
keep_only_tags = []
keep_only_tags.append(dict(name = 'h1', attrs = {'id' : 'title'}))
keep_only_tags.append(dict(name = 'ul', attrs = {'class' :
'detail-info'}))
keep_only_tags.append(dict(name = 'img', attrs = {'class' :
'detail-image'}))
keep_only_tags.append(dict(name = 'div', attrs = {'class' :
'entry'}))
remove_tags = []
remove_tags.append(dict(name = 'div', attrs = {'id':
'menu-holder'}))
remove_tags.append(dict(name = 'div', attrs = {'class':
'addthis_toolbox addthis_default_style'}))
remove_tags.append(dict(name = 'div', attrs = {'class': 'box top-item'}))
remove_tags.append(dict(name = 'div', attrs = {'class': 'column-200 column-margin-430'}))
remove_tags.append(dict(name = 'div', attrs = {'id':
'detail-related'}))
remove_tags.append(dict(name = 'div', attrs = {'id': 'related'}))
remove_tags.append(dict(name = 'id', attrs = {'class': 'footer'}))
remove_tags.append(dict(name = "ul",attrs =
{'id':'banner-highlights-images'}))

View File

@ -0,0 +1,44 @@
from calibre.web.feeds.news import BasicNewsRecipe
class AdvancedUserRecipe1271596863(BasicNewsRecipe):
title = u'The Nation'
__author__ = 'Anat R.'
language = 'en_TH'
oldest_article = 7
max_articles_per_feed = 100
no_stylesheets = True
remove_javascript = True
use_embedded_content = False
feeds = [(u'Topstory',
u'http://www.nationmultimedia.com/home/rss/topstories.rss'),
(u'National', u'http://www.nationmultimedia.com/home/rss/national.rss'),
(u'Politics',
u'http://www.nationmultimedia.com/home/rss/politics.rss'), (u'Business',
u'http://www.nationmultimedia.com/home/rss/business.rss'),
(u'Regional', u'http://www.nationmultimedia.com/home/rss/regional.rss'),
(u'Sports', u'http://www.nationmultimedia.com/home/rss/sport.rss'),
(u'Travel', u'http://www.nationmultimedia.com/home/rss/travel.rss'),
(u'Life', u'http://www.nationmultimedia.com/home/rss/life.rss')]
keep_only_tags = []
keep_only_tags.append(dict(name = 'div', attrs = {'class' :
'pd10'}))
remove_tags = []
remove_tags.append(dict(name = 'div', attrs = {'class':
'WrapperHeaderCol2-2'}))
remove_tags.append(dict(name = 'div', attrs = {'class':
'LayoutMenu2'}))
remove_tags.append(dict(name = 'div', attrs = {'class':
'TextHeaderRight'}))
remove_tags.append(dict(name = "ul",attrs = {'id':'toolZoom'}))

View File

@ -3,126 +3,141 @@ __license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
__docformat__ = 'restructuredtext en'
from calibre.web.feeds.news import BasicNewsRecipe
from calibre import strftime
# http://online.wsj.com/page/us_in_todays_paper.html
class WallStreetJournal(BasicNewsRecipe):
title = 'The Wall Street Journal (US)'
__author__ = 'Kovid Goyal and Sujata Raman'
description = 'News and current affairs'
needs_subscription = True
language = 'en'
title = 'The Wall Street Journal (US)'
__author__ = 'Kovid Goyal and Sujata Raman'
description = 'News and current affairs'
needs_subscription = True
language = 'en'
max_articles_per_feed = 1000
timefmt = ' [%a, %b %d, %Y]'
no_stylesheets = True
max_articles_per_feed = 1000
timefmt = ' [%a, %b %d, %Y]'
no_stylesheets = True
extra_css = '''h1{color:#093D72 ; font-size:large ; font-family:Georgia,"Century Schoolbook","Times New Roman",Times,serif; }
h2{color:#474537; font-family:Georgia,"Century Schoolbook","Times New Roman",Times,serif; font-size:small; font-style:italic;}
.subhead{color:gray; font-family:Georgia,"Century Schoolbook","Times New Roman",Times,serif; font-size:small; font-style:italic;}
.insettipUnit {color:#666666; font-family:Arial,Sans-serif;font-size:xx-small }
.targetCaption{ font-size:x-small; color:#333333; font-family:Arial,Helvetica,sans-serif}
.article{font-family :Arial,Helvetica,sans-serif; font-size:x-small}
.tagline {color:#333333; font-size:xx-small}
.dateStamp {color:#666666; font-family:Arial,Helvetica,sans-serif}
h3{color:blue ;font-family:Arial,Helvetica,sans-serif; font-size:xx-small}
.byline{color:blue;font-family:Arial,Helvetica,sans-serif; font-size:xx-small}
h6{color:#333333; font-family:Georgia,"Century Schoolbook","Times New Roman",Times,serif; font-size:small;font-style:italic; }
.paperLocation{color:#666666; font-size:xx-small}'''
extra_css = '''h1{color:#093D72 ; font-size:large ; font-family:Georgia,"Century Schoolbook","Times New Roman",Times,serif; }
h2{color:#474537; font-family:Georgia,"Century Schoolbook","Times New Roman",Times,serif; font-size:small; font-style:italic;}
.subhead{color:gray; font-family:Georgia,"Century Schoolbook","Times New Roman",Times,serif; font-size:small; font-style:italic;}
.insettipUnit {color:#666666; font-family:Arial,Sans-serif;font-size:xx-small }
.targetCaption{ font-size:x-small; color:#333333; font-family:Arial,Helvetica,sans-serif}
.article{font-family :Arial,Helvetica,sans-serif; font-size:x-small}
.tagline {color:#333333; font-size:xx-small}
.dateStamp {color:#666666; font-family:Arial,Helvetica,sans-serif}
h3{color:blue ;font-family:Arial,Helvetica,sans-serif; font-size:xx-small}
.byline{color:blue;font-family:Arial,Helvetica,sans-serif; font-size:xx-small}
h6{color:#333333; font-family:Georgia,"Century Schoolbook","Times New Roman",Times,serif; font-size:small;font-style:italic; }
.paperLocation{color:#666666; font-size:xx-small}'''
remove_tags_before = dict(name='h1')
remove_tags = [
dict(id=["articleTabs_tab_article", "articleTabs_tab_comments", "articleTabs_tab_interactive","articleTabs_tab_video","articleTabs_tab_map","articleTabs_tab_slideshow"]),
{'class':['footer_columns','network','insetCol3wide','interactive','video','slideshow','map','insettip','insetClose','more_in', "insetContent", 'articleTools_bottom', 'aTools', "tooltip", "adSummary", "nav-inline"]},
dict(rel='shortcut icon'),
]
remove_tags_after = [dict(id="article_story_body"), {'class':"article story"},]
remove_tags_before = dict(name='h1')
remove_tags = [
dict(id=["articleTabs_tab_article", "articleTabs_tab_comments", "articleTabs_tab_interactive","articleTabs_tab_video","articleTabs_tab_map","articleTabs_tab_slideshow"]),
{'class':['footer_columns','network','insetCol3wide','interactive','video','slideshow','map','insettip','insetClose','more_in', "insetContent", 'articleTools_bottom', 'aTools', "tooltip", "adSummary", "nav-inline"]},
dict(rel='shortcut icon'),
]
remove_tags_after = [dict(id="article_story_body"), {'class':"article story"},]
def get_browser(self):
br = BasicNewsRecipe.get_browser()
if self.username is not None and self.password is not None:
br.open('http://commerce.wsj.com/auth/login')
br.select_form(nr=0)
br['user'] = self.username
br['password'] = self.password
res = br.submit()
raw = res.read()
if 'Welcome,' not in raw:
raise ValueError('Failed to log in to wsj.com, check your '
'username and password')
return br
def get_browser(self):
br = BasicNewsRecipe.get_browser()
if self.username is not None and self.password is not None:
br.open('http://commerce.wsj.com/auth/login')
br.select_form(nr=0)
br['user'] = self.username
br['password'] = self.password
res = br.submit()
raw = res.read()
if 'Welcome,' not in raw:
raise ValueError('Failed to log in to wsj.com, check your '
'username and password')
return br
def postprocess_html(self, soup, first):
for tag in soup.findAll(name=['table', 'tr', 'td']):
tag.name = 'div'
def postprocess_html(self, soup, first):
for tag in soup.findAll(name=['table', 'tr', 'td']):
tag.name = 'div'
for tag in soup.findAll('div', dict(id=["articleThumbnail_1", "articleThumbnail_2", "articleThumbnail_3", "articleThumbnail_4", "articleThumbnail_5", "articleThumbnail_6", "articleThumbnail_7"])):
tag.extract()
for tag in soup.findAll('div', dict(id=["articleThumbnail_1", "articleThumbnail_2", "articleThumbnail_3", "articleThumbnail_4", "articleThumbnail_5", "articleThumbnail_6", "articleThumbnail_7"])):
tag.extract()
return soup
return soup
def wsj_get_index(self):
return self.index_to_soup('http://online.wsj.com/page/us_in_todays_paper.html')
def wsj_get_index(self):
return self.index_to_soup('http://online.wsj.com/itp')
def parse_index(self):
soup = self.wsj_get_index()
def parse_index(self):
soup = self.wsj_get_index()
year = strftime('%Y')
for x in soup.findAll('td', height='25', attrs={'class':'b14'}):
txt = self.tag_to_string(x).strip()
txt = txt.replace(u'\xa0', ' ')
txt = txt.encode('ascii', 'ignore')
if year in txt:
self.timefmt = ' [%s]'%txt
break
date = soup.find('span', attrs={'class':'date-date'})
if date is not None:
self.timefmt = ' [%s]'%self.tag_to_string(date)
left_column = soup.find(
text=lambda t: 'begin ITP Left Column' in str(t))
cov = soup.find('a', attrs={'class':'icon pdf'}, href=True)
if cov is not None:
self.cover_url = cov['href']
table = left_column.findNext('table')
feeds = []
div = soup.find('div', attrs={'class':'itpHeader'})
div = div.find('ul', attrs={'class':'tab'})
for a in div.findAll('a', href=lambda x: x and '/itp/' in x):
title = self.tag_to_string(a)
url = 'http://online.wsj.com' + a['href']
self.log('Found section:', title)
articles = self.wsj_find_articles(url)
if articles:
feeds.append((title, articles))
current_section = None
current_articles = []
feeds = []
for x in table.findAllNext(True):
if x.name == 'td' and x.get('class', None) == 'b13':
if current_articles and current_section:
feeds.append((current_section, current_articles))
current_section = self.tag_to_string(x.a).strip()
current_articles = []
self.log('\tProcessing section:', current_section)
if current_section is not None and x.name == 'a' and \
x.get('class', None) == 'bold80':
title = self.tag_to_string(x)
url = x.get('href', False)
if not url or not title:
continue
url = url.partition('#')[0]
desc = ''
d = x.findNextSibling(True)
if d is not None and d.get('class', None) == 'arialResize':
desc = self.tag_to_string(d)
desc = desc.partition(u'\u2022')[0]
self.log('\t\tFound article:', title)
self.log('\t\t\t', url)
if url.startswith('/'):
url = 'http://online.wsj.com'+url
if desc:
self.log('\t\t\t', desc)
current_articles.append({'title': title, 'url':url,
'description':desc, 'date':''})
return feeds
if current_articles and current_section:
feeds.append((current_section, current_articles))
def wsj_find_articles(self, url):
soup = self.index_to_soup(url)
return feeds
whats_news = soup.find('div', attrs={'class':lambda x: x and
'whatsNews-simple' in x})
if whats_news is not None:
whats_news.extract()
def cleanup(self):
self.browser.open('http://online.wsj.com/logout?url=http://online.wsj.com')
articles = []
for a in soup.findAll('a', attrs={'class':'mjLinkItem'}, href=True):
container = a.findParent(['li', 'div'])
meta = a.find(attrs={'class':'meta_sectionName'})
if meta is not None:
meta.extract()
title = self.tag_to_string(a).strip() + ' [%s]'%self.tag_to_string(meta)
url = 'http://online.wsj.com'+a['href']
desc = ''
p = container.find('p')
if p is not None:
desc = self.tag_to_string(p)
articles.append({'title':title, 'url':url,
'description':desc, 'date':''})
self.log('\tFound article:', title)
'''
# Find related articles
a.extract()
for a in container.findAll('a', href=lambda x: x and '/article/'
in x and 'articleTabs' not in x):
url = a['href']
if not url.startswith('http:'):
url = 'http://online.wsj.com'+url
title = self.tag_to_string(a).strip()
if not title or title.startswith('['): continue
if title:
articles.append({'title':self.tag_to_string(a),
'url':url, 'description':'', 'date':''})
self.log('\t\tFound related:', title)
'''
return articles
def cleanup(self):
self.browser.open('http://online.wsj.com/logout?url=http://online.wsj.com')

BIN
resources/tracer.epub Normal file

Binary file not shown.

View File

@ -2,7 +2,7 @@ __license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
__docformat__ = 'restructuredtext en'
__appname__ = 'calibre'
__version__ = '0.7.2'
__version__ = '0.7.3'
__author__ = "Kovid Goyal <kovid@kovidgoyal.net>"
import re

View File

@ -436,7 +436,7 @@ from calibre.devices.blackberry.driver import BLACKBERRY
from calibre.devices.cybook.driver import CYBOOK
from calibre.devices.eb600.driver import EB600, COOL_ER, SHINEBOOK, \
POCKETBOOK360, GER2, ITALICA, ECLICTO, DBOOK, INVESBOOK, \
BOOQ, ELONEX
BOOQ, ELONEX, POCKETBOOK301
from calibre.devices.iliad.driver import ILIAD
from calibre.devices.irexdr.driver import IREXDR1000, IREXDR800
from calibre.devices.jetbook.driver import JETBOOK
@ -457,9 +457,12 @@ from calibre.devices.misc import PALMPRE, AVANT
from calibre.devices.folder_device.driver import FOLDER_DEVICE_FOR_CONFIG
from calibre.devices.kobo.driver import KOBO
from calibre.ebooks.metadata.fetch import GoogleBooks, ISBNDB, Amazon
from calibre.ebooks.metadata.fetch import GoogleBooks, ISBNDB, Amazon, \
LibraryThing
from calibre.ebooks.metadata.douban import DoubanBooks
from calibre.library.catalog import CSV_XML, EPUB_MOBI
plugins = [HTML2ZIP, PML2PMLZ, ArchiveExtract, GoogleBooks, ISBNDB, Amazon, CSV_XML, EPUB_MOBI]
plugins = [HTML2ZIP, PML2PMLZ, ArchiveExtract, GoogleBooks, ISBNDB, Amazon,
LibraryThing, DoubanBooks, CSV_XML, EPUB_MOBI]
plugins += [
ComicInput,
EPUBInput,
@ -507,6 +510,7 @@ plugins += [
JETBOOK,
SHINEBOOK,
POCKETBOOK360,
POCKETBOOK301,
KINDLE,
KINDLE2,
KINDLE_DX,

View File

@ -279,6 +279,7 @@ class KoboReaderOutput(OutputProfile):
description = _('This profile is intended for the Kobo Reader.')
screen_size = (590, 775)
comic_screen_size = (540, 718)
dpi = 168.451
fbase = 12
fsizes = [7.5, 9, 10, 12, 15.5, 20, 22, 24]

View File

@ -21,7 +21,7 @@ from calibre.utils.config import make_config_dir, Config, ConfigProxy, \
platform = 'linux'
if iswindows:
platform = 'windows'
if isosx:
elif isosx:
platform = 'osx'
from zipfile import ZipFile
@ -32,19 +32,25 @@ def _config():
c.add_opt('filetype_mapping', default={}, help=_('Mapping for filetype plugins'))
c.add_opt('plugin_customization', default={}, help=_('Local plugin customization'))
c.add_opt('disabled_plugins', default=set([]), help=_('Disabled plugins'))
c.add_opt('enabled_plugins', default=set([]), help=_('Enabled plugins'))
return ConfigProxy(c)
config = _config()
class InvalidPlugin(ValueError):
pass
class PluginNotFound(ValueError):
pass
def load_plugin(path_to_zip_file):
def find_plugin(name):
for plugin in _initialized_plugins:
if plugin.name == name:
return plugin
def load_plugin(path_to_zip_file): # {{{
'''
Load plugin from zip file or raise InvalidPlugin error
@ -76,11 +82,120 @@ def load_plugin(path_to_zip_file):
raise InvalidPlugin(_('No valid plugin found in ')+path_to_zip_file)
_initialized_plugins = []
# }}}
# Enable/disable plugins {{{
def disable_plugin(plugin_or_name):
x = getattr(plugin_or_name, 'name', plugin_or_name)
plugin = find_plugin(x)
if not plugin.can_be_disabled:
raise ValueError('Plugin %s cannot be disabled'%x)
dp = config['disabled_plugins']
dp.add(x)
config['disabled_plugins'] = dp
ep = config['enabled_plugins']
if x in ep:
ep.remove(x)
config['enabled_plugins'] = ep
def enable_plugin(plugin_or_name):
x = getattr(plugin_or_name, 'name', plugin_or_name)
dp = config['disabled_plugins']
if x in dp:
dp.remove(x)
config['disabled_plugins'] = dp
ep = config['enabled_plugins']
ep.add(x)
config['enabled_plugins'] = ep
default_disabled_plugins = set([
'Douban Books',
])
def is_disabled(plugin):
if plugin.name in config['enabled_plugins']: return False
return plugin.name in config['disabled_plugins'] or \
plugin.name in default_disabled_plugins
# }}}
# File type plugins {{{
_on_import = {}
_on_preprocess = {}
_on_postprocess = {}
def reread_filetype_plugins():
global _on_import
global _on_preprocess
global _on_postprocess
_on_import = {}
_on_preprocess = {}
_on_postprocess = {}
for plugin in _initialized_plugins:
if isinstance(plugin, FileTypePlugin):
for ft in plugin.file_types:
if plugin.on_import:
if not _on_import.has_key(ft):
_on_import[ft] = []
_on_import[ft].append(plugin)
if plugin.on_preprocess:
if not _on_preprocess.has_key(ft):
_on_preprocess[ft] = []
_on_preprocess[ft].append(plugin)
if plugin.on_postprocess:
if not _on_postprocess.has_key(ft):
_on_postprocess[ft] = []
_on_postprocess[ft].append(plugin)
def _run_filetype_plugins(path_to_file, ft=None, occasion='preprocess'):
occasion = {'import':_on_import, 'preprocess':_on_preprocess,
'postprocess':_on_postprocess}[occasion]
customization = config['plugin_customization']
if ft is None:
ft = os.path.splitext(path_to_file)[-1].lower().replace('.', '')
nfp = path_to_file
for plugin in occasion.get(ft, []):
if is_disabled(plugin):
continue
plugin.site_customization = customization.get(plugin.name, '')
with plugin:
try:
nfp = plugin.run(path_to_file)
if not nfp:
nfp = path_to_file
except:
print 'Running file type plugin %s failed with traceback:'%plugin.name
traceback.print_exc()
x = lambda j : os.path.normpath(os.path.normcase(j))
if occasion == 'postprocess' and x(nfp) != x(path_to_file):
shutil.copyfile(nfp, path_to_file)
nfp = path_to_file
return nfp
run_plugins_on_import = functools.partial(_run_filetype_plugins,
occasion='import')
run_plugins_on_preprocess = functools.partial(_run_filetype_plugins,
occasion='preprocess')
run_plugins_on_postprocess = functools.partial(_run_filetype_plugins,
occasion='postprocess')
# }}}
# Plugin customization {{{
def customize_plugin(plugin, custom):
d = config['plugin_customization']
d[plugin.name] = custom.strip()
config['plugin_customization'] = d
def plugin_customization(plugin):
return config['plugin_customization'].get(plugin.name, '')
# }}}
# Input/Output profiles {{{
def input_profiles():
for plugin in _initialized_plugins:
if isinstance(plugin, InputProfile):
@ -90,7 +205,9 @@ def output_profiles():
for plugin in _initialized_plugins:
if isinstance(plugin, OutputProfile):
yield plugin
# }}}
# Metadata sources {{{
def metadata_sources(metadata_type='basic', customize=True, isbndb_key=None):
for plugin in _initialized_plugins:
if isinstance(plugin, MetadataSource) and \
@ -117,31 +234,9 @@ def migrate_isbndb_key():
if key:
prefs.set('isbndb_com_key', '')
set_isbndb_key(key)
# }}}
def reread_filetype_plugins():
global _on_import
global _on_preprocess
global _on_postprocess
_on_import = {}
_on_preprocess = {}
_on_postprocess = {}
for plugin in _initialized_plugins:
if isinstance(plugin, FileTypePlugin):
for ft in plugin.file_types:
if plugin.on_import:
if not _on_import.has_key(ft):
_on_import[ft] = []
_on_import[ft].append(plugin)
if plugin.on_preprocess:
if not _on_preprocess.has_key(ft):
_on_preprocess[ft] = []
_on_preprocess[ft].append(plugin)
if plugin.on_postprocess:
if not _on_postprocess.has_key(ft):
_on_postprocess[ft] = []
_on_postprocess[ft].append(plugin)
# Metadata read/write {{{
_metadata_readers = {}
_metadata_writers = {}
def reread_metadata_plugins():
@ -233,51 +328,9 @@ def set_file_type_metadata(stream, mi, ftype):
print 'Failed to set metadata for', repr(getattr(mi, 'title', ''))
traceback.print_exc()
# }}}
def _run_filetype_plugins(path_to_file, ft=None, occasion='preprocess'):
occasion = {'import':_on_import, 'preprocess':_on_preprocess,
'postprocess':_on_postprocess}[occasion]
customization = config['plugin_customization']
if ft is None:
ft = os.path.splitext(path_to_file)[-1].lower().replace('.', '')
nfp = path_to_file
for plugin in occasion.get(ft, []):
if is_disabled(plugin):
continue
plugin.site_customization = customization.get(plugin.name, '')
with plugin:
try:
nfp = plugin.run(path_to_file)
if not nfp:
nfp = path_to_file
except:
print 'Running file type plugin %s failed with traceback:'%plugin.name
traceback.print_exc()
x = lambda j : os.path.normpath(os.path.normcase(j))
if occasion == 'postprocess' and x(nfp) != x(path_to_file):
shutil.copyfile(nfp, path_to_file)
nfp = path_to_file
return nfp
run_plugins_on_import = functools.partial(_run_filetype_plugins,
occasion='import')
run_plugins_on_preprocess = functools.partial(_run_filetype_plugins,
occasion='preprocess')
run_plugins_on_postprocess = functools.partial(_run_filetype_plugins,
occasion='postprocess')
def initialize_plugin(plugin, path_to_zip_file):
try:
p = plugin(path_to_zip_file)
p.initialize()
return p
except Exception:
print 'Failed to initialize plugin:', plugin.name, plugin.version
tb = traceback.format_exc()
raise InvalidPlugin((_('Initialization of plugin %s failed with traceback:')
%tb) + '\n'+tb)
# Add/remove plugins {{{
def add_plugin(path_to_zip_file):
make_config_dir()
@ -307,14 +360,9 @@ def remove_plugin(plugin_or_name):
initialize_plugins()
return removed
def is_disabled(plugin):
return plugin.name in config['disabled_plugins']
def find_plugin(name):
for plugin in _initialized_plugins:
if plugin.name == name:
return plugin
# }}}
# Input/Output format plugins {{{
def input_format_plugins():
for plugin in _initialized_plugins:
@ -364,6 +412,9 @@ def available_output_formats():
formats.add(plugin.file_type)
return formats
# }}}
# Catalog plugins {{{
def catalog_plugins():
for plugin in _initialized_plugins:
@ -383,27 +434,32 @@ def plugin_for_catalog_format(fmt):
if fmt.lower() in plugin.file_types:
return plugin
def device_plugins():
# }}}
def device_plugins(): # {{{
for plugin in _initialized_plugins:
if isinstance(plugin, DevicePlugin):
if not is_disabled(plugin):
yield plugin
if platform in plugin.supported_platforms:
yield plugin
# }}}
def disable_plugin(plugin_or_name):
x = getattr(plugin_or_name, 'name', plugin_or_name)
plugin = find_plugin(x)
if not plugin.can_be_disabled:
raise ValueError('Plugin %s cannot be disabled'%x)
dp = config['disabled_plugins']
dp.add(x)
config['disabled_plugins'] = dp
def enable_plugin(plugin_or_name):
x = getattr(plugin_or_name, 'name', plugin_or_name)
dp = config['disabled_plugins']
if x in dp:
dp.remove(x)
config['disabled_plugins'] = dp
# Initialize plugins {{{
_initialized_plugins = []
def initialize_plugin(plugin, path_to_zip_file):
try:
p = plugin(path_to_zip_file)
p.initialize()
return p
except Exception:
print 'Failed to initialize plugin:', plugin.name, plugin.version
tb = traceback.format_exc()
raise InvalidPlugin((_('Initialization of plugin %s failed with traceback:')
%tb) + '\n'+tb)
def initialize_plugins():
global _initialized_plugins
@ -425,10 +481,14 @@ def initialize_plugins():
initialize_plugins()
def intialized_plugins():
def initialized_plugins():
for plugin in _initialized_plugins:
yield plugin
# }}}
# CLI {{{
def option_parser():
parser = OptionParser(usage=_('''\
%prog options
@ -449,17 +509,6 @@ def option_parser():
help=_('Disable the named plugin'))
return parser
def initialized_plugins():
return _initialized_plugins
def customize_plugin(plugin, custom):
d = config['plugin_customization']
d[plugin.name] = custom.strip()
config['plugin_customization'] = d
def plugin_customization(plugin):
return config['plugin_customization'].get(plugin.name, '')
def main(args=sys.argv):
parser = option_parser()
if len(args) < 2:
@ -504,3 +553,5 @@ def main(args=sys.argv):
if __name__ == '__main__':
sys.exit(main())
# }}}

File diff suppressed because it is too large Load Diff

View File

@ -201,4 +201,21 @@ class ELONEX(EB600):
def can_handle(cls, dev, debug=False):
return dev[3] == 'Elonex' and dev[4] == 'eBook'
class POCKETBOOK301(USBMS):
name = 'PocketBook 301 Device Interface'
description = _('Communicate with the PocketBook 301 reader.')
author = 'Kovid Goyal'
supported_platforms = ['windows', 'osx', 'linux']
FORMATS = ['epub', 'fb2', 'prc', 'mobi', 'pdf', 'djvu', 'rtf', 'chm', 'txt']
SUPPORTS_SUB_DIRS = True
MAIN_MEMORY_VOLUME_LABEL = 'PocketBook 301 Main Memory'
STORAGE_CARD_VOLUME_LABEL = 'PocketBook 301 Storage Card'
VENDOR_ID = [0x1]
PRODUCT_ID = [0x301]
BCD = [0x132]

View File

@ -81,9 +81,6 @@ class HANLINV3(USBMS):
return drives
class HANLINV5(HANLINV3):
name = 'Hanlin V5 driver'
gui_name = 'Hanlin V5'
@ -120,8 +117,22 @@ class BOOX(HANLINV3):
MAIN_MEMORY_VOLUME_LABEL = 'BOOX Internal Memory'
STORAGE_CARD_VOLUME_LABEL = 'BOOX Storage Card'
EBOOK_DIR_MAIN = 'MyBooks'
EBOOK_DIR_CARD_A = 'MyBooks'
EBOOK_DIR_MAIN = ['MyBooks']
EXTRA_CUSTOMIZATION_MESSAGE = _('Comma separated list of directories to '
'send e-books to on the device. The first one that exists will '
'be used.')
EXTRA_CUSTOMIZATION_DEFAULT = ', '.join(EBOOK_DIR_MAIN)
# EBOOK_DIR_CARD_A = 'MyBooks' ## Am quite sure we need this.
def post_open_callback(self):
opts = self.settings()
dirs = opts.extra_customization
if not dirs:
dirs = self.EBOOK_DIR_MAIN
else:
dirs = [x.strip() for x in dirs.split(',')]
self.EBOOK_DIR_MAIN = dirs
def windows_sort_drives(self, drives):
return drives

View File

@ -55,6 +55,7 @@ class PRS505(USBMS):
SUPPORTS_SUB_DIRS = True
MUST_READ_METADATA = True
SUPPORTS_USE_AUTHOR_SORT = True
EBOOK_DIR_MAIN = 'database/media/books'
EXTRA_CUSTOMIZATION_MESSAGE = _('Comma separated list of metadata fields '
@ -125,7 +126,7 @@ class PRS505(USBMS):
d = os.path.dirname(paths[source_id])
if not os.path.exists(d):
os.makedirs(d)
return XMLCache(paths, prefixes)
return XMLCache(paths, prefixes, self.settings().use_author_sort)
def books(self, oncard=None, end_session=True):
debug_print('PRS505: starting fetching books for card', oncard)

View File

@ -60,12 +60,13 @@ def uuid():
class XMLCache(object):
def __init__(self, paths, prefixes):
def __init__(self, paths, prefixes, use_author_sort):
if DEBUG:
debug_print('Building XMLCache...')
pprint(paths)
self.paths = paths
self.prefixes = prefixes
self.use_author_sort = use_author_sort
# Parse XML files {{{
parser = etree.XMLParser(recover=True)
@ -434,7 +435,10 @@ class XMLCache(object):
if not ts:
ts = title_sort(title)
record.set('titleSorter', ts)
record.set('author', authors_to_string(book.authors))
if self.use_author_sort and book.author_sort is not None:
record.set('author', book.author_sort)
else:
record.set('author', authors_to_string(book.authors))
ext = os.path.splitext(path)[1]
if ext:
ext = ext[1:].lower()

View File

@ -80,6 +80,7 @@ class Device(DeviceConfig, DevicePlugin):
SUPPORTS_SUB_DIRS = False
MUST_READ_METADATA = False
SUPPORTS_USE_AUTHOR_SORT = False
EBOOK_DIR_MAIN = ''
EBOOK_DIR_CARD_A = ''

View File

@ -32,6 +32,8 @@ class DeviceConfig(object):
help=_('Place files in sub directories if the device supports them'))
c.add_opt('read_metadata', default=True,
help=_('Read metadata from files on device'))
c.add_opt('use_author_sort', default=False,
help=_('Use author sort instead of author'))
c.add_opt('save_template', default=cls._default_save_template(),
help=_('Template to control how books are saved'))
c.add_opt('extra_customization',
@ -47,7 +49,8 @@ class DeviceConfig(object):
def config_widget(cls):
from calibre.gui2.device_drivers.configwidget import ConfigWidget
cw = ConfigWidget(cls.settings(), cls.FORMATS, cls.SUPPORTS_SUB_DIRS,
cls.MUST_READ_METADATA, cls.EXTRA_CUSTOMIZATION_MESSAGE)
cls.MUST_READ_METADATA, cls.SUPPORTS_USE_AUTHOR_SORT,
cls.EXTRA_CUSTOMIZATION_MESSAGE)
return cw
@classmethod
@ -58,6 +61,8 @@ class DeviceConfig(object):
proxy['use_subdirs'] = config_widget.use_subdirs()
if not cls.MUST_READ_METADATA:
proxy['read_metadata'] = config_widget.read_metadata()
if cls.SUPPORTS_USE_AUTHOR_SORT:
proxy['use_author_sort'] = config_widget.use_author_sort()
if cls.EXTRA_CUSTOMIZATION_MESSAGE:
ec = unicode(config_widget.opt_extra_customization.text()).strip()
if not ec:

View File

@ -299,7 +299,7 @@ class USBMS(CLI, Device):
def replfunc(match):
if match.group(1) in ['title', 'series', 'series_index', 'isbn']:
return '(?P<' + match.group(1) + '>.+?)'
elif match.group(1) == 'authors':
elif match.group(1) in ['authors', 'author_sort']:
return '(?P<author>.+?)'
else:
return '(.+?)'

View File

@ -8,7 +8,7 @@ import os, re
from mimetypes import guess_type as guess_mimetype
from calibre.ebooks.BeautifulSoup import BeautifulSoup, NavigableString
from calibre.constants import iswindows
from calibre.utils.chm.chm import CHMFile
from calibre.utils.chm.chmlib import (
CHM_RESOLVE_SUCCESS, CHM_ENUMERATE_NORMAL,
@ -135,10 +135,16 @@ class CHMReader(CHMFile):
if lpath.find(';') != -1:
# fix file names with ";<junk>" at the end, see _reformat()
lpath = lpath.split(';')[0]
with open(lpath, 'wb') as f:
if guess_mimetype(path)[0] == ('text/html'):
data = self._reformat(data)
f.write(data)
try:
with open(lpath, 'wb') as f:
if guess_mimetype(path)[0] == ('text/html'):
data = self._reformat(data)
f.write(data)
except:
if iswindows and len(lpath) > 250:
self.log.warn('%r filename too long, skipping'%path)
continue
raise
self._extracted = True
files = os.listdir(output_dir)
if self.hhc_path not in files:

View File

@ -385,14 +385,6 @@ class EPUBOutput(OutputFormatPlugin):
if val and not pval:
rule.style.setProperty('padding-left', val)
if stylesheet is not None:
stylesheet.data.add('a { color: inherit; text-decoration: inherit; '
'cursor: default; }')
stylesheet.data.add('a[href] { color: blue; '
'text-decoration: underline; cursor:pointer; }')
else:
self.oeb.log.warn('No stylesheet found')
# }}}
def workaround_sony_quirks(self): # {{{

View File

@ -28,10 +28,14 @@ def authors_to_string(authors):
else:
return ''
_bracket_pat = re.compile(r'[\[({].*?[})\]]')
def author_to_author_sort(author):
if not author:
return ''
method = tweaks['author_sort_copy_method']
if method == 'copy' or (method == 'comma' and ',' in author):
return author
author = _bracket_pat.sub('', author).strip()
tokens = author.split()
tokens = tokens[-1:] + tokens[:-1]
if len(tokens) > 1:
@ -223,6 +227,7 @@ class MetaInformation(object):
'isbn', 'tags', 'cover_data', 'application_id', 'guide',
'manifest', 'spine', 'toc', 'cover', 'language',
'book_producer', 'timestamp', 'lccn', 'lcc', 'ddc',
'author_sort_map',
'pubdate', 'rights', 'publication_type', 'uuid'):
if hasattr(mi, attr):
setattr(ans, attr, getattr(mi, attr))
@ -244,6 +249,7 @@ class MetaInformation(object):
self.tags = getattr(mi, 'tags', [])
#: mi.cover_data = (ext, data)
self.cover_data = getattr(mi, 'cover_data', (None, None))
self.author_sort_map = getattr(mi, 'author_sort_map', {})
for x in ('author_sort', 'title_sort', 'comments', 'category', 'publisher',
'series', 'series_index', 'rating', 'isbn', 'language',
@ -254,11 +260,11 @@ class MetaInformation(object):
setattr(self, x, getattr(mi, x, None))
def print_all_attributes(self):
for x in ('author', 'author_sort', 'title_sort', 'comments', 'category', 'publisher',
for x in ('title','author', 'author_sort', 'title_sort', 'comments', 'category', 'publisher',
'series', 'series_index', 'tags', 'rating', 'isbn', 'language',
'application_id', 'manifest', 'toc', 'spine', 'guide', 'cover',
'book_producer', 'timestamp', 'lccn', 'lcc', 'ddc', 'pubdate',
'rights', 'publication_type', 'uuid'
'rights', 'publication_type', 'uuid', 'author_sort_map'
):
prints(x, getattr(self, x, 'None'))
@ -288,6 +294,9 @@ class MetaInformation(object):
self.tags += mi.tags
self.tags = list(set(self.tags))
if mi.author_sort_map:
self.author_sort_map.update(mi.author_sort_map)
if getattr(mi, 'cover_data', False):
other_cover = mi.cover_data[-1]
self_cover = self.cover_data[-1] if self.cover_data else ''

View File

@ -35,6 +35,8 @@ PUBLICATION_METADATA_FIELDS = frozenset([
'title_sort',
# Ordered list of authors. Must never be None, can be [_('Unknown')]
'authors',
# Map of sort strings for each author
'author_sort_map',
# Pseudo field that can be set, but if not set is auto generated
# from authors and languages
'author_sort',

View File

@ -16,6 +16,7 @@ NULL_VALUES = {
'classifiers' : {},
'languages' : [],
'device_collections': [],
'author_sort_map': {},
'authors' : [_('Unknown')],
'title' : _('Unknown'),
}

View File

@ -0,0 +1,258 @@
from __future__ import with_statement
__license__ = 'GPL 3'
__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>; 2010, Li Fanxi <lifanxi@freemindworld.com>'
__docformat__ = 'restructuredtext en'
import sys, textwrap
import traceback
from urllib import urlencode
from functools import partial
from lxml import etree
from calibre import browser, preferred_encoding
from calibre.ebooks.metadata import MetaInformation
from calibre.utils.config import OptionParser
from calibre.ebooks.metadata.fetch import MetadataSource
from calibre.utils.date import parse_date, utcnow
DOUBAN_API_KEY = None
NAMESPACES = {
'openSearch':'http://a9.com/-/spec/opensearchrss/1.0/',
'atom' : 'http://www.w3.org/2005/Atom',
'db': 'http://www.douban.com/xmlns/'
}
XPath = partial(etree.XPath, namespaces=NAMESPACES)
total_results = XPath('//openSearch:totalResults')
start_index = XPath('//openSearch:startIndex')
items_per_page = XPath('//openSearch:itemsPerPage')
entry = XPath('//atom:entry')
entry_id = XPath('descendant::atom:id')
title = XPath('descendant::atom:title')
description = XPath('descendant::atom:summary')
publisher = XPath("descendant::db:attribute[@name='publisher']")
isbn = XPath("descendant::db:attribute[@name='isbn13']")
date = XPath("descendant::db:attribute[@name='pubdate']")
creator = XPath("descendant::db:attribute[@name='author']")
tag = XPath("descendant::db:tag")
class DoubanBooks(MetadataSource):
name = 'Douban Books'
description = _('Downloads metadata from Douban.com')
supported_platforms = ['windows', 'osx', 'linux'] # Platforms this plugin will run on
author = 'Li Fanxi <lifanxi@freemindworld.com>' # The author of this plugin
version = (1, 0, 0) # The version number of this plugin
def fetch(self):
try:
self.results = search(self.title, self.book_author, self.publisher,
self.isbn, max_results=10,
verbose=self.verbose)
except Exception, e:
self.exception = e
self.tb = traceback.format_exc()
def report(verbose):
if verbose:
import traceback
traceback.print_exc()
class Query(object):
SEARCH_URL = 'http://api.douban.com/book/subjects?'
ISBN_URL = 'http://api.douban.com/book/subject/isbn/'
type = "search"
def __init__(self, title=None, author=None, publisher=None, isbn=None,
max_results=20, start_index=1):
assert not(title is None and author is None and publisher is None and \
isbn is None)
assert (int(max_results) < 21)
q = ''
if isbn is not None:
q = isbn
self.type = 'isbn'
else:
def build_term(parts):
return ' '.join(x for x in parts)
if title is not None:
q += build_term(title.split())
if author is not None:
q += (' ' if q else '') + build_term(author.split())
if publisher is not None:
q += (' ' if q else '') + build_term(publisher.split())
self.type = 'search'
if isinstance(q, unicode):
q = q.encode('utf-8')
if self.type == "isbn":
self.url = self.ISBN_URL + q
if DOUBAN_API_KEY is not None:
self.url = self.url + "?apikey=" + DOUBAN_API_KEY
else:
self.url = self.SEARCH_URL+urlencode({
'q':q,
'max-results':max_results,
'start-index':start_index,
})
if DOUBAN_API_KEY is not None:
self.url = self.url + "&apikey=" + DOUBAN_API_KEY
def __call__(self, browser, verbose):
if verbose:
print 'Query:', self.url
if self.type == "search":
feed = etree.fromstring(browser.open(self.url).read())
total = int(total_results(feed)[0].text)
start = int(start_index(feed)[0].text)
entries = entry(feed)
new_start = start + len(entries)
if new_start > total:
new_start = 0
return entries, new_start
elif self.type == "isbn":
feed = etree.fromstring(browser.open(self.url).read())
entries = entry(feed)
return entries, 0
class ResultList(list):
def get_description(self, entry, verbose):
try:
desc = description(entry)
if desc:
return 'SUMMARY:\n'+desc[0].text
except:
report(verbose)
def get_title(self, entry):
candidates = [x.text for x in title(entry)]
return ': '.join(candidates)
def get_authors(self, entry):
m = creator(entry)
if not m:
m = []
m = [x.text for x in m]
return m
def get_tags(self, entry, verbose):
try:
btags = [x.attrib["name"] for x in tag(entry)]
tags = []
for t in btags:
tags.extend([y.strip() for y in t.split('/')])
tags = list(sorted(list(set(tags))))
except:
report(verbose)
tags = []
return [x.replace(',', ';') for x in tags]
def get_publisher(self, entry, verbose):
try:
pub = publisher(entry)[0].text
except:
pub = None
return pub
def get_isbn(self, entry, verbose):
try:
isbn13 = isbn(entry)[0].text
except Exception:
isbn13 = None
return isbn13
def get_date(self, entry, verbose):
try:
d = date(entry)
if d:
default = utcnow().replace(day=15)
d = parse_date(d[0].text, assume_utc=True, default=default)
else:
d = None
except:
report(verbose)
d = None
return d
def populate(self, entries, browser, verbose=False):
for x in entries:
try:
id_url = entry_id(x)[0].text
title = self.get_title(x)
except:
report(verbose)
mi = MetaInformation(title, self.get_authors(x))
try:
if DOUBAN_API_KEY is not None:
id_url = id_url + "?apikey=" + DOUBAN_API_KEY
raw = browser.open(id_url).read()
feed = etree.fromstring(raw)
x = entry(feed)[0]
except Exception, e:
if verbose:
print 'Failed to get all details for an entry'
print e
mi.comments = self.get_description(x, verbose)
mi.tags = self.get_tags(x, verbose)
mi.isbn = self.get_isbn(x, verbose)
mi.publisher = self.get_publisher(x, verbose)
mi.pubdate = self.get_date(x, verbose)
self.append(mi)
def search(title=None, author=None, publisher=None, isbn=None,
verbose=False, max_results=40):
br = browser()
start, entries = 1, []
while start > 0 and len(entries) <= max_results:
new, start = Query(title=title, author=author, publisher=publisher,
isbn=isbn, max_results=max_results, start_index=start)(br, verbose)
if not new:
break
entries.extend(new)
entries = entries[:max_results]
ans = ResultList()
ans.populate(entries, br, verbose)
return ans
def option_parser():
parser = OptionParser(textwrap.dedent(
'''\
%prog [options]
Fetch book metadata from Douban. You must specify one of title, author,
publisher or ISBN. If you specify ISBN the others are ignored. Will
fetch a maximum of 100 matches, so you should make your query as
specific as possible.
'''
))
parser.add_option('-t', '--title', help='Book title')
parser.add_option('-a', '--author', help='Book author(s)')
parser.add_option('-p', '--publisher', help='Book publisher')
parser.add_option('-i', '--isbn', help='Book ISBN')
parser.add_option('-m', '--max-results', default=10,
help='Maximum number of results to fetch')
parser.add_option('-v', '--verbose', default=0, action='count',
help='Be more verbose about errors')
return parser
def main(args=sys.argv):
parser = option_parser()
opts, args = parser.parse_args(args)
try:
results = search(opts.title, opts.author, opts.publisher, opts.isbn,
verbose=opts.verbose, max_results=int(opts.max_results))
except AssertionError:
report(True)
parser.print_help()
return 1
for result in results:
print unicode(result).encode(preferred_encoding)
print
if __name__ == '__main__':
sys.exit(main())

View File

@ -182,7 +182,7 @@ def get_metadata(stream, extract_cover=True):
def get_quick_metadata(stream):
return get_metadata(stream, False)
def set_metadata(stream, mi, apply_null=False):
def set_metadata(stream, mi, apply_null=False, update_timestamp=False):
stream.seek(0)
reader = OCFZipReader(stream, root=os.getcwdu())
mi = MetaInformation(mi)
@ -196,6 +196,8 @@ def set_metadata(stream, mi, apply_null=False):
reader.opf.tags = []
if not getattr(mi, 'isbn', None):
reader.opf.isbn = None
if update_timestamp and mi.timestamp is not None:
reader.opf.timestamp = mi.timestamp
newopf = StringIO(reader.opf.render())
safe_replace(stream, reader.container[OPF.MIMETYPE], newopf)

View File

@ -198,6 +198,38 @@ class Amazon(MetadataSource):
self.exception = e
self.tb = traceback.format_exc()
class LibraryThing(MetadataSource):
name = 'LibraryThing'
metadata_type = 'social'
description = _('Downloads series information from librarything.com')
def fetch(self):
if not self.isbn:
return
from calibre import browser
from calibre.ebooks.metadata import MetaInformation
import json
br = browser()
try:
raw = br.open(
'http://status.calibre-ebook.com/library_thing/metadata/'+self.isbn
).read()
data = json.loads(raw)
if not data:
return
if 'error' in data:
raise Exception(data['error'])
if 'series' in data and 'series_index' in data:
mi = MetaInformation(self.title, [])
mi.series = data['series']
mi.series_index = data['series_index']
self.results = mi
except Exception, e:
self.exception = e
self.tb = traceback.format_exc()
def result_index(source, result):
if not result.isbn:
return -1
@ -266,7 +298,7 @@ def get_social_metadata(mi, verbose=0):
with MetadataSources(fetchers) as manager:
manager(mi.title, mi.authors, mi.publisher, mi.isbn, verbose)
manager.join()
ratings, tags, comments = [], set([]), set([])
ratings, tags, comments, series, series_index = [], set([]), set([]), None, None
for fetcher in fetchers:
if fetcher.results:
dmi = fetcher.results
@ -279,6 +311,10 @@ def get_social_metadata(mi, verbose=0):
mi.pubdate = dmi.pubdate
if dmi.comments:
comments.add(dmi.comments)
if dmi.series is not None:
series = dmi.series
if dmi.series_index is not None:
series_index = dmi.series_index
if ratings:
rating = sum(ratings)/float(len(ratings))
if mi.rating is None or mi.rating < 0.1:
@ -295,6 +331,9 @@ def get_social_metadata(mi, verbose=0):
mi.comments = ''
for x in comments:
mi.comments += x+'\n\n'
if series and series_index is not None:
mi.series = series
mi.series_index = series_index
return [(x.name, x.exception, x.tb) for x in fetchers if x.exception is not
None]

View File

@ -736,7 +736,9 @@ class OPF(object):
def fget(self):
ans = []
for tag in self.tags_path(self.metadata):
ans.append(self.get_text(tag))
text = self.get_text(tag)
if text and text.strip():
ans.extend([x.strip() for x in text.split(',')])
return ans
def fset(self, val):

View File

@ -61,6 +61,7 @@ class FormatState(object):
self.italic = False
self.bold = False
self.strikethrough = False
self.underline = False
self.preserve = False
self.family = 'serif'
self.bgcolor = 'transparent'
@ -79,7 +80,8 @@ class FormatState(object):
and self.family == other.family \
and self.bgcolor == other.bgcolor \
and self.fgcolor == other.fgcolor \
and self.strikethrough == other.strikethrough
and self.strikethrough == other.strikethrough \
and self.underline == other.underline
def __ne__(self, other):
return not self.__eq__(other)
@ -251,6 +253,8 @@ class MobiMLizer(object):
color=unicode(istate.fgcolor))
if istate.strikethrough:
inline = etree.SubElement(inline, XHTML('s'))
if istate.underline:
inline = etree.SubElement(inline, XHTML('u'))
bstate.inline = inline
bstate.istate = istate
inline = bstate.inline
@ -330,6 +334,7 @@ class MobiMLizer(object):
istate.bgcolor = style['background-color']
istate.fgcolor = style['color']
istate.strikethrough = style['text-decoration'] == 'line-through'
istate.underline = style['text-decoration'] == 'underline'
if 'monospace' in style['font-family']:
istate.family = 'monospace'
elif 'sans-serif' in style['font-family']:

View File

@ -43,8 +43,8 @@ def _config():
help=_('Notify when a new version is available'))
c.add_opt('use_roman_numerals_for_series_number', default=True,
help=_('Use Roman numerals for series number'))
c.add_opt('sort_by_popularity', default=False,
help=_('Sort tags list by popularity'))
c.add_opt('sort_tags_by', default='name',
help=_('Sort tags list by name, popularity, or rating'))
c.add_opt('cover_flow_queue_length', default=6,
help=_('Number of covers to show in the cover browsing mode'))
c.add_opt('LRF_conversion_defaults', default=[],

View File

@ -28,6 +28,7 @@ from calibre.constants import preferred_encoding, filesystem_encoding, \
from calibre.gui2.dialogs.choose_format import ChooseFormatDialog
from calibre.ebooks import BOOK_EXTENSIONS
from calibre.gui2.dialogs.confirm_delete import confirm
from calibre.gui2.dialogs.delete_matching_from_device import DeleteMatchingFromDeviceDialog
class AnnotationsAction(object): # {{{
@ -410,6 +411,34 @@ class AddAction(object): # {{{
if hasattr(self._adder, 'cleanup'):
self._adder.cleanup()
self._adder = None
def _add_from_device_adder(self, paths=[], names=[], infos=[],
on_card=None, model=None):
self._files_added(paths, names, infos, on_card=on_card)
# set the in-library flags, and as a consequence send the library's
# metadata for this book to the device. This sets the uuid to the
# correct value.
self.set_books_in_library(booklists=[model.db], reset=True)
model.reset()
def add_books_from_device(self, view):
rows = view.selectionModel().selectedRows()
if not rows or len(rows) == 0:
d = error_dialog(self, _('Add to library'), _('No book selected'))
d.exec_()
return
paths = [p for p in view._model.paths(rows) if p is not None]
if not paths or len(paths) == 0:
d = error_dialog(self, _('Add to library'), _('No book files found'))
d.exec_()
return
from calibre.gui2.add import Adder
self.__adder_func = partial(self._add_from_device_adder, on_card=None,
model=view._model)
self._adder = Adder(self, self.library_view.model().db,
Dispatcher(self.__adder_func), spare_server=self.spare_server)
self._adder.add(paths)
# }}}
class DeleteAction(object): # {{{
@ -471,6 +500,45 @@ class DeleteAction(object): # {{{
if ids:
self.tags_view.recount()
def remove_matching_books_from_device(self, *args):
if not self.device_manager.is_device_connected:
d = error_dialog(self, _('Cannot delete books'),
_('No device is connected'))
d.exec_()
return
ids = self._get_selected_ids()
if not ids:
#_get_selected_ids shows a dialog box if nothing is selected, so we
#do not need to show one here
return
to_delete = {}
some_to_delete = False
for model,name in ((self.memory_view.model(), _('Main memory')),
(self.card_a_view.model(), _('Storage Card A')),
(self.card_b_view.model(), _('Storage Card B'))):
to_delete[name] = (model, model.paths_for_db_ids(ids))
if len(to_delete[name][1]) > 0:
some_to_delete = True
if not some_to_delete:
d = error_dialog(self, _('No books to delete'),
_('None of the selected books are on the device'))
d.exec_()
return
d = DeleteMatchingFromDeviceDialog(self, to_delete)
if d.exec_():
paths = {}
ids = {}
for (model, id, path) in d.result:
if model not in paths:
paths[model] = []
ids[model] = []
paths[model].append(path)
ids[model].append(id)
for model in paths:
job = self.remove_paths(paths[model])
self.delete_memory[job] = (paths[model], model)
model.mark_for_deletion(job, ids[model], rows_are_ids=True)
self.status_bar.show_message(_('Deleting books from device.'), 1000)
def delete_covers(self, *args):
ids = self._get_selected_ids()

View File

@ -62,11 +62,13 @@ def render_rows(data):
class CoverView(QWidget): # {{{
def __init__(self, parent=None):
def __init__(self, vertical, parent=None):
QWidget.__init__(self, parent)
self.setMaximumSize(QSize(120, 120))
self.setMinimumSize(QSize(120, 1))
self.setMinimumSize(QSize(120 if vertical else 20, 120 if vertical else
20))
self._current_pixmap_size = self.maximumSize()
self.vertical = vertical
self.animation = QPropertyAnimation(self, 'current_pixmap_size', self)
self.animation.setEasingCurve(QEasingCurve(QEasingCurve.OutExpo))
@ -74,7 +76,8 @@ class CoverView(QWidget): # {{{
self.animation.setStartValue(QSize(0, 0))
self.animation.valueChanged.connect(self.value_changed)
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
self.setSizePolicy(QSizePolicy.Expanding if vertical else
QSizePolicy.Minimum, QSizePolicy.Expanding)
self.default_pixmap = QPixmap(I('book.svg'))
self.pixmap = self.default_pixmap
@ -98,8 +101,12 @@ class CoverView(QWidget): # {{{
self.animation.setEndValue(self.current_pixmap_size)
def relayout(self, parent_size):
self.setMaximumSize(parent_size.width(),
min(int(parent_size.height()/2.),int(4/3. * parent_size.width())+1))
if self.vertical:
self.setMaximumSize(parent_size.width(),
min(int(parent_size.height()/2.),int(4/3. * parent_size.width())+1))
else:
self.setMaximumSize(1+int(3/4. * parent_size.height()),
parent_size.height())
self.resize(self.maximumSize())
self.animation.stop()
self.do_layout()
@ -109,8 +116,7 @@ class CoverView(QWidget): # {{{
def show_data(self, data):
self.animation.stop()
if data.get('id', None) == self.data.get('id', None):
return
same_item = data.get('id', True) == self.data.get('id', False)
self.data = {'id':data.get('id', None)}
if data.has_key('cover'):
self.pixmap = QPixmap.fromImage(data.pop('cover'))
@ -120,7 +126,8 @@ class CoverView(QWidget): # {{{
self.pixmap = self.default_pixmap
self.do_layout()
self.update()
self.animation.start()
if not same_item:
self.animation.start()
def paintEvent(self, event):
canvas_size = self.rect()
@ -147,6 +154,7 @@ class CoverView(QWidget): # {{{
# }}}
# Book Info {{{
class Label(QLabel):
mr = pyqtSignal(object)
@ -174,8 +182,9 @@ class Label(QLabel):
class BookInfo(QScrollArea):
def __init__(self, parent=None):
def __init__(self, vertical, parent=None):
QScrollArea.__init__(self, parent)
self.vertical = vertical
self.setWidgetResizable(True)
self.label = Label()
self.setWidget(self.label)
@ -188,13 +197,25 @@ class BookInfo(QScrollArea):
rows = render_rows(data)
rows = u'\n'.join([u'<tr><td valign="top"><b>%s:</b></td><td valign="top">%s</td></tr>'%(k,t) for
k, t in rows])
if _('Comments') in data and data[_('Comments')]:
comments = comments_to_html(data[_('Comments')])
rows += u'<tr><td colspan="2">%s</td></tr>'%comments
if self.vertical:
if _('Comments') in data and data[_('Comments')]:
comments = comments_to_html(data[_('Comments')])
rows += u'<tr><td colspan="2">%s</td></tr>'%comments
self.label.setText(u'<table>%s</table>'%rows)
else:
comments = ''
if _('Comments') in data:
comments = comments_to_html(data[_('Comments')])
left_pane = u'<table>%s</table>'%rows
right_pane = u'<div>%s</div>'%comments
self.label.setText(u'<table><tr><td valign="top" '
'style="padding-right:2em">%s</td><td valign="top">%s</td></tr></table>'
% (left_pane, right_pane))
self.label.setText(u'<table>%s</table>'%rows)
class BookDetails(QWidget):
# }}}
class BookDetails(QWidget): # {{{
resized = pyqtSignal(object)
show_book_info = pyqtSignal()
@ -234,20 +255,26 @@ class BookDetails(QWidget):
# }}}
def __init__(self, parent=None):
def __init__(self, vertical, parent=None):
QWidget.__init__(self, parent)
self.setAcceptDrops(True)
self._layout = QVBoxLayout()
if not vertical:
self._layout.setDirection(self._layout.LeftToRight)
self.setLayout(self._layout)
self.cover_view = CoverView(self)
self.cover_view = CoverView(vertical, self)
self.cover_view.relayout(self.size())
self.resized.connect(self.cover_view.relayout, type=Qt.QueuedConnection)
self._layout.addWidget(self.cover_view, alignment=Qt.AlignHCenter)
self.book_info = BookInfo(self)
self._layout.addWidget(self.cover_view)
self.book_info = BookInfo(vertical, self)
self._layout.addWidget(self.book_info)
self.book_info.link_clicked.connect(self._link_clicked)
self.book_info.mr.connect(self.mouseReleaseEvent)
self.setMinimumSize(QSize(190, 200))
if vertical:
self.setMinimumSize(QSize(190, 200))
else:
self.setMinimumSize(120, 120)
self.setCursor(Qt.PointingHandCursor)
def _link_clicked(self, link):
@ -258,8 +285,7 @@ class BookDetails(QWidget):
id_, fmt = val.split(':')
self.view_specific_format.emit(int(id_), fmt)
elif typ == 'devpath':
path = os.path.dirname(val)
QDesktopServices.openUrl(QUrl.fromLocalFile(path))
QDesktopServices.openUrl(QUrl.fromLocalFile(val))
def mouseReleaseEvent(self, ev):
@ -275,10 +301,8 @@ class BookDetails(QWidget):
self.setToolTip('<p>'+_('Click to open Book Details window') +
'<br><br>' + _('Path') + ': ' + data.get(_('Path'), ''))
def reset_info(self):
self.show_data({})
# }}}

View File

@ -602,7 +602,6 @@ class Emailer(Thread): # {{{
class DeviceMixin(object): # {{{
def __init__(self):
self.db_book_uuid_cache = set()
self.device_error_dialog = error_dialog(self, _('Error'),
_('Error communicating with device'), ' ')
self.device_error_dialog.setModal(Qt.NonModal)
@ -689,14 +688,28 @@ class DeviceMixin(object): # {{{
self.device_error_dialog.show()
# Device connected {{{
def device_detected(self, connected, is_folder_device):
'''
Called when a device is connected to the computer.
'''
def set_device_menu_items_state(self, connected, is_folder_device):
if connected:
self._sync_menu.connect_to_folder_action.setEnabled(False)
if is_folder_device:
self._sync_menu.disconnect_from_folder_action.setEnabled(True)
self._sync_menu.enable_device_actions(True,
self.device_manager.device.card_prefix(),
self.device_manager.device)
self.eject_action.setEnabled(True)
else:
self._sync_menu.connect_to_folder_action.setEnabled(True)
self._sync_menu.disconnect_from_folder_action.setEnabled(False)
self._sync_menu.enable_device_actions(False)
self.eject_action.setEnabled(False)
def device_detected(self, connected, is_folder_device):
'''
Called when a device is connected to the computer.
'''
self.set_device_menu_items_state(connected, is_folder_device)
if connected:
self.device_manager.get_device_information(\
Dispatcher(self.info_read))
self.set_default_thumbnail(\
@ -705,17 +718,10 @@ class DeviceMixin(object): # {{{
self.device_manager.device.__class__.get_gui_name()+\
_(' detected.'), 3000)
self.device_connected = 'device' if not is_folder_device else 'folder'
self._sync_menu.enable_device_actions(True,
self.device_manager.device.card_prefix(),
self.device_manager.device)
self.location_view.model().device_connected(self.device_manager.device)
self.eject_action.setEnabled(True)
self.refresh_ondevice_info (device_connected = True, reset_only = True)
else:
self._sync_menu.connect_to_folder_action.setEnabled(True)
self._sync_menu.disconnect_from_folder_action.setEnabled(False)
self.device_connected = None
self._sync_menu.enable_device_actions(False)
self.location_view.model().update_devices()
self.vanity.setText(self.vanity_template%\
dict(version=self.latest_version, device=' '))
@ -723,7 +729,6 @@ class DeviceMixin(object): # {{{
if self.current_view() != self.library_view:
self.book_details.reset_info()
self.location_view.setCurrentIndex(self.location_view.model().index(0))
self.eject_action.setEnabled(False)
self.refresh_ondevice_info (device_connected = False)
def info_read(self, job):
@ -1344,11 +1349,18 @@ class DeviceMixin(object): # {{{
return loc
def set_books_in_library(self, booklists, reset=False):
if reset:
# First build a cache of the library, so the search isn't On**2
# Force a reset if the caches are not initialized
if reset or not hasattr(self, 'db_book_title_cache'):
# It might be possible to get here without having initialized the
# library view. In this case, simply give up
if not hasattr(self, 'library_view') or self.library_view is None:
return
db = getattr(self.library_view.model(), 'db', None)
if db is None:
return
# Build a cache (map) of the library, so the search isn't On**2
self.db_book_title_cache = {}
self.db_book_uuid_cache = set()
db = self.library_view.model().db
self.db_book_uuid_cache = {}
for id in db.data.iterallids():
mi = db.get_metadata(id, index_is_id=True)
title = re.sub('(?u)\W|[_]', '', mi.title.lower())
@ -1364,7 +1376,7 @@ class DeviceMixin(object): # {{{
aus = re.sub('(?u)\W|[_]', '', aus)
self.db_book_title_cache[title]['author_sort'][aus] = mi
self.db_book_title_cache[title]['db_ids'][mi.application_id] = mi
self.db_book_uuid_cache.add(mi.uuid)
self.db_book_uuid_cache[mi.uuid] = mi.application_id
# Now iterate through all the books on the device, setting the
# in_library field Fastest and most accurate key is the uuid. Second is
@ -1376,11 +1388,13 @@ class DeviceMixin(object): # {{{
for book in booklist:
if getattr(book, 'uuid', None) in self.db_book_uuid_cache:
book.in_library = True
# ensure that the correct application_id is set
book.application_id = self.db_book_uuid_cache[book.uuid]
continue
book_title = book.title.lower() if book.title else ''
book_title = re.sub('(?u)\W|[_]', '', book_title)
book.in_library = False
book.in_library = None
d = self.db_book_title_cache.get(book_title, None)
if d is not None:
if getattr(book, 'application_id', None) in d['db_ids']:
@ -1409,7 +1423,7 @@ class DeviceMixin(object): # {{{
# Set author_sort if it isn't already
asort = getattr(book, 'author_sort', None)
if not asort and book.authors:
book.author_sort = self.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:

View File

@ -11,7 +11,8 @@ from calibre.gui2.device_drivers.configwidget_ui import Ui_ConfigWidget
class ConfigWidget(QWidget, Ui_ConfigWidget):
def __init__(self, settings, all_formats, supports_subdirs,
must_read_metadata, extra_customization_message):
must_read_metadata, supports_use_author_sort,
extra_customization_message):
QWidget.__init__(self)
Ui_ConfigWidget.__init__(self)
@ -38,6 +39,10 @@ class ConfigWidget(QWidget, Ui_ConfigWidget):
self.opt_read_metadata.setChecked(self.settings.read_metadata)
else:
self.opt_read_metadata.hide()
if supports_use_author_sort:
self.opt_use_author_sort.setChecked(self.settings.use_author_sort)
else:
self.opt_use_author_sort.hide()
if extra_customization_message:
self.extra_customization_label.setText(extra_customization_message)
if settings.extra_customization:
@ -69,3 +74,6 @@ class ConfigWidget(QWidget, Ui_ConfigWidget):
def read_metadata(self):
return self.opt_read_metadata.isChecked()
def use_author_sort(self):
return self.opt_use_author_sort.isChecked()

View File

@ -90,7 +90,14 @@
</property>
</widget>
</item>
<item row="5" column="0">
<item row="3" column="0">
<widget class="QCheckBox" name="opt_use_author_sort">
<property name="text">
<string>Use author sort for author</string>
</property>
</widget>
</item>
<item row="6" column="0">
<widget class="QLabel" name="extra_customization_label">
<property name="text">
<string>Extra customization</string>
@ -103,10 +110,10 @@
</property>
</widget>
</item>
<item row="6" column="0">
<item row="7" column="0">
<widget class="QLineEdit" name="opt_extra_customization"/>
</item>
<item row="3" column="0">
<item row="4" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>Save &amp;template:</string>
@ -116,7 +123,7 @@
</property>
</widget>
</item>
<item row="4" column="0">
<item row="5" column="0">
<widget class="QLineEdit" name="opt_save_template"/>
</item>
</layout>

View File

@ -7,7 +7,7 @@
<rect>
<x>0</x>
<y>0</y>
<width>884</width>
<width>1000</width>
<height>730</height>
</rect>
</property>
@ -89,7 +89,7 @@
<rect>
<x>0</x>
<y>0</y>
<width>604</width>
<width>720</width>
<height>679</height>
</rect>
</property>
@ -370,7 +370,7 @@
</property>
</widget>
</item>
<item row="5" column="0">
<item row="5" column="0" colspan="2">
<widget class="QCheckBox" name="show_avg_rating">
<property name="text">
<string>Show &amp;average ratings in the tags browser</string>

View File

@ -49,6 +49,9 @@ class SocialMetadata(QDialog):
self.mi.tags = self.worker.mi.tags
self.mi.rating = self.worker.mi.rating
self.mi.comments = self.worker.mi.comments
if self.worker.mi.series:
self.mi.series = self.worker.mi.series
self.mi.series_index = self.worker.mi.series_index
QDialog.accept(self)
@property

View File

@ -0,0 +1,109 @@
#!/usr/bin/env python
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
__docformat__ = 'restructuredtext en'
__license__ = 'GPL v3'
from PyQt4.Qt import Qt, QDialog, QTableWidgetItem, QAbstractItemView
from calibre import strftime
from calibre.ebooks.metadata import authors_to_string, authors_to_sort_string, \
title_sort
from calibre.gui2.dialogs.delete_matching_from_device_ui import \
Ui_DeleteMatchingFromDeviceDialog
from calibre.utils.date import UNDEFINED_DATE
class tableItem(QTableWidgetItem):
def __init__(self, text):
QTableWidgetItem.__init__(self, text)
self.setFlags(Qt.ItemIsEnabled)
self.sort = text.lower()
def __ge__(self, other):
return self.sort >= other.sort
def __lt__(self, other):
return self.sort < other.sort
class titleTableItem(tableItem):
def __init__(self, text):
tableItem.__init__(self, text)
self.sort = title_sort(text.lower())
class authorTableItem(tableItem):
def __init__(self, book):
tableItem.__init__(self, authors_to_string(book.authors))
if book.author_sort is not None:
self.sort = book.author_sort.lower()
else:
self.sort = authors_to_sort_string(book.authors).lower()
class dateTableItem(tableItem):
def __init__(self, date):
if date is not None:
tableItem.__init__(self, strftime('%x', date))
self.sort = date
else:
tableItem.__init__(self, '')
self.sort = UNDEFINED_DATE
class DeleteMatchingFromDeviceDialog(QDialog, Ui_DeleteMatchingFromDeviceDialog):
def __init__(self, parent, items):
QDialog.__init__(self, parent)
Ui_DeleteMatchingFromDeviceDialog.__init__(self)
self.setupUi(self)
self.explanation.setText('<p>'+_('All checked books will be '
'<b>permanently deleted</b> from your '
'device. Please verify the list.'+'</p>'))
self.buttonBox.accepted.connect(self.accepted)
self.table.cellClicked.connect(self.cell_clicked)
self.table.setSelectionMode(QAbstractItemView.NoSelection)
self.table.setColumnCount(5)
self.table.setHorizontalHeaderLabels(
['', _('Location'), _('Title'),
_('Author'), _('Date'), _('Format')])
rows = 0
for card in items:
rows += len(items[card][1])
self.table.setRowCount(rows)
row = 0
for card in items:
(model,books) = items[card]
for (id,book) in books:
item = QTableWidgetItem()
item.setFlags(Qt.ItemIsUserCheckable|Qt.ItemIsEnabled)
item.setCheckState(Qt.Checked)
item.setData(Qt.UserRole, (model, id, book.path))
self.table.setItem(row, 0, item)
self.table.setItem(row, 1, tableItem(card))
self.table.setItem(row, 2, titleTableItem(book.title))
self.table.setItem(row, 3, authorTableItem(book))
self.table.setItem(row, 4, dateTableItem(book.datetime))
self.table.setItem(row, 5, tableItem(book.path.rpartition('.')[2]))
row += 1
self.table.setCurrentCell(0, 1)
self.table.resizeColumnsToContents()
self.table.setSortingEnabled(True)
self.table.sortByColumn(2, Qt.AscendingOrder)
self.table.setCurrentCell(0, 1)
def cell_clicked(self, row, col):
if col == 0:
self.table.setCurrentCell(row, 1)
def accepted(self):
self.result = []
for row in range(self.table.rowCount()):
if self.table.item(row, 0).checkState() == Qt.Unchecked:
continue
(model, id, path) = self.table.item(row, 0).data(Qt.UserRole).toPyObject()
path = unicode(path)
self.result.append((model, id, path))
return

View File

@ -0,0 +1,90 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>DeleteMatchingFromDeviceDialog</class>
<widget class="QDialog" name="DeleteMatchingFromDeviceDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>730</width>
<height>342</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="MinimumExpanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="windowTitle">
<string>Delete from device</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QLabel" name="explanation">
</widget>
</item>
<item>
<widget class="QTableWidget" name="table">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="columnCount">
<number>0</number>
</property>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
<property name="centerButtons">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections>
<connection>
<sender>buttonBox</sender>
<signal>accepted()</signal>
<receiver>DeleteMatchingFromDeviceDialog</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>229</x>
<y>211</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>234</y>
</hint>
</hints>
</connection>
<connection>
<sender>buttonBox</sender>
<signal>rejected()</signal>
<receiver>DeleteMatchingFromDeviceDialog</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>297</x>
<y>217</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>234</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@ -8,17 +8,18 @@ __docformat__ = 'restructuredtext en'
import functools
from PyQt4.Qt import QMenu, Qt, pyqtSignal, QToolButton, QIcon, QStackedWidget, \
QWidget, QHBoxLayout, QToolBar, QSize, QSizePolicy
QSize, QSizePolicy, QStatusBar
from calibre.utils.config import prefs
from calibre.ebooks import BOOK_EXTENSIONS
from calibre.constants import isosx, __appname__
from calibre.constants import isosx, __appname__, preferred_encoding
from calibre.gui2 import config, is_widescreen
from calibre.gui2.library.views import BooksView, DeviceBooksView
from calibre.gui2.widgets import Splitter
from calibre.gui2.tag_view import TagBrowserWidget
from calibre.gui2.status import StatusBar, HStatusBar
from calibre.gui2.book_details import BookDetails
from calibre.gui2.notify import get_notifier
_keep_refs = []
@ -130,6 +131,10 @@ class ToolbarMixin(object): # {{{
self.delete_all_but_selected_formats)
self.delete_menu.addAction(
_('Remove covers from selected books'), self.delete_covers)
self.delete_menu.addSeparator()
self.delete_menu.addAction(
_('Remove matching books from device'),
self.remove_matching_books_from_device)
self.action_del.setMenu(self.delete_menu)
self.action_open_containing_folder.setShortcut(Qt.Key_O)
@ -158,8 +163,7 @@ class ToolbarMixin(object): # {{{
self.convert_menu = cm
pm = QMenu()
ap = self.action_preferences
pm.addAction(ap)
pm.addAction(QIcon(I('config.svg')), _('Preferences'), self.do_config)
pm.addAction(QIcon(I('wizard.svg')), _('Run welcome wizard'),
self.run_wizard)
self.action_preferences.setMenu(pm)
@ -214,20 +218,25 @@ class LibraryViewMixin(object): # {{{
partial(self.show_similar_books, 'tag'))
self.action_books_by_this_publisher.triggered.connect(
partial(self.show_similar_books, 'publisher'))
self.library_view.set_context_menu(self.action_edit, self.action_sync,
self.action_convert, self.action_view,
self.action_save,
self.action_open_containing_folder,
self.action_show_book_details,
self.action_del,
add_to_library = None,
similar_menu=similar_menu)
add_to_library = (_('Add books to library'), self.add_books_from_device)
self.memory_view.set_context_menu(None, None, None,
self.action_view, self.action_save, None, None, self.action_del)
self.action_view, self.action_save, None, None, self.action_del,
add_to_library=add_to_library)
self.card_a_view.set_context_menu(None, None, None,
self.action_view, self.action_save, None, None, self.action_del)
self.action_view, self.action_save, None, None, self.action_del,
add_to_library=add_to_library)
self.card_b_view.set_context_menu(None, None, None,
self.action_view, self.action_save, None, None, self.action_del)
self.action_view, self.action_save, None, None, self.action_del,
add_to_library=add_to_library)
self.library_view.files_dropped.connect(self.files_dropped, type=Qt.QueuedConnection)
for func, args in [
@ -332,26 +341,24 @@ class Stack(QStackedWidget): # {{{
# }}}
class SideBar(QToolBar): # {{{
class StatusBar(QStatusBar): # {{{
def initialize(self, systray=None):
self.systray = systray
self.notifier = get_notifier(systray)
def __init__(self, splitters, jobs_button, parent=None):
QToolBar.__init__(self, _('Side bar'), parent)
self.setOrientation(Qt.Vertical)
self.setMovable(False)
self.setFloatable(False)
self.setToolButtonStyle(Qt.ToolButtonIconOnly)
self.setIconSize(QSize(48, 48))
self.spacer = QWidget(self)
self.spacer.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Expanding)
for s in splitters:
self.addWidget(s.button)
self.addWidget(self.spacer)
self.addWidget(jobs_button)
def show_message(self, msg, timeout=0):
QStatusBar.showMessage(self, msg, timeout)
if self.notifier is not None and not config['disable_tray_notification']:
if isosx and isinstance(msg, unicode):
try:
msg = msg.encode(preferred_encoding)
except UnicodeEncodeError:
msg = msg.encode('utf-8')
self.notifier(msg)
for ch in self.children():
if isinstance(ch, QToolButton):
ch.setCursor(Qt.PointingHandCursor)
def clear_message(self):
QStatusBar.clearMessage(self)
# }}}
@ -361,50 +368,52 @@ class LayoutMixin(object): # {{{
self.setupUi(self)
self.setWindowTitle(__appname__)
if config['gui_layout'] == 'narrow':
self.status_bar = self.book_details = StatusBar(self)
if config['gui_layout'] == 'narrow': # narrow {{{
self.book_details = BookDetails(False, self)
self.stack = Stack(self)
self.bd_splitter = Splitter('book_details_splitter',
_('Book Details'), I('book.svg'),
orientation=Qt.Vertical, parent=self, side_index=1)
self._layout_mem = [QWidget(self), QHBoxLayout()]
self._layout_mem[0].setLayout(self._layout_mem[1])
l = self._layout_mem[1]
l.addWidget(self.stack)
self.sidebar = SideBar([getattr(self, x+'_splitter')
for x in ('bd', 'tb', 'cb')], self.jobs_button, parent=self)
l.addWidget(self.sidebar)
self.bd_splitter.addWidget(self._layout_mem[0])
self.bd_splitter.addWidget(self.status_bar)
self.bd_splitter.addWidget(self.stack)
self.bd_splitter.addWidget(self.book_details)
self.bd_splitter.setCollapsible(self.bd_splitter.other_index, False)
self.centralwidget.layout().addWidget(self.bd_splitter)
else:
self.status_bar = HStatusBar(self)
self.setStatusBar(self.status_bar)
# }}}
else: # wide {{{
self.bd_splitter = Splitter('book_details_splitter',
_('Book Details'), I('book.svg'), initial_side_size=200,
orientation=Qt.Horizontal, parent=self, side_index=1)
self.stack = Stack(self)
self.bd_splitter.addWidget(self.stack)
self.book_details = BookDetails(self)
self.book_details = BookDetails(True, self)
self.bd_splitter.addWidget(self.book_details)
self.bd_splitter.setCollapsible(self.bd_splitter.other_index, False)
self.bd_splitter.setSizePolicy(QSizePolicy(QSizePolicy.Expanding,
QSizePolicy.Expanding))
self.centralwidget.layout().addWidget(self.bd_splitter)
# }}}
for x in ('cb', 'tb', 'bd'):
button = getattr(self, x+'_splitter').button
button.setIconSize(QSize(22, 22))
self.status_bar.addPermanentWidget(button)
self.status_bar.addPermanentWidget(self.jobs_button)
self.status_bar = StatusBar(self)
for x in ('cb', 'tb', 'bd'):
button = getattr(self, x+'_splitter').button
button.setIconSize(QSize(24, 24))
self.status_bar.addPermanentWidget(button)
self.status_bar.addPermanentWidget(self.jobs_button)
self.setStatusBar(self.status_bar)
def finalize_layout(self):
self.status_bar.initialize(self.system_tray_icon)
self.book_details.show_book_info.connect(self.show_book_info)
self.book_details.files_dropped.connect(self.files_dropped_on_book)
self.book_details.open_containing_folder.connect(self.view_folder_for_id)
self.book_details.view_specific_format.connect(self.view_format_by_id)
m = self.library_view.model()
if m.rowCount(None) > 0:
self.library_view.set_current_row(0)
m.current_changed(self.library_view.currentIndex(),
self.library_view.currentIndex())
self.library_view.setFocus(Qt.OtherFocusReason)
def save_layout_state(self):

View File

@ -769,6 +769,7 @@ class OnDeviceSearch(SearchQueryParser): # {{{
'format',
'formats',
'title',
'inlibrary'
]
@ -807,12 +808,23 @@ class OnDeviceSearch(SearchQueryParser): # {{{
'author': lambda x: ' & '.join(getattr(x, 'authors')).lower(),
'collections':lambda x: ','.join(getattr(x, 'device_collections')).lower(),
'format':lambda x: os.path.splitext(x.path)[1].lower(),
'inlibrary':lambda x : getattr(x, 'in_library')
}
for x in ('author', 'format'):
q[x+'s'] = q[x]
for index, row in enumerate(self.model.db):
for locvalue in locations:
accessor = q[locvalue]
if query == 'true':
if accessor(row) is not None:
matches.add(index)
continue
if query == 'false':
if accessor(row) is None:
matches.add(index)
continue
if locvalue == 'inlibrary':
continue # this is bool, so can't match below
try:
### Can't separate authors because comma is used for name sep and author sep
### Exact match might not get what you want. For that reason, turn author
@ -862,11 +874,15 @@ class DeviceBooksModel(BooksModel): # {{{
self.editable = True
self.book_in_library = None
def mark_for_deletion(self, job, rows):
self.marked_for_deletion[job] = self.indices(rows)
for row in rows:
indices = self.row_indices(row)
self.dataChanged.emit(indices[0], indices[-1])
def mark_for_deletion(self, job, rows, rows_are_ids=False):
if rows_are_ids:
self.marked_for_deletion[job] = rows
self.reset()
else:
self.marked_for_deletion[job] = self.indices(rows)
for row in rows:
indices = self.row_indices(row)
self.dataChanged.emit(indices[0], indices[-1])
def deletion_done(self, job, succeeded=True):
if not self.marked_for_deletion.has_key(job):
@ -888,13 +904,13 @@ class DeviceBooksModel(BooksModel): # {{{
ans.extend(v)
return ans
def clear_ondevice(self, db_ids):
def clear_ondevice(self, db_ids, to_what=None):
for data in self.db:
if data is None:
continue
app_id = getattr(data, 'application_id', None)
if app_id is not None and app_id in db_ids:
data.in_library = False
data.in_library = to_what
self.reset()
def flags(self, index):
@ -1049,6 +1065,13 @@ class DeviceBooksModel(BooksModel): # {{{
def paths(self, rows):
return [self.db[self.map[r.row()]].path for r in rows ]
def paths_for_db_ids(self, db_ids):
res = []
for r,b in enumerate(self.db):
if b.application_id in db_ids:
res.append((r,b))
return res
def indices(self, rows):
'''
Return indices into underlying database from rows
@ -1089,6 +1112,8 @@ class DeviceBooksModel(BooksModel): # {{{
elif role == Qt.DecorationRole and cname == 'inlibrary':
if self.db[self.map[row]].in_library:
return QVariant(self.bool_yes_icon)
elif self.db[self.map[row]].in_library is not None:
return QVariant(self.bool_no_icon)
elif role == Qt.TextAlignmentRole:
cname = self.column_map[index.column()]
ans = Qt.AlignVCenter | ALIGNMENT_MAP[self.alignment_map.get(cname,

View File

@ -370,7 +370,8 @@ class BooksView(QTableView): # {{{
# Context Menu {{{
def set_context_menu(self, edit_metadata, send_to_device, convert, view,
save, open_folder, book_details, delete, similar_menu=None):
save, open_folder, book_details, delete,
similar_menu=None, add_to_library=None):
self.setContextMenuPolicy(Qt.DefaultContextMenu)
self.context_menu = QMenu(self)
if edit_metadata is not None:
@ -389,6 +390,9 @@ class BooksView(QTableView): # {{{
self.context_menu.addAction(book_details)
if similar_menu is not None:
self.context_menu.addMenu(similar_menu)
if add_to_library is not None:
func = partial(add_to_library[1], view=self)
self.context_menu.addAction(add_to_library[0], func)
def contextMenuEvent(self, event):
self.context_menu.popup(event.globalPos())

View File

@ -84,12 +84,12 @@ class DownloadMetadata(Thread):
if mi.isbn:
args['isbn'] = mi.isbn
else:
if not mi.title:
if not mi.title or mi.title == _('Unknown'):
self.failures[id] = \
(str(id), _('Book has neither title nor ISBN'))
continue
args['title'] = mi.title
if mi.authors:
if mi.authors and mi.authors[0] != _('Unknown'):
args['author'] = mi.authors[0]
if self.key:
args['isbndb_key'] = self.key
@ -127,6 +127,10 @@ class DownloadMetadata(Thread):
self.db.set_tags(id, mi.tags)
if mi.comments:
self.db.set_comment(id, mi.comments)
if mi.series:
self.db.set_series(id, mi.series)
if mi.series_index is not None:
self.db.set_series_index(id, mi.series_index)
self.updated = set(self.fetched_metadata)

View File

@ -18,17 +18,20 @@ from calibre.utils.config import prefs
from calibre.utils.search_query_parser import saved_searches
class SearchLineEdit(QLineEdit):
key_pressed = pyqtSignal(object)
mouse_released = pyqtSignal(object)
focus_out = pyqtSignal(object)
def keyPressEvent(self, event):
self.emit(SIGNAL('key_pressed(PyQt_PyObject)'), event)
self.key_pressed.emit(event)
QLineEdit.keyPressEvent(self, event)
def mouseReleaseEvent(self, event):
self.emit(SIGNAL('mouse_released(PyQt_PyObject)'), event)
self.mouse_released.emit(event)
QLineEdit.mouseReleaseEvent(self, event)
def focusOutEvent(self, event):
self.emit(SIGNAL('focus_out(PyQt_PyObject)'), event)
self.focus_out.emit(event)
QLineEdit.focusOutEvent(self, event)
def dropEvent(self, ev):
@ -68,10 +71,10 @@ class SearchBox2(QComboBox):
self.normal_background = 'rgb(255, 255, 255, 0%)'
self.line_edit = SearchLineEdit(self)
self.setLineEdit(self.line_edit)
self.connect(self.line_edit, SIGNAL('key_pressed(PyQt_PyObject)'),
self.key_pressed, Qt.DirectConnection)
self.connect(self.line_edit, SIGNAL('mouse_released(PyQt_PyObject)'),
self.mouse_released, Qt.DirectConnection)
self.line_edit.key_pressed.connect(self.key_pressed,
type=Qt.DirectConnection)
self.line_edit.mouse_released.connect(self.mouse_released,
type=Qt.DirectConnection)
self.setEditable(True)
self.help_state = False
self.as_you_type = True
@ -90,14 +93,18 @@ class SearchBox2(QComboBox):
self.help_text = help_text
self.colorize = colorize
self.clear_to_help()
self.connect(self, SIGNAL('editTextChanged(QString)'), self.text_edited_slot)
def normalize_state(self):
self.setEditText('')
self.line_edit.setStyleSheet(
'QLineEdit { color: black; background-color: %s; }' %
self.normal_background)
self.help_state = False
if self.help_state:
self.setEditText('')
self.line_edit.setStyleSheet(
'QLineEdit { color: black; background-color: %s; }' %
self.normal_background)
self.help_state = False
else:
self.line_edit.setStyleSheet(
'QLineEdit { color: black; background-color: %s; }' %
self.normal_background)
def clear_to_help(self):
if self.help_state:
@ -131,17 +138,13 @@ class SearchBox2(QComboBox):
self.line_edit.setStyleSheet('QLineEdit { color: black; background-color: %s; }' % col)
def key_pressed(self, event):
if self.help_state:
self.normalize_state()
if not self.as_you_type:
if event.key() in (Qt.Key_Return, Qt.Key_Enter):
self.do_search()
self.normalize_state()
if event.key() in (Qt.Key_Return, Qt.Key_Enter):
self.do_search()
self.timer = self.startTimer(self.__class__.INTERVAL)
def mouse_released(self, event):
if self.help_state:
self.normalize_state()
def text_edited_slot(self, text):
self.normalize_state()
if self.as_you_type:
self.timer = self.startTimer(self.__class__.INTERVAL)
@ -227,14 +230,13 @@ class SavedSearchBox(QComboBox):
self.line_edit = SearchLineEdit(self)
self.setLineEdit(self.line_edit)
self.connect(self.line_edit, SIGNAL('key_pressed(PyQt_PyObject)'),
self.key_pressed, Qt.DirectConnection)
self.connect(self.line_edit, SIGNAL('mouse_released(PyQt_PyObject)'),
self.mouse_released, Qt.DirectConnection)
self.connect(self.line_edit, SIGNAL('focus_out(PyQt_PyObject)'),
self.focus_out, Qt.DirectConnection)
self.connect(self, SIGNAL('activated(const QString&)'),
self.saved_search_selected)
self.line_edit.key_pressed.connect(self.key_pressed,
type=Qt.DirectConnection)
self.line_edit.mouse_released.connect(self.mouse_released,
type=Qt.DirectConnection)
self.line_edit.focus_out.connect(self.focus_out,
type=Qt.DirectConnection)
self.activated[str].connect(self.saved_search_selected)
completer = QCompleter(self) # turn off auto-completion
self.setCompleter(completer)
@ -282,7 +284,7 @@ class SavedSearchBox(QComboBox):
if self.help_state:
self.normalize_state()
def saved_search_selected (self, qname):
def saved_search_selected(self, qname):
qname = unicode(qname)
if qname is None or not qname.strip():
return

View File

@ -1,254 +0,0 @@
__license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
import os
from PyQt4.Qt import QStatusBar, QLabel, QWidget, QHBoxLayout, QPixmap, \
QSizePolicy, QScrollArea, Qt, QSize, pyqtSignal, \
QPropertyAnimation, QEasingCurve, QDesktopServices, QUrl
from calibre import fit_image, preferred_encoding, isosx
from calibre.gui2 import config
from calibre.gui2.widgets import IMAGE_EXTENSIONS
from calibre.gui2.notify import get_notifier
from calibre.ebooks import BOOK_EXTENSIONS
from calibre.library.comments import comments_to_html
from calibre.gui2.book_details import render_rows
class BookInfoDisplay(QWidget):
DROPABBLE_EXTENSIONS = IMAGE_EXTENSIONS+BOOK_EXTENSIONS
files_dropped = pyqtSignal(object, object)
@classmethod
def paths_from_event(cls, event):
'''
Accept a drop event and return a list of paths that can be read from
and represent files with extensions.
'''
if event.mimeData().hasFormat('text/uri-list'):
urls = [unicode(u.toLocalFile()) for u in event.mimeData().urls()]
urls = [u for u in urls if os.path.splitext(u)[1] and os.access(u, os.R_OK)]
return [u for u in urls if os.path.splitext(u)[1][1:].lower() in cls.DROPABBLE_EXTENSIONS]
def dragEnterEvent(self, event):
if int(event.possibleActions() & Qt.CopyAction) + \
int(event.possibleActions() & Qt.MoveAction) == 0:
return
paths = self.paths_from_event(event)
if paths:
event.acceptProposedAction()
def dropEvent(self, event):
paths = self.paths_from_event(event)
event.setDropAction(Qt.CopyAction)
self.files_dropped.emit(event, paths)
def dragMoveEvent(self, event):
event.acceptProposedAction()
class BookCoverDisplay(QLabel): # {{{
def __init__(self, coverpath=I('book.svg')):
QLabel.__init__(self)
self.animation = QPropertyAnimation(self, 'size', self)
self.animation.setEasingCurve(QEasingCurve(QEasingCurve.OutExpo))
self.animation.setDuration(1000)
self.animation.setStartValue(QSize(0, 0))
self.setMaximumWidth(81)
self.setMaximumHeight(108)
self.default_pixmap = QPixmap(coverpath)
self.setScaledContents(True)
self.statusbar_height = 120
self.setPixmap(self.default_pixmap)
def do_layout(self):
self.animation.stop()
pixmap = self.pixmap()
pwidth, pheight = pixmap.width(), pixmap.height()
width, height = fit_image(pwidth, pheight,
pwidth, self.statusbar_height-20)[1:]
self.setMaximumHeight(height)
try:
aspect_ratio = pwidth/float(pheight)
except ZeroDivisionError:
aspect_ratio = 1
self.setMaximumWidth(int(aspect_ratio*self.maximumHeight()))
self.animation.setEndValue(self.maximumSize())
def setPixmap(self, pixmap):
QLabel.setPixmap(self, pixmap)
self.do_layout()
self.animation.start()
def sizeHint(self):
return QSize(self.maximumWidth(), self.maximumHeight())
def relayout(self, statusbar_size):
self.statusbar_height = statusbar_size.height()
self.do_layout()
# }}}
class BookDataDisplay(QLabel):
mr = pyqtSignal(object)
link_clicked = pyqtSignal(object)
def __init__(self):
QLabel.__init__(self)
self.setText('')
self.setWordWrap(True)
self.setSizePolicy(QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding))
self.linkActivated.connect(self.link_activated)
self._link_clicked = False
def mouseReleaseEvent(self, ev):
QLabel.mouseReleaseEvent(self, ev)
if not self._link_clicked:
self.mr.emit(ev)
self._link_clicked = False
def link_activated(self, link):
self._link_clicked = True
link = unicode(link)
self.link_clicked.emit(link)
show_book_info = pyqtSignal()
def __init__(self, clear_message):
QWidget.__init__(self)
self.setCursor(Qt.PointingHandCursor)
self.setSizePolicy(QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding))
self._layout = QHBoxLayout()
self.setLayout(self._layout)
self.clear_message = clear_message
self.cover_display = BookInfoDisplay.BookCoverDisplay()
self._layout.addWidget(self.cover_display)
self.book_data = BookInfoDisplay.BookDataDisplay()
self.book_data.mr.connect(self.mouseReleaseEvent)
self._layout.addWidget(self.book_data)
self.data = {}
self.setVisible(False)
self._layout.setAlignment(self.cover_display, Qt.AlignTop|Qt.AlignLeft)
def mouseReleaseEvent(self, ev):
ev.accept()
self.show_book_info.emit()
def show_data(self, data):
if data.has_key('cover'):
self.cover_display.setPixmap(QPixmap.fromImage(data.pop('cover')))
else:
self.cover_display.setPixmap(self.cover_display.default_pixmap)
rows, comments = [], ''
self.book_data.setText('')
self.data = data.copy()
rows = render_rows(self.data)
rows = '\n'.join([u'<tr><td valign="top"><b>%s:</b></td><td valign="top">%s</td></tr>'%(k,t) for
k, t in rows])
if _('Comments') in self.data:
comments = comments_to_html(self.data[_('Comments')])
comments = ('<b>%s:</b>'%_('Comments'))+comments
left_pane = u'<table>%s</table>'%rows
right_pane = u'<div>%s</div>'%comments
self.book_data.setText(u'<table><tr><td valign="top" '
'style="padding-right:2em">%s</td><td valign="top">%s</td></tr></table>'
% (left_pane, right_pane))
self.clear_message()
self.book_data.updateGeometry()
self.updateGeometry()
self.setVisible(True)
self.setToolTip('<p>'+_('Click to open Book Details window') +
'<br><br>' + _('Path') + ': ' + data.get(_('Path'), ''))
class StatusBarInterface(object):
def initialize(self, systray=None):
self.systray = systray
self.notifier = get_notifier(systray)
def show_message(self, msg, timeout=0):
QStatusBar.showMessage(self, msg, timeout)
if self.notifier is not None and not config['disable_tray_notification']:
if isosx and isinstance(msg, unicode):
try:
msg = msg.encode(preferred_encoding)
except UnicodeEncodeError:
msg = msg.encode('utf-8')
self.notifier(msg)
def clear_message(self):
QStatusBar.clearMessage(self)
class BookDetailsInterface(object):
# These signals must be defined in the class implementing this interface
files_dropped = None
show_book_info = None
open_containing_folder = None
view_specific_format = None
def reset_info(self):
raise NotImplementedError()
def show_data(self, data):
raise NotImplementedError()
class HStatusBar(QStatusBar, StatusBarInterface):
pass
class StatusBar(QStatusBar, StatusBarInterface, BookDetailsInterface):
files_dropped = pyqtSignal(object, object)
show_book_info = pyqtSignal()
open_containing_folder = pyqtSignal(int)
view_specific_format = pyqtSignal(int, object)
resized = pyqtSignal(object)
def initialize(self, systray=None):
StatusBarInterface.initialize(self, systray=systray)
self.book_info = BookInfoDisplay(self.clear_message)
self.book_info.setAcceptDrops(True)
self.scroll_area = QScrollArea()
self.scroll_area.setWidget(self.book_info)
self.scroll_area.setWidgetResizable(True)
self.book_info.show_book_info.connect(self.show_book_info.emit,
type=Qt.QueuedConnection)
self.book_info.files_dropped.connect(self.files_dropped.emit,
type=Qt.QueuedConnection)
self.book_info.book_data.link_clicked.connect(self._link_clicked)
self.addWidget(self.scroll_area, 100)
self.setMinimumHeight(120)
self.resized.connect(self.book_info.cover_display.relayout)
self.book_info.cover_display.relayout(self.size())
def _link_clicked(self, link):
typ, _, val = link.partition(':')
if typ == 'path':
self.open_containing_folder.emit(int(val))
elif typ == 'format':
id_, fmt = val.split(':')
self.view_specific_format.emit(int(id_), fmt)
elif typ == 'devpath':
path = os.path.dirname(val)
QDesktopServices.openUrl(QUrl.fromLocalFile(path))
def resizeEvent(self, ev):
self.resized.emit(self.size())
def reset_info(self):
self.book_info.show_data({})
def show_data(self, data):
self.book_info.show_data(data)

View File

@ -10,14 +10,13 @@ Browsing book collection by tags.
from itertools import izip
from functools import partial
from PyQt4.Qt import Qt, QTreeView, QApplication, pyqtSignal, QCheckBox, \
from PyQt4.Qt import Qt, QTreeView, QApplication, pyqtSignal, \
QFont, QSize, QIcon, QPoint, QVBoxLayout, QComboBox, \
QAbstractItemModel, QVariant, QModelIndex, QMenu, \
QPushButton, QWidget, QItemDelegate, QString, QPen, \
QColor, QLinearGradient, QBrush
QPushButton, QWidget, QItemDelegate
from calibre.gui2 import config, NONE
from calibre.utils.config import prefs, tweaks
from calibre.utils.config import prefs
from calibre.library.field_metadata import TagsIcons
from calibre.utils.search_query_parser import saved_searches
from calibre.gui2 import error_dialog
@ -25,12 +24,7 @@ from calibre.gui2.dialogs.tag_categories import TagCategories
from calibre.gui2.dialogs.tag_list_editor import TagListEditor
from calibre.gui2.dialogs.edit_authors_dialog import EditAuthorsDialog
class TagDelegate(QItemDelegate):
def __init__(self, parent):
QItemDelegate.__init__(self, parent)
self._parent = parent
self.icon = QIcon(I('star.png'))
class TagDelegate(QItemDelegate): # {{{
def paint(self, painter, option, index):
item = index.internalPointer()
@ -38,51 +32,29 @@ class TagDelegate(QItemDelegate):
QItemDelegate.paint(self, painter, option, index)
return
r = option.rect
# Paint the decoration icon
icon = self._parent.model().data(index, Qt.DecorationRole).toPyObject()
icon.paint(painter, r, Qt.AlignLeft)
model = self.parent().model()
icon = model.data(index, Qt.DecorationRole).toPyObject()
painter.save()
if item.tag.state != 0 or not config['show_avg_rating'] or \
item.tag.avg_rating is None:
icon.paint(painter, r, Qt.AlignLeft)
else:
painter.setOpacity(0.3)
icon.paint(painter, r, Qt.AlignLeft)
painter.setOpacity(1)
rating = item.tag.avg_rating
painter.setClipRect(r.left(), r.bottom()-int(r.height()*(rating/5.0)),
r.width(), r.height())
icon.paint(painter, r, Qt.AlignLeft)
painter.setClipRect(r)
# Paint the rating, if any. The decoration icon is assumed to be square,
# filling the row top to bottom. The three is arbitrary, there to
# provide a little space between the icon and what follows
r.setLeft(r.left()+r.height()+3)
rating = item.tag.avg_rating
if config['show_avg_rating'] and item.tag.avg_rating is not None:
painter.save()
if tweaks['render_avg_rating_using'] == 'star':
painter.setClipRect(r.left(), r.top(),
int(r.height()*(rating/5.0)), r.height())
self.icon.paint(painter, r, Qt.AlignLeft | Qt.AlignVCenter)
r.setLeft(r.left() + r.height())
else:
painter.translate(r.left(), r.top())
# Compute factor so sizes can be expressed in percentages of the
# box defined by the row height
factor = r.height()/100.
width = 20
height = 80
left_offset = 5
top_offset = 10
if r > 0.0:
color = QColor(100, 100, 255) #medium blue, less glare
pen = QPen(color, 5, Qt.SolidLine, Qt.RoundCap, Qt.RoundJoin)
painter.setPen(pen)
painter.scale(factor, factor)
painter.drawRect(left_offset, top_offset, width, height)
fill_height = height*(rating/5.0)
gradient = QLinearGradient(0, 0, 0, 100)
gradient.setColorAt(0.0, color)
gradient.setColorAt(1.0, color)
painter.setBrush(QBrush(gradient))
painter.drawRect(left_offset, top_offset+(height-fill_height),
width, fill_height)
# The '3' is arbitrary, there because we need a little space
# between the rectangle and the text.
r.setLeft(r.left() + ((width+left_offset*2)*factor) + 3)
painter.restore()
# Paint the text
r.setLeft(r.left()+r.height()+3)
painter.drawText(r, Qt.AlignLeft|Qt.AlignVCenter,
QString('[%d] %s'%(item.tag.count, item.tag.name)))
model.data(index, Qt.DisplayRole).toString())
painter.restore()
# }}}
class TagsView(QTreeView): # {{{
@ -107,12 +79,12 @@ class TagsView(QTreeView): # {{{
self.setHeaderHidden(True)
self.setItemDelegate(TagDelegate(self))
def set_database(self, db, tag_match, popularity):
def set_database(self, db, tag_match, sort_by):
self.hidden_categories = config['tag_browser_hidden_categories']
self._model = TagsModel(db, parent=self,
hidden_categories=self.hidden_categories,
search_restriction=None)
self.popularity = popularity
self.sort_by = sort_by
self.tag_match = tag_match
self.db = db
self.search_restriction = None
@ -120,8 +92,9 @@ class TagsView(QTreeView): # {{{
self.setContextMenuPolicy(Qt.CustomContextMenu)
self.clicked.connect(self.toggle)
self.customContextMenuRequested.connect(self.show_context_menu)
self.popularity.setChecked(config['sort_by_popularity'])
self.popularity.stateChanged.connect(self.sort_changed)
pop = config['sort_tags_by']
self.sort_by.setCurrentIndex(self.db.CATEGORY_SORTS.index(pop))
self.sort_by.currentIndexChanged.connect(self.sort_changed)
self.refresh_required.connect(self.recount, type=Qt.QueuedConnection)
db.add_listener(self.database_changed)
@ -132,8 +105,8 @@ class TagsView(QTreeView): # {{{
def match_all(self):
return self.tag_match and self.tag_match.currentIndex() > 0
def sort_changed(self, state):
config.set('sort_by_popularity', state == Qt.Checked)
def sort_changed(self, pop):
config.set('sort_tags_by', self.db.CATEGORY_SORTS[pop])
self.recount()
def set_search_restriction(self, s):
@ -372,11 +345,7 @@ class TagTreeItem(object): # {{{
if self.tag.count == 0:
return QVariant('%s'%(self.tag.name))
else:
if self.tag.avg_rating is None:
return QVariant('[%d] %s'%(self.tag.count, self.tag.name))
else:
return QVariant('[%d][%3.1f] %s'%(self.tag.count,
self.tag.avg_rating, self.tag.name))
return QVariant('[%d] %s'%(self.tag.count, self.tag.name))
if role == Qt.EditRole:
return QVariant(self.tag.name)
if role == Qt.DecorationRole:
@ -420,7 +389,7 @@ class TagsModel(QAbstractItemModel): # {{{
self.row_map = []
# get_node_tree cannot return None here, because row_map is empty
data = self.get_node_tree(config['sort_by_popularity'])
data = self.get_node_tree(config['sort_tags_by'])
self.root_item = TagTreeItem()
for i, r in enumerate(self.row_map):
if self.hidden_categories and self.categories[i] in self.hidden_categories:
@ -464,11 +433,11 @@ class TagsModel(QAbstractItemModel): # {{{
# Now get the categories
if self.search_restriction:
data = self.db.get_categories(sort_on_count=sort,
data = self.db.get_categories(sort=sort,
icon_map=self.category_icon_map,
ids=self.db.search('', return_matches=True))
else:
data = self.db.get_categories(sort_on_count=sort, icon_map=self.category_icon_map)
data = self.db.get_categories(sort=sort, icon_map=self.category_icon_map)
tb_categories = self.db.field_metadata
for category in tb_categories:
@ -482,7 +451,7 @@ class TagsModel(QAbstractItemModel): # {{{
return data
def refresh(self):
data = self.get_node_tree(config['sort_by_popularity']) # get category data
data = self.get_node_tree(config['sort_tags_by']) # get category data
if data is None:
return False
row_index = -1
@ -691,7 +660,7 @@ class TagBrowserMixin(object): # {{{
def __init__(self, db):
self.library_view.model().count_changed_signal.connect(self.tags_view.recount)
self.tags_view.set_database(self.library_view.model().db,
self.tag_match, self.popularity)
self.tag_match, self.sort_by)
self.tags_view.tags_marked.connect(self.search.search_from_tags)
self.tags_view.tags_marked.connect(self.saved_search.clear_to_help)
self.tags_view.tag_list_edit.connect(self.do_tags_list_edit)
@ -752,9 +721,13 @@ class TagBrowserWidget(QWidget): # {{{
parent.tags_view = TagsView(parent)
self._layout.addWidget(parent.tags_view)
parent.popularity = QCheckBox(parent)
parent.popularity.setText(_('Sort by &popularity'))
self._layout.addWidget(parent.popularity)
parent.sort_by = QComboBox(parent)
# Must be in the same order as db2.CATEGORY_SORTS
for x in (_('Sort by name'), _('Sort by popularity'),
_('Sort by average rating')):
parent.sort_by.addItem(x)
parent.sort_by.setCurrentIndex(0)
self._layout.addWidget(parent.sort_by)
parent.tag_match = QComboBox(parent)
for x in (_('Match any'), _('Match all')):

View File

@ -126,8 +126,7 @@ class Main(MainWindow, Ui_MainWindow, DeviceMixin, ToolbarMixin, # {{{
# Jobs Button {{{
self.job_manager = JobManager()
self.jobs_dialog = JobsDialog(self, self.job_manager)
self.jobs_button = JobsButton(horizontal=config['gui_layout'] !=
'narrow')
self.jobs_button = JobsButton(horizontal=True)
self.jobs_button.initialize(self.jobs_dialog, self.job_manager)
# }}}
@ -216,12 +215,6 @@ class Main(MainWindow, Ui_MainWindow, DeviceMixin, ToolbarMixin, # {{{
self.vanity.setText(self.vanity_template%dict(version=' ', device=' '))
self.device_info = ' '
UpdateMixin.__init__(self, opts)
####################### Status Bar #####################
self.status_bar.initialize(self.system_tray_icon)
self.book_details.show_book_info.connect(self.show_book_info)
self.book_details.files_dropped.connect(self.files_dropped_on_book)
self.book_details.open_containing_folder.connect(self.view_folder_for_id)
self.book_details.view_specific_format.connect(self.view_format_by_id)
####################### Setup Toolbar #####################
ToolbarMixin.__init__(self)
@ -417,6 +410,8 @@ class Main(MainWindow, Ui_MainWindow, DeviceMixin, ToolbarMixin, # {{{
self.tags_view.set_new_model() # in case columns changed
self.tags_view.recount()
self.create_device_menu()
self.set_device_menu_items_state(bool(self.device_connected),
self.device_connected == 'folder')
if not patheq(self.library_path, d.database_location):
newloc = d.database_location
@ -430,7 +425,7 @@ class Main(MainWindow, Ui_MainWindow, DeviceMixin, ToolbarMixin, # {{{
self.book_on_device(None, reset=True)
db.set_book_on_device_func(self.book_on_device)
self.library_view.set_database(db)
self.tags_view.set_database(db, self.tag_match, self.popularity)
self.tags_view.set_database(db, self.tag_match, self.sort_by)
self.library_view.model().set_book_on_device_func(self.book_on_device)
self.status_bar.clear_message()
self.search.clear_to_help()

View File

@ -64,6 +64,10 @@ class Tag(object):
self.state = state
self.avg_rating = avg/2.0 if avg is not None else 0
self.sort = sort
if self.avg_rating > 0:
if tooltip:
tooltip = tooltip + ': '
tooltip = _('%sAverage rating is %3.1f')%(tooltip, self.avg_rating)
self.tooltip = tooltip
self.icon = icon
@ -132,6 +136,23 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
self.initialize_dynamic()
def initialize_dynamic(self):
self.conn.executescript('''
DROP TRIGGER IF EXISTS author_insert_trg;
CREATE TEMP TRIGGER author_insert_trg
AFTER INSERT ON authors
BEGIN
UPDATE authors SET sort=author_to_author_sort(NEW.name) WHERE id=NEW.id;
END;
DROP TRIGGER IF EXISTS author_update_trg;
CREATE TEMP TRIGGER author_update_trg
BEFORE UPDATE ON authors
BEGIN
UPDATE authors SET sort=author_to_author_sort(NEW.name)
WHERE id=NEW.id AND name <> NEW.name;
END;
''')
self.conn.execute(
'UPDATE authors SET sort=author_to_author_sort(name) WHERE sort IS NULL')
self.conn.executescript(u'''
CREATE TEMP VIEW IF NOT EXISTS tag_browser_news AS SELECT DISTINCT
id,
@ -429,7 +450,11 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
if aum: aum = [a.strip().replace('|', ',') for a in aum.split(',')]
mi = MetaInformation(self.title(idx, index_is_id=index_is_id), aum)
mi.author_sort = self.author_sort(idx, index_is_id=index_is_id)
mi.authors_sort_strings = self.authors_sort_strings(idx, index_is_id)
if mi.authors:
mi.author_sort_map = {}
for name, sort in zip(mi.authors, self.authors_sort_strings(idx,
index_is_id)):
mi.author_sort_map[name] = sort
mi.comments = self.comments(idx, index_is_id=index_is_id)
mi.publisher = self.publisher(idx, index_is_id=index_is_id)
mi.timestamp = self.timestamp(idx, index_is_id=index_is_id)
@ -687,7 +712,9 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
tn=field['table'], col=field['link_column']), (id_,))
return set(x[0] for x in ans)
def get_categories(self, sort_on_count=False, ids=None, icon_map=None):
CATEGORY_SORTS = ('name', 'popularity', 'rating')
def get_categories(self, sort='name', ids=None, icon_map=None):
self.books_list_filter.change([] if not ids else ids)
categories = {}
@ -711,10 +738,12 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
else:
query = '''SELECT id, {0}, count, avg_rating, sort
FROM tag_browser_filtered_{1}'''.format(cn, tn)
if sort_on_count:
query += ' ORDER BY count DESC'
else:
if sort == 'popularity':
query += ' ORDER BY count DESC, sort ASC'
elif sort == 'name':
query += ' ORDER BY sort ASC'
else:
query += ' ORDER BY avg_rating DESC, sort ASC'
data = self.conn.get(query)
# icon_map is not None if get_categories is to store an icon and
@ -770,11 +799,10 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
if count > 0:
categories['formats'].append(Tag(fmt, count=count, icon=icon))
if sort_on_count:
categories['formats'].sort(cmp=lambda x,y:cmp(x.count, y.count),
reverse=True)
else:
categories['formats'].sort(cmp=lambda x,y:cmp(x.name, y.name))
if sort == 'popularity':
categories['formats'].sort(key=lambda x: x.count, reverse=True)
else: # no ratings exist to sort on
categories['formats'].sort(key = lambda x:x.name)
#### Now do the user-defined categories. ####
user_categories = prefs['user_categories']
@ -799,12 +827,15 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
# Not a problem if we accumulate entries in the icon map
if icon_map is not None:
icon_map[cat_name] = icon_map[':user']
if sort_on_count:
if sort == 'popularity':
categories[cat_name] = \
sorted(items, cmp=(lambda x, y: cmp(y.count, x.count)))
sorted(items, key=lambda x: x.count, reverse=True)
elif sort == 'name':
categories[cat_name] = \
sorted(items, key=lambda x: x.sort.lower())
else:
categories[cat_name] = \
sorted(items, cmp=(lambda x, y: cmp(x.name.lower(), y.name.lower())))
sorted(items, key=lambda x:x.avg_rating, reverse=True)
#### Finally, the saved searches category ####
items = []

View File

@ -385,28 +385,5 @@ class SchemaUpgrade(object):
if table.startswith('custom_column_') and link_table in tables:
create_cust_tag_browser_view(table, link_table)
from calibre.ebooks.metadata import author_to_author_sort
self.conn.execute('UPDATE authors SET sort=author_to_author_sort(name)')
aut = self.conn.get('SELECT id, name FROM authors');
records = []
for (id, author) in aut:
records.append((id, author.replace('|', ',')))
for id,author in records:
self.conn.execute('UPDATE authors SET sort=? WHERE id=?',
(author_to_author_sort(author.replace('|', ',')).strip(), id))
self.conn.commit()
self.conn.executescript('''
DROP TRIGGER IF EXISTS author_insert_trg;
CREATE TRIGGER author_insert_trg
AFTER INSERT ON authors
BEGIN
UPDATE authors SET sort=author_to_author_sort(NEW.name) WHERE id=NEW.id;
END;
DROP TRIGGER IF EXISTS author_update_trg;
CREATE TRIGGER author_update_trg
BEFORE UPDATE ON authors
BEGIN
UPDATE authors SET sort=author_to_author_sort(NEW.name)
WHERE id=NEW.id AND name <> NEW.name;
END;
''')

View File

@ -99,17 +99,20 @@ def html_to_lxml(raw):
raw = etree.tostring(root, encoding=None)
return etree.fromstring(raw)
def CATALOG_ENTRY(item, base_href, version, updated):
def CATALOG_ENTRY(item, base_href, version, updated, ignore_count=False):
id_ = 'calibre:category:'+item.name
iid = 'N' + item.name
if item.id is not None:
iid = 'I' + str(item.id)
link = NAVLINK(href = base_href + '/' + hexlify(iid))
count = _('%d books')%item.count
if ignore_count:
count = ''
return E.entry(
TITLE(item.name),
ID(id_),
UPDATED(updated),
E.content(_('%d books')%item.count, type='text'),
E.content(count, type='text'),
link
)
@ -265,8 +268,12 @@ class CategoryFeed(NavFeed):
def __init__(self, items, which, id_, updated, version, offsets, page_url, up_url):
NavFeed.__init__(self, id_, updated, version, offsets, page_url, up_url)
base_href = self.base_href + '/category/' + hexlify(which)
ignore_count = False
if which == 'search':
ignore_count = True
for item in items:
self.root.append(CATALOG_ENTRY(item, base_href, version, updated))
self.root.append(CATALOG_ENTRY(item, base_href, version, updated,
ignore_count=ignore_count))
class CategoryGroupFeed(NavFeed):
@ -393,7 +400,7 @@ class OPDSServer(object):
owhich = hexlify('N'+which)
up_url = url_for('opdsnavcatalog', version, which=owhich)
items = categories[category]
items = [x for x in items if x.name.startswith(which)]
items = [x for x in items if getattr(x, 'sort', x.name).startswith(which)]
if not items:
raise cherrypy.HTTPError(404, 'No items in group %r:%r'%(category,
which))
@ -458,11 +465,11 @@ class OPDSServer(object):
def __init__(self, text, count):
self.text, self.count = text, count
starts = set([x.name[0] for x in items])
starts = set([getattr(x, 'sort', x.name)[0] for x in items])
category_groups = OrderedDict()
for x in sorted(starts, cmp=lambda x,y:cmp(x.lower(), y.lower())):
category_groups[x] = len([y for y in items if
y.name.startswith(x)])
getattr(y, 'sort', y.name).startswith(x)])
items = [Group(x, y) for x, y in category_groups.items()]
max_items = self.opts.max_opds_items
offsets = OPDSOffsets(offset, max_items, len(items))

View File

@ -94,6 +94,9 @@ class Connection(sqlite.Connection):
return ans[0]
return ans.fetchall()
def _author_to_author_sort(x):
if not x: return ''
return author_to_author_sort(x.replace('|', ','))
class DBThread(Thread):
@ -121,7 +124,7 @@ class DBThread(Thread):
else:
self.conn.create_function('title_sort', 1, title_sort)
self.conn.create_function('author_to_author_sort', 1,
lambda x: author_to_author_sort(x.replace('|', ',')))
_author_to_author_sort)
self.conn.create_function('uuid4', 0, lambda : str(uuid.uuid4()))
# Dummy functions for dynamically created filters
self.conn.create_function('books_list_filter', 1, lambda x: 1)

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -596,10 +596,11 @@ class DNSIncoming(object):
next = off + 1
off = ((len & 0x3F) << 8) | ord(self.data[off])
if off >= first:
raise 'Bad domain name (circular) at ' + str(off)
raise ValueError('Bad domain name (circular) at ' +
str(off))
first = off
else:
raise 'Bad domain name at ' + str(off)
raise ValueError('Bad domain name at ' + str(off))
if next >= 0:
self.offset = next

View File

@ -103,6 +103,7 @@ _extra_lang_codes = {
'en_TH' : _('English (Thailand)'),
'en_CY' : _('English (Cyprus)'),
'en_PK' : _('English (Pakistan)'),
'en_IL' : _('English (Israel)'),
'en_SG' : _('English (Singapore)'),
'en_YE' : _('English (Yemen)'),
'en_IE' : _('English (Ireland)'),

View File

@ -788,6 +788,7 @@ class BasicNewsRecipe(Recipe):
}
.summary_byline {
text-align:left;
font-family:monospace;
}
@ -1139,12 +1140,6 @@ class BasicNewsRecipe(Recipe):
mi = MetaInformation(self.short_title() + strftime(self.timefmt), [__appname__])
mi.publisher = __appname__
mi.author_sort = __appname__
if self.output_profile.name == 'iPad':
date_as_author = '%s, %s %s, %s' % (strftime('%A'), strftime('%B'), strftime('%d').lstrip('0'), strftime('%Y'))
mi = MetaInformation(self.short_title(), [date_as_author])
mi.publisher = __appname__
sort_author = re.sub('^\s*A\s+|^\s*The\s+|^\s*An\s+', '', self.title).rstrip()
mi.author_sort = '%s %s' % (sort_author, strftime('%Y-%m-%d'))
mi.publication_type = 'periodical:'+self.publication_type
mi.timestamp = nowf()
mi.comments = self.description