Add Radarr/Sonarr links and richer metadata to alert detail
- Thread titleSlug through RadarrMovie/SonarrSeries → MediaEntry → AlertCandidate, building a direct mediaUrl server-side at alert generation time (RADARR_URL/movie/slug, SONARR_URL/series/slug) - Alert detail page: single-column full-width layout, metadata chips row showing opened/closed dates, media type, and a "View in Radarr/ Sonarr" external link button when available - Comments section sits below the full-width overview card Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -128,6 +128,9 @@ export function generateAlertCandidates(
|
||||
if (flaggedMovies.has(tmdbId)) continue;
|
||||
flaggedMovies.add(tmdbId);
|
||||
const byStr = requestedBy.slice(0, 3).join(", ") + (requestedBy.length > 3 ? ` +${requestedBy.length - 3}` : "");
|
||||
const mediaUrl = entry.titleSlug && process.env.RADARR_URL
|
||||
? `${process.env.RADARR_URL}/movie/${entry.titleSlug}`
|
||||
: undefined;
|
||||
candidates.push({
|
||||
key: `unfulfilled:movie:${tmdbId}`,
|
||||
category: "unfulfilled",
|
||||
@@ -137,6 +140,7 @@ export function generateAlertCandidates(
|
||||
mediaId: tmdbId,
|
||||
mediaType: "movie",
|
||||
mediaTitle: entry.title,
|
||||
mediaUrl,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -151,6 +155,9 @@ export function generateAlertCandidates(
|
||||
const description = pct !== null
|
||||
? `Only ${pct}% of episodes downloaded (${sonarrEntry!.episodeFileCount}/${sonarrEntry!.totalEpisodeCount}). Approved ${formatAge(oldestAgeHours)} ago. Requested by ${byStr}.`
|
||||
: `Approved ${formatAge(oldestAgeHours)} ago but no files found in Sonarr. Requested by ${byStr}.`;
|
||||
const mediaUrl = sonarrEntry?.titleSlug && process.env.SONARR_URL
|
||||
? `${process.env.SONARR_URL}/series/${sonarrEntry.titleSlug}`
|
||||
: undefined;
|
||||
candidates.push({
|
||||
key: `unfulfilled:tv:${tvdbId}`,
|
||||
category: "unfulfilled",
|
||||
@@ -160,6 +167,7 @@ export function generateAlertCandidates(
|
||||
mediaId: tvdbId,
|
||||
mediaType: "tv",
|
||||
mediaTitle: entry.title,
|
||||
mediaUrl,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -48,6 +48,7 @@ interface StoredAlert {
|
||||
mediaId?: number;
|
||||
mediaType?: string;
|
||||
mediaTitle?: string;
|
||||
mediaUrl?: string;
|
||||
status: AlertStatus;
|
||||
closeReason: AlertCloseReason | null;
|
||||
suppressedUntil: string | null;
|
||||
@@ -84,6 +85,7 @@ function toAlert(s: StoredAlert): Alert {
|
||||
mediaId: s.mediaId,
|
||||
mediaType: s.mediaType as Alert["mediaType"],
|
||||
mediaTitle: s.mediaTitle,
|
||||
mediaUrl: s.mediaUrl,
|
||||
status: s.status,
|
||||
closeReason: s.closeReason ?? null,
|
||||
suppressedUntil: s.suppressedUntil,
|
||||
@@ -147,6 +149,7 @@ export function upsertAlerts(candidates: AlertCandidate[]): number {
|
||||
existing.description = c.description;
|
||||
if (c.userName) existing.userName = c.userName;
|
||||
if (c.mediaTitle) existing.mediaTitle = c.mediaTitle;
|
||||
if (c.mediaUrl) existing.mediaUrl = c.mediaUrl;
|
||||
} else {
|
||||
store.alerts[c.key] = {
|
||||
id: store.nextId++,
|
||||
@@ -160,6 +163,7 @@ export function upsertAlerts(candidates: AlertCandidate[]): number {
|
||||
mediaId: c.mediaId,
|
||||
mediaType: c.mediaType,
|
||||
mediaTitle: c.mediaTitle,
|
||||
mediaUrl: c.mediaUrl,
|
||||
status: "open",
|
||||
closeReason: null,
|
||||
suppressedUntil: null,
|
||||
|
||||
+1
-1
@@ -13,7 +13,7 @@ export async function buildRadarrMap(): Promise<Map<number, MediaEntry>> {
|
||||
return new Map(
|
||||
movies.map((m) => [
|
||||
m.tmdbId,
|
||||
{ title: m.title, sizeOnDisk: m.sizeOnDisk, available: m.isAvailable },
|
||||
{ title: m.title, titleSlug: m.titleSlug, sizeOnDisk: m.sizeOnDisk, available: m.isAvailable },
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ export async function buildSonarrMap(): Promise<Map<number, MediaEntry>> {
|
||||
s.tvdbId,
|
||||
{
|
||||
title: s.title,
|
||||
titleSlug: s.titleSlug,
|
||||
sizeOnDisk: s.statistics.sizeOnDisk,
|
||||
// "upcoming" = series hasn't started airing yet
|
||||
available: s.status !== "upcoming",
|
||||
|
||||
@@ -23,6 +23,7 @@ export interface OverseerrRequest {
|
||||
export interface RadarrMovie {
|
||||
tmdbId: number;
|
||||
title: string;
|
||||
titleSlug: string;
|
||||
sizeOnDisk: number; // bytes
|
||||
isAvailable: boolean; // false = unreleased / below minimum availability
|
||||
}
|
||||
@@ -30,6 +31,7 @@ export interface RadarrMovie {
|
||||
export interface SonarrSeries {
|
||||
tvdbId: number;
|
||||
title: string;
|
||||
titleSlug: string;
|
||||
status: string; // "continuing" | "ended" | "upcoming" | "deleted"
|
||||
statistics: {
|
||||
sizeOnDisk: number; // bytes
|
||||
@@ -51,6 +53,7 @@ export interface TautulliUser {
|
||||
|
||||
export interface MediaEntry {
|
||||
title: string;
|
||||
titleSlug?: string;
|
||||
sizeOnDisk: number; // bytes
|
||||
available: boolean; // false = unreleased, skip unfulfilled alerts
|
||||
// TV-specific (undefined for movies)
|
||||
@@ -112,6 +115,7 @@ export interface AlertCandidate {
|
||||
mediaId?: number;
|
||||
mediaType?: "movie" | "tv";
|
||||
mediaTitle?: string;
|
||||
mediaUrl?: string; // direct link to the item in Radarr/Sonarr
|
||||
}
|
||||
|
||||
export interface AlertComment {
|
||||
|
||||
Reference in New Issue
Block a user