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