Add playerctl v2 backends (#142)
* Add playerctl v2 backends * Player should be the last arguement as it's optional * Make v2 backwards compatible with v1 * Make cli v2 backwards compatible as well * Delete playerctl and rename playerctl_v2 to playerctl * Add deprecation for global signals * Update the docs * Player should be optional * Fix image downloading failing on some cases
This commit is contained in:
parent
298a6e6c8d
commit
338dba6292
|
@ -17,27 +17,65 @@ This module relies on `playerctl` and `curl`. If you have this module disabled,
|
||||||
|
|
||||||
### Usage
|
### Usage
|
||||||
|
|
||||||
To enable: `bling.signal.playerctl.enable()`
|
To enable: `playerctl = bling.signal.playerctl.lib/cli()`
|
||||||
|
|
||||||
To disable: `bling.signal.playerctl.disable()`
|
To disable: `playerctl:disable()`
|
||||||
|
|
||||||
Here are the signals available:
|
|
||||||
|
|
||||||
|
Playerctl_lib signals available:
|
||||||
```lua
|
```lua
|
||||||
-- bling::playerctl::status -- first line is the signal
|
-- metadata
|
||||||
-- playing (boolean) -- indented lines are function parameters
|
-- title (string)
|
||||||
-- player_name (string)
|
-- artist (string)
|
||||||
-- bling::playerctl::title_artist_album
|
-- album_path (string)
|
||||||
-- title (string)
|
-- album (string)
|
||||||
-- artist (string)
|
-- new (bool)
|
||||||
-- album_path (string)
|
-- player_name (string)
|
||||||
-- player_name (string)
|
-- position
|
||||||
-- bling::playerctl::position
|
-- interval_sec (number)
|
||||||
-- interval_sec (number)
|
-- length_sec (number)
|
||||||
-- length_sec (number)
|
-- player_name (string)
|
||||||
-- player_name (string)
|
-- playback_status
|
||||||
-- bling::playerctl::no_players
|
-- playing (boolean)
|
||||||
-- (No parameters)
|
-- 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)
|
||||||
|
```
|
||||||
|
|
||||||
|
Playerctl_cli signals available:
|
||||||
|
```LUA
|
||||||
|
-- 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)
|
||||||
```
|
```
|
||||||
|
|
||||||
### Example Implementation
|
### Example Implementation
|
||||||
|
@ -74,10 +112,11 @@ local artist_widget = wibox.widget {
|
||||||
}
|
}
|
||||||
|
|
||||||
-- Get Song Info
|
-- Get Song Info
|
||||||
awesome.connect_signal("bling::playerctl::title_artist_album",
|
playerctl = bling.signal.playerctl.lib()
|
||||||
function(title, artist, art_path, player_name)
|
playerctl:connect_signal("metadata",
|
||||||
|
function(title, artist, album_path, album, new, player_name)
|
||||||
-- Set art widget
|
-- Set art widget
|
||||||
art:set_image(gears.surface.load_uncached(art_path))
|
art:set_image(gears.surface.load_uncached(album_path))
|
||||||
|
|
||||||
-- Set player name, title and artist widgets
|
-- Set player name, title and artist widgets
|
||||||
name_widget:set_markup_silently(player_name)
|
name_widget:set_markup_silently(player_name)
|
||||||
|
@ -92,9 +131,11 @@ Here's another example in which you get a notification with the album art, title
|
||||||
```lua
|
```lua
|
||||||
local naughty = require("naughty")
|
local naughty = require("naughty")
|
||||||
|
|
||||||
awesome.connect_signal("bling::playerctl::title_artist_album",
|
playerctl:connect_signal("metadata",
|
||||||
function(title, artist, art_path, player_name)
|
function(title, artist, album_path, album, new, player_name)
|
||||||
naughty.notify({title = title, text = artist, image = art_path})
|
if new == true then
|
||||||
|
naughty.notify({title = title, text = artist, image = album_path})
|
||||||
|
end
|
||||||
end)
|
end)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -103,13 +144,11 @@ By default, this module will output signals from the most recently active player
|
||||||
|
|
||||||
| Option | playerctl_cli | playerctl_lib |
|
| Option | playerctl_cli | playerctl_lib |
|
||||||
| ------------------- | ------------------ | ------------------ |
|
| ------------------- | ------------------ | ------------------ |
|
||||||
| backend | :heavy_check_mark: | :heavy_check_mark: |
|
| ignore | :heavy_check_mark: | :heavy_check_mark: |
|
||||||
| ignore | | :heavy_check_mark: |
|
| player | :heavy_check_mark: | :heavy_check_mark: |
|
||||||
| player | | :heavy_check_mark: |
|
|
||||||
| update_on_activity | | :heavy_check_mark: |
|
| update_on_activity | | :heavy_check_mark: |
|
||||||
| interval | :heavy_check_mark: | :heavy_check_mark: |
|
| interval | :heavy_check_mark: | :heavy_check_mark: |
|
||||||
|
| debounce_delay | :heavy_check_mark: | :heavy_check_mark: |
|
||||||
- `backend`: This is a string containing the name of the backend that will be used to produce the playerctl signals, either `playerctl_cli` or `playertl_lib`. `playerctl_cli` is used by default because it is supported on most if not all systems. That said, if the playerctl package for your distribution supports the `playerctl_lib` backend, it is recommended, because it supports all the configuration options as seen in the table above and uses less system resources. If you are not sure if your package supports the `playerctl_lib` backend you can simply try it and will receive an error message from Awesome upon calling `bling.signal.playerctl.enable()` if it is not supported. See the examples below for how to set configuration options.
|
|
||||||
|
|
||||||
- `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.
|
- `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.
|
||||||
|
|
||||||
|
@ -119,9 +158,11 @@ By default, this module will output signals from the most recently active player
|
||||||
|
|
||||||
- `interval`: This option is a number specifying the update interval for fetching the player position. It is 1 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:
|
- `debounce_delay`: This option is a number specifying the debounce timer interval. If a new metadata signal gets emitted before debounce_delay has passed, the last signal will be dropped.
|
||||||
|
This is to help with some players sending multiple signals. It is `0.35` by default.
|
||||||
|
|
||||||
|
These options can be set through a call to `bling.signal.playerctl.lib/cli()` or these theme variables:
|
||||||
```lua
|
```lua
|
||||||
theme.playerctl_backend = "playerctl_cli"
|
|
||||||
theme.playerctl_ignore = {}
|
theme.playerctl_ignore = {}
|
||||||
theme.playerctl_player = {}
|
theme.playerctl_player = {}
|
||||||
theme.playerctl_update_on_activity = true
|
theme.playerctl_update_on_activity = true
|
||||||
|
@ -131,15 +172,13 @@ theme.playerctl_position_update_interval = 1
|
||||||
#### Example Configurations
|
#### Example Configurations
|
||||||
```lua
|
```lua
|
||||||
-- Prioritize ncspot over all other players and ignore firefox players (e.g. YouTube and Twitch tabs) completely
|
-- Prioritize ncspot over all other players and ignore firefox players (e.g. YouTube and Twitch tabs) completely
|
||||||
bling.signal.playerctl.enable {
|
playerctl = bling.signal.playerctl.lib {
|
||||||
backend = "playerctl_lib",
|
|
||||||
ignore = "firefox",
|
ignore = "firefox",
|
||||||
player = {"ncspot", "%any"}
|
player = {"ncspot", "%any"}
|
||||||
}
|
}
|
||||||
|
|
||||||
-- OR in your theme file:
|
-- OR in your theme file:
|
||||||
-- Same config as above but with theme variables
|
-- Same config as above but with theme variables
|
||||||
theme.playerctl_backend = "playerctl_lib"
|
|
||||||
theme.playerctl_ignore = "firefox"
|
theme.playerctl_ignore = "firefox"
|
||||||
theme.playerctl_player = {"ncspot", "%any"}
|
theme.playerctl_player = {"ncspot", "%any"}
|
||||||
|
|
||||||
|
@ -148,7 +187,6 @@ theme.playerctl_backend = "playerctl_lib"
|
||||||
theme.playerctl_player = {"vlc", "%any", "spotify"}
|
theme.playerctl_player = {"vlc", "%any", "spotify"}
|
||||||
|
|
||||||
-- Disable priority of most recently active players
|
-- Disable priority of most recently active players
|
||||||
theme.playerctl_backend = "playerctl_lib"
|
|
||||||
theme.playerctl_update_on_activity = false
|
theme.playerctl_update_on_activity = false
|
||||||
|
|
||||||
-- Only emit the position signal every 2 seconds
|
-- Only emit the position signal every 2 seconds
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
local Gio = require("lgi").Gio
|
local Gio = require("lgi").Gio
|
||||||
|
local awful = require("awful")
|
||||||
|
local string = string
|
||||||
|
|
||||||
local _filesystem = {}
|
local _filesystem = {}
|
||||||
|
|
||||||
|
@ -50,4 +52,11 @@ function _filesystem.list_directory_files(path, exts, recursive)
|
||||||
return files
|
return files
|
||||||
end
|
end
|
||||||
|
|
||||||
|
function _filesystem.save_image_async_curl(url, filepath, callback)
|
||||||
|
awful.spawn.with_line_callback(string.format("curl -L -s %s -o %s", url, filepath),
|
||||||
|
{
|
||||||
|
exit=callback
|
||||||
|
})
|
||||||
|
end
|
||||||
|
|
||||||
return _filesystem
|
return _filesystem
|
||||||
|
|
|
@ -1 +1,3 @@
|
||||||
return { playerctl = require(... .. ".playerctl") }
|
return {
|
||||||
|
playerctl = require(... .. ".playerctl"),
|
||||||
|
}
|
||||||
|
|
|
@ -1,4 +1,7 @@
|
||||||
|
local awful = require("awful")
|
||||||
|
local gtimer = require("gears.timer")
|
||||||
local beautiful = require("beautiful")
|
local beautiful = require("beautiful")
|
||||||
|
local naughty = require("naughty")
|
||||||
|
|
||||||
-- Use CLI backend as default as it is supported on most if not all systems
|
-- Use CLI backend as default as it is supported on most if not all systems
|
||||||
local backend_config = beautiful.playerctl_backend or "playerctl_cli"
|
local backend_config = beautiful.playerctl_backend or "playerctl_cli"
|
||||||
|
@ -7,13 +10,37 @@ local backends = {
|
||||||
playerctl_lib = require(... .. ".playerctl_lib"),
|
playerctl_lib = require(... .. ".playerctl_lib"),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
local backend = nil
|
||||||
|
|
||||||
local function enable_wrapper(args)
|
local function enable_wrapper(args)
|
||||||
|
local open = naughty.action { name = "Open" }
|
||||||
|
|
||||||
|
open:connect_signal("invoked", function()
|
||||||
|
awful.spawn("xdg-open https://blingcorp.github.io/bling/#/signals/pctl")
|
||||||
|
end)
|
||||||
|
|
||||||
|
gtimer.delayed_call(function()
|
||||||
|
naughty.notify({
|
||||||
|
title = "Bling Error",
|
||||||
|
text = "Global signals are deprecated! Please take a look at the playerctl documentation.",
|
||||||
|
app_name = "Bling Error",
|
||||||
|
app_icon = "system-error",
|
||||||
|
actions = { open }
|
||||||
|
})
|
||||||
|
end)
|
||||||
|
|
||||||
backend_config = (args and args.backend) or backend_config
|
backend_config = (args and args.backend) or backend_config
|
||||||
backends[backend_config].enable(args)
|
backend = backends[backend_config](args)
|
||||||
|
return backend
|
||||||
end
|
end
|
||||||
|
|
||||||
local function disable_wrapper()
|
local function disable_wrapper()
|
||||||
backends[backend_config].disable()
|
backend:disable()
|
||||||
end
|
end
|
||||||
|
|
||||||
return { enable = enable_wrapper, disable = disable_wrapper }
|
return {
|
||||||
|
lib = backends.playerctl_lib,
|
||||||
|
cli = backends.playerctl_cli,
|
||||||
|
enable = enable_wrapper,
|
||||||
|
disable = disable_wrapper
|
||||||
|
}
|
|
@ -1,151 +1,348 @@
|
||||||
|
-- Playerctl signals
|
||||||
--
|
--
|
||||||
-- Provides:
|
-- Provides:
|
||||||
-- bling::playerctl::status
|
-- metadata
|
||||||
-- playing (boolean)
|
|
||||||
-- bling::playerctl::title_artist_album
|
|
||||||
-- title (string)
|
-- title (string)
|
||||||
-- artist (string)
|
-- artist (string)
|
||||||
-- album_path (string)
|
-- album_path (string)
|
||||||
-- bling::playerctl::position
|
-- album (string)
|
||||||
|
-- player_name (string)
|
||||||
|
-- position
|
||||||
-- interval_sec (number)
|
-- interval_sec (number)
|
||||||
-- length_sec (number)
|
-- length_sec (number)
|
||||||
-- bling::playerctl::no_players
|
-- playback_status
|
||||||
--
|
-- playing (boolean)
|
||||||
|
-- volume
|
||||||
|
-- volume (number)
|
||||||
|
-- loop_status
|
||||||
|
-- loop_status (string)
|
||||||
|
-- shuffle
|
||||||
|
-- shuffle (bool)
|
||||||
|
-- no_players
|
||||||
|
-- (No parameters)
|
||||||
|
|
||||||
local awful = require("awful")
|
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 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 interval = beautiful.playerctl_position_update_interval or 1
|
local playerctl = { mt = {} }
|
||||||
|
|
||||||
local function emit_player_status()
|
function playerctl:disable()
|
||||||
local status_cmd = "playerctl status -F"
|
self._private.metadata_timer:stop()
|
||||||
|
self._private.metadata_timer = nil
|
||||||
-- Follow status
|
awful.spawn.with_shell("killall playerctl")
|
||||||
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)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
local function emit_player_info()
|
function playerctl:pause(player)
|
||||||
local art_script = [[
|
if player ~= nil then
|
||||||
sh -c '
|
awful.spawn.with_shell("playerctl --player=" .. player .. " pause")
|
||||||
|
else
|
||||||
|
awful.spawn.with_shell(self._private.cmd .. "pause")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
tmp_dir="$XDG_CACHE_HOME/awesome/"
|
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
|
||||||
|
|
||||||
if [ -z ${XDG_CACHE_HOME} ]; then
|
function playerctl:stop(player)
|
||||||
tmp_dir="$HOME/.cache/awesome/"
|
if player ~= nil then
|
||||||
fi
|
awful.spawn.with_shell("playerctl --player=" .. player .. " stop")
|
||||||
|
else
|
||||||
|
awful.spawn.with_shell(self._private.cmd .. "stop")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
tmp_cover_path=${tmp_dir}"cover.png"
|
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
|
||||||
|
|
||||||
if [ ! -d $tmp_dir ]; then
|
function playerctl:previous(player)
|
||||||
mkdir -p $tmp_dir
|
if player ~= nil then
|
||||||
fi
|
awful.spawn.with_shell("playerctl --player=" .. player .. " previous")
|
||||||
|
else
|
||||||
|
awful.spawn.with_shell(self._private.cmd .. "previous")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
link="$(playerctl metadata mpris:artUrl)"
|
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
|
||||||
|
|
||||||
curl -s "$link" --output $tmp_cover_path
|
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
|
||||||
|
|
||||||
echo "$tmp_cover_path"
|
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
|
||||||
|
|
||||||
-- Command that lists artist and title in a format to find and follow
|
if player ~= nil then
|
||||||
local song_follow_cmd =
|
awful.spawn.easy_async_with_shell("playerctl --player=" .. player .. " loop", function(stdout)
|
||||||
"playerctl metadata --format 'artist_{{artist}}title_{{title}}' -F"
|
set_loop_status(stdout)
|
||||||
|
end)
|
||||||
|
else
|
||||||
|
set_loop_status(self._private.loop_status)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
-- Progress Cmds
|
function playerctl:set_position(position, player)
|
||||||
local prog_cmd = "playerctl position"
|
if player ~= nil then
|
||||||
local length_cmd = "playerctl metadata mpris:length"
|
awful.spawn.with_shell("playerctl --player=" .. player .. " position " .. position)
|
||||||
|
else
|
||||||
|
awful.spawn.with_shell(self._private.cmd .. "position " .. position)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
awful.widget.watch(prog_cmd, interval, function(_, interval)
|
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)
|
awful.spawn.easy_async_with_shell(length_cmd, function(length)
|
||||||
local length_sec = tonumber(length) -- in microseconds
|
local length_sec = tonumber(length) -- in microseconds
|
||||||
local interval_sec = tonumber(interval) -- in seconds
|
local interval_sec = tonumber(interval) -- in seconds
|
||||||
if length_sec and interval_sec then
|
if length_sec and interval_sec then
|
||||||
if interval_sec >= 0 and length_sec > 0 then
|
if interval_sec >= 0 and length_sec > 0 then
|
||||||
awesome.emit_signal(
|
self:emit_signal("position", interval_sec, length_sec / 1000000)
|
||||||
"bling::playerctl::position",
|
capi.awesome.emit_signal("bling::playerctl::position", interval_sec, length_sec / 1000000)
|
||||||
interval_sec,
|
|
||||||
length_sec / 1000000
|
|
||||||
)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
collectgarbage("collect")
|
collectgarbage("collect")
|
||||||
end)
|
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::no_players")
|
|
||||||
end
|
|
||||||
end)
|
|
||||||
collectgarbage("collect")
|
|
||||||
end,
|
|
||||||
})
|
|
||||||
collectgarbage("collect")
|
|
||||||
end)
|
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Emit info
|
local function emit_player_playback_status(self)
|
||||||
-- emit_player_status()
|
local status_cmd = self._private.cmd .. "status -F"
|
||||||
-- emit_player_info()
|
|
||||||
|
|
||||||
local enable = function(args)
|
awful.spawn.with_line_callback(status_cmd, {
|
||||||
interval = (args and args.interval) or interval
|
stdout = function(line)
|
||||||
emit_player_status()
|
if line:find("Playing") then
|
||||||
emit_player_info()
|
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
|
end
|
||||||
|
|
||||||
local disable = function()
|
local function emit_player_volume(self)
|
||||||
awful.spawn.with_shell(
|
local volume_cmd = self._private.cmd .. "volume -F"
|
||||||
"pkill --full --uid " .. os.getenv("USER") .. " '^playerctl status -F'"
|
|
||||||
)
|
|
||||||
|
|
||||||
awful.spawn.with_shell(
|
awful.spawn.with_line_callback(volume_cmd, {
|
||||||
"pkill --full --uid "
|
stdout = function(line)
|
||||||
.. os.getenv("USER")
|
self:emit_signal("volume", tonumber(line))
|
||||||
.. " '^playerctl metadata --format'"
|
end,
|
||||||
)
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
return { enable = enable, disable = disable }
|
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)
|
||||||
|
|
|
@ -1,88 +1,209 @@
|
||||||
-- Playerctl signals
|
-- Playerctl signals
|
||||||
--
|
--
|
||||||
-- Provides:
|
-- Provides:
|
||||||
-- bling::playerctl::status
|
-- metadata
|
||||||
-- playing (boolean)
|
|
||||||
-- player_name (string)
|
|
||||||
-- bling::playerctl::title_artist_album
|
|
||||||
-- title (string)
|
-- title (string)
|
||||||
-- artist (string)
|
-- artist (string)
|
||||||
-- album_path (string)
|
-- album_path (string)
|
||||||
|
-- album (string)
|
||||||
|
-- new (bool)
|
||||||
-- player_name (string)
|
-- player_name (string)
|
||||||
-- bling::playerctl::position
|
-- position
|
||||||
-- interval_sec (number)
|
-- interval_sec (number)
|
||||||
-- length_sec (number)
|
-- length_sec (number)
|
||||||
-- player_name (string)
|
-- 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)
|
-- (No parameters)
|
||||||
|
|
||||||
local gears = require("gears")
|
|
||||||
local awful = require("awful")
|
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 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 playerctl = { mt = {} }
|
||||||
local metadata_timer = nil
|
|
||||||
local position_timer = nil
|
|
||||||
|
|
||||||
local ignore = {}
|
function playerctl:disable()
|
||||||
local priority = {}
|
-- Restore default settings
|
||||||
local update_on_activity = true
|
self.ignore = {}
|
||||||
local interval = 1
|
self.priority = {}
|
||||||
|
self.update_on_activity = true
|
||||||
|
self.interval = 1
|
||||||
|
self.debounce_delay = 0.35
|
||||||
|
|
||||||
-- Track position callback
|
-- Reset timers
|
||||||
local last_position = -1
|
self._private.manager = nil
|
||||||
local last_length = -1
|
self._private.metadata_timer:stop()
|
||||||
local function position_cb()
|
self._private.metadata_timer = nil
|
||||||
local player = manager.players[1]
|
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
|
if player then
|
||||||
local position = player:get_position() / 1000000
|
player:pause()
|
||||||
local length = (player.metadata.value["mpris:length"] or 0) / 1000000
|
end
|
||||||
if position ~= last_position or length ~= last_length then
|
end
|
||||||
awesome.emit_signal(
|
|
||||||
"bling::playerctl::position",
|
function playerctl:play(player)
|
||||||
position,
|
player = player or self._private.manager.players[1]
|
||||||
length,
|
if player then
|
||||||
player.player_name
|
player:play()
|
||||||
)
|
end
|
||||||
last_position = position
|
end
|
||||||
last_length = length
|
|
||||||
|
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
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local function get_album_art(url)
|
function playerctl:set_position(position, player)
|
||||||
return awful.util.shell
|
player = player or self._private.manager.players[1]
|
||||||
.. [[ -c '
|
if player then
|
||||||
|
player:set_position(position * 1000000)
|
||||||
tmp_dir="$XDG_CACHE_HOME/awesome/"
|
end
|
||||||
|
|
||||||
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
|
end
|
||||||
|
|
||||||
-- Metadata callback for title, artist, and album art
|
function playerctl:set_shuffle(shuffle, player)
|
||||||
local last_player = nil
|
player = player or self._private.manager.players[1]
|
||||||
local last_title = ""
|
if player then
|
||||||
local last_artist = ""
|
player:set_shuffle(shuffle)
|
||||||
local last_artUrl = ""
|
end
|
||||||
local function metadata_cb(player, metadata)
|
end
|
||||||
if update_on_activity then
|
|
||||||
manager:move_player_to_top(player)
|
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
|
end
|
||||||
|
|
||||||
local data = metadata.value
|
local data = metadata.value
|
||||||
|
@ -93,101 +214,136 @@ local function metadata_cb(player, metadata)
|
||||||
artist = artist .. ", " .. data["xesam:artist"][i]
|
artist = artist .. ", " .. data["xesam:artist"][i]
|
||||||
end
|
end
|
||||||
local artUrl = data["mpris:artUrl"] or ""
|
local artUrl = data["mpris:artUrl"] or ""
|
||||||
-- Spotify client doesn't report its art URL's correctly...
|
local album = data["xesam:album"] or ""
|
||||||
if player.player_name == "spotify" then
|
|
||||||
artUrl = artUrl:gsub("open.spotify.com", "i.scdn.co")
|
if player == self._private.manager.players[1] then
|
||||||
end
|
self._private.active_player = player
|
||||||
|
|
||||||
if player == manager.players[1] then
|
|
||||||
-- Callback can be called even though values we care about haven't
|
-- Callback can be called even though values we care about haven't
|
||||||
-- changed, so check to see if they have
|
-- changed, so check to see if they have
|
||||||
if
|
if
|
||||||
player ~= last_player
|
player ~= self._private.last_player
|
||||||
or title ~= last_title
|
or title ~= self._private.last_title
|
||||||
or artist ~= last_artist
|
or artist ~= self._private.last_artist
|
||||||
or artUrl ~= last_artUrl
|
or artUrl ~= self._private.last_artUrl
|
||||||
then
|
then
|
||||||
if title == "" and artist == "" and artUrl == "" then
|
if (title == "" and artist == "" and artUrl == "") then return end
|
||||||
return
|
|
||||||
|
if self._private.metadata_timer ~= nil and self._private.metadata_timer.started then
|
||||||
|
self._private.metadata_timer:stop()
|
||||||
end
|
end
|
||||||
|
|
||||||
if metadata_timer ~= nil then
|
self._private.metadata_timer = gtimer {
|
||||||
if metadata_timer.started then
|
timeout = self.debounce_delay,
|
||||||
metadata_timer:stop()
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
metadata_timer = gears.timer({
|
|
||||||
timeout = 0.3,
|
|
||||||
autostart = true,
|
autostart = true,
|
||||||
single_shot = true,
|
single_shot = true,
|
||||||
callback = function()
|
callback = function()
|
||||||
if artUrl ~= "" then
|
emit_metadata_signal(self, title, artist, artUrl, album, true, player.player_name)
|
||||||
awful.spawn.with_line_callback(get_album_art(artUrl), {
|
end
|
||||||
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,
|
|
||||||
})
|
|
||||||
|
|
||||||
-- Re-sync with position timer when track changes
|
-- Re-sync with position timer when track changes
|
||||||
position_timer:again()
|
self._private.position_timer:again()
|
||||||
last_player = player
|
self._private.last_player = player
|
||||||
last_title = title
|
self._private.last_title = title
|
||||||
last_artist = artist
|
self._private.last_artist = artist
|
||||||
last_artUrl = artUrl
|
self._private.last_artUrl = artUrl
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Playback status callback
|
local function position_cb(self)
|
||||||
-- Reported as PLAYING, PAUSED, or STOPPED
|
local player = self._private.manager.players[1]
|
||||||
local function playback_status_cb(player, status)
|
if player then
|
||||||
if update_on_activity then
|
|
||||||
manager:move_player_to_top(player)
|
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
|
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
|
if status == "PLAYING" then
|
||||||
awesome.emit_signal(
|
self:emit_signal("playback_status", true, player.player_name)
|
||||||
"bling::playerctl::status",
|
capi.awesome.emit_signal("bling::playerctl::status", true, player.player_name)
|
||||||
true,
|
|
||||||
player.player_name
|
|
||||||
)
|
|
||||||
else
|
else
|
||||||
awesome.emit_signal(
|
self:emit_signal("playback_status", false, player.player_name)
|
||||||
"bling::playerctl::status",
|
capi.awesome.emit_signal("bling::playerctl::status", false, player.player_name)
|
||||||
false,
|
|
||||||
player.player_name
|
|
||||||
)
|
|
||||||
end
|
end
|
||||||
end
|
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
|
-- Determine if player should be managed
|
||||||
local function name_is_selected(name)
|
local function name_is_selected(self, name)
|
||||||
if ignore[name.name] then
|
if self.ignore[name.name] then
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
if #priority > 0 then
|
if #self.priority > 0 then
|
||||||
for _, arg in pairs(priority) do
|
for _, arg in pairs(self.priority) do
|
||||||
if arg == name.name or arg == "%any" then
|
if arg == name.name or arg == "%any" then
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
|
@ -199,23 +355,42 @@ local function name_is_selected(name)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Create new player and connect it to callbacks
|
-- Create new player and connect it to callbacks
|
||||||
local function init_player(name)
|
local function init_player(self, name)
|
||||||
if name_is_selected(name) then
|
if name_is_selected(self, name) then
|
||||||
local player = Playerctl.Player.new_from_name(name)
|
local player = self._private.lgi_Playerctl.Player.new_from_name(name)
|
||||||
manager:manage_player(player)
|
self._private.manager:manage_player(player)
|
||||||
player.on_playback_status = playback_status_cb
|
player.on_metadata = function(player, metadata)
|
||||||
player.on_metadata = metadata_cb
|
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
|
-- Start position timer if its not already running
|
||||||
if not position_timer.started then
|
if not self._private.position_timer.started then
|
||||||
position_timer:again()
|
self._private.position_timer:again()
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Determine if a player name comes before or after another according to the
|
-- Determine if a player name comes before or after another according to the
|
||||||
-- priority order
|
-- 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 any_index = math.huge
|
||||||
local a_match_index = nil
|
local a_match_index = nil
|
||||||
local b_match_index = nil
|
local b_match_index = nil
|
||||||
|
@ -224,7 +399,7 @@ local function player_compare_name(name_a, name_b)
|
||||||
return 0
|
return 0
|
||||||
end
|
end
|
||||||
|
|
||||||
for index, name in ipairs(priority) do
|
for index, name in ipairs(self.priority) do
|
||||||
if name == "%any" then
|
if name == "%any" then
|
||||||
any_index = (any_index == math.huge) and index or any_index
|
any_index = (any_index == math.huge) and index or any_index
|
||||||
elseif name == name_a then
|
elseif name == name_a then
|
||||||
|
@ -248,103 +423,138 @@ local function player_compare_name(name_a, name_b)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Sorting function used by manager if a priority order is specified
|
-- Sorting function used by manager if a priority order is specified
|
||||||
local function player_compare(a, b)
|
local function player_compare(self, a, b)
|
||||||
local player_a = Playerctl.Player(a)
|
local player_a = self._private.lgi_Playerctl.Player(a)
|
||||||
local player_b = Playerctl.Player(b)
|
local player_b = self._private.lgi_Playerctl.Player(b)
|
||||||
return player_compare_name(player_a.player_name, player_b.player_name)
|
return player_compare_name(self, player_a.player_name, player_b.player_name)
|
||||||
end
|
end
|
||||||
|
|
||||||
local function start_manager()
|
local function get_current_player_info(self, player)
|
||||||
manager = Playerctl.PlayerManager()
|
local title = player:get_title() or ""
|
||||||
if #priority > 0 then
|
local artist = player:get_artist() or ""
|
||||||
manager:set_sort_func(player_compare)
|
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
|
end
|
||||||
|
|
||||||
-- Timer to update track position at specified interval
|
-- Timer to update track position at specified interval
|
||||||
position_timer = gears.timer({
|
self._private.position_timer = gtimer {
|
||||||
timeout = interval,
|
timeout = self.interval,
|
||||||
callback = position_cb,
|
callback = function()
|
||||||
})
|
position_cb(self)
|
||||||
|
end,
|
||||||
|
}
|
||||||
|
|
||||||
-- Manage existing players on startup
|
-- Manage existing players on startup
|
||||||
for _, name in ipairs(manager.player_names) do
|
for _, name in ipairs(self._private.manager.player_names) do
|
||||||
init_player(name)
|
init_player(self, name)
|
||||||
end
|
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
|
-- Callback to manage new players
|
||||||
function manager:on_name_appeared(name)
|
function self._private.manager:on_name_appeared(name)
|
||||||
init_player(name)
|
init_player(_self, name)
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Callback to check if all players have exited
|
function self._private.manager:on_player_appeared(player)
|
||||||
function manager:on_name_vanished(name)
|
if player == self.players[1] then
|
||||||
if #manager.players == 0 then
|
_self._private.active_player = player
|
||||||
metadata_timer:stop()
|
end
|
||||||
position_timer:stop()
|
end
|
||||||
awesome.emit_signal("bling::playerctl::no_players")
|
|
||||||
|
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
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
-- Parse arguments
|
local function parse_args(self, args)
|
||||||
local function parse_args(args)
|
self.ignore = {}
|
||||||
if args then
|
if type(args.ignore) == "string" then
|
||||||
update_on_activity = args.update_on_activity or update_on_activity
|
self.ignore[args.ignore] = true
|
||||||
interval = args.interval or interval
|
elseif type(args.ignore) == "table" then
|
||||||
|
for _, name in pairs(args.ignore) do
|
||||||
if type(args.ignore) == "string" then
|
self.ignore[name] = true
|
||||||
ignore[args.ignore] = true
|
|
||||||
elseif type(args.ignore) == "table" then
|
|
||||||
for _, name in pairs(args.ignore) do
|
|
||||||
ignore[name] = true
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
end
|
||||||
|
|
||||||
if type(args.player) == "string" then
|
self.priority = {}
|
||||||
priority[1] = args.player
|
if type(args.player) == "string" then
|
||||||
elseif type(args.player) == "table" then
|
self.priority[1] = args.player
|
||||||
priority = args.player
|
elseif type(args.player) == "table" then
|
||||||
end
|
self.priority = args.player
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local function playerctl_enable(args)
|
local function new(args)
|
||||||
args = args or {}
|
args = args or {}
|
||||||
|
|
||||||
|
local ret = gobject{}
|
||||||
|
gtable.crush(ret, playerctl, true)
|
||||||
|
|
||||||
-- Grab settings from beautiful variables if not set explicitly
|
-- Grab settings from beautiful variables if not set explicitly
|
||||||
args.ignore = args.ignore or beautiful.playerctl_ignore
|
args.ignore = args.ignore or beautiful.playerctl_ignore
|
||||||
args.player = args.player or beautiful.playerctl_player
|
args.player = args.player or beautiful.playerctl_player
|
||||||
args.update_on_activity = args.update_on_activity
|
ret.update_on_activity = args.update_on_activity or
|
||||||
or beautiful.playerctl_update_on_activity
|
beautiful.playerctl_update_on_activity or true
|
||||||
args.interval = args.interval
|
ret.interval = args.interval or beautiful.playerctl_position_update_interval or 1
|
||||||
or beautiful.playerctl_position_update_interval
|
ret.debounce_delay = args.debounce_delay or beautiful.playerctl_debounce_delay or 0.35
|
||||||
parse_args(args)
|
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
|
-- 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
|
-- 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
|
end
|
||||||
|
|
||||||
local function playerctl_disable()
|
function playerctl.mt:__call(...)
|
||||||
-- Remove manager and timer
|
return new(...)
|
||||||
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 = ""
|
|
||||||
end
|
end
|
||||||
|
|
||||||
return { enable = playerctl_enable, disable = playerctl_disable }
|
return setmetatable(playerctl, playerctl.mt)
|
||||||
|
|
Loading…
Reference in New Issue