mirror of
https://github.com/austinried/subtracks.git
synced 2026-11-11 07:29:14 +01:00
persist artwork seeds in the database
This commit is contained in:
@@ -4,7 +4,9 @@ import android.app.Application
|
||||
import coil3.ImageLoader
|
||||
import coil3.SingletonImageLoader
|
||||
import coil3.network.okhttp.OkHttpNetworkFetcherFactory
|
||||
import com.subtracks.data.repo.ArtworkSeedStore
|
||||
import com.subtracks.di.appModule
|
||||
import com.subtracks.ui.theme.ArtworkSeedCache
|
||||
import okhttp3.OkHttpClient
|
||||
import org.koin.core.context.startKoin
|
||||
import org.koin.core.context.stopKoin
|
||||
@@ -15,9 +17,11 @@ class SubtracksApp : Application() {
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
stopKoin()
|
||||
startKoin {
|
||||
modules(appModule(this@SubtracksApp, http))
|
||||
}
|
||||
val koin =
|
||||
startKoin {
|
||||
modules(appModule(this@SubtracksApp, http))
|
||||
}.koin
|
||||
ArtworkSeedCache.install(koin.get<ArtworkSeedStore>())
|
||||
SingletonImageLoader.setSafe { context ->
|
||||
ImageLoader
|
||||
.Builder(context)
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.subtracks.data.db
|
||||
|
||||
import androidx.room3.Dao
|
||||
import androidx.room3.Query
|
||||
import androidx.room3.Upsert
|
||||
import com.subtracks.data.model.ArtworkSeed
|
||||
|
||||
@Dao
|
||||
interface ArtworkSeedDao {
|
||||
@Query("SELECT * FROM artwork_seeds WHERE cacheKey = :cacheKey")
|
||||
suspend fun seed(cacheKey: String): ArtworkSeed?
|
||||
|
||||
@Upsert
|
||||
suspend fun upsert(seed: ArtworkSeed)
|
||||
|
||||
@Query("DELETE FROM artwork_seeds WHERE updatedAt < :cutoff")
|
||||
suspend fun pruneExpired(cutoff: Long)
|
||||
}
|
||||
@@ -34,4 +34,16 @@ val MIGRATION_2_3 =
|
||||
}
|
||||
}
|
||||
|
||||
val MIGRATIONS: Array<Migration> = arrayOf(MIGRATION_1_2, MIGRATION_2_3)
|
||||
val MIGRATION_3_4 =
|
||||
object : Migration(3, 4) {
|
||||
override suspend fun migrate(connection: SQLiteConnection) {
|
||||
connection.execSQL(
|
||||
"CREATE TABLE IF NOT EXISTS `artwork_seeds` (`cacheKey` TEXT NOT NULL, " +
|
||||
"`primary` INTEGER NOT NULL, `secondary` INTEGER, `updatedAt` INTEGER NOT NULL, " +
|
||||
"PRIMARY KEY(`cacheKey`))",
|
||||
)
|
||||
connection.execSQL("CREATE INDEX IF NOT EXISTS `index_artwork_seeds_updatedAt` ON `artwork_seeds` (`updatedAt`)")
|
||||
}
|
||||
}
|
||||
|
||||
val MIGRATIONS: Array<Migration> = arrayOf(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4)
|
||||
|
||||
@@ -5,6 +5,7 @@ import androidx.room3.Database
|
||||
import androidx.room3.RoomDatabase
|
||||
import com.subtracks.data.model.Album
|
||||
import com.subtracks.data.model.Artist
|
||||
import com.subtracks.data.model.ArtworkSeed
|
||||
import com.subtracks.data.model.PlaybackCursor
|
||||
import com.subtracks.data.model.Playlist
|
||||
import com.subtracks.data.model.PlaylistSong
|
||||
@@ -27,8 +28,9 @@ import com.subtracks.data.model.SubsonicSource
|
||||
SearchIndex::class,
|
||||
QueueEntry::class,
|
||||
PlaybackCursor::class,
|
||||
ArtworkSeed::class,
|
||||
],
|
||||
version = 3,
|
||||
version = 4,
|
||||
exportSchema = true,
|
||||
)
|
||||
@ColumnTypeConverters(QueueKindConverter::class)
|
||||
@@ -40,4 +42,6 @@ abstract class SubtracksDatabase : RoomDatabase() {
|
||||
abstract fun searchDao(): SearchDao
|
||||
|
||||
abstract fun queueDao(): QueueDao
|
||||
|
||||
abstract fun artworkSeedDao(): ArtworkSeedDao
|
||||
}
|
||||
|
||||
@@ -209,6 +209,14 @@ data class PlaybackCursor(
|
||||
val queuePosition: Long,
|
||||
)
|
||||
|
||||
@Entity(tableName = "artwork_seeds", indices = [Index("updatedAt")])
|
||||
data class ArtworkSeed(
|
||||
@PrimaryKey val cacheKey: String,
|
||||
val primary: Int,
|
||||
val secondary: Int?,
|
||||
val updatedAt: Long,
|
||||
)
|
||||
|
||||
class QueueKindConverter {
|
||||
@ColumnTypeConverter
|
||||
fun fromQueueKind(kind: QueueKind): String = kind.name
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.subtracks.data.repo
|
||||
|
||||
import com.subtracks.data.db.SubtracksDatabase
|
||||
import com.subtracks.data.model.ArtworkSeed
|
||||
|
||||
interface ArtworkSeedStore {
|
||||
suspend fun seed(cacheKey: String): Pair<Int, Int?>?
|
||||
|
||||
suspend fun save(
|
||||
cacheKey: String,
|
||||
seeds: Pair<Int, Int?>,
|
||||
)
|
||||
}
|
||||
|
||||
class ArtworkSeedRepository(
|
||||
private val db: SubtracksDatabase,
|
||||
) : ArtworkSeedStore {
|
||||
override suspend fun seed(cacheKey: String): Pair<Int, Int?>? {
|
||||
val row = db.artworkSeedDao().seed(cacheKey) ?: return null
|
||||
return row.primary to row.secondary
|
||||
}
|
||||
|
||||
override suspend fun save(
|
||||
cacheKey: String,
|
||||
seeds: Pair<Int, Int?>,
|
||||
) {
|
||||
val dao = db.artworkSeedDao()
|
||||
val now = System.currentTimeMillis()
|
||||
dao.upsert(ArtworkSeed(cacheKey, seeds.first, seeds.second, now))
|
||||
dao.pruneExpired(now - MAX_AGE_MS)
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val MAX_AGE_MS = 30L * 24 * 60 * 60 * 1000
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,8 @@ package com.subtracks.di
|
||||
import android.content.Context
|
||||
import com.subtracks.data.db.createAndroidDatabase
|
||||
import com.subtracks.data.prefs.createUserPreferences
|
||||
import com.subtracks.data.repo.ArtworkSeedRepository
|
||||
import com.subtracks.data.repo.ArtworkSeedStore
|
||||
import com.subtracks.data.repo.LibraryRepository
|
||||
import com.subtracks.data.repo.QueueRepository
|
||||
import com.subtracks.data.repo.SourceRepository
|
||||
@@ -31,6 +33,7 @@ fun appModule(
|
||||
single { createUserPreferences(context) }
|
||||
single { SourceRepository(get(), get(), get()) }
|
||||
single { LibraryRepository(get(), get()) }
|
||||
single<ArtworkSeedStore> { ArtworkSeedRepository(get()) }
|
||||
single { SyncManager(get()) }
|
||||
single { QueueRepository(get()) }
|
||||
single<PlayerConnection> { MediaSessionConnection(context.applicationContext, get()) }
|
||||
|
||||
@@ -16,6 +16,7 @@ import coil3.request.SuccessResult
|
||||
import coil3.request.allowHardware
|
||||
import coil3.toBitmap
|
||||
import com.subtracks.data.model.CoverArtRef
|
||||
import com.subtracks.data.repo.ArtworkSeedStore
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Deferred
|
||||
@@ -36,15 +37,21 @@ object ArtworkSeedCache {
|
||||
private val jobs = ConcurrentHashMap<String, Deferred<Pair<Int, Int?>?>>()
|
||||
private val cache = LruCache<String, Pair<Int, Int?>>(MAX_CACHED_SEEDS)
|
||||
|
||||
@Volatile
|
||||
private var store: ArtworkSeedStore? = null
|
||||
|
||||
fun install(store: ArtworkSeedStore) {
|
||||
this.store = store
|
||||
}
|
||||
|
||||
suspend fun seeds(
|
||||
context: Context,
|
||||
ref: CoverArtRef,
|
||||
): Pair<Int, Int?>? {
|
||||
cache.get(ref.cacheKey)?.let { return it }
|
||||
val job = jobs.computeIfAbsent(ref.cacheKey) { scope.async { extract(context, ref) } }
|
||||
val job = jobs.computeIfAbsent(ref.cacheKey) { scope.async { resolve(context, ref) } }
|
||||
val seeds = job.await()
|
||||
jobs.remove(ref.cacheKey, job)
|
||||
if (seeds != null) cache.put(ref.cacheKey, seeds)
|
||||
return seeds
|
||||
}
|
||||
|
||||
@@ -61,6 +68,29 @@ object ArtworkSeedCache {
|
||||
cache.evictAll()
|
||||
}
|
||||
|
||||
private suspend fun resolve(
|
||||
context: Context,
|
||||
ref: CoverArtRef,
|
||||
): Pair<Int, Int?>? {
|
||||
ignoreFailure { store?.seed(ref.cacheKey) }?.let {
|
||||
cache.put(ref.cacheKey, it)
|
||||
return it
|
||||
}
|
||||
val seeds = extract(context, ref) ?: return null
|
||||
cache.put(ref.cacheKey, seeds)
|
||||
ignoreFailure { store?.save(ref.cacheKey, seeds) }
|
||||
return seeds
|
||||
}
|
||||
|
||||
private suspend fun <T> ignoreFailure(block: suspend () -> T): T? =
|
||||
try {
|
||||
block()
|
||||
} catch (e: CancellationException) {
|
||||
throw e
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
}
|
||||
|
||||
private suspend fun extract(
|
||||
context: Context,
|
||||
ref: CoverArtRef,
|
||||
|
||||
Reference in New Issue
Block a user