602 lines
15 KiB
Lua
602 lines
15 KiB
Lua
![]() |
-------------------------------------------------------------------
|
||
|
-- Copyright (c) 2006 Stephen M. Jothen
|
||
|
-- All rights reserved.
|
||
|
--
|
||
|
-- Redistribution and use in source and binary forms, with or without
|
||
|
-- modification, are permitted provided that the following conditions
|
||
|
-- are met:
|
||
|
-- 1. Redistributions of source code must retain the above copyright
|
||
|
-- notice, this list of conditions and the following disclaimer.
|
||
|
-- 2. 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.
|
||
|
-- 3. The name of the author may not be used to endorse or promote products
|
||
|
-- derived from this software without specific prior written permission.
|
||
|
--
|
||
|
-- THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``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 AUTHOR 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.
|
||
|
-------------------------------------------------------------------
|
||
|
-- LuaMPD - Lua interface to the MusicPD protocol
|
||
|
--
|
||
|
-- Author: Steve Jothen <sjothen at gmail dot com>
|
||
|
-------------------------------------------------------------------
|
||
|
|
||
|
------------------------------------------------
|
||
|
-- Classes and requires
|
||
|
------------------------------------------------
|
||
|
|
||
|
local socket = require("socket")
|
||
|
local luampd = {}
|
||
|
|
||
|
------------------------------------------------
|
||
|
-- Private functions and tables
|
||
|
------------------------------------------------
|
||
|
|
||
|
-- errors taken from ack.h in the mpd sources
|
||
|
local err_table = {
|
||
|
["1"] = "ACK_ERROR_NOT_LIST",
|
||
|
["2"] = "ACK_ERROR_ARG",
|
||
|
["3"] = "ACK_ERROR_PASSWORD",
|
||
|
["4"] = "ACK_ERROR_PERMISSION",
|
||
|
["5"] = "ACK_ERROR_UNKNOWN",
|
||
|
|
||
|
["50"] = "ACK_ERROR_NO_EXIST",
|
||
|
["51"] = "ACK_ERROR_PLAYLIST_MAX",
|
||
|
["52"] = "ACK_ERROR_SYSTEM",
|
||
|
["53"] = "ACK_ERROR_PLAYLIST_LOAD",
|
||
|
["54"] = "ACK_ERROR_UPDATE_ALREADY",
|
||
|
["55"] = "ACK_ERROR_PLAYER_SYNC",
|
||
|
["56"] = "ACK_ERROR_EXIST",
|
||
|
}
|
||
|
|
||
|
-- turns a table of strings into groups seperated by a
|
||
|
-- delimiting string, and then turns each group of lines
|
||
|
-- into key, value pair tables
|
||
|
local function multi_hash(delimiter, lines)
|
||
|
local size = table.maxn(lines)
|
||
|
local patches = {}
|
||
|
local patch = {}
|
||
|
|
||
|
while size >= 1 do
|
||
|
|
||
|
if string.find(lines[size], delimiter) then
|
||
|
-- add to the patches repository
|
||
|
local k, v = socket.skip(2, string.find(lines[size], "(.+):%s(.+)"))
|
||
|
|
||
|
if k and v then
|
||
|
-- we have our key value pair!
|
||
|
patch[string.lower(k)] = v
|
||
|
-- we want the key to be lowercase
|
||
|
end
|
||
|
|
||
|
table.insert(patches, patch)
|
||
|
patch = {}
|
||
|
|
||
|
else
|
||
|
-- were still modifying the current patch
|
||
|
local k, v = socket.skip(2, string.find(lines[size], "(.+):%s(.+)"))
|
||
|
|
||
|
if k and v then
|
||
|
patch[string.lower(k)] = v
|
||
|
end
|
||
|
|
||
|
end
|
||
|
|
||
|
size = size - 1
|
||
|
|
||
|
end
|
||
|
|
||
|
return patches
|
||
|
end
|
||
|
|
||
|
-- will turn a table of strings that match
|
||
|
-- key: value
|
||
|
-- into a table such as { key = value, key2 = value2 }
|
||
|
local function hash(lines)
|
||
|
local hash = {}
|
||
|
for key, value in pairs(lines) do
|
||
|
local k, v = socket.skip(2, string.find(value, "(.+):%s(.+)"))
|
||
|
if k and v then
|
||
|
-- lowercase the key for easier access (tabl.artist rather than tabl.Artist)
|
||
|
hash[string.lower(k)] = v
|
||
|
end
|
||
|
end
|
||
|
return hash
|
||
|
end
|
||
|
|
||
|
|
||
|
local function handle_error(line)
|
||
|
-- line is the actual error line received from mpd
|
||
|
local err_num, com_num = socket.skip(2, string.find(line, "ACK %[(%d+)@(%d+)%]"))
|
||
|
if err_table[err_num] then
|
||
|
error(err_table[err_num])
|
||
|
else
|
||
|
-- unknown error
|
||
|
error(err_table["5"])
|
||
|
end
|
||
|
end
|
||
|
|
||
|
local function get_response(obj, command)
|
||
|
|
||
|
-- collect the lines into this table
|
||
|
local response_lines = {}
|
||
|
|
||
|
if obj.socket then
|
||
|
obj.socket:send(command .. '\n')
|
||
|
-- send the command with newline and collect response
|
||
|
-- a response is everything up until an ACK or OK
|
||
|
local line = obj.socket:receive("*l")
|
||
|
while (line ~= "OK") do
|
||
|
if line == nil then
|
||
|
-- bug?? on ubuntu mpd will close socket randomly
|
||
|
-- with large playlists while listing them
|
||
|
-- try playlistinfo (with playlist size > 13000)
|
||
|
error("SOCKET_ERROR")
|
||
|
elseif string.sub(line, 1, 3) == "ACK" then
|
||
|
break
|
||
|
-- let us handle this erro
|
||
|
end
|
||
|
table.insert(response_lines, line)
|
||
|
line = obj.socket:receive("*l")
|
||
|
end
|
||
|
-- if we stopped on an ACK then we better return information about it..
|
||
|
-- last line in the table will be the ack message
|
||
|
if string.sub(line, 1, 3) == "ACK" then
|
||
|
handle_error(line)
|
||
|
--elseif line == "OK" then
|
||
|
-- return true
|
||
|
else
|
||
|
return response_lines
|
||
|
end
|
||
|
else
|
||
|
-- not connected??
|
||
|
error("SOCKET_ERROR")
|
||
|
end
|
||
|
|
||
|
end
|
||
|
|
||
|
------------------------------------------------
|
||
|
-- Public instance methods
|
||
|
------------------------------------------------
|
||
|
|
||
|
function luampd:new(info)
|
||
|
|
||
|
-- local instance of object
|
||
|
|
||
|
local instance = {
|
||
|
hostname = info.hostname or "localhost",
|
||
|
password = info.password or nil,
|
||
|
port = info.port or 6600,
|
||
|
debug = info.debug or false,
|
||
|
}
|
||
|
|
||
|
instance.socket = socket.connect(instance.hostname, instance.port)
|
||
|
|
||
|
if instance.socket then
|
||
|
-- try and get ok from the server
|
||
|
local line = instance.socket:receive("*l")
|
||
|
if line:find("OK MPD (.+)") then
|
||
|
-- connected, send password
|
||
|
if instance.password then
|
||
|
instance.socket:send(string.format("password %s\n", self.password))
|
||
|
-- correct password?
|
||
|
local ok_pass = instance.socket:receive("*l")
|
||
|
if ok_pass ~= "OK" then
|
||
|
error(string.format("Wrong password to %s:d", instance.hostname, instance.port))
|
||
|
end
|
||
|
end
|
||
|
else
|
||
|
-- not mpd or wrong hostname
|
||
|
error(string.format("Cant get response from %s:%d", instance.hostname, instance.port))
|
||
|
end
|
||
|
else
|
||
|
-- socket.connect returns nil, somethings wrong
|
||
|
error(string.format("Socket cant connect to %s:%d", instance.hostname, instance.port))
|
||
|
end
|
||
|
|
||
|
return setmetatable(instance, { __index = luampd })
|
||
|
|
||
|
end
|
||
|
|
||
|
------------------------------------------------
|
||
|
-- Admin functions
|
||
|
------------------------------------------------
|
||
|
|
||
|
-- Some of these functions are in the documentation but dont work?
|
||
|
-- disableoutput, enableoutput
|
||
|
function luampd:disableoutput(outputid)
|
||
|
get_response(self, string.format("disableoutput %d", outputid))
|
||
|
end
|
||
|
|
||
|
function luampd:enableoutput(outputid)
|
||
|
get_response(self, string.format("enableoutput %d", outputid))
|
||
|
end
|
||
|
|
||
|
function luampd:kill()
|
||
|
get_response(self, "kill")
|
||
|
self.socket = nil
|
||
|
end
|
||
|
|
||
|
function luampd:outputs()
|
||
|
local response = get_response(self, "outputs")
|
||
|
return hash(response)
|
||
|
end
|
||
|
|
||
|
function luampd:update()
|
||
|
get_response(self, "update")
|
||
|
end
|
||
|
|
||
|
------------------------------------------------
|
||
|
-- Database functions
|
||
|
------------------------------------------------
|
||
|
|
||
|
function luampd:find(stype, swhat)
|
||
|
local send_string = string.format("find %s \"%s\"", stype, swhat)
|
||
|
local data = get_response(self, send_string)
|
||
|
return multi_hash("^file:", data)
|
||
|
end
|
||
|
|
||
|
-- how should we handle these functions?
|
||
|
-- list/listall/lsinfo
|
||
|
function luampd:list(meta, meta2, search)
|
||
|
end
|
||
|
|
||
|
function luampd:listall(path)
|
||
|
end
|
||
|
|
||
|
function luampd:listallinfo(path)
|
||
|
local send_string
|
||
|
if path then
|
||
|
send_string = string.format("listallinfo \"%s\"", path)
|
||
|
else
|
||
|
send_string = "listallinfo"
|
||
|
end
|
||
|
local response = get_response(self, send_string)
|
||
|
local songs = multi_hash("file:(.+)", response)
|
||
|
return songs
|
||
|
end
|
||
|
|
||
|
function luampd:lsinfo(path)
|
||
|
end
|
||
|
|
||
|
function luampd:search(stype, swhat)
|
||
|
local send_string = string.format("search %s \"%s\"", stype, swhat)
|
||
|
local data = get_response(self, send_string)
|
||
|
return multi_hash("^file:", data)
|
||
|
end
|
||
|
|
||
|
------------------------------------------------
|
||
|
-- Playlist functions
|
||
|
------------------------------------------------
|
||
|
|
||
|
function luampd:add(file)
|
||
|
local add = string.format("add \"%s\"", file)
|
||
|
get_response(self, add)
|
||
|
end
|
||
|
|
||
|
function luampd:clear()
|
||
|
get_response(self, "clear")
|
||
|
end
|
||
|
|
||
|
function luampd:currentsong()
|
||
|
local song = get_response(self, "currentsong")
|
||
|
local hash = hash(song)
|
||
|
return hash
|
||
|
end
|
||
|
|
||
|
function luampd:delete(song)
|
||
|
get_response(self, string.format("delete %d", song))
|
||
|
end
|
||
|
|
||
|
function luampd:deleteid(songid)
|
||
|
get_response(self, string.format("deleteid %s", songid))
|
||
|
end
|
||
|
|
||
|
function luampd:load_playlist(path)
|
||
|
get_response(self, string.format("load %s", path))
|
||
|
end
|
||
|
|
||
|
function luampd:move(from, to)
|
||
|
get_response(self, string.format("move %d %d", from, to))
|
||
|
end
|
||
|
|
||
|
function luampd:moveid(fromid, toid)
|
||
|
get_response(self, string.format("moveid %d %d", fromid, toid))
|
||
|
end
|
||
|
|
||
|
function luampd:playlistinfo(song)
|
||
|
local send_string
|
||
|
if song then
|
||
|
send_string = string.format("playlistinfo %d", song)
|
||
|
local response = get_response(self, send_string)
|
||
|
return hash(response)
|
||
|
else
|
||
|
send_string = "playlistinfo"
|
||
|
local response = get_response(self, send_string)
|
||
|
return multi_hash("^file:", response)
|
||
|
end
|
||
|
end
|
||
|
|
||
|
function luampd:playlistid(songid)
|
||
|
local send_string
|
||
|
if songid then
|
||
|
send_string = string.format("playlistid %d", songid)
|
||
|
local response = get_response(self, send_string)
|
||
|
return hash(response)
|
||
|
else
|
||
|
send_string = "playlistid"
|
||
|
local response = get_response(self, send_string)
|
||
|
return multi_hash("^file:", response)
|
||
|
end
|
||
|
end
|
||
|
|
||
|
function luampd:plchanges(version)
|
||
|
local send_string = string.format("plchanges %d", version)
|
||
|
local response = get_response(self, send_string)
|
||
|
return multi_hash(response)
|
||
|
end
|
||
|
|
||
|
function luampd:plchangesposid(version)
|
||
|
end
|
||
|
|
||
|
function luampd:rm(name)
|
||
|
get_response(self,
|
||
|
string.format("rm \"%s\"", name))
|
||
|
end
|
||
|
|
||
|
function luampd:save(name)
|
||
|
get_response(self,
|
||
|
string.format("save \"%s\"", name))
|
||
|
end
|
||
|
|
||
|
function luampd:shuffle()
|
||
|
get_response(self, "shuffle")
|
||
|
end
|
||
|
|
||
|
function luampd:swap(song1, song2)
|
||
|
get_response(self,
|
||
|
string.format("swap %d %d", song1, song2))
|
||
|
end
|
||
|
|
||
|
function luampd:swapid(songid1, songid2)
|
||
|
get_response(self,
|
||
|
string.format("swapid %d %d", songid1, songid2))
|
||
|
end
|
||
|
|
||
|
------------------------------------------------
|
||
|
-- Playback functions
|
||
|
------------------------------------------------
|
||
|
|
||
|
-- Some of the common tables:
|
||
|
--
|
||
|
-- Status:
|
||
|
-- Fields: volume, repeat, random, playlist, playlistlength, xfade,
|
||
|
-- state, song, songid, time, bitrate, audio
|
||
|
--
|
||
|
-- CurrentSong:
|
||
|
-- Fields: file, artist, album, track, title, time, pos, id
|
||
|
|
||
|
-- sets the crossfading to seconds and returns the xfade status
|
||
|
--
|
||
|
function luampd:crossfade(seconds)
|
||
|
get_response(self,
|
||
|
string.format("crossfade %d", seconds))
|
||
|
return self:status()
|
||
|
end
|
||
|
|
||
|
-- go to next song, returns song table
|
||
|
--
|
||
|
function luampd:next_()
|
||
|
get_response(self, "next")
|
||
|
return self:currentsong()
|
||
|
end
|
||
|
|
||
|
-- pause the current song, returns true/false
|
||
|
-- (play, pause, stop, etc)
|
||
|
--
|
||
|
function luampd:pause()
|
||
|
get_response(self, "pause")
|
||
|
if self:status()["state"] == "pause" then
|
||
|
return true
|
||
|
else
|
||
|
return false
|
||
|
end
|
||
|
end
|
||
|
|
||
|
-- starts playing, returns the current song as a table
|
||
|
--
|
||
|
function luampd:play()
|
||
|
get_response(self, "play")
|
||
|
return self:currentsong()
|
||
|
end
|
||
|
|
||
|
-- start playing a song by its song id
|
||
|
--
|
||
|
function luampd:playid(songid)
|
||
|
get_response(self,
|
||
|
string.format("playid %d", songid))
|
||
|
return self:currentsong()
|
||
|
end
|
||
|
|
||
|
-- go to the previous song and return the current song
|
||
|
--
|
||
|
function luampd:previous()
|
||
|
get_response(self, "previous")
|
||
|
return self:currentsong()
|
||
|
end
|
||
|
|
||
|
-- sets random on or off (state should be either
|
||
|
-- 1 (for on) or 0 (for off))
|
||
|
--
|
||
|
function luampd:random(state)
|
||
|
get_response(self,
|
||
|
string.format("random %d", state))
|
||
|
if self:state()["random"] == tostring(state) then
|
||
|
return true
|
||
|
else
|
||
|
return false
|
||
|
end
|
||
|
end
|
||
|
|
||
|
-- sets repeat (see above about state)
|
||
|
--
|
||
|
function luampd:repeat_(state)
|
||
|
get_response(self,
|
||
|
string.format("repeat %d", state))
|
||
|
if self:state()["repeat"] == tostring(state) then
|
||
|
return true
|
||
|
else
|
||
|
return false
|
||
|
end
|
||
|
end
|
||
|
|
||
|
-- seeks to the specified time (in seconds) in the song
|
||
|
-- returns status
|
||
|
function luampd:seek(song, time)
|
||
|
get_response(self,
|
||
|
string.format("seek %d %d", song, time))
|
||
|
return self:status()
|
||
|
end
|
||
|
|
||
|
-- same as above, except uses songid instead of song
|
||
|
-- returns status
|
||
|
function luampd:seekid(songid, time)
|
||
|
get_response(self,
|
||
|
string.format("seekid %d %d", songid, time))
|
||
|
return self:status()
|
||
|
end
|
||
|
|
||
|
-- set the volume (0-100), anything lower or higher should be handled
|
||
|
-- by the server (-10 will become 0, 110 will become 100)
|
||
|
-- returns status object
|
||
|
--
|
||
|
function luampd:setvol(vol)
|
||
|
get_response(self,
|
||
|
string.format("setvol %d", vol))
|
||
|
if self:status()["volume"] == tostring(vol) then
|
||
|
return true
|
||
|
else
|
||
|
return false
|
||
|
end
|
||
|
end
|
||
|
|
||
|
-- stops playing and returns the current status object
|
||
|
function luampd:stop()
|
||
|
get_response(self, "stop")
|
||
|
if self:status()["state"] == "stop" then
|
||
|
return true
|
||
|
else
|
||
|
return false
|
||
|
end
|
||
|
end
|
||
|
|
||
|
------------------------------------------------
|
||
|
-- Miscellaneous functions
|
||
|
------------------------------------------------
|
||
|
|
||
|
function luampd:clearerror()
|
||
|
get_response(self, "clearerror")
|
||
|
end
|
||
|
|
||
|
--
|
||
|
function luampd:close()
|
||
|
get_response(self, "close")
|
||
|
self.socket = nil
|
||
|
end
|
||
|
|
||
|
function luampd:commands()
|
||
|
end
|
||
|
|
||
|
function luampd:notcommands()
|
||
|
end
|
||
|
|
||
|
-- wrong password handled in get_response
|
||
|
function luampd:password(pass)
|
||
|
get_response(self,
|
||
|
string.format("password %s", pass))
|
||
|
end
|
||
|
|
||
|
function luampd:ping()
|
||
|
get_response(self, "ping")
|
||
|
end
|
||
|
|
||
|
function luampd:stats()
|
||
|
local stats = get_response(self, "stats")
|
||
|
return hash(stats)
|
||
|
end
|
||
|
|
||
|
function luampd:status()
|
||
|
local status = get_response(self, "status")
|
||
|
return hash(status)
|
||
|
end
|
||
|
|
||
|
------------------------------------------------
|
||
|
-- More lua-esque functions, iterators
|
||
|
------------------------------------------------
|
||
|
-- 2011-03-15 crater2150: let iterators return size, which is already calculated
|
||
|
-- anyway
|
||
|
|
||
|
function luampd:database()
|
||
|
local dbase = self:listallinfo(nil)
|
||
|
local count = 0
|
||
|
local size = table.maxn(dbase)
|
||
|
return function()
|
||
|
count = count + 1
|
||
|
if count <= size then
|
||
|
return dbase[count]
|
||
|
end
|
||
|
end, size
|
||
|
end
|
||
|
|
||
|
function luampd:playlist()
|
||
|
local plist = self:playlistinfo(nil)
|
||
|
local count = 0
|
||
|
local size = table.maxn(plist)
|
||
|
return function()
|
||
|
count = count + 1
|
||
|
if count <= size then
|
||
|
return plist[count]
|
||
|
end
|
||
|
end, size
|
||
|
end
|
||
|
|
||
|
function luampd:ifind(stype, swhat)
|
||
|
local found = self:find(stype, swhat)
|
||
|
local count = 0
|
||
|
local size = table.maxn(found)
|
||
|
return function()
|
||
|
count = count + 1
|
||
|
if count <= size then
|
||
|
return found[count]
|
||
|
end
|
||
|
end, size
|
||
|
end
|
||
|
|
||
|
function luampd:isearch(stype, swhat)
|
||
|
local found = self:search(stype, swhat)
|
||
|
local count = 0
|
||
|
local size = table.maxn(found)
|
||
|
return function()
|
||
|
count = count + 1
|
||
|
if count <= size then
|
||
|
return found[count]
|
||
|
end
|
||
|
end, size
|
||
|
end
|
||
|
|
||
|
function luampd:iadd(file_iterator)
|
||
|
for song in file_iterator do
|
||
|
self:add(song.file)
|
||
|
end
|
||
|
end
|
||
|
|
||
|
return luampd
|