Serverseitig: Uploads landen in der Navidrome-Bibliothek
Auf Wunsch: was vom Handy hochgeladen wird, soll auch in Navidrome auftauchen. Die Subsonic-API kennt keinen Upload — also erledigt es die Melo-Cloud selbst. Server (/home/dustin/scripts/melo_cloud.py, nicht versioniert — Backup als melo_cloud.py.bak-20260821-084506): - verknuepfe_navidrome(): Hardlink der Registry-Datei nach /home/dustin/navidrome/music (Bind-Mount des Containers). Gleiche Partition -> kein zusaetzlicher Speicher. Fallback: Kopie. - rescan_navidrome(): Subsonic startScan.view ueber Navidromes AuthProxyHeader (TrustedSources 127.0.0.1) — kein Passwort im Code. Nur zur Beschleunigung, der Watcher findet neue Dateien ohnehin. - upload() verknuepft neue UND bereits bekannte Titel (heilt Bestand). - handle_delete() entfernt die Datei aus Registry + Navidrome, sobald kein Konto sie mehr aktiv hat. Der Registry-EINTRAG bleibt stehen: handle_list haengt die Grabsteine daran (JOIN registry) — ohne ihn erfuehren die anderen Geraete nie von der Loeschung (Zombie-Song). - registry_pfad(): gemeinsame Dateisuche ueber alle Audio-Endungen, mit Rueckfall auf den Navidrome-Ordner. Nebenbei repariert: - Alle 325 Cloud-Dateien lagen nur noch im Navidrome-Ordner, REG war leer: jeder Download antwortete "File missing". Per Hardlink zurueckverknuepft (scripts/melo_cloud_migriere_registry.py). - upload() legte jede Datei als ".mp3" ab, auch m4a/flac. Jetzt echte Endung; _download liefert passenden Content-Type und einen Dateinamen ohne doppelte Endung. App: - MeloCloudService.herunterladen() gibt die geschriebene Datei zurueck und leitet die Endung aus dem Content-Type ab (endungFuer) — sonst landet eine M4A als .mp3 auf dem Handy und Android ordnet sie falsch ein. - loeschBremseGreift(): der Abgleich reicht keine Loeschwelle mehr zum Server durch (>10 Titel UND >1/3 des Serverbestands). Ohne die Bremse haette eine nicht eingehaengte Speicherkarte die Sammlung auf allen Geraeten geloescht. 276 Tests gruen (1 uebersprungen), flutter analyze ohne Befund, plus ein Durchlauf gegen den echten Server (scripts/test_melo_cloud_navidrome.py): Upload -> Registry + Navidrome (ein Hardlink) -> Navidrome liest ein -> Loeschen entfernt beides, Grabstein bleibt. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FpPu4nuKjKKeX1RpdDeX81
This commit is contained in:
co-authored by
Claude Opus 5
parent
9fa027fca1
commit
cd79213bb3
@@ -102,6 +102,16 @@ SyncPlan planeSync({
|
||||
);
|
||||
}
|
||||
|
||||
/// Ob eine Löschwelle gestoppt wird, statt sie zum Server durchzureichen.
|
||||
///
|
||||
/// Hintergrund: Der Bibliotheks-Scan markiert jeden Titel als gelöscht, dessen
|
||||
/// Datei er nicht mehr findet. Ist die Speicherkarte einmal nicht eingehängt
|
||||
/// oder die Berechtigung entzogen, betrifft das auf einen Schlag die halbe
|
||||
/// Sammlung — ohne Bremse würde der Abgleich sie am Server und damit auf allen
|
||||
/// Geräten löschen. Einzelne Löschungen laufen weiterhin ungehindert durch.
|
||||
bool loeschBremseGreift({required int zuLoeschen, required int bestand}) =>
|
||||
zuLoeschen > 10 && zuLoeschen * 3 > bestand;
|
||||
|
||||
/// Wie oft höchstens automatisch abgeglichen wird. Ein Abgleich beim
|
||||
/// Zurückkehren in die App darf nicht bei jedem Tab-Wechsel losrennen.
|
||||
const autoSyncAbstand = Duration(minutes: 15);
|
||||
@@ -176,15 +186,28 @@ class SyncService extends ChangeNotifier {
|
||||
_melde('Vergleiche mit dem Server …');
|
||||
|
||||
try {
|
||||
final amServer = await cloud.liste();
|
||||
final plan = planeSync(
|
||||
lokal: await db.allSongs(),
|
||||
server: await cloud.liste(),
|
||||
server: amServer,
|
||||
);
|
||||
_gesamt = plan.gesamt;
|
||||
notifyListeners();
|
||||
|
||||
final bestand = amServer.where((s) => !s.geloescht).length;
|
||||
final bremse = loeschBremseGreift(
|
||||
zuLoeschen: plan.serverLoeschen.length,
|
||||
bestand: bestand,
|
||||
);
|
||||
if (bremse) {
|
||||
_fehler = 'Sicherheitsbremse: ${plan.serverLoeschen.length} von '
|
||||
'$bestand Titeln würden am Server gelöscht. Das sieht nach einem '
|
||||
'Fehler aus (z. B. Speicherkarte nicht eingehängt) — es wurde '
|
||||
'nichts gelöscht.';
|
||||
}
|
||||
|
||||
await _ziehLoeschungenNach(plan.lokalLoeschen);
|
||||
await _meldeLoeschungen(plan.serverLoeschen);
|
||||
if (!bremse) await _meldeLoeschungen(plan.serverLoeschen);
|
||||
await _ladeHerunter(plan.herunterladen);
|
||||
await _ladeHoch(plan.hochladen);
|
||||
await _gleicheFavoritenAb();
|
||||
@@ -232,11 +255,14 @@ class SyncService extends ChangeNotifier {
|
||||
final ordner = await _musikOrdner();
|
||||
for (final cloudSong in songs) {
|
||||
_melde('Lade „${cloudSong.titel}“ …');
|
||||
final datei = File(p.join(
|
||||
ordner.path,
|
||||
'${_sichererDateiname(cloudSong.titel)}-${cloudSong.id}.mp3',
|
||||
));
|
||||
if (!await cloud.herunterladen(cloudSong.id, datei)) {
|
||||
// Die Endung bestimmt der Server anhand des echten Dateityps — die
|
||||
// Bibliothek enthält nicht nur MP3.
|
||||
final datei = await cloud.herunterladen(
|
||||
cloudSong.id,
|
||||
ordner,
|
||||
'${_sichererDateiname(cloudSong.titel)}-${cloudSong.id}',
|
||||
);
|
||||
if (datei == null) {
|
||||
_erledigt++;
|
||||
continue;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user