Move download management from frontends to rss_sqlite.py.
[feedingit] / src / FeedingIt.py
index 9ba9469..f7a45b3 100644 (file)
@@ -2,6 +2,7 @@
 
 # 
 # Copyright (c) 2007-2008 INdT.
+# Copyright (c) 2011 Neal H. Walfield
 # This program is free software: you can redistribute it and/or modify
 # it under the terms of the GNU Lesser General Public License as published by
 # the Free Software Foundation, either version 3 of the License, or
@@ -44,11 +45,13 @@ from feedingitdbus import ServerObject
 from updatedbus import UpdateServerObject, get_lock
 from config import Config
 from cgi import escape
+import weakref
 
 from rss_sqlite import Listing
 from opml import GetOpmlData, ExportOpmlData
 
-from urllib2 import install_opener, build_opener
+import mainthread
+from jobmanager import JobManager
 
 from socket import setdefaulttimeout
 timeout = 5
@@ -270,90 +273,95 @@ class AddCategoryWizard(gtk.Dialog):
     def getData(self):
         return self.nameEntry.get_text()
         
-class Download(Thread):
-    def __init__(self, listing, key, config):
-        Thread.__init__(self)
-        self.listing = listing
-        self.key = key
-        self.config = config
-        
-    def run (self):
-        (use_proxy, proxy) = self.config.getProxy()
-        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):
-        
-        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)
-            else:
-                opener = build_opener()
-
-            opener.addheaders = [('User-agent', USER_AGENT)]
-            install_opener(opener)
-
-            if self.total>0:
-                # In preparation for i18n/l10n
-                def N_(a, b, n):
-                    return (a if n == 1 else b)
-
-                self.set_text(N_('Updating %d feed', 'Updating %d feeds', self.total) % self.total)
-
-                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 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.)
-            #print x, k, fin, fraction
-            self.set_fraction(fraction)
-
-            if len(self.listOfKeys)>0:
-                self.current = self.current+1
-                key = self.listOfKeys.pop()
-                #if self.single == True:
-                    # Check if the feed is being displayed
-                download = Download(self.listing, key, self.config)
-                download.start()
-                return True
-            elif activeCount() > 1:
-                return True
+    @classmethod
+    def class_init(cls):
+        if hasattr (cls, 'class_init_done'):
+            return
+
+        jm = JobManager ()
+        jm.stats_hook_register (cls.update_progress,
+                                run_in_main_thread=True)
+
+        cls.downloadbars = []
+        # Total number of jobs we are monitoring.
+        cls.total = 0
+        # Number of jobs complete (of those that we are monitoring).
+        cls.done = 0
+        # Percent complete.
+        cls.progress = 0
+
+        cls.class_init_done = True
+
+    def __init__(self, parent):
+        self.class_init ()
+
+        gtk.ProgressBar.__init__(self)
+
+        self.downloadbars.append(weakref.ref (self))
+        self.set_fraction(0)
+        self.__class__.update_bars()
+        self.show_all()
+
+    @classmethod
+    def downloading(cls):
+        return hasattr (cls, 'jobs_at_start')
+
+    @classmethod
+    def update_progress(cls, jm, old_stats, new_stats, updated_feed):
+        if not cls.downloading():
+            cls.jobs_at_start = old_stats['jobs-completed']
+
+        if not cls.downloadbars:
+            return
+
+        if new_stats['jobs-in-progress'] + new_stats['jobs-queued'] == 0:
+            del cls.jobs_at_start
+            for ref in cls.downloadbars:
+                bar = ref ()
+                if bar is None:
+                    # The download bar disappeared.
+                    cls.downloadbars.remove (ref)
+                else:
+                    bar.emit("download-done", None)
+            return
+
+        # This should never be called if new_stats['jobs'] is 0, but
+        # just in case...
+        cls.total = max (1, new_stats['jobs'] - cls.jobs_at_start)
+        cls.done = new_stats['jobs-completed'] - cls.jobs_at_start
+        cls.progress = 1 - (new_stats['jobs-in-progress'] / 2.
+                            + new_stats['jobs-queued']) / cls.total
+        cls.update_bars()
+
+        if updated_feed:
+            for ref in cls.downloadbars:
+                bar = ref ()
+                if bar is None:
+                    # The download bar disappeared.
+                    cls.downloadbars.remove (ref)
+                else:
+                    bar.emit("download-done", updated_feed)
+
+    @classmethod
+    def update_bars(cls):
+        # In preparation for i18n/l10n
+        def N_(a, b, n):
+            return (a if n == 1 else b)
+
+        text = (N_('Updated %d of %d feeds ', 'Updated %d of %d feeds',
+                   cls.total)
+                % (cls.done, cls.total))
+
+        for ref in cls.downloadbars:
+            bar = ref ()
+            if bar is None:
+                # The download bar disappeared.
+                cls.downloadbars.remove (ref)
             else:
-                #self.waitingWindow.destroy()
-                #self.destroy()
-                try:
-                    del self.update_lock
-                except:
-                    pass
-                self.emit("download-done", "success")
-                return False 
-        return True
-    
-    
+                bar.set_text(text)
+                bar.set_fraction(cls.progress)
+
 class SortList(hildon.StackableWindow):
     def __init__(self, parent, listing, feedingit, after_closing, category=None):
         hildon.StackableWindow.__init__(self)
@@ -741,7 +749,14 @@ class DisplayFeed(hildon.StackableWindow):
         self.set_app_menu(menu)
         menu.show_all()
         
+        self.main_vbox = gtk.VBox(False, 0)
+        self.add(self.main_vbox)
+
+        self.pannableFeed = None
         self.displayFeed()
+
+        if DownloadBar.downloading ():
+            self.show_download_bar ()
         
         self.connect('configure-event', self.on_configure_event)
         self.connect("destroy", self.destroyWindow)
@@ -771,6 +786,9 @@ class DisplayFeed(hildon.StackableWindow):
         return escape(unescape(title).replace("<em>","").replace("</em>","").replace("<nobr>","").replace("</nobr>","").replace("<wbr>",""))
 
     def displayFeed(self):
+        if self.pannableFeed:
+            self.pannableFeed.destroy()
+
         self.pannableFeed = hildon.PannableArea()
 
         self.pannableFeed.set_property('hscrollbar-policy', gtk.POLICY_NEVER)
@@ -842,7 +860,7 @@ class DisplayFeed(hildon.StackableWindow):
             markup = ENTRY_TEMPLATE % (self.config.getFontSize(), "No Articles To Display")
             self.feedItems.append((markup, ""))
 
-        self.add(self.pannableFeed)
+        self.main_vbox.pack_start(self.pannableFeed)
         self.show_all()
 
     def clear(self):
@@ -935,22 +953,24 @@ class DisplayFeed(hildon.StackableWindow):
         self.displayFeed()
 
     def button_update_clicked(self, button):
-        #bar = DownloadBar(self, self.listing, [self.key,], self.config ) 
+        self.listing.updateFeed (self.key, priority=-1)
+            
+    def show_download_bar(self):
         if not type(self.downloadDialog).__name__=="DownloadBar":
-            self.pannableFeed.destroy()
-            self.vbox = gtk.VBox(False, 10)
-            self.downloadDialog = DownloadBar(self.window, self.listing, [self.key,], self.config, single=True )
-            self.downloadDialog.connect("download-done", self.onDownloadsDone)
-            self.vbox.pack_start(self.downloadDialog, expand=False, fill=False)
-            self.add(self.vbox)
+            self.downloadDialog = DownloadBar(self.window)
+            self.downloadDialog.connect("download-done", self.onDownloadDone)
+            self.main_vbox.pack_end(self.downloadDialog,
+                                    expand=False, fill=False)
             self.show_all()
-            
-    def onDownloadsDone(self, *widget):
-        self.vbox.destroy()
-        self.feed = self.listing.getFeed(self.key)
-        self.displayFeed()
-        self.updateDbusHandler.ArticleCountUpdated()
         
+    def onDownloadDone(self, widget, feed):
+        if feed == self.feed or feed is None:
+            self.downloadDialog.destroy()
+            self.downloadDialog = False
+            self.feed = self.listing.getFeed(self.key)
+            self.displayFeed()
+            self.updateDbusHandler.ArticleCountUpdated()
+
     def buttonReadAllClicked(self, button):
         #self.clear()
         self.feed.markAllAsRead()
@@ -1007,8 +1027,8 @@ class FeedingIt:
             self.stopButton.destroy()
         except:
             pass
-        self.listing = Listing(CONFIGDIR)
-        
+        self.listing = Listing(self.config, CONFIGDIR)
+
         self.downloadDialog = False
         try:
             self.orientation = FremantleRotation(__appname__, main_window=self.window, app=self)
@@ -1080,8 +1100,29 @@ class FeedingIt:
         self.checkAutoUpdate()
         
         hildon.hildon_gtk_window_set_progress_indicator(self.window, 0)
-        gobject.idle_add(self.enableDbus)
+        gobject.idle_add(self.late_init)
         
+    def job_manager_update(self, jm, old_stats, new_stats, updated_feed):
+        if (not self.downloadDialog
+            and new_stats['jobs-in-progress'] + new_stats['jobs-queued'] > 0):
+            self.updateDbusHandler.UpdateStarted()
+
+            self.downloadDialog = DownloadBar(self.window)
+            self.downloadDialog.connect("download-done", self.onDownloadDone)
+            self.mainVbox.pack_end(self.downloadDialog, expand=False, fill=False)
+            self.mainVbox.show_all()
+
+            if self.__dict__.get ('disp', None):
+                self.disp.show_download_bar ()
+
+    def onDownloadDone(self, widget, feed):
+        if feed is None:
+            self.downloadDialog.destroy()
+            self.downloadDialog = False
+            self.displayListing()
+            self.updateDbusHandler.UpdateFinished()
+            self.updateDbusHandler.ArticleCountUpdated()
+
     def stop_running_update(self, button):
         self.stopButton.set_sensitive(False)
         import dbus
@@ -1092,10 +1133,15 @@ class FeedingIt:
         iface = dbus.Interface(remote_object, 'org.marcoz.feedingit')
         iface.StopUpdate()
     
-    def enableDbus(self):
+    def late_init(self):
         self.dbusHandler = ServerObject(self)
         self.updateDbusHandler = UpdateServerObject(self)
 
+        jm = JobManager()
+        jm.stats_hook_register (self.job_manager_update,
+                                run_in_main_thread=True)
+        JobManager(True)
+
     def button_markAll(self, button):
         for key in self.listing.getListOfFeeds():
             feed = self.listing.getFeed(key)
@@ -1142,12 +1188,8 @@ class FeedingIt:
         SortList(self.window, self.listing, self, after_closing)
 
     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)
-            self.mainVbox.show_all()
+        for k in self.listing.getListOfFeeds():
+            self.listing.updateFeed (k)
         #self.displayListing()
 
     def onDownloadsDone(self, *widget):
@@ -1309,11 +1351,28 @@ class FeedingIt:
     def onFeedClosedTimeout(self):
         del self.feed_lock
         self.updateDbusHandler.ArticleCountUpdated()
-     
+
+    def quit(self, *args):
+        self.window.hide()
+
+        if hasattr (self, 'app_lock'):
+            del self.app_lock
+
+        # Wait until all slave threads have properly exited before
+        # terminating the mainloop.
+        jm = JobManager()
+        jm.quit ()
+        stats = jm.stats()
+        if stats['jobs-in-progress'] == 0 and stats['jobs-queued'] == 0:
+            gtk.main_quit ()
+        else:
+            gobject.timeout_add(500, self.quit)
+
+        return False
+
     def run(self):
-        self.window.connect("destroy", gtk.main_quit)
+        self.window.connect("destroy", self.quit)
         gtk.main()
-        del self.app_lock
 
     def prefsClosed(self, *widget):
         try:
@@ -1353,7 +1412,7 @@ class FeedingIt:
     def stopUpdate(self):
         # Not implemented in the app (see update_feeds.py)
         try:
-            self.downloadDialog.listOfKeys = []
+            JobManager().cancel ()
         except:
             pass
     
@@ -1367,6 +1426,8 @@ class FeedingIt:
         return status
 
 if __name__ == "__main__":
+    mainthread.init ()
+
     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,))