Files
plezy/lib/models/seerr/seerr_media.dart
T
edde746 27acbaf435 feat(explore): surface the catalog data providers already return
Explore shelf cards drew a poster, a title and a year. An audit of all six
catalog sources found the rest was lost at two boundaries — the wire-to-DTO
mapping and the DTO-to-CatalogItem mapping — and then simply not drawn: the
grid card fell through every branch of buildMetadataSubtitle to the year-only
case, while the list card used by search already composed certification,
runtime and rating from fields the synthesized MediaItem already held.

Extend CatalogItem with the neutral facts every provider had been dropping:
attributed rating sources, leaderboard ranks that keep their season window,
audience counters that keep their timeframe, broadcast slots, next-episode air
times, server availability and request state, exact release dates, alternate
titles, format, source material, studios, countries, languages, credits, tags,
links, artwork variants, play state, gallery art and background prose. Replace
fetchCast and fetchRelated with one fetchDetail returning the enriched item,
its cast, its recommendations and labelled franchise relations without adding
a request: sources needing two calls keep two and run them concurrently with
isolated failures.

Map those fields in all six sources, widening only field selections that cost
no extra round trip — MAL's fields list, AniList's selection set and a bounded
row cast that lets detail skip its character call, Trakt's guest stars, Seerr's
language parameter and TMDB size ladder, and Plex's includeUserState. Plex hub
artwork widens only on TV, where the spotlight is its only consumer, because it
doubles the payload.

Render them: a rating-first caption and bounded badges on the shelf card,
labelled sections on the detail screen, provider hub styles and result counts
on shelves, and logo, banner and accent art in the TV spotlight.

Verified against live Plex, AniList, Simkl and MAL responses, and on a Pixel 7.
2026-07-29 06:47:54 +02:00

144 lines
4.1 KiB
Dart

import 'package:json_annotation/json_annotation.dart';
import '../../utils/json_utils.dart';
import 'seerr_request.dart';
part 'seerr_media.g.dart';
/// Seerr availability of a title/season on the linked media server
/// (`MediaInfo.status`).
enum SeerrMediaStatus {
unknown(1),
pending(2),
processing(3),
partiallyAvailable(4),
available(5),
deleted(6);
final int code;
const SeerrMediaStatus(this.code);
static SeerrMediaStatus fromCode(int? code) =>
values.where((v) => v.code == code).firstOrNull ?? SeerrMediaStatus.unknown;
}
/// A movie or TV entry from Seerr's TMDB-backed discover/search endpoints.
///
/// TMDB uses `title`/`releaseDate` for movies and `name`/`firstAirDate` for
/// TV; [displayTitle]/[date] paper over the split. `mediaType` is absent on
/// the single-type discover endpoints — the client coerces it there.
@JsonSerializable(createToJson: false)
class SeerrMedia {
final int id;
final String? mediaType;
final String? title;
final String? name;
final String? overview;
final String? posterPath;
final String? backdropPath;
final String? releaseDate;
final String? firstAirDate;
final double? voteAverage;
final int? voteCount;
final SeerrMediaInfo? mediaInfo;
final double? popularity;
final String? originalLanguage;
final String? originalTitle;
final String? originalName;
final bool? adult;
final List<String>? originCountry;
const SeerrMedia({
required this.id,
this.mediaType,
this.title,
this.name,
this.overview,
this.posterPath,
this.backdropPath,
this.releaseDate,
this.firstAirDate,
this.voteAverage,
this.voteCount,
this.mediaInfo,
this.popularity,
this.originalLanguage,
this.originalTitle,
this.originalName,
this.adult,
this.originCountry,
});
bool get isMovie => mediaType == 'movie';
/// Localized title, falling back to the original when the requested
/// language has no translation. TMDB answers a `language` query with an
/// empty string rather than omitting the field, so `??` alone is not enough.
String get displayTitle => firstNonBlank([title, name, originalTitle, originalName]) ?? '';
String? get date => releaseDate ?? firstAirDate;
String? get displayOriginalTitle => originalTitle ?? originalName;
int? get year {
final d = date;
if (d == null || d.length < 4) return null;
return int.tryParse(d.substring(0, 4));
}
factory SeerrMedia.fromJson(Map<String, dynamic> json) => _$SeerrMediaFromJson(json);
}
/// Seerr's knowledge of a title on the linked media server: availability
/// status plus any open requests. Absent entirely for titles Seerr has
/// never seen.
@JsonSerializable(createToJson: false)
class SeerrMediaInfo {
final int? id;
final int? tmdbId;
final int? tvdbId;
@JsonKey(name: 'status', fromJson: SeerrMediaStatus.fromCode)
final SeerrMediaStatus status;
@JsonKey(name: 'status4k', fromJson: SeerrMediaStatus.fromCode)
final SeerrMediaStatus status4k;
/// TV only: per-season availability.
final List<SeerrSeasonInfo>? seasons;
/// Open/settled requests for this title (used to disable already-requested
/// seasons in the request sheet).
final List<SeerrRequest>? requests;
const SeerrMediaInfo({
this.id,
this.tmdbId,
this.tvdbId,
this.status = SeerrMediaStatus.unknown,
this.status4k = SeerrMediaStatus.unknown,
this.seasons,
this.requests,
});
factory SeerrMediaInfo.fromJson(Map<String, dynamic> json) => _$SeerrMediaInfoFromJson(json);
}
/// Availability of one season (`MediaInfo.seasons[]`).
@JsonSerializable(createToJson: false)
class SeerrSeasonInfo {
final int seasonNumber;
@JsonKey(name: 'status', fromJson: SeerrMediaStatus.fromCode)
final SeerrMediaStatus status;
@JsonKey(name: 'status4k', fromJson: SeerrMediaStatus.fromCode)
final SeerrMediaStatus status4k;
const SeerrSeasonInfo({
required this.seasonNumber,
this.status = SeerrMediaStatus.unknown,
this.status4k = SeerrMediaStatus.unknown,
});
factory SeerrSeasonInfo.fromJson(Map<String, dynamic> json) => _$SeerrSeasonInfoFromJson(json);
}