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 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 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 { @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> changedProperties = signal.getPropertiesChanged(); if (!changedProperties.containsKey("Metadata") && !changedProperties.containsKey("PlaybackStatus")) { return; } handleSongStateChange(signal.getSource()); } } }