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
@@ -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