Files
astal/lib/mpris/player.vala
2024-09-01 14:17:36 +02:00

468 lines
13 KiB
Vala

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";
}
}
}
}