Merge from trunk

This commit is contained in:
Charles Haley 2010-08-08 09:47:48 +01:00
commit 087189f7fe
164 changed files with 60894 additions and 53887 deletions

View File

@ -4,6 +4,187 @@
# 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.13
date: 2010-08-06
new features:
- title: "Add a button to the edit metadata dialog to generate a cover based on the book metadata"
tickets: [5959]
- title: "When using series or title in a save template to generate a file path, remove leading prepositions. This behavior can be controlled via a tweak."
- title: "News download: When downloading news for the Kindle, do not add date to the title, to allow the Kindle's periodical archiving to work."
tickets: [6411]
- title: "Content Server OPDS feeds: Grouping of items by first alphabet is now case-insensitive."
- title: "Do not allow the user to use save to disk to save files into the calibre library"
tickets: [6392]
- title: "Switch to a new C based API for using ImageMagick. More robust and a minor speedup when doing image manipulations"
- title: "Move cover downloading to a plugin based API. You can now add new cover sources to calibre using plugins."
bug fixes:
- title: "Content server OPDS feeds: Handle the case when the author field is blank"
tickets: [6371]
- title: "TXT Input: Strip out illegal chars from txt files."
tickets: [6335]
- title: "Save to disk/send to device templates: Always render {series_index} as an empty string when the book has no series."
tickets: [6409]
- title: "PD Novel driver: Remove covers when deleting books"
new recipes:
- title: "Snopes"
author: Startson17
- title: "dr.dk and Balkan Insight"
author: Darko Miletic
- title: Folha de Sao Paulo
author: Saverio Palmieri Neto
improved recipes:
- Honolulu Star Advertiser
- Nature News
- Associated Press
- Scientific American
- New Scientist
- version: 0.7.12
date: 2010-07-30
bug fixes:
- title: "Fix a typo that caused a harmless error message after setting preferences"
- title: "Linux build system: No longer search for poppler-qt4 libraries as they are not needed"
- version: 0.7.11
date: 2010-07-30
new features:
- title: "EPUB metadata: When setting metadata in an EPUB file, if it has a well defined image based cover, update it"
- title: "Support for Droid X, Samsung Vibrant and Promedia ebook reader"
- title: "Add entry to Connect/share menu to conveniently stop and start the Content Server"
- title: "News download: Make the navbars on the section index pages more useful, adding links to net and previous section"
- title: "Add a button to reset confirm dialogs to Preferences->General"
bug fixes:
- title: "Fix crash in edit metadata dialog if you click OK before cover download completes"
tickets: [6337]
- title: "Kobo driver: Show a warning when the user tries to export/view .kobo files. Also add support for the new sofroot vendor id"
- title: "Update check. Do not be fooled by a redirecting proxy when checking for new version"
tickets: [6325]
- title: "Add book count to tooltip of library button in toolbar"
tickets: [6340]
- title: "Content server: When serving OPDS feeds send the correct content-type header."
tickets: [6329]
- title: "PDF Output: Don't insert blank pages before every individual HTML file in the ebook."
tickets: [6315]
- title: "Fix saving of cover when path to book folder contains non ascii characters"
tickets: [6328]
- title: "Fix regression that broke showing send to actions for multiple email accounts"
- title: "Fix bug in handlling of hexadecimal entities"
tickets: [6305]
- title: "SONY driver: More fixes to handle broken media.xml files"
tickets: [6296]
- title: "Linux installer: Fix rendering of viewer icon and restrict all icons to 128x128 since GNOME can't handle large icons"
- title: "RTF Input: Fix handling of hard linebreaks"
tickets: [6208]
- title: "RTF Output: Fix regression that broke rendering of bold and italic text"
tickets: [6098]
new recipes:
- title: "Draw and Cook"
author: Startson17
improved recipes:
- La Nacion
- Vecernje Novosti
- Der Tagesspiegel
- Die Zeit Nachrichten
- Toms Hardware (DE)
- Welt Online
- version: 0.7.10
date: 2010-07-23
new features:
- title: "Allow user customization of static resources such as icons and templates"
type: major
description: >
"You can now change the icons used in the User Interface and other static resources. Details on how to
do this are at: http://calibre-ebook.com/user_manual/customize.html#overriding-icons-templates-etcetera"
- title: "Split the 'Send to device' button into two buttons, 'Connect/share' and 'Send to device'. The new 'Send to device' button will now only be available when a device is connected."
- title: "Store column layout, saved searches and user categories seprately per calibre library. This makes it possible to easily switch between libraries with different custom column setups"
- title: "See the last modofied date for each format in the edit metadata dialog via a tooltip"
tickets: [6252]
- title: "PD Novel driver: Add support for uploading cover thumbnails to device"
- title: "More sophisticated metadata extraction from HTML files"
tickets: [6223]
bug fixes:
- title: "Fix problems with a few windows installs caused by the upgrade to Qt 4.6.3 in the previous release. These would manifest as a not working Add Books button, or deletes not actually deleting files, etc."
- title: "Restore configurability of toolbar, which was temporarily removed in 0.7.9. You can once again set icon size via Preferences->Interface"
- title: "Fix regression in iTunes driver in 0.7.9 when sending series info"
- title: "Search: Fix parsing of search terms that contain a word that starts with 'and' or 'or' and is not the first word"
- title: "When merging records also merge metadata in custom columns"
tickets: [6120]
- title: "When scrolling to show a particular row, handle the case when the first column is a custom column"
tickets: [6176]
- title: "Fix SD card detection for The Augen Book"
tickets: [6224]
- title: "CHM Input: Fix a couple of bugs that could cause crashes"
tickets: [6240]
- title: "Conversion pipeline: Handle zero width elements with non zero indents gracefully"
tickets: [6230]
new recipes:
- title: "daum.net"
author: trustin
- title: "MIT Technology Review, Alternet, Waco Tribune Herald and Orlando Sentinel"
author: rty
improved recipes:
- The BBC
- heise
- version: 0.7.9 - version: 0.7.9
date: 2010-07-17 date: 2010-07-17

View File

@ -0,0 +1,83 @@
/* CSS for the mobile version of the content server webpage */
.navigation table.buttons {
width: 100%;
}
.navigation .button {
width: 50%;
}
.button a, .button:visited a {
padding: 0.5em;
font-size: 1.25em;
border: 1px solid black;
text-color: black;
background-color: #ddd;
border-top: 1px solid ThreeDLightShadow;
border-right: 1px solid ButtonShadow;
border-bottom: 1px solid ButtonShadow;
border-left: 1 px solid ThreeDLightShadow;
-moz-border-radius: 0.25em;
-webkit-border-radius: 0.25em;
}
.button:hover a {
border-top: 1px solid #666;
border-right: 1px solid #CCC;
border-bottom: 1 px solid #CCC;
border-left: 1 px solid #666;
}
div.navigation {
padding-bottom: 1em;
clear: both;
}
#search_box {
border: 1px solid #393;
-moz-border-radius: 0.5em;
-webkit-border-radius: 0.5em;
padding: 1em;
margin-bottom: 0.5em;
float: right;
}
#listing {
width: 100%;
border-collapse: collapse;
}
#listing td {
padding: 0.25em;
}
#listing td.thumbnail {
height: 60px;
width: 60px;
}
#listing tr:nth-child(even) {
background: #eee;
}
#listing .button a{
display: inline-block;
width: 2.5em;
padding-left: 0em;
padding-right: 0em;
overflow: hidden;
text-align: center;
}
#logo {
float: left;
}
#spacer {
clear: both;
}

View File

@ -72,4 +72,11 @@ gui_pubdate_display_format = 'MMM yyyy'
# 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'
# Control how title and series names are formatted when saving to disk/sending
# to device. If set to library_order, leading articles such as The and A will
# be put at the end
# If set to 'strictly_alphabetic', the titles will be sorted without processing
# For example, with library_order, "The Client" will become "Client, The". With
# strictly_alphabetic, it would remain "The Client".
save_template_title_series_sorting = 'library_order'

File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 274 KiB

After

Width:  |  Height:  |  Size: 28 KiB

View File

@ -1,8 +1,9 @@
<?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: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://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg" xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@ -10,106 +11,573 @@
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"
version="1.0" version="1.0"
width="48" width="128"
height="48" height="128"
id="svg2160" id="svg4486"
sodipodi:version="0.32" inkscape:version="0.47 r22583"
inkscape:version="0.45.1" sodipodi:docname="epub.svg">
sodipodi:docname="mobi.svg"
inkscape:output_extension="org.inkscape.output.svg.inkscape"
sodipodi:docbase="/home/kovid/temp">
<sodipodi:namedview
inkscape:window-height="674"
inkscape:window-width="791"
inkscape:pageshadow="2"
inkscape:pageopacity="0.0"
guidetolerance="10.0"
gridtolerance="10.0"
objecttolerance="10.0"
borderopacity="1.0"
bordercolor="#666666"
pagecolor="#ffffff"
id="base"
inkscape:zoom="9.6458333"
inkscape:cx="24"
inkscape:cy="24"
inkscape:window-x="0"
inkscape:window-y="31"
inkscape:current-layer="svg2160" />
<metadata <metadata
id="metadata7"> id="metadata52">
<rdf:RDF> <rdf:RDF>
<cc:Work <cc:Work
rdf:about=""> rdf:about="">
<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:title></dc:title>
</cc:Work> </cc:Work>
</rdf:RDF> </rdf:RDF>
</metadata> </metadata>
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1366"
inkscape:window-height="692"
id="namedview50"
showgrid="false"
inkscape:snap-bbox="true"
inkscape:zoom="2.3256144"
inkscape:cx="73.759964"
inkscape:cy="13.023094"
inkscape:window-x="0"
inkscape:window-y="25"
inkscape:window-maximized="1"
inkscape:current-layer="svg4486" />
<defs <defs
id="defs2162" /> id="defs4488">
<linearGradient
id="linearGradient3672">
<stop
style="stop-color:#2b2b2b;stop-opacity:1;"
offset="0"
id="stop3674" />
<stop
id="stop3685"
offset="0.5"
style="stop-color:#242424;stop-opacity:1;" />
<stop
style="stop-color:#363636;stop-opacity:1;"
offset="0.75"
id="stop3689" />
<stop
style="stop-color:#2b2b2b;stop-opacity:1;"
offset="1"
id="stop3676" />
</linearGradient>
<inkscape:perspective
sodipodi:type="inkscape:persp3d"
inkscape:vp_x="0 : 64 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_z="128 : 64 : 1"
inkscape:persp3d-origin="64 : 42.666667 : 1"
id="perspective54" />
<linearGradient
id="linearGradient5048">
<stop
id="stop5050"
style="stop-color:#000000;stop-opacity:0"
offset="0" />
<stop
id="stop5056"
style="stop-color:#000000;stop-opacity:1"
offset="0.5" />
<stop
id="stop5052"
style="stop-color:#000000;stop-opacity:0"
offset="1" />
</linearGradient>
<linearGradient
id="linearGradient5060">
<stop
id="stop5062"
style="stop-color:#000000;stop-opacity:1"
offset="0" />
<stop
id="stop5064"
style="stop-color:#000000;stop-opacity:0"
offset="1" />
</linearGradient>
<linearGradient
id="linearGradient3104">
<stop
id="stop3106"
style="stop-color:#aaaaaa;stop-opacity:1"
offset="0" />
<stop
id="stop3108"
style="stop-color:#c8c8c8;stop-opacity:1"
offset="1" />
</linearGradient>
<linearGradient
id="linearGradient3600">
<stop
id="stop3602"
style="stop-color:#f4f4f4;stop-opacity:1"
offset="0" />
<stop
id="stop3604"
style="stop-color:#dbdbdb;stop-opacity:1"
offset="1" />
</linearGradient>
<radialGradient
cx="102"
cy="112.3047"
r="139.55859"
id="XMLID_8_"
gradientUnits="userSpaceOnUse">
<stop
id="stop41"
style="stop-color:#b7b8b9;stop-opacity:1"
offset="0" />
<stop
id="stop47"
style="stop-color:#ececec;stop-opacity:1"
offset="0.18851049" />
<stop
id="stop49"
style="stop-color:#fafafa;stop-opacity:0"
offset="0.25718147" />
<stop
id="stop51"
style="stop-color:#ffffff;stop-opacity:0"
offset="0.30111277" />
<stop
id="stop53"
style="stop-color:#fafafa;stop-opacity:0"
offset="0.53130001" />
<stop
id="stop55"
style="stop-color:#ebecec;stop-opacity:0"
offset="0.84490001" />
<stop
id="stop57"
style="stop-color:#e1e2e3;stop-opacity:0"
offset="1" />
</radialGradient>
<linearGradient
id="linearGradient3211">
<stop
id="stop3213"
style="stop-color:#ffffff;stop-opacity:1"
offset="0" />
<stop
id="stop3215"
style="stop-color:#ffffff;stop-opacity:0"
offset="1" />
</linearGradient>
<linearGradient
id="linearGradient8589">
<stop
id="stop8591"
style="stop-color:#fefefe;stop-opacity:1"
offset="0" />
<stop
id="stop8593"
style="stop-color:#cbcbcb;stop-opacity:1"
offset="1" />
</linearGradient>
<filter
x="-0.14846256"
y="-0.16434373"
width="1.2969251"
height="1.3286875"
color-interpolation-filters="sRGB"
id="filter3212">
<feGaussianBlur
stdDeviation="0.77391625"
id="feGaussianBlur3214" />
</filter>
<linearGradient
x1="32.892288"
y1="8.0590115"
x2="36.358372"
y2="5.4565363"
id="linearGradient2455"
xlink:href="#linearGradient8589"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(2.6605294,0,0,2.7751643,0.7455334,-3.5662351)" />
<linearGradient
x1="24"
y1="1.9999999"
x2="24"
y2="46.01725"
id="linearGradient2459"
xlink:href="#linearGradient3211"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(2.7575715,0,0,2.6744155,-2.1817183,-4.1859717)" />
<radialGradient
cx="102"
cy="112.3047"
r="139.55859"
id="radialGradient2462"
xlink:href="#XMLID_8_"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0.9787237,0,0,-1.0535153,1.3617012,127.48164)" />
<linearGradient
x1="25.132275"
y1="0.98520643"
x2="25.132275"
y2="47.013336"
id="linearGradient2465"
xlink:href="#linearGradient3600"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(2.657139,0,0,2.5422197,0.2286615,-4.9132741)" />
<linearGradient
x1="-51.786404"
y1="50.786446"
x2="-51.786404"
y2="2.9062471"
id="linearGradient2467"
xlink:href="#linearGradient3104"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(2.1456312,0,0,2.3791388,158.0899,-7.7465258)" />
<linearGradient
x1="302.85715"
y1="366.64789"
x2="302.85715"
y2="609.50507"
id="linearGradient2483"
xlink:href="#linearGradient5048"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0.1725148,0,0,0.03920058,0.6482238,98.773724)" />
<radialGradient
cx="605.71429"
cy="486.64789"
r="117.14286"
fx="605.71429"
fy="486.64789"
id="radialGradient2485"
xlink:href="#linearGradient5060"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(-0.05903807,0,0,0.03920058,56.929907,98.773804)" />
<radialGradient
cx="605.71429"
cy="486.64789"
r="117.14286"
fx="605.71429"
fy="486.64789"
id="radialGradient2487"
xlink:href="#linearGradient5060"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0.05903808,0,0,0.03920058,69.07008,98.773804)" />
<inkscape:perspective
id="perspective2878"
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
inkscape:vp_z="1 : 0.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_x="0 : 0.5 : 1"
sodipodi:type="inkscape:persp3d" />
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient7012-661-145-733-759-865-745-661-970-94"
id="linearGradient2989"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0.7993221,0,0,1.0036506,40.855793,-1.5607197)"
x1="-22.539846"
y1="11.109024"
x2="-22.539846"
y2="46.263954" />
<linearGradient
id="linearGradient7012-661-145-733-759-865-745-661-970-94">
<stop
offset="0"
style="stop-color:#f0c178;stop-opacity:1"
id="stop3618" />
<stop
offset="0.5"
style="stop-color:#e18941;stop-opacity:1"
id="stop3270" />
<stop
offset="1"
style="stop-color:#ec4f18;stop-opacity:1"
id="stop3620" />
</linearGradient>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3390-178-986-453"
id="linearGradient2991"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0.9110051,0,0,0.9769973,0.6027366,-0.94793564)"
x1="9.4919996"
y1="46.314064"
x2="9.4919996"
y2="1.7164899" />
<linearGradient
id="linearGradient3390-178-986-453">
<stop
offset="0"
style="stop-color:#c92e13;stop-opacity:1;"
id="stop3624" />
<stop
offset="1"
style="stop-color:#dea176;stop-opacity:1;"
id="stop3626" />
</linearGradient>
<linearGradient
y2="46.263954"
x2="-22.539846"
y1="11.109024"
x1="-22.539846"
gradientTransform="matrix(1.2084176,0,0,2.666214,69.448297,-3.8858475)"
gradientUnits="userSpaceOnUse"
id="linearGradient2892"
xlink:href="#linearGradient7012-661-145-733-759-865-745-661-970-94"
inkscape:collect="always" />
<linearGradient
y2="1.7164899"
x2="9.4919996"
y1="46.314064"
x1="9.4919996"
gradientTransform="matrix(1.3772603,0,0,2.595409,8.5935979,-2.257977)"
gradientUnits="userSpaceOnUse"
id="linearGradient2894"
xlink:href="#linearGradient3390-178-986-453"
inkscape:collect="always" />
<inkscape:perspective
id="perspective3666"
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
inkscape:vp_z="1 : 0.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_x="0 : 0.5 : 1"
sodipodi:type="inkscape:persp3d" />
<linearGradient
x1="302.85715"
y1="366.64789"
x2="302.85715"
y2="609.50507"
id="linearGradient19619"
xlink:href="#linearGradient5048-1"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0.08449704,0,0,0.01235294,-6.5396456,38.470822)" />
<linearGradient
id="linearGradient5048-1">
<stop
id="stop5050-2"
style="stop-color:black;stop-opacity:0"
offset="0" />
<stop
id="stop5056-0"
style="stop-color:black;stop-opacity:1"
offset="0.5" />
<stop
id="stop5052-7"
style="stop-color:black;stop-opacity:0"
offset="1" />
</linearGradient>
<radialGradient
cx="605.71429"
cy="486.64789"
r="117.14286"
fx="605.71429"
fy="486.64789"
id="radialGradient19616"
xlink:href="#linearGradient5060-3"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(-0.0289166,0,0,0.01235294,21.026894,38.470848)" />
<linearGradient
id="linearGradient5060-3">
<stop
id="stop5062-1"
style="stop-color:black;stop-opacity:1"
offset="0" />
<stop
id="stop5064-1"
style="stop-color:black;stop-opacity:0"
offset="1" />
</linearGradient>
<radialGradient
cx="605.71429"
cy="486.64789"
r="117.14286"
fx="605.71429"
fy="486.64789"
id="radialGradient19613"
xlink:href="#linearGradient5060-3"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0.02891661,0,0,0.01235294,26.973101,38.470848)" />
<linearGradient
id="linearGradient3680">
<stop
id="stop3682"
style="stop-color:black;stop-opacity:1"
offset="0" />
<stop
id="stop3684"
style="stop-color:black;stop-opacity:0"
offset="1" />
</linearGradient>
<linearGradient
x1="108.26451"
y1="110.28094"
x2="25.817675"
y2="14.029031"
id="linearGradient19610"
xlink:href="#linearGradient259-942"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0.5066363,0,0,0.3512482,-58.338079,-49.085986)" />
<linearGradient
id="linearGradient259-942">
<stop
id="stop3802"
style="stop-color:white;stop-opacity:1"
offset="0" />
<stop
id="stop3804"
style="stop-color:#e0e0e0;stop-opacity:1"
offset="1" />
</linearGradient>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3650"
id="linearGradient3656"
x1="-44.02877"
y1="-26.590452"
x2="-4.1013746"
y2="-26.590452"
gradientUnits="userSpaceOnUse" />
<linearGradient
id="linearGradient3650">
<stop
style="stop-color:#73d216;stop-opacity:1;"
offset="0"
id="stop3652" />
<stop
style="stop-color:#8ae234;stop-opacity:1;"
offset="1"
id="stop3654" />
</linearGradient>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient3672"
id="linearGradient3682"
x1="32.254131"
y1="57.967407"
x2="98.357651"
y2="57.967407"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(19.439951,41.709408)" />
<inkscape:perspective
id="perspective2970"
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
inkscape:vp_z="1 : 0.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_x="0 : 0.5 : 1"
sodipodi:type="inkscape:persp3d" />
<inkscape:perspective
id="perspective2984"
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
inkscape:vp_z="1 : 0.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_x="0 : 0.5 : 1"
sodipodi:type="inkscape:persp3d" />
<inkscape:perspective
id="perspective3890"
inkscape:persp3d-origin="150 : 23.333333 : 1"
inkscape:vp_z="300 : 35 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_x="0 : 35 : 1"
sodipodi:type="inkscape:persp3d" />
<inkscape:perspective
id="perspective3915"
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
inkscape:vp_z="1 : 0.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_x="0 : 0.5 : 1"
sodipodi:type="inkscape:persp3d" />
<inkscape:perspective
id="perspective4057"
inkscape:persp3d-origin="150 : 23.333333 : 1"
inkscape:vp_z="300 : 35 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_x="0 : 35 : 1"
sodipodi:type="inkscape:persp3d" />
<inkscape:perspective
id="perspective4082"
inkscape:persp3d-origin="0.5 : 0.33333333 : 1"
inkscape:vp_z="1 : 0.5 : 1"
inkscape:vp_y="0 : 1000 : 0"
inkscape:vp_x="0 : 0.5 : 1"
sodipodi:type="inkscape:persp3d" />
</defs>
<g <g
id="layer1"> transform="matrix(1.0408163,0,0,0.6302428,-1.5714269,43.690218)"
<image id="g2478">
id="image2226" <rect
width="48" width="83.299995"
y="0" height="9.5201406"
xlink:href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAABmJLR0QA/wD/AP+gvaeTAAAACXBI x="21.349998"
WXMAAABIAAAASABGyWs+AAAACXZwQWcAAAAwAAAAMADO7oxXAAALJUlEQVRo3u2ae3DTVRbHP3k1 y="113.14653"
v6ZJk4a+W1NalAJSy4qtvKS4iCKOYtcHvvCxDuuMozu62vVRdGcRfOzsODoq7o4jyoyy49aFqYoO id="rect2879"
7rJjaREsKi2F0sC2CNImado82ib5/fJo9o+0vzS2pe2i4B97/rq553fu7/u9597zu+fcKB56+OFo style="opacity:0.3;fill:url(#linearGradient2483);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1;marker:none;visibility:visible;display:inline;overflow:visible" />
bnY2fr8fKRgkIIpIosiwaAUBAK/HA8A0s1nW6fV6RsrGTZsUnGNRPL9pU/T3TzyZ0OnxuEe0PUiS <path
hBiIkero6MDb50UMBJCCQex2OwFRRK1UyjY+vx+320NamglRFDGaTLz+2ms/CTn1WJ0mU9qYbYDS d="m 21.35,113.14694 c 0,0 0,9.51962 0,9.51962 -3.040314,0.0179 -7.35,-2.13287 -7.35,-4.76043 0,-2.62755 3.392762,-4.75919 7.35,-4.75919 z"
efPGHWyY+GPbtuNtP8ZTa29HCkp8unMn66uroz+Fh9RnP0RcPm5u5s26Iyj1M3nghhky2dbWVg63 id="path2881"
tPzY2M+ewMleD0fajnGk8yQ72rpR6meSlrsU6/F/smzWHT8J4EkR2NnUwvYvGznaI57RWKmfiV6n style="opacity:0.3;fill:url(#radialGradient2485);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1;marker:none;visibility:visible;display:inline;overflow:visible" />
J90wl5yCKGIogj8cpaLAiEGrPX8ENn20h7WrbuayEX2u/hAAPilMMDRIIDSIFB5EDEUIhOPgHQMe <path
VllSzwl4AOVkHposeF84QtD5NeuuvurnQ2Aq4AfEfioKjOcM/IQEpgLeGx7E2dPOQ1cv+3kQmDL4 d="m 104.64999,113.14694 c 0,0 0,9.51962 0,9.51962 3.04032,0.0179 7.35001,-2.13287 7.35001,-4.76043 0,-2.62755 -3.39277,-4.75919 -7.35001,-4.75919 z"
cJQbLIMUppsn/fKflMBUwfedbuaxFVeeU/BnJDAl8JFB7pkJuRnxr7YoipMCcNYE/H7/mIqpgKdj id="path2883"
d8LsnyvwcAYPTBq8388Lyy9khiX350VgUuCBctURfrv6+kRbSTr/BCYDPs19hDfuWDnaNvAz8MBE style="opacity:0.3;fill:url(#radialGradient2487);fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:1;marker:none;visibility:visible;display:inline;overflow:visible" />
4LG18PK1hcy5IG+Uba+r9/wTmAj8O5UzuHXBglF2nqHM7bwTONOyeadyBmsrlo1p5/V4zymBcfOB
8Wb+hpmMCx6g025HSNJM+OL11dVRKRiUfw977q233ppS1jYugVHgxTCkzWbrKRtsfpu3H7x/TLtu
uw2LxTKmrqqqKhoQRfqkKKecMU8lZZoJdrvQAnapi0WLFkUBcnPz+PDDmgnJjEtgFPhhSc5h6ylg
89tsvv/OBBuPx4MYCAAgDFUzACwWC3V1dSiSjcxetoQ8IJixEJ8UJq3vAMyBJFsns5iF7cFaQnXT
cLZFKCsrixYVXcQHH2wbl8i4e2BM8AkkTPx9//6E7tY2KwAqTTwbe/fdd/j6wAHmXHsdV66pRF+0
FE3uLMwGDSlaNe7UMvlZ2+JacvauRlPRS+4DPVTeeCP9Of9hxYoV0SkTGBf8CBKP7mhM6DpubUOt
UsneePyx3wGx2lJawWwGUmYP/c7E1R+SSTiS5+PTGcjZuxrb4lrMu68BwLV8F8svWMOcq/QsWrFo
TBLjEzgT+CHxCDNkL9hsNux2u6x78smnmFtSAsCMOSUUqsKsKtSywDQAQJJzX8JYUr9IS84qnI13
oRUEzLuvQWfVYltci0W9hMsrBXl/TN4DE0lyDl8caQdg/4jltGP7hyxcuACLxYKqsBCtUsWg1yPr
NREnIW8fAx178ElhkjRKfBddS7SnHk/KQnw6A67luwDI2bsaSRTJcC/HVB5mzZo7EkicHQGQKxf7
v/oKgPYTJ0gWBMzz5tHd3U1+RhYhQYc0GKHl22843Pk9IVUGutnl6IuWkqJVk6KNxRJF+hUkqxX0
/eI9efxDGgNWQwau5bu4NGUFfn//j+iBIdm7dy92W2z5dPa0U1xcTF4kIuu7jx/h+/YTlFw6n7l5
FwCxfSCJ3UAs9xiWS07Xjhp/n/8iAHQ6HWk5BVRVVcleOGsClbMyqampAWIbN2NWFEEQ6FSpEEUR
x6EmLBYLupRkXO5e9nviBeFQV5vcDoRiUc9qyEC752YAPnG/gFW0cCwi8NEXf+Rd1SLasmcmHFeU
I7+GU5aAjYHT3TQ1NQFgt9spmXYdmspdRE6cAKAgP3bYi0aCCeABHMnzRw3pSVlIUsgvk2gdTCNV
peRYRGCh7jhrjCF84fhhUR04i+RjevQk1sZ4KDWka2Ifsr9VIAgCzp5eGrocZGdl0OnoIY9Pcer0
6IuW4uoPkaRR4h6ITaAUHsQdCGPy7aPDYKZDWYZXjBUWKnXNmAKfoSnvhb2r0RjjS04pjUFgdrrA
hBKwcYmqg4aGehTKWOyfe+F8hj2qqdyFRqUk5O0jcuIEeVnpGG6vw9DrlCseAFmBb+TlM1KKBg9w
Z0Y1v1lWRYGxBk1FfNb7uuLPqQEkSUSrjYNeYdaxtbkOcivGBU/DmzR+d3RMteH2OrldfHe93HZu
Kad/WgYQLxpYKYWh2R+WAmMN/mKJsbP1RFECuN3uhM7bbrudu1V26KqLgR0JvKsOGt5E/XU9fp+P
oqIiysvLWHPrrfg0hnFf5NxSji0QxZ1allDxEEMRxFAsYomhiAxeZ51ccVgN4LA7yM7OSVBs2fwq
S999h+0fb+PL7xspzb2MFJWSJeVlZC68k+IXn2XhkqUJNk0vv4EroiF/6LcUDOLcUo5LrcWVfyVZ
gW9wDAGH2LoPhGMR0RWY+Muvs2pRqhLPn2qAg00Hx7x5uffe+7j33vtG9dfVN1C/t4Gc/AuYPr1Q
7p97URG1fdMx/yWEbXEtLD7JaetmjMkaGF4uQ0CHc24AX8ALpNAlhviSP7DI+keMPSvxF9eis2o5
6b2Fot0u8rLS6cdHOBD/GCuzMjNpam4eBdLjcVNX30BdfUNC/2uvv85gfx+P5inY8kDicdqkSyYQ
jnJIY8DZeBc6q5YZjn9j7w/iDYRwB8JEe+pxB8IEwlFcgbA8811iCGc4SuOAhg2nN1ATuZg/736J
Dac3ANBhMOPU6Wk9cpjs7Ow4gY2bNim8Hg92uy0B/MbnniNJMcjsDY/zp5delHXdtk6WuFpwb91B
yqGTCQTL5pdi8sUOaR3KMow98YrF8Gx3KucghiIycF84gissYFQr6YsMysf44fh/jyF2NaVIv4Ik
Wyd2uz0ha1NCLPlobW2VX7bxued45u5fcem+f6A63Y9vTxxkkuTDvXUHqtOxM0lpycVxD5jSWGHW
AaDFx2fayzGrQph8++TZlkghO9iIFh9afJQovyUp2ENSsEceJ1WlpFLXTIZaQYeyDMm4CK1aSVtb
W8LsywRSdDpaRlzCGRVhhM9rGNjyr1FLK9TytQw+eOdNo24xV65cSb4ocrn/W1xhAVdEQ1G/Cy0+
IBbfi/udzLvsaeZd9jQZ5e+xZOnzFAunmKkSWaCJpZpW0YJRrcScrCY7VUvk0PtoBWFUzqwGeOXV
VxVr166Vd0aXL8jAlnj8LrzlJrn967++z9sffEBmVhbP/GCDezxuTn53EoXXweeqLErSv2U3Myk2
nOIKbwN6QYNtcS0uwLz7GsT8L2TbfFHkWESAiECqCvQ6PSnJWuYH9yEdF/E5bLw+RsIvd6xbty76
yCOPUFw8i+amJrZXrycadqH/5Q388CL8h4APNh2kqblZvs03mkwkCwI1g5lUFBjZ2l/Cs/nPAuAv
jpcddVatHPNfsW0kGz92dJTqk8gzaCjVHKa9tRWFwzZutSIhqB48eJDi4lmUzptH6c5PRj1st9uw
Hm/n6OFDtB07lgDYlJo66ja+qqoqum3/UcovFIk4rkda+qEMeiSJYbHoNRSqtWQHG8ELrQfqY7f8
Zyi1yIr11dVRR3c3TzzxBNOnF2K323DYHRxtO0rb0aM4urvlqvM0sxm9Xj+p/0asr66OnnJ6sSUL
5AREpi+OJf5eux5NRa9MIOK4HkdfD6HMfHwH95GmVpOVmTnhOxKU69atiw6DFAQBURQRBGFSA02G
iKevD6/HQ7u9HU2GREawiIhSIkmbRCSgIC3NhFYQMKWmnpc/jvxf/hf5L875Zqc0KDqlAAAAAElF
TkSuQmCC
"
x="0"
height="48" />
</g> </g>
<path
d="m 17.499961,1.499962 c 21.311039,0 42.622077,0 63.933118,0 3.738416,1.2619859 23.822451,15.639336 29.066961,25.594247 0,30.468614 0,60.937223 0,91.405831 -31.000026,0 -62.000051,0 -93.000079,0 0,-39.000023 0,-78.000052 0,-117.000078 z"
id="path4160"
style="fill:url(#linearGradient2465);fill-opacity:1;stroke:url(#linearGradient2467);stroke-width:0.99992192;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;display:inline" />
<path
d="M 18.978723,118 C 18.439449,118 18,117.52697 18,116.94648 L 18,3.1668773 C 18,2.5853392 18.439449,2.1133645 18.978723,2.1133645 39.22709,2.4045657 61.66587,1.6777678 81.889633,2.1858707 L 109.71323,26.088148 110,116.94648 C 110,117.52697 109.56154,118 109.02128,118 l -90.042557,0 z"
id="path4191"
style="fill:url(#radialGradient2462);fill-opacity:1" />
<path
d="m 109.50003,26.517935 c 0,29.94778 0,61.034321 0,90.982105 -30.333353,0 -60.666712,0 -91.00007,0 0,-38.333364 0,-76.666721 0,-115.0000784 20.852737,0 42.202796,0 63.055535,0"
id="path2435"
style="opacity:0.6;fill:none;stroke:url(#linearGradient2459);stroke-width:0.99992174;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;display:inline" />
<path
d="m 28.617256,0.92125832 c 4.282522,0 2.15325,8.48317128 2.15325,8.48317128 0,0 10.357642,-1.8023467 10.357642,2.8187444 0,-2.6097233 -11.302304,-10.7285956 -12.510892,-11.30191568 z"
transform="matrix(2.6666667,0,0,2.6666667,0.3087257,-0.6174513)"
id="path12038"
style="opacity:0.4;fill:#000000;fill-opacity:1;fill-rule:evenodd;stroke:none;display:inline;filter:url(#filter3212)" />
<path
d="m 76.62141,1.8392376 c 8.497314,0 6.228693,20.4316764 6.228693,20.4316764 0,0 27.133687,-2.616144 27.133687,9.706766 0,-3.002504 0.2291,-5.152653 -0.35674,-6.089581 C 105.41822,19.156956 87.239007,4.052365 80.67425,2.0726634 80.182991,1.9245177 79.093706,1.8392376 76.62141,1.8392376 z"
id="path4474"
style="fill:url(#linearGradient2455);fill-opacity:1;fill-rule:evenodd;stroke:none;display:inline" />
<path
style="fill:url(#linearGradient2892);fill-opacity:1;fill-rule:nonzero;stroke:url(#linearGradient2894);stroke-width:1.00930357;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-opacity:1;stroke-dasharray:none;stroke-dashoffset:0;marker:none;visibility:visible;display:block;overflow:visible"
id="path4530"
d="m 25.816692,118.46233 c -2.652245,0 -5.304508,0 -7.956769,0 -0.64465,-1.3837 -0.154446,-4.13089 -0.307623,-6.08978 0,-36.699903 0,-73.399803 0,-110.0996953 l 0.08995,-0.4752429 0.217716,-0.1962367 0,0 c 2.76046,0 5.088125,0 7.848554,0" />
<text
xml:space="preserve"
style="font-size:72px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;fill:#000000;fill-opacity:1;stroke:none;font-family:Sans;-inkscape-font-specification:Sans"
x="38.310501"
y="64.757271"
id="text2892"><tspan
sodipodi:role="line"
id="tspan2894" /></text>
<text
xml:space="preserve"
style="font-size:28px;font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;fill:#000000;fill-opacity:1;stroke:none;font-family:FreeSans;-inkscape-font-specification:FreeSans"
x="35.259502"
y="109.51025"
id="text3772"><tspan
sodipodi:role="line"
id="tspan3774"
x="35.259502"
y="109.51025"
style="font-size:28px;fill:#000000;fill-opacity:1">mobi</tspan></text>
<path
id="path3858"
d="m 100.22334,68.053513 c -8.716894,6.432161 -21.357191,9.853809 -32.243032,9.853809 -15.251419,0 -28.989193,-5.636741 -39.38419,-15.021522 -0.815557,-0.738364 -0.08726,-1.746902 0.89191,-1.173831 11.217267,6.527812 25.085933,10.456247 39.410201,10.456247 9.664184,0 20.284886,-2.004491 30.058986,-6.151079 1.474215,-0.623415 2.709285,0.97246 1.266125,2.036376 z"
style="fill:#ff9201;fill-rule:evenodd" />
<path
id="path3860"
d="m 103.85056,63.911959 c -1.11342,-1.426386 -7.371902,-0.676274 -10.179364,-0.337298 -0.853315,0.09817 -0.984206,-0.641874 -0.217315,-1.184739 4.990672,-3.505554 13.168059,-2.491141 14.119539,-1.318987 0.95736,1.187256 -0.25087,9.384779 -4.92858,13.293915 -0.71907,0.604117 -1.40373,0.286117 -1.08573,-0.510142 1.05133,-2.631263 3.40906,-8.515524 2.29145,-9.942749 z"
style="fill:#ff9201;fill-rule:evenodd" />
<path
id="path4047"
d="m 69.299213,48.156661 c 0,2.157 0.052,3.954 -1.035,5.874 -0.88,1.561 -2.279,2.517 -3.833,2.517 -2.121,0 -3.366,-1.62 -3.366,-4.015 0,-4.714 4.232,-5.571 8.233,-5.571 v 1.195 z m 5.579,13.496 c -0.365,0.332 -0.896,0.352 -1.307,0.132 -1.838,-1.528 -2.167,-2.231 -3.174,-3.69 -3.035,3.094 -5.188,4.023 -9.123,4.023 -4.663,0 -8.284,-2.876 -8.284,-8.629 0,-4.49 2.433,-7.544 5.899,-9.045 3.005,-1.317 7.202,-1.556 10.41,-1.914 v -0.723 c 0,-1.313 0.104,-2.875 -0.671,-4.012 -0.674,-1.021 -1.968,-1.437 -3.106,-1.437 -2.111,0 -3.99,1.078 -4.45,3.321 -0.097,0.498 -0.46,0.991 -0.962,1.017 l -5.364,-0.581 c -0.456,-0.102 -0.958,-0.463 -0.828,-1.155 1.233,-6.511 7.109,-8.475 12.378,-8.475 2.693,0 6.215,0.719 8.335,2.757 2.693,2.515 2.432,5.869 2.432,9.524 v 8.623 c 0,2.596 1.081,3.732 2.091,5.128 0.354,0.503 0.434,1.103 -0.018,1.473 -1.131,0.949 -3.139,2.693 -4.244,3.676 l -0.014,-0.013 z"
style="fill-rule:evenodd" />
</svg> </svg>

Before

Width:  |  Height:  |  Size: 5.6 KiB

After

Width:  |  Height:  |  Size: 22 KiB

View File

@ -0,0 +1,63 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" version="1.0" width="128" height="128" id="svg2176">
<defs id="defs2178">
<linearGradient x1="406.065" y1="290.50299" x2="406.065" y2="276.29501" id="linearGradient4819" xlink:href="#linearGradient7431" gradientUnits="userSpaceOnUse" gradientTransform="matrix(4.80814, 0, 0, 2.87475, -1569.44, -758.786)" spreadMethod="pad"/>
<linearGradient x1="68.374298" y1="-410.099" x2="67.912201" y2="-478.508" id="linearGradient4817" xlink:href="#linearGradient11367" gradientUnits="userSpaceOnUse" gradientTransform="matrix(1.58223, 0, 0, -0.727268, 275.522, -213.417)" spreadMethod="pad"/>
<linearGradient x1="436.48801" y1="-278.91299" x2="436.51199" y2="-299.88699" id="linearGradient4815" xlink:href="#linearGradient11377" gradientUnits="userSpaceOnUse" gradientTransform="matrix(4.59378, 0, 0, 0.359494, -1920.95, 434.897)" spreadMethod="pad"/>
<linearGradient id="linearGradient11377">
<stop id="stop11379" style="stop-color: rgb(255, 255, 255); stop-opacity: 0;" offset="0"/>
<stop id="stop11385" style="stop-color: rgb(255, 255, 255); stop-opacity: 0.575472;" offset="1"/>
</linearGradient>
<linearGradient id="linearGradient11367">
<stop id="stop11369" style="stop-color: rgb(0, 0, 0); stop-opacity: 0;" offset="0"/>
<stop id="stop11371" style="stop-color: rgb(0, 0, 0); stop-opacity: 0.254717;" offset="0.72131097"/>
<stop id="stop18428" style="stop-color: rgb(0, 0, 0); stop-opacity: 0.12549;" offset="0.91000003"/>
<stop id="stop11375" style="stop-color: rgb(0, 0, 0); stop-opacity: 0;" offset="1"/>
</linearGradient>
<linearGradient id="linearGradient7431">
<stop id="stop7433" style="stop-color: rgb(255, 255, 255); stop-opacity: 0;" offset="0"/>
<stop id="stop7439" style="stop-color: rgb(255, 255, 255); stop-opacity: 0.858491;" offset="0.72000003"/>
<stop id="stop8224" style="stop-color: rgb(255, 255, 255); stop-opacity: 0.707547;" offset="0.89999998"/>
<stop id="stop7435" style="stop-color: rgb(255, 255, 255); stop-opacity: 0.320755;" offset="1"/>
</linearGradient>
<filter id="filter3659">
<feGaussianBlur inkscape:collect="always" stdDeviation="0.25192676" id="feGaussianBlur3661"/>
</filter>
</defs>
<g id="layer1">
<g transform="matrix(1.1475, 0, 0, 1.1475, -368.661, -33.5075)" id="g4500">
<path d="M 326.964,34.4298 L 423.481,34.4298 C 437.856,66.0223 403.767,104.222 423.481,135.402 L 326.964,135.402 L 326.964,34.4298 z" id="path21978" style="fill: rgb(95, 123, 141); fill-opacity: 1; fill-rule: evenodd; stroke: none; stroke-width: 1.25; stroke-linecap: round; stroke-linejoin: round; stroke-miterlimit: 4; stroke-opacity: 1;"/>
<path d="M 326.964,34.4298 L 353.143,34.4298 C 367.518,66.0223 333.429,104.222 353.143,135.402 L 326.964,135.402 L 326.964,34.4298 z" id="path21980" style="fill: rgb(29, 70, 89); fill-opacity: 1; fill-rule: evenodd; stroke: none; stroke-width: 1.25; stroke-linecap: round; stroke-linejoin: round; stroke-miterlimit: 4; stroke-opacity: 1;"/>
<rect width="101.089" height="7.9108801" x="34.429798" y="326.96399" transform="matrix(0, 1, 1, 0, 0, 0)" id="rect21982" style="fill: url(&quot;#linearGradient4815&quot;) rgb(0, 0, 0); fill-opacity: 1; fill-rule: nonzero; stroke: none; stroke-width: 1.25; stroke-linecap: round; stroke-linejoin: round; stroke-miterlimit: 4; stroke-opacity: 1;"/>
<g transform="matrix(3.17412, 0, 0, 3.17412, 1038.99, -354.131)" id="g21984">
<path d="M -218.445,122.416 C -213.917,132.369 -224.656,144.405 -218.445,154.228 L -216.32,154.228 C -222.531,144.405 -211.792,132.369 -216.32,122.416 L -218.445,122.416 z" id="path21986" style="fill: rgb(0, 0, 0); fill-opacity: 0.0798122; fill-rule: nonzero; stroke: none; stroke-width: 1.25; stroke-linecap: round; stroke-linejoin: round; stroke-miterlimit: 4; stroke-opacity: 1;"/>
<path d="M -217.955,122.416 C -213.426,132.369 -224.166,144.405 -217.955,154.228 L -216.32,154.228 C -222.531,144.405 -211.792,132.369 -216.32,122.416 L -217.955,122.416 z" id="path21988" style="fill: rgb(0, 0, 0); fill-opacity: 0.131455; fill-rule: nonzero; stroke: none; stroke-width: 1.25; stroke-linecap: round; stroke-linejoin: round; stroke-miterlimit: 4; stroke-opacity: 1;"/>
<path d="M -217.403,122.416 C -212.875,132.369 -223.614,144.405 -217.403,154.228 L -216.32,154.228 C -222.531,144.405 -211.792,132.369 -216.32,122.416 L -217.403,122.416 z" id="path21990" style="fill: rgb(0, 0, 0); fill-opacity: 0.197183; fill-rule: nonzero; stroke: none; stroke-width: 1.25; stroke-linecap: round; stroke-linejoin: round; stroke-miterlimit: 4; stroke-opacity: 1;"/>
<path d="M -216.852,122.416 C -212.323,132.369 -223.063,144.405 -216.852,154.228 L -216.32,154.228 C -222.531,144.405 -211.792,132.369 -216.32,122.416 L -216.852,122.416 z" id="path21992" style="fill: rgb(0, 0, 0); fill-opacity: 0.267606; fill-rule: nonzero; stroke: none; stroke-width: 1.25; stroke-linecap: round; stroke-linejoin: round; stroke-miterlimit: 4; stroke-opacity: 1;"/>
</g>
<path d="M 326.964,135.402 L 422.488,135.402 C 412.274,118.171 416.819,101.345 421.306,83.5374 L 326.964,83.2034 L 326.964,135.402 z" id="path21994" style="fill: url(&quot;#linearGradient4817&quot;) rgb(0, 0, 0); fill-opacity: 1; fill-rule: nonzero; stroke: none; stroke-width: 1.25; stroke-linecap: round; stroke-linejoin: round; stroke-miterlimit: 4; stroke-opacity: 1;"/>
<g transform="matrix(2.41517, 0, 0, 2.41517, 876.456, -197.189)" id="g21996">
<path d="M 602.125,190.59375 C 599.45874,190.67075 596.74504,191.16798 594.53125,192.78125 C 591.5146,192.34561 588.3664,192.55749 585.5,193.6875 C 583.62824,194.43267 582.15635,195.77855 580.96875,197.34375 C 580.95544,197.36301 580.94492,197.38405 580.9375,197.40625 C 580.92091,197.43509 580.91029,197.46697 580.90625,197.5 C 580.91029,197.53303 580.92091,197.56491 580.9375,197.59375 C 580.94492,197.61595 580.95544,197.63699 580.96875,197.65625 C 580.97822,197.66757 580.98868,197.67803 581,197.6875 C 581.02605,197.71524 581.05813,197.73662 581.09375,197.75 C 581.12472,197.75595 581.15653,197.75595 581.1875,197.75 C 581.20825,197.75263 581.22925,197.75263 581.25,197.75 C 584.80749,196.49944 588.39295,195.15225 592.15625,195.5 C 593.28385,195.58867 594.35616,196.00271 595.46875,196.375 C 595.50974,196.38565 595.55276,196.38565 595.59375,196.375 C 595.62678,196.37096 595.65866,196.36034 595.6875,196.34375 C 595.7097,196.33633 595.73074,196.32581 595.75,196.3125 C 598.71379,193.45164 603.00891,192.72955 606.96875,191.90625 C 606.98007,191.89678 606.99053,191.88632 607,191.875 C 607.19563,191.80037 607.32956,191.73576 607.4375,191.625 C 607.49147,191.56962 607.55414,191.50784 607.5625,191.40625 C 607.57086,191.30466 607.51945,191.21518 607.46875,191.15625 C 607.36735,191.03839 607.25573,190.98239 607.125,190.9375 C 606.99427,190.89261 606.8215,190.87546 606.65625,190.84375 C 605.99526,190.71692 605.12704,190.6454 604.8125,190.625 C 604.80209,190.62434 604.79166,190.62434 604.78125,190.625 C 603.91011,190.58739 603.02603,190.56773 602.125,190.59375 z" transform="translate(-806.724, -92.8004)" id="path21998" style="fill: rgb(0, 0, 0); fill-opacity: 1; fill-rule: evenodd; stroke: none; stroke-width: 1pt; stroke-linecap: butt; stroke-linejoin: miter; stroke-opacity: 1;"/>
<path d="M -224.344,103.835 C -219.295,101.9 -214.705,101.331 -211.263,102.86 C -208.45,100.119 -202.237,98.6242 -200.227,98.6199 C -207.528,97.8352 -210.552,99.4967 -212,100.582 C -216.698,100.015 -221.096,100.522 -224.344,103.834" id="path22000" style="fill: rgb(255, 255, 255); fill-opacity: 1; stroke: none;"/>
</g>
<g transform="matrix(2.41517, 0, 0, 2.41517, 876.456, -197.189)" id="g22002">
<path d="M 596.25,27.53125 C 587.9033,27.701471 579.93436,30.011449 573.84375,35.03125 C 565.49276,31.223728 554.44432,30.751141 544.375,32.28125 C 538.97209,33.102263 533.8987,34.480363 529.6875,36.34375 C 525.4884,38.201779 521.99675,40.484175 520.1875,43.8125 C 519.57732,44.883163 519.7128,46.206199 520.46875,47.15625 C 521.22471,48.106278 522.5049,48.470375 523.65625,48.125 C 544.63433,42.131263 561.86554,43.038041 573.6875,52.875 C 574.80806,53.806374 576.46471,53.801852 577.5625,52.84375 C 587.80668,43.812696 604.05857,37.910216 621.5625,38.875 C 622.98852,38.943575 624.2874,37.997938 624.625,36.625 C 624.96264,35.252044 624.25302,33.783013 622.96875,33.1875 C 614.73544,29.461107 605.28688,27.346954 596.25,27.53125 z" transform="matrix(0.292292, -0.0677077, 0.0677077, 0.292292, -381.543, 134.276)" id="path22004" style="fill: rgb(0, 0, 0); fill-opacity: 1; fill-rule: evenodd; stroke: none; stroke-width: 1pt; stroke-linecap: butt; stroke-linejoin: miter; stroke-opacity: 1;"/>
<path d="M -225.696,112.15 C -219.345,108.564 -214.201,107.915 -209.842,110.097 C -206.945,105.454 -199.625,102.766 -197.16,102.691 C -204.796,101.053 -210.086,104.587 -211.05,106.575 C -215.328,105.394 -224.104,108.305 -225.696,112.149" id="path22006" style="fill: rgb(255, 255, 255); fill-opacity: 1; stroke: none;"/>
</g>
<g transform="matrix(1.14159, 0, 0, 1.14159, 265.142, -259.674)" id="g22010">
<path d="M 134.221,257.626 C 146.813,285.3 116.952,318.766 134.221,346.078 L 138.68,346.078 C 121.411,318.766 151.272,285.3 138.68,257.626 L 134.221,257.626 z" id="path22012" style="fill: rgb(0, 0, 0); fill-opacity: 0.0798122; fill-rule: nonzero; stroke: none; stroke-width: 1.25; stroke-linecap: round; stroke-linejoin: round; stroke-miterlimit: 4; stroke-opacity: 1;"/>
<path d="M 135.222,257.626 C 147.814,285.3 117.953,318.766 135.222,346.078 L 138.68,346.078 C 121.411,318.766 151.272,285.3 138.68,257.626 L 135.222,257.626 z" id="path22014" style="fill: rgb(0, 0, 0); fill-opacity: 0.131455; fill-rule: nonzero; stroke: none; stroke-width: 1.25; stroke-linecap: round; stroke-linejoin: round; stroke-miterlimit: 4; stroke-opacity: 1;"/>
<path d="M 136.393,257.626 C 148.985,285.3 119.124,318.766 136.393,346.078 L 138.68,346.078 C 121.411,318.766 151.272,285.3 138.68,257.626 L 136.393,257.626 z" id="path22016" style="fill: rgb(0, 0, 0); fill-opacity: 0.197183; fill-rule: nonzero; stroke: none; stroke-width: 1.25; stroke-linecap: round; stroke-linejoin: round; stroke-miterlimit: 4; stroke-opacity: 1;"/>
<path d="M 137.564,257.626 C 150.156,285.3 120.295,318.766 137.564,346.078 L 138.68,346.078 C 121.411,318.766 151.272,285.3 138.68,257.626 L 137.564,257.626 z" id="path22018" style="fill: rgb(0, 0, 0); fill-opacity: 0.267606; fill-rule: nonzero; stroke: none; stroke-width: 1.25; stroke-linecap: round; stroke-linejoin: round; stroke-miterlimit: 4; stroke-opacity: 1;"/>
<path d="M 133.134,257.626 C 145.726,285.3 115.865,318.766 133.134,346.078 L 138.68,346.078 C 121.411,318.766 151.272,285.3 138.68,257.626 L 133.134,257.626 z" id="path22020" style="fill: rgb(0, 0, 0); fill-opacity: 0.0798122; fill-rule: nonzero; stroke: none; stroke-width: 1.25; stroke-linecap: round; stroke-linejoin: round; stroke-miterlimit: 4; stroke-opacity: 1;"/>
</g>
<g transform="matrix(1.14159, 0, 0, 1.14159, -389.722, -484.947)" id="g22103">
<path d="M 653.161,498.44 L 693.751,498.44" id="path21604" style="fill: rgb(96, 123, 142); fill-opacity: 1; fill-rule: nonzero; stroke: rgb(255, 254, 255); stroke-width: 8; stroke-linecap: round; stroke-linejoin: round; stroke-miterlimit: 4; stroke-dashoffset: 0pt; stroke-opacity: 0.698039;"/>
<path d="M 653.161,515.294 L 683.452,515.294" id="path21606" style="fill: rgb(96, 123, 142); fill-opacity: 1; fill-rule: nonzero; stroke: rgb(255, 254, 255); stroke-width: 8; stroke-linecap: round; stroke-linejoin: round; stroke-miterlimit: 4; stroke-dashoffset: 0pt; stroke-opacity: 0.698039;"/>
<path d="M 653.161,532.46 L 693.751,532.46" id="path22101" style="fill: rgb(96, 123, 142); fill-opacity: 1; fill-rule: nonzero; stroke: rgb(255, 254, 255); stroke-width: 8; stroke-linecap: round; stroke-linejoin: round; stroke-miterlimit: 4; stroke-dashoffset: 0pt; stroke-opacity: 0.698039;"/>
</g>
<path d="M 326.964,34.4298 L 423.285,34.4298 C 433.146,54.4709 420.531,82.4058 417.826,102.327 L 326.964,102.327 L 326.964,34.4298 z" id="path22008" style="fill: url(&quot;#linearGradient4819&quot;) rgb(0, 0, 0); fill-opacity: 1; fill-rule: nonzero; stroke: none; stroke-width: 1.25; stroke-linecap: round; stroke-linejoin: round; stroke-miterlimit: 4; stroke-opacity: 1;"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 12 KiB

File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 143 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 573 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1014 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 391 B

View File

@ -0,0 +1,60 @@
from calibre.web.feeds.news import BasicNewsRecipe
class DrawAndCook(BasicNewsRecipe):
title = 'DrawAndCook'
__author__ = 'Starson17'
description = 'Drawings of recipes!'
language = 'en'
publisher = 'Starson17'
category = 'news, food, recipes'
use_embedded_content= False
no_stylesheets = True
oldest_article = 24
remove_javascript = True
remove_empty_feeds = True
cover_url = 'http://farm5.static.flickr.com/4043/4471139063_4dafced67f_o.jpg'
max_articles_per_feed = 30
remove_attributes = ['style', 'font']
def parse_index(self):
feeds = []
for title, url in [
("They Draw and Cook", "http://www.theydrawandcook.com/")
]:
articles = self.make_links(url)
if articles:
feeds.append((title, articles))
print 'feeds are: ', feeds
return feeds
def make_links(self, url):
soup = self.index_to_soup(url)
title = ''
date = ''
current_articles = []
soup = self.index_to_soup(url)
recipes = soup.findAll('div', attrs={'class': 'date-outer'})
for recipe in recipes:
title = recipe.h3.a.string
page_url = recipe.h3.a['href']
current_articles.append({'title': title, 'url': page_url, 'description':'', 'date':date})
return current_articles
keep_only_tags = [dict(name='h3', attrs={'class':'post-title entry-title'})
,dict(name='div', attrs={'class':'post-body entry-content'})
]
remove_tags = [dict(name='div', attrs={'class':['separator']})
,dict(name='div', attrs={'class':['post-share-buttons']})
]
extra_css = '''
h1{font-family:Arial,Helvetica,sans-serif; font-weight:bold;font-size:large;}
h2{font-family:Arial,Helvetica,sans-serif; font-weight:normal;font-size:small;}
img {max-width:100%; min-width:100%;}
p{font-family:Arial,Helvetica,sans-serif;font-size:small;}
body{font-family:Helvetica,Arial,sans-serif;font-size:small;}
'''

View File

@ -6,30 +6,37 @@ class AssociatedPress(BasicNewsRecipe):
title = u'Associated Press' title = u'Associated Press'
description = 'Global news' description = 'Global news'
__author__ = 'Kovid Goyal' __author__ = 'Kovid Goyal and Sujata Raman'
use_embedded_content = False use_embedded_content = False
language = 'en' language = 'en'
no_stylesheets = True
max_articles_per_feed = 15 max_articles_per_feed = 15
html2lrf_options = ['--force-page-break-before-tag="chapter"'] html2lrf_options = ['--force-page-break-before-tag="chapter"']
preprocess_regexps = [ (re.compile(i[0], re.IGNORECASE | re.DOTALL), i[1]) for i in preprocess_regexps = [ (re.compile(i[0], re.IGNORECASE | re.DOTALL), i[1]) for i in
[ [
(r'<HEAD>.*?</HEAD>' , lambda match : '<HEAD></HEAD>'), (r'<span class="entry-content">', lambda match : '<div class="entry-content">'),
(r'<body class="apple-rss-no-unread-mode" onLoad="setup(null)">.*?<!-- start Entries -->', lambda match : '<body>'),
(r'<!-- end apple-rss-content-area -->.*?</body>', lambda match : '</body>'),
(r'<script.*?>.*?</script>', lambda match : ''),
(r'<body.*?>.*?<span class="headline">', lambda match : '<body><span class="headline"><chapter>'),
(r'<tr><td><div class="body">.*?<p class="ap-story-p">', lambda match : '<p class="ap-story-p">'),
(r'<p class="ap-story-p">', lambda match : '<p>'),
(r'Learn more about our <a href="http://apdigitalnews.com/privacy.html">Privacy Policy</a>.*?</body>', lambda match : '</body>'),
] ]
] ]
keep_only_tags = [ dict(name='div', attrs={'class':['body']}),
dict(name='div', attrs={'class':['entry-content']}),
]
remove_tags = [dict(name='table', attrs={'class':['ap-video-table','ap-htmlfragment-table','ap-htmltable-table']}),
dict(name='span', attrs={'class':['apCaption','tabletitle']}),
dict(name='td', attrs={'bgcolor':['#333333']}),
]
extra_css = '''
.headline{font-family:Verdana,Arial,Helvetica,sans-serif;font-weight:bold;}
.bline{color:#003366;}
body{font-family:Arial,Helvetica,sans-serif;}
'''
feeds = [ ('AP Headlines', 'http://hosted.ap.org/lineups/TOPHEADS-rss_2.0.xml?SITE=ORAST&SECTION=HOME'),
feeds = [
('AP Headlines', 'http://hosted.ap.org/lineups/TOPHEADS-rss_2.0.xml?SITE=ORAST&SECTION=HOME'),
('AP US News', 'http://hosted.ap.org/lineups/USHEADS-rss_2.0.xml?SITE=CAVIC&SECTION=HOME'), ('AP US News', 'http://hosted.ap.org/lineups/USHEADS-rss_2.0.xml?SITE=CAVIC&SECTION=HOME'),
('AP World News', 'http://hosted.ap.org/lineups/WORLDHEADS-rss_2.0.xml?SITE=SCAND&SECTION=HOME'), ('AP World News', 'http://hosted.ap.org/lineups/WORLDHEADS-rss_2.0.xml?SITE=SCAND&SECTION=HOME'),
('AP Political News', 'http://hosted.ap.org/lineups/POLITICSHEADS-rss_2.0.xml?SITE=ORMED&SECTION=HOME'), ('AP Political News', 'http://hosted.ap.org/lineups/POLITICSHEADS-rss_2.0.xml?SITE=ORMED&SECTION=HOME'),
@ -39,3 +46,4 @@ class AssociatedPress(BasicNewsRecipe):
('AP Science News', 'http://hosted.ap.org/lineups/SCIENCEHEADS-rss_2.0.xml?SITE=OHCIN&SECTION=HOME'), ('AP Science News', 'http://hosted.ap.org/lineups/SCIENCEHEADS-rss_2.0.xml?SITE=OHCIN&SECTION=HOME'),
('AP Strange News', 'http://hosted.ap.org/lineups/STRANGEHEADS-rss_2.0.xml?SITE=WCNC&SECTION=HOME'), ('AP Strange News', 'http://hosted.ap.org/lineups/STRANGEHEADS-rss_2.0.xml?SITE=WCNC&SECTION=HOME'),
] ]

View File

@ -0,0 +1,62 @@
__license__ = 'GPL v3'
__copyright__ = '2010, Darko Miletic <darko.miletic at gmail.com>'
'''
balkaninsight.com
'''
import re
from calibre.web.feeds.news import BasicNewsRecipe
class BalkanInsight(BasicNewsRecipe):
title = 'Balkan Insight'
__author__ = 'Darko Miletic'
description = 'Get exclusive news and in depth information on business, politics, events and lifestyle in the Balkans. Free and exclusive premium content.'
publisher = 'BalkanInsight.com'
category = 'news, politics, Balcans'
oldest_article = 2
max_articles_per_feed = 100
no_stylesheets = False
use_embedded_content = False
encoding = 'utf-8'
masthead_url = 'http://www.balkaninsight.com/templates/balkaninsight/images/aindex_02.jpg'
language = 'en'
publication_type = 'newsportal'
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{font-family: Arial,Verdana,Helvetica,sans1,sans-serif}
img{margin-bottom: 0.8em}
h1,h2,h3,h4{font-family: Times,Georgia,serif1,serif; color: #24569E}
.article-deck {color:#777777; font-size: small;}
.main_news_img{font-size: small} """
conversion_options = {
'comment' : description
, 'tags' : category
, 'publisher' : publisher
, 'language' : language
}
preprocess_regexps = [(re.compile(u'\u0110'), lambda match: u'\u00D0')]
keep_only_tags = [dict(name='div', attrs={'id':'article'})]
remove_tags = [
dict(name=['object','link','iframe'])
]
feeds = [
(u'Albania' , u'http://www.balkaninsight.com/?tpl=653&tpid=144' )
,(u'Bosnia' , u'http://www.balkaninsight.com/?tpl=653&tpid=145' )
,(u'Bulgaria' , u'http://www.balkaninsight.com/?tpl=653&tpid=146' )
,(u'Croatia' , u'http://www.balkaninsight.com/?tpl=653&tpid=147' )
,(u'Kosovo' , u'http://www.balkaninsight.com/?tpl=653&tpid=148' )
,(u'Macedonia' , u'http://www.balkaninsight.com/?tpl=653&tpid=149' )
,(u'Montenegro' , u'http://www.balkaninsight.com/?tpl=653&tpid=150' )
,(u'Romania' , u'http://www.balkaninsight.com/?tpl=653&tpid=151' )
,(u'Serbia' , u'http://www.balkaninsight.com/?tpl=653&tpid=152' )
]
def preprocess_html(self, soup):
for item in soup.findAll(style=True):
del item['style']
return self.adeify_images(soup)

View File

@ -3,14 +3,13 @@ __copyright__ = '2010, Darko Miletic <darko.miletic at gmail.com>'
''' '''
news.bbc.co.uk news.bbc.co.uk
''' '''
import re import re
from calibre.web.feeds.recipes import BasicNewsRecipe from calibre.web.feeds.recipes import BasicNewsRecipe
class BBC(BasicNewsRecipe): class BBC(BasicNewsRecipe):
title = 'The BBC' title = 'BBC News'
__author__ = 'Darko Miletic' __author__ = 'Darko Miletic, Starson17'
description = 'Global news and current affairs from the British Broadcasting Corporation' description = 'News from UK. '
oldest_article = 2 oldest_article = 2
max_articles_per_feed = 100 max_articles_per_feed = 100
no_stylesheets = True no_stylesheets = True
@ -23,7 +22,6 @@ class BBC(BasicNewsRecipe):
publication_type = 'newsportal' publication_type = 'newsportal'
extra_css = ' body{ font-family: Verdana,Helvetica,Arial,sans-serif } .introduction{font-weight: bold} .story-feature{display: block; padding: 0; border: 1px solid; width: 40%; font-size: small} .story-feature h2{text-align: center; text-transform: uppercase} ' extra_css = ' body{ font-family: Verdana,Helvetica,Arial,sans-serif } .introduction{font-weight: bold} .story-feature{display: block; padding: 0; border: 1px solid; width: 40%; font-size: small} .story-feature h2{text-align: center; text-transform: uppercase} '
preprocess_regexps = [(re.compile(r'<!--.*?-->', re.DOTALL), lambda m: '')] preprocess_regexps = [(re.compile(r'<!--.*?-->', re.DOTALL), lambda m: '')]
conversion_options = { conversion_options = {
'comments' : description 'comments' : description
,'tags' : category ,'tags' : category
@ -33,14 +31,15 @@ class BBC(BasicNewsRecipe):
} }
keep_only_tags = [ keep_only_tags = [
dict(attrs={'id' :['meta-information','story-body']}) dict(name='div', attrs={'class':['layout-block-a layout-block']})
,dict(attrs={'class':['mxb' ,'storybody' ]}) ,dict(attrs={'class':['story-body','storybody']})
] ]
remove_tags = [ remove_tags = [
dict(name=['object','link','table']) dict(name='div', attrs={'class':['story-feature related narrow', 'share-help', 'embedded-hyper', \
,dict(attrs={'class':['caption','caption full-width','story-actions','hidden','sharesb','audioInStoryC']}) 'story-feature wide ', 'story-feature narrow']})
] ]
remove_tags_after = dict(attrs={'class':'sharesb'})
remove_attributes = ['width','height'] remove_attributes = ['width','height']
feeds = [ feeds = [

View File

@ -8,7 +8,7 @@ from calibre.web.feeds.recipes import BasicNewsRecipe
class BBC(BasicNewsRecipe): class BBC(BasicNewsRecipe):
title = 'BBC News (fast)' title = 'BBC News (fast)'
__author__ = 'Darko Miletic' __author__ = 'Darko Miletic, Starson17'
description = 'News from UK. A much faster version that does not download pictures' description = 'News from UK. A much faster version that does not download pictures'
oldest_article = 2 oldest_article = 2
max_articles_per_feed = 100 max_articles_per_feed = 100
@ -31,14 +31,16 @@ class BBC(BasicNewsRecipe):
} }
keep_only_tags = [ keep_only_tags = [
dict(attrs={'id' :['meta-information','story-body']}) dict(name='div', attrs={'class':['layout-block-a layout-block']})
,dict(attrs={'class':['mxb' ,'storybody' ]}) ,dict(attrs={'class':['story-body','storybody']})
] ]
remove_tags = [ remove_tags = [
dict(name=['object','link','table','img']) dict(name='div', attrs={'class':['story-feature related narrow', 'share-help', 'embedded-hyper', \
,dict(attrs={'class':['caption','caption full-width','story-actions','hidden','sharesb','audioInStoryC']}) 'story-feature wide ', 'story-feature narrow']})
, dict(name=['img'])
] ]
remove_tags_after = dict(attrs={'class':'sharesb'})
remove_attributes = ['width','height'] remove_attributes = ['width','height']
feeds = [ feeds = [

View File

@ -0,0 +1,112 @@
import re
from datetime import date, timedelta
from calibre.web.feeds.recipes import BasicNewsRecipe
class MediaDaumRecipe(BasicNewsRecipe):
title = u'\uBBF8\uB514\uC5B4 \uB2E4\uC74C \uC624\uB298\uC758 \uC8FC\uC694 \uB274\uC2A4'
description = 'Articles from media.daum.net'
__author__ = 'trustin'
language = 'ko'
max_articles = 100
timefmt = ''
masthead_url = 'http://img-media.daum-img.net/2010ci/service_news.gif'
cover_margins = (18,18,'grey99')
no_stylesheets = True
remove_tags_before = dict(id='GS_con')
remove_tags_after = dict(id='GS_con')
remove_tags = [dict(attrs={'class':[
'bline',
'GS_vod',
]}),
dict(id=[
'GS_swf_poll',
'ad250',
]),
dict(name=['script', 'noscript', 'style', 'object'])]
preprocess_regexps = [
(re.compile(r'<\s+', re.DOTALL|re.IGNORECASE),
lambda match: '&lt; '),
(re.compile(r'(<br[^>]*>[ \t\r\n]*){3,}', re.DOTALL|re.IGNORECASE),
lambda match: ''),
(re.compile(r'(<br[^>]*>[ \t\r\n]*)*</div>', re.DOTALL|re.IGNORECASE),
lambda match: '</div>'),
(re.compile(r'(<br[^>]*>[ \t\r\n]*)*</p>', re.DOTALL|re.IGNORECASE),
lambda match: '</p>'),
(re.compile(r'(<br[^>]*>[ \t\r\n]*)*</td>', re.DOTALL|re.IGNORECASE),
lambda match: '</td>'),
(re.compile(r'(<br[^>]*>[ \t\r\n]*)*</strong>', re.DOTALL|re.IGNORECASE),
lambda match: '</strong>'),
(re.compile(r'(<br[^>]*>[ \t\r\n]*)*</b>', re.DOTALL|re.IGNORECASE),
lambda match: '</b>'),
(re.compile(r'(<br[^>]*>[ \t\r\n]*)*</em>', re.DOTALL|re.IGNORECASE),
lambda match: '</em>'),
(re.compile(r'(<br[^>]*>[ \t\r\n]*)*</i>', re.DOTALL|re.IGNORECASE),
lambda match: '</i>'),
(re.compile(u'\(\uB05D\)[ \t\r\n]*<br[^>]*>.*</div>', re.DOTALL|re.IGNORECASE),
lambda match: '</div>'),
(re.compile(r'(<br[^>]*>[ \t\r\n]*)*<div', re.DOTALL|re.IGNORECASE),
lambda match: '<div'),
(re.compile(r'(<br[^>]*>[ \t\r\n]*)*<p', re.DOTALL|re.IGNORECASE),
lambda match: '<p'),
(re.compile(r'(<br[^>]*>[ \t\r\n]*)*<table', re.DOTALL|re.IGNORECASE),
lambda match: '<table'),
(re.compile(r'<strong>(<br[^>]*>[ \t\r\n]*)*', re.DOTALL|re.IGNORECASE),
lambda match: '<strong>'),
(re.compile(r'<b>(<br[^>]*>[ \t\r\n]*)*', re.DOTALL|re.IGNORECASE),
lambda match: '<b>'),
(re.compile(r'<em>(<br[^>]*>[ \t\r\n]*)*', re.DOTALL|re.IGNORECASE),
lambda match: '<em>'),
(re.compile(r'<i>(<br[^>]*>[ \t\r\n]*)*', re.DOTALL|re.IGNORECASE),
lambda match: '<i>'),
(re.compile(u'(<br[^>]*>[ \t\r\n]*)*(\u25B6|\u25CF|\u261E|\u24D2|\(c\))*\[[^\]]*(\u24D2|\(c\)|\uAE30\uC0AC|\uC778\uAE30[^\]]*\uB274\uC2A4)[^\]]*\].*</div>', re.DOTALL|re.IGNORECASE),
lambda match: '</div>'),
]
def parse_index(self):
today = date.today();
articles = []
articles = self.parse_list_page(articles, today)
articles = self.parse_list_page(articles, today - timedelta(1))
return [('\uBBF8\uB514\uC5B4 \uB2E4\uC74C \uC624\uB298\uC758 \uC8FC\uC694 \uB274\uC2A4', articles)]
def parse_list_page(self, articles, date):
if len(articles) >= self.max_articles:
return articles
for page in range(1, 10):
soup = self.index_to_soup('http://media.daum.net/primary/total/list.html?cateid=100044&date=%(date)s&page=%(page)d' % {'date': date.strftime('%Y%m%d'), 'page': page})
done = True
for item in soup.findAll('dl'):
dt = item.find('dt', { 'class': 'tit' })
dd = item.find('dd', { 'class': 'txt' })
if dt is None:
break
a = dt.find('a', href=True)
url = 'http://media.daum.net/primary/total/' + a['href']
title = self.tag_to_string(dt)
if dd is None:
description = ''
else:
description = self.tag_to_string(dd)
articles.append(dict(title=title, description=description, url=url, content=''))
done = len(articles) >= self.max_articles
if done:
break
if done:
break
return articles
def preprocess_html(self, soup):
return self.strip_anchors(soup)
def strip_anchors(self, soup):
for para in soup.findAll(True):
aTags = para.findAll('a')
for a in aTags:
if a.img is None:
a.replaceWith(a.renderContents().decode('utf-8','replace'))
return soup

View File

@ -0,0 +1,42 @@
__license__ = 'GPL v3'
__copyright__ = '2010, Darko Miletic <darko.miletic at gmail.com>'
'''
dr.dk
'''
from calibre.web.feeds.news import BasicNewsRecipe
class dr_dk(BasicNewsRecipe):
title = 'DR Nyheder'
__author__ = 'Darko Miletic'
description = 'Myndighederne indfører nu eskorte af brandbiler og ambulancer i Ishøj af frygt for hærværk.'
publisher = 'Nyhedsbureauet DR Nyheder'
category = 'news, politics, Denmark'
oldest_article = 2
max_articles_per_feed = 200
no_stylesheets = True
delay = 1
encoding = 'utf8'
use_embedded_content = False
language = 'da'
extra_css = """ body{font-family: Verdana,Arial,sans-serif }
img{margin-bottom: 0.4em}
.txtContent,.stamp{font-size: small}
"""
conversion_options = {
'comment' : description
, 'tags' : category
, 'publisher' : publisher
, 'language' : language
}
keep_only_tags = [dict(name='div', attrs={'class':'articleContent'})]
remove_attributes=['xmlns:msxsl','width','height']
feeds = [(u'All news', u'http://www.dr.dk/Nyheder/Service/feeds/Allenyheder.htm')]
def preprocess_html(self, soup):
for item in soup.findAll(style=True):
del item['style']
return soup

View File

@ -0,0 +1,74 @@
#!/usr/bin/env python
__license__ = 'GPL v3'
__copyright__ = '2010, Saverio Palmieri Neto <saverio.palmieri at gmail.com>'
'''
folha.uol.com.br
'''
from calibre.web.feeds.news import BasicNewsRecipe
class FolhaOnline(BasicNewsRecipe):
title = 'Folha de Sao Paulo'
__author__ = 'Saverio Palmieri Neto'
description = 'Brazilian news from Folha de Sao Paulo Online'
publisher = 'Folha de Sao Paulo'
category = 'Brasil, news'
oldest_article = 2
max_articles_per_feed = 1000
summary_length = 2048
no_stylesheets = True
use_embedded_content = False
timefmt = ' [%d %b %Y (%a)]'
encoding = 'cp1252'
cover_url = 'http://lh5.ggpht.com/_hEb7sFmuBvk/TFoiKLRS5dI/AAAAAAAAADM/kcVKggZwKnw/capa_folha.jpg'
cover_margins = (5,5,'white')
remove_javascript = True
keep_only_tags = [dict(name='div', attrs={'id':'articleNew'})]
remove_tags = [
dict(name='script')
,dict(name='div',
attrs={'id':[
'articleButton'
,'bookmarklets'
,'ad-180x150-1'
,'contextualAdsArticle'
,'articleEnd'
,'articleComments'
]})
,dict(name='div',
attrs={'class':[
'openBox adslibraryArticle'
]})
,dict(name='a')
,dict(name='iframe')
,dict(name='link')
]
feeds = [
(u'Em cima da hora', u'http://feeds.folha.uol.com.br/emcimadahora/rss091.xml')
,(u'Ambiente', u'http://feeds.folha.uol.com.br/ambiente/rss091.xml')
,(u'Bichos', u'http://feeds.folha.uol.com.br/bichos/rss091.xml')
,(u'Poder', u'http://feeds.folha.uol.com.br/poder/rss091.xml')
,(u'Ciencia', u'http://feeds.folha.uol.com.br/ciencia/rss091.xml')
,(u'Cotidiano', u'http://feeds.folha.uol.com.br/cotidiado/rss091.xml')
,(u'Saber', u'http://feeds.folha.uol.com.br/saber/rss091.xml')
,(u'Equilíbrio e Saúde', u'http://feeds.folha.uol.com.br/equilibrioesaude/rss091.xml')
,(u'Esporte', u'http://feeds.folha.uol.com.br/esporte/rss091.xml')
,(u'Ilustrada', u'http://feeds.folha.uol.com.br/ilustrada/rss091.xml')
,(u'Ilustríssima', u'http://feeds.folha.uol.com.br/ilustrissima/rss091.xml')
,(u'Mercado', u'http://feeds.folha.uol.com.br/mercado/rss091.xml')
,(u'Mundo', u'http://feeds.folha.uol.com.br/mundo/rss091.xml')
,(u'Tec', u'http://feeds.folha.uol.com.br/tec/rss091.xml')
,(u'Turismo', u'http://feeds.folha.uol.com.br/turismo/rss091.xml')
]
def preprocess_html(self, soup):
for item in soup.findAll(style=True):
del item['style']
return soup
language = 'pt'

View File

@ -4,28 +4,23 @@ import re
class NatureNews(BasicNewsRecipe): class NatureNews(BasicNewsRecipe):
title = u'Nature News' title = u'Nature News'
language = 'en' language = 'en'
__author__ = 'Krittika Goyal' __author__ = 'Krittika Goyal, Starson17'
oldest_article = 31 #days oldest_article = 31 #days
remove_empty_feeds = True
max_articles_per_feed = 50 max_articles_per_feed = 50
#encoding = 'latin1'
no_stylesheets = True no_stylesheets = True
remove_tags_before = dict(name='h1', attrs={'class':'heading entry-title'}) remove_tags_before = dict(name='h1', attrs={'class':'heading entry-title'})
remove_tags_after = dict(name='h2', attrs={'id':'comments'}) remove_tags_after = dict(name='h2', attrs={'id':'comments'})
remove_tags = [ remove_tags = [
#dict(name='iframe'),
#dict(name='div', attrs={'class':['pt-box-title', 'pt-box-content']}),
#dict(name='div', attrs={'id':['block-td_search_160', 'block-cam_search_160']}),
dict(name='h2', attrs={'id':'comments'}), dict(name='h2', attrs={'id':'comments'}),
dict(name='ul', attrs={'class':'toolsmenu xoxo'}), dict(attrs={'alt':'Advertisement'}),
dict(name='div', attrs={'class':'ad'}),
] ]
preprocess_regexps = [ preprocess_regexps = [
(re.compile(r'<script.*?</script>', re.DOTALL), lambda m: '') (re.compile(r'<p>ADVERTISEMENT</p>', re.DOTALL|re.IGNORECASE), lambda match: ''),
] ]
feeds = [('Nature News', 'http://feeds.nature.com/news/rss/most_recent')] feeds = [('Nature News', 'http://feeds.nature.com/news/rss/most_recent')]
def get_article_url(self, article):
return article.get('id')

View File

@ -19,6 +19,32 @@ class heiseDe(BasicNewsRecipe):
max_articles_per_feed = 40 max_articles_per_feed = 40
no_stylesheets = True no_stylesheets = True
extra_css = '''
.bild_links, .bild_bu_links {
float:left;
line-height:105%;
margin:12px 1.4em 12px 0;
}
.bild_rechts, .bild_bu {
float:right;
line-height:105%;
margin:12px 0 12px 1em;
text-align:right;
}
.bild_zentriert {
clear:both;
line-height:105%;
margin:.2em auto;
text-align:center;
}
span.bild_links, span.bild_rechts, span.bild_zentriert {
display:block;
}
'''
remove_tags = [dict(id='navi_top'), remove_tags = [dict(id='navi_top'),
dict(id='navi_bottom'), dict(id='navi_bottom'),
dict(id='logo'), dict(id='logo'),

View File

@ -9,17 +9,22 @@ from calibre.web.feeds.news import BasicNewsRecipe
class Lanacion(BasicNewsRecipe): class Lanacion(BasicNewsRecipe):
title = 'La Nacion' title = 'La Nacion'
__author__ = 'Darko Miletic' __author__ = 'Darko Miletic'
description = 'Noticias de Argentina y el resto del mundo' description = "lanacion.com - Informacion actualizada las 24 horas, con noticias de Argentina y del mundo"
publisher = 'La Nacion S.A.' publisher = 'La Nacion S.A.'
category = 'news, politics, Argentina' category = 'news, politics, Argentina'
oldest_article = 2 oldest_article = 1
max_articles_per_feed = 100 max_articles_per_feed = 100
use_embedded_content = False use_embedded_content = False
no_stylesheets = True no_stylesheets = True
language = 'es' language = 'es'
encoding = 'cp1252' publication_type = 'newspaper'
remove_empty_feeds = True
masthead_url = 'http://www.lanacion.com.ar/imgs/layout/logos/ln341x47.gif' masthead_url = 'http://www.lanacion.com.ar/imgs/layout/logos/ln341x47.gif'
extra_css = ' h1{font-family: Georgia,serif} body{font-family: Arial,sans-serif} img{margin-top: 0.5em; margin-bottom: 0.2em} .notaEpigrafe{font-size: x-small} ' extra_css = """ h1{font-family: Georgia,serif}
body{font-family: Arial,sans-serif}
img{margin-top: 0.5em; margin-bottom: 0.2em}
.notaEpigrafe{font-size: x-small}
.topNota h1{font-family: Arial,sans-serif} """
conversion_options = { conversion_options = {
@ -29,19 +34,19 @@ class Lanacion(BasicNewsRecipe):
, 'language' : language , 'language' : language
} }
keep_only_tags = [dict(name='div', attrs={'class':'nota floatFix'})] keep_only_tags = [dict(name='div', attrs={'class':['nota floatFix','topNota','nota','post']})]
remove_tags = [ remove_tags = [
dict(name='div' , attrs={'class':'notaComentario floatFix noprint' }) dict(name='div' , attrs={'class':'notaComentario floatFix noprint' })
,dict(name='ul' , attrs={'class':'cajaHerramientas cajaTop noprint'}) ,dict(name='ul' , attrs={'class':['cajaHerramientas cajaTop noprint','herramientas noprint']})
,dict(name='div' , attrs={'class':'cajaHerramientas noprint' }) ,dict(name='div' , attrs={'class':'cajaHerramientas noprint' })
,dict(attrs={'class':['titulosMultimedia','derecha','techo color']}) ,dict(attrs={'class':['titulosMultimedia','derecha','techo color','encuesta','izquierda compartir','floatFix']})
,dict(name=['iframe','embed','object']) ,dict(name=['iframe','embed','object','form','base','hr'])
] ]
remove_attributes = ['height','width'] remove_tags_after = dict(attrs={'class':['tags','nota-destacado']})
remove_attributes = ['height','width','visible']
feeds = [ feeds = [
(u'Ultimas noticias' , u'http://www.lanacion.com.ar/herramientas/rss/index.asp?origen=2' ) (u'Ultimas noticias' , u'http://www.lanacion.com.ar/herramientas/rss/index.asp?origen=2' )
,(u'Diario de hoy' , u'http://www.lanacion.com.ar/herramientas/rss/index.asp?origen=1' )
,(u'Politica' , u'http://www.lanacion.com.ar/herramientas/rss/index.asp?categoria_id=30' ) ,(u'Politica' , u'http://www.lanacion.com.ar/herramientas/rss/index.asp?categoria_id=30' )
,(u'Economia' , u'http://www.lanacion.com.ar/herramientas/rss/index.asp?categoria_id=272' ) ,(u'Economia' , u'http://www.lanacion.com.ar/herramientas/rss/index.asp?categoria_id=272' )
,(u'Deportes' , u'http://www.lanacion.com.ar/herramientas/rss/index.asp?categoria_id=131' ) ,(u'Deportes' , u'http://www.lanacion.com.ar/herramientas/rss/index.asp?categoria_id=131' )
@ -50,8 +55,23 @@ class Lanacion(BasicNewsRecipe):
,(u'Opinion' , u'http://www.lanacion.com.ar/herramientas/rss/index.asp?categoria_id=28' ) ,(u'Opinion' , u'http://www.lanacion.com.ar/herramientas/rss/index.asp?categoria_id=28' )
,(u'Espectaculos' , u'http://www.lanacion.com.ar/herramientas/rss/index.asp?categoria_id=120' ) ,(u'Espectaculos' , u'http://www.lanacion.com.ar/herramientas/rss/index.asp?categoria_id=120' )
,(u'Exterior' , u'http://www.lanacion.com.ar/herramientas/rss/index.asp?categoria_id=7' ) ,(u'Exterior' , u'http://www.lanacion.com.ar/herramientas/rss/index.asp?categoria_id=7' )
,(u'Ciencia/Salud' , u'http://www.lanacion.com.ar/herramientas/rss/index.asp?categoria_id=498' ) ,(u'Ciencia&Salud' , u'http://www.lanacion.com.ar/herramientas/rss/index.asp?categoria_id=498' )
,(u'Revista' , u'http://www.lanacion.com.ar/herramientas/rss/index.asp?categoria_id=494' ) ,(u'Revista' , u'http://www.lanacion.com.ar/herramientas/rss/index.asp?categoria_id=494' )
,(u'Enfoques' , u'http://www.lanacion.com.ar/herramientas/rss/index.asp?categoria_id=421' )
,(u'Comercio Exterior' , u'http://www.lanacion.com.ar/herramientas/rss/index.asp?categoria_id=347' )
,(u'Tecnologia' , u'http://www.lanacion.com.ar/herramientas/rss/index.asp?categoria_id=432' )
,(u'Arquitectura' , u'http://www.lanacion.com.ar/herramientas/rss/index.asp?categoria_id=366' )
,(u'Turismo' , u'http://www.lanacion.com.ar/herramientas/rss/index.asp?categoria_id=504' )
,(u'Al volante' , u'http://www.lanacion.com.ar/herramientas/rss/index.asp?categoria_id=371' )
,(u'El Campo' , u'http://www.lanacion.com.ar/herramientas/rss/index.asp?categoria_id=337' )
,(u'Moda y Belleza' , u'http://www.lanacion.com.ar/herramientas/rss/index.asp?categoria_id=1312' )
,(u'Inmuebles Comerciales', u'http://www.lanacion.com.ar/herramientas/rss/index.asp?categoria_id=1363' )
,(u'Countries' , u'http://www.lanacion.com.ar/herramientas/rss/index.asp?categoria_id=1348' )
,(u'adnCultura' , u'http://www.lanacion.com.ar/herramientas/rss/index.asp?categoria_id=6734' )
,(u'The Wall Street Journal Americas', u'http://www.lanacion.com.ar/herramientas/rss/index.asp?categoria_id=6373' )
,(u'Estilo de vida' , u'http://www.lanacion.com.ar/herramientas/rss/index.asp?categoria_id=7353' )
,(u'Management' , u'http://www.lanacion.com.ar/herramientas/rss/index.asp?categoria_id=7380' )
,(u'Bicentenario' , u'http://www.lanacion.com.ar/herramientas/rss/index.asp?categoria_id=7276' )
] ]
def preprocess_html(self, soup): def preprocess_html(self, soup):

View File

@ -6,10 +6,9 @@ www.standardmedia.co.ke
import os import os
from calibre import strftime, __appname__, __version__ from calibre import strftime, __appname__, __version__
import calibre.utils.PythonMagickWand as pw
from ctypes import byref
from calibre.web.feeds.news import BasicNewsRecipe from calibre.web.feeds.news import BasicNewsRecipe
from calibre.constants import preferred_encoding from calibre.constants import preferred_encoding
from calibre.utils.magick import Image
class NationKeRecipe(BasicNewsRecipe): class NationKeRecipe(BasicNewsRecipe):
@ -95,19 +94,9 @@ class NationKeRecipe(BasicNewsRecipe):
self.cover_img_path = None self.cover_img_path = None
def prepare_cover_image(self, path_to_image, out_path): def prepare_cover_image(self, path_to_image, out_path):
with pw.ImageMagick(): img = Image()
img = pw.NewMagickWand() img.open(path_to_image)
if img < 0: img.save(out_path)
raise RuntimeError('Out of memory')
if not pw.MagickReadImage(img, path_to_image):
severity = pw.ExceptionType(0)
msg = pw.MagickGetException(img, byref(severity))
raise IOError('Failed to read image from: %s: %s'
%(path_to_image, msg))
if not pw.MagickWriteImage(img, out_path):
raise RuntimeError('Failed to save image to %s'%out_path)
pw.DestroyMagickWand(img)
def default_cover(self, cover_file): def default_cover(self, cover_file):
''' '''

View File

@ -1,4 +1,3 @@
__license__ = 'GPL v3' __license__ = 'GPL v3'
__copyright__ = '2008-2010, AprilHare, Darko Miletic <darko.miletic at gmail.com>' __copyright__ = '2008-2010, AprilHare, Darko Miletic <darko.miletic at gmail.com>'
''' '''
@ -36,7 +35,7 @@ class NewScientist(BasicNewsRecipe):
remove_tags = [ remove_tags = [
dict(name='div' , attrs={'class':['hldBd','adline','pnl','infotext' ]}) dict(name='div' , attrs={'class':['hldBd','adline','pnl','infotext' ]})
,dict(name='div' , attrs={'id' :['compnl','artIssueInfo','artTools','comments','blgsocial']}) ,dict(name='div' , attrs={'id' :['compnl','artIssueInfo','artTools','comments','blgsocial','sharebtns']})
,dict(name='p' , attrs={'class':['marker','infotext' ]}) ,dict(name='p' , attrs={'class':['marker','infotext' ]})
,dict(name='meta' , attrs={'name' :'description' }) ,dict(name='meta' , attrs={'name' :'description' })
,dict(name='a' , attrs={'rel' :'tag' }) ,dict(name='a' , attrs={'rel' :'tag' })

View File

@ -11,7 +11,7 @@ from calibre.web.feeds.news import BasicNewsRecipe
class Novosti(BasicNewsRecipe): class Novosti(BasicNewsRecipe):
title = 'Vecernje Novosti' title = 'Vecernje Novosti'
__author__ = 'Darko Miletic' __author__ = 'Darko Miletic'
description = 'Vesti' description = 'U početku su bile istinske večernje novine - pokrenute u vreme Tršćanske krize, Italijansko-jugoslovenskog konflikta oko grada Trsta - ali su brzo izrasle u dnevni informativno-politički list, koji već godinama ima najveći tiraž u Srbiji.'
publisher = 'Kompanija Novosti' publisher = 'Kompanija Novosti'
category = 'news, politics, Serbia' category = 'news, politics, Serbia'
oldest_article = 2 oldest_article = 2
@ -21,24 +21,22 @@ class Novosti(BasicNewsRecipe):
encoding = 'utf-8' encoding = 'utf-8'
language = 'sr' language = 'sr'
publication_type = 'newspaper' publication_type = 'newspaper'
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{font-family: Tahoma,Arial,Helvetica,sans1,sans-serif} ' extra_css = """ @font-face {font-family: "sans1";src:url(res:///opt/sony/ebook/FONT/tt0003m_.ttf)}
.article_description,body{font-family: Arial,Helvetica,sans1,sans-serif}
.author{font-size: small}
.articleLead{font-size: large; font-weight: bold}
"""
conversion_options = { conversion_options = {
'comment' : description 'comment' : description
, 'tags' : category , 'tags' : category
, 'publisher' : publisher , 'publisher' : publisher
, 'language' : language , 'language' : language
, 'linearize_tables' : True
} }
preprocess_regexps = [(re.compile(u'\u0110'), lambda match: u'\u00D0')] preprocess_regexps = [(re.compile(u'\u0110'), lambda match: u'\u00D0')]
keep_only_tags = [dict(name='div', attrs={'class':'jednaVest'})] keep_only_tags = [dict(attrs={'class':['articleTitle','author','articleLead','articleBody']})]
remove_tags = [dict(name='div', attrs={'class':['info','info_bottom','clip_div']})] remove_tags = [dict(name=['embed','object','iframe','base'])]
feeds = [(u'Vesti', u'http://www.novosti.rs/php/vesti/rss.php')] feeds = [(u'Vesti', u'http://www.novosti.rs/rss/rss-vesti')]
def preprocess_html(self, soup):
for item in soup.findAll(style=True):
del item['style']
return self.adeify_images(soup)

View File

@ -6,6 +6,7 @@ class AdvancedUserRecipe1257302745(BasicNewsRecipe):
language = 'en' language = 'en'
__author__ = 'onyxrev' __author__ = 'onyxrev'
max_articles_per_feed = 100 max_articles_per_feed = 100
no_stylesheets = True
remove_tags_before = {'class':'storytitle'} remove_tags_before = {'class':'storytitle'}
remove_tags_after = dict(name='div', attrs={'id':'storytext' }) remove_tags_after = dict(name='div', attrs={'id':'storytext' })

View File

@ -14,33 +14,39 @@ class Nspm(BasicNewsRecipe):
description = 'Casopis za politicku teoriju i drustvena istrazivanja' description = 'Casopis za politicku teoriju i drustvena istrazivanja'
publisher = 'NSPM' publisher = 'NSPM'
category = 'news, politics, Serbia' category = 'news, politics, Serbia'
oldest_article = 2 oldest_article = 7
max_articles_per_feed = 100 max_articles_per_feed = 100
no_stylesheets = True no_stylesheets = True
use_embedded_content = False use_embedded_content = False
INDEX = 'http://www.nspm.rs/?alphabet=l' INDEX = 'http://www.nspm.rs/?alphabet=l'
encoding = 'utf-8' encoding = 'utf-8'
language = 'sr' language = 'sr'
delay = 2
publication_type = 'magazine' publication_type = 'magazine'
masthead_url = 'http://www.nspm.rs/templates/jsn_epic_pro/images/logol.jpg' masthead_url = 'http://www.nspm.rs/templates/jsn_epic_pro/images/logol.jpg'
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)} body{font-family: "Times New Roman", serif1, serif} .article_description{font-family: Arial, sans1, sans-serif} img{margin-top:0.5em; margin-bottom: 0.7em} .author{color: #990000; font-weight: bold} .author,.createdate{font-size: 0.9em} img{margin-top:0.5em; margin-bottom: 0.7em} ' 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)}
body{font-family: "Times New Roman", serif1, serif}
.article_description{font-family: Arial, sans1, sans-serif}
img{margin-top:0.5em; margin-bottom: 0.7em}
.author{color: #990000; font-weight: bold}
.author,.createdate{font-size: 0.9em} """
conversion_options = { conversion_options = {
'comment' : description 'comment' : description
, 'tags' : category , 'tags' : category
, 'publisher' : publisher , 'publisher' : publisher
, 'language' : language , 'language' : language
, 'linearize_tables' : True
} }
preprocess_regexps = [(re.compile(u'\u0110'), lambda match: u'\u00D0')] preprocess_regexps = [(re.compile(u'\u0110'), lambda match: u'\u00D0')]
keep_only_tags = [dict(attrs={'id':'jsn-mainbody'})]
remove_tags = [ remove_tags = [
dict(name=['link','object','embed','script','meta']) dict(name=['link','object','embed','script','meta','base','iframe'])
,dict(name='td', attrs={'class':'buttonheading'}) ,dict(attrs={'class':'buttonheading'})
]
keep_only_tags = [
dict(attrs={'class':['contentpagetitle','author','createdate']})
,dict(name='p')
] ]
remove_tags_after = dict(attrs={'class':'article_separator'})
remove_attributes = ['width','height'] remove_attributes = ['width','height']
def get_browser(self): def get_browser(self):
@ -48,25 +54,18 @@ class Nspm(BasicNewsRecipe):
br.open(self.INDEX) br.open(self.INDEX)
return br return br
feeds = [(u'Nova srpska politicka misao', u'http://www.nspm.rs/feed/rss.html')] feeds = [
(u'Rubrike' , u'http://www.nspm.rs/rubrike/feed/rss.html')
def print_version(self, url): ,(u'Debate' , u'http://www.nspm.rs/debate/feed/rss.html')
return url.replace('.html','/stampa.html') ,(u'Reci i misli' , u'http://www.nspm.rs/reci-i-misli/feed/rss.html')
,(u'Samo smeh srbina spasava', u'http://www.nspm.rs/samo-smeh-srbina-spasava/feed/rss.html')
,(u'Polemike' , u'http://www.nspm.rs/polemike/feed/rss.html')
,(u'Prikazi' , u'http://www.nspm.rs/prikazi/feed/rss.html')
,(u'Prenosimo' , u'http://www.nspm.rs/prenosimo/feed/rss.html')
,(u'Hronika' , u'http://www.nspm.rs/tabela/hronika/feed/rss.html')
]
def preprocess_html(self, soup): def preprocess_html(self, soup):
for item in soup.body.findAll(style=True): for item in soup.body.findAll(style=True):
del item['style'] del item['style']
att = soup.find('a',attrs={'class':'contentpagetitle'}) return self.adeify_images(soup)
if att:
att.name = 'h1';
del att['href']
att2 = soup.find('td')
if att2:
att2.name = 'p';
del att['valign']
for pt in soup.findAll('img'):
brtag = Tag(soup,'br')
brtag2 = Tag(soup,'br')
pt.append(brtag)
pt.append(brtag2)
return soup

View File

@ -1,7 +1,5 @@
#!/usr/bin/env python
__license__ = 'GPL v3' __license__ = 'GPL v3'
__copyright__ = '2008, Darko Miletic <darko.miletic at gmail.com>' __copyright__ = '2008-2010, Darko Miletic <darko.miletic at gmail.com>'
''' '''
nspm.rs/nspm-in-english nspm.rs/nspm-in-english
''' '''
@ -12,28 +10,43 @@ class Nspm_int(BasicNewsRecipe):
title = 'NSPM in English' title = 'NSPM in English'
__author__ = 'Darko Miletic' __author__ = 'Darko Miletic'
description = 'Magazine dedicated to political theory and sociological research' description = 'Magazine dedicated to political theory and sociological research'
oldest_article = 20 publisher = 'NSPM'
category = 'news, politics, Serbia'
oldest_article = 7
max_articles_per_feed = 100 max_articles_per_feed = 100
language = 'en'
no_stylesheets = True no_stylesheets = True
use_embedded_content = False use_embedded_content = False
INDEX = 'http://www.nspm.rs/?alphabet=l' encoding = 'utf-8'
cover_url = 'http://nspm.rs/templates/jsn_epic_pro/images/logol.jpg' language = 'en'
html2lrf_options = [ delay = 2
'--comment', description publication_type = 'magazine'
, '--base-font-size', '10' masthead_url = 'http://www.nspm.rs/templates/jsn_epic_pro/images/logol.jpg'
, '--category', 'news, politics, Serbia, english' extra_css = """
, '--publisher', 'IIC NSPM' body{font-family: "Times New Roman", serif}
.article_description{font-family: Arial, sans-serif}
img{margin-top:0.5em; margin-bottom: 0.7em}
.author{color: #990000; font-weight: bold}
.author,.createdate{font-size: 0.9em} """
conversion_options = {
'comment' : description
, 'tags' : category
, 'publisher' : publisher
, 'language' : language
, 'linearize_tables' : True
}
keep_only_tags = [dict(attrs={'id':'jsn-mainbody'})]
remove_tags = [
dict(name=['link','object','embed','script','meta','base','iframe'])
,dict(attrs={'class':'buttonheading'})
] ]
remove_tags_after = dict(attrs={'class':'article_separator'})
remove_attributes = ['width','height']
def get_browser(self): feeds = [(u'Articles', u'http://www.nspm.rs/nspm-in-english/feed/rss.html')]
br = BasicNewsRecipe.get_browser()
br.open(self.INDEX)
return br
def preprocess_html(self, soup):
keep_only_tags = [dict(name='div', attrs={'id':'jsn-mainbody'})] for item in soup.body.findAll(style=True):
remove_tags = [dict(name='div', attrs={'id':'yvComment' })] del item['style']
return self.adeify_images(soup)
feeds = [ (u'NSPM in English', u'http://nspm.rs/nspm-in-english/feed/rss.html')]

View File

@ -1,13 +1,13 @@
__license__ = 'GPL v3' __license__ = 'GPL v3'
__copyright__ = '2009, Darko Miletic <darko.miletic at gmail.com>' __copyright__ = '2009-2010, Darko Miletic <darko.miletic at gmail.com>'
''' '''
sp.rian.ru sp.rian.ru
''' '''
from calibre.web.feeds.news import BasicNewsRecipe from calibre.web.feeds.news import BasicNewsRecipe
class Ria_eng(BasicNewsRecipe): class Ria_esp(BasicNewsRecipe):
title = 'Ria Novosti' title = 'Ria Novosti'
__author__ = 'Darko Miletic' __author__ = 'Darko Miletic'
description = 'Noticias desde Russia en Castellano' description = 'Noticias desde Russia en Castellano'
@ -28,14 +28,10 @@ class Ria_eng(BasicNewsRecipe):
} }
keep_only_tags = [dict(name='div', attrs={'class':'articletxt'})] keep_only_tags = [dict(name='div', attrs={'class':['mainnewsrubric','titleblock','mainnewstxt']})]
remove_tags = [dict(name=['object','link','iframe','base'])] remove_tags = [dict(name=['object','link','iframe','base'])]
remove_tags_after = dict(name='div',attrs={'class':'text'})
feeds = [(u'Noticias', u'http://sp.rian.ru/export/rss2/index.xml')] feeds = [(u'Noticias', u'http://rss.feedsportal.com/c/860/fe.ed/sp.rian.ru/export/rss2/index.xml')]
def print_version(self, url):
return url.replace('.html','-print.html')

View File

@ -14,7 +14,7 @@ class ScientificAmerican(BasicNewsRecipe):
description = u'Popular science. Monthly magazine.' description = u'Popular science. Monthly magazine.'
__author__ = 'Kovid Goyal and Sujata Raman' __author__ = 'Kovid Goyal and Sujata Raman'
language = 'en' language = 'en'
remove_javascript = True
oldest_article = 30 oldest_article = 30
max_articles_per_feed = 100 max_articles_per_feed = 100
no_stylesheets = True no_stylesheets = True
@ -31,11 +31,13 @@ class ScientificAmerican(BasicNewsRecipe):
remove_tags_after = dict(id=['article']) remove_tags_after = dict(id=['article'])
remove_tags = [ remove_tags = [
dict(id=['sharetools', 'reddit']), dict(id=['sharetools', 'reddit']),
dict(name='script'), #dict(name='script'),
{'class':['float_left', 'atools']}, {'class':['float_left', 'atools']},
{"class": re.compile(r'also-in-this')}, {"class": re.compile(r'also-in-this')},
dict(name='a',title = ["Get the Rest of the Article","Subscribe","Buy this Issue"]), dict(name='a',title = ["Get the Rest of the Article","Subscribe","Buy this Issue"]),
dict(name = 'img',alt = ["Graphic - Get the Rest of the Article"]), dict(name = 'img',alt = ["Graphic - Get the Rest of the Article"]),
dict(name='div', attrs={'class':['commentbox']}),
dict(name='h2', attrs={'class':['discuss_h2']}),
] ]
html2lrf_options = ['--base-font-size', '8'] html2lrf_options = ['--base-font-size', '8']
@ -110,3 +112,10 @@ class ScientificAmerican(BasicNewsRecipe):
div.extract() div.extract()
return soup return soup
preprocess_regexps = [
(re.compile(r'Already a Digital subscriber.*Now</a>', re.DOTALL|re.IGNORECASE), lambda match: ''),
(re.compile(r'If your institution has site license access, enter.*here</a>.', re.DOTALL|re.IGNORECASE), lambda match: ''),
(re.compile(r'to subscribe to our.*;.*\}', re.DOTALL|re.IGNORECASE), lambda match: ''),
(re.compile(r'\)\(jQuery\);.*-->', re.DOTALL|re.IGNORECASE), lambda match: ''),
]

View File

@ -0,0 +1,49 @@
from calibre.web.feeds.news import BasicNewsRecipe
import re
class Skeptic(BasicNewsRecipe):
title = u'The Skeptic'
description = 'Discussions with leading experts and investigation of fringe science and paranormal claims.'
language = 'en'
__author__ = 'Starson17'
oldest_article = 31
cover_url = 'http://www.skeptricks.com/images/Skeptic_Magazine.jpg'
remove_empty_feeds = True
remove_javascript = True
max_articles_per_feed = 50
no_stylesheets = True
remove_tags = [dict(name='div', attrs={'class':['Introduction','divider']}),
dict(name='div', attrs={'id':['feature', 'podcast']}),
dict(name='div', attrs={'id':re.compile(r'follow.*', re.DOTALL|re.IGNORECASE)}),
dict(name='hr'),
]
feeds = [
('The Skeptic', 'http://www.skeptic.com/feed'),
('E-Skeptic', 'http://www.skeptic.com/eskeptic'),
('All-SkepticBlog', 'http://skepticblog.org/feed'),
('Brian Dunning', 'http://skepticblog.org/author/dunning/feed/'),
('Daniel Loxton', 'http://skepticblog.org/author/loxton/feed/'),
('Kirsten Sanford', 'http://skepticblog.org/author/sanford/feed/'),
('Mark Edward', 'http://skepticblog.org/author/edward/feed/'),
('Michael Shermer', 'http://skepticblog.org/author/shermer/feed/'),
('Phil Plait', 'http://skepticblog.org/author/plait/feed/'),
('Ryan Johnson', 'http://skepticblog.org/author/johnson/feed/'),
('Steven Novella', 'http://skepticblog.org/author/novella/feed/'),
('Yau-Man Chan', 'http://skepticblog.org/author/chan/feed/'),
]
def get_browser(self):
br = BasicNewsRecipe.get_browser(self)
br.addheaders = [('Accept', 'text/html')]
return br
extra_css = '''
h1{font-family:Arial,Helvetica,sans-serif; font-weight:bold;font-size:large;}
h2{font-family:Arial,Helvetica,sans-serif; font-weight:normal;font-size:small;}
p{font-family:Arial,Helvetica,sans-serif;font-size:small;}
body{font-family:Helvetica,Arial,sans-serif;font-size:small;}
'''

View File

@ -0,0 +1,50 @@
from calibre.web.feeds.news import BasicNewsRecipe
import re
class TheSkepticalInquirer(BasicNewsRecipe):
title = u'The Skeptical Inquirer'
description = 'Investigation of fringe science and paranormal claims.'
language = 'en'
__author__ = 'Starson17'
oldest_article = 31
cover_url = 'http://www.skeptricks.com/images/Skeptical_Inquirer_Magazine.jpg'
remove_empty_feeds = True
remove_javascript = True
max_articles_per_feed = 50
no_stylesheets = True
keep_only_tags = [dict(name='div', attrs={'id':['content', 'bio']})]
remove_tags = [
dict(name='div', attrs={'id':['socialMedia']}),
]
preprocess_regexps = [
(re.compile(r'\.\(JavaScript must be enabled to view this email address\)', re.DOTALL|re.IGNORECASE), lambda match: ''),
]
def parse_index(self):
feeds = []
for title, url in [("The Skeptical Inquirer", "http://www.csicop.org")]:
articles = self.make_links(url)
if articles:
feeds.append((title, articles))
return feeds
def make_links(self, url):
soup = self.index_to_soup(url)
title = ''
current_articles = []
for item in soup.findAll(attrs={'class':['article-single bigger']}):
page_url = url + str(item.a["href"])
title = str(item.a.string)
current_articles.append({'title': title, 'url': page_url, 'description':'', 'date':''})
return current_articles
extra_css = '''
h1{font-family:Arial,Helvetica,sans-serif; font-weight:bold;font-size:large;}
h2{font-family:Arial,Helvetica,sans-serif; font-weight:normal;font-size:small;}
p{font-family:Arial,Helvetica,sans-serif;font-size:small;}
body{font-family:Helvetica,Arial,sans-serif;font-size:small;}
'''

View File

@ -0,0 +1,46 @@
__license__ = 'GPL v3'
__copyright__ = '2010, Starson17'
'''
snopes.com
'''
from calibre.web.feeds.recipes import BasicNewsRecipe
class Snopes(BasicNewsRecipe):
title = 'Snopes'
__author__ = 'Starson17'
description = 'Urban Legends'
oldest_article = 21
max_articles_per_feed = 100
no_stylesheets = True
use_embedded_content = False
encoding = 'utf8'
publisher = 'Snopes'
category = 'news, '
language = 'en'
publication_type = 'newsportal'
remove_javascript = True
no_stylesheets = True
conversion_options = {
'comments' : description
,'tags' : category
,'language' : language
,'publisher' : publisher
,'linearize_tables': True
}
keep_only_tags = [
dict(name='h1'),
dict(name='div', attrs={'class':['article_text']}),
]
feeds = [
('Snopes', 'http://www.snopes.com/info/whatsnew.xml'),
]
extra_css = '''
h1{font-family:Trebuchet MS,Bookman Old Style,Arial;color:#75b570}
h2{font-family:Arial,Helvetica,sans-serif; font-weight:normal;font-size:medium;}
p{font-family:Arial,Helvetica,sans-serif;font-size:small;}
body{font-family:Arial,Helvetica,sans-serif;font-size:small;}
'''

View File

@ -6,11 +6,10 @@ www.standardmedia.co.ke
import os import os
from calibre import strftime, __appname__, __version__ from calibre import strftime, __appname__, __version__
import calibre.utils.PythonMagickWand as pw
from ctypes import byref
from calibre.web.feeds.news import BasicNewsRecipe from calibre.web.feeds.news import BasicNewsRecipe
from calibre.constants import preferred_encoding from calibre.constants import preferred_encoding
from calibre.utils.magick import Image
class StandardMediaKeRecipe(BasicNewsRecipe): class StandardMediaKeRecipe(BasicNewsRecipe):
@ -88,19 +87,9 @@ class StandardMediaKeRecipe(BasicNewsRecipe):
self.cover_img_path = None self.cover_img_path = None
def prepare_cover_image(self, path_to_image, out_path): def prepare_cover_image(self, path_to_image, out_path):
with pw.ImageMagick(): img = Image()
img = pw.NewMagickWand() img.open(path_to_image)
if img < 0: img.save(out_path)
raise RuntimeError('Out of memory')
if not pw.MagickReadImage(img, path_to_image):
severity = pw.ExceptionType(0)
msg = pw.MagickGetException(img, byref(severity))
raise IOError('Failed to read image from: %s: %s'
%(path_to_image, msg))
if not pw.MagickWriteImage(img, out_path):
raise RuntimeError('Failed to save image to %s'%out_path)
pw.DestroyMagickWand(img)
def default_cover(self, cover_file): def default_cover(self, cover_file):
''' '''

View File

@ -32,8 +32,10 @@ class Starbulletin(BasicNewsRecipe):
remove_tags_before = dict(attrs={'id':'storyTitle'}) remove_tags_before = dict(attrs={'id':'storyTitle'})
remove_tags_after = dict(name='div',attrs={'class':'storytext'}) remove_tags_after = dict(name='div',attrs={'class':'storytext'})
remove_tags = [ remove_tags = [
dict(name=['object','link']) dict(name=['object','link','script','span'])
,dict(attrs={'class':'insideStoryImage'}) ,dict(attrs={'class':'insideStoryImage'})
,dict(attrs={'name':'fb_share'})
,dict(name='div',attrs={'class':'storytext'})
] ]
feeds = [ feeds = [

View File

@ -10,7 +10,7 @@ from calibre.web.feeds.news import BasicNewsRecipe
class TagesspiegelRSS(BasicNewsRecipe): class TagesspiegelRSS(BasicNewsRecipe):
title = u'Der Tagesspiegel' title = u'Der Tagesspiegel'
__author__ = 'ipaschke' __author__ = 'Ingo Paschke'
language = 'de' language = 'de'
oldest_article = 7 oldest_article = 7
max_articles_per_feed = 100 max_articles_per_feed = 100
@ -40,24 +40,29 @@ class TagesspiegelRSS(BasicNewsRecipe):
dict(name='div', attrs={'class':["hcf-jump-to-comments","hcf-clear","hcf-magnify hcf-media-control"] }), dict(name='div', attrs={'class':["hcf-jump-to-comments","hcf-clear","hcf-magnify hcf-media-control"] }),
dict(name='span', attrs={'class':["hcf-mainsearch",] }), dict(name='span', attrs={'class':["hcf-mainsearch",] }),
dict(name='ul', attrs={'class':["hcf-tools"]}), dict(name='ul', attrs={'class':["hcf-tools"]}),
dict(name='ul', attrs={'class': re.compile('hcf-services')})
] ]
def parse_index(self): def parse_index(self):
soup = self.index_to_soup('http://www.tagesspiegel.de/zeitung/') soup = self.index_to_soup('http://www.tagesspiegel.de/zeitung/')
def feed_title(div): def feed_title(div):
return ''.join(div.findAll(text=True, recursive=False)).strip() return ''.join(div.findAll(text=True, recursive=False)).strip() if div is not None else None
articles = {} articles = {}
key = None key = None
ans = [] ans = []
maincol = soup.find('div', attrs={'class':re.compile('hcf-main-col')})
for div in soup.findAll(True, attrs={'class':['hcf-teaser', 'hcf-header', 'story headline']}): for div in maincol.findAll(True, attrs={'class':['hcf-teaser', 'hcf-header', 'story headline']}):
if div['class'] == 'hcf-header': if div['class'] == 'hcf-header':
try:
key = string.capwords(feed_title(div.em.a)) key = string.capwords(feed_title(div.em.a))
articles[key] = [] articles[key] = []
ans.append(key) ans.append(key)
except:
continue
elif div['class'] == 'hcf-teaser' and getattr(div.contents[0],'name','') == 'h2': elif div['class'] == 'hcf-teaser' and getattr(div.contents[0],'name','') == 'h2':
a = div.find('a', href=True) a = div.find('a', href=True)
@ -83,4 +88,3 @@ class TagesspiegelRSS(BasicNewsRecipe):
ans = [(key, articles[key]) for key in ans if articles.has_key(key)] ans = [(key, articles[key]) for key in ans if articles.has_key(key)]
return ans return ans

View File

@ -50,12 +50,14 @@ class cdnet(BasicNewsRecipe):
dict(name='div', attrs={'class':'greyBoxR clearfix'}), dict(name='div', attrs={'class':'greyBoxR clearfix'}),
dict(name='div', attrs={'class':'greyBoxL clearfix'}), dict(name='div', attrs={'class':'greyBoxL clearfix'}),
dict(name='div', attrs={'class':'greyBox clearfix'}), dict(name='div', attrs={'class':'greyBox clearfix'}),
dict(name='div', attrs={'class':'labelized'}),
dict(id='')] dict(id='')]
#remove_tags_before = [dict(id='header-news-title')] #remove_tags_before = [dict(id='header-news-title')]
remove_tags_after = [dict(name='div', attrs={'class':'btmGreyTables'})] remove_tags_after = [dict(name='div', attrs={'class':'labelized'})]
#remove_tags_after = [dict(name='div', attrs={'class':'intelliTXT'})] #remove_tags_after = [dict(name='div', attrs={'class':'intelliTXT'})]
feeds = [ ('tomshardware', 'http://www.tomshardware.com/de/feeds/rss2/tom-s-hardware-de,12-1.xml') ] feeds = [ ('tomshardware', 'http://www.tomshardware.com/de/feeds/rss2/tom-s-hardware-de,12-1.xml') ]

View File

@ -36,7 +36,7 @@ class Vijesti(BasicNewsRecipe):
keep_only_tags = [dict(name='div', attrs={'id':'mainnews'})] keep_only_tags = [dict(name='div', attrs={'id':'mainnews'})]
remove_tags = [dict(name=['object','link','embed'])] remove_tags = [dict(name=['object','link','embed','form'])]
feeds = [(u'Sve vijesti', u'http://www.vijesti.me/rss.php' )] feeds = [(u'Sve vijesti', u'http://www.vijesti.me/rss.php' )]

View File

@ -22,7 +22,7 @@ class weltDe(BasicNewsRecipe):
remove_stylesheets = True remove_stylesheets = True
remove_javascript = True remove_javascript = True
encoding = 'utf-8' encoding = 'utf-8'
html2epub_options = 'linearize_tables = True\nbase_font_size2=10' html2epub_options = 'base_font_size=10'
BasicNewsRecipe.summary_length = 100 BasicNewsRecipe.summary_length = 100
@ -83,10 +83,9 @@ class weltDe(BasicNewsRecipe):
dict(name='div', attrs={'class':'articleOptions clear'}), dict(name='div', attrs={'class':'articleOptions clear'}),
dict(name='div', attrs={'class':'noPrint galleryIndex'}), dict(name='div', attrs={'class':'noPrint galleryIndex'}),
dict(name='div', attrs={'class':'inlineBox inlineTagCloud'}), dict(name='div', attrs={'class':'inlineBox inlineTagCloud'}),
dict(name='div', attrs={'class':'clear module imageGalleryBig bgColor1'}),
dict(name='div', attrs={'class':'clear module writeComment bgColor1'}), dict(name='div', attrs={'class':'clear module writeComment bgColor1'}),
dict(name='div', attrs={'class':'clear module textGallery bgColor1'}), dict(name='div', attrs={'class':'clear module textGallery bgColor1'}),
dict(name='div', attrs={'class':'clear module socialMedia bgColor1'}),
dict(name='div', attrs={'class':'clear module continuativeLinks'}),
dict(name='div', attrs={'class':'moreArtH3'}), dict(name='div', attrs={'class':'moreArtH3'}),
dict(name='div', attrs={'class':'jqmWindow'}), dict(name='div', attrs={'class':'jqmWindow'}),
dict(name='div', attrs={'class':'clear gap4'}), dict(name='div', attrs={'class':'clear gap4'}),
@ -99,7 +98,7 @@ class weltDe(BasicNewsRecipe):
dict(name='div', attrs={'class':'headLineH3'}), dict(name='div', attrs={'class':'headLineH3'}),
dict(name='div', attrs={'class':'print'}), dict(name='div', attrs={'class':'print'}),
dict(name='div', attrs={'class':'clear menu'}), dict(name='div', attrs={'class':'clear menu'}),
dict(name='div', attrs={'class':'clear galleryContent'}), dict(name='div', attrs={'class':'themenalarm'}),
dict(name='p', attrs={'class':'jump'}), dict(name='p', attrs={'class':'jump'}),
dict(name='a', attrs={'class':'commentLink'}), dict(name='a', attrs={'class':'commentLink'}),
dict(name='h2', attrs={'class':'jumpHeading'}), dict(name='h2', attrs={'class':'jumpHeading'}),
@ -110,7 +109,7 @@ class weltDe(BasicNewsRecipe):
dict(name='table', attrs={'class':'textGallery'}), dict(name='table', attrs={'class':'textGallery'}),
dict(name='li', attrs={'class':'active'})] dict(name='li', attrs={'class':'active'})]
remove_tags_after = [dict(name='div', attrs={'class':'clear departmentLine'})] remove_tags_after = [dict(name='div', attrs={'class':'themenalarm'})]
extra_css = ''' extra_css = '''
h2{font-family:Arial,Helvetica,sans-serif; font-size: x-small; color: #003399;} h2{font-family:Arial,Helvetica,sans-serif; font-size: x-small; color: #003399;}
@ -122,6 +121,7 @@ class weltDe(BasicNewsRecipe):
.photo {font-family:Arial,Helvetica,sans-serif; font-size: x-small; color: #666666;} ''' .photo {font-family:Arial,Helvetica,sans-serif; font-size: x-small; color: #666666;} '''
feeds = [ ('Politik', 'http://welt.de/politik/?service=Rss'), feeds = [ ('Politik', 'http://welt.de/politik/?service=Rss'),
('Deutsche Dinge', 'http://www.welt.de/deutsche-dinge/?service=Rss'),
('Wirtschaft', 'http://welt.de/wirtschaft/?service=Rss'), ('Wirtschaft', 'http://welt.de/wirtschaft/?service=Rss'),
('Finanzen', 'http://welt.de/finanzen/?service=Rss'), ('Finanzen', 'http://welt.de/finanzen/?service=Rss'),
('Sport', 'http://welt.de/sport/?service=Rss'), ('Sport', 'http://welt.de/sport/?service=Rss'),
@ -137,3 +137,4 @@ class weltDe(BasicNewsRecipe):
def print_version(self, url): def print_version(self, url):
return url.replace ('.html', '.html?print=true') return url.replace ('.html', '.html?print=true')

View File

@ -6,88 +6,105 @@ Fetch Die Zeit.
''' '''
from calibre.web.feeds.news import BasicNewsRecipe from calibre.web.feeds.news import BasicNewsRecipe
from calibre.ebooks.BeautifulSoup import Tag
class ZeitDe(BasicNewsRecipe): class ZeitDe(BasicNewsRecipe):
title = 'Die Zeit Nachrichten' title = 'ZEIT Online'
description = 'Die Zeit - Online Nachrichten' description = 'ZEIT Online'
language = 'de' language = 'de'
lang = 'de_DE' lang = 'de_DE'
__author__ = 'Martin Pitt and Sujata Raman' __author__ = 'Martin Pitt, Sujata Raman and Ingo Paschke'
use_embedded_content = False use_embedded_content = False
max_articles_per_feed = 40 max_articles_per_feed = 40
remove_empty_feeds = True remove_empty_feeds = True
no_stylesheets = True no_stylesheets = True
no_javascript = True
encoding = 'utf-8' encoding = 'utf-8'
feeds = [ feeds = [
('Seite 1', 'http://newsfeed.zeit.de/index_xml'),
('Politik', 'http://newsfeed.zeit.de/politik/index'), ('Politik', 'http://newsfeed.zeit.de/politik/index'),
('Wirtschaft', 'http://newsfeed.zeit.de/wirtschaft/index'), ('Wirtschaft', 'http://newsfeed.zeit.de/wirtschaft/index'),
('Meinung', 'http://newsfeed.zeit.de/meinung/index'), ('Meinung', 'http://newsfeed.zeit.de/meinung/index'),
('Gesellschaft', 'http://newsfeed.zeit.de/gesellschaft/index'), ('Gesellschaft', 'http://newsfeed.zeit.de/gesellschaft/index'),
('Kultur', 'http://newsfeed.zeit.de/kultur/index'), ('Kultur', 'http://newsfeed.zeit.de/kultur/index'),
('Wissen', 'http://newsfeed.zeit.de/wissen/index'), ('Wissen', 'http://newsfeed.zeit.de/wissen/index'),
('Digital', 'http://newsfeed.zeit.de/digital/index'),
('Studium', 'http://newsfeed.zeit.de/studium/index'),
('Karriere', 'http://newsfeed.zeit.de/karriere/index'),
('Lebensart', 'http://newsfeed.zeit.de/lebensart/index'),
('Reisen', 'http://newsfeed.zeit.de/reisen/index'),
('Auto', 'http://newsfeed.zeit.de/auto/index'),
('Sport', 'http://newsfeed.zeit.de/sport/index'),
] ]
extra_css = ''' extra_css = '''
.supertitle{color:#990000; font-family:Arial,Helvetica,sans-serif;font-size:xx-small;} .supertitle{color:#990000; font-family:Arial,Helvetica,sans-serif;font-size:xx-small;}
.excerpt{font-family:Georgia,Palatino,Palatino Linotype,FreeSerif,serif;font-size:large;} .excerpt{font-family:Georgia,Palatino,Palatino Linotype,FreeSerif,serif;font-size:small;}
.title{font-family:Arial,Helvetica,sans-serif;font-size:large} .title{font-family:Arial,Helvetica,sans-serif;font-size:large;clear:right;}
.caption{color:#666666; font-family:Arial,Helvetica,sans-serif;font-size:xx-small;} .caption{color:#666666; font-family:Arial,Helvetica,sans-serif;font-size:xx-small;}
.copyright{color:#666666; font-family:Arial,Helvetica,sans-serif;font-size:xx-small;} .copyright{color:#666666; font-family:Arial,Helvetica,sans-serif;font-size:xx-small;}
.article{font-family:Georgia,Palatino,Palatino Linotype,FreeSerif,serif;font-size:x-small} .article{font-family:Georgia,Palatino,Palatino Linotype,FreeSerif,serif;font-size:x-small}
.quote{font-family:Georgia,Palatino,Palatino Linotype,FreeSerif,serif;font-size:x-small}
.quote .cite{font-family:Georgia,Palatino,Palatino Linotype,FreeSerif,serif;font-size:xx-small}
.headline iconportrait_inline{font-family:Arial,Helvetica,sans-serif;font-size:x-small} .headline iconportrait_inline{font-family:Arial,Helvetica,sans-serif;font-size:x-small}
.inline{float:left;margin-top:0;margin-right:15px;position:relative;width:180px; }
img.inline{float:none}
.intertitle{font-family:Georgia,Palatino,Palatino Linotype,FreeSerif,serif;font-size:x-small;font-weight:700}
.ebinfobox{font-family:Georgia,Palatino,Palatino Linotype,FreeSerif,serif;font-size:xx-small;list-style-type:none;float:right;margin-top:0;border-left-style:solid;border-left-width:1px;padding-left:10px;}
.infobox {border-style: solid; border-width: 1px;padding:8px;}
.infobox dt {font-weight:700;}
''' '''
#filter_regexps = [r'ad.de.doubleclick.net/'] #filter_regexps = [r'ad.de.doubleclick.net/']
keep_only_tags = [ keep_only_tags = [
dict(name='div', attrs={'class':["article"]}) , dict(name='div', attrs={'class':["article"]}) ,
dict(name='ul', attrs={'class':["tools"]}) ,
] ]
remove_tags = [ remove_tags = [
dict(name='link'), dict(name='iframe'),dict(name='style'), dict(name='link'), dict(name='iframe'),dict(name='style'),dict(name='meta'),
dict(name='div', attrs={'class':["pagination block","pagenav","inline link"] }), dict(name='div', attrs={'class':["pagination block","pagenav","inline link", "copyright"] }),
dict(name='div', attrs={'id':["place_5","place_4"]}) dict(name='p', attrs={'class':["ressortbacklink", "copyright"] }),
dict(name='div', attrs={'id':["place_5","place_4","comments"]})
] ]
remove_attributes = ['style', 'font']
def get_article_url(self, article): def get_article_url(self, article):
ans = article.get('link',None)
ans = article.get('guid',None) ans += "?page=all"
try:
self.log('Looking for full story link in', ans)
soup = self.index_to_soup(ans)
x = soup.find(text="Auf einer Seite lesen")
if x is not None:
a = x.parent
if a and a.has_key('href'):
ans = a['href']
self.log('Found full story link', ans)
except:
pass
if 'video' in ans or 'quiz' in ans : if 'video' in ans or 'quiz' in ans :
ans = None ans = None
return ans return ans
def get_cover_url(self):
try:
inhalt = self.index_to_soup('http://www.zeit.de/inhalt')
return inhalt.find('div', attrs={'class':'singlearchive clearfix'}).img['src'].replace('icon_','')
except:
return 'http://images.zeit.de/bilder/titelseiten_zeit/1946/001_001.jpg'
def preprocess_html(self, soup): def preprocess_html(self, soup):
soup.html['xml:lang'] = self.lang soup.html['xml:lang'] = self.lang
soup.html['lang'] = self.lang soup.html['lang'] = self.lang
mtag = '<meta http-equiv="Content-Type" content="text/html; charset=' + self.encoding + '">' mtag = '<meta http-equiv="Content-Type" content="text/html; charset=' + self.encoding + '">'
soup.head.insert(0,mtag) soup.head.insert(0,mtag)
title = soup.find('h2', attrs={'class':'title'})
if title is None:
print "no title"
return soup
info = Tag(soup,'ul',[('class','ebinfobox')])
tools = soup.find('ul', attrs={'class':'tools'})
#author = tools.find('li','author first')
for tag in ['author first', 'date', 'date first', 'author', 'source']:
line = tools.find('li', tag)
if line:
info.insert(0,line)
title.parent.insert(0,info)
tools.extract()
return soup return soup
#def print_version(self,url):
# return url.replace('http://www.zeit.de/', 'http://images.zeit.de/text/').replace('?from=rss', '')

View File

@ -262,7 +262,7 @@
</xsl:if> </xsl:if>
<xsl:if test="@line-spacing"> <xsl:if test="@line-spacing">
<xsl:text>line-height:</xsl:text> <xsl:text>line-height:</xsl:text>
<xsl:value-of select="@line-height"/> <xsl:value-of select="@line-spacing"/>
<xsl:text>pt;</xsl:text> <xsl:text>pt;</xsl:text>
</xsl:if> </xsl:if>
<xsl:if test="(@align = 'just')"> <xsl:if test="(@align = 'just')">
@ -413,6 +413,10 @@
</xsl:element> </xsl:element>
</xsl:template> </xsl:template>
<xsl:template match="rtf:hardline-break">
<xsl:element name="br"/>
</xsl:template>
<xsl:template match="rtf:rtf-definition|rtf:font-table|rtf:color-table|rtf:style-table|rtf:page-definition|rtf:list-table|rtf:override-table|rtf:override-list|rtf:list-text"/> <xsl:template match="rtf:rtf-definition|rtf:font-table|rtf:color-table|rtf:style-table|rtf:page-definition|rtf:list-table|rtf:override-table|rtf:override-list|rtf:list-text"/>
<xsl:template match="rtf:body"> <xsl:template match="rtf:body">

View File

@ -115,7 +115,6 @@ if iswindows:
poppler_lib_dirs = consolidate('POPPLER_LIB_DIR', sw_lib_dir) poppler_lib_dirs = consolidate('POPPLER_LIB_DIR', sw_lib_dir)
popplerqt4_lib_dirs = poppler_lib_dirs popplerqt4_lib_dirs = poppler_lib_dirs
poppler_libs = ['poppler'] poppler_libs = ['poppler']
popplerqt4_libs = poppler_libs + ['QtCore4', 'QtGui4']
magick_inc_dirs = [os.path.join(prefix, 'build', 'ImageMagick-6.5.6')] magick_inc_dirs = [os.path.join(prefix, 'build', 'ImageMagick-6.5.6')]
magick_lib_dirs = [os.path.join(magick_inc_dirs[0], 'VisualMagick', 'lib')] magick_lib_dirs = [os.path.join(magick_inc_dirs[0], 'VisualMagick', 'lib')]
magick_libs = ['CORE_RL_wand_', 'CORE_RL_magick_'] magick_libs = ['CORE_RL_wand_', 'CORE_RL_magick_']
@ -129,8 +128,8 @@ elif isosx:
popplerqt4_inc_dirs = poppler_inc_dirs + [poppler_inc_dirs[0]+'/qt4'] popplerqt4_inc_dirs = poppler_inc_dirs + [poppler_inc_dirs[0]+'/qt4']
poppler_lib_dirs = consolidate('POPPLER_LIB_DIR', poppler_lib_dirs = consolidate('POPPLER_LIB_DIR',
'/sw/lib') '/sw/lib')
poppler_libs = ['poppler']
popplerqt4_lib_dirs = poppler_lib_dirs popplerqt4_lib_dirs = poppler_lib_dirs
poppler_libs = popplerqt4_libs = ['poppler']
podofo_inc = '/sw/podofo' podofo_inc = '/sw/podofo'
podofo_lib = '/sw/lib' podofo_lib = '/sw/lib'
magick_inc_dirs = consolidate('MAGICK_INC', magick_inc_dirs = consolidate('MAGICK_INC',
@ -162,9 +161,6 @@ else:
poppler_libs = pkgconfig_libs('poppler', '', '') poppler_libs = pkgconfig_libs('poppler', '', '')
if not poppler_libs: if not poppler_libs:
poppler_libs = ['poppler'] poppler_libs = ['poppler']
popplerqt4_libs = pkgconfig_libs('poppler-qt4', '', '')
if not popplerqt4_libs:
popplerqt4_libs = ['poppler-qt4', 'poppler']
magick_libs = pkgconfig_libs('MagickWand', '', '') magick_libs = pkgconfig_libs('MagickWand', '', '')
if not magick_libs: if not magick_libs:
magick_libs = ['MagickWand', 'MagickCore'] magick_libs = ['MagickWand', 'MagickCore']

View File

@ -72,6 +72,13 @@ extensions = [
lib_dirs=chmlib_lib_dirs, lib_dirs=chmlib_lib_dirs,
cflags=["-D__PYTHON__"]), cflags=["-D__PYTHON__"]),
Extension('magick',
['calibre/utils/magick/magick.c'],
headers=['calibre/utils/magick/magick_constants.h'],
libraries=magick_libs,
lib_dirs=magick_lib_dirs,
inc_dirs=magick_inc_dirs
),
Extension('pdfreflow', Extension('pdfreflow',
reflow_sources, reflow_sources,

View File

@ -1,12 +1,80 @@
Notes on setting up the windows development environment Notes on setting up the windows development environment
======================================================== ========================================================
Set CMAKE_PREFIX_PATH to C:\cygwin\home\kovid\sw Overview
----------
calibre and all its dependencies are compiled using Visual Studio 2008 express edition (free from MS). All the following instructions must be run in a visual studio command prompt unless otherwise noted.
calibre contains build script to automate the building of the calibre installer. These scripts make certain assumptions about where dependencies are installed. Your best best is to setup a VM and replicate the paths mentioned below exactly.
Basic dependencies
--------------------
Install cygwin and setup sshd (optional). Used to enable automation of the calibre build VM from linux, not needed if you are building manually.
Install MS Visual Studio 2008, cmake, python and WiX.
Set CMAKE_PREFIX_PATH environment variable to C:\cygwin\home\kovid\sw
This is where all dependencies will be installed.
Add C:\Python26\Scripts and C:\Python26 to PATH
Install setuptools from http://pypi.python.org/pypi/setuptools
If there are no windows binaries already compiled for the version of python you are using then download the source and run the following command in the folder where the source has been unpacked::
python setup.py install
Run the following command to install python dependencies::
easy_install --always-unzip -U ipython mechanize BeautifulSoup pyreadline python-dateutil dnspython
Qt
--------
Extract Qt sourcecode to C:\Qt\4.x.x. Run configure and make::
configure -opensource -qt-zlib -qt-gif -qt-libmng -qt-libpng -qt-libtiff -qt-libjpeg -release -platform win32-msvc -no-qt3support -webkit -xmlpatterns -no-phonon
nmake
SIP
-----
Available from: http://www.riverbankcomputing.co.uk/software/sip/download ::
python configure.py -p win32-msvc2008
nmake
nmake install
PyQt4
----------
Compiling instructions::
python configure.py -c -j5 -e QtCore -e QtGui -e QtSvg -e QtNetwork -e QtWebKit -e QtXmlPatterns --verbose
nmake
nmake install
Python Imaging Library
------------------------
Install as normal using provided installer.
Libunrar
----------
http://www.rarlab.com/rar/UnRARDLL.exe install and add C:\Program Files\UnrarDLL to PATH
lxml
------
http://pypi.python.org/pypi/lxml
jpeg-7 jpeg-7
------- -------
Copy Copy::
jconfig.vc to jconfig.h, makejsln.vc9 to jpeg.sln, jconfig.vc to jconfig.h, makejsln.vc9 to jpeg.sln,
makeasln.vc9 to apps.sln, makejvcp.vc9 to jpeg.vcproj, makeasln.vc9 to apps.sln, makejvcp.vc9 to jpeg.vcproj,
makecvcp.vc9 to cjpeg.vcproj, makedvcp.vc9 to djpeg.vcproj, makecvcp.vc9 to cjpeg.vcproj, makedvcp.vc9 to djpeg.vcproj,
@ -169,7 +237,7 @@ cp build/podofo/build/src/Release/podofo.exp lib/
cp build/podofo/build/podofo_config.h include/podofo/ cp build/podofo/build/podofo_config.h include/podofo/
cp -r build/podofo/src/* include/podofo/ cp -r build/podofo/src/* include/podofo/
The following patch was required to get it to compile: The following patch (against 0.8.1) was required to get it to compile:
Index: src/PdfImage.cpp Index: src/PdfImage.cpp
=================================================================== ===================================================================
@ -222,3 +290,19 @@ Undefine ProvideDllMain and MAGICKCORE_X11_DELEGATE
Now open VisualMagick/VisualDynamicMT.sln set to Release Now open VisualMagick/VisualDynamicMT.sln set to Release
Remove the CORE_xlib project Remove the CORE_xlib project
calibre
---------
Take a linux calibre tree on which you have run the following command::
python setup.py stage1
and copy it to windows.
Run::
python setup.py build
python setup.py win32_freeze
This will create the .msi in the dist directory.

View File

@ -75,9 +75,9 @@ class Manual(Command):
os.makedirs('.build'+os.sep+'html') os.makedirs('.build'+os.sep+'html')
os.environ['__appname__'] = __appname__ os.environ['__appname__'] = __appname__
os.environ['__version__'] = __version__ os.environ['__version__'] = __version__
subprocess.check_call(['sphinx-build', '-b', 'custom', '-t', 'online', subprocess.check_call(['sphinx-build', '-b', 'html', '-t', 'online',
'-d', '.build/doctrees', '.', '.build/html']) '-d', '.build/doctrees', '.', '.build/html'])
subprocess.check_call(['sphinx-build', '-b', 'epub', '-d', subprocess.check_call(['sphinx-build', '-b', 'myepub', '-d',
'.build/doctrees', '.', '.build/epub']) '.build/doctrees', '.', '.build/epub'])
shutil.copyfile(self.j('.build', 'epub', 'calibre.epub'), self.j('.build', shutil.copyfile(self.j('.build', 'epub', 'calibre.epub'), self.j('.build',
'html', 'calibre.epub')) 'html', 'calibre.epub'))

View File

@ -2,6 +2,7 @@
__license__ = 'GPL v3' __license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal <kovid@kovidgoyal.net>' __copyright__ = '2008, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
import sys, os, re, logging, time, mimetypes, \ import sys, os, re, logging, time, mimetypes, \
__builtin__, warnings, multiprocessing __builtin__, warnings, multiprocessing
from urllib import getproxies from urllib import getproxies
@ -13,12 +14,13 @@ from functools import partial
warnings.simplefilter('ignore', DeprecationWarning) warnings.simplefilter('ignore', DeprecationWarning)
from calibre.startup import plugins, winutil, winutilerror
from calibre.constants import iswindows, isosx, islinux, isfreebsd, isfrozen, \ from calibre.constants import iswindows, isosx, islinux, isfreebsd, isfrozen, \
terminal_controller, preferred_encoding, \ terminal_controller, preferred_encoding, \
__appname__, __version__, __author__, \ __appname__, __version__, __author__, \
win32event, win32api, winerror, fcntl, \ win32event, win32api, winerror, fcntl, \
filesystem_encoding filesystem_encoding, plugins, config_dir
from calibre.startup import winutil, winutilerror
import mechanize import mechanize
if False: if False:
@ -411,15 +413,13 @@ def entity_to_unicode(match, exceptions=[], encoding='cp1252',
return check("'") return check("'")
if ent == 'hellips': if ent == 'hellips':
ent = 'hellip' ent = 'hellip'
if ent.lower().startswith(u'#x'): if ent.startswith('#'):
num = int(ent[2:], 16)
if encoding is None or num > 255:
return check(my_unichr(num))
return check(chr(num).decode(encoding))
if ent.startswith(u'#'):
try: try:
if ent[1] in ('x', 'X'):
num = int(ent[2:], 16)
else:
num = int(ent[1:]) num = int(ent[1:])
except ValueError: except:
return '&'+ent+';' return '&'+ent+';'
if encoding is None or num > 255: if encoding is None or num > 255:
return check(my_unichr(num)) return check(my_unichr(num))
@ -486,7 +486,6 @@ def ipython(user_ns=None):
sys.argv = ['ipython'] sys.argv = ['ipython']
if user_ns is None: if user_ns is None:
user_ns = locals() user_ns = locals()
from calibre.utils.config import config_dir
ipydir = os.path.join(config_dir, ('_' if iswindows else '.')+'ipython') ipydir = os.path.join(config_dir, ('_' if iswindows else '.')+'ipython')
os.environ['IPYTHONDIR'] = ipydir os.environ['IPYTHONDIR'] = ipydir
if not os.path.exists(ipydir): if not os.path.exists(ipydir):

View 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.9' __version__ = '0.7.13'
__author__ = "Kovid Goyal <kovid@kovidgoyal.net>" __author__ = "Kovid Goyal <kovid@kovidgoyal.net>"
import re import re
@ -14,7 +14,7 @@ numeric_version = tuple(_ver)
Various run time constants. Various run time constants.
''' '''
import sys, locale, codecs import sys, locale, codecs, os
from calibre.utils.terminfo import TerminalController from calibre.utils.terminfo import TerminalController
terminal_controller = TerminalController(sys.stdout) terminal_controller = TerminalController(sys.stdout)
@ -47,7 +47,7 @@ def debug():
global DEBUG global DEBUG
DEBUG = True DEBUG = True
################################################################################ # plugins {{{
plugins = None plugins = None
if plugins is None: if plugins is None:
# Load plugins # Load plugins
@ -60,6 +60,7 @@ if plugins is None:
'pictureflow', 'pictureflow',
'lzx', 'lzx',
'msdes', 'msdes',
'magick',
'podofo', 'podofo',
'cPalmdoc', 'cPalmdoc',
'fontconfig', 'fontconfig',
@ -80,3 +81,22 @@ if plugins is None:
return plugins return plugins
plugins = load_plugins() plugins = load_plugins()
# }}}
# config_dir {{{
if os.environ.has_key('CALIBRE_CONFIG_DIRECTORY'):
config_dir = os.path.abspath(os.environ['CALIBRE_CONFIG_DIRECTORY'])
elif iswindows:
if plugins['winutil'][0] is None:
raise Exception(plugins['winutil'][1])
config_dir = plugins['winutil'][0].special_folder_path(plugins['winutil'][0].CSIDL_APPDATA)
if not os.access(config_dir, os.W_OK|os.X_OK):
config_dir = os.path.expanduser('~')
config_dir = os.path.join(config_dir, 'calibre')
elif isosx:
config_dir = os.path.expanduser('~/Library/Preferences/calibre')
else:
bdir = os.path.abspath(os.path.expanduser(os.environ.get('XDG_CONFIG_HOME', '~/.config')))
config_dir = os.path.join(bdir, 'calibre')
# }}}

View File

@ -262,7 +262,7 @@ class CatalogPlugin(Plugin):
type = _('Catalog generator') type = _('Catalog generator')
#: CLI parser options specific to this plugin, declared as namedtuple Option #: CLI parser options specific to this plugin, declared as namedtuple Option::
#: #:
#: from collections import namedtuple #: from collections import namedtuple
#: Option = namedtuple('Option', 'option, default, dest, help') #: Option = namedtuple('Option', 'option, default, dest, help')
@ -272,21 +272,11 @@ class CatalogPlugin(Plugin):
#: help = (_('Title of generated catalog. \nDefault:') + " '" + #: help = (_('Title of generated catalog. \nDefault:') + " '" +
#: '%default' + "'"))] #: '%default' + "'"))]
#: cli_options parsed in library.cli:catalog_option_parser() #: cli_options parsed in library.cli:catalog_option_parser()
cli_options = [] cli_options = []
def search_sort_db(self, db, opts): def search_sort_db(self, db, opts):
'''
# Don't add Catalogs to the generated Catalogs
cat = _('Catalog')
if opts.search_text:
opts.search_text += ' not tag:'+cat
else:
opts.search_text = 'not tag:'+cat
'''
db.search(opts.search_text) db.search(opts.search_text)
if opts.sort_by: if opts.sort_by:
@ -349,8 +339,7 @@ class CatalogPlugin(Plugin):
It should generate the catalog in the format specified It should generate the catalog in the format specified
in file_types, returning the absolute path to the in file_types, returning the absolute path to the
generated catalog file. If an error is encountered generated catalog file. If an error is encountered
it should raise an Exception and return None. The default it should raise an Exception.
implementation simply returns None.
The generated catalog file should be created with the The generated catalog file should be created with the
:meth:`temporary_file` method. :meth:`temporary_file` method.
@ -358,9 +347,6 @@ class CatalogPlugin(Plugin):
:param path_to_output: Absolute path to the generated catalog file. :param path_to_output: Absolute path to the generated catalog file.
:param opts: A dictionary of keyword arguments :param opts: A dictionary of keyword arguments
:param db: A LibraryDatabase2 object :param db: A LibraryDatabase2 object
:return: None
''' '''
# Default implementation does nothing # Default implementation does nothing
raise NotImplementedError('CatalogPlugin.generate_catalog() default ' raise NotImplementedError('CatalogPlugin.generate_catalog() default '

View File

@ -467,12 +467,15 @@ 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 LibraryThing
from calibre.ebooks.metadata.douban import DoubanBooks from calibre.ebooks.metadata.douban import DoubanBooks
from calibre.ebooks.metadata.covers import OpenLibraryCovers, \
LibraryThingCovers
from calibre.library.catalog import CSV_XML, EPUB_MOBI, BIBTEX from calibre.library.catalog import CSV_XML, EPUB_MOBI, BIBTEX
from calibre.ebooks.epub.fix.unmanifested import Unmanifested from calibre.ebooks.epub.fix.unmanifested import Unmanifested
from calibre.ebooks.epub.fix.epubcheck import Epubcheck from calibre.ebooks.epub.fix.epubcheck import Epubcheck
plugins = [HTML2ZIP, PML2PMLZ, ArchiveExtract, GoogleBooks, ISBNDB, Amazon, plugins = [HTML2ZIP, PML2PMLZ, ArchiveExtract, GoogleBooks, ISBNDB, Amazon,
LibraryThing, DoubanBooks, CSV_XML, EPUB_MOBI, BIBTEX, Unmanifested, Epubcheck] LibraryThing, DoubanBooks, CSV_XML, EPUB_MOBI, BIBTEX, Unmanifested,
Epubcheck, OpenLibraryCovers, LibraryThingCovers]
plugins += [ plugins += [
ComicInput, ComicInput,
EPUBInput, EPUBInput,

View File

@ -28,7 +28,7 @@ class ConversionOption(object):
def validate_parameters(self): def validate_parameters(self):
''' '''
Validate the parameters passed to :method:`__init__`. Validate the parameters passed to :meth:`__init__`.
''' '''
if re.match(r'[a-zA-Z_]([a-zA-Z0-9_])*', self.name) is None: if re.match(r'[a-zA-Z_]([a-zA-Z0-9_])*', self.name) is None:
raise ValueError(self.name + ' is not a valid Python identifier') raise ValueError(self.name + ' is not a valid Python identifier')
@ -96,7 +96,7 @@ class InputFormatPlugin(Plugin):
InputFormatPlugins are responsible for converting a document into InputFormatPlugins are responsible for converting a document into
HTML+OPF+CSS+etc. HTML+OPF+CSS+etc.
The results of the conversion *must* be encoded in UTF-8. The results of the conversion *must* be encoded in UTF-8.
The main action happens in :method:`convert`. The main action happens in :meth:`convert`.
''' '''
type = _('Conversion Input') type = _('Conversion Input')
@ -109,7 +109,7 @@ class InputFormatPlugin(Plugin):
#: If True, this input plugin generates a collection of images, #: If True, this input plugin generates a collection of images,
#: one per HTML file. You can obtain access to the images via #: one per HTML file. You can obtain access to the images via
#: convenience method, :method:`get_image_collection`. #: convenience method, :meth:`get_image_collection`.
is_image_collection = False is_image_collection = False
#: If set to True, the input plugin will perform special processing #: If set to True, the input plugin will perform special processing
@ -117,7 +117,7 @@ class InputFormatPlugin(Plugin):
for_viewer = False for_viewer = False
#: Options shared by all Input format plugins. Do not override #: Options shared by all Input format plugins. Do not override
#: in sub-classes. Use :member:`options` instead. Every option must be an #: in sub-classes. Use :attr:`options` instead. Every option must be an
#: instance of :class:`OptionRecommendation`. #: instance of :class:`OptionRecommendation`.
common_options = set([ common_options = set([
OptionRecommendation(name='input_encoding', OptionRecommendation(name='input_encoding',
@ -173,7 +173,6 @@ class InputFormatPlugin(Plugin):
returns. returns.
:param stream: A file like object that contains the input file. :param stream: A file like object that contains the input file.
:param options: Options to customize the conversion process. :param options: Options to customize the conversion process.
Guaranteed to have attributes corresponding Guaranteed to have attributes corresponding
to all the options declared by this plugin. In to all the options declared by this plugin. In
@ -182,14 +181,11 @@ class InputFormatPlugin(Plugin):
mean be more verbose. Another useful attribute is mean be more verbose. Another useful attribute is
``input_profile`` that is an instance of ``input_profile`` that is an instance of
:class:`calibre.customize.profiles.InputProfile`. :class:`calibre.customize.profiles.InputProfile`.
:param file_ext: The extension (without the .) of the input file. It :param file_ext: The extension (without the .) of the input file. It
is guaranteed to be one of the `file_types` supported is guaranteed to be one of the `file_types` supported
by this plugin. by this plugin.
:param log: A :class:`calibre.utils.logging.Log` object. All output :param log: A :class:`calibre.utils.logging.Log` object. All output
should use this object. should use this object.
:param accelarators: A dictionary of various information that the input :param accelarators: A dictionary of various information that the input
plugin can get easily that would speed up the plugin can get easily that would speed up the
subsequent stages of the conversion. subsequent stages of the conversion.
@ -235,7 +231,7 @@ class OutputFormatPlugin(Plugin):
(OPF+HTML) into an output ebook. (OPF+HTML) into an output ebook.
The OEB document can be assumed to be encoded in UTF-8. The OEB document can be assumed to be encoded in UTF-8.
The main action happens in :method:`convert`. The main action happens in :meth:`convert`.
''' '''
type = _('Conversion Output') type = _('Conversion Output')
@ -247,7 +243,7 @@ class OutputFormatPlugin(Plugin):
file_type = None file_type = None
#: Options shared by all Input format plugins. Do not override #: Options shared by all Input format plugins. Do not override
#: in sub-classes. Use :member:`options` instead. Every option must be an #: in sub-classes. Use :attr:`options` instead. Every option must be an
#: instance of :class:`OptionRecommendation`. #: instance of :class:`OptionRecommendation`.
common_options = set([ common_options = set([
OptionRecommendation(name='pretty_print', OptionRecommendation(name='pretty_print',
@ -280,14 +276,12 @@ class OutputFormatPlugin(Plugin):
it is the path to a directory that may or may not exist. The output it is the path to a directory that may or may not exist. The output
plugin should write its output into that directory. If it is a file like plugin should write its output into that directory. If it is a file like
object, the output plugin should write its output into the file. object, the output plugin should write its output into the file.
:param input_plugin: The input plugin that was used at the beginning of :param input_plugin: The input plugin that was used at the beginning of
the conversion pipeline. the conversion pipeline.
:param opts: Conversion options. Guaranteed to have attributes :param opts: Conversion options. Guaranteed to have attributes
corresponding to the OptionRecommendations of this plugin. corresponding to the OptionRecommendations of this plugin.
:param log: The logger. Print debug/info messages etc. using this. :param log: The logger. Print debug/info messages etc. using this.
''' '''
raise NotImplementedError raise NotImplementedError

View File

@ -233,18 +233,20 @@ class OutputProfile(Plugin):
'if you want to produce a document intended to be read at a ' 'if you want to produce a document intended to be read at a '
'computer or on a range of devices.') 'computer or on a range of devices.')
# The image size for comics #: The image size for comics
comic_screen_size = (584, 754) comic_screen_size = (584, 754)
# If True the MOBI renderer on the device supports MOBI indexing #: If True the MOBI renderer on the device supports MOBI indexing
supports_mobi_indexing = False supports_mobi_indexing = False
# If True output should be optimized for a touchscreen interface #: If True output should be optimized for a touchscreen interface
touchscreen = False touchscreen = False
touchscreen_news_css = '' touchscreen_news_css = ''
# A list of extra (beyond CSS 2.1) modules supported by the device #: A list of extra (beyond CSS 2.1) modules supported by the device
# Format is a cssutils profile dictionary (see iPad for example) #: Format is a cssutils profile dictionary (see iPad for example)
extra_css_modules = [] extra_css_modules = []
#: If True, the date is appended to the title of downloaded news
periodical_date_in_title = True
@classmethod @classmethod
def tags_to_string(cls, tags): def tags_to_string(cls, tags):
@ -550,6 +552,7 @@ class KindleOutput(OutputProfile):
fbase = 16 fbase = 16
fsizes = [12, 12, 14, 16, 18, 20, 22, 24] fsizes = [12, 12, 14, 16, 18, 20, 22, 24]
supports_mobi_indexing = True supports_mobi_indexing = True
periodical_date_in_title = False
@classmethod @classmethod
def tags_to_string(cls, tags): def tags_to_string(cls, tags):
@ -567,6 +570,7 @@ class KindleDXOutput(OutputProfile):
dpi = 150.0 dpi = 150.0
comic_screen_size = (741, 1022) comic_screen_size = (741, 1022)
supports_mobi_indexing = True supports_mobi_indexing = True
periodical_date_in_title = False
@classmethod @classmethod
def tags_to_string(cls, tags): def tags_to_string(cls, tags):

View File

@ -13,6 +13,7 @@ from calibre.customize.builtins import plugins as builtin_plugins
from calibre.constants import numeric_version as version, iswindows, isosx from calibre.constants import numeric_version as version, iswindows, isosx
from calibre.devices.interface import DevicePlugin from calibre.devices.interface import DevicePlugin
from calibre.ebooks.metadata import MetaInformation from calibre.ebooks.metadata import MetaInformation
from calibre.ebooks.metadata.covers import CoverDownload
from calibre.ebooks.metadata.fetch import MetadataSource from calibre.ebooks.metadata.fetch import MetadataSource
from calibre.utils.config import make_config_dir, Config, ConfigProxy, \ from calibre.utils.config import make_config_dir, Config, ConfigProxy, \
plugin_dir, OptionParser, prefs plugin_dir, OptionParser, prefs
@ -234,6 +235,15 @@ 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 cover_sources():
customization = config['plugin_customization']
for plugin in _initialized_plugins:
if isinstance(plugin, CoverDownload):
if not is_disabled(plugin):
plugin.site_customization = customization.get(plugin.name, '')
yield plugin
# }}} # }}}
# Metadata read/write {{{ # Metadata read/write {{{

View File

@ -19,10 +19,12 @@ class ANDROID(USBMS):
VENDOR_ID = { VENDOR_ID = {
# HTC # HTC
0x0bb4 : { 0x0c02 : [0x100], 0x0c01 : [0x100], 0x0ff9 : [0x0100]}, 0x0bb4 : { 0x0c02 : [0x100, 0x227], 0x0c01 : [0x100, 0x227], 0x0ff9
: [0x0100, 0x227]},
# Motorola # Motorola
0x22b8 : { 0x41d9 : [0x216], 0x2d67 : [0x100], 0x41db : [0x216]}, 0x22b8 : { 0x41d9 : [0x216], 0x2d67 : [0x100], 0x41db : [0x216],
0x4285 : [0x216]},
# Sony Ericsson # Sony Ericsson
0xfce : { 0xd12e : [0x0100]}, 0xfce : { 0xd12e : [0x0100]},
@ -52,9 +54,9 @@ class ANDROID(USBMS):
'GT-I5700', 'SAMSUNG', 'DELL', 'LINUX'] 'GT-I5700', 'SAMSUNG', 'DELL', 'LINUX']
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',
'GT-I9000', 'FILE-STOR_GADGET'] 'GT-I9000', 'FILE-STOR_GADGET', 'SGH-T959']
WINDOWS_CARD_A_MEM = ['ANDROID_PHONE', 'GT-I9000_CARD', WINDOWS_CARD_A_MEM = ['ANDROID_PHONE', 'GT-I9000_CARD',
'FILE-STOR_GADGET'] 'FILE-STOR_GADGET', 'SGH-T959']
OSX_MAIN_MEM = 'HTC Android Phone Media' OSX_MAIN_MEM = 'HTC Android Phone Media'

View File

@ -2668,7 +2668,7 @@ class ITUNES(DriverBase):
index = metadata.series_index index = metadata.series_index
integer = int(index) integer = int(index)
fraction = index-integer fraction = index-integer
series_index = '%04d%%s' % (integer, str('%0.4f' % fraction).lstrip('0')) series_index = '%04d%s' % (integer, str('%0.4f' % fraction).lstrip('0'))
if lb_added: if lb_added:
lb_added.SortName = "%s %s" % (metadata.series, series_index) lb_added.SortName = "%s %s" % (metadata.series, series_index)
lb_added.Genre = metadata.series lb_added.Genre = metadata.series

View File

@ -43,6 +43,7 @@ class THEBOOK(N516):
BCD = [0x399] BCD = [0x399]
MAIN_MEMORY_VOLUME_LABEL = 'The Book Main Memory' MAIN_MEMORY_VOLUME_LABEL = 'The Book Main Memory'
EBOOK_DIR_MAIN = 'My books' EBOOK_DIR_MAIN = 'My books'
WINDOWS_CARD_A_MEM = '_FILE-STOR_GADGE'
class ALEX(N516): class ALEX(N516):

View File

@ -1,10 +1,5 @@
__license__ = 'GPL v3' __license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>' __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
"""
Define the minimum interface that a device backend must satisfy to be used in
the GUI. A device backend must subclass the L{Device} class. See prs500.py for
a backend that implement the Device interface for the SONY PRS500 Reader.
"""
import os import os
from collections import namedtuple from collections import namedtuple
@ -15,32 +10,38 @@ class DevicePlugin(Plugin):
""" """
Defines the interface that should be implemented by backends that Defines the interface that should be implemented by backends that
communicate with an ebook reader. communicate with an ebook reader.
The C{end_session} variables are used for USB session management. Sometimes
the front-end needs to call several methods one after another, in which case
the USB session should not be closed after each method call.
""" """
type = _('Device Interface') type = _('Device Interface')
# Ordered list of supported formats #: Ordered list of supported formats
FORMATS = ["lrf", "rtf", "pdf", "txt"] FORMATS = ["lrf", "rtf", "pdf", "txt"]
#: VENDOR_ID can be either an integer, a list of integers or a dictionary #: VENDOR_ID can be either an integer, a list of integers or a dictionary
#: If it is a dictionary, it must be a dictionary of dictionaries, of the form #: If it is a dictionary, it must be a dictionary of dictionaries,
#: of the form::
#:
#: { #: {
#: integer_vendor_id : { product_id : [list of BCDs], ... }, #: integer_vendor_id : { product_id : [list of BCDs], ... },
#: ... #: ...
#: } #: }
#:
VENDOR_ID = 0x0000 VENDOR_ID = 0x0000
#: An integer or a list of integers #: An integer or a list of integers
PRODUCT_ID = 0x0000 PRODUCT_ID = 0x0000
# BCD can be either None to not distinguish between devices based on BCD, or #: BCD can be either None to not distinguish between devices based on BCD, or
# it can be a list of the BCD numbers of all devices supported by this driver. #: it can be a list of the BCD numbers of all devices supported by this driver.
BCD = None BCD = None
THUMBNAIL_HEIGHT = 68 # Height for thumbnails on device
# Whether the metadata on books can be set via the GUI. #: Height for thumbnails on the device
THUMBNAIL_HEIGHT = 68
#: Whether the metadata on books can be set via the GUI.
CAN_SET_METADATA = True CAN_SET_METADATA = True
#: Path separator for paths to books on device #: Path separator for paths to books on device
path_sep = os.sep path_sep = os.sep
#: Icon for this device #: Icon for this device
icon = I('reader.svg') icon = I('reader.svg')
@ -51,6 +52,11 @@ class DevicePlugin(Plugin):
#: long time #: long time
OPEN_FEEDBACK_MESSAGE = None OPEN_FEEDBACK_MESSAGE = None
#: Set of extensions that are "virtual books" on the device
#: and therefore cannot be viewed/saved/added to library
#: For example: ``frozenset(['kobo'])``
VIRTUAL_BOOK_EXTENSIONS = frozenset([])
@classmethod @classmethod
def get_gui_name(cls): def get_gui_name(cls):
if hasattr(cls, 'gui_name'): if hasattr(cls, 'gui_name'):
@ -121,6 +127,7 @@ class DevicePlugin(Plugin):
Return True, device_info if a device handled by this plugin is currently connected. Return True, device_info if a device handled by this plugin is currently connected.
:param devices_on_system: List of devices currently connected :param devices_on_system: List of devices currently connected
''' '''
if iswindows: if iswindows:
return self.is_usb_connected_windows(devices_on_system, return self.is_usb_connected_windows(devices_on_system,
@ -157,13 +164,14 @@ class DevicePlugin(Plugin):
def reset(self, key='-1', log_packets=False, report_progress=None, def reset(self, key='-1', log_packets=False, report_progress=None,
detected_device=None) : detected_device=None) :
""" """
:key: The key to unlock the device :param key: The key to unlock the device
:log_packets: If true the packet stream to/from the device is logged :param log_packets: If true the packet stream to/from the device is logged
:report_progress: Function that is called with a % progress :param report_progress: Function that is called with a % progress
(number between 0 and 100) for various tasks (number between 0 and 100) for various tasks
If it is called with -1 that means that the If it is called with -1 that means that the
task does not have any progress information task does not have any progress information
:detected_device: Device information from the device scanner :param detected_device: Device information from the device scanner
""" """
raise NotImplementedError() raise NotImplementedError()
@ -174,19 +182,21 @@ class DevicePlugin(Plugin):
is only called after the vendor, product ids and the bcd have matched, so is only called after the vendor, product ids and the bcd have matched, so
it can do some relatively time intensive checks. The default implementation it can do some relatively time intensive checks. The default implementation
returns True. This method is called only on windows. See also returns True. This method is called only on windows. See also
:method:`can_handle`. :meth:`can_handle`.
:param device_info: On windows a device ID string. On Unix a tuple of :param device_info: On windows a device ID string. On Unix a tuple of
``(vendor_id, product_id, bcd)``. ``(vendor_id, product_id, bcd)``.
''' '''
return True return True
def can_handle(self, device_info, debug=False): def can_handle(self, device_info, debug=False):
''' '''
Unix version of :method:`can_handle_windows` Unix version of :meth:`can_handle_windows`
:param device_info: Is a tupe of (vid, pid, bcd, manufacturer, product, :param device_info: Is a tupe of (vid, pid, bcd, manufacturer, product,
serial number) serial number)
''' '''
return True return True
@ -198,7 +208,8 @@ class DevicePlugin(Plugin):
For example: For devices that present themselves as USB Mass storage For example: For devices that present themselves as USB Mass storage
devices, this method would be responsible for mounting the device or devices, this method would be responsible for mounting the device or
if the device has been automounted, for finding out where it has been if the device has been automounted, for finding out where it has been
mounted. The base class within USBMS device.py has a implementation of mounted. The method :meth:`calibre.devices.usbms.device.Device.open` has
an implementation of
this function that should serve as a good example for USB Mass storage this function that should serve as a good example for USB Mass storage
devices. devices.
''' '''
@ -219,17 +230,20 @@ class DevicePlugin(Plugin):
def set_progress_reporter(self, report_progress): def set_progress_reporter(self, report_progress):
''' '''
@param report_progress: Function that is called with a % progress :param report_progress: Function that is called with a % progress
(number between 0 and 100) for various tasks (number between 0 and 100) for various tasks
If it is called with -1 that means that the If it is called with -1 that means that the
task does not have any progress information task does not have any progress information
''' '''
raise NotImplementedError() raise NotImplementedError()
def get_device_information(self, end_session=True): def get_device_information(self, end_session=True):
""" """
Ask device for device information. See L{DeviceInfoQuery}. Ask device for device information. See L{DeviceInfoQuery}.
@return: (device name, device version, software version on device, mime type)
:return: (device name, device version, software version on device, mime type)
""" """
raise NotImplementedError() raise NotImplementedError()
@ -252,8 +266,9 @@ class DevicePlugin(Plugin):
2. Memory Card A 2. Memory Card A
3. Memory Card B 3. Memory Card B
@return: A 3 element list with total space in bytes of (1, 2, 3). If a :return: A 3 element list with total space in bytes of (1, 2, 3). If a
particular device doesn't have any of these locations it should return 0. particular device doesn't have any of these locations it should return 0.
""" """
raise NotImplementedError() raise NotImplementedError()
@ -264,19 +279,23 @@ class DevicePlugin(Plugin):
2. Card A 2. Card A
3. Card B 3. Card B
@return: A 3 element list with free space in bytes of (1, 2, 3). If a :return: A 3 element list with free space in bytes of (1, 2, 3). If a
particular device doesn't have any of these locations it should return -1. particular device doesn't have any of these locations it should return -1.
""" """
raise NotImplementedError() raise NotImplementedError()
def books(self, oncard=None, end_session=True): def books(self, oncard=None, end_session=True):
""" """
Return a list of ebooks on the device. Return a list of ebooks on the device.
@param oncard: If 'carda' or 'cardb' return a list of ebooks on the
:param oncard: If 'carda' or 'cardb' return a list of ebooks on the
specific storage card, otherwise return list of ebooks specific storage card, otherwise return list of ebooks
in main memory of device. If a card is specified and no in main memory of device. If a card is specified and no
books are on the card return empty list. books are on the card return empty list.
@return: A BookList.
:return: A BookList.
""" """
raise NotImplementedError() raise NotImplementedError()
@ -285,25 +304,27 @@ class DevicePlugin(Plugin):
''' '''
Upload a list of books to the device. If a file already Upload a list of books to the device. If a file already
exists on the device, it should be replaced. exists on the device, it should be replaced.
This method should raise a L{FreeSpaceError} if there is not enough This method should raise a :class:`FreeSpaceError` if there is not enough
free space on the device. The text of the FreeSpaceError must contain the free space on the device. The text of the FreeSpaceError must contain the
word "card" if C{on_card} is not None otherwise it must contain the word "memory". word "card" if ``on_card`` is not None otherwise it must contain the word "memory".
:files: A list of paths and/or file-like objects. If they are paths and
:param files: A list of paths and/or file-like objects. If they are paths and
the paths point to temporary files, they may have an additional the paths point to temporary files, they may have an additional
attribute, original_file_path pointing to the originals. They may have attribute, original_file_path pointing to the originals. They may have
another optional attribute, deleted_after_upload which if True means another optional attribute, deleted_after_upload which if True means
that the file pointed to by original_file_path will be deleted after that the file pointed to by original_file_path will be deleted after
being uploaded to the device. being uploaded to the device.
:names: A list of file names that the books should have :param names: A list of file names that the books should have
once uploaded to the device. len(names) == len(files) once uploaded to the device. len(names) == len(files)
:return: A list of 3-element tuples. The list is meant to be passed :param metadata: If not None, it is a list of :class:`MetaInformation` objects.
to L{add_books_to_metadata}.
:metadata: If not None, it is a list of :class:`MetaInformation` objects.
The idea is to use the metadata to determine where on the device to The idea is to use the metadata to determine where on the device to
put the book. len(metadata) == len(files). Apart from the regular put the book. len(metadata) == len(files). Apart from the regular
cover (path to cover), there may also be a thumbnail attribute, which should cover (path to cover), there may also be a thumbnail attribute, which should
be used in preference. The thumbnail attribute is of the form be used in preference. The thumbnail attribute is of the form
(width, height, cover_data as jpeg). (width, height, cover_data as jpeg).
:return: A list of 3-element tuples. The list is meant to be passed
to :meth:`add_books_to_metadata`.
''' '''
raise NotImplementedError() raise NotImplementedError()
@ -312,12 +333,15 @@ class DevicePlugin(Plugin):
''' '''
Add locations to the booklists. This function must not communicate with Add locations to the booklists. This function must not communicate with
the device. the device.
@param locations: Result of a call to L{upload_books}
@param metadata: List of MetaInformation objects, same as for :param locations: Result of a call to L{upload_books}
:method:`upload_books`. :param metadata: List of :class:`MetaInformation` objects, same as for
@param booklists: A tuple containing the result of calls to :meth:`upload_books`.
(L{books}(oncard=None), L{books}(oncard='carda'), :param booklists: A tuple containing the result of calls to
L{books}(oncard='cardb')). (:meth:`books(oncard=None)`,
:meth:`books(oncard='carda')`,
:meth`books(oncard='cardb')`).
''' '''
raise NotImplementedError raise NotImplementedError
@ -332,26 +356,35 @@ class DevicePlugin(Plugin):
''' '''
Remove books from the metadata list. This function must not communicate Remove books from the metadata list. This function must not communicate
with the device. with the device.
@param paths: paths to books on the device.
@param booklists: A tuple containing the result of calls to :param paths: paths to books on the device.
(L{books}(oncard=None), L{books}(oncard='carda'), :param booklists: A tuple containing the result of calls to
L{books}(oncard='cardb')). (:meth:`books(oncard=None)`,
:meth:`books(oncard='carda')`,
:meth`books(oncard='cardb')`).
''' '''
raise NotImplementedError() raise NotImplementedError()
def sync_booklists(self, booklists, end_session=True): def sync_booklists(self, booklists, end_session=True):
''' '''
Update metadata on device. Update metadata on device.
@param booklists: A tuple containing the result of calls to
(L{books}(oncard=None), L{books}(oncard='carda'), :param booklists: A tuple containing the result of calls to
L{books}(oncard='cardb')). (:meth:`books(oncard=None)`,
:meth:`books(oncard='carda')`,
:meth`books(oncard='cardb')`).
''' '''
raise NotImplementedError() raise NotImplementedError()
def get_file(self, path, outfile, end_session=True): def get_file(self, path, outfile, end_session=True):
''' '''
Read the file at C{path} on the device and write it to outfile. Read the file at ``path`` on the device and write it to outfile.
@param outfile: file object like C{sys.stdout} or the result of an C{open} call
:param outfile: file object like ``sys.stdout`` or the result of an
:func:`open` call.
''' '''
raise NotImplementedError() raise NotImplementedError()
@ -365,8 +398,8 @@ class DevicePlugin(Plugin):
@classmethod @classmethod
def save_settings(cls, settings_widget): def save_settings(cls, settings_widget):
''' '''
Should save settings to disk. Takes the widget created in config_widget Should save settings to disk. Takes the widget created in
and saves all settings to disk. :meth:`config_widget` and saves all settings to disk.
''' '''
raise NotImplementedError() raise NotImplementedError()
@ -381,16 +414,18 @@ class DevicePlugin(Plugin):
class BookList(list): class BookList(list):
''' '''
A list of books. Each Book object must have the fields: A list of books. Each Book object must have the fields
1. title
2. authors #. title
3. size (file size of the book) #. authors
4. datetime (a UTC time tuple) #. size (file size of the book)
5. path (path on the device to the book) #. datetime (a UTC time tuple)
6. thumbnail (can be None) thumbnail is either a str/bytes object with the #. path (path on the device to the book)
#. thumbnail (can be None) thumbnail is either a str/bytes object with the
image data or it should have an attribute image_path that stores an image data or it should have an attribute image_path that stores an
absolute (platform native) path to the image absolute (platform native) path to the image
7. tags (a list of strings, can be empty). #. tags (a list of strings, can be empty).
''' '''
__getslice__ = None __getslice__ = None
@ -427,6 +462,7 @@ class BookList(list):
created from series, in which case series_index is used. created from series, in which case series_index is used.
:param collection_attributes: A list of attributes of the Book object :param collection_attributes: A list of attributes of the Book object
''' '''
raise NotImplementedError() raise NotImplementedError()

View File

@ -84,7 +84,7 @@ class Book(MetaInformation):
def thumbnail(self): def thumbnail(self):
return None return None
def smart_update(self, other): def smart_update(self, other, replace_metadata=False):
''' '''
Merge the information in C{other} into self. In case of conflicts, the information Merge the information in C{other} into self. In case of conflicts, the information
in C{other} takes precedence, unless the information in C{other} is NULL. in C{other} takes precedence, unless the information in C{other} is NULL.

View File

@ -38,6 +38,8 @@ class KOBO(USBMS):
EBOOK_DIR_MAIN = '' EBOOK_DIR_MAIN = ''
SUPPORTS_SUB_DIRS = True SUPPORTS_SUB_DIRS = True
VIRTUAL_BOOK_EXTENSIONS = frozenset(['kobo'])
def initialize(self): def initialize(self):
USBMS.initialize(self) USBMS.initialize(self)
self.book_class = Book self.book_class = Book
@ -355,3 +357,17 @@ class KOBO(USBMS):
# print "Internal: " + filename # print "Internal: " + filename
return path return path
def get_file(self, path, *args, **kwargs):
tpath = self.munge_path(path)
extension = os.path.splitext(tpath)[1]
if extension == '.kobo':
from calibre.devices.errors import UserFeedback
raise UserFeedback(_("Not Implemented"),
_('".kobo" files do not exist on the device as books '
'instead, they are rows in the sqlite database. '
'Currently they cannot be exported or viewed.'),
UserFeedback.WARN)
return USBMS.get_file(self, path, *args, **kwargs)

View File

@ -6,6 +6,8 @@ __license__ = 'GPL v3'
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>' __copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
import os
from calibre.devices.usbms.driver import USBMS from calibre.devices.usbms.driver import USBMS
class PALMPRE(USBMS): class PALMPRE(USBMS):
@ -44,12 +46,13 @@ class AVANT(USBMS):
BCD = [0x0319] BCD = [0x0319]
VENDOR_NAME = 'E-BOOK' VENDOR_NAME = 'E-BOOK'
WINDOWS_MAIN_MEM = 'READER' WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = 'READER'
EBOOK_DIR_MAIN = '' EBOOK_DIR_MAIN = ''
SUPPORTS_SUB_DIRS = True SUPPORTS_SUB_DIRS = True
class SWEEX(USBMS): class SWEEX(USBMS):
# Identical to the Promedia
name = 'Sweex Device Interface' name = 'Sweex Device Interface'
gui_name = 'Sweex' gui_name = 'Sweex'
description = _('Communicate with the Sweex MM300') description = _('Communicate with the Sweex MM300')
@ -83,7 +86,17 @@ class PDNOVEL(USBMS):
VENDOR_NAME = 'ANDROID' VENDOR_NAME = 'ANDROID'
WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = '__UMS_COMPOSITE' WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = '__UMS_COMPOSITE'
THUMBNAIL_HEIGHT = 144
EBOOK_DIR_MAIN = 'eBooks' EBOOK_DIR_MAIN = 'eBooks'
SUPPORTS_SUB_DIRS = False SUPPORTS_SUB_DIRS = False
DELETE_EXTS = ['.jpg', '.jpeg', '.png']
def upload_cover(self, path, filename, metadata):
coverdata = getattr(metadata, 'thumbnail', None)
if coverdata and coverdata[2]:
with open('%s.jpg' % os.path.join(path, filename), 'wb') as coverfile:
coverfile.write(coverdata[2])

View File

@ -26,7 +26,7 @@ class NOOK(USBMS):
# Ordered list of supported formats # Ordered list of supported formats
FORMATS = ['epub', 'pdb', 'pdf'] FORMATS = ['epub', 'pdb', 'pdf']
VENDOR_ID = [0x2080] VENDOR_ID = [0x2080, 0x18d1] # 0x18d1 is for softrooted nook
PRODUCT_ID = [0x001] PRODUCT_ID = [0x001]
BCD = [0x322] BCD = [0x322]

View File

@ -46,7 +46,11 @@ def strptime(src):
return time.strptime(' '.join(src), '%w, %d %m %Y %H:%M:%S %Z') return time.strptime(' '.join(src), '%w, %d %m %Y %H:%M:%S %Z')
def strftime(epoch, zone=time.localtime): def strftime(epoch, zone=time.localtime):
try:
src = time.strftime("%w, %d %m %Y %H:%M:%S GMT", zone(epoch)).split() src = time.strftime("%w, %d %m %Y %H:%M:%S GMT", zone(epoch)).split()
except:
src = time.strftime("%w, %d %m %Y %H:%M:%S GMT", zone()).split()
src[0] = INVERSE_DAY_MAP[int(src[0][:-1])]+',' src[0] = INVERSE_DAY_MAP[int(src[0][:-1])]+','
src[2] = INVERSE_MONTH_MAP[int(src[2])] src[2] = INVERSE_MONTH_MAP[int(src[2])]
return ' '.join(src) return ' '.join(src)
@ -328,7 +332,10 @@ class XMLCache(object):
'descendant::*[local-name()="jpeg"]|' 'descendant::*[local-name()="jpeg"]|'
'descendant::*[local-name()="png"]'): 'descendant::*[local-name()="png"]'):
if img.text: if img.text:
try:
raw = b64decode(img.text.strip()) raw = b64decode(img.text.strip())
except:
continue
book.thumbnail = raw book.thumbnail = raw
break break
break break

View File

@ -55,7 +55,7 @@ class WinPNPScanner(object):
def drive_order(self, pnp_id): def drive_order(self, pnp_id):
order = 0 order = 0
match = re.search(r'REV_.*?&(\d+)', pnp_id) match = re.search(r'REV_.*?&(\d+)#', pnp_id)
if match is not None: if match is not None:
order = int(match.group(1)) order = int(match.group(1))
return order return order

View File

@ -47,8 +47,8 @@ class Device(DeviceConfig, DevicePlugin):
''' '''
This class provides logic common to all drivers for devices that export themselves This class provides logic common to all drivers for devices that export themselves
as USB Mass Storage devices. If you are writing such a driver, inherit from this as USB Mass Storage devices. Provides implementations for mounting/ejecting
class. of USBMS devices on all platforms.
''' '''
VENDOR_ID = 0x0 VENDOR_ID = 0x0
@ -57,9 +57,19 @@ class Device(DeviceConfig, DevicePlugin):
VENDOR_NAME = None VENDOR_NAME = None
# These can be None, string, list of strings or compiled regex #: String identifying the main memory of the device in the windows PnP id
#: strings
#: This can be None, string, list of strings or compiled regex
WINDOWS_MAIN_MEM = None WINDOWS_MAIN_MEM = None
#: String identifying the first card of the device in the windows PnP id
#: strings
#: This can be None, string, list of strings or compiled regex
WINDOWS_CARD_A_MEM = None WINDOWS_CARD_A_MEM = None
#: String identifying the second card of the device in the windows PnP id
#: strings
#: This can be None, string, list of strings or compiled regex
WINDOWS_CARD_B_MEM = None WINDOWS_CARD_B_MEM = None
# The following are used by the check_ioreg_line method and can be either: # The following are used by the check_ioreg_line method and can be either:
@ -68,9 +78,9 @@ class Device(DeviceConfig, DevicePlugin):
OSX_CARD_A_MEM = None OSX_CARD_A_MEM = None
OSX_CARD_B_MEM = None OSX_CARD_B_MEM = None
# Used by the new driver detection to disambiguate main memory from #: Used by the new driver detection to disambiguate main memory from
# storage cards. Should be a regular expression that matches the #: storage cards. Should be a regular expression that matches the
# main memory mount point assigned by OS X #: main memory mount point assigned by OS X
OSX_MAIN_MEM_VOL_PAT = None OSX_MAIN_MEM_VOL_PAT = None
OSX_EJECT_COMMAND = ['diskutil', 'eject'] OSX_EJECT_COMMAND = ['diskutil', 'eject']
@ -780,7 +790,7 @@ class Device(DeviceConfig, DevicePlugin):
def filename_callback(self, default, mi): def filename_callback(self, default, mi):
''' '''
Callback to allow drivers to change the default file name Callback to allow drivers to change the default file name
set by :method:`create_upload_path`. set by :meth:`create_upload_path`.
''' '''
return default return default

View File

@ -33,6 +33,10 @@ def debug_print(*args):
# CLI must come before Device as it implements the CLI functions that # CLI must come before Device as it implements the CLI functions that
# are inherited from the device interface in Device. # are inherited from the device interface in Device.
class USBMS(CLI, Device): class USBMS(CLI, Device):
'''
The base class for all USBMS devices. Implements the logic for
sending/getting/updating metadata/caching metadata/etc.
'''
description = _('Communicate with an eBook reader.') description = _('Communicate with an eBook reader.')
author = _('John Schember') author = _('John Schember')
@ -195,10 +199,13 @@ class USBMS(CLI, Device):
def upload_cover(self, path, filename, metadata): def upload_cover(self, path, filename, metadata):
''' '''
:path: the full path were the associated book is located. Upload book cover to the device. Default implementation does nothing.
:filename: the name of the book file without the extension.
:metadata: metadata belonging to the book. Use metadata.thumbnail :param path: the full path were the associated book is located.
:param filename: the name of the book file without the extension.
:param metadata: metadata belonging to the book. Use metadata.thumbnail
for cover for cover
''' '''
pass pass

View File

@ -177,6 +177,7 @@ class CHMInput(InputFormatPlugin):
chapter_path = None chapter_path = None
if match_string(node.tag, 'object') and match_string(node.attrib['type'], 'text/sitemap'): if match_string(node.tag, 'object') and match_string(node.attrib['type'], 'text/sitemap'):
chapter_title = None
for child in node: for child in node:
if match_string(child.tag,'param') and match_string(child.attrib['name'], 'name'): if match_string(child.tag,'param') and match_string(child.attrib['name'], 'name'):
chapter_title = child.attrib['value'] chapter_title = child.attrib['value']

View File

@ -8,7 +8,6 @@ Based on ideas from comiclrf created by FangornUK.
''' '''
import os, shutil, traceback, textwrap, time, codecs import os, shutil, traceback, textwrap, time, codecs
from ctypes import byref
from Queue import Empty from Queue import Empty
from calibre.customize.conversion import InputFormatPlugin, OptionRecommendation from calibre.customize.conversion import InputFormatPlugin, OptionRecommendation
@ -71,60 +70,44 @@ class PageProcessor(list):
def render(self): def render(self):
import calibre.utils.PythonMagickWand as pw from calibre.utils.magick import Image
img = pw.NewMagickWand() img = Image()
if img < 0: img.open(self.path_to_page)
raise RuntimeError('Cannot create wand.') width, height = img.size
if not pw.MagickReadImage(img, self.path_to_page):
severity = pw.ExceptionType(0)
msg = pw.MagickGetException(img, byref(severity))
raise IOError('Failed to read image from: %s: %s'
%(self.path_to_page, msg))
width = pw.MagickGetImageWidth(img)
height = pw.MagickGetImageHeight(img)
if self.num == 0: # First image so create a thumbnail from it if self.num == 0: # First image so create a thumbnail from it
thumb = pw.CloneMagickWand(img) thumb = img.clone
if thumb < 0: thumb.thumbnail(60, 80)
raise RuntimeError('Cannot create wand.') thumb.save(os.path.join(self.dest, 'thumbnail.png'))
pw.MagickThumbnailImage(thumb, 60, 80)
pw.MagickWriteImage(thumb, os.path.join(self.dest, 'thumbnail.png'))
pw.DestroyMagickWand(thumb)
self.pages = [img] self.pages = [img]
if width > height: if width > height:
if self.opts.landscape: if self.opts.landscape:
self.rotate = True self.rotate = True
else: else:
split1, split2 = map(pw.CloneMagickWand, (img, img)) split1, split2 = img.clone, img.clone
pw.DestroyMagickWand(img) half = int(width/2)
if split1 < 0 or split2 < 0: split1.crop(half-1, height, 0, 0)
raise RuntimeError('Cannot create wand.') split2.crop(half-1, height, half, 0)
pw.MagickCropImage(split1, (width/2)-1, height, 0, 0)
pw.MagickCropImage(split2, (width/2)-1, height, width/2, 0 )
self.pages = [split2, split1] if self.opts.right2left else [split1, split2] self.pages = [split2, split1] if self.opts.right2left else [split1, split2]
self.process_pages() self.process_pages()
def process_pages(self): def process_pages(self):
import calibre.utils.PythonMagickWand as p from calibre.utils.magick import PixelWand
for i, wand in enumerate(self.pages): for i, wand in enumerate(self.pages):
pw = p.NewPixelWand() pw = PixelWand()
try: pw.color = 'white'
if pw < 0:
raise RuntimeError('Cannot create wand.')
p.PixelSetColor(pw, 'white')
p.MagickSetImageBorderColor(wand, pw) wand.set_border_color(pw)
if self.rotate: if self.rotate:
p.MagickRotateImage(wand, pw, -90) wand.rotate(pw, -90)
# 25 percent fuzzy trim? # 25 percent fuzzy trim?
if not self.opts.disable_trim: if not self.opts.disable_trim:
p.MagickTrimImage(wand, 25*65535/100) wand.trim(25*65535/100)
p.MagickSetImagePage(wand, 0,0,0,0) #Clear page after trim, like a "+repage" wand.set_page(0, 0, 0, 0) #Clear page after trim, like a "+repage"
# Do the Photoshop "Auto Levels" equivalent # Do the Photoshop "Auto Levels" equivalent
if not self.opts.dont_normalize: if not self.opts.dont_normalize:
p.MagickNormalizeImage(wand) wand.normalize()
sizex = p.MagickGetImageWidth(wand) sizex, sizey = wand.size
sizey = p.MagickGetImageHeight(wand)
SCRWIDTH, SCRHEIGHT = self.opts.output_profile.comic_screen_size SCRWIDTH, SCRHEIGHT = self.opts.output_profile.comic_screen_size
@ -141,9 +124,9 @@ class PageProcessor(list):
newsizey = int(newsizex / aspect) newsizey = int(newsizex / aspect)
deltax = 0 deltax = 0
deltay = (SCRHEIGHT - newsizey) / 2 deltay = (SCRHEIGHT - newsizey) / 2
p.MagickResizeImage(wand, newsizex, newsizey, p.CatromFilter, 1.0) wand.size = (newsizex, newsizey)
p.MagickSetImageBorderColor(wand, pw) wand.set_border_color(pw)
p.MagickBorderImage(wand, pw, deltax, deltay) wand.add_border(pw, deltax, deltay)
elif self.opts.wide: elif self.opts.wide:
# Keep aspect and Use device height as scaled image width so landscape mode is clean # Keep aspect and Use device height as scaled image width so landscape mode is clean
aspect = float(sizex) / float(sizey) aspect = float(sizex) / float(sizey)
@ -162,39 +145,33 @@ class PageProcessor(list):
newsizey = int(newsizex / aspect) newsizey = int(newsizex / aspect)
deltax = 0 deltax = 0
deltay = (wscreeny - newsizey) / 2 deltay = (wscreeny - newsizey) / 2
p.MagickResizeImage(wand, newsizex, newsizey, p.CatromFilter, 1.0) wand.size = (newsizex, newsizey)
p.MagickSetImageBorderColor(wand, pw) wand.set_border_color(pw)
p.MagickBorderImage(wand, pw, deltax, deltay) wand.add_border(pw, deltax, deltay)
else: else:
p.MagickResizeImage(wand, SCRWIDTH, SCRHEIGHT, p.CatromFilter, 1.0) wand.size = (SCRWIDTH, SCRHEIGHT)
if not self.opts.dont_sharpen: if not self.opts.dont_sharpen:
p.MagickSharpenImage(wand, 0.0, 1.0) wand.sharpen(0.0, 1.0)
if not self.opts.dont_grayscale: if not self.opts.dont_grayscale:
p.MagickSetImageType(wand, p.GrayscaleType) wand.type = 'GrayscaleType'
if self.opts.despeckle: if self.opts.despeckle:
p.MagickDespeckleImage(wand) wand.despeckle()
p.MagickQuantizeImage(wand, self.opts.colors, p.RGBColorspace, 0, 1, 0) wand.quantize(self.opts.colors)
dest = '%d_%d.%s'%(self.num, i, self.opts.output_format) dest = '%d_%d.%s'%(self.num, i, self.opts.output_format)
dest = os.path.join(self.dest, dest) dest = os.path.join(self.dest, dest)
p.MagickWriteImage(wand, dest+'8') wand.save(dest+'8')
os.rename(dest+'8', dest) os.rename(dest+'8', dest)
self.append(dest) self.append(dest)
finally:
if pw > 0:
p.DestroyPixelWand(pw)
p.DestroyMagickWand(wand)
def render_pages(tasks, dest, opts, notification=lambda x, y: x): def render_pages(tasks, dest, opts, notification=lambda x, y: x):
''' '''
Entry point for the job server. Entry point for the job server.
''' '''
failures, pages = [], [] failures, pages = [], []
from calibre.utils.PythonMagickWand import ImageMagick
with ImageMagick():
for num, path in tasks: for num, path in tasks:
try: try:
pages.extend(PageProcessor(path, dest, opts, num)) pages.extend(PageProcessor(path, dest, opts, num))
@ -226,9 +203,6 @@ def process_pages(pages, opts, update, tdir):
''' '''
Render all identified comic pages. Render all identified comic pages.
''' '''
from calibre.utils.PythonMagickWand import ImageMagick
ImageMagick
progress = Progress(len(pages), update) progress = Progress(len(pages), update)
server = Server() server = Server()
jobs = [] jobs = []

View File

@ -46,6 +46,7 @@ def authors_to_sort_string(authors):
return ' & '.join(map(author_to_author_sort, authors)) return ' & '.join(map(author_to_author_sort, authors))
_title_pat = re.compile('^(A|The|An)\s+', re.IGNORECASE) _title_pat = re.compile('^(A|The|An)\s+', re.IGNORECASE)
def title_sort(title): def title_sort(title):
match = _title_pat.search(title) match = _title_pat.search(title)
if match: if match:

View File

@ -82,7 +82,7 @@ class Metadata(object):
def print_all_attributes(self): def print_all_attributes(self):
pass pass
def smart_update(self, other): def smart_update(self, other, replace_metadata=False):
pass pass
def format_series_index(self): def format_series_index(self):

View File

@ -5,11 +5,253 @@ __license__ = 'GPL v3'
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>' __copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
import traceback, socket, re, sys
from functools import partial
from threading import Thread, Event
from Queue import Queue, Empty
import mechanize
from calibre.customize import Plugin from calibre.customize import Plugin
from calibre import browser, prints
from calibre.ebooks.BeautifulSoup import BeautifulSoup
from calibre.constants import preferred_encoding, DEBUG
class CoverDownload(Plugin): class CoverDownload(Plugin):
'''
These plugins are used to download covers for books.
'''
supported_platforms = ['windows', 'osx', 'linux'] supported_platforms = ['windows', 'osx', 'linux']
author = 'Kovid Goyal' author = 'Kovid Goyal'
type = _('Cover download') type = _('Cover download')
def has_cover(self, mi, ans, timeout=5.):
'''
Check if the book described by mi has a cover. Call ans.set() if it
does. Do nothing if it doesn't.
:param mi: MetaInformation object
:param timeout: timeout in seconds
:param ans: A threading.Event object
'''
raise NotImplementedError()
def get_covers(self, mi, result_queue, abort, timeout=5.):
'''
Download covers for books described by the mi object. Downloaded covers
must be put into the result_queue. If more than one cover is available,
the plugin should continue downloading them and putting them into
result_queue until abort.is_set() returns True.
:param mi: MetaInformation object
:param result_queue: A multithreaded Queue
:param abort: A threading.Event object
:param timeout: timeout in seconds
'''
raise NotImplementedError()
def exception_to_string(self, ex):
try:
return unicode(ex)
except:
try:
return str(ex).decode(preferred_encoding, 'replace')
except:
return repr(ex)
def debug(self, *args, **kwargs):
if DEBUG:
prints('\t'+self.name+':', *args, **kwargs)
class HeadRequest(mechanize.Request):
def get_method(self):
return 'HEAD'
class OpenLibraryCovers(CoverDownload): # {{{
'Download covers from openlibrary.org'
OPENLIBRARY = 'http://covers.openlibrary.org/b/isbn/%s-L.jpg?default=false'
name = 'openlibrary.org covers'
description = _('Download covers from openlibrary.org')
author = 'Kovid Goyal'
def has_cover(self, mi, ans, timeout=5.):
if not mi.isbn:
return False
br = browser()
br.set_handle_redirect(False)
try:
br.open_novisit(HeadRequest(self.OPENLIBRARY%mi.isbn), timeout=timeout)
self.debug('cover for', mi.isbn, 'found')
ans.set()
except Exception, e:
if callable(getattr(e, 'getcode', None)) and e.getcode() == 302:
self.debug('cover for', mi.isbn, 'found')
ans.set()
else:
self.debug(e)
def get_covers(self, mi, result_queue, abort, timeout=5.):
if not mi.isbn:
return
br = browser()
try:
ans = br.open(self.OPENLIBRARY%mi.isbn, timeout=timeout).read()
result_queue.put((True, ans, 'jpg', self.name))
except Exception, e:
if callable(getattr(e, 'getcode', None)) and e.getcode() == 404:
result_queue.put((False, _('ISBN: %s not found')%mi.isbn, '', self.name))
else:
result_queue.put((False, self.exception_to_string(e),
traceback.format_exc(), self.name))
# }}}
class LibraryThingCovers(CoverDownload): # {{{
name = 'librarything.com covers'
description = _('Download covers from librarything.com')
author = 'Kovid Goyal'
LIBRARYTHING = 'http://www.librarything.com/isbn/'
def get_cover_url(self, isbn, br, timeout=5.):
try:
src = br.open_novisit('http://www.librarything.com/isbn/'+isbn,
timeout=timeout).read().decode('utf-8', 'replace')
except Exception, err:
if isinstance(getattr(err, 'args', [None])[0], socket.timeout):
err = Exception(_('LibraryThing.com timed out. Try again later.'))
raise err
else:
s = BeautifulSoup(src)
url = s.find('td', attrs={'class':'left'})
if url is None:
if s.find('div', attrs={'class':'highloadwarning'}) is not None:
raise Exception(_('Could not fetch cover as server is experiencing high load. Please try again later.'))
raise Exception(_('ISBN: %s not found')%isbn)
url = url.find('img')
if url is None:
raise Exception(_('LibraryThing.com server error. Try again later.'))
url = re.sub(r'_S[XY]\d+', '', url['src'])
return url
def has_cover(self, mi, ans, timeout=5.):
if not mi.isbn:
return False
br = browser()
try:
self.get_cover_url(mi.isbn, br, timeout=timeout)
self.debug('cover for', mi.isbn, 'found')
ans.set()
except Exception, e:
self.debug(e)
def get_covers(self, mi, result_queue, abort, timeout=5.):
if not mi.isbn:
return
br = browser()
try:
url = self.get_cover_url(mi.isbn, br, timeout=timeout)
cover_data = br.open_novisit(url).read()
result_queue.put((True, cover_data, 'jpg', self.name))
except Exception, e:
result_queue.put((False, self.exception_to_string(e),
traceback.format_exc(), self.name))
# }}}
def check_for_cover(mi, timeout=5.): # {{{
from calibre.customize.ui import cover_sources
ans = Event()
checkers = [partial(p.has_cover, mi, ans, timeout=timeout) for p in
cover_sources()]
workers = [Thread(target=c) for c in checkers]
for w in workers:
w.daemon = True
w.start()
while not ans.is_set():
ans.wait(0.1)
if sum([int(w.is_alive()) for w in workers]) == 0:
break
return ans.is_set()
# }}}
def download_covers(mi, result_queue, max_covers=50, timeout=5.): # {{{
from calibre.customize.ui import cover_sources
abort = Event()
temp = Queue()
getters = [partial(p.get_covers, mi, temp, abort, timeout=timeout) for p in
cover_sources()]
workers = [Thread(target=c) for c in getters]
for w in workers:
w.daemon = True
w.start()
count = 0
while count < max_covers:
try:
result = temp.get_nowait()
if result[0]:
count += 1
result_queue.put(result)
except Empty:
pass
if sum([int(w.is_alive()) for w in workers]) == 0:
break
abort.set()
while True:
try:
result = temp.get_nowait()
count += 1
result_queue.put(result)
except Empty:
break
# }}}
def download_cover(mi, timeout=5.): # {{{
results = Queue()
download_covers(mi, results, max_covers=1, timeout=timeout)
errors, ans = [], None
while True:
try:
x = results.get_nowait()
if x[0]:
ans = x[1]
else:
errors.append(x)
except Empty:
break
return ans, errors
# }}}
def test(isbns): # {{{
from calibre.ebooks.metadata import MetaInformation
mi = MetaInformation('test', ['test'])
for isbn in isbns:
prints('Testing ISBN:', isbn)
mi.isbn = isbn
found = check_for_cover(mi)
prints('Has cover:', found)
ans, errors = download_cover(mi)
if ans is not None:
prints('Cover downloaded')
else:
prints('Download failed:')
for err in errors:
prints('\t', err[-1]+':', err[1])
print '\n'
# }}}
if __name__ == '__main__':
isbns = sys.argv[1:] + ['9781591025412', '9780307272119']
test(isbns)

View File

@ -5,7 +5,7 @@ __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
'''Read meta information from epub files''' '''Read meta information from epub files'''
import os, re import os, re, posixpath, shutil
from cStringIO import StringIO from cStringIO import StringIO
from contextlib import closing from contextlib import closing
@ -13,7 +13,7 @@ from calibre.utils.zipfile import ZipFile, BadZipfile, safe_replace
from calibre.ebooks.BeautifulSoup import BeautifulStoneSoup from calibre.ebooks.BeautifulSoup import BeautifulStoneSoup
from calibre.ebooks.metadata import MetaInformation from calibre.ebooks.metadata import MetaInformation
from calibre.ebooks.metadata.opf2 import OPF from calibre.ebooks.metadata.opf2 import OPF
from calibre.ptempfile import TemporaryDirectory from calibre.ptempfile import TemporaryDirectory, PersistentTemporaryFile
from calibre import CurrentDir from calibre import CurrentDir
class EPubException(Exception): class EPubException(Exception):
@ -126,7 +126,6 @@ class OCFDirReader(OCFReader):
return open(os.path.join(self.root, path), *args, **kwargs) return open(os.path.join(self.root, path), *args, **kwargs)
def get_cover(opf, opf_path, stream, reader=None): def get_cover(opf, opf_path, stream, reader=None):
import posixpath
from calibre.ebooks import render_html_svg_workaround from calibre.ebooks import render_html_svg_workaround
from calibre.utils.logging import default_log from calibre.utils.logging import default_log
raster_cover = opf.raster_cover raster_cover = opf.raster_cover
@ -185,7 +184,45 @@ def get_quick_metadata(stream):
def set_metadata(stream, mi, apply_null=False, update_timestamp=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())
raster_cover = reader.opf.raster_cover
mi = MetaInformation(mi) mi = MetaInformation(mi)
new_cdata = None
replacements = {}
try:
new_cdata = mi.cover_data[1]
if not new_cdata:
raise Exception('no cover')
except:
try:
new_cdata = open(mi.cover, 'rb').read()
except:
import traceback
traceback.print_exc()
if new_cdata and raster_cover:
try:
cpath = posixpath.join(posixpath.dirname(reader.opf_path),
raster_cover)
cover_replacable = not reader.encryption_meta.is_encrypted(cpath) and \
os.path.splitext(cpath)[1].lower() in ('.png', '.jpg', '.jpeg')
if cover_replacable:
from calibre.utils.magick.draw import save_cover_data_to, \
identify
new_cover = PersistentTemporaryFile(suffix=os.path.splitext(cpath)[1])
resize_to = None
if False: # Resize new cover to same size as old cover
shutil.copyfileobj(reader.open(cpath), new_cover)
new_cover.close()
width, height, fmt = identify(new_cover.name)
resize_to = (width, height)
else:
new_cover.close()
save_cover_data_to(new_cdata, new_cover.name,
resize_to=resize_to)
replacements[cpath] = open(new_cover.name, 'rb')
except:
import traceback
traceback.print_exc()
for x in ('guide', 'toc', 'manifest', 'spine'): for x in ('guide', 'toc', 'manifest', 'spine'):
setattr(mi, x, None) setattr(mi, x, None)
reader.opf.smart_update(mi) reader.opf.smart_update(mi)
@ -200,5 +237,6 @@ def set_metadata(stream, mi, apply_null=False, update_timestamp=False):
reader.opf.timestamp = mi.timestamp 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,
extra_replacements=replacements)

View File

@ -10,11 +10,27 @@ from calibre import prints
from calibre.utils.config import OptionParser from calibre.utils.config import OptionParser
from calibre.utils.logging import default_log from calibre.utils.logging import default_log
from calibre.customize import Plugin from calibre.customize import Plugin
from calibre.ebooks.metadata.library_thing import check_for_cover from calibre.ebooks.metadata.covers import check_for_cover
metadata_config = None metadata_config = None
class MetadataSource(Plugin): # {{{ class MetadataSource(Plugin): # {{{
'''
Represents a source to query for metadata. Subclasses must implement
at least the fetch method.
When :meth:`fetch` is called, the `self` object will have the following
useful attributes (each of which may be None)::
title, book_author, publisher, isbn, log, verbose and extra
Use these attributes to construct the search query. extra is reserved for
future use.
The fetch method must store the results in `self.results` as a list of
:class:`MetaInformation` objects. If there is an error, it should be stored
in `self.exception` and `self.tb` (for the traceback).
'''
author = 'Kovid Goyal' author = 'Kovid Goyal'
@ -273,9 +289,8 @@ def filter_metadata_results(item):
def do_cover_check(item): def do_cover_check(item):
item.has_cover = False item.has_cover = False
if item.isbn:
try: try:
item.has_cover = check_for_cover(item.isbn) item.has_cover = check_for_cover(item)
except: except:
pass # Cover not found pass # Cover not found

View File

@ -12,11 +12,15 @@ import re
from calibre.ebooks.metadata import MetaInformation from calibre.ebooks.metadata import MetaInformation
from calibre.ebooks.chardet import xml_to_unicode from calibre.ebooks.chardet import xml_to_unicode
from calibre import entity_to_unicode from calibre import entity_to_unicode
from calibre.utils.date import parse_date
def get_metadata(stream): def get_metadata(stream):
src = stream.read() src = stream.read()
return get_metadata_(src) return get_metadata_(src)
def get_meta_regexp_(name):
return re.compile('<meta name=[\'"]' + name + '[\'"] content=[\'"](.+?)[\'"]\s*/?>', re.IGNORECASE)
def get_metadata_(src, encoding=None): def get_metadata_(src, encoding=None):
if not isinstance(src, unicode): if not isinstance(src, unicode):
if not encoding: if not encoding:
@ -24,6 +28,9 @@ def get_metadata_(src, encoding=None):
else: else:
src = src.decode(encoding, 'replace') src = src.decode(encoding, 'replace')
# Meta data definitions as in
# http://www.mobileread.com/forums/showpost.php?p=712544&postcount=9
# Title # Title
title = None title = None
pat = re.compile(r'<!--.*?TITLE=(?P<q>[\'"])(.+?)(?P=q).*?-->', re.DOTALL) pat = re.compile(r'<!--.*?TITLE=(?P<q>[\'"])(.+?)(?P=q).*?-->', re.DOTALL)
@ -35,6 +42,13 @@ def get_metadata_(src, encoding=None):
match = pat.search(src) match = pat.search(src)
if match: if match:
title = match.group(1) title = match.group(1)
if not title:
for x in ('Title','DC.title','DCTERMS.title'):
pat = get_meta_regexp_(x)
match = pat.search(src)
if match:
title = match.group(1)
break
# Author # Author
author = None author = None
@ -42,7 +56,15 @@ def get_metadata_(src, encoding=None):
match = pat.search(src) match = pat.search(src)
if match: if match:
author = match.group(2).replace(',', ';') author = match.group(2).replace(',', ';')
else:
for x in ('Author','DC.creator.aut','DCTERMS.creator.aut'):
pat = get_meta_regexp_(x)
match = pat.search(src)
if match:
author = match.group(1)
break
# Create MetaInformation with Title and Author
ent_pat = re.compile(r'&(\S+)?;') ent_pat = re.compile(r'&(\S+)?;')
if title: if title:
title = ent_pat.sub(entity_to_unicode, title) title = ent_pat.sub(entity_to_unicode, title)
@ -51,18 +73,158 @@ def get_metadata_(src, encoding=None):
mi = MetaInformation(title, [author] if author else None) mi = MetaInformation(title, [author] if author else None)
# Publisher # Publisher
publisher = None
pat = re.compile(r'<!--.*?PUBLISHER=(?P<q>[\'"])(.+?)(?P=q).*?-->', re.DOTALL) pat = re.compile(r'<!--.*?PUBLISHER=(?P<q>[\'"])(.+?)(?P=q).*?-->', re.DOTALL)
match = pat.search(src) match = pat.search(src)
if match: if match:
mi.publisher = match.group(2) publisher = match.group(2)
else:
for x in ('Publisher','DC.publisher','DCTERMS.publisher'):
pat = get_meta_regexp_(x)
match = pat.search(src)
if match:
publisher = match.group(1)
break
if publisher:
mi.publisher = ent_pat.sub(entity_to_unicode, publisher)
# ISBN # ISBN
isbn = None
pat = re.compile(r'<!--.*?ISBN=[\'"]([^"\']+)[\'"].*?-->', re.DOTALL) pat = re.compile(r'<!--.*?ISBN=[\'"]([^"\']+)[\'"].*?-->', re.DOTALL)
match = pat.search(src) match = pat.search(src)
if match: if match:
isbn = match.group(1) isbn = match.group(1)
else:
for x in ('ISBN','DC.identifier.ISBN','DCTERMS.identifier.ISBN'):
pat = get_meta_regexp_(x)
match = pat.search(src)
if match:
isbn = match.group(1)
break
if isbn:
mi.isbn = re.sub(r'[^0-9xX]', '', isbn) mi.isbn = re.sub(r'[^0-9xX]', '', isbn)
# LANGUAGE
language = None
pat = re.compile(r'<!--.*?LANGUAGE=[\'"]([^"\']+)[\'"].*?-->', re.DOTALL)
match = pat.search(src)
if match:
language = match.group(1)
else:
for x in ('DC.language','DCTERMS.language'):
pat = get_meta_regexp_(x)
match = pat.search(src)
if match:
language = match.group(1)
break
if language:
mi.language = language
# PUBDATE
pubdate = None
pat = re.compile(r'<!--.*?PUBDATE=[\'"]([^"\']+)[\'"].*?-->', re.DOTALL)
match = pat.search(src)
if match:
pubdate = match.group(1)
else:
for x in ('Pubdate','Date of publication','DC.date.published','DC.date.publication','DC.date.issued','DCTERMS.issued'):
pat = get_meta_regexp_(x)
match = pat.search(src)
if match:
pubdate = match.group(1)
break
if pubdate:
try:
mi.pubdate = parse_date(pubdate)
except:
pass
# TIMESTAMP
timestamp = None
pat = re.compile(r'<!--.*?TIMESTAMP=[\'"]([^"\']+)[\'"].*?-->', re.DOTALL)
match = pat.search(src)
if match:
timestamp = match.group(1)
else:
for x in ('Timestamp','Date of creation','DC.date.created','DC.date.creation','DCTERMS.created'):
pat = get_meta_regexp_(x)
match = pat.search(src)
if match:
timestamp = match.group(1)
break
if timestamp:
try:
mi.timestamp = parse_date(timestamp)
except:
pass
# SERIES
series = None
pat = re.compile(r'<!--.*?SERIES=[\'"]([^"\']+)[\'"].*?-->', re.DOTALL)
match = pat.search(src)
if match:
series = match.group(1)
else:
pat = get_meta_regexp_("Series")
match = pat.search(src)
if match:
series = match.group(1)
if series:
mi.series = ent_pat.sub(entity_to_unicode, series)
# RATING
rating = None
pat = re.compile(r'<!--.*?RATING=[\'"]([^"\']+)[\'"].*?-->', re.DOTALL)
match = pat.search(src)
if match:
rating = match.group(1)
else:
pat = get_meta_regexp_("Rating")
match = pat.search(src)
if match:
rating = match.group(1)
if rating:
try:
mi.rating = float(rating)
if mi.rating < 0:
mi.rating = 0
if mi.rating > 5:
mi.rating /= 2.
if mi.rating > 5:
mi.rating = 0
except:
pass
# COMMENTS
comments = None
pat = re.compile(r'<!--.*?COMMENTS=[\'"]([^"\']+)[\'"].*?-->', re.DOTALL)
match = pat.search(src)
if match:
comments = match.group(1)
else:
pat = get_meta_regexp_("Comments")
match = pat.search(src)
if match:
comments = match.group(1)
if comments:
mi.comments = ent_pat.sub(entity_to_unicode, comments)
# TAGS
tags = None
pat = re.compile(r'<!--.*?TAGS=[\'"]([^"\']+)[\'"].*?-->', re.DOTALL)
match = pat.search(src)
if match:
tags = match.group(1)
else:
pat = get_meta_regexp_("Tags")
match = pat.search(src)
if match:
tags = match.group(1)
if tags:
mi.tags = [x.strip() for x in ent_pat.sub(entity_to_unicode,
tags).split(",")]
# Ready to return MetaInformation
return mi return mi

View File

@ -916,7 +916,7 @@ class OPF(object):
raw = '<?xml version="1.0" encoding="%s"?>\n'%encoding.upper()+raw raw = '<?xml version="1.0" encoding="%s"?>\n'%encoding.upper()+raw
return raw return raw
def smart_update(self, mi): def smart_update(self, mi, replace_metadata=False):
for attr in ('title', 'authors', 'author_sort', 'title_sort', for attr in ('title', 'authors', 'author_sort', 'title_sort',
'publisher', 'series', 'series_index', 'rating', 'publisher', 'series', 'series_index', 'rating',
'isbn', 'language', 'tags', 'category', 'comments', 'isbn', 'language', 'tags', 'category', 'comments',

View File

@ -98,7 +98,7 @@ class CoverManager(object):
authors = [unicode(x) for x in m.creator if x.role == 'aut'] authors = [unicode(x) for x in m.creator if x.role == 'aut']
try: try:
from calibre.utils.magick_draw import create_cover_page, TextLine from calibre.utils.magick.draw import create_cover_page, TextLine
lines = [TextLine(title, 44), TextLine(authors_to_string(authors), lines = [TextLine(title, 44), TextLine(authors_to_string(authors),
32)] 32)]
img_data = create_cover_page(lines, I('library.png')) img_data = create_cover_page(lines, I('library.png'))

View File

@ -262,8 +262,11 @@ class CSSFlattener(object):
indent = asfloat(style['text-indent'], 0) indent = asfloat(style['text-indent'], 0)
left += margin left += margin
if (left + indent) < 0: if (left + indent) < 0:
try:
percent = (margin - indent) / style['width'] percent = (margin - indent) / style['width']
cssdict['margin-left'] = "%d%%" % (percent * 100) cssdict['margin-left'] = "%d%%" % (percent * 100)
except ZeroDivisionError:
pass
left -= indent left -= indent
if 'display' in cssdict and cssdict['display'] == 'in-line': if 'display' in cssdict and cssdict['display'] == 'in-line':
cssdict['display'] = 'inline' cssdict['display'] = 'inline'

View File

@ -59,6 +59,29 @@ class PDFOutput(OutputFormatPlugin):
self.metadata = oeb_book.metadata self.metadata = oeb_book.metadata
self.cover_data = None self.cover_data = None
# Remove page-break-before on <body> element as it causes
# blank pages in PDF Output
from calibre.ebooks.oeb.base import OEB_STYLES, XPath
stylesheet = None
for item in self.oeb.manifest:
if item.media_type.lower() in OEB_STYLES:
stylesheet = item
break
if stylesheet is not None:
from cssutils.css import CSSRule
classes = set(['.calibre'])
for x in self.oeb.spine:
root = x.data
body = XPath('//h:body[@class]')(root)
if body:
classes.add('.'+body[0].get('class'))
for rule in stylesheet.data.cssRules.rulesOfType(CSSRule.STYLE_RULE):
if rule.selectorList.selectorText in classes:
rule.style.removeProperty('page-break-before')
rule.style.removeProperty('page-break-after')
if input_plugin.is_image_collection: if input_plugin.is_image_collection:
log.debug('Converting input as an image collection...') log.debug('Converting input as an image collection...')
self.convert_images(input_plugin.get_images()) self.convert_images(input_plugin.get_images())

View File

@ -120,8 +120,6 @@ class RTFInput(InputFormatPlugin):
return self.convert_images(imap) return self.convert_images(imap)
def convert_images(self, imap): def convert_images(self, imap):
from calibre.utils.PythonMagickWand import ImageMagick
with ImageMagick():
for count, val in imap.items(): for count, val in imap.items():
try: try:
imap[count] = self.convert_image(val) imap[count] = self.convert_image(val)
@ -130,15 +128,11 @@ class RTFInput(InputFormatPlugin):
return imap return imap
def convert_image(self, name): def convert_image(self, name):
import calibre.utils.PythonMagickWand as p from calibre.utils.magick import Image
img = p.NewMagickWand() img = Image()
if img < 0: img.open(name)
raise RuntimeError('Cannot create wand.')
if not p.MagickReadImage(img, name):
self.log.warn('Failed to read image:', name)
name = name.replace('.wmf', '.jpg') name = name.replace('.wmf', '.jpg')
p.MagickWriteImage(img, name) img.save(name)
return name return name

View File

@ -119,10 +119,11 @@ class RTFMLizer(object):
output += '{\\page } ' output += '{\\page } '
for item in self.oeb_book.spine: for item in self.oeb_book.spine:
self.log.debug('Converting %s to RTF markup...' % item.href) self.log.debug('Converting %s to RTF markup...' % item.href)
stylizer = Stylizer(item.data, item.href, self.oeb_book, self.opts, self.opts.output_profile) content = unicode(etree.tostring(item.data, encoding=unicode))
content = unicode(etree.tostring(item.data.find(XHTML('body')), encoding=unicode))
content = self.remove_newlines(content) content = self.remove_newlines(content)
output += self.dump_text(etree.fromstring(content), stylizer) content = etree.fromstring(content)
stylizer = Stylizer(content, item.href, self.oeb_book, self.opts, self.opts.output_profile)
output += self.dump_text(content.find(XHTML('body')), stylizer)
output += self.footer() output += self.footer()
output = self.insert_images(output) output = self.insert_images(output)
output = self.clean_text(output) output = self.clean_text(output)

View File

@ -90,7 +90,7 @@ class ParseRtf:
out_file = '', out_file = '',
out_dir = None, out_dir = None,
dtd = '', dtd = '',
debug = 0, #debug = 0, #why? calibre
deb_dir = None, deb_dir = None,
convert_symbol = None, convert_symbol = None,
convert_wingdings = None, convert_wingdings = None,
@ -132,7 +132,7 @@ class ParseRtf:
self.__dtd_path = dtd self.__dtd_path = dtd
self.__check_file(in_file,"file_to_parse") self.__check_file(in_file,"file_to_parse")
self.__char_data = char_data self.__char_data = char_data
self.__debug_dir = debug self.__debug_dir = deb_dir #self.__debug_dir = debug calibre
self.__check_dir(self.__temp_dir) self.__check_dir(self.__temp_dir)
self.__copy = self.__check_dir(self.__debug_dir) self.__copy = self.__check_dir(self.__debug_dir)
self.__convert_caps = convert_caps self.__convert_caps = convert_caps

View File

@ -51,6 +51,7 @@ class Inline:
'tx<ut<__________' : self.__found_text_func, 'tx<ut<__________' : self.__found_text_func,
'mi<mk<inline-fld' : self.__found_text_func, 'mi<mk<inline-fld' : self.__found_text_func,
'text' : self.__found_text_func, 'text' : self.__found_text_func,
'cw<nu<hard-lineb' : self.__found_text_func, #calibre
'cb<nu<clos-brack' : self.__close_bracket_func, 'cb<nu<clos-brack' : self.__close_bracket_func,
'mi<mk<par-end___' : self.__end_para_func, 'mi<mk<par-end___' : self.__end_para_func,
'mi<mk<footnt-ope' : self.__end_para_func, 'mi<mk<footnt-ope' : self.__end_para_func,
@ -62,6 +63,7 @@ class Inline:
'tx<hx<__________' : self.__found_text_func, 'tx<hx<__________' : self.__found_text_func,
'tx<ut<__________' : self.__found_text_func, 'tx<ut<__________' : self.__found_text_func,
'text' : self.__found_text_func, 'text' : self.__found_text_func,
'cw<nu<hard-lineb' : self.__found_text_func, #calibre
'mi<mk<inline-fld' : self.__found_text_func, 'mi<mk<inline-fld' : self.__found_text_func,
'ob<nu<open-brack': self.__found_open_bracket_func, 'ob<nu<open-brack': self.__found_open_bracket_func,
'mi<mk<par-end___' : self.__end_para_func, 'mi<mk<par-end___' : self.__end_para_func,
@ -133,10 +135,12 @@ class Inline:
Returns: Returns:
nothing nothing
Logic: Logic:
Write if not hardline break
""" """
action = self.__default_dict.get(self.__token_info) action = self.__default_dict.get(self.__token_info)
if action: if action:
action(line) action(line)
if self.__token_info != 'cw<nu<hard-lineb': #calibre
self.__write_obj.write(line) self.__write_obj.write(line)
def __found_open_bracket_func(self, line): def __found_open_bracket_func(self, line):
""" """
@ -164,7 +168,7 @@ class Inline:
Use the dictionary to get the approriate function. Use the dictionary to get the approriate function.
Always print out the line. Always print out the line.
""" """
if line[0:2] == 'cw': if line[0:5] == 'cw<ci': #calibre: bug in original function no diff between cw<ci and cw<pf
self.__handle_control_word(line) self.__handle_control_word(line)
else: else:
action = self.__after_open_bracket_dict.get(self.__token_info) action = self.__after_open_bracket_dict.get(self.__token_info)
@ -247,12 +251,13 @@ class Inline:
Return: Return:
nothing nothing
Logic: Logic:
Two cases: Three cases:
1. in a list. Simply write inline 1. in a list. Simply write inline
2. Not in a list 2. Not in a list
Text can mark the start of a paragraph. Text can mark the start of a paragraph.
If already in a paragraph, check to see if any groups are waiting If already in a paragraph, check to see if any groups are waiting
to be added. If so, use another method to write these groups. to be added. If so, use another method to write these groups.
3. If not check if hardline break, then write
""" """
if self.__place == 'in_list': if self.__place == 'in_list':
self.__write_inline() self.__write_inline()
@ -261,8 +266,11 @@ class Inline:
self.__in_para = 1 self.__in_para = 1
self.__start_para_func(line) self.__start_para_func(line)
else: else:
if self.__token_info == 'cw<nu<hard-lineb': #calibre
self.__write_obj.write('mi<tg<empty_____<hardline-break\n')
if self.__groups_in_waiting[0] != 0: if self.__groups_in_waiting[0] != 0:
self.__write_inline() self.__write_inline()
def __write_inline(self): def __write_inline(self):
""" """
Required: Required:
@ -279,7 +287,7 @@ class Inline:
Get the keys in each dictionary. If 'font-style' is in the keys, Get the keys in each dictionary. If 'font-style' is in the keys,
write a marker tag. (I will use this marker tag later when conerting write a marker tag. (I will use this marker tag later when conerting
hext text to utf8.) hext text to utf8.)
Write a tag for the inline vaues. Write a tag for the inline values.
""" """
if self.__groups_in_waiting[0] != 0: if self.__groups_in_waiting[0] != 0:
last_index = -1 * self.__groups_in_waiting[0] last_index = -1 * self.__groups_in_waiting[0]

View File

@ -73,7 +73,8 @@ class ProcessTokens:
'backslash' : ('nu', '\\', self.text_func), 'backslash' : ('nu', '\\', self.text_func),
'ob' : ('nu', '{', self.text_func), 'ob' : ('nu', '{', self.text_func),
'cb' : ('nu', '}', self.text_func), 'cb' : ('nu', '}', self.text_func),
'line' : ('nu', ' ', self.text_func), 'line' : ('nu', 'hard-lineb', self.default_func), #calibre
#'line' : ('nu', ' ', self.text_func), calibre
# paragraph formatting => pf # paragraph formatting => pf
'page' : ('pf', 'page-break', self.default_func), 'page' : ('pf', 'page-break', self.default_func),
'par' : ('pf', 'par-end___', self.default_func), 'par' : ('pf', 'par-end___', self.default_func),

View File

@ -4,10 +4,9 @@
Read content from txt file. Read content from txt file.
''' '''
import os import os, re
import re
from calibre import prepare_string_for_xml from calibre import prepare_string_for_xml, isbytestring
from calibre.ebooks.markdown import markdown from calibre.ebooks.markdown import markdown
from calibre.ebooks.metadata.opf2 import OPFCreator from calibre.ebooks.metadata.opf2 import OPFCreator
@ -18,6 +17,8 @@ __docformat__ = 'restructuredtext en'
HTML_TEMPLATE = u'<html><head><meta http-equiv="Content-Type" content="text/html; charset=utf-8"/><title>%s</title></head><body>\n%s\n</body></html>' HTML_TEMPLATE = u'<html><head><meta http-equiv="Content-Type" content="text/html; charset=utf-8"/><title>%s</title></head><body>\n%s\n</body></html>'
def convert_basic(txt, title='', epub_split_size_kb=0): def convert_basic(txt, title='', epub_split_size_kb=0):
if isbytestring(txt):
txt = txt.decode('utf-8', 'replace')
# Strip whitespace from the beginning and end of the line. Also replace # Strip whitespace from the beginning and end of the line. Also replace
# all line breaks with \n. # all line breaks with \n.
txt = '\n'.join([line.strip() for line in txt.splitlines()]) txt = '\n'.join([line.strip() for line in txt.splitlines()])
@ -30,23 +31,32 @@ def convert_basic(txt, title='', epub_split_size_kb=0):
txt = re.sub('(?<=.)\s+$', '', txt) txt = re.sub('(?<=.)\s+$', '', txt)
# Remove excessive line breaks. # Remove excessive line breaks.
txt = re.sub('\n{3,}', '\n\n', txt) txt = re.sub('\n{3,}', '\n\n', txt)
#remove ASCII invalid chars : 0 to 8 and 11-14 to 24
chars = list(range(8)) + [0x0B, 0x0E, 0x0F] + list(range(0x10, 0x19))
illegal_chars = re.compile(u'|'.join(map(unichr, chars)))
txt = illegal_chars.sub('', txt)
#Takes care if there is no point to split #Takes care if there is no point to split
if epub_split_size_kb > 0: if epub_split_size_kb > 0:
length_byte = len(txt.encode('utf-8')) if isinstance(txt, unicode):
txt = txt.encode('utf-8')
length_byte = len(txt)
#Calculating the average chunk value for easy splitting as EPUB (+2 as a safe margin) #Calculating the average chunk value for easy splitting as EPUB (+2 as a safe margin)
chunk_size = long(length_byte / (int(length_byte / (epub_split_size_kb * 1024) ) + 2 )) chunk_size = long(length_byte / (int(length_byte / (epub_split_size_kb * 1024) ) + 2 ))
#if there are chunks with a superior size then go and break #if there are chunks with a superior size then go and break
if (len(filter(lambda x: len(x.encode('utf-8')) > chunk_size, txt.split('\n\n')))) : if (len(filter(lambda x: len(x) > chunk_size, txt.split('\n\n')))) :
txt = u'\n\n'.join([split_string_separator(line, chunk_size) for line in txt.split('\n\n')]) txt = '\n\n'.join([split_string_separator(line, chunk_size)
for line in txt.split('\n\n')])
if isbytestring(txt):
txt = txt.decode('utf-8')
lines = [] lines = []
# Split into paragraphs based on having a blank line between text. # Split into paragraphs based on having a blank line between text.
for line in txt.split('\n\n'): for line in txt.split('\n\n'):
if line.strip(): if line.strip():
lines.append('<p>%s</p>' % prepare_string_for_xml(line.replace('\n', ' '))) lines.append(u'<p>%s</p>' % prepare_string_for_xml(line.replace('\n', ' ')))
return HTML_TEMPLATE % (title, '\n'.join(lines)) return HTML_TEMPLATE % (title, u'\n'.join(lines))
def convert_markdown(txt, title='', disable_toc=False): def convert_markdown(txt, title='', disable_toc=False):
md = markdown.Markdown( md = markdown.Markdown(
@ -58,11 +68,11 @@ def convert_markdown(txt, title='', disable_toc=False):
def separate_paragraphs_single_line(txt): def separate_paragraphs_single_line(txt):
txt = txt.replace('\r\n', '\n') txt = txt.replace('\r\n', '\n')
txt = txt.replace('\r', '\n') txt = txt.replace('\r', '\n')
txt = re.sub(u'(?<=.)\n(?=.)', u'\n\n', txt) txt = re.sub(u'(?<=.)\n(?=.)', '\n\n', txt)
return txt return txt
def separate_paragraphs_print_formatted(txt): def separate_paragraphs_print_formatted(txt):
txt = re.sub('(?miu)^(\t+|[ ]{2,})(?=.)', '\n\t', txt) txt = re.sub(u'(?miu)^(\t+|[ ]{2,})(?=.)', '\n\t', txt)
return txt return txt
def preserve_spaces(txt): def preserve_spaces(txt):
@ -78,9 +88,9 @@ def opf_writer(path, opf_name, manifest, spine, mi):
opf.render(opffile) opf.render(opffile)
def split_string_separator(txt, size) : def split_string_separator(txt, size) :
if len(txt.encode('utf-8')) > size: if len(txt) > size:
txt = u''.join([re.sub(u'\.(?P<ends>[^.]*)$', u'.\n\n\g<ends>', txt = ''.join([re.sub(u'\.(?P<ends>[^.]*)$', '.\n\n\g<ends>',
txt[i:i+size], 1) for i in txt[i:i+size], 1) for i in
xrange(0, len(txt.encode('utf-8')), size)]) xrange(0, len(txt), size)])
return txt return txt

View File

@ -329,6 +329,7 @@ class FileIconProvider(QFileIconProvider):
'epub' : 'epub', 'epub' : 'epub',
'fb2' : 'fb2', 'fb2' : 'fb2',
'rtf' : 'rtf', 'rtf' : 'rtf',
'odt' : 'odt',
} }
def __init__(self): def __init__(self):

View File

@ -430,6 +430,20 @@ class AddAction(object): # {{{
d.exec_() d.exec_()
return return
paths = [p for p in view._model.paths(rows) if p is not None] paths = [p for p in view._model.paths(rows) if p is not None]
ve = self.device_manager.device.VIRTUAL_BOOK_EXTENSIONS
def ext(x):
ans = os.path.splitext(x)[1]
ans = ans[1:] if len(ans) > 1 else ans
return ans.lower()
remove = set([p for p in paths if ext(p) in ve])
if remove:
paths = [p for p in paths if p not in remove]
info_dialog(self, _('Not Implemented'),
_('The following books are virtual and cannot be added'
' to the calibre library:'), '\n'.join(remove),
show=True)
if not paths:
return
if not paths or len(paths) == 0: if not paths or len(paths) == 0:
d = error_dialog(self, _('Add to library'), _('No book files found')) d = error_dialog(self, _('Add to library'), _('No book files found'))
d.exec_() d.exec_()
@ -578,9 +592,7 @@ class DeleteAction(object): # {{{
if row is not None: if row is not None:
ci = view.model().index(row, 0) ci = view.model().index(row, 0)
if ci.isValid(): if ci.isValid():
view.setCurrentIndex(ci) view.set_current_row(row)
sm = view.selectionModel()
sm.select(ci, sm.Select)
else: else:
if not confirm('<p>'+_('The selected books will be ' if not confirm('<p>'+_('The selected books will be '
'<b>permanently deleted</b> ' '<b>permanently deleted</b> '
@ -806,11 +818,11 @@ class EditMetadataAction(object): # {{{
for src_id in src_ids: for src_id in src_ids:
src_mi = db.get_metadata(src_id, index_is_id=True, get_cover=True) src_mi = db.get_metadata(src_id, index_is_id=True, get_cover=True)
if src_mi.comments and orig_dest_comments != src_mi.comments: if src_mi.comments and orig_dest_comments != src_mi.comments:
if not dest_mi.comments or len(dest_mi.comments) == 0: if not dest_mi.comments:
dest_mi.comments = src_mi.comments dest_mi.comments = src_mi.comments
else: else:
dest_mi.comments = unicode(dest_mi.comments) + u'\n\n' + unicode(src_mi.comments) dest_mi.comments = unicode(dest_mi.comments) + u'\n\n' + unicode(src_mi.comments)
if src_mi.title and src_mi.title and (not dest_mi.title or if src_mi.title and (not dest_mi.title or
dest_mi.title == _('Unknown')): dest_mi.title == _('Unknown')):
dest_mi.title = src_mi.title dest_mi.title = src_mi.title
if src_mi.title and (not dest_mi.authors or dest_mi.authors[0] == if src_mi.title and (not dest_mi.authors or dest_mi.authors[0] ==
@ -821,8 +833,7 @@ class EditMetadataAction(object): # {{{
if not dest_mi.tags: if not dest_mi.tags:
dest_mi.tags = src_mi.tags dest_mi.tags = src_mi.tags
else: else:
for tag in src_mi.tags: dest_mi.tags.extend(src_mi.tags)
dest_mi.tags.append(tag)
if src_mi.cover and not dest_mi.cover: if src_mi.cover and not dest_mi.cover:
dest_mi.cover = src_mi.cover dest_mi.cover = src_mi.cover
if not dest_mi.publisher: if not dest_mi.publisher:
@ -833,6 +844,44 @@ class EditMetadataAction(object): # {{{
dest_mi.series = src_mi.series dest_mi.series = src_mi.series
dest_mi.series_index = src_mi.series_index dest_mi.series_index = src_mi.series_index
db.set_metadata(dest_id, dest_mi, ignore_errors=False) db.set_metadata(dest_id, dest_mi, ignore_errors=False)
for key in db.field_metadata: #loop thru all defined fields
if db.field_metadata[key]['is_custom']:
colnum = db.field_metadata[key]['colnum']
# Get orig_dest_comments before it gets changed
if db.field_metadata[key]['datatype'] == 'comments':
orig_dest_value = db.get_custom(dest_id, num=colnum, index_is_id=True)
for src_id in src_ids:
dest_value = db.get_custom(dest_id, num=colnum, index_is_id=True)
src_value = db.get_custom(src_id, num=colnum, index_is_id=True)
if db.field_metadata[key]['datatype'] == 'comments':
if src_value and src_value != orig_dest_value:
if not dest_value:
db.set_custom(dest_id, src_value, num=colnum)
else:
dest_value = unicode(dest_value) + u'\n\n' + unicode(src_value)
db.set_custom(dest_id, dest_value, num=colnum)
if db.field_metadata[key]['datatype'] in \
('bool', 'int', 'float', 'rating', 'datetime') \
and not dest_value:
db.set_custom(dest_id, src_value, num=colnum)
if db.field_metadata[key]['datatype'] == 'series' \
and not dest_value:
if src_value:
src_index = db.get_custom_extra(src_id, num=colnum, index_is_id=True)
db.set_custom(dest_id, src_value, num=colnum, extra=src_index)
if db.field_metadata[key]['datatype'] == 'text' \
and not db.field_metadata[key]['is_multiple'] \
and not dest_value:
db.set_custom(dest_id, src_value, num=colnum)
if db.field_metadata[key]['datatype'] == 'text' \
and db.field_metadata[key]['is_multiple']:
if src_value:
if not dest_value:
dest_value = src_value
else:
dest_value.extend(src_value)
db.set_custom(dest_id, dest_value, num=colnum)
# }}} # }}}
def edit_device_collections(self, view, oncard=None): def edit_device_collections(self, view, oncard=None):
@ -878,6 +927,14 @@ class SaveToDiskAction(object): # {{{
_('Choose destination directory')) _('Choose destination directory'))
if not path: if not path:
return return
dpath = os.path.abspath(path).replace('/', os.sep)
lpath = self.library_view.model().db.library_path.replace('/', os.sep)
if dpath.startswith(lpath):
return error_dialog(self, _('Not allowed'),
_('You are tying to save files into the calibre '
'library. This can cause corruption of your '
'library. Save to disk is meant to export '
'files from your calibre library elsewhere.'), show=True)
if self.current_view() is self.library_view: if self.current_view() is self.library_view:
from calibre.gui2.add import Saver from calibre.gui2.add import Saver

View File

@ -72,7 +72,14 @@ class DeviceJob(BaseJob): # {{{
if self._aborted: if self._aborted:
return return
self.failed = True self.failed = True
self._details = unicode(err) + '\n\n' + \ try:
ex = unicode(err)
except:
try:
ex = str(err).decode(preferred_encoding, 'replace')
except:
ex = repr(err)
self._details = ex + '\n\n' + \
traceback.format_exc() traceback.format_exc()
self.exception = err self.exception = err
finally: finally:
@ -111,6 +118,7 @@ class DeviceManager(Thread): # {{{
self.jobs = Queue.Queue(0) self.jobs = Queue.Queue(0)
self.keep_going = True self.keep_going = True
self.job_manager = job_manager self.job_manager = job_manager
self.reported_errors = set([])
self.current_job = None self.current_job = None
self.scanner = DeviceScanner() self.scanner = DeviceScanner()
self.connected_device = None self.connected_device = None
@ -134,13 +142,16 @@ class DeviceManager(Thread): # {{{
for dev, detected_device in connected_devices: for dev, detected_device in connected_devices:
if dev.OPEN_FEEDBACK_MESSAGE is not None: if dev.OPEN_FEEDBACK_MESSAGE is not None:
self.open_feedback_slot(dev.OPEN_FEEDBACK_MESSAGE) self.open_feedback_slot(dev.OPEN_FEEDBACK_MESSAGE)
try:
dev.reset(detected_device=detected_device, dev.reset(detected_device=detected_device,
report_progress=self.report_progress) report_progress=self.report_progress)
try:
dev.open() dev.open()
except: except:
tb = traceback.format_exc()
if DEBUG or tb not in self.reported_errors:
self.reported_errors.add(tb)
prints('Unable to open device', str(dev)) prints('Unable to open device', str(dev))
traceback.print_exc() prints(tb)
continue continue
self.connected_device = dev self.connected_device = dev
self.connected_device_kind = device_kind self.connected_device_kind = device_kind
@ -185,10 +196,12 @@ class DeviceManager(Thread): # {{{
if possibly_connected_devices: if possibly_connected_devices:
if not self.do_connect(possibly_connected_devices, if not self.do_connect(possibly_connected_devices,
device_kind='device'): device_kind='device'):
if DEBUG:
prints('Connect to device failed, retrying in 5 seconds...') prints('Connect to device failed, retrying in 5 seconds...')
time.sleep(5) time.sleep(5)
if not self.do_connect(possibly_connected_devices, if not self.do_connect(possibly_connected_devices,
device_kind='usb'): device_kind='usb'):
if DEBUG:
prints('Device connect failed again, giving up') prints('Device connect failed again, giving up')
# Mount devices that don't use USB, such as the folder device and iTunes # Mount devices that don't use USB, such as the folder device and iTunes

View File

@ -75,7 +75,11 @@ class ChooseLibrary(QDialog, Ui_Dialog):
action = 'existing' action = 'existing'
elif self.empty_library.isChecked(): elif self.empty_library.isChecked():
action = 'new' action = 'new'
loc = os.path.abspath(unicode(self.location.text()).strip()) text = unicode(self.location.text()).strip()
if not text:
return error_dialog(self, _('No location'), _('No location selected'),
show=True)
loc = os.path.abspath(text)
if not loc or not os.path.exists(loc) or not self.check_action(action, if not loc or not os.path.exists(loc) or not self.check_action(action,
loc): loc):
return return

View File

@ -498,9 +498,32 @@ class ConfigDialog(ResizableDialog, Ui_Dialog):
self.opt_gui_layout.setCurrentIndex(li) self.opt_gui_layout.setCurrentIndex(li)
self.opt_disable_animations.setChecked(config['disable_animations']) self.opt_disable_animations.setChecked(config['disable_animations'])
self.opt_show_donate_button.setChecked(config['show_donate_button']) self.opt_show_donate_button.setChecked(config['show_donate_button'])
idx = 0
for i, x in enumerate([(_('Small'), 'small'), (_('Medium'), 'medium'),
(_('Large'), 'large')]):
if x[1] == gprefs.get('toolbar_icon_size', 'medium'):
idx = i
self.opt_toolbar_icon_size.addItem(x[0], x[1])
self.opt_toolbar_icon_size.setCurrentIndex(idx)
idx = 0
for i, x in enumerate([(_('Automatic'), 'auto'), (_('Always'), 'always'),
(_('Never'), 'never')]):
if x[1] == gprefs.get('toolbar_text', 'auto'):
idx = i
self.opt_toolbar_text.addItem(x[0], x[1])
self.opt_toolbar_text.setCurrentIndex(idx)
self.reset_confirmation_button.clicked.connect(self.reset_confirmation)
self.category_view.setCurrentIndex(self.category_view.model().index_for_name(initial_category)) self.category_view.setCurrentIndex(self.category_view.model().index_for_name(initial_category))
def reset_confirmation(self):
from calibre.gui2 import dynamic
for key in dynamic.keys():
if key.endswith('_again') and dynamic[key] is False:
dynamic[key] = True
info_dialog(self, _('Done'),
_('Confirmation dialogs have all been reset'), show=True)
def check_port_value(self, *args): def check_port_value(self, *args):
port = self.port.value() port = self.port.value()
if port < 1025: if port < 1025:
@ -869,6 +892,10 @@ class ConfigDialog(ResizableDialog, Ui_Dialog):
config['disable_animations'] = bool(self.opt_disable_animations.isChecked()) config['disable_animations'] = bool(self.opt_disable_animations.isChecked())
config['show_donate_button'] = bool(self.opt_show_donate_button.isChecked()) config['show_donate_button'] = bool(self.opt_show_donate_button.isChecked())
gprefs['show_splash_screen'] = bool(self.show_splash_screen.isChecked()) gprefs['show_splash_screen'] = bool(self.show_splash_screen.isChecked())
for x in ('toolbar_icon_size', 'toolbar_text'):
w = getattr(self, 'opt_'+x)
data = w.itemData(w.currentIndex()).toString()
gprefs[x] = unicode(data)
fmts = [] fmts = []
for i in range(self.viewer.count()): for i in range(self.viewer.count()):
if self.viewer.item(i).checkState() == Qt.Checked: if self.viewer.item(i).checkState() == Qt.Checked:

View File

@ -89,8 +89,8 @@
<rect> <rect>
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>724</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">
@ -222,6 +222,13 @@
</item> </item>
</layout> </layout>
</item> </item>
<item>
<widget class="QPushButton" name="reset_confirmation_button">
<property name="text">
<string>Reset all disabled &amp;confirmation dialogs</string>
</property>
</widget>
</item>
<item> <item>
<widget class="QGroupBox" name="groupBox_5"> <widget class="QGroupBox" name="groupBox_5">
<property name="title"> <property name="title">
@ -346,21 +353,21 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="7" column="0" colspan="2"> <item row="8" 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 &amp;news to ebook reader</string> <string>Automatically send downloaded &amp;news to ebook reader</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="8" column="0" colspan="2"> <item row="9" column="0" colspan="2">
<widget class="QCheckBox" name="delete_news"> <widget class="QCheckBox" name="delete_news">
<property name="text"> <property name="text">
<string>&amp;Delete news from library when it is automatically sent to reader</string> <string>&amp;Delete news from library when it is automatically sent to reader</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="9" column="0" colspan="2"> <item row="10" 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">
@ -377,7 +384,7 @@
</item> </item>
</layout> </layout>
</item> </item>
<item row="10" 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">
@ -580,6 +587,41 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="7" column="0" colspan="2">
<widget class="QGroupBox" name="groupBox_2">
<property name="title">
<string>&amp;Toolbar</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="1">
<widget class="QComboBox" name="opt_toolbar_icon_size"/>
</item>
<item row="0" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>&amp;Icon size:</string>
</property>
<property name="buddy">
<cstring>opt_toolbar_icon_size</cstring>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QComboBox" name="opt_toolbar_text"/>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_4">
<property name="text">
<string>Show &amp;text under icons:</string>
</property>
<property name="buddy">
<cstring>opt_toolbar_text</cstring>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout> </layout>
</widget> </widget>
<widget class="QWidget" name="page_6"> <widget class="QWidget" name="page_6">

View File

@ -5,7 +5,7 @@ __docformat__ = 'restructuredtext en'
from calibre.gui2 import dynamic from calibre.gui2 import dynamic
from calibre.gui2.dialogs.confirm_delete_ui import Ui_Dialog from calibre.gui2.dialogs.confirm_delete_ui import Ui_Dialog
from PyQt4.Qt import QDialog, SIGNAL, Qt from PyQt4.Qt import QDialog, Qt, QPixmap, QIcon
def _config_name(name): def _config_name(name):
return name + '_again' return name + '_again'
@ -18,15 +18,17 @@ class Dialog(QDialog, Ui_Dialog):
self.msg.setText(msg) self.msg.setText(msg)
self.name = name self.name = name
self.connect(self.again, SIGNAL('stateChanged(int)'), self.toggle) self.again.stateChanged.connect(self.toggle)
self.buttonBox.setFocus(Qt.OtherFocusReason) self.buttonBox.setFocus(Qt.OtherFocusReason)
def toggle(self, x): def toggle(self, *args):
dynamic[_config_name(self.name)] = self.again.isChecked() dynamic[_config_name(self.name)] = self.again.isChecked()
def confirm(msg, name, parent=None): def confirm(msg, name, parent=None, pixmap='dialog_warning.svg'):
if not dynamic.get(_config_name(name), True): if not dynamic.get(_config_name(name), True):
return True return True
d = Dialog(msg, name, parent) d = Dialog(msg, name, parent)
d.label.setPixmap(QPixmap(I(pixmap)))
d.setWindowIcon(QIcon(I(pixmap)))
return d.exec_() == d.Accepted return d.exec_() == d.Accepted

View File

@ -24,13 +24,15 @@ from calibre.gui2.widgets import ProgressIndicator
from calibre.ebooks import BOOK_EXTENSIONS from calibre.ebooks import BOOK_EXTENSIONS
from calibre.ebooks.metadata import 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.covers import download_cover
from calibre.ebooks.metadata.meta import get_metadata from calibre.ebooks.metadata.meta import get_metadata
from calibre.ebooks.metadata import MetaInformation
from calibre.utils.config import prefs, tweaks from calibre.utils.config import prefs, tweaks
from calibre.utils.date import qt_to_dt from calibre.utils.date import qt_to_dt, local_tz, utcfromtimestamp
from calibre.customize.ui import run_plugins_on_import, get_isbndb_key from calibre.customize.ui import run_plugins_on_import, get_isbndb_key
from calibre.gui2.dialogs.config.social import SocialMetadata from calibre.gui2.dialogs.config.social import SocialMetadata
from calibre.gui2.custom_column_widgets import populate_metadata_page from calibre.gui2.custom_column_widgets import populate_metadata_page
from calibre import strftime
class CoverFetcher(QThread): class CoverFetcher(QThread):
@ -47,12 +49,13 @@ class CoverFetcher(QThread):
def run(self): def run(self):
try: try:
au = self.author if self.author else None
mi = MetaInformation(self.title, [au])
if not self.isbn: if not self.isbn:
from calibre.ebooks.metadata.fetch import search from calibre.ebooks.metadata.fetch import search
if not self.title: if not self.title:
self.needs_isbn = True self.needs_isbn = True
return return
au = self.author if self.author else None
key = get_isbndb_key() key = get_isbndb_key()
if not key: if not key:
key = None key = None
@ -65,8 +68,10 @@ class CoverFetcher(QThread):
return return
self.isbn = results[0] self.isbn = results[0]
self.cover_data = cover_from_isbn(self.isbn, timeout=self.timeout, mi.isbn = self.isbn
username=self.username, password=self.password)[0]
self.cover_data, self.errors = download_cover(mi,
timeout=self.timeout)
except Exception, e: except Exception, e:
self.exception = e self.exception = e
self.traceback = traceback.format_exc() self.traceback = traceback.format_exc()
@ -75,13 +80,20 @@ class CoverFetcher(QThread):
class Format(QListWidgetItem): class Format(QListWidgetItem):
def __init__(self, parent, ext, size, path=None):
def __init__(self, parent, ext, size, path=None, timestamp=None):
self.path = path self.path = path
self.ext = ext self.ext = ext
self.size = float(size)/(1024*1024) self.size = float(size)/(1024*1024)
text = '%s (%.2f MB)'%(self.ext.upper(), self.size) text = '%s (%.2f MB)'%(self.ext.upper(), self.size)
QListWidgetItem.__init__(self, file_icon_provider().icon_from_ext(ext), QListWidgetItem.__init__(self, file_icon_provider().icon_from_ext(ext),
text, parent, QListWidgetItem.UserType) text, parent, QListWidgetItem.UserType)
if timestamp is not None:
ts = timestamp.astimezone(local_tz)
t = strftime('%a, %d %b %Y [%H:%M:%S]', ts.timetuple())
text = _('Last modified: %s')%t
self.setToolTip(text)
self.setStatusTip(text)
class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog): class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
@ -130,6 +142,21 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
self.cpixmap = pix self.cpixmap = pix
self.cover_data = cover self.cover_data = cover
def generate_cover(self, *args):
from calibre.utils.magick.draw import create_cover_page, TextLine
title = unicode(self.title.text()).strip()
author = unicode(self.authors.text()).strip()
if not title or not author:
return error_dialog(self, _('Specify title and author'),
_('You must specify a title and author before generating '
'a cover'), show=True)
lines = [TextLine(title, 44), TextLine(author, 32)]
self.cover_data = create_cover_page(lines, I('library.png'))
pix = QPixmap()
pix.loadFromData(self.cover_data)
self.cover.setPixmap(pix)
self.cover_changed = True
self.cpixmap = pix
def add_format(self, x): def add_format(self, x):
files = choose_files(self, 'add formats dialog', files = choose_files(self, 'add formats dialog',
@ -151,14 +178,16 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
nfile = run_plugins_on_import(_file) nfile = run_plugins_on_import(_file)
if nfile is not None: if nfile is not None:
_file = nfile _file = nfile
size = os.stat(_file).st_size stat = os.stat(_file)
size = stat.st_size
ext = os.path.splitext(_file)[1].lower().replace('.', '') ext = os.path.splitext(_file)[1].lower().replace('.', '')
timestamp = utcfromtimestamp(stat.st_mtime)
for row in range(self.formats.count()): for row in range(self.formats.count()):
fmt = self.formats.item(row) fmt = self.formats.item(row)
if fmt.ext.lower() == ext: if fmt.ext.lower() == ext:
self.formats.takeItem(row) self.formats.takeItem(row)
break break
Format(self.formats, ext, size, path=_file) Format(self.formats, ext, size, path=_file, timestamp=timestamp)
self.formats_changed = True self.formats_changed = True
added = True added = True
if bad_perms: if bad_perms:
@ -379,9 +408,10 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
if not ext: if not ext:
ext = '' ext = ''
size = self.db.sizeof_format(row, ext) size = self.db.sizeof_format(row, ext)
timestamp = self.db.format_last_modified(self.id, ext)
if size is None: if size is None:
continue continue
Format(self.formats, ext, size) Format(self.formats, ext, size, timestamp=timestamp)
self.initialize_combos() self.initialize_combos()
@ -410,6 +440,7 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
self.central_widget.tabBar().setVisible(False) self.central_widget.tabBar().setVisible(False)
else: else:
self.create_custom_column_editors() self.create_custom_column_editors()
self.generate_cover_button.clicked.connect(self.generate_cover)
def create_custom_column_editors(self): def create_custom_column_editors(self):
w = self.central_widget.widget(1) w = self.central_widget.widget(1)
@ -565,6 +596,13 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
error_dialog(self, _('Cannot fetch cover'), error_dialog(self, _('Cannot fetch cover'),
_('<b>Could not fetch cover.</b><br/>')+unicode(err)).exec_() _('<b>Could not fetch cover.</b><br/>')+unicode(err)).exec_()
return return
if self.cover_fetcher.errors and self.cover_fetcher.cover_data is None:
details = u'\n\n'.join([e[-1] + ': ' + e[1] for e in self.cover_fetcher.errors])
error_dialog(self, _('Cannot fetch cover'),
_('<b>Could not fetch cover.</b><br/>') +
_('For the error message from each cover source, '
'click Show details below.'), det_msg=details, show=True)
return
pix = QPixmap() pix = QPixmap()
pix.loadFromData(self.cover_fetcher.cover_data) pix.loadFromData(self.cover_fetcher.cover_data)
@ -666,6 +704,10 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
def accept(self): def accept(self):
cf = getattr(self, 'cover_fetcher', None)
if cf is not None and hasattr(cf, 'terminate'):
cf.terminate()
cf.wait()
try: try:
if self.formats_changed: if self.formats_changed:
self.sync_formats() self.sync_formats()

View File

@ -653,6 +653,16 @@
</property> </property>
</widget> </widget>
</item> </item>
<item>
<widget class="QPushButton" name="generate_cover_button">
<property name="toolTip">
<string>Generate a default cover based on the title and author</string>
</property>
<property name="text">
<string>&amp;Generate cover</string>
</property>
</widget>
</item>
</layout> </layout>
</item> </item>
</layout> </layout>
@ -736,15 +746,17 @@
<tabstop>fetch_metadata_button</tabstop> <tabstop>fetch_metadata_button</tabstop>
<tabstop>add_format_button</tabstop> <tabstop>add_format_button</tabstop>
<tabstop>remove_format_button</tabstop> <tabstop>remove_format_button</tabstop>
<tabstop>button_set_cover</tabstop>
<tabstop>button_set_metadata</tabstop>
<tabstop>formats</tabstop>
<tabstop>cover_path</tabstop> <tabstop>cover_path</tabstop>
<tabstop>cover_button</tabstop> <tabstop>cover_button</tabstop>
<tabstop>reset_cover</tabstop> <tabstop>reset_cover</tabstop>
<tabstop>fetch_cover_button</tabstop> <tabstop>fetch_cover_button</tabstop>
<tabstop>button_set_cover</tabstop> <tabstop>generate_cover_button</tabstop>
<tabstop>formats</tabstop>
<tabstop>button_set_metadata</tabstop>
<tabstop>button_box</tabstop>
<tabstop>scrollArea</tabstop> <tabstop>scrollArea</tabstop>
<tabstop>central_widget</tabstop>
<tabstop>button_box</tabstop>
</tabstops> </tabstops>
<resources> <resources>
<include location="../../../../resources/images.qrc"/> <include location="../../../../resources/images.qrc"/>

View File

@ -16,7 +16,7 @@ from PyQt4.Qt import QIcon, Qt, QWidget, QAction, QToolBar, QSize, \
from calibre.constants import __appname__, isosx from calibre.constants import __appname__, isosx
from calibre.gui2.search_box import SearchBox2, SavedSearchBox from calibre.gui2.search_box import SearchBox2, SavedSearchBox
from calibre.gui2.throbber import ThrobbingButton from calibre.gui2.throbber import ThrobbingButton
from calibre.gui2 import config, open_url from calibre.gui2 import config, open_url, gprefs
from calibre.gui2.widgets import ComboBoxWithHelp from calibre.gui2.widgets import ComboBoxWithHelp
from calibre import human_readable from calibre import human_readable
from calibre.utils.config import prefs from calibre.utils.config import prefs
@ -24,7 +24,6 @@ from calibre.ebooks import BOOK_EXTENSIONS
from calibre.gui2.dialogs.scheduler import Scheduler from calibre.gui2.dialogs.scheduler import Scheduler
from calibre.utils.smtp import config as email_config from calibre.utils.smtp import config as email_config
ICON_SIZE = 48
class SaveMenu(QMenu): # {{{ class SaveMenu(QMenu): # {{{
@ -229,12 +228,11 @@ class ToolBar(QToolBar): # {{{
self.setFloatable(False) self.setFloatable(False)
self.setOrientation(Qt.Horizontal) self.setOrientation(Qt.Horizontal)
self.setAllowedAreas(Qt.TopToolBarArea|Qt.BottomToolBarArea) self.setAllowedAreas(Qt.TopToolBarArea|Qt.BottomToolBarArea)
self.setIconSize(QSize(ICON_SIZE, ICON_SIZE))
self.setToolButtonStyle(Qt.ToolButtonTextUnderIcon)
self.setStyleSheet('QToolButton:checked { font-weight: bold }') self.setStyleSheet('QToolButton:checked { font-weight: bold }')
self.donate = donate
self.apply_settings()
self.all_actions = actions self.all_actions = actions
self.donate = donate
self.location_manager = location_manager self.location_manager = location_manager
self.location_manager.locations_changed.connect(self.build_bar) self.location_manager.locations_changed.connect(self.build_bar)
self.d_widget = QWidget() self.d_widget = QWidget()
@ -243,6 +241,17 @@ class ToolBar(QToolBar): # {{{
donate.setAutoRaise(True) donate.setAutoRaise(True)
donate.setCursor(Qt.PointingHandCursor) donate.setCursor(Qt.PointingHandCursor)
self.build_bar() self.build_bar()
self.preferred_width = self.sizeHint().width()
def apply_settings(self):
sz = gprefs.get('toolbar_icon_size', 'medium')
sz = {'small':24, 'medium':48, 'large':64}[sz]
self.setIconSize(QSize(sz, sz))
style = Qt.ToolButtonTextUnderIcon
if gprefs.get('toolbar_text', 'auto') == 'never':
style = Qt.ToolButtonIconOnly
self.setToolButtonStyle(style)
self.donate.set_normal_icon_size(sz, sz)
def contextMenuEvent(self, *args): def contextMenuEvent(self, *args):
pass pass
@ -293,13 +302,19 @@ class ToolBar(QToolBar): # {{{
text = _('%d books')%new_count text = _('%d books')%new_count
a = self.choose_action a = self.choose_action
a.setText(text) a.setText(text)
a.setToolTip(_('Choose calibre library to work with') + '\n\n' + text)
def resizeEvent(self, ev): def resizeEvent(self, ev):
style = Qt.ToolButtonTextUnderIcon
if self.size().width() < 1260:
style = Qt.ToolButtonIconOnly
self.setToolButtonStyle(style)
QToolBar.resizeEvent(self, ev) QToolBar.resizeEvent(self, ev)
style = Qt.ToolButtonTextUnderIcon
p = gprefs.get('toolbar_text', 'auto')
if p == 'never':
style = Qt.ToolButtonIconOnly
if p == 'auto' and self.preferred_width > self.width()+35:
style = Qt.ToolButtonIconOnly
self.setToolButtonStyle(style)
def database_changed(self, db): def database_changed(self, db):
pass pass
@ -309,11 +324,12 @@ class ToolBar(QToolBar): # {{{
class Action(QAction): class Action(QAction):
pass pass
class ShareConnMenu(QMenu): class ShareConnMenu(QMenu): # {{{
connect_to_folder = pyqtSignal() connect_to_folder = pyqtSignal()
connect_to_itunes = pyqtSignal() connect_to_itunes = pyqtSignal()
config_email = pyqtSignal() config_email = pyqtSignal()
toggle_server = pyqtSignal()
def __init__(self, parent=None): def __init__(self, parent=None):
QMenu.__init__(self, parent) QMenu.__init__(self, parent)
@ -321,20 +337,33 @@ class ShareConnMenu(QMenu):
mitem.setEnabled(True) mitem.setEnabled(True)
mitem.triggered.connect(lambda x : self.connect_to_folder.emit()) mitem.triggered.connect(lambda x : self.connect_to_folder.emit())
self.connect_to_folder_action = mitem self.connect_to_folder_action = mitem
mitem = self.addAction(QIcon(I('devices/itunes.png')), mitem = self.addAction(QIcon(I('devices/itunes.png')),
_('Connect to iTunes')) _('Connect to iTunes'))
mitem.setEnabled(True) mitem.setEnabled(True)
mitem.triggered.connect(lambda x : self.connect_to_itunes.emit()) mitem.triggered.connect(lambda x : self.connect_to_itunes.emit())
self.connect_to_itunes_action = mitem self.connect_to_itunes_action = mitem
self.addSeparator() self.addSeparator()
self.toggle_server_action = \
self.addAction(QIcon(I('network-server.svg')),
_('Start Content Server'))
self.toggle_server_action.triggered.connect(lambda x:
self.toggle_server.emit())
self.addSeparator()
self.email_actions = [] self.email_actions = []
def server_state_changed(self, running):
text = _('Start Content Server')
if running:
text = _('Stop Content Server')
self.toggle_server_action.setText(text)
def build_email_entries(self, sync_menu): def build_email_entries(self, sync_menu):
from calibre.gui2.device import DeviceAction from calibre.gui2.device import DeviceAction
for ac in self.email_actions: for ac in self.email_actions:
self.removeAction(ac) self.removeAction(ac)
self.email_actions = [] self.email_actions = []
self.memory = []
opts = email_config().parse() opts = email_config().parse()
if opts.accounts: if opts.accounts:
self.email_to_menu = QMenu(_('Email to')+'...', self) self.email_to_menu = QMenu(_('Email to')+'...', self)
@ -347,6 +376,7 @@ class ShareConnMenu(QMenu):
action2 = DeviceAction(dest, True, False, I('mail.svg'), action2 = DeviceAction(dest, True, False, I('mail.svg'),
_('Email to')+' '+account+ _(' and delete from library')) _('Email to')+' '+account+ _(' and delete from library'))
map(self.email_to_menu.addAction, (action1, action2)) map(self.email_to_menu.addAction, (action1, action2))
map(self.memory.append, (action1, action2))
if default: if default:
map(self.addAction, (action1, action2)) map(self.addAction, (action1, action2))
map(self.email_actions.append, (action1, action2)) map(self.email_actions.append, (action1, action2))
@ -363,6 +393,8 @@ class ShareConnMenu(QMenu):
def setup_email(self, *args): def setup_email(self, *args):
self.config_email.emit() self.config_email.emit()
# }}}
class MainWindowMixin(object): class MainWindowMixin(object):
def __init__(self, db): def __init__(self, db):
@ -378,7 +410,6 @@ class MainWindowMixin(object):
self.centralwidget.setLayout(self._central_widget_layout) self.centralwidget.setLayout(self._central_widget_layout)
self.resize(1012, 740) self.resize(1012, 740)
self.donate_button = ThrobbingButton(self.centralwidget) self.donate_button = ThrobbingButton(self.centralwidget)
self.donate_button.set_normal_icon_size(ICON_SIZE, ICON_SIZE)
self.location_manager = LocationManager(self) self.location_manager = LocationManager(self)
self.init_scheduler(db) self.init_scheduler(db)
@ -460,6 +491,7 @@ class MainWindowMixin(object):
self.action_news.triggered.connect( self.action_news.triggered.connect(
self.scheduler.show_dialog) self.scheduler.show_dialog)
self.share_conn_menu = ShareConnMenu(self) self.share_conn_menu = ShareConnMenu(self)
self.share_conn_menu.toggle_server.connect(self.toggle_content_server)
self.share_conn_menu.config_email.connect(partial(self.do_config, self.share_conn_menu.config_email.connect(partial(self.do_config,
initial_category='email')) initial_category='email'))
self.action_conn_share.setMenu(self.share_conn_menu) self.action_conn_share.setMenu(self.share_conn_menu)
@ -596,4 +628,12 @@ class MainWindowMixin(object):
def show_help(self, *args): def show_help(self, *args):
open_url(QUrl('http://calibre-ebook.com/user_manual')) open_url(QUrl('http://calibre-ebook.com/user_manual'))
def content_server_state_changed(self, running):
self.share_conn_menu.server_state_changed(running)
def toggle_content_server(self):
if self.content_server is None:
self.start_content_server()
else:
self.content_server.exit()
self.content_server = None

View File

@ -478,14 +478,20 @@ class BooksView(QTableView): # {{{
def set_current_row(self, row, select=True): def set_current_row(self, row, select=True):
if row > -1: if row > -1:
h = self.horizontalHeader() h = self.horizontalHeader()
for i in range(h.count()): logical_indices = list(range(h.count()))
if not h.isSectionHidden(i): logical_indices = [x for x in logical_indices if not
h.isSectionHidden(x)]
pairs = [(x, h.visualIndex(x)) for x in logical_indices if
h.visualIndex(x) > -1]
if not pairs:
pairs = [(0, 0)]
pairs.sort(cmp=lambda x,y:cmp(x[1], y[1]))
i = pairs[0][0]
index = self.model().index(row, i) index = self.model().index(row, i)
self.setCurrentIndex(index) self.setCurrentIndex(index)
if select: if select:
sm = self.selectionModel() sm = self.selectionModel()
sm.select(index, sm.ClearAndSelect|sm.Rows) sm.select(index, sm.ClearAndSelect|sm.Rows)
break
def close(self): def close(self):
self._model.close() self._model.close()

View File

@ -13,8 +13,10 @@ from Queue import Queue, Empty
from calibre.ebooks.metadata.fetch import search, get_social_metadata from calibre.ebooks.metadata.fetch import search, get_social_metadata
from calibre.gui2 import config from calibre.gui2 import config
from calibre.ebooks.metadata.library_thing import cover_from_isbn from calibre.ebooks.metadata.covers import download_cover
from calibre.customize.ui import get_isbndb_key from calibre.customize.ui import get_isbndb_key
from calibre import prints
from calibre.constants import DEBUG
class Worker(Thread): class Worker(Thread):
@ -26,13 +28,15 @@ class Worker(Thread):
def run(self): def run(self):
while True: while True:
isbn = self.jobs.get() mi = self.jobs.get()
if not isbn: if not getattr(mi, 'isbn', False):
break break
try: try:
cdata, _ = cover_from_isbn(isbn) cdata, errors = download_cover(mi)
if cdata: if cdata:
self.results.put((isbn, cdata)) self.results.put((mi.isbn, cdata))
elif DEBUG:
prints('Cover download failed:', errors)
except: except:
traceback.print_exc() traceback.print_exc()
@ -98,7 +102,7 @@ class DownloadMetadata(Thread):
fmi = results[0] fmi = results[0]
self.fetched_metadata[id] = fmi self.fetched_metadata[id] = fmi
if fmi.isbn and self.get_covers: if fmi.isbn and self.get_covers:
self.worker.jobs.put(fmi.isbn) self.worker.jobs.put(fmi)
if (not config['overwrite_author_title_metadata']): if (not config['overwrite_author_title_metadata']):
fmi.authors = mi.authors fmi.authors = mi.authors
fmi.author_sort = mi.author_sort fmi.author_sort = mi.author_sort

View File

@ -84,7 +84,7 @@ class SearchBox2(QComboBox):
self.prev_search = '' self.prev_search = ''
self.timer = QTimer() self.timer = QTimer()
self.timer.setSingleShot(True) self.timer.setSingleShot(True)
self.timer.timeout.connect(self.timer_event, Qt.QueuedConnection) self.timer.timeout.connect(self.timer_event, type=Qt.QueuedConnection)
self.setInsertPolicy(self.NoInsert) self.setInsertPolicy(self.NoInsert)
self.setMaxCount(self.MAX_COUNT) self.setMaxCount(self.MAX_COUNT)
self.setSizeAdjustPolicy(self.AdjustToMinimumContentsLengthWithIcon) self.setSizeAdjustPolicy(self.AdjustToMinimumContentsLengthWithIcon)

View File

@ -26,7 +26,10 @@ class ThrobbingButton(QToolButton):
def set_normal_icon_size(self, w, h): def set_normal_icon_size(self, w, h):
self.normal_icon_size = QSize(w, h) self.normal_icon_size = QSize(w, h)
self.setIconSize(self.normal_icon_size) self.setIconSize(self.normal_icon_size)
try:
self.setMinimumSize(self.sizeHint()) self.setMinimumSize(self.sizeHint())
except:
self.setMinimumSize(QSize(w+5, h+5))
def animation_finished(self): def animation_finished(self):
self.setIconSize(self.normal_icon_size) self.setIconSize(self.normal_icon_size)

View File

@ -221,7 +221,7 @@ def fetch_scheduled_recipe(arg):
if lf.get('base_font_size', 0.0) != 0.0: if lf.get('base_font_size', 0.0) != 0.0:
recs.append(('base_font_size', lf['base_font_size'], recs.append(('base_font_size', lf['base_font_size'],
OptionRecommendation.HIGH)) OptionRecommendation.HIGH))
recs.append(('keep_ligatures', lf['keep_ligatures'], recs.append(('keep_ligatures', lf.get('keep_ligatures', False),
OptionRecommendation.HIGH)) OptionRecommendation.HIGH))
lr = load_defaults('lrf_output') lr = load_defaults('lrf_output')

View File

@ -24,7 +24,7 @@ from calibre.ptempfile import PersistentTemporaryFile
from calibre.utils.config import prefs, dynamic from calibre.utils.config import prefs, dynamic
from calibre.utils.ipc.server import Server from calibre.utils.ipc.server import Server
from calibre.gui2 import error_dialog, GetMetadata, open_local_file, \ from calibre.gui2 import error_dialog, GetMetadata, open_local_file, \
gprefs, max_available_height, config, info_dialog gprefs, max_available_height, config, info_dialog, Dispatcher
from calibre.gui2.cover_flow import CoverFlowMixin from calibre.gui2.cover_flow import CoverFlowMixin
from calibre.gui2.widgets import ProgressIndicator from calibre.gui2.widgets import ProgressIndicator
from calibre.gui2.update import UpdateMixin from calibre.gui2.update import UpdateMixin
@ -106,6 +106,7 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, # {{{
opts = self.opts opts = self.opts
self.preferences_action, self.quit_action = actions self.preferences_action, self.quit_action = actions
self.library_path = library_path self.library_path = library_path
self.content_server = None
self.spare_servers = [] self.spare_servers = []
self.must_restart_before_config = False self.must_restart_before_config = False
# Initialize fontconfig in a separate thread as this can be a lengthy # Initialize fontconfig in a separate thread as this can be a lengthy
@ -146,7 +147,6 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, # {{{
self.default_thumbnail = None self.default_thumbnail = None
self.tb_wrapper = textwrap.TextWrapper(width=40) self.tb_wrapper = textwrap.TextWrapper(width=40)
self.viewers = collections.deque() self.viewers = collections.deque()
self.content_server = None
self.system_tray_icon = SystemTrayIcon(QIcon(I('library.png')), self) self.system_tray_icon = SystemTrayIcon(QIcon(I('library.png')), self)
self.system_tray_icon.setToolTip('calibre') self.system_tray_icon.setToolTip('calibre')
self.system_tray_icon.tooltip_requested.connect( self.system_tray_icon.tooltip_requested.connect(
@ -246,11 +246,7 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, # {{{
if config['autolaunch_server']: if config['autolaunch_server']:
from calibre.library.server.main import start_threaded_server self.start_content_server()
from calibre.library.server import server_config
self.content_server = start_threaded_server(
db, server_config().parse())
self.test_server_timer = QTimer.singleShot(10000, self.test_server)
self.keyboard_interrupt.connect(self.quit, type=Qt.QueuedConnection) self.keyboard_interrupt.connect(self.quit, type=Qt.QueuedConnection)
AddAction.__init__(self) AddAction.__init__(self)
@ -263,6 +259,15 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, # {{{
self.library_view.model().delete_books_by_id, self.library_view.model().delete_books_by_id,
type=Qt.QueuedConnection) type=Qt.QueuedConnection)
def start_content_server(self):
from calibre.library.server.main import start_threaded_server
from calibre.library.server import server_config
self.content_server = start_threaded_server(
self.library_view.model().db, server_config().parse())
self.content_server.state_callback = Dispatcher(self.content_server_state_changed)
self.content_server.state_callback(True)
self.test_server_timer = QTimer.singleShot(10000, self.test_server)
def resizeEvent(self, ev): def resizeEvent(self, ev):
MainWindow.resizeEvent(self, ev) MainWindow.resizeEvent(self, ev)
@ -308,7 +313,8 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, # {{{
setattr(window, '__systray_minimized', False) setattr(window, '__systray_minimized', False)
def test_server(self, *args): def test_server(self, *args):
if self.content_server.exception is not None: if self.content_server is not None and \
self.content_server.exception is not None:
error_dialog(self, _('Failed to start content server'), error_dialog(self, _('Failed to start content server'),
unicode(self.content_server.exception)).exec_() unicode(self.content_server.exception)).exec_()
@ -367,6 +373,11 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, # {{{
d.exec_() d.exec_()
self.content_server = d.server self.content_server = d.server
if self.content_server is not None:
self.content_server.state_callback = \
Dispatcher(self.content_server_state_changed)
self.content_server.state_callback(self.content_server.is_running)
if d.result() == d.Accepted: if d.result() == d.Accepted:
self.read_toolbar_settings() self.read_toolbar_settings()
self.search.search_as_you_type(config['search_as_you_type']) self.search.search_as_you_type(config['search_as_you_type'])
@ -380,6 +391,7 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, # {{{
self.tags_view.recount() self.tags_view.recount()
self.create_device_menu() self.create_device_menu()
self.set_device_menu_items_state(bool(self.device_connected)) self.set_device_menu_items_state(bool(self.device_connected))
self.tool_bar.apply_settings()
def library_moved(self, newloc): def library_moved(self, newloc):
if newloc is None: return if newloc is None: return
@ -582,7 +594,9 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, # {{{
try: try:
try: try:
if self.content_server is not None: if self.content_server is not None:
self.content_server.exit() s = self.content_server
self.content_server = None
s.exit()
except: except:
pass pass
time.sleep(2) time.sleep(2)

View File

@ -31,7 +31,7 @@ class CheckForUpdates(QThread):
'win' if iswindows else 'osx' if isosx else 'oth') 'win' if iswindows else 'osx' if isosx else 'oth')
req.add_header('CALIBRE_INSTALL_UUID', prefs['installation_uuid']) req.add_header('CALIBRE_INSTALL_UUID', prefs['installation_uuid'])
version = br.open(req).read().strip() version = br.open(req).read().strip()
if version and version != __version__: if version and version != __version__ and len(version) < 10:
self.update_found.emit(version) self.update_found.emit(version)
except: except:
traceback.print_exc() traceback.print_exc()

View File

@ -29,6 +29,8 @@ from calibre.utils.config import dynamic, prefs
from calibre.gui2 import NONE, choose_dir, error_dialog from calibre.gui2 import NONE, choose_dir, error_dialog
from calibre.gui2.dialogs.progress import ProgressDialog from calibre.gui2.dialogs.progress import ProgressDialog
# Devices {{{
class Device(object): class Device(object):
output_profile = 'default' output_profile = 'default'
@ -166,9 +168,9 @@ class iPhone(Device):
class Android(Device): class Android(Device):
name = 'Adroid phone + WordPlayer' name = 'Adroid phone + WordPlayer/Aldiko'
output_format = 'EPUB' output_format = 'EPUB'
manufacturer = 'Google/HTC' manufacturer = 'Android'
id = 'android' id = 'android'
class HanlinV3(Device): class HanlinV3(Device):
@ -209,6 +211,7 @@ class EZReaderPP(HanlinV5):
manufacturer = 'Astak' manufacturer = 'Astak'
id = 'ezreader_pp' id = 'ezreader_pp'
# }}}
def get_devices(): def get_devices():
for x in globals().values(): for x in globals().values():

View File

@ -37,8 +37,8 @@
</property> </property>
<property name="sizeHint" stdset="0"> <property name="sizeHint" stdset="0">
<size> <size>
<width>20</width> <width>0</width>
<height>56</height> <height>0</height>
</size> </size>
</property> </property>
</spacer> </spacer>
@ -59,19 +59,6 @@
</property> </property>
</widget> </widget>
</item> </item>
<item>
<spacer name="verticalSpacer_2">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>56</height>
</size>
</property>
</spacer>
</item>
<item> <item>
<widget class="QLabel" name="label_3"> <widget class="QLabel" name="label_3">
<property name="text"> <property name="text">
@ -88,19 +75,6 @@
</property> </property>
</widget> </widget>
</item> </item>
<item>
<spacer name="verticalSpacer_3">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout> </layout>
</widget> </widget>
<resources/> <resources/>

Some files were not shown because too many files have changed in this diff Show More