mirror of
https://github.com/bbara04/Purefin.git
synced 2026-03-31 17:10:08 +02:00
feat: add media suggestions to home screen
Introduce a new Suggestions section on the home screen that fetches and displays recommended content from the Jellyfin API. This replaces the previous manual featured items logic with a more robust suggestion system supporting movies, series, and episodes. Key changes: - Implement `getSuggestions` in `JellyfinApiClient` to fetch video content. - Update `AppContentRepository` and its implementation to manage suggestions, including caching and background refreshing. - Add `SuggestedItem` models and update `AppViewModel` to expose suggestions as a state flow. - Replace `HomeFeaturedSection` with `SuggestionsSection` using a horizontal pager. - Implement auto-scrolling logic in `HomeContent` to ensure suggestions are visible upon initial load if the user hasn't already interacted.
This commit is contained in:
@@ -8,6 +8,7 @@ import java.util.UUID
|
||||
interface AppContentRepository : MediaRepository {
|
||||
|
||||
val libraries: StateFlow<List<Library>>
|
||||
val suggestions: StateFlow<List<Media>>
|
||||
val continueWatching: StateFlow<List<Media>>
|
||||
val nextUp: StateFlow<List<Media>>
|
||||
val latestLibraryContent: StateFlow<Map<UUID, List<Media>>>
|
||||
|
||||
@@ -58,6 +58,9 @@ class InMemoryAppContentRepository @Inject constructor(
|
||||
private val _libraries: MutableStateFlow<List<Library>> = MutableStateFlow(emptyList())
|
||||
|
||||
override val libraries: StateFlow<List<Library>> = _libraries.asStateFlow()
|
||||
private val _suggestions: MutableStateFlow<List<Media>> = MutableStateFlow(emptyList())
|
||||
|
||||
override val suggestions: StateFlow<List<Media>> = _suggestions.asStateFlow()
|
||||
private val _continueWatching: MutableStateFlow<List<Media>> = MutableStateFlow(emptyList())
|
||||
|
||||
override val continueWatching: StateFlow<List<Media>> = _continueWatching.asStateFlow()
|
||||
@@ -92,6 +95,9 @@ class InMemoryAppContentRepository @Inject constructor(
|
||||
|
||||
private suspend fun loadFromCache() {
|
||||
val cache = homeCacheDataStore.data.first()
|
||||
if (cache.suggestions.isNotEmpty()) {
|
||||
_suggestions.value = cache.suggestions.mapNotNull { it.toMedia() }
|
||||
}
|
||||
if (cache.continueWatching.isNotEmpty()) {
|
||||
_continueWatching.value = cache.continueWatching.mapNotNull { it.toMedia() }
|
||||
}
|
||||
@@ -108,6 +114,7 @@ class InMemoryAppContentRepository @Inject constructor(
|
||||
|
||||
private suspend fun persistHomeCache() {
|
||||
val cache = HomeCache(
|
||||
suggestions = _suggestions.value.map { it.toCachedItem() },
|
||||
continueWatching = _continueWatching.value.map { it.toCachedItem() },
|
||||
nextUp = _nextUp.value.map { it.toCachedItem() },
|
||||
latestLibraryContent = _latestLibraryContent.value.map { (uuid, items) ->
|
||||
@@ -150,6 +157,7 @@ class InMemoryAppContentRepository @Inject constructor(
|
||||
contentRepositoryReady.value = true
|
||||
loadJob?.cancel()
|
||||
loadJob = scope.launch {
|
||||
loadSuggestions()
|
||||
loadContinueWatching()
|
||||
loadNextUp()
|
||||
loadLatestLibraryContent()
|
||||
@@ -228,6 +236,36 @@ class InMemoryAppContentRepository @Inject constructor(
|
||||
return updatedSeries
|
||||
}
|
||||
|
||||
suspend fun loadSuggestions() {
|
||||
val suggestionsItems = runCatching { jellyfinApiClient.getSuggestions() }
|
||||
.getOrElse { error ->
|
||||
Log.w(TAG, "Unable to load suggestions", error)
|
||||
return
|
||||
}
|
||||
val items = suggestionsItems.mapNotNull { item ->
|
||||
when (item.type) {
|
||||
BaseItemKind.MOVIE -> Media.MovieMedia(movieId = item.id)
|
||||
BaseItemKind.EPISODE -> Media.EpisodeMedia(
|
||||
episodeId = item.id,
|
||||
seriesId = item.seriesId!!
|
||||
)
|
||||
else -> throw UnsupportedOperationException("Unsupported item type: ${item.type}")
|
||||
}
|
||||
}
|
||||
_suggestions.value = items
|
||||
|
||||
//Load episodes, Movies are already loaded at this point
|
||||
suggestionsItems.forEach { item ->
|
||||
when (item.type) {
|
||||
BaseItemKind.EPISODE -> {
|
||||
val episode = item.toEpisode(serverUrl())
|
||||
mediaRepository.upsertEpisodes(listOf(episode))
|
||||
}
|
||||
else -> { /* Do nothing */ }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun loadContinueWatching() {
|
||||
val continueWatchingItems = runCatching { jellyfinApiClient.getContinueWatching() }
|
||||
.getOrElse { error ->
|
||||
@@ -337,6 +375,7 @@ class InMemoryAppContentRepository @Inject constructor(
|
||||
val isOnline = networkMonitor.isOnline.first()
|
||||
if (!isOnline) return@runCatching
|
||||
loadLibraries()
|
||||
loadSuggestions()
|
||||
loadContinueWatching()
|
||||
loadNextUp()
|
||||
loadLatestLibraryContent()
|
||||
|
||||
@@ -11,6 +11,7 @@ data class CachedMediaItem(
|
||||
|
||||
@Serializable
|
||||
data class HomeCache(
|
||||
val suggestions: List<CachedMediaItem> = emptyList(),
|
||||
val continueWatching: List<CachedMediaItem> = emptyList(),
|
||||
val nextUp: List<CachedMediaItem> = emptyList(),
|
||||
val latestLibraryContent: Map<String, List<CachedMediaItem>> = emptyMap()
|
||||
|
||||
@@ -12,6 +12,7 @@ import org.jellyfin.sdk.api.client.extensions.authenticateUserByName
|
||||
import org.jellyfin.sdk.api.client.extensions.itemsApi
|
||||
import org.jellyfin.sdk.api.client.extensions.mediaInfoApi
|
||||
import org.jellyfin.sdk.api.client.extensions.playStateApi
|
||||
import org.jellyfin.sdk.api.client.extensions.suggestionsApi
|
||||
import org.jellyfin.sdk.api.client.extensions.tvShowsApi
|
||||
import org.jellyfin.sdk.api.client.extensions.userApi
|
||||
import org.jellyfin.sdk.api.client.extensions.userLibraryApi
|
||||
@@ -25,6 +26,7 @@ import org.jellyfin.sdk.model.api.BaseItemKind
|
||||
import org.jellyfin.sdk.model.api.CollectionType
|
||||
import org.jellyfin.sdk.model.api.ItemFields
|
||||
import org.jellyfin.sdk.model.api.MediaSourceInfo
|
||||
import org.jellyfin.sdk.model.api.MediaType
|
||||
import org.jellyfin.sdk.model.api.PlayMethod
|
||||
import org.jellyfin.sdk.model.api.PlaybackInfoDto
|
||||
import org.jellyfin.sdk.model.api.PlaybackInfoResponse
|
||||
@@ -137,6 +139,25 @@ class JellyfinApiClient @Inject constructor(
|
||||
response.content.items
|
||||
}
|
||||
|
||||
suspend fun getSuggestions(): List<BaseItemDto> = withContext(Dispatchers.IO) {
|
||||
if (!ensureConfigured()) {
|
||||
return@withContext emptyList()
|
||||
}
|
||||
val userId = getUserId()
|
||||
if (userId == null) {
|
||||
return@withContext emptyList()
|
||||
}
|
||||
val response = api.suggestionsApi.getSuggestions(
|
||||
userId = userId,
|
||||
mediaType = listOf(MediaType.VIDEO),
|
||||
type = listOf(BaseItemKind.MOVIE, BaseItemKind.SERIES),
|
||||
limit = 8,
|
||||
enableTotalRecordCount = true
|
||||
)
|
||||
Log.d("getSuggestions", response.content.toString())
|
||||
response.content.items
|
||||
}
|
||||
|
||||
suspend fun getContinueWatching(): List<BaseItemDto> = withContext(Dispatchers.IO) {
|
||||
if (!ensureConfigured()) {
|
||||
return@withContext emptyList()
|
||||
|
||||
Reference in New Issue
Block a user