mirror of
https://github.com/austinried/subtracks.git
synced 2026-11-10 23:21:59 +01:00
play genres and downloads as lazy queue references instead of expanding every song
This commit is contained in:
@@ -753,6 +753,31 @@ interface LibraryDao {
|
||||
genre: String,
|
||||
): List<String>
|
||||
|
||||
@Query(
|
||||
"SELECT COUNT(*) 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 " +
|
||||
"AND (songs.title COLLATE NOCASE < :title OR (songs.title COLLATE NOCASE = :title AND songs.id < :id))",
|
||||
)
|
||||
suspend fun genreSongOrdinal(
|
||||
sourceId: Long,
|
||||
genre: String,
|
||||
title: String,
|
||||
id: String,
|
||||
): Long
|
||||
|
||||
@Query(
|
||||
"SELECT COUNT(*) FROM song_downloads d " +
|
||||
"JOIN songs ON songs.sourceId = d.sourceId AND songs.id = d.songId " +
|
||||
"WHERE d.sourceId = :sourceId AND d.status = 'Completed' " +
|
||||
"AND (songs.title COLLATE NOCASE < :title OR (songs.title COLLATE NOCASE = :title AND songs.id < :id))",
|
||||
)
|
||||
suspend fun downloadedSongOrdinal(
|
||||
sourceId: Long,
|
||||
title: String,
|
||||
id: String,
|
||||
): Long
|
||||
|
||||
@Query(
|
||||
"SELECT * FROM albums WHERE sourceId = :sourceId AND year >= :start AND year < :end " +
|
||||
"ORDER BY year DESC, name COLLATE NOCASE, id",
|
||||
|
||||
@@ -219,4 +219,59 @@ interface QueueDao {
|
||||
sourceId: Long,
|
||||
ids: List<String>,
|
||||
): List<AlbumSongItem>
|
||||
|
||||
@Query("SELECT COUNT(*) FROM song_genres WHERE sourceId = :sourceId AND genre = :genre")
|
||||
suspend fun genreLength(
|
||||
sourceId: Long,
|
||||
genre: String,
|
||||
): Long
|
||||
|
||||
@Query(
|
||||
"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 song_genres.sourceId = :sourceId AND song_genres.genre = :genre " +
|
||||
"ORDER BY songs.title COLLATE NOCASE, songs.id LIMIT :limit OFFSET :offset",
|
||||
)
|
||||
suspend fun genreSongs(
|
||||
sourceId: Long,
|
||||
genre: String,
|
||||
offset: Long,
|
||||
limit: Int,
|
||||
): List<AlbumSongItem>
|
||||
|
||||
@Query(
|
||||
"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 genreSongIds(
|
||||
sourceId: Long,
|
||||
genre: String,
|
||||
): List<String>
|
||||
|
||||
@Query("SELECT COUNT(*) FROM song_downloads WHERE sourceId = :sourceId AND status = 'Completed'")
|
||||
suspend fun downloadedLength(sourceId: Long): Long
|
||||
|
||||
@Query(
|
||||
"SELECT songs.*, albums.coverArt AS coverArt FROM song_downloads d " +
|
||||
"JOIN songs ON songs.sourceId = d.sourceId AND songs.id = d.songId " +
|
||||
"LEFT JOIN albums ON albums.sourceId = songs.sourceId AND albums.id = songs.albumId " +
|
||||
"WHERE d.sourceId = :sourceId AND d.status = 'Completed' " +
|
||||
"ORDER BY songs.title COLLATE NOCASE, songs.id LIMIT :limit OFFSET :offset",
|
||||
)
|
||||
suspend fun downloadedSongs(
|
||||
sourceId: Long,
|
||||
offset: Long,
|
||||
limit: Int,
|
||||
): List<AlbumSongItem>
|
||||
|
||||
@Query(
|
||||
"SELECT songs.id FROM song_downloads d " +
|
||||
"JOIN songs ON songs.sourceId = d.sourceId AND songs.id = d.songId " +
|
||||
"WHERE d.sourceId = :sourceId AND d.status = 'Completed' " +
|
||||
"ORDER BY songs.title COLLATE NOCASE, songs.id",
|
||||
)
|
||||
suspend fun downloadedSongIds(sourceId: Long): List<String>
|
||||
}
|
||||
|
||||
@@ -316,7 +316,7 @@ data class DiscKey(
|
||||
val disc: Long,
|
||||
)
|
||||
|
||||
enum class QueueKind { Playlist, Album, Song, Artist }
|
||||
enum class QueueKind { Playlist, Album, Song, Artist, Genre, Downloaded }
|
||||
|
||||
@Entity(
|
||||
tableName = "queue_entries",
|
||||
|
||||
@@ -106,6 +106,19 @@ class LibraryRepository(
|
||||
genre: String,
|
||||
): List<String> = db.libraryDao().songsByGenreIds(sourceId, genre)
|
||||
|
||||
suspend fun genreSongOrdinal(
|
||||
sourceId: Long,
|
||||
genre: String,
|
||||
title: String,
|
||||
id: String,
|
||||
): Long = db.libraryDao().genreSongOrdinal(sourceId, genre, title, id)
|
||||
|
||||
suspend fun downloadedSongOrdinal(
|
||||
sourceId: Long,
|
||||
title: String,
|
||||
id: String,
|
||||
): Long = db.libraryDao().downloadedSongOrdinal(sourceId, title, id)
|
||||
|
||||
fun albumsByDecade(
|
||||
sourceId: Long,
|
||||
decade: Long,
|
||||
|
||||
@@ -255,6 +255,13 @@ class QueueRepository(
|
||||
songId: String,
|
||||
) = QueueEntry(position = 0, sourceId = sourceId, kind = QueueKind.Song, refId = songId)
|
||||
|
||||
fun genreEntry(
|
||||
sourceId: Long,
|
||||
genre: String,
|
||||
) = QueueEntry(position = 0, sourceId = sourceId, kind = QueueKind.Genre, refId = genre)
|
||||
|
||||
fun downloadedEntry(sourceId: Long) = QueueEntry(position = 0, sourceId = sourceId, kind = QueueKind.Downloaded, refId = "")
|
||||
|
||||
suspend fun sourceName(
|
||||
kind: QueueKind,
|
||||
sourceId: Long,
|
||||
@@ -295,6 +302,14 @@ class QueueRepository(
|
||||
?.album
|
||||
?.takeIf { it.isNotBlank() }
|
||||
}
|
||||
|
||||
QueueKind.Genre -> {
|
||||
refId.takeIf { it.isNotBlank() }
|
||||
}
|
||||
|
||||
QueueKind.Downloaded -> {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -646,6 +661,8 @@ class QueueRepository(
|
||||
QueueKind.Album -> dao.albumSongIds(ref.sourceId, ref.refId)
|
||||
QueueKind.Playlist -> dao.playlistSongIds(ref.sourceId, ref.refId)
|
||||
QueueKind.Artist -> dao.artistSongIds(ref.sourceId, ref.refId)
|
||||
QueueKind.Genre -> dao.genreSongIds(ref.sourceId, ref.refId)
|
||||
QueueKind.Downloaded -> dao.downloadedSongIds(ref.sourceId)
|
||||
}
|
||||
val from = ref.offset.toInt().coerceIn(0, entryIds.size)
|
||||
val to = (ref.offset + entry.length).toInt().coerceIn(from, entryIds.size)
|
||||
@@ -746,6 +763,8 @@ class QueueRepository(
|
||||
QueueKind.Playlist -> dao.playlistSongs(entry.sourceId, entry.refId, offset, limit)
|
||||
QueueKind.Album -> dao.albumSongs(entry.sourceId, entry.refId, offset, limit)
|
||||
QueueKind.Artist -> dao.artistSongs(entry.sourceId, entry.refId, offset, limit)
|
||||
QueueKind.Genre -> dao.genreSongs(entry.sourceId, entry.refId, offset, limit)
|
||||
QueueKind.Downloaded -> dao.downloadedSongs(entry.sourceId, offset, limit)
|
||||
QueueKind.Song -> emptyList()
|
||||
}
|
||||
if (fresh.isNotEmpty()) remember(ref, offset, fresh)
|
||||
@@ -769,6 +788,8 @@ class QueueRepository(
|
||||
QueueKind.Album -> OrderKey(row.song.id, disc = row.song.disc, track = row.song.track)
|
||||
QueueKind.Playlist -> OrderKey(row.song.id, position = row.playlistPosition)
|
||||
QueueKind.Artist -> OrderKey(row.song.id)
|
||||
QueueKind.Genre -> OrderKey(row.song.id)
|
||||
QueueKind.Downloaded -> OrderKey(row.song.id)
|
||||
QueueKind.Song -> OrderKey(row.song.id)
|
||||
}
|
||||
|
||||
@@ -809,6 +830,8 @@ class QueueRepository(
|
||||
QueueKind.Playlist -> dao.playlistSongsFrom(entry.sourceId, entry.refId, key.position!!, skip, limit)
|
||||
QueueKind.Album -> dao.albumSongsFrom(entry.sourceId, entry.refId, key.disc!!, key.track!!, key.id, skip, limit)
|
||||
QueueKind.Artist -> emptyList()
|
||||
QueueKind.Genre -> emptyList()
|
||||
QueueKind.Downloaded -> emptyList()
|
||||
QueueKind.Song -> emptyList()
|
||||
}
|
||||
|
||||
@@ -822,6 +845,8 @@ class QueueRepository(
|
||||
QueueKind.Playlist -> dao.playlistSongsBefore(entry.sourceId, entry.refId, key.position!!, skip, limit)
|
||||
QueueKind.Album -> dao.albumSongsBefore(entry.sourceId, entry.refId, key.disc!!, key.track!!, key.id, skip, limit)
|
||||
QueueKind.Artist -> emptyList()
|
||||
QueueKind.Genre -> emptyList()
|
||||
QueueKind.Downloaded -> emptyList()
|
||||
QueueKind.Song -> emptyList()
|
||||
}
|
||||
|
||||
@@ -831,6 +856,8 @@ class QueueRepository(
|
||||
QueueKind.Playlist -> position != null
|
||||
QueueKind.Album -> disc != null && track != null
|
||||
QueueKind.Artist -> false
|
||||
QueueKind.Genre -> false
|
||||
QueueKind.Downloaded -> false
|
||||
QueueKind.Song -> false
|
||||
}
|
||||
|
||||
@@ -874,6 +901,14 @@ class QueueRepository(
|
||||
QueueKind.Artist -> {
|
||||
dao.artistLength(sourceId, refId)
|
||||
}
|
||||
|
||||
QueueKind.Genre -> {
|
||||
dao.genreLength(sourceId, refId)
|
||||
}
|
||||
|
||||
QueueKind.Downloaded -> {
|
||||
dao.downloadedLength(sourceId)
|
||||
}
|
||||
}
|
||||
val available = (total - offset).coerceAtLeast(0)
|
||||
return count?.coerceAtMost(available) ?: available
|
||||
|
||||
@@ -226,6 +226,17 @@ class PlaybackController(
|
||||
startPosition: Long,
|
||||
) = playContext(queueRepository.playlistEntry(sourceId, playlistId), startPosition)
|
||||
|
||||
fun playGenre(
|
||||
sourceId: Long,
|
||||
genre: String,
|
||||
startOrdinal: Long,
|
||||
) = playContext(queueRepository.genreEntry(sourceId, genre), startOrdinal)
|
||||
|
||||
fun playDownloaded(
|
||||
sourceId: Long,
|
||||
startOrdinal: Long,
|
||||
) = playContext(queueRepository.downloadedEntry(sourceId), startOrdinal)
|
||||
|
||||
fun playAlbumInOrder(
|
||||
sourceId: Long,
|
||||
albumId: String,
|
||||
|
||||
@@ -241,8 +241,8 @@ internal fun CoroutineScope.playGenreList(
|
||||
song: Song,
|
||||
) {
|
||||
launch {
|
||||
val ids = libraryRepository.genreSongIds(song.sourceId, genre)
|
||||
playbackController.playSongs(song.sourceId, ids, ids.indexOf(song.id).coerceAtLeast(0))
|
||||
val ordinal = libraryRepository.genreSongOrdinal(song.sourceId, genre, song.title, song.id)
|
||||
playbackController.playGenre(song.sourceId, genre, ordinal)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -252,7 +252,7 @@ internal fun CoroutineScope.playDownloadedList(
|
||||
song: Song,
|
||||
) {
|
||||
launch {
|
||||
val ids = libraryRepository.downloadedSongIds(song.sourceId)
|
||||
playbackController.playSongs(song.sourceId, ids, ids.indexOf(song.id).coerceAtLeast(0))
|
||||
val ordinal = libraryRepository.downloadedSongOrdinal(song.sourceId, song.title, song.id)
|
||||
playbackController.playDownloaded(song.sourceId, ordinal)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -470,6 +470,8 @@ private fun QueueKind.label(): String =
|
||||
QueueKind.Playlist -> "playlist"
|
||||
QueueKind.Song -> "song"
|
||||
QueueKind.Artist -> "artist"
|
||||
QueueKind.Genre -> "genre"
|
||||
QueueKind.Downloaded -> "downloads"
|
||||
}
|
||||
|
||||
private fun formatTime(milliseconds: Long): String {
|
||||
|
||||
@@ -3,16 +3,19 @@ package com.subtracks.data.repo
|
||||
import android.content.Context
|
||||
import androidx.room3.Room
|
||||
import androidx.room3.useReaderConnection
|
||||
import androidx.room3.useWriterConnection
|
||||
import androidx.sqlite.driver.bundled.BundledSQLiteDriver
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import com.subtracks.data.db.SubtracksDatabase
|
||||
import com.subtracks.data.model.Album
|
||||
import com.subtracks.data.model.DownloadStatus
|
||||
import com.subtracks.data.model.Playlist
|
||||
import com.subtracks.data.model.PlaylistSong
|
||||
import com.subtracks.data.model.QueueEntry
|
||||
import com.subtracks.data.model.QueueKind
|
||||
import com.subtracks.data.model.Song
|
||||
import com.subtracks.data.model.SongDownload
|
||||
import com.subtracks.data.model.Source
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.After
|
||||
@@ -55,6 +58,35 @@ class QueueRepositoryTest {
|
||||
assertEquals(listOf("s1", "s2", "s3"), resolveAll(snapshot))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun wholeGenreEntryResolvesEveryTrackInTitleOrder() =
|
||||
runTest {
|
||||
seedLibrary()
|
||||
insertGenre("s4", "Rock")
|
||||
insertGenre("s1", "Rock")
|
||||
insertGenre("s3", "Rock")
|
||||
insertGenre("s2", "Jazz")
|
||||
|
||||
val snapshot = repository.snapshotAfter(listOf(repository.genreEntry(1, "Rock")))
|
||||
|
||||
assertEquals(3, snapshot.size)
|
||||
assertEquals(listOf("s1", "s3", "s4"), resolveAll(snapshot))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun downloadedEntryResolvesOnlyCompletedDownloads() =
|
||||
runTest {
|
||||
seedLibrary()
|
||||
db.downloadDao().upsert(SongDownload(1, "s1", DownloadStatus.Completed))
|
||||
db.downloadDao().upsert(SongDownload(1, "s3", DownloadStatus.Completed))
|
||||
db.downloadDao().upsert(SongDownload(1, "s2", DownloadStatus.Running))
|
||||
|
||||
val snapshot = repository.snapshotAfter(listOf(repository.downloadedEntry(1)))
|
||||
|
||||
assertEquals(2, snapshot.size)
|
||||
assertEquals(listOf("s1", "s3"), resolveAll(snapshot))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun rangeLimitsAnEntryToASlice() =
|
||||
runTest {
|
||||
@@ -912,4 +944,15 @@ class QueueRepositoryTest {
|
||||
genre = null,
|
||||
created = created,
|
||||
)
|
||||
|
||||
private suspend fun insertGenre(
|
||||
songId: String,
|
||||
genre: String,
|
||||
) {
|
||||
db.useWriterConnection { connection ->
|
||||
connection.usePrepared(
|
||||
"INSERT INTO song_genres (sourceId, songId, position, genre) VALUES (1, '$songId', 0, '$genre')",
|
||||
) { it.step() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user