mirror of
https://github.com/austinried/subtracks.git
synced 2026-11-11 15:29:15 +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,
|
genre: String,
|
||||||
): List<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(
|
@Query(
|
||||||
"SELECT * FROM albums WHERE sourceId = :sourceId AND year >= :start AND year < :end " +
|
"SELECT * FROM albums WHERE sourceId = :sourceId AND year >= :start AND year < :end " +
|
||||||
"ORDER BY year DESC, name COLLATE NOCASE, id",
|
"ORDER BY year DESC, name COLLATE NOCASE, id",
|
||||||
|
|||||||
@@ -219,4 +219,59 @@ interface QueueDao {
|
|||||||
sourceId: Long,
|
sourceId: Long,
|
||||||
ids: List<String>,
|
ids: List<String>,
|
||||||
): List<AlbumSongItem>
|
): 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,
|
val disc: Long,
|
||||||
)
|
)
|
||||||
|
|
||||||
enum class QueueKind { Playlist, Album, Song, Artist }
|
enum class QueueKind { Playlist, Album, Song, Artist, Genre, Downloaded }
|
||||||
|
|
||||||
@Entity(
|
@Entity(
|
||||||
tableName = "queue_entries",
|
tableName = "queue_entries",
|
||||||
|
|||||||
@@ -106,6 +106,19 @@ class LibraryRepository(
|
|||||||
genre: String,
|
genre: String,
|
||||||
): List<String> = db.libraryDao().songsByGenreIds(sourceId, genre)
|
): 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(
|
fun albumsByDecade(
|
||||||
sourceId: Long,
|
sourceId: Long,
|
||||||
decade: Long,
|
decade: Long,
|
||||||
|
|||||||
@@ -255,6 +255,13 @@ class QueueRepository(
|
|||||||
songId: String,
|
songId: String,
|
||||||
) = QueueEntry(position = 0, sourceId = sourceId, kind = QueueKind.Song, refId = songId)
|
) = 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(
|
suspend fun sourceName(
|
||||||
kind: QueueKind,
|
kind: QueueKind,
|
||||||
sourceId: Long,
|
sourceId: Long,
|
||||||
@@ -295,6 +302,14 @@ class QueueRepository(
|
|||||||
?.album
|
?.album
|
||||||
?.takeIf { it.isNotBlank() }
|
?.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.Album -> dao.albumSongIds(ref.sourceId, ref.refId)
|
||||||
QueueKind.Playlist -> dao.playlistSongIds(ref.sourceId, ref.refId)
|
QueueKind.Playlist -> dao.playlistSongIds(ref.sourceId, ref.refId)
|
||||||
QueueKind.Artist -> dao.artistSongIds(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 from = ref.offset.toInt().coerceIn(0, entryIds.size)
|
||||||
val to = (ref.offset + entry.length).toInt().coerceIn(from, 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.Playlist -> dao.playlistSongs(entry.sourceId, entry.refId, offset, limit)
|
||||||
QueueKind.Album -> dao.albumSongs(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.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()
|
QueueKind.Song -> emptyList()
|
||||||
}
|
}
|
||||||
if (fresh.isNotEmpty()) remember(ref, offset, fresh)
|
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.Album -> OrderKey(row.song.id, disc = row.song.disc, track = row.song.track)
|
||||||
QueueKind.Playlist -> OrderKey(row.song.id, position = row.playlistPosition)
|
QueueKind.Playlist -> OrderKey(row.song.id, position = row.playlistPosition)
|
||||||
QueueKind.Artist -> OrderKey(row.song.id)
|
QueueKind.Artist -> OrderKey(row.song.id)
|
||||||
|
QueueKind.Genre -> OrderKey(row.song.id)
|
||||||
|
QueueKind.Downloaded -> OrderKey(row.song.id)
|
||||||
QueueKind.Song -> 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.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.Album -> dao.albumSongsFrom(entry.sourceId, entry.refId, key.disc!!, key.track!!, key.id, skip, limit)
|
||||||
QueueKind.Artist -> emptyList()
|
QueueKind.Artist -> emptyList()
|
||||||
|
QueueKind.Genre -> emptyList()
|
||||||
|
QueueKind.Downloaded -> emptyList()
|
||||||
QueueKind.Song -> emptyList()
|
QueueKind.Song -> emptyList()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -822,6 +845,8 @@ class QueueRepository(
|
|||||||
QueueKind.Playlist -> dao.playlistSongsBefore(entry.sourceId, entry.refId, key.position!!, skip, limit)
|
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.Album -> dao.albumSongsBefore(entry.sourceId, entry.refId, key.disc!!, key.track!!, key.id, skip, limit)
|
||||||
QueueKind.Artist -> emptyList()
|
QueueKind.Artist -> emptyList()
|
||||||
|
QueueKind.Genre -> emptyList()
|
||||||
|
QueueKind.Downloaded -> emptyList()
|
||||||
QueueKind.Song -> emptyList()
|
QueueKind.Song -> emptyList()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -831,6 +856,8 @@ class QueueRepository(
|
|||||||
QueueKind.Playlist -> position != null
|
QueueKind.Playlist -> position != null
|
||||||
QueueKind.Album -> disc != null && track != null
|
QueueKind.Album -> disc != null && track != null
|
||||||
QueueKind.Artist -> false
|
QueueKind.Artist -> false
|
||||||
|
QueueKind.Genre -> false
|
||||||
|
QueueKind.Downloaded -> false
|
||||||
QueueKind.Song -> false
|
QueueKind.Song -> false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -874,6 +901,14 @@ class QueueRepository(
|
|||||||
QueueKind.Artist -> {
|
QueueKind.Artist -> {
|
||||||
dao.artistLength(sourceId, refId)
|
dao.artistLength(sourceId, refId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QueueKind.Genre -> {
|
||||||
|
dao.genreLength(sourceId, refId)
|
||||||
|
}
|
||||||
|
|
||||||
|
QueueKind.Downloaded -> {
|
||||||
|
dao.downloadedLength(sourceId)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
val available = (total - offset).coerceAtLeast(0)
|
val available = (total - offset).coerceAtLeast(0)
|
||||||
return count?.coerceAtMost(available) ?: available
|
return count?.coerceAtMost(available) ?: available
|
||||||
|
|||||||
@@ -226,6 +226,17 @@ class PlaybackController(
|
|||||||
startPosition: Long,
|
startPosition: Long,
|
||||||
) = playContext(queueRepository.playlistEntry(sourceId, playlistId), startPosition)
|
) = 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(
|
fun playAlbumInOrder(
|
||||||
sourceId: Long,
|
sourceId: Long,
|
||||||
albumId: String,
|
albumId: String,
|
||||||
|
|||||||
@@ -241,8 +241,8 @@ internal fun CoroutineScope.playGenreList(
|
|||||||
song: Song,
|
song: Song,
|
||||||
) {
|
) {
|
||||||
launch {
|
launch {
|
||||||
val ids = libraryRepository.genreSongIds(song.sourceId, genre)
|
val ordinal = libraryRepository.genreSongOrdinal(song.sourceId, genre, song.title, song.id)
|
||||||
playbackController.playSongs(song.sourceId, ids, ids.indexOf(song.id).coerceAtLeast(0))
|
playbackController.playGenre(song.sourceId, genre, ordinal)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -252,7 +252,7 @@ internal fun CoroutineScope.playDownloadedList(
|
|||||||
song: Song,
|
song: Song,
|
||||||
) {
|
) {
|
||||||
launch {
|
launch {
|
||||||
val ids = libraryRepository.downloadedSongIds(song.sourceId)
|
val ordinal = libraryRepository.downloadedSongOrdinal(song.sourceId, song.title, song.id)
|
||||||
playbackController.playSongs(song.sourceId, ids, ids.indexOf(song.id).coerceAtLeast(0))
|
playbackController.playDownloaded(song.sourceId, ordinal)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -470,6 +470,8 @@ private fun QueueKind.label(): String =
|
|||||||
QueueKind.Playlist -> "playlist"
|
QueueKind.Playlist -> "playlist"
|
||||||
QueueKind.Song -> "song"
|
QueueKind.Song -> "song"
|
||||||
QueueKind.Artist -> "artist"
|
QueueKind.Artist -> "artist"
|
||||||
|
QueueKind.Genre -> "genre"
|
||||||
|
QueueKind.Downloaded -> "downloads"
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun formatTime(milliseconds: Long): String {
|
private fun formatTime(milliseconds: Long): String {
|
||||||
|
|||||||
@@ -3,16 +3,19 @@ package com.subtracks.data.repo
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.room3.Room
|
import androidx.room3.Room
|
||||||
import androidx.room3.useReaderConnection
|
import androidx.room3.useReaderConnection
|
||||||
|
import androidx.room3.useWriterConnection
|
||||||
import androidx.sqlite.driver.bundled.BundledSQLiteDriver
|
import androidx.sqlite.driver.bundled.BundledSQLiteDriver
|
||||||
import androidx.test.core.app.ApplicationProvider
|
import androidx.test.core.app.ApplicationProvider
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
import com.subtracks.data.db.SubtracksDatabase
|
import com.subtracks.data.db.SubtracksDatabase
|
||||||
import com.subtracks.data.model.Album
|
import com.subtracks.data.model.Album
|
||||||
|
import com.subtracks.data.model.DownloadStatus
|
||||||
import com.subtracks.data.model.Playlist
|
import com.subtracks.data.model.Playlist
|
||||||
import com.subtracks.data.model.PlaylistSong
|
import com.subtracks.data.model.PlaylistSong
|
||||||
import com.subtracks.data.model.QueueEntry
|
import com.subtracks.data.model.QueueEntry
|
||||||
import com.subtracks.data.model.QueueKind
|
import com.subtracks.data.model.QueueKind
|
||||||
import com.subtracks.data.model.Song
|
import com.subtracks.data.model.Song
|
||||||
|
import com.subtracks.data.model.SongDownload
|
||||||
import com.subtracks.data.model.Source
|
import com.subtracks.data.model.Source
|
||||||
import kotlinx.coroutines.test.runTest
|
import kotlinx.coroutines.test.runTest
|
||||||
import org.junit.After
|
import org.junit.After
|
||||||
@@ -55,6 +58,35 @@ class QueueRepositoryTest {
|
|||||||
assertEquals(listOf("s1", "s2", "s3"), resolveAll(snapshot))
|
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
|
@Test
|
||||||
fun rangeLimitsAnEntryToASlice() =
|
fun rangeLimitsAnEntryToASlice() =
|
||||||
runTest {
|
runTest {
|
||||||
@@ -912,4 +944,15 @@ class QueueRepositoryTest {
|
|||||||
genre = null,
|
genre = null,
|
||||||
created = created,
|
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