bling/signal/playerctl/playerctl_cli.lua

349 lines
11 KiB
Lua

-- Playerctl signals
--
-- Provides:
-- metadata
-- title (string)
-- artist (string)
-- album_path (string)
-- album (string)
-- player_name (string)
-- position
-- interval_sec (number)
-- length_sec (number)
-- playback_status
-- playing (boolean)
-- volume
-- volume (number)
-- loop_status
-- loop_status (string)
-- shuffle
-- shuffle (bool)
-- no_players
-- (No parameters)
local awful = require("awful")
local gobject = require("gears.object")
local gtable = require("gears.table")
local gtimer = require("gears.timer")
local gstring = require("gears.string")
local beautiful = require("beautiful")
local helpers = require(tostring(...):match(".*bling") .. ".helpers")
local setmetatable = setmetatable
local tonumber = tonumber
local ipairs = ipairs
local type = type
local capi = { awesome = awesome }
local playerctl = { mt = {} }
function playerctl:disable()
self._private.metadata_timer:stop()
self._private.metadata_timer = nil
awful.spawn.with_shell("killall playerctl")
end
function playerctl:pause(player)
if player ~= nil then
awful.spawn.with_shell("playerctl --player=" .. player .. " pause")
else
awful.spawn.with_shell(self._private.cmd .. "pause")
end
end
function playerctl:play(player)
if player ~= nil then
awful.spawn.with_shell("playerctl --player=" .. player .. " play")
else
awful.spawn.with_shell(self._private.cmd .. "play")
end
end
function playerctl:stop(player)
if player ~= nil then
awful.spawn.with_shell("playerctl --player=" .. player .. " stop")
else
awful.spawn.with_shell(self._private.cmd .. "stop")
end
end
function playerctl:play_pause(player)
if player ~= nil then
awful.spawn.with_shell("playerctl --player=" .. player .. " play-pause")
else
awful.spawn.with_shell(self._private.cmd .. "play-pause")
end
end
function playerctl:previous(player)
if player ~= nil then
awful.spawn.with_shell("playerctl --player=" .. player .. " previous")
else
awful.spawn.with_shell(self._private.cmd .. "previous")
end
end
function playerctl:next(player)
if player ~= nil then
awful.spawn.with_shell("playerctl --player=" .. player .. " next")
else
awful.spawn.with_shell(self._private.cmd .. "next")
end
end
function playerctl:set_loop_status(loop_status, player)
if player ~= nil then
awful.spawn.with_shell("playerctl --player=" .. player .. " loop " .. loop_status)
else
awful.spawn.with_shell(self._private.cmd .. "loop " .. loop_status)
end
end
function playerctl:cycle_loop_status(player)
local function set_loop_status(loop_status)
if loop_status == "None" then
self:set_loop_status("Track")
elseif loop_status == "Track" then
self:set_loop_status("Playlist")
elseif loop_status == "Playlist" then
self:set_loop_status("None")
end
end
if player ~= nil then
awful.spawn.easy_async_with_shell("playerctl --player=" .. player .. " loop", function(stdout)
set_loop_status(stdout)
end)
else
set_loop_status(self._private.loop_status)
end
end
function playerctl:set_position(position, player)
if player ~= nil then
awful.spawn.with_shell("playerctl --player=" .. player .. " position " .. position)
else
awful.spawn.with_shell(self._private.cmd .. "position " .. position)
end
end
function playerctl:set_shuffle(shuffle, player)
shuffle = shuffle and "on" or "off"
if player ~= nil then
awful.spawn.with_shell("playerctl --player=" .. player .. " shuffle " .. shuffle)
else
awful.spawn.with_shell(self._private.cmd .. "shuffle " .. shuffle)
end
end
function playerctl:cycle_shuffle(player)
if player ~= nil then
awful.spawn.easy_async_with_shell("playerctl --player=" .. player .. " shuffle", function(stdout)
local shuffle = stdout == "on" and true or false
self:set_shuffle(not self._private.shuffle)
end)
else
self:set_shuffle(not self._private.shuffle)
end
end
function playerctl:set_volume(volume, player)
if player ~= nil then
awful.spawn.with_shell("playerctl --player=" .. player .. " volume " .. volume)
else
awful.spawn.with_shell(self._private.cmd .. "volume " .. volume)
end
end
local function emit_player_metadata(self)
local metadata_cmd = self._private.cmd .. "metadata --format 'title_{{title}}artist_{{artist}}art_url_{{mpris:artUrl}}player_name_{{playerName}}album_{{album}}' -F"
awful.spawn.with_line_callback(metadata_cmd, {
stdout = function(line)
local title = gstring.xml_escape(line:match('title_(.*)artist_')) or ""
local artist = gstring.xml_escape(line:match('artist_(.*)art_url_')) or ""
local art_url = line:match('art_url_(.*)player_name_') or ""
local player_name = line:match('player_name_(.*)album_') or ""
local album = gstring.xml_escape(line:match('album_(.*)')) or ""
art_url = art_url:gsub('%\n', '')
if player_name == "spotify" then
art_url = art_url:gsub("open.spotify.com", "i.scdn.co")
end
if self._private.metadata_timer
and self._private.metadata_timer.started
then
self._private.metadata_timer:stop()
end
self._private.metadata_timer = gtimer {
timeout = self.debounce_delay,
autostart = true,
single_shot = true,
callback = function()
if title and title ~= "" then
if art_url ~= "" then
local art_path = os.tmpname()
helpers.filesystem.save_image_async_curl(art_url, art_path, function()
self:emit_signal("metadata", title, artist, art_path, album, player_name)
capi.awesome.emit_signal("bling::playerctl::title_artist_album", title, artist, art_path)
end)
else
self:emit_signal("metadata", title, artist, "", album, player_name)
capi.awesome.emit_signal("bling::playerctl::title_artist_album", title, artist, "")
end
else
self:emit_signal("no_players")
capi.awesome.emit_signal("bling::playerctl::no_players")
end
end
}
collectgarbage("collect")
end,
})
end
local function emit_player_position(self)
local position_cmd = self._private.cmd .. "position"
local length_cmd = self._private.cmd .. "metadata mpris:length"
awful.widget.watch(position_cmd, self.interval, function(_, interval)
awful.spawn.easy_async_with_shell(length_cmd, function(length)
local length_sec = tonumber(length) -- in microseconds
local interval_sec = tonumber(interval) -- in seconds
if length_sec and interval_sec then
if interval_sec >= 0 and length_sec > 0 then
self:emit_signal("position", interval_sec, length_sec / 1000000)
capi.awesome.emit_signal("bling::playerctl::position", interval_sec, length_sec / 1000000)
end
end
end)
collectgarbage("collect")
end)
end
local function emit_player_playback_status(self)
local status_cmd = self._private.cmd .. "status -F"
awful.spawn.with_line_callback(status_cmd, {
stdout = function(line)
if line:find("Playing") then
self:emit_signal("playback_status", true)
capi.awesome.emit_signal("bling::playerctl::status", true)
else
self:emit_signal("playback_status", false)
capi.awesome.emit_signal("bling::playerctl::status", false)
end
end,
})
end
local function emit_player_volume(self)
local volume_cmd = self._private.cmd .. "volume -F"
awful.spawn.with_line_callback(volume_cmd, {
stdout = function(line)
self:emit_signal("volume", tonumber(line))
end,
})
end
local function emit_player_loop_status(self)
local loop_status_cmd = self._private.cmd .. "loop -F"
awful.spawn.with_line_callback(loop_status_cmd, {
stdout = function(line)
self._private.loop_status = line
self:emit_signal("loop_status", line:lower())
end,
})
end
local function emit_player_shuffle(self)
local shuffle_cmd = self._private.cmd .. "shuffle -F"
awful.spawn.with_line_callback(shuffle_cmd, {
stdout = function(line)
if line:find("On") then
self._private.shuffle = true
self:emit_signal("shuffle", true)
else
self._private.shuffle = false
self:emit_signal("shuffle", false)
end
end,
})
end
local function parse_args(self, args)
if args.player then
self._private.cmd = self._private.cmd .. "--player="
if type(args.player) == "string" then
self._private.cmd = self._private.cmd .. args.player .. " "
elseif type(args.player) == "table" then
for index, player in ipairs(args.player) do
self._private.cmd = self._private.cmd .. player
if index < #args.player then
self._private.cmd = self._private.cmd .. ","
else
self._private.cmd = self._private.cmd .. " "
end
end
end
end
if args.ignore then
self._private.cmd = self._private.cmd .. "--ignore-player="
if type(args.ignore) == "string" then
self._private.cmd = self._private.cmd .. args.ignore .. " "
elseif type(args.ignore) == "table" then
for index, player in ipairs(args.ignore) do
self._private.cmd = self._private.cmd .. player
if index < #args.ignore then
self._private.cmd = self._private.cmd .. ","
else
self._private.cmd = self._private.cmd .. " "
end
end
end
end
end
local function new(args)
args = args or {}
local ret = gobject{}
gtable.crush(ret, playerctl, true)
ret.interval = args.interval or beautiful.playerctl_position_update_interval or 1
ret.debounce_delay = args.debounce_delay or beautiful.playerctl_debounce_delay or 0.35
ret._private = {}
ret._private.metadata_timer = nil
ret._private.cmd = "playerctl "
parse_args(ret, args)
emit_player_metadata(ret)
emit_player_position(ret)
emit_player_playback_status(ret)
emit_player_volume(ret)
emit_player_loop_status(ret)
emit_player_shuffle(ret)
return ret
end
function playerctl.mt:__call(...)
return new(...)
end
-- On startup instead of on playerctl object init to make it
-- possible to have more than one of these running
awful.spawn.with_shell("killall playerctl")
return setmetatable(playerctl, playerctl.mt)