Compare commits

..
+31 -15
View File
@@ -240,6 +240,22 @@ class MusicAssistantProvider with ChangeNotifier {
}; };
} }
/// Check if a server player ID matches our local builtin player ID.
/// MA beta 14+ wraps Sendspin player IDs with 'up' prefix and lowercases
/// them when merging into universal_player entities, so we need flexible matching.
/// e.g. local "ensemble_Chris_CP21.260" matches server "upensemble_chris_cp21.260"
bool _isBuiltinPlayer(String serverPlayerId, String builtinPlayerId) {
if (serverPlayerId == builtinPlayerId) return true;
return serverPlayerId.toLowerCase() == 'up${builtinPlayerId.toLowerCase()}' ||
serverPlayerId.toLowerCase() == builtinPlayerId.toLowerCase();
}
/// Check if a server player ID is an Ensemble player (this or other device)
bool _isEnsemblePlayer(String serverPlayerId) {
final lower = serverPlayerId.toLowerCase();
return lower.startsWith('ensemble_') || lower.startsWith('upensemble_');
}
/// Player filter from MA user settings (empty = all players allowed) /// Player filter from MA user settings (empty = all players allowed)
List<String> get playerFilter => _playerFilter; List<String> get playerFilter => _playerFilter;
@@ -4171,8 +4187,8 @@ class MusicAssistantProvider with ChangeNotifier {
// Smart sort: local player first, then playing, then on, then off // Smart sort: local player first, then playing, then on, then off
players.sort((a, b) { players.sort((a, b) {
// Local player always first // Local player always first
final aIsLocal = builtinPlayerId != null && a.playerId == builtinPlayerId; final aIsLocal = builtinPlayerId != null && _isBuiltinPlayer(a.playerId, builtinPlayerId);
final bIsLocal = builtinPlayerId != null && b.playerId == builtinPlayerId; final bIsLocal = builtinPlayerId != null && _isBuiltinPlayer(b.playerId, builtinPlayerId);
if (aIsLocal && !bIsLocal) return -1; if (aIsLocal && !bIsLocal) return -1;
if (bIsLocal && !aIsLocal) return 1; if (bIsLocal && !aIsLocal) return 1;
@@ -4249,8 +4265,8 @@ class MusicAssistantProvider with ChangeNotifier {
return false; return false;
} }
if (player.playerId.startsWith('ensemble_')) { if (_isEnsemblePlayer(player.playerId)) {
if (builtinPlayerId == null || player.playerId != builtinPlayerId) { if (builtinPlayerId == null || !_isBuiltinPlayer(player.playerId, builtinPlayerId)) {
_logger.log('🚫 Filtering out other device\'s player: ${player.name}'); _logger.log('🚫 Filtering out other device\'s player: ${player.name}');
filteredCount++; filteredCount++;
return false; return false;
@@ -4258,7 +4274,7 @@ class MusicAssistantProvider with ChangeNotifier {
} }
if (!player.available) { if (!player.available) {
if (builtinPlayerId != null && player.playerId == builtinPlayerId) { if (builtinPlayerId != null && _isBuiltinPlayer(player.playerId, builtinPlayerId)) {
return true; return true;
} }
filteredCount++; filteredCount++;
@@ -4482,7 +4498,7 @@ class MusicAssistantProvider with ChangeNotifier {
// Try available first, then accept unavailable (Sendspin may still be registering) // Try available first, then accept unavailable (Sendspin may still be registering)
if (playerToSelect == null && builtinPlayerId != null) { if (playerToSelect == null && builtinPlayerId != null) {
playerToSelect = _availablePlayers.cast<Player?>().firstWhere( playerToSelect = _availablePlayers.cast<Player?>().firstWhere(
(p) => p!.playerId == builtinPlayerId && p.available, (p) => _isBuiltinPlayer(p!.playerId, builtinPlayerId) && p.available,
orElse: () => null, orElse: () => null,
); );
if (playerToSelect != null) { if (playerToSelect != null) {
@@ -4490,7 +4506,7 @@ class MusicAssistantProvider with ChangeNotifier {
} else { } else {
// Player exists but not yet available (Sendspin registration in progress) // Player exists but not yet available (Sendspin registration in progress)
playerToSelect = _availablePlayers.cast<Player?>().firstWhere( playerToSelect = _availablePlayers.cast<Player?>().firstWhere(
(p) => p!.playerId == builtinPlayerId, (p) => _isBuiltinPlayer(p!.playerId, builtinPlayerId),
orElse: () => null, orElse: () => null,
); );
if (playerToSelect != null) { if (playerToSelect != null) {
@@ -4568,7 +4584,7 @@ class MusicAssistantProvider with ChangeNotifier {
// Switch audio handler mode based on player type // Switch audio handler mode based on player type
final builtinPlayerId = await SettingsService.getBuiltinPlayerId(); final builtinPlayerId = await SettingsService.getBuiltinPlayerId();
final isBuiltinPlayer = builtinPlayerId != null && player.playerId == builtinPlayerId; final isBuiltinPlayer = builtinPlayerId != null && _isBuiltinPlayer(player.playerId, builtinPlayerId);
if (isBuiltinPlayer) { if (isBuiltinPlayer) {
audioHandler.setLocalMode(); audioHandler.setLocalMode();
// Update notification for builtin player using local mode method (keeps pause working) // Update notification for builtin player using local mode method (keeps pause working)
@@ -5355,7 +5371,7 @@ class MusicAssistantProvider with ChangeNotifier {
final track = _currentTrack!; final track = _currentTrack!;
final artworkUrl = _api?.getImageUrl(track, size: 512); final artworkUrl = _api?.getImageUrl(track, size: 512);
final builtinPlayerId = await SettingsService.getBuiltinPlayerId(); final builtinPlayerId = await SettingsService.getBuiltinPlayerId();
final isBuiltinPlayer = builtinPlayerId != null && _selectedPlayer!.playerId == builtinPlayerId; final isBuiltinPlayer = builtinPlayerId != null && _isBuiltinPlayer(_selectedPlayer!.playerId, builtinPlayerId);
if (isBuiltinPlayer) { if (isBuiltinPlayer) {
// Local playback - use local mode notification (keeps pause working) // Local playback - use local mode notification (keeps pause working)
@@ -5401,7 +5417,7 @@ class MusicAssistantProvider with ChangeNotifier {
} else { } else {
// No queue data available from API // No queue data available from API
final builtinPlayerId = await SettingsService.getBuiltinPlayerId(); final builtinPlayerId = await SettingsService.getBuiltinPlayerId();
final isBuiltinPlayer = builtinPlayerId != null && _selectedPlayer!.playerId == builtinPlayerId; final isBuiltinPlayer = builtinPlayerId != null && _isBuiltinPlayer(_selectedPlayer!.playerId, builtinPlayerId);
// For builtin player, try to preserve cached track data since queue API doesn't return it // For builtin player, try to preserve cached track data since queue API doesn't return it
// The cached track was set in selectPlayer() and contains valid data // The cached track was set in selectPlayer() and contains valid data
@@ -5498,7 +5514,7 @@ class MusicAssistantProvider with ChangeNotifier {
// Sendspin PCM streaming has ~5s buffering delay on resume. // Sendspin PCM streaming has ~5s buffering delay on resume.
// For audiobooks, seek back to compensate so the user doesn't miss narration. // For audiobooks, seek back to compensate so the user doesn't miss narration.
final builtinPlayerId = await SettingsService.getBuiltinPlayerId(); final builtinPlayerId = await SettingsService.getBuiltinPlayerId();
if (builtinPlayerId != null && playerId == builtinPlayerId && _sendspinConnected && isPlayingAudiobook) { if (builtinPlayerId != null && _isBuiltinPlayer(playerId, builtinPlayerId) && _sendspinConnected && isPlayingAudiobook) {
final pos = _positionTracker.currentPosition.inSeconds; final pos = _positionTracker.currentPosition.inSeconds;
if (pos > 10) { if (pos > 10) {
_logger.log('📚 Seeking back 10s to compensate for Sendspin buffering'); _logger.log('📚 Seeking back 10s to compensate for Sendspin buffering');
@@ -6079,7 +6095,7 @@ class MusicAssistantProvider with ChangeNotifier {
// Get builtin player ID - this is cached so should be fast // Get builtin player ID - this is cached so should be fast
final builtinPlayerId = await SettingsService.getBuiltinPlayerId(); final builtinPlayerId = await SettingsService.getBuiltinPlayerId();
if (builtinPlayerId != null && playerId == builtinPlayerId && _sendspinConnected) { if (builtinPlayerId != null && _isBuiltinPlayer(playerId, builtinPlayerId) && _sendspinConnected) {
_logger.log('⏸️ Non-blocking local pause for builtin player'); _logger.log('⏸️ Non-blocking local pause for builtin player');
// CRITICAL: Don't await these - they can block the UI thread // CRITICAL: Don't await these - they can block the UI thread
@@ -6181,7 +6197,7 @@ class MusicAssistantProvider with ChangeNotifier {
try { try {
// Optimistic local stop for builtin player on skip - non-blocking // Optimistic local stop for builtin player on skip - non-blocking
final builtinPlayerId = await SettingsService.getBuiltinPlayerId(); final builtinPlayerId = await SettingsService.getBuiltinPlayerId();
if (builtinPlayerId != null && playerId == builtinPlayerId && _sendspinConnected) { if (builtinPlayerId != null && _isBuiltinPlayer(playerId, builtinPlayerId) && _sendspinConnected) {
_logger.log('⏭️ Non-blocking local stop for skip on builtin player'); _logger.log('⏭️ Non-blocking local stop for skip on builtin player');
// Stop current audio immediately - fire and forget, but log errors // Stop current audio immediately - fire and forget, but log errors
unawaited((_pcmAudioPlayer?.pause() ?? Future.value()).catchError( unawaited((_pcmAudioPlayer?.pause() ?? Future.value()).catchError(
@@ -6200,7 +6216,7 @@ class MusicAssistantProvider with ChangeNotifier {
try { try {
// Optimistic local stop for builtin player on previous - non-blocking // Optimistic local stop for builtin player on previous - non-blocking
final builtinPlayerId = await SettingsService.getBuiltinPlayerId(); final builtinPlayerId = await SettingsService.getBuiltinPlayerId();
if (builtinPlayerId != null && playerId == builtinPlayerId && _sendspinConnected) { if (builtinPlayerId != null && _isBuiltinPlayer(playerId, builtinPlayerId) && _sendspinConnected) {
_logger.log('⏮️ Non-blocking local stop for previous on builtin player'); _logger.log('⏮️ Non-blocking local stop for previous on builtin player');
// Stop current audio immediately - fire and forget, but log errors // Stop current audio immediately - fire and forget, but log errors
unawaited((_pcmAudioPlayer?.pause() ?? Future.value()).catchError( unawaited((_pcmAudioPlayer?.pause() ?? Future.value()).catchError(
@@ -6614,7 +6630,7 @@ class MusicAssistantProvider with ChangeNotifier {
} }
final builtinPlayerId = await SettingsService.getBuiltinPlayerId(); final builtinPlayerId = await SettingsService.getBuiltinPlayerId();
if (builtinPlayerId != null && playerId == builtinPlayerId) { if (builtinPlayerId != null && _isBuiltinPlayer(playerId, builtinPlayerId)) {
_localPlayerVolume = volumeLevel; _localPlayerVolume = volumeLevel;
FlutterVolumeController.setVolume(volumeLevel / 100.0); FlutterVolumeController.setVolume(volumeLevel / 100.0);
} }