import 'package:flutter/foundation.dart'; import '../services/melo_cloud_service.dart'; import '../services/navidrome_service.dart'; import 'database.dart'; /// Koordiniert Playlisten- und Favoriten-Operationen; benachrichtigt Listener /// nach jeder Mutation (für Feedback wie SnackBars — die Listen selbst /// beobachten UIs direkt über die watch()-Streams von [MeloDb]). class PlaylistService extends ChangeNotifier { // `this._cloud` würde den öffentlichen Parameternamen `cloud` zu `_cloud` // (privat) ändern und die API brechen. PlaylistService(this.db, {NavidromeService? navidrome, MeloCloudService? cloud}) : _navidrome = navidrome ?? NavidromeService(), // ignore: prefer_initializing_formals _cloud = cloud; final MeloDb db; final NavidromeService _navidrome; /// Optional: ohne Cloud-Zugang entfällt der Sofort-Push stillschweigend. final MeloCloudService? _cloud; Future createPlaylist(String name, {String? description}) async { final id = await db.createPlaylist(name, description: description); notifyListeners(); await _sichereNeuePlaylist(id, name); return id; } Future deletePlaylist(String id) async { await db.deletePlaylist(id); notifyListeners(); } Future addSongToPlaylist( String playlistId, String songId, int position) async { await db.addSongToPlaylist(playlistId, songId, position); notifyListeners(); final cloudId = await _cloudIdDerPlaylist(playlistId); final songCloudId = (await db.songById(songId))?.cloudId; if (cloudId == null || songCloudId == null) return; await _still(() => _cloud!.fuegePlaylistSongsHinzu(cloudId, [songCloudId])); } Future removeSongFromPlaylist( String playlistId, String songId) async { final songCloudId = (await db.songById(songId))?.cloudId; await db.removeSongFromPlaylist(playlistId, songId); notifyListeners(); final cloudId = await _cloudIdDerPlaylist(playlistId); if (cloudId == null || songCloudId == null) return; await _still(() => _cloud!.entfernePlaylistSong(cloudId, songCloudId)); } Future reorderSong( String playlistId, String songId, int newPosition) async { await db.reorderPlaylistSong(playlistId, songId, newPosition); notifyListeners(); await _sichereReihenfolge(playlistId); } Future reorderAll( String playlistId, List orderedSongIds) async { await db.reorderAllPlaylistSongs(playlistId, orderedSongIds); notifyListeners(); await _sichereReihenfolge(playlistId); } Future toggleFavorite(String songId) async { await db.toggleFavorite(songId); notifyListeners(); await _meldeFavorit(songId); } /// Meldet den neuen Favoriten-Stand sofort an die Melo-Cloud. /// /// Deterministisch (`set:true/false`), nicht als Umschalten: ein /// abweichender Server-Stand darf den Wunsch nicht invertieren. Nur für /// Titel mit cloudId, und Fehler werden still geschluckt — der nächste /// Abgleich pusht additiv nach. Future _meldeFavorit(String songId) async { final cloud = _cloud; if (cloud == null || !cloud.istAngemeldet) return; final cloudId = (await db.songById(songId))?.cloudId; if (cloudId == null) return; final gesetzt = await db.watchIsFavorite(songId).first; try { await cloud.setzeFavorit(cloudId, gesetzt); } catch (e) { debugPrint('Favorit nicht gemeldet: $e'); } } /// Lädt alle Favoriten vom Navidrome-Server und importiert sie lokal. /// Gibt die Anzahl geladen Favoriten zurück oder 0 wenn nicht verbunden. Future syncFavoritesFromServer() async { await _navidrome.ladeGespeicherteZugangsdaten(); if (!_navidrome.istVerbunden) return 0; try { final serverFavorites = await _navidrome.getFavorites(); for (final song in serverFavorites) { // Prüfe ob Song lokal existiert, dann markiere als Favorit final exists = await db.songExists(song.id); if (exists) { await db.setFavorite(song.id, true); } } notifyListeners(); return serverFavorites.length; } catch (e) { debugPrint('Fehler beim Laden von Server-Favoriten: $e'); return 0; } } /// Lädt alle Playlisten vom Navidrome-Server und importiert sie als lokale Playlisten. /// Gibt die Anzahl importierter Playlisten zurück oder 0 wenn nicht verbunden. Future syncPlaylistsFromServer() async { await _navidrome.ladeGespeicherteZugangsdaten(); if (!_navidrome.istVerbunden) return 0; try { final serverPlaylists = await _navidrome.getPlaylists(); int importedCount = 0; for (final playlist in serverPlaylists) { // Lade Songs aus der Server-Playlist final songs = await _navidrome.getPlaylistSongs(playlist.id); if (songs.isEmpty) continue; // Erstelle lokale Playlist mit gleichem Namen final playlistId = await createPlaylist( '🌐 ${playlist.name}', description: playlist.comment, ); // Füge Songs hinzu (nur wenn sie lokal existieren) for (int i = 0; i < songs.length; i++) { final exists = await db.songExists(songs[i].id); if (exists) { await addSongToPlaylist(playlistId, songs[i].id, i); } } importedCount++; } notifyListeners(); return importedCount; } catch (e) { debugPrint('Fehler beim Laden von Server-Playlisten: $e'); return 0; } } /// Führt [aktion] aus und verwirft jeden Fehler. /// /// Die Sicherung ist einseitig und ohne Rollback: schlägt sie fehl, bleibt /// der lokale Stand, wie er ist, und die nächste Änderung versucht es /// erneut. Future _still(Future Function() aktion) async { try { await aktion(); } catch (e) { debugPrint('Playlist-Sicherung übersprungen: $e'); } } Future _cloudIdDerPlaylist(String playlistId) async { if (_cloud == null || !_cloud.istAngemeldet) return null; return (await db.playlistById(playlistId))?.cloudId; } Future _sichereNeuePlaylist(String id, String name) async { final cloud = _cloud; if (cloud == null || !cloud.istAngemeldet) return; await _still(() async { final cloudId = await cloud.legePlaylistAn(name); await db.setPlaylistCloudId(id, cloudId); }); } Future _sichereReihenfolge(String playlistId) async { final cloudId = await _cloudIdDerPlaylist(playlistId); if (cloudId == null) return; final songs = await db.watchPlaylistSongs(playlistId).first; final ids = [ for (final s in songs) if (s.cloudId != null) s.cloudId!, ]; if (ids.isEmpty) return; await _still(() => _cloud!.setzePlaylistReihenfolge(cloudId, ids)); } /// Holt die Playlisten des Servers **nur** auf ein Gerät ohne eigene: /// Neuinstallation oder Wiederherstellung. /// /// Es gibt bewusst keinen Rück-Merge — Umbenennungen und Änderungen eines /// zweiten Geräts erscheinen hier nicht. Der beidseitige Abgleich ist eine /// eigene, spätere Spec. Future stelleWiederHer() async { final cloud = _cloud; if (cloud == null || !cloud.istAngemeldet) return 0; if (await db.countPlaylists() > 0) return 0; var angelegt = 0; try { for (final vomServer in await cloud.playlisten()) { final id = await db.createPlaylist(vomServer.name); await db.setPlaylistCloudId(id, vomServer.id); final songCloudIds = await cloud.playlistSongs(vomServer.id); var position = 0; for (final songCloudId in songCloudIds) { final song = await db.songByCloudId(songCloudId); if (song == null) continue; await db.addSongToPlaylist(id, song.id, position++); } angelegt++; } } catch (e) { debugPrint('Playlist-Wiederherstellung abgebrochen: $e'); } notifyListeners(); return angelegt; } }