Use playerctld to get player names when available

fixes #192
This commit is contained in:
Tony Crisci
2020-11-07 16:18:06 -05:00
parent a1cfd4a790
commit bfed117652
13 changed files with 245 additions and 91 deletions

View File

@@ -20,11 +20,11 @@ RUN pip3 install -r requirements.txt
ADD . /app
COPY data/test/dbus-system.conf /etc/dbus-1/system.d/test-dbus-system.conf
COPY test/data/dbus-system.conf /etc/dbus-1/system.d/test-dbus-system.conf
RUN meson --prefix=/usr build && \
ninja -C build && ninja -C build install
RUN mkdir -p /run/dbus
ENV PYTHONASYNCIODEBUG=1
ENV DBUS_SYSTEM_BUS_ADDRESS=unix:path=/var/run/dbus/system_bus_socket
CMD ["bash", "-c", "dbus-daemon --nopidfile --system && dbus-run-session python3 -m pytest -vv"]
CMD ["bash", "-c", "dbus-daemon --nopidfile --system && dbus-run-session python3 -m pytest -vvs"]

View File

@@ -38,6 +38,8 @@ Here is a list of available commands:
Without specifying any players to control, Playerctl will act on the first player it can find.
Playerctl comes with a service called `playerctld` that monitors the activity of media players in the background. If `playerctld` is running, Playerctl will act on players in order of their last activity.
You can list the names of players that are available to control that are running on the system with `playerctl --list-all`.
If you'd only like to control certain players, you can pass the names of those players separated by commas with the `--player` flag. Playerctl will select the first instance of a player in that list that supports the command. To control all players in the list, you can use the `--all-players` flag.
@@ -69,15 +71,6 @@ playerctl --player=%any,chromium play
playerctl --player=vlc,%any play
```
#### Selecting the Most Recent Player
Playerctl comes with a service called `playerctld` you can use that monitors the activity of media players to select the one with the most recent activity. To use it, simply pass `playerctld` as the selected player to Playerctl and the service should start automatically (if it doesn't, see the troubleshooting section).
```
# Command the most recent player to play
playerctl --player=playerctld play
```
### Printing Properties and Metadata
You can pass a format string with the `--format` argument to print properties in a specific format. Pass the variable you want to print in the format string between double braces like `{{ VARIABLE }}`. The variables available are either the name of the query command, or anything in the metadata map which can be viewed with `playerctl metadata`. You can use this to integrate playerctl into a statusline generator.

View File

@@ -723,7 +723,7 @@ static gboolean playercmd_metadata(PlayerctlPlayer *player, gchar **argv, gint a
return TRUE;
}
static void managed_player_properties_callback(PlayerctlPlayer *player, gpointer *data) {
static void managed_player_properties_callback(PlayerctlPlayer *player, gpointer data) {
playerctl_player_manager_move_player_to_top(manager, player);
GError *error = NULL;
managed_players_execute_command(&error);
@@ -883,6 +883,28 @@ static GList *parse_player_list(gchar *player_list_arg) {
return players;
}
static gboolean name_is_selected(const gchar *name) {
if (ignored_player_names != NULL) {
gboolean ignored =
(g_list_find_custom(ignored_player_names, name,
(GCompareFunc)pctl_player_name_string_instance_compare) != NULL);
if (ignored) {
return FALSE;
}
}
if (player_names != NULL) {
gboolean selected =
(g_list_find_custom(player_names, name,
(GCompareFunc)pctl_player_name_string_instance_compare) != NULL);
if (!selected) {
return FALSE;
}
}
return TRUE;
}
static int handle_version_flag() {
g_print("v%s\n", PLAYERCTL_VERSION_S);
return 0;
@@ -892,22 +914,26 @@ static int handle_list_all_flag() {
GError *tmp_error = NULL;
GList *player_names_list = playerctl_list_players(&tmp_error);
player_names_list =
g_list_sort_with_data(player_names_list, player_name_compare_func, (gpointer)player_names);
if (tmp_error != NULL) {
g_printerr("%s\n", tmp_error->message);
return 1;
}
if (player_names_list == NULL) {
if (!no_status_error_messages) {
g_printerr("No players were found\n");
}
return 0;
}
gboolean one_selected = FALSE;
GList *l = NULL;
for (l = player_names_list; l != NULL; l = l->next) {
PlayerctlPlayerName *name = l->data;
printf("%s\n", name->instance);
if (name_is_selected(name->instance)) {
one_selected = TRUE;
printf("%s\n", name->instance);
}
}
if (!one_selected && !no_status_error_messages) {
g_printerr("No players were found\n");
}
pctl_player_name_list_destroy(player_names_list);
@@ -958,28 +984,6 @@ static void managed_players_execute_command(GError **error) {
}
}
static gboolean name_is_selected(gchar *name) {
if (ignored_player_names != NULL) {
gboolean ignored =
(g_list_find_custom(ignored_player_names, name,
(GCompareFunc)pctl_player_name_string_instance_compare) != NULL);
if (ignored) {
return FALSE;
}
}
if (player_names != NULL) {
gboolean selected =
(g_list_find_custom(player_names, name,
(GCompareFunc)pctl_player_name_string_instance_compare) != NULL);
if (!selected) {
return FALSE;
}
}
return TRUE;
}
static void name_appeared_callback(PlayerctlPlayerManager *manager, PlayerctlPlayerName *name,
gpointer *data) {
if (!name_is_selected(name->instance)) {
@@ -1074,75 +1078,76 @@ static void player_vanished_callback(PlayerctlPlayerManager *manager, PlayerctlP
}
}
gint player_name_string_compare_func(gconstpointer a, gconstpointer b) {
gint player_name_string_compare_func(gconstpointer a, gconstpointer b, gpointer user_data) {
const gchar *name_a = a;
const gchar *name_b = b;
GList *names = user_data;
if (g_strcmp0(name_a, name_b) == 0) {
return 0;
}
int a_index = -1;
int b_index = -1;
int a_match_index = -1;
int b_match_index = -1;
int any_index = INT_MAX;
int i = 0;
GList *l = NULL;
for (l = player_names; l != NULL; l = l->next) {
for (l = names; l != NULL; l = l->next) {
gchar *name = l->data;
if (g_strcmp0(name, "%any") == 0) {
if (any_index == INT_MAX) {
any_index = i;
}
} else if (g_strcmp0(name_a, name) == 0) {
if (a_index == -1) {
a_index = i;
continue;
}
if (pctl_player_name_string_instance_compare(name, name_a) == 0) {
if (a_match_index == -1) {
a_match_index = i;
}
} else if (g_strcmp0(name_b, name) == 0) {
if (b_index == -1) {
b_index = i;
}
} else if (pctl_player_name_string_instance_compare(name, name_a) == 0) {
if (a_index == -1) {
a_index = i;
}
} else if (pctl_player_name_string_instance_compare(name, name_b) == 0) {
if (b_index == -1) {
b_index = i;
}
if (pctl_player_name_string_instance_compare(name, name_b) == 0) {
if (b_match_index == -1) {
b_match_index = i;
}
}
++i;
}
if (a_index == -1 && b_index == -1) {
if (a_match_index == -1 && b_match_index == -1) {
// neither are in the list
return 0;
} else if (a_index == -1) {
} else if (a_match_index == -1) {
// b is in the list
return (b_index < any_index ? 1 : -1);
} else if (b_index == -1) {
return (b_match_index < any_index ? 1 : -1);
} else if (b_match_index == -1) {
// a is in the list
return (a_index < any_index ? -1 : 1);
return (a_match_index < any_index ? -1 : 1);
} else if (a_match_index == b_match_index) {
// preserve order
return 0;
} else {
// both are in the list
return (a_index < b_index ? -1 : 1);
return (a_match_index < b_match_index ? -1 : 1);
}
}
gint player_name_compare_func(gconstpointer a, gconstpointer b) {
gint player_name_compare_func(gconstpointer a, gconstpointer b, gpointer user_data) {
const PlayerctlPlayerName *name_a = a;
const PlayerctlPlayerName *name_b = b;
return player_name_string_compare_func(name_a->instance, name_b->instance);
return player_name_string_compare_func(name_a->instance, name_b->instance, user_data);
}
gint player_compare_func(gconstpointer a, gconstpointer b) {
gint player_compare_func(gconstpointer a, gconstpointer b, gpointer user_data) {
PlayerctlPlayer *player_a = PLAYERCTL_PLAYER(a);
PlayerctlPlayer *player_b = PLAYERCTL_PLAYER(b);
gchar *name_a = NULL;
gchar *name_b = NULL;
g_object_get(player_a, "player-name", &name_a, NULL);
g_object_get(player_b, "player-name", &name_b, NULL);
gint result = player_name_string_compare_func(name_a, name_b);
gint result = player_name_string_compare_func(name_a, name_b, user_data);
g_free(name_a);
g_free(name_b);
return result;
@@ -1163,6 +1168,9 @@ int main(int argc, char *argv[]) {
exit(0);
}
player_names = parse_player_list(player_arg);
ignored_player_names = parse_player_list(ignore_player_arg);
if (print_version_and_exit) {
int result = handle_version_flag();
exit(result);
@@ -1191,8 +1199,6 @@ int main(int argc, char *argv[]) {
}
}
player_names = parse_player_list(player_arg);
ignored_player_names = parse_player_list(ignore_player_arg);
playercmd_args = playercmd_args_create(command_arg, num_commands);
manager = playerctl_player_manager_new(&error);
@@ -1203,13 +1209,14 @@ int main(int argc, char *argv[]) {
}
if (player_names != NULL && !select_all_players) {
playerctl_player_manager_set_sort_func(manager, (GCompareDataFunc)player_compare_func, NULL,
playerctl_player_manager_set_sort_func(manager, player_compare_func, (gpointer)player_names,
NULL);
}
g_object_get(manager, "player-names", &available_players, NULL);
available_players = g_list_copy(available_players);
available_players = g_list_sort(available_players, (GCompareFunc)player_name_compare_func);
available_players =
g_list_sort_with_data(available_players, player_name_compare_func, (gpointer)player_names);
PlayerctlPlayerName playerctld_name = {
.instance = "playerctld",
@@ -1222,10 +1229,11 @@ int main(int argc, char *argv[]) {
// playerctld is not ignored, was specified exactly in the list of
// players, and is not in the list of available players. Add it to the
// list and try to autostart it.
g_debug("%s", "playerctld was selected and is not available, attempting to autostart it");
g_debug("%s", "playerctld was selected explicitly, it may autostart");
available_players = g_list_append(
available_players, pctl_player_name_new("playerctld", PLAYERCTL_SOURCE_DBUS_SESSION));
available_players = g_list_sort(available_players, (GCompareFunc)player_name_compare_func);
available_players = g_list_sort_with_data(available_players, player_name_compare_func,
(gpointer)player_names);
}
gboolean has_selected = FALSE;

View File

@@ -21,8 +21,11 @@
#include <glib.h>
#include <stdio.h>
#include <stdlib.h>
#include <strings.h>
#define PLAYERCTLD_BUS_NAME "org.mpris.MediaPlayer2.playerctld"
gboolean pctl_parse_playback_status(const gchar *status_str, PlayerctlPlaybackStatus *status) {
if (status_str == NULL) {
return FALSE;
@@ -162,8 +165,9 @@ gint pctl_player_name_string_instance_compare(const gchar *name, const gchar *in
}
gboolean exact_match = (g_strcmp0(name, instance) == 0);
gboolean instance_match = !exact_match && (g_str_has_prefix(instance, name) &&
g_str_has_prefix(instance + strlen(name), "."));
gboolean instance_match =
!exact_match && (g_str_has_prefix(instance, name) && strlen(instance) > strlen(name) &&
g_str_has_prefix(instance + strlen(name), "."));
if (exact_match || instance_match) {
return 0;
@@ -191,12 +195,16 @@ GList *pctl_player_name_find_instance(GList *list, gchar *player_id, PlayerctlSo
}
void pctl_player_name_list_destroy(GList *list) {
if (list == NULL) {
return;
}
g_list_free_full(list, (GDestroyNotify)playerctl_player_name_free);
}
GList *pctl_list_player_names_on_bus(GBusType bus_type, GError **err) {
GError *tmp_error = NULL;
GList *players = NULL;
gboolean has_playerctld = FALSE;
GDBusProxy *proxy = g_dbus_proxy_new_for_bus_sync(
bus_type, G_DBUS_PROXY_FLAGS_NONE, NULL, "org.freedesktop.DBus", "/org/freedesktop/DBus",
@@ -237,6 +245,46 @@ GList *pctl_list_player_names_on_bus(GBusType bus_type, GError **err) {
gsize reply_count;
const gchar **names = g_variant_get_strv(reply_child, &reply_count);
// If playerctld is in the names, get the list of players from there
// because it will be in order of activity
for (gsize i = 0; i < reply_count; i += 1) {
if (g_strcmp0(names[i], PLAYERCTLD_BUS_NAME) == 0) {
g_debug("%s", "Playerctld is running. Getting names from there.");
has_playerctld = TRUE;
GDBusProxy *playerctld_proxy = g_dbus_proxy_new_for_bus_sync(
bus_type, G_DBUS_PROXY_FLAGS_NONE, NULL, PLAYERCTLD_BUS_NAME,
"/org/mpris/MediaPlayer2", "com.github.altdesktop.playerctld", NULL, &tmp_error);
if (tmp_error != NULL) {
g_warning("Could not get player names from playerctld: %s", tmp_error->message);
g_clear_error(&tmp_error);
g_object_unref(playerctld_proxy);
break;
}
GVariant *playerctld_reply =
g_dbus_proxy_get_cached_property(playerctld_proxy, "PlayerNames");
if (playerctld_reply == NULL) {
g_warning(
"%s",
"Could not get player names from playerctld: PlayerNames property not found");
g_clear_error(&tmp_error);
g_object_unref(playerctld_proxy);
break;
}
g_variant_unref(reply);
g_free(names);
reply = playerctld_reply;
names = g_variant_get_strv(reply, &reply_count);
g_object_unref(playerctld_proxy);
has_playerctld = TRUE;
break;
}
}
size_t offset = strlen(MPRIS_PREFIX);
for (gsize i = 0; i < reply_count; i += 1) {
if (g_str_has_prefix(names[i], MPRIS_PREFIX)) {
@@ -246,6 +294,10 @@ GList *pctl_list_player_names_on_bus(GBusType bus_type, GError **err) {
}
}
if (!has_playerctld) {
players = g_list_sort(players, (GCompareFunc)pctl_player_name_compare);
}
g_object_unref(proxy);
g_variant_unref(reply);
g_variant_unref(reply_child);

View File

@@ -56,7 +56,7 @@ struct _PlayerctlPlayerManagerPrivate {
GList *player_names;
GList *players;
GCompareDataFunc sort_func;
gpointer *sort_data;
gpointer sort_data;
GDestroyNotify sort_notify;
};
@@ -412,7 +412,7 @@ PlayerctlPlayerManager *playerctl_player_manager_new(GError **err) {
* for using this list as a priority queue.
*/
void playerctl_player_manager_set_sort_func(PlayerctlPlayerManager *manager,
GCompareDataFunc sort_func, gpointer *sort_data,
GCompareDataFunc sort_func, gpointer sort_data,
GDestroyNotify notify) {
// TODO figure out how to make this work with the bindings
manager->priv->sort_func = sort_func;

View File

@@ -93,7 +93,7 @@ void playerctl_player_manager_manage_player(PlayerctlPlayerManager *manager,
PlayerctlPlayer *player);
void playerctl_player_manager_set_sort_func(PlayerctlPlayerManager *manager,
GCompareDataFunc sort_func, gpointer *sort_data,
GCompareDataFunc sort_func, gpointer sort_data,
GDestroyNotify notify);
void playerctl_player_manager_move_player_to_top(PlayerctlPlayerManager *manager,

View File

@@ -24,4 +24,10 @@
char *pctl_player_get_instance(PlayerctlPlayer *player);
gint player_name_string_compare_func(gconstpointer a, gconstpointer b, gpointer user_data);
gint player_name_compare_func(gconstpointer a, gconstpointer b, gpointer user_data);
gint player_compare_func(gconstpointer a, gconstpointer b, gpointer user_data);
#endif /* __PLAYERCTL_PLAYER_PRIVATE_H__ */

View File

@@ -6,6 +6,7 @@ import asyncio
async def setup_mpris(*names, bus_address=None, system=False):
# TODO maybe they should all share a bus for speed
async def setup(name):
if system:
bus_type = BusType.SYSTEM
@@ -23,6 +24,15 @@ async def setup_mpris(*names, bus_address=None, system=False):
return await asyncio.gather(*(setup(name) for name in names))
async def setup_playerctld(bus_address=None):
bus = await MessageBus(bus_address=bus_address).connect()
playerctld = PlayerctldInterface(bus)
bus.export('/org/mpris/MediaPlayer2', playerctld)
reply = await bus.request_name('org.mpris.MediaPlayer2.playerctld')
assert reply == RequestNameReply.PRIMARY_OWNER
return playerctld
class MprisRoot(ServiceInterface):
def __init__(self):
super().__init__('org.mpris.MediaPlayer2')
@@ -240,3 +250,18 @@ class MprisPlayer(ServiceInterface):
@dbus_property(access=PropertyAccess.READ)
def CanControl(self) -> 'b':
return self.can_control
class PlayerctldInterface(ServiceInterface):
'''just enough of playerctld for testing'''
def __init__(self, bus):
super().__init__('com.github.altdesktop.playerctld')
self.bus = bus
self.player_names = []
@dbus_property(access=PropertyAccess.READ)
def PlayerNames(self) -> 'as':
return self.player_names
def disconnect(self):
self.bus.disconnect()

View File

@@ -1,5 +1,6 @@
import asyncio
import os
from shlex import join
class CommandResult:
@@ -33,7 +34,7 @@ class PlayerctlProcess:
break
asyncio.get_event_loop().create_task(reader(proc.stdout))
asyncio.get_event_loop().create_task(printer(proc.stderr))
# asyncio.get_event_loop().create_task(printer(proc.stderr))
def running(self):
return self.proc.returncode is None
@@ -69,3 +70,16 @@ class PlayerctlCli:
stdout, stderr = await proc.communicate()
await proc.wait()
return CommandResult(stdout, stderr, proc.returncode)
async def list(self, players=[], ignored=[]):
args = ['--list-all']
if players:
args.extend(['--player', ','.join(players)])
if ignored:
args.extend(['--ignored-players', ','.join(ignored)])
cmd = await self.run(join(args))
assert cmd.returncode == 0, cmd.stderr
return cmd.stdout.splitlines()

View File

@@ -62,7 +62,7 @@ async def test_list_names(bus_address):
async def test_system_list_players(bus_address):
system_players = await setup_mpris('system', system=True)
session_players = await setup_mpris('session1', bus_address=bus_address)
playerctl = PlayerctlCli(bus_address, debug=False)
playerctl = PlayerctlCli(bus_address)
result = await playerctl.run('-l')
assert result.returncode == 0, result.stdout
assert result.stdout.split() == ['session1', 'system']

View File

@@ -21,7 +21,7 @@ async def test_commands(bus_address):
def get_called(cmd):
return getattr(mpris, f'{cmd.replace("-", "_")}_called')
playerctl = PlayerctlCli(bus_address, debug=True)
playerctl = PlayerctlCli(bus_address)
results = await asyncio.gather(*(playerctl.run(f'-p commands {cmd}')
for cmd in commands + setters))

View File

@@ -1,4 +1,4 @@
from .mpris import setup_mpris
from .mpris import setup_mpris, setup_playerctld
from .playerctl import PlayerctlCli
import pytest
@@ -56,11 +56,10 @@ async def test_selection(bus_address):
(s1i, s1): (s1i, s1),
(m6, s1): (s6i, s1, s1i),
(m4, m6, s3): (s6i, s3),
(any_player, ):
(s2, s3, s1i, s6i, s1), # order undefined, but consistent
(any_player, ): (s1, s1i, s2, s3, s6i),
(s1, any_player): (s1, s1i, s2, s3, s6i), # s1 first
(any_player, s1): (s2, s3, s6i, s1i, s1), # s1 last
(m6, any_player, s2): (s6i, s3, s1i, s1, s2), # s6 first, s2 last
(any_player, s1): (s2, s3, s6i, s1, s1i), # s1 last
(m6, any_player, s2): (s6i, s1, s1i, s3, s2), # s6 first, s2 last
(m6, s1, any_player, s2): (s6i, s1, s1i, s3, s2),
}
@@ -68,9 +67,66 @@ async def test_selection(bus_address):
for selection, expected in selections.items():
result = await select(*selection)
assert result == expected[0]
assert result == expected[0], (selection, expected, result)
result = await select_many(*selection)
assert result == expected
for mpris in mpris_players:
mpris.disconnect()
@pytest.mark.asyncio
async def test_daemon_selection(bus_address):
playerctld = await setup_playerctld(bus_address=bus_address)
playerctl = PlayerctlCli(bus_address)
def iface_name(player_name):
return f'org.mpris.MediaPlayer2.{player_name}'
def set_players(players):
playerctld.player_names = [iface_name(p) for p in players]
s1 = 'selection1'
s1i = 'selection1.i_123'
s2 = 'selection2'
s2i = 'selection2.i_123'
s3 = 'selection3'
m4 = 'selection4'
m5 = 'selection5'
m6 = 'selection6'
s6i = 'selection6.i_2'
any_player = '%any'
# selection, players, expected result
all_players = [s1, s1i, s2, s3, s6i]
tests = [
(None, all_players, all_players),
(all_players, all_players, all_players),
([s2], [s1, s2], [s2]),
([s1], [s2, s1i, s1], [s1i, s1]),
([s1], [s2, s1, s1i], [s1, s1i]),
([s1i, s1], [s1, s1i], [s1i, s1]),
([any_player], all_players, all_players),
([any_player, s1], [s1, s1i, s2i, s2], [s2i, s2, s1, s1i]),
([any_player, s1], [s1, s1i, s2, s2i], [s2, s2i, s1, s1i]),
([any_player, s1], [s1i, s1, s2i, s2], [s2i, s2, s1i, s1]),
([any_player, s1], [s1i, s1, s2, s2i], [s2, s2i, s1i, s1]),
([s2, any_player], [s1, s1i, s2i, s2], [s2i, s2, s1, s1i]),
([s2, any_player], [s1, s1i, s2, s2i], [s2, s2i, s1, s1i]),
([s2, any_player], [s1i, s1, s2i, s2], [s2i, s2, s1i, s1]),
([s2, any_player], [s1i, s1, s2, s2i], [s2, s2i, s1i, s1]),
([s2i, any_player], [s1i, s1, s2, s2i], [s2i, s1i, s1, s2]),
]
async def daemon_selection_test(test):
selection, players, expected = test
set_players(players)
result = await playerctl.list(players=selection)
assert result == expected, test
for test in tests:
# unfortunately it won't work in parallel because there's only one
# playerctld
await daemon_selection_test(test)
playerctld.disconnect()