Files
sm-emby-share/lib/core/contracts/tmdb.dart
T

325 lines
11 KiB
Dart
Raw Normal View History

2026-07-14 11:11:36 +08:00
import 'package:freezed_annotation/freezed_annotation.dart';
part 'tmdb.freezed.dart';
part 'tmdb.g.dart';
@freezed
abstract class TmdbCastMember with _$TmdbCastMember {
const factory TmdbCastMember({
@JsonKey(fromJson: _intRequired) required int id,
@JsonKey(fromJson: _strOrEmpty) @Default('') String name,
String? character,
@JsonKey(name: 'profile_path') String? profilePath,
@JsonKey(fromJson: _intOrZero) @Default(0) int order,
}) = _TmdbCastMember;
factory TmdbCastMember.fromJson(Map<String, dynamic> json) =>
_$TmdbCastMemberFromJson(json);
}
@freezed
abstract class TmdbCrewMember with _$TmdbCrewMember {
const factory TmdbCrewMember({
@JsonKey(fromJson: _intRequired) required int id,
@JsonKey(fromJson: _strOrEmpty) @Default('') String name,
String? job,
String? department,
@JsonKey(name: 'profile_path') String? profilePath,
}) = _TmdbCrewMember;
factory TmdbCrewMember.fromJson(Map<String, dynamic> json) =>
_$TmdbCrewMemberFromJson(json);
}
@freezed
abstract class TmdbCreditsRes with _$TmdbCreditsRes {
const factory TmdbCreditsRes({
@Default(<TmdbCastMember>[]) List<TmdbCastMember> cast,
@Default(<TmdbCrewMember>[]) List<TmdbCrewMember> crew,
}) = _TmdbCreditsRes;
factory TmdbCreditsRes.fromJson(Map<String, dynamic> json) =>
_$TmdbCreditsResFromJson(json);
}
@freezed
abstract class TmdbRecommendation with _$TmdbRecommendation {
const factory TmdbRecommendation({
@JsonKey(fromJson: _intRequired) required int id,
required String title,
@JsonKey(name: 'poster_path') String? posterPath,
@JsonKey(name: 'backdrop_path') String? backdropPath,
String? overview,
@JsonKey(name: 'vote_average', fromJson: _doubleOrNull) double? voteAverage,
@JsonKey(name: 'media_type') String? mediaType,
}) = _TmdbRecommendation;
factory TmdbRecommendation.fromJson(Map<String, dynamic> json) =>
_$TmdbRecommendationFromJson({
...json,
'title': json['title'] ?? json['name'] ?? '',
});
}
@freezed
abstract class TmdbRecommendationsRes with _$TmdbRecommendationsRes {
const factory TmdbRecommendationsRes({
@Default(<TmdbRecommendation>[]) List<TmdbRecommendation> results,
@JsonKey(name: 'total_results', fromJson: _intOrZero)
@Default(0)
int totalResults,
}) = _TmdbRecommendationsRes;
factory TmdbRecommendationsRes.fromJson(Map<String, dynamic> json) =>
_$TmdbRecommendationsResFromJson(json);
}
@freezed
abstract class TmdbEpisode with _$TmdbEpisode {
const factory TmdbEpisode({
@JsonKey(name: 'episode_number', fromJson: _intRequired)
required int episodeNumber,
@JsonKey(fromJson: _strOrEmpty) @Default('') String name,
String? overview,
@JsonKey(name: 'still_path') String? stillPath,
@JsonKey(name: 'air_date') String? airDate,
@JsonKey(name: 'vote_average', fromJson: _doubleOrNull) double? voteAverage,
@JsonKey(fromJson: _intOrNull) int? runtime,
}) = _TmdbEpisode;
factory TmdbEpisode.fromJson(Map<String, dynamic> json) =>
_$TmdbEpisodeFromJson(json);
}
@freezed
abstract class TmdbSeasonDetail with _$TmdbSeasonDetail {
const factory TmdbSeasonDetail({
@JsonKey(name: 'season_number', fromJson: _intRequired)
required int seasonNumber,
@JsonKey(fromJson: _strOrEmpty) @Default('') String name,
String? overview,
@JsonKey(name: 'poster_path') String? posterPath,
@Default(<TmdbEpisode>[]) List<TmdbEpisode> episodes,
}) = _TmdbSeasonDetail;
factory TmdbSeasonDetail.fromJson(Map<String, dynamic> json) =>
_$TmdbSeasonDetailFromJson(json);
}
@freezed
abstract class TmdbVideo with _$TmdbVideo {
const TmdbVideo._();
const factory TmdbVideo({
@JsonKey(fromJson: _strOrEmpty) @Default('') String id,
@JsonKey(fromJson: _strOrEmpty) @Default('') String key,
@JsonKey(fromJson: _strOrEmpty) @Default('') String name,
@JsonKey(fromJson: _strOrEmpty) @Default('') String site,
@JsonKey(fromJson: _strOrEmpty) @Default('') String type,
@JsonKey(fromJson: _intOrZero) @Default(0) int size,
@JsonKey(fromJson: _boolOrFalse) @Default(false) bool official,
}) = _TmdbVideo;
factory TmdbVideo.fromJson(Map<String, dynamic> json) =>
_$TmdbVideoFromJson(json);
String? get youtubeUrl => site == 'YouTube' && key.isNotEmpty
? 'https://www.youtube.com/watch?v=$key'
: null;
String? get youtubeThumbnailUrl => site == 'YouTube' && key.isNotEmpty
? 'https://img.youtube.com/vi/$key/mqdefault.jpg'
: null;
}
@freezed
abstract class TmdbVideosRes with _$TmdbVideosRes {
const factory TmdbVideosRes({
@Default(<TmdbVideo>[]) List<TmdbVideo> results,
}) = _TmdbVideosRes;
factory TmdbVideosRes.fromJson(Map<String, dynamic> json) =>
_$TmdbVideosResFromJson(json);
}
@freezed
abstract class TmdbPersonDetail with _$TmdbPersonDetail {
const factory TmdbPersonDetail({
@JsonKey(fromJson: _intRequired) required int id,
@JsonKey(fromJson: _strOrEmpty) @Default('') String name,
String? biography,
String? birthday,
String? deathday,
@JsonKey(name: 'place_of_birth') String? placeOfBirth,
@JsonKey(name: 'profile_path') String? profilePath,
@JsonKey(name: 'known_for_department') String? knownForDepartment,
@JsonKey(fromJson: _doubleOrNull) double? popularity,
}) = _TmdbPersonDetail;
factory TmdbPersonDetail.fromJson(Map<String, dynamic> json) =>
_$TmdbPersonDetailFromJson(json);
}
@freezed
abstract class TmdbPersonCredit with _$TmdbPersonCredit {
const factory TmdbPersonCredit({
@JsonKey(fromJson: _intRequired) required int id,
required String title,
String? character,
@JsonKey(name: 'poster_path') String? posterPath,
@JsonKey(name: 'release_date') String? releaseDate,
@JsonKey(name: 'vote_average', fromJson: _doubleOrNull) double? voteAverage,
@JsonKey(name: 'media_type') String? mediaType,
}) = _TmdbPersonCredit;
factory TmdbPersonCredit.fromJson(Map<String, dynamic> json) =>
_$TmdbPersonCreditFromJson({
...json,
'title': json['title'] ?? json['name'] ?? '',
'release_date': json['release_date'] ?? json['first_air_date'],
});
}
@freezed
abstract class TmdbPersonCreditsRes with _$TmdbPersonCreditsRes {
const factory TmdbPersonCreditsRes({
@Default(<TmdbPersonCredit>[]) List<TmdbPersonCredit> cast,
}) = _TmdbPersonCreditsRes;
factory TmdbPersonCreditsRes.fromJson(Map<String, dynamic> json) =>
_$TmdbPersonCreditsResFromJson(json);
}
@freezed
abstract class TmdbGenre with _$TmdbGenre {
const factory TmdbGenre({
@JsonKey(fromJson: _intRequired) required int id,
@JsonKey(fromJson: _strOrEmpty) @Default('') String name,
}) = _TmdbGenre;
factory TmdbGenre.fromJson(Map<String, dynamic> json) =>
_$TmdbGenreFromJson(json);
}
@freezed
abstract class TmdbMediaDetail with _$TmdbMediaDetail {
const factory TmdbMediaDetail({
@JsonKey(fromJson: _intRequired) required int id,
@JsonKey(fromJson: _strOrEmpty) @Default('') String title,
@JsonKey(name: 'poster_path') String? posterPath,
@JsonKey(name: 'backdrop_path') String? backdropPath,
@JsonKey(name: 'release_date') String? releaseDate,
@Default(<TmdbGenre>[]) List<TmdbGenre> genres,
@JsonKey(fromJson: _intOrNull) int? runtime,
String? tagline,
String? overview,
String? status,
String? homepage,
@JsonKey(name: 'vote_average', fromJson: _doubleOrNull) double? voteAverage,
@JsonKey(name: 'vote_count', fromJson: _intOrNull) int? voteCount,
@JsonKey(fromJson: _intOrNull) int? revenue,
@JsonKey(fromJson: _intOrNull) int? budget,
}) = _TmdbMediaDetail;
factory TmdbMediaDetail.fromJson(Map<String, dynamic> json) =>
_$TmdbMediaDetailFromJson({
...json,
'title': json['title'] ?? json['name'] ?? '',
'release_date': json['release_date'] ?? json['first_air_date'],
'runtime':
json['runtime'] ??
(json['episode_run_time'] is List &&
(json['episode_run_time'] as List).isNotEmpty
? (json['episode_run_time'] as List).first
: null),
});
}
@freezed
abstract class TmdbImage with _$TmdbImage {
const TmdbImage._();
const factory TmdbImage({
@JsonKey(name: 'file_path', fromJson: _strOrEmpty)
@Default('')
String filePath,
@JsonKey(fromJson: _intOrZero) @Default(0) int width,
@JsonKey(fromJson: _intOrZero) @Default(0) int height,
@JsonKey(name: 'vote_average', fromJson: _doubleOrNull) double? voteAverage,
@JsonKey(name: 'iso_639_1') String? iso639,
}) = _TmdbImage;
factory TmdbImage.fromJson(Map<String, dynamic> json) =>
_$TmdbImageFromJson(json);
bool get isLanguageNeutral => iso639 == null;
}
@freezed
abstract class TmdbImageSet with _$TmdbImageSet {
const factory TmdbImageSet({
@Default(<TmdbImage>[]) List<TmdbImage> backdrops,
@Default(<TmdbImage>[]) List<TmdbImage> posters,
@Default(<TmdbImage>[]) List<TmdbImage> logos,
}) = _TmdbImageSet;
factory TmdbImageSet.fromJson(Map<String, dynamic> json) =>
_$TmdbImageSetFromJson(json);
}
@freezed
abstract class TmdbNetwork with _$TmdbNetwork {
const factory TmdbNetwork({
@JsonKey(fromJson: _intRequired) required int id,
@JsonKey(fromJson: _strOrEmpty) @Default('') String name,
@JsonKey(name: 'logo_path') String? logoPath,
@JsonKey(name: 'origin_country') String? originCountry,
}) = _TmdbNetwork;
factory TmdbNetwork.fromJson(Map<String, dynamic> json) =>
_$TmdbNetworkFromJson(json);
}
@freezed
abstract class TmdbEnrichedDetail with _$TmdbEnrichedDetail {
const factory TmdbEnrichedDetail({
required TmdbMediaDetail detail,
TmdbVideosRes? videos,
TmdbCreditsRes? credits,
TmdbRecommendationsRes? recommendations,
TmdbImageSet? images,
String? imdbId,
@Default(<TmdbNetwork>[]) List<TmdbNetwork> networks,
}) = _TmdbEnrichedDetail;
factory TmdbEnrichedDetail.fromJson(Map<String, dynamic> json) =>
_$TmdbEnrichedDetailFromJson({
'detail': json,
'videos': json['videos'],
'credits': json['credits'],
'recommendations': json['recommendations'],
'images': json['images'],
'imdbId': _imdbIdOf(json['external_ids']),
'networks': json['networks'],
});
}
String? _imdbIdOf(Object? externalIds) {
if (externalIds is! Map) return null;
final v = externalIds['imdb_id'];
if (v is! String) return null;
final trimmed = v.trim();
return trimmed.isEmpty ? null : trimmed;
}
int _intRequired(Object? v) => (v as num).toInt();
int _intOrZero(Object? v) => (v as num?)?.toInt() ?? 0;
int? _intOrNull(Object? v) => (v as num?)?.toInt();
double? _doubleOrNull(Object? v) => (v as num?)?.toDouble();
bool _boolOrFalse(Object? v) => (v as bool?) ?? false;
String _strOrEmpty(Object? v) => v?.toString() ?? '';