diff --git a/flutter_client/lib/player/audio_handler.dart b/flutter_client/lib/player/audio_handler.dart index 13886545..f9e2095b 100644 --- a/flutter_client/lib/player/audio_handler.dart +++ b/flutter_client/lib/player/audio_handler.dart @@ -1,23 +1,34 @@ +import 'dart:async'; + import 'package:audio_service/audio_service.dart'; import 'package:flutter/foundation.dart'; import 'package:just_audio/just_audio.dart'; import '../models/track.dart'; +import 'album_cover_cache.dart'; class MinstrelAudioHandler extends BaseAudioHandler with QueueHandler, SeekHandler { MinstrelAudioHandler() { _player.playbackEventStream.listen(_broadcastState); + _player.currentIndexStream.listen(_onCurrentIndexChanged); } final AudioPlayer _player = AudioPlayer(); String _baseUrl = ''; String? _token; + AlbumCoverCache? _coverCache; - void configure({required String baseUrl, required String? token}) { + void configure({ + required String baseUrl, + required String? token, + AlbumCoverCache? coverCache, + }) { _baseUrl = baseUrl; _token = token; + if (coverCache != null) _coverCache = coverCache; debugPrint('audio_handler.configure: baseUrl="$baseUrl" ' - 'tokenPresent=${token != null && token.isNotEmpty}'); + 'tokenPresent=${token != null && token.isNotEmpty} ' + 'cachePresent=${_coverCache != null}'); } Future setQueueFromTracks(List tracks, {int initialIndex = 0}) async { @@ -54,6 +65,11 @@ class MinstrelAudioHandler extends BaseAudioHandler with QueueHandler, SeekHandl sources, initialIndex: initialIndex, ); + + // Kick the cover fetch for the initial item — async, doesn't block + // playback. Subsequent track changes are handled by the + // currentIndexStream listener. + unawaited(_loadArtForCurrentItem()); } String _resolveStreamUrl(TrackRef t) { @@ -73,8 +89,35 @@ class MinstrelAudioHandler extends BaseAudioHandler with QueueHandler, SeekHandl artist: t.artistName, album: t.albumTitle, duration: Duration(seconds: t.durationSec), + // Stash album_id in extras so _loadArtForCurrentItem can pull it + // back without re-walking the track list. + extras: t.albumId.isEmpty ? null : {'album_id': t.albumId}, ); + void _onCurrentIndexChanged(int? idx) { + if (idx == null) return; + unawaited(_loadArtForCurrentItem()); + } + + /// Async-fetches the cover for whichever item is currently active and + /// pushes a MediaItem update with artUri set. No-op if no cache is + /// configured, no current item, the item has no album_id in extras, + /// or the fetch returns null. + Future _loadArtForCurrentItem() async { + final cache = _coverCache; + if (cache == null) return; + final current = mediaItem.value; + if (current == null) return; + final albumId = current.extras?['album_id'] as String?; + if (albumId == null || albumId.isEmpty) return; + if (current.artUri != null) return; // already set + final path = await cache.getOrFetch(albumId); + if (path == null) return; + // Discard if the user advanced to another track while we waited. + if (mediaItem.value?.id != current.id) return; + mediaItem.add(current.copyWith(artUri: Uri.file(path))); + } + @override Future play() => _player.play();