From: rvbglas Date: Tue, 26 Dec 2017 09:12:52 +0000 (+0300) Subject: LUA MPD Web Interface for OpenWRT/LEDE systems X-Git-Url: https://git.rvb.name/mpd-lua.git/commitdiff_plain/7e7b0d2b9ec3b3c62370de418cf9b0a4a667b1fb LUA MPD Web Interface for OpenWRT/LEDE systems --- 7e7b0d2b9ec3b3c62370de418cf9b0a4a667b1fb diff --git a/ajax/mpd.js b/ajax/mpd.js new file mode 100644 index 0000000..9cd2667 --- /dev/null +++ b/ajax/mpd.js @@ -0,0 +1,392 @@ +urlbase="mpd.lua?" + +function GetFilename(url) +{ + if (url) + return url.split('/').pop().split('#')[0].split('?')[0]; +} + +function EscapeStr(str) { + res = str.replace(/'/g,"\\'"); + return res; +} + +function RefreshPageStatus() { + + var req = new XMLHttpRequest(); + + req.onreadystatechange = function () { + if (this.readyState != 4 || this.status != 200) return; + var returnedData = JSON.parse(this.responseText); + nowPlaying = GetFilename(returnedData['current_playing']) + document.title='MPD Player: '+nowPlaying; + nowPlaying = (1+Number(returnedData['song'])) + '/' + returnedData['playlistlength'] + ' '+nowPlaying; + if (returnedData['state']=='stop') { + nowPlaying = '' + nowPlaying+ '' + } + document.getElementById('nowplaying_content').innerHTML=nowPlaying; + if (returnedData["state"]=="play") { + document.getElementById('playpausebutton').innerHTML=""; + } else { + document.getElementById('playpausebutton').innerHTML=""; + } + if (returnedData["repeat"]=="1") { + document.getElementById('repeatstate').innerHTML=""; + } else { + document.getElementById('repeatstate').innerHTML=""; + } + document.getElementById('volume_total').innerHTML="
"; + }; + + req.open("GET", urlbase+"status", true); + req.send(); + + +} + +function RefreshPlaylist() { + +var req = new XMLHttpRequest(); + +req.onreadystatechange = function () { + if (this.readyState != 4 || this.status != 200) return; + var returnedData = JSON.parse(this.responseText); + + var text = "
\ + \ + \ + \ +
\ + \ + \ + \ +
\ +
\ +
\ + \ + \ + \ + "; + + var even = 0; + for (var key in returnedData) { + var rec=returnedData[key]; + var name=GetFilename(rec["name"]); + var id=rec["id"]; + + if (even) { + evText="itemEven"; + } else { + evText="itemOdd"; + }; + + even = ! even; + + text = text + "\ + \ + \ + \ + \ + \ + \ + "; + } + + text = text + "
TitleControls
\ + \ + "+name+"\ + \ + \ + \ + \ + \ + \ +
\ +
\ +
\ + \ + \ + \ +
\ + \ + \ + \ +
\ +
"; + + document.getElementById('playlist').innerHTML=text; +}; + +req.open("GET", urlbase+"playlist", true); +req.send(); + +} + +function EditPlayList(dir) { + +var req = new XMLHttpRequest(); + +req.onreadystatechange = function () { + if (this.readyState != 4 || this.status != 200) return; + var returnedData = JSON.parse(this.responseText); + + var text = "
\ + \ + \ + \ +
\ + \ +
\ +
\ +
\ + \ + \ + \ + "; + + if (dir) { + var lastSlash=dir.lastIndexOf("/"); + if (lastSlash>0) { + var upperLevel=dir.slice(0,lastSlash); + } else { + var upperLevel=""; + } + var text = text + "\ + \ + \ + "; + } + + var even = 0; + var i = 0; + for (var key in returnedData) { + var rec=returnedData[key]; + var type=rec["type"]; + var name=rec["name"]; + var lastSlash=name.lastIndexOf("/"); + if (lastSlash>0) { + var tailName=name.slice(lastSlash+1); + } else { + var tailName=name + }; + + if (type == "directory" || type == "file") { + if (even) { + evText="itemEven"; + } else { + evText="itemOdd"; + }; + + i = i + 1; + even = ! even; + + text = text + "\ + \ + "; + + if (type == "directory") { + text = text + ""; + }; + + if (type == "file") { + text = text + ""; + }; + + text = text + ""; + + } + + } + + var text = text+"
TitleControls
\ + \ + "+tailName+"\ + \ + "+tailName+"\ +
\ +
\ + \ + \ + \ +
\ + \ +
\ +
"; + document.getElementById('playlist').innerHTML=text; +}; + +if (!dir) { dir = ''; }; + +req.open("GET", urlbase+"lists|edit|"+dir, true); +req.send(); + +} + +function LoadPlayList() { + +var req = new XMLHttpRequest(); + +req.onreadystatechange = function () { + if (this.readyState != 4 || this.status != 200) return; + var returnedData = JSON.parse(this.responseText); + text="
\ + \ + \ + \ +
\ + \ +
\ +
\ +
\ + \ + \ + \ + "; + + var even = 0; + for (var key in returnedData) { + var name=returnedData[key]; + + if (even) { + evText="itemEven"; + } else { + evText="itemOdd"; + }; + + even = ! even; + + text = text + "\ + \ + \ + \ + "; + } + + text=text+"
NameControls
"+name+"
\ + \ + \ + \ +
\ + \ +
\ +
"; + + document.getElementById('playlist').innerHTML=text; +}; + +req.open("GET", urlbase+"lists|load", true); +req.send(); + +} + +function SavePlayList() { + +var name=window.prompt('List name',''); + +var req = new XMLHttpRequest(); + +req.onreadystatechange = function () { + if (this.readyState != 4 || this.status != 200) return; + if (this.responseText != 'OK') { + window.alert(this.responseText); + } +}; + +req.open("GET", urlbase+"lists|save|"+name, true); +req.send(); + +} + +function DelPlayList(item) { + +var req = new XMLHttpRequest(); + +req.onreadystatechange = function () { + if (this.readyState != 4 || this.status != 200) return; + RefreshPageStatus(); + LoadPlayList(); +}; + +req.open("GET", urlbase+"lists|delete|"+item, true); +req.send(); + +} + +function RefreshPageContent() { + + RefreshPageStatus(); + RefreshPlaylist(); + +} + +function Command(cmd) { + +var req = new XMLHttpRequest(); + +req.onreadystatechange = function () { + if (this.readyState != 4 || this.status != 200) return; + RefreshPageStatus(); +}; + +req.open("GET", urlbase+cmd, true); +req.send(); + +} + +function PlaylistCommand(cmd,item) { + +var req = new XMLHttpRequest(); + +req.onreadystatechange = function () { + if (this.readyState != 4 || this.status != 200) return; + RefreshPageContent(); +}; + +req.open("GET", urlbase+"cpl|"+cmd+"|"+item, true); +req.send(); + +} + +function PlaylistCommandRefStatus(cmd,item) { + +var req = new XMLHttpRequest(); + +req.onreadystatechange = function () { + if (this.readyState != 4 || this.status != 200) return; + RefreshPageStatus(); +}; + +req.open("GET", urlbase+"cpl|"+cmd+"|"+item, true); +req.send(); + +} + +function PlaylistEditCommand(cmd,item) { + +var req = new XMLHttpRequest(); + +req.onreadystatechange = function () { + if (this.readyState != 4 || this.status != 200) return; + RefreshPageStatus(); +}; + +req.open("GET", urlbase+"lists|"+cmd+"|"+item, true); +req.send(); + +} + +function PlaylistEditCommandRefFull(cmd,item) { + +var req = new XMLHttpRequest(); + +req.onreadystatechange = function () { + if (this.readyState != 4 || this.status != 200) return; + RefreshPageContent(); +}; + +req.open("GET", urlbase+"lists|"+cmd+"|"+item, true); +req.send(); + +} + +setInterval(RefreshPageStatus, 10000); diff --git a/css/general.css b/css/general.css new file mode 100644 index 0000000..6bb13cc --- /dev/null +++ b/css/general.css @@ -0,0 +1,201 @@ +/* +#525356 +*/ + + + +* { + margin:0; +} + +body { + text-align: center; + font-family: arial,sans-serif; + margin-top: 15px; + margin-bottom: 15px; + color: #AEAEAE; +} + +a { + text-decoration: none; + color: #AEAEAE; +} + +table +{ + border-style: none; + padding: 0px; + border-collapse: collapse; + border-spacing: 0; +} + +img{ + border: 0px; + margin: 2px 2px 2px 2px; +} + +button +{ + background: none; + cursor: pointer; + border: 0px; + margin: 0px; + padding: 0px; +} + +.button +{ + cursor: pointer; +} + +.trackno +{ + text-align: right; + vertical-align: top; + padding: 0px 0px 5px 0px; +} + +#heading_tbl { + width: 400px +} + +#frame { + text-align: left; + border: 1px solid #000; + width: 400px; + background: url(../images/bg.png) no-repeat #251616; + margin: 0 auto; +} + + +#nowplaying_heading { + font-size: 1.3em; + font-wieght: bold; + height: 30px; + background: url(../images/control_bg.png) repeat-x; + border-bottom: 1px solid #525356; +} + +#nowplaying_content { + margin: 10px 5px 10px 5px; + text-align: center; + border: 1px solid #525356; + width: 380px; + height: 60px; + padding: 10px 0px 5px 0px; + background: url(../images/playing_bg.png); + font-weight: bold; + color: #fff; +} + +#control_buttons { + text-align: center; + width: 390px; + margin: 10px 5px 10px 5px; +} + +#control_buttons table { + margin-left: auto; + margin-right: auto; +} + + +#control_volume { + text-align: center; + width: 390px; + margin: 10px 5px 10px 5px; +} + +#control_volume table { + margin-left: auto; + margin-right: auto; +} + +#playlist_menu { + font-wieght: bold; + height: 24px; + background: url(../images/control_bg.png) repeat-x; + border-top: 2px solid #525356; + margin-top: 5px; + margin-bottom: 5px; +} + +#items_heading { + text-align: center; + font-size: 1.0em; + font-weight:bold; + border-bottom: 1px solid #525356; + border-top: 1px solid #525356; + background-color: #333; +} + +#home { + text-align: left; + font-size: 1.0em; + font-weight: bold; + border-bottom: 3px solid #525356; + background-color: #251616; +} + +#items { + font-size: 0.9em; + text-align: center; + min-height: 400px; + max-height: 600px; + background: url(../images/playing_bg.png); + overflow-y: scroll; +} + +#items table { + width: 380px; + margin: 0px 5px 0px 5px; + border: 1px solid #525356; +} + +#file { + text-align: left; + width: 100%; + +} + + +#itemEven{ + background-color: #251616; + +} + +#itemOdd{ + background-color: #333; +} + +#itemActive { + background: url(../images/playing_bg.png); + color: #fff; +} + +#itemActive a { + color: #fff; +} + + +#move img { + margin: 1px 1px 1px 1px; +} + +#remove img { + margin: 1px 1px 1px 1px; +} + + +#volume_total { + width: 100px; + border: 1px solid #525356; + height: 5px; +} + + +#volume_actual { + background-color: #d9600c; + height:5px; +} + diff --git a/images/addall.png b/images/addall.png new file mode 100755 index 0000000..fa3983e Binary files /dev/null and b/images/addall.png differ diff --git a/images/addselected.png b/images/addselected.png new file mode 100755 index 0000000..52fbb78 Binary files /dev/null and b/images/addselected.png differ diff --git a/images/bg.png b/images/bg.png new file mode 100755 index 0000000..5bd1ec8 Binary files /dev/null and b/images/bg.png differ diff --git a/images/control_bg.png b/images/control_bg.png new file mode 100755 index 0000000..685abbb Binary files /dev/null and b/images/control_bg.png differ diff --git a/images/down.png b/images/down.png new file mode 100755 index 0000000..e8f3d6e Binary files /dev/null and b/images/down.png differ diff --git a/images/highlight.png b/images/highlight.png new file mode 100755 index 0000000..3710d71 Binary files /dev/null and b/images/highlight.png differ diff --git a/images/left.png b/images/left.png new file mode 100755 index 0000000..00714b7 Binary files /dev/null and b/images/left.png differ diff --git a/images/lists.png b/images/lists.png new file mode 100644 index 0000000..cbae301 Binary files /dev/null and b/images/lists.png differ diff --git a/images/minus.png b/images/minus.png new file mode 100755 index 0000000..dff37f6 Binary files /dev/null and b/images/minus.png differ diff --git a/images/next.png b/images/next.png new file mode 100755 index 0000000..fc2c791 Binary files /dev/null and b/images/next.png differ diff --git a/images/pause.png b/images/pause.png new file mode 100755 index 0000000..9508f98 Binary files /dev/null and b/images/pause.png differ diff --git a/images/play.png b/images/play.png new file mode 100755 index 0000000..3591973 Binary files /dev/null and b/images/play.png differ diff --git a/images/playing_bg.png b/images/playing_bg.png new file mode 100755 index 0000000..f2e2337 Binary files /dev/null and b/images/playing_bg.png differ diff --git a/images/playlist.png b/images/playlist.png new file mode 100755 index 0000000..ae4af14 Binary files /dev/null and b/images/playlist.png differ diff --git a/images/plus.png b/images/plus.png new file mode 100755 index 0000000..b72d579 Binary files /dev/null and b/images/plus.png differ diff --git a/images/prev.png b/images/prev.png new file mode 100755 index 0000000..aedbbdf Binary files /dev/null and b/images/prev.png differ diff --git a/images/previous.png b/images/previous.png new file mode 100755 index 0000000..9c49139 Binary files /dev/null and b/images/previous.png differ diff --git a/images/remall.png b/images/remall.png new file mode 100755 index 0000000..ffb546a Binary files /dev/null and b/images/remall.png differ diff --git a/images/remove.png b/images/remove.png new file mode 100755 index 0000000..bf94562 Binary files /dev/null and b/images/remove.png differ diff --git a/images/removeall.png b/images/removeall.png new file mode 100755 index 0000000..4fe02d1 Binary files /dev/null and b/images/removeall.png differ diff --git a/images/removeselected.png b/images/removeselected.png new file mode 100755 index 0000000..35294aa Binary files /dev/null and b/images/removeselected.png differ diff --git a/images/remselected.png b/images/remselected.png new file mode 100755 index 0000000..6466dce Binary files /dev/null and b/images/remselected.png differ diff --git a/images/repeatoff.png b/images/repeatoff.png new file mode 100755 index 0000000..0a17659 Binary files /dev/null and b/images/repeatoff.png differ diff --git a/images/repeaton.png b/images/repeaton.png new file mode 100755 index 0000000..aaef8ce Binary files /dev/null and b/images/repeaton.png differ diff --git a/images/right.png b/images/right.png new file mode 100755 index 0000000..2ce6d36 Binary files /dev/null and b/images/right.png differ diff --git a/images/save.png b/images/save.png new file mode 100644 index 0000000..37ec086 Binary files /dev/null and b/images/save.png differ diff --git a/images/songs.png b/images/songs.png new file mode 100755 index 0000000..f24c1ef Binary files /dev/null and b/images/songs.png differ diff --git a/images/stop.png b/images/stop.png new file mode 100755 index 0000000..1e1dd0f Binary files /dev/null and b/images/stop.png differ diff --git a/images/up.png b/images/up.png new file mode 100755 index 0000000..0ada34a Binary files /dev/null and b/images/up.png differ diff --git a/images/update.png b/images/update.png new file mode 100755 index 0000000..fa2242b Binary files /dev/null and b/images/update.png differ diff --git a/index.html b/index.html new file mode 100644 index 0000000..5df6090 --- /dev/null +++ b/index.html @@ -0,0 +1,85 @@ + + + + + MPD Player + + + + + +
+ +
+ +
+ + + + + +
Now playing
+
+ +
+
+ +
+ + + + + + + + +
+
+ +
+ + + + + + +
Volume:
+
+
+ +
+ +
+ + + +
+ + + + +
+
+ +
+
+ +
+ + + +
+ + + + +
+
+ +
+ +
+ + diff --git a/mpd.lua b/mpd.lua new file mode 100755 index 0000000..0200a8a --- /dev/null +++ b/mpd.lua @@ -0,0 +1,275 @@ +#!/usr/bin/lua + +function url_decode(str) + if not str then return nil end + str = string.gsub (str, "+", " ") + str = string.gsub (str, "%%(%x%x)", function(h) return + string.char(tonumber(h,16)) end) + str = string.gsub (str, "\r\n", "\n") + return str +end + +function split(s, delimiter) + local result = {}; + for match in (s..delimiter):gmatch("(.-)"..delimiter) do + result[#result+1] = match; + end + return result; +end + +function string.starts(String,Start) + return string.sub(String,1,string.len(Start))==Start +end + +function process_playlist(playlist) + local res={} + for _,record in pairs(playlist) do + local splitted = split(record,": ") + local name = splitted[2] + local splitted = split(splitted[1],":") + local id = splitted[1] + local rectype = splitted[2] + local rec = {} + if not (id == "OK") then + rec["id"] = id + rec["type"] = rectype + rec["name"] = name + res[id] = rec + end + end + return res +end + +function process_playlists(playlists) + local res={} + for _,record in pairs(playlists) do + local splitted = split(record,": ") + if splitted[1]=="playlist" then + res[#res+1] = splitted[2] + end + end + return res +end + +function process_directory(directory) + local res={} + for _,record in pairs(directory) do + local splitted = split(record,": ") + if splitted[1]=="directory" or splitted[1]=="file" then + local rec={} + rec["type"] = splitted[1] + rec["name"] = splitted[2] + res[#res+1] = rec + end + end + return res +end + +require "uci" +require("socket") + +function mpd_new(settings) + local client = {} + if settings == nil then settings = {} end + + client.hostname = settings.hostname or "localhost" + client.port = settings.port or 6600 + client.desc = settings.desc or client.hostname + client.password = settings.password + client.timeout = settings.timeout or 1 + client.retry = settings.retry or 60 + + return client +end + + +function mpd_send(mpd,action,raw) + + local command = string.format("%s\n", action) + local values = {} + + -- connect to MPD server if not already done. + if not mpd.connected then + local now = os.time(); + if not mpd.last_try or (now - mpd.last_try) > mpd.retry then + mpd.socket = socket.tcp() + mpd.socket:settimeout(mpd.timeout, 't') + mpd.last_try = os.time() + mpd.connected = mpd.socket:connect(mpd.hostname, mpd.port) + if not mpd.connected then + return { errormsg = "could not connect" } + end + mpd.last_error = nil + + -- Read the server's hello message + local line = mpd.socket:receive("*l") + if not line:match("^OK MPD") then -- Invalid hello message? + mpd.connected = false + return { errormsg = string.format("invalid hello message: %s", line) } + else + _, _, mpd.version = string.find(line, "^OK MPD ([0-9.]+)") + end + + -- send the password if needed + if mpd.password then + local rsp = mpd_send(mpd,string.format("password %s", mpd.password)) + if rsp.errormsg then + return rsp + end + end + else + local retry_sec = mpd.retry - (now - mpd.last_try) + return { errormsg = string.format("%s (retrying in %d sec)", mpd.last_error, retry_sec) } + end + end + + mpd.socket:send(command) + + local line = ""; err=0 + while not line:match("^OK$") do + line, err = mpd.socket:receive("*l") + if not line then -- closed,timeout (mpd killed?) + mpd.last_error = err + mpd.connected = false + mpd.socket:close() + return mpd_send(mpd,action) + end + + if line:match("^ACK") then + return { errormsg = line } + end + + if not raw then + + local pattern = string.format("(%s)", ": ") + local i = string.find (line, pattern, 0) + + if i ~= nil then + local key=string.sub(line,1,i-1) + local value=string.sub(line,i+2,-1) + values[string.lower(key)] = value + end + + else + + values[#values+1]=line + + end + end + + return values +end + + +json=require("json") + +x = uci.cursor() +settings = {} +settings['host'] = x.get("mpd","server","host") or "localhost" +settings['port'] = x.get("mpd","server","port") or 6600 +password = x.get("mpd","server","password") +if password then + settings["password"] = password +end + +m = mpd_new(settings) + +command = url_decode(os.getenv('QUERY_STRING')) + +if not command or command=="" then + command="status" +end + +if command=="play" or command=="pause" or command=="stop" or command=="previous" or command=="next" then + res=mpd_send(m,command) +elseif command=="vold" then + status=mpd_send(m,"status") + volume=tonumber(status["volume"]) + res=mpd_send(m,"setvol "..(volume-3)) +elseif command=="volu" then + status=mpd_send(m,"status") + volume=tonumber(status["volume"]) + res=mpd_send(m,"setvol "..(volume+3)) +elseif command=="status" then + res=mpd_send(m,"status") + song=res["song"] + playlist=mpd_send(m,"playlist",1) + pl=process_playlist(playlist) + if song then + res['current_playing']=pl[song]['name'] + else + res['current_playing']="No songs selected" + end +elseif command=="playlist" then + playlist=mpd_send(m,"playlist",1) + res=process_playlist(playlist) +elseif command=="repeat" then + status=mpd_send(m,"status") + rep=1-status["repeat"] + res=mpd_send(m,"repeat "..rep) +elseif string.starts(command,"cpl") then + cmd=split(command,"|") + id=cmd[3] + command=cmd[2] + if command=="playitem" then + command="play "..id + res=mpd_send(m,command) + end + if command=="clear" then + res=mpd_send(m,"clear") + end + if command=="remove" then + command="delete "..id + res=mpd_send(m,command) + end + if command=="moveup" then + command="swap "..id.." "..(id-1) + res=mpd_send(m,command) + end + if command=="movedown" then + command="swap "..id.." "..(id+1) + res=mpd_send(m,command) + end +elseif string.starts(command,"lists") then + cmd=split(command,"|") + command=cmd[2] + if command=="load" then + if not cmd[3] then + lists=mpd_send(m,"listplaylists",1) + res=process_playlists(lists) + else + res=mpd_send(m,"load "..cmd[3],1) + end + end + if command=="save" and cmd[3] then + res=mpd_send(m,"save "..cmd[3],1) + end + if command=="delete" and cmd[3] then + res=mpd_send(m,"rm "..cmd[3],1) + end + if command=="edit" then + if cmd[3] then + dir=mpd_send(m,"lsinfo \""..cmd[3].."\"",1) + else + dir=mpd_send(m,"lsinfo",1) + end + res=process_directory(dir) + end + if command=="add" then + res={} + if cmd[3] then + res=mpd_send(m,"add \""..cmd[3].."\"") + end + end +end + +if not res then + print("Content-Type: text/plain\r\n") + print("MPD server - unknown command "..command) +elseif (res['error_msg']) then + print("Content-Type: text/plain\r\n") + print("MPD server connection error: "..res['error_msg']) +else + print "Content-Type: text/plain\r\n" + print(json.encode(res)) +end