-- Playerctl signals -- -- Provides: -- bling::playerctl::status -- playing (boolean) -- player_name (string) -- bling::playerctl::title_artist_album -- title (string) -- artist (string) -- album_path (string) -- player_name (string) -- bling::playerctl::position -- interval_sec (number) -- length_sec (number) -- player_name (string) -- bling::playerctl::no_players -- (No parameters) local gears = require("gears") local awful = require("awful") local beautiful = require("beautiful") local Playerctl = nil local manager = nil local position_timer = nil local ignore = {} local priority = {} local update_on_activity = true local interval = 1 -- Track position callback local last_position = -1 local last_length = -1 local 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 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" ']] end -- Metadata callback for title, artist, and album art local last_player = nil local last_title = "" local last_artist = "" local last_artUrl = "" local index = 0 local function metadata_cb(player, metadata) if update_on_activity then manager:move_player_to_top(player) end local data = metadata.value 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 index = index + 1 if (player ~= last_player or title ~= last_title or artist ~= last_artist) and (artUrl ~= "" or index >= 2) then if (title == "" and artist == "" and artUrl == "") then return end index = 0 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 -- 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 local 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 local 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 return false end 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 end if type(args.player) == "string" then priority[1] = args.player elseif type(args.player) == "table" then priority = args.player end end end 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) -- Grab playerctl library Playerctl = require("lgi").Playerctl -- Ensure main event loop has started before starting player manager gears.timer.delayed_call(start_manager) end 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 = playerctl_enable, disable = playerctl_disable}