1
0
Fork 0
mirror of https://github.com/Reuh/daccord.git synced 2025-10-27 04:39:30 +00:00

Initial commit

This commit is contained in:
Étienne Fildadut 2018-05-20 17:21:40 +02:00
commit 7a5bddaffc
6 changed files with 1617 additions and 0 deletions

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
config.lua
dev

202
LICENSE Normal file
View file

@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

207
classtoi.lua Normal file
View file

@ -0,0 +1,207 @@
--- Reuh's class library version 0.1.4. Lua 5.1-5.3 and LuaJit compatible.
-- Objects and classes behavior are identical, so you can consider this to be somewhat prototype-based.
-- Features:
-- * Multiple inheritance with class(parents...) or someclass(newstuff...)
-- * Every metamethods supported
-- * Everything in a class can be redefined (and will be usable in an object) (except __super)
-- * Preserve parents metamethods if already set
-- * Instanciate with class:new(...)
-- * Test inheritance relations with class/object.is(thing, isThis)
-- * Call object:new(...) on instanciation
-- * If object:new(...) returns non-nil values, they will be returned instead of the instance
-- * Call class.__inherit(class, inheritingClass) when creating a class inheriting the previous class. If class.__inherit returns a value, it will
-- be used as the parent table instead of class, allowing some pretty fancy behavior (it's like an inheritance metamethod).
-- * Implements Class Commons
-- * I don't like to do this, but you can redefine every field and metamethod after class creation (except __index and __super).
-- Not features / Things you may want to know:
-- * Will set the metatable of all parent classes/tables if no metatable is set (the table will be its own metatable).
-- * You can't redefine __super (any __super you define will be only avaible by searching in the default __super contents).
-- * Redefining __super or __index after class creation will break everything (though it should be ok with new, is, __call and everything else).
-- * When creating a new class, the methods new, is, __call, __index and __super will always be redefined, so trying to get theses fields
-- will return the default method and not the one you've defined. However, theses defaults will be replaced by yours automatically on instanciation,
-- except __super and __index, but __index should call your __index and act like you expect. __super will however always be the default one
-- and doesn't proxy in any way yours.
-- * __index metamethods will be called with an extra third argument, which is the current class being searched in the inheritance tree.
-- You can safely ignore it.
--
-- Please also note that the last universal ancestor of the classes (defined here in BaseClass) sets the default __tostring method
-- and __name attribute for nice class-name-printing. Unlike the previoulsy described attributes and methods however, it is done in a normal
-- inheritance-way and can be rewritten without any problem (rewritting __name is especially useful to easily identify your classes).
-- Copyright (c) 2016-2017 Étienne "Reuh" Fildadut <fildadut@reuh.eu>
--
-- Permission is hereby granted, free of charge, to any person obtaining a copy
-- of this software and associated documentation files (the "Software"), to deal
-- in the Software without restriction, including without limitation the rights
-- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-- copies of the Software, and to permit persons to whom the Software is
-- furnished to do so, subject to the following conditions:
--
-- 1. The above copyright notice and this permission notice shall be included in
-- all copies or substantial portions of the Software; and
-- 2. You must cause any modified source files to carry prominent notices stating
-- that you changed the files.
--
-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-- SOFTWARE.
-- Lua versions compatibility
local unpack = table.unpack or unpack
--- All Lua 5.3 metamethods.
local metamethods = {
"__add", "__sub", "__mul", "__div", "__mod", "__pow", "__unm", "__idiv",
"__band", "__bor", "__bxor", "__bnot", "__shl", "__shr", "__tostring",
"__concat", "__len", "__eq", "__lt", "__le", "__index", "__newindex", "__call", "__gc"
}
local different --- When set, every class __index method will only return a value different from this one.
--- When using a proxied method, contains the last indexed class.
-- This is used for class.is(object); lastIndex will contain class so the is method can react accordingly, without having to be
-- re-set for each class (and therefore doesn't break the "different" mecanism).
local lastIndexed
local makeclass, methods, BaseClass
--- Classes defaults methods: will be re-set on each class creation.
-- If you overwrite them, you will only be able to call them from an object.
-- Methods starting with a "!" are "proxied methods": they're not present in the class table and will only be called through __index,
-- allowing more control over it (for example having access to lastIndexed).
methods = {
--- Create an object from the class.
-- In pratise, this only subclass the class and call the new method on it, so technically an object is a class.
-- Objects are exaclty like classes, but the __call metamethod will be replaced by one found in the parents,
-- or nil if doesn't exist (so an object is not directly subclassable).
-- (If no __call method is defined in a parent, you won't be able to call the object, but obj.__call will still
-- returns the default (subclassing) method, from one of the parents classes.)
-- The same happens with :new and :is, but since they're not metamethods, if not defined in a parent you won't
-- notice any difference.
-- TL;DR (since I think I'm not really clear): you can redefine __call, :new and :is in parents and use them in objects only.
-- A new object will only be created if calling the method "class:new(...)", if you call for example "class.new(someTable, ...)", it
-- will only execute the constructor defined in the class on someTable. This can be used to execute the parent constructor in a child
-- object, for example.
-- It should also be noted that if the new method returns non-nil value(s), they will be returned instead of the object.
["!new"] = function(self, ...)
if lastIndexed == self then
local obj, ret = self(), nil
-- Setting class methods to the ones found in parents (we use rawset in order to avoid calling the __newindex metamethod)
different = methods["!new"] rawset(obj, "new", obj:__index("new") or nil)
different = methods["!is"] rawset(obj, "is", obj:__index("is") or nil)
different = methods.__call rawset(obj, "__call", obj:__index("__call") or nil)
different = nil
-- Call constructor
if obj.new ~= methods["!new"] and type(obj.new) == "function" then ret = { obj:new(...) } end
if not ret or #ret == 0 then
return obj
else
return unpack(ret)
end
else
different = methods["!new"]
local new = lastIndexed:__index("new") or nil
different = nil
return new(self, ...)
end
end,
--- Returns true if self is other or a subclass of other.
-- If other is nil, will return true if self is a subclass of the class who called this method.
-- Examples:
-- class.is(a) will return true if a is any class or object
-- (class()):is(class) will return true ((class()) is a subclass of class)
-- (class()).is(class) will return false (class isn't a subclass of (class()))
["!is"] = function(self, other)
if type(self) ~= "table" then return false end
if other == nil then other = lastIndexed end
if self == other then return true end
for _, t in ipairs(self.__super) do
if t == other then return true end
if t.is == methods["!is"] and t:is(other) then return true end
end
return false
end,
--- Subclass the class: will create a class inheriting self and ... (... will have priority over self).
__call = function(self, ...)
local t = {...}
table.insert(t, self)
return makeclass(unpack(t))
end,
--- Internal value getting; this follows a precise search order.
-- For example: class(Base1, Base2){stuff}
-- When getting a value from the class, it will be first searched in stuff, then in Base1, then in all Base1 parents,
-- then in Base2, then in Base2 parents.
-- A way to describe this will be search in the latest added tables (from the farthest child to the first parents), from left-to-right.
-- self always refer to the initial table the metamethod was called on, super refers to the class currently being searched for a value.
__index = function(self, k, super)
local proxied = methods["!"..tostring(k)]
if proxied ~= nil and proxied ~= different then -- proxied methods
lastIndexed = self
return proxied
end
for _, t in ipairs((super or self).__super) do -- search in super (will follow __index metamethods)
local val = rawget(t, k)
if val ~= nil and val ~= different then return val end
-- Also covers the case when different search is enabled and the raw t[k] returns an identical value, so the __index metamethod search will be tried for another value.
if getmetatable(t) and getmetatable(t).__index then
val = getmetatable(t).__index(self, k, t)
if val ~= nil and val ~= different then return val end
end
end
end
}
--- Create a new class width parents ... (left-to-right priority).
function makeclass(...)
local class = {
__super = {} -- parent classes/tables list
}
for k, v in pairs(methods) do -- copy class methods
if k:sub(1, 1) ~= "!" then class[k] = v end -- except proxied methods
end
setmetatable(class, class)
for _, t in ipairs({...}) do -- fill super
if getmetatable(t) == nil then setmetatable(t, t) end -- auto-metatable the table
if type(t.__inherit) == "function" then t = t:__inherit(class) or t end -- call __inherit callback
table.insert(class.__super, t)
end
-- Metamethods query are always raw and thefore don't follow our __index, so we need to manually define thoses.
for _, metamethod in ipairs(metamethods) do
local inSuper = class:__index(metamethod)
if inSuper and rawget(class, metamethod) == nil then
rawset(class, metamethod, inSuper)
end
end
return class
end
--- The class which will be a parents for all the other classes.
-- We add some pretty-printing default in here. We temporarly remove the metatable in order to avoid a stack overflow.
BaseClass = makeclass {
__name = "class",
__tostring = function(self)
local mt, name = getmetatable(self), self.__name
setmetatable(self, nil)
local str = ("%s (%s)"):format(tostring(name), tostring(self))
setmetatable(self, mt)
return str
end
}
--- Class Commons implementation.
-- https://github.com/bartbes/Class-Commons
if common_class and not common then
common = {}
-- class = common.class(name, table, parents...)
function common.class(name, table, ...)
return BaseClass(table, ...){ __name = name, new = table.init }
end
-- instance = common.instance(class, ...)
function common.instance(class, ...)
return class:new(...)
end
end
return BaseClass

342
daccord.can Normal file
View file

@ -0,0 +1,342 @@
--- Yet another MPD client, but this time I wrote it so it's the best.
-- Focused on an instant and hopefully powerful search.
-- Currently only a console interface is available.
--
-- Dependencies: candran, luasocket, lcurses
--
-- Highly unintuitive keybinding:
-- * Up goes up, down goes down.
-- * Tab switch between search and current playlist.
-- * Control+W quits.
-- * Control+Space toggle play/pause.
-- * In search:
-- * Control+A add all the results to the playlist.
-- * Enter add currently selected song to the playlist.
-- * Type "TagName=" to trigger tag completion: select a tag and press Enter to select it. Or type it manually and exit tag completion by typing a space.
-- * In current playlist:
-- * Enter play currently selected song.
-- * Delete remove selected song from playlist.
--
-- Most of what was initialy planned isn't implemented yet. Hopefully all will be finished before you're six feet under.
--
-- Curently implemented:
-- * Play/pause, current song status
-- * Current playlist, jump to song, remove song
-- * Instant search among all tags or specific tags
-- * Instant tag value completion
-- * Overly optimistic planned features list in a comment in the main file
--
-- Stuff which is planned:
-- * Sticker search
-- * Search requests where keyword and tag selectors can be assigned with a $probabilty, generating infinite playlists (composer=chopin$.5)
-- * Relational operators for tags and stickers (song ratings) (rating>.5)
-- * Random, single, consume and other MPD play mode are determined by :keywords in the search query (:rand, :limit=5, :asc=Track, etc.)
-- * The endgoal would be that playlists would be entirely determined and contained in a single search query (add some saved storage interface).
-- Which we would therfore mean we can regenerate them on the fly when the MPD database is updated, or probabilities where used in the query.
-- * A non-console GUI. Should be doable considering everything is neatly contained in gui.can, but damn are thoses text widget I made weird.
--
-- This version of the software is licensed under the terms of the Apache License, version 2 (https://www.apache.org/licenses/LICENSE-2.0.txt).
-- Copyright 2017-2018 Étienne "Reuh" Fildadut
--
-- Licensed under the Apache License, Version 2.0 (the "License");
-- you may not use this file except in compliance with the License.
-- You may obtain a copy of the License at
--
-- http://www.apache.org/licenses/LICENSE-2.0
--
-- Unless required by applicable law or agreed to in writing, software
-- distributed under the License is distributed on an "AS IS" BASIS,
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-- See the License for the specific language governing permissions and
-- limitations under the License.
require("candran").setup()
let gui = require("gui")
let mpc = require("mpc")
-- Constants
let VERSION = "0.0.3"
-- Configuration
let config = {
-- MPD server
host = "localhost",
port = 6600,
password = "",
-- Interface
songDisplay = { "Track", { "Title", "file" }, "Artist", "Album" } -- list of tags or list of alternative tags (first one to exist will be used) to display for each song
}
(loadfile("config.lua", "t", config) or () end)()
-- Returns list of fields to display for the song s
let songTable = (s)
let t = {}
for i, field in ipairs(config.songDisplay) do
if type(field) ~= "table" then field = { field } end
t[i] = "∅"
for _, option in ipairs(field) do
if s[option] then
t[i] = tostring(s[option])
break
end
end
end
return t
end
-- Connect
mpc.log = () end
mpc = mpc("localhost", 6600)
if config.password ~= "" then
mpc:password(config.password)
end
-- Valid tags list
let tags = [
let r, l = mpc:tagtypes()
for _, t in ipairs(l) do
push t.tagtype
end
]
table.insert(tags, "file")
-- State
let tagCompleting = {
tag = nil,
start = nil,
stop = nil
}
let results, playlist = {}, {}
let state = "stop"
gui {
{
type = "tabs",
focused = true,
width = "extend",
height = "extend",
-- Search
{
{
id = "prompt",
type = "input",
focused = true,
width = "extend",
height = "1em",
onTextInput = :()
let list = @byId("list")
list:clear()
-- Tag complete
tagCompleting.tag = nil
if @sub(1, @cursorPosition):match("[A-Za-z_]+=[^\" ]*$") or @sub(1, @cursorPosition):match("[A-Za-z_]+=\"[^\"]*$") then
let start, sel, val, stop
if @sub(1, @cursorPosition):match("[A-Za-z_]+=[^\" ]*$") then
start, sel, val, stop = @sub(1, @cursorPosition):match("()([A-Za-z_]+)=([^\" ]*)()$")
else
start, sel, val, stop = @sub(1, @cursorPosition):match("()([A-Za-z_]+)=\"([^\"]*)()$")
end
for _, tag in ipairs(tags) do
if tag:lower() == sel:lower() then
let r, songs = mpc:list(tag)
if r then results = songs end
for _, s in ipairs(results) do
if s[tag]:lower():match(val:lower()) then -- filter val
list:insert{tostring(s[tag])}
end
end
tagCompleting.tag = tag
tagCompleting.start = start
tagCompleting.stop = stop
break
end
end
-- Song search
else
-- Build query
let query = {}
-- Any selectors
let withoutSel = @content:gsub("[A-Za-z_]+=[^\" ]+", ""):gsub("[A-Za-z_]+=\"[^\"]*\"", "")
for word in withoutSel:gmatch("[^%s]+") do
table.insert(query, "any")
table.insert(query, word)
end
-- Tag selectors
for tag, val in @content:gmatch("([A-Za-z_]+)=([^\" ]+)") do
table.insert(query, tag)
table.insert(query, val)
end
for tag, val in @content:gmatch("([A-Za-z_]+)=\"([^\"]*)\"") do
table.insert(query, tag)
table.insert(query, val)
end
-- Search
let r, songs = mpc:search(unpack(query))
if r then results = songs end
-- Update widget
for _, s in ipairs(results) do
list:insert(songTable(s))
end
end
end
},
{
type = "fill",
width = "extend",
height = "1em",
fill = "ACS_HLINE"
},
{
id = "list",
type = "list",
focused = true,
width = "extend",
height = "extend",
onSelect = :(l)
-- Tag complete
if tagCompleting.tag then
let prompt = @byId("prompt")
if results[l[1]][tagCompleting.tag]:match(" ") then
prompt:replace(tagCompleting.start, tagCompleting.stop, tagCompleting.tag.."=\""..results[l[1]][tagCompleting.tag].."\"")
else
prompt:replace(tagCompleting.start, tagCompleting.stop, tagCompleting.tag.."="..results[l[1]][tagCompleting.tag])
end
-- Song search
else
for _, i in ipairs(l) do
if results[i] then
mpc:add(results[i].file)
end
end
@byId("playlist"):updateAfter(1)
let status = @byId("status")
status:set("Added "..#l.." songs to the current playlist") -- FIXME
end
end
}
},
-- Playlist
{
{
id = "playlist",
type = "list",
focused = true,
width = "extend",
height = "extend",
updateInterval = 5,
onUpdate = :()
let r, songs = mpc:playlistinfo()
if r then playlist = songs end
for i, s in ipairs(playlist) do
let item = songTable(s)
if @content[i] then
if @content[i] ~= item then
@remove(i)
@insert(i, item)
end
else
@insert(i, item)
end
end
while #@content > #playlist do
@remove()
end
end,
onSelect = :(l)
if #playlist > 0 then
mpc:playid(playlist[l[1]].Id)
end
end,
onControl = :(control)
if control == "delete" then
mpc:deleteid(playlist[@selected].Id)
@remove(@selected)
end
end
}
}
},
-- Status bar
{
id = "play-position",
type = "slider",
width = "extend",
height = "1em"
},
{
id = "status",
type = "label",
focused = true,
width = "extend",
height = "1em",
content = "No current song",
updateInterval = 1,
onUpdate = :()
let r, s = mpc:currentsong()
if r then
if s.file then
@set(table.concat(songTable(s), " - "))
else
@set("daccord v"..VERSION.." - nothing playing")
end
end
let r, s = mpc:status()
if r then
state = s.state
pos = @byId("play-position")
pos:setHead(state == "play" and "▶️" or "⏸")
pos:setMax(s.duration)
pos:set(s.elapsed)
end
end,
onControl = :(control)
if control == "space" then
if state == "play" then
mpc:pause()
else
mpc:play()
end
end
@onUpdate()
end
},
onClose = :() @exit() end
}

493
gui.can Normal file
View file

@ -0,0 +1,493 @@
--- Yet another badly designed and implemented GUI library, but this time I wrote it so it's the best.
-- Part of daccord. See daccord.can for more information.
-- Copyright 2017-2018 Étienne "Reuh" Fildadut
--
-- Licensed under the Apache License, Version 2.0 (the "License");
-- you may not use this file except in compliance with the License.
-- You may obtain a copy of the License at
--
-- http://www.apache.org/licenses/LICENSE-2.0
--
-- Unless required by applicable law or agreed to in writing, software
-- distributed under the License is distributed on an "AS IS" BASIS,
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-- See the License for the specific language governing permissions and
-- limitations under the License.
let curses = require("curses")
let class = require("classtoi")
let sleep = require("socket").sleep
let screen
os.setlocale("")
let everyWidget = {}
let widget = class {
_exit = false,
_redraw = true,
x = 0,
y = 0,
w = 0,
h = 0,
parent = {},
focused = false,
updateInterval = -1,
_nextUpdate = os.time(), -- -1 = no update
new = :(data)
table.insert(everyWidget, self)
-- Copy properties
for k, v in pairs(data) do
@[k] = v
end
-- Dimensions
if not @parent._firstX then @parent._firstX, @parent._firstY = 0, 0 end
@x, @y = @parent._firstX, @parent._firstY
if @width == "extend" @w = @parent.w - @x
elseif @width:match("%d+em$") @w = tonumber(@width:match("%d+"))
if @height == "extend" then
@h = @parent.h - @y
@parent._lastVerticalExtend = self -- will be resized if vertical space is needed
@parent._afterLastVerticalExtend = {}
elseif @height:match("%d+em$") @h = tonumber(@height:match("%d+"))
if @y >= @parent.h then -- resize
@parent._lastVerticalExtend:_resize(@parent._lastVerticalExtend.w, @parent._lastVerticalExtend.h - @h)
@parent._firstY -= @h
@y -= @h
for _, el in ipairs(@parent._afterLastVerticalExtend) do -- move widgets
el.y -= @h
end
end
@parent._firstX += @w
if @parent._firstX >= @parent.w then @parent._firstX = 0 end -- newline
@parent._firstY += @h
if @parent._lastVerticalExtend ~= self and @parent._afterLastVerticalExtend then table.insert(@parent._afterLastVerticalExtend, self) end
-- Setup
if @_setup then @_setup() end
screen:move(@parent.y + @y + @h, @parent.x + @x + @w)
end,
exit = :()
@_exit = true
end,
byId = :(id)
for _, el in ipairs(everyWidget) do
if el.id == id return el
end
error("no element with id "..tostring(id))
end,
updateAfter = :(time)
@_nextUpdate = os.time() + time
end
}
let widgets = setmetatable({
fill = widget {
_draw = :()
if type(@fill) == "string" then @fill = curses[@fill] end
for y = @parent.y + @y, @parent.y + @y + @h - 1 do
screen:move(y, @parent.x + @x)
for x = @parent.x + @x, @parent.x + @x + @w - 1 do
screen:addch(@fill or 32)
end
end
end
},
input = widget {
content = "",
cursorPosition = 1,
_input = :(charbuffer, control)
let y, x = @parent.y + @y, @parent.x + @x + @cursorPosition-1
if control == "backspace" then
screen:mvdelch(y, x-1)
if @cursorPosition > 1 then
if @cursorPosition == 2 then -- utf8.offset(s, 0) returns the start of the last character, ie something we don't want
@content = @content:sub(utf8.offset(@content, @cursorPosition))
else
@content = @content:sub(1, utf8.offset(@content, @cursorPosition-1)-1)
.. @content:sub(utf8.offset(@content, @cursorPosition))
end
@cursorPosition -= 1
@onTextInput()
end
elseif control == "right" then
if @cursorPosition <= utf8.len(@content) then
screen:addstr(@content:sub(utf8.offset(@content, @cursorPosition), utf8.offset(@content, @cursorPosition+1)-1))
@cursorPosition += 1
end
elseif control == "left" then
if @cursorPosition > 1 then
screen:move(y, x-1)
@cursorPosition -= 1
end
elseif charbuffer then
screen:move(y, x)
screen:winsstr(charbuffer)
screen:move(y, x+1)
@content = @content:sub(1, utf8.offset(@content, @cursorPosition)-1)
.. charbuffer
.. @content:sub(utf8.offset(@content, @cursorPosition))
@cursorPosition += 1
@onTextInput()
end
end,
_draw = :()
for y = @parent.y + @y, @parent.y + @y + @h - 1 do
screen:move(y, @parent.x + @x)
for x = @parent.x + @x, @parent.x + @x + @w - 1 do
screen:addch(32)
end
end
screen:mvaddstr(@parent.y + @y, @parent.x + @x, @content)
end,
_placeCursor = :()
screen:move(@parent.y + @y, @parent.x + @x + @cursorPosition-1)
return true
end,
sub = :(start, stop=utf8.len(@content))
if stop < 1 or start >= utf8.len(@content) then
return ""
else
return @content:sub(utf8.offset(@content, start), (utf8.offset(@content, stop+1) or (#@content+1))-1)
end
end,
replace = :(start, stop, newText)
if @cursorPosition >= stop then
@cursorPosition += utf8.len(newText) - (stop - start)
end
@content = @sub(1, start-1) .. newText .. @sub(stop+1)
@onTextInput()
@_redraw = true
end,
onTextInput = :() end
},
list = widget {
content = {},
columnWidth = {},
selected = 1,
scroll = 0,
_redraw = true,
_input = :(charbuffer, control)
if control == "up" and @selected > 1 then
@selected -= 1
if @selected == @scroll then
@scroll -= 1
end
@_redraw = true
elseif control == "down" and @selected < #@content then
@selected += 1
if @selected == @scroll + @h + 1 then
@scroll += 1
end
@_redraw = true
end
if control == "enter" then
@onSelect({@selected})
elseif control == "A" then
let len = #@content
@onSelect([for i=1, len do i end])
end
end,
_draw = :()
let oY, oX = screen:getyx()
for y = @parent.y + @y, @parent.y + @y + @h -1 do -- clear
screen:mvaddstr(y, @parent.x + @x, (" "):rep(@w))
end
screen:move(@parent.y + @y, @parent.x + @x)
for i=@scroll+1, @scroll + @h do -- draw
if i == @selected then
screen:standout()
end
let colx = @parent.x+@x
for c=1, #@columnWidth do
screen:mvaddstr(@parent.y+@y+i-1-@scroll, colx, (""):rep(@columnWidth[c])) -- FIXME: too lazy to do this the right way and extract utf8 substrings (also should probably check if the thing doesn't go too right)
screen:mvaddstr(@parent.y+@y+i-1-@scroll, colx, @content[i] and @content[i][c] or "")
colx += @columnWidth[c]
end
if i == @selected then
screen:standend()
end
end
screen:move(oY, oX)
end,
insert = :(pos, item)
if item then
table.insert(@content, pos, item)
if @selected >= pos and #@content > 1 then
@selected += 1
end
else
item = pos
table.insert(@content, item)
end
if #@content == 1 then -- column count is determined by 1st item (other items can do what they want, this isn't a dictatorship)
@columnWidth = [for _=1,#item do 0 end]
end
if #item >= #@columnWidth then -- if the column fits into our dictatorship, update column width
for c=1, #@columnWidth do
if utf8.len(item[c]) > @columnWidth[c] then
@columnWidth[c] = utf8.len(item[c]) + 1
end
end
end
@_redraw = true
end,
remove = :(pos=#@content)
table.remove(@content, pos)
if @selected >= pos and @selected > 1 then
@selected -= 1
end
@_redraw = true
end,
clear = :()
@.content = {}
--@.columnWidth = {}
@_redraw = true
@selected = 1
@scroll = 0
end,
onSelect = :() end
},
tabs = widget {
selected = 1,
children = {},
_children = {},
_setup = :()
for i, tab in ipairs(@) do
@children[i] = { x = @x, y = @y, w = @w, h = @h }
for _, el in ipairs(tab) do
el.parent = @children[i]
table.insert(@children[i], widgets[el.type]:new(el))
end
end
@_children = @children[@selected]
end,
_input = :(charbuffer, control)
if control == "tab" then
@selected += 1
if @selected > #@ then @selected = 1 end
@_children = @children[@selected]
for _, el in ipairs(@_children) do -- Force redraw
el._redraw = true
end
end
end,
_resize = :(w, h)
@w, @h = w, h
for i, tab in ipairs(@) do
for _, el in ipairs(@children[i]) do
if el.x + el.w > w then el.w = w - el.x end
if el.y + el.h > h then el.h = h - el.y end
end
end
end
},
label = widget {
content = "Label",
_draw = :()
screen:mvaddstr(@parent.y + @y, @parent.x + @x, @content .. (" "):rep(@w - #@content))
end,
set = :(str)
@content = str
@_redraw = true
end
},
slider = widget {
min = 0,
max = 1,
current = 0,
head = "⏸",
_draw = :()
let len = math.ceil((@current - @min) / (@max - @min) * @w)
screen:mvaddstr(@parent.y + @y, @parent.x + @x, ("="):rep(len-1))
screen:addstr(@head .. (" "):rep(@w - len))
end,
set = :(current)
@current = current
@_redraw = true
end,
setMax = :(max)
@max = max
@_redraw = true
end,
setMin = :(min)
@min = min
@_redraw = true
end,
setHead = :(head)
@head = head
@_redraw = true
end
}
}, {
__index = (t, k)
error("unknown widget "..tostring(k))
end
})
let recursiveApply = (list, fn)
for _, el in ipairs(list) do
if el._widget then
el = el._widget
end
fn(el)
if el._children then
recursiveApply(el._children, fn)
end
end
end
return (ui)
xpcall(()
-- Init
if not screen then
screen = curses.initscr()
curses.cbreak()
curses.echo(false)
screen:nodelay(true)
end
-- Create widgets
screen:clear()
let h, w = screen:getmaxyx()
let parent = {
x = 0, y = 0,
w = w, h = h
}
for _, el in ipairs(ui) do
el.parent = parent
el._widget = widgets[el.type]:new(el)
end
-- Update loop
while not widget._exit do
-- Input
local c = screen:getch()
if c and c < 256 then
let charbuffer = string.char(c)
let control
if c > 127 then -- multibyte char
charbuffer ..= string.char(screen:getch())
if c > 223 then
charbuffer ..= string.char(screen:getch())
if c > 239 then
charbuffer ..= string.char(screen:getch())
end
end
end
if curses.unctrl(c):match("^%^") then -- control char
charbuffer = nil
let k = curses.unctrl(c)
if k == "^?" then
control = "backspace"
elseif k == "^W" then
control = "close"
elseif k == "^J" then
control = "enter"
elseif k == "^I" then
control = "tab"
elseif k == "^@" then
control = "space"
elseif k == "^[" then
let k = string.char(screen:getch()) .. string.char(screen:getch())
if k == "[C" then
control = "right"
elseif k == "[D" then
control = "left"
elseif k == "[A" then
control = "up"
elseif k == "[B" then
control = "down"
elseif k == "[3" then
k ..= string.char(screen:getch())
if k == "[3~" then
control = "delete"
else
error("unknown control "..tostring(k))
end
else
error("unknown control "..tostring(k))
end
elseif k:match("^%^[A-Z]$") then
control = k:match("^%^([A-Z])$")
else
error("unknown control "..tostring(k))
end
end
recursiveApply(ui, (el)
if el.focused and el._input then
el:_input(charbuffer, control)
end
if el.focused and control and el.onControl then
el:onControl(control)
end
end)
if control == "close" then
if ui.onClose then ui.onClose(widget) end
end
end
-- Update
recursiveApply(ui, (el)
if el._nextUpdate ~= -1 and os.difftime(el._nextUpdate, os.time()) <= 0 then
if el.onUpdate then el:onUpdate() end
if el.updateInterval ~= -1 then
el._nextUpdate += el.updateInterval
else
el._nextUpdate = -1
end
end
end)
-- Redraw
recursiveApply(ui, (el)
if el._redraw and el._draw then
el._redraw = false
el:_draw()
end
end)
-- Place cursor
let cursorVis = 0
recursiveApply(ui, (el)
if el._placeCursor and el:_placeCursor() then
cursorVis = 2
end
end)
curses.curs_set(cursorVis)
-- Done
screen:refresh()
sleep(0.03)
end
curses.endwin()
end, (err)
curses.endwin()
print(require("candran").messageHandler(err))
os.exit(2)
end)
end

371
mpc.lua Normal file
View file

@ -0,0 +1,371 @@
--- Music Player Daemon (MPD) client library.
--
-- Alows basic manipulation of the MPD protocol. This provides the client part, you will need a MPD-compatible server running on
-- another machine.
--
-- Please see the MPD command reference on the [official documentation](http://www.musicpd.org/doc/protocol/command_reference.html).
--
-- You may want to generate a documentation for this file using LDoc: `ldoc .`. However, LDoc doesn't seem to
-- appreciate my coding style so it doesn't display everything, but the rendered documentation should be usable enough.
-- If you didn't understand something or think you missed something, please read the [source file](source/mpc.lua.html), which is
-- largely commented.
--
-- Variables prefixed with a `_` are private. Don't use them if you don't know what you're doing.
--
-- *Requires* `luasocket` or `ctr.socket` (ctrµLua).
--
-- When I started this project in october 2015, there weren't really any other decent MPD client library for Lua.
-- The ones available were either uncomplete or outdated, regarding either MPD or Lua. So I made this.
-- I didn't want to publish it until I used it in something, since it's a pretty small library (I didn't know things like left-pad existed and people were ok with it).
-- Well, too bad for me. As it turns out, today several new libraries have poped up and they look quite usable.
-- Well, it's too late. Now this is yet another MPD library, but this time I wrote it so it's the best.
--
-- @author Reuh
-- @release 0.2.0
-- Copyright (c) 2015-2018 Étienne "Reuh" Fildadut <fildadut@reuh.eu>
--
-- Permission is hereby granted, free of charge, to any person obtaining a copy
-- of this software and associated documentation files (the "Software"), to deal
-- in the Software without restriction, including without limitation the rights
-- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-- copies of the Software, and to permit persons to whom the Software is
-- furnished to do so, subject to the following conditions:
--
-- 1. The above copyright notice and this permission notice shall be included in
-- all copies or substantial portions of the Software; and
-- 2. You must cause any modified source files to carry prominent notices stating
-- that you changed the files.
--
-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-- SOFTWARE.
local socket
if package.loaded["ctr.socket"] then
socket = require("ctr.socket")
else
socket = require("socket")
end
-- Parsing commands response helpers.
local function cast(str)
return tonumber(str, 10) or str
end
local function parseList(lines, starters)
local list = {}
if lines[1] ~= "OK" then
if not starters then
starters = { lines[1]:match("^[^:]+") } -- first tag for each song
end
local cursong -- currently parsing song
for _, l in ipairs(lines) do
if l == "OK" then
break
else
for _, startType in ipairs(starters) do
if l:match(startType..":") then -- next song
cursong = {}
table.insert(list, cursong)
break
end
end
cursong[l:match("^[^:]+")] = cast(l:match("^[^:]+%: (.*)$")) -- add tag
end
end
end
return list
end
local function parseSticker(dict)
for k, v in pairs(dict) do
if k == "sticker" and v:match(".+=.+") then
dict[k] = { [v:match("(.+)=.+")] = cast(v:match(".+=(.+)")) }
end
if type(v) == "table" then
parseSticker(v)
end
end
return dict
end
--- Music Player Client object.
-- Used to manipulate a single MPD server.
-- @type mpc
local mpc = {
---## PUBLIC METHODS ##---
-- The functions you should use.
--- Connect to a MPD server.
-- @tparam string address server address
-- @tparam number port server port
-- @see mpcmodule
init = function(self, address, port)
self._address = address
self._port = port
self:_connect()
end,
--- Execute a command.
-- A shortcut (and generally more useful version) is `mpc:commandName([arguments, ...])`, which will also try return a parsed version of the result
-- in a nicer Lua representation as a second return value. See COMMANDS OVERWRITE.
-- @tparam string command command name
-- @tparam[opt] any ... command arguments
-- @treturn[1] boolean true if it was a success, false otherwise
-- @treturn[1] table List of the lines of the response (strings)
-- @treturn[2] nil nil if nothing was received
command = function(self, command, ...)
self:_send({ command, ... })
return self:_receive(true)
end,
--- Called each time something can be logged.
-- You will want to and can overwrite this function.
-- @tparam string message message string
-- @tparam[opt] any ... arguments to pass to message:format(...)
log = function(self, message, ...)
print(("[mpc.lua@%s:%s] %s"):format(self._address, self._port, message:format(...)))
end,
---## COMMANDS OVERWRITE ##---
-- Theses functions overwrite some MPD's command, to add somme pratical features.
-- In particular, every MPD command which return something will be wrapped here so the second return value is either (the list of lines is still available as a third return value):
-- * A list of dictionnaries, if the command can return a list of similar elements (playlistinfo, search, ...)
-- * A dictionnary if the command return something which isn't a list (status, currentsong, ...)
-- The key names are the same used by MPD in the responses. Numbers will be automatically casted to a Lua number, every other value will be a string.
-- Commands which return nothing will return nil as a second return value.
-- Most overwrites are generated at the end of the file using the overwrites lists defined below.
-- Signature for every command called through `mpc:commandName`:
-- @tparam[opt] any ... command arguments
-- @treturn[1] boolean true if it was a success
-- @treturn[1] table dictionnary or list of dictionnary containing the response data, or nil if there is no response data to be expected
-- @treturn[1] table List of the lines of the response (strings)
-- @treturn[2] boolean false if there was an error
-- @treturn[2] string error message
-- @treturn[3] nil nil if nothing was received
--- Sends the close command and close the socket.
-- This will log any uncomplete message received, if any. Returns nothing.
-- Call this when you are done with the mpc object.
-- Note however, that the client will automatically reconnect if you reuse it later.
close = function(self)
self:_send("close")
self._socket:close()
if #self._buffer > 0 then
self:log("UNCOMPLETLY RECEIVED MESSAGE:\n\t%s", table.concat(self._buffer, "\n\t"))
end
self:log("CLOSED")
end,
--- Sends the password command.
-- This will store the password in a variable so it can be resent in case of disconnection.
-- @tparam string password the password
password = function(self, password)
self._password = password
local success, lines = self:command("password", password)
return success, not success and lines[1] or nil, lines
end,
--- Returns a chunk of albumart.
-- See the MPD documentation. The raw bytes will be stored in the `chunk` field of the response dictionnary.
albumart = function(self, ...)
local success, lines = self:command("albumart", ...)
return success, success and {
size = lines[1]:match("size: (%d+)"),
binary = lines[2]:match("binary: (%d+)"),
chunk = table.concat(lines, "", 3, #lines-1)
} or lines[1], lines
end,
--- Sticker commands.
-- Will parse stickers values in dictionnary.sticker.name = value.
sticker = function(self, action, ...)
if action == "list" or action == "find" then
local success, lines = self:command("sticker", action, ...)
return success, success and parseSticker(parseList(lines)) or lines[1], lines
elseif action == "get" then
local success, lines = self:command("sticker", action, ...)
return success, success and parseSticker(parseList(lines)[1]) or lines[1], lines
else
local success, lines = self:command("sticker", action, ...)
return success, not success and lines[1] or nil, lines
end
end,
--- Commands which will return a list of dictionnaries.
_overwriteDictList = {
"idle", -- Querying MPD's status
"playlistfind", "playlistid", "playlistinfo", "playlistsearch", "plchanges", "plchangesposid", -- The current playlist
"listplaylist", "listplaylistinfo", "listplaylists", -- The current playlist
"count", "find", "list", "listall", "listallinfo", "search", listfiles = { "file", "directory" }, lsinfo = { "file", "directory" }, -- The music database
"listmounts", "listneighbors", -- Mounts and neighbors
"tagtypes", -- Connection settings
"listpartitions", -- Partition commands
"outputs", -- Audio output devices
"commands", "notcommands", "urlhandlers", "decoders", -- Reflection
"channels", "readmessages" -- Client to client
},
--- Commands which will return a dictionnary.
_overwriteDict = {
"currentsong", "status", "stats", -- Querying MPD's status
"readcomments", "update", "rescan", -- The music database
"config" -- Reflection
},
---## PRIVATE FUNCTIONS ##---
-- Theses functions are intended to be used internally by mpc.lua.
-- You can use them but they weren't meant to be used from the outside.
_socket = nil, -- socket object
_address = "", -- server address string
_port = 0, -- server port integer
_password = "", -- server password string
_buffer = {}, -- received message buffer table
--- Connects to the server.
-- The fuction will auto-login if a password was previously set.
-- If the client was already connected, it will disconnect and then reconnect.
_connect = function(self)
if self._socket then self._socket:close() end
self._socket = assert(socket.tcp())
assert(self._socket:connect(self._address, self._port))
if self._socket.settimeout then self._socket:settimeout(0.1) end
assert(self:_receive(), "something went terribly wrong")
self:log("CONNECTED")
if self._password ~= "" then self:password(self._password) end
end,
--- Send a list of commands to the server.
-- @tparam table commands List of commands (strings or tables). If table, the table represent the arguments list.
-- ie, this `:_send({"play", 18})` is equivalent to `:_send("play 18")`.
_send = function(self, ...)
local commands = {...}
for i, v in ipairs(commands) do
if type(v) == "table" then
local cmd = v[1]
for j, k in ipairs(v) do
if j > 1 then -- bweh
if type(k) == "string" then
cmd = cmd..(" %q"):format(k)
elseif type(k) == "table" then
cmd = cmd..(k[1] or "")..":"..(k[2] or "")
else
cmd = cmd.." "..tostring(k)
end
end
end
commands[i] = cmd
end
end
local success, err = self._socket:send(table.concat(commands, "\n").."\n")
if not success then
if err == "closed" then
self:log("CONNECTION CLOSED, RECONNECTING")
self:_connect()
self:_send(...)
else
error("error while sending data to MPD server: "..err)
end
end
self:log("SENT:\n\t%s", table.concat(commands, "\n\t"))
end,
--- Receive a single server response.
-- @tparam boolean block true to block until a message is received
-- @treturn[1] boolean true if was a success, false otherwise
-- @treturn[1] table List of the lines of the response (strings)
-- @treturn[2] nil nil if nothing was received
_receive = function(self, block)
local success
local received
repeat
local response, err = self._socket:receive()
if response and response ~= "" then
table.insert(self._buffer, response)
if response:sub(1, 2) == "OK" or response:sub(1, 3) == "ACK" then
success = response:sub(1, 2) == "OK"
received = self._buffer
self._buffer = {}
break
end
elseif err == "closed" then
self:log("CONNECTION CLOSED, RECONNECTING")
self:_connect()
return self:_receive()
elseif err ~= "timeout" then
error("error while receiving data from MPD server: "..err)
end
until not block and (not response or response == "")
if not received then return nil end
self:log("RECEIVED:\n\t%s", table.concat(received, "\n\t"))
return success, received
end
}
-- Overwrites for commands which return lists of dictionnaries
for k, v in pairs(mpc._overwriteDictList) do
local command, starters
if type(k) == "number" then command = v
else command, starters = k, v end
mpc[command] = function(self, ...)
local success, lines = self:command(command, ...)
return success, success and parseList(lines, starters) or lines[1], lines
end
end
-- Overwrites for commands which return a single dictionnary
for k, v in pairs(mpc._overwriteDict) do
local command, starters
if type(k) == "number" then command = v
else command, starters = k, v end
mpc[command] = function(self, ...)
local success, lines = self:command(command, ...)
return success, success and parseList(lines, starters)[1] or lines[1], lines
end
end
--- The module returns a constructor function.
-- Calling this function will create a new MPC object.
-- The arguments will be passed to the object's `:init` method.
-- @within Module
-- @function mpcmodule
-- @usage local mpc = require("mpc")("localhost", 6600) -- where "localhost", 6600 are :init's arguments
return setmetatable(mpc, {
__call = function(t, ...)
local object = setmetatable({}, {
__index = function(t, k)
if mpc[k] then
return mpc[k]
elseif k:sub(1, 1) ~= "_" then
return function(self, ...)
local success, lines = mpc.command(self, k, ...)
return success, not success and lines[1] or nil, lines
end
end
end
})
object:init(...)
return object
end
})