From 76d3b4878c32162d1b96fb4d8b0fafd6c89ea4bf Mon Sep 17 00:00:00 2001 From: Lonesome byte Date: Thu, 15 Aug 2019 08:37:05 +0200 Subject: [PATCH 1/3] Initial version of RSS news ticker --- README.md | 3 +- bumblebee/modules/rss.py | 160 ++++++++++++++++++++++++++++++++ themes/icons/awesome-fonts.json | 3 + themes/icons/ionicons.json | 3 + 4 files changed, 168 insertions(+), 1 deletion(-) create mode 100644 bumblebee/modules/rss.py diff --git a/README.md b/README.md index bcc7c3b..d2cc7f6 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ [![Test Coverage](https://codeclimate.com/github/tobi-wan-kenobi/bumblebee-status/badges/coverage.svg)](https://codeclimate.com/github/tobi-wan-kenobi/bumblebee-status/coverage) [![Issue Count](https://codeclimate.com/github/tobi-wan-kenobi/bumblebee-status/badges/issue_count.svg)](https://codeclimate.com/github/tobi-wan-kenobi/bumblebee-status) -**Many, many thanks to all contributors! As of now, 46 of the modules are from various contributors (!), and only 19 from myself.** +**Many, many thanks to all contributors! As of now, 47 of the modules are from various contributors (!), and only 19 from myself.** ![Solarized Powerline](https://github.com/tobi-wan-kenobi/bumblebee-status/blob/master/screenshots/themes/powerline-solarized.png) @@ -196,6 +196,7 @@ Modules and commandline utilities are only required for modules, the core itself * pytz (for the module 'datetimetz') * localtz (for the module 'datetimetz') * suntime (for the module 'sun') +* feedparser (for the module 'rss') # Required commandline utilities diff --git a/bumblebee/modules/rss.py b/bumblebee/modules/rss.py new file mode 100644 index 0000000..6ba7e84 --- /dev/null +++ b/bumblebee/modules/rss.py @@ -0,0 +1,160 @@ +# pylint: disable=C0111,R0903 + +"""RSS news ticker + +Fetches rss news items and shows these as a news ticker. +Left-clicking will open the full story in a browser. +New stories are highlighted. + +Parameters: + * rss.feeds : Space-separated list of RSS URLs + * rss.length : Maximum length of the module, default is 60 +""" + +try: + import feedparser + DEPENDENCIES_OK = True +except ImportError: + DEPENDENCIES_OK = False + +import webbrowser +import time +import os + +import bumblebee.input +import bumblebee.output +import bumblebee.engine + + +# pylint: disable=too-many-instance-attributes +class Module(bumblebee.engine.Module): + REFRESH_DELAY = 600 + SCROLL_SPEED = 3 + + def __init__(self, engine, config): + super(Module, self).__init__(engine, config, + bumblebee.output.Widget(full_text=self.ticker_update) + ) + # Use BBC newsfeed as demo: + self._feeds = self.parameter('feeds', 'http://feeds.bbci.co.uk/news/rss.xml').split(" ") + self._refresh_countdown = 0 + self._feeds_to_update = [] + + self._max_title_length = int(self.parameter("length", 60)) + self._scroll = self.parameter("scroll", True) + + self._items = [] + self._current_item = None + + self._ticker_offset = 0 + self._pre_delay = 0 + self._post_delay = 0 + + self._state = [] + engine.input.register_callback(self, button=bumblebee.input.LEFT_MOUSE, cmd=self._open) + + def _open(self, _): + if self._current_item: + webbrowser.open(self._current_item['link']) + + def _check_for_refresh(self): + if not DEPENDENCIES_OK: + self._items = [{'title': 'Please install feedparser first', 'new':True, 'published': 0, + 'link': 'https://pypi.org/project/feedparser/'}] + self._current_item = self._items[0] + return + if self._feeds_to_update: + # Update one feed at a time to not overload this update cycle + url = self._feeds_to_update.pop() + parser = feedparser.parse(url) + old_titles = [i['title'] for i in self._items if i['source'] == url] + new_items = [{'title': i['title'].replace('\n', ' '), + 'link': i['link'], + 'new': i['title'] not in old_titles, + 'source': url, + 'published': time.mktime(i.published_parsed) if hasattr(i, 'published_parsed') else 0} + for i in parser['entries']] + # Remove the previous items + self._items = [i for i in self._items if i['source'] != url] + # Add the new items + self._items.extend(new_items) + # Sort the items on publish date + self._items.sort(key=lambda i: i['published'], reverse=True) + + if not self._current_item: + self._next_item() + elif self._refresh_countdown == 0: + # Populate the list with feeds to update + self._feeds_to_update = self._feeds[:] + # Restart the update countdown timer + self._refresh_countdown = self.REFRESH_DELAY + else: + self._refresh_countdown -= 1 + + def _next_item(self): + self._ticker_offset = 0 + self._pre_delay = 2 + self._post_delay = 4 + + if not self._items: + return + + # Mark the previous item as 'old' + if self._current_item: + self._current_item['new'] = False + + # First show all new items + new_items = [i for i in self._items if i['new']] + if new_items: + self._current_item = new_items[0] + return + + # Take the next one in the list or wrap to the first + idx = self._items.index(self._current_item) if self._current_item in self._items else 0 + self._current_item = self._items[idx % len(self._items)] + + def _check_scroll_done(self): + # Check if the complete title has been shown or if scrolling is not enabled + if not self._scroll or self._ticker_offset + self._max_title_length > len(self._current_item['title']): + # Do not immediately show next item after scroll + if self._post_delay > 0: + self._post_delay -= 1 + else: + self._next_item() + else: + # Increase scroll position + self._ticker_offset += self.SCROLL_SPEED + + def ticker_update(self, _): + self._check_for_refresh() + + # If no items were retrieved, return an empty string + if not self._current_item: + return " "*self._max_title_length + + # Prepare a substring of the item title + response = self._current_item['title'][self._ticker_offset:self._ticker_offset+self._max_title_length] + # Add spaces if too short + response = response.ljust(self._max_title_length) + + # Do not immediately scroll + if self._pre_delay > 0: + # Change state during pre_delay for new items + if self._current_item['new']: + self._state = ['warning'] + self._pre_delay -= 1 + return response + + self._state = [] + + self._check_scroll_done() + + return response + + def update(self, widgets): + pass + + def state(self, _): + return self._state + +# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 diff --git a/themes/icons/awesome-fonts.json b/themes/icons/awesome-fonts.json index c6c9e3a..eb805d4 100644 --- a/themes/icons/awesome-fonts.json +++ b/themes/icons/awesome-fonts.json @@ -219,5 +219,8 @@ }, "sun": { "prefix": "" + }, + "rss": { + "prefix": "" } } diff --git a/themes/icons/ionicons.json b/themes/icons/ionicons.json index 6539986..08f9927 100644 --- a/themes/icons/ionicons.json +++ b/themes/icons/ionicons.json @@ -182,5 +182,8 @@ }, "sun": { "prefix": "\uf3b0" + }, + "rss": { + "prefix": "\uf1ea" } } From d1fc0f25758065d84264eb5cd1cc0a3a76f03340 Mon Sep 17 00:00:00 2001 From: Lonesome byte Date: Thu, 15 Aug 2019 11:58:10 +0200 Subject: [PATCH 2/3] Reworked code --- bumblebee/modules/rss.py | 67 +++++++++++++++++++--------------------- 1 file changed, 32 insertions(+), 35 deletions(-) diff --git a/bumblebee/modules/rss.py b/bumblebee/modules/rss.py index 6ba7e84..4c7627a 100644 --- a/bumblebee/modules/rss.py +++ b/bumblebee/modules/rss.py @@ -41,7 +41,6 @@ class Module(bumblebee.engine.Module): self._feeds_to_update = [] self._max_title_length = int(self.parameter("length", 60)) - self._scroll = self.parameter("scroll", True) self._items = [] self._current_item = None @@ -57,29 +56,33 @@ class Module(bumblebee.engine.Module): if self._current_item: webbrowser.open(self._current_item['link']) - def _check_for_refresh(self): - if not DEPENDENCIES_OK: - self._items = [{'title': 'Please install feedparser first', 'new':True, 'published': 0, - 'link': 'https://pypi.org/project/feedparser/'}] - self._current_item = self._items[0] - return - if self._feeds_to_update: - # Update one feed at a time to not overload this update cycle - url = self._feeds_to_update.pop() - parser = feedparser.parse(url) - old_titles = [i['title'] for i in self._items if i['source'] == url] - new_items = [{'title': i['title'].replace('\n', ' '), + def _update_items_from_feed(self, url): + parser = feedparser.parse(url) + old_titles = [i['title'] for i in self._items if i['source'] == url] + + new_items = [{'title': i['title'].replace('\n', ' '), 'link': i['link'], 'new': i['title'] not in old_titles, 'source': url, 'published': time.mktime(i.published_parsed) if hasattr(i, 'published_parsed') else 0} for i in parser['entries']] - # Remove the previous items - self._items = [i for i in self._items if i['source'] != url] - # Add the new items - self._items.extend(new_items) - # Sort the items on publish date - self._items.sort(key=lambda i: i['published'], reverse=True) + + # Remove the previous items + self._items = [i for i in self._items if i['source'] != url] + # Add the new items + self._items.extend(new_items) + # Sort the items on publish date + self._items.sort(key=lambda i: i['published'], reverse=True) + + def _check_for_refresh(self): + if not DEPENDENCIES_OK: + self._items = [{'title': 'Please install feedparser first', 'new':True, 'published': 0, + 'link': 'https://pypi.org/project/feedparser/'}] + self._current_item = self._items[0] + elif self._feeds_to_update: + # Update one feed at a time to not overload this update cycle + url = self._feeds_to_update.pop() + self._update_items_from_feed(url) if not self._current_item: self._next_item() @@ -99,27 +102,21 @@ class Module(bumblebee.engine.Module): if not self._items: return - # Mark the previous item as 'old' - if self._current_item: - self._current_item['new'] = False + # Index of the current element + idx = self._items.index(self._current_item) if self._current_item in self._items else - 1 - # First show all new items + # First show new items, else show next new_items = [i for i in self._items if i['new']] - if new_items: - self._current_item = new_items[0] - return - - # Take the next one in the list or wrap to the first - idx = self._items.index(self._current_item) if self._current_item in self._items else 0 - self._current_item = self._items[idx % len(self._items)] + self._current_item = next(iter(new_items), self._items[(idx+1) % len(self._items)]) def _check_scroll_done(self): - # Check if the complete title has been shown or if scrolling is not enabled - if not self._scroll or self._ticker_offset + self._max_title_length > len(self._current_item['title']): + # Check if the complete title has been shown + if self._ticker_offset + self._max_title_length > len(self._current_item['title']): # Do not immediately show next item after scroll - if self._post_delay > 0: - self._post_delay -= 1 - else: + self._post_delay -= 1 + if self._post_delay == 0: + self._current_item['new'] = False + # Mark the previous item as 'old' self._next_item() else: # Increase scroll position From ba7e7d258d1aace231c591136edeabd9e3d5cb1b Mon Sep 17 00:00:00 2001 From: Lonesome byte Date: Fri, 16 Aug 2019 10:11:22 +0200 Subject: [PATCH 3/3] Reworked code 2 --- bumblebee/modules/rss.py | 32 ++++++++++++++------------------ 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/bumblebee/modules/rss.py b/bumblebee/modules/rss.py index 4c7627a..6045ed2 100644 --- a/bumblebee/modules/rss.py +++ b/bumblebee/modules/rss.py @@ -4,7 +4,7 @@ Fetches rss news items and shows these as a news ticker. Left-clicking will open the full story in a browser. -New stories are highlighted. +New stories are highlighted. Parameters: * rss.feeds : Space-separated list of RSS URLs @@ -19,7 +19,6 @@ except ImportError: import webbrowser import time -import os import bumblebee.input import bumblebee.output @@ -33,7 +32,7 @@ class Module(bumblebee.engine.Module): def __init__(self, engine, config): super(Module, self).__init__(engine, config, - bumblebee.output.Widget(full_text=self.ticker_update) + bumblebee.output.Widget(full_text=self.ticker_update if DEPENDENCIES_OK else self._show_error) ) # Use BBC newsfeed as demo: self._feeds = self.parameter('feeds', 'http://feeds.bbci.co.uk/news/rss.xml').split(" ") @@ -56,17 +55,16 @@ class Module(bumblebee.engine.Module): if self._current_item: webbrowser.open(self._current_item['link']) + def _create_item(self, entry, url): + return {'title': entry['title'].replace('\n', ' '), + 'link': entry['link'], + 'new': all([i['title'] != entry['title'] for i in self._items]), + 'source': url, + 'published': time.mktime(entry.published_parsed) if hasattr(entry, 'published_parsed') else 0} + def _update_items_from_feed(self, url): parser = feedparser.parse(url) - old_titles = [i['title'] for i in self._items if i['source'] == url] - - new_items = [{'title': i['title'].replace('\n', ' '), - 'link': i['link'], - 'new': i['title'] not in old_titles, - 'source': url, - 'published': time.mktime(i.published_parsed) if hasattr(i, 'published_parsed') else 0} - for i in parser['entries']] - + new_items = [self._create_item(entry, url) for entry in parser['entries']] # Remove the previous items self._items = [i for i in self._items if i['source'] != url] # Add the new items @@ -75,11 +73,7 @@ class Module(bumblebee.engine.Module): self._items.sort(key=lambda i: i['published'], reverse=True) def _check_for_refresh(self): - if not DEPENDENCIES_OK: - self._items = [{'title': 'Please install feedparser first', 'new':True, 'published': 0, - 'link': 'https://pypi.org/project/feedparser/'}] - self._current_item = self._items[0] - elif self._feeds_to_update: + if self._feeds_to_update: # Update one feed at a time to not overload this update cycle url = self._feeds_to_update.pop() self._update_items_from_feed(url) @@ -122,6 +116,9 @@ class Module(bumblebee.engine.Module): # Increase scroll position self._ticker_offset += self.SCROLL_SPEED + def _show_error(self, _): + return "Please install feedparser first" + def ticker_update(self, _): self._check_for_refresh() @@ -143,7 +140,6 @@ class Module(bumblebee.engine.Module): return response self._state = [] - self._check_scroll_done() return response