c3cb3a5d8283bd415bec5b260a3fe4416a9fa181
[jamaendo] / jamaendo / api.py
1 import urllib, threading, os, gzip, time, simplejson, re
2 _DUMP_URL = '''http://img.jamendo.com/data/dbdump_artistalbumtrack.xml.gz'''
3 _DUMP = os.path.expanduser('''~/.cache/jamaendo/dbdump.xml.gz''')
4 _DUMP_TMP = os.path.expanduser('''~/.cache/jamaendo/new_dbdump.xml.gz''')
5
6 # radio stream
7 # /get2/stream/track/m3u/radio_track_inradioplaylist/?order=numradio_asc&radio_id=283
8
9
10 try:
11     os.makedirs(os.path.dirname(_DUMP))
12 except OSError:
13     pass
14
15 def has_dump():
16     return os.path.isfile(_DUMP)
17
18 def _file_is_old(fil, old_age):
19     return os.path.getmtime(fil) < (time.time() - old_age)
20
21 def _dump_is_old():
22     return not has_dump() or _file_is_old(_DUMP, 60*60*24) # 1 day
23
24 def refresh_dump(complete_callback, progress_callback=None, force=False):
25     if force or _dump_is_old():
26         downloader = Downloader(complete_callback, progress_callback)
27         downloader.start()
28     else:
29         complete_callback(True)
30
31 class Downloader(threading.Thread):
32     def __init__(self, complete_callback, progress_callback):
33         threading.Thread.__init__(self)
34         self.complete_callback = complete_callback
35         self.progress_callback = progress_callback
36
37     def actual_callback(self, numblocks, blocksize, filesize):
38         if self.progress_callback:
39             try:
40                 percent = min((numblocks*blocksize*100)/filesize, 100)
41             except:
42                 percent = 100
43             self.progress_callback(percent)
44
45     def run(self):
46         success = True
47         try:
48             urllib.urlretrieve(_DUMP_URL, _DUMP_TMP, self.actual_callback)
49             if os.path.isfile(_DUMP):
50                 os.remove(_DUMP)
51             os.rename(_DUMP_TMP, _DUMP)
52         except Exception, e:
53             success = False
54         self.complete_callback(success)
55
56 def fast_iter(context, func):
57     for event, elem in context:
58         func(elem)
59         elem.clear()
60         while elem.getprevious() is not None:
61             del elem.getparent()[0]
62     del context
63
64 from lxml import etree
65
66 class Obj(object):
67     def __repr__(self):
68         def printable(v):
69             if isinstance(v, basestring):
70                 return v.encode('utf-8')
71             else:
72                 return str(v)
73         return "{%s}" % (", ".join("%s=%s"%(k.encode('utf-8'), printable(v)) \
74                              for k,v in self.__dict__.iteritems() if not k.startswith('_')))
75
76 class LocalDB(object):
77     def __init__(self):
78         self.fil = None
79
80     def connect(self):
81         self.fil = gzip.open(_DUMP)
82
83     def close(self):
84         self.fil.close()
85
86     def make_album_brief(self, element):
87         ret = {}
88         for info in element:
89             if info.tag == 'id':
90                 ret['id'] = int(info.text)
91             elif info.tag == 'name':
92                 ret['name'] = info.text
93         return ret
94
95     def make_artist_obj(self, element):
96         ret = {}
97         for child in element:
98             if child.tag == 'id':
99                 ret['id'] = int(child.text)
100             elif child.tag in ('name', 'image'):
101                 ret[child.tag] = child.text
102             elif child.tag == 'Albums':
103                 ret['albums'] = [self.make_album_brief(a) for a in child]
104         return ret
105
106     def make_track_obj(self, element):
107         ret = {}
108         for info in element:
109             if info.tag == 'id':
110                 _id = int(info.text)
111                 ret['id'] = _id
112                 ret['mp3'] = Query.track_mp3(_id)
113                 ret['ogg'] = Query.track_ogg(_id)
114             elif info.tag in ('name', 'numalbum'):
115                 ret[info.tag] = info.text
116         return ret
117
118     def make_album_obj(self, element):
119         ret = {}
120         artist = element.getparent().getparent()
121         if artist is not None:
122             for child in artist:
123                 if child.tag == 'name':
124                     ret['artist_name'] = child.text
125                 elif child.tag == 'id':
126                     ret['artist_id'] = int(child.text)
127         for child in element:
128             if child.tag == 'id':
129                 ret['id'] = int(child.text)
130             elif child.tag in ('name', 'image'):
131                 if child.text:
132                     ret[child.tag] = child.text
133                 else:
134                     ret[child.tag] = ""
135             elif child.tag == 'Tracks':
136                 ret['tracks'] = [self.make_track_obj(t) for t in child]
137         return ret
138
139     def artist_walker(self, name_match):
140         for event, element in etree.iterparse(self.fil, tag="artist"):
141             name = element.xpath('./name')[0].text.lower()
142             if name and name.find(name_match) > -1:
143                 yield self.make_artist_obj(element)
144             element.clear()
145             while element.getprevious() is not None:
146                 del element.getparent()[0]
147         raise StopIteration
148
149     def album_walker(self, name_match):
150         for event, element in etree.iterparse(self.fil, tag="album"):
151             name = element.xpath('./name')[0].text
152             if name and name.lower().find(name_match) > -1:
153                 yield self.make_album_obj(element)
154             element.clear()
155             while element.getprevious() is not None:
156                 del element.getparent()[0]
157         raise StopIteration
158
159     def artistid_walker(self, artistids):
160         for event, element in etree.iterparse(self.fil, tag="artist"):
161             _id = element.xpath('./id')[0].text
162             if _id and int(_id) in artistids:
163                 yield self.make_artist_obj(element)
164             element.clear()
165             while element.getprevious() is not None:
166                 del element.getparent()[0]
167         raise StopIteration
168
169     def albumid_walker(self, albumids):
170         for event, element in etree.iterparse(self.fil, tag="album"):
171             _id = element.xpath('./id')[0].text
172             if _id and (int(_id) in albumids):
173                 yield self.make_album_obj(element)
174             element.clear()
175             while element.getprevious() is not None:
176                 del element.getparent()[0]
177         raise StopIteration
178
179     def search_artists(self, substr):
180         substr = substr.lower()
181         return (artist for artist in self.artist_walker(substr))
182
183     def search_albums(self, substr):
184         substr = substr.lower()
185         return (album for album in self.album_walker(substr))
186
187     def get_artists(self, artistids):
188         return (artist for artist in self.artistid_walker(artistids))
189
190     def get_albums(self, albumids):
191         return (album for album in self.albumid_walker(albumids))
192
193 _GET2 = '''http://api.jamendo.com/get2/'''
194
195 class Query(object):
196     last_query = time.time()
197     caching = True
198     cache_time = 60*60*24
199     rate_limit = 1.0 # max queries per second
200
201     def __init__(self,
202                  select=['id', 'name', 'image', 'artist_name', 'artist_id'],
203                  request='album',
204                  track=['track_album', 'album_artist']):
205         if request == 'track':
206             self.url = "%s%s/%s/json/%s" % (_GET2, '+'.join(select), request, '+'.join(track))
207         else:
208             self.url = "%s%s/%s/json/" % (_GET2, '+'.join(select), request)
209
210     def __call__(self, order=None, count=5, query=None, albumids=None):
211         return self.emit(order=order, count=count, query=query, albumids=albumids)
212
213     def emit(self, order=None, count=5, query=None, albumids=None):
214         """ratelimited query"""
215         self._ratelimit()
216         paramdict = {'n':count}
217         if order is not None:
218             paramdict['order'] = order
219         if query is not None:
220             paramdict['searchquery'] = query
221         if albumids is not None:
222             paramdict['album_id'] = " ".join(str(_id) for _id in albumids)
223         params = urllib.urlencode(paramdict)
224         url = self.url + "?%s" % (params)
225         f = urllib.urlopen(url)
226         ret = simplejson.load(f)
227         f.close()
228         return ret
229
230     def _ratelimit(self):
231         now = time.time()
232         if now - self.last_query < self.rate_limit:
233             time.sleep(self.rate_limit - (now - self.last_query))
234         self.last_query = now
235
236
237     @staticmethod
238     def album_cover(albumid, size=200):
239         to = '~/.cache/jamaendo/cover-%d-%d.jpg'%(albumid, size)
240         if not os.path.isfile(to):
241             url = _GET2+'image/album/redirect/?id=%d&imagesize=%d'%(albumid, size)
242             urllib.urlretrieve(url, to)
243         return to
244
245     @staticmethod
246     def track_ogg(trackid):
247        return _GET2+ 'stream/track/redirect/?id=%d&streamencoding=ogg2'%(trackid)
248
249     @staticmethod
250     def track_mp3(trackid):
251        return _GET2+ 'stream/track/redirect/?id=%d&streamencoding=mp31'%(trackid)
252
253 # http://www.jamendo.com/get2/id+name+idstr+image/radio/json?order=starred_desc
254 #track_id/track/json/radio_track_inradioplaylist/?order=numradio_asc&radio_id=%i
255 class Queries(object):
256     @staticmethod
257     def albums_this_week():
258         return Query().emit(order='ratingweek_desc')
259     @staticmethod
260     def albums_all_time():
261         return Query().emit(order='ratingtotal_desc')
262     @staticmethod
263     def albums_this_month():
264         return Query().emit(order='ratingmonth_desc')
265     @staticmethod
266     def albums_today():
267         return Query().emit(order='ratingday_desc')
268     @staticmethod
269     def playlists_all_time():
270         q = Query(select=['id','name', 'user_idstr'], request='playlist')
271         return q.emit(order='ratingtotal_desc')
272
273     @staticmethod
274     def tracks_this_month():
275         q = Query(select=['id', 'name',
276                           'stream',
277                           'album_name', 'artist_name',
278                           'album_id', 'artist_id'],
279                   request='track')
280         return q.emit(order='ratingmonth_desc')
281
282     @staticmethod
283     def search_albums(query):
284         q = Query()
285         return q.emit(order='searchweight_desc', query=query)
286
287     @staticmethod
288     def search_artists(query):
289         q = Query(request='artist', select=['id', 'name', 'image'])
290         return q.emit(order='searchweight_desc', query=query)
291
292     @staticmethod
293     def album_tracks(albumids, select=['id',
294                                        'name',
295                                        'numalbum',
296                                        'image',
297                                        'duration',
298                                        'album_name',
299                                        'album_id',
300                                        'artist_name',
301                                        'artist_id']):
302         #http://api.jamendo.com/get2/id+name/track/jsonpretty/?album_id=33+46
303         q = Query(select=select,
304                   request='track')
305         ret = q.emit(albumids=albumids, count=100)
306         for track in ret:
307             track['mp3'] = Query.track_mp3(int(track['id']))
308             track['ogg'] = Query.track_ogg(int(track['id']))
309         return ret