5d2bde9f1891abe2f71b8ecb49054358086b02da
[jamaendo] / jamaendo / api.py
1 import urllib, threading, os, gzip, time, json, 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_artist_obj(self, element):
75         if element.text is not None and element.text != "":
76             return element.text
77         else:
78             ret = {}
79             for child in element:
80                 if child.tag in ['name', 'id', 'image']:
81                     ret[child.tag] = child.text
82                 if child.tag == 'Albums':
83                     albums = []
84                     for album in child:
85                         albie = {}
86                         for albumitem in album:
87                             if albumitem.tag in ['name', 'id']:
88                                 albie[albumitem.tag] = albumitem.text
89                         albums.append(albie)
90                     ret['albums'] = albums
91             return ret
92
93     def make_album_obj(self, element):
94         if element.text is not None and element.text != "":
95             return element.text
96         else:
97             ret = {}
98             artist = element.getparent().getparent()
99             if artist is not None:
100                 for child in artist:
101                     if child.tag == 'name':
102                         ret['artist'] = child.text
103                     elif child.tag == 'id':
104                         ret['artist_id'] = child.text
105             for child in element:
106                 if child.tag in ['name', 'id', 'image']:
107                     if child.text:
108                         ret[child.tag] = child.text
109                     else:
110                         ret[child.tag] = ""
111                 if child.tag == 'Tracks':
112                     tracks = []
113                     for track in child:
114                         trackd = {}
115                         for trackinfo in track:
116                             if trackinfo.tag in ['name', 'id', 'numalbum']:
117                                 trackd[trackinfo.tag] = trackinfo.text
118                         tracks.append(trackd)
119                     ret['tracks'] = tracks
120             return ret
121
122     def artist_walker(self, name_match):
123         for event, element in etree.iterparse(self.fil, tag="artist"):
124             name = element.xpath('./name')[0].text.lower()
125             if name and name.find(name_match) > -1:
126                 yield self.make_artist_obj(element)
127             element.clear()
128             while element.getprevious() is not None:
129                 del element.getparent()[0]
130         raise StopIteration
131
132     def album_walker(self, name_match):
133         for event, element in etree.iterparse(self.fil, tag="album"):
134             name = element.xpath('./name')[0].text
135             if name and name.lower().find(name_match) > -1:
136                 yield self.make_album_obj(element)
137             element.clear()
138             while element.getprevious() is not None:
139                 del element.getparent()[0]
140         raise StopIteration
141
142     def artistid_walker(self, artistids):
143         for event, element in etree.iterparse(self.fil, tag="artist"):
144             _id = element.xpath('./id')[0].text
145             if _id and int(_id) in artistids:
146                 yield self.make_artist_obj(element)
147             element.clear()
148             while element.getprevious() is not None:
149                 del element.getparent()[0]
150         raise StopIteration
151
152     def albumid_walker(self, albumids):
153         for event, element in etree.iterparse(self.fil, tag="album"):
154             _id = element.xpath('./id')[0].text
155             if _id and (int(_id) in albumids):
156                 yield self.make_album_obj(element)
157             element.clear()
158             while element.getprevious() is not None:
159                 del element.getparent()[0]
160         raise StopIteration
161
162     def search_artists(self, substr):
163         substr = substr.lower()
164         return (artist for artist in self.artist_walker(substr))
165
166     def search_albums(self, substr):
167         substr = substr.lower()
168         return (album for album in self.album_walker(substr))
169
170     def get_artists(self, artistids):
171         return (artist for artist in self.artistid_walker(artistids))
172
173     def get_albums(self, albumids):
174         return (album for album in self.albumid_walker(albumids))
175
176 _GET2 = '''http://api.jamendo.com/get2/'''
177
178 class Query(object):
179     last_query = time.time()
180     caching = True
181     cache_time = 60*60*24
182     rate_limit = 1.0 # max queries per second
183
184     def __init__(self, order,
185                  select=['id', 'name', 'image', 'artist_name'],
186                  request='album',
187                  track=['track_album', 'album_artist'],
188                  count=5):
189         if request == 'track':
190             self.url = "%s%s/%s/json/%s?n=%s&order=%s" % (_GET2, '+'.join(select), request, '+'.join(track), count, order)
191         else:
192             self.url = "%s%s/%s/json/?n=%s&order=%s" % (_GET2, '+'.join(select), request, count, order)
193
194
195     def _ratelimit(self):
196         now = time.time()
197         if now - self.last_query < self.rate_limit:
198             time.sleep(self.rate_limit - (now - self.last_query))
199         self.last_query = now
200
201     def __call__(self):
202         """ratelimited query"""
203         self._ratelimit()
204         f = urllib.urlopen(self.url)
205         ret = json.load(f)
206         f.close()
207         return ret
208
209     @staticmethod
210     def album_cover(albumid, size=200):
211         to = '~/.cache/jamaendo/cover-%d-%d.jpg'%(albumid, size)
212         if not os.path.isfile(to):
213             url = _GET2+'image/album/redirect/?id=%d&imagesize=%d'%(albumid, size)
214             urllib.urlretrieve(url, to)
215         return to
216
217     @staticmethod
218     def track_ogg(trackid):
219        return _GET2+ 'stream/track/redirect/?id=%d&streamencoding=ogg2'%(trackid)
220
221     @staticmethod
222     def track_mp3(trackid):
223        return _GET2+ 'stream/track/redirect/?id=%d&streamencoding=mp31'%(trackid)
224
225 class Queries(object):
226     albums_this_week = Query(order='ratingweek_desc')
227     albums_all_time = Query(order='ratingtotal_desc')
228     albums_this_month = Query(order='ratingmonth_desc')
229     albums_today = Query(order='ratingday_desc')
230     playlists_all_time = Query(select=['id','name', 'user_idstr'], request='playlist', order='ratingtotal_desc')
231     tracks_this_month = Query(select=['id', 'name',
232                                       'stream',
233                                       'album_name', 'artist_name',
234                                       'album_id', 'artist_id'],
235                               request='track',
236                               order='ratingmonth_desc')