Big changes

This commit is contained in:
2022-04-14 00:35:06 +03:00
parent dbb49ea43a
commit fd7c4079fb
96 changed files with 1921 additions and 689 deletions

View File

@@ -1,88 +1,209 @@
-- Playerctl signals
--
-- Provides:
-- bling::playerctl::status
-- playing (boolean)
-- player_name (string)
-- bling::playerctl::title_artist_album
-- metadata
-- title (string)
-- artist (string)
-- album_path (string)
-- album (string)
-- new (bool)
-- player_name (string)
-- bling::playerctl::position
-- position
-- interval_sec (number)
-- length_sec (number)
-- player_name (string)
-- bling::playerctl::no_players
-- playback_status
-- playing (boolean)
-- player_name (string)
-- seeked
-- position (number)
-- player_name (string)
-- volume
-- volume (number)
-- player_name (string)
-- loop_status
-- loop_status (string)
-- player_name (string)
-- shuffle
-- shuffle (boolean)
-- player_name (string)
-- exit
-- player_name (string)
-- no_players
-- (No parameters)
local gears = require("gears")
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 Playerctl = nil
local helpers = require(tostring(...):match(".*bling") .. ".helpers")
local setmetatable = setmetatable
local ipairs = ipairs
local pairs = pairs
local type = type
local capi = { awesome = awesome }
local manager = nil
local metadata_timer = nil
local position_timer = nil
local playerctl = { mt = {} }
local ignore = {}
local priority = {}
local update_on_activity = true
local interval = 1
function playerctl:disable()
-- Restore default settings
self.ignore = {}
self.priority = {}
self.update_on_activity = true
self.interval = 1
self.debounce_delay = 0.35
-- Track position callback
local last_position = -1
local last_length = -1
local function position_cb()
local player = manager.players[1]
-- Reset timers
self._private.manager = nil
self._private.metadata_timer:stop()
self._private.metadata_timer = nil
self._private.position_timer:stop()
self._private.position_timer = nil
-- Reset default values
self._private.last_position = -1
self._private.last_length = -1
self._private.last_player = nil
self._private.last_title = ""
self._private.last_artist = ""
self._private.last_artUrl = ""
end
function playerctl:pause(player)
player = player or self._private.manager.players[1]
if player then
local position = player:get_position() / 1000000
local length = (player.metadata.value["mpris:length"] or 0) / 1000000
if position ~= last_position or length ~= last_length then
awesome.emit_signal(
"bling::playerctl::position",
position,
length,
player.player_name
)
last_position = position
last_length = length
player:pause()
end
end
function playerctl:play(player)
player = player or self._private.manager.players[1]
if player then
player:play()
end
end
function playerctl:stop(player)
player = player or self._private.manager.players[1]
if player then
player:stop()
end
end
function playerctl:play_pause(player)
player = player or self._private.manager.players[1]
if player then
player:play_pause()
end
end
function playerctl:previous(player)
player = player or self._private.manager.players[1]
if player then
player:previous()
end
end
function playerctl:next(player)
player = player or self._private.manager.players[1]
if player then
player:next()
end
end
function playerctl:set_loop_status(loop_status, player)
player = player or self._private.manager.players[1]
if player then
player:set_loop_status(loop_status)
end
end
function playerctl:cycle_loop_status(player)
player = player or self._private.manager.players[1]
if player then
if player.loop_status == "NONE" then
player:set_loop_status("TRACK")
elseif player.loop_status == "TRACK" then
player:set_loop_status("PLAYLIST")
elseif player.loop_status == "PLAYLIST" then
player:set_loop_status("NONE")
end
end
end
local function get_album_art(url)
return awful.util.shell
.. [[ -c '
tmp_dir="$XDG_CACHE_HOME/awesome/"
if [ -z "$XDG_CACHE_HOME" ]; then
tmp_dir="$HOME/.cache/awesome/"
fi
tmp_cover_path="${tmp_dir}cover.png"
if [ ! -d "$tmp_dir" ]; then
mkdir -p $tmp_dir
fi
curl -s ']]
.. url
.. [[' --output $tmp_cover_path
echo "$tmp_cover_path"
']]
function playerctl:set_position(position, player)
player = player or self._private.manager.players[1]
if player then
player:set_position(position * 1000000)
end
end
-- Metadata callback for title, artist, and album art
local last_player = nil
local last_title = ""
local last_artist = ""
local last_artUrl = ""
local function metadata_cb(player, metadata)
if update_on_activity then
manager:move_player_to_top(player)
function playerctl:set_shuffle(shuffle, player)
player = player or self._private.manager.players[1]
if player then
player:set_shuffle(shuffle)
end
end
function playerctl:cycle_shuffle(player)
player = player or self._private.manager.players[1]
if player then
player:set_shuffle(not player.shuffle)
end
end
function playerctl:set_volume(volume, player)
player = player or self._private.manager.players[1]
if player then
player:set_volume(volume)
end
end
function playerctl:get_manager()
return self._private.manager
end
function playerctl:get_active_player()
return self._private.manager.players[1]
end
function playerctl:get_player_of_name(name)
for _, player in ipairs(self._private.manager.players[1]) do
if player.name == name then
return player
end
end
return nil
end
local function emit_metadata_signal(self, title, artist, artUrl, album, new, player_name)
title = gstring.xml_escape(title)
artist = gstring.xml_escape(artist)
album = gstring.xml_escape(album)
-- Spotify client doesn't report its art URL's correctly...
if player_name == "spotify" then
artUrl = artUrl:gsub("open.spotify.com", "i.scdn.co")
end
if artUrl ~= "" then
local art_path = os.tmpname()
helpers.filesystem.save_image_async_curl(artUrl, art_path, function()
self:emit_signal("metadata", title, artist, art_path, album, new, player_name)
capi.awesome.emit_signal("bling::playerctl::title_artist_album", title, artist, art_path, player_name)
end)
else
capi.awesome.emit_signal("bling::playerctl::title_artist_album", title, artist, "", player_name)
self:emit_signal("metadata", title, artist, "", album, new, player_name)
end
end
local function metadata_cb(self, player, metadata)
if self.update_on_activity then
self._private.manager:move_player_to_top(player)
end
local data = metadata.value
@@ -93,101 +214,136 @@ local function metadata_cb(player, metadata)
artist = artist .. ", " .. data["xesam:artist"][i]
end
local artUrl = data["mpris:artUrl"] or ""
-- Spotify client doesn't report its art URL's correctly...
if player.player_name == "spotify" then
artUrl = artUrl:gsub("open.spotify.com", "i.scdn.co")
end
local album = data["xesam:album"] or ""
if player == self._private.manager.players[1] then
self._private.active_player = player
if player == manager.players[1] then
-- Callback can be called even though values we care about haven't
-- changed, so check to see if they have
if
player ~= last_player
or title ~= last_title
or artist ~= last_artist
or artUrl ~= last_artUrl
player ~= self._private.last_player
or title ~= self._private.last_title
or artist ~= self._private.last_artist
or artUrl ~= self._private.last_artUrl
then
if title == "" and artist == "" and artUrl == "" then
return
if (title == "" and artist == "" and artUrl == "") then return end
if self._private.metadata_timer ~= nil and self._private.metadata_timer.started then
self._private.metadata_timer:stop()
end
if metadata_timer ~= nil then
if metadata_timer.started then
metadata_timer:stop()
end
end
metadata_timer = gears.timer({
timeout = 0.3,
self._private.metadata_timer = gtimer {
timeout = self.debounce_delay,
autostart = true,
single_shot = true,
callback = function()
if artUrl ~= "" then
awful.spawn.with_line_callback(get_album_art(artUrl), {
stdout = function(line)
awesome.emit_signal(
"bling::playerctl::title_artist_album",
title,
artist,
line,
player.player_name
)
end,
})
else
awesome.emit_signal(
"bling::playerctl::title_artist_album",
title,
artist,
"",
player.player_name
)
end
end,
})
emit_metadata_signal(self, title, artist, artUrl, album, true, player.player_name)
end
}
-- Re-sync with position timer when track changes
position_timer:again()
last_player = player
last_title = title
last_artist = artist
last_artUrl = artUrl
self._private.position_timer:again()
self._private.last_player = player
self._private.last_title = title
self._private.last_artist = artist
self._private.last_artUrl = artUrl
end
end
end
-- Playback status callback
-- Reported as PLAYING, PAUSED, or STOPPED
local function playback_status_cb(player, status)
if update_on_activity then
manager:move_player_to_top(player)
local function position_cb(self)
local player = self._private.manager.players[1]
if player then
local position = player:get_position() / 1000000
local length = (player.metadata.value["mpris:length"] or 0) / 1000000
if position ~= self._private.last_position or length ~= self._private.last_length then
capi.awesome.emit_signal("bling::playerctl::position", position, length, player.player_name)
self:emit_signal("position", position, length, player.player_name)
self._private.last_position = position
self._private.last_length = length
end
end
end
local function playback_status_cb(self, player, status)
if self.update_on_activity then
self._private.manager:move_player_to_top(player)
end
if player == manager.players[1] then
if player == self._private.manager.players[1] then
self._private.active_player = player
-- Reported as PLAYING, PAUSED, or STOPPED
if status == "PLAYING" then
awesome.emit_signal(
"bling::playerctl::status",
true,
player.player_name
)
self:emit_signal("playback_status", true, player.player_name)
capi.awesome.emit_signal("bling::playerctl::status", true, player.player_name)
else
awesome.emit_signal(
"bling::playerctl::status",
false,
player.player_name
)
self:emit_signal("playback_status", false, player.player_name)
capi.awesome.emit_signal("bling::playerctl::status", false, player.player_name)
end
end
end
local function seeked_cb(self, player, position)
if self.update_on_activity then
self._private.manager:move_player_to_top(player)
end
if player == self._private.manager.players[1] then
self._private.active_player = player
self:emit_signal("seeked", position / 1000000, player.player_name)
end
end
local function volume_cb(self, player, volume)
if self.update_on_activity then
self._private.manager:move_player_to_top(player)
end
if player == self._private.manager.players[1] then
self._private.active_player = player
self:emit_signal("volume", volume, player.player_name)
end
end
local function loop_status_cb(self, player, loop_status)
if self.update_on_activity then
self._private.manager:move_player_to_top(player)
end
if player == self._private.manager.players[1] then
self._private.active_player = player
self:emit_signal("loop_status", loop_status:lower(), player.player_name)
end
end
local function shuffle_cb(self, player, shuffle)
if self.update_on_activity then
self._private.manager:move_player_to_top(player)
end
if player == self._private.manager.players[1] then
self._private.active_player = player
self:emit_signal("shuffle", shuffle, player.player_name)
end
end
local function exit_cb(self, player)
if player == self._private.manager.players[1] then
self:emit_signal("exit", player.player_name)
end
end
-- Determine if player should be managed
local function name_is_selected(name)
if ignore[name.name] then
local function name_is_selected(self, name)
if self.ignore[name.name] then
return false
end
if #priority > 0 then
for _, arg in pairs(priority) do
if #self.priority > 0 then
for _, arg in pairs(self.priority) do
if arg == name.name or arg == "%any" then
return true
end
@@ -199,23 +355,42 @@ local function name_is_selected(name)
end
-- Create new player and connect it to callbacks
local function init_player(name)
if name_is_selected(name) then
local player = Playerctl.Player.new_from_name(name)
manager:manage_player(player)
player.on_playback_status = playback_status_cb
player.on_metadata = metadata_cb
local function init_player(self, name)
if name_is_selected(self, name) then
local player = self._private.lgi_Playerctl.Player.new_from_name(name)
self._private.manager:manage_player(player)
player.on_metadata = function(player, metadata)
metadata_cb(self, player, metadata)
end
player.on_playback_status = function(player, playback_status)
playback_status_cb(self, player, playback_status)
end
player.on_seeked = function(player, position)
seeked_cb(self, player, position)
end
player.on_volume = function(player, volume)
volume_cb(self, player, volume)
end
player.on_loop_status = function(player, loop_status)
loop_status_cb(self, player, loop_status)
end
player.on_shuffle = function(player, shuffle_status)
shuffle_cb(self, player, shuffle_status)
end
player.on_exit = function(player, shuffle_status)
exit_cb(self, player)
end
-- Start position timer if its not already running
if not position_timer.started then
position_timer:again()
if not self._private.position_timer.started then
self._private.position_timer:again()
end
end
end
-- Determine if a player name comes before or after another according to the
-- priority order
local function player_compare_name(name_a, name_b)
local function player_compare_name(self, name_a, name_b)
local any_index = math.huge
local a_match_index = nil
local b_match_index = nil
@@ -224,7 +399,7 @@ local function player_compare_name(name_a, name_b)
return 0
end
for index, name in ipairs(priority) do
for index, name in ipairs(self.priority) do
if name == "%any" then
any_index = (any_index == math.huge) and index or any_index
elseif name == name_a then
@@ -248,103 +423,138 @@ local function player_compare_name(name_a, name_b)
end
-- Sorting function used by manager if a priority order is specified
local function player_compare(a, b)
local player_a = Playerctl.Player(a)
local player_b = Playerctl.Player(b)
return player_compare_name(player_a.player_name, player_b.player_name)
local function player_compare(self, a, b)
local player_a = self._private.lgi_Playerctl.Player(a)
local player_b = self._private.lgi_Playerctl.Player(b)
return player_compare_name(self, player_a.player_name, player_b.player_name)
end
local function start_manager()
manager = Playerctl.PlayerManager()
if #priority > 0 then
manager:set_sort_func(player_compare)
local function get_current_player_info(self, player)
local title = player:get_title() or ""
local artist = player:get_artist() or ""
local artUrl = player:print_metadata_prop("mpris:artUrl") or ""
local album = player:get_album() or ""
emit_metadata_signal(self, title, artist, artUrl, album, false, player.player_name)
playback_status_cb(self, player, player.playback_status)
volume_cb(self, player, player.volume)
loop_status_cb(self, player, player.loop_status)
shuffle_cb(self, player, player.shuffle)
end
local function start_manager(self)
self._private.manager = self._private.lgi_Playerctl.PlayerManager()
if #self.priority > 0 then
self._private.manager:set_sort_func(function(a, b)
return player_compare(self, a, b)
end)
end
-- Timer to update track position at specified interval
position_timer = gears.timer({
timeout = interval,
callback = position_cb,
})
self._private.position_timer = gtimer {
timeout = self.interval,
callback = function()
position_cb(self)
end,
}
-- Manage existing players on startup
for _, name in ipairs(manager.player_names) do
init_player(name)
for _, name in ipairs(self._private.manager.player_names) do
init_player(self, name)
end
if self._private.manager.players[1] then
get_current_player_info(self, self._private.manager.players[1])
end
local _self = self
-- Callback to manage new players
function manager:on_name_appeared(name)
init_player(name)
function self._private.manager:on_name_appeared(name)
init_player(_self, name)
end
-- Callback to check if all players have exited
function manager:on_name_vanished(name)
if #manager.players == 0 then
metadata_timer:stop()
position_timer:stop()
awesome.emit_signal("bling::playerctl::no_players")
function self._private.manager:on_player_appeared(player)
if player == self.players[1] then
_self._private.active_player = player
end
end
function self._private.manager:on_player_vanished(player)
if #self.players == 0 then
_self._private.metadata_timer:stop()
_self._private.position_timer:stop()
_self:emit_signal("no_players")
capi.awesome.emit_signal("bling::playerctl::no_players")
elseif player == _self._private.active_player then
_self._private.active_player = self.players[1]
get_current_player_info(_self, self.players[1])
end
end
end
-- Parse arguments
local function parse_args(args)
if args then
update_on_activity = args.update_on_activity or update_on_activity
interval = args.interval or interval
if type(args.ignore) == "string" then
ignore[args.ignore] = true
elseif type(args.ignore) == "table" then
for _, name in pairs(args.ignore) do
ignore[name] = true
end
local function parse_args(self, args)
self.ignore = {}
if type(args.ignore) == "string" then
self.ignore[args.ignore] = true
elseif type(args.ignore) == "table" then
for _, name in pairs(args.ignore) do
self.ignore[name] = true
end
end
if type(args.player) == "string" then
priority[1] = args.player
elseif type(args.player) == "table" then
priority = args.player
end
self.priority = {}
if type(args.player) == "string" then
self.priority[1] = args.player
elseif type(args.player) == "table" then
self.priority = args.player
end
end
local function playerctl_enable(args)
local function new(args)
args = args or {}
local ret = gobject{}
gtable.crush(ret, playerctl, true)
-- Grab settings from beautiful variables if not set explicitly
args.ignore = args.ignore or beautiful.playerctl_ignore
args.player = args.player or beautiful.playerctl_player
args.update_on_activity = args.update_on_activity
or beautiful.playerctl_update_on_activity
args.interval = args.interval
or beautiful.playerctl_position_update_interval
parse_args(args)
ret.update_on_activity = args.update_on_activity or
beautiful.playerctl_update_on_activity or 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
parse_args(ret, args)
ret._private = {}
-- Metadata callback for title, artist, and album art
ret._private.last_player = nil
ret._private.last_title = ""
ret._private.last_artist = ""
ret._private.last_artUrl = ""
-- Track position callback
ret._private.last_position = -1
ret._private.last_length = -1
-- Grab playerctl library
Playerctl = require("lgi").Playerctl
ret._private.lgi_Playerctl = require("lgi").Playerctl
ret._private.manager = nil
ret._private.metadata_timer = nil
ret._private.position_timer = nil
-- Ensure main event loop has started before starting player manager
gears.timer.delayed_call(start_manager)
gtimer.delayed_call(function()
start_manager(ret)
end)
return ret
end
local function playerctl_disable()
-- Remove manager and timer
manager = nil
metadata_timer:stop()
metadata_timer = nil
position_timer:stop()
position_timer = nil
-- Restore default settings
ignore = {}
priority = {}
update_on_activity = true
interval = 1
-- Reset default values
last_position = -1
last_length = -1
last_player = nil
last_title = ""
last_artist = ""
last_artUrl = ""
function playerctl.mt:__call(...)
return new(...)
end
return { enable = playerctl_enable, disable = playerctl_disable }
return setmetatable(playerctl, playerctl.mt)