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