Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ import org.prairieserver.prairie.model.catalog.ExternalRatings
* `RT 93%`. The mark is the source's name as plain text, never a star or the
* source's artwork, except TMDB, whose approved logo its terms allow. The
* score is [DisplayRating.display] as the server formatted it. Screen readers
* hear "IMDb 8.5".
* hear "IMDb 8.5". The community rating (Prairie's own, not external) uses a
* star mark and is read as "Community rating 4.2 · 7 ratings".
*
* [style] is the score's text style and should carry a color and font size;
* the mark is drawn from it semibold and dimmed, at [markFontSize] or a
Expand Down Expand Up @@ -58,7 +59,11 @@ fun RatingEntry(
)
Row(
modifier = modifier.clearAndSetSemantics {
contentDescription = "${rating.name} ${rating.display}"
contentDescription = if (rating.source == ExternalRatings.SOURCE_COMMUNITY) {
"Community rating ${rating.display}"
} else {
"${rating.name} ${rating.display}"
}
},
horizontalArrangement = Arrangement.spacedBy(6.dp),
verticalAlignment = Alignment.CenterVertically,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,8 @@ data class ItemDetail(
@SerialName("user_data") val userData: LeafItemUserData? = null,
/** The current user's personal star rating (1-5), when set. */
@SerialName("user_rating") val userRating: Int? = null,
/** Aggregate star rating across profiles; omitted by the server below 3 ratings. */
@SerialName("community_rating") val communityRating: CommunityRating? = null,
val versions: List<FileVersion> = emptyList(),
@SerialName("playback_variants") val playbackVariants: List<PlaybackVariant> = emptyList(),
val subtitles: List<SubtitleInfo> = emptyList(),
Expand Down Expand Up @@ -568,6 +570,13 @@ data class EpisodesResponse(

// --- User Data ---

/** Mean of all profiles' 1-5 star ratings ([average], one decimal) and how many there are. */
@Serializable
data class CommunityRating(
val average: Double = 0.0,
val count: Int = 0,
)

@Serializable
data class LeafItemUserData(
val played: Boolean = false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,9 @@ object ExternalRatings {
const val SOURCE_IMDB = "imdb"
const val SOURCE_TMDB = "tmdb"

/** Prairie's own aggregate of profile star ratings, not an external source. */
const val SOURCE_COMMUNITY = "community"

/**
* The most ratings a phone-width title page shows. Web and iOS use the
* same limit. The server already sends at most three, so this only guards
Expand Down Expand Up @@ -91,7 +94,29 @@ object ExternalRatings {
* of [ratings], in the server's order. Wider layouts pass the server's
* list as is.
*/
fun forPhone(ratings: List<DisplayRating>): List<DisplayRating> = ratings.take(PHONE_LIMIT)
fun forPhone(ratings: List<DisplayRating>): List<DisplayRating> {
val (community, external) = ratings.partition { it.source == SOURCE_COMMUNITY }
// The community rating rides after the limit so three external
// ratings never push it off a phone-width page.
return external.take(PHONE_LIMIT) + community
}

/**
* The community entry: `★ 4.2 · 7 ratings`. Null when the server omits
* `community_rating` (an older server, or fewer than three ratings) or
* sends a value outside 1-5.
*/
fun community(rating: CommunityRating?): DisplayRating? {
if (rating == null || rating.count <= 0 || !rating.average.isFinite()) return null
if (rating.average < 1.0 || rating.average > 5.0) return null
val noun = if (rating.count == 1) "rating" else "ratings"
return DisplayRating(
source = SOURCE_COMMUNITY,
name = "★",
score = rating.average * 20.0,
display = "${formatOneDecimal(rating.average)} · ${rating.count} $noun",
)
}

/** The one rating a hero shows from card fields: IMDb, else TMDB. */
fun primary(ratingImdb: Double?, ratingTmdb: Double?): DisplayRating? =
Expand Down Expand Up @@ -123,6 +148,10 @@ object ExternalRatings {
}
}

/** This title's ratings row; see [ExternalRatings.forTitle]. */
/**
* This title's ratings row: the external ratings (see
* [ExternalRatings.forTitle]) then, when present, the community rating.
*/
fun ItemDetail.titleRatings(): List<DisplayRating> =
ExternalRatings.forTitle(ratings, ratingImdb, ratingTmdb)
ExternalRatings.forTitle(ratings, ratingImdb, ratingTmdb) +
listOfNotNull(ExternalRatings.community(communityRating))
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ internal data class ItemDetailReadV2(
@SerialName("is_specials") val isSpecials: Boolean? = null,
@SerialName("user_data") val userData: LeafItemUserDataReadV2? = null,
@SerialName("user_rating") val userRating: Int? = null,
@SerialName("community_rating") val communityRating: CommunityRating? = null,
val versions: List<FileVersionReadV2>,
@SerialName("playback_variants") val playbackVariants: List<PlaybackVariantReadV2> = emptyList(),
val subtitles: List<SubtitleInfo>,
Expand Down Expand Up @@ -133,6 +134,7 @@ internal data class ItemDetailReadV2(
isSpecials = isSpecials,
userData = userData?.toDomain(),
userRating = userRating,
communityRating = communityRating,
versions = versions.map { it.toDomain() },
playbackVariants = playbackVariants.map { it.toDomain() },
subtitles = subtitles,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,4 +131,24 @@ class MediaSurfaceContractSerializationTest {
)
assertNull(detail.userRating)
}

@Test
fun decodesOptionalCommunityRatingOnItemDetail() {
val rated = json.decodeFromString<ItemDetail>(
"""
{ "content_id": "m3", "type": "movie", "title": "Popular Movie",
"community_rating": { "average": 4.2, "count": 7 } }
""".trimIndent(),
)
assertEquals(CommunityRating(average = 4.2, count = 7), rated.communityRating)
assertEquals("4.2 · 7 ratings", rated.titleRatings().single().display)

val unrated = json.decodeFromString<ItemDetail>(
"""
{ "content_id": "m4", "type": "movie", "title": "Quiet Movie" }
""".trimIndent(),
)
assertNull(unrated.communityRating)
assertEquals(emptyList(), unrated.titleRatings())
}
}
Loading