Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,368 changes: 1,368 additions & 0 deletions app/schemas/com.metrolist.music.db.InternalDatabase/38.json

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion app/src/main/kotlin/com/metrolist/music/db/MusicDatabase.kt
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ class MusicDatabase(
SortedSongAlbumMap::class,
PlaylistSongMapPreview::class,
],
version = 37,
version = 38,
exportSchema = true,
autoMigrations = [
AutoMigration(from = 2, to = 3),
Expand Down Expand Up @@ -156,6 +156,7 @@ class MusicDatabase(
AutoMigration(from = 34, to = 35),
AutoMigration(from = 35, to = 36, spec = Migration35To36::class),
AutoMigration(from = 36, to = 37),
AutoMigration(from = 37, to = 38),
],
)
@TypeConverters(Converters::class)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@ data class ArtistEntity(
@ColumnInfo(name = "isLocal", defaultValue = false.toString())
val isLocal: Boolean = false,
@ColumnInfo(name = "isPodcastChannel", defaultValue = false.toString())
val isPodcastChannel: Boolean = false
val isPodcastChannel: Boolean = false,
@ColumnInfo(name = "cachedPageJson")
val cachedPageJson: String? = null
) {
val isYouTubeArtist: Boolean
get() = id.startsWith("UC") || id.startsWith("FEmusic_library_privately_owned_artist")
Expand Down
278 changes: 278 additions & 0 deletions app/src/main/kotlin/com/metrolist/music/db/entities/ArtistPageCache.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,278 @@
package com.metrolist.music.db.entities

import com.metrolist.innertube.models.ArtistItem
import com.metrolist.innertube.models.AlbumItem
import com.metrolist.innertube.models.EpisodeItem
import com.metrolist.innertube.models.PlaylistItem
import com.metrolist.innertube.models.PodcastItem
import com.metrolist.innertube.models.SongItem
import com.metrolist.innertube.models.YTItem
import com.metrolist.innertube.pages.ArtistSection
import kotlinx.serialization.Serializable
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json

private val json = Json {
ignoreUnknownKeys = true
explicitNulls = false
encodeDefaults = true
}

@Serializable
data class CachedArtistPage(
val artist: CachedArtistItem? = null,
val sections: List<CachedSection>,
val description: String? = null,
val subscriberCountText: String? = null,
val monthlyListenerCount: String? = null,
val isSubscribed: Boolean = false,
)

@Serializable
data class CachedArtistItem(
val id: String,
val title: String,
val thumbnail: String? = null,
val channelId: String? = null,
val isProfile: Boolean = false,
)

@Serializable
data class CachedSection(
val title: String,
val items: List<CachedItem>,
val moreBrowseId: String? = null,
val moreParams: String? = null,
)

@Serializable
data class CachedItem(
val id: String,
val title: String,
val thumbnail: String,
val type: String,
val explicit: Boolean = false,
val artists: List<CachedArtist> = emptyList(),
val album: CachedAlbum? = null,
val duration: Int? = null,
val year: Int? = null,
val songCountText: String? = null,
val author: CachedArtist? = null,
val channelId: String? = null,
val browseId: String? = null,
val playlistId: String? = null,
val authorAvatarUrl: String? = null,
val episodeCountText: String? = null,
val videoId: String? = null,
)

@Serializable
data class CachedArtist(val name: String, val id: String? = null)

@Serializable
data class CachedAlbum(val name: String, val id: String)

fun serializeArtistPage(
sections: List<ArtistSection>,
description: String?,
subscriberCountText: String?,
monthlyListenerCount: String?,
isSubscribed: Boolean,
artist: com.metrolist.innertube.models.ArtistItem? = null,
): String {
val cached = CachedArtistPage(
artist = artist?.let { CachedArtistItem(it.id, it.title, it.thumbnail, it.channelId, it.isProfile) },
sections = sections.map { section ->
CachedSection(
title = section.title,
items = section.items.map { item -> item.toCachedItem() },
moreBrowseId = section.moreEndpoint?.browseId,
moreParams = section.moreEndpoint?.params,
)
},
description = description,
subscriberCountText = subscriberCountText,
monthlyListenerCount = monthlyListenerCount,
isSubscribed = isSubscribed,
)
return json.encodeToString(cached)
}

fun deserializeArtistPage(jsonString: String): CachedArtistPage {
return json.decodeFromString(jsonString)
}

fun CachedArtistPage.toArtistPage(): com.metrolist.innertube.pages.ArtistPage {
return com.metrolist.innertube.pages.ArtistPage(
artist = artist?.let { cached ->
com.metrolist.innertube.models.ArtistItem(
id = cached.id,
title = cached.title,
thumbnail = cached.thumbnail,
channelId = cached.channelId,
playEndpoint = null,
shuffleEndpoint = null,
radioEndpoint = null,
isProfile = cached.isProfile,
)
} ?: com.metrolist.innertube.models.ArtistItem(
id = "",
title = "",
thumbnail = null,
shuffleEndpoint = null,
radioEndpoint = null,
),
sections = sections.map { section ->
ArtistSection(
title = section.title,
items = section.items.map { it.toYTItem() },
moreEndpoint = section.moreBrowseId?.let { browseId ->
com.metrolist.innertube.models.BrowseEndpoint(
browseId = browseId,
params = section.moreParams,
)
},
)
},
description = description,
subscriberCountText = subscriberCountText,
monthlyListenerCount = monthlyListenerCount,
isSubscribed = isSubscribed,
)
}

private fun CachedItem.toYTItem(): YTItem {
return when (type) {
"song" -> SongItem(
id = id,
title = title,
thumbnail = thumbnail,
explicit = explicit,
artists = artists.map { com.metrolist.innertube.models.Artist(it.name, it.id) },
album = album?.let { com.metrolist.innertube.models.Album(it.name, it.id) },
duration = duration ?: 0,
setVideoId = videoId,
)
"album" -> AlbumItem(
browseId = browseId ?: id,
playlistId = playlistId ?: id,
id = id,
title = title,
thumbnail = thumbnail,
explicit = explicit,
artists = artists.map { com.metrolist.innertube.models.Artist(it.name, it.id) }.ifEmpty { null },
year = year,
)
"playlist" -> PlaylistItem(
id = id,
title = title,
thumbnail = thumbnail.takeIf { it.isNotBlank() },
author = author?.let { com.metrolist.innertube.models.Artist(it.name, it.id) },
songCountText = songCountText,
playEndpoint = null,
shuffleEndpoint = null,
radioEndpoint = null,
authorAvatarUrl = authorAvatarUrl,
)
"artist" -> ArtistItem(
id = id,
title = title,
thumbnail = thumbnail.takeIf { it.isNotBlank() },
channelId = channelId,
playEndpoint = null,
shuffleEndpoint = null,
radioEndpoint = null,
)
"podcast" -> PodcastItem(
id = id,
title = title,
thumbnail = thumbnail.takeIf { it.isNotBlank() },
author = author?.let { com.metrolist.innertube.models.Artist(it.name, it.id) },
episodeCountText = episodeCountText,
playEndpoint = null,
shuffleEndpoint = null,
channelId = channelId,
)
"episode" -> EpisodeItem(
id = id,
title = title,
thumbnail = thumbnail,
explicit = explicit,
author = author?.let { com.metrolist.innertube.models.Artist(it.name, it.id) },
podcast = album?.let { com.metrolist.innertube.models.Album(it.name, it.id) },
duration = duration,
endpoint = null,
)
else -> SongItem(
id = id,
title = title,
thumbnail = thumbnail,
explicit = explicit,
artists = artists.map { com.metrolist.innertube.models.Artist(it.name, it.id) },
album = album?.let { com.metrolist.innertube.models.Album(it.name, it.id) },
duration = duration ?: 0,
)
}
}

private fun YTItem.toCachedItem(): CachedItem {
return when (this) {
is SongItem -> CachedItem(
id = id,
title = title,
thumbnail = thumbnail,
type = "song",
explicit = explicit,
artists = artists.map { CachedArtist(it.name, it.id) },
album = album?.let { CachedAlbum(it.name, it.id) },
duration = duration,
videoId = setVideoId,
)
is AlbumItem -> CachedItem(
id = id,
title = title,
thumbnail = thumbnail,
type = "album",
explicit = explicit,
artists = artists?.map { CachedArtist(it.name, it.id) } ?: emptyList(),
year = year,
browseId = browseId,
playlistId = playlistId,
)
is PlaylistItem -> CachedItem(
id = id,
title = title,
thumbnail = thumbnail ?: "",
type = "playlist",
author = author?.let { CachedArtist(it.name, it.id) },
songCountText = songCountText,
authorAvatarUrl = authorAvatarUrl,
)
is ArtistItem -> CachedItem(
id = id,
title = title,
thumbnail = thumbnail ?: "",
type = "artist",
channelId = channelId,
)
is PodcastItem -> CachedItem(
id = id,
title = title,
thumbnail = thumbnail ?: "",
type = "podcast",
author = author?.let { CachedArtist(it.name, it.id) },
episodeCountText = episodeCountText,
channelId = channelId,
)
is EpisodeItem -> CachedItem(
id = id,
title = title,
thumbnail = thumbnail,
type = "episode",
explicit = explicit,
author = author?.let { CachedArtist(it.name, it.id) },
album = podcast?.let { CachedAlbum(it.name, it.id) },
duration = duration,
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import com.metrolist.music.models.toMediaMetadata
import com.metrolist.music.playback.PlayerConnection
import com.metrolist.music.playback.queues.YouTubeQueue
import com.metrolist.music.utils.dataStore
import com.metrolist.music.utils.getArtistSeparator
import com.metrolist.music.utils.joinToArtistString
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
Expand Down Expand Up @@ -1719,11 +1721,11 @@ class ListenTogetherManager
// Use a default duration of 3 minutes if duration is 0 or negative
val durationMs = if (metadata.duration > 0) metadata.duration.toLong() * 1000 else 180000L

val trackInfo =
TrackInfo(
id = metadata.id,
title = metadata.title,
artist = metadata.artists.joinToString(", ") { it.name },
val trackInfo =
TrackInfo(
id = metadata.id,
title = metadata.title,
artist = metadata.artists.joinToArtistString(getArtistSeparator(context)) { it.name },
album = metadata.album?.title,
duration = durationMs,
thumbnail = metadata.thumbnailUrl,
Expand Down Expand Up @@ -1820,10 +1822,10 @@ class ListenTogetherManager
private fun androidx.media3.common.Timeline.Window.toTrackInfo(): TrackInfo {
val metadata = mediaItem.metadata ?: return TrackInfo("unknown", "Unknown", "Unknown", "", 0, "")
val durationMs = if (metadata.duration > 0) metadata.duration.toLong() * 1000 else 180000L
return TrackInfo(
id = metadata.id,
title = metadata.title,
artist = metadata.artists.joinToString(", ") { it.name },
return TrackInfo(
id = metadata.id,
title = metadata.title,
artist = metadata.artists.joinToArtistString(getArtistSeparator(context)) { it.name },
album = metadata.album?.title,
duration = durationMs,
thumbnail = metadata.thumbnailUrl,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ import com.metrolist.music.extensions.toggleRepeatMode
import com.metrolist.music.models.toMediaMetadata
import com.metrolist.music.utils.dataStore
import com.metrolist.music.utils.get
import com.metrolist.music.utils.getArtistSeparator
import com.metrolist.music.utils.joinToArtistString
import com.metrolist.music.utils.reportException
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.CoroutineScope
Expand Down Expand Up @@ -422,8 +424,8 @@ constructor(
.setMediaMetadata(
MediaMetadata.Builder()
.setTitle(songItem.title)
.setSubtitle(songItem.artists.joinToString(", ") { it.name })
.setArtist(songItem.artists.joinToString(", ") { it.name })
.setSubtitle(songItem.artists.joinToArtistString(getArtistSeparator(context)) { it.name })
.setArtist(songItem.artists.joinToArtistString(getArtistSeparator(context)) { it.name })
.setArtworkUri(songItem.thumbnail.toUri())
.setIsPlayable(true)
.setIsBrowsable(false)
Expand Down Expand Up @@ -551,8 +553,8 @@ constructor(
.setMediaMetadata(
MediaMetadata.Builder()
.setTitle(songItem.title)
.setSubtitle(songItem.artists.joinToString(", ") { it.name })
.setArtist(songItem.artists.joinToString(", ") { it.name })
.setSubtitle(songItem.artists.joinToArtistString(getArtistSeparator(context)) { it.name })
.setArtist(songItem.artists.joinToArtistString(getArtistSeparator(context)) { it.name })
.setArtworkUri(songItem.thumbnail.toUri())
.setIsPlayable(true)
.setIsBrowsable(true)
Expand Down Expand Up @@ -826,12 +828,12 @@ constructor(
.Builder()
.setMediaId("$path/$id")
.setMediaMetadata(
MediaMetadata
.Builder()
.setTitle(song.title)
.setSubtitle(artists.joinToString { it.name })
.setArtist(artists.joinToString { it.name })
.setArtworkData(artworkBytes, MediaMetadata.PICTURE_TYPE_ILLUSTRATION)
MediaMetadata
.Builder()
.setTitle(song.title)
.setSubtitle(artists.joinToArtistString(getArtistSeparator(context)) { it.name })
.setArtist(artists.joinToArtistString(getArtistSeparator(context)) { it.name })
.setArtworkData(artworkBytes, MediaMetadata.PICTURE_TYPE_ILLUSTRATION)
.setIsPlayable(isPlayable)
.setIsBrowsable(isBrowsable)
.setMediaType(MediaMetadata.MEDIA_TYPE_MUSIC)
Expand Down
Loading
Loading