mirror of
https://github.com/austinried/subtracks.git
synced 2026-11-10 23:21:59 +01:00
library: keep artist art maintenance self-healing and cheap
Move change detection into a desired CTE with UPDATE ... FROM so mass updates do not turn the trailing WHERE into a quadratic correlated scan, and so no-op passes write zero rows. Fold the trigger optimization into the 35->36 migration and re-check/rewrite it from onOpen, so a future migration cannot silently restore the full UPDATE triggers. Re-resolve artist detail art when the active source becomes ready, and cover artist search renames in a Room-level test.
This commit is contained in:
@@ -118,6 +118,10 @@ internal object ArtistSearchTriggerOptimization : RoomDatabase.Callback() {
|
||||
override suspend fun onCreate(connection: SQLiteConnection) {
|
||||
optimizeArtistSearchTriggers(connection)
|
||||
}
|
||||
|
||||
override suspend fun onOpen(connection: SQLiteConnection) {
|
||||
if (!artistSearchTriggersOptimized(connection)) optimizeArtistSearchTriggers(connection)
|
||||
}
|
||||
}
|
||||
|
||||
internal object SqliteLifecycleLogging : RoomDatabase.Callback() {
|
||||
|
||||
@@ -225,15 +225,15 @@ internal const val ARTIST_ART_UPDATE_SQL =
|
||||
"ROW_NUMBER() OVER (PARTITION BY artistId ORDER BY coverRank) AS coverRank FROM unique_downloaded), " +
|
||||
"downloaded_covers AS (SELECT artistId, group_concat(coverArt, char(31)) AS coverArts FROM (" +
|
||||
"SELECT artistId, coverArt, coverRank FROM downloaded_deduped WHERE coverRank <= 4 ORDER BY artistId, coverRank" +
|
||||
") GROUP BY artistId) " +
|
||||
"UPDATE artists SET " +
|
||||
"coverArts = COALESCE((SELECT coverArts FROM covers WHERE covers.artistId = artists.id), ''), " +
|
||||
"downloadedCoverArts = COALESCE((SELECT coverArts FROM downloaded_covers " +
|
||||
"WHERE downloaded_covers.artistId = artists.id), '') " +
|
||||
"WHERE sourceId = :sourceId AND (" +
|
||||
"coverArts != COALESCE((SELECT coverArts FROM covers WHERE covers.artistId = artists.id), '') " +
|
||||
"OR downloadedCoverArts != COALESCE((SELECT coverArts FROM downloaded_covers " +
|
||||
"WHERE downloaded_covers.artistId = artists.id), ''))"
|
||||
") GROUP BY artistId), " +
|
||||
"desired AS (SELECT artists.rowid AS rowid, " +
|
||||
"COALESCE((SELECT coverArts FROM covers WHERE covers.artistId = artists.id), '') AS coverArts, " +
|
||||
"COALESCE((SELECT coverArts FROM downloaded_covers " +
|
||||
"WHERE downloaded_covers.artistId = artists.id), '') AS downloadedCoverArts " +
|
||||
"FROM artists WHERE artists.sourceId = :sourceId) " +
|
||||
"UPDATE artists SET coverArts = desired.coverArts, downloadedCoverArts = desired.downloadedCoverArts " +
|
||||
"FROM desired WHERE artists.rowid = desired.rowid AND " +
|
||||
"(artists.coverArts != desired.coverArts OR artists.downloadedCoverArts != desired.downloadedCoverArts)"
|
||||
|
||||
internal const val DOWNLOADED_ARTIST_ART_UPDATE_SQL =
|
||||
"WITH downloaded_albums AS (SELECT DISTINCT artistId, albumId FROM ($DOWNLOADED_COVER_ALBUM_ROWS) " +
|
||||
@@ -248,12 +248,14 @@ internal const val DOWNLOADED_ARTIST_ART_UPDATE_SQL =
|
||||
"ROW_NUMBER() OVER (PARTITION BY artistId ORDER BY coverRank) AS coverRank FROM unique_downloaded), " +
|
||||
"downloaded_covers AS (SELECT artistId, group_concat(coverArt, char(31)) AS coverArts FROM (" +
|
||||
"SELECT artistId, coverArt, coverRank FROM downloaded_deduped WHERE coverRank <= 4 ORDER BY artistId, coverRank" +
|
||||
") GROUP BY artistId) " +
|
||||
"UPDATE artists SET downloadedCoverArts = COALESCE((SELECT coverArts FROM downloaded_covers " +
|
||||
"WHERE downloaded_covers.artistId = artists.id), '') " +
|
||||
"WHERE sourceId = :sourceId AND id IN (:artistIds) AND " +
|
||||
"downloadedCoverArts != COALESCE((SELECT coverArts FROM downloaded_covers " +
|
||||
"WHERE downloaded_covers.artistId = artists.id), '')"
|
||||
") GROUP BY artistId), " +
|
||||
"desired AS (SELECT artists.rowid AS rowid, " +
|
||||
"COALESCE((SELECT coverArts FROM downloaded_covers " +
|
||||
"WHERE downloaded_covers.artistId = artists.id), '') AS downloadedCoverArts " +
|
||||
"FROM artists WHERE artists.sourceId = :sourceId AND artists.id IN (:artistIds)) " +
|
||||
"UPDATE artists SET downloadedCoverArts = desired.downloadedCoverArts " +
|
||||
"FROM desired WHERE artists.rowid = desired.rowid AND " +
|
||||
"artists.downloadedCoverArts != desired.downloadedCoverArts"
|
||||
|
||||
internal const val ARTISTS_FILTER =
|
||||
"FROM artists WHERE sourceId = :sourceId " +
|
||||
|
||||
@@ -701,16 +701,20 @@ val MIGRATION_35_36 =
|
||||
connection.execSQL("ALTER TABLE `artists` ADD COLUMN `artistImageUrl` TEXT")
|
||||
connection.execSQL("ALTER TABLE `artists` ADD COLUMN `coverArts` TEXT NOT NULL DEFAULT ''")
|
||||
connection.execSQL("ALTER TABLE `artists` ADD COLUMN `downloadedCoverArts` TEXT NOT NULL DEFAULT ''")
|
||||
optimizeArtistSearchTriggers(connection)
|
||||
backfillArtistArt(connection)
|
||||
}
|
||||
}
|
||||
|
||||
val MIGRATION_36_37 =
|
||||
object : Migration(36, 37) {
|
||||
override suspend fun migrate(connection: SQLiteConnection) {
|
||||
optimizeArtistSearchTriggers(connection)
|
||||
}
|
||||
internal fun artistSearchTriggersOptimized(connection: SQLiteConnection): Boolean {
|
||||
val triggers = mutableListOf<String>()
|
||||
connection.prepare("SELECT sql FROM sqlite_master WHERE type = 'trigger' AND name IN (?, ?)").use { statement ->
|
||||
statement.bindText(1, "room_fts_content_sync_artist_search_BEFORE_UPDATE")
|
||||
statement.bindText(2, "room_fts_content_sync_artist_search_AFTER_UPDATE")
|
||||
while (statement.step()) triggers += statement.getText(0)
|
||||
}
|
||||
return triggers.size == 2 && triggers.all { it.contains("UPDATE OF `name`, `sortName`") }
|
||||
}
|
||||
|
||||
internal fun optimizeArtistSearchTriggers(connection: SQLiteConnection) {
|
||||
connection.execSQL("DROP TRIGGER IF EXISTS room_fts_content_sync_artist_search_BEFORE_UPDATE")
|
||||
@@ -778,5 +782,4 @@ val MIGRATIONS: Array<Migration> =
|
||||
MIGRATION_33_34,
|
||||
MIGRATION_34_35,
|
||||
MIGRATION_35_36,
|
||||
MIGRATION_36_37,
|
||||
)
|
||||
|
||||
@@ -57,7 +57,7 @@ import com.subtracks.data.model.UpNextEntry
|
||||
SongDownload::class,
|
||||
PendingAction::class,
|
||||
],
|
||||
version = 37,
|
||||
version = 36,
|
||||
exportSchema = true,
|
||||
)
|
||||
@ColumnTypeConverters(
|
||||
|
||||
@@ -70,6 +70,9 @@ class SourceRepository(
|
||||
private val _offline = MutableStateFlow(false)
|
||||
val offline: StateFlow<Boolean> = _offline
|
||||
|
||||
private val _activeSource = MutableStateFlow<Long?>(null)
|
||||
val activeSource: StateFlow<Long?> = _activeSource
|
||||
|
||||
fun downloadsAllowedOverMetered(): Boolean = allowMeteredDownloads
|
||||
|
||||
fun setOfflineMode(enabled: Boolean) {
|
||||
@@ -110,6 +113,7 @@ class SourceRepository(
|
||||
Log.i("net", "active source -> ${config?.name ?: "none"}")
|
||||
}
|
||||
active = config?.let { ActiveSource(it.id, it.toMusicSource(quality, concurrency)) }
|
||||
_activeSource.value = config?.id
|
||||
_quality.value = quality
|
||||
if (sourceChanged && config != null && config.useTokenAuth && !_offline.value) {
|
||||
scope.launch {
|
||||
|
||||
@@ -17,7 +17,6 @@ import com.subtracks.ui.components.ArtistArt
|
||||
import com.subtracks.ui.components.artistArt
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
@@ -143,8 +142,13 @@ class ArtistDetailViewModel(
|
||||
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
combine(artist.filterNotNull(), sourceRepository.offline) { loaded, _ -> loaded }
|
||||
.collectLatest { loaded ->
|
||||
combine(
|
||||
artist.filterNotNull(),
|
||||
sourceRepository.offline,
|
||||
sourceRepository.activeSource,
|
||||
) { loaded, _, activeSource -> loaded to activeSource }
|
||||
.collectLatest { (loaded, activeSource) ->
|
||||
if (activeSource == null) return@collectLatest
|
||||
val hero = sourceRepository.artistArt(loaded, forIcon = false)
|
||||
val icon = sourceRepository.artistArt(loaded, forIcon = true)
|
||||
_art.value = hero.image ?: hero.covers.firstOrNull()
|
||||
|
||||
Reference in New Issue
Block a user