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:
parent
ae5bcde75e
commit
9848c3b8f0
|
@ -19,20 +19,25 @@ This module relies on `playerctl` and `curl`. If you have this module disabled,
|
|||
|
||||
To enable: `bling.signal.playerctl.enable()`
|
||||
|
||||
To disable: `bling.signal.playerctl.disable()`
|
||||
|
||||
Here are the signals available:
|
||||
|
||||
```lua
|
||||
-- bling::playerctl::status -- first line is the signal
|
||||
-- playing (boolean) -- indented lines are function parameters
|
||||
-- player_name (string)
|
||||
-- bling::playerctl::title_artist_album
|
||||
-- title (string)
|
||||
-- artist (string)
|
||||
-- album_path (string)
|
||||
-- album_path (string)
|
||||
-- player_name (string)
|
||||
-- bling::playerctl::position
|
||||
-- interval_sec (number)
|
||||
-- length_sec (number)
|
||||
-- bling::playerctl::player_stopped
|
||||
-- (No params)
|
||||
-- player_name (string)
|
||||
-- bling::playerctl::no_players
|
||||
-- (No parameters)
|
||||
```
|
||||
|
||||
### Example Implementation
|
||||
|
@ -47,6 +52,13 @@ local art = wibox.widget {
|
|||
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 {
|
||||
markup = 'Nothing Playing',
|
||||
align = 'center',
|
||||
|
@ -63,11 +75,12 @@ local artist_widget = wibox.widget {
|
|||
|
||||
-- Get Song Info
|
||||
awesome.connect_signal("bling::playerctl::title_artist_album",
|
||||
function(title, artist, art_path)
|
||||
function(title, artist, art_path, player_name)
|
||||
-- Set art widget
|
||||
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)
|
||||
artist_widget:set_markup_silently(artist)
|
||||
end)
|
||||
|
@ -80,12 +93,46 @@ Here's another example in which you get a notification with the album art, title
|
|||
local naughty = require("naughty")
|
||||
|
||||
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})
|
||||
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
|
||||
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
|
||||
```
|
||||
|
|
|
@ -15,6 +15,9 @@ theme.flash_focus_start_opacity = 0.6 -- the starting opacity
|
|||
theme.flash_focus_step = 0.01 -- the step of animation
|
||||
|
||||
-- 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
|
||||
|
||||
-- tabbed
|
||||
|
|
|
@ -1,132 +1,297 @@
|
|||
-- Playerctl signals
|
||||
--
|
||||
-- Provides:
|
||||
-- bling::playerctl::status
|
||||
-- playing (boolean)
|
||||
-- player_name (string)
|
||||
-- bling::playerctl::title_artist_album
|
||||
-- title (string)
|
||||
-- artist (string)
|
||||
-- artist (string)
|
||||
-- album_path (string)
|
||||
-- player_name (string)
|
||||
-- bling::playerctl::position
|
||||
-- interval_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 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 status_cmd = "playerctl status -F"
|
||||
local ignore = {}
|
||||
local priority = {}
|
||||
local update_on_activity = true
|
||||
local interval = 1
|
||||
|
||||
-- Follow status
|
||||
awful.spawn.easy_async({
|
||||
"pkill", "--full", "--uid", os.getenv("USER"), "^playerctl status"
|
||||
}, function()
|
||||
awful.spawn.with_line_callback(status_cmd, {
|
||||
stdout = function(line)
|
||||
local playing = false
|
||||
if line:find("Playing") then
|
||||
playing = true
|
||||
else
|
||||
playing = false
|
||||
end
|
||||
awesome.emit_signal("bling::playerctl::status", playing)
|
||||
end
|
||||
})
|
||||
collectgarbage("collect")
|
||||
end)
|
||||
-- Track position callback
|
||||
local last_position = -1
|
||||
local last_length = -1
|
||||
function position_cb()
|
||||
local player = manager.players[1]
|
||||
if player then
|
||||
local position = player:get_position() / 1000000
|
||||
local length = player.metadata.value["mpris:length"] / 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
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function emit_player_info()
|
||||
local art_script = [[
|
||||
sh -c '
|
||||
local function get_album_art(url)
|
||||
return awful.util.shell .. [[ -c '
|
||||
|
||||
tmp_dir="$XDG_CACHE_HOME/awesome/"
|
||||
|
||||
if [ -z ${XDG_CACHE_HOME} ]; then
|
||||
if [ -z "$XDG_CACHE_HOME" ]; then
|
||||
tmp_dir="$HOME/.cache/awesome/"
|
||||
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
|
||||
fi
|
||||
|
||||
link="$(playerctl metadata mpris:artUrl)"
|
||||
|
||||
curl -s "$link" --output $tmp_cover_path
|
||||
curl -s ']] .. url .. [[' --output $tmp_cover_path
|
||||
|
||||
echo "$tmp_cover_path"
|
||||
']]
|
||||
end
|
||||
|
||||
-- Command that lists artist and title in a format to find and follow
|
||||
local song_follow_cmd =
|
||||
"playerctl metadata --format 'artist_{{artist}}title_{{title}}' -F"
|
||||
-- Metadata callback for title, artist, and album art
|
||||
local last_player = nil
|
||||
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 prog_cmd = "playerctl position"
|
||||
local length_cmd = "playerctl metadata mpris:length"
|
||||
local data = metadata.value
|
||||
|
||||
awful.widget.watch(prog_cmd, 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
|
||||
awesome.emit_signal("bling::playerctl::position",
|
||||
interval_sec, length_sec / 1000000)
|
||||
local title = data["xesam:title"] or ""
|
||||
local artist = data["xesam:artist"][1] or ""
|
||||
for i = 2, #data["xesam:artist"] do
|
||||
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
|
||||
|
||||
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
|
||||
)
|
||||
-- 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)
|
||||
collectgarbage("collect")
|
||||
end)
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
-- Follow title
|
||||
awful.spawn.easy_async({
|
||||
"pkill", "--full", "--uid", os.getenv("USER"), "^playerctl metadata"
|
||||
}, function()
|
||||
awful.spawn.with_line_callback(song_follow_cmd, {
|
||||
stdout = function(line)
|
||||
local album_path = ""
|
||||
awful.spawn.easy_async_with_shell(art_script, function(out)
|
||||
-- Get album path
|
||||
album_path = out:gsub('%\n', '')
|
||||
-- Get title and artist
|
||||
local artist = line:match('artist_(.*)title_')
|
||||
local title = line:match('title_(.*)')
|
||||
-- If the title is nil or empty then the players stopped
|
||||
if title and title ~= "" then
|
||||
awesome.emit_signal(
|
||||
"bling::playerctl::title_artist_album", title,
|
||||
artist, album_path)
|
||||
else
|
||||
awesome.emit_signal("bling::playerctl::player_stopped")
|
||||
end
|
||||
end)
|
||||
collectgarbage("collect")
|
||||
return true
|
||||
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
|
||||
|
||||
-- Start position timer if its not already running
|
||||
if not position_timer.started then
|
||||
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 any_index = math.huge
|
||||
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
|
||||
})
|
||||
collectgarbage("collect")
|
||||
end)
|
||||
end
|
||||
|
||||
if type(args.player) == "string" then
|
||||
priority[1] = args.player
|
||||
elseif type(args.player) == "table" then
|
||||
priority = args.player
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Emit info
|
||||
-- emit_player_status()
|
||||
-- emit_player_info()
|
||||
local function playerctl_enable(args)
|
||||
args = args or {}
|
||||
-- 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()
|
||||
emit_player_status()
|
||||
emit_player_info()
|
||||
-- Ensure main event loop has started before starting player manager
|
||||
gears.timer.delayed_call(start_manager)
|
||||
end
|
||||
|
||||
local disable = function()
|
||||
awful.spawn.with_shell("pkill --full --uid " .. os.getenv("USER") ..
|
||||
" '^playerctl status -F'")
|
||||
|
||||
awful.spawn.with_shell("pkill --full --uid " .. os.getenv("USER") ..
|
||||
" '^playerctl metadata --format'")
|
||||
local function playerctl_disable()
|
||||
-- Remove manager and timer
|
||||
manager = 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 = ""
|
||||
end
|
||||
|
||||
return {enable = enable, disable = disable}
|
||||
return {enable = playerctl_enable, disable = playerctl_disable}
|
||||
|
|
|
@ -15,6 +15,9 @@ theme.flash_focus_start_opacity = 0.6 -- the starting opacity
|
|||
theme.flash_focus_step = 0.01 -- the step of animation
|
||||
|
||||
-- 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
|
||||
|
||||
-- tabbed
|
||||
|
|
Loading…
Reference in New Issue