Sync to trunk.

This commit is contained in:
John Schember 2011-07-18 18:30:41 -04:00
commit 6f936d34d1
147 changed files with 80890 additions and 42899 deletions

View File

@ -19,6 +19,58 @@
# new recipes:
# - title:
- version: 0.8.10
date: 2011-07-15
new features:
- title: "Add a right click menu to the cover browser. It allows you to view a book, edit metadata etc. from within the cover browser. The menu can be customized in Preferences->Toolbars"
- title: "Allow selecting and stopping multiple jobs at once in the jobs window"
tickets: [810349]
- title: "When editing metadata directly in the book list, have a little pop up menu so that all existing values can be accessed by mouse only. For example, when you edit authors, you can use the mouse to select an existing author."
- title: "Get Books: Add ebook.nl and fix price parsing for the legimi store"
- title: "Drivers for Samsung Infuse and Motorola XPERT"
- title: "Tag Browser: Make hierarchical items work in group searched terms."
bug fixes:
- title: "Allow setting numbers larger than 99 in custom series columns"
- title: "Fix a bug that caused the same news download sent via a USB connection to the device on two different days resulting in a duplicate on the device"
- title: "Ensure English in the list of interface languages in Preferences is always listed in English, so that it does not become hard to find"
- title: "SNB Output: Fix bug in handling unicode file names"
- title: "Fix sorting problem in manage categories. Fix poor performance problem when dropping multiple books onto a user category."
- title: "Remove 'empty field' error dialogs in bulk search/replace, instead setting the fields to their default value."
- title: "Fix regression that broke communicating with Kobo devices using outdated firmware"
tickets: [807832]
- title: "LRF Input: Fix conversion of LRF files with non ascii titles on some windows systems"
tickets: [807641]
improved recipes:
- Time
- Freakonomics Blog
- io9
- "Computer Act!ve"
new recipes:
- title: Techcrunch and Pecat
author: Darko Miletic
- title: Vio Mundo, IDG Now and Tojolaco
author: Diniz Bortoletto
- title: Geek and Poke, Automatiseringgids IT
author: DrMerry
- version: 0.8.9
date: 2011-07-08
@ -32,7 +84,7 @@
- title: "Conversion pipeline: Add option to control if duplicate entries are allowed when generating the Table of Contents from links."
tickets: [806095]
- title: "Metadata download: When merging results, if the query to the xisbn service hangs, wait no more than 10 seconds. Also try harder to preserve the month when downlaoding published date. Do not throw away isbnless results if there are some sources that return isbns and some that do not."
- title: "Metadata download: When merging results, if the query to the xisbn service hangs, wait no more than 10 seconds. Also try harder to preserve the month when downloading published date. Do not throw away isbnless results if there are some sources that return isbns and some that do not."
tickets: [798309]
- title: "Get Books: Remove OpenLibrary since it has the same files as archive.org. Allow direct downloading from Project Gutenberg."

View File

@ -1,25 +1,29 @@
#!/usr/bin/env python
__license__ = 'GPL v3'
__copyright__ = '2009, Kovid Goyal kovid@kovidgoyal.net'
__copyright__ = '2011, Starson17'
__docformat__ = 'restructuredtext en'
from calibre.web.feeds.news import BasicNewsRecipe
class Freakonomics(BasicNewsRecipe):
title = 'Freakonomics Blog'
description = 'The Hidden side of everything'
__author__ = 'Starson17'
__author__ = 'Starson17'
__version__ = '1.02'
__date__ = '11 July 2011'
language = 'en'
cover_url = 'http://ilkerugur.files.wordpress.com/2009/04/freakonomics.jpg'
use_embedded_content= False
no_stylesheets = True
oldest_article = 30
remove_javascript = True
remove_empty_feeds = True
max_articles_per_feed = 50
feeds = [('Blog', 'http://feeds.feedburner.com/freakonomicsblog')]
keep_only_tags = [dict(name='div', attrs={'id':'header'}),
dict(name='h1'),
dict(name='h2'),
dict(name='div', attrs={'class':'entry-content'}),
]
feeds = [(u'Freakonomics Blog', u'http://www.freakonomics.com/feed/')]
keep_only_tags = [dict(name='div', attrs={'id':['content']})]
remove_tags_after = [dict(name='div', attrs={'class':['simple_socialmedia']})]
remove_tags = [dict(name='div', attrs={'class':['simple_socialmedia','single-fb-share','wp-polls']})]
extra_css = '''
h1{font-family:Arial,Helvetica,sans-serif; font-weight:bold;font-size:large;}
h2{font-family:Arial,Helvetica,sans-serif; font-weight:normal;font-size:small;}

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 B

View File

@ -0,0 +1,88 @@
from calibre.web.feeds.recipes import BasicNewsRecipe
import re
#import pprint, sys
#pp = pprint.PrettyPrinter(indent=4)
class NikkeiNet_paper_subscription(BasicNewsRecipe):
title = u'\u65E5\u672C\u7D4C\u6E08\u65B0\u805E\uFF08\u671D\u520A\u30FB\u5915\u520A\uFF09'
__author__ = 'Ado Nishimura'
description = u'\u65E5\u7D4C\u96FB\u5B50\u7248\u306B\u3088\u308B\u65E5\u672C\u7D4C\u6E08\u65B0\u805E\u3002\u671D\u520A\u30FB\u5915\u520A\u306F\u53D6\u5F97\u6642\u9593\u306B\u3088\u308A\u5207\u308A\u66FF\u308F\u308A\u307E\u3059\u3002\u8981\u8CFC\u8AAD'
needs_subscription = True
oldest_article = 1
max_articles_per_feed = 30
language = 'ja'
no_stylesheets = True
cover_url = 'http://parts.nikkei.com/parts/ds/images/common/logo_r1.svg'
masthead_url = 'http://parts.nikkei.com/parts/ds/images/common/logo_r1.svg'
remove_tags_before = {'class':"cmn-indent"}
remove_tags = [
# {'class':"cmn-article_move"},
# {'class':"cmn-pr_list"},
# {'class':"cmnc-zoom"},
{'class':"cmn-hide"},
{'name':'form'},
]
remove_tags_after = {'class':"cmn-indent"}
def get_browser(self):
br = BasicNewsRecipe.get_browser()
#pp.pprint(self.parse_index())
#exit(1)
#br.set_debug_http(True)
#br.set_debug_redirects(True)
#br.set_debug_responses(True)
if self.username is not None and self.password is not None:
print "----------------------------open top page----------------------------------------"
br.open('http://www.nikkei.com/')
print "----------------------------open first login form--------------------------------"
link = br.links(url_regex="www.nikkei.com/etc/accounts/login").next()
br.follow_link(link)
#response = br.response()
#print response.get_data()
print "----------------------------JS redirect(send autoPostForm)-----------------------"
br.select_form(name='autoPostForm')
br.submit()
#response = br.response()
print "----------------------------got login form---------------------------------------"
br.select_form(name='LA0210Form01')
br['LA0210Form01:LA0210Email'] = self.username
br['LA0210Form01:LA0210Password'] = self.password
br.submit()
#response = br.response()
print "----------------------------JS redirect------------------------------------------"
br.select_form(nr=0)
br.submit()
#br.set_debug_http(False)
#br.set_debug_redirects(False)
#br.set_debug_responses(False)
return br
def cleanup(self):
print "----------------------------logout-----------------------------------------------"
self.browser.open('https://regist.nikkei.com/ds/etc/accounts/logout')
def parse_index(self):
print "----------------------------get index of paper-----------------------------------"
result = []
soup = self.index_to_soup('http://www.nikkei.com/paper/')
#soup = self.index_to_soup(self.test_data())
for sect in soup.findAll('div', 'cmn-section kn-special JSID_baseSection'):
sect_title = sect.find('h3', 'cmnc-title').string
sect_result = []
for elem in sect.findAll(attrs={'class':['cmn-article_title']}):
url = 'http://www.nikkei.com' + elem.span.a['href']
url = re.sub("/article/", "/print-article/", url) # print version.
span = elem.span.a.span
if ((span is not None) and (len(span.contents) > 1)):
title = span.contents[1].string
sect_result.append(dict(title=title, url=url, date='',
description='', content=''))
result.append([sect_title, sect_result])
#pp.pprint(result)

63
recipes/techcrunch.recipe Normal file
View File

@ -0,0 +1,63 @@
__license__ = 'GPL v3'
__copyright__ = '2011, Darko Miletic <darko.miletic at gmail.com>'
'''
techcrunch.com
'''
from calibre.web.feeds.news import BasicNewsRecipe
class TechCrunch(BasicNewsRecipe):
title = 'TechCrunch'
__author__ = 'Darko Miletic'
description = 'IT News'
publisher = 'AOL Inc.'
category = 'news, IT'
oldest_article = 2
max_articles_per_feed = 200
no_stylesheets = True
encoding = 'utf8'
use_embedded_content = False
language = 'en'
remove_empty_feeds = True
publication_type = 'newsportal'
masthead_url = 'http://s2.wp.com/wp-content/themes/vip/tctechcrunch2/images/site-logo.png'
extra_css = """
body{font-family: Helvetica,Arial,sans-serif }
img{margin-bottom: 0.4em; display:block}
"""
conversion_options = {
'comment' : description
, 'tags' : category
, 'publisher' : publisher
, 'language' : language
}
remove_tags = [dict(name=['meta','link'])]
remove_attributes=['lang']
keep_only_tags=[
dict(name='h1', attrs={'class':'headline'})
,dict(attrs={'class':['author','post-time','body-copy']})
]
feeds = [(u'News', u'http://feeds.feedburner.com/TechCrunch/')]
def preprocess_html(self, soup):
for item in soup.findAll(style=True):
del item['style']
for item in soup.findAll('a'):
limg = item.find('img')
if item.string is not None:
str = item.string
item.replaceWith(str)
else:
if limg:
item.name = 'div'
item.attrs = []
else:
str = self.tag_to_string(item)
item.replaceWith(str)
for item in soup.findAll('img'):
if not item.has_key('alt'):
item['alt'] = 'image'
return soup

24
recipes/tijolaco.recipe Normal file
View File

@ -0,0 +1,24 @@
from calibre.web.feeds.recipes import BasicNewsRecipe
class Tijolaco(BasicNewsRecipe):
title = u'Tijolaco.com'
__author__ = u'Diniz Bortolotto'
description = u'Posts do Blog Tijola\xe7o.com'
oldest_article = 7
max_articles_per_feed = 50
encoding = 'utf8'
publisher = u'Brizola Neto'
category = 'politics, Brazil'
language = 'pt_BR'
publication_type = 'politics portal'
use_embedded_content = False
no_stylesheets = True
remove_javascript = True
feeds = [(u'Blog Tijola\xe7o.com', u'http://feeds.feedburner.com/Tijolacoblog')]
reverse_article_order = True
keep_only_tags = [dict(name='div', attrs={'class':'post'})]
remove_tags = [dict(name='span', attrs={'class':'com'})]

View File

@ -8,47 +8,33 @@ time.com
import re
from calibre.web.feeds.news import BasicNewsRecipe
from lxml import html
class Time(BasicNewsRecipe):
#recipe_disabled = ('This recipe has been disabled as TIME no longer'
# ' publish complete articles on the web.')
title = u'Time'
__author__ = 'Kovid Goyal and Sujata Raman'
__author__ = 'Kovid Goyal'
description = 'Weekly magazine'
encoding = 'utf-8'
no_stylesheets = True
language = 'en'
remove_javascript = True
extra_css = ''' h1 {font-family:georgia,serif;color:#000000;}
.mainHd{font-family:georgia,serif;color:#000000;}
h2 {font-family:Arial,Sans-serif;}
.name{font-family:Arial,Sans-serif; font-size:x-small;font-weight:bold; }
.date{font-family:Arial,Sans-serif; font-size:x-small ;color:#999999;}
.byline{font-family:Arial,Sans-serif; font-size:x-small ;}
.photoBkt{ font-size:x-small ;}
.vertPhoto{font-size:x-small ;}
.credits{font-family:Arial,Sans-serif; font-size:x-small ;color:gray;}
.credit{font-family:Arial,Sans-serif; font-size:x-small ;color:gray;}
.artTxt{font-family:georgia,serif;}
#content{font-family:georgia,serif;}
.caption{font-family:georgia,serif; font-size:x-small;color:#333333;}
.credit{font-family:georgia,serif; font-size:x-small;color:#999999;}
a:link{color:#CC0000;}
.breadcrumb{font-family:Arial,Sans-serif;font-size:x-small;}
'''
keep_only_tags = [
{
'class':['artHd', 'articleContent',
'entry-title','entry-meta', 'entry-content', 'thumbnail']
},
]
remove_tags = [
{'class':['content-tools', 'quigo', 'see',
'first-tier-social-tools', 'navigation', 'enlarge lightbox']},
{'id':['share-tools']},
{'rel':'lightbox'},
]
keep_only_tags = [ dict(name ="div",attrs = {"id" :["content"]}) ,
dict(name ="div",attrs = {"class" :["artHd","artTxt","photoBkt","vertPhoto","image","copy"]}) ,]
remove_tags = [ dict(name ="div",attrs = {'class':['articleFooterNav','listsByTopic','articleTools2','relatedContent','sideContent','topBannerWrap','articlePagination','nextUp',"rtCol","pagination","enlarge","contentTools2",]}),
dict(name ="span",attrs = {'class':['see']}),
dict(name ="div",attrs = {'id':['header','articleSideBar',"articleTools","articleFooter","cmBotLt","quigoPackage"]}),
dict(name ="a",attrs = {'class':['listLink']}),
dict(name ="ul",attrs = {'id':['shareSocial','tabs']}),
dict(name ="li",attrs = {'class':['back']}),
dict(name ="ul",attrs = {'class':['navCount']}),
]
recursions = 10
match_regexps = [r'/[0-9,]+-(2|3|4|5|6|7|8|9)(,\d+){0,1}.html',r'http://www.time.com/time/specials/packages/article/.*']
@ -56,10 +42,11 @@ class Time(BasicNewsRecipe):
r'<meta .+/>'), lambda m:'')]
def parse_index(self):
soup = self.index_to_soup('http://www.time.com/time/magazine')
img = soup.find('a', title="View Large Cover", href=True)
if img is not None:
cover_url = 'http://www.time.com'+img['href']
raw = self.index_to_soup('http://www.time.com/time/magazine', raw=True)
root = html.fromstring(raw)
img = root.xpath('//a[.="View Large Cover" and @href]')
if img:
cover_url = 'http://www.time.com' + img[0].get('href')
try:
nsoup = self.index_to_soup(cover_url)
img = nsoup.find('img', src=re.compile('archive/covers'))
@ -70,46 +57,48 @@ class Time(BasicNewsRecipe):
feeds = []
parent = soup.find(id='tocGuts')
for seched in parent.findAll(attrs={'class':'toc_seched'}):
section = self.tag_to_string(seched).capitalize()
articles = list(self.find_articles(seched))
feeds.append((section, articles))
parent = root.xpath('//div[@class="content-main-aside"]')[0]
for sec in parent.xpath(
'descendant::section[contains(@class, "sec-mag-section")]'):
h3 = sec.xpath('./h3')
if h3:
section = html.tostring(h3[0], encoding=unicode,
method='text').strip().capitalize()
self.log('Found section', section)
articles = list(self.find_articles(sec))
if articles:
feeds.append((section, articles))
return feeds
def find_articles(self, seched):
for a in seched.findNextSiblings( attrs={'class':['toc_hed','rule2']}):
if a.name in "div":
break
else:
yield {
'title' : self.tag_to_string(a),
'url' : 'http://www.time.com'+a['href'],
'date' : '',
'description' : self.article_description(a)
}
def find_articles(self, sec):
def article_description(self, a):
ans = []
while True:
t = a.nextSibling
if t is None:
break
a = t
if getattr(t, 'name', False):
if t.get('class', '') == 'toc_parens' or t.name == 'br':
continue
if t.name in ('div', 'a'):
break
ans.append(self.tag_to_string(t))
else:
ans.append(unicode(t))
return u' '.join(ans).replace(u'\xa0', u'').strip()
for article in sec.xpath('./article'):
h2 = article.xpath('./*[@class="entry-title"]')
if not h2: continue
a = h2[0].xpath('./a[@href]')
if not a: continue
title = html.tostring(a[0], encoding=unicode,
method='text').strip()
if not title: continue
url = a[0].get('href')
if url.startswith('/'):
url = 'http://www.time.com'+url
desc = ''
p = article.xpath('./*[@class="entry-content"]')
if p:
desc = html.tostring(p[0], encoding=unicode,
method='text')
self.log('\t', title, ':\n\t\t', desc)
yield {
'title' : title,
'url' : url,
'date' : '',
'description' : desc
}
def postprocess_html(self,soup,first):
for tag in soup.findAll(attrs ={'class':['artPag','pagination']}):
tag.extract()
return soup

30
recipes/vio_mundo.recipe Normal file
View File

@ -0,0 +1,30 @@
import re
from calibre.web.feeds.news import BasicNewsRecipe
class VioMundo(BasicNewsRecipe):
title = 'Blog VioMundo'
__author__ = 'Diniz Bortolotto'
description = 'Posts do Blog VioMundo'
publisher = 'Luiz Carlos Azenha'
oldest_article = 5
max_articles_per_feed = 20
category = 'news, politics, Brazil'
language = 'pt_BR'
publication_type = 'news and politics portal'
use_embedded_content = False
no_stylesheets = True
remove_javascript = True
feeds = [(u'Blog VioMundo', u'http://www.viomundo.com.br/feed')]
reverse_article_order = True
def print_version(self, url):
return url + '/print/'
remove_tags_after = dict(id='BlogContent')
preprocess_regexps = [
(re.compile(r'\|\ <u>.*</p>'),
lambda match: '</p>')
]

View File

@ -1,28 +1,29 @@
__license__ = 'GPL v3'
__copyright__ = '2010, Darko Miletic <darko.miletic at gmail.com>'
__copyright__ = '2011, Starson17 <Starson17 at gmail.com>'
'''
www.wired.co.uk
'''
from calibre import strftime
from calibre.web.feeds.news import BasicNewsRecipe
import re
class Wired_UK(BasicNewsRecipe):
title = 'Wired Magazine - UK edition'
__author__ = 'Darko Miletic'
__author__ = 'Starson17'
__version__ = 'v1.30'
__date__ = '15 July 2011'
description = 'Gaming news'
publisher = 'Conde Nast Digital'
category = 'news, games, IT, gadgets'
oldest_article = 32
oldest_article = 40
max_articles_per_feed = 100
no_stylesheets = True
encoding = 'utf-8'
use_embedded_content = False
masthead_url = 'http://www.wired.co.uk/_/media/wired-logo_UK.gif'
#masthead_url = 'http://www.wired.co.uk/_/media/wired-logo_UK.gif'
language = 'en_GB'
extra_css = ' body{font-family: Palatino,"Palatino Linotype","Times New Roman",Times,serif} img{margin-bottom: 0.8em } .img-descr{font-family: Tahoma,Arial,Helvetica,sans-serif; font-size: 0.6875em; display: block} '
index = 'http://www.wired.co.uk/wired-magazine.aspx'
index = 'http://www.wired.co.uk'
conversion_options = {
'comment' : description
@ -31,44 +32,118 @@ class Wired_UK(BasicNewsRecipe):
, 'language' : language
}
keep_only_tags = [dict(name='div', attrs={'class':'article-box'})]
remove_tags = [
dict(name=['object','embed','iframe','link'])
,dict(attrs={'class':['opts','comment','stories']})
]
remove_tags_after = dict(name='div',attrs={'class':'stories'})
keep_only_tags = [dict(name='div', attrs={'class':['layoutColumn1']})]
remove_tags = [dict(name='div',attrs={'class':['articleSidebar1','commentAddBox linkit','commentCountBox commentCountBoxBig']})]
remove_tags_after = dict(name='div',attrs={'class':['mainCopy entry-content','mainCopy']})
'''
remove_attributes = ['height','width']
,dict(name=['object','embed','iframe','link'])
,dict(attrs={'class':['opts','comment','stories']})
]
'''
def parse_index(self):
totalfeeds = []
soup = self.index_to_soup(self.index)
maincontent = soup.find('div',attrs={'class':'main-content'})
recentcontent = soup.find('ul',attrs={'class':'linkList3'})
mfeed = []
if maincontent:
st = maincontent.find(attrs={'class':'most-wired-box'})
if st:
for itt in st.findAll('a',href=True):
url = 'http://www.wired.co.uk' + itt['href']
title = self.tag_to_string(itt)
description = ''
date = strftime(self.timefmt)
mfeed.append({
'title' :title
,'date' :date
,'url' :url
,'description':description
})
totalfeeds.append(('Articles', mfeed))
if recentcontent:
for li in recentcontent.findAll('li'):
a = li.h2.a
url = self.index + a['href'] + '?page=all'
title = self.tag_to_string(a)
description = ''
date = strftime(self.timefmt)
mfeed.append({
'title' :title
,'date' :date
,'url' :url
,'description':description
})
totalfeeds.append(('Wired UK Magazine Latest News', mfeed))
popmagcontent = soup.findAll('div',attrs={'class':'sidebarLinkList'})
magcontent = popmagcontent[1]
mfeed2 = []
if magcontent:
a = magcontent.h3.a
if a:
url = self.index + a['href'] + '?page=all'
title = self.tag_to_string(a)
description = ''
date = strftime(self.timefmt)
mfeed2.append({
'title' :title
,'date' :date
,'url' :url
,'description':description
})
for li in magcontent.findAll('li'):
a = li.a
url = self.index + a['href'] + '?page=all'
title = self.tag_to_string(a)
description = ''
date = strftime(self.timefmt)
mfeed2.append({
'title' :title
,'date' :date
,'url' :url
,'description':description
})
totalfeeds.append(('Wired UK Magazine Features', mfeed2))
magsoup = self.index_to_soup(self.index + '/magazine')
startcontent = magsoup.find('h3',attrs={'class':'magSubSectionTitle titleStart'}).parent
mfeed3 = []
if startcontent:
for li in startcontent.findAll('li'):
a = li.a
url = self.index + a['href'] + '?page=all'
title = self.tag_to_string(a)
description = ''
date = strftime(self.timefmt)
mfeed3.append({
'title' :title
,'date' :date
,'url' :url
,'description':description
})
totalfeeds.append(('Wired UK Magazine More', mfeed3))
playcontent = magsoup.find('h3',attrs={'class':'magSubSectionTitle titlePlay'}).parent
mfeed4 = []
if playcontent:
for li in playcontent.findAll('li'):
a = li.a
url = self.index + a['href'] + '?page=all'
title = self.tag_to_string(a)
description = ''
date = strftime(self.timefmt)
mfeed4.append({
'title' :title
,'date' :date
,'url' :url
,'description':description
})
totalfeeds.append(('Wired UK Magazine Play', mfeed4))
return totalfeeds
def get_cover_url(self):
cover_url = None
soup = self.index_to_soup(self.index)
cover_item = soup.find('span', attrs={'class':'cover'})
cover_url = ''
soup = self.index_to_soup(self.index + '/magazine/archive')
cover_item = soup.find('div', attrs={'class':'image linkme'})
if cover_item:
cover_url = cover_item.img['src']
return cover_url
def print_version(self, url):
return url + '?page=all'
def preprocess_html(self, soup):
for tag in soup.findAll(name='p'):
if tag.find(name='span', text=re.compile(r'This article was taken from.*', re.DOTALL|re.IGNORECASE)):
tag.extract()
return soup
extra_css = '''
h1{font-family:Arial,Helvetica,sans-serif; font-weight:bold;font-size:large;}
h2{font-family:Arial,Helvetica,sans-serif; font-weight:normal;font-size:small;}
p{font-family:Arial,Helvetica,sans-serif;font-size:small;}
body{font-family:Helvetica,Arial,sans-serif;font-size:small;}
'''

View File

@ -15,15 +15,16 @@ class ZeitDe(BasicNewsRecipe):
encoding = 'UTF-8'
__author__ = 'Martin Pitt, Sujata Raman, Ingo Paschke and Marc Toensing'
no_stylesheets = True
max_articles_per_feed = 40
remove_tags = [
dict(name='iframe'),
dict(name='div', attrs={'class':["response","pagination block","pagenav","inline link", "copyright"] }),
dict(name='p', attrs={'class':["ressortbacklink", "copyright"] }),
dict(name='div', attrs={'id':["place_5","place_4","comments"]})
]
dict(name='iframe'),
dict(name='div', attrs={'class':["response","pagination block","pagenav","inline link", "copyright"] }),
dict(name='p', attrs={'class':["ressortbacklink", "copyright"] }),
dict(name='div', attrs={'id':["place_5","place_4","comments"]})
]
keep_only_tags = [dict(id=['main'])]

View File

@ -2,18 +2,21 @@
# -*- coding: utf-8 mode: python -*-
__license__ = 'GPL v3'
__copyright__ = '2010-2011, Steffen Siebert <calibre at steffensiebert.de>'
__copyright__ = '2010, Steffen Siebert <calibre at steffensiebert.de>'
__docformat__ = 'restructuredtext de'
__version__ = '1.2'
__version__ = '1.5'
"""
Die Zeit EPUB
"""
import os, urllib2, zipfile, re
import os, zipfile, re, cStringIO
from calibre.web.feeds.news import BasicNewsRecipe
from calibre.ptempfile import PersistentTemporaryFile
from calibre import walk
from urlparse import urlparse
from contextlib import closing
from calibre.utils.magick.draw import save_cover_data_to
class ZeitEPUBAbo(BasicNewsRecipe):
@ -22,49 +25,112 @@ class ZeitEPUBAbo(BasicNewsRecipe):
language = 'de'
lang = 'de-DE'
__author__ = 'Steffen Siebert and Tobias Isenberg'
__author__ = 'Steffen Siebert, revised by Tobias Isenberg (with some code by Kovid Goyal)'
needs_subscription = True
conversion_options = {
'no_default_epub_cover' : True,
# fixing the wrong left margin
'mobi_ignore_margins' : True,
'keep_ligatures' : True,
}
preprocess_regexps = [
# filtering for correct dashes
(re.compile(r' - '), lambda match: ' '), # regular "Gedankenstrich"
(re.compile(r' -,'), lambda match: ' ,'), # "Gedankenstrich" before a comma
(re.compile(r'(?<=\d)-(?=\d)'), lambda match: ''), # number-number
# filtering for correct dashes ("Gedankenstrich" and "bis")
(re.compile(u' (-|\u2212)(?=[ ,])'), lambda match: u' \u2013'),
(re.compile(r'(?<=\d)-(?=\d)'), lambda match: u'\u2013'), # number-number
(re.compile(u'(?<=\d,)-(?= ?\u20AC)'), lambda match: u'\u2013'), # ,- Euro
# fix the number dash number dash for the title image that was broken by the previous line
(re.compile(u'(?<=\d\d\d\d)\u2013(?=\d?\d\.png)'), lambda match: '-'),
# filtering for certain dash cases
(re.compile(r'Bild - Zeitung'), lambda match: 'Bild-Zeitung'), # the obvious
(re.compile(r'EMail'), lambda match: 'E-Mail'), # the obvious
(re.compile(r'SBahn'), lambda match: 'S-Bahn'), # the obvious
(re.compile(r'UBoot'), lambda match: 'U-Boot'), # the obvious
(re.compile(r'T Shirt'), lambda match: 'T-Shirt'), # the obvious
(re.compile(r'TShirt'), lambda match: 'T-Shirt'), # the obvious
# the next two lines not only fix errors but also create new ones. this is due to additional errors in
# the typesetting such as missing commas or wrongly placed dashes. but more is fixed than broken.
(re.compile(r'(?<!und|der|\w\w,) -(?=\w)'), lambda match: '-'), # space too much before a connecting dash
(re.compile(r'(?<=\w)- (?!und\b|oder\b|wie\b|aber\b|auch\b|sondern\b|bis\b|&amp;|&\s|bzw\.|auf\b|eher\b)'), lambda match: '-'), # space too much after a connecting dash
# filtering for missing spaces before the month in long dates
(re.compile(u'(?<=\d)\.(?=(Januar|Februar|M\u00E4rz|April|Mai|Juni|Juli|August|September|Oktober|November|Dezember))'), lambda match: '. '),
# filtering for other missing spaces
(re.compile(r'Stuttgart21'), lambda match: 'Stuttgart 21'), # the obvious
(re.compile(u'(?<=\d)(?=\u20AC)'), lambda match: u'\u2013'), # Zahl[no space]Euro
(re.compile(r':(?=[^\d\s</])'), lambda match: ': '), # missing space after colon
(re.compile(u'\u00AB(?=[^\-\.:;,\?!<\)\s])'), lambda match: u'\u00AB '), # missing space after closing quotation
(re.compile(u'(?<=[^\s\(>])\u00BB'), lambda match: u' \u00BB'), # missing space before opening quotation
(re.compile(r'(?<=[a-z])(?=(I|II|III|IV|V|VI|VII|VIII|IX|X|XI|XII|XIII|XIV|XV|XVI|XVII|XVIII|XIX|XX)\.)'), lambda match: ' '), # missing space before Roman numeral
(re.compile(r'(?<=(I|V|X)\.)(?=[\w])'), lambda match: ' '), # missing space after Roman numeral
(re.compile(r'(?<=(II|IV|VI|IX|XI|XV|XX)\.)(?=[\w])'), lambda match: ' '), # missing space after Roman numeral
(re.compile(r'(?<=(III|VII|XII|XIV|XVI|XIX)\.)(?=[\w])'), lambda match: ' '), # missing space after Roman numeral
(re.compile(r'(?<=(VIII|XIII|XVII)\.)(?=[\w])'), lambda match: ' '), # missing space after Roman numeral
(re.compile(r'(?<=(XVIII)\.)(?=[\w])'), lambda match: ' '), # missing space after Roman numeral
(re.compile(r'(?<=[A-Za-zÄÖÜäöü]),(?=[A-Za-zÄÖÜäöü])'), lambda match: ', '), # missing space after comma
(re.compile(r'(?<=[a-zäöü])\.(?=[A-ZÄÖÜ][A-Za-zÄÖÜäöü])'), lambda match: '. '), # missing space after full-stop
(re.compile(r'(?<=[uU]\.) (?=a\.)'), lambda match: u'\u2008'), # fix abbreviation that was potentially broken previously
(re.compile(r'(?<=[iI]\.) (?=A\.)'), lambda match: u'\u2008'), # fix abbreviation that was potentially broken previously
(re.compile(r'(?<=[zZ]\.) (?=B\.)'), lambda match: u'\u2008'), # fix abbreviation that was potentially broken previously
(re.compile(r'(?<=\w\.) (?=[A-Z][a-z]*@)'), lambda match: ''), # fix e-mail address that was potentially broken previously
(re.compile(r'(?<=\d)[Pp]rozent'), lambda match: ' Prozent'),
(re.compile(r'\.\.\.\.+'), lambda match: '...'), # too many dots (....)
(re.compile(r'(?<=[^\s])\.\.\.'), lambda match: ' ...'), # spaces before ...
(re.compile(r'\.\.\.(?=[^\s])'), lambda match: '... '), # spaces after ...
(re.compile(r'(?<=[\[\(]) \.\.\. (?=[\]\)])'), lambda match: '...'), # fix special cases of ... in brackets
(re.compile(u'(?<=[\u00BB\u203A]) \.\.\.'), lambda match: '...'), # fix special cases of ... after a quotation mark
(re.compile(u'\.\.\. (?=[\u00AB\u2039,])'), lambda match: '...'), # fix special cases of ... before a quotation mark or comma
# fix missing spaces between numbers and any sort of units, possibly with dot
(re.compile(r'(?<=\d)(?=(Femto|Piko|Nano|Mikro|Milli|Zenti|Dezi|Hekto|Kilo|Mega|Giga|Tera|Peta|Tausend|Trilli|Kubik|Quadrat|Meter|Uhr|Jahr|Schuljahr|Seite))'), lambda match: ' '),
(re.compile(r'(?<=\d\.)(?=(Femto|Piko|Nano|Mikro|Milli|Zenti|Dezi|Hekto|Kilo|Mega|Giga|Tera|Peta|Tausend|Trilli|Kubik|Quadrat|Meter|Uhr|Jahr|Schuljahr|Seite))'), lambda match: ' '),
# fix wrong spaces
(re.compile(r'(?<=<p class="absatz">[A-ZÄÖÜ]) (?=[a-zäöü\-])'), lambda match: ''), # at beginning of paragraphs
(re.compile(u' \u00AB'), lambda match: u'\u00AB '), # before closing quotation
(re.compile(u'\u00BB '), lambda match: u' \u00BB'), # after opening quotation
# filtering for spaces in large numbers for better readability
(re.compile(r'(?<=\d\d)(?=\d\d\d[ ,\.;\)<\?!-])'), lambda match: u'\u2008'), # end of the number with some character following
(re.compile(r'(?<=\d\d)(?=\d\d\d. )'), lambda match: u'\u2008'), # end of the number with full-stop following, then space is necessary (avoid file names)
(re.compile(u'(?<=\d)(?=\d\d\d\u2008)'), lambda match: u'\u2008'), # next level
(re.compile(u'(?<=\d)(?=\d\d\d\u2008)'), lambda match: u'\u2008'), # next level
(re.compile(u'(?<=\d)(?=\d\d\d\u2008)'), lambda match: u'\u2008'), # next level
(re.compile(u'(?<=\d)(?=\d\d\d\u2008)'), lambda match: u'\u2008'), # next level
# filtering for unicode characters that are missing on the Kindle,
# try to replace them with meaningful work-arounds
(re.compile(u'\u2080'), lambda match: '<span style="font-size: 50%;">0</span>'), # subscript-0
(re.compile(u'\u2081'), lambda match: '<span style="font-size: 50%;">1</span>'), # subscript-1
(re.compile(u'\u2082'), lambda match: '<span style="font-size: 50%;">2</span>'), # subscript-2
(re.compile(u'\u2083'), lambda match: '<span style="font-size: 50%;">3</span>'), # subscript-3
(re.compile(u'\u2084'), lambda match: '<span style="font-size: 50%;">4</span>'), # subscript-4
(re.compile(u'\u2085'), lambda match: '<span style="font-size: 50%;">5</span>'), # subscript-5
(re.compile(u'\u2086'), lambda match: '<span style="font-size: 50%;">6</span>'), # subscript-6
(re.compile(u'\u2087'), lambda match: '<span style="font-size: 50%;">7</span>'), # subscript-7
(re.compile(u'\u2088'), lambda match: '<span style="font-size: 50%;">8</span>'), # subscript-8
(re.compile(u'\u2089'), lambda match: '<span style="font-size: 50%;">9</span>'), # subscript-9
(re.compile(u'\u2080'), lambda match: '<span style="font-size: 40%;">0</span>'), # subscript-0
(re.compile(u'\u2081'), lambda match: '<span style="font-size: 40%;">1</span>'), # subscript-1
(re.compile(u'\u2082'), lambda match: '<span style="font-size: 40%;">2</span>'), # subscript-2
(re.compile(u'\u2083'), lambda match: '<span style="font-size: 40%;">3</span>'), # subscript-3
(re.compile(u'\u2084'), lambda match: '<span style="font-size: 40%;">4</span>'), # subscript-4
(re.compile(u'\u2085'), lambda match: '<span style="font-size: 40%;">5</span>'), # subscript-5
(re.compile(u'\u2086'), lambda match: '<span style="font-size: 40%;">6</span>'), # subscript-6
(re.compile(u'\u2087'), lambda match: '<span style="font-size: 40%;">7</span>'), # subscript-7
(re.compile(u'\u2088'), lambda match: '<span style="font-size: 40%;">8</span>'), # subscript-8
(re.compile(u'\u2089'), lambda match: '<span style="font-size: 40%;">9</span>'), # subscript-9
# always chance CO2
(re.compile(r'CO2'), lambda match: 'CO<span style="font-size: 40%;">2</span>'), # CO2
# remove *** paragraphs
(re.compile(r'<p class="absatz">\*\*\*</p>'), lambda match: ''),
# better layout for the top line of each article
(re.compile(u'(?<=DIE ZEIT N\u00B0 \d /) (?=\d\d)'), lambda match: ' 20'), # proper year in edition number
(re.compile(u'(?<=DIE ZEIT N\u00B0 \d\d /) (?=\d\d)'), lambda match: ' 20'), # proper year in edition number
(re.compile(u'(?<=>)(?=DIE ZEIT N\u00B0 \d\d / 20\d\d)'), lambda match: u' \u2014 '), # m-dash between category and DIE ZEIT
]
def build_index(self):
domain = "http://premium.zeit.de"
url = domain + "/abovorteile/cgi-bin/_er_member/p4z.fpl?ER_Do=getUserData&ER_NextTemplate=login_ok"
domain = "https://premium.zeit.de"
url = domain + "/abo/zeit_digital"
browser = self.get_browser()
browser.add_password("http://premium.zeit.de", self.username, self.password)
try:
browser.open(url)
except urllib2.HTTPError:
self.report_progress(0,_("Can't login to download issue"))
raise ValueError('Failed to login, check your username and password')
response = browser.follow_link(text="DIE ZEIT als E-Paper")
response = browser.follow_link(url_regex=re.compile('^http://contentserver.hgv-online.de/nodrm/fulfillment\\?distributor=zeit-online&orderid=zeit_online.*'))
# new login process
response = browser.open(url)
browser.select_form(nr=2)
browser.form['name']=self.username
browser.form['pass']=self.password
browser.submit()
# now find the correct file, we will still use the ePub file
epublink = browser.find_link(text_regex=re.compile('.*Ausgabe als Datei im ePub-Format.*'))
response = browser.follow_link(epublink)
self.report_progress(1,_('next step'))
tmp = PersistentTemporaryFile(suffix='.epub')
self.report_progress(0,_('downloading epub'))
@ -104,9 +170,45 @@ class ZeitEPUBAbo(BasicNewsRecipe):
# getting url of the cover
def get_cover_url(self):
self.log.warning('Downloading cover')
try:
inhalt = self.index_to_soup('http://www.zeit.de/inhalt')
cover_url = inhalt.find('div', attrs={'class':'singlearchive clearfix'}).img['src'].replace('icon_','')
self.log.warning('Trying PDF-based cover')
domain = "https://premium.zeit.de"
url = domain + "/abo/zeit_digital"
browser = self.get_browser()
# new login process
browser.open(url)
browser.select_form(nr=2)
browser.form['name']=self.username
browser.form['pass']=self.password
browser.submit()
# actual cover search
pdflink = browser.find_link(url_regex=re.compile('system/files/epaper/DZ/pdf/DZ_ePaper*'))
cover_url = urlparse(pdflink.base_url)[0]+'://'+urlparse(pdflink.base_url)[1]+''+(urlparse(pdflink.url)[2]).replace('ePaper_','').replace('.pdf','_001.pdf')
self.log.warning('PDF link found:')
self.log.warning(cover_url)
# download the cover (has to be here due to new login process)
with closing(browser.open(cover_url)) as r:
cdata = r.read()
from calibre.ebooks.metadata.pdf import get_metadata
stream = cStringIO.StringIO(cdata)
cdata = None
mi = get_metadata(stream)
if mi.cover_data and mi.cover_data[1]:
cdata = mi.cover_data[1]
cpath = os.path.join(self.output_dir, 'cover.jpg')
save_cover_data_to(cdata, cpath)
cover_url = cpath
except:
cover_url = 'http://images.zeit.de/bilder/titelseiten_zeit/1946/001_001.jpg'
self.log.warning('Trying low-res cover')
try:
inhalt = self.index_to_soup('http://www.zeit.de/inhalt')
cover_url = inhalt.find('div', attrs={'class':'singlearchive clearfix'}).img['src'].replace('icon_','')
except:
self.log.warning('Using static old low-res cover')
cover_url = 'http://images.zeit.de/bilder/titelseiten_zeit/1946/001_001.jpg'
return cover_url

View File

@ -366,3 +366,10 @@ server_listen_on = '0.0.0.0'
# on at your own risk!
unified_title_toolbar_on_osx = False
#: Save original file when converting from same format to same format
# When calibre does a conversion from the same format to the same format, for
# example, from EPUB to EPUB, the original file is saved, so that in case the
# conversion is poor, you can tweak the settings and run it again. By setting
# this to False you can prevent calibre from saving the original file.
save_original_format = True

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

View File

@ -379,7 +379,8 @@
<!-- image -->
<xsl:template match="fb:image">
<div align="center">
<img border="1">
<xsl:element name="img">
<xsl:attribute name="border">1</xsl:attribute>
<xsl:choose>
<xsl:when test="starts-with(@xlink:href,'#')">
<xsl:attribute name="src"><xsl:value-of select="substring-after(@xlink:href,'#')"/></xsl:attribute>
@ -388,7 +389,10 @@
<xsl:attribute name="src"><xsl:value-of select="@xlink:href"/></xsl:attribute>
</xsl:otherwise>
</xsl:choose>
</img>
<xsl:if test="@title">
<xsl:attribute name="title"><xsl:value-of select="@title"/></xsl:attribute>
</xsl:if>
</xsl:element>
</div>
</xsl:template>
</xsl:stylesheet>

View File

@ -1,5 +1,5 @@
" Project wide builtins
let g:pyflakes_builtins += ["dynamic_property", "__", "P", "I", "lopen", "icu_lower", "icu_upper", "icu_title", "ngettext"]
let g:pyflakes_builtins = ["_", "dynamic_property", "__", "P", "I", "lopen", "icu_lower", "icu_upper", "icu_title", "ngettext"]
python << EOFPY
import os
@ -15,7 +15,7 @@ vipy.session.initialize(project_name='calibre', src_dir=src_dir,
project_dir=project_dir, base_dir=base_dir)
def recipe_title_callback(raw):
return eval(raw.decode('utf-8'))
return eval(raw.decode('utf-8')).replace(' ', '_')
vipy.session.add_content_browser('.r', ',r', 'Recipe',
vipy.session.glob_based_iterator(os.path.join(project_dir, 'recipes', '*.recipe')),

View File

@ -25,18 +25,11 @@ class Message:
return '%s:%s: %s'%(self.filename, self.lineno, self.msg)
def check_for_python_errors(code_string, filename):
# Since compiler.parse does not reliably report syntax errors, use the
# built in compiler first to detect those.
import _ast
# First, compile into an AST and handle syntax errors.
try:
try:
compile(code_string, filename, "exec")
except MemoryError:
# Python 2.4 will raise MemoryError if the source can't be
# decoded.
if sys.version_info[:2] == (2, 4):
raise SyntaxError(None)
raise
except (SyntaxError, IndentationError), value:
tree = compile(code_string, filename, "exec", _ast.PyCF_ONLY_AST)
except (SyntaxError, IndentationError) as value:
msg = value.args[0]
(lineno, offset, text) = value.lineno, value.offset, value.text
@ -47,13 +40,11 @@ def check_for_python_errors(code_string, filename):
# bogus message that claims the encoding the file declared was
# unknown.
msg = "%s: problem decoding source" % filename
return [Message(filename, lineno, msg)]
else:
# Okay, it's syntactically valid. Now parse it into an ast and check
# it.
import compiler
checker = __import__('pyflakes.checker').checker
tree = compiler.parse(code_string)
# Okay, it's syntactically valid. Now check it.
w = checker.Checker(tree, filename)
w.messages.sort(lambda a, b: cmp(a.lineno, b.lineno))
return [Message(x.filename, x.lineno, x.message%x.message_args) for x in

View File

@ -8,11 +8,18 @@ __docformat__ = 'restructuredtext en'
import os, tempfile, shutil, subprocess, glob, re, time, textwrap
from distutils import sysconfig
from functools import partial
from setup import Command, __appname__, __version__
from setup.build_environment import pyqt
class POT(Command):
def qt_sources():
qtdir = glob.glob('/usr/src/qt-*')[-1]
j = partial(os.path.join, qtdir)
return list(map(j, [
'src/gui/widgets/qdialogbuttonbox.cpp',
]))
class POT(Command): # {{{
description = 'Update the .pot translation template'
PATH = os.path.join(Command.SRC, __appname__, 'translations')
@ -82,6 +89,8 @@ class POT(Command):
time=time.strftime('%Y-%m-%d %H:%M+%Z'))
files = self.source_files()
qt_inputs = qt_sources()
with tempfile.NamedTemporaryFile() as fl:
fl.write('\n'.join(files))
fl.flush()
@ -91,8 +100,14 @@ class POT(Command):
subprocess.check_call(['xgettext', '-f', fl.name,
'--default-domain=calibre', '-o', out.name, '-L', 'Python',
'--from-code=UTF-8', '--sort-by-file', '--omit-header',
'--no-wrap', '-k__',
'--no-wrap', '-k__', '--add-comments=NOTE:',
])
subprocess.check_call(['xgettext', '-j',
'--default-domain=calibre', '-o', out.name,
'--from-code=UTF-8', '--sort-by-file', '--omit-header',
'--no-wrap', '-kQT_TRANSLATE_NOOP:2',
] + qt_inputs)
with open(out.name, 'rb') as f:
src = f.read()
os.remove(out.name)
@ -102,10 +117,12 @@ class POT(Command):
with open(pot, 'wb') as f:
f.write(src)
self.info('Translations template:', os.path.abspath(pot))
return pot
class Translations(POT):
return pot
# }}}
class Translations(POT): # {{{
description='''Compile the translations'''
DEST = os.path.join(os.path.dirname(POT.SRC), 'resources', 'localization',
'locales')
@ -117,7 +134,6 @@ class Translations(POT):
locale = os.path.splitext(os.path.basename(po_file))[0]
return locale, os.path.join(self.DEST, locale, 'messages.mo')
def run(self, opts):
for f in self.po_files():
locale, dest = self.mo_file(f)
@ -126,7 +142,7 @@ class Translations(POT):
os.makedirs(base)
self.info('\tCompiling translations for', locale)
subprocess.check_call(['msgfmt', '-o', dest, f])
if locale in ('en_GB', 'nds', 'te', 'yi'):
if locale in ('en_GB', 'en_CA', 'en_AU', 'si', 'ur', 'sc', 'ltg', 'nds', 'te', 'yi'):
continue
pycountry = self.j(sysconfig.get_python_lib(), 'pycountry',
'locales', locale, 'LC_MESSAGES')
@ -140,17 +156,6 @@ class Translations(POT):
self.warn('No ISO 639 translations for locale:', locale,
'\nDo you have pycountry installed?')
base = os.path.join(pyqt.qt_data_dir, 'translations')
qt_translations = glob.glob(os.path.join(base, 'qt_*.qm'))
if not qt_translations:
raise Exception('Could not find qt translations')
for f in qt_translations:
locale = self.s(self.b(f))[0][3:]
dest = self.j(self.DEST, locale, 'LC_MESSAGES', 'qt.qm')
if self.e(self.d(dest)) and self.newer(dest, f):
self.info('\tCopying Qt translation for locale:', locale)
shutil.copy2(f, dest)
self.write_stats()
self.freeze_locales()
@ -201,7 +206,7 @@ class Translations(POT):
for x in (i, j, d):
if os.path.exists(x):
os.remove(x)
# }}}
class GetTranslations(Translations):

View File

@ -341,7 +341,7 @@ def random_user_agent():
def browser(honor_time=True, max_time=2, mobile_browser=False, user_agent=None):
'''
Create a mechanize browser for web scraping. The browser handles cookies,
refresh requests and ignores robots.txt. Also uses proxy if avaialable.
refresh requests and ignores robots.txt. Also uses proxy if available.
:param honor_time: If True honors pause time in refresh requests
:param max_time: Maximum time in seconds to wait during a refresh request
@ -474,7 +474,7 @@ def strftime(fmt, t=None):
def my_unichr(num):
try:
return unichr(num)
except ValueError:
except (ValueError, OverflowError):
return u'?'
def entity_to_unicode(match, exceptions=[], encoding='cp1252',

View File

@ -4,7 +4,7 @@ __license__ = 'GPL v3'
__copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
__docformat__ = 'restructuredtext en'
__appname__ = u'calibre'
numeric_version = (0, 8, 9)
numeric_version = (0, 8, 10)
__version__ = u'.'.join(map(unicode, numeric_version))
__author__ = u"Kovid Goyal <kovid@kovidgoyal.net>"

View File

@ -8,7 +8,7 @@ __copyright__ = '2011, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
# Imports {{{
import os, shutil, uuid, json
import os, shutil, uuid, json, glob, time, tempfile
from functools import partial
import apsw
@ -25,7 +25,7 @@ from calibre.utils.config import to_json, from_json, prefs, tweaks
from calibre.utils.date import utcfromtimestamp, parse_date
from calibre.utils.filenames import is_case_sensitive
from calibre.db.tables import (OneToOneTable, ManyToOneTable, ManyToManyTable,
SizeTable, FormatsTable, AuthorsTable, IdentifiersTable)
SizeTable, FormatsTable, AuthorsTable, IdentifiersTable, CompositeTable)
# }}}
'''
@ -37,6 +37,8 @@ Differences in semantics from pysqlite:
'''
SPOOL_SIZE = 30*1024*1024
class DynamicFilter(object): # {{{
'No longer used, present for legacy compatibility'
@ -478,7 +480,6 @@ class DB(object):
remove.append(data)
continue
self.custom_column_label_map[data['label']] = data['num']
self.custom_column_num_map[data['num']] = \
self.custom_column_label_map[data['label']] = data
@ -613,10 +614,31 @@ class DB(object):
tables['size'] = SizeTable('size', self.field_metadata['size'].copy())
for label, data in self.custom_column_label_map.iteritems():
label = '#' + label
self.FIELD_MAP = {'id':0, 'title':1, 'authors':2, 'timestamp':3,
'size':4, 'rating':5, 'tags':6, 'comments':7, 'series':8,
'publisher':9, 'series_index':10, 'sort':11, 'author_sort':12,
'formats':13, 'path':14, 'pubdate':15, 'uuid':16, 'cover':17,
'au_map':18, 'last_modified':19, 'identifiers':20}
for k,v in self.FIELD_MAP.iteritems():
self.field_metadata.set_field_record_index(k, v, prefer_custom=False)
base = max(self.FIELD_MAP.itervalues())
for label_, data in self.custom_column_label_map.iteritems():
label = self.field_metadata.custom_field_prefix + label_
metadata = self.field_metadata[label].copy()
link_table = self.custom_table_names(data['num'])[1]
self.FIELD_MAP[data['num']] = base = base+1
self.field_metadata.set_field_record_index(label_, base,
prefer_custom=True)
if data['datatype'] == 'series':
# account for the series index column. Field_metadata knows that
# the series index is one larger than the series. If you change
# it here, be sure to change it there as well.
self.FIELD_MAP[str(data['num'])+'_index'] = base = base+1
self.field_metadata.set_field_record_index(label_+'_index', base,
prefer_custom=True)
if data['normalized']:
if metadata['is_multiple']:
@ -633,7 +655,16 @@ class DB(object):
metadata['table'] = link_table
tables[label] = OneToOneTable(label, metadata)
else:
tables[label] = OneToOneTable(label, metadata)
if data['datatype'] == 'composite':
tables[label] = CompositeTable(label, metadata)
else:
tables[label] = OneToOneTable(label, metadata)
self.FIELD_MAP['ondevice'] = base = base+1
self.field_metadata.set_field_record_index('ondevice', base, prefer_custom=False)
self.FIELD_MAP['marked'] = base = base+1
self.field_metadata.set_field_record_index('marked', base, prefer_custom=False)
# }}}
@property
@ -732,5 +763,57 @@ class DB(object):
pprint.pprint(table.metadata)
raise
def format_abspath(self, book_id, fmt, fname, path):
path = os.path.join(self.library_path, path)
fmt = ('.' + fmt.lower()) if fmt else ''
fmt_path = os.path.join(path, fname+fmt)
if os.path.exists(fmt_path):
return fmt_path
try:
candidates = glob.glob(os.path.join(path, '*'+fmt))
except: # If path contains strange characters this throws an exc
candidates = []
if fmt and candidates and os.path.exists(candidates[0]):
shutil.copyfile(candidates[0], fmt_path)
return fmt_path
def format_metadata(self, book_id, fmt, fname, path):
path = self.format_abspath(book_id, fmt, fname, path)
ans = {}
if path is not None:
stat = os.stat(path)
ans['size'] = stat.st_size
ans['mtime'] = utcfromtimestamp(stat.st_mtime)
return ans
def cover(self, path, as_file=False, as_image=False,
as_path=False):
path = os.path.join(self.library_path, path, 'cover.jpg')
ret = None
if os.access(path, os.R_OK):
try:
f = lopen(path, 'rb')
except (IOError, OSError):
time.sleep(0.2)
f = lopen(path, 'rb')
with f:
if as_path:
pt = PersistentTemporaryFile('_dbcover.jpg')
with pt:
shutil.copyfileobj(f, pt)
return pt.name
if as_file:
ret = tempfile.SpooledTemporaryFile(SPOOL_SIZE)
shutil.copyfileobj(f, ret)
ret.seek(0)
else:
ret = f.read()
if as_image:
from PyQt4.Qt import QImage
i = QImage()
i.loadFromData(ret)
ret = i
return ret
# }}}

View File

@ -7,5 +7,380 @@ __license__ = 'GPL v3'
__copyright__ = '2011, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
import os
from collections import defaultdict
from functools import wraps, partial
from calibre.db.locking import create_locks, RecordLock
from calibre.db.fields import create_field
from calibre.ebooks.book.base import Metadata
from calibre.utils.date import now
def api(f):
f.is_cache_api = True
return f
def read_api(f):
f = api(f)
f.is_read_api = True
return f
def write_api(f):
f = api(f)
f.is_read_api = False
return f
def wrap_simple(lock, func):
@wraps(func)
def ans(*args, **kwargs):
with lock:
return func(*args, **kwargs)
return ans
class Cache(object):
def __init__(self, backend):
self.backend = backend
self.fields = {}
self.composites = set()
self.read_lock, self.write_lock = create_locks()
self.record_lock = RecordLock(self.read_lock)
self.format_metadata_cache = defaultdict(dict)
# Implement locking for all simple read/write API methods
# An unlocked version of the method is stored with the name starting
# with a leading underscore. Use the unlocked versions when the lock
# has already been acquired.
for name in dir(self):
func = getattr(self, name)
ira = getattr(func, 'is_read_api', None)
if ira is not None:
# Save original function
setattr(self, '_'+name, func)
# Wrap it in a lock
lock = self.read_lock if ira else self.write_lock
setattr(self, name, wrap_simple(lock, func))
@property
def field_metadata(self):
return self.backend.field_metadata
def _format_abspath(self, book_id, fmt):
'''
Return absolute path to the ebook file of format `format`
WARNING: This method will return a dummy path for a network backend DB,
so do not rely on it, use format(..., as_path=True) instead.
Currently used only in calibredb list, the viewer and the catalogs (via
get_data_as_dict()).
Apart from the viewer, I don't believe any of the others do any file
I/O with the results of this call.
'''
try:
name = self.fields['formats'].format_fname(book_id, fmt)
path = self._field_for('path', book_id).replace('/', os.sep)
except:
return None
if name and path:
return self.backend.format_abspath(book_id, fmt, name, path)
def _get_metadata(self, book_id, get_user_categories=True): # {{{
mi = Metadata(None)
author_ids = self._field_ids_for('authors', book_id)
aut_list = [self._author_data(i) for i in author_ids]
aum = []
aus = {}
aul = {}
for rec in aut_list:
aut = rec['name']
aum.append(aut)
aus[aut] = rec['sort']
aul[aut] = rec['link']
mi.title = self._field_for('title', book_id,
default_value=_('Unknown'))
mi.authors = aum
mi.author_sort = self._field_for('author_sort', book_id,
default_value=_('Unknown'))
mi.author_sort_map = aus
mi.author_link_map = aul
mi.comments = self._field_for('comments', book_id)
mi.publisher = self._field_for('publisher', book_id)
n = now()
mi.timestamp = self._field_for('timestamp', book_id, default_value=n)
mi.pubdate = self._field_for('pubdate', book_id, default_value=n)
mi.uuid = self._field_for('uuid', book_id,
default_value='dummy')
mi.title_sort = self._field_for('sort', book_id,
default_value=_('Unknown'))
mi.book_size = self._field_for('size', book_id, default_value=0)
mi.ondevice_col = self._field_for('ondevice', book_id, default_value='')
mi.last_modified = self._field_for('last_modified', book_id,
default_value=n)
formats = self._field_for('formats', book_id)
mi.format_metadata = {}
if not formats:
formats = None
else:
for f in formats:
mi.format_metadata[f] = self._format_metadata(book_id, f)
formats = ','.join(formats)
mi.formats = formats
mi.has_cover = _('Yes') if self._field_for('cover', book_id,
default_value=False) else ''
mi.tags = list(self._field_for('tags', book_id, default_value=()))
mi.series = self._field_for('series', book_id)
if mi.series:
mi.series_index = self._field_for('series_index', book_id,
default_value=1.0)
mi.rating = self._field_for('rating', book_id)
mi.set_identifiers(self._field_for('identifiers', book_id,
default_value={}))
mi.application_id = book_id
mi.id = book_id
composites = {}
for key, meta in self.field_metadata.custom_iteritems():
mi.set_user_metadata(key, meta)
if meta['datatype'] == 'composite':
composites.append(key)
else:
mi.set(key, val=self._field_for(meta['label'], book_id),
extra=self._field_for(meta['label']+'_index', book_id))
for c in composites:
mi.set(key, val=self._composite_for(key, book_id, mi))
user_cat_vals = {}
if get_user_categories:
user_cats = self.prefs['user_categories']
for ucat in user_cats:
res = []
for name,cat,ign in user_cats[ucat]:
v = mi.get(cat, None)
if isinstance(v, list):
if name in v:
res.append([name,cat])
elif name == v:
res.append([name,cat])
user_cat_vals[ucat] = res
mi.user_categories = user_cat_vals
return mi
# }}}
# Cache Layer API {{{
@api
def init(self):
'''
Initialize this cache with data from the backend.
'''
with self.write_lock:
self.backend.read_tables()
for field, table in self.backend.tables.iteritems():
self.fields[field] = create_field(field, table)
if table.metadata['datatype'] == 'composite':
self.composites.add(field)
self.fields['ondevice'] = create_field('ondevice', None)
@read_api
def field_for(self, name, book_id, default_value=None):
'''
Return the value of the field ``name`` for the book identified by
``book_id``. If no such book exists or it has no defined value for the
field ``name`` or no such field exists, then ``default_value`` is returned.
The returned value for is_multiple fields are always tuples.
'''
if self.composites and name in self.composites:
return self.composite_for(name, book_id,
default_value=default_value)
try:
return self.fields[name].for_book(book_id, default_value=default_value)
except (KeyError, IndexError):
return default_value
@read_api
def composite_for(self, name, book_id, mi=None, default_value=''):
try:
f = self.fields[name]
except KeyError:
return default_value
if mi is None:
return f.get_value_with_cache(book_id, partial(self._get_metadata,
get_user_categories=False))
else:
return f.render_composite(book_id, mi)
@read_api
def field_ids_for(self, name, book_id):
'''
Return the ids (as a tuple) for the values that the field ``name`` has on the book
identified by ``book_id``. If there are no values, or no such book, or
no such field, an empty tuple is returned.
'''
try:
return self.fields[name].ids_for_book(book_id)
except (KeyError, IndexError):
return ()
@read_api
def books_for_field(self, name, item_id):
'''
Return all the books associated with the item identified by
``item_id``, where the item belongs to the field ``name``.
Returned value is a tuple of book ids, or the empty tuple if the item
or the field does not exist.
'''
try:
return self.fields[name].books_for(item_id)
except (KeyError, IndexError):
return ()
@read_api
def all_book_ids(self):
'''
Frozen set of all known book ids.
'''
return frozenset(self.fields['uuid'].iter_book_ids())
@read_api
def all_field_ids(self, name):
'''
Frozen set of ids for all values in the field ``name``.
'''
return frozenset(iter(self.fields[name]))
@read_api
def author_data(self, author_id):
'''
Return author data as a dictionary with keys: name, sort, link
If no author with the specified id is found an empty dictionary is
returned.
'''
try:
return self.fields['authors'].author_data(author_id)
except (KeyError, IndexError):
return {}
@read_api
def format_metadata(self, book_id, fmt, allow_cache=True):
if not fmt:
return {}
fmt = fmt.upper()
if allow_cache:
x = self.format_metadata_cache[book_id].get(fmt, None)
if x is not None:
return x
try:
name = self.fields['formats'].format_fname(book_id, fmt)
path = self._field_for('path', book_id).replace('/', os.sep)
except:
return {}
ans = {}
if path and name:
ans = self.backend.format_metadata(book_id, fmt, name, path)
self.format_metadata_cache[book_id][fmt] = ans
return ans
@api
def get_metadata(self, book_id,
get_cover=False, get_user_categories=True, cover_as_data=False):
'''
Return metadata for the book identified by book_id as a :class:`Metadata` object.
Note that the list of formats is not verified. If get_cover is True,
the cover is returned, either a path to temp file as mi.cover or if
cover_as_data is True then as mi.cover_data.
'''
with self.read_lock:
mi = self._get_metadata(book_id, get_user_categories=get_user_categories)
if get_cover:
if cover_as_data:
cdata = self.cover(book_id)
if cdata:
mi.cover_data = ('jpeg', cdata)
else:
mi.cover = self.cover(book_id, as_path=True)
return mi
@api
def cover(self, book_id,
as_file=False, as_image=False, as_path=False):
'''
Return the cover image or None. By default, returns the cover as a
bytestring.
WARNING: Using as_path will copy the cover to a temp file and return
the path to the temp file. You should delete the temp file when you are
done with it.
:param as_file: If True return the image as an open file object (a SpooledTemporaryFile)
:param as_image: If True return the image as a QImage object
:param as_path: If True return the image as a path pointing to a
temporary file
'''
with self.read_lock:
try:
path = self._field_for('path', book_id).replace('/', os.sep)
except:
return None
with self.record_lock.lock(book_id):
return self.backend.cover(path, as_file=as_file, as_image=as_image,
as_path=as_path)
@read_api
def multisort(self, fields):
all_book_ids = frozenset(self._all_book_ids())
get_metadata = partial(self._get_metadata, get_user_categories=False)
sort_keys = tuple(self.fields[field[0]].sort_keys_for_books(get_metadata,
all_book_ids) for field in fields)
if len(sort_keys) == 1:
sk = sort_keys[0]
return sorted(all_book_ids, key=lambda i:sk[i], reverse=not
fields[1])
else:
return sorted(all_book_ids, key=partial(SortKey, fields, sort_keys))
# }}}
class SortKey(object):
def __init__(self, fields, sort_keys, book_id):
self.orders = tuple(1 if f[1] else -1 for f in fields)
self.sort_key = tuple(sk[book_id] for sk in sort_keys)
def __cmp__(self, other):
for i, order in enumerate(self.orders):
ans = cmp(self.sort_key[i], other.sort_key[i])
if ans != 0:
return ans * order
return 0
# Testing {{{
def test(library_path):
from calibre.db.backend import DB
backend = DB(library_path)
cache = Cache(backend)
cache.init()
print ('All book ids:', cache.all_book_ids())
if __name__ == '__main__':
from calibre.utils.config import prefs
test(prefs['library_path'])
# }}}

257
src/calibre/db/fields.py Normal file
View File

@ -0,0 +1,257 @@
#!/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)
from future_builtins import map
__license__ = 'GPL v3'
__copyright__ = '2011, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
from threading import Lock
from calibre.db.tables import ONE_ONE, MANY_ONE, MANY_MANY
from calibre.utils.icu import sort_key
class Field(object):
def __init__(self, name, table):
self.name, self.table = name, table
self.has_text_data = self.metadata['datatype'] in ('text', 'comments',
'series', 'enumeration')
self.table_type = self.table.table_type
dt = self.metadata['datatype']
self._sort_key = (sort_key if dt == 'text' else lambda x: x)
@property
def metadata(self):
return self.table.metadata
def for_book(self, book_id, default_value=None):
'''
Return the value of this field for the book identified by book_id.
When no value is found, returns ``default_value``.
'''
raise NotImplementedError()
def ids_for_book(self, book_id):
'''
Return a tuple of items ids for items associated with the book
identified by book_ids. Returns an empty tuple if no such items are
found.
'''
raise NotImplementedError()
def books_for(self, item_id):
'''
Return the ids of all books associated with the item identified by
item_id as a tuple. An empty tuple is returned if no books are found.
'''
raise NotImplementedError()
def __iter__(self):
'''
Iterate over the ids for all values in this field
'''
raise NotImplementedError()
def sort_keys_for_books(self, get_metadata, all_book_ids):
'''
Return a mapping of book_id -> sort_key. The sort key is suitable for
use in sorting the list of all books by this field, via the python cmp
method.
'''
raise NotImplementedError()
class OneToOneField(Field):
def for_book(self, book_id, default_value=None):
return self.table.book_col_map.get(book_id, default_value)
def ids_for_book(self, book_id):
return (book_id,)
def books_for(self, item_id):
return (item_id,)
def __iter__(self):
return self.table.book_col_map.iterkeys()
def iter_book_ids(self):
return self.table.book_col_map.iterkeys()
def sort_keys_for_books(self, get_metadata, all_book_ids):
return {id_ : self._sort_key(self.book_col_map.get(id_, '')) for id_ in
all_book_ids}
class CompositeField(OneToOneField):
def __init__(self, *args, **kwargs):
OneToOneField.__init__(self, *args, **kwargs)
self._render_cache = {}
self._lock = Lock()
def render_composite(self, book_id, mi):
with self._lock:
ans = self._render_cache.get(book_id, None)
if ans is None:
ans = mi.get(self.metadata['label'])
with self._lock:
self._render_cache[book_id] = ans
return ans
def clear_cache(self):
with self._lock:
self._render_cache = {}
def pop_cache(self, book_id):
with self._lock:
self._render_cache.pop(book_id, None)
def get_value_with_cache(self, book_id, get_metadata):
with self._lock:
ans = self._render_cache.get(book_id, None)
if ans is None:
mi = get_metadata(book_id)
ans = mi.get(self.metadata['label'])
return ans
def sort_keys_for_books(self, get_metadata, all_book_ids):
return {id_ : sort_key(self.get_value_with_cache(id_, get_metadata)) for id_ in
all_book_ids}
class OnDeviceField(OneToOneField):
def __init__(self, name, table):
self.name = name
self.book_on_device_func = None
def book_on_device(self, book_id):
if callable(self.book_on_device_func):
return self.book_on_device_func(book_id)
return None
def set_book_on_device_func(self, func):
self.book_on_device_func = func
def for_book(self, book_id, default_value=None):
loc = []
count = 0
on = self.book_on_device(book_id)
if on is not None:
m, a, b, count = on[:4]
if m is not None:
loc.append(_('Main'))
if a is not None:
loc.append(_('Card A'))
if b is not None:
loc.append(_('Card B'))
return ', '.join(loc) + ((' (%s books)'%count) if count > 1 else '')
def __iter__(self):
return iter(())
def iter_book_ids(self):
return iter(())
def sort_keys_for_books(self, get_metadata, all_book_ids):
return {id_ : self.for_book(id_) for id_ in
all_book_ids}
class ManyToOneField(Field):
def for_book(self, book_id, default_value=None):
ids = self.table.book_col_map.get(book_id, None)
if ids is not None:
ans = self.id_map[ids]
else:
ans = default_value
return ans
def ids_for_book(self, book_id):
id_ = self.table.book_col_map.get(book_id, None)
if id_ is None:
return ()
return (id_,)
def books_for(self, item_id):
return self.table.col_book_map.get(item_id, ())
def __iter__(self):
return self.table.id_map.iterkeys()
def sort_keys_for_books(self, get_metadata, all_book_ids):
keys = {id_ : self._sort_key(self.id_map.get(id_, '')) for id_ in
all_book_ids}
return {id_ : keys.get(
self.book_col_map.get(id_, None), '') for id_ in all_book_ids}
class ManyToManyField(Field):
def __init__(self, *args, **kwargs):
Field.__init__(self, *args, **kwargs)
self.alphabetical_sort = self.name != 'authors'
def for_book(self, book_id, default_value=None):
ids = self.table.book_col_map.get(book_id, ())
if ids:
ans = tuple(self.id_map[i] for i in ids)
else:
ans = default_value
return ans
def ids_for_book(self, book_id):
return self.table.book_col_map.get(book_id, ())
def books_for(self, item_id):
return self.table.col_book_map.get(item_id, ())
def __iter__(self):
return self.table.id_map.iterkeys()
def sort_keys_for_books(self, get_metadata, all_book_ids):
keys = {id_ : self._sort_key(self.id_map.get(id_, '')) for id_ in
all_book_ids}
def sort_key_for_book(book_id):
item_ids = self.table.book_col_map.get(book_id, ())
if self.alphabetical_sort:
item_ids = sorted(item_ids, key=keys.get)
return tuple(map(keys.get, item_ids))
return {id_ : sort_key_for_book(id_) for id_ in all_book_ids}
class AuthorsField(ManyToManyField):
def author_data(self, author_id):
return {
'name' : self.table.id_map[author_id],
'sort' : self.table.asort_map[author_id],
'link' : self.table.alink_map[author_id],
}
class FormatsField(ManyToManyField):
def format_fname(self, book_id, fmt):
return self.table.fname_map[book_id][fmt.upper()]
def create_field(name, table):
cls = {
ONE_ONE : OneToOneField,
MANY_ONE : ManyToOneField,
MANY_MANY : ManyToManyField,
}[table.table_type]
if name == 'authors':
cls = AuthorsField
elif name == 'ondevice':
cls = OnDeviceField
elif name == 'formats':
cls = FormatsField
elif table.metadata['datatype'] == 'composite':
cls = CompositeField
return cls(name, table)

View File

@ -7,7 +7,9 @@ __license__ = 'GPL v3'
__copyright__ = '2011, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
from threading import Lock, Condition, current_thread
from threading import Lock, Condition, current_thread, RLock
from functools import partial
from collections import Counter
class LockingError(RuntimeError):
pass
@ -37,7 +39,7 @@ def create_locks():
l = SHLock()
return RWLockWrapper(l), RWLockWrapper(l, is_shared=False)
class SHLock(object):
class SHLock(object): # {{{
'''
Shareable lock class. Used to implement the Multiple readers-single writer
paradigm. As best as I can tell, neither writer nor reader starvation
@ -79,6 +81,11 @@ class SHLock(object):
return self._acquire_exclusive(blocking)
assert not (self.is_shared and self.is_exclusive)
def owns_lock(self):
me = current_thread()
with self._lock:
return self._exclusive_owner is me or me in self._shared_owners
def release(self):
''' Release the lock. '''
# This decrements the appropriate lock counters, and if the lock
@ -189,6 +196,8 @@ class SHLock(object):
def _return_waiter(self, waiter):
self._free_waiters.append(waiter)
# }}}
class RWLockWrapper(object):
def __init__(self, shlock, is_shared=True):
@ -200,16 +209,124 @@ class RWLockWrapper(object):
return self
def __exit__(self, *args):
self.release()
def release(self):
self._shlock.release()
def owns_lock(self):
return self._shlock.owns_lock()
class RecordLock(object):
'''
Lock records identified by hashable ids. To use
rl = RecordLock()
with rl.lock(some_id):
# do something
This will lock the record identified by some_id exclusively. The lock is
recursive, which means that you can lock the same record multiple times in
the same thread.
This class co-operates with the SHLock class. If you try to lock a record
in a thread that already holds the SHLock, a LockingError is raised. This
is to prevent the possibility of a cross-lock deadlock.
A cross-lock deadlock is still possible if you first lock a record and then
acquire the SHLock, but the usage pattern for this lock makes this highly
unlikely (this lock should be acquired immediately before any file I/O on
files in the library and released immediately after).
'''
class Wrap(object):
def __init__(self, release):
self.release = release
def __enter__(self):
return self
def __exit__(self, *args, **kwargs):
self.release()
self.release = None
def __init__(self, sh_lock):
self._lock = Lock()
# This is for recycling lock objects.
self._free_locks = [RLock()]
self._records = {}
self._counter = Counter()
self.sh_lock = sh_lock
def lock(self, record_id):
if self.sh_lock.owns_lock():
raise LockingError('Current thread already holds a shared lock,'
' you cannot also ask for record lock as this could cause a'
' deadlock.')
with self._lock:
l = self._records.get(record_id, None)
if l is None:
l = self._take_lock()
self._records[record_id] = l
self._counter[record_id] += 1
l.acquire()
return RecordLock.Wrap(partial(self.release, record_id))
def release(self, record_id):
with self._lock:
l = self._records.pop(record_id, None)
if l is None:
raise LockingError('No lock acquired for record %r'%record_id)
l.release()
self._counter[record_id] -= 1
if self._counter[record_id] > 0:
self._records[record_id] = l
else:
self._return_lock(l)
def _take_lock(self):
try:
return self._free_locks.pop()
except IndexError:
return RLock()
def _return_lock(self, lock):
self._free_locks.append(lock)
# Tests {{{
if __name__ == '__main__':
import time, random, unittest
from threading import Thread
class TestSHLock(unittest.TestCase):
"""Testcases for SHLock class."""
class TestLock(unittest.TestCase):
"""Testcases for Lock classes."""
def test_owns_locks(self):
lock = SHLock()
self.assertFalse(lock.owns_lock())
lock.acquire(shared=True)
self.assertTrue(lock.owns_lock())
lock.release()
self.assertFalse(lock.owns_lock())
lock.acquire(shared=False)
self.assertTrue(lock.owns_lock())
lock.release()
self.assertFalse(lock.owns_lock())
done = []
def test():
if not lock.owns_lock():
done.append(True)
lock.acquire()
t = Thread(target=test)
t.daemon = True
t.start()
t.join(1)
self.assertEqual(len(done), 1)
lock.release()
def test_multithread_deadlock(self):
lock = SHLock()
@ -345,8 +462,38 @@ if __name__ == '__main__':
self.assertFalse(lock.is_shared)
self.assertFalse(lock.is_exclusive)
def test_record_lock(self):
shlock = SHLock()
lock = RecordLock(shlock)
suite = unittest.TestLoader().loadTestsFromTestCase(TestSHLock)
shlock.acquire()
self.assertRaises(LockingError, lock.lock, 1)
shlock.release()
with lock.lock(1):
with lock.lock(1):
pass
def dolock():
with lock.lock(1):
time.sleep(0.1)
t = Thread(target=dolock)
t.daemon = True
with lock.lock(1):
t.start()
t.join(0.2)
self.assertTrue(t.is_alive())
t.join(0.11)
self.assertFalse(t.is_alive())
t = Thread(target=dolock)
t.daemon = True
with lock.lock(2):
t.start()
t.join(0.11)
self.assertFalse(t.is_alive())
suite = unittest.TestLoader().loadTestsFromTestCase(TestLock)
unittest.TextTestRunner(verbosity=2).run(suite)
# }}}

View File

@ -17,6 +17,8 @@ from calibre.ebooks.metadata import author_to_author_sort
_c_speedup = plugins['speedup'][0]
ONE_ONE, MANY_ONE, MANY_MANY = xrange(3)
def _c_convert_timestamp(val):
if not val:
return None
@ -57,6 +59,8 @@ class OneToOneTable(Table):
timestamp, size, etc.
'''
table_type = ONE_ONE
def read(self, db):
self.book_col_map = {}
idcol = 'id' if self.metadata['table'] == 'books' else 'book'
@ -73,6 +77,17 @@ class SizeTable(OneToOneTable):
'WHERE data.book=books.id) FROM books'):
self.book_col_map[row[0]] = self.unserialize(row[1])
class CompositeTable(OneToOneTable):
def read(self, db):
self.book_col_map = {}
d = self.metadata['display']
self.composite_template = ['composite_template']
self.contains_html = d['contains_html']
self.make_category = d['make_category']
self.composite_sort = d['composite_sort']
self.use_decorations = d['use_decorations']
class ManyToOneTable(Table):
'''
@ -82,9 +97,10 @@ class ManyToOneTable(Table):
Each book however has only one value for data of this type.
'''
table_type = MANY_ONE
def read(self, db):
self.id_map = {}
self.extra_map = {}
self.col_book_map = {}
self.book_col_map = {}
self.read_id_maps(db)
@ -105,6 +121,9 @@ class ManyToOneTable(Table):
self.col_book_map[row[1]].append(row[0])
self.book_col_map[row[0]] = row[1]
for key in tuple(self.col_book_map.iterkeys()):
self.col_book_map[key] = tuple(self.col_book_map[key])
class ManyToManyTable(ManyToOneTable):
'''
@ -113,6 +132,8 @@ class ManyToManyTable(ManyToOneTable):
book. For example: tags or authors.
'''
table_type = MANY_MANY
def read_maps(self, db):
for row in db.conn.execute(
'SELECT book, {0} FROM {1}'.format(
@ -124,14 +145,21 @@ class ManyToManyTable(ManyToOneTable):
self.book_col_map[row[0]] = []
self.book_col_map[row[0]].append(row[1])
for key in tuple(self.col_book_map.iterkeys()):
self.col_book_map[key] = tuple(self.col_book_map[key])
for key in tuple(self.book_col_map.iterkeys()):
self.book_col_map[key] = tuple(self.book_col_map[key])
class AuthorsTable(ManyToManyTable):
def read_id_maps(self, db):
self.alink_map = {}
self.asort_map = {}
for row in db.conn.execute(
'SELECT id, name, sort, link FROM authors'):
self.id_map[row[0]] = row[1]
self.extra_map[row[0]] = (row[2] if row[2] else
self.asort_map[row[0]] = (row[2] if row[2] else
author_to_author_sort(row[1]))
self.alink_map[row[0]] = row[3]
@ -141,14 +169,25 @@ class FormatsTable(ManyToManyTable):
pass
def read_maps(self, db):
self.fname_map = {}
for row in db.conn.execute('SELECT book, format, name FROM data'):
if row[1] is not None:
if row[1] not in self.col_book_map:
self.col_book_map[row[1]] = []
self.col_book_map[row[1]].append(row[0])
fmt = row[1].upper()
if fmt not in self.col_book_map:
self.col_book_map[fmt] = []
self.col_book_map[fmt].append(row[0])
if row[0] not in self.book_col_map:
self.book_col_map[row[0]] = []
self.book_col_map[row[0]].append((row[1], row[2]))
self.book_col_map[row[0]].append(fmt)
if row[0] not in self.fname_map:
self.fname_map[row[0]] = {}
self.fname_map[row[0]][fmt] = row[2]
for key in tuple(self.col_book_map.iterkeys()):
self.col_book_map[key] = tuple(self.col_book_map[key])
for key in tuple(self.book_col_map.iterkeys()):
self.book_col_map[key] = tuple(self.book_col_map[key])
class IdentifiersTable(ManyToManyTable):
@ -162,6 +201,9 @@ class IdentifiersTable(ManyToManyTable):
self.col_book_map[row[1]] = []
self.col_book_map[row[1]].append(row[0])
if row[0] not in self.book_col_map:
self.book_col_map[row[0]] = []
self.book_col_map[row[0]].append((row[1], row[2]))
self.book_col_map[row[0]] = {}
self.book_col_map[row[0]][row[1]] = row[2]
for key in tuple(self.col_book_map.iterkeys()):
self.col_book_map[key] = tuple(self.col_book_map[key])

109
src/calibre/db/view.py Normal file
View File

@ -0,0 +1,109 @@
#!/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 functools import partial
def sanitize_sort_field_name(field_metadata, field):
field = field_metadata.search_term_to_field_key(field.lower().strip())
# translate some fields to their hidden equivalent
field = {'title': 'sort', 'authors':'author_sort'}.get(field, field)
return field
class View(object):
def __init__(self, cache):
self.cache = cache
self.marked_ids = {}
self._field_getters = {}
for col, idx in cache.backend.FIELD_MAP.iteritems():
if isinstance(col, int):
label = self.cache.backend.custom_column_num_map[col]['label']
label = (self.cache.backend.field_metadata.custom_field_prefix
+ label)
self._field_getters[idx] = partial(self.get, label)
else:
try:
self._field_getters[idx] = {
'id' : self._get_id,
'au_map' : self.get_author_data,
'ondevice': self.get_ondevice,
'marked' : self.get_marked,
}[col]
except KeyError:
self._field_getters[idx] = partial(self.get, col)
self._map = list(self.cache.all_book_ids())
self._map_filtered = list(self._map)
@property
def field_metadata(self):
return self.cache.field_metadata
def _get_id(self, idx, index_is_id=True):
ans = idx if index_is_id else self.index_to_id(idx)
return ans
def get_field_map_field(self, row, col, index_is_id=True):
'''
Supports the legacy FIELD_MAP interface for getting metadata. Do not use
in new code.
'''
getter = self._field_getters[col]
return getter(row, index_is_id=index_is_id)
def index_to_id(self, idx):
return self._map_filtered[idx]
def get(self, field, idx, index_is_id=True, default_value=None):
id_ = idx if index_is_id else self.index_to_id(idx)
return self.cache.field_for(field, id_)
def get_ondevice(self, idx, index_is_id=True, default_value=''):
id_ = idx if index_is_id else self.index_to_id(idx)
self.cache.field_for('ondevice', id_, default_value=default_value)
def get_marked(self, idx, index_is_id=True, default_value=None):
id_ = idx if index_is_id else self.index_to_id(idx)
return self.marked_ids.get(id_, default_value)
def get_author_data(self, idx, index_is_id=True, default_value=()):
'''
Return author data for all authors of the book identified by idx as a
tuple of dictionaries. The dictionaries should never be empty, unless
there is a bug somewhere. The list could be empty if idx point to an
non existent book, or book with no authors (though again a book with no
authors should never happen).
Each dictionary has the keys: name, sort, link. Link can be an empty
string.
default_value is ignored, this method always returns a tuple
'''
id_ = idx if index_is_id else self.index_to_id(idx)
with self.cache.read_lock:
ids = self.cache._field_ids_for('authors', id_)
ans = []
for id_ in ids:
ans.append(self.cache._author_data(id_))
return tuple(ans)
def multisort(self, fields=[], subsort=False):
fields = [(sanitize_sort_field_name(self.field_metadata, x), bool(y)) for x, y in fields]
keys = self.field_metadata.sortable_field_keys()
fields = [x for x in fields if x[0] in keys]
if subsort and 'sort' not in [x[0] for x in fields]:
fields += [('sort', True)]
if not fields:
fields = [('timestamp', False)]
sorted_book_ids = self.cache.multisort(fields)
sorted_book_ids
# TODO: change maps

View File

@ -39,7 +39,7 @@ class ANDROID(USBMS):
0x22b8 : { 0x41d9 : [0x216], 0x2d61 : [0x100], 0x2d67 : [0x100],
0x41db : [0x216], 0x4285 : [0x216], 0x42a3 : [0x216],
0x4286 : [0x216], 0x42b3 : [0x216], 0x42b4 : [0x216],
0x7086 : [0x0226], 0x70a8: [0x9999],
0x7086 : [0x0226], 0x70a8: [0x9999], 0x42c4 : [0x216],
},
# Sony Ericsson
@ -60,6 +60,7 @@ class ANDROID(USBMS):
0x685e : [0x0400],
0x6860 : [0x0400],
0x6877 : [0x0400],
0x689e : [0x0400],
},
# Viewsonic
@ -124,7 +125,8 @@ class ANDROID(USBMS):
'IDEOS_TABLET', 'MYTOUCH_4G', 'UMS_COMPOSITE', 'SCH-I800_CARD',
'7', 'A956', 'A955', 'A43', 'ANDROID_PLATFORM', 'TEGRA_2',
'MB860', 'MULTI-CARD', 'MID7015A', 'INCREDIBLE', 'A7EB', 'STREAK',
'MB525', 'ANDROID2.3', 'SGH-I997']
'MB525', 'ANDROID2.3', 'SGH-I997', 'GT-I5800_CARD', 'MB612',
'GT-S5830_CARD']
WINDOWS_CARD_A_MEM = ['ANDROID_PHONE', 'GT-I9000_CARD', 'SGH-I897',
'FILE-STOR_GADGET', 'SGH-T959', 'SAMSUNG_ANDROID', 'GT-P1000_CARD',
'A70S', 'A101IT', '7', 'INCREDIBLE', 'A7EB', 'SGH-T849_CARD',

View File

@ -7,6 +7,7 @@ __docformat__ = 'restructuredtext en'
import os
import sqlite3 as sqlite
from contextlib import closing
from calibre.devices.usbms.books import BookList
from calibre.devices.kobo.books import Book
@ -22,7 +23,7 @@ class KOBO(USBMS):
gui_name = 'Kobo Reader'
description = _('Communicate with the Kobo Reader')
author = 'Timothy Legge'
version = (1, 0, 9)
version = (1, 0, 10)
dbversion = 0
fwversion = 0
@ -48,12 +49,16 @@ class KOBO(USBMS):
VIRTUAL_BOOK_EXTENSIONS = frozenset(['kobo'])
EXTRA_CUSTOMIZATION_MESSAGE = _('The Kobo supports only one collection '
'currently: the \"Im_Reading\" list. Create a tag called \"Im_Reading\" ')+\
'for automatic management'
EXTRA_CUSTOMIZATION_MESSAGE = [
_('The Kobo supports several collections including ')+\
'Read, Closed, Im_Reading ' +\
_('Create tags for automatic management'),
]
EXTRA_CUSTOMIZATION_DEFAULT = ', '.join(['tags'])
OPT_COLLECTIONS = 0
def initialize(self):
USBMS.initialize(self)
self.book_class = Book
@ -188,77 +193,78 @@ class KOBO(USBMS):
traceback.print_exc()
return changed
connection = sqlite.connect(self.normalize_path(self._main_prefix + '.kobo/KoboReader.sqlite'))
with closing(sqlite.connect(
self.normalize_path(self._main_prefix +
'.kobo/KoboReader.sqlite'))) as connection:
# return bytestrings if the content cannot the decoded as unicode
connection.text_factory = lambda x: unicode(x, "utf-8", "ignore")
# return bytestrings if the content cannot the decoded as unicode
connection.text_factory = lambda x: unicode(x, "utf-8", "ignore")
cursor = connection.cursor()
cursor = connection.cursor()
#query = 'select count(distinct volumeId) from volume_shortcovers'
#cursor.execute(query)
#for row in (cursor):
# numrows = row[0]
#cursor.close()
#query = 'select count(distinct volumeId) from volume_shortcovers'
#cursor.execute(query)
#for row in (cursor):
# numrows = row[0]
#cursor.close()
# Determine the database version
# 4 - Bluetooth Kobo Rev 2 (1.4)
# 8 - WIFI KOBO Rev 1
cursor.execute('select version from dbversion')
result = cursor.fetchone()
self.dbversion = result[0]
# Determine the database version
# 4 - Bluetooth Kobo Rev 2 (1.4)
# 8 - WIFI KOBO Rev 1
cursor.execute('select version from dbversion')
result = cursor.fetchone()
self.dbversion = result[0]
debug_print("Database Version: ", self.dbversion)
if self.dbversion >= 16:
query= 'select Title, Attribution, DateCreated, ContentID, MimeType, ContentType, ' \
'ImageID, ReadStatus, ___ExpirationStatus, FavouritesIndex, Accessibility from content where ' \
'BookID is Null and ( ___ExpirationStatus <> "3" or ___ExpirationStatus is Null)'
elif self.dbversion < 16 and self.dbversion >= 14:
query= 'select Title, Attribution, DateCreated, ContentID, MimeType, ContentType, ' \
'ImageID, ReadStatus, ___ExpirationStatus, FavouritesIndex, "-1" as Accessibility from content where ' \
'BookID is Null and ( ___ExpirationStatus <> "3" or ___ExpirationStatus is Null)'
elif self.dbversion < 14 and self.dbversion >= 8:
query= 'select Title, Attribution, DateCreated, ContentID, MimeType, ContentType, ' \
'ImageID, ReadStatus, ___ExpirationStatus, "-1" as FavouritesIndex, "-1" as Accessibility from content where ' \
'BookID is Null and ( ___ExpirationStatus <> "3" or ___ExpirationStatus is Null)'
else:
query= 'select Title, Attribution, DateCreated, ContentID, MimeType, ContentType, ' \
'ImageID, ReadStatus, "-1" as ___ExpirationStatus, "-1" as FavouritesIndex, "-1" as Accessibility from content where BookID is Null'
debug_print("Database Version: ", self.dbversion)
if self.dbversion >= 16:
query= 'select Title, Attribution, DateCreated, ContentID, MimeType, ContentType, ' \
'ImageID, ReadStatus, ___ExpirationStatus, FavouritesIndex, Accessibility from content where ' \
'BookID is Null and ( ___ExpirationStatus <> "3" or ___ExpirationStatus is Null)'
elif self.dbversion < 16 and self.dbversion >= 14:
query= 'select Title, Attribution, DateCreated, ContentID, MimeType, ContentType, ' \
'ImageID, ReadStatus, ___ExpirationStatus, FavouritesIndex, "-1" as Accessibility from content where ' \
'BookID is Null and ( ___ExpirationStatus <> "3" or ___ExpirationStatus is Null)'
elif self.dbversion < 14 and self.dbversion >= 8:
query= 'select Title, Attribution, DateCreated, ContentID, MimeType, ContentType, ' \
'ImageID, ReadStatus, ___ExpirationStatus, "-1" as FavouritesIndex, "-1" as Accessibility from content where ' \
'BookID is Null and ( ___ExpirationStatus <> "3" or ___ExpirationStatus is Null)'
else:
query= 'select Title, Attribution, DateCreated, ContentID, MimeType, ContentType, ' \
'ImageID, ReadStatus, "-1" as ___ExpirationStatus, "-1" as FavouritesIndex, "-1" as Accessibility from content where BookID is Null'
try:
cursor.execute (query)
except Exception as e:
err = str(e)
if not ('___ExpirationStatus' in err or 'FavouritesIndex' in err or
'Accessibility' in err):
raise
query= ('select Title, Attribution, DateCreated, ContentID, MimeType, ContentType, '
'ImageID, ReadStatus, "-1" as ___ExpirationStatus, "-1" as '
'FavouritesIndex, "-1" as Accessibility from content where '
'BookID is Null')
cursor.execute(query)
try:
cursor.execute (query)
except Exception as e:
err = str(e)
if not ('___ExpirationStatus' in err or 'FavouritesIndex' in err or
'Accessibility' in err):
raise
query= ('select Title, Attribution, DateCreated, ContentID, MimeType, ContentType, '
'ImageID, ReadStatus, "-1" as ___ExpirationStatus, "-1" as '
'FavouritesIndex, "-1" as Accessibility from content where '
'BookID is Null')
cursor.execute(query)
changed = False
for i, row in enumerate(cursor):
# self.report_progress((i+1) / float(numrows), _('Getting list of books on device...'))
if row[3].startswith("file:///usr/local/Kobo/help/"):
# These are internal to the Kobo device and do not exist
continue
path = self.path_from_contentid(row[3], row[5], row[4], oncard)
mime = mime_type_ext(path_to_ext(path)) if path.find('kepub') == -1 else 'application/epub+zip'
# debug_print("mime:", mime)
changed = False
for i, row in enumerate(cursor):
# self.report_progress((i+1) / float(numrows), _('Getting list of books on device...'))
if row[3].startswith("file:///usr/local/Kobo/help/"):
# These are internal to the Kobo device and do not exist
continue
path = self.path_from_contentid(row[3], row[5], row[4], oncard)
mime = mime_type_ext(path_to_ext(path)) if path.find('kepub') == -1 else 'application/epub+zip'
# debug_print("mime:", mime)
if oncard != 'carda' and oncard != 'cardb' and not row[3].startswith("file:///mnt/sd/"):
changed = update_booklist(self._main_prefix, path, row[0], row[1], mime, row[2], row[5], row[6], row[7], row[4], row[8], row[9], row[10])
# print "shortbook: " + path
elif oncard == 'carda' and row[3].startswith("file:///mnt/sd/"):
changed = update_booklist(self._card_a_prefix, path, row[0], row[1], mime, row[2], row[5], row[6], row[7], row[4], row[8], row[9], row[10])
if oncard != 'carda' and oncard != 'cardb' and not row[3].startswith("file:///mnt/sd/"):
changed = update_booklist(self._main_prefix, path, row[0], row[1], mime, row[2], row[5], row[6], row[7], row[4], row[8], row[9], row[10])
# print "shortbook: " + path
elif oncard == 'carda' and row[3].startswith("file:///mnt/sd/"):
changed = update_booklist(self._card_a_prefix, path, row[0], row[1], mime, row[2], row[5], row[6], row[7], row[4], row[8], row[9], row[10])
if changed:
need_sync = True
if changed:
need_sync = True
cursor.close()
connection.close()
cursor.close()
# Remove books that are no longer in the filesystem. Cache contains
# indices into the booklist if book not in filesystem, None otherwise
@ -288,56 +294,56 @@ class KOBO(USBMS):
# 2) content
debug_print('delete_via_sql: ContentID: ', ContentID, 'ContentType: ', ContentType)
connection = sqlite.connect(self.normalize_path(self._main_prefix + '.kobo/KoboReader.sqlite'))
with closing(sqlite.connect(self.normalize_path(self._main_prefix +
'.kobo/KoboReader.sqlite'))) as connection:
# return bytestrings if the content cannot the decoded as unicode
connection.text_factory = lambda x: unicode(x, "utf-8", "ignore")
# return bytestrings if the content cannot the decoded as unicode
connection.text_factory = lambda x: unicode(x, "utf-8", "ignore")
cursor = connection.cursor()
t = (ContentID,)
cursor.execute('select ImageID from content where ContentID = ?', t)
cursor = connection.cursor()
t = (ContentID,)
cursor.execute('select ImageID from content where ContentID = ?', t)
ImageID = None
for row in cursor:
# First get the ImageID to delete the images
ImageID = row[0]
cursor.close()
ImageID = None
for row in cursor:
# First get the ImageID to delete the images
ImageID = row[0]
cursor.close()
cursor = connection.cursor()
if ContentType == 6 and self.dbversion < 8:
# Delete the shortcover_pages first
cursor.execute('delete from shortcover_page where shortcoverid in (select ContentID from content where BookID = ?)', t)
cursor = connection.cursor()
if ContentType == 6 and self.dbversion < 8:
# Delete the shortcover_pages first
cursor.execute('delete from shortcover_page where shortcoverid in (select ContentID from content where BookID = ?)', t)
#Delete the volume_shortcovers second
cursor.execute('delete from volume_shortcovers where volumeid = ?', t)
#Delete the volume_shortcovers second
cursor.execute('delete from volume_shortcovers where volumeid = ?', t)
# Delete the rows from content_keys
if self.dbversion >= 8:
cursor.execute('delete from content_keys where volumeid = ?', t)
# Delete the rows from content_keys
if self.dbversion >= 8:
cursor.execute('delete from content_keys where volumeid = ?', t)
# Delete the chapters associated with the book next
t = (ContentID,)
# Kobo does not delete the Book row (ie the row where the BookID is Null)
# The next server sync should remove the row
cursor.execute('delete from content where BookID = ?', t)
try:
cursor.execute('update content set ReadStatus=0, FirstTimeReading = \'true\', ___PercentRead=0, ___ExpirationStatus=3 ' \
'where BookID is Null and ContentID =?',t)
except Exception as e:
if 'no such column' not in str(e):
raise
cursor.execute('update content set ReadStatus=0, FirstTimeReading = \'true\', ___PercentRead=0 ' \
'where BookID is Null and ContentID =?',t)
# Delete the chapters associated with the book next
t = (ContentID,)
# Kobo does not delete the Book row (ie the row where the BookID is Null)
# The next server sync should remove the row
cursor.execute('delete from content where BookID = ?', t)
try:
cursor.execute('update content set ReadStatus=0, FirstTimeReading = \'true\', ___PercentRead=0, ___ExpirationStatus=3 ' \
'where BookID is Null and ContentID =?',t)
except Exception as e:
if 'no such column' not in str(e):
raise
cursor.execute('update content set ReadStatus=0, FirstTimeReading = \'true\', ___PercentRead=0 ' \
'where BookID is Null and ContentID =?',t)
connection.commit()
connection.commit()
cursor.close()
if ImageID == None:
print "Error condition ImageID was not found"
print "You likely tried to delete a book that the kobo has not yet added to the database"
cursor.close()
if ImageID == None:
print "Error condition ImageID was not found"
print "You likely tried to delete a book that the kobo has not yet added to the database"
connection.close()
# If all this succeeds we need to delete the images files via the ImageID
return ImageID
@ -664,50 +670,49 @@ class KOBO(USBMS):
# Needs to be outside books collection as in the case of removing
# the last book from the collection the list of books is empty
# and the removal of the last book would not occur
connection = sqlite.connect(self.normalize_path(self._main_prefix + '.kobo/KoboReader.sqlite'))
with closing(sqlite.connect(self.normalize_path(self._main_prefix +
'.kobo/KoboReader.sqlite'))) as connection:
# return bytestrings if the content cannot the decoded as unicode
connection.text_factory = lambda x: unicode(x, "utf-8", "ignore")
# return bytestrings if the content cannot the decoded as unicode
connection.text_factory = lambda x: unicode(x, "utf-8", "ignore")
if collections:
if collections:
# Need to reset the collections outside the particular loops
# otherwise the last item will not be removed
self.reset_readstatus(connection, oncard)
if self.dbversion >= 14:
self.reset_favouritesindex(connection, oncard)
# Need to reset the collections outside the particular loops
# otherwise the last item will not be removed
self.reset_readstatus(connection, oncard)
if self.dbversion >= 14:
self.reset_favouritesindex(connection, oncard)
# Process any collections that exist
for category, books in collections.items():
debug_print("Category: ", category, " id = ", readstatuslist.get(category))
for book in books:
debug_print(' Title:', book.title, 'category: ', category)
if category not in book.device_collections:
book.device_collections.append(category)
# Process any collections that exist
for category, books in collections.items():
debug_print("Category: ", category, " id = ", readstatuslist.get(category))
for book in books:
debug_print(' Title:', book.title, 'category: ', category)
if category not in book.device_collections:
book.device_collections.append(category)
extension = os.path.splitext(book.path)[1]
ContentType = self.get_content_type_from_extension(extension) if extension != '' else self.get_content_type_from_path(book.path)
extension = os.path.splitext(book.path)[1]
ContentType = self.get_content_type_from_extension(extension) if extension != '' else self.get_content_type_from_path(book.path)
ContentID = self.contentid_from_path(book.path, ContentType)
ContentID = self.contentid_from_path(book.path, ContentType)
if category in readstatuslist.keys():
# Manage ReadStatus
self.set_readstatus(connection, ContentID, readstatuslist.get(category))
if category == 'Shortlist' and self.dbversion >= 14:
# Manage FavouritesIndex/Shortlist
self.set_favouritesindex(connection, ContentID)
if category in accessibilitylist.keys():
# Do not manage the Accessibility List
pass
else: # No collections
# Since no collections exist the ReadStatus needs to be reset to 0 (Unread)
debug_print("No Collections - reseting ReadStatus")
self.reset_readstatus(connection, oncard)
if self.dbversion >= 14:
debug_print("No Collections - reseting FavouritesIndex")
self.reset_favouritesindex(connection, oncard)
connection.close()
if category in readstatuslist.keys():
# Manage ReadStatus
self.set_readstatus(connection, ContentID, readstatuslist.get(category))
elif category == 'Shortlist' and self.dbversion >= 14:
# Manage FavouritesIndex/Shortlist
self.set_favouritesindex(connection, ContentID)
elif category in accessibilitylist.keys():
# Do not manage the Accessibility List
pass
else: # No collections
# Since no collections exist the ReadStatus needs to be reset to 0 (Unread)
debug_print("No Collections - reseting ReadStatus")
self.reset_readstatus(connection, oncard)
if self.dbversion >= 14:
debug_print("No Collections - reseting FavouritesIndex")
self.reset_favouritesindex(connection, oncard)
# debug_print('Finished update_device_database_collections', collections_attributes)
@ -723,7 +728,7 @@ class KOBO(USBMS):
opts = self.settings()
if opts.extra_customization:
collections = [x.lower().strip() for x in
opts.extra_customization.split(',')]
opts.extra_customization[self.OPT_COLLECTIONS].split(',')]
else:
collections = []

View File

@ -1077,8 +1077,13 @@ class Device(DeviceConfig, DevicePlugin):
settings = self.settings()
template = self.save_template()
if mdata.tags and _('News') in mdata.tags:
today = time.localtime()
template = "{title}_%d-%d-%d" % (today[0], today[1], today[2])
try:
p = mdata.pubdate
date = (p.year, p.month, p.day)
except:
today = time.localtime()
date = (today[0], today[1], today[2])
template = "{title}_%d-%d-%d" % date
use_subdirs = self.SUPPORTS_SUB_DIRS and settings.use_subdirs
fname = sanitize(fname)

View File

@ -94,11 +94,29 @@ class USBMS(CLI, Device):
self.report_progress(1.0, _('Get device information...'))
self.driveinfo = {}
if self._main_prefix is not None:
self.driveinfo['main'] = self._update_driveinfo_file(self._main_prefix, 'main')
if self._card_a_prefix is not None:
self.driveinfo['A'] = self._update_driveinfo_file(self._card_a_prefix, 'A')
if self._card_b_prefix is not None:
self.driveinfo['B'] = self._update_driveinfo_file(self._card_b_prefix, 'B')
try:
self.driveinfo['main'] = self._update_driveinfo_file(self._main_prefix, 'main')
except (IOError, OSError) as e:
raise IOError(_('Failed to access files in the main memory of'
' your device. You should contact the device'
' manufacturer for support. Common fixes are:'
' try a different USB cable/USB port on your computer.'
' If you device has a "Reset to factory defaults" type'
' of setting somewhere, use it. Underlying error: %s')
% e)
try:
if self._card_a_prefix is not None:
self.driveinfo['A'] = self._update_driveinfo_file(self._card_a_prefix, 'A')
if self._card_b_prefix is not None:
self.driveinfo['B'] = self._update_driveinfo_file(self._card_b_prefix, 'B')
except (IOError, OSError) as e:
raise IOError(_('Failed to access files on the SD card in your'
' device. This can happen for many reasons. The SD card may be'
' corrupted, it may be too large for your device, it may be'
' write-protected, etc. Try a different SD card, or reformat'
' your SD card using the FAT32 filesystem. Also make sure'
' there are not too many files in the root of your SD card.'
' Underlying error: %s') % e)
return (self.get_gui_name(), '', '', '', self.driveinfo)
def set_driveinfo_name(self, location_code, name):

View File

@ -159,7 +159,7 @@ def normalize(x):
return x
def calibre_cover(title, author_string, series_string=None,
output_format='jpg', title_size=46, author_size=36):
output_format='jpg', title_size=46, author_size=36, logo_path=None):
title = normalize(title)
author_string = normalize(author_string)
series_string = normalize(series_string)
@ -167,7 +167,9 @@ def calibre_cover(title, author_string, series_string=None,
lines = [TextLine(title, title_size), TextLine(author_string, author_size)]
if series_string:
lines.append(TextLine(series_string, author_size))
return create_cover_page(lines, I('library.png'), output_format='jpg')
if logo_path is None:
logo_path = I('library.png')
return create_cover_page(lines, logo_path, output_format='jpg')
UNIT_RE = re.compile(r'^(-*[0-9]*[.]?[0-9]*)\s*(%|em|ex|en|px|mm|cm|in|pt|pc)$')

View File

@ -38,8 +38,12 @@ ENCODING_PATS = [
ENTITY_PATTERN = re.compile(r'&(\S+?);')
def strip_encoding_declarations(raw):
limit = 50*1024
for pat in ENCODING_PATS:
raw = pat.sub('', raw)
prefix = raw[:limit]
suffix = raw[limit:]
prefix = pat.sub('', prefix)
raw = prefix + suffix
return raw
def substitute_entites(raw):

View File

@ -137,7 +137,9 @@ def add_pipeline_options(parser, plumber):
'extra_css', 'smarten_punctuation',
'margin_top', 'margin_left', 'margin_right',
'margin_bottom', 'change_justification',
'insert_blank_line', 'remove_paragraph_spacing','remove_paragraph_spacing_indent_size',
'insert_blank_line', 'insert_blank_line_size',
'remove_paragraph_spacing',
'remove_paragraph_spacing_indent_size',
'asciiize',
]
),

View File

@ -366,9 +366,9 @@ OptionRecommendation(name='remove_paragraph_spacing',
OptionRecommendation(name='remove_paragraph_spacing_indent_size',
recommended_value=1.5, level=OptionRecommendation.LOW,
help=_('When calibre removes inter paragraph spacing, it automatically '
help=_('When calibre removes blank lines between paragraphs, it automatically '
'sets a paragraph indent, to ensure that paragraphs can be easily '
'distinguished. This option controls the width of that indent.')
'distinguished. This option controls the width of that indent (in em).')
),
OptionRecommendation(name='prefer_metadata_cover',
@ -384,6 +384,13 @@ OptionRecommendation(name='insert_blank_line',
)
),
OptionRecommendation(name='insert_blank_line_size',
recommended_value=0.5, level=OptionRecommendation.LOW,
help=_('Set the height of the inserted blank lines (in em).'
' The height of the lines between paragraphs will be twice the value'
' set here.')
),
OptionRecommendation(name='remove_first_image',
recommended_value=False, level=OptionRecommendation.LOW,
help=_('Remove the first image from the input ebook. Useful if the '
@ -602,7 +609,7 @@ OptionRecommendation(name='sr3_replace',
input_fmt = os.path.splitext(self.input)[1]
if not input_fmt:
raise ValueError('Input file must have an extension')
input_fmt = input_fmt[1:].lower()
input_fmt = input_fmt[1:].lower().replace('original_', '')
self.archive_input_tdir = None
if input_fmt in ARCHIVE_FMTS:
self.log('Processing archive...')
@ -1048,6 +1055,7 @@ OptionRecommendation(name='sr3_replace',
with self.output_plugin:
self.output_plugin.convert(self.oeb, self.output, self.input_plugin,
self.opts, self.log)
self.oeb.clean_temp_files()
self.ui_reporter(1.)
run_plugins_on_postprocess(self.output, self.output_fmt)

View File

@ -8,7 +8,7 @@ __docformat__ = 'restructuredtext en'
import os
from calibre import guess_type, walk
from calibre import guess_type
from calibre.customize.conversion import InputFormatPlugin
from calibre.ebooks.chardet import xml_to_unicode
from calibre.ebooks.metadata.opf2 import OPF
@ -25,16 +25,50 @@ class HTMLZInput(InputFormatPlugin):
accelerators):
self.log = log
html = u''
top_levels = []
# Extract content from zip archive.
zf = ZipFile(stream)
zf.extractall()
for x in walk('.'):
# Find the HTML file in the archive. It needs to be
# top level.
index = u''
multiple_html = False
# Get a list of all top level files in the archive.
for x in os.listdir('.'):
if os.path.isfile(x):
top_levels.append(x)
# Try to find an index. file.
for x in top_levels:
if x.lower() in ('index.html', 'index.xhtml', 'index.htm'):
index = x
break
# Look for multiple HTML files in the archive. We look at the
# top level files only as only they matter in HTMLZ.
for x in top_levels:
if os.path.splitext(x)[1].lower() in ('.html', '.xhtml', '.htm'):
with open(x, 'rb') as tf:
html = tf.read()
break
# Set index to the first HTML file found if it's not
# called index.
if not index:
index = x
else:
multiple_html = True
# Warn the user if there multiple HTML file in the archive. HTMLZ
# supports a single HTML file. A conversion with a multiple HTML file
# HTMLZ archive probably won't turn out as the user expects. With
# Multiple HTML files ZIP input should be used in place of HTMLZ.
if multiple_html:
log.warn(_('Multiple HTML files found in the archive. Only %s will be used.') % index)
if index:
with open(index, 'rb') as tf:
html = tf.read()
else:
raise Exception(_('No top level HTML file found.'))
if not html:
raise Exception(_('Top level HTML file %s is empty') % index)
# Encoding
if options.input_encoding:
@ -75,7 +109,7 @@ class HTMLZInput(InputFormatPlugin):
# Get the cover path from the OPF.
cover_path = None
opf = None
for x in walk('.'):
for x in top_levels:
if os.path.splitext(x)[1].lower() in ('.opf'):
opf = x
break

View File

@ -742,7 +742,7 @@ class Metadata(object):
ans += [('ISBN', unicode(self.isbn))]
ans += [(_('Tags'), u', '.join([unicode(t) for t in self.tags]))]
if self.series:
ans += [_('Series'), unicode(self.series) + ' #%s'%self.format_series_index()]
ans += [(_('Series'), unicode(self.series) + ' #%s'%self.format_series_index())]
ans += [(_('Language'), unicode(self.language))]
if self.timestamp is not None:
ans += [(_('Timestamp'), unicode(self.timestamp.isoformat(' ')))]

View File

@ -7,10 +7,12 @@ __license__ = 'GPL v3'
__copyright__ = '2011, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
import struct, datetime
import struct, datetime, sys, os
from calibre.utils.date import utc_tz
from calibre.ebooks.mobi.langcodes import main_language, sub_language
from calibre.ebooks.mobi.writer2.utils import decode_hex_number
# PalmDB {{{
class PalmDOCAttributes(object):
class Attr(object):
@ -94,8 +96,9 @@ class PalmDB(object):
ans.append('Number of records: %s'%self.number_of_records)
return '\n'.join(ans)
# }}}
class Record(object):
class Record(object): # {{{
def __init__(self, raw, header):
self.offset, self.flags, self.uid = header
@ -103,9 +106,11 @@ class Record(object):
@property
def header(self):
return 'Offset: %d Flags: %d UID: %d'%(self.offset, self.flags,
self.uid)
return 'Offset: %d Flags: %d UID: %d First 4 bytes: %r Size: %d'%(self.offset, self.flags,
self.uid, self.raw[:4], len(self.raw))
# }}}
# EXTH {{{
class EXTHRecord(object):
def __init__(self, type_, data):
@ -189,9 +194,9 @@ class EXTHHeader(object):
for r in self.records:
ans.append(str(r))
return '\n'.join(ans)
# }}}
class MOBIHeader(object):
class MOBIHeader(object): # {{{
def __init__(self, record0):
self.raw = record0.raw
@ -271,6 +276,7 @@ class MOBIHeader(object):
self.drm_flags = bin(struct.unpack(b'>I', self.raw[176:180])[0])
self.has_extra_data_flags = self.length >= 232 and len(self.raw) >= 232+16
self.has_fcis_flis = False
self.has_multibytes = self.has_indexing_bytes = self.has_uncrossable_breaks = False
if self.has_extra_data_flags:
self.unknown4 = self.raw[180:192]
self.first_content_record, self.last_content_record = \
@ -280,8 +286,11 @@ class MOBIHeader(object):
self.flis_count) = struct.unpack(b'>IIII',
self.raw[200:216])
self.unknown6 = self.raw[216:240]
self.extra_data_flags = bin(struct.unpack(b'>I',
self.raw[240:244])[0])
self.extra_data_flags = struct.unpack(b'>I',
self.raw[240:244])[0]
self.has_multibytes = bool(self.extra_data_flags & 0b1)
self.has_indexing_bytes = bool(self.extra_data_flags & 0b10)
self.has_uncrossable_breaks = bool(self.extra_data_flags & 0b100)
self.primary_index_record, = struct.unpack(b'>I',
self.raw[244:248])
@ -311,7 +320,8 @@ class MOBIHeader(object):
ans.append('Secondary index record: %d (null val: %d)'%(
self.secondary_index_record, 0xffffffff))
ans.append('Reserved2: %r'%self.reserved2)
ans.append('First non-book record: %d'% self.first_non_book_record)
ans.append('First non-book record (null value: %d): %d'%(0xffffffff,
self.first_non_book_record))
ans.append('Full name offset: %d'%self.fullname_offset)
ans.append('Full name length: %d bytes'%self.fullname_length)
ans.append('Langcode: %r'%self.locale_raw)
@ -341,8 +351,12 @@ class MOBIHeader(object):
ans.append('FLIS number: %d'% self.flis_number)
ans.append('FLIS count: %d'% self.flis_count)
ans.append('Unknown6: %r'% self.unknown6)
ans.append('Extra data flags: %r'%self.extra_data_flags)
ans.append('Primary index record: %d'%self.primary_index_record)
ans.append(('Extra data flags: %s (has multibyte: %s) '
'(has indexing: %s) (has uncrossable breaks: %s)')%(
bin(self.extra_data_flags), self.has_multibytes,
self.has_indexing_bytes, self.has_uncrossable_breaks ))
ans.append('Primary index record (null value: %d): %d'%(0xffffffff,
self.primary_index_record))
ans = '\n'.join(ans)
@ -355,8 +369,200 @@ class MOBIHeader(object):
ans += '\nRecord 0 length: %d'%len(self.raw)
return ans
# }}}
class MOBIFile(object):
class TagX(object): # {{{
def __init__(self, raw, control_byte_count):
self.tag = ord(raw[0])
self.num_values = ord(raw[1])
self.bmask = ord(raw[2])
self.bitmask = bin(self.bmask)
# End of file = 1 iff last entry
# When it is 1 all others are 0
self.eof = ord(raw[3])
self.is_eof = (self.eof == 1 and self.tag == 0 and self.num_values == 0
and self.bmask == 0)
def __repr__(self):
return 'TAGX(tag=%02d, num_values=%d, bitmask=%r (%d), eof=%d)' % (self.tag,
self.num_values, self.bitmask, self.bmask, self.eof)
# }}}
class IndexHeader(object): # {{{
def __init__(self, record):
self.record = record
raw = self.record.raw
if raw[:4] != b'INDX':
raise ValueError('Invalid Primary Index Record')
self.header_length, = struct.unpack('>I', raw[4:8])
self.unknown1 = raw[8:16]
self.index_type, = struct.unpack('>I', raw[16:20])
self.index_type_desc = {0: 'normal', 2:
'inflection'}.get(self.index_type, 'unknown')
self.idxt_start, = struct.unpack('>I', raw[20:24])
self.index_count, = struct.unpack('>I', raw[24:28])
self.index_encoding_num, = struct.unpack('>I', raw[28:32])
self.index_encoding = {65001: 'utf-8', 1252:
'cp1252'}.get(self.index_encoding_num, 'unknown')
if self.index_encoding == 'unknown':
raise ValueError(
'Unknown index encoding: %d'%self.index_encoding_num)
self.locale_raw, = struct.unpack(b'>I', raw[32:36])
langcode = self.locale_raw
langid = langcode & 0xFF
sublangid = (langcode >> 10) & 0xFF
self.language = main_language.get(langid, 'ENGLISH')
self.sublanguage = sub_language.get(sublangid, 'NEUTRAL')
self.num_index_entries, = struct.unpack('>I', raw[36:40])
self.ordt_start, = struct.unpack('>I', raw[40:44])
self.ligt_start, = struct.unpack('>I', raw[44:48])
self.num_of_ligt_entries, = struct.unpack('>I', raw[48:52])
self.num_of_ctoc_blocks, = struct.unpack('>I', raw[52:56])
self.unknown2 = raw[56:180]
self.tagx_offset, = struct.unpack(b'>I', raw[180:184])
if self.tagx_offset != self.header_length:
raise ValueError('TAGX offset and header length disagree')
self.unknown3 = raw[184:self.header_length]
tagx = raw[self.header_length:]
if not tagx.startswith(b'TAGX'):
raise ValueError('Invalid TAGX section')
self.tagx_header_length, = struct.unpack('>I', tagx[4:8])
self.tagx_control_byte_count, = struct.unpack('>I', tagx[8:12])
tag_table = tagx[12:self.tagx_header_length]
if len(tag_table) % 4 != 0:
raise ValueError('Invalid Tag table')
num_tagx_entries = len(tag_table) // 4
self.tagx_entries = []
for i in range(num_tagx_entries):
self.tagx_entries.append(TagX(tag_table[i*4:(i+1)*4],
self.tagx_control_byte_count))
if self.tagx_entries and not self.tagx_entries[-1].is_eof:
raise ValueError('TAGX last entry is not EOF')
idxt0_pos = self.header_length+self.tagx_header_length
last_num, consumed = decode_hex_number(raw[idxt0_pos:])
count_pos = idxt0_pos + consumed
self.ncx_count, = struct.unpack(b'>H', raw[count_pos:count_pos+2])
if last_num != self.ncx_count - 1:
raise ValueError('Last id number in the NCX != NCX count - 1')
# There may be some alignment zero bytes between the end of the idxt0
# and self.idxt_start
idxt = raw[self.idxt_start:]
if idxt[:4] != b'IDXT':
raise ValueError('Invalid IDXT header')
length_check, = struct.unpack(b'>H', idxt[4:6])
if length_check != self.header_length + self.tagx_header_length:
raise ValueError('Length check failed')
def __str__(self):
ans = ['*'*20 + ' Index Header '+ '*'*20]
a = ans.append
def u(w):
a('Unknown: %r (%d bytes) (All zeros: %r)'%(w,
len(w), not bool(w.replace(b'\0', b'')) ))
a('Header length: %d'%self.header_length)
u(self.unknown1)
a('Index Type: %s (%d)'%(self.index_type_desc, self.index_type))
a('Offset to IDXT start: %d'%self.idxt_start)
a('Number of index records: %d'%self.index_count)
a('Index encoding: %s (%d)'%(self.index_encoding,
self.index_encoding_num))
a('Index language: %s - %s (%s)'%(self.language, self.sublanguage,
hex(self.locale_raw)))
a('Number of index entries: %d'% self.num_index_entries)
a('ORDT start: %d'%self.ordt_start)
a('LIGT start: %d'%self.ligt_start)
a('Number of LIGT entries: %d'%self.num_of_ligt_entries)
a('Number of CTOC blocks: %d'%self.num_of_ctoc_blocks)
u(self.unknown2)
a('TAGX offset: %d'%self.tagx_offset)
u(self.unknown3)
a('\n\n')
a('*'*20 + ' TAGX Header (%d bytes)'%self.tagx_header_length+ '*'*20)
a('Header length: %d'%self.tagx_header_length)
a('Control byte count: %d'%self.tagx_control_byte_count)
for i in self.tagx_entries:
a('\t' + repr(i))
a('Number of entries in the NCX: %d'% self.ncx_count)
return '\n'.join(ans)
# }}}
class IndexEntry(object): # {{{
def __init__(self, ident, entry_type, raw):
self.id = ident
self.entry_type = entry_type
# }}}
class IndexRecord(object): # {{{
def __init__(self, record):
self.record = record
raw = self.record.raw
if raw[:4] != b'INDX':
raise ValueError('Invalid Primary Index Record')
u = struct.unpack
self.header_length, = u('>I', raw[4:8])
self.unknown1 = raw[8:12]
self.header_type, = u('>I', raw[12:16])
self.unknown2 = raw[16:20]
self.idxt_offset, self.idxt_count = u(b'>II', raw[20:28])
if self.idxt_offset < 192:
raise ValueError('Unknown Index record structure')
self.unknown3 = raw[28:36]
self.unknown4 = raw[36:192] # Should be 156 bytes
self.index_offsets = []
indices = raw[self.idxt_offset:]
if indices[:4] != b'IDXT':
raise ValueError("Invalid IDXT index table")
indices = indices[4:]
for i in range(self.idxt_count):
off, = u(b'>H', indices[i*2:(i+1)*2])
self.index_offsets.append(off-192)
indxt = raw[192:self.idxt_offset]
self.indices = []
for off in self.index_offsets:
index = indxt[off:]
ident, consumed = decode_hex_number(index)
index = index[consumed:]
entry_type = u(b'>B', index[0])
self.indices.append(IndexEntry(ident, entry_type, index[1:]))
def __str__(self):
ans = ['*'*20 + ' Index Record (%d bytes)'%len(self.record.raw)+ '*'*20]
a = ans.append
def u(w):
a('Unknown: %r (%d bytes) (All zeros: %r)'%(w,
len(w), not bool(w.replace(b'\0', b'')) ))
a('Header length: %d'%self.header_length)
u(self.unknown1)
a('Header Type: %d'%self.header_type)
u(self.unknown2)
a('IDXT Offset: %d'%self.idxt_offset)
a('IDXT Count: %d'%self.idxt_count)
u(self.unknown3)
u(self.unknown4)
a('Index offsets: %r'%self.index_offsets)
return '\n'.join(ans)
# }}}
class MOBIFile(object): # {{{
def __init__(self, stream):
self.raw = stream.read()
@ -384,25 +590,44 @@ class MOBIFile(object):
self.mobi_header = MOBIHeader(self.records[0])
self.index_header = None
pir = self.mobi_header.primary_index_record
if pir != 0xffffffff:
self.index_header = IndexHeader(self.records[pir])
self.index_record = IndexRecord(self.records[pir+1])
def print_header(self):
print (str(self.palmdb).encode('utf-8'))
print ()
print ('Record headers:')
def print_header(self, f=sys.stdout):
print (str(self.palmdb).encode('utf-8'), file=f)
print (file=f)
print ('Record headers:', file=f)
for i, r in enumerate(self.records):
print ('%6d. %s'%(i, r.header))
print ('%6d. %s'%(i, r.header), file=f)
print ()
print (str(self.mobi_header).encode('utf-8'))
print (file=f)
print (str(self.mobi_header).encode('utf-8'), file=f)
# }}}
def inspect_mobi(path_or_stream):
stream = (path_or_stream if hasattr(path_or_stream, 'read') else
open(path_or_stream, 'rb'))
f = MOBIFile(stream)
f.print_header()
ddir = 'debug_' + os.path.splitext(os.path.basename(stream.name))[0]
if not os.path.exists(ddir):
os.mkdir(ddir)
with open(os.path.join(ddir, 'header.txt'), 'wb') as out:
f.print_header(f=out)
if f.index_header is not None:
with open(os.path.join(ddir, 'index.txt'), 'wb') as out:
print(str(f.index_header), file=out)
print('\n\n', file=out)
print(str(f.index_record), file=out)
print ('Debug data saved to:', ddir)
def main():
inspect_mobi(sys.argv[1])
if __name__ == '__main__':
import sys
f = MOBIFile(open(sys.argv[1], 'rb'))
f.print_header()
main()

View File

@ -27,7 +27,7 @@ class MOBIOutput(OutputFormatPlugin):
),
OptionRecommendation(name='no_inline_toc',
recommended_value=False, level=OptionRecommendation.LOW,
help=_('Don\'t add Table of Contents to end of book. Useful if '
help=_('Don\'t add Table of Contents to the book. Useful if '
'the book has its own table of contents.')),
OptionRecommendation(name='toc_title', recommended_value=None,
help=_('Title for any generated in-line table of contents.')
@ -45,6 +45,12 @@ class MOBIOutput(OutputFormatPlugin):
'the MOBI output plugin will try to convert margins specified'
' in the input document, otherwise it will ignore them.')
),
OptionRecommendation(name='mobi_toc_at_start',
recommended_value=False,
help=_('When adding the Table of Contents to the book, add it at the start of the '
'book instead of the end. Not recommended.')
),
])
def check_for_periodical(self):
@ -150,7 +156,7 @@ class MOBIOutput(OutputFormatPlugin):
# Fix up the periodical href to point to first section href
toc.nodes[0].href = toc.nodes[0].nodes[0].href
# GR diagnostics
# diagnostics
if self.opts.verbose > 3:
self.dump_toc(toc)
self.dump_manifest()
@ -158,16 +164,14 @@ class MOBIOutput(OutputFormatPlugin):
def convert(self, oeb, output_path, input_plugin, opts, log):
self.log, self.opts, self.oeb = log, opts, oeb
from calibre.ebooks.mobi.writer import PALM_MAX_IMAGE_SIZE, \
MobiWriter, PALMDOC, UNCOMPRESSED
from calibre.ebooks.mobi.mobiml import MobiMLizer
from calibre.ebooks.oeb.transforms.manglecase import CaseMangler
from calibre.ebooks.oeb.transforms.rasterize import SVGRasterizer, Unavailable
from calibre.ebooks.oeb.transforms.htmltoc import HTMLTOCAdder
from calibre.customize.ui import plugin_for_input_format
imagemax = PALM_MAX_IMAGE_SIZE if opts.rescale_images else None
if not opts.no_inline_toc:
tocadder = HTMLTOCAdder(title=opts.toc_title)
tocadder = HTMLTOCAdder(title=opts.toc_title, position='start' if
opts.mobi_toc_at_start else 'end')
tocadder(oeb, opts)
mangler = CaseMangler()
mangler(oeb, opts)
@ -179,10 +183,14 @@ class MOBIOutput(OutputFormatPlugin):
mobimlizer = MobiMLizer(ignore_tables=opts.linearize_tables)
mobimlizer(oeb, opts)
self.check_for_periodical()
write_page_breaks_after_item = not input_plugin is plugin_for_input_format('cbz')
writer = MobiWriter(opts, imagemax=imagemax,
compression=UNCOMPRESSED if opts.dont_compress else PALMDOC,
prefer_author_sort=opts.prefer_author_sort,
write_page_breaks_after_item=write_page_breaks_after_item)
write_page_breaks_after_item = input_plugin is not plugin_for_input_format('cbz')
from calibre.utils.config import tweaks
if tweaks.get('new_mobi_writer', False):
from calibre.ebooks.mobi.writer2.main import MobiWriter
MobiWriter
else:
from calibre.ebooks.mobi.writer import MobiWriter
writer = MobiWriter(opts,
write_page_breaks_after_item=write_page_breaks_after_item)
writer(oeb, output_path)

View File

@ -111,7 +111,8 @@ def align_block(raw, multiple=4, pad='\0'):
def rescale_image(data, maxsizeb, dimen=None):
if dimen is not None:
data = thumbnail(data, width=dimen, height=dimen)[-1]
data = thumbnail(data, width=dimen[0], height=dimen[1],
compression_quality=90)[-1]
else:
# Replace transparent pixels with white pixels and convert to JPEG
data = save_cover_data_to(data, 'img.jpg', return_data=True)
@ -141,7 +142,7 @@ def rescale_image(data, maxsizeb, dimen=None):
scale -= 0.05
return data
class Serializer(object):
class Serializer(object): # {{{
NSRMAP = {'': None, XML_NS: 'xml', XHTML_NS: '', MBP_NS: 'mbp'}
def __init__(self, oeb, images, write_page_breaks_after_item=True):
@ -172,6 +173,9 @@ class Serializer(object):
hrefs = self.oeb.manifest.hrefs
buffer.write('<guide>')
for ref in self.oeb.guide.values():
# The Kindle decides where to open a book based on the presence of
# an item in the guide that looks like
# <reference type="text" title="Start" href="chapter-one.xhtml"/>
path = urldefrag(ref.href)[0]
if path not in hrefs or hrefs[path].media_type not in OEB_DOCS:
continue
@ -215,12 +219,6 @@ class Serializer(object):
self.anchor_offset = buffer.tell()
buffer.write('<body>')
self.anchor_offset_kindle = buffer.tell()
# CybookG3 'Start Reading' link
if 'text' in self.oeb.guide:
href = self.oeb.guide['text'].href
buffer.write('<a ')
self.serialize_href(href)
buffer.write(' />')
spine = [item for item in self.oeb.spine if item.linear]
spine.extend([item for item in self.oeb.spine if not item.linear])
for item in spine:
@ -315,16 +313,20 @@ class Serializer(object):
buffer.seek(hoff)
buffer.write('%010d' % ioff)
# }}}
class MobiWriter(object):
COLLAPSE_RE = re.compile(r'[ \t\r\n\v]+')
def __init__(self, opts, compression=PALMDOC, imagemax=None,
prefer_author_sort=False, write_page_breaks_after_item=True):
def __init__(self, opts,
write_page_breaks_after_item=True):
self.opts = opts
self.write_page_breaks_after_item = write_page_breaks_after_item
self._compression = compression or UNCOMPRESSED
self._imagemax = imagemax or OTHER_MAX_IMAGE_SIZE
self._prefer_author_sort = prefer_author_sort
self._compression = UNCOMPRESSED if getattr(opts, 'dont_compress',
False) else PALMDOC
self._imagemax = (PALM_MAX_IMAGE_SIZE if getattr(opts,
'rescale_images', False) else OTHER_MAX_IMAGE_SIZE)
self._prefer_author_sort = getattr(opts, 'prefer_author_sort', False)
self._primary_index_record = None
self._conforming_periodical_toc = False
self._indexable = False
@ -1258,11 +1260,11 @@ class MobiWriter(object):
data = compress_doc(data)
record = StringIO()
record.write(data)
# Write trailing muti-byte sequence if any
record.write(overlap)
record.write(pack('>B', len(overlap)))
# Marshall's utf-8 break code.
if WRITE_PBREAKS :
record.write(overlap)
record.write(pack('>B', len(overlap)))
nextra = 0
pbreak = 0
running = offset
@ -1325,6 +1327,8 @@ class MobiWriter(object):
except:
self._oeb.logger.warn('Bad image file %r' % item.href)
continue
finally:
item.unload_data_from_memory()
self._records.append(data)
if self._first_image_record is None:
self._first_image_record = len(self._records)-1

View File

@ -0,0 +1,15 @@
#!/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'
UNCOMPRESSED = 1
PALMDOC = 2
HUFFDIC = 17480
PALM_MAX_IMAGE_SIZE = 63 * 1024

View File

@ -0,0 +1,502 @@
#!/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 re, random, time
from cStringIO import StringIO
from struct import pack
from calibre.ebooks import normalize
from calibre.ebooks.oeb.base import OEB_RASTER_IMAGES
from calibre.ebooks.mobi.writer2.serializer import Serializer
from calibre.ebooks.compression.palmdoc import compress_doc
from calibre.ebooks.mobi.langcodes import iana2mobi
from calibre.utils.filenames import ascii_filename
from calibre.ebooks.mobi.writer2 import PALMDOC, UNCOMPRESSED
from calibre.ebooks.mobi.writer2.utils import (rescale_image, encint)
EXTH_CODES = {
'creator': 100,
'publisher': 101,
'description': 103,
'identifier': 104,
'subject': 105,
'pubdate': 106,
'date': 106,
'review': 107,
'contributor': 108,
'rights': 109,
'type': 111,
'source': 112,
'title': 503,
}
# Disabled as I dont care about uncrossable breaks
WRITE_UNCROSSABLE_BREAKS = False
RECORD_SIZE = 0x1000 # 4096
MAX_THUMB_SIZE = 16 * 1024
MAX_THUMB_DIMEN = (180, 240)
class MobiWriter(object):
COLLAPSE_RE = re.compile(r'[ \t\r\n\v]+')
def __init__(self, opts, write_page_breaks_after_item=True):
self.opts = opts
self.write_page_breaks_after_item = write_page_breaks_after_item
self.compression = UNCOMPRESSED if opts.dont_compress else PALMDOC
self.prefer_author_sort = opts.prefer_author_sort
def __call__(self, oeb, path_or_stream):
if hasattr(path_or_stream, 'write'):
return self.dump_stream(oeb, path_or_stream)
with open(path_or_stream, 'w+b') as stream:
return self.dump_stream(oeb, stream)
def write(self, *args):
for datum in args:
self.stream.write(datum)
def tell(self):
return self.stream.tell()
def dump_stream(self, oeb, stream):
self.oeb = oeb
self.stream = stream
self.records = [None]
self.generate_content()
self.generate_record0()
self.write_header()
self.write_content()
def generate_content(self):
self.map_image_names()
self.generate_text()
# Image records come after text records
self.generate_images()
def map_image_names(self):
'''
Map image names to record indices, ensuring that the masthead image if
present has index number 1.
'''
index = 1
self.images = images = {}
mh_href = None
if 'masthead' in self.oeb.guide:
mh_href = self.oeb.guide['masthead'].href
images[mh_href] = 1
index += 1
for item in self.oeb.manifest.values():
if item.media_type in OEB_RASTER_IMAGES:
if item.href == mh_href: continue
images[item.href] = index
index += 1
def generate_images(self):
self.oeb.logger.info('Serializing images...')
images = [(index, href) for href, index in self.images.iteritems()]
images.sort()
self.first_image_record = None
for _, href in images:
item = self.oeb.manifest.hrefs[href]
try:
data = rescale_image(item.data)
except:
self.oeb.logger.warn('Bad image file %r' % item.href)
continue
finally:
item.unload_data_from_memory()
self.records.append(data)
if self.first_image_record is None:
self.first_image_record = len(self.records) - 1
def generate_text(self):
self.oeb.logger.info('Serializing markup content...')
serializer = Serializer(self.oeb, self.images,
write_page_breaks_after_item=self.write_page_breaks_after_item)
text = serializer()
breaks = serializer.breaks
self.anchor_offset_kindle = serializer.anchor_offset_kindle
self.id_offsets = serializer.id_offsets
self.content_length = len(text)
self.text_length = len(text)
text = StringIO(text)
buf = []
nrecords = 0
offset = 0
if self.compression != UNCOMPRESSED:
self.oeb.logger.info(' Compressing markup content...')
data, overlap = self.read_text_record(text)
while len(data) > 0:
if self.compression == PALMDOC:
data = compress_doc(data)
record = StringIO()
record.write(data)
self.records.append(record.getvalue())
buf.append(self.records[-1])
nrecords += 1
offset += RECORD_SIZE
data, overlap = self.read_text_record(text)
# Write information about the mutibyte character overlap, if any
record.write(overlap)
record.write(pack(b'>B', len(overlap)))
# Write information about uncrossable breaks (non linear items in
# the spine)
if WRITE_UNCROSSABLE_BREAKS:
nextra = 0
pbreak = 0
running = offset
# Write information about every uncrossable break that occurs in
# the next record.
while breaks and (breaks[0] - offset) < RECORD_SIZE:
pbreak = (breaks.pop(0) - running) >> 3
encoded = encint(pbreak)
record.write(encoded)
running += pbreak << 3
nextra += len(encoded)
lsize = 1
while True:
size = encint(nextra + lsize, forward=False)
if len(size) == lsize:
break
lsize += 1
record.write(size)
self.text_nrecords = nrecords + 1
def read_text_record(self, text):
'''
Return a Palmdoc record of size RECORD_SIZE from the text file object.
In case the record ends in the middle of a multibyte character return
the overlap as well.
Returns data, overlap: where both are byte strings. overlap is the
extra bytes needed to complete the truncated multibyte character.
'''
opos = text.tell()
text.seek(0, 2)
# npos is the position of the next record
npos = min((opos + RECORD_SIZE, text.tell()))
# Number of bytes from the next record needed to complete the last
# character in this record
extra = 0
last = b''
while not last.decode('utf-8', 'ignore'):
# last contains no valid utf-8 characters
size = len(last) + 1
text.seek(npos - size)
last = text.read(size)
# last now has one valid utf-8 char and possibly some bytes that belong
# to a truncated char
try:
last.decode('utf-8', 'strict')
except UnicodeDecodeError:
# There are some truncated bytes in last
prev = len(last)
while True:
text.seek(npos - prev)
last = text.read(len(last) + 1)
try:
last.decode('utf-8')
except UnicodeDecodeError:
pass
else:
break
extra = len(last) - prev
text.seek(opos)
data = text.read(RECORD_SIZE)
overlap = text.read(extra)
text.seek(npos)
return data, overlap
def generate_end_records(self):
self.flis_number = len(self.records)
self.records.append('\xE9\x8E\x0D\x0A')
def generate_record0(self): # {{{
metadata = self.oeb.metadata
exth = self.build_exth()
last_content_record = len(self.records) - 1
self.generate_end_records()
record0 = StringIO()
# The PalmDOC Header
record0.write(pack(b'>HHIHHHH', self.compression, 0,
self.text_length,
self.text_nrecords-1, RECORD_SIZE, 0, 0)) # 0 - 15 (0x0 - 0xf)
uid = random.randint(0, 0xffffffff)
title = normalize(unicode(metadata.title[0])).encode('utf-8')
# The MOBI Header
# 0x0 - 0x3
record0.write(b'MOBI')
# 0x4 - 0x7 : Length of header
# 0x8 - 0x11 : MOBI type
# type meaning
# 0x002 MOBI book (chapter - chapter navigation)
# 0x101 News - Hierarchical navigation with sections and articles
# 0x102 News feed - Flat navigation
# 0x103 News magazine - same as 0x101
# 0xC - 0xF : Text encoding (65001 is utf-8)
# 0x10 - 0x13 : UID
# 0x14 - 0x17 : Generator version
record0.write(pack(b'>IIIII',
0xe8, 0x002, 65001, uid, 6))
# 0x18 - 0x1f : Unknown
record0.write(b'\xff' * 8)
# 0x20 - 0x23 : Secondary index record
record0.write(pack(b'>I', 0xffffffff))
# 0x24 - 0x3f : Unknown
record0.write(b'\xff' * 28)
# 0x40 - 0x43 : Offset of first non-text record
record0.write(pack(b'>I',
self.text_nrecords + 1))
# 0x44 - 0x4b : title offset, title length
record0.write(pack(b'>II',
0xe8 + 16 + len(exth), len(title)))
# 0x4c - 0x4f : Language specifier
record0.write(iana2mobi(
str(metadata.language[0])))
# 0x50 - 0x57 : Unknown
record0.write(b'\0' * 8)
# 0x58 - 0x5b : Format version
# 0x5c - 0x5f : First image record number
record0.write(pack(b'>II',
6, self.first_image_record if self.first_image_record else 0))
# 0x60 - 0x63 : First HUFF/CDIC record number
# 0x64 - 0x67 : Number of HUFF/CDIC records
# 0x68 - 0x6b : First DATP record number
# 0x6c - 0x6f : Number of DATP records
record0.write(b'\0' * 16)
# 0x70 - 0x73 : EXTH flags
record0.write(pack(b'>I', 0x50))
# 0x74 - 0x93 : Unknown
record0.write(b'\0' * 32)
# 0x94 - 0x97 : DRM offset
# 0x98 - 0x9b : DRM count
# 0x9c - 0x9f : DRM size
# 0xa0 - 0xa3 : DRM flags
record0.write(pack(b'>IIII',
0xffffffff, 0xffffffff, 0, 0))
# 0xa4 - 0xaf : Unknown
record0.write(b'\0'*12)
# 0xb0 - 0xb1 : First content record number
# 0xb2 - 0xb3 : last content record number
# (Includes Image, DATP, HUFF, DRM)
record0.write(pack(b'>HH', 1, last_content_record))
# 0xb4 - 0xb7 : Unknown
record0.write(b'\0\0\0\x01')
# 0xb8 - 0xbb : FCIS record number
record0.write(pack(b'>I', 0xffffffff))
# 0xbc - 0xbf : Unknown (FCIS record count?)
record0.write(pack(b'>I', 0xffffffff))
# 0xc0 - 0xc3 : FLIS record number
record0.write(pack(b'>I', 0xffffffff))
# 0xc4 - 0xc7 : Unknown (FLIS record count?)
record0.write(pack(b'>I', 1))
# 0xc8 - 0xcf : Unknown
record0.write(b'\0'*8)
# 0xd0 - 0xdf : Unknown
record0.write(pack(b'>IIII', 0xffffffff, 0, 0xffffffff, 0xffffffff))
# 0xe0 - 0xe3 : Extra record data
# Extra record data flags:
# - 0x1: <extra multibyte bytes><size> (?)
# - 0x2: <TBS indexing description of this HTML record><size> GR
# - 0x4: <uncrossable breaks><size>
# GR: Use 7 for indexed files, 5 for unindexed
# Setting bit 2 (0x2) disables <guide><reference type="start"> functionality
extra_data_flags = 0b1 # Has multibyte overlap bytes
if WRITE_UNCROSSABLE_BREAKS:
extra_data_flags |= 0b100
record0.write(pack(b'>I', extra_data_flags))
# 0xe4 - 0xe7 : Primary index record
record0.write(pack(b'>I', 0xffffffff))
record0.write(exth)
record0.write(title)
record0 = record0.getvalue()
# Add some buffer so that Amazon can add encryption information if this
# MOBI is submitted for publication
record0 += (b'\0' * (1024*8))
self.records[0] = record0
# }}}
def build_exth(self): # {{{
oeb = self.oeb
exth = StringIO()
nrecs = 0
for term in oeb.metadata:
if term not in EXTH_CODES: continue
code = EXTH_CODES[term]
items = oeb.metadata[term]
if term == 'creator':
if self.prefer_author_sort:
creators = [normalize(unicode(c.file_as or c)) for c in items]
else:
creators = [normalize(unicode(c)) for c in items]
items = ['; '.join(creators)]
for item in items:
data = self.COLLAPSE_RE.sub(' ', normalize(unicode(item)))
if term == 'identifier':
if data.lower().startswith('urn:isbn:'):
data = data[9:]
elif item.scheme.lower() == 'isbn':
pass
else:
continue
data = data.encode('utf-8')
exth.write(pack(b'>II', code, len(data) + 8))
exth.write(data)
nrecs += 1
if term == 'rights' :
try:
rights = normalize(unicode(oeb.metadata.rights[0])).encode('utf-8')
except:
rights = b'Unknown'
exth.write(pack(b'>II', EXTH_CODES['rights'], len(rights) + 8))
exth.write(rights)
nrecs += 1
# Write UUID as ASIN
uuid = None
from calibre.ebooks.oeb.base import OPF
for x in oeb.metadata['identifier']:
if (x.get(OPF('scheme'), None).lower() == 'uuid' or
unicode(x).startswith('urn:uuid:')):
uuid = unicode(x).split(':')[-1]
break
if uuid is None:
from uuid import uuid4
uuid = str(uuid4())
if isinstance(uuid, unicode):
uuid = uuid.encode('utf-8')
exth.write(pack(b'>II', 113, len(uuid) + 8))
exth.write(uuid)
nrecs += 1
# Write cdetype
if not self.opts.mobi_periodical:
data = b'EBOK'
exth.write(pack(b'>II', 501, len(data)+8))
exth.write(data)
nrecs += 1
# Add a publication date entry
if oeb.metadata['date'] != [] :
datestr = str(oeb.metadata['date'][0])
elif oeb.metadata['timestamp'] != [] :
datestr = str(oeb.metadata['timestamp'][0])
if datestr is not None:
exth.write(pack(b'>II', EXTH_CODES['pubdate'], len(datestr) + 8))
exth.write(datestr)
nrecs += 1
else:
raise NotImplementedError("missing date or timestamp needed for mobi_periodical")
if (oeb.metadata.cover and
unicode(oeb.metadata.cover[0]) in oeb.manifest.ids):
id = unicode(oeb.metadata.cover[0])
item = oeb.manifest.ids[id]
href = item.href
if href in self.images:
index = self.images[href] - 1
exth.write(pack(b'>III', 0xc9, 0x0c, index))
exth.write(pack(b'>III', 0xcb, 0x0c, 0))
nrecs += 2
index = self.add_thumbnail(item)
if index is not None:
exth.write(pack(b'>III', 0xca, 0x0c, index - 1))
nrecs += 1
exth = exth.getvalue()
trail = len(exth) % 4
pad = b'\0' * (4 - trail) # Always pad w/ at least 1 byte
exth = [b'EXTH', pack(b'>II', len(exth) + 12, nrecs), exth, pad]
return b''.join(exth)
# }}}
def add_thumbnail(self, item):
try:
data = rescale_image(item.data, dimen=MAX_THUMB_DIMEN,
maxsizeb=MAX_THUMB_SIZE)
except IOError:
self.oeb.logger.warn('Bad image file %r' % item.href)
return None
manifest = self.oeb.manifest
id, href = manifest.generate('thumbnail', 'thumbnail.jpeg')
manifest.add(id, href, 'image/jpeg', data=data)
index = len(self.images) + 1
self.images[href] = index
self.records.append(data)
return index
def write_header(self):
title = ascii_filename(unicode(self.oeb.metadata.title[0]))
title = title + (b'\0' * (32 - len(title)))
now = int(time.time())
nrecords = len(self.records)
self.write(title, pack(b'>HHIIIIII', 0, 0, now, now, 0, 0, 0, 0),
b'BOOK', b'MOBI', pack(b'>IIH', nrecords, 0, nrecords))
offset = self.tell() + (8 * nrecords) + 2
for i, record in enumerate(self.records):
self.write(pack(b'>I', offset), b'\0', pack(b'>I', 2*i)[1:])
offset += len(record)
self.write(b'\0\0')
def write_content(self):
for record in self.records:
self.write(record)

View File

@ -0,0 +1,246 @@
#!/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.oeb.base import (OEB_DOCS, XHTML, XHTML_NS, XML_NS,
namespace, prefixname, urlnormalize)
from calibre.ebooks.mobi.mobiml import MBP_NS
from collections import defaultdict
from urlparse import urldefrag
from cStringIO import StringIO
class Serializer(object):
NSRMAP = {'': None, XML_NS: 'xml', XHTML_NS: '', MBP_NS: 'mbp'}
def __init__(self, oeb, images, write_page_breaks_after_item=True):
'''
Write all the HTML markup in oeb into a single in memory buffer
containing a single html document with links replaced by offsets into
the buffer.
:param oeb: OEBBook object that encapsulates the document to be
processed.
:param images: Mapping of image hrefs (urlnormalized) to image record
indices.
:param write_page_breaks_after_item: If True a MOBIpocket pagebreak tag
is written after every element of the spine in ``oeb``.
'''
self.oeb = oeb
self.images = images
self.logger = oeb.logger
self.write_page_breaks_after_item = write_page_breaks_after_item
# Mapping of hrefs (urlnormalized) to the offset in the buffer where
# the resource pointed to by the href lives. Used at the end to fill in
# the correct values into all filepos="..." links.
self.id_offsets = {}
# Mapping of hrefs (urlnormalized) to a list of offsets into the buffer
# where filepos="..." elements are written corresponding to links that
# point to the href. This is used at the end to fill in the correct values.
self.href_offsets = defaultdict(list)
# List of offsets in the buffer of non linear items in the spine. These
# become uncrossable breaks in the MOBI
self.breaks = []
def __call__(self):
'''
Return the document serialized as a single UTF-8 encoded bytestring.
'''
buf = self.buf = StringIO()
buf.write(b'<html>')
self.serialize_head()
self.serialize_body()
buf.write(b'</html>')
self.fixup_links()
return buf.getvalue()
def serialize_head(self):
buf = self.buf
buf.write(b'<head>')
if len(self.oeb.guide) > 0:
self.serialize_guide()
buf.write(b'</head>')
def serialize_guide(self):
'''
The Kindle decides where to open a book based on the presence of
an item in the guide that looks like
<reference type="text" title="Start" href="chapter-one.xhtml"/>
Similarly an item with type="toc" controls where the Goto Table of
Contents operation on the kindle goes.
'''
buf = self.buf
hrefs = self.oeb.manifest.hrefs
buf.write(b'<guide>')
for ref in self.oeb.guide.values():
path = urldefrag(ref.href)[0]
if path not in hrefs or hrefs[path].media_type not in OEB_DOCS:
continue
buf.write(b'<reference type="')
if ref.type.startswith('other.') :
self.serialize_text(ref.type.replace('other.',''), quot=True)
else:
self.serialize_text(ref.type, quot=True)
buf.write(b'" ')
if ref.title is not None:
buf.write(b'title="')
self.serialize_text(ref.title, quot=True)
buf.write(b'" ')
self.serialize_href(ref.href)
# Space required or won't work, I kid you not
buf.write(b' />')
buf.write(b'</guide>')
def serialize_href(self, href, base=None):
'''
Serialize the href attribute of an <a> or <reference> tag. It is
serialized as filepos="000000000" and a pointer to its location is
stored in self.href_offsets so that the correct value can be filled in
at the end.
'''
hrefs = self.oeb.manifest.hrefs
path, frag = urldefrag(urlnormalize(href))
if path and base:
path = base.abshref(path)
if path and path not in hrefs:
return False
buf = self.buf
item = hrefs[path] if path else None
if item and item.spine_position is None:
return False
path = item.href if item else base.href
href = '#'.join((path, frag)) if frag else path
buf.write(b'filepos=')
self.href_offsets[href].append(buf.tell())
buf.write(b'0000000000')
return True
def serialize_body(self):
'''
Serialize all items in the spine of the document. Non linear items are
moved to the end.
'''
buf = self.buf
self.anchor_offset = buf.tell()
buf.write(b'<body>')
self.anchor_offset_kindle = buf.tell()
spine = [item for item in self.oeb.spine if item.linear]
spine.extend([item for item in self.oeb.spine if not item.linear])
for item in spine:
self.serialize_item(item)
buf.write(b'</body>')
def serialize_item(self, item):
'''
Serialize an individual item from the spine of the input document.
A reference to this item is stored in self.href_offsets
'''
buf = self.buf
if not item.linear:
self.breaks.append(buf.tell() - 1)
self.id_offsets[urlnormalize(item.href)] = buf.tell()
# Kindle periodical articles are contained in a <div> tag
buf.write(b'<div>')
for elem in item.data.find(XHTML('body')):
self.serialize_elem(elem, item)
# Kindle periodical article end marker
buf.write(b'<div></div>')
if self.write_page_breaks_after_item:
buf.write(b'<mbp:pagebreak/>')
buf.write(b'</div>')
self.anchor_offset = None
def serialize_elem(self, elem, item, nsrmap=NSRMAP):
buf = self.buf
if not isinstance(elem.tag, basestring) \
or namespace(elem.tag) not in nsrmap:
return
tag = prefixname(elem.tag, nsrmap)
# Previous layers take care of @name
id_ = elem.attrib.pop('id', None)
if id_:
href = '#'.join((item.href, id_))
offset = self.anchor_offset or buf.tell()
self.id_offsets[urlnormalize(href)] = offset
if self.anchor_offset is not None and \
tag == 'a' and not elem.attrib and \
not len(elem) and not elem.text:
return
self.anchor_offset = buf.tell()
buf.write(b'<')
buf.write(tag.encode('utf-8'))
if elem.attrib:
for attr, val in elem.attrib.items():
if namespace(attr) not in nsrmap:
continue
attr = prefixname(attr, nsrmap)
buf.write(b' ')
if attr == 'href':
if self.serialize_href(val, item):
continue
elif attr == 'src':
href = urlnormalize(item.abshref(val))
if href in self.images:
index = self.images[href]
buf.write(b'recindex="%05d"' % index)
continue
buf.write(attr.encode('utf-8'))
buf.write(b'="')
self.serialize_text(val, quot=True)
buf.write(b'"')
buf.write(b'>')
if elem.text or len(elem) > 0:
if elem.text:
self.anchor_offset = None
self.serialize_text(elem.text)
for child in elem:
self.serialize_elem(child, item)
if child.tail:
self.anchor_offset = None
self.serialize_text(child.tail)
buf.write(b'</%s>' % tag.encode('utf-8'))
def serialize_text(self, text, quot=False):
text = text.replace('&', '&amp;')
text = text.replace('<', '&lt;')
text = text.replace('>', '&gt;')
text = text.replace(u'\u00AD', '') # Soft-hyphen
if quot:
text = text.replace('"', '&quot;')
self.buf.write(text.encode('utf-8'))
def fixup_links(self):
'''
Fill in the correct values for all filepos="..." links with the offsets
of the linked to content (as stored in id_offsets).
'''
buf = self.buf
id_offsets = self.id_offsets
for href, hoffs in self.href_offsets.items():
# Iterate over all filepos items
if href not in id_offsets:
self.logger.warn('Hyperlink target %r not found' % href)
# Link to the top of the document, better than just ignoring
href, _ = urldefrag(href)
if href in self.id_offsets:
ioff = self.id_offsets[href]
for hoff in hoffs:
buf.seek(hoff)
buf.write(b'%010d' % ioff)

View File

@ -0,0 +1,153 @@
#!/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 struct
from calibre.utils.magick.draw import Image, save_cover_data_to, thumbnail
IMAGE_MAX_SIZE = 10 * 1024 * 1024
def decode_hex_number(raw):
'''
Return a variable length number encoded using hexadecimal encoding. These
numbers have the first byte which tells the number of bytes that follow.
The bytes that follow are simply the hexadecimal representation of the
number.
:param raw: Raw binary data as a bytestring
:return: The number and the number of bytes from raw that the number
occupies
'''
length, = struct.unpack(b'>B', raw[0])
raw = raw[1:1+length]
consumed = length+1
return int(raw, 16), consumed
def encode_number_as_hex(num):
'''
Encode num as a variable length encoded hexadecimal number. Returns the
bytestring containing the encoded number. These
numbers have the first byte which tells the number of bytes that follow.
The bytes that follow are simply the hexadecimal representation of the
number.
'''
num = bytes(hex(num)[2:])
ans = bytearray(num)
ans.insert(0, len(num))
return bytes(ans)
def encint(value, forward=True):
'''
Some parts of the Mobipocket format encode data as variable-width integers.
These integers are represented big-endian with 7 bits per byte in bits 1-7.
They may be either forward-encoded, in which case only the first byte has bit 8 set,
or backward-encoded, in which case only the last byte has bit 8 set.
For example, the number 0x11111 = 0b10001000100010001 would be represented
forward-encoded as:
0x04 0x22 0x91 = 0b100 0b100010 0b10010001
And backward-encoded as:
0x84 0x22 0x11 = 0b10000100 0b100010 0b10001
This function encodes the integer ``value`` as a variable width integer and
returns the bytestring corresponding to it.
If forward is True the bytes returned are suitable for prepending to the
output buffer, otherwise they must be append to the output buffer.
'''
# Encode vwi
byts = bytearray()
while True:
b = value & 0b01111111
value >>= 7 # shift value to the right by 7 bits
byts.append(b)
if value == 0:
break
byts[0 if forward else -1] |= 0b10000000
byts.reverse()
return bytes(byts)
def decint(raw, forward=True):
'''
Read a variable width integer from the bytestring raw and return the
integer and the number of bytes read. If forward is True bytes are read
from the start of raw, otherwise from the end of raw.
This function is the inverse of encint above, see its docs for more
details.
'''
val = 0
byts = bytearray()
for byte in raw if forward else reversed(raw):
bnum = ord(byte)
byts.append(bnum & 0b01111111)
if bnum & 0b10000000:
break
if not forward:
byts.reverse()
for byte in byts:
val <<= 7 # Shift value to the left by 7 bits
val |= byte
return val, len(byts)
def test_decint(num):
for d in (True, False):
raw = encint(num, forward=d)
sz = len(raw)
if (num, sz) != decint(raw, forward=d):
raise ValueError('Failed for num %d, forward=%r: %r != %r' % (
num, d, (num, sz), decint(raw, forward=d)))
def rescale_image(data, maxsizeb=IMAGE_MAX_SIZE, dimen=None):
'''
Convert image setting all transparent pixels to white and changing format
to JPEG. Ensure the resultant image has a byte size less than
maxsizeb.
If dimen is not None, generate a thumbnail of width=dimen, height=dimen
Returns the image as a bytestring
'''
if dimen is not None:
data = thumbnail(data, width=dimen, height=dimen,
compression_quality=90)[-1]
else:
# Replace transparent pixels with white pixels and convert to JPEG
data = save_cover_data_to(data, 'img.jpg', return_data=True)
if len(data) <= maxsizeb:
return data
orig_data = data
img = Image()
quality = 95
img.load(data)
while len(data) >= maxsizeb and quality >= 10:
quality -= 5
img.set_compression_quality(quality)
data = img.export('jpg')
if len(data) <= maxsizeb:
return data
orig_data = data
scale = 0.9
while len(data) >= maxsizeb and scale >= 0.05:
img = Image()
img.load(orig_data)
w, h = img.size
img.size = (int(scale*w), int(scale*h))
img.set_compression_quality(quality)
data = img.export('jpg')
scale -= 0.05
return data

View File

@ -1180,8 +1180,9 @@ class Manifest(object):
if memory is None:
from calibre.ptempfile import PersistentTemporaryFile
pt = PersistentTemporaryFile(suffix='_oeb_base_mem_unloader.img')
pt.write(self._data)
pt.close()
with pt:
pt.write(self._data)
self.oeb._temp_files.append(pt.name)
def loader(*args):
with open(pt.name, 'rb') as f:
ans = f.read()
@ -1196,8 +1197,6 @@ class Manifest(object):
self._loader = loader2
self._data = None
def __str__(self):
data = self.data
if isinstance(data, etree._Element):
@ -1913,6 +1912,14 @@ class OEBBook(object):
self.toc = TOC()
self.pages = PageList()
self.auto_generated_toc = True
self._temp_files = []
def clean_temp_files(self):
for path in self._temp_files:
try:
os.remove(path)
except:
pass
@classmethod
def generate(cls, opts):

View File

@ -92,7 +92,7 @@ class EbookIterator(object):
self.config = DynamicConfig(name='iterator')
ext = os.path.splitext(pathtoebook)[1].replace('.', '').lower()
ext = re.sub(r'(x{0,1})htm(l{0,1})', 'html', ext)
self.ebook_ext = ext
self.ebook_ext = ext.replace('original_', '')
def search(self, text, index, backwards=False):
text = text.lower()

View File

@ -318,7 +318,8 @@ class CSSFlattener(object):
for edge in ('top', 'bottom'):
cssdict['%s-%s'%(prop, edge)] = '0pt'
if self.context.insert_blank_line:
cssdict['margin-top'] = cssdict['margin-bottom'] = '0.5em'
cssdict['margin-top'] = cssdict['margin-bottom'] = \
'%fem'%self.context.insert_blank_line_size
if self.context.remove_paragraph_spacing:
cssdict['text-indent'] = "%1.1fem" % self.context.remove_paragraph_spacing_indent_size

View File

@ -36,5 +36,8 @@ class Clean(object):
href = urldefrag(self.oeb.guide[x].href)[0]
if x.lower() not in ('cover', 'titlepage', 'masthead', 'toc',
'title-page', 'copyright-page', 'start'):
item = self.oeb.guide[x]
if item.title and item.title.lower() == 'start':
continue
self.oeb.guide.remove(x)

View File

@ -45,9 +45,10 @@ body > .calibre_toc_block {
}
class HTMLTOCAdder(object):
def __init__(self, title=None, style='nested'):
def __init__(self, title=None, style='nested', position='end'):
self.title = title
self.style = style
self.position = position
@classmethod
def config(cls, cfg):
@ -98,7 +99,10 @@ class HTMLTOCAdder(object):
self.add_toc_level(body, oeb.toc)
id, href = oeb.manifest.generate('contents', 'contents.xhtml')
item = oeb.manifest.add(id, href, XHTML_MIME, data=contents)
oeb.spine.add(item, linear=False)
if self.position == 'end':
oeb.spine.add(item, linear=False)
else:
oeb.spine.insert(0, item, linear=True)
oeb.guide.add('toc', 'Table of Contents', href)
def add_toc_level(self, elem, toc):

View File

@ -47,15 +47,19 @@ def meta_info_to_oeb_metadata(mi, m, log, override_input_metadata=False):
m.add('series', mi.series)
elif override_input_metadata:
m.clear('series')
if not mi.is_null('isbn'):
identifiers = mi.get_identifiers()
set_isbn = False
for typ, val in identifiers.iteritems():
has = False
if typ.lower() == 'isbn':
set_isbn = True
for x in m.identifier:
if x.scheme.lower() == 'isbn':
x.content = mi.isbn
if x.scheme.lower() == typ.lower():
x.content = val
has = True
if not has:
m.add('identifier', mi.isbn, scheme='ISBN')
elif override_input_metadata:
m.add('identifier', val, scheme=typ.upper())
if override_input_metadata and not set_isbn:
m.filter('identifier', lambda x: x.scheme.lower() == 'isbn')
if not mi.is_null('language'):
m.clear('language')

View File

@ -15,7 +15,6 @@ APP_UID = 'libprs500'
from calibre.constants import (islinux, iswindows, isbsd, isfrozen, isosx,
config_dir)
from calibre.utils.config import Config, ConfigProxy, dynamic, JSONConfig
from calibre.utils.localization import set_qt_translator
from calibre.ebooks.metadata import MetaInformation
from calibre.utils.date import UNDEFINED_DATE
@ -631,6 +630,22 @@ class ResizableDialog(QDialog):
nw = min(self.width(), nw)
self.resize(nw, nh)
class Translator(QTranslator):
'''
Translator to load translations for strings in Qt from the calibre
translations. Does not support advanced features of Qt like disambiguation
and plural forms.
'''
def translate(self, *args, **kwargs):
try:
src = unicode(args[1])
except:
return u''
t = _
return t(src)
gui_thread = None
qt_app = None
@ -677,9 +692,8 @@ class Application(QApplication):
def load_translations(self):
if self._translator is not None:
self.removeTranslator(self._translator)
self._translator = QTranslator(self)
if set_qt_translator(self._translator):
self.installTranslator(self._translator)
self._translator = Translator(self)
self.installTranslator(self._translator)
def event(self, e):
if callable(self.file_event_hook) and e.type() == QEvent.FileOpen:

View File

@ -12,7 +12,7 @@ from PyQt4.Qt import QModelIndex, QMenu
from calibre.gui2 import error_dialog, Dispatcher
from calibre.gui2.tools import convert_single_ebook, convert_bulk_ebook
from calibre.utils.config import prefs
from calibre.utils.config import prefs, tweaks
from calibre.gui2.actions import InterfaceAction
from calibre.customize.ui import plugin_for_input_format
@ -118,6 +118,8 @@ class ConvertAction(InterfaceAction):
def queue_convert_jobs(self, jobs, changed, bad, rows, previous,
converted_func, extra_job_args=[]):
for func, args, desc, fmt, id, temp_files in jobs:
func, _, same_fmt = func.partition(':')
same_fmt = same_fmt == 'same_fmt'
input_file = args[0]
input_fmt = os.path.splitext(input_file)[1]
core_usage = 1
@ -131,6 +133,7 @@ class ConvertAction(InterfaceAction):
job = self.gui.job_manager.run_job(Dispatcher(converted_func),
func, args=args, description=desc,
core_usage=core_usage)
job.conversion_of_same_fmt = same_fmt
args = [temp_files, fmt, id]+extra_job_args
self.conversion_jobs[job] = tuple(args)
@ -166,14 +169,18 @@ class ConvertAction(InterfaceAction):
if job.failed:
self.gui.job_exception(job)
return
same_fmt = getattr(job, 'conversion_of_same_fmt', False)
fmtf = temp_files[-1].name
if os.stat(fmtf).st_size < 1:
raise Exception(_('Empty output file, '
'probably the conversion process crashed'))
db = self.gui.current_db
if same_fmt and tweaks['save_original_format']:
db.save_original_format(book_id, fmt, notify=False)
with open(temp_files[-1].name, 'rb') as data:
self.gui.library_view.model().db.add_format(book_id, \
fmt, data, index_is_id=True)
db.add_format(book_id, fmt, data, index_is_id=True)
self.gui.status_bar.show_message(job.description + \
(' completed'), 2000)
finally:

View File

@ -81,7 +81,7 @@ class MultiDeleter(QObject):
class DeleteAction(InterfaceAction):
name = 'Remove Books'
action_spec = (_('Remove books'), 'trash.png', None, _('Del'))
action_spec = (_('Remove books'), 'trash.png', None, 'Del')
action_type = 'current'
def genesis(self):

View File

@ -128,7 +128,8 @@ class ViewAction(InterfaceAction):
self.gui.unsetCursor()
def _view_file(self, name):
ext = os.path.splitext(name)[1].upper().replace('.', '')
ext = os.path.splitext(name)[1].upper().replace('.',
'').replace('ORIGINAL_', '')
viewer = 'lrfviewer' if ext == 'LRF' else 'ebook-viewer'
internal = ext in config['internally_viewed_formats']
self._launch_viewer(name, viewer, internal)

View File

@ -24,7 +24,10 @@ class LookAndFeelWidget(Widget, Ui_Form):
'font_size_mapping', 'line_height', 'minimum_line_height',
'linearize_tables', 'smarten_punctuation',
'disable_font_rescaling', 'insert_blank_line',
'remove_paragraph_spacing', 'remove_paragraph_spacing_indent_size','input_encoding',
'remove_paragraph_spacing',
'remove_paragraph_spacing_indent_size',
'insert_blank_line_size',
'input_encoding',
'asciiize', 'keep_ligatures']
)
for val, text in [

View File

@ -6,7 +6,7 @@
<rect>
<x>0</x>
<y>0</y>
<width>600</width>
<width>642</width>
<height>500</height>
</rect>
</property>
@ -31,7 +31,7 @@
</property>
</widget>
</item>
<item row="1" column="1" colspan="2">
<item row="1" column="1">
<widget class="QDoubleSpinBox" name="opt_base_font_size">
<property name="suffix">
<string> pt</string>
@ -97,6 +97,29 @@
</item>
</layout>
</item>
<item row="3" column="0">
<widget class="QLabel" name="label_6">
<property name="text">
<string>Minimum &amp;line height:</string>
</property>
<property name="buddy">
<cstring>opt_minimum_line_height</cstring>
</property>
</widget>
</item>
<item row="3" column="1">
<widget class="QDoubleSpinBox" name="opt_minimum_line_height">
<property name="suffix">
<string> %</string>
</property>
<property name="decimals">
<number>1</number>
</property>
<property name="maximum">
<double>900.000000000000000</double>
</property>
</widget>
</item>
<item row="4" column="0">
<widget class="QLabel" name="label">
<property name="text">
@ -107,7 +130,7 @@
</property>
</widget>
</item>
<item row="4" column="1" colspan="2">
<item row="4" column="1">
<widget class="QDoubleSpinBox" name="opt_line_height">
<property name="suffix">
<string> pt</string>
@ -127,6 +150,13 @@
</property>
</widget>
</item>
<item row="5" column="1" colspan="2">
<widget class="EncodingComboBox" name="opt_input_encoding">
<property name="editable">
<bool>true</bool>
</property>
</widget>
</item>
<item row="6" column="0" colspan="2">
<widget class="QCheckBox" name="opt_remove_paragraph_spacing">
<property name="text">
@ -134,48 +164,58 @@
</property>
</widget>
</item>
<item row="6" column="2" colspan="2">
<layout class="QHBoxLayout" name="horizontalLayout_2">
<item>
<widget class="QLabel" name="label_4">
<property name="text">
<string>Indent size:</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
</widget>
</item>
<item>
<widget class="QDoubleSpinBox" name="opt_remove_paragraph_spacing_indent_size">
<property name="toolTip">
<string>&lt;p&gt;When calibre removes inter paragraph spacing, it automatically sets a paragraph indent, to ensure that paragraphs can be easily distinguished. This option controls the width of that indent.</string>
</property>
<property name="suffix">
<string> em</string>
</property>
<property name="decimals">
<number>1</number>
</property>
</widget>
</item>
</layout>
</item>
<item row="7" column="0">
<widget class="QLabel" name="label_5">
<item row="7" column="0" colspan="2">
<widget class="QCheckBox" name="opt_insert_blank_line">
<property name="text">
<string>Text justification:</string>
<string>Insert &amp;blank line between paragraphs</string>
</property>
</widget>
</item>
<item row="7" column="4">
<widget class="QDoubleSpinBox" name="opt_insert_blank_line_size">
<property name="suffix">
<string> em</string>
</property>
<property name="decimals">
<number>1</number>
</property>
</widget>
</item>
<item row="8" column="0">
<widget class="QLabel" name="label_5">
<property name="text">
<string>Text &amp;justification:</string>
</property>
<property name="buddy">
<cstring>opt_change_justification</cstring>
</property>
</widget>
</item>
<item row="8" column="2" colspan="3">
<widget class="QComboBox" name="opt_change_justification"/>
</item>
<item row="9" column="0">
<widget class="QCheckBox" name="opt_linearize_tables">
<property name="text">
<string>&amp;Linearize tables</string>
</property>
</widget>
</item>
<item row="11" column="0" colspan="4">
<item row="9" column="1" colspan="4">
<widget class="QCheckBox" name="opt_asciiize">
<property name="text">
<string>&amp;Transliterate unicode characters to ASCII</string>
</property>
</widget>
</item>
<item row="10" column="1" colspan="2">
<widget class="QCheckBox" name="opt_keep_ligatures">
<property name="text">
<string>Keep &amp;ligatures</string>
</property>
</widget>
</item>
<item row="12" column="0" colspan="5">
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>Extra &amp;CSS</string>
@ -187,27 +227,16 @@
</layout>
</widget>
</item>
<item row="7" column="2" colspan="2">
<widget class="QComboBox" name="opt_change_justification"/>
</item>
<item row="8" column="1" colspan="3">
<widget class="QCheckBox" name="opt_asciiize">
<property name="text">
<string>&amp;Transliterate unicode characters to ASCII</string>
<item row="6" column="4">
<widget class="QDoubleSpinBox" name="opt_remove_paragraph_spacing_indent_size">
<property name="toolTip">
<string>&lt;p&gt;When calibre removes inter paragraph spacing, it automatically sets a paragraph indent, to ensure that paragraphs can be easily distinguished. This option controls the width of that indent.</string>
</property>
</widget>
</item>
<item row="9" column="0">
<widget class="QCheckBox" name="opt_insert_blank_line">
<property name="text">
<string>Insert &amp;blank line</string>
<property name="suffix">
<string> em</string>
</property>
</widget>
</item>
<item row="9" column="1" colspan="2">
<widget class="QCheckBox" name="opt_keep_ligatures">
<property name="text">
<string>Keep &amp;ligatures</string>
<property name="decimals">
<number>1</number>
</property>
</widget>
</item>
@ -218,33 +247,29 @@
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QLabel" name="label_6">
<item row="6" column="3">
<widget class="QLabel" name="label_4">
<property name="text">
<string>Minimum &amp;line height:</string>
<string>&amp;Indent size:</string>
</property>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
<property name="buddy">
<cstring>opt_minimum_line_height</cstring>
<cstring>opt_remove_paragraph_spacing_indent_size</cstring>
</property>
</widget>
</item>
<item row="3" column="1" colspan="2">
<widget class="QDoubleSpinBox" name="opt_minimum_line_height">
<property name="suffix">
<string> %</string>
<item row="7" column="3">
<widget class="QLabel" name="label_7">
<property name="text">
<string>&amp;Line size:</string>
</property>
<property name="decimals">
<number>1</number>
<property name="alignment">
<set>Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter</set>
</property>
<property name="maximum">
<double>900.000000000000000</double>
</property>
</widget>
</item>
<item row="5" column="1" colspan="3">
<widget class="EncodingComboBox" name="opt_input_encoding">
<property name="editable">
<bool>true</bool>
<property name="buddy">
<cstring>opt_insert_blank_line_size</cstring>
</property>
</widget>
</item>

View File

@ -24,7 +24,7 @@ class PluginWidget(Widget, Ui_Form):
def __init__(self, parent, get_option, get_help, db=None, book_id=None):
Widget.__init__(self, parent,
['prefer_author_sort', 'rescale_images', 'toc_title',
'mobi_ignore_margins',
'mobi_ignore_margins', 'mobi_toc_at_start',
'dont_compress', 'no_inline_toc', 'masthead_font','personal_doc']
)
from calibre.utils.fonts import fontconfig

View File

@ -27,21 +27,21 @@
<item row="1" column="1">
<widget class="QLineEdit" name="opt_toc_title"/>
</item>
<item row="2" column="0" colspan="2">
<item row="4" column="0" colspan="2">
<widget class="QCheckBox" name="opt_rescale_images">
<property name="text">
<string>Rescale images for &amp;Palm devices</string>
</property>
</widget>
</item>
<item row="3" column="0" colspan="2">
<item row="5" column="0" colspan="2">
<widget class="QCheckBox" name="opt_prefer_author_sort">
<property name="text">
<string>Use author &amp;sort for author</string>
</property>
</widget>
</item>
<item row="4" column="0">
<item row="6" column="0">
<widget class="QCheckBox" name="opt_dont_compress">
<property name="text">
<string>Disable compression of the file contents</string>
@ -55,7 +55,7 @@
</property>
</widget>
</item>
<item row="6" column="0" colspan="2">
<item row="8" column="0" colspan="2">
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>Kindle options</string>
@ -101,7 +101,7 @@
</layout>
</widget>
</item>
<item row="7" column="0">
<item row="9" column="0">
<spacer name="verticalSpacer_2">
<property name="orientation">
<enum>Qt::Vertical</enum>
@ -114,7 +114,14 @@
</property>
</spacer>
</item>
<item row="5" column="0">
<item row="2" column="0" colspan="2">
<widget class="QCheckBox" name="opt_mobi_toc_at_start">
<property name="text">
<string>Put generated Table of Contents at &amp;start of book instead of end</string>
</property>
</widget>
</item>
<item row="3" column="0">
<widget class="QCheckBox" name="opt_mobi_ignore_margins">
<property name="text">
<string>Ignore &amp;margins</string>

View File

@ -7,8 +7,8 @@ __docformat__ = 'restructuredtext en'
import re, os
from PyQt4.QtCore import SIGNAL, Qt, pyqtSignal
from PyQt4.QtGui import QDialog, QWidget, QDialogButtonBox, \
QBrush, QTextCursor, QTextEdit
from PyQt4.QtGui import (QDialog, QWidget, QDialogButtonBox,
QBrush, QTextCursor, QTextEdit)
from calibre.gui2.convert.regex_builder_ui import Ui_RegexBuilder
from calibre.gui2.convert.xexp_edit_ui import Ui_Form as Ui_Edit
@ -16,6 +16,7 @@ from calibre.gui2 import error_dialog, choose_files
from calibre.ebooks.oeb.iterator import EbookIterator
from calibre.ebooks.conversion.preprocess import HTMLPreProcessor
from calibre.gui2.dialogs.choose_format import ChooseFormatDialog
from calibre.constants import iswindows
class RegexBuilder(QDialog, Ui_RegexBuilder):
@ -134,8 +135,18 @@ class RegexBuilder(QDialog, Ui_RegexBuilder):
_('Cannot build regex using the GUI builder without a book.'),
show=True)
return False
fpath = db.format(book_id, format, index_is_id=True,
as_path=True)
try:
fpath = db.format(book_id, format, index_is_id=True,
as_path=True)
except OSError:
if iswindows:
import traceback
error_dialog(self, _('Could not open file'),
_('Could not open the file, do you have it open in'
' another program?'), show=True,
det_msg=traceback.format_exc())
return False
raise
try:
self.open_book(fpath)
finally:

View File

@ -723,6 +723,7 @@ class BulkSeries(BulkBase):
layout.addWidget(self.force_number)
self.series_start_number = QSpinBox(parent)
self.series_start_number.setMinimum(1)
self.series_start_number.setMaximum(9999999)
self.series_start_number.setProperty("value", 1)
layout.addWidget(self.series_start_number)
layout.addItem(QSpacerItem(20, 10, QSizePolicy.Expanding, QSizePolicy.Minimum))

View File

@ -29,9 +29,6 @@
<property name="alternatingRowColors">
<bool>true</bool>
</property>
<property name="selectionMode">
<enum>QAbstractItemView::SingleSelection</enum>
</property>
<property name="selectionBehavior">
<enum>QAbstractItemView::SelectRows</enum>
</property>
@ -46,7 +43,7 @@
<item>
<widget class="QPushButton" name="kill_button">
<property name="text">
<string>&amp;Stop selected job</string>
<string>&amp;Stop selected jobs</string>
</property>
</widget>
</item>

View File

@ -172,8 +172,9 @@ class JobManager(QAbstractTableModel): # {{{
if job.is_finished:
self.job_done.emit(len(self.unfinished_jobs()))
if needs_reset:
self.layoutAboutToBeChanged.emit()
self.jobs.sort()
self.reset()
self.layoutChanged.emit()
else:
for job in jobs:
idx = self.jobs.index(job)
@ -267,7 +268,8 @@ class JobManager(QAbstractTableModel): # {{{
# }}}
# Jobs UI {{{
class ProgressBarDelegate(QAbstractItemDelegate):
class ProgressBarDelegate(QAbstractItemDelegate): # {{{
def sizeHint(self, option, index):
return QSize(120, 30)
@ -284,8 +286,9 @@ class ProgressBarDelegate(QAbstractItemDelegate):
opts.progress = percent
opts.text = QString(_('Unavailable') if percent == 0 else '%d%%'%percent)
QApplication.style().drawControl(QStyle.CE_ProgressBar, opts, painter)
# }}}
class DetailView(QDialog, Ui_Dialog):
class DetailView(QDialog, Ui_Dialog): # {{{
def __init__(self, parent, job):
QDialog.__init__(self, parent)
@ -318,8 +321,9 @@ class DetailView(QDialog, Ui_Dialog):
self.next_pos = f.tell()
if more:
self.log.appendPlainText(more.decode('utf-8', 'replace'))
# }}}
class JobsButton(QFrame):
class JobsButton(QFrame): # {{{
def __init__(self, horizontal=False, size=48, parent=None):
QFrame.__init__(self, parent)
@ -404,6 +408,7 @@ class JobsButton(QFrame):
self.stop()
QCoreApplication.instance().alert(self, 5000)
# }}}
class JobsDialog(QDialog, Ui_JobsDialog):
@ -446,7 +451,6 @@ class JobsDialog(QDialog, Ui_JobsDialog):
except:
pass
def show_job_details(self, index):
row = index.row()
job = self.jobs_view.model().row_to_job(row)
@ -455,18 +459,23 @@ class JobsDialog(QDialog, Ui_JobsDialog):
d.timer.stop()
def show_details(self, *args):
for index in self.jobs_view.selectedIndexes():
index = self.jobs_view.currentIndex()
if index.isValid():
self.show_job_details(index)
return
def kill_job(self, *args):
if question_dialog(self, _('Are you sure?'), _('Do you really want to stop the selected job?')):
for index in self.jobs_view.selectionModel().selectedRows():
row = index.row()
rows = [index.row() for index in
self.jobs_view.selectionModel().selectedRows()]
if question_dialog(self, _('Are you sure?'),
ngettext('Do you really want to stop the selected job?',
'Do you really want to stop all the selected jobs?',
len(rows))):
for row in rows:
self.model.kill_job(row, self)
def kill_all_jobs(self, *args):
if question_dialog(self, _('Are you sure?'), _('Do you really want to stop all non-device jobs?')):
if question_dialog(self, _('Are you sure?'),
_('Do you really want to stop all non-device jobs?')):
self.model.kill_all_jobs()
def closeEvent(self, e):

View File

@ -16,7 +16,7 @@ from PyQt4.Qt import (QColor, Qt, QModelIndex, QSize, QApplication,
from calibre.gui2 import UNDEFINED_QDATE, error_dialog
from calibre.gui2.widgets import EnLineEdit
from calibre.gui2.complete import MultiCompleteLineEdit
from calibre.gui2.complete import MultiCompleteLineEdit, MultiCompleteComboBox
from calibre.utils.date import now, format_date
from calibre.utils.config import tweaks
from calibre.utils.formatter import validation_formatter
@ -166,13 +166,26 @@ class TextDelegate(QStyledItemDelegate): # {{{
def createEditor(self, parent, option, index):
if self.auto_complete_function:
editor = MultiCompleteLineEdit(parent)
editor = MultiCompleteComboBox(parent)
editor.set_separator(None)
complete_items = [i[1] for i in self.auto_complete_function()]
editor.update_items_cache(complete_items)
for item in sorted(complete_items, key=sort_key):
editor.addItem(item)
ct = index.data(Qt.DisplayRole).toString()
editor.setEditText(ct)
editor.lineEdit().selectAll()
else:
editor = EnLineEdit(parent)
return editor
def setModelData(self, editor, model, index):
if isinstance(editor, MultiCompleteComboBox):
val = editor.lineEdit().text()
model.setData(index, QVariant(val), Qt.EditRole)
else:
QStyledItemDelegate.setModelData(self, editor, model, index)
#}}}
class CompleteDelegate(QStyledItemDelegate): # {{{
@ -188,7 +201,7 @@ class CompleteDelegate(QStyledItemDelegate): # {{{
def createEditor(self, parent, option, index):
if self.db and hasattr(self.db, self.items_func_name):
col = index.model().column_map[index.column()]
editor = MultiCompleteLineEdit(parent)
editor = MultiCompleteComboBox(parent)
editor.set_separator(self.sep)
editor.set_space_before_sep(self.space_before_sep)
if self.sep == '&':
@ -199,9 +212,21 @@ class CompleteDelegate(QStyledItemDelegate): # {{{
all_items = list(self.db.all_custom(
label=self.db.field_metadata.key_to_label(col)))
editor.update_items_cache(all_items)
for item in sorted(all_items, key=sort_key):
editor.addItem(item)
ct = index.data(Qt.DisplayRole).toString()
editor.setEditText(ct)
editor.lineEdit().selectAll()
else:
editor = EnLineEdit(parent)
return editor
def setModelData(self, editor, model, index):
if isinstance(editor, MultiCompleteComboBox):
val = editor.lineEdit().text()
model.setData(index, QVariant(val), Qt.EditRole)
else:
QStyledItemDelegate.setModelData(self, editor, model, index)
# }}}
class CcDateDelegate(QStyledItemDelegate): # {{{

View File

@ -11,10 +11,10 @@ import textwrap, re, os
from PyQt4.Qt import (Qt, QDateEdit, QDate, pyqtSignal, QMessageBox,
QIcon, QToolButton, QWidget, QLabel, QGridLayout, QApplication,
QDoubleSpinBox, QListWidgetItem, QSize, QPixmap, QDialog,
QPushButton, QSpinBox, QLineEdit, QSizePolicy, QDialogButtonBox)
QDoubleSpinBox, QListWidgetItem, QSize, QPixmap, QDialog, QMenu,
QPushButton, QSpinBox, QLineEdit, QSizePolicy, QDialogButtonBox, QAction)
from calibre.gui2.widgets import EnLineEdit, FormatList, ImageView
from calibre.gui2.widgets import EnLineEdit, FormatList as _FormatList, ImageView
from calibre.gui2.complete import MultiCompleteLineEdit, MultiCompleteComboBox
from calibre.utils.icu import sort_key
from calibre.utils.config import tweaks, prefs
@ -33,6 +33,7 @@ from calibre.gui2.comments_editor import Editor
from calibre.library.comments import comments_to_html
from calibre.gui2.dialogs.tag_editor import TagEditor
from calibre.utils.icu import strcmp
from calibre.ptempfile import PersistentTemporaryFile
def save_dialog(parent, title, msg, det_msg=''):
d = QMessageBox(parent)
@ -572,7 +573,9 @@ class BuddyLabel(QLabel): # {{{
self.setAlignment(Qt.AlignRight|Qt.AlignVCenter)
# }}}
class Format(QListWidgetItem): # {{{
# Formats {{{
class Format(QListWidgetItem):
def __init__(self, parent, ext, size, path=None, timestamp=None):
self.path = path
@ -588,13 +591,52 @@ class Format(QListWidgetItem): # {{{
self.setToolTip(text)
self.setStatusTip(text)
# }}}
class OrigAction(QAction):
class FormatsManager(QWidget): # {{{
restore_fmt = pyqtSignal(object)
def __init__(self, fmt, parent):
self.fmt = fmt.replace('ORIGINAL_', '')
QAction.__init__(self, _('Restore %s from the original')%self.fmt, parent)
self.triggered.connect(self._triggered)
def _triggered(self):
self.restore_fmt.emit(self.fmt)
class FormatList(_FormatList):
restore_fmt = pyqtSignal(object)
def __init__(self, parent):
_FormatList.__init__(self, parent)
self.setContextMenuPolicy(Qt.DefaultContextMenu)
def contextMenuEvent(self, event):
originals = [self.item(x).ext.upper() for x in range(self.count())]
originals = [x for x in originals if x.startswith('ORIGINAL_')]
if not originals:
return
self.cm = cm = QMenu(self)
for fmt in originals:
action = OrigAction(fmt, cm)
action.restore_fmt.connect(self.restore_fmt)
cm.addAction(action)
cm.popup(event.globalPos())
event.accept()
def remove_format(self, fmt):
for i in range(self.count()):
f = self.item(i)
if f.ext.upper() == fmt.upper():
self.takeItem(i)
break
class FormatsManager(QWidget):
def __init__(self, parent, copy_fmt):
QWidget.__init__(self, parent)
self.dialog = parent
self.copy_fmt = copy_fmt
self.changed = False
self.l = l = QGridLayout()
@ -628,6 +670,7 @@ class FormatsManager(QWidget): # {{{
self.formats = FormatList(self)
self.formats.setAcceptDrops(True)
self.formats.formats_dropped.connect(self.formats_dropped)
self.formats.restore_fmt.connect(self.restore_fmt)
self.formats.delete_format.connect(self.remove_format)
self.formats.itemDoubleClicked.connect(self.show_format)
self.formats.setDragDropMode(self.formats.DropOnly)
@ -640,7 +683,7 @@ class FormatsManager(QWidget): # {{{
l.addWidget(self.remove_format_button, 2, 2, 1, 1)
l.addWidget(self.formats, 0, 1, 3, 1)
self.temp_files = []
def initialize(self, db, id_):
self.changed = False
@ -694,6 +737,16 @@ class FormatsManager(QWidget): # {{{
[(_('Books'), BOOK_EXTENSIONS)])
self._add_formats(files)
def restore_fmt(self, fmt):
pt = PersistentTemporaryFile(suffix='_restore_fmt.'+fmt.lower())
ofmt = 'ORIGINAL_'+fmt
with pt:
self.copy_fmt(ofmt, pt)
self._add_formats((pt.name,))
self.temp_files.append(pt.name)
self.changed = True
self.formats.remove_format(ofmt)
def _add_formats(self, paths):
added = False
if not paths:
@ -774,6 +827,13 @@ class FormatsManager(QWidget): # {{{
def break_cycles(self):
self.dialog = None
self.copy_fmt = None
for name in self.temp_files:
try:
os.remove(name)
except:
pass
self.temp_files = []
# }}}
class Cover(ImageView): # {{{

View File

@ -145,7 +145,7 @@ class MetadataSingleDialogBase(ResizableDialog):
self.series_index = SeriesIndexEdit(self, self.series)
self.basic_metadata_widgets.extend([self.series, self.series_index])
self.formats_manager = FormatsManager(self)
self.formats_manager = FormatsManager(self, self.copy_fmt)
self.basic_metadata_widgets.append(self.formats_manager)
self.formats_manager.metadata_from_format_button.clicked.connect(
self.metadata_from_format)
@ -240,6 +240,8 @@ class MetadataSingleDialogBase(ResizableDialog):
else:
self.view_format.emit(self.book_id, fmt)
def copy_fmt(self, fmt, f):
self.db.copy_format_to(self.book_id, fmt, f, index_is_id=True)
def do_layout(self):
raise NotImplementedError()

View File

@ -105,13 +105,18 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
r('cover_flow_queue_length', config, restart_required=True)
def get_esc_lang(l):
if l == 'en':
return 'English'
return get_language(l)
lang = get_lang()
if lang is None or lang not in available_translations():
lang = 'en'
items = [(l, get_language(l)) for l in available_translations() \
items = [(l, get_esc_lang(l)) for l in available_translations() \
if l != lang]
if lang != 'en':
items.append(('en', get_language('en')))
items.append(('en', get_esc_lang('en')))
items.sort(cmp=lambda x, y: cmp(x[1].lower(), y[1].lower()))
choices = [(y, x) for x, y in items]
# Default language is the autodetected one

View File

@ -14,30 +14,18 @@
<string>Form</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="0" column="0">
<widget class="QLabel" name="label">
<property name="font">
<font>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="text">
<string>Choose the &amp;toolbar to customize:</string>
</property>
<property name="buddy">
<cstring>what</cstring>
</property>
</widget>
</item>
<item row="0" column="1" colspan="3">
<item row="0" column="0" colspan="5">
<widget class="QComboBox" name="what">
<property name="font">
<font>
<pointsize>20</pointsize>
<weight>75</weight>
<bold>true</bold>
</font>
</property>
<property name="toolTip">
<string>Choose the toolbar to customize</string>
</property>
<property name="sizeAdjustPolicy">
<enum>QComboBox::AdjustToMinimumContentsLengthWithIcon</enum>
</property>
@ -46,7 +34,7 @@
</property>
</widget>
</item>
<item row="1" column="0" rowspan="2">
<item row="1" column="0" colspan="2">
<widget class="QGroupBox" name="groupBox">
<property name="title">
<string>A&amp;vailable actions</string>
@ -74,7 +62,67 @@
</layout>
</widget>
</item>
<item row="1" column="2" rowspan="2">
<item row="1" column="2">
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<widget class="QToolButton" name="add_action_button">
<property name="toolTip">
<string>Add selected actions to toolbar</string>
</property>
<property name="text">
<string>...</string>
</property>
<property name="icon">
<iconset resource="../../../../resources/images.qrc">
<normaloff>:/images/forward.png</normaloff>:/images/forward.png</iconset>
</property>
<property name="iconSize">
<size>
<width>24</width>
<height>24</height>
</size>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QToolButton" name="remove_action_button">
<property name="toolTip">
<string>Remove selected actions from toolbar</string>
</property>
<property name="text">
<string>...</string>
</property>
<property name="icon">
<iconset resource="../../../../resources/images.qrc">
<normaloff>:/images/back.png</normaloff>:/images/back.png</iconset>
</property>
<property name="iconSize">
<size>
<width>24</width>
<height>24</height>
</size>
</property>
</widget>
</item>
</layout>
</item>
<item row="1" column="3" colspan="2">
<widget class="QGroupBox" name="groupBox_2">
<property name="title">
<string>&amp;Current actions</string>
@ -162,66 +210,6 @@
</layout>
</widget>
</item>
<item row="1" column="1" rowspan="2">
<layout class="QVBoxLayout" name="verticalLayout_3">
<item>
<widget class="QToolButton" name="add_action_button">
<property name="toolTip">
<string>Add selected actions to toolbar</string>
</property>
<property name="text">
<string>...</string>
</property>
<property name="icon">
<iconset resource="../../../../resources/images.qrc">
<normaloff>:/images/forward.png</normaloff>:/images/forward.png</iconset>
</property>
<property name="iconSize">
<size>
<width>24</width>
<height>24</height>
</size>
</property>
</widget>
</item>
<item>
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QToolButton" name="remove_action_button">
<property name="toolTip">
<string>Remove selected actions from toolbar</string>
</property>
<property name="text">
<string>...</string>
</property>
<property name="icon">
<iconset resource="../../../../resources/images.qrc">
<normaloff>:/images/back.png</normaloff>:/images/back.png</iconset>
</property>
<property name="iconSize">
<size>
<width>24</width>
<height>24</height>
</size>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<resources>

View File

@ -53,7 +53,9 @@ def convert_single_ebook(parent, db, book_ids, auto_conversion=False, # {{{
mi = db.get_metadata(book_id, True)
in_file = PersistentTemporaryFile('.'+d.input_format)
with in_file:
db.copy_format_to(book_id, d.input_format, in_file,
input_fmt = db.original_fmt(book_id, d.input_format).lower()
same_fmt = input_fmt == d.output_format.lower()
db.copy_format_to(book_id, input_fmt, in_file,
index_is_id=True)
out_file = PersistentTemporaryFile('.' + d.output_format)
@ -79,7 +81,10 @@ def convert_single_ebook(parent, db, book_ids, auto_conversion=False, # {{{
temp_files.append(d.cover_file)
args = [in_file.name, out_file.name, recs]
temp_files.append(out_file)
jobs.append(('gui_convert_override', args, desc, d.output_format.upper(), book_id, temp_files))
func = 'gui_convert_override'
if same_fmt:
func += ':same_fmt'
jobs.append((func, args, desc, d.output_format.upper(), book_id, temp_files))
changed = True
d.break_cycles()
@ -144,10 +149,12 @@ class QueueBulk(QProgressDialog):
try:
input_format = get_input_format_for_book(self.db, book_id, None)[0]
input_fmt = self.db.original_fmt(book_id, input_format).lower()
same_fmt = input_fmt == self.output_format.lower()
mi, opf_file = create_opf_file(self.db, book_id)
in_file = PersistentTemporaryFile('.'+input_format)
with in_file:
self.db.copy_format_to(book_id, input_format, in_file,
self.db.copy_format_to(book_id, input_fmt, in_file,
index_is_id=True)
out_file = PersistentTemporaryFile('.' + self.output_format)
@ -192,7 +199,10 @@ class QueueBulk(QProgressDialog):
args = [in_file.name, out_file.name, lrecs]
temp_files.append(out_file)
self.jobs.append(('gui_convert_override', args, desc, self.output_format.upper(), book_id, temp_files))
func = 'gui_convert_override'
if same_fmt:
func += ':same_fmt'
self.jobs.append((func, args, desc, self.output_format.upper(), book_id, temp_files))
self.changed = True
self.setValue(self.i)

View File

@ -661,12 +661,13 @@ class EbookViewer(MainWindow, Ui_EbookViewer):
def save_current_position(self):
if not self.get_remember_current_page_opt():
return
try:
pos = self.view.bookmark()
bookmark = '%d#%s'%(self.current_index, pos)
self.iterator.add_bookmark(('calibre_current_page_bookmark', bookmark))
except:
traceback.print_exc()
if hasattr(self, 'current_index'):
try:
pos = self.view.bookmark()
bookmark = '%d#%s'%(self.current_index, pos)
self.iterator.add_bookmark(('calibre_current_page_bookmark', bookmark))
except:
traceback.print_exc()
def load_ebook(self, pathtoebook):
if self.iterator is not None:

View File

@ -604,16 +604,21 @@ class LibraryPage(QWizardPage, LibraryUI):
def init_languages(self):
self.language.blockSignals(True)
self.language.clear()
from calibre.utils.localization import available_translations, \
get_language, get_lang
from calibre.utils.localization import (available_translations,
get_language, get_lang)
lang = get_lang()
if lang is None or lang not in available_translations():
lang = 'en'
self.language.addItem(get_language(lang), QVariant(lang))
items = [(l, get_language(l)) for l in available_translations() \
def get_esc_lang(l):
if l == 'en':
return 'English'
return get_language(l)
self.language.addItem(get_esc_lang(lang), QVariant(lang))
items = [(l, get_esc_lang(l)) for l in available_translations()
if l != lang]
if lang != 'en':
items.append(('en', get_language('en')))
items.append(('en', get_esc_lang('en')))
items.sort(cmp=lambda x, y: cmp(x[1], y[1]))
for item in items:
self.language.addItem(item[1], QVariant(item[0]))

View File

@ -1312,6 +1312,23 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
self.notify('metadata', [id])
return True
def save_original_format(self, book_id, fmt, notify=True):
fmt = fmt.upper()
if 'ORIGINAL' in fmt:
raise ValueError('Cannot save original of an original fmt')
opath = self.format_abspath(book_id, fmt, index_is_id=True)
if opath is None:
return False
nfmt = 'ORIGINAL_'+fmt
with lopen(opath, 'rb') as f:
return self.add_format(book_id, nfmt, f, index_is_id=True, notify=notify)
def original_fmt(self, book_id, fmt):
fmt = fmt
nfmt = ('ORIGINAL_%s'%fmt).upper()
opath = self.format_abspath(book_id, nfmt, index_is_id=True)
return fmt if opath is None else nfmt
def delete_book(self, id, notify=True, commit=True, permanent=False):
'''
Removes book from the result cache and the underlying database.

View File

@ -113,8 +113,9 @@ def config(defaults=None):
help=_('The format in which to display dates. %d - day, %b - month, '
'%Y - year. Default is: %b, %Y'))
x('send_timefmt', default='%b, %Y',
help=_('The format in which to display dates. %d - day, %b - month, '
'%Y - year. Default is: %b, %Y'))
help=_('The format in which to display dates. %(day)s - day,'
' %(month)s - month, %(year)s - year. Default is: %(default)s'
)%dict(day='%d', month='%b', year='%Y', default='%b, %Y'))
x('to_lowercase', default=False,
help=_('Convert paths to lowercase.'))
x('replace_whitespace', default=False,

View File

@ -211,9 +211,9 @@ calibre-dev.bat::
Debugging tips
----------------
Running |app| code in a python debugger is not easy unless you install from source on Linux. However, Python is a
Python is a
dynamically typed language with excellent facilities for introspection. Kovid wrote the core |app| code without once
using a debugger. There are two main strategies to debug |app| code:
using a debugger. There are many strategies to debug |app| code:
Using an interactive python interpreter
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
@ -240,6 +240,12 @@ Similarly, you can start the ebook-viewer as::
calibre-debug -w /path/to/file/to/be/viewed
Using the debugger in PyDev
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
It is possible to get the debugger in PyDev working with the |app| development environment,
see the `forum thread <http://www.mobileread.com/forums/showthread.php?t=143208>`_.
Executing arbitrary scripts in the |app| python environment
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

View File

@ -61,7 +61,7 @@ In the MOBI format, the situation is a little confused. This is because the MOBI
Now it might well seem to you that the MOBI book has two identical TOCs. Remember that one is semantically a content TOC and the other is a metadata TOC, even though both might have exactly the same entries and look the same. One can be accessed directly from the Kindle's menus, the other cannot.
When converting to MOBI, calibre detects the *metadata TOC* in the input document and generates an end-of-file TOC in the output MOBI file. You can turn this off by an option in the MOBI Output settings. You cannot control where this generated TOC will go. Remember this TOC is semantically a *metadata TOC*, in any format other than MOBI it *cannot not be part of the text*. The fact that it is part of the text in MOBI is an accident caused by the limitations of MOBI. If you want a TOC at a particular location in your document text, create one by hand.
When converting to MOBI, calibre detects the *metadata TOC* in the input document and generates an end-of-file TOC in the output MOBI file. You can turn this off by an option in the MOBI Output settings. You can also tell calibre whether to put it and the start or the end of the book via an option in the MOBI Output settings. Remember this TOC is semantically a *metadata TOC*, in any format other than MOBI it *cannot not be part of the text*. The fact that it is part of the text in MOBI is an accident caused by the limitations of MOBI. If you want a TOC at a particular location in your document text, create one by hand. So we strongly recommend that you leave the default as it is, i.e. with the metadata TOC at the end of the book.
If you have a hand edited TOC in the input document, you can use the TOC detection options in calibre to automatically generate the metadata TOC from it. See the conversion section of the User Manual for more details on how to use these options.
@ -281,6 +281,15 @@ I get the error message "Failed to start content server: Port 8080 not free on '
The most likely cause of this is your antivirus program. Try temporarily disabling it and see if it does the trick.
I cannot send emails using |app|?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Because of the large amount of spam in email, sending email can be tricky as different servers use different strategies to block email spam.
The most common problem is if you are sending email directly (without a mail relay) in |app|. Many servers (for example, Amazon) block email
that does not come from a well known relay. The easiest way around this is to setup a free GMail account and then goto Preferences->Email in |app| and
click the "Use Gmail" button. |app| will then use Gmail to send the mail. Remember to update the email preferences in on your Amazon Kindle page to
allow email sent from your Gmail email address.
Why is my device not detected in linux?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@ -382,7 +391,7 @@ With all this flexibility, it is possible to have |app| manage your author names
Why doesn't |app| let me store books in my own directory structure?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
The whole point of |app|'s library management features is that they provide a search and sort based interface for locating books that is *much* more efficient than any possible directory scheme you could come up with for your collection. Indeed, once you become comfortable using |app|'s interface to find, sort and browse your collection, you wont ever feel the need to hunt through the files on your disk to find a book again. By managing books in its own directory struture of Author -> Title -> Book files, |app| is able to achieve a high level of reliability and standardization. To illustrate why a search/tagging based interface is superior to folders, consider the following. Suppose your book collection is nicely sorted into folders with the following scheme::
The whole point of |app|'s library management features is that they provide a search and sort based interface for locating books that is *much* more efficient than any possible directory scheme you could come up with for your collection. Indeed, once you become comfortable using |app|'s interface to find, sort and browse your collection, you wont ever feel the need to hunt through the files on your disk to find a book again. By managing books in its own directory structure of Author -> Title -> Book files, |app| is able to achieve a high level of reliability and standardization. To illustrate why a search/tagging based interface is superior to folders, consider the following. Suppose your book collection is nicely sorted into folders with the following scheme::
Genre -> Author -> Series -> ReadStatus
@ -392,6 +401,14 @@ Now this makes it very easy to find for example all science fiction books by Isa
In |app|, you would instead use tags to mark genre and read status and then just use a simple search query like ``tag:scifi and not tag:read``. |app| even has a nice graphical interface, so you don't need to learn its search language instead you can just click on tags to include or exclude them from the search.
To those of you that claim that you need access to the filesystem to so that you can have access to your books over the network, |app| has an excellent content server that gives you access to your calibre library over the net.
If you are worried that someday |app| will cease to be developed, leaving all your books marooned in its folder structure, explore the powerful "Save to Disk" feature in |app| that lets you export all your files into a folder structure of arbitrary complexity based on their metadata.
Since I keep getting asked why there are numbers at the end of the title folder name, the reason is for *robustness*. That number is the id number of the book record in the |app| database. The presence of the number allows you to have multiple records with the same title and author names. More importantly, it is part of what allows |app| to magically regenerate the database with all metadata if the database file gets corrupted. Given that |app|'s mission is to get you to stop storing metadata in filenames and stop using the filesystem to find things, the increased robustness afforded by the id numbers is well worth the uglier folder names.
Finally, if you are irrevocably wedded to using the filesystem to store your metadata, feel free to patch your local copy of |app| to use whatever storage scheme you like. But, do not bother me with requests to change the directory structure, **they will be ignored**.
Why doesn't |app| have a column for foo?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|app| is designed to have columns for the most frequently and widely used fields. In addition, you can add any columns you like. Columns can be added via :guilabel:`Preferences->Interface->Add your own columns`.
@ -408,8 +425,9 @@ How do I move my |app| library from one computer to another?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Simply copy the |app| library folder from the old to the new computer. You can find out what the library folder is by clicking the calibre icon in the toolbar. The very first item is the path to the library folder. Now on the new computer, start |app| for the first time. It will run the Welcome Wizard asking you for the location of the |app| library. Point it to the previously copied folder. If the computer you are transferring to already has a calibre installation, then the Welcome wizard wont run. In that case, click the calibre icon in the tooolbar and point it to the newly copied directory. You will now have two calibre libraries on your computer and you can switch between them by clicking the calibre icon on the toolbar.
Note that if you are transferring between different types of computers (for example Windows to OS X) then after doing the above you should also go to :guilabel:`Preferences->Advanced->Miscellaneous` and click the "Check database integrity button". It will warn you about missing files, if any, which you should then transfer by hand.
Note that if you are transferring between different types of computers (for example Windows to OS X) then after doing the above you should also click the arrow next to the calibre icon on the tool bar, select Library Maintenance and run the Check Library action. It will warn you about any problems in your library, which you should fix by hand.
.. note:: A |app| library is just a folder which contains all the book files and their metadata. All the emtadata is stored in a single file called metadata.db, in the top level folder. If this file gets corrupted, you may see an empty list of books in |app|. In this case you can ask |app| to restore your books by clicking the arrow next to the |app| icon on the toolbar and selecting Library Maintenance->Restore Library.
Content From The Web
---------------------

View File

@ -104,7 +104,8 @@ The :guilabel:`Convert books` action has three variations, accessed by the arrow
This allows you to use the search features to limit the books to be catalogued. In addition, if you select multiple books using the mouse,
only those books will be added to the catalog. If you generate the catalog in an ebook format such as EPUB or MOBI,
the next time you connect your ebook reader the catalog will be automatically sent to the device.
For more information on how catalogs work, read the `catalog creation tutorial <http://www.mobileread.com/forums/showthread.php?p=755468#post755468>`_at MobileRead.
For more information on how catalogs work, read the `catalog creation tutorial <http://www.mobileread.com/forums/showthread.php?p=755468#post755468>`_
at MobileRead.
.. _view:

View File

@ -114,7 +114,17 @@ def PersistentTemporaryDirectory(suffix='', prefix='', dir=None):
'''
if dir is None:
dir = base_dir()
tdir = tempfile.mkdtemp(suffix, __appname__+"_"+ __version__+"_" +prefix, dir)
try:
tdir = tempfile.mkdtemp(suffix, __appname__+"_"+ __version__+"_" +prefix, dir)
except ValueError:
global _base_dir
from calibre.constants import filesystem_encoding
base_dir()
if not isinstance(_base_dir, unicode):
_base_dir = _base_dir.decode(filesystem_encoding)
dir = dir.decode(filesystem_encoding)
tdir = tempfile.mkdtemp(suffix, __appname__+"_"+ __version__+"_" +prefix, dir)
atexit.register(remove_dir, tdir)
return tdir

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -4,9 +4,9 @@
#
msgid ""
msgstr ""
"Project-Id-Version: calibre 0.8.9\n"
"POT-Creation-Date: 2011-07-10 13:28+MDT\n"
"PO-Revision-Date: 2011-07-10 13:28+MDT\n"
"Project-Id-Version: calibre 0.8.10\n"
"POT-Creation-Date: 2011-07-15 10:27+MDT\n"
"PO-Revision-Date: 2011-07-15 10:27+MDT\n"
"Last-Translator: Automatically generated\n"
"Language-Team: LANGUAGE\n"
"MIME-Version: 1.0\n"
@ -21,6 +21,9 @@ msgid "Does absolutely nothing"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/customize/__init__.py:59
#: /home/kovid/work/calibre/src/calibre/db/cache.py:98
#: /home/kovid/work/calibre/src/calibre/db/cache.py:101
#: /home/kovid/work/calibre/src/calibre/db/cache.py:112
#: /home/kovid/work/calibre/src/calibre/devices/hanvon/driver.py:99
#: /home/kovid/work/calibre/src/calibre/devices/hanvon/driver.py:100
#: /home/kovid/work/calibre/src/calibre/devices/jetbook/driver.py:74
@ -126,8 +129,8 @@ msgstr ""
#: /home/kovid/work/calibre/src/calibre/ebooks/pdf/writer.py:102
#: /home/kovid/work/calibre/src/calibre/ebooks/rtf/input.py:313
#: /home/kovid/work/calibre/src/calibre/ebooks/rtf/input.py:315
#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:377
#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:385
#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:376
#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:384
#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:156
#: /home/kovid/work/calibre/src/calibre/gui2/actions/edit_metadata.py:376
#: /home/kovid/work/calibre/src/calibre/gui2/actions/edit_metadata.py:379
@ -144,6 +147,7 @@ msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/add_empty_book.py:68
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/book_info.py:128
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/comicconf.py:47
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk.py:766
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler.py:371
#: /home/kovid/work/calibre/src/calibre/gui2/email.py:185
#: /home/kovid/work/calibre/src/calibre/gui2/email.py:200
@ -808,17 +812,44 @@ msgstr ""
msgid "Disable the named plugin"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/db/backend.py:268
#: /home/kovid/work/calibre/src/calibre/db/backend.py:277
#: /home/kovid/work/calibre/src/calibre/db/backend.py:270
#: /home/kovid/work/calibre/src/calibre/db/backend.py:279
#: /home/kovid/work/calibre/src/calibre/gui2/actions/choose_library.py:236
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/choose_library.py:71
#: /home/kovid/work/calibre/src/calibre/gui2/wizard/__init__.py:662
#: /home/kovid/work/calibre/src/calibre/gui2/wizard/__init__.py:667
#: /home/kovid/work/calibre/src/calibre/library/database2.py:130
#: /home/kovid/work/calibre/src/calibre/library/database2.py:139
#, python-format
msgid "Path to library too long. Must be less than %d characters."
msgstr ""
#: /home/kovid/work/calibre/src/calibre/db/cache.py:126
#: /home/kovid/work/calibre/src/calibre/ebooks/metadata/book/base.py:636
#: /home/kovid/work/calibre/src/calibre/gui2/custom_column_widgets.py:66
#: /home/kovid/work/calibre/src/calibre/gui2/custom_column_widgets.py:564
#: /home/kovid/work/calibre/src/calibre/library/database2.py:972
#: /home/kovid/work/calibre/src/calibre/utils/formatter_functions.py:754
#: /home/kovid/work/calibre/src/calibre/utils/formatter_functions.py:766
msgid "Yes"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/db/fields.py:110
#: /home/kovid/work/calibre/src/calibre/library/database2.py:1086
msgid "Main"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/db/fields.py:112
#: /home/kovid/work/calibre/src/calibre/gui2/layout.py:72
#: /home/kovid/work/calibre/src/calibre/library/database2.py:1088
msgid "Card A"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/db/fields.py:114
#: /home/kovid/work/calibre/src/calibre/gui2/layout.py:74
#: /home/kovid/work/calibre/src/calibre/library/database2.py:1090
msgid "Card B"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/debug.py:154
msgid "Debug log"
msgstr ""
@ -929,11 +960,11 @@ msgstr ""
#: /home/kovid/work/calibre/src/calibre/devices/prs505/sony_cache.py:470
#: /home/kovid/work/calibre/src/calibre/devices/usbms/device.py:1073
#: /home/kovid/work/calibre/src/calibre/devices/usbms/device.py:1079
#: /home/kovid/work/calibre/src/calibre/devices/usbms/device.py:1109
#: /home/kovid/work/calibre/src/calibre/devices/usbms/device.py:1114
#: /home/kovid/work/calibre/src/calibre/gui2/actions/fetch_news.py:73
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler.py:452
#: /home/kovid/work/calibre/src/calibre/gui2/tag_browser/model.py:1139
#: /home/kovid/work/calibre/src/calibre/gui2/tag_browser/model.py:1141
#: /home/kovid/work/calibre/src/calibre/gui2/tag_browser/model.py:1132
#: /home/kovid/work/calibre/src/calibre/gui2/tag_browser/model.py:1134
#: /home/kovid/work/calibre/src/calibre/library/database2.py:330
#: /home/kovid/work/calibre/src/calibre/library/database2.py:343
#: /home/kovid/work/calibre/src/calibre/library/database2.py:3011
@ -2225,6 +2256,20 @@ msgstr ""
msgid "Normally, when following links in HTML files calibre does it depth first, i.e. if file A links to B and C, but B links to D, the files are added in the order A, B, D, C. With this option, they will instead be added as A, B, C, D"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/ebooks/htmlz/input.py:62
#, python-format
msgid "Multiple HTML files found in the archive. Only %s will be used."
msgstr ""
#: /home/kovid/work/calibre/src/calibre/ebooks/htmlz/input.py:68
msgid "No top level HTML file found."
msgstr ""
#: /home/kovid/work/calibre/src/calibre/ebooks/htmlz/input.py:71
#, python-format
msgid "Top level HTML file %s is empty"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/ebooks/htmlz/output.py:30
msgid ""
"Specify the handling of CSS. Default is class.\n"
@ -2518,15 +2563,6 @@ msgstr ""
msgid "No"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/ebooks/metadata/book/base.py:636
#: /home/kovid/work/calibre/src/calibre/gui2/custom_column_widgets.py:66
#: /home/kovid/work/calibre/src/calibre/gui2/custom_column_widgets.py:564
#: /home/kovid/work/calibre/src/calibre/library/database2.py:972
#: /home/kovid/work/calibre/src/calibre/utils/formatter_functions.py:754
#: /home/kovid/work/calibre/src/calibre/utils/formatter_functions.py:766
msgid "Yes"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/ebooks/metadata/book/base.py:737
#: /home/kovid/work/calibre/src/calibre/ebooks/pdf/manipulate/info.py:45
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/delete_matching_from_device.py:75
@ -3307,131 +3343,131 @@ msgstr ""
msgid "Do not remove font color from output. This is only useful when txt-output-formatting is set to textile. Textile is the only formatting that supports setting font color. If this option is not specified font color will not be set and default to the color displayed by the reader (generally this is black)."
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:113
#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:112
msgid "Send file to storage card instead of main memory by default"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:115
#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:114
msgid "Confirm before deleting"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:117
#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:116
msgid "Main window geometry"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:119
#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:118
msgid "Notify when a new version is available"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:121
#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:120
msgid "Use Roman numerals for series number"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:123
#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:122
msgid "Sort tags list by name, popularity, or rating"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:125
#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:124
msgid "Match tags by any or all."
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:127
#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:126
msgid "Number of covers to show in the cover browsing mode"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:129
#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:128
msgid "Defaults for conversion to LRF"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:131
#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:130
msgid "Options for the LRF ebook viewer"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:134
#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:133
msgid "Formats that are viewed using the internal viewer"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:136
#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:135
msgid "Columns to be displayed in the book list"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:137
#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:136
msgid "Automatically launch content server on application startup"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:138
#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:137
msgid "Oldest news kept in database"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:139
#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:138
msgid "Show system tray icon"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:141
#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:140
msgid "Upload downloaded news to device"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:143
#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:142
msgid "Delete books from library after uploading to device"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:145
#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:144
msgid "Show the cover flow in a separate window instead of in the main calibre window"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:147
#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:146
msgid "Disable notifications from the system tray icon"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:149
#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:148
msgid "Default action to perform when send to device button is clicked"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:154
#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:153
msgid "Start searching as you type. If this is disabled then search will only take place when the Enter or Return key is pressed."
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:157
#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:156
msgid "When searching, show all books with search results highlighted instead of showing only the matches. You can use the N or F3 keys to go to the next match."
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:176
#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:175
msgid "Maximum number of simultaneous conversion/news download jobs. This number is twice the actual value for historical reasons."
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:179
#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:178
msgid "Download social metadata (tags/rating/etc.)"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:181
#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:180
msgid "Overwrite author and title with new metadata"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:183
#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:182
msgid "Automatically download the cover, if available"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:185
#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:184
msgid "Limit max simultaneous jobs to number of CPUs"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:187
#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:186
msgid "The layout of the user interface"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:189
#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:188
msgid "Show the average rating per item indication in the tag browser"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:191
#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:190
msgid "Disable UI animations"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:196
#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:195
msgid "tag browser categories not to display"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:491
#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:490
msgid "Choose Files"
msgstr ""
@ -3788,7 +3824,7 @@ msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/actions/choose_library.py:235
#: /home/kovid/work/calibre/src/calibre/gui2/actions/choose_library.py:289
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/choose_library.py:70
#: /home/kovid/work/calibre/src/calibre/gui2/wizard/__init__.py:661
#: /home/kovid/work/calibre/src/calibre/gui2/wizard/__init__.py:666
msgid "Too long"
msgstr ""
@ -3852,7 +3888,7 @@ msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/actions/choose_library.py:331
#: /home/kovid/work/calibre/src/calibre/gui2/actions/copy_to_library.py:160
#: /home/kovid/work/calibre/src/calibre/gui2/device.py:741
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk.py:966
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk.py:956
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/restore_library.py:101
#: /home/kovid/work/calibre/src/calibre/gui2/preferences/tweaks.py:277
#: /home/kovid/work/calibre/src/calibre/gui2/preferences/tweaks.py:317
@ -3974,8 +4010,8 @@ msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/plugin_updater.py:674
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/restore_library.py:78
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/user_profiles.py:370
#: /home/kovid/work/calibre/src/calibre/gui2/jobs.py:463
#: /home/kovid/work/calibre/src/calibre/gui2/jobs.py:469
#: /home/kovid/work/calibre/src/calibre/gui2/jobs.py:477
#: /home/kovid/work/calibre/src/calibre/gui2/preferences/columns.py:102
#: /home/kovid/work/calibre/src/calibre/gui2/preferences/metadata_sources.py:93
#: /home/kovid/work/calibre/src/calibre/gui2/preferences/plugins.py:281
@ -4327,6 +4363,7 @@ msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/actions/help.py:16
#: /home/kovid/work/calibre/src/calibre/gui2/preferences/tweaks_ui.py:91
#: /usr/src/qt-everywhere-opensource-src-4.7.2/src/gui/widgets/qdialogbuttonbox.cpp:670
msgid "Help"
msgstr ""
@ -4344,7 +4381,7 @@ msgid "Move to next highlighted match"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/actions/next_match.py:13
#: /home/kovid/work/calibre/src/calibre/gui2/library/delegates.py:390
#: /home/kovid/work/calibre/src/calibre/gui2/library/delegates.py:415
msgid "N"
msgstr ""
@ -6574,9 +6611,9 @@ msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk.py:312
#: /home/kovid/work/calibre/src/calibre/gui2/library/delegates.py:128
#: /home/kovid/work/calibre/src/calibre/gui2/library/delegates.py:148
#: /home/kovid/work/calibre/src/calibre/gui2/library/delegates.py:230
#: /home/kovid/work/calibre/src/calibre/gui2/library/delegates.py:279
#: /home/kovid/work/calibre/src/calibre/gui2/library/delegates.py:283
#: /home/kovid/work/calibre/src/calibre/gui2/library/delegates.py:255
#: /home/kovid/work/calibre/src/calibre/gui2/library/delegates.py:304
#: /home/kovid/work/calibre/src/calibre/gui2/library/delegates.py:308
#: /home/kovid/work/calibre/src/calibre/gui2/metadata/basic_widgets.py:1282
msgid "Undefined"
msgstr ""
@ -6627,19 +6664,19 @@ msgstr ""
msgid "Force numbers to start with "
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/custom_column_widgets.py:793
#: /home/kovid/work/calibre/src/calibre/gui2/custom_column_widgets.py:794
msgid "The enumeration \"{0}\" contains invalid values that will not appear in the list"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/custom_column_widgets.py:837
#: /home/kovid/work/calibre/src/calibre/gui2/custom_column_widgets.py:838
msgid "Remove all tags"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/custom_column_widgets.py:857
#: /home/kovid/work/calibre/src/calibre/gui2/custom_column_widgets.py:858
msgid "tags to add"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/custom_column_widgets.py:864
#: /home/kovid/work/calibre/src/calibre/gui2/custom_column_widgets.py:865
msgid "tags to remove"
msgstr ""
@ -6834,14 +6871,14 @@ msgid "You have enabled the <b>{0}</b> formats for your {1}. The {1} may not sup
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/device_drivers/configwidget.py:148
#: /home/kovid/work/calibre/src/calibre/gui2/library/delegates.py:439
#: /home/kovid/work/calibre/src/calibre/gui2/library/delegates.py:464
#: /home/kovid/work/calibre/src/calibre/gui2/preferences/plugboard.py:275
#: /home/kovid/work/calibre/src/calibre/gui2/preferences/save_template.py:61
msgid "Invalid template"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/device_drivers/configwidget.py:149
#: /home/kovid/work/calibre/src/calibre/gui2/library/delegates.py:440
#: /home/kovid/work/calibre/src/calibre/gui2/library/delegates.py:465
#: /home/kovid/work/calibre/src/calibre/gui2/preferences/plugboard.py:276
#: /home/kovid/work/calibre/src/calibre/gui2/preferences/save_template.py:62
#, python-format
@ -7129,7 +7166,7 @@ msgid "No location selected"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/choose_library.py:100
#: /home/kovid/work/calibre/src/calibre/gui2/wizard/__init__.py:677
#: /home/kovid/work/calibre/src/calibre/gui2/wizard/__init__.py:682
msgid "Bad location"
msgstr ""
@ -7214,6 +7251,7 @@ msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/comments_dialog.py:24
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/template_dialog.py:236
#: /usr/src/qt-everywhere-opensource-src-4.7.2/src/gui/widgets/qdialogbuttonbox.cpp:649
msgid "&OK"
msgstr ""
@ -7221,6 +7259,7 @@ msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/template_dialog.py:237
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/tweak_epub_ui.py:65
#: /home/kovid/work/calibre/src/calibre/gui2/preferences/main.py:233
#: /usr/src/qt-everywhere-opensource-src-4.7.2/src/gui/widgets/qdialogbuttonbox.cpp:658
msgid "&Cancel"
msgstr ""
@ -7347,12 +7386,12 @@ msgid "Copy to author"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/edit_authors_dialog.py:313
#: /home/kovid/work/calibre/src/calibre/gui2/tag_browser/model.py:932
#: /home/kovid/work/calibre/src/calibre/gui2/tag_browser/model.py:925
msgid "Invalid author name"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/edit_authors_dialog.py:314
#: /home/kovid/work/calibre/src/calibre/gui2/tag_browser/model.py:933
#: /home/kovid/work/calibre/src/calibre/gui2/tag_browser/model.py:926
msgid "Author names cannot contain & characters."
msgstr ""
@ -7404,19 +7443,19 @@ msgstr ""
msgid "Details of job"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/jobs_ui.py:49
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/jobs_ui.py:48
msgid "Active Jobs"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/jobs_ui.py:50
msgid "&Stop selected job"
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/jobs_ui.py:49
msgid "&Stop selected jobs"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/jobs_ui.py:51
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/jobs_ui.py:50
msgid "Show job &details"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/jobs_ui.py:52
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/jobs_ui.py:51
msgid "Stop &all non device jobs"
msgstr ""
@ -7538,53 +7577,41 @@ msgstr ""
msgid "You must specify a destination identifier type"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk.py:753
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk.py:772
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk.py:899
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk.py:889
msgid "Search/replace invalid"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk.py:754
#, python-format
msgid "Authors cannot be set to the empty string. Book title %s not processed"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk.py:773
#, python-format
msgid "Title cannot be set to the empty string. Book title %s not processed"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk.py:900
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk.py:890
#, python-format
msgid "Search pattern is invalid: %s"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk.py:952
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk.py:942
#, python-format
msgid ""
"Applying changes to %d books.\n"
"Phase {0} {1}%%."
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk.py:982
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk.py:972
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:587
msgid "Delete saved search/replace"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk.py:983
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk.py:973
msgid "The selected saved search/replace will be deleted. Are you sure?"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk.py:1000
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk.py:1008
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk.py:990
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk.py:998
msgid "Save search/replace"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk.py:1001
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk.py:991
msgid "Search/replace name:"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk.py:1009
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk.py:999
msgid "That saved search/replace already exists and will be overwritten. Are you sure?"
msgstr ""
@ -8893,12 +8920,12 @@ msgid "%(curr)s (was %(initial)s)"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/tag_list_editor.py:86
#: /home/kovid/work/calibre/src/calibre/gui2/tag_browser/model.py:882
#: /home/kovid/work/calibre/src/calibre/gui2/tag_browser/model.py:875
msgid "Item is blank"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/tag_list_editor.py:87
#: /home/kovid/work/calibre/src/calibre/gui2/tag_browser/model.py:883
#: /home/kovid/work/calibre/src/calibre/gui2/tag_browser/model.py:876
msgid "An item cannot be set to nothing. Delete it instead."
msgstr ""
@ -9006,7 +9033,7 @@ msgid "Open Template Editor"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/template_line_editor.py:41
#: /home/kovid/work/calibre/src/calibre/gui2/library/delegates.py:426
#: /home/kovid/work/calibre/src/calibre/gui2/library/delegates.py:451
msgid "Edit template"
msgstr ""
@ -9494,49 +9521,51 @@ msgstr ""
msgid "There are %d waiting jobs:"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/jobs.py:242
#: /home/kovid/work/calibre/src/calibre/gui2/jobs.py:245
#: /home/kovid/work/calibre/src/calibre/gui2/jobs.py:248
#: /home/kovid/work/calibre/src/calibre/gui2/jobs.py:243
#: /home/kovid/work/calibre/src/calibre/gui2/jobs.py:246
#: /home/kovid/work/calibre/src/calibre/gui2/jobs.py:249
msgid "Cannot kill job"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/jobs.py:243
#: /home/kovid/work/calibre/src/calibre/gui2/jobs.py:244
msgid "Cannot kill jobs that communicate with the device"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/jobs.py:246
#: /home/kovid/work/calibre/src/calibre/gui2/jobs.py:247
msgid "Job has already run"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/jobs.py:249
#: /home/kovid/work/calibre/src/calibre/gui2/jobs.py:250
msgid "This job cannot be stopped"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/jobs.py:285
#: /home/kovid/work/calibre/src/calibre/gui2/jobs.py:287
msgid "Unavailable"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/jobs.py:329
#: /home/kovid/work/calibre/src/calibre/gui2/jobs.py:333
msgid "Jobs:"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/jobs.py:331
#: /home/kovid/work/calibre/src/calibre/gui2/jobs.py:335
msgid "Shift+Alt+J"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/jobs.py:348
#: /home/kovid/work/calibre/src/calibre/gui2/jobs.py:352
msgid "Click to see list of jobs"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/jobs.py:417
#: /home/kovid/work/calibre/src/calibre/gui2/jobs.py:422
msgid " - Jobs"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/jobs.py:463
#: /home/kovid/work/calibre/src/calibre/gui2/jobs.py:470
msgid "Do you really want to stop the selected job?"
msgstr ""
msgid_plural "Do you really want to stop all the selected jobs?"
msgstr[0] ""
msgstr[1] ""
#: /home/kovid/work/calibre/src/calibre/gui2/jobs.py:469
#: /home/kovid/work/calibre/src/calibre/gui2/jobs.py:478
msgid "Do you really want to stop all non-device jobs?"
msgstr ""
@ -9552,20 +9581,10 @@ msgstr ""
msgid "Show books in the main memory of the device"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/layout.py:72
#: /home/kovid/work/calibre/src/calibre/library/database2.py:1088
msgid "Card A"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/layout.py:73
msgid "Show books in storage card A"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/layout.py:74
#: /home/kovid/work/calibre/src/calibre/library/database2.py:1090
msgid "Card B"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/layout.py:75
msgid "Show books in storage card B"
msgstr ""
@ -9606,7 +9625,7 @@ msgstr ""
msgid "Copy current search text (instead of search name)"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/library/delegates.py:390
#: /home/kovid/work/calibre/src/calibre/gui2/library/delegates.py:415
msgid "Y"
msgstr ""
@ -9799,7 +9818,7 @@ msgid "Cause a running calibre instance, if any, to be shutdown. Note that if th
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/main.py:69
#: /home/kovid/work/calibre/src/calibre/gui2/wizard/__init__.py:685
#: /home/kovid/work/calibre/src/calibre/gui2/wizard/__init__.py:690
msgid "Calibre Library"
msgstr ""
@ -11223,47 +11242,47 @@ msgstr ""
msgid "Wide"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/preferences/look_feel.py:129
#: /home/kovid/work/calibre/src/calibre/gui2/preferences/look_feel.py:134
msgid "Off"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/preferences/look_feel.py:129
#: /home/kovid/work/calibre/src/calibre/gui2/preferences/look_feel.py:134
msgid "Small"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/preferences/look_feel.py:130
#: /home/kovid/work/calibre/src/calibre/gui2/preferences/look_feel.py:135
msgid "Large"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/preferences/look_feel.py:130
#: /home/kovid/work/calibre/src/calibre/gui2/preferences/look_feel.py:135
msgid "Medium"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/preferences/look_feel.py:133
#: /home/kovid/work/calibre/src/calibre/gui2/preferences/look_feel.py:138
msgid "Always"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/preferences/look_feel.py:133
#: /home/kovid/work/calibre/src/calibre/gui2/preferences/look_feel.py:138
msgid "If there is enough room"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/preferences/look_feel.py:134
#: /home/kovid/work/calibre/src/calibre/gui2/preferences/look_feel.py:139
msgid "Never"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/preferences/look_feel.py:137
#: /home/kovid/work/calibre/src/calibre/gui2/preferences/look_feel.py:142
msgid "By first letter"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/preferences/look_feel.py:137
#: /home/kovid/work/calibre/src/calibre/gui2/preferences/look_feel.py:142
msgid "Disabled"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/preferences/look_feel.py:138
#: /home/kovid/work/calibre/src/calibre/gui2/preferences/look_feel.py:143
msgid "Partitioned"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/preferences/look_feel.py:167
#: /home/kovid/work/calibre/src/calibre/gui2/preferences/look_feel.py:172
msgid "Column coloring"
msgstr ""
@ -11933,6 +11952,7 @@ msgid ""
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/preferences/search_ui.py:131
#: /usr/src/qt-everywhere-opensource-src-4.7.2/src/gui/widgets/qdialogbuttonbox.cpp:652
msgid "&Save"
msgstr ""
@ -12658,6 +12678,7 @@ msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/store/search_ui.py:114
#: /home/kovid/work/calibre/src/calibre/gui2/store/stores/mobileread/store_dialog_ui.py:79
#: /home/kovid/work/calibre/src/calibre/gui2/store/web_store_dialog_ui.py:63
#: /usr/src/qt-everywhere-opensource-src-4.7.2/src/gui/widgets/qdialogbuttonbox.cpp:661
msgid "Close"
msgstr ""
@ -12811,40 +12832,40 @@ msgstr ""
msgid "The grouped search term name is \"{0}\""
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/tag_browser/model.py:731
#: /home/kovid/work/calibre/src/calibre/gui2/tag_browser/model.py:724
msgid "Changing the authors for several books can take a while. Are you sure?"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/tag_browser/model.py:736
#: /home/kovid/work/calibre/src/calibre/gui2/tag_browser/model.py:729
msgid "Changing the metadata for that many books can take a while. Are you sure?"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/tag_browser/model.py:823
#: /home/kovid/work/calibre/src/calibre/gui2/tag_browser/model.py:816
#: /home/kovid/work/calibre/src/calibre/library/database2.py:449
msgid "Searches"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/tag_browser/model.py:888
#: /home/kovid/work/calibre/src/calibre/gui2/tag_browser/model.py:908
#: /home/kovid/work/calibre/src/calibre/gui2/tag_browser/model.py:917
#: /home/kovid/work/calibre/src/calibre/gui2/tag_browser/model.py:881
#: /home/kovid/work/calibre/src/calibre/gui2/tag_browser/model.py:901
#: /home/kovid/work/calibre/src/calibre/gui2/tag_browser/model.py:910
msgid "Rename user category"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/tag_browser/model.py:889
#: /home/kovid/work/calibre/src/calibre/gui2/tag_browser/model.py:882
msgid "You cannot use periods in the name when renaming user categories"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/tag_browser/model.py:909
#: /home/kovid/work/calibre/src/calibre/gui2/tag_browser/model.py:918
#: /home/kovid/work/calibre/src/calibre/gui2/tag_browser/model.py:902
#: /home/kovid/work/calibre/src/calibre/gui2/tag_browser/model.py:911
#, python-format
msgid "The name %s is already used"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/tag_browser/model.py:937
#: /home/kovid/work/calibre/src/calibre/gui2/tag_browser/model.py:930
msgid "Duplicate search name"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/tag_browser/model.py:938
#: /home/kovid/work/calibre/src/calibre/gui2/tag_browser/model.py:931
#, python-format
msgid "The saved search name %s is already used."
msgstr ""
@ -13260,6 +13281,7 @@ msgid "Edit"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/viewer/bookmarkmanager_ui.py:65
#: /usr/src/qt-everywhere-opensource-src-4.7.2/src/gui/widgets/qdialogbuttonbox.cpp:667
msgid "Reset"
msgstr ""
@ -13765,16 +13787,16 @@ msgstr ""
msgid "Could not move library"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/wizard/__init__.py:657
#: /home/kovid/work/calibre/src/calibre/gui2/wizard/__init__.py:662
msgid "Select location for books"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/wizard/__init__.py:678
#: /home/kovid/work/calibre/src/calibre/gui2/wizard/__init__.py:683
#, python-format
msgid "You must choose an empty folder for the calibre library. %s is not empty."
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/wizard/__init__.py:752
#: /home/kovid/work/calibre/src/calibre/gui2/wizard/__init__.py:757
msgid "welcome wizard"
msgstr ""
@ -14035,7 +14057,7 @@ msgstr ""
#: /home/kovid/work/calibre/src/calibre/library/caches.py:568
#: /home/kovid/work/calibre/src/calibre/library/caches.py:582
#: /home/kovid/work/calibre/src/calibre/library/caches.py:592
#: /home/kovid/work/calibre/src/calibre/library/save_to_disk.py:217
#: /home/kovid/work/calibre/src/calibre/library/save_to_disk.py:218
msgid "yes"
msgstr ""
@ -14043,7 +14065,7 @@ msgstr ""
#: /home/kovid/work/calibre/src/calibre/library/caches.py:567
#: /home/kovid/work/calibre/src/calibre/library/caches.py:579
#: /home/kovid/work/calibre/src/calibre/library/caches.py:589
#: /home/kovid/work/calibre/src/calibre/library/save_to_disk.py:217
#: /home/kovid/work/calibre/src/calibre/library/save_to_disk.py:218
msgid "no"
msgstr ""
@ -14873,10 +14895,6 @@ msgstr ""
msgid "%(tt)sAverage rating is %(rating)3.1f"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/library/database2.py:1086
msgid "Main"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/library/database2.py:3308
#, python-format
msgid "<p>Migrating old database to ebook library in %s<br><center>"
@ -15002,20 +15020,24 @@ msgid "Normally, calibre will convert all non English characters into English eq
msgstr ""
#: /home/kovid/work/calibre/src/calibre/library/save_to_disk.py:113
#: /home/kovid/work/calibre/src/calibre/library/save_to_disk.py:116
msgid "The format in which to display dates. %d - day, %b - month, %Y - year. Default is: %b, %Y"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/library/save_to_disk.py:119
#: /home/kovid/work/calibre/src/calibre/library/save_to_disk.py:116
#, python-format
msgid "The format in which to display dates. %(day)s - day, %(month)s - month, %(year)s - year. Default is: %(default)s"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/library/save_to_disk.py:120
msgid "Convert paths to lowercase."
msgstr ""
#: /home/kovid/work/calibre/src/calibre/library/save_to_disk.py:121
#: /home/kovid/work/calibre/src/calibre/library/save_to_disk.py:122
msgid "Replace whitespace with underscores."
msgstr ""
#: /home/kovid/work/calibre/src/calibre/library/save_to_disk.py:380
#: /home/kovid/work/calibre/src/calibre/library/save_to_disk.py:413
#: /home/kovid/work/calibre/src/calibre/library/save_to_disk.py:381
#: /home/kovid/work/calibre/src/calibre/library/save_to_disk.py:414
msgid "Requested formats not available"
msgstr ""
@ -15771,6 +15793,26 @@ msgstr ""
msgid "Dutch (BE)"
msgstr ""
#. NOTE: Ante Meridian (i.e. like 10:00 AM)
#: /home/kovid/work/calibre/src/calibre/utils/localization.py:154
msgid "AM"
msgstr ""
#. NOTE: Post Meridian (i.e. like 10:00 PM)
#: /home/kovid/work/calibre/src/calibre/utils/localization.py:156
msgid "PM"
msgstr ""
#. NOTE: Ante Meridian (i.e. like 10:00 am)
#: /home/kovid/work/calibre/src/calibre/utils/localization.py:158
msgid "am"
msgstr ""
#. NOTE: Post Meridian (i.e. like 10:00 pm)
#: /home/kovid/work/calibre/src/calibre/utils/localization.py:160
msgid "pm"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/utils/pyconsole/console.py:56
msgid "Choose theme (needs restart)"
msgstr ""
@ -16064,6 +16106,78 @@ msgstr ""
msgid "Do not download CSS stylesheets."
msgstr ""
#: /usr/src/qt-everywhere-opensource-src-4.7.2/src/gui/widgets/qdialogbuttonbox.cpp:649
msgid "OK"
msgstr ""
#: /usr/src/qt-everywhere-opensource-src-4.7.2/src/gui/widgets/qdialogbuttonbox.cpp:652
msgid "Save"
msgstr ""
#: /usr/src/qt-everywhere-opensource-src-4.7.2/src/gui/widgets/qdialogbuttonbox.cpp:655
msgid "Open"
msgstr ""
#: /usr/src/qt-everywhere-opensource-src-4.7.2/src/gui/widgets/qdialogbuttonbox.cpp:658
msgid "Cancel"
msgstr ""
#: /usr/src/qt-everywhere-opensource-src-4.7.2/src/gui/widgets/qdialogbuttonbox.cpp:661
msgid "&Close"
msgstr ""
#: /usr/src/qt-everywhere-opensource-src-4.7.2/src/gui/widgets/qdialogbuttonbox.cpp:664
msgid "Apply"
msgstr ""
#: /usr/src/qt-everywhere-opensource-src-4.7.2/src/gui/widgets/qdialogbuttonbox.cpp:674
msgid "Don't Save"
msgstr ""
#: /usr/src/qt-everywhere-opensource-src-4.7.2/src/gui/widgets/qdialogbuttonbox.cpp:676
msgid "Close without Saving"
msgstr ""
#: /usr/src/qt-everywhere-opensource-src-4.7.2/src/gui/widgets/qdialogbuttonbox.cpp:678
msgid "Discard"
msgstr ""
#: /usr/src/qt-everywhere-opensource-src-4.7.2/src/gui/widgets/qdialogbuttonbox.cpp:681
msgid "&Yes"
msgstr ""
#: /usr/src/qt-everywhere-opensource-src-4.7.2/src/gui/widgets/qdialogbuttonbox.cpp:684
msgid "Yes to &All"
msgstr ""
#: /usr/src/qt-everywhere-opensource-src-4.7.2/src/gui/widgets/qdialogbuttonbox.cpp:687
msgid "&No"
msgstr ""
#: /usr/src/qt-everywhere-opensource-src-4.7.2/src/gui/widgets/qdialogbuttonbox.cpp:690
msgid "N&o to All"
msgstr ""
#: /usr/src/qt-everywhere-opensource-src-4.7.2/src/gui/widgets/qdialogbuttonbox.cpp:693
msgid "Save All"
msgstr ""
#: /usr/src/qt-everywhere-opensource-src-4.7.2/src/gui/widgets/qdialogbuttonbox.cpp:696
msgid "Abort"
msgstr ""
#: /usr/src/qt-everywhere-opensource-src-4.7.2/src/gui/widgets/qdialogbuttonbox.cpp:699
msgid "Retry"
msgstr ""
#: /usr/src/qt-everywhere-opensource-src-4.7.2/src/gui/widgets/qdialogbuttonbox.cpp:702
msgid "Ignore"
msgstr ""
#: /usr/src/qt-everywhere-opensource-src-4.7.2/src/gui/widgets/qdialogbuttonbox.cpp:705
msgid "Restore Defaults"
msgstr ""
#: /home/kovid/work/calibre/resources/default_tweaks.py:12
msgid "Auto increment series index"

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