Sync to trunk.

This commit is contained in:
John Schember 2010-05-04 18:00:58 -04:00
commit e3aafa1789
83 changed files with 10751 additions and 7473 deletions

View File

@ -4,6 +4,104 @@
# 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.51
date: 2010-04-30
bug fixes:
- title: "Fix regression that broke EPUB output in 0.6.50 when converting lists"
- version: 0.6.50
date: 2010-04-30
new features:
- title: "Add merge book feature"
type: major
desc: >
"You can now merge multiple books into a single book, by clicking the arrow next to the edit meta information button.
Meta information from the books will be merged as well as individual book files in different formats"
- title: "Support for the Samsung Galaxy Spica and the Palm Pre"
- title: "Add a 'Go to' context menu to the ebook viewer"
tickets: [1230]
- title: "Show an asterisk next to version number when user is using CALIBRE_DEVELOP_FROM"
tickets: [5417]
- title: "Import ComicBookLover metadata from CBZ files"
tickets: [5402]
- title: "Add keyboard shortcut for viewing a specific format"
tickets: [5408]
- title: "EPUB Output: Add option to not use SVG for covers. Useful if you want to generate an EPUB for devices like the iPhone or JetBook Lite that don't support SVG covers"
tickets: [5409]
- title: "In the book info display area, only show series and tags if there are any. Move comments to the bottom."
tickets: [5391]
bug fixes:
- title: "E-book viewer: Use the Qt API to set document padding during next page operation, instead of javascript."
tickets: [5343]
- title: "E-book viewer: Handle self-closing heading tags in XHTML documents correctly."
tickets: [5413]
- title: "Conversion pipeline: Ignore CSS pseudo selectors"
tickets: [5337]
- title: "MOBI Input: Ignore form tags"
tickets: [5378]
- title: "Handle a scheduled custom recipe being deleted gracefully"
tickets: [5366]
- title: "ebook-convert: Don't rename PNG covers to JPG"
tickets: [5379]
- title: "Conversion pipeline: Respect width and height attributes in addition to width and height in CSS"
- title: "Fix regression which broke the use of HTML files in the regexp testing wizard"
tickets: [5341]
- title: "Fix match highlighting for multi-line regexps in the regexp testing wizard"
tickets: [5414]
- title: "EPUB Output: Workaround Adobe Digital Editions bug in rendering of lists with a left margin set."
tickets: [5415]
- title: "PRS 505/600/700/300 driver: Don't give an error message when editing metadata on SD card and cache directory does not exist"
tickets: [5410]
- title: "When converting EPUB to EPUB multiple times and creating book jacket from metadata, if an existing book jacket is found,
replace it. This will only work with EPUBs created with the current release onwards"
- title: "Correctly handle HTML in comments"
tickets: [5237]
- title: "Kindle driver: When transferring files whose names start witha period, replace the period."
tickets: [5367]
- title: "Conversion pipeline: When decoding XML (but not XHTML) if no encoding is specified, assume utf-8. Make entity conversion more robust. When splitting html handle ids with quotes in them correctly"
new recipes:
- title: The Old New Thing, Berlingske, ABC, Ultima Hora, China Daily, Dani
author: Darko Miletic
- title: Observa Digital
author: yrvn
- title: "Bill O'Reilly and Sean Hannity"
author: Rob Lammert
improved recipes:
- PC Magazine
- Reuters
- Sueddeutsche Zeitung
- "il Sole 24 Ore"
- La Repubblica
- version: 0.6.49 - version: 0.6.49
date: 2010-04-23 date: 2010-04-23

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 807 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 491 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 786 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 690 B

View File

@ -0,0 +1,49 @@
__license__ = 'GPL v3'
__copyright__ = '2010, Darko Miletic <darko.miletic at gmail.com>'
'''
abc.com.py
'''
from calibre.web.feeds.news import BasicNewsRecipe
class ABC_py(BasicNewsRecipe):
title = 'ABC digital'
__author__ = 'Darko Miletic'
description = 'Noticias de Paraguay y el resto del mundo'
publisher = 'ABC'
category = 'news, politics, Paraguay'
oldest_article = 2
max_articles_per_feed = 200
no_stylesheets = True
encoding = 'cp1252'
use_embedded_content = False
language = 'es_PY'
remove_empty_feeds = True
publication_type = 'newspaper'
extra_css = ' body{font-family: Arial,Helvetica,sans-serif } img{margin-bottom: 0.4em} '
conversion_options = {
'comment' : description
, 'tags' : category
, 'publisher' : publisher
, 'language' : language
}
remove_tags = [dict(name=['form','iframe','embed','object','link','base','table']),dict(attrs={'class':'toolbox'})]
remove_tags_after = dict(attrs={'class':'date'})
keep_only_tags = [dict(attrs={'class':'zcontent'})]
feeds = [
(u'Ultimo momento' , u'http://www.abc.com.py/ultimo-momento.xml' )
,(u'Nacionales' , u'http://www.abc.com.py/nacionales.xml' )
,(u'Internacionales' , u'http://www.abc.com.py/internacionales.xml' )
,(u'Deportes' , u'http://www.abc.com.py/deportes.xml' )
,(u'Espectaculos' , u'http://www.abc.com.py/espectaculos.xml' )
,(u'Ciencia y Tecnologia', u'http://www.abc.com.py/ciencia-y-tecnologia.xml')
]
def preprocess_html(self, soup):
for item in soup.findAll(style=True):
del item['style']
return soup

View File

@ -0,0 +1,59 @@
#!/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 - 02-05-2010'
'''
http://www.adnkronos.com/
'''
from calibre.web.feeds.news import BasicNewsRecipe
class Adnkronos(BasicNewsRecipe):
__author__ = 'Gabriele Marini'
description = 'News agency'
cover_url = 'http://www.adnkronos.com/IGN6/img/popup_ign.jpg'
title = u'Adnkronos'
publisher = 'Adnkronos Group - ews agency'
category = 'News, politics, culture, economy, general interest'
language = 'it'
timefmt = '[%a, %d %b, %Y]'
oldest_article = 7
max_articles_per_feed = 80
use_embedded_content = False
recursion = 10
remove_javascript = True
def get_article_url(self, article):
link = article.get('id', article.get('guid', None))
return link
extra_css = ' .newsAbstract{font-style: italic} '
keep_only_tags = [dict(name='div', attrs={'class':['breadCrumbs','newsTop','newsText']})
]
remove_tags = [
dict(name='div', attrs={'class':['leogoo','leogoo2']})
]
feeds = [
(u'Prima Pagina', u'http://rss.adnkronos.com/RSS_PrimaPagina.xml'),
(u'Ultima Ora', u'http://rss.adnkronos.com/RSS_Ultimora.xml'),
(u'Politica', u'http://rss.adnkronos.com/RSS_Politica.xml'),
(u'Esteri', u'http://rss.adnkronos.com/RSS_Esteri.xml'),
(u'Cronoca', u'http://rss.adnkronos.com/RSS_Cronaca.xml'),
(u'Economia', u'http://rss.adnkronos.com/RSS_Economia.xml'),
(u'Finanza', u'http://rss.adnkronos.com/RSS_Finanza.xml'),
(u'CyberNews', u'http://rss.adnkronos.com/RSS_CyberNews.xml'),
(u'Spettacolo', u'http://rss.adnkronos.com/RSS_Spettacolo.xml'),
(u'Cultura', u'http://rss.adnkronos.com/RSS_Cultura.xml'),
(u'Sport', u'http://rss.adnkronos.com/RSS_Sport.xml'),
(u'Sostenibilita', u'http://rss.adnkronos.com/RSS_Sostenibilita.xml'),
(u'Salute', u'http://rss.adnkronos.com/RSS_Salute.xml')
]

View File

@ -0,0 +1,49 @@
__license__ = 'GPL v3'
__copyright__ = '2010, Darko Miletic <darko.miletic at gmail.com>'
'''
berlingske.dk
'''
from calibre.web.feeds.news import BasicNewsRecipe
class Berlingske_dk(BasicNewsRecipe):
title = 'Berlingske Tidende'
__author__ = 'Darko Miletic'
description = 'News from Denmark'
publisher = 'berlingske.dk'
category = 'news, politics, Denmark'
oldest_article = 2
max_articles_per_feed = 100
no_stylesheets = True
remove_empty_feeds = True
use_embedded_content = False
publication_type = 'newspaper'
encoding = 'utf8'
language = 'da'
masthead_url = 'http://www.berlingske.dk/sites/all/themes/bm/img/layout/masthead_bg.gif'
extra_css = ' body{font-family: Arial,Helvetica,sans-serif } h1,.manchet,.byline{font-family: Cambria,Georgia,Times,"Times New Roman",serif } '
conversion_options = {
'comment' : description
, 'tags' : category
, 'publisher': publisher
, 'language' : language
}
feeds = [
(u'Breaking news' , u'http://www.berlingske.dk/breaking/rss' )
,(u'Seneste nyt' , u'http://www.berlingske.dk/seneste/rss' )
,(u'Topnyheder' , u'http://www.berlingske.dk/top/rss' )
,(u'Danmark' , u'http://www.berlingske.dk/danmark/seneste/rss' )
,(u'Verden' , u'http://www.berlingske.dk/verden/seneste/rss' )
,(u'Klima' , u'http://www.berlingske.dk/klima/seneste/rss' )
,(u'Debat' , u'http://www.berlingske.dk/debat/seneste/rss' )
,(u'Koebenhavn' , u'http://www.berlingske.dk/koebenhavn/seneste/rss')
,(u'Politik' , u'http://www.berlingske.dk/politik/seneste/rss' )
,(u'Kultur' , u'http://www.berlingske.dk/kultur/seneste/rss' )
]
keep_only_tags = [dict(attrs={'class':['first','pt-article']})]
remove_tags = [dict(name=['object','link','base','iframe','embed'])]

View File

@ -0,0 +1,48 @@
__license__ = 'GPL v3'
__copyright__ = '2010, Darko Miletic <darko.miletic at gmail.com>'
'''
www.chinadaily.com.cn
'''
from calibre.web.feeds.news import BasicNewsRecipe
class Pagina12(BasicNewsRecipe):
title = 'China Daily'
__author__ = 'Darko Miletic'
description = 'Chinadaily.com.cn is the largest English portal in China, providing news, business information, BBS, learning materials.'
publisher = 'China Daily Information Co.'
category = 'news, politics, China'
oldest_article = 2
max_articles_per_feed = 200
no_stylesheets = True
encoding = 'utf8'
use_embedded_content = False
language = 'en_CN'
remove_empty_feeds = True
publication_type = 'newsportal'
masthead_url = 'http://www.chinadaily.com.cn/15421.files/chinadailylogo_e_20100301.jpg'
extra_css = ' body{font-family: Arial,Helvetica,sans-serif } '
conversion_options = {
'comment' : description
, 'tags' : category
, 'publisher' : publisher
, 'language' : language
}
remove_tags = [dict(name=['object','embed','iframe','table'])]
keep_only_tags = [dict(attrs={'id':['Title_e','Content']})]
feeds = [
(u'China' , u'http://www.chinadaily.com.cn/rss/china_rss.xml' )
,(u'Business', u'http://www.chinadaily.com.cn/rss/bizchina_rss.xml')
,(u'World' , u'http://www.chinadaily.com.cn/rss/world_rss.xml' )
,(u'Sports' , u'http://www.chinadaily.com.cn/rss/sports_rss.xml' )
,(u'Opinions', u'http://www.chinadaily.com.cn/rss/opinion_rss.xml' )
]
def preprocess_html(self, soup):
for item in soup.findAll(style=True):
del item['style']
return soup

View File

@ -0,0 +1,57 @@
#!/usr/bin/env python
__license__ = 'GPL v3'
__author__ = '2010, Yuri Alvarez<me at yurialvarez.com>'
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
'''
observa.com.uy
'''
from calibre.web.feeds.news import BasicNewsRecipe
class ObservaDigital(BasicNewsRecipe):
title = 'Observa Digital'
__author__ = 'yrvn'
description = 'Noticias de Uruguay'
language = 'es'
timefmt = '[%a, %d %b, %Y]'
use_embedded_content = False
recursion = 5
encoding = 'utf8'
remove_javascript = True
no_stylesheets = True
oldest_article = 2
max_articles_per_feed = 100
keep_only_tags = [dict(id=['contenido'])]
remove_tags = [
dict(name='div', attrs={'id':'contenedorVinculadas'}),
dict(name='p', attrs={'id':'nota_firma'}),
dict(name=['object','link'])
]
extra_css = '''
h1{font-family:Geneva, Arial, Helvetica, sans-serif;color:#154B7A;}
h3{font-size: 14px;color:#999999; font-family:Geneva, Arial, Helvetica, sans-serif;font-weight: bold;}
h2{color:#666666; font-family:Geneva, Arial, Helvetica, sans-serif;font-size:small;}
p {font-family:Arial,Helvetica,sans-serif;}
'''
feeds = [
(u'Actualidad', u'http://www.observa.com.uy/RSS/actualidad.xml'),
(u'Deportes', u'http://www.observa.com.uy/RSS/deportes.xml'),
(u'Vida', u'http://www.observa.com.uy/RSS/vida.xml'),
(u'Ciencia y Tecnologia', u'http://www.observa.com.uy/RSS/ciencia.xml')
]
def get_cover_url(self):
index = 'http://www.observa.com.uy/'
soup = self.index_to_soup(index)
for image in soup.findAll('img',alt=True):
if image['alt'].startswith('Tapa El Observador'):
return image['src'].rstrip('b.jpg') + '.jpg'
return None
def preprocess_html(self, soup):
for item in soup.findAll(style=True):
del item['style']
return soup

View File

@ -0,0 +1,60 @@
#!/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 - 19-04-2010'
'''
http://www.ilgiornale.it/
'''
from calibre.ebooks.BeautifulSoup import BeautifulSoup
from calibre.web.feeds.news import BasicNewsRecipe
class IlGiornale(BasicNewsRecipe):
__author__ = 'Marini Gabriele'
description = 'Italian daily newspaper'
cover_url = 'http://www.ilgiornale.it/img_v1/logo.gif'
title = u'Il Giornale'
publisher = 'Il Giornale ON-LINE S.r.l.'
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
def get_article_url(self, article):
return article.get('guid', article.get('id', None))
def print_version(self, url):
raw = self.browser.open(url).read()
soup = BeautifulSoup(raw.decode('utf8', 'replace'))
all_print_tags = soup.find('div', {'style':'float:left; width:35%;'})
print_link = all_print_tags.contents[1]
if all_print_tags is None:
return url
return print_link['href']
feeds = [
(u'Ultime Notizie',u'http://www.ilgiornale.it/?RSS=S'),
(u'All\'Interno', u'http://www.ilgiornale.it/la_s.pic1?SID=8&RSS=S'),
(u'Esteri', u'http://www.ilgiornale.it/la_s.pic1?SID=6&RSS=S'),
(u'Economia', u'http://www.ilgiornale.it/la_s.pic1?SID=5&RSS=S'),
(u'Cultura', u'http://www.ilgiornale.it/la_s.pic1?SID=4&RSS=S'),
(u'Spettacoli', u'http://www.ilgiornale.it/la_s.pic1?SID=14&RSS=S'),
(u'Sport', u'http://www.ilgiornale.it/la_s.pic1?SID=15&RSS=S'),
(u'Tech&Web', u'http://www.ilgiornale.it/la_s.pic1?SID=35&RSS=S'),
(u'Edizione di Roma', u'http://www.ilgiornale.it/roma.pic1?SID=13&RSS=S'),
(u'Edizione di Milano', u'http://www.ilgiornale.it/milano.pic1?SID=9&RSS=S'),
(u'Edizione di Genova', u'http://www.ilgiornale.it/genova.pic1?SID=7&RSS=S')
]

View File

@ -0,0 +1,56 @@
#!/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.01 (04, January 2010)'
'''
http://www.messaggero.it/
'''
from calibre.web.feeds.news import BasicNewsRecipe
class IlMessaggero(BasicNewsRecipe):
__author__ = 'Gabriele Marini'
description = 'Italian News'
cover_url = 'http://www.ilmessaggero.it/img_tst/logomsgr.gif'
title = u'Il Messaggero'
publisher = 'Caltagirone Editore'
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 = 10
remove_javascript = True
keep_only_tags = [dict(name='h1', attrs={'class':'titoloLettura2'}),
dict(name='h2', attrs={'class':'sottotitLettura'}),
dict(name='span', attrs={'class':'testoArticoloG'})
]
feeds = [
(u'HomePage', u'http://www.ilmessaggero.it/rss/home.xml'),
(u'Primo Piano', u'http://www.ilmessaggero.it/rss/initalia_primopiano.xml'),
(u'Cronaca Bianca', u'http://www.ilmessaggero.it/rss/initalia_cronacabianca.xml'),
(u'Cronaca Nera', u'http://www.ilmessaggero.it/rss/initalia_cronacanera.xml'),
(u'Economia e Finanza', u'http://www.ilmessaggero.it/rss/economia.xml'),
(u'Politica', u'http://www.ilmessaggero.it/rss/initalia_politica.xml'),
(u'Scienza e Tecnologia', u'http://www.ilmessaggero.it/rss/scienza.xml'),
(u'Cinema', u'http://www.ilmessaggero.it/rss.php?refresh_ce#'),
(u'Viaggi', u'http://www.ilmessaggero.it/rss.php?refresh_ce#'),
(u'Roma', u'http://www.ilmessaggero.it/rss/roma.xml'),
(u'Cultura e Tendenze', u'http://www.ilmessaggero.it/rss/roma_culturaspet.xml'),
(u'Sport', u'http://www.ilmessaggero.it/rss/sport.xml'),
(u'Calcio', u'http://www.ilmessaggero.it/rss/sport_calcio.xml'),
(u'Motori', u'http://www.ilmessaggero.it/rss/sport_motori.xml')
]

View File

@ -11,12 +11,13 @@ http://www.ilsole24ore.com/
from calibre.web.feeds.news import BasicNewsRecipe from calibre.web.feeds.news import BasicNewsRecipe
class ilsole(BasicNewsRecipe): class ilsole24Ore(BasicNewsRecipe):
author = 'Lorenzo Vigentini & Edwin van Maastrigt' author = 'Lorenzo Vigentini & Edwin van Maastrigt'
description = 'Financial news daily paper' description = 'Financial news daily paper'
cover_url = 'http://www.ilsole24ore.com/img2009/header/t_logosole.gif' cover_url = 'http://www.ilsole24ore.com/img2007/print_header.gif'
title = u'il Sole 24 Ore '
title = u'il Sole 24 Ore New'
publisher = 'italiaNews' publisher = 'italiaNews'
category = 'News, finance, economy, politics' category = 'News, finance, economy, politics'
@ -35,12 +36,14 @@ class ilsole(BasicNewsRecipe):
def print_version(self, url): def print_version(self, url):
link, sep, params = url.rpartition('?') link, sep, params = url.rpartition('?')
if link is None:
return link.replace('_1.php', '_php')
return link.replace('.shtml', '_PRN.shtml') return link.replace('.shtml', '_PRN.shtml')
keep_only_tags = [ keep_only_tags = [
dict(name='div', attrs={'class':'txt'}) dict(name='div', attrs={'class':'txt'})
] ]
remove_tags = [dict(name='br')] # remove_tags = [dict(name='br')]
feeds = [ feeds = [
(u'Prima pagina', u'http://www.ilsole24ore.com/rss/primapagina.xml'), (u'Prima pagina', u'http://www.ilsole24ore.com/rss/primapagina.xml'),
@ -52,13 +55,14 @@ class ilsole(BasicNewsRecipe):
(u'Tecnologia e business', u'http://www.ilsole24ore.com/rss/tecnologia-business.xml'), (u'Tecnologia e business', u'http://www.ilsole24ore.com/rss/tecnologia-business.xml'),
(u'Cultura e tempo libero', u'http://www.ilsole24ore.com/rss/tempolibero-cultura.xml'), (u'Cultura e tempo libero', u'http://www.ilsole24ore.com/rss/tempolibero-cultura.xml'),
(u'Sport', u'http://www.ilsole24ore.com/rss/sport.xml'), (u'Sport', u'http://www.ilsole24ore.com/rss/sport.xml'),
(u'Professionisti 24', u'http://www.ilsole24ore.com/rss/prof_home.xml') (u'Professionisti 24', u'http://www.ilsole24ore.com/rss/prof_home.xml'),
(u'Ambiente e Sicurezza',u'http://www.ilsole24ore.com/rss/prof_as.xml')
] ]
extra_css = ''' extra_css = '''
html, body, table, tr, td, h1, h2, h3, h4, h5, h6, p, a, span, br, img {margin:0;padding:0;border:0;font-size:12px;font-family:Arial;} html, body, table, tr, td, h1, h2, h3, h4, h5, h6, p, a, span, br, img {margin:0;padding:0;border:0;font-size:12px;font-family:"Georgia","Times New Roman";}
.linkHighlight {color:#0292c6;} .linkHighlight {color:#0292c6;}
.txt {border-bottom:1px solid #7c7c7c;padding-bottom:20px;text-align:justify;} .txt {border-bottom:1px solid #7c7c7c;padding-bottom:20px};text-align:justify;font-family:"serif"}
.txt p {line-height:18px;} .txt p {line-height:18px;}
.txt span {line-height:22px;} .txt span {line-height:22px;}
.title h3 {color:#7b7b7b;} .title h3 {color:#7b7b7b;}

View File

@ -1,3 +1,4 @@
import re
from calibre.web.feeds.news import BasicNewsRecipe from calibre.web.feeds.news import BasicNewsRecipe
class JerusalemPost(BasicNewsRecipe): class JerusalemPost(BasicNewsRecipe):
@ -10,8 +11,6 @@ class JerusalemPost(BasicNewsRecipe):
__author__ = 'Kovid Goyal' __author__ = 'Kovid Goyal'
max_articles_per_feed = 10 max_articles_per_feed = 10
no_stylesheets = True no_stylesheets = True
remove_tags_before = {'class':'jp-grid-content'}
remove_tags_after = {'id':'body_val'}
feeds = [ ('Front Page', 'http://www.jpost.com/servlet/Satellite?pagename=JPost/Page/RSS&cid=1123495333346'), feeds = [ ('Front Page', 'http://www.jpost.com/servlet/Satellite?pagename=JPost/Page/RSS&cid=1123495333346'),
('Israel News', 'http://www.jpost.com/servlet/Satellite?pagename=JPost/Page/RSS&cid=1178443463156'), ('Israel News', 'http://www.jpost.com/servlet/Satellite?pagename=JPost/Page/RSS&cid=1178443463156'),
@ -20,9 +19,24 @@ class JerusalemPost(BasicNewsRecipe):
('Editorials', 'http://www.jpost.com/servlet/Satellite?pagename=JPost/Page/RSS&cid=1123495333211'), ('Editorials', 'http://www.jpost.com/servlet/Satellite?pagename=JPost/Page/RSS&cid=1123495333211'),
] ]
remove_tags = [
dict(id=lambda x: x and 'ads.' in x),
dict(attrs={'class':['printinfo', 'tt1']}),
dict(onclick='DoPrint()'),
dict(name='input'),
]
conversion_options = {'linearize_tables':True}
def preprocess_html(self, soup): def preprocess_html(self, soup):
for x in soup.findAll(name=['form', 'input']): for tag in soup.findAll('form'):
x.name = 'div' tag.name = 'div'
for x in soup.findAll('body', style=True):
del x['style']
return soup return soup
def print_version(self, url):
m = re.search(r'(ID|id)=(\d+)', url)
if m is not None:
id_ = m.group(2)
return 'http://www.jpost.com/LandedPages/PrintArticle.aspx?id=%s'%id_
return url

View File

@ -10,7 +10,7 @@ http://www.repubblica.it/
from calibre.web.feeds.news import BasicNewsRecipe from calibre.web.feeds.news import BasicNewsRecipe
class LaRepublica(BasicNewsRecipe): class LaRepubblica(BasicNewsRecipe):
author = 'Lorenzo Vigentini, based on Darko Miletic' author = 'Lorenzo Vigentini, based on Darko Miletic'
description = 'Italian daily newspaper' description = 'Italian daily newspaper'
@ -54,21 +54,24 @@ class LaRepublica(BasicNewsRecipe):
] ]
feeds = [ feeds = [
(u'Repubblica Rilievo', u'http://www.repubblica.it/rss/homepage/rss2.0.xml'), (u'Rilievo', u'http://www.repubblica.it/rss/homepage/rss2.0.xml'),
(u'Repubblica Cronaca', u'http://www.repubblica.it/rss/cronaca/rss2.0.xml'), (u'Cronaca', u'http://www.repubblica.it/rss/cronaca/rss2.0.xml'),
(u'Repubblica Esteri', u'http://www.repubblica.it/rss/esteri/rss2.0.xml'), (u'Esteri', u'http://www.repubblica.it/rss/esteri/rss2.0.xml'),
(u'Repubblica Economia', u'http://www.repubblica.it/rss/economia/rss2.0.xml'), (u'Economia', u'http://www.repubblica.it/rss/economia/rss2.0.xml'),
(u'Repubblica Politica', u'http://www.repubblica.it/rss/politica/rss2.0.xml'), (u'Politica', u'http://www.repubblica.it/rss/politica/rss2.0.xml'),
(u'Repubblica Scienze', u'http://www.repubblica.it/rss/scienze/rss2.0.xml'), (u'Scienze', u'http://www.repubblica.it/rss/scienze/rss2.0.xml'),
(u'Repubblica Tecnologia', u'http://www.repubblica.it/rss/tecnologia/rss2.0.xml'), (u'Tecnologia', u'http://www.repubblica.it/rss/tecnologia/rss2.0.xml'),
(u'Repubblica Scuola e Universita', u'http://www.repubblica.it/rss/scuola_e_universita/rss2.0.xml'), (u'Scuola e Universita', u'http://www.repubblica.it/rss/scuola_e_universita/rss2.0.xml'),
(u'Repubblica Ambiente', u'http://www.repubblica.it/rss/ambiente/rss2.0.xml'), (u'Ambiente', u'http://www.repubblica.it/rss/ambiente/rss2.0.xml'),
(u'Repubblica Cultura', u'http://www.repubblica.it/rss/spettacoli_e_cultura/rss2.0.xml'), (u'Cultura', u'http://www.repubblica.it/rss/spettacoli_e_cultura/rss2.0.xml'),
(u'Repubblica Persone', u'http://www.repubblica.it/rss/persone/rss2.0.xml'), (u'Persone', u'http://www.repubblica.it/rss/persone/rss2.0.xml'),
(u'Repubblica Sport', u'http://www.repubblica.it/rss/sport/rss2.0.xml'), (u'Sport', u'http://www.repubblica.it/rss/sport/rss2.0.xml'),
(u'Repubblica Calcio', u'http://www.repubblica.it/rss/sport/calcio/rss2.0.xml'), (u'Calcio', u'http://www.repubblica.it/rss/sport/calcio/rss2.0.xml'),
(u'Repubblica Motori', u'http://www.repubblica.it/rss/motori/rss2.0.xml'), (u'Motori', u'http://www.repubblica.it/rss/motori/rss2.0.xml'),
(u'Repubblica Roma', u'http://roma.repubblica.it/rss/rss2.0.xml'), (u'Edizione Roma', u'http://roma.repubblica.it/rss/rss2.0.xml'),
(u'Repubblica Torino', u'http://torino.repubblica.it/rss/rss2.0.xml') (u'Edizione Torino', u'http://torino.repubblica.it/rss/rss2.0.xml'),
(u'Edizione Milano', u'feed://milano.repubblica.it/rss/rss2.0.xml'),
(u'Edizione Napoli', u'feed://napoli.repubblica.it/rss/rss2.0.xml'),
(u'Edizione Palermo', u'feed://palermo.repubblica.it/rss/rss2.0.xml')
] ]

View File

@ -0,0 +1,34 @@
__license__ = 'GPL v3'
__copyright__ = '2010, Darko Miletic <darko.miletic at gmail.com>'
'''
blogs.msdn.com/oldnewthing
'''
from calibre.web.feeds.news import BasicNewsRecipe
class OldNewThing(BasicNewsRecipe):
title = 'The Old New Thing'
__author__ = 'Darko Miletic'
description = 'Famous blog by Windows guru Raymond Chen'
oldest_article = 15
max_articles_per_feed = 100
language = 'en'
encoding = 'utf-8'
no_stylesheets = True
use_embedded_content = False
publication_type = 'blog'
extra_css = ' body{font-family: Verdana,Arial,Helvetica,sans-serif} .code{font-family: "Lucida Console",monospace} '
conversion_options = {
'comment' : description
, 'tags' : 'blog, windows, microsoft, programming'
, 'publisher': 'Raymond Chen'
, 'language' : language
}
remove_attributes = ['width','height']
keep_only_tags = [dict(attrs={'class':['postsub','comment']})]
feeds = [(u'Posts', u'http://blogs.msdn.com/oldnewthing/rss.xml')]

View File

@ -0,0 +1,36 @@
#!/usr/bin/env python
__license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal <kovid at kovidgoyal.net>'
'''
bbc.co.uk
'''
from calibre.web.feeds.news import BasicNewsRecipe
class BBC(BasicNewsRecipe):
title = u'The Onion AV Club'
__author__ = 'Stephen Williams'
description = 'Film, Television and Music Reviews'
no_stylesheets = True
oldest_article = 2
max_articles_per_feed = 100
keep_only_tags = [dict(name='div', attrs={'id':'content'})
]
remove_tags = [dict(name='div', attrs={'class':['footer','tools_horizontal']}),
dict(name='div', attrs={'id':['tool_holder','elsewhere_on_avclub']})
]
extra_css = '.headline {font-size: x-large;} \n .fact { padding-top: 10pt }'
feeds = [
('Interviews', 'http://www.avclub.com/feed/interview/'),
('AV Club Daily', 'http://www.avclub.com/feed/daily'),
('Film', 'http://www.avclub.com/feed/film/'),
('Music', 'http://www.avclub.com/feed/music/'),
('DVD', 'http://www.avclub.com/feed/dvd/'),
('Books', 'http://www.avclub.com/feed/books/'),
('Games', 'http://www.avclub.com/feed/games/'),
('Interviews', 'http://www.avclub.com/feed/interview/'),
]

View File

@ -9,8 +9,9 @@ __description__ = 'PCMag (www.pcmag.com) delivers authoritative, labs-based comp
''' '''
http://www.pcmag.com/ http://www.pcmag.com/
''' '''
import re
from calibre.web.feeds.news import BasicNewsRecipe from calibre.web.feeds.news import BasicNewsRecipe
from calibre.ebooks.BeautifulSoup import Comment
class pcMag(BasicNewsRecipe): class pcMag(BasicNewsRecipe):
__author__ = 'Lorenzo Vigentini' __author__ = 'Lorenzo Vigentini'
@ -33,9 +34,6 @@ class pcMag(BasicNewsRecipe):
remove_javascript = True remove_javascript = True
no_stylesheets = True no_stylesheets = True
keep_only_tags = [
dict(name='div', attrs={'id':'articleContent'})
]
feeds = [ feeds = [
(u'Tech Commentary from the Editors of PC Magazine', u'http://rssnewsapps.ziffdavis.com/PCMAG_commentary.xml'), (u'Tech Commentary from the Editors of PC Magazine', u'http://rssnewsapps.ziffdavis.com/PCMAG_commentary.xml'),
@ -49,8 +47,13 @@ class pcMag(BasicNewsRecipe):
(u'Technology News from Ziff Davis', u'http://rssnewsapps.ziffdavis.com/pcmagbreakingnews.xml') (u'Technology News from Ziff Davis', u'http://rssnewsapps.ziffdavis.com/pcmagbreakingnews.xml')
] ]
keep_only_tags = [dict(attrs={'class':'content-page'})]
remove_tags = [ remove_tags = [
dict(name='div', attrs={'id':['microAd','intellitxt','articleDeckTalkback','inlineDigg','underArticleLinks','w_talkback']}), dict(attrs={'class':['control-side','comment','highlights_content','btn-holder','subscribe-panel',
dict(name='span', attrs={'id':['highlights_content','yahooBuzzBadge-48558872521263350499378']}) 'grey-box comments-box']}),
] dict(id=['inlineDigg']),
dict(text=lambda text:isinstance(text, Comment)),
dict(name='img', width='1'),
]
preprocess_regexps = [(re.compile(r"<img '[^']+?'"), lambda m : '<img ')]

View File

@ -7,14 +7,31 @@ class Reuters(BasicNewsRecipe):
title = 'Reuters' title = 'Reuters'
description = 'Global news' description = 'Global news'
__author__ = 'Kovid Goyal' __author__ = 'Kovid Goyal and Sujata Raman'
use_embedded_content = False use_embedded_content = False
language = 'en' language = 'en'
max_articles_per_feed = 10 max_articles_per_feed = 10
no_stylesheets = True
remove_javascript = True
extra_css = '''
preprocess_regexps = [ (re.compile(i[0], re.IGNORECASE | re.DOTALL), i[1]) for i in body{font-family:arial,helvetica,sans;}
h1{ font-size:larger ; font-weight:bold; }
.byline{color:#006E97;font-size:x-small; font-weight:bold;}
.location{font-size:x-small; font-weight:bold;}
.timestamp{font-size:x-small; }
'''
keep_only_tags = [dict(name='div', attrs={'class':'column2 gridPanel grid8'})]
remove_tags = [dict(name='div', attrs={'id':['recommendedArticles','relatedNews','relatedVideo','relatedFactboxes']}),
dict(name='p', attrs={'class':['relatedTopics']}),
dict(name='a', attrs={'id':['fullSizeLink']}),
dict(name='div', attrs={'class':['photoNav','relatedTopicButtons','articleComments','gridPanel grid8','footerHalf gridPanel grid1','gridPanel grid2','gridPanel grid3']}),]
preprocess_regexps = [ (re.compile(i[0], re.IGNORECASE | re.DOTALL), i[1]) for i in
[ [
##(r'<HEAD>.*?</HEAD>' , lambda match : '<HEAD></HEAD>'), ##(r'<HEAD>.*?</HEAD>' , lambda match : '<HEAD></HEAD>'),
(r'<div id="apple-rss-sidebar-background">.*?<!-- start Entries -->', lambda match : ''), (r'<div id="apple-rss-sidebar-background">.*?<!-- start Entries -->', lambda match : ''),
@ -24,10 +41,10 @@ class Reuters(BasicNewsRecipe):
(r'<h3>Share:</h3>.*?</body>', lambda match : '<!-- END:: Shared Module id=36615 --></body>'), (r'<h3>Share:</h3>.*?</body>', lambda match : '<!-- END:: Shared Module id=36615 --></body>'),
(r'<div id="atools" class="articleTools">.*?<div class="linebreak">', lambda match : '<div class="linebreak">'), (r'<div id="atools" class="articleTools">.*?<div class="linebreak">', lambda match : '<div class="linebreak">'),
] ]
] ]
feeds = [ ('Top Stories', 'http://feeds.reuters.com/reuters/topNews?format=xml'), feeds = [ ('Top Stories', 'http://feeds.reuters.com/reuters/topNews?format=xml'),
('US News', 'http://feeds.reuters.com/reuters/domesticNews?format=xml'), ('US News', 'http://feeds.reuters.com/reuters/domesticNews?format=xml'),
('World News', 'http://feeds.reuters.com/reuters/worldNews?format=xml'), ('World News', 'http://feeds.reuters.com/reuters/worldNews?format=xml'),
@ -37,6 +54,4 @@ class Reuters(BasicNewsRecipe):
('Technology News', 'http://feeds.reuters.com/reuters/technologyNews?format=xml'), ('Technology News', 'http://feeds.reuters.com/reuters/technologyNews?format=xml'),
('Oddly Enough News', 'http://feeds.reuters.com/reuters/oddlyEnoughNews?format=xml') ('Oddly Enough News', 'http://feeds.reuters.com/reuters/oddlyEnoughNews?format=xml')
] ]
def print_version(self, url):
return ('http://www.reuters.com/article/id' + url + '?sp=true')

View File

@ -5,9 +5,8 @@ __copyright__ = '2010, Darko Miletic <darko.miletic at gmail.com>'
www.sueddeutsche.de/sz/ www.sueddeutsche.de/sz/
''' '''
import urllib
from calibre import strftime
from calibre.web.feeds.news import BasicNewsRecipe from calibre.web.feeds.news import BasicNewsRecipe
from calibre import strftime
class SueddeutcheZeitung(BasicNewsRecipe): class SueddeutcheZeitung(BasicNewsRecipe):
title = 'Sueddeutche Zeitung' title = 'Sueddeutche Zeitung'
@ -20,12 +19,13 @@ class SueddeutcheZeitung(BasicNewsRecipe):
encoding = 'cp1252' encoding = 'cp1252'
needs_subscription = True needs_subscription = True
remove_empty_feeds = True remove_empty_feeds = True
delay = 2
PREFIX = 'http://www.sueddeutsche.de' PREFIX = 'http://www.sueddeutsche.de'
INDEX = PREFIX + strftime('/sz/%Y-%m-%d/') INDEX = PREFIX + '/app/epaper/textversion/'
LOGIN = PREFIX + '/app/lbox/index.html'
use_embedded_content = False use_embedded_content = False
masthead_url = 'http://pix.sueddeutsche.de/img/g_.gif' masthead_url = 'http://pix.sueddeutsche.de/img/layout/header/logo.gif'
language = 'de' language = 'de'
publication_type = 'newspaper'
extra_css = ' body{font-family: Arial,Helvetica,sans-serif} ' extra_css = ' body{font-family: Arial,Helvetica,sans-serif} '
conversion_options = { conversion_options = {
@ -40,49 +40,49 @@ class SueddeutcheZeitung(BasicNewsRecipe):
def get_browser(self): def get_browser(self):
br = BasicNewsRecipe.get_browser() br = BasicNewsRecipe.get_browser()
br.open(self.INDEX)
if self.username is not None and self.password is not None: if self.username is not None and self.password is not None:
data = urllib.urlencode({ 'login_name':self.username br.open(self.INDEX)
,'login_passwort':self.password br.select_form(name='lbox')
,'lboxaction':'doLogin' br['login_name' ] = self.username
,'passtxt':'Passwort' br['login_passwort'] = self.password
,'referer':self.INDEX br.submit()
,'x':'22'
,'y':'7'
})
br.open(self.LOGIN,data)
return br return br
remove_tags =[ remove_tags =[
dict(attrs={'class':'hidePrint'}) dict(attrs={'class':'hidePrint'})
,dict(name=['link','object','embed','base','iframe']) ,dict(name=['link','object','embed','base','iframe'])
] ]
remove_tags_before = dict(name='h2') keep_only_tags = [dict(attrs={'class':'artikelBox'})]
remove_tags_after = dict(attrs={'class':'author'}) remove_tags_before = dict(attrs={'class':'artikelTitel'})
remove_tags_after = dict(attrs={'class':'author'})
feeds = [ feeds = [
(u'Politik' , INDEX + 'politik/' ) (u'Politik' , INDEX + 'Politik/' )
,(u'Seite drei' , INDEX + 'seitedrei/' ) ,(u'Seite drei' , INDEX + 'Seite+drei/' )
,(u'Meinungsseite', INDEX + 'meinungsseite/') ,(u'Meinungsseite', INDEX + 'Meinungsseite/')
,(u'Wissen' , INDEX + 'wissen/' ) ,(u'Wissen' , INDEX + 'Wissen/' )
,(u'Panorama' , INDEX + 'panorama/' ) ,(u'Panorama' , INDEX + 'Panorama/' )
,(u'Feuilleton' , INDEX + 'feuilleton/' ) ,(u'Feuilleton' , INDEX + 'Feuilleton/' )
,(u'Medien' , INDEX + 'medien/' ) ,(u'Medien' , INDEX + 'Medien/' )
,(u'Wirtschaft' , INDEX + 'wirtschaft/' ) ,(u'Wirtschaft' , INDEX + 'Wirtschaft/' )
,(u'Sport' , INDEX + 'sport/' ) ,(u'Sport' , INDEX + 'Sport/' )
,(u'Bayern' , INDEX + 'bayern/' ) ,(u'Bayern' , INDEX + 'Bayern/' )
,(u'Muenchen' , INDEX + 'muenchen/' ) ,(u'Muenchen' , INDEX + 'M%FCnchen/' )
,(u'jetzt.de' , INDEX + 'jetzt.de/' )
] ]
def parse_index(self): def parse_index(self):
src = self.index_to_soup(self.INDEX)
id = ''
for itt in src.findAll('a',href=True):
if itt['href'].startswith('/app/epaper/textversion/inhalt/'):
id = itt['href'].rpartition('/inhalt/')[2]
totalfeeds = [] totalfeeds = []
lfeeds = self.get_feeds() lfeeds = self.get_feeds()
for feedobj in lfeeds: for feedobj in lfeeds:
feedtitle, feedurl = feedobj feedtitle, feedurl = feedobj
self.report_progress(0, _('Fetching feed')+' %s...'%(feedtitle if feedtitle else feedurl)) self.report_progress(0, _('Fetching feed')+' %s...'%(feedtitle if feedtitle else feedurl))
articles = [] articles = []
soup = self.index_to_soup(feedurl) soup = self.index_to_soup(feedurl + id)
tbl = soup.find(attrs={'class':'szprintd'}) tbl = soup.find(attrs={'class':'szprintd'})
for item in tbl.findAll(name='td',attrs={'class':'topthema'}): for item in tbl.findAll(name='td',attrs={'class':'topthema'}):
atag = item.find(attrs={'class':'Titel'}).a atag = item.find(attrs={'class':'Titel'}).a
@ -101,7 +101,3 @@ class SueddeutcheZeitung(BasicNewsRecipe):
}) })
totalfeeds.append((feedtitle, articles)) totalfeeds.append((feedtitle, articles))
return totalfeeds return totalfeeds
def print_version(self, url):
return url + 'print.html'

View File

@ -0,0 +1,52 @@
__license__ = 'GPL v3'
__copyright__ = '2010, Darko Miletic <darko.miletic at gmail.com>'
'''
ultimahora.com
'''
from calibre.web.feeds.news import BasicNewsRecipe
class UltimaHora_py(BasicNewsRecipe):
title = 'Ultima Hora'
__author__ = 'Darko Miletic'
description = 'Noticias de Paraguay y el resto del mundo'
publisher = 'EDITORIAL EL PAIS S.A.'
category = 'news, politics, Paraguay'
oldest_article = 2
max_articles_per_feed = 200
no_stylesheets = True
encoding = 'cp1252'
use_embedded_content = False
language = 'es_PY'
remove_empty_feeds = True
publication_type = 'newspaper'
masthead_url = 'http://www.ultimahora.com/imgs/uh-com.gif'
extra_css = ' body{font-family: Arial,Helvetica,sans-serif } img{margin-bottom: 0.4em} .sub_titulo_mediano,.TituloNota{font-family: Georgia,"Times New Roman",Times,serif} .sub_titulo_mediano{font-weight: bold} '
conversion_options = {
'comment' : description
, 'tags' : category
, 'publisher' : publisher
, 'language' : language
}
remove_tags = [dict(name=['form','iframe','embed','object','link','base','table'])]
keep_only_tags = [dict(attrs={'id':['nota_titulo','nota_copete','texto']})]
feeds = [
(u'Arte y Espectaculos' , u'http://www.ultimahora.com/adjuntos/rss/UHEspectaculos.xml' )
,(u'Ciudad del Este' , u'http://www.ultimahora.com/adjuntos/rss/UHCDE.xml' )
,(u'Deportes' , u'http://www.ultimahora.com/adjuntos/rss/UHDeportes.xml' )
,(u'Ultimo momento' , u'http://www.ultimahora.com/adjuntos/rss/UltimoMomento.xml' )
,(u'Nacionales' , u'http://www.ultimahora.com/adjuntos/rss/uh-rss-nacionales.xml' )
,(u'Politica' , u'http://www.ultimahora.com/adjuntos/rss/uh-rss-politica.xml' )
,(u'Sucesos' , u'http://www.ultimahora.com/adjuntos/rss/uh-rss-sucesos.xml' )
,(u'Economia' , u'http://www.ultimahora.com/adjuntos/rss/uh-rss-economia.xml' )
,(u'Ciencia y Tecnologia', u'http://www.ultimahora.com/adjuntos/rss/uh-rss-ciencia.xml' )
]
def preprocess_html(self, soup):
for item in soup.findAll(style=True):
del item['style']
return soup

View File

@ -153,7 +153,7 @@ class LinuxFreeze(Command):
sys.resources_location = os.path.join(DIR_NAME, 'resources') sys.resources_location = os.path.join(DIR_NAME, 'resources')
dfv = os.environ.get('CALIBRE_DEVELOP_FROM', None) dfv = os.environ.get('CALIBRE_DEVELOP_FROM', None)
if dfv and os.path.exists(dfv): if dfv and os.path.exists(dfv):
sys.path.insert(0, dfv) sys.path.insert(0, os.path.abspath(dfv))
executables = %(executables)s executables = %(executables)s

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.49' __version__ = '0.6.51'
__author__ = "Kovid Goyal <kovid@kovidgoyal.net>" __author__ = "Kovid Goyal <kovid@kovidgoyal.net>"
import re import re
@ -56,9 +56,18 @@ if plugins is None:
plugin_path = sys.extensions_location plugin_path = sys.extensions_location
sys.path.insert(0, plugin_path) sys.path.insert(0, plugin_path)
for plugin in ['pictureflow', 'lzx', 'msdes', 'podofo', 'cPalmdoc', for plugin in [
'fontconfig', 'pdfreflow', 'progress_indicator', 'chmlib', 'pictureflow',
'chm_extra'] + \ 'lzx',
'msdes',
'podofo',
'cPalmdoc',
'fontconfig',
'pdfreflow',
'progress_indicator',
'chmlib',
'chm_extra'
] + \
(['winutil'] if iswindows else []) + \ (['winutil'] if iswindows else []) + \
(['usbobserver'] if isosx else []): (['usbobserver'] if isosx else []):
try: try:

View File

@ -7,7 +7,7 @@ import os
import glob import glob
from calibre.customize import FileTypePlugin, MetadataReaderPlugin, MetadataWriterPlugin from calibre.customize import FileTypePlugin, MetadataReaderPlugin, MetadataWriterPlugin
from calibre.constants import numeric_version from calibre.constants import numeric_version
from calibre.ebooks.metadata.archive import ArchiveExtract from calibre.ebooks.metadata.archive import ArchiveExtract, get_cbz_metadata
class HTML2ZIP(FileTypePlugin): class HTML2ZIP(FileTypePlugin):
name = 'HTML to ZIP' name = 'HTML to ZIP'
@ -97,6 +97,12 @@ class ComicMetadataReader(MetadataReaderPlugin):
from calibre.ebooks.metadata import MetaInformation from calibre.ebooks.metadata import MetaInformation
ret = extract_first(stream) ret = extract_first(stream)
mi = MetaInformation(None, None) mi = MetaInformation(None, None)
stream.seek(0)
if ftype == 'cbz':
try:
mi.smart_update(get_cbz_metadata(stream))
except:
pass
if ret is not None: if ret is not None:
path, data = ret path, data = ret
ext = os.path.splitext(path)[1][1:] ext = os.path.splitext(path)[1][1:]
@ -448,7 +454,7 @@ from calibre.devices.hanvon.driver import N516, EB511, ALEX
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 from calibre.devices.misc import PALMPRE, KOBO
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
@ -530,7 +536,8 @@ plugins += [
EDGE, EDGE,
SNE, SNE,
ALEX, ALEX,
PALMPRE PALMPRE,
KOBO,
] ]
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

@ -4,6 +4,7 @@ __copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
from itertools import izip from itertools import izip
from xml.sax.saxutils import escape
from calibre.customize import Plugin as _Plugin from calibre.customize import Plugin as _Plugin
@ -238,14 +239,14 @@ class OutputProfile(Plugin):
@classmethod @classmethod
def tags_to_string(cls, tags): def tags_to_string(cls, tags):
return ', '.join(tags) return escape(', '.join(tags))
class iPadOutput(OutputProfile): class iPadOutput(OutputProfile):
name = 'iPad' name = 'iPad'
short_name = 'ipad' short_name = 'ipad'
screen_size = (1024, 768) screen_size = (768, 1024)
comic_screen_size = (1024, 768) comic_screen_size = (768, 1024)
dpi = 132.0 dpi = 132.0
class SonyReaderOutput(OutputProfile): class SonyReaderOutput(OutputProfile):
@ -383,7 +384,8 @@ class KindleOutput(OutputProfile):
@classmethod @classmethod
def tags_to_string(cls, tags): def tags_to_string(cls, tags):
return 'ttt '.join(tags)+'ttt ' return u'%s <br/><span style="color: white">%s</span>' % (', '.join(tags),
'ttt '.join(tags)+'ttt ')
class KindleDXOutput(OutputProfile): class KindleDXOutput(OutputProfile):
@ -399,7 +401,8 @@ class KindleDXOutput(OutputProfile):
@classmethod @classmethod
def tags_to_string(cls, tags): def tags_to_string(cls, tags):
return 'ttt '.join(tags)+'ttt ' return u'%s <br/><span style="color: white">%s</span>' % (', '.join(tags),
'ttt '.join(tags)+'ttt ')
class IlliadOutput(OutputProfile): class IlliadOutput(OutputProfile):

View File

@ -18,7 +18,8 @@ class ANDROID(USBMS):
FORMATS = ['epub', 'pdf'] FORMATS = ['epub', 'pdf']
VENDOR_ID = { VENDOR_ID = {
0x0bb4 : { 0x0c02 : [0x100], 0x0c01 : [0x100]}, # HTC
0x0bb4 : { 0x0c02 : [0x100], 0x0c01 : [0x100], 0x0ff9 : [0x0100]},
# Motorola # Motorola
0x22b8 : { 0x41d9 : [0x216], 0x2d67 : [0x100], 0x41db : [0x216]}, 0x22b8 : { 0x41d9 : [0x216], 0x2d67 : [0x100], 0x41db : [0x216]},
@ -26,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]}, 0x04e8 : { 0x681d : [0x0222], 0x681c : [0x0222]},
# Acer # Acer
0x502 : { 0x3203 : [0x0100]}, 0x502 : { 0x3203 : [0x0100]},

View File

@ -62,7 +62,7 @@ class KINDLE(USBMS):
def filename_callback(self, fname, mi): def filename_callback(self, fname, mi):
if fname.startswith('.'): if fname.startswith('.'):
return fname[1:] return 'x'+fname[1:]
return fname return fname
def get_annotations(self, path_map): def get_annotations(self, path_map):

View File

@ -28,3 +28,24 @@ class PALMPRE(USBMS):
EBOOK_DIR_MAIN = 'E-books' EBOOK_DIR_MAIN = 'E-books'
class KOBO(USBMS):
name = 'Kobo Reader Device Interface'
gui_name = 'Kobo Reader'
description = _('Communicate with the Kobo Reader')
author = 'Kovid Goyal'
supported_platforms = ['windows', 'osx', 'linux']
# Ordered list of supported formats
FORMATS = ['epub', 'pdf']
VENDOR_ID = [0x2237]
PRODUCT_ID = [0x4161]
BCD = [0x0110]
VENDOR_NAME = 'KOBO_INC'
WINDOWS_MAIN_MEM = '.KOBOEREADER'
EBOOK_DIR_MAIN = 'e-books'

View File

@ -69,13 +69,15 @@ class PRS505(CLI, Device):
def write_cache(prefix): def write_cache(prefix):
try: try:
cachep = os.path.join(prefix, self.CACHE_XML) cachep = os.path.join(prefix, *(self.CACHE_XML.split('/')))
if not os.path.exists(cachep): if not os.path.exists(cachep):
try: dname = os.path.dirname(cachep)
os.makedirs(os.path.dirname(cachep), mode=0777) if not os.path.exists(dname):
except: try:
time.sleep(5) os.makedirs(dname, mode=0777)
os.makedirs(os.path.dirname(cachep), mode=0777) except:
time.sleep(5)
os.makedirs(dname, mode=0777)
with open(cachep, 'wb') as f: with open(cachep, 'wb') as f:
f.write(u'''<?xml version="1.0" encoding="UTF-8"?> f.write(u'''<?xml version="1.0" encoding="UTF-8"?>
<cache xmlns="http://www.kinoma.com/FskCache/1"> <cache xmlns="http://www.kinoma.com/FskCache/1">
@ -202,9 +204,11 @@ class PRS505(CLI, Device):
def write_card_prefix(prefix, listid): def write_card_prefix(prefix, listid):
if prefix is not None and hasattr(booklists[listid], 'write'): if prefix is not None and hasattr(booklists[listid], 'write'):
if not os.path.exists(prefix): tgt = os.path.join(prefix, *(self.CACHE_XML.split('/')))
os.makedirs(prefix) base = os.path.dirname(tgt)
with open(prefix + self.__class__.CACHE_XML, 'wb') as f: if not os.path.exists(base):
os.makedirs(base)
with open(tgt, 'wb') as f:
booklists[listid].write(f) booklists[listid].write(f)
write_card_prefix(self._card_a_prefix, 1) write_card_prefix(self._card_a_prefix, 1)
write_card_prefix(self._card_b_prefix, 2) write_card_prefix(self._card_b_prefix, 2)

View File

@ -25,6 +25,7 @@
#include <stdio.h> #include <stdio.h>
#include <CoreFoundation/CFNumber.h> #include <CoreFoundation/CFNumber.h>
#include <CoreServices/CoreServices.h>
#include <IOKit/usb/IOUSBLib.h> #include <IOKit/usb/IOUSBLib.h>
#include <IOKit/IOCFPlugIn.h> #include <IOKit/IOCFPlugIn.h>
#include <IOKit/IOKitLib.h> #include <IOKit/IOKitLib.h>
@ -52,6 +53,28 @@
#define NUKE(x) Py_XDECREF(x); x = NULL; #define NUKE(x) Py_XDECREF(x); x = NULL;
/* This function only works on 10.5 and later
static PyObject* send2trash(PyObject *self, PyObject *args)
{
UInt8 *utf8_chars;
FSRef fp;
OSStatus op_result;
if (!PyArg_ParseTuple(args, "es", "utf-8", &utf8_chars)) {
return NULL;
}
FSPathMakeRefWithOptions(utf8_chars, kFSPathMakeRefDoNotFollowLeafSymlink, &fp, NULL);
op_result = FSMoveObjectToTrashSync(&fp, NULL, kFSFileOperationDefaultOptions);
PyMem_Free(utf8_chars);
if (op_result != noErr) {
PyErr_SetString(PyExc_OSError, GetMacOSStatusCommentString(op_result));
return NULL;
}
Py_RETURN_NONE;
}
*/
static PyObject* static PyObject*
usbobserver_get_iokit_string_property(io_service_t dev, CFStringRef prop) { usbobserver_get_iokit_string_property(io_service_t dev, CFStringRef prop) {
CFTypeRef PropRef; CFTypeRef PropRef;

View File

@ -676,7 +676,10 @@ OptionRecommendation(name='timestamp',
if mi.cover: if mi.cover:
if mi.cover.startswith('http:') or mi.cover.startswith('https:'): if mi.cover.startswith('http:') or mi.cover.startswith('https:'):
mi.cover = self.download_cover(mi.cover) mi.cover = self.download_cover(mi.cover)
mi.cover_data = ('', open(mi.cover, 'rb').read()) ext = mi.cover.rpartition('.')[-1].lower().strip()
if ext not in ('png', 'jpg', 'jpeg'):
ext = 'jpg'
mi.cover_data = (ext, open(mi.cover, 'rb').read())
mi.cover = None mi.cover = None
self.user_metadata = mi self.user_metadata = mi

View File

@ -81,12 +81,40 @@ class EPUBOutput(OutputFormatPlugin):
OptionRecommendation(name='no_default_epub_cover', recommended_value=False, OptionRecommendation(name='no_default_epub_cover', recommended_value=False,
help=_('Normally, if the input file has no cover and you don\'t' help=_('Normally, if the input file has no cover and you don\'t'
' specify one, a default cover is generated with the title, ' ' specify one, a default cover is generated with the title, '
'authors, etc. This option disables the generation of this cover.')), 'authors, etc. This option disables the generation of this cover.')
),
OptionRecommendation(name='no_svg_cover', recommended_value=False,
help=_('Do not use SVG for the book cover. Use this option if '
'your EPUB is going to be used ona device that does not '
'support SVG, like the iPhone or the JetBook Lite. '
'Without this option, such devices will display the cover '
'as a blank page.')
),
]) ])
recommendations = set([('pretty_print', True, OptionRecommendation.HIGH)]) recommendations = set([('pretty_print', True, OptionRecommendation.HIGH)])
NONSVG_TITLEPAGE_COVER = '''\
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta name="calibre:cover" content="true" />
<title>Cover</title>
<style type="text/css" title="override_css">
@page {padding: 0pt; margin:0pt}
body { text-align: center; padding:0pt; margin: 0pt; }
div { padding:0pt; margin: 0pt; }
</style>
</head>
<body>
<div>
<img src="%s" alt="cover" style="height: 100%%" />
</div>
</body>
</html>
'''
TITLEPAGE_COVER = '''\ TITLEPAGE_COVER = '''\
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en"> <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
@ -301,7 +329,9 @@ class EPUBOutput(OutputFormatPlugin):
else: else:
href = self.default_cover() href = self.default_cover()
if href is not None: if href is not None:
tp = self.TITLEPAGE_COVER%unquote(href) templ = self.NONSVG_TITLEPAGE_COVER if self.opts.no_svg_cover \
else self.TITLEPAGE_COVER
tp = templ%unquote(href)
id, href = m.generate('titlepage', 'titlepage.xhtml') id, href = m.generate('titlepage', 'titlepage.xhtml')
item = m.add(id, href, guess_type('t.xhtml')[0], item = m.add(id, href, guess_type('t.xhtml')[0],
data=etree.fromstring(tp)) data=etree.fromstring(tp))
@ -334,6 +364,12 @@ class EPUBOutput(OutputFormatPlugin):
''' '''
from calibre.ebooks.oeb.base import XPath, XHTML, OEB_STYLES, barename, urlunquote from calibre.ebooks.oeb.base import XPath, XHTML, OEB_STYLES, barename, urlunquote
stylesheet = None
for item in self.oeb.manifest:
if item.media_type.lower() in OEB_STYLES:
stylesheet = item
break
# ADE cries big wet tears when it encounters an invalid fragment # ADE cries big wet tears when it encounters an invalid fragment
# identifier in the NCX toc. # identifier in the NCX toc.
frag_pat = re.compile(r'[-A-Za-z0-9_:.]+$') frag_pat = re.compile(r'[-A-Za-z0-9_:.]+$')
@ -430,11 +466,17 @@ class EPUBOutput(OutputFormatPlugin):
elem.tail = special_chars.sub('', elem.tail) elem.tail = special_chars.sub('', elem.tail)
elem.tail = elem.tail.replace(u'\u2011', '-') elem.tail = elem.tail.replace(u'\u2011', '-')
stylesheet = None if stylesheet is not None:
for item in self.oeb.manifest: # ADE doesn't render lists correctly if they have left margins
if item.media_type.lower() in OEB_STYLES: from cssutils.css import CSSRule
stylesheet = item for lb in XPath('//h:ul[@class]|//h:ol[@class]')(root):
break sel = '.'+lb.get('class')
for rule in stylesheet.data.cssRules.rulesOfType(CSSRule.STYLE_RULE):
if sel == rule.selectorList.selectorText:
val = rule.style.removeProperty('margin-left')
pval = rule.style.getProperty('padding-left')
if val and not pval:
rule.style.setProperty('padding-left', val)
if stylesheet is not None: if stylesheet is not None:
stylesheet.data.add('a { color: inherit; text-decoration: inherit; ' stylesheet.data.add('a { color: inherit; text-decoration: inherit; '

View File

@ -11,7 +11,7 @@ __docformat__ = 'restructuredtext en'
Input plugin for HTML or OPF ebooks. Input plugin for HTML or OPF ebooks.
''' '''
import os, re, sys, uuid import os, re, sys, uuid, tempfile
from urlparse import urlparse, urlunparse from urlparse import urlparse, urlunparse
from urllib import unquote from urllib import unquote
from functools import partial from functools import partial
@ -272,6 +272,7 @@ class HTMLInput(InputFormatPlugin):
def convert(self, stream, opts, file_ext, log, def convert(self, stream, opts, file_ext, log,
accelerators): accelerators):
self._is_case_sensitive = None
basedir = os.getcwd() basedir = os.getcwd()
self.opts = opts self.opts = opts
@ -290,6 +291,15 @@ class HTMLInput(InputFormatPlugin):
return create_oebbook(log, stream.name, opts, self, return create_oebbook(log, stream.name, opts, self,
encoding=opts.input_encoding) encoding=opts.input_encoding)
def is_case_sensitive(self, path):
if self._is_case_sensitive is not None:
return self._is_case_sensitive
if not path or not os.path.exists(path):
return islinux or isfreebsd
self._is_case_sensitive = os.path.exists(path.lower()) \
and os.path.exists(path.upper())
return self._is_case_sensitive
def create_oebbook(self, htmlpath, basedir, opts, log, mi): def create_oebbook(self, htmlpath, basedir, opts, log, mi):
from calibre.ebooks.conversion.plumber import create_oebbook from calibre.ebooks.conversion.plumber import create_oebbook
from calibre.ebooks.oeb.base import DirContainer, \ from calibre.ebooks.oeb.base import DirContainer, \
@ -320,7 +330,6 @@ class HTMLInput(InputFormatPlugin):
if not metadata.title: if not metadata.title:
oeb.logger.warn('Title not specified') oeb.logger.warn('Title not specified')
metadata.add('title', self.oeb.translate(__('Unknown'))) metadata.add('title', self.oeb.translate(__('Unknown')))
bookid = str(uuid.uuid4()) bookid = str(uuid.uuid4())
metadata.add('identifier', bookid, id='uuid_id', scheme='uuid') metadata.add('identifier', bookid, id='uuid_id', scheme='uuid')
for ident in metadata.identifier: for ident in metadata.identifier:
@ -328,7 +337,6 @@ class HTMLInput(InputFormatPlugin):
self.oeb.uid = metadata.identifier[0] self.oeb.uid = metadata.identifier[0]
break break
filelist = get_filelist(htmlpath, basedir, opts, log) filelist = get_filelist(htmlpath, basedir, opts, log)
filelist = [f for f in filelist if not f.is_binary] filelist = [f for f in filelist if not f.is_binary]
htmlfile_map = {} htmlfile_map = {}
@ -345,14 +353,16 @@ class HTMLInput(InputFormatPlugin):
self.added_resources = {} self.added_resources = {}
self.log = log self.log = log
self.log('Normalizing filename cases')
for path, href in htmlfile_map.items(): for path, href in htmlfile_map.items():
if not (islinux or isfreebsd): if not self.is_case_sensitive(path):
path = path.lower() path = path.lower()
self.added_resources[path] = href self.added_resources[path] = href
self.urlnormalize, self.DirContainer = urlnormalize, DirContainer self.urlnormalize, self.DirContainer = urlnormalize, DirContainer
self.urldefrag = urldefrag self.urldefrag = urldefrag
self.guess_type, self.BINARY_MIME = guess_type, BINARY_MIME self.guess_type, self.BINARY_MIME = guess_type, BINARY_MIME
self.log('Rewriting HTML links')
for f in filelist: for f in filelist:
path = f.path path = f.path
dpath = os.path.dirname(path) dpath = os.path.dirname(path)
@ -417,7 +427,7 @@ class HTMLInput(InputFormatPlugin):
if os.path.isdir(link): if os.path.isdir(link):
self.log.warn(link_, 'is a link to a directory. Ignoring.') self.log.warn(link_, 'is a link to a directory. Ignoring.')
return link_ return link_
if not (islinux or isfreebsd): if not self.is_case_sensitive(tempfile.gettempdir()):
link = link.lower() link = link.lower()
if link not in self.added_resources: if link not in self.added_resources:
bhref = os.path.basename(link) bhref = os.path.basename(link)

View File

@ -64,3 +64,45 @@ class ArchiveExtract(FileTypePlugin):
of.write(zf.read(fname)) of.write(zf.read(fname))
return of.name return of.name
def get_comic_book_info(d, mi):
series = d.get('series', '')
if series.strip():
mi.series = series
if d.get('volume', -1) > -1:
mi.series_index = float(d['volume'])
if d.get('rating', -1) > -1:
mi.rating = d['rating']
for x in ('title', 'publisher'):
y = d.get(x, '').strip()
if y:
setattr(mi, x, y)
tags = d.get('tags', [])
if tags:
mi.tags = tags
authors = []
for credit in d.get('credits', []):
if credit.get('role', '') in ('Writer', 'Artist', 'Cartoonist',
'Creator'):
x = credit.get('person', '')
if x:
x = ' '.join((reversed(x.split(', '))))
authors.append(x)
if authors:
mi.authors = authors
def get_cbz_metadata(stream):
from calibre.utils.zipfile import ZipFile
from calibre.ebooks.metadata import MetaInformation
import json
zf = ZipFile(stream)
mi = MetaInformation(None, None)
if zf.comment:
m = json.loads(zf.comment)
if hasattr(m, 'keys'):
for cat in m.keys():
if cat.startswith('ComicBookInfo'):
get_comic_book_info(m[cat], mi)
return mi

View File

@ -61,7 +61,8 @@ class EXTHHeader(object):
# last update time # last update time
pass pass
elif id == 503: # Long title elif id == 503: # Long title
if not title or title == _('Unknown'): if not title or title == _('Unknown') or \
'USER_CONTENT' in title or title.startswith('dtp_'):
try: try:
title = content.decode(codec) title = content.decode(codec)
except: except:
@ -253,6 +254,8 @@ class MobiReader(object):
stream = open(filename_or_stream, 'rb') stream = open(filename_or_stream, 'rb')
raw = stream.read() raw = stream.read()
if raw.startswith('TPZ'):
raise ValueError(_('This is an Amazon Topaz book. It cannot be processed.'))
self.header = raw[0:72] self.header = raw[0:72]
self.name = self.header[:32].replace('\x00', '') self.name = self.header[:32].replace('\x00', '')
@ -260,7 +263,7 @@ class MobiReader(object):
self.ident = self.header[0x3C:0x3C + 8].upper() self.ident = self.header[0x3C:0x3C + 8].upper()
if self.ident not in ['BOOKMOBI', 'TEXTREAD']: if self.ident not in ['BOOKMOBI', 'TEXTREAD']:
raise MobiError('Unknown book type: %s' % self.ident) raise MobiError('Unknown book type: %s' % repr(self.ident))
self.sections = [] self.sections = []
self.section_headers = [] self.section_headers = []
@ -497,8 +500,8 @@ class MobiReader(object):
if ':' in x: if ':' in x:
del tag.attrib[x] del tag.attrib[x]
if tag.tag in ('country-region', 'place', 'placetype', 'placename', if tag.tag in ('country-region', 'place', 'placetype', 'placename',
'state', 'city', 'street', 'address', 'content'): 'state', 'city', 'street', 'address', 'content', 'form'):
tag.tag = 'div' if tag.tag == 'content' else 'span' tag.tag = 'div' if tag.tag in ('content', 'form') else 'span'
for key in tag.attrib.keys(): for key in tag.attrib.keys():
tag.attrib.pop(key) tag.attrib.pop(key)
continue continue

View File

@ -294,6 +294,9 @@ def xml2str(root, pretty_print=False, strip_comments=False):
def xml2unicode(root, pretty_print=False): def xml2unicode(root, pretty_print=False):
return etree.tostring(root, pretty_print=pretty_print) return etree.tostring(root, pretty_print=pretty_print)
def xml2text(elem):
return etree.tostring(elem, method='text', encoding=unicode, with_tail=False)
ASCII_CHARS = set(chr(x) for x in xrange(128)) ASCII_CHARS = set(chr(x) for x in xrange(128))
UNIBYTE_CHARS = set(chr(x) for x in xrange(256)) UNIBYTE_CHARS = set(chr(x) for x in xrange(256))
URL_SAFE = set('ABCDEFGHIJKLMNOPQRSTUVWXYZ' URL_SAFE = set('ABCDEFGHIJKLMNOPQRSTUVWXYZ'
@ -1187,7 +1190,8 @@ class Manifest(object):
if item in self.ids: if item in self.ids:
item = self.ids[item] item = self.ids[item]
del self.ids[item.id] del self.ids[item.id]
del self.hrefs[item.href] if item.href in self.hrefs:
del self.hrefs[item.href]
self.items.remove(item) self.items.remove(item)
if item in self.oeb.spine: if item in self.oeb.spine:
self.oeb.spine.remove(item) self.oeb.spine.remove(item)

View File

@ -184,6 +184,8 @@ class EbookIterator(object):
if processed or plumber.input_fmt.lower() in ('pdb', 'pdf', 'rb') and \ if processed or plumber.input_fmt.lower() in ('pdb', 'pdf', 'rb') and \
not hasattr(self.pathtoopf, 'manifest'): not hasattr(self.pathtoopf, 'manifest'):
if hasattr(self.pathtoopf, 'manifest'):
self.pathtoopf = write_oebbook(self.pathtoopf, self.base)
self.pathtoopf = create_oebbook(self.log, self.pathtoopf, plumber.opts, self.pathtoopf = create_oebbook(self.log, self.pathtoopf, plumber.opts,
plumber.input_plugin) plumber.input_plugin)
if hasattr(self.pathtoopf, 'manifest'): if hasattr(self.pathtoopf, 'manifest'):

View File

@ -96,6 +96,8 @@ class CSSSelector(etree.XPath):
path = css_to_xpath(css) path = css_to_xpath(css)
except UnicodeEncodeError: # Bug in css_to_xpath except UnicodeEncodeError: # Bug in css_to_xpath
path = '/' path = '/'
except NotImplementedError: # Probably a subselect like :hover
path = '/'
path = self.LOCAL_NAME_RE.sub(r"local-name() = '", path) path = self.LOCAL_NAME_RE.sub(r"local-name() = '", path)
etree.XPath.__init__(self, path, namespaces=namespaces) etree.XPath.__init__(self, path, namespaces=namespaces)
self.css = css self.css = css
@ -526,7 +528,7 @@ class Style(object):
base = parent.width base = parent.width
else: else:
base = self._profile.width base = self._profile.width
if 'width' is self._element.attrib: if 'width' in self._element.attrib:
width = self._element.attrib['width'] width = self._element.attrib['width']
elif 'width' in self._style: elif 'width' in self._style:
width = self._style['width'] width = self._style['width']
@ -534,6 +536,8 @@ class Style(object):
result = base result = base
else: else:
result = self._unit_convert(width, base=base) result = self._unit_convert(width, base=base)
if isinstance(result, (unicode, str, bytes)):
result = self._profile.width
self._width = result self._width = result
return self._width return self._width
@ -547,7 +551,7 @@ class Style(object):
base = parent.height base = parent.height
else: else:
base = self._profile.height base = self._profile.height
if 'height' is self._element.attrib: if 'height' in self._element.attrib:
height = self._element.attrib['height'] height = self._element.attrib['height']
elif 'height' in self._style: elif 'height' in self._style:
height = self._style['height'] height = self._style['height']
@ -555,6 +559,8 @@ class Style(object):
result = base result = base
else: else:
result = self._unit_convert(height, base=base) result = self._unit_convert(height, base=base)
if isinstance(result, (unicode, str, bytes)):
result = self._profile.height
self._height = result self._height = result
return self._height return self._height

View File

@ -14,7 +14,7 @@ from lxml import etree
from calibre.ebooks.oeb.base import XPath, XPNSMAP from calibre.ebooks.oeb.base import XPath, XPNSMAP
from calibre import guess_type from calibre import guess_type
from calibre.library.comments import comments_to_html
class Jacket(object): class Jacket(object):
''' '''
Book jacket manipulation. Remove first image and insert comments at start of Book jacket manipulation. Remove first image and insert comments at start of
@ -25,6 +25,7 @@ class Jacket(object):
<html xmlns="%(xmlns)s"> <html xmlns="%(xmlns)s">
<head> <head>
<title>%(title)s</title> <title>%(title)s</title>
<meta name="calibre-content" content="jacket"/>
</head> </head>
<body> <body>
<div class="calibre_rescale_100"> <div class="calibre_rescale_100">
@ -83,7 +84,9 @@ class Jacket(object):
comments = '' comments = ''
if not comments.strip(): if not comments.strip():
comments = '' comments = ''
comments = comments.replace('\r\n', '\n').replace('\n\n', '<br/><br/>') orig_comments = comments
if comments:
comments = comments_to_html(comments)
series = '<b>Series: </b>' + escape(mi.series if mi.series else '') series = '<b>Series: </b>' + escape(mi.series if mi.series else '')
if mi.series and mi.series_index is not None: if mi.series and mi.series_index is not None:
series += escape(' [%s]'%mi.format_series_index()) series += escape(' [%s]'%mi.format_series_index())
@ -96,21 +99,41 @@ class Jacket(object):
except: except:
tags = [] tags = []
if tags: if tags:
tags = '<b>Tags: </b>' + escape(self.opts.dest.tags_to_string(tags)) tags = '<b>Tags: </b>' + self.opts.dest.tags_to_string(tags)
else: else:
tags = '' tags = ''
try: try:
title = mi.title if mi.title else unicode(self.oeb.metadata.title[0]) title = mi.title if mi.title else unicode(self.oeb.metadata.title[0])
except: except:
title = _('Unknown') title = _('Unknown')
html = self.JACKET_TEMPLATE%dict(xmlns=XPNSMAP['h'],
title=escape(title), comments=escape(comments), def generate_html(comments):
return self.JACKET_TEMPLATE%dict(xmlns=XPNSMAP['h'],
title=escape(title), comments=comments,
jacket=escape(_('Book Jacket')), series=series, jacket=escape(_('Book Jacket')), series=series,
tags=tags, rating=self.get_rating(mi.rating)) tags=tags, rating=self.get_rating(mi.rating))
id, href = self.oeb.manifest.generate('jacket', 'jacket.xhtml') id, href = self.oeb.manifest.generate('jacket', 'jacket.xhtml')
root = etree.fromstring(html) from calibre.ebooks.oeb.base import RECOVER_PARSER, XPath
item = self.oeb.manifest.add(id, href, guess_type(href)[0], data=root) try:
self.oeb.spine.insert(0, item, True) root = etree.fromstring(generate_html(comments), parser=RECOVER_PARSER)
except:
root = etree.fromstring(generate_html(escape(orig_comments)),
parser=RECOVER_PARSER)
jacket = XPath('//h:meta[@name="calibre-content" and @content="jacket"]')
found = None
for item in list(self.oeb.spine)[:4]:
try:
if jacket(item.data):
found = item
break
except:
continue
if found is None:
item = self.oeb.manifest.add(id, href, guess_type(href)[0], data=root)
self.oeb.spine.insert(0, item, True)
else:
self.log('Found existing book jacket, replacing...')
found.data = root
def __call__(self, oeb, opts, metadata): def __call__(self, oeb, opts, metadata):

View File

@ -8,6 +8,7 @@ __docformat__ = 'restructuredtext en'
import os import os
from calibre.utils.date import isoformat, now from calibre.utils.date import isoformat, now
from calibre import guess_type
def meta_info_to_oeb_metadata(mi, m, log): def meta_info_to_oeb_metadata(mi, m, log):
from calibre.ebooks.oeb.base import OPF from calibre.ebooks.oeb.base import OPF
@ -92,15 +93,16 @@ class MergeMetadata(object):
scheme='uuid') scheme='uuid')
self.oeb.uid = self.oeb.metadata.identifier[-1] self.oeb.uid = self.oeb.metadata.identifier[-1]
def set_cover(self, mi, prefer_metadata_cover): def set_cover(self, mi, prefer_metadata_cover):
cdata = '' cdata, ext = '', 'jpg'
if mi.cover and os.access(mi.cover, os.R_OK): if mi.cover and os.access(mi.cover, os.R_OK):
cdata = open(mi.cover, 'rb').read() cdata = open(mi.cover, 'rb').read()
ext = mi.cover.rpartition('.')[-1].lower().strip()
elif mi.cover_data and mi.cover_data[-1]: elif mi.cover_data and mi.cover_data[-1]:
cdata = mi.cover_data[1] cdata = mi.cover_data[1]
ext = mi.cover_data[0]
if ext not in ('png', 'jpg', 'jpeg'):
ext = 'jpg'
id = old_cover = None id = old_cover = None
if 'cover' in self.oeb.guide: if 'cover' in self.oeb.guide:
old_cover = self.oeb.guide['cover'] old_cover = self.oeb.guide['cover']
@ -120,8 +122,8 @@ class MergeMetadata(object):
self.oeb.manifest.add(id, old_cover.href, 'image/jpeg') self.oeb.manifest.add(id, old_cover.href, 'image/jpeg')
return id return id
if cdata: if cdata:
id, href = self.oeb.manifest.generate('cover', 'cover.jpg') id, href = self.oeb.manifest.generate('cover', 'cover.'+ext)
self.oeb.manifest.add(id, href, 'image/jpeg', data=cdata) self.oeb.manifest.add(id, href, guess_type('cover.'+ext)[0], data=cdata)
self.oeb.guide.add('cover', 'Cover', href) self.oeb.guide.add('cover', 'Cover', href)
return id return id

View File

@ -5,7 +5,7 @@ __docformat__ = 'restructuredtext en'
''' '''
Splitting of the XHTML flows. Splitting can happen on page boundaries or can be Splitting of the XHTML flows. Splitting can happen on page boundaries or can be
forces at "likely" locations to conform to size limitations. This transform forced at "likely" locations to conform to size limitations. This transform
assumes a prior call to the flatcss transform. assumes a prior call to the flatcss transform.
''' '''
@ -385,12 +385,18 @@ class FlowSplitter(object):
raise SplitError(self.item.href, root) raise SplitError(self.item.href, root)
self.log.debug('\t\t\tSplit point:', split_point.tag, tree.getpath(split_point)) self.log.debug('\t\t\tSplit point:', split_point.tag, tree.getpath(split_point))
for t in self.do_split(tree, split_point, before): trees = self.do_split(tree, split_point, before)
sizes = [len(tostring(t.getroot())) for t in trees]
if min(sizes) < 5*1024:
self.log.debug('\t\t\tSplit tree too small')
self.split_to_size(tree)
return
for t, size in zip(trees, sizes):
r = t.getroot() r = t.getroot()
if self.is_page_empty(r): if self.is_page_empty(r):
continue continue
size = len(tostring(r)) elif size <= self.max_flow_size:
if size <= self.max_flow_size:
self.split_trees.append(t) self.split_trees.append(t)
self.log.debug( self.log.debug(
'\t\t\tCommitted sub-tree #%d (%d KB)'%( '\t\t\tCommitted sub-tree #%d (%d KB)'%(

View File

@ -11,7 +11,7 @@ import re
from lxml import etree from lxml import etree
from urlparse import urlparse from urlparse import urlparse
from calibre.ebooks.oeb.base import XPNSMAP, TOC, XHTML from calibre.ebooks.oeb.base import XPNSMAP, TOC, XHTML, xml2text
from calibre.ebooks import ConversionError from calibre.ebooks import ConversionError
def XPath(x): def XPath(x):
@ -79,8 +79,7 @@ class DetectStructure(object):
page_break_before = 'display: block; page-break-before: always' page_break_before = 'display: block; page-break-before: always'
page_break_after = 'display: block; page-break-after: always' page_break_after = 'display: block; page-break-after: always'
for item, elem in self.detected_chapters: for item, elem in self.detected_chapters:
text = u' '.join([t.strip() for t in elem.xpath('descendant::text()')]) text = xml2text(elem).strip()
text = text.strip()
self.log('\tDetected chapter:', text[:50]) self.log('\tDetected chapter:', text[:50])
if chapter_mark == 'none': if chapter_mark == 'none':
continue continue
@ -120,8 +119,7 @@ class DetectStructure(object):
if frag: if frag:
href = '#'.join((href, frag)) href = '#'.join((href, frag))
if not self.oeb.toc.has_href(href): if not self.oeb.toc.has_href(href):
text = u' '.join([t.strip() for t in \ text = xml2text(a)
a.xpath('descendant::text()')])
text = text[:100].strip() text = text[:100].strip()
if not self.oeb.toc.has_text(text): if not self.oeb.toc.has_text(text):
num += 1 num += 1
@ -135,7 +133,7 @@ class DetectStructure(object):
def elem_to_link(self, item, elem, counter): def elem_to_link(self, item, elem, counter):
text = u' '.join([t.strip() for t in elem.xpath('descendant::text()')]) text = xml2text(elem)
text = text[:100].strip() text = text[:100].strip()
id = elem.get('id', 'calibre_toc_%d'%counter) id = elem.get('id', 'calibre_toc_%d'%counter)
elem.set('id', id) elem.set('id', id)

View File

@ -461,7 +461,7 @@ class FileDialog(QObject):
def get_files(self): def get_files(self):
if self.selected_files is None: if self.selected_files is None:
return tuple(os.path.abspath(qstring_to_unicode(i)) for i in self.fd.selectedFiles()) return tuple(os.path.abspath(unicode(i)) for i in self.fd.selectedFiles())
return tuple(self.selected_files) return tuple(self.selected_files)

View File

@ -17,7 +17,8 @@ class PluginWidget(Widget, Ui_Form):
def __init__(self, parent, get_option, get_help, db=None, book_id=None): def __init__(self, parent, get_option, get_help, db=None, book_id=None):
Widget.__init__(self, parent, 'epub_output', Widget.__init__(self, parent, 'epub_output',
['dont_split_on_page_breaks', 'flow_size', 'no_default_epub_cover'] ['dont_split_on_page_breaks', 'flow_size',
'no_default_epub_cover', 'no_svg_cover']
) )
self.db, self.book_id = db, book_id self.db, self.book_id = db, book_id
self.initialize_options(get_option, get_help, db, book_id) self.initialize_options(get_option, get_help, db, book_id)

View File

@ -21,7 +21,7 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="2" column="0"> <item row="3" column="0">
<widget class="QLabel" name="label"> <widget class="QLabel" name="label">
<property name="text"> <property name="text">
<string>Split files &amp;larger than:</string> <string>Split files &amp;larger than:</string>
@ -31,7 +31,7 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="2" column="1"> <item row="3" column="1">
<widget class="QSpinBox" name="opt_flow_size"> <widget class="QSpinBox" name="opt_flow_size">
<property name="suffix"> <property name="suffix">
<string> KB</string> <string> KB</string>
@ -47,7 +47,7 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="3" column="0"> <item row="4" column="0">
<spacer name="verticalSpacer"> <spacer name="verticalSpacer">
<property name="orientation"> <property name="orientation">
<enum>Qt::Vertical</enum> <enum>Qt::Vertical</enum>
@ -67,6 +67,13 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="2" column="0">
<widget class="QCheckBox" name="opt_no_svg_cover">
<property name="text">
<string>No &amp;SVG cover</string>
</property>
</widget>
</item>
</layout> </layout>
</widget> </widget>
<resources/> <resources/>

View File

@ -71,7 +71,7 @@ class MetadataWidget(Widget, Ui_Form):
self.author_sort.setText(mi.author_sort if mi.author_sort else '') self.author_sort.setText(mi.author_sort if mi.author_sort else '')
self.tags.setText(', '.join(mi.tags if mi.tags else [])) self.tags.setText(', '.join(mi.tags if mi.tags else []))
self.tags.update_tags_cache(self.db.all_tags()) self.tags.update_tags_cache(self.db.all_tags())
self.comment.setText(mi.comments if mi.comments else '') self.comment.setPlainText(mi.comments if mi.comments else '')
if mi.series: if mi.series:
self.series.setCurrentIndex(self.series.findText(mi.series)) self.series.setCurrentIndex(self.series.findText(mi.series))
if mi.series_index is not None: if mi.series_index is not None:

View File

@ -7,41 +7,15 @@ __docformat__ = 'restructuredtext en'
import re import re
from PyQt4.QtCore import SIGNAL, Qt from PyQt4.QtCore import SIGNAL, Qt
from PyQt4.QtGui import QDialog, QWidget, QDialogButtonBox, QFileDialog, \ from PyQt4.QtGui import QDialog, QWidget, QDialogButtonBox, \
QBrush, QSyntaxHighlighter, QTextCharFormat QBrush, QTextCursor, QTextEdit
from calibre.gui2.convert.regex_builder_ui import Ui_RegexBuilder from calibre.gui2.convert.regex_builder_ui import Ui_RegexBuilder
from calibre.gui2.convert.xexp_edit_ui import Ui_Form as Ui_Edit from calibre.gui2.convert.xexp_edit_ui import Ui_Form as Ui_Edit
from calibre.gui2 import qstring_to_unicode from calibre.gui2 import error_dialog, choose_files
from calibre.gui2 import error_dialog
from calibre.ebooks.oeb.iterator import EbookIterator from calibre.ebooks.oeb.iterator import EbookIterator
from calibre.gui2.dialogs.choose_format import ChooseFormatDialog from calibre.gui2.dialogs.choose_format import ChooseFormatDialog
class RegexHighlighter(QSyntaxHighlighter):
def __init__(self, *args):
QSyntaxHighlighter.__init__(self, *args)
self.regex = u''
def update_regex(self, regex):
self.regex = regex
self.rehighlight()
def highlightBlock(self, text):
valid_regex = True
text = qstring_to_unicode(text)
format = QTextCharFormat()
format.setBackground(QBrush(Qt.yellow))
if self.regex:
try:
for mo in re.finditer(self.regex, text):
self.setFormat(mo.start(), mo.end() - mo.start(), format)
except:
valid_regex = False
self.emit(SIGNAL('regex_valid(PyQt_PyObject)'), valid_regex)
class RegexBuilder(QDialog, Ui_RegexBuilder): class RegexBuilder(QDialog, Ui_RegexBuilder):
def __init__(self, db, book_id, regex, *args): def __init__(self, db, book_id, regex, *args):
@ -49,9 +23,7 @@ class RegexBuilder(QDialog, Ui_RegexBuilder):
self.setupUi(self) self.setupUi(self)
self.regex.setText(regex) self.regex.setText(regex)
self.regex_valid(True) self.regex_valid()
self.highlighter = RegexHighlighter(self.preview.document())
self.highlighter.update_regex(regex)
if not db or not book_id: if not db or not book_id:
self.button_box.addButton(QDialogButtonBox.Open) self.button_box.addButton(QDialogButtonBox.Open)
@ -62,19 +34,37 @@ class RegexBuilder(QDialog, Ui_RegexBuilder):
self.connect(self.regex, SIGNAL('textChanged(QString)'), self.regex_valid) self.connect(self.regex, SIGNAL('textChanged(QString)'), self.regex_valid)
self.connect(self.test, SIGNAL('clicked()'), self.do_test) self.connect(self.test, SIGNAL('clicked()'), self.do_test)
def regex_valid(self, valid): def regex_valid(self):
regex = qstring_to_unicode(self.regex.text()) regex = unicode(self.regex.text())
if regex: if regex:
try: try:
re.compile(regex) re.compile(regex)
self.regex.setStyleSheet('QLineEdit { color: black; background-color: rgba(0,255,0,20%); }') self.regex.setStyleSheet('QLineEdit { color: black; background-color: rgba(0,255,0,20%); }')
except: except:
self.regex.setStyleSheet('QLineEdit { color: black; background-color: rgb(255,0,0,20%); }') self.regex.setStyleSheet('QLineEdit { color: black; background-color: rgb(255,0,0,20%); }')
return False
else: else:
self.regex.setStyleSheet('QLineEdit { color: black; background-color: white; }') self.regex.setStyleSheet('QLineEdit { color: black; background-color: white; }')
return True
def do_test(self): def do_test(self):
self.highlighter.update_regex(qstring_to_unicode(self.regex.text())) selections = []
if self.regex_valid():
text = unicode(self.preview.toPlainText())
regex = unicode(self.regex.text())
cursor = QTextCursor(self.preview.document())
extsel = QTextEdit.ExtraSelection()
extsel.cursor = cursor
extsel.format.setBackground(QBrush(Qt.yellow))
try:
for match in re.finditer(regex, text):
es = QTextEdit.ExtraSelection(extsel)
es.cursor.setPosition(match.start(), QTextCursor.MoveAnchor)
es.cursor.setPosition(match.end(), QTextCursor.KeepAnchor)
selections.append(es)
except:
pass
self.preview.setExtraSelections(selections)
def select_format(self, db, book_id): def select_format(self, db, book_id):
format = None format = None
@ -104,9 +94,10 @@ class RegexBuilder(QDialog, Ui_RegexBuilder):
def button_clicked(self, button): def button_clicked(self, button):
if button == self.button_box.button(QDialogButtonBox.Open): if button == self.button_box.button(QDialogButtonBox.Open):
name = QFileDialog.getOpenFileName(self, _('Open book'), _('~')) files = choose_files(self, 'regexp tester dialog', _('Open book'),
if name: select_only_single_file=True)
self.open_book(qstring_to_unicode(name)) if files:
self.open_book(files[0])
if button == self.button_box.button(QDialogButtonBox.Ok): if button == self.button_box.button(QDialogButtonBox.Ok):
self.accept() self.accept()

View File

@ -11,6 +11,7 @@ from PyQt4.QtGui import QDialog, QPixmap, QGraphicsScene, QIcon, QDesktopService
from calibre.gui2.dialogs.book_info_ui import Ui_BookInfo from calibre.gui2.dialogs.book_info_ui import Ui_BookInfo
from calibre.gui2 import dynamic from calibre.gui2 import dynamic
from calibre import fit_image from calibre import fit_image
from calibre.library.comments import comments_to_html
class BookInfo(QDialog, Ui_BookInfo): class BookInfo(QDialog, Ui_BookInfo):
@ -96,6 +97,8 @@ class BookInfo(QDialog, Ui_BookInfo):
self.setWindowTitle(info[_('Title')]) self.setWindowTitle(info[_('Title')])
self.title.setText('<b>'+info.pop(_('Title'))) self.title.setText('<b>'+info.pop(_('Title')))
comments = info.pop(_('Comments'), '') comments = info.pop(_('Comments'), '')
if comments:
comments = comments_to_html(comments)
if re.search(r'<[a-zA-Z]+>', comments) is None: if re.search(r'<[a-zA-Z]+>', comments) is None:
lines = comments.splitlines() lines = comments.splitlines()
lines = [x if x.strip() else '<br><br>' for x in lines] lines = [x if x.strip() else '<br><br>' for x in lines]

View File

@ -180,27 +180,34 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
self.formats_changed = True self.formats_changed = True
def get_selected_format_metadata(self): def get_selected_format_metadata(self):
row = self.formats.currentRow() old = prefs['read_file_metadata']
fmt = self.formats.item(row) if not old:
if fmt is None: prefs['read_file_metadata'] = True
if self.formats.count() == 1:
fmt = self.formats.item(0)
if fmt is None:
error_dialog(self, _('No format selected'),
_('No format selected')).exec_()
return None, None
ext = fmt.ext.lower()
if fmt.path is None:
stream = self.db.format(self.row, ext, as_file=True)
else:
stream = open(fmt.path, 'r+b')
try: try:
mi = get_metadata(stream, ext) row = self.formats.currentRow()
return mi, ext fmt = self.formats.item(row)
except: if fmt is None:
error_dialog(self, _('Could not read metadata'), if self.formats.count() == 1:
_('Could not read metadata from %s format')%ext).exec_() fmt = self.formats.item(0)
return None, None if fmt is None:
error_dialog(self, _('No format selected'),
_('No format selected')).exec_()
return None, None
ext = fmt.ext.lower()
if fmt.path is None:
stream = self.db.format(self.row, ext, as_file=True)
else:
stream = open(fmt.path, 'r+b')
try:
mi = get_metadata(stream, ext)
return mi, ext
except:
error_dialog(self, _('Could not read metadata'),
_('Could not read metadata from %s format')%ext).exec_()
return None, None
finally:
if old != prefs['read_file_metadata']:
prefs['read_file_metadata'] = old
def set_metadata_from_format(self): def set_metadata_from_format(self):
mi, ext = self.get_selected_format_metadata() mi, ext = self.get_selected_format_metadata()
@ -231,7 +238,7 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
if mi.series_index is not None: if mi.series_index is not None:
self.series_index.setValue(float(mi.series_index)) self.series_index.setValue(float(mi.series_index))
if mi.comments and mi.comments.strip(): if mi.comments and mi.comments.strip():
self.comments.setText(mi.comments) self.comments.setPlainText(mi.comments)
def set_cover(self): def set_cover(self):
@ -555,7 +562,7 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
title = qstring_to_unicode(self.title.text()) title = qstring_to_unicode(self.title.text())
try: try:
author = string_to_authors(unicode(self.authors.text()))[0] author = string_to_authors(unicode(self.authors.text()))[0]
except IndexError: except:
author = '' author = ''
publisher = qstring_to_unicode(self.publisher.currentText()) publisher = qstring_to_unicode(self.publisher.currentText())
if isbn or title or author or publisher: if isbn or title or author or publisher:
@ -590,7 +597,7 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
prefix = unicode(self.comments.toPlainText()) prefix = unicode(self.comments.toPlainText())
if prefix: if prefix:
prefix += '\n' prefix += '\n'
self.comments.setText(prefix + summ) self.comments.setPlainText(prefix + summ)
if book.rating is not None: if book.rating is not None:
self.rating.setValue(int(book.rating)) self.rating.setValue(int(book.rating))
if book.tags: if book.tags:
@ -654,7 +661,7 @@ class MetadataSingleDialog(ResizableDialog, Ui_MetadataSingleDialog):
self.db.set_series(self.id, self.db.set_series(self.id,
unicode(self.series.currentText()).strip(), notify=False) unicode(self.series.currentText()).strip(), notify=False)
self.db.set_series_index(self.id, self.series_index.value(), notify=False) self.db.set_series_index(self.id, self.series_index.value(), notify=False)
self.db.set_comment(self.id, qstring_to_unicode(self.comments.toPlainText()), notify=False) self.db.set_comment(self.id, unicode(self.comments.toPlainText()), notify=False)
d = self.pubdate.date() d = self.pubdate.date()
d = qt_to_dt(d) d = qt_to_dt(d)
self.db.set_pubdate(self.id, d, notify=False) self.db.set_pubdate(self.id, d, notify=False)

View File

@ -220,6 +220,10 @@ class Scheduler(QObject):
self.cac = QAction(QIcon(I('user_profile.svg')), _('Add a custom news source'), self) self.cac = QAction(QIcon(I('user_profile.svg')), _('Add a custom news source'), self)
self.connect(self.cac, SIGNAL('triggered(bool)'), self.customize_feeds) self.connect(self.cac, SIGNAL('triggered(bool)'), self.customize_feeds)
self.news_menu.addAction(self.cac) self.news_menu.addAction(self.cac)
self.news_menu.addSeparator()
self.all_action = self.news_menu.addAction(
_('Download all scheduled new sources'),
self.download_all_scheduled)
self.timer = QTimer(self) self.timer = QTimer(self)
self.timer.start(int(self.INTERVAL * 60000)) self.timer.start(int(self.INTERVAL * 60000))
@ -304,7 +308,11 @@ class Scheduler(QObject):
if urn is not None: if urn is not None:
return self.download(urn) return self.download(urn)
for urn in self.recipe_model.scheduled_urns(): for urn in self.recipe_model.scheduled_urns():
self.download(urn) if not self.download(urn):
break
def download_all_scheduled(self):
self.download_clicked(None)
def download(self, urn): def download(self, urn):
self.lock.lock() self.lock.lock()
@ -316,12 +324,13 @@ class Scheduler(QObject):
'is active')) 'is active'))
d.setModal(False) d.setModal(False)
d.show() d.show()
return return False
self.internet_connection_failed = False self.internet_connection_failed = False
doit = urn not in self.download_queue doit = urn not in self.download_queue
self.lock.unlock() self.lock.unlock()
if doit: if doit:
self.do_download(urn) self.do_download(urn)
return True
def check(self): def check(self):
recipes = self.recipe_model.get_to_be_downloaded_recipes() recipes = self.recipe_model.get_to_be_downloaded_recipes()

View File

@ -777,7 +777,7 @@ class BooksView(TableView):
self.setItemDelegateForColumn(cm.index('series'), self.series_delegate) self.setItemDelegateForColumn(cm.index('series'), self.series_delegate)
def set_context_menu(self, edit_metadata, send_to_device, convert, view, def set_context_menu(self, edit_metadata, send_to_device, convert, view,
save, open_folder, book_details, merge, delete, similar_menu=None): save, open_folder, book_details, delete, similar_menu=None):
self.setContextMenuPolicy(Qt.DefaultContextMenu) self.setContextMenuPolicy(Qt.DefaultContextMenu)
self.context_menu = QMenu(self) self.context_menu = QMenu(self)
if edit_metadata is not None: if edit_metadata is not None:
@ -790,8 +790,6 @@ class BooksView(TableView):
self.context_menu.addAction(save) self.context_menu.addAction(save)
if open_folder is not None: if open_folder is not None:
self.context_menu.addAction(open_folder) self.context_menu.addAction(open_folder)
if merge is not None:
self.context_menu.addAction(merge)
if delete is not None: if delete is not None:
self.context_menu.addAction(delete) self.context_menu.addAction(delete)
if book_details is not None: if book_details is not None:

View File

@ -1379,5 +1379,5 @@ void PictureFlow::dataChanged() { d->dataChanged(); }
void PictureFlow::emitcurrentChanged(int index) { emit currentChanged(index); } void PictureFlow::emitcurrentChanged(int index) { emit currentChanged(index); }
int FlowImages::count() { return 0; } int FlowImages::count() { return 0; }
QImage FlowImages::image(int index) { return QImage(); } QImage FlowImages::image(int index) { index=0; return QImage(); }
QString FlowImages::caption(int index) {return QString(); } QString FlowImages::caption(int index) {index=0; return QString(); }

View File

@ -260,7 +260,10 @@ class ShortcutConfig(QWidget):
self.view.setModel(model) self.view.setModel(model)
self.delegate = Delegate() self.delegate = Delegate()
self.view.setItemDelegate(self.delegate) self.view.setItemDelegate(self.delegate)
self.delegate.sizeHintChanged.connect(self.view.scrollTo) self.delegate.sizeHintChanged.connect(self.scrollTo)
def scrollTo(self, index):
self.view.scrollTo(index)
if __name__ == '__main__': if __name__ == '__main__':

View File

@ -11,6 +11,7 @@ from calibre.gui2.widgets import IMAGE_EXTENSIONS
from calibre.gui2.progress_indicator import ProgressIndicator from calibre.gui2.progress_indicator import ProgressIndicator
from calibre.gui2.notify import get_notifier from calibre.gui2.notify import get_notifier
from calibre.ebooks import BOOK_EXTENSIONS from calibre.ebooks import BOOK_EXTENSIONS
from calibre.library.comments import comments_to_html
class BookInfoDisplay(QWidget): class BookInfoDisplay(QWidget):
@ -91,9 +92,9 @@ class BookInfoDisplay(QWidget):
WEIGHTS = collections.defaultdict(lambda : 100) WEIGHTS = collections.defaultdict(lambda : 100)
WEIGHTS[_('Path')] = 0 WEIGHTS[_('Path')] = 0
WEIGHTS[_('Formats')] = 1 WEIGHTS[_('Formats')] = 1
WEIGHTS[_('Comments')] = 2 WEIGHTS[_('Comments')] = 4
WEIGHTS[_('Series')] = 3 WEIGHTS[_('Series')] = 2
WEIGHTS[_('Tags')] = 4 WEIGHTS[_('Tags')] = 3
def __init__(self, clear_message): def __init__(self, clear_message):
QWidget.__init__(self) QWidget.__init__(self)
@ -127,10 +128,14 @@ class BookInfoDisplay(QWidget):
keys.sort(cmp=lambda x, y: cmp(self.WEIGHTS[x], self.WEIGHTS[y])) keys.sort(cmp=lambda x, y: cmp(self.WEIGHTS[x], self.WEIGHTS[y]))
for key in keys: for key in keys:
txt = data[key] txt = data[key]
if not txt or not txt.strip() or txt == 'None':
continue
if isinstance(key, str): if isinstance(key, str):
key = key.decode(preferred_encoding, 'replace') key = key.decode(preferred_encoding, 'replace')
if isinstance(txt, str): if isinstance(txt, str):
txt = txt.decode(preferred_encoding, 'replace') txt = txt.decode(preferred_encoding, 'replace')
if key == _('Comments'):
txt = comments_to_html(txt)
rows += u'<tr><td><b>%s:</b></td><td>%s</td></tr>'%(key, txt) rows += u'<tr><td><b>%s:</b></td><td>%s</td></tr>'%(key, txt)
self.book_data.setText(u'<table>'+rows+u'</table>') self.book_data.setText(u'<table>'+rows+u'</table>')

View File

@ -242,8 +242,12 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
####################### Vanity ######################## ####################### Vanity ########################
self.vanity_template = _('<p>For help see the: <a href="%s">User Manual</a>' self.vanity_template = _('<p>For help see the: <a href="%s">User Manual</a>'
'<br>')%'http://calibre-ebook.com/user_manual' '<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 ' self.vanity_template += _('<b>%s</b>: %s by <b>Kovid Goyal '
'%%(version)s</b><br>%%(device)s</p>')%(__appname__, __version__) '%%(version)s</b><br>%%(device)s</p>')%(__appname__, v)
self.latest_version = ' ' self.latest_version = ' '
self.vanity.setText(self.vanity_template%dict(version=' ', device=' ')) self.vanity.setText(self.vanity_template%dict(version=' ', device=' '))
self.device_info = ' ' self.device_info = ' '
@ -350,7 +354,8 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
self.view_menu = QMenu() self.view_menu = QMenu()
self.view_menu.addAction(_('View')) self.view_menu.addAction(_('View'))
self.view_menu.addAction(_('View specific format')) ac = self.view_menu.addAction(_('View specific format'))
ac.setShortcut(Qt.AltModifier+Qt.Key_V)
self.action_view.setMenu(self.view_menu) self.action_view.setMenu(self.view_menu)
self.delete_menu = QMenu() self.delete_menu = QMenu()
@ -478,16 +483,15 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
self.action_save, self.action_save,
self.action_open_containing_folder, self.action_open_containing_folder,
self.action_show_book_details, self.action_show_book_details,
self.action_merge,
self.action_del, self.action_del,
similar_menu=similar_menu) similar_menu=similar_menu)
self.memory_view.set_context_menu(None, None, None, self.memory_view.set_context_menu(None, None, None,
self.action_view, self.action_save, None, None, None, self.action_del) self.action_view, self.action_save, None, None, self.action_del)
self.card_a_view.set_context_menu(None, None, None, self.card_a_view.set_context_menu(None, None, None,
self.action_view, self.action_save, None, None, None, self.action_del) self.action_view, self.action_save, None, None, self.action_del)
self.card_b_view.set_context_menu(None, None, None, self.card_b_view.set_context_menu(None, None, None,
self.action_view, self.action_save, None, None, None, self.action_del) self.action_view, self.action_save, None, None, self.action_del)
QObject.connect(self.library_view, QObject.connect(self.library_view,
SIGNAL('files_dropped(PyQt_PyObject)'), SIGNAL('files_dropped(PyQt_PyObject)'),
@ -1669,7 +1673,7 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
if src_book: if src_book:
fmt = os.path.splitext(src_book)[-1].replace('.', '').upper() fmt = os.path.splitext(src_book)[-1].replace('.', '').upper()
with open(src_book, 'rb') as f: with open(src_book, 'rb') as f:
self.db.add_format(dest_id, fmt, f, index_is_id=True, self.library_view.model().db.add_format(dest_id, fmt, f, index_is_id=True,
notify=False, replace=replace) notify=False, replace=replace)
def books_to_merge(self, rows): def books_to_merge(self, rows):
@ -1684,7 +1688,7 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
src_ids.append(id_) src_ids.append(id_)
dbfmts = m.db.formats(id_, index_is_id=True) dbfmts = m.db.formats(id_, index_is_id=True)
if dbfmts: if dbfmts:
for fmt in dbfmts: for fmt in dbfmts.split(','):
src_books.append(m.db.format_abspath(id_, fmt, src_books.append(m.db.format_abspath(id_, fmt,
index_is_id=True)) index_is_id=True))
return [dest_id, src_books, src_ids] return [dest_id, src_books, src_ids]
@ -2092,7 +2096,7 @@ class Main(MainWindow, Ui_MainWindow, DeviceGUI):
return return
for row in rows: for row in rows:
path = self.library_view.model().db.abspath(row.row()) path = self.library_view.model().db.abspath(row.row())
QDesktopServices.openUrl(QUrl('file:'+path)) QDesktopServices.openUrl(QUrl.fromLocalFile(path))
def view_book(self, triggered): def view_book(self, triggered):

View File

@ -7,10 +7,12 @@ __docformat__ = 'restructuredtext en'
''' '''
import os, math, re, glob, sys import os, math, re, glob, sys
from base64 import b64encode from base64 import b64encode
from functools import partial
from PyQt4.Qt import QSize, QSizePolicy, QUrl, SIGNAL, Qt, QTimer, \ from PyQt4.Qt import QSize, QSizePolicy, QUrl, SIGNAL, Qt, QTimer, \
QPainter, QPalette, QBrush, QFontDatabase, QDialog, \ QPainter, QPalette, QBrush, QFontDatabase, QDialog, \
QColor, QPoint, QImage, QRegion, QVariant, QIcon, \ QColor, QPoint, QImage, QRegion, QVariant, QIcon, \
QFont, pyqtSignature, QAction, QByteArray QFont, pyqtSignature, QAction, QByteArray, QMenu
from PyQt4.QtWebKit import QWebPage, QWebView, QWebSettings from PyQt4.QtWebKit import QWebPage, QWebView, QWebSettings
from calibre.utils.config import Config, StringConfig from calibre.utils.config import Config, StringConfig
@ -392,13 +394,14 @@ class Document(QWebPage):
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):
padding = '%dpx'%amount body = self.mainFrame().documentElement().findFirst('body')
try: if body.isNull():
old_padding = unicode(self.javascript('$("body").css("padding-bottom")').toString()) return
except: old_padding = unicode(body.styleProperty('padding-bottom',
old_padding = '' body.ComputedStyle)).strip()
padding = u'%dpx'%amount
if old_padding != padding: if old_padding != padding:
self.javascript('$("body").css("padding-bottom", "%s")' % padding) body.setStyleProperty('padding-bottom', padding + ' !important')
class EntityDeclarationProcessor(object): class EntityDeclarationProcessor(object):
@ -421,7 +424,7 @@ class DocumentView(QWebView):
QWebView.__init__(self, *args) QWebView.__init__(self, *args)
self.debug_javascript = False self.debug_javascript = False
self.shortcuts = Shortcuts(SHORTCUTS, 'shortcuts/viewer') self.shortcuts = Shortcuts(SHORTCUTS, 'shortcuts/viewer')
self.self_closing_pat = re.compile(r'<([a-z]+)\s+([^>]+)/>', self.self_closing_pat = re.compile(r'<([a-z1-6]+)\s+([^>]+)/>',
re.IGNORECASE) re.IGNORECASE)
self.setSizePolicy(QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)) self.setSizePolicy(QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding))
self._size_hint = QSize(510, 680) self._size_hint = QSize(510, 680)
@ -449,6 +452,50 @@ class DocumentView(QWebView):
_('&Lookup in dictionary'), self) _('&Lookup in dictionary'), self)
self.dictionary_action.setShortcut(Qt.CTRL+Qt.Key_L) self.dictionary_action.setShortcut(Qt.CTRL+Qt.Key_L)
self.dictionary_action.triggered.connect(self.lookup) self.dictionary_action.triggered.connect(self.lookup)
self.goto_location_action = QAction(_('Go to...'), self)
self.goto_location_menu = m = QMenu(self)
self.goto_location_actions = a = {
'Next Page': self.next_page,
'Previous Page': self.previous_page,
'Section Top' : partial(self.scroll_to, 0),
'Document Top': self.goto_document_start,
'Section Bottom':partial(self.scroll_to, 1),
'Document Bottom': self.goto_document_end,
'Next Section': self.goto_next_section,
'Previous Section': self.goto_previous_section,
}
for name, key in [(_('Next Section'), 'Next Section'),
(_('Previous Section'), 'Previous Section'),
(None, None),
(_('Document Start'), 'Document Top'),
(_('Document End'), 'Document Bottom'),
(None, None),
(_('Section Start'), 'Section Top'),
(_('Section End'), 'Section Bottom'),
(None, None),
(_('Next Page'), 'Next Page'),
(_('Previous Page'), 'Previous Page')]:
if key is None:
m.addSeparator()
else:
m.addAction(name, a[key], self.shortcuts.get_sequences(key)[0])
self.goto_location_action.setMenu(self.goto_location_menu)
def goto_next_section(self, *args):
if self.manager is not None:
self.manager.goto_next_section()
def goto_previous_section(self, *args):
if self.manager is not None:
self.manager.goto_previous_section()
def goto_document_start(self, *args):
if self.manager is not None:
self.manager.goto_start()
def goto_document_end(self, *args):
if self.manager is not None:
self.manager.goto_end()
@property @property
def copy_action(self): def copy_action(self):
@ -488,6 +535,8 @@ class DocumentView(QWebView):
text = unicode(self.selectedText()) text = unicode(self.selectedText())
if text: if text:
menu.insertAction(list(menu.actions())[0], self.dictionary_action) menu.insertAction(list(menu.actions())[0], self.dictionary_action)
menu.addSeparator()
menu.addAction(self.goto_location_action)
menu.exec_(ev.globalPos()) menu.exec_(ev.globalPos())
def lookup(self, *args): def lookup(self, *args):
@ -763,20 +812,9 @@ class DocumentView(QWebView):
def keyPressEvent(self, event): def keyPressEvent(self, event):
key = self.shortcuts.get_match(event) key = self.shortcuts.get_match(event)
if key == 'Next Page': func = self.goto_location_actions.get(key, None)
self.next_page() if func is not None:
elif key == 'Previous Page': func()
self.previous_page()
elif key == 'Section Top':
self.scroll_to(0)
elif key == 'Document Top':
if self.manager is not None:
self.manager.goto_start()
elif key == 'Section Bottom':
self.scroll_to(1)
elif key == 'Document Bottom':
if self.manager is not None:
self.manager.goto_end()
elif key == 'Down': elif key == 'Down':
self.scroll_by(y=15) self.scroll_by(y=15)
elif key == 'Up': elif key == 'Up':
@ -785,12 +823,6 @@ class DocumentView(QWebView):
self.scroll_by(x=-15) self.scroll_by(x=-15)
elif key == 'Right': elif key == 'Right':
self.scroll_by(x=15) self.scroll_by(x=15)
elif key == 'Next Section':
if self.manager is not None:
self.manager.goto_next_section()
elif key == 'Previous Section':
if self.manager is not None:
self.manager.goto_previous_section()
else: else:
return QWebView.keyPressEvent(self, event) return QWebView.keyPressEvent(self, event)

View File

@ -822,7 +822,7 @@ def do_remove_custom_column(db, label, force):
if not force: if not force:
q = raw_input(_('You will lose all data in the column: %r.' q = raw_input(_('You will lose all data in the column: %r.'
' Are you sure (y/n)? ')%label) ' Are you sure (y/n)? ')%label)
if q.lower().strip() != 'y': if q.lower().strip() != _('y'):
return return
db.delete_custom_column(label=label) db.delete_custom_column(label=label)
prints('Column %r removed.'%label) prints('Column %r removed.'%label)

View File

@ -0,0 +1,114 @@
#!/usr/bin/env python
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
__license__ = 'GPL v3'
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
import re
from calibre.constants import preferred_encoding
from calibre.ebooks.BeautifulSoup import BeautifulSoup, Tag, NavigableString
from calibre import prepare_string_for_xml
def comments_to_html(comments):
'''
Convert random comment text to normalized, xml-legal block of <p>s
'plain text' returns as
<p>plain text</p>
'plain text with <i>minimal</i> <b>markup</b>' returns as
<p>plain text with <i>minimal</i> <b>markup</b></p>
'<p>pre-formatted text</p> returns untouched
'A line of text\n\nFollowed by a line of text' returns as
<p>A line of text</p>
<p>Followed by a line of text</p>
'A line of text.\nA second line of text.\rA third line of text' returns as
<p>A line of text.<br />A second line of text.<br />A third line of text.</p>
'...end of a paragraph.Somehow the break was lost...' returns as
<p>...end of a paragraph.</p>
<p>Somehow the break was lost...</p>
Deprecated HTML returns as HTML via BeautifulSoup()
'''
if not isinstance(comments, unicode):
comments = comments.decode(preferred_encoding, 'replace')
# Hackish - ignoring sentences ending or beginning in numbers to avoid
# confusion with decimal points.
# Explode lost CRs to \n\n
for lost_cr in re.finditer('([a-z])([\.\?!])([A-Z])', comments):
comments = comments.replace(lost_cr.group(),
'%s%s\n\n%s' % (lost_cr.group(1),
lost_cr.group(2),
lost_cr.group(3)))
# 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)
# Convert solo returns to <br />
comments = re.sub('[\r\n]','<br />', comments)
# Convert two hyphens to emdash
comments = re.sub('--', '&mdash;', comments)
soup = BeautifulSoup(comments)
result = BeautifulSoup()
rtc = 0
open_pTag = False
all_tokens = list(soup.contents)
for token in all_tokens:
if type(token) is NavigableString:
if not open_pTag:
pTag = Tag(result,'p')
open_pTag = True
ptc = 0
pTag.insert(ptc,prepare_string_for_xml(token))
ptc += 1
elif token.name in ['br','b','i','em']:
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:
p['class'] = 'description'
return result.renderContents(encoding=None)

View File

@ -27,7 +27,9 @@ FORMAT_ARG_DESCS = dict(
'of the name use {author_sort[0]}'), 'of the name use {author_sort[0]}'),
tags=_('The tags'), tags=_('The tags'),
series=_('The series'), series=_('The series'),
series_index=_('The series number. To get leading zeros use {series_index:0>3s}'), series_index=_('The series number. '
'To get leading zeros use {series_index:0>3s} or '
'{series_index:>3s} for leading spaces'),
rating=_('The rating'), rating=_('The rating'),
isbn=_('The ISBN'), isbn=_('The ISBN'),
publisher=_('The publisher'), publisher=_('The publisher'),

View File

@ -113,7 +113,7 @@ Metadata download plugins
When :meth:`fetch` is called, the `self` object will have the following When :meth:`fetch` is called, the `self` object will have the following
useful attributes (each of which may be None):: useful attributes (each of which may be None)::
title, author, publisher, isbn, log, verbose and extra title, book_author, publisher, isbn, log, verbose and extra
Use these attributes to construct the search query. extra is reserved for Use these attributes to construct the search query. extra is reserved for
future use. future use.

View File

@ -19,12 +19,13 @@ __builtin__.__dict__['__'] = lambda s: s
from calibre.constants import iswindows, preferred_encoding, plugins from calibre.constants import iswindows, preferred_encoding, plugins
_run_once = False _run_once = False
winutil = winutilerror = None
if not _run_once: if not _run_once:
_run_once = True _run_once = True
################################################################################ ################################################################################
# Platform specific modules # Platform specific modules
winutil = winutilerror = None
if iswindows: if iswindows:
winutil, winutilerror = plugins['winutil'] winutil, winutilerror = plugins['winutil']
if not winutil: if not winutil:

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -106,6 +106,7 @@ _extra_lang_codes = {
'en_SG' : _('English (Singapore)'), 'en_SG' : _('English (Singapore)'),
'en_YE' : _('English (Yemen)'), 'en_YE' : _('English (Yemen)'),
'en_IE' : _('English (Ireland)'), 'en_IE' : _('English (Ireland)'),
'es_PY' : _('Spanish (Paraguay)'),
'de_AT' : _('German (AT)'), 'de_AT' : _('German (AT)'),
'nl' : _('Dutch (NL)'), 'nl' : _('Dutch (NL)'),
'nl_BE' : _('Dutch (BE)'), 'nl_BE' : _('Dutch (BE)'),

View File

@ -183,10 +183,6 @@ class RecipeModel(QAbstractItemModel, SearchQueryParser):
lang_map = {} lang_map = {}
self.all_urns = set([]) self.all_urns = set([])
self.showing_count = 0 self.showing_count = 0
for x in self.scheduler_config.iter_recipes():
urn = x.get('id')
if ok(urn):
factory(NewsItem, scheduled, urn, x.get('title'))
for x in self.custom_recipe_collection: for x in self.custom_recipe_collection:
urn = x.get('id') urn = x.get('id')
self.all_urns.add(urn) self.all_urns.add(urn)
@ -202,6 +198,13 @@ class RecipeModel(QAbstractItemModel, SearchQueryParser):
lang_map[lang] = factory(NewsCategory, new_root, lang) lang_map[lang] = factory(NewsCategory, new_root, lang)
factory(NewsItem, lang_map[lang], urn, x.get('title')) factory(NewsItem, lang_map[lang], urn, x.get('title'))
self.showing_count += 1 self.showing_count += 1
for x in self.scheduler_config.iter_recipes():
urn = x.get('id')
if urn not in self.all_urns:
self.scheduler_config.un_schedule_recipe(urn)
continue
if ok(urn):
factory(NewsItem, scheduled, urn, x.get('title'))
new_root.prune() new_root.prune()
new_root.sort() new_root.sort()
self.root = new_root self.root = new_root