fix(media-requests): incorrect availability mapping (#4520)

This commit is contained in:
Meier Lukas
2025-11-21 19:05:51 +01:00
committed by GitHub
parent 5a418d73bd
commit d9cc35b985
8 changed files with 322 additions and 202 deletions

View File

@@ -38,7 +38,7 @@ export type {
FirewallMemorySummary,
} from "./interfaces/firewall-summary/firewall-summary-types";
export type { SystemHealthMonitoring } from "./interfaces/health-monitoring/health-monitoring-types";
export { MediaRequestStatus } from "./interfaces/media-requests/media-request-types";
export { UpstreamMediaRequestStatus } from "./interfaces/media-requests/media-request-types";
export type { MediaRequestList, MediaRequestStats } from "./interfaces/media-requests/media-request-types";
export type { StreamSession } from "./interfaces/media-server/media-server-types";
export type {

View File

@@ -1,3 +1,5 @@
import { objectKeys } from "@homarr/common";
interface SerieSeason {
id: number;
seasonNumber: number;
@@ -34,6 +36,64 @@ export interface MediaRequest {
requestedBy?: Omit<RequestUser, "requestCount">;
}
export const mediaAvailabilityConfiguration = {
available: {
color: "green",
},
partiallyAvailable: {
color: "yellow",
},
processing: {
color: "blue",
},
requested: {
color: "violet",
},
pending: {
color: "violet",
},
unknown: {
color: "orange",
},
deleted: {
color: "red",
},
blacklisted: {
color: "gray",
},
} satisfies Record<string, { color: string }>;
export const mediaAvailabilities = objectKeys(mediaAvailabilityConfiguration);
export type MediaAvailability = (typeof mediaAvailabilities)[number];
export const mediaRequestStatusConfiguration = {
pending: {
color: "blue",
position: 1,
},
approved: {
color: "green",
position: 2,
},
declined: {
color: "red",
position: 3,
},
failed: {
color: "red",
position: 4,
},
completed: {
color: "green",
position: 5,
},
} satisfies Record<string, { color: string; position: number }>;
export const mediaRequestStatuses = objectKeys(mediaRequestStatusConfiguration);
export type MediaRequestStatus = (typeof mediaRequestStatuses)[number];
export interface MediaRequestList {
integration: {
id: string;
@@ -66,7 +126,7 @@ export interface MediaRequestStats {
}
// https://github.com/fallenbagel/jellyseerr/blob/0fd03f38480f853e7015ad9229ed98160e37602e/server/constants/media.ts#L1
export enum MediaRequestStatus {
export enum UpstreamMediaRequestStatus {
PendingApproval = 1,
Approved = 2,
Declined = 3,
@@ -75,12 +135,12 @@ export enum MediaRequestStatus {
}
// https://github.com/fallenbagel/jellyseerr/blob/0fd03f38480f853e7015ad9229ed98160e37602e/server/constants/media.ts#L14
export enum MediaAvailability {
export enum UpstreamMediaAvailability {
Unknown = 1,
Pending = 2,
Processing = 3,
PartiallyAvailable = 4,
Available = 5,
Blacklisted = 6,
Deleted = 7,
JellyseerrBlacklistedOrOverseerrDeleted = 6,
JellyseerrDeleted = 7,
}

View File

@@ -1,3 +1,12 @@
import type { MediaAvailability } from "../interfaces/media-requests/media-request-types";
import { UpstreamMediaAvailability } from "../interfaces/media-requests/media-request-types";
import { OverseerrIntegration } from "../overseerr/overseerr-integration";
export class JellyseerrIntegration extends OverseerrIntegration {}
export class JellyseerrIntegration extends OverseerrIntegration {
protected override mapAvailability(availability: UpstreamMediaAvailability, inProgress: boolean): MediaAvailability {
// Availability statuses are not exactly the same between Jellyseerr and Overseerr (Jellyseerr has "blacklisted" additionally (deleted is the same value in overseerr))
if (availability === UpstreamMediaAvailability.JellyseerrBlacklistedOrOverseerrDeleted) return "blacklisted";
if (availability === UpstreamMediaAvailability.JellyseerrDeleted) return "deleted";
return super.mapAvailability(availability, inProgress);
}
}

View File

@@ -1,8 +1,13 @@
import { objectEntries } from "@homarr/common";
import type { IMediaRequestIntegration } from "../../interfaces/media-requests/media-request-integration";
import type { MediaInformation, MediaRequest, RequestStats, RequestUser } from "../../types";
import { MediaAvailability, MediaRequestStatus } from "../../types";
import type {
MediaAvailability,
MediaInformation,
MediaRequest,
MediaRequestStatus,
RequestStats,
RequestUser,
} from "../../types";
import { mediaAvailabilities, mediaRequestStatuses } from "../../types";
export class MediaRequestMockService implements IMediaRequestIntegration {
public async getSeriesInformationAsync(mediaType: "movie" | "tv", id: number): Promise<MediaInformation> {
@@ -86,12 +91,10 @@ export class MediaRequestMockService implements IMediaRequestIntegration {
}
private static randomAvailability(): MediaAvailability {
const values = objectEntries(MediaAvailability).filter(([key]) => typeof key === "number");
return values[Math.floor(Math.random() * values.length)]?.[1] ?? MediaAvailability.Available;
return mediaAvailabilities.at(Math.floor(Math.random() * mediaAvailabilities.length)) ?? "unknown";
}
private static randomStatus(): MediaRequestStatus {
const values = objectEntries(MediaRequestStatus).filter(([key]) => typeof key === "number");
return values[Math.floor(Math.random() * values.length)]?.[1] ?? MediaRequestStatus.PendingApproval;
return mediaRequestStatuses.at(Math.floor(Math.random() * mediaRequestStatuses.length)) ?? "pending";
}
}

View File

@@ -9,8 +9,17 @@ import type { ISearchableIntegration } from "../base/searchable-integration";
import { TestConnectionError } from "../base/test-connection/test-connection-error";
import type { TestingResult } from "../base/test-connection/test-connection-service";
import type { IMediaRequestIntegration } from "../interfaces/media-requests/media-request-integration";
import type { MediaRequest, RequestStats, RequestUser } from "../interfaces/media-requests/media-request-types";
import { MediaAvailability, MediaRequestStatus } from "../interfaces/media-requests/media-request-types";
import type {
MediaAvailability,
MediaRequest,
MediaRequestStatus,
RequestStats,
RequestUser,
} from "../interfaces/media-requests/media-request-types";
import {
UpstreamMediaAvailability,
UpstreamMediaRequestStatus,
} from "../interfaces/media-requests/media-request-types";
interface OverseerrSearchResult {
id: number;
@@ -128,7 +137,7 @@ export class OverseerrIntegration
if (pendingResults.length > 0 && allResults.length > 0) {
requests = pendingResults.concat(
allResults.filter(({ status }) => status !== MediaRequestStatus.PendingApproval),
allResults.filter(({ status }) => status !== UpstreamMediaRequestStatus.PendingApproval),
);
} else if (pendingResults.length > 0) requests = pendingResults;
else if (allResults.length > 0) requests = allResults;
@@ -137,11 +146,15 @@ export class OverseerrIntegration
return await Promise.all(
requests.map(async (request): Promise<MediaRequest> => {
const information = await this.getItemInformationAsync(request.media.tmdbId, request.type);
// See https://github.com/seerr-team/seerr/blob/af083a3cd5c3e3d5d7917fdf4fdd67fe3f39c46b/src/components/StatusBadge/index.tsx#L40
const inProgress = (request.media.downloadStatus ?? []).length >= 1;
return {
id: request.id,
name: information.name,
status: request.status,
availability: request.media.status,
status: this.mapRequestStatus(request.status),
availability: this.mapAvailability(request.media.status, inProgress),
backdropImageUrl: `https://image.tmdb.org/t/p/original/${information.backdropPath}`,
posterImagePath: `https://image.tmdb.org/t/p/w600_and_h900_bestv2/${information.posterPath}`,
href: this.externalUrl(`/${request.type}/${request.media.tmdbId}`).toString(),
@@ -161,6 +174,42 @@ export class OverseerrIntegration
);
}
protected mapRequestStatus(status: UpstreamMediaRequestStatus): MediaRequestStatus {
switch (status) {
case UpstreamMediaRequestStatus.PendingApproval:
return "pending";
case UpstreamMediaRequestStatus.Approved:
return "approved";
case UpstreamMediaRequestStatus.Declined:
return "declined";
case UpstreamMediaRequestStatus.Failed:
return "failed";
case UpstreamMediaRequestStatus.Completed:
return "completed";
default:
return "failed";
}
}
// See https://github.com/seerr-team/seerr/blob/af083a3cd5c3e3d5d7917fdf4fdd67fe3f39c46b/src/components/StatusBadge/index.tsx#L153-L387
protected mapAvailability(availability: UpstreamMediaAvailability, inProgress: boolean): MediaAvailability {
switch (availability) {
case UpstreamMediaAvailability.Available:
return inProgress ? "processing" : "available";
case UpstreamMediaAvailability.PartiallyAvailable:
return inProgress ? "processing" : "partiallyAvailable";
case UpstreamMediaAvailability.Processing:
return inProgress ? "processing" : "requested";
case UpstreamMediaAvailability.Pending:
return "pending";
case UpstreamMediaAvailability.JellyseerrBlacklistedOrOverseerrDeleted:
return "deleted";
case UpstreamMediaAvailability.Unknown:
default:
return inProgress ? "processing" : "unknown";
}
}
public async getStatsAsync(): Promise<RequestStats> {
const response = await fetchWithTrustedCertificatesAsync(this.url("/api/v1/request/count"), {
headers: {
@@ -339,11 +388,12 @@ const getRequestsSchema = z.object({
.array(
z.object({
id: z.number(),
status: z.nativeEnum(MediaRequestStatus),
status: z.enum(UpstreamMediaRequestStatus),
createdAt: z.string().transform((value) => new Date(value)),
media: z.object({
status: z.nativeEnum(MediaAvailability),
status: z.enum(UpstreamMediaAvailability),
tmdbId: z.number(),
downloadStatus: z.array(z.unknown()).optional(),
}),
type: z.enum(["movie", "tv"]),
requestedBy: z