diff --git a/recipes/aachener_nachrichten.recipe b/recipes/aachener_nachrichten.recipe
new file mode 100644
index 0000000000..a2294fc472
--- /dev/null
+++ b/recipes/aachener_nachrichten.recipe
@@ -0,0 +1,42 @@
+from calibre.web.feeds.recipes import BasicNewsRecipe
+class AdvancedUserRecipe(BasicNewsRecipe):
+
+ title = u'Aachener Nachrichten'
+ __author__ = 'schuster'
+ oldest_article = 1
+ max_articles_per_feed = 100
+ use_embedded_content = False
+ language = 'de'
+ remove_javascript = True
+ cover_url = 'http://www.an-online.de/einwaage/images/an_logo.png'
+ masthead_url = 'http://www.an-online.de/einwaage/images/an_logo.png'
+ extra_css = '''
+ .fliesstext_detail:{margin-bottom:10%;}
+ .headline_1:{margin-bottom:25%;}
+ b{font-family:Arial,Helvetica,sans-serif; font-weight:200;font-size:large;}
+ a{font-family:Arial,Helvetica,sans-serif; font-weight:400;font-size:large;}
+ ll{font-family:Arial,Helvetica,sans-serif; font-weight:100;font-size:large;}
+ h4{font-family:Arial,Helvetica,sans-serif; font-weight:normal;font-size:small;}
+ img {min-width:300px; max-width:600px; min-height:300px; max-height:800px}
+ dd{font-family:Arial,Helvetica,sans-serif;font-size:large;}
+ body{font-family:Helvetica,Arial,sans-serif;font-size:small;}
+ '''
+
+
+
+ keep_only_tags = [
+ dict(name='span', attrs={'class':['fliesstext_detail', 'headline_1', 'autor_detail']}),
+ dict(id=['header-logo'])
+ ]
+
+ feeds = [(u'Euregio', u'http://www.an-online.de/an/rss/Euregio.xml'),
+ (u'Aachen', u'http://www.an-online.de/an/rss/Aachen.xml'),
+ (u'Nordkreis', u'http://www.an-online.de/an/rss/Nordkreis.xml'),
+ (u'Düren', u'http://www.an-online.de/an/rss/Dueren.xml'),
+ (u'Eiffel', u'http://www.an-online.de/an/rss/Eifel.xml'),
+ (u'Eschweiler', u'http://www.an-online.de/an/rss/Eschweiler.xml'),
+ (u'Geilenkirchen', u'http://www.an-online.de/an/rss/Geilenkirchen.xml'),
+ (u'Heinsberg', u'http://www.an-online.de/an/rss/Heinsberg.xml'),
+ (u'Jülich', u'http://www.an-online.de/an/rss/Juelich.xml'),
+ (u'Stolberg', u'http://www.an-online.de/an/rss/Stolberg.xml'),
+ (u'Ratgebenr', u'http://www.an-online.de/an/rss/Ratgeber.xml')]
diff --git a/recipes/faznet.recipe b/recipes/faznet.recipe
index 01a46d43ba..50a66c59b8 100644
--- a/recipes/faznet.recipe
+++ b/recipes/faznet.recipe
@@ -3,10 +3,7 @@ class AdvancedUserRecipe1303841067(BasicNewsRecipe):
title = u'Faz.net'
__author__ = 'schuster'
- remove_tags = [dict(attrs={'class':['right', 'ArrowLinkRight', 'ModulVerlagsInfo', 'left', 'Head']}),
- dict(id=['BreadCrumbs', 'tstag', 'FazFooterPrint']),
- dict(name=['script', 'noscript', 'style'])]
- oldest_article = 2
+ oldest_article = 1
description = 'Frankfurter Allgemeine Zeitung'
max_articles_per_feed = 100
no_stylesheets = True
@@ -15,9 +12,9 @@ class AdvancedUserRecipe1303841067(BasicNewsRecipe):
remove_javascript = True
cover_url = 'http://www.faz.net/f30/Images/Logos/logo.gif'
- def print_version(self, url):
- return url.replace('.html', '~Afor~Eprint.html')
-
+ remove_tags = [dict(attrs={'class':['LinkBoxModulSmall', 'ModulLesermeinungenFooter', 'ModulArtikelServices', 'SocialMediaUnten', 'ArrowLinkRight', 'ModulVerlagsInfo', 'AdData', 'FazFooter', 'Date']}),
+ dict(id=['FAZNavHeader', 'FAZNavMain', 'RightColumn', 'FazFooter', 'BreadCrumbs', 'FAZNavSubMain', 'FAZImgEvent']),
+ dict(name=['jksrdt'])]
feeds = [(u'Politik', u'http://www.faz.net/s/RubA24ECD630CAE40E483841DB7D16F4211/Tpl~Epartner~SRss_.xml'),
diff --git a/recipes/frankfurter_rundschau.recipe b/recipes/frankfurter_rundschau.recipe
new file mode 100644
index 0000000000..3c3bb32ca3
--- /dev/null
+++ b/recipes/frankfurter_rundschau.recipe
@@ -0,0 +1,35 @@
+from calibre.web.feeds.recipes import BasicNewsRecipe
+class AdvancedUserRecipe(BasicNewsRecipe):
+
+ title = u'Frankfurter Rundschau'
+ __author__ = 'schuster'
+ oldest_article = 1
+ max_articles_per_feed = 100
+ no_stylesheets = True
+ use_embedded_content = False
+ language = 'de'
+ remove_javascript = True
+ cover_url = 'http://www.fr-online.de/image/view/-/1474018/data/823538/-/logo.png'
+ extra_css = '''
+ h1{font-family:Arial,Helvetica,sans-serif; font-weight:bold;font-size:large;}
+ h4{font-family:Arial,Helvetica,sans-serif; font-weight:normal;font-size:small;}
+ img {min-width:300px; max-width:600px; min-height:300px; max-height:800px}
+ p{font-family:Arial,Helvetica,sans-serif;font-size:small;}
+ body{font-family:Helvetica,Arial,sans-serif;font-size:small;}
+ '''
+
+ feeds = [(u'Startseite', u'http://www.fr-online.de/home/-/1472778/1472778/-/view/asFeed/-/index.xml'),
+ (u'Politik', u'http://www.fr-online.de/politik/-/1472596/1472596/-/view/asFeed/-/index.xml'),
+ (u'Meinungen', u'http://www.fr-online.de/politik/meinung/-/1472602/1472602/-/view/asFeed/-/index.xml'),
+ (u'Wirtschaft', u'http://www.fr-online.de/wirtschaft/-/1472780/1472780/-/view/asFeed/-/index.xml'),
+ (u'Sport', u'http://www.fr-online.de/sport/-/1472784/1472784/-/view/asFeed/-/index.xml'),
+ (u'Kultur', u'http://www.fr-online.de/kultur/-/1472786/1472786/-/view/asFeed/-/index.xml'),
+ (u'Panorama', u'http://www.fr-online.de/panorama/-/1472782/1472782/-/view/asFeed/-/index.xml'),
+ (u'Digital', u'http://www.fr-online.de/digital/-/1472406/1472406/-/view/asFeed/-/index.xml'),
+ (u'Wissenschaft', u'http://www.fr-online.de/wissenschaft/-/1472788/1472788/-/view/asFeed/-/index.xml')
+]
+
+
+ def print_version(self, url):
+ return url.replace('index.html', 'view/printVersion/-/index.html')
+
diff --git a/recipes/rheinische_post.recipe b/recipes/rheinische_post.recipe
new file mode 100644
index 0000000000..1d3efc710d
--- /dev/null
+++ b/recipes/rheinische_post.recipe
@@ -0,0 +1,55 @@
+from calibre.web.feeds.recipes import BasicNewsRecipe
+class AdvancedUserRecipe(BasicNewsRecipe):
+
+ title = u'RP-online'
+ __author__ = 'schuster'
+ oldest_article = 2
+ max_articles_per_feed = 100
+ no_stylesheets = True
+ use_embedded_content = False
+ language = 'de'
+ remove_javascript = True
+ masthead_url = 'http://www.die-zeitungen.de/uploads/pics/LOGO_RP_ONLINE_01.jpg'
+ cover_url = 'http://www.manroland.com/com/pressinfo_images/com/RheinischePost_Logo_300dpi.jpg'
+ extra_css = '''
+ h1{font-family:Arial,Helvetica,sans-serif; font-weight:bold;font-size:large;}
+ h4{font-family:Arial,Helvetica,sans-serif; font-weight:normal;font-size:small;}
+ img {min-width:300px; max-width:600px; min-height:300px; max-height:800px}
+ p{font-family:Arial,Helvetica,sans-serif;font-size:small;}
+ body{font-family:Helvetica,Arial,sans-serif;font-size:small;}
+ '''
+ remove_tags_before = dict(id='article_content')
+ remove_tags_after = dict(id='article_content')
+ remove_tags = [dict(attrs={'class':['goodies', 'left', 'right', 'clear-all', 'teaser anzeigenwerbung', 'lesermeinung', 'goodiebox', 'goodiebox 1', 'goodiebox 2', 'goodiebox 3', 'boxframe', 'link']}),
+ dict(id=['click_Fotos_link']),
+ dict(name=['script', 'noscript', 'style', '_top', 'click_Fotos_link'])]
+
+ feeds = [ (u'Top-News', u'http://www.ngz-online.de/app/feed/rss/topnews'),
+ (u'Politik', u'http://www.ngz-online.de/app/feed/rss/politik'),
+ (u'Wirtschaft', u'http://www.ngz-online.de/app/feed/rss/wirtschaft'),
+ (u'Panorama', u'http://www.ngz-online.de/app/feed/rss/panorama'),
+ (u'Sport', u'http://www.ngz-online.de/app/feed/rss/sport'),
+ (u'Tour de France', u'http://www.ngz-online.de/app/feed/rss/tourdefrance'),
+ (u'Fußball', u'http://www.ngz-online.de/app/feed/rss/fussball'),
+ (u'Fußball BuLi', u'http://www.ngz-online.de/app/feed/rss/bundesliga'),
+ (u'Formel 1', u'http://www.ngz-online.de/app/feed/rss/formel1'),
+ (u'US-Sport', u'http://www.ngz-online.de/app/feed/rss/us-sports'),
+ (u'Boxen', u'http://www.ngz-online.de/app/feed/rss/boxen'),
+ (u'Eishockey', u'http://www.ngz-online.de/app/feed/rss/eishockey'),
+ (u'Basketball', u'http://www.ngz-online.de/app/feed/rss/basketball'),
+ (u'Handball', u'http://www.ngz-online.de/app/feed/rss/handball'),
+ (u'Motorsport', u'http://www.ngz-online.de/app/feed/rss/motorsport'),
+ (u'Tennis', u'http://www.ngz-online.de/app/feed/rss/tennis'),
+ (u'Radsport', u'http://www.ngz-online.de/app/feed/rss/radsport'),
+ (u'Kultur', u'http://www.ngz-online.de/app/feed/rss/kultur'),
+ (u'Gesellschaft', u'http://www.ngz-online.de/app/feed/rss/gesellschaft'),
+ (u'Wissenschaft', u'http://www.ngz-online.de/app/feed/rss/wissen'),
+ (u'Gesundheit', u'http://www.ngz-online.de/app/feed/rss/gesundheit'),
+ (u'Digitale Welt', u'http://www.ngz-online.de/app/feed/rss/digitale'),
+ (u'Auto & Mobil', u'http://www.ngz-online.de/app/feed/rss/auto'),
+ (u'Reise & Welt', u'http://www.ngz-online.de/app/feed/rss/reise'),
+ (u'Beruf & Karriere', u'http://www.ngz-online.de/app/feed/rss/beruf'),
+ (u'Herzrasen', u'http://www.ngz-online.de/app/feed/rss/herzrasen'),
+ (u'About a Boy', u'http://www.ngz-online.de/app/feed/rss/about_a_boy'),
+
+]
diff --git a/resources/template-functions.json b/resources/template-functions.json
index cf858c7691..b0a2225dd4 100644
--- a/resources/template-functions.json
+++ b/resources/template-functions.json
@@ -1,17 +1,21 @@
{
+ "and": "def evaluate(self, formatter, kwargs, mi, locals, *args):\n i = 0\n while i < len(args):\n if not args[i]:\n return ''\n i += 1\n return '1'\n",
"contains": "def evaluate(self, formatter, kwargs, mi, locals,\n val, test, value_if_present, value_if_not):\n if re.search(test, val):\n return value_if_present\n else:\n return value_if_not\n",
"divide": "def evaluate(self, formatter, kwargs, mi, locals, x, y):\n x = float(x if x else 0)\n y = float(y if y else 0)\n return unicode(x / y)\n",
"uppercase": "def evaluate(self, formatter, kwargs, mi, locals, val):\n return val.upper()\n",
"strcat": "def evaluate(self, formatter, kwargs, mi, locals, *args):\n i = 0\n res = ''\n for i in range(0, len(args)):\n res += args[i]\n return res\n",
- "substr": "def evaluate(self, formatter, kwargs, mi, locals, str_, start_, end_):\n return str_[int(start_): len(str_) if int(end_) == 0 else int(end_)]\n",
+ "in_list": "def evaluate(self, formatter, kwargs, mi, locals, val, sep, pat, fv, nfv):\n l = [v.strip() for v in val.split(sep) if v.strip()]\n for v in l:\n if re.search(pat, v):\n return fv\n return nfv\n",
+ "multiply": "def evaluate(self, formatter, kwargs, mi, locals, x, y):\n x = float(x if x else 0)\n y = float(y if y else 0)\n return unicode(x * y)\n",
"ifempty": "def evaluate(self, formatter, kwargs, mi, locals, val, value_if_empty):\n if val:\n return val\n else:\n return value_if_empty\n",
"booksize": "def evaluate(self, formatter, kwargs, mi, locals):\n if mi.book_size is not None:\n try:\n return str(mi.book_size)\n except:\n pass\n return ''\n",
"select": "def evaluate(self, formatter, kwargs, mi, locals, val, key):\n if not val:\n return ''\n vals = [v.strip() for v in val.split(',')]\n for v in vals:\n if v.startswith(key+':'):\n return v[len(key)+1:]\n return ''\n",
- "field": "def evaluate(self, formatter, kwargs, mi, locals, name):\n return formatter.get_value(name, [], kwargs)\n",
+ "strcmp": "def evaluate(self, formatter, kwargs, mi, locals, x, y, lt, eq, gt):\n v = strcmp(x, y)\n if v < 0:\n return lt\n if v == 0:\n return eq\n return gt\n",
+ "first_non_empty": "def evaluate(self, formatter, kwargs, mi, locals, *args):\n i = 0\n while i < len(args):\n if args[i]:\n return args[i]\n i += 1\n return ''\n",
+ "re": "def evaluate(self, formatter, kwargs, mi, locals, val, pattern, replacement):\n return re.sub(pattern, replacement, val)\n",
"subtract": "def evaluate(self, formatter, kwargs, mi, locals, x, y):\n x = float(x if x else 0)\n y = float(y if y else 0)\n return unicode(x - y)\n",
"list_item": "def evaluate(self, formatter, kwargs, mi, locals, val, index, sep):\n if not val:\n return ''\n index = int(index)\n val = val.split(sep)\n try:\n return val[index]\n except:\n return ''\n",
"shorten": "def evaluate(self, formatter, kwargs, mi, locals,\n val, leading, center_string, trailing):\n l = max(0, int(leading))\n t = max(0, int(trailing))\n if len(val) > l + len(center_string) + t:\n return val[0:l] + center_string + ('' if t == 0 else val[-t:])\n else:\n return val\n",
- "re": "def evaluate(self, formatter, kwargs, mi, locals, val, pattern, replacement):\n return re.sub(pattern, replacement, val)\n",
+ "field": "def evaluate(self, formatter, kwargs, mi, locals, name):\n return formatter.get_value(name, [], kwargs)\n",
"add": "def evaluate(self, formatter, kwargs, mi, locals, x, y):\n x = float(x if x else 0)\n y = float(y if y else 0)\n return unicode(x + y)\n",
"lookup": "def evaluate(self, formatter, kwargs, mi, locals, val, *args):\n if len(args) == 2: # here for backwards compatibility\n if val:\n return formatter.vformat('{'+args[0].strip()+'}', [], kwargs)\n else:\n return formatter.vformat('{'+args[1].strip()+'}', [], kwargs)\n if (len(args) % 2) != 1:\n raise ValueError(_('lookup requires either 2 or an odd number of arguments'))\n i = 0\n while i < len(args):\n if i + 1 >= len(args):\n return formatter.vformat('{' + args[i].strip() + '}', [], kwargs)\n if re.search(args[i], val):\n return formatter.vformat('{'+args[i+1].strip() + '}', [], kwargs)\n i += 2\n",
"template": "def evaluate(self, formatter, kwargs, mi, locals, template):\n template = template.replace('[[', '{').replace(']]', '}')\n return formatter.__class__().safe_format(template, kwargs, 'TEMPLATE', mi)\n",
@@ -21,14 +25,15 @@
"sublist": "def evaluate(self, formatter, kwargs, mi, locals, val, start_index, end_index, sep):\n if not val:\n return ''\n si = int(start_index)\n ei = int(end_index)\n val = val.split(sep)\n try:\n if ei == 0:\n return sep.join(val[si:])\n else:\n return sep.join(val[si:ei])\n except:\n return ''\n",
"test": "def evaluate(self, formatter, kwargs, mi, locals, val, value_if_set, value_not_set):\n if val:\n return value_if_set\n else:\n return value_not_set\n",
"eval": "def evaluate(self, formatter, kwargs, mi, locals, template):\n from formatter import eval_formatter\n template = template.replace('[[', '{').replace(']]', '}')\n return eval_formatter.safe_format(template, locals, 'EVAL', None)\n",
- "multiply": "def evaluate(self, formatter, kwargs, mi, locals, x, y):\n x = float(x if x else 0)\n y = float(y if y else 0)\n return unicode(x * y)\n",
+ "not": "def evaluate(self, formatter, kwargs, mi, locals, *args):\n i = 0\n while i < len(args):\n if args[i]:\n return '1'\n i += 1\n return ''\n",
"format_date": "def evaluate(self, formatter, kwargs, mi, locals, val, format_string):\n if not val:\n return ''\n try:\n dt = parse_date(val)\n s = format_date(dt, format_string)\n except:\n s = 'BAD DATE'\n return s\n",
"capitalize": "def evaluate(self, formatter, kwargs, mi, locals, val):\n return capitalize(val)\n",
"count": "def evaluate(self, formatter, kwargs, mi, locals, val, sep):\n return unicode(len(val.split(sep)))\n",
"lowercase": "def evaluate(self, formatter, kwargs, mi, locals, val):\n return val.lower()\n",
- "strcmp": "def evaluate(self, formatter, kwargs, mi, locals, x, y, lt, eq, gt):\n v = strcmp(x, y)\n if v < 0:\n return lt\n if v == 0:\n return eq\n return gt\n",
- "switch": "def evaluate(self, formatter, kwargs, mi, locals, val, *args):\n if (len(args) % 2) != 1:\n raise ValueError(_('switch requires an odd number of arguments'))\n i = 0\n while i < len(args):\n if i + 1 >= len(args):\n return args[i]\n if re.search(args[i], val):\n return args[i+1]\n i += 2\n",
+ "substr": "def evaluate(self, formatter, kwargs, mi, locals, str_, start_, end_):\n return str_[int(start_): len(str_) if int(end_) == 0 else int(end_)]\n",
"assign": "def evaluate(self, formatter, kwargs, mi, locals, target, value):\n locals[target] = value\n return value\n",
+ "switch": "def evaluate(self, formatter, kwargs, mi, locals, val, *args):\n if (len(args) % 2) != 1:\n raise ValueError(_('switch requires an odd number of arguments'))\n i = 0\n while i < len(args):\n if i + 1 >= len(args):\n return args[i]\n if re.search(args[i], val):\n return args[i+1]\n i += 2\n",
+ "or": "def evaluate(self, formatter, kwargs, mi, locals, *args):\n i = 0\n while i < len(args):\n if args[i]:\n return '1'\n i += 1\n return ''\n",
"raw_field": "def evaluate(self, formatter, kwargs, mi, locals, name):\n return unicode(getattr(mi, name, None))\n",
"cmp": "def evaluate(self, formatter, kwargs, mi, locals, x, y, lt, eq, gt):\n x = float(x if x else 0)\n y = float(y if y else 0)\n if x < y:\n return lt\n if x == y:\n return eq\n return gt\n"
}
\ No newline at end of file
diff --git a/src/calibre/customize/builtins.py b/src/calibre/customize/builtins.py
index 4c80c53d78..4a970b4661 100644
--- a/src/calibre/customize/builtins.py
+++ b/src/calibre/customize/builtins.py
@@ -594,7 +594,7 @@ from calibre.devices.iliad.driver import ILIAD
from calibre.devices.irexdr.driver import IREXDR1000, IREXDR800
from calibre.devices.jetbook.driver import JETBOOK, MIBUK, JETBOOK_MINI
from calibre.devices.kindle.driver import KINDLE, KINDLE2, KINDLE_DX
-from calibre.devices.nook.driver import NOOK, NOOK_COLOR
+from calibre.devices.nook.driver import NOOK, NOOK_COLOR, NOOK_TSR
from calibre.devices.prs505.driver import PRS505
from calibre.devices.user_defined.driver import USER_DEFINED
from calibre.devices.android.driver import ANDROID, S60
@@ -693,8 +693,7 @@ plugins += [
KINDLE,
KINDLE2,
KINDLE_DX,
- NOOK,
- NOOK_COLOR,
+ NOOK, NOOK_COLOR, NOOK_TSR,
PRS505,
ANDROID,
S60,
@@ -1272,6 +1271,16 @@ class StoreKoboStore(StoreBase):
headquarters = 'CA'
formats = ['EPUB']
+class StoreLegimiStore(StoreBase):
+ name = 'Legimi'
+ author = u'Tomasz Długosz'
+ description = u'Tanie oraz darmowe ebooki, egazety i blogi w formacie EPUB, wprost na Twój e-czytnik, iPhone, iPad, Android i komputer'
+ actual_plugin = 'calibre.gui2.store.legimi_plugin:LegimiStore'
+
+ drm_free_only = False
+ headquarters = 'PL'
+ formats = ['EPUB']
+
class StoreManyBooksStore(StoreBase):
name = 'ManyBooks'
description = u'Public domain and creative commons works from many sources.'
@@ -1306,7 +1315,7 @@ class StoreOpenLibraryStore(StoreBase):
actual_plugin = 'calibre.gui2.store.open_library_plugin:OpenLibraryStore'
drm_free_only = True
- headquarters = ['US']
+ headquarters = 'US'
formats = ['DAISY', 'DJVU', 'EPUB', 'MOBI', 'PDF', 'TXT']
class StoreOReillyStore(StoreBase):
@@ -1336,6 +1345,16 @@ class StoreSmashwordsStore(StoreBase):
headquarters = 'US'
formats = ['EPUB', 'HTML', 'LRF', 'MOBI', 'PDB', 'RTF', 'TXT']
+class StoreVirtualoStore(StoreBase):
+ name = 'Virtualo'
+ author = u'Tomasz Długosz'
+ description = u'Księgarnia internetowa, która oferuje bezpieczny i szeroki dostęp do książek w formie cyfrowej.'
+ actual_plugin = 'calibre.gui2.store.virtualo_plugin:VirtualoStore'
+
+ drm_free_only = False
+ headquarters = 'PL'
+ formats = ['EPUB', 'PDF']
+
class StoreWaterstonesUKStore(StoreBase):
name = 'Waterstones UK'
author = 'Charles Haley'
@@ -1366,12 +1385,12 @@ class StoreWizardsTowerBooksStore(StoreBase):
class StoreWoblinkStore(StoreBase):
name = 'Woblink'
- author = 'Tomasz Długosz'
+ author = u'Tomasz Długosz'
description = u'Czytanie zdarza się wszędzie!'
actual_plugin = 'calibre.gui2.store.woblink_plugin:WoblinkStore'
drm_free_only = False
- location = 'PL'
+ headquarters = 'PL'
formats = ['EPUB']
plugins += [
@@ -1393,6 +1412,7 @@ plugins += [
StoreGoogleBooksStore,
StoreGutenbergStore,
StoreKoboStore,
+ StoreLegimiStore,
StoreManyBooksStore,
StoreMobileReadStore,
StoreNextoStore,
@@ -1400,6 +1420,7 @@ plugins += [
StoreOReillyStore,
StorePragmaticBookshelfStore,
StoreSmashwordsStore,
+ StoreVirtualoStore,
StoreWaterstonesUKStore,
StoreWeightlessBooksStore,
StoreWizardsTowerBooksStore,
diff --git a/src/calibre/devices/android/driver.py b/src/calibre/devices/android/driver.py
index db473a755e..1cdf394c24 100644
--- a/src/calibre/devices/android/driver.py
+++ b/src/calibre/devices/android/driver.py
@@ -59,7 +59,7 @@ class ANDROID(USBMS):
0x0489 : { 0xc001 : [0x0226], 0xc004 : [0x0226], },
# Acer
- 0x502 : { 0x3203 : [0x0100]},
+ 0x502 : { 0x3203 : [0x0100, 0x224]},
# Dell
0x413c : { 0xb007 : [0x0100, 0x0224, 0x0226]},
diff --git a/src/calibre/devices/nook/driver.py b/src/calibre/devices/nook/driver.py
index 39d0763735..3c30b88568 100644
--- a/src/calibre/devices/nook/driver.py
+++ b/src/calibre/devices/nook/driver.py
@@ -107,3 +107,13 @@ class NOOK_COLOR(NOOK):
return filepath
+class NOOK_TSR(NOOK):
+ gui_name = _('Nook Simple')
+ description = _('Communicate with the Nook TSR eBook reader.')
+
+ PRODUCT_ID = [0x003]
+ BCD = [0x216]
+
+ EBOOK_DIR_MAIN = EBOOK_DIR_CARD_A = 'My Files/Books'
+
+
diff --git a/src/calibre/ebooks/epub/output.py b/src/calibre/ebooks/epub/output.py
index 0ed6d7e222..bea90eeba8 100644
--- a/src/calibre/ebooks/epub/output.py
+++ b/src/calibre/ebooks/epub/output.py
@@ -413,6 +413,13 @@ class EPUBOutput(OutputFormatPlugin):
rule.style.removeProperty('margin-left')
# padding-left breaks rendering in webkit and gecko
rule.style.removeProperty('padding-left')
+ # Change whitespace:pre to pre-line to accommodate readers that
+ # cannot scroll horizontally
+ for rule in stylesheet.data.cssRules.rulesOfType(CSSRule.STYLE_RULE):
+ style = rule.style
+ ws = style.getPropertyValue('white-space')
+ if ws == 'pre':
+ style.setProperty('white-space', 'pre-wrap')
# }}}
diff --git a/src/calibre/ebooks/metadata/sources/amazon.py b/src/calibre/ebooks/metadata/sources/amazon.py
index f291959475..7da37ce5af 100644
--- a/src/calibre/ebooks/metadata/sources/amazon.py
+++ b/src/calibre/ebooks/metadata/sources/amazon.py
@@ -29,7 +29,7 @@ class Worker(Thread): # Get details {{{
Get book details from amazons book page in a separate thread
'''
- def __init__(self, url, result_queue, browser, log, relevance, plugin, timeout=20):
+ def __init__(self, url, result_queue, browser, log, relevance, domain, plugin, timeout=20):
Thread.__init__(self)
self.daemon = True
self.url, self.result_queue = url, result_queue
@@ -37,7 +37,7 @@ class Worker(Thread): # Get details {{{
self.relevance, self.plugin = relevance, plugin
self.browser = browser.clone_browser()
self.cover_url = self.amazon_id = self.isbn = None
- self.domain = self.plugin.domain
+ self.domain = domain
months = {
'de': {
@@ -199,7 +199,8 @@ class Worker(Thread): # Get details {{{
return
mi = Metadata(title, authors)
- mi.set_identifier('amazon', asin)
+ idtype = 'amazon' if self.domain == 'com' else 'amazon_'+self.domain
+ mi.set_identifier(idtype, asin)
self.amazon_id = asin
try:
@@ -404,12 +405,30 @@ class Amazon(Source):
'country\'s Amazon website.'), choices=AMAZON_DOMAINS),
)
+ def get_domain_and_asin(self, identifiers):
+ for key, val in identifiers.iteritems():
+ key = key.lower()
+ if key in ('amazon', 'asin'):
+ return 'com', val
+ if key.startswith('amazon_'):
+ domain = key.split('_')[-1]
+ if domain and domain in self.AMAZON_DOMAINS:
+ return domain, val
+ return None, None
+
def get_book_url(self, identifiers): # {{{
- asin = identifiers.get('amazon', None)
- if asin is None:
- asin = identifiers.get('asin', None)
- if asin:
- return ('amazon', asin, 'http://amzn.com/%s'%asin)
+ domain, asin = self.get_domain_and_asin(identifiers)
+ if domain and asin:
+ url = None
+ if domain == 'com':
+ url = 'http://amzn.com/'+asin
+ elif domain == 'uk':
+ url = 'http://www.amazon.co.uk/dp/'+asin
+ else:
+ url = 'http://www.amazon.%s/dp/%s'%(domain, asin)
+ if url:
+ idtype = 'amazon' if self.domain == 'com' else 'amazon_'+self.domain
+ return (idtype, asin, url)
# }}}
@property
@@ -420,8 +439,14 @@ class Amazon(Source):
return domain
- def create_query(self, log, title=None, authors=None, identifiers={}): # {{{
- domain = self.domain
+ def create_query(self, log, title=None, authors=None, identifiers={}, # {{{
+ domain=None):
+ if domain is None:
+ domain = self.domain
+
+ idomain, asin = self.get_domain_and_asin(identifiers)
+ if idomain is not None:
+ domain = idomain
# See the amazon detailed search page to get all options
q = { 'search-alias' : 'aps',
@@ -433,7 +458,6 @@ class Amazon(Source):
else:
q['sort'] = 'relevancerank'
- asin = identifiers.get('amazon', None)
isbn = check_isbn(identifiers.get('isbn', None))
if asin is not None:
@@ -456,23 +480,22 @@ class Amazon(Source):
if not ('field-keywords' in q or 'field-isbn' in q or
('field-title' in q)):
# Insufficient metadata to make an identify query
- return None
+ return None, None
latin1q = dict([(x.encode('latin1', 'ignore'), y.encode('latin1',
'ignore')) for x, y in
q.iteritems()])
+ udomain = domain
if domain == 'uk':
- domain = 'co.uk'
- url = 'http://www.amazon.%s/s/?'%domain + urlencode(latin1q)
- return url
+ udomain = 'co.uk'
+ url = 'http://www.amazon.%s/s/?'%udomain + urlencode(latin1q)
+ return url, domain
# }}}
def get_cached_cover_url(self, identifiers): # {{{
url = None
- asin = identifiers.get('amazon', None)
- if asin is None:
- asin = identifiers.get('asin', None)
+ domain, asin = self.get_domain_and_asin(identifiers)
if asin is None:
isbn = identifiers.get('isbn', None)
if isbn is not None:
@@ -489,7 +512,7 @@ class Amazon(Source):
Note this method will retry without identifiers automatically if no
match is found with identifiers.
'''
- query = self.create_query(log, title=title, authors=authors,
+ query, domain = self.create_query(log, title=title, authors=authors,
identifiers=identifiers)
if query is None:
log.error('Insufficient metadata to construct query')
@@ -571,7 +594,7 @@ class Amazon(Source):
log.error('No matches found with query: %r'%query)
return
- workers = [Worker(url, result_queue, br, log, i, self) for i, url in
+ workers = [Worker(url, result_queue, br, log, i, domain, self) for i, url in
enumerate(matches)]
for w in workers:
diff --git a/src/calibre/ebooks/metadata/sources/douban.py b/src/calibre/ebooks/metadata/sources/douban.py
index 3c6bb7b6c7..70bf01a00e 100644
--- a/src/calibre/ebooks/metadata/sources/douban.py
+++ b/src/calibre/ebooks/metadata/sources/douban.py
@@ -211,7 +211,10 @@ class Douban(Source):
'q': q,
})
if self.DOUBAN_API_KEY and self.DOUBAN_API_KEY != '':
- url = url + "?apikey=" + self.DOUBAN_API_KEY
+ if t == "isbn" or t == "subject":
+ url = url + "?apikey=" + self.DOUBAN_API_KEY
+ else:
+ url = url + "&apikey=" + self.DOUBAN_API_KEY
return url
# }}}
diff --git a/src/calibre/ebooks/mobi/mobiml.py b/src/calibre/ebooks/mobi/mobiml.py
index 3c36a6166d..2275552c15 100644
--- a/src/calibre/ebooks/mobi/mobiml.py
+++ b/src/calibre/ebooks/mobi/mobiml.py
@@ -297,9 +297,11 @@ class MobiMLizer(object):
if id_:
# Keep anchors so people can use display:none
# to generate hidden TOCs
+ tail = elem.tail
elem.clear()
elem.text = None
elem.set('id', id_)
+ elem.tail = tail
else:
return
tag = barename(elem.tag)
@@ -309,7 +311,8 @@ class MobiMLizer(object):
istates.append(istate)
left = 0
display = style['display']
- isblock = not display.startswith('inline')
+ isblock = (not display.startswith('inline') and style['display'] !=
+ 'none')
isblock = isblock and style['float'] == 'none'
isblock = isblock and tag != 'br'
if isblock:
diff --git a/src/calibre/gui2/actions/store.py b/src/calibre/gui2/actions/store.py
index c8507e851c..6d9720548e 100644
--- a/src/calibre/gui2/actions/store.py
+++ b/src/calibre/gui2/actions/store.py
@@ -34,6 +34,8 @@ class StoreAction(InterfaceAction):
self.store_list_menu = self.store_menu.addMenu(_('Stores'))
for n, p in sorted(self.gui.istores.items(), key=lambda x: x[0].lower()):
self.store_list_menu.addAction(n, partial(self.open_store, p))
+ self.store_menu.addSeparator()
+ self.store_menu.addAction(_('Choose stores'), self.choose)
self.qaction.setMenu(self.store_menu)
def do_search(self):
@@ -42,7 +44,7 @@ class StoreAction(InterfaceAction):
def search(self, query=''):
self.show_disclaimer()
from calibre.gui2.store.search.search import SearchDialog
- sd = SearchDialog(self.gui.istores, self.gui, query)
+ sd = SearchDialog(self.gui, self.gui, query)
sd.exec_()
def _get_selected_row(self):
@@ -107,6 +109,13 @@ class StoreAction(InterfaceAction):
query = 'author:"%s" title:"%s"' % (self._get_author(row), self._get_title(row))
self.search(query)
+ def choose(self):
+ from calibre.gui2.store.config.chooser.chooser_dialog import StoreChooserDialog
+ d = StoreChooserDialog(self.gui)
+ d.exec_()
+ self.gui.load_store_plugins()
+ self.load_menu()
+
def open_store(self, store_plugin):
self.show_disclaimer()
store_plugin.open(self.gui)
diff --git a/src/calibre/gui2/dialogs/scheduler.py b/src/calibre/gui2/dialogs/scheduler.py
index b25d66979d..7d1d87b472 100644
--- a/src/calibre/gui2/dialogs/scheduler.py
+++ b/src/calibre/gui2/dialogs/scheduler.py
@@ -207,8 +207,9 @@ class SchedulerDialog(QDialog, Ui_Dialog):
self.recipe_model.searched.connect(self.search.search_done,
type=Qt.QueuedConnection)
self.recipe_model.searched.connect(self.search_done)
- self.search.setFocus(Qt.OtherFocusReason)
+ self.recipes.setFocus(Qt.OtherFocusReason)
self.commit_on_change = True
+ self.previous_urn = None
self.recipes.setModel(self.recipe_model)
self.detail_box.setVisible(False)
@@ -228,6 +229,9 @@ class SchedulerDialog(QDialog, Ui_Dialog):
self.old_news.setValue(gconf['oldest_news'])
+ self.go_button.clicked.connect(self.search.do_search)
+ self.clear_search_button.clicked.connect(self.search.clear_clicked)
+
def set_pw_echo_mode(self, state):
self.password.setEchoMode(self.password.Normal
if state == Qt.Checked else self.password.Password)
@@ -265,14 +269,9 @@ class SchedulerDialog(QDialog, Ui_Dialog):
self.last_downloaded.setVisible(enabled)
def current_changed(self, current, previous):
- if self.commit_on_change:
- if previous.isValid():
- if not self.commit(urn=getattr(previous.internalPointer(),
- 'urn', None)):
- self.commit_on_change = False
- self.recipes.setCurrentIndex(previous)
- else:
- self.commit_on_change = True
+ if self.previous_urn is not None:
+ self.commit(urn=self.previous_urn)
+ self.previous_urn = None
urn = self.current_urn
if urn is not None:
@@ -332,6 +331,7 @@ class SchedulerDialog(QDialog, Ui_Dialog):
return True
def initialize_detail_box(self, urn):
+ self.previous_urn = urn
self.detail_box.setVisible(True)
self.download_button.setVisible(True)
self.detail_box.setCurrentIndex(0)
diff --git a/src/calibre/gui2/dialogs/scheduler.ui b/src/calibre/gui2/dialogs/scheduler.ui
index f26bfc7285..6acbb01dd8 100644
--- a/src/calibre/gui2/dialogs/scheduler.ui
+++ b/src/calibre/gui2/dialogs/scheduler.ui
@@ -17,21 +17,30 @@
' +
- _('If you want to color a field based on tags, then right-click '
- 'in an empty template line and choose tags wizard. '
+ _('If you want to color a field based on tags, then click the '
+ 'button next to an empty line to open the tags wizard. '
'It will build a template for you. You can later edit that '
'template with the same wizard. If you edit it by hand, the '
'wizard might not work or might restore old values.') +
diff --git a/src/calibre/gui2/preferences/look_feel.ui b/src/calibre/gui2/preferences/look_feel.ui
index a67a3585cb..fe6134f235 100644
--- a/src/calibre/gui2/preferences/look_feel.ui
+++ b/src/calibre/gui2/preferences/look_feel.ui
@@ -442,6 +442,9 @@ then the tags will be displayed each on their own line.
This store is currently diabled and cannot be used in other parts of calibre. This store is currently enabled and can be used in other parts of calibre. %s This store only distributes ebooks with DRM. This store distributes ebooks with DRM. It may have some titles without DRM, but you will need to check on a per title basis. This store is headquartered in %s. This is a good indication of what market the store caters to. However, this does not necessarily mean that the store is limited to that market only. This store distributes ebooks in the following formats: %s '+_('Setup sending email using') +
+ ' {name} ' +
+ _('If you don\'t have an account, you can sign up for a free {name} email '
+ 'account at http://{url}. {extra}')).format(
+ **service))
+ l.addWidget(self.tl, 0, 0, 3, 0)
+ self.tl.setWordWrap(True)
+ self.tl.setOpenExternalLinks(True)
+ for name, label in (
+ ['from_', _('Your %s &email address:')],
+ ['username', _('Your %s &username:')],
+ ['password', _('Your %s &password:')],
+ ):
+ la = QLabel(label%service['name'])
+ le = QLineEdit(self)
+ setattr(self, name, le)
+ setattr(self, name+'_label', la)
+ r = l.rowCount()
+ l.addWidget(la, r, 0)
+ l.addWidget(le, r, 1)
+ la.setBuddy(le)
+ if name == 'password':
+ self.ptoggle = QCheckBox(_('&Show password'), self)
+ l.addWidget(self.ptoggle, r, 2)
+ self.ptoggle.stateChanged.connect(
+ lambda s: self.password.setEchoMode(self.password.Normal if s
+ == Qt.Checked else self.password.Password))
+ self.username.setText(service['username'])
+ self.password.setEchoMode(self.password.Password)
+ self.bl = QLabel(' ' + _(
+ 'If you plan to use email to send books to your Kindle, remember to'
+ ' add the your %s email address to the allowed email addresses in your '
+ 'Amazon.com Kindle management page.')%service['name'])
+ self.bl.setWordWrap(True)
+ l.addWidget(self.bl, l.rowCount(), 0, 3, 0)
+ l.addWidget(bb, l.rowCount(), 0, 3, 0)
+ self.setWindowTitle(_('Setup') + ' ' + service['name'])
+ self.resize(self.sizeHint())
+ self.service = service
+
+ def accept(self):
+ un = unicode(self.username.text())
+ if self.service.get('at_in_username', False) and '@' not in un:
+ return error_dialog(self, _('Incorrect username'),
+ _('%s needs the full email address as your username') %
+ self.service['name'], show=True)
+ QDialog.accept(self)
+
class SendEmail(QWidget, Ui_Form):
@@ -129,7 +187,8 @@ class SendEmail(QWidget, Ui_Form):
'port': 587,
'username': '@gmail.com',
'url': 'www.gmail.com',
- 'extra': ''
+ 'extra': '',
+ 'at_in_username': True,
},
'hotmail': {
'name': 'Hotmail',
@@ -143,53 +202,10 @@ class SendEmail(QWidget, Ui_Form):
' will let calibre send email. In this case, I'
' strongly suggest you setup a free gmail account'
' instead.'),
+ 'at_in_username': True,
}
}[service]
- d = QDialog(self)
- l = QGridLayout()
- d.setLayout(l)
- bb = QDialogButtonBox(QDialogButtonBox.Ok|QDialogButtonBox.Cancel)
- bb.accepted.connect(d.accept)
- bb.rejected.connect(d.reject)
- d.tl = QLabel((' '+_('Setup sending email using') +
- ' {name} ' +
- _('If you don\'t have an account, you can sign up for a free {name} email '
- 'account at http://{url}. {extra}')).format(
- **service))
- l.addWidget(d.tl, 0, 0, 3, 0)
- d.tl.setWordWrap(True)
- d.tl.setOpenExternalLinks(True)
- for name, label in (
- ['from_', _('Your %s &email address:')],
- ['username', _('Your %s &username:')],
- ['password', _('Your %s &password:')],
- ):
- la = QLabel(label%service['name'])
- le = QLineEdit(d)
- setattr(d, name, le)
- setattr(d, name+'_label', la)
- r = l.rowCount()
- l.addWidget(la, r, 0)
- l.addWidget(le, r, 1)
- la.setBuddy(le)
- if name == 'password':
- d.ptoggle = QCheckBox(_('&Show password'), d)
- l.addWidget(d.ptoggle, r, 2)
- d.ptoggle.stateChanged.connect(
- lambda s: d.password.setEchoMode(d.password.Normal if s
- == Qt.Checked else d.password.Password))
- d.username.setText(service['username'])
- d.password.setEchoMode(d.password.Password)
- d.bl = QLabel(' ' + _(
- 'If you plan to use email to send books to your Kindle, remember to'
- ' add the your %s email address to the allowed email addresses in your '
- 'Amazon.com Kindle management page.')%service['name'])
- d.bl.setWordWrap(True)
- l.addWidget(d.bl, l.rowCount(), 0, 3, 0)
- l.addWidget(bb, l.rowCount(), 0, 3, 0)
- d.setWindowTitle(_('Setup') + ' ' + service['name'])
- d.resize(d.sizeHint())
- bb.setVisible(True)
+ d = RelaySetup(service, self)
if d.exec_() != d.Accepted:
return
self.relay_username.setText(d.username.text())
diff --git a/src/calibre/library/server/base.py b/src/calibre/library/server/base.py
index 862e724809..319feefa44 100644
--- a/src/calibre/library/server/base.py
+++ b/src/calibre/library/server/base.py
@@ -221,7 +221,12 @@ class LibraryServer(ContentServer, MobileServer, XMLServer, OPDSServer, Cache,
if not ip or ip.startswith('127.'):
raise
cherrypy.log('Trying to bind to single interface: '+ip)
+ # Change the host we listen on
cherrypy.config.update({'server.socket_host' : ip})
+ # This ensures that the change is actually applied
+ cherrypy.server.socket_host = ip
+ cherrypy.server.httpserver = cherrypy.server.instance = None
+
cherrypy.engine.start()
self.is_running = True
@@ -231,6 +236,8 @@ class LibraryServer(ContentServer, MobileServer, XMLServer, OPDSServer, Cache,
cherrypy.engine.block()
except Exception as e:
self.exception = e
+ import traceback
+ traceback.print_exc()
finally:
self.is_running = False
try:
diff --git a/src/calibre/linux.py b/src/calibre/linux.py
index 1e7a62b869..9e58d4f638 100644
--- a/src/calibre/linux.py
+++ b/src/calibre/linux.py
@@ -356,7 +356,7 @@ class PostInstall:
mimetypes = set([])
for x in all_input_formats():
mt = guess_type('dummy.'+x)[0]
- if mt and 'chemical' not in mt:
+ if mt and 'chemical' not in mt and 'ctc-posml' not in mt:
mimetypes.add(mt)
def write_mimetypes(f):
@@ -376,11 +376,10 @@ class PostInstall:
des = ('calibre-gui.desktop', 'calibre-lrfviewer.desktop',
'calibre-ebook-viewer.desktop')
for x in des:
- cmd = ['xdg-desktop-menu', 'install', './'+x]
- if x != des[-1]:
- cmd.insert(2, '--noupdate')
+ cmd = ['xdg-desktop-menu', 'install', '--noupdate', './'+x]
check_call(' '.join(cmd), shell=True)
self.menu_resources.append(x)
+ check_call(['xdg-desktop-menu', 'forceupdate'])
f = open('calibre-mimetypes', 'wb')
f.write(MIME)
f.close()
diff --git a/src/calibre/manual/faq.rst b/src/calibre/manual/faq.rst
index d3784eda6f..1c0b49f30b 100644
--- a/src/calibre/manual/faq.rst
+++ b/src/calibre/manual/faq.rst
@@ -138,7 +138,7 @@ Follow these steps to find the problem:
My device is non-standard or unusual. What can I do to connect to it?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-In addition to the :guilabel:`Connect to Folder` function found under the Connect/Share button, |app| provides a ``User Defined`` device plugin that can be used to connect to any USB device that presents that shows up as a disk drive in your operating system. See the device plugin ``Preferences -> Plugins -> Device Plugins -> User Defined`` and ``Preferences -> Miscellaneous -> Get information to setup the user defined device`` for more information.
+In addition to the :guilabel:`Connect to Folder` function found under the Connect/Share button, |app| provides a ``User Defined`` device plugin that can be used to connect to any USB device that shows up as a disk drive in your operating system. Note: on windows, the device must have a drive letter for calibre to use it. See the device plugin ``Preferences -> Plugins -> Device Plugins -> User Defined`` and ``Preferences -> Miscellaneous -> Get information to setup the user defined device`` for more information.
How does |app| manage collections on my SONY reader?
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -587,7 +587,7 @@ You can download news and convert it into an ebook with the command::
/opt/calibre/ebook-convert "Title of news source.recipe" outputfile.epub
-If you want to generate MOBI, use outputfile.mobi instead.
+If you want to generate MOBI, use outputfile.mobi instead and use ``--output-profile kindle``.
You can email downloaded news with the command::
diff --git a/src/calibre/manual/template_lang.rst b/src/calibre/manual/template_lang.rst
index 69c77e5bfd..16a90f7531 100644
--- a/src/calibre/manual/template_lang.rst
+++ b/src/calibre/manual/template_lang.rst
@@ -229,13 +229,14 @@ For various values of series_index, the program returns:
The following functions are available in addition to those described in single-function mode. Remember from the example above that the single-function mode functions require an additional first parameter specifying the field to operate on. With the exception of the ``id`` parameter of assign, all parameters can be statements (sequences of expressions):
+ * ``and(value, value, ...)`` -- returns the string "1" if all values are not empty, otherwise returns the empty string. This function works well with test or first_non_empty. You can have as many values as you want.
* ``add(x, y)`` -- returns x + y. Throws an exception if either x or y are not numbers.
* ``assign(id, val)`` -- assigns val to id, then returns val. id must be an identifier, not an expression
* ``booksize()`` -- returns the value of the |app| 'size' field. Returns '' if there are no formats.
* ``cmp(x, y, lt, eq, gt)`` -- compares x and y after converting both to numbers. Returns ``lt`` if x < y. Returns ``eq`` if x == y. Otherwise returns ``gt``.
* ``divide(x, y)`` -- returns x / y. Throws an exception if either x or y are not numbers.
* ``field(name)`` -- returns the metadata field named by ``name``.
- * ``first_non_empty(value, value, ...) -- returns the first value that is not empty. If all values are empty, then the empty value is returned. You can have as many values as you want.
+ * ``first_non_empty(value, value, ...)`` -- returns the first value that is not empty. If all values are empty, then the empty value is returned. You can have as many values as you want.
* ``format_date(x, date_format)`` -- format_date(val, format_string) -- format the value, which must be a date field, using the format_string, returning a string. The formatting codes are::
d : the day as number without a leading zero (1 to 31)
@@ -251,7 +252,9 @@ The following functions are available in addition to those described in single-f
iso : the date with time and timezone. Must be the only format present.
* ``eval(string)`` -- evaluates the string as a program, passing the local variables (those ``assign`` ed to). This permits using the template processor to construct complex results from local variables.
+ * ``not(value)`` -- returns the string "1" if the value is empty, otherwise returns the empty string. This function works well with test or first_non_empty. You can have as many values as you want.
* ``multiply(x, y)`` -- returns x * y. Throws an exception if either x or y are not numbers.
+ * ``or(value, value, ...)`` -- returns the string "1" if any value is not empty, otherwise returns the empty string. This function works well with test or first_non_empty. You can have as many values as you want.
* ``print(a, b, ...)`` -- prints the arguments to standard output. Unless you start calibre from the command line (``calibre-debug -g``), the output will go to a black hole.
* ``raw_field(name)`` -- returns the metadata field named by name without applying any formatting.
* ``strcat(a, b, ...)`` -- can take any number of arguments. Returns a string formed by concatenating all the arguments.
@@ -259,7 +262,22 @@ The following functions are available in addition to those described in single-f
* ``substr(str, start, end)`` -- returns the ``start``'th through the ``end``'th characters of ``str``. The first character in ``str`` is the zero'th character. If end is negative, then it indicates that many characters counting from the right. If end is zero, then it indicates the last character. For example, ``substr('12345', 1, 0)`` returns ``'2345'``, and ``substr('12345', 1, -1)`` returns ``'234'``.
* ``subtract(x, y)`` -- returns x - y. Throws an exception if either x or y are not numbers.
* ``template(x)`` -- evaluates x as a template. The evaluation is done in its own context, meaning that variables are not shared between the caller and the template evaluation. Because the `{` and `}` characters are special, you must use `[[` for the `{` character and `]]` for the '}' character; they are converted automatically. For example, ``template('[[title_sort]]') will evaluate the template ``{title_sort}`` and return its value.
+
+Function classification summary:
+ * Get values from metadata: ``field``. ``raw_field``. In some situations, ``lookup`` can be used in place of ``field``.
+ * Arithmetic: ``add``, ``subtract``, ``multiply``, ``divide``
+ * Boolean: ``and``, ``or``, ``not``. The function ``if_empty`` is similar to ``and`` called with one argument.
+ * If-then-else: ``contains``, ``test``
+ * Iterating over values: ``first_non_empty``, ``lookup``, ``switch``
+ * List lookup: ``in_list``, ``list_item``, ``select``,
+ * List manipulation: ``count``, ``sublist``, ``subitems``
+ * Recursion: ``eval``, ``template``
+ * Relational: ``cmp`` , ``strcmp`` for strings
+ * String case changes: ``lowercase``, ``uppercase``, ``titlecase``, ``capitalize``
+ * String manipulation: ``re``, ``shorten``, ``substr``
+ * Other: ``assign``, ``booksize``, ``print``, ``format_date``,
+
.. _general_mode:
Using general program mode
diff --git a/src/calibre/utils/formatter_functions.py b/src/calibre/utils/formatter_functions.py
index c53277f3ce..a3a156648f 100644
--- a/src/calibre/utils/formatter_functions.py
+++ b/src/calibre/utils/formatter_functions.py
@@ -594,7 +594,56 @@ class BuiltinFirstNonEmpty(BuiltinFormatterFunction):
i += 1
return ''
+class BuiltinAnd(BuiltinFormatterFunction):
+ name = 'and'
+ arg_count = -1
+ doc = _('and(value, value, ...) -- '
+ 'returns the string "1" if all values are not empty, otherwise '
+ 'returns the empty string. This function works well with test or '
+ 'first_non_empty. You can have as many values as you want.')
+
+ def evaluate(self, formatter, kwargs, mi, locals, *args):
+ i = 0
+ while i < len(args):
+ if not args[i]:
+ return ''
+ i += 1
+ return '1'
+
+class BuiltinOr(BuiltinFormatterFunction):
+ name = 'or'
+ arg_count = -1
+ doc = _('or(value, value, ...) -- '
+ 'returns the string "1" if any value is not empty, otherwise '
+ 'returns the empty string. This function works well with test or '
+ 'first_non_empty. You can have as many values as you want.')
+
+ def evaluate(self, formatter, kwargs, mi, locals, *args):
+ i = 0
+ while i < len(args):
+ if args[i]:
+ return '1'
+ i += 1
+ return ''
+
+class BuiltinNot(BuiltinFormatterFunction):
+ name = 'not'
+ arg_count = 1
+ doc = _('not(value) -- '
+ 'returns the string "1" if the value is empty, otherwise '
+ 'returns the empty string. This function works well with test or '
+ 'first_non_empty. You can have as many values as you want.')
+
+ def evaluate(self, formatter, kwargs, mi, locals, *args):
+ i = 0
+ while i < len(args):
+ if args[i]:
+ return '1'
+ i += 1
+ return ''
+
builtin_add = BuiltinAdd()
+builtin_and = BuiltinAnd()
builtin_assign = BuiltinAssign()
builtin_booksize = BuiltinBooksize()
builtin_capitalize = BuiltinCapitalize()
@@ -612,6 +661,8 @@ builtin_list_item = BuiltinListitem()
builtin_lookup = BuiltinLookup()
builtin_lowercase = BuiltinLowercase()
builtin_multiply = BuiltinMultiply()
+builtin_not = BuiltinNot()
+builtin_or = BuiltinOr()
builtin_print = BuiltinPrint()
builtin_raw_field = BuiltinRaw_field()
builtin_re = BuiltinRe()
%s' % (result.name, result.description))
+ elif col == 3:
+ return QVariant(result.headquarters)
+ elif col == 4:
+ return QVariant(', '.join(result.formats).upper())
+ elif role == Qt.DecorationRole:
+ if col == 2:
+ if result.drm_free_only:
+ return QVariant(self.NO_DRM_ICON)
+ elif role == Qt.CheckStateRole:
+ if col == 0:
+ if is_disabled(result):
+ return Qt.Unchecked
+ return Qt.Checked
+ elif role == Qt.ToolTipRole:
+ if col == 0:
+ if is_disabled(result):
+ return QVariant(_('