commit 3a1987cb35896fad29c431a16424ee7548faf9ac Author: josch Date: Fri Feb 24 14:52:08 2012 +0100 first commit diff --git a/README b/README new file mode 100644 index 0000000..b2d38ee --- /dev/null +++ b/README @@ -0,0 +1 @@ +Cookies are saved in cookies.txt diff --git a/feeds.yaml.example b/feeds.yaml.example new file mode 100644 index 0000000..b18e548 --- /dev/null +++ b/feeds.yaml.example @@ -0,0 +1,87 @@ +http://blog.fefe.de/rss.xml?html: + category: "general news" + loadlink: False +http://www.lawblog.de/index.php/feed/: + category: "general news" + loadlink: False +http://feeds.feedburner.com/theatlantic/infocus: + category: "general news" + loadlink: False +http://feeds.boston.com/boston/bigpicture/index: + category: "general news" + loadlink: False +http://neusprech.org/feed/: + category: "general news" + loadlink: False +http://planet.debian.org/rss20.xml: + category: "IT news" + loadlink: False +http://www.debian-administration.org/atom.xml: + category: "IT news" + loadlink: False +http://www.engadget.com/exclude/Apple/rss.xml: + category: "IT news" + loadlink: False +http://www.heise.de/newsticker/heise-atom.xml: + category: "IT news" + loadlink: True +http://slashdot.org/slashdot.rss: + category: "IT news" + loadlink: True +https://netzpolitik.org/feed/: + category: "IT news" + loadlink: False +http://www.unknown-horizons.org/feed: + category: "IT news" + loadlink: True +http://www.raspberrypi.org/?feed=rss2: + category: "IT news" + loadlink: False +http://www.rockpapershotgun.com/feed/: + category: "IT news" + loadlink: True +http://freegamer.blogspot.com/feeds/posts/default?alt=rss: + category: "IT news" + loadlink: False +http://feed.dilbert.com/dilbert/daily_strip: + category: "comic" + loadlink: False +http://www.smbc-comics.com/rss.php: + category: "comic" + loadlink: False +http://feeds.feedburner.com/Explosm: + category: "comic" + loadlink: True +http://xkcd.com/rss.xml: + category: "comic" + loadlink: False +http://www.questionablecontent.net/QCRSS.xml: + category: "comic" + loadlink: False +http://www.rsspect.com/rss/gunner.xml: + category: "comic" + loadlink: True +http://www.giantitp.com/comics/oots.rss: + category: "comic" + loadlink: True +http://guildedage.net/feed/: + category: "comic" + loadlink: True +http://www.nichtlustig.de/rss/nichtrss.rss: + category: "comic" + loadlink: False +http://www.notquitewrong.com/rosscottinc/feed/: + category: "comic" + loadlink: False +http://www.darthsanddroids.net/rss.xml: + category: "comic" + loadlink: False +http://www.phdcomics.com/gradfeed_justcomics.php: + category: "comic" + loadlink: True +http://9gag.com/rss/site/feed.rss: + category: "comic" + loadlink: False +http://endlessorigami.com/?feed=rss2: + category: "comic" + loadlink: False diff --git a/pyferea.py b/pyferea.py new file mode 100644 index 0000000..6fe5f6d --- /dev/null +++ b/pyferea.py @@ -0,0 +1,998 @@ +#!/usr/bin/env python + +#TODO +# gettext +# custom css/javascript +# get addressbar/title/tabtitle right +# adjust date/time correctly + +# no double entries for smbc +# drag&drop doesnt create new items + +from gettext import gettext as _ +from gi.repository import Gtk, GLib, GObject, GdkPixbuf, Pango, WebKit, Soup +import yaml +from urlparse import urlparse, urlunparse, urljoin +import feedparser +from lxml import etree +from cStringIO import StringIO +import shelve +import time +from datetime import datetime + +def get_time_pretty(time): + """ + return a pretty string representation of time given in unix time + """ + time = datetime.fromtimestamp(time) + diff = datetime.now() - time + + if diff.days == 0: + return _("Today")+" "+time.strftime("%H:%M") + elif diff.days == 1: + return _("Yesterday")+" "+time.strftime("%H:%M") + elif diff.days < 7: + return time.strftime("%a %H:%M") + elif diff.days < 365: + return time.strftime("%b %d %H:%M") + else: + return time.strftime("%b %d %Y") + + +def pixbuf_new_from_file_in_memory(data, size=None): + """ + return a pixbuf of imagedata given by data + optionally resize image to width/height tuple given by size + """ + loader = GdkPixbuf.PixbufLoader() + if size: + loader.set_size(*size) + loader.write(data) + loader.close() + return loader.get_pixbuf() + +def find_shortcut_icon_link_in_html(data): + """ + data is a html document that will be parsed by lxml.etree.HTMLParser() + returns the href attribute of the first link tag containing a rel attribute + that lists icon as one of its types + """ + tree = etree.parse(StringIO(data), etree.HTMLParser()) + #for link in tree.xpath("//link[@rel='icon' or @rel='shortcut icon']/@href"): + # return link + links = tree.findall("//link") + for link in links: + rel = link.attrib.get('rel') + if not rel: + continue + if 'icon' not in rel.split(): + continue + href = link.attrib.get('href') + if not href: + continue + return href + +def markup_escape_text(text): + """ + use GLib.markup_escape_text to escape text for usage in pango markup + fields. + it will escape <, >, &, ' and " and some html entities + """ + if not text: + return "" + #passing -1 will let strlen() figure out the byte length of the null + #terminated string and avoids problems with multibyte characters + return GLib.markup_escape_text(text, -1) + +class TabLabel(Gtk.HBox): + """A class for Tab labels""" + + __gsignals__ = { + "close": (GObject.SIGNAL_RUN_FIRST, GObject.TYPE_NONE, (GObject.TYPE_OBJECT,)) + } + + def __init__ (self, title, child): + """initialize the tab label""" + Gtk.HBox.__init__(self) + self.set_homogeneous(False) + self.set_spacing(4) + self.title = title + self.child = child + self.label = Gtk.Label(title) + self.label.props.max_width_chars = 30 + self.label.set_ellipsize(Pango.EllipsizeMode.MIDDLE) + self.label.set_alignment(0.0, 0.5) + + icon = Gtk.Image.new_from_stock(Gtk.STOCK_ORIENTATION_PORTRAIT, Gtk.IconSize.BUTTON) + close_image = Gtk.Image.new_from_stock(Gtk.STOCK_CLOSE, Gtk.IconSize.MENU) + close_button = Gtk.Button() + close_button.set_relief(Gtk.ReliefStyle.NONE) + def _close_tab (widget, child): + self.emit("close", child) + close_button.connect("clicked", _close_tab, child) + close_button.set_image(close_image) + self.pack_start(icon, False, False, 0) + self.pack_start(self.label, True, True, 0) + self.pack_start(close_button, False, False, 0) + + self.set_data("label", self.label) + self.set_data("close-button", close_button) + + def tab_label_style_set_cb (tab_label, style): + context = tab_label.get_pango_context() + metrics = context.get_metrics(tab_label.get_style().font_desc, context.get_language()) + char_width = metrics.get_approximate_digit_width() + (_, width, height) = Gtk.icon_size_lookup(Gtk.IconSize.MENU) + tab_label.set_size_request(20 * char_width/1024.0 + 2 * width, (metrics.get_ascent() + metrics.get_descent())/1024.0) + self.connect("style-set", tab_label_style_set_cb) + + def set_label (self, text): + """sets the text of this label""" + self.label.set_label(text) + +class ContentPane (Gtk.Notebook): + + __gsignals__ = { + "focus-view-title-changed": (GObject.SIGNAL_RUN_FIRST, GObject.TYPE_NONE, (GObject.TYPE_OBJECT, GObject.TYPE_STRING,)) + } + + def __init__ (self): + """initialize the content pane""" + Gtk.Notebook.__init__(self) + self.props.scrollable = True + def _switch_page (notebook, page, page_num): + child = self.get_nth_page(page_num) + view = child.get_child() + frame = view.get_main_frame() + self.emit("focus-view-title-changed", frame, frame.props.title) + self.connect("switch-page", _switch_page) + + self.show_all() + self._hovered_uri = None + + def load_uri (self, text): + """load the given uri in the current web view""" + #child = self.get_nth_page(self.get_current_page()) + child = self.get_nth_page(0); + view = child.get_child() + view.load_uri(text) + + def load_string (self, text): + """load the given uri in the current web view""" + #child = self.get_nth_page(self.get_current_page()) + child = self.get_nth_page(0); + view = child.get_child() + view.load_string(text, "text/html", "utf-8", "") + + def back(self): + child = self.get_nth_page(self.get_current_page()) + view = child.get_child() + view.go_back() + + def forward(self): + child = self.get_nth_page(self.get_current_page()) + view = child.get_child() + view.go_forward() + + def refresh(self): + child = self.get_nth_page(self.get_current_page()) + view = child.get_child() + view.reload() + + def zoom_in(self): + child = self.get_nth_page(self.get_current_page()) + view = child.get_child() + view.zoom_in() + + def zoom_out(self): + child = self.get_nth_page(self.get_current_page()) + view = child.get_child() + view.zoom_out() + + def zoom_100(self): + child = self.get_nth_page(self.get_current_page()) + view = child.get_child() + if not (view.get_zoom_level() == 1.0): + view.set_zoom_level(1.0) + + def set_focus(self): + child = self.get_nth_page(self.get_current_page()) + view = child.get_child() + view.grab_focus() + + def print_page(self): + child = self.get_nth_page(self.get_current_page()) + view = child.get_child() + mainframe = view.get_main_frame() + mainframe.print_full(Gtk.PrintOperation(), Gtk.PrintOperationAction.PRINT_DIALOG); + + def new_tab (self, url=None): + """creates a new page in a new tab""" + # create the tab content + web_view = WebKit.WebView() + web_view.set_full_content_zoom(True) + + def _hovering_over_link_cb (view, title, uri): + self._hovered_uri = uri + web_view.connect("hovering-over-link", _hovering_over_link_cb) + + def _populate_page_popup_cb(view, menu): + if self._hovered_uri: + open_in_new_tab = Gtk.MenuItem() + open_in_new_tab.set_label(_("Open Link in New Tab")) + def _open_in_new_tab (menuitem, view): + self.new_tab(self._hovered_uri) + open_in_new_tab.connect("activate", _open_in_new_tab, view) + menu.insert(open_in_new_tab, 0) + menu.show_all() + web_view.connect("populate-popup", _populate_page_popup_cb) + + def _view_load_finished_cb(view, frame): + child = self.get_nth_page(self.get_current_page()) + label = self.get_tab_label(child) + title = frame.get_title() + if not title: + title = frame.get_uri() + if title: + label.set_label(title) + #view.execute_script(open("youtube_html5_everywhere.user.js", "r").read()) + view.execute_script(open("ythtml5.js", "r").read()) + """ + dom = view.get_dom_document() + head = dom.get_head() + if not head: + return + style = dom.create_element("style") + style.set_attribute("type", "text/css") + style.set_text_content("* {color:green;}") + head.append_child(style) + """ + web_view.connect("load-finished", _view_load_finished_cb) + + def _title_changed_cb (view, frame, title): + child = self.get_nth_page(self.get_current_page()) + label = self.get_tab_label(child) + label.set_label(title) + self.emit("focus-view-title-changed", frame, title) + web_view.connect("title-changed", _title_changed_cb) + + scrolled_window = Gtk.ScrolledWindow() + scrolled_window.props.hscrollbar_policy = Gtk.PolicyType.AUTOMATIC + scrolled_window.props.vscrollbar_policy = Gtk.PolicyType.AUTOMATIC + scrolled_window.add(web_view) + + # create the tab + label = TabLabel(url, scrolled_window) + def _close_tab (label, child): + page_num = self.page_num(child) + if page_num != -1: + view = child.get_child() + view.destroy() + self.remove_page(page_num) + self.set_show_tabs(self.get_n_pages() > 1) + label.connect("close", _close_tab) + label.show_all() + + new_tab_number = self.append_page(scrolled_window, label) + #self.set_tab_label_packing(scrolled_window, False, False, Gtk.PACK_START) + self.set_tab_label(scrolled_window, label) + + # hide the tab if there's only one + self.set_show_tabs(self.get_n_pages() > 1) + + self.show_all() + self.set_current_page(new_tab_number) + + # load the content + self._hovered_uri = None + if not url: + web_view.load_uri("about:blank") + else: + web_view.load_uri(url) + + +class WebToolbar(Gtk.Toolbar): + + __gsignals__ = { + "load-requested": (GObject.SIGNAL_RUN_FIRST, GObject.TYPE_NONE, (GObject.TYPE_STRING,)), + "back-requested": (GObject.SIGNAL_RUN_FIRST, GObject.TYPE_NONE, ()), + "forward-requested": (GObject.SIGNAL_RUN_FIRST, GObject.TYPE_NONE, ()), + "refresh-requested": (GObject.SIGNAL_RUN_FIRST, GObject.TYPE_NONE, ()), + "new-tab-requested": (GObject.SIGNAL_RUN_FIRST, GObject.TYPE_NONE, ()), + "zoom-in-requested": (GObject.SIGNAL_RUN_FIRST, GObject.TYPE_NONE, ()), + "zoom-out-requested": (GObject.SIGNAL_RUN_FIRST, GObject.TYPE_NONE, ()), + "zoom-100-requested": (GObject.SIGNAL_RUN_FIRST, GObject.TYPE_NONE, ()), + "print-requested": (GObject.SIGNAL_RUN_FIRST, GObject.TYPE_NONE, ()), + } + + def __init__(self): + Gtk.Toolbar.__init__(self) + + self.set_style(Gtk.ToolbarStyle.ICONS) + + backButton = Gtk.ToolButton() + backButton.set_stock_id(Gtk.STOCK_GO_BACK) + def back_cb(button): + self.emit("back-requested") + backButton.connect("clicked", back_cb) + self.insert(backButton, -1) + + forwardButton = Gtk.ToolButton() + forwardButton.set_stock_id(Gtk.STOCK_GO_FORWARD) + def forward_cb(button): + self.emit("forward-requested") + forwardButton.connect("clicked", forward_cb) + self.insert(forwardButton, -1) + + self._entry = Gtk.Entry() + def entry_activate_cb(entry): + self.emit("load-requested", entry.props.text) + self._entry.connect('activate', entry_activate_cb) + entry_item = Gtk.ToolItem() + entry_item.set_expand(True) + entry_item.add(self._entry) + self._entry.show() + self.insert(entry_item, -1) + + refreshButton = Gtk.ToolButton() + refreshButton.set_stock_id(Gtk.STOCK_REFRESH) + def refresh_cb(button): + self.emit("refresh-requested") + refreshButton.connect("clicked", refresh_cb) + self.insert(refreshButton, -1) + + zoom_in_button = Gtk.ToolButton() + zoom_in_button.set_stock_id(Gtk.STOCK_ZOOM_IN) + def zoom_in_cb(button): + self.emit("zoom-in-requested") + zoom_in_button.connect('clicked', zoom_in_cb) + self.insert(zoom_in_button, -1) + + zoom_out_button = Gtk.ToolButton() + zoom_out_button.set_stock_id(Gtk.STOCK_ZOOM_OUT) + def zoom_out_cb(button): + self.emit("zoom-out-requested") + zoom_out_button.connect('clicked', zoom_out_cb) + self.insert(zoom_out_button, -1) + + zoom_100_button = Gtk.ToolButton() + zoom_100_button.set_stock_id(Gtk.STOCK_ZOOM_100) + def zoom_hundred_cb(button): + self.emit("zoom-100-requested") + zoom_100_button.connect('clicked', zoom_hundred_cb) + self.insert(zoom_100_button, -1) + + print_button = Gtk.ToolButton() + print_button.set_stock_id(Gtk.STOCK_PRINT) + def print_cb(button): + self.emit("print-requested") + print_button.connect('clicked', print_cb) + self.insert(print_button, -1) + + addTabButton = Gtk.ToolButton() + addTabButton.set_stock_id(Gtk.STOCK_ADD) + def add_tab_cb(button): + self.emit("new-tab-requested") + addTabButton.connect("clicked", add_tab_cb) + self.insert(addTabButton, -1) + + def location_set_text (self, text): + self._entry.set_text(text) + + +class EntryTree(Gtk.TreeView): + __gsignals__ = { + "item-selected": (GObject.SIGNAL_RUN_FIRST, GObject.TYPE_NONE, (GObject.TYPE_STRING, GObject.TYPE_STRING)) + } + + def __init__(self, config, feeddb): + Gtk.TreeView.__init__(self) + + self.feeddb = feeddb + + def on_cursor_changed_cb(treeview): + _, it = self.get_selection().get_selected() + if not it: return + item = self.get_model().get_value(it, 0) + entry = self.feeddb[self.feedurl] + if entry['items'][item]['unread']: + title = markup_escape_text(entry['items'][item]['title']) + date = get_time_pretty(entry['items'][item]['date']) + self.get_model().set_value(it, 1, title) + self.get_model().set_value(it, 2, date) + entry['items'][item]['unread'] = False + entry['unread'] -= 1 + self.feeddb[self.feedurl] = entry + #self.feeddb.sync() # dont sync on every unread item - makes stuff extremely slow over time + self.emit("item-selected", self.feedurl, item) + self.connect("cursor-changed", on_cursor_changed_cb) + + self.models = dict() + # id, date, label + self.empty_model = Gtk.ListStore(GObject.TYPE_STRING, GObject.TYPE_INT, GObject.TYPE_STRING) + + cell = Gtk.CellRendererText() + column1 = Gtk.TreeViewColumn("Date", cell, markup=2) + column1.set_sort_column_id(0) + column2 = Gtk.TreeViewColumn("Headline", cell, markup=1) + column2.set_sort_column_id(1) + self.append_column(column1) + self.append_column(column2) + self.set_model(self.empty_model) + self.feedurl = None + + for feedurl in config: + if self.feeddb.get(feedurl): + self.update(feedurl) + + def display(self, feedurl): + if not feedurl or feedurl not in self.feeddb: + self.set_model(self.empty_model) + self.feedurl = None + else: + self.set_model(self.models[feedurl]) + self.feedurl = feedurl + + def update(self, feedurl): + model = Gtk.ListStore(GObject.TYPE_STRING, GObject.TYPE_STRING, GObject.TYPE_STRING) + # using model.set_sort_column_id is horribly slow, so append them + # sorted instead + items = sorted(self.feeddb[feedurl]['items'].iteritems(), key=lambda x: x[1]['date'], reverse=True) + for guid, value in items: + title = markup_escape_text(value.get('title', "")) + date = get_time_pretty(value['date']) + if value['unread']: + title = ""+title+"" + date = ""+date+"" + model.append([guid, title, date]) + def compare_date(model, a, b, data): + item1 = model.get_value(a, 0) + item2 = model.get_value(b, 0) + items = self.feeddb[feedurl]['items'] + return -1 if items[item1]['date'] < items[item2]['date'] else 1 + model.set_sort_func(0, compare_date) + def compare_title(model, a, b, data): + item1 = model.get_value(a, 0) + item2 = model.get_value(b, 0) + items = self.feeddb[feedurl]['items'] + return -1 if items[item1]['title'] < items[item2]['title'] else 1 + model.set_sort_func(1, compare_title) + # this takes loooooong + #model.set_sort_column_id(0, Gtk.SortType.DESCENDING) + self.models[feedurl] = model + if self.feedurl == feedurl: + self.set_model(model) + +class FeedTree(Gtk.TreeView): + __gsignals__ = { + "refresh-begin": (GObject.SIGNAL_RUN_FIRST, GObject.TYPE_NONE, ()), + "refresh-complete": (GObject.SIGNAL_RUN_FIRST, GObject.TYPE_NONE, ()), + "feed-selected": (GObject.SIGNAL_RUN_FIRST, GObject.TYPE_NONE, (GObject.TYPE_STRING,)), + "update-feed": (GObject.SIGNAL_RUN_FIRST, GObject.TYPE_NONE, (GObject.TYPE_STRING,)) + } + + def __init__(self, config, feeddb): + Gtk.TreeView.__init__(self) + + self.updating = set() + self.feeddb = feeddb + + def on_button_press_event(treeview, event): + if event.button != 3: return False + pthinfo = self.get_path_at_pos(event.x, event.y) + if pthinfo is None: return False + path, col, cellx, celly = pthinfo + #self.grab_focus() + #self.set_cursor(path, col, 0) + it = self.model.get_iter(path) + popup = self.model.get_value(it, 3) + popup.popup(None, None, None, None, event.button, event.time) + #return True + self.connect("button_press_event", on_button_press_event) + + def on_cursor_changed_cb(treeview): + _, it = self.get_selection().get_selected() + if it: + self.emit("feed-selected", self.model.get_value(it, 0)) + self.connect("cursor-changed", on_cursor_changed_cb) + + error_icon = self.render_icon(Gtk.STOCK_DIALOG_ERROR, Gtk.IconSize.MENU, None) + folder_icon = self.render_icon(Gtk.STOCK_DIRECTORY, Gtk.IconSize.MENU, None) + + # url, label, icon, popup + self.model = Gtk.TreeStore(GObject.TYPE_STRING, GObject.TYPE_STRING, GdkPixbuf.Pixbuf, Gtk.Menu) + + # reorganize configuration data into categories + categories = dict() + for feedurl, feedprops in config.items(): + if feedprops['category'] not in categories: + categories[feedprops['category']] = list() + categories[feedprops['category']].append(feedurl) + + for category, feeds in categories.items(): + it = self.model.append(None, [None, category, folder_icon, None]) + for feedurl in feeds: + if self.feeddb.get(feedurl): + feed_icon = self.feeddb[feedurl].get('favicon') + if feed_icon: + feed_icon = pixbuf_new_from_file_in_memory(feed_icon, (16, 16)) + else: + feed_icon = self.render_icon(Gtk.STOCK_FILE, Gtk.IconSize.MENU, None) + + label = markup_escape_text(self.feeddb[feedurl].get('title', feedurl)) + unread = self.feeddb[feedurl].get('unread') + if unread > 0: + label = ""+label+" (%d)"%unread+"" + else: + feed_icon = error_icon + label = feedurl + # append new item to category + # use resulting iter to update popup menu entry + itc = self.model.append(it, [feedurl, label, feed_icon, None]) + self.model.set_value(itc, 3, self.get_popup_menu(itc)) + + column = Gtk.TreeViewColumn("Feeds") + col_cell_img = Gtk.CellRendererPixbuf() + col_cell_text = Gtk.CellRendererText() + column.pack_start(col_cell_img, False) + column.pack_start(col_cell_text, True) + column.add_attribute(col_cell_text, "markup", 1) + column.add_attribute(col_cell_img, "pixbuf", 2) + + self.set_model(self.model) + self.append_column(column) + self.set_headers_visible(False) + self.expand_all() + self.show() + + self.session = Soup.SessionAsync.new() + + def mark_read_all(self): + it = self.model.get_iter_first() + while (it): + itc = self.model.iter_children(it) + while (itc): + self.mark_read(itc, sync=False) + itc = self.model.iter_next(itc) + it = self.model.iter_next(it) + self.feeddb.sync() + + def mark_read(self, it, sync=True): + feedurl = self.model.get_value(it, 0) + entry = self.feeddb.get(feedurl) + if not entry: return + entry['unread'] = 0 + for item in entry['items'].values(): + item['unread'] = False + self.model.set_value(it, 1, markup_escape_text(entry['title'])) + self.feeddb[feedurl] = entry + self.emit("update-feed", feedurl) + if sync: + self.feeddb.sync() + + def disable_context_update(self): + it = self.model.get_iter_first() + while (it): + itc = self.model.iter_children(it) + while (itc): + self.model.get_value(itc, 3).deactivate_update() + itc = self.model.iter_next(itc) + it = self.model.iter_next(it) + + def get_popup_menu(self, it): + popup = Gtk.Menu() + feedurl = self.model.get_value(it, 0) + + update_item = Gtk.ImageMenuItem.new_from_stock(Gtk.STOCK_REFRESH, None) + update_item.set_label(_("Update")) + def on_update_item_activate_cb(menuitem): + self.emit("refresh-begin") + self.disable_context_update() + self.updating.add(feedurl) + self.update_feed(it) + update_item.connect("activate", on_update_item_activate_cb) + + mark_item = Gtk.ImageMenuItem.new_from_stock(Gtk.STOCK_APPLY, None) + mark_item.set_label(_("Mark As Read")) + def on_mark_item_activate_cb(menuitem): + self.mark_read(it, sync=True) + self.emit("update-feed", feedurl) + mark_item.connect("activate", on_mark_item_activate_cb) + + popup.deactivate_update = lambda: update_item.set_sensitive(False) + popup.activate_update = lambda: update_item.set_sensitive(True) + popup.append(update_item) + popup.append(mark_item) + popup.show_all() + return popup + + def update_view_all(self): + it = self.model.get_iter_first() + while (it): + itc = self.model.iter_children(it) + while (itc): + feedurl = self.model.get_value(itc, 0) + title = markup_escape_text(self.feeddb[feedurl]['title']) + unread = self.feeddb[feedurl]['unread'] + if unread > 0: + title = ""+title+" (%d)"%unread+"" + self.model.set_value(itc, 1, title) + itc = self.model.iter_next(itc) + it = self.model.iter_next(it) + + def update_feed_all(self): + self.emit("refresh-begin") + + # disable updating via context menu + self.disable_context_update() + + it = self.model.get_iter_first() + while (it): + # add feedurls to self.updating so that each feed can remove itself + # from it once it is done and the last feed knows to take cleanup + # actions + itc = self.model.iter_children(it) + while (itc): + self.updating.add(self.model.get_value(itc, 0)) + itc = self.model.iter_next(itc) + + itc = self.model.iter_children(it) + while (itc): + self.update_feed(itc) + itc = self.model.iter_next(itc) + it = self.model.iter_next(it) + + def update_feed_done(self, feedurl): + self.updating.remove(feedurl) + if self.updating: return + self.feeddb.sync() + + # enable updating via context menu + it = self.model.get_iter_first() + while (it): + itc = self.model.iter_children(it) + while (itc): + self.model.get_value(itc, 3).activate_update() + itc = self.model.iter_next(itc) + it = self.model.iter_next(it) + # enable updating + self.emit("refresh-complete") + + def update_feed(self, it): + feedurl = self.model.get_value(it, 0) + msg = Soup.Message.new("GET", feedurl) + if self.feeddb.get(feedurl) and self.feeddb[feedurl].get('etag'): + msg.request_headers.append('If-None-Match', self.feeddb[feedurl]['etag']) + if self.feeddb.get(feedurl) and self.feeddb[feedurl].get('lastmodified'): + msg.request_headers.append('If-Modified-Since', self.feeddb[feedurl]['lastmodified']) + + def complete_cb(session, msg, it): + if msg.status_code not in [200, 304]: + error_icon = self.render_icon(Gtk.STOCK_DIALOG_ERROR, Gtk.IconSize.MENU, None) + self.model.set_value(it, 2, error_icon) + self.update_feed_done(feedurl) + return + + # get existing feedentry or create new one + entry = self.feeddb.get(feedurl, dict()) + + if entry.get('favicon'): + icon = pixbuf_new_from_file_in_memory(entry['favicon'], (16, 16)) + else: + icon = self.render_icon(Gtk.STOCK_FILE, Gtk.IconSize.MENU, None) + self.model.set_value(it, 2, icon) + + if msg.status_code == 304: + self.update_feed_done(feedurl) + return + + # filling default values + if not entry.has_key('unread'): + entry['unread'] = 0 + if not entry.has_key('items'): + entry['items'] = dict() + + # updating etag and lastmodified + if msg.response_headers.get_one('ETag'): + entry['etag'] = msg.response_headers.get_one('ETag') + if msg.response_headers.get_one('Last-Modified'): + entry['lastmodified'] = msg.response_headers.get_one('Last-Modified') + + feed = feedparser.parse(msg.response_body.flatten().get_data()) + + # TODO check if parsing succeeded + + entry['title'] = feed.feed.get('title') + self.model.set_value(it, 1, markup_escape_text(entry['title'])) + + # assumption: favicon never changes + if not entry.has_key('favicon'): + self.updating.add(feedurl+"_icon") + self.update_icon(it, feedurl) + + for item in feed.entries: + # use guid with fallback to link as identifier + itemid = item.get("id", item.get("link")) + if not itemid: + # TODO: display error "cannot identify feeditems" + break + + if entry['items'].has_key(itemid): + # already exists + continue + + new_item = { + 'link': item.get('link'), + 'title': item.get('title'), + 'date': item.get('published_parsed'), + 'content': item.get('content'), + 'categories': [cat for _, cat in item.get('categories', [])] or None, + 'unread': True + } + + if not new_item['date']: + new_item['date'] = item.get('updated_parsed') + + if new_item['date']: + new_item['date'] = int(time.mktime(new_item['date'])) + else: + new_item['date'] = int(time.time()) + + if new_item['content']: + new_item['content'] = new_item['content'][0] + else: + new_item['content'] = item.get('summary_detail') + + if new_item['content']: + new_item['content'] = new_item['content']['value'] + else: + new_item['content'] = "" + + entry['items'][itemid] = new_item + + entry['unread'] += 1 + + if entry['unread'] > 0: + self.model.set_value(it, 1, ''+markup_escape_text(entry['title'])+" (%d)"%entry['unread']+'') + + self.feeddb[feedurl] = entry + + self.emit("update-feed", feedurl) + + self.update_feed_done(feedurl) + self.session.queue_message(msg, complete_cb, it) + + def update_icon(self, it, feedurl): + msg = Soup.Message.new("GET", feedurl) + def complete_cb(session, msg, it): + if msg.status_code == 200: + icon_url = find_shortcut_icon_link_in_html(msg.response_body.flatten().get_data()) + if icon_url: + icon_url = urljoin(feedurl, icon_url) + self.update_icon_link(it, feedurl, icon_url) + else: + self.update_icon_favicon(it, feedurl) + else: + self.update_icon_favicon(it, feedurl) + self.session.queue_message(msg, complete_cb, it) + + # get shortcut icon from link rel + def update_icon_link(self, it, feedurl, url): + msg = Soup.Message.new("GET", url) + def complete_cb(session, msg, it): + if msg.status_code == 200: + data = msg.response_body.flatten().get_data() + if len(data): + icon = pixbuf_new_from_file_in_memory(data, (16, 16)) + entry = self.feeddb[feedurl] + entry['favicon'] = data + self.feeddb[feedurl] = entry + self.model.set_value(it, 2, icon) + self.update_feed_done(feedurl) + else: + self.update_icon_favicon(it, feedurl) + else: + self.update_icon_favicon(it, feedurl) + self.session.queue_message(msg, complete_cb, it) + + # get /favicon.ico + def update_icon_favicon(self, it, feedurl): + url = urlparse(feedurl) + url = urlunparse((url.scheme, url.netloc, 'favicon.ico', '', '', '')) + msg = Soup.Message.new("GET", url) + def complete_cb(session, msg, it): + data = None + if msg.status_code == 200: + data = msg.response_body.flatten().get_data() + if len(data): + icon = pixbuf_new_from_file_in_memory(data, (16, 16)) + else: + icon = self.render_icon(Gtk.STOCK_FILE, Gtk.IconSize.MENU, None) + else: + icon = self.render_icon(Gtk.STOCK_FILE, Gtk.IconSize.MENU, None) + entry = self.feeddb[feedurl] + entry['favicon'] = data + self.feeddb[feedurl] = entry + self.model.set_value(it, 2, icon) + self.update_feed_done(feedurl+"_icon") + self.session.queue_message(msg, complete_cb, it) + + +class FeedReaderWindow(Gtk.Window): + def __init__(self): + Gtk.Window.__init__(self) + + feeddb = shelve.open("pyferea.db") + + with open('feeds.yaml') as f: + config = yaml.load(f) + + toolbar = WebToolbar() + + def load_requested_cb(widget, text): + if not text: + return + content_pane.load_uri(text) + toolbar.connect("load-requested", load_requested_cb) + + def new_tab_requested_cb(toolbar): + content_pane.new_tab("about:blank") + toolbar.connect("new-tab-requested", new_tab_requested_cb) + + def back_requested_cb(toolbar): + content_pane.back() + toolbar.connect("back-requested", back_requested_cb) + + def forward_requested_cb(toolbar): + content_pane.forward() + toolbar.connect("forward-requested", forward_requested_cb) + + def refresh_requested_cb(toolbar): + content_pane.refresh() + toolbar.connect("refresh-requested", refresh_requested_cb) + + def zoom_in_requested_cb(toolbar): + content_pane.zoom_in() + toolbar.connect("zoom-in-requested", zoom_in_requested_cb) + + def zoom_out_requested_cb(toolbar): + content_pane.zoom_out() + toolbar.connect("zoom-out-requested", zoom_out_requested_cb) + + def zoom_100_requested_cb(toolbar): + content_pane.zoom_100() + toolbar.connect("zoom-100-requested", zoom_100_requested_cb) + + def print_requested_cb(toolbar): + content_pane.print_page() + toolbar.connect("print-requested", print_requested_cb) + + content_pane = ContentPane() + def title_changed_cb (tabbed_pane, frame, title): + if not title: + title = frame.get_uri() + self.set_title(_("PyFeRea - %s") % title) + uri = frame.get_uri() + if uri: + toolbar.location_set_text(uri) + content_pane.connect("focus-view-title-changed", title_changed_cb) + + entries = EntryTree(config, feeddb) + + def item_selected_cb(entry, feedurl, item): + item = feeddb[feedurl]['items'][item] + if config[feedurl]['loadlink']: + content_pane.load_uri(item['link']) + else: + if item.get('categories'): + content_string = "

%s

%s

"%(item['title'], ', '.join(item['categories'])) + else: + content_string = "

%s

"%item['title'] + content_pane.load_string(content_string+item['content']) + toolbar.location_set_text(item['link']) + self.set_title(_("PyFeRea - %s")%item['title']) + feedtree.update_view_all() + entries.connect("item-selected", item_selected_cb) + + feedtree = FeedTree(config, feeddb) + + def feed_selected_cb(feedtree, feedurl): + entries.display(feedurl) + feedtree.connect("feed-selected", feed_selected_cb) + + def update_feed_cb(feedtree, feedurl): + entries.update(feedurl) + feedtree.connect("update-feed", update_feed_cb) + + def refresh_begin_cb(feedtree): + button_refresh.set_label(_("Updating...")) + button_refresh.set_sensitive(False) + feedtree.connect("refresh-begin", refresh_begin_cb) + def refresh_complete_cb(feedtree): + button_refresh.set_label(_("Update All")) + button_refresh.set_sensitive(True) + feedtree.connect("refresh-complete", refresh_complete_cb) + + def timeout_cb(foo): + if feedtree.updating: return True + feedtree.update_feed_all() + return True + GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, 3600, timeout_cb, None) + + button_refresh = Gtk.Button() + button_refresh.set_image(Gtk.Image.new_from_stock(Gtk.STOCK_REFRESH, Gtk.IconSize.MENU)) + button_refresh.set_label(_("Update All")) + def refresh_cb(button): + feedtree.update_feed_all() + button_refresh.connect("clicked", refresh_cb) + + button_mark_all = Gtk.Button() + button_mark_all.set_image(Gtk.Image.new_from_stock(Gtk.STOCK_APPLY, Gtk.IconSize.MENU)) + button_mark_all.set_label(_("Mark All As Read")) + def mark_all_cb(button): + feedtree.mark_read_all() + button_mark_all.connect("clicked", mark_all_cb) + + hbox = Gtk.HBox() + hbox.pack_start(button_refresh, False, False, 0) + hbox.pack_start(button_mark_all, False, False, 0) + + scrolled_feedtree = Gtk.ScrolledWindow() + scrolled_feedtree.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) + scrolled_feedtree.add(feedtree) + + vbox2 = Gtk.VBox() + vbox2.pack_start(hbox, False, False, 0) + vbox2.pack_start(scrolled_feedtree, True, True, 0) + + scrolled_entries = Gtk.ScrolledWindow() + scrolled_entries.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) + scrolled_entries.add(entries) + + vbox = Gtk.VBox() + vbox.pack_start(toolbar, False, False, 0) + vbox.pack_start(content_pane, True, True, 0) + + vpane2 = Gtk.HPaned() + vpane2.add1(scrolled_entries) + vpane2.add2(vbox) + + vpane1 = Gtk.HPaned() + vpane1.add1(vbox2) + vpane1.add2(vpane2) + + self.add(vpane1) + self.set_default_size(800, 600) + + def destroy_cb(window): + feeddb.close() + self.destroy() + Gtk.main_quit() + self.connect('destroy', destroy_cb) + + def key_press_event(window, event): + if event.keyval == 49: # 1 + feedtree.grab_focus() + return True + elif event.keyval == 50: # 2 + entries.grab_focus() + return True + elif event.keyval == 51: # 3 + content_pane.set_focus() + return True + return False + self.connect('key-press-event', key_press_event) + + feedtree.update_feed_all() + + self.show_all() + + content_pane.new_tab() + +if __name__ == "__main__": + jar = Soup.CookieJarText.new("cookies.txt", False) + session = WebKit.get_default_session() + session.add_feature(jar) + session.set_property("timeout", 60) + feedreader = FeedReaderWindow() + Gtk.main() diff --git a/ythtml5.js b/ythtml5.js new file mode 100644 index 0000000..7a7e1c8 --- /dev/null +++ b/ythtml5.js @@ -0,0 +1,94 @@ +/* + * inspired by http://userscripts.org/scripts/show/116935 + * by http://userscripts.org/users/miguillo + */ + +function transform() { + nodes = document.getElementsByTagName("object"); + for (i=0; i + var children = node.childNodes; + for ( var j = 0; j < children.length; j++) { + var child = children[j]; + if (child.nodeName.toLowerCase() == "embed") { + embedChild = child; + break; + } + } + } + + var src = node.getAttribute('src'); // case + if (src == null) { // case + src = node.getAttribute('data'); + } + if (src == null && embedChild != null) { // case + src = embedChild.getAttribute('src'); + } + if (src == null) { + return; + } + src = src.replace(/^\s+/, '').replace(/\s+$/, ''); + + function isZero(s) { return s==null || s=="" || s=="0" || s=="0px"; } + + var width = node.getAttribute('width'); + var height = node.getAttribute('height'); + + if (isZero(width) && embedChild != null) width = embedChild.getAttribute('width'); + if (isZero(height) && embedChild != null) height = embedChild.getAttribute('height'); + + var nodeStyle = document.defaultView.getComputedStyle(node, ""); + if (isZero(width) && nodeStyle != null) width = nodeStyle.getPropertyValue('width'); + if (isZero(height) && nodeStyle != null) height = nodeStyle.getPropertyValue('height'); + + var childStyle = document.defaultView.getComputedStyle(embedChild, ""); + if (isZero(width) && childStyle != null) width = childStyle.getPropertyValue('width'); + if (isZero(height) && childStyle != null) height = childStyle.getPropertyValue('height'); + + if (isZero(width)) width = '100%'; + if (isZero(height)) height = '100%'; + + var youtubevRegex = /^(?:http:|https:)?\/\/www.youtube.com\/v\/([A-Za-z0-9_-]+)(?:\?(.*))?$/; + matches = src.match(youtubevRegex); + if (!matches) { + return; + } + + var querystring = ""; + if (matches[2]) { + querystring = "?"+matches[2]; + } + + var iframe = document.createElement("iframe"); + + iframe.setAttribute("class", "youtube-player"); + iframe.setAttribute('type', 'text/html'); + if (width != null) { + iframe.setAttribute('width', width); + } + if (height != null) { + iframe.setAttribute('height', height); + } + iframe.setAttribute('frameborder', 0); + + var src = "//www.youtube.com/embed/" + matches[1] + querystring; + iframe.setAttribute('src', src); + node.parentNode.replaceChild(iframe, node); +} + +transform();