mirror of
https://github.com/austinried/subtracks.git
synced 2026-11-10 15:11:58 +01:00
keep playback running when rebuilding the offline window, and order downloads newest-first
This commit is contained in:
+1584
File diff suppressed because it is too large
Load Diff
@@ -709,7 +709,7 @@ interface LibraryDao {
|
||||
"LEFT JOIN albums ON albums.sourceId = songs.sourceId AND albums.id = songs.albumId " +
|
||||
"JOIN song_downloads d ON d.sourceId = songs.sourceId AND d.songId = songs.id " +
|
||||
"WHERE songs.sourceId = :sourceId AND d.status = 'Completed' " +
|
||||
"ORDER BY songs.title COLLATE NOCASE, songs.id",
|
||||
"ORDER BY d.downloadedAt DESC, songs.title COLLATE NOCASE, songs.id",
|
||||
)
|
||||
fun downloadedSongs(sourceId: Long): Flow<List<AlbumSongItem>>
|
||||
|
||||
@@ -717,7 +717,7 @@ interface LibraryDao {
|
||||
"SELECT songs.id FROM songs " +
|
||||
"JOIN song_downloads d ON d.sourceId = songs.sourceId AND d.songId = songs.id " +
|
||||
"WHERE songs.sourceId = :sourceId AND d.status = 'Completed' " +
|
||||
"ORDER BY songs.title COLLATE NOCASE, songs.id",
|
||||
"ORDER BY d.downloadedAt DESC, songs.title COLLATE NOCASE, songs.id",
|
||||
)
|
||||
suspend fun downloadedSongIds(sourceId: Long): List<String>
|
||||
|
||||
@@ -726,10 +726,31 @@ interface LibraryDao {
|
||||
"LEFT JOIN albums ON albums.sourceId = songs.sourceId AND albums.id = songs.albumId " +
|
||||
"JOIN song_downloads d ON d.sourceId = songs.sourceId AND d.songId = songs.id " +
|
||||
"WHERE songs.sourceId = :sourceId AND d.status = 'Completed' " +
|
||||
"ORDER BY songs.title COLLATE NOCASE, songs.id",
|
||||
"ORDER BY d.downloadedAt DESC, songs.title COLLATE NOCASE, songs.id",
|
||||
)
|
||||
fun homeDownloadedSongs(sourceId: Long): PagingSource<Int, AlbumSongItem>
|
||||
|
||||
@Query(
|
||||
"SELECT albums.* FROM albums " +
|
||||
"JOIN songs ON songs.sourceId = albums.sourceId AND songs.albumId = albums.id " +
|
||||
"JOIN song_downloads d ON d.sourceId = songs.sourceId AND d.songId = songs.id AND d.status = 'Completed' " +
|
||||
"WHERE albums.sourceId = :sourceId " +
|
||||
"GROUP BY albums.sourceId, albums.id " +
|
||||
"ORDER BY MAX(d.downloadedAt) DESC, albums.name COLLATE NOCASE, albums.id",
|
||||
)
|
||||
fun downloadedAlbums(sourceId: Long): PagingSource<Int, Album>
|
||||
|
||||
@Query(
|
||||
"SELECT artists.* FROM artists " +
|
||||
"JOIN albums ON albums.sourceId = artists.sourceId AND albums.artistId = artists.id " +
|
||||
"JOIN songs ON songs.sourceId = albums.sourceId AND songs.albumId = albums.id " +
|
||||
"JOIN song_downloads d ON d.sourceId = songs.sourceId AND d.songId = songs.id AND d.status = 'Completed' " +
|
||||
"WHERE artists.sourceId = :sourceId " +
|
||||
"GROUP BY artists.sourceId, artists.id " +
|
||||
"ORDER BY MAX(d.downloadedAt) DESC, artists.name COLLATE NOCASE, artists.id",
|
||||
)
|
||||
fun downloadedArtists(sourceId: Long): PagingSource<Int, Artist>
|
||||
|
||||
@Query(
|
||||
"SELECT songs.*, albums.coverArt AS coverArt FROM song_genres " +
|
||||
"JOIN songs ON songs.sourceId = song_genres.sourceId AND songs.id = song_genres.songId " +
|
||||
@@ -768,13 +789,15 @@ interface LibraryDao {
|
||||
|
||||
@Query(
|
||||
"SELECT COUNT(*) FROM song_downloads d " +
|
||||
"JOIN songs ON songs.sourceId = d.sourceId AND songs.id = d.songId " +
|
||||
"JOIN songs s ON s.sourceId = d.sourceId AND s.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))",
|
||||
"AND (d.downloadedAt > (SELECT downloadedAt FROM song_downloads WHERE sourceId = :sourceId AND songId = :id) " +
|
||||
"OR (d.downloadedAt = (SELECT downloadedAt FROM song_downloads WHERE sourceId = :sourceId AND songId = :id) " +
|
||||
"AND (s.title COLLATE NOCASE < (SELECT title FROM songs WHERE sourceId = :sourceId AND id = :id) " +
|
||||
"OR (s.title COLLATE NOCASE = (SELECT title FROM songs WHERE sourceId = :sourceId AND id = :id) AND s.id < :id))))",
|
||||
)
|
||||
suspend fun downloadedSongOrdinal(
|
||||
sourceId: Long,
|
||||
title: String,
|
||||
id: String,
|
||||
): Long
|
||||
|
||||
|
||||
@@ -352,6 +352,15 @@ val MIGRATION_23_24 =
|
||||
}
|
||||
}
|
||||
|
||||
val MIGRATION_24_25 =
|
||||
object : Migration(24, 25) {
|
||||
override suspend fun migrate(connection: SQLiteConnection) {
|
||||
connection.execSQL(
|
||||
"ALTER TABLE `song_downloads` ADD COLUMN `downloadedAt` INTEGER NOT NULL DEFAULT 0",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Mirrors the FTS5 table and content-sync triggers Room generates for the @Fts5 entities.
|
||||
private suspend fun createSearchIndex(
|
||||
connection: SQLiteConnection,
|
||||
@@ -409,4 +418,5 @@ val MIGRATIONS: Array<Migration> =
|
||||
MIGRATION_21_22,
|
||||
MIGRATION_22_23,
|
||||
MIGRATION_23_24,
|
||||
MIGRATION_24_25,
|
||||
)
|
||||
|
||||
@@ -259,7 +259,7 @@ interface QueueDao {
|
||||
"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",
|
||||
"ORDER BY d.downloadedAt DESC, songs.title COLLATE NOCASE, songs.id LIMIT :limit OFFSET :offset",
|
||||
)
|
||||
suspend fun downloadedSongs(
|
||||
sourceId: Long,
|
||||
@@ -271,7 +271,7 @@ interface QueueDao {
|
||||
"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",
|
||||
"ORDER BY d.downloadedAt DESC, songs.title COLLATE NOCASE, songs.id",
|
||||
)
|
||||
suspend fun downloadedSongIds(sourceId: Long): List<String>
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ import com.subtracks.data.model.UpNextEntry
|
||||
ArtworkSeed::class,
|
||||
SongDownload::class,
|
||||
],
|
||||
version = 24,
|
||||
version = 25,
|
||||
exportSchema = true,
|
||||
)
|
||||
@ColumnTypeConverters(QueueKindConverter::class, DownloadStatusConverter::class)
|
||||
|
||||
@@ -35,6 +35,7 @@ data class SongDownload(
|
||||
@ColumnInfo(defaultValue = "0") val bytes: Long = 0,
|
||||
@ColumnInfo(defaultValue = "0") val total: Long = 0,
|
||||
val error: String? = null,
|
||||
@ColumnInfo(defaultValue = "0") val downloadedAt: Long = 0,
|
||||
) {
|
||||
val progress: Float?
|
||||
get() = total.takeIf { it > 0 }?.let { (bytes.toFloat() / it).coerceIn(0f, 1f) }
|
||||
|
||||
@@ -555,6 +555,12 @@ class DownloadRepository(
|
||||
status == DownloadStatus.Failed -> "Download failed"
|
||||
else -> null
|
||||
},
|
||||
downloadedAt =
|
||||
if (status == DownloadStatus.Completed && row.downloadedAt == 0L) {
|
||||
System.currentTimeMillis()
|
||||
} else {
|
||||
row.downloadedAt
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -96,6 +96,10 @@ class LibraryRepository(
|
||||
|
||||
fun downloadedSongsPage(sourceId: Long): Flow<PagingData<AlbumSongItem>> = pager(60) { db.libraryDao().homeDownloadedSongs(sourceId) }
|
||||
|
||||
fun downloadedAlbumsPage(sourceId: Long): Flow<PagingData<Album>> = pager(40) { db.libraryDao().downloadedAlbums(sourceId) }
|
||||
|
||||
fun downloadedArtistsPage(sourceId: Long): Flow<PagingData<Artist>> = pager(60) { db.libraryDao().downloadedArtists(sourceId) }
|
||||
|
||||
fun songsByGenre(
|
||||
sourceId: Long,
|
||||
genre: String,
|
||||
@@ -115,9 +119,8 @@ class LibraryRepository(
|
||||
|
||||
suspend fun downloadedSongOrdinal(
|
||||
sourceId: Long,
|
||||
title: String,
|
||||
id: String,
|
||||
): Long = db.libraryDao().downloadedSongOrdinal(sourceId, title, id)
|
||||
): Long = db.libraryDao().downloadedSongOrdinal(sourceId, id)
|
||||
|
||||
fun albumsByDecade(
|
||||
sourceId: Long,
|
||||
|
||||
@@ -1241,25 +1241,29 @@ class PlaybackController(
|
||||
window: List<QueueWindowItem>,
|
||||
position: Long,
|
||||
) {
|
||||
if (offline) {
|
||||
loadWindow(position, autoplay = player?.playWhenReady == true, startPositionMs = player?.currentPositionMs ?: 0)
|
||||
val player = player ?: return
|
||||
downloads.awaitLoaded()
|
||||
val ordered =
|
||||
if (offline) window.filter { downloads.localUri(it.item.song.id) != null } else window
|
||||
val index = ordered.indexOfFirst { it.position == position }
|
||||
if (index < 0) {
|
||||
loadWindow(position, autoplay = player.playWhenReady, startPositionMs = player.currentPositionMs)
|
||||
return
|
||||
}
|
||||
val player = player ?: return
|
||||
val index = window.indexOfFirst { it.position == position }
|
||||
if (index < 0) return
|
||||
windowPositions = LongArray(ordered.size) { ordered[it].position }
|
||||
windowFiltered = offline
|
||||
windowStart = ordered.first().position
|
||||
windowEnd = ordered.last().position
|
||||
lastPosition = position
|
||||
updating = true
|
||||
try {
|
||||
repeat(player.currentIndex) { player.removeFirst() }
|
||||
while (player.itemCount > 1) player.removeLast()
|
||||
for (i in index - 1 downTo 0) player.addFirst(window[i].item.toQueueItem())
|
||||
for (i in index + 1 until window.size) player.addLast(window[i].item.toQueueItem())
|
||||
for (i in index - 1 downTo 0) player.addFirst(ordered[i].item.toQueueItem())
|
||||
for (i in index + 1 until ordered.size) player.addLast(ordered[i].item.toQueueItem())
|
||||
} finally {
|
||||
updating = false
|
||||
}
|
||||
windowStart = window.first().position
|
||||
windowEnd = window.last().position
|
||||
lastPosition = position
|
||||
}
|
||||
|
||||
private fun currentPosition(): Long? {
|
||||
|
||||
@@ -10,10 +10,7 @@ import com.subtracks.data.model.Artist
|
||||
import com.subtracks.data.model.CoverArtRef
|
||||
import com.subtracks.data.model.Playlist
|
||||
import com.subtracks.data.model.Song
|
||||
import com.subtracks.data.prefs.AlbumSort
|
||||
import com.subtracks.data.prefs.ArtistSort
|
||||
import com.subtracks.data.prefs.PlaylistSort
|
||||
import com.subtracks.data.prefs.StarredFilter
|
||||
import com.subtracks.data.repo.LibraryRepository
|
||||
import com.subtracks.data.repo.SourceRepository
|
||||
import com.subtracks.playback.PlaybackController
|
||||
@@ -57,14 +54,7 @@ class HomeListViewModel(
|
||||
.flatMapLatest { id ->
|
||||
when {
|
||||
request.downloaded == OfflineListKind.Albums -> {
|
||||
libraryRepository.albums(
|
||||
sourceId = id,
|
||||
sort = AlbumSort.Added,
|
||||
descending = false,
|
||||
starred = StarredFilter.Any,
|
||||
search = "",
|
||||
downloaded = true,
|
||||
)
|
||||
libraryRepository.downloadedAlbumsPage(id)
|
||||
}
|
||||
|
||||
request.decade != null -> {
|
||||
@@ -107,14 +97,7 @@ class HomeListViewModel(
|
||||
|
||||
else -> {
|
||||
if (request.downloaded == OfflineListKind.Artists) {
|
||||
libraryRepository.artists(
|
||||
sourceId = id,
|
||||
sort = ArtistSort.Name,
|
||||
descending = false,
|
||||
starred = StarredFilter.Any,
|
||||
search = "",
|
||||
downloaded = true,
|
||||
)
|
||||
libraryRepository.downloadedArtistsPage(id)
|
||||
} else {
|
||||
flowOf(PagingData.empty<Artist>())
|
||||
}
|
||||
|
||||
@@ -10,10 +10,7 @@ import com.subtracks.data.model.Artist
|
||||
import com.subtracks.data.model.CoverArtRef
|
||||
import com.subtracks.data.model.Playlist
|
||||
import com.subtracks.data.model.Song
|
||||
import com.subtracks.data.prefs.AlbumSort
|
||||
import com.subtracks.data.prefs.ArtistSort
|
||||
import com.subtracks.data.prefs.PlaylistSort
|
||||
import com.subtracks.data.prefs.StarredFilter
|
||||
import com.subtracks.data.repo.LibraryRepository
|
||||
import com.subtracks.data.repo.SourceRepository
|
||||
import com.subtracks.data.repo.rediscoverCutoff
|
||||
@@ -66,30 +63,10 @@ class HomeViewModel(
|
||||
private val activeSource = libraryRepository.activeSourceId.filterNotNull()
|
||||
|
||||
val downloadedAlbums: Flow<PagingData<Album>> =
|
||||
activeSource
|
||||
.flatMapLatest { id ->
|
||||
libraryRepository.albums(
|
||||
sourceId = id,
|
||||
sort = AlbumSort.Added,
|
||||
descending = false,
|
||||
starred = StarredFilter.Any,
|
||||
search = "",
|
||||
downloaded = true,
|
||||
)
|
||||
}.cachedIn(viewModelScope)
|
||||
activeSource.flatMapLatest { libraryRepository.downloadedAlbumsPage(it) }.cachedIn(viewModelScope)
|
||||
|
||||
val downloadedArtists: Flow<PagingData<Artist>> =
|
||||
activeSource
|
||||
.flatMapLatest { id ->
|
||||
libraryRepository.artists(
|
||||
sourceId = id,
|
||||
sort = ArtistSort.Name,
|
||||
descending = false,
|
||||
starred = StarredFilter.Any,
|
||||
search = "",
|
||||
downloaded = true,
|
||||
)
|
||||
}.cachedIn(viewModelScope)
|
||||
activeSource.flatMapLatest { libraryRepository.downloadedArtistsPage(it) }.cachedIn(viewModelScope)
|
||||
|
||||
val downloadedPlaylists: Flow<PagingData<Playlist>> =
|
||||
activeSource
|
||||
@@ -252,7 +229,7 @@ internal fun CoroutineScope.playDownloadedList(
|
||||
song: Song,
|
||||
) {
|
||||
launch {
|
||||
val ordinal = libraryRepository.downloadedSongOrdinal(song.sourceId, song.title, song.id)
|
||||
val ordinal = libraryRepository.downloadedSongOrdinal(song.sourceId, song.id)
|
||||
playbackController.playDownloaded(song.sourceId, ordinal)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -217,6 +217,29 @@ class LibraryHomeDaoTest {
|
||||
assertEquals(page.map { it.song.id }, dao.songsByGenreIds(sourceId, "Rock"))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun downloadedSongsOrderByNewestDownload() =
|
||||
runTest {
|
||||
val sourceId = source()
|
||||
val dao = db.libraryDao()
|
||||
dao.upsertAlbums(listOf(album(sourceId, "al1", "Album", coverArt = "cover-1")))
|
||||
dao.upsertSongs(
|
||||
listOf(
|
||||
song(sourceId, "s1", "One"),
|
||||
song(sourceId, "s2", "Two"),
|
||||
song(sourceId, "s3", "Three"),
|
||||
),
|
||||
)
|
||||
val downloads = db.downloadDao()
|
||||
downloads.upsert(SongDownload(sourceId, "s1", DownloadStatus.Completed, downloadedAt = 100))
|
||||
downloads.upsert(SongDownload(sourceId, "s3", DownloadStatus.Completed, downloadedAt = 300))
|
||||
downloads.upsert(SongDownload(sourceId, "s2", DownloadStatus.Completed, downloadedAt = 200))
|
||||
|
||||
assertEquals(listOf("s3", "s2", "s1"), dao.downloadedSongs(sourceId).first().map { it.song.id })
|
||||
assertEquals(listOf("s3", "s2", "s1"), dao.downloadedSongIds(sourceId))
|
||||
assertEquals(2L, dao.downloadedSongOrdinal(sourceId, "s1"))
|
||||
}
|
||||
|
||||
private suspend fun insertGenre(
|
||||
sourceId: Long,
|
||||
songId: String,
|
||||
|
||||
Reference in New Issue
Block a user