diff --git a/lib/player/audio_handler.dart b/lib/player/audio_handler.dart index eb39395..c407216 100644 --- a/lib/player/audio_handler.dart +++ b/lib/player/audio_handler.dart @@ -3,6 +3,8 @@ import 'dart:async'; import 'package:audio_service/audio_service.dart'; import 'package:just_audio/just_audio.dart'; import '../library/database.dart'; +import '../services/cache_manager.dart'; +import '../services/navidrome_service.dart'; import 'sleep_timer.dart'; /// Entscheidet, ob eine gespeicherte Wiedergabeposition beim Laden @@ -25,8 +27,12 @@ class MeloAudioHandler extends BaseAudioHandler with QueueHandler, SeekHandler { final MeloDb db; late final SleepTimer sleepTimer; late final Timer _positionRecordTimer; + late final CacheManager _cache; + final NavidromeService _nav = NavidromeService(); MeloAudioHandler({required this.db}) { + _cache = CacheManager(); + _cache.init(); sleepTimer = SleepTimer(onElapsed: pause); // just_audio-Events → audio_service PlaybackState _player.playbackEventStream.map(_transformEvent).pipe(playbackState); @@ -58,11 +64,25 @@ class MeloAudioHandler extends BaseAudioHandler with QueueHandler, SeekHandler { } /// Ersetzt die Warteschlange und startet ab [startIndex]. + /// Auto-Caching: Streame von Server + speichere lokal gleichzeitig. Future loadPlaylist(List items, {int startIndex = 0}) async { queue.add(items); - final sources = items - .map((item) => AudioSource.uri(Uri.parse(item.id), tag: item)) - .toList(); + await _nav.ladeGespeicherteZugangsdaten(); + + final sources = []; + for (final item in items) { + Uri sourceUri = Uri.parse(item.id); + + if (_nav.istVerbunden) { + final cachedUri = await _nav.streamAndCacheToLocal(item.id, _cache); + if (cachedUri != null) { + sourceUri = cachedUri; + } + } + + sources.add(AudioSource.uri(sourceUri, tag: item)); + } + await _player.setAudioSources(sources, initialIndex: startIndex); // Bei bekannter letzter Position an dieser Stelle fortsetzen, diff --git a/lib/services/navidrome_service.dart b/lib/services/navidrome_service.dart index dc64fa2..4ebd637 100644 --- a/lib/services/navidrome_service.dart +++ b/lib/services/navidrome_service.dart @@ -5,6 +5,7 @@ import 'package:crypto/crypto.dart'; import 'package:http/http.dart' as http; import 'package:shared_preferences/shared_preferences.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'cache_manager.dart'; class SubsonicSong { final String id; @@ -264,6 +265,39 @@ class NavidromeService { return Lyrics.empty(); } } + + Future streamAndCacheToLocal(String songId, CacheManager cache) async { + try { + final streamUri = streamUrl(songId); + final cacheFile = await cache.getCacheFile(streamUri.toString()); + + if (await cacheFile.exists()) { + debugPrint('Cache-Hit: ${cacheFile.path}'); + return Uri.file(cacheFile.path); + } + + debugPrint('Cache-Miss: Starten download zu ${cacheFile.path}'); + final request = http.Request('GET', streamUri); + final response = await http.Client().send(request).timeout(const Duration(seconds: 30)); + + if (response.statusCode != 200) { + debugPrint('Stream-Fehler: HTTP ${response.statusCode}'); + return streamUri; + } + + final sink = cacheFile.openWrite(); + await for (final chunk in response.stream) { + sink.add(chunk); + } + await sink.close(); + + debugPrint('Cache-Speicherung erfolgreich: ${cacheFile.path}'); + return Uri.file(cacheFile.path); + } catch (e) { + debugPrint('streamAndCacheToLocal Fehler: $e'); + return streamUrl(songId); + } + } } class SubsonicArtist {