Commits

Damián Nohales committed 334cea3

Improve app startup with InitThread, improves downloading pause/resume.

- Previously, the app startup was buggy and slow, now, the main window is showed faster as possible while InitThread is executing to initialize Grooveshark API. The download queue file is loaded after InitThread finish its execution.
- Grooveshark does not allow partial downloads, but Song support this anyway and detects when the partial download fails to truncate the downloaded file to 0 bytes. To avoid user confusions, the app should use the verb "Stop" instead of "Pause" at frontend, since the partial download expect to fail (for now).
- Somes methods are added to AbstractSongList to improve list handle (first, next, range)

Comments (0)

Files changed (6)

 from lib.enviroment import env
 from lib.SingleService import SingleService
 from lib.SharkDown import SharkDown
-from lib.tfuncs import InitThread
 
 # Enviroment Initialization
 env().HAVE_NOTIFY = HAVE_NOTIFY
         app = SharkDown()
         env().set_app(app)
         service = SingleService(app)
-        init_thread = InitThread(app)
-        init_thread.start()
         gtk.main()

lib/AbstractSongList.py

 import gtk
+import gobject
 
-class AbstractSongList:
+class AbstractSongList(gobject.GObject):
     
     def __init__(self, view):
         self._view = view
     def get_model(self):
         return self.get_view().get_model()
     
+    def append_song(self, song):
+        self.get_model().append([song])
+    
     def find_song(self, song):
         iter = self.get_model().get_iter_first()
         while iter != None:
     def get_song(self, path_or_iter):
         return self.get_model()[path_or_iter][0]
         
-    def append_song(self, song):
-        self.get_model().append([song])
+    
+    # Shortcuts to handle the list
     
     def clear(self):
         self.get_model().clear()
+        
+    def __len__(self):
+        return len(self.get_model())
+    
+    def first(self):
+        return self.get_model().get_iter_first()
+    
+    def next(self, iter):
+        return self.get_model().iter_next(iter)
+    
+    def range(self):
+        return range(len(self.get_model()))

lib/DownloadList.py

+from enviroment import config
 from AbstractSongList import AbstractSongList
 import gtk
 import os
         AbstractSongList.__init__(self, view)
     
     def create_model(self):
-        # Song, File Name, SongID, Progress, Size
-        return gtk.ListStore(object, str, str, int, str)
+        # Song, File Name, SongID, Progress, Size, Icon info stock
+        return gtk.ListStore(object, str, str, int, str, str)
     
-    def append_song(self, song):
+    def __append_song_to_model(self, song):
         self.get_model().append([
             song,
             unicode(os.path.basename(song.get_filename()), errors = 'replace'),
             song.get_id(),
             0,
-            ""
+            "",
+            "gtk-network"
         ])
         
+        song.connect("download-initializing", self.on_download_initializing)
         song.connect("download-started", self.on_download_started)
         song.connect("download-updated", self.on_download_updated)
         song.connect("download-completed", self.on_download_completed)
+        song.connect("download-paused", self.on_download_paused)
+        song.connect("download-canceled", self.on_download_canceled)
         song.connect("download-error", self.on_download_error)
+    
+    def append_song(self, song):
+        """
+        Append song for download, if file exists, tries to continue the download
+        """
+        self.__append_song_to_model(song);
+        song.download_file(False)
         
-        song.download_file()
+    def append_song_restarting(self, song):
+        """
+        Append song for download, forcing to redownload the file
+        """
+        self.__append_song_to_model(song);
+        song.download_file(True)
         
     def create_view(self):
-        rendererText = gtk.CellRendererText()
-        column = gtk.TreeViewColumn(_("File name"), rendererText, text = 1)
+        column = gtk.TreeViewColumn("", gtk.CellRendererPixbuf(), stock_id = 5)
+        column.set_resizable(False)
+        self.get_view().append_column(column)
+        
+        column = gtk.TreeViewColumn(_("File name"), gtk.CellRendererText(), text = 1)
         column.set_resizable(True)
         self.get_view().append_column(column)
 
-        rendererText = gtk.CellRendererText()
-        column = gtk.TreeViewColumn(_("SongID"), rendererText, text = 2)
+        column = gtk.TreeViewColumn(_("SongID"), gtk.CellRendererText(), text = 2)
         column.set_resizable(True)
         self.get_view().append_column(column)
 
-        rendererProgress = gtk.CellRendererProgress()
-        column = gtk.TreeViewColumn(_("Download progress"), rendererProgress, value = 3)
+        column = gtk.TreeViewColumn(_("Download progress"), gtk.CellRendererProgress(), value = 3)
         column.set_resizable(True)
         self.get_view().append_column(column)
 
-        rendererText = gtk.CellRendererText()
-        column = gtk.TreeViewColumn(_("Size"), rendererText, text = 4)
+        column = gtk.TreeViewColumn(_("Size"), gtk.CellRendererText(), text = 4)
         column.set_resizable(True)
         self.get_view().append_column(column)
-        
+    
+    def on_download_initializing(self, song):
+        self.get_song_row(song)[5] = "gtk-network"
+    
     def on_download_started(self, song):
-        print "Started"
+        print "[Download started]", song.get_id()
     
     def on_download_updated(self, song):
-        self.get_song_row(song)[3] = song.get_download_progress()
-        self.get_song_row(song)[4] = "%.02f MB" % (song.get_file_size() / (1024 ** 2))
+        row = self.get_song_row(song)
+        
+        row[3] = song.get_download_progress()
+        row[4] = "%.02f MB" % (song.get_file_size() / (1024 ** 2))
+        row[5] = "gtk-go-down"
     
     def on_download_completed(self, song):
+        print "[Download completed]", song.get_id()
+        row = self.get_song_row(song)
+        
+        row[3] = 100
+        row[5] = "gtk-ok"
+        
+    def on_download_paused(self, song):
+        print "[Download paused]", song.get_id()
+        row = self.get_song_row(song)
+        
+        row[3] = 0
+        row[5] = "gtk-media-stop"
+        
+    def on_download_canceled(self, song):
+        print "[Download canceled]", song.get_id()
         self.get_model().remove(self.get_song_iter(song))
     
     def on_download_error(self, song, msg):
-        print "ERROR: ", msg
+        print "[Download error]", song.get_id(), ":", msg
+        self.get_song_row(song)[5] = "gtk-dialog-error"
+        
+    def stop_all_downloads(self):
+        """
+        Stop all downloads in a sync way for quit the main app
+        """
+        for i in self.range():
+            self.get_song(i).pause_download_sync()
+        
 import os
 import sys
 import random
+from tfuncs import InitThread
 from tfuncs import PlayThread
 from tfuncs import SearchThread
 from tfuncs import KeyListenerThread
     """
     The main application class for gSharkDown
     """
-    working = None
-    playing = None
-    # just to prevend scrobbeling more then once for 
-    # track.
-    scrobbled = 0
-    # Download dictionary in form Filename : Thread
-    downqueue = []
-
-    last_iter = None
 
     def __init__(self):
         """
         """
         gobject.threads_init()
         
+        self.working = None
+        self.playing = None
+        # just to prevend scrobbeling more then once for 
+        # track.
+        self.scrobbled = 0
+        # Download dictionary in form Filename : Thread
+        self.last_iter = None
+        
+        init_thread = InitThread(self)
+        init_thread.start()
+        
+        # Main window initialization
         builder = gtk.Builder()
         builder.set_translation_domain(env().APP)
         builder.add_from_file('%s/data/main_window.ui' % env().BASEPATH)
         self.downloads_expander = builder.get_object('expander_download')
         self.update_downloads_count()
         self.downmenu = builder.get_object('downloadmenu')
-
-        self.staticon = guihelpers.GsharkIndicator(self)
-
+        
+        # Song lists initialization
         if os.path.exists("%s/.gsharkdown/playlist.pkl" % os.environ.get("HOME")):
             self.load_saved_playlist("%s/.gsharkdown/playlist.pkl" % os.environ.get("HOME"))
         else:
             print "Playlist not found"
-
-        if os.path.exists("%s/.gsharkdown/downqueue.pkl" % os.environ.get("HOME")):
-            self.load_downqueue_list("%s/.gsharkdown/downqueue.pkl" % os.environ.get("HOME"))
-
-        if int(config()['show_notification']) == 1 and env().HAVE_NOTIFY:
-            pynotify.init("gSharkDown")
-
+            
+        self.staticon = guihelpers.GsharkIndicator(self)
+        
         # Set default directory if is empty
         if config()['down_path'] == "":
             config()['down_path'] = env().get_default_down_path()
 
+        # Pynotify initialization
+        if int(config()['show_notification']) == 1 and env().HAVE_NOTIFY:
+            pynotify.init("gSharkDown")
+            
         # Scrobbling initialisation
         self.lastfm = ""
         if int(config()['scrobbling']) == 1 and env().HAVE_PYLAST:
                                           password_hash = config()['lastpass'])
                 self.lovebutton.set_sensitive(True)
             except pylast.WSError:
-                error = lib.guihelpers.ErrorMessage(self.window,
+                error = guihelpers.ErrorMessage(self.window,
                             _("Check your password and username for Last.fm"))
                 error.show_all()
                 pass
         builder.connect_signals(self)
         self.tlisten = KeyListenerThread(self)
         self.tlisten.start()
+        
+        # Updates checking
         if config()['update_checked'] == 0:
             self.check_for_update(None)
         else:
             if config()['startup_update_check'] == 1:
                 self.check_for_update(None)
+        
+        self.window.set_sensitive(False)
         self.window.show_all()
+        
+    def on_init_thread_end(self):
+        self.window.set_sensitive(True)
+        if os.path.exists("%s/.gsharkdown/downqueue.pkl" % os.environ.get("HOME")):
+            self.load_downqueue_list("%s/.gsharkdown/downqueue.pkl" % os.environ.get("HOME"))
 
     def get_playing_iter(self):
         iter = self.playlist.get_iter_first()
                 eoferror = False
 
         for i in range(0, len(downitems)):
-            tDownload = DownloadThread(self,
-                                       downitems[i]['song'],
-                                       downitems[i]['filename'])
-            tDownload.start()
+            self.downloads.append_song(Song(downitems[i]))
 
     def save_playlist(self, path):
         """
                             _("Error saving the playlist"))
             error.show_all()
 
-    def save_downwueue(self, path):
+    def save_downqueue(self, path):
         """
         Saves the download queue to pickle file in
         /home/$USER/.gsharkdown/downqueue.pkl
         """
         try:
             output = open(path, 'w')
-            for i in range(0, len(self.downqueue)):
-                pickle.dump(self.downqueue[i], output, -1)
+            for i in self.downloads.range():
+                data = self.downloads.get_song(i).get_data()
+                data["filename"] = self.downloads.get_song(i).get_filename()
+                pickle.dump(data, output, -1)
             output.close()
-        except:
-            error = lib.guihelpers.ErrorMessage(None,
+        except Exception, e:
+            error = guihelpers.ErrorMessage(None,
                             _("Error saving the download queue"))
             error.show_all()
 
             dialog = gtk.MessageDialog(None, gtk.DIALOG_MODAL,
                                   type = gtk.MESSAGE_QUESTION,
                                   buttons = gtk.BUTTONS_YES_NO,
-                                  message_format = _("Are you sure to quit?"))
+                                  message_format = _("Are you sure you want to quit gSharkDown?"))
             dialog.set_title(_("Quit?"))
             response = dialog.run()
             dialog.destroy()
 
         if response == gtk.RESPONSE_YES:
             self.save_playlist("%s/.gsharkdown/playlist.pkl" % os.environ.get("HOME"))
-            self.save_downwueue("%s/.gsharkdown/downqueue.pkl" % os.environ.get("HOME"))
-            self.stop_all_downloads()
+            self.save_downqueue("%s/.gsharkdown/downqueue.pkl" % os.environ.get("HOME"))
+            self.downloads.stop_all_downloads()
             gtk.main_quit()
 
     def copy_song(self, widget, data = None):
             #lyric = lib.lyrdblib.search(song['ArtistName'],
             #                             song['SongName'])
         else:
-            info = lib.guihelpers.InfoDialog(self.window,
+            info = guihelpers.InfoDialog(self.window,
                 _("There should be a playing song to view\nthe lyrics for it"))
             info.show_all()
 
                                           password_hash = config()['lastpass'])
             self.lovebutton.set_sensitive(True)
         except pylast.WSError:
-            lib.guihelpers.ErrorMessage(self.window,
+            guihelpers.ErrorMessage(self.window,
                     _("Please check your username and password for Last.fm"))
             config()['lastuser'] = ""
             config()['lastpass'] = ""
 
             if filename != None:
                 song.set_filename(filename)
-                self.downloads.append_song(song)
+                self.downloads.append_song_restarting(song)
 
     def update_downloads_count(self):
         """
             else:
                 self.downmenu.popup(None, None, None, event.button, event.time)
 
-    def cancel_download(self, path, option = None):
-        """
-        Cancel a download by a list path
-        """
-        thread = self.downloads[path[0]][4]
-        thread.stop(option)
-        # TODO: Should improve the busy waiting
-        while(thread.is_alive()):
-            pass
-
-    def cancel_all_downloads(self):
+    def on_cancel_all_downloads(self):
         """
         Cancel all downloads
         """
-        iter = self.downloads.get_iter_first()
+        iter = self.downloads.first()
         while iter != None:
-            path = self.downloads.get_path(iter)
-            iter = self.downloads.iter_next(iter)
-            self.cancel_download(path)
-
-    def stop_all_downloads(self):
+            self.downloads.get_song(iter).cancel_download()
+            iter = self.downloads.next(iter)
+            
+    def on_pause_all_downloads(self):
         """
-        Stop all downloads used, when quitting.
-        Sets the option to stop for the thread
+        Cancel all downloads
         """
-        for i in range(0, len(self.downqueue)):
-            path = self.downloads.get_path(self.downqueue[i]['iter'])
-            self.cancel_download(path, 'stop')
+        iter = self.downloads.first()
+        while iter != None:
+            self.downloads.get_song(iter).pause_download()
+            iter = self.downloads.next(iter)
+            
+    def on_resume_all_downloads(self):
+        """
+        Cancel all downloads
+        """
+        iter = self.downloads.first()
+        while iter != None:
+            if self.downloads.get_song(iter).is_downloading() == False:
+                self.downloads.get_song(iter).download_file(False)
+            iter = self.downloads.next(iter)
 
     def on_cancel_download(self, menu, data = None):
         """
 
     def on_love_song(self, button):
         if self.current_song == None:
-            info = lib.guihelpers.InfoDialog(self.window,
+            info = guihelpers.InfoDialog(self.window,
                     _("There is no song to be loved.\n A song should be playing"))
             info.show_all()
         else:
 import pycurl
 import time
 import groove
+import re
 
 class Song(gobject.GObject):
     """
         self.__gobject_init__()
         self.data = data
         self.cover_pixbuf = None
-        self.download_thread = None
         self.download_progress = None
         self.file_size = None
-        self.filename = self.get_default_filename()
+        self.filename = None
+        self.download_thread = DownloadThread(self)
+        
+        try:
+            self.filename = self.data["filename"]
+        except KeyError:
+            self.filename = self.get_default_filename()
         
         if self.get_cover_filename() == "":
             self.set_cover_missed_pixbuf()
         thread = SongCoverThread(self)
         thread.start()
         
-    def download_file(self, options = None):
-        if options == None:
-            options = {"continue": None, "speed": 0}
-            
-        self.download_thread = DownloadThread(self, options)
+    def download_file(self, restart = False, speed = None):
+        if speed == None:
+            speed = int(config()["speed_limit"])
+        
+        self.restart = restart
+        self.speed = speed
         self.download_thread.start()
         
-    def cancel_download_sync(self):
-        self.download_thread.stop()
-        while(thread.is_alive()):
-            time.sleep(0.01)
+    def is_downloading(self):
+        return self.download_thread.is_alive()
+    
+    def is_paused(self):
+        return self.is_downloading() == False and self.download_thread.is_canceled == False
+        
+    def pause_download(self):
+        if self.is_downloading():
+            self.download_thread.pause()
+        
+    def cancel_download(self):
+        if self.is_downloading():
+            self.download_thread.cancel()
+        
+    def pause_download_sync(self):
+        if self.is_downloading():
+            self.download_thread.pause()
+            self.download_thread.join();
             
     def get_download_progress(self):
         return self.download_progress
 gobject.type_register(Song)
 # Emitted when the cover download was finished
 gobject.signal_new("cover-downloaded", Song, gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ())
-# Emitted when download starts
+# Emitted when download starts or resumes
+gobject.signal_new("download-initializing", Song, gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ())
+# Emitted when download starts or resumes
 gobject.signal_new("download-started", Song, gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ())
 # Emitted when cUrl receive data from GrooveShark (useful for update download progress)
 gobject.signal_new("download-updated", Song, gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ())
 gobject.signal_new("download-completed", Song, gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ())
 # Emitted when download is canceled
 gobject.signal_new("download-canceled", Song, gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ())
+# Emitted when download is paused
+gobject.signal_new("download-paused", Song, gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ())
+# Emitted when download is stopped
+gobject.signal_new("download-stopped", Song, gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, ())
 # Emitted when a download error occurs, an error message is passed by parameter
 gobject.signal_new("download-error", Song, gobject.SIGNAL_RUN_FIRST, gobject.TYPE_NONE, (gobject.TYPE_STRING,))
 
 
 class DownloadThread(threading.Thread):
 
-    def __init__(self, song, options):
+    def __init__(self, song):
         """
         _window: The main window object
         _song: The song to download
         threading.Thread.__init__(self)
 
         self.song = song
-        self.cont = options["continue"]
-        self.speed = options["speed"]
+        self.restart = False
+        self.speed = 0
         self.first_hook = True
         self._stop = threading.Event()
+        self.is_canceled = True
+        self.file = None
+        self.curl = None
 
+    def run(self):
+        self.song.emit("download-initializing")
+        
+        self.first_hook = True
+        self._stop.clear()
+        restart = self.restart
+        
+        if restart == False and os.path.exists(self.song.get_filename()) == False:
+            restart = True
+        
         try:
-            if self.cont == None:
-                self.file = open(self.song.get_filename(), "w")
+            if restart == True:
+                self.file = open(self.song.get_filename(), "wb")
             else:
                 self.file = open(self.song.get_filename(), "ab")
         except IOError:
             self.song.emit("download-error", _("Failed to create '%s' for writing.") % self.song.get_filename())
             return
-
-    def run(self):
+        
         try:
             key = groove.getStreamKeyFromSongIDEx(self.song.get_id())
-        except Exception, e:
-            self.song.emit("download-error", e.args)
-            return
+        except Exception as e:
+            self.song.emit("download-error", e.__str__())
+            raise
 
         try:
             url = "http://" + key["result"]["%s" % self.song.get_id()]["ip"] + "/stream.php"
             c.setopt(pycurl.URL, url)
             c.setopt(pycurl.NOPROGRESS, 0)
             c.setopt(pycurl.PROGRESSFUNCTION, self.hook)
+            c.setopt(pycurl.HEADERFUNCTION, self.header_hook)
             c.setopt(pycurl.FAILONERROR, True)
-            if self.cont == None:
-                c.setopt(pycurl.FILE, self.file)
-            else:
+            c.setopt(pycurl.WRITEDATA, self.file)
+            if restart == False:
                 c.setopt(pycurl.RESUME_FROM, os.path.getsize(self.song.get_filename()))
-                c.setopt(pycurl.WRITEDATA, self.file)
             c.setopt(pycurl.POST, True)
             c.setopt(pycurl.NOSIGNAL, True)
             if self.speed != 0:
                 c.setopt(pycurl.MAX_RECV_SPEED_LARGE, self.speed)
             c.setopt(pycurl.POSTFIELDS, str("streamKey=" + key["result"]["%s" % self.song.get_id()]["streamKey"]))
+            
             c.perform()
+            
             self.file.close()
             self.song.set_download_progress(100)
             self.song.emit("download-completed")
         except pycurl.error, e:
+            self.file.close()
             if e[0] == pycurl.E_ABORTED_BY_CALLBACK:
-                self.file.close()
-                self.song.emit("download-canceled")
+                if self.is_canceled == True:
+                    os.remove(self.song.get_filename())
+                    self.song.emit("download-canceled")
+                else:
+                    self.song.emit("download-paused")
             else:
-                self.file.close()
                 os.remove(self.song.get_filename())
+                self.song.emit("download-error", e[1])
         except Exception, e:
             self.file.close()
             os.remove(self.song.get_filename())
-            self.song.emit("download-error", e.args)
-
-    def stop(self, option = 'cancel'):
-        # TODO: This one should be checked how to resolve, at the moment
-        # stopping the thread terminates the program
+            self.song.emit("download-error", e.__str__())
+            raise
+            
+        self.song.emit("download-stopped")
+    
+    def cancel(self):
+        self.is_canceled = True
         self._stop.set()
-
+        
+    def pause(self):
+        self.is_canceled = False
+        self._stop.set()
+    
     def stopped(self):
         return self._stop.isSet()
-
+    
+    def header_hook(self, data):
+        data = data.split("\r\n")
+        matches = re.match(r"HTTP/\d\.\d (\d+)", data[0])
+        if matches:
+            status = matches.group(1)
+            
+            # If is not partial content (partial content is status code 206)
+            if status == "200":
+                self.file.truncate(0)
+    
     def hook(self, downloadTotal, downloadCurrent, uploadTotal, uploadCurrent):
         if downloadTotal > 0:
             if self.first_hook:
 
 
 class InitThread(threading.Thread):
-    def __init__(self, _frame):
+    def __init__(self, app):
         threading.Thread.__init__(self)
         self._stop = threading.Event()
-        self.frame = _frame
+        self.app = app
 
     def run(self):
         p = 1
                     error.show_all()
                     p = 0
                     self.stop()
+                    
+        gobject.idle_add(self.app.on_init_thread_end)
+        
     def stop(self):
         self._stop.set()