From 8b561bccd4758dfc14ac06ad76024f90644de806 Mon Sep 17 00:00:00 2001 From: Corey Woodworth Date: Tue, 7 Oct 2025 13:01:56 -0400 Subject: [PATCH] feat: Merge duplicate MPRIS players when browser dual report as their base suchas Librewolf as Firefox --- Services/MediaService.qml | 178 +++++++++++++++++++++++++------------- 1 file changed, 117 insertions(+), 61 deletions(-) diff --git a/Services/MediaService.qml b/Services/MediaService.qml index 4a6e0b68..97b59e53 100644 --- a/Services/MediaService.qml +++ b/Services/MediaService.qml @@ -32,67 +32,127 @@ Singleton { function getAvailablePlayers() { if (!Mpris.players || !Mpris.players.values) { - return [] + return []; } - let allPlayers = Mpris.players.values - let controllablePlayers = [] + let allPlayers = Mpris.players.values; + let finalPlayers = []; + const genericBrowsers = ["firefox", "chromium", "chrome"]; - // Apply blacklist and controllable filter - const blacklist = (Settings.data.audio && Settings.data.audio.mprisBlacklist) ? Settings.data.audio.mprisBlacklist : [] + // 1. Separate players into specific and generic lists + let specificPlayers = []; + let genericPlayers = []; for (var i = 0; i < allPlayers.length; i++) { - let player = allPlayers[i] - if (!player) - continue - const identity = String(player.identity || "") - const busName = String(player.busName || "") - const desktop = String(player.desktopEntry || "") - const idKey = identity.toLowerCase() - const match = blacklist.find(b => { - const s = String(b || "").toLowerCase() - return s && (idKey.includes(s) || busName.toLowerCase().includes(s) || desktop.toLowerCase().includes(s)) - }) - if (match) - continue - if (player.canControl) - controllablePlayers.push(player) + const identity = String(allPlayers[i].identity || "").toLowerCase(); + if (genericBrowsers.some(b => identity.includes(b))) { + genericPlayers.push(allPlayers[i]); + } else { + specificPlayers.push(allPlayers[i]); + } } - return controllablePlayers + let matchedGenericIndices = {}; + + // 2. For each specific player, try to find and pair it with a generic partner + for (var i = 0; i < specificPlayers.length; i++) { + let specificPlayer = specificPlayers[i]; + let title1 = String(specificPlayer.trackTitle || "").trim(); + let wasMatched = false; + + if (title1) { + for (var j = 0; j < genericPlayers.length; j++) { + if (matchedGenericIndices[j]) continue; + let genericPlayer = genericPlayers[j]; + let title2 = String(genericPlayer.trackTitle || "").trim(); + + if (title2 && (title1.includes(title2) || title2.includes(title1))) { + let dataPlayer = genericPlayer; + let identityPlayer = specificPlayer; + + let scoreSpecific = (specificPlayer.trackArtUrl ? 1 : 0); + let scoreGeneric = (genericPlayer.trackArtUrl ? 1 : 0); + if(scoreSpecific > scoreGeneric){ dataPlayer = specificPlayer; } + + let virtualPlayer = { + identity: identityPlayer.identity, + desktopEntry: identityPlayer.desktopEntry, + trackTitle: dataPlayer.trackTitle, + trackArtist: dataPlayer.trackArtist, + trackAlbum: dataPlayer.trackAlbum, + trackArtUrl: dataPlayer.trackArtUrl, + length: dataPlayer.length || 0, + position: dataPlayer.position || 0, + playbackState: dataPlayer.playbackState, + isPlaying: dataPlayer.isPlaying || false, + canPlay: dataPlayer.canPlay || false, + canPause: dataPlayer.canPause || false, + canGoNext: dataPlayer.canGoNext || false, + canGoPrevious: dataPlayer.canGoPrevious || false, + canSeek: dataPlayer.canSeek || false, + canControl: dataPlayer.canControl || false, + _stateSource: dataPlayer, + _controlTarget: identityPlayer + }; + finalPlayers.push(virtualPlayer); + matchedGenericIndices[j] = true; + wasMatched = true; + break; + } + } + } + if (!wasMatched) { + finalPlayers.push(specificPlayer); + } + } + + // 3. Add any generic players that were not matched + for (var i = 0; i < genericPlayers.length; i++) { + if (!matchedGenericIndices[i]) { + finalPlayers.push(genericPlayers[i]); + } + } + + // 4. Filter for controllable players + let controllablePlayers = []; + for (var i = 0; i < finalPlayers.length; i++) { + let player = finalPlayers[i]; + if (player && player.canControl) { + controllablePlayers.push(player); + } + } + return controllablePlayers; } function findActivePlayer() { let availablePlayers = getAvailablePlayers() if (availablePlayers.length === 0) { + Logger.log("Media", "No active player found") return null } - // First, check if any player is currently playing + // --- NEW: Prioritize the actively playing player --- for (var i = 0; i < availablePlayers.length; i++) { - const p = availablePlayers[i] - if (p.isPlaying && p.playbackState === MprisPlaybackState.Playing) { - selectedPlayerIndex = i - return p + if (availablePlayers[i] && availablePlayers[i].playbackState === MprisPlaybackState.Playing) { + Logger.log("Media", "Found actively playing player: " + availablePlayers[i].identity); + selectedPlayerIndex = i; + return availablePlayers[i]; } } - // If no player is playing, use preferred player logic + // --- OLD LOGIC (used as a fallback if nothing is playing) --- const preferred = (Settings.data.audio.preferredPlayer || "") if (preferred !== "") { for (var i = 0; i < availablePlayers.length; i++) { const p = availablePlayers[i] const identity = String(p.identity || "").toLowerCase() - const busName = String(p.busName || "").toLowerCase() - const desktop = String(p.desktopEntry || "").toLowerCase() const pref = preferred.toLowerCase() - if (identity.includes(pref) || busName.includes(pref) || desktop.includes(pref)) { + if (identity.includes(pref)) { selectedPlayerIndex = i return p } } } - // Fallback to selected index or first player if (selectedPlayerIndex < availablePlayers.length) { return availablePlayers[selectedPlayerIndex] } else { @@ -107,46 +167,54 @@ Singleton { if (newPlayer !== currentPlayer) { currentPlayer = newPlayer currentPosition = currentPlayer ? currentPlayer.position : 0 + Logger.log("Media", "Switching player") } } function playPause() { if (currentPlayer) { - if (currentPlayer.isPlaying) { - currentPlayer.pause() + let stateSource = currentPlayer._stateSource || currentPlayer + let controlTarget = currentPlayer._controlTarget || currentPlayer + if (stateSource.playbackState === MprisPlaybackState.Playing) { + controlTarget.pause() } else { - currentPlayer.play() + controlTarget.play() } } } function play() { - if (currentPlayer && currentPlayer.canPlay) { - currentPlayer.play() + let target = currentPlayer ? (currentPlayer._controlTarget || currentPlayer) : null + if (target && target.canPlay) { + target.play() } } function pause() { - if (currentPlayer && currentPlayer.canPause) { - currentPlayer.pause() + let target = currentPlayer ? (currentPlayer._controlTarget || currentPlayer) : null + if (target && target.canPause) { + target.pause() } } function next() { - if (currentPlayer && currentPlayer.canGoNext) { - currentPlayer.next() + let target = currentPlayer ? (currentPlayer._controlTarget || currentPlayer) : null + if (target && target.canGoNext) { + target.next() } } function previous() { - if (currentPlayer && currentPlayer.canGoPrevious) { - currentPlayer.previous() + let target = currentPlayer ? (currentPlayer._controlTarget || currentPlayer) : null + if (target && target.canGoPrevious) { + target.previous() } } function seek(position) { - if (currentPlayer && currentPlayer.canSeek) { - currentPlayer.position = position + let target = currentPlayer ? (currentPlayer._controlTarget || currentPlayer) : null + if (target && target.canSeek) { + target.position = position currentPosition = position } } @@ -161,9 +229,10 @@ Singleton { // Seek to position based on ratio (0.0 to 1.0) function seekByRatio(ratio) { - if (currentPlayer && currentPlayer.canSeek && currentPlayer.length > 0) { - let seekPosition = ratio * currentPlayer.length - currentPlayer.position = seekPosition + let target = currentPlayer ? (currentPlayer._controlTarget || currentPlayer) : null + if (target && target.canSeek && target.length > 0) { + let seekPosition = ratio * target.length + target.position = seekPosition currentPosition = seekPosition } } @@ -209,21 +278,8 @@ Singleton { Connections { target: Mpris.players function onValuesChanged() { + Logger.log("Media", "Players changed") updateCurrentPlayer() } } - - // Monitor playback state changes across all players to switch to playing ones - Timer { - id: playerStateMonitor - interval: 2000 // Check every 2 seconds - repeat: true - running: true - onTriggered: { - // Only update if we don't have a playing player or if current player is paused - if (!currentPlayer || !currentPlayer.isPlaying || currentPlayer.playbackState !== MprisPlaybackState.Playing) { - updateCurrentPlayer() - } - } - } }