diff --git a/manual/faq.rst b/manual/faq.rst index 24774c8c7d..2d2862e4e6 100644 --- a/manual/faq.rst +++ b/manual/faq.rst @@ -616,7 +616,10 @@ or a Remote Desktop solution. If you must share the actual library, use a file syncing tool like DropBox or rsync or Microsoft SkyDrive instead of a networked drive. Even with these tools there is danger of data corruption/loss, so only do this if you are -willing to live with that risk. +willing to live with that risk. In particular, be aware that **Google Drive** +is incompatible with |app|, if you put your |app| library in Google Drive, you +*will* suffer data loss. See +`this thread `_ for details. Content From The Web --------------------- diff --git a/recipes/democracy_journal.recipe b/recipes/democracy_journal.recipe new file mode 100644 index 0000000000..f02a3b70a8 --- /dev/null +++ b/recipes/democracy_journal.recipe @@ -0,0 +1,27 @@ +from calibre.web.feeds.news import BasicNewsRecipe +import re + +class AdvancedUserRecipe1361743898(BasicNewsRecipe): + title = u'Democracy Journal' + description = '''A journal of ideas. Published quarterly.''' + __author__ = u'David Nye' + language = 'en' + oldest_article = 90 + max_articles_per_feed = 30 + no_stylesheets = True + auto_cleanup = True + + def parse_index(self): + articles = [] + feeds = [] + soup = self.index_to_soup("http://www.democracyjournal.org") + for x in soup.findAll(href=re.compile("http://www\.democracyjournal\.org/\d*/.*php$")): + url = x.get('href') + title = self.tag_to_string(x) + articles.append({'title':title, 'url':url, 'description':'', 'date':''}) + feeds.append(('Articles', articles)) + return feeds + + def print_version(self, url): + return url + '?page=all' + diff --git a/recipes/el_malpensante.recipe b/recipes/el_malpensante.recipe new file mode 100644 index 0000000000..7a014735b6 --- /dev/null +++ b/recipes/el_malpensante.recipe @@ -0,0 +1,27 @@ +# coding=utf-8 +# https://github.com/iemejia/calibrecolombia + +''' +http://www.elmalpensante.com/ +''' + +from calibre.web.feeds.news import BasicNewsRecipe + +class ElMalpensante(BasicNewsRecipe): + title = u'El Malpensante' + language = 'es_CO' + __author__ = 'Ismael Mejia ' + cover_url = 'http://elmalpensante.com/img/layout/logo.gif' + description = 'El Malpensante' + oldest_article = 30 + simultaneous_downloads = 20 + #tags = 'news, sport, blog' + use_embedded_content = True + remove_empty_feeds = True + max_articles_per_feed = 100 + feeds = [(u'Artículos', u'http://www.elmalpensante.com/articulosRSS.php'), + (u'Malpensantías', u'http://www.elmalpensante.com/malpensantiasRSS.php'), + (u'Margaritas', u'http://www.elmalpensante.com/margaritasRSS.php'), +# This one is almost the same as articulos so we leave articles +# (u'Noticias', u'http://www.elmalpensante.com/noticiasRSS.php'), + ] diff --git a/recipes/geopolityka.recipe b/recipes/geopolityka.recipe new file mode 100644 index 0000000000..9749007479 --- /dev/null +++ b/recipes/geopolityka.recipe @@ -0,0 +1,12 @@ +from calibre.web.feeds.news import BasicNewsRecipe + +class BasicUserRecipe1361379046(BasicNewsRecipe): + title = u'Geopolityka.org' + language = 'pl' + __author__ = 'chemik111' + oldest_article = 15 + max_articles_per_feed = 100 + auto_cleanup = True + + feeds = [(u'Rss', u'http://geopolityka.org/index.php?format=feed&type=rss')] + diff --git a/recipes/hnonline.recipe b/recipes/hnonline.recipe new file mode 100644 index 0000000000..d9faafd0f1 --- /dev/null +++ b/recipes/hnonline.recipe @@ -0,0 +1,68 @@ +from calibre.web.feeds.news import BasicNewsRecipe +import re + +class HNonlineRecipe(BasicNewsRecipe): + __license__ = 'GPL v3' + __author__ = 'lacike' + language = 'sk' + version = 1 + + title = u'HNonline' + publisher = u'HNonline' + category = u'News, Newspaper' + description = u'News from Slovakia' + cover_url = u'http://hnonline.sk/img/sk/_relaunch/logo2.png' + + oldest_article = 1 + max_articles_per_feed = 100 + use_embedded_content = False + remove_empty_feeds = True + + no_stylesheets = True + remove_javascript = True + + # Feeds from: http://rss.hnonline.sk, for listing see http://rss.hnonline.sk/prehlad + feeds = [] + feeds.append((u'HNonline|Ekonomika a firmy', u'http://rss.hnonline.sk/?p=kC1000')) + feeds.append((u'HNonline|Slovensko', u'http://rss.hnonline.sk/?p=kC2000')) + feeds.append((u'HNonline|Svet', u'http://rss.hnonline.sk/?p=kC3000')) + feeds.append((u'HNonline|\u0160port', u'http://rss.hnonline.sk/?p=kC4000')) + feeds.append((u'HNonline|Online rozhovor', u'http://rss.hnonline.sk/?p=kCR000')) + + feeds.append((u'FinWeb|Spr\u00E1vy zo sveta financi\u00ED', u'http://rss.finweb.hnonline.sk/spravodajstvo')) + feeds.append((u'FinWeb|Koment\u00E1re a anal\u00FDzy', u'http://rss.finweb.hnonline.sk/?p=kPC200')) + feeds.append((u'FinWeb|Invest\u00EDcie', u'http://rss.finweb.hnonline.sk/?p=kPC300')) + feeds.append((u'FinWeb|Svet akci\u00ED', u'http://rss.finweb.hnonline.sk/?p=kPC400')) + feeds.append((u'FinWeb|Rozhovory', u'http://rss.finweb.hnonline.sk/?p=kPC500')) + feeds.append((u'FinWeb|T\u00E9ma t\u00FD\u017Ed\u0148a', u'http://rss.finweb.hnonline.sk/?p=kPC600')) + feeds.append((u'FinWeb|Rebr\u00ED\u010Dky', u'http://rss.finweb.hnonline.sk/?p=kPC700')) + + feeds.append((u'HNstyle|Kult\u00FAra', u'http://style.hnonline.sk/?p=kTC100')) + feeds.append((u'HNstyle|Auto-moto', u'http://style.hnonline.sk/?p=kTC200')) + feeds.append((u'HNstyle|Digit\u00E1l', u'http://style.hnonline.sk/?p=kTC300')) + feeds.append((u'HNstyle|Veda', u'http://style.hnonline.sk/?p=kTCV00')) + feeds.append((u'HNstyle|Dizajn', u'http://style.hnonline.sk/?p=kTC400')) + feeds.append((u'HNstyle|Cestovanie', u'http://style.hnonline.sk/?p=kTCc00')) + feeds.append((u'HNstyle|V\u00EDkend', u'http://style.hnonline.sk/?p=kTC800')) + feeds.append((u'HNstyle|Gastro', u'http://style.hnonline.sk/?p=kTC600')) + feeds.append((u'HNstyle|M\u00F3da', u'http://style.hnonline.sk/?p=kTC700')) + feeds.append((u'HNstyle|Modern\u00E1 \u017Eena', u'http://style.hnonline.sk/?p=kTCA00')) + feeds.append((u'HNstyle|Pre\u010Do nie?!', u'http://style.hnonline.sk/?p=k7C000')) + + keep_only_tags = [] + keep_only_tags.append(dict(name = 'h1', attrs = {'class': 'detail-titulek'})) + keep_only_tags.append(dict(name = 'div', attrs = {'class': 'detail-podtitulek'})) + keep_only_tags.append(dict(name = 'div', attrs = {'class': 'detail-perex'})) + keep_only_tags.append(dict(name = 'div', attrs = {'class': 'detail-text'})) + + remove_tags = [] + #remove_tags.append(dict(name = 'div', attrs = {'id': re.compile('smeplayer.*')})) + + remove_tags_after = [] + #remove_tags_after = [dict(name = 'p', attrs = {'class': 'autor_line'})] + + extra_css = ''' + @font-face {font-family: "serif1";src:url(res:///opt/sony/ebook/FONT/tt0011m_.ttf)} + @font-face {font-family: "sans1";src:url(res:///opt/sony/ebook/FONT/LiberationSans.ttf)} + body {font-family: sans1, serif1;} + ''' \ No newline at end of file diff --git a/recipes/icons/hnonline.png b/recipes/icons/hnonline.png new file mode 100644 index 0000000000..1e073839ad Binary files /dev/null and b/recipes/icons/hnonline.png differ diff --git a/recipes/icons/nezavisne_novine.png b/recipes/icons/nezavisne_novine.png new file mode 100644 index 0000000000..29da3de24f Binary files /dev/null and b/recipes/icons/nezavisne_novine.png differ diff --git a/recipes/nezavisne_novine.recipe b/recipes/nezavisne_novine.recipe new file mode 100644 index 0000000000..357c478ce1 --- /dev/null +++ b/recipes/nezavisne_novine.recipe @@ -0,0 +1,59 @@ +__license__ = 'GPL v3' +__copyright__ = '2013, Darko Miletic ' +''' +www.nezavisne.com +''' +from calibre import strftime +from calibre.web.feeds.news import BasicNewsRecipe + +class NezavisneNovine(BasicNewsRecipe): + title = 'Nezavisne novine' + __author__ = 'Darko Miletic' + description = 'Nezavisne novine - Najnovije vijesti iz BiH, Srbije, Hrvatske, Crne Gore i svijeta' + publisher = 'NIGP "DNN"' + category = 'news, politics, Bosnia, Balcans' + oldest_article = 2 + max_articles_per_feed = 200 + no_stylesheets = True + encoding = 'utf8' + use_embedded_content = False + language = 'sr' + remove_empty_feeds = True + publication_type = 'newspaper' + cover_url = strftime('http://pdf.nezavisne.com/slika/novina/nezavisne_novine.jpg?v=%Y%m%d') + masthead_url = 'http://www.nezavisne.com/slika/osnova/nezavisne-novine-logo.gif' + extra_css = """ + body{font-family: Arial,Helvetica,sans-serif } + img{margin-bottom: 0.4em; display:block} + """ + + conversion_options = { + 'comment' : description + , 'tags' : category + , 'publisher' : publisher + , 'language' : language + } + keep_only_tags = [dict(name='div', attrs={'class':'vijest'})] + remove_tags_after = dict(name='div', attrs={'id':'wrap'}) + remove_tags = [ + dict(name=['meta','link','iframe','object']) + ,dict(name='div', attrs={'id':'wrap'}) + ] + remove_attributes=['lang','xmlns:fb','xmlns:og'] + + + feeds = [ + (u'Novosti' , u'http://feeds.feedburner.com/Novosti-NezavisneNovine' ) + ,(u'Posao' , u'http://feeds.feedburner.com/Posao-NezavisneNovine' ) + ,(u'Sport' , u'http://feeds.feedburner.com/Sport-NezavisneNovine' ) + ,(u'Komentar' , u'http://feeds.feedburner.com/Komentari-NezavisneNovine' ) + ,(u'Umjetnost i zabava' , u'http://feeds.feedburner.com/UmjetnostIZabava-NezavisneNovine' ) + ,(u'Život i stil' , u'http://feeds.feedburner.com/ZivotIStil-NezavisneNovine' ) + ,(u'Auto' , u'http://feeds.feedburner.com/Auto-NezavisneNovine' ) + ,(u'Nauka i tehnologija', u'http://feeds.feedburner.com/NaukaITehnologija-NezavisneNovine') + ] + + def preprocess_html(self, soup): + for item in soup.findAll(style=True): + del item['style'] + return soup diff --git a/recipes/revista_cromos.recipe b/recipes/revista_cromos.recipe new file mode 100644 index 0000000000..29515971dd --- /dev/null +++ b/recipes/revista_cromos.recipe @@ -0,0 +1,33 @@ +# coding=utf-8 +# https://github.com/iemejia/calibrecolombia + +''' +http://www.cromos.com.co/ +''' + +from calibre.web.feeds.news import BasicNewsRecipe + +class ElMalpensante(BasicNewsRecipe): + title = u'Revista Cromos' + language = 'es_CO' + __author__ = 'Ismael Mejia ' + cover_url = 'http://www.cromos.com.co/sites/cromos.com.co/themes/cromos_theme/images/logo_morado.gif' + description = 'Revista Cromos' + oldest_article = 7 + simultaneous_downloads = 20 + #tags = 'news, sport, blog' + use_embedded_content = True + remove_empty_feeds = True + max_articles_per_feed = 100 + feeds = [(u'Cromos', u'http://www.cromos.com.co/rss.xml'), + (u'Moda', u'http://www.cromos.com.co/moda/feed'), + (u'Estilo de Vida', u'http://www.cromos.com.co/estilo-de-vida/feed'), + (u'Cuidado Personal', u'http://www.cromos.com.co/estilo-de-vida/cuidado-personal/feed'), + (u'Salud y Alimentación', u'http://www.cromos.com.co/estilo-de-vida/salud-y-alimentacion/feed'), + (u'Personajes', u'http://www.cromos.com.co/personajes/feed'), + (u'Actualidad', u'http://www.cromos.com.co/personajes/actualidad/feed'), + (u'Espectáculo', u'http://www.cromos.com.co/personajes/espectaculo/feed'), + (u'Reportajes', u'http://www.cromos.com.co/reportajes/feed'), + (u'Eventos', u'http://www.cromos.com.co/eventos/feed'), + (u'Modelos', u'http://www.cromos.com.co/modelos/feed'), + ] diff --git a/recipes/science_news.recipe b/recipes/science_news.recipe index fa24bbadcf..53b451030a 100644 --- a/recipes/science_news.recipe +++ b/recipes/science_news.recipe @@ -1,24 +1,38 @@ #!/usr/bin/env python __license__ = 'GPL v3' -__copyright__ = '2008, Darko Miletic ' ''' sciencenews.org ''' from calibre.web.feeds.news import BasicNewsRecipe -class Sciencenews(BasicNewsRecipe): - title = u'ScienceNews' - __author__ = u'Darko Miletic and Sujata Raman' - description = u"Science News is an award-winning weekly newsmagazine covering the most important research in all fields of science. Its 16 pages each week are packed with short, accurate articles that appeal to both general readers and scientists. Published since 1922, the magazine now reaches about 150,000 subscribers and more than 1 million readers. These are the latest News Items from Science News." +class ScienceNewsIssue(BasicNewsRecipe): + title = u'Science News Recent Issues' + __author__ = u'Darko Miletic, Sujata Raman and Starson17' + description = u'''Science News is an award-winning weekly + newsmagazine covering the most important research in all fields of science. + Its 16 pages each week are packed with short, accurate articles that appeal + to both general readers and scientists. Published since 1922, the magazine + now reaches about 150,000 subscribers and more than 1 million readers. + These are the latest News Items from Science News. This recipe downloads + the last 30 days worth of articles.''' + category = u'Science, Technology, News' + publisher = u'Society for Science & the Public' oldest_article = 30 language = 'en' - max_articles_per_feed = 100 no_stylesheets = True use_embedded_content = False - auto_cleanup = True timefmt = ' [%A, %d %B, %Y]' + recursions = 1 + remove_attributes = ['style'] + + conversion_options = {'linearize_tables' : True + , 'comment' : description + , 'tags' : category + , 'publisher' : publisher + , 'language' : language + } extra_css = ''' .content_description{font-family:georgia ;font-size:x-large; color:#646464 ; font-weight:bold;} @@ -27,36 +41,33 @@ class Sciencenews(BasicNewsRecipe): .content_edition{font-family:helvetica,arial ;font-size: xx-small ;} .exclusive{color:#FF0000 ;} .anonymous{color:#14487E ;} - .content_content{font-family:helvetica,arial ;font-size: x-small ; color:#000000;} - .description{color:#585858;font-family:helvetica,arial ;font-size: xx-small ;} + .content_content{font-family:helvetica,arial ;font-size: medium ; color:#000000;} + .description{color:#585858;font-family:helvetica,arial ;font-size: large ;} .credit{color:#A6A6A6;font-family:helvetica,arial ;font-size: xx-small ;} ''' - #keep_only_tags = [ dict(name='div', attrs={'id':'column_action'}) ] - #remove_tags_after = dict(name='ul', attrs={'id':'content_functions_bottom'}) - #remove_tags = [ - #dict(name='ul', attrs={'id':'content_functions_bottom'}) - #,dict(name='div', attrs={'id':['content_functions_top','breadcrumb_content']}) - #,dict(name='img', attrs={'class':'icon'}) - #,dict(name='div', attrs={'class': 'embiggen'}) - #] + keep_only_tags = [ dict(name='div', attrs={'class':'content_content'}), + dict(name='ul', attrs={'id':'toc'}) + ] - feeds = [(u"Science News / News Items", u'http://sciencenews.org/index.php/feed/type/news/name/news.rss/view/feed/name/all.rss')] + feeds = [(u"Science News Current Issues", u'http://www.sciencenews.org/view/feed/type/edition/name/issues.rss')] + + match_regexps = [ + r'www.sciencenews.org/view/feature/id/', + r'www.sciencenews.org/view/generic/id' + ] def get_cover_url(self): cover_url = None index = 'http://www.sciencenews.org/view/home' soup = self.index_to_soup(index) link_item = soup.find(name = 'img',alt = "issue") - print link_item if link_item: cover_url = 'http://www.sciencenews.org' + link_item['src'] + '.jpg' return cover_url - #def preprocess_html(self, soup): - - #for tag in soup.findAll(name=['span']): - #tag.name = 'div' - - #return soup + def preprocess_html(self, soup): + for tag in soup.findAll(name=['span']): + tag.name = 'div' + return soup diff --git a/recipes/unperiodico.recipe b/recipes/unperiodico.recipe new file mode 100644 index 0000000000..d4edb4e5dc --- /dev/null +++ b/recipes/unperiodico.recipe @@ -0,0 +1,22 @@ +# -*- coding: utf-8 -*- +# https://github.com/iemejia/calibrecolombia + +''' +http://www.unperiodico.unal.edu.co/ +''' + +from calibre import strftime +from calibre.web.feeds.news import BasicNewsRecipe + +class UNPeriodico(BasicNewsRecipe): + title = u'UN Periodico' + language = 'es_CO' + __author__ = 'Ismael Mejia ' + cover_url = 'http://www.unperiodico.unal.edu.co/fileadmin/templates/periodico/img/logoperiodico.png' + description = 'UN Periodico' + oldest_article = 30 + max_articles_per_feed = 100 + publication_type = 'newspaper' + feeds = [ + (u'UNPeriodico', u'http://www.unperiodico.unal.edu.co/rss/type/rss2/') + ] diff --git a/recipes/wsj.recipe b/recipes/wsj.recipe index f4254ee7cc..a6a7aa634d 100644 --- a/recipes/wsj.recipe +++ b/recipes/wsj.recipe @@ -55,20 +55,14 @@ class WallStreetJournal(BasicNewsRecipe): ] remove_tags_after = [dict(id="article_story_body"), {'class':"article story"},] + use_javascript_to_login = True - def get_browser(self): - br = BasicNewsRecipe.get_browser(self) - if self.username is not None and self.password is not None: - br.open('http://commerce.wsj.com/auth/login') - br.select_form(nr=1) - br['user'] = self.username - br['password'] = self.password - res = br.submit() - raw = res.read() - if 'Welcome,' not in raw and '>Logout<' not in raw and '>Log Out<' not in raw: - raise ValueError('Failed to log in to wsj.com, check your ' - 'username and password') - return br + def javascript_login(self, br, username, password): + br.visit('https://id.wsj.com/access/pages/wsj/us/login_standalone.html?mg=com-wsj', timeout=120) + f = br.select_form(nr=0) + f['username'] = username + f['password'] = password + br.submit(timeout=120) def populate_article_metadata(self, article, soup, first): if first and hasattr(self, 'add_toc_thumbnail'): diff --git a/recipes/zeitde_sub.recipe b/recipes/zeitde_sub.recipe index b22e9793ed..3efcb610a3 100644 --- a/recipes/zeitde_sub.recipe +++ b/recipes/zeitde_sub.recipe @@ -88,7 +88,7 @@ class ZeitEPUBAbo(BasicNewsRecipe): (re.compile(u' \u00AB'), lambda match: u'\u00AB '), # before closing quotation (re.compile(u'\u00BB '), lambda match: u' \u00BB'), # after opening quotation # filtering for spaces in large numbers for better readability - (re.compile(r'(?<=\d\d)(?=\d\d\d[ ,\.;\)<\?!-])'), lambda match: u'\u2008'), # end of the number with some character following + (re.compile(r'(?<=\d\d)(?=\d\d\d[ ,;\)<\?!-])'), lambda match: u'\u2008'), # end of the number with some character following (re.compile(r'(?<=\d\d)(?=\d\d\d. )'), lambda match: u'\u2008'), # end of the number with full-stop following, then space is necessary (avoid file names) (re.compile(u'(?<=\d)(?=\d\d\d\u2008)'), lambda match: u'\u2008'), # next level (re.compile(u'(?<=\d)(?=\d\d\d\u2008)'), lambda match: u'\u2008'), # next level diff --git a/resources/compiled_coffeescript.zip b/resources/compiled_coffeescript.zip index 7edbd43dc9..c9ed508913 100644 Binary files a/resources/compiled_coffeescript.zip and b/resources/compiled_coffeescript.zip differ diff --git a/resources/default_tweaks.py b/resources/default_tweaks.py index 1aa820819c..9f6297ac79 100644 --- a/resources/default_tweaks.py +++ b/resources/default_tweaks.py @@ -517,3 +517,10 @@ default_tweak_format = None # your library and your personal editing style. preselect_first_completion = False +#: Recognize numbers inside text when sorting +# This means that when sorting on text fields like title the text "Book 2" +# will sort before the text "Book 100". If you want this behavior, set +# numeric_collation = True note that doing so will cause problems with text +# that starts with numbers and is a little slower. +numeric_collation = False + diff --git a/src/calibre/db/backend.py b/src/calibre/db/backend.py index c70c656f78..61ee2e3a18 100644 --- a/src/calibre/db/backend.py +++ b/src/calibre/db/backend.py @@ -20,7 +20,7 @@ from calibre.ptempfile import PersistentTemporaryFile from calibre.db.schema_upgrades import SchemaUpgrade from calibre.library.field_metadata import FieldMetadata from calibre.ebooks.metadata import title_sort, author_to_author_sort -from calibre.utils.icu import strcmp +from calibre.utils.icu import sort_key from calibre.utils.config import to_json, from_json, prefs, tweaks from calibre.utils.date import utcfromtimestamp, parse_date from calibre.utils.filenames import (is_case_sensitive, samefile, hardlink_file) @@ -172,7 +172,9 @@ def _author_to_author_sort(x): return author_to_author_sort(x.replace('|', ',')) def icu_collator(s1, s2): - return strcmp(force_unicode(s1, 'utf-8'), force_unicode(s2, 'utf-8')) + return cmp(sort_key(force_unicode(s1, 'utf-8')), + sort_key(force_unicode(s2, 'utf-8'))) + # }}} # Unused aggregators {{{ diff --git a/src/calibre/db/cache.py b/src/calibre/db/cache.py index 236d25c3e9..e28f32c0f8 100644 --- a/src/calibre/db/cache.py +++ b/src/calibre/db/cache.py @@ -211,6 +211,12 @@ class Cache(object): self.fields['ondevice'] = create_field('ondevice', VirtualTable('ondevice')) + for name, field in self.fields.iteritems(): + if name[0] == '#' and name.endswith('_index'): + field.series_field = self.fields[name[:-len('_index')]] + elif name == 'series_index': + field.series_field = self.fields['series'] + @read_api def field_for(self, name, book_id, default_value=None): ''' @@ -609,11 +615,11 @@ class Cache(object): icon_map=icon_map) @write_api - def set_field(self, name, book_id_to_val_map): + def set_field(self, name, book_id_to_val_map, allow_case_change=True): # TODO: Specialize title/authors to also update path # TODO: Handle updating caches used by composite fields dirtied = self.fields[name].writer.set_books( - book_id_to_val_map, self.backend) + book_id_to_val_map, self.backend, allow_case_change=allow_case_change) return dirtied # }}} diff --git a/src/calibre/db/fields.py b/src/calibre/db/fields.py index 46386b9ba5..dee312dbcd 100644 --- a/src/calibre/db/fields.py +++ b/src/calibre/db/fields.py @@ -46,6 +46,7 @@ class Field(object): elif name == 'languages': self.category_formatter = calibre_langcode_to_name self.writer = Writer(self) + self.series_field = None @property def metadata(self): diff --git a/src/calibre/db/tests/writing.py b/src/calibre/db/tests/writing.py index 7b7e815587..314d6fbb7a 100644 --- a/src/calibre/db/tests/writing.py +++ b/src/calibre/db/tests/writing.py @@ -22,7 +22,11 @@ class WritingTest(BaseTest): def create_getter(self, name, getter=None): if getter is None: - ans = lambda db:partial(db.get_custom, label=name[1:], + if name.endswith('_index'): + ans = lambda db:partial(db.get_custom_extra, index_is_id=True, + label=name[1:].replace('_index', '')) + else: + ans = lambda db:partial(db.get_custom, label=name[1:], index_is_id=True) else: ans = lambda db:partial(getattr(db, getter), index_is_id=True) @@ -41,11 +45,11 @@ class WritingTest(BaseTest): self.create_setter(name, setter)) def run_tests(self, tests): - cl = self.cloned_library results = {} for test in tests: results[test] = [] for val in test.vals: + cl = self.cloned_library cache = self.init_cache(cl) cache.set_field(test.name, {1: val}) cached_res = cache.field_for(test.name, 1) @@ -53,23 +57,35 @@ class WritingTest(BaseTest): db = self.init_old(cl) getter = test.getter(db) sqlite_res = getter(1) - test.setter(db)(1, val) - old_cached_res = getter(1) - self.assertEqual(old_cached_res, cached_res, - 'Failed setting for %s with value %r, cached value not the same. Old: %r != New: %r'%( - test.name, val, old_cached_res, cached_res)) - db.refresh() - old_sqlite_res = getter(1) - self.assertEqual(old_sqlite_res, sqlite_res, - 'Failed setting for %s, sqlite value not the same: %r != %r'%( - test.name, old_sqlite_res, sqlite_res)) + if test.name.endswith('_index'): + val = float(val) if val is not None else 1.0 + self.assertEqual(sqlite_res, val, + 'Failed setting for %s with value %r, sqlite value not the same. val: %r != sqlite_val: %r'%( + test.name, val, val, sqlite_res)) + else: + test.setter(db)(1, val) + old_cached_res = getter(1) + self.assertEqual(old_cached_res, cached_res, + 'Failed setting for %s with value %r, cached value not the same. Old: %r != New: %r'%( + test.name, val, old_cached_res, cached_res)) + db.refresh() + old_sqlite_res = getter(1) + self.assertEqual(old_sqlite_res, sqlite_res, + 'Failed setting for %s, sqlite value not the same: %r != %r'%( + test.name, old_sqlite_res, sqlite_res)) del db - - def test_one_one(self): 'Test setting of values in one-one fields' - tests = [] + tests = [self.create_test('#yesno', (True, False, 'true', 'false', None))] + for name, getter, setter in ( + ('#series_index', None, None), + ('series_index', 'series_index', 'set_series_index'), + ('#float', None, None), + ): + vals = ['1.5', None, 0, 1.0] + tests.append(self.create_test(name, tuple(vals), getter, setter)) + for name, getter, setter in ( ('pubdate', 'pubdate', 'set_pubdate'), ('timestamp', 'timestamp', 'set_timestamp'), @@ -78,6 +94,25 @@ class WritingTest(BaseTest): tests.append(self.create_test( name, ('2011-1-12', UNDEFINED_DATE, None), getter, setter)) + for name, getter, setter in ( + ('title', 'title', 'set_title'), + ('uuid', 'uuid', 'set_uuid'), + ('author_sort', 'author_sort', 'set_author_sort'), + ('sort', 'title_sort', 'set_title_sort'), + ('#comments', None, None), + ('comments', 'comments', 'set_comment'), + ): + vals = ['something', None] + if name not in {'comments', '#comments'}: + # Setting text column to '' returns None in the new backend + # and '' in the old. I think None is more correct. + vals.append('') + if name == 'comments': + # Again new behavior of deleting comment rather than setting + # empty string is more correct. + vals.remove(None) + tests.append(self.create_test(name, tuple(vals), getter, setter)) + self.run_tests(tests) def tests(): diff --git a/src/calibre/db/write.py b/src/calibre/db/write.py index 14ddea8dfb..5b3fc73f0c 100644 --- a/src/calibre/db/write.py +++ b/src/calibre/db/write.py @@ -13,11 +13,13 @@ from datetime import datetime from calibre.constants import preferred_encoding, ispy3 from calibre.utils.date import (parse_only_date, parse_date, UNDEFINED_DATE, isoformat) +if ispy3: + unicode = str # Convert data into values suitable for the db {{{ -if ispy3: - unicode = str +def sqlite_datetime(x): + return isoformat(x, sep=' ') if isinstance(x, datetime) else x def single_text(x): if x is None: @@ -98,17 +100,19 @@ def get_adapter(name, metadata): if name == 'title': return lambda x: ans(x) or _('Unknown') + if name == 'author_sort': + return lambda x: ans(x) or '' if name == 'authors': return lambda x: ans(x) or (_('Unknown'),) if name in {'timestamp', 'last_modified'}: return lambda x: ans(x) or UNDEFINED_DATE + if name == 'series_index': + return lambda x: 1.0 if ans(x) is None else ans(x) return ans # }}} -def sqlite_datetime(x): - return isoformat(x, sep=' ') if isinstance(x, datetime) else x - +# One-One fields {{{ def one_one_in_books(book_id_val_map, db, field, *args): 'Set a one-one field in the books table' if book_id_val_map: @@ -134,6 +138,22 @@ def one_one_in_other(book_id_val_map, db, field, *args): field.table.book_col_map.update(updated) return set(book_id_val_map) +def custom_series_index(book_id_val_map, db, field, *args): + series_field = field.series_field + sequence = [] + for book_id, sidx in book_id_val_map.iteritems(): + if sidx is None: + sidx = 1.0 + ids = series_field.ids_for_book(book_id) + if ids: + sequence.append((sidx, book_id, ids[0])) + field.table.book_col_map[book_id] = sidx + if sequence: + db.conn.executemany('UPDATE %s SET %s=? WHERE book=? AND value=?'%( + field.metadata['table'], field.metadata['column']), sequence) + return {s[0] for s in sequence} +# }}} + def dummy(book_id_val_map, *args): return set() @@ -148,16 +168,19 @@ class Writer(object): if dt == 'composite' or field.name in { 'id', 'cover', 'size', 'path', 'formats', 'news'}: self.set_books_func = dummy + elif self.name[0] == '#' and self.name.endswith('_index'): + self.set_books_func = custom_series_index elif field.is_many: # TODO: Implement this pass + # TODO: Remember to change commas to | when writing authors to sqlite else: self.set_books_func = (one_one_in_books if field.metadata['table'] == 'books' else one_one_in_other) - if self.name in {'timestamp', 'uuid'}: + if self.name in {'timestamp', 'uuid', 'sort'}: self.accept_vals = bool - def set_books(self, book_id_val_map, db): + def set_books(self, book_id_val_map, db, allow_case_change=True): book_id_val_map = {k:self.adapter(v) for k, v in book_id_val_map.iteritems() if self.accept_vals(v)} if not book_id_val_map: diff --git a/src/calibre/devices/iriver/driver.py b/src/calibre/devices/iriver/driver.py index 7479c10866..1e3a4e1e37 100644 --- a/src/calibre/devices/iriver/driver.py +++ b/src/calibre/devices/iriver/driver.py @@ -22,13 +22,14 @@ class IRIVER_STORY(USBMS): FORMATS = ['epub', 'fb2', 'pdf', 'djvu', 'txt'] VENDOR_ID = [0x1006] - PRODUCT_ID = [0x4023, 0x4024, 0x4025, 0x4034] - BCD = [0x0323, 0x0326] + PRODUCT_ID = [0x4023, 0x4024, 0x4025, 0x4034, 0x4037] + BCD = [0x0323, 0x0326, 0x226] VENDOR_NAME = 'IRIVER' - WINDOWS_MAIN_MEM = ['STORY', 'STORY_EB05', 'STORY_WI-FI', 'STORY_EB07'] + WINDOWS_MAIN_MEM = ['STORY', 'STORY_EB05', 'STORY_WI-FI', 'STORY_EB07', + 'STORY_EB12'] WINDOWS_MAIN_MEM = re.compile(r'(%s)&'%('|'.join(WINDOWS_MAIN_MEM))) - WINDOWS_CARD_A_MEM = ['STORY', 'STORY_SD'] + WINDOWS_CARD_A_MEM = ['STORY', 'STORY_SD', 'STORY_EB12_SD'] WINDOWS_CARD_A_MEM = re.compile(r'(%s)&'%('|'.join(WINDOWS_CARD_A_MEM))) #OSX_MAIN_MEM = 'Kindle Internal Storage Media' diff --git a/src/calibre/devices/kobo/books.py b/src/calibre/devices/kobo/books.py index fc18a61fc8..fb502bccac 100644 --- a/src/calibre/devices/kobo/books.py +++ b/src/calibre/devices/kobo/books.py @@ -6,7 +6,7 @@ import os, time, sys from calibre.constants import preferred_encoding, DEBUG from calibre import isbytestring, force_unicode -from calibre.utils.icu import strcmp +from calibre.utils.icu import sort_key from calibre.devices.usbms.books import Book as Book_ from calibre.devices.usbms.books import CollectionsBookList @@ -239,9 +239,8 @@ class KTCollectionsBookList(CollectionsBookList): if y is None: return -1 if isinstance(x, basestring) and isinstance(y, basestring): - c = strcmp(force_unicode(x), force_unicode(y)) - else: - c = cmp(x, y) + x, y = sort_key(force_unicode(x)), sort_key(force_unicode(y)) + c = cmp(x, y) if c != 0: return c # same as above -- no sort_key needed here diff --git a/src/calibre/devices/usbms/books.py b/src/calibre/devices/usbms/books.py index 7d6377ca96..97527a92ed 100644 --- a/src/calibre/devices/usbms/books.py +++ b/src/calibre/devices/usbms/books.py @@ -13,7 +13,7 @@ from calibre.devices.interface import BookList as _BookList from calibre.constants import preferred_encoding from calibre import isbytestring, force_unicode from calibre.utils.config import device_prefs, tweaks -from calibre.utils.icu import strcmp +from calibre.utils.icu import sort_key from calibre.utils.formatter import EvalFormatter class Book(Metadata): @@ -281,9 +281,8 @@ class CollectionsBookList(BookList): if y is None: return -1 if isinstance(x, basestring) and isinstance(y, basestring): - c = strcmp(force_unicode(x), force_unicode(y)) - else: - c = cmp(x, y) + x, y = sort_key(force_unicode(x)), sort_key(force_unicode(y)) + c = cmp(x, y) if c != 0: return c # same as above -- no sort_key needed here diff --git a/src/calibre/ebooks/oeb/display/paged.coffee b/src/calibre/ebooks/oeb/display/paged.coffee index aae9aeddd2..2f698966dd 100644 --- a/src/calibre/ebooks/oeb/display/paged.coffee +++ b/src/calibre/ebooks/oeb/display/paged.coffee @@ -75,6 +75,13 @@ class PagedDisplay this.margin_side = margin_side this.margin_bottom = margin_bottom + handle_rtl_body: (body_style) -> + if body_style.direction == "rtl" + for node in document.body.childNodes + if node.nodeType == node.ELEMENT_NODE and window.getComputedStyle(node).direction == "rtl" + node.style.setProperty("direction", "rtl") + document.body.style.direction = "ltr" + layout: (is_single_page=false) -> # start_time = new Date().getTime() body_style = window.getComputedStyle(document.body) @@ -84,6 +91,7 @@ class PagedDisplay # Check if the current document is a full screen layout like # cover, if so we treat it specially. single_screen = (document.body.scrollHeight < window.innerHeight + 75) + this.handle_rtl_body(body_style) first_layout = true ww = window.innerWidth diff --git a/src/calibre/ebooks/oeb/display/webview.py b/src/calibre/ebooks/oeb/display/webview.py index a186044fe5..d7769cb33a 100644 --- a/src/calibre/ebooks/oeb/display/webview.py +++ b/src/calibre/ebooks/oeb/display/webview.py @@ -31,7 +31,7 @@ def self_closing_sub(match): return '<%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, path_is_html=False, + pre_load_callback=lambda x:None, path_is_html=False, force_as_html=False): from PyQt4.Qt import QUrl, QByteArray if mime_type is None: @@ -45,13 +45,13 @@ def load_html(path, view, codec='utf-8', mime_type=None, html = f.read().decode(codec, 'replace') html = EntityDeclarationProcessor(html).processed_html - self_closing_pat = re.compile(r'<\s*([A-Za-z1-6]+)([^>]*)/\s*>') + self_closing_pat = re.compile(r'<\s*([:A-Za-z0-9-]+)([^>]*)/\s*>') html = self_closing_pat.sub(self_closing_sub, html) loading_url = QUrl.fromLocalFile(path) pre_load_callback(loading_url) - if force_as_html or re.search(r'<[:a-zA-Z]*svg', html) is None: + if force_as_html or re.search(r'<[:a-zA-Z0-9-]*svg', html) is None: view.setHtml(html, loading_url) else: view.setContent(QByteArray(html.encode(codec)), mime_type, diff --git a/src/calibre/ebooks/oeb/polish/main.py b/src/calibre/ebooks/oeb/polish/main.py index bd222cdbd2..c04686ed6c 100644 --- a/src/calibre/ebooks/oeb/polish/main.py +++ b/src/calibre/ebooks/oeb/polish/main.py @@ -174,6 +174,7 @@ def gui_polish(data): files = data.pop('files') if not data.pop('metadata'): data.pop('opf') + if not data.pop('do_cover'): data.pop('cover') file_map = {x:x for x in files} opts = ALL_OPTS.copy() diff --git a/src/calibre/ebooks/oeb/polish/subset.py b/src/calibre/ebooks/oeb/polish/subset.py index 191d5265a4..adee8b2fb6 100644 --- a/src/calibre/ebooks/oeb/polish/subset.py +++ b/src/calibre/ebooks/oeb/polish/subset.py @@ -9,10 +9,11 @@ __docformat__ = 'restructuredtext en' import os, sys -from calibre import prints +from calibre import prints, as_unicode from calibre.ebooks.oeb.base import OEB_STYLES, OEB_DOCS, XPath from calibre.ebooks.oeb.polish.container import OEB_FONTS from calibre.utils.fonts.sfnt.subset import subset +from calibre.utils.fonts.sfnt.errors import UnsupportedFont from calibre.utils.fonts.utils import get_font_names def remove_font_face_rules(container, sheet, remove_names, base): @@ -46,9 +47,16 @@ def subset_all_fonts(container, font_stats, report): raw = f.read() font_name = get_font_names(raw)[-1] warnings = [] - container.log('Subsetting font: %s'%font_name) - nraw, old_sizes, new_sizes = subset(raw, chars, + container.log('Subsetting font: %s'%(font_name or name)) + try: + nraw, old_sizes, new_sizes = subset(raw, chars, warnings=warnings) + except UnsupportedFont as e: + container.log.warning( + 'Unsupported font: %s, ignoring. Error: %s'%( + name, as_unicode(e))) + continue + for w in warnings: container.log.warn(w) olen = sum(old_sizes.itervalues()) diff --git a/src/calibre/ebooks/oeb/transforms/flatcss.py b/src/calibre/ebooks/oeb/transforms/flatcss.py index 03410e1a65..f2a0b1f2ea 100644 --- a/src/calibre/ebooks/oeb/transforms/flatcss.py +++ b/src/calibre/ebooks/oeb/transforms/flatcss.py @@ -363,7 +363,10 @@ class CSSFlattener(object): cssdict['font-weight'] = 'normal' # ADE chokes on font-weight medium fsize = font_size - if not self.context.disable_font_rescaling: + is_drop_cap = (cssdict.get('float', None) == 'left' and 'font-size' in + cssdict and len(node) == 0 and node.text and + len(node.text) == 1) + if not self.context.disable_font_rescaling and not is_drop_cap: _sbase = self.sbase if self.sbase is not None else \ self.context.source.fbase dyn_rescale = dynamic_rescale_factor(node) @@ -382,7 +385,7 @@ class CSSFlattener(object): try: minlh = self.context.minimum_line_height / 100. - if style['line-height'] < minlh * fsize: + if not is_drop_cap and style['line-height'] < minlh * fsize: cssdict['line-height'] = str(minlh) except: self.oeb.logger.exception('Failed to set minimum line-height') diff --git a/src/calibre/ebooks/pdf/render/fonts.py b/src/calibre/ebooks/pdf/render/fonts.py index e99cc7c218..9a1167021c 100644 --- a/src/calibre/ebooks/pdf/render/fonts.py +++ b/src/calibre/ebooks/pdf/render/fonts.py @@ -13,9 +13,10 @@ from operator import itemgetter from collections import Counter, OrderedDict from future_builtins import map +from calibre import as_unicode from calibre.ebooks.pdf.render.common import (Array, String, Stream, Dictionary, Name) -from calibre.utils.fonts.sfnt.subset import pdf_subset +from calibre.utils.fonts.sfnt.subset import pdf_subset, UnsupportedFont STANDARD_FONTS = { 'Times-Roman', 'Helvetica', 'Courier', 'Symbol', 'Times-Bold', @@ -150,12 +151,16 @@ class Font(object): self.used_glyphs = set() - def embed(self, objects): + def embed(self, objects, debug): self.font_descriptor['FontFile'+('3' if self.is_otf else '2') ] = objects.add(self.font_stream) self.write_widths(objects) self.write_to_unicode(objects) - pdf_subset(self.metrics.sfnt, self.used_glyphs) + try: + pdf_subset(self.metrics.sfnt, self.used_glyphs) + except UnsupportedFont as e: + debug('Subsetting of %s not supported, embedding full font. Error: %s'%( + self.metrics.names.get('full_name', 'Unknown'), as_unicode(e))) if self.is_otf: self.font_stream.write(self.metrics.sfnt['CFF '].raw) else: @@ -221,7 +226,7 @@ class FontManager(object): })) return self.std_map[name] - def embed_fonts(self): + def embed_fonts(self, debug): for font in self.fonts: - font.embed(self.objects) + font.embed(self.objects, debug) diff --git a/src/calibre/ebooks/pdf/render/serialize.py b/src/calibre/ebooks/pdf/render/serialize.py index 936cb5f156..fd10e0756f 100644 --- a/src/calibre/ebooks/pdf/render/serialize.py +++ b/src/calibre/ebooks/pdf/render/serialize.py @@ -488,7 +488,7 @@ class PDFStream(object): def end(self): if self.current_page.getvalue(): self.end_page() - self.font_manager.embed_fonts() + self.font_manager.embed_fonts(self.debug) inforef = self.objects.add(self.info) self.links.add_links() self.objects.pdf_serialize(self.stream) diff --git a/src/calibre/gui2/actions/polish.py b/src/calibre/gui2/actions/polish.py index aeb3a2e332..1be05f181b 100644 --- a/src/calibre/gui2/actions/polish.py +++ b/src/calibre/gui2/actions/polish.py @@ -44,13 +44,18 @@ class Polish(QDialog): # {{{ _('

Smarten punctuation

%s')%HELP['smarten_punctuation'], 'metadata':_('

Updating metadata

' - '

This will update all metadata and covers in the' + '

This will update all metadata except the cover in the' ' ebook files to match the current metadata in the' - ' calibre library.

If the ebook file does not have' - ' an identifiable cover, a new cover is inserted.

' + ' calibre library.

' '

Note that most ebook' ' formats are not capable of supporting all the' - ' metadata in calibre.

'), + ' metadata in calibre.

There is a separate option to' + ' update the cover.

'), + 'do_cover': _('

Update the covers in the ebook files to match the' + ' current cover in the calibre library.

' + '

If the ebook file does not have' + ' an identifiable cover, a new cover is inserted.

' + ), 'jacket':_('

Book Jacket

%s')%HELP['jacket'], 'remove_jacket':_('

Remove Book Jacket

%s')%HELP['remove_jacket'], } @@ -63,11 +68,12 @@ class Polish(QDialog): # {{{ count = 0 self.all_actions = OrderedDict([ - ('subset', _('Subset all embedded fonts')), - ('smarten_punctuation', _('Smarten punctuation')), - ('metadata', _('Update metadata in book files')), - ('jacket', _('Add metadata as a "book jacket" page')), - ('remove_jacket', _('Remove a previously inserted book jacket')), + ('subset', _('&Subset all embedded fonts')), + ('smarten_punctuation', _('Smarten &punctuation')), + ('metadata', _('Update &metadata in the book files')), + ('do_cover', _('Update the &cover in the book files')), + ('jacket', _('Add metadata as a "book &jacket" page')), + ('remove_jacket', _('&Remove a previously inserted book jacket')), ]) prefs = gprefs.get('polishing_settings', {}) for name, text in self.all_actions.iteritems(): @@ -243,8 +249,10 @@ class Polish(QDialog): # {{{ cover = os.path.join(base, 'cover.jpg') if db.copy_cover_to(book_id, cover, index_is_id=True): data['cover'] = cover + is_orig = {} for fmt in formats: ext = fmt.replace('ORIGINAL_', '').lower() + is_orig[ext.upper()] = 'ORIGINAL_' in fmt with open(os.path.join(base, '%s.%s'%(book_id, ext)), 'wb') as f: db.copy_format_to(book_id, fmt, f, index_is_id=True) data['files'].append(f.name) @@ -257,7 +265,7 @@ class Polish(QDialog): # {{{ self.pd.set_msg(_('Queueing book %(nums)s of %(tot)s (%(title)s)')%dict( nums=num, tot=len(self.book_id_map), title=mi.title)) - self.jobs.append((desc, data, book_id, base)) + self.jobs.append((desc, data, book_id, base, is_orig)) # }}} class Report(QDialog): # {{{ @@ -404,11 +412,11 @@ class PolishAction(InterfaceAction): d = Polish(self.gui.library_view.model().db, book_id_map, parent=self.gui) if d.exec_() == d.Accepted and d.jobs: show_reports = bool(d.show_reports.isChecked()) - for desc, data, book_id, base in reversed(d.jobs): + for desc, data, book_id, base, is_orig in reversed(d.jobs): job = self.gui.job_manager.run_job( Dispatcher(self.book_polished), 'gui_polish', args=(data,), description=desc) - job.polish_args = (book_id, base, data['files'], show_reports) + job.polish_args = (book_id, base, data['files'], show_reports, is_orig) if d.jobs: self.gui.jobs_pointer.start() self.gui.status_bar.show_message( @@ -419,11 +427,11 @@ class PolishAction(InterfaceAction): self.gui.job_exception(job) return db = self.gui.current_db - book_id, base, files, show_reports = job.polish_args + book_id, base, files, show_reports, is_orig = job.polish_args fmts = set() for path in files: fmt = path.rpartition('.')[-1].upper() - if tweaks['save_original_format_when_polishing']: + if tweaks['save_original_format_when_polishing'] and not is_orig[fmt]: fmts.add(fmt) db.save_original_format(book_id, fmt, notify=False) with open(path, 'rb') as f: diff --git a/src/calibre/gui2/comments_editor.py b/src/calibre/gui2/comments_editor.py index 39ada6b6cb..41f92b509a 100644 --- a/src/calibre/gui2/comments_editor.py +++ b/src/calibre/gui2/comments_editor.py @@ -327,6 +327,13 @@ class EditorWidget(QWebView): # {{{ else: return QWebView.keyReleaseEvent(self, ev) + def contextMenuEvent(self, ev): + menu = self.page().createStandardContextMenu() + paste = self.pageAction(QWebPage.Paste) + for action in menu.actions(): + if action == paste: + menu.insertAction(action, self.pageAction(QWebPage.PasteAndMatchStyle)) + menu.exec_(ev.globalPos()) # }}} diff --git a/src/calibre/gui2/custom_column_widgets.py b/src/calibre/gui2/custom_column_widgets.py index 0f5bb0a1c9..3597b0fb19 100644 --- a/src/calibre/gui2/custom_column_widgets.py +++ b/src/calibre/gui2/custom_column_widgets.py @@ -622,8 +622,7 @@ class BulkBase(Base): return val = self.gui_val val = self.normalize_ui_val(val) - if val != self.initial_val: - self.db.set_custom_bulk(book_ids, val, num=self.col_id, notify=notify) + self.db.set_custom_bulk(book_ids, val, num=self.col_id, notify=notify) def make_widgets(self, parent, main_widget_class, extra_label_text=''): w = QWidget(parent) @@ -1030,8 +1029,7 @@ class BulkText(BulkBase): else: val = self.gui_val val = self.normalize_ui_val(val) - if val != self.initial_val: - self.db.set_custom_bulk(book_ids, val, num=self.col_id, notify=notify) + self.db.set_custom_bulk(book_ids, val, num=self.col_id, notify=notify) def getter(self): if self.col_metadata['is_multiple']: diff --git a/src/calibre/gui2/main.py b/src/calibre/gui2/main.py index f722cf226f..d43e618e9a 100644 --- a/src/calibre/gui2/main.py +++ b/src/calibre/gui2/main.py @@ -369,7 +369,7 @@ def build_pipe(print_error=True): t.start() t.join(3.0) if t.is_alive(): - if iswindows(): + if iswindows: cant_start() else: f = os.path.expanduser('~/.calibre_calibre GUI.lock') diff --git a/src/calibre/gui2/viewer/documentview.py b/src/calibre/gui2/viewer/documentview.py index 7df171a404..1878b5e760 100644 --- a/src/calibre/gui2/viewer/documentview.py +++ b/src/calibre/gui2/viewer/documentview.py @@ -790,8 +790,7 @@ class DocumentView(QWebView): # {{{ self.manager.load_started() load_html(path, self, codec=getattr(path, 'encoding', 'utf-8'), mime_type=getattr(path, - 'mime_type', 'text/html'), pre_load_callback=callback, - force_as_html=True) + 'mime_type', 'text/html'), pre_load_callback=callback) entries = set() for ie in getattr(path, 'index_entries', []): if ie.start_anchor: diff --git a/src/calibre/gui2/viewer/main.py b/src/calibre/gui2/viewer/main.py index b1b0e7bd87..4587a6542b 100644 --- a/src/calibre/gui2/viewer/main.py +++ b/src/calibre/gui2/viewer/main.py @@ -725,13 +725,15 @@ class EbookViewer(MainWindow, Ui_EbookViewer): self.view.shrink_fonts() def magnification_changed(self, val): - tt = _('%(which)s font size [%(sc)s]\nCurrent magnification: %(mag).1f') + tt = '%(action)s [%(sc)s]\n'+_('Current magnification: %(mag).1f') sc = unicode(self.action_font_size_larger.shortcut().toString()) self.action_font_size_larger.setToolTip( - tt %dict(which=_('Increase'), mag=val, sc=sc)) + tt %dict(action=unicode(self.action_font_size_larger.text()), + mag=val, sc=sc)) sc = unicode(self.action_font_size_smaller.shortcut().toString()) self.action_font_size_smaller.setToolTip( - tt %dict(which=_('Decrease'), mag=val, sc=sc)) + tt %dict(action=unicode(self.action_font_size_smaller.text()), + mag=val, sc=sc)) self.action_font_size_larger.setEnabled(self.view.multiplier < 3) self.action_font_size_smaller.setEnabled(self.view.multiplier > 0.2) diff --git a/src/calibre/gui2/widgets.py b/src/calibre/gui2/widgets.py index 997502a9d7..5d3d1e26c6 100644 --- a/src/calibre/gui2/widgets.py +++ b/src/calibre/gui2/widgets.py @@ -955,8 +955,8 @@ class LayoutButton(QToolButton): def set_state_to_hide(self, *args): self.setChecked(True) - label = _('Hide') - self.setText(label + ' ' + self.label+ u' (%s)'%self.shortcut) + self.setText(_('Hide %(label)s %(shortcut)s'%dict( + label=self.label, shortcut=self.shortcut))) self.setToolTip(self.text()) self.setStatusTip(self.text()) diff --git a/src/calibre/library/sqlite.py b/src/calibre/library/sqlite.py index 90d293ba64..2e438ceb55 100644 --- a/src/calibre/library/sqlite.py +++ b/src/calibre/library/sqlite.py @@ -20,7 +20,7 @@ from calibre.ebooks.metadata import title_sort, author_to_author_sort from calibre.utils.date import parse_date, isoformat, local_tz, UNDEFINED_DATE from calibre import isbytestring, force_unicode from calibre.constants import iswindows, DEBUG, plugins -from calibre.utils.icu import strcmp +from calibre.utils.icu import sort_key from calibre import prints from dateutil.tz import tzoffset @@ -189,7 +189,8 @@ def pynocase(one, two, encoding='utf-8'): return cmp(one.lower(), two.lower()) def icu_collator(s1, s2): - return strcmp(force_unicode(s1, 'utf-8'), force_unicode(s2, 'utf-8')) + return cmp(sort_key(force_unicode(s1, 'utf-8')), + sort_key(force_unicode(s2, 'utf-8'))) def load_c_extensions(conn, debug=DEBUG): try: diff --git a/src/calibre/translations/calibre.pot b/src/calibre/translations/calibre.pot index 52a0aa73d2..f5f97d3059 100644 --- a/src/calibre/translations/calibre.pot +++ b/src/calibre/translations/calibre.pot @@ -5,8 +5,8 @@ msgid "" msgstr "" "Project-Id-Version: calibre 0.9.20\n" -"POT-Creation-Date: 2013-02-22 10:18+IST\n" -"PO-Revision-Date: 2013-02-22 10:18+IST\n" +"POT-Creation-Date: 2013-02-24 10:08+IST\n" +"PO-Revision-Date: 2013-02-24 10:08+IST\n" "Last-Translator: Automatically generated\n" "Language-Team: LANGUAGE\n" "MIME-Version: 1.0\n" @@ -24,8 +24,8 @@ msgstr "" #: /home/kovid/work/calibre/src/calibre/db/cache.py:124 #: /home/kovid/work/calibre/src/calibre/db/cache.py:127 #: /home/kovid/work/calibre/src/calibre/db/cache.py:138 -#: /home/kovid/work/calibre/src/calibre/db/write.py:100 #: /home/kovid/work/calibre/src/calibre/db/write.py:102 +#: /home/kovid/work/calibre/src/calibre/db/write.py:106 #: /home/kovid/work/calibre/src/calibre/devices/android/driver.py:383 #: /home/kovid/work/calibre/src/calibre/devices/android/driver.py:384 #: /home/kovid/work/calibre/src/calibre/devices/hanvon/driver.py:114 @@ -882,8 +882,8 @@ msgstr "" msgid "Disable the named plugin" msgstr "" -#: /home/kovid/work/calibre/src/calibre/db/backend.py:321 -#: /home/kovid/work/calibre/src/calibre/db/backend.py:330 +#: /home/kovid/work/calibre/src/calibre/db/backend.py:323 +#: /home/kovid/work/calibre/src/calibre/db/backend.py:332 #: /home/kovid/work/calibre/src/calibre/gui2/actions/choose_library.py:322 #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/choose_library.py:98 #: /home/kovid/work/calibre/src/calibre/gui2/wizard/__init__.py:749 @@ -896,7 +896,7 @@ msgstr "" #: /home/kovid/work/calibre/src/calibre/db/cache.py:152 #: /home/kovid/work/calibre/src/calibre/ebooks/metadata/book/base.py:666 #: /home/kovid/work/calibre/src/calibre/gui2/custom_column_widgets.py:67 -#: /home/kovid/work/calibre/src/calibre/gui2/custom_column_widgets.py:678 +#: /home/kovid/work/calibre/src/calibre/gui2/custom_column_widgets.py:677 #: /home/kovid/work/calibre/src/calibre/library/database2.py:1030 #: /home/kovid/work/calibre/src/calibre/utils/formatter_functions.py:887 #: /home/kovid/work/calibre/src/calibre/utils/formatter_functions.py:910 @@ -908,25 +908,25 @@ msgstr "" msgid "%(tt)sAverage rating is %(rating)3.1f" msgstr "" -#: /home/kovid/work/calibre/src/calibre/db/fields.py:232 +#: /home/kovid/work/calibre/src/calibre/db/fields.py:233 #: /home/kovid/work/calibre/src/calibre/library/database2.py:1187 msgid "Main" msgstr "" -#: /home/kovid/work/calibre/src/calibre/db/fields.py:234 +#: /home/kovid/work/calibre/src/calibre/db/fields.py:235 #: /home/kovid/work/calibre/src/calibre/gui2/layout.py:77 #: /home/kovid/work/calibre/src/calibre/library/database2.py:1189 msgid "Card A" msgstr "" -#: /home/kovid/work/calibre/src/calibre/db/fields.py:236 +#: /home/kovid/work/calibre/src/calibre/db/fields.py:237 #: /home/kovid/work/calibre/src/calibre/gui2/layout.py:79 #: /home/kovid/work/calibre/src/calibre/library/database2.py:1191 msgid "Card B" msgstr "" -#: /home/kovid/work/calibre/src/calibre/db/fields.py:471 -#: /home/kovid/work/calibre/src/calibre/db/fields.py:486 +#: /home/kovid/work/calibre/src/calibre/db/fields.py:472 +#: /home/kovid/work/calibre/src/calibre/db/fields.py:487 #: /home/kovid/work/calibre/src/calibre/devices/apple/driver.py:2822 #: /home/kovid/work/calibre/src/calibre/devices/nook/driver.py:106 #: /home/kovid/work/calibre/src/calibre/devices/prs505/sony_cache.py:448 @@ -3479,7 +3479,7 @@ msgstr "" #: /home/kovid/work/calibre/src/calibre/ebooks/metadata/book/base.py:666 #: /home/kovid/work/calibre/src/calibre/gui2/custom_column_widgets.py:67 -#: /home/kovid/work/calibre/src/calibre/gui2/custom_column_widgets.py:678 +#: /home/kovid/work/calibre/src/calibre/gui2/custom_column_widgets.py:677 msgid "No" msgstr "" @@ -3928,7 +3928,7 @@ msgid "" msgstr "" #: /home/kovid/work/calibre/src/calibre/ebooks/oeb/polish/main.py:48 -#: /home/kovid/work/calibre/src/calibre/gui2/actions/polish.py:392 +#: /home/kovid/work/calibre/src/calibre/gui2/actions/polish.py:400 #: /home/kovid/work/calibre/src/calibre/gui2/preferences/look_feel.py:197 #: /home/kovid/work/calibre/src/calibre/gui2/shortcuts.py:132 #: /home/kovid/work/calibre/src/calibre/gui2/shortcuts.py:223 @@ -4034,27 +4034,27 @@ msgstr "" msgid "Polishing took: %.1f seconds" msgstr "" -#: /home/kovid/work/calibre/src/calibre/ebooks/oeb/polish/main.py:201 +#: /home/kovid/work/calibre/src/calibre/ebooks/oeb/polish/main.py:202 msgid "Path to a cover image. Changes the cover specified in the ebook. If no cover is present, or the cover is not properly identified, inserts a new cover." msgstr "" -#: /home/kovid/work/calibre/src/calibre/ebooks/oeb/polish/main.py:204 +#: /home/kovid/work/calibre/src/calibre/ebooks/oeb/polish/main.py:205 msgid "Path to an OPF file. The metadata in the book is updated from the OPF file." msgstr "" -#: /home/kovid/work/calibre/src/calibre/ebooks/oeb/polish/main.py:209 +#: /home/kovid/work/calibre/src/calibre/ebooks/oeb/polish/main.py:210 msgid "Produce more verbose output, useful for debugging." msgstr "" -#: /home/kovid/work/calibre/src/calibre/ebooks/oeb/polish/main.py:219 +#: /home/kovid/work/calibre/src/calibre/ebooks/oeb/polish/main.py:220 msgid "You must provide the input file to polish" msgstr "" -#: /home/kovid/work/calibre/src/calibre/ebooks/oeb/polish/main.py:223 +#: /home/kovid/work/calibre/src/calibre/ebooks/oeb/polish/main.py:224 msgid "Unknown extra arguments" msgstr "" -#: /home/kovid/work/calibre/src/calibre/ebooks/oeb/polish/main.py:241 +#: /home/kovid/work/calibre/src/calibre/ebooks/oeb/polish/main.py:242 msgid "You must specify at least one action to perform" msgstr "" @@ -4385,7 +4385,7 @@ msgstr "" #: /home/kovid/work/calibre/src/calibre/gui2/actions/edit_metadata.py:192 #: /home/kovid/work/calibre/src/calibre/gui2/actions/edit_metadata.py:256 #: /home/kovid/work/calibre/src/calibre/gui2/actions/edit_metadata.py:293 -#: /home/kovid/work/calibre/src/calibre/gui2/actions/polish.py:377 +#: /home/kovid/work/calibre/src/calibre/gui2/actions/polish.py:385 #: /home/kovid/work/calibre/src/calibre/gui2/actions/save_to_disk.py:82 #: /home/kovid/work/calibre/src/calibre/gui2/actions/view.py:271 msgid "No books selected" @@ -5393,165 +5393,175 @@ msgid "

Smarten punctuation

%s" msgstr "" #: /home/kovid/work/calibre/src/calibre/gui2/actions/polish.py:46 -msgid "

Updating metadata

This will update all metadata and covers in the ebook files to match the current metadata in the calibre library.

If the ebook file does not have an identifiable cover, a new cover is inserted.

Note that most ebook formats are not capable of supporting all the metadata in calibre.

" +msgid "

Updating metadata

This will update all metadata except the cover in the ebook files to match the current metadata in the calibre library.

Note that most ebook formats are not capable of supporting all the metadata in calibre.

There is a separate option to update the cover.

" msgstr "" #: /home/kovid/work/calibre/src/calibre/gui2/actions/polish.py:54 +msgid "

Update the covers in the ebook files to match the current cover in the calibre library.

If the ebook file does not have an identifiable cover, a new cover is inserted.

" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/actions/polish.py:59 #, python-format msgid "

Book Jacket

%s" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/actions/polish.py:55 +#: /home/kovid/work/calibre/src/calibre/gui2/actions/polish.py:60 #, python-format msgid "

Remove Book Jacket

%s" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/actions/polish.py:61 +#: /home/kovid/work/calibre/src/calibre/gui2/actions/polish.py:66 msgid "Select actions to perform:" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/actions/polish.py:66 -msgid "Subset all embedded fonts" +#: /home/kovid/work/calibre/src/calibre/gui2/actions/polish.py:71 +#: /home/kovid/work/calibre/src/calibre/gui2/convert/look_and_feel_ui.py:249 +msgid "&Subset all embedded fonts" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/actions/polish.py:67 -msgid "Smarten punctuation" +#: /home/kovid/work/calibre/src/calibre/gui2/actions/polish.py:72 +#: /home/kovid/work/calibre/src/calibre/gui2/convert/look_and_feel_ui.py:240 +msgid "Smarten &punctuation" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/actions/polish.py:68 -msgid "Update metadata in book files" +#: /home/kovid/work/calibre/src/calibre/gui2/actions/polish.py:73 +msgid "Update &metadata in the book files" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/actions/polish.py:69 -msgid "Add metadata as a \"book jacket\" page" +#: /home/kovid/work/calibre/src/calibre/gui2/actions/polish.py:74 +msgid "Update the &cover in the book files" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/actions/polish.py:70 -msgid "Remove a previously inserted book jacket" +#: /home/kovid/work/calibre/src/calibre/gui2/actions/polish.py:75 +msgid "Add metadata as a \"book &jacket\" page" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/actions/polish.py:80 +#: /home/kovid/work/calibre/src/calibre/gui2/actions/polish.py:76 +msgid "&Remove a previously inserted book jacket" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/gui2/actions/polish.py:86 msgid "About" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/actions/polish.py:99 +#: /home/kovid/work/calibre/src/calibre/gui2/actions/polish.py:105 msgid "Show &report" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/actions/polish.py:101 +#: /home/kovid/work/calibre/src/calibre/gui2/actions/polish.py:107 msgid "Show a report of all the actions performed after polishing is completed" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/actions/polish.py:107 +#: /home/kovid/work/calibre/src/calibre/gui2/actions/polish.py:113 msgid "&Save Settings" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/actions/polish.py:109 +#: /home/kovid/work/calibre/src/calibre/gui2/actions/polish.py:115 msgid "&Load Settings" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/actions/polish.py:112 +#: /home/kovid/work/calibre/src/calibre/gui2/actions/polish.py:118 #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/duplicates.py:47 msgid "Select &all" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/actions/polish.py:114 +#: /home/kovid/work/calibre/src/calibre/gui2/actions/polish.py:120 #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/duplicates.py:49 msgid "Select &none" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/actions/polish.py:130 -#: /home/kovid/work/calibre/src/calibre/gui2/actions/polish.py:195 +#: /home/kovid/work/calibre/src/calibre/gui2/actions/polish.py:136 +#: /home/kovid/work/calibre/src/calibre/gui2/actions/polish.py:201 msgid "No actions selected" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/actions/polish.py:131 +#: /home/kovid/work/calibre/src/calibre/gui2/actions/polish.py:137 msgid "You must select at least one action before saving" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/actions/polish.py:133 +#: /home/kovid/work/calibre/src/calibre/gui2/actions/polish.py:139 msgid "Choose name" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/actions/polish.py:134 +#: /home/kovid/work/calibre/src/calibre/gui2/actions/polish.py:140 msgid "Choose a name for these settings" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/actions/polish.py:154 +#: /home/kovid/work/calibre/src/calibre/gui2/actions/polish.py:160 msgid "Remove saved settings" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/actions/polish.py:196 +#: /home/kovid/work/calibre/src/calibre/gui2/actions/polish.py:202 msgid "You must select at least one action, or click Cancel." msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/actions/polish.py:210 +#: /home/kovid/work/calibre/src/calibre/gui2/actions/polish.py:216 msgid "Queueing books for polishing" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/actions/polish.py:252 +#: /home/kovid/work/calibre/src/calibre/gui2/actions/polish.py:260 #, python-format msgid "Polish %s" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/actions/polish.py:253 +#: /home/kovid/work/calibre/src/calibre/gui2/actions/polish.py:261 #, python-format msgid "Polish book %(nums)s of %(tot)s (%(title)s)" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/actions/polish.py:257 +#: /home/kovid/work/calibre/src/calibre/gui2/actions/polish.py:265 #, python-format msgid "Queueing book %(nums)s of %(tot)s (%(title)s)" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/actions/polish.py:283 +#: /home/kovid/work/calibre/src/calibre/gui2/actions/polish.py:291 #, python-format msgid "Ignore remaining %d reports" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/actions/polish.py:290 +#: /home/kovid/work/calibre/src/calibre/gui2/actions/polish.py:298 msgid "View full &log" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/actions/polish.py:313 +#: /home/kovid/work/calibre/src/calibre/gui2/actions/polish.py:321 #, python-format msgid "Polishing of %s" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/actions/polish.py:319 +#: /home/kovid/work/calibre/src/calibre/gui2/actions/polish.py:327 #, python-format msgid "The original file has been saved as %s." msgid_plural "The original files have been saved as %s." msgstr[0] "" msgstr[1] "" -#: /home/kovid/work/calibre/src/calibre/gui2/actions/polish.py:321 +#: /home/kovid/work/calibre/src/calibre/gui2/actions/polish.py:329 msgid " and " msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/actions/polish.py:324 +#: /home/kovid/work/calibre/src/calibre/gui2/actions/polish.py:332 msgid "If you polish again, the polishing will run on the originals." msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/actions/polish.py:359 +#: /home/kovid/work/calibre/src/calibre/gui2/actions/polish.py:367 msgid "P" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/actions/polish.py:359 +#: /home/kovid/work/calibre/src/calibre/gui2/actions/polish.py:367 msgid "Polish books" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/actions/polish.py:376 -#: /home/kovid/work/calibre/src/calibre/gui2/actions/polish.py:389 +#: /home/kovid/work/calibre/src/calibre/gui2/actions/polish.py:384 +#: /home/kovid/work/calibre/src/calibre/gui2/actions/polish.py:397 msgid "Cannot polish" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/actions/polish.py:390 +#: /home/kovid/work/calibre/src/calibre/gui2/actions/polish.py:398 #, python-format msgid "Polishing is only supported for books in the %s formats. Convert to one of those formats before polishing." msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/actions/polish.py:415 +#: /home/kovid/work/calibre/src/calibre/gui2/actions/polish.py:423 #, python-format msgid "Start polishing of %d book(s)" msgstr "" @@ -7317,10 +7327,6 @@ msgstr "" msgid "Text &justification:" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/convert/look_and_feel_ui.py:240 -msgid "Smarten &punctuation" -msgstr "" - #: /home/kovid/work/calibre/src/calibre/gui2/convert/look_and_feel_ui.py:241 msgid "&Transliterate unicode characters to ASCII" msgstr "" @@ -7353,10 +7359,6 @@ msgstr "" msgid "&Disable font size rescaling" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/convert/look_and_feel_ui.py:249 -msgid "&Subset all embedded fonts" -msgstr "" - #: /home/kovid/work/calibre/src/calibre/gui2/convert/lrf_output.py:16 msgid "LRF Output" msgstr "" @@ -8263,10 +8265,10 @@ msgstr "" #: /home/kovid/work/calibre/src/calibre/gui2/custom_column_widgets.py:116 #: /home/kovid/work/calibre/src/calibre/gui2/custom_column_widgets.py:153 #: /home/kovid/work/calibre/src/calibre/gui2/custom_column_widgets.py:187 -#: /home/kovid/work/calibre/src/calibre/gui2/custom_column_widgets.py:682 -#: /home/kovid/work/calibre/src/calibre/gui2/custom_column_widgets.py:723 -#: /home/kovid/work/calibre/src/calibre/gui2/custom_column_widgets.py:746 -#: /home/kovid/work/calibre/src/calibre/gui2/custom_column_widgets.py:797 +#: /home/kovid/work/calibre/src/calibre/gui2/custom_column_widgets.py:681 +#: /home/kovid/work/calibre/src/calibre/gui2/custom_column_widgets.py:722 +#: /home/kovid/work/calibre/src/calibre/gui2/custom_column_widgets.py:745 +#: /home/kovid/work/calibre/src/calibre/gui2/custom_column_widgets.py:796 #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk.py:348 #: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk.py:356 #: /home/kovid/work/calibre/src/calibre/gui2/library/delegates.py:83 @@ -8279,23 +8281,23 @@ msgid "Undefined" msgstr "" #: /home/kovid/work/calibre/src/calibre/gui2/custom_column_widgets.py:130 -#: /home/kovid/work/calibre/src/calibre/gui2/custom_column_widgets.py:754 +#: /home/kovid/work/calibre/src/calibre/gui2/custom_column_widgets.py:753 msgid "star(s)" msgstr "" #: /home/kovid/work/calibre/src/calibre/gui2/custom_column_widgets.py:131 -#: /home/kovid/work/calibre/src/calibre/gui2/custom_column_widgets.py:755 +#: /home/kovid/work/calibre/src/calibre/gui2/custom_column_widgets.py:754 msgid "Unrated" msgstr "" #: /home/kovid/work/calibre/src/calibre/gui2/custom_column_widgets.py:174 -#: /home/kovid/work/calibre/src/calibre/gui2/custom_column_widgets.py:784 +#: /home/kovid/work/calibre/src/calibre/gui2/custom_column_widgets.py:783 #, python-format msgid "Set '%s' to today" msgstr "" #: /home/kovid/work/calibre/src/calibre/gui2/custom_column_widgets.py:176 -#: /home/kovid/work/calibre/src/calibre/gui2/custom_column_widgets.py:786 +#: /home/kovid/work/calibre/src/calibre/gui2/custom_column_widgets.py:785 #, python-format msgid "Clear '%s'" msgstr "" @@ -8320,35 +8322,35 @@ msgstr "" msgid "The enumeration \"{0}\" contains an invalid value that will be set to the default" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/custom_column_widgets.py:637 +#: /home/kovid/work/calibre/src/calibre/gui2/custom_column_widgets.py:636 msgid "Apply changes" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/custom_column_widgets.py:830 +#: /home/kovid/work/calibre/src/calibre/gui2/custom_column_widgets.py:829 msgid "Remove series" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/custom_column_widgets.py:833 +#: /home/kovid/work/calibre/src/calibre/gui2/custom_column_widgets.py:832 msgid "Automatically number books" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/custom_column_widgets.py:836 +#: /home/kovid/work/calibre/src/calibre/gui2/custom_column_widgets.py:835 msgid "Force numbers to start with " msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/custom_column_widgets.py:906 +#: /home/kovid/work/calibre/src/calibre/gui2/custom_column_widgets.py:905 msgid "The enumeration \"{0}\" contains invalid values that will not appear in the list" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/custom_column_widgets.py:950 +#: /home/kovid/work/calibre/src/calibre/gui2/custom_column_widgets.py:949 msgid "Remove all tags" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/custom_column_widgets.py:970 +#: /home/kovid/work/calibre/src/calibre/gui2/custom_column_widgets.py:969 msgid "tags to add" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/custom_column_widgets.py:977 +#: /home/kovid/work/calibre/src/calibre/gui2/custom_column_widgets.py:976 msgid "tags to remove" msgstr "" @@ -9343,7 +9345,7 @@ msgstr "" #: /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/metadata/single_download.py:543 -#: /home/kovid/work/calibre/src/calibre/gui2/viewer/main.py:751 +#: /home/kovid/work/calibre/src/calibre/gui2/viewer/main.py:753 msgid "No matches found" msgstr "" @@ -16154,7 +16156,7 @@ msgid "Options to customize the ebook viewer" msgstr "" #: /home/kovid/work/calibre/src/calibre/gui2/viewer/config.py:30 -#: /home/kovid/work/calibre/src/calibre/gui2/viewer/main.py:1146 +#: /home/kovid/work/calibre/src/calibre/gui2/viewer/main.py:1148 msgid "Remember last used window size" msgstr "" @@ -16724,83 +16726,73 @@ msgstr "" #: /home/kovid/work/calibre/src/calibre/gui2/viewer/main.py:728 #, python-format -msgid "" -"%(which)s font size [%(sc)s]\n" -"Current magnification: %(mag).1f" +msgid "Current magnification: %(mag).1f" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/viewer/main.py:731 -msgid "Increase" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/viewer/main.py:734 -msgid "Decrease" -msgstr "" - -#: /home/kovid/work/calibre/src/calibre/gui2/viewer/main.py:752 +#: /home/kovid/work/calibre/src/calibre/gui2/viewer/main.py:754 #, python-format msgid "No matches found for: %s" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/viewer/main.py:801 +#: /home/kovid/work/calibre/src/calibre/gui2/viewer/main.py:803 msgid "Loading flow..." msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/viewer/main.py:879 +#: /home/kovid/work/calibre/src/calibre/gui2/viewer/main.py:881 #, python-format msgid "Laying out %s" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/viewer/main.py:946 +#: /home/kovid/work/calibre/src/calibre/gui2/viewer/main.py:948 #, python-format msgid "Bookmark #%d" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/viewer/main.py:950 +#: /home/kovid/work/calibre/src/calibre/gui2/viewer/main.py:952 msgid "Add bookmark" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/viewer/main.py:951 +#: /home/kovid/work/calibre/src/calibre/gui2/viewer/main.py:953 msgid "Enter title for bookmark:" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/viewer/main.py:962 +#: /home/kovid/work/calibre/src/calibre/gui2/viewer/main.py:964 msgid "Manage Bookmarks" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/viewer/main.py:1004 +#: /home/kovid/work/calibre/src/calibre/gui2/viewer/main.py:1006 msgid "Loading ebook..." msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/viewer/main.py:1017 +#: /home/kovid/work/calibre/src/calibre/gui2/viewer/main.py:1019 msgid "Could not open ebook" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/viewer/main.py:1018 +#: /home/kovid/work/calibre/src/calibre/gui2/viewer/main.py:1020 msgid "Unknown error" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/viewer/main.py:1133 +#: /home/kovid/work/calibre/src/calibre/gui2/viewer/main.py:1135 msgid "Options to control the ebook viewer" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/viewer/main.py:1140 +#: /home/kovid/work/calibre/src/calibre/gui2/viewer/main.py:1142 msgid "If specified, viewer window will try to come to the front when started." msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/viewer/main.py:1143 +#: /home/kovid/work/calibre/src/calibre/gui2/viewer/main.py:1145 msgid "If specified, viewer window will try to open full screen when started." msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/viewer/main.py:1148 +#: /home/kovid/work/calibre/src/calibre/gui2/viewer/main.py:1150 msgid "Print javascript alert and console messages to the console" msgstr "" -#: /home/kovid/work/calibre/src/calibre/gui2/viewer/main.py:1150 +#: /home/kovid/work/calibre/src/calibre/gui2/viewer/main.py:1152 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 "" -#: /home/kovid/work/calibre/src/calibre/gui2/viewer/main.py:1157 +#: /home/kovid/work/calibre/src/calibre/gui2/viewer/main.py:1159 msgid "" "%prog [options] file\n" "\n" @@ -16914,7 +16906,8 @@ msgid "Show" msgstr "" #: /home/kovid/work/calibre/src/calibre/gui2/widgets.py:958 -msgid "Hide" +#, python-format +msgid "Hide %(label)s %(shortcut)s" msgstr "" #: /home/kovid/work/calibre/src/calibre/gui2/widgets.py:995 @@ -19363,6 +19356,18 @@ msgstr "" msgid "pm" msgstr "" +#: /home/kovid/work/calibre/src/calibre/utils/localization.py:204 +msgid "&Copy" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/utils/localization.py:205 +msgid "Select All" +msgstr "" + +#: /home/kovid/work/calibre/src/calibre/utils/localization.py:206 +msgid "Copy &Link location" +msgstr "" + #: /home/kovid/work/calibre/src/calibre/utils/pyconsole/console.py:56 msgid "Choose theme (needs restart)" msgstr "" @@ -19988,3 +19993,11 @@ msgstr "" #: /home/kovid/work/calibre/resources/default_tweaks.py:512 msgid "This means that you can make changes and press Enter and your changes will\nnot be overwritten by a matching completion. However, if you wish to use the\ncompletions you will now have to press Tab to select one before pressing\nEnter. Which technique you prefer will depend on the state of metadata in\nyour library and your personal editing style." msgstr "" + +#: /home/kovid/work/calibre/resources/default_tweaks.py:519 +msgid "Recognize numbers inside text when sorting" +msgstr "" + +#: /home/kovid/work/calibre/resources/default_tweaks.py:520 +msgid "This means that when sorting on text fields like title the text \"Book 2\"\nwill sort before the text \"Book 100\". If you want this behavior, set\nnumeric_collation = True note that doing so will cause problems with text\nthat starts with numbers and is a little slower." +msgstr "" diff --git a/src/calibre/utils/icu.c b/src/calibre/utils/icu.c index 3c133418b1..ccb1cfb5b9 100644 --- a/src/calibre/utils/icu.c +++ b/src/calibre/utils/icu.c @@ -110,6 +110,21 @@ icu_Collator_set_strength(icu_Collator *self, PyObject *val, void *closure) { } // }}} +// Collator.numeric {{{ +static PyObject * +icu_Collator_get_numeric(icu_Collator *self, void *closure) { + UErrorCode status = U_ZERO_ERROR; + return Py_BuildValue("O", (ucol_getAttribute(self->collator, UCOL_NUMERIC_COLLATION, &status) == UCOL_ON) ? Py_True : Py_False); +} + +static int +icu_Collator_set_numeric(icu_Collator *self, PyObject *val, void *closure) { + UErrorCode status = U_ZERO_ERROR; + ucol_setAttribute(self->collator, UCOL_NUMERIC_COLLATION, (PyObject_IsTrue(val)) ? UCOL_ON : UCOL_OFF, &status); + return 0; +} +// }}} + // Collator.actual_locale {{{ static PyObject * icu_Collator_actual_locale(icu_Collator *self, void *closure) { @@ -415,6 +430,10 @@ static PyGetSetDef icu_Collator_getsetters[] = { (char *)"The strength of this collator.", NULL}, + {(char *)"numeric", + (getter)icu_Collator_get_numeric, (setter)icu_Collator_set_numeric, + (char *)"If True the collator sorts contiguous digits as numbers rather than strings, so 2 will sort before 10.", + NULL}, {NULL} /* Sentinel */ }; diff --git a/src/calibre/utils/icu.py b/src/calibre/utils/icu.py index 66ee8fd59f..e1e6c1a1c6 100644 --- a/src/calibre/utils/icu.py +++ b/src/calibre/utils/icu.py @@ -12,7 +12,7 @@ from functools import partial from calibre.constants import plugins from calibre.utils.config_base import tweaks -_icu = _collator = _primary_collator = _secondary_collator = None +_icu = _collator = _primary_collator = _sort_collator = None _locale = None _none = u'' @@ -41,6 +41,7 @@ def load_icu(): return _icu def load_collator(): + 'The default collator for most locales takes both case and accented letters into account' global _collator if _collator is None: icu = load_icu() @@ -49,18 +50,25 @@ def load_collator(): return _collator def primary_collator(): + 'Ignores case differences and accented characters' global _primary_collator if _primary_collator is None: _primary_collator = _collator.clone() _primary_collator.strength = _icu.UCOL_PRIMARY return _primary_collator -def secondary_collator(): - global _secondary_collator - if _secondary_collator is None: - _secondary_collator = _collator.clone() - _secondary_collator.strength = _icu.UCOL_SECONDARY - return _secondary_collator +def sort_collator(): + 'Ignores case differences and recognizes numbers in strings' + global _sort_collator + if _sort_collator is None: + _sort_collator = _collator.clone() + _sort_collator.strength = _icu.UCOL_SECONDARY + if tweaks['numeric_collation']: + try: + _sort_collator.numeric = True + except AttributeError: + pass + return _sort_collator def py_sort_key(obj): if not obj: @@ -72,15 +80,15 @@ def icu_sort_key(collator, obj): return _none2 try: try: - return _secondary_collator.sort_key(obj) + return _sort_collator.sort_key(obj) except AttributeError: - return secondary_collator().sort_key(obj) + return sort_collator().sort_key(obj) except TypeError: if isinstance(obj, unicode): obj = obj.replace(u'\0', u'') else: obj = obj.replace(b'\0', b'') - return _secondary_collator.sort_key(obj) + return _sort_collator.sort_key(obj) def icu_change_case(upper, locale, obj): func = _icu.upper if upper else _icu.lower @@ -233,9 +241,9 @@ def collation_order(a): if _icu_not_ok: return (ord(a[0]), 1) if a else (0, 0) try: - return icu_collation_order(_secondary_collator, a) + return icu_collation_order(_sort_collator, a) except AttributeError: - return icu_collation_order(secondary_collator(), a) + return icu_collation_order(sort_collator(), a) ################################################################################ @@ -333,6 +341,7 @@ pêché''' german = create(german) c = _icu.Collator('de') + c.numeric = True gs = list(sorted(german, key=c.sort_key)) if gs != create(german_good): print 'German sorting failed' @@ -340,6 +349,7 @@ pêché''' print french = create(french) c = _icu.Collator('fr') + c.numeric = True fs = list(sorted(french, key=c.sort_key)) if fs != create(french_good): print 'French sorting failed (note that French fails with icu < 4.6)' @@ -388,6 +398,25 @@ pêché''' print 'startswith() failed' return + print '\nTesting collation_order()' + for group in [ + ('Šaa', 'Smith', 'Solženicyn', 'Štepánek'), + ('calibre', 'Charon', 'Collins'), + ('01', '1'), + ('1', '11', '13'), + ]: + last = None + for x in group: + val = icu_collation_order(sort_collator(), x) + if val[1] != 1: + prints('collation_order() returned incorrect length for', x) + if last is None: + last = val + else: + if val != last: + prints('collation_order() returned incorrect value for', x) + last = val + # }}} if __name__ == '__main__': diff --git a/src/calibre/utils/localization.py b/src/calibre/utils/localization.py index 9b49bf687e..1a4a335d52 100644 --- a/src/calibre/utils/localization.py +++ b/src/calibre/utils/localization.py @@ -201,6 +201,9 @@ if False: _('am') # NOTE: Post Meridian (i.e. like 10:00 pm) _('pm') + _('&Copy') + _('Select All') + _('Copy &Link location') _lcase_map = {} for k in _extra_lang_codes: