From 3a376faf50fd45546ff02977ef3f6bc11bf7a8e1 Mon Sep 17 00:00:00 2001 From: "Hermes (Server)" Date: Thu, 27 Aug 2026 00:04:06 +0200 Subject: [PATCH] Spec: Sync-Ausbau (Auswahl-Upload, Einzel-Song-Offline, Merge-Fixes, SSE) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Claude-Session: https://claude.ai/code/session_01CcDiyJdVRqh1TtJk5JiabX --- .../specs/2026-08-27-sync-ausbau-design.md | 280 ++++++++++++++++++ 1 file changed, 280 insertions(+) create mode 100644 docs/superpowers/specs/2026-08-27-sync-ausbau-design.md diff --git a/docs/superpowers/specs/2026-08-27-sync-ausbau-design.md b/docs/superpowers/specs/2026-08-27-sync-ausbau-design.md new file mode 100644 index 0000000..ddd8c14 --- /dev/null +++ b/docs/superpowers/specs/2026-08-27-sync-ausbau-design.md @@ -0,0 +1,280 @@ +# 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.de` → `melo_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 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.