diff --git a/src/calibre/gui2/dialogs/scheduler.py b/src/calibre/gui2/dialogs/scheduler.py index b3ee95ec88..cdbf0a6dcd 100644 --- a/src/calibre/gui2/dialogs/scheduler.py +++ b/src/calibre/gui2/dialogs/scheduler.py @@ -11,7 +11,7 @@ import sys, copy from datetime import datetime, timedelta from PyQt4.Qt import QDialog, QApplication, QLineEdit, QPalette, SIGNAL, QBrush, \ QColor, QAbstractListModel, Qt, QVariant, QFont, QIcon, \ - QFile, QObject, QTimer, QMutex + QFile, QObject, QTimer, QMutex, QMenu, QAction from calibre import english_sort from calibre.gui2.dialogs.scheduler_ui import Ui_Dialog @@ -20,6 +20,7 @@ from calibre.utils.search_query_parser import SearchQueryParser from calibre.utils.pyparsing import ParseException from calibre.gui2 import NONE, error_dialog from calibre.utils.config import DynamicConfig +from calibre.gui2.dialogs.user_profiles import UserProfiles config = DynamicConfig('scheduler') @@ -307,6 +308,23 @@ class Scheduler(QObject): self.dirtied = False self.connect(self.timer, SIGNAL('timeout()'), self.check) self.timer.start(int(self.INTERVAL * 60000)) + + self.news_menu = QMenu() + self.news_icon = QIcon(':/images/news.svg') + self.scheduler_action = QAction(QIcon(':/images/scheduler.svg'), _('Schedule news download'), self) + self.news_menu.addAction(self.scheduler_action) + self.connect(self.scheduler_action, SIGNAL('triggered(bool)'), self.show_dialog) + self.cac = QAction(QIcon(':/images/user_profile.svg'), _('Add a custom news source'), self) + self.connect(self.cac, SIGNAL('triggered(bool)'), self.customize_feeds) + self.news_menu.addAction(self.cac) + + def customize_feeds(self, *args): + main = self.main + d = UserProfiles(main, main.library_view.model().db.get_feeds()) + if d.exec_() == QDialog.Accepted: + feeds = tuple(d.profiles()) + main.library_view.model().db.set_feeds(feeds) + def debug(self, *args): if self.verbose: @@ -387,7 +405,7 @@ class Scheduler(QObject): def refresh_schedule(self, recipes): self.recipes = recipes - def show_dialog(self): + def show_dialog(self, *args): self.lock.lock() try: d = SchedulerDialog(self.main.library_view.model().db) diff --git a/src/calibre/gui2/images/donate.svg b/src/calibre/gui2/images/donate.svg new file mode 100644 index 0000000000..b17d0ec7a0 --- /dev/null +++ b/src/calibre/gui2/images/donate.svg @@ -0,0 +1,302 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + Oxygen team + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/calibre/gui2/images/window-close.svg b/src/calibre/gui2/images/window-close.svg new file mode 100644 index 0000000000..754eb5fd30 --- /dev/null +++ b/src/calibre/gui2/images/window-close.svg @@ -0,0 +1,389 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + diff --git a/src/calibre/gui2/news.py b/src/calibre/gui2/news.py deleted file mode 100644 index 4a533975c1..0000000000 --- a/src/calibre/gui2/news.py +++ /dev/null @@ -1,96 +0,0 @@ -__license__ = 'GPL v3' -__copyright__ = '2008, Kovid Goyal ' -from PyQt4.QtCore import QObject, SIGNAL, QFile -from PyQt4.QtGui import QMenu, QIcon, QDialog, QAction - -from calibre.gui2.dialogs.password import PasswordDialog -from calibre.web.feeds.recipes import titles, get_builtin_recipe, compile_recipe - -class NewsAction(QAction): - - def __init__(self, recipe, parent): - self.recipe = recipe - self.module = recipe.__module__.rpartition('.')[-1] - if QFile(':/images/news/'+self.module+'.png').exists(): - ic = QIcon(':/images/news/'+self.module+'.png') - else: - ic = QIcon(':/images/news.svg') - QAction.__init__(self, ic, recipe.title, parent) - QObject.connect(self, SIGNAL('triggered(bool)'), self.fetch_news) - QObject.connect(self, SIGNAL('start_news_fetch(PyQt_PyObject, PyQt_PyObject)'), - parent.fetch_news) - - def fetch_news(self, checked): - self.emit(SIGNAL('start_news_fetch(PyQt_PyObject, PyQt_PyObject)'), - self.recipe, self.module) - - -class NewsMenu(QMenu): - - def __init__(self, customize_feeds_func): - QMenu.__init__(self) - self.scheduler = QAction(QIcon(':/images/scheduler.svg'), _('Schedule news download'), self) - self.addAction(self.scheduler) - self.cac = QAction(QIcon(':/images/user_profile.svg'), _('Add a custom news source'), self) - self.connect(self.cac, SIGNAL('triggered(bool)'), customize_feeds_func) - self.addAction(self.cac) - self.addSeparator() - self.custom_menu = CustomNewsMenu() - self.addMenu(self.custom_menu) - self.connect(self.custom_menu, SIGNAL('start_news_fetch(PyQt_PyObject, PyQt_PyObject)'), - self.fetch_news) - - self.dmenu = QMenu(self) - self.dmenu.setTitle(_('Download news')) - self.dmenu.setIcon(QIcon(':/images/news.svg')) - self.addMenu(self.dmenu) - - for title in titles: - recipe = get_builtin_recipe(title)[0] - self.dmenu.addAction(NewsAction(recipe, self)) - - - def fetch_news(self, recipe, module): - username = password = None - fetch = True - - if recipe.needs_subscription: - name = module if module else recipe.title - d = PasswordDialog(self, name + ' info dialog', - _('

Please enter your username and password for %s
If you do not have one, please subscribe to get access to the articles.
Click OK to proceed.')%(recipe.title,)) - d.exec_() - if d.result() == QDialog.Accepted: - username, password = d.username(), d.password() - else: - fetch = False - if fetch: - data = dict(title=recipe.title, username=username, password=password, - script=getattr(recipe, 'gui_recipe_script', None)) - self.emit(SIGNAL('fetch_news(PyQt_PyObject)'), data) - - def set_custom_feeds(self, feeds): - self.custom_menu.set_feeds(feeds) - -class CustomNewMenuItem(QAction): - - def __init__(self, title, script, parent): - QAction.__init__(self, QIcon(':/images/user_profile.svg'), title, parent) - self.title = title - self.recipe = compile_recipe(script) - self.recipe.gui_recipe_script = script - -class CustomNewsMenu(QMenu): - - def __init__(self): - QMenu.__init__(self) - self.setTitle(_('Download custom news')) - self.connect(self, SIGNAL('triggered(QAction*)'), self.launch) - - def launch(self, action): - self.emit(SIGNAL('start_news_fetch(PyQt_PyObject, PyQt_PyObject)'), - action.recipe, None) - - def set_feeds(self, feeds): - self.clear() - for title, src in feeds: - self.addAction(CustomNewMenuItem(title, src, self)) diff --git a/src/calibre/web/feeds/main.py b/src/calibre/web/feeds/main.py index 4ef7d89dd4..ebc2c937ed 100644 --- a/src/calibre/web/feeds/main.py +++ b/src/calibre/web/feeds/main.py @@ -8,8 +8,7 @@ CLI for downloading feeds. import sys, os, logging from calibre.web.feeds.recipes import get_builtin_recipe, compile_recipe, titles from calibre.web.fetch.simple import option_parser as _option_parser -from calibre.web.feeds.news import Profile2Recipe, BasicNewsRecipe -from calibre.ebooks.lrf.web.profiles import DefaultProfile, FullContentProfile +from calibre.web.feeds.news import BasicNewsRecipe from calibre.utils.config import Config, StringConfig def config(defaults=None): @@ -119,23 +118,19 @@ def run_recipe(opts, recipe_arg, parser, notification=None, handler=None): pb = ProgressBar(term, _('Fetching feeds...'), no_progress_bar=opts.no_progress_bar) notification = pb.update - recipe, is_profile = None, False + recipe = None if opts.feeds is not None: recipe = BasicNewsRecipe else: try: if os.access(recipe_arg, os.R_OK): - recipe = compile_recipe(open(recipe_arg).read()) - is_profile = DefaultProfile in recipe.__bases__ or \ - FullContentProfile in recipe.__bases__ + recipe = compile_recipe(open(recipe_arg).read()) else: raise Exception('not file') except: - recipe, is_profile = get_builtin_recipe(recipe_arg) + recipe = get_builtin_recipe(recipe_arg) if recipe is None: recipe = compile_recipe(recipe_arg) - is_profile = DefaultProfile in recipe.__bases__ or \ - FullContentProfile in recipe.__bases__ if recipe is None: raise RecipeError(recipe_arg+ ' is an invalid recipe') @@ -148,10 +143,7 @@ def run_recipe(opts, recipe_arg, parser, notification=None, handler=None): handler.setFormatter(ColoredFormatter('%(levelname)s: %(message)s\n')) # The trailing newline is need because of the progress bar logging.getLogger('feeds2disk').addHandler(handler) - if is_profile: - recipe = Profile2Recipe(recipe, opts, parser, notification) - else: - recipe = recipe(opts, parser, notification) + recipe = recipe(opts, parser, notification) if not os.path.exists(recipe.output_dir): os.makedirs(recipe.output_dir) diff --git a/src/calibre/web/feeds/news.py b/src/calibre/web/feeds/news.py index fe621f9bfa..24fe30fa6f 100644 --- a/src/calibre/web/feeds/news.py +++ b/src/calibre/web/feeds/news.py @@ -22,7 +22,6 @@ from calibre.web.feeds import feed_from_xml, templates, feeds_from_index, Feed from calibre.web.fetch.simple import option_parser as web2disk_option_parser from calibre.web.fetch.simple import RecursiveFetcher from calibre.utils.threadpool import WorkRequest, ThreadPool, NoResultsPending -from calibre.ebooks.lrf.web.profiles import FullContentProfile from calibre.ptempfile import PersistentTemporaryFile @@ -359,19 +358,19 @@ class BasicNewsRecipe(object, LoggingInterface): ''' if re.match(r'\w+://', url_or_raw): f = self.browser.open(url_or_raw) - raw = f.read() + _raw = f.read() f.close() - if not raw: + if not _raw: raise RuntimeError('Could not fetch index from %s'%url_or_raw) else: - raw = url_or_raw + _raw = url_or_raw if raw: - return raw - if not isinstance(raw, unicode) and self.encoding: - raw = raw.decode(self.encoding) + return _raw + if not isinstance(_raw, unicode) and self.encoding: + _raw = _raw.decode(self.encoding) massage = list(BeautifulSoup.MARKUP_MASSAGE) massage.append((re.compile(r'&(\S+?);'), lambda match: entity_to_unicode(match, encoding=self.encoding))) - return BeautifulSoup(raw, markupMassage=massage) + return BeautifulSoup(_raw, markupMassage=massage) def sort_index_by(self, index, weights): @@ -943,34 +942,6 @@ class BasicNewsRecipe(object, LoggingInterface): nmassage.extend(entity_replace) return BeautifulSoup(raw, markupMassage=nmassage) -class Profile2Recipe(BasicNewsRecipe): - ''' - Used to migrate the old news Profiles to the new Recipes. Uses the settings - from the old Profile to populate the settings in the Recipe. Also uses, the - Profile's get_browser and parse_feeds. - ''' - def __init__(self, profile_class, options, parser, progress_reporter): - self.old_profile = profile_class(logging.getLogger('feeds2disk'), - username=options.username, - password=options.password, - lrf=options.lrf) - for attr in ('preprocess_regexps', 'oldest_article', 'delay', 'timeout', - 'match_regexps', 'filter_regexps', 'html2lrf_options', - 'timefmt', 'needs_subscription', 'summary_length', - 'max_articles_per_feed', 'title','no_stylesheets', 'encoding'): - setattr(self, attr, getattr(self.old_profile, attr)) - - self.simultaneous_downloads = 1 - BasicNewsRecipe.__init__(self, options, parser, progress_reporter) - self.browser = self.old_profile.browser - self.use_embedded_content = isinstance(self.old_profile, FullContentProfile) - - def parse_index(self): - feeds = [] - for key, val in self.old_profile.parse_feeds().items(): - feeds.append((key, val)) - return feeds - class CustomIndexRecipe(BasicNewsRecipe): def custom_index(self): diff --git a/src/calibre/web/feeds/recipes/__init__.py b/src/calibre/web/feeds/recipes/__init__.py index 3337353597..736d2a2249 100644 --- a/src/calibre/web/feeds/recipes/__init__.py +++ b/src/calibre/web/feeds/recipes/__init__.py @@ -12,13 +12,13 @@ recipe_modules = [ 'discover_magazine', 'scientific_american', 'new_york_review_of_books', 'daily_telegraph', 'guardian', 'el_pais', 'new_scientist', 'b92', 'politika', 'moscow_times', 'latimes', 'japan_times', 'san_fran_chronicle', - 'demorgen_be', 'de_standaard' + 'demorgen_be', 'de_standaard', 'ap', 'barrons', 'chr_mon', 'cnn', 'faznet', + 'jpost', 'jutarnji', 'nasa', 'reuters', 'spiegelde', 'wash_post', 'zeitde', ] import re, imp, inspect, time, os from calibre.web.feeds.news import BasicNewsRecipe, CustomIndexRecipe, AutomaticNewsRecipe from calibre.ebooks.lrf.web.profiles import DefaultProfile, FullContentProfile -from calibre.ebooks.lrf.web import builtin_profiles from calibre.ebooks.BeautifulSoup import BeautifulSoup from calibre.path import path from calibre.ptempfile import PersistentTemporaryDirectory @@ -95,13 +95,10 @@ def get_builtin_recipe(title): ''' for r in recipes: if r.title == title: - return r, False - for p in builtin_profiles: - if p.title == title: - return p, True - return None, False + return r + return None -_titles = list(frozenset([r.title for r in recipes] + [p.title for p in builtin_profiles])) +_titles = [r.title for r in recipes] _titles.sort(cmp=english_sort) titles = _titles diff --git a/src/calibre/web/feeds/recipes/ap.py b/src/calibre/web/feeds/recipes/ap.py new file mode 100644 index 0000000000..cbd9055b61 --- /dev/null +++ b/src/calibre/web/feeds/recipes/ap.py @@ -0,0 +1,39 @@ +import re +from calibre.web.feeds.news import BasicNewsRecipe + + +class AssociatedPress(BasicNewsRecipe): + + title = u'Associated Press' + description = 'Global news' + __author__ = 'Kovid Goyal' + use_embedded_content = False + max_articles_per_feed = 15 + html2lrf_options = ['--force-page-break-before-tag="chapter"'] + + + preprocess_regexps = [ (re.compile(i[0], re.IGNORECASE | re.DOTALL), i[1]) for i in +[ + (r'.*?' , lambda match : ''), + (r'.*?', lambda match : ''), + (r'.*?', lambda match : ''), + (r'.*?', lambda match : ''), + (r'.*?', lambda match : ''), + (r'

.*?

', lambda match : '

'), + (r'

', lambda match : '

'), + (r'Learn more about our Privacy Policy.*?', lambda match : ''), + ] + ] + + + + feeds = [ ('AP Headlines', 'http://hosted.ap.org/lineups/TOPHEADS-rss_2.0.xml?SITE=ORAST&SECTION=HOME'), + ('AP US News', 'http://hosted.ap.org/lineups/USHEADS-rss_2.0.xml?SITE=CAVIC&SECTION=HOME'), + ('AP World News', 'http://hosted.ap.org/lineups/WORLDHEADS-rss_2.0.xml?SITE=SCAND&SECTION=HOME'), + ('AP Political News', 'http://hosted.ap.org/lineups/POLITICSHEADS-rss_2.0.xml?SITE=ORMED&SECTION=HOME'), + ('AP Washington State News', 'http://hosted.ap.org/lineups/WASHINGTONHEADS-rss_2.0.xml?SITE=NYPLA&SECTION=HOME'), + ('AP Technology News', 'http://hosted.ap.org/lineups/TECHHEADS-rss_2.0.xml?SITE=CTNHR&SECTION=HOME'), + ('AP Health News', 'http://hosted.ap.org/lineups/HEALTHHEADS-rss_2.0.xml?SITE=FLDAY&SECTION=HOME'), + ('AP Science News', 'http://hosted.ap.org/lineups/SCIENCEHEADS-rss_2.0.xml?SITE=OHCIN&SECTION=HOME'), + ('AP Strange News', 'http://hosted.ap.org/lineups/STRANGEHEADS-rss_2.0.xml?SITE=WCNC&SECTION=HOME'), + ] \ No newline at end of file diff --git a/src/calibre/web/feeds/recipes/barrons.py b/src/calibre/web/feeds/recipes/barrons.py new file mode 100644 index 0000000000..44ebdaef21 --- /dev/null +++ b/src/calibre/web/feeds/recipes/barrons.py @@ -0,0 +1,91 @@ +## +## web2lrf profile to download articles from Barrons.com +## can download subscriber-only content if username and +## password are supplied. +## +''' +''' + +import re + +from calibre.web.feeds.news import BasicNewsRecipe + +class Barrons(BasicNewsRecipe): + + title = 'Barron\'s' + max_articles_per_feed = 50 + needs_subscription = True + __author__ = 'Kovid Goyal' + description = 'Weekly publication for investors from the publisher of the Wall Street Journal' + timefmt = ' [%a, %b %d, %Y]' + use_embedded_content = False + no_stylesheets = False + match_regexps = ['http://online.barrons.com/.*?html\?mod=.*?|file:.*'] + html2lrf_options = [('--ignore-tables'),('--base-font-size=10')] + ##delay = 1 + + ## Don't grab articles more than 7 days old + oldest_article = 7 + + + preprocess_regexps = [(re.compile(i[0], re.IGNORECASE | re.DOTALL), i[1]) for i in + [ + ## Remove anything before the body of the article. + (r'', lambda match : ''), + (r'.*?', lambda match : ''), + (r'.*?', lambda match : ''), + (r'

', lambda match : ''), + (r'\'NWAnews.com', lambda match : ''), + (r'', lambda match : ''), + (r'

.*?', lambda match : ''), + + ] + ] + + feeds = [ ('Front Page', 'http://www.jpost.com/servlet/Satellite?pagename=JPost/Page/RSS&cid=1123495333346'), + ('Israel News', 'http://www.jpost.com/servlet/Satellite?pagename=JPost/Page/RSS&cid=1178443463156'), + ('Middle East News', 'http://www.jpost.com/servlet/Satellite?pagename=JPost/Page/RSS&cid=1123495333498'), + ('International News', 'http://www.jpost.com/servlet/Satellite?pagename=JPost/Page/RSS&cid=1178443463144'), + ('Editorials', 'http://www.jpost.com/servlet/Satellite?pagename=JPost/Page/RSS&cid=1123495333211'), + ] + + def print_version(self, url): + return ('http://www.jpost.com/servlet/Satellite?cid=' + url.rpartition('&')[2] + '&pagename=JPost%2FJPArticle%2FPrinter') + diff --git a/src/calibre/web/feeds/recipes/jutarnji.py b/src/calibre/web/feeds/recipes/jutarnji.py new file mode 100644 index 0000000000..062242429d --- /dev/null +++ b/src/calibre/web/feeds/recipes/jutarnji.py @@ -0,0 +1,45 @@ +''' + Profile to download Jutarnji.hr by Valloric +''' + +import re + +from calibre.web.feeds.news import BasicNewsRecipe + +class Jutarnji(BasicNewsRecipe): + + title = 'Jutarnji' + description = 'News from Croatia' + __author__ = 'Valloric' + use_embedded_content = False + timefmt = ' [%d %b %Y]' + max_articles_per_feed = 80 + html_description = True + no_stylesheets = True + + preprocess_regexps = [ + (re.compile(r'', re.IGNORECASE | re.DOTALL), lambda match : ''), + (re.compile(r'

.*?', re.IGNORECASE | re.DOTALL), lambda match : '
'), + (re.compile(r')|(
)|(
)|(

)|())', lambda match: '

'), + + ## Remove any links/ads/comments/cruft from the end of the body of the article. + (r'(()|(
)|(

©)|(