diff --git a/Changelog.yaml b/Changelog.yaml
index a25b52bfbf..8fecf3d95e 100644
--- a/Changelog.yaml
+++ b/Changelog.yaml
@@ -19,6 +19,73 @@
# new recipes:
# - title:
+- version: 0.7.50
+ date: 2011-03-18
+
+ new features:
+ - title: "Add 'Read a random book' to the view menu"
+
+ - title: "Add option to show composite columns in the tag browser."
+
+ - title: "Add a tweak in Preferences->Tweaks to control where news that is automatically uploaded to a reader is sent."
+ tickets: [9427]
+
+ - title: "Do not also show text in composite columns when showing an icon"
+
+ - title: "Add a menu item to clear the last viewed books history in the ebook viewer"
+
+ - title: "Kobo driver: Add support for the 'Closed' collection"
+
+ - title: "Add rename/delete saved search options to Tag browser context menu"
+
+ - title: "Make searches in the tag browser a possible hierarchical field"
+
+ - title: "Allow using empty username and password when setting up an SMTP relay"
+ tickets: [9195]
+
+ bug fixes:
+ - title: "Fix regression in 0.7.49 that broke deleting of news downloads older than x days."
+ tickets: [9417]
+
+ - title: "Restore the ability to remove missing formats from metadata.db to the Check Library operation"
+ tickets: [9377]
+
+ - title: "EPUB metadata: Read ISBN from Penguin epubs that dont correctly specify it"
+
+ - title: "Conversion pipeline: Handle the case where the ncx file is incorrectly given an HTML mimetype"
+
+ - title: "Make numpad navigation keys work in viewer"
+ tickets: [9428]
+
+ - title: "Fix ratings not being downloaded from Amazon"
+
+ - title: "Content server: Add workaround for Internet Explorer not supporting the ' entity."
+ tickets: [9413]
+
+ - title: "Conversion pipeline: When detecting chapters/toc links from HTML normalize spaces and increase maximum TOC title length to 1000 characters from 100 characters."
+ tickets: [9363]
+
+ - title: "Fix regression that broke Search and Replace on custom fields"
+ tickets: [9397]
+
+ - title: "Fix regression that caused currently selected row to be unfocussed int he device view when updataing metadata"
+ tickets: [9395]
+
+ - title: "Coversion S&R: Do not strip leading and trailing whitespace from the search and replace expressions in the GUI"
+
+
+ improved recipes:
+ - Sports Illustrated
+ - Draw and Cook
+
+ new recipes:
+ - title: "Evangelizo.org and pro-linux.de"
+ author: Bobus
+
+ - title: "Office Space and Modoros"
+ author: Zsolt Botykai
+
+
- version: 0.7.49
date: 2011-03-11
@@ -47,7 +114,7 @@
- title: "When setting covers in calibre, resize to fit within a maximum size of (1200, 1600), to prevent slowdowns due to extra large covers. This size can be controlled via Preferences->Tweaks."
tickets: [9277]
-
+
bug fixes:
- title: "Fix long standing bug that caused errors when saving books to disk if the book metadata has certain chinese/russian characters on windows. The fix required some changes to how unicode paths are handled in calibre, so it might have broken something else. If so, please open a ticket."
tickets: [7250]
diff --git a/resources/default_tweaks.py b/resources/default_tweaks.py
index 38c1685b7c..464c9d2cfd 100644
--- a/resources/default_tweaks.py
+++ b/resources/default_tweaks.py
@@ -355,3 +355,11 @@ draw_hidden_section_indicators = True
# large covers
maximum_cover_size = (1200, 1600)
+#: Where to send downloaded news
+# When automatically sending downloaded news to a connected device, calibre
+# will by default send it to the main memory. By changing this tweak, you can
+# control where it is sent. Valid values are "main", "carda", "cardb". Note
+# that if there isn't enough free space available on the location you choose,
+# the files will be sent to the location with the most free space.
+send_news_to_device_location = "main"
+
diff --git a/resources/images/news/DrawAndCook.png b/resources/images/news/DrawAndCook.png
new file mode 100644
index 0000000000..8b40b75344
Binary files /dev/null and b/resources/images/news/DrawAndCook.png differ
diff --git a/resources/recipes/DrawAndCook.recipe b/resources/recipes/DrawAndCook.recipe
index 1c080b85db..8db4f71014 100644
--- a/resources/recipes/DrawAndCook.recipe
+++ b/resources/recipes/DrawAndCook.recipe
@@ -1,8 +1,11 @@
from calibre.web.feeds.news import BasicNewsRecipe
+import re
class DrawAndCook(BasicNewsRecipe):
title = 'DrawAndCook'
__author__ = 'Starson17'
+ __version__ = 'v1.10'
+ __date__ = '13 March 2011'
description = 'Drawings of recipes!'
language = 'en'
publisher = 'Starson17'
@@ -13,6 +16,7 @@ class DrawAndCook(BasicNewsRecipe):
remove_javascript = True
remove_empty_feeds = True
cover_url = 'http://farm5.static.flickr.com/4043/4471139063_4dafced67f_o.jpg'
+ INDEX = 'http://www.theydrawandcook.com'
max_articles_per_feed = 30
remove_attributes = ['style', 'font']
@@ -34,20 +38,21 @@ class DrawAndCook(BasicNewsRecipe):
date = ''
current_articles = []
soup = self.index_to_soup(url)
- recipes = soup.findAll('div', attrs={'class': 'date-outer'})
+ featured_major_slider = soup.find(name='div', attrs={'id':'featured_major_slider'})
+ recipes = featured_major_slider.findAll('li', attrs={'data-id': re.compile(r'artwork_entry_\d+', re.DOTALL)})
for recipe in recipes:
- title = recipe.h3.a.string
- page_url = recipe.h3.a['href']
+ page_url = self.INDEX + recipe.a['href']
+ print 'page_url is: ', page_url
+ title = recipe.find('strong').string
+ print 'title is: ', title
current_articles.append({'title': title, 'url': page_url, 'description':'', 'date':date})
return current_articles
-
- keep_only_tags = [dict(name='h3', attrs={'class':'post-title entry-title'})
- ,dict(name='div', attrs={'class':'post-body entry-content'})
+ keep_only_tags = [dict(name='h1', attrs={'id':'page_title'})
+ ,dict(name='section', attrs={'id':'artwork'})
]
- remove_tags = [dict(name='div', attrs={'class':['separator']})
- ,dict(name='div', attrs={'class':['post-share-buttons']})
+ remove_tags = [dict(name='article', attrs={'id':['recipe_actions', 'metadata']})
]
extra_css = '''
diff --git a/resources/recipes/evangelizo.recipe b/resources/recipes/evangelizo.recipe
new file mode 100644
index 0000000000..81ac74bc25
--- /dev/null
+++ b/resources/recipes/evangelizo.recipe
@@ -0,0 +1,21 @@
+import re
+from calibre.web.feeds.news import BasicNewsRecipe
+
+class Evangelizo(BasicNewsRecipe):
+ title = 'Evangelizo.org'
+ oldest_article = 2
+ max_articles_per_feed = 30
+ language = 'de'
+ __author__ = 'Bobus'
+ feeds = [
+ ('EvangleliumTagfuerTag', 'http://www.evangeliumtagfuertag.org/rss/evangelizo_rss-de.xml'),
+ ]
+ use_embedded_content = True
+ preprocess_regexps = [
+ (re.compile(r'<font size="-2">([(][0-9]*[)])</font>'), r'\g<1>'),
+ (re.compile(r'([\.!]\n)'), r'\g<1>
'),
+ ]
+
+ def populate_article_metadata(self, article, soup, first):
+ article.title = re.sub(r'([(][0-9]*[)])', r'\g<1>', article.title)
+ return
diff --git a/resources/recipes/instapaper.recipe b/resources/recipes/instapaper.recipe
index 73c32d08a7..0eb5cf0f09 100644
--- a/resources/recipes/instapaper.recipe
+++ b/resources/recipes/instapaper.recipe
@@ -1,23 +1,12 @@
-__license__ = 'GPL v3'
-__copyright__ = '2009-2010, Darko Miletic
Cat’s Cradle by Vonnegut
' """ - # fix: hackish + # fix: hackish text = re.sub(r'"\Z', '\" ', text) glyph_search = ( - re.compile(r"(\w)\'(\w)"), # apostrophe's - re.compile(r'(\s)\'(\d+\w?)\b(?!\')'), # back in '88 - re.compile(r'(\S)\'(?=\s|'+self.pnct+'|<|$)'), # single closing + re.compile(r'(\d+\'?\"?)( ?)x( ?)(?=\d+)'), # dimension sign + re.compile(r"(\w)\'(\w)"), # apostrophe's + re.compile(r'(\s)\'(\d+\w?)\b(?!\')'), # back in '88 + re.compile(r'(\S)\'(?=\s|'+self.pnct+'|<|$)'), # single closing re.compile(r'\'/'), # single opening - re.compile(r'(\S)\"(?=\s|'+self.pnct+'|<|$)'), # double closing + re.compile(r'(\")\"'), # double closing - following another + re.compile(r'(\S)\"(?=\s|'+self.pnct+'|<|$)'), # double closing re.compile(r'"'), # double opening re.compile(r'\b([A-Z][A-Z0-9]{2,})\b(?:[(]([^)]*)[)])'), # 3+ uppercase acronym re.compile(r'\b([A-Z][A-Z\'\-]+[A-Z])(?=[\s.,\)>])'), # 3+ uppercase - re.compile(r'\b(\s{0,1})?\.{3}'), # ellipsis + re.compile(r'\b(\s{0,1})?\.{3}'), # ellipsis re.compile(r'(\s?)--(\s?)'), # em dash re.compile(r'\s-(?:\s|$)'), # en dash - re.compile(r'(\d+)( ?)x( ?)(?=\d+)'), # dimension sign - re.compile(r'\b ?[([]TM[])]', re.I), # trademark - re.compile(r'\b ?[([]R[])]', re.I), # registered - re.compile(r'\b ?[([]C[])]', re.I), # copyright + re.compile(r'\b( ?)[([]TM[])]', re.I), # trademark + re.compile(r'\b( ?)[([]R[])]', re.I), # registered + re.compile(r'\b( ?)[([]C[])]', re.I) # copyright ) glyph_replace = [x % dict(self.glyph_defaults) for x in ( - r'\1%(txt_apostrophe)s\2', # apostrophe's - r'\1%(txt_apostrophe)s\2', # back in '88 + r'\1\2%(txt_dimension)s\3', # dimension sign + r'\1%(txt_apostrophe)s\2', # apostrophe's + r'\1%(txt_apostrophe)s\2', # back in '88 r'\1%(txt_quote_single_close)s', # single closing - r'%(txt_quote_single_open)s', # single opening - r'\1%(txt_quote_double_close)s', # double closing - r'%(txt_quote_double_open)s', # double opening + r'%(txt_quote_single_open)s', # single opening + r'\1%(txt_quote_double_close)s', # double closing - following another + r'\1%(txt_quote_double_close)s', # double closing + r'%(txt_quote_double_open)s', # double opening r'\1', # 3+ uppercase acronym r'\1', # 3+ uppercase - r'\1%(txt_ellipsis)s', # ellipsis + r'\1%(txt_ellipsis)s', # ellipsis r'\1%(txt_emdash)s\2', # em dash r' %(txt_endash)s ', # en dash - r'\1\2%(txt_dimension)s\3', # dimension sign - r'%(txt_trademark)s', # trademark - r'%(txt_registered)s', # registered - r'%(txt_copyright)s', # copyright + r'\1%(txt_trademark)s', # trademark + r'\1%(txt_registered)s', # registered + r'\1%(txt_copyright)s' # copyright )] + if re.search(r'{.+?}', text): + glyph_search += ( + re.compile(r'{(c\||\|c)}'), # cent + re.compile(r'{(L-|-L)}'), # pound + re.compile(r'{(Y=|=Y)}'), # yen + re.compile(r'{\(c\)}'), # copyright + re.compile(r'{\(r\)}'), # registered + re.compile(r'{1/4}'), # quarter + re.compile(r'{1/2}'), # half + re.compile(r'{3/4}'), # three-quarter + re.compile(r'{(A`|`A)}'), # 192; + re.compile(r'{(A\'|\'A)}'), # 193; + re.compile(r'{(A\^|\^A)}'), # 194; + re.compile(r'{(A~|~A)}'), # 195; + re.compile(r'{(A\"|\"A)}'), # 196; + re.compile(r'{(Ao|oA)}'), # 197; + re.compile(r'{(AE)}'), # 198; + re.compile(r'{(C,|,C)}'), # 199; + re.compile(r'{(E`|`E)}'), # 200; + re.compile(r'{(E\'|\'E)}'), # 201; + re.compile(r'{(E\^|\^E)}'), # 202; + re.compile(r'{(E\"|\"E)}'), # 203; + re.compile(r'{(I`|`I)}'), # 204; + re.compile(r'{(I\'|\'I)}'), # 205; + re.compile(r'{(I\^|\^I)}'), # 206; + re.compile(r'{(I\"|\"I)}'), # 207; + re.compile(r'{(D-|-D)}'), # 208; + re.compile(r'{(N~|~N)}'), # 209; + re.compile(r'{(O`|`O)}'), # 210; + re.compile(r'{(O\'|\'O)}'), # 211; + re.compile(r'{(O\^|\^O)}'), # 212; + re.compile(r'{(O~|~O)}'), # 213; + re.compile(r'{(O\"|\"O)}'), # 214; + re.compile(r'{(O\/|\/O)}'), # 215; + re.compile(r'{(U`|`U)}'), # 216; + re.compile(r'{(U\'|\'U)}'), # 217; + re.compile(r'{(U\^|\^U)}'), # 218; + re.compile(r'{(U\"|\"U)}'), # 219; + re.compile(r'{(Y\'|\'Y)}'), # 220; + re.compile(r'{(a`|`a)}'), # a-grace + re.compile(r'{(a\'|\'a)}'), # a-acute + re.compile(r'{(a\^|\^a)}'), # a-circumflex + re.compile(r'{(a~|~a)}'), # a-tilde + re.compile(r'{(a\"|\"a)}'), # a-diaeresis + re.compile(r'{(ao|oa)}'), # a-ring + re.compile(r'{ae}'), # ae + re.compile(r'{(c,|,c)}'), # c-cedilla + re.compile(r'{(e`|`e)}'), # e-grace + re.compile(r'{(e\'|\'e)}'), # e-acute + re.compile(r'{(e\^|\^e)}'), # e-circumflex + re.compile(r'{(e\"|\"e)}'), # e-diaeresis + re.compile(r'{(i`|`i)}'), # i-grace + re.compile(r'{(i\'|\'i)}'), # i-acute + re.compile(r'{(i\^|\^i)}'), # i-circumflex + re.compile(r'{(i\"|\"i)}'), # i-diaeresis + re.compile(r'{(n~|~n)}'), # n-tilde + re.compile(r'{(o`|`o)}'), # o-grace + re.compile(r'{(o\'|\'o)}'), # o-acute + re.compile(r'{(o\^|\^o)}'), # o-circumflex + re.compile(r'{(o~|~o)}'), # o-tilde + re.compile(r'{(o\"|\"o)}'), # o-diaeresis + re.compile(r'{(o\/|\/o)}'), # o-stroke + re.compile(r'{(u`|`u)}'), # u-grace + re.compile(r'{(u\'|\'u)}'), # u-acute + re.compile(r'{(u\^|\^u)}'), # u-circumflex + re.compile(r'{(u\"|\"u)}'), # u-diaeresis + re.compile(r'{(y\'|\'y)}'), # y-acute + re.compile(r'{(y\"|\"y)}'), # y-diaeresis + re.compile(r'{OE}'), # y-diaeresis + re.compile(r'{oe}'), # y-diaeresis + re.compile(r'{\*}'), # bullet + re.compile(r'{Fr}'), # Franc + re.compile(r'{(L=|=L)}'), # Lira + re.compile(r'{Rs}'), # Rupee + re.compile(r'{(C=|=C)}'), # euro + re.compile(r'{tm}'), # euro + re.compile(r'{spade}'), # spade + re.compile(r'{club}'), # club + re.compile(r'{heart}'), # heart + re.compile(r'{diamond}') # diamond + ) + + glyph_replace += [x % dict(self.glyph_defaults) for x in ( + r'%(mac_cent)s', # cent + r'%(mac_pound)s', # pound + r'%(mac_yen)s', # yen + r'%(txt_copyright)s', # copyright + r'%(txt_registered)s', # registered + r'%(mac_quarter)s', # quarter + r'%(mac_half)s', # half + r'%(mac_three-quarter)s', # three-quarter + r'%(mac_cA-grave)s', # 192; + r'%(mac_cA-acute)s', # 193; + r'%(mac_cA-circumflex)s', # 194; + r'%(mac_cA-tilde)s', # 195; + r'%(mac_cA-diaeresis)s', # 196; + r'%(mac_cA-ring)s', # 197; + r'%(mac_cAE)s', # 198; + r'%(mac_cC-cedilla)s', # 199; + r'%(mac_cE-grave)s', # 200; + r'%(mac_cE-acute)s', # 201; + r'%(mac_cE-circumflex)s', # 202; + r'%(mac_cE-diaeresis)s', # 203; + r'%(mac_cI-grave)s', # 204; + r'%(mac_cI-acute)s', # 205; + r'%(mac_cI-circumflex)s', # 206; + r'%(mac_cI-diaeresis)s', # 207; + r'%(mac_cEth)s', # 208; + r'%(mac_cN-tilde)s', # 209; + r'%(mac_cO-grave)s', # 210; + r'%(mac_cO-acute)s', # 211; + r'%(mac_cO-circumflex)s', # 212; + r'%(mac_cO-tilde)s', # 213; + r'%(mac_cO-diaeresis)s', # 214; + r'%(mac_cO-stroke)s', # 216; + r'%(mac_cU-grave)s', # 217; + r'%(mac_cU-acute)s', # 218; + r'%(mac_cU-circumflex)s', # 219; + r'%(mac_cU-diaeresis)s', # 220; + r'%(mac_cY-acute)s', # 221; + r'%(mac_sa-grave)s', # 224; + r'%(mac_sa-acute)s', # 225; + r'%(mac_sa-circumflex)s', # 226; + r'%(mac_sa-tilde)s', # 227; + r'%(mac_sa-diaeresis)s', # 228; + r'%(mac_sa-ring)s', # 229; + r'%(mac_sae)s', # 230; + r'%(mac_sc-cedilla)s', # 231; + r'%(mac_se-grave)s', # 232; + r'%(mac_se-acute)s', # 233; + r'%(mac_se-circumflex)s', # 234; + r'%(mac_se-diaeresis)s', # 235; + r'%(mac_si-grave)s', # 236; + r'%(mac_si-acute)s', # 237; + r'%(mac_si-circumflex)s', # 238; + r'%(mac_si-diaeresis)s', # 239; + r'%(mac_sn-tilde)s', # 241; + r'%(mac_so-grave)s', # 242; + r'%(mac_so-acute)s', # 243; + r'%(mac_so-circumflex)s', # 244; + r'%(mac_so-tilde)s', # 245; + r'%(mac_so-diaeresis)s', # 246; + r'%(mac_so-stroke)s', # 248; + r'%(mac_su-grave)s', # 249; + r'%(mac_su-acute)s', # 250; + r'%(mac_su-circumflex)s', # 251; + r'%(mac_su-diaeresis)s', # 252; + r'%(mac_sy-acute)s', # 253; + r'%(mac_sy-diaeresis)s', # 255; + r'%(mac_cOE)s', # 338; + r'%(mac_soe)s', # 339; + r'%(mac_bullet)s', # bullet + r'%(mac_franc)s', # franc + r'%(mac_lira)s', # lira + r'%(mac_rupee)s', # rupee + r'%(mac_euro)s', # euro + r'%(txt_trademark)s', # trademark + r'%(mac_spade)s', # spade + r'%(mac_club)s', # club + r'%(mac_heart)s', # heart + r'%(mac_diamond)s' # diamond + )] + result = [] for line in re.compile(r'(<.*?>)', re.U).split(text): if not re.search(r'<.*>', line): @@ -807,7 +1049,7 @@ class Textile(object): for qtag in qtags: pattern = re.compile(r""" - (?:^|(?<=[\s>%(pnct)s])|([\]}])) + (?:^|(?<=[\s>%(pnct)s])|\[|([\]}])) (%(qtag)s)(?!%(qtag)s) (%(c)s) (?::(\S+))? diff --git a/src/calibre/gui2/actions/view.py b/src/calibre/gui2/actions/view.py index 7b14de8176..a606ca09bc 100644 --- a/src/calibre/gui2/actions/view.py +++ b/src/calibre/gui2/actions/view.py @@ -34,6 +34,13 @@ class ViewAction(InterfaceAction): self.qaction.setMenu(self.view_menu) ac.triggered.connect(self.view_specific_format, type=Qt.QueuedConnection) + self.view_menu.addSeparator() + ac = self.create_action(spec=(_('Read a random book'), 'catalog.png', + None, None), attr='action_pick_random') + ac.triggered.connect(self.view_random) + self.view_menu.addAction(ac) + + def location_selected(self, loc): enabled = loc == 'library' for action in list(self.view_menu.actions())[1:]: @@ -151,6 +158,10 @@ class ViewAction(InterfaceAction): def view_specific_book(self, index): self._view_books([index]) + def view_random(self, *args): + self.gui.iactions['Choose Library'].pick_random() + self._view_books([self.gui.library_view.currentIndex()]) + def _view_books(self, rows): if not rows or len(rows) == 0: self._launch_viewer() diff --git a/src/calibre/gui2/convert/search_and_replace.py b/src/calibre/gui2/convert/search_and_replace.py index 88446344ec..c2241ff8eb 100644 --- a/src/calibre/gui2/convert/search_and_replace.py +++ b/src/calibre/gui2/convert/search_and_replace.py @@ -6,6 +6,8 @@ __docformat__ = 'restructuredtext en' import re +from PyQt4.Qt import QLineEdit, QTextEdit + from calibre.gui2.convert.search_and_replace_ui import Ui_Form from calibre.gui2.convert import Widget from calibre.gui2 import error_dialog @@ -72,3 +74,13 @@ class SearchAndReplaceWidget(Widget, Ui_Form): _('Invalid regular expression: %s')%err, show=True) return False return True + + def get_vaule(self, g): + if isinstance(g, (QLineEdit, QTextEdit)): + func = getattr(g, 'toPlainText', getattr(g, 'text', None))() + ans = unicode(func) + if not ans: + ans = None + return ans + else: + return Widget.get_value(self, g) diff --git a/src/calibre/gui2/cover_flow.py b/src/calibre/gui2/cover_flow.py index cb951b09be..1d79d93bb2 100644 --- a/src/calibre/gui2/cover_flow.py +++ b/src/calibre/gui2/cover_flow.py @@ -53,7 +53,7 @@ if pictureflow is not None: def __init__(self, model, buffer=20): pictureflow.FlowImages.__init__(self) self.model = model - self.model.modelReset.connect(self.reset) + self.model.modelReset.connect(self.reset, type=Qt.QueuedConnection) def count(self): return self.model.count() @@ -83,6 +83,8 @@ if pictureflow is not None: class CoverFlow(pictureflow.PictureFlow): + dc_signal = pyqtSignal() + def __init__(self, parent=None): pictureflow.PictureFlow.__init__(self, parent, config['cover_flow_queue_length']+1) @@ -90,6 +92,8 @@ if pictureflow is not None: self.setFocusPolicy(Qt.WheelFocus) self.setSizePolicy(QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)) + self.dc_signal.connect(self._data_changed, + type=Qt.QueuedConnection) def sizeHint(self): return self.minimumSize() @@ -101,6 +105,12 @@ if pictureflow is not None: elif ev.delta() > 0: self.showPrevious() + def dataChanged(self): + self.dc_signal.emit() + + def _data_changed(self): + pictureflow.PictureFlow.dataChanged(self) + else: CoverFlow = None @@ -135,8 +145,7 @@ class CoverFlowMixin(object): self.cover_flow = None if CoverFlow is not None: self.cf_last_updated_at = None - self.cover_flow_sync_timer = QTimer(self) - self.cover_flow_sync_timer.timeout.connect(self.cover_flow_do_sync) + self.cover_flow_syncing_enabled = False self.cover_flow_sync_flag = True self.cover_flow = CoverFlow(parent=self) self.cover_flow.currentChanged.connect(self.sync_listview_to_cf) @@ -179,14 +188,15 @@ class CoverFlowMixin(object): self.cover_flow.setFocus(Qt.OtherFocusReason) if CoverFlow is not None: self.cover_flow.setCurrentSlide(self.library_view.currentIndex().row()) - self.cover_flow_sync_timer.start(500) + self.cover_flow_syncing_enabled = True + QTimer.singleShot(500, self.cover_flow_do_sync) self.library_view.setCurrentIndex( self.library_view.currentIndex()) self.library_view.scroll_to_row(self.library_view.currentIndex().row()) def cover_browser_hidden(self): if CoverFlow is not None: - self.cover_flow_sync_timer.stop() + self.cover_flow_syncing_enabled = False idx = self.library_view.model().index(self.cover_flow.currentSlide(), 0) if idx.isValid(): sm = self.library_view.selectionModel() @@ -242,6 +252,8 @@ class CoverFlowMixin(object): except: import traceback traceback.print_exc() + if self.cover_flow_syncing_enabled: + QTimer.singleShot(500, self.cover_flow_do_sync) def sync_listview_to_cf(self, row): self.cf_last_updated_at = time.time() diff --git a/src/calibre/gui2/device.py b/src/calibre/gui2/device.py index 2cbecc134c..215e67c46f 100644 --- a/src/calibre/gui2/device.py +++ b/src/calibre/gui2/device.py @@ -1052,11 +1052,13 @@ class DeviceMixin(object): # {{{ except: pass total_size = self.location_manager.free[0] - if self.location_manager.free[0] > total_size + (1024**2): + loc = tweaks['send_news_to_device_location'] + loc_index = {"carda": 1, "cardb": 2}.get(loc, 0) + if self.location_manager.free[loc_index] > total_size + (1024**2): # Send news to main memory if enough space available # as some devices like the Nook Color cannot handle # periodicals on SD cards properly - on_card = None + on_card = loc if loc in ('carda', 'cardb') else None self.upload_books(files, names, metadata, on_card=on_card, memory=[files, remove]) diff --git a/src/calibre/gui2/dialogs/check_library.py b/src/calibre/gui2/dialogs/check_library.py index f9db87b087..560090d2b3 100644 --- a/src/calibre/gui2/dialogs/check_library.py +++ b/src/calibre/gui2/dialogs/check_library.py @@ -202,13 +202,19 @@ class CheckLibraryDialog(QDialog):Delete marked is used to remove extra files/folders/covers that have no entries in the database. Check the box next to the item you want to delete. Use with caution.
-Fix marked is applicable only to covers (the two lines marked - 'fixable'). In the case of missing cover files, checking the fixable - box and pushing this button will remove the cover mark from the - database for all the files in that category. In the case of extra - cover files, checking the fixable box and pushing this button will - add the cover mark to the database for all the files in that - category.
+ +Fix marked is applicable only to covers and missing formats + (the three lines marked 'fixable'). In the case of missing cover files, + checking the fixable box and pushing this button will tell calibre that + there is no cover for all of the books listed. Use this option if you + are not going to restore the covers from a backup. In the case of extra + cover files, checking the fixable box and pushing this button will tell + calibre that the cover files it found are correct for all the books + listed. Use this when you are not going to delete the file(s). In the + case of missing formats, checking the fixable box and pushing this + button will tell calibre that the formats are really gone. Use this if + you are not going to restore the formats from a backup.
+ ''')) self.log = QTreeWidget(self) @@ -381,6 +387,19 @@ class CheckLibraryDialog(QDialog): unicode(it.text(1)))) self.run_the_check() + def fix_missing_formats(self): + tl = self.top_level_items['missing_formats'] + child_count = tl.childCount() + for i in range(0, child_count): + item = tl.child(i); + id = item.data(0, Qt.UserRole).toInt()[0] + all = self.db.formats(id, index_is_id=True, verify_formats=False) + all = set([f.strip() for f in all.split(',')]) if all else set() + valid = self.db.formats(id, index_is_id=True, verify_formats=True) + valid = set([f.strip() for f in valid.split(',')]) if valid else set() + for fmt in all-valid: + self.db.remove_format(id, fmt, index_is_id=True, db_only=True) + def fix_missing_covers(self): tl = self.top_level_items['missing_covers'] child_count = tl.childCount() diff --git a/src/calibre/gui2/dialogs/metadata_bulk.py b/src/calibre/gui2/dialogs/metadata_bulk.py index d918991aad..9b25545252 100644 --- a/src/calibre/gui2/dialogs/metadata_bulk.py +++ b/src/calibre/gui2/dialogs/metadata_bulk.py @@ -783,6 +783,12 @@ class MetadataBulkDialog(ResizableDialog, Ui_MetadataBulkDialog): books_to_refresh = self.db.set_custom(id, val, label=dfm['label'], extra=extra, commit=False, allow_case_change=True) + elif dest.startswith('#') and dest.endswith('_index'): + label = self.db.field_metadata[dest[:-6]]['label'] + series = self.db.get_custom(id, label=label, index_is_id=True) + books_to_refresh = self.db.set_custom(id, series, label=label, + extra=val, commit=False, + allow_case_change=True) else: if dest == 'comments': setter = self.db.set_comment diff --git a/src/calibre/gui2/dialogs/saved_search_editor.py b/src/calibre/gui2/dialogs/saved_search_editor.py index 1143a6f06a..c9f843109a 100644 --- a/src/calibre/gui2/dialogs/saved_search_editor.py +++ b/src/calibre/gui2/dialogs/saved_search_editor.py @@ -9,12 +9,13 @@ from PyQt4.QtGui import QDialog from calibre.gui2.dialogs.saved_search_editor_ui import Ui_SavedSearchEditor from calibre.utils.search_query_parser import saved_searches from calibre.utils.icu import sort_key +from calibre.gui2 import error_dialog from calibre.gui2.dialogs.confirm_delete import confirm class SavedSearchEditor(QDialog, Ui_SavedSearchEditor): - def __init__(self, window, initial_search=None): - QDialog.__init__(self, window) + def __init__(self, parent, initial_search=None): + QDialog.__init__(self, parent) Ui_SavedSearchEditor.__init__(self) self.setupUi(self) @@ -22,12 +23,13 @@ class SavedSearchEditor(QDialog, Ui_SavedSearchEditor): self.connect(self.search_name_box, SIGNAL('currentIndexChanged(int)'), self.current_index_changed) self.connect(self.delete_search_button, SIGNAL('clicked()'), self.del_search) + self.rename_button.clicked.connect(self.rename_search) self.current_search_name = None self.searches = {} - self.searches_to_delete = [] for name in saved_searches().names(): self.searches[name] = saved_searches().lookup(name) + self.search_names = set([icu_lower(n) for n in saved_searches().names()]) self.populate_search_list() if initial_search is not None and initial_search in self.searches: @@ -42,6 +44,11 @@ class SavedSearchEditor(QDialog, Ui_SavedSearchEditor): search_name = unicode(self.input_box.text()).strip() if search_name == '': return False + if icu_lower(search_name) in self.search_names: + error_dialog(self, _('Saved search already exists'), + _('The saved search %s already exists, perhaps with ' + 'different case')%search_name).exec_() + return False if search_name not in self.searches: self.searches[search_name] = '' self.populate_search_list() @@ -57,10 +64,25 @@ class SavedSearchEditor(QDialog, Ui_SavedSearchEditor): +'', 'saved_search_editor_delete', self): return del self.searches[self.current_search_name] - self.searches_to_delete.append(self.current_search_name) self.current_search_name = None self.search_name_box.removeItem(self.search_name_box.currentIndex()) + def rename_search(self): + new_search_name = unicode(self.input_box.text()).strip() + if new_search_name == '': + return False + if icu_lower(new_search_name) in self.search_names: + error_dialog(self, _('Saved search already exists'), + _('The saved search %s already exists, perhaps with ' + 'different case')%new_search_name).exec_() + return False + if self.current_search_name in self.searches: + self.searches[new_search_name] = self.searches[self.current_search_name] + del self.searches[self.current_search_name] + self.populate_search_list() + self.select_search(new_search_name) + return True + def select_search(self, name): self.search_name_box.setCurrentIndex(self.search_name_box.findText(name)) @@ -78,7 +100,7 @@ class SavedSearchEditor(QDialog, Ui_SavedSearchEditor): def accept(self): if self.current_search_name: self.searches[self.current_search_name] = unicode(self.search_text.toPlainText()) - for name in self.searches_to_delete: + for name in saved_searches().names(): saved_searches().delete(name) for name in self.searches: saved_searches().add(name, self.searches[name]) diff --git a/src/calibre/gui2/dialogs/saved_search_editor.ui b/src/calibre/gui2/dialogs/saved_search_editor.ui index 3ba37bdf10..99672b5b8e 100644 --- a/src/calibre/gui2/dialogs/saved_search_editor.ui +++ b/src/calibre/gui2/dialogs/saved_search_editor.ui @@ -134,6 +134,20 @@ +' +
_('You have started calibre in debug mode. After you '
@@ -435,6 +438,7 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
elif msg.startswith('refreshdb:'):
self.library_view.model().refresh()
self.library_view.model().research()
+ self.tags_view.recount()
else:
print msg
@@ -499,6 +503,9 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
self.card_a_view.reset()
self.card_b_view.reset()
self.device_manager.set_current_library_uuid(db.library_id)
+ # Run a garbage collection now so that it does not freeze the
+ # interface later
+ gc.collect()
def set_window_title(self):
@@ -685,6 +692,8 @@ class Main(MainWindow, MainWindowMixin, DeviceMixin, EmailMixin, # {{{
pass
time.sleep(2)
self.hide_windows()
+ # Do not report any errors that happen after the shutdown
+ sys.excepthook = sys.__excepthook__
return True
def run_wizard(self, *args):
diff --git a/src/calibre/gui2/viewer/main.py b/src/calibre/gui2/viewer/main.py
index 964616ab48..c704b98dc9 100644
--- a/src/calibre/gui2/viewer/main.py
+++ b/src/calibre/gui2/viewer/main.py
@@ -225,6 +225,12 @@ class EbookViewer(MainWindow, Ui_EbookViewer):
self.action_quit.setShortcuts(qs)
self.connect(self.action_quit, SIGNAL('triggered(bool)'),
lambda x:QApplication.instance().quit())
+ self.action_focus_search = QAction(self)
+ self.addAction(self.action_focus_search)
+ self.action_focus_search.setShortcuts([Qt.Key_Slash,
+ QKeySequence(QKeySequence.Find)])
+ self.action_focus_search.triggered.connect(lambda x:
+ self.search.setFocus(Qt.OtherFocusReason))
self.action_copy.setDisabled(True)
self.action_metadata.setCheckable(True)
self.action_metadata.setShortcut(Qt.CTRL+Qt.Key_I)
@@ -293,6 +299,9 @@ class EbookViewer(MainWindow, Ui_EbookViewer):
ca.setShortcut(QKeySequence.Copy)
self.addAction(ca)
self.open_history_menu = QMenu()
+ self.clear_recent_history_action = QAction(
+ _('Clear list of recently opened books'), self)
+ self.clear_recent_history_action.triggered.connect(self.clear_recent_history)
self.build_recent_menu()
self.action_open_ebook.setMenu(self.open_history_menu)
self.open_history_menu.triggered[QAction].connect(self.open_recent)
@@ -301,11 +310,19 @@ class EbookViewer(MainWindow, Ui_EbookViewer):
self.restore_state()
+ def clear_recent_history(self, *args):
+ vprefs.set('viewer_open_history', [])
+ self.build_recent_menu()
+
def build_recent_menu(self):
m = self.open_history_menu
m.clear()
+ recent = vprefs.get('viewer_open_history', [])
+ if recent:
+ m.addAction(self.clear_recent_history_action)
+ m.addSeparator()
count = 0
- for path in vprefs.get('viewer_open_history', []):
+ for path in recent:
if count > 9:
break
if os.path.exists(path):
@@ -494,12 +511,6 @@ class EbookViewer(MainWindow, Ui_EbookViewer):
if self.view.search(text, backwards=backwards):
self.scrolled(self.view.scroll_fraction)
- def keyPressEvent(self, event):
- if event.key() == Qt.Key_Slash:
- self.search.setFocus(Qt.OtherFocusReason)
- else:
- return MainWindow.keyPressEvent(self, event)
-
def internal_link_clicked(self, frac):
self.history.add(self.pos.value())
diff --git a/src/calibre/gui2/wizard/send_email.py b/src/calibre/gui2/wizard/send_email.py
index 5785f52276..44cd8dd2e4 100644
--- a/src/calibre/gui2/wizard/send_email.py
+++ b/src/calibre/gui2/wizard/send_email.py
@@ -92,7 +92,8 @@ class SendEmail(QWidget, Ui_Form):
pa = self.preferred_to_address()
to_set = pa is not None
if self.set_email_settings(to_set):
- if question_dialog(self, _('OK to proceed?'),
+ opts = smtp_prefs().parse()
+ if not opts.relay_password or question_dialog(self, _('OK to proceed?'),
_('This will display your email password on the screen'
'. Is it OK to proceed?'), show_copy_button=False):
TestEmail(pa, self).exec_()
@@ -204,19 +205,32 @@ class SendEmail(QWidget, Ui_Form):
username = unicode(self.relay_username.text()).strip()
password = unicode(self.relay_password.text()).strip()
host = unicode(self.relay_host.text()).strip()
- if host and not (username and password):
- error_dialog(self, _('Bad configuration'),
- _('You must set the username and password for '
- 'the mail server.')).exec_()
- return False
+ enc_method = ('TLS' if self.relay_tls.isChecked() else 'SSL'
+ if self.relay_ssl.isChecked() else 'NONE')
+ if host:
+ # Validate input
+ if ((username and not password) or (not username and password)):
+ error_dialog(self, _('Bad configuration'),
+ _('You must either set both the username and password for '
+ 'the mail server or no username and no password at all.')).exec_()
+ return False
+ if not username and not password and enc_method != 'NONE':
+ error_dialog(self, _('Bad configuration'),
+ _('Please enter a username and password or set'
+ ' encryption to None ')).exec_()
+ return False
+ if not (username and password) and not question_dialog(self,
+ _('Are you sure?'),
+ _('No username and password set for mailserver. Most '
+ ' mailservers need a username and password. Are you sure?')):
+ return False
conf = smtp_prefs()
conf.set('from_', from_)
conf.set('relay_host', host if host else None)
conf.set('relay_port', self.relay_port.value())
conf.set('relay_username', username if username else None)
conf.set('relay_password', hexlify(password))
- conf.set('encryption', 'TLS' if self.relay_tls.isChecked() else 'SSL'
- if self.relay_ssl.isChecked() else 'NONE')
+ conf.set('encryption', enc_method)
return True
diff --git a/src/calibre/library/caches.py b/src/calibre/library/caches.py
index 97ddaeb51a..19ef7e213c 100644
--- a/src/calibre/library/caches.py
+++ b/src/calibre/library/caches.py
@@ -123,14 +123,22 @@ REGEXP_MATCH = 2
def _match(query, value, matchkind):
if query.startswith('..'):
query = query[1:]
- prefix_match_ok = False
+ sq = query[1:]
+ internal_match_ok = True
else:
- prefix_match_ok = True
+ internal_match_ok = False
for t in value:
t = icu_lower(t)
try: ### ignore regexp exceptions, required because search-ahead tries before typing is finished
if (matchkind == EQUALS_MATCH):
- if prefix_match_ok and query[0] == '.':
+ if internal_match_ok:
+ if query == t:
+ return True
+ comps = [c.strip() for c in t.split('.') if c.strip()]
+ for comp in comps:
+ if sq == comp:
+ return True
+ elif query[0] == '.':
if t.startswith(query[1:]):
ql = len(query) - 1
if (len(t) == ql) or (t[ql:ql+1] == '.'):
@@ -575,6 +583,8 @@ class ResultCache(SearchQueryParser): # {{{
candidates = self.universal_set()
if len(candidates) == 0:
return matches
+ if location not in self.all_search_locations:
+ return matches
if len(location) > 2 and location.startswith('@') and \
location[1:] in self.db_prefs['grouped_search_terms']:
diff --git a/src/calibre/library/check_library.py b/src/calibre/library/check_library.py
index 19ecb97308..6013c76b9c 100644
--- a/src/calibre/library/check_library.py
+++ b/src/calibre/library/check_library.py
@@ -27,7 +27,7 @@ CHECKS = [('invalid_titles', _('Invalid titles'), True, False),
('extra_titles', _('Extra titles'), True, False),
('invalid_authors', _('Invalid authors'), True, False),
('extra_authors', _('Extra authors'), True, False),
- ('missing_formats', _('Missing book formats'), False, False),
+ ('missing_formats', _('Missing book formats'), False, True),
('extra_formats', _('Extra book formats'), True, False),
('extra_files', _('Unknown files in books'), True, False),
('missing_covers', _('Missing covers files'), False, True),
diff --git a/src/calibre/library/database2.py b/src/calibre/library/database2.py
index d03975baea..50a0ba98dd 100644
--- a/src/calibre/library/database2.py
+++ b/src/calibre/library/database2.py
@@ -56,7 +56,7 @@ class Tag(object):
self.is_hierarchical = False
self.is_editable = is_editable
self.is_searchable = is_searchable
- self.id_set = id_set
+ self.id_set = id_set if id_set is not None else set([])
self.avg_rating = avg/2.0 if avg is not None else 0
self.sort = sort
if self.avg_rating > 0:
@@ -1154,15 +1154,18 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
if notify:
self.notify('delete', [id])
- def remove_format(self, index, format, index_is_id=False, notify=True, commit=True):
+ def remove_format(self, index, format, index_is_id=False, notify=True,
+ commit=True, db_only=False):
id = index if index_is_id else self.id(index)
name = self.conn.get('SELECT name FROM data WHERE book=? AND format=?', (id, format), all=False)
if name:
- path = self.format_abspath(id, format, index_is_id=True)
- try:
- delete_file(path)
- except:
- traceback.print_exc()
+ if not db_only:
+ try:
+ path = self.format_abspath(id, format, index_is_id=True)
+ if path:
+ delete_file(path)
+ except:
+ traceback.print_exc()
self.conn.execute('DELETE FROM data WHERE book=? AND format=?', (id, format.upper()))
if commit:
self.conn.commit()
@@ -1207,6 +1210,13 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
return ans
field = self.field_metadata[category]
+ if field['datatype'] == 'composite':
+ dex = field['rec_index']
+ for book in self.data.iterall():
+ if book[dex] == id_:
+ ans.add(book[0])
+ return ans
+
ans = self.conn.get(
'SELECT book FROM books_{tn}_link WHERE {col}=?'.format(
tn=field['table'], col=field['link_column']), (id_,))
@@ -1278,7 +1288,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
# First, build the maps. We need a category->items map and an
# item -> (item_id, sort_val) map to use in the books loop
- for category in tb_cats.keys():
+ for category in tb_cats.iterkeys():
cat = tb_cats[category]
if not cat['is_category'] or cat['kind'] in ['user', 'search'] \
or category in ['news', 'formats'] or cat.get('is_csp',
@@ -1321,8 +1331,15 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
tcategories[category] = {}
# create a list of category/field_index for the books scan to use.
# This saves iterating through field_metadata for each book
- md.append((category, cat['rec_index'], cat['is_multiple']))
+ md.append((category, cat['rec_index'], cat['is_multiple'], False))
+ for category in tb_cats.iterkeys():
+ cat = tb_cats[category]
+ if cat['datatype'] == 'composite' and \
+ cat['display'].get('make_category', False):
+ tcategories[category] = {}
+ md.append((category, cat['rec_index'], cat['is_multiple'],
+ cat['datatype'] == 'composite'))
#print 'end phase "collection":', time.clock() - last, 'seconds'
#last = time.clock()
@@ -1336,11 +1353,22 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
continue
rating = book[rating_dex]
# We kept track of all possible category field_map positions above
- for (cat, dex, mult) in md:
- if book[dex] is None:
+ for (cat, dex, mult, is_comp) in md:
+ if not book[dex]:
continue
if not mult:
val = book[dex]
+ if is_comp:
+ item = tcategories[cat].get(val, None)
+ if not item:
+ item = tag_class(val, val)
+ tcategories[cat][val] = item
+ item.c += 1
+ item.id = val
+ if rating > 0:
+ item.rt += rating
+ item.rc += 1
+ continue
try:
(item_id, sort_val) = tids[cat][val] # let exceptions fly
item = tcategories[cat].get(val, None)
@@ -1402,7 +1430,7 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
# and building the Tag instances.
categories = {}
tag_class = Tag
- for category in tb_cats.keys():
+ for category in tb_cats.iterkeys():
if category not in tcategories:
continue
cat = tb_cats[category]
@@ -1690,10 +1718,20 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
self.notify('metadata', [id])
return books_to_refresh
- def set_metadata(self, id, mi, ignore_errors=False,
- set_title=True, set_authors=True, commit=True):
+ def set_metadata(self, id, mi, ignore_errors=False, set_title=True,
+ set_authors=True, commit=True, force_changes=False):
'''
Set metadata for the book `id` from the `Metadata` object `mi`
+
+ Setting force_changes=True will force set_metadata to update fields even
+ if mi contains empty values. In this case, 'None' is distinguished from
+ 'empty'. If mi.XXX is None, the XXX is not replaced, otherwise it is.
+ The tags, identifiers, and cover attributes are special cases. Tags and
+ identifiers cannot be set to None so then will always be replaced if
+ force_changes is true. You must ensure that mi contains the values you
+ want the book to have. Covers are always changed if a new cover is
+ provided, but are never deleted. Also note that force_changes has no
+ effect on setting title or authors.
'''
if callable(getattr(mi, 'to_book_metadata', None)):
# Handle code passing in a OPF object instead of a Metadata object
@@ -1707,6 +1745,11 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
traceback.print_exc()
else:
raise
+
+ def should_replace_field(attr):
+ return (force_changes and (mi.get(attr, None) is not None)) or \
+ not mi.is_null(attr)
+
path_changed = False
if set_title and mi.title:
self._set_title(id, mi.title)
@@ -1721,16 +1764,21 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
path_changed = True
if path_changed:
self.set_path(id, index_is_id=True)
- if mi.author_sort:
+
+ if should_replace_field('author_sort'):
doit(self.set_author_sort, id, mi.author_sort, notify=False,
commit=False)
- if mi.publisher:
+ if should_replace_field('publisher'):
doit(self.set_publisher, id, mi.publisher, notify=False,
commit=False)
- if mi.rating:
+
+ # Setting rating to zero is acceptable.
+ if mi.rating is not None:
doit(self.set_rating, id, mi.rating, notify=False, commit=False)
- if mi.series:
+ if should_replace_field('series'):
doit(self.set_series, id, mi.series, notify=False, commit=False)
+
+ # force_changes has no effect on cover manipulation
if mi.cover_data[1] is not None:
doit(self.set_cover, id, mi.cover_data[1], commit=False)
elif mi.cover is not None:
@@ -1739,21 +1787,30 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
raw = f.read()
if raw:
doit(self.set_cover, id, raw, commit=False)
- if mi.tags:
+
+ # if force_changes is true, tags are always replaced because the
+ # attribute cannot be set to None.
+ if should_replace_field('tags'):
doit(self.set_tags, id, mi.tags, notify=False, commit=False)
- if mi.comments:
+
+ if should_replace_field('comments'):
doit(self.set_comment, id, mi.comments, notify=False, commit=False)
- if mi.series_index:
+
+ # Setting series_index to zero is acceptable
+ if mi.series_index is not None:
doit(self.set_series_index, id, mi.series_index, notify=False,
commit=False)
- if mi.pubdate:
+ if should_replace_field('pubdate'):
doit(self.set_pubdate, id, mi.pubdate, notify=False, commit=False)
if getattr(mi, 'timestamp', None) is not None:
doit(self.set_timestamp, id, mi.timestamp, notify=False,
commit=False)
+ # identifiers will always be replaced if force_changes is True
mi_idents = mi.get_identifiers()
- if mi_idents:
+ if force_changes:
+ self.set_identifiers(id, mi_idents, notify=False, commit=False)
+ elif mi_idents:
identifiers = self.get_identifiers(id, index_is_id=True)
for key, val in mi_idents.iteritems():
if val and val.strip(): # Don't delete an existing identifier
@@ -1765,10 +1822,10 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
for key in user_mi.iterkeys():
if key in self.field_metadata and \
user_mi[key]['datatype'] == self.field_metadata[key]['datatype']:
- doit(self.set_custom, id,
- val=mi.get(key),
- extra=mi.get_extra(key),
- label=user_mi[key]['label'], commit=False)
+ val = mi.get(key, None)
+ if force_changes or val is not None:
+ doit(self.set_custom, id, val=val, extra=mi.get_extra(key),
+ label=user_mi[key]['label'], commit=False)
if commit:
self.conn.commit()
self.notify('metadata', [id])
@@ -2358,6 +2415,8 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
@param tags: list of strings
@param append: If True existing tags are not removed
'''
+ if not tags:
+ tags = []
if not append:
self.conn.execute('DELETE FROM books_tags_link WHERE book=?', (id,))
self.conn.execute('''DELETE FROM tags WHERE (SELECT COUNT(id)
@@ -2508,6 +2567,8 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
self.notify('metadata', [id])
def set_rating(self, id, rating, notify=True, commit=True):
+ if not rating:
+ rating = 0
rating = int(rating)
self.conn.execute('DELETE FROM books_ratings_link WHERE book=?',(id,))
rat = self.conn.get('SELECT id FROM ratings WHERE rating=?', (rating,), all=False)
@@ -2522,7 +2583,10 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
def set_comment(self, id, text, notify=True, commit=True):
self.conn.execute('DELETE FROM comments WHERE book=?', (id,))
- self.conn.execute('INSERT INTO comments(book,text) VALUES (?,?)', (id, text))
+ if text:
+ self.conn.execute('INSERT INTO comments(book,text) VALUES (?,?)', (id, text))
+ else:
+ text = ''
if commit:
self.conn.commit()
self.data.set(id, self.FIELD_MAP['comments'], text, row_is_id=True)
@@ -2531,6 +2595,8 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
self.notify('metadata', [id])
def set_author_sort(self, id, sort, notify=True, commit=True):
+ if not sort:
+ sort = ''
self.conn.execute('UPDATE books SET author_sort=? WHERE id=?', (sort, id))
self.dirtied([id], commit=False)
if commit:
@@ -2602,6 +2668,8 @@ class LibraryDatabase2(LibraryDatabase, SchemaUpgrade, CustomColumns):
def set_identifiers(self, id_, identifiers, notify=True, commit=True):
cleaned = {}
+ if not identifiers:
+ identifiers = {}
for typ, val in identifiers.iteritems():
typ, val = self._clean_identifier(typ, val)
if val:
diff --git a/src/calibre/library/server/browse.py b/src/calibre/library/server/browse.py
index 97bfc30f14..f1d9b9785c 100644
--- a/src/calibre/library/server/browse.py
+++ b/src/calibre/library/server/browse.py
@@ -12,7 +12,7 @@ import cherrypy
from calibre.constants import filesystem_encoding
from calibre import isbytestring, force_unicode, fit_image, \
- prepare_string_for_xml as xml
+ prepare_string_for_xml
from calibre.utils.ordered_dict import OrderedDict
from calibre.utils.filenames import ascii_filename
from calibre.utils.config import prefs, tweaks
@@ -23,6 +23,10 @@ from calibre.library.server import custom_fields_to_display
from calibre.library.field_metadata import category_icon_map
from calibre.library.server.utils import quote, unquote
+def xml(*args, **kwargs):
+ ans = prepare_string_for_xml(*args, **kwargs)
+ return ans.replace(''', ''')
+
def render_book_list(ids, prefix, suffix=''): # {{{
pages = []
num = len(ids)
@@ -626,6 +630,8 @@ class BrowseServer(object):
elif category == 'allbooks':
ids = all_ids
else:
+ if fm.get(category, {'datatype':None})['datatype'] == 'composite':
+ cid = cid.decode('utf-8')
q = category
if q == 'news':
q = 'tags'
diff --git a/src/calibre/manual/faq.rst b/src/calibre/manual/faq.rst
index a3d4332fd0..948611f775 100644
--- a/src/calibre/manual/faq.rst
+++ b/src/calibre/manual/faq.rst
@@ -508,9 +508,9 @@ You have two choices:
1. Create a patch by hacking on |app| and send it to me for review and inclusion. See `Development Cannot upload books to device there is no more free space available "
msgstr ""
@@ -7008,14 +7008,14 @@ msgid ""
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/device_drivers/configwidget.py:137
-#: /home/kovid/work/calibre/src/calibre/gui2/library/delegates.py:403
+#: /home/kovid/work/calibre/src/calibre/gui2/library/delegates.py:402
#: /home/kovid/work/calibre/src/calibre/gui2/preferences/plugboard.py:255
#: /home/kovid/work/calibre/src/calibre/gui2/preferences/save_template.py:61
msgid "Invalid template"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/device_drivers/configwidget.py:138
-#: /home/kovid/work/calibre/src/calibre/gui2/library/delegates.py:404
+#: /home/kovid/work/calibre/src/calibre/gui2/library/delegates.py:403
#: /home/kovid/work/calibre/src/calibre/gui2/preferences/plugboard.py:256
#: /home/kovid/work/calibre/src/calibre/gui2/preferences/save_template.py:62
msgid "The template %s is invalid:"
@@ -7265,7 +7265,7 @@ msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/choose_format_device_ui.py:49
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/delete_matching_from_device.py:76
-#: /home/kovid/work/calibre/src/calibre/gui2/library/models.py:1188
+#: /home/kovid/work/calibre/src/calibre/gui2/library/models.py:1185
msgid "Format"
msgstr ""
@@ -7356,6 +7356,20 @@ msgstr ""
msgid "&Move current library to new location"
msgstr ""
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/choose_plugin_toolbars.py:23
+msgid "Add \"%s\" to toolbars or menus"
+msgstr ""
+
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/choose_plugin_toolbars.py:29
+msgid "Select the toolbars and/or menus to add %s to:"
+msgstr ""
+
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/choose_plugin_toolbars.py:45
+msgid ""
+"You can also customise the plugin locations using Preferences -> "
+"Customise the toolbar"
+msgstr ""
+
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/comicconf.py:33
msgid "Set defaults for conversion of comics (CBR/CBZ files)"
msgstr ""
@@ -7435,11 +7449,11 @@ msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/delete_matching_from_device.py:76
#: /home/kovid/work/calibre/src/calibre/gui2/library/models.py:69
-#: /home/kovid/work/calibre/src/calibre/gui2/library/models.py:998
+#: /home/kovid/work/calibre/src/calibre/gui2/library/models.py:995
#: /home/kovid/work/calibre/src/calibre/gui2/preferences/create_custom_column.py:32
#: /home/kovid/work/calibre/src/calibre/gui2/preferences/create_custom_column.py:71
#: /home/kovid/work/calibre/src/calibre/library/field_metadata.py:241
-#: /home/kovid/work/calibre/src/calibre/library/field_metadata.py:311
+#: /home/kovid/work/calibre/src/calibre/library/field_metadata.py:321
#: /home/kovid/work/calibre/src/calibre/library/server/opds.py:575
msgid "Date"
msgstr ""
@@ -7467,12 +7481,12 @@ msgid "Author sort"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/edit_authors_dialog.py:128
-#: /home/kovid/work/calibre/src/calibre/gui2/tag_view.py:1371
+#: /home/kovid/work/calibre/src/calibre/gui2/tag_view.py:1379
msgid "Invalid author name"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/edit_authors_dialog.py:129
-#: /home/kovid/work/calibre/src/calibre/gui2/tag_view.py:1372
+#: /home/kovid/work/calibre/src/calibre/gui2/tag_view.py:1380
msgid "Author names cannot contain & characters."
msgstr ""
@@ -7656,22 +7670,22 @@ msgid "Working"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk.py:260
-#: /home/kovid/work/calibre/src/calibre/gui2/widgets.py:414
+#: /home/kovid/work/calibre/src/calibre/gui2/widgets.py:412
msgid "Lower Case"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk.py:261
-#: /home/kovid/work/calibre/src/calibre/gui2/widgets.py:413
+#: /home/kovid/work/calibre/src/calibre/gui2/widgets.py:411
msgid "Upper Case"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk.py:262
-#: /home/kovid/work/calibre/src/calibre/gui2/widgets.py:416
+#: /home/kovid/work/calibre/src/calibre/gui2/widgets.py:414
msgid "Title Case"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk.py:263
-#: /home/kovid/work/calibre/src/calibre/gui2/widgets.py:417
+#: /home/kovid/work/calibre/src/calibre/gui2/widgets.py:415
msgid "Capitalize"
msgstr ""
@@ -7705,11 +7719,15 @@ msgid ""
"cannot be canceled or undone"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk.py:381
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk.py:382
msgid "Book %d:"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk.py:396
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk.py:400
+msgid "Enter an identifier type"
+msgstr ""
+
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk.py:405
msgid ""
"You can destroy your library using this feature. Changes are "
"permanent. There is no undo function. You are strongly encouraged to back up "
@@ -7717,7 +7735,7 @@ msgid ""
"character matching or regular expressions. "
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk.py:404
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk.py:413
msgid ""
"In character mode, the field is searched for the entered search text. The "
"text is replaced by the specified replacement text everywhere it is found in "
@@ -7727,7 +7745,7 @@ msgid ""
"text will match both upper- and lower-case letters"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk.py:415
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk.py:424
msgid ""
"In regular expression mode, the search text is an arbitrary python-"
"compatible regular expression. The replacement text can contain "
@@ -7742,145 +7760,149 @@ msgid ""
"function."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk.py:489
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk.py:502
msgid "S/R TEMPLATE ERROR"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk.py:616
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk.py:646
msgid "You must specify a destination when source is a composite field"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk.py:715
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk.py:723
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk.py:844
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk.py:652
+msgid "You must specify a destination identifier type"
+msgstr ""
+
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk.py:756
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk.py:775
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk.py:896
msgid "Search/replace invalid"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk.py:716
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk.py:757
msgid ""
"Authors cannot be set to the empty string. Book title %s not processed"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk.py:724
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk.py:776
msgid "Title cannot be set to the empty string. Book title %s not processed"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk.py:845
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk.py:897
msgid "Search pattern is invalid: %s"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk.py:897
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk.py:949
msgid ""
"Applying changes to %d books.\n"
"Phase {0} {1}%%."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk.py:927
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:561
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk.py:979
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:587
msgid "Delete saved search/replace"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk.py:928
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk.py:980
msgid "The selected saved search/replace will be deleted. Are you sure?"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk.py:945
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk.py:953
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk.py:997
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk.py:1005
msgid "Save search/replace"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk.py:946
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk.py:998
msgid "Search/replace name:"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk.py:954
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk.py:1006
msgid ""
"That saved search/replace already exists and will be overwritten. Are you "
"sure?"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:498
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:524
msgid "Edit Meta information"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:500
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:526
msgid "A&utomatically set author sort"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:501
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:527
msgid "&Swap title and author"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:502
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:528
msgid "Author s&ort: "
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:503
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:529
msgid ""
"Specify how the author(s) of this book should be sorted. For example Charles "
"Dickens should be sorted as Dickens, Charles."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:504
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:530
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:424
#: /home/kovid/work/calibre/src/calibre/gui2/metadata/basic_widgets.py:786
msgid "&Rating:"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:505
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:506
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:531
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:532
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:425
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:426
#: /home/kovid/work/calibre/src/calibre/gui2/metadata/basic_widgets.py:787
msgid "Rating of this book. 0-5 stars"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:507
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:533
msgid "No change"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:508
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:534
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:427
msgid " stars"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:510
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:536
msgid "Add ta&gs: "
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:512
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:513
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:538
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:539
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:431
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:432
-#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single.py:140
+#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single.py:143
msgid "Open Tag Editor"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:514
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:540
msgid "&Remove tags:"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:515
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:541
msgid "Comma separated list of tags to remove from the books. "
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:516
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:542
msgid "Check this box to remove all tags from the books."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:517
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:543
msgid "Remove &all"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:521
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:547
msgid "If checked, the series will be cleared"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:522
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:548
msgid "&Clear series"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:523
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:549
msgid ""
"If not checked, the series number for the books will be set to 1.\n"
"If checked, selected books will be automatically numbered, in the order\n"
@@ -7888,192 +7910,202 @@ msgid ""
"Book A will have series number 1 and Book B series number 2."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:527
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:553
msgid "&Automatically number books in this series"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:528
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:554
msgid ""
"Series will normally be renumbered from the highest number in the database\n"
"for that series. Checking this box will tell calibre to start numbering\n"
"from the value in the box"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:531
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:557
msgid "&Force numbers to start with:"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:532
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:558
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:440
#: /home/kovid/work/calibre/src/calibre/gui2/metadata/basic_widgets.py:978
msgid "&Date:"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:533
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:559
msgid "d MMM yyyy"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:535
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:540
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:561
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:566
msgid "&Apply date"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:536
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:562
msgid "&Published:"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:538
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:564
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:444
msgid "Clear published date"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:541
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:567
msgid "Remove &format:"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:542
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:568
msgid ""
"Force the title to be in title case. If both this and swap authors are "
"checked,\n"
"title and author are swapped before the title case is set"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:544
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:570
msgid "Change title to title &case"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:545
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:571
msgid ""
"Update title sort based on the current title. This will be applied only "
"after other changes to title."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:546
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:572
msgid "Update &title sort"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:547
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:573
msgid ""
"Remove stored conversion settings for the selected books.\n"
"\n"
"Future conversion of these books will use the default settings."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:550
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:576
msgid "Remove &stored conversion settings for the selected books"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:551
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:577
msgid "Change &cover"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:552
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:578
msgid "&Generate default cover"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:553
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:579
msgid "&Remove cover"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:554
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:580
msgid "Set from &ebook file(s)"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:555
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:581
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:465
-#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single.py:392
-#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single.py:521
+#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single.py:397
+#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single.py:526
msgid "&Basic metadata"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:556
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:582
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:466
-#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single.py:399
+#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single.py:404
msgid "&Custom metadata"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:557
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:583
msgid "Load searc&h/replace:"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:558
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:584
msgid "Select saved search/replace to load."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:559
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:585
msgid "Save current search/replace"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:560
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:586
msgid "Sa&ve"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:562
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:588
#: /home/kovid/work/calibre/src/calibre/gui2/viewer/bookmarkmanager_ui.py:64
msgid "Delete"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:563
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:589
msgid "Search &field:"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:564
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:590
msgid "The name of the field that you want to search"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:565
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:591
msgid "Search &mode:"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:566
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:592
msgid ""
"Choose whether to use basic text matching or advanced regular expression "
"matching"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:567
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:593
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:615
+msgid "Identifier type:"
+msgstr ""
+
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:594
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:616
+msgid "Choose which identifier type to operate upon"
+msgstr ""
+
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:595
msgid "Te&mplate:"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:568
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:596
msgid "Enter a template to be used as the source for the search/replace"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:569
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:597
msgid "&Search for:"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:570
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:598
msgid ""
"Enter the what you are looking for, either plain text or a regular "
"expression, depending on the mode"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:571
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:599
msgid ""
"Check this box if the search string must match exactly upper and lower case. "
"Uncheck it if case is to be ignored"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:572
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:600
msgid "Cas&e sensitive"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:573
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:601
msgid "&Replace with:"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:574
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:602
msgid ""
"The replacement text. The matched search text will be replaced with this "
"string"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:575
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:603
msgid "&Apply function after replace:"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:576
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:604
msgid ""
"Specify how the text is to be processed after matching and replacement. In "
"character mode, the entire\n"
@@ -8081,25 +8113,25 @@ msgid ""
"processed"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:578
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:606
msgid "&Destination field:"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:579
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:607
msgid ""
"The field that the text will be put into after all replacements.\n"
"If blank, the source field is used if the field is modifiable"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:581
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:609
msgid "M&ode:"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:582
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:610
msgid "Specify how the text should be copied into the destination."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:583
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:611
msgid ""
"Specifies whether result items should be split into multiple values or\n"
"left as single values. This option has the most effect when the source field "
@@ -8107,41 +8139,41 @@ msgid ""
"not multiple and the destination field is multiple"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:586
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:614
msgid "Split &result"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:587
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:617
msgid "For multiple-valued fields, sho&w"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:588
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:618
msgid "values starting a&t"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:589
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:619
msgid "with values separated b&y"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:590
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:620
msgid ""
"Used when displaying test results to separate values in multiple-valued "
"fields"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:591
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:621
msgid "Test text"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:592
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:622
msgid "Test result"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:593
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:623
msgid "Your test:"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:594
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_bulk_ui.py:624
msgid "&Search and replace"
msgstr ""
@@ -8152,18 +8184,18 @@ msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:122
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:128
-#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single.py:252
-#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single.py:259
+#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single.py:255
+#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single.py:262
msgid "Could not read cover"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:123
-#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single.py:253
+#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single.py:256
msgid "Could not read cover from %s format"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:129
-#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single.py:260
+#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single.py:263
msgid "The cover in the %s format is invalid"
msgstr ""
@@ -8286,7 +8318,7 @@ msgid ""
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:472
-#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single.py:49
+#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single.py:50
#: /home/kovid/work/calibre/src/calibre/library/server/browse.py:102
#: /home/kovid/work/calibre/src/calibre/web/feeds/templates.py:221
#: /home/kovid/work/calibre/src/calibre/web/feeds/templates.py:384
@@ -8294,13 +8326,13 @@ msgid "Previous"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:475
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:483
-#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single.py:358
-#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single.py:362
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:484
+#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single.py:361
+#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single.py:366
msgid "Save changes and edit the metadata of %s"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:480
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:481
#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single.py:46
#: /home/kovid/work/calibre/src/calibre/library/server/browse.py:103
#: /home/kovid/work/calibre/src/calibre/web/feeds/templates.py:211
@@ -8308,62 +8340,62 @@ msgstr ""
msgid "Next"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:688
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:693
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:690
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:695
#: /home/kovid/work/calibre/src/calibre/gui2/metadata/basic_widgets.py:913
msgid "This ISBN number is valid"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:696
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:698
#: /home/kovid/work/calibre/src/calibre/gui2/metadata/basic_widgets.py:920
msgid "This ISBN number is invalid"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:781
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:783
#: /home/kovid/work/calibre/src/calibre/gui2/metadata/basic_widgets.py:862
msgid "Tags changed"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:782
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:784
#: /home/kovid/work/calibre/src/calibre/gui2/metadata/basic_widgets.py:863
msgid ""
"You have changed the tags. In order to use the tags editor, you must either "
"discard or apply these changes. Apply changes?"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:817
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:819
msgid "Timed out"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:818
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:820
msgid ""
"The download of social metadata timed out, the servers are probably busy. "
"Try again later."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:825
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:827
msgid "There were errors"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:826
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:828
msgid "There were errors downloading social metadata"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:860
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:862
msgid "Cannot fetch metadata"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:861
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:863
msgid "You must specify at least one of ISBN, Title, Authors or Publisher"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:959
-#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single.py:307
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:961
+#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single.py:310
msgid "Permission denied"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:960
-#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single.py:308
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single.py:962
+#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single.py:311
msgid "Could not open %s. Is it being used by another program?"
msgstr ""
@@ -8376,7 +8408,7 @@ msgid "Meta information"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:410
-#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single.py:89
+#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single.py:92
msgid ""
"Automatically create the title sort entry based on the current title entry.\n"
"Using this button to create title sort will change title sort from red to "
@@ -8384,12 +8416,12 @@ msgid ""
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:413
-#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single.py:111
+#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single.py:114
msgid "Swap the author and title"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:415
-#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single.py:100
+#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single.py:103
msgid ""
"Automatically create the author sort entry based on the current author "
"entry.\n"
@@ -8422,7 +8454,7 @@ msgid ""
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:436
-#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single.py:118
+#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single.py:121
msgid "Remove unused series (Series that have no books)"
msgstr ""
@@ -8441,7 +8473,7 @@ msgid "Publishe&d:"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:445
-#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single.py:156
+#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single.py:159
msgid "&Fetch metadata from server"
msgstr ""
@@ -8504,7 +8536,7 @@ msgid "Update metadata from the metadata in the selected format"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/metadata_single_ui.py:464
-#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single.py:580
+#: /home/kovid/work/calibre/src/calibre/gui2/metadata/single.py:585
msgid "&Comments"
msgstr ""
@@ -8513,21 +8545,21 @@ msgid "Password needed"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/password_ui.py:63
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler_ui.py:233
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler_ui.py:205
#: /home/kovid/work/calibre/src/calibre/gui2/preferences/server_ui.py:125
#: /home/kovid/work/calibre/src/calibre/gui2/wizard/send_email_ui.py:133
msgid "&Username:"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/password_ui.py:64
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler_ui.py:234
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler_ui.py:206
#: /home/kovid/work/calibre/src/calibre/gui2/preferences/server_ui.py:126
#: /home/kovid/work/calibre/src/calibre/gui2/wizard/send_email_ui.py:135
msgid "&Password:"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/password_ui.py:65
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler_ui.py:235
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler_ui.py:207
#: /home/kovid/work/calibre/src/calibre/gui2/preferences/server_ui.py:130
#: /home/kovid/work/calibre/src/calibre/gui2/wizard/send_email.py:172
msgid "&Show password"
@@ -8605,163 +8637,194 @@ msgstr ""
msgid "Change the contents of the saved search"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler.py:30
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler.py:42
+msgid ""
+" Download this periodical every week on the specified days "
+"after\n"
+" the specified time. For example, if you choose: Monday "
+"after\n"
+" 9:00 AM, then the periodical will be download every Monday "
+"as\n"
+" soon after 9:00 AM as possible.\n"
+" "
+msgstr ""
+
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler.py:61
+msgid "&Download after:"
+msgstr ""
+
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler.py:91
+msgid ""
+" Download this periodical every month, on the specified "
+"days.\n"
+" The download will happen as soon after the specified time "
+"as\n"
+" possible on the specified days of each month. For example,\n"
+" if you choose the 1st and the 15th after 9:00 AM, the\n"
+" periodical will be downloaded on the 1st and 15th of every\n"
+" month, as soon after 9:00 AM as possible.\n"
+" "
+msgstr ""
+
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler.py:103
+msgid "&Days of the month:"
+msgstr ""
+
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler.py:105
+msgid "Comma separated list of days of the month. For example: 1, 15"
+msgstr ""
+
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler.py:109
+msgid "Download &after:"
+msgstr ""
+
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler.py:142
+msgid ""
+" Download this periodical every x days. For example, if you\n"
+" choose 30 days, the periodical will be downloaded every 30\n"
+" days. Note that you can set periods of less than a day, "
+"like\n"
+" 0.1 days to download a periodical more than once a day.\n"
+" "
+msgstr ""
+
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler.py:151
+msgid "&Download every:"
+msgstr ""
+
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler.py:154
+msgid "every hour"
+msgstr ""
+
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler.py:157
+msgid "days"
+msgstr ""
+
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler.py:161
+msgid ""
+"Note: You can set intervals of less than a day, by typing the value manually."
+msgstr ""
+
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler.py:196
msgid "%s news sources"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler.py:125
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler.py:311
msgid "Need username and password"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler.py:126
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler.py:312
msgid "You must provide a username and/or password to use this news source."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler.py:168
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler.py:346
msgid "Account"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler.py:169
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler.py:347
msgid "(optional)"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler.py:170
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler.py:348
msgid "(required)"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler.py:187
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler.py:365
msgid "Created by: "
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler.py:194
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler.py:372
msgid "Last downloaded: never"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler.py:209
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler.py:373
+msgid "never"
+msgstr ""
+
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler.py:379
msgid "%d days, %d hours and %d minutes ago"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler.py:211
-msgid "Last downloaded"
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler.py:393
+msgid "Last downloaded:"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler.py:242
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler_ui.py:215
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler.py:421
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler_ui.py:197
msgid "Schedule news download"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler.py:245
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler.py:424
msgid "Add a custom news source"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler.py:250
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler.py:429
msgid "Download all scheduled new sources"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler.py:354
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler.py:534
msgid "No internet connection"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler.py:355
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler.py:535
msgid "Cannot download news as no internet connection is active"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler_ui.py:216
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler_ui.py:198
msgid "&Search:"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler_ui.py:217
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler_ui.py:199
msgid "blurb"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler_ui.py:218
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler_ui.py:200
msgid "&Schedule for download:"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler_ui.py:219
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler_ui.py:229
-msgid "Every "
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler_ui.py:201
+msgid "Days of week"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler_ui.py:220
-msgid "day"
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler_ui.py:202
+msgid "Days of month"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler_ui.py:221
-msgid "Monday"
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler_ui.py:203
+msgid "Every x days"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler_ui.py:222
-msgid "Tuesday"
-msgstr ""
-
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler_ui.py:223
-msgid "Wednesday"
-msgstr ""
-
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler_ui.py:224
-msgid "Thursday"
-msgstr ""
-
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler_ui.py:225
-msgid "Friday"
-msgstr ""
-
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler_ui.py:226
-msgid "Saturday"
-msgstr ""
-
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler_ui.py:227
-msgid "Sunday"
-msgstr ""
-
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler_ui.py:228
-msgid "at"
-msgstr ""
-
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler_ui.py:230
-msgid ""
-"Interval at which to download this recipe. A value of zero means that the "
-"recipe will be downloaded every hour."
-msgstr ""
-
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler_ui.py:231
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler_ui.py:253
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/user_profiles_ui.py:268
-msgid " days"
-msgstr ""
-
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler_ui.py:232
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler_ui.py:204
msgid "&Account"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler_ui.py:236
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler_ui.py:208
msgid "For the scheduling to work, you must leave calibre running."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler_ui.py:237
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler_ui.py:209
msgid "&Schedule"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler_ui.py:238
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler_ui.py:210
msgid "Add &title as tag"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler_ui.py:239
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler_ui.py:211
msgid "&Extra tags:"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler_ui.py:240
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler_ui.py:212
msgid ""
"Maximum number of copies (issues) of this recipe to keep. Set to 0 to keep "
"all (disable)."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler_ui.py:241
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler_ui.py:213
msgid "&Keep at most:"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler_ui.py:242
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler_ui.py:214
msgid ""
" When set, this option will cause calibre to keep, at most, the specified "
"number of issues of this periodical. Every time a new issue is downloaded, "
@@ -8772,27 +8835,27 @@ msgid ""
"below, takes priority over this setting."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler_ui.py:245
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler_ui.py:217
msgid "all issues"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler_ui.py:246
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler_ui.py:218
msgid " issues"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler_ui.py:247
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler_ui.py:219
msgid "&Advanced"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler_ui.py:248
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler_ui.py:220
msgid "&Download now"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler_ui.py:249
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler_ui.py:221
msgid "&Delete downloaded news older than:"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler_ui.py:250
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler_ui.py:222
msgid ""
" Delete downloaded news older than the specified number of days. Set to "
"zero to disable.\n"
@@ -8801,15 +8864,20 @@ msgid ""
"above."
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler_ui.py:252
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler_ui.py:224
msgid "never delete"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler_ui.py:254
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler_ui.py:225
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/user_profiles_ui.py:273
+msgid " days"
+msgstr ""
+
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler_ui.py:226
msgid "Download all scheduled news sources at once"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler_ui.py:255
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/scheduler_ui.py:227
msgid "Download &all scheduled"
msgstr ""
@@ -9079,12 +9147,12 @@ msgid "%s (was %s)"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/tag_list_editor.py:85
-#: /home/kovid/work/calibre/src/calibre/gui2/tag_view.py:1319
+#: /home/kovid/work/calibre/src/calibre/gui2/tag_view.py:1325
msgid "Item is blank"
msgstr ""
#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/tag_list_editor.py:86
-#: /home/kovid/work/calibre/src/calibre/gui2/tag_view.py:1320
+#: /home/kovid/work/calibre/src/calibre/gui2/tag_view.py:1326
msgid "An item cannot be set to nothing. Delete it instead."
msgstr ""
@@ -9186,120 +9254,124 @@ msgid ""
"updating your calibre library. Could not create recipe. Error:
%s"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/user_profiles.py:247
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/user_profiles.py:306
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/user_profiles.py:333
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/user_profiles.py:255
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/user_profiles.py:314
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/user_profiles.py:341
msgid "Replace recipe?"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/user_profiles.py:248
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/user_profiles.py:307
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/user_profiles.py:334
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/user_profiles.py:256
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/user_profiles.py:315
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/user_profiles.py:342
msgid "A custom recipe named %s already exists. Do you want to replace it?"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/user_profiles.py:274
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/user_profiles.py:282
msgid "Choose builtin recipe"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/user_profiles.py:320
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/user_profiles.py:328
msgid "Choose a recipe file"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/user_profiles.py:321
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/user_profiles.py:329
msgid "Recipes"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/user_profiles.py:361
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/user_profiles.py:369
msgid ""
"You will lose any unsaved changes. To save your changes, click the "
"Add/Update recipe button. Continue?"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/user_profiles_ui.py:253
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/user_profiles_ui.py:257
msgid "Add custom news source"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/user_profiles_ui.py:254
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/user_profiles_ui.py:258
msgid "Available user recipes"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/user_profiles_ui.py:255
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/user_profiles_ui.py:259
msgid "Add/Update &recipe"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/user_profiles_ui.py:256
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/user_profiles_ui.py:260
msgid "&Remove recipe"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/user_profiles_ui.py:257
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/user_profiles_ui.py:261
msgid "&Share recipe"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/user_profiles_ui.py:258
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/user_profiles_ui.py:262
+msgid "S&how recipe files"
+msgstr ""
+
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/user_profiles_ui.py:263
msgid "Customize &builtin recipe"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/user_profiles_ui.py:259
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/user_profiles_ui.py:264
msgid "&Load recipe from file"
msgstr ""
-#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/user_profiles_ui.py:261
+#: /home/kovid/work/calibre/src/calibre/gui2/dialogs/user_profiles_ui.py:266
msgid ""
"