merge with John's branch

This commit is contained in:
Tomasz Długosz 2012-04-14 23:49:02 +02:00
commit d142db2799
127 changed files with 44683 additions and 37935 deletions

View File

@ -19,6 +19,61 @@
# new recipes: # new recipes:
# - title: # - title:
- version: 0.8.45
date: 2012-03-30
new features:
- title: "E-book viewer: Allow the up and down keys to scroll past section boundaries"
- title: "calibredb: Allow specification of basic metadata on the command line when adding books."
tickets: [951063]
- title: "Driver for Samsung Galaxy Plus GT-I9001"
- title: "KF8 Input: Support KF8 format Amazon book samples."
tickets: [963418]
- title: "When a new plugin is added to calibre for the first time, have its icon (if any) show up even when a device is connected (this can be changed by the user at the time of plugin installation)"
- title: "Add keyboard shortcuts for Bold, Italic and Underline to the comments editor in the edit metadata dialog"
tickets: [963559]
bug fixes:
- title: "E-book viewer: Fix last read position (and bookmarks in general) being inaccurate for some books."
description: "The technique for marking locations in books used by the viewer has changed. The new technique should be much more accurate than the last one, especially when the font size at which the book is being viewed is changed. Note that this change means that bookmarks created with this release of calibre will not be read by previous calibre versions. On a technical note, the viewer now uses the CFI specification from the EPUB 3 standard for bookmarks."
type: major
- title: "Workarounds for a few regressions in the user interface in 0.8.44 caused by the update to Qt 4.8.0"
- title: "Books list: Preserve the horizontal scroll position when sorting by a column"
- title: "Fix saving to disk and then adding the book back not restoring tags-like custom columns"
- title: "Linux installer: Fix completion for ebook-convert not working."
tickets: [967834]
- title: "MOBI Output: Recognize type=text in addition to type=start guide elements"
- title: "Get Books: Updates to Nexto, Ebookpoint and Woblink stores"
- title: "Fix unable to clear username/password in Fetch news dialog"
- title: "PDF Output: Fix margin specifications not being applied"
- title: "Linux installer: Manually preserve the defaults.list mimetype association file to workaround buggy xdg-desktop-menu implementations in some distros."
tickets: [926559]
- title: "E-book viewer: Fix regression that caused the ebook viewer to stop functioning if it is launched from the main calibre program and then the main calibre program is closed."
tickets: [963960]
improved recipes:
- Our Daily Bread
new recipes:
- title: NRC Handelsblad (free)
author: veezh
- version: 0.8.44 - version: 0.8.44
date: 2012-03-23 date: 2012-03-23

View File

@ -13,7 +13,7 @@ class HighCountryNews(BasicNewsRecipe):
__author__ = 'Armin Geller' # 2012-01-31 __author__ = 'Armin Geller' # 2012-01-31
publisher = 'High Country News' publisher = 'High Country News'
timefmt = ' [%a, %d %b %Y]' timefmt = ' [%a, %d %b %Y]'
language = 'en-Us' language = 'en'
encoding = 'UTF-8' encoding = 'UTF-8'
publication_type = 'newspaper' publication_type = 'newspaper'
oldest_article = 7 oldest_article = 7

View File

@ -1,8 +1,9 @@
__license__ = 'GPL v3' __license__ = 'GPL v3'
__copyright__ = '2011' __copyright__ = '2012'
''' '''
lemonde.fr lemonde.fr
''' '''
import re
from calibre.web.feeds.recipes import BasicNewsRecipe from calibre.web.feeds.recipes import BasicNewsRecipe
class LeMonde(BasicNewsRecipe): class LeMonde(BasicNewsRecipe):
@ -24,7 +25,7 @@ class LeMonde(BasicNewsRecipe):
.ariane{font-size:xx-small;} .ariane{font-size:xx-small;}
.source{font-size:xx-small;} .source{font-size:xx-small;}
#.href{font-size:xx-small;} #.href{font-size:xx-small;}
.LM_caption{color:#666666; font-size:x-small;} #.figcaption style{color:#666666; font-size:x-small;}
#.main-article-info{font-family:Arial,Helvetica,sans-serif;} #.main-article-info{font-family:Arial,Helvetica,sans-serif;}
#full-contents{font-size:small; font-family:Arial,Helvetica,sans-serif;font-weight:normal;} #full-contents{font-size:small; font-family:Arial,Helvetica,sans-serif;font-weight:normal;}
#match-stats-summary{font-size:small; font-family:Arial,Helvetica,sans-serif;font-weight:normal;} #match-stats-summary{font-size:small; font-family:Arial,Helvetica,sans-serif;font-weight:normal;}
@ -40,8 +41,88 @@ class LeMonde(BasicNewsRecipe):
remove_empty_feeds = True remove_empty_feeds = True
auto_cleanup = True filterDuplicates = True
def preprocess_html(self, soup):
for alink in soup.findAll('a'):
if alink.string is not None:
tstr = alink.string
alink.replaceWith(tstr)
return soup
preprocess_regexps = [
(re.compile(r'([0-9])%'), lambda m: m.group(1) + ' %'),
(re.compile(r'([0-9])([0-9])([0-9]) ([0-9])([0-9])([0-9])'), lambda m: m.group(1) + m.group(2) + m.group(3) + ' ' + m.group(4) + m.group(5) + m.group(6)),
(re.compile(r'([0-9]) ([0-9])([0-9])([0-9])'), lambda m: m.group(1) + ' ' + m.group(2) + m.group(3) + m.group(4)),
(re.compile(r'<span>'), lambda match: ' <span>'),
(re.compile(r'\("'), lambda match: '(&laquo;&nbsp;'),
(re.compile(r'"\)'), lambda match: '&nbsp;&raquo;)'),
(re.compile(r'&ldquo;'), lambda match: '(&laquo;&nbsp;'),
(re.compile(r'&rdquo;'), lambda match: '&nbsp;&raquo;)'),
(re.compile(r'>\''), lambda match: '>&lsquo;'),
(re.compile(r' \''), lambda match: ' &lsquo;'),
(re.compile(r' &quot;'), lambda match: ' &laquo;&nbsp;'),
(re.compile(r'>&quot;'), lambda match: '>&laquo;&nbsp;'),
(re.compile(r'&quot;<'), lambda match: '&nbsp;&raquo;<'),
(re.compile(r'&quot; '), lambda match: '&nbsp;&raquo; '),
(re.compile(r'&quot;,'), lambda match: '&nbsp;&raquo;,'),
(re.compile(r'\''), lambda match: '&rsquo;'),
(re.compile(r'"<em>'), lambda match: '<em>&laquo;&nbsp;'),
(re.compile(r'"<em>"</em><em>'), lambda match: '<em>&laquo;&nbsp;'),
(re.compile(r'"<a href='), lambda match: '&laquo;&nbsp;<a href='),
(re.compile(r'</em>"'), lambda match: '&nbsp;&raquo;</em>'),
(re.compile(r'</a>"'), lambda match: '&nbsp;&raquo;</a>'),
(re.compile(r'"</'), lambda match: '&nbsp;&raquo;</'),
(re.compile(r'>"'), lambda match: '>&laquo;&nbsp;'),
(re.compile(r'"<'), lambda match: '&nbsp;&raquo;<'),
(re.compile(r'&rsquo;"'), lambda match: '&rsquo;«&nbsp;'),
(re.compile(r' "'), lambda match: ' &laquo;&nbsp;'),
(re.compile(r'" '), lambda match: '&nbsp;&raquo; '),
(re.compile(r'"\.'), lambda match: '&nbsp;&raquo;.'),
(re.compile(r'",'), lambda match: '&nbsp;&raquo;,'),
(re.compile(r'"\?'), lambda match: '&nbsp;&raquo;?'),
(re.compile(r'":'), lambda match: '&nbsp;&raquo;:'),
(re.compile(r'";'), lambda match: '&nbsp;&raquo;;'),
(re.compile(r'"\!'), lambda match: '&nbsp;&raquo;!'),
(re.compile(r' :'), lambda match: '&nbsp;:'),
(re.compile(r' ;'), lambda match: '&nbsp;;'),
(re.compile(r' \?'), lambda match: '&nbsp;?'),
(re.compile(r' \!'), lambda match: '&nbsp;!'),
(re.compile(r'\s»'), lambda match: '&nbsp;»'),
(re.compile(r'«\s'), lambda match: '«&nbsp;'),
(re.compile(r' %'), lambda match: '&nbsp;%'),
(re.compile(r'\.jpg&nbsp;&raquo; width='), lambda match: '.jpg'),
(re.compile(r'\.png&nbsp;&raquo; width='), lambda match: '.png'),
(re.compile(r' &ndash; '), lambda match: '&nbsp;&ndash; '),
(re.compile(r'figcaption style="display:none"'), lambda match: 'figcaption'),
(re.compile(r' '), lambda match: '&nbsp;&ndash; '),
(re.compile(r' - '), lambda match: '&nbsp;&ndash; '),
(re.compile(r' -,'), lambda match: '&nbsp;&ndash;,'),
(re.compile(r'&raquo;:'), lambda match: '&raquo;&nbsp;:'),
]
keep_only_tags = [
dict(name='div', attrs={'class':['global']})
]
remove_tags = [
dict(name='div', attrs={'class':['bloc_base meme_sujet']}),
dict(name='p', attrs={'class':['lire']})
]
remove_tags_after = [dict(id='fb-like')]
def get_article_url(self, article):
url = article.get('guid', None)
if '/chat/' in url or '.blog' in url or '/video/' in url or '/sport/' in url or '/portfolio/' in url or '/visuel/' in url :
url = None
return url
# def get_article_url(self, article):
# link = article.get('link')
# if 'blog' not in link and ('chat' not in link):
# return link
feeds = [ feeds = [
('A la une', 'http://www.lemonde.fr/rss/une.xml'), ('A la une', 'http://www.lemonde.fr/rss/une.xml'),
@ -66,11 +147,3 @@ class LeMonde(BasicNewsRecipe):
cover_url = link_item.img['src'] cover_url = link_item.img['src']
return cover_url return cover_url
def get_article_url(self, article):
url = article.get('guid', None)
if '/chat/' in url or '.blog' in url or '/video/' in url or '/sport/' in url or '/portfolio/' in url or '/visuel/' in url :
url = None
return url

View File

@ -0,0 +1,76 @@
__license__ = 'GPL v3'
__copyright__ = '2012'
'''
nrc.nl
'''
from calibre.web.feeds.recipes import BasicNewsRecipe
class NRC(BasicNewsRecipe):
title = 'NRC Handelsblad'
__author__ = 'veezh'
description = 'Nieuws (no subscription needed)'
oldest_article = 1
max_articles_per_feed = 100
no_stylesheets = True
#delay = 1
use_embedded_content = False
encoding = 'utf-8'
publisher = 'nrc.nl'
category = 'news, Netherlands, world'
language = 'nl'
timefmt = ''
#publication_type = 'newsportal'
extra_css = '''
h1{font-size:130%;}
#h2{font-size:100%;font-weight:normal;}
#.href{font-size:xx-small;}
.bijschrift{color:#666666; font-size:x-small;}
#.main-article-info{font-family:Arial,Helvetica,sans-serif;}
#full-contents{font-size:small; font-family:Arial,Helvetica,sans-serif;font-weight:normal;}
#match-stats-summary{font-size:small; font-family:Arial,Helvetica,sans-serif;font-weight:normal;}
'''
#preprocess_regexps = [(re.compile(r'<!--.*?-->', re.DOTALL), lambda m: '')]
conversion_options = {
'comments' : description
,'tags' : category
,'language' : language
,'publisher' : publisher
,'linearize_tables': True
}
remove_empty_feeds = True
filterDuplicates = True
def preprocess_html(self, soup):
for alink in soup.findAll('a'):
if alink.string is not None:
tstr = alink.string
alink.replaceWith(tstr)
return soup
keep_only_tags = [dict(name='div', attrs={'class':'article'})]
remove_tags_after = [dict(id='broodtekst')]
# keep_only_tags = [
# dict(name='div', attrs={'class':['label']})
# ]
# remove_tags_after = [dict(name='dl', attrs={'class':['tags']})]
# def get_article_url(self, article):
# link = article.get('link')
# if 'blog' not in link and ('chat' not in link):
# return link
feeds = [
# ('Nieuws', 'http://www.nrc.nl/rss.php'),
('Binnenland', 'http://www.nrc.nl/nieuws/categorie/binnenland/rss.php'),
('Buitenland', 'http://www.nrc.nl/nieuws/categorie/buitenland/rss.php'),
('Economie', 'http://www.nrc.nl/nieuws/categorie/economie/rss.php'),
('Wetenschap', 'http://www.nrc.nl/nieuws/categorie/wetenschap/rss.php'),
('Cultuur', 'http://www.nrc.nl/nieuws/categorie/cultuur/rss.php'),
('Boeken', 'http://www.nrc.nl/boeken/rss.php'),
('Tech', 'http://www.nrc.nl/tech/rss.php/'),
('Klimaat', 'http://www.nrc.nl/klimaat/rss.php/'),
]

View File

@ -14,6 +14,7 @@ class OurDailyBread(BasicNewsRecipe):
language = 'en' language = 'en'
max_articles_per_feed = 100 max_articles_per_feed = 100
no_stylesheets = True no_stylesheets = True
auto_cleanup = True
use_embedded_content = False use_embedded_content = False
category = 'ODB, Daily Devotional, Bible, Christian Devotional, Devotional, RBC Ministries, Our Daily Bread, Devotionals, Daily Devotionals, Christian Devotionals, Faith, Bible Study, Bible Studies, Scripture, RBC, religion' category = 'ODB, Daily Devotional, Bible, Christian Devotional, Devotional, RBC Ministries, Our Daily Bread, Devotionals, Daily Devotionals, Christian Devotionals, Faith, Bible Study, Bible Studies, Scripture, RBC, religion'
encoding = 'utf-8' encoding = 'utf-8'
@ -25,12 +26,12 @@ class OurDailyBread(BasicNewsRecipe):
,'linearize_tables' : True ,'linearize_tables' : True
} }
keep_only_tags = [dict(attrs={'class':'module-content'})] #keep_only_tags = [dict(attrs={'class':'module-content'})]
remove_tags = [ #remove_tags = [
dict(attrs={'id':'article-zoom'}) #dict(attrs={'id':'article-zoom'})
,dict(attrs={'class':'listen-now-box'}) #,dict(attrs={'class':'listen-now-box'})
] #]
remove_tags_after = dict(attrs={'class':'readable-area'}) #remove_tags_after = dict(attrs={'class':'readable-area'})
extra_css = ''' extra_css = '''
.text{font-family:Arial,Helvetica,sans-serif;font-size:x-small;} .text{font-family:Arial,Helvetica,sans-serif;font-size:x-small;}

View File

@ -6,6 +6,7 @@ Rue89
__author__ = '2010-2012, Louis Gesbert <meta at antislash dot info>' __author__ = '2010-2012, Louis Gesbert <meta at antislash dot info>'
import re
from calibre.web.feeds.news import BasicNewsRecipe from calibre.web.feeds.news import BasicNewsRecipe
class Rue89(BasicNewsRecipe): class Rue89(BasicNewsRecipe):
@ -15,23 +16,24 @@ class Rue89(BasicNewsRecipe):
title = u'Rue89' title = u'Rue89'
language = 'fr' language = 'fr'
oldest_article = 7 oldest_article = 7
max_articles_per_feed = 12 max_articles_per_feed = 50
use_embedded_content = False use_embedded_content = False
# From http://www.rue89.com/les-flux-rss-de-rue89 # From http://www.rue89.com/les-flux-rss-de-rue89
feeds = [ feeds = [
(u'La Une', u'http://www.rue89.com/feed'), (u'La Une', u'http://www.rue89.com/feed'),
(u'Rue69', u'http://www.rue89.com/rue69/feed'), # Other feeds disabled, 'La Une' seems to include them all
(u'Eco', u'http://www.rue89.com/rue89-eco/feed'), # (u'Rue69', u'http://www.rue89.com/rue69/feed'),
(u'Planète', u'http://www.rue89.com/rue89-planete/feed'), # (u'Eco', u'http://www.rue89.com/rue89-eco/feed'),
(u'Sport', u'http://www.rue89.com/rue89-sport/feed'), # (u'Planète', u'http://www.rue89.com/rue89-planete/feed'),
(u'Culture', u'http://www.rue89.com/culture/feed'), # (u'Sport', u'http://www.rue89.com/rue89-sport/feed'),
(u'Hi-tech', u'http://www.rue89.com/hi-tech/feed'), # (u'Culture', u'http://www.rue89.com/culture/feed'),
(u'Media', u'http://www.rue89.com/medias/feed'), # (u'Hi-tech', u'http://www.rue89.com/hi-tech/feed'),
(u'Monde', u'http://www.rue89.com/monde/feed'), # (u'Media', u'http://www.rue89.com/medias/feed'),
(u'Politique', u'http://www.rue89.com/politique/feed'), # (u'Monde', u'http://www.rue89.com/monde/feed'),
(u'Societe', u'http://www.rue89.com/societe/feed'), # (u'Politique', u'http://www.rue89.com/politique/feed'),
# (u'Societe', u'http://www.rue89.com/societe/feed'),
] ]
# Follow redirection from feedsportal.com # Follow redirection from feedsportal.com
@ -41,19 +43,36 @@ class Rue89(BasicNewsRecipe):
def print_version(self, url): def print_version(self, url):
return url + '?imprimer=1' return url + '?imprimer=1'
no_stylesheets = True
conversion_options = { 'smarten_punctuation' : True } conversion_options = { 'smarten_punctuation' : True }
keep_only_tags = [ keep_only_tags = [
dict(name='div', attrs={'id':'article'}), dict(name='div', attrs={'id':'content'}),
] ]
remove_tags_after = [ remove_tags_after = [
dict(name='div', attrs={'id':'plus_loin'}), dict(name='div', attrs={'id':'plus_loin'}),
dict(name='div', attrs={'class':'stats'}),
] ]
remove_tags = [ remove_tags = [
dict(name='div', attrs={'id':'article_tools'}), dict(name='div', attrs={'id':'article_tools'}),
dict(name='div', attrs={'id':'plus_loin'}), dict(name='div', attrs={'id':'plus_loin'}),
dict(name='div', attrs={'class':'stats'}),
dict(name='div', attrs={'class':'tools'}),
] ]
extra_css = "#content { padding: 0 0; }"
# Without this, parsing of video articles returns strange results
preprocess_regexps = [
(re.compile(r'<script.*?</script>', re.IGNORECASE|re.DOTALL), ''),
]
def preprocess_html(self, soup):
# Remove whole article if it's a "zapnet" (video)
if soup.find('h1', {'class':'zapnet_title'}):
return None
# Reduce h2 titles to h3
for title in soup.findAll('h2'):
title.name = 'h3'
return soup

View File

@ -48,7 +48,7 @@ class Push(Command):
threads = [] threads = []
for host in ( for host in (
r'Owner@winxp:/cygdrive/c/Documents\ and\ Settings/Owner/calibre', r'Owner@winxp:/cygdrive/c/Documents\ and\ Settings/Owner/calibre',
'kovid@leopard_test:calibre', 'kovid@ox:calibre',
r'kovid@win7:/cygdrive/c/Users/kovid/calibre', r'kovid@win7:/cygdrive/c/Users/kovid/calibre',
): ):
rcmd = BASE_RSYNC + EXCLUDES + ['.', host] rcmd = BASE_RSYNC + EXCLUDES + ['.', host]

File diff suppressed because it is too large Load Diff

View File

@ -8,14 +8,14 @@ msgstr ""
"Report-Msgid-Bugs-To: Debian iso-codes team <pkg-isocodes-" "Report-Msgid-Bugs-To: Debian iso-codes team <pkg-isocodes-"
"devel@lists.alioth.debian.org>\n" "devel@lists.alioth.debian.org>\n"
"POT-Creation-Date: 2011-11-25 14:01+0000\n" "POT-Creation-Date: 2011-11-25 14:01+0000\n"
"PO-Revision-Date: 2012-03-21 15:46+0000\n" "PO-Revision-Date: 2012-03-25 12:19+0000\n"
"Last-Translator: Иван Старчевић <ivanstar61@gmail.com>\n" "Last-Translator: Radan Putnik <srastral@gmail.com>\n"
"Language-Team: Serbian <gnu@prevod.org>\n" "Language-Team: Serbian <gnu@prevod.org>\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"X-Launchpad-Export-Date: 2012-03-22 04:56+0000\n" "X-Launchpad-Export-Date: 2012-03-26 04:37+0000\n"
"X-Generator: Launchpad (build 14981)\n" "X-Generator: Launchpad (build 15008)\n"
"Language: sr\n" "Language: sr\n"
#. name for aaa #. name for aaa
@ -888,7 +888,7 @@ msgstr "Ака-Кеде"
#. name for aky #. name for aky
msgid "Aka-Kol" msgid "Aka-Kol"
msgstr "" msgstr "ака-кол"
#. name for akz #. name for akz
msgid "Alabama" msgid "Alabama"
@ -968,11 +968,11 @@ msgstr "Алтајски;Јужни"
#. name for alu #. name for alu
msgid "'Are'are" msgid "'Are'are"
msgstr "" msgstr "ареаре"
#. name for alw #. name for alw
msgid "Alaba-Kabeena" msgid "Alaba-Kabeena"
msgstr "" msgstr "алаба-кабеена"
#. name for alx #. name for alx
msgid "Amol" msgid "Amol"
@ -1004,7 +1004,7 @@ msgstr ""
#. name for amf #. name for amf
msgid "Hamer-Banna" msgid "Hamer-Banna"
msgstr "" msgstr "хаммер-банна"
#. name for amg #. name for amg
msgid "Amarag" msgid "Amarag"
@ -1104,7 +1104,7 @@ msgstr "Ансус"
#. name for ane #. name for ane
msgid "Xârâcùù" msgid "Xârâcùù"
msgstr "" msgstr "ксаракуу"
#. name for anf #. name for anf
msgid "Animere" msgid "Animere"
@ -1156,7 +1156,7 @@ msgstr "Јарава(Индија)"
#. name for anr #. name for anr
msgid "Andh" msgid "Andh"
msgstr "" msgstr "андх"
#. name for ans #. name for ans
msgid "Anserma" msgid "Anserma"
@ -1256,7 +1256,7 @@ msgstr "Таикат"
#. name for aot #. name for aot
msgid "A'tong" msgid "A'tong"
msgstr "" msgstr "атонг"
#. name for aox #. name for aox
msgid "Atorada" msgid "Atorada"
@ -1284,7 +1284,7 @@ msgstr ""
#. name for apf #. name for apf
msgid "Agta; Pahanan" msgid "Agta; Pahanan"
msgstr "" msgstr "агта (паханан)"
#. name for apg #. name for apg
msgid "Ampanang" msgid "Ampanang"
@ -1392,7 +1392,7 @@ msgstr "Атакапа"
#. name for aqr #. name for aqr
msgid "Arhâ" msgid "Arhâ"
msgstr "" msgstr "арга"
#. name for aqz #. name for aqz
msgid "Akuntsu" msgid "Akuntsu"
@ -1424,7 +1424,7 @@ msgstr "арагонски"
#. name for arh #. name for arh
msgid "Arhuaco" msgid "Arhuaco"
msgstr "" msgstr "архуако"
#. name for ari #. name for ari
msgid "Arikara" msgid "Arikara"
@ -1504,7 +1504,7 @@ msgstr ""
#. name for asd #. name for asd
msgid "Asas" msgid "Asas"
msgstr "" msgstr "асас"
#. name for ase #. name for ase
msgid "American Sign Language" msgid "American Sign Language"
@ -1532,7 +1532,7 @@ msgstr "Нсари"
#. name for ask #. name for ask
msgid "Ashkun" msgid "Ashkun"
msgstr "" msgstr "ашкун"
#. name for asl #. name for asl
msgid "Asilulu" msgid "Asilulu"
@ -1604,11 +1604,11 @@ msgstr "Заива"
#. name for atc #. name for atc
msgid "Atsahuaca" msgid "Atsahuaca"
msgstr "" msgstr "атсахуака"
#. name for atd #. name for atd
msgid "Manobo; Ata" msgid "Manobo; Ata"
msgstr "" msgstr "манобо (Ата)"
#. name for ate #. name for ate
msgid "Atemble" msgid "Atemble"
@ -1648,7 +1648,7 @@ msgstr "Атон"
#. name for atp #. name for atp
msgid "Atta; Pudtol" msgid "Atta; Pudtol"
msgstr "" msgstr "атта (Пудтол)"
#. name for atq #. name for atq
msgid "Aralle-Tabulahan" msgid "Aralle-Tabulahan"
@ -1660,7 +1660,7 @@ msgstr ""
#. name for ats #. name for ats
msgid "Gros Ventre" msgid "Gros Ventre"
msgstr "" msgstr "грос-вентре"
#. name for att #. name for att
msgid "Atta; Pamplona" msgid "Atta; Pamplona"
@ -1692,7 +1692,7 @@ msgstr "Арта"
#. name for aua #. name for aua
msgid "Asumboa" msgid "Asumboa"
msgstr "" msgstr "асумбоа"
#. name for aub #. name for aub
msgid "Alugu" msgid "Alugu"
@ -1712,7 +1712,7 @@ msgstr ""
#. name for aug #. name for aug
msgid "Aguna" msgid "Aguna"
msgstr "" msgstr "агуна"
#. name for auh #. name for auh
msgid "Aushi" msgid "Aushi"
@ -1752,7 +1752,7 @@ msgstr ""
#. name for auq #. name for auq
msgid "Anus" msgid "Anus"
msgstr "" msgstr "анус"
#. name for aur #. name for aur
msgid "Aruek" msgid "Aruek"
@ -1852,7 +1852,7 @@ msgstr "Авети"
#. name for awh #. name for awh
msgid "Awbono" msgid "Awbono"
msgstr "" msgstr "авбоно"
#. name for awi #. name for awi
msgid "Aekyom" msgid "Aekyom"
@ -1860,7 +1860,7 @@ msgstr ""
#. name for awk #. name for awk
msgid "Awabakal" msgid "Awabakal"
msgstr "" msgstr "авабакал"
#. name for awm #. name for awm
msgid "Arawum" msgid "Arawum"
@ -1884,7 +1884,7 @@ msgstr ""
#. name for awt #. name for awt
msgid "Araweté" msgid "Araweté"
msgstr "" msgstr "аравете"
#. name for awu #. name for awu
msgid "Awyu; Central" msgid "Awyu; Central"
@ -1912,7 +1912,7 @@ msgstr "Абипон"
#. name for axg #. name for axg
msgid "Arára; Mato Grosso" msgid "Arára; Mato Grosso"
msgstr "" msgstr "арара (Мату-Гросу)"
#. name for axk #. name for axk
msgid "Yaka (Central African Republic)" msgid "Yaka (Central African Republic)"
@ -1924,7 +1924,7 @@ msgstr ""
#. name for axx #. name for axx
msgid "Xaragure" msgid "Xaragure"
msgstr "" msgstr "ксарагуре"
#. name for aya #. name for aya
msgid "Awar" msgid "Awar"

View File

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

View File

@ -107,6 +107,7 @@ class ANDROID(USBMS):
0xc004 : [0x0226], 0xc004 : [0x0226],
0x8801 : [0x0226, 0x0227], 0x8801 : [0x0226, 0x0227],
0xe115 : [0x0216], # PocketBook A10 0xe115 : [0x0216], # PocketBook A10
0xe107 : [0x326], # PocketBook 622
}, },
# Acer # Acer
@ -187,7 +188,7 @@ class ANDROID(USBMS):
'UMS', '.K080', 'P990', 'LTE', 'MB853', 'GT-S5660_CARD', 'A107', 'UMS', '.K080', 'P990', 'LTE', 'MB853', 'GT-S5660_CARD', 'A107',
'GT-I9003_CARD', 'XT912', 'FILE-CD_GADGET', 'RK29_SDK', 'MB855', 'GT-I9003_CARD', 'XT912', 'FILE-CD_GADGET', 'RK29_SDK', 'MB855',
'XT910', 'BOOK_A10', 'USB_2.0_DRIVER', 'I9100T', 'P999DW', 'XT910', 'BOOK_A10', 'USB_2.0_DRIVER', 'I9100T', 'P999DW',
'KTABLET_PC', 'INGENIC'] 'KTABLET_PC', 'INGENIC', 'GT-I9001_CARD']
WINDOWS_CARD_A_MEM = ['ANDROID_PHONE', 'GT-I9000_CARD', 'SGH-I897', WINDOWS_CARD_A_MEM = ['ANDROID_PHONE', 'GT-I9000_CARD', 'SGH-I897',
'FILE-STOR_GADGET', 'SGH-T959', 'SAMSUNG_ANDROID', 'GT-P1000_CARD', 'FILE-STOR_GADGET', 'SGH-T959', 'SAMSUNG_ANDROID', 'GT-P1000_CARD',
'A70S', 'A101IT', '7', 'INCREDIBLE', 'A7EB', 'SGH-T849_CARD', 'A70S', 'A101IT', '7', 'INCREDIBLE', 'A7EB', 'SGH-T849_CARD',
@ -195,7 +196,7 @@ class ANDROID(USBMS):
'ANDROID_MID', 'P990_SD_CARD', '.K080', 'LTE_CARD', 'MB853', 'ANDROID_MID', 'P990_SD_CARD', '.K080', 'LTE_CARD', 'MB853',
'A1-07___C0541A4F', 'XT912', 'MB855', 'XT910', 'BOOK_A10_CARD', 'A1-07___C0541A4F', 'XT912', 'MB855', 'XT910', 'BOOK_A10_CARD',
'USB_2.0_DRIVER', 'I9100T', 'P999DW_SD_CARD', 'KTABLET_PC', 'USB_2.0_DRIVER', 'I9100T', 'P999DW_SD_CARD', 'KTABLET_PC',
'FILE-CD_GADGET'] 'FILE-CD_GADGET', 'GT-I9001_CARD']
OSX_MAIN_MEM = 'Android Device Main Memory' OSX_MAIN_MEM = 'Android Device Main Memory'

View File

@ -527,7 +527,13 @@ class HeuristicProcessor(object):
if re.findall('(<|>)', replacement_break): if re.findall('(<|>)', replacement_break):
if re.match('^<hr', replacement_break): if re.match('^<hr', replacement_break):
if replacement_break.find('width') != -1: if replacement_break.find('width') != -1:
try:
width = int(re.sub('.*?width(:|=)(?P<wnum>\d+).*', '\g<wnum>', replacement_break)) width = int(re.sub('.*?width(:|=)(?P<wnum>\d+).*', '\g<wnum>', replacement_break))
except:
scene_break = hr_open+'<hr style="height: 3px; background:#505050" /></div>'
self.log.warn('Invalid replacement scene break'
' expression, using default')
else:
replacement_break = re.sub('(?i)(width=\d+\%?|width:\s*\d+(\%|px|pt|em)?;?)', '', replacement_break) replacement_break = re.sub('(?i)(width=\d+\%?|width:\s*\d+(\%|px|pt|em)?;?)', '', replacement_break)
divpercent = (100 - width) / 2 divpercent = (100 - width) / 2
hr_open = re.sub('45', str(divpercent), hr_open) hr_open = re.sub('45', str(divpercent), hr_open)

View File

@ -108,6 +108,8 @@ def decode_is_multiple(fm):
else: else:
im = {'cache_to_list': '|', 'ui_to_list': ',', im = {'cache_to_list': '|', 'ui_to_list': ',',
'list_to_ui': ', '} 'list_to_ui': ', '}
elif im is None:
im = {}
fm['is_multiple'] = im fm['is_multiple'] = im
class JsonCodec(object): class JsonCodec(object):

View File

@ -205,7 +205,10 @@ class EXTHHeader(object):
@property @property
def kf8_header_index(self): def kf8_header_index(self):
return self.get(121, None) ans = self.get(121, None)
if ans == NULL_INDEX:
ans = None
return ans
def __str__(self): def __str__(self):
ans = ['*'*20 + ' EXTH Header '+ '*'*20] ans = ['*'*20 + ' EXTH Header '+ '*'*20]
@ -467,8 +470,14 @@ class MOBIFile(object):
if mh.file_version >= 8: if mh.file_version >= 8:
self.kf8_type = 'standalone' self.kf8_type = 'standalone'
elif mh.has_exth and mh.exth.kf8_header_index is not None: elif mh.has_exth and mh.exth.kf8_header_index is not None:
self.kf8_type = 'joint'
kf8i = mh.exth.kf8_header_index kf8i = mh.exth.kf8_header_index
try:
rec = self.records[kf8i-1]
except IndexError:
pass
else:
if rec.raw == b'BOUNDARY':
self.kf8_type = 'joint'
mh8 = MOBIHeader(self.records[kf8i], kf8i) mh8 = MOBIHeader(self.records[kf8i], kf8i)
self.mobi8_header = mh8 self.mobi8_header = mh8

View File

@ -7,9 +7,10 @@ __license__ = 'GPL v3'
__copyright__ = '2012, Kovid Goyal <kovid@kovidgoyal.net>' __copyright__ = '2012, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
import sys, os import sys, os, imghdr
from calibre.ebooks.mobi.debug.headers import TextRecord from calibre.ebooks.mobi.debug.headers import TextRecord
from calibre.ebooks.mobi.utils import read_font_record
class MOBIFile(object): class MOBIFile(object):
@ -30,6 +31,7 @@ class MOBIFile(object):
first_text_record+offset+h8.number_of_text_records])] first_text_record+offset+h8.number_of_text_records])]
self.raw_text = b''.join(r.raw for r in self.text_records) self.raw_text = b''.join(r.raw for r in self.text_records)
self.extract_resources()
def print_header(self, f=sys.stdout): def print_header(self, f=sys.stdout):
print (str(self.mf.palmdb).encode('utf-8'), file=f) print (str(self.mf.palmdb).encode('utf-8'), file=f)
@ -41,6 +43,42 @@ class MOBIFile(object):
print (file=f) print (file=f)
print (str(self.mf.mobi8_header).encode('utf-8'), file=f) print (str(self.mf.mobi8_header).encode('utf-8'), file=f)
def extract_resources(self):
self.resource_map = []
known_types = {b'FLIS', b'FCIS', b'SRCS',
b'\xe9\x8e\r\n', b'RESC', b'BOUN', b'FDST', b'DATP',
b'AUDI', b'VIDE'}
for i, rec in enumerate(self.resource_records):
sig = rec.raw[:4]
payload = rec.raw
ext = 'dat'
prefix = 'binary'
suffix = ''
if sig in {b'HUFF', b'CDIC', b'INDX'}: continue
# TODO: Ignore CNCX records as well
if sig == b'FONT':
font = read_font_record(rec.raw)
if font['err']:
raise ValueError('Failed to read font record: %s Headers: %s'%(
font['err'], font['headers']))
payload = (font['font_data'] if font['font_data'] else
font['raw_data'])
prefix, ext = 'fonts', font['ext']
elif sig not in known_types:
q = imghdr.what(None, rec.raw)
if q:
prefix, ext = 'images', q
if prefix == 'binary':
if sig == b'\xe9\x8e\r\n':
suffix = '-EOF'
elif sig in known_types:
suffix = '-' + sig.decode('ascii')
self.resource_map.append(('%s/%06d%s.%s'%(prefix, i, suffix, ext),
payload))
def inspect_mobi(mobi_file, ddir): def inspect_mobi(mobi_file, ddir):
f = MOBIFile(mobi_file) f = MOBIFile(mobi_file)
@ -51,12 +89,14 @@ def inspect_mobi(mobi_file, ddir):
with open(alltext, 'wb') as of: with open(alltext, 'wb') as of:
of.write(f.raw_text) of.write(f.raw_text)
for tdir, attr in [('text_records', 'text_records'), ('images', for x in ('text_records', 'images', 'fonts', 'binary'):
'image_records'), ('binary', 'binary_records'), ('font', os.mkdir(os.path.join(ddir, x))
'font_records')]:
tdir = os.path.join(ddir, tdir) for rec in f.text_records:
os.mkdir(tdir) rec.dump(os.path.join(ddir, 'text_records'))
for rec in getattr(f, attr, []):
rec.dump(tdir) for href, payload in f.resource_map:
with open(os.path.join(ddir, href), 'wb') as f:
f.write(payload)

View File

@ -11,7 +11,7 @@ import struct, re, os
from calibre import replace_entities from calibre import replace_entities
from calibre.utils.date import parse_date from calibre.utils.date import parse_date
from calibre.ebooks.mobi import MobiError from calibre.ebooks.mobi import MobiError
from calibre.ebooks.metadata import MetaInformation from calibre.ebooks.metadata import MetaInformation, check_isbn
from calibre.ebooks.mobi.langcodes import main_language, sub_language, mobi2iana from calibre.ebooks.mobi.langcodes import main_language, sub_language, mobi2iana
NULL_INDEX = 0xffffffff NULL_INDEX = 0xffffffff
@ -75,10 +75,14 @@ class EXTHHeader(object): # {{{
self.mi.author_sort = au.strip() self.mi.author_sort = au.strip()
elif idx == 101: elif idx == 101:
self.mi.publisher = content.decode(codec, 'ignore').strip() self.mi.publisher = content.decode(codec, 'ignore').strip()
if self.mi.publisher in {'Unknown', _('Unknown')}:
self.mi.publisher = None
elif idx == 103: elif idx == 103:
self.mi.comments = content.decode(codec, 'ignore') self.mi.comments = content.decode(codec, 'ignore')
elif idx == 104: elif idx == 104:
self.mi.isbn = content.decode(codec, 'ignore').strip().replace('-', '') raw = check_isbn(content.decode(codec, 'ignore').strip().replace('-', ''))
if raw:
self.mi.isbn = raw
elif idx == 105: elif idx == 105:
if not self.mi.tags: if not self.mi.tags:
self.mi.tags = [] self.mi.tags = []
@ -92,12 +96,24 @@ class EXTHHeader(object): # {{{
pass pass
elif idx == 108: elif idx == 108:
self.mi.book_producer = content.decode(codec, 'ignore').strip() self.mi.book_producer = content.decode(codec, 'ignore').strip()
elif idx == 112: # dc:source set in some EBSP amazon samples
try:
content = content.decode(codec).strip()
isig = 'urn:isbn:'
if content.lower().startswith(isig):
raw = check_isbn(content[len(isig):])
if raw and not self.mi.isbn:
self.mi.isbn = raw
except:
pass
elif idx == 113: elif idx == 113:
pass # ASIN or UUID pass # ASIN or UUID
elif idx == 116: elif idx == 116:
self.start_offset, = struct.unpack(b'>L', content) self.start_offset, = struct.unpack(b'>L', content)
elif idx == 121: elif idx == 121:
self.kf8_header, = struct.unpack(b'>L', content) self.kf8_header, = struct.unpack(b'>L', content)
if self.kf8_header == NULL_INDEX:
self.kf8_header = None
#else: #else:
# print 'unhandled metadata record', idx, repr(content) # print 'unhandled metadata record', idx, repr(content)
# }}} # }}}

View File

@ -39,10 +39,41 @@ def parse_indx_header(data):
words = ( words = (
'len', 'nul1', 'type', 'gen', 'start', 'count', 'code', 'len', 'nul1', 'type', 'gen', 'start', 'count', 'code',
'lng', 'total', 'ordt', 'ligt', 'nligt', 'ncncx' 'lng', 'total', 'ordt', 'ligt', 'nligt', 'ncncx'
) ) + tuple('unknown%d'%i for i in xrange(27)) + ('ocnt', 'oentries',
'ordt1', 'ordt2', 'tagx')
num = len(words) num = len(words)
values = struct.unpack(bytes('>%dL' % num), data[4:4*(num+1)]) values = struct.unpack(bytes('>%dL' % num), data[4:4*(num+1)])
return dict(zip(words, values)) ans = dict(zip(words, values))
ordt1, ordt2 = ans['ordt1'], ans['ordt2']
ans['ordt1_raw'], ans['ordt2_raw'] = [], []
ans['ordt_map'] = ''
if ordt1 > 0 and data[ordt1:ordt1+4] == b'ORDT':
# I dont know what this is, but using it seems to be unnecessary, so
# just leave it as the raw bytestring
ans['ordt1_raw'] = data[ordt1+4:ordt1+4+ans['oentries']]
if ordt2 > 0 and data[ordt2:ordt2+4] == b'ORDT':
ans['ordt2_raw'] = raw = bytearray(data[ordt2+4:ordt2+4+2*ans['oentries']])
if ans['code'] == 65002:
# This appears to be EBCDIC-UTF (65002) encoded. I can't be
# bothered to write a decoder for this (see
# http://www.unicode.org/reports/tr16/) Just how stupid is Amazon?
# Instead, we use a weird hack that seems to do the trick for all
# the books with this type of ORDT record that I have come across.
# Some EBSP book samples in KF8 format from Amazon have this type
# of encoding.
# Basically we try to interpret every second byte as a printable
# ascii character. If we cannot, we map to the ? char.
parsed = bytearray(ans['oentries'])
for i in xrange(0, 2*ans['oentries'], 2):
parsed[i//2] = raw[i+1] if 0x20 < raw[i+1] < 0x7f else ord(b'?')
ans['ordt_map'] = bytes(parsed).decode('ascii')
else:
ans['ordt_map'] = '?'*ans['oentries']
return ans
class CNCX(object): # {{{ class CNCX(object): # {{{
@ -163,7 +194,7 @@ def get_tag_map(control_byte_count, tagx, data, strict=False):
return ans return ans
def parse_index_record(table, data, control_byte_count, tags, codec, def parse_index_record(table, data, control_byte_count, tags, codec,
strict=False): ordt_map, strict=False):
header = parse_indx_header(data) header = parse_indx_header(data)
idxt_pos = header['start'] idxt_pos = header['start']
if data[idxt_pos:idxt_pos+4] != b'IDXT': if data[idxt_pos:idxt_pos+4] != b'IDXT':
@ -184,12 +215,11 @@ def parse_index_record(table, data, control_byte_count, tags, codec,
for j in xrange(entry_count): for j in xrange(entry_count):
start, end = idx_positions[j:j+2] start, end = idx_positions[j:j+2]
rec = data[start:end] rec = data[start:end]
ident, consumed = decode_string(rec, codec=codec) ident, consumed = decode_string(rec, codec=codec, ordt_map=ordt_map)
rec = rec[consumed:] rec = rec[consumed:]
tag_map = get_tag_map(control_byte_count, tags, rec, strict=strict) tag_map = get_tag_map(control_byte_count, tags, rec, strict=strict)
table[ident] = tag_map table[ident] = tag_map
def read_index(sections, idx, codec): def read_index(sections, idx, codec):
table, cncx = OrderedDict(), CNCX([], codec) table, cncx = OrderedDict(), CNCX([], codec)
@ -203,12 +233,13 @@ def read_index(sections, idx, codec):
cncx_records = [x[0] for x in sections[off:off+indx_header['ncncx']]] cncx_records = [x[0] for x in sections[off:off+indx_header['ncncx']]]
cncx = CNCX(cncx_records, codec) cncx = CNCX(cncx_records, codec)
tag_section_start = indx_header['len'] tag_section_start = indx_header['tagx']
control_byte_count, tags = parse_tagx_section(data[tag_section_start:]) control_byte_count, tags = parse_tagx_section(data[tag_section_start:])
for i in xrange(idx + 1, idx + 1 + indx_count): for i in xrange(idx + 1, idx + 1 + indx_count):
# Index record # Index record
data = sections[i][0] data = sections[i][0]
parse_index_record(table, data, control_byte_count, tags, codec) parse_index_record(table, data, control_byte_count, tags, codec,
indx_header['ordt_map'])
return table, cncx return table, cncx

View File

@ -10,13 +10,19 @@ __docformat__ = 'restructuredtext en'
import struct, re, os, imghdr import struct, re, os, imghdr
from collections import namedtuple from collections import namedtuple
from itertools import repeat from itertools import repeat
from urlparse import urldefrag
from lxml import etree
from calibre.ebooks.mobi.reader.headers import NULL_INDEX from calibre.ebooks.mobi.reader.headers import NULL_INDEX
from calibre.ebooks.mobi.reader.index import read_index from calibre.ebooks.mobi.reader.index import read_index
from calibre.ebooks.mobi.reader.ncx import read_ncx, build_toc from calibre.ebooks.mobi.reader.ncx import read_ncx, build_toc
from calibre.ebooks.mobi.reader.markup import expand_mobi8_markup from calibre.ebooks.mobi.reader.markup import expand_mobi8_markup
from calibre.ebooks.metadata.opf2 import Guide, OPFCreator from calibre.ebooks.metadata.opf2 import Guide, OPFCreator
from calibre.ebooks.metadata.toc import TOC
from calibre.ebooks.mobi.utils import read_font_record from calibre.ebooks.mobi.utils import read_font_record
from calibre.ebooks.oeb.parse_utils import parse_html
from calibre.ebooks.oeb.base import XPath, XHTML, xml2text
Part = namedtuple('Part', Part = namedtuple('Part',
'num type filename start end aid') 'num type filename start end aid')
@ -285,7 +291,11 @@ class Mobi8Reader(object):
def create_guide(self): def create_guide(self):
guide = Guide() guide = Guide()
for ref_type, ref_title, fileno in self.guide: for ref_type, ref_title, fileno in self.guide:
try:
elem = self.elems[fileno] elem = self.elems[fileno]
except IndexError:
# Happens for thumbnailstandard in Amazon book samples
continue
fi = self.get_file_info(elem.insert_pos) fi = self.get_file_info(elem.insert_pos)
idtext = self.get_id_tag(elem.insert_pos).decode(self.header.codec) idtext = self.get_id_tag(elem.insert_pos).decode(self.header.codec)
linktgt = fi.filename linktgt = fi.filename
@ -379,6 +389,19 @@ class Mobi8Reader(object):
len(resource_map)): len(resource_map)):
mi.cover = resource_map[self.cover_offset] mi.cover = resource_map[self.cover_offset]
if len(list(toc)) < 2:
self.log.warn('KF8 has no metadata Table of Contents')
for ref in guide:
if ref.type == 'toc':
href = ref.href()
href, frag = urldefrag(href)
if os.path.exists(href.replace('/', os.sep)):
try:
toc = self.read_inline_toc(href, frag)
except:
self.log.exception('Failed to read inline ToC')
opf = OPFCreator(os.getcwdu(), mi) opf = OPFCreator(os.getcwdu(), mi)
opf.guide = guide opf.guide = guide
@ -393,4 +416,61 @@ class Mobi8Reader(object):
opf.render(of, ncx, 'toc.ncx') opf.render(of, ncx, 'toc.ncx')
return 'metadata.opf' return 'metadata.opf'
def read_inline_toc(self, href, frag):
ans = TOC()
base_href = '/'.join(href.split('/')[:-1])
with open(href.replace('/', os.sep), 'rb') as f:
raw = f.read().decode(self.header.codec)
root = parse_html(raw, log=self.log)
body = XPath('//h:body')(root)
reached = False
if body:
start = body[0]
else:
start = None
reached = True
if frag:
elems = XPath('//*[@id="%s"]'%frag)
if elems:
start = elems[0]
def node_depth(elem):
ans = 0
parent = elem.getparent()
while parent is not None:
parent = parent.getparent()
ans += 1
return ans
# Layer the ToC based on nesting order in the source HTML
current_depth = None
parent = ans
seen = set()
for elem in root.iterdescendants(etree.Element):
if reached and elem.tag == XHTML('a') and elem.get('href',
False):
href = elem.get('href')
href, frag = urldefrag(href)
href = base_href + '/' + href
text = xml2text(elem).strip()
if text in seen:
continue
seen.add(text)
depth = node_depth(elem)
if current_depth is None:
current_depth = depth
if current_depth == depth:
parent.add_item(href, frag, text)
elif current_depth < depth:
parent = parent[-1]
parent.add_item(href, frag, text)
current_depth = depth
else:
parent = parent.parent
parent.add_item(href, frag, text)
current_depth = depth
else:
if elem is start:
reached = True
return ans

View File

@ -15,10 +15,12 @@ from calibre.ebooks import normalize
IMAGE_MAX_SIZE = 10 * 1024 * 1024 IMAGE_MAX_SIZE = 10 * 1024 * 1024
def decode_string(raw, codec='utf-8'): def decode_string(raw, codec='utf-8', ordt_map=''):
length, = struct.unpack(b'>B', raw[0]) length, = struct.unpack(b'>B', raw[0])
raw = raw[1:1+length] raw = raw[1:1+length]
consumed = length+1 consumed = length+1
if ordt_map:
return ''.join(ordt_map[ord(x)] for x in raw), consumed
return raw.decode(codec), consumed return raw.decode(codec), consumed
def decode_hex_number(raw, codec='utf-8'): def decode_hex_number(raw, codec='utf-8'):

View File

@ -161,8 +161,8 @@ class Serializer(object):
self.serialize_text(ref.title, quot=True) self.serialize_text(ref.title, quot=True)
buf.write(b'" ') buf.write(b'" ')
if (ref.title.lower() == 'start' or if (ref.title.lower() == 'start' or
(ref.type and ref.type.lower() in ('start', (ref.type and ref.type.lower() in {'start',
'other.start'))): 'other.start', 'text'})):
self._start_href = ref.href self._start_href = ref.href
self.serialize_href(ref.href) self.serialize_href(ref.href)
# Space required or won't work, I kid you not # Space required or won't work, I kid you not

View File

@ -0,0 +1,11 @@
#!/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__ = '2012, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'

View File

@ -0,0 +1,59 @@
#!/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__ = '2012, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'
import re
from calibre import guess_type
class EntityDeclarationProcessor(object): # {{{
def __init__(self, html):
self.declared_entities = {}
for match in re.finditer(r'<!\s*ENTITY\s+([^>]+)>', html):
tokens = match.group(1).split()
if len(tokens) > 1:
self.declared_entities[tokens[0].strip()] = tokens[1].strip().replace('"', '')
self.processed_html = html
for key, val in self.declared_entities.iteritems():
self.processed_html = self.processed_html.replace('&%s;'%key, val)
# }}}
def self_closing_sub(match):
tag = match.group(1)
if tag.lower().strip() == 'br':
return match.group()
return '<%s %s></%s>'%(match.group(1), match.group(2), match.group(1))
def load_html(path, view, codec='utf-8', mime_type=None,
pre_load_callback=lambda x:None):
from PyQt4.Qt import QUrl, QByteArray
if mime_type is None:
mime_type = guess_type(path)[0]
with open(path, 'rb') as f:
html = f.read().decode(codec, 'replace')
html = EntityDeclarationProcessor(html).processed_html
has_svg = re.search(r'<[:a-zA-Z]*svg', html) is not None
if 'xhtml' in mime_type:
self_closing_pat = re.compile(r'<([a-z1-6]+)\s+([^>]+)/>',
re.IGNORECASE)
html = self_closing_pat.sub(self_closing_sub, html)
html = re.sub(ur'<\s*title\s*/\s*>', u'', html, flags=re.IGNORECASE)
loading_url = QUrl.fromLocalFile(path)
pre_load_callback(loading_url)
if has_svg:
view.setContent(QByteArray(html.encode(codec)), mime_type,
loading_url)
else:
view.setHtml(html, loading_url)

View File

@ -26,6 +26,8 @@ from calibre.constants import filesystem_encoding
TITLEPAGE = CoverManager.SVG_TEMPLATE.decode('utf-8').replace(\ TITLEPAGE = CoverManager.SVG_TEMPLATE.decode('utf-8').replace(\
'__ar__', 'none').replace('__viewbox__', '0 0 600 800' '__ar__', 'none').replace('__viewbox__', '0 0 600 800'
).replace('__width__', '600').replace('__height__', '800') ).replace('__width__', '600').replace('__height__', '800')
BM_FIELD_SEP = u'*|!|?|*'
BM_LEGACY_ESC = u'esc-text-%&*#%(){}ads19-end-esc'
def character_count(html): def character_count(html):
''' '''
@ -273,27 +275,62 @@ class EbookIterator(object):
def parse_bookmarks(self, raw): def parse_bookmarks(self, raw):
for line in raw.splitlines(): for line in raw.splitlines():
bm = None
if line.count('^') > 0: if line.count('^') > 0:
tokens = line.rpartition('^') tokens = line.rpartition('^')
title, ref = tokens[0], tokens[2] title, ref = tokens[0], tokens[2]
self.bookmarks.append((title, ref)) try:
spine, _, pos = ref.partition('#')
spine = int(spine.strip())
except:
continue
bm = {'type':'legacy', 'title':title, 'spine':spine, 'pos':pos}
elif BM_FIELD_SEP in line:
try:
title, spine, pos = line.strip().split(BM_FIELD_SEP)
spine = int(spine)
except:
continue
# Unescape from serialization
pos = pos.replace(BM_LEGACY_ESC, u'^')
# Check for pos being a scroll fraction
try:
pos = float(pos)
except:
pass
bm = {'type':'cfi', 'title':title, 'pos':pos, 'spine':spine}
if bm:
self.bookmarks.append(bm)
def serialize_bookmarks(self, bookmarks): def serialize_bookmarks(self, bookmarks):
dat = [] dat = []
for title, bm in bookmarks: for bm in bookmarks:
dat.append(u'%s^%s'%(title, bm)) if bm['type'] == 'legacy':
return (u'\n'.join(dat) +'\n').encode('utf-8') rec = u'%s^%d#%s'%(bm['title'], bm['spine'], bm['pos'])
else:
pos = bm['pos']
if isinstance(pos, (int, float)):
pos = unicode(pos)
else:
pos = pos.replace(u'^', BM_LEGACY_ESC)
rec = BM_FIELD_SEP.join([bm['title'], unicode(bm['spine']), pos])
dat.append(rec)
return (u'\n'.join(dat) +u'\n')
def read_bookmarks(self): def read_bookmarks(self):
self.bookmarks = [] self.bookmarks = []
bmfile = os.path.join(self.base, 'META-INF', 'calibre_bookmarks.txt') bmfile = os.path.join(self.base, 'META-INF', 'calibre_bookmarks.txt')
raw = '' raw = ''
if os.path.exists(bmfile): if os.path.exists(bmfile):
raw = open(bmfile, 'rb').read().decode('utf-8') with open(bmfile, 'rb') as f:
raw = f.read()
else: else:
saved = self.config['bookmarks_'+self.pathtoebook] saved = self.config['bookmarks_'+self.pathtoebook]
if saved: if saved:
raw = saved raw = saved
if not isinstance(raw, unicode):
raw = raw.decode('utf-8')
self.parse_bookmarks(raw) self.parse_bookmarks(raw)
def save_bookmarks(self, bookmarks=None): def save_bookmarks(self, bookmarks=None):
@ -306,18 +343,15 @@ class EbookIterator(object):
zf = open(self.pathtoebook, 'r+b') zf = open(self.pathtoebook, 'r+b')
except IOError: except IOError:
return return
safe_replace(zf, 'META-INF/calibre_bookmarks.txt', StringIO(dat), safe_replace(zf, 'META-INF/calibre_bookmarks.txt',
StringIO(dat.encode('utf-8')),
add_missing=True) add_missing=True)
else: else:
self.config['bookmarks_'+self.pathtoebook] = dat self.config['bookmarks_'+self.pathtoebook] = dat
def add_bookmark(self, bm): def add_bookmark(self, bm):
dups = [] self.bookmarks = [x for x in self.bookmarks if x['title'] !=
for x in self.bookmarks: bm['title']]
if x[0] == bm[0]:
dups.append(x)
for x in dups:
self.bookmarks.remove(x)
self.bookmarks.append(bm) self.bookmarks.append(bm)
self.save_bookmarks() self.save_bookmarks()

View File

@ -8,10 +8,9 @@ __docformat__ = 'restructuredtext en'
class Clean(object): class Clean(object):
'''Clean up guide, leaving only a pointer to the cover''' '''Clean up guide, leaving only known values '''
def __call__(self, oeb, opts): def __call__(self, oeb, opts):
from calibre.ebooks.oeb.base import urldefrag
self.oeb, self.log, self.opts = oeb, oeb.log, opts self.oeb, self.log, self.opts = oeb, oeb.log, opts
if 'cover' not in self.oeb.guide: if 'cover' not in self.oeb.guide:
@ -32,10 +31,15 @@ class Clean(object):
ref.type = 'cover' ref.type = 'cover'
self.oeb.guide.refs['cover'] = ref self.oeb.guide.refs['cover'] = ref
if ('start' in self.oeb.guide and 'text' not in self.oeb.guide):
# Prefer text to start as per the OPF 2.0 spec
x = self.oeb.guide['start']
self.oeb.guide.add('text', x.title, x.href)
self.oeb.guide.remove('start')
for x in list(self.oeb.guide): for x in list(self.oeb.guide):
href = urldefrag(self.oeb.guide[x].href)[0] if x.lower() not in {'cover', 'titlepage', 'masthead', 'toc',
if x.lower() not in ('cover', 'titlepage', 'masthead', 'toc', 'title-page', 'copyright-page', 'text'}:
'title-page', 'copyright-page', 'start'):
item = self.oeb.guide[x] item = self.oeb.guide[x]
if item.title and item.title.lower() == 'start': if item.title and item.title.lower() == 'start':
continue continue

View File

@ -18,10 +18,11 @@ from calibre.ebooks.pdf.pageoptions import unit, paper_size, \
from calibre.ebooks.metadata import authors_to_string from calibre.ebooks.metadata import authors_to_string
from calibre.ptempfile import PersistentTemporaryFile from calibre.ptempfile import PersistentTemporaryFile
from calibre import __appname__, __version__, fit_image from calibre import __appname__, __version__, fit_image
from calibre.ebooks.oeb.display.webview import load_html
from PyQt4 import QtCore from PyQt4 import QtCore
from PyQt4.Qt import QUrl, QEventLoop, QObject, \ from PyQt4.Qt import (QEventLoop, QObject,
QPrinter, QMetaObject, QSizeF, Qt, QPainter, QPixmap QPrinter, QMetaObject, QSizeF, Qt, QPainter, QPixmap)
from PyQt4.QtWebKit import QWebView from PyQt4.QtWebKit import QWebView
from pyPdf import PdfFileWriter, PdfFileReader from pyPdf import PdfFileWriter, PdfFileReader
@ -39,14 +40,21 @@ def get_custom_size(opts):
custom_size = None custom_size = None
return custom_size return custom_size
def get_pdf_printer(opts, for_comic=False): def get_pdf_printer(opts, for_comic=False, output_file_name=None):
from calibre.gui2 import is_ok_to_use_qt from calibre.gui2 import is_ok_to_use_qt
if not is_ok_to_use_qt(): if not is_ok_to_use_qt():
raise Exception('Not OK to use Qt') raise Exception('Not OK to use Qt')
printer = QPrinter(QPrinter.HighResolution) printer = QPrinter(QPrinter.HighResolution)
custom_size = get_custom_size(opts) custom_size = get_custom_size(opts)
if isosx and not for_comic:
# On OSX, the native engine can only produce a single page size
# (usually A4). The Qt engine on the other hand produces image based
# PDFs. If we set a custom page size using QSizeF the native engine
# produces unreadable output, so we just ignore the custom size
# settings.
printer.setPaperSize(paper_size(opts.paper_size))
else:
if opts.output_profile.short_name == 'default' or \ if opts.output_profile.short_name == 'default' or \
opts.output_profile.width > 9999: opts.output_profile.width > 9999:
if custom_size is None: if custom_size is None:
@ -70,7 +78,13 @@ def get_pdf_printer(opts, for_comic=False):
opts.margin_right, opts.margin_bottom, QPrinter.Point) opts.margin_right, opts.margin_bottom, QPrinter.Point)
printer.setOrientation(orientation(opts.orientation)) printer.setOrientation(orientation(opts.orientation))
printer.setOutputFormat(QPrinter.PdfFormat) printer.setOutputFormat(QPrinter.PdfFormat)
printer.setFullPage(True) printer.setFullPage(for_comic)
if output_file_name:
printer.setOutputFileName(output_file_name)
if isosx and not for_comic:
# Ensure we are not generating enormous image based PDFs
printer.setOutputFormat(QPrinter.NativeFormat)
return printer return printer
def get_printer_page_size(opts, for_comic=False): def get_printer_page_size(opts, for_comic=False):
@ -156,21 +170,17 @@ class PDFWriter(QObject): # {{{
self.combine_queue.append(os.path.join(self.tmp_path, '%i.pdf' % (len(self.combine_queue) + 1))) self.combine_queue.append(os.path.join(self.tmp_path, '%i.pdf' % (len(self.combine_queue) + 1)))
self.logger.debug('Processing %s...' % item) self.logger.debug('Processing %s...' % item)
load_html(item, self.view)
self.view.load(QUrl.fromLocalFile(item))
def _render_html(self, ok): def _render_html(self, ok):
if ok: if ok:
item_path = os.path.join(self.tmp_path, '%i.pdf' % len(self.combine_queue)) item_path = os.path.join(self.tmp_path, '%i.pdf' % len(self.combine_queue))
self.logger.debug('\tRendering item %s as %i.pdf' % (os.path.basename(str(self.view.url().toLocalFile())), len(self.combine_queue))) self.logger.debug('\tRendering item %s as %i.pdf' % (os.path.basename(str(self.view.url().toLocalFile())), len(self.combine_queue)))
printer = get_pdf_printer(self.opts) printer = get_pdf_printer(self.opts, output_file_name=item_path)
printer.setOutputFileName(item_path) self.view.page().mainFrame().evaluateJavaScript('''
# We have to set the engine to Native on OS X after the call to set document.body.style.backgroundColor = "white";
# filename. Setting a filename with .pdf as the extension causes
# Qt to set the format to use Qt's PDF engine even if native was ''')
# previously set on the printer.
if isosx:
printer.setOutputFormat(QPrinter.NativeFormat)
self.view.print_(printer) self.view.print_(printer)
printer.abort() printer.abort()
else: else:
@ -188,10 +198,7 @@ class PDFWriter(QObject): # {{{
if self.cover_data is None: if self.cover_data is None:
return return
item_path = os.path.join(self.tmp_path, 'cover.pdf') item_path = os.path.join(self.tmp_path, 'cover.pdf')
printer = get_pdf_printer(self.opts) printer = get_pdf_printer(self.opts, output_file_name=item_path)
printer.setOutputFileName(item_path)
if isosx:
printer.setOutputFormat(QPrinter.NativeFormat)
self.combine_queue.insert(0, item_path) self.combine_queue.insert(0, item_path)
p = QPixmap() p = QPixmap()
p.loadFromData(self.cover_data) p.loadFromData(self.cover_data)
@ -243,10 +250,8 @@ class ImagePDFWriter(object):
os.remove(f.name) os.remove(f.name)
def render_images(self, outpath, mi, items): def render_images(self, outpath, mi, items):
printer = get_pdf_printer(self.opts, for_comic=True) printer = get_pdf_printer(self.opts, for_comic=True,
printer.setOutputFileName(outpath) output_file_name=outpath)
if isosx:
printer.setOutputFormat(QPrinter.NativeFormat)
printer.setDocName(mi.title) printer.setDocName(mi.title)
printer.setCreator(u'%s [%s]'%(__appname__, __version__)) printer.setCreator(u'%s [%s]'%(__appname__, __version__))
# Seems to be no way to set author # Seems to be no way to set author

View File

@ -105,6 +105,7 @@ gprefs.defaults['show_files_after_save'] = True
gprefs.defaults['auto_add_path'] = None gprefs.defaults['auto_add_path'] = None
gprefs.defaults['auto_add_check_for_duplicates'] = False gprefs.defaults['auto_add_check_for_duplicates'] = False
gprefs.defaults['blocked_auto_formats'] = [] gprefs.defaults['blocked_auto_formats'] = []
gprefs.defaults['auto_add_auto_convert'] = True
# }}} # }}}
NONE = QVariant() #: Null value to return from the data function of item models NONE = QVariant() #: Null value to return from the data function of item models

View File

@ -71,7 +71,7 @@ class AddAction(InterfaceAction):
ma('add-formats', _('Add files to selected book records'), ma('add-formats', _('Add files to selected book records'),
triggered=self.add_formats, shortcut=_('Shift+A')) triggered=self.add_formats, shortcut=_('Shift+A'))
self.add_menu.addSeparator() self.add_menu.addSeparator()
ma('add-config', _('Configure the adding of books'), ma('add-config', _('Control the adding of books'),
triggered=self.add_config) triggered=self.add_config)
self.qaction.triggered.connect(self.add_books) self.qaction.triggered.connect(self.add_books)

View File

@ -53,6 +53,24 @@ class ConvertAction(InterfaceAction):
self.queue_convert_jobs(jobs, changed, bad, rows, previous, self.queue_convert_jobs(jobs, changed, bad, rows, previous,
self.book_auto_converted, extra_job_args=[on_card]) self.book_auto_converted, extra_job_args=[on_card])
def auto_convert_auto_add(self, book_ids):
previous = self.gui.library_view.currentIndex()
db = self.gui.current_db
needed = set()
of = prefs['output_format'].lower()
for book_id in book_ids:
fmts = db.formats(book_id, index_is_id=True)
fmts = set(x.lower() for x in fmts.split(',')) if fmts else set()
if of not in fmts:
needed.add(book_id)
if needed:
jobs, changed, bad = convert_single_ebook(self.gui,
self.gui.library_view.model().db, needed, True, of,
show_no_format_warning=False)
if not jobs: return
self.queue_convert_jobs(jobs, changed, bad, list(needed), previous,
self.book_converted, rows_are_ids=True)
def auto_convert_mail(self, to, fmts, delete_from_library, book_ids, format, subject): def auto_convert_mail(self, to, fmts, delete_from_library, book_ids, format, subject):
previous = self.gui.library_view.currentIndex() previous = self.gui.library_view.currentIndex()
rows = [x.row() for x in \ rows = [x.row() for x in \
@ -118,7 +136,7 @@ class ConvertAction(InterfaceAction):
num, 2000) num, 2000)
def queue_convert_jobs(self, jobs, changed, bad, rows, previous, def queue_convert_jobs(self, jobs, changed, bad, rows, previous,
converted_func, extra_job_args=[]): converted_func, extra_job_args=[], rows_are_ids=False):
for func, args, desc, fmt, id, temp_files in jobs: for func, args, desc, fmt, id, temp_files in jobs:
func, _, same_fmt = func.partition(':') func, _, same_fmt = func.partition(':')
same_fmt = same_fmt == 'same_fmt' same_fmt = same_fmt == 'same_fmt'
@ -140,7 +158,11 @@ class ConvertAction(InterfaceAction):
self.conversion_jobs[job] = tuple(args) self.conversion_jobs[job] = tuple(args)
if changed: if changed:
self.gui.library_view.model().refresh_rows(rows) m = self.gui.library_view.model()
if rows_are_ids:
m.refresh_ids(rows)
else:
m.refresh_rows(rows)
current = self.gui.library_view.currentIndex() current = self.gui.library_view.currentIndex()
self.gui.library_view.model().current_changed(current, previous) self.gui.library_view.model().current_changed(current, previous)

View File

@ -113,6 +113,7 @@ class Worker(Thread):
class AutoAdder(QObject): class AutoAdder(QObject):
metadata_read = pyqtSignal(object) metadata_read = pyqtSignal(object)
auto_convert = pyqtSignal(object)
def __init__(self, path, parent): def __init__(self, path, parent):
QObject.__init__(self, parent) QObject.__init__(self, parent)
@ -124,6 +125,8 @@ class AutoAdder(QObject):
self.metadata_read.connect(self.add_to_db, self.metadata_read.connect(self.add_to_db,
type=Qt.QueuedConnection) type=Qt.QueuedConnection)
QTimer.singleShot(2000, self.initialize) QTimer.singleShot(2000, self.initialize)
self.auto_convert.connect(self.do_auto_convert,
type=Qt.QueuedConnection)
elif path: elif path:
prints(path, prints(path,
'is not a valid directory to watch for new ebooks, ignoring') 'is not a valid directory to watch for new ebooks, ignoring')
@ -163,6 +166,7 @@ class AutoAdder(QObject):
needs_rescan = False needs_rescan = False
duplicates = [] duplicates = []
added_ids = set()
for fname, tdir in data.iteritems(): for fname, tdir in data.iteritems():
paths = [os.path.join(self.worker.path, fname)] paths = [os.path.join(self.worker.path, fname)]
@ -187,9 +191,12 @@ class AutoAdder(QObject):
continue continue
mi = [OPF(open(mi, 'rb'), tdir, mi = [OPF(open(mi, 'rb'), tdir,
populate_spine=False).to_book_metadata()] populate_spine=False).to_book_metadata()]
dups, num = m.add_books(paths, dups, ids = m.add_books(paths,
[os.path.splitext(fname)[1][1:].upper()], mi, [os.path.splitext(fname)[1][1:].upper()], mi,
add_duplicates=not gprefs['auto_add_check_for_duplicates']) add_duplicates=not gprefs['auto_add_check_for_duplicates'],
return_ids=True)
added_ids |= set(ids)
num = len(ids)
if dups: if dups:
path = dups[0][0] path = dups[0][0]
with open(os.path.join(tdir, 'dup_cache.'+dups[1][0].lower()), with open(os.path.join(tdir, 'dup_cache.'+dups[1][0].lower()),
@ -217,8 +224,10 @@ class AutoAdder(QObject):
_('Books with the same title as the following already ' _('Books with the same title as the following already '
'exist in the database. Add them anyway?'), 'exist in the database. Add them anyway?'),
'\n'.join(files)): '\n'.join(files)):
dups, num = m.add_books(paths, formats, metadata, dups, ids = m.add_books(paths, formats, metadata,
add_duplicates=True) add_duplicates=True, return_ids=True)
added_ids |= set(ids)
num = len(ids)
count += num count += num
for tdir in data.itervalues(): for tdir in data.itervalues():
@ -227,6 +236,9 @@ class AutoAdder(QObject):
except: except:
pass pass
if added_ids and gprefs['auto_add_auto_convert']:
self.auto_convert.emit(added_ids)
if count > 0: if count > 0:
m.books_added(count) m.books_added(count)
gui.status_bar.show_message(_( gui.status_bar.show_message(_(
@ -238,4 +250,7 @@ class AutoAdder(QObject):
if needs_rescan: if needs_rescan:
QTimer.singleShot(2000, self.dir_changed) QTimer.singleShot(2000, self.dir_changed)
def do_auto_convert(self, added_ids):
gui = self.parent()
gui.iactions['Convert Books'].auto_convert_auto_add(added_ids)

View File

@ -9,8 +9,8 @@ __docformat__ = 'restructuredtext en'
__license__ = 'GPL v3' __license__ = 'GPL v3'
from PyQt4.Qt import QDialog, QVBoxLayout, QLabel, QDialogButtonBox, \ from PyQt4.Qt import (QDialog, QVBoxLayout, QLabel, QDialogButtonBox,
QListWidget, QAbstractItemView QListWidget, QAbstractItemView)
from PyQt4 import QtGui from PyQt4 import QtGui
class ChoosePluginToolbarsDialog(QDialog): class ChoosePluginToolbarsDialog(QDialog):
@ -39,6 +39,9 @@ class ChoosePluginToolbarsDialog(QDialog):
self._locations_list.setSizePolicy(sizePolicy) self._locations_list.setSizePolicy(sizePolicy)
for key, text in locations: for key, text in locations:
self._locations_list.addItem(text) self._locations_list.addItem(text)
if key in {'toolbar', 'toolbar-device'}:
self._locations_list.item(self._locations_list.count()-1
).setSelected(True)
self._layout.addWidget(self._locations_list) self._layout.addWidget(self._locations_list)
self._footer_label = QLabel( self._footer_label = QLabel(

View File

@ -11,9 +11,9 @@ from datetime import timedelta
import calendar, textwrap import calendar, textwrap
from collections import OrderedDict from collections import OrderedDict
from PyQt4.Qt import QDialog, Qt, QTime, QObject, QMenu, QHBoxLayout, \ from PyQt4.Qt import (QDialog, Qt, QTime, QObject, QMenu, QHBoxLayout,
QAction, QIcon, QMutex, QTimer, pyqtSignal, QWidget, QGridLayout, \ QAction, QIcon, QMutex, QTimer, pyqtSignal, QWidget, QGridLayout,
QCheckBox, QTimeEdit, QLabel, QLineEdit, QDoubleSpinBox QCheckBox, QTimeEdit, QLabel, QLineEdit, QDoubleSpinBox)
from calibre.gui2.dialogs.scheduler_ui import Ui_Dialog from calibre.gui2.dialogs.scheduler_ui import Ui_Dialog
from calibre.gui2 import config as gconf, error_dialog from calibre.gui2 import config as gconf, error_dialog
@ -317,6 +317,8 @@ class SchedulerDialog(QDialog, Ui_Dialog):
return False return False
if un or pw: if un or pw:
self.recipe_model.set_account_info(urn, un, pw) self.recipe_model.set_account_info(urn, un, pw)
else:
self.recipe_model.clear_account_info(urn)
if self.schedule.isChecked(): if self.schedule.isChecked():
schedule_type, schedule = \ schedule_type, schedule = \

View File

@ -140,34 +140,6 @@
</item> </item>
</layout> </layout>
</item> </item>
<item>
<widget class="QGroupBox" name="groupBox">
<property name="maximumSize">
<size>
<width>16777215</width>
<height>60</height>
</size>
</property>
<layout class="QHBoxLayout" name="horizontalLayout_5">
<item>
<widget class="QLabel" name="label_51">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
<horstretch>40</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string/>
</property>
<property name="buddy">
<cstring>matchkind</cstring>
</property>
</widget>
</item>
</layout>
</widget>
</item>
<item> <item>
<widget class="QLabel" name="label_6"> <widget class="QLabel" name="label_6">
<property name="maximumSize"> <property name="maximumSize">

View File

@ -187,9 +187,10 @@ class BooksModel(QAbstractTableModel): # {{{
self.db = None self.db = None
self.reset() self.reset()
def add_books(self, paths, formats, metadata, add_duplicates=False): def add_books(self, paths, formats, metadata, add_duplicates=False,
return_ids=False):
ret = self.db.add_books(paths, formats, metadata, ret = self.db.add_books(paths, formats, metadata,
add_duplicates=add_duplicates) add_duplicates=add_duplicates, return_ids=return_ids)
self.count_changed() self.count_changed()
return ret return ret

View File

@ -262,9 +262,11 @@ class BooksView(QTableView): # {{{
self.selected_ids = [idc(r) for r in selected_rows] self.selected_ids = [idc(r) for r in selected_rows]
def sorting_done(self, indexc): def sorting_done(self, indexc):
pos = self.horizontalScrollBar().value()
self.select_rows(self.selected_ids, using_ids=True, change_current=True, self.select_rows(self.selected_ids, using_ids=True, change_current=True,
scroll=True) scroll=True)
self.selected_ids = [] self.selected_ids = []
self.horizontalScrollBar().setValue(pos)
def sort_by_named_field(self, field, order, reset=True): def sort_by_named_field(self, field, order, reset=True):
if field in self.column_map: if field in self.column_map:

View File

@ -36,6 +36,7 @@ class ConfigWidget(ConfigWidgetBase, Ui_Form):
r('new_book_tags', prefs, setting=CommaSeparatedList) r('new_book_tags', prefs, setting=CommaSeparatedList)
r('auto_add_path', gprefs, restart_required=True) r('auto_add_path', gprefs, restart_required=True)
r('auto_add_check_for_duplicates', gprefs) r('auto_add_check_for_duplicates', gprefs)
r('auto_add_auto_convert', gprefs)
self.filename_pattern = FilenamePattern(self) self.filename_pattern = FilenamePattern(self)
self.metadata_box.layout().insertWidget(0, self.filename_pattern) self.metadata_box.layout().insertWidget(0, self.filename_pattern)

View File

@ -151,6 +151,19 @@ Author matching is exact.</string>
<string>&amp;Automatic Adding</string> <string>&amp;Automatic Adding</string>
</attribute> </attribute>
<layout class="QGridLayout" name="gridLayout_3"> <layout class="QGridLayout" name="gridLayout_3">
<item row="3" column="0" colspan="2">
<widget class="QCheckBox" name="opt_auto_add_check_for_duplicates">
<property name="toolTip">
<string>If set, this option will causes calibre to check if a file
being auto-added is already in the calibre library.
If it is, a meesage will pop up asking you whether
you want to add it anyway.</string>
</property>
<property name="text">
<string>Check for &amp;duplicates when auto-adding files</string>
</property>
</widget>
</item>
<item row="0" column="0" colspan="2"> <item row="0" column="0" colspan="2">
<widget class="QLabel" name="label"> <widget class="QLabel" name="label">
<property name="text"> <property name="text">
@ -168,7 +181,7 @@ Author matching is exact.</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="4" column="0"> <item row="5" column="0">
<widget class="QGroupBox" name="groupBox"> <widget class="QGroupBox" name="groupBox">
<property name="title"> <property name="title">
<string>Ignore files with the following extensions when automatically adding </string> <string>Ignore files with the following extensions when automatically adding </string>
@ -187,7 +200,7 @@ Author matching is exact.</string>
</layout> </layout>
</widget> </widget>
</item> </item>
<item row="4" column="1"> <item row="5" column="1">
<spacer name="horizontalSpacer_2"> <spacer name="horizontalSpacer_2">
<property name="orientation"> <property name="orientation">
<enum>Qt::Horizontal</enum> <enum>Qt::Horizontal</enum>
@ -225,16 +238,10 @@ Author matching is exact.</string>
</item> </item>
</layout> </layout>
</item> </item>
<item row="3" column="0" colspan="2"> <item row="4" column="0">
<widget class="QCheckBox" name="opt_auto_add_check_for_duplicates"> <widget class="QCheckBox" name="opt_auto_add_auto_convert">
<property name="toolTip">
<string>If set, this option will causes calibre to check if a file
being auto-added is already in the calibre library.
If it is, a meesage will pop up asking you whether
you want to add it anyway.</string>
</property>
<property name="text"> <property name="text">
<string>Check for &amp;duplicates when auto-adding files</string> <string>Automatically &amp;convert added files to the current output format</string>
</property> </property>
</widget> </widget>
</item> </item>

View File

@ -73,11 +73,13 @@ class OpenSearchOPDSStore(StorePlugin):
type = link.get('type') type = link.get('type')
if rel and href and type: if rel and href and type:
if rel in ('http://opds-spec.org/thumbnail', 'http://opds-spec.org/image/thumbnail'): if 'http://opds-spec.org/thumbnail' in rel:
s.cover_url = href s.cover_url = href
elif rel == u'http://opds-spec.org/acquisition/buy': elif 'http://opds-spec.org/image/thumbnail' in rel:
s.cover_url = href
elif 'http://opds-spec.org/acquisition/buy' in rel:
s.detail_item = href s.detail_item = href
elif rel == u'http://opds-spec.org/acquisition': elif 'http://opds-spec.org/acquisition' in rel:
if type: if type:
ext = mimetypes.guess_extension(type) ext = mimetypes.guess_extension(type)
if ext: if ext:

View File

@ -6,10 +6,10 @@ __license__ = 'GPL 3'
__copyright__ = '2011-2012, Tomasz Długosz <tomek3d@gmail.com>' __copyright__ = '2011-2012, Tomasz Długosz <tomek3d@gmail.com>'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
import copy
import re import re
import urllib import urllib
from contextlib import closing from contextlib import closing
import copy
from lxml import html from lxml import html

View File

@ -25,7 +25,7 @@ from calibre.ebooks.conversion.config import GuiRecommendations, \
from calibre.gui2.convert import bulk_defaults_for_input_format from calibre.gui2.convert import bulk_defaults_for_input_format
def convert_single_ebook(parent, db, book_ids, auto_conversion=False, # {{{ def convert_single_ebook(parent, db, book_ids, auto_conversion=False, # {{{
out_format=None): out_format=None, show_no_format_warning=True):
changed = False changed = False
jobs = [] jobs = []
bad = [] bad = []
@ -91,7 +91,7 @@ def convert_single_ebook(parent, db, book_ids, auto_conversion=False, # {{{
except NoSupportedInputFormats: except NoSupportedInputFormats:
bad.append(book_id) bad.append(book_id)
if bad != []: if bad and show_no_format_warning:
res = [] res = []
for id in bad: for id in bad:
title = db.title(id, True) title = db.title(id, True)

View File

@ -151,7 +151,7 @@ class UpdateMixin(object):
plt = u'' plt = u''
if has_plugin_updates: if has_plugin_updates:
plt = _(' (%d plugin updates)')%plugin_updates plt = _(' (%d plugin updates)')%plugin_updates
msg = (u'<span style="color:red; font-weight: bold">%s: ' msg = (u'<span style="color:green; font-weight: bold">%s: '
u'<a href="update:%s">%s%s</a></span>') % ( u'<a href="update:%s">%s%s</a></span>') % (
_('Update found'), version, calibre_version, plt) _('Update found'), version, calibre_version, plt)
else: else:

View File

@ -31,6 +31,7 @@ class BookmarkManager(QDialog, Ui_BookmarkManager):
bookmarks = self.bookmarks[:] bookmarks = self.bookmarks[:]
self._model = BookmarkTableModel(self, bookmarks) self._model = BookmarkTableModel(self, bookmarks)
self.bookmarks_table.setModel(self._model) self.bookmarks_table.setModel(self._model)
self.bookmarks_table.resizeColumnsToContents()
def delete_bookmark(self): def delete_bookmark(self):
indexes = self.bookmarks_table.selectionModel().selectedIndexes() indexes = self.bookmarks_table.selectionModel().selectedIndexes()
@ -80,7 +81,7 @@ class BookmarkManager(QDialog, Ui_BookmarkManager):
if not bad: if not bad:
bookmarks = self._model.bookmarks[:] bookmarks = self._model.bookmarks[:]
for bm in imported: for bm in imported:
if bm not in bookmarks and bm[0] != 'calibre_current_page_bookmark': if bm not in bookmarks and bm['title'] != 'calibre_current_page_bookmark':
bookmarks.append(bm) bookmarks.append(bm)
self.set_bookmarks(bookmarks) self.set_bookmarks(bookmarks)
@ -105,13 +106,14 @@ class BookmarkTableModel(QAbstractTableModel):
def data(self, index, role): def data(self, index, role):
if role in (Qt.DisplayRole, Qt.EditRole): if role in (Qt.DisplayRole, Qt.EditRole):
ans = self.bookmarks[index.row()][0] ans = self.bookmarks[index.row()]['title']
return NONE if ans is None else QVariant(ans) return NONE if ans is None else QVariant(ans)
return NONE return NONE
def setData(self, index, value, role): def setData(self, index, value, role):
if role == Qt.EditRole: if role == Qt.EditRole:
self.bookmarks[index.row()] = (unicode(value.toString()).strip(), self.bookmarks[index.row()][1]) bm = self.bookmarks[index.row()]
bm['title'] = unicode(value.toString()).strip()
self.emit(SIGNAL("dataChanged(QModelIndex, QModelIndex)"), index, index) self.emit(SIGNAL("dataChanged(QModelIndex, QModelIndex)"), index, index)
return True return True
return False return False

View File

@ -4,14 +4,14 @@ __copyright__ = '2008, Kovid Goyal kovid@kovidgoyal.net'
__docformat__ = 'restructuredtext en' __docformat__ = 'restructuredtext en'
# Imports {{{ # Imports {{{
import os, math, re, glob, sys, zipfile import os, math, glob, zipfile
from base64 import b64encode from base64 import b64encode
from functools import partial from functools import partial
from PyQt4.Qt import (QSize, QSizePolicy, QUrl, SIGNAL, Qt, from PyQt4.Qt import (QSize, QSizePolicy, QUrl, SIGNAL, Qt,
QPainter, QPalette, QBrush, QFontDatabase, QDialog, QPainter, QPalette, QBrush, QFontDatabase, QDialog,
QColor, QPoint, QImage, QRegion, QVariant, QIcon, QColor, QPoint, QImage, QRegion, QVariant, QIcon,
QFont, pyqtSignature, QAction, QByteArray, QMenu, QFont, pyqtSignature, QAction, QMenu,
pyqtSignal, QSwipeGesture, QApplication) pyqtSignal, QSwipeGesture, QApplication)
from PyQt4.QtWebKit import QWebPage, QWebView, QWebSettings from PyQt4.QtWebKit import QWebPage, QWebView, QWebSettings
@ -21,10 +21,11 @@ from calibre.gui2.viewer.config_ui import Ui_Dialog
from calibre.gui2.viewer.flip import SlideFlip from calibre.gui2.viewer.flip import SlideFlip
from calibre.gui2.shortcuts import Shortcuts, ShortcutConfig from calibre.gui2.shortcuts import Shortcuts, ShortcutConfig
from calibre.constants import iswindows from calibre.constants import iswindows
from calibre import prints, guess_type from calibre import prints
from calibre.gui2.viewer.keys import SHORTCUTS from calibre.gui2.viewer.keys import SHORTCUTS
from calibre.gui2.viewer.javascript import JavaScriptLoader from calibre.gui2.viewer.javascript import JavaScriptLoader
from calibre.gui2.viewer.position import PagePosition from calibre.gui2.viewer.position import PagePosition
from calibre.ebooks.oeb.display.webview import load_html
# }}} # }}}
@ -312,10 +313,14 @@ class Document(QWebPage): # {{{
self.javascript('goto_reference("%s")'%ref) self.javascript('goto_reference("%s")'%ref)
def goto_bookmark(self, bm): def goto_bookmark(self, bm):
if bm['type'] == 'legacy':
bm = bm['pos']
bm = bm.strip() bm = bm.strip()
if bm.startswith('>'): if bm.startswith('>'):
bm = bm[1:].strip() bm = bm[1:].strip()
self.javascript('scroll_to_bookmark("%s")'%bm) self.javascript('scroll_to_bookmark("%s")'%bm)
elif bm['type'] == 'cfi':
self.page_position.to_pos(bm['pos'])
def javascript(self, string, typ=None): def javascript(self, string, typ=None):
ans = self.mainFrame().evaluateJavaScript(string) ans = self.mainFrame().evaluateJavaScript(string)
@ -366,40 +371,9 @@ class Document(QWebPage): # {{{
def elem_outer_xml(self, elem): def elem_outer_xml(self, elem):
return unicode(elem.toOuterXml()) return unicode(elem.toOuterXml())
def find_bookmark_element(self):
mf = self.mainFrame()
doc_pos = self.ypos
min_delta, min_elem = sys.maxint, None
for y in range(10, -500, -10):
for x in range(-50, 500, 10):
pos = QPoint(x, y)
result = mf.hitTestContent(pos)
if result.isNull(): continue
elem = result.enclosingBlockElement()
if elem.isNull(): continue
try:
ypos = self.element_ypos(elem)
except:
continue
delta = abs(ypos - doc_pos)
if delta < 25:
return elem
if delta < min_delta:
min_elem, min_delta = elem, delta
return min_elem
def bookmark(self): def bookmark(self):
elem = self.find_bookmark_element() pos = self.page_position.current_pos
return {'type':'cfi', 'pos':pos}
if elem is None or self.element_ypos(elem) < 100:
bm = 'body|%f'%(float(self.ypos)/(self.height*0.7))
else:
bm = unicode(elem.evaluateJavaScript(
'calculate_bookmark(%d, this)'%self.ypos).toString())
if not bm:
bm = 'body|%f'%(float(self.ypos)/(self.height*0.7))
return bm
@property @property
def at_bottom(self): def at_bottom(self):
@ -474,19 +448,6 @@ class Document(QWebPage): # {{{
# }}} # }}}
class EntityDeclarationProcessor(object): # {{{
def __init__(self, html):
self.declared_entities = {}
for match in re.finditer(r'<!\s*ENTITY\s+([^>]+)>', html):
tokens = match.group(1).split()
if len(tokens) > 1:
self.declared_entities[tokens[0].strip()] = tokens[1].strip().replace('"', '')
self.processed_html = html
for key, val in self.declared_entities.iteritems():
self.processed_html = self.processed_html.replace('&%s;'%key, val)
# }}}
class DocumentView(QWebView): # {{{ class DocumentView(QWebView): # {{{
magnification_changed = pyqtSignal(object) magnification_changed = pyqtSignal(object)
@ -497,8 +458,6 @@ class DocumentView(QWebView): # {{{
self.is_auto_repeat_event = False self.is_auto_repeat_event = False
self.debug_javascript = debug_javascript self.debug_javascript = debug_javascript
self.shortcuts = Shortcuts(SHORTCUTS, 'shortcuts/viewer') self.shortcuts = Shortcuts(SHORTCUTS, 'shortcuts/viewer')
self.self_closing_pat = re.compile(r'<([a-z1-6]+)\s+([^>]+)/>',
re.IGNORECASE)
self.setSizePolicy(QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)) self.setSizePolicy(QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding))
self._size_hint = QSize(510, 680) self._size_hint = QSize(510, 680)
self.initial_pos = 0.0 self.initial_pos = 0.0
@ -689,31 +648,16 @@ class DocumentView(QWebView): # {{{
def path(self): def path(self):
return os.path.abspath(unicode(self.url().toLocalFile())) return os.path.abspath(unicode(self.url().toLocalFile()))
def self_closing_sub(self, match):
tag = match.group(1)
if tag.lower().strip() == 'br':
return match.group()
return '<%s %s></%s>'%(match.group(1), match.group(2), match.group(1))
def load_path(self, path, pos=0.0): def load_path(self, path, pos=0.0):
self.initial_pos = pos self.initial_pos = pos
mt = getattr(path, 'mime_type', None)
if mt is None:
mt = guess_type(path)[0]
html = open(path, 'rb').read().decode(path.encoding, 'replace')
html = EntityDeclarationProcessor(html).processed_html
has_svg = re.search(r'<[:a-zA-Z]*svg', html) is not None
if 'xhtml' in mt: def callback(lu):
html = self.self_closing_pat.sub(self.self_closing_sub, html) self.loading_url = lu
if self.manager is not None: if self.manager is not None:
self.manager.load_started() self.manager.load_started()
self.loading_url = QUrl.fromLocalFile(path)
html = re.sub(ur'<\s*title\s*/\s*>', u'', html, flags=re.IGNORECASE) load_html(path, self, codec=path.encoding, mime_type=getattr(path,
if has_svg: 'mime_type', None), pre_load_callback=callback)
self.setContent(QByteArray(html.encode(path.encoding)), mt, QUrl.fromLocalFile(path))
else:
self.setHtml(html, self.loading_url)
self.turn_off_internal_scrollbars() self.turn_off_internal_scrollbars()
def initialize_scrollbar(self): def initialize_scrollbar(self):
@ -1011,8 +955,12 @@ class DocumentView(QWebView): # {{{
finally: finally:
self.is_auto_repeat_event = False self.is_auto_repeat_event = False
elif key == 'Down': elif key == 'Down':
if self.document.at_bottom:
self.manager.next_document()
self.scroll_by(y=15) self.scroll_by(y=15)
elif key == 'Up': elif key == 'Up':
if self.document.at_top:
self.manager.previous_document()
self.scroll_by(y=-15) self.scroll_by(y=-15)
elif key == 'Left': elif key == 'Left':
self.scroll_by(x=-15) self.scroll_by(x=-15)

View File

@ -27,6 +27,7 @@ from calibre.ebooks.metadata import MetaInformation
from calibre.customize.ui import available_input_formats from calibre.customize.ui import available_input_formats
from calibre.gui2.viewer.dictionary import Lookup from calibre.gui2.viewer.dictionary import Lookup
from calibre import as_unicode, force_unicode, isbytestring from calibre import as_unicode, force_unicode, isbytestring
from calibre.ptempfile import reset_base_dir
vprefs = JSONConfig('viewer') vprefs = JSONConfig('viewer')
@ -512,11 +513,12 @@ class EbookViewer(MainWindow, Ui_EbookViewer):
self.load_path(self.iterator.spine[spine_index]) self.load_path(self.iterator.spine[spine_index])
def goto_bookmark(self, bm): def goto_bookmark(self, bm):
m = bm[1].split('#') spine_index = bm['spine']
if len(m) > 1:
spine_index, m = int(m[0]), m[1]
if spine_index > -1 and self.current_index == spine_index: if spine_index > -1 and self.current_index == spine_index:
self.view.goto_bookmark(m) if self.resize_in_progress:
self.view.document.page_position.set_pos(bm['pos'])
else:
self.view.goto_bookmark(bm)
else: else:
self.pending_bookmark = bm self.pending_bookmark = bm
if spine_index < 0 or spine_index >= len(self.iterator.spine): if spine_index < 0 or spine_index >= len(self.iterator.spine):
@ -699,6 +701,14 @@ class EbookViewer(MainWindow, Ui_EbookViewer):
self.view.load_path(path, pos=pos) self.view.load_path(path, pos=pos)
def viewport_resize_started(self, event): def viewport_resize_started(self, event):
old, curr = event.size(), event.oldSize()
if not self.window_mode_changed and old.width() == curr.width():
# No relayout changes, so page position does not need to be saved
# This is needed as Qt generates a viewport resized event that
# changes only the height after a file has been loaded. This can
# cause the last read position bookmark to become slightly
# inaccurate
return
if not self.resize_in_progress: if not self.resize_in_progress:
# First resize, so save the current page position # First resize, so save the current page position
self.resize_in_progress = True self.resize_in_progress = True
@ -746,9 +756,10 @@ class EbookViewer(MainWindow, Ui_EbookViewer):
_('Enter title for bookmark:'), text=bm) _('Enter title for bookmark:'), text=bm)
title = unicode(title).strip() title = unicode(title).strip()
if ok and title: if ok and title:
pos = self.view.bookmark() bm = self.view.bookmark()
bookmark = '%d#%s'%(self.current_index, pos) bm['spine'] = self.current_index
self.iterator.add_bookmark((title, bookmark)) bm['title'] = title
self.iterator.add_bookmark(bm)
self.set_bookmarks(self.iterator.bookmarks) self.set_bookmarks(self.iterator.bookmarks)
def set_bookmarks(self, bookmarks): def set_bookmarks(self, bookmarks):
@ -758,12 +769,12 @@ class EbookViewer(MainWindow, Ui_EbookViewer):
current_page = None current_page = None
self.existing_bookmarks = [] self.existing_bookmarks = []
for bm in bookmarks: for bm in bookmarks:
if bm[0] == 'calibre_current_page_bookmark' and \ if bm['title'] == 'calibre_current_page_bookmark':
self.get_remember_current_page_opt(): if self.get_remember_current_page_opt():
current_page = bm current_page = bm
else: else:
self.existing_bookmarks.append(bm[0]) self.existing_bookmarks.append(bm['title'])
self.bookmarks_menu.addAction(bm[0], partial(self.goto_bookmark, bm)) self.bookmarks_menu.addAction(bm['title'], partial(self.goto_bookmark, bm))
return current_page return current_page
def manage_bookmarks(self): def manage_bookmarks(self):
@ -783,9 +794,10 @@ class EbookViewer(MainWindow, Ui_EbookViewer):
return return
if hasattr(self, 'current_index'): if hasattr(self, 'current_index'):
try: try:
pos = self.view.bookmark() bm = self.view.bookmark()
bookmark = '%d#%s'%(self.current_index, pos) bm['spine'] = self.current_index
self.iterator.add_bookmark(('calibre_current_page_bookmark', bookmark)) bm['title'] = 'calibre_current_page_bookmark'
self.iterator.add_bookmark(bm)
except: except:
traceback.print_exc() traceback.print_exc()
@ -947,6 +959,7 @@ View an ebook.
def main(args=sys.argv): def main(args=sys.argv):
# Ensure viewer can continue to function if GUI is closed # Ensure viewer can continue to function if GUI is closed
os.environ.pop('CALIBRE_WORKER_TEMP_DIR', None) os.environ.pop('CALIBRE_WORKER_TEMP_DIR', None)
reset_base_dir()
parser = option_parser() parser = option_parser()
opts, args = parser.parse_args(args) opts, args = parser.parse_args(args)

View File

@ -67,10 +67,16 @@ class PagePosition(object):
def restore(self): def restore(self):
if self._cpos is None: return if self._cpos is None: return
if isinstance(self._cpos, (int, float)): self.to_pos(self._cpos)
self.document.scroll_fraction = self._cpos
else:
self.scroll_to_cfi(self._cpos)
self._cpos = None self._cpos = None
def to_pos(self, pos):
if isinstance(pos, (int, float)):
self.document.scroll_fraction = pos
else:
self.scroll_to_cfi(pos)
def set_pos(self, pos):
self._cpos = pos

View File

@ -233,7 +233,7 @@ def do_add(db, paths, one_book_per_directory, recurse, add_duplicates, otitle,
if not mi.authors: if not mi.authors:
mi.authors = [_('Unknown')] mi.authors = [_('Unknown')]
for x in ('title', 'authors', 'isbn', 'tags', 'series'): for x in ('title', 'authors', 'isbn', 'tags', 'series'):
val = locals()[x] val = locals()['o'+x]
if val: setattr(mi, x[1:], val) if val: setattr(mi, x[1:], val)
if oseries: if oseries:
mi.series_index = oseries_index mi.series_index = oseries_index
@ -356,7 +356,7 @@ def command_add(args, dbpath):
print >>sys.stderr, _('You must specify at least one file to add') print >>sys.stderr, _('You must specify at least one file to add')
return 1 return 1
do_add(get_db(dbpath, opts), args[1:], opts.one_book_per_directory, do_add(get_db(dbpath, opts), args[1:], opts.one_book_per_directory,
opts.recurse, opts.duplicates, opts.title, opts.author, opts.isbn, opts.recurse, opts.duplicates, opts.title, opts.authors, opts.isbn,
tags, opts.series, opts.series_index) tags, opts.series, opts.series_index)
return 0 return 0

View File

@ -3243,7 +3243,8 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
return id return id
def add_books(self, paths, formats, metadata, add_duplicates=True): def add_books(self, paths, formats, metadata, add_duplicates=True,
return_ids=False):
''' '''
Add a book to the database. The result cache is not updated. Add a book to the database. The result cache is not updated.
:param:`paths` List of paths to book files or file-like objects :param:`paths` List of paths to book files or file-like objects
@ -3289,7 +3290,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
formats = list(duplicate[1] for duplicate in duplicates) formats = list(duplicate[1] for duplicate in duplicates)
metadata = list(duplicate[2] for duplicate in duplicates) metadata = list(duplicate[2] for duplicate in duplicates)
return (paths, formats, metadata), len(ids) return (paths, formats, metadata), len(ids)
return None, len(ids) return None, (ids if return_ids else len(ids))
def import_book(self, mi, formats, notify=True, import_hooks=True, def import_book(self, mi, formats, notify=True, import_hooks=True,
apply_import_tags=True, preserve_uuid=False): apply_import_tags=True, preserve_uuid=False):

View File

@ -40,6 +40,46 @@ entry_points = {
], ],
} }
class PreserveMIMEDefaults(object):
def __init__(self):
self.initial_values = {}
def __enter__(self):
def_data_dirs = '/usr/local/share:/usr/share'
paths = os.environ.get('XDG_DATA_DIRS', def_data_dirs)
paths = paths.split(':')
paths.append(os.environ.get('XDG_DATA_HOME', os.path.expanduser(
'~/.local/share')))
paths = list(filter(os.path.isdir, paths))
if not paths:
# Env var had garbage in it, ignore it
paths = def_data_dirs.split(':')
paths = list(filter(os.path.isdir, paths))
self.paths = {os.path.join(x, 'applications/defaults.list') for x in
paths}
self.initial_values = {}
for x in self.paths:
try:
with open(x, 'rb') as f:
self.initial_values[x] = f.read()
except:
self.initial_values[x] = None
def __exit__(self, *args):
for path, val in self.initial_values.iteritems():
if val is None:
try:
os.remove(path)
except:
pass
elif os.path.exists(path):
with open(path, 'r+b') as f:
if f.read() != val:
f.seek(0)
f.truncate()
f.write(val)
# Uninstall script {{{ # Uninstall script {{{
UNINSTALL = '''\ UNINSTALL = '''\
#!{python} #!{python}
@ -202,6 +242,10 @@ class PostInstall:
if not os.path.exists(os.path.dirname(f)): if not os.path.exists(os.path.dirname(f)):
os.makedirs(os.path.dirname(f)) os.makedirs(os.path.dirname(f))
self.manifest.append(f) self.manifest.append(f)
complete = 'calibre-complete'
if getattr(sys, 'frozen_path', None):
complete = os.path.join(getattr(sys, 'frozen_path'), complete)
self.info('Installing bash completion to', f) self.info('Installing bash completion to', f)
with open(f, 'wb') as f: with open(f, 'wb') as f:
f.write('# calibre Bash Shell Completion\n') f.write('# calibre Bash Shell Completion\n')
@ -286,8 +330,8 @@ class PostInstall:
} }
complete -o nospace -F _ebook_device ebook-device complete -o nospace -F _ebook_device ebook-device
complete -o nospace -C calibre-complete ebook-convert complete -o nospace -C %s ebook-convert
''')) ''')%complete)
except TypeError as err: except TypeError as err:
if 'resolve_entities' in str(err): if 'resolve_entities' in str(err):
print 'You need python-lxml >= 2.0.5 for calibre' print 'You need python-lxml >= 2.0.5 for calibre'
@ -333,12 +377,10 @@ class PostInstall:
def setup_desktop_integration(self): # {{{ def setup_desktop_integration(self): # {{{
try: try:
self.info('Setting up desktop integration...') self.info('Setting up desktop integration...')
with TemporaryDirectory() as tdir, CurrentDir(tdir), \
with TemporaryDirectory() as tdir: PreserveMIMEDefaults():
with CurrentDir(tdir):
render_img('mimetypes/lrf.png', 'calibre-lrf.png') render_img('mimetypes/lrf.png', 'calibre-lrf.png')
check_call('xdg-icon-resource install --noupdate --context mimetypes --size 128 calibre-lrf.png application-lrf', shell=True) check_call('xdg-icon-resource install --noupdate --context mimetypes --size 128 calibre-lrf.png application-lrf', shell=True)
self.icon_resources.append(('mimetypes', 'application-lrf', '128')) self.icon_resources.append(('mimetypes', 'application-lrf', '128'))

View File

@ -74,6 +74,11 @@ def base_dir():
return _base_dir return _base_dir
def reset_base_dir():
global _base_dir
_base_dir = None
base_dir()
def force_unicode(x): def force_unicode(x):
# Cannot use the implementation in calibre.__init__ as it causes a circular # Cannot use the implementation in calibre.__init__ as it causes a circular
# dependency # dependency

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -4,9 +4,9 @@
# #
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: calibre 0.8.44\n" "Project-Id-Version: calibre 0.8.45\n"
"POT-Creation-Date: 2012-03-24 16:05+IST\n" "POT-Creation-Date: 2012-03-30 09:11+IST\n"
"PO-Revision-Date: 2012-03-24 16:05+IST\n" "PO-Revision-Date: 2012-03-30 09:11+IST\n"
"Last-Translator: Automatically generated\n" "Last-Translator: Automatically generated\n"
"Language-Team: LANGUAGE\n" "Language-Team: LANGUAGE\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
@ -100,10 +100,11 @@ msgstr ""
#: /home/kovid/work/calibre/src/calibre/ebooks/metadata/sources/ozon.py:125 #: /home/kovid/work/calibre/src/calibre/ebooks/metadata/sources/ozon.py:125
#: /home/kovid/work/calibre/src/calibre/ebooks/metadata/txt.py:18 #: /home/kovid/work/calibre/src/calibre/ebooks/metadata/txt.py:18
#: /home/kovid/work/calibre/src/calibre/ebooks/mobi/reader/headers.py:26 #: /home/kovid/work/calibre/src/calibre/ebooks/mobi/reader/headers.py:26
#: /home/kovid/work/calibre/src/calibre/ebooks/mobi/reader/headers.py:117 #: /home/kovid/work/calibre/src/calibre/ebooks/mobi/reader/headers.py:78
#: /home/kovid/work/calibre/src/calibre/ebooks/mobi/reader/headers.py:159 #: /home/kovid/work/calibre/src/calibre/ebooks/mobi/reader/headers.py:133
#: /home/kovid/work/calibre/src/calibre/ebooks/mobi/reader/headers.py:175
#: /home/kovid/work/calibre/src/calibre/ebooks/mobi/reader/mobi6.py:615 #: /home/kovid/work/calibre/src/calibre/ebooks/mobi/reader/mobi6.py:615
#: /home/kovid/work/calibre/src/calibre/ebooks/mobi/utils.py:312 #: /home/kovid/work/calibre/src/calibre/ebooks/mobi/utils.py:314
#: /home/kovid/work/calibre/src/calibre/ebooks/mobi/writer2/indexer.py:497 #: /home/kovid/work/calibre/src/calibre/ebooks/mobi/writer2/indexer.py:497
#: /home/kovid/work/calibre/src/calibre/ebooks/odt/input.py:168 #: /home/kovid/work/calibre/src/calibre/ebooks/odt/input.py:168
#: /home/kovid/work/calibre/src/calibre/ebooks/odt/input.py:170 #: /home/kovid/work/calibre/src/calibre/ebooks/odt/input.py:170
@ -135,8 +136,8 @@ msgstr ""
#: /home/kovid/work/calibre/src/calibre/ebooks/pdf/manipulate/rotate.py:63 #: /home/kovid/work/calibre/src/calibre/ebooks/pdf/manipulate/rotate.py:63
#: /home/kovid/work/calibre/src/calibre/ebooks/pdf/manipulate/split.py:81 #: /home/kovid/work/calibre/src/calibre/ebooks/pdf/manipulate/split.py:81
#: /home/kovid/work/calibre/src/calibre/ebooks/pdf/manipulate/split.py:82 #: /home/kovid/work/calibre/src/calibre/ebooks/pdf/manipulate/split.py:82
#: /home/kovid/work/calibre/src/calibre/ebooks/pdf/writer.py:102
#: /home/kovid/work/calibre/src/calibre/ebooks/pdf/writer.py:103 #: /home/kovid/work/calibre/src/calibre/ebooks/pdf/writer.py:103
#: /home/kovid/work/calibre/src/calibre/ebooks/pdf/writer.py:104
#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:416 #: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:416
#: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:424 #: /home/kovid/work/calibre/src/calibre/gui2/__init__.py:424
#: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:166 #: /home/kovid/work/calibre/src/calibre/gui2/actions/add.py:166
@ -156,7 +157,7 @@ msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/book_info.py:128 #: /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/comicconf.py:47
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk.py:811 #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk.py:811
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler.py:375 #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler.py:377
#: /home/kovid/work/calibre/src/calibre/gui2/email.py:191 #: /home/kovid/work/calibre/src/calibre/gui2/email.py:191
#: /home/kovid/work/calibre/src/calibre/gui2/email.py:206 #: /home/kovid/work/calibre/src/calibre/gui2/email.py:206
#: /home/kovid/work/calibre/src/calibre/gui2/library/models.py:408 #: /home/kovid/work/calibre/src/calibre/gui2/library/models.py:408
@ -174,7 +175,7 @@ msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single_download.py:168 #: /home/kovid/work/calibre/src/calibre/gui2/metadata/single_download.py:168
#: /home/kovid/work/calibre/src/calibre/gui2/store/search/models.py:204 #: /home/kovid/work/calibre/src/calibre/gui2/store/search/models.py:204
#: /home/kovid/work/calibre/src/calibre/gui2/store/stores/google_books_plugin.py:107 #: /home/kovid/work/calibre/src/calibre/gui2/store/stores/google_books_plugin.py:107
#: /home/kovid/work/calibre/src/calibre/gui2/viewer/main.py:205 #: /home/kovid/work/calibre/src/calibre/gui2/viewer/main.py:206
#: /home/kovid/work/calibre/src/calibre/library/cli.py:234 #: /home/kovid/work/calibre/src/calibre/library/cli.py:234
#: /home/kovid/work/calibre/src/calibre/library/database.py:914 #: /home/kovid/work/calibre/src/calibre/library/database.py:914
#: /home/kovid/work/calibre/src/calibre/library/database2.py:561 #: /home/kovid/work/calibre/src/calibre/library/database2.py:561
@ -1007,7 +1008,7 @@ msgstr ""
#: /home/kovid/work/calibre/src/calibre/devices/usbms/device.py:1057 #: /home/kovid/work/calibre/src/calibre/devices/usbms/device.py:1057
#: /home/kovid/work/calibre/src/calibre/devices/usbms/device.py:1092 #: /home/kovid/work/calibre/src/calibre/devices/usbms/device.py:1092
#: /home/kovid/work/calibre/src/calibre/gui2/actions/fetch_news.py:73 #: /home/kovid/work/calibre/src/calibre/gui2/actions/fetch_news.py:73
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler.py:464 #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler.py:466
#: /home/kovid/work/calibre/src/calibre/gui2/tag_browser/model.py:1154 #: /home/kovid/work/calibre/src/calibre/gui2/tag_browser/model.py:1154
#: /home/kovid/work/calibre/src/calibre/gui2/tag_browser/model.py:1156 #: /home/kovid/work/calibre/src/calibre/gui2/tag_browser/model.py:1156
#: /home/kovid/work/calibre/src/calibre/library/database2.py:346 #: /home/kovid/work/calibre/src/calibre/library/database2.py:346
@ -3315,7 +3316,7 @@ msgstr ""
#: /home/kovid/work/calibre/src/calibre/ebooks/oeb/base.py:1239 #: /home/kovid/work/calibre/src/calibre/ebooks/oeb/base.py:1239
#: /home/kovid/work/calibre/src/calibre/ebooks/oeb/transforms/htmltoc.py:15 #: /home/kovid/work/calibre/src/calibre/ebooks/oeb/transforms/htmltoc.py:15
#: /home/kovid/work/calibre/src/calibre/gui2/viewer/main.py:56 #: /home/kovid/work/calibre/src/calibre/gui2/viewer/main.py:57
#: /home/kovid/work/calibre/src/calibre/gui2/viewer/main_ui.py:199 #: /home/kovid/work/calibre/src/calibre/gui2/viewer/main_ui.py:199
msgid "Table of Contents" msgid "Table of Contents"
msgstr "" msgstr ""
@ -3376,7 +3377,7 @@ msgstr ""
msgid "Main Text" msgid "Main Text"
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/ebooks/oeb/iterator.py:43 #: /home/kovid/work/calibre/src/calibre/ebooks/oeb/iterator.py:45
#, python-format #, python-format
msgid "%s format books are not supported" msgid "%s format books are not supported"
msgstr "" msgstr ""
@ -4232,7 +4233,7 @@ msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/actions/choose_library.py:479 #: /home/kovid/work/calibre/src/calibre/gui2/actions/choose_library.py:479
#: /home/kovid/work/calibre/src/calibre/gui2/actions/copy_to_library.py:224 #: /home/kovid/work/calibre/src/calibre/gui2/actions/copy_to_library.py:224
#: /home/kovid/work/calibre/src/calibre/gui2/actions/save_to_disk.py:100 #: /home/kovid/work/calibre/src/calibre/gui2/actions/save_to_disk.py:100
#: /home/kovid/work/calibre/src/calibre/gui2/library/views.py:943 #: /home/kovid/work/calibre/src/calibre/gui2/library/views.py:945
msgid "Not allowed" msgid "Not allowed"
msgstr "" msgstr ""
@ -6359,7 +6360,7 @@ msgid "&Monospaced font family:"
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata.py:47 #: /home/kovid/work/calibre/src/calibre/gui2/convert/metadata.py:47
#: /home/kovid/work/calibre/src/calibre/gui2/viewer/main.py:115 #: /home/kovid/work/calibre/src/calibre/gui2/viewer/main.py:116
#: /home/kovid/work/calibre/src/calibre/gui2/viewer/main_ui.py:200 #: /home/kovid/work/calibre/src/calibre/gui2/viewer/main_ui.py:200
msgid "Metadata" msgid "Metadata"
msgstr "" msgstr ""
@ -7622,7 +7623,7 @@ msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/check_library.py:342 #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/check_library.py:342
#: /home/kovid/work/calibre/src/calibre/gui2/store/config/chooser/models.py:21 #: /home/kovid/work/calibre/src/calibre/gui2/store/config/chooser/models.py:21
#: /home/kovid/work/calibre/src/calibre/gui2/viewer/bookmarkmanager.py:89 #: /home/kovid/work/calibre/src/calibre/gui2/viewer/bookmarkmanager.py:90
#: /home/kovid/work/calibre/src/calibre/library/server/browse.py:257 #: /home/kovid/work/calibre/src/calibre/library/server/browse.py:257
msgid "Name" msgid "Name"
msgstr "" msgstr ""
@ -7748,7 +7749,7 @@ msgstr ""
msgid "Select the toolbars and/or menus to add <b>%s</b> to:" msgid "Select the toolbars and/or menus to add <b>%s</b> to:"
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/choose_plugin_toolbars.py:45 #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/choose_plugin_toolbars.py:48
msgid "You can also customise the plugin locations using <b>Preferences -> Customise the toolbar</b>" msgid "You can also customise the plugin locations using <b>Preferences -> Customise the toolbar</b>"
msgstr "" msgstr ""
@ -7935,7 +7936,7 @@ msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/edit_authors_dialog.py:122 #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/edit_authors_dialog.py:122
#: /home/kovid/work/calibre/src/calibre/gui2/lrf_renderer/main.py:160 #: /home/kovid/work/calibre/src/calibre/gui2/lrf_renderer/main.py:160
#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single_download.py:496 #: /home/kovid/work/calibre/src/calibre/gui2/metadata/single_download.py:496
#: /home/kovid/work/calibre/src/calibre/gui2/viewer/main.py:622 #: /home/kovid/work/calibre/src/calibre/gui2/viewer/main.py:624
msgid "No matches found" msgid "No matches found"
msgstr "" msgstr ""
@ -9112,62 +9113,62 @@ msgstr ""
msgid "You must provide a username and/or password to use this news source." msgid "You must provide a username and/or password to use this news source."
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler.py:355 #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler.py:357
msgid "Account" msgid "Account"
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler.py:356 #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler.py:358
msgid "(optional)" msgid "(optional)"
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler.py:357 #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler.py:359
msgid "(required)" msgid "(required)"
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler.py:374 #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler.py:376
msgid "Created by: " msgid "Created by: "
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler.py:378 #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler.py:380
#, python-format #, python-format
msgid "Download %s now" msgid "Download %s now"
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler.py:382 #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler.py:384
msgid "Last downloaded: never" msgid "Last downloaded: never"
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler.py:383 #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler.py:385
msgid "never" msgid "never"
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler.py:389 #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler.py:391
#, python-format #, python-format
msgid "%(days)d days, %(hours)d hours and %(mins)d minutes ago" msgid "%(days)d days, %(hours)d hours and %(mins)d minutes ago"
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler.py:405 #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler.py:407
msgid "Last downloaded:" msgid "Last downloaded:"
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler.py:426 #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler.py:428
msgid "Cannot download news as no internet connection is active" msgid "Cannot download news as no internet connection is active"
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler.py:429 #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler.py:431
msgid "No internet connection" msgid "No internet connection"
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler.py:440 #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler.py:442
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler_ui.py:203 #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler_ui.py:203
msgid "Schedule news download" msgid "Schedule news download"
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler.py:443 #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler.py:445
msgid "Add a custom news source" msgid "Add a custom news source"
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler.py:448 #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler.py:450
msgid "Download all scheduled news sources" msgid "Download all scheduled news sources"
msgstr "" msgstr ""
@ -10471,7 +10472,7 @@ msgstr ""
msgid "Restore default layout" msgid "Restore default layout"
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/library/views.py:944 #: /home/kovid/work/calibre/src/calibre/gui2/library/views.py:946
msgid "Dropping onto a device is not supported. First add the book to the calibre library." msgid "Dropping onto a device is not supported. First add the book to the calibre library."
msgstr "" msgstr ""
@ -10513,12 +10514,12 @@ msgid "LRF Viewer toolbar"
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/lrf_renderer/main_ui.py:131 #: /home/kovid/work/calibre/src/calibre/gui2/lrf_renderer/main_ui.py:131
#: /home/kovid/work/calibre/src/calibre/gui2/viewer/documentview.py:557 #: /home/kovid/work/calibre/src/calibre/gui2/viewer/documentview.py:516
msgid "Next Page" msgid "Next Page"
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/lrf_renderer/main_ui.py:132 #: /home/kovid/work/calibre/src/calibre/gui2/lrf_renderer/main_ui.py:132
#: /home/kovid/work/calibre/src/calibre/gui2/viewer/documentview.py:558 #: /home/kovid/work/calibre/src/calibre/gui2/viewer/documentview.py:517
msgid "Previous Page" msgid "Previous Page"
msgstr "" msgstr ""
@ -12123,7 +12124,7 @@ msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/preferences/look_feel.py:182 #: /home/kovid/work/calibre/src/calibre/gui2/preferences/look_feel.py:182
#: /home/kovid/work/calibre/src/calibre/gui2/shortcuts.py:132 #: /home/kovid/work/calibre/src/calibre/gui2/shortcuts.py:132
#: /home/kovid/work/calibre/src/calibre/gui2/shortcuts.py:223 #: /home/kovid/work/calibre/src/calibre/gui2/shortcuts.py:223
#: /home/kovid/work/calibre/src/calibre/gui2/viewer/main.py:272 #: /home/kovid/work/calibre/src/calibre/gui2/viewer/main.py:273
msgid " or " msgid " or "
msgstr "" msgstr ""
@ -14200,27 +14201,27 @@ msgstr ""
msgid "Install and configure user plugins" msgid "Install and configure user plugins"
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/viewer/bookmarkmanager.py:43 #: /home/kovid/work/calibre/src/calibre/gui2/viewer/bookmarkmanager.py:44
msgid "Edit bookmark" msgid "Edit bookmark"
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/viewer/bookmarkmanager.py:43 #: /home/kovid/work/calibre/src/calibre/gui2/viewer/bookmarkmanager.py:44
msgid "New title for bookmark:" msgid "New title for bookmark:"
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/viewer/bookmarkmanager.py:52 #: /home/kovid/work/calibre/src/calibre/gui2/viewer/bookmarkmanager.py:53
msgid "Export Bookmarks" msgid "Export Bookmarks"
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/viewer/bookmarkmanager.py:54 #: /home/kovid/work/calibre/src/calibre/gui2/viewer/bookmarkmanager.py:55
msgid "Saved Bookmarks (*.pickle)" msgid "Saved Bookmarks (*.pickle)"
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/viewer/bookmarkmanager.py:62 #: /home/kovid/work/calibre/src/calibre/gui2/viewer/bookmarkmanager.py:63
msgid "Import Bookmarks" msgid "Import Bookmarks"
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/viewer/bookmarkmanager.py:62 #: /home/kovid/work/calibre/src/calibre/gui2/viewer/bookmarkmanager.py:63
msgid "Pickled Bookmarks (*.pickle)" msgid "Pickled Bookmarks (*.pickle)"
msgstr "" msgstr ""
@ -14336,7 +14337,7 @@ msgid "Mouse &wheel flips pages"
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/viewer/config_ui.py:208 #: /home/kovid/work/calibre/src/calibre/gui2/viewer/config_ui.py:208
#: /home/kovid/work/calibre/src/calibre/gui2/viewer/documentview.py:50 #: /home/kovid/work/calibre/src/calibre/gui2/viewer/documentview.py:51
msgid "Set the maximum width that the book's text and pictures will take when in fullscreen mode. This allows you to read the book text without it becoming too wide." msgid "Set the maximum width that the book's text and pictures will take when in fullscreen mode. This allows you to read the book text without it becoming too wide."
msgstr "" msgstr ""
@ -14382,118 +14383,118 @@ msgstr ""
msgid "No results found for:" msgid "No results found for:"
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/viewer/documentview.py:39 #: /home/kovid/work/calibre/src/calibre/gui2/viewer/documentview.py:40
msgid "Options to customize the ebook viewer" msgid "Options to customize the ebook viewer"
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/viewer/documentview.py:46 #: /home/kovid/work/calibre/src/calibre/gui2/viewer/documentview.py:47
#: /home/kovid/work/calibre/src/calibre/gui2/viewer/main.py:929 #: /home/kovid/work/calibre/src/calibre/gui2/viewer/main.py:941
msgid "Remember last used window size" msgid "Remember last used window size"
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/viewer/documentview.py:48 #: /home/kovid/work/calibre/src/calibre/gui2/viewer/documentview.py:49
#: /home/kovid/work/calibre/src/calibre/gui2/viewer/documentview.py:105 #: /home/kovid/work/calibre/src/calibre/gui2/viewer/documentview.py:106
msgid "Set the user CSS stylesheet. This can be used to customize the look of all books." msgid "Set the user CSS stylesheet. This can be used to customize the look of all books."
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/viewer/documentview.py:54 #: /home/kovid/work/calibre/src/calibre/gui2/viewer/documentview.py:55
msgid "Resize images larger than the viewer window to fit inside it" msgid "Resize images larger than the viewer window to fit inside it"
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/viewer/documentview.py:55 #: /home/kovid/work/calibre/src/calibre/gui2/viewer/documentview.py:56
msgid "Hyphenate text" msgid "Hyphenate text"
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/viewer/documentview.py:57 #: /home/kovid/work/calibre/src/calibre/gui2/viewer/documentview.py:58
msgid "Default language for hyphenation rules" msgid "Default language for hyphenation rules"
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/viewer/documentview.py:59 #: /home/kovid/work/calibre/src/calibre/gui2/viewer/documentview.py:60
msgid "Save the current position in the document, when quitting" msgid "Save the current position in the document, when quitting"
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/viewer/documentview.py:61 #: /home/kovid/work/calibre/src/calibre/gui2/viewer/documentview.py:62
msgid "Have the mouse wheel turn pages" msgid "Have the mouse wheel turn pages"
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/viewer/documentview.py:63 #: /home/kovid/work/calibre/src/calibre/gui2/viewer/documentview.py:64
msgid "The time, in seconds, for the page flip animation. Default is half a second." msgid "The time, in seconds, for the page flip animation. Default is half a second."
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/viewer/documentview.py:66 #: /home/kovid/work/calibre/src/calibre/gui2/viewer/documentview.py:67
msgid "The amount by which to change the font size when clicking the font larger/smaller buttons. Should be a number between 0 and 1." msgid "The amount by which to change the font size when clicking the font larger/smaller buttons. Should be a number between 0 and 1."
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/viewer/documentview.py:70 #: /home/kovid/work/calibre/src/calibre/gui2/viewer/documentview.py:71
msgid "Font options" msgid "Font options"
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/viewer/documentview.py:72 #: /home/kovid/work/calibre/src/calibre/gui2/viewer/documentview.py:73
msgid "The serif font family" msgid "The serif font family"
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/viewer/documentview.py:74 #: /home/kovid/work/calibre/src/calibre/gui2/viewer/documentview.py:75
msgid "The sans-serif font family" msgid "The sans-serif font family"
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/viewer/documentview.py:76 #: /home/kovid/work/calibre/src/calibre/gui2/viewer/documentview.py:77
msgid "The monospaced font family" msgid "The monospaced font family"
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/viewer/documentview.py:77 #: /home/kovid/work/calibre/src/calibre/gui2/viewer/documentview.py:78
msgid "The standard font size in px" msgid "The standard font size in px"
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/viewer/documentview.py:78 #: /home/kovid/work/calibre/src/calibre/gui2/viewer/documentview.py:79
msgid "The monospaced font size in px" msgid "The monospaced font size in px"
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/viewer/documentview.py:79 #: /home/kovid/work/calibre/src/calibre/gui2/viewer/documentview.py:80
msgid "The standard font type" msgid "The standard font type"
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/viewer/documentview.py:134 #: /home/kovid/work/calibre/src/calibre/gui2/viewer/documentview.py:135
msgid "Still editing" msgid "Still editing"
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/viewer/documentview.py:135 #: /home/kovid/work/calibre/src/calibre/gui2/viewer/documentview.py:136
msgid "You are in the middle of editing a keyboard shortcut first complete that, by clicking outside the shortcut editing box." msgid "You are in the middle of editing a keyboard shortcut first complete that, by clicking outside the shortcut editing box."
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/viewer/documentview.py:526 #: /home/kovid/work/calibre/src/calibre/gui2/viewer/documentview.py:485
msgid "&Lookup in dictionary" msgid "&Lookup in dictionary"
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/viewer/documentview.py:531 #: /home/kovid/work/calibre/src/calibre/gui2/viewer/documentview.py:490
msgid "&Search for next occurrence" msgid "&Search for next occurrence"
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/viewer/documentview.py:536 #: /home/kovid/work/calibre/src/calibre/gui2/viewer/documentview.py:495
#: /home/kovid/work/calibre/src/calibre/gui2/viewer/main.py:146 #: /home/kovid/work/calibre/src/calibre/gui2/viewer/main.py:147
msgid "Go to..." msgid "Go to..."
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/viewer/documentview.py:548 #: /home/kovid/work/calibre/src/calibre/gui2/viewer/documentview.py:507
msgid "Next Section" msgid "Next Section"
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/viewer/documentview.py:549 #: /home/kovid/work/calibre/src/calibre/gui2/viewer/documentview.py:508
msgid "Previous Section" msgid "Previous Section"
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/viewer/documentview.py:551 #: /home/kovid/work/calibre/src/calibre/gui2/viewer/documentview.py:510
msgid "Document Start" msgid "Document Start"
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/viewer/documentview.py:552 #: /home/kovid/work/calibre/src/calibre/gui2/viewer/documentview.py:511
msgid "Document End" msgid "Document End"
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/viewer/documentview.py:554 #: /home/kovid/work/calibre/src/calibre/gui2/viewer/documentview.py:513
msgid "Section Start" msgid "Section Start"
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/viewer/documentview.py:555 #: /home/kovid/work/calibre/src/calibre/gui2/viewer/documentview.py:514
msgid "Section End" msgid "Section End"
msgstr "" msgstr ""
@ -14545,147 +14546,147 @@ msgstr ""
msgid "Scroll right" msgid "Scroll right"
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/viewer/main.py:116 #: /home/kovid/work/calibre/src/calibre/gui2/viewer/main.py:117
msgid "Book format" msgid "Book format"
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/viewer/main.py:134 #: /home/kovid/work/calibre/src/calibre/gui2/viewer/main.py:135
msgid "Position in book" msgid "Position in book"
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/viewer/main.py:211 #: /home/kovid/work/calibre/src/calibre/gui2/viewer/main.py:212
msgid "Go to a reference. To get reference numbers, use the reference mode." msgid "Go to a reference. To get reference numbers, use the reference mode."
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/viewer/main.py:219 #: /home/kovid/work/calibre/src/calibre/gui2/viewer/main.py:220
msgid "Search for text in book" msgid "Search for text in book"
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/viewer/main.py:271 #: /home/kovid/work/calibre/src/calibre/gui2/viewer/main.py:272
#, python-format #, python-format
msgid "Toggle full screen (%s)" msgid "Toggle full screen (%s)"
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/viewer/main.py:306 #: /home/kovid/work/calibre/src/calibre/gui2/viewer/main.py:307
msgid "Full screen mode" msgid "Full screen mode"
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/viewer/main.py:307 #: /home/kovid/work/calibre/src/calibre/gui2/viewer/main.py:308
msgid "Right click to show controls" msgid "Right click to show controls"
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/viewer/main.py:308 #: /home/kovid/work/calibre/src/calibre/gui2/viewer/main.py:309
msgid "Press Esc to quit" msgid "Press Esc to quit"
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/viewer/main.py:322 #: /home/kovid/work/calibre/src/calibre/gui2/viewer/main.py:323
msgid "Show/hide controls" msgid "Show/hide controls"
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/viewer/main.py:334 #: /home/kovid/work/calibre/src/calibre/gui2/viewer/main.py:335
msgid "Print Preview" msgid "Print Preview"
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/viewer/main.py:344 #: /home/kovid/work/calibre/src/calibre/gui2/viewer/main.py:345
msgid "Clear list of recently opened books" msgid "Clear list of recently opened books"
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/viewer/main.py:425 #: /home/kovid/work/calibre/src/calibre/gui2/viewer/main.py:426
#, python-format #, python-format
msgid "Connecting to dict.org to lookup: <b>%s</b>&hellip;" msgid "Connecting to dict.org to lookup: <b>%s</b>&hellip;"
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/viewer/main.py:531 #: /home/kovid/work/calibre/src/calibre/gui2/viewer/main.py:533
msgid "No such location" msgid "No such location"
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/viewer/main.py:532 #: /home/kovid/work/calibre/src/calibre/gui2/viewer/main.py:534
msgid "The location pointed to by this item does not exist." msgid "The location pointed to by this item does not exist."
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/viewer/main.py:582 #: /home/kovid/work/calibre/src/calibre/gui2/viewer/main.py:584
msgid "Choose ebook" msgid "Choose ebook"
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/viewer/main.py:583 #: /home/kovid/work/calibre/src/calibre/gui2/viewer/main.py:585
msgid "Ebooks" msgid "Ebooks"
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/viewer/main.py:603 #: /home/kovid/work/calibre/src/calibre/gui2/viewer/main.py:605
#, python-format #, python-format
msgid "" msgid ""
"Make font size %(which)s\n" "Make font size %(which)s\n"
"Current magnification: %(mag).1f" "Current magnification: %(mag).1f"
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/viewer/main.py:605 #: /home/kovid/work/calibre/src/calibre/gui2/viewer/main.py:607
msgid "larger" msgid "larger"
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/viewer/main.py:607 #: /home/kovid/work/calibre/src/calibre/gui2/viewer/main.py:609
msgid "smaller" msgid "smaller"
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/viewer/main.py:623 #: /home/kovid/work/calibre/src/calibre/gui2/viewer/main.py:625
#, python-format #, python-format
msgid "No matches found for: %s" msgid "No matches found for: %s"
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/viewer/main.py:660 #: /home/kovid/work/calibre/src/calibre/gui2/viewer/main.py:662
msgid "Loading flow..." msgid "Loading flow..."
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/viewer/main.py:698 #: /home/kovid/work/calibre/src/calibre/gui2/viewer/main.py:700
#, python-format #, python-format
msgid "Laying out %s" msgid "Laying out %s"
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/viewer/main.py:741 #: /home/kovid/work/calibre/src/calibre/gui2/viewer/main.py:751
#, python-format #, python-format
msgid "Bookmark #%d" msgid "Bookmark #%d"
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/viewer/main.py:745 #: /home/kovid/work/calibre/src/calibre/gui2/viewer/main.py:755
msgid "Add bookmark" msgid "Add bookmark"
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/viewer/main.py:746 #: /home/kovid/work/calibre/src/calibre/gui2/viewer/main.py:756
msgid "Enter title for bookmark:" msgid "Enter title for bookmark:"
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/viewer/main.py:756 #: /home/kovid/work/calibre/src/calibre/gui2/viewer/main.py:767
msgid "Manage Bookmarks" msgid "Manage Bookmarks"
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/viewer/main.py:797 #: /home/kovid/work/calibre/src/calibre/gui2/viewer/main.py:809
msgid "Loading ebook..." msgid "Loading ebook..."
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/viewer/main.py:809 #: /home/kovid/work/calibre/src/calibre/gui2/viewer/main.py:821
msgid "Could not open ebook" msgid "Could not open ebook"
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/viewer/main.py:916 #: /home/kovid/work/calibre/src/calibre/gui2/viewer/main.py:928
msgid "Options to control the ebook viewer" msgid "Options to control the ebook viewer"
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/viewer/main.py:923 #: /home/kovid/work/calibre/src/calibre/gui2/viewer/main.py:935
msgid "If specified, viewer window will try to come to the front when started." msgid "If specified, viewer window will try to come to the front when started."
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/viewer/main.py:926 #: /home/kovid/work/calibre/src/calibre/gui2/viewer/main.py:938
msgid "If specified, viewer window will try to open full screen when started." msgid "If specified, viewer window will try to open full screen when started."
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/viewer/main.py:931 #: /home/kovid/work/calibre/src/calibre/gui2/viewer/main.py:943
msgid "Print javascript alert and console messages to the console" msgid "Print javascript alert and console messages to the console"
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/viewer/main.py:933 #: /home/kovid/work/calibre/src/calibre/gui2/viewer/main.py:945
msgid "The position at which to open the specified book. The position is a location as displayed in the top left corner of the viewer." msgid "The position at which to open the specified book. The position is a location as displayed in the top left corner of the viewer."
msgstr "" msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/viewer/main.py:940 #: /home/kovid/work/calibre/src/calibre/gui2/viewer/main.py:952
msgid "" msgid ""
"%prog [options] file\n" "%prog [options] file\n"
"\n" "\n"

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

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