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 7656db4021..b0a2225dd4 100644 --- a/resources/template-functions.json +++ b/resources/template-functions.json @@ -1,19 +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", "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", - "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", + "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", + "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", - "field": "def evaluate(self, formatter, kwargs, mi, locals, name):\n return formatter.get_value(name, [], kwargs)\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", @@ -23,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 9172a5aec6..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, @@ -1277,7 +1276,7 @@ class StoreLegimiStore(StoreBase): 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'] @@ -1346,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' @@ -1376,7 +1385,7 @@ 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' @@ -1411,6 +1420,7 @@ plugins += [ StoreOReillyStore, StorePragmaticBookshelfStore, StoreSmashwordsStore, + StoreVirtualoStore, StoreWaterstonesUKStore, StoreWeightlessBooksStore, StoreWizardsTowerBooksStore, 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/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 0fd783f0a3..6d9720548e 100644 --- a/src/calibre/gui2/actions/store.py +++ b/src/calibre/gui2/actions/store.py @@ -44,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): 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 @@ :/images/scheduler.png:/images/scheduler.png - + - - - &Search: - - - search - - + + + + + + + + Go + + + + + + + + :/images/clear_left.png:/images/clear_left.png + + + + - - - - + QFrame::NoFrame @@ -44,7 +53,7 @@ 0 0 - 486 + 524 504 @@ -320,7 +329,7 @@ - + @@ -345,7 +354,17 @@ - + + + + + + + Qt::AlignCenter + + + + @@ -376,17 +395,7 @@ - - - - Qt::Horizontal - - - QDialogButtonBox::Save - - - - + Download all scheduled news sources at once @@ -394,15 +403,19 @@ Download &all scheduled + + + :/images/news.png:/images/news.png + - - - - + + + + Qt::Horizontal - - Qt::AlignCenter + + QDialogButtonBox::Save diff --git a/src/calibre/gui2/store/config/chooser/models.py b/src/calibre/gui2/store/config/chooser/models.py index 0c784f6614..6c95d74ffc 100644 --- a/src/calibre/gui2/store/config/chooser/models.py +++ b/src/calibre/gui2/store/config/chooser/models.py @@ -103,7 +103,22 @@ class Matches(QAbstractItemModel): return Qt.Unchecked return Qt.Checked elif role == Qt.ToolTipRole: - return QVariant('

%s

' % result.description) + if col == 0: + if is_disabled(result): + return QVariant(_('

This store is currently diabled and cannot be used in other parts of calibre.

')) + else: + return QVariant(_('

This store is currently enabled and can be used in other parts of calibre.

')) + elif col == 1: + return QVariant('

%s

' % result.description) + elif col == 2: + if result.drm_free_only: + return QVariant(_('

This store only distributes ebooks with DRM.

')) + else: + return QVariant(_('

This store distributes ebooks with DRM. It may have some titles without DRM, but you will need to check on a per title basis.

')) + elif col == 3: + return QVariant(_('

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.

') % result.headquarters) + elif col == 4: + return QVariant(_('

This store distributes ebooks in the following formats: %s

') % ', '.join(result.formats)) return NONE def setData(self, index, data, role): @@ -130,7 +145,7 @@ class Matches(QAbstractItemModel): elif col == 1: text = match.name elif col == 2: - text = 'b' if getattr(match, 'drm', True) else 'a' + text = 'a' if getattr(match, 'drm_free_only', True) else 'b' elif col == 3: text = getattr(match, 'headquarters', '') return text @@ -240,5 +255,3 @@ class SearchFilter(SearchQueryParser): import traceback traceback.print_exc() return matches - - diff --git a/src/calibre/gui2/store/search/search.py b/src/calibre/gui2/store/search/search.py index 3be39e3e87..faeaf507c9 100644 --- a/src/calibre/gui2/store/search/search.py +++ b/src/calibre/gui2/store/search/search.py @@ -10,10 +10,11 @@ import re from random import shuffle from PyQt4.Qt import (Qt, QDialog, QDialogButtonBox, QTimer, QCheckBox, - QVBoxLayout, QIcon, QWidget) + QVBoxLayout, QIcon, QWidget, QTabWidget) from calibre.gui2 import JSONConfig, info_dialog from calibre.gui2.progress_indicator import ProgressIndicator +from calibre.gui2.store.config.chooser.chooser_widget import StoreChooserWidget from calibre.gui2.store.config.search.search_widget import StoreConfigWidget from calibre.gui2.store.search.adv_search_builder import AdvSearchBuilderDialog from calibre.gui2.store.search.download_thread import SearchThreadPool, \ @@ -22,7 +23,7 @@ from calibre.gui2.store.search.search_ui import Ui_Dialog class SearchDialog(QDialog, Ui_Dialog): - def __init__(self, istores, parent=None, query=''): + def __init__(self, gui, parent=None, query=''): QDialog.__init__(self, parent) self.setupUi(self) @@ -34,8 +35,7 @@ class SearchDialog(QDialog, Ui_Dialog): # the variables it sets up are used later. self.load_settings() - # We keep a cache of store plugins and reference them by name. - self.store_plugins = istores + self.gui = gui # Setup our worker threads. self.search_pool = SearchThreadPool(self.search_thread_count) @@ -49,22 +49,11 @@ class SearchDialog(QDialog, Ui_Dialog): self.hang_check = 0 # Update store caches silently. - for p in self.store_plugins.values(): + for p in self.gui.istores.values(): self.cache_pool.add_task(p, self.timeout) - # Add check boxes for each store so the user - # can disable searching specific stores on a - # per search basis. - stores_check_widget = QWidget() - store_list_layout = QVBoxLayout() - stores_check_widget.setLayout(store_list_layout) - for x in sorted(self.store_plugins.keys(), key=lambda x: x.lower()): - cbox = QCheckBox(x) - cbox.setChecked(False) - store_list_layout.addWidget(cbox) - setattr(self, 'store_check_' + x, cbox) - store_list_layout.addStretch() - self.store_list.setWidget(stores_check_widget) + self.store_checks = {} + self.setup_store_checks() # Set the search query self.search_edit.setText(query) @@ -91,6 +80,27 @@ class SearchDialog(QDialog, Ui_Dialog): self.progress_checker.start(100) self.restore_state() + + def setup_store_checks(self): + # Add check boxes for each store so the user + # can disable searching specific stores on a + # per search basis. + existing = {} + for n in self.store_checks: + existing[n] = self.store_checks[n].isChecked() + + self.store_checks = {} + + stores_check_widget = QWidget() + store_list_layout = QVBoxLayout() + stores_check_widget.setLayout(store_list_layout) + for x in sorted(self.gui.istores.keys(), key=lambda x: x.lower()): + cbox = QCheckBox(x) + cbox.setChecked(existing.get(x, False)) + store_list_layout.addWidget(cbox) + self.store_checks[x] = cbox + store_list_layout.addStretch() + self.store_list.setWidget(stores_check_widget) def build_adv_search(self): adv = AdvSearchBuilderDialog(self) @@ -126,11 +136,12 @@ class SearchDialog(QDialog, Ui_Dialog): # futher filtering. self.results_view.model().set_query(query) - # Plugins are in alphebetic order. Randomize the - # order of plugin names. This way plugins closer + # Plugins are in random order that does not change. + # Randomize the ord of the plugin names every time + # there is a search. This way plugins closer # to a don't have an unfair advantage over # plugins further from a. - store_names = self.store_plugins.keys() + store_names = self.store_checks.keys() if not store_names: return # Remove all of our internal filtering logic from the query. @@ -138,8 +149,8 @@ class SearchDialog(QDialog, Ui_Dialog): shuffle(store_names) # Add plugins that the user has checked to the search pool's work queue. for n in store_names: - if getattr(self, 'store_check_' + n).isChecked(): - self.search_pool.add_task(query, n, self.store_plugins[n], self.max_results, self.timeout) + if self.store_checks[n].isChecked(): + self.search_pool.add_task(query, n, self.gui.istores[n], self.max_results, self.timeout) self.hang_check = 0 self.checker.start(100) self.pi.startAnimation() @@ -179,8 +190,8 @@ class SearchDialog(QDialog, Ui_Dialog): self.config['open_external'] = self.open_external.isChecked() store_check = {} - for n in self.store_plugins: - store_check[n] = getattr(self, 'store_check_' + n).isChecked() + for k, v in self.store_checks.items(): + store_check[k] = v.isChecked() self.config['store_checked'] = store_check def restore_state(self): @@ -206,8 +217,8 @@ class SearchDialog(QDialog, Ui_Dialog): store_check = self.config.get('store_checked', None) if store_check: for n in store_check: - if hasattr(self, 'store_check_' + n): - getattr(self, 'store_check_' + n).setChecked(store_check[n]) + if n in self.store_checks: + self.store_checks[n].setChecked(store_check[n]) self.results_view.model().sort_col = self.config.get('sort_col', 2) self.results_view.model().sort_order = self.config.get('sort_order', Qt.AscendingOrder) @@ -234,20 +245,27 @@ class SearchDialog(QDialog, Ui_Dialog): self.config['open_external'] = self.open_external.isChecked() d = QDialog(self) - button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) + button_box = QDialogButtonBox(QDialogButtonBox.Close) v = QVBoxLayout(d) button_box.accepted.connect(d.accept) button_box.rejected.connect(d.reject) d.setWindowTitle(_('Customize get books search')) - config_widget = StoreConfigWidget(self.config) - v.addWidget(config_widget) + + tab_widget = QTabWidget(d) + v.addWidget(tab_widget) v.addWidget(button_box) - d.exec_() + chooser_config_widget = StoreChooserWidget() + search_config_widget = StoreConfigWidget(self.config) + + tab_widget.addTab(chooser_config_widget, _('Choose stores')) + tab_widget.addTab(search_config_widget, _('Configure search')) - if d.result() == QDialog.Accepted: - config_widget.save_settings() - self.config_changed() + d.exec_() + search_config_widget.save_settings() + self.config_changed() + self.gui.load_store_plugins() + self.setup_store_checks() def config_changed(self): self.load_settings() @@ -283,7 +301,7 @@ class SearchDialog(QDialog, Ui_Dialog): def open_store(self, index): result = self.results_view.model().get_result(index) - self.store_plugins[result.store_name].open(self, result.detail_item, self.open_external.isChecked()) + self.gui.istores[result.store_name].open(self, result.detail_item, self.open_external.isChecked()) def check_progress(self): if not self.search_pool.threads_running() and not self.results_view.model().cover_pool.threads_running() and not self.results_view.model().details_pool.threads_running(): @@ -292,27 +310,16 @@ class SearchDialog(QDialog, Ui_Dialog): if not self.pi.isAnimated(): self.pi.startAnimation() - def get_store_checks(self): - ''' - Returns a list of QCheckBox's for each store. - ''' - checks = [] - for x in self.store_plugins: - check = getattr(self, 'store_check_' + x, None) - if check: - checks.append(check) - return checks - def stores_select_all(self): - for check in self.get_store_checks(): + for check in self.store_checks.values(): check.setChecked(True) def stores_select_invert(self): - for check in self.get_store_checks(): + for check in self.store_checks.values(): check.setChecked(not check.isChecked()) def stores_select_none(self): - for check in self.get_store_checks(): + for check in self.store_checks.values(): check.setChecked(False) def dialog_closed(self, result): diff --git a/src/calibre/gui2/store/virtualo_plugin.py b/src/calibre/gui2/store/virtualo_plugin.py new file mode 100644 index 0000000000..d86aa3e0e5 --- /dev/null +++ b/src/calibre/gui2/store/virtualo_plugin.py @@ -0,0 +1,72 @@ +# -*- coding: utf-8 -*- + +from __future__ import (unicode_literals, division, absolute_import, print_function) + +__license__ = 'GPL 3' +__copyright__ = '2011, Tomasz Długosz ' +__docformat__ = 'restructuredtext en' + +import re +import urllib +from contextlib import closing + +from lxml import html + +from PyQt4.Qt import QUrl + +from calibre import browser, url_slash_cleaner +from calibre.gui2 import open_url +from calibre.gui2.store import StorePlugin +from calibre.gui2.store.basic_config import BasicStoreConfig +from calibre.gui2.store.search_result import SearchResult +from calibre.gui2.store.web_store_dialog import WebStoreDialog + +class VirtualoStore(BasicStoreConfig, StorePlugin): + + def open(self, parent=None, detail_item=None, external=False): + url = 'http://virtualo.pl/ebook/c2/' + detail_url = None + + if external or self.config.get('open_external', False): + open_url(QUrl(url_slash_cleaner(detail_item if detail_item else url))) + else: + d = WebStoreDialog(self.gui, url, parent, detail_url) + d.setWindowTitle(self.name) + d.set_tags(self.config.get('tags', '')) + d.exec_() + + def search(self, query, max_results=10, timeout=60): + url = 'http://virtualo.pl/c2/?q=' + urllib.quote(query.encode('utf-8')) + + br = browser() + + counter = max_results + with closing(br.open(url, timeout=timeout)) as f: + doc = html.fromstring(f.read()) + for data in doc.xpath('//div[@id="product_list"]/div/div[@class="column"]'): + if counter <= 0: + break + + id = ''.join(data.xpath('.//table/tr[2]/td[1]/a/@href')) + if not id: + continue + + price = ''.join(data.xpath('.//span[@class="price"]/text() | .//span[@class="price abbr"]/text()')) + cover_url = ''.join(data.xpath('.//table/tr[2]/td[1]/a/img/@src')) + title = ''.join(data.xpath('.//div[@class="title"]/a/text()')) + author = ', '.join(data.xpath('.//div[@class="authors"]/a/text()')) + formats = ', '.join(data.xpath('.//span[@class="format"]/a/text()')) + formats = re.sub(r'(, )?ONLINE(, )?', '', formats) + + counter -= 1 + + s = SearchResult() + s.cover_url = cover_url + s.title = title.strip() + ' ' + formats + s.author = author.strip() + s.price = price + ' zł' + s.detail_item = 'http://virtualo.pl' + id.strip() + s.formats = formats.upper().strip() + s.drm = SearchResult.DRM_UNKNOWN + + yield s diff --git a/src/calibre/gui2/wizard/send_email.py b/src/calibre/gui2/wizard/send_email.py index 5c7d916e1a..4337e558eb 100644 --- a/src/calibre/gui2/wizard/send_email.py +++ b/src/calibre/gui2/wizard/send_email.py @@ -46,6 +46,64 @@ class TestEmail(QDialog, TE_Dialog): finally: self.test_button.setEnabled(True) +class RelaySetup(QDialog): + + def __init__(self, service, parent): + QDialog.__init__(self, parent) + + self.l = l = QGridLayout() + self.setLayout(l) + self.bb = bb = QDialogButtonBox(QDialogButtonBox.Ok|QDialogButtonBox.Cancel) + bb.accepted.connect(self.accept) + bb.rejected.connect(self.reject) + self.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(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/manual/faq.rst b/src/calibre/manual/faq.rst index b120fd4a1b..1c0b49f30b 100644 --- a/src/calibre/manual/faq.rst +++ b/src/calibre/manual/faq.rst @@ -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::