X-Git-Url: https://git.rmz.io/dotfiles.git/blobdiff_plain/2118554cdc7a30ba45771e25dc007e8a27c008c3..dd30f585b4ec95426cb4f7348b4893c238250e1c:/awesome/widgets/awesompd/jamendo.lua?ds=inline diff --git a/awesome/widgets/awesompd/jamendo.lua b/awesome/widgets/awesompd/jamendo.lua new file mode 100644 index 0000000..3851e0a --- /dev/null +++ b/awesome/widgets/awesompd/jamendo.lua @@ -0,0 +1,518 @@ +--------------------------------------------------------------------------- +-- @author Alexander Yakushev +-- @copyright 2011-2013 Alexander Yakushev +-- @release v1.2.4 +--------------------------------------------------------------------------- + +-- Grab environment +local awful = require('awful') + +local jamendo = {} + +-- UTILITY STUFF +-- Checks whether file specified by filename exists. +local function file_exists(filename, mode) + mode = mode or 'r' + f = io.open(filename, mode) + if f then + f:close() + return true + else + return false + end +end + +local function str_interpose(coll, sep) + if #coll == 0 then + return "" + end + local result = coll[1] + for i = 2, #coll do + result = result .. sep .. coll[i] + end + print(result) + return result +end + +-- Global variables +jamendo.FORMAT_MP3 = { display = "MP3 (128k)", + short_display = "MP3", + value = "mp31" } +jamendo.FORMAT_OGG = { display = "Ogg Vorbis (q4)", + short_display = "Ogg", + value = "ogg2" } +jamendo.ORDER_RATINGDAILY = { display = "Daily rating", + short_display = "daily rating", + value = "ratingday_desc" } +jamendo.ORDER_RATINGWEEKLY = { display = "Weekly rating", + short_display = "weekly rating", + value = "ratingweek_desc" } +jamendo.ORDER_RATINGTOTAL = { display = "All time rating", + short_display = "all time rating", + value = "ratingtotal_desc" } +jamendo.ORDER_RANDOM = { display = "Random", + short_display = "random", + value = "random_desc" } +jamendo.ORDER_RELEVANCE = { display = "None (consecutive)", + short_display = "none", + value = "searchweight_desc" } +jamendo.SEARCH_ARTIST = { display = "Artist", + unit = "artist", + value = "artist_id" } +jamendo.SEARCH_ALBUM = { display = "Album", + unit = "album", + value = "album_id" } +jamendo.SEARCH_TAG = { display = "Tag", + unit = "tag", + value = "tag_id" } +jamendo.ALL_FORMATS = { jamendo.FORMAT_MP3, jamendo.FORMAT_OGG } +jamendo.ALL_ORDERS = { ORDER_RELEVANCE, ORDER_RANDOM, ORDER_RATINGDAILY, + ORDER_RATINGWEEKLY, ORDER_RATINGTOTAL } + +jamendo.current_request_table = { unit = "track", + fields = {"id", "artist_url", "artist_name", "name", + "stream", "album_image", "album_name" }, + joins = { "track_album", "album_artist" }, + params = { streamencoding = jamendo.FORMAT_MP3, + order = jamendo.ORDER_RATINGWEEKLY, + n = 100 }} + +-- Local variables +local jamendo_list = {} +local cache_file = awful.util.getdir ("cache").."/jamendo_cache" +local cache_header = "[version=1.1.0]" +local album_covers_folder = awful.util.getdir("cache") .. "/jamendo_covers/" +local default_mp3_stream = nil +local search_template = { fields = { "id", "name" }, + joins = {}, + params = { order = ORDER_RELEVANCE, + n = 1}} + +-- DEPRECATED. Will be removed in the next major release. +-- Returns default stream number for MP3 format. Requests API for it +-- not more often than every hour. +local function get_default_mp3_stream() + if not default_mp3_stream or + (os.time() - default_mp3_stream.last_checked) > 3600 then + local trygetlink = + jamendo.perform_request("echo $(curl -w %{redirect_url} " .. + "'http://api.jamendo.com/get2/stream/track/redirect/" .. + "?streamencoding="..jamendo.FORMAT_MP3.value.."&id=729304')") + local _, _, prefix = string.find(trygetlink, "stream(%d+)%.jamendo%.com") + default_mp3_stream = { id = prefix, last_checked = os.time() } + end + return default_mp3_stream.id +end + +-- Returns the track ID from the given link to Jamendo stream. If the +-- given text is not the Jamendo stream returns nil. +function jamendo.get_id_from_link(link) + local _, _, id = string.find(link,"storage%-new.newjamendo.com/%?trackid=(%d+)") + return id +end + +-- Returns link to music stream for the given track ID. Uses MP3 +-- format and the default stream for it. +local function get_link_by_id(id) + -- This function is subject to change in the future. + return string.format("http://storage-new.newjamendo.com?trackid=%s&format=mp31&u=0", id) +end + +-- -- Returns the album id for given music stream. +-- function get_album_id_by_link(link) +-- local id = get_id_from_link(link, true) +-- if id and jamendo_list[id] then +-- return jamendo_list[id].album_id +-- end +-- end + +-- Returns the track table for the given music stream. +function jamendo.get_track_by_link(link) + local id = jamendo.get_id_from_link(link, true) + if id and jamendo_list[id] then + return jamendo_list[id] + end +end + +-- If a track is actually a Jamendo stream, replace it with normal +-- track name. +function jamendo.replace_link(track_name) + local track = jamendo.get_track_by_link(track_name) + if track then + return track.display_name + else + return track_name + end +end + +-- Returns table of track IDs, names and other things based on the +-- request table. +function jamendo.return_track_table(request_table) + local req_string = jamendo.form_request(request_table) + local response = jamendo.perform_request(req_string) + if not response then + return nil -- Bad internet connection + end + local parse_table = jamendo.parse_json(response) + for i = 1, #parse_table do + if parse_table[i].stream == "" then + -- Some songs don't have Ogg stream, use MP3 instead + parse_table[i].stream = get_link_by_id(parse_table[i].id) + end + _, _, parse_table[i].artist_link_name = + string.find(parse_table[i].artist_url, "\\/artist\\/(.+)") + -- Remove Jamendo escape slashes + parse_table[i].artist_name = + string.gsub(parse_table[i].artist_name, "\\/", "/") + parse_table[i].name = string.gsub(parse_table[i].name, "\\/", "/") + + parse_table[i].display_name = + parse_table[i].artist_name .. " - " .. parse_table[i].name + -- Do Jamendo a favor, extract album_id for the track yourself + -- from album_image link :) + local _, _, album_id = + string.find(parse_table[i].album_image, "\\/(%d+)\\/covers") + parse_table[i].album_id = album_id or 0 + -- Save fetched tracks for further caching + jamendo_list[parse_table[i].id] = parse_table[i] + end + jamendo.save_cache() + return parse_table +end + +-- Generates the request to Jamendo API based on provided request +-- table. If request_table is nil, uses current_request_table instead. +-- For all values that do not exist in request_table use ones from +-- current_request_table. +-- return - HTTP-request +function jamendo.form_request(request_table) + local curl_str = "curl -A 'Mozilla/4.0' -fsm 5 \"%s\"" + local url = "http://api.jamendo.com/get2/%s/%s/json/%s/?%s" + request_table = request_table or jamendo.current_request_table + + local fields = request_table.fields or jamendo.current_request_table.fields + local joins = request_table.joins or jamendo.current_request_table.joins + local unit = request_table.unit or jamendo.current_request_table.unit + + -- Form fields string (like field1+field2+fieldN) + local f_string = str_interpose(fields, "+") + -- Form joins string + local j_string = str_interpose(joins, "+") + + local params = {} + -- If parameters where supplied in request_table, add them to the + -- parameters in current_request_table. + if request_table.params and + request_table.params ~= jamendo.current_request_table.params then + -- First fill params with current_request_table parameters + for k, v in pairs(jamendo.current_request_table.params) do + params[k] = v + end + -- Then add and overwrite them with request_table parameters + for k, v in pairs(request_table.params) do + params[k] = v + end + else -- Or just use current_request_table.params + params = jamendo.current_request_table.params + end + -- Form parameter string (like param1=value1¶m2=value2) + local param_string = "" + for k, v in pairs(params) do + if type(v) == "table" then + v = v.value + end + v = string.gsub(v, " ", "+") + param_string = param_string .. "&" .. k .. "=" .. v + end + + return string.format(curl_str, string.format(url, f_string, unit, j_string, param_string)) +end + +-- Primitive function for parsing Jamendo API JSON response. Does not +-- support arrays. Supports only strings and numbers as values. +-- Provides basic safety (correctly handles special symbols like comma +-- and curly brackets inside strings) +-- text - JSON text +function jamendo.parse_json(text) + local parse_table = {} + local block = {} + local i = 0 + local inblock = false + local instring = false + local curr_key = nil + local curr_val = nil + while i and i < string.len(text) do + if not inblock then -- We are not inside the block, find next { + i = string.find(text, "{", i+1) + inblock = true + block = {} + else + if not curr_key then -- We haven't found key yet + if not instring then -- We are not in string, check for more tags + local j = string.find(text, '"', i+1) + local k = string.find(text, '}', i+1) + if j and j < k then -- There are more tags in this block + i = j + instring = true + else -- Block is over, we found its ending + i = k + inblock = false + table.insert(parse_table, block) + end + else -- We are in string, find its ending + _, i, curr_key = string.find(text,'(.-[^%\\])"', i+1) + instring = false + end + else -- We have the key, let's find the value + if not curr_val then -- Value is not found yet + if not instring then -- Not in string, check if value is string + local j = string.find(text, '"', i+1) + local k = string.find(text, '[,}]', i+1) + if j and j < k then -- Value is string + i = j + instring = true + else -- Value is int + _, i, curr_val = string.find(text,'(%d+)', i+1) + end + else -- We are in string, find its ending + local j = string.find(text, '"', i+1) + if j == i+1 then -- String is empty + i = j + curr_val = "" + else + _, i, curr_val = string.find(text,'(.-[^%\\])"', i+1) + curr_val = jamendo.utf8_codes_to_symbols(curr_val) + end + instring = false + end + else -- We have both key and value, add it to table + block[curr_key] = curr_val + curr_key = nil + curr_val = nil + end + end + end + end + return parse_table +end + +-- Jamendo returns Unicode symbols as \uXXXX. Lua does not transform +-- them into symbols so we need to do it ourselves. +function jamendo.utf8_codes_to_symbols (s) + local hexnums = "[%dabcdefABCDEF]" + local pattern = string.format("\\u(%s%s%s%s?)", + hexnums, hexnums, hexnums, hexnums) + local decode = function(code) + code = tonumber(code, 16) + if code < 128 then -- one-byte symbol + return string.char(code) + elseif code < 2048 then -- two-byte symbol + -- Grab high and low bytes + local hi = math.floor(code / 64) + local lo = math.fmod(code, 64) + -- Return symbol as \hi\lo + return string.char(hi + 192, lo + 128) + elseif code < 65536 then + -- Grab high, middle and low bytes + local hi = math.floor(code / 4096) + local leftover = code - hi * 4096 + local mi = math.floor(leftover / 64) + leftover = leftover - mi * 64 + local lo = math.fmod(leftover, 64) + -- Return symbol as \hi\mi\lo + return string.char(hi + 224, mi + 160, lo + 128) + elseif code < 1114112 then + -- Grab high, highmiddle, lowmiddle and low bytes + local hi = math.floor(code / 262144) + local leftover = code - hi * 262144 + local hm = math.floor(leftover / 4096) + leftover = leftover - hm * 4096 + local lm = math.floor(leftover / 64) + local lo = math.fmod(leftover, 64) + -- Return symbol as \hi\hm\lm\lo + return string.char(hi + 240, hm + 128, lm + 128, lo + 128) + else -- It is not Unicode symbol at all + return tostring(code) + end + end + return string.gsub(s, pattern, decode) +end + +-- Retrieves mapping of track IDs to track names and album IDs to +-- avoid redundant queries when Awesome gets restarted. +local function retrieve_cache() + local bus = io.open(cache_file) + local track = {} + if bus then + local header = bus:read("*line") + if header == cache_header then + for l in bus:lines() do + local _, _, id, artist_link_name, album_name, album_id, track_name = + string.find(l,"(%d+)-([^-]+)-([^-]+)-(%d+)-(.+)") + track = {} + track.id = id + track.artist_link_name = string.gsub(artist_link_name, '\\_', '-') + track.album_name = string.gsub(album_name, '\\_', '-') + track.album_id = album_id + track.display_name = track_name + jamendo_list[id] = track + end + else + -- We encountered an outdated version of the cache + -- file. Let's just remove it. + awful.util.spawn("rm -f " .. cache_file) + end + end +end + +-- Saves track IDs to track names and album IDs mapping into the cache +-- file. +function jamendo.save_cache() + local bus = io.open(cache_file, "w") + bus:write(cache_header .. "\n") + for id,track in pairs(jamendo_list) do + bus:write(string.format("%s-%s-%s-%s-%s\n", id, + string.gsub(track.artist_link_name, '-', '\\_'), + string.gsub(track.album_name, '-', '\\_'), + track.album_id, track.display_name)) + end + bus:flush() + bus:close() +end + +-- Retrieve cache on initialization +retrieve_cache() + +-- Returns a filename of the album cover and formed wget request that +-- downloads the album cover for the given track name. If the album +-- cover already exists returns nil as the second argument. +function jamendo.fetch_album_cover_request(track_id) + local track = jamendo_list[track_id] + local album_id = track.album_id + + if album_id == 0 then -- No cover for tracks without album! + return nil + end + local file_path = album_covers_folder .. album_id .. ".jpg" + + if not file_exists(file_path) then -- We need to download it + -- First check if cache directory exists + f = io.popen('test -d ' .. album_covers_folder .. ' && echo t') + if f:read("*line") ~= 't' then + awful.util.spawn("mkdir " .. album_covers_folder) + end + f:close() + + if not track.album_image then -- Wow! We have album_id, but + local a_id = tostring(album_id) --don't have album_image. Well, + local prefix = --it happens. + string.sub(a_id, 1, #a_id - 3) + track.album_image = + string.format("http://imgjam.com/albums/s%s/%s/covers/1.100.jpg", + prefix == "" and 0 or prefix, a_id) + end + + return file_path, string.format("wget %s -O %s 2> /dev/null", + track.album_image, file_path) + else -- Cover already downloaded, return its filename and nil + return file_path, nil + end +end + +-- Returns a file containing an album cover for given track id. First +-- searches in the cache folder. If file is not there, fetches it from +-- the Internet and saves into the cache folder. +function jamendo.get_album_cover(track_id) + local file_path, fetch_req = jamendo.fetch_album_cover_request(track_id) + if fetch_req then + local f = io.popen(fetch_req) + f:close() + + -- Let's check if file is finally there, just in case + if not file_exists(file_path) then + return nil + end + end + return file_path +end + +-- Same as get_album_cover, but downloads (if necessary) the cover +-- asynchronously. +function jamendo.get_album_cover_async(track_id) + local file_path, fetch_req = jamendo.fetch_album_cover_request(track_id) + if fetch_req then + asyncshell.request(fetch_req) + end +end + +-- Checks if track_name is actually a link to Jamendo stream. If true +-- returns the file with album cover for the track. +function jamendo.try_get_cover(track_name) + local id = jamendo.get_id_from_link(track_name) + if id then + return jamendo.get_album_cover(id) + end +end + +-- Same as try_get_cover, but calls get_album_cover_async inside. +function jamendo.try_get_cover_async(track_name) + local id = jamendo.get_id_from_link(track_name) + if id then + return jamendo.get_album_cover_async(id) + end +end + +-- Returns the track table for given query and search method. +-- what - search method - SEARCH_ARTIST, ALBUM or TAG +-- s - string to search +function jamendo.search_by(what, s) + -- Get a default request and set unit and query + local req = search_template + req.unit = what.unit + req.params.searchquery = s + local resp = jamendo.perform_request(jamendo.form_request(req)) + if resp then + local search_res = jamendo.parse_json(resp)[1] + + if search_res then + -- Now when we got the search result, find tracks filtered by + -- this result. + local params = {} + params[what.value] = search_res.id + req = { params = params } + local track_table = jamendo.return_track_table(req) + return { search_res = search_res, tracks = track_table } + end + end +end + +-- Executes request_string with io.popen and returns the response. +function jamendo.perform_request(request_string) + local bus = assert(io.popen(request_string,'r')) + local response = bus:read("*all") + bus:close() + -- Curl with popen can sometimes fail to fetch data when the + -- connection is slow. Let's try again if it fails. + if #response == 0 then + bus = assert(io.popen(request_string,'r')) + response = bus:read("*all") + bus:close() + -- If it still can't read anything, return nil + if #response ~= 0 then + return nil + end + end + return response +end + +-- Sets default streamencoding in current_request_table. +function jamendo.set_current_format(format) + jamendo.current_request_table.params.streamencoding = format +end + +-- Sets default order in current_request_table. +function jamendo.set_current_order(order) + jamendo.current_request_table.params.order = order +end + +return jamendo