From: Kristoffer Grönlund Date: Thu, 31 Dec 2009 00:55:36 +0000 (+0100) Subject: New backend: api2.py X-Git-Url: https://vcs.maemo.org/git/?a=commitdiff_plain;h=a3199dfe2efb02877f3cb0086e407409bdba6b29;p=jamaendo New backend: api2.py --- diff --git a/data/bg.xcf b/data/bg.xcf new file mode 100644 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 index 0000000..f4b72f4 --- /dev/null +++ b/data/jamaendo.desktop @@ -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 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 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 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 index 0000000..4e01b8b Binary files /dev/null and b/data/play.png differ diff --git a/jamaendo/api.py b/jamaendo/api.py index 75335a1..c3cb3a5 100644 --- a/jamaendo/api.py +++ b/jamaendo/api.py @@ -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 index 0000000..32c8f5d --- /dev/null +++ b/jamaendo/api2.py @@ -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 diff --git a/jamaui/player.py b/jamaui/player.py index f2ae37b..d67f9fc 100644 --- a/jamaui/player.py +++ b/jamaui/player.py @@ -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 index 0000000..2d08955 --- /dev/null +++ b/jamaui/refresh.py @@ -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() diff --git a/jamaui/ui.py b/jamaui/ui.py index efbd2b2..dca1faf 100644 --- a/jamaui/ui.py +++ b/jamaui/ui.py @@ -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() diff --git a/jamaui/util.py b/jamaui/util.py index 6bff67f..09904d7 100644 --- a/jamaui/util.py +++ b/jamaui/util.py @@ -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 index 0000000..e69de29 diff --git a/tests/testicle b/tests/testicle new file mode 100755 index 0000000..304caaa --- /dev/null +++ b/tests/testicle @@ -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()