This commit was merged in pull request #92.
This commit is contained in:
Vendored
+5
@@ -14,6 +14,11 @@ import kotlinx.datetime.Instant
|
|||||||
* - "rediscover_artists"
|
* - "rediscover_artists"
|
||||||
* - "most_played_tracks"
|
* - "most_played_tracks"
|
||||||
* - "last_played_artists"
|
* - "last_played_artists"
|
||||||
|
* - "you_might_like_albums"
|
||||||
|
* - "you_might_like_artists"
|
||||||
|
*
|
||||||
|
* New `section` values need no schema change — the column stores the
|
||||||
|
* string verbatim and the DAO keys on it.
|
||||||
*
|
*
|
||||||
* `entityType` is "album" | "artist" | "track" — dispatches hydration
|
* `entityType` is "album" | "artist" | "track" — dispatches hydration
|
||||||
* to the right per-entity endpoint when a tile is rendered.
|
* to the right per-entity endpoint when a tile is rendered.
|
||||||
|
|||||||
@@ -39,6 +39,9 @@ import javax.inject.Singleton
|
|||||||
* pre-warms the top artists via [HomeArtistPrewarmer].
|
* pre-warms the top artists via [HomeArtistPrewarmer].
|
||||||
*/
|
*/
|
||||||
@Singleton
|
@Singleton
|
||||||
|
// Per-section observe accessors (one per Home row) inflate the function
|
||||||
|
// count past detekt's 11 default — accessor density, not complexity.
|
||||||
|
@Suppress("TooManyFunctions")
|
||||||
class HomeRepository @Inject constructor(
|
class HomeRepository @Inject constructor(
|
||||||
private val homeIndexDao: CachedHomeIndexDao,
|
private val homeIndexDao: CachedHomeIndexDao,
|
||||||
private val metadataProvider: MetadataProvider,
|
private val metadataProvider: MetadataProvider,
|
||||||
@@ -76,6 +79,12 @@ class HomeRepository @Inject constructor(
|
|||||||
fun observeLastPlayedArtists(): Flow<List<HomeTile<ArtistRef>>> =
|
fun observeLastPlayedArtists(): Flow<List<HomeTile<ArtistRef>>> =
|
||||||
observeArtistSection(SECTION_LAST_PLAYED_ARTISTS)
|
observeArtistSection(SECTION_LAST_PLAYED_ARTISTS)
|
||||||
|
|
||||||
|
fun observeYouMightLikeAlbums(): Flow<List<HomeTile<AlbumRef>>> =
|
||||||
|
observeAlbumSection(SECTION_YOU_MIGHT_LIKE_ALBUMS)
|
||||||
|
|
||||||
|
fun observeYouMightLikeArtists(): Flow<List<HomeTile<ArtistRef>>> =
|
||||||
|
observeArtistSection(SECTION_YOU_MIGHT_LIKE_ARTISTS)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Pulls /api/home/index, replaces each cached_home_index section,
|
* Pulls /api/home/index, replaces each cached_home_index section,
|
||||||
* and pre-warms the top artists. The section Flows re-fire on the
|
* and pre-warms the top artists. The section Flows re-fire on the
|
||||||
@@ -88,7 +97,11 @@ class HomeRepository @Inject constructor(
|
|||||||
replaceSection(SECTION_REDISCOVER_ARTISTS, "artist", wire.rediscoverArtists)
|
replaceSection(SECTION_REDISCOVER_ARTISTS, "artist", wire.rediscoverArtists)
|
||||||
replaceSection(SECTION_MOST_PLAYED_TRACKS, "track", wire.mostPlayedTracks)
|
replaceSection(SECTION_MOST_PLAYED_TRACKS, "track", wire.mostPlayedTracks)
|
||||||
replaceSection(SECTION_LAST_PLAYED_ARTISTS, "artist", wire.lastPlayedArtists)
|
replaceSection(SECTION_LAST_PLAYED_ARTISTS, "artist", wire.lastPlayedArtists)
|
||||||
prewarmer.warm(wire.rediscoverArtists + wire.lastPlayedArtists)
|
replaceSection(SECTION_YOU_MIGHT_LIKE_ALBUMS, "album", wire.youMightLikeAlbums)
|
||||||
|
replaceSection(SECTION_YOU_MIGHT_LIKE_ARTISTS, "artist", wire.youMightLikeArtists)
|
||||||
|
prewarmer.warm(
|
||||||
|
wire.rediscoverArtists + wire.lastPlayedArtists + wire.youMightLikeArtists,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun replaceSection(section: String, entityType: String, ids: List<String>) {
|
private suspend fun replaceSection(section: String, entityType: String, ids: List<String>) {
|
||||||
@@ -148,5 +161,7 @@ class HomeRepository @Inject constructor(
|
|||||||
const val SECTION_REDISCOVER_ARTISTS = "rediscover_artists"
|
const val SECTION_REDISCOVER_ARTISTS = "rediscover_artists"
|
||||||
const val SECTION_MOST_PLAYED_TRACKS = "most_played_tracks"
|
const val SECTION_MOST_PLAYED_TRACKS = "most_played_tracks"
|
||||||
const val SECTION_LAST_PLAYED_ARTISTS = "last_played_artists"
|
const val SECTION_LAST_PLAYED_ARTISTS = "last_played_artists"
|
||||||
|
const val SECTION_YOU_MIGHT_LIKE_ALBUMS = "you_might_like_albums"
|
||||||
|
const val SECTION_YOU_MIGHT_LIKE_ARTISTS = "you_might_like_artists"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -116,6 +116,8 @@ data class HomeSections(
|
|||||||
val rediscoverArtists: List<HomeTile<ArtistRef>> = emptyList(),
|
val rediscoverArtists: List<HomeTile<ArtistRef>> = emptyList(),
|
||||||
val mostPlayedTracks: List<HomeTile<TrackRef>> = emptyList(),
|
val mostPlayedTracks: List<HomeTile<TrackRef>> = emptyList(),
|
||||||
val lastPlayedArtists: List<HomeTile<ArtistRef>> = emptyList(),
|
val lastPlayedArtists: List<HomeTile<ArtistRef>> = emptyList(),
|
||||||
|
val youMightLikeAlbums: List<HomeTile<AlbumRef>> = emptyList(),
|
||||||
|
val youMightLikeArtists: List<HomeTile<ArtistRef>> = emptyList(),
|
||||||
) {
|
) {
|
||||||
val isAllEmpty: Boolean
|
val isAllEmpty: Boolean
|
||||||
get() = playlists.isEmpty() &&
|
get() = playlists.isEmpty() &&
|
||||||
@@ -123,7 +125,9 @@ data class HomeSections(
|
|||||||
rediscoverAlbums.isEmpty() &&
|
rediscoverAlbums.isEmpty() &&
|
||||||
rediscoverArtists.isEmpty() &&
|
rediscoverArtists.isEmpty() &&
|
||||||
mostPlayedTracks.isEmpty() &&
|
mostPlayedTracks.isEmpty() &&
|
||||||
lastPlayedArtists.isEmpty()
|
lastPlayedArtists.isEmpty() &&
|
||||||
|
youMightLikeAlbums.isEmpty() &&
|
||||||
|
youMightLikeArtists.isEmpty()
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── ViewModel ───────────────────────────────────────────────────────
|
// ─── ViewModel ───────────────────────────────────────────────────────
|
||||||
@@ -289,13 +293,14 @@ class HomeViewModel @Inject constructor(
|
|||||||
combineHomeFlows().asCacheFirstStateFlow(viewModelScope)
|
combineHomeFlows().asCacheFirstStateFlow(viewModelScope)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Five home-section flows merged into the sections struct without
|
* The core five section flows merged into the sections struct. Split
|
||||||
* playlists; then merged with the playlists flow in [combineHomeFlows].
|
* out so each combine call stays inside the coroutines built-in arity
|
||||||
* Splitting it like this keeps each combine call inside the
|
* limit (5) and avoids the typed-vararg acrobatics a wider combine
|
||||||
* coroutines built-in arity limit (5) and avoids the typed-vararg
|
* across heterogeneous types would need. [observeHomeSections] then
|
||||||
* acrobatics needed for a 6-flow combine across heterogeneous types.
|
* folds in the two you-might-like flows, and [combineHomeFlows] the
|
||||||
|
* playlists flow.
|
||||||
*/
|
*/
|
||||||
private fun observeHomeSections() = combine(
|
private fun observeCoreSections() = combine(
|
||||||
homeRepository.observeRecentlyAddedAlbums(),
|
homeRepository.observeRecentlyAddedAlbums(),
|
||||||
homeRepository.observeRediscoverAlbums(),
|
homeRepository.observeRediscoverAlbums(),
|
||||||
homeRepository.observeRediscoverArtists(),
|
homeRepository.observeRediscoverArtists(),
|
||||||
@@ -311,6 +316,14 @@ class HomeViewModel @Inject constructor(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun observeHomeSections() = combine(
|
||||||
|
observeCoreSections(),
|
||||||
|
homeRepository.observeYouMightLikeAlbums(),
|
||||||
|
homeRepository.observeYouMightLikeArtists(),
|
||||||
|
) { core, ymlAlbums, ymlArtists ->
|
||||||
|
core.copy(youMightLikeAlbums = ymlAlbums, youMightLikeArtists = ymlArtists)
|
||||||
|
}
|
||||||
|
|
||||||
private fun combineHomeFlows() =
|
private fun combineHomeFlows() =
|
||||||
observeHomeSections().combine(playlistsRepository.observeAll()) { sections, playlists ->
|
observeHomeSections().combine(playlistsRepository.observeAll()) { sections, playlists ->
|
||||||
val merged = sections.copy(playlists = playlists)
|
val merged = sections.copy(playlists = playlists)
|
||||||
@@ -472,6 +485,14 @@ private fun HomeSuccessContent(
|
|||||||
onPlayAlbum = onPlayAlbum,
|
onPlayAlbum = onPlayAlbum,
|
||||||
onPlayArtist = onPlayArtist,
|
onPlayArtist = onPlayArtist,
|
||||||
)
|
)
|
||||||
|
youMightLikeSection(
|
||||||
|
albums = sections.youMightLikeAlbums,
|
||||||
|
artists = sections.youMightLikeArtists,
|
||||||
|
onAlbumClick = onAlbumClick,
|
||||||
|
onArtistClick = onArtistClick,
|
||||||
|
onPlayAlbum = onPlayAlbum,
|
||||||
|
onPlayArtist = onPlayArtist,
|
||||||
|
)
|
||||||
mostPlayedSection(sections.mostPlayedTracks, onMostPlayedTap)
|
mostPlayedSection(sections.mostPlayedTracks, onMostPlayedTap)
|
||||||
lastPlayedSection(sections.lastPlayedArtists, onArtistClick, onPlayArtist)
|
lastPlayedSection(sections.lastPlayedArtists, onArtistClick, onPlayArtist)
|
||||||
item { Spacer(Modifier.height(BOTTOM_PADDING_FOR_MINIPLAYER_DP.dp)) }
|
item { Spacer(Modifier.height(BOTTOM_PADDING_FOR_MINIPLAYER_DP.dp)) }
|
||||||
@@ -667,6 +688,61 @@ private fun RediscoverBlock(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* "You might like" — in-library albums/artists predicted from the user's
|
||||||
|
* taste that they don't actively spin (server section, daily-built, cold-
|
||||||
|
* start gated). Same albums-then-artists block shape as Rediscover; the
|
||||||
|
* artist sub-row drops its title when an album sub-row precedes it so the
|
||||||
|
* two read as one section.
|
||||||
|
*/
|
||||||
|
private fun LazyListScope.youMightLikeSection(
|
||||||
|
albums: List<HomeTile<AlbumRef>>,
|
||||||
|
artists: List<HomeTile<ArtistRef>>,
|
||||||
|
onAlbumClick: (String) -> Unit,
|
||||||
|
onArtistClick: (String) -> Unit,
|
||||||
|
onPlayAlbum: suspend (String) -> Unit,
|
||||||
|
onPlayArtist: suspend (String) -> Unit,
|
||||||
|
) {
|
||||||
|
item {
|
||||||
|
if (albums.isNotEmpty() || artists.isNotEmpty()) {
|
||||||
|
YouMightLikeBlock(
|
||||||
|
albums = albums,
|
||||||
|
artists = artists,
|
||||||
|
onAlbumClick = onAlbumClick,
|
||||||
|
onArtistClick = onArtistClick,
|
||||||
|
onPlayAlbum = onPlayAlbum,
|
||||||
|
onPlayArtist = onPlayArtist,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
EmptySection(
|
||||||
|
title = "You might like",
|
||||||
|
body = "We're still learning your taste — keep listening and " +
|
||||||
|
"picks you might like will show up here.",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun YouMightLikeBlock(
|
||||||
|
albums: List<HomeTile<AlbumRef>>,
|
||||||
|
artists: List<HomeTile<ArtistRef>>,
|
||||||
|
onAlbumClick: (String) -> Unit,
|
||||||
|
onArtistClick: (String) -> Unit,
|
||||||
|
onPlayAlbum: suspend (String) -> Unit,
|
||||||
|
onPlayArtist: suspend (String) -> Unit,
|
||||||
|
) {
|
||||||
|
Column {
|
||||||
|
if (albums.isNotEmpty()) {
|
||||||
|
AlbumsRow("You might like", albums, onAlbumClick, onPlayAlbum)
|
||||||
|
}
|
||||||
|
if (artists.isNotEmpty()) {
|
||||||
|
val title = if (albums.isEmpty()) "You might like" else ""
|
||||||
|
ArtistsRow(title, artists, onArtistClick, onPlayArtist)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun AlbumsRow(
|
private fun AlbumsRow(
|
||||||
title: String,
|
title: String,
|
||||||
|
|||||||
@@ -10,13 +10,17 @@ import kotlinx.serialization.Serializable
|
|||||||
* `internal/api/types.go HomeIndexPayload`).
|
* `internal/api/types.go HomeIndexPayload`).
|
||||||
*
|
*
|
||||||
* Section name implies entity type; no per-entry type tag is needed:
|
* Section name implies entity type; no per-entry type tag is needed:
|
||||||
* - recentlyAddedAlbums → album
|
* - recentlyAddedAlbums → album
|
||||||
* - rediscoverAlbums → album
|
* - rediscoverAlbums → album
|
||||||
* - rediscoverArtists → artist
|
* - rediscoverArtists → artist
|
||||||
* - mostPlayedTracks → track
|
* - mostPlayedTracks → track
|
||||||
* - lastPlayedArtists → artist
|
* - lastPlayedArtists → artist
|
||||||
|
* - youMightLikeAlbums → album
|
||||||
|
* - youMightLikeArtists → artist
|
||||||
*
|
*
|
||||||
* All slices default to empty so missing keys don't throw on decode.
|
* All slices default to empty so missing keys don't throw on decode —
|
||||||
|
* which also keeps the client safe against older servers that don't
|
||||||
|
* emit the you-might-like sections yet.
|
||||||
*/
|
*/
|
||||||
@Serializable
|
@Serializable
|
||||||
data class HomeIndexWire(
|
data class HomeIndexWire(
|
||||||
@@ -25,4 +29,6 @@ data class HomeIndexWire(
|
|||||||
@SerialName("rediscover_artists") val rediscoverArtists: List<String> = emptyList(),
|
@SerialName("rediscover_artists") val rediscoverArtists: List<String> = emptyList(),
|
||||||
@SerialName("most_played_tracks") val mostPlayedTracks: List<String> = emptyList(),
|
@SerialName("most_played_tracks") val mostPlayedTracks: List<String> = emptyList(),
|
||||||
@SerialName("last_played_artists") val lastPlayedArtists: List<String> = emptyList(),
|
@SerialName("last_played_artists") val lastPlayedArtists: List<String> = emptyList(),
|
||||||
|
@SerialName("you_might_like_albums") val youMightLikeAlbums: List<String> = emptyList(),
|
||||||
|
@SerialName("you_might_like_artists") val youMightLikeArtists: List<String> = emptyList(),
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user