New backend: api2.py
authorKristoffer Grönlund <kristoffer.gronlund@purplescout.se>
Thu, 31 Dec 2009 00:55:36 +0000 (01:55 +0100)
committerKristoffer Grönlund <kristoffer.gronlund@purplescout.se>
Sat, 2 Jan 2010 23:37:44 +0000 (00:37 +0100)
14 files changed:
data/bg.xcf [new file with mode: 0644]
data/jamaendo.desktop [new file with mode: 0644]
data/jamaendo.png [new file with mode: 0644]
data/jamendo-icon.jpg [new file with mode: 0644]
data/logo.png [new file with mode: 0644]
data/play.png [new file with mode: 0644]
jamaendo/api.py
jamaendo/api2.py [new file with mode: 0644]
jamaui/player.py
jamaui/refresh.py [new file with mode: 0644]
jamaui/ui.py
jamaui/util.py
tests/__init__.py [new file with mode: 0644]
tests/testicle [new file with mode: 0755]

diff --git a/data/bg.xcf b/data/bg.xcf
new file mode 100644 (file)
index 0000000..a017eca
Binary files /dev/null and b/data/bg.xcf differ
diff --git a/data/jamaendo.desktop b/data/jamaendo.desktop
new file mode 100644 (file)
index 0000000..f4b72f4
--- /dev/null
@@ -0,0 +1,14 @@
+[Desktop Entry]
+Name=Jamaendo Media Player
+GenericName=Jamendo.com Free Music Player
+Comment=Streams music licensed under Creative Commons from jamendo.com
+Exec=jamaendo
+Icon=jamaendo
+Terminal=false
+Type=Application
+Categories=Audio;GTK;
+StartupWMClass=jamaendo
+StartupNotify=true
+X-Osso-Type=application/x-executable
+X-Osso-Service=org.jamaendo
+X-HildonDesk-ShowInToolbar=true
\ No newline at end of file
diff --git a/data/jamaendo.png b/data/jamaendo.png
new file mode 100644 (file)
index 0000000..113ef26
Binary files /dev/null and b/data/jamaendo.png differ
diff --git a/data/jamendo-icon.jpg b/data/jamendo-icon.jpg
new file mode 100644 (file)
index 0000000..9f241c2
Binary files /dev/null and b/data/jamendo-icon.jpg differ
diff --git a/data/logo.png b/data/logo.png
new file mode 100644 (file)
index 0000000..3b24930
Binary files /dev/null and b/data/logo.png differ
diff --git a/data/play.png b/data/play.png
new file mode 100644 (file)
index 0000000..4e01b8b
Binary files /dev/null and b/data/play.png differ
index 75335a1..c3cb3a5 100644 (file)
@@ -3,6 +3,10 @@ _DUMP_URL = '''http://img.jamendo.com/data/dbdump_artistalbumtrack.xml.gz'''
 _DUMP = os.path.expanduser('''~/.cache/jamaendo/dbdump.xml.gz''')
 _DUMP_TMP = os.path.expanduser('''~/.cache/jamaendo/new_dbdump.xml.gz''')
 
+# radio stream
+# /get2/stream/track/m3u/radio_track_inradioplaylist/?order=numradio_asc&radio_id=283
+
+
 try:
     os.makedirs(os.path.dirname(_DUMP))
 except OSError:
@@ -246,6 +250,8 @@ class Query(object):
     def track_mp3(trackid):
        return _GET2+ 'stream/track/redirect/?id=%d&streamencoding=mp31'%(trackid)
 
+# http://www.jamendo.com/get2/id+name+idstr+image/radio/json?order=starred_desc
+#track_id/track/json/radio_track_inradioplaylist/?order=numradio_asc&radio_id=%i
 class Queries(object):
     @staticmethod
     def albums_this_week():
@@ -284,7 +290,15 @@ class Queries(object):
         return q.emit(order='searchweight_desc', query=query)
 
     @staticmethod
-    def album_tracks(albumids, select=['id', 'name', 'numalbum']):
+    def album_tracks(albumids, select=['id',
+                                       'name',
+                                       'numalbum',
+                                       'image',
+                                       'duration',
+                                       'album_name',
+                                       'album_id',
+                                       'artist_name',
+                                       'artist_id']):
         #http://api.jamendo.com/get2/id+name/track/jsonpretty/?album_id=33+46
         q = Query(select=select,
                   request='track')
diff --git a/jamaendo/api2.py b/jamaendo/api2.py
new file mode 100644 (file)
index 0000000..32c8f5d
--- /dev/null
@@ -0,0 +1,520 @@
+# An improved, structured jamendo API for the N900 with cacheing
+# Image / cover downloads.. and more?
+import urllib, threading, os, gzip, time, simplejson, re
+#import util
+#if util.platform == 'maemo':
+#    _CACHEDIR = os.path.expanduser('''~/MyDocs/.jamaendo''')
+#else:
+#    _CACHEDIR = os.path.expanduser('''~/.cache/jamaendo''')
+
+_CACHEDIR = None#'/tmp/jamaendo'
+_COVERDIR = None#os.path.join(_CACHEDIR, 'covers')
+_GET2 = '''http://api.jamendo.com/get2/'''
+_MP3URL = _GET2+'stream/track/redirect/?id=%d&streamencoding=mp31'
+_OGGURL = _GET2+'stream/track/redirect/?id=%d&streamencoding=ogg2'
+
+
+def set_cache_dir(cachedir):
+    global _CACHEDIR
+    global _COVERDIR
+    _CACHEDIR = cachedir
+    _COVERDIR = os.path.join(_CACHEDIR, 'covers')
+
+    try:
+        os.makedirs(_CACHEDIR)
+    except OSError:
+        pass
+
+    try:
+        os.makedirs(_COVERDIR)
+    except OSError:
+        pass
+
+# These classes can be partially constructed,
+# and if asked for a property they don't know,
+# makes a query internally to get the full story
+
+_ARTIST_FIELDS = ['id', 'name', 'image']
+_ALBUM_FIELDS = ['id', 'name', 'image', 'artist_name', 'artist_id']
+_TRACK_FIELDS = ['id', 'name', 'image', 'artist_name', 'album_name', 'album_id', 'numalbum', 'duration']
+_RADIO_FIELDS = ['id', 'name', 'idstr', 'image']
+
+class LazyQuery(object):
+    def set_from_json(self, json):
+        for key, value in json.iteritems():
+            if key == 'id':
+                assert(self.ID == int(value))
+            else:
+                if key.endswith('_id'):
+                    value = int(value)
+                setattr(self, key, value)
+
+    def load(self):
+        """Not automatic for now,
+        will have to do artist.load()
+
+        This is filled in further down
+        in the file
+        """
+        raise NotImplemented
+
+    def _needs_load(self):
+        return True
+
+    def _set_from(self, other):
+        raise NotImplemented
+
+    def _needs_load_impl(self, *attrs):
+        for attr in attrs:
+            if getattr(self, attr) is None:
+                return True
+        return False
+
+    def _set_from_impl(self, other, *attrs):
+        for attr in attrs:
+            self._set_if(other, attr)
+
+    def _set_if(self, other, attrname):
+        if getattr(self, attrname) is None and getattr(other, attrname) is not None:
+            setattr(self, attrname, getattr(other, attrname))
+
+    def __repr__(self):
+        try:
+            return u"%s(%s)"%(self.__class__.__name__,
+                              u", ".join(repr(v) for k,v in self.__dict__.iteritems() if not k.startswith('_')))
+        except UnicodeEncodeError:
+            import traceback
+            traceback.print_exc()
+            return u"%s(?)"%(self.__class__.__name__)
+
+class Artist(LazyQuery):
+    def __init__(self, ID, json=None):
+        self.ID = int(ID)
+        self.name = None
+        self.image = None
+        self.albums = None # None means not downloaded
+        if json:
+            self.set_from_json(json)
+
+    def _needs_load(self):
+        return self._needs_load_impl('name', 'image', 'albums')
+
+    def _set_from(self, other):
+        return self._set_from_impl(other, 'name', 'image', 'albums')
+
+class Album(LazyQuery):
+    def __init__(self, ID, json=None):
+        self.ID = int(ID)
+        self.name = None
+        self.image = None
+        self.artist_name = None
+        self.artist_id = None
+        self.tracks = None # None means not downloaded
+        if json:
+            self.set_from_json(json)
+
+    def _needs_load(self):
+        return self._needs_load_impl('name', 'image', 'artist_name', 'artist_id', 'tracks')
+
+    def _set_from(self, other):
+        return self._set_from_impl(other, 'name', 'image', 'artist_name', 'artist_id', 'tracks')
+
+class Track(LazyQuery):
+    def __init__(self, ID, json=None):
+        self.ID = int(ID)
+        self.name = None
+        self.image = None
+        self.artist_name = None
+        self.album_name = None
+        self.album_id = None
+        self.numalbum = None
+        self.duration = None
+        if json:
+            self.set_from_json(json)
+
+    def mp3_url(self):
+       return _MP3URL%(self.ID)
+
+    def ogg_url(self):
+       return _OGGURL%(self.ID)
+
+    def _needs_load(self):
+        return self._needs_load_impl('name', 'image', 'artist_name', 'artist_id', 'tracks')
+
+    def _set_from(self, other):
+        return self._set_from_impl(other, 'name', 'image', 'artist_name', 'artist_id', 'tracks')
+
+class Radio(LazyQuery):
+    def __init__(self, ID, json=None):
+        self.ID = int(ID)
+        self.name = None
+        self.idstr = None
+        self.image = None
+        if json:
+            self.set_from_json(json)
+
+    def _needs_load(self):
+        return self._needs_load_impl('name', 'idstr', 'image')
+
+    def _set_from(self, other):
+        return self._set_from_impl(other, 'name', 'idstr', 'image')
+
+
+_artists = {} # id -> Artist()
+_albums = {} # id -> Album()
+_tracks = {} # id -> Track()
+_radios = {} # id -> Radio()
+
+
+# cache sizes per session (TODO)
+_CACHED_ARTISTS = 100
+_CACHED_ALBUMS = 200
+_CACHED_TRACKS = 500
+_CACHED_RADIOS = 10
+
+# TODO: cache queries?
+
+class Query(object):
+    last_query = time.time()
+    rate_limit = 1.0 # max queries per second
+
+    @classmethod
+    def _ratelimit(cls):
+        now = time.time()
+        if now - cls.last_query < cls.rate_limit:
+            time.sleep(cls.rate_limit - (now - cls.last_query))
+        cls.last_query = now
+
+    def __init__(self):
+        pass
+
+    def _geturl(self, url):
+        print "geturl: %s" % (url)
+        f = urllib.urlopen(url)
+        ret = simplejson.load(f)
+        f.close()
+        return ret
+
+    def __str__(self):
+        return "#%s" % (self.__class__.__name__)
+
+    def execute(self):
+        raise NotImplemented
+
+class CoverCache(object):
+    """
+    cache and fetch covers
+    TODO: background thread that
+    fetches and returns covers,
+    asynchronously, LIFO
+    """
+    def __init__(self):
+        self._covers = {} # (albumid, size) -> file
+        coverdir = _COVERDIR if _COVERDIR else '/tmp'
+        if os.path.isdir(coverdir):
+            covermatch = re.compile(r'(\d+)\-(\d+)\.jpg')
+            for fil in os.listdir(coverdir):
+                fl = os.path.join(coverdir, fil)
+                m = covermatch.match(fil)
+                if m and os.path.isfile(fl):
+                    self._covers[(int(m.group(1)), int(m.group(2)))] = fl
+
+    def fetch_cover(self, albumid, size):
+        Query._ratelimit() # ratelimit cover fetching too?
+        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)
+            self._covers[(albumid, size)] = to
+        return to
+
+    def get_cover(self, albumid, size):
+        cover = self._covers.get((albumid, size), None)
+        if not cover:
+            cover = self.fetch_cover(albumid, size)
+        return cover
+
+    def get_async(self, albumid, size, cb):
+        cover = self._covers.get((albumid, size), None)
+        if cover:
+            cb(cover)
+        else:
+            # TODO
+            cover = self.fetch_cover(albumid, size)
+            cb(cover)
+
+_cover_cache = CoverCache()
+
+def get_album_cover(albumid, size=200):
+    return _cover_cache.get_cover(albumid, size)
+
+def get_album_cover_async(cb, albumid, size=200):
+    _cover_cache.get_async(albumid, size, cb)
+
+class CustomQuery(Query):
+    def __init__(self, url):
+        Query.__init__(self)
+        self.url = url
+
+    def execute(self):
+        return self._geturl(self.url)
+
+    def __str__(self):
+        return self.url
+
+class GetQuery(Query):
+    queries = {
+        'artist' : {
+            'url' : _GET2+'+'.join(_ARTIST_FIELDS)+'/artist/json/?',
+            'params' : 'artist_id=%d',
+            'constructor' : Artist
+            },
+        'album' : {
+            'url' : _GET2+'+'.join(_ALBUM_FIELDS)+'/album/json/?',
+            'params' : 'album_id=%d',
+            'constructor' : Album
+            },
+        'albums' : {
+            'url' : _GET2+'+'.join(_ALBUM_FIELDS)+'/album/json/?',
+            'params' : 'artist_id=%d',
+            'constructor' : [Album]
+            },
+        'track' : {
+            'url' : _GET2+'+'.join(_TRACK_FIELDS)+'/track/json/track_album+album_artist?',
+            'params' : 'id=%d',
+            'constructor' : Track
+            },
+        'tracks' : {
+            'url' : _GET2+'+'.join(_TRACK_FIELDS)+'/track/json/track_album+album_artist?',
+            'params' : 'album_id=%d',
+            'constructor' : [Track]
+            },
+        'radio' : {
+            'url' : _GET2+'+'.join(_TRACK_FIELDS)+'/track/json/radio_track_inradioplaylist+track_album+album_artist/?',
+            'params' : 'order=numradio_asc&radio_id=%d',
+            'constructor' : [Track]
+            },
+        }
+#http://api.jamendo.com/get2/id+name+image+artist_name+album_name+album_id+numalbum+duration/track/json/radio_track_inradioplaylist+track_album+album_artist/?order=numradio_asc&radio_id=283
+
+    def __init__(self, what, ID):
+        Query.__init__(self)
+        self.ID = ID
+        info = GetQuery.queries[what]
+        self.url = info['url']
+        self.params = info['params']
+        self.constructor = info['constructor']
+
+    def construct(self, data):
+        constructor = self.constructor
+        if isinstance(constructor, list):
+            constructor = constructor[0]
+        if isinstance(data, list):
+            return [constructor(int(x['id']), json=x) for x in data]
+        else:
+            return constructor(int(data['id']), json=data)
+
+    def execute(self):
+        js = self._geturl(self.url + self.params % (self.ID))
+        if not js:
+            return None
+        return self.construct(js)
+
+    def __str__(self):
+        return self.url + self.params % (self.ID)
+
+class SearchQuery(GetQuery):
+    def __init__(self, what, query=None, order=None, count=10):
+        GetQuery.__init__(self, what, None)
+        self.query = query
+        self.order = order
+        self.count = count
+
+    def execute(self):
+        params = {}
+        if self.query:
+            params['searchquery'] = self.query
+        if self.order:
+            params['order'] = self.order
+        if self.count:
+            params['n'] = self.count
+        js = self._geturl(self.url +  urllib.urlencode(params))
+        if not js:
+            return None
+        return self.construct(js)
+
+    def __str__(self):
+        params = {'searchquery':self.query, 'order':self.order, 'n':self.count}
+        return self.url +  urllib.urlencode(params)
+
+class JamendoAPIException(Exception):
+    def __init__(self, url):
+        Exception.__init__(url)
+
+def _update_cache(cache, new_items):
+    if not isinstance(new_items, list):
+        new_items = [new_items]
+    for item in new_items:
+        old = cache.get(item.ID)
+        if old:
+            old._set_from(item)
+        else:
+            cache[item.ID] = item
+
+def get_artist(artist_id):
+    """Returns: Artist"""
+    a = _artists.get(artist_id, None)
+    if not a:
+        q = GetQuery('artist', artist_id)
+        a = q.execute()
+        if not a:
+            raise JamendoAPIException(str(q))
+        _update_cache(_artists, a)
+    return a
+
+def get_albums(artist_id):
+    """Returns: [Album]"""
+    q = GetQuery('albums', artist_id)
+    a = q.execute()
+    if not a:
+        raise JamendoAPIException(str(q))
+    _update_cache(_artists, a)
+    return a
+
+def get_album(album_id):
+    """Returns: Album"""
+    a = _albums.get(album_id, None)
+    if not a:
+        q = GetQuery('album', album_id)
+        a = q.execute()
+        if not a:
+            raise JamendoAPIException(str(q))
+        _update_cache(_albums, a)
+    return a
+
+def get_tracks(album_id):
+    """Returns: [Track]"""
+    q = GetQuery('tracks', album_id)
+    a = q.execute()
+    if not a:
+        raise JamendoAPIException(str(q))
+    _update_cache(_tracks, a)
+    return a
+
+def get_track(track_id):
+    """Returns: Track"""
+    a = _tracks.get(track_id, None)
+    if not a:
+        q = GetQuery('track', track_id)
+        a = q.execute()
+        if not a:
+            raise JamendoAPIException(str(q))
+        _update_cache(_tracks, a)
+    return a
+
+def get_radio_tracks(radio_id):
+    """Returns: [Track]"""
+    q = GetQuery('radio', radio_id)
+    a = q.execute()
+    if not a:
+        raise JamendoAPIException(str(q))
+    _update_cache(_tracks, a)
+    return a
+
+def search_artists(query):
+    """Returns: [Artist]"""
+    q = SearchQuery('artist', query, 'searchweight_desc')
+    a = q.execute()
+    if not a:
+        raise JamendoAPIException(str(q))
+    _update_cache(_artists, a)
+    return a
+
+def search_albums(query):
+    """Returns: [Album]"""
+    q = SearchQuery('album', query, 'searchweight_desc')
+    a = q.execute()
+    if not a:
+        raise JamendoAPIException(str(q))
+    _update_cache(_albums, a)
+    return a
+
+def search_tracks(query):
+    """Returns: [Track]"""
+    q = SearchQuery('track', query=query, order='searchweight_desc')
+    a = q.execute()
+    if not a:
+        raise JamendoAPIException(str(q))
+    _update_cache(_tracks, a)
+    return a
+
+def albums_of_the_week():
+    """Returns: [Album]"""
+    q = SearchQuery('album', order='ratingweek_desc')
+    a = q.execute()
+    if not a:
+        raise JamendoAPIException(str(q))
+    _update_cache(_albums, a)
+    return a
+
+def new_releases():
+    """Returns: [Track] (playlist)"""
+    q = SearchQuery('track', order='releasedate_desc')
+    a = q.execute()
+    if not a:
+        raise JamendoAPIException(str(q))
+    _update_cache(_tracks, a)
+    return a
+
+def tracks_of_the_week():
+    """Returns: [Track] (playlist)"""
+    q = SearchQuery('track', order='ratingweek_desc')
+    a = q.execute()
+    if not a:
+        raise JamendoAPIException(str(q))
+    _update_cache(_tracks, a)
+    return a
+
+def get_radio(radio_id):
+    """Returns: Radio"""
+    q = CustomQuery(_GET2+"id+name+idstr+image/radio/json?id=%d"%(radio_id))
+    js = q.execute()
+    if not js:
+        raise JamendoAPIException(str(q))
+    if isinstance(js, list):
+        return [Radio(x['id'], json=x) for x in js]
+    else:
+        return Radio(radio_id, json=js)
+
+def starred_radios():
+    """Returns: [Radio]"""
+    q = CustomQuery(_GET2+"id+name+idstr+image/radio/json?order=starred_desc")
+    js = q.execute()
+    if not js:
+        raise JamendoAPIException(str(q))
+    return [Radio(int(radio['id']), json=radio) for radio in js]
+
+### Set loader functions for classes
+
+def _artist_loader(self):
+    if self._needs_load():
+        artist = get_artist(self.ID)
+        self._set_from(artist)
+Artist.load = _artist_loader
+
+def _album_loader(self):
+    if self._needs_load():
+        album = get_album(self.ID)
+        self._set_from(album)
+Album.load = _album_loader
+
+def _track_loader(self):
+    track = get_track(self.ID)
+    self._set_from(track)
+Track.load = _track_loader
+
+def _radio_loader(self):
+    radio = get_radio(self.ID)
+    self._set_from(radio)
+Radio.load = _radio_loader
index f2ae37b..d67f9fc 100644 (file)
@@ -316,12 +316,14 @@ class Playlist(object):
             if isinstance(data, dict):
                 self.id = data['id']
                 self.name = data['name']
+                self.image = data['image']
                 self.numalbum = int(data['numalbum'])
                 self.url = data['mp3']
                 self.type = 'mp3'
             elif isinstance(data, basestring): # assume URI
                 self.id = 0
                 self.name = ''
+                self.image = None
                 self.numalbum = 0
                 self.url = data
                 self.type = 'mp3'
@@ -332,19 +334,30 @@ class Playlist(object):
         if items is None:
             items = []
         self.items = [Playlist.Entry(item) for item in items]
-        self.current = -1
+        self._current = -1
 
     def add(self, item):
         self.items.append(Playlist.Entry(item))
 
     def next(self):
         if self.has_next():
-            self.current = self.current + 1
-            return self.items[self.current]
+            self._current = self._current + 1
+            return self.items[self._current]
         return None
 
     def has_next(self):
-        return self.current < (len(self.items)-1)
+        return self._current < (len(self.items)-1)
+
+    def current(self):
+        if self._current >= 0:
+            return self.items[self._current]
+        return None
+
+    def current_index(self):
+        return self._current
+
+    def __len__(self):
+        return len(self.items)
 
 class Player(Playlist):
     def __init__(self):
diff --git a/jamaui/refresh.py b/jamaui/refresh.py
new file mode 100644 (file)
index 0000000..2d08955
--- /dev/null
@@ -0,0 +1,67 @@
+class RefreshDialog(object):
+    def __init__(self):
+        self.notebook = gtk.Notebook()
+        info = gtk.VBox()
+        info.pack_start(gtk.Label("Downloading complete DB from jamendo.com."), True, False)
+        info.pack_start(gtk.Label("This will download approximately 8 MB."), True, False)
+        self.force = hildon.GtkToggleButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
+        self.force.set_label("Force refresh")
+
+        info.pack_start(self.force, True, False)
+        self.notebook.append_page(info)
+
+        pcont = gtk.VBox()
+        self.progress = gtk.ProgressBar()
+        pcont.pack_start(self.progress, True, False)
+        self.notebook.append_page(pcont,
+                                  gtk.Label("Updating Database"))
+        self.progress.set_fraction(0)
+        self.progress.set_orientation(gtk.PROGRESS_LEFT_TO_RIGHT)
+        self.progress.set_text("Downloading...")
+
+        self.notebook.append_page(gtk.Label("Database refreshed."))
+
+        self.dialog = hildon.WizardDialog(None, "Refresh", self.notebook)
+        self.notebook.connect("switch-page", self.on_switch)
+        self.dialog.set_forward_page_func(self.forward_func)
+
+        self.refresher = None
+
+    def on_complete(self, status):
+        hildon.hildon_gtk_window_set_progress_indicator(self.dialog, 0)
+        if status:
+            self.progress.set_fraction(1)
+            self.progress.set_text("DB up to date.")
+        else:
+            self.progress.set_fraction(0)
+            self.progress.set_text("Download failed.")
+
+    def on_progress(self, percent):
+        if percent < 100:
+            self.progress.set_text("Downloading...")
+        self.progress.set_fraction(percent/100.0)
+
+    def on_switch(self, notebook, page, num):
+        if num == 1:
+            hildon.hildon_gtk_window_set_progress_indicator(self.dialog, 1)
+            refresh_dump(self.on_complete, self.on_progress, force=self.force.get_active())
+        elif self.refresher:
+            # cancel download
+            pass
+        return True
+
+    def forward_func(self, notebook, current, userdata):
+        #page = notebook.get_nth_page(current)
+        if current == 0:
+            return True
+        else:
+            return False
+
+    def show_all(self):
+        self.dialog.show_all()
+
+    def run(self):
+        self.dialog.run()
+
+    def hide(self):
+        self.dialog.hide()
index efbd2b2..dca1faf 100644 (file)
@@ -13,6 +13,7 @@ import gobject
 # we don't use the local DB...
 from jamaendo.api import LocalDB, Query, Queries, refresh_dump
 from jamaui.player import Player, Playlist
+from util import jsonprint
 
 import ossohelper
 
@@ -34,74 +35,6 @@ from dbus.mainloop.glib import DBusGMainLoop
 
 DBusGMainLoop(set_as_default=True)
 
-class RefreshDialog(object):
-    def __init__(self):
-        self.notebook = gtk.Notebook()
-        info = gtk.VBox()
-        info.pack_start(gtk.Label("Downloading complete DB from jamendo.com."), True, False)
-        info.pack_start(gtk.Label("This will download approximately 8 MB."), True, False)
-        self.force = hildon.GtkToggleButton(gtk.HILDON_SIZE_FINGER_HEIGHT)
-        self.force.set_label("Force refresh")
-
-        info.pack_start(self.force, True, False)
-        self.notebook.append_page(info)
-
-        pcont = gtk.VBox()
-        self.progress = gtk.ProgressBar()
-        pcont.pack_start(self.progress, True, False)
-        self.notebook.append_page(pcont,
-                                  gtk.Label("Updating Database"))
-        self.progress.set_fraction(0)
-        self.progress.set_orientation(gtk.PROGRESS_LEFT_TO_RIGHT)
-        self.progress.set_text("Downloading...")
-
-        self.notebook.append_page(gtk.Label("Database refreshed."))
-
-        self.dialog = hildon.WizardDialog(None, "Refresh", self.notebook)
-        self.notebook.connect("switch-page", self.on_switch)
-        self.dialog.set_forward_page_func(self.forward_func)
-
-        self.refresher = None
-
-    def on_complete(self, status):
-        hildon.hildon_gtk_window_set_progress_indicator(self.dialog, 0)
-        if status:
-            self.progress.set_fraction(1)
-            self.progress.set_text("DB up to date.")
-        else:
-            self.progress.set_fraction(0)
-            self.progress.set_text("Download failed.")
-
-    def on_progress(self, percent):
-        if percent < 100:
-            self.progress.set_text("Downloading...")
-        self.progress.set_fraction(percent/100.0)
-
-    def on_switch(self, notebook, page, num):
-        if num == 1:
-            hildon.hildon_gtk_window_set_progress_indicator(self.dialog, 1)
-            refresh_dump(self.on_complete, self.on_progress, force=self.force.get_active())
-        elif self.refresher:
-            # cancel download
-            pass
-        return True
-
-    def forward_func(self, notebook, current, userdata):
-        #page = notebook.get_nth_page(current)
-        if current == 0:
-            return True
-        else:
-            return False
-
-    def show_all(self):
-        self.dialog.show_all()
-
-    def run(self):
-        self.dialog.run()
-
-    def hide(self):
-        self.dialog.hide()
-
 class PlayerWindow(hildon.StackableWindow):
     def __init__(self, playlist=None):
         hildon.StackableWindow.__init__(self)
@@ -109,29 +42,28 @@ class PlayerWindow(hildon.StackableWindow):
 
         self.playlist = Playlist(playlist)
         self.player = Player()
-        #self.player.play(playlist)
 
         vbox = gtk.VBox()
 
         hbox = gtk.HBox()
 
-        cover = gtk.Image()
+        self.cover = gtk.Image()
 
         vbox2 = gtk.VBox()
 
-        playlist_pos = gtk.Label("0/0 songs")
-        track = gtk.Label("Track name")
-        progress = hildon.GtkHScale()
-        artist = gtk.Label("Artist")
-        album = gtk.Label("Album")
+        self.playlist_pos = gtk.Label("0/0 songs")
+        self.track = gtk.Label("Track name")
+        self.progress = hildon.GtkHScale()
+        self.artist = gtk.Label("Artist")
+        self.album = gtk.Label("Album")
 
-        vbox2.pack_start(playlist_pos, False)
-        vbox2.pack_start(track, False)
-        vbox2.pack_start(progress, True, True)
-        vbox2.pack_start(artist, False)
-        vbox2.pack_start(album, False)
+        vbox2.pack_start(self.playlist_pos, False)
+        vbox2.pack_start(self.track, False)
+        vbox2.pack_start(self.progress, True, True)
+        vbox2.pack_start(self.artist, False)
+        vbox2.pack_start(self.album, False)
 
-        hbox.pack_start(cover, True, True, 0)
+        hbox.pack_start(self.cover, True, True, 0)
         hbox.pack_start(vbox2, True, True, 0)
 
         vbox.pack_start(hbox, True, True, 0)
@@ -156,14 +88,30 @@ class PlayerWindow(hildon.StackableWindow):
         btn.connect('clicked', cb)
         btns.add(btn)
 
+    def update_state(self):
+        item = self.playlist.current()
+        if item:
+            self.track.set_text(item.name)
+            self.playlist_pos.set_text("%d/%d songs",
+                                       self.playlist.current_index(),
+                                       len(self.playlist))
+            self.artist.set_text("Unknown")
+            self.album.set_text("Unknown")
+            if item.image:
+                pass
+                #self.cover = self.get_cover(item.image)
+
     def on_play(self, button):
         self.player.play(self.playlist)
+        self.update_state()
     def on_pause(self, button):
         self.player.pause()
     def on_prev(self, button):
         self.player.prev()
+        self.update_state()
     def on_next(self, button):
         self.player.next()
+        self.update_state()
     def on_stop(self, button):
         self.player.stop()
 
@@ -206,13 +154,9 @@ class SearchWindow(hildon.StackableWindow):
 
     def on_search(self, w):
         txt = self.entry.get_text()
-        print "Search for: %s" % (txt)
-        #db = LocalDB()
-        #db.connect()
         for album in Queries.search_albums(query=txt):
             title = "%s - %s" % (album['artist_name'], album['name'])
             self.idmap[title] = album
-            print "Found %s" % (album)
             self.results.append_text(title)
 
     def selection_changed(self, results, userdata):
@@ -223,13 +167,32 @@ class SearchWindow(hildon.StackableWindow):
 
         album = self.idmap[current_selection]
         selected = [int(album['id'])]
-        print "Selected: %s" % (selected)
         tracks = Queries.album_tracks(selected)
         if tracks:
-            print "Playing: %s" % (tracks)
+            jsonprint(tracks)
             self.pwnd = PlayerWindow(tracks)
             self.pwnd.show_all()
 
+class RadiosWindow(hildon.StackableWindow):
+    def __init__(self):
+        hildon.StackableWindow.__init__(self)
+        self.set_title("Radios")
+
+        label = gtk.Label("Radios")
+        vbox = gtk.VBox(False, 0)
+        vbox.pack_start(label, True, True, 0)
+        self.add(vbox)
+
+class FeaturedWindow(hildon.StackableWindow):
+    def __init__(self):
+        hildon.StackableWindow.__init__(self)
+        self.set_title("Featured")
+
+        label = gtk.Label("featured")
+        vbox = gtk.VBox(False, 0)
+        vbox.pack_start(label, True, True, 0)
+        self.add(vbox)
+
 class PlaylistsWindow(hildon.StackableWindow):
     def __init__(self):
         hildon.StackableWindow.__init__(self)
@@ -279,6 +242,16 @@ class Jamaui(object):
         player.connect("clicked", self.on_player)
         self.menu.append(player)
 
+        player = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
+        player.set_label("Favorites")
+        player.connect("clicked", self.on_favorites)
+        self.menu.append(player)
+
+        player = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
+        player.set_label("Playlists")
+        player.connect("clicked", self.on_playlists)
+        self.menu.append(player)
+
         # Don't use localdb ATM
         #refresh = hildon.GtkButton(gtk.HILDON_SIZE_AUTO)
         #refresh.set_label("Refresh")
@@ -316,9 +289,9 @@ class Jamaui(object):
         self.bbox = bbox
         self.window.add(alignment)
 
+        self.add_mainscreen_button("Featured", "Most listened to", self.on_featured)
+        self.add_mainscreen_button("Radios", "The best in free music", self.on_radios)
         self.add_mainscreen_button("Search", "Search for artists/albums", self.on_search)
-        self.add_mainscreen_button("Playlists", "Browse playlists", self.on_playlists)
-        self.add_mainscreen_button("Favorites", "Your favorite albums", self.on_favorites)
 
         self.window.show_all()
 
@@ -337,9 +310,10 @@ class Jamaui(object):
         dialog = gtk.AboutDialog()
         dialog.set_website("http://github.com/krig")
         dialog.set_website_label("http://github.com/krig")
-        dialog.set_name("Jamaendo")
         dialog.set_authors(("Kristoffer Gronlund (Purple Scout AB)",))
-        dialog.set_comments("Media player for jamendo.com")
+        dialog.set_comments("""Jamaendo plays music from the music catalog of JAMENDO.
+
+JAMENDO is an online platform that distributes musical works under Creative Commons licenses.""")
         dialog.set_version('')
         dialog.run()
         dialog.destroy()
@@ -349,11 +323,19 @@ class Jamaui(object):
         webbrowser.open_new(url)
 
 
-    def on_refresh(self, button):
-        dialog = RefreshDialog()
-        dialog.show_all()
-        dialog.run()
-        dialog.hide()
+    #def on_refresh(self, button):
+    #    dialog = RefreshDialog()
+    #    dialog.show_all()
+    #    dialog.run()
+    #    dialog.hide()
+
+    def on_featured(self, button):
+        self.featuredwnd = FeaturedWindow()
+        self.featuredwnd.show_all()
+
+    def on_radios(self, button):
+        self.radiownd = RadioWindow()
+        self.radiownd.show_all()
 
     def on_search(self, button):
         self.searchwnd = SearchWindow()
index 6bff67f..09904d7 100644 (file)
@@ -1,4 +1,5 @@
 import os
+import simplejson
 
 def string_in_file( filepath, string ):
     try:
@@ -19,3 +20,6 @@ def get_platform():
         return 'linux'
 
 platform = get_platform()
+
+def jsonprint(x):
+    print simplejson.dumps(x, sort_keys=True, indent=4)
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644 (file)
index 0000000..e69de29
diff --git a/tests/testicle b/tests/testicle
new file mode 100755 (executable)
index 0000000..304caaa
--- /dev/null
@@ -0,0 +1,103 @@
+#!/usr/bin/env python
+# runs tons of test queries against the API,
+# sleeping between each and printing the result
+
+# debugging hack - add . to path
+import os, sys
+local_module_dir = os.path.join(os.path.dirname(sys.argv[0]), '..')
+if os.path.isdir(local_module_dir):
+    sys.path.append(os.path.abspath(local_module_dir))
+
+import time
+
+import jamaendo.api2 as api2
+
+class Tests(object):
+    def XXXtestSearchArtists(self):
+        result = api2.search_artists('porn')
+        print "Result:", result
+        print "Cache:", api2._artists
+
+    def XXXtestSearchAlbums(self):
+        result = api2.search_albums('porn')
+        print "Result:", result
+        print "Cache:", api2._albums
+
+    def XXXtestSearchTracks(self):
+        result = api2.search_tracks('porn')
+        print "Result:", result
+        print "Cache:", api2._tracks
+
+    def XXXtestAlbumsOfTheWeek(self):
+        result = api2.albums_of_the_week()
+        print "Result:", result
+        print "Cache:", api2._albums
+
+    def XXXtestNewReleases(self):
+        result = api2.new_releases()
+        print "Result:", result
+        print "Cache:", api2._tracks
+
+    def XXXtestTracksOfTheWeek(self):
+        result = api2.tracks_of_the_week()
+        print "Result:", result
+        print "Cache:", api2._tracks
+
+    def XXXtestStarredRadios(self):
+        result = api2.starred_radios()
+        print "Result:", result
+
+    def XXXtestGetRadio283(self):
+        result = api2.get_radio(283)
+        print "Result:", result
+
+    def XXXtestGetArtist91(self):
+        result = api2.get_artist(91)
+        print "Result:", result
+
+    def XXXtestGetAlbum27865(self):
+        result = api2.get_album(27865)
+        print "Result:", result
+
+    def XXXtestGetTrack353341(self):
+        result = api2.get_track(353341)
+        print "Result:", result
+
+    def XXXtestGetTracks27865(self):
+        result = api2.get_tracks(27865)
+        print "Result:", result
+
+    def XXXtestGetAlbums91(self):
+        result = api2.get_albums(91)
+        print "Result:", result
+
+    def testGetAlbumCover27865(self):
+        result = api2.get_album_cover(27865)
+        print "Result:", result
+
+    def testGetAlbumCoverAsync27865(self):
+        self.got_cover = False
+        def gotit(cover):
+            print "Got:", cover
+            self.got_cover = True
+        api2.get_album_cover_async(gotit, 27865)
+        while not self.got_cover:
+            print "Waiting for cover..."
+            time.sleep(4)
+
+import traceback
+
+def main():
+    for name in Tests.__dict__.keys():
+        if name.startswith('test'):
+            print "Running %s" % (name)
+            try:
+                t = Tests()
+                getattr(t, name)()
+            except Exception, e:
+                traceback.print_exc()
+            print "Waiting..."
+            time.sleep(10)
+
+if __name__=="__main__":
+    main()