Added 'helldon' to transparently port the thing to the desktop :P
[jamaendo] / jamaendo / api.py
index 0d293bc..7792c33 100644 (file)
@@ -24,6 +24,7 @@
 # Image / cover downloads.. and more?
 import urllib, threading, os, time, simplejson, re
 import logging, hashlib
+import pycurl, StringIO
 
 _CACHEDIR = None
 _COVERDIR = None
@@ -48,8 +49,22 @@ except:
 
 _ARTIST_FIELDS = ['id', 'name', 'image']
 _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']
+_TRACK_FIELDS = ['id', 'name', 'album_image', 'artist_id', 'artist_name', 'album_name', 'album_id', 'numalbum', 'duration']
 _RADIO_FIELDS = ['id', 'name', 'idstr', 'image']
+_TAG_FIELDS = ['id', 'name']
+
+def curlGET(url):
+    c = pycurl.Curl()
+    s = StringIO.StringIO()
+    c.setopt(pycurl.FOLLOWLOCATION, 1)
+    c.setopt(pycurl.URL, url)
+    c.setopt(pycurl.WRITEFUNCTION, s.write)
+    try:
+        c.perform()
+    finally:
+        c.close()
+    s.seek(0)
+    return s.read()
 
 class LazyQuery(object):
     def set_from_json(self, json):
@@ -114,6 +129,9 @@ class Artist(LazyQuery):
     def _set_from(self, other):
         return self._set_from_impl(other, 'name', 'image', 'albums')
 
+    def get_data(self):
+        return {'name':self.name, 'image':self.image}
+
 class Album(LazyQuery):
     def __init__(self, ID, json=None):
         self.ID = int(ID)
@@ -136,13 +154,19 @@ class Album(LazyQuery):
     def _set_from(self, other):
         return self._set_from_impl(other, 'name', 'image', 'artist_name', 'artist_id', 'license_url', 'tracks')
 
+    def get_data(self):
+        return {'name':self.name, 'image':self.image,
+                'artist_name':self.artist_name,
+                'artist_id':self.artist_id,
+                'license_url':self.license_url}
+
 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_image = None
         self.album_name = None
         self.album_id = None
         self.numalbum = None
@@ -156,11 +180,21 @@ class Track(LazyQuery):
     def ogg_url(self):
        return _OGGURL%(self.ID)
 
+    def get_data(self):
+        return {'name':self.name,
+                'artist_id':self.artist_id,
+                'artist_name':self.artist_name,
+                'album_image':self.album_image,
+                'album_name':self.album_name,
+                'album_id':self.album_id,
+                'numalbum':self.numalbum,
+                'duration':self.duration}
+
     def _needs_load(self):
         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', 'artist_id', 'album_name', 'album_id', 'numalbum', 'duration')
+        return self._set_from_impl(other, 'name', 'album_image', 'artist_name', 'artist_id', 'album_name', 'album_id', 'numalbum', 'duration')
 
 class Radio(LazyQuery):
     def __init__(self, ID, json=None):
@@ -177,6 +211,18 @@ class Radio(LazyQuery):
     def _set_from(self, other):
         return self._set_from_impl(other, 'name', 'idstr', 'image')
 
+class Tag(LazyQuery):
+    def __init__(self, ID, json=None):
+        self.ID = int(ID)
+        self.name = None
+        if json:
+            self.set_from_json(json)
+
+    def _needs_load(self):
+        return self._needs_load_impl('name')
+
+    def _set_from(self, other):
+        return self._set_from_impl(other, 'name')
 
 _artists = {} # id -> Artist()
 _albums = {} # id -> Album()
@@ -212,9 +258,7 @@ class Query(object):
         log.info("%s", url)
         Query._ratelimit()
         try:
-            f = urllib.urlopen(url)
-            ret = simplejson.load(f)
-            f.close()
+            ret = simplejson.loads(curlGET(url))
         except Exception, e:
             return None
         return ret
@@ -232,13 +276,32 @@ class CoverFetcher(threading.Thread):
         self.cond = threading.Condition()
         self.work = []
 
+    def _retrieve(self, url, fname):
+        BROKEN = 'http://imgjam.com/radios/default/default.100.png'
+        if url == BROKEN:
+            return None
+        f = open(fname, 'wb')
+        c = pycurl.Curl()
+        c.setopt(pycurl.FOLLOWLOCATION, 1)
+        c.setopt(pycurl.URL, str(url))
+        c.setopt(pycurl.WRITEFUNCTION, f.write)
+        try:
+            c.perform()
+        except:
+            fname = None
+        finally:
+            c.close()
+            f.close()
+        log.debug("Coverfetch: %s -> %s", url, fname)
+        return fname
+
     def _fetch_cover(self, albumid, size):
         try:
             coverdir = _COVERDIR if _COVERDIR else '/tmp'
             to = os.path.join(coverdir, '%d-%d.jpg'%(albumid, size))
             if not os.path.isfile(to):
                 url = _GET2+'image/album/redirect/?id=%d&imagesize=%d'%(albumid, size)
-                urllib.urlretrieve(url, to)
+                to = self._retrieve(url, to)
             return to
         except Exception, e:
             return None
@@ -249,7 +312,7 @@ class CoverFetcher(threading.Thread):
             coverdir = _COVERDIR if _COVERDIR else '/tmp'
             to = os.path.join(coverdir, h+'.jpg')
             if not os.path.isfile(to):
-                urllib.urlretrieve(url, to)
+                to = self._retrieve(url, to)
             return to
         except Exception, e:
             return None
@@ -263,7 +326,7 @@ class CoverFetcher(threading.Thread):
     def request_images(self, urls, cb):
         """cb([(url, image)])"""
         self.cond.acquire()
-        self.work.insert(0, ('images', urls, cb))
+        self.work = [('image', url, cb) for url in urls] + self.work
         self.cond.notify()
         self.cond.release()
 
@@ -281,23 +344,20 @@ class CoverFetcher(threading.Thread):
 
             multi = len(work) > 1
             for job in work:
-                if job[0] == 'images':
-                    self.process_images(job[1], job[2])
+                if job[0] == 'image':
+                    self.process_image(job[1], job[2])
                 else:
                     self.process_cover(*job)
-                if multi:
-                    time.sleep(1.0)
 
     def process_cover(self, albumid, size, cb):
-        albumid, size, cb = job
         cover = self._fetch_cover(albumid, size)
         if cover:
             cb(albumid, size, cover)
 
-    def process_images(self, urls, cb):
-        results = [(url, image) for url, image in ((url, self._fetch_image(url)) for url in urls) if image is not None]
-        if results:
-            cb(results)
+    def process_image(self, url, cb):
+        image = self._fetch_image(url)
+        if image:
+            cb([(url, image)])
 
 class CoverCache(object):
     """
@@ -469,7 +529,7 @@ class GetQuery(Query):
             },
         'radio' : {
             'url' : _GET2+'+'.join(_TRACK_FIELDS)+'/track/json/radio_track_inradioplaylist+track_album+album_artist/?',
-            'params' : 'order=random_asc&radio_id=%d',
+            'params' : 'order=random_asc&radio_id=%d&n=16',
             'constructor' : [Track]
             },
         'favorite_albums' : {
@@ -477,6 +537,11 @@ class GetQuery(Query):
             'params' : 'user_idstr=%s',
             'constructor' : [Album]
             },
+        'tag' : {
+            'url' : _GET2+'+'.join(_TRACK_FIELDS)+'/track/json/track_album+album_artist?',
+            'params' : 'tag_id=%d&n=50&order=rating_desc',
+            'constructor' : [Track]
+            },
         }
 
     def __init__(self, what, ID):
@@ -506,7 +571,7 @@ class GetQuery(Query):
         return self.url + self.params % (self.ID)
 
 class SearchQuery(GetQuery):
-    def __init__(self, what, query=None, order=None, user=None, count=10):
+    def __init__(self, what, query=None, order=None, user=None, count=20):
         GetQuery.__init__(self, what, None)
         self.query = query
         self.order = order
@@ -627,7 +692,7 @@ def get_albums(artist_id):
     a = q.execute()
     if not a:
         raise JamendoAPIException(str(q))
-    _update_cache(_artists, a)
+    _update_cache(_albums, a)
     return a
 
 def get_album(album_id):
@@ -702,6 +767,16 @@ def get_radio_tracks(radio_id):
     _update_cache(_tracks, a)
     return a
 
+#http://api.jamendo.com/get2/id+name/track/plain/?tag_id=327&n=50&order=rating_desc
+def get_tag_tracks(tag_id):
+    """Returns: [Track]"""
+    q = GetQuery('tag', tag_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')
@@ -756,6 +831,33 @@ def tracks_of_the_week():
     _update_cache(_tracks, a)
     return a
 
+def top_artists(order='rating_desc', count=20):
+    """Returns: [Artist]"""
+    q = SearchQuery('artist', order=order, count=count)
+    a = q.execute()
+    if not a:
+        raise JamendoAPIException(str(q))
+    _update_cache(_artists, a)
+    return a
+
+def top_albums(order='rating_desc', count=20):
+    """Returns: [Album]"""
+    q = SearchQuery('album', order=order, count=count)
+    a = q.execute()
+    if not a:
+        raise JamendoAPIException(str(q))
+    _update_cache(_albums, a)
+    return a
+
+def top_tracks(order='rating_desc', count=20):
+    """Returns: [Track]"""
+    q = SearchQuery('track', order=order, count=count)
+    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))
@@ -774,6 +876,14 @@ def starred_radios():
         raise JamendoAPIException(str(q))
     return [Radio(int(radio['id']), json=radio) for radio in js]
 
+def top_tags(count=50, order='rating_desc'):
+    """Returns: [Tag]"""
+    q = CustomQuery(_GET2+"id+name/tag/json?n=%d&order=%s"%(count, order))
+    js = q.execute()
+    if not js:
+        raise JamendoAPIException(str(q))
+    return [Tag(int(tag['id']), json=tag) for tag in js]
+
 def favorite_albums(user):
     """Returns: [Album]"""
     q = SearchQuery('favorite_albums', user=user, count=20)