Download links
[jamaendo] / jamaendo / api.py
index 86058ce..74c96f7 100644 (file)
@@ -1,18 +1,41 @@
-# 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
@@ -35,7 +58,7 @@ def set_cache_dir(cachedir):
 # makes a query internally to get the full story
 
 _ARTIST_FIELDS = ['id', 'name', 'image']
-_ALBUM_FIELDS = ['id', 'name', 'image', 'artist_name', 'artist_id']
+_ALBUM_FIELDS = ['id', 'name', 'image', 'artist_name', 'artist_id', 'license_url']
 _TRACK_FIELDS = ['id', 'name', 'image', 'artist_name', 'album_name', 'album_id', 'numalbum', 'duration']
 _RADIO_FIELDS = ['id', 'name', 'idstr', 'image']
 
@@ -109,15 +132,20 @@ class Album(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):
@@ -175,7 +203,7 @@ _CACHED_RADIOS = 10
 # 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
@@ -202,6 +230,48 @@ class Query(object):
     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):
+        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
+
+    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)
+                cb(albumid, size, cover)
+                if multi:
+                    time.sleep(1.0)
+
 class CoverCache(object):
     """
     cache and fetch covers
@@ -219,9 +289,10 @@ class CoverCache(object):
                 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):
@@ -239,18 +310,16 @@ class CoverCache(object):
     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):
@@ -362,7 +431,7 @@ class SearchQuery(GetQuery):
 
 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):