242 lines
8.5 KiB
Java
242 lines
8.5 KiB
Java
package download.darkworld;
|
|
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
import java.util.concurrent.ConcurrentHashMap;
|
|
import com.mojang.logging.LogUtils;
|
|
import org.freedesktop.dbus.connections.impl.DBusConnection;
|
|
import org.freedesktop.dbus.connections.impl.DBusConnectionBuilder;
|
|
import org.freedesktop.dbus.exceptions.DBusException;
|
|
import org.freedesktop.dbus.interfaces.DBus;
|
|
import org.freedesktop.dbus.interfaces.DBusSigHandler;
|
|
import org.freedesktop.dbus.interfaces.Properties;
|
|
import org.freedesktop.dbus.matchrules.DBusMatchRule;
|
|
import org.freedesktop.dbus.matchrules.DBusMatchRuleBuilder;
|
|
import org.freedesktop.dbus.types.Variant;
|
|
import net.minecraft.client.Minecraft;
|
|
import net.minecraft.client.gui.components.toasts.ToastManager;
|
|
import org.jetbrains.annotations.Nullable;
|
|
import org.slf4j.Logger;
|
|
|
|
public final class MinecraftMprisClient {
|
|
private static final Logger LOGGER = LogUtils.getLogger();
|
|
private static final long CACHE_TTL_MS = 1000L;
|
|
private static final String DBUS_SERVICE = "org.freedesktop.DBus";
|
|
private static final String DBUS_PATH = "/org/freedesktop/DBus";
|
|
private static final String MPRIS_PATH = "/org/mpris/MediaPlayer2";
|
|
private static final String MPRIS_PREFIX = "org.mpris.MediaPlayer2.";
|
|
private static final String PLAYER_INTERFACE = "org.mpris.MediaPlayer2.Player";
|
|
|
|
@Nullable
|
|
private static DBusConnection connection;
|
|
@Nullable
|
|
private static AutoCloseable propertiesChangedHandler;
|
|
private static final Map<String, String> lastAnnouncedTrackIds = new ConcurrentHashMap<>();
|
|
|
|
private MinecraftMprisClient() {
|
|
}
|
|
|
|
public static void initialize() {
|
|
synchronized (MinecraftMprisClient.class) {
|
|
try {
|
|
LOGGER.info("[mpris] initializing DBus MPRIS bridge");
|
|
DBusConnection dbusConnection = getConnection();
|
|
if (dbusConnection == null) {
|
|
LOGGER.warn("[mpris] DBus connection was null during initialization");
|
|
return;
|
|
}
|
|
|
|
if (propertiesChangedHandler == null) {
|
|
LOGGER.info("[mpris] registering global PropertiesChanged handler");
|
|
DBusMatchRule matchRule = DBusMatchRuleBuilder.create()
|
|
.withType(Properties.PropertiesChanged.class)
|
|
.withPath(MPRIS_PATH)
|
|
.withArg0123(0, PLAYER_INTERFACE)
|
|
.build();
|
|
propertiesChangedHandler = dbusConnection.addSigHandler(matchRule, new PlayerPropertiesChangedHandler());
|
|
}
|
|
LOGGER.info("[mpris] initialization complete");
|
|
} catch (DBusException | RuntimeException exception) {
|
|
LOGGER.error("[mpris] initialization failed", exception);
|
|
resetConnectionState();
|
|
}
|
|
}
|
|
}
|
|
|
|
@Nullable
|
|
private static SongInfo refreshCurrentSong(String serviceName) {
|
|
try {
|
|
DBusConnection dbusConnection = getConnection();
|
|
if (dbusConnection == null) {
|
|
LOGGER.warn("[mpris] refreshCurrentSong({}): connection was null", serviceName);
|
|
return null;
|
|
}
|
|
LOGGER.info("[mpris] checking player {}", serviceName);
|
|
Properties properties = dbusConnection.getRemoteObject(serviceName, MPRIS_PATH, Properties.class);
|
|
String playbackStatus = properties.Get(PLAYER_INTERFACE, "PlaybackStatus");
|
|
LOGGER.info("[mpris] player {} playbackStatus={}", serviceName, playbackStatus);
|
|
if (!"Playing".equals(playbackStatus)) {
|
|
return null;
|
|
}
|
|
|
|
Map<String, ?> metadata = properties.Get(PLAYER_INTERFACE, "Metadata");
|
|
if (metadata == null) {
|
|
LOGGER.info("[mpris] player {} returned null metadata", serviceName);
|
|
return null;
|
|
}
|
|
|
|
String title = stringValue(metadata.get("xesam:title"));
|
|
String artist = artistsValue(metadata.get("xesam:artist"));
|
|
String trackId = stringValue(metadata.get("mpris:trackid"));
|
|
String displayText = formatDisplayText(artist, title);
|
|
LOGGER.info("[mpris] player {} metadata artist='{}' title='{}' trackId='{}'", serviceName, artist, title, trackId);
|
|
if (displayText != null && trackId != null && !trackId.isBlank()) {
|
|
return new SongInfo(serviceName, trackId, displayText);
|
|
}
|
|
} catch (DBusException | RuntimeException exception) {
|
|
LOGGER.error("[mpris] refreshCurrentSong failed for {}", serviceName, exception);
|
|
return null;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
@Nullable
|
|
private static DBusConnection getConnection() throws DBusException {
|
|
if (connection == null) {
|
|
LOGGER.info("[mpris] opening session bus connection");
|
|
connection = DBusConnectionBuilder.forSessionBus()
|
|
.withShared(true)
|
|
.build();
|
|
}
|
|
|
|
return connection;
|
|
}
|
|
|
|
private static synchronized void handleSongStateChange(String serviceName) {
|
|
LOGGER.info("[mpris] handleSongStateChange triggered for {}", serviceName);
|
|
SongInfo song = refreshCurrentSong(serviceName);
|
|
String currentTrackId = song != null ? song.trackId() : null;
|
|
String lastAnnouncedTrackId = lastAnnouncedTrackIds.get(serviceName);
|
|
LOGGER.info("[mpris] player={} currentTrackId={}, lastAnnouncedTrackId={}", serviceName, currentTrackId, lastAnnouncedTrackId);
|
|
if (currentTrackId == null || currentTrackId.equals(lastAnnouncedTrackId)) {
|
|
LOGGER.info("[mpris] not showing toast because track id is null or unchanged");
|
|
return;
|
|
}
|
|
|
|
lastAnnouncedTrackIds.put(serviceName, currentTrackId);
|
|
Minecraft minecraft = Minecraft.getInstance();
|
|
String title = song.displayText();
|
|
LOGGER.info("[mpris] scheduling toast for player={} trackId={} title='{}'", serviceName, currentTrackId, title);
|
|
minecraft.execute(() -> showToast(minecraft, serviceName, currentTrackId, title));
|
|
}
|
|
|
|
private static void showToast(Minecraft minecraft, String playerId, String trackId, String title) {
|
|
LOGGER.info("[mpris] showToast executing on client thread for player={} trackId={} title='{}'", playerId, trackId, title);
|
|
ToastManager toastManager = minecraft.getToastManager();
|
|
MprisNowPlayingToast toast = toastManager.getToast(MprisNowPlayingToast.class, playerId);
|
|
if (toast == null) {
|
|
LOGGER.info("[mpris] creating new toast instance");
|
|
toast = new MprisNowPlayingToast(playerId, trackId, title);
|
|
toast.showToast(trackId, title, minecraft.options);
|
|
toastManager.addToast(toast);
|
|
return;
|
|
}
|
|
|
|
LOGGER.info("[mpris] reusing existing toast instance");
|
|
toast.showToast(trackId, title, minecraft.options);
|
|
}
|
|
|
|
private static synchronized void resetConnectionState() {
|
|
LOGGER.warn("[mpris] resetting DBus connection state");
|
|
lastAnnouncedTrackIds.clear();
|
|
if (propertiesChangedHandler != null) {
|
|
try {
|
|
propertiesChangedHandler.close();
|
|
} catch (Exception ignored) {
|
|
}
|
|
propertiesChangedHandler = null;
|
|
}
|
|
|
|
if (connection != null) {
|
|
connection.disconnect();
|
|
connection = null;
|
|
}
|
|
}
|
|
|
|
@Nullable
|
|
private static String stringValue(@Nullable Object value) {
|
|
if (value == null) {
|
|
return null;
|
|
}
|
|
|
|
if (value instanceof Variant<?> variant) {
|
|
return stringValue(variant.getValue());
|
|
}
|
|
|
|
return value.toString();
|
|
}
|
|
|
|
@Nullable
|
|
private static String artistsValue(@Nullable Object value) {
|
|
if (value == null) {
|
|
return null;
|
|
}
|
|
|
|
if (value instanceof Variant<?> variant) {
|
|
return artistsValue(variant.getValue());
|
|
}
|
|
if (value instanceof String string) {
|
|
return string;
|
|
}
|
|
if (value instanceof String[] strings) {
|
|
return String.join(", ", strings);
|
|
}
|
|
if (value instanceof List<?> list) {
|
|
return list.stream()
|
|
.map(MinecraftMprisClient::stringValue)
|
|
.filter(part -> part != null && !part.isBlank())
|
|
.reduce((left, right) -> left + ", " + right)
|
|
.orElse(null);
|
|
}
|
|
|
|
return value.toString();
|
|
}
|
|
|
|
@Nullable
|
|
private static String formatDisplayText(@Nullable String artist, @Nullable String title) {
|
|
String cleanArtist = artist != null ? artist.trim() : null;
|
|
String cleanTitle = title != null ? title.trim() : null;
|
|
if (cleanArtist != null && !cleanArtist.isBlank() && cleanTitle != null && !cleanTitle.isBlank()) {
|
|
return cleanArtist + " - " + cleanTitle;
|
|
}
|
|
if (cleanTitle != null && !cleanTitle.isBlank()) {
|
|
return cleanTitle;
|
|
}
|
|
if (cleanArtist != null && !cleanArtist.isBlank()) {
|
|
return cleanArtist;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
private record SongInfo(String playerId, String trackId, String displayText) {
|
|
}
|
|
|
|
private static final class PlayerPropertiesChangedHandler implements DBusSigHandler<Properties.PropertiesChanged> {
|
|
@Override
|
|
public void handle(Properties.PropertiesChanged signal) {
|
|
LOGGER.info("[mpris] PropertiesChanged interface={} changedKeys={} removedKeys={}", signal.getInterfaceName(), signal.getPropertiesChanged().keySet(), signal.getPropertiesRemoved());
|
|
if (!PLAYER_INTERFACE.equals(signal.getInterfaceName())) {
|
|
return;
|
|
}
|
|
|
|
Map<String, Variant<?>> changedProperties = signal.getPropertiesChanged();
|
|
if (!changedProperties.containsKey("Metadata") && !changedProperties.containsKey("PlaybackStatus")) {
|
|
return;
|
|
}
|
|
|
|
handleSongStateChange(signal.getSource());
|
|
}
|
|
}
|
|
}
|