feat(api): carry poster and rating through search and lists

Widen the unified search so both §9.2 halves can render a thumbnail
and a rating: TMDB results gain vote_average/vote_count from the
search bodies, library results read the #145 artwork columns, and
episodes take their series' art. GET /api/movies and /api/series gain
the same two fields for the #151 grid. Mirrors in web updated.
This commit is contained in:
Miguel Palhas
2026-08-23 22:45:08 +01:00
parent 30382585ea
commit 5e2bb5dff7
21 changed files with 357 additions and 66 deletions
+36 -3
View File
@@ -12,7 +12,9 @@ use serde::{Deserialize, Serialize};
const RELEASE_TYPE_DIGITAL: u8 = 4;
/// One row of a TMDB search.
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
///
/// `vote_average` is the one float, so this is `PartialEq` only.
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct MovieSearchResult {
/// TMDB's own id, the key everything else hangs off.
pub tmdb_id: u32,
@@ -30,6 +32,11 @@ pub struct MovieSearchResult {
pub overview: Option<String>,
/// Path fragment, not a URL. TMDB's image base is a separate concern.
pub poster_path: Option<String>,
/// TMDB's rating, out of 10, when TMDB has votes for it; zero normalised
/// away like [`Movie::vote_average`].
pub vote_average: Option<f64>,
/// How many votes the rating rests on.
pub vote_count: u32,
}
impl MovieSearchResult {
@@ -86,6 +93,8 @@ pub struct Movie {
/// artwork (§9.6). TMDB sends `0` where "no rating yet" is meant, so a
/// zero rating is normalised away here rather than stored as one.
pub vote_average: Option<f64>,
/// How many votes the rating rests on.
pub vote_count: u32,
}
impl Movie {
@@ -106,7 +115,7 @@ impl Movie {
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct SeriesSearchResult {
pub tmdb_id: u32,
pub title: String,
@@ -114,6 +123,10 @@ pub struct SeriesSearchResult {
pub first_air_date: Option<NaiveDate>,
pub overview: Option<String>,
pub poster_path: Option<String>,
/// Zero normalised away, like [`Movie::vote_average`].
pub vote_average: Option<f64>,
/// How many votes the rating rests on.
pub vote_count: u32,
}
impl SeriesSearchResult {
@@ -141,6 +154,8 @@ pub struct Series {
/// TMDB's rating, out of 10, when TMDB has votes for it. Stored with the
/// artwork (§9.6); zero is normalised away like [`Movie::vote_average`].
pub vote_average: Option<f64>,
/// How many votes the rating rests on.
pub vote_count: u32,
pub seasons: Vec<SeasonSummary>,
}
@@ -310,6 +325,10 @@ pub(crate) struct RawSeriesSearchResult {
overview: Option<String>,
#[serde(default)]
poster_path: Option<String>,
#[serde(default)]
vote_average: f64,
#[serde(default)]
vote_count: u32,
}
impl From<RawSeriesSearchResult> for SeriesSearchResult {
@@ -321,6 +340,8 @@ impl From<RawSeriesSearchResult> for SeriesSearchResult {
first_air_date: raw.first_air_date.as_deref().and_then(parse_date),
overview: non_empty(raw.overview),
poster_path: non_empty(raw.poster_path),
vote_average: rating(raw.vote_average),
vote_count: raw.vote_count,
}
}
}
@@ -345,6 +366,8 @@ pub(crate) struct RawSeries {
#[serde(default)]
vote_average: f64,
#[serde(default)]
vote_count: u32,
#[serde(default)]
seasons: Vec<RawSeasonSummary>,
#[serde(default)]
external_ids: Option<RawExternalIds>,
@@ -382,6 +405,7 @@ impl From<RawSeries> for Series {
poster_path: non_empty(raw.poster_path),
backdrop_path: non_empty(raw.backdrop_path),
vote_average: rating(raw.vote_average),
vote_count: raw.vote_count,
seasons: raw
.seasons
.into_iter()
@@ -436,7 +460,7 @@ impl From<RawSeason> for Season {
/// What one `IMDb` id resolved to. An id names one title, so at most one of
/// the two lists is non-empty — but TMDB answers both kinds in the same
/// response, and both are surfaced rather than filtered here.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
#[derive(Debug, Clone, Default, PartialEq)]
pub struct FindResults {
pub movies: Vec<MovieSearchResult>,
pub series: Vec<SeriesSearchResult>,
@@ -465,6 +489,10 @@ pub(crate) struct RawSearchResult {
overview: Option<String>,
#[serde(default)]
poster_path: Option<String>,
#[serde(default)]
vote_average: f64,
#[serde(default)]
vote_count: u32,
}
impl From<RawSearchResult> for MovieSearchResult {
@@ -477,6 +505,8 @@ impl From<RawSearchResult> for MovieSearchResult {
release_date: raw.release_date.as_deref().and_then(parse_date),
overview: non_empty(raw.overview),
poster_path: non_empty(raw.poster_path),
vote_average: rating(raw.vote_average),
vote_count: raw.vote_count,
}
}
}
@@ -511,6 +541,8 @@ pub(crate) struct RawMovie {
#[serde(default)]
vote_average: f64,
#[serde(default)]
vote_count: u32,
#[serde(default)]
release_dates: Option<RawReleaseDates>,
}
@@ -572,6 +604,7 @@ impl From<RawMovie> for Movie {
poster_path: non_empty(raw.poster_path),
backdrop_path: non_empty(raw.backdrop_path),
vote_average: rating(raw.vote_average),
vote_count: raw.vote_count,
}
}
}