Playerctl signals backend rewritten with playerctl library (#43)

* Playerctl signals backend rewritten with playerctl library

* Removed collectgarbage() calls and updated docs

* All docs updated
This commit is contained in:
HumblePresent 2021-04-18 18:33:35 -05:00 committed by GitHub
parent ae5bcde75e
commit 9848c3b8f0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 314 additions and 96 deletions

View File

@ -19,20 +19,25 @@ This module relies on `playerctl` and `curl`. If you have this module disabled,
To enable: `bling.signal.playerctl.enable()` To enable: `bling.signal.playerctl.enable()`
To disable: `bling.signal.playerctl.disable()`
Here are the signals available: Here are the signals available:
```lua ```lua
-- bling::playerctl::status -- first line is the signal -- bling::playerctl::status -- first line is the signal
-- playing (boolean) -- indented lines are function parameters -- playing (boolean) -- indented lines are function parameters
-- player_name (string)
-- bling::playerctl::title_artist_album -- bling::playerctl::title_artist_album
-- title (string) -- title (string)
-- artist (string) -- artist (string)
-- album_path (string) -- album_path (string)
-- player_name (string)
-- bling::playerctl::position -- bling::playerctl::position
-- interval_sec (number) -- interval_sec (number)
-- length_sec (number) -- length_sec (number)
-- bling::playerctl::player_stopped -- player_name (string)
-- (No params) -- bling::playerctl::no_players
-- (No parameters)
``` ```
### Example Implementation ### Example Implementation
@ -47,6 +52,13 @@ local art = wibox.widget {
widget = wibox.widget.imagebox widget = wibox.widget.imagebox
} }
local name_widget = wibox.widget {
markup = 'No players',
align = 'center',
valign = 'center',
widget = wibox.widget.textbox
}
local title_widget = wibox.widget { local title_widget = wibox.widget {
markup = 'Nothing Playing', markup = 'Nothing Playing',
align = 'center', align = 'center',
@ -63,11 +75,12 @@ local artist_widget = wibox.widget {
-- Get Song Info -- Get Song Info
awesome.connect_signal("bling::playerctl::title_artist_album", awesome.connect_signal("bling::playerctl::title_artist_album",
function(title, artist, art_path) function(title, artist, art_path, player_name)
-- Set art widget -- Set art widget
art:set_image(gears.surface.load_uncached(art_path)) art:set_image(gears.surface.load_uncached(art_path))
-- Set title and artist widgets -- Set player name, title and artist widgets
name_widget:set_markup_silently(player_name)
title_widget:set_markup_silently(title) title_widget:set_markup_silently(title)
artist_widget:set_markup_silently(artist) artist_widget:set_markup_silently(artist)
end) end)
@ -80,12 +93,46 @@ Here's another example in which you get a notification with the album art, title
local naughty = require("naughty") local naughty = require("naughty")
awesome.connect_signal("bling::playerctl::title_artist_album", awesome.connect_signal("bling::playerctl::title_artist_album",
function(title, artist, art_path) function(title, artist, art_path, player_name)
naughty.notify({title = title, text = artist, image = art_path}) naughty.notify({title = title, text = artist, image = art_path})
end) end)
``` ```
### Theme Variables ### Theme Variables and Configuration
By default, this module will output signals from the most recently active player. If you wish to customize the behavior furthur, the following configuration options are available.
- `ignore`: This option is either a string with a single name or a table of strings containing names of players that will be ignored by this module. It is empty by default.
- `player`: This option is either a string with a single name or a table of strings containing names of players this module will emit signals for. It also acts as a way to prioritize certain players over others with players listed earlier in the table being preferred over players listed later. The special name `%any` can be used once to match any player not found in the list. It is empty by default.
- `update_on_activity`: This option is a boolean that, when true, will cause the module to output signals from the most recently active player while still adhering to the player priority specified with the `player` option. If `false`, the module will output signals from the player that started first, again, while still adhering to the player priority. It is `true` by default.
- `interval`: This option is a number specifying the update interval for fetching the player position. It is 1 by default.
These options can be set through a call to `bling.signal.playerctl.enable()` or these theme variables:
```lua ```lua
theme.playerctl_position_update_interval = 1 -- the update interval for fetching the position from playerctl theme.playerctl_ignore = {}
theme.playerctl_player = {}
theme.playerctl_update_on_activity = true
theme.playerctl_position_update_interval = 1
```
#### Example Configurations
```lua
-- Prioritize ncspot over all other players and ignore firefox players (e.g. YouTube and Twitch tabs) completely
bling.signal.playerctl.enable {
ignore = "firefox",
player = {"ncspot", "%any"}
}
-- OR in your theme file:
-- Same config as above but with theme variables
theme.playerctl_ignore = "firefox"
theme.playerctl_player = {"ncspot", "%any"}
-- Prioritize vlc over all other players and deprioritize spotify
theme.playerctl_player = {"vlc", "%any", "spotify"}
-- Disable priority of most recently active players
theme.playerctl_update_on_activity = false
``` ```

View File

@ -15,6 +15,9 @@ theme.flash_focus_start_opacity = 0.6 -- the starting opacity
theme.flash_focus_step = 0.01 -- the step of animation theme.flash_focus_step = 0.01 -- the step of animation
-- playerctl signal -- playerctl signal
theme.playerctl_ignore = {} -- list of players to be ignored
theme.playerctl_player = {} -- list of players to be used in priority order
theme.playerctl_update_on_activity = true -- whether to prioritize the most recently active players or not
theme.playerctl_position_update_interval = 1 -- the update interval for fetching the position from playerctl theme.playerctl_position_update_interval = 1 -- the update interval for fetching the position from playerctl
-- tabbed -- tabbed

View File

@ -1,132 +1,297 @@
-- Playerctl signals
-- --
-- Provides: -- Provides:
-- bling::playerctl::status -- bling::playerctl::status
-- playing (boolean) -- playing (boolean)
-- player_name (string)
-- bling::playerctl::title_artist_album -- bling::playerctl::title_artist_album
-- title (string) -- title (string)
-- artist (string) -- artist (string)
-- album_path (string) -- album_path (string)
-- player_name (string)
-- bling::playerctl::position -- bling::playerctl::position
-- interval_sec (number) -- interval_sec (number)
-- length_sec (number) -- length_sec (number)
-- bling::playerctl::player_stopped -- player_name (string)
-- -- bling::playerctl::no_players
-- (No parameters)
local gears = require("gears")
local awful = require("awful") local awful = require("awful")
local beautiful = require("beautiful") local beautiful = require("beautiful")
local Playerctl = require("lgi").Playerctl
local interval = beautiful.playerctl_position_update_interval or 1 local manager = nil
local position_timer = nil
local function emit_player_status() local ignore = {}
local status_cmd = "playerctl status -F" local priority = {}
local update_on_activity = true
local interval = 1
-- Follow status -- Track position callback
awful.spawn.easy_async({ local last_position = -1
"pkill", "--full", "--uid", os.getenv("USER"), "^playerctl status" local last_length = -1
}, function() function position_cb()
awful.spawn.with_line_callback(status_cmd, { local player = manager.players[1]
stdout = function(line) if player then
local playing = false local position = player:get_position() / 1000000
if line:find("Playing") then local length = player.metadata.value["mpris:length"] / 1000000
playing = true if position ~= last_position or length ~= last_length then
else awesome.emit_signal("bling::playerctl::position",
playing = false position,
end length,
awesome.emit_signal("bling::playerctl::status", playing) player.player_name)
end last_position = position
}) last_length = length
collectgarbage("collect") end
end) end
end end
local function emit_player_info() local function get_album_art(url)
local art_script = [[ return awful.util.shell .. [[ -c '
sh -c '
tmp_dir="$XDG_CACHE_HOME/awesome/" tmp_dir="$XDG_CACHE_HOME/awesome/"
if [ -z ${XDG_CACHE_HOME} ]; then if [ -z "$XDG_CACHE_HOME" ]; then
tmp_dir="$HOME/.cache/awesome/" tmp_dir="$HOME/.cache/awesome/"
fi fi
tmp_cover_path=${tmp_dir}"cover.png" tmp_cover_path="${tmp_dir}cover.png"
if [ ! -d $tmp_dir ]; then if [ ! -d "$tmp_dir" ]; then
mkdir -p $tmp_dir mkdir -p $tmp_dir
fi fi
link="$(playerctl metadata mpris:artUrl)" curl -s ']] .. url .. [[' --output $tmp_cover_path
curl -s "$link" --output $tmp_cover_path
echo "$tmp_cover_path" echo "$tmp_cover_path"
']] ']]
end
-- Command that lists artist and title in a format to find and follow -- Metadata callback for title, artist, and album art
local song_follow_cmd = local last_player = nil
"playerctl metadata --format 'artist_{{artist}}title_{{title}}' -F" local last_title = ""
local last_artist = ""
local last_artUrl = ""
function metadata_cb(player, metadata)
if update_on_activity then
manager:move_player_to_top(player)
end
-- Progress Cmds local data = metadata.value
local prog_cmd = "playerctl position"
local length_cmd = "playerctl metadata mpris:length"
awful.widget.watch(prog_cmd, interval, function(_, interval) local title = data["xesam:title"] or ""
awful.spawn.easy_async_with_shell(length_cmd, function(length) local artist = data["xesam:artist"][1] or ""
local length_sec = tonumber(length) -- in microseconds for i = 2, #data["xesam:artist"] do
local interval_sec = tonumber(interval) -- in seconds artist = artist .. ", " .. data["xesam:artist"][i]
if length_sec and interval_sec then end
if interval_sec >= 0 and length_sec > 0 then local artUrl = data["mpris:artUrl"] or ""
awesome.emit_signal("bling::playerctl::position", -- Spotify client doesn't report its art URL's correctly...
interval_sec, length_sec / 1000000) if player.player_name == "spotify" then
artUrl = artUrl:gsub("open.spotify.com", "i.scdn.co")
end
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
then
awful.spawn.easy_async_with_shell(get_album_art(artUrl),
function(stdout)
art_path = stdout:gsub('%\n', '')
awesome.emit_signal("bling::playerctl::title_artist_album",
title,
artist,
art_path,
player.player_name)
end end
)
-- Re-sync with position timer when track changes
position_timer:again()
last_player = player
last_title = title
last_artist = artist
last_artUrl = artUrl
end
end
end
-- Playback status callback
-- Reported as PLAYING, PAUSED, or STOPPED
function playback_status_cb(player, status)
if update_on_activity then
manager:move_player_to_top(player)
end
if player == manager.players[1] then
if status == "PLAYING" then
awesome.emit_signal("bling::playerctl::status", true, player.player_name)
else
awesome.emit_signal("bling::playerctl::status", false, player.player_name)
end
end
end
-- Determine if player should be managed
function name_is_selected(name)
if ignore[name.name] then
return false
end
if #priority > 0 then
for _, arg in pairs(priority) do
if arg == name.name or arg == "%any" then
return true
end end
end) end
collectgarbage("collect") return false
end) end
-- Follow title return true
awful.spawn.easy_async({ end
"pkill", "--full", "--uid", os.getenv("USER"), "^playerctl metadata"
}, function() -- Create new player and connect it to callbacks
awful.spawn.with_line_callback(song_follow_cmd, { local function init_player(name)
stdout = function(line) if name_is_selected(name) then
local album_path = "" local player = Playerctl.Player.new_from_name(name)
awful.spawn.easy_async_with_shell(art_script, function(out) manager:manage_player(player)
-- Get album path player.on_playback_status = playback_status_cb
album_path = out:gsub('%\n', '') player.on_metadata = metadata_cb
-- Get title and artist
local artist = line:match('artist_(.*)title_') -- Start position timer if its not already running
local title = line:match('title_(.*)') if not position_timer.started then
-- If the title is nil or empty then the players stopped position_timer:again()
if title and title ~= "" then end
awesome.emit_signal( end
"bling::playerctl::title_artist_album", title, end
artist, album_path)
else -- Determine if a player name comes before or after another according to the
awesome.emit_signal("bling::playerctl::player_stopped") -- priority order
end local function player_compare_name(name_a, name_b)
end) local any_index = math.huge
collectgarbage("collect") local a_match_index = nil
local b_match_index = nil
if name_a == name_b then
return 0
end
for index, name in ipairs(priority) do
if name == "%any" then
any_index = (any_index == math.huge) and index or any_index
elseif name == name_a then
a_match_index = a_match_index or index
elseif name == name_b then
b_match_index = b_match_index or index
end
end
if not a_match_index and not b_match_index then
return 0
elseif not a_match_index then
return (b_match_index < any_index) and 1 or -1
elseif not b_match_index then
return (a_match_index < any_index) and -1 or 1
elseif a_match_index == b_match_index then
return 0
else
return (a_match_index < b_match_index) and -1 or 1
end
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)
end
local function start_manager()
manager = Playerctl.PlayerManager()
if #priority > 0 then
manager:set_sort_func(player_compare)
end
-- Timer to update track position at specified interval
position_timer = gears.timer {
timeout = interval,
callback = position_cb
}
-- Manage existing players on startup
for _, name in ipairs(manager.player_names) do
init_player(name)
end
-- Callback to manage new players
function manager:on_name_appeared(name)
init_player(name)
end
-- Callback to check if all players have exited
function manager:on_name_vanished(name)
if #manager.players == 0 then
position_timer:stop()
awesome.emit_signal("bling::playerctl::no_players")
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 end
}) end
collectgarbage("collect")
end) if type(args.player) == "string" then
priority[1] = args.player
elseif type(args.player) == "table" then
priority = args.player
end
end
end end
-- Emit info local function playerctl_enable(args)
-- emit_player_status() args = args or {}
-- emit_player_info() -- 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)
local enable = function() -- Ensure main event loop has started before starting player manager
emit_player_status() gears.timer.delayed_call(start_manager)
emit_player_info()
end end
local disable = function() local function playerctl_disable()
awful.spawn.with_shell("pkill --full --uid " .. os.getenv("USER") .. -- Remove manager and timer
" '^playerctl status -F'") manager = nil
position_timer:stop()
awful.spawn.with_shell("pkill --full --uid " .. os.getenv("USER") .. position_timer = nil
" '^playerctl metadata --format'") -- 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 = ""
end end
return {enable = enable, disable = disable} return {enable = playerctl_enable, disable = playerctl_disable}

View File

@ -15,6 +15,9 @@ theme.flash_focus_start_opacity = 0.6 -- the starting opacity
theme.flash_focus_step = 0.01 -- the step of animation theme.flash_focus_step = 0.01 -- the step of animation
-- playerctl signal -- playerctl signal
theme.playerctl_ignore = {} -- list of players to be ignored
theme.playerctl_player = {} -- list of players to be used in priority order
theme.playerctl_update_on_activity = true -- whether to prioritize the most recently active players or not
theme.playerctl_position_update_interval = 1 -- the update interval for fetching the position from playerctl theme.playerctl_position_update_interval = 1 -- the update interval for fetching the position from playerctl
-- tabbed -- tabbed