Major list upgrade - now images in most places, and when selecting a featured track...
authorKristoffer Grönlund <kristoffer.gronlund@purplescout.se>
Sun, 3 Jan 2010 20:12:55 +0000 (21:12 +0100)
committerKristoffer Grönlund <kristoffer.gronlund@purplescout.se>
Sun, 3 Jan 2010 20:12:55 +0000 (21:12 +0100)
debian/control
jamaendo/api.py
jamaui/albumlist.py
jamaui/featured.py
jamaui/playerwindow.py
jamaui/radios.py
jamaui/search.py
jamaui/songposition.py
jamaui/ui.py

index 6c717f1..be163ea 100644 (file)
@@ -10,7 +10,7 @@ Homepage: http://jamaendo.garage.maemo.org/
 
 Package: jamaendo
 Architecture: all
-Depends: ${python:Depends}, python2.5-gtk2, python2.5-gstreamer, python2.5-dbus, python2.5-hildon, python-lxml, python2.5-osso
+Depends: ${python:Depends}, python2.5-gtk2, python2.5-gstreamer, python2.5-dbus, python2.5-hildon, python-lxml, python2.5-osso, python-pycurl
 Description: 
 XB-Maemo-Display-Name: Jamaendo Player
 XB-Maemo-Icon-26:
index f7cbc0e..3de54e9 100644 (file)
@@ -24,6 +24,7 @@
 # Image / cover downloads.. and more?
 import urllib, threading, os, time, simplejson, re
 import logging, hashlib
+import pycurl, StringIO
 
 _CACHEDIR = None
 _COVERDIR = None
@@ -52,6 +53,19 @@ _TRACK_FIELDS = ['id', 'name', 'album_image', 'artist_id', 'artist_name', 'album
 _RADIO_FIELDS = ['id', 'name', 'idstr', 'image']
 _TAG_FIELDS = ['id', 'name']
 
+def curlGET(url):
+    c = pycurl.Curl()
+    s = StringIO.StringIO()
+    c.setopt(pycurl.FOLLOWLOCATION, 1)
+    c.setopt(pycurl.URL, url)
+    c.setopt(pycurl.WRITEFUNCTION, s.write)
+    try:
+        c.perform()
+    finally:
+        c.close()
+    s.seek(0)
+    return s.read()
+
 class LazyQuery(object):
     def set_from_json(self, json):
         for key, value in json.iteritems():
@@ -225,9 +239,7 @@ class Query(object):
         log.info("%s", url)
         Query._ratelimit()
         try:
-            f = urllib.urlopen(url)
-            ret = simplejson.load(f)
-            f.close()
+            ret = simplejson.loads(curlGET(url))
         except Exception, e:
             return None
         return ret
@@ -245,13 +257,29 @@ class CoverFetcher(threading.Thread):
         self.cond = threading.Condition()
         self.work = []
 
+    def _retrieve(self, url, fname):
+        f = open(fname, 'wb')
+        c = pycurl.Curl()
+        c.setopt(pycurl.FOLLOWLOCATION, 1)
+        c.setopt(pycurl.URL, str(url))
+        c.setopt(pycurl.WRITEFUNCTION, f.write)
+        try:
+            c.perform()
+        except:
+            fname = None
+        finally:
+            c.close()
+            f.close()
+        log.debug("Coverfetch: %s -> %s", url, fname)
+        return fname
+
     def _fetch_cover(self, albumid, size):
         try:
             coverdir = _COVERDIR if _COVERDIR else '/tmp'
             to = os.path.join(coverdir, '%d-%d.jpg'%(albumid, size))
             if not os.path.isfile(to):
                 url = _GET2+'image/album/redirect/?id=%d&imagesize=%d'%(albumid, size)
-                urllib.urlretrieve(url, to)
+                to = self._retrieve(url, to)
             return to
         except Exception, e:
             return None
@@ -262,7 +290,7 @@ class CoverFetcher(threading.Thread):
             coverdir = _COVERDIR if _COVERDIR else '/tmp'
             to = os.path.join(coverdir, h+'.jpg')
             if not os.path.isfile(to):
-                urllib.urlretrieve(url, to)
+                to = self._retrieve(url, to)
             return to
         except Exception, e:
             return None
@@ -276,7 +304,7 @@ class CoverFetcher(threading.Thread):
     def request_images(self, urls, cb):
         """cb([(url, image)])"""
         self.cond.acquire()
-        self.work.insert(0, ('images', urls, cb))
+        self.work = [('image', url, cb) for url in urls] + self.work
         self.cond.notify()
         self.cond.release()
 
@@ -294,22 +322,20 @@ class CoverFetcher(threading.Thread):
 
             multi = len(work) > 1
             for job in work:
-                if job[0] == 'images':
-                    self.process_images(job[1], job[2])
+                if job[0] == 'image':
+                    self.process_image(job[1], job[2])
                 else:
                     self.process_cover(*job)
-                if multi:
-                    time.sleep(1.0)
 
     def process_cover(self, albumid, size, cb):
         cover = self._fetch_cover(albumid, size)
         if cover:
             cb(albumid, size, cover)
 
-    def process_images(self, urls, cb):
-        results = [(url, image) for url, image in ((url, self._fetch_image(url)) for url in urls) if image is not None]
-        if results:
-            cb(results)
+    def process_image(self, url, cb):
+        image = self._fetch_image(url)
+        if image:
+            cb([(url, image)])
 
 class CoverCache(object):
     """
index 5c82637..5653310 100644 (file)
+import os
 import gtk
+import gobject
 import hildon
 import jamaendo
+import util
 from settings import settings
 from postoffice import postoffice
 import logging
 
 log = logging.getLogger(__name__)
 
-class ImageDownloader(object):
+class _BaseList(gtk.TreeView):
     """
-    TODO: background downloader of images
-    for album lists, track lists, etc
+    TODO: unify the different lists into one
     """
+    ICON_SIZE = 50
 
-class AlbumList(gtk.TreeView):
     def __init__(self):
         gtk.TreeView.__init__(self)
-        self.__store = gtk.ListStore(str, int)
+        self.__store = None
+        self.default_pixbuf = util.find_resource('album.png')
+        self.connect('destroy', self.on_destroy)
+
+    def get_pixbuf(self, img):
+        try:
+            return gtk.gdk.pixbuf_new_from_file_at_size(img,
+                                                        self.ICON_SIZE,
+                                                        self.ICON_SIZE)
+        except gobject.GError:
+            log.error("Broken image in cache: %s", img)
+            try:
+                os.unlink(img)
+            except OSError, e:
+                log.warning("Failed to unlink broken image.")
+            if img != self.default_pixbuf:
+                return self.get_default_pixbuf()
+            else:
+                return None
+
+    def get_default_pixbuf(self):
+        if self.default_pixbuf:
+            return self.get_pixbuf(self.default_pixbuf)
+
+    def on_destroy(self, wnd):
+        pass
+
+class MusicList(_BaseList):
+    def __init__(self):
+        _BaseList.__init__(self)
+        (self.COL_ICON, self.COL_NAME, self.COL_ID, self.COL_IMAGE) = range(4)
+        self.__store = gtk.ListStore(gtk.gdk.Pixbuf, str, int, str)
+
         self.set_model(self.__store)
 
+        icon = gtk.TreeViewColumn('Icon')
+        self.append_column(icon)
+        cell = gtk.CellRendererPixbuf()
+        icon.pack_start(cell, True)
+        icon.add_attribute(cell, 'pixbuf', self.COL_ICON)
+
         col = gtk.TreeViewColumn('Name')
         self.append_column(col)
         cell = gtk.CellRendererText()
         col.pack_start(cell, True)
-        col.add_attribute(cell, 'text', 0)
-        self.set_search_column(0)
-        col.set_sort_column_id(0)
-
+        col.add_attribute(cell, 'text', self.COL_NAME)
+        self.set_search_column(self.COL_NAME)
+        col.set_sort_column_id(self.COL_NAME)
+
+        postoffice.connect('images', self, self.on_images)
+
+    def get_item_id(self, path):
+        return self.__store.get(self.__store.get_iter(path), self.COL_ID)[0]
+
+    def on_destroy(self, wnd):
+        postoffice.disconnect('images', self)
+
+    def on_images(self, images):
+        for url, image in images:
+            for row in self.__store:
+                if row[self.COL_IMAGE] == url:
+                    pb = self.get_pixbuf(image)
+                    if pb:
+                        row[self.COL_ICON] = pb
+
+    def add_items(self, items):
+        images = [x for x in (self.get_item_image(item) for item in items) if x]
+        for item in items:
+            txt = self.get_item_text(item)
+            self.__store.append([self.get_default_pixbuf(), txt, item.ID, self.get_item_image(item)])
+        if images:
+            postoffice.notify('request-images', images)
+
+    def get_item_text(self, item):
+        if isinstance(item, jamaendo.Album):
+            return "%s - %s" % (item.artist_name, item.name)
+        elif isinstance(item, jamaendo.Track):
+            return "%s - %s" % (item.artist_name, item.name)
+        else:
+            return item.name
+
+    def get_item_image(self, item):
+        ret = None
+        if isinstance(item, jamaendo.Track):
+            ret = item.album_image
+        elif hasattr(item, 'image'):
+            ret = item.image
+        if ret:
+            ret = ret.replace('1.100.jpg', '1.%d.jpg'%(self.ICON_SIZE))
+        return ret
+
+class AlbumList(_BaseList):
+    def __init__(self):
+        _BaseList.__init__(self)
+        (self.COL_ICON, self.COL_NAME, self.COL_ID) = range(3)
+        self.__store = gtk.ListStore(gtk.gdk.Pixbuf, str, int)
         self.__show_artist = True
 
+        self.set_model(self.__store)
+
+        icon = gtk.TreeViewColumn('Icon')
+        self.append_column(icon)
+        cell = gtk.CellRendererPixbuf()
+        icon.pack_start(cell, True)
+        icon.add_attribute(cell, 'pixbuf', self.COL_ICON)
+
+        col = gtk.TreeViewColumn('Name')
+        self.append_column(col)
+        cell = gtk.CellRendererText()
+        col.pack_start(cell, True)
+        col.add_attribute(cell, 'text', self.COL_NAME)
+        self.set_search_column(self.COL_NAME)
+        col.set_sort_column_id(self.COL_NAME)
+
+        postoffice.connect('album-cover', self, self.on_album_cover)
+
+    def on_destroy(self, wnd):
+        _BaseList.on_destroy(self, wnd)
+        postoffice.disconnect('album-cover', self)
+
+    def on_album_cover(self, albumid, size, cover):
+        if size == self.ICON_SIZE:
+            for row in self.__store:
+                if row[self.COL_ID] == albumid:
+                    row[self.COL_ICON] = self.get_pixbuf(cover)
+
     def add_album(self, album):
         if self.__show_artist:
             txt = "%s - %s" % (album.artist_name, album.name)
         else:
             txt = album.name
-        self.__store.append([txt, album.ID])
+        self.__store.append([self.get_default_pixbuf(), txt, album.ID])
+        postoffice.notify('request-album-cover', album.ID, self.ICON_SIZE)
 
     def get_album_id(self, path):
-        treeiter = self.__store.get_iter(path)
-        _, _id = self.__store.get(treeiter, 0, 1)
-        return _id
+        return self.__store.get(self.__store.get_iter(path), self.COL_ID)[0]
 
     def show_artist(self, show):
         self.__show_artist = show
 
-class TrackList(gtk.TreeView):
+class TrackList(_BaseList):
     def __init__(self, numbers = True):
-        gtk.TreeView.__init__(self)
+        _BaseList.__init__(self)
         self.track_numbers = numbers
         self.__store = gtk.ListStore(int, str, int)
         self.set_model(self.__store)
@@ -75,29 +189,51 @@ class TrackList(gtk.TreeView):
         _, _, _id = self.__store.get(treeiter, 0, 1, 2)
         return _id
 
-class RadioList(gtk.TreeView):
+class RadioList(_BaseList):
     def __init__(self):
-        gtk.TreeView.__init__(self)
-        self.__store = gtk.ListStore(str, int)
+        _BaseList.__init__(self)
+        (self.COL_ICON, self.COL_NAME, self.COL_ID, self.COL_IMAGE) = range(4)
+        self.__store = gtk.ListStore(gtk.gdk.Pixbuf, str, int, str)
         self.set_model(self.__store)
 
+        icon = gtk.TreeViewColumn('Icon')
+        self.append_column(icon)
+        cell = gtk.CellRendererPixbuf()
+        icon.pack_start(cell, True)
+        icon.add_attribute(cell, 'pixbuf', self.COL_ICON)
+
         col = gtk.TreeViewColumn('Name')
         self.append_column(col)
         cell = gtk.CellRendererText()
         col.pack_start(cell, True)
-        col.add_attribute(cell, 'text', 0)
+        col.add_attribute(cell, 'text', self.COL_NAME)
+        self.set_search_column(self.COL_NAME)
+        col.set_sort_column_id(self.COL_NAME)
 
-        self.set_search_column(0)
-        col.set_sort_column_id(0)
+        postoffice.connect('images', self, self.on_images)
+
+    def on_destroy(self, wnd):
+        postoffice.disconnect('images', self)
+
+    def add_radios(self, radios):
+        for radio in radios:
+            self.__store.append([self.get_default_pixbuf(), self.radio_name(radio), radio.ID, radio.image])
+        postoffice.notify('request-images', [radio.image for radio in radios])
 
-    def add_radio(self, radio):
-        self.__store.append([self.radio_name(radio), radio.ID])
 
     def get_radio_id(self, path):
         treeiter = self.__store.get_iter(path)
-        name, _id = self.__store.get(treeiter, 0, 1)
+        name, _id = self.__store.get(treeiter, self.COL_NAME, self.COL_ID)
         return name, _id
 
+    def on_images(self, images):
+        for url, image in images:
+            for row in self.__store:
+                if row[self.COL_IMAGE] == url:
+                    pb = self.get_pixbuf(image)
+                    if pb:
+                        row[self.COL_ICON] = pb
+
     def radio_name(self, radio):
         if radio.idstr:
             return radio.idstr.capitalize()
index eec493b..7f20c39 100644 (file)
@@ -27,6 +27,8 @@ import jamaendo
 from playerwindow import open_playerwindow
 from showartist import ShowArtist
 from showalbum import ShowAlbum
+from albumlist import MusicList
+from player import Playlist
 
 def _alist(l, match):
     for key, value in l:
@@ -45,30 +47,21 @@ class FeaturedWindow(hildon.StackableWindow):
 
     def __init__(self, feature):
         hildon.StackableWindow.__init__(self)
-        self.set_title("Featured")
+        self.set_title(feature)
 
         self.featurefn = _alist(self.features, feature)
 
         # Results list
         self.panarea = hildon.PannableArea()
-        self.result_store = gtk.ListStore(str, int)
-        #self.result_store.append(['red'])
-        self.result_view = gtk.TreeView(self.result_store)
-        col = gtk.TreeViewColumn('Name')
-        self.result_view.append_column(col)
-        cell = gtk.CellRendererText()
-        col.pack_start(cell, True)
-        col.add_attribute(cell, 'text', 0)
-        self.result_view.set_search_column(0)
-        col.set_sort_column_id(0)
-        self.result_view.connect('row-activated', self.row_activated)
-
-        self.panarea.add(self.result_view)
+        self.musiclist = MusicList()
+        self.musiclist.connect('row-activated', self.row_activated)
+        self.panarea.add(self.musiclist)
 
         self.idmap = {}
-        for item in self.featurefn():
+        self.items = self.featurefn()
+        for item in self.items:
             self.idmap[item.ID] = item
-            self.result_store.append([self.get_item_text(item), item.ID])
+        self.musiclist.add_items(self.items)
 
         self.add(self.panarea)
 
@@ -86,33 +79,9 @@ class FeaturedWindow(hildon.StackableWindow):
         self.menu.show_all()
         self.set_app_menu(self.menu)
 
-    def get_item_text(self, item):
-        if isinstance(item, jamaendo.Album):
-            return "%s - %s" % (item.artist_name, item.name)
-        elif isinstance(item, jamaendo.Track):
-            return "%s - %s" % (item.artist_name, item.name)
-        else:
-            return item.name
-
-    def make_button(self, text, subtext, callback):
-        button = hildon.Button(gtk.HILDON_SIZE_FINGER_HEIGHT,
-                               hildon.BUTTON_ARRANGEMENT_VERTICAL)
-        button.set_text(text, subtext)
-
-        if callback:
-            button.connect('clicked', callback)
-
-        #image = gtk.image_new_from_stock(gtk.STOCK_INFO, gtk.ICON_SIZE_BUTTON)
-        #button.set_image(image)
-        #button.set_image_position(gtk.POS_RIGHT)
-
-        return button
-
     def row_activated(self, treeview, path, view_column):
-        treeiter = self.result_store.get_iter(path)
-        title, _id = self.result_store.get(treeiter, 0, 1)
+        _id = self.musiclist.get_item_id(path)
         item = self.idmap[_id]
-        #print _id, item
         self.open_item(item)
 
     def open_item(self, item):
@@ -123,8 +92,10 @@ class FeaturedWindow(hildon.StackableWindow):
             wnd = ShowArtist(item)
             wnd.show_all()
         elif isinstance(item, jamaendo.Track):
+            playlist = Playlist(self.items)
+            playlist.jump_to(item.ID)
             wnd = open_playerwindow()
-            wnd.play_tracks([item])
+            wnd.play_tracks(playlist)
         elif isinstance(item, jamaendo.Tag):
             wnd = open_playerwindow()
             wnd.play_tracks(jamaendo.get_tag_tracks(item.ID))
index ab6e95e..16bfffe 100644 (file)
@@ -240,6 +240,7 @@ class PlayerWindow(hildon.StackableWindow):
     def set_default_cover(self):
         tmp = util.find_resource('album.png')
         if tmp:
+            log.debug("Setting cover to %s", tmp)
             self.cover.set_from_file(tmp)
 
     def update_state(self):
@@ -259,6 +260,7 @@ class PlayerWindow(hildon.StackableWindow):
         if size == 300:
             playing = self.get_album_id()
             if playing and albumid and (int(playing) == int(albumid)):
+                log.debug("Setting cover to %s", cover)
                 self.cover.set_from_file(cover)
 
     def play_radio(self, radio_name, radio_id):
index dc3109c..da10c77 100644 (file)
@@ -41,9 +41,10 @@ class RadiosWindow(hildon.StackableWindow):
 
         self.radios = {}
         hildon.hildon_gtk_window_set_progress_indicator(self, 1)
-        for item in jamaendo.starred_radios():
+        radios = jamaendo.starred_radios()
+        for item in radios:
             self.radios[item.ID] = item
-            self.radiolist.add_radio(item)
+        self.radiolist.add_radios(radios)
         hildon.hildon_gtk_window_set_progress_indicator(self, 0)
 
         self.add(self.panarea)
index e487765..4092048 100644 (file)
@@ -27,6 +27,7 @@ import jamaendo
 from playerwindow import open_playerwindow
 from showartist import ShowArtist
 from showalbum import ShowAlbum
+from albumlist import MusicList
 
 class SearchWindow(hildon.StackableWindow):
     def __init__(self):
@@ -39,6 +40,7 @@ class SearchWindow(hildon.StackableWindow):
 
         # Results list
         self.panarea = hildon.PannableArea()
+        self.musiclist = MusicList()
         self.result_store = gtk.ListStore(str, int)
         #self.result_store.append(['red'])
         self.result_view = gtk.TreeView(self.result_store)
index 705962c..668deec 100644 (file)
@@ -6,7 +6,7 @@ log = logging.getLogger(__name__)
 
 # shows the current song position (looking a bit nicer than a default widget, hopefully)
 class SongPosition(gtk.DrawingArea):
-    WIDTH = 8.0
+    WIDTH = 32.0
     HEIGHT = 8.0
 
     def __init__(self):
@@ -29,9 +29,9 @@ class SongPosition(gtk.DrawingArea):
         darkclr.add_color_stop_rgba(1.0, 0.25, 0.25, 0.25, 1.0)
 
         markerclr = cairo.LinearGradient(0.0, 0.0, 0.0, self.HEIGHT)
-        markerclr.add_color_stop_rgba(0.0, *orange1)
-        markerclr.add_color_stop_rgba(0.5, *orange0)
-        markerclr.add_color_stop_rgba(1.0, *orange0)
+        markerclr.add_color_stop_rgba(0.0, 1.0, 1.0, 1.0, 0.0)
+        markerclr.add_color_stop_rgba(0.5, 1.0, 1.0, 1.0, 0.75)
+        markerclr.add_color_stop_rgba(1.0, 1.0, 1.0, 1.0, 1.0)
 
         self.lightclr = lightclr
         self.darkclr = darkclr
@@ -84,7 +84,10 @@ class SongPosition(gtk.DrawingArea):
         context.fill()
 
     def set_position(self, pos):
-        assert 0 <= pos <= 1
+        if pos < 0.0:
+            pos = 0.0
+        elif pos > 1.0:
+            pos = 1.0
         self.pos = pos
         self.invalidate()
 
index 9b6300d..2c0ad65 100644 (file)
@@ -38,7 +38,7 @@ gobject.threads_init()
 
 log = logging.getLogger(__name__)
 
-VERSION = '0.1'
+VERSION = '0.2'
 
 try:
     import hildon
@@ -95,6 +95,7 @@ class Jamaui(object):
         settings.load()
 
         postoffice.connect('request-album-cover', self, self.on_request_cover)
+        postoffice.connect('request-images', self, self.on_request_images)
         log.debug("Created main window.")
 
     def save_settings(self):
@@ -176,9 +177,15 @@ class Jamaui(object):
     def on_request_cover(self, albumid, size):
         jamaendo.get_album_cover_async(self.got_album_cover, int(albumid), size)
 
+    def on_request_images(self, urls):
+        jamaendo.get_images_async(self.got_images, urls)
+
     def got_album_cover(self, albumid, size, cover):
         postoffice.notify('album-cover', albumid, size, cover)
 
+    def got_images(self, images):
+        postoffice.notify('images', images)
+
     #def add_featured_button(self):
     #    self.featured_sel = hildon.TouchSelector(text=True)
     #    self.featured_sel.append_text("Albums of the week")
@@ -193,7 +200,7 @@ class Jamaui(object):
     #    self.bbox.add(btn)
 
     def destroy(self, widget):
-        postoffice.disconnect('request-album-cover', self)
+        postoffice.disconnect(['request-album-cover', 'request-images'], self)
         self.save_settings()
         from player import the_player
         if the_player:
@@ -261,11 +268,12 @@ JAMENDO is an online platform that distributes musical works under Creative Comm
 
     def on_featured(self, button):
         dialog = hildon.PickerDialog(self.window)
-        sel = hildon.TouchSelector(text=True)
+        sel = hildon.TouchSelectorEntry(text=True)
         for feature, _ in FeaturedWindow.features:
             sel.append_text(feature)
         dialog.set_selector(sel)
         dialog.set_title("Featured")
+        sel.unselect_all(0)
         if dialog.run() == gtk.RESPONSE_OK:
             txt = sel.get_current_text()
             self.featuredwnd = FeaturedWindow(txt)