feat(phase2): Auto-Caching beim Musikstreaming (P0)
IMPLEMENTATION: - NavidromeService.streamAndCacheToLocal(): Stream + gleichzeitig cachen - Cache-First-Logik: Wenn Cache existiert → lokal spielen - AudioHandler erweitert: loadPlaylist() nutzt Auto-Caching - CacheManager Integration in AudioHandler FLOW: 1. User spielt Album ab → streamt vom Server 2. Während des Abspielens → lokal in Cache speichern 3. Nächstes Mal: Lokal spielen (schneller, Offline möglich) Phase 2 P0 COMPLETE! ✅ Tests: 70/70 ✅
This commit is contained in:
@@ -3,6 +3,8 @@ import 'dart:async';
|
|||||||
import 'package:audio_service/audio_service.dart';
|
import 'package:audio_service/audio_service.dart';
|
||||||
import 'package:just_audio/just_audio.dart';
|
import 'package:just_audio/just_audio.dart';
|
||||||
import '../library/database.dart';
|
import '../library/database.dart';
|
||||||
|
import '../services/cache_manager.dart';
|
||||||
|
import '../services/navidrome_service.dart';
|
||||||
import 'sleep_timer.dart';
|
import 'sleep_timer.dart';
|
||||||
|
|
||||||
/// Entscheidet, ob eine gespeicherte Wiedergabeposition beim Laden
|
/// Entscheidet, ob eine gespeicherte Wiedergabeposition beim Laden
|
||||||
@@ -25,8 +27,12 @@ class MeloAudioHandler extends BaseAudioHandler with QueueHandler, SeekHandler {
|
|||||||
final MeloDb db;
|
final MeloDb db;
|
||||||
late final SleepTimer sleepTimer;
|
late final SleepTimer sleepTimer;
|
||||||
late final Timer _positionRecordTimer;
|
late final Timer _positionRecordTimer;
|
||||||
|
late final CacheManager _cache;
|
||||||
|
final NavidromeService _nav = NavidromeService();
|
||||||
|
|
||||||
MeloAudioHandler({required this.db}) {
|
MeloAudioHandler({required this.db}) {
|
||||||
|
_cache = CacheManager();
|
||||||
|
_cache.init();
|
||||||
sleepTimer = SleepTimer(onElapsed: pause);
|
sleepTimer = SleepTimer(onElapsed: pause);
|
||||||
// just_audio-Events → audio_service PlaybackState
|
// just_audio-Events → audio_service PlaybackState
|
||||||
_player.playbackEventStream.map(_transformEvent).pipe(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].
|
/// Ersetzt die Warteschlange und startet ab [startIndex].
|
||||||
|
/// Auto-Caching: Streame von Server + speichere lokal gleichzeitig.
|
||||||
Future<void> loadPlaylist(List<MediaItem> items, {int startIndex = 0}) async {
|
Future<void> loadPlaylist(List<MediaItem> items, {int startIndex = 0}) async {
|
||||||
queue.add(items);
|
queue.add(items);
|
||||||
final sources = items
|
await _nav.ladeGespeicherteZugangsdaten();
|
||||||
.map((item) => AudioSource.uri(Uri.parse(item.id), tag: item))
|
|
||||||
.toList();
|
final sources = <AudioSource>[];
|
||||||
|
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);
|
await _player.setAudioSources(sources, initialIndex: startIndex);
|
||||||
|
|
||||||
// Bei bekannter letzter Position an dieser Stelle fortsetzen,
|
// Bei bekannter letzter Position an dieser Stelle fortsetzen,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import 'package:crypto/crypto.dart';
|
|||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||||
|
import 'cache_manager.dart';
|
||||||
|
|
||||||
class SubsonicSong {
|
class SubsonicSong {
|
||||||
final String id;
|
final String id;
|
||||||
@@ -264,6 +265,39 @@ class NavidromeService {
|
|||||||
return Lyrics.empty();
|
return Lyrics.empty();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<Uri?> 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 {
|
class SubsonicArtist {
|
||||||
|
|||||||
Reference in New Issue
Block a user