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

281 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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<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.