jamendo.lua 20 KB

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