mirror of
https://github.com/bbara04/Purefin.git
synced 2026-03-31 17:10:08 +02:00
feat: update MediaRepository and related models to support asynchronous data fetching and add CastMember data class
This commit is contained in:
@@ -20,6 +20,7 @@ import org.jellyfin.sdk.model.api.BaseItemDto
|
||||
import org.jellyfin.sdk.model.api.BaseItemDtoQueryResult
|
||||
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||
import org.jellyfin.sdk.model.api.DeviceProfile
|
||||
import org.jellyfin.sdk.model.api.ItemFields
|
||||
import org.jellyfin.sdk.model.api.MediaSourceInfo
|
||||
import org.jellyfin.sdk.model.api.PlaybackInfoDto
|
||||
import org.jellyfin.sdk.model.api.SubtitleDeliveryMethod
|
||||
@@ -152,7 +153,7 @@ class JellyfinApiClient @Inject constructor(
|
||||
}
|
||||
val result = api.userLibraryApi.getItem(
|
||||
itemId = mediaId,
|
||||
userId = getUserId()
|
||||
userId = getUserId(),
|
||||
)
|
||||
Log.d("getItemInfo response: {}", result.content.toString())
|
||||
return result.content
|
||||
@@ -165,6 +166,7 @@ class JellyfinApiClient @Inject constructor(
|
||||
val result = api.tvShowsApi.getSeasons(
|
||||
userId = getUserId(),
|
||||
seriesId = seriesId,
|
||||
fields = listOf(ItemFields.CHILD_COUNT, ItemFields.PARENT_ID),
|
||||
enableUserData = true
|
||||
)
|
||||
Log.d("getSeasons response: {}", result.content.toString())
|
||||
@@ -179,6 +181,7 @@ class JellyfinApiClient @Inject constructor(
|
||||
userId = getUserId(),
|
||||
seriesId = seriesId,
|
||||
seasonId = seasonId,
|
||||
fields = listOf(ItemFields.CHILD_COUNT, ItemFields.PARENT_ID),
|
||||
enableUserData = true
|
||||
)
|
||||
Log.d("getEpisodesInSeason response: {}", result.content.toString())
|
||||
|
||||
@@ -0,0 +1,282 @@
|
||||
package hu.bbara.purefin.data
|
||||
|
||||
import androidx.collection.LruCache
|
||||
import hu.bbara.purefin.client.JellyfinApiClient
|
||||
import hu.bbara.purefin.data.model.Episode
|
||||
import hu.bbara.purefin.data.model.Season
|
||||
import hu.bbara.purefin.data.model.Series
|
||||
import hu.bbara.purefin.image.JellyfinImageHelper
|
||||
import hu.bbara.purefin.session.UserSessionRepository
|
||||
import kotlinx.coroutines.flow.first
|
||||
import org.jellyfin.sdk.model.api.BaseItemDto
|
||||
import org.jellyfin.sdk.model.api.ImageType
|
||||
import java.time.LocalDateTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.util.Locale
|
||||
import java.util.UUID
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class InMemoryMediaRepository @Inject constructor(
|
||||
val userSessionRepository: UserSessionRepository,
|
||||
val jellyfinApiClient: JellyfinApiClient
|
||||
) : MediaRepository {
|
||||
|
||||
val seriesCache : LruCache<UUID, Series> = LruCache(100)
|
||||
|
||||
override suspend fun getSeries(
|
||||
seriesId: UUID,
|
||||
includeContent: Boolean
|
||||
): Series {
|
||||
val series = fetchAndUpdateSeriesIfMissing(seriesId)
|
||||
if (includeContent.not()) {
|
||||
return series.copy(seasons = emptyList())
|
||||
}
|
||||
|
||||
if (hasContent(series)) {
|
||||
return series
|
||||
}
|
||||
|
||||
val seasons = getSeasons(
|
||||
seriesId = seriesId,
|
||||
includeContent = true
|
||||
)
|
||||
return series.copy(seasons = seasons)
|
||||
}
|
||||
|
||||
override suspend fun getSeason(
|
||||
seriesId: UUID,
|
||||
seasonId: UUID,
|
||||
includeContent: Boolean
|
||||
): Season {
|
||||
val season = fetchAndUpdateSeasonIfMissing(seriesId, seasonId)
|
||||
if (includeContent.not()) {
|
||||
return season.copy(episodes = emptyList())
|
||||
}
|
||||
|
||||
if (hasContent(season)) {
|
||||
return season
|
||||
}
|
||||
|
||||
val episodes = getEpisodes(
|
||||
seriesId = seriesId,
|
||||
seasonId = seasonId
|
||||
)
|
||||
return season.copy(episodes = episodes)
|
||||
}
|
||||
|
||||
override suspend fun getSeasons(
|
||||
seriesId: UUID,
|
||||
includeContent: Boolean
|
||||
): List<Season> {
|
||||
val cachedSeasons = fetchAndUpdateSeasonsIfMissing(seriesId)
|
||||
if (includeContent.not()) {
|
||||
return cachedSeasons.map { it.copy(episodes = emptyList()) }
|
||||
}
|
||||
|
||||
val hasContent = cachedSeasons.all { season ->
|
||||
hasContent(season)
|
||||
}
|
||||
if (hasContent) {
|
||||
return cachedSeasons
|
||||
}
|
||||
|
||||
return cachedSeasons.map { season ->
|
||||
// TODO use batch api that gives back all of the episodes in a single request
|
||||
val episodes = getEpisodes(seriesId, season.id)
|
||||
season.copy(episodes = episodes)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun getEpisode(
|
||||
seriesId: UUID,
|
||||
seasonId: UUID,
|
||||
episodeId: UUID
|
||||
): Episode {
|
||||
val cachedSeason = fetchAndUpdateSeasonIfMissing(seriesId, seasonId)
|
||||
cachedSeason.episodes.find { it.id == episodeId }?.let {
|
||||
return it
|
||||
}
|
||||
|
||||
val episodesItemInfo = jellyfinApiClient.getEpisodesInSeason(seriesId, seasonId)
|
||||
val episodes = episodesItemInfo.map { it.toEpisode(serverUrl()) }
|
||||
val cachedSeries = seriesCache[seriesId]!!
|
||||
val season = cachedSeason.copy(episodes = episodes)
|
||||
val updatedSeasons = cachedSeries.seasons.map { if (it.id == seasonId) season else it }
|
||||
val updatedSeries = cachedSeries.copy(seasons = updatedSeasons)
|
||||
seriesCache.put(seriesId, updatedSeries)
|
||||
return episodes.find { it.id == episodeId }!!
|
||||
}
|
||||
|
||||
override suspend fun getEpisodes(
|
||||
seriesId: UUID,
|
||||
seasonId: UUID
|
||||
): List<Episode> {
|
||||
val cachedSeason = fetchAndUpdateSeasonIfMissing(seriesId, seasonId)
|
||||
if (hasContent(cachedSeason)) {
|
||||
return cachedSeason.episodes
|
||||
}
|
||||
|
||||
val episodesItemInfo = jellyfinApiClient.getEpisodesInSeason(seriesId, seasonId)
|
||||
val episodes = episodesItemInfo.map { it.toEpisode(serverUrl()) }
|
||||
val cachedSeries = seriesCache[seriesId]!!
|
||||
val updatedSeason = cachedSeason.copy(episodes = episodes)
|
||||
val updateSeries = cachedSeries.copy(seasons = cachedSeries.seasons.map { if (it.id == seasonId) updatedSeason else it })
|
||||
seriesCache.put(seriesId, updateSeries)
|
||||
return episodes
|
||||
}
|
||||
|
||||
override suspend fun getEpisodes(seriesId: UUID): List<Episode> {
|
||||
val cachedSeasons = fetchAndUpdateSeasonsIfMissing(seriesId)
|
||||
if (cachedSeasons.all { hasContent(it) }) {
|
||||
return cachedSeasons.flatMap { it.episodes }
|
||||
}
|
||||
|
||||
return cachedSeasons.flatMap { season ->
|
||||
getEpisodes(seriesId, season.id)
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun fetchAndUpdateSeriesIfMissing(seriesId: UUID): Series {
|
||||
val cachedSeries = seriesCache[seriesId]
|
||||
if (cachedSeries == null) {
|
||||
val seriesItemInfo = jellyfinApiClient.getItemInfo(seriesId)
|
||||
?: throw RuntimeException("Series not found")
|
||||
val series = seriesItemInfo.toSeries(serverUrl())
|
||||
seriesCache.put(seriesId, series)
|
||||
}
|
||||
return seriesCache[seriesId]!!
|
||||
}
|
||||
|
||||
private suspend fun fetchAndUpdateSeasonIfMissing(seriesId: UUID, seasonId: UUID): Season {
|
||||
val cachedSeries = fetchAndUpdateSeriesIfMissing(seriesId)
|
||||
cachedSeries.seasons.find { it.id == seasonId }?.let {
|
||||
return it
|
||||
}
|
||||
val seasonsItemInfo = jellyfinApiClient.getSeasons(seriesId)
|
||||
val seasons = seasonsItemInfo.map { it.toSeason(serverUrl()) }
|
||||
val series = cachedSeries.copy(
|
||||
seasons = seasons
|
||||
)
|
||||
seriesCache.put(seriesId, series)
|
||||
return seasons.find { it.id == seasonId }!!
|
||||
}
|
||||
|
||||
private suspend fun fetchAndUpdateSeasonsIfMissing(seriesId: UUID): List<Season> {
|
||||
val cachedSeries = fetchAndUpdateSeriesIfMissing(seriesId)
|
||||
if (cachedSeries.seasons.size == cachedSeries.seasonCount) {
|
||||
return cachedSeries.seasons
|
||||
}
|
||||
|
||||
val seasonsItemInfo = jellyfinApiClient.getSeasons(seriesId)
|
||||
val seasons = seasonsItemInfo.map { it.toSeason(serverUrl()) }
|
||||
val series = cachedSeries.copy(
|
||||
seasons = seasons
|
||||
)
|
||||
seriesCache.put(seriesId, series)
|
||||
return seasons
|
||||
|
||||
}
|
||||
|
||||
private fun hasContent(series: Series): Boolean {
|
||||
if (series.seasons.size != series.seasonCount) {
|
||||
return false
|
||||
}
|
||||
for (season in series.seasons) {
|
||||
if (hasContent(season).not()) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private fun hasContent(season: Season) : Boolean {
|
||||
return season.episodes.size == season.episodeCount
|
||||
}
|
||||
|
||||
private suspend fun serverUrl(): String {
|
||||
return userSessionRepository.serverUrl.first()
|
||||
}
|
||||
|
||||
private fun BaseItemDto.toSeries(serverUrl: String): Series {
|
||||
return Series(
|
||||
id = this.id,
|
||||
name = this.name ?: "Unknown",
|
||||
synopsis = this.overview ?: "No synopsis available",
|
||||
year = this.productionYear?.toString()
|
||||
?: this.premiereDate?.year?.toString().orEmpty(),
|
||||
heroImageUrl = JellyfinImageHelper.toImageUrl(
|
||||
url = serverUrl,
|
||||
itemId = this.id,
|
||||
type = ImageType.PRIMARY
|
||||
),
|
||||
seasonCount = this.childCount!!,
|
||||
seasons = emptyList(),
|
||||
cast = emptyList()
|
||||
)
|
||||
}
|
||||
|
||||
private fun BaseItemDto.toSeason(serverUrl: String): Season {
|
||||
return Season(
|
||||
id = this.id,
|
||||
seriesId = this.seriesId!!,
|
||||
name = this.name ?: "Unknown",
|
||||
index = this.indexNumber!!,
|
||||
episodeCount = this.childCount!!,
|
||||
episodes = emptyList()
|
||||
)
|
||||
}
|
||||
|
||||
private fun BaseItemDto.toEpisode(serverUrl: String): Episode {
|
||||
val releaseDate = formatReleaseDate(premiereDate, productionYear)
|
||||
val rating = officialRating ?: "NR"
|
||||
val runtime = formatRuntime(runTimeTicks)
|
||||
val format = container?.uppercase() ?: "VIDEO"
|
||||
val synopsis = overview ?: "No synopsis available."
|
||||
val heroImageUrl = id?.let { itemId ->
|
||||
JellyfinImageHelper.toImageUrl(
|
||||
url = serverUrl,
|
||||
itemId = itemId,
|
||||
type = ImageType.PRIMARY
|
||||
)
|
||||
} ?: ""
|
||||
return Episode(
|
||||
id = id,
|
||||
seriesId = seriesId!!,
|
||||
seasonId = parentId!!,
|
||||
title = name ?: "Unknown title",
|
||||
index = indexNumber!!,
|
||||
releaseDate = releaseDate,
|
||||
rating = rating,
|
||||
runtime = runtime,
|
||||
progress = userData!!.playedPercentage,
|
||||
watched = userData!!.played,
|
||||
format = format,
|
||||
synopsis = synopsis,
|
||||
heroImageUrl = heroImageUrl,
|
||||
cast = emptyList()
|
||||
)
|
||||
}
|
||||
|
||||
private fun formatReleaseDate(date: LocalDateTime?, fallbackYear: Int?): String {
|
||||
if (date == null) {
|
||||
return fallbackYear?.toString() ?: "—"
|
||||
}
|
||||
val formatter = DateTimeFormatter.ofPattern("MMM d, yyyy", Locale.getDefault())
|
||||
return date.toLocalDate().format(formatter)
|
||||
}
|
||||
|
||||
private fun formatRuntime(ticks: Long?): String {
|
||||
if (ticks == null || ticks <= 0) return "—"
|
||||
val totalSeconds = ticks / 10_000_000
|
||||
val hours = TimeUnit.SECONDS.toHours(totalSeconds)
|
||||
val minutes = TimeUnit.SECONDS.toMinutes(totalSeconds) % 60
|
||||
return if (hours > 0) {
|
||||
"${hours}h ${minutes}m"
|
||||
} else {
|
||||
"${minutes}m"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,16 +7,16 @@ import java.util.UUID
|
||||
|
||||
interface MediaRepository {
|
||||
|
||||
fun getSeries(seriesId: UUID, includeContent: Boolean) : Series
|
||||
suspend fun getSeries(seriesId: UUID, includeContent: Boolean) : Series
|
||||
|
||||
fun getSeason(seriesId: UUID, seasonId: UUID, includeContent: Boolean) : Season
|
||||
suspend fun getSeason(seriesId: UUID, seasonId: UUID, includeContent: Boolean) : Season
|
||||
|
||||
fun getSeasons(seriesId: UUID, includeContent: Boolean) : List<Season>
|
||||
suspend fun getSeasons(seriesId: UUID, includeContent: Boolean) : List<Season>
|
||||
|
||||
fun getEpisode(seriesId: UUID, seasonId: UUID, episodeId: UUID) : Episode
|
||||
suspend fun getEpisode(seriesId: UUID, seasonId: UUID, episodeId: UUID) : Episode
|
||||
|
||||
fun getEpisodes(seriesId: UUID, seasonId: UUID) : List<Episode>
|
||||
suspend fun getEpisodes(seriesId: UUID, seasonId: UUID) : List<Episode>
|
||||
|
||||
fun getEpisodes(seriesId: UUID) : List<Episode>
|
||||
suspend fun getEpisodes(seriesId: UUID) : List<Episode>
|
||||
|
||||
}
|
||||
@@ -13,12 +13,12 @@ import hu.bbara.purefin.data.local.relations.SeriesWithSeasonsAndEpisodes
|
||||
import hu.bbara.purefin.data.model.Episode
|
||||
import hu.bbara.purefin.data.model.Season
|
||||
import hu.bbara.purefin.data.model.Series
|
||||
import java.util.UUID
|
||||
import javax.inject.Inject
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.util.UUID
|
||||
import javax.inject.Inject
|
||||
|
||||
class RoomLocalMediaRepository @Inject constructor(
|
||||
private val seriesDao: SeriesDao,
|
||||
@@ -147,7 +147,9 @@ private fun SeriesEntity.toDomain(seasons: List<Season>): Series = Series(
|
||||
year = year,
|
||||
heroImageUrl = heroImageUrl,
|
||||
seasonCount = seasonCount,
|
||||
seasons = seasons
|
||||
seasons = seasons,
|
||||
//TODO check if it is needed
|
||||
cast = emptyList()
|
||||
)
|
||||
|
||||
private fun SeasonEntity.toDomain(episodes: List<Episode>): Season = Season(
|
||||
@@ -171,7 +173,9 @@ private fun EpisodeEntity.toDomain(): Episode = Episode(
|
||||
format = format,
|
||||
synopsis = synopsis,
|
||||
heroImageUrl = heroImageUrl,
|
||||
cast = cast.map { it.toDomain() }
|
||||
progress = 13.0,
|
||||
watched = false,
|
||||
cast = emptyList()
|
||||
)
|
||||
|
||||
private fun SeriesWithSeasonsAndEpisodes.toDomain(): Series =
|
||||
@@ -209,7 +213,7 @@ private fun Episode.toEntity(): EpisodeEntity = EpisodeEntity(
|
||||
format = format,
|
||||
synopsis = synopsis,
|
||||
heroImageUrl = heroImageUrl,
|
||||
cast = cast.map { it.toEntity() }
|
||||
cast = emptyList()
|
||||
)
|
||||
|
||||
private fun EpisodeCastMemberEntity.toDomain(): CastMember = CastMember(
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
package hu.bbara.purefin.data.model
|
||||
|
||||
data class CastMember(
|
||||
val name: String,
|
||||
val role: String,
|
||||
val imageUrl: String?
|
||||
)
|
||||
@@ -1,19 +1,20 @@
|
||||
package hu.bbara.purefin.data.model
|
||||
|
||||
import hu.bbara.purefin.app.content.episode.CastMember
|
||||
import java.util.UUID
|
||||
|
||||
data class Episode(
|
||||
val id: UUID,
|
||||
val seriesId: UUID,
|
||||
val seasonId: UUID,
|
||||
val title: String,
|
||||
val index: Int,
|
||||
val title: String,
|
||||
val synopsis: String,
|
||||
val releaseDate: String,
|
||||
val rating: String,
|
||||
val runtime: String,
|
||||
val progress: Double?,
|
||||
val watched: Boolean,
|
||||
val format: String,
|
||||
val synopsis: String,
|
||||
val heroImageUrl: String,
|
||||
val cast: List<CastMember>
|
||||
)
|
||||
|
||||
@@ -9,5 +9,6 @@ data class Series(
|
||||
val year: String,
|
||||
val heroImageUrl: String,
|
||||
val seasonCount: Int,
|
||||
val seasons: List<Season>
|
||||
val seasons: List<Season>,
|
||||
val cast: List<CastMember>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user