normalize every song's genres into song_genres and simplify the genre queries

This commit is contained in:
austinried
2026-10-04 00:30:06 +00:00
parent 3df16826e6
commit adbd3b8d8e
6 changed files with 1630 additions and 47 deletions
@@ -305,16 +305,10 @@ interface LibraryDao {
): Flow<List<AlbumSongItem>>
@Query(
"SELECT genre FROM (" +
"SELECT song_genres.genre AS genre, songs.playCount AS plays FROM song_genres " +
"SELECT song_genres.genre FROM song_genres " +
"JOIN songs ON songs.sourceId = song_genres.sourceId AND songs.id = song_genres.songId " +
"WHERE song_genres.sourceId = :sourceId " +
"UNION ALL " +
"SELECT songs.genre AS genre, songs.playCount AS plays FROM songs " +
"WHERE songs.sourceId = :sourceId AND songs.genre IS NOT NULL AND songs.genre != '' " +
"AND NOT EXISTS (SELECT 1 FROM song_genres " +
"WHERE song_genres.sourceId = songs.sourceId AND song_genres.songId = songs.id)" +
") GROUP BY genre ORDER BY SUM(plays) DESC, genre COLLATE NOCASE",
"GROUP BY song_genres.genre ORDER BY SUM(songs.playCount) DESC, song_genres.genre COLLATE NOCASE",
)
fun genresByMostPlayed(sourceId: Long): Flow<List<String>>
@@ -737,14 +731,10 @@ interface LibraryDao {
fun homeDownloadedSongs(sourceId: Long): PagingSource<Int, AlbumSongItem>
@Query(
"SELECT songs.*, albums.coverArt AS coverArt FROM songs " +
"SELECT songs.*, albums.coverArt AS coverArt FROM song_genres " +
"JOIN songs ON songs.sourceId = song_genres.sourceId AND songs.id = song_genres.songId " +
"LEFT JOIN albums ON albums.sourceId = songs.sourceId AND albums.id = songs.albumId " +
"WHERE songs.sourceId = :sourceId AND (" +
"EXISTS (SELECT 1 FROM song_genres " +
"WHERE song_genres.sourceId = songs.sourceId AND song_genres.songId = songs.id " +
"AND song_genres.genre = :genre) " +
"OR (songs.genre = :genre AND NOT EXISTS (SELECT 1 FROM song_genres " +
"WHERE song_genres.sourceId = songs.sourceId AND song_genres.songId = songs.id))) " +
"WHERE song_genres.sourceId = :sourceId AND song_genres.genre = :genre " +
"ORDER BY songs.title COLLATE NOCASE, songs.id",
)
fun songsByGenre(
@@ -753,13 +743,9 @@ interface LibraryDao {
): PagingSource<Int, AlbumSongItem>
@Query(
"SELECT songs.id FROM songs " +
"WHERE songs.sourceId = :sourceId AND (" +
"EXISTS (SELECT 1 FROM song_genres " +
"WHERE song_genres.sourceId = songs.sourceId AND song_genres.songId = songs.id " +
"AND song_genres.genre = :genre) " +
"OR (songs.genre = :genre AND NOT EXISTS (SELECT 1 FROM song_genres " +
"WHERE song_genres.sourceId = songs.sourceId AND song_genres.songId = songs.id))) " +
"SELECT songs.id FROM song_genres " +
"JOIN songs ON songs.sourceId = song_genres.sourceId AND songs.id = song_genres.songId " +
"WHERE song_genres.sourceId = :sourceId AND song_genres.genre = :genre " +
"ORDER BY songs.title COLLATE NOCASE, songs.id",
)
suspend fun songsByGenreIds(
@@ -826,16 +812,10 @@ interface LibraryDao {
): Flow<List<String>>
@Query(
"SELECT genre FROM (" +
"SELECT song_genres.genre AS genre FROM song_genres " +
"SELECT DISTINCT song_genres.genre FROM song_genres " +
"JOIN songs ON songs.sourceId = song_genres.sourceId AND songs.id = song_genres.songId " +
"WHERE songs.sourceId = :sourceId AND songs.albumId = :albumId " +
"UNION " +
"SELECT songs.genre AS genre FROM songs " +
"WHERE songs.sourceId = :sourceId AND songs.albumId = :albumId AND songs.genre IS NOT NULL " +
"AND NOT EXISTS (SELECT 1 FROM song_genres " +
"WHERE song_genres.sourceId = songs.sourceId AND song_genres.songId = songs.id)" +
") ORDER BY genre COLLATE NOCASE",
"ORDER BY song_genres.genre COLLATE NOCASE",
)
fun albumGenres(
sourceId: Long,
@@ -339,6 +339,19 @@ val MIGRATION_22_23 =
}
}
val MIGRATION_23_24 =
object : Migration(23, 24) {
override suspend fun migrate(connection: SQLiteConnection) {
connection.execSQL(
"INSERT INTO `song_genres` (`sourceId`, `songId`, `position`, `genre`) " +
"SELECT `sourceId`, `id`, 0, `genre` FROM `songs` " +
"WHERE `genre` IS NOT NULL AND `genre` != '' " +
"AND NOT EXISTS (SELECT 1 FROM `song_genres` sg " +
"WHERE sg.`sourceId` = `songs`.`sourceId` AND sg.`songId` = `songs`.`id`)",
)
}
}
// Mirrors the FTS5 table and content-sync triggers Room generates for the @Fts5 entities.
private suspend fun createSearchIndex(
connection: SQLiteConnection,
@@ -395,4 +408,5 @@ val MIGRATIONS: Array<Migration> =
MIGRATION_20_21,
MIGRATION_21_22,
MIGRATION_22_23,
MIGRATION_23_24,
)
@@ -45,7 +45,7 @@ import com.subtracks.data.model.UpNextEntry
ArtworkSeed::class,
SongDownload::class,
],
version = 23,
version = 24,
exportSchema = true,
)
@ColumnTypeConverters(QueueKindConverter::class, DownloadStatusConverter::class)
@@ -72,9 +72,10 @@ class SyncService(
}
private suspend fun writeSongs(batch: List<Song>) {
val names = batch.associate { it.id to it.genreNames() }
val genres =
batch.flatMap { song ->
song.genres.mapIndexed { index, genre -> SongGenre(song.sourceId, song.id, index.toLong(), genre) }
names.getValue(song.id).mapIndexed { index, genre -> SongGenre(song.sourceId, song.id, index.toLong(), genre) }
}
db.useWriterConnection { connection ->
connection.immediateTransaction {
@@ -83,7 +84,7 @@ class SyncService(
batch.forEach { song ->
statement.bind(1, song.sourceId)
statement.bind(2, song.id)
statement.bind(3, song.genres.size.toLong())
statement.bind(3, names.getValue(song.id).size.toLong())
statement.step()
statement.reset()
statement.clearBindings()
@@ -284,6 +285,8 @@ private val songColumns =
private val playlistColumns = listOf("sourceId", "id", "name", "comment", "coverArt", "songCount", "created", "changed", "duration")
private fun Song.genreNames(): List<String> = genres.ifEmpty { listOfNotNull(genre?.trim()?.takeIf { it.isNotEmpty() }) }
private val songGenreColumns = listOf("sourceId", "songId", "position", "genre")
private val songGenreKey = listOf("sourceId", "songId", "position")