Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package playerctl for openSUSE:Factory 
checked in at 2021-09-28 19:16:48
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/playerctl (Old)
 and      /work/SRC/openSUSE:Factory/.playerctl.new.1899 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "playerctl"

Tue Sep 28 19:16:48 2021 rev:4 rq:922013 version:2.4.1

Changes:
--------
--- /work/SRC/openSUSE:Factory/playerctl/playerctl.changes      2020-12-02 
13:58:03.453790781 +0100
+++ /work/SRC/openSUSE:Factory/.playerctl.new.1899/playerctl.changes    
2021-09-28 19:17:46.852272137 +0200
@@ -1,0 +2,13 @@
+Sat Sep 25 16:21:35 UTC 2021 - Luigi Baldoni <[email protected]>
+
+- Update to version 2.4.1
+  * Fix a crash in playerctld when players use TrackList and
+    Playlists interfaces
+  * Add the trunc() template function
+  * Allow to use playerctl as a subproject and cpp linking
+  * bugfix: subscribe to all signals when multiple template
+    functions are used
+  * bugfix: workaround for players that use uint64 values in the
+    formatter
+
+-------------------------------------------------------------------

Old:
----
  playerctl-2.3.1.tar.gz

New:
----
  playerctl-2.4.1.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ playerctl.spec ++++++
--- /var/tmp/diff_new_pack.XgO69N/_old  2021-09-28 19:17:47.300272653 +0200
+++ /var/tmp/diff_new_pack.XgO69N/_new  2021-09-28 19:17:47.304272657 +0200
@@ -1,7 +1,7 @@
 #
 # spec file for package playerctl
 #
-# Copyright (c) 2020 SUSE LLC
+# Copyright (c) 2021 SUSE LLC
 # Copyright (c) 2017 Dakota Williams <[email protected]>
 #
 # All modifications and additions to the file contributed by third parties
@@ -23,14 +23,14 @@
 %global libname lib%{name}%{sover}
 %global typelibname typelib-1_0-Playerctl-%{majorver}_%{minorver}
 Name:           playerctl
-Version:        2.3.1
+Version:        2.4.1
 Release:        0
 Summary:        MPRIS command-line controller and library for media players
 License:        LGPL-3.0-or-later
 Group:          Productivity/Multimedia/Other
 URL:            https://github.com/acrisci/playerctl
 Source0:        
https://github.com/acrisci/playerctl/archive/v%{version}/%{name}-%{version}.tar.gz
-BuildRequires:  meson >= 0.50.0
+BuildRequires:  meson >= 0.56.0
 BuildRequires:  pkgconfig
 BuildRequires:  pkgconfig(dbus-1)
 BuildRequires:  pkgconfig(gio-unix-2.0)

++++++ playerctl-2.3.1.tar.gz -> playerctl-2.4.1.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/playerctl-2.3.1/.gitignore 
new/playerctl-2.4.1/.gitignore
--- old/playerctl-2.3.1/.gitignore      2020-11-30 20:06:59.000000000 +0100
+++ new/playerctl-2.4.1/.gitignore      2021-09-21 14:23:38.000000000 +0200
@@ -3,8 +3,8 @@
 /build
 /mesonbuild
 /playerctl-fpm
-.swp
-.swo
-.swn
-.snap
-env
+*.swp
+*.swo
+*.swn
+*.snap
+/env
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/playerctl-2.3.1/CHANGELOG.md 
new/playerctl-2.4.1/CHANGELOG.md
--- old/playerctl-2.3.1/CHANGELOG.md    2020-11-30 20:06:59.000000000 +0100
+++ new/playerctl-2.4.1/CHANGELOG.md    2021-09-21 14:23:38.000000000 +0200
@@ -1,5 +1,15 @@
 # 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.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/playerctl-2.3.1/README.md 
new/playerctl-2.4.1/README.md
--- old/playerctl-2.3.1/README.md       2020-11-30 20:06:59.000000000 +0100
+++ new/playerctl-2.4.1/README.md       2021-09-21 14:23:38.000000000 +0200
@@ -116,6 +116,7 @@
 | `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                                       |
 | ------------ | ------------------------------------------------- |
@@ -135,6 +136,21 @@
 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
@@ -143,15 +159,16 @@
 
 ### No Players Found
 
-If you are using Quod Libet as your music player you need to install/activate 
a plugin for it.
-In Quod Libet open the window File -> Plugins and select the plugin called 
*MPRIS D-Bus Support*.
-
 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:
@@ -207,7 +224,7 @@
 sudo ninja -C mesonbuild install
 ```
 
-Note that you need `meson >= 0.50.0` installed. In case your distro only has 
an older version of meson in its repository you can install the newest version 
via pip:
+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
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/playerctl-2.3.1/data/mpris-dbus-interface.xml 
new/playerctl-2.4.1/data/mpris-dbus-interface.xml
--- old/playerctl-2.3.1/data/mpris-dbus-interface.xml   2020-11-30 
20:06:59.000000000 +0100
+++ new/playerctl-2.4.1/data/mpris-dbus-interface.xml   2021-09-21 
14:23:38.000000000 +0200
@@ -50,7 +50,90 @@
     <property access="read" type="b" name="CanSeek"/>
     <property access="read" type="b" name="CanControl"/>
   </interface>
-  <interface name="org.freedesktop.DBus.Properties">
+  <interface name="org.mpris.MediaPlayer2.TrackList">
+    <method name="GetTracksMetadata">
+      <arg direction="in" name="TrackIds" type="ao">
+      </arg>
+      <arg direction="out" type="aa{sv}" name="Metadata">
+      </arg>
+    </method>
+    <method name="AddTrack">
+      <arg direction="in" type="s" name="Uri">
+      </arg>
+      <arg direction="in" type="o" name="AfterTrack">
+      </arg>
+      <arg direction="in" type="b" name="SetAsCurrent">
+      </arg>
+    </method>
+    <method name="RemoveTrack">
+      <arg direction="in" type="o" name="TrackId">
+      </arg>
+    </method>
+    <method name="GoTo">
+      <arg direction="in" type="o" name="TrackId">
+      </arg>
+    </method>
+    <property name="Tracks" type="ao" access="read">
+      <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" 
value="invalidates"/>
+    </property>
+    <property name="CanEditTracks" type="b" access="read">
+      <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" 
value="true"/>
+    </property>
+    <signal name="TrackListReplaced">
+      <arg name="Tracks" type="ao">
+      </arg>
+      <arg name="CurrentTrack" type="o">
+      </arg>
+    </signal>
+    <signal name="TrackAdded">
+      <arg type="a{sv}" name="Metadata">
+      </arg>
+      <arg type="o" name="AfterTrack">
+      </arg>
+    </signal>
+    <signal name="TrackRemoved">
+      <arg type="o" name="TrackId">
+      </arg>
+    </signal>
+    <signal name="TrackMetadataChanged">
+      <arg type="o" name="TrackId">
+      </arg>
+      <arg type="a{sv}" name="Metadata">
+      </arg>
+    </signal>
+  </interface>
+  <interface name="org.mpris.MediaPlayer2.Playlists">
+    <method name="ActivatePlaylist">
+      <arg direction="in" name="PlaylistId" type="o">
+      </arg>
+    </method>
+    <method name="GetPlaylists">
+      <arg direction="in" name="Index" type="u">
+      </arg>
+      <arg direction="in" name="MaxCount" type="u">
+      </arg>
+      <arg direction="in" name="Order" type="s">
+      </arg>
+      <arg direction="in" name="ReverseOrder" type="b">
+      </arg>
+      <arg direction="out" name="Playlists" type="a(oss)">
+      </arg>
+    </method>
+    <property name="PlaylistCount" type="u" access="read">
+      <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" 
value="true"/>
+    </property>
+    <property name="Orderings" type="as" access="read">
+      <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" 
value="true"/>
+    </property>
+    <property name="ActivePlaylist" type="(b(oss))" access="read">
+      <annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" 
value="true"/>
+    </property>
+    <signal name="PlaylistChanged">
+      <arg name="Playlist" type="(oss)">
+      </arg>
+    </signal>
+    </interface>
+    <interface name="org.freedesktop.DBus.Properties">
     <method name="Get">
       <arg name="interface_name" type="s" direction="in"/>
       <arg name="property_name" type="s" direction="in"/>
@@ -78,4 +161,5 @@
       <arg name="machine_uuid" type="s" direction="out"/>
     </method>
   </interface>
+
 </node>
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/playerctl-2.3.1/data/playerctl.zsh 
new/playerctl-2.4.1/data/playerctl.zsh
--- old/playerctl-2.3.1/data/playerctl.zsh      2020-11-30 20:06:59.000000000 
+0100
+++ new/playerctl-2.4.1/data/playerctl.zsh      2021-09-21 14:23:38.000000000 
+0200
@@ -43,7 +43,7 @@
        '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 palyer to go or seek to the position' \
+       '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' \
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/playerctl-2.3.1/doc/playerctl.1.in 
new/playerctl-2.4.1/doc/playerctl.1.in
--- old/playerctl-2.3.1/doc/playerctl.1.in      2020-11-30 20:06:59.000000000 
+0100
+++ new/playerctl-2.4.1/doc/playerctl.1.in      2021-09-21 14:23:38.000000000 
+0200
@@ -251,6 +251,12 @@
 .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.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/playerctl-2.3.1/meson.build 
new/playerctl-2.4.1/meson.build
--- old/playerctl-2.3.1/meson.build     2020-11-30 20:06:59.000000000 +0100
+++ new/playerctl-2.4.1/meson.build     2021-09-21 14:23:38.000000000 +0200
@@ -1,11 +1,11 @@
 project(
   'playerctl',
   'c',
-  version: '2.3.1',
-  meson_version: '>=0.50.0'
+  version: '2.4.1',
+  meson_version: '>=0.56.0'
 )
 
-release_date = 'November 30, 2020'
+release_date = 'September 21, 2021'
 
 gnome = import('gnome')
 pkgconfig = import('pkgconfig')
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/playerctl-2.3.1/playerctl/meson.build 
new/playerctl-2.4.1/playerctl/meson.build
--- old/playerctl-2.3.1/playerctl/meson.build   2020-11-30 20:06:59.000000000 
+0100
+++ new/playerctl-2.4.1/playerctl/meson.build   2021-09-21 14:23:38.000000000 
+0200
@@ -9,7 +9,7 @@
 
 playerctl_generated = gnome.gdbus_codegen(
   'playerctl-generated',
-  join_paths(meson.source_root(), 'data', 'mpris-dbus-interface.xml'),
+  join_paths(meson.project_source_root(), 'data', 'mpris-dbus-interface.xml'),
 )
 
 headers = [
@@ -30,11 +30,10 @@
 ]
 
 # Allow including playerctl.h during compilation
-add_global_arguments(
+c_args = [
   '-DPLAYERCTL_COMPILATION',
   '-DG_LOG_DOMAIN="playerctl"',
-  language: 'c',
-)
+]
 
 enums = gnome.mkenums_simple(
   'playerctl-enum-types',
@@ -48,10 +47,13 @@
   gio_dep,
 ]
 
-symbols_file = join_paths(meson.source_root(), 'data', 'playerctl.syms')
+symbols_file = join_paths(meson.project_source_root(), 'data', 
'playerctl.syms')
 symbols_flag = '-Wl,--version-script,@0@'.format(symbols_file)
 
-playerctl_lib = both_libraries(
+# 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,
@@ -60,12 +62,14 @@
   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.get_shared_lib(),
+  link_with: playerctl_lib,
   dependencies: deps,
+  include_directories: include_directories('..'),
 )
 
 playerctl_executable = executable(
@@ -82,6 +86,7 @@
   dependencies: deps,
   include_directories: configuration_inc,
   install: true,
+  c_args: c_args,
 )
 
 install_headers(
@@ -99,7 +104,7 @@
   endif
 
   gnome.generate_gir(
-    playerctl_lib.get_shared_lib(),
+    playerctl_lib,
     sources: [
       enums,
       'playerctl-player-name.c',
@@ -118,7 +123,7 @@
 endif
 
 pkgconfig.generate(
-  libraries: playerctl_lib.get_shared_lib(),
+  libraries: playerctl_lib,
   subdirs: 'playerctl',
   version: meson.project_version(),
   name: 'Playerctl',
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/playerctl-2.3.1/playerctl/playerctl-cli.c 
new/playerctl-2.4.1/playerctl/playerctl-cli.c
--- old/playerctl-2.3.1/playerctl/playerctl-cli.c       2020-11-30 
20:06:59.000000000 +0100
+++ new/playerctl-2.4.1/playerctl/playerctl-cli.c       2021-09-21 
14:23:38.000000000 +0200
@@ -228,7 +228,7 @@
     GError *tmp_error = NULL;
     gchar *instance = pctl_player_get_instance(player);
 
-    // XXX there is no CanStop propery on the mpris player. CanPlay is supposed
+    // 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;
@@ -936,7 +936,7 @@
     }
 
     if (!one_selected && !no_status_error_messages) {
-        g_printerr("No players were found\n");
+        g_printerr("No players found\n");
     }
 
     pctl_player_name_list_destroy(player_names_list);
@@ -1103,6 +1103,7 @@
             if (any_index == INT_MAX) {
                 any_index = i;
             }
+            ++i;
             continue;
         }
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/playerctl-2.3.1/playerctl/playerctl-daemon.c 
new/playerctl-2.4.1/playerctl/playerctl-daemon.c
--- old/playerctl-2.3.1/playerctl/playerctl-daemon.c    2020-11-30 
20:06:59.000000000 +0100
+++ new/playerctl-2.4.1/playerctl/playerctl-daemon.c    2021-09-21 
14:23:38.000000000 +0200
@@ -9,15 +9,29 @@
 #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 {
@@ -27,6 +41,8 @@
     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;
@@ -34,10 +50,21 @@
     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;
 }
 
@@ -56,6 +83,12 @@
     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);
@@ -85,12 +118,31 @@
     GVariantDict cached_properties;
     GVariantIter iter;
     GVariant *child;
-    bool is_player_interface = false;  // otherwise, the root interface
+    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);
-        is_player_interface = true;
+    } 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);
@@ -102,13 +154,14 @@
         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 (is_player_interface && g_strcmp0(key, "Position") == 0) {
+        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;
@@ -133,16 +186,31 @@
         g_variant_unref(child);
     }
 
-    if (is_player_interface) {
+    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));
-    } else {
+        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;
@@ -215,7 +283,7 @@
         }
 
         GVariant *root_children[3] = {
-            g_variant_new_string(PLAYER_INTERFACE),
+            g_variant_new_string(ROOT_INTERFACE),
             player->root_properties,
             g_variant_new_array(G_VARIANT_TYPE_STRING, NULL, 0),
         };
@@ -226,6 +294,42 @@
             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);
@@ -272,6 +376,41 @@
             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;
@@ -495,6 +634,90 @@
     "    <property name=\"CanSeek\" type=\"b\" access=\"read\"/>\n"
     "    <property name=\"CanControl\" type=\"b\" access=\"read\"/>\n"
     "  </interface>\n"
+    "  <interface name=\"org.mpris.MediaPlayer2.TrackList\">\n"
+    "    <method name=\"GetTracksMetadata\">\n"
+    "      <arg direction=\"in\" name=\"TrackIds\" type=\"ao\">\n"
+    "      </arg>\n"
+    "      <arg direction=\"out\" type=\"aa{sv}\" name=\"Metadata\">\n"
+    "      </arg>\n"
+    "    </method>\n"
+    "    <method name=\"AddTrack\">\n"
+    "      <arg direction=\"in\" type=\"s\" name=\"Uri\">\n"
+    "      </arg>\n"
+    "      <arg direction=\"in\" type=\"o\" name=\"AfterTrack\">\n"
+    "      </arg>\n"
+    "      <arg direction=\"in\" type=\"b\" name=\"SetAsCurrent\">\n"
+    "      </arg>\n"
+    "    </method>\n"
+    "    <method name=\"RemoveTrack\">\n"
+    "      <arg direction=\"in\" type=\"o\" name=\"TrackId\">\n"
+    "      </arg>\n"
+    "    </method>\n"
+    "    <method name=\"GoTo\">\n"
+    "      <arg direction=\"in\" type=\"o\" name=\"TrackId\">\n"
+    "      </arg>\n"
+    "    </method>\n"
+    "    <property name=\"Tracks\" type=\"ao\" access=\"read\">\n"
+    "      <annotation 
name=\"org.freedesktop.DBus.Property.EmitsChangedSignal\" "
+    "value=\"invalidates\"/>\n"
+    "    </property>\n"
+    "    <property name=\"CanEditTracks\" type=\"b\" access=\"read\">\n"
+    "      <annotation 
name=\"org.freedesktop.DBus.Property.EmitsChangedSignal\" value=\"true\"/>\n"
+    "    </property>\n"
+    "    <signal name=\"TrackListReplaced\">\n"
+    "      <arg name=\"Tracks\" type=\"ao\">\n"
+    "      </arg>\n"
+    "      <arg name=\"CurrentTrack\" type=\"o\">\n"
+    "      </arg>\n"
+    "    </signal>\n"
+    "    <signal name=\"TrackAdded\">\n"
+    "      <arg type=\"a{sv}\" name=\"Metadata\">\n"
+    "      </arg>\n"
+    "      <arg type=\"o\" name=\"AfterTrack\">\n"
+    "      </arg>\n"
+    "    </signal>\n"
+    "    <signal name=\"TrackRemoved\">\n"
+    "      <arg type=\"o\" name=\"TrackId\">\n"
+    "      </arg>\n"
+    "    </signal>\n"
+    "    <signal name=\"TrackMetadataChanged\">\n"
+    "      <arg type=\"o\" name=\"TrackId\">\n"
+    "      </arg>\n"
+    "      <arg type=\"a{sv}\" name=\"Metadata\">\n"
+    "      </arg>\n"
+    "    </signal>\n"
+    "  </interface>\n"
+    "  <interface name=\"org.mpris.MediaPlayer2.Playlists\">\n"
+    "    <method name=\"ActivatePlaylist\">\n"
+    "      <arg direction=\"in\" name=\"PlaylistId\" type=\"o\">\n"
+    "      </arg>\n"
+    "    </method>\n"
+    "    <method name=\"GetPlaylists\">\n"
+    "      <arg direction=\"in\" name=\"Index\" type=\"u\">\n"
+    "      </arg>\n"
+    "      <arg direction=\"in\" name=\"MaxCount\" type=\"u\">\n"
+    "      </arg>\n"
+    "      <arg direction=\"in\" name=\"Order\" type=\"s\">\n"
+    "      </arg>\n"
+    "      <arg direction=\"in\" name=\"ReverseOrder\" type=\"b\">\n"
+    "      </arg>\n"
+    "      <arg direction=\"out\" name=\"Playlists\" type=\"a(oss)\">\n"
+    "      </arg>\n"
+    "    </method>\n"
+    "    <property name=\"PlaylistCount\" type=\"u\" access=\"read\">\n"
+    "      <annotation 
name=\"org.freedesktop.DBus.Property.EmitsChangedSignal\" value=\"true\"/>\n"
+    "    </property>\n"
+    "    <property name=\"Orderings\" type=\"as\" access=\"read\">\n"
+    "      <annotation 
name=\"org.freedesktop.DBus.Property.EmitsChangedSignal\" value=\"true\"/>\n"
+    "    </property>\n"
+    "    <property name=\"ActivePlaylist\" type=\"(b(oss))\" 
access=\"read\">\n"
+    "      <annotation 
name=\"org.freedesktop.DBus.Property.EmitsChangedSignal\" value=\"true\"/>\n"
+    "    </property>\n"
+    "    <signal name=\"PlaylistChanged\">\n"
+    "      <arg name=\"Playlist\" type=\"(oss)\">\n"
+    "      </arg>\n"
+    "    </signal>\n"
+    "  </interface>\n"
     "</node>\n";
 
 static void proxy_method_call_async_callback(GObject *source_object, 
GAsyncResult *res,
@@ -536,6 +759,10 @@
     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,
@@ -575,6 +802,9 @@
     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,
@@ -586,6 +816,11 @@
     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));
@@ -596,6 +831,11 @@
                 "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));
@@ -606,19 +846,26 @@
                 "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) {
-        // glib library error
+        // Fail on unknown properties.
+        // FIXME: Should return a DBus error and not crash
         g_error("unknown property: %s", property_name);
         assert(false);
     }
@@ -626,26 +873,48 @@
     return context_player_names_to_gvariant(ctx);
 }
 
-static GDBusInterfaceVTable vtable_player = 
{player_method_call_proxy_callback, NULL, NULL, {0}};
-
-static GDBusInterfaceVTable vtable_root = {player_method_call_proxy_callback, 
NULL, NULL, {0}};
+/**
+ * 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_root, user_data, NULL, &error);
+                                      &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_player, user_data, NULL, &error);
+                                      &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);
@@ -792,6 +1061,26 @@
                                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) {
@@ -846,6 +1135,9 @@
         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;
@@ -1008,6 +1300,7 @@
         exit(0);
     }
 
+    // Setup DBus connection
     GDBusConnectionFlags connection_flags = 
G_DBUS_CONNECTION_FLAGS_AUTHENTICATION_CLIENT |
                                             
G_DBUS_CONNECTION_FLAGS_MESSAGE_BUS_CONNECTION;
 
@@ -1083,11 +1376,30 @@
     }
     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);
@@ -1100,6 +1412,7 @@
     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,
@@ -1120,6 +1433,7 @@
                 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);
@@ -1131,11 +1445,13 @@
             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);
@@ -1147,6 +1463,42 @@
             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);
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/playerctl-2.3.1/playerctl/playerctl-formatter.c 
new/playerctl-2.4.1/playerctl/playerctl-formatter.c
--- old/playerctl-2.3.1/playerctl/playerctl-formatter.c 2020-11-30 
20:06:59.000000000 +0100
+++ new/playerctl-2.4.1/playerctl/playerctl-formatter.c 2021-09-21 
14:23:38.000000000 +0200
@@ -93,7 +93,9 @@
             }
             break;
         case TOKEN_FUNCTION:
-            return token_list_contains_key(token->args, key);
+            if (token_list_contains_key(token->args, key)) {
+                return TRUE;
+            }
         default:
             break;
         }
@@ -481,14 +483,23 @@
         return g_variant_new("s", "");
     }
 
-    // mpris durations are represented as int64 in microseconds
-    if (!g_variant_type_equal(g_variant_get_type(value), 
G_VARIANT_TYPE_INT64)) {
+    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 position can only be called on int64 values");
+                    "function duration can only be called on track position 
values");
         return NULL;
     }
 
-    gint64 duration = g_variant_get_int64(value);
     gint64 seconds = (duration / 1000000) % 60;
     gint64 minutes = (duration / 1000000 / 60) % 60;
     gint64 hours = (duration / 1000000 / 60 / 60);
@@ -608,15 +619,55 @@
     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.
+    // 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;
     }
@@ -625,8 +676,12 @@
 }
 
 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 {
@@ -830,6 +885,7 @@
     {"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},
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/playerctl-2.3.1/playerctl/playerctl-player-manager.c 
new/playerctl-2.4.1/playerctl/playerctl-player-manager.c
--- old/playerctl-2.3.1/playerctl/playerctl-player-manager.c    2020-11-30 
20:06:59.000000000 +0100
+++ new/playerctl-2.4.1/playerctl/playerctl-player-manager.c    2021-09-21 
14:23:38.000000000 +0200
@@ -249,9 +249,9 @@
     }
 
     if (!g_variant_is_of_type(parameters, G_VARIANT_TYPE("(sss)"))) {
-        g_warning("Got unknown parameters on org.freedesktop.DBus "
-                  "NameOwnerChange signal: %s",
-                  g_variant_get_type_string(parameters));
+        g_debug("Got unknown parameters on org.freedesktop.DBus "
+                "NameOwnerChange signal: %s",
+                g_variant_get_type_string(parameters));
         return;
     }
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/playerctl-2.3.1/playerctl/playerctl-player.c 
new/playerctl-2.4.1/playerctl/playerctl-player.c
--- old/playerctl-2.3.1/playerctl/playerctl-player.c    2020-11-30 
20:06:59.000000000 +0100
+++ new/playerctl-2.4.1/playerctl/playerctl-player.c    2021-09-21 
14:23:38.000000000 +0200
@@ -124,7 +124,7 @@
         // 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_warning("mpris:trackid is a string, not a D-Bus object reference");
+        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);
     }
 
@@ -197,7 +197,7 @@
         // 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_warning("Playback status not set on track change; getting status 
from interface instead");
+        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"),
@@ -270,7 +270,7 @@
                 g_signal_emit(self, connection_signals[PLAYBACK_STATUS], 
quark, status);
             }
         } else {
-            g_warning("%s: got unknown playback state: %s", instance, 
status_str);
+            g_debug("%s: got unknown playback state: %s", instance, 
status_str);
         }
 
         g_variant_unref(playback_status);
@@ -306,7 +306,7 @@
     if (!metadata) {
         // XXX: Ugly spotify workaround. Spotify does not seem to use the 
property
         // cache. We have to get the properties directly.
-        g_warning("Spotify does not use the D-Bus property cache, getting 
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"),
@@ -388,7 +388,7 @@
             g_value_set_enum(value, status);
         } else {
             if (status_str != NULL) {
-                g_warning("got unknown loop status: %s", status_str);
+                g_debug("got unknown loop status: %s", status_str);
             }
             g_value_set_enum(value, PLAYERCTL_LOOP_STATUS_NONE);
         }
@@ -864,7 +864,7 @@
 
     GList *exact_match = pctl_player_name_find(names, name, 
pctl_bus_type_to_source(bus_type));
     if (exact_match != NULL) {
-        g_debug("Geting bus name for player %s by exact match", name);
+        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);
@@ -874,7 +874,7 @@
     GList *instance_match =
         pctl_player_name_find_instance(names, name, 
pctl_bus_type_to_source(bus_type));
     if (instance_match != NULL) {
-        g_debug("Geting bus name for player %s by instance match", name);
+        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);
@@ -939,7 +939,7 @@
                 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_warning("Bus address set incorrectly, cannot get bus");
+                    g_debug("Bus address set incorrectly, cannot get bus");
                     g_clear_error(&tmp_error);
                     continue;
                 }
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/playerctl-2.3.1/playerctl/playerctl.h 
new/playerctl-2.4.1/playerctl/playerctl.h
--- old/playerctl-2.3.1/playerctl/playerctl.h   2020-11-30 20:06:59.000000000 
+0100
+++ new/playerctl-2.4.1/playerctl/playerctl.h   2021-09-21 14:23:38.000000000 
+0200
@@ -22,12 +22,19 @@
 
 #define __PLAYERCTL_INSIDE__
 
+#ifdef __cplusplus
+extern "C" {
+#endif
+
 #include <playerctl/playerctl-enum-types.h>
 #include <playerctl/playerctl-player-manager.h>
 #include <playerctl/playerctl-player-name.h>
 #include <playerctl/playerctl-player.h>
 #include <playerctl/playerctl-version.h>
 
+#ifdef __cplusplus
+} // extern "C"
+#endif
 #undef __PLAYERCTL_INSIDE__
 
 #endif /* __PLAYERCTL_H__ */
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/playerctl-2.3.1/snap/snapcraft.yaml 
new/playerctl-2.4.1/snap/snapcraft.yaml
--- old/playerctl-2.3.1/snap/snapcraft.yaml     2020-11-30 20:06:59.000000000 
+0100
+++ new/playerctl-2.4.1/snap/snapcraft.yaml     2021-09-21 14:23:38.000000000 
+0200
@@ -30,15 +30,20 @@
     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 ]
+    slots: [ dbus-svc, system-observe, desktop-legacy ]
     environment:
       LD_LIBRARY_PATH: $SNAP/usr/local/lib/$SNAPCRAFT_ARCH_TRIPLET
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/playerctl-2.3.1/test/test_format.py 
new/playerctl-2.4.1/test/test_format.py
--- old/playerctl-2.3.1/test/test_format.py     2020-11-30 20:06:59.000000000 
+0100
+++ new/playerctl-2.4.1/test/test_format.py     2021-09-21 14:23:38.000000000 
+0200
@@ -114,6 +114,10 @@
     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()
 

Reply via email to