feat(explore): add AniList, Simkl, and Plex catalogs
This commit is contained in:
@@ -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']),
|
||||
);
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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>),
|
||||
);
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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';
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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>),
|
||||
);
|
||||
@@ -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>),
|
||||
);
|
||||
@@ -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>),
|
||||
);
|
||||
Reference in New Issue
Block a user