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.BaseItemDtoQueryResult
|
||||||
import org.jellyfin.sdk.model.api.BaseItemKind
|
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||||
import org.jellyfin.sdk.model.api.DeviceProfile
|
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.MediaSourceInfo
|
||||||
import org.jellyfin.sdk.model.api.PlaybackInfoDto
|
import org.jellyfin.sdk.model.api.PlaybackInfoDto
|
||||||
import org.jellyfin.sdk.model.api.SubtitleDeliveryMethod
|
import org.jellyfin.sdk.model.api.SubtitleDeliveryMethod
|
||||||
@@ -152,7 +153,7 @@ class JellyfinApiClient @Inject constructor(
|
|||||||
}
|
}
|
||||||
val result = api.userLibraryApi.getItem(
|
val result = api.userLibraryApi.getItem(
|
||||||
itemId = mediaId,
|
itemId = mediaId,
|
||||||
userId = getUserId()
|
userId = getUserId(),
|
||||||
)
|
)
|
||||||
Log.d("getItemInfo response: {}", result.content.toString())
|
Log.d("getItemInfo response: {}", result.content.toString())
|
||||||
return result.content
|
return result.content
|
||||||
@@ -165,6 +166,7 @@ class JellyfinApiClient @Inject constructor(
|
|||||||
val result = api.tvShowsApi.getSeasons(
|
val result = api.tvShowsApi.getSeasons(
|
||||||
userId = getUserId(),
|
userId = getUserId(),
|
||||||
seriesId = seriesId,
|
seriesId = seriesId,
|
||||||
|
fields = listOf(ItemFields.CHILD_COUNT, ItemFields.PARENT_ID),
|
||||||
enableUserData = true
|
enableUserData = true
|
||||||
)
|
)
|
||||||
Log.d("getSeasons response: {}", result.content.toString())
|
Log.d("getSeasons response: {}", result.content.toString())
|
||||||
@@ -179,6 +181,7 @@ class JellyfinApiClient @Inject constructor(
|
|||||||
userId = getUserId(),
|
userId = getUserId(),
|
||||||
seriesId = seriesId,
|
seriesId = seriesId,
|
||||||
seasonId = seasonId,
|
seasonId = seasonId,
|
||||||
|
fields = listOf(ItemFields.CHILD_COUNT, ItemFields.PARENT_ID),
|
||||||
enableUserData = true
|
enableUserData = true
|
||||||
)
|
)
|
||||||
Log.d("getEpisodesInSeason response: {}", result.content.toString())
|
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 {
|
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.Episode
|
||||||
import hu.bbara.purefin.data.model.Season
|
import hu.bbara.purefin.data.model.Season
|
||||||
import hu.bbara.purefin.data.model.Series
|
import hu.bbara.purefin.data.model.Series
|
||||||
import java.util.UUID
|
|
||||||
import javax.inject.Inject
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import java.util.UUID
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
class RoomLocalMediaRepository @Inject constructor(
|
class RoomLocalMediaRepository @Inject constructor(
|
||||||
private val seriesDao: SeriesDao,
|
private val seriesDao: SeriesDao,
|
||||||
@@ -147,7 +147,9 @@ private fun SeriesEntity.toDomain(seasons: List<Season>): Series = Series(
|
|||||||
year = year,
|
year = year,
|
||||||
heroImageUrl = heroImageUrl,
|
heroImageUrl = heroImageUrl,
|
||||||
seasonCount = seasonCount,
|
seasonCount = seasonCount,
|
||||||
seasons = seasons
|
seasons = seasons,
|
||||||
|
//TODO check if it is needed
|
||||||
|
cast = emptyList()
|
||||||
)
|
)
|
||||||
|
|
||||||
private fun SeasonEntity.toDomain(episodes: List<Episode>): Season = Season(
|
private fun SeasonEntity.toDomain(episodes: List<Episode>): Season = Season(
|
||||||
@@ -171,7 +173,9 @@ private fun EpisodeEntity.toDomain(): Episode = Episode(
|
|||||||
format = format,
|
format = format,
|
||||||
synopsis = synopsis,
|
synopsis = synopsis,
|
||||||
heroImageUrl = heroImageUrl,
|
heroImageUrl = heroImageUrl,
|
||||||
cast = cast.map { it.toDomain() }
|
progress = 13.0,
|
||||||
|
watched = false,
|
||||||
|
cast = emptyList()
|
||||||
)
|
)
|
||||||
|
|
||||||
private fun SeriesWithSeasonsAndEpisodes.toDomain(): Series =
|
private fun SeriesWithSeasonsAndEpisodes.toDomain(): Series =
|
||||||
@@ -209,7 +213,7 @@ private fun Episode.toEntity(): EpisodeEntity = EpisodeEntity(
|
|||||||
format = format,
|
format = format,
|
||||||
synopsis = synopsis,
|
synopsis = synopsis,
|
||||||
heroImageUrl = heroImageUrl,
|
heroImageUrl = heroImageUrl,
|
||||||
cast = cast.map { it.toEntity() }
|
cast = emptyList()
|
||||||
)
|
)
|
||||||
|
|
||||||
private fun EpisodeCastMemberEntity.toDomain(): CastMember = CastMember(
|
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
|
package hu.bbara.purefin.data.model
|
||||||
|
|
||||||
import hu.bbara.purefin.app.content.episode.CastMember
|
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
data class Episode(
|
data class Episode(
|
||||||
val id: UUID,
|
val id: UUID,
|
||||||
val seriesId: UUID,
|
val seriesId: UUID,
|
||||||
val seasonId: UUID,
|
val seasonId: UUID,
|
||||||
val title: String,
|
|
||||||
val index: Int,
|
val index: Int,
|
||||||
|
val title: String,
|
||||||
|
val synopsis: String,
|
||||||
val releaseDate: String,
|
val releaseDate: String,
|
||||||
val rating: String,
|
val rating: String,
|
||||||
val runtime: String,
|
val runtime: String,
|
||||||
|
val progress: Double?,
|
||||||
|
val watched: Boolean,
|
||||||
val format: String,
|
val format: String,
|
||||||
val synopsis: String,
|
|
||||||
val heroImageUrl: String,
|
val heroImageUrl: String,
|
||||||
val cast: List<CastMember>
|
val cast: List<CastMember>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -9,5 +9,6 @@ data class Series(
|
|||||||
val year: String,
|
val year: String,
|
||||||
val heroImageUrl: String,
|
val heroImageUrl: String,
|
||||||
val seasonCount: Int,
|
val seasonCount: Int,
|
||||||
val seasons: List<Season>
|
val seasons: List<Season>,
|
||||||
|
val cast: List<CastMember>
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user