mirror of
https://github.com/kovidgoyal/calibre.git
synced 2025-07-09 03:04:10 -04:00
Merge from trunk
This commit is contained in:
commit
e9bddedf2e
@ -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."
|
||||
|
@ -1,5 +1,4 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from calibre.web.feeds.news import BasicNewsRecipe
|
||||
from calibre.web.feeds import Feed
|
||||
|
||||
@ -36,14 +35,13 @@ class GC_gl(BasicNewsRecipe):
|
||||
|
||||
|
||||
def feed_to_index_append(self, feedObject, masterFeed):
|
||||
for feed in feedObject:
|
||||
newArticles = []
|
||||
for article in feed.articles:
|
||||
newArt = {
|
||||
'title' : article.title,
|
||||
'url' : article.url,
|
||||
'date' : article.date
|
||||
}
|
||||
newArticles.append(newArt)
|
||||
masterFeed.append((feed.title,newArticles))
|
||||
|
||||
for feed in feedObject:
|
||||
newArticles = []
|
||||
for article in feed.articles:
|
||||
newArt = {
|
||||
'title' : article.title,
|
||||
'url' : article.url,
|
||||
'date' : article.date
|
||||
}
|
||||
newArticles.append(newArt)
|
||||
masterFeed.append((feed.title,newArticles))
|
||||
|
BIN
recipes/icons/losandes.png
Normal file
BIN
recipes/icons/losandes.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 285 B |
BIN
recipes/icons/techcrunch.png
Normal file
BIN
recipes/icons/techcrunch.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 119 B |
@ -1,22 +1,31 @@
|
||||
from calibre import strftime
|
||||
from calibre.web.feeds.news import BasicNewsRecipe
|
||||
|
||||
class AdvancedUserRecipe1299694372(BasicNewsRecipe):
|
||||
title = u'Instapaper'
|
||||
__author__ = 'Darko Miletic'
|
||||
publisher = 'Instapaper.com'
|
||||
category = 'info, custom, Instapaper'
|
||||
oldest_article = 365
|
||||
title = u'Instapaper'
|
||||
__author__ = 'Darko Miletic'
|
||||
publisher = 'Instapaper.com'
|
||||
category = 'info, custom, Instapaper'
|
||||
oldest_article = 365
|
||||
max_articles_per_feed = 100
|
||||
no_stylesheets = True
|
||||
remove_javascript = True
|
||||
remove_tags = [
|
||||
dict(name='div', attrs={'id':'text_controls_toggle'})
|
||||
,dict(name='script')
|
||||
,dict(name='div', attrs={'id':'text_controls'})
|
||||
,dict(name='div', attrs={'id':'editing_controls'})
|
||||
,dict(name='div', attrs={'class':'bar bottom'})
|
||||
]
|
||||
use_embedded_content = False
|
||||
needs_subscription = True
|
||||
INDEX = u'http://www.instapaper.com'
|
||||
LOGIN = INDEX + u'/user/login'
|
||||
|
||||
|
||||
|
||||
feeds = [(u'Instapaper Unread', u'http://www.instapaper.com/u'), (u'Instapaper Starred', u'http://www.instapaper.com/starred')]
|
||||
feeds = [
|
||||
(u'Instapaper Unread', u'http://www.instapaper.com/u'),
|
||||
(u'Instapaper Starred', u'http://www.instapaper.com/starred')
|
||||
]
|
||||
|
||||
def get_browser(self):
|
||||
br = BasicNewsRecipe.get_browser()
|
||||
@ -37,18 +46,20 @@ class AdvancedUserRecipe1299694372(BasicNewsRecipe):
|
||||
self.report_progress(0, _('Fetching feed')+' %s...'%(feedtitle if feedtitle else feedurl))
|
||||
articles = []
|
||||
soup = self.index_to_soup(feedurl)
|
||||
for item in soup.findAll('div', attrs={'class':'titleRow'}):
|
||||
description = self.tag_to_string(item.div)
|
||||
for item in soup.findAll('div', attrs={'class':'cornerControls'}):
|
||||
#description = self.tag_to_string(item.div)
|
||||
atag = item.a
|
||||
if atag and atag.has_key('href'):
|
||||
url = atag['href']
|
||||
title = self.tag_to_string(atag)
|
||||
date = strftime(self.timefmt)
|
||||
articles.append({
|
||||
'title' :title
|
||||
,'date' :date
|
||||
,'url' :url
|
||||
,'description':description
|
||||
'url' :url
|
||||
})
|
||||
totalfeeds.append((feedtitle, articles))
|
||||
return totalfeeds
|
||||
|
||||
def print_version(self, url):
|
||||
return 'http://www.instapaper.com' + url
|
||||
|
||||
def populate_article_metadata(self, article, soup, first):
|
||||
article.title = soup.find('title').contents[0].strip()
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import urllib2
|
||||
import urllib2, re
|
||||
from calibre.web.feeds.news import BasicNewsRecipe
|
||||
|
||||
class JBPress(BasicNewsRecipe):
|
||||
@ -40,3 +40,12 @@ class JBPress(BasicNewsRecipe):
|
||||
def print_version(self, url):
|
||||
url = urllib2.urlopen(url).geturl() # resolve redirect.
|
||||
return url.replace('/-/', '/print/')
|
||||
|
||||
def preprocess_html(self, soup):
|
||||
# remove breadcrumb
|
||||
h3s = soup.findAll('h3')
|
||||
for h3 in h3s:
|
||||
if re.compile('^JBpress>').match(h3.string):
|
||||
h3.extract()
|
||||
return soup
|
||||
|
||||
|
78
recipes/losandes.recipe
Normal file
78
recipes/losandes.recipe
Normal file
@ -0,0 +1,78 @@
|
||||
__license__ = 'GPL v3'
|
||||
__copyright__ = '2011, Darko Miletic <darko.miletic at gmail.com>'
|
||||
'''
|
||||
www.losandes.com.ar
|
||||
'''
|
||||
|
||||
from calibre import strftime
|
||||
from calibre.web.feeds.news import BasicNewsRecipe
|
||||
|
||||
class LosAndes(BasicNewsRecipe):
|
||||
title = 'Los Andes'
|
||||
__author__ = 'Darko Miletic'
|
||||
description = 'Noticias de Mendoza, Argentina y el resto del mundo'
|
||||
publisher = 'Los Andes'
|
||||
category = 'news, politics, Argentina'
|
||||
oldest_article = 2
|
||||
max_articles_per_feed = 200
|
||||
no_stylesheets = True
|
||||
encoding = 'cp1252'
|
||||
use_embedded_content = False
|
||||
language = 'es_AR'
|
||||
remove_empty_feeds = True
|
||||
publication_type = 'newspaper'
|
||||
masthead_url = 'http://www.losandes.com.ar/graficos/losandes.png'
|
||||
extra_css = """
|
||||
body{font-family: Arial,Helvetica,sans-serif }
|
||||
h1,h2{font-family: "Times New Roman",Times,serif}
|
||||
.fechaNota{font-weight: bold; color: gray}
|
||||
"""
|
||||
|
||||
conversion_options = {
|
||||
'comment' : description
|
||||
, 'tags' : category
|
||||
, 'publisher' : publisher
|
||||
, 'language' : language
|
||||
}
|
||||
|
||||
remove_tags = [
|
||||
dict(name=['meta','link'])
|
||||
,dict(attrs={'class':['cabecera', 'url']})
|
||||
]
|
||||
remove_tags_before=dict(attrs={'class':'cabecera'})
|
||||
remove_tags_after=dict(attrs={'class':'url'})
|
||||
|
||||
|
||||
feeds = [
|
||||
(u'Ultimas Noticias' , u'http://www.losandes.com.ar/servicios/rss.asp?r=78' )
|
||||
,(u'Politica' , u'http://www.losandes.com.ar/servicios/rss.asp?r=68' )
|
||||
,(u'Economia nacional' , u'http://www.losandes.com.ar/servicios/rss.asp?r=65' )
|
||||
,(u'Economia internacional' , u'http://www.losandes.com.ar/servicios/rss.asp?r=505')
|
||||
,(u'Internacionales' , u'http://www.losandes.com.ar/servicios/rss.asp?r=66' )
|
||||
,(u'Turismo' , u'http://www.losandes.com.ar/servicios/rss.asp?r=502')
|
||||
,(u'Fincas' , u'http://www.losandes.com.ar/servicios/rss.asp?r=504')
|
||||
,(u'Isha nos habla' , u'http://www.losandes.com.ar/servicios/rss.asp?r=562')
|
||||
,(u'Estilo' , u'http://www.losandes.com.ar/servicios/rss.asp?r=81' )
|
||||
,(u'Cultura' , u'http://www.losandes.com.ar/servicios/rss.asp?r=503')
|
||||
,(u'Policiales' , u'http://www.losandes.com.ar/servicios/rss.asp?r=70' )
|
||||
,(u'Deportes' , u'http://www.losandes.com.ar/servicios/rss.asp?r=69' )
|
||||
,(u'Sociedad' , u'http://www.losandes.com.ar/servicios/rss.asp?r=67' )
|
||||
,(u'Opinion' , u'http://www.losandes.com.ar/servicios/rss.asp?r=80' )
|
||||
,(u'Editorial' , u'http://www.losandes.com.ar/servicios/rss.asp?r=76' )
|
||||
,(u'Mirador' , u'http://www.losandes.com.ar/servicios/rss.asp?r=79' )
|
||||
]
|
||||
|
||||
def print_version(self, url):
|
||||
artid = url.rpartition('.')[0].rpartition('-')[2]
|
||||
return "http://www.losandes.com.ar/includes/modulos/imprimir.asp?tipo=noticia&id=" + artid
|
||||
|
||||
def get_cover_url(self):
|
||||
month = strftime("%m").lstrip('0')
|
||||
day = strftime("%d").lstrip('0')
|
||||
year = strftime("%Y")
|
||||
return "http://www.losandes.com.ar/fotografias/fotosnoticias/" + year + "/" + month + "/" + day + "/th_tapa.jpg"
|
||||
|
||||
def preprocess_html(self, soup):
|
||||
for item in soup.findAll(style=True):
|
||||
del item['style']
|
||||
return soup
|
44
recipes/luns_a_venres.recipe
Normal file
44
recipes/luns_a_venres.recipe
Normal file
@ -0,0 +1,44 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from calibre.web.feeds.news import BasicNewsRecipe
|
||||
|
||||
class LV_gl(BasicNewsRecipe):
|
||||
title = u'De Luns a Venres (RSS)'
|
||||
__author__ = u'Susana Sotelo Docío'
|
||||
description = u'O gratuíto galego'
|
||||
publisher = u'Galiciaé'
|
||||
category = u'news'
|
||||
encoding = 'utf-8'
|
||||
language = 'gl'
|
||||
direction = 'ltr'
|
||||
cover_url = 'http://lv.galiciae.com/new_estilos/lv/logo.gif'
|
||||
oldest_article = 2
|
||||
max_articles_per_feed = 200
|
||||
center_navbar = False
|
||||
|
||||
feeds = [
|
||||
(u'Galicia', u'http://lv.galiciae.com/cache/rss/sec_galicia_gl.rss'),
|
||||
(u'Cultura', u'http://lv.galiciae.com/cache/rss/sec_cultura_gl.rss'),
|
||||
(u'Mundo', u'http://lv.galiciae.com/cache/rss/sec_mundo_gl.rss'),
|
||||
(u'Cidadanía', u'http://lv.galiciae.com/cache/rss/sec_ciudadania_gl.rss'),
|
||||
(u'Tecnoloxía', u'http://lv.galiciae.com/cache/rss/sec_tecnologia_gl.rss'),
|
||||
(u'España', u'http://lv.galiciae.com/cache/rss/sec_espana_gl.rss'),
|
||||
(u'Deportes', u'http://lv.galiciae.com/cache/rss/sec_deportes_gl.rss'),
|
||||
(u'Economía', u'http://lv.galiciae.com/cache/rss/sec_economia_gl.rss'),
|
||||
(u'Lercheo', u'http://lv.galiciae.com/cache/rss/sec_gente_gl.rss'),
|
||||
(u'Medio ambiente', u'http://lv.galiciae.com/cache/rss/sec_medioambiente_gl.rss'),
|
||||
(u'España/Mundo', u'http://lv.galiciae.com/cache/rss/sec_espanamundo_gl.rss'),
|
||||
(u'Sociedade', u'http://lv.galiciae.com/cache/rss/sec_sociedad_gl.rss'),
|
||||
(u'Ciencia', u'http://lv.galiciae.com/cache/rss/sec_ciencia_gl.rss'),
|
||||
(u'Motor', u'http://lv.galiciae.com/cache/rss/sec_motor_gl.rss'),
|
||||
(u'Coches', u'http://lv.galiciae.com/cache/rss/sec_coches_gl.rss'),
|
||||
(u'Motos', u'http://lv.galiciae.com/cache/rss/sec_motos_gl.rss'),
|
||||
(u'Industriais', u'http://lv.galiciae.com/cache/rss/sec_industriales_gl.rss')
|
||||
]
|
||||
|
||||
extra_css = u' p{text-align:left} '
|
||||
html2epub_options = 'publisher="' + publisher + '"\ncomments="' + description + '"\nencoding="' + encoding + '"\ntags="' + category + '"\noverride_css=" p {text-align:left; text-indent: 0cm} "'
|
||||
|
||||
def print_version(self, url):
|
||||
url += '?imprimir&lang=gl'
|
||||
return url
|
||||
|
@ -1,11 +1,10 @@
|
||||
EMAILADDRESS = 'hoge@foobar.co.jp'
|
||||
from calibre.web.feeds.news import BasicNewsRecipe
|
||||
|
||||
|
||||
class NBOnline(BasicNewsRecipe):
|
||||
title = u'Nikkei Business Online'
|
||||
language = 'ja'
|
||||
description = u'Nikkei Business Online New articles. PLEASE NOTE: You need to edit EMAILADDRESS line of this "nbonline.recipe" file to set your e-mail address which is needed when login. (file is in "Calibre2/resources/recipes" directory.)'
|
||||
description = u'Nikkei Business Online.\u6CE8\uFF1A\u30E6\u30FC\u30B6\u30FC\u540D\u306Bemail\u30A2\u30C9\u30EC\u30B9\u3068\u30E6\u30FC\u30B6\u30FC\u540D\u3092\u30BB\u30DF\u30B3\u30ED\u30F3\u3067\u533A\u5207\u3063\u3066\u5165\u308C\u3066\u304F\u3060\u3055\u3044\u3002\u4F8B\uFF1Aemail@address.jp;username . PLEASE NOTE: You need to put your email address and username into username filed separeted by ; (semi-colon).'
|
||||
__author__ = 'Ado Nishimura'
|
||||
needs_subscription = True
|
||||
oldest_article = 7
|
||||
@ -23,8 +22,8 @@ class NBOnline(BasicNewsRecipe):
|
||||
if self.username is not None and self.password is not None:
|
||||
br.open('https://signon.nikkeibp.co.jp/front/login/?ct=p&ts=nbo')
|
||||
br.select_form(name='loginActionForm')
|
||||
br['email'] = EMAILADDRESS
|
||||
br['userId'] = self.username
|
||||
br['email'] = self.username.split(';')[0]
|
||||
br['userId'] = self.username.split(';')[1]
|
||||
br['password'] = self.password
|
||||
br.submit()
|
||||
return br
|
||||
|
88
recipes/nikkei_news.recipe
Normal file
88
recipes/nikkei_news.recipe
Normal 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
63
recipes/techcrunch.recipe
Normal 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
|
@ -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;}
|
||||
'''
|
||||
|
||||
|
@ -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|&|&\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
|
||||
|
||||
|
@ -11,6 +11,7 @@
|
||||
<link rel="stylesheet" type="text/css" href="{prefix}/static/browse/browse.css" />
|
||||
<link type="text/css" href="{prefix}/static/jquery_ui/css/humanity-custom/jquery-ui-1.8.5.custom.css" rel="stylesheet" />
|
||||
<link rel="stylesheet" type="text/css" href="{prefix}/static/jquery.multiselect.css" />
|
||||
<link rel="apple-touch-icon" href="/static/calibre.png" />
|
||||
|
||||
<script type="text/javascript" src="{prefix}/static/jquery.js"></script>
|
||||
<script type="text/javascript" src="{prefix}/static/jquery.corner.js"></script>
|
||||
|
@ -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 |
@ -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>
|
||||
|
@ -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')),
|
||||
|
@ -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
|
||||
|
@ -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):
|
||||
|
||||
|
@ -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',
|
||||
|
@ -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>"
|
||||
|
||||
|
@ -1181,6 +1181,26 @@ class StoreBeWriteStore(StoreBase):
|
||||
headquarters = 'US'
|
||||
formats = ['EPUB', 'MOBI', 'PDF']
|
||||
|
||||
class StoreBookotekaStore(StoreBase):
|
||||
name = 'Bookoteka'
|
||||
author = u'Tomasz Długosz'
|
||||
description = u'E-booki w Bookotece dostępne są w formacie EPUB oraz PDF. Publikacje sprzedawane w Bookotece są objęte prawami autorskimi. Zobowiązaliśmy się chronić te prawa, ale bez ograniczania dostępu do książki użytkownikowi, który nabył ją w legalny sposób. Dlatego też Bookoteka stosuje tak zwany „watermarking transakcyjny” czyli swego rodzaju znaki wodne.'
|
||||
actual_plugin = 'calibre.gui2.store.stores.bookoteka_plugin:BookotekaStore'
|
||||
|
||||
drm_free_only = True
|
||||
headquarters = 'PL'
|
||||
formats = ['EPUB', 'PDF']
|
||||
|
||||
class StoreChitankaStore(StoreBase):
|
||||
name = u'Моята библиотека'
|
||||
author = 'Alex Stanev'
|
||||
description = u'Независим сайт за DRM свободна литература на български език'
|
||||
actual_plugin = 'calibre.gui2.store.stores.chitanka_plugin:ChitankaStore'
|
||||
|
||||
drm_free_only = True
|
||||
headquarters = 'BG'
|
||||
formats = ['FB2', 'EPUB', 'TXT', 'SFB']
|
||||
|
||||
class StoreDieselEbooksStore(StoreBase):
|
||||
name = 'Diesel eBooks'
|
||||
description = u'Instant access to over 2.4 million titles from hundreds of publishers including Harlequin, HarperCollins, John Wiley & Sons, McGraw-Hill, Simon & Schuster and Random House.'
|
||||
@ -1208,16 +1228,16 @@ class StoreEbookscomStore(StoreBase):
|
||||
formats = ['EPUB', 'LIT', 'MOBI', 'PDF']
|
||||
affiliate = True
|
||||
|
||||
class StoreEPubBuyDEStore(StoreBase):
|
||||
name = 'EPUBBuy DE'
|
||||
author = 'Charles Haley'
|
||||
description = u'Bei EPUBBuy.com finden Sie ausschliesslich eBooks im weitverbreiteten EPUB-Format und ohne DRM. So haben Sie die freie Wahl, wo Sie Ihr eBook lesen: Tablet, eBook-Reader, Smartphone oder einfach auf Ihrem PC. So macht eBook-Lesen Spaß!'
|
||||
actual_plugin = 'calibre.gui2.store.stores.epubbuy_de_plugin:EPubBuyDEStore'
|
||||
|
||||
drm_free_only = True
|
||||
headquarters = 'DE'
|
||||
formats = ['EPUB']
|
||||
affiliate = True
|
||||
#class StoreEPubBuyDEStore(StoreBase):
|
||||
# name = 'EPUBBuy DE'
|
||||
# author = 'Charles Haley'
|
||||
# description = u'Bei EPUBBuy.com finden Sie ausschliesslich eBooks im weitverbreiteten EPUB-Format und ohne DRM. So haben Sie die freie Wahl, wo Sie Ihr eBook lesen: Tablet, eBook-Reader, Smartphone oder einfach auf Ihrem PC. So macht eBook-Lesen Spaß!'
|
||||
# actual_plugin = 'calibre.gui2.store.stores.epubbuy_de_plugin:EPubBuyDEStore'
|
||||
#
|
||||
# drm_free_only = True
|
||||
# headquarters = 'DE'
|
||||
# formats = ['EPUB']
|
||||
# affiliate = True
|
||||
|
||||
class StoreEBookShoppeUKStore(StoreBase):
|
||||
name = 'ebookShoppe UK'
|
||||
@ -1455,11 +1475,13 @@ plugins += [
|
||||
StoreBNStore,
|
||||
StoreBeamEBooksDEStore,
|
||||
StoreBeWriteStore,
|
||||
StoreBookotekaStore,
|
||||
StoreChitankaStore,
|
||||
StoreDieselEbooksStore,
|
||||
StoreEbookNLStore,
|
||||
StoreEbookscomStore,
|
||||
StoreEBookShoppeUKStore,
|
||||
StoreEPubBuyDEStore,
|
||||
# StoreEPubBuyDEStore,
|
||||
StoreEHarlequinStore,
|
||||
StoreEpubBudStore,
|
||||
StoreFeedbooksStore,
|
||||
|
@ -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'
|
||||
@ -624,7 +626,7 @@ class DB(object):
|
||||
base = max(self.FIELD_MAP.itervalues())
|
||||
|
||||
for label_, data in self.custom_column_label_map.iteritems():
|
||||
label = '#' + label_
|
||||
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
|
||||
@ -653,7 +655,10 @@ 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)
|
||||
@ -758,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
|
||||
|
||||
# }}}
|
||||
|
||||
|
@ -7,10 +7,14 @@ __license__ = 'GPL v3'
|
||||
__copyright__ = '2011, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
from functools import wraps
|
||||
import os
|
||||
from collections import defaultdict
|
||||
from functools import wraps, partial
|
||||
|
||||
from calibre.db.locking import create_locks
|
||||
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
|
||||
@ -39,7 +43,10 @@ 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
|
||||
@ -55,6 +62,113 @@ class Cache(object):
|
||||
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
|
||||
@ -67,6 +181,10 @@ class Cache(object):
|
||||
|
||||
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):
|
||||
@ -77,11 +195,27 @@ class Cache(object):
|
||||
|
||||
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):
|
||||
'''
|
||||
@ -122,8 +256,120 @@ class Cache(object):
|
||||
'''
|
||||
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):
|
||||
|
@ -2,12 +2,16 @@
|
||||
# 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):
|
||||
|
||||
@ -16,6 +20,8 @@ class Field(object):
|
||||
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):
|
||||
@ -49,6 +55,15 @@ class Field(object):
|
||||
'''
|
||||
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):
|
||||
@ -66,6 +81,86 @@ class OneToOneField(Field):
|
||||
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):
|
||||
@ -77,10 +172,10 @@ class ManyToOneField(Field):
|
||||
return ans
|
||||
|
||||
def ids_for_book(self, book_id):
|
||||
ids = self.table.book_col_map.get(book_id, None)
|
||||
if ids is None:
|
||||
id_ = self.table.book_col_map.get(book_id, None)
|
||||
if id_ is None:
|
||||
return ()
|
||||
return ids
|
||||
return (id_,)
|
||||
|
||||
def books_for(self, item_id):
|
||||
return self.table.col_book_map.get(item_id, ())
|
||||
@ -88,8 +183,18 @@ class ManyToOneField(Field):
|
||||
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:
|
||||
@ -107,11 +212,46 @@ class ManyToManyField(Field):
|
||||
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)
|
||||
|
||||
|
@ -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)
|
||||
|
||||
# }}}
|
||||
|
@ -77,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):
|
||||
|
||||
'''
|
||||
@ -144,11 +155,11 @@ class AuthorsTable(ManyToManyTable):
|
||||
|
||||
def read_id_maps(self, db):
|
||||
self.alink_map = {}
|
||||
self.sort_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.sort_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]
|
||||
|
||||
@ -158,14 +169,19 @@ 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])
|
||||
@ -185,12 +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])
|
||||
|
||||
for key in tuple(self.book_col_map.iterkeys()):
|
||||
self.book_col_map[key] = tuple(self.book_col_map[key])
|
||||
|
||||
|
@ -7,15 +7,103 @@ __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._field_idx_map = {}
|
||||
self.marked_ids = {}
|
||||
self._field_getters = {}
|
||||
for col, idx in cache.backend.FIELD_MAP.iteritems():
|
||||
if isinstance(col, int):
|
||||
pass # custom column
|
||||
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:
|
||||
self._field_idx_map[idx] = col
|
||||
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
|
||||
|
||||
|
||||
|
@ -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', 'GT-I5800_CARD', 'MB612']
|
||||
'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',
|
||||
|
@ -131,7 +131,7 @@ class AZBOOKA(ALEX):
|
||||
description = _('Communicate with the Azbooka')
|
||||
|
||||
VENDOR_NAME = 'LINUX'
|
||||
WINDOWS_MAIN_MEM = 'FILE-STOR_GADGET'
|
||||
WINDOWS_MAIN_MEM = WINDOWS_CARD_A_MEM = 'FILE-STOR_GADGET'
|
||||
|
||||
MAIN_MEMORY_VOLUME_LABEL = 'Azbooka Internal Memory'
|
||||
|
||||
|
@ -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 = []
|
||||
|
||||
|
@ -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):
|
||||
|
@ -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',
|
||||
]
|
||||
),
|
||||
|
@ -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)
|
||||
|
||||
|
@ -303,6 +303,9 @@ class CSSPreProcessor(object):
|
||||
class HTMLPreProcessor(object):
|
||||
|
||||
PREPROCESS = [
|
||||
# Remove huge block of contiguous spaces as they slow down
|
||||
# the following regexes pretty badly
|
||||
(re.compile(r'\s{10000,}'), lambda m: ''),
|
||||
# Some idiotic HTML generators (Frontpage I'm looking at you)
|
||||
# Put all sorts of crap into <head>. This messes up lxml
|
||||
(re.compile(r'<head[^>]*>\n*(.*?)\n*</head>', re.IGNORECASE|re.DOTALL),
|
||||
|
@ -7,10 +7,15 @@ __license__ = 'GPL v3'
|
||||
__copyright__ = '2011, Kovid Goyal <kovid@kovidgoyal.net>'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
import struct, datetime
|
||||
import struct, datetime, sys, os, shutil
|
||||
from collections import OrderedDict
|
||||
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, decint,
|
||||
get_trailing_data)
|
||||
from calibre.utils.magick.draw import identify_data
|
||||
|
||||
# PalmDB {{{
|
||||
class PalmDOCAttributes(object):
|
||||
|
||||
class Attr(object):
|
||||
@ -94,8 +99,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 +109,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 +197,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 +279,8 @@ 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
|
||||
self.extra_data_flags = 0
|
||||
if self.has_extra_data_flags:
|
||||
self.unknown4 = self.raw[180:192]
|
||||
self.first_content_record, self.last_content_record = \
|
||||
@ -280,8 +290,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 +324,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 +355,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 +373,482 @@ 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.bitmask = ord(raw[2])
|
||||
# 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.bitmask == 0)
|
||||
|
||||
def __repr__(self):
|
||||
return 'TAGX(tag=%02d, num_values=%d, bitmask=%r, eof=%d)' % (self.tag,
|
||||
self.num_values, bin(self.bitmask), 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_cncx_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')
|
||||
self.tagx_entries = self.tagx_entries[:-1]
|
||||
|
||||
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 cncx blocks: %d'%self.num_of_cncx_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 Tag(object): # {{{
|
||||
|
||||
'''
|
||||
Index entries are a collection of tags. Each tag is represented by this
|
||||
class.
|
||||
'''
|
||||
|
||||
TAG_MAP = {
|
||||
1: ('offset', 'Offset in HTML'),
|
||||
2: ('size', 'Size in HTML'),
|
||||
3: ('label_offset', 'Offset to label in CNCX'),
|
||||
4: ('depth', 'Depth of this entry in TOC'),
|
||||
|
||||
# The remaining tag types have to be interpreted subject to the type
|
||||
# of index entry they are present in
|
||||
}
|
||||
|
||||
INTERPRET_MAP = {
|
||||
'subchapter': {
|
||||
5 : ('Parent chapter index', 'parent_index')
|
||||
},
|
||||
|
||||
'article' : {
|
||||
5 : ('Class offset in cncx', 'class_offset'),
|
||||
21 : ('Parent section index', 'parent_index'),
|
||||
22 : ('Description offset in cncx', 'desc_offset'),
|
||||
23 : ('Author offset in cncx', 'author_offset'),
|
||||
},
|
||||
|
||||
'chapter_with_subchapters' : {
|
||||
22 : ('First subchapter index', 'first_subchapter_index'),
|
||||
23 : ('Last subchapter index', 'last_subchapter_index'),
|
||||
},
|
||||
|
||||
'periodical' : {
|
||||
5 : ('Class offset in cncx', 'class_offset'),
|
||||
22 : ('First section index', 'first_section_index'),
|
||||
23 : ('Last section index', 'last_section_index'),
|
||||
},
|
||||
|
||||
'section' : {
|
||||
5 : ('Class offset in cncx', 'class_offset'),
|
||||
21 : ('Periodical index', 'periodical_index'),
|
||||
22 : ('First article index', 'first_article_index'),
|
||||
23 : ('Last article index', 'last_article_index'),
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def __init__(self, tagx, vals, entry_type, cncx):
|
||||
self.value = vals if len(vals) > 1 else vals[0]
|
||||
self.entry_type = entry_type
|
||||
self.cncx_value = None
|
||||
if tagx.tag in self.TAG_MAP:
|
||||
self.attr, self.desc = self.TAG_MAP[tagx.tag]
|
||||
else:
|
||||
try:
|
||||
td = self.INTERPRET_MAP[entry_type]
|
||||
except:
|
||||
raise ValueError('Unknown entry type: %s'%entry_type)
|
||||
try:
|
||||
self.desc, self.attr = td[tagx.tag]
|
||||
except:
|
||||
raise ValueError('Unknown tag: %d for entry type: %s'%(
|
||||
tagx.tag, entry_type))
|
||||
if '_offset' in self.attr:
|
||||
self.cncx_value = cncx[self.value]
|
||||
|
||||
def __str__(self):
|
||||
if self.cncx_value is not None:
|
||||
return '%s : %r [%r]'%(self.desc, self.value, self.cncx_value)
|
||||
return '%s : %r'%(self.desc, self.value)
|
||||
|
||||
# }}}
|
||||
|
||||
class IndexEntry(object): # {{{
|
||||
|
||||
'''
|
||||
The index is made up of entries, each of which is represented by an
|
||||
instance of this class. Index entries typically point to offsets int eh
|
||||
HTML, specify HTML sizes and point to text strings in the CNCX that are
|
||||
used in the navigation UI.
|
||||
'''
|
||||
|
||||
TYPES = {
|
||||
# Present in book type files
|
||||
0x0f : 'chapter',
|
||||
0x6f : 'chapter_with_subchapters',
|
||||
0x1f : 'subchapter',
|
||||
# Present in periodicals
|
||||
0xdf : 'periodical',
|
||||
0xff : 'section',
|
||||
0x3f : 'article',
|
||||
}
|
||||
|
||||
def __init__(self, ident, entry_type, raw, cncx, tagx_entries):
|
||||
self.index = ident
|
||||
self.raw = raw
|
||||
self.tags = []
|
||||
|
||||
try:
|
||||
self.entry_type = self.TYPES[entry_type]
|
||||
except KeyError:
|
||||
raise ValueError('Unknown Index Entry type: %s'%hex(entry_type))
|
||||
|
||||
expected_tags = [tag for tag in tagx_entries if tag.bitmask &
|
||||
entry_type]
|
||||
|
||||
for tag in expected_tags:
|
||||
vals = []
|
||||
for i in range(tag.num_values):
|
||||
if not raw:
|
||||
raise ValueError('Index entry does not match TAGX header')
|
||||
val, consumed = decint(raw)
|
||||
raw = raw[consumed:]
|
||||
vals.append(val)
|
||||
self.tags.append(Tag(tag, vals, self.entry_type, cncx))
|
||||
|
||||
@property
|
||||
def label(self):
|
||||
for tag in self.tags:
|
||||
if tag.attr == 'label_offset':
|
||||
return tag.cncx_value
|
||||
return ''
|
||||
|
||||
def __str__(self):
|
||||
ans = ['Index Entry(index=%s, entry_type=%s, length=%d)'%(
|
||||
self.index, self.entry_type, len(self.tags))]
|
||||
for tag in self.tags:
|
||||
ans.append('\t'+str(tag))
|
||||
return '\n'.join(ans)
|
||||
|
||||
# }}}
|
||||
|
||||
class IndexRecord(object): # {{{
|
||||
|
||||
'''
|
||||
Represents all indexing information in the MOBI, apart from indexing info
|
||||
in the trailing data of the text records.
|
||||
'''
|
||||
|
||||
def __init__(self, record, index_header, cncx):
|
||||
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 i, off in enumerate(self.index_offsets):
|
||||
try:
|
||||
next_off = self.index_offsets[i+1]
|
||||
except:
|
||||
next_off = len(indxt)
|
||||
index, consumed = decode_hex_number(indxt[off:])
|
||||
entry_type = ord(indxt[off+consumed])
|
||||
self.indices.append(IndexEntry(index, entry_type,
|
||||
indxt[off+consumed+1:next_off], cncx, index_header.tagx_entries))
|
||||
|
||||
|
||||
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)
|
||||
a('\nIndex Entries:')
|
||||
for entry in self.indices:
|
||||
a(str(entry)+'\n')
|
||||
|
||||
return '\n'.join(ans)
|
||||
|
||||
# }}}
|
||||
|
||||
class CNCX(object) : # {{{
|
||||
|
||||
'''
|
||||
Parses the records that contain the compiled NCX (all strings from the
|
||||
NCX). Presents a simple offset : string mapping interface to access the
|
||||
data.
|
||||
'''
|
||||
|
||||
def __init__(self, records, codec):
|
||||
self.records = OrderedDict()
|
||||
pos = 0
|
||||
for record in records:
|
||||
raw = record.raw
|
||||
while pos < len(raw):
|
||||
length, consumed = decint(raw[pos:])
|
||||
if length > 0:
|
||||
self.records[pos] = raw[pos+consumed:pos+consumed+length].decode(
|
||||
codec)
|
||||
pos += consumed+length
|
||||
|
||||
def __getitem__(self, offset):
|
||||
return self.records.get(offset)
|
||||
|
||||
def __str__(self):
|
||||
ans = ['*'*20 + ' cncx (%d strings) '%len(self.records)+ '*'*20]
|
||||
for k, v in self.records.iteritems():
|
||||
ans.append('%10d : %s'%(k, v))
|
||||
return '\n'.join(ans)
|
||||
|
||||
|
||||
# }}}
|
||||
|
||||
class TextRecord(object): # {{{
|
||||
|
||||
def __init__(self, idx, record, extra_data_flags, decompress, index_record,
|
||||
doc_type):
|
||||
self.trailing_data, self.raw = get_trailing_data(record.raw, extra_data_flags)
|
||||
self.raw = decompress(self.raw)
|
||||
if 0 in self.trailing_data:
|
||||
self.trailing_data['multibyte_overlap'] = self.trailing_data.pop(0)
|
||||
if 1 in self.trailing_data:
|
||||
self.trailing_data['indexing'] = self.trailing_data.pop(1)
|
||||
if 2 in self.trailing_data:
|
||||
self.trailing_data['uncrossable_breaks'] = self.trailing_data.pop(2)
|
||||
|
||||
self.idx = idx
|
||||
|
||||
if 'indexing' in self.trailing_data and index_record is not None:
|
||||
self.interpret_indexing(doc_type, index_record.indices)
|
||||
|
||||
def interpret_indexing(self, doc_type, indices):
|
||||
raw = self.trailing_data['indexing']
|
||||
ident, consumed = decint(raw)
|
||||
raw = raw[consumed:]
|
||||
entry_type = ident & 0b111
|
||||
index_entry_idx = ident >> 3
|
||||
index_entry = None
|
||||
for i in indices:
|
||||
if i.index == index_entry_idx:
|
||||
index_entry = i.label
|
||||
break
|
||||
self.trailing_data['interpreted_indexing'] = (
|
||||
'Type: %s, Index Entry: %s'%(entry_type, index_entry))
|
||||
if doc_type == 2: # Book
|
||||
self.interpret_book_indexing(raw, entry_type)
|
||||
|
||||
def interpret_book_indexing(self, raw, entry_type):
|
||||
arg1, consumed = decint(raw)
|
||||
raw = raw[consumed:]
|
||||
if arg1 != 0:
|
||||
raise ValueError('TBS index entry has unknown arg1: %d'%
|
||||
arg1)
|
||||
if entry_type == 2:
|
||||
desc = ('This record has only a single starting or a single'
|
||||
' ending point')
|
||||
if raw:
|
||||
raise ValueError('TBS index entry has unknown extra bytes:'
|
||||
' %r'%raw)
|
||||
elif entry_type == 3:
|
||||
desc = ('This record is spanned by a single node (i.e. it'
|
||||
' has no start or end points)')
|
||||
arg2, consumed = decint(raw)
|
||||
if arg2 != 0:
|
||||
raise ValueError('TBS index entry has unknown arg2: %d'%
|
||||
arg2)
|
||||
elif entry_type == 6:
|
||||
if len(raw) != 1:
|
||||
raise ValueError('TBS index entry has unknown extra bytes:'
|
||||
' %r'%raw)
|
||||
num = ord(raw[0])
|
||||
# An unmatched starting or ending point each contributes 1 to
|
||||
# this count. A matched pair of starting and ending points
|
||||
# together contribute 1 to this count. Note that you can only
|
||||
# ever have either 1 unmatched start point or 1 unmatched end
|
||||
# point, never both (logically impossible).
|
||||
desc = ('This record has %d starting/ending points and/or complete'
|
||||
' nodes.')%num
|
||||
else:
|
||||
raise ValueError('Unknown TBS index entry type: %d for book'%entry_type)
|
||||
self.trailing_data['interpreted_indexing'] += ' :: ' + desc
|
||||
|
||||
def dump(self, folder):
|
||||
name = '%06d'%self.idx
|
||||
with open(os.path.join(folder, name+'.txt'), 'wb') as f:
|
||||
f.write(self.raw)
|
||||
with open(os.path.join(folder, name+'.trailing_data'), 'wb') as f:
|
||||
for k, v in self.trailing_data.iteritems():
|
||||
raw = '%s : %r\n\n'%(k, v)
|
||||
f.write(raw.encode('utf-8'))
|
||||
|
||||
# }}}
|
||||
|
||||
class ImageRecord(object): # {{{
|
||||
|
||||
def __init__(self, idx, record, fmt):
|
||||
self.raw = record.raw
|
||||
self.fmt = fmt
|
||||
self.idx = idx
|
||||
|
||||
def dump(self, folder):
|
||||
name = '%06d'%self.idx
|
||||
with open(os.path.join(folder, name+'.'+self.fmt), 'wb') as f:
|
||||
f.write(self.raw)
|
||||
|
||||
# }}}
|
||||
|
||||
class BinaryRecord(object): # {{{
|
||||
|
||||
def __init__(self, idx, record):
|
||||
self.raw = record.raw
|
||||
sig = self.raw[:4]
|
||||
name = '%06d'%idx
|
||||
if sig in (b'FCIS', b'FLIS', b'SRCS'):
|
||||
name += '-' + sig.decode('ascii')
|
||||
elif sig == b'\xe9\x8e\r\n':
|
||||
name += '-' + 'EOF'
|
||||
self.name = name
|
||||
|
||||
def dump(self, folder):
|
||||
with open(os.path.join(folder, self.name+'.bin'), 'wb') as f:
|
||||
f.write(self.raw)
|
||||
|
||||
# }}}
|
||||
|
||||
class MOBIFile(object): # {{{
|
||||
|
||||
def __init__(self, stream):
|
||||
self.raw = stream.read()
|
||||
@ -384,25 +876,104 @@ class MOBIFile(object):
|
||||
|
||||
self.mobi_header = MOBIHeader(self.records[0])
|
||||
|
||||
if 'huff' in self.mobi_header.compression.lower():
|
||||
huffrecs = [r.raw for r in
|
||||
xrange(self.mobi_header.huffman_record_offset,
|
||||
self.mobi_header.huffman_record_offset +
|
||||
self.mobi_header.huffman_record_count)]
|
||||
from calibre.ebooks.mobi.huffcdic import HuffReader
|
||||
huffs = HuffReader(huffrecs)
|
||||
decompress = huffs.decompress
|
||||
elif 'palmdoc' in self.mobi_header.compression.lower():
|
||||
from calibre.ebooks.compression.palmdoc import decompress_doc
|
||||
decompress = decompress_doc
|
||||
else:
|
||||
decompress = lambda x: x
|
||||
|
||||
def print_header(self):
|
||||
print (str(self.palmdb).encode('utf-8'))
|
||||
print ()
|
||||
print ('Record headers:')
|
||||
self.index_header = self.index_record = None
|
||||
self.indexing_record_nums = set()
|
||||
pir = self.mobi_header.primary_index_record
|
||||
if pir != 0xffffffff:
|
||||
self.index_header = IndexHeader(self.records[pir])
|
||||
self.cncx = CNCX(self.records[
|
||||
pir+2:pir+2+self.index_header.num_of_cncx_blocks],
|
||||
self.index_header.index_encoding)
|
||||
self.index_record = IndexRecord(self.records[pir+1],
|
||||
self.index_header, self.cncx)
|
||||
self.indexing_record_nums = set(xrange(pir,
|
||||
pir+2+self.index_header.num_of_cncx_blocks))
|
||||
|
||||
|
||||
ntr = self.mobi_header.number_of_text_records
|
||||
fntbr = self.mobi_header.first_non_book_record
|
||||
fii = self.mobi_header.first_image_index
|
||||
if fntbr == 0xffffffff:
|
||||
fntbr = len(self.records)
|
||||
self.text_records = [TextRecord(r, self.records[r],
|
||||
self.mobi_header.extra_data_flags, decompress, self.index_record,
|
||||
self.mobi_header.type_raw) for r in xrange(1,
|
||||
min(len(self.records), ntr+1))]
|
||||
self.image_records, self.binary_records = [], []
|
||||
for i in xrange(fntbr, len(self.records)):
|
||||
if i in self.indexing_record_nums:
|
||||
continue
|
||||
r = self.records[i]
|
||||
fmt = None
|
||||
if i >= fii and r.raw[:4] not in (b'FLIS', b'FCIS', b'SRCS',
|
||||
b'\xe9\x8e\r\n'):
|
||||
try:
|
||||
width, height, fmt = identify_data(r.raw)
|
||||
except:
|
||||
pass
|
||||
if fmt is not None:
|
||||
self.image_records.append(ImageRecord(i, r, fmt))
|
||||
else:
|
||||
self.binary_records.append(BinaryRecord(i, r))
|
||||
|
||||
|
||||
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):
|
||||
def inspect_mobi(path_or_stream, prefix='decompiled'):
|
||||
stream = (path_or_stream if hasattr(path_or_stream, 'read') else
|
||||
open(path_or_stream, 'rb'))
|
||||
f = MOBIFile(stream)
|
||||
f.print_header()
|
||||
ddir = prefix + '_' + os.path.splitext(os.path.basename(stream.name))[0]
|
||||
try:
|
||||
shutil.rmtree(ddir)
|
||||
except:
|
||||
pass
|
||||
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.cncx).encode('utf-8'), file=out)
|
||||
print('\n\n', file=out)
|
||||
print(str(f.index_record), file=out)
|
||||
|
||||
for tdir, attr in [('text', 'text_records'), ('images', 'image_records'),
|
||||
('binary', 'binary_records')]:
|
||||
tdir = os.path.join(ddir, tdir)
|
||||
os.mkdir(tdir)
|
||||
for rec in getattr(f, attr):
|
||||
rec.dump(tdir)
|
||||
|
||||
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()
|
||||
|
||||
|
@ -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)
|
||||
|
||||
|
@ -933,6 +933,9 @@ class MobiReader(object):
|
||||
continue
|
||||
processed_records.append(i)
|
||||
data = self.sections[i][0]
|
||||
if data[:4] in (b'FLIS', b'FCIS', b'SRCS', b'\xe9\x8e\r\n'):
|
||||
# A FLIS, FCIS, SRCS or EOF record, ignore
|
||||
continue
|
||||
buf = cStringIO.StringIO(data)
|
||||
image_index += 1
|
||||
try:
|
||||
|
@ -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
|
||||
@ -1638,6 +1642,61 @@ class MobiWriter(object):
|
||||
for record in self._records:
|
||||
self._write(record)
|
||||
|
||||
def _clean_text_value(self, text):
|
||||
if text is not None and text.strip() :
|
||||
text = text.strip()
|
||||
if not isinstance(text, unicode):
|
||||
text = text.decode('utf-8', 'replace')
|
||||
text = normalize(text).encode('utf-8')
|
||||
else :
|
||||
text = "(none)".encode('utf-8')
|
||||
return text
|
||||
|
||||
def _compute_offset_length(self, i, node, entries) :
|
||||
h = node.href
|
||||
if h not in self._id_offsets:
|
||||
self._oeb.log.warning('Could not find TOC entry:', node.title)
|
||||
return -1, -1
|
||||
|
||||
offset = self._id_offsets[h]
|
||||
length = None
|
||||
# Calculate length based on next entry's offset
|
||||
for sibling in entries[i+1:]:
|
||||
h2 = sibling.href
|
||||
if h2 in self._id_offsets:
|
||||
offset2 = self._id_offsets[h2]
|
||||
if offset2 > offset:
|
||||
length = offset2 - offset
|
||||
break
|
||||
if length is None:
|
||||
length = self._content_length - offset
|
||||
return offset, length
|
||||
|
||||
def _establish_document_structure(self) :
|
||||
documentType = None
|
||||
try :
|
||||
klass = self._ctoc_map[0]['klass']
|
||||
except :
|
||||
klass = None
|
||||
|
||||
if klass == 'chapter' or klass == None :
|
||||
documentType = 'book'
|
||||
if self.opts.verbose > 2 :
|
||||
self._oeb.logger.info("Adding a MobiBook to self._MobiDoc")
|
||||
self._MobiDoc.documentStructure = MobiBook()
|
||||
|
||||
elif klass == 'periodical' :
|
||||
documentType = klass
|
||||
if self.opts.verbose > 2 :
|
||||
self._oeb.logger.info("Adding a MobiPeriodical to self._MobiDoc")
|
||||
self._MobiDoc.documentStructure = MobiPeriodical(self._MobiDoc.getNextNode())
|
||||
self._MobiDoc.documentStructure.startAddress = self._anchor_offset_kindle
|
||||
else :
|
||||
raise NotImplementedError('_establish_document_structure: unrecognized klass: %s' % klass)
|
||||
return documentType
|
||||
|
||||
# Index {{{
|
||||
|
||||
def _generate_index(self):
|
||||
self._oeb.log('Generating INDX ...')
|
||||
self._primary_index_record = None
|
||||
@ -1811,276 +1870,7 @@ class MobiWriter(object):
|
||||
open(os.path.join(t, n+'.bin'), 'wb').write(self._records[-(i+1)])
|
||||
self._oeb.log.debug('Index records dumped to', t)
|
||||
|
||||
def _clean_text_value(self, text):
|
||||
if text is not None and text.strip() :
|
||||
text = text.strip()
|
||||
if not isinstance(text, unicode):
|
||||
text = text.decode('utf-8', 'replace')
|
||||
text = normalize(text).encode('utf-8')
|
||||
else :
|
||||
text = "(none)".encode('utf-8')
|
||||
return text
|
||||
|
||||
def _add_to_ctoc(self, ctoc_str, record_offset):
|
||||
# Write vwilen + string to ctoc
|
||||
# Return offset
|
||||
# Is there enough room for this string in the current ctoc record?
|
||||
if 0xfbf8 - self._ctoc.tell() < 2 + len(ctoc_str):
|
||||
# flush this ctoc, start a new one
|
||||
# print "closing ctoc_record at 0x%X" % self._ctoc.tell()
|
||||
# print "starting new ctoc with '%-50.50s ...'" % ctoc_str
|
||||
# pad with 00
|
||||
pad = 0xfbf8 - self._ctoc.tell()
|
||||
# print "padding %d bytes of 00" % pad
|
||||
self._ctoc.write('\0' * (pad))
|
||||
self._ctoc_records.append(self._ctoc.getvalue())
|
||||
self._ctoc.truncate(0)
|
||||
self._ctoc_offset += 0x10000
|
||||
record_offset = self._ctoc_offset
|
||||
|
||||
offset = self._ctoc.tell() + record_offset
|
||||
self._ctoc.write(decint(len(ctoc_str), DECINT_FORWARD) + ctoc_str)
|
||||
return offset
|
||||
|
||||
def _add_flat_ctoc_node(self, node, ctoc, title=None):
|
||||
# Process 'chapter' or 'article' nodes only, force either to 'chapter'
|
||||
t = node.title if title is None else title
|
||||
t = self._clean_text_value(t)
|
||||
self._last_toc_entry = t
|
||||
|
||||
# Create an empty dictionary for this node
|
||||
ctoc_name_map = {}
|
||||
|
||||
# article = chapter
|
||||
if node.klass == 'article' :
|
||||
ctoc_name_map['klass'] = 'chapter'
|
||||
else :
|
||||
ctoc_name_map['klass'] = node.klass
|
||||
|
||||
# Add title offset to name map
|
||||
ctoc_name_map['titleOffset'] = self._add_to_ctoc(t, self._ctoc_offset)
|
||||
self._chapterCount += 1
|
||||
|
||||
# append this node's name_map to map
|
||||
self._ctoc_map.append(ctoc_name_map)
|
||||
|
||||
return
|
||||
|
||||
def _add_structured_ctoc_node(self, node, ctoc, title=None):
|
||||
# Process 'periodical', 'section' and 'article'
|
||||
|
||||
# Fetch the offset referencing the current ctoc_record
|
||||
if node.klass is None :
|
||||
return
|
||||
t = node.title if title is None else title
|
||||
t = self._clean_text_value(t)
|
||||
self._last_toc_entry = t
|
||||
|
||||
# Create an empty dictionary for this node
|
||||
ctoc_name_map = {}
|
||||
|
||||
# Add the klass of this node
|
||||
ctoc_name_map['klass'] = node.klass
|
||||
|
||||
if node.klass == 'chapter':
|
||||
# Add title offset to name map
|
||||
ctoc_name_map['titleOffset'] = self._add_to_ctoc(t, self._ctoc_offset)
|
||||
self._chapterCount += 1
|
||||
|
||||
elif node.klass == 'periodical' :
|
||||
# Add title offset
|
||||
ctoc_name_map['titleOffset'] = self._add_to_ctoc(t, self._ctoc_offset)
|
||||
|
||||
# Look for existing class entry 'periodical' in _ctoc_map
|
||||
for entry in self._ctoc_map:
|
||||
if entry['klass'] == 'periodical':
|
||||
# Use the pre-existing instance
|
||||
ctoc_name_map['classOffset'] = entry['classOffset']
|
||||
break
|
||||
else :
|
||||
continue
|
||||
else:
|
||||
# class names should always be in CNCX 0 - no offset
|
||||
ctoc_name_map['classOffset'] = self._add_to_ctoc(node.klass, 0)
|
||||
|
||||
self._periodicalCount += 1
|
||||
|
||||
elif node.klass == 'section' :
|
||||
# Add title offset
|
||||
ctoc_name_map['titleOffset'] = self._add_to_ctoc(t, self._ctoc_offset)
|
||||
|
||||
# Look for existing class entry 'section' in _ctoc_map
|
||||
for entry in self._ctoc_map:
|
||||
if entry['klass'] == 'section':
|
||||
# Use the pre-existing instance
|
||||
ctoc_name_map['classOffset'] = entry['classOffset']
|
||||
break
|
||||
else :
|
||||
continue
|
||||
else:
|
||||
# class names should always be in CNCX 0 - no offset
|
||||
ctoc_name_map['classOffset'] = self._add_to_ctoc(node.klass, 0)
|
||||
|
||||
self._sectionCount += 1
|
||||
|
||||
elif node.klass == 'article' :
|
||||
# Add title offset/title
|
||||
ctoc_name_map['titleOffset'] = self._add_to_ctoc(t, self._ctoc_offset)
|
||||
|
||||
# Look for existing class entry 'article' in _ctoc_map
|
||||
for entry in self._ctoc_map:
|
||||
if entry['klass'] == 'article':
|
||||
ctoc_name_map['classOffset'] = entry['classOffset']
|
||||
break
|
||||
else :
|
||||
continue
|
||||
else:
|
||||
# class names should always be in CNCX 0 - no offset
|
||||
ctoc_name_map['classOffset'] = self._add_to_ctoc(node.klass, 0)
|
||||
|
||||
# Add description offset/description
|
||||
if node.description :
|
||||
d = self._clean_text_value(node.description)
|
||||
ctoc_name_map['descriptionOffset'] = self._add_to_ctoc(d, self._ctoc_offset)
|
||||
else :
|
||||
ctoc_name_map['descriptionOffset'] = None
|
||||
|
||||
# Add author offset/attribution
|
||||
if node.author :
|
||||
a = self._clean_text_value(node.author)
|
||||
ctoc_name_map['authorOffset'] = self._add_to_ctoc(a, self._ctoc_offset)
|
||||
else :
|
||||
ctoc_name_map['authorOffset'] = None
|
||||
|
||||
self._articleCount += 1
|
||||
|
||||
else :
|
||||
raise NotImplementedError( \
|
||||
'writer._generate_ctoc.add_node: title: %s has unrecognized klass: %s, playOrder: %d' % \
|
||||
(node.title, node.klass, node.play_order))
|
||||
|
||||
# append this node's name_map to map
|
||||
self._ctoc_map.append(ctoc_name_map)
|
||||
|
||||
def _generate_ctoc(self):
|
||||
# Generate the compiled TOC strings
|
||||
# Each node has 1-4 CTOC entries:
|
||||
# Periodical (0xDF)
|
||||
# title, class
|
||||
# Section (0xFF)
|
||||
# title, class
|
||||
# Article (0x3F)
|
||||
# title, class, description, author
|
||||
# Chapter (0x0F)
|
||||
# title, class
|
||||
# nb: Chapters don't actually have @class, so we synthesize it
|
||||
# in reader._toc_from_navpoint
|
||||
|
||||
toc = self._oeb.toc
|
||||
reduced_toc = []
|
||||
self._ctoc_map = [] # per node dictionary of {class/title/desc/author} offsets
|
||||
self._last_toc_entry = None
|
||||
#ctoc = StringIO()
|
||||
self._ctoc = StringIO()
|
||||
|
||||
# Track the individual node types
|
||||
self._periodicalCount = 0
|
||||
self._sectionCount = 0
|
||||
self._articleCount = 0
|
||||
self._chapterCount = 0
|
||||
|
||||
#first = True
|
||||
|
||||
if self._conforming_periodical_toc :
|
||||
self._oeb.logger.info('Generating structured CTOC ...')
|
||||
for (child) in toc.iter():
|
||||
if self.opts.verbose > 2 :
|
||||
self._oeb.logger.info(" %s" % child)
|
||||
self._add_structured_ctoc_node(child, self._ctoc)
|
||||
#first = False
|
||||
|
||||
else :
|
||||
self._oeb.logger.info('Generating flat CTOC ...')
|
||||
previousOffset = -1
|
||||
currentOffset = 0
|
||||
for (i, child) in enumerate(toc.iterdescendants()):
|
||||
# Only add chapters or articles at depth==1
|
||||
# no class defaults to 'chapter'
|
||||
if child.klass is None : child.klass = 'chapter'
|
||||
if (child.klass == 'article' or child.klass == 'chapter') and child.depth() == 1 :
|
||||
if self.opts.verbose > 2 :
|
||||
self._oeb.logger.info("adding (klass:%s depth:%d) %s to flat ctoc" % \
|
||||
(child.klass, child.depth(), child) )
|
||||
|
||||
# Test to see if this child's offset is the same as the previous child's
|
||||
# offset, skip it
|
||||
h = child.href
|
||||
|
||||
if h is None:
|
||||
self._oeb.logger.warn(' Ignoring TOC entry with no href:',
|
||||
child.title)
|
||||
continue
|
||||
if h not in self._id_offsets:
|
||||
self._oeb.logger.warn(' Ignoring missing TOC entry:',
|
||||
unicode(child))
|
||||
continue
|
||||
|
||||
currentOffset = self._id_offsets[h]
|
||||
# print "_generate_ctoc: child offset: 0x%X" % currentOffset
|
||||
|
||||
if currentOffset != previousOffset :
|
||||
self._add_flat_ctoc_node(child, self._ctoc)
|
||||
reduced_toc.append(child)
|
||||
previousOffset = currentOffset
|
||||
else :
|
||||
self._oeb.logger.warn(" Ignoring redundant href: %s in '%s'" % (h, child.title))
|
||||
|
||||
else :
|
||||
if self.opts.verbose > 2 :
|
||||
self._oeb.logger.info("skipping class: %s depth %d at position %d" % \
|
||||
(child.klass, child.depth(),i))
|
||||
|
||||
# Update the TOC with our edited version
|
||||
self._oeb.toc.nodes = reduced_toc
|
||||
|
||||
# Instantiate a MobiDocument(mobitype)
|
||||
if (not self._periodicalCount and not self._sectionCount and not self._articleCount) or \
|
||||
not self.opts.mobi_periodical :
|
||||
mobiType = 0x002
|
||||
elif self._periodicalCount:
|
||||
pt = None
|
||||
if self._oeb.metadata.publication_type:
|
||||
x = unicode(self._oeb.metadata.publication_type[0]).split(':')
|
||||
if len(x) > 1:
|
||||
pt = x[1]
|
||||
mobiType = {'newspaper':0x101}.get(pt, 0x103)
|
||||
else :
|
||||
raise NotImplementedError('_generate_ctoc: Unrecognized document structured')
|
||||
|
||||
self._MobiDoc = MobiDocument(mobiType)
|
||||
|
||||
if self.opts.verbose > 2 :
|
||||
structType = 'book'
|
||||
if mobiType > 0x100 :
|
||||
structType = 'flat periodical' if mobiType == 0x102 else 'structured periodical'
|
||||
self._oeb.logger.info("Instantiating a %s MobiDocument of type 0x%X" % (structType, mobiType ) )
|
||||
if mobiType > 0x100 :
|
||||
self._oeb.logger.info("periodicalCount: %d sectionCount: %d articleCount: %d"% \
|
||||
(self._periodicalCount, self._sectionCount, self._articleCount) )
|
||||
else :
|
||||
self._oeb.logger.info("chapterCount: %d" % self._chapterCount)
|
||||
|
||||
# Apparently the CTOC must end with a null byte
|
||||
self._ctoc.write('\0')
|
||||
|
||||
ctoc = self._ctoc.getvalue()
|
||||
rec_count = len(self._ctoc_records)
|
||||
self._oeb.logger.info(" CNCX utilization: %d %s %.0f%% full" % \
|
||||
(rec_count + 1, 'records, last record' if rec_count else 'record,',
|
||||
len(ctoc)/655) )
|
||||
|
||||
return align_block(ctoc)
|
||||
|
||||
# Index nodes {{{
|
||||
def _write_periodical_node(self, indxt, indices, index, offset, length, count, firstSection, lastSection) :
|
||||
pos = 0xc0 + indxt.tell()
|
||||
indices.write(pack('>H', pos)) # Save the offset for IDXTIndices
|
||||
@ -2172,48 +1962,8 @@ class MobiWriter(object):
|
||||
indxt.write(decint(self._ctoc_map[index]['titleOffset'], DECINT_FORWARD)) # vwi title offset in CNCX
|
||||
indxt.write(decint(0, DECINT_FORWARD)) # unknown byte
|
||||
|
||||
def _compute_offset_length(self, i, node, entries) :
|
||||
h = node.href
|
||||
if h not in self._id_offsets:
|
||||
self._oeb.log.warning('Could not find TOC entry:', node.title)
|
||||
return -1, -1
|
||||
# }}}
|
||||
|
||||
offset = self._id_offsets[h]
|
||||
length = None
|
||||
# Calculate length based on next entry's offset
|
||||
for sibling in entries[i+1:]:
|
||||
h2 = sibling.href
|
||||
if h2 in self._id_offsets:
|
||||
offset2 = self._id_offsets[h2]
|
||||
if offset2 > offset:
|
||||
length = offset2 - offset
|
||||
break
|
||||
if length is None:
|
||||
length = self._content_length - offset
|
||||
return offset, length
|
||||
|
||||
def _establish_document_structure(self) :
|
||||
documentType = None
|
||||
try :
|
||||
klass = self._ctoc_map[0]['klass']
|
||||
except :
|
||||
klass = None
|
||||
|
||||
if klass == 'chapter' or klass == None :
|
||||
documentType = 'book'
|
||||
if self.opts.verbose > 2 :
|
||||
self._oeb.logger.info("Adding a MobiBook to self._MobiDoc")
|
||||
self._MobiDoc.documentStructure = MobiBook()
|
||||
|
||||
elif klass == 'periodical' :
|
||||
documentType = klass
|
||||
if self.opts.verbose > 2 :
|
||||
self._oeb.logger.info("Adding a MobiPeriodical to self._MobiDoc")
|
||||
self._MobiDoc.documentStructure = MobiPeriodical(self._MobiDoc.getNextNode())
|
||||
self._MobiDoc.documentStructure.startAddress = self._anchor_offset_kindle
|
||||
else :
|
||||
raise NotImplementedError('_establish_document_structure: unrecognized klass: %s' % klass)
|
||||
return documentType
|
||||
|
||||
def _generate_section_indices(self, child, currentSection, myPeriodical, myDoc ) :
|
||||
sectionTitles = list(child.iter())[1:]
|
||||
@ -2491,6 +2241,270 @@ class MobiWriter(object):
|
||||
last_name, c = self._add_periodical_structured_articles(myDoc, indxt, indices)
|
||||
|
||||
return align_block(indxt.getvalue()), c, align_block(indices.getvalue()), last_name
|
||||
# }}}
|
||||
|
||||
# CTOC {{{
|
||||
def _add_to_ctoc(self, ctoc_str, record_offset):
|
||||
# Write vwilen + string to ctoc
|
||||
# Return offset
|
||||
# Is there enough room for this string in the current ctoc record?
|
||||
if 0xfbf8 - self._ctoc.tell() < 2 + len(ctoc_str):
|
||||
# flush this ctoc, start a new one
|
||||
# print "closing ctoc_record at 0x%X" % self._ctoc.tell()
|
||||
# print "starting new ctoc with '%-50.50s ...'" % ctoc_str
|
||||
# pad with 00
|
||||
pad = 0xfbf8 - self._ctoc.tell()
|
||||
# print "padding %d bytes of 00" % pad
|
||||
self._ctoc.write('\0' * (pad))
|
||||
self._ctoc_records.append(self._ctoc.getvalue())
|
||||
self._ctoc.truncate(0)
|
||||
self._ctoc_offset += 0x10000
|
||||
record_offset = self._ctoc_offset
|
||||
|
||||
offset = self._ctoc.tell() + record_offset
|
||||
self._ctoc.write(decint(len(ctoc_str), DECINT_FORWARD) + ctoc_str)
|
||||
return offset
|
||||
|
||||
def _add_flat_ctoc_node(self, node, ctoc, title=None):
|
||||
# Process 'chapter' or 'article' nodes only, force either to 'chapter'
|
||||
t = node.title if title is None else title
|
||||
t = self._clean_text_value(t)
|
||||
self._last_toc_entry = t
|
||||
|
||||
# Create an empty dictionary for this node
|
||||
ctoc_name_map = {}
|
||||
|
||||
# article = chapter
|
||||
if node.klass == 'article' :
|
||||
ctoc_name_map['klass'] = 'chapter'
|
||||
else :
|
||||
ctoc_name_map['klass'] = node.klass
|
||||
|
||||
# Add title offset to name map
|
||||
ctoc_name_map['titleOffset'] = self._add_to_ctoc(t, self._ctoc_offset)
|
||||
self._chapterCount += 1
|
||||
|
||||
# append this node's name_map to map
|
||||
self._ctoc_map.append(ctoc_name_map)
|
||||
|
||||
return
|
||||
|
||||
def _add_structured_ctoc_node(self, node, ctoc, title=None):
|
||||
# Process 'periodical', 'section' and 'article'
|
||||
|
||||
# Fetch the offset referencing the current ctoc_record
|
||||
if node.klass is None :
|
||||
return
|
||||
t = node.title if title is None else title
|
||||
t = self._clean_text_value(t)
|
||||
self._last_toc_entry = t
|
||||
|
||||
# Create an empty dictionary for this node
|
||||
ctoc_name_map = {}
|
||||
|
||||
# Add the klass of this node
|
||||
ctoc_name_map['klass'] = node.klass
|
||||
|
||||
if node.klass == 'chapter':
|
||||
# Add title offset to name map
|
||||
ctoc_name_map['titleOffset'] = self._add_to_ctoc(t, self._ctoc_offset)
|
||||
self._chapterCount += 1
|
||||
|
||||
elif node.klass == 'periodical' :
|
||||
# Add title offset
|
||||
ctoc_name_map['titleOffset'] = self._add_to_ctoc(t, self._ctoc_offset)
|
||||
|
||||
# Look for existing class entry 'periodical' in _ctoc_map
|
||||
for entry in self._ctoc_map:
|
||||
if entry['klass'] == 'periodical':
|
||||
# Use the pre-existing instance
|
||||
ctoc_name_map['classOffset'] = entry['classOffset']
|
||||
break
|
||||
else :
|
||||
continue
|
||||
else:
|
||||
# class names should always be in CNCX 0 - no offset
|
||||
ctoc_name_map['classOffset'] = self._add_to_ctoc(node.klass, 0)
|
||||
|
||||
self._periodicalCount += 1
|
||||
|
||||
elif node.klass == 'section' :
|
||||
# Add title offset
|
||||
ctoc_name_map['titleOffset'] = self._add_to_ctoc(t, self._ctoc_offset)
|
||||
|
||||
# Look for existing class entry 'section' in _ctoc_map
|
||||
for entry in self._ctoc_map:
|
||||
if entry['klass'] == 'section':
|
||||
# Use the pre-existing instance
|
||||
ctoc_name_map['classOffset'] = entry['classOffset']
|
||||
break
|
||||
else :
|
||||
continue
|
||||
else:
|
||||
# class names should always be in CNCX 0 - no offset
|
||||
ctoc_name_map['classOffset'] = self._add_to_ctoc(node.klass, 0)
|
||||
|
||||
self._sectionCount += 1
|
||||
|
||||
elif node.klass == 'article' :
|
||||
# Add title offset/title
|
||||
ctoc_name_map['titleOffset'] = self._add_to_ctoc(t, self._ctoc_offset)
|
||||
|
||||
# Look for existing class entry 'article' in _ctoc_map
|
||||
for entry in self._ctoc_map:
|
||||
if entry['klass'] == 'article':
|
||||
ctoc_name_map['classOffset'] = entry['classOffset']
|
||||
break
|
||||
else :
|
||||
continue
|
||||
else:
|
||||
# class names should always be in CNCX 0 - no offset
|
||||
ctoc_name_map['classOffset'] = self._add_to_ctoc(node.klass, 0)
|
||||
|
||||
# Add description offset/description
|
||||
if node.description :
|
||||
d = self._clean_text_value(node.description)
|
||||
ctoc_name_map['descriptionOffset'] = self._add_to_ctoc(d, self._ctoc_offset)
|
||||
else :
|
||||
ctoc_name_map['descriptionOffset'] = None
|
||||
|
||||
# Add author offset/attribution
|
||||
if node.author :
|
||||
a = self._clean_text_value(node.author)
|
||||
ctoc_name_map['authorOffset'] = self._add_to_ctoc(a, self._ctoc_offset)
|
||||
else :
|
||||
ctoc_name_map['authorOffset'] = None
|
||||
|
||||
self._articleCount += 1
|
||||
|
||||
else :
|
||||
raise NotImplementedError( \
|
||||
'writer._generate_ctoc.add_node: title: %s has unrecognized klass: %s, playOrder: %d' % \
|
||||
(node.title, node.klass, node.play_order))
|
||||
|
||||
# append this node's name_map to map
|
||||
self._ctoc_map.append(ctoc_name_map)
|
||||
|
||||
def _generate_ctoc(self):
|
||||
# Generate the compiled TOC strings
|
||||
# Each node has 1-4 CTOC entries:
|
||||
# Periodical (0xDF)
|
||||
# title, class
|
||||
# Section (0xFF)
|
||||
# title, class
|
||||
# Article (0x3F)
|
||||
# title, class, description, author
|
||||
# Chapter (0x0F)
|
||||
# title, class
|
||||
# nb: Chapters don't actually have @class, so we synthesize it
|
||||
# in reader._toc_from_navpoint
|
||||
|
||||
toc = self._oeb.toc
|
||||
reduced_toc = []
|
||||
self._ctoc_map = [] # per node dictionary of {class/title/desc/author} offsets
|
||||
self._last_toc_entry = None
|
||||
#ctoc = StringIO()
|
||||
self._ctoc = StringIO()
|
||||
|
||||
# Track the individual node types
|
||||
self._periodicalCount = 0
|
||||
self._sectionCount = 0
|
||||
self._articleCount = 0
|
||||
self._chapterCount = 0
|
||||
|
||||
#first = True
|
||||
|
||||
if self._conforming_periodical_toc :
|
||||
self._oeb.logger.info('Generating structured CTOC ...')
|
||||
for (child) in toc.iter():
|
||||
if self.opts.verbose > 2 :
|
||||
self._oeb.logger.info(" %s" % child)
|
||||
self._add_structured_ctoc_node(child, self._ctoc)
|
||||
#first = False
|
||||
|
||||
else :
|
||||
self._oeb.logger.info('Generating flat CTOC ...')
|
||||
previousOffset = -1
|
||||
currentOffset = 0
|
||||
for (i, child) in enumerate(toc.iterdescendants()):
|
||||
# Only add chapters or articles at depth==1
|
||||
# no class defaults to 'chapter'
|
||||
if child.klass is None : child.klass = 'chapter'
|
||||
if (child.klass == 'article' or child.klass == 'chapter') and child.depth() == 1 :
|
||||
if self.opts.verbose > 2 :
|
||||
self._oeb.logger.info("adding (klass:%s depth:%d) %s to flat ctoc" % \
|
||||
(child.klass, child.depth(), child) )
|
||||
|
||||
# Test to see if this child's offset is the same as the previous child's
|
||||
# offset, skip it
|
||||
h = child.href
|
||||
|
||||
if h is None:
|
||||
self._oeb.logger.warn(' Ignoring TOC entry with no href:',
|
||||
child.title)
|
||||
continue
|
||||
if h not in self._id_offsets:
|
||||
self._oeb.logger.warn(' Ignoring missing TOC entry:',
|
||||
unicode(child))
|
||||
continue
|
||||
|
||||
currentOffset = self._id_offsets[h]
|
||||
# print "_generate_ctoc: child offset: 0x%X" % currentOffset
|
||||
|
||||
if currentOffset != previousOffset :
|
||||
self._add_flat_ctoc_node(child, self._ctoc)
|
||||
reduced_toc.append(child)
|
||||
previousOffset = currentOffset
|
||||
else :
|
||||
self._oeb.logger.warn(" Ignoring redundant href: %s in '%s'" % (h, child.title))
|
||||
|
||||
else :
|
||||
if self.opts.verbose > 2 :
|
||||
self._oeb.logger.info("skipping class: %s depth %d at position %d" % \
|
||||
(child.klass, child.depth(),i))
|
||||
|
||||
# Update the TOC with our edited version
|
||||
self._oeb.toc.nodes = reduced_toc
|
||||
|
||||
# Instantiate a MobiDocument(mobitype)
|
||||
if (not self._periodicalCount and not self._sectionCount and not self._articleCount) or \
|
||||
not self.opts.mobi_periodical :
|
||||
mobiType = 0x002
|
||||
elif self._periodicalCount:
|
||||
pt = None
|
||||
if self._oeb.metadata.publication_type:
|
||||
x = unicode(self._oeb.metadata.publication_type[0]).split(':')
|
||||
if len(x) > 1:
|
||||
pt = x[1]
|
||||
mobiType = {'newspaper':0x101}.get(pt, 0x103)
|
||||
else :
|
||||
raise NotImplementedError('_generate_ctoc: Unrecognized document structured')
|
||||
|
||||
self._MobiDoc = MobiDocument(mobiType)
|
||||
|
||||
if self.opts.verbose > 2 :
|
||||
structType = 'book'
|
||||
if mobiType > 0x100 :
|
||||
structType = 'flat periodical' if mobiType == 0x102 else 'structured periodical'
|
||||
self._oeb.logger.info("Instantiating a %s MobiDocument of type 0x%X" % (structType, mobiType ) )
|
||||
if mobiType > 0x100 :
|
||||
self._oeb.logger.info("periodicalCount: %d sectionCount: %d articleCount: %d"% \
|
||||
(self._periodicalCount, self._sectionCount, self._articleCount) )
|
||||
else :
|
||||
self._oeb.logger.info("chapterCount: %d" % self._chapterCount)
|
||||
|
||||
# Apparently the CTOC must end with a null byte
|
||||
self._ctoc.write('\0')
|
||||
|
||||
ctoc = self._ctoc.getvalue()
|
||||
rec_count = len(self._ctoc_records)
|
||||
self._oeb.logger.info(" CNCX utilization: %d %s %.0f%% full" % \
|
||||
(rec_count + 1, 'records, last record' if rec_count else 'record,',
|
||||
len(ctoc)/655) )
|
||||
|
||||
return align_block(ctoc)
|
||||
|
||||
# }}}
|
||||
|
||||
class HTMLRecordData(object):
|
||||
""" A data structure containing indexing/navigation data for an HTML record """
|
||||
|
15
src/calibre/ebooks/mobi/writer2/__init__.py
Normal file
15
src/calibre/ebooks/mobi/writer2/__init__.py
Normal 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
|
||||
|
502
src/calibre/ebooks/mobi/writer2/main.py
Normal file
502
src/calibre/ebooks/mobi/writer2/main.py
Normal 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)
|
||||
|
||||
|
246
src/calibre/ebooks/mobi/writer2/serializer.py
Normal file
246
src/calibre/ebooks/mobi/writer2/serializer.py
Normal 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('&', '&')
|
||||
text = text.replace('<', '<')
|
||||
text = text.replace('>', '>')
|
||||
text = text.replace(u'\u00AD', '') # Soft-hyphen
|
||||
if quot:
|
||||
text = text.replace('"', '"')
|
||||
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)
|
||||
|
||||
|
177
src/calibre/ebooks/mobi/writer2/utils.py
Normal file
177
src/calibre/ebooks/mobi/writer2/utils.py
Normal file
@ -0,0 +1,177 @@
|
||||
#!/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 collections import OrderedDict
|
||||
|
||||
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
|
||||
|
||||
def get_trailing_data(record, extra_data_flags):
|
||||
'''
|
||||
Given a text record as a bytestring and the extra data flags from the MOBI
|
||||
header, return the trailing data as a dictionary, mapping bit number to
|
||||
data as bytestring. Also returns the record - all trailing data.
|
||||
|
||||
:return: Trailing data, record - trailing data
|
||||
'''
|
||||
data = OrderedDict()
|
||||
for i in xrange(16, -1, -1):
|
||||
flag = 2**i
|
||||
if flag & extra_data_flags:
|
||||
if i == 0:
|
||||
# Only the first two bits are used for the size since there can
|
||||
# never be more than 3 trailing multibyte chars
|
||||
sz = (ord(record[-1]) & 0b11) + 1
|
||||
consumed = 1
|
||||
else:
|
||||
sz, consumed = decint(record, forward=False)
|
||||
if sz > consumed:
|
||||
data[i] = record[-sz:-consumed]
|
||||
record = record[:-sz]
|
||||
return data, record
|
||||
|
@ -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):
|
||||
|
@ -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()
|
||||
|
@ -163,6 +163,8 @@ class OEBReader(object):
|
||||
if item.media_type in check:
|
||||
try:
|
||||
item.data
|
||||
except KeyboardInterrupt:
|
||||
raise
|
||||
except:
|
||||
self.logger.exception('Failed to parse content in %s'%
|
||||
item.href)
|
||||
@ -186,8 +188,13 @@ class OEBReader(object):
|
||||
href, _ = urldefrag(href)
|
||||
if not href:
|
||||
continue
|
||||
href = item.abshref(urlnormalize(href))
|
||||
scheme = urlparse(href).scheme
|
||||
try:
|
||||
href = item.abshref(urlnormalize(href))
|
||||
scheme = urlparse(href).scheme
|
||||
except:
|
||||
self.oeb.log.exception(
|
||||
'Skipping invalid href: %r'%href)
|
||||
continue
|
||||
if not scheme and href not in known:
|
||||
new.add(href)
|
||||
elif item.media_type in OEB_STYLES:
|
||||
|
@ -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
|
||||
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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):
|
||||
|
@ -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')
|
||||
|
@ -47,7 +47,10 @@ class ManifestTrimmer(object):
|
||||
item.data is not None:
|
||||
hrefs = [r[2] for r in iterlinks(item.data)]
|
||||
for href in hrefs:
|
||||
href = item.abshref(urlnormalize(href))
|
||||
try:
|
||||
href = item.abshref(urlnormalize(href))
|
||||
except:
|
||||
continue
|
||||
if href in oeb.manifest.hrefs:
|
||||
found = oeb.manifest.hrefs[href]
|
||||
if found not in used:
|
||||
|
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -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):
|
||||
|
@ -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)
|
||||
|
@ -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 [
|
||||
|
@ -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 &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><p>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 &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 &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>&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>&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 &ligatures</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="12" column="0" colspan="5">
|
||||
<widget class="QGroupBox" name="groupBox">
|
||||
<property name="title">
|
||||
<string>Extra &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>&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><p>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 &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 &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 &line height:</string>
|
||||
<string>&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>&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>
|
||||
|
@ -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
|
||||
|
@ -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 &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 &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 &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 &margins</string>
|
||||
|
@ -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:
|
||||
|
@ -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>&Stop selected job</string>
|
||||
<string>&Stop selected jobs</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
|
@ -268,7 +268,8 @@ class JobManager(QAbstractTableModel): # {{{
|
||||
# }}}
|
||||
|
||||
# Jobs UI {{{
|
||||
class ProgressBarDelegate(QAbstractItemDelegate):
|
||||
|
||||
class ProgressBarDelegate(QAbstractItemDelegate): # {{{
|
||||
|
||||
def sizeHint(self, option, index):
|
||||
return QSize(120, 30)
|
||||
@ -285,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)
|
||||
@ -319,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)
|
||||
@ -405,6 +408,7 @@ class JobsButton(QFrame):
|
||||
self.stop()
|
||||
QCoreApplication.instance().alert(self, 5000)
|
||||
|
||||
# }}}
|
||||
|
||||
class JobsDialog(QDialog, Ui_JobsDialog):
|
||||
|
||||
@ -447,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)
|
||||
@ -456,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):
|
||||
|
@ -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): # {{{
|
||||
|
@ -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): # {{{
|
||||
|
@ -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()
|
||||
|
@ -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 &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&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>&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>
|
||||
|
@ -4,3 +4,4 @@ or asked not to be included in the store integration.
|
||||
* Borders (http://www.borders.com/).
|
||||
* Indigo (http://www.chapters.indigo.ca/).
|
||||
* Libraria Rizzoli (http://libreriarizzoli.corriere.it/).
|
||||
* EPubBuy DE: reason: too much traffic for too little sales
|
||||
|
78
src/calibre/gui2/store/stores/bookoteka_plugin.py
Normal file
78
src/calibre/gui2/store/stores/bookoteka_plugin.py
Normal file
@ -0,0 +1,78 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from __future__ import (unicode_literals, division, absolute_import, print_function)
|
||||
|
||||
__license__ = 'GPL 3'
|
||||
__copyright__ = '2011, Tomasz Długosz <tomek3d@gmail.com>'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
import re
|
||||
import urllib
|
||||
from contextlib import closing
|
||||
|
||||
from lxml import html
|
||||
|
||||
from PyQt4.Qt import QUrl
|
||||
|
||||
from calibre import browser, url_slash_cleaner
|
||||
from calibre.gui2 import open_url
|
||||
from calibre.gui2.store import StorePlugin
|
||||
from calibre.gui2.store.basic_config import BasicStoreConfig
|
||||
from calibre.gui2.store.search_result import SearchResult
|
||||
from calibre.gui2.store.web_store_dialog import WebStoreDialog
|
||||
|
||||
class BookotekaStore(BasicStoreConfig, StorePlugin):
|
||||
|
||||
def open(self, parent=None, detail_item=None, external=False):
|
||||
|
||||
url = 'http://bookoteka.pl/ebooki'
|
||||
detail_url = None
|
||||
|
||||
if detail_item:
|
||||
detail_url = detail_item
|
||||
|
||||
if external or self.config.get('open_external', False):
|
||||
open_url(QUrl(url_slash_cleaner(detail_url if detail_url else url)))
|
||||
else:
|
||||
d = WebStoreDialog(self.gui, url, parent, detail_url)
|
||||
d.setWindowTitle(self.name)
|
||||
d.set_tags(self.config.get('tags', ''))
|
||||
d.exec_()
|
||||
|
||||
def search(self, query, max_results=10, timeout=60):
|
||||
url = 'http://bookoteka.pl/list?search=' + urllib.quote_plus(query) + '&cat=1&hp=1&type=1'
|
||||
|
||||
br = browser()
|
||||
|
||||
counter = max_results
|
||||
with closing(br.open(url, timeout=timeout)) as f:
|
||||
doc = html.fromstring(f.read())
|
||||
for data in doc.xpath('//li[@class="EBOOK"]'):
|
||||
if counter <= 0:
|
||||
break
|
||||
|
||||
id = ''.join(data.xpath('.//a[@class="item_link"]/@href'))
|
||||
if not id:
|
||||
continue
|
||||
|
||||
cover_url = ''.join(data.xpath('.//a[@class="item_link"]/@style'))
|
||||
cover_url = re.sub(r'.*\(', '', cover_url)
|
||||
cover_url = re.sub(r'\).*', '', cover_url)
|
||||
title = ''.join(data.xpath('.//div[@class="shelf_title"]/a/text()'))
|
||||
author = ''.join(data.xpath('.//div[@class="shelf_authors"]/text()'))
|
||||
price = ''.join(data.xpath('.//span[@class="EBOOK"]/text()'))
|
||||
price = price.replace('.', ',')
|
||||
formats = ', '.join(data.xpath('.//a[@class="fancybox protected"]/text()'))
|
||||
|
||||
counter -= 1
|
||||
|
||||
s = SearchResult()
|
||||
s.cover_url = 'http://bookoteka.pl' + cover_url
|
||||
s.title = title.strip()
|
||||
s.author = author.strip()
|
||||
s.price = price
|
||||
s.detail_item = 'http://bookoteka.pl' + id.strip()
|
||||
s.drm = SearchResult.DRM_UNLOCKED
|
||||
s.formats = formats.strip()
|
||||
|
||||
yield s
|
140
src/calibre/gui2/store/stores/chitanka_plugin.py
Normal file
140
src/calibre/gui2/store/stores/chitanka_plugin.py
Normal file
@ -0,0 +1,140 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from __future__ import (unicode_literals, division, absolute_import, print_function)
|
||||
|
||||
__license__ = 'GPL 3'
|
||||
__copyright__ = '2011, Alex Stanev <alex@stanev.org>'
|
||||
__docformat__ = 'restructuredtext en'
|
||||
|
||||
import re
|
||||
import urllib
|
||||
from contextlib import closing
|
||||
|
||||
from lxml import html
|
||||
|
||||
from PyQt4.Qt import QUrl
|
||||
|
||||
from calibre import browser, url_slash_cleaner
|
||||
from calibre.gui2 import open_url
|
||||
from calibre.gui2.store import StorePlugin
|
||||
from calibre.gui2.store.basic_config import BasicStoreConfig
|
||||
from calibre.gui2.store.search_result import SearchResult
|
||||
from calibre.gui2.store.web_store_dialog import WebStoreDialog
|
||||
|
||||
class ChitankaStore(BasicStoreConfig, StorePlugin):
|
||||
|
||||
def open(self, parent=None, detail_item=None, external=False):
|
||||
url = 'http://chitanka.info'
|
||||
|
||||
if external or self.config.get('open_external', False):
|
||||
if detail_item:
|
||||
url = url + detail_item
|
||||
open_url(QUrl(url_slash_cleaner(url)))
|
||||
else:
|
||||
detail_url = None
|
||||
if detail_item:
|
||||
detail_url = url + detail_item
|
||||
d = WebStoreDialog(self.gui, url, parent, detail_url)
|
||||
d.setWindowTitle(self.name)
|
||||
d.set_tags(self.config.get('tags', ''))
|
||||
d.exec_()
|
||||
|
||||
def search(self, query, max_results=10, timeout=60):
|
||||
|
||||
base_url = 'http://chitanka.info'
|
||||
url = base_url + '/search?q=' + urllib.quote(query)
|
||||
counter = max_results
|
||||
|
||||
# search for book title
|
||||
br = browser()
|
||||
with closing(br.open(url, timeout=timeout)) as f:
|
||||
f = unicode(f.read(), 'utf-8')
|
||||
doc = html.fromstring(f)
|
||||
|
||||
for data in doc.xpath('//ul[@class="superlist booklist"]/li'):
|
||||
if counter <= 0:
|
||||
break
|
||||
|
||||
id = ''.join(data.xpath('.//a[@class="booklink"]/@href'))
|
||||
if not id:
|
||||
continue
|
||||
|
||||
cover_url = ''.join(data.xpath('.//a[@class="booklink"]/img/@src'))
|
||||
title = ''.join(data.xpath('.//a[@class="booklink"]/i/text()'))
|
||||
author = ''.join(data.xpath('.//span[@class="bookauthor"]/a/text()'))
|
||||
fb2 = ''.join(data.xpath('.//a[@class="dl dl-fb2"]/@href'))
|
||||
epub = ''.join(data.xpath('.//a[@class="dl dl-epub"]/@href'))
|
||||
txt = ''.join(data.xpath('.//a[@class="dl dl-txt"]/@href'))
|
||||
|
||||
# remove .zip extensions
|
||||
if fb2.find('.zip') != -1:
|
||||
fb2 = fb2[:fb2.find('.zip')]
|
||||
if epub.find('.zip') != -1:
|
||||
epub = epub[:epub.find('.zip')]
|
||||
if txt.find('.zip') != -1:
|
||||
txt = txt[:txt.find('.zip')]
|
||||
|
||||
counter -= 1
|
||||
|
||||
s = SearchResult()
|
||||
s.cover_url = cover_url
|
||||
s.title = title.strip()
|
||||
s.author = author.strip()
|
||||
s.detail_item = id.strip()
|
||||
s.drm = SearchResult.DRM_UNLOCKED
|
||||
s.downloads['FB2'] = base_url + fb2.strip()
|
||||
s.downloads['EPUB'] = base_url + epub.strip()
|
||||
s.downloads['TXT'] = base_url + txt.strip()
|
||||
s.formats = 'FB2, EPUB, TXT, SFB'
|
||||
yield s
|
||||
|
||||
# search for author names
|
||||
for data in doc.xpath('//ul[@class="superlist"][1]/li'):
|
||||
author_url = ''.join(data.xpath('.//a[contains(@href,"/person/")]/@href'))
|
||||
if counter <= 0:
|
||||
break
|
||||
|
||||
br2 = browser()
|
||||
with closing(br2.open(base_url + author_url, timeout=timeout)) as f:
|
||||
if counter <= 0:
|
||||
break
|
||||
f = unicode(f.read(), 'utf-8')
|
||||
doc2 = html.fromstring(f)
|
||||
|
||||
# search for book title
|
||||
for data in doc2.xpath('//ul[@class="superlist booklist"]/li'):
|
||||
if counter <= 0:
|
||||
break
|
||||
|
||||
id = ''.join(data.xpath('.//a[@class="booklink"]/@href'))
|
||||
if not id:
|
||||
continue
|
||||
|
||||
cover_url = ''.join(data.xpath('.//a[@class="booklink"]/img/@src'))
|
||||
title = ''.join(data.xpath('.//a[@class="booklink"]/i/text()'))
|
||||
author = ''.join(data.xpath('.//span[@class="bookauthor"]/a/text()'))
|
||||
fb2 = ''.join(data.xpath('.//a[@class="dl dl-fb2"]/@href'))
|
||||
epub = ''.join(data.xpath('.//a[@class="dl dl-epub"]/@href'))
|
||||
txt = ''.join(data.xpath('.//a[@class="dl dl-txt"]/@href'))
|
||||
|
||||
# remove .zip extensions
|
||||
if fb2.find('.zip') != -1:
|
||||
fb2 = fb2[:fb2.find('.zip')]
|
||||
if epub.find('.zip') != -1:
|
||||
epub = epub[:epub.find('.zip')]
|
||||
if txt.find('.zip') != -1:
|
||||
txt = txt[:txt.find('.zip')]
|
||||
|
||||
counter -= 1
|
||||
|
||||
s = SearchResult()
|
||||
s.cover_url = cover_url
|
||||
s.title = title.strip()
|
||||
s.author = author.strip()
|
||||
s.detail_item = id.strip()
|
||||
s.drm = SearchResult.DRM_UNLOCKED
|
||||
s.downloads['FB2'] = base_url + fb2.strip()
|
||||
s.downloads['EPUB'] = base_url + epub.strip()
|
||||
s.downloads['TXT'] = base_url + txt.strip()
|
||||
s.formats = 'FB2, EPUB, TXT, SFB'
|
||||
yield s
|
@ -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)
|
||||
|
@ -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:
|
||||
|
@ -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]))
|
||||
|
@ -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.
|
||||
|
@ -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,
|
||||
|
@ -153,12 +153,22 @@ def build_index(books, num, search, sort, order, start, total, url_base, CKEYS,
|
||||
bookt.append(TR(thumbnail, data))
|
||||
# }}}
|
||||
|
||||
body.append(HR())
|
||||
body.append(DIV(
|
||||
A(_('Switch to the full interface (non-mobile interface)'),
|
||||
href="/browse",
|
||||
style="text-decoration: none; color: blue",
|
||||
title=_('The full interface gives you many more features, '
|
||||
'but it may not work well on a small screen')),
|
||||
style="text-align:center"))
|
||||
return HTML(
|
||||
HEAD(
|
||||
TITLE(__appname__ + ' Library'),
|
||||
LINK(rel='icon', href='http://calibre-ebook.com/favicon.ico',
|
||||
type='image/x-icon'),
|
||||
LINK(rel='stylesheet', type='text/css', href=prefix+'/mobile/style.css')
|
||||
LINK(rel='stylesheet', type='text/css',
|
||||
href=prefix+'/mobile/style.css'),
|
||||
LINK(rel='apple-touch-icon', href="/static/calibre.png")
|
||||
), # End head
|
||||
body
|
||||
) # End html
|
||||
|
@ -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
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
|
@ -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.
|
||||
|
||||
Finally, the reason there are numbers at the end of every title folder, 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. It is also 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.
|
||||
|
||||
If you are still not convinced, then I'm afraid |app| is not for you. Look elsewhere for your book cataloguing needs. Just so we're clear, **this is not going to change**. Kindly do not contact us in an attempt to get us to change this.
|
||||
|
||||
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`.
|
||||
|
@ -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:
|
||||
|
||||
|
@ -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
@ -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
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user