feat(explore): add AniList, Simkl, and Plex catalogs

This commit is contained in:
edde746
2026-07-20 10:28:31 +02:00
parent f8cb550be7
commit e32fcc2190
78 changed files with 4327 additions and 199 deletions
@@ -0,0 +1,76 @@
import 'package:json_annotation/json_annotation.dart';
import '../../utils/json_utils.dart';
import 'simkl_ids.dart';
part 'simkl_all_items_entry.g.dart';
@JsonSerializable(createToJson: false)
class SimklAllItemsMedia {
final String? title;
@JsonKey(fromJson: flexibleInt)
final int? year;
final String? poster;
final String? fanart;
@JsonKey(fromJson: flexibleInt)
final int? runtime;
final String? overview;
final List<String>? genres;
final String? network;
final String? status;
final SimklIds ids;
const SimklAllItemsMedia({
this.title,
this.year,
this.poster,
this.fanart,
this.runtime,
this.overview,
this.genres,
this.network,
this.status,
required this.ids,
});
factory SimklAllItemsMedia.fromJson(Map<String, dynamic> json) => _$SimklAllItemsMediaFromJson(json);
}
@JsonSerializable(createToJson: false)
class SimklAllItemsEntry {
/// User-list status (`plantowatch`, `watching`, ...), not production state.
final String? status;
final SimklAllItemsMedia? show;
final SimklAllItemsMedia? movie;
@JsonKey(name: 'anime_type')
final String? animeType;
@JsonKey(name: 'total_episodes_count', fromJson: flexibleInt)
final int? totalEpisodes;
const SimklAllItemsEntry({this.status, this.show, this.movie, this.animeType, this.totalEpisodes});
SimklAllItemsMedia? get media => movie ?? show;
bool get isShow => show != null;
factory SimklAllItemsEntry.fromJson(Map<String, dynamic> json) => _$SimklAllItemsEntryFromJson(json);
}
class SimklAllItems {
final List<SimklAllItemsEntry> shows;
final List<SimklAllItemsEntry> movies;
final List<SimklAllItemsEntry> anime;
const SimklAllItems({this.shows = const [], this.movies = const [], this.anime = const []});
factory SimklAllItems.fromJson(Map<String, dynamic> json) =>
SimklAllItems(shows: _entries(json['shows']), movies: _entries(json['movies']), anime: _entries(json['anime']));
static List<SimklAllItemsEntry> _entries(Object? value) {
if (value is! List) return const [];
return [
for (final entry in value)
if (entry is Map<String, dynamic>) SimklAllItemsEntry.fromJson(entry),
];
}
}
@@ -0,0 +1,36 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'simkl_all_items_entry.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
SimklAllItemsMedia _$SimklAllItemsMediaFromJson(Map<String, dynamic> json) =>
SimklAllItemsMedia(
title: json['title'] as String?,
year: flexibleInt(json['year']),
poster: json['poster'] as String?,
fanart: json['fanart'] as String?,
runtime: flexibleInt(json['runtime']),
overview: json['overview'] as String?,
genres: (json['genres'] as List<dynamic>?)
?.map((e) => e as String)
.toList(),
network: json['network'] as String?,
status: json['status'] as String?,
ids: SimklIds.fromJson(json['ids'] as Map<String, dynamic>),
);
SimklAllItemsEntry _$SimklAllItemsEntryFromJson(Map<String, dynamic> json) =>
SimklAllItemsEntry(
status: json['status'] as String?,
show: json['show'] == null
? null
: SimklAllItemsMedia.fromJson(json['show'] as Map<String, dynamic>),
movie: json['movie'] == null
? null
: SimklAllItemsMedia.fromJson(json['movie'] as Map<String, dynamic>),
animeType: json['anime_type'] as String?,
totalEpisodes: flexibleInt(json['total_episodes_count']),
);
+21
View File
@@ -0,0 +1,21 @@
import 'package:json_annotation/json_annotation.dart';
import '../../utils/json_utils.dart';
import 'simkl_ids.dart';
import 'simkl_rating.dart';
part 'simkl_best_item.g.dart';
@JsonSerializable(createToJson: false)
class SimklBestItem {
final String? title;
@JsonKey(fromJson: flexibleInt)
final int? year;
final String? poster;
final SimklIds ids;
final SimklRatings? ratings;
const SimklBestItem({this.title, this.year, this.poster, required this.ids, this.ratings});
factory SimklBestItem.fromJson(Map<String, dynamic> json) => _$SimklBestItemFromJson(json);
}
+18
View File
@@ -0,0 +1,18 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'simkl_best_item.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
SimklBestItem _$SimklBestItemFromJson(Map<String, dynamic> json) =>
SimklBestItem(
title: json['title'] as String?,
year: flexibleInt(json['year']),
poster: json['poster'] as String?,
ids: SimklIds.fromJson(json['ids'] as Map<String, dynamic>),
ratings: json['ratings'] == null
? null
: SimklRatings.fromJson(json['ratings'] as Map<String, dynamic>),
);
+51
View File
@@ -0,0 +1,51 @@
import 'package:json_annotation/json_annotation.dart';
import '../../utils/json_utils.dart';
import '../catalog/catalog_item.dart';
part 'simkl_ids.g.dart';
Object? _readSimklId(Map json, String key) => json['simkl'] ?? json['simkl_id'];
/// Simkl's identifier block. CDN feeds call the native key `simkl_id`, while
/// API responses and mutation bodies use `simkl`.
@JsonSerializable(includeIfNull: false)
class SimklIds {
@JsonKey(readValue: _readSimklId, fromJson: flexibleInt)
final int? simkl;
/// Response-only according to Simkl's current API contract.
@JsonKey(includeToJson: false)
final String? slug;
final String? imdb;
@JsonKey(fromJson: flexibleInt)
final int? tmdb;
@JsonKey(fromJson: flexibleInt)
final int? tvdb;
@JsonKey(fromJson: flexibleInt)
final int? mal;
@JsonKey(fromJson: flexibleInt)
final int? anilist;
const SimklIds({this.simkl, this.slug, this.imdb, this.tmdb, this.tvdb, this.mal, this.anilist});
bool get hasAny => simkl != null || imdb != null || tmdb != null || tvdb != null || mal != null || anilist != null;
CatalogItemIds toCatalogItemIds() =>
CatalogItemIds(simkl: simkl, slug: slug, imdb: imdb, tmdb: tmdb, tvdb: tvdb, mal: mal, anilist: anilist);
factory SimklIds.fromCatalogItemIds(CatalogItemIds ids) => SimklIds(
simkl: ids.simkl,
slug: ids.slug,
imdb: ids.imdb,
tmdb: ids.tmdb,
tvdb: ids.tvdb,
mal: ids.mal,
anilist: ids.anilist,
);
Map<String, dynamic> toJson() => _$SimklIdsToJson(this);
factory SimklIds.fromJson(Map<String, dynamic> json) => _$SimklIdsFromJson(json);
}
+26
View File
@@ -0,0 +1,26 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'simkl_ids.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
SimklIds _$SimklIdsFromJson(Map<String, dynamic> json) => SimklIds(
simkl: flexibleInt(_readSimklId(json, 'simkl')),
slug: json['slug'] as String?,
imdb: json['imdb'] as String?,
tmdb: flexibleInt(json['tmdb']),
tvdb: flexibleInt(json['tvdb']),
mal: flexibleInt(json['mal']),
anilist: flexibleInt(json['anilist']),
);
Map<String, dynamic> _$SimklIdsToJson(SimklIds instance) => <String, dynamic>{
'simkl': ?instance.simkl,
'imdb': ?instance.imdb,
'tmdb': ?instance.tmdb,
'tvdb': ?instance.tvdb,
'mal': ?instance.mal,
'anilist': ?instance.anilist,
};
+9
View File
@@ -0,0 +1,9 @@
String? simklPosterUrl(String? hash) {
if (hash == null || hash.isEmpty) return null;
return 'https://simkl.in/posters/${hash}_m.webp';
}
String? simklFanartUrl(String? hash) {
if (hash == null || hash.isEmpty) return null;
return 'https://simkl.in/fanart/${hash}_medium.webp';
}
+30
View File
@@ -0,0 +1,30 @@
import 'package:json_annotation/json_annotation.dart';
import '../../utils/json_utils.dart';
part 'simkl_rating.g.dart';
@JsonSerializable(createToJson: false)
class SimklRating {
@JsonKey(fromJson: flexibleDouble)
final double? rating;
@JsonKey(fromJson: flexibleInt)
final int? votes;
const SimklRating({this.rating, this.votes});
factory SimklRating.fromJson(Map<String, dynamic> json) => _$SimklRatingFromJson(json);
}
@JsonSerializable(createToJson: false)
class SimklRatings {
final SimklRating? simkl;
final SimklRating? imdb;
final SimklRating? mal;
const SimklRatings({this.simkl, this.imdb, this.mal});
SimklRating? get primary => simkl ?? mal ?? imdb;
factory SimklRatings.fromJson(Map<String, dynamic> json) => _$SimklRatingsFromJson(json);
}
+24
View File
@@ -0,0 +1,24 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'simkl_rating.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
SimklRating _$SimklRatingFromJson(Map<String, dynamic> json) => SimklRating(
rating: flexibleDouble(json['rating']),
votes: flexibleInt(json['votes']),
);
SimklRatings _$SimklRatingsFromJson(Map<String, dynamic> json) => SimklRatings(
simkl: json['simkl'] == null
? null
: SimklRating.fromJson(json['simkl'] as Map<String, dynamic>),
imdb: json['imdb'] == null
? null
: SimklRating.fromJson(json['imdb'] as Map<String, dynamic>),
mal: json['mal'] == null
? null
: SimklRating.fromJson(json['mal'] as Map<String, dynamic>),
);
@@ -0,0 +1,20 @@
import 'package:json_annotation/json_annotation.dart';
import '../../utils/json_utils.dart';
import 'simkl_ids.dart';
part 'simkl_recommendation.g.dart';
@JsonSerializable(createToJson: false)
class SimklRecommendation {
final String? title;
@JsonKey(fromJson: flexibleInt)
final int? year;
final String? poster;
final String? type;
final SimklIds ids;
const SimklRecommendation({this.title, this.year, this.poster, this.type, required this.ids});
factory SimklRecommendation.fromJson(Map<String, dynamic> json) => _$SimklRecommendationFromJson(json);
}
@@ -0,0 +1,16 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'simkl_recommendation.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
SimklRecommendation _$SimklRecommendationFromJson(Map<String, dynamic> json) =>
SimklRecommendation(
title: json['title'] as String?,
year: flexibleInt(json['year']),
poster: json['poster'] as String?,
type: json['type'] as String?,
ids: SimklIds.fromJson(json['ids'] as Map<String, dynamic>),
);
+39
View File
@@ -0,0 +1,39 @@
import 'package:json_annotation/json_annotation.dart';
import '../../utils/json_utils.dart';
import 'simkl_ids.dart';
import 'simkl_rating.dart';
part 'simkl_search_result.g.dart';
@JsonSerializable(createToJson: false)
class SimklSearchResult {
final String? title;
@JsonKey(fromJson: flexibleInt)
final int? year;
/// Anime format (`tv`, `movie`, `ova`, ...).
final String? type;
@JsonKey(name: 'endpoint_type')
final String? endpointType;
final String? poster;
final SimklIds ids;
@JsonKey(name: 'ep_count', fromJson: flexibleInt)
final int? episodeCount;
final String? status;
final SimklRatings? ratings;
const SimklSearchResult({
this.title,
this.year,
this.type,
this.endpointType,
this.poster,
required this.ids,
this.episodeCount,
this.status,
this.ratings,
});
factory SimklSearchResult.fromJson(Map<String, dynamic> json) => _$SimklSearchResultFromJson(json);
}
@@ -0,0 +1,22 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'simkl_search_result.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
SimklSearchResult _$SimklSearchResultFromJson(Map<String, dynamic> json) =>
SimklSearchResult(
title: json['title'] as String?,
year: flexibleInt(json['year']),
type: json['type'] as String?,
endpointType: json['endpoint_type'] as String?,
poster: json['poster'] as String?,
ids: SimklIds.fromJson(json['ids'] as Map<String, dynamic>),
episodeCount: flexibleInt(json['ep_count']),
status: json['status'] as String?,
ratings: json['ratings'] == null
? null
: SimklRatings.fromJson(json['ratings'] as Map<String, dynamic>),
);
+68
View File
@@ -0,0 +1,68 @@
import 'package:json_annotation/json_annotation.dart';
import '../../utils/json_utils.dart';
import 'simkl_ids.dart';
import 'simkl_rating.dart';
part 'simkl_trending_item.g.dart';
@JsonSerializable(createToJson: false)
class SimklTrendingItem {
final String? title;
final String? url;
final String? poster;
final String? fanart;
@JsonKey(name: 'release_date')
final String? releaseDate;
final String? runtime;
final String? status;
final List<String>? genres;
final String? trailer;
final String? overview;
final SimklRatings? ratings;
@JsonKey(name: 'total_episodes', fromJson: flexibleInt)
final int? totalEpisodes;
final String? network;
@JsonKey(name: 'anime_type')
final String? animeType;
final SimklIds ids;
const SimklTrendingItem({
this.title,
this.url,
this.poster,
this.fanart,
this.releaseDate,
this.runtime,
this.status,
this.genres,
this.trailer,
this.overview,
this.ratings,
this.totalEpisodes,
this.network,
this.animeType,
required this.ids,
});
int? get year {
final parts = releaseDate?.split('/');
if (parts == null || parts.length != 3) return null;
return int.tryParse(parts[2]);
}
int? get runtimeMinutes {
final value = runtime?.trim();
if (value == null || value.isEmpty) return null;
final match = RegExp(r'^(?:(\d+)\s*h)?(?:\s*(\d+)\s*m)?$', caseSensitive: false).firstMatch(value);
if (match == null || (match.group(1) == null && match.group(2) == null)) return null;
final hours = int.tryParse(match.group(1) ?? '') ?? 0;
final minutes = int.tryParse(match.group(2) ?? '') ?? 0;
final total = hours * 60 + minutes;
return total > 0 ? total : null;
}
String? get trailerUrl => trailer == null || trailer!.isEmpty ? null : 'https://www.youtube.com/watch?v=$trailer';
factory SimklTrendingItem.fromJson(Map<String, dynamic> json) => _$SimklTrendingItemFromJson(json);
}
@@ -0,0 +1,30 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'simkl_trending_item.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
SimklTrendingItem _$SimklTrendingItemFromJson(Map<String, dynamic> json) =>
SimklTrendingItem(
title: json['title'] as String?,
url: json['url'] as String?,
poster: json['poster'] as String?,
fanart: json['fanart'] as String?,
releaseDate: json['release_date'] as String?,
runtime: json['runtime'] as String?,
status: json['status'] as String?,
genres: (json['genres'] as List<dynamic>?)
?.map((e) => e as String)
.toList(),
trailer: json['trailer'] as String?,
overview: json['overview'] as String?,
ratings: json['ratings'] == null
? null
: SimklRatings.fromJson(json['ratings'] as Map<String, dynamic>),
totalEpisodes: flexibleInt(json['total_episodes']),
network: json['network'] as String?,
animeType: json['anime_type'] as String?,
ids: SimklIds.fromJson(json['ids'] as Map<String, dynamic>),
);