Compare commits

..
Author SHA1 Message Date
Your NameandClaude Opus 4.5 f7378e7a25 Fix favorites not persisting after navigation
- Replace invalid 'music/album' API command with 'music/item_by_uri'
- Use album URI to fetch fresh favorite status on album screen load
- Invalidate home cache when favorite is toggled so home screen
  shows updated status

Fixes issue where favorites would appear in search/library but
not persist on home screen due to stale cached data.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-10 10:23:32 +00:00
Your NameandClaude Opus 4.5 9e045b1bd4 Fix favorites status not persisting after navigation
Fetch fresh album details on screen load to get current favorite
status from MA, instead of relying on potentially stale cached data.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-10 10:04:12 +00:00
Your NameandClaude Opus 4.5 7049128f01 Fix connection race condition on reconnect
- Add _isDisposed flag to prevent reconnects after API disposal
- Check if connection already in progress before reconnecting
- Check if already connected/authenticated before attempting reconnect
- Properly dispose old API when creating new connection
- Complete pending connection futures on dispose to prevent leaks

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-10 09:36:46 +00:00
Your NameandClaude Opus 4.5 e5dc9cbf89 Add note about local playback on MA beta 20+
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-10 09:10:54 +00:00
4 changed files with 87 additions and 15 deletions
+2
View File
@@ -87,6 +87,8 @@ Ensemble supports multiple authentication methods:
- Network connectivity to your Music Assistant server
- Android device (Android 5.0+)
> **Note:** Local device playback (streaming to your phone) is not currently supported on Music Assistant beta 20+. This is due to MA replacing the built-in player with Sendspin. Support will be added once Sendspin is more stable. Remote control of other players works fine.
## About Music Assistant
Ensemble is a mobile client for [Music Assistant](https://music-assistant.io/), an open-source music library manager that integrates with various music sources and streaming providers.
+2 -1
View File
@@ -141,7 +141,8 @@ class MusicAssistantProvider with ChangeNotifier {
_serverUrl = serverUrl;
await SettingsService.setServerUrl(serverUrl);
await _api?.disconnect();
// Dispose the old API to stop any pending reconnects
_api?.dispose();
_api = MusicAssistantAPI(serverUrl, _authManager);
+27
View File
@@ -45,6 +45,30 @@ class _AlbumDetailsScreenState extends State<AlbumDetailsScreen> with SingleTick
_loadTracks();
_extractColors();
_loadAlbumDescription();
_refreshFavoriteStatus();
}
Future<void> _refreshFavoriteStatus() async {
final maProvider = context.read<MusicAssistantProvider>();
if (maProvider.api == null) return;
// Need a URI to fetch fresh album data
final albumUri = widget.album.uri;
if (albumUri == null || albumUri.isEmpty) {
_logger.log('Cannot refresh favorite status: album has no URI');
return;
}
try {
final freshAlbum = await maProvider.api!.getAlbumByUri(albumUri);
if (freshAlbum != null && mounted) {
setState(() {
_isFavorite = freshAlbum.favorite ?? false;
});
}
} catch (e) {
_logger.log('Error refreshing favorite status: $e');
}
}
Future<void> _extractColors() async {
@@ -128,6 +152,9 @@ class _AlbumDetailsScreenState extends State<AlbumDetailsScreen> with SingleTick
_isFavorite = newState;
});
// Invalidate home cache so the home screen shows updated favorite status
maProvider.invalidateHomeCache();
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
+56 -14
View File
@@ -63,15 +63,24 @@ class MusicAssistantAPI {
// Guard to prevent multiple simultaneous connection attempts
Completer<void>? _connectionInProgress;
bool _isDisposed = false;
Future<void> connect() async {
// If already connected, nothing to do
if (_currentState == MAConnectionState.connected) {
// If disposed, don't try to connect
if (_isDisposed) {
_logger.log('Connection: API disposed, skipping connect');
return;
}
// If already connected or authenticated, nothing to do
if (_currentState == MAConnectionState.connected ||
_currentState == MAConnectionState.authenticated) {
_logger.log('Connection: Already connected, skipping');
return;
}
// If connection is in progress, wait for it instead of starting another
if (_connectionInProgress != null) {
if (_connectionInProgress != null && !_connectionInProgress!.isCompleted) {
_logger.log('Connection already in progress, waiting...');
return _connectionInProgress!.future;
}
@@ -611,13 +620,14 @@ class MusicAssistantAPI {
}
}
Future<Album?> getAlbumDetails(String provider, String itemId) async {
/// Get album details by URI
/// Use the album's uri field (e.g., "library://album/123" or "spotify://album/xyz")
Future<Album?> getAlbumByUri(String uri) async {
try {
final response = await _sendCommand(
'music/album',
'music/item_by_uri',
args: {
'provider': provider,
'item_id': itemId,
'uri': uri,
},
);
@@ -626,7 +636,7 @@ class MusicAssistantAPI {
return Album.fromJson(result as Map<String, dynamic>);
} catch (e) {
_logger.log('Error getting album details: $e');
_logger.log('Error getting album by URI: $e');
return null;
}
}
@@ -1975,13 +1985,39 @@ class MusicAssistantAPI {
}
Future<void> _reconnect() async {
// Don't reconnect if disposed
if (_isDisposed) {
_logger.log('Reconnect: API disposed, skipping');
return;
}
// Don't reconnect if already connected or connecting
if (_currentState == MAConnectionState.connected ||
_currentState == MAConnectionState.authenticated ||
_currentState == MAConnectionState.connecting) {
_logger.log('Reconnect: Already connected/connecting, skipping');
return;
}
// Don't reconnect if another connection is in progress
if (_connectionInProgress != null && !_connectionInProgress!.isCompleted) {
_logger.log('Reconnect: Connection already in progress, skipping');
return;
}
await Future.delayed(Timings.reconnectDelay);
if (_currentState != MAConnectionState.connected) {
try {
await connect();
} catch (e) {
_logger.log('Reconnection failed: $e');
}
// Check again after delay
if (_isDisposed ||
_currentState == MAConnectionState.connected ||
_currentState == MAConnectionState.authenticated) {
return;
}
try {
await connect();
} catch (e) {
_logger.log('Reconnection failed: $e');
}
}
@@ -1994,7 +2030,13 @@ class MusicAssistantAPI {
}
void dispose() {
_isDisposed = true;
_stopHeartbeat();
// Complete any pending connection to prevent hanging futures
if (_connectionInProgress != null && !_connectionInProgress!.isCompleted) {
_connectionInProgress!.completeError(Exception('API disposed'));
}
_connectionInProgress = null;
disconnect();
_connectionStateController.close();
for (final stream in _eventStreams.values) {