lua-resty-mpd

一个 OpenResty/Luasocket/cqueues MPD 客户端库

$ opm get jprjr/lua-resty-mpd

lua-resty-mpd

尽管名为“lua-resty-mpd”,但它也适用于普通的 Lua!

这是一个用于通过 TCP 套接字或 Unix 套接字与 Music Player Daemon 交互的库。

它与 OpenResty 的 cosocketscqueues 套接字LuaSocket 兼容。它将尝试自动检测最合适的库,您也可以指定是否要使用特定库。

您可以在 nginx 和 cqueues 中同步或异步使用此库,在 Luasocket 上,您只能执行同步操作。

安装

您可以使用 luarocks

luarocks install lua-resty-mpd

或 OPM

opm get jprjr/lua-resty-mpd

或者从此仓库获取合并文件(在 lib/ 下),它将该模块的所有源代码合并到一个文件中。

由于这可以使用多个套接字库,我没有将其列为依赖项,您需要自行安装 luasocket 或 cqueues。无需其他外部依赖项。

示例用法

这是一个循环调用 idle() 的脚本。

    local mpd = require'resty.mpd'
    local client = mpd()
    client:settimeout(1000) -- set a low timeout just to demo idle timing out.
    client:connect('tcp://127.0.0.1:6600')
    
    -- loop until we've read 5 events
    local events = 5
    
    while events > 0 do
      local res, err = client:idle()
      if err and err ~= 'socket:timeout' then
        print('Error: ' .. err)
        os.exit(1)
      end
      for _,event in ipairs(res) do
        print('Event: ' .. event)
        events = events - 1
        -- do something based on the event
      end
    end
    client:close()
    

这是在 openresty 下使用 nginx 线程的异步用法示例。

    local mpd = require'resty.mpd'
    
    local client = mpd()
    
    -- If MPD isn't running, bail
    
    assert(client:connect('127.0.0.1'))
    
    -- Holds references to our threads
    
    local threads = {}
    
    -- Each entry is the command to run and a
    -- key that should be in the response.
    --
    -- We do this to verify that each thread is
    -- getting the correct response (if we called
    -- "status" but didn't get the "state" key, then
    -- something went really, really wong.
    
    local commands = {
      { 'status', 'state' },
      { 'stats',  'uptime' },
      { 'replay_gain_status','replay_gain_mode'},
    }
    
    -- Start a loop around client:idle().
    -- This will write out any idle events (should be zero
    -- unless you happen to do something to MPD in the
    -- 2 seconds that this script runs for), and
    -- exits if it receives an error.
    table.insert(threads,ngx.thread.spawn(function()
      while true do
        print('calling client:idle()')
        local events, err = client:idle()
        if err and err ~= 'socket:timeout' then
          print(string.format('client:idle() error %s',err))
          return false, err
        end
    
        print(string.format('client:idle() returned %d events',#events))
        for _,event in ipairs(events) do
          print(string.format('client:idle() event: %s',event))
        end
      end
    end))
    
    -- Start threads to send individual commands.
    -- These will interrupt the idle call and force
    -- idle to return zero events.
    for i=1,#commands do
      table.insert(threads,ngx.thread.spawn(function()
        local func = commands[i][1]
        local key  = commands[i][2]
        print(string.format('calling client:%s()',func))
        local res, err = client[func](client)
        if err then
          print(string.format('client:%s() error: %s',func,err))
          return false, err
        end
        if not res[key] then
          err = string.format('missing key %s',key)
          print(string.format('client:%s() error: %s',func,err))
          return false,err
        end
        print(string.format('client:%s() success',func))
        return true
      end))
    end
    
    -- Shut everything down after 2 seconds.
    table.insert(threads,ngx.thread.spawn(function()
      ngx.sleep(2)
      print('calling client:close()')
      local ok, err = client:close()
      if err then
        print(string.format('client:close() err: ' .. err))
      end
    end))
    
    -- Rejoin all the threads
    for i=1,#threads do
      local ok, err = ngx.thread.wait(threads[i])
      if not ok then error(err) end
    end
    

这基本上与 nginx 示例相同,但使用 cqueues。由于它实际上与 nginx 示例相同,因此没有注释。

    local cqueues = require'cqueues'
    local mpd = require'resty.mpd'
    local loop = cqueues.new()
    
    local client = mpd.new()
    assert(client:connect('127.0.0.1'))
    
    local commands = {
      { 'status', 'state' },
      { 'stats',  'uptime' },
      { 'replay_gain_status','replay_gain_mode'},
    }
    
    loop:wrap(function()
      while true do
        print('calling client:idle()')
        local events, err = client:idle()
        if err and err ~= 'socket:timeout' then
          print(string.format('client:idle() error %s',err))
          return false, err
        end
    
        print(string.format('client:idle() returned %d events',#events))
        for _,event in ipairs(events) do
          print(string.format('client:idle() event: %s',event))
        end
      end
    end)
    
    for i=1,#commands do
      loop:wrap((function()
        local func = commands[i][1]
        local key  = commands[i][2]
        print(string.format('calling client:%s()',func))
        local res, err = client[func](client)
        if err then
          print(string.format('client:%s() error: %s',func,err))
          return false, err
        end
    
        if not res[key] then
          err = string.format('missing key %s',key)
          print(string.format('client:%s() error: %s',func,err))
          return false,err
        end
        print(string.format('client:%s() success',func))
        return true
      end))
    end
    
    loop:wrap(function()
      cqueues.sleep(2)
      print('calling client:close()')
      local ok, err = client:close()
      if err then
        print(string.format('client:close() err: ' .. err))
      end
    end)
    
    assert(loop:loop())

全局选项

lib = mpd:backend([name])

返回所有新客户端使用的套接字/条件变量库,name 是一个可选参数,用于选择特定库。有效的 name 值为

  • nginx - nginx cosockets。

  • cqueues - cqueues。

  • luasocket - luasocket。

如果库不可用,它将返回默认库。

返回值是正在使用的库,您可以检查 .name 字段以查看它是哪个特定的库。示例

    lib = mpd:backend('luasocket')
    assert(lib.name == 'luasocket')

实例化客户端

client = mpd()

创建一个新的客户端实例。

您也可以将其称为 mpd.new()

lib = client:backend([name])

返回此特定客户端使用的套接字库,它的行为与上面的 mpd:backend 相同。

  • nginx - nginx cosockets。

  • cqueues - cqueues。

  • luasocket - luasocket。

如果您的客户端已经调用了 connect,您将无法更改库,您需要调用 close,更改库,然后重新连接。

ok, err = client:connect(url)

连接到 MPD,支持 tcp 和 unix 套接字连接。

URL 应该采用以下两种格式之一

  • tcp://host:port

  • unix:/path/to/socket

  • host:port

  • host(隐含端口 6600)

  • tcp://host(隐含端口 6600)

  • path/to/socket(不必是绝对路径)

对于 TCP 连接,您也可以将其称为 client:connect(host,port)

ok, err = client:settimeout(ms)

以毫秒为单位设置套接字超时,或使用 nil 表示无超时。

默认情况下,客户端没有超时,并且将永远阻塞,请注意这包括 nginx/OpenResty 后端。

(从技术上讲,OpenResty 不支持没有超时,因此它被设置为最大值)。

ok, err = client:close()

关闭连接,强制所有挂起的操作出错。

已实现的协议功能和错误处理

我过去曾经列出每个已实现的函数,相反,我建议您直接查阅 MPD 协议文档:https://www.musicpd.org/doc/protocol/command_reference.html

命令返回结果表或布尔值作为第一个返回值,以及错误(如果有)作为第二个返回值。

如果错误来自 MPD,则消息将以字符串 mpd: 开头,后跟错误号,以及括号中的错误消息,例如

mpd:50(No such file)

任何与套接字相关的错误消息都将以 socket: 开头,这些错误是不可恢复的(您应该断开连接/退出等)。idle 除外,见下文。

client:idle()

idle 超时时,它会自动发送 noidle 来取消当前的 idle 请求。否则,您的调度程序(nginx 线程、cqueues 等)可能会在您从应用程序中调用 noidle 之前发送命令,因为当 idle 结束时,下一个排队的命令将被调用。

这意味着 idle 将始终返回一个事件列表,该列表可能是超时情况下为空的表,您应该检查 err 的值以查看是否已超时(如果 errnil,则 idle 是通过另一个排队的命令故意取消的)。

一般用法

一般来说,您只需像 MPD 协议文档中列出的那样发送值。例如,MPD 协议文档对 list 命令有以下原型

list {TYPE} {FILTER} [group {GROUPTYPE}]

这将转换为

    response, err = client:list(type,filter,'group',grouptype)

对于接受范围的函数,您对范围的每个部分使用单独的参数。例如,使用 find 命令,您可以指定 window 范围

find {FILTER} [sort {TYPE}] [window {START:END}]

这将变为

    response, err = client:find(filter,'sort',type,'window',start,end)

对于可选参数,只需将其省略。如果您想仅使用过滤器和窗口调用 find

    response, err = client:find(filter,'window',start,end)

或者仅使用过滤器

    response, err = client:find(filter)

对于使用它们的命令,完全支持组(和嵌套组),组将返回类似数组的表而不是对象,因此,例如

    local res, err = client:list('title','group','album','group','albumartist')

res 将是一个类似数组的表,每个条目将包含一个 titlealbumalbumartist 键。

变更日志

版本 5.2.1

添加了三个缺少的命令

  • addtagid

  • outputset

  • volume

版本 5.2.0

行为更改 - MPD 可以返回具有相同键的多个响应。例如,如果 FLAC 文件列出了多个 COMPOSER 标签,MPD 将返回

    Album: An Album
    Artist: An Artist
    Composer: First Composer
    Composer: Second Composer

在版本 5.2.0 之前,lua-resty-mpd 只会返回最后一个标签,因此您的响应将类似于

    {
      album = "An Album",
      artist = "An Artist",
      composer = "Second Composer",
    }

从版本 5.2.0 开始,如果检测到重复的键,则该值将被转换为表,例如

    {
      album = "An Album",
      artist = "An Artist",
      composer = { "First Composer", "Second Composer" },
    }

版本 5.1.1

添加了一个缺少的命令:deleteid

版本 5.1.0

添加了新的 binarylimit 协议命令。

版本 5.0.1

小错误修复,如果未连接,则返回套接字错误。

版本 5.0.0

完全重写,客户端命令(list、play 等)应与旧版本兼容,但选择后端库的函数则不兼容。

这是为了异步操作而重写的,新版本可以根据需要自动调用 noidle,而无需像版本 3 那样进行任何 hack。

版本 4.0.0

恢复通过 condvar/semaphore 的自动 noidle,事实证明这并不是一个好主意。

保留处理二进制响应并与 MPD 0.22.0 及其更高版本兼容的先前增强功能。

版本 3.0.1

关于条件变量/信号量的错误修复,现在似乎在调用 noidle 时更加可靠。

版本 3.0.0

主要版本升级。

版本 3.0.0 尝试检测是否在等待 IDLE 命令完成时发送了命令,并自动调用 noidle,它通过 nginx 信号量和 cqueues 条件变量来完成此操作。

此新行为在 LuaSocket 上不受支持,您需要自己调用 noidle

还处理二进制响应,并且应该处理截至 MPD 0.22.0 的所有 MPD 协议函数。

版本 2.2.0

新功能,现在支持 cqueues 套接字库。

库使用以下优先级自动检测

  1. nginx cosockets

  2. cqueues

  3. luasocket

这可以在全局级别或每个客户端级别覆盖。

版本 2.1.1

错误修复:在发送时转义引号/反斜杠。

版本 2.1.0

新功能:new 接受一个可选的表,请参阅文档。

版本 2.0.2

修复了 noidle 中的潜在竞争条件。

使用正确的套接字超时比例(luasocket 以秒为单位,nginx 以毫秒为单位)。

版本 2.0.1

修复了超时操作。

版本 2.0.0

重大变更

idle 更改

在以前的版本中,调用 idle 将返回一个字符串,如果使用 noidle 取消了空闲,则使用特殊字符串 (“interrupted”)。在 MPD 中,对 idle 的调用可以返回多个事件。

idle 现在返回一个事件数组,使用空数组表示取消了 idle

commandsnotcommands 更改

以前的版本返回一个表,其中每个命令都是一个设置为 true 的键。

commandsnotcommands 现在返回一个命令数组。

非重大变更

以前的版本要求 URL 匹配以下格式

  • tcp://host:port

  • unix:/path/to/socket

URL 还可以使用以下格式

  • host:port

  • host(隐含端口 6600)

  • tcp://host(隐含端口 6600)

  • path/to/socket(不必是绝对路径)

我仍然建议使用 tcp://unix: 前缀以明确表示

许可证

MIT 许可证(见 LICENSE

作者

John Regan

许可证

mit

依赖项

luajit

版本