summaryrefslogtreecommitdiff
path: root/lib/mpris
diff options
context:
space:
mode:
Diffstat (limited to 'lib/mpris')
-rw-r--r--lib/mpris/cli.vala331
-rw-r--r--lib/mpris/config.vala.in6
-rw-r--r--lib/mpris/ifaces.vala60
-rw-r--r--lib/mpris/meson.build94
-rw-r--r--lib/mpris/meson_options.txt11
-rw-r--r--lib/mpris/mpris.vala66
-rw-r--r--lib/mpris/player.vala467
-rw-r--r--lib/mpris/version1
8 files changed, 1036 insertions, 0 deletions
diff --git a/lib/mpris/cli.vala b/lib/mpris/cli.vala
new file mode 100644
index 0000000..b71def9
--- /dev/null
+++ b/lib/mpris/cli.vala
@@ -0,0 +1,331 @@
+namespace AstalMpris {
+static bool help;
+static bool version;
+static bool list;
+static bool raw;
+[CCode (array_length = false, array_null_terminated = true)]
+static string[] players;
+
+const OptionEntry[] options = {
+ { "version", 'v', OptionFlags.NONE, OptionArg.NONE, ref version, null, null },
+ { "help", 'h', OptionFlags.NONE, OptionArg.NONE, ref help, null, null },
+ { "player", 'p', OptionFlags.NONE, OptionArg.STRING_ARRAY, ref players, null, null },
+ { "list", 'l', OptionFlags.NONE, OptionArg.NONE, ref list, null, null },
+ { "raw", 'r', OptionFlags.NONE, OptionArg.NONE, ref raw, null, null },
+ { null },
+};
+
+int main(string[] argv) {
+ try {
+ var opts = new OptionContext();
+ opts.add_main_entries(options, null);
+ opts.set_help_enabled(false);
+ opts.set_ignore_unknown_options(false);
+ opts.parse(ref argv);
+ } catch (OptionError err) {
+ printerr(err.message);
+ return 1;
+ }
+
+ if (help) {
+ print("Usage:\n");
+ print(" %s [flags] [command]\n\n", argv[0]);
+ print("Flags:\n");
+ print(" -h, --help Print this help and exit\n");
+ print(" -v, --version Print version number and exit\n");
+ print(" -l, --list List available players\n");
+ print(" -p, --player Operate on given player\n");
+ print(" -r, --raw Print single line json info\n");
+ print("\nCommands:\n");
+ print(" info Print info about player\n");
+ print(" monitor Monitor changes\n");
+ print(" play Play track\n");
+ print(" pause Pause track\n");
+ print(" play-pause Play if paused, Pause if playing\n");
+ print(" stop Stop player\n");
+ print(" next Play next track\n");
+ print(" previous Play previous track\n");
+ print(" quit Quit player\n");
+ print(" raise Ask compositor to raise the player\n");
+ print(" position [OFFSET][+/-/%] Set position of player\n");
+ print(" volume [LEVEL][+/-/%] Set volume of player\n");
+ print(" loop [STATUS] One of: \"None\", \"Track\", \"Playlist\"\n");
+ print(" shuffle [STATUS] One of: \"On\", \"Off\", \"Toggle\"\n");
+ return 0;
+ }
+
+ if (version) {
+ print(VERSION);
+ return 0;
+ }
+
+ var mpris = new Mpris();
+ var mpris_players = new List<Player>();
+
+ if (list) {
+ foreach (var p in mpris.players)
+ print("%s\n", p.bus_name.replace(Mpris.PREFIX, ""));
+
+ return 0;
+ }
+
+ if (players.length > 0) {
+ foreach (var name in players)
+ mpris_players.append(new Player(name));
+ } else {
+ foreach (var p in mpris.players)
+ mpris_players.append(p);
+ }
+
+ var cmd = argv[1];
+ var arg = argv[2];
+
+ switch (cmd) {
+ case "monitor":
+ return do_monitor(mpris);
+
+ case "info":
+ print_players(mpris_players.copy());
+ break;
+
+ case "play":
+ foreach (var player in mpris_players)
+ player.play();
+ break;
+
+ case "pause":
+ foreach (var player in mpris_players)
+ player.pause();
+ break;
+
+ case "play-pause":
+ foreach (var player in mpris_players)
+ player.play_pause();
+ break;
+
+ case "stop":
+ foreach (var player in mpris_players)
+ player.stop();
+ break;
+
+ case "next":
+ foreach (var player in mpris_players)
+ player.next();
+ break;
+
+ case "previous":
+ foreach (var player in mpris_players)
+ player.previous();
+ break;
+
+ case "raise":
+ foreach (var player in mpris_players)
+ player.raise();
+ break;
+
+ case "quit":
+ foreach (var player in mpris_players)
+ player.quit();
+ break;
+
+ case "position":
+ foreach (var player in mpris_players) {
+ if (do_position(player, arg) != 0)
+ return 1;
+ }
+ break;
+
+ case "volume":
+ foreach (var player in mpris_players) {
+ if (do_volume(player, arg) != 0)
+ return 1;
+ }
+ break;
+
+ case "loop":
+ foreach (var player in mpris_players) {
+ if (do_loop(player, arg) != 0)
+ return 1;
+ }
+ break;
+
+ case "shuffle":
+ foreach (var player in mpris_players) {
+ if (do_shuffle(player, arg) != 0)
+ return 1;
+ }
+ break;
+
+ case "open":
+ if (arg == null) {
+ stderr.printf("missing open arg");
+ return 1;
+ }
+
+ foreach (var player in mpris_players)
+ player.open_uri(arg);
+ break;
+
+ default:
+ if (cmd == null)
+ stderr.printf("missing command\n");
+ else
+ stderr.printf(@"unknown command \"$cmd\"\n");
+ return 1;
+ }
+
+ return 0;
+}
+
+Json.Node to_json(Player p) {
+ var uris = new Json.Builder().begin_array();
+ foreach (var uri in p.supported_uri_schemas)
+ uris.add_string_value(uri);
+
+ uris.end_array();
+
+ return new Json.Builder().begin_object()
+ .set_member_name("bus_name").add_string_value(p.bus_name)
+ .set_member_name("available").add_boolean_value(p.available)
+ .set_member_name("identity").add_string_value(p.identity)
+ .set_member_name("entry").add_string_value(p.entry)
+ .set_member_name("supported_uri_schemas").add_value(uris.get_root())
+ .set_member_name("loop_status").add_string_value(p.loop_status.to_string())
+ .set_member_name("shuffle_status").add_string_value(p.shuffle_status.to_string())
+ .set_member_name("rate").add_double_value(p.rate)
+ .set_member_name("volume").add_double_value(p.volume)
+ .set_member_name("position").add_double_value(p.position)
+ .set_member_name("cover_art").add_string_value(p.cover_art)
+ .set_member_name("metadata").add_value(Json.gvariant_serialize(
+ p.metadata != null ? p.metadata : new HashTable<string, Variant>(str_hash, str_equal)))
+ .end_object()
+ .get_root();
+}
+
+void print_players(List<weak Player> players) {
+ var json = new Json.Builder().begin_array();
+
+ foreach (var p in players)
+ json.add_value(to_json(p));
+
+ stdout.printf("%s\n", Json.to_string(json.end_array().get_root(), !raw));
+ stdout.flush();
+}
+
+int do_monitor(Mpris mpris) {
+ print_players(mpris.players);
+ foreach (var player in mpris.players) {
+ player.notify.connect(() => print_players(mpris.players));
+ }
+
+ mpris.player_added.connect((player) => {
+ player.notify.connect(() => print_players(mpris.players));
+ });
+
+ mpris.player_closed.connect(() => {
+ print_players(mpris.players);
+ });
+
+ new MainLoop(null, false).run();
+ return 0;
+}
+
+int do_position(Player player, string? arg) {
+ if (arg == null) {
+ stderr.printf("missing position argument\n");
+ return 1;
+ }
+
+ else if (arg.has_suffix("%")) {
+ var percent = double.parse(arg.slice(0, -1)) / 100;
+ player.position = player.length * percent;
+ }
+
+ else if (arg.has_suffix("-")) {
+ player.position += double.parse(arg.slice(0, -1)) * -1;
+ }
+
+ else if (arg.has_suffix("+")) {
+ player.position += double.parse(arg.slice(0, -1));
+ }
+
+ else {
+ player.position = double.parse(arg);
+ }
+
+ return 0;
+}
+
+int do_volume(Player player, string? arg) {
+ if (arg == null) {
+ stderr.printf("missing volume argument\n");
+ return 1;
+ }
+
+ else if (arg.has_suffix("%")) {
+ player.volume = double.parse(arg.slice(0, -1)) / 100;
+ }
+
+ else if (arg.has_suffix("-")) {
+ player.volume += (double.parse(arg.slice(0, -1)) * -1) / 100;
+ }
+
+ else if (arg.has_suffix("+")) {
+ player.volume += double.parse(arg.slice(0, -1)) / 100;
+ }
+
+ else {
+ player.volume = double.parse(arg);
+ }
+
+ return 0;
+}
+
+int do_loop(Player player, string? arg) {
+ if (arg == null) {
+ player.loop();
+ return 0;
+ }
+
+ switch (arg) {
+ case "None":
+ player.loop_status = Loop.NONE;
+ break;
+ case "Track":
+ player.loop_status = Loop.TRACK;
+ break;
+ case "Playlist":
+ player.loop_status = Loop.PLAYLIST;
+ break;
+ default:
+ stderr.printf(@"unknown shuffle status \"$arg\"");
+ return 1;
+ }
+
+ return 0;
+}
+
+int do_shuffle(Player player, string? arg) {
+ if (arg == null) {
+ player.shuffle();
+ return 1;
+ }
+
+ switch (arg) {
+ case "On":
+ player.shuffle_status = Shuffle.ON;
+ break;
+ case "Off":
+ player.shuffle_status = Shuffle.OFF;
+ break;
+ case "Toggle":
+ player.shuffle();
+ break;
+ default:
+ stderr.printf(@"unknown shuffle status \"$arg\"");
+ return 1;
+ }
+
+ return 0;
+}
+}
diff --git a/lib/mpris/config.vala.in b/lib/mpris/config.vala.in
new file mode 100644
index 0000000..767c4bd
--- /dev/null
+++ b/lib/mpris/config.vala.in
@@ -0,0 +1,6 @@
+namespace AstalMpris {
+ public const int MAJOR_VERSION = @MAJOR_VERSION@;
+ public const int MINOR_VERSION = @MINOR_VERSION@;
+ public const int MICRO_VERSION = @MICRO_VERSION@;
+ public const string VERSION = "@VERSION@";
+}
diff --git a/lib/mpris/ifaces.vala b/lib/mpris/ifaces.vala
new file mode 100644
index 0000000..4a9d715
--- /dev/null
+++ b/lib/mpris/ifaces.vala
@@ -0,0 +1,60 @@
+namespace AstalMpris {
+[DBus (name="org.freedesktop.DBus")]
+internal interface DBusImpl : DBusProxy {
+ public abstract string[] list_names () throws GLib.Error;
+ public signal void name_owner_changed (string name, string old_owner, string new_owner);
+}
+
+[DBus (name="org.freedesktop.DBus.Properties")]
+internal interface PropsIface : DBusProxy {
+ public abstract HashTable<string, Variant> get_all (string iface);
+}
+
+[DBus (name="org.mpris.MediaPlayer2")]
+internal interface IMpris : PropsIface {
+ public abstract void raise () throws GLib.Error;
+ public abstract void quit () throws GLib.Error;
+
+ public abstract bool can_quit { get; }
+ public abstract bool fullscreen { get; set; }
+ public abstract bool can_set_fullscreen { get; }
+ public abstract bool can_raise { get; }
+ public abstract bool has_track_list { get; }
+ public abstract string identity { owned get; }
+ public abstract string desktop_entry { owned get; }
+ public abstract string[] supported_uri_schemas { owned get; }
+ public abstract string[] supported_mime_types { owned get; }
+}
+
+[DBus (name="org.mpris.MediaPlayer2.Player")]
+internal interface IPlayer : IMpris {
+ public abstract void next () throws GLib.Error;
+ public abstract void previous () throws GLib.Error;
+ public abstract void pause () throws GLib.Error;
+ public abstract void play_pause () throws GLib.Error;
+ public abstract void stop () throws GLib.Error;
+ public abstract void play () throws GLib.Error;
+ public abstract void seek (int64 offset) throws GLib.Error;
+ public abstract void set_position (ObjectPath track_id, int64 position) throws GLib.Error;
+ public abstract void open_uri (string uri) throws GLib.Error;
+
+ public signal void seeked (int64 position);
+
+ public abstract string playback_status { owned get; }
+ public abstract string loop_status { owned get; set; }
+ public abstract double rate { get; set; }
+ public abstract bool shuffle { get; set; }
+ public abstract HashTable<string,Variant> metadata { owned get; }
+ public abstract double volume { get; set; }
+ public abstract int64 position { get; }
+ public abstract double minimum_rate { get; set; }
+ public abstract double maximum_rate { get; set; }
+
+ public abstract bool can_go_next { get; }
+ public abstract bool can_go_previous { get; }
+ public abstract bool can_play { get; }
+ public abstract bool can_pause { get; }
+ public abstract bool can_seek { get; }
+ public abstract bool can_control { get; }
+}
+}
diff --git a/lib/mpris/meson.build b/lib/mpris/meson.build
new file mode 100644
index 0000000..c9a5c53
--- /dev/null
+++ b/lib/mpris/meson.build
@@ -0,0 +1,94 @@
+project(
+ 'astal-mpris',
+ 'vala',
+ 'c',
+ version: run_command('cat', join_paths(meson.project_source_root(), 'version')).stdout().strip(),
+ meson_version: '>= 0.62.0',
+ default_options: [
+ 'warning_level=2',
+ 'werror=false',
+ 'c_std=gnu11',
+ ],
+)
+
+assert(
+ get_option('lib') or get_option('cli'),
+ 'Either lib or cli option must be set to true.',
+)
+
+version_split = meson.project_version().split('.')
+api_version = version_split[0] + '.' + version_split[1]
+gir = 'AstalMpris-' + api_version + '.gir'
+typelib = 'AstalMpris-' + api_version + '.typelib'
+
+config = configure_file(
+ input: 'config.vala.in',
+ output: 'config.vala',
+ configuration: {
+ 'VERSION': meson.project_version(),
+ 'MAJOR_VERSION': version_split[0],
+ 'MINOR_VERSION': version_split[1],
+ 'MICRO_VERSION': version_split[2],
+ },
+)
+
+deps = [
+ dependency('glib-2.0'),
+ dependency('gobject-2.0'),
+ dependency('json-glib-1.0'),
+]
+
+sources = [
+ config,
+ 'ifaces.vala',
+ 'player.vala',
+ 'mpris.vala',
+]
+
+if get_option('lib')
+ lib = library(
+ meson.project_name(),
+ sources,
+ dependencies: deps,
+ vala_header: meson.project_name() + '.h',
+ vala_vapi: meson.project_name() + '-' + api_version + '.vapi',
+ vala_gir: gir,
+ version: meson.project_version(),
+ install: true,
+ install_dir: [true, true, true, true],
+ )
+
+ import('pkgconfig').generate(
+ lib,
+ name: meson.project_name(),
+ filebase: meson.project_name() + '-' + api_version,
+ version: meson.project_version(),
+ subdirs: meson.project_name(),
+ requires: deps,
+ install_dir: get_option('libdir') / 'pkgconfig',
+ )
+
+ custom_target(
+ typelib,
+ command: [
+ find_program('g-ir-compiler'),
+ '--output', '@OUTPUT@',
+ '--shared-library', get_option('prefix') / get_option('libdir') / '@PLAINNAME@',
+ meson.current_build_dir() / gir,
+ ],
+ input: lib,
+ output: typelib,
+ depends: lib,
+ install: true,
+ install_dir: get_option('libdir') / 'girepository-1.0',
+ )
+endif
+
+if get_option('cli')
+ executable(
+ meson.project_name(),
+ ['cli.vala', sources],
+ dependencies: deps,
+ install: true,
+ )
+endif
diff --git a/lib/mpris/meson_options.txt b/lib/mpris/meson_options.txt
new file mode 100644
index 0000000..f110242
--- /dev/null
+++ b/lib/mpris/meson_options.txt
@@ -0,0 +1,11 @@
+option(
+ 'lib',
+ type: 'boolean',
+ value: true,
+)
+
+option(
+ 'cli',
+ type: 'boolean',
+ value: true,
+)
diff --git a/lib/mpris/mpris.vala b/lib/mpris/mpris.vala
new file mode 100644
index 0000000..0e55a2e
--- /dev/null
+++ b/lib/mpris/mpris.vala
@@ -0,0 +1,66 @@
+namespace AstalMpris {
+public Mpris get_default() {
+ return Mpris.get_default();
+}
+
+public class Mpris : Object {
+ internal static string PREFIX = "org.mpris.MediaPlayer2.";
+
+ private static Mpris instance;
+ public static Mpris get_default() {
+ if (instance == null)
+ instance = new Mpris();
+
+ return instance;
+ }
+
+ private DBusImpl proxy;
+
+ private HashTable<string, Player> _players =
+ new HashTable<string, Player> (str_hash, str_equal);
+
+ public List<weak Player> players { owned get { return _players.get_values(); } }
+
+ public signal void player_added (Player player);
+ public signal void player_closed (Player player);
+
+ construct {
+ try {
+ proxy = Bus.get_proxy_sync(
+ BusType.SESSION,
+ "org.freedesktop.DBus",
+ "/org/freedesktop/DBus"
+ );
+
+ foreach (var busname in proxy.list_names()) {
+ if (busname.has_prefix(Mpris.PREFIX))
+ add_player(busname);
+ }
+
+ proxy.name_owner_changed.connect((name, old_owner, new_owner) => {
+ if (!name.has_prefix(Mpris.PREFIX))
+ return;
+
+ if (new_owner != "" && old_owner == "")
+ add_player(name);
+ });
+ } catch (Error error) {
+ critical(error.message);
+ }
+ }
+
+ private void add_player(string busname) {
+ var p = new Player(busname);
+ _players.set(busname, p);
+
+ p.closed.connect(() => {
+ player_closed(p);
+ _players.remove(busname);
+ notify_property("players");
+ });
+
+ player_added(p);
+ notify_property("players");
+ }
+}
+}
diff --git a/lib/mpris/player.vala b/lib/mpris/player.vala
new file mode 100644
index 0000000..ed146f6
--- /dev/null
+++ b/lib/mpris/player.vala
@@ -0,0 +1,467 @@
+namespace AstalMpris {
+public class Player : Object {
+ private static string COVER_CACHE = Environment.get_user_cache_dir() + "/astal/mpris";
+
+ private IPlayer proxy;
+
+ public signal void appeared () { available = true; }
+ public signal void closed () { available = false; }
+
+ // identifiers
+ public string bus_name { owned get; construct set; }
+ public bool available { get; private set; }
+
+ // periodically notify position
+ private uint pollid;
+
+ // mpris
+ public void raise() {
+ try { proxy.raise(); } catch (Error error) { critical(error.message); }
+ }
+
+ public void quit() {
+ try { proxy.quit(); } catch (Error error) { critical(error.message); }
+ }
+
+ public bool can_quit { get; private set; }
+ public bool fullscreen { get; private set; }
+ public bool can_set_fullscreen { get; private set; }
+ public bool can_raise { get; private set; }
+ public bool has_track_list { get; private set; }
+ public string identity { owned get; private set; }
+ public string entry { owned get; private set; }
+ public string[] supported_uri_schemas { owned get; private set; }
+ public string[] supported_mime_types { owned get; private set; }
+
+ public void toggle_fullscreen() {
+ if (!can_set_fullscreen)
+ critical("can not set fullscreen on " + bus_name);
+
+ proxy.fullscreen = !fullscreen;
+ }
+
+ // player
+ public void next() {
+ try { proxy.next(); } catch (Error error) { critical(error.message); }
+ }
+
+ public void previous() {
+ try { proxy.previous(); } catch (Error error) { critical(error.message); }
+ }
+
+ public void pause() {
+ try { proxy.pause(); } catch (Error error) { critical(error.message); }
+ }
+
+ public void play_pause() {
+ try { proxy.play_pause(); } catch (Error error) { critical(error.message); }
+ }
+
+ public void stop() {
+ try { proxy.stop(); } catch (Error error) { critical(error.message); }
+ }
+
+ public void play() {
+ try { proxy.play(); } catch (Error error) { critical(error.message); }
+ }
+
+ public void open_uri(string uri) {
+ try { proxy.open_uri(uri); } catch (Error error) { critical(error.message); }
+ }
+
+ public void loop() {
+ switch (loop_status) {
+ case Loop.NONE:
+ loop_status = Loop.TRACK;
+ break;
+ case Loop.TRACK:
+ loop_status = Loop.PLAYLIST;
+ break;
+ case Loop.PLAYLIST:
+ loop_status = Loop.NONE;
+ break;
+ default:
+ break;
+ }
+ }
+
+ public void shuffle() {
+ shuffle_status = shuffle_status == Shuffle.ON
+ ? Shuffle.OFF
+ : Shuffle.ON;
+ }
+
+ public signal void seeked (int64 position);
+
+ public double _get_position() {
+ try {
+ var reply = proxy.call_sync(
+ "org.freedesktop.DBus.Properties.Get",
+ new Variant("(ss)",
+ "org.mpris.MediaPlayer2.Player",
+ "Position"
+ ),
+ DBusCallFlags.NONE,
+ -1,
+ null
+ );
+
+ var body = reply.get_child_value(0);
+ if (body.classify() == Variant.Class.STRING) {
+ return -1; // Position not supported
+ }
+
+ return (double)body.get_variant().get_int64() / 1000000;
+ } catch (Error err) {
+ return -1;
+ }
+ }
+
+ private void _set_position(double pos) {
+ try {
+ proxy.set_position((ObjectPath)trackid, (int64)(pos * 1000000));
+ } catch (Error error) {
+ critical(error.message);
+ }
+ }
+
+ private Loop _loop_status = Loop.UNSUPPORTED;
+ private double _rate;
+ private Shuffle _shuffle_status = Shuffle.UNSUPPORTED;
+ private double _volume = -1;
+
+ public Loop loop_status {
+ get { return _loop_status; }
+ set { proxy.loop_status = value.to_string(); }
+ }
+
+ public double rate {
+ get { return _rate; }
+ set { proxy.rate = value; }
+ }
+
+ public Shuffle shuffle_status {
+ get { return _shuffle_status; }
+ set { proxy.shuffle = value == Shuffle.ON; }
+ }
+
+ public double volume {
+ get { return _volume; }
+ set { proxy.volume = value; }
+ }
+
+ public double position {
+ get { return _get_position(); }
+ set { _set_position(value); }
+ }
+
+ public PlaybackStatus playback_status { get; private set; }
+ public double minimum_rate { get; private set; }
+ public double maximum_rate { get; private set; }
+ public bool can_go_next { get; private set; }
+ public bool can_go_previous { get; private set; }
+ public bool can_play { get; private set; }
+ public bool can_pause { get; private set; }
+ public bool can_seek { get; private set; }
+ public bool can_control { get; private set; }
+
+ // metadata
+ [CCode (notify = false)]
+ public HashTable<string,Variant> metadata { owned get; private set; }
+
+ public string trackid { owned get; private set; }
+ public double length { get; private set; }
+ public string art_url { owned get; private set; }
+
+ public string album { owned get; private set; }
+ public string album_artist { owned get; private set; }
+ public string artist { owned get; private set; }
+ public string lyrics { owned get; private set; }
+ public string title { owned get; private set; }
+ public string composer { owned get; private set; }
+ public string comments { owned get; private set; }
+
+ // cached cover art
+ public string cover_art { owned get; private set; }
+
+ public Player(string name) {
+ Object(bus_name: name.has_prefix("org.mpris.MediaPlayer2.")
+ ? name : "org.mpris.MediaPlayer2." + name);
+ }
+
+ private void sync() {
+ // mpris
+ can_quit = proxy.can_quit;
+ fullscreen = proxy.fullscreen;
+ can_set_fullscreen = proxy.can_set_fullscreen;
+ can_raise = proxy.can_raise;
+ has_track_list = proxy.has_track_list;
+ identity = proxy.identity;
+ entry = proxy.desktop_entry;
+ supported_uri_schemas = proxy.supported_uri_schemas;
+ supported_mime_types = proxy.supported_mime_types;
+
+ if (position >= 0)
+ notify_property("position");
+
+ // LoopStatus and Shuffle are optional props
+ var props = proxy.get_all("org.mpris.MediaPlayer2.Player");
+
+ // player
+ if (props != null && props.get("LoopStatus") != null) {
+ if (loop_status != Loop.from_string(proxy.loop_status)) {
+ _loop_status = Loop.from_string(proxy.loop_status);
+ notify_property("loop-status");
+ }
+ }
+
+ if (rate != proxy.rate) {
+ _rate = proxy.rate;
+ notify_property("rate");
+ }
+
+ if (props != null && props.get("Shuffle") != null) {
+ if (shuffle_status != Shuffle.from_bool(proxy.shuffle)) {
+ _shuffle_status = Shuffle.from_bool(proxy.shuffle);
+ notify_property("shuffle-status");
+ }
+ }
+
+ if (volume != proxy.volume) {
+ _volume = proxy.volume;
+ notify_property("volume");
+ }
+
+ playback_status = PlaybackStatus.from_string(proxy.playback_status);
+ minimum_rate = proxy.minimum_rate;
+ maximum_rate = proxy.maximum_rate;
+ can_go_next = proxy.can_go_next;
+ can_go_previous = proxy.can_go_previous;
+ can_play = proxy.can_play;
+ can_pause = proxy.can_pause;
+ can_seek = proxy.can_seek;
+ can_control = proxy.can_control;
+
+ // metadata
+ metadata = proxy.metadata;
+ if (metadata != null) {
+ if (metadata.get("mpris:length") != null)
+ length = (double)metadata.get("mpris:length").get_uint64() / 1000000;
+ else
+ length = -1;
+
+ trackid = get_str("mpris:trackid");
+ art_url = get_str("mpris:artUrl");
+ album = get_str("xesam:album");
+ lyrics = get_str("xesam:asText");
+ title = get_str("xesam:title");
+ album_artist = join_strv("xesam:albumArtist", ", ");
+ artist = join_strv("xesam:artist", ", ");
+ comments = join_strv("xesam:comments", "\n");
+ composer = join_strv("xesam:composer", ", ");
+ cache_cover.begin((_, res) => cache_cover.end(res));
+ notify_property("metadata");
+ }
+ }
+
+ private async void cache_cover() {
+ if (art_url == null || art_url == "")
+ return;
+
+ var file = File.new_for_uri(art_url);
+ if (file.get_path() != null) {
+ cover_art = file.get_path();
+ return;
+ }
+
+ var path = COVER_CACHE + "/" + Checksum.compute_for_string(ChecksumType.SHA1, art_url, -1);
+ if (FileUtils.test(path, FileTest.EXISTS)) {
+ cover_art = path;
+ return;
+ }
+
+ try {
+ if (!FileUtils.test(COVER_CACHE, FileTest.IS_DIR))
+ File.new_for_path(COVER_CACHE).make_directory_with_parents(null);
+
+ file.copy_async.begin(
+ File.new_for_path(path),
+ FileCopyFlags.OVERWRITE,
+ Priority.DEFAULT,
+ null,
+ null,
+ (_, res) => {
+ try {
+ file.copy_async.end(res);
+ cover_art = path;
+ } catch (Error err) {
+ critical("Failed to cache cover art with url \"%s\": %s", art_url, err.message);
+ }
+ }
+ );
+ } catch (Error err) {
+ critical(err.message);
+ }
+ }
+
+ public Variant? get_meta(string key) {
+ return metadata.lookup(key);
+ }
+
+ private string get_str(string key) {
+ if (metadata.get(key) == null)
+ return "";
+
+ var str = metadata.get(key).get_string(null);
+ return str == null ? "" : str;
+ }
+
+ private string? join_strv(string key, string sep) {
+ if (metadata.get(key) == null)
+ return null;
+
+ var arr = metadata.get(key).get_strv();
+ if (arr.length == 0)
+ return null;
+
+ var builder = new StringBuilder();
+ for (var i = 0; i < arr.length; ++i) {
+ builder.append(arr[i]);
+ if (i + 1 < arr.length)
+ builder.append(sep);
+ }
+
+ return builder.str;
+ }
+
+ construct {
+ try {
+ try_proxy();
+ sync();
+ } catch (Error error) {
+ critical(error.message);
+ }
+ }
+
+ public void try_proxy() throws Error {
+ if (proxy != null)
+ return;
+
+ proxy = Bus.get_proxy_sync(
+ BusType.SESSION,
+ bus_name,
+ "/org/mpris/MediaPlayer2"
+ );
+
+ if (proxy.g_name_owner != null)
+ appeared();
+
+ proxy.notify["g-name-owner"].connect(() => {
+ if (proxy.g_name_owner != null)
+ appeared();
+ else
+ closed();
+ });
+
+ proxy.g_properties_changed.connect(sync);
+
+ pollid = Timeout.add_seconds(1, () => {
+ if (!available)
+ return Source.CONTINUE;
+
+ if (position >= 0) {
+ notify_property("position");
+ }
+ return Source.CONTINUE;
+ }, Priority.DEFAULT);
+ }
+
+ ~Player() {
+ Source.remove(pollid);
+ }
+}
+
+public enum PlaybackStatus {
+ PLAYING,
+ PAUSED,
+ STOPPED;
+
+ public static PlaybackStatus from_string(string? str) {
+ switch (str) {
+ case "Playing":
+ return PLAYING;
+ case "Paused":
+ return PAUSED;
+ case "Stopped":
+ default:
+ return STOPPED;
+ }
+ }
+
+ public string to_string() {
+ switch (this) {
+ case PLAYING:
+ return "Playing";
+ case PAUSED:
+ return "Paused";
+ case STOPPED:
+ default:
+ return "Stopped";
+ }
+ }
+}
+
+public enum Loop {
+ UNSUPPORTED,
+ NONE,
+ TRACK,
+ PLAYLIST;
+
+ public static Loop from_string(string? str) {
+ switch (str) {
+ case "None":
+ return NONE;
+ case "Track":
+ return TRACK;
+ case "Playlist":
+ return PLAYLIST;
+ default:
+ return UNSUPPORTED;
+ }
+ }
+
+ public string? to_string() {
+ switch (this) {
+ case NONE:
+ return "None";
+ case TRACK:
+ return "Track";
+ case PLAYLIST:
+ return "Playlist";
+ default:
+ return "Unsupported";
+ }
+ }
+}
+
+public enum Shuffle {
+ UNSUPPORTED,
+ ON,
+ OFF;
+
+ public static Shuffle from_bool(bool b) {
+ return b ? Shuffle.ON : Shuffle.OFF;
+ }
+
+ public string? to_string() {
+ switch (this) {
+ case OFF:
+ return "Off";
+ case ON:
+ return "On";
+ default:
+ return "Unsupported";
+ }
+ }
+}
+}
diff --git a/lib/mpris/version b/lib/mpris/version
new file mode 100644
index 0000000..6e8bf73
--- /dev/null
+++ b/lib/mpris/version
@@ -0,0 +1 @@
+0.1.0