123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519 |
- ---------------------------------------------------------------------------
- -- @author Alexander Yakushev <yakushev.alex@gmail.com>
- -- @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
|