Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f7378e7a25 | ||
|
|
9e045b1bd4 | ||
|
|
7049128f01 | ||
|
|
e5dc9cbf89 |
@@ -87,6 +87,8 @@ Ensemble supports multiple authentication methods:
|
|||||||
- Network connectivity to your Music Assistant server
|
- Network connectivity to your Music Assistant server
|
||||||
- Android device (Android 5.0+)
|
- 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
|
## 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.
|
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.
|
||||||
|
|||||||
@@ -141,7 +141,8 @@ class MusicAssistantProvider with ChangeNotifier {
|
|||||||
_serverUrl = serverUrl;
|
_serverUrl = serverUrl;
|
||||||
await SettingsService.setServerUrl(serverUrl);
|
await SettingsService.setServerUrl(serverUrl);
|
||||||
|
|
||||||
await _api?.disconnect();
|
// Dispose the old API to stop any pending reconnects
|
||||||
|
_api?.dispose();
|
||||||
|
|
||||||
_api = MusicAssistantAPI(serverUrl, _authManager);
|
_api = MusicAssistantAPI(serverUrl, _authManager);
|
||||||
|
|
||||||
|
|||||||
@@ -45,6 +45,30 @@ class _AlbumDetailsScreenState extends State<AlbumDetailsScreen> with SingleTick
|
|||||||
_loadTracks();
|
_loadTracks();
|
||||||
_extractColors();
|
_extractColors();
|
||||||
_loadAlbumDescription();
|
_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 {
|
Future<void> _extractColors() async {
|
||||||
@@ -128,6 +152,9 @@ class _AlbumDetailsScreenState extends State<AlbumDetailsScreen> with SingleTick
|
|||||||
_isFavorite = newState;
|
_isFavorite = newState;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Invalidate home cache so the home screen shows updated favorite status
|
||||||
|
maProvider.invalidateHomeCache();
|
||||||
|
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
|
|||||||
@@ -63,15 +63,24 @@ class MusicAssistantAPI {
|
|||||||
|
|
||||||
// Guard to prevent multiple simultaneous connection attempts
|
// Guard to prevent multiple simultaneous connection attempts
|
||||||
Completer<void>? _connectionInProgress;
|
Completer<void>? _connectionInProgress;
|
||||||
|
bool _isDisposed = false;
|
||||||
|
|
||||||
Future<void> connect() async {
|
Future<void> connect() async {
|
||||||
// If already connected, nothing to do
|
// If disposed, don't try to connect
|
||||||
if (_currentState == MAConnectionState.connected) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If connection is in progress, wait for it instead of starting another
|
// 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...');
|
_logger.log('Connection already in progress, waiting...');
|
||||||
return _connectionInProgress!.future;
|
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 {
|
try {
|
||||||
final response = await _sendCommand(
|
final response = await _sendCommand(
|
||||||
'music/album',
|
'music/item_by_uri',
|
||||||
args: {
|
args: {
|
||||||
'provider': provider,
|
'uri': uri,
|
||||||
'item_id': itemId,
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -626,7 +636,7 @@ class MusicAssistantAPI {
|
|||||||
|
|
||||||
return Album.fromJson(result as Map<String, dynamic>);
|
return Album.fromJson(result as Map<String, dynamic>);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
_logger.log('Error getting album details: $e');
|
_logger.log('Error getting album by URI: $e');
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1975,15 +1985,41 @@ class MusicAssistantAPI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _reconnect() async {
|
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);
|
await Future.delayed(Timings.reconnectDelay);
|
||||||
if (_currentState != MAConnectionState.connected) {
|
|
||||||
|
// Check again after delay
|
||||||
|
if (_isDisposed ||
|
||||||
|
_currentState == MAConnectionState.connected ||
|
||||||
|
_currentState == MAConnectionState.authenticated) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await connect();
|
await connect();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
_logger.log('Reconnection failed: $e');
|
_logger.log('Reconnection failed: $e');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> disconnect() async {
|
Future<void> disconnect() async {
|
||||||
_stopHeartbeat();
|
_stopHeartbeat();
|
||||||
@@ -1994,7 +2030,13 @@ class MusicAssistantAPI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void dispose() {
|
void dispose() {
|
||||||
|
_isDisposed = true;
|
||||||
_stopHeartbeat();
|
_stopHeartbeat();
|
||||||
|
// Complete any pending connection to prevent hanging futures
|
||||||
|
if (_connectionInProgress != null && !_connectionInProgress!.isCompleted) {
|
||||||
|
_connectionInProgress!.completeError(Exception('API disposed'));
|
||||||
|
}
|
||||||
|
_connectionInProgress = null;
|
||||||
disconnect();
|
disconnect();
|
||||||
_connectionStateController.close();
|
_connectionStateController.close();
|
||||||
for (final stream in _eventStreams.values) {
|
for (final stream in _eventStreams.values) {
|
||||||
|
|||||||
Reference in New Issue
Block a user