aboutsummaryrefslogtreecommitdiff
path: root/jamendo.lua
blob: 1f4a915e68f5228802624b575949c7efb7d23d04 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
module('jamendo', package.seeall)

-- Grab environment
local os = os

-- Global variables
FORMAT_MP3 = { display = "MP3 (128k)", 
               short_display = "MP3", 
               value = "mp31" }
FORMAT_OGG = { display = "Ogg Vorbis (q4)", 
               short_display = "Ogg", 
               value = "ogg2" }
ORDER_RATINGDAILY = { display = "Daily rating", 
                      short_display = "daily rating", 
                      value = "ratingday_desc" }
ORDER_RATINGWEEKLY = { display = "Weekly rating", 
                      short_display = "weekly rating", 
                      value = "ratingweek_desc" }
ORDER_RATINGTOTAL = { display = "All time rating", 
                      short_display = "all time rating", 
                      value = "ratingtotal_desc" }
ORDER_RANDOM = { display = "Random", 
                 short_display = "random", 
                 value = "random_desc" }
SEARCH_ARTIST = { display = "Artist",
                  value = "artist" }
SEARCH_ALBUM = { display = "Album",
                 value = "album" }
SEARCH_TAG = { display = "Tag",
               value = "tag_idstr" }

current_request_table = { format = FORMAT_MP3,
                          order = ORDER_RATINGWEEKLY }

-- Local variables
local jamendo_list = {}
local cache_file = awful.util.getdir ("cache").."/jamendo_cache"
local default_mp3_stream = nil

-- Returns default stream number for MP3 format. Requests API for it
-- not more often than every hour.
function get_default_mp3_stream()
   if not default_mp3_stream or 
      (os.time() - default_mp3_stream.last_checked) > 3600 then
      local trygetlink = 
         assert(io.popen("echo $(curl -w %{redirect_url} " .. 
                         "'http://api.jamendo.com/get2/stream/track/redirect/" .. 
                         "?streamencoding="..format.."&id=729304')",'r')):read("*line")
      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.
function get_id_from_link(link)
   local _, _, id = string.find(link,"stream/(%d+)")
   return id
end

-- Returns link to music stream for the given track ID. Uses MP3
-- format and the default stream for it.
function get_link_by_id(id)
   return string.format("http://stream%s.jamendo.com/stream/%s/mp31/", 
                        get_default_mp3_stream(), id)
end

-- Returns track name for given music stream.
function get_name_by_link(link)
   return jamendo_list[get_id_from_link(link)]
end

-- Returns table of track IDs, names and other things based on the
-- request table.
function return_track_table(request_table)
   local req_string = form_request(request_table)
   local bus = assert(io.popen(req_string, 'r'))
   local response = bus:read("*all")
   bus:close()
   parse_table = parse_json(response)
   for i = 1, table.getn(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].display_name = 
         parse_table[i].artist_name .. " - " .. parse_table[i].name
      -- Save fetched tracks for further caching
      jamendo_list[parse_table[i].id] = parse_table[i].display_name
   end
   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.
function form_request(request_table)
   local curl_str = 'echo $(curl -w %%{redirect_url} ' ..
      '"http://api.jamendo.com/get2/id+artist_name+name+stream/' ..
      'track/json/track_album+album_artist/?n=100&order=%s&streamencoding=%s")'
   if request_table then
      local format = request_table.format or current_request_table.format
      local order = request_table.order or current_request_table.order
      return string.format(curl_str, order.value, format.value)
   else
      print("Request : " .. string.format(curl_str, 
                           current_request_table.order.value,
                                        current_request_table.format.value))
      return string.format(curl_str, 
                           current_request_table.order.value,
                           current_request_table.format.value)
   end
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 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, find 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 = 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 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)
                     -- Grab high and low byte
                     local hi = math.floor(code / 256) * 4 + 192
                     local lo = math.mod(code, 256)
                     -- Reduce low byte to 64, add overflow to high
                     local oflow = math.floor(lo / 64)
                     hi = hi + oflow
                     lo = math.mod(code, 64) + 128
                     -- Return symbol as \hi\lo
                     return string.char(hi, lo)
                  end
   return string.gsub(s, pattern, decode)
end

-- Retrieves mapping of track IDs to track names to avoid redundant
-- queries when Awesome gets restarted.
function retrieve_cache()
   local bus = io.open(cache_file)
   if bus then
      for l in bus:lines() do
         local _, _, id, track = string.find(l,"(%d+)-(.+)")
         jamendo_list[id] = track
      end
   end
end

-- Saves track IDs to track names mapping into the cache file.
function save_cache()
   local bus = io.open(cache_file, "w")
   for id,name in pairs(jamendo_list) do
      bus:write(id.."-"..name.."\n")
   end
   bus:flush()
   bus:close()
end

-- Retrieve cache on initialization
retrieve_cache()