9913fcc6f2262055a1cd9703cc1d2398c265ee6a
[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 DB(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
165     def __init__(self, order, select=['id', 'name', 'image', 'artist_name'], request='album', track=None, n=8):
166         if request == 'track':
167             self.url = "%s%s/%s/json/%s?n=%s&order=%s" % (_GET2, '+'.join(select), request, '+'.join(track), n, order)
168         else:
169             self.url = "%s%s/%s/json/?n=%s&order=%s" % (_GET2, '+'.join(select), request, n, order)
170
171     def emit(self):
172         """ratelimited query"""
173         now = time.time()
174         if now - self.last_query < 1.0:
175             time.sleep(1.0 - (now - self.last_query))
176         self.last_query = now
177         f = urllib.urlopen(self.url)
178         ret = json.load(f)
179         f.close()
180         return ret
181
182 class Queries(object):
183     albums_this_week = Query(order='ratingweek_desc')
184     albums_all_time = Query(order='ratingtotal_desc')
185     albums_this_month = Query(order='ratingmonth_desc')
186     albums_today = Query(order='ratingday_desc')
187     playlists_all_time = Query(select=['id','name', 'user_idstr'], request='playlist', order='ratingtotal_desc')
188     tracks_this_month = Query(select=['id', 'name', 'url', 'stream', 'album_name', 'album_url', 'album_id', 'artist_id', 'artist_name'],
189                               request='track',
190                               track=['track_album', 'album_artist'],
191                               order='ratingmonth_desc')
192
193 def get_cover(albumid, size=200):
194     to = '~/.cache/jamaendo/cover-%d-%d.jpg'%(albumid, size)
195     if not os.path.isfile(to):
196         url = _GET2+'image/album/redirect/?id=%d&imagesize=%d'%(albumid, size)
197         urllib.urlretrieve(url, to)
198     return to
199
200 def get_ogg_url(trackid):
201    return _GET2+ 'stream/track/redirect/?id=%d&streamencoding=ogg2'%(trackid)
202
203 def get_mp3_url(trackid):
204    return _GET2+ 'stream/track/redirect/?id=%d&streamencoding=mp31'%(trackid)