]> git.rmz.io Git - dotfiles.git/blobdiff - awesome/widgets/awesompd/jamendo.lua
awesome: add awesompd widget + mpd bindings
[dotfiles.git] / awesome / widgets / awesompd / jamendo.lua
diff --git a/awesome/widgets/awesompd/jamendo.lua b/awesome/widgets/awesompd/jamendo.lua
new file mode 100644 (file)
index 0000000..3851e0a
--- /dev/null
@@ -0,0 +1,518 @@
+---------------------------------------------------------------------------
+-- @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&param2=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