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
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
eee94467a0
commit
3a376faf50
@@ -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<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.
|
||||
Reference in New Issue
Block a user