0.6.1-7, fix for broken feeds
[feedingit] / src / FeedingIt.py
index ce0369c..b30d0e9 100644 (file)
 # ============================================================================
 # Name        : FeedingIt.py
 # Author      : Yves Marcoz
-# Version     : 0.5.4
+# Version     : 0.6.0
 # Description : Simple RSS Reader
 # ============================================================================
 
 import gtk
-import feedparser
-import pango
+from pango import FontDescription
 import hildon
 #import gtkhtml2
 #try:
-import webkit
+from webkit import WebView
 #    has_webkit=True
 #except:
 #    import gtkhtml2
 #    has_webkit=False
-import time
-import dbus
-import pickle
-from os.path import isfile, isdir
-from os import mkdir
-import sys   
-import urllib2
+from os.path import isfile, isdir, exists
+from os import mkdir, remove, stat
 import gobject
 from portrait import FremantleRotation
-import threading
-import thread
+from threading import Thread, activeCount
 from feedingitdbus import ServerObject
+from updatedbus import UpdateServerObject, get_lock
 from config import Config
 from cgi import escape
 
-from rss import *
+from rss import Listing
 from opml import GetOpmlData, ExportOpmlData
-   
-import socket
+
+from urllib2 import install_opener, build_opener
+
+from socket import setdefaulttimeout
 timeout = 5
-socket.setdefaulttimeout(timeout)
+setdefaulttimeout(timeout)
+del timeout
 
 color_style = gtk.rc_get_style_by_paths(gtk.settings_get_default() , 'GtkButton', 'osso-logical-colors', gtk.Button)
 unread_color = color_style.lookup_color('ActiveTextColor')
@@ -62,6 +59,38 @@ read_color = color_style.lookup_color('DefaultTextColor')
 del color_style
 
 CONFIGDIR="/home/user/.feedingit/"
+LOCK = CONFIGDIR + "update.lock"
+
+from re import sub
+from htmlentitydefs import name2codepoint
+
+##
+# Removes HTML or XML character references and entities from a text string.
+#
+# @param text The HTML (or XML) source text.
+# @return The plain text, as a Unicode string, if necessary.
+# http://effbot.org/zone/re-sub.htm#unescape-html
+def unescape(text):
+    def fixup(m):
+        text = m.group(0)
+        if text[:2] == "&#":
+            # character reference
+            try:
+                if text[:3] == "&#x":
+                    return unichr(int(text[3:-1], 16))
+                else:
+                    return unichr(int(text[2:-1]))
+            except ValueError:
+                pass
+        else:
+            # named entity
+            try:
+                text = unichr(name2codepoint[text[1:-1]])
+            except KeyError:
+                pass
+        return text # leave as is
+    return sub("&#?\w+;", fixup, text)
+
 
 class AddWidgetWizard(hildon.WizardDialog):
     
@@ -121,92 +150,59 @@ class AddWidgetWizard(hildon.WizardDialog):
             return False
         else:
             return True
-
-#class GetImage(threading.Thread):
-#    def __init__(self, url, stream):
-#        threading.Thread.__init__(self)
-#        self.url = url
-#        self.stream = stream
-#    
-#    def run(self):
-#        f = urllib2.urlopen(self.url)
-#        data = f.read()
-#        f.close()
-#        self.stream.write(data)
-#        self.stream.close()
-#
-#class ImageDownloader():
-#    def __init__(self):
-#        self.images = []
-#        self.downloading = False
-#        
-#    def queueImage(self, url, stream):
-#        self.images.append((url, stream))
-#        if not self.downloading:
-#            self.downloading = True
-#            gobject.timeout_add(50, self.checkQueue)
-#        
-#    def checkQueue(self):
-#        for i in range(4-threading.activeCount()):
-#            if len(self.images) > 0:
-#                (url, stream) = self.images.pop() 
-#                GetImage(url, stream).start()
-#        if len(self.images)>0:
-#            gobject.timeout_add(200, self.checkQueue)
-#        else:
-#            self.downloading=False
-#            
-#    def stopAll(self):
-#        self.images = []
-        
-        
-class Download(threading.Thread):
+        
+class Download(Thread):
     def __init__(self, listing, key, config):
-        threading.Thread.__init__(self)
+        Thread.__init__(self)
         self.listing = listing
         self.key = key
         self.config = config
         
     def run (self):
         (use_proxy, proxy) = self.config.getProxy()
-        if use_proxy:
-            self.listing.updateFeed(self.key, self.config.getExpiry(), proxy=proxy, imageCache=self.config.getImageCache() )
-        else:
-            self.listing.updateFeed(self.key, self.config.getExpiry(), imageCache=self.config.getImageCache() )
+        key_lock = get_lock(self.key)
+        if key_lock != None:
+            if use_proxy:
+                self.listing.updateFeed(self.key, self.config.getExpiry(), proxy=proxy, imageCache=self.config.getImageCache() )
+            else:
+                self.listing.updateFeed(self.key, self.config.getExpiry(), imageCache=self.config.getImageCache() )
+        del key_lock
 
         
 class DownloadBar(gtk.ProgressBar):
     def __init__(self, parent, listing, listOfKeys, config, single=False):
-        gtk.ProgressBar.__init__(self)
-        self.listOfKeys = listOfKeys[:]
-        self.listing = listing
-        self.total = len(self.listOfKeys)
-        self.config = config
-        self.current = 0
-        self.single = single
-        
-        if self.total>0:
-            #self.progress = gtk.ProgressBar()
-            #self.waitingWindow = hildon.Note("cancel", parent, "Downloading",
-            #                     progressbar=self.progress)
-            self.set_text("Updating...")
-            self.fraction = 0
-            self.set_fraction(self.fraction)
-            self.show_all()
-            # Create a timeout
-            self.timeout_handler_id = gobject.timeout_add(50, self.update_progress_bar)
-            #self.waitingWindow.show_all()
-            #response = self.waitingWindow.run()
-            #self.listOfKeys = []
-            #while threading.activeCount() > 1:
-                # Wait for current downloads to finish
-            #    time.sleep(0.1)
-            #self.waitingWindow.destroy()
+        
+        update_lock = get_lock("update_lock")
+        if update_lock != None:
+            gtk.ProgressBar.__init__(self)
+            self.listOfKeys = listOfKeys[:]
+            self.listing = listing
+            self.total = len(self.listOfKeys)
+            self.config = config
+            self.current = 0
+            self.single = single
+            (use_proxy, proxy) = self.config.getProxy()
+            if use_proxy:
+                opener = build_opener(proxy)
+                opener.addheaders = [('User-agent', 'Mozilla/5.0 (compatible; Maemo 5;) FeedingIt 0.6.1')]
+                install_opener(opener)
+            else:
+                opener = build_opener()
+                opener.addheaders = [('User-agent', 'Mozilla/5.0 (compatible; Maemo 5;) FeedingIt 0.6.1')]
+                install_opener(opener)
+
+            if self.total>0:
+                self.set_text("Updating...")
+                self.fraction = 0
+                self.set_fraction(self.fraction)
+                self.show_all()
+                # Create a timeout
+                self.timeout_handler_id = gobject.timeout_add(50, self.update_progress_bar)
 
     def update_progress_bar(self):
         #self.progress_bar.pulse()
-        if threading.activeCount() < 4:
-            x = threading.activeCount() - 1
+        if activeCount() < 4:
+            x = activeCount() - 1
             k = len(self.listOfKeys)
             fin = self.total - k - x
             fraction = float(fin)/float(self.total) + float(x)/(self.total*2.)
@@ -216,16 +212,20 @@ class DownloadBar(gtk.ProgressBar):
             if len(self.listOfKeys)>0:
                 self.current = self.current+1
                 key = self.listOfKeys.pop()
-                if (not self.listing.getCurrentlyDisplayedFeed() == key) or (self.single == True):
+                #if self.single == True:
                     # Check if the feed is being displayed
-                    download = Download(self.listing, key, self.config)
-                    download.start()
+                download = Download(self.listing, key, self.config)
+                download.start()
                 return True
-            elif threading.activeCount() > 1:
+            elif activeCount() > 1:
                 return True
             else:
                 #self.waitingWindow.destroy()
                 #self.destroy()
+                try:
+                    del self.update_lock
+                except:
+                    pass
                 self.emit("download-done", "success")
                 return False 
         return True
@@ -382,10 +382,11 @@ class DisplayArticle(hildon.StackableWindow):
         #self.set_title(feed.getTitle(id))
         self.set_title(self.listing.getFeedTitle(key))
         self.config = config
+        self.set_for_removal = False
         
         # Init the article display
         #if self.config.getWebkitSupport():
-        self.view = webkit.WebView()
+        self.view = WebView()
             #self.view.set_editable(False)
         #else:
         #    import gtkhtml2
@@ -432,9 +433,14 @@ class DisplayArticle(hildon.StackableWindow):
         button.connect("clicked", self._signal_link_clicked, self.feed.getExternalLink(self.id))
         menu.append(button)
         
-        button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
-        button.set_label("Add to Archived Articles")
-        button.connect("clicked", self.archive_button)
+        if key == "ArchivedArticles":
+            button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
+            button.set_label("Remove from Archived Articles")
+            button.connect("clicked", self.remove_archive_button)
+        else:
+            button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
+            button.set_label("Add to Archived Articles")
+            button.connect("clicked", self.archive_button)
         menu.append(button)
         
         self.set_app_menu(menu)
@@ -485,7 +491,10 @@ class DisplayArticle(hildon.StackableWindow):
 
     def destroyWindow(self, *args):
         self.disconnect(self.destroyId)
-        self.emit("article-closed", self.id)
+        if self.set_for_removal:
+            self.emit("article-deleted", self.id)
+        else:
+            self.emit("article-closed", self.id)
         #self.imageDownloader.stopAll()
         self.destroy()
         
@@ -497,6 +506,9 @@ class DisplayArticle(hildon.StackableWindow):
         # Call the listing.addArchivedArticle
         self.listing.addArchivedArticle(self.key, self.id)
         
+    def remove_archive_button(self, *widget):
+        self.set_for_removal = True
+        
     #def reloadArticle(self, *widget):
     #    if threading.activeCount() > 1:
             # Image thread are still running, come back in a bit
@@ -510,6 +522,7 @@ class DisplayArticle(hildon.StackableWindow):
     #    self.show_all()
 
     def _signal_link_clicked(self, object, link):
+        import dbus
         bus = dbus.SessionBus()
         proxy = bus.get_object("com.nokia.osso_browser", "/com/nokia/osso_browser/request")
         iface = dbus.Interface(proxy, 'com.nokia.osso_browser')
@@ -524,7 +537,7 @@ class DisplayArticle(hildon.StackableWindow):
 
 
 class DisplayFeed(hildon.StackableWindow):
-    def __init__(self, listing, feed, title, key, config):
+    def __init__(self, listing, feed, title, key, config, updateDbusHandler):
         hildon.StackableWindow.__init__(self)
         self.listing = listing
         self.feed = feed
@@ -532,10 +545,11 @@ class DisplayFeed(hildon.StackableWindow):
         self.set_title(title)
         self.key=key
         self.config = config
+        self.updateDbusHandler = updateDbusHandler
         
         self.downloadDialog = False
         
-        self.listing.setCurrentlyDisplayedFeed(self.key)
+        #self.listing.setCurrentlyDisplayedFeed(self.key)
         
         self.disp = False
         
@@ -549,6 +563,13 @@ class DisplayFeed(hildon.StackableWindow):
         button.set_label("Mark All As Read")
         button.connect("clicked", self.buttonReadAllClicked)
         menu.append(button)
+        
+        if key=="ArchivedArticles":
+            button = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
+            button.set_label("Purge Read Articles")
+            button.connect("clicked", self.buttonPurgeArticles)
+            menu.append(button)
+        
         self.set_app_menu(menu)
         menu.show_all()
         
@@ -557,12 +578,13 @@ class DisplayFeed(hildon.StackableWindow):
         self.connect("destroy", self.destroyWindow)
         
     def destroyWindow(self, *args):
-        self.feed.saveUnread(CONFIGDIR)
+        #self.feed.saveUnread(CONFIGDIR)
+        gobject.idle_add(self.feed.saveUnread, CONFIGDIR)
         self.listing.updateUnread(self.key, self.feed.getNumberOfUnreadItems())
         self.emit("feed-closed", self.key)
         self.destroy()
         #gobject.idle_add(self.feed.saveFeed, CONFIGDIR)
-        self.listing.closeCurrentlyDisplayedFeed()
+        #self.listing.closeCurrentlyDisplayedFeed()
 
     def displayFeed(self):
         self.vboxFeed = gtk.VBox(False, 10)
@@ -572,17 +594,20 @@ class DisplayFeed(hildon.StackableWindow):
         self.buttons = {}
         for id in self.feed.getIds():
             title = self.feed.getTitle(id)
-            esc_title = title.replace("<em>","").replace("</em>","").replace("&amp;","&").replace("&mdash;", "-").replace("&#8217;", "'")
+            
+            esc_title = unescape(title).replace("<em>","").replace("</em>","")
+            #title.replace("<em>","").replace("</em>","").replace("&amp;","&").replace("&mdash;", "-").replace("&#8217;", "'")
             button = gtk.Button(esc_title)
             button.set_alignment(0,0)
             label = button.child
+
             if self.feed.isEntryRead(id):
-                #label.modify_font(pango.FontDescription("sans 16"))
-                label.modify_font(pango.FontDescription(self.config.getReadFont()))
+                #label.modify_font(FontDescription("sans 16"))
+                label.modify_font(FontDescription(self.config.getReadFont()))
                 label.modify_fg(gtk.STATE_NORMAL, read_color) # gtk.gdk.color_parse("white"))
             else:
                 #print self.listing.getFont() + " bold"
-                label.modify_font(pango.FontDescription(self.config.getUnreadFont()))
+                label.modify_font(FontDescription(self.config.getUnreadFont()))
                 label.modify_fg(gtk.STATE_NORMAL, unread_color)
             label.set_line_wrap(True)
             
@@ -620,32 +645,48 @@ class DisplayFeed(hildon.StackableWindow):
             self.disp.show_all()
         
         self.ids = []
+        if self.key == "ArchivedArticles":
+            self.ids.append(self.disp.connect("article-deleted", self.onArticleDeleted))
         self.ids.append(self.disp.connect("article-closed", self.onArticleClosed))
         self.ids.append(self.disp.connect("article-next", self.nextArticle))
         self.ids.append(self.disp.connect("article-previous", self.previousArticle))
 
+    def buttonPurgeArticles(self, *widget):
+        self.clear()
+        self.feed.purgeReadArticles()
+        self.feed.saveUnread(CONFIGDIR)
+        self.feed.saveFeed(CONFIGDIR)
+        self.displayFeed()
+
     def destroyArticle(self, handle):
         handle.destroyWindow()
 
     def nextArticle(self, object, index):
         label = self.buttons[index].child
-        label.modify_font(pango.FontDescription(self.config.getReadFont()))
+        label.modify_font(FontDescription(self.config.getReadFont()))
         label.modify_fg(gtk.STATE_NORMAL, read_color) #  gtk.gdk.color_parse("white"))
         id = self.feed.getNextId(index)
         self.button_clicked(object, id, next=True)
 
     def previousArticle(self, object, index):
         label = self.buttons[index].child
-        label.modify_font(pango.FontDescription(self.config.getReadFont()))
+        label.modify_font(FontDescription(self.config.getReadFont()))
         label.modify_fg(gtk.STATE_NORMAL, read_color) # gtk.gdk.color_parse("white"))
         id = self.feed.getPreviousId(index)
         self.button_clicked(object, id, previous=True)
 
     def onArticleClosed(self, object, index):
         label = self.buttons[index].child
-        label.modify_font(pango.FontDescription(self.config.getReadFont()))
+        label.modify_font(FontDescription(self.config.getReadFont()))
         label.modify_fg(gtk.STATE_NORMAL, read_color) # gtk.gdk.color_parse("white"))
         self.buttons[index].show()
+        
+    def onArticleDeleted(self, object, index):
+        self.clear()
+        self.feed.removeArticle(index)
+        self.feed.saveUnread(CONFIGDIR)
+        self.feed.saveFeed(CONFIGDIR)
+        self.displayFeed()
 
     def button_update_clicked(self, button):
         #bar = DownloadBar(self, self.listing, [self.key,], self.config ) 
@@ -662,12 +703,13 @@ class DisplayFeed(hildon.StackableWindow):
         self.vbox.destroy()
         self.feed = self.listing.getFeed(self.key)
         self.displayFeed()
+        self.updateDbusHandler.ArticleCountUpdated()
         
     def buttonReadAllClicked(self, button):
         for index in self.feed.getIds():
             self.feed.setEntryRead(index)
             label = self.buttons[index].child
-            label.modify_font(pango.FontDescription(self.config.getReadFont()))
+            label.modify_font(FontDescription(self.config.getReadFont()))
             label.modify_fg(gtk.STATE_NORMAL, read_color) # gtk.gdk.color_parse("white"))
             self.buttons[index].show()
 
@@ -687,6 +729,11 @@ class FeedingIt:
         gobject.idle_add(self.createWindow)
         
     def createWindow(self):
+        self.app_lock = get_lock("app_lock")
+        if self.app_lock == None:
+            self.pannableListing.set_label("Update in progress, please wait.")
+            gobject.timeout_add_seconds(3, self.createWindow)
+            return False
         self.listing = Listing(CONFIGDIR)
         
         self.downloadDialog = False
@@ -739,6 +786,7 @@ class FeedingIt:
         
     def enableDbus(self):
         self.dbusHandler = ServerObject(self)
+        self.updateDbusHandler = UpdateServerObject(self)
 
     def button_markAll(self, button):
         for key in self.listing.getListOfFeeds():
@@ -778,6 +826,7 @@ class FeedingIt:
         
     def button_update_clicked(self, button, key):
         if not type(self.downloadDialog).__name__=="DownloadBar":
+            self.updateDbusHandler.UpdateStarted()
             self.downloadDialog = DownloadBar(self.window, self.listing, self.listing.getListOfFeeds(), self.config )
             self.downloadDialog.connect("download-done", self.onDownloadsDone)
             self.mainVbox.pack_end(self.downloadDialog, expand=False, fill=False)
@@ -789,7 +838,8 @@ class FeedingIt:
         self.downloadDialog = False
         #self.displayListing()
         self.refreshList()
-        self.dbusHandler.ArticleCountUpdated()
+        self.updateDbusHandler.UpdateFinished()
+        self.updateDbusHandler.ArticleCountUpdated()
 
     def button_preferences_clicked(self, button):
         dialog = self.config.createDialog()
@@ -851,18 +901,31 @@ class FeedingIt:
                 break
 
     def buttonFeedClicked(widget, button, self, window, key):
-        self.disp = DisplayFeed(self.listing, self.listing.getFeed(key), self.listing.getFeedTitle(key), key, self.config)
-        self.disp.connect("feed-closed", self.onFeedClosed)
+        try:
+            self.feed_lock
+        except:
+            # If feed_lock doesn't exist, we can open the feed, else we do nothing
+            self.feed_lock = get_lock(key)
+            self.disp = DisplayFeed(self.listing, self.listing.getFeed(key), self.listing.getFeedTitle(key), key, self.config, self.updateDbusHandler)
+            self.disp.connect("feed-closed", self.onFeedClosed)
 
     def onFeedClosed(self, object, key):
-        self.listing.saveConfig()
+        #self.listing.saveConfig()
+        #del self.feed_lock
+        gobject.idle_add(self.onFeedClosedTimeout)
         self.refreshList()
-        self.dbusHandler.ArticleCountUpdated()
+        #self.updateDbusHandler.ArticleCountUpdated()
+        
+    def onFeedClosedTimeout(self):
+        self.listing.saveConfig()
+        del self.feed_lock
+        self.updateDbusHandler.ArticleCountUpdated()
      
     def run(self):
         self.window.connect("destroy", gtk.main_quit)
         gtk.main()
         self.listing.saveConfig()
+        del self.app_lock
 
     def prefsClosed(self, *widget):
         self.orientation.set_mode(self.config.getOrientation())
@@ -888,9 +951,20 @@ class FeedingIt:
         # Need to check for internet connection
         # If no internet connection, try again in 10 minutes:
         # gobject.timeout_add(int(5*3600000), self.automaticUpdate)
+        file = open("/home/user/.feedingit/feedingit_widget.log", "a")
+        from time import localtime, strftime
+        file.write("App: %s\n" % strftime("%a, %d %b %Y %H:%M:%S +0000", localtime()))
+        file.close()
         self.button_update_clicked(None, None)
         return True
     
+    def stopUpdate(self):
+        # Not implemented in the app (see update_feeds.py)
+        try:
+            self.downloadDialog.listOfKeys = []
+        except:
+            pass
+    
     def getStatus(self):
         status = ""
         for key in self.listing.getListOfFeeds():
@@ -903,6 +977,7 @@ class FeedingIt:
 if __name__ == "__main__":
     gobject.signal_new("feed-closed", DisplayFeed, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
     gobject.signal_new("article-closed", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
+    gobject.signal_new("article-deleted", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
     gobject.signal_new("article-next", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
     gobject.signal_new("article-previous", DisplayArticle, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
     gobject.signal_new("download-done", DownloadBar, gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (gobject.TYPE_PYOBJECT,))
@@ -912,6 +987,7 @@ if __name__ == "__main__":
             mkdir(CONFIGDIR)
         except:
             print "Error: Can't create configuration directory"
-            sys.exit(1)
+            from sys import exit
+            exit(1)
     app = FeedingIt()
     app.run()