Sync to trunk.

This commit is contained in:
John Schember 2010-05-16 08:48:07 -04:00
commit b652e42fe4
63 changed files with 1604 additions and 597 deletions

View File

@ -4,8 +4,89 @@
# for important features/bug fixes. # for important features/bug fixes.
# Also, each release can have new and improved recipes. # Also, each release can have new and improved recipes.
- version: 0.6.53
date: 2010-05-15
new features:
- title: "Clean up GUI initialization and add support for restoring corrupted databases automatically"
- title: "Make proxy detection more robust on windows and OS X. calibre now queries OS X Network Settigns if no environment variables are set. Also handle proxies with a trailing slash correctly"
- title: "Add EPUB advanced formatting demo to User Manual"
- title: "Support for the Booq Avant, Azbooka and the Samsung GT-I5700"
- title: "Backwards search in the E-book viewer"
- title: "calibredb: Add ability to create empty books in the database."
tickets: [5504]
- title: "Conversion pipeline: Support for the :first-letter pseudo selector"
- title: "Interpret a Keyboard interrupt (Ctrl+C) as a request to quit the main GUI"
- title: "CBC Input: Handle comics.txt encoded in UTF-16 with a BOM"
bug fixes:
- title: "HTML Input: Fix silly bug in case sensitivity detection"
- title: "Kobo driver: Show all sideloaded content on the device."
tickets: [5492]
- title: "EPUB metadata: Fix bug with relative apths in encryption detection when reading cover"
tickets: [5471]
- title: "E-book viewer: Fix next page scrolling when current document is just a little more than a screenfull. Also use a more robust method to insert blank space at the end of the document when the last screenfull is partially empty."
- title: "EPUB metadata: Allow deletion of series/tags/isbn from EPUB files when Saving to Disk"
tickets: [5518]
- title: "Fix regression that caused temporary blank line at the bottom of the books list when adding duplicates"
tickets: [5500]
- title: "Add icon for RTF"
tickets: [5503]
- title: "Amazon metadata: If ISBN is not found, don't report an error message."
tickets: [5501]
- title: "EPUB Input: Fix typo that caused incorrect processing of EPUB files with more than one identifier element and encrypted fonts"
- title: "Fix bug that caused send to device to send multiple copies to the device if you had previously used Prefrences"
- title: "Linux prs 500 udev rule: Use SUBSYSTEMS instead of the deprecated BUS"
- title: "PML2PMLZ plugin: Actually compress the PML file stored in the PMLZ archive"
tickets: [5511]
- title: "SONY drivers: Fix regression that broke collection ordering by series when sending to device. And fix another rare error condition."
tickets: [5487]
- title: "CHM Input: Regression that broke CHM conversion on OS X."
tickets: [5483]
- title: "Fix PDB created in Dropbook not convertable by Calibre"
tickets: [5441]
new recipes:
- title: APCOM, Leggo (it), Ansa and Punto Informatico
author: Gabriele Marini
- title: Scinexx.de
author: JSuer
- title: Various Russian news sources
author: Darko Miletic
improved recipes:
- Christian Science Monitor
- The Nation
- Physics World
- Discover Magazine
- version: 0.6.52 - version: 0.6.52
date: 2010-07-30 date: 2010-05-07
new features: new features:
- title: "Support for the Kobo Reader and the HTC Desire" - title: "Support for the Kobo Reader and the HTC Desire"

Binary file not shown.

After

Width:  |  Height:  |  Size: 1003 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 475 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 353 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 534 B

View File

@ -0,0 +1,72 @@
#!/usr/bin/env python
__license__ = 'GPL v3'
__author__ = 'Gabriele Marini, based on Darko Miletic'
__copyright__ = '2010, Darko Miletic <darko.miletic at gmail.com>'
description = 'Italian daily newspaper - 01-05-2010'
'''
http://www.ansa.it/
'''
from calibre.web.feeds.news import BasicNewsRecipe
class Ansa(BasicNewsRecipe):
__author__ = 'Gabriele Marini'
description = 'Italian News Agency'
cover_url = 'http://www.ansa.it/web/images/logo_ansa_interna.gif'
title = u'Ansa'
publisher = 'Ansa'
category = 'News, politics, culture, economy, general interest'
language = 'it'
timefmt = '[%a, %d %b, %Y]'
oldest_article = 1
max_articles_per_feed = 10
use_embedded_content = False
recursion = 10
remove_javascript = True
no_stylesheets = True
conversion_options = {'linearize_tables':True}
remove_attributes = ['colspan']
keep_only_tags = [dict(name='div', attrs={'class':['path','header-content','corpo']}),
]
remove_tags = [
dict(name='div', attrs={'class':'tools-bar'}),
dict(name='div', attrs={'id':['rssdiv','blocco']})
]
feeds = [
(u'HomePage', u'http://www.ansa.it/web/ansait_web_rss_homepage.xml'),
(u'Top New', u'http://www.ansa.it/web/notizie/rubriche/topnews/topnews_rss.xml'),
(u'Cronaca', u'http://www.ansa.it/web/notizie/rubriche/cronaca/cronaca_rss.xml'),
(u'Mondo', u'http://www.ansa.it/web/notizie/rubriche/mondo/mondo_rss.xml'),
(u'Economia', u'http://www.ansa.it/web/notizie/rubriche/economia/economia_rss.xml'),
(u'Politica', u'http://www.ansa.it/web/notizie/rubriche/politica/politica_rss.xml'),
(u'Scienze', u'http://www.ansa.it/web/notizie/rubriche/scienza/scienza_rss.xml'),
(u'Cinema', u'http://www.ansa.it/web/notizie/rubriche/cinema/cinema_rss.xml'),
(u'Tecnologia e Internet', u'http://www.ansa.it/web/notizie/rubriche/tecnologia/tecnologia_rss.xml'),
(u'Spettacolo', u'http://www.ansa.it/web/notizie/rubriche/spettacolo/spettacolo_rss.xml'),
(u'Cultura e Tendenze', u'http://www.ansa.it/web/notizie/rubriche/cultura/cultura_rss.xml'),
(u'Sport', u'http://www.ansa.it/web/notizie/rubriche/altrisport/altrisport_rss.xml'),
(u'Calcio', u'http://www.ansa.it/web/notizie/rubriche/calcio/calcio_rss.xml'),
(u'Lazio', u'http://www.ansa.it/web/notizie/regioni/lazio/lazio_rss.xml'),
(u'Lombardia', u'http://www.ansa.it/web/notizie/regioni/lombardia/lombardia.shtml'),
(u'Veneto', u'http://www.ansa.it/web/notizie/regioni/veneto/veneto.shtml'),
(u'Campanioa', u'http://www.ansa.it/web/notizie/regioni/campania/campania.shtml'),
(u'Sicilia', u'http://www.ansa.it/web/notizie/regioni/sicilia/sicilia.shtml'),
(u'Toscana', u'http://www.ansa.it/web/notizie/regioni/toscana/toscana.shtml'),
(u'Trentino', u'http://www.ansa.it/web/notizie/regioni/trentino/trentino.shtml')
]
extra_css = '''
.path{font-style: italic; font-size: small}
.header-content h1{font-weight: bold; font-size: xx-large}
.header-content h2{font-weight: bold; font-size: x-large; font-syle: italic}
.content-corpo{font-size: 14px;font-family: Times New Roman;}
'''

View File

@ -0,0 +1,31 @@
__license__ = 'GPL v3'
__copyright__ = '2010, Darko Miletic <darko.miletic at gmail.com>'
'''
www.aif.ru
'''
from calibre.web.feeds.news import BasicNewsRecipe
class AIF_ru(BasicNewsRecipe):
title = 'Arguments & Facts - Russian'
__author__ = 'Darko Miletic'
description = 'News from Russia'
publisher = 'AIF'
category = 'news, politics, Russia'
oldest_article = 2
max_articles_per_feed = 100
no_stylesheets = True
use_embedded_content = False
encoding = 'cp1251'
language = 'ru'
publication_type = 'magazine'
extra_css = ' @font-face {font-family: "sans1";src:url(res:///opt/sony/ebook/FONT/tt0003m_.ttf)} body{font-family: Verdana,Arial,Helvetica,sans1,sans-serif} '
keep_only_tags = [dict(name='div',attrs={'id':'inner'})]
remove_tags = [
dict(name=['iframe','object','link','base','input','img'])
,dict(name='div',attrs={'class':'photo'})
,dict(name='p',attrs={'class':'resizefont'})
]
feeds = [(u'News', u'http://www.aif.ru/rss/all.php')]

View File

@ -0,0 +1,48 @@
#!/usr/bin/env python
__license__ = 'GPL v3'
__author__ = 'Gabriele Marini, based on Darko Miletic'
__copyright__ = '2009-2010, Darko Miletic <darko.miletic at gmail.com>'
description = 'Italian daily newspaper - 14-05-2010'
'''
http://www.apcom.NET/
'''
from calibre.web.feeds.news import BasicNewsRecipe
class Apcom(BasicNewsRecipe):
__author__ = 'Marini Gabriele'
description = 'Italian daily newspaper'
cover_url = 'http://www.apcom.net/img/logoAP.gif'
title = u'Apcom'
publisher = 'TM News S.p.A.'
category = 'News, politics, culture, economy, general interest'
language = 'it'
timefmt = '[%a, %d %b, %Y]'
oldest_article = 7
max_articles_per_feed = 50
use_embedded_content = False
recursion = 100
no_stylesheets = True
conversion_options = {'linearize_tables':True}
remove_javascript = True
keep_only_tags = [
dict(name='div', attrs={'id':'ag_center'})
]
feeds = [
(u'Globale', u'http://www.apcom.net/rss/globale.xml '),
(u'Politica', u'http://www.apcom.net/rss/politica.xml'),
(u'Cronaca', u'http://www.apcom.net/rss/cronaca.xml'),
(u'Econimia', u'http://www.apcom.net/rss/economia.xml'),
(u'Esteri', u'http://www.apcom.net/rss/esteri.xml'),
(u'Cultura', u'http://www.apcom.net/rss/cultura.xml'),
(u'Sport', u'http://www.apcom.net/rss/sport.xml')
]

View File

@ -37,6 +37,7 @@ class ChristianScienceMonitor(BasicNewsRecipe):
preprocess_regexps = [ (re.compile(i[0], re.IGNORECASE | re.DOTALL), i[1]) for i in preprocess_regexps = [ (re.compile(i[0], re.IGNORECASE | re.DOTALL), i[1]) for i in
[ [
(r'<!--.*?-->', lambda match : ''),
(r'<body.*?<div id="story"', lambda match : '<body><div id="story"'), (r'<body.*?<div id="story"', lambda match : '<body><div id="story"'),
(r'<div class="pubdate">.*?</div>', lambda m: ''), (r'<div class="pubdate">.*?</div>', lambda m: ''),
(r'Full HTML version of this story which may include photos, graphics, and related links.*</body>', (r'Full HTML version of this story which may include photos, graphics, and related links.*</body>',
@ -79,7 +80,10 @@ class ChristianScienceMonitor(BasicNewsRecipe):
remove_tags = [ remove_tags = [
dict(name='div', attrs={'id':['story-tools','videoPlayer','storyRelatedBottom','enlarge-photo','photo-paginate']}), dict(name='div', attrs={'id':['story-tools','videoPlayer','storyRelatedBottom','enlarge-photo','photo-paginate']}),
dict(name='div', attrs={'class':['storyToolbar cfx','podStoryRel','spacer3','divvy spacer7','comment','storyIncludeBottom']}), dict(name=['div','a'], attrs={'class':
['storyToolbar cfx','podStoryRel','spacer3',
'divvy spacer7','comment','storyIncludeBottom',
'hide', 'podBrdr']}),
dict(name='ul', attrs={'class':[ 'centerliststories']}) , dict(name='ul', attrs={'class':[ 'centerliststories']}) ,
dict(name='form', attrs={'id':[ 'commentform']}) , dict(name='form', attrs={'id':[ 'commentform']}) ,
] ]

View File

@ -7,13 +7,14 @@ __docformat__ = 'restructuredtext en'
discovermagazine.com discovermagazine.com
''' '''
import re
from calibre.web.feeds.news import BasicNewsRecipe from calibre.web.feeds.news import BasicNewsRecipe
class DiscoverMagazine(BasicNewsRecipe): class DiscoverMagazine(BasicNewsRecipe):
title = u'Discover Magazine' title = u'Discover Magazine'
description = u'Science, Technology and the Future' description = u'Science, Technology and the Future'
__author__ = 'Mike Diaz' __author__ = 'Starson17'
language = 'en' language = 'en'
oldest_article = 33 oldest_article = 33
@ -23,25 +24,63 @@ class DiscoverMagazine(BasicNewsRecipe):
use_embedded_content = False use_embedded_content = False
encoding = 'utf-8' encoding = 'utf-8'
extra_css = '.headline {font-size: x-large;} \n .fact {padding-top: 10pt}' extra_css = '.headline {font-size: x-large;} \n .fact {padding-top: 10pt}'
remove_tags = [dict(name='div', attrs={'id':['searchModule', 'mainMenu', 'tool-box']}), remove_tags = [
dict(name='div', attrs={'id':['searchModule', 'mainMenu', 'tool-box']}),
dict(name='div', attrs={'id':['footer','teaser','already-subscriber','teaser-suite','related-articles']}),
dict(name='div', attrs={'class':['column']}),
dict(name='img', attrs={'src':'http://discovermagazine.com/onebyone.gif'})] dict(name='img', attrs={'src':'http://discovermagazine.com/onebyone.gif'})]
remove_tags_after = [dict(name='div', attrs={'class':'articlebody'})] remove_tags_after = [dict(name='div', attrs={'class':'listingBar'})]
def append_page(self, soup, appendtag, position):
pager = soup.find('span',attrs={'class':'next'})
if pager:
nexturl = pager.a['href']
soup2 = self.index_to_soup(nexturl)
texttag = soup2.find('div', attrs={'class':'articlebody'})
newpos = len(texttag.contents)
self.append_page(soup2,texttag,newpos)
texttag.extract()
appendtag.insert(position,texttag)
def preprocess_html(self, soup):
mtag = '<meta http-equiv="Content-Language" content="en-US"/>\n<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>'
soup.head.insert(0,mtag)
self.append_page(soup, soup.body, 3)
pager = soup.find('div',attrs={'class':'listingBar'})
if pager:
pager.extract()
return soup
def postprocess_html(self, soup, first_fetch):
for tag in soup.findAll(text=re.compile('^This article is a sample')):
tag.parent.extract()
for tag in soup.findAll(['table', 'tr', 'td']):
tag.name = 'div'
for tag in soup.findAll('div', attrs={'class':'discreet advert'}):
tag.extract()
for tag in soup.findAll('hr', attrs={'size':'1'}):
tag.extract()
for tag in soup.findAll('br'):
tag.extract()
return soup
feeds = [ feeds = [
(u'Technology', u'http://discovermagazine.com/topics/technology/rss.xml'), (u'Technology', u'http://discovermagazine.com/topics/technology/rss.xml'),
(u'Health - Medicine', u'http://discovermagazine.com/topics/health-medicine/rss.xml'), (u'Health - Medicine', u'http://discovermagazine.com/topics/health-medicine/rss.xml'),
(u'Mind Brain', u'http://discovermagazine.com/topics/mind-brain/rss.xml'), (u'Mind Brain', u'http://discovermagazine.com/topics/mind-brain/rss.xml'),
(u'Space', u'http://discovermagazine.com/topics/space/rss.xml'), (u'Space', u'http://discovermagazine.com/topics/space/rss.xml'),
(u'Human Origins', u'http://discovermagazine.com/topics/human-origins/rss.xml'), (u'Human Origins', u'http://discovermagazine.com/topics/human-origins/rss.xml'),
(u'Living World', u'http://discovermagazine.com/topics/living-world/rss.xml'), (u'Living World', u'http://discovermagazine.com/topics/living-world/rss.xml'),
(u'Environment', u'http://discovermagazine.com/topics/environment/rss.xml'), (u'Environment', u'http://discovermagazine.com/topics/environment/rss.xml'),
(u'Physics & Math', u'http://discovermagazine.com/topics/physics-math/rss.xml'), (u'Physics & Math', u'http://discovermagazine.com/topics/physics-math/rss.xml'),
(u'Vital Signs', u'http://discovermagazine.com/columns/vital-signs/rss.xml'), (u"20 Things you didn't know about...", u'http://discovermagazine.com/columns/20-things-you-didnt-know/rss.xml'),
(u"20 Things you didn't know about...", u'http://discovermagazine.com/columns/20-things-you-didnt-know/rss.xml'), (u'Fuzzy Math', u'http://discovermagazine.com/columns/fuzzy-math/rss.xml'),
(u'Fuzzy Math', u'http://discovermagazine.com/columns/fuzzy-math/rss.xml'), (u'The Brain', u'http://discovermagazine.com/columns/the-brain/rss.xml'),
(u'The Brain', u'http://discovermagazine.com/columns/the-brain/rss.xml'), (u'What is This', u'http://discovermagazine.com/columns/what-is-this/rss.xml'),
(u'Stupid Science Word of the Month', u'http://discovermagazine.com/columns/stupid-science-word-of-the-month/rss.xml'), (u'Vital Signs', u'http://discovermagazine.com/columns/vital-signs/rss.xml'),
(u'Science Not Fiction', u'http://blogs.discovermagazine.com/sciencenotfiction/wp-rss.php') (u'Think Tech', u'http://discovermagazine.com/columns/think-tech/rss.xml'),
] (u'Future Tech', u'http://discovermagazine.com/columns/future-tech/rss.xml'),
(u'Discover Interview', u'http://discovermagazine.com/columns/discover-interview/rss.xml'),
]

View File

@ -69,9 +69,9 @@ class Economist(BasicNewsRecipe):
key = None key = None
for tag in soup.findAll(['h1', 'h2']): for tag in soup.findAll(['h1', 'h2']):
text = ''.join(tag.findAll(text=True)) text = ''.join(tag.findAll(text=True))
if tag.name in ('h1', 'h2') and 'Classified ads' in text:
break
if tag.name == 'h1': if tag.name == 'h1':
if 'Classified ads' in text:
break
if 'The world this week' in text or 'The world this year' in text: if 'The world this week' in text or 'The world this year' in text:
index_started = True index_started = True
if not index_started: if not index_started:

View File

@ -0,0 +1,28 @@
__license__ = 'GPL v3'
__copyright__ = '2010, Darko Miletic <darko.miletic at gmail.com>'
'''
izvestia.ru
'''
from calibre.web.feeds.news import BasicNewsRecipe
class Izvestia(BasicNewsRecipe):
title = 'Izvestia'
__author__ = 'Darko Miletic'
description = 'News from Russia'
publisher = 'Izvestia'
category = 'news, politics, Russia'
oldest_article = 5
max_articles_per_feed = 100
no_stylesheets = True
use_embedded_content = False
encoding = 'cp1251'
language = 'ru'
publication_type = 'newspaper'
masthead_url = 'http://images.izvestia.ru/izv/sys/logo.gif'
extra_css = ' @font-face {font-family: "sans1";src:url(res:///opt/sony/ebook/FONT/tt0003m_.ttf)} body{font-family: Verdana,Arial,Helvetica,sans1,sans-serif} '
keep_only_tags = [dict(name='div', attrs={'class':'newsFull'})]
remove_tags = [dict(name=['iframe','object','img','link','base'])]
remove_tags_before = dict(name='h1', attrs={'class':'statya'})
feeds = [(u'Daily edition', u'http://rss.feedsportal.com/c/32171/f/424076/index.rss')]

View File

@ -0,0 +1,42 @@
__license__ = 'GPL v3'
__copyright__ = '2010, Darko Miletic <darko.miletic at gmail.com>'
'''
www.kommersant.ru
'''
from calibre.web.feeds.news import BasicNewsRecipe
class Kommersant_ru(BasicNewsRecipe):
title = 'Kommersant'
__author__ = 'Darko Miletic'
description = 'News from Russia'
publisher = 'Kommersant'
category = 'news, politics, Russia'
oldest_article = 5
max_articles_per_feed = 100
no_stylesheets = True
use_embedded_content = False
encoding = 'cp1251'
language = 'ru'
publication_type = 'newspaper'
masthead_url = 'http://www.kommersant.ru/CorpPics/logo_daily_1.gif'
extra_css = ' @font-face {font-family: "sans1";src:url(res:///opt/sony/ebook/FONT/tt0003m_.ttf)} body{font-family: Arial, sans1, sans-serif} span#ctl00_ContentPlaceHolderStyle_LabelSubTitle{margin-bottom: 1em; display: block} .author{margin-bottom: 1em; display: block} .paragraph{margin-bottom: 1em; display: block} .vvodka{font-weight: bold; margin-bottom: 1em} '
conversion_options = {
'comment' : description
, 'tags' : category
, 'publisher' : publisher
, 'language' : language
}
keep_only_tags = [
dict(attrs={'id':'ctl00_ContentPlaceHolderStyle_PanelHeader'})
,dict(attrs={'class':['vvodka','paragraph','author']})
]
remove_tags = [dict(name=['iframe','object','link','img','base'])]
feeds = [(u'Articles', u'http://feeds.kommersant.ru/RSS_Export/RU/daily.xml')]
def print_version(self, url):
return url.replace('doc-rss.aspx','doc.aspx') + '&print=true'

View File

@ -0,0 +1,49 @@
#!/usr/bin/env python
__license__ = 'GPL v3'
__author__ = 'Gabriele Marini, based on Darko Miletic'
__copyright__ = '2009, Darko Miletic <darko.miletic at gmail.com>'
description = 'Italian daily newspaper - v1.00 05-05-2010'
'''
http://www.leggo.it
'''
from calibre.web.feeds.news import BasicNewsRecipe
class LeggoIT(BasicNewsRecipe):
__author__ = 'Gabriele Marini'
description = 'Italian Free daily newspaper'
cover_url = 'http://www.leggo.it/img/logo-leggo2.gif'
title = u'Leggo.it'
publisher = 'Ced Caltagirone Editore S.p.A.'
category = 'News, politics, culture, economy, general interest'
language = 'it'
timefmt = '[%a, %d %b, %Y]'
oldest_article = 5
max_articles_per_feed = 100
use_embedded_content = False
recursion = 100
no_stylesheets = True
remove_javascript = True
conversion_options = {'linearize_tables':True}
keep_only_tags = [
dict(name='h1',attrs={'class':'nero22'}),
dict(name='div',attrs={'id':'testodim'})
]
feeds = [
(u'Home Page', u'http://www.leggo.it/rss/home.xml'),
(u'Italia', u'http://www.leggo.it/rss/italia.xml'),
(u'Esteri', u'http://www.leggo.it/rss/esteri.xml'),
(u'Economia', u'http://www.leggo.it/rss/economia.xml'),
(u'Sport', u'http://www.leggo.it/rss/sport.xml'),
(u'Gossip', u'http://www.leggo.it/rss/gossip.xml'),
(u'Spettacoli', u'http://www.leggo.it/rss/spettacoli.xml'),
(u'Salute', u'http://www.leggo.it/rss/salute.xml'),
(u'Scienza', u'http://www.leggo.it/rss/scienza.xml')
]

View File

@ -10,6 +10,7 @@ class PhysicsWorld(BasicNewsRecipe):
oldest_article = 7 oldest_article = 7
max_articles_per_feed = 100 max_articles_per_feed = 100
no_stylesheets = True no_stylesheets = True
cover_url = 'http://images.iop.org/cws/icons/themes/phw/header-logo.png'
use_embedded_content = False use_embedded_content = False
remove_javascript = True remove_javascript = True
needs_subscription = True needs_subscription = True
@ -27,7 +28,7 @@ class PhysicsWorld(BasicNewsRecipe):
br = BasicNewsRecipe.get_browser(self) br = BasicNewsRecipe.get_browser(self)
if self.username is not None and self.password is not None: if self.username is not None and self.password is not None:
br.open('http://physicsworld.com/cws/sign-in') br.open('http://physicsworld.com/cws/sign-in')
br.select_form(nr=1) br.select_form(nr=2)
br['username'] = self.username br['username'] = self.username
br['password'] = self.password br['password'] = self.password
br.submit() br.submit()

View File

@ -0,0 +1,38 @@
#!/usr/bin/env python
__license__ = 'GPL v3'
__author__ = 'Gabriele Marini'
__copyright__ = 'Gabriele Marini'
__description__ = 'Punto Informatico'
'''
http://www.punto-informatico.it/
'''
from calibre.web.feeds.news import BasicNewsRecipe
class PuntoInformatico(BasicNewsRecipe):
__author__ = 'Gabriele Marini'
description = 'Punto Informatico: Internet dal 1996'
cover_url = 'http://punto-informatico.it/images/logo_8bit.png'
title = u'Punto Informatico '
publisher = 'italiaNews High Tech'
category = 'News, Information Tecnology'
language = 'it'
timefmt = '[%a, %d %b, %Y]'
oldest_article = 15
max_articles_per_feed = 50
use_embedded_content = False
remove_javascript = True
no_stylesheets = True
keep_only_tags = [dict(name='div', attrs={'class':'box'})]
remove_tags = [dict(name='div',attrs={'class':'boxadv'})]
def get_article_url(self, article):
return article.get('id', article.get('guid', None))
feeds = [(u'Punto Informatico',u'http://feeds.punto-informatico.it/c/32288/f/438866/index.rss')]

View File

@ -0,0 +1,43 @@
__license__ = 'GPL v3'
__copyright__ = '2010, Darko Miletic <darko.miletic at gmail.com>'
'''
www.rian.ru
'''
from calibre.web.feeds.news import BasicNewsRecipe
class RIANovosti(BasicNewsRecipe):
title = 'RIA Novosti - Russian'
__author__ = 'Darko Miletic'
description = 'News from Russia'
publisher = 'RIA'
category = 'news, politics, Russia'
oldest_article = 2
max_articles_per_feed = 100
no_stylesheets = True
use_embedded_content = False
encoding = 'utf8'
language = 'ru'
publication_type = 'newsportal'
masthead_url = 'http://img.beta.rian.ru/images/22868/43/228684314.jpg'
extra_css = ' @font-face {font-family: "sans1";src:url(res:///opt/sony/ebook/FONT/tt0003m_.ttf)} body{font-family: Arial,Helvetica,sans1,sans-serif} '
remove_tags_before = dict(name='h1')
remove_tags_after = dict(name='div', attrs={'class':'text'})
remove_tags = [dict(name=['iframe','object','link','img','base'])]
feeds = [
(u'Frontpage', u'http://www.rian.ru/export/rss2/lenta/index.xml')
,(u'Politics', u'http://www.rian.ru/export/rss2/politics/index.xml')
,(u'World', u'http://www.rian.ru/export/rss2/world/index.xml')
,(u'Economy', u'http://www.rian.ru/export/rss2/economy/index.xml')
,(u'Society', u'http://www.rian.ru/export/rss2/society/index.xml')
,(u'Moscow', u'http://www.rian.ru/export/rss2/moscow/index.xml')
,(u'Defense', u'http://www.rian.ru/export/rss2/defense_safety/index.xml')
,(u'Science', u'http://www.rian.ru/export/rss2/science/index.xml')
,(u'Turism', u'http://www.rian.ru/export/rss2/tourism/index.xml')
,(u'Culture', u'http://www.rian.ru/export/rss2/culture/index.xml')
]
def print_version(self, url):
return url.replace('.html','-print.html')

View File

@ -0,0 +1,35 @@
from calibre.web.feeds.news import BasicNewsRecipe
class AdvancedUserRecipe1265145870(BasicNewsRecipe):
title = u'Scinexx.de'
language = 'de'
__author__ = 'JSuer'
cover_url = 'http://www.g-o.de/grafiken/web_scinexx/head2.gif'
oldest_article = 14
max_articles_per_feed = 100
no_stylesheets = True
use_embedded_content = False
encoding = 'ISO-8859-1'
feeds = [(u'Scinexx.de', u'http://feeds.feedburner.com/scinexx')]
remove_tags = [{'class':['text1fett']}]
remove_tags = [{'href':['javascript:window.print()']}]
extra_css = '''
.text2normal{font-family:Verdana,Geneva,Kalimati,sans-serif; font-size:x-small;}
.text1normalblau{font-family:Verdana,Geneva,Kalimati,sans-serif; font-size:small;}
.text1fett{font-color:grey; font-size:small;}
.titel1{font-family:Georgia,"Times New Roman",Times,serif; font-size:large;}
.titel2{font-family:Georgia,"Times New Roman",Times,serif; }
.titel3{font-family:Georgia,"Times New Roman",Times,serif; font-size:larger;}
h1{font-size:large;}
'''
def print_version(self, url):
id_start = url.rfind('2010') - 6
id_end = id_start + 5
id = url[id_start : id_end]
result = 'http://www.scinexx.de/inc/artikel_drucken.php?id=' + id + '&a_flag=1'
return result

View File

@ -15,7 +15,7 @@ class al(BasicNewsRecipe):
description = 'The Escapist Magazine' description = 'The Escapist Magazine'
cover_url = 'http://cdn.themis-media.com/themes/escapistmagazine/default/images/logo.png' cover_url = 'http://cdn.themis-media.com/themes/escapistmagazine/default/images/logo.png'
title = u'the Escapist Magazine' title = u'The Escapist Magazine'
publisher = 'Themis media' publisher = 'Themis media'
category = 'Video games news, lifestyle, gaming culture' category = 'Video games news, lifestyle, gaming culture'

View File

@ -1,37 +1,40 @@
#!/usr/bin/env python
__license__ = 'GPL v3' __license__ = 'GPL v3'
__copyright__ = '2008, Darko Miletic <darko.miletic at gmail.com>' __copyright__ = '2008 - 2010, Darko Miletic <darko.miletic at gmail.com>'
''' '''
thenation.com thenation.com
''' '''
from calibre.web.feeds.news import BasicNewsRecipe from calibre.web.feeds.news import BasicNewsRecipe
class Thenation(BasicNewsRecipe): class Thenation(BasicNewsRecipe):
title = u'The Nation' title = 'The Nation'
__author__ = u'Darko Miletic' __author__ = 'Darko Miletic'
description = u'Unconventional Wisdom Since 1865' description = 'Unconventional Wisdom Since 1865'
publisher = 'The Nation'
category = 'news, politics, USA'
oldest_article = 120 oldest_article = 120
encoding = 'utf-8'
max_articles_per_feed = 100 max_articles_per_feed = 100
no_stylesheets = True no_stylesheets = True
language = 'en' language = 'en'
use_embedded_content = False
use_embedded_content = False
simultaneous_downloads = 1
delay = 1 delay = 1
timefmt = ' [%A, %d %B, %Y]' masthead_url = 'http://www.thenation.com/sites/default/themes/thenation/images/logo-main.gif'
exra_css = ' body{font-family: Arial,Helvetica,sans-serif;} .print-created{font-size: small;} .caption{display: block; font-size: x-small;} '
keep_only_tags = [ dict(name='div', attrs={'class':'main'}) ] conversion_options = {
remove_tags = [ 'comment' : description
dict(name='div', attrs={'class':'mod tools'}) , 'tags' : category
,dict(name='div', attrs={'class':'inset' }) , 'publisher' : publisher
,dict(name='div', attrs={'class':'share' }) , 'language' : language
,dict(name='ol' , attrs={'id' :'comments' }) }
,dict(name='p' , attrs={'class':'info' })
,dict(name='a' , attrs={'class':'comments' })
,dict(name='ul' , attrs={'class':'important'})
,dict(name='object')
]
feeds = [(u"Top Stories", u'http://feedproxy.google.com/TheNationEdPicks')] keep_only_tags = [ dict(attrs={'class':['print-title','print-created','print-content','print-links']}) ]
remove_tags = [dict(name='link')]
feeds = [(u"Editor's Picks", u'http://www.thenation.com/rss/editors_picks')]
def print_version(self, url):
return url.replace('.thenation.com/','.thenation.com/print/')
def preprocess_html(self, soup):
return self.adeify_images(soup)

View File

@ -4,6 +4,7 @@ __copyright__ = '2008, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
import sys, os, re, logging, time, mimetypes, \ import sys, os, re, logging, time, mimetypes, \
__builtin__, warnings, multiprocessing __builtin__, warnings, multiprocessing
from urllib import getproxies
__builtin__.__dict__['dynamic_property'] = lambda(func): func(None) __builtin__.__dict__['dynamic_property'] = lambda(func): func(None)
from htmlentitydefs import name2codepoint from htmlentitydefs import name2codepoint
from math import floor from math import floor
@ -199,71 +200,54 @@ def extract(path, dir):
extractor(path, dir) extractor(path, dir)
def get_proxies(debug=True): def get_proxies(debug=True):
proxies = {} proxies = getproxies()
for key, proxy in list(proxies.items()):
if not proxy or '..' in proxy:
del proxies[key]
continue
if proxy.startswith(key+'://'):
proxy = proxy[len(key)+3:]
if proxy.endswith('/'):
proxy = proxy[:-1]
if len(proxy) > 4:
proxies[key] = proxy
else:
prints('Removing invalid', key, 'proxy:', proxy)
del proxies[key]
for q in ('http', 'ftp'):
proxy = os.environ.get(q+'_proxy', None)
if not proxy: continue
if proxy.startswith(q+'://'):
proxy = proxy[7:]
proxies[q] = proxy
if iswindows:
try:
winreg = __import__('_winreg')
settings = winreg.OpenKey(winreg.HKEY_CURRENT_USER,
'Software\\Microsoft\\Windows'
'\\CurrentVersion\\Internet Settings')
proxy = winreg.QueryValueEx(settings, "ProxyEnable")[0]
if proxy:
server = str(winreg.QueryValueEx(settings, 'ProxyServer')[0])
if ';' in server:
for p in server.split(';'):
protocol, address = p.split('=')
proxies[protocol] = address
else:
proxies['http'] = server
proxies['ftp'] = server
settings.Close()
except Exception, e:
prints('Unable to detect proxy settings: %s' % str(e))
for x in list(proxies):
if len(proxies[x]) < 5:
prints('Removing invalid', x, 'proxy:', proxies[x])
del proxies[x]
if proxies and debug: if proxies and debug:
prints('Using proxies:', proxies) prints('Using proxies:', proxies)
return proxies return proxies
def get_parsed_proxy(typ='http', debug=True): def get_parsed_proxy(typ='http', debug=True):
proxies = get_proxies(debug) proxies = get_proxies(debug)
if typ not in proxies: proxy = proxies.get(typ, None)
return if proxy:
pattern = re.compile(( pattern = re.compile((
'(?:ptype://)?' \ '(?:ptype://)?' \
'(?:(?P<user>\w+):(?P<pass>.*)@)?' \ '(?:(?P<user>\w+):(?P<pass>.*)@)?' \
'(?P<host>[\w\-\.]+)' \ '(?P<host>[\w\-\.]+)' \
'(?::(?P<port>\d+))?').replace('ptype', typ) '(?::(?P<port>\d+))?').replace('ptype', typ)
) )
match = pattern.match(proxies['typ']) match = pattern.match(proxies[typ])
if match: if match:
try: try:
ans = { ans = {
'host' : match.group('host'), 'host' : match.group('host'),
'port' : match.group('port'), 'port' : match.group('port'),
'user' : match.group('user'), 'user' : match.group('user'),
'pass' : match.group('pass') 'pass' : match.group('pass')
} }
if ans['port']: if ans['port']:
ans['port'] = int(ans['port']) ans['port'] = int(ans['port'])
except: except:
if debug: if debug:
traceback.print_exc() traceback.print_exc()
return else:
if debug: if debug:
prints('Using http proxy', ans) prints('Using http proxy', str(ans))
return ans return ans
def browser(honor_time=True, max_time=2, mobile_browser=False): def browser(honor_time=True, max_time=2, mobile_browser=False):

View File

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

View File

@ -236,6 +236,10 @@ class MetadataWriterPlugin(Plugin):
type = _('Metadata writer') type = _('Metadata writer')
def __init__(self, *args, **kwargs):
Plugin.__init__(self, *args, **kwargs)
self.apply_null = False
def set_metadata(self, stream, mi, type): def set_metadata(self, stream, mi, type):
''' '''
Set metadata for the file represented by stream (a file like object Set metadata for the file represented by stream (a file like object

View File

@ -68,7 +68,7 @@ class PML2PMLZ(FileTypePlugin):
of = self.temporary_file('_plugin_pml2pmlz.pmlz') of = self.temporary_file('_plugin_pml2pmlz.pmlz')
pmlz = zipfile.ZipFile(of.name, 'w') pmlz = zipfile.ZipFile(of.name, 'w')
pmlz.write(pmlfile, os.path.basename(pmlfile)) pmlz.write(pmlfile, os.path.basename(pmlfile), zipfile.ZIP_DEFLATED)
pml_img = os.path.splitext(pmlfile)[0] + '_img' pml_img = os.path.splitext(pmlfile)[0] + '_img'
i_img = os.path.join(os.path.dirname(pmlfile),'images') i_img = os.path.join(os.path.dirname(pmlfile),'images')
@ -329,7 +329,7 @@ class EPUBMetadataWriter(MetadataWriterPlugin):
def set_metadata(self, stream, mi, type): def set_metadata(self, stream, mi, type):
from calibre.ebooks.metadata.epub import set_metadata from calibre.ebooks.metadata.epub import set_metadata
set_metadata(stream, mi) set_metadata(stream, mi, apply_null=self.apply_null)
class LRFMetadataWriter(MetadataWriterPlugin): class LRFMetadataWriter(MetadataWriterPlugin):
@ -450,11 +450,11 @@ from calibre.devices.eslick.driver import ESLICK
from calibre.devices.nuut2.driver import NUUT2 from calibre.devices.nuut2.driver import NUUT2
from calibre.devices.iriver.driver import IRIVER_STORY from calibre.devices.iriver.driver import IRIVER_STORY
from calibre.devices.binatone.driver import README from calibre.devices.binatone.driver import README
from calibre.devices.hanvon.driver import N516, EB511, ALEX from calibre.devices.hanvon.driver import N516, EB511, ALEX, AZBOOKA
from calibre.devices.edge.driver import EDGE from calibre.devices.edge.driver import EDGE
from calibre.devices.teclast.driver import TECLAST_K3 from calibre.devices.teclast.driver import TECLAST_K3
from calibre.devices.sne.driver import SNE from calibre.devices.sne.driver import SNE
from calibre.devices.misc import PALMPRE, KOBO from calibre.devices.misc import PALMPRE, KOBO, AVANT
from calibre.ebooks.metadata.fetch import GoogleBooks, ISBNDB, Amazon from calibre.ebooks.metadata.fetch import GoogleBooks, ISBNDB, Amazon
from calibre.library.catalog import CSV_XML, EPUB_MOBI from calibre.library.catalog import CSV_XML, EPUB_MOBI
@ -538,6 +538,8 @@ plugins += [
ALEX, ALEX,
PALMPRE, PALMPRE,
KOBO, KOBO,
AZBOOKA,
AVANT,
] ]
plugins += [x for x in list(locals().values()) if isinstance(x, type) and \ plugins += [x for x in list(locals().values()) if isinstance(x, type) and \
x.__name__.endswith('MetadataReader')] x.__name__.endswith('MetadataReader')]

View File

@ -187,6 +187,18 @@ class QuickMetadata(object):
quick_metadata = QuickMetadata() quick_metadata = QuickMetadata()
class ApplyNullMetadata(object):
def __init__(self):
self.apply_null = False
def __enter__(self):
self.apply_null = True
def __exit__(self, *args):
self.apply_null = False
apply_null_metadata = ApplyNullMetadata()
def get_file_type_metadata(stream, ftype): def get_file_type_metadata(stream, ftype):
mi = MetaInformation(None, None) mi = MetaInformation(None, None)
@ -214,6 +226,7 @@ def set_file_type_metadata(stream, mi, ftype):
if not is_disabled(plugin): if not is_disabled(plugin):
with plugin: with plugin:
try: try:
plugin.apply_null = apply_null_metadata.apply_null
plugin.set_metadata(stream, mi, ftype.lower().strip()) plugin.set_metadata(stream, mi, ftype.lower().strip())
break break
except: except:

View File

@ -34,9 +34,51 @@ Run an embedded python interpreter.
help='Add a simple plugin (i.e. a plugin that consists of only a ' help='Add a simple plugin (i.e. a plugin that consists of only a '
'.py file), by specifying the path to the py file containing the ' '.py file), by specifying the path to the py file containing the '
'plugin code.') 'plugin code.')
parser.add_option('--reinitialize-db', default=None,
help='Re-initialize the sqlite calibre database at the '
'specified path. Useful to recover from db corruption.')
return parser return parser
def reinit_db(dbpath, callback=None):
if not os.path.exists(dbpath):
raise ValueError(dbpath + ' does not exist')
from calibre.library.sqlite import connect
from contextlib import closing
import shutil
conn = connect(dbpath, False)
uv = conn.get('PRAGMA user_version;', all=False)
conn.execute('PRAGMA writable_schema=ON')
conn.commit()
sql_lines = conn.dump()
conn.close()
dest = dbpath + '.tmp'
try:
with closing(connect(dest, False)) as nconn:
nconn.execute('create temporary table temp_sequence(id INTEGER PRIMARY KEY AUTOINCREMENT)')
nconn.commit()
if callable(callback):
callback(len(sql_lines), True)
for i, line in enumerate(sql_lines):
try:
nconn.execute(line)
except:
import traceback
prints('SQL line %r failed with error:'%line)
prints(traceback.format_exc())
continue
finally:
if callable(callback):
callback(i, False)
nconn.execute('pragma user_version=%d'%int(uv))
nconn.commit()
os.remove(dbpath)
shutil.copyfile(dest, dbpath)
finally:
if os.path.exists(dest):
os.remove(dest)
prints('Database successfully re-initialized')
def migrate(old, new): def migrate(old, new):
from calibre.utils.config import prefs from calibre.utils.config import prefs
from calibre.library.database import LibraryDatabase from calibre.library.database import LibraryDatabase
@ -122,6 +164,8 @@ def main(args=sys.argv):
prints('CALIBRE_RESOURCES_PATH='+sys.resources_location) prints('CALIBRE_RESOURCES_PATH='+sys.resources_location)
prints('CALIBRE_EXTENSIONS_PATH='+sys.extensions_location) prints('CALIBRE_EXTENSIONS_PATH='+sys.extensions_location)
prints('CALIBRE_PYTHON_PATH='+os.pathsep.join(sys.path)) prints('CALIBRE_PYTHON_PATH='+os.pathsep.join(sys.path))
elif opts.reinitialize_db is not None:
reinit_db(opts.reinitialize_db)
else: else:
from calibre import ipython from calibre import ipython
ipython() ipython()

View File

@ -27,7 +27,7 @@ class ANDROID(USBMS):
0x18d1 : { 0x4e11 : [0x0100, 0x226], 0x4e12: [0x0100, 0x226]}, 0x18d1 : { 0x4e11 : [0x0100, 0x226], 0x4e12: [0x0100, 0x226]},
# Samsung # Samsung
0x04e8 : { 0x681d : [0x0222], 0x681c : [0x0222]}, 0x04e8 : { 0x681d : [0x0222], 0x681c : [0x0222, 0x0224]},
# Acer # Acer
0x502 : { 0x3203 : [0x0100]}, 0x502 : { 0x3203 : [0x0100]},
@ -38,9 +38,9 @@ class ANDROID(USBMS):
'be used') 'be used')
EXTRA_CUSTOMIZATION_DEFAULT = ', '.join(EBOOK_DIR_MAIN) EXTRA_CUSTOMIZATION_DEFAULT = ', '.join(EBOOK_DIR_MAIN)
VENDOR_NAME = ['HTC', 'MOTOROLA', 'GOOGLE_', 'ANDROID', 'ACER'] VENDOR_NAME = ['HTC', 'MOTOROLA', 'GOOGLE_', 'ANDROID', 'ACER', 'GT-I5700']
WINDOWS_MAIN_MEM = ['ANDROID_PHONE', 'A855', 'A853', 'INC.NEXUS_ONE', WINDOWS_MAIN_MEM = ['ANDROID_PHONE', 'A855', 'A853', 'INC.NEXUS_ONE',
'__UMS_COMPOSITE', '_MB200', 'MASS_STORAGE'] '__UMS_COMPOSITE', '_MB200', 'MASS_STORAGE', '_-_CARD']
OSX_MAIN_MEM = 'HTC Android Phone Media' OSX_MAIN_MEM = 'HTC Android Phone Media'

View File

@ -24,7 +24,7 @@ class N516(USBMS):
VENDOR_ID = [0x0525] VENDOR_ID = [0x0525]
PRODUCT_ID = [0xa4a5] PRODUCT_ID = [0xa4a5]
BCD = [0x323, 0x326] BCD = [0x323, 0x326, 0x399]
VENDOR_NAME = 'INGENIC' VENDOR_NAME = 'INGENIC'
WINDOWS_MAIN_MEM = '_FILE-STOR_GADGE' WINDOWS_MAIN_MEM = '_FILE-STOR_GADGE'
@ -50,6 +50,20 @@ class ALEX(N516):
EBOOK_DIR_MAIN = 'eBooks' EBOOK_DIR_MAIN = 'eBooks'
SUPPORTS_SUB_DIRS = True SUPPORTS_SUB_DIRS = True
class AZBOOKA(ALEX):
name = 'Azbooka driver'
gui_name = 'Azbooka'
description = _('Communicate with the Azbooka')
VENDOR_NAME = 'LINUX'
WINDOWS_MAIN_MEM = 'FILE-STOR_GADGET'
MAIN_MEMORY_VOLUME_LABEL = 'Azbooka Internal Memory'
EBOOK_DIR_MAIN = ''
class EB511(USBMS): class EB511(USBMS):
name = 'Elonex EB 511 driver' name = 'Elonex EB 511 driver'
gui_name = 'EB 511' gui_name = 'EB 511'

View File

@ -47,5 +47,25 @@ class KOBO(USBMS):
VENDOR_NAME = 'KOBO_INC' VENDOR_NAME = 'KOBO_INC'
WINDOWS_MAIN_MEM = '.KOBOEREADER' WINDOWS_MAIN_MEM = '.KOBOEREADER'
EBOOK_DIR_MAIN = 'e-books' EBOOK_DIR_MAIN = ''
class AVANT(USBMS):
name = 'Booq Avant Device Interface'
gui_name = 'Avant'
description = _('Communicate with the Booq Avant')
author = 'Kovid Goyal'
supported_platforms = ['windows', 'osx', 'linux']
# Ordered list of supported formats
FORMATS = ['epub', 'fb2', 'html', 'rtf', 'pdf', 'txt']
VENDOR_ID = [0x0525]
PRODUCT_ID = [0xa4a5]
BCD = [0x0319]
VENDOR_NAME = 'E-BOOK'
WINDOWS_MAIN_MEM = 'READER'
EBOOK_DIR_MAIN = ''
SUPPORTS_SUB_DIRS = True

View File

@ -284,7 +284,12 @@ class BookList(_BookList):
plitems = [] plitems = []
for pl in self.playlists(): for pl in self.playlists():
for c in pl.childNodes: for c in pl.childNodes:
if hasattr(c, 'tagName') and c.tagName.endswith('item'): if hasattr(c, 'tagName') and c.tagName.endswith('item') and \
hasattr(c, 'getAttribute'):
try:
c.getAttribute('id')
except: # Unlinked node
continue
plitems.append(c) plitems.append(c)
return plitems return plitems
@ -385,9 +390,9 @@ class BookList(_BookList):
continue continue
db_ids = [i.getAttribute('id') for i in pl.childNodes if hasattr(i, 'getAttribute')] db_ids = [i.getAttribute('id') for i in pl.childNodes if hasattr(i, 'getAttribute')]
pl_book_ids = [getattr(self.book_by_id(i), 'db_id', None) for i in db_ids] pl_book_ids = [getattr(self.book_by_id(i), 'db_id', None) for i in db_ids]
map = {} imap = {}
for i, j in zip(pl_book_ids, db_ids): for i, j in zip(pl_book_ids, db_ids):
map[i] = j imap[i] = j
pl_book_ids = [i for i in pl_book_ids if i is not None] pl_book_ids = [i for i in pl_book_ids if i is not None]
ordered_ids = [i for i in self.tag_order[title] if i in pl_book_ids] ordered_ids = [i for i in self.tag_order[title] if i in pl_book_ids]
@ -399,7 +404,7 @@ class BookList(_BookList):
child.unlink() child.unlink()
for id in ordered_ids: for id in ordered_ids:
item = self.document.createElement(self.prefix+'item') item = self.document.createElement(self.prefix+'item')
item.setAttribute('id', str(map[id])) item.setAttribute('id', str(imap[id]))
pl.appendChild(item) pl.appendChild(item)
def fix_ids(main, carda, cardb): def fix_ids(main, carda, cardb):

View File

@ -121,6 +121,14 @@ class PRS505(CLI, Device):
self.report_progress(1.0, _('Getting list of books on device...')) self.report_progress(1.0, _('Getting list of books on device...'))
return bl return bl
def filename_callback(self, fname, mi):
if getattr(mi, 'application_id', None) is not None:
base = fname.rpartition('.')[0]
suffix = '_%s'%mi.application_id
if not base.endswith(suffix):
fname = base + suffix + '.' + fname.rpartition('.')[-1]
return fname
def upload_books(self, files, names, on_card=None, end_session=True, def upload_books(self, files, names, on_card=None, end_session=True,
metadata=None): metadata=None):

View File

@ -825,7 +825,10 @@ class Device(DeviceConfig, DevicePlugin):
from calibre.library.save_to_disk import get_components from calibre.library.save_to_disk import get_components
if not isinstance(template, unicode): if not isinstance(template, unicode):
template = template.decode('utf-8') template = template.decode('utf-8')
extra_components = get_components(template, mdata, fname) app_id = str(getattr(mdata, 'application_id', ''))
# The SONY readers need to have the db id in the created filename
extra_components = get_components(template, mdata, fname,
length=250-len(app_id)-1)
if not extra_components: if not extra_components:
extra_components.append(sanitize(self.filename_callback(fname, extra_components.append(sanitize(self.filename_callback(fname,
mdata))) mdata)))

View File

@ -341,8 +341,15 @@ class ComicInput(InputFormatPlugin):
if not os.path.exists('comics.txt'): if not os.path.exists('comics.txt'):
raise ValueError('%s is not a valid comic collection' raise ValueError('%s is not a valid comic collection'
%stream.name) %stream.name)
raw = open('comics.txt', 'rb').read().decode('utf-8') raw = open('comics.txt', 'rb').read()
raw.lstrip(unicode(codecs.BOM_UTF8, "utf8" )) if raw.startswith(codecs.BOM_UTF16_BE):
raw = raw.decode('utf-16-be')[1:]
elif raw.startswith(codecs.BOM_UTF16_LE):
raw = raw.decode('utf-16-le')[1:]
elif raw.startswith(codecs.BOM_UTF8):
raw = raw.decode('utf-8')[1:]
else:
raw = raw.decode('utf-8')
for line in raw.splitlines(): for line in raw.splitlines():
line = line.strip() line = line.strip()
if not line: if not line:

View File

@ -32,9 +32,9 @@ class EPUBInput(InputFormatPlugin):
key = None key = None
for item in opf.identifier_iter(): for item in opf.identifier_iter():
scheme = None scheme = None
for key in item.attrib.keys(): for xkey in item.attrib.keys():
if key.endswith('scheme'): if xkey.endswith('scheme'):
scheme = item.get(key) scheme = item.get(xkey)
if (scheme and scheme.lower() == 'uuid') or \ if (scheme and scheme.lower() == 'uuid') or \
(item.text and item.text.startswith('urn:uuid:')): (item.text and item.text.startswith('urn:uuid:')):
key = str(item.text).rpartition(':')[-1] key = str(item.text).rpartition(':')[-1]

View File

@ -292,12 +292,12 @@ class HTMLInput(InputFormatPlugin):
encoding=opts.input_encoding) encoding=opts.input_encoding)
def is_case_sensitive(self, path): def is_case_sensitive(self, path):
if self._is_case_sensitive is not None: if getattr(self, '_is_case_sensitive', None) is not None:
return self._is_case_sensitive return self._is_case_sensitive
if not path or not os.path.exists(path): if not path or not os.path.exists(path):
return islinux or isfreebsd return islinux or isfreebsd
self._is_case_sensitive = os.path.exists(path.lower()) \ self._is_case_sensitive = not (os.path.exists(path.lower()) \
and os.path.exists(path.upper()) and os.path.exists(path.upper()))
return self._is_case_sensitive return self._is_case_sensitive
def create_oebbook(self, htmlpath, basedir, opts, log, mi): def create_oebbook(self, htmlpath, basedir, opts, log, mi):

View File

@ -19,12 +19,18 @@ AWS_NS = 'http://webservices.amazon.com/AWSECommerceService/2005-10-05'
def AWS(tag): def AWS(tag):
return '{%s}%s'%(AWS_NS, tag) return '{%s}%s'%(AWS_NS, tag)
def check_for_errors(root): class ISBNNotFound(ValueError):
pass
def check_for_errors(root, isbn):
err = root.find('.//'+AWS('Error')) err = root.find('.//'+AWS('Error'))
if err is not None: if err is not None:
text = etree.tostring(err, method='text', pretty_print=True,
encoding=unicode)
if 'AWS.InvalidParameterValue'+isbn in text:
raise ISBNNotFound(isbn)
raise Exception('Failed to get metadata with error: '\ raise Exception('Failed to get metadata with error: '\
+ etree.tostring(err, method='text', pretty_print=True, + text)
encoding=unicode))
def get_social_metadata(title, authors, publisher, isbn): def get_social_metadata(title, authors, publisher, isbn):
mi = MetaInformation(title, authors) mi = MetaInformation(title, authors)
@ -32,7 +38,10 @@ def get_social_metadata(title, authors, publisher, isbn):
br = browser() br = browser()
response_xml = br.open('http://status.calibre-ebook.com/aws/metadata/'+isbn).read() response_xml = br.open('http://status.calibre-ebook.com/aws/metadata/'+isbn).read()
root = etree.fromstring(response_xml) root = etree.fromstring(response_xml)
check_for_errors(root) try:
check_for_errors(root, isbn)
except ISBNNotFound:
return mi
mi.title = root.findtext('.//'+AWS('Title')) mi.title = root.findtext('.//'+AWS('Title'))
authors = [x.text for x in root.findall('.//'+AWS('Author'))] authors = [x.text for x in root.findall('.//'+AWS('Author'))]
if authors: if authors:

View File

@ -133,11 +133,11 @@ def get_cover(opf, opf_path, stream, reader=None):
stream.seek(0) stream.seek(0)
zf = ZipFile(stream) zf = ZipFile(stream)
if raster_cover: if raster_cover:
if reader is not None and \
reader.encryption_meta.is_encrypted(raster_cover):
return
base = posixpath.dirname(opf_path) base = posixpath.dirname(opf_path)
cpath = posixpath.normpath(posixpath.join(base, raster_cover)) cpath = posixpath.normpath(posixpath.join(base, raster_cover))
if reader is not None and \
reader.encryption_meta.is_encrypted(cpath):
return
try: try:
member = zf.getinfo(cpath) member = zf.getinfo(cpath)
except: except:
@ -182,13 +182,21 @@ def get_metadata(stream, extract_cover=True):
def get_quick_metadata(stream): def get_quick_metadata(stream):
return get_metadata(stream, False) return get_metadata(stream, False)
def set_metadata(stream, mi): def set_metadata(stream, mi, apply_null=False):
stream.seek(0) stream.seek(0)
reader = OCFZipReader(stream, root=os.getcwdu()) reader = OCFZipReader(stream, root=os.getcwdu())
mi = MetaInformation(mi) mi = MetaInformation(mi)
for x in ('guide', 'toc', 'manifest', 'spine'): for x in ('guide', 'toc', 'manifest', 'spine'):
setattr(mi, x, None) setattr(mi, x, None)
reader.opf.smart_update(mi) reader.opf.smart_update(mi)
if apply_null:
if not getattr(mi, 'series', None):
reader.opf.series = None
if not getattr(mi, 'tags', []):
reader.opf.tags = []
if not getattr(mi, 'isbn', None):
reader.opf.isbn = None
newopf = StringIO(reader.opf.render()) newopf = StringIO(reader.opf.render())
safe_replace(stream, reader.container[OPF.MIMETYPE], newopf) safe_replace(stream, reader.container[OPF.MIMETYPE], newopf)

View File

@ -404,6 +404,10 @@ class MetadataField(object):
def __set__(self, obj, val): def __set__(self, obj, val):
elem = obj.get_metadata_element(self.name) elem = obj.get_metadata_element(self.name)
if val is None:
if elem is not None:
elem.getparent().remove(elem)
return
if elem is None: if elem is None:
elem = obj.create_metadata_element(self.name, is_dc=self.is_dc) elem = obj.create_metadata_element(self.name, is_dc=self.is_dc)
obj.set_text(elem, unicode(val)) obj.set_text(elem, unicode(val))
@ -722,6 +726,11 @@ class OPF(object):
def fset(self, val): def fset(self, val):
matches = self.isbn_path(self.metadata) matches = self.isbn_path(self.metadata)
if val is None:
if matches:
for x in matches:
x.getparent().remove(x)
return
if not matches: if not matches:
attrib = {'{%s}scheme'%self.NAMESPACES['opf']: 'ISBN'} attrib = {'{%s}scheme'%self.NAMESPACES['opf']: 'ISBN'}
matches = [self.create_metadata_element('identifier', matches = [self.create_metadata_element('identifier',

View File

@ -235,6 +235,7 @@ def save_book(task, library_path, path, recs, notification=lambda x,y:x):
from calibre.library.database2 import LibraryDatabase2 from calibre.library.database2 import LibraryDatabase2
db = LibraryDatabase2(library_path) db = LibraryDatabase2(library_path)
from calibre.library.save_to_disk import config, save_to_disk from calibre.library.save_to_disk import config, save_to_disk
from calibre.customize.ui import apply_null_metadata
opts = config().parse() opts = config().parse()
for name in recs: for name in recs:
setattr(opts, name, recs[name]) setattr(opts, name, recs[name])
@ -244,5 +245,6 @@ def save_book(task, library_path, path, recs, notification=lambda x,y:x):
notification((id, title, not failed, tb)) notification((id, title, not failed, tb))
return True return True
save_to_disk(db, task, path, opts, callback) with apply_null_metadata:
save_to_disk(db, task, path, opts, callback)

View File

@ -92,10 +92,10 @@ class EbookIterator(object):
ext = re.sub(r'(x{0,1})htm(l{0,1})', 'html', ext) ext = re.sub(r'(x{0,1})htm(l{0,1})', 'html', ext)
self.ebook_ext = ext self.ebook_ext = ext
def search(self, text, index): def search(self, text, index, backwards=False):
text = text.lower() text = text.lower()
for i, path in enumerate(self.spine): for i, path in enumerate(self.spine):
if i > index: if (backwards and i < index) or (not backwards and i > index):
if text in open(path, 'rb').read().decode(path.encoding).lower(): if text in open(path, 'rb').read().decode(path.encoding).lower():
return i return i

View File

@ -176,6 +176,9 @@ class Stylizer(object):
class_sel_pat = re.compile(r'\.[a-z]+', re.IGNORECASE) class_sel_pat = re.compile(r'\.[a-z]+', re.IGNORECASE)
capital_sel_pat = re.compile(r'h|[A-Z]+') capital_sel_pat = re.compile(r'h|[A-Z]+')
for _, _, cssdict, text, _ in rules: for _, _, cssdict, text, _ in rules:
fl = ':first-letter' in text
if fl:
text = text.replace(':first-letter', '')
try: try:
selector = CSSSelector(text) selector = CSSSelector(text)
except (AssertionError, ExpressionError, etree.XPathSyntaxError, except (AssertionError, ExpressionError, etree.XPathSyntaxError,
@ -202,8 +205,21 @@ class Stylizer(object):
if found: if found:
self.logger.warn('Ignoring case mismatches for CSS selector: %s in %s' self.logger.warn('Ignoring case mismatches for CSS selector: %s in %s'
%(text, item.href)) %(text, item.href))
for elem in matches: if fl:
self.style(elem)._update_cssdict(cssdict) from lxml.builder import ElementMaker
E = ElementMaker(namespace=XHTML_NS)
for elem in matches:
for x in elem.iter():
if x.text:
span = E.span(x.text[0])
span.tail = x.text[1:]
x.text = None
x.insert(0, span)
self.style(span)._update_cssdict(cssdict)
break
else:
for elem in matches:
self.style(elem)._update_cssdict(cssdict)
for elem in xpath(tree, '//h:img[@width or @height]'): for elem in xpath(tree, '//h:img[@width or @height]'):
base = elem.get('style', '').strip() base = elem.get('style', '').strip()
if base: if base:

View File

@ -339,6 +339,7 @@ class FileIconProvider(QFileIconProvider):
'tan' : 'zero', 'tan' : 'zero',
'epub' : 'epub', 'epub' : 'epub',
'fb2' : 'fb2', 'fb2' : 'fb2',
'rtf' : 'rtf',
} }
def __init__(self): def __init__(self):
@ -410,6 +411,7 @@ class FileDialog(QObject):
modal = True, modal = True,
name = '', name = '',
mode = QFileDialog.ExistingFiles, mode = QFileDialog.ExistingFiles,
default_dir='~'
): ):
QObject.__init__(self) QObject.__init__(self)
ftext = '' ftext = ''
@ -428,9 +430,10 @@ class FileDialog(QObject):
self.selected_files = None self.selected_files = None
self.fd = None self.fd = None
initial_dir = dynamic.get(self.dialog_name, os.path.expanduser('~')) initial_dir = dynamic.get(self.dialog_name,
os.path.expanduser(default_dir))
if not isinstance(initial_dir, basestring): if not isinstance(initial_dir, basestring):
initial_dir = os.path.expanduser('~') initial_dir = os.path.expanduser(default_dir)
self.selected_files = [] self.selected_files = []
if mode == QFileDialog.AnyFile: if mode == QFileDialog.AnyFile:
f = unicode(QFileDialog.getSaveFileName(parent, title, initial_dir, ftext, "")) f = unicode(QFileDialog.getSaveFileName(parent, title, initial_dir, ftext, ""))
@ -465,9 +468,10 @@ class FileDialog(QObject):
return tuple(self.selected_files) return tuple(self.selected_files)
def choose_dir(window, name, title): def choose_dir(window, name, title, default_dir='~'):
fd = FileDialog(title, [], False, window, name=name, fd = FileDialog(title=title, filters=[], add_all_files_filter=False,
mode=QFileDialog.DirectoryOnly) parent=window, name=name, mode=QFileDialog.DirectoryOnly,
default_dir=default_dir)
dir = fd.get_files() dir = fd.get_files()
if dir: if dir:
return dir[0] return dir[0]

View File

@ -199,11 +199,11 @@ class DBAdder(Thread):
self.add_formats(id, formats) self.add_formats(id, formats)
else: else:
id = self.db.create_book_entry(mi, cover=cover, add_duplicates=False) id = self.db.create_book_entry(mi, cover=cover, add_duplicates=False)
self.number_of_books_added += 1
if id is None: if id is None:
self.duplicates.append((mi, cover, orig_formats)) self.duplicates.append((mi, cover, orig_formats))
else: else:
self.add_formats(id, formats) self.add_formats(id, formats)
self.number_of_books_added += 1
else: else:
self.names.append(name) self.names.append(name)
self.paths.append(formats[0]) self.paths.append(formats[0])

View File

@ -4,15 +4,18 @@ __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
import sys, os, time, socket, traceback import sys, os, time, socket, traceback
from functools import partial from functools import partial
from PyQt4.Qt import QCoreApplication, QIcon, QMessageBox from PyQt4.Qt import QCoreApplication, QIcon, QMessageBox, QObject, QTimer, \
QThread, pyqtSignal, Qt, QProgressDialog, QString
from calibre import prints from calibre import prints, plugins
from calibre.constants import iswindows, __appname__, isosx from calibre.constants import iswindows, __appname__, isosx, filesystem_encoding
from calibre.utils.ipc import ADDRESS, RC from calibre.utils.ipc import ADDRESS, RC
from calibre.gui2 import ORG_NAME, APP_UID, initialize_file_icon_provider, \ from calibre.gui2 import ORG_NAME, APP_UID, initialize_file_icon_provider, \
Application Application, choose_dir, error_dialog, question_dialog
from calibre.gui2.main_window import option_parser as _option_parser from calibre.gui2.main_window import option_parser as _option_parser
from calibre.utils.config import prefs, dynamic from calibre.utils.config import prefs, dynamic
from calibre.library.database2 import LibraryDatabase2
from calibre.library.sqlite import sqlite, DatabaseException
def option_parser(): def option_parser():
parser = _option_parser('''\ parser = _option_parser('''\
@ -48,25 +51,186 @@ def init_qt(args):
app.setWindowIcon(QIcon(I('library.png'))) app.setWindowIcon(QIcon(I('library.png')))
return app, opts, args, actions return app, opts, args, actions
def get_library_path():
library_path = prefs['library_path']
if library_path is None: # Need to migrate to new database layout
base = os.path.expanduser('~')
if iswindows:
base = plugins['winutil'][0].special_folder_path(
plugins['winutil'][0].CSIDL_PERSONAL)
if not base or not os.path.exists(base):
from PyQt4.Qt import QDir
base = unicode(QDir.homePath()).replace('/', os.sep)
candidate = choose_dir(None, 'choose calibre library',
_('Choose a location for your calibre e-book library'),
default_dir=base)
if not candidate:
candidate = os.path.join(base, 'Calibre Library')
library_path = os.path.abspath(candidate)
if not os.path.exists(library_path):
try:
os.makedirs(library_path)
except:
error_dialog(None, _('Failed to create library'),
_('Failed to create calibre library at: %r. Aborting.')%library_path,
det_msg=traceback.format_exc(), show=True)
library_path = None
return library_path
class DBRepair(QThread):
repair_done = pyqtSignal(object, object)
progress = pyqtSignal(object, object)
def __init__(self, library_path, parent, pd):
QThread.__init__(self, parent)
self.library_path = library_path
self.pd = pd
self.progress.connect(self._callback, type=Qt.QueuedConnection)
def _callback(self, num, is_length):
if is_length:
self.pd.setRange(0, num-1)
num = 0
self.pd.setValue(num)
def callback(self, num, is_length):
self.progress.emit(num, is_length)
def run(self):
from calibre.debug import reinit_db
try:
reinit_db(os.path.join(self.library_path, 'metadata.db'),
self.callback)
db = LibraryDatabase2(self.library_path)
tb = None
except:
db, tb = None, traceback.format_exc()
self.repair_done.emit(db, tb)
class GuiRunner(QObject):
'''Make sure an event loop is running before starting the main work of
initialization'''
def __init__(self, opts, args, actions, listener, app):
self.opts, self.args, self.listener, self.app = opts, args, listener, app
self.actions = actions
self.main = None
QObject.__init__(self)
self.timer = QTimer.singleShot(1, self.initialize)
def start_gui(self):
from calibre.gui2.ui import Main
main = Main(self.library_path, self.db, self.listener, self.opts, self.actions)
add_filesystem_book = partial(main.add_filesystem_book, allow_device=False)
sys.excepthook = main.unhandled_exception
if len(self.args) > 1:
p = os.path.abspath(self.args[1])
add_filesystem_book(p)
self.app.file_event_hook = add_filesystem_book
self.main = main
def initialization_failed(self):
print 'Catastrophic failure initializing GUI, bailing out...'
QCoreApplication.exit(1)
raise SystemExit(1)
def initialize_db_stage2(self, db, tb):
repair_pd = getattr(self, 'repair_pd', None)
if repair_pd is not None:
repair_pd.cancel()
if db is None and tb is not None:
# DB Repair failed
error_dialog(None, _('Repairing failed'),
_('The database repair failed. Starting with '
'a new empty library.'),
det_msg=tb, show=True)
if db is None:
fname = _('Calibre Library')
if isinstance(fname, unicode):
try:
fname = fname.encode(filesystem_encoding)
except:
fname = 'Calibre Library'
x = os.path.expanduser('~'+os.sep+fname)
if not os.path.exists(x):
try:
os.makedirs(x)
except:
x = os.path.expanduser('~')
candidate = choose_dir(None, 'choose calibre library',
_('Choose a location for your new calibre e-book library'),
default_dir=x)
if not candidate:
self.initialization_failed()
try:
self.library_path = candidate
db = LibraryDatabase2(candidate)
except:
error_dialog(None, _('Bad database location'),
_('Bad database location %r. calibre will now quit.'
)%self.library_path,
det_msg=traceback.format_exc(), show=True)
self.initialization_failed()
self.db = db
self.start_gui()
def initialize_db(self):
db = None
try:
db = LibraryDatabase2(self.library_path)
except (sqlite.Error, DatabaseException):
repair = question_dialog(None, _('Corrupted database'),
_('Your calibre database appears to be corrupted. Do '
'you want calibre to try and repair it automatically? '
'If you say No, a new empty calibre library will be created.'),
det_msg=traceback.format_exc()
)
if repair:
self.repair_pd = QProgressDialog(_('Repairing database. This '
'can take a very long time for a large collection'), QString(),
0, 0)
self.repair_pd.setWindowModality(Qt.WindowModal)
self.repair_pd.show()
self.repair = DBRepair(self.library_path, self, self.repair_pd)
self.repair.repair_done.connect(self.initialize_db_stage2,
type=Qt.QueuedConnection)
self.repair.start()
return
except:
error_dialog(None, _('Bad database location'),
_('Bad database location %r. Will start with '
' a new, empty calibre library')%self.library_path,
det_msg=traceback.format_exc(), show=True)
self.initialize_db_stage2(db, None)
def initialize(self, *args):
self.library_path = get_library_path()
if self.library_path is None:
self.initialization_failed()
self.initialize_db()
def run_gui(opts, args, actions, listener, app): def run_gui(opts, args, actions, listener, app):
from calibre.gui2.ui import Main
initialize_file_icon_provider() initialize_file_icon_provider()
if not dynamic.get('welcome_wizard_was_run', False): if not dynamic.get('welcome_wizard_was_run', False):
from calibre.gui2.wizard import wizard from calibre.gui2.wizard import wizard
wizard().exec_() wizard().exec_()
dynamic.set('welcome_wizard_was_run', True) dynamic.set('welcome_wizard_was_run', True)
main = Main(listener, opts, actions) runner = GuiRunner(opts, args, actions, listener, app)
add_filesystem_book = partial(main.add_filesystem_book, allow_device=False)
sys.excepthook = main.unhandled_exception
if len(args) > 1:
args[1] = os.path.abspath(args[1])
add_filesystem_book(args[1])
app.file_event_hook = add_filesystem_book
ret = app.exec_() ret = app.exec_()
if getattr(main, 'run_wizard_b4_shutdown', False): if getattr(runner.main, 'run_wizard_b4_shutdown', False):
from calibre.gui2.wizard import wizard from calibre.gui2.wizard import wizard
wizard().exec_() wizard().exec_()
if getattr(main, 'restart_after_quit', False): if getattr(runner.main, 'restart_after_quit', False):
e = sys.executable if getattr(sys, 'frozen', False) else sys.argv[0] e = sys.executable if getattr(sys, 'frozen', False) else sys.argv[0]
print 'Restarting with:', e, sys.argv print 'Restarting with:', e, sys.argv
if hasattr(sys, 'frameworks_dir'): if hasattr(sys, 'frameworks_dir'):
@ -78,7 +242,7 @@ def run_gui(opts, args, actions, listener, app):
else: else:
if iswindows: if iswindows:
try: try:
main.system_tray_icon.hide() runner.main.system_tray_icon.hide()
except: except:
pass pass
return ret return ret

View File

@ -4,7 +4,7 @@ __copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
import StringIO, traceback, sys import StringIO, traceback, sys
from PyQt4.Qt import QMainWindow, QString, Qt, QFont, QCoreApplication, SIGNAL,\ from PyQt4.Qt import QMainWindow, QString, Qt, QFont, QCoreApplication, SIGNAL,\
QAction, QMenu, QMenuBar, QIcon QAction, QMenu, QMenuBar, QIcon, pyqtSignal
from calibre.gui2.dialogs.conversion_error import ConversionErrorDialog from calibre.gui2.dialogs.conversion_error import ConversionErrorDialog
from calibre.utils.config import OptionParser from calibre.utils.config import OptionParser
from calibre.gui2 import error_dialog from calibre.gui2 import error_dialog
@ -41,6 +41,8 @@ class MainWindow(QMainWindow):
___menu = None ___menu = None
__actions = [] __actions = []
keyboard_interrupt = pyqtSignal()
@classmethod @classmethod
def create_application_menubar(cls): def create_application_menubar(cls):
mb = QMenuBar(None) mb = QMenuBar(None)
@ -76,6 +78,9 @@ class MainWindow(QMainWindow):
print 'Received signal:', repr(signal) print 'Received signal:', repr(signal)
def unhandled_exception(self, type, value, tb): def unhandled_exception(self, type, value, tb):
if type == KeyboardInterrupt:
self.keyboard_interrupt.emit()
return
try: try:
sio = StringIO.StringIO() sio = StringIO.StringIO()
traceback.print_exception(type, value, tb, file=sio) traceback.print_exception(type, value, tb, file=sio)

View File

@ -137,6 +137,13 @@ class SearchBox2(QComboBox):
if event.timerId() == self.timer: if event.timerId() == self.timer:
self.do_search() self.do_search()
@property
def smart_text(self):
text = unicode(self.currentText()).strip()
if not text or text == self.help_text:
return ''
return text
def do_search(self): def do_search(self):
text = unicode(self.currentText()).strip() text = unicode(self.currentText()).strip()
if not text or text == self.help_text: if not text or text == self.help_text:

View File

@ -14,9 +14,9 @@ from xml.parsers.expat import ExpatError
from Queue import Queue, Empty from Queue import Queue, Empty
from threading import Thread from threading import Thread
from functools import partial from functools import partial
from PyQt4.Qt import Qt, SIGNAL, QObject, QCoreApplication, QUrl, QTimer, \ from PyQt4.Qt import Qt, SIGNAL, QObject, QUrl, QTimer, \
QModelIndex, QPixmap, QColor, QPainter, QMenu, QIcon, \ QModelIndex, QPixmap, QColor, QPainter, QMenu, QIcon, \
QToolButton, QDialog, QDesktopServices, QFileDialog, \ QToolButton, QDialog, QDesktopServices, \
QSystemTrayIcon, QApplication, QKeySequence, QAction, \ QSystemTrayIcon, QApplication, QKeySequence, QAction, \
QMessageBox, QStackedLayout, QHelpEvent, QInputDialog,\ QMessageBox, QStackedLayout, QHelpEvent, QInputDialog,\
QThread, pyqtSignal QThread, pyqtSignal
@ -125,8 +125,9 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
self.default_thumbnail = (pixmap.width(), pixmap.height(), self.default_thumbnail = (pixmap.width(), pixmap.height(),
pixmap_to_data(pixmap)) pixmap_to_data(pixmap))
def __init__(self, listener, opts, actions, parent=None): def __init__(self, library_path, db, listener, opts, actions, parent=None):
self.preferences_action, self.quit_action = actions self.preferences_action, self.quit_action = actions
self.library_path = library_path
self.spare_servers = [] self.spare_servers = []
MainWindow.__init__(self, opts, parent) MainWindow.__init__(self, opts, parent)
# Initialize fontconfig in a separate thread as this can be a lengthy # Initialize fontconfig in a separate thread as this can be a lengthy
@ -395,6 +396,9 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
self.action_sync.setShortcut(Qt.Key_D) self.action_sync.setShortcut(Qt.Key_D)
self.action_sync.setEnabled(True) self.action_sync.setEnabled(True)
self.create_device_menu() self.create_device_menu()
self.connect(self.action_sync, SIGNAL('triggered(bool)'),
self._sync_action_triggered)
self.action_edit.setMenu(md) self.action_edit.setMenu(md)
self.action_save.setMenu(self.save_menu) self.action_save.setMenu(self.save_menu)
@ -513,31 +517,6 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
if self.system_tray_icon.isVisible() and opts.start_in_tray: if self.system_tray_icon.isVisible() and opts.start_in_tray:
self.hide_windows() self.hide_windows()
self.stack.setCurrentIndex(0) self.stack.setCurrentIndex(0)
try:
db = LibraryDatabase2(self.library_path)
except Exception:
import traceback
error_dialog(self, _('Bad database location'),
_('Bad database location')+':'+self.library_path,
det_msg=traceback.format_exc()).exec_()
fname = _('Calibre Library')
if isinstance(fname, unicode):
try:
fname = fname.encode(filesystem_encoding)
except:
fname = 'Calibre Library'
x = os.path.expanduser('~'+os.sep+fname)
if not os.path.exists(x):
os.makedirs(x)
dir = unicode(QFileDialog.getExistingDirectory(self,
_('Choose a location for your ebook library.'),
x))
if not dir:
QCoreApplication.exit(1)
raise SystemExit(1)
else:
self.library_path = dir
db = LibraryDatabase2(self.library_path)
self.library_view.set_database(db) self.library_view.set_database(db)
prefs['library_path'] = self.library_path prefs['library_path'] = self.library_path
self.library_view.sortByColumn(*dynamic.get('sort_column', self.library_view.sortByColumn(*dynamic.get('sort_column',
@ -658,11 +637,17 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
height = v.rowHeight(0) height = v.rowHeight(0)
self.library_view.verticalHeader().setDefaultSectionSize(height) self.library_view.verticalHeader().setDefaultSectionSize(height)
self.keyboard_interrupt.connect(self.quit, type=Qt.QueuedConnection)
def resizeEvent(self, ev): def resizeEvent(self, ev):
MainWindow.resizeEvent(self, ev) MainWindow.resizeEvent(self, ev)
self.search.setMaximumWidth(self.width()-150) self.search.setMaximumWidth(self.width()-150)
def _sync_action_triggered(self, *args):
m = getattr(self, '_sync_menu', None)
if m is not None:
m.trigger_default()
def create_device_menu(self): def create_device_menu(self):
self._sync_menu = DeviceMenu(self) self._sync_menu = DeviceMenu(self)
@ -670,8 +655,6 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
self.connect(self._sync_menu, self.connect(self._sync_menu,
SIGNAL('sync(PyQt_PyObject, PyQt_PyObject, PyQt_PyObject)'), SIGNAL('sync(PyQt_PyObject, PyQt_PyObject, PyQt_PyObject)'),
self.dispatch_sync_event) self.dispatch_sync_event)
self.connect(self.action_sync, SIGNAL('triggered(bool)'),
self._sync_menu.trigger_default)
self._sync_menu.fetch_annotations.connect(self.fetch_annotations) self._sync_menu.fetch_annotations.connect(self.fetch_annotations)
def add_spare_server(self, *args): def add_spare_server(self, *args):
@ -2330,38 +2313,7 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
d.show() d.show()
self._modeless_dialogs.append(d) self._modeless_dialogs.append(d)
def initialize_database(self):
self.library_path = prefs['library_path']
if self.library_path is None: # Need to migrate to new database layout
base = os.path.expanduser('~')
if iswindows:
from calibre import plugins
from PyQt4.Qt import QDir
base = plugins['winutil'][0].special_folder_path(
plugins['winutil'][0].CSIDL_PERSONAL)
if not base or not os.path.exists(base):
base = unicode(QDir.homePath()).replace('/', os.sep)
dir = unicode(QFileDialog.getExistingDirectory(self,
_('Choose a location for your ebook library.'), base))
if not dir:
dir = os.path.expanduser('~/Library')
self.library_path = os.path.abspath(dir)
if not os.path.exists(self.library_path):
try:
os.makedirs(self.library_path)
except:
self.library_path = os.path.expanduser('~/CalibreLibrary')
error_dialog(self, _('Invalid library location'),
_('Could not access %s. Using %s as the library.')%
(repr(self.library_path), repr(self.library_path))
).exec_()
if not os.path.exists(self.library_path):
os.makedirs(self.library_path)
def read_settings(self): def read_settings(self):
self.initialize_database()
geometry = config['main_window_geometry'] geometry = config['main_window_geometry']
if geometry is not None: if geometry is not None:
self.restoreGeometry(geometry) self.restoreGeometry(geometry)

View File

@ -384,25 +384,25 @@ class Document(QWebPage):
@property @property
def height(self): def height(self):
ans = self.javascript('document.body.offsetHeight', 'int') # contentsSize gives inaccurate results j = self.javascript('document.body.offsetHeight', 'int')
if ans == 0: q = self.mainFrame().contentsSize().height()
ans = self.mainFrame().contentsSize().height() if q == j:
return ans return j
if min(j, q) <= 0:
return max(j, q)
window_height = self.window_height
if j == window_height:
return j if q < 1.2*j else q
return j
@property @property
def width(self): def width(self):
return self.mainFrame().contentsSize().width() # offsetWidth gives inaccurate results return self.mainFrame().contentsSize().width() # offsetWidth gives inaccurate results
def set_bottom_padding(self, amount): def set_bottom_padding(self, amount):
body = self.mainFrame().documentElement().findFirst('body') s = QSize(-1, -1) if amount == 0 else QSize(self.width,
if body.isNull(): self.height+amount)
return self.setPreferredContentsSize(s)
old_padding = unicode(body.styleProperty('padding-bottom',
body.ComputedStyle)).strip()
padding = u'%dpx'%amount
if old_padding != padding:
body.setStyleProperty('padding-bottom', padding + ' !important')
class EntityDeclarationProcessor(object): class EntityDeclarationProcessor(object):
@ -585,7 +585,9 @@ class DocumentView(QWebView):
def fset(self, val): self.document.current_language = val def fset(self, val): self.document.current_language = val
return property(fget=fget, fset=fset) return property(fget=fget, fset=fset)
def search(self, text): def search(self, text, backwards=False):
if backwards:
return self.findText(text, self.document.FindBackwards)
return self.findText(text) return self.findText(text)
def path(self): def path(self):
@ -705,13 +707,21 @@ class DocumentView(QWebView):
def next_page(self): def next_page(self):
window_height = self.document.window_height window_height = self.document.window_height
document_height = self.document.height
ddelta = document_height - window_height
#print '\nWindow height:', window_height
#print 'Document height:', self.document.height
delta_y = window_height - 25 delta_y = window_height - 25
if self.document.at_bottom: if self.document.at_bottom or ddelta <= 0:
if self.manager is not None: if self.manager is not None:
self.manager.next_document() self.manager.next_document()
elif ddelta < 25:
self.scroll_by(y=ddelta)
return
else: else:
oopos = self.document.ypos oopos = self.document.ypos
#print '\nOriginal position:', oopos #print 'Original position:', oopos
self.document.set_bottom_padding(0) self.document.set_bottom_padding(0)
opos = self.document.ypos opos = self.document.ypos
#print 'After set padding=0:', self.document.ypos #print 'After set padding=0:', self.document.ypos
@ -722,8 +732,14 @@ class DocumentView(QWebView):
lower_limit = opos + delta_y # Max value of top y co-ord after scrolling lower_limit = opos + delta_y # Max value of top y co-ord after scrolling
max_y = self.document.height - window_height # The maximum possible top y co-ord max_y = self.document.height - window_height # The maximum possible top y co-ord
if max_y < lower_limit: if max_y < lower_limit:
padding = lower_limit - max_y
if padding == window_height:
if self.manager is not None:
self.manager.next_document()
return
#print 'Setting padding to:', lower_limit - max_y #print 'Setting padding to:', lower_limit - max_y
self.document.set_bottom_padding(lower_limit - max_y) self.document.set_bottom_padding(lower_limit - max_y)
#print 'Document height:', self.document.height
max_y = self.document.height - window_height max_y = self.document.height - window_height
lower_limit = min(max_y, lower_limit) lower_limit = min(max_y, lower_limit)
#print 'Scroll to:', lower_limit #print 'Scroll to:', lower_limit

View File

@ -229,7 +229,11 @@ class EbookViewer(MainWindow, Ui_EbookViewer):
self.connect(self.action_previous_page, SIGNAL('triggered(bool)'), self.connect(self.action_previous_page, SIGNAL('triggered(bool)'),
lambda x:self.view.previous_page()) lambda x:self.view.previous_page())
self.connect(self.action_find_next, SIGNAL('triggered(bool)'), self.connect(self.action_find_next, SIGNAL('triggered(bool)'),
lambda x:self.find(unicode(self.search.text()), True, repeat=True)) lambda x:self.find(self.search.smart_text, True, repeat=True))
self.connect(self.action_find_previous, SIGNAL('triggered(bool)'),
lambda x:self.find(self.search.smart_text, True,
repeat=True, backwards=True))
self.connect(self.action_full_screen, SIGNAL('triggered(bool)'), self.connect(self.action_full_screen, SIGNAL('triggered(bool)'),
self.toggle_fullscreen) self.toggle_fullscreen)
self.action_full_screen.setShortcuts([Qt.Key_F11, Qt.CTRL+Qt.SHIFT+Qt.Key_F]) self.action_full_screen.setShortcuts([Qt.Key_F11, Qt.CTRL+Qt.SHIFT+Qt.Key_F])
@ -420,13 +424,15 @@ class EbookViewer(MainWindow, Ui_EbookViewer):
self.set_bookmarks(self.iterator.bookmarks) self.set_bookmarks(self.iterator.bookmarks)
def find(self, text, refinement, repeat=False): def find(self, text, refinement, repeat=False, backwards=False):
if not text: if not text:
self.view.search('')
return self.search.search_done(False) return self.search.search_done(False)
if self.view.search(text): if self.view.search(text):
self.scrolled(self.view.scroll_fraction) self.scrolled(self.view.scroll_fraction)
return self.search.search_done(True) return self.search.search_done(True)
index = self.iterator.search(text, self.current_index) index = self.iterator.search(text, self.current_index,
backwards=backwards)
if index is None: if index is None:
if self.current_index > 0: if self.current_index > 0:
index = self.iterator.search(text, 0) index = self.iterator.search(text, 0)
@ -444,10 +450,7 @@ class EbookViewer(MainWindow, Ui_EbookViewer):
self.scrolled(self.view.scroll_fraction) self.scrolled(self.view.scroll_fraction)
def keyPressEvent(self, event): def keyPressEvent(self, event):
if event.key() == Qt.Key_F3: if event.key() == Qt.Key_Slash:
text = unicode(self.search.text())
self.find(text, True, repeat=True)
elif event.key() == Qt.Key_Slash:
self.search.setFocus(Qt.OtherFocusReason) self.search.setFocus(Qt.OtherFocusReason)
else: else:
return MainWindow.keyPressEvent(self, event) return MainWindow.keyPressEvent(self, event)

View File

@ -11,7 +11,7 @@
</rect> </rect>
</property> </property>
<property name="windowTitle"> <property name="windowTitle">
<string>Ebook Viewer</string> <string>E-book Viewer</string>
</property> </property>
<property name="windowIcon"> <property name="windowIcon">
<iconset resource="../../../../resources/images.qrc"> <iconset resource="../../../../resources/images.qrc">
@ -142,6 +142,7 @@
<bool>false</bool> <bool>false</bool>
</attribute> </attribute>
<addaction name="action_find_next"/> <addaction name="action_find_next"/>
<addaction name="action_find_previous"/>
</widget> </widget>
<action name="action_back"> <action name="action_back">
<property name="icon"> <property name="icon">
@ -232,6 +233,12 @@
<property name="text"> <property name="text">
<string>Find next</string> <string>Find next</string>
</property> </property>
<property name="toolTip">
<string>Find next occurrence</string>
</property>
<property name="shortcut">
<string>F3</string>
</property>
</action> </action>
<action name="action_copy"> <action name="action_copy">
<property name="icon"> <property name="icon">
@ -287,6 +294,21 @@
<string>Print</string> <string>Print</string>
</property> </property>
</action> </action>
<action name="action_find_previous">
<property name="icon">
<iconset resource="../../../../resources/images.qrc">
<normaloff>:/images/arrow-up.svg</normaloff>:/images/arrow-up.svg</iconset>
</property>
<property name="text">
<string>Find previous</string>
</property>
<property name="toolTip">
<string>Find previous occurrence</string>
</property>
<property name="shortcut">
<string>Shift+F3</string>
</property>
</action>
</widget> </widget>
<customwidgets> <customwidgets>
<customwidget> <customwidget>

View File

@ -97,9 +97,18 @@ class Kobo(Device):
manufacturer = 'Kobo' manufacturer = 'Kobo'
output_profile = 'kobo' output_profile = 'kobo'
output_format = 'EPUB' output_format = 'EPUB'
name = 'Kobo Reader'
id = 'kobo' id = 'kobo'
class Booq(Device):
name = 'Booq Reader'
manufacturer = 'Booq'
output_profile = 'prs505'
output_format = 'EPUB'
id = 'booq'
class Avant(Booq):
name = 'Booq Avant'
class Sony300(Sony505): class Sony300(Sony505):
name = 'SONY Reader Pocket Edition' name = 'SONY Reader Pocket Edition'

View File

@ -26,3 +26,8 @@ def server_config(defaults=None):
help=_('The maximum number of matches to return per OPDS query. ' help=_('The maximum number of matches to return per OPDS query. '
'This affects Stanza, WordPlayer, etc. integration.')) 'This affects Stanza, WordPlayer, etc. integration.'))
return c return c
def db():
from calibre.library.database2 import LibraryDatabase2
from calibre.utils.config import prefs
return LibraryDatabase2(prefs['library_path'])

View File

@ -376,12 +376,35 @@ the directory related options below.
help=_('Process directories recursively')) help=_('Process directories recursively'))
parser.add_option('-d', '--duplicates', action='store_true', default=False, parser.add_option('-d', '--duplicates', action='store_true', default=False,
help=_('Add books to database even if they already exist. Comparison is done based on book titles.')) help=_('Add books to database even if they already exist. Comparison is done based on book titles.'))
parser.add_option('-e', '--empty', action='store_true', default=False,
help=_('Add an empty book (a book with no formats)'))
parser.add_option('-t', '--title', default=None,
help=_('Set the title of the added empty book'))
parser.add_option('-a', '--authors', default=None,
help=_('Set the authors of the added empty book'))
parser.add_option('-i', '--isbn', default=None,
help=_('Set the ISBN of the added empty book'))
return parser return parser
def do_add_empty(db, title, authors, isbn):
from calibre.ebooks.metadata import MetaInformation, string_to_authors
mi = MetaInformation(None)
if title is not None:
mi.title = title
if authors:
mi.authors = string_to_authors(authors)
if isbn:
mi.isbn = isbn
db.import_book(mi, [])
send_message()
def command_add(args, dbpath): def command_add(args, dbpath):
parser = add_option_parser() parser = add_option_parser()
opts, args = parser.parse_args(sys.argv[:1] + args) opts, args = parser.parse_args(sys.argv[:1] + args)
if opts.empty:
do_add_empty(get_db(dbpath, opts), opts.title, opts.authors, opts.isbn)
return 0
if len(args) < 2: if len(args) < 2:
parser.print_help() parser.print_help()
print print

View File

@ -1399,7 +1399,7 @@ books_series_link feeds
def check_integrity(self, callback): def check_integrity(self, callback):
callback(0., _('Checking SQL integrity...')) callback(0., _('Checking SQL integrity...'))
user_version = self.user_version user_version = self.user_version
sql = self.conn.dump() sql = '\n'.join(self.conn.dump())
self.conn.close() self.conn.close()
dest = self.dbpath+'.tmp' dest = self.dbpath+'.tmp'
if os.path.exists(dest): if os.path.exists(dest):

View File

@ -0,0 +1,9 @@
#!/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'

View File

@ -0,0 +1,37 @@
#!/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'
''' Design documentation {{{
Storage paradigm {{{
* Agnostic to storage paradigm (i.e. no book per folder assumptions)
* Two separate concepts: A store and collection
A store is a backend, like a sqlite database associated with a path on
the local filesystem, or a cloud based storage solution.
A collection is a user defined group of stores. Most of the logic for
data manipulation sorting/searching/restrictions should be in the collection
class. The collection class should transparently handle the
conversion from store name + id to row number in the collection.
* Not sure how feasible it is to allow many-many maps between stores
and collections.
}}}
Event system {{{
* Comprehensive event system that other components can subscribe to
* Subscribers should be able to temporarily block receiving events
* Should event dispatch be asynchronous?
* Track last modified time for metadata and each format
}}}
}}}'''
# Imports {{{
# }}}

View File

@ -116,7 +116,7 @@ class DBThread(Thread):
break break
if func == 'dump': if func == 'dump':
try: try:
ok, res = True, '\n'.join(self.conn.iterdump()) ok, res = True, tuple(self.conn.iterdump())
except Exception, err: except Exception, err:
ok, res = False, (err, traceback.format_exc()) ok, res = False, (err, traceback.format_exc())
else: else:

View File

@ -313,7 +313,7 @@ class PostInstall:
with open(os.path.join(base, '95-calibre.rules'), 'wb') as udev: with open(os.path.join(base, '95-calibre.rules'), 'wb') as udev:
self.manifest.append(udev.name) self.manifest.append(udev.name)
udev.write('''# Sony Reader PRS-500\n''' udev.write('''# Sony Reader PRS-500\n'''
'''BUS=="usb", SYSFS{idProduct}=="029b", SYSFS{idVendor}=="054c", MODE="660", GROUP="%s"\n'''%(group,) '''SUBSYSTEMS=="usb", SYSFS{idProduct}=="029b", SYSFS{idVendor}=="054c", MODE="660", GROUP="%s"\n'''%(group,)
) )
except: except:
if self.opts.fatal_errors: if self.opts.fatal_errors:

View File

@ -533,3 +533,17 @@ The .cbc file will then contain::
|app| will automatically convert this .cbc file into a e-book with a Table of Contents pointing to each entry in comics.txt. |app| will automatically convert this .cbc file into a e-book with a Table of Contents pointing to each entry in comics.txt.
EPUB advanced formatting demo
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Various advanced formatting for EPUB files is demonstrated in this `demo file <http://calibre-ebook.com/downloads/demos/demo.epub>`_.
The file was created from hand coded HTML using calibre and is meant to be used as a template for your own EPUB creation efforts.
The source HTML it was created from is available `here <http://calibre-ebook.com/downloads/demos/demo.zip>`_. The settings used to create the
EPUB from the ZIP file are::
ebook-convert demo.zip .epub -vv --authors "Kovid Goyal" --language en --level1-toc '//*[@class="title"]' --disable-font-rescaling --page-breaks-before / --no-default-epub-cover
Note that because this file explores the potential of EPUB, most of the advanced formatting is not going to work on readers less capable than |app|'s builtin EPUB viewer.

View File

@ -149,10 +149,10 @@ Linux development environment
|app| is primarily developed on linux. You have two choices in setting up the development environment. You can install the |app| is primarily developed on linux. You have two choices in setting up the development environment. You can install the
|app| binary as normal and use that as a runtime environment to do your development. This approach is similar to that |app| binary as normal and use that as a runtime environment to do your development. This approach is similar to that
used in windows and linux. Alternatively, you can install |app| from source. Instructions for setting up a development used in windows and OS X. Alternatively, you can install |app| from source. Instructions for setting up a development
environment from source are in the INSTALL file in the source tree. Here we will address using the binary a runtime. environment from source are in the INSTALL file in the source tree. Here we will address using the binary a runtime.
Install the |app| using the binary installer. The opena terminal and change to the previously checked out |app| code directory, for example:: Install the |app| using the binary installer. Then open a terminal and change to the previously checked out |app| code directory, for example::
cd /home/kovid/work/calibre cd /home/kovid/work/calibre

View File

@ -125,7 +125,7 @@ With recent reader iterations, SONY, in all its wisdom has decided to try to for
use their software. If you install it, it auto-launches whenever you connect the reader. use their software. If you install it, it auto-launches whenever you connect the reader.
If you don't want to uninstall it altogether, there are a couple of tricks you can use. The If you don't want to uninstall it altogether, there are a couple of tricks you can use. The
simplest is to simply re-name the executable file that launches the library program. More detail simplest is to simply re-name the executable file that launches the library program. More detail
`here <http://www.mobileread.com/forums/showthread.php?t=65809>`_. `in the forums <http://www.mobileread.com/forums/showthread.php?t=65809>`_.
Can I use the collections feature of the SONY reader? Can I use the collections feature of the SONY reader?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@ -262,7 +262,7 @@ I want |app| to download news from my favorite news website.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
If you are reasonably proficient with computers, you can teach |app| to download news from any website of your choosing. To learn how to do this see :ref:`news`. If you are reasonably proficient with computers, you can teach |app| to download news from any website of your choosing. To learn how to do this see :ref:`news`.
Otherwise, you can register a request for a particular news site by adding a comment `here <http://bugs.calibre-ebook.com/ticket/405>`_. Otherwise, you can register a request for a particular news site by adding a comment `to this ticket <http://bugs.calibre-ebook.com/ticket/405>`_.
Can I use web2disk to download an arbitrary website? Can I use web2disk to download an arbitrary website?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@ -335,6 +335,6 @@ You have two choices:
Can I include |app| on a CD to be distributed with my product/magazine? Can I include |app| on a CD to be distributed with my product/magazine?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|app| is licensed under the GNU General Public License v3 (an open source license). This means that you are free to redistribute |app| as long as you make the source code available. So if you want to put |app| on a CD with your product, you must also put the |app| source code on the CD. The source code is available for download `here <http://code.google.com/p/calibre-ebook/downloads/list>`_. |app| is licensed under the GNU General Public License v3 (an open source license). This means that you are free to redistribute |app| as long as you make the source code available. So if you want to put |app| on a CD with your product, you must also put the |app| source code on the CD. The source code is available for download `from googlecode <http://code.google.com/p/calibre-ebook/downloads/list>`_.

File diff suppressed because it is too large Load Diff

View File

@ -1266,7 +1266,7 @@ class BasicNewsRecipe(Recipe):
feed = Feed() feed = Feed()
msg = 'Failed feed: %s'%(title if title else url) msg = 'Failed feed: %s'%(title if title else url)
feed.populate_from_preparsed_feed(msg, []) feed.populate_from_preparsed_feed(msg, [])
feed.description = unicode(err) feed.description = repr(err)
parsed_feeds.append(feed) parsed_feeds.append(feed)
self.log.exception(msg) self.log.exception(msg)