This commit is contained in:
GRiker 2011-04-01 13:15:00 -07:00
commit 7590f2b3c9
115 changed files with 35824 additions and 31782 deletions

View File

@ -19,6 +19,75 @@
# new recipes: # new recipes:
# - title: # - title:
- version: 0.7.53
date: 2011-04-01
new features:
- title: "Email delivery: You can now specify a subject that calibre will use when sending emails per email account, configured in Preferences->Sending by email. The subject is a template of the same kind used in Save to Disk, etc. So youcan specift the title/authors/series/whatever in the template."
tickets: [743535]
- title: "Apple driver: When an iDevice is detected, inform the user about the Connect to iTunes method instead of trying to connect directly to the device, as the latter can be buggy. See http://www.mobileread.com/forums/showthread.php?t=127883 for details"
- title: "SONY driver: Search for books on the device in all directories not just database/media/books. This can be turned off by customizing the SONY plugin in Preferences->Plugins"
- title: "EPUB Output: Remove any margins specified via an Adobe page template in the input document. This means that the margins psecified in calibre are more likely to be the actual margins used."
- title: "When reading metadata from filenames, allow publisher and published date to be read from the filename"
tickets: [744020]
- title: "Remove the option to show a second tool bar from Preferences->Look & Feel. Instead go to Preferences->Toolbars and add items to the second toolbar to control exactly what is visible there."
tickets: [742686]
- title: "Add a tweak that can be used to have the calibre content server listen for IPv6 connections."
tickets: [743486]
- title: "When clicking Next or Previous in the edit metadata dialog, then active book in the main book list is also changed"
tickets: [743533]
- title: "Remember the previously used setting for Match all/Match any under the Tag Browser when calibre restarts"
tickets: [743645]
- title: "FB2 Output: Option to set the FB2 genre explicitly."
tickets: [743178]
bug fixes:
- title: "Fix text color in the search bar set to black instead of the system font color"
tickets: [746846]
- title: "Workaround for Word bug where Word uses gb2312 as the encoding when exporting CHinese docs to HTML istead of gbk"
tickets: [745428]
- title: "Make sorting on the device view faster and more robust."
tickets: [742626]
- title: "E-book viewer: Fix viewer losing place in very long single file documents when window resized."
tickets: [745001]
- title: "MOBI Output: Workaround for Amazon's MOBI renderer not rendering top margins on ul and ol tags."
tickets: [744365]
- title: "EPUB Input: Workaround for invalid EPUBs produced by someone named 'ibooks, Inc.'."
tickets: [744122]
- title: "RTF Input: Handle RTF files with too many levels of list nesting."
tickets: [743243]
improved recipes:
- Irish Times
- LifeHacker
- Estadao
- Folha de Sao Paulo
new recipes:
- title: Financieele Dagblad
author: marvin_2
- title: "Prost Amerika, WV Hooligan and SB Nation"
author: rylsfan
- title: "Cracked.com"
author: Nudgenudge
- version: 0.7.52 - version: 0.7.52
date: 2011-03-25 date: 2011-03-25

View File

@ -1,4 +1,3 @@
#!/usr/bin/env python
from calibre.web.feeds.news import BasicNewsRecipe from calibre.web.feeds.news import BasicNewsRecipe
from datetime import datetime, timedelta from datetime import datetime, timedelta
from calibre.ebooks.BeautifulSoup import Tag,BeautifulSoup from calibre.ebooks.BeautifulSoup import Tag,BeautifulSoup
@ -6,18 +5,17 @@ from calibre.utils.magick import Image, PixelWand
from urllib2 import Request, urlopen, URLError from urllib2 import Request, urlopen, URLError
class Estadao(BasicNewsRecipe): class Estadao(BasicNewsRecipe):
THUMBALIZR_API = "0123456789abcdef01234567890" # ---->Get your at http://www.thumbalizr.com/ THUMBALIZR_API = '' # ---->Get your at http://www.thumbalizr.com/ and put here
LANGUAGE = 'pt_br' LANGUAGE = 'pt_br'
language = 'pt' language = 'pt'
LANGHTM = 'pt-br' LANGHTM = 'pt-br'
ENCODING = 'utf' ENCODING = 'utf'
ENCHTM = 'utf-8' ENCHTM = 'utf-8'
directionhtm = 'ltr' directionhtm = 'ltr'
requires_version = (0,8,47) requires_version = (0,7,47)
news = True news = True
publication_type = 'newsportal'
title = u'Estadao' title = u'Estad\xe3o'
__author__ = 'Euler Alves' __author__ = 'Euler Alves'
description = u'Brazilian news from Estad\xe3o' description = u'Brazilian news from Estad\xe3o'
publisher = u'Estad\xe3o' publisher = u'Estad\xe3o'
@ -33,14 +31,6 @@ class Estadao(BasicNewsRecipe):
remove_empty_feeds = True remove_empty_feeds = True
timefmt = ' [%d %b %Y (%a)]' timefmt = ' [%d %b %Y (%a)]'
html2lrf_options = [
'--comment', description
,'--category', category
,'--publisher', publisher
]
html2epub_options = 'publisher="' + publisher + '"\ncomments="' + description + '"\ntags="' + category + '"'
hoje = datetime.now()-timedelta(days=2) hoje = datetime.now()-timedelta(days=2)
pubdate = hoje.strftime('%a, %d %b') pubdate = hoje.strftime('%a, %d %b')
if hoje.hour<10: if hoje.hour<10:
@ -69,6 +59,7 @@ class Estadao(BasicNewsRecipe):
,dict(name='script') ,dict(name='script')
] ]
feeds = [ feeds = [
(u'\xDAltimas Not\xEDcias', u'http://www.estadao.com.br/rss/ultimas.xml') (u'\xDAltimas Not\xEDcias', u'http://www.estadao.com.br/rss/ultimas.xml')
,(u'Manchetes', u'http://www.estadao.com.br/rss/manchetes.xml') ,(u'Manchetes', u'http://www.estadao.com.br/rss/manchetes.xml')
@ -109,6 +100,8 @@ class Estadao(BasicNewsRecipe):
img.open(iurl) img.open(iurl)
width, height = img.size width, height = img.size
print 'img is: ', iurl, 'width is: ', width, 'height is: ', height print 'img is: ', iurl, 'width is: ', width, 'height is: ', height
if img < 0:
raise RuntimeError('Out of memory')
pw = PixelWand() pw = PixelWand()
if( width > height and width > 590) : if( width > height and width > 590) :
print 'Rotate image' print 'Rotate image'
@ -117,18 +110,20 @@ class Estadao(BasicNewsRecipe):
return soup return soup
def get_cover_url(self): def get_cover_url(self):
cover_url = self.CAPA if self.THUMBALIZR_API:
pedido = Request(self.CAPA) cover_url = self.CAPA
pedido.add_header('User-agent','Mozilla/5.0 (Windows; U; Windows NT 5.1; '+self.LANGHTM+'; userid='+self.THUMBALIZR_API+') Calibre/0.8.47 (like Gecko)') pedido = Request(self.CAPA)
pedido.add_header('Accept-Charset',self.ENCHTM) pedido.add_header('User-agent','Mozilla/5.0 (Windows; U; Windows NT 5.1; '+self.LANGHTM+'; userid='+self.THUMBALIZR_API+') Calibre/0.8.47 (like Gecko)')
pedido.add_header('Referer',self.SCREENSHOT) pedido.add_header('Accept-Charset',self.ENCHTM)
try: pedido.add_header('Referer',self.SCREENSHOT)
resposta = urlopen(pedido) try:
soup = BeautifulSoup(resposta) resposta = urlopen(pedido)
cover_item = soup.find('body') soup = BeautifulSoup(resposta)
if cover_item: cover_item = soup.find('body')
if cover_item:
cover_url='http://api.thumbalizr.com/?api_key='+self.THUMBALIZR_API+'&url='+self.SCREENSHOT+'&width=600&quality=90'
return cover_url
except URLError:
cover_url='http://api.thumbalizr.com/?api_key='+self.THUMBALIZR_API+'&url='+self.SCREENSHOT+'&width=600&quality=90' cover_url='http://api.thumbalizr.com/?api_key='+self.THUMBALIZR_API+'&url='+self.SCREENSHOT+'&width=600&quality=90'
return cover_url return cover_url
except URLError:
cover_url='http://api.thumbalizr.com/?api_key='+self.THUMBALIZR_API+'&url='+self.SCREENSHOT+'&width=600&quality=90'
return cover_url

View File

@ -0,0 +1,29 @@
from calibre.web.feeds.news import BasicNewsRecipe
class fd(BasicNewsRecipe):
title = u'Het Financieele Dagblad'
__author__ = 'marvin_2'
oldest_article = 7
max_articles_per_feed = 100
no_stylesheets = True
cover_url = 'http://www.fd.nl/static/gfx/logo-fd-164x78.gif'
language = 'nl'
keep_only_tags = (dict(name = 'div', attrs = {'class': ['headlinearticle']}))
remove_tags = [dict(name='span' , attrs={'class':['opties']})]
feeds = [
(u'Overzicht',u'http://www.fd.nl/nieuws/overzicht/?view=RSS&profiel=OPENBAAR')
]
extra_css = '''
h1 {font-family:Arial,Helvetica,sans-serif; font-weight:bold;font-size:x-large;}
p{font-family:Arial,Helvetica,sans-serif;}
strong{font-weight:bold; margin-right:5pt;margin-top:20pt;}
.datum_ie {font-style:italic;font-size:small;}
img {align:left;}
'''

View File

@ -5,16 +5,15 @@ from calibre.utils.magick import Image, PixelWand
from urllib2 import Request, urlopen, URLError from urllib2 import Request, urlopen, URLError
class FolhaOnline(BasicNewsRecipe): class FolhaOnline(BasicNewsRecipe):
THUMBALIZR_API = "0123456789abcdef01234567890" # ---->Get your at http://www.thumbalizr.com/ THUMBALIZR_API = '' # ---->Get your at http://www.thumbalizr.com/ and put here
LANGUAGE = 'pt_br' LANGUAGE = 'pt_br'
language = 'pt' language = 'pt'
LANGHTM = 'pt-br' LANGHTM = 'pt-br'
ENCODING = 'cp1252' ENCODING = 'cp1252'
ENCHTM = 'iso-8859-1' ENCHTM = 'iso-8859-1'
directionhtm = 'ltr' directionhtm = 'ltr'
requires_version = (0,8,47) requires_version = (0,7,47)
news = True news = True
publication_type = 'newsportal'
title = u'Folha de S\xE3o Paulo' title = u'Folha de S\xE3o Paulo'
__author__ = 'Euler Alves' __author__ = 'Euler Alves'
@ -96,6 +95,7 @@ class FolhaOnline(BasicNewsRecipe):
,(u'Valdo Cruz', u'http://http://feeds.folha.uol.com.br/folha/pensata/valdocruz/rss091.xml') ,(u'Valdo Cruz', u'http://http://feeds.folha.uol.com.br/folha/pensata/valdocruz/rss091.xml')
] ]
conversion_options = { conversion_options = {
'title' : title 'title' : title
,'comments' : description ,'comments' : description
@ -124,6 +124,8 @@ class FolhaOnline(BasicNewsRecipe):
img.open(iurl) img.open(iurl)
width, height = img.size width, height = img.size
print 'img is: ', iurl, 'width is: ', width, 'height is: ', height print 'img is: ', iurl, 'width is: ', width, 'height is: ', height
if img < 0:
raise RuntimeError('Out of memory')
pw = PixelWand() pw = PixelWand()
if( width > height and width > 590) : if( width > height and width > 590) :
print 'Rotate image' print 'Rotate image'

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 714 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 392 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 561 B

BIN
recipes/icons/sb_nation.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -1,5 +1,5 @@
__license__ = 'GPL v3' __license__ = 'GPL v3'
__copyright__ = "2008, Derry FitzGerald. 2009 Modified by Ray Kinsella and David O'Callaghan" __copyright__ = "2008, Derry FitzGerald. 2009 Modified by Ray Kinsella and David O'Callaghan, 2011 Modified by Phil Burns"
''' '''
irishtimes.com irishtimes.com
''' '''
@ -9,17 +9,20 @@ from calibre.web.feeds.news import BasicNewsRecipe
class IrishTimes(BasicNewsRecipe): class IrishTimes(BasicNewsRecipe):
title = u'The Irish Times' title = u'The Irish Times'
__author__ = "Derry FitzGerald, Ray Kinsella and David O'Callaghan" encoding = 'ISO-8859-15'
__author__ = "Derry FitzGerald, Ray Kinsella, David O'Callaghan and Phil Burns"
language = 'en_IE' language = 'en_IE'
timefmt = ' (%A, %B %d, %Y)' timefmt = ' (%A, %B %d, %Y)'
oldest_article = 3
oldest_article = 1.0
max_articles_per_feed = 100
no_stylesheets = True no_stylesheets = True
simultaneous_downloads= 1 simultaneous_downloads= 5
r = re.compile('.*(?P<url>http:\/\/(www.irishtimes.com)|(rss.feedsportal.com\/c)\/.*\.html?).*') r = re.compile('.*(?P<url>http:\/\/(www.irishtimes.com)|(rss.feedsportal.com\/c)\/.*\.html?).*')
remove_tags = [dict(name='div', attrs={'class':'footer'})] remove_tags = [dict(name='div', attrs={'class':'footer'})]
extra_css = '.headline {font-size: x-large;} \n .fact { padding-top: 10pt }' extra_css = 'p, div { margin: 0pt; border: 0pt; text-indent: 0.5em } .headline {font-size: large;} \n .fact { padding-top: 10pt }'
feeds = [ feeds = [
('Frontpage', 'http://www.irishtimes.com/feeds/rss/newspaper/index.rss'), ('Frontpage', 'http://www.irishtimes.com/feeds/rss/newspaper/index.rss'),
@ -30,15 +33,29 @@ class IrishTimes(BasicNewsRecipe):
('Sport', 'http://www.irishtimes.com/feeds/rss/newspaper/sport.rss'), ('Sport', 'http://www.irishtimes.com/feeds/rss/newspaper/sport.rss'),
('Opinion', 'http://www.irishtimes.com/feeds/rss/newspaper/opinion.rss'), ('Opinion', 'http://www.irishtimes.com/feeds/rss/newspaper/opinion.rss'),
('Letters', 'http://www.irishtimes.com/feeds/rss/newspaper/letters.rss'), ('Letters', 'http://www.irishtimes.com/feeds/rss/newspaper/letters.rss'),
('Magazine', 'http://www.irishtimes.com/feeds/rss/newspaper/magazine.rss'),
('Health', 'http://www.irishtimes.com/feeds/rss/newspaper/health.rss'),
('Education & Parenting', 'http://www.irishtimes.com/feeds/rss/newspaper/education.rss'),
('Motors', 'http://www.irishtimes.com/feeds/rss/newspaper/motors.rss'),
('An Teanga Bheo', 'http://www.irishtimes.com/feeds/rss/newspaper/anteangabheo.rss'),
('Commercial Property', 'http://www.irishtimes.com/feeds/rss/newspaper/commercialproperty.rss'),
('Science Today', 'http://www.irishtimes.com/feeds/rss/newspaper/sciencetoday.rss'),
('Property', 'http://www.irishtimes.com/feeds/rss/newspaper/property.rss'),
('The Tickets', 'http://www.irishtimes.com/feeds/rss/newspaper/theticket.rss'),
('Weekend', 'http://www.irishtimes.com/feeds/rss/newspaper/weekend.rss'),
('News features', 'http://www.irishtimes.com/feeds/rss/newspaper/newsfeatures.rss'),
('Obituaries', 'http://www.irishtimes.com/feeds/rss/newspaper/obituaries.rss'),
] ]
def print_version(self, url): def print_version(self, url):
if url.count('rss.feedsportal.com'): if url.count('rss.feedsportal.com'):
u = 'http://www.irishtimes.com' + \ u = url.replace('0Bhtml/story01.htm','_pf0Bhtml/story01.htm')
(((url[70:].replace('0C','/')).replace('0A','0'))).replace('0Bhtml/story01.htm','_pf.html') else:
else: u = url.replace('.html','_pf.html')
u = url.replace('.html','_pf.html') return u
return u
def get_article_url(self, article): def get_article_url(self, article):
return article.link return article.link

View File

@ -1,37 +1,100 @@
__license__ = 'GPL v3'
__copyright__ = '2010, NA'
'''
lifehacker.com
'''
from calibre.web.feeds.news import BasicNewsRecipe from calibre.web.feeds.news import BasicNewsRecipe
from datetime import datetime
from calibre.ebooks.BeautifulSoup import Tag
from calibre.utils.magick import Image, PixelWand
class Lifehacker(BasicNewsRecipe): class LifeHacker(BasicNewsRecipe):
title = 'Lifehacker' THUMBALIZR_API = '' # ---->Get your at http://www.thumbalizr.com/ and put here
__author__ = 'Kovid Goyal' LANGUAGE = 'en'
description = "Computers make us more productive. Yeah, right. Lifehacker recommends the software downloads and web sites that actually save time. Don't live to geek; geek to live." LANGHTM = 'en'
publisher = 'lifehacker.com' language = 'en'
category = 'news, IT, Internet, gadgets, tips and tricks, howto, diy' ENCODING = 'utf'
oldest_article = 2 ENCHTM = 'utf-8'
max_articles_per_feed = 100 requires_version = (0,7,47)
news = True
title = u'LifeHacker'
__author__ = 'Euler Alves'
description = u'Tips, tricks, and downloads for getting things done.'
publisher = u'lifehacker.com'
author = u'Adam Pash & Kevin Purdy & Adam Dachis & Whitson Gordon & Gina Trapani'
category = 'news, rss'
oldest_article = 4
max_articles_per_feed = 20
summary_length = 1000
remove_javascript = True
no_stylesheets = True no_stylesheets = True
encoding = 'utf-8'
use_embedded_content = True use_embedded_content = True
language = 'en' remove_empty_feeds = True
timefmt = ' [%d %b %Y (%a)]'
hoje = datetime.now()
pubdate = hoje.strftime('%a, %d %b')
cover_url = 'http://api.thumbalizr.com/?api_key='+THUMBALIZR_API+'&url=http://lifehacker.com&width=600&quality=90'
cover_margins = (0,0,'white')
masthead_url = 'http://cache.gawkerassets.com/assets/lifehacker.com/img/logo.png' masthead_url = 'http://cache.gawkerassets.com/assets/lifehacker.com/img/logo.png'
conversion_options = {
'comment' : description
, 'tags' : category
, 'publisher' : publisher
, 'language' : language
}
remove_tags = [ remove_tags = [
{'class': 'feedflare'}, {'class': 'feedflare'},
dict(name='div',
attrs={'class':[
'ad_container'
,'ad_300x250'
,'ad_interstitial'
,'share-wrap'
,'ad_300x600'
,'ad_perma-footer-adsense'
,'ad_perma-panorama'
,'ad panorama'
,'ad_container'
]})
,dict(name='div',
attrs={'id':[
'agegate_container'
,'agegate_container_rejected'
,'sharemenu-wrap'
]})
] ]
feeds = [(u'Articles', u'http://feeds.gawker.com/lifehacker/vip?format=xml')] feeds = [(u'Articles', u'http://feeds.gawker.com/lifehacker/vip?format=xml')]
def preprocess_html(self, soup): conversion_options = {
return self.adeify_images(soup) 'title' : title
,'comments' : description
,'publisher' : publisher
,'tags' : category
,'language' : LANGUAGE
,'linearize_tables': True
}
def preprocess_html(self, soup):
for item in soup.findAll(style=True):
del item['style']
if not soup.find(attrs={'http-equiv':'Content-Language'}):
meta0 = Tag(soup,'meta',[("http-equiv","Content-Language"),("content",self.LANGHTM)])
soup.head.insert(0,meta0)
if not soup.find(attrs={'http-equiv':'Content-Type'}):
meta1 = Tag(soup,'meta',[("http-equiv","Content-Type"),("content","text/html; charset="+self.ENCHTM)])
soup.head.insert(0,meta1)
return soup
def postprocess_html(self, soup, first):
#process all the images. assumes that the new html has the correct path
for tag in soup.findAll(lambda tag: tag.name.lower()=='img' and tag.has_key('src')):
iurl = tag['src']
img = Image()
img.open(iurl)
width, height = img.size
print 'img is: ', iurl, 'width is: ', width, 'height is: ', height
if img < 0:
raise RuntimeError('Out of memory')
pw = PixelWand()
if( width > height and width > 590) :
print 'Rotate image'
img.rotate(pw, -90)
img.save(iurl)
return soup

View File

@ -0,0 +1,68 @@
#!/usr/bin/env python
# encoding: utf-8
__license__ = 'GPL 3'
__copyright__ = 'zotzo'
"""
http://www.prostamerika.com/
"""
from calibre.web.feeds.news import BasicNewsRecipe
class ProstAmerika(BasicNewsRecipe):
title = 'Prost Amerika'
language = 'en'
__author__ = 'rylsfan'
#authors =
description = 'Seattle soccer with a European accent. News, features, and match reports.'
publisher = 'ProstAmerika' # 4464 fremont avenue n, # 209, Seattle, 98103, United States
category = 'Sports'
oldest_article = 7
max_articles_per_feed = 100
cover_url = 'http://img17.imageshack.us/img17/9498/prostamerika.jpg'
masthead_url = 'http://www.prostamerika.com/soundersfc/wp-content/uploads/2011/02/PASoccer_taglinewhole.jpg'
encoding = 'utf-8'
no_stylesheets = True
use_embedded_content = False
remove_javascript = True
feeds =[
(u'Cascadia', u'http://www.prostamerika.com/category/localfootball/feed/' ),
(u'MLS', u'http://www.prostamerika.com/category/mls/feed/'),
(u'EPL', u'http://www.prostamerika.com/category/epl/feed/'),
(u'World', u'http://www.prostamerika.com/category/international-soccer/feed/'),
(u'Fan Culture',u'http://www.prostamerika.com/category/fan-culture/feed/')
]
keep_only_tags = [dict(name='div', attrs={'id':'maincontent'})]
remove_tags = [
{'class':'tweetmeme_button'},
{'class':'wp-caption-text'}
]
remove_tags_after =[
{'class':'tweetmeme_button'}
]
extra_css = '''
h1{font-family:Didot,Helvetica,sans-serif; font-weight:bold;font-size:large;}
h2{font-family:Arial,Helvetica,sans-serif; font-weight:normal;font-size:small;}
p{font-family:Arial,Helvetica,sans-serif;font-size:small;}
body{font-family:Helvetica,Arial,sans-serif;font-size:small;}
'''
def preprocess_html(self, soup):
return self.adeify_images(soup)

56
recipes/sb_nation.recipe Normal file
View File

@ -0,0 +1,56 @@
#!/usr/bin/env python
__license__ = 'GPL v3'
__copyright__ = 'Zotzo'
'''
http://www.stumptownfooty.com/
http://www.eightysixforever.com
http://www.sounderatheart.com
http://www.dailysoccerfix.com/
'''
from calibre.web.feeds.news import BasicNewsRecipe
class SBNation(BasicNewsRecipe):
title = u'SBNation'
__author__ = 'rylsfan'
description = u"More than 290 individual communities, each offering high quality year-round coverage and conversation led by fans who are passionate."
oldest_article = 3
language = 'en'
max_articles_per_feed = 100
no_stylesheets = True
use_embedded_content = False
#cover_url = 'http://img132.imageshack.us/img132/4913/2hyggjegqqdywzn9.png'
keep_only_tags = [
dict(name='h2', attrs={'class':'title'})
,dict(name='div', attrs={'class':'entry-body'})
]
remove_tags_after = dict(name='div', attrs={'class':'footline entry-actions'})
remove_tags = [
dict(name='div', attrs={'class':'footline entry-actions'}),
{'class': 'extend-divide'}
]
# SBNation has 300 special blogs to choose from. These are just a couple!
feeds = [
(u'Daily Fix', u'http://www.dailysoccerfix.com/rss/'),
(u"Stumptown Footy", u'http://www.stumptownfooty.com/rss/'),
(u'Sounders', u'http://www.sounderatheart.com/rss/'),
(u'Whitecaps', u'http://www.eightysixforever.com/rss/'),
]
extra_css = """
h1{font-family:Arial,Helvetica,sans-serif; font-weight:bold;font-size:large;}
h2{font-family:Arial,Helvetica,sans-serif; font-weight:bold;font-size:large;}
p{font-family:Helvetica,sans-serif; display: block; text-align: left; text-decoration: none; text-indent: 0%;}
body{font-family:Helvetica,Arial,sans-serif;font-size:small;}
"""
def preprocess_html(self, soup):
return self.adeify_images(soup)
def populate_article_metadata(self, article, soup, first):
h2 = soup.find('h2')
h2.replaceWith(h2.prettify() + '<p><em> By ' + article.author + '</em></p>')

61
recipes/wvhooligan.recipe Normal file
View File

@ -0,0 +1,61 @@
#!/usr/bin/env python
__license__ = 'GPL 3'
__copyright__ = 'zotzo'
__docformat__ = 'restructuredtext en'
'''
http://wvhooligan.com/
'''
from calibre.web.feeds.news import BasicNewsRecipe
#import re
class wvHooligan(BasicNewsRecipe):
authors = u'Drew Epperley'
__author__ = 'rylsfan'
language = 'en'
version = 2
title = u'WV Hooligan'
publisher = u'Drew Epperley'
publication_type = 'Blog'
category = u'Soccer'
description = u'A look at Major League Soccer (MLS) through the eyes of a MLS writer and fan.'
cover_url = 'http://wvhooligan.com/wp-content/themes/urbanelements/images/logo3.png'
oldest_article = 15
max_articles_per_feed = 150
use_embedded_content = True
no_stylesheets = True
remove_javascript = True
encoding = 'utf8'
conversion_options = {
'comment' : description
, 'tags' : category
, 'publisher' : publisher
, 'language' : language
}
remove_tags = [
{'class': 'feedflare'},
{'class': 'tweetmeme_button'},
]
def preprocess_html(self, soup):
return self.adeify_images(soup)
feeds =[
(u'Stories', u'http://feeds2.feedburner.com/wvhooligan'),
(u'MLS', u'http://wvhooligan.com/category/mls/feed/'),
(u'MLS Power Rankings', u'http://wvhooligan.com/category/power-rankings/feed/'),
(u'MLS Expansion', u'http://wvhooligan.com/category/mls/expansion-talk/feed/'),
(u'US National Team', u'http://wvhooligan.com/category/us-national-team/feed/'),
(u'College', u'http://wvhooligan.com/category/college-soccer/feed/'),
]
extra_css = '''
h1{font-family:Arial,Helvetica,sans-serif; font-weight:bold;font-size:large;}
h2{font-family:Arial,Helvetica,sans-serif; font-weight:normal;font-size:small;}
p{font-family:Arial,Helvetica,sans-serif;font-size:small;}
body{font-family:Helvetica,Arial,sans-serif;font-size:small;}
'''

View File

@ -5,7 +5,8 @@ __license__ = 'GPL v3'
__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>' __copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
import os, re, cStringIO, base64, httplib, subprocess, hashlib, shutil, time, glob import os, re, cStringIO, base64, httplib, subprocess, hashlib, shutil, time, \
glob, stat
from subprocess import check_call from subprocess import check_call
from tempfile import NamedTemporaryFile, mkdtemp from tempfile import NamedTemporaryFile, mkdtemp
from zipfile import ZipFile from zipfile import ZipFile
@ -344,6 +345,8 @@ class UploadUserManual(Command): # {{{
def build_plugin_example(self, path): def build_plugin_example(self, path):
from calibre import CurrentDir from calibre import CurrentDir
with NamedTemporaryFile(suffix='.zip') as f: with NamedTemporaryFile(suffix='.zip') as f:
os.fchmod(f.fileno(),
stat.S_IRUSR|stat.S_IRGRP|stat.S_IROTH|stat.S_IWRITE)
with CurrentDir(self.d(path)): with CurrentDir(self.d(path)):
with ZipFile(f, 'w') as zf: with ZipFile(f, 'w') as zf:
for x in os.listdir('.'): for x in os.listdir('.'):
@ -352,8 +355,8 @@ class UploadUserManual(Command): # {{{
for y in os.listdir(x): for y in os.listdir(x):
zf.write(os.path.join(x, y)) zf.write(os.path.join(x, y))
bname = self.b(path) + '_plugin.zip' bname = self.b(path) + '_plugin.zip'
subprocess.check_call(['scp', f.name, 'divok:%s/%s'%(DOWNLOADS, dest = '%s/%s'%(DOWNLOADS, bname)
bname)]) subprocess.check_call(['scp', f.name, 'divok:'+dest])
def run(self, opts): def run(self, opts):
path = self.j(self.SRC, 'calibre', 'manual', 'plugin_examples') path = self.j(self.SRC, 'calibre', 'manual', 'plugin_examples')

View File

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

View File

@ -10,7 +10,7 @@ Sanda library wrapper
import ctypes, uuid, hashlib, os, sys import ctypes, uuid, hashlib, os, sys
from threading import Event, Lock from threading import Event, Lock
from calibre.constants import iswindows, islinux, isosx from calibre.constants import iswindows
from calibre import load_library from calibre import load_library
try: try:
@ -29,12 +29,9 @@ try:
except: except:
lib_handle = None lib_handle = None
text_encoding = 'utf-8'
if iswindows: if iswindows:
text_encoding = 'mbcs' text_encoding = 'mbcs'
elif islinux:
text_encoding = 'utf-8'
elif isosx:
text_encoding = 'utf-8'
def is_bambook_lib_ready(): def is_bambook_lib_ready():
return lib_handle != None return lib_handle != None

View File

@ -110,4 +110,11 @@ def xml_to_unicode(raw, verbose=False, strip_encoding_pats=False,
if resolve_entities: if resolve_entities:
raw = substitute_entites(raw) raw = substitute_entites(raw)
if encoding and encoding.lower().replace('_', '-').strip() in (
'gb2312', 'chinese', 'csiso58gb231280', 'euc-cn', 'euccn',
'eucgb2312-cn', 'gb2312-1980', 'gb2312-80', 'iso-ir-58'):
# Microsoft Word exports to HTML with encoding incorrectly set to
# gb2312 instead of gbk. gbk is a superset of gb2312, anyway.
encoding = 'gbk'
return raw, encoding return raw, encoding

View File

@ -6,7 +6,7 @@ __license__ = 'GPL v3'
__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>' __copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
import os, textwrap import os, textwrap, sys
from copy import deepcopy from copy import deepcopy
from lxml import etree from lxml import etree
@ -413,7 +413,12 @@ class LRFInput(InputFormatPlugin):
('calibre', 'image-block'): image_block, ('calibre', 'image-block'): image_block,
} }
transform = etree.XSLT(styledoc, extensions=extensions) transform = etree.XSLT(styledoc, extensions=extensions)
result = transform(doc) try:
result = transform(doc)
except RuntimeError:
sys.setrecursionlimit(5000)
result = transform(doc)
with open('content.opf', 'wb') as f: with open('content.opf', 'wb') as f:
f.write(result) f.write(result)
styles.write() styles.write()

View File

@ -198,8 +198,10 @@ class Metadata(object):
return copy.deepcopy(ans) return copy.deepcopy(ans)
def _clean_identifier(self, typ, val): def _clean_identifier(self, typ, val):
typ = icu_lower(typ).strip().replace(':', '').replace(',', '') if typ:
val = val.strip().replace(',', '|').replace(':', '|') typ = icu_lower(typ).strip().replace(':', '').replace(',', '')
if val:
val = val.strip().replace(',', '|').replace(':', '|')
return typ, val return typ, val
def set_identifiers(self, identifiers): def set_identifiers(self, identifiers):

View File

@ -483,7 +483,6 @@ class Amazon(Source):
log.exception('Failed to download cover from:', cached_url) log.exception('Failed to download cover from:', cached_url)
# }}} # }}}
if __name__ == '__main__': # tests {{{ if __name__ == '__main__': # tests {{{
# To run these test use: calibre-debug -e # To run these test use: calibre-debug -e
# src/calibre/ebooks/metadata/sources/amazon.py # src/calibre/ebooks/metadata/sources/amazon.py
@ -504,7 +503,7 @@ if __name__ == '__main__': # tests {{{
( # This isbn not on amazon ( # This isbn not on amazon
{'identifiers':{'isbn': '8324616489'}, 'title':'Learning Python', {'identifiers':{'isbn': '8324616489'}, 'title':'Learning Python',
'authors':['Lutz']}, 'authors':['Lutz']},
[title_test('Learning Python: Powerful Object-Oriented Programming', [title_test('Learning Python, 3rd Edition',
exact=True), authors_test(['Mark Lutz']) exact=True), authors_test(['Mark Lutz'])
] ]

View File

@ -15,6 +15,7 @@ from calibre.customize import Plugin
from calibre.utils.logging import ThreadSafeLog, FileStream from calibre.utils.logging import ThreadSafeLog, FileStream
from calibre.utils.config import JSONConfig from calibre.utils.config import JSONConfig
from calibre.utils.titlecase import titlecase from calibre.utils.titlecase import titlecase
from calibre.ebooks.metadata import check_isbn
msprefs = JSONConfig('metadata_sources.json') msprefs = JSONConfig('metadata_sources.json')
@ -236,6 +237,7 @@ class Source(Plugin):
mi.title = fixcase(mi.title) mi.title = fixcase(mi.title)
mi.authors = list(map(fixcase, mi.authors)) mi.authors = list(map(fixcase, mi.authors))
mi.tags = list(map(fixcase, mi.tags)) mi.tags = list(map(fixcase, mi.tags))
mi.isbn = check_isbn(mi.isbn)
# }}} # }}}

View File

@ -14,6 +14,7 @@ from io import BytesIO
from calibre.customize.ui import metadata_plugins from calibre.customize.ui import metadata_plugins
from calibre.ebooks.metadata.sources.base import create_log from calibre.ebooks.metadata.sources.base import create_log
from calibre.ebooks.metadata.xisbn import xisbn
# How long to wait for more results after first result is found # How long to wait for more results after first result is found
WAIT_AFTER_FIRST_RESULT = 30 # seconds WAIT_AFTER_FIRST_RESULT = 30 # seconds
@ -42,6 +43,7 @@ def is_worker_alive(workers):
return False return False
def identify(log, abort, title=None, authors=None, identifiers=[], timeout=30): def identify(log, abort, title=None, authors=None, identifiers=[], timeout=30):
start_time = time.time()
plugins = list(metadata_plugins['identify']) plugins = list(metadata_plugins['identify'])
kwargs = { kwargs = {
@ -105,3 +107,55 @@ def identify(log, abort, title=None, authors=None, identifiers=[], timeout=30):
log(plog) log(plog)
log('\n'+'*'*80) log('\n'+'*'*80)
for i, result in enumerate(results):
result.relevance_in_source = i
result.has_cached_cover_url = \
plugin.get_cached_cover_url(result.identifiers) is not None
result.identify_plugin = plugin
log('The identify phase took %.2f seconds'%(time.time() - start_time))
log('Merging results from different sources and finding earliest',
'publication dates')
start_time = time.time()
merged_results = merge_identify_results(results, log)
log('We have %d merged results, merging took: %.2f seconds' %
(len(merged_results), time.time() - start_time))
class ISBNMerge(object):
def __init__(self):
self.pools = {}
def isbn_in_pool(self, isbn):
if isbn:
for p in self.pools:
if isbn in p:
return p
return None
def pool_has_result_from_same_source(self, pool, result):
results = self.pools[pool][1]
for r in results:
if r.identify_plugin is result.identify_plugin:
return True
return False
def add_result(self, result, isbn):
pool = self.isbn_in_pool(isbn)
if pool is None:
isbns, min_year = xisbn.get_isbn_pool(isbn)
if not isbns:
isbns = frozenset([isbn])
self.pool[isbns] = pool = (min_year, [])
if not self.pool_has_result_from_same_source(pool, result):
pool[1].append(result)
def merge_identify_results(result_map, log):
for plugin, results in result_map.iteritems():
for result in results:
isbn = result.isbn
if isbn:
isbns, min_year = xisbn.get_isbn_pool(isbn)

View File

@ -71,14 +71,28 @@ class xISBN(object):
ans.add(i) ans.add(i)
return ans return ans
def get_isbn_pool(self, isbn):
data = self.get_data(isbn)
isbns = frozenset([x.get('isbn') for x in data if 'isbn' in x])
min_year = 100000
for x in data:
try:
year = int(x['year'])
if year < min_year:
min_year = year
except:
continue
if min_year == 100000:
min_year = None
return isbns, min_year
xisbn = xISBN() xisbn = xISBN()
if __name__ == '__main__': if __name__ == '__main__':
import sys import sys, pprint
isbn = sys.argv[-1] isbn = sys.argv[-1]
print xisbn.get_data(isbn) print pprint.pprint(xisbn.get_data(isbn))
print print
print xisbn.get_associated_isbns(isbn) print xisbn.get_associated_isbns(isbn)

View File

@ -34,7 +34,7 @@ class PDFInput(InputFormatPlugin):
from calibre.ebooks.pdf.reflow import PDFDocument from calibre.ebooks.pdf.reflow import PDFDocument
if pdfreflow_err: if pdfreflow_err:
raise RuntimeError('Failed to load pdfreflow: ' + pdfreflow_err) raise RuntimeError('Failed to load pdfreflow: ' + pdfreflow_err)
pdfreflow.reflow(stream.read()) pdfreflow.reflow(stream.read(), 1, -1)
xml = open('index.xml', 'rb').read() xml = open('index.xml', 'rb').read()
PDFDocument(xml, self.opts, self.log) PDFDocument(xml, self.opts, self.log)
return os.path.join(os.getcwd(), 'metadata.opf') return os.path.join(os.getcwd(), 'metadata.opf')

View File

@ -24,13 +24,14 @@ extern "C" {
pdfreflow_reflow(PyObject *self, PyObject *args) { pdfreflow_reflow(PyObject *self, PyObject *args) {
char *pdfdata; char *pdfdata;
Py_ssize_t size; Py_ssize_t size;
int first_page, last_page, num = 0;
if (!PyArg_ParseTuple(args, "s#", &pdfdata, &size)) if (!PyArg_ParseTuple(args, "s#ii", &pdfdata, &size, &first_page, &last_page))
return NULL; return NULL;
try { try {
Reflow reflow(pdfdata, static_cast<std::ifstream::pos_type>(size)); Reflow reflow(pdfdata, static_cast<std::ifstream::pos_type>(size));
reflow.render(); num = reflow.render(first_page, last_page);
} catch (std::exception &e) { } catch (std::exception &e) {
PyErr_SetString(PyExc_RuntimeError, e.what()); return NULL; PyErr_SetString(PyExc_RuntimeError, e.what()); return NULL;
} catch (...) { } catch (...) {
@ -38,7 +39,7 @@ extern "C" {
"Unknown exception raised while rendering PDF"); return NULL; "Unknown exception raised while rendering PDF"); return NULL;
} }
Py_RETURN_NONE; return Py_BuildValue("i", num);
} }
static PyObject * static PyObject *
@ -166,8 +167,8 @@ extern "C" {
static static
PyMethodDef pdfreflow_methods[] = { PyMethodDef pdfreflow_methods[] = {
{"reflow", pdfreflow_reflow, METH_VARARGS, {"reflow", pdfreflow_reflow, METH_VARARGS,
"reflow(pdf_data)\n\n" "reflow(pdf_data, first_page, last_page)\n\n"
"Reflow the specified PDF." "Reflow the specified PDF. Returns the number of pages in the PDF. If last_page is -1 renders to end of document."
}, },
{"get_metadata", pdfreflow_get_metadata, METH_VARARGS, {"get_metadata", pdfreflow_get_metadata, METH_VARARGS,
"get_metadata(pdf_data, cover)\n\n" "get_metadata(pdf_data, cover)\n\n"

View File

@ -712,16 +712,18 @@ Reflow::Reflow(char *pdfdata, size_t sz) :
} }
void int
Reflow::render() { Reflow::render(int first_page, int last_page) {
if (!this->doc->okToCopy()) if (!this->doc->okToCopy())
cout << "Warning, this document has the copy protection flag set, ignoring." << endl; cout << "Warning, this document has the copy protection flag set, ignoring." << endl;
globalParams->setTextEncoding(encoding); globalParams->setTextEncoding(encoding);
int first_page = 1; int doc_pages = doc->getNumPages();
int last_page = doc->getNumPages(); if (last_page < 1 || last_page > doc_pages) last_page = doc_pages;
if (first_page < 1) first_page = 1;
if (first_page > last_page) first_page = last_page;
XMLOutputDev *xml_out = new XMLOutputDev(this->doc); XMLOutputDev *xml_out = new XMLOutputDev(this->doc);
doc->displayPages(xml_out, first_page, last_page, doc->displayPages(xml_out, first_page, last_page,
@ -733,9 +735,12 @@ Reflow::render() {
false //Printing false //Printing
); );
this->dump_outline(); if (last_page - first_page == doc_pages - 1)
this->dump_outline();
delete xml_out; delete xml_out;
return doc_pages;
} }
void Reflow::dump_outline() { void Reflow::dump_outline() {

View File

@ -66,7 +66,7 @@ class Reflow {
~Reflow(); ~Reflow();
/* Convert the PDF to XML. All files are output to the current directory */ /* Convert the PDF to XML. All files are output to the current directory */
void render(); int render(int first_page, int last_page);
/* Get the PDF Info Dictionary */ /* Get the PDF Info Dictionary */
map<string, string> get_info(); map<string, string> get_info();

View File

@ -51,7 +51,7 @@ class ConvertAction(InterfaceAction):
self.queue_convert_jobs(jobs, changed, bad, rows, previous, self.queue_convert_jobs(jobs, changed, bad, rows, previous,
self.book_auto_converted, extra_job_args=[on_card]) self.book_auto_converted, extra_job_args=[on_card])
def auto_convert_mail(self, to, fmts, delete_from_library, book_ids, format): def auto_convert_mail(self, to, fmts, delete_from_library, book_ids, format, subject):
previous = self.gui.library_view.currentIndex() previous = self.gui.library_view.currentIndex()
rows = [x.row() for x in \ rows = [x.row() for x in \
self.gui.library_view.selectionModel().selectedRows()] self.gui.library_view.selectionModel().selectedRows()]
@ -59,7 +59,7 @@ class ConvertAction(InterfaceAction):
if jobs == []: return if jobs == []: return
self.queue_convert_jobs(jobs, changed, bad, rows, previous, self.queue_convert_jobs(jobs, changed, bad, rows, previous,
self.book_auto_converted_mail, self.book_auto_converted_mail,
extra_job_args=[delete_from_library, to, fmts]) extra_job_args=[delete_from_library, to, fmts, subject])
def auto_convert_news(self, book_ids, format): def auto_convert_news(self, book_ids, format):
previous = self.gui.library_view.currentIndex() previous = self.gui.library_view.currentIndex()
@ -145,9 +145,10 @@ class ConvertAction(InterfaceAction):
self.gui.sync_to_device(on_card, False, specific_format=fmt, send_ids=[book_id], do_auto_convert=False) self.gui.sync_to_device(on_card, False, specific_format=fmt, send_ids=[book_id], do_auto_convert=False)
def book_auto_converted_mail(self, job): def book_auto_converted_mail(self, job):
temp_files, fmt, book_id, delete_from_library, to, fmts = self.conversion_jobs[job] temp_files, fmt, book_id, delete_from_library, to, fmts, subject = self.conversion_jobs[job]
self.book_converted(job) self.book_converted(job)
self.gui.send_by_mail(to, fmts, delete_from_library, specific_format=fmt, send_ids=[book_id], do_auto_convert=False) self.gui.send_by_mail(to, fmts, delete_from_library, subject=subject,
specific_format=fmt, send_ids=[book_id], do_auto_convert=False)
def book_auto_converted_news(self, job): def book_auto_converted_news(self, job):
temp_files, fmt, book_id = self.conversion_jobs[job] temp_files, fmt, book_id = self.conversion_jobs[job]

View File

@ -82,7 +82,8 @@ class ShareConnMenu(QMenu): # {{{
keys = sorted(opts.accounts.keys()) keys = sorted(opts.accounts.keys())
for account in keys: for account in keys:
formats, auto, default = opts.accounts[account] formats, auto, default = opts.accounts[account]
dest = 'mail:'+account+';'+formats subject = opts.subjects.get(account, '')
dest = 'mail:'+account+';'+formats+';'+subject
action1 = DeviceAction(dest, False, False, I('mail.png'), action1 = DeviceAction(dest, False, False, I('mail.png'),
account) account)
action2 = DeviceAction(dest, True, False, I('mail.png'), action2 = DeviceAction(dest, True, False, I('mail.png'),

View File

@ -5,6 +5,8 @@ __license__ = 'GPL v3'
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>' __copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
import gc
from PyQt4.Qt import Qt from PyQt4.Qt import Qt
from calibre.gui2 import Dispatcher from calibre.gui2 import Dispatcher
@ -53,11 +55,11 @@ class FetchNewsAction(InterfaceAction):
def scheduled_recipe_fetched(self, job): def scheduled_recipe_fetched(self, job):
temp_files, fmt, arg = self.conversion_jobs.pop(job) temp_files, fmt, arg = self.conversion_jobs.pop(job)
pt = temp_files[0] fname = temp_files[0].name
if job.failed: if job.failed:
self.scheduler.recipe_download_failed(arg) self.scheduler.recipe_download_failed(arg)
return self.gui.job_exception(job) return self.gui.job_exception(job)
id = self.gui.library_view.model().add_news(pt.name, arg) id = self.gui.library_view.model().add_news(fname, arg)
# Arg may contain a "keep_issues" variable. If it is non-zero, # Arg may contain a "keep_issues" variable. If it is non-zero,
# delete all but newest x issues. # delete all but newest x issues.
@ -81,5 +83,6 @@ class FetchNewsAction(InterfaceAction):
self.gui.status_bar.show_message(arg['title'] + _(' fetched.'), 3000) self.gui.status_bar.show_message(arg['title'] + _(' fetched.'), 3000)
self.gui.email_news(id) self.gui.email_news(id)
self.gui.sync_news() self.gui.sync_news()
gc.collect()

View File

@ -887,9 +887,14 @@ class DeviceMixin(object): # {{{
on_card = dest on_card = dest
self.sync_to_device(on_card, delete, fmt) self.sync_to_device(on_card, delete, fmt)
elif dest == 'mail': elif dest == 'mail':
to, fmts = sub_dest.split(';') sub_dest_parts = sub_dest.split(';')
while len(sub_dest_parts) < 3:
sub_dest_parts.append('')
to = sub_dest_parts[0]
fmts = sub_dest_parts[1]
subject = ';'.join(sub_dest_parts[2:])
fmts = [x.strip().lower() for x in fmts.split(',')] fmts = [x.strip().lower() for x in fmts.split(',')]
self.send_by_mail(to, fmts, delete) self.send_by_mail(to, fmts, delete, subject=subject)
def cover_to_thumbnail(self, data): def cover_to_thumbnail(self, data):
if self.device_manager.device and \ if self.device_manager.device and \

View File

@ -22,6 +22,7 @@ from calibre.customize.ui import available_input_formats, available_output_forma
from calibre.ebooks.metadata import authors_to_string from calibre.ebooks.metadata import authors_to_string
from calibre.constants import preferred_encoding from calibre.constants import preferred_encoding
from calibre.gui2 import config, Dispatcher, warning_dialog from calibre.gui2 import config, Dispatcher, warning_dialog
from calibre.library.save_to_disk import get_components
from calibre.utils.config import tweaks from calibre.utils.config import tweaks
class EmailJob(BaseJob): # {{{ class EmailJob(BaseJob): # {{{
@ -210,7 +211,7 @@ class EmailMixin(object): # {{{
def __init__(self): def __init__(self):
self.emailer = Emailer(self.job_manager) self.emailer = Emailer(self.job_manager)
def send_by_mail(self, to, fmts, delete_from_library, send_ids=None, def send_by_mail(self, to, fmts, delete_from_library, subject='', send_ids=None,
do_auto_convert=True, specific_format=None): do_auto_convert=True, specific_format=None):
ids = [self.library_view.model().id(r) for r in self.library_view.selectionModel().selectedRows()] if send_ids is None else send_ids ids = [self.library_view.model().id(r) for r in self.library_view.selectionModel().selectedRows()] if send_ids is None else send_ids
if not ids or len(ids) == 0: if not ids or len(ids) == 0:
@ -239,7 +240,14 @@ class EmailMixin(object): # {{{
remove_ids.append(id) remove_ids.append(id)
jobnames.append(t) jobnames.append(t)
attachments.append(f) attachments.append(f)
subjects.append(_('E-book:')+ ' '+t) if not subject:
subjects.append(_('E-book:')+ ' '+t)
else:
components = get_components(subject, mi, id)
if not components:
components = [mi.title]
subject = os.path.join(*components)
subjects.append(subject)
a = authors_to_string(mi.authors if mi.authors else \ a = authors_to_string(mi.authors if mi.authors else \
[_('Unknown')]) [_('Unknown')])
texts.append(_('Attached, you will find the e-book') + \ texts.append(_('Attached, you will find the e-book') + \
@ -292,7 +300,7 @@ class EmailMixin(object): # {{{
if self.auto_convert_question( if self.auto_convert_question(
_('Auto convert the following books before sending via ' _('Auto convert the following books before sending via '
'email?'), autos): 'email?'), autos):
self.iactions['Convert Books'].auto_convert_mail(to, fmts, delete_from_library, auto, format) self.iactions['Convert Books'].auto_convert_mail(to, fmts, delete_from_library, auto, format, subject)
if bad: if bad:
bad = '\n'.join('%s'%(i,) for i in bad) bad = '\n'.join('%s'%(i,) for i in bad)

View File

@ -7,7 +7,6 @@ __docformat__ = 'restructuredtext en'
import shutil, functools, re, os, traceback import shutil, functools, re, os, traceback
from contextlib import closing from contextlib import closing
from operator import attrgetter
from PyQt4.Qt import QAbstractTableModel, Qt, pyqtSignal, QIcon, QImage, \ from PyQt4.Qt import QAbstractTableModel, Qt, pyqtSignal, QIcon, QImage, \
QModelIndex, QVariant, QDate, QColor QModelIndex, QVariant, QDate, QColor
@ -18,7 +17,7 @@ from calibre.ebooks.metadata import fmt_sidx, authors_to_string, string_to_autho
from calibre.ptempfile import PersistentTemporaryFile from calibre.ptempfile import PersistentTemporaryFile
from calibre.utils.config import tweaks, prefs from calibre.utils.config import tweaks, prefs
from calibre.utils.date import dt_factory, qt_to_dt, isoformat from calibre.utils.date import dt_factory, qt_to_dt, isoformat
from calibre.utils.icu import sort_key, strcmp as icu_strcmp from calibre.utils.icu import sort_key
from calibre.ebooks.metadata.meta import set_metadata as _set_metadata from calibre.ebooks.metadata.meta import set_metadata as _set_metadata
from calibre.utils.search_query_parser import SearchQueryParser from calibre.utils.search_query_parser import SearchQueryParser
from calibre.library.caches import _match, CONTAINS_MATCH, EQUALS_MATCH, \ from calibre.library.caches import _match, CONTAINS_MATCH, EQUALS_MATCH, \
@ -984,6 +983,21 @@ class OnDeviceSearch(SearchQueryParser): # {{{
# }}} # }}}
class DeviceDBSortKeyGen(object): # {{{
def __init__(self, attr, keyfunc, db):
self.attr = attr
self.db = db
self.keyfunc = keyfunc
def __call__(self, x):
try:
ans = self.keyfunc(getattr(self.db[x], self.attr))
except:
ans = None
return ans
# }}}
class DeviceBooksModel(BooksModel): # {{{ class DeviceBooksModel(BooksModel): # {{{
booklist_dirtied = pyqtSignal() booklist_dirtied = pyqtSignal()
@ -1089,59 +1103,40 @@ class DeviceBooksModel(BooksModel): # {{{
def sort(self, col, order, reset=True): def sort(self, col, order, reset=True):
descending = order != Qt.AscendingOrder descending = order != Qt.AscendingOrder
def strcmp(attr):
ag = attrgetter(attr)
def _strcmp(x, y):
x = ag(self.db[x])
y = ag(self.db[y])
if x == None:
x = ''
if y == None:
y = ''
return icu_strcmp(x.strip(), y.strip())
return _strcmp
def datecmp(x, y):
x = self.db[x].datetime
y = self.db[y].datetime
return cmp(dt_factory(x, assume_utc=True), dt_factory(y,
assume_utc=True))
def sizecmp(x, y):
x, y = int(self.db[x].size), int(self.db[y].size)
return cmp(x, y)
def tagscmp(x, y):
x = ','.join(sorted(getattr(self.db[x], 'device_collections', []),key=sort_key))
y = ','.join(sorted(getattr(self.db[y], 'device_collections', []),key=sort_key))
return cmp(x, y)
def libcmp(x, y):
x, y = self.db[x].in_library, self.db[y].in_library
return cmp(x, y)
def authorcmp(x, y):
ax = getattr(self.db[x], 'author_sort', None)
ay = getattr(self.db[y], 'author_sort', None)
if ax and ay:
x = ax
y = ay
else:
x, y = authors_to_string(self.db[x].authors), \
authors_to_string(self.db[y].authors)
return cmp(x, y)
cname = self.column_map[col] cname = self.column_map[col]
fcmp = { def author_key(x):
'title': strcmp('title_sorter'), try:
'authors' : authorcmp, ax = self.db[x].author_sort
'size' : sizecmp, if not ax:
'timestamp': datecmp, raise Exception('')
'collections': tagscmp, except:
'inlibrary': libcmp, try:
ax = authors_to_string(self.db[x].authors)
except:
ax = ''
return ax
keygen = {
'title': ('title_sorter', lambda x: sort_key(x) if x else ''),
'authors' : author_key,
'size' : ('size', int),
'timestamp': ('datetime', functools.partial(dt_factory, assume_utc=True)),
'collections': ('device_collections', lambda x:sorted(x,
key=sort_key)),
'inlibrary': ('in_library', lambda x: x),
}[cname] }[cname]
self.map.sort(cmp=fcmp, reverse=descending) keygen = keygen if callable(keygen) else DeviceDBSortKeyGen(
keygen[0], keygen[1], self.db)
self.map.sort(key=keygen, reverse=descending)
if len(self.map) == len(self.db): if len(self.map) == len(self.db):
self.sorted_map = list(self.map) self.sorted_map = list(self.map)
else: else:
self.sorted_map = list(range(len(self.db))) self.sorted_map = list(range(len(self.db)))
self.sorted_map.sort(cmp=fcmp, reverse=descending) self.sorted_map.sort(key=keygen, reverse=descending)
self.sorted_on = (self.column_map[col], order) self.sorted_on = (self.column_map[col], order)
self.sort_history.insert(0, self.sorted_on) self.sort_history.insert(0, self.sorted_on)
if hasattr(keygen, 'db'):
keygen.db = None
if reset: if reset:
self.reset() self.reset()

View File

@ -5,6 +5,8 @@ __license__ = 'GPL v3'
__copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>' __copyright__ = '2010, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
import textwrap
from PyQt4.Qt import QAbstractTableModel, QVariant, QFont, Qt from PyQt4.Qt import QAbstractTableModel, QVariant, QFont, Qt
@ -17,25 +19,30 @@ from calibre.utils.smtp import config as smtp_prefs
class EmailAccounts(QAbstractTableModel): # {{{ class EmailAccounts(QAbstractTableModel): # {{{
def __init__(self, accounts): def __init__(self, accounts, subjects):
QAbstractTableModel.__init__(self) QAbstractTableModel.__init__(self)
self.accounts = accounts self.accounts = accounts
self.subjects = subjects
self.account_order = sorted(self.accounts.keys()) self.account_order = sorted(self.accounts.keys())
self.headers = map(QVariant, [_('Email'), _('Formats'), _('Auto send')]) self.headers = map(QVariant, [_('Email'), _('Formats'), _('Subject'), _('Auto send')])
self.default_font = QFont() self.default_font = QFont()
self.default_font.setBold(True) self.default_font.setBold(True)
self.default_font = QVariant(self.default_font) self.default_font = QVariant(self.default_font)
self.tooltips =[NONE] + map(QVariant, self.tooltips =[NONE] + list(map(QVariant, map(textwrap.fill,
[_('Formats to email. The first matching format will be sent.'), [_('Formats to email. The first matching format will be sent.'),
_('Subject of the email to use when sending. When left blank '
'the title will be used for the subject. Also, the same '
'templates used for "Save to disk" such as {title} and '
'{author_sort} can be used here.'),
'<p>'+_('If checked, downloaded news will be automatically ' '<p>'+_('If checked, downloaded news will be automatically '
'mailed <br>to this email address ' 'mailed <br>to this email address '
'(provided it is in one of the listed formats).')]) '(provided it is in one of the listed formats).')])))
def rowCount(self, *args): def rowCount(self, *args):
return len(self.account_order) return len(self.account_order)
def columnCount(self, *args): def columnCount(self, *args):
return 3 return len(self.headers)
def headerData(self, section, orientation, role): def headerData(self, section, orientation, role):
if role == Qt.DisplayRole and orientation == Qt.Horizontal: if role == Qt.DisplayRole and orientation == Qt.Horizontal:
@ -56,14 +63,16 @@ class EmailAccounts(QAbstractTableModel): # {{{
return QVariant(account) return QVariant(account)
if col == 1: if col == 1:
return QVariant(self.accounts[account][0]) return QVariant(self.accounts[account][0])
if col == 2:
return QVariant(self.subjects.get(account, ''))
if role == Qt.FontRole and self.accounts[account][2]: if role == Qt.FontRole and self.accounts[account][2]:
return self.default_font return self.default_font
if role == Qt.CheckStateRole and col == 2: if role == Qt.CheckStateRole and col == 3:
return QVariant(Qt.Checked if self.accounts[account][1] else Qt.Unchecked) return QVariant(Qt.Checked if self.accounts[account][1] else Qt.Unchecked)
return NONE return NONE
def flags(self, index): def flags(self, index):
if index.column() == 2: if index.column() == 3:
return QAbstractTableModel.flags(self, index)|Qt.ItemIsUserCheckable return QAbstractTableModel.flags(self, index)|Qt.ItemIsUserCheckable
else: else:
return QAbstractTableModel.flags(self, index)|Qt.ItemIsEditable return QAbstractTableModel.flags(self, index)|Qt.ItemIsEditable
@ -73,8 +82,10 @@ class EmailAccounts(QAbstractTableModel): # {{{
return False return False
row, col = index.row(), index.column() row, col = index.row(), index.column()
account = self.account_order[row] account = self.account_order[row]
if col == 2: if col == 3:
self.accounts[account][1] ^= True self.accounts[account][1] ^= True
if col == 2:
self.subjects[account] = unicode(value.toString())
elif col == 1: elif col == 1:
self.accounts[account][0] = unicode(value.toString()).upper() self.accounts[account][0] = unicode(value.toString()).upper()
else: else:
@ -143,7 +154,7 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
self.send_email_widget.initialize(self.preferred_to_address) self.send_email_widget.initialize(self.preferred_to_address)
self.send_email_widget.changed_signal.connect(self.changed_signal.emit) self.send_email_widget.changed_signal.connect(self.changed_signal.emit)
opts = self.send_email_widget.smtp_opts opts = self.send_email_widget.smtp_opts
self._email_accounts = EmailAccounts(opts.accounts) self._email_accounts = EmailAccounts(opts.accounts, opts.subjects)
self._email_accounts.dataChanged.connect(lambda x,y: self._email_accounts.dataChanged.connect(lambda x,y:
self.changed_signal.emit()) self.changed_signal.emit())
self.email_view.setModel(self._email_accounts) self.email_view.setModel(self._email_accounts)
@ -170,6 +181,7 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
if not self.send_email_widget.set_email_settings(to_set): if not self.send_email_widget.set_email_settings(to_set):
raise AbortCommit('abort') raise AbortCommit('abort')
self.proxy['accounts'] = self._email_accounts.accounts self.proxy['accounts'] = self._email_accounts.accounts
self.proxy['subjects'] = self._email_accounts.subjects
return ConfigWidgetBase.commit(self) return ConfigWidgetBase.commit(self)

View File

@ -109,7 +109,7 @@ class SearchBox2(QComboBox): # {{{
def normalize_state(self): def normalize_state(self):
self.setToolTip(self.tool_tip_text) self.setToolTip(self.tool_tip_text)
self.line_edit.setStyleSheet( self.line_edit.setStyleSheet(
'QLineEdit{color:black;background-color:%s;}' % self.normal_background) 'QLineEdit{color:none;background-color:%s;}' % self.normal_background)
def text(self): def text(self):
return self.currentText() return self.currentText()

View File

@ -171,10 +171,11 @@ class Document(QWebPage): # {{{
self.misc_config() self.misc_config()
self.after_load() self.after_load()
def __init__(self, shortcuts, parent=None): def __init__(self, shortcuts, parent=None, resize_callback=lambda: None):
QWebPage.__init__(self, parent) QWebPage.__init__(self, parent)
self.setObjectName("py_bridge") self.setObjectName("py_bridge")
self.debug_javascript = False self.debug_javascript = False
self.resize_callback = resize_callback
self.current_language = None self.current_language = None
self.loaded_javascript = False self.loaded_javascript = False
@ -237,6 +238,12 @@ class Document(QWebPage): # {{{
if self.loaded_javascript: if self.loaded_javascript:
return return
self.loaded_javascript = True self.loaded_javascript = True
self.javascript(
'''
window.onresize = function(event) {
window.py_bridge.window_resized();
}
''')
if jquery is None: if jquery is None:
jquery = P('content_server/jquery.js', data=True) jquery = P('content_server/jquery.js', data=True)
self.javascript(jquery) self.javascript(jquery)
@ -298,6 +305,10 @@ class Document(QWebPage): # {{{
def debug(self, msg): def debug(self, msg):
prints(msg) prints(msg)
@pyqtSignature('')
def window_resized(self):
self.resize_callback()
def reference_mode(self, enable): def reference_mode(self, enable):
self.javascript(('enter' if enable else 'leave')+'_reference_mode()') self.javascript(('enter' if enable else 'leave')+'_reference_mode()')
@ -424,12 +435,19 @@ class Document(QWebPage): # {{{
def xpos(self): def xpos(self):
return self.mainFrame().scrollPosition().x() return self.mainFrame().scrollPosition().x()
@property @dynamic_property
def scroll_fraction(self): def scroll_fraction(self):
try: def fget(self):
return float(self.ypos)/(self.height-self.window_height) try:
except ZeroDivisionError: return float(self.ypos)/(self.height-self.window_height)
return 0. except ZeroDivisionError:
return 0.
def fset(self, val):
npos = val * (self.height - self.window_height)
if npos < 0:
npos = 0
self.scroll_to(x=self.xpos, y=npos)
return property(fget=fget, fset=fset)
@property @property
def hscroll_fraction(self): def hscroll_fraction(self):
@ -493,7 +511,8 @@ class DocumentView(QWebView): # {{{
self._size_hint = QSize(510, 680) self._size_hint = QSize(510, 680)
self.initial_pos = 0.0 self.initial_pos = 0.0
self.to_bottom = False self.to_bottom = False
self.document = Document(self.shortcuts, parent=self) self.document = Document(self.shortcuts, parent=self,
resize_callback=self.viewport_resized)
self.setPage(self.document) self.setPage(self.document)
self.manager = None self.manager = None
self._reference_mode = False self._reference_mode = False
@ -630,9 +649,13 @@ class DocumentView(QWebView): # {{{
def sizeHint(self): def sizeHint(self):
return self._size_hint return self._size_hint
@property @dynamic_property
def scroll_fraction(self): def scroll_fraction(self):
return self.document.scroll_fraction def fget(self):
return self.document.scroll_fraction
def fset(self, val):
self.document.scroll_fraction = float(val)
return property(fget=fget, fset=fset)
@property @property
def hscroll_fraction(self): def hscroll_fraction(self):
@ -968,9 +991,11 @@ class DocumentView(QWebView): # {{{
def resizeEvent(self, event): def resizeEvent(self, event):
ret = QWebView.resizeEvent(self, event) ret = QWebView.resizeEvent(self, event)
QTimer.singleShot(10, self.initialize_scrollbar) QTimer.singleShot(10, self.initialize_scrollbar)
return ret
def viewport_resized(self):
if self.manager is not None: if self.manager is not None:
self.manager.viewport_resized(self.scroll_fraction) self.manager.viewport_resized(self.scroll_fraction)
return ret
def event(self, ev): def event(self, ev):
typ = ev.type() typ = ev.type()

View File

@ -240,7 +240,7 @@ class EbookViewer(MainWindow, Ui_EbookViewer):
self.connect(self.action_reference_mode, SIGNAL('triggered(bool)'), self.connect(self.action_reference_mode, SIGNAL('triggered(bool)'),
lambda x: self.view.reference_mode(x)) lambda x: self.view.reference_mode(x))
self.connect(self.action_metadata, SIGNAL('triggered(bool)'), lambda x:self.metadata.setVisible(x)) self.connect(self.action_metadata, SIGNAL('triggered(bool)'), lambda x:self.metadata.setVisible(x))
self.connect(self.action_table_of_contents, SIGNAL('toggled(bool)'), lambda x:self.toc.setVisible(x)) self.action_table_of_contents.toggled[bool].connect(self.set_toc_visible)
self.connect(self.action_copy, SIGNAL('triggered(bool)'), self.copy) self.connect(self.action_copy, SIGNAL('triggered(bool)'), self.copy)
self.connect(self.action_font_size_larger, SIGNAL('triggered(bool)'), self.connect(self.action_font_size_larger, SIGNAL('triggered(bool)'),
self.font_size_larger) self.font_size_larger)
@ -310,6 +310,9 @@ class EbookViewer(MainWindow, Ui_EbookViewer):
self.restore_state() self.restore_state()
def set_toc_visible(self, yes):
self.toc.setVisible(yes)
def clear_recent_history(self, *args): def clear_recent_history(self, *args):
vprefs.set('viewer_open_history', []) vprefs.set('viewer_open_history', [])
self.build_recent_menu() self.build_recent_menu()

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

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

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

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