Add "Refetch" option for albums and artists (#93)

This commit is contained in:
vfsfitvnm
2022-07-09 15:14:02 +02:00
parent 3608c912b5
commit 29025da7e9
6 changed files with 632 additions and 56 deletions

View File

@@ -0,0 +1,530 @@
{
"formatVersion": 1,
"database": {
"version": 13,
"identityHash": "61cd3db93beeafd3ca398be54544c752",
"entities": [
{
"tableName": "Song",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT NOT NULL, `artistsText` TEXT, `durationText` TEXT NOT NULL, `thumbnailUrl` TEXT, `lyrics` TEXT, `likedAt` INTEGER, `totalPlayTimeMs` INTEGER NOT NULL, `loudnessDb` REAL, `contentLength` INTEGER, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "artistsText",
"columnName": "artistsText",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "durationText",
"columnName": "durationText",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "thumbnailUrl",
"columnName": "thumbnailUrl",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "lyrics",
"columnName": "lyrics",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "likedAt",
"columnName": "likedAt",
"affinity": "INTEGER",
"notNull": false
},
{
"fieldPath": "totalPlayTimeMs",
"columnName": "totalPlayTimeMs",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "loudnessDb",
"columnName": "loudnessDb",
"affinity": "REAL",
"notNull": false
},
{
"fieldPath": "contentLength",
"columnName": "contentLength",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "SongPlaylistMap",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`songId` TEXT NOT NULL, `playlistId` INTEGER NOT NULL, `position` INTEGER NOT NULL, PRIMARY KEY(`songId`, `playlistId`), FOREIGN KEY(`songId`) REFERENCES `Song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`playlistId`) REFERENCES `Playlist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "songId",
"columnName": "songId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "playlistId",
"columnName": "playlistId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "position",
"columnName": "position",
"affinity": "INTEGER",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"songId",
"playlistId"
]
},
"indices": [
{
"name": "index_SongPlaylistMap_songId",
"unique": false,
"columnNames": [
"songId"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_SongPlaylistMap_songId` ON `${TABLE_NAME}` (`songId`)"
},
{
"name": "index_SongPlaylistMap_playlistId",
"unique": false,
"columnNames": [
"playlistId"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_SongPlaylistMap_playlistId` ON `${TABLE_NAME}` (`playlistId`)"
}
],
"foreignKeys": [
{
"table": "Song",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"songId"
],
"referencedColumns": [
"id"
]
},
{
"table": "Playlist",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"playlistId"
],
"referencedColumns": [
"id"
]
}
]
},
{
"tableName": "Playlist",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "Artist",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `thumbnailUrl` TEXT, `info` TEXT, `shuffleVideoId` TEXT, `shufflePlaylistId` TEXT, `radioVideoId` TEXT, `radioPlaylistId` TEXT, `timestamp` INTEGER, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "name",
"columnName": "name",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "thumbnailUrl",
"columnName": "thumbnailUrl",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "info",
"columnName": "info",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "shuffleVideoId",
"columnName": "shuffleVideoId",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "shufflePlaylistId",
"columnName": "shufflePlaylistId",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "radioVideoId",
"columnName": "radioVideoId",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "radioPlaylistId",
"columnName": "radioPlaylistId",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "timestamp",
"columnName": "timestamp",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "SongArtistMap",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`songId` TEXT NOT NULL, `artistId` TEXT NOT NULL, PRIMARY KEY(`songId`, `artistId`), FOREIGN KEY(`songId`) REFERENCES `Song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`artistId`) REFERENCES `Artist`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "songId",
"columnName": "songId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "artistId",
"columnName": "artistId",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"songId",
"artistId"
]
},
"indices": [
{
"name": "index_SongArtistMap_songId",
"unique": false,
"columnNames": [
"songId"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_SongArtistMap_songId` ON `${TABLE_NAME}` (`songId`)"
},
{
"name": "index_SongArtistMap_artistId",
"unique": false,
"columnNames": [
"artistId"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_SongArtistMap_artistId` ON `${TABLE_NAME}` (`artistId`)"
}
],
"foreignKeys": [
{
"table": "Song",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"songId"
],
"referencedColumns": [
"id"
]
},
{
"table": "Artist",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"artistId"
],
"referencedColumns": [
"id"
]
}
]
},
{
"tableName": "Album",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT, `thumbnailUrl` TEXT, `year` TEXT, `authorsText` TEXT, `shareUrl` TEXT, `timestamp` INTEGER, PRIMARY KEY(`id`))",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "title",
"columnName": "title",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "thumbnailUrl",
"columnName": "thumbnailUrl",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "year",
"columnName": "year",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "authorsText",
"columnName": "authorsText",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "shareUrl",
"columnName": "shareUrl",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "timestamp",
"columnName": "timestamp",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
},
{
"tableName": "SongAlbumMap",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`songId` TEXT NOT NULL, `albumId` TEXT NOT NULL, `position` INTEGER, PRIMARY KEY(`songId`, `albumId`), FOREIGN KEY(`songId`) REFERENCES `Song`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`albumId`) REFERENCES `Album`(`id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
"fields": [
{
"fieldPath": "songId",
"columnName": "songId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "albumId",
"columnName": "albumId",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "position",
"columnName": "position",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"songId",
"albumId"
]
},
"indices": [
{
"name": "index_SongAlbumMap_songId",
"unique": false,
"columnNames": [
"songId"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_SongAlbumMap_songId` ON `${TABLE_NAME}` (`songId`)"
},
{
"name": "index_SongAlbumMap_albumId",
"unique": false,
"columnNames": [
"albumId"
],
"orders": [],
"createSql": "CREATE INDEX IF NOT EXISTS `index_SongAlbumMap_albumId` ON `${TABLE_NAME}` (`albumId`)"
}
],
"foreignKeys": [
{
"table": "Song",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"songId"
],
"referencedColumns": [
"id"
]
},
{
"table": "Album",
"onDelete": "CASCADE",
"onUpdate": "NO ACTION",
"columns": [
"albumId"
],
"referencedColumns": [
"id"
]
}
]
},
{
"tableName": "SearchQuery",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `query` TEXT NOT NULL)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "query",
"columnName": "query",
"affinity": "TEXT",
"notNull": true
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [
{
"name": "index_SearchQuery_query",
"unique": true,
"columnNames": [
"query"
],
"orders": [],
"createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_SearchQuery_query` ON `${TABLE_NAME}` (`query`)"
}
],
"foreignKeys": []
},
{
"tableName": "QueuedMediaItem",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `mediaItem` BLOB NOT NULL, `position` INTEGER)",
"fields": [
{
"fieldPath": "id",
"columnName": "id",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "mediaItem",
"columnName": "mediaItem",
"affinity": "BLOB",
"notNull": true
},
{
"fieldPath": "position",
"columnName": "position",
"affinity": "INTEGER",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": true,
"columnNames": [
"id"
]
},
"indices": [],
"foreignKeys": []
}
],
"views": [
{
"viewName": "SortedSongPlaylistMap",
"createSql": "CREATE VIEW `${VIEW_NAME}` AS SELECT * FROM SongPlaylistMap ORDER BY position"
}
],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '61cd3db93beeafd3ca398be54544c752')"
]
}
}

View File

@@ -156,6 +156,7 @@ interface Database {
authorsText = null,
thumbnailUrl = null,
shareUrl = null,
timestamp = null,
).also(::insert)
upsert(
@@ -174,7 +175,8 @@ interface Database {
id = artistIds[index],
name = artistName,
thumbnailUrl = null,
info = null
info = null,
timestamp = null,
).also(::insert)
}
}
@@ -212,6 +214,9 @@ interface Database {
@Delete
fun delete(playlist: Playlist)
@Delete
fun delete(playlist: Album)
@Delete
fun delete(songPlaylistMap: SongPlaylistMap)
@@ -249,7 +254,7 @@ interface Database {
views = [
SortedSongPlaylistMap::class
],
version = 12,
version = 13,
exportSchema = true,
autoMigrations = [
AutoMigration(from = 1, to = 2),
@@ -261,6 +266,7 @@ interface Database {
AutoMigration(from = 7, to = 8, spec = DatabaseInitializer.From7To8Migration::class),
AutoMigration(from = 9, to = 10),
AutoMigration(from = 11, to = 12, spec = DatabaseInitializer.From11To12Migration::class),
AutoMigration(from = 12, to = 13),
],
)
@TypeConverters(Converters::class)

View File

@@ -10,5 +10,6 @@ data class Album(
val thumbnailUrl: String? = null,
val year: String? = null,
val authorsText: String? = null,
val shareUrl: String? = null
val shareUrl: String? = null,
val timestamp: Long?
)

View File

@@ -13,4 +13,5 @@ data class Artist(
val shufflePlaylistId: String? = null,
val radioVideoId: String? = null,
val radioPlaylistId: String? = null,
val timestamp: Long?
)

View File

@@ -45,6 +45,9 @@ import it.vfsfitvnm.youtubemusic.YouTube
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.runBlocking
import java.text.DateFormat
import java.util.*
@ExperimentalAnimationApi
@@ -56,32 +59,10 @@ fun AlbumScreen(
val albumResult by remember(browseId) {
Database.album(browseId).map { album ->
album?.takeIf {
album.thumbnailUrl != null
}?.let(Result.Companion::success) ?: YouTube.playlistOrAlbum(browseId)
?.map { youtubeAlbum ->
Album(
id = browseId,
title = youtubeAlbum.title,
thumbnailUrl = youtubeAlbum.thumbnail?.url,
year = youtubeAlbum.year,
authorsText = youtubeAlbum.authors?.joinToString("") { it.name },
shareUrl = youtubeAlbum.url
).also(Database::upsert).also {
youtubeAlbum.withAudioSources().items?.forEachIndexed { position, albumItem ->
albumItem.toMediaItem(browseId, youtubeAlbum)?.let { mediaItem ->
Database.insert(mediaItem)
Database.upsert(
SongAlbumMap(
songId = mediaItem.mediaId,
albumId = browseId,
position = position
)
)
}
}
}
}
album
?.takeIf { album.timestamp != null }
?.let(Result.Companion::success)
?: fetchAlbum(browseId)
}.distinctUntilChanged()
}.collectAsState(initial = null, context = Dispatchers.IO)
@@ -210,6 +191,25 @@ fun AlbumScreen(
}
}
)
MenuEntry(
icon = R.drawable.download,
text = "Refetch",
secondaryText = albumResult?.getOrNull()?.timestamp?.let { timestamp ->
"Last updated on ${DateFormat.getDateTimeInstance().format(Date(timestamp))}"
},
isEnabled = albumResult?.getOrNull() != null,
onClick = {
menuState.hide()
query {
albumResult?.getOrNull()?.let(Database::delete)
runBlocking(Dispatchers.IO) {
fetchAlbum(browseId)
}
}
}
)
}
}
}
@@ -392,3 +392,31 @@ private fun LoadingOrError(
}
}
}
private suspend fun fetchAlbum(browseId: String): Result<Album>? {
return YouTube.playlistOrAlbum(browseId)
?.map { youtubeAlbum ->
Album(
id = browseId,
title = youtubeAlbum.title,
thumbnailUrl = youtubeAlbum.thumbnail?.url,
year = youtubeAlbum.year,
authorsText = youtubeAlbum.authors?.joinToString("") { it.name },
shareUrl = youtubeAlbum.url,
timestamp = System.currentTimeMillis()
).also(Database::upsert).also {
youtubeAlbum.withAudioSources().items?.forEachIndexed { position, albumItem ->
albumItem.toMediaItem(browseId, youtubeAlbum)?.let { mediaItem ->
Database.insert(mediaItem)
Database.upsert(
SongAlbumMap(
songId = mediaItem.mediaId,
albumId = browseId,
position = position
)
)
}
}
}
}
}

View File

@@ -82,21 +82,10 @@ fun ArtistScreen(
val artistResult by remember(browseId) {
Database.artist(browseId).map { artist ->
artist?.takeIf {
artist.shufflePlaylistId != null
}?.let(Result.Companion::success) ?: YouTube.artist(browseId)
?.map { youtubeArtist ->
Artist(
id = browseId,
name = youtubeArtist.name,
thumbnailUrl = youtubeArtist.thumbnail?.url,
info = youtubeArtist.description,
shuffleVideoId = youtubeArtist.shuffleEndpoint?.videoId,
shufflePlaylistId = youtubeArtist.shuffleEndpoint?.playlistId,
radioVideoId = youtubeArtist.radioEndpoint?.videoId,
radioPlaylistId = youtubeArtist.radioEndpoint?.playlistId,
).also(Database::upsert)
}
artist
?.takeIf { artist.timestamp != null }
?.let(Result.Companion::success)
?: fetchArtist(browseId)
}.distinctUntilChanged()
}.collectAsState(initial = null, context = Dispatchers.IO)
@@ -139,17 +128,6 @@ fun ArtistScreen(
contentDescription = null,
modifier = Modifier
.clip(CircleShape)
.clickable {
query {
runBlocking {
Database
.artist(browseId)
.first()
?.copy(shufflePlaylistId = null)
?.let(Database::update)
}
}
}
.size(Dimensions.thumbnails.artist)
)
@@ -177,6 +155,12 @@ fun ArtistScreen(
playlistId = artist.shufflePlaylistId
)
)
query {
runBlocking {
fetchArtist(browseId)
}
}
}
.shadow(elevation = 2.dp, shape = CircleShape)
.background(
@@ -200,6 +184,12 @@ fun ArtistScreen(
playlistId = artist.radioPlaylistId
)
)
query {
runBlocking {
fetchArtist(browseId)
}
}
}
.shadow(elevation = 2.dp, shape = CircleShape)
.background(
@@ -285,7 +275,10 @@ fun ArtistScreen(
artistResult?.getOrNull()?.info?.let { description ->
item {
TextCard {
TextCard(
modifier = Modifier
.fillMaxWidth()
) {
Title(text = "Information")
Text(text = description)
}
@@ -329,3 +322,20 @@ private fun LoadingOrError(
}
}
}
private suspend fun fetchArtist(browseId: String): Result<Artist>? {
return YouTube.artist(browseId)
?.map { youtubeArtist ->
Artist(
id = browseId,
name = youtubeArtist.name,
thumbnailUrl = youtubeArtist.thumbnail?.url,
info = youtubeArtist.description,
shuffleVideoId = youtubeArtist.shuffleEndpoint?.videoId,
shufflePlaylistId = youtubeArtist.shuffleEndpoint?.playlistId,
radioVideoId = youtubeArtist.radioEndpoint?.videoId,
radioPlaylistId = youtubeArtist.radioEndpoint?.playlistId,
timestamp = System.currentTimeMillis()
).also(Database::upsert)
}
}