Sync to trunk.
136
Changelog.yaml
@ -4,6 +4,142 @@
|
|||||||
# for important features/bug fixes.
|
# for important features/bug fixes.
|
||||||
# Also, each release can have new and improved recipes.
|
# Also, each release can have new and improved recipes.
|
||||||
|
|
||||||
|
- version: 0.7.4
|
||||||
|
date: 2010-06-19
|
||||||
|
|
||||||
|
bug fixes:
|
||||||
|
- title: "Fix regression in 0.7.3 that broke creating custom columns of rating or text types"
|
||||||
|
|
||||||
|
- title: "Fix cover browser breaking if you click on a book in the book list while cover browser is animated"
|
||||||
|
|
||||||
|
- title: "Fix a bug that could be triggered with the new book details pane if a book has a zero size cover"
|
||||||
|
tickets: [5889]
|
||||||
|
|
||||||
|
- title: "SONY driver: Fix bug preventing the editing of collections in the device view"
|
||||||
|
|
||||||
|
new recipes:
|
||||||
|
- title: Auto Prove
|
||||||
|
author: Gabriele Marini
|
||||||
|
|
||||||
|
- title: Forbes India, Maximum PC, Today Online
|
||||||
|
author: rty
|
||||||
|
|
||||||
|
improved recipes:
|
||||||
|
- WSJ
|
||||||
|
- Psychology Today
|
||||||
|
|
||||||
|
|
||||||
|
- 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
|
- version: 0.7.2
|
||||||
date: 2010-06-11
|
date: 2010-06-11
|
||||||
|
|
||||||
|
Before Width: | Height: | Size: 158 KiB After Width: | Height: | Size: 160 KiB |
@ -71,3 +71,5 @@ gui_pubdate_display_format = 'MMM yyyy'
|
|||||||
# order until the title is edited. Double-clicking on a title and hitting return
|
# order until the title is edited. Double-clicking on a title and hitting return
|
||||||
# without changing anything is sufficient to change the sort.
|
# without changing anything is sufficient to change the sort.
|
||||||
title_series_sorting = 'library_order'
|
title_series_sorting = 'library_order'
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
<svg
|
<svg
|
||||||
xmlns:i="http://ns.adobe.com/AdobeIllustrator/10.0/"
|
|
||||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||||
xmlns:cc="http://web.resource.org/cc/"
|
xmlns:cc="http://web.resource.org/cc/"
|
||||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
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:xlink="http://www.w3.org/1999/xlink"
|
||||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
width="128"
|
width="48"
|
||||||
height="128"
|
height="48"
|
||||||
id="svg2"
|
id="svg2"
|
||||||
sodipodi:version="0.32"
|
sodipodi:version="0.32"
|
||||||
inkscape:version="0.44.1"
|
inkscape:version="0.45"
|
||||||
version="1.0"
|
version="1.0"
|
||||||
sodipodi:docbase="/Users/david/Progetti/oxygen-svn/theme/svg/actions"
|
sodipodi:docbase="/home/dobey/Projects/gnome-icon-theme/scalable/apps"
|
||||||
sodipodi:docname="bookmark.svg">
|
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
|
<defs
|
||||||
id="defs4">
|
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
|
<linearGradient
|
||||||
inkscape:collect="always"
|
inkscape:collect="always"
|
||||||
id="linearGradient3765">
|
id="linearGradient2309">
|
||||||
<stop
|
<stop
|
||||||
style="stop-color:#ffffff;stop-opacity:1;"
|
style="stop-color:#ffffff;stop-opacity:1;"
|
||||||
offset="0"
|
offset="0"
|
||||||
id="stop3767" />
|
id="stop2311" />
|
||||||
<stop
|
<stop
|
||||||
style="stop-color:#ffffff;stop-opacity:0;"
|
style="stop-color:#ffffff;stop-opacity:0;"
|
||||||
offset="1"
|
offset="1"
|
||||||
id="stop3769" />
|
id="stop2313" />
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
<linearGradient
|
<linearGradient
|
||||||
inkscape:collect="always"
|
inkscape:collect="always"
|
||||||
id="linearGradient3747">
|
id="linearGradient2301">
|
||||||
<stop
|
<stop
|
||||||
style="stop-color:#ffffff;stop-opacity:1;"
|
style="stop-color:#790000;stop-opacity:1"
|
||||||
offset="0"
|
offset="0"
|
||||||
id="stop3749" />
|
id="stop2303" />
|
||||||
<stop
|
<stop
|
||||||
style="stop-color:#ffffff;stop-opacity:0;"
|
style="stop-color:#b03636;stop-opacity:1"
|
||||||
offset="1"
|
offset="1"
|
||||||
id="stop3751" />
|
id="stop2305" />
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
<linearGradient
|
<linearGradient
|
||||||
id="linearGradient3638">
|
inkscape:collect="always"
|
||||||
|
id="linearGradient2286">
|
||||||
<stop
|
<stop
|
||||||
style="stop-color:#ffffff;stop-opacity:0;"
|
style="stop-color:#555753"
|
||||||
offset="0"
|
offset="0"
|
||||||
id="stop3640" />
|
id="stop2288" />
|
||||||
<stop
|
<stop
|
||||||
id="stop3661"
|
style="stop-color:#555753;stop-opacity:0"
|
||||||
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;"
|
|
||||||
offset="1"
|
offset="1"
|
||||||
id="stop3642" />
|
id="stop2290" />
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
<linearGradient
|
<linearGradient
|
||||||
id="linearGradient1563">
|
inkscape:collect="always"
|
||||||
|
id="linearGradient2276">
|
||||||
<stop
|
<stop
|
||||||
id="stop1565"
|
style="stop-color:#babdb6;stop-opacity:1;"
|
||||||
offset="0"
|
offset="0"
|
||||||
style="stop-color:#ffffff;stop-opacity:1;" />
|
id="stop2278" />
|
||||||
<stop
|
<stop
|
||||||
id="stop1567"
|
style="stop-color:#8f9488;stop-opacity:1"
|
||||||
offset="1"
|
offset="1"
|
||||||
style="stop-color:white;stop-opacity:0;" />
|
id="stop2280" />
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
<linearGradient
|
<linearGradient
|
||||||
id="linearGradient3273">
|
inkscape:collect="always"
|
||||||
|
id="linearGradient2258">
|
||||||
<stop
|
<stop
|
||||||
id="stop3275"
|
style="stop-color:#ffa4a4;stop-opacity:1"
|
||||||
offset="0"
|
offset="0"
|
||||||
style="stop-color:#ffffff;stop-opacity:0.55035973;" />
|
id="stop2260" />
|
||||||
<stop
|
<stop
|
||||||
id="stop3277"
|
style="stop-color:#a40000"
|
||||||
offset="1"
|
offset="1"
|
||||||
style="stop-color:#ffffff;stop-opacity:0;" />
|
id="stop2262" />
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
<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">
|
inkscape:collect="always">
|
||||||
<stop
|
<stop
|
||||||
id="stop3293"
|
id="stop2223"
|
||||||
offset="0"
|
offset="0"
|
||||||
style="stop-color:#000000;stop-opacity:1;" />
|
style="stop-color:#babdb6" />
|
||||||
<stop
|
<stop
|
||||||
id="stop3295"
|
id="stop2225"
|
||||||
offset="1"
|
offset="1"
|
||||||
style="stop-color:#000000;stop-opacity:0;" />
|
style="stop-color:#d3d7cf;stop-opacity:0;" />
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
<linearGradient
|
<linearGradient
|
||||||
id="linearGradient12948">
|
inkscape:collect="always"
|
||||||
|
id="linearGradient2184">
|
||||||
<stop
|
<stop
|
||||||
style="stop-color:#ffffff;stop-opacity:1;"
|
style="stop-color:#ffffff;stop-opacity:1;"
|
||||||
offset="0"
|
offset="0"
|
||||||
id="stop12950" />
|
id="stop2186" />
|
||||||
<stop
|
<stop
|
||||||
style="stop-color:#c0c0c0;stop-opacity:0;"
|
style="stop-color:#e3e3e3;stop-opacity:1"
|
||||||
offset="1"
|
offset="1"
|
||||||
id="stop12952" />
|
id="stop2188" />
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
<linearGradient
|
<linearGradient
|
||||||
inkscape:collect="always"
|
inkscape:collect="always"
|
||||||
xlink:href="#linearGradient3273"
|
xlink:href="#linearGradient2229"
|
||||||
id="linearGradient3605"
|
id="linearGradient2211"
|
||||||
x1="80.100487"
|
x1="24"
|
||||||
y1="44.807674"
|
y1="19.505583"
|
||||||
x2="77.714729"
|
x2="19.982143"
|
||||||
y2="101.4734"
|
y2="19.550226"
|
||||||
gradientUnits="userSpaceOnUse"
|
gradientUnits="userSpaceOnUse"
|
||||||
gradientTransform="matrix(0.959962,0,0,0.959962,2.35549,3.275418)"
|
gradientTransform="matrix(-1,0,0,1,48,0)" />
|
||||||
spreadMethod="reflect" />
|
|
||||||
<linearGradient
|
<linearGradient
|
||||||
inkscape:collect="always"
|
inkscape:collect="always"
|
||||||
xlink:href="#linearGradient3638"
|
xlink:href="#linearGradient2221"
|
||||||
id="linearGradient3644"
|
id="linearGradient2219"
|
||||||
x1="57.287113"
|
x1="24"
|
||||||
y1="1.1597457"
|
y1="19.996655"
|
||||||
x2="144.2531"
|
x2="32"
|
||||||
y2="16.876789"
|
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" />
|
gradientUnits="userSpaceOnUse" />
|
||||||
<linearGradient
|
<linearGradient
|
||||||
inkscape:collect="always"
|
inkscape:collect="always"
|
||||||
xlink:href="#linearGradient3638"
|
xlink:href="#linearGradient2276"
|
||||||
id="linearGradient3646"
|
id="linearGradient2282"
|
||||||
x1="57.287113"
|
x1="37.535713"
|
||||||
y1="1.1597457"
|
y1="34.196426"
|
||||||
x2="144.2531"
|
x2="9.9285688"
|
||||||
y2="16.876789"
|
y2="20.089285"
|
||||||
gradientUnits="userSpaceOnUse" />
|
|
||||||
<linearGradient
|
|
||||||
inkscape:collect="always"
|
|
||||||
xlink:href="#linearGradient3638"
|
|
||||||
id="linearGradient3648"
|
|
||||||
x1="57.287113"
|
|
||||||
y1="1.1597457"
|
|
||||||
x2="144.2531"
|
|
||||||
y2="16.876789"
|
|
||||||
gradientUnits="userSpaceOnUse" />
|
gradientUnits="userSpaceOnUse" />
|
||||||
<radialGradient
|
<radialGradient
|
||||||
inkscape:collect="always"
|
inkscape:collect="always"
|
||||||
xlink:href="#linearGradient12948"
|
xlink:href="#linearGradient2286"
|
||||||
id="radialGradient3716"
|
id="radialGradient2292"
|
||||||
gradientUnits="userSpaceOnUse"
|
cx="24"
|
||||||
gradientTransform="matrix(1,-7.045514e-15,1.946707e-15,0.941176,2.788953e-13,3.492906)"
|
cy="36.75"
|
||||||
cx="23.190451"
|
fx="24"
|
||||||
cy="59.379417"
|
fy="36.75"
|
||||||
fx="22.471308"
|
r="22.5"
|
||||||
fy="59.354759"
|
gradientTransform="matrix(1,0,0,0.3,-3.16587e-17,25.725)"
|
||||||
r="2.1082227" />
|
|
||||||
<linearGradient
|
|
||||||
inkscape:collect="always"
|
|
||||||
xlink:href="#linearGradient1563"
|
|
||||||
id="linearGradient3732"
|
|
||||||
x1="98.291809"
|
|
||||||
y1="-126.7503"
|
|
||||||
x2="44.242641"
|
|
||||||
y2="101.45739"
|
|
||||||
gradientUnits="userSpaceOnUse" />
|
gradientUnits="userSpaceOnUse" />
|
||||||
<linearGradient
|
<linearGradient
|
||||||
inkscape:collect="always"
|
inkscape:collect="always"
|
||||||
xlink:href="#linearGradient1563"
|
xlink:href="#linearGradient2301"
|
||||||
id="linearGradient3739"
|
id="linearGradient2307"
|
||||||
gradientUnits="userSpaceOnUse"
|
x1="23.955357"
|
||||||
x1="98.291809"
|
y1="10.008928"
|
||||||
y1="-44.01474"
|
x2="29.214285"
|
||||||
x2="44.242641"
|
y2="30.276785"
|
||||||
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)"
|
|
||||||
gradientUnits="userSpaceOnUse" />
|
gradientUnits="userSpaceOnUse" />
|
||||||
<linearGradient
|
<linearGradient
|
||||||
inkscape:collect="always"
|
inkscape:collect="always"
|
||||||
xlink:href="#linearGradient3290"
|
xlink:href="#linearGradient2309"
|
||||||
id="linearGradient3106"
|
id="linearGradient2315"
|
||||||
x1="84.634949"
|
x1="6.7230334"
|
||||||
y1="116.10083"
|
y1="37.683041"
|
||||||
x2="89.72541"
|
x2="37.804565"
|
||||||
y2="-15.33666"
|
y2="29.096745"
|
||||||
gradientUnits="userSpaceOnUse" />
|
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>
|
</defs>
|
||||||
<sodipodi:namedview
|
<sodipodi:namedview
|
||||||
id="base"
|
id="base"
|
||||||
pagecolor="#ffffff"
|
pagecolor="#ffffff"
|
||||||
bordercolor="#666666"
|
bordercolor="#a8a8a8"
|
||||||
borderopacity="1.0"
|
borderopacity="1"
|
||||||
inkscape:pageopacity="0.0"
|
inkscape:pageopacity="0.0"
|
||||||
inkscape:pageshadow="2"
|
inkscape:pageshadow="2"
|
||||||
inkscape:zoom="4.8203125"
|
inkscape:zoom="7.919596"
|
||||||
inkscape:cx="64"
|
inkscape:cx="41.482905"
|
||||||
inkscape:cy="64"
|
inkscape:cy="24.425816"
|
||||||
inkscape:document-units="px"
|
inkscape:document-units="px"
|
||||||
inkscape:current-layer="layer1"
|
inkscape:current-layer="layer1"
|
||||||
inkscape:window-width="1247"
|
inkscape:showpageshadow="false"
|
||||||
inkscape:window-height="816"
|
inkscape:grid-bbox="true"
|
||||||
inkscape:window-x="388"
|
|
||||||
inkscape:window-y="110"
|
|
||||||
showgrid="true"
|
showgrid="true"
|
||||||
gridspacingx="4px"
|
inkscape:grid-points="true"
|
||||||
gridspacingy="4px"
|
gridspacingx="0.5px"
|
||||||
gridempspacing="0"
|
gridspacingy="0.5px"
|
||||||
inkscape:grid-points="true" />
|
gridempspacing="2"
|
||||||
|
inkscape:window-width="872"
|
||||||
|
inkscape:window-height="694"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="25"
|
||||||
|
fill="#75507b" />
|
||||||
<metadata
|
<metadata
|
||||||
id="metadata7">
|
id="metadata7">
|
||||||
<rdf:RDF>
|
<rdf:RDF>
|
||||||
@ -458,133 +249,108 @@
|
|||||||
<dc:format>image/svg+xml</dc:format>
|
<dc:format>image/svg+xml</dc:format>
|
||||||
<dc:type
|
<dc:type
|
||||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
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: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>
|
</rdf:RDF>
|
||||||
</metadata>
|
</metadata>
|
||||||
<g
|
<g
|
||||||
inkscape:label="Layer 1"
|
inkscape:label="Livello 1"
|
||||||
inkscape:groupmode="layer"
|
inkscape:groupmode="layer"
|
||||||
id="layer1">
|
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
|
<path
|
||||||
sodipodi:type="arc"
|
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"
|
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="path3289"
|
id="path2284"
|
||||||
sodipodi:cx="63.912209"
|
sodipodi:cx="24"
|
||||||
sodipodi:cy="115.70919"
|
sodipodi:cy="36.75"
|
||||||
sodipodi:rx="63.912209"
|
sodipodi:rx="22.5"
|
||||||
sodipodi:ry="12.641975"
|
sodipodi:ry="6.75"
|
||||||
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"
|
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.001374,0,0,0.410379,128,75.32738)" />
|
transform="matrix(1.066667,0,0,0.962963,-1.600001,1.111111)" />
|
||||||
<path
|
<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"
|
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 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"
|
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="path3632"
|
id="rect1304"
|
||||||
sodipodi:nodetypes="csss" />
|
sodipodi:nodetypes="ccccccccc" />
|
||||||
<path
|
<path
|
||||||
sodipodi:nodetypes="csss"
|
sodipodi:type="inkscape:offset"
|
||||||
id="path3634"
|
inkscape:radius="-0.91809106"
|
||||||
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"
|
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 "
|
||||||
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" />
|
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
|
<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"
|
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 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"
|
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="path3636"
|
id="path2180"
|
||||||
sodipodi:nodetypes="csss" />
|
sodipodi:nodetypes="cccccccc" />
|
||||||
<path
|
<path
|
||||||
sodipodi:type="arc"
|
sodipodi:type="inkscape:offset"
|
||||||
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"
|
inkscape:radius="-1.0582203"
|
||||||
id="path11160"
|
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 "
|
||||||
sodipodi:cx="23.190451"
|
xlink:href="#path2180"
|
||||||
sodipodi:cy="59.379417"
|
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"
|
||||||
sodipodi:rx="2.1082227"
|
id="path2266"
|
||||||
sodipodi:ry="1.9842097"
|
inkscape:href="#path2180"
|
||||||
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"
|
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 " />
|
||||||
transform="matrix(-1.742936,-1.063485,-0.470527,1.244278,191.1539,-3.699137)" />
|
|
||||||
<path
|
<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"
|
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 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 "
|
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="path3718" />
|
id="path2182"
|
||||||
|
sodipodi:nodetypes="ccccccc" />
|
||||||
<path
|
<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"
|
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 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 "
|
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="path3736" />
|
id="rect2192"
|
||||||
<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"
|
|
||||||
sodipodi:nodetypes="ccccc" />
|
sodipodi:nodetypes="ccccc" />
|
||||||
<path
|
<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"
|
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 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 "
|
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="path3296" />
|
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>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 28 KiB |
Before Width: | Height: | Size: 224 KiB After Width: | Height: | Size: 396 KiB |
BIN
resources/images/news/akter.png
Normal file
After Width: | Height: | Size: 429 B |
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 47 KiB |
Before Width: | Height: | Size: 156 KiB After Width: | Height: | Size: 140 KiB |
78
resources/recipes/akter.recipe
Normal 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
|
||||||
|
|
63
resources/recipes/auto.recipe
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
__license__ = 'GPL v3'
|
||||||
|
__author__ = 'GabrieleMarini, based on Darko Miletic'
|
||||||
|
__copyright__ = '2009, Darko Miletic <darko.miletic at gmail.com>, Gabriele Marini'
|
||||||
|
__version__ = 'v1.02 Marini Gabriele '
|
||||||
|
__date__ = '14062010'
|
||||||
|
__description__ = 'Italian daily newspaper'
|
||||||
|
|
||||||
|
'''
|
||||||
|
http://www.corrieredellosport.it/
|
||||||
|
'''
|
||||||
|
from calibre.web.feeds.news import BasicNewsRecipe
|
||||||
|
|
||||||
|
class Auto(BasicNewsRecipe):
|
||||||
|
__author__ = 'Gabriele Marini'
|
||||||
|
description = 'Auto and Formula 1'
|
||||||
|
|
||||||
|
cover_url = 'http://www.auto.it/res/imgs/logo_Auto.png'
|
||||||
|
|
||||||
|
|
||||||
|
title = u'Auto'
|
||||||
|
publisher = 'CONTE Editore'
|
||||||
|
category = 'Sport'
|
||||||
|
|
||||||
|
language = 'it'
|
||||||
|
timefmt = '[%a, %d %b, %Y]'
|
||||||
|
|
||||||
|
oldest_article = 60
|
||||||
|
max_articles_per_feed = 30
|
||||||
|
use_embedded_content = False
|
||||||
|
recursion = 10
|
||||||
|
|
||||||
|
remove_javascript = True
|
||||||
|
no_stylesheets = True
|
||||||
|
|
||||||
|
html2lrf_options = [
|
||||||
|
'--comment', description
|
||||||
|
, '--category', category
|
||||||
|
, '--publisher', publisher
|
||||||
|
, '--ignore-tables'
|
||||||
|
]
|
||||||
|
|
||||||
|
html2epub_options = 'publisher="' + publisher + '"\ncomments="' + description + '"\ntags="' + category + '"\nlinearize_tables=True'
|
||||||
|
|
||||||
|
keep_only_tags = [
|
||||||
|
dict(name='h2', attrs={'class':['tit_Article y_Txt']}),
|
||||||
|
dict(name='h2', attrs={'class':['tit_Article']}),
|
||||||
|
dict(name='div', attrs={'class':['box_Img newsdet_new ']}),
|
||||||
|
dict(name='div', attrs={'class':['box_Img newsdet_as ']}),
|
||||||
|
dict(name='table', attrs={'class':['table_A']}),
|
||||||
|
dict(name='div', attrs={'class':['txt_Article txtBox_cms']}),
|
||||||
|
dict(name='testoscheda')]
|
||||||
|
|
||||||
|
|
||||||
|
feeds = [
|
||||||
|
(u'Tutte le News' , u'http://www.auto.it/rss/articoli.xml' ),
|
||||||
|
(u'Prove su Strada' , u'http://www.auto.it/rss/prove+6.xml'),
|
||||||
|
(u'Novit\xe0' , u'http://www.auto.it/rss/novita+3.xml')
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
90
resources/recipes/auto_prove.recipe
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
__license__ = 'GPL v3'
|
||||||
|
__author__ = 'GabrieleMarini, based on Darko Miletic'
|
||||||
|
__copyright__ = '2009, Darko Miletic <darko.miletic at gmail.com>, Gabriele Marini'
|
||||||
|
__version__ = 'v1.02 Marini Gabriele '
|
||||||
|
__date__ = '10, January 2010'
|
||||||
|
__description__ = 'Italian daily newspaper'
|
||||||
|
|
||||||
|
'''
|
||||||
|
http://www.corrieredellosport.it/
|
||||||
|
'''
|
||||||
|
from calibre.web.feeds.news import BasicNewsRecipe
|
||||||
|
|
||||||
|
class AutoPR(BasicNewsRecipe):
|
||||||
|
__author__ = 'Gabriele Marini'
|
||||||
|
description = 'Auto and Formula 1'
|
||||||
|
|
||||||
|
cover_url = 'http://www.auto.it/res/imgs/logo_Auto.png'
|
||||||
|
|
||||||
|
|
||||||
|
title = u'Auto Prove'
|
||||||
|
publisher = 'CONTE Editore'
|
||||||
|
category = 'Sport'
|
||||||
|
|
||||||
|
language = 'it'
|
||||||
|
timefmt = '[%a, %d %b, %Y]'
|
||||||
|
|
||||||
|
oldest_article = 60
|
||||||
|
max_articles_per_feed = 20
|
||||||
|
use_embedded_content = False
|
||||||
|
recursion = 100
|
||||||
|
|
||||||
|
remove_javascript = True
|
||||||
|
no_stylesheets = True
|
||||||
|
|
||||||
|
#html2lrf_options = [
|
||||||
|
# '--comment', description
|
||||||
|
# , '--category', category
|
||||||
|
# , '--publisher', publisher
|
||||||
|
# , '--ignore-tables'
|
||||||
|
# ]
|
||||||
|
|
||||||
|
#html2epub_options = 'publisher="' + publisher + '"\ncomments="' + description + '"\ntags="' + category + '"\nlinearize_tables=True'
|
||||||
|
|
||||||
|
keep_only_tags = [
|
||||||
|
dict(name='h2', attrs={'class':['tit_Article y_Txt']}),
|
||||||
|
dict(name='h2', attrs={'class':['tit_Article']}),
|
||||||
|
dict(name='div', attrs={'class':['box_Img newsdet_new ']}),
|
||||||
|
dict(name='div', attrs={'class':['box_Img newsdet_as ']}),
|
||||||
|
dict(name='table', attrs={'class':['table_A']}),
|
||||||
|
dict(name='div', attrs={'class':['txt_Article txtBox_cms']}),
|
||||||
|
dict(name='testoscheda')]
|
||||||
|
|
||||||
|
def parse_index(self):
|
||||||
|
feeds = []
|
||||||
|
for title, url in [
|
||||||
|
("Prove su Strada" , "http://www.auto.it/rss/prove+6.xml")
|
||||||
|
]:
|
||||||
|
soup = self.index_to_soup(url)
|
||||||
|
soup = soup.find('channel')
|
||||||
|
print soup
|
||||||
|
|
||||||
|
for article in soup.findAllNext('item'):
|
||||||
|
title = self.tag_to_string(article.title)
|
||||||
|
date = self.tag_to_string(article.pubDate)
|
||||||
|
description = self.tag_to_string(article.description)
|
||||||
|
link = self.tag_to_string(article.guid)
|
||||||
|
# print article
|
||||||
|
articles = self.create_links_append(link, date, description)
|
||||||
|
if articles:
|
||||||
|
feeds.append((title, articles))
|
||||||
|
return feeds
|
||||||
|
|
||||||
|
def create_links_append(self, link, date, description):
|
||||||
|
current_articles = []
|
||||||
|
|
||||||
|
current_articles.append({'title': 'Generale', 'url': link,'description':description, 'date':date}),
|
||||||
|
current_articles.append({'title': 'Design', 'url': link.replace('scheda','design'),'description':'scheda', 'date':''}),
|
||||||
|
current_articles.append({'title': 'Interni', 'url': link.replace('scheda','interni'),'description':'Interni', 'date':''}),
|
||||||
|
current_articles.append({'title': 'Tecnica', 'url': link.replace('scheda','tecnica'),'description':'Tecnica', 'date':''}),
|
||||||
|
current_articles.append({'title': 'Su Strada', 'url': link.replace('scheda','su_strada'),'description':'Su Strada', 'date':''}),
|
||||||
|
current_articles.append({'title': 'Pagella', 'url': link.replace('scheda','pagella'),'description':'Pagella', 'date':''}),
|
||||||
|
current_articles.append({'title': 'Rilevamenti', 'url': link.replace('scheda','telemetria'),'description':'Rilevamenti', 'date':''})
|
||||||
|
|
||||||
|
return current_articles
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
60
resources/recipes/corriere_dello_sport.recipe
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
__license__ = 'GPL v3'
|
||||||
|
__author__ = 'GabrieleMarini, based on Darko Miletic'
|
||||||
|
__copyright__ = '2009, Darko Miletic <darko.miletic at gmail.com>, Gabriele Marini'
|
||||||
|
__version__ = ' '
|
||||||
|
__date__ = '14-06-2010'
|
||||||
|
__description__ = 'Italian daily newspaper'
|
||||||
|
|
||||||
|
'''
|
||||||
|
http://www.corrieredellosport.it/
|
||||||
|
'''
|
||||||
|
from calibre.web.feeds.news import BasicNewsRecipe
|
||||||
|
|
||||||
|
class ilCorrieredelloSport(BasicNewsRecipe):
|
||||||
|
__author__ = 'Gabriele Marini'
|
||||||
|
description = 'Italian daily newspaper'
|
||||||
|
|
||||||
|
cover_url = 'http://edicola.corrieredellosport.it/newsmem/corsport/prima/nazionale_prima.jpg'
|
||||||
|
|
||||||
|
|
||||||
|
title = u'Il Corriere dello Sport'
|
||||||
|
publisher = 'CORRIERE DELLO SPORT s.r.l. '
|
||||||
|
category = 'Sport'
|
||||||
|
|
||||||
|
language = 'it'
|
||||||
|
timefmt = '[%a, %d %b, %Y]'
|
||||||
|
|
||||||
|
oldest_article = 10
|
||||||
|
max_articles_per_feed = 100
|
||||||
|
use_embedded_content = False
|
||||||
|
recursion = 10
|
||||||
|
|
||||||
|
remove_javascript = True
|
||||||
|
no_stylesheets = True
|
||||||
|
|
||||||
|
html2lrf_options = [
|
||||||
|
'--comment', description
|
||||||
|
, '--category', category
|
||||||
|
, '--publisher', publisher
|
||||||
|
, '--ignore-tables'
|
||||||
|
]
|
||||||
|
|
||||||
|
html2epub_options = 'publisher="' + publisher + '"\ncomments="' + description + '"\ntags="' + category + '"\nlinearize_tables=True'
|
||||||
|
|
||||||
|
keep_only_tags = [
|
||||||
|
dict(name='h1', attrs={'class':['tit_Article']}),
|
||||||
|
dict(name='h1', attrs={'class':['tit_Article_mondiali']}),
|
||||||
|
dict(name='div', attrs={'class':['box_Img']}),
|
||||||
|
dict(name='p', attrs={'class':['summary','text']})]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
feeds = [
|
||||||
|
(u'Primo Piano' , u'http://www.corrieredellosport.it/rss/primo_piano.xml' ),
|
||||||
|
(u'Calcio' , u'http://www.corrieredellosport.it/rss/Calcio-3.xml'),
|
||||||
|
(u'Formula 1' , u'http://www.corrieredellosport.it/rss/Formula-1-7.xml'),
|
||||||
|
(u'Moto' , u'http://www.corrieredellosport.it/rss/Moto-8.xml'),
|
||||||
|
(u'Piu visti' , u'http://www.corrieredellosport.it/rss/piu_visti.xml')
|
||||||
|
]
|
@ -1,4 +1,3 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
__license__ = 'GPL v3'
|
__license__ = 'GPL v3'
|
||||||
__copyright__ = '2008-2010, Darko Miletic <darko.miletic at gmail.com>'
|
__copyright__ = '2008-2010, Darko Miletic <darko.miletic at gmail.com>'
|
||||||
'''
|
'''
|
||||||
@ -23,7 +22,14 @@ class Danas(BasicNewsRecipe):
|
|||||||
language = 'sr'
|
language = 'sr'
|
||||||
publication_type = 'newspaper'
|
publication_type = 'newspaper'
|
||||||
remove_empty_feeds = True
|
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 = {
|
conversion_options = {
|
||||||
'comment' : description
|
'comment' : description
|
||||||
@ -44,9 +50,9 @@ class Danas(BasicNewsRecipe):
|
|||||||
feeds = [
|
feeds = [
|
||||||
(u'Politika' , u'http://www.danas.rs/rss/rss.asp?column_id=27')
|
(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'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'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'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'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'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'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'Kultura' , u'http://www.danas.rs/rss/rss.asp?column_id=5' )
|
||||||
@ -55,6 +61,19 @@ class Danas(BasicNewsRecipe):
|
|||||||
,(u'Feljton' , u'http://www.danas.rs/rss/rss.asp?column_id=19')
|
,(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'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'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):
|
def preprocess_html(self, soup):
|
||||||
@ -65,3 +84,10 @@ class Danas(BasicNewsRecipe):
|
|||||||
def print_version(self, url):
|
def print_version(self, url):
|
||||||
return url + '&action=print'
|
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
|
||||||
|
55
resources/recipes/forbes_india.recipe
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
from calibre.ptempfile import PersistentTemporaryFile
|
||||||
|
from calibre.web.feeds.news import BasicNewsRecipe
|
||||||
|
|
||||||
|
class AdvancedUserRecipe1276934715(BasicNewsRecipe):
|
||||||
|
title = u'Forbes India'
|
||||||
|
__author__ = 'rty'
|
||||||
|
description = 'India Edition Forbes'
|
||||||
|
publisher = 'Forbes India'
|
||||||
|
category = 'Business News, Economy, India'
|
||||||
|
oldest_article = 7
|
||||||
|
max_articles_per_feed = 100
|
||||||
|
remove_javascript = True
|
||||||
|
use_embedded_content = False
|
||||||
|
no_stylesheets = True
|
||||||
|
language = 'en_IN'
|
||||||
|
temp_files = []
|
||||||
|
articles_are_obfuscated = True
|
||||||
|
conversion_options = {'linearize_tables':True}
|
||||||
|
feeds = [
|
||||||
|
(u'Contents', u'http://business.in.com/rssfeed/rss_all.xml'),
|
||||||
|
]
|
||||||
|
extra_css = '''
|
||||||
|
.t-10-gy-l{font-style: italic; font-size: small}
|
||||||
|
.t-30-b-d{font-weight: bold; font-size: xx-large}
|
||||||
|
.t-16-gy-l{font-weight: bold; font-size: x-large; font-syle: italic}
|
||||||
|
.storycontent{font-size: 4px;font-family: Times New Roman;}
|
||||||
|
'''
|
||||||
|
|
||||||
|
remove_tags_before = dict(name='div', attrs={'class':'pdl10 pdr15'})
|
||||||
|
|
||||||
|
|
||||||
|
def get_obfuscated_article(self, url):
|
||||||
|
br = self.get_browser()
|
||||||
|
br.open(url)
|
||||||
|
response = br.follow_link(url_regex = r'/printcontent/[0-9]+', nr = 0)
|
||||||
|
html = response.read()
|
||||||
|
self.temp_files.append(PersistentTemporaryFile('_fa.html'))
|
||||||
|
self.temp_files[-1].write(html)
|
||||||
|
self.temp_files[-1].close()
|
||||||
|
return self.temp_files[-1].name
|
||||||
|
|
||||||
|
def get_cover_url(self):
|
||||||
|
index = 'http://business.in.com/magazine/'
|
||||||
|
soup = self.index_to_soup(index)
|
||||||
|
for image in soup.findAll('a',{ "class" : "lbOn a-9-b-d" }):
|
||||||
|
return image['href']
|
||||||
|
#return image['href'] + '.jpg'
|
||||||
|
return None
|
||||||
|
|
||||||
|
def preprocess_html(self, soup):
|
||||||
|
for item in soup.findAll(style=True):
|
||||||
|
del item['style']
|
||||||
|
for item in soup.findAll(width=True):
|
||||||
|
del item['width']
|
||||||
|
return soup
|
@ -14,8 +14,8 @@ class LiberoNews(BasicNewsRecipe):
|
|||||||
__author__ = 'Marini Gabriele'
|
__author__ = 'Marini Gabriele'
|
||||||
description = 'Italian daily newspaper'
|
description = 'Italian daily newspaper'
|
||||||
|
|
||||||
cover_url = 'http://www.ilgiornale.it/img_v1/logo.gif'
|
cover_url = 'http://www.libero-news.it/images/logo.png'
|
||||||
title = u'Libero'
|
title = u'Libero '
|
||||||
publisher = 'EDITORIALE LIBERO s.r.l 2006'
|
publisher = 'EDITORIALE LIBERO s.r.l 2006'
|
||||||
category = 'News, politics, culture, economy, general interest'
|
category = 'News, politics, culture, economy, general interest'
|
||||||
|
|
||||||
|
38
resources/recipes/london_free_press.recipe
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
from calibre.web.feeds.news import BasicNewsRecipe
|
||||||
|
|
||||||
|
class LondonFreePress(BasicNewsRecipe):
|
||||||
|
title = u'London Free Press'
|
||||||
|
__author__ = 'rty'
|
||||||
|
oldest_article = 4
|
||||||
|
max_articles_per_feed = 100
|
||||||
|
|
||||||
|
pubisher = 'lfpress.com'
|
||||||
|
description = 'Ontario Canada Newspaper'
|
||||||
|
category = 'News, Ontario, Canada'
|
||||||
|
remove_javascript = True
|
||||||
|
use_embedded_content = False
|
||||||
|
no_stylesheets = True
|
||||||
|
language = 'en_CA'
|
||||||
|
encoding = 'utf-8'
|
||||||
|
conversion_options = {'linearize_tables':True}
|
||||||
|
|
||||||
|
feeds = [
|
||||||
|
(u'News', u'http://www.lfpress.com/news/rss.xml'),
|
||||||
|
(u'Comment', u'http://www.lfpress.com/comment/rss.xml'),
|
||||||
|
(u'Entertainment', u'http://www.lfpress.com/entertainment/rss.xml '),
|
||||||
|
(u'Money', u'http://www.lfpress.com/money/rss.xml '),
|
||||||
|
(u'Life', u'http://www.lfpress.com/life/rss.xml '),
|
||||||
|
(u'Sports', u'http://www.lfpress.com/sports/rss.xml ')
|
||||||
|
]
|
||||||
|
|
||||||
|
keep_only_tags = [
|
||||||
|
dict(name='div', attrs={'id':'article'}),
|
||||||
|
]
|
||||||
|
remove_tags = [
|
||||||
|
dict(name='div', attrs={'id':'commentsBottom'}),
|
||||||
|
dict(name='div', attrs={'class':['leftBox','bottomBox clear']}),
|
||||||
|
dict(name='ul', attrs={'class':'tabs dl contentSwap'}),
|
||||||
|
]
|
||||||
|
remove_tags_after = [
|
||||||
|
dict(name='div', attrs={'class':'bottomBox clear'}),
|
||||||
|
]
|
48
resources/recipes/losservatoreromano_it.recipe
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
__license__ = 'GPL v3'
|
||||||
|
__copyright__ = '2010, Darko Miletic <darko.miletic at gmail.com>'
|
||||||
|
'''
|
||||||
|
www.vatican.va/news_services/or/or_quo
|
||||||
|
'''
|
||||||
|
|
||||||
|
|
||||||
|
from calibre.web.feeds.news import BasicNewsRecipe
|
||||||
|
|
||||||
|
class LOsservatoreRomano_it(BasicNewsRecipe):
|
||||||
|
title = "L'Osservatore Romano"
|
||||||
|
__author__ = 'Darko Miletic'
|
||||||
|
description = 'Quiornale quotidiano, politico, religioso del Vaticano'
|
||||||
|
publisher = 'La Santa Sede'
|
||||||
|
category = 'news, politics, religion, Vatican'
|
||||||
|
no_stylesheets = True
|
||||||
|
INDEX = 'http://www.vatican.va'
|
||||||
|
FEEDPAGE = INDEX + '/news_services/or/or_quo/index.html'
|
||||||
|
CONTENTPAGE = INDEX + '/news_services/or/or_quo/text.html'
|
||||||
|
use_embedded_content = False
|
||||||
|
encoding = 'cp1252'
|
||||||
|
language = 'it'
|
||||||
|
publication_type = 'newspaper'
|
||||||
|
|
||||||
|
|
||||||
|
conversion_options = {
|
||||||
|
'comment' : description
|
||||||
|
, 'tags' : category
|
||||||
|
, 'publisher' : publisher
|
||||||
|
, 'language' : language
|
||||||
|
, 'linearize_tables' : True
|
||||||
|
}
|
||||||
|
|
||||||
|
def parse_index(self):
|
||||||
|
articles = []
|
||||||
|
articles.append({
|
||||||
|
'title' :self.title
|
||||||
|
,'date' :''
|
||||||
|
,'url' :self.CONTENTPAGE
|
||||||
|
,'description':''
|
||||||
|
})
|
||||||
|
return [(self.title, articles)]
|
||||||
|
|
||||||
|
def preprocess_html(self, soup):
|
||||||
|
for item in soup.findAll(style=True):
|
||||||
|
del item['style']
|
||||||
|
return self.adeify_images(soup)
|
||||||
|
|
43
resources/recipes/maximum_pc.recipe
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
from calibre.ptempfile import PersistentTemporaryFile
|
||||||
|
from calibre.web.feeds.news import BasicNewsRecipe
|
||||||
|
|
||||||
|
class AdvancedUserRecipe1276930924(BasicNewsRecipe):
|
||||||
|
title = u'Maximum PC'
|
||||||
|
__author__ = 'rty'
|
||||||
|
description = 'Maximum PC'
|
||||||
|
publisher = 'http://www.maximumpc.com'
|
||||||
|
category = 'news, computer, technology'
|
||||||
|
language = 'en'
|
||||||
|
oldest_article = 30
|
||||||
|
max_articles_per_feed = 100
|
||||||
|
remove_javascript = True
|
||||||
|
use_embedded_content = False
|
||||||
|
no_stylesheets = True
|
||||||
|
language = 'en'
|
||||||
|
temp_files = []
|
||||||
|
articles_are_obfuscated = True
|
||||||
|
feeds = [(u'News', u'http://www.maximumpc.com/articles/4/feed'),
|
||||||
|
(u'Reviews', u'http://www.maximumpc.com/articles/40/feed'),
|
||||||
|
(u'Editors Blog', u'http://www.maximumpc.com/articles/6/feed'),
|
||||||
|
(u'How-to', u'http://www.maximumpc.com/articles/32/feed'),
|
||||||
|
(u'Features', u'http://www.maximumpc.com/articles/31/feed'),
|
||||||
|
(u'From the Magazine', u'http://www.maximumpc.com/articles/72/feed')
|
||||||
|
]
|
||||||
|
keep_only_tags = [
|
||||||
|
dict(name='div', attrs={'class':['print-title','article_body']}),
|
||||||
|
]
|
||||||
|
remove_tags = [
|
||||||
|
dict(name='div', attrs={'class':'comments-tags-actions'}),
|
||||||
|
]
|
||||||
|
remove_tags_before = dict(name='div', attrs={'class':'print-title'})
|
||||||
|
remove_tags_after = dict(name='div', attrs={'class':'meta-content'})
|
||||||
|
|
||||||
|
def get_obfuscated_article(self, url):
|
||||||
|
br = self.get_browser()
|
||||||
|
br.open(url)
|
||||||
|
response = br.follow_link(url_regex = r'/print/[0-9]+', nr = 0)
|
||||||
|
html = response.read()
|
||||||
|
self.temp_files.append(PersistentTemporaryFile('_fa.html'))
|
||||||
|
self.temp_files[-1].write(html)
|
||||||
|
self.temp_files[-1].close()
|
||||||
|
return self.temp_files[-1].name
|
@ -17,6 +17,7 @@ class NYTimes(BasicNewsRecipe):
|
|||||||
title = 'New York Times Top Stories'
|
title = 'New York Times Top Stories'
|
||||||
__author__ = 'GRiker'
|
__author__ = 'GRiker'
|
||||||
language = 'en'
|
language = 'en'
|
||||||
|
requires_version = (0, 7, 3)
|
||||||
description = 'Top Stories from the New York Times'
|
description = 'Top Stories from the New York Times'
|
||||||
|
|
||||||
# List of sections typically included in Top Stories. Use a keyword from the
|
# List of sections typically included in Top Stories. Use a keyword from the
|
||||||
@ -64,6 +65,7 @@ class NYTimes(BasicNewsRecipe):
|
|||||||
timefmt = ''
|
timefmt = ''
|
||||||
needs_subscription = True
|
needs_subscription = True
|
||||||
masthead_url = 'http://graphics8.nytimes.com/images/misc/nytlogo379x64.gif'
|
masthead_url = 'http://graphics8.nytimes.com/images/misc/nytlogo379x64.gif'
|
||||||
|
cover_margins = (18,18,'grey99')
|
||||||
|
|
||||||
remove_tags_before = dict(id='article')
|
remove_tags_before = dict(id='article')
|
||||||
remove_tags_after = dict(id='article')
|
remove_tags_after = dict(id='article')
|
||||||
@ -183,6 +185,16 @@ class NYTimes(BasicNewsRecipe):
|
|||||||
self.log("\nFailed to login")
|
self.log("\nFailed to login")
|
||||||
return br
|
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):
|
def get_cover_url(self):
|
||||||
cover = None
|
cover = None
|
||||||
st = time.localtime()
|
st = time.localtime()
|
||||||
@ -391,14 +403,6 @@ class NYTimes(BasicNewsRecipe):
|
|||||||
return ans
|
return ans
|
||||||
|
|
||||||
def preprocess_html(self, soup):
|
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)
|
return self.strip_anchors(soup)
|
||||||
|
|
||||||
def postprocess_html(self,soup, True):
|
def postprocess_html(self,soup, True):
|
||||||
|
@ -20,6 +20,7 @@ class NYTimes(BasicNewsRecipe):
|
|||||||
title = 'The New York Times'
|
title = 'The New York Times'
|
||||||
__author__ = 'GRiker'
|
__author__ = 'GRiker'
|
||||||
language = 'en'
|
language = 'en'
|
||||||
|
requires_version = (0, 7, 3)
|
||||||
|
|
||||||
description = 'Daily news from the New York Times (subscription version)'
|
description = 'Daily news from the New York Times (subscription version)'
|
||||||
allSectionKeywords = ['The Front Page', 'International','National','Obituaries','Editorials',
|
allSectionKeywords = ['The Front Page', 'International','National','Obituaries','Editorials',
|
||||||
@ -103,6 +104,7 @@ class NYTimes(BasicNewsRecipe):
|
|||||||
]),
|
]),
|
||||||
dict(name=['script', 'noscript', 'style'])]
|
dict(name=['script', 'noscript', 'style'])]
|
||||||
masthead_url = 'http://graphics8.nytimes.com/images/misc/nytlogo379x64.gif'
|
masthead_url = 'http://graphics8.nytimes.com/images/misc/nytlogo379x64.gif'
|
||||||
|
cover_margins = (18,18,'grey99')
|
||||||
no_stylesheets = True
|
no_stylesheets = True
|
||||||
extra_css = '.headline {text-align: left;}\n \
|
extra_css = '.headline {text-align: left;}\n \
|
||||||
.byline {font-family: monospace; \
|
.byline {font-family: monospace; \
|
||||||
@ -158,7 +160,7 @@ class NYTimes(BasicNewsRecipe):
|
|||||||
return cover
|
return cover
|
||||||
|
|
||||||
def get_masthead_title(self):
|
def get_masthead_title(self):
|
||||||
return 'NYTimes GR Version'
|
return self.title
|
||||||
|
|
||||||
def dump_ans(self, ans):
|
def dump_ans(self, ans):
|
||||||
total_article_count = 0
|
total_article_count = 0
|
||||||
@ -279,15 +281,17 @@ class NYTimes(BasicNewsRecipe):
|
|||||||
self.dump_ans(ans)
|
self.dump_ans(ans)
|
||||||
return ans
|
return ans
|
||||||
|
|
||||||
def preprocess_html(self, soup):
|
def skip_ad_pages(self, soup):
|
||||||
# Skip ad pages served before actual article
|
# Skip ad pages served before actual article
|
||||||
skip_tag = soup.find(True, {'name':'skip'})
|
skip_tag = soup.find(True, {'name':'skip'})
|
||||||
if skip_tag is not None:
|
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 = 'http://www.nytimes.com' + re.sub(r'\?.*', '', skip_tag.parent['href'])
|
||||||
url += '?pagewanted=all'
|
url += '?pagewanted=all'
|
||||||
self.log.error("Skipping ad to article at '%s'" % url)
|
self.log.warn("Skipping ad to article at '%s'" % url)
|
||||||
soup = self.index_to_soup(url)
|
return self.index_to_soup(url, raw=True)
|
||||||
|
|
||||||
|
def preprocess_html(self, soup):
|
||||||
return self.strip_anchors(soup)
|
return self.strip_anchors(soup)
|
||||||
|
|
||||||
def postprocess_html(self,soup, True):
|
def postprocess_html(self,soup, True):
|
||||||
|
57
resources/recipes/people_daily.recipe
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
from calibre.web.feeds.news import BasicNewsRecipe
|
||||||
|
|
||||||
|
class AdvancedUserRecipe1277129332(BasicNewsRecipe):
|
||||||
|
title = u'People Daily - China'
|
||||||
|
oldest_article = 2
|
||||||
|
max_articles_per_feed = 100
|
||||||
|
__author__ = 'rty'
|
||||||
|
|
||||||
|
pubisher = 'people.com.cn'
|
||||||
|
description = 'People Daily Newspaper'
|
||||||
|
language = 'zh'
|
||||||
|
category = 'News, China'
|
||||||
|
remove_javascript = True
|
||||||
|
use_embedded_content = False
|
||||||
|
no_stylesheets = True
|
||||||
|
encoding = 'GB2312'
|
||||||
|
conversion_options = {'linearize_tables':True}
|
||||||
|
|
||||||
|
feeds = [(u'\u56fd\u5185\u65b0\u95fb', u'http://www.people.com.cn/rss/politics.xml'),
|
||||||
|
(u'\u56fd\u9645\u65b0\u95fb', u'http://www.people.com.cn/rss/world.xml'),
|
||||||
|
(u'\u7ecf\u6d4e\u65b0\u95fb', u'http://www.people.com.cn/rss/finance.xml'),
|
||||||
|
(u'\u4f53\u80b2\u65b0\u95fb', u'http://www.people.com.cn/rss/sports.xml'),
|
||||||
|
(u'\u53f0\u6e7e\u65b0\u95fb', u'http://www.people.com.cn/rss/haixia.xml')]
|
||||||
|
keep_only_tags = [
|
||||||
|
dict(name='div', attrs={'class':'left_content'}),
|
||||||
|
]
|
||||||
|
remove_tags = [
|
||||||
|
dict(name='table', attrs={'class':'title'}),
|
||||||
|
]
|
||||||
|
remove_tags_after = [
|
||||||
|
dict(name='table', attrs={'class':'bianji'}),
|
||||||
|
]
|
||||||
|
|
||||||
|
def append_page(self, soup, appendtag, position):
|
||||||
|
pager = soup.find('img',attrs={'src':'/img/next_b.gif'})
|
||||||
|
if pager:
|
||||||
|
nexturl = self.INDEX + pager.a['href']
|
||||||
|
soup2 = self.index_to_soup(nexturl)
|
||||||
|
texttag = soup2.find('div', attrs={'class':'left_content'})
|
||||||
|
#for it in texttag.findAll(style=True):
|
||||||
|
# del it['style']
|
||||||
|
newpos = len(texttag.contents)
|
||||||
|
self.append_page(soup2,texttag,newpos)
|
||||||
|
texttag.extract()
|
||||||
|
appendtag.insert(position,texttag)
|
||||||
|
|
||||||
|
|
||||||
|
def preprocess_html(self, soup):
|
||||||
|
mtag = '<meta http-equiv="content-type" content="text/html;charset=GB2312" />\n<meta http-equiv="content-language" content="utf-8" />'
|
||||||
|
soup.head.insert(0,mtag)
|
||||||
|
for item in soup.findAll(style=True):
|
||||||
|
del item['form']
|
||||||
|
self.append_page(soup, soup.body, 3)
|
||||||
|
#pager = soup.find('a',attrs={'class':'ab12'})
|
||||||
|
#if pager:
|
||||||
|
# pager.extract()
|
||||||
|
return soup
|
@ -1,39 +1,44 @@
|
|||||||
|
|
||||||
|
from calibre.ptempfile import PersistentTemporaryFile
|
||||||
from calibre.web.feeds.news import BasicNewsRecipe
|
from calibre.web.feeds.news import BasicNewsRecipe
|
||||||
from calibre.ebooks.BeautifulSoup import BeautifulSoup
|
|
||||||
|
|
||||||
class PsychologyToday(BasicNewsRecipe):
|
class AdvancedUserRecipe1275708473(BasicNewsRecipe):
|
||||||
title = u'Psychology Today'
|
title = u'Psychology Today'
|
||||||
|
_author__ = 'rty'
|
||||||
|
publisher = u'www.psychologytoday.com'
|
||||||
|
category = u'Psychology'
|
||||||
|
max_articles_per_feed = 100
|
||||||
|
remove_javascript = True
|
||||||
|
use_embedded_content = False
|
||||||
|
no_stylesheets = True
|
||||||
language = 'en'
|
language = 'en'
|
||||||
__author__ = 'Krittika Goyal'
|
temp_files = []
|
||||||
oldest_article = 1 #days
|
articles_are_obfuscated = True
|
||||||
max_articles_per_feed = 25
|
|
||||||
#encoding = 'latin1'
|
|
||||||
|
|
||||||
remove_stylesheets = True
|
|
||||||
#remove_tags_before = dict(name='h1', attrs={'class':'heading'})
|
|
||||||
#remove_tags_after = dict(name='td', attrs={'class':'newptool1'})
|
|
||||||
remove_tags = [
|
remove_tags = [
|
||||||
dict(name='iframe'),
|
dict(name='div', attrs={'class':['print-source_url','field-items','print-footer']}),
|
||||||
dict(name='div', attrs={'class':['pt-box-title', 'pt-box-content', 'blog-entry-footer', 'item-list', 'article-sub-meta']}),
|
dict(name='span', attrs={'class':'print-footnote'}),
|
||||||
dict(name='div', attrs={'id':['block-td_search_160', 'block-cam_search_160']}),
|
|
||||||
#dict(name='ul', attrs={'class':'article-tools'}),
|
|
||||||
#dict(name='ul', attrs={'class':'articleTools'}),
|
|
||||||
]
|
]
|
||||||
|
remove_tags_before = dict(name='h1', attrs={'class':'print-title'})
|
||||||
|
remove_tags_after = dict(name='div', attrs={'class':['field-items','print-footer']})
|
||||||
|
|
||||||
feeds = [
|
feeds = [(u'Contents', u'http://www.psychologytoday.com/articles/index.rss')]
|
||||||
('PSY TODAY',
|
|
||||||
'http://www.psychologytoday.com/articles/index.rss'),
|
|
||||||
]
|
|
||||||
|
|
||||||
def preprocess_html(self, soup):
|
def get_article_url(self, article):
|
||||||
story = soup.find(name='div', attrs={'id':'contentColumn'})
|
return article.get('link', None)
|
||||||
#td = heading.findParent(name='td')
|
|
||||||
#td.extract()
|
def get_obfuscated_article(self, url):
|
||||||
soup = BeautifulSoup('<html><head><title>t</title></head><body></body></html>')
|
br = self.get_browser()
|
||||||
body = soup.find(name='body')
|
br.open(url)
|
||||||
body.insert(0, story)
|
response = br.follow_link(url_regex = r'/print/[0-9]+', nr = 0)
|
||||||
for x in soup.findAll(name='p', text=lambda x:x and '-->' in x):
|
html = response.read()
|
||||||
p = x.findParent('p')
|
self.temp_files.append(PersistentTemporaryFile('_fa.html'))
|
||||||
if p is not None:
|
self.temp_files[-1].write(html)
|
||||||
p.extract()
|
self.temp_files[-1].close()
|
||||||
return soup
|
return self.temp_files[-1].name
|
||||||
|
|
||||||
|
def get_cover_url(self):
|
||||||
|
index = 'http://www.psychologytoday.com/magazine/'
|
||||||
|
soup = self.index_to_soup(index)
|
||||||
|
for image in soup.findAll('img',{ "class" : "imagefield imagefield-field_magazine_cover" }):
|
||||||
|
return image['src'] + '.jpg'
|
||||||
|
return None
|
||||||
|
@ -1,41 +1,43 @@
|
|||||||
"""
|
#!/usr/bin/env python
|
||||||
publico.py - v1.0
|
__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.
|
publico.pt
|
||||||
"""
|
'''
|
||||||
|
|
||||||
__license__ = 'GPL 3'
|
|
||||||
|
|
||||||
from calibre.web.feeds.news import BasicNewsRecipe
|
from calibre.web.feeds.news import BasicNewsRecipe
|
||||||
import re
|
|
||||||
|
|
||||||
class Publico(BasicNewsRecipe):
|
class PublicoPT(BasicNewsRecipe):
|
||||||
title = u'P\xfablico'
|
description = u'Jornal portugu\xeas'
|
||||||
__author__ = 'David Rodrigues'
|
cover_url = 'http://static.publico.pt/files/header/img/publico.gif'
|
||||||
oldest_article = 1
|
title = u'Publico.PT'
|
||||||
max_articles_per_feed = 30
|
category = 'News, politics, culture, economy, general interest'
|
||||||
encoding='utf-8'
|
oldest_article = 2
|
||||||
no_stylesheets = True
|
no_stylesheets = True
|
||||||
|
encoding = 'utf8'
|
||||||
|
use_embedded_content = False
|
||||||
language = 'pt'
|
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 = [
|
feeds = [
|
||||||
(u'Geral', u'http://feeds.feedburner.com/PublicoUltimaHora'),
|
(u'Geral', u'http://feeds.feedburner.com/publicoRSS'),
|
||||||
(u'Internacional', u'http://www.publico.clix.pt/rss.ashx?idCanal=11'),
|
(u'Mundo', u'http://feeds.feedburner.com/PublicoMundo'),
|
||||||
(u'Pol\xedtica', u'http://www.publico.clix.pt/rss.ashx?idCanal=12'),
|
(u'Pol\xedtica', u'http://feeds.feedburner.com/PublicoPolitica'),
|
||||||
(u'Ci\xcencias', u'http://www.publico.clix.pt/rss.ashx?idCanal=13'),
|
(u'Economia', u'http://feeds.feedburner.com/PublicoEconomia'),
|
||||||
(u'Desporto', u'http://desporto.publico.pt/rss.ashx'),
|
(u'Desporto', u'http://feeds.feedburner.com/PublicoDesporto'),
|
||||||
(u'Economia', u'http://www.publico.clix.pt/rss.ashx?idCanal=57'),
|
(u'Sociedade', u'http://feeds.feedburner.com/PublicoSociedade'),
|
||||||
(u'Educa\xe7\xe3o', u'http://www.publico.clix.pt/rss.ashx?idCanal=58'),
|
(u'Educa\xe7\xe3o', u'http://feeds.feedburner.com/PublicoEducacao'),
|
||||||
(u'Local', u'http://www.publico.clix.pt/rss.ashx?idCanal=59'),
|
(u'Ci\xeancias', u'http://feeds.feedburner.com/PublicoCiencias'),
|
||||||
(u'Media e Tecnologia', u'http://www.publico.clix.pt/rss.ashx?idCanal=61'),
|
(u'Ecosfera', u'http://feeds.feedburner.com/PublicoEcosfera'),
|
||||||
(u'Sociedade', u'http://www.publico.clix.pt/rss.ashx?idCanal=62')
|
(u'Cultura', u'http://feeds.feedburner.com/PublicoCultura'),
|
||||||
|
(u'Local', u'http://feeds.feedburner.com/PublicoLocal'),
|
||||||
|
(u'Tecnologia', u'http://feeds.feedburner.com/PublicoTecnologia')
|
||||||
]
|
]
|
||||||
remove_tags = [dict(name='script'), dict(id='linhaTitulosHeader')]
|
|
||||||
keep_only_tags = [dict(name='div')]
|
|
||||||
|
|
||||||
def print_version(self,url):
|
|
||||||
s=re.findall("id=[0-9]+",url);
|
|
||||||
return "http://ww2.publico.clix.pt/print.aspx?"+s[0]
|
|
||||||
|
@ -10,8 +10,10 @@ from calibre.web.feeds.news import BasicNewsRecipe
|
|||||||
class Slashdot(BasicNewsRecipe):
|
class Slashdot(BasicNewsRecipe):
|
||||||
title = u'Slashdot.org'
|
title = u'Slashdot.org'
|
||||||
description = '''Tech news. WARNING: This recipe downloads a lot
|
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
|
oldest_article = 7
|
||||||
|
simultaneous_downloads = 1
|
||||||
|
delay = 3
|
||||||
max_articles_per_feed = 100
|
max_articles_per_feed = 100
|
||||||
language = 'en'
|
language = 'en'
|
||||||
|
|
||||||
|
58
resources/recipes/thairath.recipe
Normal 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'}))
|
44
resources/recipes/the_nation_thai.recipe
Normal 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'}))
|
||||||
|
|
@ -16,7 +16,7 @@ class DailyTelegraph(BasicNewsRecipe):
|
|||||||
language = 'en_AU'
|
language = 'en_AU'
|
||||||
|
|
||||||
oldest_article = 2
|
oldest_article = 2
|
||||||
max_articles_per_feed = 10
|
max_articles_per_feed = 20
|
||||||
remove_javascript = True
|
remove_javascript = True
|
||||||
no_stylesheets = True
|
no_stylesheets = True
|
||||||
encoding = 'utf8'
|
encoding = 'utf8'
|
||||||
|
59
resources/recipes/today_online.recipe
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
from calibre.ptempfile import PersistentTemporaryFile
|
||||||
|
from calibre.web.feeds.news import BasicNewsRecipe
|
||||||
|
|
||||||
|
class AdvancedUserRecipe1276486274(BasicNewsRecipe):
|
||||||
|
title = u'Today Online - Singapore'
|
||||||
|
publisher = 'MediaCorp Press Ltd - Singapore'
|
||||||
|
__author__ = 'rty'
|
||||||
|
category = 'news, Singapore'
|
||||||
|
oldest_article = 7
|
||||||
|
max_articles_per_feed = 100
|
||||||
|
remove_javascript = True
|
||||||
|
use_embedded_content = False
|
||||||
|
no_stylesheets = True
|
||||||
|
language = 'en_SG'
|
||||||
|
temp_files = []
|
||||||
|
articles_are_obfuscated = True
|
||||||
|
masthead_url = 'http://www.todayonline.com/App_Themes/Default/images/icons/TodayOnlineLogo.gif'
|
||||||
|
conversion_options = {'linearize_tables':True}
|
||||||
|
extra_css = '''
|
||||||
|
.author{font-style: italic; font-size: small}
|
||||||
|
.date{font-style: italic; font-size: small}
|
||||||
|
.Headline{font-weight: bold; font-size: xx-large}
|
||||||
|
.headerStrap{font-weight: bold; font-size: x-large; font-syle: italic}
|
||||||
|
.bodyText{font-size: 4px;font-family: Times New Roman;}
|
||||||
|
'''
|
||||||
|
keep_only_tags = [
|
||||||
|
dict(name='div', attrs={'id':['fullPrintBodyHolder']})
|
||||||
|
]
|
||||||
|
remove_tags_after = [ dict(name='div', attrs={'class':'button'})]
|
||||||
|
|
||||||
|
|
||||||
|
remove_tags = [
|
||||||
|
dict(name='div', attrs={'class':['url','button']})
|
||||||
|
]
|
||||||
|
feeds = [
|
||||||
|
(u'Singapore', u'http://www.todayonline.com/RSS/Singapore'),
|
||||||
|
(u'Hot News', u'http://www.todayonline.com/RSS/Hotnews'),
|
||||||
|
(u'Today Online', u'http://www.todayonline.com/RSS/Todayonline'),
|
||||||
|
(u'Voices', u'http://www.todayonline.com/RSS/Voices'),
|
||||||
|
(u'Commentary', u'http://www.todayonline.com/RSS/Commentary'),
|
||||||
|
(u'World', u'http://www.todayonline.com/RSS/World'),
|
||||||
|
(u'Business', u'http://www.todayonline.com/RSS/Business'),
|
||||||
|
(u'Column', u'http://www.todayonline.com/RSS/Columns'),
|
||||||
|
]
|
||||||
|
|
||||||
|
def get_obfuscated_article(self, url):
|
||||||
|
br = self.get_browser()
|
||||||
|
br.open(url)
|
||||||
|
response = br.follow_link(url_regex = r'/Print/', nr = 0)
|
||||||
|
html = response.read()
|
||||||
|
self.temp_files.append(PersistentTemporaryFile('_fa.html'))
|
||||||
|
self.temp_files[-1].write(html)
|
||||||
|
self.temp_files[-1].close()
|
||||||
|
return self.temp_files[-1].name
|
||||||
|
|
||||||
|
def preprocess_html(self, soup):
|
||||||
|
for item in soup.findAll(style=True):
|
||||||
|
del item['style']
|
||||||
|
return soup
|
@ -3,16 +3,15 @@ __license__ = 'GPL v3'
|
|||||||
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
|
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
|
||||||
__docformat__ = 'restructuredtext en'
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
|
|
||||||
from calibre.web.feeds.news import BasicNewsRecipe
|
from calibre.web.feeds.news import BasicNewsRecipe
|
||||||
from calibre import strftime
|
import copy
|
||||||
|
|
||||||
# http://online.wsj.com/page/us_in_todays_paper.html
|
# http://online.wsj.com/page/us_in_todays_paper.html
|
||||||
|
|
||||||
class WallStreetJournal(BasicNewsRecipe):
|
class WallStreetJournal(BasicNewsRecipe):
|
||||||
|
|
||||||
title = 'The Wall Street Journal (US)'
|
title = 'The Wall Street Journal'
|
||||||
__author__ = 'Kovid Goyal and Sujata Raman'
|
__author__ = 'Kovid Goyal, Sujata Raman, and Joshua Oster-Morris'
|
||||||
description = 'News and current affairs'
|
description = 'News and current affairs'
|
||||||
needs_subscription = True
|
needs_subscription = True
|
||||||
language = 'en'
|
language = 'en'
|
||||||
@ -67,62 +66,109 @@ class WallStreetJournal(BasicNewsRecipe):
|
|||||||
return soup
|
return soup
|
||||||
|
|
||||||
def wsj_get_index(self):
|
def wsj_get_index(self):
|
||||||
return self.index_to_soup('http://online.wsj.com/page/us_in_todays_paper.html')
|
return self.index_to_soup('http://online.wsj.com/itp')
|
||||||
|
|
||||||
|
def wsj_add_feed(self,feeds,title,url):
|
||||||
|
self.log('Found section:', title)
|
||||||
|
if url.endswith('whatsnews'):
|
||||||
|
articles = self.wsj_find_wn_articles(url)
|
||||||
|
else:
|
||||||
|
articles = self.wsj_find_articles(url)
|
||||||
|
if articles:
|
||||||
|
feeds.append((title, articles))
|
||||||
|
return feeds
|
||||||
|
|
||||||
def parse_index(self):
|
def parse_index(self):
|
||||||
soup = self.wsj_get_index()
|
soup = self.wsj_get_index()
|
||||||
|
|
||||||
year = strftime('%Y')
|
date = soup.find('span', attrs={'class':'date-date'})
|
||||||
for x in soup.findAll('td', height='25', attrs={'class':'b14'}):
|
if date is not None:
|
||||||
txt = self.tag_to_string(x).strip()
|
self.timefmt = ' [%s]'%self.tag_to_string(date)
|
||||||
txt = txt.replace(u'\xa0', ' ')
|
|
||||||
txt = txt.encode('ascii', 'ignore')
|
|
||||||
if year in txt:
|
|
||||||
self.timefmt = ' [%s]'%txt
|
|
||||||
break
|
|
||||||
|
|
||||||
left_column = soup.find(
|
cov = soup.find('a', attrs={'class':'icon pdf'}, href=True)
|
||||||
text=lambda t: 'begin ITP Left Column' in str(t))
|
if cov is not None:
|
||||||
|
self.cover_url = cov['href']
|
||||||
|
|
||||||
table = left_column.findNext('table')
|
|
||||||
|
|
||||||
current_section = None
|
|
||||||
current_articles = []
|
|
||||||
feeds = []
|
feeds = []
|
||||||
for x in table.findAllNext(True):
|
div = soup.find('div', attrs={'class':'itpHeader'})
|
||||||
if x.name == 'td' and x.get('class', None) == 'b13':
|
div = div.find('ul', attrs={'class':'tab'})
|
||||||
if current_articles and current_section:
|
for a in div.findAll('a', href=lambda x: x and '/itp/' in x):
|
||||||
feeds.append((current_section, current_articles))
|
pageone = a['href'].endswith('pageone')
|
||||||
current_section = self.tag_to_string(x.a).strip()
|
if pageone:
|
||||||
current_articles = []
|
title = 'Front Section'
|
||||||
self.log('\tProcessing section:', current_section)
|
url = 'http://online.wsj.com' + a['href']
|
||||||
if current_section is not None and x.name == 'a' and \
|
feeds = self.wsj_add_feed(feeds,title,url)
|
||||||
x.get('class', None) == 'bold80':
|
title = 'What''s News'
|
||||||
title = self.tag_to_string(x)
|
url = url.replace('pageone','whatsnews')
|
||||||
url = x.get('href', False)
|
feeds = self.wsj_add_feed(feeds,title,url)
|
||||||
if not url or not title:
|
else:
|
||||||
continue
|
title = self.tag_to_string(a)
|
||||||
url = url.partition('#')[0]
|
url = 'http://online.wsj.com' + a['href']
|
||||||
|
feeds = self.wsj_add_feed(feeds,title,url)
|
||||||
|
return feeds
|
||||||
|
|
||||||
|
def wsj_find_wn_articles(self, url):
|
||||||
|
soup = self.index_to_soup(url)
|
||||||
|
articles = []
|
||||||
|
|
||||||
|
whats_news = soup.find('div', attrs={'class':lambda x: x and 'whatsNews-simple' in x})
|
||||||
|
if whats_news is not None:
|
||||||
|
for a in whats_news.findAll('a', href=lambda x: x and '/article/' in x):
|
||||||
|
container = a.findParent(['p'])
|
||||||
|
meta = a.find(attrs={'class':'meta_sectionName'})
|
||||||
|
if meta is not None:
|
||||||
|
meta.extract()
|
||||||
|
title = self.tag_to_string(a).strip()
|
||||||
|
url = a['href']
|
||||||
desc = ''
|
desc = ''
|
||||||
d = x.findNextSibling(True)
|
if container is not None:
|
||||||
if d is not None and d.get('class', None) == 'arialResize':
|
desc = self.tag_to_string(container)
|
||||||
desc = self.tag_to_string(d)
|
|
||||||
desc = desc.partition(u'\u2022')[0]
|
articles.append({'title':title, 'url':url,
|
||||||
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':''})
|
'description':desc, 'date':''})
|
||||||
|
|
||||||
if current_articles and current_section:
|
self.log('\tFound WN article:', title)
|
||||||
feeds.append((current_section, current_articles))
|
|
||||||
|
return articles
|
||||||
|
|
||||||
|
def wsj_find_articles(self, url):
|
||||||
|
soup = self.index_to_soup(url)
|
||||||
|
|
||||||
|
whats_news = soup.find('div', attrs={'class':lambda x: x and 'whatsNews-simple' in x})
|
||||||
|
if whats_news is not None:
|
||||||
|
whats_news.extract()
|
||||||
|
|
||||||
|
articles = []
|
||||||
|
|
||||||
|
flavorarea = soup.find('div', attrs={'class':lambda x: x and 'ahed' in x})
|
||||||
|
if flavorarea is not None:
|
||||||
|
flavorstory = flavorarea.find('a', href=lambda x: x and x.startswith('/article'))
|
||||||
|
if flavorstory is not None:
|
||||||
|
flavorstory['class'] = 'mjLinkItem'
|
||||||
|
metapage = soup.find('span', attrs={'class':lambda x: x and 'meta_sectionName' in x})
|
||||||
|
if metapage is not None:
|
||||||
|
flavorstory.append( copy.copy(metapage) ) #metapage should always be A1 because that should be first on the page
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
return articles
|
||||||
|
|
||||||
return feeds
|
|
||||||
|
|
||||||
def cleanup(self):
|
def cleanup(self):
|
||||||
self.browser.open('http://online.wsj.com/logout?url=http://online.wsj.com')
|
self.browser.open('http://online.wsj.com/logout?url=http://online.wsj.com')
|
||||||
|
|
||||||
|
|
||||||
|
BIN
resources/tracer.epub
Normal file
@ -2,7 +2,7 @@ __license__ = 'GPL v3'
|
|||||||
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
|
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
|
||||||
__docformat__ = 'restructuredtext en'
|
__docformat__ = 'restructuredtext en'
|
||||||
__appname__ = 'calibre'
|
__appname__ = 'calibre'
|
||||||
__version__ = '0.7.2'
|
__version__ = '0.7.4'
|
||||||
__author__ = "Kovid Goyal <kovid@kovidgoyal.net>"
|
__author__ = "Kovid Goyal <kovid@kovidgoyal.net>"
|
||||||
|
|
||||||
import re
|
import re
|
||||||
|
@ -436,7 +436,7 @@ from calibre.devices.blackberry.driver import BLACKBERRY
|
|||||||
from calibre.devices.cybook.driver import CYBOOK
|
from calibre.devices.cybook.driver import CYBOOK
|
||||||
from calibre.devices.eb600.driver import EB600, COOL_ER, SHINEBOOK, \
|
from calibre.devices.eb600.driver import EB600, COOL_ER, SHINEBOOK, \
|
||||||
POCKETBOOK360, GER2, ITALICA, ECLICTO, DBOOK, INVESBOOK, \
|
POCKETBOOK360, GER2, ITALICA, ECLICTO, DBOOK, INVESBOOK, \
|
||||||
BOOQ, ELONEX
|
BOOQ, ELONEX, POCKETBOOK301
|
||||||
from calibre.devices.iliad.driver import ILIAD
|
from calibre.devices.iliad.driver import ILIAD
|
||||||
from calibre.devices.irexdr.driver import IREXDR1000, IREXDR800
|
from calibre.devices.irexdr.driver import IREXDR1000, IREXDR800
|
||||||
from calibre.devices.jetbook.driver import JETBOOK
|
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.folder_device.driver import FOLDER_DEVICE_FOR_CONFIG
|
||||||
from calibre.devices.kobo.driver import KOBO
|
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
|
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 += [
|
plugins += [
|
||||||
ComicInput,
|
ComicInput,
|
||||||
EPUBInput,
|
EPUBInput,
|
||||||
@ -507,6 +510,7 @@ plugins += [
|
|||||||
JETBOOK,
|
JETBOOK,
|
||||||
SHINEBOOK,
|
SHINEBOOK,
|
||||||
POCKETBOOK360,
|
POCKETBOOK360,
|
||||||
|
POCKETBOOK301,
|
||||||
KINDLE,
|
KINDLE,
|
||||||
KINDLE2,
|
KINDLE2,
|
||||||
KINDLE_DX,
|
KINDLE_DX,
|
||||||
|
@ -279,6 +279,7 @@ class KoboReaderOutput(OutputProfile):
|
|||||||
description = _('This profile is intended for the Kobo Reader.')
|
description = _('This profile is intended for the Kobo Reader.')
|
||||||
|
|
||||||
screen_size = (590, 775)
|
screen_size = (590, 775)
|
||||||
|
comic_screen_size = (540, 718)
|
||||||
dpi = 168.451
|
dpi = 168.451
|
||||||
fbase = 12
|
fbase = 12
|
||||||
fsizes = [7.5, 9, 10, 12, 15.5, 20, 22, 24]
|
fsizes = [7.5, 9, 10, 12, 15.5, 20, 22, 24]
|
||||||
|
@ -21,7 +21,7 @@ from calibre.utils.config import make_config_dir, Config, ConfigProxy, \
|
|||||||
platform = 'linux'
|
platform = 'linux'
|
||||||
if iswindows:
|
if iswindows:
|
||||||
platform = 'windows'
|
platform = 'windows'
|
||||||
if isosx:
|
elif isosx:
|
||||||
platform = 'osx'
|
platform = 'osx'
|
||||||
|
|
||||||
from zipfile import ZipFile
|
from zipfile import ZipFile
|
||||||
@ -32,19 +32,25 @@ def _config():
|
|||||||
c.add_opt('filetype_mapping', default={}, help=_('Mapping for filetype plugins'))
|
c.add_opt('filetype_mapping', default={}, help=_('Mapping for filetype plugins'))
|
||||||
c.add_opt('plugin_customization', default={}, help=_('Local plugin customization'))
|
c.add_opt('plugin_customization', default={}, help=_('Local plugin customization'))
|
||||||
c.add_opt('disabled_plugins', default=set([]), help=_('Disabled plugins'))
|
c.add_opt('disabled_plugins', default=set([]), help=_('Disabled plugins'))
|
||||||
|
c.add_opt('enabled_plugins', default=set([]), help=_('Enabled plugins'))
|
||||||
|
|
||||||
return ConfigProxy(c)
|
return ConfigProxy(c)
|
||||||
|
|
||||||
config = _config()
|
config = _config()
|
||||||
|
|
||||||
|
|
||||||
class InvalidPlugin(ValueError):
|
class InvalidPlugin(ValueError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
class PluginNotFound(ValueError):
|
class PluginNotFound(ValueError):
|
||||||
pass
|
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
|
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)
|
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_import = {}
|
||||||
_on_preprocess = {}
|
_on_preprocess = {}
|
||||||
_on_postprocess = {}
|
_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():
|
def input_profiles():
|
||||||
for plugin in _initialized_plugins:
|
for plugin in _initialized_plugins:
|
||||||
if isinstance(plugin, InputProfile):
|
if isinstance(plugin, InputProfile):
|
||||||
@ -90,7 +205,9 @@ def output_profiles():
|
|||||||
for plugin in _initialized_plugins:
|
for plugin in _initialized_plugins:
|
||||||
if isinstance(plugin, OutputProfile):
|
if isinstance(plugin, OutputProfile):
|
||||||
yield plugin
|
yield plugin
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
# Metadata sources {{{
|
||||||
def metadata_sources(metadata_type='basic', customize=True, isbndb_key=None):
|
def metadata_sources(metadata_type='basic', customize=True, isbndb_key=None):
|
||||||
for plugin in _initialized_plugins:
|
for plugin in _initialized_plugins:
|
||||||
if isinstance(plugin, MetadataSource) and \
|
if isinstance(plugin, MetadataSource) and \
|
||||||
@ -117,31 +234,9 @@ def migrate_isbndb_key():
|
|||||||
if key:
|
if key:
|
||||||
prefs.set('isbndb_com_key', '')
|
prefs.set('isbndb_com_key', '')
|
||||||
set_isbndb_key(key)
|
set_isbndb_key(key)
|
||||||
|
# }}}
|
||||||
|
|
||||||
def reread_filetype_plugins():
|
# Metadata read/write {{{
|
||||||
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_readers = {}
|
_metadata_readers = {}
|
||||||
_metadata_writers = {}
|
_metadata_writers = {}
|
||||||
def reread_metadata_plugins():
|
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', ''))
|
print 'Failed to set metadata for', repr(getattr(mi, 'title', ''))
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
|
|
||||||
|
# }}}
|
||||||
|
|
||||||
def _run_filetype_plugins(path_to_file, ft=None, occasion='preprocess'):
|
# Add/remove plugins {{{
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||
def add_plugin(path_to_zip_file):
|
def add_plugin(path_to_zip_file):
|
||||||
make_config_dir()
|
make_config_dir()
|
||||||
@ -307,14 +360,9 @@ def remove_plugin(plugin_or_name):
|
|||||||
initialize_plugins()
|
initialize_plugins()
|
||||||
return removed
|
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():
|
def input_format_plugins():
|
||||||
for plugin in _initialized_plugins:
|
for plugin in _initialized_plugins:
|
||||||
@ -364,6 +412,9 @@ def available_output_formats():
|
|||||||
formats.add(plugin.file_type)
|
formats.add(plugin.file_type)
|
||||||
return formats
|
return formats
|
||||||
|
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
# Catalog plugins {{{
|
||||||
|
|
||||||
def catalog_plugins():
|
def catalog_plugins():
|
||||||
for plugin in _initialized_plugins:
|
for plugin in _initialized_plugins:
|
||||||
@ -383,27 +434,32 @@ def plugin_for_catalog_format(fmt):
|
|||||||
if fmt.lower() in plugin.file_types:
|
if fmt.lower() in plugin.file_types:
|
||||||
return plugin
|
return plugin
|
||||||
|
|
||||||
def device_plugins():
|
# }}}
|
||||||
|
|
||||||
|
def device_plugins(): # {{{
|
||||||
for plugin in _initialized_plugins:
|
for plugin in _initialized_plugins:
|
||||||
if isinstance(plugin, DevicePlugin):
|
if isinstance(plugin, DevicePlugin):
|
||||||
if not is_disabled(plugin):
|
if not is_disabled(plugin):
|
||||||
|
if platform in plugin.supported_platforms:
|
||||||
yield plugin
|
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):
|
# Initialize plugins {{{
|
||||||
x = getattr(plugin_or_name, 'name', plugin_or_name)
|
|
||||||
dp = config['disabled_plugins']
|
_initialized_plugins = []
|
||||||
if x in dp:
|
|
||||||
dp.remove(x)
|
def initialize_plugin(plugin, path_to_zip_file):
|
||||||
config['disabled_plugins'] = dp
|
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():
|
def initialize_plugins():
|
||||||
global _initialized_plugins
|
global _initialized_plugins
|
||||||
@ -425,10 +481,14 @@ def initialize_plugins():
|
|||||||
|
|
||||||
initialize_plugins()
|
initialize_plugins()
|
||||||
|
|
||||||
def intialized_plugins():
|
def initialized_plugins():
|
||||||
for plugin in _initialized_plugins:
|
for plugin in _initialized_plugins:
|
||||||
yield plugin
|
yield plugin
|
||||||
|
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
# CLI {{{
|
||||||
|
|
||||||
def option_parser():
|
def option_parser():
|
||||||
parser = OptionParser(usage=_('''\
|
parser = OptionParser(usage=_('''\
|
||||||
%prog options
|
%prog options
|
||||||
@ -449,17 +509,6 @@ def option_parser():
|
|||||||
help=_('Disable the named plugin'))
|
help=_('Disable the named plugin'))
|
||||||
return parser
|
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):
|
def main(args=sys.argv):
|
||||||
parser = option_parser()
|
parser = option_parser()
|
||||||
if len(args) < 2:
|
if len(args) < 2:
|
||||||
@ -504,3 +553,5 @@ def main(args=sys.argv):
|
|||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
sys.exit(main())
|
sys.exit(main())
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
@ -30,7 +30,7 @@ class ANDROID(USBMS):
|
|||||||
0x18d1 : { 0x4e11 : [0x0100, 0x226], 0x4e12: [0x0100, 0x226]},
|
0x18d1 : { 0x4e11 : [0x0100, 0x226], 0x4e12: [0x0100, 0x226]},
|
||||||
|
|
||||||
# Samsung
|
# Samsung
|
||||||
0x04e8 : { 0x681d : [0x0222], 0x681c : [0x0222, 0x0224]},
|
0x04e8 : { 0x681d : [0x0222, 0x0400], 0x681c : [0x0222, 0x0224]},
|
||||||
|
|
||||||
# Acer
|
# Acer
|
||||||
0x502 : { 0x3203 : [0x0100]},
|
0x502 : { 0x3203 : [0x0100]},
|
||||||
@ -41,10 +41,12 @@ class ANDROID(USBMS):
|
|||||||
'be used')
|
'be used')
|
||||||
EXTRA_CUSTOMIZATION_DEFAULT = ', '.join(EBOOK_DIR_MAIN)
|
EXTRA_CUSTOMIZATION_DEFAULT = ', '.join(EBOOK_DIR_MAIN)
|
||||||
|
|
||||||
VENDOR_NAME = ['HTC', 'MOTOROLA', 'GOOGLE_', 'ANDROID', 'ACER', 'GT-I5700']
|
VENDOR_NAME = ['HTC', 'MOTOROLA', 'GOOGLE_', 'ANDROID', 'ACER',
|
||||||
|
'GT-I5700', 'SAMSUNG']
|
||||||
WINDOWS_MAIN_MEM = ['ANDROID_PHONE', 'A855', 'A853', 'INC.NEXUS_ONE',
|
WINDOWS_MAIN_MEM = ['ANDROID_PHONE', 'A855', 'A853', 'INC.NEXUS_ONE',
|
||||||
'__UMS_COMPOSITE', '_MB200', 'MASS_STORAGE', '_-_CARD']
|
'__UMS_COMPOSITE', '_MB200', 'MASS_STORAGE', '_-_CARD',
|
||||||
WINDOWS_CARD_A_MEM = ['ANDROID_PHONE']
|
'PROD_GT-I9000']
|
||||||
|
WINDOWS_CARD_A_MEM = ['ANDROID_PHONE', 'PROD_GT-I9000_CARD']
|
||||||
|
|
||||||
OSX_MAIN_MEM = 'HTC Android Phone Media'
|
OSX_MAIN_MEM = 'HTC Android Phone Media'
|
||||||
|
|
||||||
|
@ -201,4 +201,21 @@ class ELONEX(EB600):
|
|||||||
def can_handle(cls, dev, debug=False):
|
def can_handle(cls, dev, debug=False):
|
||||||
return dev[3] == 'Elonex' and dev[4] == 'eBook'
|
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]
|
||||||
|
|
||||||
|
|
||||||
|
@ -81,9 +81,6 @@ class HANLINV3(USBMS):
|
|||||||
return drives
|
return drives
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class HANLINV5(HANLINV3):
|
class HANLINV5(HANLINV3):
|
||||||
name = 'Hanlin V5 driver'
|
name = 'Hanlin V5 driver'
|
||||||
gui_name = 'Hanlin V5'
|
gui_name = 'Hanlin V5'
|
||||||
@ -120,8 +117,22 @@ class BOOX(HANLINV3):
|
|||||||
MAIN_MEMORY_VOLUME_LABEL = 'BOOX Internal Memory'
|
MAIN_MEMORY_VOLUME_LABEL = 'BOOX Internal Memory'
|
||||||
STORAGE_CARD_VOLUME_LABEL = 'BOOX Storage Card'
|
STORAGE_CARD_VOLUME_LABEL = 'BOOX Storage Card'
|
||||||
|
|
||||||
EBOOK_DIR_MAIN = 'MyBooks'
|
EBOOK_DIR_MAIN = ['MyBooks']
|
||||||
EBOOK_DIR_CARD_A = '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):
|
def windows_sort_drives(self, drives):
|
||||||
return drives
|
return drives
|
||||||
|
@ -7,12 +7,33 @@ __docformat__ = 'restructuredtext en'
|
|||||||
'''
|
'''
|
||||||
Device driver for Amazon's Kindle
|
Device driver for Amazon's Kindle
|
||||||
'''
|
'''
|
||||||
import datetime, os, re, sys
|
import datetime, os, re, sys, json, hashlib
|
||||||
from cStringIO import StringIO
|
from cStringIO import StringIO
|
||||||
from struct import unpack
|
from struct import unpack
|
||||||
|
|
||||||
from calibre.devices.usbms.driver import USBMS
|
from calibre.devices.usbms.driver import USBMS
|
||||||
|
|
||||||
|
'''
|
||||||
|
Notes on collections:
|
||||||
|
|
||||||
|
A collections cache is stored at system/collections.json
|
||||||
|
The cache is read only, changes made to it are overwritten (it is regenerated)
|
||||||
|
on device disconnect
|
||||||
|
|
||||||
|
A log of collection creation/manipulation is available at
|
||||||
|
system/userannotationlog
|
||||||
|
|
||||||
|
collections.json refers to books via a SHA1 hash of the absolute path to the
|
||||||
|
book (prefix is /mnt/us on my Kindle). The SHA1 hash may or may not be prefixed
|
||||||
|
by some characters, use the last 40 characters.
|
||||||
|
|
||||||
|
Changing the metadata and resending the file doesn't seem to affect collections
|
||||||
|
|
||||||
|
Adding a book to a collection on the Kindle does not change the book file at all
|
||||||
|
(i.e. it is binary identical). Therefore collection information is not stored in
|
||||||
|
file metadata.
|
||||||
|
'''
|
||||||
|
|
||||||
class KINDLE(USBMS):
|
class KINDLE(USBMS):
|
||||||
|
|
||||||
name = 'Kindle Device Interface'
|
name = 'Kindle Device Interface'
|
||||||
@ -60,6 +81,7 @@ class KINDLE(USBMS):
|
|||||||
'replace')
|
'replace')
|
||||||
return mi
|
return mi
|
||||||
|
|
||||||
|
|
||||||
def get_annotations(self, path_map):
|
def get_annotations(self, path_map):
|
||||||
MBP_FORMATS = [u'azw', u'mobi', u'prc', u'txt']
|
MBP_FORMATS = [u'azw', u'mobi', u'prc', u'txt']
|
||||||
mbp_formats = set(MBP_FORMATS)
|
mbp_formats = set(MBP_FORMATS)
|
||||||
@ -150,6 +172,37 @@ class KINDLE2(KINDLE):
|
|||||||
PRODUCT_ID = [0x0002]
|
PRODUCT_ID = [0x0002]
|
||||||
BCD = [0x0100]
|
BCD = [0x0100]
|
||||||
|
|
||||||
|
def books(self, oncard=None, end_session=True):
|
||||||
|
bl = USBMS.books(self, oncard=oncard, end_session=end_session)
|
||||||
|
# Read collections information
|
||||||
|
collections = os.path.join(self._main_prefix, 'system', 'collections.json')
|
||||||
|
if os.access(collections, os.R_OK):
|
||||||
|
try:
|
||||||
|
self.kindle_update_booklist(bl, collections)
|
||||||
|
except:
|
||||||
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
return bl
|
||||||
|
|
||||||
|
def kindle_update_booklist(self, bl, collections):
|
||||||
|
with open(collections, 'rb') as f:
|
||||||
|
collections = f.read()
|
||||||
|
collections = json.loads(collections)
|
||||||
|
path_map = {}
|
||||||
|
for name, val in collections.items():
|
||||||
|
col = name.split('@')[0]
|
||||||
|
items = val.get('items', [])
|
||||||
|
for x in items:
|
||||||
|
x = x[-40:]
|
||||||
|
if x not in path_map:
|
||||||
|
path_map[x] = set([])
|
||||||
|
path_map[x].add(col)
|
||||||
|
if path_map:
|
||||||
|
for book in bl:
|
||||||
|
path = '/mnt/us/'+book.lpath
|
||||||
|
h = hashlib.sha1(path).hexdigest()
|
||||||
|
if h in path_map:
|
||||||
|
book.device_collections = list(sorted(path_map[h]))
|
||||||
|
|
||||||
class KINDLE_DX(KINDLE2):
|
class KINDLE_DX(KINDLE2):
|
||||||
|
|
||||||
|
@ -55,6 +55,7 @@ class PRS505(USBMS):
|
|||||||
|
|
||||||
SUPPORTS_SUB_DIRS = True
|
SUPPORTS_SUB_DIRS = True
|
||||||
MUST_READ_METADATA = True
|
MUST_READ_METADATA = True
|
||||||
|
SUPPORTS_USE_AUTHOR_SORT = True
|
||||||
EBOOK_DIR_MAIN = 'database/media/books'
|
EBOOK_DIR_MAIN = 'database/media/books'
|
||||||
|
|
||||||
EXTRA_CUSTOMIZATION_MESSAGE = _('Comma separated list of metadata fields '
|
EXTRA_CUSTOMIZATION_MESSAGE = _('Comma separated list of metadata fields '
|
||||||
@ -125,7 +126,7 @@ class PRS505(USBMS):
|
|||||||
d = os.path.dirname(paths[source_id])
|
d = os.path.dirname(paths[source_id])
|
||||||
if not os.path.exists(d):
|
if not os.path.exists(d):
|
||||||
os.makedirs(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):
|
def books(self, oncard=None, end_session=True):
|
||||||
debug_print('PRS505: starting fetching books for card', oncard)
|
debug_print('PRS505: starting fetching books for card', oncard)
|
||||||
|
@ -60,12 +60,13 @@ def uuid():
|
|||||||
|
|
||||||
class XMLCache(object):
|
class XMLCache(object):
|
||||||
|
|
||||||
def __init__(self, paths, prefixes):
|
def __init__(self, paths, prefixes, use_author_sort):
|
||||||
if DEBUG:
|
if DEBUG:
|
||||||
debug_print('Building XMLCache...')
|
debug_print('Building XMLCache...')
|
||||||
pprint(paths)
|
pprint(paths)
|
||||||
self.paths = paths
|
self.paths = paths
|
||||||
self.prefixes = prefixes
|
self.prefixes = prefixes
|
||||||
|
self.use_author_sort = use_author_sort
|
||||||
|
|
||||||
# Parse XML files {{{
|
# Parse XML files {{{
|
||||||
parser = etree.XMLParser(recover=True)
|
parser = etree.XMLParser(recover=True)
|
||||||
@ -434,6 +435,9 @@ class XMLCache(object):
|
|||||||
if not ts:
|
if not ts:
|
||||||
ts = title_sort(title)
|
ts = title_sort(title)
|
||||||
record.set('titleSorter', ts)
|
record.set('titleSorter', ts)
|
||||||
|
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))
|
record.set('author', authors_to_string(book.authors))
|
||||||
ext = os.path.splitext(path)[1]
|
ext = os.path.splitext(path)[1]
|
||||||
if ext:
|
if ext:
|
||||||
|
@ -80,6 +80,7 @@ class Device(DeviceConfig, DevicePlugin):
|
|||||||
|
|
||||||
SUPPORTS_SUB_DIRS = False
|
SUPPORTS_SUB_DIRS = False
|
||||||
MUST_READ_METADATA = False
|
MUST_READ_METADATA = False
|
||||||
|
SUPPORTS_USE_AUTHOR_SORT = False
|
||||||
|
|
||||||
EBOOK_DIR_MAIN = ''
|
EBOOK_DIR_MAIN = ''
|
||||||
EBOOK_DIR_CARD_A = ''
|
EBOOK_DIR_CARD_A = ''
|
||||||
|
@ -32,6 +32,8 @@ class DeviceConfig(object):
|
|||||||
help=_('Place files in sub directories if the device supports them'))
|
help=_('Place files in sub directories if the device supports them'))
|
||||||
c.add_opt('read_metadata', default=True,
|
c.add_opt('read_metadata', default=True,
|
||||||
help=_('Read metadata from files on device'))
|
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(),
|
c.add_opt('save_template', default=cls._default_save_template(),
|
||||||
help=_('Template to control how books are saved'))
|
help=_('Template to control how books are saved'))
|
||||||
c.add_opt('extra_customization',
|
c.add_opt('extra_customization',
|
||||||
@ -47,7 +49,8 @@ class DeviceConfig(object):
|
|||||||
def config_widget(cls):
|
def config_widget(cls):
|
||||||
from calibre.gui2.device_drivers.configwidget import ConfigWidget
|
from calibre.gui2.device_drivers.configwidget import ConfigWidget
|
||||||
cw = ConfigWidget(cls.settings(), cls.FORMATS, cls.SUPPORTS_SUB_DIRS,
|
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
|
return cw
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@ -58,6 +61,8 @@ class DeviceConfig(object):
|
|||||||
proxy['use_subdirs'] = config_widget.use_subdirs()
|
proxy['use_subdirs'] = config_widget.use_subdirs()
|
||||||
if not cls.MUST_READ_METADATA:
|
if not cls.MUST_READ_METADATA:
|
||||||
proxy['read_metadata'] = config_widget.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:
|
if cls.EXTRA_CUSTOMIZATION_MESSAGE:
|
||||||
ec = unicode(config_widget.opt_extra_customization.text()).strip()
|
ec = unicode(config_widget.opt_extra_customization.text()).strip()
|
||||||
if not ec:
|
if not ec:
|
||||||
|
@ -299,7 +299,7 @@ class USBMS(CLI, Device):
|
|||||||
def replfunc(match):
|
def replfunc(match):
|
||||||
if match.group(1) in ['title', 'series', 'series_index', 'isbn']:
|
if match.group(1) in ['title', 'series', 'series_index', 'isbn']:
|
||||||
return '(?P<' + match.group(1) + '>.+?)'
|
return '(?P<' + match.group(1) + '>.+?)'
|
||||||
elif match.group(1) == 'authors':
|
elif match.group(1) in ['authors', 'author_sort']:
|
||||||
return '(?P<author>.+?)'
|
return '(?P<author>.+?)'
|
||||||
else:
|
else:
|
||||||
return '(.+?)'
|
return '(.+?)'
|
||||||
|
@ -8,7 +8,7 @@ import os, re
|
|||||||
from mimetypes import guess_type as guess_mimetype
|
from mimetypes import guess_type as guess_mimetype
|
||||||
|
|
||||||
from calibre.ebooks.BeautifulSoup import BeautifulSoup, NavigableString
|
from calibre.ebooks.BeautifulSoup import BeautifulSoup, NavigableString
|
||||||
|
from calibre.constants import iswindows
|
||||||
from calibre.utils.chm.chm import CHMFile
|
from calibre.utils.chm.chm import CHMFile
|
||||||
from calibre.utils.chm.chmlib import (
|
from calibre.utils.chm.chmlib import (
|
||||||
CHM_RESOLVE_SUCCESS, CHM_ENUMERATE_NORMAL,
|
CHM_RESOLVE_SUCCESS, CHM_ENUMERATE_NORMAL,
|
||||||
@ -135,10 +135,16 @@ class CHMReader(CHMFile):
|
|||||||
if lpath.find(';') != -1:
|
if lpath.find(';') != -1:
|
||||||
# fix file names with ";<junk>" at the end, see _reformat()
|
# fix file names with ";<junk>" at the end, see _reformat()
|
||||||
lpath = lpath.split(';')[0]
|
lpath = lpath.split(';')[0]
|
||||||
|
try:
|
||||||
with open(lpath, 'wb') as f:
|
with open(lpath, 'wb') as f:
|
||||||
if guess_mimetype(path)[0] == ('text/html'):
|
if guess_mimetype(path)[0] == ('text/html'):
|
||||||
data = self._reformat(data)
|
data = self._reformat(data)
|
||||||
f.write(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
|
self._extracted = True
|
||||||
files = os.listdir(output_dir)
|
files = os.listdir(output_dir)
|
||||||
if self.hhc_path not in files:
|
if self.hhc_path not in files:
|
||||||
|
@ -84,7 +84,7 @@ class EPUBOutput(OutputFormatPlugin):
|
|||||||
|
|
||||||
OptionRecommendation(name='no_svg_cover', recommended_value=False,
|
OptionRecommendation(name='no_svg_cover', recommended_value=False,
|
||||||
help=_('Do not use SVG for the book cover. Use this option if '
|
help=_('Do not use SVG for the book cover. Use this option if '
|
||||||
'your EPUB is going to be used ona device that does not '
|
'your EPUB is going to be used on a device that does not '
|
||||||
'support SVG, like the iPhone or the JetBook Lite. '
|
'support SVG, like the iPhone or the JetBook Lite. '
|
||||||
'Without this option, such devices will display the cover '
|
'Without this option, such devices will display the cover '
|
||||||
'as a blank page.')
|
'as a blank page.')
|
||||||
@ -385,14 +385,6 @@ class EPUBOutput(OutputFormatPlugin):
|
|||||||
if val and not pval:
|
if val and not pval:
|
||||||
rule.style.setProperty('padding-left', val)
|
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): # {{{
|
def workaround_sony_quirks(self): # {{{
|
||||||
|
@ -28,10 +28,14 @@ def authors_to_string(authors):
|
|||||||
else:
|
else:
|
||||||
return ''
|
return ''
|
||||||
|
|
||||||
|
_bracket_pat = re.compile(r'[\[({].*?[})\]]')
|
||||||
def author_to_author_sort(author):
|
def author_to_author_sort(author):
|
||||||
|
if not author:
|
||||||
|
return ''
|
||||||
method = tweaks['author_sort_copy_method']
|
method = tweaks['author_sort_copy_method']
|
||||||
if method == 'copy' or (method == 'comma' and author.count(',') > 0):
|
if method == 'copy' or (method == 'comma' and ',' in author):
|
||||||
return author
|
return author
|
||||||
|
author = _bracket_pat.sub('', author).strip()
|
||||||
tokens = author.split()
|
tokens = author.split()
|
||||||
tokens = tokens[-1:] + tokens[:-1]
|
tokens = tokens[-1:] + tokens[:-1]
|
||||||
if len(tokens) > 1:
|
if len(tokens) > 1:
|
||||||
@ -223,6 +227,7 @@ class MetaInformation(object):
|
|||||||
'isbn', 'tags', 'cover_data', 'application_id', 'guide',
|
'isbn', 'tags', 'cover_data', 'application_id', 'guide',
|
||||||
'manifest', 'spine', 'toc', 'cover', 'language',
|
'manifest', 'spine', 'toc', 'cover', 'language',
|
||||||
'book_producer', 'timestamp', 'lccn', 'lcc', 'ddc',
|
'book_producer', 'timestamp', 'lccn', 'lcc', 'ddc',
|
||||||
|
'author_sort_map',
|
||||||
'pubdate', 'rights', 'publication_type', 'uuid'):
|
'pubdate', 'rights', 'publication_type', 'uuid'):
|
||||||
if hasattr(mi, attr):
|
if hasattr(mi, attr):
|
||||||
setattr(ans, attr, getattr(mi, attr))
|
setattr(ans, attr, getattr(mi, attr))
|
||||||
@ -244,6 +249,7 @@ class MetaInformation(object):
|
|||||||
self.tags = getattr(mi, 'tags', [])
|
self.tags = getattr(mi, 'tags', [])
|
||||||
#: mi.cover_data = (ext, data)
|
#: mi.cover_data = (ext, data)
|
||||||
self.cover_data = getattr(mi, 'cover_data', (None, None))
|
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',
|
for x in ('author_sort', 'title_sort', 'comments', 'category', 'publisher',
|
||||||
'series', 'series_index', 'rating', 'isbn', 'language',
|
'series', 'series_index', 'rating', 'isbn', 'language',
|
||||||
@ -254,11 +260,11 @@ class MetaInformation(object):
|
|||||||
setattr(self, x, getattr(mi, x, None))
|
setattr(self, x, getattr(mi, x, None))
|
||||||
|
|
||||||
def print_all_attributes(self):
|
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',
|
'series', 'series_index', 'tags', 'rating', 'isbn', 'language',
|
||||||
'application_id', 'manifest', 'toc', 'spine', 'guide', 'cover',
|
'application_id', 'manifest', 'toc', 'spine', 'guide', 'cover',
|
||||||
'book_producer', 'timestamp', 'lccn', 'lcc', 'ddc', 'pubdate',
|
'book_producer', 'timestamp', 'lccn', 'lcc', 'ddc', 'pubdate',
|
||||||
'rights', 'publication_type', 'uuid'
|
'rights', 'publication_type', 'uuid', 'author_sort_map'
|
||||||
):
|
):
|
||||||
prints(x, getattr(self, x, 'None'))
|
prints(x, getattr(self, x, 'None'))
|
||||||
|
|
||||||
@ -288,6 +294,9 @@ class MetaInformation(object):
|
|||||||
self.tags += mi.tags
|
self.tags += mi.tags
|
||||||
self.tags = list(set(self.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):
|
if getattr(mi, 'cover_data', False):
|
||||||
other_cover = mi.cover_data[-1]
|
other_cover = mi.cover_data[-1]
|
||||||
self_cover = self.cover_data[-1] if self.cover_data else ''
|
self_cover = self.cover_data[-1] if self.cover_data else ''
|
||||||
|
@ -10,12 +10,31 @@ import os
|
|||||||
from contextlib import closing
|
from contextlib import closing
|
||||||
|
|
||||||
from calibre.customize import FileTypePlugin
|
from calibre.customize import FileTypePlugin
|
||||||
|
from calibre.utils.zipfile import ZipFile, stringFileHeader
|
||||||
|
|
||||||
def is_comic(list_of_names):
|
def is_comic(list_of_names):
|
||||||
extensions = set([x.rpartition('.')[-1].lower() for x in list_of_names])
|
extensions = set([x.rpartition('.')[-1].lower() for x in list_of_names])
|
||||||
comic_extensions = set(['jpg', 'jpeg', 'png'])
|
comic_extensions = set(['jpg', 'jpeg', 'png'])
|
||||||
return len(extensions - comic_extensions) == 0
|
return len(extensions - comic_extensions) == 0
|
||||||
|
|
||||||
|
def archive_type(stream):
|
||||||
|
try:
|
||||||
|
pos = stream.tell()
|
||||||
|
except:
|
||||||
|
pos = 0
|
||||||
|
id_ = stream.read(4)
|
||||||
|
ans = None
|
||||||
|
if id_ == stringFileHeader:
|
||||||
|
ans = 'zip'
|
||||||
|
elif id_.startswith('Rar'):
|
||||||
|
ans = 'rar'
|
||||||
|
try:
|
||||||
|
stream.seek(pos)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
return ans
|
||||||
|
|
||||||
|
|
||||||
class ArchiveExtract(FileTypePlugin):
|
class ArchiveExtract(FileTypePlugin):
|
||||||
name = 'Archive Extract'
|
name = 'Archive Extract'
|
||||||
author = 'Kovid Goyal'
|
author = 'Kovid Goyal'
|
||||||
@ -31,7 +50,6 @@ class ArchiveExtract(FileTypePlugin):
|
|||||||
if is_rar:
|
if is_rar:
|
||||||
from calibre.libunrar import extract_member, names
|
from calibre.libunrar import extract_member, names
|
||||||
else:
|
else:
|
||||||
from calibre.utils.zipfile import ZipFile
|
|
||||||
zf = ZipFile(archive, 'r')
|
zf = ZipFile(archive, 'r')
|
||||||
|
|
||||||
if is_rar:
|
if is_rar:
|
||||||
|
@ -35,6 +35,8 @@ PUBLICATION_METADATA_FIELDS = frozenset([
|
|||||||
'title_sort',
|
'title_sort',
|
||||||
# Ordered list of authors. Must never be None, can be [_('Unknown')]
|
# Ordered list of authors. Must never be None, can be [_('Unknown')]
|
||||||
'authors',
|
'authors',
|
||||||
|
# Map of sort strings for each author
|
||||||
|
'author_sort_map',
|
||||||
# Pseudo field that can be set, but if not set is auto generated
|
# Pseudo field that can be set, but if not set is auto generated
|
||||||
# from authors and languages
|
# from authors and languages
|
||||||
'author_sort',
|
'author_sort',
|
||||||
|
@ -16,6 +16,7 @@ NULL_VALUES = {
|
|||||||
'classifiers' : {},
|
'classifiers' : {},
|
||||||
'languages' : [],
|
'languages' : [],
|
||||||
'device_collections': [],
|
'device_collections': [],
|
||||||
|
'author_sort_map': {},
|
||||||
'authors' : [_('Unknown')],
|
'authors' : [_('Unknown')],
|
||||||
'title' : _('Unknown'),
|
'title' : _('Unknown'),
|
||||||
}
|
}
|
||||||
|
258
src/calibre/ebooks/metadata/douban.py
Normal 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())
|
@ -182,7 +182,7 @@ def get_metadata(stream, extract_cover=True):
|
|||||||
def get_quick_metadata(stream):
|
def get_quick_metadata(stream):
|
||||||
return get_metadata(stream, False)
|
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)
|
stream.seek(0)
|
||||||
reader = OCFZipReader(stream, root=os.getcwdu())
|
reader = OCFZipReader(stream, root=os.getcwdu())
|
||||||
mi = MetaInformation(mi)
|
mi = MetaInformation(mi)
|
||||||
@ -196,6 +196,8 @@ def set_metadata(stream, mi, apply_null=False):
|
|||||||
reader.opf.tags = []
|
reader.opf.tags = []
|
||||||
if not getattr(mi, 'isbn', None):
|
if not getattr(mi, 'isbn', None):
|
||||||
reader.opf.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())
|
newopf = StringIO(reader.opf.render())
|
||||||
safe_replace(stream, reader.container[OPF.MIMETYPE], newopf)
|
safe_replace(stream, reader.container[OPF.MIMETYPE], newopf)
|
||||||
|
@ -198,6 +198,38 @@ class Amazon(MetadataSource):
|
|||||||
self.exception = e
|
self.exception = e
|
||||||
self.tb = traceback.format_exc()
|
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):
|
def result_index(source, result):
|
||||||
if not result.isbn:
|
if not result.isbn:
|
||||||
return -1
|
return -1
|
||||||
@ -266,7 +298,7 @@ def get_social_metadata(mi, verbose=0):
|
|||||||
with MetadataSources(fetchers) as manager:
|
with MetadataSources(fetchers) as manager:
|
||||||
manager(mi.title, mi.authors, mi.publisher, mi.isbn, verbose)
|
manager(mi.title, mi.authors, mi.publisher, mi.isbn, verbose)
|
||||||
manager.join()
|
manager.join()
|
||||||
ratings, tags, comments = [], set([]), set([])
|
ratings, tags, comments, series, series_index = [], set([]), set([]), None, None
|
||||||
for fetcher in fetchers:
|
for fetcher in fetchers:
|
||||||
if fetcher.results:
|
if fetcher.results:
|
||||||
dmi = fetcher.results
|
dmi = fetcher.results
|
||||||
@ -279,6 +311,10 @@ def get_social_metadata(mi, verbose=0):
|
|||||||
mi.pubdate = dmi.pubdate
|
mi.pubdate = dmi.pubdate
|
||||||
if dmi.comments:
|
if dmi.comments:
|
||||||
comments.add(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:
|
if ratings:
|
||||||
rating = sum(ratings)/float(len(ratings))
|
rating = sum(ratings)/float(len(ratings))
|
||||||
if mi.rating is None or mi.rating < 0.1:
|
if mi.rating is None or mi.rating < 0.1:
|
||||||
@ -295,6 +331,9 @@ def get_social_metadata(mi, verbose=0):
|
|||||||
mi.comments = ''
|
mi.comments = ''
|
||||||
for x in comments:
|
for x in comments:
|
||||||
mi.comments += x+'\n\n'
|
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
|
return [(x.name, x.exception, x.tb) for x in fetchers if x.exception is not
|
||||||
None]
|
None]
|
||||||
|
@ -736,12 +736,14 @@ class OPF(object):
|
|||||||
def fget(self):
|
def fget(self):
|
||||||
ans = []
|
ans = []
|
||||||
for tag in self.tags_path(self.metadata):
|
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
|
return ans
|
||||||
|
|
||||||
def fset(self, val):
|
def fset(self, val):
|
||||||
for tag in list(self.tags_path(self.metadata)):
|
for tag in list(self.tags_path(self.metadata)):
|
||||||
self.metadata.remove(tag)
|
tag.getparent().remove(tag)
|
||||||
for tag in val:
|
for tag in val:
|
||||||
elem = self.create_metadata_element('subject')
|
elem = self.create_metadata_element('subject')
|
||||||
self.set_text(elem, unicode(tag))
|
self.set_text(elem, unicode(tag))
|
||||||
|
@ -61,6 +61,7 @@ class FormatState(object):
|
|||||||
self.italic = False
|
self.italic = False
|
||||||
self.bold = False
|
self.bold = False
|
||||||
self.strikethrough = False
|
self.strikethrough = False
|
||||||
|
self.underline = False
|
||||||
self.preserve = False
|
self.preserve = False
|
||||||
self.family = 'serif'
|
self.family = 'serif'
|
||||||
self.bgcolor = 'transparent'
|
self.bgcolor = 'transparent'
|
||||||
@ -79,7 +80,8 @@ class FormatState(object):
|
|||||||
and self.family == other.family \
|
and self.family == other.family \
|
||||||
and self.bgcolor == other.bgcolor \
|
and self.bgcolor == other.bgcolor \
|
||||||
and self.fgcolor == other.fgcolor \
|
and self.fgcolor == other.fgcolor \
|
||||||
and self.strikethrough == other.strikethrough
|
and self.strikethrough == other.strikethrough \
|
||||||
|
and self.underline == other.underline
|
||||||
|
|
||||||
def __ne__(self, other):
|
def __ne__(self, other):
|
||||||
return not self.__eq__(other)
|
return not self.__eq__(other)
|
||||||
@ -251,6 +253,8 @@ class MobiMLizer(object):
|
|||||||
color=unicode(istate.fgcolor))
|
color=unicode(istate.fgcolor))
|
||||||
if istate.strikethrough:
|
if istate.strikethrough:
|
||||||
inline = etree.SubElement(inline, XHTML('s'))
|
inline = etree.SubElement(inline, XHTML('s'))
|
||||||
|
if istate.underline:
|
||||||
|
inline = etree.SubElement(inline, XHTML('u'))
|
||||||
bstate.inline = inline
|
bstate.inline = inline
|
||||||
bstate.istate = istate
|
bstate.istate = istate
|
||||||
inline = bstate.inline
|
inline = bstate.inline
|
||||||
@ -330,6 +334,7 @@ class MobiMLizer(object):
|
|||||||
istate.bgcolor = style['background-color']
|
istate.bgcolor = style['background-color']
|
||||||
istate.fgcolor = style['color']
|
istate.fgcolor = style['color']
|
||||||
istate.strikethrough = style['text-decoration'] == 'line-through'
|
istate.strikethrough = style['text-decoration'] == 'line-through'
|
||||||
|
istate.underline = style['text-decoration'] == 'underline'
|
||||||
if 'monospace' in style['font-family']:
|
if 'monospace' in style['font-family']:
|
||||||
istate.family = 'monospace'
|
istate.family = 'monospace'
|
||||||
elif 'sans-serif' in style['font-family']:
|
elif 'sans-serif' in style['font-family']:
|
||||||
|
@ -103,8 +103,8 @@ class CoverManager(object):
|
|||||||
32)]
|
32)]
|
||||||
img_data = create_cover_page(lines, I('library.png'))
|
img_data = create_cover_page(lines, I('library.png'))
|
||||||
id, href = self.oeb.manifest.generate('cover_image',
|
id, href = self.oeb.manifest.generate('cover_image',
|
||||||
'cover_image.png')
|
'cover_image.jpg')
|
||||||
item = self.oeb.manifest.add(id, href, guess_type('t.png')[0],
|
item = self.oeb.manifest.add(id, href, guess_type('t.jpg')[0],
|
||||||
data=img_data)
|
data=img_data)
|
||||||
m.clear('cover')
|
m.clear('cover')
|
||||||
m.add('cover', item.id)
|
m.add('cover', item.id)
|
||||||
|
@ -43,8 +43,8 @@ def _config():
|
|||||||
help=_('Notify when a new version is available'))
|
help=_('Notify when a new version is available'))
|
||||||
c.add_opt('use_roman_numerals_for_series_number', default=True,
|
c.add_opt('use_roman_numerals_for_series_number', default=True,
|
||||||
help=_('Use Roman numerals for series number'))
|
help=_('Use Roman numerals for series number'))
|
||||||
c.add_opt('sort_by_popularity', default=False,
|
c.add_opt('sort_tags_by', default='name',
|
||||||
help=_('Sort tags list by popularity'))
|
help=_('Sort tags list by name, popularity, or rating'))
|
||||||
c.add_opt('cover_flow_queue_length', default=6,
|
c.add_opt('cover_flow_queue_length', default=6,
|
||||||
help=_('Number of covers to show in the cover browsing mode'))
|
help=_('Number of covers to show in the cover browsing mode'))
|
||||||
c.add_opt('LRF_conversion_defaults', default=[],
|
c.add_opt('LRF_conversion_defaults', default=[],
|
||||||
@ -100,7 +100,9 @@ def _config():
|
|||||||
c.add_opt('tag_browser_hidden_categories', default=set(),
|
c.add_opt('tag_browser_hidden_categories', default=set(),
|
||||||
help=_('tag browser categories not to display'))
|
help=_('tag browser categories not to display'))
|
||||||
c.add_opt('gui_layout', choices=['wide', 'narrow'],
|
c.add_opt('gui_layout', choices=['wide', 'narrow'],
|
||||||
help=_('The layout of the user interface'), default='narrow')
|
help=_('The layout of the user interface'), default='wide')
|
||||||
|
c.add_opt('show_avg_rating', default=True,
|
||||||
|
help=_('Show the average rating per item indication in the tag browser'))
|
||||||
return ConfigProxy(c)
|
return ConfigProxy(c)
|
||||||
|
|
||||||
config = _config()
|
config = _config()
|
||||||
|
1277
src/calibre/gui2/actions.py
Normal file
315
src/calibre/gui2/book_details.py
Normal file
@ -0,0 +1,315 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
|
||||||
|
|
||||||
|
__license__ = 'GPL v3'
|
||||||
|
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||||
|
__docformat__ = 'restructuredtext en'
|
||||||
|
|
||||||
|
import os, collections
|
||||||
|
|
||||||
|
from PyQt4.Qt import QLabel, QPixmap, QSize, QWidget, Qt, pyqtSignal, \
|
||||||
|
QVBoxLayout, QScrollArea, QPropertyAnimation, QEasingCurve, \
|
||||||
|
QSizePolicy, QPainter, QRect, pyqtProperty, QDesktopServices, QUrl
|
||||||
|
|
||||||
|
from calibre import fit_image, prepare_string_for_xml
|
||||||
|
from calibre.gui2.widgets import IMAGE_EXTENSIONS
|
||||||
|
from calibre.ebooks import BOOK_EXTENSIONS
|
||||||
|
from calibre.constants import preferred_encoding
|
||||||
|
from calibre.library.comments import comments_to_html
|
||||||
|
|
||||||
|
# render_rows(data) {{{
|
||||||
|
WEIGHTS = collections.defaultdict(lambda : 100)
|
||||||
|
WEIGHTS[_('Path')] = 5
|
||||||
|
WEIGHTS[_('Formats')] = 1
|
||||||
|
WEIGHTS[_('Collections')] = 2
|
||||||
|
WEIGHTS[_('Series')] = 3
|
||||||
|
WEIGHTS[_('Tags')] = 4
|
||||||
|
|
||||||
|
def render_rows(data):
|
||||||
|
keys = data.keys()
|
||||||
|
keys.sort(cmp=lambda x, y: cmp(WEIGHTS[x], WEIGHTS[y]))
|
||||||
|
rows = []
|
||||||
|
for key in keys:
|
||||||
|
txt = data[key]
|
||||||
|
if key in ('id', _('Comments')) or not hasattr(txt, 'strip') or not txt.strip() or \
|
||||||
|
txt == 'None':
|
||||||
|
continue
|
||||||
|
if isinstance(key, str):
|
||||||
|
key = key.decode(preferred_encoding, 'replace')
|
||||||
|
if isinstance(txt, str):
|
||||||
|
txt = txt.decode(preferred_encoding, 'replace')
|
||||||
|
if '</font>' not in txt:
|
||||||
|
txt = prepare_string_for_xml(txt)
|
||||||
|
if 'id' in data:
|
||||||
|
if key == _('Path'):
|
||||||
|
txt = u'<a href="path:%s" title="%s">%s</a>'%(data['id'],
|
||||||
|
txt, _('Click to open'))
|
||||||
|
if key == _('Formats') and txt and txt != _('None'):
|
||||||
|
fmts = [x.strip() for x in txt.split(',')]
|
||||||
|
fmts = [u'<a href="format:%s:%s">%s</a>' % (data['id'], x, x) for x
|
||||||
|
in fmts]
|
||||||
|
txt = ', '.join(fmts)
|
||||||
|
else:
|
||||||
|
if key == _('Path'):
|
||||||
|
txt = u'<a href="devpath:%s">%s</a>'%(txt,
|
||||||
|
_('Click to open'))
|
||||||
|
|
||||||
|
rows.append((key, txt))
|
||||||
|
return rows
|
||||||
|
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
class CoverView(QWidget): # {{{
|
||||||
|
|
||||||
|
|
||||||
|
def __init__(self, vertical, parent=None):
|
||||||
|
QWidget.__init__(self, parent)
|
||||||
|
self.setMaximumSize(QSize(120, 120))
|
||||||
|
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))
|
||||||
|
self.animation.setDuration(1000)
|
||||||
|
self.animation.setStartValue(QSize(0, 0))
|
||||||
|
self.animation.valueChanged.connect(self.value_changed)
|
||||||
|
|
||||||
|
self.setSizePolicy(QSizePolicy.Expanding if vertical else
|
||||||
|
QSizePolicy.Minimum, QSizePolicy.Expanding)
|
||||||
|
|
||||||
|
self.default_pixmap = QPixmap(I('book.svg'))
|
||||||
|
self.pixmap = self.default_pixmap
|
||||||
|
self.pwidth = self.pheight = None
|
||||||
|
self.data = {}
|
||||||
|
|
||||||
|
self.do_layout()
|
||||||
|
|
||||||
|
def value_changed(self, val):
|
||||||
|
self.update()
|
||||||
|
|
||||||
|
def setCurrentPixmapSize(self, val):
|
||||||
|
self._current_pixmap_size = val
|
||||||
|
|
||||||
|
def do_layout(self):
|
||||||
|
if self.rect().width() == 0 or self.rect().height() == 0:
|
||||||
|
return
|
||||||
|
pixmap = self.pixmap
|
||||||
|
pwidth, pheight = pixmap.width(), pixmap.height()
|
||||||
|
try:
|
||||||
|
self.pwidth, self.pheight = fit_image(pwidth, pheight,
|
||||||
|
self.rect().width(), self.rect().height())[1:]
|
||||||
|
except:
|
||||||
|
self.pwidth, self.pheight = self.rect().width()-1, \
|
||||||
|
self.rect().height()-1
|
||||||
|
self.current_pixmap_size = QSize(self.pwidth, self.pheight)
|
||||||
|
self.animation.setEndValue(self.current_pixmap_size)
|
||||||
|
|
||||||
|
def relayout(self, parent_size):
|
||||||
|
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()
|
||||||
|
|
||||||
|
def sizeHint(self):
|
||||||
|
return self.maximumSize()
|
||||||
|
|
||||||
|
def show_data(self, data):
|
||||||
|
self.animation.stop()
|
||||||
|
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'))
|
||||||
|
if self.pixmap.isNull() or self.pixmap.width() < 5 or \
|
||||||
|
self.pixmap.height() < 5:
|
||||||
|
self.pixmap = self.default_pixmap
|
||||||
|
else:
|
||||||
|
self.pixmap = self.default_pixmap
|
||||||
|
self.do_layout()
|
||||||
|
self.update()
|
||||||
|
if not same_item:
|
||||||
|
self.animation.start()
|
||||||
|
|
||||||
|
def paintEvent(self, event):
|
||||||
|
canvas_size = self.rect()
|
||||||
|
width = self.current_pixmap_size.width()
|
||||||
|
extrax = canvas_size.width() - width
|
||||||
|
if extrax < 0: extrax = 0
|
||||||
|
x = int(extrax/2.)
|
||||||
|
height = self.current_pixmap_size.height()
|
||||||
|
extray = canvas_size.height() - height
|
||||||
|
if extray < 0: extray = 0
|
||||||
|
y = int(extray/2.)
|
||||||
|
target = QRect(x, y, width, height)
|
||||||
|
p = QPainter(self)
|
||||||
|
p.setRenderHints(QPainter.Antialiasing | QPainter.SmoothPixmapTransform)
|
||||||
|
p.drawPixmap(target, self.pixmap.scaled(target.size(),
|
||||||
|
Qt.KeepAspectRatio, Qt.SmoothTransformation))
|
||||||
|
p.end()
|
||||||
|
|
||||||
|
current_pixmap_size = pyqtProperty('QSize',
|
||||||
|
fget=lambda self: self._current_pixmap_size,
|
||||||
|
fset=setCurrentPixmapSize
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
# Book Info {{{
|
||||||
|
class Label(QLabel):
|
||||||
|
|
||||||
|
mr = pyqtSignal(object)
|
||||||
|
link_clicked = pyqtSignal(object)
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
QLabel.__init__(self)
|
||||||
|
self.setTextFormat(Qt.RichText)
|
||||||
|
self.setText('')
|
||||||
|
self.setWordWrap(True)
|
||||||
|
self.linkActivated.connect(self.link_activated)
|
||||||
|
self._link_clicked = False
|
||||||
|
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
||||||
|
|
||||||
|
def link_activated(self, link):
|
||||||
|
self._link_clicked = True
|
||||||
|
link = unicode(link)
|
||||||
|
self.link_clicked.emit(link)
|
||||||
|
|
||||||
|
def mouseReleaseEvent(self, ev):
|
||||||
|
QLabel.mouseReleaseEvent(self, ev)
|
||||||
|
if not self._link_clicked:
|
||||||
|
self.mr.emit(ev)
|
||||||
|
self._link_clicked = False
|
||||||
|
|
||||||
|
class BookInfo(QScrollArea):
|
||||||
|
|
||||||
|
def __init__(self, vertical, parent=None):
|
||||||
|
QScrollArea.__init__(self, parent)
|
||||||
|
self.vertical = vertical
|
||||||
|
self.setWidgetResizable(True)
|
||||||
|
self.label = Label()
|
||||||
|
self.setWidget(self.label)
|
||||||
|
self.link_clicked = self.label.link_clicked
|
||||||
|
self.mr = self.label.mr
|
||||||
|
self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
|
||||||
|
|
||||||
|
def show_data(self, data):
|
||||||
|
self.label.setText('')
|
||||||
|
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 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))
|
||||||
|
|
||||||
|
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
class BookDetails(QWidget): # {{{
|
||||||
|
|
||||||
|
resized = pyqtSignal(object)
|
||||||
|
show_book_info = pyqtSignal()
|
||||||
|
open_containing_folder = pyqtSignal(int)
|
||||||
|
view_specific_format = pyqtSignal(int, object)
|
||||||
|
|
||||||
|
# Drag 'n drop {{{
|
||||||
|
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()
|
||||||
|
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
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(vertical, self)
|
||||||
|
self.cover_view.relayout(self.size())
|
||||||
|
self.resized.connect(self.cover_view.relayout, type=Qt.QueuedConnection)
|
||||||
|
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)
|
||||||
|
if vertical:
|
||||||
|
self.setMinimumSize(QSize(190, 200))
|
||||||
|
else:
|
||||||
|
self.setMinimumSize(120, 120)
|
||||||
|
self.setCursor(Qt.PointingHandCursor)
|
||||||
|
|
||||||
|
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':
|
||||||
|
QDesktopServices.openUrl(QUrl.fromLocalFile(val))
|
||||||
|
|
||||||
|
|
||||||
|
def mouseReleaseEvent(self, ev):
|
||||||
|
ev.accept()
|
||||||
|
self.show_book_info.emit()
|
||||||
|
|
||||||
|
def resizeEvent(self, ev):
|
||||||
|
self.resized.emit(self.size())
|
||||||
|
|
||||||
|
def show_data(self, data):
|
||||||
|
self.cover_view.show_data(data)
|
||||||
|
self.book_info.show_data(data)
|
||||||
|
self.setToolTip('<p>'+_('Click to open Book Details window') +
|
||||||
|
'<br><br>' + _('Path') + ': ' + data.get(_('Path'), ''))
|
||||||
|
|
||||||
|
def reset_info(self):
|
||||||
|
self.show_data({})
|
||||||
|
|
||||||
|
# }}}
|
||||||
|
|
@ -13,7 +13,7 @@ from PyQt4.Qt import QPixmap, SIGNAL
|
|||||||
from calibre.gui2 import choose_images, error_dialog
|
from calibre.gui2 import choose_images, error_dialog
|
||||||
from calibre.gui2.convert.metadata_ui import Ui_Form
|
from calibre.gui2.convert.metadata_ui import Ui_Form
|
||||||
from calibre.ebooks.metadata import authors_to_string, string_to_authors, \
|
from calibre.ebooks.metadata import authors_to_string, string_to_authors, \
|
||||||
MetaInformation, authors_to_sort_string
|
MetaInformation
|
||||||
from calibre.ebooks.metadata.opf2 import metadata_to_opf
|
from calibre.ebooks.metadata.opf2 import metadata_to_opf
|
||||||
from calibre.ptempfile import PersistentTemporaryFile
|
from calibre.ptempfile import PersistentTemporaryFile
|
||||||
from calibre.gui2.convert import Widget
|
from calibre.gui2.convert import Widget
|
||||||
@ -57,7 +57,7 @@ class MetadataWidget(Widget, Ui_Form):
|
|||||||
au = unicode(self.author.currentText())
|
au = unicode(self.author.currentText())
|
||||||
au = re.sub(r'\s+et al\.$', '', au)
|
au = re.sub(r'\s+et al\.$', '', au)
|
||||||
authors = string_to_authors(au)
|
authors = string_to_authors(au)
|
||||||
self.author_sort.setText(authors_to_sort_string(authors))
|
self.author_sort.setText(self.db.author_sort_from_authors(authors))
|
||||||
|
|
||||||
|
|
||||||
def initialize_metadata_options(self):
|
def initialize_metadata_options(self):
|
||||||
|
@ -83,7 +83,6 @@ if pictureflow is not None:
|
|||||||
self.setFocusPolicy(Qt.WheelFocus)
|
self.setFocusPolicy(Qt.WheelFocus)
|
||||||
self.setSizePolicy(QSizePolicy(QSizePolicy.Expanding,
|
self.setSizePolicy(QSizePolicy(QSizePolicy.Expanding,
|
||||||
QSizePolicy.Expanding))
|
QSizePolicy.Expanding))
|
||||||
self.setZoomFactor(150)
|
|
||||||
|
|
||||||
def sizeHint(self):
|
def sizeHint(self):
|
||||||
return self.minimumSize()
|
return self.minimumSize()
|
||||||
@ -206,8 +205,8 @@ class CoverFlowMixin(object):
|
|||||||
sm.select(index, sm.ClearAndSelect|sm.Rows)
|
sm.select(index, sm.ClearAndSelect|sm.Rows)
|
||||||
self.library_view.setCurrentIndex(index)
|
self.library_view.setCurrentIndex(index)
|
||||||
except:
|
except:
|
||||||
pass
|
import traceback
|
||||||
|
traceback.print_exc()
|
||||||
|
|
||||||
def sync_listview_to_cf(self, row):
|
def sync_listview_to_cf(self, row):
|
||||||
self.cf_last_updated_at = time.time()
|
self.cf_last_updated_at = time.time()
|
||||||
|
@ -3,25 +3,27 @@ __license__ = 'GPL v3'
|
|||||||
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
|
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
|
||||||
|
|
||||||
# Imports {{{
|
# Imports {{{
|
||||||
import os, traceback, Queue, time, socket, cStringIO, re
|
import os, traceback, Queue, time, socket, cStringIO, re, sys
|
||||||
from threading import Thread, RLock
|
from threading import Thread, RLock
|
||||||
from itertools import repeat
|
from itertools import repeat
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from binascii import unhexlify
|
from binascii import unhexlify
|
||||||
|
|
||||||
from PyQt4.Qt import QMenu, QAction, QActionGroup, QIcon, SIGNAL, QPixmap, \
|
from PyQt4.Qt import QMenu, QAction, QActionGroup, QIcon, SIGNAL, QPixmap, \
|
||||||
Qt, pyqtSignal
|
Qt, pyqtSignal, QColor, QPainter
|
||||||
|
from PyQt4.QtSvg import QSvgRenderer
|
||||||
|
|
||||||
from calibre.customize.ui import available_input_formats, available_output_formats, \
|
from calibre.customize.ui import available_input_formats, available_output_formats, \
|
||||||
device_plugins
|
device_plugins
|
||||||
from calibre.devices.interface import DevicePlugin
|
from calibre.devices.interface import DevicePlugin
|
||||||
|
from calibre.devices.errors import UserFeedback
|
||||||
from calibre.gui2.dialogs.choose_format import ChooseFormatDialog
|
from calibre.gui2.dialogs.choose_format import ChooseFormatDialog
|
||||||
from calibre.utils.ipc.job import BaseJob
|
from calibre.utils.ipc.job import BaseJob
|
||||||
from calibre.devices.scanner import DeviceScanner
|
from calibre.devices.scanner import DeviceScanner
|
||||||
from calibre.gui2 import config, error_dialog, Dispatcher, dynamic, \
|
from calibre.gui2 import config, error_dialog, Dispatcher, dynamic, \
|
||||||
pixmap_to_data, warning_dialog, \
|
pixmap_to_data, warning_dialog, \
|
||||||
question_dialog
|
question_dialog, info_dialog, choose_dir
|
||||||
from calibre.ebooks.metadata import authors_to_string, authors_to_sort_string
|
from calibre.ebooks.metadata import authors_to_string
|
||||||
from calibre import preferred_encoding, prints
|
from calibre import preferred_encoding, prints
|
||||||
from calibre.utils.filenames import ascii_filename
|
from calibre.utils.filenames import ascii_filename
|
||||||
from calibre.devices.errors import FreeSpaceError
|
from calibre.devices.errors import FreeSpaceError
|
||||||
@ -597,10 +599,209 @@ class Emailer(Thread): # {{{
|
|||||||
|
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
class DeviceMixin(object):
|
class DeviceMixin(object): # {{{
|
||||||
|
|
||||||
def __init__(self):
|
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)
|
||||||
|
self.device_connected = None
|
||||||
|
self.emailer = Emailer()
|
||||||
|
self.emailer.start()
|
||||||
|
self.device_manager = DeviceManager(Dispatcher(self.device_detected),
|
||||||
|
self.job_manager, Dispatcher(self.status_bar.show_message))
|
||||||
|
self.device_manager.start()
|
||||||
|
|
||||||
|
def set_default_thumbnail(self, height):
|
||||||
|
r = QSvgRenderer(I('book.svg'))
|
||||||
|
pixmap = QPixmap(height, height)
|
||||||
|
pixmap.fill(QColor(255,255,255))
|
||||||
|
p = QPainter(pixmap)
|
||||||
|
r.render(p)
|
||||||
|
p.end()
|
||||||
|
self.default_thumbnail = (pixmap.width(), pixmap.height(),
|
||||||
|
pixmap_to_data(pixmap))
|
||||||
|
|
||||||
|
def connect_to_folder(self):
|
||||||
|
dir = choose_dir(self, 'Select Device Folder',
|
||||||
|
_('Select folder to open as device'))
|
||||||
|
if dir is not None:
|
||||||
|
self.device_manager.connect_to_folder(dir)
|
||||||
|
|
||||||
|
def disconnect_from_folder(self):
|
||||||
|
self.device_manager.disconnect_folder()
|
||||||
|
|
||||||
|
def _sync_action_triggered(self, *args):
|
||||||
|
m = getattr(self, '_sync_menu', None)
|
||||||
|
if m is not None:
|
||||||
|
m.trigger_default()
|
||||||
|
|
||||||
|
def create_device_menu(self):
|
||||||
|
self._sync_menu = DeviceMenu(self)
|
||||||
|
self.action_sync.setMenu(self._sync_menu)
|
||||||
|
self.connect(self._sync_menu,
|
||||||
|
SIGNAL('sync(PyQt_PyObject, PyQt_PyObject, PyQt_PyObject)'),
|
||||||
|
self.dispatch_sync_event)
|
||||||
|
self._sync_menu.fetch_annotations.connect(self.fetch_annotations)
|
||||||
|
self._sync_menu.connect_to_folder.connect(self.connect_to_folder)
|
||||||
|
self._sync_menu.disconnect_from_folder.connect(self.disconnect_from_folder)
|
||||||
|
if self.device_connected:
|
||||||
|
self._sync_menu.connect_to_folder_action.setEnabled(False)
|
||||||
|
if self.device_connected == 'folder':
|
||||||
|
self._sync_menu.disconnect_from_folder_action.setEnabled(True)
|
||||||
|
else:
|
||||||
|
self._sync_menu.disconnect_from_folder_action.setEnabled(False)
|
||||||
|
else:
|
||||||
|
self._sync_menu.connect_to_folder_action.setEnabled(True)
|
||||||
|
self._sync_menu.disconnect_from_folder_action.setEnabled(False)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def device_job_exception(self, job):
|
||||||
|
'''
|
||||||
|
Handle exceptions in threaded device jobs.
|
||||||
|
'''
|
||||||
|
if isinstance(getattr(job, 'exception', None), UserFeedback):
|
||||||
|
ex = job.exception
|
||||||
|
func = {UserFeedback.ERROR:error_dialog,
|
||||||
|
UserFeedback.WARNING:warning_dialog,
|
||||||
|
UserFeedback.INFO:info_dialog}[ex.level]
|
||||||
|
return func(self, _('Failed'), ex.msg, det_msg=ex.details if
|
||||||
|
ex.details else '', show=True)
|
||||||
|
|
||||||
|
try:
|
||||||
|
if 'Could not read 32 bytes on the control bus.' in \
|
||||||
|
unicode(job.details):
|
||||||
|
error_dialog(self, _('Error talking to device'),
|
||||||
|
_('There was a temporary error talking to the '
|
||||||
|
'device. Please unplug and reconnect the device '
|
||||||
|
'and or reboot.')).show()
|
||||||
|
return
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
prints(job.details, file=sys.stderr)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
if not self.device_error_dialog.isVisible():
|
||||||
|
self.device_error_dialog.setDetailedText(job.details)
|
||||||
|
self.device_error_dialog.show()
|
||||||
|
|
||||||
|
# Device connected {{{
|
||||||
|
|
||||||
|
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(\
|
||||||
|
self.device_manager.device.THUMBNAIL_HEIGHT)
|
||||||
|
self.status_bar.show_message(_('Device: ')+\
|
||||||
|
self.device_manager.device.__class__.get_gui_name()+\
|
||||||
|
_(' detected.'), 3000)
|
||||||
|
self.device_connected = 'device' if not is_folder_device else 'folder'
|
||||||
|
self.location_view.model().device_connected(self.device_manager.device)
|
||||||
|
self.refresh_ondevice_info (device_connected = True, reset_only = True)
|
||||||
|
else:
|
||||||
|
self.device_connected = None
|
||||||
|
self.location_view.model().update_devices()
|
||||||
|
self.vanity.setText(self.vanity_template%\
|
||||||
|
dict(version=self.latest_version, device=' '))
|
||||||
|
self.device_info = ' '
|
||||||
|
if self.current_view() != self.library_view:
|
||||||
|
self.book_details.reset_info()
|
||||||
|
self.location_view.setCurrentIndex(self.location_view.model().index(0))
|
||||||
|
self.refresh_ondevice_info (device_connected = False)
|
||||||
|
|
||||||
|
def info_read(self, job):
|
||||||
|
'''
|
||||||
|
Called once device information has been read.
|
||||||
|
'''
|
||||||
|
if job.failed:
|
||||||
|
return self.device_job_exception(job)
|
||||||
|
info, cp, fs = job.result
|
||||||
|
self.location_view.model().update_devices(cp, fs)
|
||||||
|
self.device_info = _('Connected ')+info[0]
|
||||||
|
self.vanity.setText(self.vanity_template%\
|
||||||
|
dict(version=self.latest_version, device=self.device_info))
|
||||||
|
|
||||||
|
self.device_manager.books(Dispatcher(self.metadata_downloaded))
|
||||||
|
|
||||||
|
def metadata_downloaded(self, job):
|
||||||
|
'''
|
||||||
|
Called once metadata has been read for all books on the device.
|
||||||
|
'''
|
||||||
|
if job.failed:
|
||||||
|
self.device_job_exception(job)
|
||||||
|
return
|
||||||
|
self.set_books_in_library(job.result, reset=True)
|
||||||
|
mainlist, cardalist, cardblist = job.result
|
||||||
|
self.memory_view.set_database(mainlist)
|
||||||
|
self.memory_view.set_editable(self.device_manager.device.CAN_SET_METADATA)
|
||||||
|
self.card_a_view.set_database(cardalist)
|
||||||
|
self.card_a_view.set_editable(self.device_manager.device.CAN_SET_METADATA)
|
||||||
|
self.card_b_view.set_database(cardblist)
|
||||||
|
self.card_b_view.set_editable(self.device_manager.device.CAN_SET_METADATA)
|
||||||
|
self.sync_news()
|
||||||
|
self.sync_catalogs()
|
||||||
|
self.refresh_ondevice_info(device_connected = True)
|
||||||
|
|
||||||
|
def refresh_ondevice_info(self, device_connected, reset_only = False):
|
||||||
|
'''
|
||||||
|
Force the library view to refresh, taking into consideration
|
||||||
|
books information
|
||||||
|
'''
|
||||||
|
self.book_on_device(None, reset=True)
|
||||||
|
if reset_only:
|
||||||
|
return
|
||||||
|
self.library_view.set_device_connected(device_connected)
|
||||||
|
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
def remove_paths(self, paths):
|
||||||
|
return self.device_manager.delete_books(
|
||||||
|
Dispatcher(self.books_deleted), paths)
|
||||||
|
|
||||||
|
def books_deleted(self, job):
|
||||||
|
'''
|
||||||
|
Called once deletion is done on the device
|
||||||
|
'''
|
||||||
|
for view in (self.memory_view, self.card_a_view, self.card_b_view):
|
||||||
|
view.model().deletion_done(job, job.failed)
|
||||||
|
if job.failed:
|
||||||
|
self.device_job_exception(job)
|
||||||
|
return
|
||||||
|
|
||||||
|
if self.delete_memory.has_key(job):
|
||||||
|
paths, model = self.delete_memory.pop(job)
|
||||||
|
self.device_manager.remove_books_from_metadata(paths,
|
||||||
|
self.booklists())
|
||||||
|
model.paths_deleted(paths)
|
||||||
|
self.upload_booklists()
|
||||||
|
# Clear the ondevice info so it will be recomputed
|
||||||
|
self.book_on_device(None, None, reset=True)
|
||||||
|
# We want to reset all the ondevice flags in the library. Use a big
|
||||||
|
# hammer, so we don't need to worry about whether some succeeded or not
|
||||||
|
self.library_view.model().refresh()
|
||||||
|
|
||||||
|
|
||||||
def dispatch_sync_event(self, dest, delete, specific):
|
def dispatch_sync_event(self, dest, delete, specific):
|
||||||
rows = self.library_view.selectionModel().selectedRows()
|
rows = self.library_view.selectionModel().selectedRows()
|
||||||
@ -1148,11 +1349,18 @@ class DeviceMixin(object):
|
|||||||
return loc
|
return loc
|
||||||
|
|
||||||
def set_books_in_library(self, booklists, reset=False):
|
def set_books_in_library(self, booklists, reset=False):
|
||||||
if reset:
|
# Force a reset if the caches are not initialized
|
||||||
# First build a cache of the library, so the search isn't On**2
|
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_title_cache = {}
|
||||||
self.db_book_uuid_cache = set()
|
self.db_book_uuid_cache = {}
|
||||||
db = self.library_view.model().db
|
|
||||||
for id in db.data.iterallids():
|
for id in db.data.iterallids():
|
||||||
mi = db.get_metadata(id, index_is_id=True)
|
mi = db.get_metadata(id, index_is_id=True)
|
||||||
title = re.sub('(?u)\W|[_]', '', mi.title.lower())
|
title = re.sub('(?u)\W|[_]', '', mi.title.lower())
|
||||||
@ -1168,7 +1376,7 @@ class DeviceMixin(object):
|
|||||||
aus = re.sub('(?u)\W|[_]', '', aus)
|
aus = re.sub('(?u)\W|[_]', '', aus)
|
||||||
self.db_book_title_cache[title]['author_sort'][aus] = mi
|
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_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
|
# Now iterate through all the books on the device, setting the
|
||||||
# in_library field Fastest and most accurate key is the uuid. Second is
|
# in_library field Fastest and most accurate key is the uuid. Second is
|
||||||
@ -1180,11 +1388,13 @@ class DeviceMixin(object):
|
|||||||
for book in booklist:
|
for book in booklist:
|
||||||
if getattr(book, 'uuid', None) in self.db_book_uuid_cache:
|
if getattr(book, 'uuid', None) in self.db_book_uuid_cache:
|
||||||
book.in_library = True
|
book.in_library = True
|
||||||
|
# ensure that the correct application_id is set
|
||||||
|
book.application_id = self.db_book_uuid_cache[book.uuid]
|
||||||
continue
|
continue
|
||||||
|
|
||||||
book_title = book.title.lower() if book.title else ''
|
book_title = book.title.lower() if book.title else ''
|
||||||
book_title = re.sub('(?u)\W|[_]', '', book_title)
|
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)
|
d = self.db_book_title_cache.get(book_title, None)
|
||||||
if d is not None:
|
if d is not None:
|
||||||
if getattr(book, 'application_id', None) in d['db_ids']:
|
if getattr(book, 'application_id', None) in d['db_ids']:
|
||||||
@ -1213,10 +1423,13 @@ class DeviceMixin(object):
|
|||||||
# Set author_sort if it isn't already
|
# Set author_sort if it isn't already
|
||||||
asort = getattr(book, 'author_sort', None)
|
asort = getattr(book, 'author_sort', None)
|
||||||
if not asort and book.authors:
|
if not asort and book.authors:
|
||||||
book.author_sort = authors_to_sort_string(book.authors)
|
book.author_sort = self.library_view.model().db.author_sort_from_authors(book.authors)
|
||||||
resend_metadata = True
|
resend_metadata = True
|
||||||
|
|
||||||
if resend_metadata:
|
if resend_metadata:
|
||||||
# Correct the metadata cache on device.
|
# Correct the metadata cache on device.
|
||||||
if self.device_manager.is_device_connected:
|
if self.device_manager.is_device_connected:
|
||||||
self.device_manager.sync_booklists(None, booklists)
|
self.device_manager.sync_booklists(None, booklists)
|
||||||
|
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
@ -11,7 +11,8 @@ from calibre.gui2.device_drivers.configwidget_ui import Ui_ConfigWidget
|
|||||||
class ConfigWidget(QWidget, Ui_ConfigWidget):
|
class ConfigWidget(QWidget, Ui_ConfigWidget):
|
||||||
|
|
||||||
def __init__(self, settings, all_formats, supports_subdirs,
|
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)
|
QWidget.__init__(self)
|
||||||
Ui_ConfigWidget.__init__(self)
|
Ui_ConfigWidget.__init__(self)
|
||||||
@ -38,6 +39,10 @@ class ConfigWidget(QWidget, Ui_ConfigWidget):
|
|||||||
self.opt_read_metadata.setChecked(self.settings.read_metadata)
|
self.opt_read_metadata.setChecked(self.settings.read_metadata)
|
||||||
else:
|
else:
|
||||||
self.opt_read_metadata.hide()
|
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:
|
if extra_customization_message:
|
||||||
self.extra_customization_label.setText(extra_customization_message)
|
self.extra_customization_label.setText(extra_customization_message)
|
||||||
if settings.extra_customization:
|
if settings.extra_customization:
|
||||||
@ -69,3 +74,6 @@ class ConfigWidget(QWidget, Ui_ConfigWidget):
|
|||||||
|
|
||||||
def read_metadata(self):
|
def read_metadata(self):
|
||||||
return self.opt_read_metadata.isChecked()
|
return self.opt_read_metadata.isChecked()
|
||||||
|
|
||||||
|
def use_author_sort(self):
|
||||||
|
return self.opt_use_author_sort.isChecked()
|
||||||
|
@ -90,7 +90,14 @@
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</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">
|
<widget class="QLabel" name="extra_customization_label">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Extra customization</string>
|
<string>Extra customization</string>
|
||||||
@ -103,10 +110,10 @@
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="6" column="0">
|
<item row="7" column="0">
|
||||||
<widget class="QLineEdit" name="opt_extra_customization"/>
|
<widget class="QLineEdit" name="opt_extra_customization"/>
|
||||||
</item>
|
</item>
|
||||||
<item row="3" column="0">
|
<item row="4" column="0">
|
||||||
<widget class="QLabel" name="label">
|
<widget class="QLabel" name="label">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Save &template:</string>
|
<string>Save &template:</string>
|
||||||
@ -116,7 +123,7 @@
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="4" column="0">
|
<item row="5" column="0">
|
||||||
<widget class="QLineEdit" name="opt_save_template"/>
|
<widget class="QLineEdit" name="opt_save_template"/>
|
||||||
</item>
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
|
@ -121,6 +121,7 @@ class BookInfo(QDialog, Ui_BookInfo):
|
|||||||
f = f.strip()
|
f = f.strip()
|
||||||
info[_('Formats')] += '<a href="%s">%s</a>, '%(f,f)
|
info[_('Formats')] += '<a href="%s">%s</a>, '%(f,f)
|
||||||
for key in info.keys():
|
for key in info.keys():
|
||||||
|
if key == 'id': continue
|
||||||
txt = info[key]
|
txt = info[key]
|
||||||
txt = u'<br />\n'.join(textwrap.wrap(txt, 120))
|
txt = u'<br />\n'.join(textwrap.wrap(txt, 120))
|
||||||
rows += u'<tr><td><b>%s:</b></td><td>%s</td></tr>'%(key, txt)
|
rows += u'<tr><td><b>%s:</b></td><td>%s</td></tr>'%(key, txt)
|
||||||
|
@ -481,8 +481,18 @@ class ConfigDialog(ResizableDialog, Ui_Dialog):
|
|||||||
self.opt_enforce_cpu_limit.setChecked(config['enforce_cpu_limit'])
|
self.opt_enforce_cpu_limit.setChecked(config['enforce_cpu_limit'])
|
||||||
self.device_detection_button.clicked.connect(self.debug_device_detection)
|
self.device_detection_button.clicked.connect(self.debug_device_detection)
|
||||||
self.port.editingFinished.connect(self.check_port_value)
|
self.port.editingFinished.connect(self.check_port_value)
|
||||||
|
self.search_as_you_type.setChecked(config['search_as_you_type'])
|
||||||
|
self.show_avg_rating.setChecked(config['show_avg_rating'])
|
||||||
self.show_splash_screen.setChecked(gprefs.get('show_splash_screen',
|
self.show_splash_screen.setChecked(gprefs.get('show_splash_screen',
|
||||||
True))
|
True))
|
||||||
|
li = None
|
||||||
|
for i, z in enumerate([('wide', _('Wide')),
|
||||||
|
('narrow', _('Narrow'))]):
|
||||||
|
x, y = z
|
||||||
|
self.opt_gui_layout.addItem(y, QVariant(x))
|
||||||
|
if x == config['gui_layout']:
|
||||||
|
li = i
|
||||||
|
self.opt_gui_layout.setCurrentIndex(li)
|
||||||
|
|
||||||
def check_port_value(self, *args):
|
def check_port_value(self, *args):
|
||||||
port = self.port.value()
|
port = self.port.value()
|
||||||
@ -854,6 +864,7 @@ class ConfigDialog(ResizableDialog, Ui_Dialog):
|
|||||||
config['delete_news_from_library_on_upload'] = self.delete_news.isChecked()
|
config['delete_news_from_library_on_upload'] = self.delete_news.isChecked()
|
||||||
config['upload_news_to_device'] = self.sync_news.isChecked()
|
config['upload_news_to_device'] = self.sync_news.isChecked()
|
||||||
config['search_as_you_type'] = self.search_as_you_type.isChecked()
|
config['search_as_you_type'] = self.search_as_you_type.isChecked()
|
||||||
|
config['show_avg_rating'] = self.show_avg_rating.isChecked()
|
||||||
config['get_social_metadata'] = self.opt_get_social_metadata.isChecked()
|
config['get_social_metadata'] = self.opt_get_social_metadata.isChecked()
|
||||||
config['overwrite_author_title_metadata'] = self.opt_overwrite_author_title_metadata.isChecked()
|
config['overwrite_author_title_metadata'] = self.opt_overwrite_author_title_metadata.isChecked()
|
||||||
config['enforce_cpu_limit'] = bool(self.opt_enforce_cpu_limit.isChecked())
|
config['enforce_cpu_limit'] = bool(self.opt_enforce_cpu_limit.isChecked())
|
||||||
@ -863,6 +874,8 @@ class ConfigDialog(ResizableDialog, Ui_Dialog):
|
|||||||
if self.viewer.item(i).checkState() == Qt.Checked:
|
if self.viewer.item(i).checkState() == Qt.Checked:
|
||||||
fmts.append(str(self.viewer.item(i).text()))
|
fmts.append(str(self.viewer.item(i).text()))
|
||||||
config['internally_viewed_formats'] = fmts
|
config['internally_viewed_formats'] = fmts
|
||||||
|
val = self.opt_gui_layout.itemData(self.opt_gui_layout.currentIndex()).toString()
|
||||||
|
config['gui_layout'] = unicode(val)
|
||||||
|
|
||||||
if not path or not os.path.exists(path) or not os.path.isdir(path):
|
if not path or not os.path.exists(path) or not os.path.isdir(path):
|
||||||
d = error_dialog(self, _('Invalid database location'),
|
d = error_dialog(self, _('Invalid database location'),
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
<rect>
|
<rect>
|
||||||
<x>0</x>
|
<x>0</x>
|
||||||
<y>0</y>
|
<y>0</y>
|
||||||
<width>884</width>
|
<width>1000</width>
|
||||||
<height>730</height>
|
<height>730</height>
|
||||||
</rect>
|
</rect>
|
||||||
</property>
|
</property>
|
||||||
@ -89,8 +89,8 @@
|
|||||||
<rect>
|
<rect>
|
||||||
<x>0</x>
|
<x>0</x>
|
||||||
<y>0</y>
|
<y>0</y>
|
||||||
<width>608</width>
|
<width>720</width>
|
||||||
<height>683</height>
|
<height>679</height>
|
||||||
</rect>
|
</rect>
|
||||||
</property>
|
</property>
|
||||||
<layout class="QGridLayout" name="gridLayout_7">
|
<layout class="QGridLayout" name="gridLayout_7">
|
||||||
@ -332,7 +332,7 @@
|
|||||||
</widget>
|
</widget>
|
||||||
<widget class="QWidget" name="page">
|
<widget class="QWidget" name="page">
|
||||||
<layout class="QGridLayout" name="gridLayout_8">
|
<layout class="QGridLayout" name="gridLayout_8">
|
||||||
<item row="0" column="0">
|
<item row="1" column="0">
|
||||||
<widget class="QCheckBox" name="roman_numerals">
|
<widget class="QCheckBox" name="roman_numerals">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Use &Roman numerals for series number</string>
|
<string>Use &Roman numerals for series number</string>
|
||||||
@ -342,35 +342,45 @@
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="1" column="0">
|
<item row="2" column="0">
|
||||||
<widget class="QCheckBox" name="systray_icon">
|
<widget class="QCheckBox" name="systray_icon">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Enable system &tray icon (needs restart)</string>
|
<string>Enable system &tray icon (needs restart)</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="1" column="1">
|
<item row="2" column="1">
|
||||||
<widget class="QCheckBox" name="systray_notifications">
|
<widget class="QCheckBox" name="systray_notifications">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Show &notifications in system tray</string>
|
<string>Show &notifications in system tray</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="2" column="0" colspan="2">
|
<item row="3" column="0" colspan="2">
|
||||||
<widget class="QCheckBox" name="show_splash_screen">
|
<widget class="QCheckBox" name="show_splash_screen">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Show &splash screen at startup</string>
|
<string>Show &splash screen at startup</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="3" column="0" colspan="2">
|
<item row="4" column="0" colspan="2">
|
||||||
<widget class="QCheckBox" name="separate_cover_flow">
|
<widget class="QCheckBox" name="separate_cover_flow">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Show cover &browser in a separate window (needs restart)</string>
|
<string>Show cover &browser in a separate window (needs restart)</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="4" column="0">
|
<item row="5" column="0" colspan="2">
|
||||||
|
<widget class="QCheckBox" name="show_avg_rating">
|
||||||
|
<property name="text">
|
||||||
|
<string>Show &average ratings in the tags browser</string>
|
||||||
|
</property>
|
||||||
|
<property name="checked">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="6" column="0">
|
||||||
<widget class="QCheckBox" name="search_as_you_type">
|
<widget class="QCheckBox" name="search_as_you_type">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Search as you type</string>
|
<string>Search as you type</string>
|
||||||
@ -380,21 +390,21 @@
|
|||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="5" column="0" colspan="2">
|
<item row="7" column="0" colspan="2">
|
||||||
<widget class="QCheckBox" name="sync_news">
|
<widget class="QCheckBox" name="sync_news">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>Automatically send downloaded &news to ebook reader</string>
|
<string>Automatically send downloaded &news to ebook reader</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="6" column="0" colspan="2">
|
<item row="8" column="0" colspan="2">
|
||||||
<widget class="QCheckBox" name="delete_news">
|
<widget class="QCheckBox" name="delete_news">
|
||||||
<property name="text">
|
<property name="text">
|
||||||
<string>&Delete news from library when it is automatically sent to reader</string>
|
<string>&Delete news from library when it is automatically sent to reader</string>
|
||||||
</property>
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="7" column="0" colspan="2">
|
<item row="9" column="0" colspan="2">
|
||||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||||
<item>
|
<item>
|
||||||
<widget class="QLabel" name="label_6">
|
<widget class="QLabel" name="label_6">
|
||||||
@ -411,7 +421,7 @@
|
|||||||
</item>
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</item>
|
</item>
|
||||||
<item row="8" column="0" colspan="2">
|
<item row="10" column="0" colspan="2">
|
||||||
<widget class="QGroupBox" name="groupBox_2">
|
<widget class="QGroupBox" name="groupBox_2">
|
||||||
<property name="title">
|
<property name="title">
|
||||||
<string>Toolbar</string>
|
<string>Toolbar</string>
|
||||||
@ -459,7 +469,7 @@
|
|||||||
</layout>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
<item row="9" column="0" colspan="2">
|
<item row="11" column="0" colspan="2">
|
||||||
<layout class="QHBoxLayout" name="horizontalLayout_7">
|
<layout class="QHBoxLayout" name="horizontalLayout_7">
|
||||||
<item>
|
<item>
|
||||||
<widget class="QGroupBox" name="groupBox">
|
<widget class="QGroupBox" name="groupBox">
|
||||||
@ -625,6 +635,26 @@
|
|||||||
</item>
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</item>
|
</item>
|
||||||
|
<item row="0" column="0">
|
||||||
|
<widget class="QLabel" name="label_17">
|
||||||
|
<property name="text">
|
||||||
|
<string>User Interface &layout (needs restart):</string>
|
||||||
|
</property>
|
||||||
|
<property name="buddy">
|
||||||
|
<cstring>opt_gui_layout</cstring>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
|
<item row="0" column="1">
|
||||||
|
<widget class="QComboBox" name="opt_gui_layout">
|
||||||
|
<property name="maximumSize">
|
||||||
|
<size>
|
||||||
|
<width>250</width>
|
||||||
|
<height>16777215</height>
|
||||||
|
</size>
|
||||||
|
</property>
|
||||||
|
</widget>
|
||||||
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
</widget>
|
</widget>
|
||||||
<widget class="QWidget" name="page_6">
|
<widget class="QWidget" name="page_6">
|
||||||
|
@ -49,6 +49,9 @@ class SocialMetadata(QDialog):
|
|||||||
self.mi.tags = self.worker.mi.tags
|
self.mi.tags = self.worker.mi.tags
|
||||||
self.mi.rating = self.worker.mi.rating
|
self.mi.rating = self.worker.mi.rating
|
||||||
self.mi.comments = self.worker.mi.comments
|
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)
|
QDialog.accept(self)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
|
109
src/calibre/gui2/dialogs/delete_matching_from_device.py
Normal 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
|
||||||
|
|
90
src/calibre/gui2/dialogs/delete_matching_from_device.ui
Normal 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>
|
82
src/calibre/gui2/dialogs/edit_authors_dialog.py
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
#!/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.ebooks.metadata import author_to_author_sort
|
||||||
|
from calibre.gui2.dialogs.edit_authors_dialog_ui import Ui_EditAuthorsDialog
|
||||||
|
|
||||||
|
class tableItem(QTableWidgetItem):
|
||||||
|
def __ge__(self, other):
|
||||||
|
return unicode(self.text()).lower() >= unicode(other.text()).lower()
|
||||||
|
|
||||||
|
def __lt__(self, other):
|
||||||
|
return unicode(self.text()).lower() < unicode(other.text()).lower()
|
||||||
|
|
||||||
|
class EditAuthorsDialog(QDialog, Ui_EditAuthorsDialog):
|
||||||
|
|
||||||
|
def __init__(self, parent, db, id_to_select):
|
||||||
|
QDialog.__init__(self, parent)
|
||||||
|
Ui_EditAuthorsDialog.__init__(self)
|
||||||
|
self.setupUi(self)
|
||||||
|
|
||||||
|
self.buttonBox.accepted.connect(self.accepted)
|
||||||
|
|
||||||
|
self.table.setSelectionMode(QAbstractItemView.SingleSelection)
|
||||||
|
self.table.setColumnCount(2)
|
||||||
|
self.table.setHorizontalHeaderLabels([_('Author'), _('Author sort')])
|
||||||
|
|
||||||
|
self.authors = {}
|
||||||
|
auts = db.get_authors_with_ids()
|
||||||
|
self.table.setRowCount(len(auts))
|
||||||
|
select_item = None
|
||||||
|
for row, (id, author, sort) in enumerate(auts):
|
||||||
|
author = author.replace('|', ',')
|
||||||
|
self.authors[id] = (author, sort)
|
||||||
|
aut = tableItem(author)
|
||||||
|
aut.setData(Qt.UserRole, id)
|
||||||
|
sort = tableItem(sort)
|
||||||
|
self.table.setItem(row, 0, aut)
|
||||||
|
self.table.setItem(row, 1, sort)
|
||||||
|
if id == id_to_select:
|
||||||
|
select_item = sort
|
||||||
|
self.table.resizeColumnsToContents()
|
||||||
|
|
||||||
|
# set up the signal after the table is filled
|
||||||
|
self.table.cellChanged.connect(self.cell_changed)
|
||||||
|
|
||||||
|
self.table.setSortingEnabled(True)
|
||||||
|
self.table.sortByColumn(1, Qt.AscendingOrder)
|
||||||
|
if select_item is not None:
|
||||||
|
self.table.setCurrentItem(select_item)
|
||||||
|
self.table.editItem(select_item)
|
||||||
|
else:
|
||||||
|
self.table.setCurrentCell(0, 0)
|
||||||
|
|
||||||
|
def accepted(self):
|
||||||
|
self.result = []
|
||||||
|
for row in range(0,self.table.rowCount()):
|
||||||
|
id = self.table.item(row, 0).data(Qt.UserRole).toInt()[0]
|
||||||
|
aut = unicode(self.table.item(row, 0).text()).strip()
|
||||||
|
sort = unicode(self.table.item(row, 1).text()).strip()
|
||||||
|
orig_aut,orig_sort = self.authors[id]
|
||||||
|
if orig_aut != aut or orig_sort != sort:
|
||||||
|
self.result.append((id, orig_aut, aut, sort))
|
||||||
|
|
||||||
|
def cell_changed(self, row, col):
|
||||||
|
if col == 0:
|
||||||
|
item = self.table.item(row, 0)
|
||||||
|
aut = unicode(item.text()).strip()
|
||||||
|
c = self.table.item(row, 1)
|
||||||
|
c.setText(author_to_author_sort(aut))
|
||||||
|
item = c
|
||||||
|
else:
|
||||||
|
item = self.table.item(row, 1)
|
||||||
|
self.table.setCurrentItem(item)
|
||||||
|
# disable and reenable sorting to force the sort now, so we can scroll
|
||||||
|
# to the item after it moves
|
||||||
|
self.table.setSortingEnabled(False)
|
||||||
|
self.table.setSortingEnabled(True)
|
||||||
|
self.table.scrollToItem(item)
|
86
src/calibre/gui2/dialogs/edit_authors_dialog.ui
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<ui version="4.0">
|
||||||
|
<class>EditAuthorsDialog</class>
|
||||||
|
<widget class="QDialog" name="EditAuthorsDialog">
|
||||||
|
<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>Manage authors</string>
|
||||||
|
</property>
|
||||||
|
<layout class="QVBoxLayout" name="verticalLayout">
|
||||||
|
<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>EditAuthorsDialog</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>EditAuthorsDialog</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>
|
@ -8,7 +8,7 @@ from PyQt4.QtGui import QDialog, QGridLayout
|
|||||||
|
|
||||||
from calibre.gui2.dialogs.metadata_bulk_ui import Ui_MetadataBulkDialog
|
from calibre.gui2.dialogs.metadata_bulk_ui import Ui_MetadataBulkDialog
|
||||||
from calibre.gui2.dialogs.tag_editor import TagEditor
|
from calibre.gui2.dialogs.tag_editor import TagEditor
|
||||||
from calibre.ebooks.metadata import string_to_authors, authors_to_sort_string, \
|
from calibre.ebooks.metadata import string_to_authors, \
|
||||||
authors_to_string
|
authors_to_string
|
||||||
from calibre.gui2.custom_column_widgets import populate_bulk_metadata_page
|
from calibre.gui2.custom_column_widgets import populate_bulk_metadata_page
|
||||||
|
|
||||||
@ -110,10 +110,7 @@ class MetadataBulkDialog(QDialog, Ui_MetadataBulkDialog):
|
|||||||
au = string_to_authors(au)
|
au = string_to_authors(au)
|
||||||
self.db.set_authors(id, au, notify=False)
|
self.db.set_authors(id, au, notify=False)
|
||||||
if self.auto_author_sort.isChecked():
|
if self.auto_author_sort.isChecked():
|
||||||
aut = self.db.authors(id, index_is_id=True)
|
x = self.db.author_sort_from_book(id, index_is_id=True)
|
||||||
aut = aut if aut else ''
|
|
||||||
aut = [a.strip().replace('|', ',') for a in aut.strip().split(',')]
|
|
||||||
x = authors_to_sort_string(aut)
|
|
||||||
if x:
|
if x:
|
||||||
self.db.set_author_sort(id, x, notify=False)
|
self.db.set_author_sort(id, x, notify=False)
|
||||||
aus = unicode(self.author_sort.text())
|
aus = unicode(self.author_sort.text())
|
||||||
|
@ -23,7 +23,7 @@ from calibre.gui2.dialogs.fetch_metadata import FetchMetadata
|
|||||||
from calibre.gui2.dialogs.tag_editor import TagEditor
|
from calibre.gui2.dialogs.tag_editor import TagEditor
|
||||||
from calibre.gui2.widgets import ProgressIndicator
|
from calibre.gui2.widgets import ProgressIndicator
|
||||||
from calibre.ebooks import BOOK_EXTENSIONS
|
from calibre.ebooks import BOOK_EXTENSIONS
|
||||||
from calibre.ebooks.metadata import authors_to_sort_string, string_to_authors, \
|
from calibre.ebooks.metadata import string_to_authors, \
|
||||||
authors_to_string, check_isbn
|
authors_to_string, check_isbn
|
||||||
from calibre.ebooks.metadata.library_thing import cover_from_isbn
|
from calibre.ebooks.metadata.library_thing import cover_from_isbn
|
||||||
from calibre import islinux, isfreebsd
|
from calibre import islinux, isfreebsd
|
||||||
@ -357,7 +357,8 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
|
|||||||
aus = self.db.author_sort(row)
|
aus = self.db.author_sort(row)
|
||||||
self.author_sort.setText(aus if aus else '')
|
self.author_sort.setText(aus if aus else '')
|
||||||
tags = self.db.tags(row)
|
tags = self.db.tags(row)
|
||||||
self.tags.setText(', '.join(tags.split(',')) if tags else '')
|
self.original_tags = ', '.join(tags.split(',')) if tags else ''
|
||||||
|
self.tags.setText(self.original_tags)
|
||||||
self.tags.update_tags_cache(self.db.all_tags())
|
self.tags.update_tags_cache(self.db.all_tags())
|
||||||
rating = self.db.rating(row)
|
rating = self.db.rating(row)
|
||||||
if rating > 0:
|
if rating > 0:
|
||||||
@ -459,7 +460,7 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
|
|||||||
au = unicode(self.authors.text())
|
au = unicode(self.authors.text())
|
||||||
au = re.sub(r'\s+et al\.$', '', au)
|
au = re.sub(r'\s+et al\.$', '', au)
|
||||||
authors = string_to_authors(au)
|
authors = string_to_authors(au)
|
||||||
self.author_sort.setText(authors_to_sort_string(authors))
|
self.author_sort.setText(self.db.author_sort_from_authors(authors))
|
||||||
|
|
||||||
def swap_title_author(self):
|
def swap_title_author(self):
|
||||||
title = self.title.text()
|
title = self.title.text()
|
||||||
@ -527,6 +528,10 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
|
|||||||
self.publisher.setCurrentIndex(idx)
|
self.publisher.setCurrentIndex(idx)
|
||||||
|
|
||||||
def edit_tags(self):
|
def edit_tags(self):
|
||||||
|
if self.tags.text() != self.original_tags:
|
||||||
|
error_dialog(self, _('Cannot use tag editor'),
|
||||||
|
_('The tags editor cannot be used if you have modified the tags')).exec_()
|
||||||
|
return
|
||||||
d = TagEditor(self, self.db, self.row)
|
d = TagEditor(self, self.db, self.row)
|
||||||
d.exec_()
|
d.exec_()
|
||||||
if d.result() == QDialog.Accepted:
|
if d.result() == QDialog.Accepted:
|
||||||
|
@ -121,6 +121,9 @@
|
|||||||
<property name="standardButtons">
|
<property name="standardButtons">
|
||||||
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
|
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
|
||||||
</property>
|
</property>
|
||||||
|
<property name="centerButtons">
|
||||||
|
<bool>true</bool>
|
||||||
|
</property>
|
||||||
</widget>
|
</widget>
|
||||||
</item>
|
</item>
|
||||||
</layout>
|
</layout>
|
||||||
|
@ -8,15 +8,18 @@ __docformat__ = 'restructuredtext en'
|
|||||||
import functools
|
import functools
|
||||||
|
|
||||||
from PyQt4.Qt import QMenu, Qt, pyqtSignal, QToolButton, QIcon, QStackedWidget, \
|
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.utils.config import prefs
|
||||||
from calibre.ebooks import BOOK_EXTENSIONS
|
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 import config, is_widescreen
|
||||||
from calibre.gui2.library.views import BooksView, DeviceBooksView
|
from calibre.gui2.library.views import BooksView, DeviceBooksView
|
||||||
from calibre.gui2.widgets import Splitter
|
from calibre.gui2.widgets import Splitter
|
||||||
from calibre.gui2.tag_view import TagBrowserWidget
|
from calibre.gui2.tag_view import TagBrowserWidget
|
||||||
|
from calibre.gui2.book_details import BookDetails
|
||||||
|
from calibre.gui2.notify import get_notifier
|
||||||
|
|
||||||
|
|
||||||
_keep_refs = []
|
_keep_refs = []
|
||||||
|
|
||||||
@ -47,7 +50,7 @@ class ToolbarMixin(object): # {{{
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
md = QMenu()
|
md = QMenu()
|
||||||
md.addAction(_('Edit metadata individually'),
|
md.addAction(_('Edit metadata individually'),
|
||||||
partial(self.edit_metadata, False))
|
partial(self.edit_metadata, False, bulk=False))
|
||||||
md.addSeparator()
|
md.addSeparator()
|
||||||
md.addAction(_('Edit metadata in bulk'),
|
md.addAction(_('Edit metadata in bulk'),
|
||||||
partial(self.edit_metadata, False, bulk=True))
|
partial(self.edit_metadata, False, bulk=True))
|
||||||
@ -128,6 +131,10 @@ class ToolbarMixin(object): # {{{
|
|||||||
self.delete_all_but_selected_formats)
|
self.delete_all_but_selected_formats)
|
||||||
self.delete_menu.addAction(
|
self.delete_menu.addAction(
|
||||||
_('Remove covers from selected books'), self.delete_covers)
|
_('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_del.setMenu(self.delete_menu)
|
||||||
|
|
||||||
self.action_open_containing_folder.setShortcut(Qt.Key_O)
|
self.action_open_containing_folder.setShortcut(Qt.Key_O)
|
||||||
@ -156,8 +163,7 @@ class ToolbarMixin(object): # {{{
|
|||||||
self.convert_menu = cm
|
self.convert_menu = cm
|
||||||
|
|
||||||
pm = QMenu()
|
pm = QMenu()
|
||||||
ap = self.action_preferences
|
pm.addAction(QIcon(I('config.svg')), _('Preferences'), self.do_config)
|
||||||
pm.addAction(ap)
|
|
||||||
pm.addAction(QIcon(I('wizard.svg')), _('Run welcome wizard'),
|
pm.addAction(QIcon(I('wizard.svg')), _('Run welcome wizard'),
|
||||||
self.run_wizard)
|
self.run_wizard)
|
||||||
self.action_preferences.setMenu(pm)
|
self.action_preferences.setMenu(pm)
|
||||||
@ -212,20 +218,25 @@ class LibraryViewMixin(object): # {{{
|
|||||||
partial(self.show_similar_books, 'tag'))
|
partial(self.show_similar_books, 'tag'))
|
||||||
self.action_books_by_this_publisher.triggered.connect(
|
self.action_books_by_this_publisher.triggered.connect(
|
||||||
partial(self.show_similar_books, 'publisher'))
|
partial(self.show_similar_books, 'publisher'))
|
||||||
|
|
||||||
self.library_view.set_context_menu(self.action_edit, self.action_sync,
|
self.library_view.set_context_menu(self.action_edit, self.action_sync,
|
||||||
self.action_convert, self.action_view,
|
self.action_convert, self.action_view,
|
||||||
self.action_save,
|
self.action_save,
|
||||||
self.action_open_containing_folder,
|
self.action_open_containing_folder,
|
||||||
self.action_show_book_details,
|
self.action_show_book_details,
|
||||||
self.action_del,
|
self.action_del,
|
||||||
|
add_to_library = None,
|
||||||
similar_menu=similar_menu)
|
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.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.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.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)
|
self.library_view.files_dropped.connect(self.files_dropped, type=Qt.QueuedConnection)
|
||||||
for func, args in [
|
for func, args in [
|
||||||
@ -290,9 +301,9 @@ class LibraryViewMixin(object): # {{{
|
|||||||
class LibraryWidget(Splitter): # {{{
|
class LibraryWidget(Splitter): # {{{
|
||||||
|
|
||||||
def __init__(self, parent):
|
def __init__(self, parent):
|
||||||
orientation = Qt.Vertical if config['gui_layout'] == 'narrow' and \
|
orientation = Qt.Vertical
|
||||||
not is_widescreen() else Qt.Horizontal
|
if config['gui_layout'] == 'narrow':
|
||||||
#orientation = Qt.Vertical
|
orientation = Qt.Horizontal if is_widescreen() else Qt.Vertical
|
||||||
idx = 0 if orientation == Qt.Vertical else 1
|
idx = 0 if orientation == Qt.Vertical else 1
|
||||||
size = 300 if orientation == Qt.Vertical else 550
|
size = 300 if orientation == Qt.Vertical else 550
|
||||||
Splitter.__init__(self, 'cover_browser_splitter', _('Cover Browser'),
|
Splitter.__init__(self, 'cover_browser_splitter', _('Cover Browser'),
|
||||||
@ -330,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):
|
def show_message(self, msg, timeout=0):
|
||||||
QToolBar.__init__(self, _('Side bar'), parent)
|
QStatusBar.showMessage(self, msg, timeout)
|
||||||
self.setOrientation(Qt.Vertical)
|
if self.notifier is not None and not config['disable_tray_notification']:
|
||||||
self.setMovable(False)
|
if isosx and isinstance(msg, unicode):
|
||||||
self.setFloatable(False)
|
try:
|
||||||
self.setToolButtonStyle(Qt.ToolButtonIconOnly)
|
msg = msg.encode(preferred_encoding)
|
||||||
self.setIconSize(QSize(48, 48))
|
except UnicodeEncodeError:
|
||||||
self.spacer = QWidget(self)
|
msg = msg.encode('utf-8')
|
||||||
self.spacer.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Expanding)
|
self.notifier(msg)
|
||||||
for s in splitters:
|
|
||||||
self.addWidget(s.button)
|
|
||||||
self.addWidget(self.spacer)
|
|
||||||
self.addWidget(jobs_button)
|
|
||||||
|
|
||||||
for ch in self.children():
|
def clear_message(self):
|
||||||
if isinstance(ch, QToolButton):
|
QStatusBar.clearMessage(self)
|
||||||
ch.setCursor(Qt.PointingHandCursor)
|
|
||||||
|
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
@ -359,31 +368,52 @@ class LayoutMixin(object): # {{{
|
|||||||
self.setupUi(self)
|
self.setupUi(self)
|
||||||
self.setWindowTitle(__appname__)
|
self.setWindowTitle(__appname__)
|
||||||
|
|
||||||
if config['gui_layout'] == 'narrow':
|
if config['gui_layout'] == 'narrow': # narrow {{{
|
||||||
from calibre.gui2.status import StatusBar
|
self.book_details = BookDetails(False, self)
|
||||||
self.status_bar = self.book_details = StatusBar(self)
|
|
||||||
self.stack = Stack(self)
|
self.stack = Stack(self)
|
||||||
self.bd_splitter = Splitter('book_details_splitter',
|
self.bd_splitter = Splitter('book_details_splitter',
|
||||||
_('Book Details'), I('book.svg'),
|
_('Book Details'), I('book.svg'),
|
||||||
orientation=Qt.Vertical, parent=self, side_index=1)
|
orientation=Qt.Vertical, parent=self, side_index=1)
|
||||||
self._layout_mem = [QWidget(self), QHBoxLayout()]
|
self.bd_splitter.addWidget(self.stack)
|
||||||
self._layout_mem[0].setLayout(self._layout_mem[1])
|
self.bd_splitter.addWidget(self.book_details)
|
||||||
l = self._layout_mem[1]
|
self.bd_splitter.setCollapsible(self.bd_splitter.other_index, False)
|
||||||
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.setCollapsible((self.bd_splitter.side_index+1)%2, False)
|
|
||||||
self.centralwidget.layout().addWidget(self.bd_splitter)
|
self.centralwidget.layout().addWidget(self.bd_splitter)
|
||||||
|
# }}}
|
||||||
|
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(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)
|
||||||
|
# }}}
|
||||||
|
|
||||||
|
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):
|
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()
|
m = self.library_view.model()
|
||||||
if m.rowCount(None) > 0:
|
if m.rowCount(None) > 0:
|
||||||
self.library_view.set_current_row(0)
|
self.library_view.set_current_row(0)
|
||||||
m.current_changed(self.library_view.currentIndex(),
|
m.current_changed(self.library_view.currentIndex(),
|
||||||
self.library_view.currentIndex())
|
self.library_view.currentIndex())
|
||||||
|
self.library_view.setFocus(Qt.OtherFocusReason)
|
||||||
|
|
||||||
|
|
||||||
def save_layout_state(self):
|
def save_layout_state(self):
|
||||||
|
@ -274,11 +274,15 @@ class JobsButton(QFrame):
|
|||||||
|
|
||||||
def __init__(self, horizontal=False, size=48, parent=None):
|
def __init__(self, horizontal=False, size=48, parent=None):
|
||||||
QFrame.__init__(self, parent)
|
QFrame.__init__(self, parent)
|
||||||
|
if horizontal:
|
||||||
|
size = 24
|
||||||
self.pi = ProgressIndicator(self, size)
|
self.pi = ProgressIndicator(self, size)
|
||||||
self._jobs = QLabel('<b>'+_('Jobs:')+' 0')
|
self._jobs = QLabel('<b>'+_('Jobs:')+' 0')
|
||||||
|
self._jobs.mouseReleaseEvent = self.mouseReleaseEvent
|
||||||
|
|
||||||
if horizontal:
|
if horizontal:
|
||||||
self.setLayout(QHBoxLayout())
|
self.setLayout(QHBoxLayout())
|
||||||
|
self.layout().setDirection(self.layout().RightToLeft)
|
||||||
else:
|
else:
|
||||||
self.setLayout(QVBoxLayout())
|
self.setLayout(QVBoxLayout())
|
||||||
self._jobs.setAlignment(Qt.AlignHCenter|Qt.AlignBottom)
|
self._jobs.setAlignment(Qt.AlignHCenter|Qt.AlignBottom)
|
||||||
|
@ -21,7 +21,7 @@ from calibre.utils.date import dt_factory, qt_to_dt, isoformat
|
|||||||
from calibre.ebooks.metadata.meta import set_metadata as _set_metadata
|
from calibre.ebooks.metadata.meta import set_metadata as _set_metadata
|
||||||
from calibre.utils.search_query_parser import SearchQueryParser
|
from calibre.utils.search_query_parser import SearchQueryParser
|
||||||
from calibre.library.caches import _match, CONTAINS_MATCH, EQUALS_MATCH, REGEXP_MATCH
|
from calibre.library.caches import _match, CONTAINS_MATCH, EQUALS_MATCH, REGEXP_MATCH
|
||||||
from calibre import strftime, isbytestring
|
from calibre import strftime, isbytestring, prepare_string_for_xml
|
||||||
from calibre.constants import filesystem_encoding
|
from calibre.constants import filesystem_encoding
|
||||||
from calibre.gui2.library import DEFAULT_SORT
|
from calibre.gui2.library import DEFAULT_SORT
|
||||||
|
|
||||||
@ -300,6 +300,7 @@ class BooksModel(QAbstractTableModel): # {{{
|
|||||||
formats = _('None')
|
formats = _('None')
|
||||||
data[_('Formats')] = formats
|
data[_('Formats')] = formats
|
||||||
data[_('Path')] = self.db.abspath(idx)
|
data[_('Path')] = self.db.abspath(idx)
|
||||||
|
data['id'] = self.id(idx)
|
||||||
comments = self.db.comments(idx)
|
comments = self.db.comments(idx)
|
||||||
if not comments:
|
if not comments:
|
||||||
comments = _('None')
|
comments = _('None')
|
||||||
@ -308,7 +309,9 @@ class BooksModel(QAbstractTableModel): # {{{
|
|||||||
if series:
|
if series:
|
||||||
sidx = self.db.series_index(idx)
|
sidx = self.db.series_index(idx)
|
||||||
sidx = fmt_sidx(sidx, use_roman = self.use_roman_numbers)
|
sidx = fmt_sidx(sidx, use_roman = self.use_roman_numbers)
|
||||||
data[_('Series')] = _('Book <font face="serif">%s</font> of %s.')%(sidx, series)
|
data[_('Series')] = \
|
||||||
|
_('Book <font face="serif">%s</font> of %s.')%\
|
||||||
|
(sidx, prepare_string_for_xml(series))
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
@ -417,8 +420,11 @@ class BooksModel(QAbstractTableModel): # {{{
|
|||||||
pt.orig_file_path = os.path.abspath(src.name)
|
pt.orig_file_path = os.path.abspath(src.name)
|
||||||
pt.seek(0)
|
pt.seek(0)
|
||||||
if set_metadata:
|
if set_metadata:
|
||||||
|
try:
|
||||||
_set_metadata(pt, self.db.get_metadata(id, get_cover=True, index_is_id=True),
|
_set_metadata(pt, self.db.get_metadata(id, get_cover=True, index_is_id=True),
|
||||||
format)
|
format)
|
||||||
|
except:
|
||||||
|
traceback.print_exc()
|
||||||
pt.close()
|
pt.close()
|
||||||
def to_uni(x):
|
def to_uni(x):
|
||||||
if isbytestring(x):
|
if isbytestring(x):
|
||||||
@ -763,6 +769,7 @@ class OnDeviceSearch(SearchQueryParser): # {{{
|
|||||||
'format',
|
'format',
|
||||||
'formats',
|
'formats',
|
||||||
'title',
|
'title',
|
||||||
|
'inlibrary'
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@ -801,12 +808,23 @@ class OnDeviceSearch(SearchQueryParser): # {{{
|
|||||||
'author': lambda x: ' & '.join(getattr(x, 'authors')).lower(),
|
'author': lambda x: ' & '.join(getattr(x, 'authors')).lower(),
|
||||||
'collections':lambda x: ','.join(getattr(x, 'device_collections')).lower(),
|
'collections':lambda x: ','.join(getattr(x, 'device_collections')).lower(),
|
||||||
'format':lambda x: os.path.splitext(x.path)[1].lower(),
|
'format':lambda x: os.path.splitext(x.path)[1].lower(),
|
||||||
|
'inlibrary':lambda x : getattr(x, 'in_library')
|
||||||
}
|
}
|
||||||
for x in ('author', 'format'):
|
for x in ('author', 'format'):
|
||||||
q[x+'s'] = q[x]
|
q[x+'s'] = q[x]
|
||||||
for index, row in enumerate(self.model.db):
|
for index, row in enumerate(self.model.db):
|
||||||
for locvalue in locations:
|
for locvalue in locations:
|
||||||
accessor = q[locvalue]
|
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:
|
try:
|
||||||
### Can't separate authors because comma is used for name sep and author sep
|
### 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
|
### Exact match might not get what you want. For that reason, turn author
|
||||||
@ -856,7 +874,11 @@ class DeviceBooksModel(BooksModel): # {{{
|
|||||||
self.editable = True
|
self.editable = True
|
||||||
self.book_in_library = None
|
self.book_in_library = None
|
||||||
|
|
||||||
def mark_for_deletion(self, job, rows):
|
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)
|
self.marked_for_deletion[job] = self.indices(rows)
|
||||||
for row in rows:
|
for row in rows:
|
||||||
indices = self.row_indices(row)
|
indices = self.row_indices(row)
|
||||||
@ -882,13 +904,13 @@ class DeviceBooksModel(BooksModel): # {{{
|
|||||||
ans.extend(v)
|
ans.extend(v)
|
||||||
return ans
|
return ans
|
||||||
|
|
||||||
def clear_ondevice(self, db_ids):
|
def clear_ondevice(self, db_ids, to_what=None):
|
||||||
for data in self.db:
|
for data in self.db:
|
||||||
if data is None:
|
if data is None:
|
||||||
continue
|
continue
|
||||||
app_id = getattr(data, 'application_id', None)
|
app_id = getattr(data, 'application_id', None)
|
||||||
if app_id is not None and app_id in db_ids:
|
if app_id is not None and app_id in db_ids:
|
||||||
data.in_library = False
|
data.in_library = to_what
|
||||||
self.reset()
|
self.reset()
|
||||||
|
|
||||||
def flags(self, index):
|
def flags(self, index):
|
||||||
@ -897,8 +919,8 @@ class DeviceBooksModel(BooksModel): # {{{
|
|||||||
flags = QAbstractTableModel.flags(self, index)
|
flags = QAbstractTableModel.flags(self, index)
|
||||||
if index.isValid() and self.editable:
|
if index.isValid() and self.editable:
|
||||||
cname = self.column_map[index.column()]
|
cname = self.column_map[index.column()]
|
||||||
if cname in ('title', 'authors') or (cname == 'collection' and \
|
if cname in ('title', 'authors') or \
|
||||||
self.db.supports_collections()):
|
(cname == 'collections' and self.db.supports_collections()):
|
||||||
flags |= Qt.ItemIsEditable
|
flags |= Qt.ItemIsEditable
|
||||||
return flags
|
return flags
|
||||||
|
|
||||||
@ -1043,6 +1065,13 @@ class DeviceBooksModel(BooksModel): # {{{
|
|||||||
def paths(self, rows):
|
def paths(self, rows):
|
||||||
return [self.db[self.map[r.row()]].path for r in 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):
|
def indices(self, rows):
|
||||||
'''
|
'''
|
||||||
Return indices into underlying database from rows
|
Return indices into underlying database from rows
|
||||||
@ -1083,6 +1112,8 @@ class DeviceBooksModel(BooksModel): # {{{
|
|||||||
elif role == Qt.DecorationRole and cname == 'inlibrary':
|
elif role == Qt.DecorationRole and cname == 'inlibrary':
|
||||||
if self.db[self.map[row]].in_library:
|
if self.db[self.map[row]].in_library:
|
||||||
return QVariant(self.bool_yes_icon)
|
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:
|
elif role == Qt.TextAlignmentRole:
|
||||||
cname = self.column_map[index.column()]
|
cname = self.column_map[index.column()]
|
||||||
ans = Qt.AlignVCenter | ALIGNMENT_MAP[self.alignment_map.get(cname,
|
ans = Qt.AlignVCenter | ALIGNMENT_MAP[self.alignment_map.get(cname,
|
||||||
|
@ -370,7 +370,8 @@ class BooksView(QTableView): # {{{
|
|||||||
|
|
||||||
# Context Menu {{{
|
# Context Menu {{{
|
||||||
def set_context_menu(self, edit_metadata, send_to_device, convert, view,
|
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.setContextMenuPolicy(Qt.DefaultContextMenu)
|
||||||
self.context_menu = QMenu(self)
|
self.context_menu = QMenu(self)
|
||||||
if edit_metadata is not None:
|
if edit_metadata is not None:
|
||||||
@ -389,6 +390,9 @@ class BooksView(QTableView): # {{{
|
|||||||
self.context_menu.addAction(book_details)
|
self.context_menu.addAction(book_details)
|
||||||
if similar_menu is not None:
|
if similar_menu is not None:
|
||||||
self.context_menu.addMenu(similar_menu)
|
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):
|
def contextMenuEvent(self, event):
|
||||||
self.context_menu.popup(event.globalPos())
|
self.context_menu.popup(event.globalPos())
|
||||||
|
@ -84,12 +84,12 @@ class DownloadMetadata(Thread):
|
|||||||
if mi.isbn:
|
if mi.isbn:
|
||||||
args['isbn'] = mi.isbn
|
args['isbn'] = mi.isbn
|
||||||
else:
|
else:
|
||||||
if not mi.title:
|
if not mi.title or mi.title == _('Unknown'):
|
||||||
self.failures[id] = \
|
self.failures[id] = \
|
||||||
(str(id), _('Book has neither title nor ISBN'))
|
(str(id), _('Book has neither title nor ISBN'))
|
||||||
continue
|
continue
|
||||||
args['title'] = mi.title
|
args['title'] = mi.title
|
||||||
if mi.authors:
|
if mi.authors and mi.authors[0] != _('Unknown'):
|
||||||
args['author'] = mi.authors[0]
|
args['author'] = mi.authors[0]
|
||||||
if self.key:
|
if self.key:
|
||||||
args['isbndb_key'] = self.key
|
args['isbndb_key'] = self.key
|
||||||
@ -127,6 +127,10 @@ class DownloadMetadata(Thread):
|
|||||||
self.db.set_tags(id, mi.tags)
|
self.db.set_tags(id, mi.tags)
|
||||||
if mi.comments:
|
if mi.comments:
|
||||||
self.db.set_comment(id, 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)
|
self.updated = set(self.fetched_metadata)
|
||||||
|
|
||||||
|
@ -85,7 +85,9 @@ typedef long PFreal;
|
|||||||
|
|
||||||
typedef unsigned short QRgb565;
|
typedef unsigned short QRgb565;
|
||||||
|
|
||||||
#define FONT_SIZE 18
|
#define REFLECTION_FACTOR 1.5
|
||||||
|
|
||||||
|
#define MAX(x, y) ((x > y) ? x : y)
|
||||||
|
|
||||||
#define RGB565_RED_MASK 0xF800
|
#define RGB565_RED_MASK 0xF800
|
||||||
#define RGB565_GREEN_MASK 0x07E0
|
#define RGB565_GREEN_MASK 0x07E0
|
||||||
@ -124,6 +126,7 @@ inline PFreal floatToFixed(float val)
|
|||||||
return (PFreal)(val*PFREAL_ONE);
|
return (PFreal)(val*PFREAL_ONE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// sinTable {{{
|
||||||
#define IANGLE_MAX 1024
|
#define IANGLE_MAX 1024
|
||||||
#define IANGLE_MASK 1023
|
#define IANGLE_MASK 1023
|
||||||
|
|
||||||
@ -293,6 +296,7 @@ int main(int, char**)
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
// }}}
|
||||||
|
|
||||||
inline PFreal fsin(int iangle)
|
inline PFreal fsin(int iangle)
|
||||||
{
|
{
|
||||||
@ -315,6 +319,8 @@ struct SlideInfo
|
|||||||
PFreal cy;
|
PFreal cy;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// PicturePlowPrivate {{{
|
||||||
|
|
||||||
class PictureFlowPrivate
|
class PictureFlowPrivate
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
@ -369,6 +375,7 @@ private:
|
|||||||
|
|
||||||
int slideWidth;
|
int slideWidth;
|
||||||
int slideHeight;
|
int slideHeight;
|
||||||
|
int fontSize;
|
||||||
int zoom;
|
int zoom;
|
||||||
int queueLength;
|
int queueLength;
|
||||||
|
|
||||||
@ -406,6 +413,7 @@ PictureFlowPrivate::PictureFlowPrivate(PictureFlow* w, int queueLength_)
|
|||||||
|
|
||||||
slideWidth = 200;
|
slideWidth = 200;
|
||||||
slideHeight = 200;
|
slideHeight = 200;
|
||||||
|
fontSize = 10;
|
||||||
zoom = 100;
|
zoom = 100;
|
||||||
|
|
||||||
centerIndex = 0;
|
centerIndex = 0;
|
||||||
@ -488,6 +496,7 @@ int PictureFlowPrivate::currentSlide() const
|
|||||||
|
|
||||||
void PictureFlowPrivate::setCurrentSlide(int index)
|
void PictureFlowPrivate::setCurrentSlide(int index)
|
||||||
{
|
{
|
||||||
|
animateTimer.stop();
|
||||||
step = 0;
|
step = 0;
|
||||||
centerIndex = qBound(index, 0, slideImages->count()-1);
|
centerIndex = qBound(index, 0, slideImages->count()-1);
|
||||||
target = centerIndex;
|
target = centerIndex;
|
||||||
@ -542,8 +551,11 @@ void PictureFlowPrivate::showSlide(int index)
|
|||||||
|
|
||||||
void PictureFlowPrivate::resize(int w, int h)
|
void PictureFlowPrivate::resize(int w, int h)
|
||||||
{
|
{
|
||||||
slideHeight = int(float(h)/2.);
|
if (w < 10) w = 10;
|
||||||
|
if (h < 10) h = 10;
|
||||||
|
slideHeight = int(float(h)/REFLECTION_FACTOR);
|
||||||
slideWidth = int(float(slideHeight) * 2/3.);
|
slideWidth = int(float(slideHeight) * 2/3.);
|
||||||
|
fontSize = MAX(int(h/15.), 12);
|
||||||
recalc(w, h);
|
recalc(w, h);
|
||||||
resetSlides();
|
resetSlides();
|
||||||
triggerRender();
|
triggerRender();
|
||||||
@ -592,8 +604,8 @@ static QImage prepareSurface(QImage img, int w, int h)
|
|||||||
img = img.scaled(w, h, Qt::IgnoreAspectRatio, mode);
|
img = img.scaled(w, h, Qt::IgnoreAspectRatio, mode);
|
||||||
|
|
||||||
// slightly larger, to accomodate for the reflection
|
// slightly larger, to accomodate for the reflection
|
||||||
int hs = h * 2;
|
int hs = int(h * REFLECTION_FACTOR);
|
||||||
int hofs = h / 3;
|
int hofs = 0;
|
||||||
|
|
||||||
// offscreen buffer: black is sweet
|
// offscreen buffer: black is sweet
|
||||||
QImage result(hs, w, QImage::Format_RGB16);
|
QImage result(hs, w, QImage::Format_RGB16);
|
||||||
@ -715,13 +727,13 @@ void PictureFlowPrivate::render()
|
|||||||
|
|
||||||
QFont font = QFont();
|
QFont font = QFont();
|
||||||
font.setBold(true);
|
font.setBold(true);
|
||||||
font.setPointSize(FONT_SIZE);
|
font.setPixelSize(fontSize);
|
||||||
painter.setFont(font);
|
painter.setFont(font);
|
||||||
painter.setPen(Qt::white);
|
painter.setPen(Qt::white);
|
||||||
//painter.setPen(QColor(255,255,255,127));
|
//painter.setPen(QColor(255,255,255,127));
|
||||||
|
|
||||||
if (centerIndex < slideCount() && centerIndex > -1)
|
if (centerIndex < slideCount() && centerIndex > -1)
|
||||||
painter.drawText( QRect(0,0, buffer.width(), buffer.height()*2-FONT_SIZE*3),
|
painter.drawText( QRect(0,0, buffer.width(), buffer.height()*2-fontSize*3),
|
||||||
Qt::AlignCenter, slideImages->caption(centerIndex));
|
Qt::AlignCenter, slideImages->caption(centerIndex));
|
||||||
|
|
||||||
painter.end();
|
painter.end();
|
||||||
@ -766,7 +778,7 @@ void PictureFlowPrivate::render()
|
|||||||
|
|
||||||
QFont font = QFont();
|
QFont font = QFont();
|
||||||
font.setBold(true);
|
font.setBold(true);
|
||||||
font.setPointSize(FONT_SIZE);
|
font.setPixelSize(fontSize);
|
||||||
painter.setFont(font);
|
painter.setFont(font);
|
||||||
|
|
||||||
int leftTextIndex = (step>0) ? centerIndex : centerIndex-1;
|
int leftTextIndex = (step>0) ? centerIndex : centerIndex-1;
|
||||||
@ -774,12 +786,12 @@ void PictureFlowPrivate::render()
|
|||||||
|
|
||||||
painter.setPen(QColor(255,255,255, (255-fade) ));
|
painter.setPen(QColor(255,255,255, (255-fade) ));
|
||||||
if (leftTextIndex < sc && leftTextIndex > -1)
|
if (leftTextIndex < sc && leftTextIndex > -1)
|
||||||
painter.drawText( QRect(0,0, buffer.width(), buffer.height()*2 - FONT_SIZE*3),
|
painter.drawText( QRect(0,0, buffer.width(), buffer.height()*2 - fontSize*3),
|
||||||
Qt::AlignCenter, slideImages->caption(leftTextIndex));
|
Qt::AlignCenter, slideImages->caption(leftTextIndex));
|
||||||
|
|
||||||
painter.setPen(QColor(255,255,255, fade));
|
painter.setPen(QColor(255,255,255, fade));
|
||||||
if (leftTextIndex+1 < sc && leftTextIndex > -2)
|
if (leftTextIndex+1 < sc && leftTextIndex > -2)
|
||||||
painter.drawText( QRect(0,0, buffer.width(), buffer.height()*2 - FONT_SIZE*3),
|
painter.drawText( QRect(0,0, buffer.width(), buffer.height()*2 - fontSize*3),
|
||||||
Qt::AlignCenter, slideImages->caption(leftTextIndex+1));
|
Qt::AlignCenter, slideImages->caption(leftTextIndex+1));
|
||||||
|
|
||||||
|
|
||||||
@ -893,7 +905,7 @@ int col1, int col2)
|
|||||||
int center = (sh*BILINEAR_STRETCH_VER/2);
|
int center = (sh*BILINEAR_STRETCH_VER/2);
|
||||||
int dy = dist*BILINEAR_STRETCH_VER / h;
|
int dy = dist*BILINEAR_STRETCH_VER / h;
|
||||||
#else
|
#else
|
||||||
int center = (sh/2);
|
int center = sh/2;
|
||||||
int dy = dist / h;
|
int dy = dist / h;
|
||||||
#endif
|
#endif
|
||||||
int p1 = center*PFREAL_ONE - dy/2;
|
int p1 = center*PFREAL_ONE - dy/2;
|
||||||
@ -1110,8 +1122,9 @@ void PictureFlowPrivate::clearSurfaceCache()
|
|||||||
surfaceCache.clear();
|
surfaceCache.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
// -----------------------------------------
|
// }}}
|
||||||
|
|
||||||
|
// PictureFlow {{{
|
||||||
PictureFlow::PictureFlow(QWidget* parent, int queueLength): QWidget(parent)
|
PictureFlow::PictureFlow(QWidget* parent, int queueLength): QWidget(parent)
|
||||||
{
|
{
|
||||||
d = new PictureFlowPrivate(this, queueLength);
|
d = new PictureFlowPrivate(this, queueLength);
|
||||||
@ -1387,3 +1400,5 @@ void PictureFlow::emitcurrentChanged(int index) { emit currentChanged(index); }
|
|||||||
int FlowImages::count() { return 0; }
|
int FlowImages::count() { return 0; }
|
||||||
QImage FlowImages::image(int index) { index=0; return QImage(); }
|
QImage FlowImages::image(int index) { index=0; return QImage(); }
|
||||||
QString FlowImages::caption(int index) {index=0; return QString(); }
|
QString FlowImages::caption(int index) {index=0; return QString(); }
|
||||||
|
|
||||||
|
// }}}
|
||||||
|
@ -18,17 +18,20 @@ from calibre.utils.config import prefs
|
|||||||
from calibre.utils.search_query_parser import saved_searches
|
from calibre.utils.search_query_parser import saved_searches
|
||||||
|
|
||||||
class SearchLineEdit(QLineEdit):
|
class SearchLineEdit(QLineEdit):
|
||||||
|
key_pressed = pyqtSignal(object)
|
||||||
|
mouse_released = pyqtSignal(object)
|
||||||
|
focus_out = pyqtSignal(object)
|
||||||
|
|
||||||
def keyPressEvent(self, event):
|
def keyPressEvent(self, event):
|
||||||
self.emit(SIGNAL('key_pressed(PyQt_PyObject)'), event)
|
self.key_pressed.emit(event)
|
||||||
QLineEdit.keyPressEvent(self, event)
|
QLineEdit.keyPressEvent(self, event)
|
||||||
|
|
||||||
def mouseReleaseEvent(self, event):
|
def mouseReleaseEvent(self, event):
|
||||||
self.emit(SIGNAL('mouse_released(PyQt_PyObject)'), event)
|
self.mouse_released.emit(event)
|
||||||
QLineEdit.mouseReleaseEvent(self, event)
|
QLineEdit.mouseReleaseEvent(self, event)
|
||||||
|
|
||||||
def focusOutEvent(self, event):
|
def focusOutEvent(self, event):
|
||||||
self.emit(SIGNAL('focus_out(PyQt_PyObject)'), event)
|
self.focus_out.emit(event)
|
||||||
QLineEdit.focusOutEvent(self, event)
|
QLineEdit.focusOutEvent(self, event)
|
||||||
|
|
||||||
def dropEvent(self, ev):
|
def dropEvent(self, ev):
|
||||||
@ -68,10 +71,10 @@ class SearchBox2(QComboBox):
|
|||||||
self.normal_background = 'rgb(255, 255, 255, 0%)'
|
self.normal_background = 'rgb(255, 255, 255, 0%)'
|
||||||
self.line_edit = SearchLineEdit(self)
|
self.line_edit = SearchLineEdit(self)
|
||||||
self.setLineEdit(self.line_edit)
|
self.setLineEdit(self.line_edit)
|
||||||
self.connect(self.line_edit, SIGNAL('key_pressed(PyQt_PyObject)'),
|
self.line_edit.key_pressed.connect(self.key_pressed,
|
||||||
self.key_pressed, Qt.DirectConnection)
|
type=Qt.DirectConnection)
|
||||||
self.connect(self.line_edit, SIGNAL('mouse_released(PyQt_PyObject)'),
|
self.line_edit.mouse_released.connect(self.mouse_released,
|
||||||
self.mouse_released, Qt.DirectConnection)
|
type=Qt.DirectConnection)
|
||||||
self.setEditable(True)
|
self.setEditable(True)
|
||||||
self.help_state = False
|
self.help_state = False
|
||||||
self.as_you_type = True
|
self.as_you_type = True
|
||||||
@ -90,14 +93,18 @@ class SearchBox2(QComboBox):
|
|||||||
self.help_text = help_text
|
self.help_text = help_text
|
||||||
self.colorize = colorize
|
self.colorize = colorize
|
||||||
self.clear_to_help()
|
self.clear_to_help()
|
||||||
self.connect(self, SIGNAL('editTextChanged(QString)'), self.text_edited_slot)
|
|
||||||
|
|
||||||
def normalize_state(self):
|
def normalize_state(self):
|
||||||
|
if self.help_state:
|
||||||
self.setEditText('')
|
self.setEditText('')
|
||||||
self.line_edit.setStyleSheet(
|
self.line_edit.setStyleSheet(
|
||||||
'QLineEdit { color: black; background-color: %s; }' %
|
'QLineEdit { color: black; background-color: %s; }' %
|
||||||
self.normal_background)
|
self.normal_background)
|
||||||
self.help_state = False
|
self.help_state = False
|
||||||
|
else:
|
||||||
|
self.line_edit.setStyleSheet(
|
||||||
|
'QLineEdit { color: black; background-color: %s; }' %
|
||||||
|
self.normal_background)
|
||||||
|
|
||||||
def clear_to_help(self):
|
def clear_to_help(self):
|
||||||
if self.help_state:
|
if self.help_state:
|
||||||
@ -131,17 +138,13 @@ class SearchBox2(QComboBox):
|
|||||||
self.line_edit.setStyleSheet('QLineEdit { color: black; background-color: %s; }' % col)
|
self.line_edit.setStyleSheet('QLineEdit { color: black; background-color: %s; }' % col)
|
||||||
|
|
||||||
def key_pressed(self, event):
|
def key_pressed(self, event):
|
||||||
if self.help_state:
|
|
||||||
self.normalize_state()
|
self.normalize_state()
|
||||||
if not self.as_you_type:
|
|
||||||
if event.key() in (Qt.Key_Return, Qt.Key_Enter):
|
if event.key() in (Qt.Key_Return, Qt.Key_Enter):
|
||||||
self.do_search()
|
self.do_search()
|
||||||
|
self.timer = self.startTimer(self.__class__.INTERVAL)
|
||||||
|
|
||||||
def mouse_released(self, event):
|
def mouse_released(self, event):
|
||||||
if self.help_state:
|
|
||||||
self.normalize_state()
|
self.normalize_state()
|
||||||
|
|
||||||
def text_edited_slot(self, text):
|
|
||||||
if self.as_you_type:
|
if self.as_you_type:
|
||||||
self.timer = self.startTimer(self.__class__.INTERVAL)
|
self.timer = self.startTimer(self.__class__.INTERVAL)
|
||||||
|
|
||||||
@ -227,14 +230,13 @@ class SavedSearchBox(QComboBox):
|
|||||||
|
|
||||||
self.line_edit = SearchLineEdit(self)
|
self.line_edit = SearchLineEdit(self)
|
||||||
self.setLineEdit(self.line_edit)
|
self.setLineEdit(self.line_edit)
|
||||||
self.connect(self.line_edit, SIGNAL('key_pressed(PyQt_PyObject)'),
|
self.line_edit.key_pressed.connect(self.key_pressed,
|
||||||
self.key_pressed, Qt.DirectConnection)
|
type=Qt.DirectConnection)
|
||||||
self.connect(self.line_edit, SIGNAL('mouse_released(PyQt_PyObject)'),
|
self.line_edit.mouse_released.connect(self.mouse_released,
|
||||||
self.mouse_released, Qt.DirectConnection)
|
type=Qt.DirectConnection)
|
||||||
self.connect(self.line_edit, SIGNAL('focus_out(PyQt_PyObject)'),
|
self.line_edit.focus_out.connect(self.focus_out,
|
||||||
self.focus_out, Qt.DirectConnection)
|
type=Qt.DirectConnection)
|
||||||
self.connect(self, SIGNAL('activated(const QString&)'),
|
self.activated[str].connect(self.saved_search_selected)
|
||||||
self.saved_search_selected)
|
|
||||||
|
|
||||||
completer = QCompleter(self) # turn off auto-completion
|
completer = QCompleter(self) # turn off auto-completion
|
||||||
self.setCompleter(completer)
|
self.setCompleter(completer)
|
||||||
@ -282,7 +284,7 @@ class SavedSearchBox(QComboBox):
|
|||||||
if self.help_state:
|
if self.help_state:
|
||||||
self.normalize_state()
|
self.normalize_state()
|
||||||
|
|
||||||
def saved_search_selected (self, qname):
|
def saved_search_selected(self, qname):
|
||||||
qname = unicode(qname)
|
qname = unicode(qname)
|
||||||
if qname is None or not qname.strip():
|
if qname is None or not qname.strip():
|
||||||
return
|
return
|
||||||
|
@ -1,228 +0,0 @@
|
|||||||
__license__ = 'GPL v3'
|
|
||||||
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
|
|
||||||
import os, collections
|
|
||||||
|
|
||||||
from PyQt4.QtGui import QStatusBar, QLabel, QWidget, QHBoxLayout, QPixmap, \
|
|
||||||
QSizePolicy, QScrollArea
|
|
||||||
from PyQt4.QtCore import Qt, QSize, pyqtSignal
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
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.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):
|
|
||||||
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()))
|
|
||||||
|
|
||||||
def setPixmap(self, pixmap):
|
|
||||||
QLabel.setPixmap(self, pixmap)
|
|
||||||
self.do_layout()
|
|
||||||
|
|
||||||
|
|
||||||
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(int)
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
QLabel.__init__(self)
|
|
||||||
self.setText('')
|
|
||||||
self.setWordWrap(True)
|
|
||||||
self.setSizePolicy(QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding))
|
|
||||||
|
|
||||||
def mouseReleaseEvent(self, ev):
|
|
||||||
self.mr.emit(1)
|
|
||||||
|
|
||||||
WEIGHTS = collections.defaultdict(lambda : 100)
|
|
||||||
WEIGHTS[_('Path')] = 0
|
|
||||||
WEIGHTS[_('Formats')] = 1
|
|
||||||
WEIGHTS[_('Collections')] = 2
|
|
||||||
WEIGHTS[_('Series')] = 3
|
|
||||||
WEIGHTS[_('Tags')] = 4
|
|
||||||
WEIGHTS[_('Comments')] = 5
|
|
||||||
|
|
||||||
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):
|
|
||||||
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()
|
|
||||||
keys = data.keys()
|
|
||||||
keys.sort(cmp=lambda x, y: cmp(self.WEIGHTS[x], self.WEIGHTS[y]))
|
|
||||||
for key in keys:
|
|
||||||
txt = data[key]
|
|
||||||
if not txt or not txt.strip() or txt == 'None':
|
|
||||||
continue
|
|
||||||
if isinstance(key, str):
|
|
||||||
key = key.decode(preferred_encoding, 'replace')
|
|
||||||
if isinstance(txt, str):
|
|
||||||
txt = txt.decode(preferred_encoding, 'replace')
|
|
||||||
if key == _('Comments'):
|
|
||||||
comments = comments_to_html(txt)
|
|
||||||
else:
|
|
||||||
rows.append((key, txt))
|
|
||||||
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:
|
|
||||||
comments = '<b>Comments:</b>'+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)
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
def reset_info(self):
|
|
||||||
raise NotImplementedError()
|
|
||||||
|
|
||||||
def show_data(self, data):
|
|
||||||
raise NotImplementedError()
|
|
||||||
|
|
||||||
class StatusBar(QStatusBar, StatusBarInterface, BookDetailsInterface):
|
|
||||||
|
|
||||||
files_dropped = pyqtSignal(object, object)
|
|
||||||
show_book_info = pyqtSignal()
|
|
||||||
|
|
||||||
|
|
||||||
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.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 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)
|
|
||||||
|
|
@ -10,10 +10,10 @@ Browsing book collection by tags.
|
|||||||
from itertools import izip
|
from itertools import izip
|
||||||
from functools import partial
|
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, \
|
QFont, QSize, QIcon, QPoint, QVBoxLayout, QComboBox, \
|
||||||
QAbstractItemModel, QVariant, QModelIndex, QMenu, \
|
QAbstractItemModel, QVariant, QModelIndex, QMenu, \
|
||||||
QPushButton, QWidget
|
QPushButton, QWidget, QItemDelegate
|
||||||
|
|
||||||
from calibre.gui2 import config, NONE
|
from calibre.gui2 import config, NONE
|
||||||
from calibre.utils.config import prefs
|
from calibre.utils.config import prefs
|
||||||
@ -22,6 +22,39 @@ from calibre.utils.search_query_parser import saved_searches
|
|||||||
from calibre.gui2 import error_dialog
|
from calibre.gui2 import error_dialog
|
||||||
from calibre.gui2.dialogs.tag_categories import TagCategories
|
from calibre.gui2.dialogs.tag_categories import TagCategories
|
||||||
from calibre.gui2.dialogs.tag_list_editor import TagListEditor
|
from calibre.gui2.dialogs.tag_list_editor import TagListEditor
|
||||||
|
from calibre.gui2.dialogs.edit_authors_dialog import EditAuthorsDialog
|
||||||
|
|
||||||
|
class TagDelegate(QItemDelegate): # {{{
|
||||||
|
|
||||||
|
def paint(self, painter, option, index):
|
||||||
|
item = index.internalPointer()
|
||||||
|
if item.type != TagTreeItem.TAG:
|
||||||
|
QItemDelegate.paint(self, painter, option, index)
|
||||||
|
return
|
||||||
|
r = option.rect
|
||||||
|
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 text
|
||||||
|
r.setLeft(r.left()+r.height()+3)
|
||||||
|
painter.drawText(r, Qt.AlignLeft|Qt.AlignVCenter,
|
||||||
|
model.data(index, Qt.DisplayRole).toString())
|
||||||
|
painter.restore()
|
||||||
|
|
||||||
|
# }}}
|
||||||
|
|
||||||
class TagsView(QTreeView): # {{{
|
class TagsView(QTreeView): # {{{
|
||||||
|
|
||||||
@ -30,6 +63,7 @@ class TagsView(QTreeView): # {{{
|
|||||||
user_category_edit = pyqtSignal(object)
|
user_category_edit = pyqtSignal(object)
|
||||||
tag_list_edit = pyqtSignal(object, object)
|
tag_list_edit = pyqtSignal(object, object)
|
||||||
saved_search_edit = pyqtSignal(object)
|
saved_search_edit = pyqtSignal(object)
|
||||||
|
author_sort_edit = pyqtSignal(object, object)
|
||||||
tag_item_renamed = pyqtSignal()
|
tag_item_renamed = pyqtSignal()
|
||||||
search_item_renamed = pyqtSignal()
|
search_item_renamed = pyqtSignal()
|
||||||
|
|
||||||
@ -43,13 +77,14 @@ class TagsView(QTreeView): # {{{
|
|||||||
self.setAlternatingRowColors(True)
|
self.setAlternatingRowColors(True)
|
||||||
self.setAnimated(True)
|
self.setAnimated(True)
|
||||||
self.setHeaderHidden(True)
|
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.hidden_categories = config['tag_browser_hidden_categories']
|
||||||
self._model = TagsModel(db, parent=self,
|
self._model = TagsModel(db, parent=self,
|
||||||
hidden_categories=self.hidden_categories,
|
hidden_categories=self.hidden_categories,
|
||||||
search_restriction=None)
|
search_restriction=None)
|
||||||
self.popularity = popularity
|
self.sort_by = sort_by
|
||||||
self.tag_match = tag_match
|
self.tag_match = tag_match
|
||||||
self.db = db
|
self.db = db
|
||||||
self.search_restriction = None
|
self.search_restriction = None
|
||||||
@ -57,8 +92,9 @@ class TagsView(QTreeView): # {{{
|
|||||||
self.setContextMenuPolicy(Qt.CustomContextMenu)
|
self.setContextMenuPolicy(Qt.CustomContextMenu)
|
||||||
self.clicked.connect(self.toggle)
|
self.clicked.connect(self.toggle)
|
||||||
self.customContextMenuRequested.connect(self.show_context_menu)
|
self.customContextMenuRequested.connect(self.show_context_menu)
|
||||||
self.popularity.setChecked(config['sort_by_popularity'])
|
pop = config['sort_tags_by']
|
||||||
self.popularity.stateChanged.connect(self.sort_changed)
|
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)
|
self.refresh_required.connect(self.recount, type=Qt.QueuedConnection)
|
||||||
db.add_listener(self.database_changed)
|
db.add_listener(self.database_changed)
|
||||||
|
|
||||||
@ -69,8 +105,8 @@ class TagsView(QTreeView): # {{{
|
|||||||
def match_all(self):
|
def match_all(self):
|
||||||
return self.tag_match and self.tag_match.currentIndex() > 0
|
return self.tag_match and self.tag_match.currentIndex() > 0
|
||||||
|
|
||||||
def sort_changed(self, state):
|
def sort_changed(self, pop):
|
||||||
config.set('sort_by_popularity', state == Qt.Checked)
|
config.set('sort_tags_by', self.db.CATEGORY_SORTS[pop])
|
||||||
self.recount()
|
self.recount()
|
||||||
|
|
||||||
def set_search_restriction(self, s):
|
def set_search_restriction(self, s):
|
||||||
@ -112,6 +148,9 @@ class TagsView(QTreeView): # {{{
|
|||||||
if action == 'manage_searches':
|
if action == 'manage_searches':
|
||||||
self.saved_search_edit.emit(category)
|
self.saved_search_edit.emit(category)
|
||||||
return
|
return
|
||||||
|
if action == 'edit_author_sort':
|
||||||
|
self.author_sort_edit.emit(self, index)
|
||||||
|
return
|
||||||
if action == 'hide':
|
if action == 'hide':
|
||||||
self.hidden_categories.add(category)
|
self.hidden_categories.add(category)
|
||||||
elif action == 'show':
|
elif action == 'show':
|
||||||
@ -132,6 +171,7 @@ class TagsView(QTreeView): # {{{
|
|||||||
if item.type == TagTreeItem.TAG:
|
if item.type == TagTreeItem.TAG:
|
||||||
tag_item = item
|
tag_item = item
|
||||||
tag_name = item.tag.name
|
tag_name = item.tag.name
|
||||||
|
tag_id = item.tag.id
|
||||||
item = item.parent
|
item = item.parent
|
||||||
if item.type == TagTreeItem.CATEGORY:
|
if item.type == TagTreeItem.CATEGORY:
|
||||||
category = unicode(item.name.toString())
|
category = unicode(item.name.toString())
|
||||||
@ -147,9 +187,13 @@ class TagsView(QTreeView): # {{{
|
|||||||
(key in ['authors', 'tags', 'series', 'publisher', 'search'] or \
|
(key in ['authors', 'tags', 'series', 'publisher', 'search'] or \
|
||||||
self.db.field_metadata[key]['is_custom'] and \
|
self.db.field_metadata[key]['is_custom'] and \
|
||||||
self.db.field_metadata[key]['datatype'] != 'rating'):
|
self.db.field_metadata[key]['datatype'] != 'rating'):
|
||||||
self.context_menu.addAction(_('Rename') + " '" + tag_name + "'",
|
self.context_menu.addAction(_('Rename \'%s\'')%tag_name,
|
||||||
partial(self.context_menu_handler, action='edit_item',
|
partial(self.context_menu_handler, action='edit_item',
|
||||||
category=tag_item, index=index))
|
category=tag_item, index=index))
|
||||||
|
if key == 'authors':
|
||||||
|
self.context_menu.addAction(_('Edit sort for \'%s\'')%tag_name,
|
||||||
|
partial(self.context_menu_handler,
|
||||||
|
action='edit_author_sort', index=tag_id))
|
||||||
self.context_menu.addSeparator()
|
self.context_menu.addSeparator()
|
||||||
# Hide/Show/Restore categories
|
# Hide/Show/Restore categories
|
||||||
self.context_menu.addAction(_('Hide category %s') % category,
|
self.context_menu.addAction(_('Hide category %s') % category,
|
||||||
@ -166,9 +210,12 @@ class TagsView(QTreeView): # {{{
|
|||||||
self.context_menu.addSeparator()
|
self.context_menu.addSeparator()
|
||||||
if key in ['tags', 'publisher', 'series'] or \
|
if key in ['tags', 'publisher', 'series'] or \
|
||||||
self.db.field_metadata[key]['is_custom']:
|
self.db.field_metadata[key]['is_custom']:
|
||||||
self.context_menu.addAction(_('Manage ') + category,
|
self.context_menu.addAction(_('Manage %s')%category,
|
||||||
partial(self.context_menu_handler, action='open_editor',
|
partial(self.context_menu_handler, action='open_editor',
|
||||||
category=tag_name, key=key))
|
category=tag_name, key=key))
|
||||||
|
elif key == 'authors':
|
||||||
|
self.context_menu.addAction(_('Manage %s')%category,
|
||||||
|
partial(self.context_menu_handler, action='edit_author_sort'))
|
||||||
elif key == 'search':
|
elif key == 'search':
|
||||||
self.context_menu.addAction(_('Manage Saved Searches'),
|
self.context_menu.addAction(_('Manage Saved Searches'),
|
||||||
partial(self.context_menu_handler, action='manage_searches',
|
partial(self.context_menu_handler, action='manage_searches',
|
||||||
@ -332,6 +379,7 @@ class TagsModel(QAbstractItemModel): # {{{
|
|||||||
':custom' : QIcon(I('column.svg')),
|
':custom' : QIcon(I('column.svg')),
|
||||||
':user' : QIcon(I('drawer.svg')),
|
':user' : QIcon(I('drawer.svg')),
|
||||||
'search' : QIcon(I('search.svg'))})
|
'search' : QIcon(I('search.svg'))})
|
||||||
|
self.categories_with_ratings = ['authors', 'series', 'publisher', 'tags']
|
||||||
|
|
||||||
self.icon_state_map = [None, QIcon(I('plus.svg')), QIcon(I('minus.svg'))]
|
self.icon_state_map = [None, QIcon(I('plus.svg')), QIcon(I('minus.svg'))]
|
||||||
self.db = db
|
self.db = db
|
||||||
@ -341,7 +389,7 @@ class TagsModel(QAbstractItemModel): # {{{
|
|||||||
self.row_map = []
|
self.row_map = []
|
||||||
|
|
||||||
# get_node_tree cannot return None here, because row_map is empty
|
# 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()
|
self.root_item = TagTreeItem()
|
||||||
for i, r in enumerate(self.row_map):
|
for i, r in enumerate(self.row_map):
|
||||||
if self.hidden_categories and self.categories[i] in self.hidden_categories:
|
if self.hidden_categories and self.categories[i] in self.hidden_categories:
|
||||||
@ -354,7 +402,14 @@ class TagsModel(QAbstractItemModel): # {{{
|
|||||||
data=self.categories[i],
|
data=self.categories[i],
|
||||||
category_icon=self.category_icon_map[r],
|
category_icon=self.category_icon_map[r],
|
||||||
tooltip=tt, category_key=r)
|
tooltip=tt, category_key=r)
|
||||||
|
# This duplicates code in refresh(). Having it here as well
|
||||||
|
# can save seconds during startup, because we avoid a second
|
||||||
|
# call to get_node_tree.
|
||||||
for tag in data[r]:
|
for tag in data[r]:
|
||||||
|
if r not in self.categories_with_ratings and \
|
||||||
|
not self.db.field_metadata[r]['is_custom'] and \
|
||||||
|
not self.db.field_metadata[r]['kind'] == 'user':
|
||||||
|
tag.avg_rating = None
|
||||||
TagTreeItem(parent=c, data=tag, icon_map=self.icon_state_map)
|
TagTreeItem(parent=c, data=tag, icon_map=self.icon_state_map)
|
||||||
|
|
||||||
def set_search_restriction(self, s):
|
def set_search_restriction(self, s):
|
||||||
@ -378,11 +433,11 @@ class TagsModel(QAbstractItemModel): # {{{
|
|||||||
|
|
||||||
# Now get the categories
|
# Now get the categories
|
||||||
if self.search_restriction:
|
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,
|
icon_map=self.category_icon_map,
|
||||||
ids=self.db.search('', return_matches=True))
|
ids=self.db.search('', return_matches=True))
|
||||||
else:
|
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
|
tb_categories = self.db.field_metadata
|
||||||
for category in tb_categories:
|
for category in tb_categories:
|
||||||
@ -396,7 +451,7 @@ class TagsModel(QAbstractItemModel): # {{{
|
|||||||
return data
|
return data
|
||||||
|
|
||||||
def refresh(self):
|
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:
|
if data is None:
|
||||||
return False
|
return False
|
||||||
row_index = -1
|
row_index = -1
|
||||||
@ -417,6 +472,10 @@ class TagsModel(QAbstractItemModel): # {{{
|
|||||||
if len(data[r]) > 0:
|
if len(data[r]) > 0:
|
||||||
self.beginInsertRows(category_index, 0, len(data[r])-1)
|
self.beginInsertRows(category_index, 0, len(data[r])-1)
|
||||||
for tag in data[r]:
|
for tag in data[r]:
|
||||||
|
if r not in self.categories_with_ratings and \
|
||||||
|
not self.db.field_metadata[r]['is_custom'] and \
|
||||||
|
not self.db.field_metadata[r]['kind'] == 'user':
|
||||||
|
tag.avg_rating = None
|
||||||
tag.state = state_map.get(tag.name, 0)
|
tag.state = state_map.get(tag.name, 0)
|
||||||
t = TagTreeItem(parent=category, data=tag, icon_map=self.icon_state_map)
|
t = TagTreeItem(parent=category, data=tag, icon_map=self.icon_state_map)
|
||||||
self.endInsertRows()
|
self.endInsertRows()
|
||||||
@ -601,12 +660,13 @@ class TagBrowserMixin(object): # {{{
|
|||||||
def __init__(self, db):
|
def __init__(self, db):
|
||||||
self.library_view.model().count_changed_signal.connect(self.tags_view.recount)
|
self.library_view.model().count_changed_signal.connect(self.tags_view.recount)
|
||||||
self.tags_view.set_database(self.library_view.model().db,
|
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.search.search_from_tags)
|
||||||
self.tags_view.tags_marked.connect(self.saved_search.clear_to_help)
|
self.tags_view.tags_marked.connect(self.saved_search.clear_to_help)
|
||||||
self.tags_view.tag_list_edit.connect(self.do_tags_list_edit)
|
self.tags_view.tag_list_edit.connect(self.do_tags_list_edit)
|
||||||
self.tags_view.user_category_edit.connect(self.do_user_categories_edit)
|
self.tags_view.user_category_edit.connect(self.do_user_categories_edit)
|
||||||
self.tags_view.saved_search_edit.connect(self.do_saved_search_edit)
|
self.tags_view.saved_search_edit.connect(self.do_saved_search_edit)
|
||||||
|
self.tags_view.author_sort_edit.connect(self.do_author_sort_edit)
|
||||||
self.tags_view.tag_item_renamed.connect(self.do_tag_item_renamed)
|
self.tags_view.tag_item_renamed.connect(self.do_tag_item_renamed)
|
||||||
self.tags_view.search_item_renamed.connect(self.saved_search.clear_to_help)
|
self.tags_view.search_item_renamed.connect(self.saved_search.clear_to_help)
|
||||||
self.edit_categories.clicked.connect(lambda x:
|
self.edit_categories.clicked.connect(lambda x:
|
||||||
@ -636,6 +696,19 @@ class TagBrowserMixin(object): # {{{
|
|||||||
self.saved_search.clear_to_help()
|
self.saved_search.clear_to_help()
|
||||||
self.search.clear_to_help()
|
self.search.clear_to_help()
|
||||||
|
|
||||||
|
def do_author_sort_edit(self, parent, id):
|
||||||
|
db = self.library_view.model().db
|
||||||
|
editor = EditAuthorsDialog(parent, db, id)
|
||||||
|
d = editor.exec_()
|
||||||
|
if d:
|
||||||
|
for (id, old_author, new_author, new_sort) in editor.result:
|
||||||
|
if old_author != new_author:
|
||||||
|
# The id might change if the new author already exists
|
||||||
|
id = db.rename_author(id, new_author)
|
||||||
|
db.set_sort_field_for_author(id, unicode(new_sort))
|
||||||
|
self.library_view.model().refresh()
|
||||||
|
self.tags_view.recount()
|
||||||
|
|
||||||
# }}}
|
# }}}
|
||||||
|
|
||||||
class TagBrowserWidget(QWidget): # {{{
|
class TagBrowserWidget(QWidget): # {{{
|
||||||
@ -648,9 +721,13 @@ class TagBrowserWidget(QWidget): # {{{
|
|||||||
parent.tags_view = TagsView(parent)
|
parent.tags_view = TagsView(parent)
|
||||||
self._layout.addWidget(parent.tags_view)
|
self._layout.addWidget(parent.tags_view)
|
||||||
|
|
||||||
parent.popularity = QCheckBox(parent)
|
parent.sort_by = QComboBox(parent)
|
||||||
parent.popularity.setText(_('Sort by &popularity'))
|
# Must be in the same order as db2.CATEGORY_SORTS
|
||||||
self._layout.addWidget(parent.popularity)
|
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)
|
parent.tag_match = QComboBox(parent)
|
||||||
for x in (_('Match any'), _('Match all')):
|
for x in (_('Match any'), _('Match all')):
|
||||||
|
@ -3,12 +3,13 @@ __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
|
|||||||
|
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
from PyQt4.QtCore import QThread, pyqtSignal
|
from PyQt4.Qt import QThread, pyqtSignal, QDesktopServices, QUrl, Qt
|
||||||
import mechanize
|
import mechanize
|
||||||
|
|
||||||
from calibre.constants import __version__, iswindows, isosx
|
from calibre.constants import __appname__, __version__, iswindows, isosx
|
||||||
from calibre import browser
|
from calibre import browser
|
||||||
from calibre.utils.config import prefs
|
from calibre.utils.config import prefs
|
||||||
|
from calibre.gui2 import config, dynamic, question_dialog
|
||||||
|
|
||||||
URL = 'http://status.calibre-ebook.com/latest'
|
URL = 'http://status.calibre-ebook.com/latest'
|
||||||
|
|
||||||
@ -36,3 +37,35 @@ class CheckForUpdates(QThread):
|
|||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
self.sleep(self.INTERVAL)
|
self.sleep(self.INTERVAL)
|
||||||
|
|
||||||
|
class UpdateMixin(object):
|
||||||
|
|
||||||
|
def __init__(self, opts):
|
||||||
|
if not opts.no_update_check:
|
||||||
|
self.update_checker = CheckForUpdates(self)
|
||||||
|
self.update_checker.update_found.connect(self.update_found,
|
||||||
|
type=Qt.QueuedConnection)
|
||||||
|
self.update_checker.start()
|
||||||
|
|
||||||
|
def update_found(self, version):
|
||||||
|
os = 'windows' if iswindows else 'osx' if isosx else 'linux'
|
||||||
|
url = 'http://calibre-ebook.com/download_%s'%os
|
||||||
|
self.latest_version = '<br>' + _('<span style="color:red; font-weight:bold">'
|
||||||
|
'Latest version: <a href="%s">%s</a></span>')%(url, version)
|
||||||
|
self.vanity.setText(self.vanity_template%\
|
||||||
|
(dict(version=self.latest_version,
|
||||||
|
device=self.device_info)))
|
||||||
|
self.vanity.update()
|
||||||
|
if config.get('new_version_notification') and \
|
||||||
|
dynamic.get('update to version %s'%version, True):
|
||||||
|
if question_dialog(self, _('Update available'),
|
||||||
|
_('%s has been updated to version %s. '
|
||||||
|
'See the <a href="http://calibre-ebook.com/whats-new'
|
||||||
|
'">new features</a>. Visit the download pa'
|
||||||
|
'ge?')%(__appname__, version)):
|
||||||
|
url = 'http://calibre-ebook.com/download_'+\
|
||||||
|
('windows' if iswindows else 'osx' if isosx else 'linux')
|
||||||
|
QDesktopServices.openUrl(QUrl(url))
|
||||||
|
dynamic.set('update to version %s'%version, False)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -1110,6 +1110,8 @@ class Splitter(QSplitter):
|
|||||||
def show_side_pane(self):
|
def show_side_pane(self):
|
||||||
if self.count() < 2 or not self.is_side_index_hidden:
|
if self.count() < 2 or not self.is_side_index_hidden:
|
||||||
return
|
return
|
||||||
|
if self.desired_side_size == 0:
|
||||||
|
self.desired_side_size = self.initial_side_size
|
||||||
self.apply_state((True, self.desired_side_size))
|
self.apply_state((True, self.desired_side_size))
|
||||||
|
|
||||||
def hide_side_pane(self):
|
def hide_side_pane(self):
|
||||||
|
@ -10,14 +10,14 @@ import collections, glob, os, re, itertools, functools
|
|||||||
from itertools import repeat
|
from itertools import repeat
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
|
||||||
from PyQt4.QtCore import QThread, QReadWriteLock
|
from PyQt4.Qt import QThread, QReadWriteLock, QImage, Qt
|
||||||
from PyQt4.QtGui import QImage
|
|
||||||
|
|
||||||
from calibre.utils.config import tweaks
|
from calibre.utils.config import tweaks
|
||||||
from calibre.utils.date import parse_date, now, UNDEFINED_DATE
|
from calibre.utils.date import parse_date, now, UNDEFINED_DATE
|
||||||
from calibre.utils.search_query_parser import SearchQueryParser
|
from calibre.utils.search_query_parser import SearchQueryParser
|
||||||
from calibre.utils.pyparsing import ParseException
|
from calibre.utils.pyparsing import ParseException
|
||||||
from calibre.ebooks.metadata import title_sort
|
from calibre.ebooks.metadata import title_sort
|
||||||
|
from calibre import fit_image
|
||||||
|
|
||||||
class CoverCache(QThread):
|
class CoverCache(QThread):
|
||||||
|
|
||||||
@ -96,6 +96,11 @@ class CoverCache(QThread):
|
|||||||
img.loadFromData(data)
|
img.loadFromData(data)
|
||||||
if img.isNull():
|
if img.isNull():
|
||||||
continue
|
continue
|
||||||
|
scaled, nwidth, nheight = fit_image(img.width(),
|
||||||
|
img.height(), 600, 800)
|
||||||
|
if scaled:
|
||||||
|
img = img.scaled(nwidth, nheight, Qt.KeepAspectRatio,
|
||||||
|
Qt.SmoothTransformation)
|
||||||
except:
|
except:
|
||||||
continue
|
continue
|
||||||
self.cache_lock.lockForWrite()
|
self.cache_lock.lockForWrite()
|
||||||
|
@ -461,14 +461,29 @@ class CustomColumns(object):
|
|||||||
CREATE VIEW tag_browser_{table} AS SELECT
|
CREATE VIEW tag_browser_{table} AS SELECT
|
||||||
id,
|
id,
|
||||||
value,
|
value,
|
||||||
(SELECT COUNT(id) FROM {lt} WHERE value={table}.id) count
|
(SELECT COUNT(id) FROM {lt} WHERE value={table}.id) count,
|
||||||
|
(SELECT AVG(r.rating)
|
||||||
|
FROM {lt},
|
||||||
|
books_ratings_link as bl,
|
||||||
|
ratings as r
|
||||||
|
WHERE {lt}.value={table}.id and bl.book={lt}.book and
|
||||||
|
r.id = bl.rating and r.rating <> 0) avg_rating,
|
||||||
|
value AS sort
|
||||||
FROM {table};
|
FROM {table};
|
||||||
|
|
||||||
CREATE VIEW tag_browser_filtered_{table} AS SELECT
|
CREATE VIEW tag_browser_filtered_{table} AS SELECT
|
||||||
id,
|
id,
|
||||||
value,
|
value,
|
||||||
(SELECT COUNT({lt}.id) FROM {lt} WHERE value={table}.id AND
|
(SELECT COUNT({lt}.id) FROM {lt} WHERE value={table}.id AND
|
||||||
books_list_filter(book)) count
|
books_list_filter(book)) count,
|
||||||
|
(SELECT AVG(r.rating)
|
||||||
|
FROM {lt},
|
||||||
|
books_ratings_link as bl,
|
||||||
|
ratings as r
|
||||||
|
WHERE {lt}.value={table}.id AND bl.book={lt}.book AND
|
||||||
|
r.id = bl.rating AND r.rating <> 0 AND
|
||||||
|
books_list_filter(bl.book)) avg_rating,
|
||||||
|
value AS sort
|
||||||
FROM {table};
|
FROM {table};
|
||||||
|
|
||||||
'''.format(lt=lt, table=table),
|
'''.format(lt=lt, table=table),
|
||||||
@ -505,7 +520,6 @@ class CustomColumns(object):
|
|||||||
END;
|
END;
|
||||||
'''.format(table=table),
|
'''.format(table=table),
|
||||||
]
|
]
|
||||||
|
|
||||||
script = ' \n'.join(lines)
|
script = ' \n'.join(lines)
|
||||||
self.conn.executescript(script)
|
self.conn.executescript(script)
|
||||||
self.conn.commit()
|
self.conn.commit()
|
||||||
|
@ -12,7 +12,7 @@ from math import floor
|
|||||||
|
|
||||||
from PyQt4.QtGui import QImage
|
from PyQt4.QtGui import QImage
|
||||||
|
|
||||||
from calibre.ebooks.metadata import title_sort
|
from calibre.ebooks.metadata import title_sort, author_to_author_sort
|
||||||
from calibre.library.database import LibraryDatabase
|
from calibre.library.database import LibraryDatabase
|
||||||
from calibre.library.field_metadata import FieldMetadata, TagsIcons
|
from calibre.library.field_metadata import FieldMetadata, TagsIcons
|
||||||
from calibre.library.schema_upgrades import SchemaUpgrade
|
from calibre.library.schema_upgrades import SchemaUpgrade
|
||||||
@ -20,7 +20,7 @@ from calibre.library.caches import ResultCache
|
|||||||
from calibre.library.custom_columns import CustomColumns
|
from calibre.library.custom_columns import CustomColumns
|
||||||
from calibre.library.sqlite import connect, IntegrityError, DBThread
|
from calibre.library.sqlite import connect, IntegrityError, DBThread
|
||||||
from calibre.ebooks.metadata import string_to_authors, authors_to_string, \
|
from calibre.ebooks.metadata import string_to_authors, authors_to_string, \
|
||||||
MetaInformation, authors_to_sort_string
|
MetaInformation
|
||||||
from calibre.ebooks.metadata.meta import get_metadata, metadata_from_formats
|
from calibre.ebooks.metadata.meta import get_metadata, metadata_from_formats
|
||||||
from calibre.constants import preferred_encoding, iswindows, isosx, filesystem_encoding
|
from calibre.constants import preferred_encoding, iswindows, isosx, filesystem_encoding
|
||||||
from calibre.ptempfile import PersistentTemporaryFile
|
from calibre.ptempfile import PersistentTemporaryFile
|
||||||
@ -56,11 +56,18 @@ copyfile = os.link if hasattr(os, 'link') else shutil.copyfile
|
|||||||
|
|
||||||
class Tag(object):
|
class Tag(object):
|
||||||
|
|
||||||
def __init__(self, name, id=None, count=0, state=0, tooltip=None, icon=None):
|
def __init__(self, name, id=None, count=0, state=0, avg=0, sort=None,
|
||||||
|
tooltip=None, icon=None):
|
||||||
self.name = name
|
self.name = name
|
||||||
self.id = id
|
self.id = id
|
||||||
self.count = count
|
self.count = count
|
||||||
self.state = state
|
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.tooltip = tooltip
|
||||||
self.icon = icon
|
self.icon = icon
|
||||||
|
|
||||||
@ -129,11 +136,30 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
|||||||
self.initialize_dynamic()
|
self.initialize_dynamic()
|
||||||
|
|
||||||
def initialize_dynamic(self):
|
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'''
|
self.conn.executescript(u'''
|
||||||
CREATE TEMP VIEW IF NOT EXISTS tag_browser_news AS SELECT DISTINCT
|
CREATE TEMP VIEW IF NOT EXISTS tag_browser_news AS SELECT DISTINCT
|
||||||
id,
|
id,
|
||||||
name,
|
name,
|
||||||
(SELECT COUNT(books_tags_link.id) FROM books_tags_link WHERE tag=x.id) count
|
(SELECT COUNT(books_tags_link.id) FROM books_tags_link WHERE tag=x.id) count,
|
||||||
|
(0) as avg_rating,
|
||||||
|
name as sort
|
||||||
FROM tags as x WHERE name!="{0}" AND id IN
|
FROM tags as x WHERE name!="{0}" AND id IN
|
||||||
(SELECT DISTINCT tag FROM books_tags_link WHERE book IN
|
(SELECT DISTINCT tag FROM books_tags_link WHERE book IN
|
||||||
(SELECT DISTINCT book FROM books_tags_link WHERE tag IN
|
(SELECT DISTINCT book FROM books_tags_link WHERE tag IN
|
||||||
@ -144,7 +170,9 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
|||||||
CREATE TEMP VIEW IF NOT EXISTS tag_browser_filtered_news AS SELECT DISTINCT
|
CREATE TEMP VIEW IF NOT EXISTS tag_browser_filtered_news AS SELECT DISTINCT
|
||||||
id,
|
id,
|
||||||
name,
|
name,
|
||||||
(SELECT COUNT(books_tags_link.id) FROM books_tags_link WHERE tag=x.id and books_list_filter(book)) count
|
(SELECT COUNT(books_tags_link.id) FROM books_tags_link WHERE tag=x.id and books_list_filter(book)) count,
|
||||||
|
(0) as avg_rating,
|
||||||
|
name as sort
|
||||||
FROM tags as x WHERE name!="{0}" AND id IN
|
FROM tags as x WHERE name!="{0}" AND id IN
|
||||||
(SELECT DISTINCT tag FROM books_tags_link WHERE book IN
|
(SELECT DISTINCT tag FROM books_tags_link WHERE book IN
|
||||||
(SELECT DISTINCT book FROM books_tags_link WHERE tag IN
|
(SELECT DISTINCT book FROM books_tags_link WHERE tag IN
|
||||||
@ -422,6 +450,11 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
|||||||
if aum: aum = [a.strip().replace('|', ',') for a in aum.split(',')]
|
if aum: aum = [a.strip().replace('|', ',') for a in aum.split(',')]
|
||||||
mi = MetaInformation(self.title(idx, index_is_id=index_is_id), aum)
|
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.author_sort = self.author_sort(idx, index_is_id=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.comments = self.comments(idx, index_is_id=index_is_id)
|
||||||
mi.publisher = self.publisher(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)
|
mi.timestamp = self.timestamp(idx, index_is_id=index_is_id)
|
||||||
@ -679,7 +712,9 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
|||||||
tn=field['table'], col=field['link_column']), (id_,))
|
tn=field['table'], col=field['link_column']), (id_,))
|
||||||
return set(x[0] for x in ans)
|
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)
|
self.books_list_filter.change([] if not ids else ids)
|
||||||
|
|
||||||
categories = {}
|
categories = {}
|
||||||
@ -698,13 +733,17 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
|||||||
continue
|
continue
|
||||||
cn = cat['column']
|
cn = cat['column']
|
||||||
if ids is None:
|
if ids is None:
|
||||||
query = 'SELECT id, {0}, count FROM tag_browser_{1}'.format(cn, tn)
|
query = '''SELECT id, {0}, count, avg_rating, sort
|
||||||
|
FROM tag_browser_{1}'''.format(cn, tn)
|
||||||
else:
|
else:
|
||||||
query = 'SELECT id, {0}, count FROM tag_browser_filtered_{1}'.format(cn, tn)
|
query = '''SELECT id, {0}, count, avg_rating, sort
|
||||||
if sort_on_count:
|
FROM tag_browser_filtered_{1}'''.format(cn, tn)
|
||||||
query += ' ORDER BY count DESC'
|
if sort == 'popularity':
|
||||||
|
query += ' ORDER BY count DESC, sort ASC'
|
||||||
|
elif sort == 'name':
|
||||||
|
query += ' ORDER BY sort ASC'
|
||||||
else:
|
else:
|
||||||
query += ' ORDER BY {0} ASC'.format(cn)
|
query += ' ORDER BY avg_rating DESC, sort ASC'
|
||||||
data = self.conn.get(query)
|
data = self.conn.get(query)
|
||||||
|
|
||||||
# icon_map is not None if get_categories is to store an icon and
|
# icon_map is not None if get_categories is to store an icon and
|
||||||
@ -722,6 +761,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
|||||||
|
|
||||||
datatype = cat['datatype']
|
datatype = cat['datatype']
|
||||||
if datatype == 'rating':
|
if datatype == 'rating':
|
||||||
|
# eliminate the zero ratings line as well as count == 0
|
||||||
item_not_zero_func = (lambda x: x[1] > 0 and x[2] > 0)
|
item_not_zero_func = (lambda x: x[1] > 0 and x[2] > 0)
|
||||||
formatter = (lambda x:u'\u2605'*int(round(x/2.)))
|
formatter = (lambda x:u'\u2605'*int(round(x/2.)))
|
||||||
elif category == 'authors':
|
elif category == 'authors':
|
||||||
@ -733,15 +773,9 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
|||||||
formatter = (lambda x:unicode(x))
|
formatter = (lambda x:unicode(x))
|
||||||
|
|
||||||
categories[category] = [Tag(formatter(r[1]), count=r[2], id=r[0],
|
categories[category] = [Tag(formatter(r[1]), count=r[2], id=r[0],
|
||||||
icon=icon, tooltip = tooltip)
|
avg=r[3], sort=r[4],
|
||||||
|
icon=icon, tooltip=tooltip)
|
||||||
for r in data if item_not_zero_func(r)]
|
for r in data if item_not_zero_func(r)]
|
||||||
if category == 'series' and not sort_on_count:
|
|
||||||
if tweaks['title_series_sorting'] == 'library_order':
|
|
||||||
ts = lambda x: title_sort(x)
|
|
||||||
else:
|
|
||||||
ts = lambda x:x
|
|
||||||
categories[category].sort(cmp=lambda x,y:cmp(ts(x.name).lower(),
|
|
||||||
ts(y.name).lower()))
|
|
||||||
|
|
||||||
# We delayed computing the standard formats category because it does not
|
# We delayed computing the standard formats category because it does not
|
||||||
# use a view, but is computed dynamically
|
# use a view, but is computed dynamically
|
||||||
@ -765,11 +799,10 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
|||||||
if count > 0:
|
if count > 0:
|
||||||
categories['formats'].append(Tag(fmt, count=count, icon=icon))
|
categories['formats'].append(Tag(fmt, count=count, icon=icon))
|
||||||
|
|
||||||
if sort_on_count:
|
if sort == 'popularity':
|
||||||
categories['formats'].sort(cmp=lambda x,y:cmp(x.count, y.count),
|
categories['formats'].sort(key=lambda x: x.count, reverse=True)
|
||||||
reverse=True)
|
else: # no ratings exist to sort on
|
||||||
else:
|
categories['formats'].sort(key = lambda x:x.name)
|
||||||
categories['formats'].sort(cmp=lambda x,y:cmp(x.name, y.name))
|
|
||||||
|
|
||||||
#### Now do the user-defined categories. ####
|
#### Now do the user-defined categories. ####
|
||||||
user_categories = prefs['user_categories']
|
user_categories = prefs['user_categories']
|
||||||
@ -794,12 +827,15 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
|||||||
# Not a problem if we accumulate entries in the icon map
|
# Not a problem if we accumulate entries in the icon map
|
||||||
if icon_map is not None:
|
if icon_map is not None:
|
||||||
icon_map[cat_name] = icon_map[':user']
|
icon_map[cat_name] = icon_map[':user']
|
||||||
if sort_on_count:
|
if sort == 'popularity':
|
||||||
categories[cat_name] = \
|
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:
|
else:
|
||||||
categories[cat_name] = \
|
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 ####
|
#### Finally, the saved searches category ####
|
||||||
items = []
|
items = []
|
||||||
@ -909,6 +945,38 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
|||||||
self.set_path(id, True)
|
self.set_path(id, True)
|
||||||
self.notify('metadata', [id])
|
self.notify('metadata', [id])
|
||||||
|
|
||||||
|
# Given a book, return the list of author sort strings for the book's authors
|
||||||
|
def authors_sort_strings(self, id, index_is_id=False):
|
||||||
|
id = id if index_is_id else self.id(id)
|
||||||
|
aut_strings = self.conn.get('''
|
||||||
|
SELECT sort
|
||||||
|
FROM authors, books_authors_link as bl
|
||||||
|
WHERE bl.book=? and authors.id=bl.author
|
||||||
|
ORDER BY bl.id''', (id,))
|
||||||
|
result = []
|
||||||
|
for (sort,) in aut_strings:
|
||||||
|
result.append(sort)
|
||||||
|
return result
|
||||||
|
|
||||||
|
# Given a book, return the author_sort string for authors of the book
|
||||||
|
def author_sort_from_book(self, id, index_is_id=False):
|
||||||
|
auts = self.authors_sort_strings(id, index_is_id)
|
||||||
|
return ' & '.join(auts).replace('|', ',')
|
||||||
|
|
||||||
|
# Given a list of authors, return the author_sort string for the authors,
|
||||||
|
# preferring the author sort associated with the author over the computed
|
||||||
|
# string
|
||||||
|
def author_sort_from_authors(self, authors):
|
||||||
|
result = []
|
||||||
|
for aut in authors:
|
||||||
|
r = self.conn.get('SELECT sort FROM authors WHERE name=?',
|
||||||
|
(aut.replace(',', '|'),), all=False)
|
||||||
|
if r is None:
|
||||||
|
result.append(author_to_author_sort(aut))
|
||||||
|
else:
|
||||||
|
result.append(r)
|
||||||
|
return ' & '.join(result).replace('|', ',')
|
||||||
|
|
||||||
def set_authors(self, id, authors, notify=True):
|
def set_authors(self, id, authors, notify=True):
|
||||||
'''
|
'''
|
||||||
`authors`: A list of authors.
|
`authors`: A list of authors.
|
||||||
@ -935,7 +1003,8 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
|||||||
(id, aid))
|
(id, aid))
|
||||||
except IntegrityError: # Sometimes books specify the same author twice in their metadata
|
except IntegrityError: # Sometimes books specify the same author twice in their metadata
|
||||||
pass
|
pass
|
||||||
ss = authors_to_sort_string(authors)
|
self.conn.commit()
|
||||||
|
ss = self.author_sort_from_book(id, index_is_id=True)
|
||||||
self.conn.execute('UPDATE books SET author_sort=? WHERE id=?',
|
self.conn.execute('UPDATE books SET author_sort=? WHERE id=?',
|
||||||
(ss, id))
|
(ss, id))
|
||||||
self.conn.commit()
|
self.conn.commit()
|
||||||
@ -1007,6 +1076,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
def rename_tag(self, old_id, new_name):
|
def rename_tag(self, old_id, new_name):
|
||||||
|
new_name = new_name.strip()
|
||||||
new_id = self.conn.get(
|
new_id = self.conn.get(
|
||||||
'''SELECT id from tags
|
'''SELECT id from tags
|
||||||
WHERE name=?''', (new_name,), all=False)
|
WHERE name=?''', (new_name,), all=False)
|
||||||
@ -1046,6 +1116,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
def rename_series(self, old_id, new_name):
|
def rename_series(self, old_id, new_name):
|
||||||
|
new_name = new_name.strip()
|
||||||
new_id = self.conn.get(
|
new_id = self.conn.get(
|
||||||
'''SELECT id from series
|
'''SELECT id from series
|
||||||
WHERE name=?''', (new_name,), all=False)
|
WHERE name=?''', (new_name,), all=False)
|
||||||
@ -1075,7 +1146,6 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
|||||||
index = index + 1
|
index = index + 1
|
||||||
self.conn.commit()
|
self.conn.commit()
|
||||||
|
|
||||||
|
|
||||||
def delete_series_using_id(self, id):
|
def delete_series_using_id(self, id):
|
||||||
books = self.conn.get('SELECT book from books_series_link WHERE series=?', (id,))
|
books = self.conn.get('SELECT book from books_series_link WHERE series=?', (id,))
|
||||||
self.conn.execute('DELETE FROM books_series_link WHERE series=?', (id,))
|
self.conn.execute('DELETE FROM books_series_link WHERE series=?', (id,))
|
||||||
@ -1091,6 +1161,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
def rename_publisher(self, old_id, new_name):
|
def rename_publisher(self, old_id, new_name):
|
||||||
|
new_name = new_name.strip()
|
||||||
new_id = self.conn.get(
|
new_id = self.conn.get(
|
||||||
'''SELECT id from publishers
|
'''SELECT id from publishers
|
||||||
WHERE name=?''', (new_name,), all=False)
|
WHERE name=?''', (new_name,), all=False)
|
||||||
@ -1113,12 +1184,25 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
|||||||
self.conn.execute('DELETE FROM publishers WHERE id=?', (old_id,))
|
self.conn.execute('DELETE FROM publishers WHERE id=?', (old_id,))
|
||||||
self.conn.commit()
|
self.conn.commit()
|
||||||
|
|
||||||
# There is no editor for author, so we do not need get_authors_with_ids or
|
def get_authors_with_ids(self):
|
||||||
# delete_author_using_id.
|
result = self.conn.get('SELECT id,name,sort FROM authors')
|
||||||
|
if not result:
|
||||||
|
return []
|
||||||
|
return result
|
||||||
|
|
||||||
|
def set_sort_field_for_author(self, old_id, new_sort):
|
||||||
|
self.conn.execute('UPDATE authors SET sort=? WHERE id=?', \
|
||||||
|
(new_sort.strip(), old_id))
|
||||||
|
self.conn.commit()
|
||||||
|
# Now change all the author_sort fields in books by this author
|
||||||
|
bks = self.conn.get('SELECT book from books_authors_link WHERE author=?', (old_id,))
|
||||||
|
for (book_id,) in bks:
|
||||||
|
ss = self.author_sort_from_book(book_id, index_is_id=True)
|
||||||
|
self.set_author_sort(book_id, ss)
|
||||||
|
|
||||||
def rename_author(self, old_id, new_name):
|
def rename_author(self, old_id, new_name):
|
||||||
# Make sure that any commas in new_name are changed to '|'!
|
# Make sure that any commas in new_name are changed to '|'!
|
||||||
new_name = new_name.replace(',', '|')
|
new_name = new_name.replace(',', '|').strip()
|
||||||
|
|
||||||
# Get the list of books we must fix up, one way or the other
|
# Get the list of books we must fix up, one way or the other
|
||||||
# Save the list so we can use it twice
|
# Save the list so we can use it twice
|
||||||
@ -1141,7 +1225,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
|||||||
self.conn.execute('UPDATE authors SET name=? WHERE id=?',
|
self.conn.execute('UPDATE authors SET name=? WHERE id=?',
|
||||||
(new_name, old_id))
|
(new_name, old_id))
|
||||||
self.conn.commit()
|
self.conn.commit()
|
||||||
return
|
return new_id
|
||||||
# Author exists. To fix this, we must replace all the authors
|
# Author exists. To fix this, we must replace all the authors
|
||||||
# instead of replacing the one. Reason: db integrity checks can stop
|
# instead of replacing the one. Reason: db integrity checks can stop
|
||||||
# the rename process, which would leave everything half-done. We
|
# the rename process, which would leave everything half-done. We
|
||||||
@ -1184,24 +1268,11 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
|||||||
# now fix the filesystem paths
|
# now fix the filesystem paths
|
||||||
self.set_path(book_id, index_is_id=True)
|
self.set_path(book_id, index_is_id=True)
|
||||||
# Next fix the author sort. Reset it to the default
|
# Next fix the author sort. Reset it to the default
|
||||||
authors = self.conn.get('''
|
ss = self.author_sort_from_book(book_id, index_is_id=True)
|
||||||
SELECT authors.name
|
self.set_author_sort(book_id, ss)
|
||||||
FROM authors, books_authors_link as bl
|
|
||||||
WHERE bl.book = ? and bl.author = authors.id
|
|
||||||
ORDER BY bl.id
|
|
||||||
''' , (book_id,))
|
|
||||||
# unpack the double-list structure
|
|
||||||
for i,aut in enumerate(authors):
|
|
||||||
authors[i] = aut[0]
|
|
||||||
ss = authors_to_sort_string(authors)
|
|
||||||
# Change the '|'s to ','
|
|
||||||
ss = ss.replace('|', ',')
|
|
||||||
self.conn.execute('''UPDATE books
|
|
||||||
SET author_sort=?
|
|
||||||
WHERE id=?''', (ss, book_id))
|
|
||||||
self.conn.commit()
|
|
||||||
# the caller will do a general refresh, so we don't need to
|
# the caller will do a general refresh, so we don't need to
|
||||||
# do one here
|
# do one here
|
||||||
|
return new_id
|
||||||
|
|
||||||
# end convenience methods
|
# end convenience methods
|
||||||
|
|
||||||
@ -1436,7 +1507,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
|||||||
if not add_duplicates and self.has_book(mi):
|
if not add_duplicates and self.has_book(mi):
|
||||||
return None
|
return None
|
||||||
series_index = 1.0 if mi.series_index is None else mi.series_index
|
series_index = 1.0 if mi.series_index is None else mi.series_index
|
||||||
aus = mi.author_sort if mi.author_sort else ', '.join(mi.authors)
|
aus = mi.author_sort if mi.author_sort else self.author_sort_from_authors(mi.authors)
|
||||||
title = mi.title
|
title = mi.title
|
||||||
if isinstance(aus, str):
|
if isinstance(aus, str):
|
||||||
aus = aus.decode(preferred_encoding, 'replace')
|
aus = aus.decode(preferred_encoding, 'replace')
|
||||||
@ -1476,7 +1547,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
|||||||
duplicates.append((path, format, mi))
|
duplicates.append((path, format, mi))
|
||||||
continue
|
continue
|
||||||
series_index = 1.0 if mi.series_index is None else mi.series_index
|
series_index = 1.0 if mi.series_index is None else mi.series_index
|
||||||
aus = mi.author_sort if mi.author_sort else ', '.join(mi.authors)
|
aus = mi.author_sort if mi.author_sort else self.author_sort_from_authors(mi.authors)
|
||||||
title = mi.title
|
title = mi.title
|
||||||
if isinstance(aus, str):
|
if isinstance(aus, str):
|
||||||
aus = aus.decode(preferred_encoding, 'replace')
|
aus = aus.decode(preferred_encoding, 'replace')
|
||||||
@ -1515,7 +1586,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
|
|||||||
mi.title = _('Unknown')
|
mi.title = _('Unknown')
|
||||||
if not mi.authors:
|
if not mi.authors:
|
||||||
mi.authors = [_('Unknown')]
|
mi.authors = [_('Unknown')]
|
||||||
aus = mi.author_sort if mi.author_sort else authors_to_sort_string(mi.authors)
|
aus = mi.author_sort if mi.author_sort else self.author_sort_from_authors(mi.authors)
|
||||||
if isinstance(aus, str):
|
if isinstance(aus, str):
|
||||||
aus = aus.decode(preferred_encoding, 'replace')
|
aus = aus.decode(preferred_encoding, 'replace')
|
||||||
title = mi.title if isinstance(mi.title, unicode) else \
|
title = mi.title if isinstance(mi.title, unicode) else \
|
||||||
|
@ -44,9 +44,12 @@ class FieldMetadata(dict):
|
|||||||
is_category: is a tag browser category. If true, then:
|
is_category: is a tag browser category. If true, then:
|
||||||
table: name of the db table used to construct item list
|
table: name of the db table used to construct item list
|
||||||
column: name of the column in the normalized table to join on
|
column: name of the column in the normalized table to join on
|
||||||
link_column: name of the column in the connection table to join on
|
link_column: name of the column in the connection table to join on. This
|
||||||
|
key should not be present if there is no link table
|
||||||
|
category_sort: the field in the normalized table to sort on. This
|
||||||
|
key must be present if is_category is True
|
||||||
If these are None, then the category constructor must know how
|
If these are None, then the category constructor must know how
|
||||||
to build the item list (e.g., formats).
|
to build the item list (e.g., formats, news).
|
||||||
The order below is the order that the categories will
|
The order below is the order that the categories will
|
||||||
appear in the tags pane.
|
appear in the tags pane.
|
||||||
|
|
||||||
@ -66,6 +69,7 @@ class FieldMetadata(dict):
|
|||||||
('authors', {'table':'authors',
|
('authors', {'table':'authors',
|
||||||
'column':'name',
|
'column':'name',
|
||||||
'link_column':'author',
|
'link_column':'author',
|
||||||
|
'category_sort':'sort',
|
||||||
'datatype':'text',
|
'datatype':'text',
|
||||||
'is_multiple':',',
|
'is_multiple':',',
|
||||||
'kind':'field',
|
'kind':'field',
|
||||||
@ -76,6 +80,7 @@ class FieldMetadata(dict):
|
|||||||
('series', {'table':'series',
|
('series', {'table':'series',
|
||||||
'column':'name',
|
'column':'name',
|
||||||
'link_column':'series',
|
'link_column':'series',
|
||||||
|
'category_sort':'(title_sort(name))',
|
||||||
'datatype':'text',
|
'datatype':'text',
|
||||||
'is_multiple':None,
|
'is_multiple':None,
|
||||||
'kind':'field',
|
'kind':'field',
|
||||||
@ -95,6 +100,7 @@ class FieldMetadata(dict):
|
|||||||
('publisher', {'table':'publishers',
|
('publisher', {'table':'publishers',
|
||||||
'column':'name',
|
'column':'name',
|
||||||
'link_column':'publisher',
|
'link_column':'publisher',
|
||||||
|
'category_sort':'name',
|
||||||
'datatype':'text',
|
'datatype':'text',
|
||||||
'is_multiple':None,
|
'is_multiple':None,
|
||||||
'kind':'field',
|
'kind':'field',
|
||||||
@ -105,6 +111,7 @@ class FieldMetadata(dict):
|
|||||||
('rating', {'table':'ratings',
|
('rating', {'table':'ratings',
|
||||||
'column':'rating',
|
'column':'rating',
|
||||||
'link_column':'rating',
|
'link_column':'rating',
|
||||||
|
'category_sort':'rating',
|
||||||
'datatype':'rating',
|
'datatype':'rating',
|
||||||
'is_multiple':None,
|
'is_multiple':None,
|
||||||
'kind':'field',
|
'kind':'field',
|
||||||
@ -114,6 +121,7 @@ class FieldMetadata(dict):
|
|||||||
'is_category':True}),
|
'is_category':True}),
|
||||||
('news', {'table':'news',
|
('news', {'table':'news',
|
||||||
'column':'name',
|
'column':'name',
|
||||||
|
'category_sort':'name',
|
||||||
'datatype':None,
|
'datatype':None,
|
||||||
'is_multiple':None,
|
'is_multiple':None,
|
||||||
'kind':'category',
|
'kind':'category',
|
||||||
@ -124,6 +132,7 @@ class FieldMetadata(dict):
|
|||||||
('tags', {'table':'tags',
|
('tags', {'table':'tags',
|
||||||
'column':'name',
|
'column':'name',
|
||||||
'link_column': 'tag',
|
'link_column': 'tag',
|
||||||
|
'category_sort':'name',
|
||||||
'datatype':'text',
|
'datatype':'text',
|
||||||
'is_multiple':',',
|
'is_multiple':',',
|
||||||
'kind':'field',
|
'kind':'field',
|
||||||
@ -374,7 +383,7 @@ class FieldMetadata(dict):
|
|||||||
'search_terms':[key], 'label':label,
|
'search_terms':[key], 'label':label,
|
||||||
'colnum':colnum, 'display':display,
|
'colnum':colnum, 'display':display,
|
||||||
'is_custom':True, 'is_category':is_category,
|
'is_custom':True, 'is_category':is_category,
|
||||||
'link_column':'value',
|
'link_column':'value','category_sort':'value',
|
||||||
'is_editable': is_editable,}
|
'is_editable': is_editable,}
|
||||||
self._add_search_terms_to_map(key, [key])
|
self._add_search_terms_to_map(key, [key])
|
||||||
self.custom_label_to_key_map[label] = key
|
self.custom_label_to_key_map[label] = key
|
||||||
|
@ -291,4 +291,99 @@ class SchemaUpgrade(object):
|
|||||||
|
|
||||||
for field in self.field_metadata.itervalues():
|
for field in self.field_metadata.itervalues():
|
||||||
if field['is_category'] and not field['is_custom'] and 'link_column' in field:
|
if field['is_category'] and not field['is_custom'] and 'link_column' in field:
|
||||||
|
table = self.conn.get(
|
||||||
|
'SELECT name FROM sqlite_master WHERE type="table" AND name=?',
|
||||||
|
('books_%s_link'%field['table'],), all=False)
|
||||||
|
if table is not None:
|
||||||
create_tag_browser_view(field['table'], field['link_column'], field['column'])
|
create_tag_browser_view(field['table'], field['link_column'], field['column'])
|
||||||
|
|
||||||
|
def upgrade_version_11(self):
|
||||||
|
'Add average rating to tag browser views'
|
||||||
|
def create_std_tag_browser_view(table_name, column_name,
|
||||||
|
view_column_name, sort_column_name):
|
||||||
|
script = ('''
|
||||||
|
DROP VIEW IF EXISTS tag_browser_{tn};
|
||||||
|
CREATE VIEW tag_browser_{tn} AS SELECT
|
||||||
|
id,
|
||||||
|
{vcn},
|
||||||
|
(SELECT COUNT(id) FROM books_{tn}_link WHERE {cn}={tn}.id) count,
|
||||||
|
(SELECT AVG(ratings.rating)
|
||||||
|
FROM books_{tn}_link AS tl, books_ratings_link AS bl, ratings
|
||||||
|
WHERE tl.{cn}={tn}.id AND bl.book=tl.book AND
|
||||||
|
ratings.id = bl.rating AND ratings.rating <> 0) avg_rating,
|
||||||
|
{scn} AS sort
|
||||||
|
FROM {tn};
|
||||||
|
DROP VIEW IF EXISTS tag_browser_filtered_{tn};
|
||||||
|
CREATE VIEW tag_browser_filtered_{tn} AS SELECT
|
||||||
|
id,
|
||||||
|
{vcn},
|
||||||
|
(SELECT COUNT(books_{tn}_link.id) FROM books_{tn}_link WHERE
|
||||||
|
{cn}={tn}.id AND books_list_filter(book)) count,
|
||||||
|
(SELECT AVG(ratings.rating)
|
||||||
|
FROM books_{tn}_link AS tl, books_ratings_link AS bl, ratings
|
||||||
|
WHERE tl.{cn}={tn}.id AND bl.book=tl.book AND
|
||||||
|
ratings.id = bl.rating AND ratings.rating <> 0 AND
|
||||||
|
books_list_filter(bl.book)) avg_rating,
|
||||||
|
{scn} AS sort
|
||||||
|
FROM {tn};
|
||||||
|
|
||||||
|
'''.format(tn=table_name, cn=column_name,
|
||||||
|
vcn=view_column_name, scn= sort_column_name))
|
||||||
|
self.conn.executescript(script)
|
||||||
|
|
||||||
|
def create_cust_tag_browser_view(table_name, link_table_name):
|
||||||
|
script = '''
|
||||||
|
DROP VIEW IF EXISTS tag_browser_{table};
|
||||||
|
CREATE VIEW tag_browser_{table} AS SELECT
|
||||||
|
id,
|
||||||
|
value,
|
||||||
|
(SELECT COUNT(id) FROM {lt} WHERE value={table}.id) count,
|
||||||
|
(SELECT AVG(r.rating)
|
||||||
|
FROM {lt},
|
||||||
|
books_ratings_link AS bl,
|
||||||
|
ratings AS r
|
||||||
|
WHERE {lt}.value={table}.id AND bl.book={lt}.book AND
|
||||||
|
r.id = bl.rating AND r.rating <> 0) avg_rating,
|
||||||
|
value AS sort
|
||||||
|
FROM {table};
|
||||||
|
|
||||||
|
DROP VIEW IF EXISTS tag_browser_filtered_{table};
|
||||||
|
CREATE VIEW tag_browser_filtered_{table} AS SELECT
|
||||||
|
id,
|
||||||
|
value,
|
||||||
|
(SELECT COUNT({lt}.id) FROM {lt} WHERE value={table}.id AND
|
||||||
|
books_list_filter(book)) count,
|
||||||
|
(SELECT AVG(r.rating)
|
||||||
|
FROM {lt},
|
||||||
|
books_ratings_link AS bl,
|
||||||
|
ratings AS r
|
||||||
|
WHERE {lt}.value={table}.id AND bl.book={lt}.book AND
|
||||||
|
r.id = bl.rating AND r.rating <> 0 AND
|
||||||
|
books_list_filter(bl.book)) avg_rating,
|
||||||
|
value AS sort
|
||||||
|
FROM {table};
|
||||||
|
'''.format(lt=link_table_name, table=table_name)
|
||||||
|
self.conn.executescript(script)
|
||||||
|
|
||||||
|
for field in self.field_metadata.itervalues():
|
||||||
|
if field['is_category'] and not field['is_custom'] and 'link_column' in field:
|
||||||
|
table = self.conn.get(
|
||||||
|
'SELECT name FROM sqlite_master WHERE type="table" AND name=?',
|
||||||
|
('books_%s_link'%field['table'],), all=False)
|
||||||
|
if table is not None:
|
||||||
|
create_std_tag_browser_view(field['table'], field['link_column'],
|
||||||
|
field['column'], field['category_sort'])
|
||||||
|
|
||||||
|
db_tables = self.conn.get('''SELECT name FROM sqlite_master
|
||||||
|
WHERE type='table'
|
||||||
|
ORDER BY name''');
|
||||||
|
tables = []
|
||||||
|
for (table,) in db_tables:
|
||||||
|
tables.append(table)
|
||||||
|
for table in tables:
|
||||||
|
link_table = 'books_%s_link'%table
|
||||||
|
if table.startswith('custom_column_') and link_table in tables:
|
||||||
|
create_cust_tag_browser_view(table, link_table)
|
||||||
|
|
||||||
|
self.conn.execute('UPDATE authors SET sort=author_to_author_sort(name)')
|
||||||
|
|
||||||
|
@ -60,6 +60,7 @@ class ContentServer(object):
|
|||||||
items.sort(cmp=self.seriescmp, reverse=not order)
|
items.sort(cmp=self.seriescmp, reverse=not order)
|
||||||
else:
|
else:
|
||||||
lookup = 'sort' if field == 'title' else field
|
lookup = 'sort' if field == 'title' else field
|
||||||
|
lookup = 'author_sort' if field == 'authors' else field
|
||||||
field = self.db.FIELD_MAP[lookup]
|
field = self.db.FIELD_MAP[lookup]
|
||||||
getter = operator.itemgetter(field)
|
getter = operator.itemgetter(field)
|
||||||
items.sort(cmp=lambda x, y: cmpf(getter(x), getter(y)), reverse=not order)
|
items.sort(cmp=lambda x, y: cmpf(getter(x), getter(y)), reverse=not order)
|
||||||
|
@ -99,17 +99,20 @@ def html_to_lxml(raw):
|
|||||||
raw = etree.tostring(root, encoding=None)
|
raw = etree.tostring(root, encoding=None)
|
||||||
return etree.fromstring(raw)
|
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
|
id_ = 'calibre:category:'+item.name
|
||||||
iid = 'N' + item.name
|
iid = 'N' + item.name
|
||||||
if item.id is not None:
|
if item.id is not None:
|
||||||
iid = 'I' + str(item.id)
|
iid = 'I' + str(item.id)
|
||||||
link = NAVLINK(href = base_href + '/' + hexlify(iid))
|
link = NAVLINK(href = base_href + '/' + hexlify(iid))
|
||||||
|
count = _('%d books')%item.count
|
||||||
|
if ignore_count:
|
||||||
|
count = ''
|
||||||
return E.entry(
|
return E.entry(
|
||||||
TITLE(item.name),
|
TITLE(item.name),
|
||||||
ID(id_),
|
ID(id_),
|
||||||
UPDATED(updated),
|
UPDATED(updated),
|
||||||
E.content(_('%d books')%item.count, type='text'),
|
E.content(count, type='text'),
|
||||||
link
|
link
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -265,8 +268,12 @@ class CategoryFeed(NavFeed):
|
|||||||
def __init__(self, items, which, id_, updated, version, offsets, page_url, up_url):
|
def __init__(self, items, which, id_, updated, version, offsets, page_url, up_url):
|
||||||
NavFeed.__init__(self, 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)
|
base_href = self.base_href + '/category/' + hexlify(which)
|
||||||
|
ignore_count = False
|
||||||
|
if which == 'search':
|
||||||
|
ignore_count = True
|
||||||
for item in items:
|
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):
|
class CategoryGroupFeed(NavFeed):
|
||||||
|
|
||||||
@ -393,7 +400,7 @@ class OPDSServer(object):
|
|||||||
owhich = hexlify('N'+which)
|
owhich = hexlify('N'+which)
|
||||||
up_url = url_for('opdsnavcatalog', version, which=owhich)
|
up_url = url_for('opdsnavcatalog', version, which=owhich)
|
||||||
items = categories[category]
|
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:
|
if not items:
|
||||||
raise cherrypy.HTTPError(404, 'No items in group %r:%r'%(category,
|
raise cherrypy.HTTPError(404, 'No items in group %r:%r'%(category,
|
||||||
which))
|
which))
|
||||||
@ -458,11 +465,11 @@ class OPDSServer(object):
|
|||||||
def __init__(self, text, count):
|
def __init__(self, text, count):
|
||||||
self.text, self.count = 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()
|
category_groups = OrderedDict()
|
||||||
for x in sorted(starts, cmp=lambda x,y:cmp(x.lower(), y.lower())):
|
for x in sorted(starts, cmp=lambda x,y:cmp(x.lower(), y.lower())):
|
||||||
category_groups[x] = len([y for y in items if
|
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()]
|
items = [Group(x, y) for x, y in category_groups.items()]
|
||||||
max_items = self.opts.max_opds_items
|
max_items = self.opts.max_opds_items
|
||||||
offsets = OPDSOffsets(offset, max_items, len(items))
|
offsets = OPDSOffsets(offset, max_items, len(items))
|
||||||
|
@ -14,7 +14,7 @@ from Queue import Queue
|
|||||||
from threading import RLock
|
from threading import RLock
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
from calibre.ebooks.metadata import title_sort
|
from calibre.ebooks.metadata import title_sort, author_to_author_sort
|
||||||
from calibre.utils.config import tweaks
|
from calibre.utils.config import tweaks
|
||||||
from calibre.utils.date import parse_date, isoformat
|
from calibre.utils.date import parse_date, isoformat
|
||||||
|
|
||||||
@ -94,6 +94,9 @@ class Connection(sqlite.Connection):
|
|||||||
return ans[0]
|
return ans[0]
|
||||||
return ans.fetchall()
|
return ans.fetchall()
|
||||||
|
|
||||||
|
def _author_to_author_sort(x):
|
||||||
|
if not x: return ''
|
||||||
|
return author_to_author_sort(x.replace('|', ','))
|
||||||
|
|
||||||
class DBThread(Thread):
|
class DBThread(Thread):
|
||||||
|
|
||||||
@ -116,10 +119,12 @@ class DBThread(Thread):
|
|||||||
self.conn.create_aggregate('concat', 1, Concatenate)
|
self.conn.create_aggregate('concat', 1, Concatenate)
|
||||||
self.conn.create_aggregate('sortconcat', 2, SortedConcatenate)
|
self.conn.create_aggregate('sortconcat', 2, SortedConcatenate)
|
||||||
self.conn.create_aggregate('sort_concat', 2, SafeSortedConcatenate)
|
self.conn.create_aggregate('sort_concat', 2, SafeSortedConcatenate)
|
||||||
if tweaks['title_series_sorting'] == 'library_order':
|
if tweaks['title_series_sorting'] == 'strictly_alphabetic':
|
||||||
self.conn.create_function('title_sort', 1, title_sort)
|
|
||||||
else:
|
|
||||||
self.conn.create_function('title_sort', 1, lambda x:x)
|
self.conn.create_function('title_sort', 1, lambda x:x)
|
||||||
|
else:
|
||||||
|
self.conn.create_function('title_sort', 1, title_sort)
|
||||||
|
self.conn.create_function('author_to_author_sort', 1,
|
||||||
|
_author_to_author_sort)
|
||||||
self.conn.create_function('uuid4', 0, lambda : str(uuid.uuid4()))
|
self.conn.create_function('uuid4', 0, lambda : str(uuid.uuid4()))
|
||||||
# Dummy functions for dynamically created filters
|
# Dummy functions for dynamically created filters
|
||||||
self.conn.create_function('books_list_filter', 1, lambda x: 1)
|
self.conn.create_function('books_list_filter', 1, lambda x: 1)
|
||||||
|
@ -190,11 +190,16 @@ The most likely cause of this is your antivirus program. Try temporarily disabli
|
|||||||
Why is my device not detected in linux?
|
Why is my device not detected in linux?
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|app| uses something called SYSFS to detect devices in linux. The linux kernel can export two version of SYSFS, one of which is deprecated. Some linux distributions still ship with kernels that support the deprecated version of SYSFS, even though it was deprecated a long time ago. In this case, device detection in |app| will not work. You can check what version of SYSFS is exported by your kernel with the following command::
|
|app| needs your linux kernel to have been setup correctly to detect devices. If your devices are not detected, perform the following tests::
|
||||||
|
|
||||||
grep SYSFS_DEPRECATED /boot/config-`uname -r`
|
grep SYSFS_DEPRECATED /boot/config-`uname -r`
|
||||||
|
|
||||||
You should see something like ``CONFIG_SYSFS_DEPRECATED_V2 is not set``. If you don't you have to either recompile your kernel with the correct setting, or upgrade your linux distro to a more modern version, where this will not be set.
|
You should see something like ``CONFIG_SYSFS_DEPRECATED_V2 is not set``.
|
||||||
|
Also, ::
|
||||||
|
|
||||||
|
grep CONFIG_SCSI_MULTI_LUN /boot/config-`uname -r`
|
||||||
|
|
||||||
|
must return ``CONFIG_SCSI_MULTI_LUN=y``. If you don't see either, you have to recompile your kernel with the correct settings.
|
||||||
|
|
||||||
Library Management
|
Library Management
|
||||||
------------------
|
------------------
|
||||||
|
@ -111,7 +111,7 @@ Pre/post processing of downloaded HTML
|
|||||||
|
|
||||||
.. automember:: BasicNewsRecipe.remove_javascript
|
.. automember:: BasicNewsRecipe.remove_javascript
|
||||||
|
|
||||||
.. automethod:: BasicNewsRecipe.prepreprocess_html
|
.. automethod:: BasicNewsRecipe.skip_ad_pages
|
||||||
|
|
||||||
.. automethod:: BasicNewsRecipe.preprocess_html
|
.. automethod:: BasicNewsRecipe.preprocess_html
|
||||||
|
|
||||||
|