first commit

This commit is contained in:
josch 2012-02-24 14:52:08 +01:00
commit 3a1987cb35
4 changed files with 1180 additions and 0 deletions

1
README Normal file
View file

@ -0,0 +1 @@
Cookies are saved in cookies.txt

87
feeds.yaml.example Normal file
View file

@ -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

998
pyferea.py Normal file
View file

@ -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 = "<b>"+title+"</b>"
date = "<b>"+date+"</b>"
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 = "<b>"+label+" (%d)"%unread+"</b>"
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 = "<b>"+title+" (%d)"%unread+"</b>"
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, '<b>'+markup_escape_text(entry['title'])+" (%d)"%entry['unread']+'</b>')
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 = "<h1>%s</h1><p>%s</p>"%(item['title'], ', '.join(item['categories']))
else:
content_string = "<h1>%s</h1>"%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()

94
ythtml5.js Normal file
View file

@ -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<nodes.length; i++) {
transformNode(nodes[i]);
}
nodes = document.getElementsByTagName("embed");
for (i=0; i<nodes.length; i++) {
if (node.parentNode.nodeName.toLowerCase() == "object") {
continue;
}
transformNode(nodes[i]);
}
}
function transformNode(node) {
var embedChild = null;
if (node.nodeName.toLowerCase() == "object") {
// it can contains an <embed>
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 <embed src="xxx">
if (src == null) { // case <object data="xxx">
src = node.getAttribute('data');
}
if (src == null && embedChild != null) { // case <object><embed src="xx"></object>
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();