]> git.rmz.io Git - dotfiles.git/blob - awesome/widgets/awesompd/jamendo.lua
awesome: add awesompd widget + mpd bindings
[dotfiles.git] / awesome / widgets / awesompd / jamendo.lua
1 ---------------------------------------------------------------------------
2 -- @author Alexander Yakushev <yakushev.alex@gmail.com>
3 -- @copyright 2011-2013 Alexander Yakushev
4 -- @release v1.2.4
5 ---------------------------------------------------------------------------
6
7 -- Grab environment
8 local awful = require('awful')
9
10 local jamendo = {}
11
12 -- UTILITY STUFF
13 -- Checks whether file specified by filename exists.
14 local function file_exists(filename, mode)
15 mode = mode or 'r'
16 f = io.open(filename, mode)
17 if f then
18 f:close()
19 return true
20 else
21 return false
22 end
23 end
24
25 local function str_interpose(coll, sep)
26 if #coll == 0 then
27 return ""
28 end
29 local result = coll[1]
30 for i = 2, #coll do
31 result = result .. sep .. coll[i]
32 end
33 print(result)
34 return result
35 end
36
37 -- Global variables
38 jamendo.FORMAT_MP3 = { display = "MP3 (128k)",
39 short_display = "MP3",
40 value = "mp31" }
41 jamendo.FORMAT_OGG = { display = "Ogg Vorbis (q4)",
42 short_display = "Ogg",
43 value = "ogg2" }
44 jamendo.ORDER_RATINGDAILY = { display = "Daily rating",
45 short_display = "daily rating",
46 value = "ratingday_desc" }
47 jamendo.ORDER_RATINGWEEKLY = { display = "Weekly rating",
48 short_display = "weekly rating",
49 value = "ratingweek_desc" }
50 jamendo.ORDER_RATINGTOTAL = { display = "All time rating",
51 short_display = "all time rating",
52 value = "ratingtotal_desc" }
53 jamendo.ORDER_RANDOM = { display = "Random",
54 short_display = "random",
55 value = "random_desc" }
56 jamendo.ORDER_RELEVANCE = { display = "None (consecutive)",
57 short_display = "none",
58 value = "searchweight_desc" }
59 jamendo.SEARCH_ARTIST = { display = "Artist",
60 unit = "artist",
61 value = "artist_id" }
62 jamendo.SEARCH_ALBUM = { display = "Album",
63 unit = "album",
64 value = "album_id" }
65 jamendo.SEARCH_TAG = { display = "Tag",
66 unit = "tag",
67 value = "tag_id" }
68 jamendo.ALL_FORMATS = { jamendo.FORMAT_MP3, jamendo.FORMAT_OGG }
69 jamendo.ALL_ORDERS = { ORDER_RELEVANCE, ORDER_RANDOM, ORDER_RATINGDAILY,
70 ORDER_RATINGWEEKLY, ORDER_RATINGTOTAL }
71
72 jamendo.current_request_table = { unit = "track",
73 fields = {"id", "artist_url", "artist_name", "name",
74 "stream", "album_image", "album_name" },
75 joins = { "track_album", "album_artist" },
76 params = { streamencoding = jamendo.FORMAT_MP3,
77 order = jamendo.ORDER_RATINGWEEKLY,
78 n = 100 }}
79
80 -- Local variables
81 local jamendo_list = {}
82 local cache_file = awful.util.getdir ("cache").."/jamendo_cache"
83 local cache_header = "[version=1.1.0]"
84 local album_covers_folder = awful.util.getdir("cache") .. "/jamendo_covers/"
85 local default_mp3_stream = nil
86 local search_template = { fields = { "id", "name" },
87 joins = {},
88 params = { order = ORDER_RELEVANCE,
89 n = 1}}
90
91 -- DEPRECATED. Will be removed in the next major release.
92 -- Returns default stream number for MP3 format. Requests API for it
93 -- not more often than every hour.
94 local function get_default_mp3_stream()
95 if not default_mp3_stream or
96 (os.time() - default_mp3_stream.last_checked) > 3600 then
97 local trygetlink =
98 jamendo.perform_request("echo $(curl -w %{redirect_url} " ..
99 "'http://api.jamendo.com/get2/stream/track/redirect/" ..
100 "?streamencoding="..jamendo.FORMAT_MP3.value.."&id=729304')")
101 local _, _, prefix = string.find(trygetlink, "stream(%d+)%.jamendo%.com")
102 default_mp3_stream = { id = prefix, last_checked = os.time() }
103 end
104 return default_mp3_stream.id
105 end
106
107 -- Returns the track ID from the given link to Jamendo stream. If the
108 -- given text is not the Jamendo stream returns nil.
109 function jamendo.get_id_from_link(link)
110 local _, _, id = string.find(link,"storage%-new.newjamendo.com/%?trackid=(%d+)")
111 return id
112 end
113
114 -- Returns link to music stream for the given track ID. Uses MP3
115 -- format and the default stream for it.
116 local function get_link_by_id(id)
117 -- This function is subject to change in the future.
118 return string.format("http://storage-new.newjamendo.com?trackid=%s&format=mp31&u=0", id)
119 end
120
121 -- -- Returns the album id for given music stream.
122 -- function get_album_id_by_link(link)
123 -- local id = get_id_from_link(link, true)
124 -- if id and jamendo_list[id] then
125 -- return jamendo_list[id].album_id
126 -- end
127 -- end
128
129 -- Returns the track table for the given music stream.
130 function jamendo.get_track_by_link(link)
131 local id = jamendo.get_id_from_link(link, true)
132 if id and jamendo_list[id] then
133 return jamendo_list[id]
134 end
135 end
136
137 -- If a track is actually a Jamendo stream, replace it with normal
138 -- track name.
139 function jamendo.replace_link(track_name)
140 local track = jamendo.get_track_by_link(track_name)
141 if track then
142 return track.display_name
143 else
144 return track_name
145 end
146 end
147
148 -- Returns table of track IDs, names and other things based on the
149 -- request table.
150 function jamendo.return_track_table(request_table)
151 local req_string = jamendo.form_request(request_table)
152 local response = jamendo.perform_request(req_string)
153 if not response then
154 return nil -- Bad internet connection
155 end
156 local parse_table = jamendo.parse_json(response)
157 for i = 1, #parse_table do
158 if parse_table[i].stream == "" then
159 -- Some songs don't have Ogg stream, use MP3 instead
160 parse_table[i].stream = get_link_by_id(parse_table[i].id)
161 end
162 _, _, parse_table[i].artist_link_name =
163 string.find(parse_table[i].artist_url, "\\/artist\\/(.+)")
164 -- Remove Jamendo escape slashes
165 parse_table[i].artist_name =
166 string.gsub(parse_table[i].artist_name, "\\/", "/")
167 parse_table[i].name = string.gsub(parse_table[i].name, "\\/", "/")
168
169 parse_table[i].display_name =
170 parse_table[i].artist_name .. " - " .. parse_table[i].name
171 -- Do Jamendo a favor, extract album_id for the track yourself
172 -- from album_image link :)
173 local _, _, album_id =
174 string.find(parse_table[i].album_image, "\\/(%d+)\\/covers")
175 parse_table[i].album_id = album_id or 0
176 -- Save fetched tracks for further caching
177 jamendo_list[parse_table[i].id] = parse_table[i]
178 end
179 jamendo.save_cache()
180 return parse_table
181 end
182
183 -- Generates the request to Jamendo API based on provided request
184 -- table. If request_table is nil, uses current_request_table instead.
185 -- For all values that do not exist in request_table use ones from
186 -- current_request_table.
187 -- return - HTTP-request
188 function jamendo.form_request(request_table)
189 local curl_str = "curl -A 'Mozilla/4.0' -fsm 5 \"%s\""
190 local url = "http://api.jamendo.com/get2/%s/%s/json/%s/?%s"
191 request_table = request_table or jamendo.current_request_table
192
193 local fields = request_table.fields or jamendo.current_request_table.fields
194 local joins = request_table.joins or jamendo.current_request_table.joins
195 local unit = request_table.unit or jamendo.current_request_table.unit
196
197 -- Form fields string (like field1+field2+fieldN)
198 local f_string = str_interpose(fields, "+")
199 -- Form joins string
200 local j_string = str_interpose(joins, "+")
201
202 local params = {}
203 -- If parameters where supplied in request_table, add them to the
204 -- parameters in current_request_table.
205 if request_table.params and
206 request_table.params ~= jamendo.current_request_table.params then
207 -- First fill params with current_request_table parameters
208 for k, v in pairs(jamendo.current_request_table.params) do
209 params[k] = v
210 end
211 -- Then add and overwrite them with request_table parameters
212 for k, v in pairs(request_table.params) do
213 params[k] = v
214 end
215 else -- Or just use current_request_table.params
216 params = jamendo.current_request_table.params
217 end
218 -- Form parameter string (like param1=value1&param2=value2)
219 local param_string = ""
220 for k, v in pairs(params) do
221 if type(v) == "table" then
222 v = v.value
223 end
224 v = string.gsub(v, " ", "+")
225 param_string = param_string .. "&" .. k .. "=" .. v
226 end
227
228 return string.format(curl_str, string.format(url, f_string, unit, j_string, param_string))
229 end
230
231 -- Primitive function for parsing Jamendo API JSON response. Does not
232 -- support arrays. Supports only strings and numbers as values.
233 -- Provides basic safety (correctly handles special symbols like comma
234 -- and curly brackets inside strings)
235 -- text - JSON text
236 function jamendo.parse_json(text)
237 local parse_table = {}
238 local block = {}
239 local i = 0
240 local inblock = false
241 local instring = false
242 local curr_key = nil
243 local curr_val = nil
244 while i and i < string.len(text) do
245 if not inblock then -- We are not inside the block, find next {
246 i = string.find(text, "{", i+1)
247 inblock = true
248 block = {}
249 else
250 if not curr_key then -- We haven't found key yet
251 if not instring then -- We are not in string, check for more tags
252 local j = string.find(text, '"', i+1)
253 local k = string.find(text, '}', i+1)
254 if j and j < k then -- There are more tags in this block
255 i = j
256 instring = true
257 else -- Block is over, we found its ending
258 i = k
259 inblock = false
260 table.insert(parse_table, block)
261 end
262 else -- We are in string, find its ending
263 _, i, curr_key = string.find(text,'(.-[^%\\])"', i+1)
264 instring = false
265 end
266 else -- We have the key, let's find the value
267 if not curr_val then -- Value is not found yet
268 if not instring then -- Not in string, check if value is string
269 local j = string.find(text, '"', i+1)
270 local k = string.find(text, '[,}]', i+1)
271 if j and j < k then -- Value is string
272 i = j
273 instring = true
274 else -- Value is int
275 _, i, curr_val = string.find(text,'(%d+)', i+1)
276 end
277 else -- We are in string, find its ending
278 local j = string.find(text, '"', i+1)
279 if j == i+1 then -- String is empty
280 i = j
281 curr_val = ""
282 else
283 _, i, curr_val = string.find(text,'(.-[^%\\])"', i+1)
284 curr_val = jamendo.utf8_codes_to_symbols(curr_val)
285 end
286 instring = false
287 end
288 else -- We have both key and value, add it to table
289 block[curr_key] = curr_val
290 curr_key = nil
291 curr_val = nil
292 end
293 end
294 end
295 end
296 return parse_table
297 end
298
299 -- Jamendo returns Unicode symbols as \uXXXX. Lua does not transform
300 -- them into symbols so we need to do it ourselves.
301 function jamendo.utf8_codes_to_symbols (s)
302 local hexnums = "[%dabcdefABCDEF]"
303 local pattern = string.format("\\u(%s%s%s%s?)",
304 hexnums, hexnums, hexnums, hexnums)
305 local decode = function(code)
306 code = tonumber(code, 16)
307 if code < 128 then -- one-byte symbol
308 return string.char(code)
309 elseif code < 2048 then -- two-byte symbol
310 -- Grab high and low bytes
311 local hi = math.floor(code / 64)
312 local lo = math.fmod(code, 64)
313 -- Return symbol as \hi\lo
314 return string.char(hi + 192, lo + 128)
315 elseif code < 65536 then
316 -- Grab high, middle and low bytes
317 local hi = math.floor(code / 4096)
318 local leftover = code - hi * 4096
319 local mi = math.floor(leftover / 64)
320 leftover = leftover - mi * 64
321 local lo = math.fmod(leftover, 64)
322 -- Return symbol as \hi\mi\lo
323 return string.char(hi + 224, mi + 160, lo + 128)
324 elseif code < 1114112 then
325 -- Grab high, highmiddle, lowmiddle and low bytes
326 local hi = math.floor(code / 262144)
327 local leftover = code - hi * 262144
328 local hm = math.floor(leftover / 4096)
329 leftover = leftover - hm * 4096
330 local lm = math.floor(leftover / 64)
331 local lo = math.fmod(leftover, 64)
332 -- Return symbol as \hi\hm\lm\lo
333 return string.char(hi + 240, hm + 128, lm + 128, lo + 128)
334 else -- It is not Unicode symbol at all
335 return tostring(code)
336 end
337 end
338 return string.gsub(s, pattern, decode)
339 end
340
341 -- Retrieves mapping of track IDs to track names and album IDs to
342 -- avoid redundant queries when Awesome gets restarted.
343 local function retrieve_cache()
344 local bus = io.open(cache_file)
345 local track = {}
346 if bus then
347 local header = bus:read("*line")
348 if header == cache_header then
349 for l in bus:lines() do
350 local _, _, id, artist_link_name, album_name, album_id, track_name =
351 string.find(l,"(%d+)-([^-]+)-([^-]+)-(%d+)-(.+)")
352 track = {}
353 track.id = id
354 track.artist_link_name = string.gsub(artist_link_name, '\\_', '-')
355 track.album_name = string.gsub(album_name, '\\_', '-')
356 track.album_id = album_id
357 track.display_name = track_name
358 jamendo_list[id] = track
359 end
360 else
361 -- We encountered an outdated version of the cache
362 -- file. Let's just remove it.
363 awful.util.spawn("rm -f " .. cache_file)
364 end
365 end
366 end
367
368 -- Saves track IDs to track names and album IDs mapping into the cache
369 -- file.
370 function jamendo.save_cache()
371 local bus = io.open(cache_file, "w")
372 bus:write(cache_header .. "\n")
373 for id,track in pairs(jamendo_list) do
374 bus:write(string.format("%s-%s-%s-%s-%s\n", id,
375 string.gsub(track.artist_link_name, '-', '\\_'),
376 string.gsub(track.album_name, '-', '\\_'),
377 track.album_id, track.display_name))
378 end
379 bus:flush()
380 bus:close()
381 end
382
383 -- Retrieve cache on initialization
384 retrieve_cache()
385
386 -- Returns a filename of the album cover and formed wget request that
387 -- downloads the album cover for the given track name. If the album
388 -- cover already exists returns nil as the second argument.
389 function jamendo.fetch_album_cover_request(track_id)
390 local track = jamendo_list[track_id]
391 local album_id = track.album_id
392
393 if album_id == 0 then -- No cover for tracks without album!
394 return nil
395 end
396 local file_path = album_covers_folder .. album_id .. ".jpg"
397
398 if not file_exists(file_path) then -- We need to download it
399 -- First check if cache directory exists
400 f = io.popen('test -d ' .. album_covers_folder .. ' && echo t')
401 if f:read("*line") ~= 't' then
402 awful.util.spawn("mkdir " .. album_covers_folder)
403 end
404 f:close()
405
406 if not track.album_image then -- Wow! We have album_id, but
407 local a_id = tostring(album_id) --don't have album_image. Well,
408 local prefix = --it happens.
409 string.sub(a_id, 1, #a_id - 3)
410 track.album_image =
411 string.format("http://imgjam.com/albums/s%s/%s/covers/1.100.jpg",
412 prefix == "" and 0 or prefix, a_id)
413 end
414
415 return file_path, string.format("wget %s -O %s 2> /dev/null",
416 track.album_image, file_path)
417 else -- Cover already downloaded, return its filename and nil
418 return file_path, nil
419 end
420 end
421
422 -- Returns a file containing an album cover for given track id. First
423 -- searches in the cache folder. If file is not there, fetches it from
424 -- the Internet and saves into the cache folder.
425 function jamendo.get_album_cover(track_id)
426 local file_path, fetch_req = jamendo.fetch_album_cover_request(track_id)
427 if fetch_req then
428 local f = io.popen(fetch_req)
429 f:close()
430
431 -- Let's check if file is finally there, just in case
432 if not file_exists(file_path) then
433 return nil
434 end
435 end
436 return file_path
437 end
438
439 -- Same as get_album_cover, but downloads (if necessary) the cover
440 -- asynchronously.
441 function jamendo.get_album_cover_async(track_id)
442 local file_path, fetch_req = jamendo.fetch_album_cover_request(track_id)
443 if fetch_req then
444 asyncshell.request(fetch_req)
445 end
446 end
447
448 -- Checks if track_name is actually a link to Jamendo stream. If true
449 -- returns the file with album cover for the track.
450 function jamendo.try_get_cover(track_name)
451 local id = jamendo.get_id_from_link(track_name)
452 if id then
453 return jamendo.get_album_cover(id)
454 end
455 end
456
457 -- Same as try_get_cover, but calls get_album_cover_async inside.
458 function jamendo.try_get_cover_async(track_name)
459 local id = jamendo.get_id_from_link(track_name)
460 if id then
461 return jamendo.get_album_cover_async(id)
462 end
463 end
464
465 -- Returns the track table for given query and search method.
466 -- what - search method - SEARCH_ARTIST, ALBUM or TAG
467 -- s - string to search
468 function jamendo.search_by(what, s)
469 -- Get a default request and set unit and query
470 local req = search_template
471 req.unit = what.unit
472 req.params.searchquery = s
473 local resp = jamendo.perform_request(jamendo.form_request(req))
474 if resp then
475 local search_res = jamendo.parse_json(resp)[1]
476
477 if search_res then
478 -- Now when we got the search result, find tracks filtered by
479 -- this result.
480 local params = {}
481 params[what.value] = search_res.id
482 req = { params = params }
483 local track_table = jamendo.return_track_table(req)
484 return { search_res = search_res, tracks = track_table }
485 end
486 end
487 end
488
489 -- Executes request_string with io.popen and returns the response.
490 function jamendo.perform_request(request_string)
491 local bus = assert(io.popen(request_string,'r'))
492 local response = bus:read("*all")
493 bus:close()
494 -- Curl with popen can sometimes fail to fetch data when the
495 -- connection is slow. Let's try again if it fails.
496 if #response == 0 then
497 bus = assert(io.popen(request_string,'r'))
498 response = bus:read("*all")
499 bus:close()
500 -- If it still can't read anything, return nil
501 if #response ~= 0 then
502 return nil
503 end
504 end
505 return response
506 end
507
508 -- Sets default streamencoding in current_request_table.
509 function jamendo.set_current_format(format)
510 jamendo.current_request_table.params.streamencoding = format
511 end
512
513 -- Sets default order in current_request_table.
514 function jamendo.set_current_order(order)
515 jamendo.current_request_table.params.order = order
516 end
517
518 return jamendo