fix(android): Offline-Downloads vollständig löschen, Fortschritt selbst begradigen
Löschen ließ Dateien zurück: Cover wurden nie entfernt (nur die Audiodatei), unfertige Titel behielten ihre Datei, weil der Pfad aus der Karte der FERTIGEN Titel kam, und laufende Downloadarbeit lief nach dem Löschen noch minutenlang ins Leere. Jetzt kommt der Pfad aus der Datenbankzeile, .tmp wird mitgenommen, Cover gehen nur weg wenn kein anderer Titel sie noch benutzt, und die Arbeit der Gruppe wird abgebrochen. Dazu: ein zu hoch gezählter Fortschritt aus der Zeit vor 1.10 konnte sich nie mehr korrigieren — die Gruppe galt als vollständig, der Knopf war unantastbar und das Fortsetzen sucht nur Gruppen unter dem Sollwert. Beim App-Start wird der Fortschritt aller Gruppen einmal aus dem echten Zustand neu berechnet. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
ffedc7532b
commit
de674dbe3f
4 files changed
+74
-12
No files matched your search
+20
-2
@@ -77,8 +77,9 @@ Die Android-App zählt eigenständig weiter (mehrere APKs pro Release). Welche d
|
||||
|
||||
## [2.6] — 2026-09-12
|
||||
|
||||
**Android:** 1.9 (versionCode 12) → 1.11 (versionCode 14). 1.9 brachte den Besitzer geteilter
|
||||
Playlists und die Offline-Korrekturen, 1.10 den Zählfehler im Downloader, 1.11 den WLAN-Schalter.
|
||||
**Android:** 1.9 (versionCode 12) → 1.12 (versionCode 15). 1.9 brachte den Besitzer geteilter
|
||||
Playlists und die Offline-Korrekturen, 1.10 den Zählfehler im Downloader, 1.11 den WLAN-Schalter,
|
||||
1.12 das vollständige Löschen.
|
||||
|
||||
### Fixed
|
||||
- **Zurück und Vorwärts gingen im installierten Fenster am PC gar nicht (Web)** — im Browser-Tab
|
||||
@@ -110,6 +111,23 @@ Playlists und die Offline-Korrekturen, 1.10 den Zählfehler im Downloader, 1.11
|
||||
toter Zurück-Druck gleich zu Beginn. Vorwärts funktioniert unverändert (mitgetestet).
|
||||
|
||||
### Fixed
|
||||
- **Offline-Downloads löschen ließ Dateien zurück (Android)** — „komplett löschen und neu laden" ist
|
||||
der naheliegende Ausweg, wenn eine Gruppe durcheinandergeraten ist; dafür muss Löschen aber wirklich
|
||||
alles erwischen. Es fehlte an drei Stellen:
|
||||
- **Cover blieben liegen.** Entfernt wurde nur die Audiodatei. Bei einer Playlist über viele Alben
|
||||
sammeln sich so hunderte JPEGs an. Jetzt werden sie mitgelöscht — aber nur, wenn kein anderer
|
||||
Titel dasselbe Cover mehr benutzt, denn Cover werden geteilt gespeichert.
|
||||
- **Unfertige Titel behielten ihre Datei.** Der Pfad kam aus der Karte der *fertigen* Titel; ein
|
||||
abgebrochener oder fehlgeschlagener Download tauchte darin nicht auf und blieb unberührt.
|
||||
Der Pfad kommt jetzt aus der Datenbankzeile selbst, dazu die angefangene `.tmp`.
|
||||
- **Laufende Downloadarbeit lief weiter.** Nach dem Löschen lud der Worker noch minutenlang ins
|
||||
Leere; jetzt wird die Arbeit der Gruppe abgebrochen.
|
||||
- **Ein zu hoch gezählter Fortschritt konnte sich nie mehr korrigieren (Android)** — Gruppen aus der
|
||||
Zeit vor 1.10 tragen womöglich einen Wert über `totalTracks` und gelten damit als „Vollständig":
|
||||
der Herunterladen-Knopf ist unantastbar, und das Fortsetzen beim Start sucht nur Gruppen *unter*
|
||||
dem Sollwert — die Gruppe fiel also durch jedes Raster. Beim App-Start wird der Fortschritt aller
|
||||
Gruppen jetzt einmal aus dem tatsächlichen Zustand der Titel neu berechnet (eine SQL-Anweisung),
|
||||
danach greifen Knopf und Fortsetzen wieder.
|
||||
- **Halb geladene Gruppen meldeten sich als „Vollständig" (Android)** — der Worker startete seinen
|
||||
Zähler bei `group.downloadedTracks` und zählte dieselben fertigen Titel beim Überspringen **gleich
|
||||
noch einmal**: nach einem Fortsetzen mit 412 fertigen Titeln meldete die Gruppe 824. Überschreitet
|
||||
|
||||
@@ -14,8 +14,8 @@ android {
|
||||
applicationId = "com.novasonic.app"
|
||||
minSdk = 26
|
||||
targetSdk = 35
|
||||
versionCode = 14
|
||||
versionName = "1.11"
|
||||
versionCode = 15
|
||||
versionName = "1.12"
|
||||
// Host der eigenen NovaSonic-Web-App — Ziel der geteilten Links. Einmal in
|
||||
// gradle.properties setzen (novasonicHost=music.example.com); ohne Angabe bleibt der
|
||||
// Platzhalter stehen und der App-Link greift schlicht nicht.
|
||||
|
||||
@@ -53,6 +53,22 @@ interface OfflineDao {
|
||||
@Query("SELECT * FROM download_groups WHERE groupId = :groupId")
|
||||
suspend fun getGroup(groupId: String): DownloadGroupEntity?
|
||||
|
||||
/**
|
||||
* Rechnet den gespeicherten Fortschritt ALLER Gruppen aus dem tatsaechlichen Zustand der Titel
|
||||
* neu. Noetig, weil der Worker bis 1.10 doppelt zaehlte: der Wert konnte totalTracks
|
||||
* uebersteigen, die Gruppe galt als "Vollstaendig", der Herunterladen-Knopf wurde unantastbar
|
||||
* und das Fortsetzen uebersprang sie — der falsche Wert konnte sich also nie mehr korrigieren.
|
||||
* Eine Anweisung fuer alle Gruppen, laeuft beim App-Start.
|
||||
*/
|
||||
@Query("""
|
||||
UPDATE download_groups SET downloadedTracks = (
|
||||
SELECT COUNT(*) FROM group_track_cross_refs r
|
||||
INNER JOIN downloaded_tracks t ON t.songId = r.songId
|
||||
WHERE r.groupId = download_groups.groupId AND t.state = 'DONE'
|
||||
)
|
||||
""")
|
||||
suspend fun fortschrittNeuBerechnen()
|
||||
|
||||
/** Gruppen, bei denen noch Titel fehlen — Grundlage fuers Fortsetzen beim App-Start. */
|
||||
@Query("SELECT * FROM download_groups WHERE totalTracks > 0 AND downloadedTracks < totalTracks")
|
||||
suspend fun incompleteGroups(): List<DownloadGroupEntity>
|
||||
@@ -99,6 +115,10 @@ interface OfflineDao {
|
||||
@Query("DELETE FROM group_track_cross_refs WHERE groupId = :groupId AND songId = :songId")
|
||||
suspend fun removeCrossRef(groupId: String, songId: String)
|
||||
|
||||
/** Wie viele Titel dieses Cover noch benutzen — Cover werden geteilt gespeichert. */
|
||||
@Query("SELECT COUNT(*) FROM downloaded_tracks WHERE coverArt = :coverArtId")
|
||||
suspend fun titelMitCover(coverArtId: String): Int
|
||||
|
||||
/** Find a locally-stored cover path by Navidrome coverArt ID (deduped storage). */
|
||||
@Query("SELECT coverPath FROM downloaded_tracks WHERE coverArt = :coverArtId AND state = 'DONE' AND coverPath IS NOT NULL LIMIT 1")
|
||||
suspend fun localCoverPathByCoverArt(coverArtId: String): String?
|
||||
|
||||
@@ -211,6 +211,10 @@ class OfflineRepository(context: Context) {
|
||||
*/
|
||||
fun setzeUnvollstaendigeFort() {
|
||||
scope.launch {
|
||||
// Erst den gespeicherten Fortschritt begradigen: Gruppen mit einem zu hohen Wert aus
|
||||
// der Zeit vor 1.10 sehen sonst vollstaendig aus und werden hier gar nicht gefunden.
|
||||
runCatching { dao.fortschrittNeuBerechnen() }
|
||||
.onFailure { Log.w("OfflineRepository", "Fortschritt neu berechnen fehlgeschlagen", it) }
|
||||
val offen = runCatching { dao.incompleteGroups() }.getOrDefault(emptyList())
|
||||
offen.forEach { g ->
|
||||
Log.i("OfflineRepository",
|
||||
@@ -220,27 +224,47 @@ class OfflineRepository(context: Context) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Entfernt alle Dateien eines Titels und dann die Datenbankzeile.
|
||||
*
|
||||
* Frueher lief das ueber _audioMap — die kennt aber nur FERTIGE Titel. Ein abgebrochener oder
|
||||
* fehlgeschlagener Download hinterliess seine Datei also unberuehrt. Jetzt kommt der Pfad aus
|
||||
* der Zeile selbst, dazu die angefangene .tmp und das Cover, sofern es kein anderer Titel mehr
|
||||
* benutzt (Cover werden geteilt gespeichert).
|
||||
*/
|
||||
private suspend fun loescheTitelVollstaendig(songId: String) {
|
||||
val track = dao.getTrackById(songId)
|
||||
val pfade = listOfNotNull(track?.localPath, _audioMap.value[songId])
|
||||
pfade.distinct().forEach { p ->
|
||||
runCatching { File(p).delete(); File("$p.tmp").delete() }
|
||||
}
|
||||
track?.coverArt?.let { coverArtId ->
|
||||
// Diese Zeile zaehlt noch mit, deshalb ist 1 der letzte Benutzer.
|
||||
if (dao.titelMitCover(coverArtId) <= 1) {
|
||||
runCatching { File(coversDir(), "$coverArtId.jpg").delete() }
|
||||
track.coverPath?.let { runCatching { File(it).delete() } }
|
||||
}
|
||||
}
|
||||
dao.deleteTrackById(songId)
|
||||
}
|
||||
|
||||
// ── Delete with reference-count file cleanup ──────────────────────────────
|
||||
|
||||
/** Removes a group and any tracks that are no longer referenced by another group. */
|
||||
suspend fun deleteGroup(groupId: String) {
|
||||
val songIds = dao.songIdsForGroup(groupId)
|
||||
// Laufende Downloadarbeit stoppen, sonst laedt sie noch Minuten weiter ins Leere.
|
||||
WorkManager.getInstance(appContext).cancelUniqueWork(groupId)
|
||||
dao.deleteGroup(groupId) // cascades GroupTrackCrossRef rows
|
||||
for (songId in songIds) {
|
||||
if (dao.groupCountForSong(songId) == 0) {
|
||||
_audioMap.value[songId]?.let { File(it).delete() }
|
||||
dao.deleteTrackById(songId)
|
||||
}
|
||||
if (dao.groupCountForSong(songId) == 0) loescheTitelVollstaendig(songId)
|
||||
}
|
||||
}
|
||||
|
||||
/** Removes a single track from a group, deleting the file if no other group still needs it. */
|
||||
suspend fun removeSongFromGroup(groupId: String, songId: String) {
|
||||
dao.removeCrossRef(groupId, songId)
|
||||
if (dao.groupCountForSong(songId) == 0) {
|
||||
_audioMap.value[songId]?.let { File(it).delete() }
|
||||
dao.deleteTrackById(songId)
|
||||
}
|
||||
if (dao.groupCountForSong(songId) == 0) loescheTitelVollstaendig(songId)
|
||||
recountGroup(groupId)
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user