Clean the config code.
|
@ -1,19 +0,0 @@
|
|||
## Description ##
|
||||
|
||||
This is an advanced MPD widget\client for AwesomeWM.
|
||||
|
||||
For the detailed installation guide please see http://awesome.naquadah.org/wiki/Awesompd_widget .
|
||||
|
||||
Also you can find an example of the widget configuration in the file rcsample.lua.
|
||||
|
||||
## Version explanation ##
|
||||
|
||||
Use this version with Awesome v3.4.x. If you are using the git pre-4.0 version of Awesome, please consider using [this](https://github.com/alexander-yakushev/awesompd/tree/for-awesome-git) version instead.
|
||||
|
||||
### Changes in 1.1.0 ###
|
||||
|
||||
* Album covers are now also shown for the local tracks (images are taken from the current track's folder)
|
||||
* When the Jamendo track is playing you can visit artist's or album's page from the Jamendo menu
|
||||
* Notification now shows the album name for the current track (for both local and Jamendo tracks)
|
||||
* A few minor modifications and bugfixes
|
||||
|
|
@ -1,67 +0,0 @@
|
|||
-- Asynchronous io.popen for Awesome WM.
|
||||
-- How to use...
|
||||
-- ...asynchronously:
|
||||
-- asyncshell.request('wscript -Kiev', function(f) wwidget.text = f:read("*l") end)
|
||||
-- ...synchronously
|
||||
-- wwidget.text = asyncshell.demand('wscript -Kiev', 5):read("*l") or "Error"
|
||||
|
||||
asyncshell = {}
|
||||
asyncshell.request_table = {}
|
||||
asyncshell.id_counter = 0
|
||||
asyncshell.folder = "/tmp/asyncshell"
|
||||
asyncshell.file_template = asyncshell.folder .. '/req'
|
||||
|
||||
-- Create a directory for asynchell response files
|
||||
os.execute("mkdir -p " .. asyncshell.folder)
|
||||
|
||||
-- Returns next tag - unique identifier of the request
|
||||
local function next_id()
|
||||
asyncshell.id_counter = (asyncshell.id_counter + 1) % 100000
|
||||
return asyncshell.id_counter
|
||||
end
|
||||
|
||||
-- Sends an asynchronous request for an output of the shell command.
|
||||
-- @param command Command to be executed and taken output from
|
||||
-- @param callback Function to be called when the command finishes
|
||||
-- @return Request ID
|
||||
function asyncshell.request(command, callback)
|
||||
local id = next_id()
|
||||
local tmpfname = asyncshell.file_template .. id
|
||||
asyncshell.request_table[id] = {callback = callback}
|
||||
local req =
|
||||
string.format("bash -c '%s > %s; " ..
|
||||
'echo "asyncshell.deliver(%s)" | ' ..
|
||||
"awesome-client' 2> /dev/null",
|
||||
string.gsub(command, "'", "'\\''"), tmpfname, id, tmpfname)
|
||||
awful.util.spawn(req)
|
||||
return id
|
||||
end
|
||||
|
||||
-- Calls the remembered callback function on the output of the shell
|
||||
-- command.
|
||||
-- @param id Request ID
|
||||
-- @param output The output file of the shell command to be delievered
|
||||
function asyncshell.deliver(id)
|
||||
if asyncshell.request_table[id] and
|
||||
asyncshell.request_table[id].callback then
|
||||
local output = io.open(asyncshell.file_template .. id, 'r')
|
||||
asyncshell.request_table[id].callback(output)
|
||||
end
|
||||
end
|
||||
|
||||
-- Sends a synchronous request for an output of the command. Waits for
|
||||
-- the output, but if the given timeout expires returns nil.
|
||||
-- @param command Command to be executed and taken output from
|
||||
-- @param timeout Maximum amount of time to wait for the result
|
||||
-- @return File handler on success, nil otherwise
|
||||
function asyncshell.demand(command, timeout)
|
||||
local id = next_id()
|
||||
local tmpfname = asyncshell.file_template .. id
|
||||
local f = io.popen(string.format("(%s > %s; echo asyncshell_done) & " ..
|
||||
"(sleep %s; echo asynchell_timeout)",
|
||||
command, tmpfname, timeout))
|
||||
local result = f:read("*line")
|
||||
if result == "asyncshell_done" then
|
||||
return io.open(tmpfname)
|
||||
end
|
||||
end
|
Before Width: | Height: | Size: 241 B |
Before Width: | Height: | Size: 18 KiB |
Before Width: | Height: | Size: 242 B |
Before Width: | Height: | Size: 210 B |
Before Width: | Height: | Size: 234 B |
Before Width: | Height: | Size: 248 B |
Before Width: | Height: | Size: 240 B |
Before Width: | Height: | Size: 342 B |
Before Width: | Height: | Size: 210 B |
|
@ -1,524 +0,0 @@
|
|||
---------------------------------------------------------------------------
|
||||
-- @author Alexander Yakushev <yakushev.alex@gmail.com>
|
||||
-- @copyright 2011 Alexander Yakushev
|
||||
-- @release v1.1.5
|
||||
---------------------------------------------------------------------------
|
||||
|
||||
-- Grab environment
|
||||
local os = os
|
||||
local awful = awful
|
||||
local string = string
|
||||
local table = table
|
||||
local io = io
|
||||
local pairs = pairs
|
||||
local type = type
|
||||
local assert = assert
|
||||
local print = print
|
||||
local tonumber = tonumber
|
||||
local math = math
|
||||
local tostring = tostring
|
||||
local asyncshell = asyncshell
|
||||
|
||||
module('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
|
||||
|
||||
-- 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" }
|
||||
ORDER_RELEVANCE = { display = "None (consecutive)",
|
||||
short_display = "none",
|
||||
value = "searchweight_desc" }
|
||||
SEARCH_ARTIST = { display = "Artist",
|
||||
unit = "artist",
|
||||
value = "artist_id" }
|
||||
SEARCH_ALBUM = { display = "Album",
|
||||
unit = "album",
|
||||
value = "album_id" }
|
||||
SEARCH_TAG = { display = "Tag",
|
||||
unit = "tag",
|
||||
value = "tag_id" }
|
||||
ALL_FORMATS = { FORMAT_MP3, FORMAT_OGG }
|
||||
ALL_ORDERS = { ORDER_RELEVANCE, ORDER_RANDOM, ORDER_RATINGDAILY,
|
||||
ORDER_RATINGWEEKLY, ORDER_RATINGTOTAL }
|
||||
|
||||
current_request_table = { unit = "track",
|
||||
fields = {"id", "artist_url", "artist_name", "name",
|
||||
"stream", "album_image", "album_name" },
|
||||
joins = { "track_album", "album_artist" },
|
||||
params = { streamencoding = FORMAT_MP3,
|
||||
order = 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 =
|
||||
perform_request("echo $(curl -w %{redirect_url} " ..
|
||||
"'http://api.jamendo.com/get2/stream/track/redirect/" ..
|
||||
"?streamencoding="..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 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 get_track_by_link(link)
|
||||
local id = 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 replace_link(track_name)
|
||||
local track = 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 return_track_table(request_table)
|
||||
local req_string = form_request(request_table)
|
||||
local response = perform_request(req_string)
|
||||
if not response then
|
||||
return nil -- Bad internet connection
|
||||
end
|
||||
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].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
|
||||
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 form_request(request_table)
|
||||
local curl_str = "curl -A 'Mozilla/4.0' -fsm 5 \"%s\""
|
||||
local url = "http://api.jamendo.com/en/?m=get2%s%s"
|
||||
request_table = request_table or current_request_table
|
||||
|
||||
local fields = request_table.fields or current_request_table.fields
|
||||
local joins = request_table.joins or current_request_table.joins
|
||||
local unit = request_table.unit or current_request_table.unit
|
||||
|
||||
-- Form field&joins string (like field1+field2+fieldN%2Fjoin+)
|
||||
local fnj_string = "&m_params="
|
||||
for i = 1, table.getn(fields) do
|
||||
fnj_string = fnj_string .. fields[i] .. "+"
|
||||
end
|
||||
fnj_string = string.sub(fnj_string,1,string.len(fnj_string)-1)
|
||||
|
||||
fnj_string = fnj_string .. "%2F" .. unit .. "%2Fjson%2F"
|
||||
for i = 1, table.getn(joins) do
|
||||
fnj_string = fnj_string .. joins[i] .. "+"
|
||||
end
|
||||
fnj_string = fnj_string .. "%2F"
|
||||
|
||||
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 ~= current_request_table.params then
|
||||
-- First fill params with current_request_table parameters
|
||||
for k, v in pairs(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 = 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, fnj_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 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 = 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)
|
||||
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.mod(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.mod(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.mod(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 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 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, string.len(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 get_album_cover(track_id)
|
||||
local file_path, fetch_req = 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 get_album_cover_async(track_id)
|
||||
local file_path, fetch_req = 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 try_get_cover(track_name)
|
||||
local id = get_id_from_link(track_name)
|
||||
if id then
|
||||
return get_album_cover(id)
|
||||
end
|
||||
end
|
||||
|
||||
-- Same as try_get_cover, but calls get_album_cover_async inside.
|
||||
function try_get_cover_async(track_name)
|
||||
local id = get_id_from_link(track_name)
|
||||
if id then
|
||||
return 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 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 = perform_request(form_request(req))
|
||||
if resp then
|
||||
local search_res = 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 = 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 perform_request(reqest_string)
|
||||
local bus = assert(io.popen(reqest_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 string.len(response) == 0 then
|
||||
bus = assert(io.popen(reqest_string,'r'))
|
||||
response = bus:read("*all")
|
||||
bus:close()
|
||||
-- If it still can't read anything, return nil
|
||||
if string.len(response) ~= 0 then
|
||||
return nil
|
||||
end
|
||||
end
|
||||
return response
|
||||
end
|
||||
|
||||
-- Sets default streamencoding in current_request_table.
|
||||
function set_current_format(format)
|
||||
current_request_table.params.streamencoding = format
|
||||
end
|
||||
|
||||
-- Sets default order in current_request_table.
|
||||
function set_current_order(order)
|
||||
current_request_table.params.order = order
|
||||
end
|
|
@ -1,407 +0,0 @@
|
|||
-- Standard awesome library
|
||||
require("awful")
|
||||
require("awful.autofocus")
|
||||
require("awful.rules")
|
||||
-- Theme handling library
|
||||
require("beautiful")
|
||||
-- Notification library
|
||||
require("naughty")
|
||||
|
||||
-- Load Debian menu entries
|
||||
require("debian.menu")
|
||||
|
||||
-- {{{ Variable definitions
|
||||
-- Themes define colours, icons, and wallpapers
|
||||
beautiful.init("/usr/share/awesome/themes/default/theme.lua")
|
||||
|
||||
-- This is used later as the default terminal and editor to run.
|
||||
terminal = "x-terminal-emulator"
|
||||
editor = os.getenv("EDITOR") or "editor"
|
||||
editor_cmd = terminal .. " -e " .. editor
|
||||
|
||||
-- Default modkey.
|
||||
-- Usually, Mod4 is the key with a logo between Control and Alt.
|
||||
-- If you do not like this or do not have such a key,
|
||||
-- I suggest you to remap Mod4 to another key using xmodmap or other tools.
|
||||
-- However, you can use another modifier like Mod1, but it may interact with others.
|
||||
modkey = "Mod4"
|
||||
|
||||
-- Table of layouts to cover with awful.layout.inc, order matters.
|
||||
layouts =
|
||||
{
|
||||
awful.layout.suit.floating,
|
||||
awful.layout.suit.tile,
|
||||
awful.layout.suit.tile.left,
|
||||
awful.layout.suit.tile.bottom,
|
||||
awful.layout.suit.tile.top,
|
||||
awful.layout.suit.fair,
|
||||
awful.layout.suit.fair.horizontal,
|
||||
awful.layout.suit.spiral,
|
||||
awful.layout.suit.spiral.dwindle,
|
||||
awful.layout.suit.max,
|
||||
awful.layout.suit.max.fullscreen,
|
||||
awful.layout.suit.magnifier
|
||||
}
|
||||
-- }}}
|
||||
|
||||
-- {{{ Tags
|
||||
-- Define a tag table which hold all screen tags.
|
||||
tags = {}
|
||||
for s = 1, screen.count() do
|
||||
-- Each screen has its own tag table.
|
||||
tags[s] = awful.tag({ 1, 2, 3, 4, 5, 6, 7, 8, 9 }, s, layouts[1])
|
||||
end
|
||||
-- }}}
|
||||
|
||||
-- {{{ Menu
|
||||
-- Create a laucher widget and a main menu
|
||||
myawesomemenu = {
|
||||
{ "manual", terminal .. " -e man awesome" },
|
||||
{ "edit config", editor_cmd .. " " .. awful.util.getdir("config") .. "/rc.lua" },
|
||||
{ "restart", awesome.restart },
|
||||
{ "quit", awesome.quit }
|
||||
}
|
||||
|
||||
mymainmenu = awful.menu({ items = { { "awesome", myawesomemenu, beautiful.awesome_icon },
|
||||
{ "Debian", debian.menu.Debian_menu.Debian },
|
||||
{ "open terminal", terminal }
|
||||
}
|
||||
})
|
||||
|
||||
mylauncher = awful.widget.launcher({ image = image(beautiful.awesome_icon),
|
||||
menu = mymainmenu })
|
||||
-- }}}
|
||||
|
||||
-- {{{ Wibox
|
||||
-- Create a textclock widget
|
||||
mytextclock = awful.widget.textclock({ align = "right" })
|
||||
|
||||
-- Create a systray
|
||||
mysystray = widget({ type = "systray" })
|
||||
|
||||
-- BEGIN OF AWESOMPD WIDGET DECLARATION
|
||||
|
||||
require('awesompd/awesompd')
|
||||
|
||||
musicwidget = awesompd:create() -- Create awesompd widget
|
||||
musicwidget.font = "Liberation Mono" -- Set widget font
|
||||
musicwidget.scrolling = true -- If true, the text in the widget will be scrolled
|
||||
musicwidget.output_size = 30 -- Set the size of widget in symbols
|
||||
musicwidget.update_interval = 10 -- Set the update interval in seconds
|
||||
|
||||
-- Set the folder where icons are located (change username to your login name)
|
||||
musicwidget.path_to_icons = "/home/username/.config/awesome/icons"
|
||||
|
||||
-- Set the default music format for Jamendo streams. You can change
|
||||
-- this option on the fly in awesompd itself.
|
||||
-- possible formats: awesompd.FORMAT_MP3, awesompd.FORMAT_OGG
|
||||
musicwidget.jamendo_format = awesompd.FORMAT_MP3
|
||||
|
||||
-- Specify the browser you use so awesompd can open links from
|
||||
-- Jamendo in it.
|
||||
musicwidget.browser = "firefox"
|
||||
|
||||
-- If true, song notifications for Jamendo tracks and local tracks
|
||||
-- will also contain album cover image.
|
||||
musicwidget.show_album_cover = true
|
||||
|
||||
-- Specify how big in pixels should an album cover be. Maximum value
|
||||
-- is 100.
|
||||
musicwidget.album_cover_size = 50
|
||||
|
||||
-- This option is necessary if you want the album covers to be shown
|
||||
-- for your local tracks.
|
||||
musicwidget.mpd_config = "/home/username/.mpdconf"
|
||||
|
||||
-- Specify decorators on the left and the right side of the
|
||||
-- widget. Or just leave empty strings if you decorate the widget
|
||||
-- from outside.
|
||||
musicwidget.ldecorator = " "
|
||||
musicwidget.rdecorator = " "
|
||||
|
||||
-- Set all the servers to work with (here can be any servers you use)
|
||||
musicwidget.servers = {
|
||||
{ server = "localhost",
|
||||
port = 6600 },
|
||||
{ server = "192.168.0.72",
|
||||
port = 6600 }
|
||||
}
|
||||
|
||||
-- Set the buttons of the widget. Keyboard keys are working in the
|
||||
-- entire Awesome environment. Also look at the line 352.
|
||||
musicwidget:register_buttons({ { "", awesompd.MOUSE_LEFT, musicwidget:command_playpause() },
|
||||
{ "Control", awesompd.MOUSE_SCROLL_UP, musicwidget:command_prev_track() },
|
||||
{ "Control", awesompd.MOUSE_SCROLL_DOWN, musicwidget:command_next_track() },
|
||||
{ "", awesompd.MOUSE_SCROLL_UP, musicwidget:command_volume_up() },
|
||||
{ "", awesompd.MOUSE_SCROLL_DOWN, musicwidget:command_volume_down() },
|
||||
{ "", awesompd.MOUSE_RIGHT, musicwidget:command_show_menu() },
|
||||
{ "", "XF86AudioLowerVolume", musicwidget:command_volume_down() },
|
||||
{ "", "XF86AudioRaiseVolume", musicwidget:command_volume_up() },
|
||||
{ modkey, "Pause", musicwidget:command_playpause() } })
|
||||
|
||||
musicwidget:run() -- After all configuration is done, run the widget
|
||||
|
||||
-- END OF AWESOMPD WIDGET DECLARATION
|
||||
-- Don't forget to add the widget to the wibox. It is done on the line 216.
|
||||
|
||||
mywibox = {}
|
||||
mypromptbox = {}
|
||||
mylayoutbox = {}
|
||||
mytaglist = {}
|
||||
mytaglist.buttons = awful.util.table.join(
|
||||
awful.button({ }, 1, awful.tag.viewonly),
|
||||
awful.button({ modkey }, 1, awful.client.movetotag),
|
||||
awful.button({ }, 3, awful.tag.viewtoggle),
|
||||
awful.button({ modkey }, 3, awful.client.toggletag),
|
||||
awful.button({ }, 4, awful.tag.viewnext),
|
||||
awful.button({ }, 5, awful.tag.viewprev)
|
||||
)
|
||||
mytasklist = {}
|
||||
mytasklist.buttons = awful.util.table.join(
|
||||
awful.button({ }, 1, function (c)
|
||||
if not c:isvisible() then
|
||||
awful.tag.viewonly(c:tags()[1])
|
||||
end
|
||||
client.focus = c
|
||||
c:raise()
|
||||
end),
|
||||
awful.button({ }, 3, function ()
|
||||
if instance then
|
||||
instance:hide()
|
||||
instance = nil
|
||||
else
|
||||
instance = awful.menu.clients({ width=250 })
|
||||
end
|
||||
end),
|
||||
awful.button({ }, 4, function ()
|
||||
awful.client.focus.byidx(1)
|
||||
if client.focus then client.focus:raise() end
|
||||
end),
|
||||
awful.button({ }, 5, function ()
|
||||
awful.client.focus.byidx(-1)
|
||||
if client.focus then client.focus:raise() end
|
||||
end))
|
||||
|
||||
for s = 1, screen.count() do
|
||||
-- Create a promptbox for each screen
|
||||
mypromptbox[s] = awful.widget.prompt({ layout = awful.widget.layout.horizontal.leftright })
|
||||
-- Create an imagebox widget which will contains an icon indicating which layout we're using.
|
||||
-- We need one layoutbox per screen.
|
||||
mylayoutbox[s] = awful.widget.layoutbox(s)
|
||||
mylayoutbox[s]:buttons(awful.util.table.join(
|
||||
awful.button({ }, 1, function () awful.layout.inc(layouts, 1) end),
|
||||
awful.button({ }, 3, function () awful.layout.inc(layouts, -1) end),
|
||||
awful.button({ }, 4, function () awful.layout.inc(layouts, 1) end),
|
||||
awful.button({ }, 5, function () awful.layout.inc(layouts, -1) end)))
|
||||
-- Create a taglist widget
|
||||
mytaglist[s] = awful.widget.taglist(s, awful.widget.taglist.label.all, mytaglist.buttons)
|
||||
|
||||
-- Create a tasklist widget
|
||||
mytasklist[s] = awful.widget.tasklist(function(c)
|
||||
return awful.widget.tasklist.label.currenttags(c, s)
|
||||
end, mytasklist.buttons)
|
||||
|
||||
-- Create the wibox
|
||||
mywibox[s] = awful.wibox({ position = "top", screen = s })
|
||||
-- Add widgets to the wibox - order matters
|
||||
mywibox[s].widgets = {
|
||||
{
|
||||
mylauncher,
|
||||
mytaglist[s],
|
||||
mypromptbox[s],
|
||||
layout = awful.widget.layout.horizontal.leftright
|
||||
},
|
||||
mylayoutbox[s],
|
||||
mytextclock,
|
||||
musicwidget.widget, -- Awesompd widget is added like this
|
||||
s == 1 and mysystray or nil,
|
||||
mytasklist[s],
|
||||
layout = awful.widget.layout.horizontal.rightleft
|
||||
}
|
||||
end
|
||||
-- }}}
|
||||
|
||||
-- {{{ Mouse bindings
|
||||
root.buttons(awful.util.table.join(
|
||||
awful.button({ }, 3, function () mymainmenu:toggle() end),
|
||||
awful.button({ }, 4, awful.tag.viewnext),
|
||||
awful.button({ }, 5, awful.tag.viewprev)
|
||||
))
|
||||
-- }}}
|
||||
|
||||
-- {{{ Key bindings
|
||||
globalkeys = awful.util.table.join(
|
||||
awful.key({ modkey, }, "Left", awful.tag.viewprev ),
|
||||
awful.key({ modkey, }, "Right", awful.tag.viewnext ),
|
||||
awful.key({ modkey, }, "Escape", awful.tag.history.restore),
|
||||
|
||||
awful.key({ modkey, }, "j",
|
||||
function ()
|
||||
awful.client.focus.byidx( 1)
|
||||
if client.focus then client.focus:raise() end
|
||||
end),
|
||||
awful.key({ modkey, }, "k",
|
||||
function ()
|
||||
awful.client.focus.byidx(-1)
|
||||
if client.focus then client.focus:raise() end
|
||||
end),
|
||||
awful.key({ modkey, }, "w", function () mymainmenu:show({keygrabber=true}) end),
|
||||
|
||||
-- Layout manipulation
|
||||
awful.key({ modkey, "Shift" }, "j", function () awful.client.swap.byidx( 1) end),
|
||||
awful.key({ modkey, "Shift" }, "k", function () awful.client.swap.byidx( -1) end),
|
||||
awful.key({ modkey, "Control" }, "j", function () awful.screen.focus_relative( 1) end),
|
||||
awful.key({ modkey, "Control" }, "k", function () awful.screen.focus_relative(-1) end),
|
||||
awful.key({ modkey, }, "u", awful.client.urgent.jumpto),
|
||||
awful.key({ modkey, }, "Tab",
|
||||
function ()
|
||||
awful.client.focus.history.previous()
|
||||
if client.focus then
|
||||
client.focus:raise()
|
||||
end
|
||||
end),
|
||||
|
||||
-- Standard program
|
||||
awful.key({ modkey, }, "Return", function () awful.util.spawn(terminal) end),
|
||||
awful.key({ modkey, "Control" }, "r", awesome.restart),
|
||||
awful.key({ modkey, "Shift" }, "q", awesome.quit),
|
||||
|
||||
awful.key({ modkey, }, "l", function () awful.tag.incmwfact( 0.05) end),
|
||||
awful.key({ modkey, }, "h", function () awful.tag.incmwfact(-0.05) end),
|
||||
awful.key({ modkey, "Shift" }, "h", function () awful.tag.incnmaster( 1) end),
|
||||
awful.key({ modkey, "Shift" }, "l", function () awful.tag.incnmaster(-1) end),
|
||||
awful.key({ modkey, "Control" }, "h", function () awful.tag.incncol( 1) end),
|
||||
awful.key({ modkey, "Control" }, "l", function () awful.tag.incncol(-1) end),
|
||||
awful.key({ modkey, }, "space", function () awful.layout.inc(layouts, 1) end),
|
||||
awful.key({ modkey, "Shift" }, "space", function () awful.layout.inc(layouts, -1) end),
|
||||
|
||||
-- Prompt
|
||||
awful.key({ modkey }, "r", function () mypromptbox[mouse.screen]:run() end),
|
||||
|
||||
awful.key({ modkey }, "x",
|
||||
function ()
|
||||
awful.prompt.run({ prompt = "Run Lua code: " },
|
||||
mypromptbox[mouse.screen].widget,
|
||||
awful.util.eval, nil,
|
||||
awful.util.getdir("cache") .. "/history_eval")
|
||||
end)
|
||||
)
|
||||
|
||||
clientkeys = awful.util.table.join(
|
||||
awful.key({ modkey, }, "f", function (c) c.fullscreen = not c.fullscreen end),
|
||||
awful.key({ modkey, "Shift" }, "c", function (c) c:kill() end),
|
||||
awful.key({ modkey, "Control" }, "space", awful.client.floating.toggle ),
|
||||
awful.key({ modkey, "Control" }, "Return", function (c) c:swap(awful.client.getmaster()) end),
|
||||
awful.key({ modkey, }, "o", awful.client.movetoscreen ),
|
||||
awful.key({ modkey, "Shift" }, "r", function (c) c:redraw() end),
|
||||
awful.key({ modkey, }, "t", function (c) c.ontop = not c.ontop end),
|
||||
awful.key({ modkey, }, "n", function (c) c.minimized = not c.minimized end),
|
||||
awful.key({ modkey, }, "m",
|
||||
function (c)
|
||||
c.maximized_horizontal = not c.maximized_horizontal
|
||||
c.maximized_vertical = not c.maximized_vertical
|
||||
end)
|
||||
)
|
||||
|
||||
-- Compute the maximum number of digit we need, limited to 9
|
||||
keynumber = 0
|
||||
for s = 1, screen.count() do
|
||||
keynumber = math.min(9, math.max(#tags[s], keynumber));
|
||||
end
|
||||
|
||||
-- Bind all key numbers to tags.
|
||||
-- Be careful: we use keycodes to make it works on any keyboard layout.
|
||||
-- This should map on the top row of your keyboard, usually 1 to 9.
|
||||
for i = 1, keynumber do
|
||||
globalkeys = awful.util.table.join(globalkeys,
|
||||
awful.key({ modkey }, "#" .. i + 9,
|
||||
function ()
|
||||
local screen = mouse.screen
|
||||
if tags[screen][i] then
|
||||
awful.tag.viewonly(tags[screen][i])
|
||||
end
|
||||
end),
|
||||
awful.key({ modkey, "Control" }, "#" .. i + 9,
|
||||
function ()
|
||||
local screen = mouse.screen
|
||||
if tags[screen][i] then
|
||||
awful.tag.viewtoggle(tags[screen][i])
|
||||
end
|
||||
end),
|
||||
awful.key({ modkey, "Shift" }, "#" .. i + 9,
|
||||
function ()
|
||||
if client.focus and tags[client.focus.screen][i] then
|
||||
awful.client.movetotag(tags[client.focus.screen][i])
|
||||
end
|
||||
end),
|
||||
awful.key({ modkey, "Control", "Shift" }, "#" .. i + 9,
|
||||
function ()
|
||||
if client.focus and tags[client.focus.screen][i] then
|
||||
awful.client.toggletag(tags[client.focus.screen][i])
|
||||
end
|
||||
end))
|
||||
end
|
||||
|
||||
clientbuttons = awful.util.table.join(
|
||||
awful.button({ }, 1, function (c) client.focus = c; c:raise() end),
|
||||
awful.button({ modkey }, 1, awful.mouse.client.move),
|
||||
awful.button({ modkey }, 3, awful.mouse.client.resize))
|
||||
|
||||
-- Set keys
|
||||
-- Add this line before root.keys(globalkeys).
|
||||
musicwidget:append_global_keys()
|
||||
|
||||
root.keys(globalkeys)
|
||||
-- }}}
|
||||
|
||||
-- {{{ Rules
|
||||
awful.rules.rules = {
|
||||
-- All clients will match this rule.
|
||||
{ rule = { },
|
||||
properties = { border_width = beautiful.border_width,
|
||||
border_color = beautiful.border_normal,
|
||||
focus = true,
|
||||
keys = clientkeys,
|
||||
buttons = clientbuttons } },
|
||||
{ rule = { class = "MPlayer" },
|
||||
properties = { floating = true } },
|
||||
{ rule = { class = "pinentry" },
|
||||
properties = { floating = true } },
|
||||
{ rule = { class = "gimp" },
|
||||
properties = { floating = true } },
|
||||
-- Set Firefox to always map on tags number 2 of screen 1.
|
||||
-- { rule = { class = "Firefox" },
|
||||
-- properties = { tag = tags[1][2] } },
|
||||
}
|
||||
-- }}}
|
||||
|
||||
-- {{{ Signals
|
||||
-- Signal function to execute when a new client appears.
|
||||
client.add_signal("manage", function (c, startup)
|
||||
-- Add a titlebar
|
||||
-- awful.titlebar.add(c, { modkey = modkey })
|
||||
|
||||
-- Enable sloppy focus
|
||||
c:add_signal("mouse::enter", function(c)
|
||||
if awful.layout.get(c.screen) ~= awful.layout.suit.magnifier
|
||||
and awful.client.focus.filter(c) then
|
||||
client.focus = c
|
||||
end
|
||||
end)
|
||||
|
||||
if not startup then
|
||||
-- Set the windows at the slave,
|
||||
-- i.e. put it at the end of others instead of setting it master.
|
||||
-- awful.client.setslave(c)
|
||||
|
||||
-- Put windows in a smart way, only if they does not set an initial position.
|
||||
if not c.size_hints.user_position and not c.size_hints.program_position then
|
||||
awful.placement.no_overlap(c)
|
||||
awful.placement.no_offscreen(c)
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
client.add_signal("focus", function(c) c.border_color = beautiful.border_focus end)
|
||||
client.add_signal("unfocus", function(c) c.border_color = beautiful.border_normal end)
|
||||
-- }}}
|
|
@ -1,158 +0,0 @@
|
|||
-- Provides UTF-8 aware string functions implemented in pure lua:
|
||||
-- * string.utf8len(s)
|
||||
-- * string.utf8sub(s, i, j)
|
||||
--
|
||||
-- All functions behave as their non UTF-8 aware counterparts with the exception
|
||||
-- that UTF-8 characters are used instead of bytes for all units.
|
||||
--
|
||||
-- Note: all validations had been removed due to awesome usage specifics.
|
||||
--[[
|
||||
Copyright (c) 2006-2007, Kyle Smith
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice,
|
||||
this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
* Neither the name of the author nor the names of its contributors may be
|
||||
used to endorse or promote products derived from this software without
|
||||
specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
--]]
|
||||
|
||||
-- ABNF from RFC 3629
|
||||
--
|
||||
-- UTF8-octets = *( UTF8-char )
|
||||
-- UTF8-char = UTF8-1 / UTF8-2 / UTF8-3 / UTF8-4
|
||||
-- UTF8-1 = %x00-7F
|
||||
-- UTF8-2 = %xC2-DF UTF8-tail
|
||||
-- UTF8-3 = %xE0 %xA0-BF UTF8-tail / %xE1-EC 2( UTF8-tail ) /
|
||||
-- %xED %x80-9F UTF8-tail / %xEE-EF 2( UTF8-tail )
|
||||
-- UTF8-4 = %xF0 %x90-BF 2( UTF8-tail ) / %xF1-F3 3( UTF8-tail ) /
|
||||
-- %xF4 %x80-8F 2( UTF8-tail )
|
||||
-- UTF8-tail = %x80-BF
|
||||
--
|
||||
|
||||
-- returns the number of bytes used by the UTF-8 character at byte i in s
|
||||
-- also doubles as a UTF-8 character validator
|
||||
function utf8charbytes (s, i)
|
||||
-- argument defaults
|
||||
i = i or 1
|
||||
local c = string.byte(s, i)
|
||||
|
||||
-- determine bytes needed for character, based on RFC 3629
|
||||
if c > 0 and c <= 127 then
|
||||
-- UTF8-1
|
||||
return 1
|
||||
elseif c >= 194 and c <= 223 then
|
||||
-- UTF8-2
|
||||
local c2 = string.byte(s, i + 1)
|
||||
return 2
|
||||
elseif c >= 224 and c <= 239 then
|
||||
-- UTF8-3
|
||||
local c2 = s:byte(i + 1)
|
||||
local c3 = s:byte(i + 2)
|
||||
return 3
|
||||
elseif c >= 240 and c <= 244 then
|
||||
-- UTF8-4
|
||||
local c2 = s:byte(i + 1)
|
||||
local c3 = s:byte(i + 2)
|
||||
local c4 = s:byte(i + 3)
|
||||
return 4
|
||||
end
|
||||
end
|
||||
|
||||
-- returns the number of characters in a UTF-8 string
|
||||
function utf8len (s)
|
||||
local pos = 1
|
||||
local bytes = string.len(s)
|
||||
local len = 0
|
||||
|
||||
while pos <= bytes and len ~= chars do
|
||||
local c = string.byte(s,pos)
|
||||
len = len + 1
|
||||
|
||||
pos = pos + utf8charbytes(s, pos)
|
||||
end
|
||||
|
||||
if chars ~= nil then
|
||||
return pos - 1
|
||||
end
|
||||
|
||||
return len
|
||||
end
|
||||
|
||||
-- functions identically to string.sub except that i and j are UTF-8 characters
|
||||
-- instead of bytes
|
||||
function utf8sub (s, i, j)
|
||||
j = j or -1
|
||||
|
||||
if i == nil then
|
||||
return ""
|
||||
end
|
||||
|
||||
local pos = 1
|
||||
local bytes = string.len(s)
|
||||
local len = 0
|
||||
|
||||
-- only set l if i or j is negative
|
||||
local l = (i >= 0 and j >= 0) or utf8len(s)
|
||||
local startChar = (i >= 0) and i or l + i + 1
|
||||
local endChar = (j >= 0) and j or l + j + 1
|
||||
|
||||
-- can't have start before end!
|
||||
if startChar > endChar then
|
||||
return ""
|
||||
end
|
||||
|
||||
-- byte offsets to pass to string.sub
|
||||
local startByte, endByte = 1, bytes
|
||||
|
||||
while pos <= bytes do
|
||||
len = len + 1
|
||||
|
||||
if len == startChar then
|
||||
startByte = pos
|
||||
end
|
||||
|
||||
pos = pos + utf8charbytes(s, pos)
|
||||
|
||||
if len == endChar then
|
||||
endByte = pos - 1
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
return string.sub(s, startByte, endByte)
|
||||
end
|
||||
|
||||
-- replace UTF-8 characters based on a mapping table
|
||||
function utf8replace (s, mapping)
|
||||
local pos = 1
|
||||
local bytes = string.len(s)
|
||||
local charbytes
|
||||
local newstr = ""
|
||||
|
||||
while pos <= bytes do
|
||||
charbytes = utf8charbytes(s, pos)
|
||||
local c = string.sub(s, pos, pos + charbytes - 1)
|
||||
newstr = newstr .. (mapping[c] or c)
|
||||
pos = pos + charbytes
|
||||
end
|
||||
|
||||
return newstr
|
||||
end
|