mirror of
https://github.com/austinried/subtracks.git
synced 2026-11-10 23:21:59 +01:00
- SourceRepository.coverArt and networkCoverArt read the source id and the source object separately, so a source switch between the two could pair one source's cache key with another source's URL; for the store that meant writing the wrong server's artwork into the per-source file for good. Both now resolve from a single active snapshot. - ArtworkStore wrote through a fixed `<name>.part`, so two downloads of songs from one album could interleave: one publishing the file while the other still had the shared inode open, which leaves a torn image for Coil to decode. Each write gets its own partial now. - OkHttpArtworkFetcher read the whole body into memory whatever its size. It refuses anything past 8 MB, checked by declared length and again while reading. - Artwork whose library rows could not all be resolved is no longer swept: pruning an album while its songs remain used to delete art a download still held. A source with no download rows still sweeps everything. - fetchArtworkSafely and storeArtwork no longer swallow CancellationException, and the poll loop fetches a resumed row's artwork off the tick instead of holding up the next one. - Reconcile reads the download rows once rather than twice, and download() no longer repeats the check enqueueIfAbsent makes under the lock.
63 lines
1.9 KiB
Kotlin
63 lines
1.9 KiB
Kotlin
package com.subtracks.data.model
|
|
|
|
import androidx.room3.ColumnInfo
|
|
import androidx.room3.ColumnTypeConverter
|
|
import androidx.room3.Entity
|
|
import androidx.room3.ForeignKey
|
|
import androidx.room3.Index
|
|
|
|
enum class DownloadStatus { Queued, Running, Completed, Failed }
|
|
|
|
@Entity(
|
|
tableName = "song_downloads",
|
|
primaryKeys = ["sourceId", "songId"],
|
|
foreignKeys = [
|
|
ForeignKey(
|
|
entity = Source::class,
|
|
parentColumns = ["id"],
|
|
childColumns = ["sourceId"],
|
|
onDelete = ForeignKey.CASCADE,
|
|
),
|
|
ForeignKey(
|
|
entity = Song::class,
|
|
parentColumns = ["sourceId", "id"],
|
|
childColumns = ["sourceId", "songId"],
|
|
onDelete = ForeignKey.CASCADE,
|
|
),
|
|
],
|
|
indices = [Index("sourceId"), Index("engineId")],
|
|
)
|
|
data class SongDownload(
|
|
val sourceId: Long,
|
|
val songId: String,
|
|
val status: DownloadStatus,
|
|
val engineId: Long? = null,
|
|
@ColumnInfo(defaultValue = "0") val bytes: Long = 0,
|
|
@ColumnInfo(defaultValue = "0") val total: Long = 0,
|
|
val error: String? = null,
|
|
) {
|
|
val progress: Float?
|
|
get() = total.takeIf { it > 0 }?.let { (bytes.toFloat() / it).coerceIn(0f, 1f) }
|
|
}
|
|
|
|
data class DownloadArtwork(
|
|
val sourceId: Long,
|
|
val albumId: String?,
|
|
val artistId: String?,
|
|
val albumRow: String?,
|
|
val artistRow: String?,
|
|
val albumCoverArt: String?,
|
|
val artistCoverArt: String?,
|
|
) {
|
|
val hasMissingLibraryRow: Boolean
|
|
get() = (albumId != null && albumRow == null) || (artistId != null && artistRow == null)
|
|
}
|
|
|
|
class DownloadStatusConverter {
|
|
@ColumnTypeConverter
|
|
fun fromDownloadStatus(status: DownloadStatus): String = status.name
|
|
|
|
@ColumnTypeConverter
|
|
fun toDownloadStatus(value: String): DownloadStatus = DownloadStatus.entries.firstOrNull { it.name == value } ?: DownloadStatus.Failed
|
|
}
|