This commit is contained in:
GRiker 2011-04-08 13:34:22 -06:00
commit 99af4d355c
150 changed files with 42398 additions and 35010 deletions

View File

@ -19,18 +19,77 @@
# new recipes: # new recipes:
# - title: # - title:
- version: 0.7.54
date: 2011-04-08
new features:
- title: "New output format, HTMLZ which is a single HTML file with its associated images/stylesheets in a zipped up file"
description: "Useful when you want to convert your ebook into a single HTML file for easy editing. Note that this output plugin is still new and needs testing"
- title: "When dealing with ZIP/RAR archives, use the file header rather than the file extension to detrmine the file type, when possible. This fixes the common case of CBZ files being actually cbr files and vice versa"
- title: "Support for the Motorola Atrix"
- title: "Allow the icons in the toolbar to be turned off completely via Preferences->Look & Feel"
- title: "When downloading metadata use the gzip transfer encoding when possible for a speedup."
tickets: [749304]
bug fixes:
- title: "Conversion pipeline: Workaround for bug in lxml that causes a massive mem leak on windows and OS X when the input document contains non ASCII CSS selectors."
tickets: [754555]
- title: "Conversion pipeline: Handle inline <style> tags that put all the actual CSS inside an XML comment."
tickets: [750063]
- title: "The 'Choose Library' button now shows its popup menu when you already have more than one library instead of the dialog to create a new library"
tickets: [754154]
- title: "Apply all content server setting when clicking the Start Server button in Preferences->Sharing over the net"
tickets: [753122]
- title: "Fix content server breaking if its restriction is set to a saved search that was deleted"
tickets: [751950]
- title: "Fix detection of PocketBook with 2.0.6 firmware on windows"
tickets: [750336]
- title: "ODT Input: Fix handling of the <text:s> element."
tickets: [749655]
- title: "MOBI Output: Don't use self closed tags"
- title: "Fix book details popup becoming too tall if there is a lot of metadata"
- title: "Fix new PDF engine crashing on PDF files with embedded fonts with null names"
improved recipes:
- Kommersant
- Perfil
- Times of India
- IHT
- Guardian
new recipes:
- title: "Al Ahram"
authors: Hassan Williamson
- title: "F-Secure and developpez.com"
authors: louhike
- version: 0.7.53 - version: 0.7.53
date: 2011-04-01 date: 2011-04-01
new features: 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." - 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 you can specift the title/authors/series/whatever in the template."
tickets: [743535] 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: "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: "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: "EPUB Output: Remove any margins specified via an Adobe page template in the input document. This means that the margins specified 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" - title: "When reading metadata from filenames, allow publisher and published date to be read from the filename"
tickets: [744020] tickets: [744020]
@ -49,6 +108,8 @@
- title: "FB2 Output: Option to set the FB2 genre explicitly." - title: "FB2 Output: Option to set the FB2 genre explicitly."
tickets: [743178] tickets: [743178]
- title: "Plugin developers: calibre now has a new plugin API, see http://calibre-ebook.com/user_manual/creating_plugins.html. Your existing plugins should continue to work, but it would be good to test them to make sure."
bug fixes: bug fixes:
- title: "Fix text color in the search bar set to black instead of the system font color" - title: "Fix text color in the search bar set to black instead of the system font color"

View File

@ -1,6 +1,9 @@
calibre supports installation from source, only on Linux. calibre supports installation from source, only on Linux.
On Windows and OS X use the provided installers and use
the facilities of the calibre-debug command to hack on the calibre source. Note that you *do not* need to install from source to hack on
the calibre source code. To get started with calibre development,
use a normal calibre install and follow the instructions at
http://calibre-ebook.com/user_manual/develop.html
On Linux, there are two kinds of installation from source possible. On Linux, there are two kinds of installation from source possible.
Note that both kinds require lots of dependencies as well as a Note that both kinds require lots of dependencies as well as a
@ -45,3 +48,4 @@ This type of install can be run with the command::
sudo python setup.py develop sudo python setup.py develop
Use the -h flag for help on the develop command. Use the -h flag for help on the develop command.

2
README
View File

@ -7,7 +7,7 @@ reading. It is cross platform, running on Linux, Windows and OS X.
For screenshots: https://calibre-ebook.com/demo For screenshots: https://calibre-ebook.com/demo
For installation/usage instructions please see For installation/usage instructions please see
http://calibre-ebook.com http://calibre-ebook.com/user_manual
For source code access: For source code access:
bzr branch lp:calibre bzr branch lp:calibre

62
recipes/al_ahram.recipe Normal file
View File

@ -0,0 +1,62 @@
# coding=utf-8
__license__ = 'GPL v3'
__copyright__ = '2011, Hassan Williamson <haz at hazrpg.co.uk>'
'''
ahram.org.eg
'''
from calibre.web.feeds.recipes import BasicNewsRecipe
class AlAhram(BasicNewsRecipe):
title = 'Al-Ahram'
__author__ = 'Hassan Williamson'
description = 'News from Egypt in Arabic.'
oldest_article = 7
max_articles_per_feed = 100
no_stylesheets = True
#delay = 1
use_embedded_content = False
encoding = 'utf8'
publisher = 'Al-Ahram'
category = 'News'
language = 'ar'
publication_type = 'newsportal'
extra_css = ' body{ font-family: Verdana,Helvetica,Arial,sans-serif; direction: rtl; } .txtTitle{ font-weight: bold; } '
keep_only_tags = [
dict(name='div', attrs={'class':['bbcolright']})
]
remove_tags = [
dict(name='div', attrs={'class':['bbnav', 'bbsp']}),
dict(name='div', attrs={'id':['AddThisButton']})
]
remove_attributes = [
'width','height'
]
feeds = [
(u'الأولى', 'http://www.ahram.org.eg/RssXml.aspx?CategoryID=25'),
(u'مصر', 'http://www.ahram.org.eg/RssXml.aspx?CategoryID=27'),
(u'المحافظات', 'http://www.ahram.org.eg/RssXml.aspx?CategoryID=29'),
(u'الوطن العربي', 'http://www.ahram.org.eg/RssXml.aspx?CategoryID=31'),
(u'العالم', 'http://www.ahram.org.eg/RssXml.aspx?CategoryID=26'),
(u'تقارير المراسلين', 'http://www.ahram.org.eg/RssXml.aspx?CategoryID=2'),
(u'تحقيقات', 'http://www.ahram.org.eg/RssXml.aspx?CategoryID=3'),
(u'قضايا واراء', 'http://www.ahram.org.eg/RssXml.aspx?CategoryID=4'),
(u'اقتصاد', 'http://www.ahram.org.eg/RssXml.aspx?CategoryID=5'),
(u'رياضة', 'http://www.ahram.org.eg/RssXml.aspx?CategoryID=6'),
(u'حوادث', 'http://www.ahram.org.eg/RssXml.aspx?CategoryID=38'),
(u'دنيا الثقافة', 'http://www.ahram.org.eg/RssXml.aspx?CategoryID=7'),
(u'المراة والطفل', 'http://www.ahram.org.eg/RssXml.aspx?CategoryID=8'),
(u'يوم جديد', 'http://www.ahram.org.eg/RssXml.aspx?CategoryID=9'),
(u'الكتاب', 'http://www.ahram.org.eg/RssXml.aspx?CategoryID=10'),
(u'الاعمدة', 'http://www.ahram.org.eg/RssXml.aspx?CategoryID=11'),
(u'أراء حرة', 'http://www.ahram.org.eg/RssXml.aspx?CategoryID=59'),
(u'ملفات الاهرام', 'http://www.ahram.org.eg/RssXml.aspx?CategoryID=12'),
(u'بريد الاهرام', 'http://www.ahram.org.eg/RssXml.aspx?CategoryID=15'),
(u'الاخيرة', 'http://www.ahram.org.eg/RssXml.aspx?CategoryID=16'),
]

21
recipes/developpez.recipe Normal file
View File

@ -0,0 +1,21 @@
from calibre.web.feeds.news import BasicNewsRecipe
class AdvancedUserRecipe1301849956(BasicNewsRecipe):
title = u'Developpez.com'
description = u'Toutes les news du site Developpez.com'
publisher = u'Developpez.com'
timefmt = ' [%a, %d %b, %Y]'
oldest_article = 7
max_articles_per_feed = 100
no_stylesheets = True
encoding = 'ISO-8859-1'
language = 'fr'
__author__ = 'louhike'
remove_javascript = True
keep_only_tags = [dict(name='div', attrs={'class':'content'})]
feeds = [(u'Tous les articles', u'http://www.developpez.com/index/rss')]
def get_cover_url(self):
return 'http://javascript.developpez.com/template/images/logo.gif'

View File

@ -18,7 +18,8 @@ class Economist(BasicNewsRecipe):
__author__ = "Kovid Goyal" __author__ = "Kovid Goyal"
INDEX = 'http://www.economist.com/printedition' INDEX = 'http://www.economist.com/printedition'
description = 'Global news and current affairs from a European perspective.' description = ('Global news and current affairs from a European'
' perspective. Best downloaded on Friday mornings (GMT)')
oldest_article = 7.0 oldest_article = 7.0
cover_url = 'http://www.economist.com/images/covers/currentcoverus_large.jpg' cover_url = 'http://www.economist.com/images/covers/currentcoverus_large.jpg'

View File

@ -11,7 +11,8 @@ class Economist(BasicNewsRecipe):
language = 'en' language = 'en'
__author__ = "Kovid Goyal" __author__ = "Kovid Goyal"
description = ('Global news and current affairs from a European perspective.' description = ('Global news and current affairs from a European'
' perspective. Best downloaded on Friday mornings (GMT).'
' Much slower than the print edition based version.') ' Much slower than the print edition based version.')
oldest_article = 7.0 oldest_article = 7.0

22
recipes/f_secure.recipe Normal file
View File

@ -0,0 +1,22 @@
from calibre.web.feeds.news import BasicNewsRecipe
class AdvancedUserRecipe1301860159(BasicNewsRecipe):
title = u'F-Secure Weblog'
language = 'en'
__author__ = 'louhike'
description = u'All the news from the weblog of F-Secure'
publisher = u'F-Secure'
timefmt = ' [%a, %d %b, %Y]'
encoding = 'ISO-8859-1'
oldest_article = 7
max_articles_per_feed = 100
no_stylesheets = True
use_embedded_content = False
language = 'en_EN'
remove_javascript = True
keep_only_tags = [dict(name='div', attrs={'class':'modSectionTd2'})]
remove_tags = [dict(name='a'),dict(name='hr')]
feeds = [(u'Weblog', u'http://www.f-secure.com/weblog/weblog.rss')]
def get_cover_url(self):
return 'http://www.f-secure.com/weblog/archives/images/company_logo.png'

View File

@ -11,7 +11,8 @@ from calibre.web.feeds.news import BasicNewsRecipe
class FinancialTimes(BasicNewsRecipe): class FinancialTimes(BasicNewsRecipe):
title = u'Financial Times' title = u'Financial Times'
__author__ = 'Darko Miletic and Sujata Raman' __author__ = 'Darko Miletic and Sujata Raman'
description = 'Financial world news' description = ('Financial world news. Available after 5AM '
'GMT, daily.')
oldest_article = 2 oldest_article = 2
language = 'en' language = 'en'

View File

@ -35,8 +35,8 @@ class AdvancedUserRecipe1287083651(BasicNewsRecipe):
(u'Arts', u'http://www.theglobeandmail.com/news/arts/?service=rss'), (u'Arts', u'http://www.theglobeandmail.com/news/arts/?service=rss'),
(u'Life', u'http://www.theglobeandmail.com/life/?service=rss'), (u'Life', u'http://www.theglobeandmail.com/life/?service=rss'),
(u'Real Estate', u'http://www.theglobeandmail.com/real-estate/?service=rss'), (u'Real Estate', u'http://www.theglobeandmail.com/real-estate/?service=rss'),
(u'Auto', u'http://www.theglobeandmail.com/sports/?service=rss'), (u'Sports', u'http://www.theglobeandmail.com/sports/?service=rss'),
(u'Sports', u'http://www.theglobeandmail.com/auto/?service=rss') (u'Drive', u'http://www.theglobeandmail.com/auto/?service=rss')
] ]
preprocess_regexps = [ preprocess_regexps = [

View File

@ -36,6 +36,7 @@ class Guardian(BasicNewsRecipe):
remove_tags = [ remove_tags = [
dict(name='div', attrs={'class':["video-content","videos-third-column"]}), dict(name='div', attrs={'class':["video-content","videos-third-column"]}),
dict(name='div', attrs={'id':["article-toolbox","subscribe-feeds",]}), dict(name='div', attrs={'id':["article-toolbox","subscribe-feeds",]}),
dict(name='div', attrs={'class':["guardian-tickets promo-component",]}),
dict(name='ul', attrs={'class':["pagination"]}), dict(name='ul', attrs={'class':["pagination"]}),
dict(name='ul', attrs={'id':["content-actions"]}), dict(name='ul', attrs={'id':["content-actions"]}),
#dict(name='img'), #dict(name='img'),

View File

@ -2,7 +2,7 @@ from calibre.web.feeds.news import BasicNewsRecipe
class AdvancedUserRecipe1282101454(BasicNewsRecipe): class AdvancedUserRecipe1282101454(BasicNewsRecipe):
title = 'West Hawaii Today' title = 'West Hawaii Today'
__author__ = 'Tony Stegall' __author__ = 'Tony Stegall, fixed by HK'
language = 'en' language = 'en'
description = 'Westhawaiitoday.com' description = 'Westhawaiitoday.com'
publisher = 'West Hawaii ' publisher = 'West Hawaii '
@ -15,7 +15,14 @@ class AdvancedUserRecipe1282101454(BasicNewsRecipe):
masthead_url = 'http://images.townnews.com/westhawaiitoday.com/art/whttoplogo.gif' masthead_url = 'http://images.townnews.com/westhawaiitoday.com/art/whttoplogo.gif'
feeds = [
feeds = [ 'http://www.westhawaiitoday.com/rss.xml'] ('http://www.westhawaiitoday.com/taxonomy/term/2/feed'), #Local News
('http://www.westhawaiitoday.com/taxonomy/term/15/feed'), #Local Sports
('http://www.westhawaiitoday.com/taxonomy/term/4/feed'), #Local Features
('http://www.westhawaiitoday.com/taxonomy/term/12/feed'), #Obituaries
('http://www.westhawaiitoday.com/taxonomy/term/18/feed'), #Letters
('http://www.westhawaiitoday.com/taxonomy/term/19/feed'), #Editorial
('http://www.westhawaiitoday.com/taxonomy/term/20/feed'), #columns
('http://www.westhawaiitoday.com/taxonomy/term/13/feed') #Volcano Update (Sundays)
]

View File

@ -34,7 +34,7 @@ class iHeuteRecipe(BasicNewsRecipe):
dict(name='table', attrs={'class':['video-16ku9']})] dict(name='table', attrs={'class':['video-16ku9']})]
remove_tags_after = [dict(name='div',attrs={'id':['related','related2']})] remove_tags_after = [dict(name='div',attrs={'id':['related','related2']})]
keep_only_tags = [dict(name='div', attrs={'class':['art-full adwords-text','dil-day']}) keep_only_tags = [dict(name='div', attrs={'class':['art-full adwords-text','dil-day','art-full']})
,dict(name='table',attrs={'class':['kemel-box']})] ,dict(name='table',attrs={'class':['kemel-box']})]
def print_version(self, url): def print_version(self, url):

View File

@ -15,10 +15,10 @@ class InternationalHeraldTribune(BasicNewsRecipe):
language = 'en' language = 'en'
oldest_article = 1 oldest_article = 1
max_articles_per_feed = 10 max_articles_per_feed = 30
no_stylesheets = True no_stylesheets = True
remove_tags = [dict(name='div', attrs={'class':'footer'}), remove_tags = [dict(name='div', attrs={'class':['footer','header']}),
dict(name=['form'])] dict(name=['form'])]
preprocess_regexps = [ preprocess_regexps = [
(re.compile(r'<!-- webtrends.*', re.DOTALL), (re.compile(r'<!-- webtrends.*', re.DOTALL),
@ -26,6 +26,8 @@ class InternationalHeraldTribune(BasicNewsRecipe):
] ]
extra_css = '.headline {font-size: x-large;} \n .fact { padding-top: 10pt }' extra_css = '.headline {font-size: x-large;} \n .fact { padding-top: 10pt }'
remove_empty_feeds = True
feeds = [ feeds = [
(u'Frontpage', u'http://www.iht.com/rss/frontpage.xml'), (u'Frontpage', u'http://www.iht.com/rss/frontpage.xml'),
(u'Business', u'http://www.iht.com/rss/business.xml'), (u'Business', u'http://www.iht.com/rss/business.xml'),
@ -46,13 +48,15 @@ class InternationalHeraldTribune(BasicNewsRecipe):
] ]
temp_files = [] temp_files = []
articles_are_obfuscated = True articles_are_obfuscated = True
def get_obfuscated_article(self, url, logger): masthead_url = 'http://graphics8.nytimes.com/images/misc/iht-masthead-logo.gif'
def get_obfuscated_article(self, url):
br = self.get_browser() br = self.get_browser()
br.open(url) br.open(url)
br.select_form(name='printFriendly') response1 = br.follow_link(url_regex=re.compile(r'.*pagewanted=print.*'))
res = br.submit() html = response1.read()
html = res.read()
self.temp_files.append(PersistentTemporaryFile('_iht.html')) self.temp_files.append(PersistentTemporaryFile('_iht.html'))
self.temp_files[-1].write(html) self.temp_files[-1].write(html)
self.temp_files[-1].close() self.temp_files[-1].close()

View File

@ -1,5 +1,5 @@
__license__ = 'GPL v3' __license__ = 'GPL v3'
__copyright__ = '2010, Darko Miletic <darko.miletic at gmail.com>' __copyright__ = '2010-2011, Darko Miletic <darko.miletic at gmail.com>'
''' '''
www.kommersant.ru www.kommersant.ru
''' '''
@ -20,7 +20,13 @@ class Kommersant_ru(BasicNewsRecipe):
language = 'ru' language = 'ru'
publication_type = 'newspaper' publication_type = 'newspaper'
masthead_url = 'http://www.kommersant.ru/CorpPics/logo_daily_1.gif' masthead_url = 'http://www.kommersant.ru/CorpPics/logo_daily_1.gif'
extra_css = ' @font-face {font-family: "sans1";src:url(res:///opt/sony/ebook/FONT/tt0003m_.ttf)} body{font-family: Arial, sans1, sans-serif} span#ctl00_ContentPlaceHolderStyle_LabelSubTitle{margin-bottom: 1em; display: block} .author{margin-bottom: 1em; display: block} .paragraph{margin-bottom: 1em; display: block} .vvodka{font-weight: bold; margin-bottom: 1em} ' extra_css = """
@font-face {font-family: "sans1";src:url(res:///opt/sony/ebook/FONT/tt0003m_.ttf)}
body{font-family: Tahoma, Arial, Helvetica, sans1, sans-serif}
.title{font-size: x-large; font-weight: bold; margin-bottom: 1em}
.subtitle{font-size: large; margin-bottom: 1em}
.document_vvodka{font-weight: bold; margin-bottom: 1em}
"""
conversion_options = { conversion_options = {
'comment' : description 'comment' : description
@ -29,14 +35,11 @@ class Kommersant_ru(BasicNewsRecipe):
, 'language' : language , 'language' : language
} }
keep_only_tags = [ keep_only_tags = [dict(attrs={'class':['document','document_vvodka','document_text','document_authors vblock']})]
dict(attrs={'id':'ctl00_ContentPlaceHolderStyle_PanelHeader'}) remove_tags = [dict(name=['iframe','object','link','img','base','meta'])]
,dict(attrs={'class':['vvodka','paragraph','author']})
]
remove_tags = [dict(name=['iframe','object','link','img','base'])]
feeds = [(u'Articles', u'http://feeds.kommersant.ru/RSS_Export/RU/daily.xml')] feeds = [(u'Articles', u'http://feeds.kommersant.ru/RSS_Export/RU/daily.xml')]
def print_version(self, url): def print_version(self, url):
return url.replace('doc-rss.aspx','doc.aspx') + '&print=true' return url.replace('/doc-rss/','/Doc/') + '/Print'

View File

@ -1,5 +1,5 @@
__license__ = 'GPL v3' __license__ = 'GPL v3'
__copyright__ = '2010, Darko Miletic <darko.miletic at gmail.com>' __copyright__ = '2010-2011, Darko Miletic <darko.miletic at gmail.com>'
''' '''
perfil.com perfil.com
''' '''
@ -39,9 +39,9 @@ class Perfil(BasicNewsRecipe):
dict(name=['iframe','embed','object','base','meta','link']) dict(name=['iframe','embed','object','base','meta','link'])
,dict(name='a', attrs={'href':'#comentarios'}) ,dict(name='a', attrs={'href':'#comentarios'})
,dict(name='div', attrs={'class':'foto3'}) ,dict(name='div', attrs={'class':'foto3'})
,dict(name='img', attrs={'alt':'ampliar'}) ,dict(name='img', attrs={'alt':['ampliar','Ampliar']})
] ]
keep_only_tags=[dict(attrs={'class':['bd468a','cuerpoSuperior']})] keep_only_tags=[dict(attrs={'class':['articulo','cuerpoSuperior']})]
remove_attributes=['onload','lang','width','height','border'] remove_attributes=['onload','lang','width','height','border']
feeds = [ feeds = [

View File

@ -7,6 +7,7 @@ class SmithsonianMagazine(BasicNewsRecipe):
__author__ = 'Krittika Goyal' __author__ = 'Krittika Goyal'
oldest_article = 31#days oldest_article = 31#days
max_articles_per_feed = 50 max_articles_per_feed = 50
use_embedded_content = False
#encoding = 'latin1' #encoding = 'latin1'
recursions = 1 recursions = 1
match_regexps = ['&page=[2-9]$'] match_regexps = ['&page=[2-9]$']

View File

@ -1,3 +1,4 @@
import re
from calibre.web.feeds.news import BasicNewsRecipe from calibre.web.feeds.news import BasicNewsRecipe
class TimesOfIndia(BasicNewsRecipe): class TimesOfIndia(BasicNewsRecipe):
@ -8,10 +9,10 @@ class TimesOfIndia(BasicNewsRecipe):
max_articles_per_feed = 25 max_articles_per_feed = 25
no_stylesheets = True no_stylesheets = True
keep_only_tags = [dict(attrs={'class':'maintable12'})] keep_only_tags = [{'class':['maintable12', 'prttabl']}]
remove_tags = [ remove_tags = [
dict(style=lambda x: x and 'float' in x), dict(style=lambda x: x and 'float' in x),
dict(attrs={'class':'prvnxtbg'}), {'class':['prvnxtbg', 'footbdrin', 'bcclftr']},
] ]
feeds = [ feeds = [
@ -38,8 +39,28 @@ class TimesOfIndia(BasicNewsRecipe):
('Most Read', ('Most Read',
'http://timesofindia.indiatimes.com/rssfeedmostread.cms') 'http://timesofindia.indiatimes.com/rssfeedmostread.cms')
] ]
def print_version(self, url):
return url + '?prtpage=1' def get_article_url(self, article):
url = BasicNewsRecipe.get_article_url(self, article)
if '/0Ltimesofindia' in url:
url = url.partition('/0L')[-1]
url = url.replace('0B', '.').replace('0N', '.com').replace('0C',
'/').replace('0E', '-')
url = 'http://' + url.rpartition('/')[0]
match = re.search(r'/([0-9a-zA-Z]+?)\.cms', url)
if match is not None:
num = match.group(1)
num = re.sub(r'[^0-9]', '', num)
return ('http://timesofindia.indiatimes.com/articleshow/%s.cms?prtpage=1' %
num)
else:
cms = re.search(r'/(\d+)\.cms', url)
if cms is not None:
return ('http://timesofindia.indiatimes.com/articleshow/%s.cms?prtpage=1' %
cms.group(1))
return url
def preprocess_html(self, soup): def preprocess_html(self, soup):
return soup return soup

View File

@ -45,7 +45,6 @@ class Stage3(Command):
sub_commands = ['upload_user_manual', 'upload_demo', 'sdist', sub_commands = ['upload_user_manual', 'upload_demo', 'sdist',
'upload_to_sourceforge', 'upload_to_google_code', 'upload_to_sourceforge', 'upload_to_google_code',
'tag_release', 'upload_to_server', 'tag_release', 'upload_to_server',
'upload_to_mobileread',
] ]
class Stage4(Command): class Stage4(Command):

View File

@ -217,14 +217,25 @@ def filename_to_utf8(name):
return name.decode(codec, 'replace').encode('utf8') return name.decode(codec, 'replace').encode('utf8')
def extract(path, dir): def extract(path, dir):
ext = os.path.splitext(path)[1][1:].lower()
extractor = None extractor = None
if ext in ['zip', 'cbz', 'epub', 'oebzip']: # First use the file header to identify its type
from calibre.libunzip import extract as zipextract with open(path, 'rb') as f:
extractor = zipextract id_ = f.read(3)
elif ext in ['cbr', 'rar']: if id_ == b'Rar':
from calibre.libunrar import extract as rarextract from calibre.libunrar import extract as rarextract
extractor = rarextract extractor = rarextract
elif id_.startswith(b'PK'):
from calibre.libunzip import extract as zipextract
extractor = zipextract
if extractor is None:
# Fallback to file extension
ext = os.path.splitext(path)[1][1:].lower()
if ext in ['zip', 'cbz', 'epub', 'oebzip']:
from calibre.libunzip import extract as zipextract
extractor = zipextract
elif ext in ['cbr', 'rar']:
from calibre.libunrar import extract as rarextract
extractor = rarextract
if extractor is None: if extractor is None:
raise Exception('Unknown archive type') raise Exception('Unknown archive type')
extractor(path, dir) extractor(path, dir)
@ -281,16 +292,17 @@ def get_parsed_proxy(typ='http', debug=True):
def random_user_agent(): def random_user_agent():
choices = [ choices = [
'Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/523.15 (KHTML, like Gecko, Safari/419.3) Arora/0.3 (Change: 287 c9dfb30)', 'Mozilla/5.0 (Windows NT 5.2; rv:2.0.1) Gecko/20100101 Firefox/4.0.1',
'Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:2.0.1) Gecko/20100101 Firefox/4.0.1',
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:2.0.1) Gecko/20100101 Firefox/4.0.1',
'Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US; rv:1.9.2.11) Gecko/20101012 Firefox/3.6.11', 'Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US; rv:1.9.2.11) Gecko/20101012 Firefox/3.6.11',
'Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/525.19 (KHTML, like Gecko) Chrome/0.2.153.1 Safari/525.19', 'Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/525.19 (KHTML, like Gecko) Chrome/0.2.153.1 Safari/525.19',
'Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US; rv:1.9.2.11) Gecko/20101012 Firefox/3.6.11', 'Mozilla/5.0 (Windows; U; Windows NT 6.0; en-US; rv:1.9.2.11) Gecko/20101012 Firefox/3.6.11',
'Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en; rv:1.8.1.14) Gecko/20080409 Camino/1.6 (like Firefox/2.0.0.14)',
'Mozilla/5.0 (Macintosh; U; Intel Mac OS X; en-US; rv:1.8.0.1) Gecko/20060118 Camino/1.0b2+',
'Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/534.3 (KHTML, like Gecko) Chrome/6.0.472.63 Safari/534.3', 'Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/534.3 (KHTML, like Gecko) Chrome/6.0.472.63 Safari/534.3',
'Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/532.5 (KHTML, like Gecko) Chrome/4.0.249.78 Safari/532.5', 'Mozilla/5.0 (Windows; U; Windows NT 6.1; en-US) AppleWebKit/532.5 (KHTML, like Gecko) Chrome/4.0.249.78 Safari/532.5',
'Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0)', 'Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; WOW64; Trident/5.0)',
] ]
#return choices[-1]
return choices[random.randint(0, len(choices)-1)] return choices[random.randint(0, len(choices)-1)]

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.53' __version__ = '0.7.54'
__author__ = "Kovid Goyal <kovid@kovidgoyal.net>" __author__ = "Kovid Goyal <kovid@kovidgoyal.net>"
import re, importlib import re, importlib

View File

@ -10,6 +10,7 @@ from calibre.constants import numeric_version
from calibre.ebooks.metadata.archive import ArchiveExtract, get_cbz_metadata from calibre.ebooks.metadata.archive import ArchiveExtract, get_cbz_metadata
from calibre.ebooks.metadata.opf2 import metadata_to_opf from calibre.ebooks.metadata.opf2 import metadata_to_opf
from calibre.ebooks.oeb.base import OEB_IMAGES from calibre.ebooks.oeb.base import OEB_IMAGES
from calibre.utils.config import test_eight_code
# To archive plugins {{{ # To archive plugins {{{
class HTML2ZIP(FileTypePlugin): class HTML2ZIP(FileTypePlugin):
@ -166,6 +167,14 @@ class ComicMetadataReader(MetadataReaderPlugin):
description = _('Extract cover from comic files') description = _('Extract cover from comic files')
def get_metadata(self, stream, ftype): def get_metadata(self, stream, ftype):
if hasattr(stream, 'seek') and hasattr(stream, 'tell'):
pos = stream.tell()
id_ = stream.read(3)
stream.seek(pos)
if id_ == b'Rar':
ftype = 'cbr'
elif id.startswith(b'PK'):
ftype = 'cbz'
if ftype == 'cbr': if ftype == 'cbr':
from calibre.libunrar import extract_first_alphabetically as extract_first from calibre.libunrar import extract_first_alphabetically as extract_first
extract_first extract_first
@ -231,6 +240,17 @@ class HTMLMetadataReader(MetadataReaderPlugin):
from calibre.ebooks.metadata.html import get_metadata from calibre.ebooks.metadata.html import get_metadata
return get_metadata(stream) return get_metadata(stream)
class HTMLZMetadataReader(MetadataReaderPlugin):
name = 'Read HTMLZ metadata'
file_types = set(['htmlz'])
description = _('Read metadata from %s files') % 'HTMLZ'
author = 'John Schember'
def get_metadata(self, stream, ftype):
from calibre.ebooks.metadata.extz import get_metadata
return get_metadata(stream)
class IMPMetadataReader(MetadataReaderPlugin): class IMPMetadataReader(MetadataReaderPlugin):
name = 'Read IMP metadata' name = 'Read IMP metadata'
@ -407,7 +427,7 @@ class TXTZMetadataReader(MetadataReaderPlugin):
author = 'John Schember' author = 'John Schember'
def get_metadata(self, stream, ftype): def get_metadata(self, stream, ftype):
from calibre.ebooks.metadata.txtz import get_metadata from calibre.ebooks.metadata.extz import get_metadata
return get_metadata(stream) return get_metadata(stream)
class ZipMetadataReader(MetadataReaderPlugin): class ZipMetadataReader(MetadataReaderPlugin):
@ -433,6 +453,17 @@ class EPUBMetadataWriter(MetadataWriterPlugin):
from calibre.ebooks.metadata.epub import set_metadata from calibre.ebooks.metadata.epub import set_metadata
set_metadata(stream, mi, apply_null=self.apply_null) set_metadata(stream, mi, apply_null=self.apply_null)
class HTMLZMetadataWriter(MetadataWriterPlugin):
name = 'Set HTMLZ metadata'
file_types = set(['htmlz'])
description = _('Set metadata from %s files') % 'HTMLZ'
author = 'John Schember'
def set_metadata(self, stream, mi, type):
from calibre.ebooks.metadata.extz import set_metadata
set_metadata(stream, mi)
class LRFMetadataWriter(MetadataWriterPlugin): class LRFMetadataWriter(MetadataWriterPlugin):
name = 'Set LRF metadata' name = 'Set LRF metadata'
@ -505,7 +536,7 @@ class TXTZMetadataWriter(MetadataWriterPlugin):
author = 'John Schember' author = 'John Schember'
def set_metadata(self, stream, mi, type): def set_metadata(self, stream, mi, type):
from calibre.ebooks.metadata.txtz import set_metadata from calibre.ebooks.metadata.extz import set_metadata
set_metadata(stream, mi) set_metadata(stream, mi)
# }}} # }}}
@ -514,6 +545,7 @@ from calibre.ebooks.comic.input import ComicInput
from calibre.ebooks.epub.input import EPUBInput from calibre.ebooks.epub.input import EPUBInput
from calibre.ebooks.fb2.input import FB2Input from calibre.ebooks.fb2.input import FB2Input
from calibre.ebooks.html.input import HTMLInput from calibre.ebooks.html.input import HTMLInput
from calibre.ebooks.htmlz.input import HTMLZInput
from calibre.ebooks.lit.input import LITInput from calibre.ebooks.lit.input import LITInput
from calibre.ebooks.mobi.input import MOBIInput from calibre.ebooks.mobi.input import MOBIInput
from calibre.ebooks.odt.input import ODTInput from calibre.ebooks.odt.input import ODTInput
@ -544,6 +576,7 @@ from calibre.ebooks.tcr.output import TCROutput
from calibre.ebooks.txt.output import TXTOutput from calibre.ebooks.txt.output import TXTOutput
from calibre.ebooks.txt.output import TXTZOutput from calibre.ebooks.txt.output import TXTZOutput
from calibre.ebooks.html.output import HTMLOutput from calibre.ebooks.html.output import HTMLOutput
from calibre.ebooks.htmlz.output import HTMLZOutput
from calibre.ebooks.snb.output import SNBOutput from calibre.ebooks.snb.output import SNBOutput
from calibre.customize.profiles import input_profiles, output_profiles from calibre.customize.profiles import input_profiles, output_profiles
@ -580,25 +613,40 @@ from calibre.devices.folder_device.driver import FOLDER_DEVICE_FOR_CONFIG
from calibre.devices.kobo.driver import KOBO from calibre.devices.kobo.driver import KOBO
from calibre.devices.bambook.driver import BAMBOOK from calibre.devices.bambook.driver import BAMBOOK
from calibre.ebooks.metadata.fetch import GoogleBooks, ISBNDB, Amazon, \
KentDistrictLibrary
from calibre.ebooks.metadata.douban import DoubanBooks
from calibre.ebooks.metadata.nicebooks import NiceBooks, NiceBooksCovers
from calibre.ebooks.metadata.covers import OpenLibraryCovers, \
AmazonCovers, DoubanCovers
from calibre.library.catalog import CSV_XML, EPUB_MOBI, BIBTEX from calibre.library.catalog import CSV_XML, EPUB_MOBI, BIBTEX
from calibre.ebooks.epub.fix.unmanifested import Unmanifested from calibre.ebooks.epub.fix.unmanifested import Unmanifested
from calibre.ebooks.epub.fix.epubcheck import Epubcheck from calibre.ebooks.epub.fix.epubcheck import Epubcheck
plugins = [HTML2ZIP, PML2PMLZ, TXT2TXTZ, ArchiveExtract, GoogleBooks, ISBNDB, Amazon, plugins = [HTML2ZIP, PML2PMLZ, TXT2TXTZ, ArchiveExtract, CSV_XML, EPUB_MOBI, BIBTEX, Unmanifested,
KentDistrictLibrary, DoubanBooks, NiceBooks, CSV_XML, EPUB_MOBI, BIBTEX, Unmanifested, Epubcheck, ]
Epubcheck, OpenLibraryCovers, AmazonCovers, DoubanCovers,
NiceBooksCovers] if test_eight_code:
# New metadata download plugins {{{
from calibre.ebooks.metadata.sources.google import GoogleBooks
from calibre.ebooks.metadata.sources.amazon import Amazon
from calibre.ebooks.metadata.sources.openlibrary import OpenLibrary
plugins += [GoogleBooks, Amazon, OpenLibrary]
# }}}
else:
from calibre.ebooks.metadata.fetch import GoogleBooks, ISBNDB, Amazon, \
KentDistrictLibrary
from calibre.ebooks.metadata.douban import DoubanBooks
from calibre.ebooks.metadata.nicebooks import NiceBooks, NiceBooksCovers
from calibre.ebooks.metadata.covers import OpenLibraryCovers, \
AmazonCovers, DoubanCovers
plugins += [GoogleBooks, ISBNDB, Amazon,
OpenLibraryCovers, AmazonCovers, DoubanCovers,
NiceBooksCovers, KentDistrictLibrary, DoubanBooks, NiceBooks]
plugins += [ plugins += [
ComicInput, ComicInput,
EPUBInput, EPUBInput,
FB2Input, FB2Input,
HTMLInput, HTMLInput,
HTMLZInput,
LITInput, LITInput,
MOBIInput, MOBIInput,
ODTInput, ODTInput,
@ -630,6 +678,7 @@ plugins += [
TXTOutput, TXTOutput,
TXTZOutput, TXTZOutput,
HTMLOutput, HTMLOutput,
HTMLZOutput,
SNBOutput, SNBOutput,
] ]
# Order here matters. The first matched device is the one used. # Order here matters. The first matched device is the one used.
@ -1029,11 +1078,4 @@ plugins += [LookAndFeel, Behavior, Columns, Toolbar, Search, InputOptions,
#}}} #}}}
# New metadata download plugins {{{
from calibre.ebooks.metadata.sources.google import GoogleBooks
from calibre.ebooks.metadata.sources.amazon import Amazon
from calibre.ebooks.metadata.sources.openlibrary import OpenLibrary
plugins += [GoogleBooks, Amazon, OpenLibrary]
# }}}

View File

@ -471,8 +471,8 @@ class KoboReaderOutput(OutputProfile):
description = _('This profile is intended for the Kobo Reader.') description = _('This profile is intended for the Kobo Reader.')
screen_size = (540, 718) screen_size = (536, 710)
comic_screen_size = (540, 718) comic_screen_size = (536, 710)
dpi = 168.451 dpi = 168.451
fbase = 12 fbase = 12
fsizes = [7.5, 9, 10, 12, 15.5, 20, 22, 24] fsizes = [7.5, 9, 10, 12, 15.5, 20, 22, 24]

View File

@ -36,7 +36,9 @@ class ANDROID(USBMS):
# Motorola # Motorola
0x22b8 : { 0x41d9 : [0x216], 0x2d61 : [0x100], 0x2d67 : [0x100], 0x22b8 : { 0x41d9 : [0x216], 0x2d61 : [0x100], 0x2d67 : [0x100],
0x41db : [0x216], 0x4285 : [0x216], 0x42a3 : [0x216], 0x41db : [0x216], 0x4285 : [0x216], 0x42a3 : [0x216],
0x4286 : [0x216], 0x42b3 : [0x216], 0x42b4 : [0x216] }, 0x4286 : [0x216], 0x42b3 : [0x216], 0x42b4 : [0x216],
0x7086 : [0x0226],
},
# Sony Ericsson # Sony Ericsson
0xfce : { 0xd12e : [0x0100]}, 0xfce : { 0xd12e : [0x0100]},
@ -101,7 +103,8 @@ class ANDROID(USBMS):
'SCH-I500_CARD', 'SPH-D700_CARD', 'MB810', 'GT-P1000', 'DESIRE', 'SCH-I500_CARD', 'SPH-D700_CARD', 'MB810', 'GT-P1000', 'DESIRE',
'SGH-T849', '_MB300', 'A70S', 'S_ANDROID', 'A101IT', 'A70H', 'SGH-T849', '_MB300', 'A70S', 'S_ANDROID', 'A101IT', 'A70H',
'IDEOS_TABLET', 'MYTOUCH_4G', 'UMS_COMPOSITE', 'SCH-I800_CARD', 'IDEOS_TABLET', 'MYTOUCH_4G', 'UMS_COMPOSITE', 'SCH-I800_CARD',
'7', 'A956', 'A955', 'A43', 'ANDROID_PLATFORM', 'TEGRA_2'] '7', 'A956', 'A955', 'A43', 'ANDROID_PLATFORM', 'TEGRA_2',
'MB860']
WINDOWS_CARD_A_MEM = ['ANDROID_PHONE', 'GT-I9000_CARD', 'SGH-I897', WINDOWS_CARD_A_MEM = ['ANDROID_PHONE', 'GT-I9000_CARD', 'SGH-I897',
'FILE-STOR_GADGET', 'SGH-T959', 'SAMSUNG_ANDROID', 'GT-P1000_CARD', 'FILE-STOR_GADGET', 'SGH-T959', 'SAMSUNG_ANDROID', 'GT-P1000_CARD',
'A70S', 'A101IT', '7'] 'A70S', 'A101IT', '7']

View File

@ -244,7 +244,8 @@ class POCKETBOOK602(USBMS):
BCD = [0x0324] BCD = [0x0324]
VENDOR_NAME = '' VENDOR_NAME = ''
WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = ['PB602', 'PB603', 'PB902', 'PB903'] WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = ['PB602', 'PB603', 'PB902',
'PB903', 'PB']
class POCKETBOOK701(USBMS): class POCKETBOOK701(USBMS):

View File

@ -100,6 +100,12 @@ def xml_to_unicode(raw, verbose=False, strip_encoding_pats=False,
try: try:
if encoding.lower().strip() == 'macintosh': if encoding.lower().strip() == 'macintosh':
encoding = 'mac-roman' encoding = 'mac-roman'
if 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'
raw = raw.decode(encoding, 'replace') raw = raw.decode(encoding, 'replace')
except LookupError: except LookupError:
encoding = 'utf-8' encoding = 'utf-8'
@ -110,11 +116,6 @@ 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

View File

@ -0,0 +1,66 @@
# -*- coding: utf-8 -*-
from __future__ import (unicode_literals, division, absolute_import, print_function)
__license__ = 'GPL 3'
__copyright__ = '2011, John Schember <john@nachtimwald.com>'
__docformat__ = 'restructuredtext en'
import os
from calibre import walk
from calibre.customize.conversion import InputFormatPlugin
from calibre.utils.zipfile import ZipFile
class HTMLZInput(InputFormatPlugin):
name = 'HTLZ Input'
author = 'John Schember'
description = 'Convert HTML files to HTML'
file_types = set(['htmlz'])
def convert(self, stream, options, file_ext, log,
accelerators):
self.log = log
html = u''
# Extract content from zip archive.
zf = ZipFile(stream)
zf.extractall('.')
for x in walk('.'):
if os.path.splitext(x)[1].lower() in ('.html', '.xhtml', '.htm'):
with open(x, 'rb') as tf:
html = tf.read()
break
# Run the HTML through the html processing plugin.
from calibre.customize.ui import plugin_for_input_format
html_input = plugin_for_input_format('html')
for opt in html_input.options:
setattr(options, opt.option.name, opt.recommended_value)
options.input_encoding = 'utf-8'
base = os.getcwdu()
fname = os.path.join(base, 'index.html')
c = 0
while os.path.exists(fname):
c += 1
fname = 'index%d.html'%c
htmlfile = open(fname, 'wb')
with htmlfile:
htmlfile.write(html.encode('utf-8'))
odi = options.debug_pipeline
options.debug_pipeline = None
# Generate oeb from html conversion.
oeb = html_input.convert(open(htmlfile.name, 'rb'), options, 'html', log,
{})
options.debug_pipeline = odi
os.remove(htmlfile.name)
# Set metadata from file.
from calibre.customize.ui import get_file_type_metadata
from calibre.ebooks.oeb.transforms.metadata import meta_info_to_oeb_metadata
mi = get_file_type_metadata(stream, file_ext)
meta_info_to_oeb_metadata(mi, oeb.metadata, log)
return oeb

View File

@ -0,0 +1,385 @@
# -*- coding: utf-8 -*-
from __future__ import (unicode_literals, division, absolute_import, print_function)
__license__ = 'GPL 3'
__copyright__ = '2011, John Schember <john@nachtimwald.com>'
__docformat__ = 'restructuredtext en'
'''
Transform OEB content into a single (more or less) HTML file.
'''
import os
from functools import partial
from lxml import html
from urlparse import urldefrag
from calibre import prepare_string_for_xml
from calibre.ebooks.oeb.base import XHTML, XHTML_NS, barename, namespace,\
OEB_IMAGES, XLINK, rewrite_links
from calibre.ebooks.oeb.stylizer import Stylizer
from calibre.utils.logging import default_log
class OEB2HTML(object):
'''
Base class. All subclasses should implement dump_text to actually transform
content. Also, callers should use oeb2html to get the transformed html.
links and images can be retrieved after calling oeb2html to get the mapping
of OEB links and images to the new names used in the html returned by oeb2html.
Images will always be referenced as if they are in an images directory.
Use get_css to get the CSS classes for the OEB document as a string.
'''
def __init__(self, log=None):
self.log = default_log if log is None else log
self.links = {}
self.images = {}
def oeb2html(self, oeb_book, opts):
self.log.info('Converting OEB book to HTML...')
self.opts = opts
self.links = {}
self.images = {}
self.base_hrefs = [item.href for item in oeb_book.spine]
self.map_resources(oeb_book)
return self.mlize_spine(oeb_book)
def mlize_spine(self, oeb_book):
output = [u'<html><body><head><meta http-equiv="Content-Type" content="text/html;charset=utf-8" /></head>']
for item in oeb_book.spine:
self.log.debug('Converting %s to HTML...' % item.href)
self.rewrite_ids(item.data, item)
rewrite_links(item.data, partial(self.rewrite_link, page=item))
stylizer = Stylizer(item.data, item.href, oeb_book, self.opts)
output += self.dump_text(item.data.find(XHTML('body')), stylizer, item)
output.append('\n\n')
output.append('</body></html>')
return ''.join(output)
def dump_text(self, elem, stylizer, page):
raise NotImplementedError
def get_link_id(self, href, id=''):
if id:
href += '#%s' % id
if href not in self.links:
self.links[href] = '#calibre_link-%s' % len(self.links.keys())
return self.links[href]
def map_resources(self, oeb_book):
for item in oeb_book.manifest:
if item.media_type in OEB_IMAGES:
if item.href not in self.images:
ext = os.path.splitext(item.href)[1]
fname = '%s%s' % (len(self.images), ext)
fname = fname.zfill(10)
self.images[item.href] = fname
if item in oeb_book.spine:
self.get_link_id(item.href)
root = item.data.find(XHTML('body'))
link_attrs = set(html.defs.link_attrs)
link_attrs.add(XLINK('href'))
for el in root.iter():
attribs = el.attrib
try:
if not isinstance(el.tag, basestring):
continue
except:
continue
for attr in attribs:
if attr in link_attrs:
href = item.abshref(attribs[attr])
href, id = urldefrag(href)
if href in self.base_hrefs:
self.get_link_id(href, id)
def rewrite_link(self, url, page=None):
if not page:
return url
abs_url = page.abshref(url)
if abs_url in self.images:
return 'images/%s' % self.images[abs_url]
if abs_url in self.links:
return self.links[abs_url]
return url
def rewrite_ids(self, root, page):
for el in root.iter():
try:
tag = el.tag
except UnicodeDecodeError:
continue
if tag == XHTML('body'):
el.attrib['id'] = self.get_link_id(page.href)[1:]
continue
if 'id' in el.attrib:
el.attrib['id'] = self.get_link_id(page.href, el.attrib['id'])[1:]
def get_css(self, oeb_book):
css = u''
for item in oeb_book.manifest:
if item.media_type == 'text/css':
css = item.data.cssText
break
return css
class OEB2HTMLNoCSSizer(OEB2HTML):
'''
This will remap a small number of CSS styles to equivalent HTML tags.
'''
def dump_text(self, elem, stylizer, page):
'''
@elem: The element in the etree that we are working on.
@stylizer: The style information attached to the element.
'''
# We can only processes tags. If there isn't a tag return any text.
if not isinstance(elem.tag, basestring) \
or namespace(elem.tag) != XHTML_NS:
p = elem.getparent()
if p is not None and isinstance(p.tag, basestring) and namespace(p.tag) == XHTML_NS \
and elem.tail:
return [elem.tail]
return ['']
# Setup our variables.
text = ['']
style = stylizer.style(elem)
tags = []
tag = barename(elem.tag)
attribs = elem.attrib
if tag == 'body':
tag = 'div'
tags.append(tag)
# Ignore anything that is set to not be displayed.
if style['display'] in ('none', 'oeb-page-head', 'oeb-page-foot') \
or style['visibility'] == 'hidden':
return ['']
# Remove attributes we won't want.
if 'class' in attribs:
del attribs['class']
if 'style' in attribs:
del attribs['style']
# Turn the rest of the attributes into a string we can write with the tag.
at = ''
for k, v in attribs.items():
at += ' %s="%s"' % (k, prepare_string_for_xml(v, attribute=True))
# Write the tag.
text.append('<%s%s>' % (tag, at))
# Turn styles into tags.
if style['font-weight'] in ('bold', 'bolder'):
text.append('<b>')
tags.append('b')
if style['font-style'] == 'italic':
text.append('<i>')
tags.append('i')
if style['text-decoration'] == 'underline':
text.append('<u>')
tags.append('u')
if style['text-decoration'] == 'line-through':
text.append('<s>')
tags.append('s')
# Process tags that contain text.
if hasattr(elem, 'text') and elem.text:
text.append(elem.text)
# Recurse down into tags within the tag we are in.
for item in elem:
text += self.dump_text(item, stylizer, page)
# Close all open tags.
tags.reverse()
for t in tags:
text.append('</%s>' % t)
# Add the text that is outside of the tag.
if hasattr(elem, 'tail') and elem.tail:
text.append(elem.tail)
return text
class OEB2HTMLInlineCSSizer(OEB2HTML):
'''
Turns external CSS classes into inline style attributes.
'''
def dump_text(self, elem, stylizer, page):
'''
@elem: The element in the etree that we are working on.
@stylizer: The style information attached to the element.
'''
# We can only processes tags. If there isn't a tag return any text.
if not isinstance(elem.tag, basestring) \
or namespace(elem.tag) != XHTML_NS:
p = elem.getparent()
if p is not None and isinstance(p.tag, basestring) and namespace(p.tag) == XHTML_NS \
and elem.tail:
return [elem.tail]
return ['']
# Setup our variables.
text = ['']
style = stylizer.style(elem)
tags = []
tag = barename(elem.tag)
attribs = elem.attrib
style_a = '%s' % style
if tag == 'body':
tag = 'div'
if not style['page-break-before'] == 'always':
style_a = 'page-break-before: always;' + ' ' if style_a else '' + style_a
tags.append(tag)
# Remove attributes we won't want.
if 'class' in attribs:
del attribs['class']
if 'style' in attribs:
del attribs['style']
# Turn the rest of the attributes into a string we can write with the tag.
at = ''
for k, v in attribs.items():
at += ' %s="%s"' % (k, prepare_string_for_xml(v, attribute=True))
# Turn style into strings for putting in the tag.
style_t = ''
if style_a:
style_t = ' style="%s"' % style_a
# Write the tag.
text.append('<%s%s%s>' % (tag, at, style_t))
# Process tags that contain text.
if hasattr(elem, 'text') and elem.text:
text.append(elem.text)
# Recurse down into tags within the tag we are in.
for item in elem:
text += self.dump_text(item, stylizer, page)
# Close all open tags.
tags.reverse()
for t in tags:
text.append('</%s>' % t)
# Add the text that is outside of the tag.
if hasattr(elem, 'tail') and elem.tail:
text.append(elem.tail)
return text
class OEB2HTMLClassCSSizer(OEB2HTML):
'''
Use CSS classes. css_style option can specify whether to use
inline classes (style tag in the head) or reference an external
CSS file called style.css.
'''
def mlize_spine(self, oeb_book):
output = []
for item in oeb_book.spine:
self.log.debug('Converting %s to HTML...' % item.href)
self.rewrite_ids(item.data, item)
rewrite_links(item.data, partial(self.rewrite_link, page=item))
stylizer = Stylizer(item.data, item.href, oeb_book, self.opts)
output += self.dump_text(item.data.find(XHTML('body')), stylizer, item)
output.append('\n\n')
if self.opts.htmlz_class_style == 'external':
css = u'<link href="style.css" rel="stylesheet" type="text/css" />'
else:
css = u'<style type="text/css">' + self.get_css(oeb_book) + u'</style>'
output = [u'<html><head><meta http-equiv="Content-Type" content="text/html;charset=utf-8" />'] + [css] + [u'</head><body>'] + output + [u'</body></html>']
return ''.join(output)
def dump_text(self, elem, stylizer, page):
'''
@elem: The element in the etree that we are working on.
@stylizer: The style information attached to the element.
'''
# We can only processes tags. If there isn't a tag return any text.
if not isinstance(elem.tag, basestring) \
or namespace(elem.tag) != XHTML_NS:
p = elem.getparent()
if p is not None and isinstance(p.tag, basestring) and namespace(p.tag) == XHTML_NS \
and elem.tail:
return [elem.tail]
return ['']
# Setup our variables.
text = ['']
tags = []
tag = barename(elem.tag)
attribs = elem.attrib
if tag == 'body':
tag = 'div'
tags.append(tag)
# Remove attributes we won't want.
if 'style' in attribs:
del attribs['style']
# Turn the rest of the attributes into a string we can write with the tag.
at = ''
for k, v in attribs.items():
at += ' %s="%s"' % (k, prepare_string_for_xml(v, attribute=True))
# Write the tag.
text.append('<%s%s>' % (tag, at))
# Process tags that contain text.
if hasattr(elem, 'text') and elem.text:
text.append(elem.text)
# Recurse down into tags within the tag we are in.
for item in elem:
text += self.dump_text(item, stylizer, page)
# Close all open tags.
tags.reverse()
for t in tags:
text.append('</%s>' % t)
# Add the text that is outside of the tag.
if hasattr(elem, 'tail') and elem.tail:
text.append(elem.tail)
return text
def oeb2html_no_css(oeb_book, log, opts):
izer = OEB2HTMLNoCSSizer(log)
html = izer.oeb2html(oeb_book, opts)
images = izer.images
return (html, images)
def oeb2html_inline_css(oeb_book, log, opts):
izer = OEB2HTMLInlineCSSizer(log)
html = izer.oeb2html(oeb_book, opts)
images = izer.images
return (html, images)
def oeb2html_class_css(oeb_book, log, opts):
izer = OEB2HTMLClassCSSizer(log)
setattr(opts, 'class_style', 'inline')
html = izer.oeb2html(oeb_book, opts)
images = izer.images
return (html, images)

View File

@ -0,0 +1,83 @@
# -*- coding: utf-8 -*-
from __future__ import (unicode_literals, division, absolute_import, print_function)
__license__ = 'GPL 3'
__copyright__ = '2011, John Schember <john@nachtimwald.com>'
__docformat__ = 'restructuredtext en'
import os
from lxml import etree
from calibre.customize.conversion import OutputFormatPlugin, \
OptionRecommendation
from calibre.ebooks.oeb.base import OEB_IMAGES
from calibre.ptempfile import TemporaryDirectory
from calibre.utils.zipfile import ZipFile
class HTMLZOutput(OutputFormatPlugin):
name = 'HTMLZ Output'
author = 'John Schember'
file_type = 'htmlz'
options = set([
OptionRecommendation(name='htmlz_css_type', recommended_value='class',
level=OptionRecommendation.LOW,
choices=['class', 'inline', 'tag'],
help=_('Specify the handling of CSS. Default is class.\n'
'class: Use CSS classes and have elements reference them.\n'
'inline: Write the CSS as an inline style attribute.\n'
'tag: Turn as many CSS styles as possible into HTML tags.'
)),
OptionRecommendation(name='htmlz_class_style', recommended_value='external',
level=OptionRecommendation.LOW,
choices=['external', 'inline'],
help=_('How to handle the CSS when using css-type = \'class\'.\n'
'Default is external.\n'
'external: Use an external CSS file that is linked in the document.\n'
'inline: Place the CSS in the head section of the document.'
)),
])
def convert(self, oeb_book, output_path, input_plugin, opts, log):
# HTML
if opts.htmlz_css_type == 'inline':
from calibre.ebooks.htmlz.oeb2html import OEB2HTMLInlineCSSizer
OEB2HTMLizer = OEB2HTMLInlineCSSizer
elif opts.htmlz_css_type == 'tag':
from calibre.ebooks.htmlz.oeb2html import OEB2HTMLNoCSSizer
OEB2HTMLizer = OEB2HTMLNoCSSizer
else:
from calibre.ebooks.htmlz.oeb2html import OEB2HTMLClassCSSizer as OEB2HTMLizer
with TemporaryDirectory('_htmlz_output') as tdir:
htmlizer = OEB2HTMLizer(log)
html = htmlizer.oeb2html(oeb_book, opts)
with open(os.path.join(tdir, 'index.html'), 'wb') as tf:
tf.write(html)
# CSS
if opts.htmlz_css_type == 'class' and opts.htmlz_class_style == 'external':
with open(os.path.join(tdir, 'style.css'), 'wb') as tf:
tf.write(htmlizer.get_css(oeb_book))
# Images
images = htmlizer.images
if images:
if not os.path.exists(os.path.join(tdir, 'images')):
os.makedirs(os.path.join(tdir, 'images'))
for item in oeb_book.manifest:
if item.media_type in OEB_IMAGES and item.href in images:
fname = os.path.join(tdir, 'images', images[item.href])
with open(fname, 'wb') as img:
img.write(item.data)
# Metadata
with open(os.path.join(tdir, 'metadata.opf'), 'wb') as mdataf:
mdataf.write(etree.tostring(oeb_book.metadata.to_opf1()))
htmlz = ZipFile(output_path, 'w')
htmlz.add_dir(tdir)

View File

@ -125,7 +125,10 @@ class Metadata(object):
_data = object.__getattribute__(self, '_data') _data = object.__getattribute__(self, '_data')
if field in TOP_LEVEL_IDENTIFIERS: if field in TOP_LEVEL_IDENTIFIERS:
field, val = self._clean_identifier(field, val) field, val = self._clean_identifier(field, val)
_data['identifiers'].update({field: val}) identifiers = _data['identifiers']
identifiers.pop(field, None)
if val:
identifiers[field] = val
elif field == 'identifiers': elif field == 'identifiers':
if not val: if not val:
val = copy.copy(NULL_VALUES.get('identifiers', None)) val = copy.copy(NULL_VALUES.get('identifiers', None))
@ -224,8 +227,7 @@ class Metadata(object):
identifiers = object.__getattribute__(self, identifiers = object.__getattribute__(self,
'_data')['identifiers'] '_data')['identifiers']
if not val and typ in identifiers: identifiers.pop(typ, None)
identifiers.pop(typ)
if val: if val:
identifiers[typ] = val identifiers[typ] = val
@ -647,7 +649,7 @@ class Metadata(object):
fmt('Tags', u', '.join([unicode(t) for t in self.tags])) fmt('Tags', u', '.join([unicode(t) for t in self.tags]))
if self.series: if self.series:
fmt('Series', self.series + ' #%s'%self.format_series_index()) fmt('Series', self.series + ' #%s'%self.format_series_index())
if self.language: if not self.is_null('language'):
fmt('Language', self.language) fmt('Language', self.language)
if self.rating is not None: if self.rating is not None:
fmt('Rating', self.rating) fmt('Rating', self.rating)

View File

@ -4,7 +4,7 @@ __license__ = 'GPL v3'
__copyright__ = '2011, John Schember <john@nachtimwald.com>' __copyright__ = '2011, John Schember <john@nachtimwald.com>'
''' '''
Read meta information from TXT files Read meta information from extZ (TXTZ, HTMLZ...) files.
''' '''
import os import os

View File

@ -193,6 +193,7 @@ class ResultList(list):
def search(title=None, author=None, publisher=None, isbn=None, def search(title=None, author=None, publisher=None, isbn=None,
min_viewability='none', verbose=False, max_results=40): min_viewability='none', verbose=False, max_results=40):
br = browser() br = browser()
br.set_handle_gzip(True)
start, entries = 1, [] start, entries = 1, []
while start > 0 and len(entries) <= max_results: while start > 0 and len(entries) <= max_results:
new, start = Query(title=title, author=author, publisher=publisher, new, start = Query(title=title, author=author, publisher=publisher,

View File

@ -23,7 +23,7 @@ from calibre.ebooks.metadata.book.base import Metadata
from calibre.library.comments import sanitize_comments_html from calibre.library.comments import sanitize_comments_html
from calibre.utils.date import parse_date from calibre.utils.date import parse_date
class Worker(Thread): # {{{ class Worker(Thread): # Get details {{{
''' '''
Get book details from amazons book page in a separate thread Get book details from amazons book page in a separate thread
@ -64,7 +64,7 @@ class Worker(Thread): # {{{
raw = xml_to_unicode(raw, strip_encoding_pats=True, raw = xml_to_unicode(raw, strip_encoding_pats=True,
resolve_entities=True)[0] resolve_entities=True)[0]
# open('/t/t.html', 'wb').write(raw) #open('/t/t.html', 'wb').write(raw)
if '<title>404 - ' in raw: if '<title>404 - ' in raw:
self.log.error('URL malformed: %r'%self.url) self.log.error('URL malformed: %r'%self.url)
@ -218,6 +218,9 @@ class Worker(Thread): # {{{
' @class="emptyClear" or @href]'): ' @class="emptyClear" or @href]'):
c.getparent().remove(c) c.getparent().remove(c)
desc = tostring(desc, method='html', encoding=unicode).strip() desc = tostring(desc, method='html', encoding=unicode).strip()
# Encoding bug in Amazon data U+fffd (replacement char)
# in some examples it is present in place of '
desc = desc.replace('\ufffd', "'")
# remove all attributes from tags # remove all attributes from tags
desc = re.sub(r'<([a-zA-Z0-9]+)\s[^>]+>', r'<\1>', desc) desc = re.sub(r'<([a-zA-Z0-9]+)\s[^>]+>', r'<\1>', desc)
# Collapse whitespace # Collapse whitespace
@ -276,12 +279,14 @@ class Worker(Thread): # {{{
class Amazon(Source): class Amazon(Source):
name = 'Amazon' name = 'Amazon Store'
description = _('Downloads metadata from Amazon') description = _('Downloads metadata from Amazon')
capabilities = frozenset(['identify', 'cover']) capabilities = frozenset(['identify', 'cover'])
touched_fields = frozenset(['title', 'authors', 'identifier:amazon', touched_fields = frozenset(['title', 'authors', 'identifier:amazon',
'identifier:isbn', 'rating', 'comments', 'publisher', 'pubdate']) 'identifier:isbn', 'rating', 'comments', 'publisher', 'pubdate'])
has_html_comments = True
supports_gzip_transfer_encoding = True
AMAZON_DOMAINS = { AMAZON_DOMAINS = {
'com': _('US'), 'com': _('US'),
@ -408,6 +413,18 @@ class Amazon(Source):
if 'bulk pack' not in title: if 'bulk pack' not in title:
matches.append(a.get('href')) matches.append(a.get('href'))
break break
if not matches:
# This can happen for some user agents that Amazon thinks are
# mobile/less capable
log('Trying alternate results page markup')
for td in root.xpath(
r'//div[@id="Results"]/descendant::td[starts-with(@id, "search:Td:")]'):
for a in td.xpath(r'descendant::td[@class="dataColumn"]/descendant::a[@href]/span[@class="srTitle"]/..'):
title = tostring(a, method='text', encoding=unicode).lower()
if 'bulk pack' not in title:
matches.append(a.get('href'))
break
# Keep only the top 5 matches as the matches are sorted by relevance by # Keep only the top 5 matches as the matches are sorted by relevance by
# Amazon so lower matches are not likely to be very relevant # Amazon so lower matches are not likely to be very relevant
@ -476,9 +493,10 @@ class Amazon(Source):
if abort.is_set(): if abort.is_set():
return return
br = self.browser br = self.browser
log('Downloading cover from:', cached_url)
try: try:
cdata = br.open_novisit(cached_url, timeout=timeout).read() cdata = br.open_novisit(cached_url, timeout=timeout).read()
result_queue.put(cdata) result_queue.put((self, cdata))
except: except:
log.exception('Failed to download cover from:', cached_url) log.exception('Failed to download cover from:', cached_url)
# }}} # }}}

View File

@ -15,9 +15,20 @@ 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.utils.icu import capitalize, lower
from calibre.ebooks.metadata import check_isbn from calibre.ebooks.metadata import check_isbn
msprefs = JSONConfig('metadata_sources.json') msprefs = JSONConfig('metadata_sources/global.json')
msprefs.defaults['txt_comments'] = False
msprefs.defaults['ignore_fields'] = []
msprefs.defaults['max_tags'] = 20
msprefs.defaults['wait_after_first_identify_result'] = 30 # seconds
msprefs.defaults['wait_after_first_cover_result'] = 60 # seconds
# Google covers are often poor quality (scans/errors) but they have high
# resolution, so they trump covers from better sources. So make sure they
# are only used if no other covers are found.
msprefs.defaults['cover_priorities'] = {'Google':2}
def create_log(ostream=None): def create_log(ostream=None):
log = ThreadSafeLog(level=ThreadSafeLog.DEBUG) log = ThreadSafeLog(level=ThreadSafeLog.DEBUG)
@ -89,6 +100,39 @@ class InternalMetadataCompareKeyGen(object):
# }}} # }}}
def get_cached_cover_urls(mi):
from calibre.customize.ui import metadata_plugins
plugins = list(metadata_plugins(['identify']))
for p in plugins:
url = p.get_cached_cover_url(mi.identifiers)
if url:
yield (p, url)
def cap_author_token(token):
lt = lower(token)
if lt in ('von', 'de', 'el', 'van', 'le'):
return lt
if re.match(r'([a-z]\.){2,}$', lt) is not None:
# Normalize tokens of the form J.K. to J. K.
parts = token.split('.')
return '. '.join(map(capitalize, parts)).strip()
return capitalize(token)
def fixauthors(authors):
if not authors:
return authors
ans = []
for x in authors:
ans.append(' '.join(map(cap_author_token, x.split())))
return ans
def fixcase(x):
if x:
x = titlecase(x)
return x
class Source(Plugin): class Source(Plugin):
type = _('Metadata source') type = _('Metadata source')
@ -104,6 +148,15 @@ class Source(Plugin):
#: during the identify phase #: during the identify phase
touched_fields = frozenset() touched_fields = frozenset()
#: Set this to True if your plugin return HTML formatted comments
has_html_comments = False
#: Setting this to True means that the browser object will add
#: Accept-Encoding: gzip to all requests. This can speedup downloads
#: but make sure that the source actually supports gzip transfer encoding
#: correctly first
supports_gzip_transfer_encoding = False
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
Plugin.__init__(self, *args, **kwargs) Plugin.__init__(self, *args, **kwargs)
self._isbn_to_identifier_cache = {} self._isbn_to_identifier_cache = {}
@ -114,6 +167,13 @@ class Source(Plugin):
# Configuration {{{ # Configuration {{{
def is_configured(self):
'''
Return False if your plugin needs to be configured before it can be
used. For example, it might need a username/password/API key.
'''
return True
@property @property
def prefs(self): def prefs(self):
if self._config_obj is None: if self._config_obj is None:
@ -127,6 +187,8 @@ class Source(Plugin):
def browser(self): def browser(self):
if self._browser is None: if self._browser is None:
self._browser = browser(user_agent=random_user_agent()) self._browser = browser(user_agent=random_user_agent())
if self.supports_gzip_transfer_encoding:
self._browser.set_handle_gzip(True)
return self._browser.clone_browser() return self._browser.clone_browser()
# }}} # }}}
@ -229,13 +291,9 @@ class Source(Plugin):
before putting the Metadata object into result_queue. You can of before putting the Metadata object into result_queue. You can of
course, use a custom algorithm suited to your metadata source. course, use a custom algorithm suited to your metadata source.
''' '''
def fixcase(x):
if x:
x = titlecase(x)
return x
if mi.title: if mi.title:
mi.title = fixcase(mi.title) mi.title = fixcase(mi.title)
mi.authors = list(map(fixcase, mi.authors)) mi.authors = fixauthors(mi.authors)
mi.tags = list(map(fixcase, mi.tags)) mi.tags = list(map(fixcase, mi.tags))
mi.isbn = check_isbn(mi.isbn) mi.isbn = check_isbn(mi.isbn)
@ -316,7 +374,8 @@ class Source(Plugin):
title=None, authors=None, identifiers={}, timeout=30): title=None, authors=None, identifiers={}, timeout=30):
''' '''
Download a cover and put it into result_queue. The parameters all have Download a cover and put it into result_queue. The parameters all have
the same meaning as for :meth:`identify`. the same meaning as for :meth:`identify`. Put (self, cover_data) into
result_queue.
This method should use cached cover URLs for efficiency whenever This method should use cached cover URLs for efficiency whenever
possible. When cached data is not present, most plugins simply call possible. When cached data is not present, most plugins simply call

View File

@ -0,0 +1,105 @@
#!/usr/bin/env python
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
from __future__ import (unicode_literals, division, absolute_import,
print_function)
__license__ = 'GPL v3'
__copyright__ = '2011, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
import sys, textwrap
from io import BytesIO
from threading import Event
from calibre import prints
from calibre.utils.config import OptionParser
from calibre.utils.magick.draw import save_cover_data_to
from calibre.ebooks.metadata import string_to_authors
from calibre.ebooks.metadata.opf2 import metadata_to_opf
from calibre.ebooks.metadata.sources.base import create_log
from calibre.ebooks.metadata.sources.identify import identify
from calibre.ebooks.metadata.sources.covers import download_cover
from calibre.utils.config import test_eight_code
def option_parser():
if not test_eight_code:
from calibre.ebooks.metadata.fetch import option_parser
return option_parser()
parser = OptionParser(textwrap.dedent(
'''\
%prog [options]
Fetch book metadata from online sources. You must specify at least one
of title, authors or ISBN.
'''
))
parser.add_option('-t', '--title', help='Book title')
parser.add_option('-a', '--authors', help='Book author(s)')
parser.add_option('-i', '--isbn', help='Book ISBN')
parser.add_option('-v', '--verbose', default=False, action='store_true',
help='Print the log to the console (stderr)')
parser.add_option('-o', '--opf', help='Output the metadata in OPF format')
parser.add_option('-c', '--cover',
help='Specify a filename. The cover, if available, will be saved to it')
parser.add_option('-d', '--timeout', default='30',
help='Timeout in seconds. Default is 30')
return parser
def main(args=sys.argv):
if not test_eight_code:
from calibre.ebooks.metadata.fetch import main
return main(args)
parser = option_parser()
opts, args = parser.parse_args(args)
buf = BytesIO()
log = create_log(buf)
abort = Event()
authors = []
if opts.authors:
authors = string_to_authors(opts.authors)
identifiers = {}
if opts.isbn:
identifiers['isbn'] = opts.isbn
results = identify(log, abort, title=opts.title, authors=authors,
identifiers=identifiers, timeout=int(opts.timeout))
if not results:
print (log, file=sys.stderr)
prints('No results found', file=sys.stderr)
raise SystemExit(1)
result = results[0]
cf = None
if opts.cover and results:
cover = download_cover(log, title=opts.title, authors=authors,
identifiers=result.identifiers, timeout=int(opts.timeout))
if cover is None:
prints('No cover found', file=sys.stderr)
else:
save_cover_data_to(cover[-1], opts.cover)
result.cover = cf = opts.cover
log = buf.getvalue()
result = (metadata_to_opf(result) if opts.opf else
unicode(result).encode('utf-8'))
if opts.verbose:
print (log, file=sys.stderr)
print (result)
if not opts.opf and opts.cover:
prints('Cover :', cf)
return 0
if __name__ == '__main__':
sys.exit(main())

View File

@ -0,0 +1,178 @@
#!/usr/bin/env python
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
from __future__ import (unicode_literals, division, absolute_import,
print_function)
__license__ = 'GPL v3'
__copyright__ = '2011, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
import time
from Queue import Queue, Empty
from threading import Thread, Event
from io import BytesIO
from calibre.customize.ui import metadata_plugins
from calibre.ebooks.metadata.sources.base import msprefs, create_log
from calibre.utils.magick.draw import Image, save_cover_data_to
class Worker(Thread):
def __init__(self, plugin, abort, title, authors, identifiers, timeout, rq):
Thread.__init__(self)
self.daemon = True
self.plugin = plugin
self.abort = abort
self.buf = BytesIO()
self.log = create_log(self.buf)
self.title, self.authors, self.identifiers = (title, authors,
identifiers)
self.timeout, self.rq = timeout, rq
self.time_spent = None
def run(self):
start_time = time.time()
if not self.abort.is_set():
try:
self.plugin.download_cover(self.log, self.rq, self.abort,
title=self.title, authors=self.authors,
identifiers=self.identifiers, timeout=self.timeout)
except:
self.log.exception('Failed to download cover from',
self.plugin.name)
self.time_spent = time.time() - start_time
def is_worker_alive(workers):
for w in workers:
if w.is_alive():
return True
return False
def process_result(log, result):
plugin, data = result
try:
im = Image()
im.load(data)
im.trim(10)
width, height = im.size
fmt = im.format
if width < 50 or height < 50:
raise ValueError('Image too small')
data = save_cover_data_to(im, '/cover.jpg', return_data=True)
except:
log.exception('Invalid cover from', plugin.name)
return None
return (plugin, width, height, fmt, data)
def run_download(log, results, abort,
title=None, authors=None, identifiers={}, timeout=30):
'''
Run the cover download, putting results into the queue :param:`results`.
Each result is a tuple of the form:
(plugin, width, height, fmt, bytes)
'''
plugins = [p for p in metadata_plugins(['cover']) if p.is_configured()]
rq = Queue()
workers = [Worker(p, abort, title, authors, identifiers, timeout, rq) for p
in plugins]
for w in workers:
w.start()
first_result_at = None
wait_time = msprefs['wait_after_first_cover_result']
found_results = {}
while True:
time.sleep(0.1)
try:
x = rq.get_nowait()
result = process_result(log, x)
if result is not None:
results.put(result)
found_results[result[0]] = result
if first_result_at is not None:
first_result_at = time.time()
except Empty:
pass
if not is_worker_alive(workers):
break
if first_result_at is not None and time.time() - first_result_at > wait_time:
log('Not waiting for any more results')
abort.set()
if abort.is_set():
break
while True:
try:
x = rq.get_nowait()
result = process_result(log, x)
if result is not None:
results.put(result)
found_results[result[0]] = result
except Empty:
break
for w in workers:
wlog = w.buf.getvalue().strip()
log('\n'+'*'*30, w.plugin.name, 'Covers', '*'*30)
log('Request extra headers:', w.plugin.browser.addheaders)
if w.plugin in found_results:
result = found_results[w.plugin]
log('Downloaded cover:', '%dx%d'%(result[1], result[2]))
else:
log('Failed to download valid cover')
if w.time_spent is None:
log('Download aborted')
else:
log('Took', w.time_spent, 'seconds')
if wlog:
log(wlog)
log('\n'+'*'*80)
def download_cover(log,
title=None, authors=None, identifiers={}, timeout=30):
'''
Synchronous cover download. Returns the "best" cover as per user
prefs/cover resolution.
Return cover is a tuple: (plugin, width, height, fmt, data)
Returns None if no cover is found.
'''
rq = Queue()
abort = Event()
run_download(log, rq, abort, title=title, authors=authors,
identifiers=identifiers, timeout=timeout)
results = []
while True:
try:
results.append(rq.get_nowait())
except Empty:
break
cp = msprefs['cover_priorities']
def keygen(result):
plugin, width, height, fmt, data = result
return (cp.get(plugin.name, 1), 1/(width*height))
results.sort(key=keygen)
return results[0] if results else None

View File

@ -145,21 +145,25 @@ def to_metadata(browser, log, entry_, timeout): # {{{
log.exception('Failed to parse rating') log.exception('Failed to parse rating')
# Cover # Cover
mi.has_google_cover = len(extra.xpath( mi.has_google_cover = None
'//*[@rel="http://schemas.google.com/books/2008/thumbnail"]')) > 0 for x in extra.xpath(
'//*[@href and @rel="http://schemas.google.com/books/2008/thumbnail"]'):
mi.has_google_cover = x.get('href')
break
return mi return mi
# }}} # }}}
class GoogleBooks(Source): class GoogleBooks(Source):
name = 'Google Books' name = 'Google'
description = _('Downloads metadata from Google Books') description = _('Downloads metadata from Google Books')
capabilities = frozenset(['identify', 'cover']) capabilities = frozenset(['identify', 'cover'])
touched_fields = frozenset(['title', 'authors', 'tags', 'pubdate', touched_fields = frozenset(['title', 'authors', 'tags', 'pubdate',
'comments', 'publisher', 'identifier:isbn', 'rating', 'comments', 'publisher', 'identifier:isbn', 'rating',
'identifier:google']) # language currently disabled 'identifier:google']) # language currently disabled
supports_gzip_transfer_encoding = True
GOOGLE_COVER = 'http://books.google.com/books?id=%s&printsec=frontcover&img=1' GOOGLE_COVER = 'http://books.google.com/books?id=%s&printsec=frontcover&img=1'
@ -212,7 +216,7 @@ class GoogleBooks(Source):
results.sort(key=self.identify_results_keygen( results.sort(key=self.identify_results_keygen(
title=title, authors=authors, identifiers=identifiers)) title=title, authors=authors, identifiers=identifiers))
for mi in results: for mi in results:
cached_url = self.cover_url_from_identifiers(mi.identifiers) cached_url = self.get_cached_cover_url(mi.identifiers)
if cached_url is not None: if cached_url is not None:
break break
if cached_url is None: if cached_url is None:
@ -222,9 +226,10 @@ class GoogleBooks(Source):
if abort.is_set(): if abort.is_set():
return return
br = self.browser br = self.browser
log('Downloading cover from:', cached_url)
try: try:
cdata = br.open_novisit(cached_url, timeout=timeout).read() cdata = br.open_novisit(cached_url, timeout=timeout).read()
result_queue.put(cdata) result_queue.put((self, cdata))
except: except:
log.exception('Failed to download cover from:', cached_url) log.exception('Failed to download cover from:', cached_url)
@ -253,9 +258,9 @@ class GoogleBooks(Source):
goog = ans.identifiers['google'] goog = ans.identifiers['google']
for isbn in getattr(ans, 'all_isbns', []): for isbn in getattr(ans, 'all_isbns', []):
self.cache_isbn_to_identifier(isbn, goog) self.cache_isbn_to_identifier(isbn, goog)
if ans.has_google_cover: if ans.has_google_cover:
self.cache_identifier_to_cover_url(goog, self.cache_identifier_to_cover_url(goog,
self.GOOGLE_COVER%goog) self.GOOGLE_COVER%goog)
self.clean_downloaded_metadata(ans) self.clean_downloaded_metadata(ans)
result_queue.put(ans) result_queue.put(ans)
except: except:
@ -270,6 +275,9 @@ class GoogleBooks(Source):
identifiers={}, timeout=30): identifiers={}, timeout=30):
query = self.create_query(log, title=title, authors=authors, query = self.create_query(log, title=title, authors=authors,
identifiers=identifiers) identifiers=identifiers)
if not query:
log.error('Insufficient metadata to construct query')
return
br = self.browser br = self.browser
try: try:
raw = br.open_novisit(query, timeout=timeout).read() raw = br.open_novisit(query, timeout=timeout).read()

View File

@ -8,17 +8,21 @@ __copyright__ = '2011, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
import time import time
from datetime import datetime
from Queue import Queue, Empty from Queue import Queue, Empty
from threading import Thread from threading import Thread
from io import BytesIO from io import BytesIO
from operator import attrgetter
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, msprefs
from calibre.ebooks.metadata.xisbn import xisbn from calibre.ebooks.metadata.xisbn import xisbn
from calibre.ebooks.metadata.book.base import Metadata
from calibre.utils.date import utc_tz
from calibre.utils.html2text import html2text
from calibre.utils.icu import lower
# How long to wait for more results after first result is found # Download worker {{{
WAIT_AFTER_FIRST_RESULT = 30 # seconds
class Worker(Thread): class Worker(Thread):
def __init__(self, plugin, kwargs, abort): def __init__(self, plugin, kwargs, abort):
@ -31,10 +35,12 @@ class Worker(Thread):
self.log = create_log(self.buf) self.log = create_log(self.buf)
def run(self): def run(self):
start = time.time()
try: try:
self.plugin.identify(self.log, self.rq, self.abort, **self.kwargs) self.plugin.identify(self.log, self.rq, self.abort, **self.kwargs)
except: except:
self.log.exception('Plugin', self.plugin.name, 'failed') self.log.exception('Plugin', self.plugin.name, 'failed')
self.plugin.dl_time_spent = time.time() - start
def is_worker_alive(workers): def is_worker_alive(workers):
for w in workers: for w in workers:
@ -42,28 +48,235 @@ def is_worker_alive(workers):
return True return True
return False return False
def identify(log, abort, title=None, authors=None, identifiers=[], timeout=30): # }}}
# Merge results from different sources {{{
class ISBNMerge(object):
def __init__(self):
self.pools = {}
self.isbnless_results = []
def isbn_in_pool(self, isbn):
if isbn:
for isbns, pool in self.pools.iteritems():
if isbn in isbns:
return pool
return None
def pool_has_result_from_same_source(self, pool, result):
results = pool[1]
for r in results:
if r.identify_plugin is result.identify_plugin:
return True
return False
def add_result(self, result):
isbn = result.isbn
if 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.pools[isbns] = pool = (min_year, [])
if not self.pool_has_result_from_same_source(pool, result):
pool[1].append(result)
else:
self.isbnless_results.append(result)
def finalize(self):
has_isbn_result = False
for results in self.pools.itervalues():
if results:
has_isbn_result = True
break
self.has_isbn_result = has_isbn_result
if has_isbn_result:
self.merge_isbn_results()
else:
results = sorted(self.isbnless_results,
key=attrgetter('relevance_in_source'))
# Pick only the most relevant result from each source
self.results = []
seen = set()
for result in results:
if result.identify_plugin not in seen:
seen.add(result.identify_plugin)
self.results.append(result)
result.average_source_relevance = \
result.relevance_in_source
self.merge_metadata_results()
return self.results
def merge_metadata_results(self):
' Merge results with identical title and authors '
groups = {}
for result in self.results:
title = lower(result.title if result.title else '')
key = (title, tuple([lower(x) for x in result.authors]))
if key not in groups:
groups[key] = []
groups[key].append(result)
if len(groups) != len(self.results):
self.results = []
for rgroup in groups.itervalues():
rel = [r.average_source_relevance for r in rgroup]
if len(rgroup) > 1:
result = self.merge(rgroup, None, do_asr=False)
result.average_source_relevance = sum(rel)/len(rel)
else:
result = rgroup[0]
self.results.append(result)
self.results.sort(key=attrgetter('average_source_relevance'))
def merge_isbn_results(self):
self.results = []
for min_year, results in self.pools.itervalues():
if results:
self.results.append(self.merge(results, min_year))
self.results.sort(key=attrgetter('average_source_relevance'))
def length_merge(self, attr, results, null_value=None, shortest=True):
values = [getattr(x, attr) for x in results if not x.is_null(attr)]
values = [x for x in values if len(x) > 0]
if not values:
return null_value
values.sort(key=len, reverse=not shortest)
return values[0]
def random_merge(self, attr, results, null_value=None):
values = [getattr(x, attr) for x in results if not x.is_null(attr)]
return values[0] if values else null_value
def merge(self, results, min_year, do_asr=True):
ans = Metadata(_('Unknown'))
# We assume the shortest title has the least cruft in it
ans.title = self.length_merge('title', results, null_value=ans.title)
# No harm in having extra authors, maybe something useful like an
# editor or translator
ans.authors = self.length_merge('authors', results,
null_value=ans.authors, shortest=False)
# We assume the shortest publisher has the least cruft in it
ans.publisher = self.length_merge('publisher', results,
null_value=ans.publisher)
# We assume the smallest set of tags has the least cruft in it
ans.tags = self.length_merge('tags', results,
null_value=ans.tags)
# We assume the longest series has the most info in it
ans.series = self.length_merge('series', results,
null_value=ans.series, shortest=False)
for r in results:
if r.series and r.series == ans.series:
ans.series_index = r.series_index
break
# Average the rating over all sources
ratings = []
for r in results:
rating = r.rating
if rating and rating > 0 and rating <= 5:
ratings.append(rating)
if ratings:
ans.rating = sum(ratings)/len(ratings)
# Smallest language is likely to be valid
ans.language = self.length_merge('language', results,
null_value=ans.language)
# Choose longest comments
ans.comments = self.length_merge('comments', results,
null_value=ans.comments, shortest=False)
# Published date
if min_year:
min_date = datetime(min_year, 1, 2, tzinfo=utc_tz)
ans.pubdate = min_date
else:
min_date = datetime(3001, 1, 1, tzinfo=utc_tz)
for r in results:
if r.pubdate is not None and r.pubdate < min_date:
min_date = r.pubdate
if min_date.year < 3000:
ans.pubdate = min_date
# Identifiers
for r in results:
ans.identifiers.update(r.identifiers)
# Cover URL
ans.has_cached_cover_url = bool([r for r in results if
getattr(r, 'has_cached_cover_url', False)])
# Merge any other fields with no special handling (random merge)
touched_fields = set()
for r in results:
if hasattr(r, 'identify_plugin'):
touched_fields |= r.identify_plugin.touched_fields
for f in touched_fields:
if f.startswith('identifier:') or not ans.is_null(f):
continue
setattr(ans, f, self.random_merge(f, results,
null_value=getattr(ans, f)))
if do_asr:
avg = [x.relevance_in_source for x in results]
avg = sum(avg)/len(avg)
ans.average_source_relevance = avg
return ans
def merge_identify_results(result_map, log):
isbn_merge = ISBNMerge()
for plugin, results in result_map.iteritems():
for result in results:
isbn_merge.add_result(result)
return isbn_merge.finalize()
# }}}
def identify(log, abort, # {{{
title=None, authors=None, identifiers={}, timeout=30):
start_time = time.time() start_time = time.time()
plugins = list(metadata_plugins['identify']) plugins = [p for p in metadata_plugins(['identify']) if p.is_configured()]
kwargs = { kwargs = {
'title': title, 'title': title,
'authors': authors, 'authors': authors,
'identifiers': identifiers, 'identifiers': identifiers,
'timeout': timeout, 'timeout': timeout,
} }
log('Running identify query with parameters:') log('Running identify query with parameters:')
log(kwargs) log(kwargs)
log('Using plugins:', ', '.join([p.name for p in plugins])) log('Using plugins:', ', '.join([p.name for p in plugins]))
log('The log (if any) from individual plugins is below') log('The log from individual plugins is below')
workers = [Worker(p, kwargs, abort) for p in plugins] workers = [Worker(p, kwargs, abort) for p in plugins]
for w in workers: for w in workers:
w.start() w.start()
first_result_at = None first_result_at = None
results = dict.fromkeys(plugins, []) results = {}
for p in plugins:
results[p] = []
logs = dict([(w.plugin, w.buf) for w in workers])
def get_results(): def get_results():
found = False found = False
@ -77,6 +290,7 @@ def identify(log, abort, title=None, authors=None, identifiers=[], timeout=30):
found = True found = True
return found return found
wait_time = msprefs['wait_after_first_identify_result']
while True: while True:
time.sleep(0.2) time.sleep(0.2)
@ -86,76 +300,118 @@ def identify(log, abort, title=None, authors=None, identifiers=[], timeout=30):
if not is_worker_alive(workers): if not is_worker_alive(workers):
break break
if (first_result_at is not None and time.time() - first_result_at < if (first_result_at is not None and time.time() - first_result_at >
WAIT_AFTER_FIRST_RESULT): wait_time):
log('Not waiting any longer for more results') log('Not waiting any longer for more results')
abort.set() abort.set()
break break
get_results() while not abort.is_set() and get_results():
pass
sort_kwargs = dict(kwargs) sort_kwargs = dict(kwargs)
for k in list(sort_kwargs.iterkeys()): for k in list(sort_kwargs.iterkeys()):
if k not in ('title', 'authors', 'identifiers'): if k not in ('title', 'authors', 'identifiers'):
sort_kwargs.pop(k) sort_kwargs.pop(k)
for plugin, results in results.iteritems(): longest, lp = -1, ''
results.sort(key=plugin.identify_results_keygen(**sort_kwargs)) for plugin, presults in results.iteritems():
plog = plugin.buf.getvalue().strip() presults.sort(key=plugin.identify_results_keygen(**sort_kwargs))
plog = logs[plugin].getvalue().strip()
log('\n'+'*'*30, plugin.name, '*'*30)
log('Request extra headers:', plugin.browser.addheaders)
log('Found %d results'%len(presults))
time_spent = getattr(plugin, 'dl_time_spent', None)
if time_spent is None:
log('Downloading was aborted')
longest, lp = -1, plugin.name
else:
log('Downloading from', plugin.name, 'took', time_spent)
if time_spent > longest:
longest, lp = time_spent, plugin.name
for r in presults:
log('\n\n---')
log(unicode(r))
if plog: if plog:
log('\n'+'*'*35, plugin.name, '*'*35)
log('Found %d results'%len(results))
log(plog) log(plog)
log('\n'+'*'*80) log('\n'+'*'*80)
for i, result in enumerate(results): for i, result in enumerate(presults):
result.relevance_in_source = i result.relevance_in_source = i
result.has_cached_cover_url = \ result.has_cached_cover_url = \
plugin.get_cached_cover_url(result.identifiers) is not None plugin.get_cached_cover_url(result.identifiers) is not None
result.identify_plugin = plugin result.identify_plugin = plugin
log('The identify phase took %.2f seconds'%(time.time() - start_time)) log('The identify phase took %.2f seconds'%(time.time() - start_time))
log('The longest time (%f) was taken by:'%longest, lp)
log('Merging results from different sources and finding earliest', log('Merging results from different sources and finding earliest',
'publication dates') 'publication dates')
start_time = time.time() start_time = time.time()
merged_results = merge_identify_results(results, log) results = merge_identify_results(results, log)
log('We have %d merged results, merging took: %.2f seconds' % log('We have %d merged results, merging took: %.2f seconds' %
(len(merged_results), time.time() - start_time)) (len(results), time.time() - start_time))
class ISBNMerge(object): if msprefs['txt_comments']:
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: for r in results:
if r.identify_plugin is result.identify_plugin: if r.plugin.has_html_comments and r.comments:
return True r.comments = html2text(r.comments)
return False
def add_result(self, result, isbn): dummy = Metadata(_('Unknown'))
pool = self.isbn_in_pool(isbn) max_tags = msprefs['max_tags']
if pool is None: for r in results:
isbns, min_year = xisbn.get_isbn_pool(isbn) for f in msprefs['ignore_fields']:
if not isbns: setattr(r, f, getattr(dummy, f))
isbns = frozenset([isbn]) r.tags = r.tags[:max_tags]
self.pool[isbns] = pool = (min_year, [])
if not self.pool_has_result_from_same_source(pool, result): return results
pool[1].append(result) # }}}
def merge_identify_results(result_map, log): if __name__ == '__main__': # tests {{{
for plugin, results in result_map.iteritems(): # To run these test use: calibre-debug -e
for result in results: # src/calibre/ebooks/metadata/sources/identify.py
isbn = result.isbn from calibre.ebooks.metadata.sources.test import (test_identify,
if isbn: title_test, authors_test)
isbns, min_year = xisbn.get_isbn_pool(isbn) tests = [
( # An e-book ISBN not on Amazon, one of the authors is
# unknown to Amazon
{'identifiers':{'isbn': '9780307459671'},
'title':'Invisible Gorilla', 'authors':['Christopher Chabris']},
[title_test('The Invisible Gorilla: And Other Ways Our Intuitions Deceive Us',
exact=True), authors_test(['Christopher Chabris', 'Daniel Simons'])]
),
( # Test absence of identifiers
{'title':'Learning Python',
'authors':['Lutz']},
[title_test('Learning Python',
exact=True), authors_test(['Mark Lutz'])
]
),
( # Sophisticated comment formatting
{'identifiers':{'isbn': '9781416580829'}},
[title_test('Angels & Demons',
exact=True), authors_test(['Dan Brown'])]
),
( # No ISBN
{'title':'Justine', 'authors':['Durrel']},
[title_test('Justine', exact=True),
authors_test(['Lawrence Durrel'])]
),
( # A newer book
{'identifiers':{'isbn': '9780316044981'}},
[title_test('The Heroes', exact=True),
authors_test(['Joe Abercrombie'])]
),
]
#test_identify(tests[1:2])
test_identify(tests)
# }}}

View File

@ -0,0 +1,43 @@
#!/usr/bin/env python
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
from __future__ import (unicode_literals, division, absolute_import,
print_function)
__license__ = 'GPL v3'
__copyright__ = '2011, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
from calibre.ebooks.metadata.sources.base import Source
class ISBNDB(Source):
name = 'ISBNDB'
description = _('Downloads metadata from isbndb.com')
capabilities = frozenset(['identify'])
touched_fields = frozenset(['title', 'authors',
'identifier:isbn', 'comments', 'publisher'])
supports_gzip_transfer_encoding = True
def __init__(self, *args, **kwargs):
Source.__init__(self, *args, **kwargs)
prefs = self.prefs
prefs.defaults['key_migrated'] = False
prefs.defaults['isbndb_key'] = None
if not prefs['key_migrated']:
prefs['key_migrated'] = True
try:
from calibre.customize.ui import config
key = config['plugin_customization']['IsbnDB']
prefs['isbndb_key'] = key
except:
pass
self.isbndb_key = prefs['isbndb_key']
def is_configured(self):
return self.isbndb_key is not None

View File

@ -26,7 +26,7 @@ class OpenLibrary(Source):
br = self.browser br = self.browser
try: try:
ans = br.open_novisit(self.OPENLIBRARY%isbn, timeout=timeout).read() ans = br.open_novisit(self.OPENLIBRARY%isbn, timeout=timeout).read()
result_queue.put(ans) result_queue.put((self, ans))
except Exception as e: except Exception as e:
if callable(getattr(e, 'getcode', None)) and e.getcode() == 404: if callable(getattr(e, 'getcode', None)) and e.getcode() == 404:
log.error('No cover for ISBN: %r found'%isbn) log.error('No cover for ISBN: %r found'%isbn)

View File

@ -14,7 +14,8 @@ from threading import Event
from calibre.customize.ui import metadata_plugins from calibre.customize.ui import metadata_plugins
from calibre import prints, sanitize_file_name2 from calibre import prints, sanitize_file_name2
from calibre.ebooks.metadata import check_isbn from calibre.ebooks.metadata import check_isbn
from calibre.ebooks.metadata.sources.base import create_log from calibre.ebooks.metadata.sources.base import (create_log,
get_cached_cover_urls)
def isbn_test(isbn): def isbn_test(isbn):
isbn_ = check_isbn(isbn) isbn_ = check_isbn(isbn)
@ -45,8 +46,80 @@ def authors_test(authors):
return test return test
def init_test(tdir_name):
tdir = tempfile.gettempdir()
lf = os.path.join(tdir, tdir_name.replace(' ', '')+'_identify_test.txt')
log = create_log(open(lf, 'wb'))
abort = Event()
return tdir, lf, log, abort
def test_identify_plugin(name, tests): def test_identify(tests): # {{{
'''
:param tests: List of 2-tuples. Each two tuple is of the form (args,
test_funcs). args is a dict of keyword arguments to pass to
the identify method. test_funcs are callables that accept a
Metadata object and return True iff the object passes the
test.
'''
from calibre.ebooks.metadata.sources.identify import identify
tdir, lf, log, abort = init_test('Full Identify')
prints('Log saved to', lf)
times = []
for kwargs, test_funcs in tests:
log('#'*80)
log('### Running test with:', kwargs)
log('#'*80)
prints('Running test with:', kwargs)
args = (log, abort)
start_time = time.time()
results = identify(*args, **kwargs)
total_time = time.time() - start_time
times.append(total_time)
if not results:
prints('identify failed to find any results')
break
prints('Found', len(results), 'matches:', end=' ')
prints('Smaller relevance means better match')
for i, mi in enumerate(results):
prints('*'*30, 'Relevance:', i, '*'*30)
prints(mi)
prints('\nCached cover URLs :',
[x[0].name for x in get_cached_cover_urls(mi)])
prints('*'*75, '\n\n')
possibles = []
for mi in results:
test_failed = False
for tfunc in test_funcs:
if not tfunc(mi):
test_failed = True
break
if not test_failed:
possibles.append(mi)
if not possibles:
prints('ERROR: No results that passed all tests were found')
prints('Log saved to', lf)
raise SystemExit(1)
if results[0] is not possibles[0]:
prints('Most relevant result failed the tests')
raise SystemExit(1)
log('\n\n')
prints('Average time per query', sum(times)/len(times))
prints('Full log is at:', lf)
# }}}
def test_identify_plugin(name, tests): # {{{
''' '''
:param name: Plugin name :param name: Plugin name
:param tests: List of 2-tuples. Each two tuple is of the form (args, :param tests: List of 2-tuples. Each two tuple is of the form (args,
@ -61,11 +134,9 @@ def test_identify_plugin(name, tests):
plugin = x plugin = x
break break
prints('Testing the identify function of', plugin.name) prints('Testing the identify function of', plugin.name)
prints('Using extra headers:', plugin.browser.addheaders)
tdir = tempfile.gettempdir() tdir, lf, log, abort = init_test(plugin.name)
lf = os.path.join(tdir, plugin.name.replace(' ', '')+'_identify_test.txt')
log = create_log(open(lf, 'wb'))
abort = Event()
prints('Log saved to', lf) prints('Log saved to', lf)
times = [] times = []
@ -159,4 +230,5 @@ def test_identify_plugin(name, tests):
if os.stat(lf).st_size > 10: if os.stat(lf).st_size > 10:
prints('There were some errors/warnings, see log', lf) prints('There were some errors/warnings, see log', lf)
# }}}

View File

@ -73,7 +73,11 @@ class xISBN(object):
def get_isbn_pool(self, isbn): def get_isbn_pool(self, isbn):
data = self.get_data(isbn) data = self.get_data(isbn)
isbns = frozenset([x.get('isbn') for x in data if 'isbn' in x]) raw = tuple(x.get('isbn') for x in data if 'isbn' in x)
isbns = []
for x in raw:
isbns += x
isbns = frozenset(isbns)
min_year = 100000 min_year = 100000
for x in data: for x in data:
try: try:

View File

@ -282,8 +282,8 @@ class Serializer(object):
buffer.write('="') buffer.write('="')
self.serialize_text(val, quot=True) self.serialize_text(val, quot=True)
buffer.write('"') buffer.write('"')
buffer.write('>')
if elem.text or len(elem) > 0: if elem.text or len(elem) > 0:
buffer.write('>')
if elem.text: if elem.text:
self.anchor_offset = None self.anchor_offset = None
self.serialize_text(elem.text) self.serialize_text(elem.text)
@ -292,9 +292,7 @@ class Serializer(object):
if child.tail: if child.tail:
self.anchor_offset = None self.anchor_offset = None
self.serialize_text(child.tail) self.serialize_text(child.tail)
buffer.write('</%s>' % tag) buffer.write('</%s>' % tag)
else:
buffer.write('/>')
def serialize_text(self, text, quot=False): def serialize_text(self, text, quot=False):
text = text.replace('&', '&amp;') text = text.replace('&', '&amp;')

View File

@ -17,6 +17,8 @@ from cssutils.css import CSSStyleRule, CSSPageRule, CSSStyleDeclaration, \
from cssutils import profile as cssprofiles from cssutils import profile as cssprofiles
from lxml import etree from lxml import etree
from lxml.cssselect import css_to_xpath, ExpressionError, SelectorSyntaxError from lxml.cssselect import css_to_xpath, ExpressionError, SelectorSyntaxError
from calibre import force_unicode
from calibre.ebooks.oeb.base import XHTML, XHTML_NS, CSS_MIME, OEB_STYLES from calibre.ebooks.oeb.base import XHTML, XHTML_NS, CSS_MIME, OEB_STYLES
from calibre.ebooks.oeb.base import XPNSMAP, xpath, urlnormalize from calibre.ebooks.oeb.base import XPNSMAP, xpath, urlnormalize
from calibre.ebooks.oeb.profile import PROFILES from calibre.ebooks.oeb.profile import PROFILES
@ -95,6 +97,10 @@ class CSSSelector(etree.XPath):
def __init__(self, css, namespaces=XPNSMAP): def __init__(self, css, namespaces=XPNSMAP):
css = self.MIN_SPACE_RE.sub(r'\1', css) css = self.MIN_SPACE_RE.sub(r'\1', css)
if isinstance(css, unicode):
# Workaround for bug in lxml on windows/OS X that causes a massive
# memory leak with non ASCII selectors
css = css.encode('ascii', 'ignore').decode('ascii')
try: try:
path = css_to_xpath(css) path = css_to_xpath(css)
except UnicodeEncodeError: # Bug in css_to_xpath except UnicodeEncodeError: # Bug in css_to_xpath
@ -140,13 +146,22 @@ class Stylizer(object):
log=logging.getLogger('calibre.css')) log=logging.getLogger('calibre.css'))
self.font_face_rules = [] self.font_face_rules = []
for elem in head: for elem in head:
if elem.tag == XHTML('style') and elem.text \ if (elem.tag == XHTML('style') and
and elem.get('type', CSS_MIME) in OEB_STYLES: elem.get('type', CSS_MIME) in OEB_STYLES):
text = XHTML_CSS_NAMESPACE + elem.text text = elem.text if elem.text else u''
text = oeb.css_preprocessor(text) for x in elem:
stylesheet = parser.parseString(text, href=cssname) t = getattr(x, 'text', None)
stylesheet.namespaces['h'] = XHTML_NS if t:
stylesheets.append(stylesheet) text += u'\n\n' + force_unicode(t, u'utf-8')
t = getattr(x, 'tail', None)
if t:
text += u'\n\n' + force_unicode(t, u'utf-8')
if text:
text = XHTML_CSS_NAMESPACE + elem.text
text = oeb.css_preprocessor(text)
stylesheet = parser.parseString(text, href=cssname)
stylesheet.namespaces['h'] = XHTML_NS
stylesheets.append(stylesheet)
elif elem.tag == XHTML('link') and elem.get('href') \ elif elem.tag == XHTML('link') and elem.get('href') \
and elem.get('rel', 'stylesheet').lower() == 'stylesheet' \ and elem.get('rel', 'stylesheet').lower() == 'stylesheet' \
and elem.get('type', CSS_MIME).lower() in OEB_STYLES: and elem.get('type', CSS_MIME).lower() in OEB_STYLES:

View File

@ -20,7 +20,8 @@ class RemoveAdobeMargins(object):
self.oeb, self.opts, self.log = oeb, opts, log self.oeb, self.opts, self.log = oeb, opts, log
for item in self.oeb.manifest: for item in self.oeb.manifest:
if item.media_type == 'application/vnd.adobe-page-template+xml': if item.media_type in ('application/vnd.adobe-page-template+xml',
'application/vnd.adobe.page-template+xml'):
self.log('Removing page margins specified in the' self.log('Removing page margins specified in the'
' Adobe page template') ' Adobe page template')
for elem in item.data.xpath( for elem in item.data.xpath(
@ -35,7 +36,7 @@ class RemoveFakeMargins(object):
''' '''
Remove left and right margins from paragraph/divs if the same margin is specified Remove left and right margins from paragraph/divs if the same margin is specified
on almost all the elements of at that level. on almost all the elements at that level.
Must be called only after CSS flattening Must be called only after CSS flattening
''' '''

View File

@ -72,6 +72,7 @@ XMLFont::XMLFont(string* font_name, double size, GfxRGB rgb) :
size(size-1), line_size(-1.0), italic(false), bold(false), font_name(font_name), size(size-1), line_size(-1.0), italic(false), bold(false), font_name(font_name),
font_family(NULL), color(rgb) { font_family(NULL), color(rgb) {
if (!this->font_name) this->font_name = new string(DEFAULT_FONT_FAMILY); if (!this->font_name) this->font_name = new string(DEFAULT_FONT_FAMILY);
this->font_family = family_name(this->font_name); this->font_family = family_name(this->font_name);
if (strcasestr(font_name->c_str(), "bold")) this->bold = true; if (strcasestr(font_name->c_str(), "bold")) this->bold = true;
@ -134,7 +135,12 @@ Fonts::size_type Fonts::add_font(XMLFont *f) {
} }
Fonts::size_type Fonts::add_font(string* font_name, double size, GfxRGB rgb) { Fonts::size_type Fonts::add_font(string* font_name, double size, GfxRGB rgb) {
XMLFont *f = new XMLFont(font_name, size, rgb); XMLFont *f = NULL;
if (font_name == NULL)
font_name = new string("Unknown");
// font_name must not be deleted
f = new XMLFont(font_name, size, rgb);
return this->add_font(f); return this->add_font(f);
} }

View File

@ -37,7 +37,7 @@ class MarkdownMLizer(object):
if not self.opts.keep_links: if not self.opts.keep_links:
html = re.sub(r'<\s*/*\s*a[^>]*>', '', html) html = re.sub(r'<\s*/*\s*a[^>]*>', '', html)
if not self.opts.keep_image_references: if not self.opts.keep_image_references:
html = re.sub(r'<\s*img[^>]*>', '', html)\ html = re.sub(r'<\s*img[^>]*>', '', html)
text = html2text(html) text = html2text(html)

View File

@ -11,7 +11,7 @@ from lxml import etree
from calibre.customize.conversion import OutputFormatPlugin, \ from calibre.customize.conversion import OutputFormatPlugin, \
OptionRecommendation OptionRecommendation
from calibre.ebooks.oeb.base import OEB_IMAGES from calibre.ebooks.oeb.base import OEB_IMAGES
from calibre.ebooks.txt.txtml import TXTMLizer from calibre.ebooks.txt.txtml import TXTMLizer
from calibre.ebooks.txt.newlines import TxtNewlines, specified_newlines from calibre.ebooks.txt.newlines import TxtNewlines, specified_newlines
from calibre.ptempfile import TemporaryDirectory, TemporaryFile from calibre.ptempfile import TemporaryDirectory, TemporaryFile

View File

@ -145,11 +145,10 @@ class InterfaceAction(QObject):
ans[candidate] = zf.read(candidate) ans[candidate] = zf.read(candidate)
return ans return ans
def genesis(self): def genesis(self):
''' '''
Setup this plugin. Only called once during initialization. self.gui is Setup this plugin. Only called once during initialization. self.gui is
available. The action secified by :attr:`action_spec` is available as available. The action specified by :attr:`action_spec` is available as
``self.qaction``. ``self.qaction``.
''' '''
pass pass

View File

@ -31,10 +31,10 @@ class GenerateCatalogAction(InterfaceAction):
_('No books selected for catalog generation'), _('No books selected for catalog generation'),
show=True) show=True)
db = self.gui.library_view.model().db db = self.gui.library_view.model().db
dbspec = {} dbspec = {}
for id in ids: for id in ids:
dbspec[id] = {'ondevice': db.ondevice(id, index_is_id=True)} dbspec[id] = {'ondevice': db.ondevice(id, index_is_id=True)}
# Calling gui2.tools:generate_catalog() # Calling gui2.tools:generate_catalog()
ret = generate_catalog(self.gui, dbspec, ids, self.gui.device_manager, ret = generate_catalog(self.gui, dbspec, ids, self.gui.device_manager,

View File

@ -8,7 +8,7 @@ __docformat__ = 'restructuredtext en'
import os, shutil import os, shutil
from functools import partial from functools import partial
from PyQt4.Qt import QMenu, Qt, QInputDialog from PyQt4.Qt import QMenu, Qt, QInputDialog, QToolButton
from calibre import isbytestring from calibre import isbytestring
from calibre.constants import filesystem_encoding from calibre.constants import filesystem_encoding
@ -88,6 +88,9 @@ class ChooseLibraryAction(InterfaceAction):
type=Qt.QueuedConnection) type=Qt.QueuedConnection)
self.stats = LibraryUsageStats() self.stats = LibraryUsageStats()
self.popup_type = (QToolButton.InstantPopup if len(self.stats.stats) > 1 else
QToolButton.MenuButtonPopup)
self.create_action(spec=(_('Switch/create library...'), 'lt.png', None, self.create_action(spec=(_('Switch/create library...'), 'lt.png', None,
None), attr='action_choose') None), attr='action_choose')
self.action_choose.triggered.connect(self.choose_library, self.action_choose.triggered.connect(self.choose_library,
@ -123,6 +126,7 @@ class ChooseLibraryAction(InterfaceAction):
type=Qt.QueuedConnection) type=Qt.QueuedConnection)
self.choose_menu.addAction(ac) self.choose_menu.addAction(ac)
self.rename_separator = self.choose_menu.addSeparator() self.rename_separator = self.choose_menu.addSeparator()
self.maintenance_menu = QMenu(_('Library Maintenance')) self.maintenance_menu = QMenu(_('Library Maintenance'))
@ -172,6 +176,7 @@ class ChooseLibraryAction(InterfaceAction):
return return
db = self.gui.library_view.model().db db = self.gui.library_view.model().db
locations = list(self.stats.locations(db)) locations = list(self.stats.locations(db))
for ac in self.switch_actions: for ac in self.switch_actions:
ac.setVisible(False) ac.setVisible(False)
self.quick_menu.clear() self.quick_menu.clear()

View File

@ -17,6 +17,7 @@ from calibre.gui2.dialogs.confirm_delete import confirm
from calibre.gui2.dialogs.tag_list_editor import TagListEditor from calibre.gui2.dialogs.tag_list_editor import TagListEditor
from calibre.gui2.actions import InterfaceAction from calibre.gui2.actions import InterfaceAction
from calibre.utils.icu import sort_key from calibre.utils.icu import sort_key
from calibre.utils.config import test_eight_code
class EditMetadataAction(InterfaceAction): class EditMetadataAction(InterfaceAction):
@ -133,8 +134,6 @@ class EditMetadataAction(InterfaceAction):
row_list = [r.row() for r in rows] row_list = [r.row() for r in rows]
current_row = 0 current_row = 0
changed = set([])
db = self.gui.library_view.model().db
if len(row_list) == 1: if len(row_list) == 1:
cr = row_list[0] cr = row_list[0]
@ -142,6 +141,27 @@ class EditMetadataAction(InterfaceAction):
list(range(self.gui.library_view.model().rowCount(QModelIndex()))) list(range(self.gui.library_view.model().rowCount(QModelIndex())))
current_row = row_list.index(cr) current_row = row_list.index(cr)
func = (self.do_edit_metadata if test_eight_code else
self.do_edit_metadata_old)
changed, rows_to_refresh = func(row_list, current_row)
m = self.gui.library_view.model()
if rows_to_refresh:
m.refresh_rows(rows_to_refresh)
if changed:
m.refresh_ids(list(changed))
current = self.gui.library_view.currentIndex()
if self.gui.cover_flow:
self.gui.cover_flow.dataChanged()
m.current_changed(current, previous)
self.gui.tags_view.recount()
def do_edit_metadata_old(self, row_list, current_row):
changed = set([])
db = self.gui.library_view.model().db
while True: while True:
prev = next_ = None prev = next_ = None
if current_row > 0: if current_row > 0:
@ -166,16 +186,30 @@ class EditMetadataAction(InterfaceAction):
current_row += d.row_delta current_row += d.row_delta
self.gui.library_view.set_current_row(current_row) self.gui.library_view.set_current_row(current_row)
self.gui.library_view.scroll_to_row(current_row) self.gui.library_view.scroll_to_row(current_row)
return changed, set()
def do_edit_metadata(self, row_list, current_row):
from calibre.gui2.metadata.single import edit_metadata
db = self.gui.library_view.model().db
changed, rows_to_refresh = edit_metadata(db, row_list, current_row,
parent=self.gui, view_slot=self.view_format_callback,
set_current_callback=self.set_current_callback)
return changed, rows_to_refresh
def set_current_callback(self, id_):
db = self.gui.library_view.model().db
current_row = db.row(id_)
self.gui.library_view.set_current_row(current_row)
self.gui.library_view.scroll_to_row(current_row)
def view_format_callback(self, id_, fmt):
view = self.gui.iactions['View']
if id_ is None:
view._view_file(fmt)
else:
db = self.gui.library_view.model().db
view.view_format(db.row(id_), fmt)
if changed:
self.gui.library_view.model().refresh_ids(list(changed))
current = self.gui.library_view.currentIndex()
m = self.gui.library_view.model()
if self.gui.cover_flow:
self.gui.cover_flow.dataChanged()
m.current_changed(current, previous)
self.gui.tags_view.recount()
def edit_bulk_metadata(self, checked): def edit_bulk_metadata(self, checked):
''' '''

View File

@ -270,6 +270,8 @@ class BookInfo(QWebView):
<style type="text/css"> <style type="text/css">
body, td {background-color: transparent; font-size: %dpx; color: %s } body, td {background-color: transparent; font-size: %dpx; color: %s }
a { text-decoration: none; color: blue } a { text-decoration: none; color: blue }
div.description { margin-top: 0; padding-top: 0; text-indent: 0 }
table { margin-bottom: 0; padding-bottom: 0; }
</style> </style>
</head> </head>
<body> <body>
@ -278,9 +280,10 @@ class BookInfo(QWebView):
<html> <html>
'''%(f, c) '''%(f, c)
if self.vertical: if self.vertical:
extra = ''
if comments: if comments:
rows += u'<tr><td colspan="2">%s</td></tr>'%comments extra = u'<div class="description">%s</div>'%comments
self.setHtml(templ%(u'<table>%s</table>'%rows)) self.setHtml(templ%(u'<table>%s</table>%s'%(rows, extra)))
else: else:
left_pane = u'<table>%s</table>'%rows left_pane = u'<table>%s</table>'%rows
right_pane = u'<div>%s</div>'%comments right_pane = u'<div>%s</div>'%comments

View File

@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
__license__ = 'GPL 3'
__copyright__ = '2009, John Schember <john@nachtimwald.com>'
__docformat__ = 'restructuredtext en'
from calibre.gui2.convert.htmlz_output_ui import Ui_Form
from calibre.gui2.convert import Widget
format_model = None
class PluginWidget(Widget, Ui_Form):
TITLE = _('HTMLZ Output')
HELP = _('Options specific to')+' HTMLZ '+_('output')
COMMIT_NAME = 'htmlz_output'
ICON = I('mimetypes/html.png')
def __init__(self, parent, get_option, get_help, db=None, book_id=None):
Widget.__init__(self, parent, ['htmlz_css_type', 'htmlz_class_style'])
self.db, self.book_id = db, book_id
for x in get_option('htmlz_css_type').option.choices:
self.opt_htmlz_css_type.addItem(x)
for x in get_option('htmlz_class_style').option.choices:
self.opt_htmlz_class_style.addItem(x)
self.initialize_options(get_option, get_help, db, book_id)

View File

@ -0,0 +1,61 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Form</class>
<widget class="QWidget" name="Form">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>438</width>
<height>300</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="2" column="0">
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>246</height>
</size>
</property>
</spacer>
</item>
<item row="0" column="0">
<widget class="QLabel" name="label">
<property name="text">
<string>How to handle CSS</string>
</property>
<property name="buddy">
<cstring>opt_htmlz_css_type</cstring>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="opt_htmlz_css_type">
<property name="minimumContentsLength">
<number>20</number>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="label_2">
<property name="text">
<string>How to handle class based CSS</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QComboBox" name="opt_htmlz_class_style"/>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@ -62,7 +62,7 @@ class Bool(Base):
w = self.widgets[1] w = self.widgets[1]
items = [_('Yes'), _('No'), _('Undefined')] items = [_('Yes'), _('No'), _('Undefined')]
icons = [I('ok.png'), I('list_remove.png'), I('blank.png')] icons = [I('ok.png'), I('list_remove.png'), I('blank.png')]
if tweaks['bool_custom_columns_are_tristate'] == 'no': if not self.db.prefs.get('bools_are_tristate'):
items = items[:-1] items = items[:-1]
icons = icons[:-1] icons = icons[:-1]
for icon, text in zip(icons, items): for icon, text in zip(icons, items):
@ -70,7 +70,7 @@ class Bool(Base):
def setter(self, val): def setter(self, val):
val = {None: 2, False: 1, True: 0}[val] val = {None: 2, False: 1, True: 0}[val]
if tweaks['bool_custom_columns_are_tristate'] == 'no' and val == 2: if not self.db.prefs.get('bools_are_tristate') and val == 2:
val = 1 val = 1
self.widgets[1].setCurrentIndex(val) self.widgets[1].setCurrentIndex(val)
@ -549,7 +549,7 @@ class BulkBool(BulkBase, Bool):
value = None value = None
for book_id in book_ids: for book_id in book_ids:
val = self.db.get_custom(book_id, num=self.col_id, index_is_id=True) val = self.db.get_custom(book_id, num=self.col_id, index_is_id=True)
if tweaks['bool_custom_columns_are_tristate'] == 'no' and val is None: if not self.db.prefs.get('bools_are_tristate') and val is None:
val = False val = False
if value is not None and value != val: if value is not None and value != val:
return None return None
@ -559,7 +559,7 @@ class BulkBool(BulkBase, Bool):
def setup_ui(self, parent): def setup_ui(self, parent):
self.make_widgets(parent, QComboBox) self.make_widgets(parent, QComboBox)
items = [_('Yes'), _('No')] items = [_('Yes'), _('No')]
if tweaks['bool_custom_columns_are_tristate'] == 'no': if not self.db.prefs.get('bools_are_tristate'):
items.append('') items.append('')
else: else:
items.append(_('Undefined')) items.append(_('Undefined'))
@ -571,7 +571,7 @@ class BulkBool(BulkBase, Bool):
def getter(self): def getter(self):
val = self.main_widget.currentIndex() val = self.main_widget.currentIndex()
if tweaks['bool_custom_columns_are_tristate'] == 'no': if not self.db.prefs.get('bools_are_tristate'):
return {2: False, 1: False, 0: True}[val] return {2: False, 1: False, 0: True}[val]
else: else:
return {2: None, 1: False, 0: True}[val] return {2: None, 1: False, 0: True}[val]
@ -586,13 +586,13 @@ class BulkBool(BulkBase, Bool):
return return
val = self.gui_val val = self.gui_val
val = self.normalize_ui_val(val) val = self.normalize_ui_val(val)
if tweaks['bool_custom_columns_are_tristate'] == 'no' and val is None: if not self.db.prefs.get('bools_are_tristate') and val is None:
val = False val = False
self.db.set_custom_bulk(book_ids, val, num=self.col_id, notify=notify) self.db.set_custom_bulk(book_ids, val, num=self.col_id, notify=notify)
def a_c_checkbox_changed(self): def a_c_checkbox_changed(self):
if not self.ignore_change_signals: if not self.ignore_change_signals:
if tweaks['bool_custom_columns_are_tristate'] == 'no' and \ if not self.db.prefs.get('bools_are_tristate') and \
self.main_widget.currentIndex() == 2: self.main_widget.currentIndex() == 2:
self.a_c_checkbox.setChecked(False) self.a_c_checkbox.setChecked(False)
else: else:

View File

@ -7,15 +7,25 @@
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>917</width> <width>917</width>
<height>480</height> <height>492</height>
</rect> </rect>
</property> </property>
<property name="windowTitle"> <property name="windowTitle">
<string>Dialog</string> <string>Dialog</string>
</property> </property>
<property name="windowIcon">
<iconset resource="../../../../resources/images.qrc">
<normaloff>:/images/metadata.png</normaloff>:/images/metadata.png</iconset>
</property>
<layout class="QGridLayout" name="gridLayout"> <layout class="QGridLayout" name="gridLayout">
<item row="0" column="0" colspan="2"> <item row="0" column="0" colspan="2">
<widget class="QLabel" name="title"> <widget class="QLabel" name="title">
<property name="font">
<font>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="text"> <property name="text">
<string>TextLabel</string> <string>TextLabel</string>
</property> </property>
@ -24,86 +34,104 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="1" column="0"> <item row="1" column="0" rowspan="3">
<widget class="CoverView" name="cover"/> <widget class="CoverView" name="cover"/>
</item> </item>
<item row="1" column="1"> <item row="1" column="1">
<layout class="QVBoxLayout" name="verticalLayout"> <widget class="QScrollArea" name="scrollArea">
<item> <property name="frameShape">
<widget class="QLabel" name="text"> <enum>QFrame::NoFrame</enum>
<property name="text"> </property>
<string>TextLabel</string> <property name="widgetResizable">
</property> <bool>true</bool>
<property name="alignment"> </property>
<set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set> <widget class="QWidget" name="scrollAreaWidgetContents">
</property> <property name="geometry">
<property name="wordWrap"> <rect>
<bool>true</bool> <x>0</x>
</property> <y>0</y>
</widget> <width>435</width>
</item> <height>670</height>
<item> </rect>
<widget class="QGroupBox" name="groupBox"> </property>
<property name="title"> <layout class="QVBoxLayout" name="verticalLayout">
<string>Comments</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QWebView" name="comments">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="maximumSize">
<size>
<width>350</width>
<height>16777215</height>
</size>
</property>
<property name="url">
<url>
<string>about:blank</string>
</url>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QCheckBox" name="fit_cover">
<property name="text">
<string>Fit &amp;cover within view</string>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item> <item>
<widget class="QPushButton" name="previous_button"> <widget class="QLabel" name="text">
<property name="text"> <property name="text">
<string>&amp;Previous</string> <string>TextLabel</string>
</property> </property>
<property name="icon"> <property name="alignment">
<iconset resource="../../../../resources/images.qrc"> <set>Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop</set>
<normaloff>:/images/previous.png</normaloff>:/images/previous.png</iconset> </property>
<property name="wordWrap">
<bool>true</bool>
</property> </property>
</widget> </widget>
</item> </item>
<item> <item>
<widget class="QPushButton" name="next_button"> <widget class="QGroupBox" name="groupBox">
<property name="text"> <property name="title">
<string>&amp;Next</string> <string>Comments</string>
</property>
<property name="icon">
<iconset resource="../../../../resources/images.qrc">
<normaloff>:/images/next.png</normaloff>:/images/next.png</iconset>
</property> </property>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QWebView" name="comments">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Expanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="maximumSize">
<size>
<width>350</width>
<height>16777215</height>
</size>
</property>
<property name="url">
<url>
<string>about:blank</string>
</url>
</property>
</widget>
</item>
</layout>
</widget> </widget>
</item> </item>
</layout> </layout>
</widget>
</widget>
</item>
<item row="2" column="1">
<widget class="QCheckBox" name="fit_cover">
<property name="text">
<string>Fit &amp;cover within view</string>
</property>
</widget>
</item>
<item row="3" column="1">
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QPushButton" name="previous_button">
<property name="text">
<string>&amp;Previous</string>
</property>
<property name="icon">
<iconset resource="../../../../resources/images.qrc">
<normaloff>:/images/previous.png</normaloff>:/images/previous.png</iconset>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="next_button">
<property name="text">
<string>&amp;Next</string>
</property>
<property name="icon">
<iconset resource="../../../../resources/images.qrc">
<normaloff>:/images/next.png</normaloff>:/images/next.png</iconset>
</property>
</widget>
</item> </item>
</layout> </layout>
</item> </item>

View File

@ -7,9 +7,9 @@ __docformat__ = 'restructuredtext en'
from functools import partial from functools import partial
from PyQt4.Qt import QIcon, Qt, QWidget, QToolBar, QSize, \ from PyQt4.Qt import (QIcon, Qt, QWidget, QToolBar, QSize,
pyqtSignal, QToolButton, QMenu, \ pyqtSignal, QToolButton, QMenu,
QObject, QVBoxLayout, QSizePolicy, QLabel, QHBoxLayout, QActionGroup QObject, QVBoxLayout, QSizePolicy, QLabel, QHBoxLayout, QActionGroup)
from calibre.constants import __appname__ from calibre.constants import __appname__
@ -264,11 +264,11 @@ class ToolBar(QToolBar): # {{{
def apply_settings(self): def apply_settings(self):
sz = gprefs['toolbar_icon_size'] sz = gprefs['toolbar_icon_size']
sz = {'small':24, 'medium':48, 'large':64}[sz] sz = {'off':0, 'small':24, 'medium':48, 'large':64}[sz]
self.setIconSize(QSize(sz, sz)) self.setIconSize(QSize(sz, sz))
self.child_bar.setIconSize(QSize(sz, sz)) self.child_bar.setIconSize(QSize(sz, sz))
style = Qt.ToolButtonTextUnderIcon style = Qt.ToolButtonTextUnderIcon
if gprefs['toolbar_text'] == 'never': if sz > 0 and gprefs['toolbar_text'] == 'never':
style = Qt.ToolButtonIconOnly style = Qt.ToolButtonIconOnly
self.setToolButtonStyle(style) self.setToolButtonStyle(style)
self.child_bar.setToolButtonStyle(style) self.child_bar.setToolButtonStyle(style)

View File

@ -353,7 +353,7 @@ class CcBoolDelegate(QStyledItemDelegate): # {{{
editor = DelegateCB(parent) editor = DelegateCB(parent)
items = [_('Y'), _('N'), ' '] items = [_('Y'), _('N'), ' ']
icons = [I('ok.png'), I('list_remove.png'), I('blank.png')] icons = [I('ok.png'), I('list_remove.png'), I('blank.png')]
if tweaks['bool_custom_columns_are_tristate'] == 'no': if not index.model().db.prefs.get('bools_are_tristate'):
items = items[:-1] items = items[:-1]
icons = icons[:-1] icons = icons[:-1]
for icon, text in zip(icons, items): for icon, text in zip(icons, items):
@ -367,7 +367,7 @@ class CcBoolDelegate(QStyledItemDelegate): # {{{
def setEditorData(self, editor, index): def setEditorData(self, editor, index):
m = index.model() m = index.model()
val = m.db.data[index.row()][m.custom_columns[m.column_map[index.column()]]['rec_index']] val = m.db.data[index.row()][m.custom_columns[m.column_map[index.column()]]['rec_index']]
if tweaks['bool_custom_columns_are_tristate'] == 'no': if not m.db.prefs.get('bools_are_tristate'):
val = 1 if not val else 0 val = 1 if not val else 0
else: else:
val = 2 if val is None else 1 if not val else 0 val = 2 if val is None else 1 if not val else 0

View File

@ -700,7 +700,7 @@ class BooksModel(QAbstractTableModel): # {{{
self.dc_decorator[col] = functools.partial( self.dc_decorator[col] = functools.partial(
bool_type_decorator, idx=idx, bool_type_decorator, idx=idx,
bool_cols_are_tristate= bool_cols_are_tristate=
tweaks['bool_custom_columns_are_tristate'] != 'no') self.db.prefs.get('bools_are_tristate'))
elif datatype in ('int', 'float'): elif datatype in ('int', 'float'):
self.dc[col] = functools.partial(number_type, idx=idx) self.dc[col] = functools.partial(number_type, idx=idx)
elif datatype == 'datetime': elif datatype == 'datetime':
@ -710,7 +710,7 @@ class BooksModel(QAbstractTableModel): # {{{
self.dc_decorator[col] = functools.partial( self.dc_decorator[col] = functools.partial(
bool_type_decorator, idx=idx, bool_type_decorator, idx=idx,
bool_cols_are_tristate= bool_cols_are_tristate=
tweaks['bool_custom_columns_are_tristate'] != 'no') self.db.prefs.get('bools_are_tristate'))
elif datatype == 'rating': elif datatype == 'rating':
self.dc[col] = functools.partial(rating_type, idx=idx) self.dc[col] = functools.partial(rating_type, idx=idx)
elif datatype == 'series': elif datatype == 'series':

View File

@ -1,5 +1,7 @@
#!/usr/bin/env python #!/usr/bin/env python
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai # vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
from __future__ import (unicode_literals, division, absolute_import,
print_function)
__license__ = 'GPL v3' __license__ = 'GPL v3'
__copyright__ = '2011, Kovid Goyal <kovid@kovidgoyal.net>' __copyright__ = '2011, Kovid Goyal <kovid@kovidgoyal.net>'
@ -7,10 +9,10 @@ __docformat__ = 'restructuredtext en'
import textwrap, re, os import textwrap, re, os
from PyQt4.Qt import Qt, QDateEdit, QDate, \ from PyQt4.Qt import (Qt, QDateEdit, QDate,
QIcon, QToolButton, QWidget, QLabel, QGridLayout, \ QIcon, QToolButton, QWidget, QLabel, QGridLayout,
QDoubleSpinBox, QListWidgetItem, QSize, QPixmap, \ QDoubleSpinBox, QListWidgetItem, QSize, QPixmap,
QPushButton, QSpinBox, QLineEdit QPushButton, QSpinBox, QLineEdit, QSizePolicy)
from calibre.gui2.widgets import EnLineEdit, FormatList, ImageView from calibre.gui2.widgets import EnLineEdit, FormatList, ImageView
from calibre.gui2.complete import MultiCompleteLineEdit, MultiCompleteComboBox from calibre.gui2.complete import MultiCompleteLineEdit, MultiCompleteComboBox
@ -22,7 +24,7 @@ from calibre.ebooks.metadata.meta import get_metadata
from calibre.gui2 import file_icon_provider, UNDEFINED_QDATE, UNDEFINED_DATE, \ from calibre.gui2 import file_icon_provider, UNDEFINED_QDATE, UNDEFINED_DATE, \
choose_files, error_dialog, choose_images, question_dialog choose_files, error_dialog, choose_images, question_dialog
from calibre.utils.date import local_tz, qt_to_dt from calibre.utils.date import local_tz, qt_to_dt
from calibre import strftime from calibre import strftime, fit_image
from calibre.ebooks import BOOK_EXTENSIONS from calibre.ebooks import BOOK_EXTENSIONS
from calibre.customize.ui import run_plugins_on_import from calibre.customize.ui import run_plugins_on_import
from calibre.utils.date import utcfromtimestamp from calibre.utils.date import utcfromtimestamp
@ -426,7 +428,7 @@ class Format(QListWidgetItem): # {{{
if timestamp is not None: if timestamp is not None:
ts = timestamp.astimezone(local_tz) ts = timestamp.astimezone(local_tz)
t = strftime('%a, %d %b %Y [%H:%M:%S]', ts.timetuple()) t = strftime('%a, %d %b %Y [%H:%M:%S]', ts.timetuple())
text = _('Last modified: %s')%t text = _('Last modified: %s\n\nDouble click to view')%t
self.setToolTip(text) self.setToolTip(text)
self.setStatusTip(text) self.setStatusTip(text)
@ -480,6 +482,7 @@ class FormatsManager(QWidget): # {{{
def initialize(self, db, id_): def initialize(self, db, id_):
self.changed = False self.changed = False
self.formats.clear()
exts = db.formats(id_, index_is_id=True) exts = db.formats(id_, index_is_id=True)
self.original_val = set([]) self.original_val = set([])
if exts: if exts:
@ -574,8 +577,7 @@ class FormatsManager(QWidget): # {{{
self.changed = True self.changed = True
def show_format(self, item, *args): def show_format(self, item, *args):
fmt = item.ext self.dialog.do_view_format(item.path, item.ext)
self.dialog.view_format.emit(fmt)
def get_selected_format_metadata(self, db, id_): def get_selected_format_metadata(self, db, id_):
old = prefs['read_file_metadata'] old = prefs['read_file_metadata']
@ -638,6 +640,23 @@ class Cover(ImageView): # {{{
self.trim_cover_button, self.download_cover_button, self.trim_cover_button, self.download_cover_button,
self.generate_cover_button] self.generate_cover_button]
self.frame_size = (300, 400)
self.setSizePolicy(QSizePolicy(QSizePolicy.Preferred,
QSizePolicy.Preferred))
def frame_resized(self, ev):
sz = ev.size()
self.frame_size = (sz.width()//3, sz.height())
def sizeHint(self):
sz = ImageView.sizeHint(self)
w, h = sz.width(), sz.height()
resized, nw, nh = fit_image(w, h, self.frame_size[0],
self.frame_size[1])
if resized:
sz = QSize(nw, nh)
return sz
def select_cover(self, *args): def select_cover(self, *args):
files = choose_images(self, 'change cover dialog', files = choose_images(self, 'change cover dialog',
_('Choose cover for ') + _('Choose cover for ') +
@ -882,8 +901,11 @@ class TagsEdit(MultiCompleteLineEdit): # {{{
# }}} # }}}
class ISBNEdit(QLineEdit): # {{{ class IdentifiersEdit(QLineEdit): # {{{
LABEL = _('IS&BN:') LABEL = _('I&ds:')
BASE_TT = _('Edit the identifiers for this book. '
'For example: \n\n%s')%(
'isbn:1565927249, doi:10.1000/182, amazon:1565927249')
def __init__(self, parent): def __init__(self, parent):
QLineEdit.__init__(self, parent) QLineEdit.__init__(self, parent)
@ -893,32 +915,44 @@ class ISBNEdit(QLineEdit): # {{{
@dynamic_property @dynamic_property
def current_val(self): def current_val(self):
def fget(self): def fget(self):
return self.pat.sub('', unicode(self.text()).strip()) raw = unicode(self.text()).strip()
parts = [x.strip() for x in raw.split(',')]
ans = {}
for x in parts:
c = x.split(':')
if len(c) == 2:
ans[c[0]] = c[1]
return ans
def fset(self, val): def fset(self, val):
if not val: if not val:
val = '' val = {}
self.setText(val.strip()) txt = ', '.join(['%s:%s'%(k, v) for k, v in val.iteritems()])
self.setText(txt.strip())
return property(fget=fget, fset=fset) return property(fget=fget, fset=fset)
def initialize(self, db, id_): def initialize(self, db, id_):
self.current_val = db.isbn(id_, index_is_id=True) self.current_val = db.get_identifiers(id_, index_is_id=True)
self.original_val = self.current_val self.original_val = self.current_val
def commit(self, db, id_): def commit(self, db, id_):
db.set_isbn(id_, self.current_val, notify=False, commit=False) if self.original_val != self.current_val:
db.set_identifiers(id_, self.current_val, notify=False, commit=False)
return True return True
def validate(self, *args): def validate(self, *args):
isbn = self.current_val identifiers = self.current_val
tt = _('This ISBN number is valid') isbn = identifiers.get('isbn', '')
tt = self.BASE_TT
extra = ''
if not isbn: if not isbn:
col = 'rgba(0,255,0,0%)' col = 'rgba(0,255,0,0%)'
elif check_isbn(isbn) is not None: elif check_isbn(isbn) is not None:
col = 'rgba(0,255,0,20%)' col = 'rgba(0,255,0,20%)'
extra = '\n\n'+_('This ISBN number is valid')
else: else:
col = 'rgba(255,0,0,20%)' col = 'rgba(255,0,0,20%)'
tt = _('This ISBN number is invalid') extra = '\n\n' + _('This ISBN number is invalid')
self.setToolTip(tt) self.setToolTip(tt+extra)
self.setStyleSheet('QLineEdit { background-color: %s }'%col) self.setStyleSheet('QLineEdit { background-color: %s }'%col)
# }}} # }}}

View File

@ -1,5 +1,7 @@
#!/usr/bin/env python #!/usr/bin/env python
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai # vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
from __future__ import (unicode_literals, division, absolute_import,
print_function)
__license__ = 'GPL v3' __license__ = 'GPL v3'
__copyright__ = '2011, Kovid Goyal <kovid@kovidgoyal.net>' __copyright__ = '2011, Kovid Goyal <kovid@kovidgoyal.net>'
@ -8,31 +10,31 @@ __docformat__ = 'restructuredtext en'
import os import os
from functools import partial from functools import partial
from PyQt4.Qt import Qt, QVBoxLayout, QHBoxLayout, QWidget, QPushButton, \ from PyQt4.Qt import (Qt, QVBoxLayout, QHBoxLayout, QWidget, QPushButton,
QGridLayout, pyqtSignal, QDialogButtonBox, QScrollArea, QFont, \ QGridLayout, pyqtSignal, QDialogButtonBox, QScrollArea, QFont,
QTabWidget, QIcon, QToolButton, QSplitter, QGroupBox, QSpacerItem, \ QTabWidget, QIcon, QToolButton, QSplitter, QGroupBox, QSpacerItem,
QSizePolicy, QPalette, QFrame, QSize, QKeySequence QSizePolicy, QPalette, QFrame, QSize, QKeySequence)
from calibre.ebooks.metadata import authors_to_string, string_to_authors from calibre.ebooks.metadata import authors_to_string, string_to_authors
from calibre.gui2 import ResizableDialog, error_dialog, gprefs from calibre.gui2 import ResizableDialog, error_dialog, gprefs
from calibre.gui2.metadata.basic_widgets import TitleEdit, AuthorsEdit, \ from calibre.gui2.metadata.basic_widgets import (TitleEdit, AuthorsEdit,
AuthorSortEdit, TitleSortEdit, SeriesEdit, SeriesIndexEdit, ISBNEdit, \ AuthorSortEdit, TitleSortEdit, SeriesEdit, SeriesIndexEdit, IdentifiersEdit,
RatingEdit, PublisherEdit, TagsEdit, FormatsManager, Cover, CommentsEdit, \ RatingEdit, PublisherEdit, TagsEdit, FormatsManager, Cover, CommentsEdit,
BuddyLabel, DateEdit, PubdateEdit BuddyLabel, DateEdit, PubdateEdit)
from calibre.gui2.custom_column_widgets import populate_metadata_page from calibre.gui2.custom_column_widgets import populate_metadata_page
from calibre.utils.config import tweaks from calibre.utils.config import tweaks
class MetadataSingleDialogBase(ResizableDialog): class MetadataSingleDialogBase(ResizableDialog):
view_format = pyqtSignal(object) view_format = pyqtSignal(object, object)
cc_two_column = tweaks['metadata_single_use_2_cols_for_custom_fields'] cc_two_column = tweaks['metadata_single_use_2_cols_for_custom_fields']
one_line_comments_toolbar = False one_line_comments_toolbar = False
def __init__(self, db, parent=None): def __init__(self, db, parent=None):
self.db = db self.db = db
self.changed = set([]) self.changed = set()
self.books_to_refresh = set([]) self.books_to_refresh = set()
self.rows_to_refresh = set([]) self.rows_to_refresh = set()
ResizableDialog.__init__(self, parent) ResizableDialog.__init__(self, parent)
def setupUi(self, *args): # {{{ def setupUi(self, *args): # {{{
@ -145,8 +147,8 @@ class MetadataSingleDialogBase(ResizableDialog):
self.tags_editor_button.clicked.connect(self.tags_editor) self.tags_editor_button.clicked.connect(self.tags_editor)
self.basic_metadata_widgets.append(self.tags) self.basic_metadata_widgets.append(self.tags)
self.isbn = ISBNEdit(self) self.identifiers = IdentifiersEdit(self)
self.basic_metadata_widgets.append(self.isbn) self.basic_metadata_widgets.append(self.identifiers)
self.publisher = PublisherEdit(self) self.publisher = PublisherEdit(self)
self.basic_metadata_widgets.append(self.publisher) self.basic_metadata_widgets.append(self.publisher)
@ -192,6 +194,13 @@ class MetadataSingleDialogBase(ResizableDialog):
pass # Do something pass # Do something
# }}} # }}}
def do_view_format(self, path, fmt):
if path:
self.view_format.emit(None, path)
else:
self.view_format.emit(self.book_id, fmt)
def do_layout(self): def do_layout(self):
raise NotImplementedError() raise NotImplementedError()
@ -202,6 +211,8 @@ class MetadataSingleDialogBase(ResizableDialog):
widget.initialize(self.db, id_) widget.initialize(self.db, id_)
for widget in getattr(self, 'custom_metadata_widgets', []): for widget in getattr(self, 'custom_metadata_widgets', []):
widget.initialize(id_) widget.initialize(id_)
if callable(self.set_current_callback):
self.set_current_callback(id_)
# Commented out as it doesn't play nice with Next, Prev buttons # Commented out as it doesn't play nice with Next, Prev buttons
#self.fetch_metadata_button.setFocus(Qt.OtherFocusReason) #self.fetch_metadata_button.setFocus(Qt.OtherFocusReason)
@ -280,8 +291,8 @@ class MetadataSingleDialogBase(ResizableDialog):
self.publisher.current_val = mi.publisher self.publisher.current_val = mi.publisher
if not mi.is_null('tags'): if not mi.is_null('tags'):
self.tags.current_val = mi.tags self.tags.current_val = mi.tags
if not mi.is_null('isbn'): if not mi.is_null('identifiers'):
self.isbn.current_val = mi.isbn self.identifiers.current_val = mi.identifiers
if not mi.is_null('pubdate'): if not mi.is_null('pubdate'):
self.pubdate.current_val = mi.pubdate self.pubdate.current_val = mi.pubdate
if not mi.is_null('series') and mi.series.strip(): if not mi.is_null('series') and mi.series.strip():
@ -337,11 +348,13 @@ class MetadataSingleDialogBase(ResizableDialog):
gprefs['metasingle_window_geometry3'] = bytearray(self.saveGeometry()) gprefs['metasingle_window_geometry3'] = bytearray(self.saveGeometry())
# Dialog use methods {{{ # Dialog use methods {{{
def start(self, row_list, current_row, view_slot=None): def start(self, row_list, current_row, view_slot=None,
set_current_callback=None):
self.row_list = row_list self.row_list = row_list
self.current_row = current_row self.current_row = current_row
if view_slot is not None: if view_slot is not None:
self.view_format.connect(view_slot) self.view_format.connect(view_slot)
self.set_current_callback = set_current_callback
self.do_one(apply_changes=False) self.do_one(apply_changes=False)
ret = self.exec_() ret = self.exec_()
self.break_cycles() self.break_cycles()
@ -373,6 +386,7 @@ class MetadataSingleDialogBase(ResizableDialog):
def break_cycles(self): def break_cycles(self):
# Break any reference cycles that could prevent python # Break any reference cycles that could prevent python
# from garbage collecting this dialog # from garbage collecting this dialog
self.set_current_callback = self.db = None
def disconnect(signal): def disconnect(signal):
try: try:
signal.disconnect() signal.disconnect()
@ -385,6 +399,14 @@ class MetadataSingleDialogBase(ResizableDialog):
disconnect(x.clicked) disconnect(x.clicked)
# }}} # }}}
class Splitter(QSplitter):
frame_resized = pyqtSignal(object)
def resizeEvent(self, ev):
self.frame_resized.emit(ev)
return QSplitter.resizeEvent(self, ev)
class MetadataSingleDialog(MetadataSingleDialogBase): # {{{ class MetadataSingleDialog(MetadataSingleDialogBase): # {{{
def do_layout(self): def do_layout(self):
@ -437,8 +459,9 @@ class MetadataSingleDialog(MetadataSingleDialogBase): # {{{
tl.addWidget(self.formats_manager, 0, 6, 3, 1) tl.addWidget(self.formats_manager, 0, 6, 3, 1)
self.splitter = QSplitter(Qt.Horizontal, self) self.splitter = Splitter(Qt.Horizontal, self)
self.splitter.addWidget(self.cover) self.splitter.addWidget(self.cover)
self.splitter.frame_resized.connect(self.cover.frame_resized)
l.addWidget(self.splitter) l.addWidget(self.splitter)
self.tabs[0].gb = gb = QGroupBox(_('Change cover'), self) self.tabs[0].gb = gb = QGroupBox(_('Change cover'), self)
gb.l = l = QGridLayout() gb.l = l = QGridLayout()
@ -475,9 +498,9 @@ class MetadataSingleDialog(MetadataSingleDialogBase): # {{{
create_row2(1, self.rating) create_row2(1, self.rating)
sto(self.rating, self.tags) sto(self.rating, self.tags)
create_row2(2, self.tags, self.tags_editor_button) create_row2(2, self.tags, self.tags_editor_button)
sto(self.tags_editor_button, self.isbn) sto(self.tags_editor_button, self.identifiers)
create_row2(3, self.isbn) create_row2(3, self.identifiers)
sto(self.isbn, self.timestamp) sto(self.identifiers, self.timestamp)
create_row2(4, self.timestamp, self.timestamp.clear_button) create_row2(4, self.timestamp, self.timestamp.clear_button)
sto(self.timestamp.clear_button, self.pubdate) sto(self.timestamp.clear_button, self.pubdate)
create_row2(5, self.pubdate, self.pubdate.clear_button) create_row2(5, self.pubdate, self.pubdate.clear_button)
@ -498,7 +521,7 @@ class MetadataSingleDialog(MetadataSingleDialogBase): # {{{
# }}} # }}}
class MetadataSingleDialogAlt(MetadataSingleDialogBase): # {{{ class MetadataSingleDialogAlt1(MetadataSingleDialogBase): # {{{
cc_two_column = False cc_two_column = False
one_line_comments_toolbar = True one_line_comments_toolbar = True
@ -562,9 +585,9 @@ class MetadataSingleDialogAlt(MetadataSingleDialogBase): # {{{
create_row(8, self.pubdate, self.publisher, create_row(8, self.pubdate, self.publisher,
button=self.pubdate.clear_button, icon='trash.png') button=self.pubdate.clear_button, icon='trash.png')
create_row(9, self.publisher, self.timestamp) create_row(9, self.publisher, self.timestamp)
create_row(10, self.timestamp, self.isbn, create_row(10, self.timestamp, self.identifiers,
button=self.timestamp.clear_button, icon='trash.png') button=self.timestamp.clear_button, icon='trash.png')
create_row(11, self.isbn, self.comments) create_row(11, self.identifiers, self.comments)
tl.addItem(QSpacerItem(1, 1, QSizePolicy.Fixed, QSizePolicy.Expanding), tl.addItem(QSpacerItem(1, 1, QSizePolicy.Fixed, QSizePolicy.Expanding),
12, 1, 1 ,1) 12, 1, 1 ,1)
@ -580,7 +603,7 @@ class MetadataSingleDialogAlt(MetadataSingleDialogBase): # {{{
sr.setWidget(w) sr.setWidget(w)
gbl.addWidget(sr) gbl.addWidget(sr)
self.tabs[0].l.addWidget(gb, 0, 1, 1, 1) self.tabs[0].l.addWidget(gb, 0, 1, 1, 1)
sto(self.isbn, gb) sto(self.identifiers, gb)
w = QGroupBox(_('&Comments'), tab0) w = QGroupBox(_('&Comments'), tab0)
sp = QSizePolicy() sp = QSizePolicy()
@ -631,10 +654,16 @@ class MetadataSingleDialogAlt(MetadataSingleDialogBase): # {{{
# }}} # }}}
editors = {'default': MetadataSingleDialog, 'alt1': MetadataSingleDialogAlt1}
def edit_metadata(db, row_list, current_row, parent=None, view_slot=None): def edit_metadata(db, row_list, current_row, parent=None, view_slot=None,
d = MetadataSingleDialog(db, parent) set_current_callback=None):
d.start(row_list, current_row, view_slot=view_slot) cls = db.prefs.get('edit_metadata_single_layout', '')
if cls not in editors:
cls = 'default'
d = editors[cls](db, parent)
d.start(row_list, current_row, view_slot=view_slot,
set_current_callback=set_current_callback)
return d.changed, d.rows_to_refresh return d.changed, d.rows_to_refresh
if __name__ == '__main__': if __name__ == '__main__':

View File

@ -0,0 +1,465 @@
#!/usr/bin/env python
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
from __future__ import (unicode_literals, division, absolute_import,
print_function)
__license__ = 'GPL v3'
__copyright__ = '2011, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
from threading import Thread, Event
from operator import attrgetter
from PyQt4.Qt import (QStyledItemDelegate, QTextDocument, QRectF, QIcon, Qt,
QStyle, QApplication, QDialog, QVBoxLayout, QLabel, QDialogButtonBox,
QStackedWidget, QWidget, QTableView, QGridLayout, QFontInfo, QPalette,
QTimer, pyqtSignal, QAbstractTableModel, QVariant, QSize)
from PyQt4.QtWebKit import QWebView
from calibre.customize.ui import metadata_plugins
from calibre.ebooks.metadata import authors_to_string
from calibre.utils.logging import GUILog as Log
from calibre.ebooks.metadata.sources.identify import identify
from calibre.ebooks.metadata.book.base import Metadata
from calibre.gui2 import error_dialog, NONE
from calibre.utils.date import utcnow, fromordinal, format_date
from calibre.library.comments import comments_to_html
class RichTextDelegate(QStyledItemDelegate): # {{{
def __init__(self, parent=None):
QStyledItemDelegate.__init__(self, parent)
def to_doc(self, index):
doc = QTextDocument()
doc.setHtml(index.data().toString())
return doc
def sizeHint(self, option, index):
ans = self.to_doc(index).size().toSize()
ans.setHeight(ans.height()+10)
return ans
def paint(self, painter, option, index):
painter.save()
painter.setClipRect(QRectF(option.rect))
if hasattr(QStyle, 'CE_ItemViewItem'):
QApplication.style().drawControl(QStyle.CE_ItemViewItem, option, painter)
elif option.state & QStyle.State_Selected:
painter.fillRect(option.rect, option.palette.highlight())
painter.translate(option.rect.topLeft())
self.to_doc(index).drawContents(painter)
painter.restore()
# }}}
class ResultsModel(QAbstractTableModel): # {{{
COLUMNS = (
'#', _('Title'), _('Published'), _('Has cover'), _('Has summary')
)
HTML_COLS = (1, 2)
ICON_COLS = (3, 4)
def __init__(self, results, parent=None):
QAbstractTableModel.__init__(self, parent)
self.results = results
self.yes_icon = QVariant(QIcon(I('ok.png')))
def rowCount(self, parent=None):
return len(self.results)
def columnCount(self, parent=None):
return len(self.COLUMNS)
def headerData(self, section, orientation, role):
if orientation == Qt.Horizontal and role == Qt.DisplayRole:
try:
return QVariant(self.COLUMNS[section])
except:
return NONE
return NONE
def data_as_text(self, book, col):
if col == 0:
return unicode(book.gui_rank+1)
if col == 1:
t = book.title if book.title else _('Unknown')
a = authors_to_string(book.authors) if book.authors else ''
return '<b>%s</b><br><i>%s</i>' % (t, a)
if col == 2:
d = format_date(book.pubdate, 'yyyy') if book.pubdate else _('Unknown')
p = book.publisher if book.publisher else ''
return '<b>%s</b><br><i>%s</i>' % (d, p)
def data(self, index, role):
row, col = index.row(), index.column()
try:
book = self.results[row]
except:
return NONE
if role == Qt.DisplayRole and col not in self.ICON_COLS:
res = self.data_as_text(book, col)
if res:
return QVariant(res)
return NONE
elif role == Qt.DecorationRole and col in self.ICON_COLS:
if col == 3 and getattr(book, 'has_cached_cover_url', False):
return self.yes_icon
if col == 4 and book.comments:
return self.yes_icon
elif role == Qt.UserRole:
return book
return NONE
def sort(self, col, order=Qt.AscendingOrder):
key = lambda x: x
if col == 0:
key = attrgetter('gui_rank')
elif col == 1:
key = attrgetter('title')
elif col == 2:
key = attrgetter('authors')
elif col == 3:
key = attrgetter('has_cached_cover_url')
elif key == 4:
key = lambda x: bool(x.comments)
self.results.sort(key=key, reverse=order==Qt.AscendingOrder)
self.reset()
# }}}
class ResultsView(QTableView): # {{{
show_details_signal = pyqtSignal(object)
book_selected = pyqtSignal(object)
def __init__(self, parent=None):
QTableView.__init__(self, parent)
self.rt_delegate = RichTextDelegate(self)
self.setSelectionMode(self.SingleSelection)
self.setAlternatingRowColors(True)
self.setSelectionBehavior(self.SelectRows)
self.setIconSize(QSize(24, 24))
self.clicked.connect(self.show_details)
self.doubleClicked.connect(self.select_index)
self.setSortingEnabled(True)
def show_results(self, results):
self._model = ResultsModel(results, self)
self.setModel(self._model)
for i in self._model.HTML_COLS:
self.setItemDelegateForColumn(i, self.rt_delegate)
self.resizeRowsToContents()
self.resizeColumnsToContents()
self.setFocus(Qt.OtherFocusReason)
def currentChanged(self, current, previous):
ret = QTableView.currentChanged(self, current, previous)
self.show_details(current)
return ret
def show_details(self, index):
book = self.model().data(index, Qt.UserRole)
parts = [
'<center>',
'<h2>%s</h2>'%book.title,
'<div><i>%s</i></div>'%authors_to_string(book.authors),
]
if not book.is_null('rating'):
parts.append('<div>%s</div>'%('\u2605'*int(book.rating)))
parts.append('</center>')
if book.tags:
parts.append('<div>%s</div><div>\u00a0</div>'%', '.join(book.tags))
if book.comments:
parts.append(comments_to_html(book.comments))
self.show_details_signal.emit(''.join(parts))
def select_index(self, index):
if not index.isValid():
index = self.model().index(0, 0)
book = self.model().data(index, Qt.UserRole)
self.book_selected.emit(book)
def get_result(self):
self.select_index(self.currentIndex())
# }}}
class Comments(QWebView): # {{{
def __init__(self, parent=None):
QWebView.__init__(self, parent)
self.setAcceptDrops(False)
self.setMaximumWidth(300)
self.setMinimumWidth(300)
palette = self.palette()
palette.setBrush(QPalette.Base, Qt.transparent)
self.page().setPalette(palette)
self.setAttribute(Qt.WA_OpaquePaintEvent, False)
def turnoff_scrollbar(self, *args):
self.page().mainFrame().setScrollBarPolicy(Qt.Horizontal, Qt.ScrollBarAlwaysOff)
def show_data(self, html):
def color_to_string(col):
ans = '#000000'
if col.isValid():
col = col.toRgb()
if col.isValid():
ans = unicode(col.name())
return ans
f = QFontInfo(QApplication.font(self.parent())).pixelSize()
c = color_to_string(QApplication.palette().color(QPalette.Normal,
QPalette.WindowText))
templ = '''\
<html>
<head>
<style type="text/css">
body, td {background-color: transparent; font-size: %dpx; color: %s }
a { text-decoration: none; color: blue }
div.description { margin-top: 0; padding-top: 0; text-indent: 0 }
table { margin-bottom: 0; padding-bottom: 0; }
</style>
</head>
<body>
<div class="description">
%%s
</div>
</body>
<html>
'''%(f, c)
self.setHtml(templ%html)
# }}}
class IdentifyWorker(Thread): # {{{
def __init__(self, log, abort, title, authors, identifiers):
Thread.__init__(self)
self.daemon = True
self.log, self.abort = log, abort
self.title, self.authors, self.identifiers = (title, authors,
identifiers)
self.results = []
self.error = None
def sample_results(self):
m1 = Metadata('The Great Gatsby', ['Francis Scott Fitzgerald'])
m2 = Metadata('The Great Gatsby', ['F. Scott Fitzgerald'])
m1.has_cached_cover_url = True
m2.has_cached_cover_url = False
m1.comments = 'Some comments '*10
m1.tags = ['tag%d'%i for i in range(20)]
m1.rating = 4.4
m1.language = 'en'
m2.language = 'fr'
m1.pubdate = utcnow()
m2.pubdate = fromordinal(1000000)
m1.publisher = 'Publisher 1'
m2.publisher = 'Publisher 2'
return [m1, m2]
def run(self):
try:
if True:
self.results = self.sample_results()
else:
self.results = identify(self.log, self.abort, title=self.title,
authors=self.authors, identifiers=self.identifiers)
for i, result in enumerate(self.results):
result.gui_rank = i
except:
import traceback
self.error = traceback.format_exc()
# }}}
class IdentifyWidget(QWidget): # {{{
rejected = pyqtSignal()
results_found = pyqtSignal()
book_selected = pyqtSignal(object)
def __init__(self, log, parent=None):
QWidget.__init__(self, parent)
self.log = log
self.abort = Event()
self.l = l = QGridLayout()
self.setLayout(l)
names = ['<b>'+p.name+'</b>' for p in metadata_plugins(['identify']) if
p.is_configured()]
self.top = QLabel('<p>'+_('calibre is downloading metadata from: ') +
', '.join(names))
self.top.setWordWrap(True)
l.addWidget(self.top, 0, 0)
self.results_view = ResultsView(self)
self.results_view.book_selected.connect(self.book_selected.emit)
self.get_result = self.results_view.get_result
l.addWidget(self.results_view, 1, 0)
self.comments_view = Comments(self)
l.addWidget(self.comments_view, 1, 1)
self.results_view.show_details_signal.connect(self.comments_view.show_data)
self.query = QLabel('download starting...')
f = self.query.font()
f.setPointSize(f.pointSize()-2)
self.query.setFont(f)
self.query.setWordWrap(True)
l.addWidget(self.query, 2, 0, 1, 2)
self.comments_view.show_data('<h2>'+_('Downloading')+
'<br><span id="dots">.</span></h2>'+
'''
<script type="text/javascript">
window.onload=function(){
var dotspan = document.getElementById('dots');
window.setInterval(function(){
if(dotspan.textContent == '............'){
dotspan.textContent = '.';
}
else{
dotspan.textContent += '.';
}
}, 400);
}
</script>
''')
def start(self, title=None, authors=None, identifiers={}):
self.log.clear()
self.log('Starting download')
parts = []
if title:
parts.append('title:'+title)
if authors:
parts.append('authors:'+authors_to_string(authors))
if identifiers:
x = ', '.join('%s:%s'%(k, v) for k, v in identifiers)
parts.append(x)
self.query.setText(_('Query: ')+'; '.join(parts))
self.log(unicode(self.query.text()))
self.worker = IdentifyWorker(self.log, self.abort, title,
authors, identifiers)
self.worker.start()
QTimer.singleShot(50, self.update)
def update(self):
if self.worker.is_alive():
QTimer.singleShot(50, self.update)
else:
self.process_results()
def process_results(self):
if self.worker.error is not None:
error_dialog(self, _('Download failed'),
_('Failed to download metadata. Click '
'Show Details to see details'),
show=True, det_msg=self.worker.error)
self.rejected.emit()
return
if not self.worker.results:
log = ''.join(self.log.plain_text)
error_dialog(self, _('No matches found'), '<p>' +
_('Failed to find any books that '
'match your search. Try making the search <b>less '
'specific</b>. For example, use only the author\'s '
'last name and a single distinctive word from '
'the title.<p>To see the full log, click Show Details.'),
show=True, det_msg=log)
self.rejected.emit()
return
self.results_view.show_results(self.worker.results)
self.comments_view.show_data('''
<div style="margin-bottom:2ex">Found <b>%d</b> results</div>
<div>To see <b>details</b>, click on any result</div>''' %
len(self.worker.results))
self.results_found.emit()
def cancel(self):
self.abort.set()
# }}}
class FullFetch(QDialog): # {{{
def __init__(self, log, parent=None):
QDialog.__init__(self, parent)
self.log = log
self.setWindowTitle(_('Downloading metadata...'))
self.setWindowIcon(QIcon(I('metadata.png')))
self.stack = QStackedWidget()
self.l = l = QVBoxLayout()
self.setLayout(l)
l.addWidget(self.stack)
self.bb = QDialogButtonBox(QDialogButtonBox.Cancel|QDialogButtonBox.Ok)
l.addWidget(self.bb)
self.bb.rejected.connect(self.reject)
self.next_button = self.bb.addButton(_('Next'), self.bb.AcceptRole)
self.next_button.setDefault(True)
self.next_button.setEnabled(False)
self.next_button.clicked.connect(self.next_clicked)
self.ok_button = self.bb.button(self.bb.Ok)
self.ok_button.setVisible(False)
self.ok_button.clicked.connect(self.ok_clicked)
self.identify_widget = IdentifyWidget(log, self)
self.identify_widget.rejected.connect(self.reject)
self.identify_widget.results_found.connect(self.identify_results_found)
self.identify_widget.book_selected.connect(self.book_selected)
self.stack.addWidget(self.identify_widget)
self.resize(850, 500)
def book_selected(self, book):
print (book)
self.next_button.setVisible(False)
self.ok_button.setVisible(True)
def accept(self):
# Prevent the usual dialog accept mechanisms from working
pass
def reject(self):
self.identify_widget.cancel()
return QDialog.reject(self)
def identify_results_found(self):
self.next_button.setEnabled(True)
def next_clicked(self, *args):
self.identify_widget.get_result()
def ok_clicked(self, *args):
pass
def start(self, title=None, authors=None, identifiers={}):
self.identify_widget.start(title=title, authors=authors,
identifiers=identifiers)
self.exec_()
# }}}
if __name__ == '__main__':
app = QApplication([])
d = FullFetch(Log())
d.start(title='great gatsby', authors=['Fitzgerald'])

View File

@ -13,6 +13,7 @@ from calibre.gui2.preferences import ConfigWidgetBase, test_widget
from calibre.gui2.preferences.columns_ui import Ui_Form from calibre.gui2.preferences.columns_ui import Ui_Form
from calibre.gui2.preferences.create_custom_column import CreateCustomColumn from calibre.gui2.preferences.create_custom_column import CreateCustomColumn
from calibre.gui2 import error_dialog, question_dialog, ALL_COLUMNS from calibre.gui2 import error_dialog, question_dialog, ALL_COLUMNS
from calibre.utils.config import test_eight_code
class ConfigWidget(ConfigWidgetBase, Ui_Form): class ConfigWidget(ConfigWidgetBase, Ui_Form):
@ -33,6 +34,14 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
signal = getattr(self.opt_columns, 'item'+signal) signal = getattr(self.opt_columns, 'item'+signal)
signal.connect(self.columns_changed) signal.connect(self.columns_changed)
if test_eight_code:
r = self.register
choices = [(_('Default'), 'default'), (_('Compact Metadata'), 'alt1')]
r('edit_metadata_single_layout', db.prefs, choices=choices)
r('bools_are_tristate', db.prefs, restart_required=True)
else:
self.items_in_v_eight.setVisible(False)
def initialize(self): def initialize(self):
ConfigWidgetBase.initialize(self) ConfigWidgetBase.initialize(self)
self.init_columns() self.init_columns()
@ -169,6 +178,10 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
must_restart = True must_restart = True
return must_restart return must_restart
def refresh_gui(self, gui):
gui.library_view.reset()
if __name__ == '__main__': if __name__ == '__main__':
from PyQt4.Qt import QApplication from PyQt4.Qt import QApplication

View File

@ -197,6 +197,67 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="1" column="2">
<layout class="QVBoxLayout">
<item>
<widget class="QGroupBox" name="items_in_v_eight">
<property name="title">
<string>Related Options</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<widget class="QLabel">
<property name="text">
<string>Edit metadata layout:</string>
</property>
<property name="buddy">
<cstring>opt_edit_metadata_single_layout</cstring>
</property>
</widget>
</item>
<item row="0" column="1">
<widget class="QComboBox" name="opt_edit_metadata_single_layout">
<property name="toolTip">
<string>Choose a different layout for the Edit Metadata dialog. Alternate layouts make it easier to edit custom columns.</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel">
<property name="text">
<string>Boolean columns are tristate:</string>
</property>
<property name="buddy">
<cstring>opt_bools_are_tristate</cstring>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QCheckBox" name="opt_bools_are_tristate">
<property name="toolTip">
<string>If checked, boolean columns values can be Yes, No, and Unknown.
If not checked, the values can be Yes and No.</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item>
<spacer name="verticalSpacer_5">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
</layout>
</item>
</layout> </layout>
</widget> </widget>
<resources> <resources>

View File

@ -49,8 +49,8 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
r('use_roman_numerals_for_series_number', config) r('use_roman_numerals_for_series_number', config)
r('separate_cover_flow', config, restart_required=True) r('separate_cover_flow', config, restart_required=True)
choices = [(_('Small'), 'small'), (_('Medium'), 'medium'), choices = [(_('Off'), 'off'), (_('Small'), 'small'),
(_('Large'), 'large')] (_('Medium'), 'medium'), (_('Large'), 'large')]
r('toolbar_icon_size', gprefs, choices=choices) r('toolbar_icon_size', gprefs, choices=choices)
choices = [(_('Automatic'), 'auto'), (_('Always'), 'always'), choices = [(_('Automatic'), 'auto'), (_('Always'), 'always'),

View File

@ -18,6 +18,7 @@ from calibre.utils.config import ConfigProxy
from calibre.gui2 import error_dialog, config, open_url, warning_dialog, \ from calibre.gui2 import error_dialog, config, open_url, warning_dialog, \
Dispatcher, info_dialog Dispatcher, info_dialog
from calibre import as_unicode from calibre import as_unicode
from calibre.utils.icu import sort_key
class ConfigWidget(ConfigWidgetBase, Ui_Form): class ConfigWidget(ConfigWidgetBase, Ui_Form):
@ -42,8 +43,11 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
else self.opt_password.Password)) else self.opt_password.Password))
self.opt_password.setEchoMode(self.opt_password.Password) self.opt_password.setEchoMode(self.opt_password.Password)
restrictions = sorted(saved_searches().names(), restrictions = sorted(saved_searches().names(), key=sort_key)
cmp=lambda x,y: cmp(x.lower(), y.lower())) # verify that the current restriction still exists. If not, clear it.
csr = db.prefs.get('cs_restriction', None)
if csr and csr not in restrictions:
db.prefs.set('cs_restriction', '')
choices = [('', '')] + [(x, x) for x in restrictions] choices = [('', '')] + [(x, x) for x in restrictions]
r('cs_restriction', db.prefs, choices=choices) r('cs_restriction', db.prefs, choices=choices)
@ -57,17 +61,8 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
r('autolaunch_server', config) r('autolaunch_server', config)
def set_server_options(self):
c = self.proxy
c.set('port', self.opt_port.value())
c.set('username', unicode(self.opt_username.text()).strip())
p = unicode(self.opt_password.text()).strip()
if not p:
p = None
c.set('password', p)
def start_server(self): def start_server(self):
self.set_server_options() ConfigWidgetBase.commit(self)
self.gui.start_content_server(check_started=False) self.gui.start_content_server(check_started=False)
while not self.gui.content_server.is_running and self.gui.content_server.exception is None: while not self.gui.content_server.is_running and self.gui.content_server.exception is None:
time.sleep(1) time.sleep(1)

View File

@ -985,6 +985,7 @@ class TagsModel(QAbstractItemModel): # {{{
def do_drop_from_library(self, md, action, row, column, parent): def do_drop_from_library(self, md, action, row, column, parent):
idx = parent idx = parent
if idx.isValid(): if idx.isValid():
self.tags_view.setCurrentIndex(idx)
node = self.data(idx, Qt.UserRole) node = self.data(idx, Qt.UserRole)
if node.type == TagTreeItem.TAG: if node.type == TagTreeItem.TAG:
fm = self.db.metadata_for_field(node.tag.category) fm = self.db.metadata_for_field(node.tag.category)

View File

@ -312,6 +312,7 @@ class ImageView(QWidget, ImageDropMixin):
p.setPen(pen) p.setPen(pen)
if self.draw_border: if self.draw_border:
p.drawRect(target) p.drawRect(target)
#p.drawRect(self.rect())
p.end() p.end()
class CoverView(QGraphicsView, ImageDropMixin): class CoverView(QGraphicsView, ImageDropMixin):

View File

@ -547,7 +547,7 @@ class ResultCache(SearchQueryParser): # {{{
return matchkind, query return matchkind, query
def get_bool_matches(self, location, query, candidates): def get_bool_matches(self, location, query, candidates):
bools_are_tristate = tweaks['bool_custom_columns_are_tristate'] != 'no' bools_are_tristate = not self.db_prefs.get('bools_are_tristate')
loc = self.field_metadata[location]['rec_index'] loc = self.field_metadata[location]['rec_index']
matches = set() matches = set()
query = icu_lower(query) query = icu_lower(query)
@ -947,7 +947,7 @@ class ResultCache(SearchQueryParser): # {{{
if not fields: if not fields:
fields = [('timestamp', False)] fields = [('timestamp', False)]
keyg = SortKeyGenerator(fields, self.field_metadata, self._data) keyg = SortKeyGenerator(fields, self.field_metadata, self._data, self.db_prefs)
self._map.sort(key=keyg) self._map.sort(key=keyg)
tmap = list(itertools.repeat(False, len(self._data))) tmap = list(itertools.repeat(False, len(self._data)))
@ -970,9 +970,10 @@ class SortKey(object):
class SortKeyGenerator(object): class SortKeyGenerator(object):
def __init__(self, fields, field_metadata, data): def __init__(self, fields, field_metadata, data, db_prefs):
from calibre.utils.icu import sort_key from calibre.utils.icu import sort_key
self.field_metadata = field_metadata self.field_metadata = field_metadata
self.db_prefs = db_prefs
self.orders = [1 if x[1] else -1 for x in fields] self.orders = [1 if x[1] else -1 for x in fields]
self.entries = [(x[0], field_metadata[x[0]]) for x in fields] self.entries = [(x[0], field_metadata[x[0]]) for x in fields]
self.library_order = tweaks['title_series_sorting'] == 'library_order' self.library_order = tweaks['title_series_sorting'] == 'library_order'
@ -1032,7 +1033,7 @@ class SortKeyGenerator(object):
val = self.string_sort_key(val) val = self.string_sort_key(val)
elif dt == 'bool': elif dt == 'bool':
if tweaks['bool_custom_columns_are_tristate'] == 'no': if not self.db_prefs.get('bools_are_tristate'):
val = {True: 1, False: 2, None: 2}.get(val, 2) val = {True: 1, False: 2, None: 2}.get(val, 2)
else: else:
val = {True: 1, False: 2, None: 3}.get(val, 3) val = {True: 1, False: 2, None: 3}.get(val, 3)

View File

@ -40,7 +40,7 @@ from calibre.ebooks import BOOK_EXTENSIONS, check_ebook_format
from calibre.utils.magick.draw import save_cover_data_to from calibre.utils.magick.draw import save_cover_data_to
from calibre.utils.recycle_bin import delete_file, delete_tree from calibre.utils.recycle_bin import delete_file, delete_tree
from calibre.utils.formatter_functions import load_user_template_functions from calibre.utils.formatter_functions import load_user_template_functions
from calibre.utils.config import test_eight_code
copyfile = os.link if hasattr(os, 'link') else shutil.copyfile copyfile = os.link if hasattr(os, 'link') else shutil.copyfile
@ -213,6 +213,12 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
defs = self.prefs.defaults defs = self.prefs.defaults
defs['gui_restriction'] = defs['cs_restriction'] = '' defs['gui_restriction'] = defs['cs_restriction'] = ''
defs['categories_using_hierarchy'] = [] defs['categories_using_hierarchy'] = []
defs['edit_metadata_single_layout'] = 'default'
defs['bools_are_tristate'] = \
tweaks.get('bool_custom_columns_are_tristate', 'yes') == 'yes'
if self.prefs.get('bools_are_tristate') is None or not test_eight_code:
self.prefs.set('bools_are_tristate', defs['bools_are_tristate'])
# Migrate saved search and user categories to db preference scheme # Migrate saved search and user categories to db preference scheme
def migrate_preference(key, default): def migrate_preference(key, default):

View File

@ -24,6 +24,8 @@ from calibre.library.server.xml import XMLServer
from calibre.library.server.opds import OPDSServer from calibre.library.server.opds import OPDSServer
from calibre.library.server.cache import Cache from calibre.library.server.cache import Cache
from calibre.library.server.browse import BrowseServer from calibre.library.server.browse import BrowseServer
from calibre.utils.search_query_parser import saved_searches
from calibre import prints
class DispatchController(object): # {{{ class DispatchController(object): # {{{
@ -178,7 +180,12 @@ class LibraryServer(ContentServer, MobileServer, XMLServer, OPDSServer, Cache,
def set_search_restriction(self, restriction): def set_search_restriction(self, restriction):
self.search_restriction_name = restriction self.search_restriction_name = restriction
if restriction: if restriction:
self.search_restriction = 'search:"%s"'%restriction if restriction not in saved_searches().names():
prints('WARNING: Content server: search restriction ',
restriction, ' does not exist')
self.search_restriction = ''
else:
self.search_restriction = 'search:"%s"'%restriction
else: else:
self.search_restriction = '' self.search_restriction = ''
self.reset_caches() self.reset_caches()

View File

@ -17,8 +17,8 @@ from calibre.utils.magick.draw import save_cover_data_to, Image, \
class CSSortKeyGenerator(SortKeyGenerator): class CSSortKeyGenerator(SortKeyGenerator):
def __init__(self, fields, fm): def __init__(self, fields, fm, db_prefs):
SortKeyGenerator.__init__(self, fields, fm, None) SortKeyGenerator.__init__(self, fields, fm, None, db_prefs)
def __call__(self, record): def __call__(self, record):
return self.itervals(record).next() return self.itervals(record).next()
@ -56,7 +56,8 @@ class ContentServer(object):
field = self.db.data.sanitize_sort_field_name(field) field = self.db.data.sanitize_sort_field_name(field)
if field not in self.db.field_metadata.sortable_field_keys(): if field not in self.db.field_metadata.sortable_field_keys():
raise cherrypy.HTTPError(400, '%s is not a valid sort field'%field) raise cherrypy.HTTPError(400, '%s is not a valid sort field'%field)
keyg = CSSortKeyGenerator([(field, order)], self.db.field_metadata) keyg = CSSortKeyGenerator([(field, order)], self.db.field_metadata,
self.db.prefs)
items.sort(key=keyg, reverse=not order) items.sort(key=keyg, reverse=not order)
# }}} # }}}

View File

@ -30,7 +30,7 @@ entry_points = {
'calibre-customize = calibre.customize.ui:main', 'calibre-customize = calibre.customize.ui:main',
'calibre-complete = calibre.utils.complete:main', 'calibre-complete = calibre.utils.complete:main',
'pdfmanipulate = calibre.ebooks.pdf.manipulate.cli:main', 'pdfmanipulate = calibre.ebooks.pdf.manipulate.cli:main',
'fetch-ebook-metadata = calibre.ebooks.metadata.fetch:main', 'fetch-ebook-metadata = calibre.ebooks.metadata.sources.cli:main',
'epub-fix = calibre.ebooks.epub.fix.main:main', 'epub-fix = calibre.ebooks.epub.fix.main:main',
'calibre-smtp = calibre.utils.smtp:main', 'calibre-smtp = calibre.utils.smtp:main',
], ],
@ -136,17 +136,17 @@ class PostInstall:
self.icon_resources = [] self.icon_resources = []
self.menu_resources = [] self.menu_resources = []
self.mime_resources = [] self.mime_resources = []
if islinux: if islinux or isfreebsd:
self.setup_completion() self.setup_completion()
self.install_man_pages() self.install_man_pages()
if islinux: if islinux or isfreebsd:
self.setup_desktop_integration() self.setup_desktop_integration()
self.create_uninstaller() self.create_uninstaller()
from calibre.utils.config import config_dir from calibre.utils.config import config_dir
if os.path.exists(config_dir): if os.path.exists(config_dir):
os.chdir(config_dir) os.chdir(config_dir)
if islinux: if islinux or isfreebsd:
for f in os.listdir('.'): for f in os.listdir('.'):
if os.stat(f).st_uid == 0: if os.stat(f).st_uid == 0:
os.rmdir(f) if os.path.isdir(f) else os.unlink(f) os.rmdir(f) if os.path.isdir(f) else os.unlink(f)
@ -183,7 +183,7 @@ class PostInstall:
from calibre.ebooks.lrf.lrfparser import option_parser as lrf2lrsop from calibre.ebooks.lrf.lrfparser import option_parser as lrf2lrsop
from calibre.gui2.lrf_renderer.main import option_parser as lrfviewerop from calibre.gui2.lrf_renderer.main import option_parser as lrfviewerop
from calibre.gui2.viewer.main import option_parser as viewer_op from calibre.gui2.viewer.main import option_parser as viewer_op
from calibre.ebooks.metadata.fetch import option_parser as fem_op from calibre.ebooks.metadata.sources.cli import option_parser as fem_op
from calibre.gui2.main import option_parser as guiop from calibre.gui2.main import option_parser as guiop
from calibre.utils.smtp import option_parser as smtp_op from calibre.utils.smtp import option_parser as smtp_op
from calibre.library.server.main import option_parser as serv_op from calibre.library.server.main import option_parser as serv_op

View File

@ -126,7 +126,7 @@ html_use_modindex = False
html_use_index = False html_use_index = False
# If true, the reST sources are included in the HTML build as _sources/<name>. # If true, the reST sources are included in the HTML build as _sources/<name>.
html_copy_source = False html_copy_source = True
# Output file base name for HTML help builder. # Output file base name for HTML help builder.
htmlhelp_basename = 'calibredoc' htmlhelp_basename = 'calibredoc'

View File

@ -197,7 +197,7 @@ Once you've located the zip file of your plugin you can then directly update it
zip -R /path/to/plugin/zip/file.zip * zip -R /path/to/plugin/zip/file.zip *
This will automatically update all changed files. It relies on the freely available zip command line tool. This will update all changed files. It relies on the freely available zip command line tool. Note that you should quit calibre before running this command.
More plugin examples More plugin examples
---------------------- ----------------------

View File

@ -99,7 +99,8 @@ We just need some information from you:
device. device.
Once you send us the output for a particular operating system, support for the device in that operating system Once you send us the output for a particular operating system, support for the device in that operating system
will appear in the next release of |app|. will appear in the next release of |app|. To send us the output, open a bug report and attach the output to it.
See `calibre bugs <http://calibre-ebook.com/bugs>`_.
My device is not being detected by |app|? My device is not being detected by |app|?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

View File

@ -71,7 +71,7 @@ Edit metadata
|emii| The :guilabel:`Edit metadata` action has six variations, which can be accessed by clicking the down arrow on the right side of the button. |emii| The :guilabel:`Edit metadata` action has six variations, which can be accessed by clicking the down arrow on the right side of the button.
1. **Edit metadata individually**: This allows you to edit the metadata of books one-by-one, with the option of fetching metadata, including covers from the internet. It also allows you to add/remove particular ebook formats from a book. For more detail see :ref:`metadata`. 1. **Edit metadata individually**: This allows you to edit the metadata of books one-by-one, with the option of fetching metadata, including covers from the internet. It also allows you to add/remove particular ebook formats from a book.
2. **Edit metadata in bulk**: This allows you to edit common metadata fields for large numbers of books simulataneously. It operates on all the books you have selected in the :ref:`Library view <search_sort>`. 2. **Edit metadata in bulk**: This allows you to edit common metadata fields for large numbers of books simulataneously. It operates on all the books you have selected in the :ref:`Library view <search_sort>`.
3. **Download metadata and covers**: Downloads metadata and covers (if available), for the books that are selected in the book list. 3. **Download metadata and covers**: Downloads metadata and covers (if available), for the books that are selected in the book list.
4. **Download only metadata**: Downloads only metadata (if available), for the books that are selected in the book list. 4. **Download only metadata**: Downloads only metadata (if available), for the books that are selected in the book list.
@ -79,6 +79,7 @@ Edit metadata
6. **Download only social metadata**: Downloads only social metadata such as tags and reviews (if available), for the books that are selected in the book list. 6. **Download only social metadata**: Downloads only social metadata such as tags and reviews (if available), for the books that are selected in the book list.
7. **Merge Book Records**: Gives you the capability of merging the metadata and formats of two or more book records together. You can choose to either delete or keep the records that were not clicked first. 7. **Merge Book Records**: Gives you the capability of merging the metadata and formats of two or more book records together. You can choose to either delete or keep the records that were not clicked first.
For more details see :ref:`metadata`.
.. _convert_ebooks: .. _convert_ebooks:

View File

@ -40,3 +40,84 @@ Sections
glossary glossary
The main |app| user interface
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. toctree::
:maxdepth: 2
gui
Adding your favorite news website to |app|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. toctree::
:maxdepth: 2
news
The |app| e-book viewer
^^^^^^^^^^^^^^^^^^^^^^^^^^
.. toctree::
:maxdepth: 2
viewer
Customizing |app|'s e-book conversion
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. toctree::
:maxdepth: 2
conversion
Editing e-book metadata
^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. toctree::
:maxdepth: 2
metadata
Frequently Asked Questions
^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. toctree::
:maxdepth: 2
faq
Tutorials
^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. toctree::
:maxdepth: 2
tutorials
Customizing |app|
^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. toctree::
:maxdepth: 2
customize
The Command Line Interface
^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. toctree::
:maxdepth: 2
cli/cli-index
Setting up a |app| development environment
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. toctree::
:maxdepth: 2
develop

View File

@ -263,20 +263,18 @@ Tips for developing new recipes
The best way to develop new recipes is to use the command line interface. Create the recipe using your favorite python editor and save it to a file say :file:`myrecipe.recipe`. The `.recipe` extension is required. You can download content using this recipe with the command:: The best way to develop new recipes is to use the command line interface. Create the recipe using your favorite python editor and save it to a file say :file:`myrecipe.recipe`. The `.recipe` extension is required. You can download content using this recipe with the command::
ebook-convert myrecipe.recipe output_dir --test -vv ebook-convert myrecipe.recipe .epub --test -vv --debug-pipeline debug
The :command:`ebook-convert` will download all the webpages and save them to the directory :file:`output_dir`, creating it if necessary. The :option:`-vv` makes ebook-convert spit out a lot of information about what it is doing. The :option:`--test` makes it download only a couple of articles from at most two feeds. The command :command:`ebook-convert` will download all the webpages and save them to the EPUB file :file:`myrecipe.epub`. The :option:`-vv` makes ebook-convert spit out a lot of information about what it is doing. The :option:`--test` makes it download only a couple of articles from at most two feeds. In addition, ebook-convert will put the downloaded HTML into the ``debug/input`` directory, where ``debug`` is the directory you specified in the :option:`--debug-pipeline` option.
Once the download is complete, you can look at the downloaded :term:`HTML` by opening the file :file:`index.html` in a browser. Once you're satisfied that the download and preprocessing is happening correctly, you can generate ebooks in different formats as shown below:: Once the download is complete, you can look at the downloaded :term:`HTML` by opening the file :file:`debug/input/index.html` in a browser. Once you're satisfied that the download and preprocessing is happening correctly, you can generate ebooks in different formats as shown below::
ebook-convert myrecipe.recipe myrecipe.epub ebook-convert myrecipe.recipe myrecipe.epub
ebook-convert myrecipe.recipe myrecipe.mobi ebook-convert myrecipe.recipe myrecipe.mobi
... ...
If you're satisfied with your recipe, and you feel there is enough demand to justify its inclusion into the set of built-in recipes, add a comment to the ticket http://bugs.calibre-ebook.com/ticket/405 If you're satisfied with your recipe, and you feel there is enough demand to justify its inclusion into the set of built-in recipes, post your recipe in the `calibre recipes forum <http://www.mobileread.com/forums/forumdisplay.php?f=228>`_ to share it with other calibre users.
Alternatively, you could just post your recipe in the calibre forum at http://www.mobileread.com/forums/forumdisplay.php?f=166 to share it with other calibre users.
.. seealso:: .. seealso::

View File

@ -16,7 +16,7 @@ Here, we will show you how to integrate the |app| content server into another se
Using a reverse proxy Using a reverse proxy
----------------------- -----------------------
This is the simplest approach as it allows you to use the binary calibre install with no external dependencies/system integration requirements. A reverse proxy is when your normal server accepts incoming requests and passes them onto the calibre server. It then reads the response from the calibre server and forwards it to the client. This means that you can simply run the calibre server as normal without trying to integrate it closely with your main server, and you can take advantage of whatever authentication systems you main server has in place. This is the simplest approach as it allows you to use the binary calibre install with no external dependencies/system integration requirements. Below, is an example of how to achieve this with Apache as your main server, but it will work with any server that supports Reverse Proxies.
First start the |app| content server as shown below:: First start the |app| content server as shown below::
@ -33,7 +33,7 @@ The exact technique for enabling the proxy modules will vary depending on your A
RewriteRule ^/calibre/(.*) http://localhost:8080/calibre/$1 [proxy] RewriteRule ^/calibre/(.*) http://localhost:8080/calibre/$1 [proxy]
RewriteRule ^/calibre http://localhost:8080 [proxy] RewriteRule ^/calibre http://localhost:8080 [proxy]
That's all, you will now be able to access the |app| Content Server under the /calibre URL in your apache server. That's all, you will now be able to access the |app| Content Server under the /calibre URL in your apache server. The above rules pass all requests under /calibre to the calibre server running on port 8080 and thanks to the --url-prefix option above, the calibre server handles them transparently.
.. note:: If you are willing to devote an entire VirtualHost to the content server, then there is no need to use --url-prefix and RewriteRule, instead just use the ProxyPass directive. .. note:: If you are willing to devote an entire VirtualHost to the content server, then there is no need to use --url-prefix and RewriteRule, instead just use the ProxyPass directive.

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