Tolerate unknown RT labels, expire cached ratings, avoid duplicate collectors and Jellyfin flash
All checks were successful
review/code-claude Reviewed c54acc9c (PASS); review-fix delta re-checked by firstmate
All checks were successful
review/code-claude Reviewed c54acc9c (PASS); review-fix delta re-checked by firstmate
This commit is contained in:
parent
c54acc9c1d
commit
04d659b115
5 changed files with 89 additions and 38 deletions
|
|
@ -15,6 +15,7 @@ import kotlinx.coroutines.flow.flowOf
|
|||
import kotlinx.coroutines.flow.mapLatest
|
||||
import org.jellyfin.sdk.model.api.BaseItemKind
|
||||
import timber.log.Timber
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
|
|
@ -50,6 +51,7 @@ class SeerrRatingsService
|
|||
CacheBuilder
|
||||
.newBuilder()
|
||||
.maximumSize(CACHE_SIZE)
|
||||
.expireAfterWrite(CACHE_HOURS, TimeUnit.HOURS)
|
||||
.build<Key, SourceRatings>()
|
||||
|
||||
fun cached(
|
||||
|
|
@ -57,6 +59,26 @@ class SeerrRatingsService
|
|||
tmdbId: Int,
|
||||
): SourceRatings? = cache.getIfPresent(Key(type, tmdbId))
|
||||
|
||||
private fun target(item: BaseItem): Pair<SeerrItemType, Int>? {
|
||||
val type =
|
||||
when (item.type) {
|
||||
BaseItemKind.MOVIE -> SeerrItemType.MOVIE
|
||||
BaseItemKind.SERIES -> SeerrItemType.TV
|
||||
else -> return null
|
||||
}
|
||||
val tmdbId =
|
||||
item.data.providerIds
|
||||
?.get("Tmdb")
|
||||
?.toIntOrNull() ?: return null
|
||||
return type to tmdbId
|
||||
}
|
||||
|
||||
/**
|
||||
* The already cached ratings for a Jellyfin movie or series, used to seed a screen's initial state so it
|
||||
* doesn't show the Jellyfin values first
|
||||
*/
|
||||
fun cachedFor(item: BaseItem): SourceRatings? = target(item)?.let { (type, id) -> cached(type, id) }
|
||||
|
||||
/**
|
||||
* Ratings for a Jellyfin movie or series using its TMDB id. Follows whether Seerr is active, since the
|
||||
* connection is established asynchronously after login.
|
||||
|
|
@ -65,23 +87,10 @@ class SeerrRatingsService
|
|||
* Callers should keep showing the Jellyfin values for null.
|
||||
*/
|
||||
fun forItem(item: BaseItem): Flow<SourceRatings?> {
|
||||
val type =
|
||||
when (item.type) {
|
||||
BaseItemKind.MOVIE -> SeerrItemType.MOVIE
|
||||
BaseItemKind.SERIES -> SeerrItemType.TV
|
||||
else -> null
|
||||
}
|
||||
val tmdbId =
|
||||
item.data.providerIds
|
||||
?.get("Tmdb")
|
||||
?.toIntOrNull()
|
||||
return if (type == null || tmdbId == null) {
|
||||
flowOf(null)
|
||||
} else {
|
||||
seerrService.active
|
||||
.distinctUntilChanged()
|
||||
.mapLatest { active -> if (active) get(type, tmdbId) else null }
|
||||
}
|
||||
val (type, tmdbId) = target(item) ?: return flowOf(null)
|
||||
return seerrService.active
|
||||
.distinctUntilChanged()
|
||||
.mapLatest { active -> if (active) get(type, tmdbId) else null }
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -176,9 +185,9 @@ class SeerrRatingsService
|
|||
val rt = combined.response
|
||||
SourceRatings.of(
|
||||
rtCriticsScore = rt.criticsScore,
|
||||
rtCriticsLabel = rt.criticsRating?.value,
|
||||
rtCriticsLabel = rt.criticsRating,
|
||||
rtAudienceScore = rt.audienceScore,
|
||||
rtAudienceLabel = rt.audienceRating?.value,
|
||||
rtAudienceLabel = rt.audienceRating,
|
||||
tmdbScore = tmdb?.average,
|
||||
tmdbVotes = tmdb?.count,
|
||||
)
|
||||
|
|
@ -195,9 +204,9 @@ class SeerrRatingsService
|
|||
): SourceRatings =
|
||||
SourceRatings.of(
|
||||
rtCriticsScore = response.rt?.criticsScore,
|
||||
rtCriticsLabel = response.rt?.criticsRating?.value,
|
||||
rtCriticsLabel = response.rt?.criticsRating,
|
||||
rtAudienceScore = response.rt?.audienceScore,
|
||||
rtAudienceLabel = response.rt?.audienceRating?.value,
|
||||
rtAudienceLabel = response.rt?.audienceRating,
|
||||
imdbScore = response.imdb?.criticsScore,
|
||||
imdbVotes = response.imdb?.criticsScoreCount,
|
||||
tmdbScore = tmdb?.average,
|
||||
|
|
@ -218,5 +227,6 @@ class SeerrRatingsService
|
|||
|
||||
companion object {
|
||||
private const val CACHE_SIZE = 300L
|
||||
private const val CACHE_HOURS = 6L
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -103,6 +103,8 @@ class MovieViewModel
|
|||
return item
|
||||
}
|
||||
|
||||
private var ratingsJob: Job? = null
|
||||
|
||||
fun init(): Job =
|
||||
viewModelScope.launchDefault {
|
||||
val movie =
|
||||
|
|
@ -127,6 +129,7 @@ class MovieViewModel
|
|||
chosenStreams = chosenStreams,
|
||||
trailers = remoteTrailers,
|
||||
chapters = chapters,
|
||||
sourceRatings = it.sourceRatings ?: ratingsService.cachedFor(movie),
|
||||
)
|
||||
}
|
||||
backdropService.submit(movie)
|
||||
|
|
@ -155,11 +158,14 @@ class MovieViewModel
|
|||
)
|
||||
}
|
||||
}
|
||||
viewModelScope.launchIO {
|
||||
ratingsService.forItem(movie).collect { ratings ->
|
||||
_state.update { it.copy(sourceRatings = ratings) }
|
||||
// init() runs again on resume and the flow never completes, so don't stack collectors
|
||||
ratingsJob?.cancel()
|
||||
ratingsJob =
|
||||
viewModelScope.launchIO {
|
||||
ratingsService.forItem(movie).collect { ratings ->
|
||||
_state.update { it.copy(sourceRatings = ratings ?: it.sourceRatings) }
|
||||
}
|
||||
}
|
||||
}
|
||||
viewModelScope.launchIO {
|
||||
val results = seerrService.similar(movie).orEmpty()
|
||||
_state.update {
|
||||
|
|
|
|||
|
|
@ -221,6 +221,7 @@ class SeriesViewModel
|
|||
seasons = seasons,
|
||||
episodes = episodes,
|
||||
trailers = remoteTrailers,
|
||||
sourceRatings = ratingsService.cachedFor(series),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -256,7 +257,7 @@ class SeriesViewModel
|
|||
}
|
||||
viewModelScope.launchIO {
|
||||
ratingsService.forItem(series).collect { ratings ->
|
||||
_state.update { it.copy(sourceRatings = ratings) }
|
||||
_state.update { it.copy(sourceRatings = ratings ?: it.sourceRatings) }
|
||||
}
|
||||
}
|
||||
viewModelScope.launchIO {
|
||||
|
|
|
|||
|
|
@ -6528,13 +6528,13 @@ paths:
|
|||
example: 85
|
||||
criticsRating:
|
||||
type: string
|
||||
enum: ['Rotten', 'Fresh', 'Certified Fresh']
|
||||
example: 'Certified Fresh'
|
||||
audienceScore:
|
||||
type: integer
|
||||
example: 65
|
||||
audienceRating:
|
||||
type: string
|
||||
enum: ['Spilled', 'Upright']
|
||||
example: 'Upright'
|
||||
/movie/{movieId}/ratingscombined:
|
||||
get:
|
||||
summary: Get RT and IMDB movie ratings combined
|
||||
|
|
@ -6573,13 +6573,13 @@ paths:
|
|||
example: 85
|
||||
criticsRating:
|
||||
type: string
|
||||
enum: ['Rotten', 'Fresh', 'Certified Fresh']
|
||||
example: 'Certified Fresh'
|
||||
audienceScore:
|
||||
type: integer
|
||||
example: 65
|
||||
audienceRating:
|
||||
type: string
|
||||
enum: ['Spilled', 'Upright']
|
||||
example: 'Upright'
|
||||
imdb:
|
||||
type: object
|
||||
properties:
|
||||
|
|
@ -6777,13 +6777,13 @@ paths:
|
|||
example: 85
|
||||
criticsRating:
|
||||
type: string
|
||||
enum: ['Rotten', 'Fresh', 'Certified Fresh']
|
||||
example: 'Certified Fresh'
|
||||
audienceScore:
|
||||
type: integer
|
||||
example: 65
|
||||
audienceRating:
|
||||
type: string
|
||||
enum: ['Spilled', 'Upright']
|
||||
example: 'Upright'
|
||||
/person/{personId}:
|
||||
get:
|
||||
summary: Get person details
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue