Pull from Trunk

This commit is contained in:
Timothy Legge 2010-07-06 22:47:05 -03:00
commit f4c2f69f9e
124 changed files with 24541 additions and 30432 deletions

View File

@ -4,6 +4,61 @@
# for important features/bug fixes.
# Also, each release can have new and improved recipes.
- version: 0.7.7
date: 2010-07-02
new features:
- title: "Support for the Nokia E52"
- title: "Searching on the size column"
- title: "iTunes driver: Add option to disable cover fetching for speeding up the fetching of large book collections"
bug fixes:
- title: "SONY driver: Only update metadata when books are sent to device."
- title: "TXT Input: Ensure the generated html is splittable"
tickets: [5904]
- title: "Fix infinite loop in default cover generation."
tickets: [6061]
- title: "HTML Input: Fix a parsing bug that was triggered in rare conditions"
tickets: [6064]
- title: "HTML2Zip plugin: Do not replace ligatures"
tickets: [6019]
- title: "iTunes driver: Fix transmission of non integral series numbers"
tickets: [6046]
- title: "Simplify implementation of cover caching and ensure cover browser is updated when covers are changed"
- title: "PDF metadata: Fix last character corrupted when setting metadata in encrypted files."
- title: "PDF metadata: Update the version of PoDoFo used to set metadata to 0.8.1. Hopefully that means more PDF files will work"
- title: "Device drivers: Speedup for dumping metadata cache to devices on Windows XP"
- title: "EPUB Output: Ensure that language setting is conformant to the specs"
- title: "MOBI Output: Fix a memory leak and a crash in the palmdoc compression routine"
- title: "Metadata download: Fix a regression that resulted in a failed download for some books"
new recipes:
- title: "Foreign Policy and Alo!"
author: Darko Miletic
- title: Statesman and ifzm
author: rty
improved recipes:
- Akter
- The Old New Thing
- version: 0.7.6
date: 2010-06-28

File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 89 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 75 KiB

After

Width:  |  Height:  |  Size: 11 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 117 KiB

After

Width:  |  Height:  |  Size: 5.0 KiB

View File

@ -1752,7 +1752,7 @@
sodipodi:cy="93.331604"
sodipodi:cx="-166.53223"
id="path6082"
style="opacity:1;fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:1.08779998;stroke-opacity:1;filter:url(#filter6074)"
style="opacity:1;fill:url(#radialGradient6084);fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:1.08779998;stroke-opacity:1;filter:url(#filter6074)"
sodipodi:type="arc" /></clipPath><radialGradient
inkscape:collect="always"
xlink:href="#linearGradient5990"
@ -2513,7 +2513,7 @@
transform="matrix(-1.7332269,0,0,1.7332269,-228.13814,-101.76485)"
clip-path="none" /><path
sodipodi:type="arc"
style="opacity:1;fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:1.08779998;stroke-opacity:1;filter:url(#filter6074)"
style="opacity:1;fill:url(#radialGradient6084);fill-opacity:1;stroke:none;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:1.08779998;stroke-opacity:1;filter:url(#filter6074)"
id="path3915"
sodipodi:cx="-166.53223"
sodipodi:cy="93.331604"
@ -2901,22 +2901,8 @@
id="g133">
<defs
id="defs135" />
<use
id="use138"
x="0"
y="0"
width="121"
height="120" />
<clipPath
id="XMLID_215_">
<use
id="use141"
x="0"
y="0"
width="121"
height="120" />
</clipPath>
<g
clip-path="url(#XMLID_215_)"

Before

Width:  |  Height:  |  Size: 116 KiB

After

Width:  |  Height:  |  Size: 116 KiB

File diff suppressed because it is too large Load Diff

After

Width:  |  Height:  |  Size: 133 KiB

File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 3.1 KiB

269
resources/images/help.svg Normal file
View File

@ -0,0 +1,269 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 12.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 51448) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://web.resource.org/cc/"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.0"
id="Livello_1"
width="128"
height="128"
viewBox="0 0 139 139"
overflow="visible"
enable-background="new 0 0 139 139"
xml:space="preserve"
sodipodi:version="0.32"
inkscape:version="0.45+devel"
sodipodi:docname="system-help.svgz"
inkscape:output_extension="org.inkscape.output.svgz.inkscape"
style="overflow:visible"><metadata
id="metadata3164"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /></cc:Work></rdf:RDF></metadata><defs
id="defs3162"><filter
inkscape:collect="always"
x="-0.132641"
width="1.265282"
y="-0.34752154"
height="1.6950431"
id="filter3547"><feGaussianBlur
inkscape:collect="always"
stdDeviation="2.7512044"
id="feGaussianBlur3549" /></filter><filter
inkscape:collect="always"
id="filter5097"><feGaussianBlur
inkscape:collect="always"
stdDeviation="2.32"
id="feGaussianBlur5099" /></filter><filter
inkscape:collect="always"
x="-0.143268"
width="1.286536"
y="-0.072184406"
height="1.1443688"
id="filter5125"><feGaussianBlur
inkscape:collect="always"
stdDeviation="1.91024"
id="feGaussianBlur5127" /></filter></defs><sodipodi:namedview
inkscape:window-height="697"
inkscape:window-width="1024"
inkscape:pageshadow="2"
inkscape:pageopacity="0.0"
guidetolerance="10.0"
gridtolerance="10.0"
objecttolerance="10.0"
borderopacity="1.0"
bordercolor="#666666"
pagecolor="#ffffff"
id="base"
inkscape:zoom="2.9352518"
inkscape:cx="99.496726"
inkscape:cy="69.329657"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:current-layer="Livello_1"
height="128px"
width="128px" />
<filter
id="AI_Sfocatura_4">
<feGaussianBlur
stdDeviation="4"
id="feGaussianBlur3096" />
</filter>
<filter
id="AI_Sfocatura_2">
<feGaussianBlur
stdDeviation="2"
id="feGaussianBlur3099" />
</filter>
<radialGradient
id="XMLID_12_"
cx="69.600098"
cy="69.576698"
r="58"
gradientTransform="matrix(1,0,0,-0.1823,0,134.8566)"
gradientUnits="userSpaceOnUse">
<stop
offset="0"
style="stop-color:#000000"
id="stop3102" />
<stop
offset="1"
style="stop-color:#000000;stop-opacity:0;"
id="stop3104" />
</radialGradient>
<circle
sodipodi:ry="58"
sodipodi:rx="58"
sodipodi:cy="69.599998"
sodipodi:cx="69.599998"
style="opacity:0.7;fill:#000000;fill-opacity:1;stroke:none;filter:url(#filter5097)"
id="circle5091"
r="58"
cy="69.599998"
cx="69.599998"
transform="matrix(1.0859375,0,0,1.0859375,-3.9093733,-8.2531233)" /><ellipse
cx="69.599998"
cy="122.173"
rx="58"
ry="10.573"
id="ellipse3106"
style="opacity:0.6;fill:url(#XMLID_12_)"
sodipodi:cx="69.599998"
sodipodi:cy="122.173"
sodipodi:rx="58"
sodipodi:ry="10.573"
transform="translate(-9.9998474e-2,1.9102535)" />
<radialGradient
id="XMLID_13_"
cx="69.600098"
cy="69.600098"
r="58"
gradientUnits="userSpaceOnUse">
<stop
offset="0.6154"
style="stop-color:#EEEEEE"
id="stop3113" />
<stop
offset="0.8225"
style="stop-color:#DDDDDD"
id="stop3115" />
<stop
offset="1"
style="stop-color:#FFFFFF"
id="stop3117" />
</radialGradient>
<circle
cx="69.599998"
cy="69.599998"
r="58"
id="circle3119"
style="fill:url(#XMLID_13_)"
sodipodi:cx="69.599998"
sodipodi:cy="69.599998"
sodipodi:rx="58"
sodipodi:ry="58"
transform="matrix(1.0859375,0,0,1.0859375,-3.9093733,-8.2531233)" />
<linearGradient
id="XMLID_14_"
gradientUnits="userSpaceOnUse"
x1="27.6001"
y1="69.600098"
x2="111.6001"
y2="69.600098"
gradientTransform="matrix(1.0859375,0,0,1.0859375,-3.9093733,-8.2531233)">
<stop
offset="0"
style="stop-color:#2A94EC"
id="stop3122" />
<stop
offset="1"
style="stop-color:#0057AE"
id="stop3124" />
</linearGradient>
<path
d="M 26.062502,67.328127 C 26.062502,92.477355 46.522651,112.9375 71.671877,112.9375 C 96.821104,112.9375 117.28125,92.477355 117.28125,67.328127 C 117.28125,42.178901 96.821104,21.718753 71.671877,21.718753 C 46.522651,21.718753 26.062502,42.178901 26.062502,67.328127 z"
id="path3126"
style="fill:url(#XMLID_14_)" />
<g
id="circle22111"
cy="92"
rx="36"
ry="36"
cx="343.99899"
enable-background="new "
style="opacity:0.3;filter:url(#filter3547)"
transform="matrix(1.0859375,0,0,1.0859375,-3.9093733,-8.2531233)">
<path
d="M 77.041,104.759 C 63.767,106.115 50.122,103.11 46.565,98.042 C 43.007,92.976 50.885,87.768 64.16,86.41 C 77.434,85.054 91.079,88.058 94.637,93.126 C 98.193,98.194 90.315,103.401 77.041,104.759 z"
id="path3129"
style="fill:#a8dde0" />
</g>
<linearGradient
id="circle16776_1_"
gradientUnits="userSpaceOnUse"
x1="135.5601"
y1="417.66461"
x2="161.87621"
y2="417.66461"
gradientTransform="matrix(0,1.7280523,1.7280523,0,-650.07477,-218.71693)">
<stop
offset="0"
style="stop-color:#FFFFFF"
id="stop3132" />
<stop
offset="1"
style="stop-color:#ffffff;stop-opacity:0;"
id="stop3134" />
</linearGradient>
<path
id="circle16776"
enable-background="new "
d="M 71.671877,24.06655 C 50.288682,24.06655 32.41958,38.77123 28.113838,58.349597 C 36.698174,66.142284 52.986151,54.358777 71.671877,54.358777 C 90.357604,54.358777 106.64666,66.142284 115.22991,58.349597 C 110.92417,38.77123 93.056158,24.06655 71.671877,24.06655 z"
style="opacity:0.8;fill:url(#circle16776_1_)" />
<g
id="g3137"
transform="matrix(1.0859375,0,0,1.0859375,-3.9093733,-8.2531233)">
<defs
id="defs3139"><path
id="XMLID_10_"
d="M 27.6,69.6 C 27.6,92.759 46.441,111.6 69.6,111.6 C 92.759,111.6 111.6,92.759 111.6,69.6 C 111.6,46.441 92.759,27.6 69.6,27.6 C 46.441,27.6 27.6,46.441 27.6,69.6 z" /></defs>
<clipPath
id="XMLID_6_">
<use
xlink:href="#XMLID_10_"
id="use3143"
x="0"
y="0"
width="139"
height="139" />
</clipPath>
<g
clip-path="url(#XMLID_6_)"
id="g3145"
style="filter:url(#AI_Sfocatura_2)">
<path
d="M 27.6,69.6 C 27.6,92.759 46.441,111.6 69.6,111.6 C 92.759,111.6 111.6,92.759 111.6,69.6 C 111.6,46.441 92.759,27.6 69.6,27.6 C 46.441,27.6 27.6,46.441 27.6,69.6 z"
id="path3147"
style="fill:none;stroke:#00316e;stroke-width:2" />
</g>
</g>
<g
transform="matrix(1.0859375,0,0,1.1113796,-3.201342,-9.3177223)"
id="g5119"
style="fill:#00316e;filter:url(#filter5125)"><path
style="fill:#00316e"
d="M 63.37,80.089 L 63.192,77.746 C 63.012,73.148 64.44,68.462 68.451,63.684 C 71.304,60.26 73.62,57.286 73.62,54.221 C 73.62,51.157 71.571,48.994 67.202,48.903 C 64.173,48.903 60.696,49.895 58.289,51.517 L 55.348,41.784 C 58.556,39.89 63.815,38.088 70.233,38.088 C 81.91,38.088 87.348,44.668 87.348,52.058 C 87.348,58.997 83.069,63.415 79.681,67.289 C 76.472,70.894 75.046,74.41 75.135,78.466 L 75.135,80.088 L 63.37,80.088 L 63.37,80.089 z"
id="path5121" /><circle
style="fill:#00316e"
sodipodi:ry="8"
sodipodi:rx="8"
sodipodi:cy="93.599998"
sodipodi:cx="69.599998"
cx="69.599998"
cy="93.599998"
r="8"
id="circle5123" /></g><g
id="g5101"
transform="matrix(1.0859375,0,0,1.0859375,-3.201342,-8.2531233)"><path
id="path3157"
d="M 63.37,80.089 L 63.192,77.746 C 63.012,73.148 64.44,68.462 68.451,63.684 C 71.304,60.26 73.62,57.286 73.62,54.221 C 73.62,51.157 71.571,48.994 67.202,48.903 C 64.173,48.903 60.696,49.895 58.289,51.517 L 55.348,41.784 C 58.556,39.89 63.815,38.088 70.233,38.088 C 81.91,38.088 87.348,44.668 87.348,52.058 C 87.348,58.997 83.069,63.415 79.681,67.289 C 76.472,70.894 75.046,74.41 75.135,78.466 L 75.135,80.088 L 63.37,80.088 L 63.37,80.089 z"
style="fill:#ffffff" /><circle
id="circle3159"
r="8"
cy="93.599998"
cx="69.599998"
sodipodi:cx="69.599998"
sodipodi:cy="93.599998"
sodipodi:rx="8"
sodipodi:ry="8"
style="fill:#ffffff" /></g>
</svg>

After

Width:  |  Height:  |  Size: 8.4 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 109 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 753 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 6.3 KiB

File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 69 KiB

After

Width:  |  Height:  |  Size: 7.2 KiB

File diff suppressed because it is too large Load Diff

Before

Width:  |  Height:  |  Size: 73 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@ -15,7 +15,7 @@ class Akter(BasicNewsRecipe):
category = 'vesti, online vesti, najnovije vesti, politika, sport, ekonomija, biznis, finansije, berza, kultura, zivot, putovanja, auto, automobili, tehnologija, politicki magazin, dogadjaji, desavanja, lifestyle, zdravlje, zdravstvo, vest, novine, nedeljnik, srbija, novi sad, vojvodina, svet, drustvo, zabava, republika srpska, beograd, intervju, komentar, reportaza, arhiva vesti, news, serbia, politics'
oldest_article = 8
max_articles_per_feed = 100
no_stylesheets = False
no_stylesheets = True
use_embedded_content = False
encoding = 'utf-8'
masthead_url = 'http://www.akter.co.rs/templates/gk_thenews2/images/style2/logo.png'
@ -23,9 +23,9 @@ class Akter(BasicNewsRecipe):
publication_type = 'magazine'
remove_empty_feeds = True
PREFIX = 'http://www.akter.co.rs'
extra_css = """ @font-face {font-family: "serif1";src:url(res:///opt/sony/ebook/FONT/tt0011m_.ttf)}
extra_css = """
@font-face {font-family: "sans1";src:url(res:///opt/sony/ebook/FONT/tt0003m_.ttf)}
.article_description,body,.lokacija{font-family: Arial,Helvetica,sans1,sans-serif}
.article_description,body{font-family: Arial,Helvetica,sans1,sans-serif}
.color-2{display:block; margin-bottom: 10px; padding: 5px, 10px;
border-left: 1px solid #D00000; color: #D00000}
img{margin-bottom: 0.8em} """

View File

@ -0,0 +1,65 @@
__license__ = 'GPL v3'
__copyright__ = '2010, Darko Miletic <darko.miletic at gmail.com>'
'''
www.alo.rs
'''
import re
from calibre.web.feeds.recipes import BasicNewsRecipe
class Alo_Novine(BasicNewsRecipe):
title = 'Alo!'
__author__ = 'Darko Miletic'
description = "News Portal from Serbia"
publisher = 'Alo novine d.o.o.'
category = 'news, politics, Serbia'
oldest_article = 2
max_articles_per_feed = 100
delay = 4
no_stylesheets = True
encoding = 'utf-8'
use_embedded_content = False
language = 'sr'
extra_css = """
@font-face {font-family: "sans1";src:url(res:///opt/sony/ebook/FONT/tt0003m_.ttf)}
.article_description,body{font-family: Arial,Helvetica,sans1,sans-serif}
.lead {font-size: 1.3em}
h1{color: #DB0700}
.article_uvod{font-style: italic; font-size: 1.2em}
img{margin-bottom: 0.8em} """
conversion_options = {
'comment' : description
, 'tags' : category
, 'publisher': publisher
, 'language' : language
}
preprocess_regexps = [(re.compile(u'\u0110'), lambda match: u'\u00D0')]
remove_tags = [dict(name=['object','link','embed'])]
remove_attributes = ['height','width']
feeds = [
(u'Najnovije Vijesti', u'http://www.alo.rs/rss/danasnje_vesti')
,(u'Politika' , u'http://www.alo.rs/rss/politika')
,(u'Vesti' , u'http://www.alo.rs/rss/vesti')
,(u'Sport' , u'http://www.alo.rs/rss/sport')
,(u'Ljudi' , u'http://www.alo.rs/rss/ljudi')
,(u'Saveti' , u'http://www.alo.rs/rss/saveti')
]
def preprocess_html(self, soup):
for item in soup.findAll(style=True):
del item['style']
return soup
def print_version(self, url):
artl = url.rpartition('/')[0]
artid = artl.rpartition('/')[2]
return 'http://www.alo.rs/resources/templates/tools/print.php?id=' + artid
def image_url_processor(self, baseurl, url):
return url.replace('alo.rs//','alo.rs/')

View File

@ -0,0 +1,40 @@
from calibre.web.feeds.news import BasicNewsRecipe
class AdvancedUserRecipe1278347258(BasicNewsRecipe):
title = u'Anchorage Daily News'
__author__ = 'rty'
oldest_article = 7
max_articles_per_feed = 100
feeds = [(u'Alaska News', u'http://www.adn.com/news/alaska/index.xml'),
(u'Business', u'http://www.adn.com/money/index.xml'),
(u'Sports', u'http://www.adn.com/sports/index.xml'),
(u'Politics', u'http://www.adn.com/politics/index.xml'),
(u'Lifestyles', u'http://www.adn.com/life/index.xml'),
(u'Iditarod', u'http://www.adn.com/iditarod/index.xml')
]
description = ''''Alaska's Newspaper'''
publisher = 'http://www.adn.com'
category = 'news, Alaska, Anchorage'
language = 'en'
extra_css = '''
p{font-weight: normal;text-align: justify}
'''
remove_javascript = True
use_embedded_content = False
no_stylesheets = True
language = 'en'
encoding = 'latin-1'
conversion_options = {'linearize_tables':True}
masthead_url = 'http://media.adn.com/includes/assets/images/adn_logo.2.gif'
keep_only_tags = [
dict(name='div', attrs={'class':'left_col story_mainbar'}),
]
remove_tags = [
dict(name='div', attrs={'class':'story_tools'}),
dict(name='p', attrs={'class':'ad_label'}),
]
remove_tags_after = [
dict(name='div', attrs={'class':'advertisement'}),
]

View File

@ -0,0 +1,39 @@
from calibre.web.feeds.news import BasicNewsRecipe
class AdvancedUserRecipe1277443634(BasicNewsRecipe):
title = u'BBC Chinese'
oldest_article = 7
max_articles_per_feed = 100
feeds = [
(u'\u4e3b\u9875', u'http://www.bbc.co.uk/zhongwen/simp/index.xml'),
(u'\u56fd\u9645\u65b0\u95fb', u'http://www.bbc.co.uk/zhongwen/simp/world/index.xml'),
(u'\u4e24\u5cb8\u4e09\u5730', u'http://www.bbc.co.uk/zhongwen/simp/china/index.xml'),
(u'\u91d1\u878d\u8d22\u7ecf', u'http://www.bbc.co.uk/zhongwen/simp/business/index.xml'),
(u'\u7f51\u4e0a\u4e92\u52a8', u'http://www.bbc.co.uk/zhongwen/simp/interactive/index.xml'),
(u'\u97f3\u89c6\u56fe\u7247', u'http://www.bbc.co.uk/zhongwen/simp/multimedia/index.xml'),
(u'\u5206\u6790\u8bc4\u8bba', u'http://www.bbc.co.uk/zhongwen/simp/indepth/index.xml')
]
extra_css = '''
@font-face {font-family: "DroidFont", serif, sans-serif; src: url(res:///system/fonts/DroidSansFallback.ttf); }\n
body {margin-right: 8pt; font-family: 'DroidFont', serif;}\n
h1 {font-family: 'DroidFont', serif;}\n
.articledescription {font-family: 'DroidFont', serif;}
'''
__author__ = 'rty'
__version__ = '1.0'
language = 'zh'
pubisher = 'British Broadcasting Corporation'
description = 'BBC news in Chinese'
category = 'News, Chinese'
remove_javascript = True
use_embedded_content = False
no_stylesheets = True
encoding = 'UTF-8'
conversion_options = {'linearize_tables':True}
masthead_url = 'http://wscdn.bbc.co.uk/zhongwen/simp/images/1024/brand.jpg'
keep_only_tags = [
dict(name='h1'),
dict(name='p', attrs={'class':['primary-topic','summary']}),
dict(name='div', attrs={'class':['bodytext','datestamp']}),
]

View File

@ -0,0 +1,64 @@
from calibre.web.feeds.news import BasicNewsRecipe
class BigOven(BasicNewsRecipe):
title = 'BigOven'
__author__ = 'Starson17'
description = 'Recipes for the Foodie in us all. Registration is free. A fake username and password just gives smaller photos.'
language = 'en'
category = 'news, food, recipes, gourmet'
publisher = 'Starson17'
use_embedded_content= False
no_stylesheets = True
oldest_article = 24
remove_javascript = True
remove_empty_feeds = True
cover_url = 'http://www.software.com/images/products/BigOven%20Logo_177_216.JPG'
max_articles_per_feed = 30
needs_subscription = True
conversion_options = {'linearize_tables' : True
, 'comment' : description
, 'tags' : category
, 'publisher' : publisher
, 'language' : language
}
def get_browser(self):
br = BasicNewsRecipe.get_browser()
if self.username is not None and self.password is not None:
br.open('http://www.bigoven.com/')
br.select_form(name='form1')
br['TopMenu_bo1$email'] = self.username
br['TopMenu_bo1$password'] = self.password
br.submit()
return br
remove_attributes = ['style', 'font']
keep_only_tags = [dict(name='h1')
,dict(name='div', attrs={'class':'img'})
,dict(name='div', attrs={'id':'intro'})
]
remove_tags = [dict(name='div', attrs={'style':["overflow: visible;"]})
,dict(name='div', attrs={'class':['ctas']})
#,dict(name='a', attrs={'class':['edit']})
,dict(name='p', attrs={'class':['byline']})
]
feeds = [(u'4 & 5 Star Rated Recipes', u'http://feeds.feedburner.com/Bigovencom-RecipeRaves?format=xml')]
def preprocess_html(self, soup):
for tag in soup.findAll(name='a', attrs={'class':['edit']}):
tag.parent.extract()
for tag in soup.findAll(name='a', attrs={'class':['deflink']}):
tag.replaceWith(tag.string)
return soup
extra_css = '''
h1{font-family:Arial,Helvetica,sans-serif; font-weight:bold;font-size:large;}
h2{font-family:Arial,Helvetica,sans-serif; font-weight:normal;font-size:medium;}
p{font-family:Arial,Helvetica,sans-serif;font-size:small;}
body{font-family:Helvetica,Arial,sans-serif;font-size:small;}
'''

View File

@ -0,0 +1,39 @@
from calibre.web.feeds.news import BasicNewsRecipe
class AdvancedUserRecipe1278162597(BasicNewsRecipe):
__author__ = 'rty'
title = u'China Economic Net'
oldest_article = 7
max_articles_per_feed = 100
pubisher = 'www.ce.cn - China Economic net - Beijing'
description = 'China Economic Net Magazine'
category = 'Economic News Magazine, Chinese, China'
feeds = [
(u'Stock Market 股市', u'http://finance.ce.cn/stock/index_6304.xml'),
(u'Money 理财', u'http://finance.ce.cn/money/index_6301.xml'),
(u'Health 健康', u'http://www.ce.cn/health/index_6294.xml'),
(u'Technology 科技', u'http://sci.ce.cn/mainpage/index_6307.xml'),
(u'Domestic Politics 国内时政', u'http://www.ce.cn/xwzx/gnsz/index_6273.xml')
]
masthead_url = 'http://finance.ce.cn/images/08mdy_logo.gif'
extra_css = '''
@font-face {font-family: "DroidFont", serif, sans-serif; src: url(res:///system/fonts/DroidSansFallback.ttf); }\n
body {margin-right: 8pt; font-family: 'DroidFont', serif;}\n
h1 {font-family: 'DroidFont', serif;}\n
.articledescription {font-family: 'DroidFont', serif;}
'''
remove_javascript = True
use_embedded_content = False
no_stylesheets = True
language = 'zh-cn'
encoding = 'gb2312'
conversion_options = {'linearize_tables':True}
keep_only_tags = [
dict(name='h1', attrs={'id':'articleTitle'}),
dict(name='div', attrs={'class':'laiyuan'}),
dict(name='div', attrs={'id':'articleText'}),
]

View File

@ -7,7 +7,7 @@ class AdvancedUserRecipe1277228948(BasicNewsRecipe):
__author__ = 'rty'
__version__ = '1.0'
language = 'zh_CN'
language = 'zh'
pubisher = 'www.chinapressusa.com'
description = 'Overseas Chinese Network Newspaper in the USA'
category = 'News in Chinese, USA'

View File

@ -1,14 +1,29 @@
import re
#!/usr/bin/env python
__license__ = 'GPL v3'
__copyright__ = '2010 elsuave'
from calibre.web.feeds.news import BasicNewsRecipe
class EandP(BasicNewsRecipe):
title = u'Editor and Publisher'
__author__ = u'Xanthan Gum'
__author__ = u'elsuave (modified from Xanthan Gum)'
description = 'News about newspapers and journalism.'
publisher = 'Editor and Publisher'
category = 'news, journalism, industry'
language = 'en'
no_stylesheets = True
max_articles_per_feed = 25
no_stylesheets = True
use_embedded_content = False
encoding = 'utf8'
cover_url = 'http://www.editorandpublisher.com/images/EP_main_logo.gif'
remove_javascript = True
oldest_article = 7
max_articles_per_feed = 100
html2lrf_options = [
'--comment', description
, '--category', category
, '--publisher', publisher
]
html2epub_options = 'publisher="' + publisher + '"\ncomments="' + description + '"\ntags="' + category + '"'
# Font formatting code borrowed from kwetal
@ -18,17 +33,21 @@ class EandP(BasicNewsRecipe):
h2{font-size: large;}
'''
# Delete everything before the article
# Keep only div:itemmgap
remove_tags_before = dict(name='font', attrs={'class':'titlebar_black'})
keep_only_tags = [
dict(name='div', attrs={'class':'itemmgap'})
]
# Delete everything after the article
# Remove commenting/social media lins
preprocess_regexps = [(re.compile(r'<!--endclickprintinclude-->.*</body>', re.DOTALL|re.IGNORECASE),
lambda match: '</body>'),]
remove_tags_after = [dict(name='div', attrs={'class':'clear'})]
feeds = [(u'Breaking News', u'http://www.editorandpublisher.com/GenerateRssFeed.aspx'),
(u'Business News', u'http://www.editorandpublisher.com/GenerateRssFeed.aspx?CategoryId=2'),
(u'Ad/Circ News', u'http://www.editorandpublisher.com/GenerateRssFeed.aspx?CategoryId=3'),
(u'Newsroom', u'http://www.editorandpublisher.com/GenerateRssFeed.aspx?CategoryId=4'),
(u'Technology News', u'http://www.editorandpublisher.com/GenerateRssFeed.aspx?CategoryId=5'),
(u'Syndicates News', u'http://www.editorandpublisher.com/GenerateRssFeed.aspx?CategoryId=7')]
feeds = [(u'Breaking News', u'http://feeds.feedburner.com/EditorAndPublisher-BreakingNews'),
(u'Business News', u'http://feeds.feedburner.com/EditorAndPublisher-BusinessNews'),
(u'Newsroom', u'http://feeds.feedburner.com/EditorAndPublisher-Newsroom'),
(u'Technology News', u'http://feeds.feedburner.com/EditorAndPublisher-Technology'),
(u'Syndicates News', u'http://feeds.feedburner.com/EditorAndPublisher-Syndicates')]

View File

@ -1,7 +1,7 @@
#!/usr/bin/env python
__license__ = 'GPL v3'
__copyright__ = '2009, Darko Miletic <darko.miletic at gmail.com>'
__copyright__ = '2010, elsuave'
'''
estadao.com.br
'''
@ -10,12 +10,12 @@ from calibre.web.feeds.news import BasicNewsRecipe
class Estadao(BasicNewsRecipe):
title = 'O Estado de S. Paulo'
__author__ = 'Darko Miletic'
__author__ = 'elsuave (modified from Darko Miletic)'
description = 'News from Brasil in Portuguese'
publisher = 'O Estado de S. Paulo'
category = 'news, politics, Brasil'
oldest_article = 2
max_articles_per_feed = 100
max_articles_per_feed = 25
no_stylesheets = True
use_embedded_content = False
encoding = 'utf8'
@ -30,13 +30,14 @@ class Estadao(BasicNewsRecipe):
html2epub_options = 'publisher="' + publisher + '"\ncomments="' + description + '"\ntags="' + category + '"'
keep_only_tags = [dict(name='div', attrs={'id':'c1'})]
keep_only_tags = [
dict(name='div', attrs={'class':['bb-md-noticia','c5']})
]
remove_tags = [
dict(name=['script','object','form','ul'])
,dict(name='div', attrs={'id':['votacao','estadaohoje']})
,dict(name='p', attrs={'id':'ctrl_texto'})
,dict(name='p', attrs={'class':'texto'})
,dict(name='div', attrs={'class':['fnt2 Color_04 bold','right fnt2 innerTop15 dvTmFont','™_01 right outerLeft15','tituloBox','tags']})
,dict(name='div', attrs={'id':['bb-md-noticia-subcom']})
]
feeds = [
@ -51,13 +52,12 @@ class Estadao(BasicNewsRecipe):
,(u'Vida &', u'http://www.estadao.com.br/rss/vidae.xml')
]
def preprocess_html(self, soup):
ifr = soup.find('iframe')
if ifr:
ifr.extract()
for item in soup.findAll(style=True):
del item['style']
return soup
language = 'pt'
def get_article_url(self, article):
url = BasicNewsRecipe.get_article_url(self, article)
if '/Multimidia/' not in url:
return url

View File

@ -0,0 +1,45 @@
__license__ = 'GPL v3'
__copyright__ = '2010, Darko Miletic <darko.miletic at gmail.com>'
'''
www.foreignpolicy.com
'''
from calibre.web.feeds.news import BasicNewsRecipe
class ForeignPolicy(BasicNewsRecipe):
title = 'Foreign Policy'
__author__ = 'Darko Miletic'
description = 'International News'
publisher = 'Washingtonpost.Newsweek Interactive, LLC'
category = 'news, politics, USA'
oldest_article = 31
max_articles_per_feed = 200
no_stylesheets = True
encoding = 'utf8'
use_embedded_content = False
language = 'en'
remove_empty_feeds = True
extra_css = ' body{font-family: Georgia,"Times New Roman",Times,serif } img{margin-bottom: 0.4em} h1,h2,h3,h4,h5,h6{font-family: Arial,Helvetica,sans-serif} '
conversion_options = {
'comment' : description
, 'tags' : category
, 'publisher' : publisher
, 'language' : language
}
keep_only_tags = [dict(attrs={'id':['art-mast','art-body','auth-bio']})]
remove_tags = [dict(name='iframe'),dict(attrs={'id':['share-box','base-ad']})]
remove_attributes = ['height','width']
feeds = [(u'Articles', u'http://www.foreignpolicy.com/node/feed')]
def print_version(self, url):
return url + '?print=yes&page=full'
def preprocess_html(self, soup):
for item in soup.findAll(style=True):
del item['style']
return soup

View File

@ -1,56 +1,95 @@
__license__ = 'GPL v3'
__copyright__ = '2010, Darko Miletic <darko.miletic at gmail.com>'
'''
haaretz.com
www.haaretz.com
'''
import re
from calibre import strftime
from time import gmtime
from calibre.web.feeds.news import BasicNewsRecipe
class Haaretz_en(BasicNewsRecipe):
title = 'Haaretz in English'
class HaaretzPrint_en(BasicNewsRecipe):
title = 'Haaretz - print edition'
__author__ = 'Darko Miletic'
description = 'Haaretz.com, the online edition of Haaretz Newspaper in Israel, and analysis from Israel and the Middle East. Haaretz.com provides extensive and in-depth coverage of Israel, the Jewish World and the Middle East, including defense, diplomacy, the Arab-Israeli conflict, the peace process, Israeli politics, Jerusalem affairs, international relations, Iran, Iraq, Syria, Lebanon, the Palestinian Authority, the West Bank and the Gaza Strip, the Israeli business world and Jewish life in Israel and the Diaspora. '
publisher = 'haaretz.com'
category = 'news, politics, Israel'
description = "Haaretz.com is the world's leading English-language Website for real-time news and analysis of Israel and the Middle East."
publisher = 'Haaretz'
category = "news, Haaretz, Israel news, Israel newspapers, Israel business news, Israel financial news, Israeli news,Israeli newspaper, Israeli newspapers, news from Israel, news in Israel, news Israel, news on Israel, newspaper Israel, Israel sports news, Israel diplomacy news"
oldest_article = 2
max_articles_per_feed = 200
no_stylesheets = True
encoding = 'cp1252'
encoding = 'utf8'
use_embedded_content = False
language = 'en_IL'
publication_type = 'newspaper'
remove_empty_feeds = True
masthead_url = 'http://www.haaretz.com/images/logos/logoGrey.gif'
PREFIX = 'http://www.haaretz.com'
masthead_url = PREFIX + '/images/logos/logoGrey.gif'
extra_css = ' body{font-family: Verdana,Arial,Helvetica,sans-serif } '
preprocess_regexps = [(re.compile(r'</body>.*?</html>', re.DOTALL|re.IGNORECASE),lambda match: '</body></html>')]
conversion_options = {
'comment' : description
, 'tags' : category
, 'publisher' : publisher
, 'language' : language
'comment' : description
, 'tags' : category
, 'publisher': publisher
, 'language' : language
}
remove_tags = [dict(name='div', attrs={'class':['rightcol']}),dict(name='table')]
remove_tags_before = dict(name='h1')
remove_tags_after = dict(attrs={'id':'innerArticle'})
keep_only_tags = [dict(attrs={'id':'content'})]
keep_only_tags = [dict(attrs={'id':'threecolumns'})]
remove_attributes = ['width','height']
remove_tags = [
dict(name=['iframe','link','object','embed'])
,dict(name='div',attrs={'class':'rightcol'})
]
feeds = [
(u'Opinion' , u'http://www.haaretz.com/cmlink/opinion-rss-1.209234?localLinksEnabled=false' )
,(u'Defense and diplomacy' , u'http://www.haaretz.com/cmlink/defense-and-diplomacy-rss-1.208894?localLinksEnabled=false')
,(u'National' , u'http://www.haaretz.com/cmlink/national-rss-1.208896?localLinksEnabled=false' )
,(u'International' , u'http://www.haaretz.com/cmlink/international-rss-1.208898?localLinksEnabled=false' )
,(u'Jewish World' , u'http://www.haaretz.com/cmlink/jewish-world-rss-1.209085?localLinksEnabled=false' )
,(u'Business' , u'http://www.haaretz.com/cmlink/business-print-rss-1.264904?localLinksEnabled=false' )
,(u'Real Estate' , u'http://www.haaretz.com/cmlink/real-estate-print-rss-1.264977?localLinksEnabled=false' )
,(u'Features' , u'http://www.haaretz.com/cmlink/features-print-rss-1.264912?localLinksEnabled=false' )
,(u'Arts and leisure' , u'http://www.haaretz.com/cmlink/arts-and-leisure-rss-1.286090?localLinksEnabled=false' )
,(u'Books' , u'http://www.haaretz.com/cmlink/books-rss-1.264947?localLinksEnabled=false' )
,(u'Food and Wine' , u'http://www.haaretz.com/cmlink/food-and-wine-print-rss-1.265034?localLinksEnabled=false' )
,(u'Sports' , u'http://www.haaretz.com/cmlink/sports-rss-1.286092?localLinksEnabled=false' )
(u'News' , PREFIX + u'/print-edition/news' )
,(u'Opinion' , PREFIX + u'/print-edition/opinion' )
,(u'Business' , PREFIX + u'/print-edition/business' )
,(u'Real estate' , PREFIX + u'/print-edition/real-estate' )
,(u'Sports' , PREFIX + u'/print-edition/sports' )
,(u'Travel' , PREFIX + u'/print-edition/travel' )
,(u'Books' , PREFIX + u'/print-edition/books' )
,(u'Food & Wine' , PREFIX + u'/print-edition/food-wine' )
,(u'Arts & Leisure', PREFIX + u'/print-edition/arts-leisure' )
,(u'Features' , PREFIX + u'/print-edition/features' )
]
def print_version(self, url):
article = url.rpartition('/')[2]
return 'http://www.haaretz.com/misc/article-print-page/' + article
def parse_index(self):
totalfeeds = []
lfeeds = self.get_feeds()
for feedobj in lfeeds:
feedtitle, feedurl = feedobj
self.report_progress(0, _('Fetching feed')+' %s...'%(feedtitle if feedtitle else feedurl))
articles = []
soup = self.index_to_soup(feedurl)
for item in soup.findAll(attrs={'class':'text'}):
sp = item.find('span',attrs={'class':'h3 font-weight-normal'})
desc = item.find('p')
description = ''
if sp:
if desc:
description = self.tag_to_string(desc)
link = sp.a
url = self.PREFIX + link['href']
title = self.tag_to_string(link)
times = strftime('%a, %d %b %Y %H:%M:%S +0000',gmtime())
articles.append({
'title' :title
,'date' :times
,'url' :url
,'description':description
})
totalfeeds.append((feedtitle, articles))
return totalfeeds
def preprocess_html(self, soup):
for item in soup.findAll(style=True):
del item['style']

View File

@ -0,0 +1,50 @@
from calibre.web.feeds.news import BasicNewsRecipe
class AdvancedUserRecipe1277305250(BasicNewsRecipe):
title = u'infzm - China Southern Weekly'
oldest_article = 14
max_articles_per_feed = 100
feeds = [(u'\u5357\u65b9\u5468\u672b-\u70ed\u70b9\u65b0\u95fb', u'http://www.infzm.com/rss/home/rss2.0.xml'),
(u'\u5357\u65b9\u5468\u672b-\u7ecf\u6d4e\u65b0\u95fb', u'http://www.infzm.com/rss/economic.xml'),
(u'\u5357\u65b9\u5468\u672b-\u6587\u5316\u65b0\u95fb', u'http://www.infzm.com/rss/culture.xml'),
(u'\u5357\u65b9\u5468\u672b-\u751f\u6d3b\u65f6\u5c1a', u'http://www.infzm.com/rss/lifestyle.xml'),
(u'\u5357\u65b9\u5468\u672b-\u89c2\u70b9', u'http://www.infzm.com/rss/opinion.xml')
]
__author__ = 'rty'
__version__ = '1.0'
language = 'zh'
pubisher = 'http://www.infzm.com'
description = 'Chinese Weekly Tabloid'
category = 'News, China'
remove_javascript = True
use_embedded_content = False
no_stylesheets = True
#encoding = 'GB2312'
encoding = 'UTF-8'
conversion_options = {'linearize_tables':True}
masthead_url = 'http://i50.tinypic.com/2qmfb7l.jpg'
extra_css = '''
@font-face { font-family: "DroidFont", serif, sans-serif; src: url(res:///system/fonts/DroidSansFallback.ttf); }\n
body {
margin-right: 8pt;
font-family: 'DroidFont', serif;}
.detailContent {font-family: 'DroidFont', serif, sans-serif}
'''
keep_only_tags = [
dict(name='div', attrs={'id':'detailContent'}),
]
remove_tags = [
dict(name='div', attrs={'id':['detailTools', 'detailSideL', 'pageNum']}),
]
remove_tags_after = [
dict(name='div', attrs={'id':'pageNum'}),
]
def preprocess_html(self, soup):
for item in soup.findAll(color=True):
del item['font']
for item in soup.findAll(style=True):
del item['style']
return soup

View File

@ -89,6 +89,7 @@ class NYTimes(BasicNewsRecipe):
'relatedSearchesModule',
'side_tool',
'singleAd',
'subNavigation clearfix',
'subNavigation tabContent active',
'subNavigation tabContent active clearfix',
]}),
@ -460,8 +461,10 @@ class NYTimes(BasicNewsRecipe):
if mp_off >= 0:
c = c[:mp_off]
emTag.insert(0, c)
hrTag = Tag(soup, 'hr')
#hrTag['style'] = "margin-top:0em;margin-bottom:0em"
#hrTag = Tag(soup, 'hr')
#hrTag['class'] = 'caption_divider'
hrTag = Tag(soup, 'div')
hrTag['class'] = 'divider'
emTag.insert(1, hrTag)
caption.replaceWith(emTag)

View File

@ -76,6 +76,7 @@ class NYTimes(BasicNewsRecipe):
'relatedSearchesModule',
'side_tool',
'singleAd',
'subNavigation clearfix',
'subNavigation tabContent active',
'subNavigation tabContent active clearfix',
]}),
@ -335,7 +336,7 @@ class NYTimes(BasicNewsRecipe):
self.log(">>> No class:'columnGroup first' found <<<")
# Change class="kicker" to <h3>
kicker = soup.find(True, {'class':'kicker'})
if kicker and kicker.contents[0]:
if kicker and kicker.contents and kicker.contents[0]:
h3Tag = Tag(soup, "h3")
h3Tag.insert(0, self.fixChars(self.tag_to_string(kicker,
use_alt=False)))
@ -350,8 +351,10 @@ class NYTimes(BasicNewsRecipe):
if mp_off >= 0:
c = c[:mp_off]
emTag.insert(0, c)
hrTag = Tag(soup, 'hr')
#hrTag['style'] = "margin-top:0em;margin-bottom:0em"
#hrTag = Tag(soup, 'hr')
#hrTag['class'] = 'caption_divider'
hrTag = Tag(soup, 'div')
hrTag['class'] = 'divider'
emTag.insert(1, hrTag)
caption.replaceWith(emTag)
@ -457,8 +460,10 @@ class NYTimes(BasicNewsRecipe):
return self.massageNCXText(self.tag_to_string(p,use_alt=False))
return None
article.author = extract_author(soup)
article.summary = article.text_summary = extract_description(soup)
if not article.author:
article.author = extract_author(soup)
if not article.summary:
article.summary = article.text_summary = extract_description(soup)
def strip_anchors(self,soup):
paras = soup.findAll(True)

View File

@ -0,0 +1,79 @@
from calibre.web.feeds.recipes import BasicNewsRecipe
class AdvancedUserRecipe1278063072(BasicNewsRecipe):
title = u'Singtao Daily - Canada'
oldest_article = 7
max_articles_per_feed = 100
__author__ = 'rty'
description = 'Toronto Canada Chinese Newspaper'
publisher = 'news.singtao.ca'
category = 'Chinese, News, Canada'
remove_javascript = True
use_embedded_content = False
no_stylesheets = True
language = 'zh'
conversion_options = {'linearize_tables':True}
masthead_url = 'http://news.singtao.ca/i/site_2009/logo.jpg'
extra_css = '''
@font-face {font-family: "DroidFont", serif, sans-serif; src: url(res:///system/fonts/DroidSansFallback.ttf); }\
body {text-align: justify; margin-right: 8pt; font-family: 'DroidFont', serif;}\
h1 {font-family: 'DroidFont', serif;}\
.articledescription {font-family: 'DroidFont', serif;}
'''
keep_only_tags = [
dict(name='div', attrs={'id':['title','storybody']}),
dict(name='div', attrs={'class':'content'})
]
def parse_index(self):
feeds = []
for title, url in [
('Editorial',
'http://news.singtao.ca/toronto/editorial.html'),
('Toronto \xe5\x9f\x8e\xe5\xb8\x82/\xe7\xa4\xbe\xe5\x8d\x80'.decode('utf-8'),
'http://news.singtao.ca/toronto/city.html'),
('Canada \xe5\x8a\xa0\xe5\x9c\x8b'.decode('utf-8'),
'http://news.singtao.ca/toronto/canada.html'),
('Entertainment',
'http://news.singtao.ca/toronto/entertainment.html'),
('World',
'http://news.singtao.ca/toronto/world.html'),
('Finance \xe5\x9c\x8b\xe9\x9a\x9b\xe8\xb2\xa1\xe7\xb6\x93'.decode('utf-8'),
'http://news.singtao.ca/toronto/finance.html'),
('Sports', 'http://news.singtao.ca/toronto/sports.html'),
]:
articles = self.parse_section(url)
if articles:
feeds.append((title, articles))
return feeds
def parse_section(self, url):
soup = self.index_to_soup(url)
div = soup.find(attrs={'class': ['newslist paddingL10T10','newslist3 paddingL10T10']})
#date = div.find(attrs={'class': 'underlineBLK'})
current_articles = []
for li in div.findAll('li'):
a = li.find('a', href = True)
if a is None:
continue
title = self.tag_to_string(a)
url = a.get('href', False)
if not url or not title:
continue
if url.startswith('/'):
url = 'http://news.singtao.ca'+url
# self.log('\ \ Found article:', title)
# self.log('\ \ \ ', url)
current_articles.append({'title': title, 'url': url, 'description':''})
return current_articles
def preprocess_html(self, soup):
for item in soup.findAll(style=True):
del item['style']
for item in soup.findAll(width=True):
del item['width']
return soup

View File

@ -0,0 +1,35 @@
from calibre.web.feeds.news import BasicNewsRecipe
class AdvancedUserRecipe1278049615(BasicNewsRecipe):
title = u'Statesman'
pubisher = 'http://www.statesman.com/'
description = 'Austin Texas Daily Newspaper'
category = 'News, Austin, Texas'
__author__ = 'rty'
oldest_article = 3
max_articles_per_feed = 100
feeds = [(u'News', u'http://www.statesman.com/section-rss.do?source=news&includeSubSections=true'),
(u'Business', u'http://www.statesman.com/section-rss.do?source=business&includeSubSections=true'),
(u'Life', u'http://www.statesman.com/section-rss.do?source=life&includesubsection=true'),
(u'Editorial', u'http://www.statesman.com/section-rss.do?source=opinion&includesubsections=true'),
(u'Sports', u'http://www.statesman.com/section-rss.do?source=sports&includeSubSections=true')
]
masthead_url = "http://www.statesman.com/images/cmg-logo.gif"
#temp_files = []
#articles_are_obfuscated = True
remove_javascript = True
use_embedded_content = False
no_stylesheets = True
language = 'en'
encoding = 'utf-8'
conversion_options = {'linearize_tables':True}
remove_tags = [
dict(name='div', attrs={'id':'cxArticleOptions'}),
]
keep_only_tags = [
dict(name='div', attrs={'class':'cxArticleHeader'}),
dict(name='div', attrs={'id':'cxArticleBodyText'}),
]

View File

@ -40,13 +40,14 @@ class LinuxFreeze(Command):
'/usr/bin/pdftohtml',
'/usr/lib/libwmflite-0.2.so.7',
'/usr/lib/liblcms.so.1',
'/usr/lib/libstlport.so.5.1',
'/tmp/calibre-mount-helper',
'/usr/lib/libunrar.so',
'/usr/lib/libchm.so.0',
'/usr/lib/libsqlite3.so.0',
'/usr/lib/libsqlite3.so.0',
'/usr/lib/libmng.so.1',
'/usr/lib/libpodofo.so.0.6.99',
'/usr/lib/libpodofo.so.0.8.1',
'/lib/libz.so.1',
'/lib/libuuid.so.1',
'/usr/lib/libtiff.so.3',

View File

@ -265,6 +265,9 @@ class Py2App(object):
@flush
def get_local_dependencies(self, path_to_lib):
for x in self.get_dependencies(path_to_lib):
if x.startswith('libpodofo'):
yield x, x
continue
for y in (SW+'/lib/', '/usr/local/lib/', SW+'/qt/lib/',
'/opt/local/lib/',
'/Library/Frameworks/Python.framework/', SW+'/freetype/lib/'):
@ -397,7 +400,7 @@ class Py2App(object):
@flush
def add_podofo(self):
info('\nAdding PoDoFo')
pdf = join(SW, 'lib', 'libpodofo.0.6.99.dylib')
pdf = join(SW, 'lib', 'libpodofo.0.8.1.dylib')
self.install_dylib(pdf)
@flush

View File

@ -162,9 +162,50 @@ SET(WANT_LIB64 FALSE)
SET(PODOFO_BUILD_SHARED TRUE)
SET(PODOFO_BUILD_STATIC FALSE)
cp build/podofo-0.7.0/build/src/Release/podofo.dll bin/
cp build/podofo-0.7.0/build/src/Release/podofo.lib lib/
cp build/podofo-0.7.0/build/src/Release/podofo.exp lib/
cp build/podofo/build/src/Release/podofo.dll bin/
cp build/podofo/build/src/Release/podofo.lib lib/
cp build/podofo/build/src/Release/podofo.exp lib/
cp build/podofo/build/podofo_config.h include/podofo/
cp -r build/podofo/src/* include/podofo/
The following patch was required to get it to compile:
Index: src/PdfImage.cpp
===================================================================
--- src/PdfImage.cpp (revision 1261)
+++ src/PdfImage.cpp (working copy)
@@ -627,7 +627,7 @@
long lLen = static_cast<long>(pInfo->rowbytes * height);
char* pBuffer = static_cast<char*>(malloc(sizeof(char) * lLen));
- png_bytep pRows[height];
+ png_bytepp pRows = static_cast<png_bytepp>(malloc(sizeof(png_bytep)*height));
for(int y=0; y<height; y++)
{
pRows[y] = reinterpret_cast<png_bytep>(pBuffer + (y * pInfo->rowbytes));
@@ -672,6 +672,7 @@
this->SetImageData( width, height, pInfo->bit_depth, &stream );
free(pBuffer);
+ free(pRows);
}
#endif // PODOFO_HAVE_PNG_LIB
Index: src/PdfFiltersPrivate.cpp
===================================================================
--- src/PdfFiltersPrivate.cpp (revision 1261)
+++ src/PdfFiltersPrivate.cpp (working copy)
@@ -1019,7 +1019,7 @@
/*
* Prepare for input from a memory buffer.
*/
-GLOBAL(void)
+void
jpeg_memory_src (j_decompress_ptr cinfo, const JOCTET * buffer, size_t bufsize)
{
my_src_ptr src;
ImageMagick
--------------

View File

@ -153,18 +153,10 @@
<Property Id="WixShellExecTarget" Value="[#{exe_map[calibre]}]" />
<CustomAction Id="LaunchApplication" BinaryKey="WixCA"
DllEntry="WixShellExec" Impersonate="yes"/>
<InstallExecuteSequence>
<FileCost Suppress="yes" />
</InstallExecuteSequence>
<InstallUISequence>
<FileCost Suppress="yes" />
</InstallUISequence>
<AdminExecuteSequence>
<FileCost Suppress="yes" />
</AdminExecuteSequence>
<AdminUISequence>
<FileCost Suppress="yes" />
</AdminUISequence>
</Product>
</Wix>

View File

@ -342,13 +342,6 @@ def detect_ncpus():
return ans
def launch(path_or_url):
from PyQt4.QtCore import QUrl
from PyQt4.QtGui import QDesktopServices
if os.path.exists(path_or_url):
path_or_url = 'file:'+path_or_url
QDesktopServices.openUrl(QUrl(path_or_url))
relpath = os.path.relpath
_spat = re.compile(r'^the\s+|^a\s+|^an\s+', re.IGNORECASE)
def english_sort(x, y):

View File

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

View File

@ -30,6 +30,7 @@ every time you add an HTML file to the library.\
with TemporaryDirectory('_plugin_html2zip') as tdir:
recs =[('debug_pipeline', tdir, OptionRecommendation.HIGH)]
recs.append(['keep_ligatures', True, OptionRecommendation.HIGH])
if self.site_customization and self.site_customization.strip():
recs.append(['input_encoding', self.site_customization.strip(),
OptionRecommendation.HIGH])
@ -81,7 +82,7 @@ class PML2PMLZ(FileTypePlugin):
return of.name
# Metadata reader plugins {{{
class ComicMetadataReader(MetadataReaderPlugin):
name = 'Read comic metadata'
@ -319,7 +320,9 @@ class ZipMetadataReader(MetadataReaderPlugin):
def get_metadata(self, stream, ftype):
from calibre.ebooks.metadata.zip import get_metadata
return get_metadata(stream)
# }}}
# Metadata writer plugins {{{
class EPUBMetadataWriter(MetadataWriterPlugin):
@ -395,6 +398,7 @@ class TOPAZMetadataWriter(MetadataWriterPlugin):
from calibre.ebooks.metadata.topaz import set_metadata
set_metadata(stream, mi)
# }}}
from calibre.ebooks.comic.input import ComicInput
from calibre.ebooks.epub.input import EPUBInput
@ -444,7 +448,7 @@ from calibre.devices.kindle.driver import KINDLE, KINDLE2, KINDLE_DX
from calibre.devices.nook.driver import NOOK
from calibre.devices.prs505.driver import PRS505
from calibre.devices.android.driver import ANDROID, S60
from calibre.devices.nokia.driver import N770, N810, E71X
from calibre.devices.nokia.driver import N770, N810, E71X, E52
from calibre.devices.eslick.driver import ESLICK, EBK52
from calibre.devices.nuut2.driver import NUUT2
from calibre.devices.iriver.driver import IRIVER_STORY
@ -519,6 +523,7 @@ plugins += [
S60,
N770,
E71X,
E52,
N810,
COOL_ER,
ESLICK,

View File

@ -275,13 +275,44 @@ class iPadOutput(OutputProfile):
# touchscreen_news_css {{{
touchscreen_news_css = u'''
/* hr used in articles */
.article_articles_list {
width:18%;
}
.article_link {
color: #593f29;
font-style: italic;
}
.article_next {
-webkit-border-top-right-radius:4px;
-webkit-border-bottom-right-radius:4px;
font-style: italic;
width:32%;
}
.article_prev {
-webkit-border-top-left-radius:4px;
-webkit-border-bottom-left-radius:4px;
font-style: italic;
width:32%;
}
.article_sections_list {
width:18%;
}
.articles_link {
font-weight: bold;
}
.sections_link {
font-weight: bold;
}
.caption_divider {
border:#ccc 1px solid;
}
.touchscreen_navbar {
background:#ccc;
border:#ccc 1px solid;
background:#c3bab2;
border:#ccc 0px solid;
border-collapse:separate;
border-spacing:1px;
margin-left: 5%;
@ -292,22 +323,16 @@ class iPadOutput(OutputProfile):
.touchscreen_navbar td {
background:#fff;
font-family:Helvetica;
font-size:90%;
padding: 5px;
font-size:80%;
/* UI touchboxes use 8px padding */
padding: 6px;
text-align:center;
}
.touchscreen_navbar td:first-child {
-webkit-border-top-left-radius:4px;
-webkit-border-bottom-left-radius:4px;
}
.touchscreen_navbar td:last-child {
-webkit-border-top-right-radius:4px;
-webkit-border-bottom-right-radius:4px;
}
.feed_link {
font-style: italic;
}
.touchscreen_navbar td a:link {
color: #593f29;
text-decoration: none;
}
/* Index formatting */
.publish_date {
@ -318,12 +343,50 @@ class iPadOutput(OutputProfile):
border-top:1px solid gray;
}
hr.caption_divider {
border-color:black;
border-style:solid;
border-width:1px;
}
/* Feed summary formatting */
.article_summary {
display:inline-block;
}
.feed {
font-family:sans-serif;
font-weight:bold;
font-size:larger;
}
.feed_link {
font-style: italic;
}
.feed_next {
-webkit-border-top-right-radius:4px;
-webkit-border-bottom-right-radius:4px;
font-style: italic;
width:40%;
}
.feed_prev {
-webkit-border-top-left-radius:4px;
-webkit-border-bottom-left-radius:4px;
font-style: italic;
width:40%;
}
.feed_title {
text-align: center;
font-size: 160%;
}
.feed_up {
font-weight: bold;
width:20%;
}
.summary_headline {
font-weight:bold;
text-align:left;
@ -338,12 +401,6 @@ class iPadOutput(OutputProfile):
text-align:left;
}
.feed {
font-family:sans-serif;
font-weight:bold;
font-size:larger;
}
'''
# }}}

File diff suppressed because it is too large Load Diff

View File

@ -106,9 +106,11 @@ class BOOX(HANLINV3):
description = _('Communicate with the BOOX eBook reader.')
author = 'Jesus Manuel Marinho Valcarce'
supported_platforms = ['windows', 'osx', 'linux']
METADATA_CACHE = '.metadata.calibre'
# Ordered list of supported formats
FORMATS = ['epub', 'fb2', 'djvu', 'pdf', 'html', 'txt', 'rtf', 'mobi', 'prc', 'chm']
FORMATS = ['epub', 'fb2', 'djvu', 'pdf', 'html', 'txt', 'rtf', 'mobi',
'prc', 'chm', 'doc']
VENDOR_ID = [0x0525]
PRODUCT_ID = [0xa4a5]

View File

@ -67,3 +67,24 @@ class E71X(USBMS):
VENDOR_NAME = 'NOKIA'
WINDOWS_MAIN_MEM = 'S60'
class E52(USBMS):
name = 'Nokia E52 device interface'
gui_name = 'Nokia E52'
description = _('Communicate with the Nokia E52')
author = 'David Ignjic'
supported_platforms = ['windows', 'linux', 'osx']
VENDOR_ID = [0x421]
PRODUCT_ID = [0x1CD]
BCD = [0x100]
FORMATS = ['mobi', 'prc']
EBOOK_DIR_MAIN = 'eBooks'
SUPPORTS_SUB_DIRS = True
VENDOR_NAME = 'NOKIA'
WINDOWS_MAIN_MEM = 'S60'

View File

@ -144,52 +144,73 @@ class XMLCache(object):
if title+str(i) not in seen:
title = title+str(i)
playlist.set('title', title)
seen.add(title)
break
else:
seen.add(title)
def build_playlist_id_map(self):
debug_print('Start build_playlist_id_map')
ans = {}
self.ensure_unique_playlist_titles()
debug_print('after ensure_unique_playlist_titles')
self.prune_empty_playlists()
for i, root in self.record_roots.items():
debug_print('build_playlist_id_map loop', i)
id_map = self.build_id_map(root)
ans[i] = []
for playlist in root.xpath('//*[local-name()="playlist"]'):
items = []
for item in playlist:
id_ = item.get('id', None)
record = id_map.get(id_, None)
if record is not None:
items.append(record)
ans[i].append((playlist.get('title'), items))
debug_print('end build_playlist_id_map')
return ans
def build_id_playlist_map(self, bl_index):
'''
Return a map of the collections in books: {lpaths: [collection names]}
'''
debug_print('Start build_id_playlist_map')
pmap = self.build_playlist_id_map()[bl_index]
self.ensure_unique_playlist_titles()
self.prune_empty_playlists()
debug_print('after cleaning playlists')
root = self.record_roots[bl_index]
if root is None:
return
id_map = self.build_id_map(root)
playlist_map = {}
for title, records in pmap:
for record in records:
path = record.get('path', None)
if path:
if path not in playlist_map:
playlist_map[path] = []
playlist_map[path].append(title)
# foreach playlist, get the lpaths for the ids in it, then add to dict
for playlist in root.xpath('//*[local-name()="playlist"]'):
name = playlist.get('title')
if name is None:
debug_print('build_id_playlist_map: unnamed playlist!')
continue
for item in playlist:
# translate each id into its lpath
id_ = item.get('id', None)
if id_ is None:
debug_print('build_id_playlist_map: id_ is None!')
continue
bk = id_map.get(id_, None)
if bk is None:
debug_print('build_id_playlist_map: book is None!', id_)
continue
lpath = bk.get('path', None)
if lpath is None:
debug_print('build_id_playlist_map: lpath is None!', id_)
continue
if lpath not in playlist_map:
playlist_map[lpath] = []
playlist_map[lpath].append(name)
debug_print('Finish build_id_playlist_map. Found', len(playlist_map))
return playlist_map
def reset_existing_playlists_map(self):
'''
Call this method before calling get_or_create_playlist in the context of
a given job. Call it again after deleting any playlists. The current
implementation adds all new playlists before deleting any, so that
constraint is respected.
'''
self._playlist_to_playlist_id_map = {}
def get_or_create_playlist(self, bl_idx, title):
# maintain a private map of playlists to their ids. Don't check if it
# exists, because reset_existing_playlist_map must be called before it
# is used to ensure that deleted playlists are taken into account
root = self.record_roots[bl_idx]
for playlist in root.xpath('//*[local-name()="playlist"]'):
if playlist.get('title', None) == title:
return playlist
if DEBUG:
debug_print('Creating playlist:', title)
if bl_idx not in self._playlist_to_playlist_id_map:
self._playlist_to_playlist_id_map[bl_idx] = {}
for playlist in root.xpath('//*[local-name()="playlist"]'):
pl_title = playlist.get('title', None)
if pl_title is not None:
self._playlist_to_playlist_id_map[bl_idx][pl_title] = playlist
if title in self._playlist_to_playlist_id_map[bl_idx]:
return self._playlist_to_playlist_id_map[bl_idx][title]
debug_print('Creating playlist:', title)
ans = root.makeelement('{%s}playlist'%self.namespaces[bl_idx],
nsmap=root.nsmap, attrib={
'uuid' : uuid(),
@ -198,6 +219,7 @@ class XMLCache(object):
'sourceid': '1'
})
root.append(ans)
self._playlist_to_playlist_id_map[bl_idx][title] = ans
return ans
# }}}
@ -260,7 +282,9 @@ class XMLCache(object):
ensure_media_xml_base_ids(root)
idmap = ensure_numeric_ids(root)
remap_playlist_references(root, idmap)
if len(idmap) > 0:
debug_print('fix_ids: found some non-numeric ids')
remap_playlist_references(root, idmap)
if i == 0:
sourceid, playlist_sid = 1, 0
base = 0
@ -321,17 +345,20 @@ class XMLCache(object):
debug_print('Updating XML Cache:', i)
root = self.record_roots[i]
lpath_map = self.build_lpath_map(root)
gtz_count = ltz_count = 0
for book in booklist:
path = os.path.join(self.prefixes[i], *(book.lpath.split('/')))
record = lpath_map.get(book.lpath, None)
if record is None:
record = self.create_text_record(root, i, book.lpath)
self.update_text_record(record, book, path, i)
(gtz_count, ltz_count) = self.update_text_record(record, book,
path, i, gtz_count, ltz_count)
# Ensure the collections in the XML database are recorded for
# this book
if book.device_collections is None:
book.device_collections = []
book.device_collections = playlist_map.get(book.lpath, [])
debug_print('Timezone votes: %d GMT, %d LTZ'%(gtz_count, ltz_count))
self.update_playlists(i, root, booklist, collections_attributes)
# Update the device collections because update playlist could have added
# some new ones.
@ -352,8 +379,10 @@ class XMLCache(object):
def update_playlists(self, bl_index, root, booklist, collections_attributes):
debug_print('Starting update_playlists', collections_attributes, bl_index)
self.reset_existing_playlists_map()
collections = booklist.get_collections(collections_attributes)
lpath_map = self.build_lpath_map(root)
debug_print('update_playlists: finished building maps')
for category, books in collections.items():
records = [lpath_map.get(b.lpath, None) for b in books]
# Remove any books that were not found, although this
@ -362,23 +391,34 @@ class XMLCache(object):
debug_print('WARNING: Some elements in the JSON cache were not'
' found in the XML cache')
records = [x for x in records if x is not None]
# Ensure each book has an ID.
for rec in records:
if rec.get('id', None) is None:
rec.set('id', str(self.max_id(root)+1))
ids = [x.get('id', None) for x in records]
# Given that we set the ids, there shouldn't be any None's. But
# better to be safe...
if None in ids:
debug_print('WARNING: Some <text> elements do not have ids')
ids = [x for x in ids if x is not None]
playlist = self.get_or_create_playlist(bl_index, category)
# Get the books currently in the playlist. We will need them to be
# sure to put back any books that were manually added.
playlist_ids = []
for item in playlist:
id_ = item.get('id', None)
if id_ is not None:
playlist_ids.append(id_)
# Empty the playlist. We do this so that the playlist will have the
# order specified by get_collections
for item in list(playlist):
playlist.remove(item)
# Get a list of ids not known by get_collections
extra_ids = [x for x in playlist_ids if x not in ids]
# Rebuild the collection in the order specified by get_collections. Then
# add the ids that get_collections didn't know about.
for id_ in ids + extra_ids:
item = playlist.makeelement(
'{%s}item'%self.namespaces[bl_index],
@ -416,11 +456,38 @@ class XMLCache(object):
root.append(ans)
return ans
def update_text_record(self, record, book, path, bl_index):
def update_text_record(self, record, book, path, bl_index, gtz_count, ltz_count):
'''
Update the Sony database from the book. This is done if the timestamp in
the db differs from the timestamp on the file.
'''
# It seems that a Sony device can sometimes know what timezone it is in,
# and apparently converts the dates to GMT when it writes them to the
# db. Unfortunately, we can't tell when it does this, so we use a
# horrible heuristic. First, set dates only for new books, trying to
# avoid upsetting the sony. Use the timezone determined through the
# voting described next. Second, voting: if a book is not new, compare
# its Sony DB date against localtime and gmtime. Count the matches. When
# we must set a date, use the one with the most matches. Use localtime
# if the case of a tie, and hope it is right.
timestamp = os.path.getmtime(path)
date = strftime(timestamp)
if date != record.get('date', None):
rec_date = record.get('date', None)
if not getattr(book, '_new_book', False): # book is not new
if strftime(timestamp, zone=time.gmtime) == rec_date:
gtz_count += 1
elif strftime(timestamp, zone=time.localtime) == rec_date:
ltz_count += 1
else: # book is new. Set the time using the current votes
if ltz_count >= gtz_count:
tz = time.localtime
debug_print("Using localtime TZ for new book", book.lpath)
else:
tz = time.gmtime
debug_print("Using GMT TZ for new book", book.lpath)
date = strftime(timestamp, zone=tz)
record.set('date', date)
record.set('size', str(os.stat(path).st_size))
title = book.title if book.title else _('Unknown')
record.set('title', title)
@ -445,6 +512,7 @@ class XMLCache(object):
if 'id' not in record.attrib:
num = self.max_id(record.getroottree().getroot())
record.set('id', str(num+1))
return (gtz_count, ltz_count)
# }}}
# Writing the XML files {{{

View File

@ -134,9 +134,16 @@ class CollectionsBookList(BookList):
def get_collections(self, collection_attributes):
collections = {}
series_categories = set([])
# This map of sets is used to avoid linear searches when testing for
# book equality
collections_lpaths = {}
for book in self:
# The default: leave the book in all existing collections. Do not
# add any new ones.
# Make sure we can identify this book via the lpath
lpath = getattr(book, 'lpath', None)
if lpath is None:
continue
# Decide how we will build the collections. The default: leave the
# book in all existing collections. Do not add any new ones.
attrs = ['device_collections']
if getattr(book, '_new_book', False):
if prefs['preserve_user_collections']:
@ -163,11 +170,12 @@ class CollectionsBookList(BookList):
continue
if category not in collections:
collections[category] = []
if book not in collections[category]:
collections_lpaths[category] = set()
if lpath not in collections_lpaths[category]:
collections_lpaths[category].add(lpath)
collections[category].append(book)
if attr == 'series':
series_categories.add(category)
if attr == 'series':
series_categories.add(category)
# Sort collections
for category, books in collections.items():
def tgetter(x):

View File

@ -290,7 +290,7 @@ class USBMS(CLI, Device):
js = [item.to_json() for item in booklists[listid] if
hasattr(item, 'to_json')]
with open(self.normalize_path(os.path.join(prefix, self.METADATA_CACHE)), 'wb') as f:
json.dump(js, f, indent=2, encoding='utf-8')
f.write(json.dumps(js, indent=2, encoding='utf-8'))
write_prefix(self._main_prefix, 0)
write_prefix(self._card_a_prefix, 1)
write_prefix(self._card_b_prefix, 2)

View File

@ -92,7 +92,7 @@ class CHMInput(InputFormatPlugin):
metadata.add('identifier', mi.isbn, attrib={'scheme':'ISBN'})
if not metadata.language:
oeb.logger.warn(u'Language not specified')
metadata.add('language', get_lang())
metadata.add('language', get_lang().replace('_', '-'))
if not metadata.creator:
oeb.logger.warn('Creator not specified')
metadata.add('creator', _('Unknown'))

View File

@ -329,7 +329,7 @@ class HTMLInput(InputFormatPlugin):
metadata.add('identifier', mi.isbn, attrib={'scheme':'ISBN'})
if not metadata.language:
oeb.logger.warn(u'Language not specified')
metadata.add('language', get_lang())
metadata.add('language', get_lang().replace('_', '-'))
if not metadata.creator:
oeb.logger.warn('Creator not specified')
metadata.add('creator', self.oeb.translate(__('Unknown')))

View File

@ -0,0 +1,15 @@
#!/usr/bin/env python
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
__license__ = 'GPL v3'
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
from calibre.customize import Plugin
class CoverDownload(Plugin):
supported_platforms = ['windows', 'osx', 'linux']
author = 'Kovid Goyal'
type = _('Cover download')

View File

@ -15,7 +15,6 @@ from calibre.utils.config import OptionParser
from calibre.ebooks.metadata.fetch import MetadataSource
from calibre.utils.date import parse_date, utcnow
DOUBAN_API_KEY = None
NAMESPACES = {
'openSearch':'http://a9.com/-/spec/opensearchrss/1.0/',
'atom' : 'http://www.w3.org/2005/Atom',
@ -35,13 +34,15 @@ date = XPath("descendant::db:attribute[@name='pubdate']")
creator = XPath("descendant::db:attribute[@name='author']")
tag = XPath("descendant::db:tag")
CALIBRE_DOUBAN_API_KEY = '0bd1672394eb1ebf2374356abec15c3d'
class DoubanBooks(MetadataSource):
name = 'Douban Books'
description = _('Downloads metadata from Douban.com')
supported_platforms = ['windows', 'osx', 'linux'] # Platforms this plugin will run on
author = 'Li Fanxi <lifanxi@freemindworld.com>' # The author of this plugin
version = (1, 0, 0) # The version number of this plugin
version = (1, 0, 1) # The version number of this plugin
def fetch(self):
try:
@ -65,7 +66,7 @@ class Query(object):
type = "search"
def __init__(self, title=None, author=None, publisher=None, isbn=None,
max_results=20, start_index=1):
max_results=20, start_index=1, api_key=''):
assert not(title is None and author is None and publisher is None and \
isbn is None)
assert (int(max_results) < 21)
@ -89,16 +90,16 @@ class Query(object):
if self.type == "isbn":
self.url = self.ISBN_URL + q
if DOUBAN_API_KEY is not None:
self.url = self.url + "?apikey=" + DOUBAN_API_KEY
if api_key != '':
self.url = self.url + "?apikey=" + api_key
else:
self.url = self.SEARCH_URL+urlencode({
'q':q,
'max-results':max_results,
'start-index':start_index,
})
if DOUBAN_API_KEY is not None:
self.url = self.url + "&apikey=" + DOUBAN_API_KEY
if api_key != '':
self.url = self.url + "&apikey=" + api_key
def __call__(self, browser, verbose):
if verbose:
@ -177,7 +178,7 @@ class ResultList(list):
d = None
return d
def populate(self, entries, browser, verbose=False):
def populate(self, entries, browser, verbose=False, api_key=''):
for x in entries:
try:
id_url = entry_id(x)[0].text
@ -186,8 +187,8 @@ class ResultList(list):
report(verbose)
mi = MetaInformation(title, self.get_authors(x))
try:
if DOUBAN_API_KEY is not None:
id_url = id_url + "?apikey=" + DOUBAN_API_KEY
if api_key != '':
id_url = id_url + "?apikey=" + api_key
raw = browser.open(id_url).read()
feed = etree.fromstring(raw)
x = entry(feed)[0]
@ -203,12 +204,16 @@ class ResultList(list):
self.append(mi)
def search(title=None, author=None, publisher=None, isbn=None,
verbose=False, max_results=40):
verbose=False, max_results=40, api_key=None):
br = browser()
start, entries = 1, []
if api_key is None:
api_key = CALIBRE_DOUBAN_API_KEY
while start > 0 and len(entries) <= max_results:
new, start = Query(title=title, author=author, publisher=publisher,
isbn=isbn, max_results=max_results, start_index=start)(br, verbose)
isbn=isbn, max_results=max_results, start_index=start, api_key=api_key)(br, verbose)
if not new:
break
entries.extend(new)
@ -216,7 +221,7 @@ def search(title=None, author=None, publisher=None, isbn=None,
entries = entries[:max_results]
ans = ResultList()
ans.populate(entries, br, verbose)
ans.populate(entries, br, verbose, api_key)
return ans
def option_parser():

View File

@ -351,9 +351,13 @@ def search(title=None, author=None, publisher=None, isbn=None, isbndb_key=None,
if len(results) > 1:
if not results[0].comments or len(results[0].comments) == 0:
for r in results[1:]:
if title.lower() == r.title[:len(title)].lower() and r.comments and len(r.comments):
results[0].comments = r.comments
break
try:
if title and title.lower() == r.title[:len(title)].lower() \
and r.comments and len(r.comments):
results[0].comments = r.comments
break
except:
pass
# Find a pubdate
pubdate = None
for r in results:

View File

@ -1069,7 +1069,10 @@ class OPFCreator(MetaInformation):
dc_attrs={'id':__appname__+'_id'}))
if getattr(self, 'pubdate', None) is not None:
a(DC_ELEM('date', self.pubdate.isoformat()))
a(DC_ELEM('language', self.language if self.language else get_lang()))
lang = self.language
if not lang or lang.lower() == 'und':
lang = get_lang().replace('_', '-')
a(DC_ELEM('language', lang))
if self.comments:
a(DC_ELEM('description', self.comments))
if self.publisher:
@ -1194,7 +1197,8 @@ def metadata_to_opf(mi, as_string=True):
factory(DC('identifier'), mi.isbn, scheme='ISBN')
if mi.rights:
factory(DC('rights'), mi.rights)
factory(DC('language'), mi.language if mi.language and mi.language.lower() != 'und' else get_lang())
factory(DC('language'), mi.language if mi.language and mi.language.lower()
!= 'und' else get_lang().replace('_', '-'))
if mi.tags:
for tag in mi.tags:
factory(DC('subject'), tag)

View File

@ -8,7 +8,7 @@ from functools import partial
from calibre import prints
from calibre.constants import plugins
from calibre.ebooks.metadata import MetaInformation, string_to_authors, authors_to_string
from calibre.ebooks.metadata import MetaInformation, string_to_authors
pdfreflow, pdfreflow_error = plugins['pdfreflow']
@ -56,66 +56,10 @@ def get_metadata(stream, cover=True):
get_quick_metadata = partial(get_metadata, cover=False)
import cStringIO
from threading import Thread
from calibre.utils.pdftk import set_metadata as pdftk_set_metadata
from calibre.utils.podofo import set_metadata as podofo_set_metadata, Unavailable
from calibre.utils.podofo import set_metadata as podofo_set_metadata
def set_metadata(stream, mi):
stream.seek(0)
try:
return podofo_set_metadata(stream, mi)
except Unavailable:
pass
try:
return pdftk_set_metadata(stream, mi)
except:
pass
set_metadata_pypdf(stream, mi)
class MetadataWriter(Thread):
def __init__(self, out_pdf, buf):
self.out_pdf = out_pdf
self.buf = buf
Thread.__init__(self)
self.daemon = True
def run(self):
try:
self.out_pdf.write(self.buf)
except RuntimeError:
pass
def set_metadata_pypdf(stream, mi):
# Use a StringIO object for the pdf because we will want to over
# write it later and if we are working on the stream directly it
# could cause some issues.
from pyPdf import PdfFileReader, PdfFileWriter
raw = cStringIO.StringIO(stream.read())
orig_pdf = PdfFileReader(raw)
title = mi.title if mi.title else orig_pdf.documentInfo.title
author = authors_to_string(mi.authors) if mi.authors else orig_pdf.documentInfo.author
out_pdf = PdfFileWriter(title=title, author=author)
out_str = cStringIO.StringIO()
writer = MetadataWriter(out_pdf, out_str)
for page in orig_pdf.pages:
out_pdf.addPage(page)
writer.start()
writer.join(10) # Wait 10 secs for writing to complete
out_pdf.killed = True
writer.join()
if out_pdf.killed:
print 'Failed to set metadata: took too long'
return
stream.seek(0)
stream.truncate()
out_str.seek(0)
stream.write(out_str.read())
stream.seek(0)
return podofo_set_metadata(stream, mi)

View File

@ -808,7 +808,8 @@ class Manifest(object):
pat = re.compile(r'&(%s);'%('|'.join(user_entities.keys())))
data = pat.sub(lambda m:user_entities[m.group(1)], data)
parser = etree.XMLParser(no_network=True, huge_tree=True)
# Setting huge_tree=True causes crashes in windows with large files
parser = etree.XMLParser(no_network=True)
# Try with more & more drastic measures to parse
def first_pass(data):
try:
@ -844,7 +845,7 @@ class Manifest(object):
nroot = etree.fromstring('<html></html>')
has_body = False
for child in list(data):
if barename(child.tag) == 'body':
if isinstance(child.tag, (unicode, str)) and barename(child.tag) == 'body':
has_body = True
break
parent = nroot

View File

@ -131,7 +131,7 @@ class OEBReader(object):
stream = cStringIO.StringIO(etree.tostring(opf))
mi = MetaInformation(OPF(stream))
if not mi.language:
mi.language = get_lang()
mi.language = get_lang().replace('_', '-')
self.oeb.metadata.add('language', mi.language)
if not mi.title:
mi.title = self.oeb.translate(__('Unknown'))

View File

@ -25,10 +25,17 @@ from calibre.ebooks.oeb.base import XHTML, XHTML_NS, CSS_MIME, OEB_STYLES
from calibre.ebooks.oeb.base import XPNSMAP, xpath, urlnormalize
from calibre.ebooks.oeb.profile import PROFILES
html_css = open(P('templates/html.css'), 'rb').read()
_html_css_stylesheet = None
def html_css_stylesheet():
global _html_css_stylesheet
if _html_css_stylesheet is None:
html_css = open(P('templates/html.css'), 'rb').read()
_html_css_stylesheet = cssutils.parseString(html_css)
_html_css_stylesheet.namespaces['h'] = XHTML_NS
return _html_css_stylesheet
XHTML_CSS_NAMESPACE = '@namespace "%s";\n' % XHTML_NS
HTML_CSS_STYLESHEET = cssutils.parseString(html_css)
HTML_CSS_STYLESHEET.namespaces['h'] = XHTML_NS
INHERITED = set(['azimuth', 'border-collapse', 'border-spacing',
'caption-side', 'color', 'cursor', 'direction', 'elevation',
@ -120,7 +127,7 @@ class Stylizer(object):
item = oeb.manifest.hrefs[path]
basename = os.path.basename(path)
cssname = os.path.splitext(basename)[0] + '.css'
stylesheets = [HTML_CSS_STYLESHEET]
stylesheets = [html_css_stylesheet()]
head = xpath(tree, '/h:html/h:head')
if head:
head = head[0]

View File

@ -63,7 +63,8 @@ class TXTInput(InputFormatPlugin):
raise ValueError('This txt file has malformed markup, it cannot be'
' converted by calibre. See http://daringfireball.net/projects/markdown/syntax')
else:
html = convert_basic(txt)
flow_size = getattr(options, 'flow_size', 0)
html = convert_basic(txt, epub_split_size_kb=flow_size)
from calibre.customize.ui import plugin_for_input_format
html_input = plugin_for_input_format('html')

View File

@ -17,13 +17,10 @@ __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>'
def convert_basic(txt, title=''):
lines = []
def convert_basic(txt, title='', epub_split_size_kb=0):
# Strip whitespace from the beginning and end of the line. Also replace
# all line breaks with \n.
for line in txt.splitlines():
lines.append(line.strip())
txt = '\n'.join(lines)
txt = '\n'.join([line.strip() for line in txt.splitlines()])
# Condense redundant spaces
txt = re.sub('[ ]{2,}', ' ', txt)
@ -34,6 +31,15 @@ def convert_basic(txt, title=''):
# Remove excessive line breaks.
txt = re.sub('\n{3,}', '\n\n', txt)
#Takes care if there is no point to split
if epub_split_size_kb > 0:
length_byte = len(txt.encode('utf-8'))
#Calculating the average chunk value for easy splitting as EPUB (+2 as a safe margin)
chunk_size = long(length_byte / (int(length_byte / (epub_split_size_kb * 1024) ) + 2 ))
#if there are chunks with a superior size then go and break
if (len(filter(lambda x: len(x.encode('utf-8')) > chunk_size, txt.split('\n\n')))) :
txt = u'\n\n'.join([split_string_separator(line, chunk_size) for line in txt.split('\n\n')])
lines = []
# Split into paragraphs based on having a blank line between text.
for line in txt.split('\n\n'):
@ -71,3 +77,10 @@ def opf_writer(path, opf_name, manifest, spine, mi):
with open(os.path.join(path, opf_name), 'wb') as opffile:
opf.render(opffile)
def split_string_separator(txt, size) :
if len(txt.encode('utf-8')) > size:
txt = u''.join([re.sub(u'\.(?P<ends>[^.]*)$', u'.\n\n\g<ends>',
txt[i:i+size], 1) for i in
xrange(0, len(txt.encode('utf-8')), size)])
return txt

View File

@ -1,18 +1,18 @@
__license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
""" The GUI """
import os
import os, sys
from threading import RLock
from PyQt4.QtCore import QVariant, QFileInfo, QObject, SIGNAL, QBuffer, Qt, QSize, \
from PyQt4.Qt import QVariant, QFileInfo, QObject, SIGNAL, QBuffer, Qt, QSize, \
QByteArray, QTranslator, QCoreApplication, QThread, \
QEvent, QTimer, pyqtSignal, QDate
from PyQt4.QtGui import QFileDialog, QMessageBox, QPixmap, QFileIconProvider, \
QIcon, QApplication, QDialog, QPushButton
QEvent, QTimer, pyqtSignal, QDate, QDesktopServices, \
QFileDialog, QMessageBox, QPixmap, QFileIconProvider, \
QIcon, QApplication, QDialog, QPushButton, QUrl
ORG_NAME = 'KovidsBrain'
APP_UID = 'libprs500'
from calibre import islinux, iswindows, isosx, isfreebsd
from calibre.constants import islinux, iswindows, isosx, isfreebsd, isfrozen
from calibre.utils.config import Config, ConfigProxy, dynamic, JSONConfig
from calibre.utils.localization import set_qt_translator
from calibre.ebooks.metadata.meta import get_metadata, metadata_from_formats
@ -579,6 +579,22 @@ class Application(QApplication):
_store_app = None
def open_url(qurl):
paths = os.environ.get('LD_LIBRARY_PATH',
'').split(os.pathsep)
paths = [x for x in paths if x]
if isfrozen and islinux and paths:
npaths = [x for x in paths if x != sys.frozen_path]
os.environ['LD_LIBRARY_PATH'] = os.pathsep.join(npaths)
QDesktopServices.openUrl(qurl)
if isfrozen and islinux and paths:
os.environ['LD_LIBRARY_PATH'] = os.pathsep.join(paths)
def open_local_file(path):
url = QUrl.fromLocalFile(path)
open_url(url)
def is_ok_to_use_qt():
global gui_thread, _store_app
if (islinux or isfreebsd) and ':' not in os.environ.get('DISPLAY', ''):

View File

@ -5,17 +5,18 @@ __license__ = 'GPL v3'
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
import shutil, os, datetime, sys, time
import shutil, os, datetime, time
from functools import partial
from PyQt4.Qt import QInputDialog, pyqtSignal, QModelIndex, QThread, Qt, \
SIGNAL, QPixmap, QTimer, QDesktopServices, QUrl, QDialog
SIGNAL, QPixmap, QTimer, QDialog
from calibre import strftime
from calibre.ptempfile import PersistentTemporaryFile
from calibre.utils.config import prefs, dynamic
from calibre.gui2 import error_dialog, Dispatcher, gprefs, choose_files, \
choose_dir, warning_dialog, info_dialog, question_dialog, config
choose_dir, warning_dialog, info_dialog, question_dialog, config, \
open_local_file
from calibre.ebooks.BeautifulSoup import BeautifulSoup, Tag, NavigableString
from calibre.utils.filenames import ascii_filename
from calibre.gui2.widgets import IMAGE_EXTENSIONS
@ -25,7 +26,7 @@ from calibre.gui2.dialogs.tag_list_editor import TagListEditor
from calibre.gui2.tools import convert_single_ebook, convert_bulk_ebook, \
fetch_scheduled_recipe, generate_catalog
from calibre.constants import preferred_encoding, filesystem_encoding, \
isosx, isfrozen, islinux
isosx
from calibre.gui2.dialogs.choose_format import ChooseFormatDialog
from calibre.ebooks import BOOK_EXTENSIONS
from calibre.gui2.dialogs.confirm_delete import confirm
@ -322,7 +323,6 @@ class AddAction(object): # {{{
accept = True
if accept:
event.accept()
self.cover_cache.refresh([cid])
self.library_view.model().current_changed(current_idx, current_idx)
def __add_filesystem_book(self, paths, allow_device=True):
@ -645,6 +645,8 @@ class EditMetadataAction(object): # {{{
if x.exception is None:
self.library_view.model().refresh_ids(
x.updated, cr)
if self.cover_flow:
self.cover_flow.dataChanged()
if x.failures:
details = ['%s: %s'%(title, reason) for title,
reason in x.failures.values()]
@ -689,7 +691,6 @@ class EditMetadataAction(object): # {{{
if rows:
current = self.library_view.currentIndex()
m = self.library_view.model()
m.refresh_cover_cache(map(m.id, rows))
if self.cover_flow:
self.cover_flow.dataChanged()
m.current_changed(current, previous)
@ -711,6 +712,8 @@ class EditMetadataAction(object): # {{{
self.library_view.model().resort(reset=False)
self.library_view.model().research()
self.tags_view.recount()
if self.cover_flow:
self.cover_flow.dataChanged()
# Merge books {{{
def merge_books(self, safe_merge=False):
@ -917,7 +920,7 @@ class SaveToDiskAction(object): # {{{
_('Could not save some books') + ', ' +
_('Click the show details button to see which ones.'),
u'\n\n'.join(failures), show=True)
QDesktopServices.openUrl(QUrl.fromLocalFile(path))
open_local_file(path)
def books_saved(self, job):
if job.failed:
@ -1183,15 +1186,7 @@ class ViewAction(object): # {{{
self.job_manager.launch_gui_app(viewer,
kwargs=dict(args=args))
else:
paths = os.environ.get('LD_LIBRARY_PATH',
'').split(os.pathsep)
paths = [x for x in paths if x]
if isfrozen and islinux and paths:
npaths = [x for x in paths if x != sys.frozen_path]
os.environ['LD_LIBRARY_PATH'] = os.pathsep.join(npaths)
QDesktopServices.openUrl(QUrl.fromLocalFile(name))#launch(name)
if isfrozen and islinux and paths:
os.environ['LD_LIBRARY_PATH'] = os.pathsep.join(paths)
open_local_file(name)
time.sleep(2) # User feedback
finally:
self.unsetCursor()
@ -1237,11 +1232,11 @@ class ViewAction(object): # {{{
return
for row in rows:
path = self.library_view.model().db.abspath(row.row())
QDesktopServices.openUrl(QUrl.fromLocalFile(path))
open_local_file(path)
def view_folder_for_id(self, id_):
path = self.library_view.model().db.abspath(id_, index_is_id=True)
QDesktopServices.openUrl(QUrl.fromLocalFile(path))
open_local_file(path)
def view_book(self, triggered):
rows = self.current_view().selectionModel().selectedRows()

View File

@ -15,7 +15,7 @@ from calibre.ebooks.metadata import MetaInformation
from calibre.constants import preferred_encoding, filesystem_encoding
from calibre.utils.config import prefs
class DuplicatesAdder(QThread):
class DuplicatesAdder(QThread): # {{{
# Add duplicate books
def __init__(self, parent, db, duplicates, db_adder):
QThread.__init__(self, parent)
@ -34,9 +34,9 @@ class DuplicatesAdder(QThread):
self.emit(SIGNAL('added(PyQt_PyObject)'), count)
count += 1
self.emit(SIGNAL('adding_done()'))
# }}}
class RecursiveFind(QThread):
class RecursiveFind(QThread): # {{{
def __init__(self, parent, db, root, single):
QThread.__init__(self, parent)
@ -79,7 +79,9 @@ class RecursiveFind(QThread):
if not self.canceled:
self.emit(SIGNAL('found(PyQt_PyObject)'), self.books)
class DBAdder(Thread):
# }}}
class DBAdder(Thread): # {{{
def __init__(self, db, ids, nmap):
self.db, self.ids, self.nmap = db, dict(**ids), dict(**nmap)
@ -219,8 +221,9 @@ class DBAdder(Thread):
self.db.add_format(id, fmt, f, index_is_id=True,
notify=False, replace=replace)
# }}}
class Adder(QObject):
class Adder(QObject): # {{{
ADD_TIMEOUT = 600 # seconds
@ -410,6 +413,7 @@ class Adder(QObject):
return getattr(getattr(self, 'db_adder', None), 'infos',
[])
# }}}
###############################################################################
############################## END ADDER ######################################

View File

@ -9,14 +9,14 @@ import os, collections
from PyQt4.Qt import QLabel, QPixmap, QSize, QWidget, Qt, pyqtSignal, \
QVBoxLayout, QScrollArea, QPropertyAnimation, QEasingCurve, \
QSizePolicy, QPainter, QRect, pyqtProperty, QDesktopServices, QUrl
QSizePolicy, QPainter, QRect, pyqtProperty
from calibre import fit_image, prepare_string_for_xml
from calibre.gui2.widgets import IMAGE_EXTENSIONS
from calibre.ebooks import BOOK_EXTENSIONS
from calibre.constants import preferred_encoding
from calibre.library.comments import comments_to_html
from calibre.gui2 import config
from calibre.gui2 import config, open_local_file
# render_rows(data) {{{
WEIGHTS = collections.defaultdict(lambda : 100)
@ -294,7 +294,7 @@ class BookDetails(QWidget): # {{{
id_, fmt = val.split(':')
self.view_specific_format.emit(int(id_), fmt)
elif typ == 'devpath':
QDesktopServices.openUrl(QUrl.fromLocalFile(val))
open_local_file(val)
def mouseReleaseEvent(self, ev):

View File

@ -67,6 +67,13 @@ if pictureflow is not None:
ans = ''
return ans
def subtitle(self, index):
try:
return u'\u2605'*self.model.rating(index)
except:
pass
return ''
def reset(self):
self.dataChanged.emit()
@ -115,6 +122,7 @@ class CoverFlowMixin(object):
self.sync_cf_to_listview)
self.db_images = DatabaseImages(self.library_view.model())
self.cover_flow.setImages(self.db_images)
self.cover_flow.itemActivated.connect(self.view_specific_book)
else:
self.cover_flow = QLabel('<p>'+_('Cover browser could not be loaded')
+'<br>'+pictureflowerror)

View File

@ -31,6 +31,8 @@ from calibre.utils.smtp import compose_mail, sendmail, extract_email_address, \
config as email_config
from calibre.devices.apple.driver import ITUNES_ASYNC
from calibre.devices.folder_device.driver import FOLDER_DEVICE
from calibre.ebooks.metadata.meta import set_metadata
from calibre.constants import DEBUG
# }}}
@ -304,6 +306,21 @@ class DeviceManager(Thread): # {{{
def _upload_books(self, files, names, on_card=None, metadata=None):
'''Upload books to device: '''
if metadata and files and len(metadata) == len(files):
for f, mi in zip(files, metadata):
if isinstance(f, unicode):
ext = f.rpartition('.')[-1].lower()
if ext:
try:
if DEBUG:
prints('Setting metadata in:', mi.title, 'at:',
f, file=sys.__stdout__)
with open(f, 'r+b') as stream:
set_metadata(stream, mi, stream_type=ext)
except:
if DEBUG:
prints(traceback.format_exc(), file=sys.__stdout__)
return self.device.upload_books(files, names, on_card,
metadata=metadata, end_session=False)
@ -495,7 +512,7 @@ class DeviceMenu(QMenu): # {{{
self.connect_to_folder_action = mitem
mitem = self.addAction(QIcon(I('devices/itunes.png')),
_('Connect to iTunes (EXPERIMENTAL)'))
_('Connect to iTunes'))
mitem.setEnabled(True)
mitem.triggered.connect(lambda x : self.connect_to_itunes.emit())
self.connect_to_itunes_action = mitem
@ -741,10 +758,8 @@ class DeviceMixin(object): # {{{
self.refresh_ondevice_info (device_connected = True, reset_only = True)
else:
self.device_connected = None
self.status_bar.device_disconnected()
self.location_view.model().update_devices()
self.vanity.setText(self.vanity_template%\
dict(version=self.latest_version, device=' '))
self.device_info = ' '
if self.current_view() != self.library_view:
self.book_details.reset_info()
self.location_view.setCurrentIndex(self.location_view.model().index(0))
@ -758,10 +773,7 @@ class DeviceMixin(object): # {{{
return self.device_job_exception(job)
info, cp, fs = job.result
self.location_view.model().update_devices(cp, fs)
self.device_info = _('Connected ')+info[0]
self.vanity.setText(self.vanity_template%\
dict(version=self.latest_version, device=self.device_info))
self.status_bar.device_connected(info[0])
self.device_manager.books(Dispatcher(self.metadata_downloaded))
def metadata_downloaded(self, job):
@ -1145,7 +1157,6 @@ class DeviceMixin(object): # {{{
_files, _auto_ids = self.library_view.model().get_preferred_formats_from_ids(ids,
settings.format_map,
set_metadata=True,
specific_format=specific_format,
exclude_auto=do_auto_convert)
if do_auto_convert:
@ -1416,7 +1427,6 @@ class DeviceMixin(object): # {{{
# the application_id, which is really the db key, but as this can
# accidentally match across libraries we also verify the title. The
# db_id exists on Sony devices. Fallback is title and author match
resend_metadata = False
for booklist in booklists:
for book in booklist:
if getattr(book, 'uuid', None) in self.db_book_uuid_cache:
@ -1433,12 +1443,10 @@ class DeviceMixin(object): # {{{
if getattr(book, 'application_id', None) in d['db_ids']:
book.in_library = True
book.smart_update(d['db_ids'][book.application_id])
resend_metadata = True
continue
if book.db_id in d['db_ids']:
book.in_library = True
book.smart_update(d['db_ids'][book.db_id])
resend_metadata = True
continue
if book.authors:
# Compare against both author and author sort, because
@ -1448,21 +1456,13 @@ class DeviceMixin(object): # {{{
if book_authors in d['authors']:
book.in_library = True
book.smart_update(d['authors'][book_authors])
resend_metadata = True
elif book_authors in d['author_sort']:
book.in_library = True
book.smart_update(d['author_sort'][book_authors])
resend_metadata = True
# Set author_sort if it isn't already
asort = getattr(book, 'author_sort', None)
if not asort and book.authors:
book.author_sort = self.library_view.model().db.author_sort_from_authors(book.authors)
resend_metadata = True
if resend_metadata:
# Correct the metadata cache on device.
if self.device_manager.is_device_connected:
self.device_manager.sync_booklists(None, booklists)
# }}}

View File

@ -5,11 +5,11 @@ __docformat__ = 'restructuredtext en'
import textwrap, os, re
from PyQt4.QtCore import QCoreApplication, SIGNAL, QModelIndex, QUrl, QTimer, Qt
from PyQt4.QtGui import QDialog, QPixmap, QGraphicsScene, QIcon, QDesktopServices
from PyQt4.QtCore import QCoreApplication, SIGNAL, QModelIndex, QTimer, Qt
from PyQt4.QtGui import QDialog, QPixmap, QGraphicsScene, QIcon
from calibre.gui2.dialogs.book_info_ui import Ui_BookInfo
from calibre.gui2 import dynamic
from calibre.gui2 import dynamic, open_local_file
from calibre import fit_image
from calibre.library.comments import comments_to_html
@ -49,12 +49,12 @@ class BookInfo(QDialog, Ui_BookInfo):
def open_book_path(self, path):
if os.sep in unicode(path):
QDesktopServices.openUrl(QUrl.fromLocalFile(path))
open_local_file(path)
else:
format = unicode(path)
path = self.view.model().db.format_abspath(self.current_row, format)
if path is not None:
QDesktopServices.openUrl(QUrl.fromLocalFile(path))
open_local_file(path)
def next(self):
@ -123,6 +123,7 @@ class BookInfo(QDialog, Ui_BookInfo):
for key in info.keys():
if key == 'id': continue
txt = info[key]
txt = u'<br />\n'.join(textwrap.wrap(txt, 120))
if key != _('Path'):
txt = u'<br />\n'.join(textwrap.wrap(txt, 120))
rows += u'<tr><td><b>%s:</b></td><td>%s</td></tr>'%(key, txt)
self.text.setText(u'<table>'+rows+'</table>')

View File

@ -4,7 +4,7 @@ __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
import os, re, time, textwrap, copy, sys
from PyQt4.Qt import QDialog, QListWidgetItem, QIcon, \
QDesktopServices, QVBoxLayout, QLabel, QPlainTextEdit, \
QVBoxLayout, QLabel, QPlainTextEdit, \
QStringListModel, QAbstractItemModel, QFont, \
SIGNAL, QThread, Qt, QSize, QVariant, QUrl, \
QModelIndex, QAbstractTableModel, \
@ -15,8 +15,9 @@ from calibre.constants import iswindows, isosx
from calibre.gui2.dialogs.config.config_ui import Ui_Dialog
from calibre.gui2.dialogs.config.create_custom_column import CreateCustomColumn
from calibre.gui2 import choose_dir, error_dialog, config, gprefs, \
ALL_COLUMNS, NONE, info_dialog, choose_files, \
warning_dialog, ResizableDialog, question_dialog
open_url, open_local_file, \
ALL_COLUMNS, NONE, info_dialog, choose_files, \
warning_dialog, ResizableDialog, question_dialog
from calibre.utils.config import prefs
from calibre.ebooks import BOOK_EXTENSIONS
from calibre.ebooks.oeb.iterator import is_supported
@ -494,6 +495,7 @@ class ConfigDialog(ResizableDialog, Ui_Dialog):
li = i
self.opt_gui_layout.setCurrentIndex(li)
self.opt_disable_animations.setChecked(config['disable_animations'])
self.opt_show_donate_button.setChecked(config['show_donate_button'])
def check_port_value(self, *args):
port = self.port.value()
@ -512,7 +514,7 @@ class ConfigDialog(ResizableDialog, Ui_Dialog):
def open_config_dir(self):
from calibre.utils.config import config_dir
QDesktopServices.openUrl(QUrl.fromLocalFile(config_dir))
open_local_file(config_dir)
def create_symlinks(self):
from calibre.utils.osx_symlinks import create_symlinks
@ -805,7 +807,7 @@ class ConfigDialog(ResizableDialog, Ui_Dialog):
self.stop.setEnabled(False)
def test_server(self):
QDesktopServices.openUrl(QUrl('http://127.0.0.1:'+str(self.port.value())))
open_url(QUrl('http://127.0.0.1:'+str(self.port.value())))
def compact(self, toggled):
d = CheckIntegrity(self.db, self)
@ -870,6 +872,7 @@ class ConfigDialog(ResizableDialog, Ui_Dialog):
config['overwrite_author_title_metadata'] = self.opt_overwrite_author_title_metadata.isChecked()
config['enforce_cpu_limit'] = bool(self.opt_enforce_cpu_limit.isChecked())
config['disable_animations'] = bool(self.opt_disable_animations.isChecked())
config['show_donate_button'] = bool(self.opt_show_donate_button.isChecked())
gprefs['show_splash_screen'] = bool(self.show_splash_screen.isChecked())
fmts = []
for i in range(self.viewer.count()):

View File

@ -356,7 +356,7 @@
</property>
</widget>
</item>
<item row="3" column="0" colspan="2">
<item row="3" column="0">
<widget class="QCheckBox" name="show_splash_screen">
<property name="text">
<string>Show &amp;splash screen at startup</string>
@ -665,6 +665,13 @@
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QCheckBox" name="opt_show_donate_button">
<property name="text">
<string>Show &amp;donate button (restart)</string>
</property>
</widget>
</item>
</layout>
</widget>
<widget class="QWidget" name="page_6">

View File

@ -25,6 +25,12 @@ class tableItem(QTableWidgetItem):
def __lt__(self, other):
return self.sort < other.sort
class centeredTableItem(tableItem):
def __init__(self, text):
tableItem.__init__(self, text)
self.setTextAlignment(Qt.AlignCenter)
class titleTableItem(tableItem):
def __init__(self, text):
@ -64,10 +70,10 @@ class DeleteMatchingFromDeviceDialog(QDialog, Ui_DeleteMatchingFromDeviceDialog)
self.buttonBox.accepted.connect(self.accepted)
self.table.cellClicked.connect(self.cell_clicked)
self.table.setSelectionMode(QAbstractItemView.NoSelection)
self.table.setColumnCount(5)
self.table.setColumnCount(7)
self.table.setHorizontalHeaderLabels(
['', _('Location'), _('Title'),
_('Author'), _('Date'), _('Format')])
['', _('Location'), _('Title'), _('Author'),
_('Date'), _('Format'), _('Path')])
rows = 0
for card in items:
rows += len(items[card][1])
@ -85,7 +91,8 @@ class DeleteMatchingFromDeviceDialog(QDialog, Ui_DeleteMatchingFromDeviceDialog)
self.table.setItem(row, 2, titleTableItem(book.title))
self.table.setItem(row, 3, authorTableItem(book))
self.table.setItem(row, 4, dateTableItem(book.datetime))
self.table.setItem(row, 5, tableItem(book.path.rpartition('.')[2]))
self.table.setItem(row, 5, centeredTableItem(book.path.rpartition('.')[2]))
self.table.setItem(row, 6, tableItem(book.path))
row += 1
self.table.setCurrentCell(0, 1)
self.table.resizeColumnsToContents()

View File

@ -103,7 +103,7 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
if _file:
_file = os.path.abspath(_file)
if not os.access(_file, os.R_OK):
d = error_dialog(self.window, _('Cannot read'),
d = error_dialog(self, _('Cannot read'),
_('You do not have permission to read the file: ') + _file)
d.exec_()
return
@ -112,14 +112,14 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
cf = open(_file, "rb")
cover = cf.read()
except IOError, e:
d = error_dialog(self.window, _('Error reading file'),
d = error_dialog(self, _('Error reading file'),
_("<p>There was an error reading from file: <br /><b>") + _file + "</b></p><br />"+str(e))
d.exec_()
if cover:
pix = QPixmap()
pix.loadFromData(cover)
if pix.isNull():
d = error_dialog(self.window,
d = error_dialog(self,
_("Not a valid picture"),
_file + _(" is not a valid picture"))
d.exec_()
@ -162,7 +162,7 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
self.formats_changed = True
added = True
if bad_perms:
error_dialog(self.window, _('No permission'),
error_dialog(self, _('No permission'),
_('You do not have '
'permission to read the following files:'),
det_msg='\n'.join(bad_perms), show=True)

View File

@ -3,13 +3,13 @@ __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
import time, os
from PyQt4.Qt import SIGNAL, QUrl, QDesktopServices, QAbstractListModel, Qt, \
from PyQt4.Qt import SIGNAL, QUrl, QAbstractListModel, Qt, \
QVariant, QInputDialog
from calibre.web.feeds.recipes import compile_recipe
from calibre.web.feeds.news import AutomaticNewsRecipe
from calibre.gui2.dialogs.user_profiles_ui import Ui_Dialog
from calibre.gui2 import error_dialog, question_dialog, \
from calibre.gui2 import error_dialog, question_dialog, open_url, \
choose_files, ResizableDialog, NONE
from calibre.gui2.widgets import PythonHighlighter
from calibre.ptempfile import PersistentTemporaryFile
@ -135,7 +135,7 @@ class UserProfiles(ResizableDialog, Ui_Dialog):
url.addQueryItem('subject', subject)
url.addQueryItem('body', body)
url.addQueryItem('attachment', pt.name)
QDesktopServices.openUrl(url)
open_url(url)
def current_changed(self, current, previous):

View File

@ -5,22 +5,22 @@ __license__ = 'GPL v3'
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
import functools
import functools, sys, os
from PyQt4.Qt import QMenu, Qt, pyqtSignal, QToolButton, QIcon, QStackedWidget, \
QSize, QSizePolicy, QStatusBar
QSize, QSizePolicy, QStatusBar, QUrl, QLabel, QFont
from calibre.utils.config import prefs
from calibre.ebooks import BOOK_EXTENSIONS
from calibre.constants import isosx, __appname__, preferred_encoding
from calibre.gui2 import config, is_widescreen
from calibre.constants import isosx, __appname__, preferred_encoding, \
__version__
from calibre.gui2 import config, is_widescreen, open_url
from calibre.gui2.library.views import BooksView, DeviceBooksView
from calibre.gui2.widgets import Splitter
from calibre.gui2.tag_view import TagBrowserWidget
from calibre.gui2.book_details import BookDetails
from calibre.gui2.notify import get_notifier
_keep_refs = []
def partial(*args, **kwargs):
@ -48,6 +48,7 @@ class SaveMenu(QMenu): # {{{
class ToolbarMixin(object): # {{{
def __init__(self):
self.action_help.triggered.connect(self.show_help)
md = QMenu()
md.addAction(_('Edit metadata individually'),
partial(self.edit_metadata, False, bulk=False))
@ -182,9 +183,13 @@ class ToolbarMixin(object): # {{{
for ch in self.tool_bar.children():
if isinstance(ch, QToolButton):
ch.setCursor(Qt.PointingHandCursor)
ch.setStatusTip(ch.toolTip())
self.tool_bar.contextMenuEvent = self.no_op
def show_help(self, *args):
open_url(QUrl('http://calibre-ebook.com/user_manual'))
def read_toolbar_settings(self):
self.tool_bar.setIconSize(config['toolbar_icon_size'])
self.tool_bar.setToolButtonStyle(
@ -362,12 +367,50 @@ class Stack(QStackedWidget): # {{{
class StatusBar(QStatusBar): # {{{
def __init__(self, parent=None):
QStatusBar.__init__(self, parent)
self.default_message = __appname__ + ' ' + _('version') + ' ' + \
self.get_version() + ' ' + _('created by Kovid Goyal')
self.device_string = ''
self.update_label = QLabel('')
self.update_label.setOpenExternalLinks(True)
self.addPermanentWidget(self.update_label)
self.update_label.setVisible(False)
self._font = QFont()
self._font.setBold(True)
self.setFont(self._font)
def initialize(self, systray=None):
self.systray = systray
self.notifier = get_notifier(systray)
self.messageChanged.connect(self.message_changed,
type=Qt.QueuedConnection)
self.message_changed('')
def device_connected(self, devname):
self.device_string = _('Connected ') + devname
self.clearMessage()
def device_disconnected(self):
self.device_string = ''
self.clearMessage()
def new_version_available(self, ver, url):
msg = (u'<span style="color:red; font-weight: bold">%s: <a href="%s">%s<a></span>') % (
_('Update found'), url, ver)
self.update_label.setText(msg)
self.update_label.setCursor(Qt.PointingHandCursor)
self.update_label.setVisible(True)
def get_version(self):
dv = os.environ.get('CALIBRE_DEVELOP_FROM', None)
v = __version__
if getattr(sys, 'frozen', False) and dv and os.path.abspath(dv) in sys.path:
v += '*'
return v
def show_message(self, msg, timeout=0):
QStatusBar.showMessage(self, msg, timeout)
self.showMessage(msg, timeout)
if self.notifier is not None and not config['disable_tray_notification']:
if isosx and isinstance(msg, unicode):
try:
@ -377,7 +420,15 @@ class StatusBar(QStatusBar): # {{{
self.notifier(msg)
def clear_message(self):
QStatusBar.clearMessage(self)
self.clearMessage()
def message_changed(self, msg):
if not msg or msg.isEmpty() or msg.isNull():
extra = ''
if self.device_string:
extra = ' ..::.. ' + self.device_string
self.showMessage(self.default_message + extra)
# }}}

View File

@ -20,7 +20,8 @@ from calibre.utils.config import tweaks, prefs
from calibre.utils.date import dt_factory, qt_to_dt, isoformat
from calibre.ebooks.metadata.meta import set_metadata as _set_metadata
from calibre.utils.search_query_parser import SearchQueryParser
from calibre.library.caches import _match, CONTAINS_MATCH, EQUALS_MATCH, REGEXP_MATCH
from calibre.library.caches import _match, CONTAINS_MATCH, EQUALS_MATCH, \
REGEXP_MATCH, CoverCache
from calibre.library.cli import parse_series_string
from calibre import strftime, isbytestring, prepare_string_for_xml
from calibre.constants import filesystem_encoding
@ -149,21 +150,22 @@ class BooksModel(QAbstractTableModel): # {{{
self.build_data_convertors()
self.reset()
self.database_changed.emit(db)
if self.cover_cache is not None:
self.cover_cache.stop()
self.cover_cache = CoverCache(db)
self.cover_cache.start()
def refresh_cover(event, ids):
if event == 'cover' and self.cover_cache is not None:
self.cover_cache.refresh(ids)
db.add_listener(refresh_cover)
def refresh_ids(self, ids, current_row=-1):
rows = self.db.refresh_ids(ids)
if rows:
self.refresh_rows(rows, current_row=current_row)
def refresh_cover_cache(self, ids):
if self.cover_cache:
self.cover_cache.refresh(ids)
def refresh_rows(self, rows, current_row=-1):
for row in rows:
if self.cover_cache:
id = self.db.id(row)
self.cover_cache.refresh([id])
if row == current_row:
self.new_bookdisplay_data.emit(
self.get_book_display_info(row))
@ -326,7 +328,7 @@ class BooksModel(QAbstractTableModel): # {{{
def set_cache(self, idx):
l, r = 0, self.count()-1
if self.cover_cache:
if self.cover_cache is not None:
l = max(l, idx-self.buffer_size)
r = min(r, idx+self.buffer_size)
k = min(r-idx, idx-l)
@ -490,15 +492,18 @@ class BooksModel(QAbstractTableModel): # {{{
def title(self, row_number):
return self.db.title(row_number)
def rating(self, row_number):
ans = self.db.rating(row_number)
ans = ans/2 if ans else 0
return int(ans)
def cover(self, row_number):
data = None
try:
id = self.db.id(row_number)
if self.cover_cache:
if self.cover_cache is not None:
img = self.cover_cache.cover(id)
if img:
if img.isNull():
img = self.default_image
if not img.isNull():
return img
if not data:
data = self.db.cover(row_number)
@ -937,6 +942,7 @@ class DeviceBooksModel(BooksModel): # {{{
cname = self.column_map[index.column()]
if cname in ('title', 'authors') or \
(cname == 'collections' and \
callable(getattr(self.db, 'supports_collections', None)) and \
self.db.supports_collections() and \
prefs['preserve_user_collections']):
flags |= Qt.ItemIsEditable

View File

@ -501,8 +501,9 @@ class DeviceBooksView(BooksView): # {{{
def contextMenuEvent(self, event):
self.edit_collections_menu.setVisible(
self._model.db.supports_collections() and \
prefs['preserve_user_collections'])
callable(getattr(self._model.db, 'supports_collections', None)) and \
self._model.db.supports_collections() and \
prefs['preserve_user_collections'])
self.context_menu.popup(event.globalPos())
event.accept()

View File

@ -96,10 +96,7 @@
</widget>
</item>
<item>
<widget class="QToolButton" name="donate_button">
<property name="cursor">
<cursorShape>PointingHandCursor</cursorShape>
</property>
<widget class="ThrobbingButton" name="donate_button">
<property name="text">
<string>...</string>
</property>
@ -107,45 +104,13 @@
<iconset resource="../../../resources/images.qrc">
<normaloff>:/images/donate.svg</normaloff>:/images/donate.svg</iconset>
</property>
<property name="iconSize">
<size>
<width>64</width>
<height>64</height>
</size>
</property>
<property name="autoRaise">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<widget class="QLabel" name="vanity">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="maximumSize">
<size>
<width>16777215</width>
<height>90</height>
</size>
</property>
<property name="text">
<string/>
</property>
<property name="textFormat">
<enum>Qt::RichText</enum>
</property>
<property name="openExternalLinks">
<bool>true</bool>
</property>
</widget>
</item>
</layout>
<layout class="QVBoxLayout" name="verticalLayout_3"/>
</item>
</layout>
</item>
@ -259,7 +224,7 @@
</size>
</property>
<property name="toolTip">
<string>Choose saved search or enter name for new saved search</string>
<string/>
</property>
<property name="minimumContentsLength">
<number>15</number>
@ -353,6 +318,8 @@
<addaction name="action_save"/>
<addaction name="action_del"/>
<addaction name="separator"/>
<addaction name="action_help"/>
<addaction name="separator"/>
<addaction name="action_preferences"/>
</widget>
<action name="action_add">
@ -544,6 +511,21 @@
<string>Ctrl+P</string>
</property>
</action>
<action name="action_help">
<property name="icon">
<iconset resource="../../../resources/images.qrc">
<normaloff>:/images/help.svg</normaloff>:/images/help.svg</iconset>
</property>
<property name="text">
<string>Help</string>
</property>
<property name="toolTip">
<string>Browse the calibre User Manual</string>
</property>
<property name="shortcut">
<string>F1</string>
</property>
</action>
</widget>
<customwidgets>
<customwidget>
@ -561,6 +543,11 @@
<extends>QComboBox</extends>
<header>calibre.gui2.search_box</header>
</customwidget>
<customwidget>
<class>ThrobbingButton</class>
<extends>QToolButton</extends>
<header>calibre/gui2/throbber.h</header>
</customwidget>
</customwidgets>
<resources>
<include location="../../../resources/images.qrc"/>

View File

@ -84,6 +84,7 @@ typedef unsigned short QRgb565;
#define REFLECTION_FACTOR 1.5
#define MAX(x, y) ((x > y) ? x : y)
#define MIN(x, y) ((x < y) ? x : y)
#define RGB565_RED_MASK 0xF800
#define RGB565_GREEN_MASK 0x07E0
@ -578,12 +579,10 @@ void PictureFlowPrivate::resetSlides()
static QImage prepareSurface(QImage img, int w, int h)
{
Qt::TransformationMode mode = Qt::SmoothTransformation;
img = img.scaled(w, h, Qt::IgnoreAspectRatio, mode);
img = img.scaled(w, h, Qt::IgnoreAspectRatio, Qt::SmoothTransformation);
// slightly larger, to accommodate for the reflection
int hs = int(h * REFLECTION_FACTOR);
int hofs = 0;
// offscreen buffer: black is sweet
QImage result(hs, w, QImage::Format_RGB16);
@ -594,21 +593,20 @@ static QImage prepareSurface(QImage img, int w, int h)
// (and much better and faster to work row-wise, i.e in one scanline)
for(int x = 0; x < w; x++)
for(int y = 0; y < h; y++)
result.setPixel(hofs + y, x, img.pixel(x, y));
result.setPixel(y, x, img.pixel(x, y));
// create the reflection
int ht = hs - h - hofs;
int hte = ht;
int ht = hs - h;
for(int x = 0; x < w; x++)
for(int y = 0; y < ht; y++)
{
QRgb color = img.pixel(x, img.height()-y-1);
//QRgb565 color = img.scanLine(img.height()-y-1) + x*sizeof(QRgb565); //img.pixel(x, img.height()-y-1);
int a = qAlpha(color);
int r = qRed(color) * a / 256 * (hte - y) / hte * 3/5;
int g = qGreen(color) * a / 256 * (hte - y) / hte * 3/5;
int b = qBlue(color) * a / 256 * (hte - y) / hte * 3/5;
result.setPixel(h+hofs+y, x, qRgb(r, g, b));
int r = qRed(color) * a / 256 * (ht - y) / ht * 3/5;
int g = qGreen(color) * a / 256 * (ht - y) / ht * 3/5;
int b = qBlue(color) * a / 256 * (ht - y) / ht * 3/5;
result.setPixel(h+y, x, qRgb(r, g, b));
}
return result;
@ -708,9 +706,12 @@ void PictureFlowPrivate::render()
painter.setPen(Qt::white);
//painter.setPen(QColor(255,255,255,127));
if (centerIndex < slideCount() && centerIndex > -1)
painter.drawText( QRect(0,0, buffer.width(), buffer.height()*2-fontSize*3),
if (centerIndex < slideCount() && centerIndex > -1) {
painter.drawText( QRect(0,0, buffer.width(), buffer.height()*2-fontSize*4),
Qt::AlignCenter, slideImages->caption(centerIndex));
painter.drawText( QRect(0,0, buffer.width(), buffer.height()*2-fontSize*2),
Qt::AlignCenter, slideImages->subtitle(centerIndex));
}
painter.end();
@ -761,15 +762,22 @@ void PictureFlowPrivate::render()
int sc = slideCount();
painter.setPen(QColor(255,255,255, (255-fade) ));
if (leftTextIndex < sc && leftTextIndex > -1)
painter.drawText( QRect(0,0, buffer.width(), buffer.height()*2 - fontSize*3),
if (leftTextIndex < sc && leftTextIndex > -1) {
painter.drawText( QRect(0,0, buffer.width(), buffer.height()*2 - fontSize*4),
Qt::AlignCenter, slideImages->caption(leftTextIndex));
painter.drawText( QRect(0,0, buffer.width(), buffer.height()*2 - fontSize*2),
Qt::AlignCenter, slideImages->subtitle(leftTextIndex));
}
painter.setPen(QColor(255,255,255, fade));
if (leftTextIndex+1 < sc && leftTextIndex > -2)
painter.drawText( QRect(0,0, buffer.width(), buffer.height()*2 - fontSize*3),
if (leftTextIndex+1 < sc && leftTextIndex > -2) {
painter.drawText( QRect(0,0, buffer.width(), buffer.height()*2 - fontSize*4),
Qt::AlignCenter, slideImages->caption(leftTextIndex+1));
painter.drawText( QRect(0,0, buffer.width(), buffer.height()*2 - fontSize*2),
Qt::AlignCenter, slideImages->subtitle(leftTextIndex+1));
}
painter.end();
}
@ -797,12 +805,20 @@ QRect PictureFlowPrivate::renderCenterSlide(const SlideInfo &slide) {
int sw = src->height();
int sh = src->width();
int h = buffer.height();
QRect rect(buffer.width()/2 - sw/2, 0, sw, h-1);
int left = rect.left();
int srcoff = 0;
int left = buffer.width()/2 - sw/2;
if (left < 0) {
srcoff = -left;
sw += left;
left = 0;
}
QRect rect(left, 0, sw, h-1);
int xcon = MIN(h-1, sh-1);
int ycon = MIN(sw, buffer.width() - left);
for(int x = 0; x < sh-1; x++)
for(int y = 0; y < sw; y++)
buffer.setPixel(left + y, 1+x, src->pixel(x, y));
for(int x = 0; x < xcon; x++)
for(int y = 0; y < ycon; y++)
buffer.setPixel(left + y, 1+x, src->pixel(x, srcoff+y));
return rect;
}
@ -1366,5 +1382,6 @@ void PictureFlow::emitcurrentChanged(int index) { emit currentChanged(index); }
int FlowImages::count() { return 0; }
QImage FlowImages::image(int index) { index=0; return QImage(); }
QString FlowImages::caption(int index) {index=0; return QString(); }
QString FlowImages::subtitle(int index) {index=0; return QString(); }
// }}}

View File

@ -67,6 +67,7 @@ public:
virtual int count();
virtual QImage image(int index);
virtual QString caption(int index);
virtual QString subtitle(int index);
signals:
void dataChanged();

View File

@ -16,6 +16,7 @@ public:
virtual int count();
virtual QImage image(int index);
virtual QString caption(int index);
virtual QString subtitle(int index);
signals:
void dataChanged();

View File

@ -6,6 +6,8 @@ __license__ = 'GPL v3'
__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
import re
from PyQt4.Qt import QComboBox, Qt, QLineEdit, QStringList, pyqtSlot, \
pyqtSignal, SIGNAL, QObject, QDialog, QCompleter, \
QAction, QKeySequence
@ -368,6 +370,10 @@ class SearchBoxMixin(object):
self.action_focus_search.triggered.connect(lambda x:
self.search.setFocus(Qt.OtherFocusReason))
self.addAction(self.action_focus_search)
self.search.setStatusTip(re.sub(r'<\w+>', ' ',
unicode(self.search.toolTip())))
self.advanced_search_button.setStatusTip(self.advanced_search_button.toolTip())
self.clear_button.setStatusTip(self.clear_button.toolTip())
def search_box_cleared(self):
self.tags_view.clear()
@ -396,6 +402,12 @@ class SavedSearchBoxMixin(object):
self.saved_search.delete_search_button_clicked)
self.connect(self.copy_search_button, SIGNAL('clicked()'),
self.saved_search.copy_search_button_clicked)
self.saved_search.setToolTip(
_('Choose saved search or enter name for new saved search'))
self.saved_search.setStatusTip(self.saved_search.toolTip())
for x in ('copy', 'save', 'delete'):
b = getattr(self, x+'_search_button')
b.setStatusTip(b.toolTip())
def saved_searches_changed(self):

View File

@ -11,6 +11,7 @@ class SearchRestrictionMixin(object):
self.library_view.model().count_changed_signal.connect(self.restriction_count_changed)
self.search_restriction.setSizeAdjustPolicy(self.search_restriction.AdjustToMinimumContentsLengthWithIcon)
self.search_restriction.setMinimumContentsLength(10)
self.search_restriction.setStatusTip(self.search_restriction.toolTip())
'''
Adding and deleting books while restricted creates a complexity. When added,

View File

@ -768,6 +768,9 @@ class TagBrowserWidget(QWidget): # {{{
for x in (_('Sort by name'), _('Sort by popularity'),
_('Sort by average rating')):
parent.sort_by.addItem(x)
parent.sort_by.setToolTip(
_('Set the sort order for entries in the Tag Browser'))
parent.sort_by.setStatusTip(parent.sort_by.toolTip())
parent.sort_by.setCurrentIndex(0)
self._layout.addWidget(parent.sort_by)
@ -776,9 +779,16 @@ class TagBrowserWidget(QWidget): # {{{
parent.tag_match.addItem(x)
parent.tag_match.setCurrentIndex(0)
self._layout.addWidget(parent.tag_match)
parent.tag_match.setToolTip(
_('When selecting multiple entries in the Tag Browser '
'match any or all of them'))
parent.tag_match.setStatusTip(parent.tag_match.toolTip())
parent.edit_categories = QPushButton(_('Manage &user categories'), parent)
self._layout.addWidget(parent.edit_categories)
parent.edit_categories.setToolTip(
_('Add your own categories to the Tag Browser'))
parent.edit_categories.setStatusTip(parent.edit_categories.toolTip())
# }}}

View File

@ -0,0 +1,70 @@
#!/usr/bin/env python
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
__license__ = 'GPL v3'
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
from PyQt4.Qt import QToolButton, QSize, QPropertyAnimation, Qt, \
QMetaObject
from calibre.gui2 import config
class ThrobbingButton(QToolButton):
def __init__(self, *args):
QToolButton.__init__(self, *args)
self.animation = QPropertyAnimation(self, 'iconSize', self)
self.animation.setDuration(60/72.*1000)
self.animation.setLoopCount(4)
self.normal_icon_size = QSize(64, 64)
self.animation.valueChanged.connect(self.value_changed)
self.setCursor(Qt.PointingHandCursor)
self.animation.finished.connect(self.animation_finished)
def set_normal_icon_size(self, w, h):
self.normal_icon_size = QSize(w, h)
self.setIconSize(self.normal_icon_size)
self.setMinimumSize(self.sizeHint())
def animation_finished(self):
self.setIconSize(self.normal_icon_size)
def enterEvent(self, ev):
self.start_animation()
def leaveEvent(self, ev):
self.stop_animation()
def value_changed(self, val):
self.update()
def start_animation(self):
if config['disable_animations']: return
if self.animation.state() != self.animation.Stopped or not self.isVisible():
return
size = self.normal_icon_size.width()
smaller = int(0.7 * size)
self.animation.setStartValue(QSize(smaller, smaller))
self.animation.setEndValue(self.normal_icon_size)
QMetaObject.invokeMethod(self.animation, 'start', Qt.QueuedConnection)
def stop_animation(self):
self.animation.stop()
self.animation_finished()
if __name__ == '__main__':
from PyQt4.Qt import QApplication, QWidget, QHBoxLayout, QIcon
app = QApplication([])
w = QWidget()
w.setLayout(QHBoxLayout())
b = ThrobbingButton()
b.setIcon(QIcon(I('donate.svg')))
w.layout().addWidget(b)
w.show()
b.set_normal_icon_size(64, 64)
b.start_animation()
app.exec_()

View File

@ -12,18 +12,18 @@ __docformat__ = 'restructuredtext en'
import collections, os, sys, textwrap, time
from Queue import Queue, Empty
from threading import Thread
from PyQt4.Qt import Qt, SIGNAL, QObject, QUrl, QTimer, \
from PyQt4.Qt import Qt, SIGNAL, QObject, QTimer, \
QPixmap, QMenu, QIcon, pyqtSignal, \
QDialog, QDesktopServices, \
QDialog, \
QSystemTrayIcon, QApplication, QKeySequence, QAction, \
QMessageBox, QHelpEvent
from calibre import prints, patheq
from calibre.constants import __version__, __appname__, isosx
from calibre.constants import __appname__, isosx
from calibre.ptempfile import PersistentTemporaryFile
from calibre.utils.config import prefs, dynamic
from calibre.utils.ipc.server import Server
from calibre.gui2 import error_dialog, GetMetadata, \
from calibre.gui2 import error_dialog, GetMetadata, open_local_file, \
gprefs, max_available_height, config, info_dialog
from calibre.gui2.cover_flow import CoverFlowMixin
from calibre.gui2.widgets import ProgressIndicator
@ -38,7 +38,6 @@ from calibre.gui2.dialogs.config import ConfigDialog
from calibre.gui2.dialogs.book_info import BookInfo
from calibre.library.database2 import LibraryDatabase2
from calibre.library.caches import CoverCache
from calibre.gui2.init import ToolbarMixin, LibraryViewMixin, LayoutMixin
from calibre.gui2.search_box import SearchBoxMixin, SavedSearchBoxMixin
from calibre.gui2.search_restriction_mixin import SearchRestrictionMixin
@ -138,6 +137,7 @@ class Main(MainWindow, Ui_MainWindow, DeviceMixin, ToolbarMixin, # {{{
self.restriction_in_effect = False
self.progress_indicator = ProgressIndicator(self)
self.progress_indicator.pos = (0, 20)
self.verbose = opts.verbose
self.get_metadata = GetMetadata()
self.upload_memory = {}
@ -163,6 +163,7 @@ class Main(MainWindow, Ui_MainWindow, DeviceMixin, ToolbarMixin, # {{{
self.donate_action = self.system_tray_menu.addAction(
QIcon(I('donate.svg')), _('&Donate to support calibre'))
self.donate_button.setDefaultAction(self.donate_action)
self.donate_button.setStatusTip(self.donate_button.toolTip())
self.eject_action = self.system_tray_menu.addAction(
QIcon(I('eject.svg')), _('&Eject connected device'))
self.eject_action.setEnabled(False)
@ -202,18 +203,7 @@ class Main(MainWindow, Ui_MainWindow, DeviceMixin, ToolbarMixin, # {{{
self.device_manager.umount_device)
self.eject_action.triggered.connect(self.device_manager.umount_device)
####################### Vanity ########################
self.vanity_template = _('<p>For help see the: <a href="%s">User Manual</a>'
'<br>')%'http://calibre-ebook.com/user_manual'
dv = os.environ.get('CALIBRE_DEVELOP_FROM', None)
v = __version__
if getattr(sys, 'frozen', False) and dv and os.path.abspath(dv) in sys.path:
v += '*'
self.vanity_template += _('<b>%s</b>: %s by <b>Kovid Goyal '
'%%(version)s</b><br>%%(device)s</p>')%(__appname__, v)
self.latest_version = ' '
self.vanity.setText(self.vanity_template%dict(version=' ', device=' '))
self.device_info = ' '
#################### Update notification ###################
UpdateMixin.__init__(self, opts)
####################### Setup Toolbar #####################
@ -230,9 +220,6 @@ class Main(MainWindow, Ui_MainWindow, DeviceMixin, ToolbarMixin, # {{{
if self.system_tray_icon.isVisible() and opts.start_in_tray:
self.hide_windows()
self.cover_cache = CoverCache(self.library_path)
self.cover_cache.start()
self.library_view.model().cover_cache = self.cover_cache
self.library_view.model().count_changed_signal.connect \
(self.location_view.count_changed)
if not gprefs.get('quick_start_guide_added', False):
@ -294,6 +281,8 @@ class Main(MainWindow, Ui_MainWindow, DeviceMixin, ToolbarMixin, # {{{
self.read_settings()
self.finalize_layout()
self.donate_button.set_normal_icon_size(64, 64)
self.donate_button.start_animation()
def resizeEvent(self, ev):
MainWindow.resizeEvent(self, ev)
@ -557,7 +546,8 @@ class Main(MainWindow, Ui_MainWindow, DeviceMixin, ToolbarMixin, # {{{
'''
MSG = _('is the result of the efforts of many volunteers from all '
'over the world. If you find it useful, please consider '
'donating to support its development.')
'donating to support its development. Your donation helps '
'keep calibre development going.')
HTML = u'''
<html>
<head>
@ -575,7 +565,7 @@ class Main(MainWindow, Ui_MainWindow, DeviceMixin, ToolbarMixin, # {{{
pt = PersistentTemporaryFile('_donate.htm')
pt.write(HTML.encode('utf-8'))
pt.close()
QDesktopServices.openUrl(QUrl.fromLocalFile(pt.name))
open_local_file(pt.name)
def confirm_quit(self):
@ -606,9 +596,10 @@ class Main(MainWindow, Ui_MainWindow, DeviceMixin, ToolbarMixin, # {{{
while self.spare_servers:
self.spare_servers.pop().close()
self.device_manager.keep_going = False
self.cover_cache.stop()
cc = self.library_view.model().cover_cache
if cc is not None:
cc.stop()
self.hide_windows()
self.cover_cache.terminate()
self.emailer.stop()
try:
try:

View File

@ -3,13 +3,13 @@ __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
import traceback
from PyQt4.Qt import QThread, pyqtSignal, QDesktopServices, QUrl, Qt
from PyQt4.Qt import QThread, pyqtSignal, Qt, QUrl
import mechanize
from calibre.constants import __appname__, __version__, iswindows, isosx
from calibre import browser
from calibre.utils.config import prefs
from calibre.gui2 import config, dynamic, question_dialog
from calibre.gui2 import config, dynamic, question_dialog, open_url
URL = 'http://status.calibre-ebook.com/latest'
@ -49,12 +49,8 @@ class UpdateMixin(object):
def update_found(self, version):
os = 'windows' if iswindows else 'osx' if isosx else 'linux'
url = 'http://calibre-ebook.com/download_%s'%os
self.latest_version = '<br>' + _('<span style="color:red; font-weight:bold">'
'Latest version: <a href="%s">%s</a></span>')%(url, version)
self.vanity.setText(self.vanity_template%\
(dict(version=self.latest_version,
device=self.device_info)))
self.vanity.update()
self.status_bar.new_version_available(version, url)
if config.get('new_version_notification') and \
dynamic.get('update to version %s'%version, True):
if question_dialog(self, _('Update available'),
@ -64,7 +60,7 @@ class UpdateMixin(object):
'ge?')%(__appname__, version)):
url = 'http://calibre-ebook.com/download_'+\
('windows' if iswindows else 'osx' if isosx else 'linux')
QDesktopServices.openUrl(QUrl(url))
open_url(QUrl(url))
dynamic.set('update to version %s'%version, False)

View File

@ -6,7 +6,7 @@ from functools import partial
from threading import Thread
from PyQt4.Qt import QApplication, Qt, QIcon, QTimer, SIGNAL, QByteArray, \
QDesktopServices, QDoubleSpinBox, QLabel, QTextBrowser, \
QDoubleSpinBox, QLabel, QTextBrowser, \
QPainter, QBrush, QColor, QStandardItemModel, QPalette, \
QStandardItem, QUrl, QRegExpValidator, QRegExp, QLineEdit, \
QToolButton, QMenu, QInputDialog, QAction, QKeySequence
@ -17,7 +17,7 @@ from calibre.gui2.viewer.bookmarkmanager import BookmarkManager
from calibre.gui2.widgets import ProgressIndicator
from calibre.gui2.main_window import MainWindow
from calibre.gui2 import Application, ORG_NAME, APP_UID, choose_files, \
info_dialog, error_dialog
info_dialog, error_dialog, open_url
from calibre.ebooks.oeb.iterator import EbookIterator
from calibre.ebooks import DRMError
from calibre.constants import islinux, isfreebsd
@ -472,7 +472,7 @@ class EbookViewer(MainWindow, Ui_EbookViewer):
elif frag:
self.view.scroll_to(frag)
else:
QDesktopServices.openUrl(url)
open_url(url)
def load_started(self):
self.open_progress_indicator(_('Loading flow...'))

View File

@ -243,7 +243,7 @@
<action name="action_copy">
<property name="icon">
<iconset resource="../../../../resources/images.qrc">
<normaloff>:/images/convert.svg</normaloff>:/images/convert.svg</iconset>
<normaloff>:/images/edit_copy.svg</normaloff>:/images/edit_copy.svg</iconset>
</property>
<property name="text">
<string>Copy to clipboard</string>

View File

@ -38,12 +38,16 @@ class ProgressIndicator(QWidget):
self.status.setWordWrap(True)
self.status.setAlignment(Qt.AlignHCenter|Qt.AlignTop)
self.setVisible(False)
self.pos = None
def start(self, msg=''):
view = self.parent()
pwidth, pheight = view.size().width(), view.size().height()
self.resize(pwidth, min(pheight, 250))
self.move(0, (pheight-self.size().height())/2.)
if self.pos is None:
self.move(0, (pheight-self.size().height())/2.)
else:
self.move(self.pos[0], self.pos[1])
self.pi.resize(self.pi.sizeHint())
self.pi.move(int((self.size().width()-self.pi.size().width())/2.), 0)
self.status.resize(self.size().width(), self.size().height()-self.pi.size().height()-10)
@ -263,10 +267,10 @@ class LocationModel(QAbstractListModel):
QVariant(QIcon(I('reader.svg'))),
QVariant(QIcon(I('sd.svg'))),
QVariant(QIcon(I('sd.svg')))]
self.text = [_('Library\n%d\nbooks'),
_('Reader\n%s\navailable'),
_('Card A\n%s\navailable'),
_('Card B\n%s\navailable')]
self.text = [_('Library\n%d books'),
_('Reader\n%s'),
_('Card A\n%s'),
_('Card B\n%s')]
self.free = [-1, -1, -1]
self.count = 0
self.highlight_row = 0
@ -294,6 +298,14 @@ class LocationModel(QAbstractListModel):
row = 3
return row
def get_tooltip(self, row, drow):
ans = self.tooltips[row]
if row > 0:
fs = self.free[drow-1]
if fs > -1:
ans += '\n\n%s '%(human_readable(fs)) + _('free')
return ans
def data(self, index, role):
row = index.row()
drow = self.get_device_row(row)
@ -304,8 +316,9 @@ class LocationModel(QAbstractListModel):
data = QVariant(text)
elif role == Qt.DecorationRole:
data = self.icons[drow]
elif role == Qt.ToolTipRole:
data = QVariant(self.tooltips[drow])
elif role in (Qt.ToolTipRole, Qt.StatusTipRole):
ans = self.get_tooltip(row, drow)
data = QVariant(ans)
elif role == Qt.SizeHintRole:
data = QVariant(QSize(155, 90))
elif role == Qt.FontRole:
@ -1002,12 +1015,14 @@ class LayoutButton(QToolButton):
label =_('Show')
self.setText(label + ' ' + self.label)
self.setToolTip(self.text())
self.setStatusTip(self.text())
def set_state_to_hide(self, *args):
self.setChecked(True)
label = _('Hide')
self.setText(label + ' ' + self.label)
self.setToolTip(self.text())
self.setStatusTip(self.text())
def update_state(self, *args):
if self.splitter.is_side_index_hidden:

View File

@ -6,11 +6,13 @@ __license__ = 'GPL v3'
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
import collections, glob, os, re, itertools, functools
import re, itertools, functools
from itertools import repeat
from datetime import timedelta
from threading import Thread, RLock
from Queue import Queue, Empty
from PyQt4.Qt import QThread, QReadWriteLock, QImage, Qt
from PyQt4.Qt import QImage, Qt
from calibre.utils.config import tweaks
from calibre.utils.date import parse_date, now, UNDEFINED_DATE
@ -19,120 +21,73 @@ from calibre.utils.pyparsing import ParseException
from calibre.ebooks.metadata import title_sort
from calibre import fit_image
class CoverCache(QThread):
class CoverCache(Thread):
def __init__(self, library_path, parent=None):
QThread.__init__(self, parent)
self.library_path = library_path
self.id_map = None
self.id_map_lock = QReadWriteLock()
self.load_queue = collections.deque()
self.load_queue_lock = QReadWriteLock(QReadWriteLock.Recursive)
self.cache = {}
self.cache_lock = QReadWriteLock()
self.id_map_stale = True
def __init__(self, db):
Thread.__init__(self)
self.daemon = True
self.db = db
self.load_queue = Queue()
self.keep_running = True
def build_id_map(self):
self.id_map_lock.lockForWrite()
self.id_map = {}
for f in glob.glob(os.path.join(self.library_path, '*', '* (*)', 'cover.jpg')):
c = os.path.basename(os.path.dirname(f))
try:
id = int(re.search(r'\((\d+)\)', c[c.rindex('('):]).group(1))
self.id_map[id] = f
except:
continue
self.id_map_lock.unlock()
self.id_map_stale = False
def set_cache(self, ids):
self.cache_lock.lockForWrite()
already_loaded = set([])
for id in self.cache.keys():
if id in ids:
already_loaded.add(id)
else:
self.cache.pop(id)
self.cache_lock.unlock()
ids = [i for i in ids if i not in already_loaded]
self.load_queue_lock.lockForWrite()
self.load_queue = collections.deque(ids)
self.load_queue_lock.unlock()
def run(self):
while self.keep_running:
if self.id_map is None or self.id_map_stale:
self.build_id_map()
while True: # Load images from the load queue
self.load_queue_lock.lockForWrite()
try:
id = self.load_queue.popleft()
except IndexError:
break
finally:
self.load_queue_lock.unlock()
self.cache_lock.lockForRead()
need = True
if id in self.cache.keys():
need = False
self.cache_lock.unlock()
if not need:
continue
path = None
self.id_map_lock.lockForRead()
if id in self.id_map.keys():
path = self.id_map[id]
else:
self.id_map_stale = True
self.id_map_lock.unlock()
if path and os.access(path, os.R_OK):
try:
img = QImage()
data = open(path, 'rb').read()
img.loadFromData(data)
if img.isNull():
continue
scaled, nwidth, nheight = fit_image(img.width(),
img.height(), 600, 800)
if scaled:
img = img.scaled(nwidth, nheight, Qt.KeepAspectRatio,
Qt.SmoothTransformation)
except:
continue
self.cache_lock.lockForWrite()
self.cache[id] = img
self.cache_lock.unlock()
self.sleep(1)
self.cache = {}
self.lock = RLock()
self.null_image = QImage()
def stop(self):
self.keep_running = False
def cover(self, id):
val = None
if self.cache_lock.tryLockForRead(50):
val = self.cache.get(id, None)
self.cache_lock.unlock()
return val
def _image_for_id(self, id_):
img = self.db.cover(id_, index_is_id=True, as_image=True)
if img is None:
img = QImage()
if not img.isNull():
scaled, nwidth, nheight = fit_image(img.width(),
img.height(), 600, 800)
if scaled:
img = img.scaled(nwidth, nheight, Qt.KeepAspectRatio,
Qt.SmoothTransformation)
return img
def run(self):
while self.keep_running:
try:
id_ = self.load_queue.get(True, 1)
except Empty:
continue
try:
img = self._image_for_id(id_)
except:
import traceback
traceback.print_exc()
continue
with self.lock:
self.cache[id_] = img
def set_cache(self, ids):
with self.lock:
already_loaded = set([])
for id in self.cache.keys():
if id in ids:
already_loaded.add(id)
else:
self.cache.pop(id)
for id_ in set(ids) - already_loaded:
self.load_queue.put(id_)
def cover(self, id_):
with self.lock:
return self.cache.get(id_, self.null_image)
def clear_cache(self):
self.cache_lock.lockForWrite()
self.cache = {}
self.cache_lock.unlock()
with self.lock:
self.cache = {}
def refresh(self, ids):
self.cache_lock.lockForWrite()
for id in ids:
self.cache.pop(id, None)
self.cache_lock.unlock()
self.load_queue_lock.lockForWrite()
for id in ids:
self.load_queue.appendleft(id)
self.load_queue_lock.unlock()
with self.lock:
for id_ in ids:
self.cache.pop(id_, None)
self.load_queue.put(id_)
### Global utility function for get_match here and in gui2/library.py
CONTAINS_MATCH = 0
@ -341,8 +296,15 @@ class ResultCache(SearchQueryParser):
cast = lambda x : float (x)
adjust = lambda x: x
if len(query) > 1:
mult = query[-1:].lower()
mult = {'k':1024.,'m': 1024.**2, 'g': 1024.**3}.get(mult, 1.0)
if mult != 1.0:
query = query[:-1]
else:
mult = 1.0
try:
q = cast(query)
q = cast(query) * mult
except:
return matches

View File

@ -8,9 +8,14 @@ __docformat__ = 'restructuredtext en'
import re
from calibre.constants import preferred_encoding
from calibre.ebooks.BeautifulSoup import BeautifulSoup, Tag, NavigableString
from calibre.ebooks.BeautifulSoup import BeautifulSoup, Tag, NavigableString, \
CData, Comment, Declaration, ProcessingInstruction
from calibre import prepare_string_for_xml
# Hackish - ignoring sentences ending or beginning in numbers to avoid
# confusion with decimal points.
lost_cr_pat = re.compile('([a-z])([\.\?!])([A-Z])')
def comments_to_html(comments):
'''
Convert random comment text to normalized, xml-legal block of <p>s
@ -41,36 +46,25 @@ def comments_to_html(comments):
if '<' not in comments:
comments = prepare_string_for_xml(comments)
comments = comments.replace(u'\n', u'<br />')
return u'<p>%s</p>'%comments
# Hackish - ignoring sentences ending or beginning in numbers to avoid
# confusion with decimal points.
parts = [u'<p class="description">%s</p>'%x.replace(u'\n', u'<br />')
for x in comments.split('\n\n')]
return '\n'.join(parts)
# Explode lost CRs to \n\n
for lost_cr in re.finditer('([a-z])([\.\?!])([A-Z])', comments):
for lost_cr in lost_cr_pat.finditer(comments):
comments = comments.replace(lost_cr.group(),
'%s%s\n\n%s' % (lost_cr.group(1),
lost_cr.group(2),
lost_cr.group(3)))
comments = comments.replace(u'\r', u'')
# Convert \n\n to <p>s
if re.search('\n\n', comments):
soup = BeautifulSoup()
split_ps = comments.split(u'\n\n')
tsc = 0
for p in split_ps:
pTag = Tag(soup,'p')
pTag.insert(0,p)
soup.insert(tsc,pTag)
tsc += 1
comments = soup.renderContents(None)
comments = comments.replace(u'\n\n', u'<p>')
# Convert solo returns to <br />
comments = re.sub('[\r\n]','<br />', comments)
comments = comments.replace(u'\n', '<br />')
# Convert two hyphens to emdash
comments = re.sub('--', '&mdash;', comments)
comments = comments.replace('--', '&mdash;')
soup = BeautifulSoup(comments)
result = BeautifulSoup()
rtc = 0
@ -85,35 +79,52 @@ def comments_to_html(comments):
ptc = 0
pTag.insert(ptc,prepare_string_for_xml(token))
ptc += 1
elif token.name in ['br','b','i','em']:
elif type(token) in (CData, Comment, Declaration,
ProcessingInstruction):
continue
elif token.name in ['br', 'b', 'i', 'em', 'strong', 'span', 'font', 'a',
'hr']:
if not open_pTag:
pTag = Tag(result,'p')
open_pTag = True
ptc = 0
pTag.insert(ptc, token)
ptc += 1
else:
if open_pTag:
result.insert(rtc, pTag)
rtc += 1
open_pTag = False
ptc = 0
# Clean up NavigableStrings for xml
sub_tokens = list(token.contents)
for sub_token in sub_tokens:
if type(sub_token) is NavigableString:
sub_token.replaceWith(prepare_string_for_xml(sub_token))
result.insert(rtc, token)
rtc += 1
if open_pTag:
result.insert(rtc, pTag)
paras = result.findAll('p')
for p in paras:
for p in result.findAll('p'):
p['class'] = 'description'
for t in result.findAll(text=True):
t.replaceWith(prepare_string_for_xml(unicode(t)))
return result.renderContents(encoding=None)
def test():
for pat, val in [
('lineone\n\nlinetwo',
'<p class="description">lineone</p>\n<p class="description">linetwo</p>'),
('a <b>b&c</b>\nf', '<p class="description">a <b>b&amp;c;</b><br />f</p>'),
('a <?xml asd> b\n\ncd', '<p class="description">a b</p><p class="description">cd</p>'),
]:
print
print 'Testing: %r'%pat
cval = comments_to_html(pat)
print 'Value: %r'%cval
if comments_to_html(pat) != val:
print 'FAILED'
break
if __name__ == '__main__':
test()

View File

@ -6,7 +6,7 @@ __docformat__ = 'restructuredtext en'
'''
The database used to store ebook metadata
'''
import os, sys, shutil, cStringIO, glob,functools, traceback
import os, sys, shutil, cStringIO, glob, time, functools, traceback
from itertools import repeat
from math import floor
@ -440,12 +440,20 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
if os.access(path, os.R_OK):
if as_path:
return path
f = open(path, 'rb')
try:
f = open(path, 'rb')
except (IOError, OSError):
time.sleep(0.2)
f = open(path, 'rb')
if as_image:
img = QImage()
img.loadFromData(f.read())
f.close()
return img
return f if as_file else f.read()
ans = f if as_file else f.read()
if ans is not f:
f.close()
return ans
def get_metadata(self, idx, index_is_id=False, get_cover=False):
'''
@ -492,12 +500,18 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
path = os.path.join(self.library_path, self.path(id, index_is_id=True), 'cover.jpg')
return os.access(path, os.R_OK)
def remove_cover(self, id):
def remove_cover(self, id, notify=True):
path = os.path.join(self.library_path, self.path(id, index_is_id=True), 'cover.jpg')
if os.path.exists(path):
os.remove(path)
try:
os.remove(path)
except (IOError, OSError):
time.sleep(0.2)
os.remove(path)
if notify:
self.notify('cover', [id])
def set_cover(self, id, data):
def set_cover(self, id, data, notify=True):
'''
Set the cover for this book.
@ -509,7 +523,13 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
else:
if callable(getattr(data, 'read', None)):
data = data.read()
save_cover_data_to(data, path)
try:
save_cover_data_to(data, path)
except (IOError, OSError):
time.sleep(0.2)
save_cover_data_to(data, path)
if notify:
self.notify('cover', [id])
def book_on_device(self, id):
if callable(self.book_on_device_func):

View File

@ -253,7 +253,7 @@ class FieldMetadata(dict):
'is_multiple':None,
'kind':'field',
'name':None,
'search_terms':[],
'search_terms':['size'],
'is_custom':False,
'is_category':False}),
('timestamp', {'table':None,

View File

@ -162,6 +162,9 @@ turned into a collection on the reader. Note that the PRS-500 does not support c
How do I use |app| with my iPad/iPhone/iTouch?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Over the air
^^^^^^^^^^^^^^
The easiest way to browse your |app| collection on your Apple device (iPad/iPhone/iPod) is by using the *free* Stanza app, available from the Apple app store. You need at least Stanza version 3.0. Stanza allows you to access your |app| collection wirelessly, over the air.
First perform the following steps in |app|
@ -181,13 +184,13 @@ Replace ``192.168.1.2`` with the local IP address of the computer running |app|.
If you get timeout errors while browsing the calibre catalog in Stanza, try increasing the connection timeout value in the stanza settings. Go to Info->Settings and increase the value of Download Timeout.
Alternative for the iPad
With the USB cable
^^^^^^^^^^^^^^^^^^^^^^^^^^^
As of |app| version 0.7.0, you can plugin your iPad into the computer using its charging cable, and |app| will detect it and show you a list of books on the iPad. You can then use the Send to device button to send books directly to iBooks on the iPad.
As of |app| version 0.7.0, you can plug your iDevice into the computer using its charging cable, and |app| will detect it and show you a list of books on the device. You can then use the *Send to device button* to send books directly to iBooks on the device. Note that you must have at least iOS 4 installed on your iPhone/iTouch for this to work.
This method only works on Windows XP and higher and OS X 10.5 and higher. Linux is not supported (iTunes is not available in linux) and OS X 10.4 is not supported. For more details, see
`this forum post <http://www.mobileread.com/forums/showpost.php?p=944079&postcount=1>`_.
This method only works on Windows XP and higher and OS X 10.5 and higher. Linux is not supported (iTunes is not available in linux) and OS X 10.4 is not supported.
For more details on how this works, see `this forum post <http://www.mobileread.com/forums/showpost.php?p=944079&postcount=1>`_.
How do I use |app| with my Android phone?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

View File

@ -199,31 +199,59 @@ Searches are by default 'contains'. An item matches if the search string appears
Two other kinds of searches are available: equality search and search using regular expressions.
Equality searches are indicated by prefixing the search string with an equals sign (=). For example, the query
``tag:"=science"`` will match "science", but not "science fiction". Regular expression searches are
``tag:"=science"`` will match "science", but not "science fiction" or "hard science". Regular expression searches are
indicated by prefixing the search string with a tilde (~). Any python-compatible regular expression can
be used. Regular expression searches are contains searches unless the expression contains anchors.
Should you need to search for a string with a leading equals or tilde, prefix the string with a backslash.
Enclose search strings with quotes (") if the string contains parenthesis or spaces. For example, to search
for the tag ``Science Fiction``, you would need to search for ``tag:"=science fiction"``. If you search for
``tag:=science fiction``, you will find all books with the tag 'science' and containing the word 'fiction' in any
metadata.
You can build advanced search queries easily using the :guilabel:`Advanced Search Dialog`, accessed by
clicking the button |sbi|.
Available fields for searching are: ``tag, title, author, publisher, series, rating cover, comments, format,
isbn, date, pubdate, search``.
Available fields for searching are: ``tag, title, author, publisher, series, rating, cover, comments, format,
isbn, date, pubdate, search, size`` and custom columns. If a device is plugged in, the ``ondevice`` field
becomes available. To find the search name for a custom column, hover your mouse over the column header.
The syntax for searching for dates and publication dates is::
The syntax for searching for dates is::
pubdate:>2000-1 Will find all books published after Jan, 2000
date:<=2000-1-3 Will find all books added to calibre beforre 3 Jan, 2000
date:<=2000-1-3 Will find all books added to calibre before 3 Jan, 2000
pubdate:=2009 Will find all books published in 2009
If the date is ambiguous, the current locale is used for date comparison. For example, in an mm/dd/yyyy
locale, 2/1/2009 is interpreted as 1 Feb 2009. In a dd/mm/yyyy locale, it is interpreted as 2 Jan 2009.
Some special date strings are available. The string ``today`` translates to today's date, whatever it is. The
strings `yesterday`` and ``thismonth`` also work. In addition, the string ``daysago`` can be used to compare
to a date some number of days ago, for example: date:>10daysago, date:<=45daysago.
You can search for books that have a format of a certain size like this::
size:>1.1M Will find books with a format larger than 1.1MB
size:<=1K Will find books with a format smaller than 1KB
Dates and numeric fields support the operators ``=`` (equals), ``>`` (greater than), ``>=`` (greater than or
equal to), ``<`` (less than), ``<=`` (less than or equal to), and ``!=`` (not equal to). Rating fields are
considered to be numeric. For example, the search ``rating:>=3`` will find all books rated 3 or higher.
The special field ``search`` is used for saved searches. So if you save a search with the name
"My spouse's books" you can enter ``search:"My spouses' books"`` in the search bar to reuse the saved
"My spouse's books" you can enter ``search:"My spouse's books"`` in the search bar to reuse the saved
search. More about saving searches, below.
You can search for the absence or presnce of a filed using the specia "true" and "false" values. For example::
You can search for the absence or presence of a field using the special "true" and "false" values. For example::
cover:false Will give you all books without a cover
series:true Will give you all books that belong to a series
cover:false will give you all books without a cover
series:true will give you all books that belong to a series
comments:false will give you all books with an empty comment
Yes/no custom columns are searchable. Searching for ``false``, ``empty``, or ``blank`` will find all books
with undefined values in the column. Searching for ``true`` will find all books that do not have undefined
values in the column. Searching for ``yes`` or ``checked`` will find all books with ``Yes`` in the column.
Searching for ``no`` or ``unchecked`` will find all books with ``No`` in the column.
.. |sbi| image:: images/search_button.png
:align: middle

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

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