mirror of
https://github.com/austinried/subtracks.git
synced 2026-11-11 15:29:15 +01:00
normalize every song's genres into song_genres and simplify the genre queries
This commit is contained in:
+1577
File diff suppressed because it is too large
Load Diff
@@ -305,16 +305,10 @@ interface LibraryDao {
|
|||||||
): Flow<List<AlbumSongItem>>
|
): Flow<List<AlbumSongItem>>
|
||||||
|
|
||||||
@Query(
|
@Query(
|
||||||
"SELECT genre FROM (" +
|
"SELECT song_genres.genre FROM song_genres " +
|
||||||
"SELECT song_genres.genre AS genre, songs.playCount AS plays FROM song_genres " +
|
|
||||||
"JOIN songs ON songs.sourceId = song_genres.sourceId AND songs.id = song_genres.songId " +
|
"JOIN songs ON songs.sourceId = song_genres.sourceId AND songs.id = song_genres.songId " +
|
||||||
"WHERE song_genres.sourceId = :sourceId " +
|
"WHERE song_genres.sourceId = :sourceId " +
|
||||||
"UNION ALL " +
|
"GROUP BY song_genres.genre ORDER BY SUM(songs.playCount) DESC, song_genres.genre COLLATE NOCASE",
|
||||||
"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",
|
|
||||||
)
|
)
|
||||||
fun genresByMostPlayed(sourceId: Long): Flow<List<String>>
|
fun genresByMostPlayed(sourceId: Long): Flow<List<String>>
|
||||||
|
|
||||||
@@ -737,14 +731,10 @@ interface LibraryDao {
|
|||||||
fun homeDownloadedSongs(sourceId: Long): PagingSource<Int, AlbumSongItem>
|
fun homeDownloadedSongs(sourceId: Long): PagingSource<Int, AlbumSongItem>
|
||||||
|
|
||||||
@Query(
|
@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 " +
|
"LEFT JOIN albums ON albums.sourceId = songs.sourceId AND albums.id = songs.albumId " +
|
||||||
"WHERE songs.sourceId = :sourceId AND (" +
|
"WHERE song_genres.sourceId = :sourceId AND song_genres.genre = :genre " +
|
||||||
"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))) " +
|
|
||||||
"ORDER BY songs.title COLLATE NOCASE, songs.id",
|
"ORDER BY songs.title COLLATE NOCASE, songs.id",
|
||||||
)
|
)
|
||||||
fun songsByGenre(
|
fun songsByGenre(
|
||||||
@@ -753,13 +743,9 @@ interface LibraryDao {
|
|||||||
): PagingSource<Int, AlbumSongItem>
|
): PagingSource<Int, AlbumSongItem>
|
||||||
|
|
||||||
@Query(
|
@Query(
|
||||||
"SELECT songs.id FROM songs " +
|
"SELECT songs.id FROM song_genres " +
|
||||||
"WHERE songs.sourceId = :sourceId AND (" +
|
"JOIN songs ON songs.sourceId = song_genres.sourceId AND songs.id = song_genres.songId " +
|
||||||
"EXISTS (SELECT 1 FROM song_genres " +
|
"WHERE song_genres.sourceId = :sourceId AND song_genres.genre = :genre " +
|
||||||
"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))) " +
|
|
||||||
"ORDER BY songs.title COLLATE NOCASE, songs.id",
|
"ORDER BY songs.title COLLATE NOCASE, songs.id",
|
||||||
)
|
)
|
||||||
suspend fun songsByGenreIds(
|
suspend fun songsByGenreIds(
|
||||||
@@ -826,16 +812,10 @@ interface LibraryDao {
|
|||||||
): Flow<List<String>>
|
): Flow<List<String>>
|
||||||
|
|
||||||
@Query(
|
@Query(
|
||||||
"SELECT genre FROM (" +
|
"SELECT DISTINCT song_genres.genre FROM song_genres " +
|
||||||
"SELECT song_genres.genre AS genre FROM song_genres " +
|
|
||||||
"JOIN songs ON songs.sourceId = song_genres.sourceId AND songs.id = song_genres.songId " +
|
"JOIN songs ON songs.sourceId = song_genres.sourceId AND songs.id = song_genres.songId " +
|
||||||
"WHERE songs.sourceId = :sourceId AND songs.albumId = :albumId " +
|
"WHERE songs.sourceId = :sourceId AND songs.albumId = :albumId " +
|
||||||
"UNION " +
|
"ORDER BY song_genres.genre COLLATE NOCASE",
|
||||||
"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",
|
|
||||||
)
|
)
|
||||||
fun albumGenres(
|
fun albumGenres(
|
||||||
sourceId: Long,
|
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.
|
// Mirrors the FTS5 table and content-sync triggers Room generates for the @Fts5 entities.
|
||||||
private suspend fun createSearchIndex(
|
private suspend fun createSearchIndex(
|
||||||
connection: SQLiteConnection,
|
connection: SQLiteConnection,
|
||||||
@@ -395,4 +408,5 @@ val MIGRATIONS: Array<Migration> =
|
|||||||
MIGRATION_20_21,
|
MIGRATION_20_21,
|
||||||
MIGRATION_21_22,
|
MIGRATION_21_22,
|
||||||
MIGRATION_22_23,
|
MIGRATION_22_23,
|
||||||
|
MIGRATION_23_24,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ import com.subtracks.data.model.UpNextEntry
|
|||||||
ArtworkSeed::class,
|
ArtworkSeed::class,
|
||||||
SongDownload::class,
|
SongDownload::class,
|
||||||
],
|
],
|
||||||
version = 23,
|
version = 24,
|
||||||
exportSchema = true,
|
exportSchema = true,
|
||||||
)
|
)
|
||||||
@ColumnTypeConverters(QueueKindConverter::class, DownloadStatusConverter::class)
|
@ColumnTypeConverters(QueueKindConverter::class, DownloadStatusConverter::class)
|
||||||
|
|||||||
@@ -72,9 +72,10 @@ class SyncService(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun writeSongs(batch: List<Song>) {
|
private suspend fun writeSongs(batch: List<Song>) {
|
||||||
|
val names = batch.associate { it.id to it.genreNames() }
|
||||||
val genres =
|
val genres =
|
||||||
batch.flatMap { song ->
|
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 ->
|
db.useWriterConnection { connection ->
|
||||||
connection.immediateTransaction {
|
connection.immediateTransaction {
|
||||||
@@ -83,7 +84,7 @@ class SyncService(
|
|||||||
batch.forEach { song ->
|
batch.forEach { song ->
|
||||||
statement.bind(1, song.sourceId)
|
statement.bind(1, song.sourceId)
|
||||||
statement.bind(2, song.id)
|
statement.bind(2, song.id)
|
||||||
statement.bind(3, song.genres.size.toLong())
|
statement.bind(3, names.getValue(song.id).size.toLong())
|
||||||
statement.step()
|
statement.step()
|
||||||
statement.reset()
|
statement.reset()
|
||||||
statement.clearBindings()
|
statement.clearBindings()
|
||||||
@@ -284,6 +285,8 @@ private val songColumns =
|
|||||||
|
|
||||||
private val playlistColumns = listOf("sourceId", "id", "name", "comment", "coverArt", "songCount", "created", "changed", "duration")
|
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 songGenreColumns = listOf("sourceId", "songId", "position", "genre")
|
||||||
|
|
||||||
private val songGenreKey = listOf("sourceId", "songId", "position")
|
private val songGenreKey = listOf("sourceId", "songId", "position")
|
||||||
|
|||||||
@@ -132,12 +132,16 @@ class LibraryHomeDaoTest {
|
|||||||
val dao = db.libraryDao()
|
val dao = db.libraryDao()
|
||||||
dao.upsertSongs(
|
dao.upsertSongs(
|
||||||
listOf(
|
listOf(
|
||||||
song(sourceId, "s1", "One", genre = "Rock", playCount = 5),
|
song(sourceId, "s1", "One", playCount = 5),
|
||||||
song(sourceId, "s2", "Two", genre = "Jazz", playCount = 2),
|
song(sourceId, "s2", "Two", playCount = 2),
|
||||||
song(sourceId, "s3", "Three", genre = "Rock", playCount = 4),
|
song(sourceId, "s3", "Three", playCount = 4),
|
||||||
song(sourceId, "s4", "Four", genre = "Classical", playCount = 0),
|
song(sourceId, "s4", "Four", playCount = 0),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
insertGenre(sourceId, "s1", 0, "Rock")
|
||||||
|
insertGenre(sourceId, "s3", 0, "Rock")
|
||||||
|
insertGenre(sourceId, "s2", 0, "Jazz")
|
||||||
|
insertGenre(sourceId, "s4", 0, "Classical")
|
||||||
|
|
||||||
assertEquals(
|
assertEquals(
|
||||||
listOf("Rock", "Jazz", "Classical"),
|
listOf("Rock", "Jazz", "Classical"),
|
||||||
@@ -146,16 +150,17 @@ class LibraryHomeDaoTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun genresComeFromTheGenreRelationsAndFallBackPerSong() =
|
fun genresComeFromTheStoredRelations() =
|
||||||
runTest {
|
runTest {
|
||||||
val sourceId = source()
|
val sourceId = source()
|
||||||
val dao = db.libraryDao()
|
val dao = db.libraryDao()
|
||||||
dao.upsertSongs(
|
dao.upsertSongs(
|
||||||
listOf(
|
listOf(
|
||||||
song(sourceId, "s1", "One", genre = "Legacy", playCount = 1),
|
song(sourceId, "s1", "One", playCount = 1),
|
||||||
song(sourceId, "s2", "Two", genre = "Legacy", playCount = 3),
|
song(sourceId, "s2", "Two", playCount = 3),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
insertGenre(sourceId, "s1", 0, "Legacy")
|
||||||
insertGenre(sourceId, "s2", 0, "Rock")
|
insertGenre(sourceId, "s2", 0, "Rock")
|
||||||
insertGenre(sourceId, "s2", 1, "Electronic")
|
insertGenre(sourceId, "s2", 1, "Electronic")
|
||||||
|
|
||||||
@@ -190,18 +195,19 @@ class LibraryHomeDaoTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun songsByGenreMatchesTheGenreRelationsWithFallback() =
|
fun songsByGenreReturnsSongsWithTheGenreRelation() =
|
||||||
runTest {
|
runTest {
|
||||||
val sourceId = source()
|
val sourceId = source()
|
||||||
val dao = db.libraryDao()
|
val dao = db.libraryDao()
|
||||||
dao.upsertAlbums(listOf(album(sourceId, "al1", "Album", coverArt = "cover-1")))
|
dao.upsertAlbums(listOf(album(sourceId, "al1", "Album", coverArt = "cover-1")))
|
||||||
dao.upsertSongs(
|
dao.upsertSongs(
|
||||||
listOf(
|
listOf(
|
||||||
song(sourceId, "s1", "Fallback", genre = "Rock"),
|
song(sourceId, "s1", "Fallback"),
|
||||||
song(sourceId, "s2", "Relation", genre = "Jazz"),
|
song(sourceId, "s2", "Relation"),
|
||||||
song(sourceId, "s3", "NotRock", genre = "Rock"),
|
song(sourceId, "s3", "NotRock"),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
insertGenre(sourceId, "s1", 0, "Rock")
|
||||||
insertGenre(sourceId, "s2", 0, "Rock")
|
insertGenre(sourceId, "s2", 0, "Rock")
|
||||||
insertGenre(sourceId, "s3", 0, "Jazz")
|
insertGenre(sourceId, "s3", 0, "Jazz")
|
||||||
|
|
||||||
@@ -370,11 +376,14 @@ class LibraryHomeDaoTest {
|
|||||||
dao.upsertAlbums(listOf(album(sourceId, "al1", "Album", coverArt = "cover-1")))
|
dao.upsertAlbums(listOf(album(sourceId, "al1", "Album", coverArt = "cover-1")))
|
||||||
dao.upsertSongs(
|
dao.upsertSongs(
|
||||||
listOf(
|
listOf(
|
||||||
song(sourceId, "s1", "Rock One", genre = "Rock"),
|
song(sourceId, "s1", "Rock One"),
|
||||||
song(sourceId, "s2", "Jazz One", genre = "Jazz"),
|
song(sourceId, "s2", "Jazz One"),
|
||||||
song(sourceId, "s3", "Rock Two", genre = "Rock"),
|
song(sourceId, "s3", "Rock Two"),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
insertGenre(sourceId, "s1", 0, "Rock")
|
||||||
|
insertGenre(sourceId, "s2", 0, "Jazz")
|
||||||
|
insertGenre(sourceId, "s3", 0, "Rock")
|
||||||
|
|
||||||
val page = dao.songsByGenre(sourceId, "Rock").page()
|
val page = dao.songsByGenre(sourceId, "Rock").page()
|
||||||
assertEquals(listOf("s1", "s3"), page.map { it.song.id })
|
assertEquals(listOf("s1", "s3"), page.map { it.song.id })
|
||||||
|
|||||||
Reference in New Issue
Block a user