Files
Melo/docs/superpowers/specs/2026-08-27-sync-ausbau-design.md
T
Hermes (Server)andClaude Sonnet 5 3a376faf50 Spec: Sync-Ausbau (Auswahl-Upload, Einzel-Song-Offline, Merge-Fixes, SSE)
Design-Dokument für den Sync-Ausbau der Android-App, mit Dustin im
Brainstorming abgestimmt (Workflow-Bestandsaufnahme über 6 Schichten:
App-Sync, Offline, melo_cloud.py, Navidrome, Cross-Platform, Melo v2).
Kern: Favoriten-Merge-Fix (Datenverlust-Bug), gezielter Upload im
Auswahl-Modus, Einzel-Song-Offline, Playlist-Union-Sync,
Sync-Notification + Bericht, Echtzeit per SSE. Cross-Platform
Mac+Windows bewusst als eigenes Folgeprojekt ausgeklammert.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CcDiyJdVRqh1TtJk5JiabX
2026-08-27 00:04:06 +02:00

14 KiB
Raw Blame History

Sync-Ausbau: Auswahl-Upload, Einzel-Song-Offline, Merge-Fixes, Echtzeit

Status: Approved (Dustin, 2026-08-27) — vor dem Implementierungsplan noch durchs agent-review-panel (Pflicht-Workflow).

Kontext

Die Melo-App synchronisiert heute schon Musikdateien UND Metadaten mit der Melo-Cloud (cloud.baka-net.demelo_cloud.py, Bearer-JWT via BakaAuth): SyncService lädt lokale Songs ohne Cloud-ID automatisch hoch (Multipart, 50 MB-Grenze, Server-Dedup per sha256 + Akustik-Fingerprint), lädt neue Server-Songs herunter, zieht Tombstones in beide Richtungen nach (mit Lösch-Bremse) und läuft automatisch bei App-Start/Resume (15-Minuten-Drossel) mit Fortschrittsanzeige in den Einstellungen. Der Server hardlinkt jede hochgeladene Datei aktiv nach /home/dustin/navidrome/music und stößt einen Navidrome-Scan an — der Weg Handy→Server→Navidrome-Bibliothek existiert also bereits vollständig. DownloadService kann ganze Alben/Künstler in den App-Speicher offline nehmen (Fortschritt + Abbrechen), dazu gibt es einen automatischen 2-GB-Abspiel-Cache.

Diese Spec schließt die verbliebenen Lücken (Auftrag Dustin, 2026-08-26, im Brainstorming zerlegt und entschieden):

Entscheidung Ergebnis
Zerlegung Erst Sync-Ausbau (diese Spec, Android), Cross-Platform Mac+Windows als eigenes späteres Projekt
Upload-UX Bestehenden Auswahl-Modus in „Meine Musik“ um „Auf den Server laden“ erweitern; Auto-Upload beim Sync bleibt
Umfang Einzel-Song-Offline + Playlist-Sync + Fortschritts-Notification/Sync-Bericht + Echtzeit-SSE; Favoriten-Merge-Fix immer dabei
Playlist-Konflikte Vereinigung (Union) wie bei Favoriten; Reihenfolge: längerer Stand gewinnt
Bau-Ansatz Ansatz 1: inkrementeller Ausbau des bestehenden SyncService, neue Logik in kleinen separaten Einheiten

Ziele

  1. Favoriten-Merge-Fix (Datenverlust-Bug): kein Gerät überschreibt mehr die Server-Favoriten mit seinem lokalen Stand.
  2. Gezielter Upload: einzelne Songs im Auswahl-Modus markieren und hochladen.
  3. Einzel-Song-Offline: einzelne Server-Titel offline nehmen, nicht nur ganze Alben/Künstler.
  4. Playlist-Sync: Playlisten beidseitig mit der Melo-Cloud abgleichen.
  5. Sichtbarkeit: persistente Sync-Notification + „Was ist neu“-Bericht.
  6. Echtzeit: Änderungen anderer Geräte in Sekunden statt bis zu 15 Minuten (SSE), Poll bleibt Fallback.

Nicht-Ziele

  • Kein Cross-Platform (Mac/Windows) — eigenes Folgeprojekt. windows//linux/ existieren nicht, just_audio hat kein Windows-Backend; das ist Plattform-Infrastruktur, keine Sync-Logik.
  • Kein Delta-Protokoll (/sync?since=…): bei ~330 Songs auf dem Server ist die Voll-Liste billig; der Delta-Endpunkt deckt zudem nur Songs ab, nicht Favoriten/Playlisten. YAGNI.
  • Kein Hintergrund-Sync (WorkManager o. ä.): Sync weiterhin nur bei geöffneter App (Start/Resume/SSE/manuell) — wie bisher und wie in v2.
  • Keine Playlist-Tombstones auf dem Server: offline gelöschte Playlisten kommen beim nächsten Sync zurück (bekannte Einschränkung, siehe unten) — ein Server-Tombstone-System wäre ein eigener Auftrag.
  • Kein Chunked/Resumable Upload: 50-MB-Dateien am Stück wie bisher.

Architektur

Rückgrat bleibt SyncService (lib/services/sync_service.dart) + MeloCloudService (lib/services/melo_cloud_service.dart). Neu:

Einheit Datei Verantwortung
Merge-Logik lib/services/sync_merge.dart (neu) Reine Funktionen ohne I/O: favoritenVereinigung, playlistVereinigung, Playlist-Zuordnung per Name. Voll unit-testbar.
Benachrichtigung lib/services/sync_benachrichtigung.dart (neu) Dünner Wrapper um flutter_local_notifications + reine Entscheidungslogik (wann zeigen/aktualisieren/Bericht fällig).
Echtzeit lib/services/echtzeit_sync.dart (neu) SSE-Verbindung zu GET /subscribe, Event-Parser, Debounce, Reconnect-Backoff, Lifecycle-Anbindung.
Cloud-Erweiterung melo_cloud_service.dart (erweitert) Playlisten-CRUD, Favoriten-Toggle, SSE-Stream öffnen.
Sync-Erweiterung sync_service.dart (erweitert) Zwei neue Phasen (Favoriten-Union, Playlist-Union), ladeAusgewaehlteHoch, Drossel-Umgehung für SSE.
UI bestehende Screens Auswahl-Modus-Aktion, Einzel-Song-Offline-Knopf, Bericht-Dialog.

Neue Abhängigkeit: flutter_local_notifications (die einzige neue Dependency dieser Spec).

Feature-Details

1. Favoriten-Merge (Datenverlust-Fix)

Heutiger Bug: _gleicheFavoritenAb() (sync_service.dart) POSTet die lokale Favoritenliste als Komplett-Ersatz — ein frisch installiertes Gerät löscht damit beim ersten Sync alle Server-Favoriten.

Neu, zwei Mechanismen (Vorbild Melo v2):

  • Sofort-Push beim Antippen: PlaylistService.toggleFavorite schickt online zusätzlich fire-and-forget ein deterministisches set:true/false an den Server (Endpunkt POST /favorites/toggle mit set-Parameter existiert in melo_cloud.py). Deterministisch statt Toggle, damit ein abweichender Server-Zustand den Wunsch nie invertiert. Nur für Songs mit cloudId; Fehler werden still geschluckt (der nächste Voll-Sync korrigiert).
  • Vereinigung beim Sync: neue Phase in synchronisiere():
    1. GET /favorites (der bereits implementierte, bisher ungenutzte MeloCloudService.favoriten()).
    2. favoritenVereinigung(lokal, server) (sync_merge.dart): Union über cloudIds.
    3. Ergebnis als POST /favorites zum Server UND lokal übernehmen (fehlende Favoriten lokal setzen).
  • Sicherheitsregel (hart): Schlägt das GET fehl, wird die gesamte Phase übersprungen — es wird NIE ohne vorheriges erfolgreiches GET gePOSTet. Genau das ist der heutige Bug; er darf durch keinen Fehlerpfad wieder entstehen.
  • Songs ohne cloudId: bleiben lokal-only, tauchen in keiner Richtung im Abgleich auf.
  • Semantik der Union: Ent-Favorisierungen propagieren über den Sofort-Push (online) — die Sync-Union gleicht nur Hinzufügungen ab. Offline entfernte Herzen kommen beim nächsten Sync zurück, wenn kein Online-Push sie vorher gemeldet hat. Das ist dieselbe bewusste v2-Semantik (kein Datenverlust > perfekte Lösch-Propagation).

2. Playlist-Sync

  • Drift-Migration: Tabelle Playlists bekommt cloudId TEXT NULL. (Schema-Version erhöhen, Migration schreiben + testen.)
  • Zuordnung: Playlisten mit cloudId sind eindeutig verbunden. Ohne cloudId: einmalige Zuordnung per Namensvergleich (case-insensitive, getrimmt); Treffer bekommt die Server-cloudId persistiert. Lokale Playlist ohne Server-Gegenstück → auf dem Server anlegen (POST /playlists), cloudId übernehmen. Server-Playlist ohne lokales Gegenstück → lokal anlegen.
  • Song-Vereinigung: playlistVereinigung(lokal, server) — Union der Songs per Song-cloudId. Reihenfolge: der längere Stand liefert die Grundreihenfolge, nur auf der jeweils anderen Seite vorhandene Songs werden hinten angehängt. Ergebnis geht an Server (Playlist-Songs-Endpunkt) und in die lokale DB.
  • Sofort-Push online: Playlist anlegen/umbenennen/löschen sowie Song hinzufügen/entfernen werden bei bestehender Verbindung fire-and-forget direkt zum Server durchgereicht (Endpunkte existieren: Playlists-CRUD + songs + positions).
  • Songs ohne cloudId in einer Playlist: bleiben lokal in der Playlist, werden zum Server einfach nicht mitgemeldet — kein Fehler.
  • Bekannte Einschränkung (dokumentiert, akzeptiert): Der Server hat keine Playlist-Tombstones (Löschen = hartes DELETE). Eine OFFLINE gelöschte Playlist kommt beim nächsten Sync vom Server zurück. Online-Löschungen greifen sofort und dauerhaft. Falls das in der Praxis stört: Server-Tombstones als separater Auftrag an Hermes/claude-server.

3. Upload-Auswahl („Auf den Server laden“)

  • Auswahl-Modus in „Meine Musik“ (existiert, siehe auswahl_modus_test.dart) bekommt die Aktion „Auf den Server laden“.
  • Neue Methode SyncService.ladeAusgewaehlteHoch(List<Song> songs): nutzt den bestehenden _ladeHoch-Pfad pro Song, mit Fortschritt über die bestehenden Felder (laeuft/erledigt/gesamt/status) und der neuen Notification.
  • Songs, die schon eine cloudId haben, werden übersprungen; zu große Dateien (>50 MB) einzeln als Fehler vermerkt, der Rest läuft weiter. Ergebnis-Meldung im Stil „3 hochgeladen, 2 waren schon da, 1 zu groß“.
  • Läuft bereits ein Sync, wird die Aktion abgewiesen („Sync läuft gerade“) — der bestehende Doppel-Lauf-Schutz des SyncService gilt.
  • Der automatische Voll-Upload beim Sync (alles ohne cloudId) bleibt unverändert bestehen.

4. Einzel-Song-Offline

  • DownloadService bekommt ladeEinzelnenTitel(SubsonicSong song) — der interne Pro-Song-Lade-Loop existiert bereits (Album-Pfad), wird nur als Einzel-API zugänglich. Gleiche Ablage (App-Speicher, Application-Support/melo_downloads), gleiche Buchführung (Downloads-Tabelle per navidromeId), gleiche Fehlerbehandlung.
  • UI: Songzeilen in der Server-Album-/Künstler-Ansicht (server_titel_screen.dart) bekommen den Lade-Knopf, den es heute nur pro Album gibt (Muster _LadeKnopf), inklusive Zustand „schon offline“ mit Entfernen-Option (bestehendes DownloadService.entferne).

5. Fortschritts-Notification + Sync-Bericht

  • SyncBenachrichtigung (Wrapper um flutter_local_notifications, eigener Channel z. B. de.baka.melo.sync):
    • Während SyncService.laeuft: persistente (ongoing) Notification „Synchronisiere… X/Y“ mit Fortschrittsbalken, aktualisiert über die bestehenden ChangeNotifier-Felder.
    • Bei Abschluss: kurze Erfolgs-Notification (bzw. Fehlertext), nicht persistent, tippbar → App öffnen.
    • Benachrichtigungs-Berechtigung wird seit dem Berechtigungs-Feature beim App-Start angefragt; verweigert → stiller Verzicht, die In-App-Anzeige in den Einstellungen bleibt wie heute.
    • Die ENTSCHEIDUNGEN (wann zeigen, wann aktualisieren, wann Bericht fällig) liegen als reine Funktionen in derselben Datei und sind ohne Plattform-Kanäle testbar; der Plugin-Aufruf selbst bleibt dünn.
  • Sync-Bericht („Was ist neu“): Ist der letzte erfolgreiche Sync

    24 h her, sammelt der nächste Sync Zähler (neue Songs, gelöschte, Favoriten geändert, Playlisten geändert) und zeigt danach einmalig einen Dialog (v2-Parität „Willkommen zurück!“). Stand in SharedPreferences.

6. Echtzeit per SSE

  • EchtzeitSync: öffnet im Vordergrund GET /subscribe (Bearer-Header; http-Paket, client.send() → StreamedResponse, zeilenweises SSE-Parsing — keine neue Dependency). Server schickt Heartbeat alle 30 s.
  • Events (song_delete, song_update, song_favorite + das neue Upload-Event, s. u.): 3 s Debounce (Bursts bündeln), dann SyncService.synchronisiere() mit neuem Parameter erzwinge: true, der die 15-Minuten-Drossel (sollAutoSync) umgeht. Der Event-INHALT wird bewusst nicht einzeln angewendet — ein angestoßener Voll-Sync ist robuster als Event-Replays (Events sind serverseitig lückenhaft, siehe Bestandsaufnahme).
  • Lifecycle: verbinden bei resumed, trennen bei paused (kein Hintergrund-Socket, kein Akku-Fresser).
  • Verbindungsabriss → Reconnect mit exponentiellem Backoff (Start 5 s, Deckel 5 min). SSE komplett tot → App verhält sich exakt wie heute (Poll bei Start/Resume).
  • Server-Auftrag (separat, an Hermes/claude-server — nicht Teil des App-Plans): melo_cloud.py feuert beim Upload bisher KEIN SSE-Event (_emit_event fehlt in upload()); ein song_upload-Event ergänzen. Die App funktioniert auch ohne (Poll-Fallback), aber „neuer Song erscheint in Sekunden auf dem anderen Gerät“ braucht es.

Sync-Phasen nach Ausbau (Reihenfolge)

  1. Tombstones nachziehen (bestehend)
  2. Neue Server-Songs herunterladen (bestehend)
  3. Lokale Songs ohne cloudId hochladen (bestehend)
  4. Favoriten-Vereinigung (neu)
  5. Playlist-Vereinigung (neu)
  6. Verlauf melden (bestehend)
  7. Zeitstempel + ggf. Bericht (erweitert)

Fehlerfälle

  • Favoriten-GET scheitert → Phase 4 komplett überspringen (nie blind POSTen), Sync läuft weiter.
  • Playlist-Endpunkt scheitert → Phase 5 überspringen, Sync läuft weiter.
  • Auswahl-Upload: Datei >50 MB oder Einzel-Upload-Fehler → im Ergebnis vermerken, mit nächstem Song fortfahren.
  • Einzel-Song-Offline: wie bestehender Album-Pfad (Fehler pro Titel, kein Abbruch des Rests).
  • SSE nicht erreichbar/abgerissen → Backoff-Reconnect, still; kein Nutzer-Fehler, Poll bleibt.
  • Notification-Berechtigung verweigert → In-App-Fortschritt wie heute.
  • Sync bereits aktiv → Auswahl-Upload/SSE-Anstoß werden abgewiesen bzw. verzögert (bestehender Schutz).

Risiken (aus der Bestandsaufnahme, im Plan zu beachten)

  • Die Lösch-Bremse bremst nur Server-Löschungen; lokale Tombstones laufen ungebremst — beim Ausbau nicht verschlimmern.
  • Uploads/Downloads liegen serverseitig komplett im RAM — der Auswahl-Upload bleibt sequenziell (kein Parallel-Upload), um den Server nicht aufzublähen.
  • v2-Lektionen übernehmen: Sync-Zeitstempel = Snapshot VOR dem Listen (Tombstone-Race), neue Server-Songs sofort MIT cloudId in die DB (Doppel-Download-Falle), Dateinamen-Kollisionszähler.
  • POST /favorites bleibt technisch ein Voll-Ersatz — die Sicherheit liegt allein in der GET-vor-POST-Regel. Tests müssen genau diesen Pfad absichern.

Tests

  • sync_merge.dart: Unit-Tests für Favoriten-Union (leer×leer, einseitig, disjunkt, Songs ohne cloudId), Playlist-Union (Reihenfolge-Regel, Erst-Zuordnung per Name, Groß/Kleinschreibung, Namens-Kollision), deterministisch, ohne I/O.
  • echtzeit_sync.dart: SSE-Zeilen-Parser (event/data/Heartbeat/ Fragmentierung), Debounce- und Backoff-Entscheidungen als reine Funktionen.
  • sync_service.dart: neue Phasen mit MockClient (inkl. GET-Fehler → kein POST; Auswahl-Upload mit Mischung aus ok/zu groß/schon da).
  • sync_benachrichtigung.dart: Entscheidungslogik (zeigen/aktualisieren/ Bericht fällig) pur; Plugin-Aufrufe nicht getestet (dünner Wrapper).
  • Widget-Tests: Auswahl-Modus-Aktion sichtbar + ruft Upload auf; Einzel-Song-Knopf lädt/entfernt; Bericht-Dialog erscheint nach

    24h-Marke.

  • Drift-Migration: Test, dass Bestandsdaten die neue Spalte überleben.

Offene Abhängigkeiten

  1. Server: song_upload-SSE-Event in melo_cloud.py (Hermes / claude-server) — App funktioniert ohne, Echtzeit für neue Songs braucht es.
  2. Optional/nachrangig (nur falls Praxisproblem): Playlist-Tombstones serverseitig.