Files
Melo/lib/services/download_speicher.dart
T
Hermes (Server)andClaude Opus 5 b42e045e67 Alben und Kuenstler offline mitnehmen; Zwischenspeicher bekommt eine Grenze
Bisher landete Musik nur zufaellig auf dem Geraet: was man abspielte, blieb
nebenbei liegen. Gezielt "dieses Album fuer die Zugfahrt" gab es nicht. Und
dieser Zwischenspeicher wuchs unbegrenzt — es gab nur "alles loeschen".

Zwei getrennte Ablagen. Bewusste Downloads liegen NICHT im Cache-Ordner:
den darf Android bei Speichernot jederzeit selbst leeren, und genau das
darf einem mitgenommenen Album nicht passieren. Sie liegen deshalb neben
der Datenbank im Support-Ordner. Die Grenze gilt nur fuer den
Zufalls-Cache; Downloads sind unbegrenzt und werden nie verdraengt.

- Download-Pfeil je Album und Kuenstler, mit Fortschritt und Abbruch
- Rueckfrage ab 30 Titeln samt Groessenschaetzung
- Obergrenze wahlbar (aus / 512 MB / 1 / 2 / 4 / 8 GB), Standard 2 GB;
  am laengsten nicht Gehoertes weicht zuerst
- Liste "Heruntergeladen" mit Titel, Kuenstler, Groesse, einzeln oder alle
  entfernbar (Schema 10)
- Wiedergabe nimmt erst den Download, dann den Zwischenspeicher

Aus dem Code-Review nachgebessert, vier ernste Punkte: "aus" (0 MB)
schaltete das Aufraeumen ab statt das Zwischenspeichern und liess den
Cache unbegrenzt wachsen; wer sich erst waehrend der Sitzung am Server
anmeldete, konnte gar nichts laden; die Verdraengung konnte Dateien
loeschen, die die laufende Warteschlange schon als Quelle eingetragen
hatte (Abbruch mitten im Album, ohne Rueckfall aufs Streamen); und ein
heruntergeladener Titel wurde beim Abspielen ein zweites Mal in den Cache
geholt. Dazu sechs kleinere und der "zuletzt gehoert"-Stempel, der beim
Aufbau der Warteschlange statt beim Abspielen gesetzt wurde.

Nebenbei aufgeraeumt, weil die Dateien ohnehin offen waren: keine Emoji
als Knoepfe mehr, kein Colors.redAccent und kein grey.shade mehr im
ganzen Projekt, und die Einstellungen heissen nicht mehr "Settings".

433 Tests gruen (vorher 411), flutter analyze ohne Befund, Release-APK
gebaut.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013xAHJTJM6UUqmjUgk1PUEd
2026-08-23 21:31:53 +02:00

69 lines
2.4 KiB
Dart

import 'dart:io';
import 'package:crypto/crypto.dart';
import 'package:flutter/foundation.dart';
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';
/// Ablage für bewusst heruntergeladene Titel.
///
/// Bewusst **nicht** im Cache-Ordner: den darf Android bei Speichernot ohne
/// Vorwarnung leeren — das ist sein Zweck. Einem Album, das jemand für die
/// Zugfahrt mitgenommen hat, darf genau das nicht passieren. Deshalb liegt
/// es neben der Datenbank im Support-Ordner, den das System in Ruhe lässt.
///
/// Die Trennung ist auch die Grundlage der Speicher-Rechnung: die Grenze für
/// den Zwischenspeicher gilt nur dort, Downloads sind unbegrenzt.
class DownloadSpeicher {
static const ordnerName = 'melo_downloads';
Directory? _ordner;
Future<Directory> _ordnerHolen() async {
final vorhanden = _ordner;
if (vorhanden != null) return vorhanden;
final basis = await getApplicationSupportDirectory();
final ordner = Directory(p.join(basis.path, ordnerName));
if (!await ordner.exists()) await ordner.create(recursive: true);
_ordner = ordner;
return ordner;
}
/// Dateiname eines Titels. Wie im Zwischenspeicher über die Server-ID und
/// nicht über die Stream-Adresse: die trägt bei jedem App-Start ein neues
/// Token und wäre als Schlüssel wertlos.
static String dateiName(String navidromeId) =>
'${md5.convert('navidrome:$navidromeId'.codeUnits)}.mp3';
/// Wohin der Titel geschrieben wird — die Datei muss es noch nicht geben.
Future<File> ziel(String navidromeId) async =>
File(p.join((await _ordnerHolen()).path, dateiName(navidromeId)));
/// Die fertige Datei, oder `null`, wenn der Titel nicht heruntergeladen ist.
Future<File?> treffer(String navidromeId) async {
final datei = await ziel(navidromeId);
return await datei.exists() ? datei : null;
}
Future<void> loesche(String navidromeId) async {
try {
final datei = await ziel(navidromeId);
if (await datei.exists()) await datei.delete();
} catch (e) {
debugPrint('Download nicht löschbar: $e');
}
}
Future<void> loescheAlle() async {
try {
final ordner = await _ordnerHolen();
if (await ordner.exists()) await ordner.delete(recursive: true);
_ordner = null;
await _ordnerHolen();
} catch (e) {
debugPrint('Downloads nicht löschbar: $e');
}
}
}