/* * Copyright 2013 Canonical Ltd. * Copyright 2021 Robert Tari * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * This program 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . * * Authors: * Lars Uebernickel * Robert Tari */ /** * MediaPlayerMpris represents an MRPIS-capable media player. */ public class MediaPlayerMpris: MediaPlayer { public MediaPlayerMpris (DesktopAppInfo appinfo) { this.appinfo = appinfo; } /** Desktop id of the player */ public override string id { get { return this.appinfo.get_id (); } } /** Display name of the player */ public override string name { get { return this.appinfo.get_name (); } } /** Application icon of the player */ public override Icon? icon { get { return this.appinfo.get_icon (); } } /** * True if an instance of the player is currently running. * * See also: attach(), detach() */ public override bool is_running { get { return this.proxy != null; } } /** Name of the player on the bus, if an instance is currently running */ public override string dbus_name { get { return this._dbus_name; } } public override string state { get; set; default = "Paused"; } public override MediaPlayer.Track? current_track { get; set; } public override bool can_raise { get { return this.root != null ? this.root.CanRaise : true; } } public override bool can_do_play { get { return this.proxy.CanPlay; } } public override bool can_do_prev { get { return this.proxy.CanGoPrevious; } } public override bool can_do_next { get { return this.proxy.CanGoNext; } } /** * Attach this object to a process of the associated media player. The player must own @dbus_name and * implement the org.mpris.MediaPlayer2.Player interface. * * Only one player can be attached at any given time. Use detach() to detach a player. * * This method does not block. If it is successful, "is-running" will be set to %TRUE. */ public void attach (MprisRoot root, string dbus_name) { return_if_fail (this._dbus_name == null && this.proxy == null); this.root = root; this.notify_property ("can-raise"); this._dbus_name = dbus_name; Bus.get_proxy.begin (BusType.SESSION, dbus_name, "/org/mpris/MediaPlayer2", DBusProxyFlags.GET_INVALIDATED_PROPERTIES, null, got_proxy); Bus.get_proxy.begin (BusType.SESSION, dbus_name, "/org/mpris/MediaPlayer2", DBusProxyFlags.GET_INVALIDATED_PROPERTIES, null, got_playlists_proxy); } /** * Detach this object from a process running the associated media player. * * See also: attach() */ public void detach () { this.root = null; this.proxy = null; this._dbus_name = null; this.notify_property ("is-running"); this.notify_property ("can-raise"); this.state = "Paused"; this.current_track = null; } /** * Activate the associated media player. * * Note: this will _not_ call attach(), because it doesn't know on which dbus-name the player will appear. * Use attach() to attach this object to a running instance of the player. */ public override void activate () { try { if (this.proxy == null) { this.appinfo.launch (null, null); this.state = "Launching"; } else if (this.root != null && this.root.CanRaise) { this.root.Raise.begin (); } } catch (Error e) { warning ("unable to activate %s: %s", appinfo.get_name (), e.message); } } /** * Toggles playing status. */ public override void play_pause () { if (this.proxy != null) { this.proxy.PlayPause.begin (); } else if (this.state != "Launching") { this.play_when_attached = true; this.activate (); } } /** * Skips to the next track. */ public override void next () { if (this.proxy != null) this.proxy.Next.begin (); } /** * Skips to the previous track. */ public override void previous () { if (this.proxy != null) this.proxy.Previous.begin (); } public override uint get_n_playlists () { return this.playlists != null ? this.playlists.length : 0; } public override string get_playlist_id (int index) { return_val_if_fail (index < this.playlists.length, ""); return this.playlists[index].path; } public override string get_playlist_name (int index) { return_val_if_fail (index < this.playlists.length, ""); return this.playlists[index].name; } public override void activate_playlist_by_name (string name) { if (this.playlists_proxy != null) this.playlists_proxy.ActivatePlaylist.begin (new ObjectPath (name)); } DesktopAppInfo appinfo; MprisPlayer? proxy; MprisPlaylists ?playlists_proxy; string _dbus_name; bool play_when_attached = false; MprisRoot root; PlaylistDetails[] playlists = null; void got_proxy (Object? obj, AsyncResult res) { try { this.proxy = Bus.get_proxy.end (res); /* Connecting to GDBusProxy's "g-properties-changed" signal here, because vala's dbus objects don't * emit notify signals */ var gproxy = this.proxy as DBusProxy; gproxy.g_properties_changed.connect (this.proxy_properties_changed); this.notify_property ("is-running"); this.state = this.proxy.PlaybackStatus != null ? this.proxy.PlaybackStatus : "Unknown"; this.update_current_track (gproxy.get_cached_property ("Metadata")); if (this.play_when_attached) { /* wait a little before calling PlayPause, some players need some time to set themselves up */ Timeout.add (1000, () => { proxy.PlayPause.begin (); return Source.REMOVE; } ); this.play_when_attached = false; } } catch (Error e) { this._dbus_name = null; warning ("unable to attach to media player: %s", e.message); } } void fetch_playlists () { /* The proxy is created even when the interface is not supported. GDBusProxy will return 0 for the PlaylistCount property in that case. */ if (this.playlists_proxy != null && this.playlists_proxy.PlaylistCount > 0) { this.playlists_proxy.GetPlaylists.begin (0, 100, "Alphabetical", false, (obj, res) => { try { this.playlists = playlists_proxy.GetPlaylists.end (res); this.playlists_changed (); } catch (Error e) { warning ("could not fetch playlists: %s", e.message); this.playlists = null; } }); } else { this.playlists = null; this.playlists_changed (); } } void got_playlists_proxy (Object? obj, AsyncResult res) { try { this.playlists_proxy = Bus.get_proxy.end (res); var gproxy = this.proxy as DBusProxy; gproxy.g_properties_changed.connect (this.playlists_proxy_properties_changed); } catch (Error e) { warning ("unable to create mpris plalists proxy: %s", e.message); return; } Timeout.add (500, () => { this.fetch_playlists (); return Source.REMOVE; } ); } /* some players (e.g. Spotify) don't follow the spec closely and pass single strings in metadata fields * where an array of string is expected */ static string sanitize_metadata_value (Variant? v) { if (v == null) return ""; else if (v.is_of_type (VariantType.STRING)) return v.get_string (); else if (v.is_of_type (VariantType.STRING_ARRAY)) return string.joinv (",", v.get_strv ()); warn_if_reached (); return ""; } void proxy_properties_changed (DBusProxy proxy, Variant changed_properties, string[] invalidated_properties) { if (changed_properties.lookup ("PlaybackStatus", "s", null)) { this.state = this.proxy.PlaybackStatus != null ? this.proxy.PlaybackStatus : "Unknown"; } if (changed_properties.lookup ("CanGoNext", "b", null) || changed_properties.lookup ("CanGoPrevious", "b", null) || changed_properties.lookup ("CanPlay", "b", null) || changed_properties.lookup ("CanPause", "b", null)) { this.playbackstatus_changed (); } var metadata = changed_properties.lookup_value ("Metadata", VariantType.VARDICT); if (metadata != null) this.update_current_track (metadata); } void playlists_proxy_properties_changed (DBusProxy proxy, Variant changed_properties, string[] invalidated_properties) { if (changed_properties.lookup ("PlaylistCount", "u", null)) this.fetch_playlists (); } void update_current_track (Variant? metadata) { if (metadata != null) { this.current_track = new Track ( sanitize_metadata_value (metadata.lookup_value ("xesam:artist", null)), sanitize_metadata_value (metadata.lookup_value ("xesam:title", null)), sanitize_metadata_value (metadata.lookup_value ("xesam:album", null)), sanitize_metadata_value (metadata.lookup_value ("mpris:artUrl", null)) ); } else { this.current_track = null; } } }