From fb3e7e801a14ed1c53e769750646363c20ce7fce Mon Sep 17 00:00:00 2001 From: =?utf8?q?Kristoffer=20Gr=C3=B6nlund?= Date: Thu, 31 Dec 2009 02:13:42 +0100 Subject: [PATCH] Moved things around and fixed a minor bug --- jamaendo/__init__.py | 8 + jamaendo/api.py | 795 ++++++++++++++++++++++++++++++++------------------ jamaendo/api2.py | 520 --------------------------------- tests/testicle | 14 +- 4 files changed, 532 insertions(+), 805 deletions(-) delete mode 100644 jamaendo/api2.py diff --git a/jamaendo/__init__.py b/jamaendo/__init__.py index e69de29..0d14861 100644 --- a/jamaendo/__init__.py +++ b/jamaendo/__init__.py @@ -0,0 +1,8 @@ +from api import set_cache_dir, \ + Artist, Album, Track, Radio, \ + get_album_cover, get_album_cover_async, \ + JamendoAPIException, \ + get_artist, get_albums, get_album, get_tracks, \ + get_track, get_radio_tracks, search_artists, search_albums, \ + search_tracks, albums_of_the_week, new_releases, \ + tracks_of_the_week, get_radio, starred_radios, favorite_albums diff --git a/jamaendo/api.py b/jamaendo/api.py index c3cb3a5..8bd7d91 100644 --- a/jamaendo/api.py +++ b/jamaendo/api.py @@ -1,309 +1,540 @@ +# An improved, structured jamendo API for the N900 with cacheing +# Image / cover downloads.. and more? import urllib, threading, os, gzip, time, simplejson, re -_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: - pass - -def has_dump(): - return os.path.isfile(_DUMP) - -def _file_is_old(fil, old_age): - return os.path.getmtime(fil) < (time.time() - old_age) - -def _dump_is_old(): - return not has_dump() or _file_is_old(_DUMP, 60*60*24) # 1 day - -def refresh_dump(complete_callback, progress_callback=None, force=False): - if force or _dump_is_old(): - downloader = Downloader(complete_callback, progress_callback) - downloader.start() - else: - complete_callback(True) - -class Downloader(threading.Thread): - def __init__(self, complete_callback, progress_callback): - threading.Thread.__init__(self) - self.complete_callback = complete_callback - self.progress_callback = progress_callback - - def actual_callback(self, numblocks, blocksize, filesize): - if self.progress_callback: - try: - percent = min((numblocks*blocksize*100)/filesize, 100) - except: - percent = 100 - self.progress_callback(percent) - - def run(self): - success = True - try: - urllib.urlretrieve(_DUMP_URL, _DUMP_TMP, self.actual_callback) - if os.path.isfile(_DUMP): - os.remove(_DUMP) - os.rename(_DUMP_TMP, _DUMP) - except Exception, e: - success = False - self.complete_callback(success) - -def fast_iter(context, func): - for event, elem in context: - func(elem) - elem.clear() - while elem.getprevious() is not None: - del elem.getparent()[0] - del context - -from lxml import etree - -class Obj(object): - def __repr__(self): - def printable(v): - if isinstance(v, basestring): - return v.encode('utf-8') +#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: - return str(v) - return "{%s}" % (", ".join("%s=%s"%(k.encode('utf-8'), printable(v)) \ - for k,v in self.__dict__.iteritems() if not k.startswith('_'))) - -class LocalDB(object): - def __init__(self): - self.fil = None + if key.endswith('_id'): + value = int(value) + setattr(self, key, value) - def connect(self): - self.fil = gzip.open(_DUMP) + def load(self): + """Not automatic for now, + will have to do artist.load() - def close(self): - self.fil.close() + This is filled in further down + in the file + """ + raise NotImplemented - def make_album_brief(self, element): - ret = {} - for info in element: - if info.tag == 'id': - ret['id'] = int(info.text) - elif info.tag == 'name': - ret['name'] = info.text - return ret + def _needs_load(self): + return True - def make_artist_obj(self, element): - ret = {} - for child in element: - if child.tag == 'id': - ret['id'] = int(child.text) - elif child.tag in ('name', 'image'): - ret[child.tag] = child.text - elif child.tag == 'Albums': - ret['albums'] = [self.make_album_brief(a) for a in child] - return ret + def _set_from(self, other): + raise NotImplemented - def make_track_obj(self, element): - ret = {} - for info in element: - if info.tag == 'id': - _id = int(info.text) - ret['id'] = _id - ret['mp3'] = Query.track_mp3(_id) - ret['ogg'] = Query.track_ogg(_id) - elif info.tag in ('name', 'numalbum'): - ret[info.tag] = info.text - return ret + def _needs_load_impl(self, *attrs): + for attr in attrs: + if getattr(self, attr) is None: + return True + return False - def make_album_obj(self, element): - ret = {} - artist = element.getparent().getparent() - if artist is not None: - for child in artist: - if child.tag == 'name': - ret['artist_name'] = child.text - elif child.tag == 'id': - ret['artist_id'] = int(child.text) - for child in element: - if child.tag == 'id': - ret['id'] = int(child.text) - elif child.tag in ('name', 'image'): - if child.text: - ret[child.tag] = child.text - else: - ret[child.tag] = "" - elif child.tag == 'Tracks': - ret['tracks'] = [self.make_track_obj(t) for t in child] - return ret + def _set_from_impl(self, other, *attrs): + for attr in attrs: + self._set_if(other, attr) - def artist_walker(self, name_match): - for event, element in etree.iterparse(self.fil, tag="artist"): - name = element.xpath('./name')[0].text.lower() - if name and name.find(name_match) > -1: - yield self.make_artist_obj(element) - element.clear() - while element.getprevious() is not None: - del element.getparent()[0] - raise StopIteration - - def album_walker(self, name_match): - for event, element in etree.iterparse(self.fil, tag="album"): - name = element.xpath('./name')[0].text - if name and name.lower().find(name_match) > -1: - yield self.make_album_obj(element) - element.clear() - while element.getprevious() is not None: - del element.getparent()[0] - raise StopIteration - - def artistid_walker(self, artistids): - for event, element in etree.iterparse(self.fil, tag="artist"): - _id = element.xpath('./id')[0].text - if _id and int(_id) in artistids: - yield self.make_artist_obj(element) - element.clear() - while element.getprevious() is not None: - del element.getparent()[0] - raise StopIteration - - def albumid_walker(self, albumids): - for event, element in etree.iterparse(self.fil, tag="album"): - _id = element.xpath('./id')[0].text - if _id and (int(_id) in albumids): - yield self.make_album_obj(element) - element.clear() - while element.getprevious() is not None: - del element.getparent()[0] - raise StopIteration - - def search_artists(self, substr): - substr = substr.lower() - return (artist for artist in self.artist_walker(substr)) - - def search_albums(self, substr): - substr = substr.lower() - return (album for album in self.album_walker(substr)) - - def get_artists(self, artistids): - return (artist for artist in self.artistid_walker(artistids)) - - def get_albums(self, albumids): - return (album for album in self.albumid_walker(albumids)) + 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)) -_GET2 = '''http://api.jamendo.com/get2/''' + 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', 'album_name', 'album_id', 'numalbum', 'duration') + + def _set_from(self, other): + return self._set_from_impl(other, 'name', 'image', 'artist_name', 'album_name', 'album_id', 'numalbum', 'duration') + +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() - caching = True - cache_time = 60*60*24 rate_limit = 1.0 # max queries per second - def __init__(self, - select=['id', 'name', 'image', 'artist_name', 'artist_id'], - request='album', - track=['track_album', 'album_artist']): - if request == 'track': - self.url = "%s%s/%s/json/%s" % (_GET2, '+'.join(select), request, '+'.join(track)) - else: - self.url = "%s%s/%s/json/" % (_GET2, '+'.join(select), request) - - def __call__(self, order=None, count=5, query=None, albumids=None): - return self.emit(order=order, count=count, query=query, albumids=albumids) - - def emit(self, order=None, count=5, query=None, albumids=None): - """ratelimited query""" - self._ratelimit() - paramdict = {'n':count} - if order is not None: - paramdict['order'] = order - if query is not None: - paramdict['searchquery'] = query - if albumids is not None: - paramdict['album_id'] = " ".join(str(_id) for _id in albumids) - params = urllib.urlencode(paramdict) - url = self.url + "?%s" % (params) + @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 _ratelimit(self): - now = time.time() - if now - self.last_query < self.rate_limit: - time.sleep(self.rate_limit - (now - self.last_query)) - self.last_query = now + def __str__(self): + return "#%s" % (self.__class__.__name__) + def execute(self): + raise NotImplemented - @staticmethod - def album_cover(albumid, size=200): - to = '~/.cache/jamaendo/cover-%d-%d.jpg'%(albumid, size) +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 - @staticmethod - def track_ogg(trackid): - return _GET2+ 'stream/track/redirect/?id=%d&streamencoding=ogg2'%(trackid) - - @staticmethod - 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(): - return Query().emit(order='ratingweek_desc') - @staticmethod - def albums_all_time(): - return Query().emit(order='ratingtotal_desc') - @staticmethod - def albums_this_month(): - return Query().emit(order='ratingmonth_desc') - @staticmethod - def albums_today(): - return Query().emit(order='ratingday_desc') - @staticmethod - def playlists_all_time(): - q = Query(select=['id','name', 'user_idstr'], request='playlist') - return q.emit(order='ratingtotal_desc') - - @staticmethod - def tracks_this_month(): - q = Query(select=['id', 'name', - 'stream', - 'album_name', 'artist_name', - 'album_id', 'artist_id'], - request='track') - return q.emit(order='ratingmonth_desc') - - @staticmethod - def search_albums(query): - q = Query() - return q.emit(order='searchweight_desc', query=query) - - @staticmethod - def search_artists(query): - q = Query(request='artist', select=['id', 'name', 'image']) - return q.emit(order='searchweight_desc', query=query) - - @staticmethod - 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') - ret = q.emit(albumids=albumids, count=100) - for track in ret: - track['mp3'] = Query.track_mp3(int(track['id'])) - track['ogg'] = Query.track_ogg(int(track['id'])) - return ret + 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] + }, + 'favorite_albums' : { + 'url' : _GET2+'+'.join(_ALBUM_FIELDS)+'/album/json/album_user_starred/?', + 'params' : 'user_idstr=%s', + 'constructor' : [Album] + }, + #http://api.jamendo.com/get2/id+name+url+image+artist_name/album/jsonpretty/album_user_starred/?user_idstr=sylvinus&n=all + #q = SearchQuery('album', user_idstr=user) + + } +#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, user=None, count=10): + GetQuery.__init__(self, what, None) + self.query = query + self.order = order + self.count = count + self.user = user + + 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 + if self.user: + params['user_idstr'] = self.user + 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] + +def favorite_albums(user): + """Returns: [Album]""" + q = SearchQuery('favorite_albums', user=user, count=20) + a = q.execute() + if not a: + raise JamendoAPIException(str(q)) + _update_cache(_albums, a) + return a + +### 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/jamaendo/api2.py b/jamaendo/api2.py deleted file mode 100644 index 32c8f5d..0000000 --- a/jamaendo/api2.py +++ /dev/null @@ -1,520 +0,0 @@ -# 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/tests/testicle b/tests/testicle index 304caaa..2a62fa5 100755 --- a/tests/testicle +++ b/tests/testicle @@ -10,7 +10,7 @@ if os.path.isdir(local_module_dir): import time -import jamaendo.api2 as api2 +import jamaendo as api2 class Tests(object): def XXXtestSearchArtists(self): @@ -51,6 +51,10 @@ class Tests(object): result = api2.get_radio(283) print "Result:", result + def testGetRadioTracks283(self): + result = api2.get_radio_tracks(283) + print "Result:", result + def XXXtestGetArtist91(self): result = api2.get_artist(91) print "Result:", result @@ -71,11 +75,15 @@ class Tests(object): result = api2.get_albums(91) print "Result:", result - def testGetAlbumCover27865(self): + def XXXtestFavoriteAlbumsKegie(self): + result = api2.favorite_albums('kegie') + print "Result:", result + + def XXXtestGetAlbumCover27865(self): result = api2.get_album_cover(27865) print "Result:", result - def testGetAlbumCoverAsync27865(self): + def XXXtestGetAlbumCoverAsync27865(self): self.got_cover = False def gotit(cover): print "Got:", cover -- 1.7.9.5