mirror of
https://github.com/zoriya/astal.git
synced 2026-05-31 17:33:15 +00:00
monorepo structuring
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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@";
|
||||
}
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -0,0 +1,11 @@
|
||||
option(
|
||||
'lib',
|
||||
type: 'boolean',
|
||||
value: true,
|
||||
)
|
||||
|
||||
option(
|
||||
'cli',
|
||||
type: 'boolean',
|
||||
value: true,
|
||||
)
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
0.1.0
|
||||
Reference in New Issue
Block a user