deletePlaylist() schickte ein POST auf /api/v1/cloud/playlists. Der Server
unterscheidet dort nach Methode (POST = anlegen, DELETE = löschen), also
landete das Löschen im Anlegen-Handler, scheiterte an der leeren
Namensprüfung und lieferte HTTP 200 mit `status: error` zurück.
Zusätzlich wurde der Rückgabewert in cloud_screen.dart ignoriert: die UI
meldete "Playlist gelöscht", während die Playlist auf dem Server bestehen
blieb und beim nächsten Sync wieder auftauchte.
- cloud_service.dart: DELETE mit id im Body, Fehler werden geloggt
- cloud_screen.dart: Rückgabewert auswerten statt pauschal Erfolg melden
- cloud_service_test.dart: 3 Regressionstests (Methode ist DELETE, id im
Body, status != ok → false, Netzwerkfehler → false)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- cloud_screen.dart: Sync-Popup mit Live-Fortschritt („12/325 synchronisiert"),
„Im Hintergrund fortsetzen"-Button, Ergebnis-Snackbar nach Abschluss
- sync_service.dart: Notification-Kanal auf HIGH + Visibility.public,
Foreground-Service starten/stoppen während Sync (Display-aus-Garantie),
statische syncNotificationDetails() für Tests
- main.dart: Sync-Channel-Importance auf HIGH (überschreibt alte low-Einstellung)
- Tests: +7 (syncNotificationDetails HIGH/public, Lifecycle-Unabhängigkeit)
CRITICAL (C1): RealtimeSyncService jetzt Singleton (Factory-Pattern)
- Verhindert doppelte SSE-Verbindungen durch home_screen + settings
- UI-Notifier (istVerbundenNotifier) app-weit konsistent
HIGH (H1): _reconnecteOderFallback + starteWennAktiviert setzen
cloud_interval vor starteAutoSyncTimer() — sonst bleibt Timer
bei Fallback auf 0 (kein Sync)
HIGH (H2): Race-Condition in _verbinde() nach await request.close()
— _pausiert-Flag re-check; bei true → clean exit mit _laeuft=false
HIGH (H3): fortsetzen() re-checkt _pausiert nach starteWennAktiviert()
— verhindert Leak bei schnellem App-Umschalten
MEDIUM (M1): _syncModusSetzen stoppt SSE bei Wechsel auf manual/interval
(vorher lief die Verbindung weiter)
MEDIUM (M2): Event-Logging loggt nur noch Event-Typ, nicht rohe Daten
(Privacy: Song-IDs/Titel/Künstler nicht im Log)
LOW (L1): _verbinde() setzt _laeuft=false bei Token-Fehler (kein Deadlock)
## MED-2 (Review-Fix, KRITISCH)
- sync_service.dart: Nach erfolgreichem cloud.download wird der Song jetzt
MIT cloud_id + Titeldaten (Titel/Künstler/Dauer/Größe/istKorrupt aus der
Server-Liste) in die lokale DB eingefügt — vorher fehlte der Song in der
Bibliothek, songNachCloudId blieb null und jeder Sync lud alle 325 Songs
erneut (≈ GB/Sync) und meldete sie fälschlich als „+neu“
- Neuer statischer Helfer `songAusServerMap` (testbar): baut den Song aus
den handle_list-Feldern; ytUrl wird bewusst NICHT übernommen (v2.52-F2:
Server bleibt YT-Link-Quelle)
- Zielpfad-Kollisionen abgesichert: existiert die Datei bereits (verwaister
Download aus früherem Sync ohne DB-Eintrag), wird „ (2)“, „ (3)“ … an-
gehängt — sonst scheiterte der Insert am UNIQUE-Constraint datei_pfad
## MED-1 (Review-Fix)
- sync_service.dart: letzterSync-Snapshot wird VOR listSongs gelesen und am
Sync-Ende persistiert (Sync-BEGINN statt now-am-Ende) — Löschungen, die
zwischen listSongs und Sync-Ende auf dem Server ankommen, werden im
nächsten Sync korrekt angewendet statt als „bereits gesehen“ übersprungen
- cloud_screen.dart: _download schreibt cloud_last_sync_ts nicht mehr
nachträglich mit now-am-Ende (redundant + Race-Wieder-Einführung);
UI-Anzeige liest den von SyncService persistierten Snapshot
- melo_cloud.py: handle_delete stempelt deleted_at IMMER mit Server-Zeit
(datetime('now')) — Client-Timestamps werden ignoriert (Uhr-Drift würde
Löschungen dauerhaft überspringen lassen; konsistente Uhr für alle Geräte)
## Chip-Puls (home_screen)
- ListenableBuilder hört auf Listenable.merge([_vm, _cloud, SyncService.laeuftNotifier]) — der ☁️-Musikserver-Chip pulsiert jetzt bei laufendem Sync (auch im Hintergrund/Auto-Sync), nicht nur beim Verbinden/Laden
- serverSyncLaeuft = SyncService.laeuftNotifier.value || CloudStatus.verbinde || serverLadt
## Auto-Sync-Timer app-weit (main.dart)
- _starteAutoSyncFallsNoetig() entfernt (F3a: kein Sync beim App-Start); stattdessen SyncService.starteAutoSyncTimer() — Timer tickt erst nach dem Intervall, Fälligkeit via istSyncFaellig
- shared_preferences-Import aus main.dart entfernt (Intervall-Logik lebt in SyncService/CloudScreen)
## Sync-Notification
- Sync-Kanal de.baka.melo.sync wird in main() angelegt (Android 8+: Channel nötig, sonst erscheint die Notification nicht)
- onDidReceiveNotificationResponse: Tipp auf 'sync_fertig'/'sync_fehler' → SyncService.syncBenachrichtigungGetippt++ → MeloHome öffnet den Cloud-Tab
## Version
- Versionsanzeige 2.51 → 2.52.3 (settings/home/erweitert), MeloLogger.init('2.35') → '2.52.3'
## YT-Quell-URL (Server ist die Quelle)
- cloud_service.ytUrlSetzen(): POST /api/v1/cloud/yt-url (per-User UPDATE user_songs.yt_url)
- musik_scanner._sucheYtUrls(): durchsucht NUR Songs mit cloud_id, die auf dem Server noch keine yt_url haben; Treffer werden auf den Server hochgeladen statt lokal gespeichert
- db_helper: ytUrlAktualisieren/songsOhneYtUrl entfernt, songsMitCloudId() ergänzt
- download_service: yt_url wird beim Download nicht mehr lokal persistiert
## Fallback-Kette reDownload (korrupte/fehlende Datei)
- 1) Server-Download via cloud_id + Magic-Byte-Check (primäre Quelle)
- 2) Server-ytUrl (aus listSongs) → yt-proxy als letzte Chance
- 3) Legacy: lokal gespeicherte ytUrl (Bestandsdaten vor v2.52)
- song_tile: „Neu laden“ erscheint bei istKorrupt && (cloudId != null || ytUrl != null)
- 3 neue Tests (ytUrlSetzen: ok/error/401) → 111 Tests grün, analyze 0 Issues
## LOW (song_tile.dart)
- Label für gesunden Song mit ytUrl heißt jetzt „Neu laden“ statt
„Herunterladen“ — die Aktion ist ein destruktiver reDownload
(löscht DB-Eintrag + lokale Datei), das alte Label war irreführend
## MED-3 (cloud_screen.dart)
- Konflikterkennung prüft jetzt Titel ODER Künstler (case-insensitiv)
- Dialog dismissable (Tap außerhalb = Abbruch → lokale Version behalten,
Sync läuft weiter — kein Hängenbleiben)
- Batch-Optionen: Checkbox „Für alle weiteren Konflikte übernehmen“
(_konfliktBatch) + Button „Alle weiteren überspringen“
- Auto-Sync (_autoSyncDurchfuehren) löst Konflikte NICHT interaktiv:
Server-Metadaten gewinnen still (automatisch: true)
## MED-4 (cloud_screen.dart)
- Neuer echter Sync-Guard _syncProzessLaeuft, getrennt vom UI-Flag _syncLaeuft
- „Im Hintergrund fortsetzen“ setzt nur noch _syncLaeuft zurück — der Guard
bleibt bis zum echten Loop-Ende (finally) gesperrt → kein Doppel-Sync
- _download/_autoSyncDurchfuehren prüfen _syncProzessLaeuft; „Jetzt Syncen“
während Hintergrund-Sync zeigt Hinweis
- finally gibt den Guard immer frei + AnimationController-stop try/catch
(Screen kann während Hintergrund-Sync disposed sein)
## LOW (cloud_screen.dart)
- cloud_last_sync/_ts werden nur noch bei erfolgreichem Sync geschrieben
(_syncAlles returned bool; Fehler/Guard → kein Schreiben)
- _syncedItems-Zähler konsistent (Server-Übernahmen/Downloads je ++ statt
Überschreiben)
## MED-1 (settings_screen.dart)
- Erweitert-Push wird awaited; Ergebnis 'scanner' wird an MeloHome weitergereicht
- onScan-Closure poppt nur noch den Erweitert-Screen, SettingsScreen propagiert
das Ergebnis per eigenem Navigator.pop (mit State-Context + mounted-Guard)
## Cloud-Dashboard (cloud_screen.dart)
- Statistik-Karten 2x2: Songs, Playlisten, Favoriten, Letzter Sync (Daten aus statusDaten/syncStatus)
- Speicheranzeige mit Balken — Platzhalter „–" wenn der Server keine Storage-Daten liefert (storage_used/storage_total o.ä.)
- Sync-Historie-Zeile: „Heute: X Dateien, Y Favoriten, Z Playlisten" aus _syncHistorie (heute gefiltert)
- Konflikt-Dialog NUR bei abweichendem Titel (lokal ≠ Server), sonst kein Dialog
- Lint-Fixes: curly_braces (if ohne Block) + unused _syncHistorieHeuteText (jetzt in der Historie-Zeile verdrahtet)
- Sync-Animation während des Syncs: rotierendes ☁️-Icon + Fortschrittsring (CloudStatus/AnimationController)
Keine neuen Packages, flutter analyze 0 Issues, Tests 98/98 grün
## Schnellaktionen (song_tile.dart)
- Longpress öffnet Bottom-Sheet mit: ▶️ Abspielen, ➕ Als Nächstes, ➕ Am Ende hinzufügen, ⭐ Favorit (hinzufügen/entfernen), ⬇️ Herunterladen/Erneut herunterladen, 🏷 Tags bearbeiten, ✏️ Umbenennen
- Nur verfügbare Aktionen werden gezeigt (Download nur mit ytUrl, Tags/Umbenennen nur mit DB-id)
- Song-Info-Kopfzeile im Sheet (Titel + Künstler)
## Fix 1 (MED): Glow-Pulsation rebuildet jetzt wirklich
- now_playing_screen.dart: Cover-Container mit AnimatedBuilder(animation: _glowController) gewrappt — BoxShadow (blur 50–70 / spread 6–12) wird jetzt pro Animations-Tick neu gebaut statt nur bei fremden setStates (Pulsation war vorher nur zufällig über positionStream sichtbar und fror bei Pause/Pufferung ein)
## Fix 2 (MED): Echter Crossfade beim Songwechsel
- _songSub-Listener setzt _hatCover/_coverPfad jetzt SYNCHRON auf false/null (im selben setState) — Hintergrund und Front-Cover zeigen beim Songwechsel kein altes Cover mehr, bis die async Existenz-Prüfung durch ist
- _hintergrundBlur-Key auf den tatsächlichen Cover-Pfad gelegt ('cover-$pfad' statt song.id) — Switcher wechselt erst bei geladenem neuem Cover (Crossfade Gradient→Cover), kein Fade alt→alt + harter Cut mehr
flutter analyze: 0 Issues, Tests 98/98 grün, keine neuen Packages
## Fullscreen-Player (now_playing_screen.dart)
- Glow-Farbe folgt MeloTheme.akzentNotifier (Per-User-Akzent) statt fixem Rot; Listener rebuildet bei Akzentwechsel
- Dynamische Glow-Pulsation: AnimationController (Bordmittel) pulsiert blurRadius 50–70 / spreadRadius 6–12 nur bei Wiedergabe, stillstehend bei Pause
- Blur-Hintergrund crossfaded beim Songwechsel (AnimatedSwitcher 600ms, layoutBuilder mit StackFit.expand, ValueKey je Song)
- existsSync aus dem Build-Pfad entfernt: async _ladeCoverStatus mit try/catch, Guard gegen stale Song-Wechsel
## Fix (Code-Review Finding 3, MED)
- spieleAlsNaechstes: Song bereits in der Queue wird an Position nach dem
aktuellen VERSCHOBEN (inkl. _aktuellerIndex-Anpassung), keine Kopie mehr
- Shuffle: keine Neu-Mischung nach Play-Next — Song wird via
_pinneInShuffleReihenfolge an die nächste Position gepinnt (Index-
Korrektur für Verschiebungen, _shufflePos-Nachführung am Reihenende)
- 3 Tests ergänzt: Verschieben statt Duplizieren, Song von vor dem
aktuellen, Shuffle-Pin (kein Duplikat + naechsterIndex == gepinnter Song)
## Fix (Code-Review Finding 2, HIGH)
- DbHelper.songZurPlaylist: INSERT OR IGNORE (ConflictAlgorithm.ignore) —
Doppel-Tap-Race verletzt PRIMARY KEY (playlist_id, song_id) nicht mehr
- FavoritenService.umschalten: gibt neuen Status zurück, try/catch mit
MeloLogger, Zustand nach Fehler aus DB neu geladen statt blind geflippt
- NowPlayingScreen._toggleFavorit: In-Flight-Guard _toggleLaeuft gegen
parallele Toggles + Statusübernahme aus DB (Quelle der Wahrheit)
## Fix (Code-Review Finding 1, HIGH)
- _baueShuffleReihenfolge: Index 0 nur noch bei Repeat-playlist in die
Shuffle-Reihenfolge aufnehmen (Einzel-Song-Queue)
- naechsterIndex: Sicherheitsnetz gegen stale Reihenfolge — Kandidat ==
aktueller Song bei Repeat-aus liefert null (Queue-Ende) statt denselben
Song erneut
- Test ergänzt: shuffle + repeat aus + 1-Song-Queue → null
## PlayerService
- warteschlange Getter (unveränderliche Kopie) + aktuellerIndex
- spieleAlsNaechstes(Song): fügt nach aktuellem Song ein
- amEndeHinzufuegen(Song): hängt ans Ende an
- verschiebeInWarteschlange(von, nach): Drag-and-Drop mit korrekter Anpassung des aktuellen Index
## WarteschlangeSheet (neu: lib/widgets/warteschlange_sheet.dart)
- Bottom-Sheet: aktuelle Warteschlange, aktueller Song rot markiert (▶)
- Tipp auf Song → sofort abspielen
- ReorderableListView (Bordmittel) zum Umsortieren per Drag
- Leer-Zustand + Titel-Zähler
## MiniPlayer
- Tipp auf den Miniplayer öffnet die Warteschlange (Bottom-Sheet)
Keine neuen Packages, flutter analyze 0 Issues, Tests 4/4
## PlayerService
- Enum Wiederholmodus { aus, titel, playlist } + Getter/Setter
- naechstes(): titel → gleicher Song von vorn (seek 0 + play)
- naechsterIndex(): zentrale Berechnung des nächsten Index
- playlist: nach letztem Song wieder von vorne (auch bei Shuffle: neu mischen)
- aus: Ende der Warteschlange stoppt
- Auto-Next (ProcessingState.completed) läuft über naechstes() → Repeat wirkt auch automatisch
## MiniPlayer
- Repeat-Icon mit 3 Zuständen (repeat / repeat_one), rot wenn aktiv, Wechsel bei Tipp
Keine neuen Packages, flutter analyze 0 Issues, Tests 4/4
## PlayerService
- PlayerService ist jetzt ChangeNotifier (UI hängt per addListener/ListenableBuilder an)
- zufallsmodus Getter + setZufallsmodus(bool)
- Zufallsreihenfolge: alle Queue-Indizes außer dem aktuellen Song, gemischt
- naechstes(): bei Shuffle nächster Song aus Zufallsreihenfolge, ohne Wiederholung bis alle durch
- vorheriges(): geht in der Zufallsreihenfolge zurück
- spiele(): synchronisiert Shuffle-Position (auch bei manueller Auswahl)
## MiniPlayer
- Shuffle-Icon (rot wenn aktiv, Tipp schaltet um)
Keine neuen Packages, flutter analyze 0 Issues, Tests 4/4
## CloudService
- CloudStatus-Enum (verbinde/verbunden/fehler), CloudService extends ChangeNotifier
- verbinde(): Login+Status beim App-Start, setzt Status, notifyListeners
- Getter status/statusText/istVerbunden/verbindungGestartet
- Bestehende Methoden behalten — nur status() → statusDaten() umbenannt (Namenskonflikt mit neuem status-Getter)
## Home-Screen
- _cloud.verbinde() in initState (feuern-und-vergessen) — Verbindung steht beim App-Start, nicht erst beim Tab-Öffnen
## Cloud-Screen
- initState verbindet nicht mehr selbst — zeigt Service-Status via ListenableBuilder
- Status wechselt live: 'Verbinde...' (gelb) → 'Verbunden' (grün) / 'Keine Verbindung' (rot)
- Serverdaten (Playlists/Favoriten/Zähler) laden automatisch, sobald Service verbunden
- Refresh-Button re-verbindet + lädt Zähler neu
- Fallback: ungestarteter Service (Einstellungen-Screen) wird beim Öffnen gestartet