feat(seerr): Seerr API, account provider, and Explore catalog source
This commit is contained in:
@@ -0,0 +1,153 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
import 'seerr_media.dart';
|
||||
|
||||
part 'seerr_details.g.dart';
|
||||
|
||||
/// Full movie detail from `GET /movie/{tmdbId}` — the subset the catalog
|
||||
/// surfaces need (credits, external ids, availability, air status).
|
||||
@JsonSerializable(createToJson: false)
|
||||
class SeerrMovieDetails {
|
||||
final int id;
|
||||
final String? title;
|
||||
final String? overview;
|
||||
final String? posterPath;
|
||||
final String? backdropPath;
|
||||
final String? releaseDate;
|
||||
|
||||
/// Minutes.
|
||||
final int? runtime;
|
||||
|
||||
/// `Released` / `In Production` / `Post Production` / `Planned` /
|
||||
/// `Canceled` / `Rumored`.
|
||||
final String? status;
|
||||
final double? voteAverage;
|
||||
final int? voteCount;
|
||||
final List<SeerrGenre>? genres;
|
||||
final SeerrCredits? credits;
|
||||
final SeerrExternalIds? externalIds;
|
||||
final SeerrMediaInfo? mediaInfo;
|
||||
|
||||
const SeerrMovieDetails({
|
||||
required this.id,
|
||||
this.title,
|
||||
this.overview,
|
||||
this.posterPath,
|
||||
this.backdropPath,
|
||||
this.releaseDate,
|
||||
this.runtime,
|
||||
this.status,
|
||||
this.voteAverage,
|
||||
this.voteCount,
|
||||
this.genres,
|
||||
this.credits,
|
||||
this.externalIds,
|
||||
this.mediaInfo,
|
||||
});
|
||||
|
||||
factory SeerrMovieDetails.fromJson(Map<String, dynamic> json) => _$SeerrMovieDetailsFromJson(json);
|
||||
}
|
||||
|
||||
/// Full TV detail from `GET /tv/{tmdbId}`.
|
||||
@JsonSerializable(createToJson: false)
|
||||
class SeerrTvDetails {
|
||||
final int id;
|
||||
final String? name;
|
||||
final String? overview;
|
||||
final String? posterPath;
|
||||
final String? backdropPath;
|
||||
final String? firstAirDate;
|
||||
final List<int>? episodeRunTime;
|
||||
|
||||
/// `Returning Series` / `Ended` / `Canceled` / `In Production` /
|
||||
/// `Planned` / `Pilot`.
|
||||
final String? status;
|
||||
final double? voteAverage;
|
||||
final int? voteCount;
|
||||
final List<SeerrGenre>? genres;
|
||||
final List<SeerrNetwork>? networks;
|
||||
final int? numberOfEpisodes;
|
||||
final int? numberOfSeasons;
|
||||
final List<SeerrSeason>? seasons;
|
||||
final SeerrCredits? credits;
|
||||
final SeerrExternalIds? externalIds;
|
||||
final SeerrMediaInfo? mediaInfo;
|
||||
|
||||
const SeerrTvDetails({
|
||||
required this.id,
|
||||
this.name,
|
||||
this.overview,
|
||||
this.posterPath,
|
||||
this.backdropPath,
|
||||
this.firstAirDate,
|
||||
this.episodeRunTime,
|
||||
this.status,
|
||||
this.voteAverage,
|
||||
this.voteCount,
|
||||
this.genres,
|
||||
this.networks,
|
||||
this.numberOfEpisodes,
|
||||
this.numberOfSeasons,
|
||||
this.seasons,
|
||||
this.credits,
|
||||
this.externalIds,
|
||||
this.mediaInfo,
|
||||
});
|
||||
|
||||
factory SeerrTvDetails.fromJson(Map<String, dynamic> json) => _$SeerrTvDetailsFromJson(json);
|
||||
}
|
||||
|
||||
@JsonSerializable(createToJson: false)
|
||||
class SeerrGenre {
|
||||
final String? name;
|
||||
const SeerrGenre({this.name});
|
||||
factory SeerrGenre.fromJson(Map<String, dynamic> json) => _$SeerrGenreFromJson(json);
|
||||
}
|
||||
|
||||
@JsonSerializable(createToJson: false)
|
||||
class SeerrNetwork {
|
||||
final String? name;
|
||||
const SeerrNetwork({this.name});
|
||||
factory SeerrNetwork.fromJson(Map<String, dynamic> json) => _$SeerrNetworkFromJson(json);
|
||||
}
|
||||
|
||||
/// One TMDB season entry (`TvDetails.seasons[]`). Season 0 is specials.
|
||||
@JsonSerializable(createToJson: false)
|
||||
class SeerrSeason {
|
||||
final int seasonNumber;
|
||||
final String? name;
|
||||
final int? episodeCount;
|
||||
final String? airDate;
|
||||
|
||||
const SeerrSeason({required this.seasonNumber, this.name, this.episodeCount, this.airDate});
|
||||
|
||||
factory SeerrSeason.fromJson(Map<String, dynamic> json) => _$SeerrSeasonFromJson(json);
|
||||
}
|
||||
|
||||
@JsonSerializable(createToJson: false)
|
||||
class SeerrCredits {
|
||||
final List<SeerrCastMember>? cast;
|
||||
const SeerrCredits({this.cast});
|
||||
factory SeerrCredits.fromJson(Map<String, dynamic> json) => _$SeerrCreditsFromJson(json);
|
||||
}
|
||||
|
||||
@JsonSerializable(createToJson: false)
|
||||
class SeerrCastMember {
|
||||
final String? name;
|
||||
final String? character;
|
||||
final String? profilePath;
|
||||
|
||||
const SeerrCastMember({this.name, this.character, this.profilePath});
|
||||
|
||||
factory SeerrCastMember.fromJson(Map<String, dynamic> json) => _$SeerrCastMemberFromJson(json);
|
||||
}
|
||||
|
||||
@JsonSerializable(createToJson: false)
|
||||
class SeerrExternalIds {
|
||||
final String? imdbId;
|
||||
final int? tvdbId;
|
||||
|
||||
const SeerrExternalIds({this.imdbId, this.tvdbId});
|
||||
|
||||
factory SeerrExternalIds.fromJson(Map<String, dynamic> json) => _$SeerrExternalIdsFromJson(json);
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'seerr_details.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
SeerrMovieDetails _$SeerrMovieDetailsFromJson(Map<String, dynamic> json) =>
|
||||
SeerrMovieDetails(
|
||||
id: (json['id'] as num).toInt(),
|
||||
title: json['title'] as String?,
|
||||
overview: json['overview'] as String?,
|
||||
posterPath: json['posterPath'] as String?,
|
||||
backdropPath: json['backdropPath'] as String?,
|
||||
releaseDate: json['releaseDate'] as String?,
|
||||
runtime: (json['runtime'] as num?)?.toInt(),
|
||||
status: json['status'] as String?,
|
||||
voteAverage: (json['voteAverage'] as num?)?.toDouble(),
|
||||
voteCount: (json['voteCount'] as num?)?.toInt(),
|
||||
genres: (json['genres'] as List<dynamic>?)
|
||||
?.map((e) => SeerrGenre.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
credits: json['credits'] == null
|
||||
? null
|
||||
: SeerrCredits.fromJson(json['credits'] as Map<String, dynamic>),
|
||||
externalIds: json['externalIds'] == null
|
||||
? null
|
||||
: SeerrExternalIds.fromJson(
|
||||
json['externalIds'] as Map<String, dynamic>,
|
||||
),
|
||||
mediaInfo: json['mediaInfo'] == null
|
||||
? null
|
||||
: SeerrMediaInfo.fromJson(json['mediaInfo'] as Map<String, dynamic>),
|
||||
);
|
||||
|
||||
SeerrTvDetails _$SeerrTvDetailsFromJson(Map<String, dynamic> json) =>
|
||||
SeerrTvDetails(
|
||||
id: (json['id'] as num).toInt(),
|
||||
name: json['name'] as String?,
|
||||
overview: json['overview'] as String?,
|
||||
posterPath: json['posterPath'] as String?,
|
||||
backdropPath: json['backdropPath'] as String?,
|
||||
firstAirDate: json['firstAirDate'] as String?,
|
||||
episodeRunTime: (json['episodeRunTime'] as List<dynamic>?)
|
||||
?.map((e) => (e as num).toInt())
|
||||
.toList(),
|
||||
status: json['status'] as String?,
|
||||
voteAverage: (json['voteAverage'] as num?)?.toDouble(),
|
||||
voteCount: (json['voteCount'] as num?)?.toInt(),
|
||||
genres: (json['genres'] as List<dynamic>?)
|
||||
?.map((e) => SeerrGenre.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
networks: (json['networks'] as List<dynamic>?)
|
||||
?.map((e) => SeerrNetwork.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
numberOfEpisodes: (json['numberOfEpisodes'] as num?)?.toInt(),
|
||||
numberOfSeasons: (json['numberOfSeasons'] as num?)?.toInt(),
|
||||
seasons: (json['seasons'] as List<dynamic>?)
|
||||
?.map((e) => SeerrSeason.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
credits: json['credits'] == null
|
||||
? null
|
||||
: SeerrCredits.fromJson(json['credits'] as Map<String, dynamic>),
|
||||
externalIds: json['externalIds'] == null
|
||||
? null
|
||||
: SeerrExternalIds.fromJson(
|
||||
json['externalIds'] as Map<String, dynamic>,
|
||||
),
|
||||
mediaInfo: json['mediaInfo'] == null
|
||||
? null
|
||||
: SeerrMediaInfo.fromJson(json['mediaInfo'] as Map<String, dynamic>),
|
||||
);
|
||||
|
||||
SeerrGenre _$SeerrGenreFromJson(Map<String, dynamic> json) =>
|
||||
SeerrGenre(name: json['name'] as String?);
|
||||
|
||||
SeerrNetwork _$SeerrNetworkFromJson(Map<String, dynamic> json) =>
|
||||
SeerrNetwork(name: json['name'] as String?);
|
||||
|
||||
SeerrSeason _$SeerrSeasonFromJson(Map<String, dynamic> json) => SeerrSeason(
|
||||
seasonNumber: (json['seasonNumber'] as num).toInt(),
|
||||
name: json['name'] as String?,
|
||||
episodeCount: (json['episodeCount'] as num?)?.toInt(),
|
||||
airDate: json['airDate'] as String?,
|
||||
);
|
||||
|
||||
SeerrCredits _$SeerrCreditsFromJson(Map<String, dynamic> json) => SeerrCredits(
|
||||
cast: (json['cast'] as List<dynamic>?)
|
||||
?.map((e) => SeerrCastMember.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
);
|
||||
|
||||
SeerrCastMember _$SeerrCastMemberFromJson(Map<String, dynamic> json) =>
|
||||
SeerrCastMember(
|
||||
name: json['name'] as String?,
|
||||
character: json['character'] as String?,
|
||||
profilePath: json['profilePath'] as String?,
|
||||
);
|
||||
|
||||
SeerrExternalIds _$SeerrExternalIdsFromJson(Map<String, dynamic> json) =>
|
||||
SeerrExternalIds(
|
||||
imdbId: json['imdbId'] as String?,
|
||||
tvdbId: (json['tvdbId'] as num?)?.toInt(),
|
||||
);
|
||||
@@ -0,0 +1,124 @@
|
||||
import 'package:json_annotation/json_annotation.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;
|
||||
|
||||
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,
|
||||
});
|
||||
|
||||
bool get isMovie => mediaType == 'movie';
|
||||
|
||||
String get displayTitle => title ?? name ?? '';
|
||||
|
||||
String? get date => releaseDate ?? firstAirDate;
|
||||
|
||||
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);
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'seerr_media.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
SeerrMedia _$SeerrMediaFromJson(Map<String, dynamic> json) => SeerrMedia(
|
||||
id: (json['id'] as num).toInt(),
|
||||
mediaType: json['mediaType'] as String?,
|
||||
title: json['title'] as String?,
|
||||
name: json['name'] as String?,
|
||||
overview: json['overview'] as String?,
|
||||
posterPath: json['posterPath'] as String?,
|
||||
backdropPath: json['backdropPath'] as String?,
|
||||
releaseDate: json['releaseDate'] as String?,
|
||||
firstAirDate: json['firstAirDate'] as String?,
|
||||
voteAverage: (json['voteAverage'] as num?)?.toDouble(),
|
||||
voteCount: (json['voteCount'] as num?)?.toInt(),
|
||||
mediaInfo: json['mediaInfo'] == null
|
||||
? null
|
||||
: SeerrMediaInfo.fromJson(json['mediaInfo'] as Map<String, dynamic>),
|
||||
);
|
||||
|
||||
SeerrMediaInfo _$SeerrMediaInfoFromJson(Map<String, dynamic> json) =>
|
||||
SeerrMediaInfo(
|
||||
id: (json['id'] as num?)?.toInt(),
|
||||
tmdbId: (json['tmdbId'] as num?)?.toInt(),
|
||||
tvdbId: (json['tvdbId'] as num?)?.toInt(),
|
||||
status: json['status'] == null
|
||||
? SeerrMediaStatus.unknown
|
||||
: SeerrMediaStatus.fromCode((json['status'] as num?)?.toInt()),
|
||||
status4k: json['status4k'] == null
|
||||
? SeerrMediaStatus.unknown
|
||||
: SeerrMediaStatus.fromCode((json['status4k'] as num?)?.toInt()),
|
||||
seasons: (json['seasons'] as List<dynamic>?)
|
||||
?.map((e) => SeerrSeasonInfo.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
requests: (json['requests'] as List<dynamic>?)
|
||||
?.map((e) => SeerrRequest.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
);
|
||||
|
||||
SeerrSeasonInfo _$SeerrSeasonInfoFromJson(Map<String, dynamic> json) =>
|
||||
SeerrSeasonInfo(
|
||||
seasonNumber: (json['seasonNumber'] as num).toInt(),
|
||||
status: json['status'] == null
|
||||
? SeerrMediaStatus.unknown
|
||||
: SeerrMediaStatus.fromCode((json['status'] as num?)?.toInt()),
|
||||
status4k: json['status4k'] == null
|
||||
? SeerrMediaStatus.unknown
|
||||
: SeerrMediaStatus.fromCode((json['status4k'] as num?)?.toInt()),
|
||||
);
|
||||
@@ -0,0 +1,27 @@
|
||||
/// One page of a paginated Seerr response. Discover/search endpoints use the
|
||||
/// TMDB shape `{page, totalPages, results}`; Seerr's own listings (e.g.
|
||||
/// `GET /request`) use `{pageInfo: {page, pages}, results}` — both parse.
|
||||
class SeerrPage<T> {
|
||||
final List<T> items;
|
||||
final bool hasMore;
|
||||
|
||||
const SeerrPage({required this.items, required this.hasMore});
|
||||
|
||||
/// [skip] drops results the mapper can't represent (person entries in
|
||||
/// mixed trending/search results) — return null from [fromItem] for those.
|
||||
factory SeerrPage.fromJson(Map<String, dynamic> json, T? Function(Map<String, dynamic>) fromItem) {
|
||||
final info = json['pageInfo'] is Map<String, dynamic> ? json['pageInfo'] as Map<String, dynamic> : json;
|
||||
final page = (info['page'] as num?)?.toInt() ?? 1;
|
||||
final totalPages = ((info['pages'] ?? info['totalPages']) as num?)?.toInt() ?? page;
|
||||
final results = json['results'];
|
||||
return SeerrPage(
|
||||
items: [
|
||||
if (results is List)
|
||||
for (final item in results)
|
||||
if (item is Map<String, dynamic>)
|
||||
if (fromItem(item) case final T parsed) parsed,
|
||||
],
|
||||
hasMore: page < totalPages,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
part 'seerr_public_settings.g.dart';
|
||||
|
||||
/// `GET /settings/public` — unauthenticated instance metadata. The connect
|
||||
/// flow keys the offered sign-in methods off it; the request sheet keys the
|
||||
/// 4K toggle and per-season selection off it.
|
||||
@JsonSerializable(createToJson: false)
|
||||
class SeerrPublicSettings {
|
||||
final bool initialized;
|
||||
final String? applicationTitle;
|
||||
|
||||
/// Whether `/auth/local` is enabled.
|
||||
final bool localLogin;
|
||||
|
||||
/// Whether signing in through the linked media server is enabled.
|
||||
final bool mediaServerLogin;
|
||||
|
||||
/// `SeerrMediaServerType` of the linked media server.
|
||||
final int? mediaServerType;
|
||||
|
||||
final bool movie4kEnabled;
|
||||
final bool series4kEnabled;
|
||||
|
||||
/// Whether users may request individual seasons rather than whole shows.
|
||||
final bool partialRequestsEnabled;
|
||||
|
||||
const SeerrPublicSettings({
|
||||
this.initialized = false,
|
||||
this.applicationTitle,
|
||||
this.localLogin = true,
|
||||
this.mediaServerLogin = true,
|
||||
this.mediaServerType,
|
||||
this.movie4kEnabled = false,
|
||||
this.series4kEnabled = false,
|
||||
this.partialRequestsEnabled = true,
|
||||
});
|
||||
|
||||
String get instanceLabel => (applicationTitle?.isNotEmpty ?? false) ? applicationTitle! : 'Seerr';
|
||||
|
||||
factory SeerrPublicSettings.fromJson(Map<String, dynamic> json) => _$SeerrPublicSettingsFromJson(json);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'seerr_public_settings.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
SeerrPublicSettings _$SeerrPublicSettingsFromJson(Map<String, dynamic> json) =>
|
||||
SeerrPublicSettings(
|
||||
initialized: json['initialized'] as bool? ?? false,
|
||||
applicationTitle: json['applicationTitle'] as String?,
|
||||
localLogin: json['localLogin'] as bool? ?? true,
|
||||
mediaServerLogin: json['mediaServerLogin'] as bool? ?? true,
|
||||
mediaServerType: (json['mediaServerType'] as num?)?.toInt(),
|
||||
movie4kEnabled: json['movie4kEnabled'] as bool? ?? false,
|
||||
series4kEnabled: json['series4kEnabled'] as bool? ?? false,
|
||||
partialRequestsEnabled: json['partialRequestsEnabled'] as bool? ?? true,
|
||||
);
|
||||
@@ -0,0 +1,84 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
import 'seerr_media.dart';
|
||||
|
||||
part 'seerr_request.g.dart';
|
||||
|
||||
/// Approval state of a Seerr request (`MediaRequest.status`).
|
||||
enum SeerrRequestStatus {
|
||||
pending(1),
|
||||
approved(2),
|
||||
declined(3);
|
||||
|
||||
final int code;
|
||||
const SeerrRequestStatus(this.code);
|
||||
|
||||
static SeerrRequestStatus fromCode(int? code) =>
|
||||
values.where((v) => v.code == code).firstOrNull ?? SeerrRequestStatus.pending;
|
||||
}
|
||||
|
||||
/// A media request as returned by `POST /request` and `GET /request`.
|
||||
@JsonSerializable(createToJson: false)
|
||||
class SeerrRequest {
|
||||
final int id;
|
||||
@JsonKey(name: 'status', fromJson: SeerrRequestStatus.fromCode)
|
||||
final SeerrRequestStatus status;
|
||||
final bool? is4k;
|
||||
final SeerrMediaInfo? media;
|
||||
|
||||
/// TV only: the seasons this request covers.
|
||||
final List<SeerrRequestSeason>? seasons;
|
||||
|
||||
const SeerrRequest({required this.id, required this.status, this.is4k, this.media, this.seasons});
|
||||
|
||||
factory SeerrRequest.fromJson(Map<String, dynamic> json) => _$SeerrRequestFromJson(json);
|
||||
}
|
||||
|
||||
/// One season within a request (`MediaRequest.seasons[]`).
|
||||
@JsonSerializable(createToJson: false)
|
||||
class SeerrRequestSeason {
|
||||
final int seasonNumber;
|
||||
|
||||
const SeerrRequestSeason({required this.seasonNumber});
|
||||
|
||||
factory SeerrRequestSeason.fromJson(Map<String, dynamic> json) => _$SeerrRequestSeasonFromJson(json);
|
||||
}
|
||||
|
||||
/// Body of `POST /request`. Advanced fields require `REQUEST_ADVANCED`;
|
||||
/// `is4k` requires the 4K request permissions.
|
||||
class SeerrRequestPayload {
|
||||
final String mediaType;
|
||||
|
||||
/// TMDB id.
|
||||
final int mediaId;
|
||||
|
||||
/// TV only: season numbers, or null for `all`.
|
||||
final List<int>? seasons;
|
||||
final bool is4k;
|
||||
final int? serverId;
|
||||
final int? profileId;
|
||||
final String? rootFolder;
|
||||
final int? languageProfileId;
|
||||
|
||||
const SeerrRequestPayload({
|
||||
required this.mediaType,
|
||||
required this.mediaId,
|
||||
this.seasons,
|
||||
this.is4k = false,
|
||||
this.serverId,
|
||||
this.profileId,
|
||||
this.rootFolder,
|
||||
this.languageProfileId,
|
||||
});
|
||||
|
||||
Map<String, Object?> toJson() => {
|
||||
'mediaType': mediaType,
|
||||
'mediaId': mediaId,
|
||||
if (mediaType == 'tv') 'seasons': seasons ?? 'all',
|
||||
'is4k': is4k,
|
||||
if (serverId != null) 'serverId': serverId,
|
||||
if (profileId != null) 'profileId': profileId,
|
||||
if (rootFolder != null) 'rootFolder': rootFolder,
|
||||
if (languageProfileId != null) 'languageProfileId': languageProfileId,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'seerr_request.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
SeerrRequest _$SeerrRequestFromJson(Map<String, dynamic> json) => SeerrRequest(
|
||||
id: (json['id'] as num).toInt(),
|
||||
status: SeerrRequestStatus.fromCode((json['status'] as num?)?.toInt()),
|
||||
is4k: json['is4k'] as bool?,
|
||||
media: json['media'] == null
|
||||
? null
|
||||
: SeerrMediaInfo.fromJson(json['media'] as Map<String, dynamic>),
|
||||
seasons: (json['seasons'] as List<dynamic>?)
|
||||
?.map((e) => SeerrRequestSeason.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
);
|
||||
|
||||
SeerrRequestSeason _$SeerrRequestSeasonFromJson(Map<String, dynamic> json) =>
|
||||
SeerrRequestSeason(seasonNumber: (json['seasonNumber'] as num).toInt());
|
||||
@@ -0,0 +1,66 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
part 'seerr_service.g.dart';
|
||||
|
||||
/// One configured Radarr/Sonarr instance (`GET /service/radarr|sonarr`).
|
||||
@JsonSerializable(createToJson: false)
|
||||
class SeerrServiceInstance {
|
||||
final int id;
|
||||
final String? name;
|
||||
final bool is4k;
|
||||
final bool isDefault;
|
||||
final String? activeDirectory;
|
||||
final int? activeProfileId;
|
||||
|
||||
/// Sonarr only.
|
||||
final int? activeLanguageProfileId;
|
||||
|
||||
const SeerrServiceInstance({
|
||||
required this.id,
|
||||
this.name,
|
||||
this.is4k = false,
|
||||
this.isDefault = false,
|
||||
this.activeDirectory,
|
||||
this.activeProfileId,
|
||||
this.activeLanguageProfileId,
|
||||
});
|
||||
|
||||
factory SeerrServiceInstance.fromJson(Map<String, dynamic> json) => _$SeerrServiceInstanceFromJson(json);
|
||||
}
|
||||
|
||||
/// Quality profile / root folder / language profile options of one instance
|
||||
/// (`GET /service/radarr|sonarr/{id}`).
|
||||
@JsonSerializable(createToJson: false)
|
||||
class SeerrServiceDetail {
|
||||
final SeerrServiceInstance? server;
|
||||
final List<SeerrServiceProfile>? profiles;
|
||||
final List<SeerrRootFolder>? rootFolders;
|
||||
|
||||
/// Sonarr v3 only; absent on Radarr and newer Sonarr.
|
||||
final List<SeerrServiceProfile>? languageProfiles;
|
||||
|
||||
const SeerrServiceDetail({this.server, this.profiles, this.rootFolders, this.languageProfiles});
|
||||
|
||||
factory SeerrServiceDetail.fromJson(Map<String, dynamic> json) => _$SeerrServiceDetailFromJson(json);
|
||||
}
|
||||
|
||||
/// Quality or language profile option `{id, name}`.
|
||||
@JsonSerializable(createToJson: false)
|
||||
class SeerrServiceProfile {
|
||||
final int id;
|
||||
final String? name;
|
||||
|
||||
const SeerrServiceProfile({required this.id, this.name});
|
||||
|
||||
factory SeerrServiceProfile.fromJson(Map<String, dynamic> json) => _$SeerrServiceProfileFromJson(json);
|
||||
}
|
||||
|
||||
@JsonSerializable(createToJson: false)
|
||||
class SeerrRootFolder {
|
||||
final int id;
|
||||
final String? path;
|
||||
|
||||
const SeerrRootFolder({required this.id, this.path});
|
||||
|
||||
factory SeerrRootFolder.fromJson(Map<String, dynamic> json) => _$SeerrRootFolderFromJson(json);
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'seerr_service.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
SeerrServiceInstance _$SeerrServiceInstanceFromJson(
|
||||
Map<String, dynamic> json,
|
||||
) => SeerrServiceInstance(
|
||||
id: (json['id'] as num).toInt(),
|
||||
name: json['name'] as String?,
|
||||
is4k: json['is4k'] as bool? ?? false,
|
||||
isDefault: json['isDefault'] as bool? ?? false,
|
||||
activeDirectory: json['activeDirectory'] as String?,
|
||||
activeProfileId: (json['activeProfileId'] as num?)?.toInt(),
|
||||
activeLanguageProfileId: (json['activeLanguageProfileId'] as num?)?.toInt(),
|
||||
);
|
||||
|
||||
SeerrServiceDetail _$SeerrServiceDetailFromJson(Map<String, dynamic> json) =>
|
||||
SeerrServiceDetail(
|
||||
server: json['server'] == null
|
||||
? null
|
||||
: SeerrServiceInstance.fromJson(
|
||||
json['server'] as Map<String, dynamic>,
|
||||
),
|
||||
profiles: (json['profiles'] as List<dynamic>?)
|
||||
?.map((e) => SeerrServiceProfile.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
rootFolders: (json['rootFolders'] as List<dynamic>?)
|
||||
?.map((e) => SeerrRootFolder.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
languageProfiles: (json['languageProfiles'] as List<dynamic>?)
|
||||
?.map((e) => SeerrServiceProfile.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
);
|
||||
|
||||
SeerrServiceProfile _$SeerrServiceProfileFromJson(Map<String, dynamic> json) =>
|
||||
SeerrServiceProfile(
|
||||
id: (json['id'] as num).toInt(),
|
||||
name: json['name'] as String?,
|
||||
);
|
||||
|
||||
SeerrRootFolder _$SeerrRootFolderFromJson(Map<String, dynamic> json) =>
|
||||
SeerrRootFolder(
|
||||
id: (json['id'] as num).toInt(),
|
||||
path: json['path'] as String?,
|
||||
);
|
||||
@@ -0,0 +1,113 @@
|
||||
import 'dart:convert';
|
||||
|
||||
/// How the session was established — determines how a silent re-login is
|
||||
/// performed when the server-side session expires.
|
||||
enum SeerrAuthMethod {
|
||||
/// `POST /auth/plex` with the profile's Plex account token (read live at
|
||||
/// re-auth time, never copied into the session).
|
||||
plex,
|
||||
|
||||
/// `POST /auth/jellyfin` with stored username/password, serverType 2.
|
||||
jellyfin,
|
||||
|
||||
/// `POST /auth/jellyfin` with stored username/password, serverType 3.
|
||||
emby,
|
||||
|
||||
/// `POST /auth/local` with stored email/password.
|
||||
local,
|
||||
}
|
||||
|
||||
/// An authenticated Seerr session for one profile: instance URL, the Express
|
||||
/// session cookie, the credentials needed to re-login silently, and the
|
||||
/// Seerr-side user it maps to.
|
||||
///
|
||||
/// [secret] is the plaintext password while in memory; the store protects it
|
||||
/// with CredentialVault before persisting. Empty for [SeerrAuthMethod.plex]
|
||||
/// and after an unrecoverable decrypt failure (session then lives until the
|
||||
/// cookie expires and the user must reconnect).
|
||||
class SeerrSession {
|
||||
final String baseUrl;
|
||||
final SeerrAuthMethod method;
|
||||
|
||||
/// Username (jellyfin/emby) or email (local); empty for plex.
|
||||
final String identifier;
|
||||
final String secret;
|
||||
|
||||
/// `connect.sid` cookie value.
|
||||
final String cookie;
|
||||
final int userId;
|
||||
|
||||
/// Seerr permission bitmask — see `SeerrPermission`.
|
||||
final int permissions;
|
||||
final String displayName;
|
||||
|
||||
/// Instance `applicationTitle` from `/settings/public`.
|
||||
final String instanceLabel;
|
||||
final int createdAt;
|
||||
|
||||
const SeerrSession({
|
||||
required this.baseUrl,
|
||||
required this.method,
|
||||
required this.identifier,
|
||||
required this.secret,
|
||||
required this.cookie,
|
||||
required this.userId,
|
||||
required this.permissions,
|
||||
required this.displayName,
|
||||
required this.instanceLabel,
|
||||
required this.createdAt,
|
||||
});
|
||||
|
||||
SeerrSession copyWith({
|
||||
String? secret,
|
||||
String? cookie,
|
||||
int? permissions,
|
||||
String? displayName,
|
||||
String? instanceLabel,
|
||||
}) => SeerrSession(
|
||||
baseUrl: baseUrl,
|
||||
method: method,
|
||||
identifier: identifier,
|
||||
secret: secret ?? this.secret,
|
||||
cookie: cookie ?? this.cookie,
|
||||
userId: userId,
|
||||
permissions: permissions ?? this.permissions,
|
||||
displayName: displayName ?? this.displayName,
|
||||
instanceLabel: instanceLabel ?? this.instanceLabel,
|
||||
createdAt: createdAt,
|
||||
);
|
||||
|
||||
Map<String, Object?> toJson() => {
|
||||
'base_url': baseUrl,
|
||||
'method': method.name,
|
||||
'identifier': identifier,
|
||||
'secret': secret,
|
||||
'cookie': cookie,
|
||||
'user_id': userId,
|
||||
'permissions': permissions,
|
||||
'display_name': displayName,
|
||||
'instance_label': instanceLabel,
|
||||
'created_at': createdAt,
|
||||
};
|
||||
|
||||
factory SeerrSession.fromJson(Map<String, Object?> json) => SeerrSession(
|
||||
baseUrl: json['base_url'] as String,
|
||||
// An unknown method must not fall back to another (re-auth would post
|
||||
// garbage credentials); the store's decode try/catch drops the session.
|
||||
method:
|
||||
SeerrAuthMethod.values.asNameMap()[json['method']] ??
|
||||
(throw ArgumentError('Unknown Seerr auth method: ${json['method']}')),
|
||||
identifier: json['identifier'] as String? ?? '',
|
||||
secret: json['secret'] as String? ?? '',
|
||||
cookie: json['cookie'] as String? ?? '',
|
||||
userId: (json['user_id'] as num).toInt(),
|
||||
permissions: (json['permissions'] as num?)?.toInt() ?? 0,
|
||||
displayName: json['display_name'] as String? ?? '',
|
||||
instanceLabel: json['instance_label'] as String? ?? '',
|
||||
createdAt: (json['created_at'] as num?)?.toInt() ?? 0,
|
||||
);
|
||||
|
||||
String encode() => jsonEncode(toJson());
|
||||
|
||||
static SeerrSession decode(String raw) => SeerrSession.fromJson((jsonDecode(raw) as Map).cast<String, Object?>());
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import 'package:json_annotation/json_annotation.dart';
|
||||
|
||||
part 'seerr_user.g.dart';
|
||||
|
||||
/// The authenticated Seerr user, as returned by the `/auth/*` login
|
||||
/// endpoints and `GET /auth/me`.
|
||||
@JsonSerializable(createToJson: false)
|
||||
class SeerrUser {
|
||||
final int id;
|
||||
final String? displayName;
|
||||
final String? email;
|
||||
final int? permissions;
|
||||
final String? avatar;
|
||||
|
||||
const SeerrUser({required this.id, this.displayName, this.email, this.permissions, this.avatar});
|
||||
|
||||
factory SeerrUser.fromJson(Map<String, dynamic> json) => _$SeerrUserFromJson(json);
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'seerr_user.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
SeerrUser _$SeerrUserFromJson(Map<String, dynamic> json) => SeerrUser(
|
||||
id: (json['id'] as num).toInt(),
|
||||
displayName: json['displayName'] as String?,
|
||||
email: json['email'] as String?,
|
||||
permissions: (json['permissions'] as num?)?.toInt(),
|
||||
avatar: json['avatar'] as String?,
|
||||
);
|
||||
Reference in New Issue
Block a user