pax_global_header00006660000000000000000000000064141223473120014511gustar00rootroot0000000000000052 comment=e5304e9dc9a0c0c32b3689c3f141cf266d27f59c playerctl-2.4.1/000077500000000000000000000000001412234731200135145ustar00rootroot00000000000000playerctl-2.4.1/.clang-format000066400000000000000000000005201412234731200160640ustar00rootroot00000000000000BasedOnStyle: google AllowShortIfStatementsOnASingleLine: false AllowShortLoopsOnASingleLine: false AllowShortFunctionsOnASingleLine: None AllowShortBlocksOnASingleLine: false AlwaysBreakBeforeMultilineStrings: false IndentWidth: 4 PointerBindsToType: false ColumnLimit: 100 SpaceBeforeParens: ControlStatements IndentCaseLabels: false playerctl-2.4.1/.dockerignore000066400000000000000000000000471412234731200161710ustar00rootroot00000000000000/build **/__pycache__ **/.pytest_cache playerctl-2.4.1/.flake8000066400000000000000000000003571412234731200146740ustar00rootroot00000000000000[flake8] ignore= E501 E126 E402 F722 # F821 is still relevant, but causes too many false positives in tests and # examples per-file-ignores= test/*:F821 test/util.py:F401 examples/*:F821 */__init__.py:F401 playerctl-2.4.1/.github/000077500000000000000000000000001412234731200150545ustar00rootroot00000000000000playerctl-2.4.1/.github/ISSUE_TEMPLATE/000077500000000000000000000000001412234731200172375ustar00rootroot00000000000000playerctl-2.4.1/.github/ISSUE_TEMPLATE/bug_report.md000066400000000000000000000002211412234731200217240ustar00rootroot00000000000000--- name: Bug report about: Create a report to help us improve title: '' labels: bug assignees: acrisci --- The player I am using is [PLAYER]. playerctl-2.4.1/.gitignore000066400000000000000000000001251412234731200155020ustar00rootroot00000000000000tags .clang_complete /build /mesonbuild /playerctl-fpm *.swp *.swo *.swn *.snap /env playerctl-2.4.1/.travis.yml000066400000000000000000000002431412234731200156240ustar00rootroot00000000000000language: minimal sudo: required dist: xenial services: - docker before_install: - docker build -t playerctl-test . script: - docker run -it playerctl-test playerctl-2.4.1/CHANGELOG.md000066400000000000000000000227701412234731200153350ustar00rootroot00000000000000# Changelog ## Version 2.4.1 Version 2.4.1 contains bugfixes and new features. * Fix a crash in playerctld when players use TrackList and Playlists interfaces (#215) * Add the `trunc()` template function (#224) * Allow to use playerctl as a subproject and cpp linking (#228) * bugfix: subscribe to all signals when multiple template functions are used (#235) * bugfix: workaround for players that use uint64 values in the formatter (#234) ## Version 2.3.1 Version 2.3.1 contains bugfixes and new features. * Add option to toggle shuffle (#197) * Add `-s`, quiet flag to supress some messages from stderr (#108, #193) * Add math operations to the formatter (#149, 119d0a5) * Change instance delimiter to "." (#198, d0a59e2) * Use playerctld to get players in order of activation when it is running (#192, bfed117) * Remove warning message when the system bus isn't found (a1cfd4a) * Add zsh shell completions (#127, #201, #202) * playerctld: add `daemon` activation command (1266063) * playerctld: add `unshift` command (#204) * bugfix: improve property setter reliability (c617911) ## Version 2.2.1 Version 2.2.1 contains some bugfixes and new features. * Fix a crash when the `emoji()` template function is used (#167) * Add a `shift` command to `playerctld` to shift the active player (#173) * Fix a crash when system players are present (#175) ## Version 2.1.1 Version 2.1.1 contains some bugfixes and new features. Playerctl now has a test suite that covers most features of the CLI. **playerctld** * Add `playerctld`: an activatable DBus service for selecting the most recently active player (#161, #164, #128) **CLI** * Add the `markup_escape()` formatter function (#133) * Mark the `emoji()` formatter function as no longer experimental * Add the `default()` formatter function (#142, fd0b4ab) * Add the special `%any` player token for prioritizing player selection (#143) * Add bash completions (#153) * Add debug logging (#152) * Rewrite and expand manpage with `mdoc(7)` (#130) * Attempt to autostart `playerctld` if it is present in players * bugfix: incorrect error message for shuffle command (#158) * bugfix: don't crash if no system bus is present (2330b64f) * bugfix: don't crash if given a nonexistent format function (#162) **Build** * Required meson version is now `0.50.0`. ## Version 2.0.2 Version 2.0.2 contains some minor bugfixes for the CLI and build system. The author would like to inform you as part of my effort to improve media player integration on the Linux Desktop, I have fixed many bugs in Electron based media players that should be available soon (see #40, #81, #35 which were closed recently). **CLI** * Regression: exit 1 when no players are found (#126, #119) * Regression: fix sort order for `--player` command (#112) * Handle nonfile uris in the `open` command (#122) **Build** * Fix documentation of the `--follow` flag (#117) * Update manpage release date at build time (#118) * fix gir build on cross compilation (#120) ## Version 2.0.1 Version 2.0.1 includes new major features and breaking changes to the library and CLI. **CLI** * Add `--ignore-player` flag to ignore specific players (#2) * Add `--follow` flag to block and print updated values when they change (#37, #98, #101) * The `--player` command acts on the first player without `--all-players` (breaking) (#54) * Accept multiple keys for `metadata [key]` command (#68) * `metadata` command has tabular output. (breaking) (#72) * Add `--format [fmt]` for metadata formatting (#73) * Add `duration()` template formatter for formatting durations (#75) * Print player name and instance with format strings (#90) * Add command to get and set `shuffle` status (#92) * Add a command to get and set `loop` status (#99) * Add the `open` command to open a URI with the player (#79) * Fix some errors with utf8 printing (#80) * Skip players from selection when they don't support a command (determined by the `can-*` properties) * Select all player instances with the `--player` and `--ignore-player` command * Print help information to stdout (not stderr) when no arguments are passed **Library** * add `playerctl_list_players()` to public api for listing players (#47) * Implement the "seeked" signal on the player (#94) * Add the "volume" signal on the player (#95) * Deprecate the "play", "pause", and "stopped" signal for a single "status" signal (#96) * Add the `PlayerctlPlayerManager()` class (#100) * Cache and compute the position property (#102) * Remove chaining abilities from the library (breaking) * Library query functions return `NULL` instead of empty string when properties aren't found (breaking) * Deprecate `status` property in favor of the `playback-status` property as an enum * Add library functions for `shuffle` and `loop` status (#92, #99) * Deprecate setting volume via the object properties interface * Fix the "exit" signal * Add properties "can-control", "can-play", "can-pause", "can-seek", "can-go-next", "can-go-previous" * Add the "source" property to determine the source of the player (session or system bus) * Change first keyword arg for `playerctl_player_new()` from `name` to `player_name` (breaking) * Add `playerctl_player_new_for_source()` to select players based on the source (session or system bus) * Add `playerctl_player_new_from_name()` to create a player from a PlayerManager name * `playerctl_player_new()` selects an instance of the `player_name` if found * Add documentation for the entire public library API **Build** * Remove autotools and switch to the meson build system (breaking) (#57) * Fix various compiler warnings (#97) * Remove library version from pkg-config name and add it to the so in the standard way (new pkg-config name is just `playerctl`). ## Version 0.6.1 Version 0.6.1 includes bug fixes and some minor features. * Bugfix: unref of a null player when no players are present * Playerctl now searches the system bus for players * Parse trackid as a string as a workaround for noncompliant players * Various meson fixes ## Version 0.6.0 Version 0.6.0 includes bug fixes and new features. * control multiple players at once by putting commas between the names * add the --all-players option to control all players at once * lib: better cache invalidation strategy for getting properties * bugfix: Set position in fractional seconds * Fix various memory leaks and errors NOTE: This will be the last minor release that uses autotools. Playerctl will switch to the meson build system as of the next minor release. Github releases will have a debian package and an rpm, but these will soon be deprecated as package maintainers create official packages for distros. ## Version 0.5.0 Version 0.5.0 includes some new features. New features: - Add workaround for Spotify to get metadata - Add `position` cli command to query and set position - Add `position` property to Player and method to set position to library ## Version 0.4.2 Version 0.4.2 includes several important bug fixes. - Send `Play` directly instead of a `PlayPause` message depending on player status. This was an exception for Spotify that is no longer needed. - Fix memory errors when an initialization error occurs. ## Version 0.4.1 This version includes a fix to support unicode characters when printing metadata. ## Version 0.4.0 This version adds the following features and bugfixes - List players with cli `-l` option. - Fix a bug in the build for some platforms - Remove claim of mplayer support ## Version 0.3.0 This release includes some major bugfixes and some new features mostly for the library for use in applications. - Add the "stop" library and cli command - Add the "exit" signal - emitted when the player exits - Implement player class memory management - Add version macros The following quirks have been corrected (should not be breaking) - Player "player_name" property getter returns the player name and not the DBus name - Player "stop" event correctly emits "stop" and not "pause" - Add include guards so only `` can be included directly Additional packages available by request ## Version 0.2.1 This minor release adds a pkg-config file and relicenses the code under the LGPL. ## Version 0.2.0 This release adds convenient metadata accessors and improves error handling - Add get_artist method to player - Add get_title method to player - Add get_album method to player - Add get_metadata_prop to player - Add [KEY] option to metadata cli - Bugfix: gracefully handle property access when connection to dbus fails by returning empty properties ## Version 0.1.0 This release adds some new player commands and improves error handling - Add the "next" CLI command and player method used to switch to the next track - Add the "previous" CLI command and player method used to switch to the previous track - Print an error message when no players are found in the CLI and propagate an error on initialization in this case in the library - Print an error message when a command fails in the CLI and propagate an error in this case in the library # Version 0.0.1 Playerctl is a command-line utility and library for controlling media players that implement the [MPRIS](http://specifications.freedesktop.org/mpris-spec/latest/) D-Bus Interface Specification. Playerctl makes it easy to bind player actions, such as play and pause, to media keys. For more advanced users, Playerctl provides an [introspectable](https://wiki.gnome.org/action/show/Projects/GObjectIntrospection) library available in your favorite scripting language that allows more detailed control like the ability to subscribe to media player events or get metadata such as artist and title for the playing track. playerctl-2.4.1/CONTRIBUTORS000066400000000000000000000004631412234731200153770ustar00rootroot00000000000000A list of contributors to playerctl, sorted alphabetically on last names. Please keep this list sorted when adding yourself. Pedro Alves Tony Crisci Jente Hidskes Nick Morrott Rasmus Thomsen playerctl-2.4.1/COPYING000066400000000000000000000167431412234731200145620ustar00rootroot00000000000000 GNU LESSER GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. This version of the GNU Lesser General Public License incorporates the terms and conditions of version 3 of the GNU General Public License, supplemented by the additional permissions listed below. 0. Additional Definitions. As used herein, "this License" refers to version 3 of the GNU Lesser General Public License, and the "GNU GPL" refers to version 3 of the GNU General Public License. "The Library" refers to a covered work governed by this License, other than an Application or a Combined Work as defined below. An "Application" is any work that makes use of an interface provided by the Library, but which is not otherwise based on the Library. Defining a subclass of a class defined by the Library is deemed a mode of using an interface provided by the Library. A "Combined Work" is a work produced by combining or linking an Application with the Library. The particular version of the Library with which the Combined Work was made is also called the "Linked Version". The "Minimal Corresponding Source" for a Combined Work means the Corresponding Source for the Combined Work, excluding any source code for portions of the Combined Work that, considered in isolation, are based on the Application, and not on the Linked Version. The "Corresponding Application Code" for a Combined Work means the object code and/or source code for the Application, including any data and utility programs needed for reproducing the Combined Work from the Application, but excluding the System Libraries of the Combined Work. 1. Exception to Section 3 of the GNU GPL. You may convey a covered work under sections 3 and 4 of this License without being bound by section 3 of the GNU GPL. 2. Conveying Modified Versions. If you modify a copy of the Library, and, in your modifications, a facility refers to a function or data to be supplied by an Application that uses the facility (other than as an argument passed when the facility is invoked), then you may convey a copy of the modified version: a) under this License, provided that you make a good faith effort to ensure that, in the event an Application does not supply the function or data, the facility still operates, and performs whatever part of its purpose remains meaningful, or b) under the GNU GPL, with none of the additional permissions of this License applicable to that copy. 3. Object Code Incorporating Material from Library Header Files. The object code form of an Application may incorporate material from a header file that is part of the Library. You may convey such object code under terms of your choice, provided that, if the incorporated material is not limited to numerical parameters, data structure layouts and accessors, or small macros, inline functions and templates (ten or fewer lines in length), you do both of the following: a) Give prominent notice with each copy of the object code that the Library is used in it and that the Library and its use are covered by this License. b) Accompany the object code with a copy of the GNU GPL and this license document. 4. Combined Works. You may convey a Combined Work under terms of your choice that, taken together, effectively do not restrict modification of the portions of the Library contained in the Combined Work and reverse engineering for debugging such modifications, if you also do each of the following: a) Give prominent notice with each copy of the Combined Work that the Library is used in it and that the Library and its use are covered by this License. b) Accompany the Combined Work with a copy of the GNU GPL and this license document. c) For a Combined Work that displays copyright notices during execution, include the copyright notice for the Library among these notices, as well as a reference directing the user to the copies of the GNU GPL and this license document. d) Do one of the following: 0) Convey the Minimal Corresponding Source under the terms of this License, and the Corresponding Application Code in a form suitable for, and under terms that permit, the user to recombine or relink the Application with a modified version of the Linked Version to produce a modified Combined Work, in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source. 1) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (a) uses at run time a copy of the Library already present on the user's computer system, and (b) will operate properly with a modified version of the Library that is interface-compatible with the Linked Version. e) Provide Installation Information, but only if you would otherwise be required to provide such information under section 6 of the GNU GPL, and only to the extent that such information is necessary to install and execute a modified version of the Combined Work produced by recombining or relinking the Application with a modified version of the Linked Version. (If you use option 4d0, the Installation Information must accompany the Minimal Corresponding Source and Corresponding Application Code. If you use option 4d1, you must provide the Installation Information in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source.) 5. Combined Libraries. You may place library facilities that are a work based on the Library side by side in a single library together with other library facilities that are not Applications and are not covered by this License, and convey such a combined library under terms of your choice, if you do both of the following: a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities, conveyed under the terms of this License. b) Give prominent notice with the combined library that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. 6. Revised Versions of the GNU Lesser General Public License. The Free Software Foundation may publish revised and/or new versions of the GNU Lesser General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Library as you received it specifies that a certain numbered version of the GNU Lesser General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that published version or of any later version published by the Free Software Foundation. If the Library as you received it does not specify a version number of the GNU Lesser General Public License, you may choose any version of the GNU Lesser General Public License ever published by the Free Software Foundation. If the Library as you received it specifies that a proxy can decide whether future versions of the GNU Lesser General Public License shall apply, that proxy's public statement of acceptance of any version is permanent authorization for you to choose that version for the Library. playerctl-2.4.1/Dockerfile000066400000000000000000000016241412234731200155110ustar00rootroot00000000000000FROM ubuntu:20.04 WORKDIR /app RUN export DEBIAN_FRONTEND=noninteractive; \ export DEBCONF_NONINTERACTIVE_SEEN=true; \ echo 'tzdata tzdata/Areas select Etc' | debconf-set-selections; \ echo 'tzdata tzdata/Zones/Etc select UTC' | debconf-set-selections; \ apt update && apt install -y --no-install-recommends \ python3-pip \ ninja-build \ build-essential \ libglib2.0-dev \ libgirepository1.0-dev \ gtk-doc-tools \ dbus-x11 COPY requirements.txt . RUN pip3 install -r requirements.txt ADD . /app 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 -vvs"] playerctl-2.4.1/Makefile000066400000000000000000000010311412234731200151470ustar00rootroot00000000000000.PHONY: test docker-test format all .DEFAULT_GOAL := all FORMAT_C_SOURCE = $(shell find playerctl | grep \.[ch]$) EXECUTABLES = clang-format python3 docker yapf dbus-run-session K := $(foreach exec,$(EXECUTABLES),\ $(if $(shell which $(exec)),some string,$(error "No $(exec) in PATH"))) test: dbus-run-session python3 -m pytest -sq docker-test: docker build -t playerctl-test . docker run -it playerctl-test format: yapf -rip test examples clang-format -i ${FORMAT_C_SOURCE} lint: flake8 test all: format docker-test playerctl-2.4.1/README.md000066400000000000000000000361751412234731200150070ustar00rootroot00000000000000# Playerctl For true players only: vlc, mpv, RhythmBox, web browsers, cmus, mpd, spotify and others. [Chat](https://discord.gg/UdbXHVX) ## About Playerctl is a command-line utility and library for controlling media players that implement the [MPRIS](http://specifications.freedesktop.org/mpris-spec/latest/) D-Bus Interface Specification. Playerctl makes it easy to bind player actions, such as play and pause, to media keys. You can also get metadata about the playing track such as the artist and title for integration into statusline generators or other command-line tools. Playerctl also comes with a daemon that allows it to act on the currently active media player called `playerctld`. ## Using the CLI ``` playerctl [--version] [--list-all] [--all-players] [--player=NAME] [--ignore-player=IGNORE] [--format=FORMAT] [--no-messages] COMMAND ``` Here is a list of available commands: | Command | Description | |:----------------------------:| ------------------------------------------------------------------------------------------------------ | | **`play`** | Command the player to play. | | **`pause`** | Command the player to pause | | **`play-pause`** | Command the player to toggle between play/pause. | | **`stop`** | Command the player to stop. | | **`next`** | Command the player to skip to the next track. | | **`previous`** | Command the player to skip to the previous track. | | **`position [OFFSET][+/-]`** | Command the player to go to the position or seek forward or backward OFFSET in seconds. | | **`volume [LEVEL][+/-]`** | Print or set the volume to LEVEL from 0.0 to 1.0. | | **`status`** | Get the play status of the player. Either "Playing", "Paused", or "Stopped". | | **`metadata [KEY...]`** | Print the metadata for the current track. If KEY is passed, print only those values from the metadata. | | **`open [URI]`** | Command for the player to open a given URI. Can be either a file path or a remote URL. | | **`loop [STATUS]`** | Print or set the loop status. Either "None", "Track", or "Playlist". | | **`shuffle [STATUS]`** | Print or set the shuffle status. Either "On", "Off". | ### Selecting Players to Control 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. To start `playerctld`, add the following command to your system startup script: ``` playerctld daemon ``` 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. Similarly, you can ignore players by passing their names with the `--ignore-player` flag. The special player name `%any` can be used in the list of selected players once to match any player not in the list. This can be used to prioritize or deprioritize players. Examples: ```bash # Command the first instance of VLC to play playerctl --player=vlc play # Command all players to stop playerctl --all-players stop # Command VLC to go to the next track if it's running. If it's not, send the # command to Spotify. playerctl --player=vlc,spotify next # Get the status of the first player that is not Gwenview. playerctl --ignore-player=Gwenview status # Command any player to play, but select Chromium last playerctl --player=%any,chromium play # Command any player to play, but select VLC first playerctl --player=vlc,%any 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. For a simple "now playing" banner: ```bash playerctl metadata --format "Now playing: {{ artist }} - {{ album }} - {{ title }}" # prints 'Now playing: Lana Del Rey - Born To Die - Video Games' ``` Included in the template language are some built-in variables and helper functions for common formatting that you can call on template variables. It can also do basic math operations on numbers. ```bash # Prints 'Total length: 3:23' playerctl metadata --format "Total length: {{ duration(mpris:length) }}" # Prints 'At position: 1:16' playerctl position --format "At position: {{ duration(position) }}" # Prints 'Artist in lowercase: lana del rey' playerctl metadata --format "Artist in lowercase: {{ lc(artist) }}" # Prints 'STATUS: PLAYING' playerctl status --format "STATUS: {{ uc(status) }}" # Prints the time remaining in the track (e.g, 'Time remaining: 2:07') playerctl metadata --format "Time remaining: {{ duration(mpris:length - position) }}" # Prints volume from 0 - 100 playerctl metadata --format "Volume: {{ volume * 100 }}" ``` | Function | Argument | Description | | --------------- | --------------- | ------------------------------------------------------------------ | | `lc` | string | Convert the string to lowercase. | | `uc` | string | Convert the string to uppercase. | | `duration` | int | Convert the duration to hh:mm:ss format. | | `markup_escape` | string | Escape XML markup characters in the string. | | `default` | any, any | Print the first value if it is present, or else print the second. | | `emoji` | status or volume | Try to convert the variable to an emoji representation. | | `trunc` | string, int | Truncate string to a maximum length. | | Variable | Description | | ------------ | ------------------------------------------------- | | `playerName` | The name of the current player. | | `position` | The position of the current track in microseconds | | `status` | The playback status of the current player | | `volume` | The volume from 0.0 to 1.0 | | `album` | The album of the current track. | | `artist` | The artist of the current track. | | `title` | The title of the current track. | ### Following changes You can pass the `--follow` flag to query commands to block, wait for players to connect, and print the query whenever it changes. If players are passed with `--player`, players earlier in the list will be preferred in the order they appear unless `--all-players` is passed. When no player can support the query, such as when all the players exit, a newline will be printed. For example, to be notified of information about the latest currently playing track for your media players, use: ```bash playerctl metadata --format '{{ playerName }}: {{ artist }} - {{ title }} {{ duration(position) }}|{{ duration(mpris:length) }}' --follow ``` ### Changing the position of the track You can seek to a position in the track or skip forward and back. ```bash # Go back 30 seconds playerctl position 30- # Go forward 30 seconds playerctl position 30+ # Seek to the position at 30 seconds playerctl position 30 ``` ## Troubleshooting ### Debug Logging To enable debug logging, set the environment variable `G_MESSAGES_DEBUG=playerctl`. It's helpful to include a debug log when you report issues. ### No Players Found Some players like Spotify require certain DBus environment variables to be set which are normally set within the session manager. If you're not using a session manager or it does not set these variables automatically (like `xinit`), launch your desktop environment wrapped in a `dbus-launch` command. For example, in your `.xinitrc` file, use this to start your WM: ``` exec dbus-launch --autolaunch=$(cat /var/lib/dbus/machine-id) i3 ``` Some players may require installation of a plugin or other configuration. In Quod Libet open the window File -> Plugins and select the plugin called *MPRIS D-Bus Support*. ### Playerctld Autostart Issues If `playerctld` does not autostart and you use `xinit` and systemd, you might need this fix to enable DBus activation to work correctly: ``` systemctl --user import-environment DISPLAY XAUTHORITY if which dbus-update-activation-environment >/dev/null 2>&1; then dbus-update-activation-environment DISPLAY XAUTHORITY fi ``` ## Installing First, check and see if Playerctl is available from your package manager (if it is not, get someone to host a package for you) and also check the [releases](https://github.com/altdesktop/playerctl/releases) page on github. ### Fedora `playerctl` is available for Fedora 28 or later: ``` sudo dnf install playerctl ``` ### Mageia, openSUSE `playerctl` is available for Mageia and openSUSE via [this COPR repository](https://copr.fedorainfracloud.org/coprs/jflory7/playerctl/). First, install the repository file for your distribution from COPR. Then, install `playerctl` with your package manager of choice. ### Guix `playerctl` is available as a [Guix](https://guix.gnu.org) package which can be installed on any Linux distribution after [installing Guix](https://guix.gnu.org/manual/en/html_node/Installation.html): ``` guix install playerctl ``` ### Compile from source Using the cli and library requires [GLib](https://developer.gnome.org/glib/) (which is a dependency of almost all of these players as well, so you probably already have it). You can use the library in almost any programming language with the associated [introspection binding library](https://wiki.gnome.org/Projects/GObjectIntrospection/Users). Additionally, you also need the following build dependencies: [gobject-introspection](https://wiki.gnome.org/action/show/Projects/GObjectIntrospection) for building introspection data (configurable with the `introspection` meson option) [gtk-doc](http://www.gtk.org/gtk-doc/) for building documentation (configurable with the `gtk-doc` meson option) Fedora users also need to install `redhat-rpm-config` To generate and build the project to contribute to development and install playerctl to `/`: ``` meson mesonbuild sudo ninja -C mesonbuild install ``` Note that you need `meson` installed. In case your distro only has an older version of meson in its repository you can install the newest version via pip: ``` pip3 install meson ``` Also keep in mind that gtk-doc and gobject-introspection are enabled by default, you can disable them with `-Dintrospection=false` and `-Dgtk-doc=false`. If you don't want to install playerctl to `/` you can install it elsewhere by exporting `DESTDIR` before invoking ninja, e.g.: ``` export PREFIX="/usr/local" meson --prefix="${PREFIX}" --libdir="${PREFIX}/lib" mesonbuild export DESTDIR="$(pwd)/install" ninja -C mesonbuild install ``` You can use it later on by exporting the following variables: ``` export LD_LIBRARY_PATH="$DESTDIR/${PREFIX}/lib/:$LD_LIBRARY_PATH" export GI_TYPELIB_PATH="$DESTDIR/${PREFIX}/lib/:$GI_TYPELIB_PATH" export PATH="$DESTDIR/${PREFIX}/bin:$PATH" ``` ## Using the Library To use a scripting library, find your favorite language from [this list](https://wiki.gnome.org/Projects/GObjectIntrospection/Users) and install the bindings library. Documentation for the library is hosted [here](https://dubstepdish.com/playerctl). For examples on how to use the library, see the [examples](https://github.com/acrisci/playerctl/blob/master/examples) folder. ### Example Python Script For more advanced users, Playerctl provides an [introspectable](https://wiki.gnome.org/action/show/Projects/GObjectIntrospection) library available in your favorite scripting language that allows more detailed control like the ability to subscribe to media player events or get metadata such as artist and title for the playing track. This example uses the [Python bindings](https://wiki.gnome.org/action/show/Projects/PyGObject). ```python #!/usr/bin/env python3 from gi.repository import Playerctl, GLib player = Playerctl.Player('vlc') def on_metadata(player, metadata): if 'xesam:artist' in metadata.keys() and 'xesam:title' in metadata.keys(): print('Now playing:') print('{artist} - {title}'.format( artist=metadata['xesam:artist'][0], title=metadata['xesam:title'])) def on_play(player, status): print('Playing at volume {}'.format(player.props.volume)) def on_pause(player, status): print('Paused the song: {}'.format(player.get_title())) player.connect('playback-status::playing', on_play) player.connect('playback-status::paused', on_pause) player.connect('metadata', on_metadata) # start playing some music player.play() if player.get_artist() == 'Lana Del Rey': # I meant some good music! player.next() # wait for events main = GLib.MainLoop() main.run() ``` For a more complete example which is capable of listening to when players start and exit, see [player-manager.py](https://github.com/acrisci/playerctl/blob/master/examples/player-manager.py) from the official examples. ## Resources Check out the following articles about Playerctl: * [2 new apps for music tweakers on Fedora Workstation - Fedora Magazine](https://fedoramagazine.org/2-new-apps-for-music-tweakers-on-fedora-workstation/ "2 new apps for music tweakers on Fedora Workstation") * [Playerctl at Version 2.0](https://dubstepdish.com/index.php/2018/10/21/playerctl-at-version-2-0/) Related projects from the maker of Playerctl: * [altdesktop/python-dbus-next](https://github.com/altdesktop/python-dbus-next) - The DBus library used in the Playerctl test suite. * [altdesktop/playerbm](https://github.com/altdesktop/playerbm) - A CLI bookmark utility for audiobooks and podcasts. * [dbusjs/mpris-service](https://github.com/dbusjs/mpris-service) - MPRIS implementation for JavaScript targeting Electron apps. ## License This work is available under the GNU Lesser General Public License (See COPYING). Copyright © 2014, Tony Crisci playerctl-2.4.1/data/000077500000000000000000000000001412234731200144255ustar00rootroot00000000000000playerctl-2.4.1/data/meson.build000066400000000000000000000016421412234731200165720ustar00rootroot00000000000000application_id = 'org.mpris.MediaPlayer2.playerctld' service_conf = configuration_data() service_conf.set('application_id', application_id) service_conf.set('bindir', bindir) service_conf.set('prefix', prefix) service = application_id + '.service' configure_file( input: service + '.in', output: service, install: true, install_dir: join_paths(datadir, 'dbus-1', 'services'), configuration: service_conf ) if get_option('bash-completions') bash_files = files( 'playerctl.bash', ) if bash_comp.found() bash_install_dir = bash_comp.get_pkgconfig_variable('completionsdir') else bash_install_dir = join_paths(datadir, 'bash-completion', 'completions') endif install_data(bash_files, install_dir: bash_install_dir) endif if get_option('zsh-completions') zsh_install_dir = join_paths(datadir, 'zsh', 'site-functions') install_data('playerctl.zsh', install_dir: zsh_install_dir, rename: '_playerctl') endif playerctl-2.4.1/data/mpris-dbus-interface.xml000066400000000000000000000136121412234731200211750ustar00rootroot00000000000000 playerctl-2.4.1/data/org.mpris.MediaPlayer2.playerctld.service.in000066400000000000000000000001101412234731200247430ustar00rootroot00000000000000[D-BUS Service] Name=@application_id@ Exec=@prefix@/@bindir@/playerctld playerctl-2.4.1/data/playerctl.bash000066400000000000000000000017371412234731200172730ustar00rootroot00000000000000#!/usr/bin/env bash _playerctl_completions() { local cur="${COMP_WORDS[$COMP_CWORD]}" local prev="${COMP_WORDS[$COMP_CWORD - 1]}" local root_words=" play pause play-pause stop next previous position volume status metadata open loop shuffle -h --help -p --player= -a --all-players -i --ignore-player= -f --format -F --follow -l --list-all -v --version" case $prev in loop) COMPREPLY=($(compgen -W "none track playlist" -- "$cur")) return 0 ;; shuffle) COMPREPLY=($(compgen -W "on off" -- "$cur")) return 0 ;; -p|--player=|-i|--ignore-player=) COMPREPLY=($(compgen -W "$(playerctl --list-all)" -- "$cur")) return 0 ;; -f|--format) COMPREPLY=() return 0 ;; open) compopt -o default COMPREPLY=() ;; position|volume|metadata) COMPREPLY=() return 0 ;; *) COMPREPLY=($(compgen -W "$root_words" -- "$cur")) return 0 ;; esac } complete -F _playerctl_completions playerctl playerctl-2.4.1/data/playerctl.syms000066400000000000000000000001641412234731200173420ustar00rootroot00000000000000{ global: Playerctl*; PLAYERCTL*; playerctl_*; pctl_*; local: *; }; playerctl-2.4.1/data/playerctl.zsh000066400000000000000000000050301412234731200171500ustar00rootroot00000000000000#compdef playerctl typeset -A opt_args __playerctl() { command playerctl "$@" 2>/dev/null } __playerctl_ctx() { local -a player_opts=( ${(kv)opt_args[(I)-p|--player]} ${(kv)opt_args[(I)-i|--ignore-player]} ${(kv)opt_args[(I)-a|--all-players]} ) __playerctl "$player_opts[@]" "$@" } local -a playercmd_loop=(/$'(none|track|playlist)\0'/ ':(none track playlist)') local -a playercmd_shuffle=(/$'(on|off)\0'/ ':(on off)') (( $+functions[_playerctl_players] )) || _playerctl_players() { local -a players=( ${(@f)"$(__playerctl --list-all)"} ) players+=( "%all" ) compadd "$@" -a players } (( $+functions[_playerctl_metadata_keys] )) || _playerctl_metadata_keys() { local -a keys __playerctl_ctx metadata | while read PLAYER KEY VALUE; do keys+="$KEY" done _multi_parts "$@" -i ":" keys } local -a playerctl_command_metadata_keys=(/$'[^\0]#\0'/ ':keys:key:_playerctl_metadata_keys') local -a playerctl_command _regex_words commands 'playerctl command' \ 'play:Command the player to play' \ 'pause:Command the player to pause' \ 'play-pause:Command the player to toggle between play/pause' \ 'stop:Command the player to stop' \ 'next:Command the player to skip to the next track' \ 'previous:Command the player to skip to the previous track' \ 'position:Command the player to go or seek to the position' \ 'volume:Print or set the volume level from 0.0 to 1.0' \ 'status:Get the play status of the player' \ 'metadata:Print the metadata information for the current track:$playerctl_command_metadata_keys' \ 'open:Command the player to open the given URI' \ 'loop:Print or set the loop status:$playercmd_loop' \ 'shuffle:Print or set the shuffle status:$playercmd_shuffle' playerctl_command=( /$'[^\0]#\0'/ "$reply[@]" ) _regex_arguments _playerctl_command "$playerctl_command[@]" _arguments -S -s\ '(-h --help)'{-h,--help}'[Show help message and quit]' \ '(-v --version)'{-v,--version}'[Print version information and quit]' \ '(-l --list-all)'{-l,--list-all}'[List all available players]' \ '(-F, --follow)'{-F,--follow}'[Bock and append the query to output when it changes]' \ '(-f --format)'{-f,--format=}'[Format string for printing properties and metadata]' \ '(-i --ignore-player)'{-i,--ignore-player=}'[Comma separated list of players to ignore]:players:_sequence _playerctl_players' \ '(-a --all-players)'{-a,--all-players}'[Control all players instead of just the first]' \ '(-p --player)'{-p,--player=}'[Comma separated list of players to control]:players:_sequence _playerctl_players' \ '*::playerctl command:= _playerctl_command' playerctl-2.4.1/doc/000077500000000000000000000000001412234731200142615ustar00rootroot00000000000000playerctl-2.4.1/doc/meson.build000066400000000000000000000006331412234731200164250ustar00rootroot00000000000000man_page = configure_file( input: 'playerctl.1.in', output: 'playerctl.1', configuration: version_conf, ) install_man(man_page) if get_option('gtk-doc') gtkdoc = find_program('gtkdoc-scan', required: false) if not gtkdoc.found() error('You need to have gtk-doc installed to generate docs. Disable it with `-Dgtk-doc=false` if you don\'t want to build docs') endif subdir('reference') endif playerctl-2.4.1/doc/playerctl.1.in000066400000000000000000000160761412234731200167610ustar00rootroot00000000000000.Dd @PLAYERCTL_RELEASE_DATE@ .Dt PLAYERCTL 1 .Os .Sh NAME .Nm playerctl .Nd control media players via MPRIS .Sh SYNOPSIS .Nm .Op Fl aFhlV .Op Fl f Ar FORMAT .Op Fl i Ar NAME .Op Fl p Ar NAME .Cm command .Sh DESCRIPTION The .Nm utility controls MPRIS-enabled media players. In addition to offering play, pause and stop control, .Nm also offers previous and next track support, the ability to seek backwards and forwards in a track, and volume control. .Nm also supports displaying metadata .Pq e.g., artist, title, album for the current track, and showing the status of the player. .Pp Players that can be controlled using .Nm include .Xr audacious 1 , .Xr cmus 1 , .Xr mopidy 1 , .Xr mpd 1 , .Xr quodlibet 1 , .Xr rhythmbox 1 , .Xr vlc 1 and .Xr xmms2 1 . However, any player that implements the MPRIS interface specification can be controlled using .Nm including web browsers. .Pp Playerctl also comes with a daemon called .Nm playerctld which keeps track of media player activity. When .Nm playerctld is running, .Nm commands will act on the media player with the most recent activity. Run the command .Nm playerctld daemon to start the daemon. .Pp The options are as follows: .Bl -tag -width Ds .It Fl a , -all-players Apply command to all available players. .It Fl F , -follow Block and output the updated query when it changes. .It Fl f Ar FORMAT , Fl -format Ar FORMAT Set the output of the current command to .Ar FORMAT . See .Sx Format Strings . .It Fl h , -help Print this help, then exit. .It Fl i Ar NAME , Fl -ignore-player Ar NAME Ignore the specific player .Ar NAME . Multiple players can be specified in a comma-separated list. .It Fl l , -list-all List the names of running players that can be controlled. .It Fl p Ar NAME , Fl -player Ar NAME Control the specific player .Ar NAME . Multiple players can be specified in a comma-separated list. Defaults to the first available player. The name "name" matches both "name" and "name.{INSTANCE}". Additionally, the name "%any" matches any player. .It Fl s, -no-messages Silence some diagnostic and error messages. .It Fl V , -version Print version number, then exit. .El .Pp The commands are as follows: .Bl -tag -width Ds .It Cm status Get the current status of the player. .It Cm play Command the player to play. .It Cm pause Command the player to pause. .It Cm play-pause Command the player to toggle between play and pause. .It Cm stop Command the player to stop. .It Cm next Command the player to skip to the next track. .It Cm previous Command the player to skip to the previous track. .It Cm position Op Ar OFFSET Ns Op Ar + Ns | Ns Ar - Print the position of the current track in seconds. With .Ar OFFSET specified, seek to .Ar OFFSET seconds from the start of the current track. With the optional .Op Ar + Ns | Ns Ar - appended, seek forward or backward .Ar OFFSET seconds from the current position. .It Cm volume Op Ar LEVEL Ns Op + Ns | Ns Ar - Print the player's volume scaled from 0.0 .Pq 0% to 1.0 .Pq 100% . With .Ar LEVEL specified, set the player's volume to .Ar LEVEL . With the optional .Op Ar + Ns | Ns Ar - appended, increase or decrease the player's volume by .Ar LEVEL . .It Cm metadata Op Ar KEY Print all metadata properties for the current track set by the current player. If .Ar KEY is specified only the value of .Ar KEY is printed. .It Cm open Ar URI Open .Ar URI in the player. .Ar URI may be the name of a file or an external URL. .It Cm shuffle Op Ic On | Off | Toggle Print the shuffle status of the player. With the shuffle status specified, set the shuffle status to either .Ic On , .Ic Off , or .Ic Toggle .It Cm loop Op Ic None | Track | Playlist Print the loop status of the player. With the loop status specified, set the loop status to .Ic None .Pq disable looping , .Ic Track .Pq loop the current track , or .Ic Playlist .Pq loop the current playlist . .El .Ss Format Strings The output of the .Cm position , .Cm metadata , .Cm status and .Cm volume commands can be controlled using a format string. Variables set by these commands can be included in the format string by enclosing them in curly braces: .Ql Brq Brq Va var . These will then be expanded on output. .Pp Each command has access to the following variables: .Bl -tag -width Ds .It Va playerName The name of the current player. .It Va position The time position of the current track, in microseconds. .It Va status The status of the current player. .It Va volume The player's volume scaled from 0.0 .Pq 0% to 1.0 .Pq 100% . .El .Pp Each property listed in the .Cm metadata command are also set as variables. It is recommended to check this list for each player, as different players may not set the same properties. See the .%T MPRIS v2 metadata guidelines for a list of all properties in the MPRIS specification. The most common properties are as follows: .Bl -tag -width Ds .It Va album , xesam:album The album of the current track. .It Va artist , xesam:artist The artist of the current track. .It Va title , xesam:title The title of the current track. .El .Pp Helper functions are also available to transform expanded variables into other representations. They are called in the form .Ql Brq Brq Fn func var . The helper functions are as follows: .Bl -tag -width Ds .It Fn lc str Convert string .Va str to lowercase. .It Fn uc str Convert string .Va str to uppercase. .It Fn markup_escape str Escape XML characters in string .Va str . .It Fn default str1 str2 Print .Fa str1 if set, else print .Fa str2 . .It Fn duration time Reformat the microsecond timestamp .Va time in the form .Ql hh:mm:ss . Can only be called with .Va position or .Va mpris:length . .It Fn emoji key Try to convert the value for .Fa key to an emoji representation. Currently implemented for .Fa status and .Fa volume . .It Fn trunc str len Truncate .Fa str to a maximum of .Fa len characters, adding an ellipsis (…) if necessary. .El .Pp The template language is also able to perform basic math operations. .Pp References to unknown functions will cause .Nm to exit with an error. References to unknown variables will be expanded to empty strings. Text not enclosed in braces will be printed verbatim. .Sh EXIT STATUS .Ex -std .Sh EXAMPLES Print the player name, playback status in lowercase, and position and length in human readable form: .Bd -literal -offset indent $ playerctl metadata --format '{{playerName}}: {{lc(status)}} '\e \&'{{duration(position)}}|{{duration(mpris:length)}}' .Ed .Sh SEE ALSO .Rs .%T MPRIS v2 metadata guidelines .%D September 18, 2013 .%I freedesktop.org .%U https://freedesktop.org/wiki/Specifications/mpris-spec/metadata/ .Re .Pp .Lk https://github.com/altdesktop/playerctl "playerctl homepage" , .Lk https://dubstepdish.com/playerctl "playerctl API documentation" , .Lk https://wiki.gnome.org/Projects/GObjectIntrospection/Users \ "GObject introspection language bindings" .Sh AUTHORS .An -nosplit The .Nm utility is maintained by .An Tony Crisci Aq Mt tony@dubstepdish.com and is made available under the GNU Lesser General Public License 3.0. .Pp This reference was written by .An Nick Morrott Aq Mt knowledgejunkie@gmail.com for the Debian GNU/Linux project. It was later updated and expanded by .An Stephen Gregoratto Aq Mt dev@sgregoratto.me . playerctl-2.4.1/doc/reference/000077500000000000000000000000001412234731200162175ustar00rootroot00000000000000playerctl-2.4.1/doc/reference/meson.build000066400000000000000000000020101412234731200203520ustar00rootroot00000000000000glib_prefix = dependency('glib-2.0').get_pkgconfig_variable('prefix') glib_docpath = join_paths(glib_prefix, 'share', 'gtk-doc', 'html') configure_file( input : 'version.xml.in', output : 'version.xml', configuration : version_conf, ) gnome.gtkdoc( meson.project_name(), src_dir: join_paths(meson.source_root(), 'playerctl'), dependencies: [ glib_dep, playerctl_shared_link, ], mkdb_args: [ '--output-format=xml', '--name-space=' + meson.project_name(), ], scan_args: '--deprecated-guards="PLAYERCTL_DISABLE_DEPRECATED"', gobject_typesfile: join_paths(meson.current_build_dir(), meson.project_name() + '.types'), fixxref_args: [ '--extra-dir=@0@'.format(join_paths(glib_docpath, 'gobject')), '--extra-dir=@0@'.format(join_paths(glib_docpath, 'gio')), '--extra-dir=@0@'.format(glib_docpath), ], main_sgml: 'playerctl-docs.xml', ignore_headers: [ 'playerctl.h', 'playerctl-generated.h', 'playerctl-common.h', 'playerctl-formatter.h', ], install: true, ) playerctl-2.4.1/doc/reference/playerctl-docs.xml000066400000000000000000000026241412234731200216720ustar00rootroot00000000000000 ]> Playerctl Reference Manual for Playerctl &version;. The latest version of this documentation can be found on-line at http://dubstepdish.com/playerctl/. Playerctl Object Hierarchy API Index Index of deprecated API playerctl-2.4.1/doc/reference/version.xml.in000066400000000000000000000000241412234731200210270ustar00rootroot00000000000000@PLAYERCTL_VERSION@ playerctl-2.4.1/examples/000077500000000000000000000000001412234731200153325ustar00rootroot00000000000000playerctl-2.4.1/examples/basic-example.py000077500000000000000000000015601412234731200204230ustar00rootroot00000000000000#!/usr/bin/env python3 from gi.repository import Playerctl, GLib player = Playerctl.Player() def on_metadata(player, metadata): if 'xesam:artist' in metadata.keys() and 'xesam:title' in metadata.keys(): print('Now playing:') print('{artist} - {title}'.format(artist=metadata['xesam:artist'][0], title=metadata['xesam:title'])) def on_play(player, status): print('Playing at volume {}'.format(player.props.volume)) def on_pause(player, status): print('Paused the song: {}'.format(player.get_title())) player.connect('playback-status::playing', on_play) player.connect('playback-status::paused', on_pause) player.connect('metadata', on_metadata) # start playing some music player.play() if player.get_artist() == 'Lana Del Rey': player.next() # wait for events main = GLib.MainLoop() main.run() playerctl-2.4.1/examples/notify.py000077500000000000000000000030421412234731200172160ustar00rootroot00000000000000#!/usr/bin/env python3 from gi.repository import GLib import gi gi.require_version('Playerctl', '2.0') from gi.repository import Playerctl manager = Playerctl.PlayerManager() gi.require_version('Notify', '0.7') from gi.repository import Notify Notify.init("Media Player") notification = Notify.Notification.new("") from urllib.parse import urlparse, unquote from pathlib import Path from gi.repository import GdkPixbuf import os.path def notify(player): metadata = player.props.metadata keys = metadata.keys() if 'xesam:artist' in keys and 'xesam:title' in keys: notification.update(metadata['xesam:title'], metadata['xesam:artist'][0]) path = Path(unquote(urlparse( metadata['xesam:url']).path)).parent / "cover.jpg" if os.path.exists(path): image = GdkPixbuf.Pixbuf.new_from_file(str(path)) notification.set_image_from_pixbuf(image) notification.show() def on_play(player, status, manager): notify(player) def on_metadata(player, metadata, manager): notify(player) def init_player(name): player = Playerctl.Player.new_from_name(name) player.connect('playback-status::playing', on_play, manager) player.connect('metadata', on_metadata, manager) manager.manage_player(player) notify(player) def on_name_appeared(manager, name): init_player(name) manager.connect('name-appeared', on_name_appeared) for name in manager.props.player_names: init_player(name) main = GLib.MainLoop() main.run() Notify.uninit() playerctl-2.4.1/examples/player-manager.py000077500000000000000000000022341412234731200206140ustar00rootroot00000000000000#!/usr/bin/env python3 from gi.repository import Playerctl, GLib manager = Playerctl.PlayerManager() def on_play(player, status, manager): print('player is playing: {}'.format(player.props.player_name)) def on_metadata(player, metadata, manager): keys = metadata.keys() if 'xesam:artist' in keys and 'xesam:title' in keys: print('{} - {}'.format(metadata['xesam:artist'][0], metadata['xesam:title'])) def init_player(name): # choose if you want to manage the player based on the name if name.name in ['vlc', 'cmus']: player = Playerctl.Player.new_from_name(name) player.connect('playback-status::playing', on_play, manager) player.connect('metadata', on_metadata, manager) manager.manage_player(player) def on_name_appeared(manager, name): init_player(name) def on_player_vanished(manager, player): print('player has exited: {}'.format(player.props.player_name)) manager.connect('name-appeared', on_name_appeared) manager.connect('player-vanished', on_player_vanished) for name in manager.props.player_names: init_player(name) main = GLib.MainLoop() main.run() playerctl-2.4.1/fpm-packages.sh000077500000000000000000000041351412234731200164140ustar00rootroot00000000000000#!/bin/sh set -e PROJECT_ROOT=${PWD} FPM_DIR=${PWD}/playerctl-fpm DEB_DIR=${FPM_DIR}/deb RPM_DIR=${FPM_DIR}/rpm MESON_DIR=${FPM_DIR}/build # sanity check if [[ ! -f playerctl/playerctl.h ]]; then echo 'You must run this from the playerctl project directory' exit 1 fi packages=(fpm rpm dpkg) for pkg in ${packages[@]}; do if ! hash ${pkg}; then echo "you need ${pkg} to package playerctl" exit 127 fi done rm -rf ${FPM_DIR} mkdir -p ${FPM_DIR} fpm_deb() { cd ${PROJECT_ROOT} meson ${DEB_DIR}/build --prefix=/usr --libdir=/usr/lib DESTDIR=${DEB_DIR}/install ninja -C ${DEB_DIR}/build install VERSION=`LD_LIBRARY_PATH=${DEB_DIR}/install/usr/lib ${DEB_DIR}/install/usr/bin/playerctl -v | sed s/^v// | sed s/-.*//` cd ${DEB_DIR}/install fpm -s dir -t deb -n playerctl -v ${VERSION} \ -p playerctl-VERSION_ARCH.deb \ -d "libglib2.0-0" \ usr/include usr/lib usr/bin usr/share echo -e "\nDEBIAN PACKAGE CONTENTS" echo -e "-----------------------" dpkg -c ${DEB_DIR}/install/playerctl-${VERSION}_amd64.deb mv ${DEB_DIR}/install/playerctl-${VERSION}_amd64.deb ${FPM_DIR} cd - &> /dev/null } fpm_rpm() { cd ${PROJECT_ROOT} meson ${RPM_DIR}/build --prefix=/usr --libdir=/usr/lib64 DESTDIR=${RPM_DIR}/install ninja -C ${RPM_DIR}/build install VERSION=`LD_LIBRARY_PATH=${RPM_DIR}/install/usr/lib64 ${RPM_DIR}/install/usr/bin/playerctl -v | sed s/^v// | sed s/-.*//` cd ${RPM_DIR}/install fpm -s dir -t rpm -n playerctl -v ${VERSION} \ -p playerctl-VERSION_ARCH.rpm \ -d "glib2" \ usr/include usr/lib64 usr/bin usr/share echo -e "\nRPM PACKAGE CONTENTS" echo -e "--------------------" rpm -qlp ${RPM_DIR}/install/playerctl-${VERSION}_x86_64.rpm mv ${RPM_DIR}/install/playerctl-${VERSION}_x86_64.rpm ${FPM_DIR} cd - &> /dev/null } do_dist() { local DIST_DIR=${FPM_DIR}/dist meson ${DIST_DIR} ninja -C ${DIST_DIR} dist gpg --sign --armor --detach-sign ${DIST_DIR}/meson-dist/playerctl-*.tar.xz mv ${DIST_DIR}/meson-dist/* ${FPM_DIR} } fpm_deb fpm_rpm do_dist playerctl-2.4.1/letter-to-spotify-support.md000066400000000000000000000021041412234731200211570ustar00rootroot00000000000000 Dear Spotify Customer Support, Thank you for supporting the Linux Desktop environment. Linux support is an important part of my Spotify user experience. Part of that experience is integration of the media player into my desktop environment. This includes making the keyboard media keys work correctly and getting basic information about a playing track for display in external desktop applications. Please continue supporting these features. There are some features right now that could use better support. This includes complete track metadata, getting and setting the position of the playing track, and getting and setting the volume of the player. On Linux, these features are specified in the MPRIS D-Bus Interface Specification. If Spotify were to completely implement this interface, it would greatly increase my satisfaction with your product. Please allocate some time for your developers to implement these features. Thanks playerctl-2.4.1/meson.build000066400000000000000000000020541412234731200156570ustar00rootroot00000000000000project( 'playerctl', 'c', version: '2.4.1', meson_version: '>=0.56.0' ) release_date = 'September 21, 2021' gnome = import('gnome') pkgconfig = import('pkgconfig') datadir = get_option('datadir') bindir = get_option('bindir') prefix = get_option('prefix') version_conf = configuration_data() playerctl_version = meson.project_version().split('-')[0] version_array = playerctl_version.split('.') playerctl_major_version = version_array[0] version_conf.set( 'PLAYERCTL_VERSION', meson.project_version(), ) version_conf.set( 'PLAYERCTL_MAJOR_VERSION', playerctl_major_version.to_int(), ) version_conf.set( 'PLAYERCTL_MINOR_VERSION', version_array[1].to_int(), ) version_conf.set( 'PLAYERCTL_MICRO_VERSION', version_array[2].to_int(), ) version_conf.set( 'PLAYERCTL_RELEASE_DATE', release_date, ) gobject_dep = dependency('gobject-2.0', version: '>=2.38') gio_dep = dependency('gio-unix-2.0') glib_dep = dependency('glib-2.0') bash_comp = dependency('bash-completion', required: false) subdir('playerctl') subdir('data') subdir('doc') playerctl-2.4.1/meson_options.txt000066400000000000000000000005621412234731200171540ustar00rootroot00000000000000option('gtk-doc', type: 'boolean', value: true, description: 'build docs') option('introspection', type: 'boolean', value: true, description: 'build gir data') option('bash-completions', type: 'boolean', value: false, description: 'Install bash shell completions.') option('zsh-completions', type: 'boolean', value: false, description: 'Install zsh shell completions.') playerctl-2.4.1/playerctl/000077500000000000000000000000001412234731200155135ustar00rootroot00000000000000playerctl-2.4.1/playerctl/meson.build000066400000000000000000000070441412234731200176620ustar00rootroot00000000000000playerctl_version_header = configure_file( input: 'playerctl-version.h.in', output: 'playerctl-version.h', configuration: version_conf, ) # Include the just generated playerctl_version header configuration_inc = include_directories('..') playerctl_generated = gnome.gdbus_codegen( 'playerctl-generated', join_paths(meson.project_source_root(), 'data', 'mpris-dbus-interface.xml'), ) headers = [ 'playerctl.h', 'playerctl-player.h', 'playerctl-player-manager.h', 'playerctl-player-name.h', playerctl_version_header, ] playerctl_sources = [ 'playerctl-player-name.c', 'playerctl-formatter.c', 'playerctl-player.c', 'playerctl-common.c', 'playerctl-player-manager.c', playerctl_generated, ] # Allow including playerctl.h during compilation c_args = [ '-DPLAYERCTL_COMPILATION', '-DG_LOG_DOMAIN="playerctl"', ] enums = gnome.mkenums_simple( 'playerctl-enum-types', sources: headers, install_header: true, install_dir: join_paths(get_option('includedir'), 'playerctl') ) deps = [ gobject_dep, gio_dep, ] symbols_file = join_paths(meson.project_source_root(), 'data', 'playerctl.syms') symbols_flag = '-Wl,--version-script,@0@'.format(symbols_file) # default_library is shared by default see # https://mesonbuild.com/Builtin-options.html this enabled the project # to be either statically or dynamically linked as a subproject playerctl_lib = library( 'playerctl', playerctl_sources, enums, dependencies: deps, include_directories: configuration_inc, version: playerctl_version, install: true, link_args : symbols_flag, link_depends: symbols_file, c_args: c_args, ) # Required for linking against the shared lib we just built playerctl_shared_link = declare_dependency( link_with: playerctl_lib, dependencies: deps, include_directories: include_directories('..'), ) playerctl_executable = executable( 'playerctl', 'playerctl-cli.c', enums, dependencies: playerctl_shared_link, include_directories: configuration_inc, install: true, ) playerctld_executable = executable( 'playerctld', 'playerctl-daemon.c', enums, dependencies: deps, include_directories: configuration_inc, install: true, c_args: c_args, ) install_headers( headers, install_dir: join_paths(get_option('includedir'), 'playerctl'), ) if get_option('introspection') # The below isn't strictly required, since meson checks for gobject-introspection anyway when # we call gnome.generate_gir. However, doing it this way we have a little nicer error reporting # in case the user enabled instropection but doesn't have gobject-introspection installed. introspection_dep = dependency('gobject-introspection-1.0', required: false) if not introspection_dep.found() error('You need to have gobject-introspection installed to generate Gir data. Disable it with `-Dintrospection=false` if you don\'t want to build it') endif gnome.generate_gir( playerctl_lib, sources: [ enums, 'playerctl-player-name.c', 'playerctl-player-name.h', 'playerctl-player-manager.c', 'playerctl-player-manager.h', 'playerctl-player.c', 'playerctl-player.h', ], extra_args : [ '-DPLAYERCTL_COMPILATION' ], nsversion: playerctl_major_version + '.0', namespace: 'Playerctl', includes: ['GObject-2.0'], install: true, ) endif pkgconfig.generate( libraries: playerctl_lib, subdirs: 'playerctl', version: meson.project_version(), name: 'Playerctl', filebase: 'playerctl', description: 'A C library for MPRIS players', requires: ['gobject-2.0'], requires_private: 'gio-2.0', ) playerctl-2.4.1/playerctl/playerctl-cli.c000066400000000000000000001311051412234731200204240ustar00rootroot00000000000000/* * This file is part of playerctl. * * playerctl is free software: you can redistribute it and/or modify it under * the terms of the GNU Lesser General Public License as published by the Free * Software Foundation, either version 3 of the License, or (at your option) * any later version. * * playerctl is distributed in the hope that it will be useful, but WITHOUT ANY * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for * more details. * * You should have received a copy of the GNU Lesser General Public License * along with playerctl If not, see . * * Copyright © 2014, Tony Crisci and contributors. */ #include #include #include #include #include #include #include #include #include #include "playerctl-common.h" #include "playerctl-formatter.h" #include "playerctl-player-private.h" #define LENGTH(array) (sizeof array / sizeof array[0]) // clang-format off G_DEFINE_QUARK(playerctl-cli-error-quark, playerctl_cli_error); // clang-format on /* The CLI will exit with this exit status */ static gint exit_status = 0; /* A comma separated list of players to control. */ static gchar *player_arg = NULL; /* A comma separated list of players to ignore. */ static gchar *ignore_player_arg = NULL; /* If true, control all available media players */ static gboolean select_all_players; /* If true, list all available players' names and exit. */ static gboolean list_all_players_and_exit; /* If true, print the version and exit. */ static gboolean print_version_and_exit; /* If true, don't print error messages related to status. */ static gboolean no_status_error_messages; /* The commands passed on the command line, filled in via G_OPTION_REMAINING. */ static gchar **command_arg = NULL; /* A format string for printing properties and metadata */ static gchar *format_string_arg = NULL; /* The formatter for the format string argument if present */ static PlayerctlFormatter *formatter = NULL; /* Block and follow the command */ static gboolean follow = FALSE; /* The main loop for the follow command */ static GMainLoop *main_loop = NULL; /* The last output printed by the cli */ static gchar *last_output = NULL; /* The manager of all the players we connect to */ static PlayerctlPlayerManager *manager = NULL; /* List of player names parsed from the --player arg */ static GList *player_names = NULL; /* List of ignored player names passed from the --ignore-player arg*/ static GList *ignored_player_names = NULL; /* forward definitions */ static void managed_players_execute_command(GError **error); /* * Sometimes players may notify metadata when nothing we care about has * changed, so we have this to avoid printing duplicate lines in follow * mode. Prints a newline if output is NULL which denotes that the property has * been cleared. Only use this in follow mode. * * This consumes the output string. */ static void cli_print_output(gchar *output) { if (output == NULL && last_output == NULL) { return; } if (output == NULL) { output = g_strdup("\n"); } if (g_strcmp0(output, last_output) == 0) { g_free(output); return; } printf("%s", output); fflush(stdout); g_free(last_output); last_output = output; } struct playercmd_args { gchar **argv; gint argc; }; /* Arguments given to the player for the follow command */ static struct playercmd_args *playercmd_args = NULL; static struct playercmd_args *playercmd_args_create(gchar **argv, gint argc) { struct playercmd_args *user_data = calloc(1, sizeof(struct playercmd_args)); user_data->argc = argc; user_data->argv = g_strdupv(argv); return user_data; } static void playercmd_args_destroy(struct playercmd_args *data) { if (data == NULL) { return; } g_strfreev(data->argv); free(data); return; } static gchar *get_metadata_formatted(PlayerctlPlayer *player, GError **error) { GError *tmp_error = NULL; GVariant *metadata = NULL; g_return_val_if_fail(formatter != NULL, NULL); g_object_get(player, "metadata", &metadata, NULL); if (metadata == NULL) { return NULL; } if (g_variant_n_children(metadata) == 0) { g_variant_unref(metadata); return NULL; } GVariantDict *context = playerctl_formatter_default_template_context(formatter, player, metadata); gchar *result = playerctl_formatter_expand_format(formatter, context, &tmp_error); if (tmp_error) { g_variant_unref(metadata); g_variant_dict_unref(context); g_propagate_error(error, tmp_error); return NULL; } g_variant_unref(metadata); g_variant_dict_unref(context); return result; } static gboolean playercmd_play(PlayerctlPlayer *player, gchar **argv, gint argc, gchar **output, GError **error) { GError *tmp_error = NULL; gchar *instance = pctl_player_get_instance(player); gboolean can_play = FALSE; g_object_get(player, "can-play", &can_play, NULL); if (!can_play) { g_debug("%s: can-play is false, skipping", instance); return FALSE; } playerctl_player_play(player, &tmp_error); if (tmp_error) { g_propagate_error(error, tmp_error); return FALSE; } return TRUE; } static gboolean playercmd_pause(PlayerctlPlayer *player, gchar **argv, gint argc, gchar **output, GError **error) { GError *tmp_error = NULL; gchar *instance = pctl_player_get_instance(player); gboolean can_pause = FALSE; g_object_get(player, "can-pause", &can_pause, NULL); if (!can_pause) { g_debug("%s: player cannot pause", instance); return FALSE; } playerctl_player_pause(player, &tmp_error); if (tmp_error) { g_propagate_error(error, tmp_error); return FALSE; } return TRUE; } static gboolean playercmd_play_pause(PlayerctlPlayer *player, gchar **argv, gint argc, gchar **output, GError **error) { GError *tmp_error = NULL; gchar *instance = pctl_player_get_instance(player); gboolean can_play = FALSE; g_object_get(player, "can-play", &can_play, NULL); if (!can_play) { g_debug("%s: can-play is false, skipping", instance); return FALSE; } playerctl_player_play_pause(player, &tmp_error); if (tmp_error) { g_propagate_error(error, tmp_error); return FALSE; } return TRUE; } static gboolean playercmd_stop(PlayerctlPlayer *player, gchar **argv, gint argc, gchar **output, GError **error) { GError *tmp_error = NULL; gchar *instance = pctl_player_get_instance(player); // XXX there is no CanStop property on the mpris player. CanPlay is supposed // to indicate whether there is a current track. If there is no current // track, then I assume the player cannot stop. gboolean can_play = FALSE; g_object_get(player, "can-play", &can_play, NULL); if (!can_play) { g_debug("%s: can-play is false, skipping", instance); return FALSE; } playerctl_player_stop(player, &tmp_error); if (tmp_error) { g_propagate_error(error, tmp_error); return FALSE; } return TRUE; } static gboolean playercmd_next(PlayerctlPlayer *player, gchar **argv, gint argc, gchar **output, GError **error) { GError *tmp_error = NULL; gchar *instance = pctl_player_get_instance(player); gboolean can_go_next = FALSE; g_object_get(player, "can-go-next", &can_go_next, NULL); if (!can_go_next) { g_debug("%s: player cannot go next", instance); return FALSE; } playerctl_player_next(player, &tmp_error); if (tmp_error) { g_propagate_error(error, tmp_error); return FALSE; } return TRUE; } static gboolean playercmd_previous(PlayerctlPlayer *player, gchar **argv, gint argc, gchar **output, GError **error) { GError *tmp_error = NULL; gchar *instance = pctl_player_get_instance(player); gboolean can_go_previous = FALSE; g_object_get(player, "can-go-previous", &can_go_previous, NULL); if (!can_go_previous) { g_debug("%s: player cannot go previous", instance); return FALSE; } playerctl_player_previous(player, &tmp_error); if (tmp_error) { g_propagate_error(error, tmp_error); return FALSE; } return TRUE; } static gboolean playercmd_open(PlayerctlPlayer *player, gchar **argv, gint argc, gchar **output, GError **error) { const gchar *uri = argv[1]; GError *tmp_error = NULL; gchar *instance = pctl_player_get_instance(player); gboolean can_control = FALSE; g_object_get(player, "can-control", &can_control, NULL); if (!can_control) { g_debug("%s: player cannot control", instance); return FALSE; } if (uri) { GFile *file = g_file_new_for_commandline_arg(uri); gboolean exists = g_file_query_exists(file, NULL); gchar *full_uri = NULL; if (exists) { // it's a file, so pass the absolute path of the file full_uri = g_file_get_uri(file); } else { // it may be some other scheme, just pass the uri directly full_uri = g_strdup(uri); } playerctl_player_open(player, full_uri, &tmp_error); g_free(full_uri); g_object_unref(file); if (tmp_error != NULL) { g_propagate_error(error, tmp_error); return FALSE; } } return TRUE; } static gboolean playercmd_position(PlayerctlPlayer *player, gchar **argv, gint argc, gchar **output, GError **error) { const gchar *position = argv[1]; gint64 offset; GError *tmp_error = NULL; gchar *instance = pctl_player_get_instance(player); if (position) { if (format_string_arg != NULL) { g_set_error(error, playerctl_cli_error_quark(), 1, "format strings are not supported on command functions."); return FALSE; } char *endptr = NULL; offset = 1000000.0 * strtod(position, &endptr); if (position == endptr) { g_set_error(error, playerctl_cli_error_quark(), 1, "Could not parse position as a number: %s\n", position); return FALSE; } gboolean can_seek = FALSE; g_object_get(player, "can-seek", &can_seek, NULL); if (!can_seek) { return FALSE; } size_t last = strlen(position) - 1; if (position[last] == '+' || position[last] == '-') { if (position[last] == '-') { offset *= -1; } playerctl_player_seek(player, offset, &tmp_error); if (tmp_error != NULL) { g_propagate_error(error, tmp_error); return FALSE; } } else { playerctl_player_set_position(player, offset, &tmp_error); if (tmp_error != NULL) { g_propagate_error(error, tmp_error); return FALSE; } } } else { if (formatter != NULL) { GVariantDict *context = playerctl_formatter_default_template_context(formatter, player, NULL); gchar *formatted = playerctl_formatter_expand_format(formatter, context, &tmp_error); if (tmp_error != NULL) { g_propagate_error(error, tmp_error); g_variant_dict_unref(context); return FALSE; } *output = g_strdup_printf("%s\n", formatted); g_free(formatted); g_variant_dict_unref(context); } else { if (!pctl_player_has_cached_property(player, "Position")) { g_debug("%s: player has no cached position, skipping", instance); return FALSE; } g_object_get(player, "position", &offset, NULL); *output = g_strdup_printf("%f\n", (double)offset / 1000000.0); } } return TRUE; } static gboolean playercmd_volume(PlayerctlPlayer *player, gchar **argv, gint argc, gchar **output, GError **error) { GError *tmp_error = NULL; const gchar *volume = argv[1]; gdouble level; gchar *instance = pctl_player_get_instance(player); if (volume) { if (format_string_arg != NULL) { g_set_error(error, playerctl_cli_error_quark(), 1, "format strings are not supported on command functions."); return FALSE; } char *endptr = NULL; size_t last = strlen(volume) - 1; if (volume[last] == '+' || volume[last] == '-') { gdouble adjustment = strtod(volume, &endptr); if (volume == endptr) { g_set_error(error, playerctl_cli_error_quark(), 1, "could not parse volume as a number: %s\n", volume); return FALSE; } if (volume[last] == '-') { adjustment *= -1; } g_object_get(player, "volume", &level, NULL); level += adjustment; } else { level = strtod(volume, &endptr); if (volume == endptr) { g_set_error(error, playerctl_cli_error_quark(), 1, "could not parse volume as a number: %s\n", volume); return FALSE; } } gboolean can_control = FALSE; g_object_get(player, "can-control", &can_control, NULL); if (!can_control) { g_debug("%s: player cannot control", instance); return FALSE; } g_debug("%s: setting volume to %f\n", instance, level); playerctl_player_set_volume(player, level, &tmp_error); if (tmp_error != NULL) { g_propagate_error(error, tmp_error); return FALSE; } } else { if (!pctl_player_has_cached_property(player, "Volume")) { g_debug("%s: player has no volume set, skipping", instance); return FALSE; } g_object_get(player, "volume", &level, NULL); if (formatter != NULL) { GVariantDict *context = playerctl_formatter_default_template_context(formatter, player, NULL); gchar *formatted = playerctl_formatter_expand_format(formatter, context, &tmp_error); if (tmp_error != NULL) { g_propagate_error(error, tmp_error); return FALSE; } *output = g_strdup_printf("%s\n", formatted); g_free(formatted); } else { *output = g_strdup_printf("%f\n", level); } } return TRUE; } static gboolean playercmd_status(PlayerctlPlayer *player, gchar **argv, gint argc, gchar **output, GError **error) { GError *tmp_error = NULL; gchar *instance = pctl_player_get_instance(player); if (!pctl_player_has_cached_property(player, "PlaybackStatus")) { g_debug("%s: player has no playback status set, skipping", instance); return FALSE; } if (formatter != NULL) { GVariantDict *context = playerctl_formatter_default_template_context(formatter, player, NULL); gchar *formatted = playerctl_formatter_expand_format(formatter, context, &tmp_error); if (tmp_error != NULL) { g_propagate_error(error, tmp_error); g_variant_dict_unref(context); return FALSE; } *output = g_strdup_printf("%s\n", formatted); g_variant_dict_unref(context); g_free(formatted); } else { PlayerctlPlaybackStatus status = 0; g_object_get(player, "playback-status", &status, NULL); const gchar *status_str = pctl_playback_status_to_string(status); assert(status_str != NULL); *output = g_strdup_printf("%s\n", status_str); } return TRUE; } static gboolean playercmd_shuffle(PlayerctlPlayer *player, gchar **argv, gint argc, gchar **output, GError **error) { GError *tmp_error = NULL; gchar *instance = pctl_player_get_instance(player); if (argc > 1) { gchar *status_str = argv[1]; gboolean status = FALSE; if (strcasecmp(status_str, "on") == 0) { status = TRUE; } else if (strcasecmp(status_str, "off") == 0) { status = FALSE; } else if (strcasecmp(status_str, "toggle") == 0) { g_object_get(player, "shuffle", &status, NULL); status = !status; } else { g_set_error(error, playerctl_cli_error_quark(), 1, "Got unknown shuffle status: '%s' (expected 'on', " "'off', or 'toggle').", argv[1]); return FALSE; } gboolean can_control = FALSE; g_object_get(player, "can-control", &can_control, NULL); if (!can_control) { g_debug("%s: player cannot control, not setting shuffle", instance); return FALSE; } playerctl_player_set_shuffle(player, status, &tmp_error); if (tmp_error != NULL) { g_propagate_error(error, tmp_error); return FALSE; } g_debug("%s: setting shuffle to %d\n", instance, status); } else { if (!pctl_player_has_cached_property(player, "Shuffle")) { g_debug("%s: player has no shuffle status set, skipping", instance); return FALSE; } if (formatter != NULL) { GVariantDict *context = playerctl_formatter_default_template_context(formatter, player, NULL); gchar *formatted = playerctl_formatter_expand_format(formatter, context, &tmp_error); if (tmp_error != NULL) { g_propagate_error(error, tmp_error); g_variant_dict_unref(context); return FALSE; } *output = g_strdup_printf("%s\n", formatted); g_variant_dict_unref(context); g_free(formatted); } else { gboolean status = FALSE; g_object_get(player, "shuffle", &status, NULL); if (status) { *output = g_strdup("On\n"); } else { *output = g_strdup("Off\n"); } } } return TRUE; } static gboolean playercmd_loop(PlayerctlPlayer *player, gchar **argv, gint argc, gchar **output, GError **error) { GError *tmp_error = NULL; gchar *instance = pctl_player_get_instance(player); if (argc > 1) { gchar *status_str = argv[1]; PlayerctlLoopStatus status = 0; if (!pctl_parse_loop_status(status_str, &status)) { g_set_error(error, playerctl_cli_error_quark(), 1, "Got unknown loop status: '%s' (expected 'none', " "'playlist', or 'track').", argv[1]); return FALSE; } gboolean can_control = FALSE; g_object_get(player, "can-control", &can_control, NULL); if (!can_control) { g_debug("%s: player cannot control", instance); return FALSE; } g_debug("%s: setting loop status to %d\n", instance, status); playerctl_player_set_loop_status(player, status, &tmp_error); if (tmp_error != NULL) { g_propagate_error(error, tmp_error); return FALSE; } } else { if (formatter != NULL) { GVariantDict *context = playerctl_formatter_default_template_context(formatter, player, NULL); gchar *formatted = playerctl_formatter_expand_format(formatter, context, &tmp_error); if (tmp_error != NULL) { g_propagate_error(error, tmp_error); g_variant_dict_unref(context); return FALSE; } *output = g_strdup_printf("%s\n", formatted); g_variant_dict_unref(context); g_free(formatted); } else { if (!pctl_player_has_cached_property(player, "LoopStatus")) { g_debug("%s: player has no cached loop status, skipping", instance); return FALSE; } PlayerctlLoopStatus status = 0; g_object_get(player, "loop-status", &status, NULL); const gchar *status_str = pctl_loop_status_to_string(status); assert(status_str != NULL); *output = g_strdup_printf("%s\n", status_str); } } return TRUE; } static gboolean playercmd_metadata(PlayerctlPlayer *player, gchar **argv, gint argc, gchar **output, GError **error) { g_debug("metadata command for player: %s", pctl_player_get_instance(player)); GError *tmp_error = NULL; gchar *instance = pctl_player_get_instance(player); gboolean can_play = FALSE; g_object_get(player, "can-play", &can_play, NULL); if (!can_play) { // XXX: This is gotten from the property cache which may not be up to // date in all cases. If there is a bug with a player not printing // metadata, look here. g_debug("%s: can-play is false, skipping", instance); return FALSE; } if (format_string_arg != NULL) { gchar *data = get_metadata_formatted(player, &tmp_error); if (tmp_error) { g_propagate_error(error, tmp_error); return FALSE; } if (data != NULL) { *output = g_strdup_printf("%s\n", data); g_free(data); } else { g_debug("%s: no metadata, skipping", instance); return FALSE; } } else if (argc == 1) { gchar *data = playerctl_player_print_metadata_prop(player, NULL, &tmp_error); if (tmp_error) { g_propagate_error(error, tmp_error); return FALSE; } if (data != NULL) { *output = g_strdup_printf("%s\n", data); g_free(data); } else { return FALSE; } } else { for (int i = 1; i < argc; ++i) { const gchar *type = argv[i]; gchar *data; if (g_strcmp0(type, "artist") == 0) { data = playerctl_player_get_artist(player, &tmp_error); } else if (g_strcmp0(type, "title") == 0) { data = playerctl_player_get_title(player, &tmp_error); } else if (g_strcmp0(type, "album") == 0) { data = playerctl_player_get_album(player, &tmp_error); } else { data = playerctl_player_print_metadata_prop(player, type, &tmp_error); } if (tmp_error) { g_propagate_error(error, tmp_error); return FALSE; } if (data != NULL) { *output = g_strdup_printf("%s\n", data); g_free(data); } else { return FALSE; } } } return TRUE; } 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); } static gboolean playercmd_tick_callback(gpointer data) { GError *tmp_error = NULL; managed_players_execute_command(&tmp_error); if (tmp_error != NULL) { exit_status = 1; g_printerr("Error while executing command: %s\n", tmp_error->message); g_clear_error(&tmp_error); g_main_loop_quit(main_loop); return FALSE; } return TRUE; } struct player_command { const gchar *name; gboolean (*func)(PlayerctlPlayer *player, gchar **argv, gint argc, gchar **output, GError **error); gboolean supports_format; const gchar *follow_signal; } player_commands[] = { {"open", &playercmd_open, FALSE, NULL}, {"play", &playercmd_play, FALSE, NULL}, {"pause", &playercmd_pause, FALSE, NULL}, {"play-pause", &playercmd_play_pause, FALSE, NULL}, {"stop", &playercmd_stop, FALSE, NULL}, {"next", &playercmd_next, FALSE, NULL}, {"previous", &playercmd_previous, FALSE, NULL}, {"position", &playercmd_position, TRUE, "seeked"}, {"volume", &playercmd_volume, TRUE, "volume"}, {"status", &playercmd_status, TRUE, "playback-status"}, {"loop", &playercmd_loop, TRUE, "loop-status"}, {"shuffle", &playercmd_shuffle, TRUE, "shuffle"}, {"metadata", &playercmd_metadata, TRUE, "metadata"}, }; static const struct player_command *get_player_command(gchar **argv, gint argc, GError **error) { for (gsize i = 0; i < LENGTH(player_commands); ++i) { const struct player_command command = player_commands[i]; if (g_strcmp0(command.name, argv[0]) == 0) { if (format_string_arg != NULL && !command.supports_format) { g_set_error(error, playerctl_cli_error_quark(), 1, "format strings are not supported on command: %s", argv[0]); return NULL; } if (follow && (command.follow_signal == NULL)) { g_set_error(error, playerctl_cli_error_quark(), 1, "follow is not supported on command: %s", argv[0]); return NULL; } return &player_commands[i]; } } g_set_error(error, playerctl_cli_error_quark(), 1, "Command not recognized: %s", argv[0]); return NULL; } static const GOptionEntry entries[] = { {"player", 'p', G_OPTION_FLAG_NONE, G_OPTION_ARG_STRING, &player_arg, "A comma separated list of names of players to control (default: the " "first available player)", "NAME"}, {"all-players", 'a', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &select_all_players, "Select all available players to be controlled", NULL}, {"ignore-player", 'i', G_OPTION_FLAG_NONE, G_OPTION_ARG_STRING, &ignore_player_arg, "A comma separated list of names of players to ignore.", "IGNORE"}, {"format", 'f', G_OPTION_FLAG_NONE, G_OPTION_ARG_STRING, &format_string_arg, "A format string for printing properties and metadata", NULL}, {"follow", 'F', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &follow, "Block and append the query to output when it changes for the most recently updated player.", NULL}, {"list-all", 'l', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &list_all_players_and_exit, "List the names of running players that can be controlled", NULL}, {"no-messages", 's', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &no_status_error_messages, "Suppress diagnostic messages", NULL}, {"version", 'v', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &print_version_and_exit, "Print version information", NULL}, {G_OPTION_REMAINING, 0, 0, G_OPTION_ARG_STRING_ARRAY, &command_arg, NULL, "COMMAND"}, {NULL}}; static gboolean parse_setup_options(int argc, char *argv[], GError **error) { static const gchar *description = "Available Commands:" "\n play Command the player to play" "\n pause Command the player to pause" "\n play-pause Command the player to toggle between " "play/pause" "\n stop Command the player to stop" "\n next Command the player to skip to the next track" "\n previous Command the player to skip to the previous " "track" "\n position [OFFSET][+/-] Command the player to go to the position or " "seek forward/backward OFFSET in seconds" "\n volume [LEVEL][+/-] Print or set the volume to LEVEL from 0.0 " "to 1.0" "\n status Get the play status of the player" "\n metadata [KEY...] Print metadata information for the current " "track. If KEY is passed," "\n print only those values. KEY may be artist," "title, album, or any key found in the metadata." "\n open [URI] Command for the player to open given URI." "\n URI can be either file path or remote URL." "\n loop [STATUS] Print or set the loop status." "\n Can be \"None\", \"Track\", or \"Playlist\"." "\n shuffle [STATUS] Print or set the shuffle status." "\n Can be \"On\", \"Off\", or \"Toggle\"."; static const gchar *summary = " For players supporting the MPRIS D-Bus specification"; GOptionContext *context = NULL; gboolean success; context = g_option_context_new("- Controller for media players"); g_option_context_add_main_entries(context, entries, NULL); g_option_context_set_description(context, description); g_option_context_set_summary(context, summary); success = g_option_context_parse(context, &argc, &argv, error); if (!success) { g_option_context_free(context); return FALSE; } if (command_arg == NULL && !print_version_and_exit && !list_all_players_and_exit) { gchar *help = g_option_context_get_help(context, TRUE, NULL); printf("%s\n", help); g_option_context_free(context); g_free(help); exit(1); } g_option_context_free(context); return TRUE; } static GList *parse_player_list(gchar *player_list_arg) { GList *players = NULL; if (player_list_arg == NULL) { return NULL; } const gchar *delim = ","; gchar *token = strtok(player_list_arg, delim); while (token != NULL) { players = g_list_append(players, g_strdup(g_strstrip(token))); token = strtok(NULL, ","); } 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; } 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; } gboolean one_selected = FALSE; GList *l = NULL; for (l = player_names_list; l != NULL; l = l->next) { PlayerctlPlayerName *name = l->data; 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 found\n"); } pctl_player_name_list_destroy(player_names_list); return 0; } static void managed_players_execute_command(GError **error) { GError *tmp_error = NULL; const struct player_command *player_cmd = get_player_command(playercmd_args->argv, playercmd_args->argc, &tmp_error); if (tmp_error != NULL) { g_propagate_error(error, tmp_error); return; } g_debug("executing command: %s", player_cmd->name); assert(player_cmd->func != NULL); gboolean did_command = FALSE; GList *players = NULL; g_object_get(manager, "players", &players, NULL); GList *l = NULL; for (l = players; l != NULL; l = l->next) { PlayerctlPlayer *player = PLAYERCTL_PLAYER(l->data); assert(player != NULL); gchar *output = NULL; gboolean result = player_cmd->func(player, playercmd_args->argv, playercmd_args->argc, &output, &tmp_error); if (tmp_error != NULL) { g_propagate_error(error, tmp_error); g_free(output); return; } if (output != NULL) { cli_print_output(output); } did_command = did_command || result; if (result) { break; } } if (!did_command) { cli_print_output(NULL); } } static void name_appeared_callback(PlayerctlPlayerManager *manager, PlayerctlPlayerName *name, gpointer *data) { if (!name_is_selected(name->instance)) { return; } g_debug("a selected name appeared: %s (source=%d)", name->instance, name->source); // make sure we are not managing the player already GList *players = NULL; g_object_get(manager, "players", &players, NULL); for (GList *l = players; l != NULL; l = l->next) { PlayerctlPlayer *player = PLAYERCTL_PLAYER(l->data); gchar *instance = pctl_player_get_instance(player); PlayerctlSource source = 0; g_object_get(player, "source", &source, NULL); if (source == name->source && g_strcmp0(instance, name->instance) == 0) { g_debug("this player is already managed: %s (source=%d)", name->instance, name->source); return; } } GError *error = NULL; PlayerctlPlayer *player = playerctl_player_new_from_name(name, &error); if (error != NULL) { exit_status = 1; g_printerr("Could not connect to player: %s\n", error->message); g_clear_error(&error); g_main_loop_quit(main_loop); return; } playerctl_player_manager_manage_player(manager, player); g_object_unref(player); } static void init_managed_player(PlayerctlPlayer *player, const struct player_command *player_cmd) { assert(player_cmd->follow_signal != NULL); g_signal_connect(G_OBJECT(player), player_cmd->follow_signal, G_CALLBACK(managed_player_properties_callback), playercmd_args); if (formatter != NULL) { for (gsize i = 0; i < LENGTH(player_commands); ++i) { const struct player_command cmd = player_commands[i]; if (&cmd != player_cmd && cmd.follow_signal != NULL && g_strcmp0(cmd.name, "metadata") != 0 && playerctl_formatter_contains_key(formatter, cmd.name)) { g_signal_connect(G_OBJECT(player), cmd.follow_signal, G_CALLBACK(managed_player_properties_callback), playercmd_args); } } } } static void player_appeared_callback(PlayerctlPlayerManager *manager, PlayerctlPlayer *player, gpointer *data) { GError *error = NULL; const struct player_command *player_cmd = get_player_command(playercmd_args->argv, playercmd_args->argc, &error); if (error != NULL) { exit_status = 1; g_printerr("Could not get player command: %s\n", error->message); g_clear_error(&error); g_main_loop_quit(main_loop); return; } init_managed_player(player, player_cmd); managed_players_execute_command(&error); if (error != NULL) { exit_status = 1; g_printerr("Could not execute command: %s\n", error->message); g_clear_error(&error); g_main_loop_quit(main_loop); return; } } static void player_vanished_callback(PlayerctlPlayerManager *manager, PlayerctlPlayer *player, gpointer *data) { GError *error = NULL; managed_players_execute_command(&error); if (error != NULL) { exit_status = 1; g_printerr("Could not execute command: %s\n", error->message); g_clear_error(&error); g_main_loop_quit(main_loop); return; } } 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_match_index = -1; int b_match_index = -1; int any_index = INT_MAX; int i = 0; GList *l = NULL; 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; } ++i; continue; } if (pctl_player_name_string_instance_compare(name, name_a) == 0) { if (a_match_index == -1) { a_match_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_match_index == -1 && b_match_index == -1) { // neither are in the list return 0; } else if (a_match_index == -1) { // b is in the list return (b_match_index < any_index ? 1 : -1); } else if (b_match_index == -1) { // a is in the list return (a_match_index < any_index ? -1 : 1); } else if (a_match_index == b_match_index) { // preserve order return 0; } else { return (a_match_index < b_match_index ? -1 : 1); } } 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, user_data); } 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, user_data); g_free(name_a); g_free(name_b); return result; } int main(int argc, char *argv[]) { g_debug("playerctl version %s", PLAYERCTL_VERSION_S); GError *error = NULL; guint num_commands = 0; GList *available_players = NULL; // seems to be required to print unicode (see #8) setlocale(LC_CTYPE, ""); if (!parse_setup_options(argc, argv, &error)) { g_printerr("%s\n", error->message); g_clear_error(&error); 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); } if (list_all_players_and_exit) { int result = handle_list_all_flag(); exit(result); } num_commands = g_strv_length(command_arg); const struct player_command *player_cmd = get_player_command(command_arg, num_commands, &error); if (error != NULL) { g_printerr("Could not execute command: %s\n", error->message); g_clear_error(&error); exit(1); } if (format_string_arg != NULL) { formatter = playerctl_formatter_new(format_string_arg, &error); if (error != NULL) { g_printerr("Could not execute command: %s\n", error->message); g_clear_error(&error); exit(1); } } playercmd_args = playercmd_args_create(command_arg, num_commands); manager = playerctl_player_manager_new(&error); if (error != NULL) { g_printerr("Could not connect to players: %s\n", error->message); exit_status = 1; goto end; } if (player_names != NULL && !select_all_players) { 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_with_data(available_players, player_name_compare_func, (gpointer)player_names); PlayerctlPlayerName playerctld_name = { .instance = "playerctld", .source = PLAYERCTL_SOURCE_DBUS_SESSION, }; if (name_is_selected("playerctld") && (g_list_find_custom(player_names, "playerctld", (GCompareFunc)g_strcmp0)) != NULL && (g_list_find_custom(available_players, &playerctld_name, (GCompareFunc)pctl_player_name_compare) == NULL)) { // 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 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_with_data(available_players, player_name_compare_func, (gpointer)player_names); } gboolean has_selected = FALSE; gboolean did_command = FALSE; GList *l = NULL; for (l = available_players; l != NULL; l = l->next) { PlayerctlPlayerName *name = l->data; g_debug("found player: %s", name->instance); if (!name_is_selected(name->instance)) { continue; } has_selected = TRUE; PlayerctlPlayer *player = playerctl_player_new_from_name(name, &error); if (error != NULL) { g_printerr("Could not connect to player: %s\n", error->message); exit_status = 1; goto end; } if (follow) { playerctl_player_manager_manage_player(manager, player); init_managed_player(player, player_cmd); } else { gchar *output = NULL; g_debug("executing command %s", player_cmd->name); gboolean result = player_cmd->func(player, command_arg, num_commands, &output, &error); if (error != NULL) { g_printerr("Could not execute command: %s\n", error->message); exit_status = 1; g_object_unref(player); goto end; } if (result) { did_command = TRUE; if (output != NULL) { printf("%s", output); fflush(stdout); g_free(output); } if (!select_all_players) { g_object_unref(player); goto end; } } } g_object_unref(player); } if (!follow) { if (!has_selected) { if (!no_status_error_messages) { g_printerr("No players found\n"); } exit_status = 1; goto end; } else if (!did_command) { if (!no_status_error_messages) { g_printerr("No player could handle this command\n"); } exit_status = 1; goto end; } } else { managed_players_execute_command(&error); if (error != NULL) { g_printerr("Connection to player failed: %s\n", error->message); exit_status = 1; goto end; } g_signal_connect(PLAYERCTL_PLAYER_MANAGER(manager), "name-appeared", G_CALLBACK(name_appeared_callback), NULL); g_signal_connect(PLAYERCTL_PLAYER_MANAGER(manager), "player-appeared", G_CALLBACK(player_appeared_callback), NULL); g_signal_connect(PLAYERCTL_PLAYER_MANAGER(manager), "player-vanished", G_CALLBACK(player_vanished_callback), NULL); if (formatter != NULL && playerctl_formatter_contains_key(formatter, "position")) { g_timeout_add(1000, playercmd_tick_callback, NULL); } main_loop = g_main_loop_new(NULL, FALSE); g_main_loop_run(main_loop); g_main_loop_unref(main_loop); } end: if (available_players != NULL) { g_list_free(available_players); } playercmd_args_destroy(playercmd_args); if (manager != NULL) { g_object_unref(manager); } playerctl_formatter_destroy(formatter); g_free(last_output); g_list_free_full(player_names, g_free); g_list_free_full(ignored_player_names, g_free); exit(exit_status); } playerctl-2.4.1/playerctl/playerctl-common.c000066400000000000000000000240541412234731200211510ustar00rootroot00000000000000/* * This file is part of playerctl. * * playerctl is free software: you can redistribute it and/or modify it under * the terms of the GNU Lesser General Public License as published by the Free * Software Foundation, either version 3 of the License, or (at your option) * any later version. * * playerctl is distributed in the hope that it will be useful, but WITHOUT ANY * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for * more details. * * You should have received a copy of the GNU Lesser General Public License * along with playerctl If not, see . * * Copyright © 2014, Tony Crisci and contributors. */ #include "playerctl-common.h" #include #include #include #include #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; } if (strcasecmp(status_str, "Playing") == 0) { *status = PLAYERCTL_PLAYBACK_STATUS_PLAYING; return TRUE; } else if (strcasecmp(status_str, "Paused") == 0) { *status = PLAYERCTL_PLAYBACK_STATUS_PAUSED; return TRUE; } else if (strcasecmp(status_str, "Stopped") == 0) { *status = PLAYERCTL_PLAYBACK_STATUS_STOPPED; return TRUE; } return FALSE; } const gchar *pctl_playback_status_to_string(PlayerctlPlaybackStatus status) { switch (status) { case PLAYERCTL_PLAYBACK_STATUS_PLAYING: return "Playing"; case PLAYERCTL_PLAYBACK_STATUS_PAUSED: return "Paused"; case PLAYERCTL_PLAYBACK_STATUS_STOPPED: return "Stopped"; } return NULL; } const gchar *pctl_loop_status_to_string(PlayerctlLoopStatus status) { switch (status) { case PLAYERCTL_LOOP_STATUS_NONE: return "None"; case PLAYERCTL_LOOP_STATUS_TRACK: return "Track"; case PLAYERCTL_LOOP_STATUS_PLAYLIST: return "Playlist"; } return NULL; } gboolean pctl_parse_loop_status(const gchar *status_str, PlayerctlLoopStatus *status) { if (status_str == NULL) { return FALSE; } if (strcasecmp(status_str, "None") == 0) { *status = PLAYERCTL_LOOP_STATUS_NONE; return TRUE; } else if (strcasecmp(status_str, "Track") == 0) { *status = PLAYERCTL_LOOP_STATUS_TRACK; return TRUE; } else if (strcasecmp(status_str, "Playlist") == 0) { *status = PLAYERCTL_LOOP_STATUS_PLAYLIST; return TRUE; } return FALSE; } gchar *pctl_print_gvariant(GVariant *value) { GString *printed = g_string_new(""); if (g_variant_is_of_type(value, G_VARIANT_TYPE_STRING_ARRAY)) { gsize prop_count; const gchar **prop_strv = g_variant_get_strv(value, &prop_count); for (gsize i = 0; i < prop_count; i += 1) { g_string_append(printed, prop_strv[i]); if (i != prop_count - 1) { g_string_append(printed, ", "); } } g_free(prop_strv); } else if (g_variant_is_of_type(value, G_VARIANT_TYPE_STRING)) { g_string_append(printed, g_variant_get_string(value, NULL)); } else { printed = g_variant_print_string(value, printed, FALSE); } return g_string_free(printed, FALSE); } GBusType pctl_source_to_bus_type(PlayerctlSource source) { switch (source) { case PLAYERCTL_SOURCE_DBUS_SESSION: return G_BUS_TYPE_SESSION; case PLAYERCTL_SOURCE_DBUS_SYSTEM: return G_BUS_TYPE_SYSTEM; default: return G_BUS_TYPE_NONE; } } PlayerctlSource pctl_bus_type_to_source(GBusType bus_type) { switch (bus_type) { case G_BUS_TYPE_SESSION: return PLAYERCTL_SOURCE_DBUS_SESSION; case G_BUS_TYPE_SYSTEM: return PLAYERCTL_SOURCE_DBUS_SYSTEM; default: g_warning("could not convert bus type to source: %d\n", bus_type); return PLAYERCTL_SOURCE_NONE; } } PlayerctlPlayerName *pctl_player_name_new(const gchar *instance, PlayerctlSource source) { PlayerctlPlayerName *player_name = g_slice_new(PlayerctlPlayerName); gchar **split = g_strsplit(instance, ".", 2); player_name->name = g_strdup(split[0]); g_strfreev(split); player_name->instance = g_strdup(instance); player_name->source = source; return player_name; } gint pctl_player_name_compare(PlayerctlPlayerName *name_a, PlayerctlPlayerName *name_b) { if (name_a->source != name_b->source) { return 1; } return g_strcmp0(name_a->instance, name_b->instance); } gint pctl_player_name_instance_compare(PlayerctlPlayerName *name, PlayerctlPlayerName *instance) { if (name->source != instance->source) { return 1; } return pctl_player_name_string_instance_compare(name->instance, instance->instance); } gint pctl_player_name_string_instance_compare(const gchar *name, const gchar *instance) { if (g_strcmp0(name, "%any") == 0 || g_strcmp0(instance, "%any") == 0) { return 0; } gboolean exact_match = (g_strcmp0(name, instance) == 0); 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; } else { return 1; } } GList *pctl_player_name_find(GList *list, gchar *player_id, PlayerctlSource source) { PlayerctlPlayerName player_name = { .instance = player_id, .source = source, }; return g_list_find_custom(list, &player_name, (GCompareFunc)pctl_player_name_compare); } GList *pctl_player_name_find_instance(GList *list, gchar *player_id, PlayerctlSource source) { PlayerctlPlayerName player_name = { .instance = player_id, .source = source, }; return g_list_find_custom(list, &player_name, (GCompareFunc)pctl_player_name_instance_compare); } 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", "org.freedesktop.DBus", NULL, &tmp_error); if (tmp_error != NULL) { if (tmp_error->domain == G_IO_ERROR && tmp_error->code == G_IO_ERROR_NOT_FOUND) { // XXX: This means the dbus socket address is not found which may // mean that the bus is not running or the address was set // incorrectly. I think we can pass through here because it is true // that there are no names on the bus that is supposed to be at // this socket path. But we need a better way of dealing with this case. const gchar *message = "D-Bus socket address not found, unable to list player names"; if (bus_type == G_BUS_TYPE_SESSION) { g_warning("%s", message); } else { g_debug("%s", message); } g_clear_error(&tmp_error); return NULL; } g_propagate_error(err, tmp_error); return NULL; } g_debug("Getting list of player names from D-Bus"); GVariant *reply = g_dbus_proxy_call_sync(proxy, "ListNames", NULL, G_DBUS_CALL_FLAGS_NONE, -1, NULL, &tmp_error); if (tmp_error != NULL) { g_propagate_error(err, tmp_error); g_object_unref(proxy); return NULL; } GVariant *reply_child = g_variant_get_child_value(reply, 0); 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)) { PlayerctlPlayerName *player_name = pctl_player_name_new(names[i] + offset, pctl_bus_type_to_source(bus_type)); players = g_list_append(players, player_name); } } 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); g_free(names); return players; } playerctl-2.4.1/playerctl/playerctl-common.h000066400000000000000000000043161412234731200211550ustar00rootroot00000000000000/* * This file is part of playerctl. * * playerctl is free software: you can redistribute it and/or modify it under * the terms of the GNU Lesser General Public License as published by the Free * Software Foundation, either version 3 of the License, or (at your option) * any later version. * * playerctl is distributed in the hope that it will be useful, but WITHOUT ANY * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for * more details. * * You should have received a copy of the GNU Lesser General Public License * along with playerctl If not, see . * * Copyright © 2014, Tony Crisci and contributors */ #ifndef __PLAYERCTL_COMMON_H__ #define __PLAYERCTL_COMMON_H__ #include #include #include #include #define MPRIS_PREFIX "org.mpris.MediaPlayer2." gboolean pctl_parse_playback_status(const gchar *playback_status, PlayerctlPlaybackStatus *status); const gchar *pctl_playback_status_to_string(PlayerctlPlaybackStatus status); gboolean pctl_parse_loop_status(const gchar *loop_status, PlayerctlLoopStatus *status); const gchar *pctl_loop_status_to_string(PlayerctlLoopStatus status); gchar *pctl_print_gvariant(GVariant *value); GBusType pctl_source_to_bus_type(PlayerctlSource source); PlayerctlSource pctl_bus_type_to_source(GBusType bus_type); PlayerctlPlayerName *pctl_player_name_new(const gchar *name, PlayerctlSource source); gint pctl_player_name_compare(PlayerctlPlayerName *name_a, PlayerctlPlayerName *name_b); gint pctl_player_name_instance_compare(PlayerctlPlayerName *name, PlayerctlPlayerName *instance); gint pctl_player_name_string_instance_compare(const gchar *name, const gchar *instance); GList *pctl_player_name_find(GList *list, gchar *player_id, PlayerctlSource source); GList *pctl_player_name_find_instance(GList *list, gchar *player_id, PlayerctlSource source); void pctl_player_name_list_destroy(GList *list); GList *pctl_list_player_names_on_bus(GBusType bus_type, GError **err); bool pctl_player_has_cached_property(PlayerctlPlayer *player, const gchar *name); #undef __PLAYERCTL_COMMON_H__ #endif playerctl-2.4.1/playerctl/playerctl-daemon.c000066400000000000000000001764341412234731200211360ustar00rootroot00000000000000#include #include #include #include #define MPRIS_PATH "/org/mpris/MediaPlayer2" #define DBUS_NAME "org.freedesktop.DBus" #define DBUS_INTERFACE "org.freedesktop.DBus" #define DBUS_PATH "/org/freedesktop/DBus" #define PLAYER_INTERFACE "org.mpris.MediaPlayer2.Player" #define ROOT_INTERFACE "org.mpris.MediaPlayer2" #define PLAYLISTS_INTERFACE "org.mpris.MediaPlayer2.Playlists" #define TRACKLIST_INTERFACE "org.mpris.MediaPlayer2.TrackList" #define PROPERTIES_INTERFACE "org.freedesktop.DBus.Properties" #define PLAYERCTLD_INTERFACE "com.github.altdesktop.playerctld" /** * A representation of an MPRIS player and its cached MPRIS properties */ struct Player { char *unique; char *well_known; gint64 position; GVariant *player_properties; GVariant *root_properties; // org.mpris.MediaPlayer2.TrackList and org.mpris.MediaPlayer2.Playlists are optional struct { bool supported; GVariant *properties; } tracklist; struct { bool supported; GVariant *properties; } playlists; }; struct PlayerctldContext { GMainLoop *loop; gint bus_id; gchar *bus_address; GDBusConnection *connection; GDBusInterfaceInfo *root_interface_info; GDBusInterfaceInfo *player_interface_info; GDBusInterfaceInfo *playlists_interface_info; GDBusInterfaceInfo *tracklist_interface_info; GDBusInterfaceInfo *playerctld_interface_info; GQueue *players; GQueue *pending_players; gint return_code; struct Player *pending_active; }; /** * Allocate and create a new player, with the specified connection name and well-known bus name */ static struct Player *player_new(const char *unique, const char *well_known) { struct Player *player = calloc(1, sizeof(struct Player)); player->unique = g_strdup(unique); player->well_known = g_strdup(well_known); // Explicitly initialize everything else - just in case player->position = 0; player->player_properties = NULL; player->root_properties = NULL; player->tracklist.supported = false; player->tracklist.properties = NULL; player->playlists.supported = false; player->playlists.properties = NULL; return player; } static void player_set_unique_name(struct Player *player, const char *unique) { g_free(player->unique); player->unique = g_strdup(unique); } static void player_free(struct Player *name) { if (name == NULL) { return; } if (name->player_properties != NULL) { g_variant_unref(name->player_properties); } if (name->root_properties != NULL) { g_variant_unref(name->root_properties); } if (name->tracklist.properties != NULL) { g_variant_unref(name->tracklist.properties); } if (name->playlists.properties != NULL) { g_variant_unref(name->playlists.properties); } g_free(name->unique); g_free(name->well_known); free(name); } static gint player_compare(gconstpointer a, gconstpointer b) { struct Player *fn_a = (struct Player *)a; struct Player *fn_b = (struct Player *)b; if (fn_a->unique != NULL && fn_b->unique != NULL && strcmp(fn_a->unique, fn_b->unique) != 0) { return 1; } if (fn_a->well_known != NULL && fn_b->well_known != NULL && strcmp(fn_a->well_known, fn_b->well_known) != 0) { return 1; } return 0; } /* * Updates the properties for the player. Returns TRUE if the properties have * changed, or else FALSE. */ static gboolean player_update_properties(struct Player *player, const char *interface_name, GVariant *properties) { gboolean changed = FALSE; GVariantDict cached_properties; GVariantIter iter; GVariant *child; enum MprisInterface { PLAYER, TRACKLIST, PLAYLISTS, ROOT } interface; if (g_strcmp0(interface_name, PLAYER_INTERFACE) == 0) { interface = PLAYER; g_variant_dict_init(&cached_properties, player->player_properties); } else if (g_strcmp0(interface_name, TRACKLIST_INTERFACE) == 0) { interface = TRACKLIST; // FIXME: new value of Tracks property is not sent in PropertiesChanged signal // We may want to listen for TrackAdded, TrackRemoved and TrackListReplaced if (!player->playlists.supported) { g_warning("Player %s doesn't appear to support interface %s, but sent " "PropertiesChanged regarding its properties.", player->well_known, interface_name); } g_variant_dict_init(&cached_properties, player->tracklist.properties); } else if (g_strcmp0(interface_name, PLAYLISTS_INTERFACE) == 0) { interface = PLAYLISTS; if (!player->playlists.supported) { g_warning("Player %s doesn't appear to support interface %s, but sent " "PropertiesChanged regarding its properties.", player->well_known, interface_name); } g_variant_dict_init(&cached_properties, player->playlists.properties); } else if (g_strcmp0(interface_name, ROOT_INTERFACE) == 0) { interface = ROOT; g_variant_dict_init(&cached_properties, player->root_properties); } else { g_error("cannot update properties for unknown interface: %s", interface_name); assert(false); } if (!g_variant_is_of_type(properties, G_VARIANT_TYPE_VARDICT)) { g_error("cannot update properties with unknown type: %s", g_variant_get_type_string(properties)); assert(false); } g_variant_iter_init(&iter, properties); while ((child = g_variant_iter_next_value(&iter))) { GVariant *key_variant = g_variant_get_child_value(child, 0); const gchar *key = g_variant_get_string(key_variant, 0); GVariant *prop_variant = g_variant_get_child_value(child, 1); GVariant *prop_value = g_variant_get_variant(prop_variant); // g_debug("key=%s, value=%s", key, g_variant_print(prop_value, TRUE)); if (interface == PLAYER && g_strcmp0(key, "Position") == 0) { // gets cached separately (never counts as changed) player->position = g_variant_get_int64(prop_value); goto loop_out; } GVariant *cache_value = g_variant_dict_lookup_value(&cached_properties, key, NULL); if (cache_value != NULL) { if (!g_variant_equal(cache_value, prop_value)) { g_debug("%s: changed property '%s.%s'", player->well_known, interface_name, key); // g_debug("old = %s, new = %s", g_variant_print(cache_value, FALSE), changed = TRUE; } g_variant_unref(cache_value); } else { g_debug("%s: new property '%s.%s'", player->well_known, interface_name, key); changed = TRUE; } g_variant_dict_insert_value(&cached_properties, key, prop_value); loop_out: g_variant_unref(prop_value); g_variant_unref(prop_variant); g_variant_unref(key_variant); g_variant_unref(child); } switch (interface) { case PLAYER: if (player->player_properties != NULL) { g_variant_unref(player->player_properties); } player->player_properties = g_variant_ref_sink(g_variant_dict_end(&cached_properties)); break; case TRACKLIST: if (player->tracklist.properties != NULL) { g_variant_unref(player->tracklist.properties); } player->tracklist.properties = g_variant_ref_sink(g_variant_dict_end(&cached_properties)); break; case PLAYLISTS: if (player->playlists.properties != NULL) { g_variant_unref(player->playlists.properties); } player->playlists.properties = g_variant_ref_sink(g_variant_dict_end(&cached_properties)); break; case ROOT: if (player->root_properties != NULL) { g_variant_unref(player->root_properties); } player->root_properties = g_variant_ref_sink(g_variant_dict_end(&cached_properties)); break; } return changed; } static void player_update_position_sync(struct Player *player, struct PlayerctldContext *ctx, GError **error) { GError *tmp_error = NULL; g_return_if_fail(error == NULL || *error == NULL); g_debug("updating position for player unique='%s', well_known='%s'", player->unique, player->well_known); GVariant *reply = g_dbus_connection_call_sync( ctx->connection, player->unique, MPRIS_PATH, PROPERTIES_INTERFACE, "Get", g_variant_new("(ss)", PLAYER_INTERFACE, "Position"), NULL, G_DBUS_CALL_FLAGS_NO_AUTO_START, -1, NULL, &tmp_error); if (tmp_error != NULL) { g_propagate_error(error, tmp_error); return; } GVariant *position_unwrapped = g_variant_get_child_value(reply, 0); GVariant *position_variant = g_variant_get_variant(position_unwrapped); player->position = g_variant_get_int64(position_variant); g_debug("new position: %ld", player->position); g_variant_unref(position_variant); g_variant_unref(position_unwrapped); g_variant_unref(reply); } static GVariant *context_player_names_to_gvariant(struct PlayerctldContext *ctx) { GVariantBuilder builder; g_variant_builder_init(&builder, G_VARIANT_TYPE_STRING_ARRAY); guint len = g_queue_get_length(ctx->players); for (int i = 0; i < len; ++i) { struct Player *current = g_queue_pop_head(ctx->players); g_variant_builder_add_value(&builder, g_variant_new_string(current->well_known)); g_queue_push_tail(ctx->players, current); } return g_variant_builder_end(&builder); } static void context_emit_active_player_changed(struct PlayerctldContext *ctx, GError **error) { GError *tmp_error = NULL; g_return_if_fail(error == NULL || *error == NULL); struct Player *player = g_queue_peek_head(ctx->players); g_dbus_connection_emit_signal( ctx->connection, NULL, MPRIS_PATH, PLAYERCTLD_INTERFACE, "ActivePlayerChangeBegin", g_variant_new("(s)", (player != NULL ? player->well_known : "")), &tmp_error); if (tmp_error != NULL) { g_propagate_error(error, tmp_error); return; } if (player != NULL) { g_debug("emitting signals for new active player: '%s'", player->well_known); GVariant *player_children[3] = { g_variant_new_string(PLAYER_INTERFACE), player->player_properties, g_variant_new_array(G_VARIANT_TYPE_STRING, NULL, 0), }; GVariant *player_properties_tuple = g_variant_new_tuple(player_children, 3); g_dbus_connection_emit_signal(ctx->connection, NULL, MPRIS_PATH, PROPERTIES_INTERFACE, "PropertiesChanged", player_properties_tuple, &tmp_error); if (tmp_error != NULL) { g_propagate_error(error, tmp_error); return; } GVariant *root_children[3] = { g_variant_new_string(ROOT_INTERFACE), player->root_properties, g_variant_new_array(G_VARIANT_TYPE_STRING, NULL, 0), }; GVariant *root_properties_tuple = g_variant_new_tuple(root_children, 3); g_dbus_connection_emit_signal(ctx->connection, NULL, MPRIS_PATH, PROPERTIES_INTERFACE, "PropertiesChanged", root_properties_tuple, &tmp_error); if (tmp_error != NULL) { g_propagate_error(error, tmp_error); return; } // Emit nothing for unsupported optional interfaces if (player->tracklist.supported) { GVariant *tracklist_children[3] = { g_variant_new_string(TRACKLIST_INTERFACE), player->tracklist.properties, g_variant_new_array(G_VARIANT_TYPE_STRING, NULL, 0), }; GVariant *tracklist_properties_tuple = g_variant_new_tuple(tracklist_children, 3); g_dbus_connection_emit_signal(ctx->connection, NULL, MPRIS_PATH, PROPERTIES_INTERFACE, "PropertiesChanged", tracklist_properties_tuple, &tmp_error); if (tmp_error != NULL) { g_propagate_error(error, tmp_error); return; } } if (player->playlists.supported) { GVariant *playlists_children[3] = { g_variant_new_string(PLAYLISTS_INTERFACE), player->playlists.properties, g_variant_new_array(G_VARIANT_TYPE_STRING, NULL, 0), }; GVariant *playlists_properties_tuple = g_variant_new_tuple(playlists_children, 3); g_dbus_connection_emit_signal(ctx->connection, NULL, MPRIS_PATH, PROPERTIES_INTERFACE, "PropertiesChanged", playlists_properties_tuple, &tmp_error); if (tmp_error != NULL) { g_propagate_error(error, tmp_error); return; } } g_debug("sending Seeked signal with position %ld", player->position); g_dbus_connection_emit_signal(ctx->connection, NULL, MPRIS_PATH, PLAYER_INTERFACE, "Seeked", g_variant_new("(x)", player->position), &tmp_error); if (tmp_error != NULL) { g_propagate_error(error, tmp_error); return; } } else { g_debug("emitting invalidated property signals, no active player"); const gchar *const player_properties[] = { "CanControl", "CanGoNext", "CanGoPrevious", "CanPause", "CanPlay", "CanSeek", "Shuffle", "Metadata", "MaximumRate", "MinimumRate", "Rate", "Volume", "Position", "LoopStatus", "PlaybackStatus"}; const gchar *const root_properties[] = { "SupportedMimeTypes", "SupportedUriSchemes", "CanQuit", "CanRaise", "CanSetFullScreen", "HasTrackList", "DesktopEntry", "Identity"}; GVariant *player_invalidated = g_variant_new_strv( player_properties, sizeof(player_properties) / sizeof(player_properties[0])); GVariant *root_invalidated = g_variant_new_strv( root_properties, sizeof(root_properties) / sizeof(root_properties[0])); GVariant *player_children[3] = { g_variant_new_string(PLAYER_INTERFACE), g_variant_new_array(G_VARIANT_TYPE("{sv}"), NULL, 0), player_invalidated, }; GVariant *root_children[3] = { g_variant_new_string(ROOT_INTERFACE), g_variant_new_array(G_VARIANT_TYPE("{sv}"), NULL, 0), root_invalidated, }; GVariant *player_invalidated_tuple = g_variant_new_tuple(player_children, 3); GVariant *root_invalidated_tuple = g_variant_new_tuple(root_children, 3); g_dbus_connection_emit_signal(ctx->connection, NULL, MPRIS_PATH, PROPERTIES_INTERFACE, "PropertiesChanged", player_invalidated_tuple, &tmp_error); if (tmp_error != NULL) { g_propagate_error(error, tmp_error); return; } g_dbus_connection_emit_signal(ctx->connection, NULL, MPRIS_PATH, PROPERTIES_INTERFACE, "PropertiesChanged", root_invalidated_tuple, &tmp_error); if (tmp_error != NULL) { g_propagate_error(error, tmp_error); return; } // Assume old player supported all optional interfaces and invalidate those properties // unconditionally const gchar *const tracklist_properties[] = {"Tracks", "CanEditTracks"}; GVariant *tracklist_invalidated = g_variant_new_strv( tracklist_properties, sizeof(tracklist_properties) / sizeof(tracklist_properties[0])); GVariant *tracklist_children[3] = { g_variant_new_string(ROOT_INTERFACE), g_variant_new_array(G_VARIANT_TYPE("{sv}"), NULL, 0), tracklist_invalidated, }; GVariant *tracklist_invalidated_tuple = g_variant_new_tuple(tracklist_children, 3); g_dbus_connection_emit_signal(ctx->connection, NULL, MPRIS_PATH, PROPERTIES_INTERFACE, "PropertiesChanged", tracklist_invalidated_tuple, &tmp_error); if (tmp_error != NULL) { g_propagate_error(error, tmp_error); return; } const gchar *const playlists_properties[] = {"PlaylistCount", "Orderings", "ActivePlaylist"}; GVariant *playlists_invalidated = g_variant_new_strv( playlists_properties, sizeof(playlists_properties) / sizeof(playlists_properties[0])); GVariant *playlists_children[3] = { g_variant_new_string(ROOT_INTERFACE), g_variant_new_array(G_VARIANT_TYPE("{sv}"), NULL, 0), playlists_invalidated, }; GVariant *playlists_invalidated_tuple = g_variant_new_tuple(playlists_children, 3); g_dbus_connection_emit_signal(ctx->connection, NULL, MPRIS_PATH, PROPERTIES_INTERFACE, "PropertiesChanged", playlists_invalidated_tuple, &tmp_error); if (tmp_error != NULL) { g_propagate_error(error, tmp_error); return; } } GVariantDict dict; g_variant_dict_init(&dict, NULL); g_variant_dict_insert_value(&dict, "PlayerNames", context_player_names_to_gvariant(ctx)); GVariant *playerctld_children[3] = { g_variant_new_string(PLAYERCTLD_INTERFACE), g_variant_dict_end(&dict), g_variant_new_array(G_VARIANT_TYPE_STRING, NULL, 0), }; GVariant *playerctld_properties = g_variant_new_tuple(playerctld_children, 3); g_dbus_connection_emit_signal(ctx->connection, NULL, MPRIS_PATH, PROPERTIES_INTERFACE, "PropertiesChanged", playerctld_properties, &tmp_error); if (tmp_error != NULL) { g_propagate_error(error, tmp_error); return; } g_dbus_connection_emit_signal( ctx->connection, NULL, MPRIS_PATH, PLAYERCTLD_INTERFACE, "ActivePlayerChangeEnd", g_variant_new("(s)", (player != NULL ? player->well_known : "")), &tmp_error); if (tmp_error != NULL) { g_propagate_error(error, tmp_error); return; } } static struct Player *context_find_player(struct PlayerctldContext *ctx, const char *unique, const char *well_known) { const struct Player find_name = { .unique = (char *)unique, .well_known = (char *)well_known, }; GList *found = g_queue_find_custom(ctx->players, &find_name, player_compare); if (found != NULL) { return (struct Player *)found->data; } found = g_queue_find_custom(ctx->pending_players, &find_name, player_compare); if (found != NULL) { return (struct Player *)found->data; } return NULL; } static struct Player *context_get_active_player(struct PlayerctldContext *ctx) { return g_queue_peek_head(ctx->players); } static void context_set_active_player(struct PlayerctldContext *ctx, struct Player *player) { g_queue_remove(ctx->players, player); g_queue_remove(ctx->pending_players, player); g_queue_push_head(ctx->players, player); ctx->pending_active = NULL; } static void context_add_player(struct PlayerctldContext *ctx, struct Player *player) { g_queue_remove(ctx->players, player); g_queue_remove(ctx->pending_players, player); g_queue_push_tail(ctx->players, player); } static void context_add_pending_player(struct PlayerctldContext *ctx, struct Player *player) { g_queue_remove(ctx->players, player); g_queue_remove(ctx->pending_players, player); g_queue_push_tail(ctx->pending_players, player); } static void context_remove_player(struct PlayerctldContext *ctx, struct Player *player) { g_queue_remove(ctx->players, player); g_queue_remove(ctx->pending_players, player); if (ctx->pending_active == player) { ctx->pending_active = NULL; } } static void context_rotate_queue(struct PlayerctldContext *ctx) { struct Player *player; if ((player = g_queue_peek_head(ctx->players))) { context_remove_player(ctx, player); g_queue_push_tail(ctx->players, player); } } static void context_unrotate_queue(struct PlayerctldContext *ctx) { struct Player *player; if ((player = g_queue_peek_tail(ctx->players))) { context_remove_player(ctx, player); g_queue_push_head(ctx->players, player); } } /** * Returns the newly activated player */ static struct Player *context_shift_active_player(struct PlayerctldContext *ctx) { GError *error = NULL; struct Player *previous, *current; if (!(previous = current = context_get_active_player(ctx))) { return NULL; } context_remove_player(ctx, previous); context_add_player(ctx, previous); if ((current = context_get_active_player(ctx)) != previous) { player_update_position_sync(current, ctx, &error); if (error != NULL) { g_warning("could not update player position: %s", error->message); g_clear_error(&error); } context_emit_active_player_changed(ctx, &error); if (error != NULL) { g_warning("could not emit active player change: %s", error->message); g_clear_error(&error); } } return current; } static struct Player *context_unshift_active_player(struct PlayerctldContext *ctx) { GError *error = NULL; struct Player *previous, *current; if (!(previous = current = context_get_active_player(ctx))) { return NULL; } context_unrotate_queue(ctx); if ((current = context_get_active_player(ctx)) != previous) { player_update_position_sync(current, ctx, &error); if (error != NULL) { g_warning("could not update player position: %s", error->message); g_clear_error(&error); } context_emit_active_player_changed(ctx, &error); if (error != NULL) { g_warning("could not emit active player change: %s", error->message); g_clear_error(&error); } } return current; } static const char *playerctld_introspection_xml = "\n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" "\n"; static const char *mpris_introspection_xml = "\n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" " \n" "\n"; static void proxy_method_call_async_callback(GObject *source_object, GAsyncResult *res, gpointer user_data) { GDBusMethodInvocation *invocation = G_DBUS_METHOD_INVOCATION(user_data); GDBusConnection *connection = G_DBUS_CONNECTION(source_object); GError *error = NULL; GDBusMessage *reply = g_dbus_connection_send_message_with_reply_finish(connection, res, &error); if (error != NULL) { g_dbus_method_invocation_return_gerror(invocation, error); g_error_free(error); return; } GVariant *body = g_dbus_message_get_body(reply); GDBusMessageType message_type = g_dbus_message_get_message_type(reply); switch (message_type) { case G_DBUS_MESSAGE_TYPE_METHOD_RETURN: g_dbus_method_invocation_return_value(invocation, body); break; case G_DBUS_MESSAGE_TYPE_ERROR: { if (g_variant_n_children(body) > 1) { GVariant *error_message_variant = g_variant_get_child_value(body, 1); const char *error_message = g_variant_get_string(error_message_variant, 0); g_dbus_method_invocation_return_dbus_error( invocation, g_dbus_message_get_error_name(reply), error_message); g_variant_unref(error_message_variant); } else { g_dbus_method_invocation_return_dbus_error( invocation, g_dbus_message_get_error_name(reply), "Failed to call method"); } break; } default: g_warning("got unexpected message type: %d (this is a dbus spec violation)", message_type); break; } g_object_unref(invocation); g_object_unref(reply); } /** * Implement MPRIS method calls by delegating to the active player. * If there is no active player, send an error to our caller. */ static void player_method_call_proxy_callback(GDBusConnection *connection, const char *sender, const char *object_path, const char *interface_name, const char *method_name, GVariant *parameters, GDBusMethodInvocation *invocation, gpointer user_data) { g_debug("got method call: sender=%s, object_path=%s, interface_name=%s, method_name=%s", sender, object_path, interface_name, method_name); GError *error = NULL; struct PlayerctldContext *ctx = user_data; struct Player *active_player = context_get_active_player(ctx); if (active_player == NULL) { g_debug("no active player, returning error"); g_dbus_method_invocation_return_dbus_error( invocation, "com.github.altdesktop.playerctld.NoActivePlayer", "No player is being controlled by playerctld"); return; } GDBusMessage *message = g_dbus_message_copy(g_dbus_method_invocation_get_message(invocation), &error); if (error != NULL) { g_warning("could not copy message"); g_dbus_method_invocation_return_gerror(invocation, error); return; } g_debug("sending command '%s.%s' to player '%s'", interface_name, method_name, active_player->well_known); g_dbus_message_set_destination(message, active_player->unique); g_object_ref(invocation); g_dbus_connection_send_message_with_reply(ctx->connection, message, G_DBUS_SEND_MESSAGE_FLAGS_NONE, -1, NULL, NULL, proxy_method_call_async_callback, invocation); g_object_unref(message); } /** * Implement com.github.altdesktop.playerctld methods */ static void playerctld_method_call_func(GDBusConnection *connection, const char *sender, const char *object_path, const char *interface_name, const char *method_name, GVariant *parameters, GDBusMethodInvocation *invocation, gpointer user_data) { g_debug("got method call: sender=%s, object_path=%s, interface_name=%s, method_name=%s", sender, object_path, interface_name, method_name); struct PlayerctldContext *ctx = user_data; struct Player *active_player; if (strcmp(method_name, "Shift") == 0) { /** * com.github.altdesktop.playerctld.Shift * Move the active player to the back of the queue, * return the new active player */ if ((active_player = context_shift_active_player(ctx))) { g_dbus_method_invocation_return_value(invocation, g_variant_new("(s)", active_player->well_known)); } else { g_debug("no active player, returning error"); g_dbus_method_invocation_return_dbus_error( invocation, "com.github.altdesktop.playerctld.NoActivePlayer", "No player is being controlled by playerctld"); } } else if (strcmp(method_name, "Unshift") == 0) { /** * com.github.altdesktop.playerctld.Unshift * Set the least recently active player to active, * return that player. Inverse of Shift. */ if ((active_player = context_unshift_active_player(ctx))) { g_dbus_method_invocation_return_value(invocation, g_variant_new("(s)", active_player->well_known)); } else { g_debug("no active player, returning error"); g_dbus_method_invocation_return_dbus_error( invocation, "com.github.altdesktop.playerctld.NoActivePlayer", "No player is being controlled by playerctld"); } } else { /** * Fail on unknown methods. */ g_dbus_method_invocation_return_dbus_error(invocation, "com.github.altdesktop.playerctld.InvalidMethod", "This method is not valid"); } } /** * Property getter for com.github.altdesktop.playerctld */ static GVariant *playerctld_get_property_func(GDBusConnection *connection, const gchar *sender, const gchar *object_path, const gchar *interface_name, const gchar *property_name, GError **error, gpointer user_data) { struct PlayerctldContext *ctx = user_data; if (g_strcmp0(property_name, "PlayerNames") != 0) { // Fail on unknown properties. // FIXME: Should return a DBus error and not crash g_error("unknown property: %s", property_name); assert(false); } return context_player_names_to_gvariant(ctx); } /** * Location of implementation of MPRIS interfaces */ static GDBusInterfaceVTable vtable_mpris = {player_method_call_proxy_callback, NULL, NULL, {0}}; /** * Location of implementation of com.github.altdesktop.playerctld interface */ static GDBusInterfaceVTable vtable_playerctld = { playerctld_method_call_func, playerctld_get_property_func, NULL, {0}}; /** * Register callbacks to implement the interfaces we're supposed to * That is to say, the four MPRIS interfaces as well as com.github.altdesktop.playerctld */ static void on_bus_acquired(GDBusConnection *connection, const char *name, gpointer user_data) { GError *error = NULL; struct PlayerctldContext *ctx = user_data; g_dbus_connection_register_object(connection, MPRIS_PATH, ctx->root_interface_info, &vtable_mpris, user_data, NULL, &error); if (error != NULL) { g_warning("%s", error->message); g_clear_error(&error); } g_dbus_connection_register_object(connection, MPRIS_PATH, ctx->player_interface_info, &vtable_mpris, user_data, NULL, &error); if (error != NULL) { g_warning("%s", error->message); g_clear_error(&error); } g_dbus_connection_register_object(connection, MPRIS_PATH, ctx->playlists_interface_info, &vtable_mpris, user_data, NULL, &error); if (error != NULL) { g_warning("%s", error->message); g_clear_error(&error); } g_dbus_connection_register_object(connection, MPRIS_PATH, ctx->tracklist_interface_info, &vtable_mpris, user_data, NULL, &error); if (error != NULL) { g_warning("%s", error->message); g_clear_error(&error); } g_dbus_connection_register_object(connection, MPRIS_PATH, ctx->playerctld_interface_info, &vtable_playerctld, user_data, NULL, &error); if (error != NULL) { g_warning("%s", error->message); g_clear_error(&error); } } static void on_name_lost(GDBusConnection *connection, const char *name, gpointer user_data) { struct PlayerctldContext *ctx = user_data; if (connection == NULL) { g_printerr("%s\n", "could not acquire bus name: unknown connection error"); } else { g_printerr("%s\n", "could not acquire bus name: playerctld is already running"); } ctx->return_code = 1; g_main_loop_quit(ctx->loop); } static bool well_known_name_is_managed(const char *name) { return g_str_has_prefix(name, "org.mpris.MediaPlayer2.") && !g_str_has_prefix(name, "org.mpris.MediaPlayer2.playerctld"); } struct GetPropertiesUserData { struct PlayerctldContext *ctx; const char *interface_name; struct Player *player; }; static void active_player_get_properties_async_callback(GObject *source_object, GAsyncResult *res, gpointer user_data) { GDBusConnection *connection = G_DBUS_CONNECTION(source_object); struct GetPropertiesUserData *data = user_data; GError *error = NULL; GVariant *body = g_dbus_connection_call_finish(connection, res, &error); if (error != NULL) { g_warning("could not get properties for active player: %s", error->message); g_clear_error(&error); goto out; } g_debug("got all properties response for name='%s', interface '%s'", data->player->well_known, data->interface_name); // g_debug("%s", g_variant_print(body_value, TRUE)); GVariant *body_value = g_variant_get_child_value(body, 0); player_update_properties(data->player, data->interface_name, body_value); g_variant_unref(body_value); g_variant_unref(body); if (data->player->player_properties == NULL || data->player->root_properties == NULL) { // wait for both requests to complete before setting the player // nonpending goto out; } if (data->player == data->ctx->pending_active) { context_set_active_player(data->ctx, data->player); context_emit_active_player_changed(data->ctx, &error); if (error != NULL) { g_warning("could not emit properties changed signal for active player: %s", error->message); context_remove_player(data->ctx, data->player); g_clear_error(&error); goto out; } } else { context_add_player(data->ctx, data->player); } out: free(data); } static void name_owner_changed_signal_callback(GDBusConnection *connection, const gchar *sender_name, const gchar *object_path, const gchar *interface_name, const gchar *signal_name, GVariant *parameters, gpointer user_data) { struct PlayerctldContext *ctx = user_data; GError *error = NULL; GVariant *name_variant = g_variant_get_child_value(parameters, 0); GVariant *new_owner_variant = g_variant_get_child_value(parameters, 2); const gchar *name = g_variant_get_string(name_variant, 0); const gchar *new_owner = g_variant_get_string(new_owner_variant, 0); if (!well_known_name_is_managed(name)) { goto out; } g_debug("got name owner changed signal: name='%s', owner='%s'", name, new_owner); if (strlen(new_owner) > 0) { g_debug("player name appeared: unique=%s, well_known=%s", new_owner, name); // see if it's already managed struct Player *player = context_find_player(ctx, NULL, name); if (player != NULL) { g_debug("player already managed, setting to active"); player_set_unique_name(player, new_owner); if (player != context_get_active_player(ctx)) { context_set_active_player(ctx, player); player_update_position_sync(player, ctx, &error); if (error != NULL) { g_warning("could not update player position: %s", error->message); g_clear_error(&error); } context_emit_active_player_changed(ctx, &error); if (error != NULL) { g_warning("could not emit active player change: %s", error->message); g_clear_error(&error); } } goto out; } g_debug("setting player to pending active"); player = player_new(new_owner, name); context_add_pending_player(ctx, player); ctx->pending_active = player; struct GetPropertiesUserData *player_data = calloc(1, sizeof(struct GetPropertiesUserData)); player_data->interface_name = PLAYER_INTERFACE; player_data->player = player; player_data->ctx = ctx; g_dbus_connection_call(connection, new_owner, MPRIS_PATH, PROPERTIES_INTERFACE, "GetAll", g_variant_new("(s)", PLAYER_INTERFACE), NULL, G_DBUS_CALL_FLAGS_NO_AUTO_START, -1, NULL, active_player_get_properties_async_callback, player_data); struct GetPropertiesUserData *root_data = calloc(1, sizeof(struct GetPropertiesUserData)); root_data->interface_name = ROOT_INTERFACE; root_data->player = player; root_data->ctx = ctx; g_dbus_connection_call(connection, new_owner, MPRIS_PATH, PROPERTIES_INTERFACE, "GetAll", g_variant_new("(s)", ROOT_INTERFACE), NULL, G_DBUS_CALL_FLAGS_NO_AUTO_START, -1, NULL, active_player_get_properties_async_callback, root_data); struct GetPropertiesUserData *tracklist_data = calloc(1, sizeof(struct GetPropertiesUserData)); tracklist_data->interface_name = TRACKLIST_INTERFACE; tracklist_data->player = player; tracklist_data->ctx = ctx; g_dbus_connection_call(connection, new_owner, MPRIS_PATH, PROPERTIES_INTERFACE, "GetAll", g_variant_new("(s)", TRACKLIST_INTERFACE), NULL, G_DBUS_CALL_FLAGS_NO_AUTO_START, -1, NULL, active_player_get_properties_async_callback, tracklist_data); struct GetPropertiesUserData *playlists_data = calloc(1, sizeof(struct GetPropertiesUserData)); playlists_data->interface_name = PLAYLISTS_INTERFACE; playlists_data->player = player; playlists_data->ctx = ctx; g_dbus_connection_call(connection, new_owner, MPRIS_PATH, PROPERTIES_INTERFACE, "GetAll", g_variant_new("(s)", PLAYLISTS_INTERFACE), NULL, G_DBUS_CALL_FLAGS_NO_AUTO_START, -1, NULL, active_player_get_properties_async_callback, playlists_data); } else { struct Player *player = context_find_player(ctx, NULL, name); if (player == NULL) { g_debug("%s", "name not found in queue"); goto out; } bool is_active = (player == context_get_active_player(ctx)); g_debug("removing name from players: unique=%s, well_known=%s", player->unique, player->well_known); context_remove_player(ctx, player); player_free(player); if (!is_active) { goto out; } struct Player *active_player = context_get_active_player(ctx); if (active_player != NULL) { player_update_position_sync(active_player, ctx, &error); if (error != NULL) { active_player->position = 0l; g_warning("could not update player position for player '%s': %s", active_player->well_known, error->message); g_clear_error(&error); } } context_emit_active_player_changed(ctx, &error); if (error != NULL) { g_warning("could not emit player properties changed signal: %s", error->message); g_clear_error(&error); } } out: g_variant_unref(name_variant); g_variant_unref(new_owner_variant); } static void player_signal_proxy_callback(GDBusConnection *connection, const gchar *sender_name, const gchar *object_path, const gchar *interface_name, const gchar *signal_name, GVariant *parameters, gpointer user_data) { gboolean changed = TRUE; GError *error = NULL; struct PlayerctldContext *ctx = user_data; struct Player *player = context_find_player(ctx, sender_name, NULL); if (player == NULL) { return; } g_debug("got player signal: sender=%s, object_path=%s, interface_name=%s, signal_name=%s", sender_name, object_path, interface_name, signal_name); if (g_strcmp0(interface_name, PLAYER_INTERFACE) != 0 && g_strcmp0(interface_name, PROPERTIES_INTERFACE) != 0) { return; } g_debug("got player signal: sender=%s, object_path=%s, interface_name=%s, signal_name=%s", sender_name, object_path, interface_name, signal_name); if (player == ctx->pending_active) { // TODO buffer seeked signals return; } bool is_properties_changed = (g_strcmp0(signal_name, "PropertiesChanged") == 0); if (is_properties_changed) { GVariant *interface = g_variant_get_child_value(parameters, 0); GVariant *properties = g_variant_get_child_value(parameters, 1); changed = player_update_properties(player, g_variant_get_string(interface, 0), properties); g_variant_unref(interface); g_variant_unref(properties); } if (changed && player != context_get_active_player(ctx)) { g_debug("new active player: %s", player->well_known); context_set_active_player(ctx, player); player_update_position_sync(player, ctx, &error); if (error != NULL) { player->position = 0l; g_warning("could not update player position: %s", error->message); g_clear_error(&error); } context_emit_active_player_changed(ctx, &error); if (error != NULL) { g_warning("could not emit all properties changed signal: %s", error->message); g_clear_error(&error); } } g_dbus_connection_emit_signal(ctx->connection, NULL, object_path, interface_name, signal_name, parameters, &error); if (error != NULL) { g_debug("could not emit signal: %s", error->message); g_clear_error(&error); } } static gchar **command_arg = NULL; static const GOptionEntry entries[] = { {G_OPTION_REMAINING, 0, 0, G_OPTION_ARG_STRING_ARRAY, &command_arg, NULL, "COMMAND"}, {NULL}, }; static gboolean parse_setup_options(int argc, char **argv, GError **error) { static const gchar *description = "Available Commands:" "\n daemon Activate playerctld and exit" "\n shift Shift to next player" "\n unshift Unshift to previous player"; GOptionContext *context; gboolean success; context = g_option_context_new("- Playerctl Daemon"); g_option_context_add_main_entries(context, entries, NULL); g_option_context_set_description(context, description); success = g_option_context_parse(context, &argc, &argv, error); if (success && command_arg && (g_strcmp0(command_arg[0], "shift") != 0 && g_strcmp0(command_arg[0], "unshift") != 0 && g_strcmp0(command_arg[0], "daemon") != 0)) { gchar *help = g_option_context_get_help(context, TRUE, NULL); printf("%s\n", help); g_option_context_free(context); g_free(help); exit(1); } g_option_context_free(context); return success; } int playercmd_shift(GDBusConnection *connection) { GError *error = NULL; g_dbus_connection_call_sync(connection, "org.mpris.MediaPlayer2.playerctld", MPRIS_PATH, PLAYERCTLD_INTERFACE, "Shift", NULL, NULL, G_DBUS_CALL_FLAGS_NO_AUTO_START, -1, NULL, &error); g_object_unref(connection); if (error != NULL) { g_printerr("Cannot shift: %s\n", error->message); return 1; } return 0; } int playercmd_unshift(GDBusConnection *connection) { GError *error = NULL; g_dbus_connection_call_sync(connection, "org.mpris.MediaPlayer2.playerctld", MPRIS_PATH, PLAYERCTLD_INTERFACE, "Unshift", NULL, NULL, G_DBUS_CALL_FLAGS_NO_AUTO_START, -1, NULL, &error); g_object_unref(connection); if (error != NULL) { g_printerr("Cannot unshift: %s\n", error->message); return 1; } return 0; } enum activation_result { ACTIVATION_FAIL = 0, ACTIVATION_NOT_SUPPORTED, ACTIVATION_SUCCESS, ACTIVATION_ALREADY_RUNNING, }; enum activation_result start_playerctld_dbus_activation(GDBusConnection *connection, GError **error) { GError *tmp_error = NULL; GVariant *child = NULL; GVariant *result = g_dbus_connection_call_sync( connection, "org.freedesktop.DBus", "/org/freedesktop/DBus", "org.freedesktop.DBus", "StartServiceByName", g_variant_new("(su)", "org.mpris.MediaPlayer2.playerctld", 0), NULL, G_DBUS_CALL_FLAGS_NO_AUTO_START, -1, NULL, &tmp_error); if (tmp_error != NULL) { if (g_str_has_prefix(tmp_error->message, "GDBus.Error:org.freedesktop.DBus.Error.ServiceUnknown")) { g_clear_error(&tmp_error); return ACTIVATION_NOT_SUPPORTED; } g_propagate_error(error, tmp_error); return ACTIVATION_FAIL; } child = g_variant_get_child_value(result, 0); guint32 result_code = g_variant_get_uint32(child); g_variant_unref(child); g_variant_unref(result); if (result_code == 1) { return ACTIVATION_SUCCESS; } else if (result_code == 2) { return ACTIVATION_ALREADY_RUNNING; } else { g_warning("Got unknown result from StartServiceByName: %d\n", result_code); return ACTIVATION_SUCCESS; } } int main(int argc, char *argv[]) { struct PlayerctldContext ctx = {0}; GError *error = NULL; if (!parse_setup_options(argc, argv, &error)) { g_printerr("%s\n", error->message); g_clear_error(&error); exit(0); } // Setup DBus connection GDBusConnectionFlags connection_flags = G_DBUS_CONNECTION_FLAGS_AUTHENTICATION_CLIENT | G_DBUS_CONNECTION_FLAGS_MESSAGE_BUS_CONNECTION; ctx.bus_address = g_dbus_address_get_for_bus_sync(G_BUS_TYPE_SESSION, NULL, &error); if (error != NULL) { g_printerr("could not get bus address: %s", error->message); return 1; } // ctx.connection = g_bus_get_sync(G_BUS_TYPE_SESSION, NULL, &error); ctx.connection = g_dbus_connection_new_for_address_sync(ctx.bus_address, connection_flags, NULL, NULL, &error); if (error != NULL) { g_printerr("could not connect to message bus: %s", error->message); return 1; } g_debug("connected to dbus: %s", g_dbus_connection_get_unique_name(ctx.connection)); if (command_arg && g_strcmp0(command_arg[0], "daemon") == 0) { enum activation_result result = start_playerctld_dbus_activation(ctx.connection, &error); if (error != NULL) { g_printerr("could not activate playerctld service: %s\n", error->message); g_clear_error(&error); return 1; } switch (result) { case ACTIVATION_NOT_SUPPORTED: // TODO: find some other way to daemonize the process g_printerr("%s\n", "org.freedesktop.DBus.Error.ServiceUnknown: DBus service activation " "of playerctld is not supported"); return 1; break; case ACTIVATION_SUCCESS: g_printerr("%s\n", "playerctld successfully started with DBus service activation"); return 0; break; case ACTIVATION_ALREADY_RUNNING: g_printerr("%s\n", "playerctld DBus service is already running"); return 0; break; case ACTIVATION_FAIL: // not reached, already handled in the error condition return 1; } } if (command_arg && g_strcmp0(command_arg[0], "shift") == 0) { return playercmd_shift(ctx.connection); } if (command_arg && g_strcmp0(command_arg[0], "unshift") == 0) { return playercmd_unshift(ctx.connection); } GDBusNodeInfo *mpris_introspection_data = NULL; GDBusNodeInfo *playerctld_introspection_data = NULL; ctx.players = g_queue_new(); ctx.pending_players = g_queue_new(); ctx.loop = g_main_loop_new(NULL, FALSE); // Load introspection data and split into separate interfaces mpris_introspection_data = g_dbus_node_info_new_for_xml(mpris_introspection_xml, &error); if (error != NULL) { g_printerr("%s", error->message); return 1; } playerctld_introspection_data = g_dbus_node_info_new_for_xml(playerctld_introspection_xml, &error); if (error != NULL) { g_printerr("%s", error->message); return 1; } ctx.root_interface_info = g_dbus_node_info_lookup_interface(mpris_introspection_data, ROOT_INTERFACE); if (ctx.root_interface_info == NULL) { g_error("Missing introspection data for MPRIS root interface"); } // Is the player interface missing from the introspection data? ctx.player_interface_info = g_dbus_node_info_lookup_interface(mpris_introspection_data, PLAYER_INTERFACE); if (ctx.player_interface_info == NULL) { g_error("Missing introspection data for MPRIS player interface"); } ctx.playlists_interface_info = g_dbus_node_info_lookup_interface(mpris_introspection_data, PLAYLISTS_INTERFACE); if (ctx.playlists_interface_info == NULL) { g_error("Missing introspection data for MPRIS playlists interface"); } ctx.tracklist_interface_info = g_dbus_node_info_lookup_interface(mpris_introspection_data, TRACKLIST_INTERFACE); if (ctx.tracklist_interface_info == NULL) { g_error("Missing introspection data for MPRIS tracklist interface"); } ctx.playerctld_interface_info = g_dbus_node_info_lookup_interface( playerctld_introspection_data, "com.github.altdesktop.playerctld"); // Get all names of players (names that start with "org.mpris.MediaPlayer2.") // then fetch their properties on all supported interfaces GVariant *names_reply = g_dbus_connection_call_sync( ctx.connection, DBUS_NAME, DBUS_PATH, DBUS_INTERFACE, "ListNames", NULL, NULL, G_DBUS_CALL_FLAGS_NO_AUTO_START, -1, NULL, &error); if (error != NULL) { g_warning("could not call ListNames: %s", error->message); return 1; } GVariant *names_reply_value = g_variant_get_child_value(names_reply, 0); gsize nnames; const gchar **names = g_variant_get_strv(names_reply_value, &nnames); for (int i = 0; i < nnames; ++i) { if (well_known_name_is_managed(names[i])) { // org.mpris.MediaPlayer2.Player properties GVariant *owner_reply = g_dbus_connection_call_sync(ctx.connection, DBUS_NAME, DBUS_PATH, DBUS_INTERFACE, "GetNameOwner", g_variant_new("(s)", names[i]), NULL, G_DBUS_CALL_FLAGS_NO_AUTO_START, -1, NULL, &error); if (error != NULL) { g_warning("could not get owner for name %s: %s", names[i], error->message); g_clear_error(&error); continue; } GVariant *owner_reply_value = g_variant_get_child_value(owner_reply, 0); const gchar *owner = g_variant_get_string(owner_reply_value, 0); struct Player *player = player_new(owner, names[i]); GVariant *reply = g_dbus_connection_call_sync( ctx.connection, player->unique, MPRIS_PATH, PROPERTIES_INTERFACE, "GetAll", g_variant_new("(s)", PLAYER_INTERFACE), NULL, G_DBUS_CALL_FLAGS_NO_AUTO_START, -1, NULL, &error); if (error != NULL) { // This interface is mandatory, get rid of "players" who don't support it g_warning("could not get player properties for player: %s", player->well_known); player_free(player); g_clear_error(&error); continue; } GVariant *properties = g_variant_get_child_value(reply, 0); player_update_properties(player, PLAYER_INTERFACE, properties); g_variant_unref(properties); g_variant_unref(reply); // org.mpris.MediaPlayer2 properties reply = g_dbus_connection_call_sync(ctx.connection, player->unique, MPRIS_PATH, PROPERTIES_INTERFACE, "GetAll", g_variant_new("(s)", ROOT_INTERFACE), NULL, G_DBUS_CALL_FLAGS_NO_AUTO_START, -1, NULL, &error); if (error != NULL) { // This interface is mandatory, get rid of "players" who don't support it g_warning("could not get root properties for player: %s", player->well_known); player_free(player); g_clear_error(&error); continue; } properties = g_variant_get_child_value(reply, 0); player_update_properties(player, ROOT_INTERFACE, properties); g_variant_unref(properties); g_variant_unref(reply); // org.mpris.MediaPlayer2.TrackList properties player->tracklist.supported = true; // Or so we hope reply = g_dbus_connection_call_sync(ctx.connection, player->unique, MPRIS_PATH, PROPERTIES_INTERFACE, "GetAll", g_variant_new("(s)", TRACKLIST_INTERFACE), NULL, G_DBUS_CALL_FLAGS_NO_AUTO_START, -1, NULL, &error); if (error != NULL) { // This interface is optional, so we can keep the player around player->tracklist.supported = false; g_warning("could not get tracklist properties for player: %s", player->well_known); g_clear_error(&error); } else { properties = g_variant_get_child_value(reply, 0); player_update_properties(player, TRACKLIST_INTERFACE, properties); g_variant_unref(properties); g_variant_unref(reply); } // org.mpris.MediaPlayer2.Playlists properties player->playlists.supported = true; // Or so we hope reply = g_dbus_connection_call_sync(ctx.connection, player->unique, MPRIS_PATH, PROPERTIES_INTERFACE, "GetAll", g_variant_new("(s)", PLAYLISTS_INTERFACE), NULL, G_DBUS_CALL_FLAGS_NO_AUTO_START, -1, NULL, &error); if (error != NULL) { // This interface is optional, so we can keep the player around player->playlists.supported = false; g_warning("could not get playlists properties for player: %s", player->well_known); g_clear_error(&error); } else { properties = g_variant_get_child_value(reply, 0); player_update_properties(player, PLAYLISTS_INTERFACE, properties); g_variant_unref(properties); g_variant_unref(reply); } g_debug("found player: %s", player->well_known); g_queue_push_head(ctx.players, player); g_variant_unref(owner_reply_value); g_variant_unref(owner_reply); } } g_free(names); g_variant_unref(names_reply_value); g_variant_unref(names_reply); g_dbus_connection_signal_subscribe( ctx.connection, DBUS_NAME, DBUS_INTERFACE, "NameOwnerChanged", DBUS_PATH, NULL, G_DBUS_SIGNAL_FLAGS_NONE, name_owner_changed_signal_callback, &ctx, NULL); g_dbus_connection_signal_subscribe(ctx.connection, NULL, NULL, NULL, MPRIS_PATH, NULL, G_DBUS_SIGNAL_FLAGS_NONE, player_signal_proxy_callback, &ctx, NULL); ctx.bus_id = g_bus_own_name_on_connection(ctx.connection, "org.mpris.MediaPlayer2.playerctld", G_BUS_NAME_OWNER_FLAGS_DO_NOT_QUEUE, on_bus_acquired, on_name_lost, &ctx, NULL); g_main_loop_run(ctx.loop); g_bus_unown_name(ctx.bus_id); g_main_loop_unref(ctx.loop); g_dbus_node_info_unref(mpris_introspection_data); g_dbus_node_info_unref(playerctld_introspection_data); g_queue_free_full(ctx.players, (GDestroyNotify)player_free); g_object_unref(ctx.connection); g_free(ctx.bus_address); return ctx.return_code; } playerctl-2.4.1/playerctl/playerctl-enum-types.c.in000066400000000000000000000016051412234731200223710ustar00rootroot00000000000000/*** BEGIN file-header ***/ #include "config.h" #include "enum-types.h" /*** END file-header ***/ /*** BEGIN file-production ***/ /* enumerations from "@filename@" */ /*** END file-production ***/ /*** BEGIN value-header ***/ GType @enum_name@_get_type (void) { static volatile gsize g_@type@_type_id__volatile; if (g_once_init_enter (&g_define_type_id__volatile)) { static const G@Type@Value values[] = { /*** END value-header ***/ /*** BEGIN value-production ***/ { @VALUENAME@, "@VALUENAME@", "@valuenick@" }, /*** END value-production ***/ /*** BEGIN value-tail ***/ { 0, NULL, NULL } }; GType g_@type@_type_id = g_@type@_register_static (g_intern_static_string ("@EnumName@"), values); g_once_init_leave (&g_@type@_type_id__volatile, g_@type@_type_id); } return g_@type@_type_id__volatile; } /*** END value-tail ***/ playerctl-2.4.1/playerctl/playerctl-enum-types.h.in000066400000000000000000000007331412234731200223770ustar00rootroot00000000000000/*** BEGIN file-header ***/ #pragma once /* Include the main project header */ #include "project.h" G_BEGIN_DECLS /*** END file-header ***/ /*** BEGIN file-production ***/ /* enumerations from "@filename@" */ /*** END file-production ***/ /*** BEGIN value-header ***/ GType @enum_name@_get_type (void) G_GNUC_CONST; #define @ENUMPREFIX@_TYPE_@ENUMSHORT@ (@enum_name@_get_type ()) /*** END value-header ***/ /*** BEGIN file-tail ***/ G_END_DECLS /*** END file-tail ***/ playerctl-2.4.1/playerctl/playerctl-formatter.c000066400000000000000000001100701412234731200216560ustar00rootroot00000000000000#include "playerctl/playerctl-formatter.h" #include #include #include #include #include #include "playerctl/playerctl-common.h" #define LENGTH(array) (sizeof array / sizeof array[0]) #define MAX_ARGS 32 #define INFIX_ADD "+" #define INFIX_SUB "-" #define INFIX_MUL "*" #define INFIX_DIV "/" // clang-format off G_DEFINE_QUARK(playerctl-formatter-error-quark, playerctl_formatter_error); // clang-format on enum token_type { TOKEN_VARIABLE, TOKEN_STRING, TOKEN_FUNCTION, TOKEN_NUMBER, }; struct token { enum token_type type; gchar *data; gdouble numeric_data; GList *args; }; enum parser_state { STATE_EXPRESSION = 0, STATE_IDENTIFIER, STATE_STRING, STATE_NUMBER, }; enum parse_level { PARSE_FULL = 0, PARSE_NEXT_IDENT, PARSE_MULT_DIV, }; struct _PlayerctlFormatterPrivate { GList *tokens; }; static struct token *token_create(enum token_type type) { struct token *token = calloc(1, sizeof(struct token)); token->type = type; return token; } static void token_list_destroy(GList *tokens); static void token_destroy(struct token *token) { if (token == NULL) { return; } token_list_destroy(token->args); g_free(token->data); free(token); } static void token_list_destroy(GList *tokens) { if (tokens == NULL) { return; } g_list_free_full(tokens, (GDestroyNotify)token_destroy); } static gboolean token_list_contains_key(GList *tokens, const gchar *key) { if (tokens == NULL) { return FALSE; } GList *t = NULL; for (t = tokens; t != NULL; t = t->next) { struct token *token = t->data; switch (token->type) { case TOKEN_VARIABLE: if (g_strcmp0(token->data, key) == 0) { return TRUE; } break; case TOKEN_FUNCTION: if (token_list_contains_key(token->args, key)) { return TRUE; } default: break; } } return FALSE; } static gboolean is_identifier_start_char(gchar c) { return g_ascii_isalpha(c) || c == '_'; } static gboolean is_identifier_char(gchar c) { return g_ascii_isalnum(c) || c == '_' || c == ':'; } static gboolean is_numeric_char(gchar c) { return g_ascii_isdigit(c) || c == '.'; } static gchar *infix_to_identifier(gchar infix) { switch (infix) { case '+': return g_strdup(INFIX_ADD); case '-': return g_strdup(INFIX_SUB); case '*': return g_strdup(INFIX_MUL); case '/': return g_strdup(INFIX_DIV); default: assert(false && "not reached"); } } static struct token *tokenize_expression(const gchar *format, gint pos, gint *end, enum parse_level level, GError **error) { GError *tmp_error = NULL; int len = strlen(format); char buf[1028]; int buf_len = 0; struct token *tok = NULL; enum parser_state state = STATE_EXPRESSION; if (pos > len - 1) { g_set_error(error, playerctl_formatter_error_quark(), 1, "unexpected end of expression"); return NULL; } for (int i = pos; i < len; ++i) { switch (state) { case STATE_EXPRESSION: if (format[i] == ' ') { continue; } else if (format[i] == '(') { // ordering parens tok = tokenize_expression(format, i + 1, end, PARSE_FULL, &tmp_error); if (tmp_error != NULL) { g_propagate_error(error, tmp_error); return NULL; } if (*end > len - 1 || format[*end] != ')') { g_set_error(error, playerctl_formatter_error_quark(), 1, "expected \")\" (position %d)", *end); token_destroy(tok); return NULL; } *end += 1; goto loop_out; } else if (format[i] == '+' || format[i] == '-') { // unary + or - struct token *operand = tokenize_expression(format, i + 1, end, PARSE_NEXT_IDENT, &tmp_error); if (tmp_error != NULL) { g_propagate_error(error, tmp_error); return NULL; } tok = token_create(TOKEN_FUNCTION); tok->data = infix_to_identifier(format[i]); tok->args = g_list_append(tok->args, operand); goto loop_out; } else if (format[i] == '"') { state = STATE_STRING; continue; } else if (is_numeric_char(format[i])) { state = STATE_NUMBER; buf[buf_len++] = format[i]; continue; } else if (!is_identifier_start_char(format[i])) { g_set_error(error, playerctl_formatter_error_quark(), 1, "unexpected \"%c\", expected expression (position %d)", format[i], i); return NULL; } else { state = STATE_IDENTIFIER; buf[buf_len++] = format[i]; continue; } break; case STATE_STRING: if (format[i] == '"') { tok = token_create(TOKEN_STRING); buf[buf_len] = '\0'; tok->data = g_strdup(buf); i++; while (i < len && format[i] == ' ') { i++; } *end = i; // printf("string: '%s'\n", tok->data); goto loop_out; } else { buf[buf_len++] = format[i]; } break; case STATE_NUMBER: if (!is_numeric_char(format[i]) || i == len - 2) { tok = token_create(TOKEN_NUMBER); buf[buf_len] = '\0'; tok->data = g_strdup(buf); char *endptr = NULL; gdouble number = strtod(tok->data, &endptr); if (endptr == NULL || *endptr != '\0') { g_set_error(error, playerctl_formatter_error_quark(), 1, "invalid number: \"%s\" (position %d)", tok->data, i); token_destroy(tok); return NULL; } tok->numeric_data = number; while (i < len && format[i] == ' ') { i++; } *end = i; // printf("number: '%f'\n", tok->numeric_data); goto loop_out; } else { buf[buf_len++] = format[i]; } break; case STATE_IDENTIFIER: if (format[i] == '(') { tok = token_create(TOKEN_FUNCTION); buf[buf_len] = '\0'; tok->data = g_strdup(buf); i += 1; // printf("function: '%s'\n", tok->data); int nargs = 0; while (TRUE) { tok->args = g_list_append( tok->args, tokenize_expression(format, i, end, PARSE_FULL, &tmp_error)); nargs++; if (nargs > MAX_ARGS) { g_set_error(error, playerctl_formatter_error_quark(), 1, "maximum args of %d exceeded", MAX_ARGS); token_destroy(tok); return NULL; } if (tmp_error != NULL) { token_destroy(tok); g_propagate_error(error, tmp_error); return NULL; } while (*end < len && format[*end] == ' ') { *end += 1; } if (format[*end] == ')') { *end += 1; break; } else if (format[*end] == ',') { i = *end + 1; continue; } else { g_set_error(error, playerctl_formatter_error_quark(), 1, "expecting \")\" (position %d)", *end); token_destroy(tok); return NULL; } } goto loop_out; } else if (!is_identifier_char(format[i])) { tok = token_create(TOKEN_VARIABLE); buf[buf_len] = '\0'; tok->data = g_strdup(buf); while (i < len && format[i] == ' ') { i++; } *end = i; // printf("variable: '%s' end='%c'\n", tok->data, format[*end]); goto loop_out; } else { buf[buf_len] = format[i]; ++buf_len; } break; } } loop_out: if (tok == NULL) { g_set_error(error, playerctl_formatter_error_quark(), 1, "unexpected end of expression"); return NULL; } while (*end < len && format[*end] == ' ') { *end += 1; } if (level == PARSE_NEXT_IDENT || *end >= len - 1) { return tok; } gchar infix_id = format[*end]; while (infix_id == '*' || infix_id == '/' || infix_id == '+' || infix_id == '-') { while (infix_id == '*' || infix_id == '/') { struct token *operand = tokenize_expression(format, *end + 1, end, PARSE_NEXT_IDENT, &tmp_error); if (tmp_error != NULL) { token_destroy(tok); g_propagate_error(error, tmp_error); return NULL; } struct token *operation = token_create(TOKEN_FUNCTION); operation->data = infix_to_identifier(infix_id); operation->args = g_list_append(operation->args, tok); operation->args = g_list_append(operation->args, operand); tok = operation; infix_id = format[*end]; } if (level == PARSE_MULT_DIV) { return tok; } if (infix_id == '+' || infix_id == '-') { struct token *operand = tokenize_expression(format, *end + 1, end, PARSE_MULT_DIV, &tmp_error); if (tmp_error != NULL) { token_destroy(tok); g_propagate_error(error, tmp_error); return NULL; } struct token *operation = token_create(TOKEN_FUNCTION); operation->data = infix_to_identifier(infix_id); operation->args = g_list_append(operation->args, tok); operation->args = g_list_append(operation->args, operand); tok = operation; infix_id = format[*end]; } } return tok; } static GList *tokenize_format(const char *format, GError **error) { GError *tmp_error = NULL; GList *tokens = NULL; if (format == NULL) { return NULL; } int len = strlen(format); char buf[1028]; int buf_len = 0; if (len >= 1028) { g_set_error(error, playerctl_formatter_error_quark(), 1, "the maximum format string length is 1028"); return NULL; } for (int i = 0; i < len; ++i) { if (format[i] == '{' && i < len + 1 && format[i + 1] == '{') { if (buf_len > 0) { buf[buf_len] = '\0'; buf_len = 0; struct token *token = token_create(TOKEN_STRING); token->data = g_strdup(buf); // printf("passthrough: '%s'\n", token->data); tokens = g_list_append(tokens, token); } i += 2; int end = 0; struct token *token = tokenize_expression(format, i, &end, PARSE_FULL, &tmp_error); if (tmp_error != NULL) { token_list_destroy(tokens); g_propagate_error(error, tmp_error); return NULL; } tokens = g_list_append(tokens, token); i = end; while (i < len && format[i] == ' ') { i++; } if (i >= len || format[i] != '}' || format[i + 1] != '}') { token_list_destroy(tokens); g_set_error(error, playerctl_formatter_error_quark(), 1, "expecting \"}}\" (position %d)", i); return NULL; } i += 1; } else { buf[buf_len++] = format[i]; } } if (buf_len > 0) { buf[buf_len] = '\0'; struct token *token = token_create(TOKEN_STRING); token->data = g_strdup(buf); tokens = g_list_append(tokens, token); } return tokens; } static GVariant *helperfn_lc(struct token *token, GVariant **args, int nargs, GError **error) { if (nargs != 1) { g_set_error(error, playerctl_formatter_error_quark(), 1, "function lc takes exactly one argument (got %d)", nargs); return NULL; } GVariant *value = args[0]; if (value == NULL) { return g_variant_new("s", ""); } gchar *printed = pctl_print_gvariant(value); gchar *printed_lc = g_utf8_strdown(printed, -1); GVariant *ret = g_variant_new("s", printed_lc); g_free(printed); g_free(printed_lc); return ret; } static GVariant *helperfn_uc(struct token *token, GVariant **args, int nargs, GError **error) { if (nargs != 1) { g_set_error(error, playerctl_formatter_error_quark(), 1, "function uc takes exactly one argument (got %d)", nargs); return NULL; } GVariant *value = args[0]; if (value == NULL) { return g_variant_new("s", ""); } gchar *printed = pctl_print_gvariant(value); gchar *printed_uc = g_utf8_strup(printed, -1); GVariant *ret = g_variant_new("s", printed_uc); g_free(printed); g_free(printed_uc); return ret; } static GVariant *helperfn_duration(struct token *token, GVariant **args, int nargs, GError **error) { if (nargs != 1) { g_set_error(error, playerctl_formatter_error_quark(), 1, "function uc takes exactly one argument (got %d)", nargs); return NULL; } GVariant *value = args[0]; if (value == NULL) { return g_variant_new("s", ""); } gint64 duration; if (g_variant_type_equal(g_variant_get_type(value), G_VARIANT_TYPE_INT64)) { // mpris specifies all track position values to be int64 duration = g_variant_get_int64(value); } else if (g_variant_type_equal(g_variant_get_type(value), G_VARIANT_TYPE_UINT64)) { // XXX: spotify may give uint64 duration = g_variant_get_uint64(value); } else if (g_variant_type_equal(g_variant_get_type(value), G_VARIANT_TYPE_DOUBLE)) { // only if supplied by a constant or position value type goes against spec duration = g_variant_get_double(value); } else { g_set_error(error, playerctl_formatter_error_quark(), 1, "function duration can only be called on track position values"); return NULL; } gint64 seconds = (duration / 1000000) % 60; gint64 minutes = (duration / 1000000 / 60) % 60; gint64 hours = (duration / 1000000 / 60 / 60); GString *formatted = g_string_new(""); if (hours != 0) { g_string_append_printf(formatted, "%" PRId64 ":%02" PRId64 ":%02" PRId64, hours, minutes, seconds); } else { g_string_append_printf(formatted, "%" PRId64 ":%02" PRId64, minutes, seconds); } gchar *formatted_inner = g_string_free(formatted, FALSE); GVariant *ret = g_variant_new("s", formatted_inner); g_free(formatted_inner); return ret; } /* Calls g_markup_escape_text to replace the text with appropriately escaped characters for XML */ static GVariant *helperfn_markup_escape(struct token *token, GVariant **args, int nargs, GError **error) { if (nargs != 1) { g_set_error(error, playerctl_formatter_error_quark(), 1, "function markup_escape takes exactly one argument (got %d)", nargs); return NULL; } GVariant *value = args[0]; if (value == NULL) { return g_variant_new("s", ""); } gchar *printed = pctl_print_gvariant(value); gchar *escaped = g_markup_escape_text(printed, -1); GVariant *ret = g_variant_new("s", escaped); g_free(escaped); g_free(printed); return ret; } static GVariant *helperfn_default(struct token *token, GVariant **args, int nargs, GError **error) { if (nargs != 2) { g_set_error(error, playerctl_formatter_error_quark(), 1, "function default takes exactly two arguments (got %d)", nargs); return NULL; } if (args[0] == NULL && args[1] == NULL) { return NULL; } if (args[0] == NULL) { g_variant_ref(args[1]); return args[1]; } else { if (g_variant_is_of_type(args[0], G_VARIANT_TYPE_STRING) && strlen(g_variant_get_string(args[0], NULL)) == 0) { g_variant_ref(args[1]); return args[1]; } g_variant_ref(args[0]); return args[0]; } } static GVariant *helperfn_emoji(struct token *token, GVariant **args, int nargs, GError **error) { if (nargs != 1) { g_set_error(error, playerctl_formatter_error_quark(), 1, "function emoji takes exactly one argument (got %d)", nargs); return NULL; } GVariant *value = args[0]; if (value == NULL) { return g_variant_new("s", ""); } struct token *arg_token = g_list_first(token->args)->data; if (arg_token->type != TOKEN_VARIABLE) { g_set_error(error, playerctl_formatter_error_quark(), 1, "the emoji function can only be called with a variable"); return NULL; } gchar *key = arg_token->data; if (g_strcmp0(key, "status") == 0 && g_variant_is_of_type(value, G_VARIANT_TYPE_STRING)) { const gchar *status_str = g_variant_get_string(value, NULL); PlayerctlPlaybackStatus status = 0; if (pctl_parse_playback_status(status_str, &status)) { switch (status) { case PLAYERCTL_PLAYBACK_STATUS_PLAYING: return g_variant_new("s", "▶️"); case PLAYERCTL_PLAYBACK_STATUS_STOPPED: return g_variant_new("s", "⏹️"); case PLAYERCTL_PLAYBACK_STATUS_PAUSED: return g_variant_new("s", "⏸️"); } } } else if (g_strcmp0(key, "volume") == 0 && g_variant_is_of_type(value, G_VARIANT_TYPE_DOUBLE)) { const gdouble volume = g_variant_get_double(value); if (volume < 0.3333) { return g_variant_new("s", "🔈"); } else if (volume < 0.6666) { return g_variant_new("s", "🔉"); } else { return g_variant_new("s", "🔊"); } } g_variant_ref(value); return value; } static GVariant *helperfn_trunc(struct token *token, GVariant **args, int nargs, GError **error) { if (nargs != 2) { g_set_error(error, playerctl_formatter_error_quark(), 1, "function trunc takes exactly two arguments (got %d)", nargs); return NULL; } GVariant *value = args[0]; GVariant *len = args[1]; if (value == NULL || len == NULL) { return g_variant_new("s", ""); } if (!g_variant_type_equal(g_variant_get_type(len), G_VARIANT_TYPE_DOUBLE)) { g_set_error(error, playerctl_formatter_error_quark(), 1, "function trunc's length parameter can only be called with an int"); return NULL; } gchar *orig = pctl_print_gvariant(value); gchar *trunc = g_utf8_substring(orig, 0, g_variant_get_double(len)); GString *formatted = g_string_new(trunc); if (g_utf8_strlen(trunc, 256) < g_utf8_strlen(orig, 256)) { g_string_append(formatted, "…"); } gchar *formatted_inner = g_string_free(formatted, FALSE); GVariant *ret = g_variant_new("s", formatted_inner); g_free(formatted_inner); g_free(trunc); g_free(orig); return ret; } static gboolean is_valid_numeric_type(GVariant *value) { // This is all the types we know about for numeric operations. May be // expanded at a later time. MPRIS only uses INT64 and DOUBLE as numeric // types. Formatter constants are always DOUBLE. All other types are for // player workarounds. if (value == NULL) { return FALSE; } if (g_variant_is_of_type(value, G_VARIANT_TYPE_INT64)) { return TRUE; } else if (g_variant_is_of_type(value, G_VARIANT_TYPE_UINT64)) { return TRUE; } else if (g_variant_is_of_type(value, G_VARIANT_TYPE_DOUBLE)) { return TRUE; } return FALSE; } static gdouble get_double_value(GVariant *value) { // Keep this in sync with above is_value_numeric_type() if (g_variant_is_of_type(value, G_VARIANT_TYPE_INT64)) { return (gdouble)g_variant_get_int64(value); } else if (g_variant_is_of_type(value, G_VARIANT_TYPE_UINT64)) { return (gdouble)g_variant_get_uint64(value); } else if (g_variant_is_of_type(value, G_VARIANT_TYPE_DOUBLE)) { return g_variant_get_double(value); } else { assert(FALSE && "not reached"); } return 0.0; } static GVariant *infixfn_add(struct token *token, GVariant **args, int nargs, GError **error) { if (nargs == 1) { // unary addition if (!is_valid_numeric_type(args[0])) { g_set_error(error, playerctl_formatter_error_quark(), 1, "Got unsupported operand type for unary +: '%s'", g_variant_get_type_string(args[0])); return NULL; } g_variant_ref(args[0]); return args[0]; } if (nargs != 2) { g_set_error(error, playerctl_formatter_error_quark(), 1, "Addition takes two arguments (got %d). This is a bug in Playerctl.", nargs); return NULL; } if (args[0] == NULL || args[1] == NULL) { g_set_error(error, playerctl_formatter_error_quark(), 1, "Got unsupported operand type for +: NULL"); return NULL; } if (!is_valid_numeric_type(args[0]) || !is_valid_numeric_type(args[1])) { g_set_error(error, playerctl_formatter_error_quark(), 1, "Got unsupported operand types for +: '%s' and '%s'", g_variant_get_type_string(args[0]), g_variant_get_type_string(args[1])); return NULL; } if (g_variant_is_of_type(args[0], G_VARIANT_TYPE_INT64) && g_variant_is_of_type(args[1], G_VARIANT_TYPE_INT64)) { gint64 val0 = g_variant_get_int64(args[0]); gint64 val1 = g_variant_get_int64(args[1]); gint64 result = val0 + val1; if ((val0 > 0 && val1 > 0 && result < 0) || (val0 < 0 && val1 < 0 && result > 0)) { g_set_error(error, playerctl_formatter_error_quark(), 1, "Numeric overflow detected"); return NULL; } return g_variant_new("x", result); } gdouble val0 = get_double_value(args[0]); gdouble val1 = get_double_value(args[1]); gdouble result = val0 + val1; return g_variant_new("d", result); } static GVariant *infixfn_sub(struct token *token, GVariant **args, int nargs, GError **error) { if (nargs == 1) { // unary addition if (g_variant_is_of_type(args[0], G_VARIANT_TYPE_INT64)) { gint64 value = g_variant_get_int64(args[0]); return g_variant_new("x", value * -1); } else if (g_variant_is_of_type(args[0], G_VARIANT_TYPE_DOUBLE)) { gdouble value = g_variant_get_double(args[0]); return g_variant_new("d", value * -1); } else { g_set_error(error, playerctl_formatter_error_quark(), 1, "Got unsupported operand type for unary -: '%s'", g_variant_get_type_string(args[0])); return NULL; } } if (nargs != 2) { g_set_error(error, playerctl_formatter_error_quark(), 1, "Subtraction takes two arguments (got %d). This is a bug in Playerctl.", nargs); return NULL; } if (args[0] == NULL || args[1] == NULL) { g_set_error(error, playerctl_formatter_error_quark(), 1, "Got unsupported operand type for -: NULL"); return NULL; } if (!is_valid_numeric_type(args[0]) || !is_valid_numeric_type(args[1])) { g_set_error(error, playerctl_formatter_error_quark(), 1, "Got unsupported operand types for -: '%s' and '%s'", g_variant_get_type_string(args[0]), g_variant_get_type_string(args[1])); return NULL; } if (g_variant_is_of_type(args[0], G_VARIANT_TYPE_INT64) && g_variant_is_of_type(args[1], G_VARIANT_TYPE_INT64)) { gint64 val0 = g_variant_get_int64(args[0]); gint64 val1 = g_variant_get_int64(args[1]); gint64 result = val0 - val1; if ((val0 > 0 && val1 < 0 && result < 0) || (val0 < 0 && val1 > 0 && result > 0)) { g_set_error(error, playerctl_formatter_error_quark(), 1, "Numeric overflow detected"); return NULL; } return g_variant_new("x", result); } gdouble val0 = get_double_value(args[0]); gdouble val1 = get_double_value(args[1]); gdouble result = val0 - val1; return g_variant_new("d", result); } static GVariant *infixfn_mul(struct token *token, GVariant **args, int nargs, GError **error) { if (nargs != 2) { g_set_error(error, playerctl_formatter_error_quark(), 1, "Multiplication takes two arguments (got %d). This is a bug in Playerctl.", nargs); return NULL; } if (!is_valid_numeric_type(args[0]) || !is_valid_numeric_type(args[1])) { g_set_error(error, playerctl_formatter_error_quark(), 1, "Got unsupported operand types for *: '%s' and '%s'", g_variant_get_type_string(args[0]), g_variant_get_type_string(args[1])); return NULL; } if (g_variant_is_of_type(args[0], G_VARIANT_TYPE_INT64) && g_variant_is_of_type(args[1], G_VARIANT_TYPE_INT64)) { gint64 val0 = g_variant_get_int64(args[0]); gint64 val1 = g_variant_get_int64(args[1]); gint64 result = val0 * val1; if (val0 != 0 && val1 / val0 != val1) { g_set_error(error, playerctl_formatter_error_quark(), 1, "Numeric overflow detected"); return NULL; } return g_variant_new("x", result); } gdouble val0 = get_double_value(args[0]); gdouble val1 = get_double_value(args[1]); gdouble result = val0 * val1; return g_variant_new("d", result); } static GVariant *infixfn_div(struct token *token, GVariant **args, int nargs, GError **error) { if (nargs != 2) { g_set_error(error, playerctl_formatter_error_quark(), 1, "Division takes two arguments (got %d). This is a bug in Playerctl.", nargs); return NULL; } if (!is_valid_numeric_type(args[0]) || !is_valid_numeric_type(args[1])) { g_set_error(error, playerctl_formatter_error_quark(), 1, "Got unsupported operand types for /: '%s' and '%s'", g_variant_get_type_string(args[0]), g_variant_get_type_string(args[1])); return NULL; } if (g_variant_is_of_type(args[0], G_VARIANT_TYPE_INT64) && g_variant_is_of_type(args[1], G_VARIANT_TYPE_INT64)) { gint64 val0 = g_variant_get_int64(args[0]); gint64 val1 = g_variant_get_int64(args[1]); if (val1 == 0) { g_set_error(error, playerctl_formatter_error_quark(), 1, "Divide by zero error"); return NULL; } gint64 result = val0 / val1; return g_variant_new("x", result); } gdouble val0 = get_double_value(args[0]); gdouble val1 = get_double_value(args[1]); if (val1 == 0.0) { g_set_error(error, playerctl_formatter_error_quark(), 1, "Divide by zero error"); return NULL; } gdouble result = val0 / val1; return g_variant_new("d", result); } struct template_function { const gchar *name; GVariant *(*func)(struct token *token, GVariant **args, int nargs, GError **error); } template_functions[] = { {"lc", &helperfn_lc}, {"uc", &helperfn_uc}, {"duration", &helperfn_duration}, {"markup_escape", &helperfn_markup_escape}, {"default", &helperfn_default}, {"emoji", &helperfn_emoji}, {"trunc", &helperfn_trunc}, {INFIX_ADD, &infixfn_add}, {INFIX_SUB, &infixfn_sub}, {INFIX_MUL, &infixfn_mul}, {INFIX_DIV, &infixfn_div}, }; static GVariant *expand_token(struct token *token, GVariantDict *context, GError **error) { GError *tmp_error = NULL; switch (token->type) { case TOKEN_STRING: return g_variant_new("s", token->data); case TOKEN_NUMBER: return g_variant_new("d", token->numeric_data); case TOKEN_VARIABLE: if (g_variant_dict_contains(context, token->data)) { return g_variant_dict_lookup_value(context, token->data, NULL); } else { return NULL; } case TOKEN_FUNCTION: { // TODO lift required arg assumption assert(token->args != NULL); GVariant *ret = NULL; int nargs = 0; GVariant *args[MAX_ARGS + 1]; GList *t; for (t = token->args; t != NULL; t = t->next) { struct token *arg_token = t->data; assert(nargs < MAX_ARGS); args[nargs++] = expand_token(arg_token, context, &tmp_error); if (tmp_error != NULL) { g_propagate_error(error, tmp_error); goto func_out; } } for (gsize i = 0; i < LENGTH(template_functions); ++i) { if (g_strcmp0(template_functions[i].name, token->data) == 0) { ret = template_functions[i].func(token, args, nargs, &tmp_error); if (tmp_error != NULL) { g_propagate_error(error, tmp_error); goto func_out; } goto func_out; } } g_set_error(error, playerctl_formatter_error_quark(), 1, "unknown template function: %s", token->data); func_out: for (int i = 0; i < nargs; ++i) { if (args[i] != NULL) { g_variant_unref(args[i]); } } return ret; } } assert(FALSE && "not reached"); return NULL; } static gchar *expand_format(GList *tokens, GVariantDict *context, GError **error) { GError *tmp_error = NULL; GString *expanded; expanded = g_string_new(""); GList *t = tokens; for (t = tokens; t != NULL; t = t->next) { GVariant *value = expand_token(t->data, context, &tmp_error); if (tmp_error != NULL) { g_propagate_error(error, tmp_error); return NULL; } if (value != NULL) { gchar *result = pctl_print_gvariant(value); expanded = g_string_append(expanded, result); g_free(result); g_variant_unref(value); } } return g_string_free(expanded, FALSE); } static GVariantDict *get_default_template_context(PlayerctlPlayer *player, GVariant *base) { GVariantDict *context = g_variant_dict_new(base); if (!g_variant_dict_contains(context, "artist") && g_variant_dict_contains(context, "xesam:artist")) { GVariant *artist = g_variant_dict_lookup_value(context, "xesam:artist", NULL); g_variant_dict_insert_value(context, "artist", artist); g_variant_unref(artist); } if (!g_variant_dict_contains(context, "album") && g_variant_dict_contains(context, "xesam:album")) { GVariant *album = g_variant_dict_lookup_value(context, "xesam:album", NULL); g_variant_dict_insert_value(context, "album", album); g_variant_unref(album); } if (!g_variant_dict_contains(context, "title") && g_variant_dict_contains(context, "xesam:title")) { GVariant *title = g_variant_dict_lookup_value(context, "xesam:title", NULL); g_variant_dict_insert_value(context, "title", title); g_variant_unref(title); } if (!g_variant_dict_contains(context, "playerName")) { gchar *player_name = NULL; g_object_get(player, "player-name", &player_name, NULL); GVariant *player_name_variant = g_variant_new_string(player_name); g_variant_dict_insert_value(context, "playerName", player_name_variant); g_free(player_name); } if (!g_variant_dict_contains(context, "playerInstance")) { gchar *instance = NULL; g_object_get(player, "player-instance", &instance, NULL); GVariant *player_instance_variant = g_variant_new_string(instance); g_variant_dict_insert_value(context, "playerInstance", player_instance_variant); g_free(instance); } if (!g_variant_dict_contains(context, "shuffle")) { gboolean shuffle = FALSE; g_object_get(player, "shuffle", &shuffle, NULL); GVariant *shuffle_variant = g_variant_new_boolean(shuffle); g_variant_dict_insert_value(context, "shuffle", shuffle_variant); } if (!g_variant_dict_contains(context, "status")) { PlayerctlPlaybackStatus status = 0; g_object_get(player, "playback-status", &status, NULL); const gchar *status_str = pctl_playback_status_to_string(status); GVariant *status_variant = g_variant_new_string(status_str); g_variant_dict_insert_value(context, "status", status_variant); } if (!g_variant_dict_contains(context, "loop")) { PlayerctlLoopStatus status = 0; g_object_get(player, "loop-status", &status, NULL); const gchar *status_str = pctl_loop_status_to_string(status); GVariant *status_variant = g_variant_new_string(status_str); g_variant_dict_insert_value(context, "loop", status_variant); } if (!g_variant_dict_contains(context, "volume")) { gdouble level = 0.0; g_object_get(player, "volume", &level, NULL); GVariant *volume_variant = g_variant_new_double(level); g_variant_dict_insert_value(context, "volume", volume_variant); } if (!g_variant_dict_contains(context, "position")) { gint64 position = 0; g_object_get(player, "position", &position, NULL); GVariant *position_variant = g_variant_new_int64(position); g_variant_dict_insert_value(context, "position", position_variant); } return context; } PlayerctlFormatter *playerctl_formatter_new(const gchar *format, GError **error) { GError *tmp_error = NULL; GList *tokens = tokenize_format(format, &tmp_error); if (tmp_error != NULL) { g_propagate_error(error, tmp_error); return NULL; } PlayerctlFormatter *formatter = calloc(1, sizeof(PlayerctlFormatter)); formatter->priv = calloc(1, sizeof(PlayerctlFormatterPrivate)); formatter->priv->tokens = tokens; return formatter; } void playerctl_formatter_destroy(PlayerctlFormatter *formatter) { if (formatter == NULL) { return; } token_list_destroy(formatter->priv->tokens); free(formatter->priv); free(formatter); } gboolean playerctl_formatter_contains_key(PlayerctlFormatter *formatter, const gchar *key) { return token_list_contains_key(formatter->priv->tokens, key); } GVariantDict *playerctl_formatter_default_template_context(PlayerctlFormatter *formatter, PlayerctlPlayer *player, GVariant *base) { return get_default_template_context(player, base); } gchar *playerctl_formatter_expand_format(PlayerctlFormatter *formatter, GVariantDict *context, GError **error) { GError *tmp_error = NULL; gchar *expanded = expand_format(formatter->priv->tokens, context, &tmp_error); if (tmp_error != NULL) { g_propagate_error(error, tmp_error); return NULL; } return expanded; } playerctl-2.4.1/playerctl/playerctl-formatter.h000066400000000000000000000032431412234731200216660ustar00rootroot00000000000000/* * This file is part of playerctl. * * playerctl is free software: you can redistribute it and/or modify it under * the terms of the GNU Lesser General Public License as published by the Free * Software Foundation, either version 3 of the License, or (at your option) * any later version. * * playerctl is distributed in the hope that it will be useful, but WITHOUT ANY * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for * more details. * * You should have received a copy of the GNU Lesser General Public License * along with playerctl If not, see . * * Copyright © 2014, Tony Crisci and contributors */ #ifndef __PLAYERCTL_FORMATTER_H__ #define __PLAYERCTL_FORMATTER_H__ #include #include typedef struct _PlayerctlFormatter PlayerctlFormatter; typedef struct _PlayerctlFormatterPrivate PlayerctlFormatterPrivate; struct _PlayerctlFormatter { PlayerctlFormatterPrivate *priv; }; PlayerctlFormatter *playerctl_formatter_new(const gchar *format, GError **error); void playerctl_formatter_destroy(PlayerctlFormatter *formatter); gboolean playerctl_formatter_contains_key(PlayerctlFormatter *formatter, const gchar *key); GVariantDict *playerctl_formatter_default_template_context(PlayerctlFormatter *formatter, PlayerctlPlayer *player, GVariant *base); gchar *playerctl_formatter_expand_format(PlayerctlFormatter *formatter, GVariantDict *context, GError **error); #endif /* __PLAYERCTL_FORMATTER_H__ */ playerctl-2.4.1/playerctl/playerctl-player-manager.c000066400000000000000000000460031412234731200225630ustar00rootroot00000000000000/* * This file is part of playerctl. * * playerctl is free software: you can redistribute it and/or modify it under * the terms of the GNU Lesser General Public License as published by the Free * Software Foundation, either version 3 of the License, or (at your option) * any later version. * * playerctl is distributed in the hope that it will be useful, but WITHOUT ANY * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for * more details. * * You should have received a copy of the GNU Lesser General Public License * along with playerctl If not, see . * * Copyright © 2014, Tony Crisci and contributors. */ #include "playerctl/playerctl-player-manager.h" #include #include #include "playerctl/playerctl-common.h" #include "playerctl/playerctl-player-name.h" #include "playerctl/playerctl-player-private.h" #include "playerctl/playerctl-player.h" enum { PROP_0, PROP_PLAYERS, PROP_PLAYER_NAMES, N_PROPERTIES, }; enum { NAME_APPEARED, NAME_VANISHED, PLAYER_APPEARED, PLAYER_VANISHED, LAST_SIGNAL, }; static GParamSpec *obj_properties[N_PROPERTIES] = { NULL, }; static guint connection_signals[LAST_SIGNAL] = {0}; struct _PlayerctlPlayerManagerPrivate { gboolean initted; GError *init_error; GDBusProxy *session_proxy; GDBusProxy *system_proxy; GList *player_names; GList *players; GCompareDataFunc sort_func; gpointer sort_data; GDestroyNotify sort_notify; }; static void playerctl_player_manager_initable_iface_init(GInitableIface *iface); G_DEFINE_TYPE_WITH_CODE(PlayerctlPlayerManager, playerctl_player_manager, G_TYPE_OBJECT, G_ADD_PRIVATE(PlayerctlPlayerManager) G_IMPLEMENT_INTERFACE(G_TYPE_INITABLE, playerctl_player_manager_initable_iface_init)); static void playerctl_player_manager_set_property(GObject *object, guint property_id, const GValue *value, GParamSpec *pspec) { // PlayerctlPlayerManager *manager = PLAYERCTL_PLAYER_MANAGER(object); switch (property_id) { default: G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec); break; } } static void playerctl_player_manager_get_property(GObject *object, guint property_id, GValue *value, GParamSpec *pspec) { PlayerctlPlayerManager *manager = PLAYERCTL_PLAYER_MANAGER(object); switch (property_id) { case PROP_PLAYERS: g_value_set_pointer(value, manager->priv->players); break; case PROP_PLAYER_NAMES: g_value_set_pointer(value, manager->priv->player_names); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec); break; } } static void playerctl_player_manager_constructed(GObject *gobject) { PlayerctlPlayerManager *manager = PLAYERCTL_PLAYER_MANAGER(gobject); g_initable_init(G_INITABLE(manager), NULL, &manager->priv->init_error); G_OBJECT_CLASS(playerctl_player_manager_parent_class)->constructed(gobject); } static void playerctl_player_manager_dispose(GObject *gobject) { PlayerctlPlayerManager *manager = PLAYERCTL_PLAYER_MANAGER(gobject); g_clear_error(&manager->priv->init_error); g_clear_object(&manager->priv->session_proxy); g_clear_object(&manager->priv->system_proxy); G_OBJECT_CLASS(playerctl_player_manager_parent_class)->dispose(gobject); } static void playerctl_player_manager_finalize(GObject *gobject) { PlayerctlPlayerManager *manager = PLAYERCTL_PLAYER_MANAGER(gobject); g_list_free_full(manager->priv->player_names, (GDestroyNotify)playerctl_player_name_free); g_list_free_full(manager->priv->players, g_object_unref); G_OBJECT_CLASS(playerctl_player_manager_parent_class)->finalize(gobject); } static void playerctl_player_manager_class_init(PlayerctlPlayerManagerClass *klass) { GObjectClass *gobject_class = G_OBJECT_CLASS(klass); gobject_class->set_property = playerctl_player_manager_set_property; gobject_class->get_property = playerctl_player_manager_get_property; gobject_class->constructed = playerctl_player_manager_constructed; gobject_class->dispose = playerctl_player_manager_dispose; gobject_class->finalize = playerctl_player_manager_finalize; /** * PlayerctlPlayerManager:players: (transfer none) (type GList(PlayerctlPlayer)) * * A list of players that are currently connected and managed by this class. */ obj_properties[PROP_PLAYERS] = g_param_spec_pointer( "players", "players", "A list of player objects managed by this manager", G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); /** * PlayerctlPlayerManager:player-names: (transfer none) (type GList(PlayerctlPlayerName)) * * A list of fully qualified player names that are currently available to control. */ obj_properties[PROP_PLAYER_NAMES] = g_param_spec_pointer("player-names", "player names", "A list of player names that are currently available to control.", G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); g_object_class_install_properties(gobject_class, N_PROPERTIES, obj_properties); /** * PlayerctlPlayerManager::name-appeared: * @self: the #PlayerctlPlayerManager on which the signal was emitted * @name: A #PlayerctlPlayerName containing information about the name that * has appeared. * * Emitted when a new name has appeared and is available to connect to. Use * playerctl_player_new_from_name() to connect to the player and * playerctl_player_manager_manage_player() to add it to the managed list of * players. */ connection_signals[NAME_APPEARED] = g_signal_new( "name-appeared", PLAYERCTL_TYPE_PLAYER_MANAGER, G_SIGNAL_RUN_LAST, 0, NULL, NULL, g_cclosure_marshal_VOID__BOXED, G_TYPE_NONE, 1, PLAYERCTL_TYPE_PLAYER_NAME); /** * PlayerctlPlayerManager::name-vanished: * @self: the #PlayerctlPlayerManager on which this signal was emitted. * @name: The #PlayerctlPlayerName containing connection information about * the name that is going away. * * Emitted when the name has vanished and is no longer available to be * controlled by playerctl. If the player is managed, it will automatically * be removed from the list of players and the * #PlayerctlPlayerManager::player-vanished signal will be emitted * automatically. */ connection_signals[NAME_VANISHED] = g_signal_new( "name-vanished", PLAYERCTL_TYPE_PLAYER_MANAGER, G_SIGNAL_RUN_FIRST, 0, NULL, NULL, g_cclosure_marshal_VOID__BOXED, G_TYPE_NONE, 1, PLAYERCTL_TYPE_PLAYER_NAME); /** * PlayerctlPlayerManager::player-appeared: * @self: The #PlayerctlPlayerManager on which this event was emitted. * @player: The #PlayerctlPlayer that will be managed by this manager * * Emitted when a new player will be managed by this manager through a call * to playerctl_player_manager_manage_player(). */ connection_signals[PLAYER_APPEARED] = g_signal_new("player-appeared", PLAYERCTL_TYPE_PLAYER_MANAGER, G_SIGNAL_RUN_FIRST, 0, NULL, NULL, g_cclosure_marshal_VOID__OBJECT, G_TYPE_NONE, 1, PLAYERCTL_TYPE_PLAYER); /** * PlayerctlPlayerManager::player-vanished: * @self: The #PlayerctlPlayerManager on which this event was emitted. * @player: The #PlayerctlPlayer that will no longer be managed by this * manager * * Emitted when a player has disconnected and will no longer be managed by * this manager. The player is removed from the list of players * automatically. */ connection_signals[PLAYER_VANISHED] = g_signal_new("player-vanished", PLAYERCTL_TYPE_PLAYER_MANAGER, G_SIGNAL_RUN_FIRST, 0, NULL, NULL, g_cclosure_marshal_VOID__OBJECT, G_TYPE_NONE, 1, PLAYERCTL_TYPE_PLAYER); } static void playerctl_player_manager_init(PlayerctlPlayerManager *manager) { manager->priv = playerctl_player_manager_get_instance_private(manager); } static gchar *player_id_from_bus_name(const gchar *bus_name) { const size_t prefix_len = strlen(MPRIS_PREFIX); if (bus_name == NULL || !g_str_has_prefix(bus_name, MPRIS_PREFIX) || strlen(bus_name) <= prefix_len) { return NULL; } return g_strdup(bus_name + prefix_len); } static void manager_remove_managed_player_by_name(PlayerctlPlayerManager *manager, PlayerctlPlayerName *player_name) { GList *l = NULL; for (l = manager->priv->players; l != NULL; l = l->next) { PlayerctlPlayer *player = PLAYERCTL_PLAYER(l->data); gchar *instance = pctl_player_get_instance(player); // TODO match bus type if (g_strcmp0(instance, player_name->instance) == 0) { manager->priv->players = g_list_remove_link(manager->priv->players, l); g_debug("removing managed player: %s", instance); g_signal_emit(manager, connection_signals[PLAYER_VANISHED], 0, player); g_list_free_full(l, g_object_unref); break; } } } static void dbus_name_owner_changed_callback(GDBusProxy *proxy, gchar *sender_name, gchar *signal_name, GVariant *parameters, gpointer *data) { PlayerctlPlayerManager *manager = PLAYERCTL_PLAYER_MANAGER(data); if (g_strcmp0(signal_name, "NameOwnerChanged") != 0) { return; } if (!g_variant_is_of_type(parameters, G_VARIANT_TYPE("(sss)"))) { g_debug("Got unknown parameters on org.freedesktop.DBus " "NameOwnerChange signal: %s", g_variant_get_type_string(parameters)); return; } GVariant *name_variant = g_variant_get_child_value(parameters, 0); const gchar *name = g_variant_get_string(name_variant, NULL); gchar *player_id = player_id_from_bus_name(name); if (player_id == NULL) { g_variant_unref(name_variant); return; } GBusType bus_type = 0; if (proxy == manager->priv->session_proxy) { bus_type = G_BUS_TYPE_SESSION; } else if (proxy == manager->priv->system_proxy) { bus_type = G_BUS_TYPE_SYSTEM; } else { g_error("got unknown proxy in callback (this is a bug in playerctl)"); g_variant_unref(name_variant); return; } GVariant *previous_owner_variant = g_variant_get_child_value(parameters, 1); const gchar *previous_owner = g_variant_get_string(previous_owner_variant, NULL); GVariant *new_owner_variant = g_variant_get_child_value(parameters, 2); const gchar *new_owner = g_variant_get_string(new_owner_variant, NULL); GList *player_entry = NULL; if (strlen(new_owner) == 0 && strlen(previous_owner) != 0) { // the name has vanished player_entry = pctl_player_name_find(manager->priv->player_names, player_id, pctl_bus_type_to_source(bus_type)); if (player_entry != NULL) { PlayerctlPlayerName *player_name = player_entry->data; manager->priv->player_names = g_list_remove_link(manager->priv->player_names, player_entry); manager_remove_managed_player_by_name(manager, player_name); g_debug("player name vanished: %s", player_name->instance); g_signal_emit(manager, connection_signals[NAME_VANISHED], 0, player_name); pctl_player_name_list_destroy(player_entry); } } else if (strlen(previous_owner) == 0 && strlen(new_owner) != 0) { // the name has appeared player_entry = pctl_player_name_find(manager->priv->players, player_id, pctl_bus_type_to_source(bus_type)); if (player_entry == NULL) { PlayerctlPlayerName *player_name = pctl_player_name_new(player_id, pctl_bus_type_to_source(bus_type)); manager->priv->player_names = g_list_prepend(manager->priv->player_names, player_name); g_debug("player name appeared: %s", player_name->instance); g_signal_emit(manager, connection_signals[NAME_APPEARED], 0, player_name); } } g_free(player_id); g_variant_unref(name_variant); g_variant_unref(previous_owner_variant); g_variant_unref(new_owner_variant); } static gboolean playerctl_player_manager_initable_init(GInitable *initable, GCancellable *cancellable, GError **error) { GError *tmp_error = NULL; PlayerctlPlayerManager *manager = PLAYERCTL_PLAYER_MANAGER(initable); if (manager->priv->initted) { return TRUE; } manager->priv->session_proxy = g_dbus_proxy_new_for_bus_sync( G_BUS_TYPE_SESSION, G_DBUS_PROXY_FLAGS_NONE, NULL, "org.freedesktop.DBus", "/org/freedesktop/DBus", "org.freedesktop.DBus", NULL, &tmp_error); if (tmp_error != NULL) { if (tmp_error->domain == G_IO_ERROR && tmp_error->code == G_IO_ERROR_NOT_FOUND) { // TODO the bus address was set incorrectly so log a warning g_clear_error(&tmp_error); } else { g_propagate_error(error, tmp_error); return FALSE; } } manager->priv->system_proxy = g_dbus_proxy_new_for_bus_sync( G_BUS_TYPE_SYSTEM, G_DBUS_PROXY_FLAGS_NONE, NULL, "org.freedesktop.DBus", "/org/freedesktop/DBus", "org.freedesktop.DBus", NULL, &tmp_error); if (tmp_error != NULL) { if (tmp_error->domain == G_IO_ERROR && tmp_error->code == G_IO_ERROR_NOT_FOUND) { // TODO the bus address was set incorrectly so log a warning g_clear_error(&tmp_error); } else { g_propagate_error(error, tmp_error); return FALSE; } } manager->priv->player_names = playerctl_list_players(&tmp_error); if (tmp_error != NULL) { g_propagate_error(error, tmp_error); return FALSE; } if (manager->priv->session_proxy) { g_signal_connect(G_DBUS_PROXY(manager->priv->session_proxy), "g-signal", G_CALLBACK(dbus_name_owner_changed_callback), manager); } if (manager->priv->system_proxy) { g_signal_connect(G_DBUS_PROXY(manager->priv->system_proxy), "g-signal", G_CALLBACK(dbus_name_owner_changed_callback), manager); } manager->priv->initted = TRUE; return TRUE; } static void playerctl_player_manager_initable_iface_init(GInitableIface *iface) { iface->init = playerctl_player_manager_initable_init; } /** * playerctl_player_manager_new: * @err:(allow-none): The location of a GError or NULL. * * Create a new player manager that contains a list of player names available * in the #PlayerctlPlayerManager:player-names property. You can create new * players from the names with the playerctl_player_new_from_name() function * and then start managing them with the * playerctl_player_manager_manage_player() function. * * Returns:(transfer full): A new #PlayerctlPlayerManager. */ PlayerctlPlayerManager *playerctl_player_manager_new(GError **err) { GError *tmp_error = NULL; PlayerctlPlayerManager *manager = g_initable_new(PLAYERCTL_TYPE_PLAYER_MANAGER, NULL, &tmp_error, NULL); if (tmp_error != NULL) { g_propagate_error(err, tmp_error); return NULL; } return manager; } /** * playerctl_player_manager_set_sort_func: * @manager: A #PlayerctlPlayerManager. * @sort_func: The compare function to be used to sort the * #PlayerctlPlayerManager:players. * @sort_data:(allow-none): User data for the sort function. * @notify:(allow-none): A function to notify when the sort function will no * longer be used. * * Keeps the #PlayerctlPlayerManager:players list of this manager in sorted order which is useful * for using this list as a priority queue. */ void playerctl_player_manager_set_sort_func(PlayerctlPlayerManager *manager, 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; manager->priv->sort_data = sort_data; manager->priv->sort_notify = notify; manager->priv->players = g_list_sort_with_data(manager->priv->players, sort_func, sort_data); } /** * playerctl_player_manager_move_player_to_top: * @manager: A #PlayerctlPlayerManager * @player: A #PlayerctlPlayer in the list of #PlayerctlPlayerManager:players * * Moves the player to the top of the list of #PlayerctlPlayerManager:players. If this manager has a * sort function set with playerctl_player_manager_set_sort_func(), the list of * players will be sorted afterward, but will be on top of equal players in the * sorted order. */ void playerctl_player_manager_move_player_to_top(PlayerctlPlayerManager *manager, PlayerctlPlayer *player) { GList *l; for (l = manager->priv->players; l != NULL; l = l->next) { PlayerctlPlayer *current = PLAYERCTL_PLAYER(l->data); if (current == player) { manager->priv->players = g_list_remove_link(manager->priv->players, l); manager->priv->players = g_list_concat(l, manager->priv->players); if (manager->priv->sort_func) { manager->priv->players = g_list_sort_with_data( manager->priv->players, manager->priv->sort_func, manager->priv->sort_data); } break; } } } /** * playerctl_player_manager_manage_player: * @manager: A #PlayerctlPlayerManager * @player: A #PlayerctlPlayer to manage * * Add the given player to the list of managed players. Takes a reference to * the player (so you can unref it after you call this function). The player * will automatically be unreffed and removed from the list of * #PlayerctlPlayerManager:players when * it disconnects and the #PlayerctlPlayerManager::player-vanished signal will * be emitted on the manager. */ void playerctl_player_manager_manage_player(PlayerctlPlayerManager *manager, PlayerctlPlayer *player) { if (player == NULL) { return; } GList *l = NULL; for (l = manager->priv->players; l != NULL; l = l->next) { PlayerctlPlayer *current = PLAYERCTL_PLAYER(l->data); if (player == current) { return; } } if (manager->priv->sort_func) { manager->priv->players = g_list_insert_sorted_with_data( manager->priv->players, player, manager->priv->sort_func, manager->priv->sort_data); } else { manager->priv->players = g_list_prepend(manager->priv->players, player); } g_object_ref(player); g_debug("player appeared: %s", pctl_player_get_instance(player)); g_signal_emit(manager, connection_signals[PLAYER_APPEARED], 0, player); } playerctl-2.4.1/playerctl/playerctl-player-manager.h000066400000000000000000000106501412234731200225670ustar00rootroot00000000000000/* * This file is part of playerctl. * * playerctl is free software: you can redistribute it and/or modify it under * the terms of the GNU Lesser General Public License as published by the Free * Software Foundation, either version 3 of the License, or (at your option) * any later version. * * playerctl is distributed in the hope that it will be useful, but WITHOUT ANY * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for * more details. * * You should have received a copy of the GNU Lesser General Public License * along with playerctl If not, see . * * Copyright © 2014, Tony Crisci */ #ifndef __PLAYERCTL_PLAYER_MANAGER_H__ #define __PLAYERCTL_PLAYER_MANAGER_H__ #if !defined(__PLAYERCTL_INSIDE__) && !defined(PLAYERCTL_COMPILATION) #error "Only can be included directly." #endif #include #include /** * SECTION: playerctl-player-manager * @short_description: A class to watch for players appearing and vanishing. * * The #PlayerctlPlayerManager is a class to watch for players appearing and * vanishing. When a player opens and is available to control by `playerctl`, * the #PlayerctlPlayerManager::name-appeared event will be emitted on the * manager during the main loop. You can inspect this #PlayerctlPlayerName to * see if you want to manage it. If you do, create a #PlayerctlPlayer from it * with the playerctl_player_new_from_name() function. The manager is also * capable of keeping an up-to-date list of players you want it to manage in * the #PlayerctlPlayerManager:players list. These players are connected and * should be able to be controlled. Managing players is optional, and you can * do so manually if you like. * * When the player disconnects, the #PlayerctlPlayerManager::name-vanished * event will be emitted. If the player is managed and is going to be removed * from the list, the #PlayerctlPlayerManager::player-vanished event will also * be emitted. After this event, the player will be cleaned up and removed from * the manager. * * The manager has other features such as being able to keep the players in a * sorted order and moving a player to the top of the list. The * #PlayerctlPlayerManager:player-names will always be in the order that they * were known to appear after the manager was created. * * For examples on how to use the manager, see the `examples` folder in the git * repository. */ #define PLAYERCTL_TYPE_PLAYER_MANAGER (playerctl_player_manager_get_type()) #define PLAYERCTL_PLAYER_MANAGER(obj) \ (G_TYPE_CHECK_INSTANCE_CAST((obj), PLAYERCTL_TYPE_PLAYER_MANAGER, PlayerctlPlayerManager)) #define PLAYERCTL_IS_PLAYER_MANAGER(obj) \ (G_TYPE_CHECK_INSTANCE_TYPE((obj), PLAYERCTL_TYPE_PLAYER_MANAGER)) #define PLAYERCTL_PLAYER_MANAGER_CLASS(klass) \ (G_TYPE_CHECK_CLASS_CAST((klass), PLAYERCTL_TYPE_PLAYER_MANAGER, PlayerctlPlayerManagerClass)) #define PLAYERCTL_IS_PLAYER_MANAGER_CLASS(klass) \ (G_TYPE_CHECK_CLASS_TYPE((klass), PLAYERCTL_TYPE_PLAYER_MANAGER)) #define PLAYERCTL_PLAYER_MANAGER_GET_CLASS(obj) \ (G_TYPE_INSTANCE_GET_CLASS((obj), PLAYERCTL_TYPE_PLAYER_MANAGER, PlayerctlPlayerManagerClass)) typedef struct _PlayerctlPlayerManager PlayerctlPlayerManager; typedef struct _PlayerctlPlayerManagerClass PlayerctlPlayerManagerClass; typedef struct _PlayerctlPlayerManagerPrivate PlayerctlPlayerManagerPrivate; struct _PlayerctlPlayerManager { /* Parent instance structure */ GObject parent_instance; /* Private members */ PlayerctlPlayerManagerPrivate *priv; }; struct _PlayerctlPlayerManagerClass { /* Parent class structure */ GObjectClass parent_class; }; GType playerctl_player_manager_get_type(void); PlayerctlPlayerManager *playerctl_player_manager_new(GError **err); 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, GDestroyNotify notify); void playerctl_player_manager_move_player_to_top(PlayerctlPlayerManager *manager, PlayerctlPlayer *player); #endif /* __PLAYERCTL_PLAYER_MANAGER_H__ */ playerctl-2.4.1/playerctl/playerctl-player-name.c000066400000000000000000000035251412234731200220730ustar00rootroot00000000000000/* * This file is part of playerctl. * * playerctl is free software: you can redistribute it and/or modify it under * the terms of the GNU Lesser General Public License as published by the Free * Software Foundation, either version 3 of the License, or (at your option) * any later version. * * playerctl is distributed in the hope that it will be useful, but WITHOUT ANY * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for * more details. * * You should have received a copy of the GNU Lesser General Public License * along with playerctl If not, see . * * Copyright © 2014, Tony Crisci and contributors. */ #include "playerctl-player-name.h" /** * playerctl_player_name_copy: * @name: a #PlayerctlPlayerName * * Creates a dynamically allocated name name container as a copy of * @name. * * Returns: (transfer full): a newly-allocated copy of @name */ PlayerctlPlayerName *playerctl_player_name_copy(PlayerctlPlayerName *name) { PlayerctlPlayerName *retval; g_return_val_if_fail(name != NULL, NULL); retval = g_slice_new0(PlayerctlPlayerName); *retval = *name; retval->source = name->source; retval->instance = g_strdup(name->instance); retval->name = g_strdup(name->name); return retval; } /** * playerctl_player_name_free: * @name:(allow-none): a #PlayerctlPlayerName * * Frees @name. If @name is %NULL, it simply returns. */ void playerctl_player_name_free(PlayerctlPlayerName *name) { if (name == NULL) { return; } g_free(name->instance); g_free(name->name); g_slice_free(PlayerctlPlayerName, name); } G_DEFINE_BOXED_TYPE(PlayerctlPlayerName, playerctl_player_name, playerctl_player_name_copy, playerctl_player_name_free); playerctl-2.4.1/playerctl/playerctl-player-name.h000066400000000000000000000056031412234731200220770ustar00rootroot00000000000000/* * This file is part of playerctl. * * playerctl is free software: you can redistribute it and/or modify it under * the terms of the GNU Lesser General Public License as published by the Free * Software Foundation, either version 3 of the License, or (at your option) * any later version. * * playerctl is distributed in the hope that it will be useful, but WITHOUT ANY * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for * more details. * * You should have received a copy of the GNU Lesser General Public License * along with playerctl If not, see . * * Copyright © 2014, Tony Crisci and contributors */ #ifndef __PLAYERCTL_PLAYER_NAME_H__ #define __PLAYERCTL_PLAYER_NAME_H__ #include #include /** * SECTION: playerctl-player-name * @short_description: Contains connection information that fully qualifies a * potential connection to a player. * * Contains connection information that fully qualifies a potential connection * to a player. You should not have to construct one of these directly. You can * list the names that are available to control from the * playerctl_list_players() function or use the * #PlayerctlPlayerManager:player-names property from a * #PlayerctlPlayerManager. * * Once you have gotten a player name like this, you can check the type of * player with the "name" property to see if you are interested in connecting * to it. If you are, you can pass it directly to the * playerctl_player_new_from_name() function to get a #PlayerctlPlayer that is * connected to this name and ready to command and query. */ /** * PlayerctlSource * @PLAYERCTL_SOURCE_NONE: Only for unitialized players. Source will be chosen automatically. * @PLAYERCTL_SOURCE_DBUS_SESSION: The player is on the DBus session bus. * @PLAYERCTL_SOURCE_DBUS_SYSTEM: The player is on the DBus system bus. * * The source of the name used to control the player. * */ typedef enum { PLAYERCTL_SOURCE_NONE, PLAYERCTL_SOURCE_DBUS_SESSION, PLAYERCTL_SOURCE_DBUS_SYSTEM, } PlayerctlSource; typedef struct _PlayerctlPlayerName PlayerctlPlayerName; #define PLAYERCTL_TYPE_PLAYER_NAME (playerctl_player_name_get_type()) void playerctl_player_name_free(PlayerctlPlayerName *name); PlayerctlPlayerName *playerctl_player_name_copy(PlayerctlPlayerName *name); GType playerctl_player_name_get_type(void); /** * PlayerctlPlayerName: * @name: the name of the type of player. * @instance: the complete name and instance of the player. * @source: the source of the player name. * * Event container for when names of players appear or disapear as the * controllable media player applications open and close. */ struct _PlayerctlPlayerName { gchar *name; gchar *instance; PlayerctlSource source; }; #endif /* __PLAYERCTL_PLAYER_NAME_H__ */ playerctl-2.4.1/playerctl/playerctl-player-private.h000066400000000000000000000023141412234731200226250ustar00rootroot00000000000000/* * This file is part of playerctl. * * playerctl is free software: you can redistribute it and/or modify it under * the terms of the GNU Lesser General Public License as published by the Free * Software Foundation, either version 3 of the License, or (at your option) * any later version. * * playerctl is distributed in the hope that it will be useful, but WITHOUT ANY * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for * more details. * * You should have received a copy of the GNU Lesser General Public License * along with playerctl If not, see . * * Copyright © 2014, Tony Crisci and contributors */ #ifndef __PLAYERCTL_PLAYER_PRIVATE_H__ #define __PLAYERCTL_PLAYER_PRIVATE_H__ #include "playerctl-player.h" 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__ */ playerctl-2.4.1/playerctl/playerctl-player.c000066400000000000000000001736531412234731200211670ustar00rootroot00000000000000/* * This file is part of playerctl. * * playerctl is free software: you can redistribute it and/or modify it under * the terms of the GNU Lesser General Public License as published by the Free * Software Foundation, either version 3 of the License, or (at your option) * any later version. * * playerctl is distributed in the hope that it will be useful, but WITHOUT ANY * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for * more details. * * You should have received a copy of the GNU Lesser General Public License * along with playerctl If not, see . * * Copyright © 2014, Tony Crisci and contributors. */ #include "playerctl-player.h" #include #include #include #include #include #include #include #include "playerctl-common.h" #include "playerctl-generated.h" #define LENGTH(array) (sizeof array / sizeof array[0]) #define MPRIS_PATH "/org/mpris/MediaPlayer2" #define PROPERTIES_IFACE "org.freedesktop.DBus.Properties" #define PLAYER_IFACE "org.mpris.MediaPlayer2.Player" #define SET_MEMBER "Set" enum { PROP_0, PROP_PLAYER_NAME, PROP_PLAYER_INSTANCE, PROP_SOURCE, PROP_PLAYBACK_STATUS, PROP_LOOP_STATUS, PROP_SHUFFLE, PROP_STATUS, // deprecated PROP_VOLUME, PROP_METADATA, PROP_POSITION, PROP_CAN_CONTROL, PROP_CAN_PLAY, PROP_CAN_PAUSE, PROP_CAN_SEEK, PROP_CAN_GO_NEXT, PROP_CAN_GO_PREVIOUS, N_PROPERTIES }; enum { // PROPERTIES_CHANGED, PLAYBACK_STATUS, LOOP_STATUS, SHUFFLE, PLAY, // deprecated PAUSE, // deprecated STOP, // deprecated METADATA, VOLUME, SEEKED, EXIT, LAST_SIGNAL }; static GParamSpec *obj_properties[N_PROPERTIES] = { NULL, }; static guint connection_signals[LAST_SIGNAL] = {0}; struct _PlayerctlPlayerPrivate { OrgMprisMediaPlayer2Player *proxy; gchar *player_name; gchar *instance; gchar *bus_name; PlayerctlSource source; GError *init_error; gboolean initted; PlayerctlPlaybackStatus cached_status; gint64 cached_position; gchar *cached_track_id; struct timespec cached_position_monotonic; }; static inline int64_t timespec_to_usec(const struct timespec *a) { return (int64_t)a->tv_sec * 1e+6 + a->tv_nsec / 1000; } static gint64 calculate_cached_position(PlayerctlPlaybackStatus status, struct timespec *position_monotonic, gint64 position) { gint64 offset = 0; struct timespec current_time; switch (status) { case PLAYERCTL_PLAYBACK_STATUS_PLAYING: clock_gettime(CLOCK_MONOTONIC, ¤t_time); offset = timespec_to_usec(¤t_time) - timespec_to_usec(position_monotonic); return position + offset; case PLAYERCTL_PLAYBACK_STATUS_PAUSED: return position; default: return 0; } } static gchar *metadata_get_track_id(GVariant *metadata) { GVariant *track_id_variant = g_variant_lookup_value(metadata, "mpris:trackid", G_VARIANT_TYPE_OBJECT_PATH); if (track_id_variant == NULL) { // XXX some players set this as a string, which is against the protocol, // but a lot of them do it and I don't feel like fixing it on all the // players in the world. g_debug("mpris:trackid is a string, not a D-Bus object reference"); track_id_variant = g_variant_lookup_value(metadata, "mpris:trackid", G_VARIANT_TYPE_STRING); } if (track_id_variant != NULL) { const gchar *track_id = g_variant_get_string(track_id_variant, NULL); g_variant_unref(track_id_variant); return g_strdup(track_id); } return NULL; } static void playerctl_player_properties_changed_callback(GDBusProxy *_proxy, GVariant *changed_properties, const gchar *const *invalidated_properties, gpointer user_data) { g_debug("%s", g_variant_print(changed_properties, TRUE)); PlayerctlPlayer *self = user_data; gchar *instance = self->priv->instance; g_debug("%s: properties changed", instance); // TODO probably need to replace this with an iterator GVariant *metadata = g_variant_lookup_value(changed_properties, "Metadata", NULL); GVariant *playback_status = g_variant_lookup_value(changed_properties, "PlaybackStatus", NULL); GVariant *loop_status = g_variant_lookup_value(changed_properties, "LoopStatus", NULL); GVariant *volume = g_variant_lookup_value(changed_properties, "Volume", NULL); GVariant *shuffle = g_variant_lookup_value(changed_properties, "Shuffle", NULL); if (shuffle != NULL) { gboolean shuffle_value = g_variant_get_boolean(shuffle); g_debug("%s: shuffle value set to %s", instance, shuffle_value ? "true" : "false"); g_signal_emit(self, connection_signals[SHUFFLE], 0, shuffle_value); g_variant_unref(shuffle); } if (volume != NULL) { gdouble volume_value = g_variant_get_double(volume); g_debug("%s: volume set to %f", instance, volume_value); g_signal_emit(self, connection_signals[VOLUME], 0, volume_value); g_variant_unref(volume); } gboolean track_id_invalidated = FALSE; if (metadata != NULL) { // update the cached track id gchar *track_id = metadata_get_track_id(metadata); if ((track_id == NULL && self->priv->cached_track_id != NULL) || (track_id != NULL && self->priv->cached_track_id == NULL) || (g_strcmp0(track_id, self->priv->cached_track_id) != 0)) { g_free(self->priv->cached_track_id); g_debug("%s: track id updated to %s", instance, track_id); self->priv->cached_track_id = track_id; track_id_invalidated = TRUE; } else { g_free(track_id); } g_debug("%s: metadata changed", instance); // g_debug("metadata: %s", g_variant_print(metadata, TRUE)); g_signal_emit(self, connection_signals[METADATA], 0, metadata); g_variant_unref(metadata); } if (track_id_invalidated) { self->priv->cached_position = 0; clock_gettime(CLOCK_MONOTONIC, &self->priv->cached_position_monotonic); } if (playback_status == NULL && track_id_invalidated) { // XXX: Lots of player aren't setting status correctly when the track // changes so we have to get it from the interface. We should // definitely go fix this bug on the players. g_debug("Playback status not set on track change; getting status from interface instead"); GVariant *call_reply = g_dbus_proxy_call_sync( G_DBUS_PROXY(self->priv->proxy), "org.freedesktop.DBus.Properties.Get", g_variant_new("(ss)", "org.mpris.MediaPlayer2.Player", "PlaybackStatus"), G_DBUS_CALL_FLAGS_NONE, -1, NULL, NULL); if (call_reply != NULL) { GVariant *call_reply_box = g_variant_get_child_value(call_reply, 0); playback_status = g_variant_get_child_value(call_reply_box, 0); g_variant_unref(call_reply); g_variant_unref(call_reply_box); } } if (loop_status != NULL) { const gchar *status_str = g_variant_get_string(loop_status, NULL); PlayerctlLoopStatus status = 0; GQuark quark = 0; if (pctl_parse_loop_status(status_str, &status)) { switch (status) { case PLAYERCTL_LOOP_STATUS_TRACK: quark = g_quark_from_string("track"); break; case PLAYERCTL_LOOP_STATUS_PLAYLIST: quark = g_quark_from_string("playlist"); break; case PLAYERCTL_LOOP_STATUS_NONE: quark = g_quark_from_string("none"); break; } g_debug("%s: loop status set to %s", instance, g_quark_to_string(quark)); g_signal_emit(self, connection_signals[LOOP_STATUS], quark, status); } g_variant_unref(loop_status); } if (playback_status != NULL) { const gchar *status_str = g_variant_get_string(playback_status, NULL); g_debug("%s: playback status set to %s", instance, status_str); PlayerctlPlaybackStatus status = 0; GQuark quark = 0; if (pctl_parse_playback_status(status_str, &status)) { switch (status) { case PLAYERCTL_PLAYBACK_STATUS_PLAYING: quark = g_quark_from_string("playing"); if (self->priv->cached_status != PLAYERCTL_PLAYBACK_STATUS_PLAYING) { clock_gettime(CLOCK_MONOTONIC, &self->priv->cached_position_monotonic); } g_signal_emit(self, connection_signals[PLAY], 0); break; case PLAYERCTL_PLAYBACK_STATUS_PAUSED: quark = g_quark_from_string("paused"); self->priv->cached_position = calculate_cached_position( self->priv->cached_status, &self->priv->cached_position_monotonic, self->priv->cached_position); // DEPRECATED g_signal_emit(self, connection_signals[PAUSE], 0); break; case PLAYERCTL_PLAYBACK_STATUS_STOPPED: self->priv->cached_position = 0; quark = g_quark_from_string("stopped"); // DEPRECATED g_signal_emit(self, connection_signals[STOP], 0); break; } if (self->priv->cached_status != status) { self->priv->cached_status = status; g_signal_emit(self, connection_signals[PLAYBACK_STATUS], quark, status); } } else { g_debug("%s: got unknown playback state: %s", instance, status_str); } g_variant_unref(playback_status); } } static void playerctl_player_seeked_callback(GDBusProxy *_proxy, gint64 position, gpointer *user_data) { PlayerctlPlayer *player = PLAYERCTL_PLAYER(user_data); player->priv->cached_position = position; g_debug("%s: new player position %ld", player->priv->instance, position); clock_gettime(CLOCK_MONOTONIC, &player->priv->cached_position_monotonic); g_signal_emit(player, connection_signals[SEEKED], 0, position); } static void playerctl_player_initable_iface_init(GInitableIface *iface); G_DEFINE_TYPE_WITH_CODE(PlayerctlPlayer, playerctl_player, G_TYPE_OBJECT, G_ADD_PRIVATE(PlayerctlPlayer) G_IMPLEMENT_INTERFACE(G_TYPE_INITABLE, playerctl_player_initable_iface_init)); // clang-format off G_DEFINE_QUARK(playerctl-player-error-quark, playerctl_player_error); // clang-format on static GVariant *playerctl_player_get_metadata(PlayerctlPlayer *self, GError **err) { GVariant *metadata; GError *tmp_error = NULL; metadata = org_mpris_media_player2_player_dup_metadata(self->priv->proxy); if (!metadata) { // XXX: Ugly spotify workaround. Spotify does not seem to use the property // cache. We have to get the properties directly. g_debug("Spotify does not use the D-Bus property cache, getting properties directly"); GVariant *call_reply = g_dbus_proxy_call_sync( G_DBUS_PROXY(self->priv->proxy), "org.freedesktop.DBus.Properties.Get", g_variant_new("(ss)", "org.mpris.MediaPlayer2.Player", "Metadata"), G_DBUS_CALL_FLAGS_NONE, -1, NULL, &tmp_error); if (tmp_error != NULL) { g_propagate_error(err, tmp_error); return NULL; } GVariant *call_reply_properties = g_variant_get_child_value(call_reply, 0); metadata = g_variant_get_child_value(call_reply_properties, 0); g_variant_unref(call_reply); g_variant_unref(call_reply_properties); } return metadata; } static void playerctl_player_set_property(GObject *object, guint property_id, const GValue *value, GParamSpec *pspec) { PlayerctlPlayer *self = PLAYERCTL_PLAYER(object); switch (property_id) { case PROP_PLAYER_NAME: g_free(self->priv->player_name); self->priv->player_name = g_strdup(g_value_get_string(value)); break; case PROP_PLAYER_INSTANCE: g_free(self->priv->instance); self->priv->instance = g_strdup(g_value_get_string(value)); break; case PROP_SOURCE: self->priv->source = g_value_get_enum(value); break; case PROP_VOLUME: g_warning("setting the volume property directly is deprecated and will " "be removed in a future version. Use " "playerctl_player_set_volume() instead."); org_mpris_media_player2_player_set_volume(self->priv->proxy, g_value_get_double(value)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec); break; } } static void playerctl_player_get_property(GObject *object, guint property_id, GValue *value, GParamSpec *pspec) { PlayerctlPlayer *self = PLAYERCTL_PLAYER(object); switch (property_id) { case PROP_PLAYER_NAME: g_value_set_string(value, self->priv->player_name); break; case PROP_PLAYER_INSTANCE: g_value_set_string(value, self->priv->instance); break; case PROP_SOURCE: g_value_set_enum(value, self->priv->source); break; case PROP_PLAYBACK_STATUS: g_value_set_enum(value, self->priv->cached_status); break; case PROP_LOOP_STATUS: { const gchar *status_str = org_mpris_media_player2_player_get_loop_status(self->priv->proxy); PlayerctlLoopStatus status = 0; if (pctl_parse_loop_status(status_str, &status)) { g_value_set_enum(value, status); } else { if (status_str != NULL) { g_debug("got unknown loop status: %s", status_str); } g_value_set_enum(value, PLAYERCTL_LOOP_STATUS_NONE); } break; } case PROP_SHUFFLE: { if (self->priv->proxy == NULL) { g_value_set_boolean(value, FALSE); break; } g_value_set_boolean(value, org_mpris_media_player2_player_get_shuffle(self->priv->proxy)); break; } case PROP_STATUS: // DEPRECATED if (self->priv->proxy) { g_value_set_string(value, pctl_playback_status_to_string(self->priv->cached_status)); } else { g_value_set_string(value, ""); } break; case PROP_METADATA: { GError *error = NULL; GVariant *metadata = NULL; metadata = playerctl_player_get_metadata(self, &error); if (error != NULL) { g_error("could not get metadata: %s", error->message); g_clear_error(&error); } g_value_set_variant(value, metadata); break; } case PROP_VOLUME: if (self->priv->proxy) { g_value_set_double(value, org_mpris_media_player2_player_get_volume(self->priv->proxy)); } else { g_value_set_double(value, 0); } break; case PROP_POSITION: { gint64 position = calculate_cached_position(self->priv->cached_status, &self->priv->cached_position_monotonic, self->priv->cached_position); g_value_set_int64(value, position); break; } case PROP_CAN_CONTROL: if (self->priv->proxy == NULL) { g_value_set_boolean(value, FALSE); break; } g_value_set_boolean(value, org_mpris_media_player2_player_get_can_control(self->priv->proxy)); break; case PROP_CAN_PLAY: if (self->priv->proxy == NULL) { g_value_set_boolean(value, FALSE); break; } g_value_set_boolean(value, org_mpris_media_player2_player_get_can_play(self->priv->proxy)); break; case PROP_CAN_PAUSE: if (self->priv->proxy == NULL) { g_value_set_boolean(value, FALSE); break; } g_value_set_boolean(value, org_mpris_media_player2_player_get_can_pause(self->priv->proxy)); break; case PROP_CAN_SEEK: if (self->priv->proxy == NULL) { g_value_set_boolean(value, FALSE); break; } g_value_set_boolean(value, org_mpris_media_player2_player_get_can_seek(self->priv->proxy)); break; case PROP_CAN_GO_NEXT: if (self->priv->proxy == NULL) { g_value_set_boolean(value, FALSE); break; } g_value_set_boolean(value, org_mpris_media_player2_player_get_can_go_next(self->priv->proxy)); break; case PROP_CAN_GO_PREVIOUS: if (self->priv->proxy == NULL) { g_value_set_boolean(value, FALSE); break; } g_value_set_boolean(value, org_mpris_media_player2_player_get_can_go_previous(self->priv->proxy)); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec); break; } } static void playerctl_player_constructed(GObject *gobject) { PlayerctlPlayer *self = PLAYERCTL_PLAYER(gobject); self->priv->init_error = NULL; g_initable_init((GInitable *)self, NULL, &self->priv->init_error); G_OBJECT_CLASS(playerctl_player_parent_class)->constructed(gobject); } static void playerctl_player_dispose(GObject *gobject) { PlayerctlPlayer *self = PLAYERCTL_PLAYER(gobject); g_clear_error(&self->priv->init_error); g_clear_object(&self->priv->proxy); G_OBJECT_CLASS(playerctl_player_parent_class)->dispose(gobject); } static void playerctl_player_finalize(GObject *gobject) { PlayerctlPlayer *self = PLAYERCTL_PLAYER(gobject); g_free(self->priv->player_name); g_free(self->priv->instance); g_free(self->priv->cached_track_id); g_free(self->priv->bus_name); G_OBJECT_CLASS(playerctl_player_parent_class)->finalize(gobject); } static void playerctl_player_class_init(PlayerctlPlayerClass *klass) { GObjectClass *gobject_class = G_OBJECT_CLASS(klass); gobject_class->set_property = playerctl_player_set_property; gobject_class->get_property = playerctl_player_get_property; gobject_class->constructed = playerctl_player_constructed; gobject_class->dispose = playerctl_player_dispose; gobject_class->finalize = playerctl_player_finalize; obj_properties[PROP_PLAYER_NAME] = g_param_spec_string("player-name", "Player name", "The name of the type of player this is. " "The instance is fully qualified with the player-instance and the " "source.", NULL, /* default */ G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS); obj_properties[PROP_PLAYER_INSTANCE] = g_param_spec_string("player-instance", "Player instance", "An instance name that identifies " "this player on the source", NULL, /* default */ G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS); obj_properties[PROP_SOURCE] = g_param_spec_enum("source", "Player source", "The source of this player. Currently supported " "sources are the DBus session bus and DBus system bus.", playerctl_source_get_type(), G_BUS_TYPE_NONE, G_PARAM_CONSTRUCT_ONLY | G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); obj_properties[PROP_PLAYBACK_STATUS] = g_param_spec_enum( "playback-status", "Player playback status", "Whether the player is playing, paused, or stopped", playerctl_playback_status_get_type(), PLAYERCTL_PLAYBACK_STATUS_STOPPED, G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); obj_properties[PROP_LOOP_STATUS] = g_param_spec_enum("loop-status", "Player loop status", "The loop status of the player", playerctl_loop_status_get_type(), PLAYERCTL_LOOP_STATUS_NONE, G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); obj_properties[PROP_SHUFFLE] = g_param_spec_boolean( "shuffle", "Shuffle", "A value of false indicates that playback is " "progressing linearly through a playlist, while true means playback is " "progressing through a playlist in some other order.", FALSE, G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); /** * PlayerctlPlayer:status: * * The playback status of the player as a string * * Deprecated:2.0.0: Use the "playback-status" signal instead. */ obj_properties[PROP_STATUS] = g_param_spec_string("status", "Player status", "The play status of the player (deprecated: use " "playback-status)", NULL, /* default */ G_PARAM_READABLE | G_PARAM_STATIC_STRINGS | G_PARAM_DEPRECATED); obj_properties[PROP_VOLUME] = g_param_spec_double( "volume", "Player volume", "The volume level of the player. Setting " "this property directly is deprecated and this property will become read " "only in a future version. Use playerctl_player_set_volume() to set the " "volume.", 0, 100, 0, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS); obj_properties[PROP_POSITION] = g_param_spec_int64("position", "Player position", "The position in the current track of the player in microseconds", 0, INT64_MAX, 0, G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); obj_properties[PROP_METADATA] = g_param_spec_variant( "metadata", "Player metadata", "The metadata of the currently playing track as an array of key-value " "pairs. The metadata available depends on the track, but may include the " "artist, title, length, art url, and other metadata.", g_variant_type_new("a{sv}"), NULL, G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); obj_properties[PROP_CAN_CONTROL] = g_param_spec_boolean( "can-control", "Can control", "Whether the player can be controlled by playerctl", FALSE, G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); obj_properties[PROP_CAN_PLAY] = g_param_spec_boolean("can-play", "Can play", "Whether the player can start playing and has a " "current track.", FALSE, G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); obj_properties[PROP_CAN_PAUSE] = g_param_spec_boolean("can-pause", "Can pause", "Whether the player can pause", FALSE, G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); obj_properties[PROP_CAN_SEEK] = g_param_spec_boolean( "can-seek", "Can seek", "Whether the position of the player can be controlled", FALSE, G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); obj_properties[PROP_CAN_GO_NEXT] = g_param_spec_boolean( "can-go-next", "Can go next", "Whether the player can go to the next track", FALSE, G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); obj_properties[PROP_CAN_GO_PREVIOUS] = g_param_spec_boolean( "can-go-previous", "Can go previous", "Whether the player can go to the previous track", FALSE, G_PARAM_READABLE | G_PARAM_STATIC_STRINGS); g_object_class_install_properties(gobject_class, N_PROPERTIES, obj_properties); /** * PlayerctlPlayer::playback-status: * @player: the player this event was emitted on * @playback_status: the playback status of the player * * Emitted when the playback status changes. Detail will be "playing", * "paused", or "stopped" which you can listen to by connecting to the * "playback-status::[STATUS]" signal. */ connection_signals[PLAYBACK_STATUS] = g_signal_new("playback-status", /* signal_name */ PLAYERCTL_TYPE_PLAYER, /* itype */ G_SIGNAL_RUN_FIRST | G_SIGNAL_DETAILED, /* signal_flags */ 0, /* class_offset */ NULL, /* accumulator */ NULL, /* accu_data */ g_cclosure_marshal_VOID__ENUM, /* c_marshaller */ G_TYPE_NONE, /* return_type */ 1, /* n_params */ playerctl_playback_status_get_type()); /** * PlayerctlPlayer::loop-status: * @player: the player this event was emitted on * @loop_status: the loop status of the player * * Emitted when the loop status changes. */ connection_signals[LOOP_STATUS] = g_signal_new("loop-status", /* signal_name */ PLAYERCTL_TYPE_PLAYER, /* itype */ G_SIGNAL_RUN_FIRST | G_SIGNAL_DETAILED, /* signal_flags */ 0, /* class_offset */ NULL, /* accumulator */ NULL, /* accu_data */ g_cclosure_marshal_VOID__ENUM, /* c_marshaller */ G_TYPE_NONE, /* return_type */ 1, /* n_params */ playerctl_loop_status_get_type()); /** * PlayerctlPlayer::shuffle: * @player: the player this event was emitted on * @shuffle_status: the shuffle status of the player * * Emitted when the shuffle status changes. */ connection_signals[SHUFFLE] = g_signal_new("shuffle", /* signal_name */ PLAYERCTL_TYPE_PLAYER, /* itype */ G_SIGNAL_RUN_FIRST, /* signal_flags */ 0, /* class_offset */ NULL, /* accumulator */ NULL, /* accu_data */ g_cclosure_marshal_VOID__BOOLEAN, /* c_marshaller */ G_TYPE_NONE, /* return_type */ 1, /* n_params */ G_TYPE_BOOLEAN); /** * PlayerctlPlayer::play: * @player: the player this event was emitted on * * Emitted when the player begins to play. * * Deprecated:2.0.0: Use the "playback-status::playing" signal instead. */ connection_signals[PLAY] = g_signal_new("play", /* signal_name */ PLAYERCTL_TYPE_PLAYER, /* itype */ G_SIGNAL_RUN_FIRST | G_SIGNAL_DEPRECATED, /* signal_flags */ 0, /* class_offset */ NULL, /* accumulator */ NULL, /* accu_data */ g_cclosure_marshal_VOID__VOID, /* c_marshaller */ G_TYPE_NONE, /* return_type */ 0); /* n_params */ /** * PlayerctlPlayer::pause: * @player: the player this event was emitted on * * Emitted when the player pauses. * * Deprecated:2.0.0: Use the "playback-status::paused" signal instead. */ connection_signals[PAUSE] = g_signal_new("pause", /* signal_name */ PLAYERCTL_TYPE_PLAYER, /* itype */ G_SIGNAL_RUN_FIRST | G_SIGNAL_DEPRECATED, /* signal_flags */ 0, /* class_offset */ NULL, /* accumulator */ NULL, /* accu_data */ g_cclosure_marshal_VOID__VOID, /* c_marshaller */ G_TYPE_NONE, /* return_type */ 0); /* n_params */ /** * PlayerctlPlayer::stop: * @player: the player this event was emitted on * * Emitted when the player stops. * * Deprecated:2.0.0: Use the "playback-status::stopped" signal instead. */ connection_signals[STOP] = g_signal_new("stop", /* signal_name */ PLAYERCTL_TYPE_PLAYER, /* itype */ G_SIGNAL_RUN_FIRST | G_SIGNAL_DEPRECATED, /* signal_flags */ 0, /* class_offset */ NULL, /* accumulator */ NULL, /* accu_data */ g_cclosure_marshal_VOID__VOID, /* c_marshaller */ G_TYPE_NONE, /* return_type */ 0); /* n_params */ /** * PlayerctlPlayer::metadata: * @player: the player this event was emitted on * @metadata: the metadata for the currently playing track. * * Emitted when the metadata for the currently playing track changes. */ connection_signals[METADATA] = g_signal_new("metadata", /* signal_name */ PLAYERCTL_TYPE_PLAYER, /* itype */ G_SIGNAL_RUN_FIRST, /* signal_flags */ 0, /* class_offset */ NULL, /* accumulator */ NULL, /* accu_data */ g_cclosure_marshal_VOID__VARIANT, /* c_marshaller */ G_TYPE_NONE, /* return_type */ 1, /* n_params */ G_TYPE_VARIANT); /** * PlayerctlPlayer::volume: * @player: the player this event was emitted on * @volume: the volume of the player from 0 to 100. * * Emitted when the volume of the player changes. */ connection_signals[VOLUME] = g_signal_new("volume", /* signal_name */ PLAYERCTL_TYPE_PLAYER, /* itype */ G_SIGNAL_RUN_FIRST, /* signal_flags */ 0, /* class_offset */ NULL, /* accumulator */ NULL, /* accu_data */ g_cclosure_marshal_VOID__DOUBLE, /* c_marshaller */ G_TYPE_NONE, /* return_type */ 1, /* n_params */ G_TYPE_DOUBLE); /** * PlayerctlPlayer::seeked: * @player: the player this event was emitted on. * @position: the new position in the track in microseconds. * * Emitted when the track changes position unexpectedly or begins in a * position other than the beginning. Otherwise, position is assumed to * progress normally. */ connection_signals[SEEKED] = g_signal_new("seeked", /* signal_name */ PLAYERCTL_TYPE_PLAYER, /* itype */ G_SIGNAL_RUN_FIRST, /* signal_flags */ 0, /* class_offset */ NULL, /* accumulator */ NULL, /* accu_data */ g_cclosure_marshal_VOID__LONG, /* c_marshaller */ G_TYPE_NONE, /* return_type */ 1, /* n_params */ G_TYPE_INT64); /** * PlayerctlPlayer::exit: * @player: the player this event was emitted on. * * Emitted when the player has disconnected and will no longer respond to * queries and commands. */ connection_signals[EXIT] = g_signal_new("exit", /* signal_name */ PLAYERCTL_TYPE_PLAYER, /* itype */ G_SIGNAL_RUN_FIRST, /* signal_flags */ 0, /* class_offset */ NULL, /* accumulator */ NULL, /* accu_data */ g_cclosure_marshal_VOID__VOID, /* c_marshaller */ G_TYPE_NONE, /* return_type */ 0); /* n_params */ } static void playerctl_player_init(PlayerctlPlayer *self) { self->priv = playerctl_player_get_instance_private(self); } /* * Get the matching bus name for this player name. Bus name will be like: * "org.mpris.MediaPlayer2.{PLAYER_NAME}[.{INSTANCE}]" * Pass a NULL player_name to get the first name on the bus * Returns NULL if no matching bus name is found on the bus. * Returns an error if there was a problem listing the names on the bus. */ static gchar *bus_name_for_player_name(gchar *name, GBusType bus_type, GError **err) { gchar *bus_name = NULL; GError *tmp_error = NULL; g_return_val_if_fail(err == NULL || *err == NULL, FALSE); GList *names = pctl_list_player_names_on_bus(bus_type, &tmp_error); if (tmp_error != NULL) { g_propagate_error(err, tmp_error); return NULL; } if (names == NULL) { return NULL; } if (name == NULL) { g_debug("Getting bus name for first available player"); PlayerctlPlayerName *name = names->data; bus_name = g_strdup_printf(MPRIS_PREFIX "%s", name->instance); pctl_player_name_list_destroy(names); return bus_name; } GList *exact_match = pctl_player_name_find(names, name, pctl_bus_type_to_source(bus_type)); if (exact_match != NULL) { g_debug("Getting bus name for player %s by exact match", name); PlayerctlPlayerName *name = exact_match->data; bus_name = g_strdup_printf(MPRIS_PREFIX "%s", name->instance); g_list_free_full(names, (GDestroyNotify)playerctl_player_name_free); return bus_name; } GList *instance_match = pctl_player_name_find_instance(names, name, pctl_bus_type_to_source(bus_type)); if (instance_match != NULL) { g_debug("Getting bus name for player %s by instance match", name); gchar *name = instance_match->data; bus_name = g_strdup_printf(MPRIS_PREFIX "%s", name); pctl_player_name_list_destroy(names); return bus_name; } return NULL; } static void playerctl_player_name_owner_changed_callback(GObject *object, GParamSpec *pspec, gpointer *user_data) { PlayerctlPlayer *player = PLAYERCTL_PLAYER(user_data); GDBusProxy *proxy = G_DBUS_PROXY(object); char *name_owner = g_dbus_proxy_get_name_owner(proxy); if (name_owner == NULL) { g_signal_emit(player, connection_signals[EXIT], 0); } g_free(name_owner); } static gboolean playerctl_player_initable_init(GInitable *initable, GCancellable *cancellable, GError **err) { GError *tmp_error = NULL; PlayerctlPlayer *player = PLAYERCTL_PLAYER(initable); if (player->priv->initted) { return TRUE; } g_return_val_if_fail(err == NULL || *err == NULL, FALSE); if (player->priv->instance != NULL && player->priv->player_name != NULL) { // if instance is specified, ignore name g_free(player->priv->player_name); player->priv->player_name = NULL; } if (player->priv->instance != NULL && player->priv->source == PLAYERCTL_SOURCE_NONE) { g_set_error(err, playerctl_player_error_quark(), 3, "A player cannot be constructed with an instance and no source"); return FALSE; } gchar *bus_name = NULL; if (player->priv->instance != NULL) { bus_name = g_strdup_printf(MPRIS_PREFIX "%s", player->priv->instance); } else if (player->priv->source != PLAYERCTL_SOURCE_NONE) { // the source was specified bus_name = bus_name_for_player_name( player->priv->player_name, pctl_source_to_bus_type(player->priv->source), &tmp_error); if (tmp_error) { g_propagate_error(err, tmp_error); return FALSE; } } else { // the source was not specified const GBusType bus_types[] = {G_BUS_TYPE_SESSION, G_BUS_TYPE_SYSTEM}; for (int i = 0; i < LENGTH(bus_types); ++i) { bus_name = bus_name_for_player_name(player->priv->player_name, bus_types[i], &tmp_error); if (tmp_error != NULL) { if (tmp_error->domain == G_IO_ERROR && tmp_error->code == G_IO_ERROR_NOT_FOUND) { g_debug("Bus address set incorrectly, cannot get bus"); g_clear_error(&tmp_error); continue; } g_propagate_error(err, tmp_error); return FALSE; } if (bus_name != NULL) { player->priv->source = pctl_bus_type_to_source(bus_types[i]); break; } } } if (bus_name == NULL) { g_set_error(err, playerctl_player_error_quark(), 1, "Player not found"); return FALSE; } player->priv->bus_name = bus_name; /* org.mpris.MediaPlayer2.{NAME}[.{INSTANCE}] */ int offset = strlen(MPRIS_PREFIX); gchar **split = g_strsplit(bus_name + offset, ".", 2); g_free(player->priv->player_name); player->priv->player_name = g_strdup(split[0]); g_strfreev(split); player->priv->proxy = org_mpris_media_player2_player_proxy_new_for_bus_sync( pctl_source_to_bus_type(player->priv->source), G_DBUS_PROXY_FLAGS_NONE, bus_name, "/org/mpris/MediaPlayer2", NULL, &tmp_error); if (tmp_error != NULL) { g_propagate_error(err, tmp_error); return FALSE; } // init the cache g_debug("initializing player: %s", player->priv->instance); player->priv->cached_position = org_mpris_media_player2_player_get_position(player->priv->proxy); clock_gettime(CLOCK_MONOTONIC, &player->priv->cached_position_monotonic); const gchar *playback_status_str = org_mpris_media_player2_player_get_playback_status(player->priv->proxy); PlayerctlPlaybackStatus status = 0; if (pctl_parse_playback_status(playback_status_str, &status)) { player->priv->cached_status = status; } g_signal_connect(player->priv->proxy, "g-properties-changed", G_CALLBACK(playerctl_player_properties_changed_callback), player); g_signal_connect(player->priv->proxy, "seeked", G_CALLBACK(playerctl_player_seeked_callback), player); g_signal_connect(player->priv->proxy, "notify::g-name-owner", G_CALLBACK(playerctl_player_name_owner_changed_callback), player); player->priv->initted = TRUE; return TRUE; } static void playerctl_player_initable_iface_init(GInitableIface *iface) { iface->init = playerctl_player_initable_init; } /** * playerctl_list_players: * @err: The location of a GError or NULL * * Lists all the players that can be controlled by Playerctl. * * Returns:(transfer full) (element-type PlayerctlPlayerName): A list of player names. */ GList *playerctl_list_players(GError **err) { GError *tmp_error = NULL; g_return_val_if_fail(err == NULL || *err == NULL, NULL); GList *session_players = pctl_list_player_names_on_bus(G_BUS_TYPE_SESSION, &tmp_error); if (tmp_error != NULL) { g_propagate_error(err, tmp_error); return NULL; } GList *system_players = pctl_list_player_names_on_bus(G_BUS_TYPE_SYSTEM, &tmp_error); if (tmp_error != NULL) { g_propagate_error(err, tmp_error); return NULL; } GList *players = g_list_concat(session_players, system_players); return players; } /** * playerctl_player_new: * @player_name:(allow-none): The name to use to find the bus name of the player * @err: The location of a GError or NULL * * Allocates a new #PlayerctlPlayer and tries to connect to an instance of the * player with the given name. * * Returns:(transfer full): A new #PlayerctlPlayer connected to an instance of * the player or NULL if an error occurred */ PlayerctlPlayer *playerctl_player_new(const gchar *player_name, GError **err) { GError *tmp_error = NULL; PlayerctlPlayer *player; player = g_initable_new(PLAYERCTL_TYPE_PLAYER, NULL, &tmp_error, "player-name", player_name, NULL); if (tmp_error != NULL) { g_propagate_error(err, tmp_error); return NULL; } return player; } /** * playerctl_player_new_for_source: * @player_name:(allow-none): The name to use to find the bus name of the player * @source: The source where the player name is. * @err: The location of a GError or NULL * * Allocates a new #PlayerctlPlayer and tries to connect to an instance of the * player with the given name from the given source. * * Returns:(transfer full): A new #PlayerctlPlayer connected to an instance of * the player or NULL if an error occurred */ PlayerctlPlayer *playerctl_player_new_for_source(const gchar *player_name, PlayerctlSource source, GError **err) { GError *tmp_error = NULL; PlayerctlPlayer *player; player = g_initable_new(PLAYERCTL_TYPE_PLAYER, NULL, &tmp_error, "player-name", player_name, "source", source, NULL); if (tmp_error != NULL) { g_propagate_error(err, tmp_error); return NULL; } return player; } /** * playerctl_player_new_from_name: * @player_name: The name type to use to find the player * @err:(allow-none): The location of a GError or NULL * * Allocates a new #PlayerctlPlayer and tries to connect to the player * identified by the #PlayerctlPlayerName. * * Returns:(transfer full): A new #PlayerctlPlayer connected to the player or * NULL if an error occurred */ PlayerctlPlayer *playerctl_player_new_from_name(PlayerctlPlayerName *player_name, GError **err) { GError *tmp_error = NULL; PlayerctlPlayer *player; player = g_initable_new(PLAYERCTL_TYPE_PLAYER, NULL, &tmp_error, "player-instance", player_name->instance, "source", player_name->source, NULL); if (tmp_error != NULL) { g_propagate_error(err, tmp_error); return NULL; } return player; } /** * playerctl_player_on: * @self: a #PlayerctlPlayer * @event: the event to subscribe to * @callback: the callback to run on the event * @err:(allow-none): the location of a GError or NULL * * A convenience function for bindings to subscribe to an event with a callback * * Deprecated:2.0.0: Use g_object_connect() to listen to events. */ void playerctl_player_on(PlayerctlPlayer *self, const gchar *event, GClosure *callback, GError **err) { g_return_if_fail(self != NULL); g_return_if_fail(event != NULL); g_return_if_fail(callback != NULL); g_return_if_fail(err == NULL || *err == NULL); if (self->priv->init_error != NULL) { g_propagate_error(err, g_error_copy(self->priv->init_error)); return; } g_closure_ref(callback); g_closure_sink(callback); g_signal_connect_closure(self, event, callback, TRUE); return; } #define PLAYER_COMMAND_FUNC(COMMAND) \ GError *tmp_error = NULL; \ \ g_return_if_fail(self != NULL); \ g_return_if_fail(err == NULL || *err == NULL); \ \ if (self->priv->init_error != NULL) { \ g_propagate_error(err, g_error_copy(self->priv->init_error)); \ return; \ } \ \ org_mpris_media_player2_player_call_##COMMAND##_sync(self->priv->proxy, NULL, &tmp_error); \ \ if (tmp_error != NULL) { \ g_propagate_error(err, tmp_error); \ } /** * playerctl_player_play_pause: * @self: a #PlayerctlPlayer * @err:(allow-none): the location of a GError or NULL * * Command the player to play if it is paused or pause if it is playing */ void playerctl_player_play_pause(PlayerctlPlayer *self, GError **err) { PLAYER_COMMAND_FUNC(play_pause); } /** * playerctl_player_open: * @self: a #PlayerctlPlayer * @uri: the URI to open, either a file name or an external URL * @err:(allow-none): the location of a GError or NULL * * Command the player to open given URI */ void playerctl_player_open(PlayerctlPlayer *self, gchar *uri, GError **err) { GError *tmp_error = NULL; g_return_if_fail(self != NULL); g_return_if_fail(err == NULL || *err == NULL); if (self->priv->init_error != NULL) { g_propagate_error(err, g_error_copy(self->priv->init_error)); return; } org_mpris_media_player2_player_call_open_uri_sync(self->priv->proxy, uri, NULL, &tmp_error); if (tmp_error != NULL) { g_propagate_error(err, tmp_error); return; } return; } /** * playerctl_player_play: * @self: a #PlayerctlPlayer * @err:(allow-none): the location of a GError or NULL * * Command the player to play */ void playerctl_player_play(PlayerctlPlayer *self, GError **err) { PLAYER_COMMAND_FUNC(play); } /** * playerctl_player_pause: * @self: a #PlayerctlPlayer * @err:(allow-none): the location of a GError or NULL * * Command the player to pause */ void playerctl_player_pause(PlayerctlPlayer *self, GError **err) { PLAYER_COMMAND_FUNC(pause); } /** * playerctl_player_stop: * @self: a #PlayerctlPlayer * @err:(allow-none): the location of a GError or NULL * * Command the player to stop */ void playerctl_player_stop(PlayerctlPlayer *self, GError **err) { PLAYER_COMMAND_FUNC(stop); } /** * playerctl_player_seek: * @self: a #PlayerctlPlayer * @offset: the offset to seek forward to in microseconds * @err:(allow-none): the location of a GError or NULL * * Command the player to seek forward by offset given in microseconds. */ void playerctl_player_seek(PlayerctlPlayer *self, gint64 offset, GError **err) { GError *tmp_error = NULL; g_return_if_fail(self != NULL); g_return_if_fail(err == NULL || *err == NULL); if (self->priv->init_error != NULL) { g_propagate_error(err, g_error_copy(self->priv->init_error)); return; } org_mpris_media_player2_player_call_seek_sync(self->priv->proxy, offset, NULL, &tmp_error); if (tmp_error != NULL) { g_propagate_error(err, tmp_error); return; } return; } /** * playerctl_player_next: * @self: a #PlayerctlPlayer * @err:(allow-none): the location of a GError or NULL * * Command the player to go to the next track */ void playerctl_player_next(PlayerctlPlayer *self, GError **err) { PLAYER_COMMAND_FUNC(next); } /** * playerctl_player_previous: * @self: a #PlayerctlPlayer * @err:(allow-none): the location of a GError or NULL * * Command the player to go to the previous track */ void playerctl_player_previous(PlayerctlPlayer *self, GError **err) { PLAYER_COMMAND_FUNC(previous); } static gchar *print_metadata_table(GVariant *metadata, gchar *player_name) { GVariantIter iter; GVariant *child; GString *table = g_string_new(""); const gchar *fmt = "%-5s %-25s %s\n"; if (g_strcmp0(g_variant_get_type_string(metadata), "a{sv}") != 0) { return NULL; } g_variant_iter_init(&iter, metadata); while ((child = g_variant_iter_next_value(&iter))) { GVariant *key_variant = g_variant_get_child_value(child, 0); const gchar *key = g_variant_get_string(key_variant, 0); GVariant *value_variant = g_variant_lookup_value(metadata, key, NULL); if (g_variant_is_container(value_variant)) { // only go depth 1 int len = g_variant_n_children(value_variant); for (int i = 0; i < len; ++i) { GVariant *child_value = g_variant_get_child_value(value_variant, i); gchar *child_value_str = pctl_print_gvariant(child_value); g_string_append_printf(table, fmt, player_name, key, child_value_str); g_free(child_value_str); g_variant_unref(child_value); } } else { gchar *value = pctl_print_gvariant(value_variant); g_string_append_printf(table, fmt, player_name, key, value); g_free(value); } g_variant_unref(child); g_variant_unref(key_variant); g_variant_unref(value_variant); } if (table->len == 0) { g_string_free(table, TRUE); return NULL; } // cut off the last newline table = g_string_truncate(table, table->len - 1); return g_string_free(table, FALSE); } /** * playerctl_player_print_metadata_prop: * @self: a #PlayerctlPlayer * @property:(allow-none): the property from the metadata to print * @err:(allow-none): the location of a GError or NULL * * Gets the given property from the metadata of the current track. If property * is null, prints all the metadata properties. Returns NULL if no track is * playing. * * Returns:(transfer full): The artist from the metadata of the current track */ gchar *playerctl_player_print_metadata_prop(PlayerctlPlayer *self, const gchar *property, GError **err) { GError *tmp_error = NULL; g_return_val_if_fail(self != NULL, NULL); g_return_val_if_fail(err == NULL || *err == NULL, NULL); if (self->priv->init_error != NULL) { g_propagate_error(err, g_error_copy(self->priv->init_error)); return NULL; } GVariant *metadata = playerctl_player_get_metadata(self, &tmp_error); if (tmp_error != NULL) { g_propagate_error(err, tmp_error); return NULL; } if (!metadata) { return NULL; } if (!property) { gchar *res = print_metadata_table(metadata, self->priv->player_name); g_variant_unref(metadata); return res; } GVariant *prop_variant = g_variant_lookup_value(metadata, property, NULL); g_variant_unref(metadata); if (!prop_variant) { return NULL; } gchar *prop = pctl_print_gvariant(prop_variant); g_variant_unref(prop_variant); return prop; } /** * playerctl_player_get_artist: * @self: a #PlayerctlPlayer * @err:(allow-none): the location of a GError or NULL * * Gets the artist from the metadata of the current track, or NULL if no * track is playing. * * Returns:(transfer full): The artist from the metadata of the current track */ gchar *playerctl_player_get_artist(PlayerctlPlayer *self, GError **err) { g_return_val_if_fail(self != NULL, NULL); g_return_val_if_fail(err == NULL || *err == NULL, NULL); if (self->priv->init_error != NULL) { g_propagate_error(err, g_error_copy(self->priv->init_error)); return NULL; } return playerctl_player_print_metadata_prop(self, "xesam:artist", NULL); } /** * playerctl_player_get_title: * @self: a #PlayerctlPlayer * @err:(allow-none): the location of a GError or NULL * * Gets the title from the metadata of the current track, or NULL if * no track is playing. * * Returns:(transfer full): The title from the metadata of the current track */ gchar *playerctl_player_get_title(PlayerctlPlayer *self, GError **err) { g_return_val_if_fail(self != NULL, NULL); g_return_val_if_fail(err == NULL || *err == NULL, NULL); if (self->priv->init_error != NULL) { g_propagate_error(err, g_error_copy(self->priv->init_error)); return NULL; } return playerctl_player_print_metadata_prop(self, "xesam:title", NULL); } /** * playerctl_player_get_album: * @self: a #PlayerctlPlayer * @err:(allow-none): the location of a GError or NULL * * Gets the album from the metadata of the current track, or NULL if * no track is playing. * * Returns:(transfer full): The album from the metadata of the current track */ gchar *playerctl_player_get_album(PlayerctlPlayer *self, GError **err) { g_return_val_if_fail(self != NULL, NULL); g_return_val_if_fail(err == NULL || *err == NULL, NULL); if (self->priv->init_error != NULL) { g_propagate_error(err, g_error_copy(self->priv->init_error)); return NULL; } return playerctl_player_print_metadata_prop(self, "xesam:album", NULL); } /** * playerctl_player_set_volume * @self: a #PlayerctlPlayer * @volume: the volume level from 0.0 to 1.0 * @err:(allow-none): the location of a GError or NULL * * Sets the volume level for the player from 0.0 for no volume to 1.0 for * maximum volume. Passing negative numbers should set the volume to 0.0. */ void playerctl_player_set_volume(PlayerctlPlayer *self, gdouble volume, GError **err) { GError *tmp_error = NULL; g_return_if_fail(self != NULL); g_return_if_fail(err == NULL || *err == NULL); if (self->priv->init_error != NULL) { g_propagate_error(err, g_error_copy(self->priv->init_error)); return; } GDBusConnection *connection = g_bus_get_sync(G_BUS_TYPE_SESSION, NULL, &tmp_error); if (tmp_error != NULL) { g_propagate_error(err, tmp_error); return; } GVariant *result = g_dbus_connection_call_sync( connection, self->priv->bus_name, MPRIS_PATH, PROPERTIES_IFACE, SET_MEMBER, g_variant_new("(ssv)", PLAYER_IFACE, "Volume", g_variant_new("d", volume)), NULL, G_DBUS_CALL_FLAGS_NONE, -1, NULL, &tmp_error); if (result != NULL) { g_variant_unref(result); } if (tmp_error != NULL) { g_propagate_error(err, tmp_error); return; } } /** * playerctl_player_get_position * @self: a #PlayerctlPlayer * @err:(allow-none): the location of a GError or NULL * * Gets the position of the current track in microseconds ignoring the property * cache. */ gint64 playerctl_player_get_position(PlayerctlPlayer *self, GError **err) { GError *tmp_error = NULL; g_return_val_if_fail(self != NULL, 0); g_return_val_if_fail(err == NULL || *err == NULL, 0); if (self->priv->init_error != NULL) { g_propagate_error(err, g_error_copy(self->priv->init_error)); return 0; } GVariant *call_reply = g_dbus_proxy_call_sync(G_DBUS_PROXY(self->priv->proxy), "org.freedesktop.DBus.Properties.Get", g_variant_new("(ss)", PLAYER_IFACE, "Position"), G_DBUS_CALL_FLAGS_NONE, -1, NULL, &tmp_error); if (tmp_error) { g_propagate_error(err, tmp_error); return 0; } GVariant *call_reply_properties = g_variant_get_child_value(call_reply, 0); GVariant *call_reply_unboxed = g_variant_get_variant(call_reply_properties); gint64 position = g_variant_get_int64(call_reply_unboxed); g_variant_unref(call_reply); g_variant_unref(call_reply_properties); g_variant_unref(call_reply_unboxed); return position; } /** * playerctl_player_set_position * @self: a #PlayerctlPlayer * @position: The absolute position in the track to set as the position * @err:(allow-none): the location of a GError or NULL * * Sets the absolute position of the current track to the given position in microseconds. */ void playerctl_player_set_position(PlayerctlPlayer *self, gint64 position, GError **err) { GError *tmp_error = NULL; g_return_if_fail(self != NULL); g_return_if_fail(err == NULL || *err == NULL); if (self->priv->init_error != NULL) { g_propagate_error(err, g_error_copy(self->priv->init_error)); return; } // calling the function requires the track id GVariant *metadata = playerctl_player_get_metadata(self, &tmp_error); if (tmp_error != NULL) { g_propagate_error(err, tmp_error); return; } gchar *track_id = metadata_get_track_id(metadata); g_variant_unref(metadata); if (track_id == NULL) { tmp_error = g_error_new(playerctl_player_error_quark(), 2, "Could not get track id to set position"); g_propagate_error(err, tmp_error); return; } org_mpris_media_player2_player_call_set_position_sync(self->priv->proxy, track_id, position, NULL, &tmp_error); if (tmp_error != NULL) { g_propagate_error(err, tmp_error); } } /** * playerctl_player_set_loop_status: * @self: a #PlayerctlPlayer * @status: the requested #PlayerctlLoopStatus to set the player to * @err:(allow-none): the location of a GError or NULL * * Set the loop status of the player. Can be set to either None, Track, or Playlist. */ void playerctl_player_set_loop_status(PlayerctlPlayer *self, PlayerctlLoopStatus status, GError **err) { GError *tmp_error = NULL; g_return_if_fail(self != NULL); g_return_if_fail(err == NULL || *err == NULL); if (self->priv->init_error != NULL) { g_propagate_error(err, g_error_copy(self->priv->init_error)); return; } const gchar *status_str = pctl_loop_status_to_string(status); g_return_if_fail(status_str != NULL); GDBusConnection *connection = g_bus_get_sync(G_BUS_TYPE_SESSION, NULL, &tmp_error); if (tmp_error != NULL) { g_propagate_error(err, tmp_error); return; } GVariant *result = g_dbus_connection_call_sync( connection, self->priv->bus_name, MPRIS_PATH, PROPERTIES_IFACE, SET_MEMBER, g_variant_new("(ssv)", PLAYER_IFACE, "LoopStatus", g_variant_new("s", status_str)), NULL, G_DBUS_CALL_FLAGS_NONE, -1, NULL, &tmp_error); if (result != NULL) { g_variant_unref(result); } if (tmp_error != NULL) { g_propagate_error(err, tmp_error); return; } } /** * playerctl_player_set_shuffle: * @self: a #PlayerctlPlayer * @shuffle: whether to enable shuffle * @err:(allow-none): the location of a GError or NULL * * Request to set the shuffle state of the player, either on or off. */ void playerctl_player_set_shuffle(PlayerctlPlayer *self, gboolean shuffle, GError **err) { GError *tmp_error = NULL; g_return_if_fail(self != NULL); g_return_if_fail(err == NULL || *err == NULL); if (self->priv->init_error != NULL) { g_propagate_error(err, g_error_copy(self->priv->init_error)); return; } GDBusConnection *connection = g_bus_get_sync(G_BUS_TYPE_SESSION, NULL, &tmp_error); if (tmp_error != NULL) { g_propagate_error(err, tmp_error); return; } GVariant *result = g_dbus_connection_call_sync( connection, self->priv->bus_name, MPRIS_PATH, PROPERTIES_IFACE, SET_MEMBER, g_variant_new("(ssv)", PLAYER_IFACE, "Shuffle", g_variant_new("b", shuffle)), NULL, G_DBUS_CALL_FLAGS_NONE, -1, NULL, &tmp_error); if (result != NULL) { g_variant_unref(result); } if (tmp_error != NULL) { g_propagate_error(err, tmp_error); return; } } char *pctl_player_get_instance(PlayerctlPlayer *player) { return player->priv->instance; } bool pctl_player_has_cached_property(PlayerctlPlayer *player, const gchar *name) { GVariant *value = g_dbus_proxy_get_cached_property(G_DBUS_PROXY(player->priv->proxy), name); if (value == NULL) { return false; } g_variant_unref(value); return true; } playerctl-2.4.1/playerctl/playerctl-player.h000066400000000000000000000154501412234731200211620ustar00rootroot00000000000000/* * This file is part of playerctl. * * playerctl is free software: you can redistribute it and/or modify it under * the terms of the GNU Lesser General Public License as published by the Free * Software Foundation, either version 3 of the License, or (at your option) * any later version. * * playerctl is distributed in the hope that it will be useful, but WITHOUT ANY * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for * more details. * * You should have received a copy of the GNU Lesser General Public License * along with playerctl If not, see . * * Copyright © 2014, Tony Crisci and contributors */ #ifndef __PLAYERCTL_PLAYER_H__ #define __PLAYERCTL_PLAYER_H__ #if !defined(__PLAYERCTL_INSIDE__) && !defined(PLAYERCTL_COMPILATION) #error "Only can be included directly." #endif #include #include #include /** * SECTION: playerctl-player * @short_description: A class to control a media player. * * The #PlayerctlPlayer represents a proxy connection to a media player through * an IPC interface that is capable of performing commands and executing * queries on the player for properties and metadata. * * If you know the name of your player and that it is running, you can use * playerctl_player_new() giving the player name to connect to it. The player * names given are the same as you can get with the binary `playerctl * --list-all` command. Using this function will get you the first instance of * the player it can find, or the exact instance if you pass the instance as * the player name. * * If you would like to connect to a player dynamically, you can list players * to be controlled with playerctl_list_players() or use the * #PlayerctlPlayerManager class and read the list of player name containers in * the #PlayerctlPlayerManager:player-names property or listen to the * #PlayerctlPlayerManager::name-appeared event. If you have a * #PlayerctlPlayerName, you can use the playerctl_player_new_from_name() * function to create a #PlayerctlPlayer from this name. * * Once you have a player, you can give it commands to play, pause, stop, open * a file, etc with the provided functions listed below. You can also query for * properties such as the playback status, position, and shuffle status. Each * of these has an event that will be emitted when these properties change * during a main loop. * * For examples on how to use the #PlayerctlPlayer, see the `examples` * directory in the git repository. */ #define PLAYERCTL_TYPE_PLAYER (playerctl_player_get_type()) #define PLAYERCTL_PLAYER(obj) \ (G_TYPE_CHECK_INSTANCE_CAST((obj), PLAYERCTL_TYPE_PLAYER, PlayerctlPlayer)) #define PLAYERCTL_IS_PLAYER(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj), PLAYERCTL_TYPE_PLAYER)) #define PLAYERCTL_PLAYER_CLASS(klass) \ (G_TYPE_CHECK_CLASS_CAST((klass), PLAYERCTL_TYPE_PLAYER, PlayerctlPlayerClass)) #define PLAYERCTL_IS_PLAYER_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE((klass), PLAYERCTL_TYPE_PLAYER)) #define PLAYERCTL_PLAYER_GET_CLASS(obj) \ (G_TYPE_INSTANCE_GET_CLASS((obj), PLAYERCTL_TYPE_PLAYER, PlayerctlPlayerClass)) typedef struct _PlayerctlPlayer PlayerctlPlayer; typedef struct _PlayerctlPlayerClass PlayerctlPlayerClass; typedef struct _PlayerctlPlayerPrivate PlayerctlPlayerPrivate; struct _PlayerctlPlayer { /* Parent instance structure */ GObject parent_instance; /* Private members */ PlayerctlPlayerPrivate *priv; }; struct _PlayerctlPlayerClass { /* Parent class structure */ GObjectClass parent_class; }; GType playerctl_player_get_type(void); PlayerctlPlayer *playerctl_player_new(const gchar *player_name, GError **err); PlayerctlPlayer *playerctl_player_new_for_source(const gchar *player_name, PlayerctlSource source, GError **err); PlayerctlPlayer *playerctl_player_new_from_name(PlayerctlPlayerName *player_name, GError **err); /** * PlayerctlPlaybackStatus: * @PLAYERCTL_PLAYBACK_STATUS_PLAYING: A track is currently playing. * @PLAYERCTL_PLAYBACK_STATUS_PAUSED: A track is currently paused. * @PLAYERCTL_PLAYBACK_STATUS_STOPPED: There is no track currently playing. * * Playback status enumeration for a #PlayerctlPlayer * */ typedef enum { PLAYERCTL_PLAYBACK_STATUS_PLAYING, /*< nick=Playing >*/ PLAYERCTL_PLAYBACK_STATUS_PAUSED, /*< nick=Paused >*/ PLAYERCTL_PLAYBACK_STATUS_STOPPED, /*< nick=Stopped >*/ } PlayerctlPlaybackStatus; /** * PlayerctlLoopStatus: * @PLAYERCTL_LOOP_STATUS_NONE: The playback will stop when there are no more tracks to play. * @PLAYERCTL_LOOP_STATUS_TRACK: The current track will start again from the beginning once it has * finished playing. * @PLAYERCTL_LOOP_STATUS_PLAYLIST: The playback loops through a list of tracks. * * Loop status enumeration for a #PlayerctlPlayer * */ typedef enum { PLAYERCTL_LOOP_STATUS_NONE, /*< nick=None >*/ PLAYERCTL_LOOP_STATUS_TRACK, /*< nick=Track >*/ PLAYERCTL_LOOP_STATUS_PLAYLIST, /* nick=Playlist >*/ } PlayerctlLoopStatus; /* * Static methods */ GList *playerctl_list_players(GError **err); /* * Method definitions. */ void playerctl_player_on(PlayerctlPlayer *self, const gchar *event, GClosure *callback, GError **err); void playerctl_player_open(PlayerctlPlayer *self, gchar *uri, GError **err); void playerctl_player_play_pause(PlayerctlPlayer *self, GError **err); void playerctl_player_play(PlayerctlPlayer *self, GError **err); void playerctl_player_stop(PlayerctlPlayer *self, GError **err); void playerctl_player_seek(PlayerctlPlayer *self, gint64 offset, GError **err); void playerctl_player_pause(PlayerctlPlayer *self, GError **err); void playerctl_player_next(PlayerctlPlayer *self, GError **err); void playerctl_player_previous(PlayerctlPlayer *self, GError **err); gchar *playerctl_player_print_metadata_prop(PlayerctlPlayer *self, const gchar *property, GError **err); gchar *playerctl_player_get_artist(PlayerctlPlayer *self, GError **err); gchar *playerctl_player_get_title(PlayerctlPlayer *self, GError **err); gchar *playerctl_player_get_album(PlayerctlPlayer *self, GError **err); void playerctl_player_set_volume(PlayerctlPlayer *self, gdouble volume, GError **err); gint64 playerctl_player_get_position(PlayerctlPlayer *self, GError **err); void playerctl_player_set_position(PlayerctlPlayer *self, gint64 position, GError **err); void playerctl_player_set_loop_status(PlayerctlPlayer *self, PlayerctlLoopStatus status, GError **err); void playerctl_player_set_shuffle(PlayerctlPlayer *self, gboolean shuffle, GError **err); #endif /* __PLAYERCTL_PLAYER_H__ */ playerctl-2.4.1/playerctl/playerctl-version.h.in000066400000000000000000000047531412234731200217640ustar00rootroot00000000000000/* * This file is part of playerctl. * * playerctl is free software: you can redistribute it and/or modify it under * the terms of the GNU Lesser General Public License as published by the Free * Software Foundation, either version 3 of the License, or (at your option) * any later version. * * playerctl is distributed in the hope that it will be useful, but WITHOUT ANY * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for * more details. * * You should have received a copy of the GNU Lesser General Public License * along with playerctl If not, see . * * Copyright © 2014, Tony Crisci */ #ifndef __PLAYERCTL_VERSION_H__ #define __PLAYERCTL_VERSION_H__ #if !defined(__PLAYERCTL_INSIDE__) && !defined(PLAYERCTL_COMPILATION) #error "Only can be included directly." #endif /** * SECTION:playerctl-version * @short_description: Playerctl version checking * * Playerctl provides macros to check the version of the library at * compile-time */ /** * PLAYERCTL_MAJOR_VERSION: * * Playerctl major version component */ #define PLAYERCTL_MAJOR_VERSION (@PLAYERCTL_MAJOR_VERSION@) /** * PLAYERCTL_MINOR_VERSION: * * Playerctl minor version component */ #define PLAYERCTL_MINOR_VERSION (@PLAYERCTL_MINOR_VERSION@) /** * PLAYERCTL_MICRO_VERSION: * * Playerctl micro version component */ #define PLAYERCTL_MICRO_VERSION (@PLAYERCTL_MICRO_VERSION@) /** * PLAYERCTL_VERSION: * * Playerctl version */ #define PLAYERCTL_VERSION (@PLAYERCTL_VERSION@) /** * PLAYERCTL_VERSION_S: * * Playerctl version, encoded as a string */ #define PLAYERCTL_VERSION_S "@PLAYERCTL_VERSION@" #define PLAYERCTL_ENCODE_VERSION(major,minor,micro) \ ((major) << 24 | (minor) << 16 | (micro) << 8) /** * PLAYERCTL_VERSION_HEX: * * Playerctl version, encoded as an hexadecimal number, useful for integer * comparisons. */ #define PLAYERCTL_VERSION_HEX \ (PLAYERCTL_ENCODE_VERSION (PLAYERCTL_MAJOR_VERSION, PLAYERCTL_MINOR_VERSION, PLAYERCTL_MICRO_VERSION)) #define PLAYERCTL_CHECK_VERSION(major, minor, micro) \ (PLAYERCTL_MAJOR_VERSION > (major) || \ (PLAYERCTL_MAJOR_VERSION == (major) && PLAYERCTL_MINOR_VERSION > (minor)) || \ (PLAYERCTL_MAJOR_VERSION == (major) && PLAYERCTL_MINOR_VERSION == (minor) && \ PLAYERCTL_MICRO_VERSION >= (micro))) #endif /* __PLAYERCTL_VERSION_H__ */ playerctl-2.4.1/playerctl/playerctl.h000066400000000000000000000022521412234731200176640ustar00rootroot00000000000000/* * This file is part of playerctl. * * playerctl is free software: you can redistribute it and/or modify it under * the terms of the GNU Lesser General Public License as published by the Free * Software Foundation, either version 3 of the License, or (at your option) * any later version. * * playerctl is distributed in the hope that it will be useful, but WITHOUT ANY * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for * more details. * * You should have received a copy of the GNU Lesser General Public License * along with playerctl If not, see . * * Copyright © 2014, Tony Crisci and contributors */ #ifndef __PLAYERCTL_H__ #define __PLAYERCTL_H__ #define __PLAYERCTL_INSIDE__ #ifdef __cplusplus extern "C" { #endif #include #include #include #include #include #ifdef __cplusplus } // extern "C" #endif #undef __PLAYERCTL_INSIDE__ #endif /* __PLAYERCTL_H__ */ playerctl-2.4.1/pytest.ini000066400000000000000000000000251412234731200155420ustar00rootroot00000000000000[pytest] timeout = 5 playerctl-2.4.1/requirements.txt000066400000000000000000000000651412234731200170010ustar00rootroot00000000000000dbus-next meson pytest pytest-timeout pytest-asyncio playerctl-2.4.1/snap/000077500000000000000000000000001412234731200144555ustar00rootroot00000000000000playerctl-2.4.1/snap/snapcraft.yaml000066400000000000000000000025041412234731200173230ustar00rootroot00000000000000name: playerctl base: core18 version: '2.2.1+git' summary: Media player command-line controller description: | Playerctl is a command-line utility and library for controlling media players that implement the MPRIS D-Bus Interface Specification. Playerctl makes it easy to bind player actions, such as play and pause, to media keys. You can also get metadata about the playing track such as the artist and title for integration into statusline generators or other command-line tools. grade: devel confinement: strict parts: playerctl: plugin: meson source: https://github.com/altdesktop/playerctl source-type: git meson-parameters: - -Dintrospection=false - -Dgtk-doc=false build-packages: - libglib2.0-dev stage-packages: - libglib2.0-0 slots: dbus-svc: interface: dbus bus: session name: org.mpris.MediaPlayer2.playerctld system-observe: interface: system-observe desktop-legacy: interface: desktop-legacy apps: playerctl: command: playerctl slots: [ system-observe, desktop-legacy ] environment: LD_LIBRARY_PATH: $SNAP/usr/local/lib/$SNAPCRAFT_ARCH_TRIPLET playerctld: command: playerctld slots: [ dbus-svc, system-observe, desktop-legacy ] environment: LD_LIBRARY_PATH: $SNAP/usr/local/lib/$SNAPCRAFT_ARCH_TRIPLET playerctl-2.4.1/test/000077500000000000000000000000001412234731200144735ustar00rootroot00000000000000playerctl-2.4.1/test/.gitignore000066400000000000000000000032401412234731200164620ustar00rootroot00000000000000# Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ pip-wheel-metadata/ share/python-wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .nox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover .hypothesis/ .pytest_cache/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py db.sqlite3 # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder target/ # Jupyter Notebook .ipynb_checkpoints # IPython profile_default/ ipython_config.py # pyenv .python-version # pipenv # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. # However, in case of collaboration, if having platform-specific dependencies or dependencies # having no cross-platform support, pipenv may install dependencies that don’t work, or not # install all needed dependencies. #Pipfile.lock # celery beat schedule file celerybeat-schedule # SageMath parsed files *.sage.py # Environments .env .venv env/ venv/ ENV/ env.bak/ venv.bak/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ .dmypy.json dmypy.json # Pyre type checker .pyre/ playerctl-2.4.1/test/__init__.py000066400000000000000000000000001412234731200165720ustar00rootroot00000000000000playerctl-2.4.1/test/conftest.py000066400000000000000000000010031412234731200166640ustar00rootroot00000000000000import pytest import asyncio @pytest.fixture() async def bus_address(scope='class'): proc = await asyncio.create_subprocess_shell( 'dbus-launch', stdout=asyncio.subprocess.PIPE) stdout, __ = await proc.communicate() await proc.wait() assert proc.returncode == 0 address = None for line in stdout.decode().split(): if line.startswith('DBUS_SESSION_BUS_ADDRESS='): address = line.split('=', 1)[1].strip() break assert address return address playerctl-2.4.1/test/data/000077500000000000000000000000001412234731200154045ustar00rootroot00000000000000playerctl-2.4.1/test/data/dbus-system.conf000066400000000000000000000006411412234731200205330ustar00rootroot00000000000000 playerctl-2.4.1/test/mpris.py000066400000000000000000000164051412234731200162050ustar00rootroot00000000000000from dbus_next.service import ServiceInterface, dbus_property, method, signal, Variant from dbus_next import PropertyAccess, RequestNameReply, BusType from dbus_next.aio import MessageBus 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 else: bus_type = BusType.SESSION bus = await MessageBus(bus_type=bus_type, bus_address=bus_address).connect() player = MprisPlayer(bus) bus.export('/org/mpris/MediaPlayer2', player) bus.export('/org/mpris/MediaPlayer2', MprisRoot()) reply = await bus.request_name(f'org.mpris.MediaPlayer2.{name}') assert reply == RequestNameReply.PRIMARY_OWNER return player players = await asyncio.gather(*(setup(name) for name in names)) await asyncio.gather(*(p.ping() for p in players)) return players 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') @method() def Raise(self): return @method() def Quit(self): return @dbus_property(access=PropertyAccess.READ) def CanRaise(self) -> 'b': return False @dbus_property(access=PropertyAccess.READ) def HasTrackList(self) -> 'b': return False @dbus_property(access=PropertyAccess.READ) def Identity(self) -> 's': return 'playerctl test client' @dbus_property(access=PropertyAccess.READ) def SupportedUriSchemes(self) -> 'as': return ['file'] @dbus_property(access=PropertyAccess.READ) def SupportedMimeTypes(self) -> 'as': return ['audio/mp3'] class MprisPlayer(ServiceInterface): def __init__(self, bus): super().__init__('org.mpris.MediaPlayer2.Player') self.counter = 0 self.reset() self.bus = bus def reset(self): # method calls self.next_called = False self.previous_called = False self.pause_called = False self.play_pause_called = False self.stop_called = False self.play_called = False self.seek_called_with = None self.set_position_called_with = None self.open_uri_called_with = None # properties self.playback_status = 'Playing' self.loop_status = 'None' self.rate = 1.0 self.shuffle = False self.metadata = {} self.volume = 1.0 self.position = 0 self.minimum_rate = 1.0 self.maximum_rate = 1.0 self.can_go_next = True self.can_go_previous = True self.can_play = True self.can_pause = True self.can_seek = True self.can_control = True # signals self.seeked_value = 0 async def ping(self): await self.bus.introspect('org.freedesktop.DBus', '/org/freedesktop/DBus') async def set_artist_title(self, artist, title, track_id=None): if track_id is None: self.counter += 1 track_id = '/' + str(self.counter) self.metadata = { 'xesam:title': Variant('s', title), 'xesam:artist': Variant('as', [artist]), 'mpris:trackid': Variant('o', track_id), } self.emit_properties_changed({ 'Metadata': self.metadata, }) await self.ping() async def clear_metadata(self): self.counter += 1 self.metadata = { 'mpris:trackid': Variant('o', '/' + str(self.counter)), } self.emit_properties_changed({ 'Metadata': self.metadata, }) await self.ping() async def disconnect(self): self.bus.disconnect() await self.bus.wait_for_disconnect() @method() def Next(self): self.next_called = True @method() def Previous(self): self.previous_called = True @method() def Pause(self): self.pause_called = True @method() def PlayPause(self): self.play_pause_called = True @method() def Stop(self): self.stop_called = True @method() def Play(self): self.play_called = True @method() def Seek(self, offset: 'x'): self.seek_called_with = offset @method() def SetPosition(self, track_id: 'o', position: 'x'): self.set_position_called_with = (track_id, position) @method() def OpenUri(self, uri: 's'): self.open_uri_called_with = uri @signal() def Seeked(self) -> 'x': return self.seeked_value @dbus_property(access=PropertyAccess.READ) def PlaybackStatus(self) -> 's': return self.playback_status @dbus_property() def LoopStatus(self) -> 's': return self.loop_status @LoopStatus.setter def LoopStatus(self, status: 's'): self.loop_status = status @dbus_property() def Rate(self) -> 'd': return self.rate @Rate.setter def Rate(self, rate: 'd'): self.rate = rate @dbus_property() def Shuffle(self) -> 'b': return self.shuffle @Shuffle.setter def Shuffle(self, shuffle: 'b'): self.shuffle = shuffle @dbus_property(access=PropertyAccess.READ) def Metadata(self) -> 'a{sv}': return self.metadata @dbus_property() def Volume(self) -> 'd': return self.volume @Volume.setter def Volume(self, volume: 'd'): self.volume = volume @dbus_property(access=PropertyAccess.READ) def Position(self) -> 'x': return self.position @dbus_property(access=PropertyAccess.READ) def MinimumRate(self) -> 'd': return self.minimum_rate @dbus_property(access=PropertyAccess.READ) def MaximumRate(self) -> 'd': return self.maximum_rate @dbus_property(access=PropertyAccess.READ) def CanGoNext(self) -> 'b': return self.can_go_next @dbus_property(access=PropertyAccess.READ) def CanGoPrevious(self) -> 'b': return self.can_go_previous @dbus_property(access=PropertyAccess.READ) def CanPlay(self) -> 'b': return self.can_play @dbus_property(access=PropertyAccess.READ) def CanPause(self) -> 'b': return self.can_pause @dbus_property(access=PropertyAccess.READ) def CanSeek(self) -> 'b': return self.can_seek @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 async def disconnect(self): self.bus.disconnect() await self.bus.wait_for_disconnect() playerctl-2.4.1/test/playerctl.py000066400000000000000000000046201412234731200170460ustar00rootroot00000000000000import asyncio import os from shlex import join class CommandResult: def __init__(self, stdout, stderr, returncode): self.stdout = stdout.decode().strip() self.stderr = stderr.decode().strip() self.returncode = returncode class PlayerctlProcess: def __init__(self, proc, debug=False): self.queue = asyncio.Queue() self.proc = proc async def reader(stream): while True: line = await stream.readline() if not line: break line = line.decode().strip() if 'playerctl-DEBUG:' in line: print(line) else: self.queue.put_nowait(line) async def printer(stream): while True: line = await stream.readline() print(line) if not line: break asyncio.get_event_loop().create_task(reader(proc.stdout)) # asyncio.get_event_loop().create_task(printer(proc.stderr)) def running(self): return self.proc.returncode is None class PlayerctlCli: def __init__(self, bus_address=None, debug=False): self.bus_address = bus_address self.debug = debug self.proc = None async def _start(self, cmd): env = os.environ.copy() shell_cmd = f'playerctl {cmd}' if self.bus_address: env['DBUS_SESSION_BUS_ADDRESS'] = self.bus_address if self.debug: env['G_MESSAGES_DEBUG'] = 'playerctl' return await asyncio.create_subprocess_shell( shell_cmd, env=env, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE) async def start(self, cmd): proc = await self._start(cmd) return PlayerctlProcess(proc) async def run(self, cmd): proc = await self._start(cmd) 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() playerctl-2.4.1/test/test_basics.py000066400000000000000000000060121412234731200173470ustar00rootroot00000000000000from .mpris import setup_mpris from .playerctl import PlayerctlCli import math import asyncio import pytest @pytest.mark.asyncio async def test_basics(): playerctl = PlayerctlCli() result = await playerctl.run('--help') assert result.returncode == 0, result.stderr assert result.stdout assert not result.stderr # with no players result = await playerctl.run('--list-all') assert result.returncode == 0, result.stderr assert not result.stdout assert result.stderr result = await playerctl.run('--version') assert result.returncode == 0, result.stderr assert result.stdout assert not result.stderr commands = ('play', 'pause', 'play-pause', 'stop', 'next', 'previous', 'position', 'position 5', 'volume', 'volume 0.5', 'status', 'metadata', 'loop', 'loop None', 'shuffle', 'shuffle On', 'open https://google.com') results = await asyncio.gather(*(playerctl.run(cmd) for cmd in commands)) for result in results: assert result.returncode == 1 assert not result.stdout assert 'No players found' in result.stderr.split('\n') @pytest.mark.asyncio async def test_list_names(bus_address): mpris_players = await setup_mpris('basics1', 'basics2', 'basics3', bus_address=bus_address) playerctl = PlayerctlCli(bus_address) result = await playerctl.run('--list-all') assert result.returncode == 0, result.stderr players = result.stdout.splitlines() assert 'basics1' in players assert 'basics2' in players assert 'basics3' in players await asyncio.gather(*[mpris.disconnect() for mpris in mpris_players]) @pytest.mark.asyncio 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) result = await playerctl.run('-l') assert result.returncode == 0, result.stdout assert result.stdout.split() == ['session1', 'system'] await asyncio.gather( *[mpris.disconnect() for mpris in system_players + session_players]) @pytest.mark.asyncio async def test_queries(bus_address): [mpris] = await setup_mpris('queries', bus_address=bus_address) mpris.position = 2500000 playerctl = PlayerctlCli(bus_address) query = await playerctl.run('status') assert query.stdout == mpris.playback_status, query.stderr query = await playerctl.run('volume') assert float(query.stdout) == mpris.volume, query.stderr query = await playerctl.run('loop') assert query.stdout == mpris.loop_status, query.stderr query = await playerctl.run('position') assert math.fabs(float(query.stdout) * 1000000 - mpris.position) < 100, query.stderr query = await playerctl.run('shuffle') assert query.stdout == ('On' if mpris.shuffle else 'Off'), query.stderr playerctl-2.4.1/test/test_commands.py000066400000000000000000000022661412234731200177130ustar00rootroot00000000000000from .mpris import setup_mpris from .playerctl import PlayerctlCli import asyncio import pytest # TODO: test sending a command to all players @pytest.mark.asyncio async def test_commands(bus_address): [mpris] = await setup_mpris('commands', bus_address=bus_address) mpris.shuffle = False mpris.volume = 1.0 mpris.loop_status = 'Track' commands = ('play', 'pause', 'play-pause', 'stop', 'next', 'previous') setters = ('volume 0.8', 'loop playlist', 'shuffle on') def get_called(cmd): return getattr(mpris, f'{cmd.replace("-", "_")}_called') playerctl = PlayerctlCli(bus_address) results = await asyncio.gather(*(playerctl.run(f'-p commands {cmd}') for cmd in commands + setters)) for result in results: assert result.returncode == 0, result.stderr for i, cmd in enumerate(commands): result = results[i] assert get_called(cmd), f'{cmd} was not called: {result.stderr}' assert mpris.shuffle assert mpris.volume == 0.8 assert mpris.loop_status == 'Playlist' await playerctl.run('-p commands shuffle toggle') assert not mpris.shuffle await mpris.disconnect() playerctl-2.4.1/test/test_daemon.py000066400000000000000000000245571412234731200173640ustar00rootroot00000000000000import pytest import os from .mpris import setup_mpris from .playerctl import PlayerctlCli from dbus_next.aio import MessageBus from dbus_next import Message, MessageType import asyncio from asyncio import Queue from subprocess import run as run_process async def start_playerctld(bus_address, debug=False): pkill = await asyncio.create_subprocess_shell('pkill playerctld') await pkill.wait() env = os.environ.copy() env['DBUS_SESSION_BUS_ADDRESS'] = bus_address env['G_MESSAGES_DEBUG'] = 'playerctl' proc = await asyncio.create_subprocess_shell( 'playerctld', env=env, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.STDOUT) async def printer(stream): while True: line = await stream.readline() print(line) if not line: break if debug: asyncio.get_event_loop().create_task(printer(proc.stdout)) return proc async def get_playerctld(bus): path = '/com/github/altdesktop/playerctld' interface = 'com.github.altdesktop.playerctld' introspection = await bus.introspect('org.mpris.MediaPlayer2.playerctld', path) obj = bus.get_proxy_object(interface, path, introspection) return obj.get_interface('org.freedesktop.DBus.Properties') @pytest.mark.asyncio async def test_daemon_commands(bus_address): playerctl = PlayerctlCli(bus_address) async def run(cmd): return await playerctl.run('-p playerctld ' + cmd) # with no other players running, these should error because there's no # active player (not no players found). This tests activation and property # errors as well. results = await asyncio.gather(*(run(cmd) for cmd in ('play', 'pause', 'play-pause', 'stop', 'next', 'previous', 'position', 'volume', 'status', 'metadata', 'loop', 'shuffle'))) for result in results: assert result.returncode == 1 assert 'No player could handle this command' in result.stderr.splitlines( ) # restart playerctld so we can manage the process and see debug info playerctld_proc = await start_playerctld(bus_address) [mpris1, mpris2, mpris3] = await setup_mpris('daemon1', 'daemon2', 'daemon3', bus_address=bus_address) await mpris2.set_artist_title('artist', 'title') cmd = await run('play') assert cmd.returncode == 0, cmd.stdout assert mpris2.play_called, cmd.stdout mpris2.reset() await mpris1.set_artist_title('artist', 'title') cmd = await run('play') assert cmd.returncode == 0, cmd.stderr assert mpris1.play_called mpris1.reset() await mpris3.set_artist_title('artist', 'title') cmd = await run('play') assert cmd.returncode == 0, cmd.stderr assert mpris3.play_called mpris3.reset() await mpris3.disconnect() cmd = await run('play') assert cmd.returncode == 0, cmd.stderr assert mpris1.play_called mpris1.reset() await asyncio.gather(mpris1.disconnect(), mpris2.disconnect()) playerctld_proc.terminate() await playerctld_proc.wait() @pytest.mark.asyncio async def test_daemon_follow(bus_address): playerctld_proc = await start_playerctld(bus_address) [mpris1, mpris2] = await setup_mpris('player1', 'player2', bus_address=bus_address) playerctl = PlayerctlCli(bus_address) pctl_cmd = '--player playerctld metadata --format "{{playerInstance}}: {{artist}} - {{title}}" --follow' proc = await playerctl.start(pctl_cmd) await mpris1.set_artist_title('artist1', 'title1') line = await proc.queue.get() assert line == 'playerctld: artist1 - title1', proc.queue await mpris2.set_artist_title('artist2', 'title2') line = await proc.queue.get() assert line == 'playerctld: artist2 - title2', proc.queue [mpris3] = await setup_mpris('player3', bus_address=bus_address) await mpris3.set_artist_title('artist3', 'title3') line = await proc.queue.get() if line == '': # the line might be blank here because of the test setup line = await proc.queue.get() assert line == 'playerctld: artist3 - title3', proc.queue await mpris1.set_artist_title('artist4', 'title4') line = await proc.queue.get() assert line == 'playerctld: artist4 - title4', proc.queue await mpris1.set_artist_title('artist5', 'title5') line = await proc.queue.get() assert line == 'playerctld: artist5 - title5', proc.queue await mpris1.disconnect() line = await proc.queue.get() assert line == 'playerctld: artist3 - title3', proc.queue await asyncio.gather(mpris2.disconnect(), mpris3.disconnect()) playerctld_proc.terminate() proc.proc.terminate() await proc.proc.wait() await playerctld_proc.wait() async def playerctld_shift(bus_address, reverse=False): env = os.environ.copy() env['DBUS_SESSION_BUS_ADDRESS'] = bus_address env['G_MESSAGES_DEBUG'] = 'playerctl' cmd = 'playerctld unshift' if reverse else 'playerctld shift' shift = await asyncio.create_subprocess_shell( cmd, env=env, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.STDOUT) return await shift.wait() @pytest.mark.asyncio async def test_daemon_shift_simple(bus_address): playerctld_proc = await start_playerctld(bus_address) mprises = await setup_mpris('player1', 'player2', 'player3', bus_address=bus_address) [mpris1, mpris2, mpris3] = mprises playerctl = PlayerctlCli(bus_address) pctl_cmd = '--player playerctld metadata --format "{{playerInstance}}: {{artist}} - {{title}}" --follow' proc = await playerctl.start(pctl_cmd) await mpris1.set_artist_title('artist1', 'title1') line = await proc.queue.get() assert line == 'playerctld: artist1 - title1', proc.queue await mpris2.set_artist_title('artist2', 'title2') line = await proc.queue.get() assert line == 'playerctld: artist2 - title2', proc.queue await mpris3.set_artist_title('artist3', 'title3') line = await proc.queue.get() assert line == 'playerctld: artist3 - title3', proc.queue code = await playerctld_shift(bus_address) assert code == 0 line = await proc.queue.get() assert line == 'playerctld: artist2 - title2', proc.queue code = await playerctld_shift(bus_address) assert code == 0 line = await proc.queue.get() assert line == 'playerctld: artist1 - title1', proc.queue code = await playerctld_shift(bus_address, reverse=True) assert code == 0 line = await proc.queue.get() assert line == 'playerctld: artist2 - title2', proc.queue code = await playerctld_shift(bus_address, reverse=True) assert code == 0 line = await proc.queue.get() assert line == 'playerctld: artist3 - title3', proc.queue playerctld_proc.terminate() proc.proc.terminate() await asyncio.gather(mpris1.disconnect(), mpris2.disconnect(), playerctld_proc.wait(), proc.proc.wait()) @pytest.mark.asyncio async def test_daemon_shift_no_player(bus_address): playerctld_proc = await start_playerctld(bus_address) playerctl = PlayerctlCli(bus_address) pctl_cmd = '--player playerctld metadata --format "{{playerInstance}}: {{artist}} - {{title}}" --follow' proc = await playerctl.start(pctl_cmd) code = await playerctld_shift(bus_address) assert code == 1 [mpris1] = await setup_mpris('player1', bus_address=bus_address) code = await playerctld_shift(bus_address) assert code == 0 await mpris1.disconnect() code = await playerctld_shift(bus_address) assert code == 1 code = await playerctld_shift(bus_address, reverse=True) assert code == 1 [mpris1] = await setup_mpris('player1', bus_address=bus_address) code = await playerctld_shift(bus_address, reverse=True) assert code == 0 await mpris1.disconnect() code = await playerctld_shift(bus_address, reverse=True) assert code == 1 playerctld_proc.terminate() await playerctld_proc.wait() @pytest.mark.asyncio async def test_active_player_change(bus_address): queue = Queue() playerctld_proc = await start_playerctld(bus_address) bus = await MessageBus(bus_address=bus_address).connect() reply = await bus.call( Message(destination='org.freedesktop.DBus', interface='org.freedesktop.DBus', path='/org/freedesktop/DBus', member='AddMatch', signature='s', body=["sender='org.mpris.MediaPlayer2.playerctld'"])) assert reply.message_type == MessageType.METHOD_RETURN, reply.body def message_handler(message): if message.member == 'PropertiesChanged' and message.body[ 0] == 'com.github.altdesktop.playerctld' and 'PlayerNames' in message.body[ 1]: queue.put_nowait(message.body[1]['PlayerNames'].value) def player_list(*args): return [f'org.mpris.MediaPlayer2.{name}' for name in args] bus.add_message_handler(message_handler) [mpris1] = await setup_mpris('player1', bus_address=bus_address) assert player_list('player1') == await queue.get() [mpris2] = await setup_mpris('player2', bus_address=bus_address) assert player_list('player2', 'player1') == await queue.get() # changing artist/title should bump the player up await mpris1.set_artist_title('artist1', 'title1', '/1') assert player_list('player1', 'player2') == await queue.get() # if properties are not actually different, it shouldn't update await mpris2.set_artist_title('artist2', 'title2', '/2') assert player_list('player2', 'player1') == await queue.get() await mpris1.set_artist_title('artist1', 'title1', '/1') await mpris1.ping() assert queue.empty() bus.disconnect() await asyncio.gather(mpris1.disconnect(), mpris2.disconnect(), bus.wait_for_disconnect()) playerctld_proc.terminate() await playerctld_proc.wait() playerctl-2.4.1/test/test_follow.py000066400000000000000000000142101412234731200174040ustar00rootroot00000000000000from .mpris import setup_mpris from .playerctl import PlayerctlCli import pytest import asyncio @pytest.mark.asyncio async def test_follow(bus_address): player1 = 'test1' [mpris1] = await setup_mpris(player1, bus_address=bus_address) playerctl = PlayerctlCli(bus_address) pctl_cmd = 'metadata --format "{{playerInstance}}: {{artist}} - {{title}}" --follow' proc = await playerctl.start(pctl_cmd) await mpris1.set_artist_title('artist', 'title') line = await proc.queue.get() assert line == 'test1: artist - title' await mpris1.set_artist_title('artist2', 'title2') line = await proc.queue.get() assert line == 'test1: artist2 - title2' await mpris1.clear_metadata() line = await proc.queue.get() assert line == 'test1: -' await mpris1.set_artist_title('artist3', 'title3') line = await proc.queue.get() assert line == 'test1: artist3 - title3' await mpris1.disconnect() line = await proc.queue.get() assert line == '' @pytest.mark.asyncio async def test_follow_selection(bus_address): player1 = 'test1' player2 = 'test2' player3 = 'test3' player4 = 'test4' [mpris1, mpris2, mpris3, mpris4] = await setup_mpris(player1, player2, player3, player4, bus_address=bus_address) await mpris1.set_artist_title('artist', 'title') playerctl = PlayerctlCli(bus_address) pctl_cmd = '--player test3,test2,test1 metadata --format "{{playerInstance}}: {{artist}} - {{title}}" --follow' proc = await playerctl.start(pctl_cmd) line = await proc.queue.get() assert line == 'test1: artist - title' # player4 is ignored await mpris4.set_artist_title('artist', 'title') assert proc.queue.empty() # setting metadata the same twice doesn't print await mpris1.set_artist_title('artist', 'title') assert proc.queue.empty() await mpris2.set_artist_title('artist', 'title') line = await proc.queue.get() assert line == 'test2: artist - title' # player2 takes precedence await mpris1.set_artist_title('artist2', 'title2') assert proc.queue.empty() await mpris3.set_artist_title('artist', 'title') line = await proc.queue.get() assert line == 'test3: artist - title' # player 3 takes precedence await mpris2.set_artist_title('artist2', 'title2') assert proc.queue.empty() # when bus3 disconnects, it should show the next one await mpris3.disconnect() await mpris2.ping() line = await proc.queue.get() assert line == 'test2: artist2 - title2' # same for bus2 await mpris2.disconnect() await mpris1.ping() line = await proc.queue.get() assert line == 'test1: artist2 - title2' await mpris1.disconnect() line = await proc.queue.get() assert line == '' await mpris4.disconnect() @pytest.mark.asyncio async def test_follow_selection_any(bus_address): player1 = 'test1' player2 = 'test2' player3 = 'test3' player4 = 'test4' [mpris1, mpris2, mpris3, mpris4] = await setup_mpris(player1, player2, player3, player4, bus_address=bus_address) playerctl = PlayerctlCli(bus_address) pctl_cmd = '--player test3,%any,test1 metadata --format "{{playerInstance}}: {{artist}} - {{title}}" --follow' proc = await playerctl.start(pctl_cmd) # test3 takes first precedence await mpris3.set_artist_title('artist', 'title') line = await proc.queue.get() assert line == 'test3: artist - title', proc.queue await mpris2.set_artist_title('artist', 'title') assert proc.queue.empty() await mpris1.set_artist_title('artist', 'title') assert proc.queue.empty() await mpris3.disconnect() line = await proc.queue.get() assert line == 'test2: artist - title' await mpris2.disconnect() line = await proc.queue.get() assert line == 'test1: artist - title' await mpris1.disconnect() line = await proc.queue.get() assert line == '' await mpris4.disconnect() @pytest.mark.asyncio async def test_follow_all_players(bus_address): player1 = 'test1' player2 = 'test2' player3 = 'test3' player4 = 'test4' [mpris1, mpris2, mpris3, mpris4] = await setup_mpris(player1, player2, player3, player4, bus_address=bus_address) await asyncio.gather(*[ mpris.set_artist_title('artist', 'title') for mpris in [mpris1, mpris2, mpris3, mpris4] ]) playerctl = PlayerctlCli(bus_address) pctl_cmd = '--all-players --player test3,test2,test1 metadata --format "{{playerInstance}}: {{artist}} - {{title}}" --follow' proc = await playerctl.start(pctl_cmd) # player4 is ignored await mpris4.set_artist_title('artist', 'title') assert proc.queue.empty() # no precedence, just whoever changes metadata last await mpris1.set_artist_title('artist2', 'title2') line = await proc.queue.get() assert line == 'test1: artist2 - title2' await mpris2.set_artist_title('artist2', 'title2') line = await proc.queue.get() assert line == 'test2: artist2 - title2' await mpris3.set_artist_title('artist2', 'title2') line = await proc.queue.get() assert line == 'test3: artist2 - title2' await mpris2.set_artist_title('artist2', 'title2') line = await proc.queue.get() assert line == 'test2: artist2 - title2' await mpris1.set_artist_title('artist2', 'title2') line = await proc.queue.get() assert line == 'test1: artist2 - title2' await mpris1.disconnect() await mpris4.ping() line = await proc.queue.get() assert line == 'test2: artist2 - title2' await mpris2.disconnect() await mpris4.ping() line = await proc.queue.get() assert line == 'test3: artist2 - title2' await mpris3.disconnect() await mpris4.ping() line = await proc.queue.get() assert line == '' await mpris4.disconnect() playerctl-2.4.1/test/test_format.py000066400000000000000000000130451412234731200173770ustar00rootroot00000000000000from dbus_next import Variant from .mpris import setup_mpris from .playerctl import PlayerctlCli import pytest import asyncio # TODO: test missing function does not segv @pytest.mark.asyncio async def test_emoji(bus_address): [mpris] = await setup_mpris('emoji-format-test', bus_address=bus_address) mpris.metadata = {'mpris:length': Variant('x', 100000)} await mpris.ping() playerctl = PlayerctlCli(bus_address) status_emoji_cmd = 'metadata --format \'{{emoji(status)}}\'' mpris.playback_status = 'Playing' cmd = await playerctl.run(status_emoji_cmd) assert cmd.stdout == '▶️', cmd.stderr mpris.playback_status = 'Paused' cmd = await playerctl.run(status_emoji_cmd) assert cmd.stdout == '⏸️', cmd.stderr mpris.playback_status = 'Stopped' cmd = await playerctl.run(status_emoji_cmd) assert cmd.stdout == '⏹️', cmd.stderr volume_emoji_cmd = 'metadata --format \'{{emoji(volume)}}\'' mpris.volume = 0.0 cmd = await playerctl.run(volume_emoji_cmd) assert cmd.stdout == '🔈', cmd.stderr mpris.volume = 0.5 cmd = await playerctl.run(volume_emoji_cmd) assert cmd.stdout == '🔉', cmd.stderr mpris.volume = 1.0 cmd = await playerctl.run(volume_emoji_cmd) assert cmd.stdout == '🔊', cmd.stderr cmd = await playerctl.run('metadata --format \'{{emoji("hi")}}\'') assert cmd.returncode == 1, cmd.stderr cmd = await playerctl.run('metadata --format \'{{emoji(status, volume)}}\'' ) assert cmd.returncode == 1, cmd.stderr await mpris.disconnect() class MetadataTest: def __init__(self, playerctl): self.tests = [] self.playerctl = playerctl def add(self, fmt, expected, ret=0): fmt = fmt.replace("'", r"\'") self.tests.append((f"metadata --format '{fmt}'", expected, ret)) async def run(self): coros = [] for fmt, _, _ in self.tests: coros.append(self.playerctl.run(fmt)) results = await asyncio.gather(*coros) for i, cmd in enumerate(results): fmt, expected, ret = self.tests[i] assert cmd.returncode == ret, cmd.stderr if ret == 0: assert cmd.stdout == expected, cmd.stderr @pytest.mark.asyncio async def test_format(bus_address): title = 'A Title' artist = 'An Artist' album = 'An Album' player_name = 'format-test' player_instance = f'{player_name}.instance123' [mpris] = await setup_mpris(player_instance, bus_address=bus_address) mpris.metadata = { 'xesam:title': Variant('s', title), 'xesam:artist': Variant('as', [artist]), 'xesam:escapeme': Variant('s', ''), 'xesam:album': Variant('s', album), 'mpris:length': Variant('x', 100000) } mpris.volume = 2.0 playerctl = PlayerctlCli(bus_address) test = MetadataTest(playerctl) test.add('{{artist}} - {{title}}', f'{artist} - {title}') test.add("{{markup_escape(xesam:escapeme)}}", "<hi>") test.add("{{lc(artist)}}", artist.lower()) test.add("{{uc(title)}}", title.upper()) test.add("{{uc(lc(title))}}", title.upper()) test.add('{{uc("Hi")}}', "HI") test.add("{{mpris:length}}", "100000") test.add( '@{{ uc( "hi" ) }} - {{uc( lc( "HO" ) ) }} . {{lc( uc( title ) ) }}@', f'@HI - HO . {title.lower()}@') test.add("{{default(xesam:missing, artist)}}", artist) test.add("{{default(title, artist)}}", title) test.add('{{default("", "ok")}}', 'ok') test.add('{{default("ok", "not")}}', 'ok') test.add(' {{lc(album)}} ', album.lower()) test.add('{{playerName}} - {{playerInstance}}', f'{player_name} - {player_instance}') test.add("{{trunc(title, 10)}}", title) test.add("{{trunc(title, 5)}}", f"{title[:5]}…") test.add('{{trunc("", 0)}}', "") test.add('{{trunc("", 10)}}', "") await test.run() # numbers math = [ '10', '-10 + 20', '10 + 10', '10 * 10', '10 / 10', '10 + 10 * 10 + 10', '10 + 10 * -10 + 10', '10 + 10 * -10 + -10', '-10 * 10 + 10', '-10 * -10 * -1 + -10', '-10 * 10 + -10 * -10 + 20 / 10 * -20 + -10', '8+-+--++-4', '2 - 10 * 1 + 1', '2 / -2 + 2 * 2 * -2 - 2 - 2 * -2', '2 * (2 + 2)', '10 * (10 + 12) - 4', '-(10)', '-(10 + 12 * -2)', '14 - (10 * 2 + 5) * -6', '(14 - 2 * 3) * (14 * -2 - 6) + -(4 - 2) * 5', ] # variables math += [ 'volume', 'volume + 10', '-volume', '-volume * -1', '-volume + volume', 'volume * volume', 'volume * -volume', 'volume + volume * -volume * volume + -volume', 'volume / -volume + volume * volume * -volume - volume - volume * -volume', '-(volume + 3) * 5 * (volume + 2)', ] # functions math += [ 'default(5+5, None)', '-default(5 + 5, None)', '(-default(5 - 5, None) + 2) * 8', '2 + (5 * 4 + 3 * -default(5, default(6 * (3 + 4 * (6 + 2)) / 2, None)) + -56)', ] def default_shim(arg1, arg2): if arg1 is None: return arg2 return arg1 async def math_test(math): cmd = await playerctl.run("metadata --format '{{" + math + "}}'") assert cmd.returncode == 0, cmd.stderr assert float(cmd.stdout) == eval(math, { 'volume': mpris.volume, 'default': default_shim }), math await asyncio.gather(*[math_test(m) for m in math]) await mpris.disconnect() playerctl-2.4.1/test/test_selection.py000066400000000000000000000105631412234731200200760ustar00rootroot00000000000000from .mpris import setup_mpris, setup_playerctld from .playerctl import PlayerctlCli import pytest import asyncio def selector(bus_address): playerctl = PlayerctlCli(bus_address) async def select(*players): assert players cmd = '-p ' + str.join( ',', players) + ' status --format "{{playerInstance}}"' result = await playerctl.run(cmd) assert result.returncode == 0, result.stderr return result.stdout async def select_many(*players): assert players cmd = '--all-players -p ' + str.join( ',', players) + ' status --format "{{playerInstance}}"' result = await playerctl.run(cmd) assert result.returncode == 0, result.stderr return tuple(result.stdout.split('\n')) return select, select_many @pytest.mark.asyncio async def test_selection(bus_address): s1 = 'selection1' s1i = 'selection1.i_123' s2 = 'selection2' s3 = 'selection3' m4 = 'selection4' m5 = 'selection5' m6 = 'selection6' s6i = 'selection6.i_2' any_player = '%any' mpris_players = await setup_mpris(s1, s1i, s2, s3, s6i, bus_address=bus_address) # TODO: test ignored players selections = { (s1, ): (s1, s1i), (s3, s1): (s3, s1, s1i), (s2, s1, s3): (s2, s1, s1i, s3), (s1, s2): (s1, s1i, s2), (m4, m5, s2, s3): (s2, s3), (m5, s1, m4, s3): (s1, s1i, s3), (s1, s1i): (s1, s1i), (s1i, s1): (s1i, s1), (m6, s1): (s6i, s1, s1i), (m4, m6, s3): (s6i, s3), (any_player, ): (s1, s1i, s2, s3, s6i), (s1, any_player): (s1, s1i, s2, s3, s6i), # s1 first (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), } select, select_many = selector(bus_address) for selection, expected in selections.items(): result = await select(*selection) assert result == expected[0], (selection, expected, result) result = await select_many(*selection) assert result == expected await asyncio.gather(*[mpris.disconnect() for mpris in mpris_players]) @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) await playerctld.disconnect()