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? 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 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? seasons; /// Open/settled requests for this title (used to disable already-requested /// seasons in the request sheet). final List? 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 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 json) => _$SeerrSeasonInfoFromJson(json); }