Files
plezy/lib/models/seerr/seerr_request.dart
edde746 a029b1a7c8 fix(seerr): decode media status per product and model failed/completed request states
Jellyseerr shifts MediaStatus codes 6/7 relative to Overseerr (6=blocklisted, 7=deleted vs 6=deleted), and the shared unconditional mapping misread both. The product is now detected from the presence of mediaServerType in /settings/public, persisted on the session, and used to resolve raw wire codes; blocklisted titles are shown as non-requestable.

Request statuses failed(4) and completed(5) previously decoded as pending, blocking re-requests and mislabeling failures; both are now modeled and active requests are defined positively as pending or approved. Seerr also marks a request Failed on arr-push failure while leaving the media status Processing, and precedence checked processing first, so failed titles rendered as Processing and the request sheet kept blocking re-requests; a failed request with no live pending/approved request now wins over stale pipeline status in both the catalog state and the sheet's blocked labels.

Reauthentication completing from a stale session snapshot wholesale-replaced the session and could downgrade a concurrently detected product discriminator back to unknown permanently (the settings cache never reapplies it); adoption now merges the known product.
2026-08-21 19:23:42 +02:00

92 lines
2.7 KiB
Dart

import 'package:json_annotation/json_annotation.dart';
import 'seerr_media.dart';
part 'seerr_request.g.dart';
/// Approval state of a Seerr request (`MediaRequest.status`) — identical
/// wire codes in Overseerr and Jellyseerr. FAILED is set when the arr
/// submission is rejected; COMPLETED once the request's media is available.
enum SeerrRequestStatus {
pending(1),
approved(2),
declined(3),
failed(4),
completed(5);
final int code;
const SeerrRequestStatus(this.code);
/// An unrecognized code deliberately falls back to [pending]: it renders
/// as "requested" and blocks re-submission, the conservative reading for
/// a state this client does not understand.
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,
};
}