-# An improved, structured jamendo API for the N900 with cacheing
+#!/usr/bin/env python
+#
+# This file is part of Jamaendo.
+# Copyright (c) 2010, Kristoffer Gronlund
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+# * Neither the name of Jamaendo nor the
+# names of its contributors may be used to endorse or promote products
+# derived from this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+# DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY
+# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
+# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+# An improved, structured jamendo API wrapper 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')
+
+_CACHEDIR = None
+_COVERDIR = None
_GET2 = '''http://api.jamendo.com/get2/'''
_MP3URL = _GET2+'stream/track/redirect/?id=%d&streamencoding=mp31'
_OGGURL = _GET2+'stream/track/redirect/?id=%d&streamencoding=ogg2'
-
+_TORRENTURL = _GET2+'bittorrent/file/redirect/?album_id=%d&type=archive&class=mp32'
def set_cache_dir(cachedir):
global _CACHEDIR
# 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']
+_ALBUM_FIELDS = ['id', 'name', 'image', 'artist_name', 'artist_id', 'license_url']
+_TRACK_FIELDS = ['id', 'name', 'image', 'artist_id', 'artist_name', 'album_name', 'album_id', 'numalbum', 'duration']
_RADIO_FIELDS = ['id', 'name', 'idstr', 'image']
class LazyQuery(object):
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()
+ #import traceback
+ #traceback.print_exc()
return u"%s(?)"%(self.__class__.__name__)
class Artist(LazyQuery):
self.image = None
self.artist_name = None
self.artist_id = None
+ self.license_url = None
self.tracks = None # None means not downloaded
if json:
self.set_from_json(json)
+ def torrent_url(self):
+ return _TORRENTURL%(self.ID)
+
+
def _needs_load(self):
- return self._needs_load_impl('name', 'image', 'artist_name', 'artist_id', 'tracks')
+ return self._needs_load_impl('name', 'image', 'artist_name', 'artist_id', 'license_url', 'tracks')
def _set_from(self, other):
- return self._set_from_impl(other, 'name', 'image', 'artist_name', 'artist_id', 'tracks')
+ return self._set_from_impl(other, 'name', 'image', 'artist_name', 'artist_id', 'license_url', 'tracks')
class Track(LazyQuery):
def __init__(self, ID, json=None):
self.ID = int(ID)
self.name = None
self.image = None
+ self.artist_id = None
self.artist_name = None
self.album_name = None
self.album_id = None
return _OGGURL%(self.ID)
def _needs_load(self):
- return self._needs_load_impl('name', 'artist_name', 'album_name', 'album_id', 'numalbum', 'duration')
+ return self._needs_load_impl('name', 'artist_name', 'artist_id', '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')
+ return self._set_from_impl(other, 'name', 'image', 'artist_name', 'artist_id', 'album_name', 'album_id', 'numalbum', 'duration')
class Radio(LazyQuery):
def __init__(self, ID, json=None):
# TODO: cache queries?
class Query(object):
- rate_limit = 1.0 # max queries per second
+ rate_limit = 1.1 # seconds between queries
last_query = time.time() - 1.5
@classmethod
def _geturl(self, url):
print "*** %s" % (url)
Query._ratelimit()
- f = urllib.urlopen(url)
- ret = simplejson.load(f)
- f.close()
+ try:
+ f = urllib.urlopen(url)
+ ret = simplejson.load(f)
+ f.close()
+ except Exception, e:
+ return None
return ret
def __str__(self):
def execute(self):
raise NotImplemented
+import threading
+
+class CoverFetcher(threading.Thread):
+ def __init__(self):
+ threading.Thread.__init__(self)
+ self.setDaemon(True)
+ self.cond = threading.Condition()
+ self.work = []
+
+ def _fetch_cover(self, albumid, size):
+ try:
+ coverdir = _COVERDIR if _COVERDIR else '/tmp'
+ to = os.path.join(coverdir, '%d-%d.jpg'%(albumid, size))
+ if not os.path.isfile(to):
+ url = _GET2+'image/album/redirect/?id=%d&imagesize=%d'%(albumid, size)
+ urllib.urlretrieve(url, to)
+ return to
+ except Exception, e:
+ return None
+
+ def request_cover(self, albumid, size, cb):
+ self.cond.acquire()
+ self.work.insert(0, (albumid, size, cb))
+ self.cond.notify()
+ self.cond.release()
+
+ def run(self):
+ while True:
+ work = []
+ self.cond.acquire()
+ while True:
+ work = self.work
+ if work:
+ self.work = []
+ break
+ self.cond.wait()
+ self.cond.release()
+
+ multi = len(work) > 1
+ for albumid, size, cb in work:
+ cover = self._fetch_cover(albumid, size)
+ if cover:
+ cb(albumid, size, cover)
+ if multi:
+ time.sleep(1.0)
+
class CoverCache(object):
"""
cache and fetch covers
m = covermatch.match(fil)
if m and os.path.isfile(fl):
self._covers[(int(m.group(1)), int(m.group(2)))] = fl
+ self._fetcher = CoverFetcher()
+ self._fetcher.start()
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):
def get_async(self, albumid, size, cb):
cover = self._covers.get((albumid, size), None)
if cover:
- cb(cover)
+ cb(albumid, size, cover)
else:
- # TODO
- cover = self.fetch_cover(albumid, size)
- cb(cover)
+ self._fetcher.request_cover(albumid, size, cb)
_cover_cache = CoverCache()
-def get_album_cover(albumid, size=200):
+def get_album_cover(albumid, size=100):
return _cover_cache.get_cover(albumid, size)
-def get_album_cover_async(cb, albumid, size=200):
+def get_album_cover_async(cb, albumid, size=100):
_cover_cache.get_async(albumid, size, cb)
class CustomQuery(Query):
'params' : 'artist_id=%d',
'constructor' : Artist
},
+ 'artist_list' : {
+ 'url' : _GET2+'+'.join(_ALBUM_FIELDS)+'/artist/json/?',
+ 'params' : 'artist_id=%s',
+ 'constructor' : Album
+ },
'album' : {
'url' : _GET2+'+'.join(_ALBUM_FIELDS)+'/album/json/?',
'params' : 'album_id=%d',
'constructor' : Album
},
+ 'album_list' : {
+ 'url' : _GET2+'+'.join(_ALBUM_FIELDS)+'/album/json/?',
+ 'params' : 'album_id=%s',
+ 'constructor' : Album
+ },
'albums' : {
'url' : _GET2+'+'.join(_ALBUM_FIELDS)+'/album/json/?',
'params' : 'artist_id=%d',
'params' : 'id=%d',
'constructor' : Track
},
+ 'track_list' : {
+ 'url' : _GET2+'+'.join(_TRACK_FIELDS)+'/track/json/track_album+album_artist?',
+ 'params' : 'id=%s',
+ 'constructor' : Track
+ },
'tracks' : {
'url' : _GET2+'+'.join(_TRACK_FIELDS)+'/track/json/track_album+album_artist?',
- 'params' : 'album_id=%d',
+ 'params' : 'order=numalbum_asc&album_id=%d',
'constructor' : [Track]
},
'radio' : {
class JamendoAPIException(Exception):
def __init__(self, url):
- Exception.__init__(url)
+ Exception.__init__(self, url)
def _update_cache(cache, new_items):
if not isinstance(new_items, list):
old._set_from(item)
else:
cache[item.ID] = item
+ if isinstance(item, Artist) and item.albums:
+ for album in item.albums:
+ _update_cache(_albums, album)
+ elif isinstance(item, Album) and item.tracks:
+ for track in item.tracks:
+ _update_cache(_tracks, track)
def get_artist(artist_id):
"""Returns: Artist"""
a = a[0]
return a
-def get_albums(artist_id):
+def get_artists(artist_ids):
+ """Returns: [Artist]"""
+ assert(isinstance(artist_ids, list))
+ found = []
+ lookup = []
+ for artist_id in artist_ids:
+ a = _artists.get(artist_id, None)
+ if not a:
+ lookup.append(artist_id)
+ else:
+ found.append(a)
+ if lookup:
+ q = GetQuery('artist_list', '+'.join(str(x) for x in lookup))
+ a = q.execute()
+ if not a:
+ raise JamendoAPIException(str(q))
+ _update_cache(_artists, a)
+ lookup = a
+ return found + lookup
+
+def get_album_list(album_ids):
"""Returns: [Album]"""
+ assert(isinstance(album_ids, list))
+ found = []
+ lookup = []
+ for album_id in album_ids:
+ a = _albums.get(album_id, None)
+ if not a:
+ lookup.append(album_id)
+ else:
+ found.append(a)
+ if lookup:
+ q = GetQuery('album_list', '+'.join(str(x) for x in lookup))
+ a = q.execute()
+ if not a:
+ raise JamendoAPIException(str(q))
+ _update_cache(_albums, a)
+ lookup = a
+ return found + lookup
+
+def get_albums(artist_id):
+ """Returns: [Album]
+ Parameter can either be an artist_id or a list of album ids.
+ """
+ if isinstance(artist_id, list):
+ return get_album_list(artist_id)
+ a = _artists.get(artist_id, None)
+ if a and a.albums:
+ return a.albums
+
q = GetQuery('albums', artist_id)
a = q.execute()
if not a:
a = a[0]
return a
-def get_tracks(album_id):
+def get_track_list(track_ids):
"""Returns: [Track]"""
+ assert(isinstance(track_ids, list))
+ found = []
+ lookup = []
+ for track_id in track_ids:
+ a = _tracks.get(track_id, None)
+ if not a:
+ lookup.append(track_id)
+ else:
+ found.append(a)
+ if lookup:
+ q = GetQuery('track_list', '+'.join(str(x) for x in lookup))
+ a = q.execute()
+ if not a:
+ raise JamendoAPIException(str(q))
+ _update_cache(_tracks, a)
+ lookup = a
+ return found + lookup
+
+def get_tracks(album_id):
+ """Returns: [Track]
+ Parameter can either be an album_id or a list of track ids.
+ """
+ if isinstance(album_id, list):
+ return get_track_list(album_id)
+ a = _albums.get(album_id, None)
+ if a and a.tracks:
+ return a.tracks
+
q = GetQuery('tracks', album_id)
a = q.execute()
if not a:
def _artist_loader(self):
if self._needs_load():
artist = get_artist(self.ID)
+ artist.albums = get_albums(self.ID)
self._set_from(artist)
Artist.load = _artist_loader