All checks were successful
Build and Push Docker Image / build (push) Successful in 1m56s
monitoring the mqtt broker revealed two bugs and one design dead-end: 1. the jellyfin-plugin-webhook publishes pascalcase fields (NotificationType, ItemId, ItemType) and we were reading camelcase (event, itemId, itemType). every real payload was rejected by the first guard — the mqtt path never ingested anything. 2. the plugin has no ItemUpdated / Library.* notifications. file rewrites on existing items produce zero broker traffic (observed: transcode + manual refresh metadata + 'recently added' appearance → no mqtt messages). ✓✓ via webhook is structurally impossible. fix the webhook path so brand-new library items actually get ingested, and narrow ACCEPTED_EVENTS to just 'ItemAdded' (the only library-side event the plugin emits). move the ✓✓ signal from webhook-corroboration to post-execute ffprobe via the existing verifyDesiredState helper: after ffmpeg returns 0 we probe the output file ourselves and flip verified=1 on match. the preflight-skipped path sets verified=1 too. renamed the db column webhook_verified → verified (via idempotent RENAME COLUMN migration) since the signal is no longer webhook-sourced, and updated the Done column tooltip to reflect that ffprobe is doing the verification. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
192 lines
5.2 KiB
TypeScript
192 lines
5.2 KiB
TypeScript
// ─── Database row types ───────────────────────────────────────────────────────
|
|
|
|
export interface MediaItem {
|
|
id: number;
|
|
jellyfin_id: string;
|
|
type: "Movie" | "Episode";
|
|
name: string;
|
|
original_title: string | null;
|
|
series_name: string | null;
|
|
series_jellyfin_id: string | null;
|
|
season_number: number | null;
|
|
episode_number: number | null;
|
|
year: number | null;
|
|
file_path: string;
|
|
file_size: number | null;
|
|
container: string | null;
|
|
runtime_ticks: number | null;
|
|
date_last_refreshed: string | null;
|
|
original_language: string | null;
|
|
orig_lang_source: "jellyfin" | "radarr" | "sonarr" | "manual" | null;
|
|
needs_review: number;
|
|
imdb_id: string | null;
|
|
tmdb_id: string | null;
|
|
tvdb_id: string | null;
|
|
jellyfin_raw: string | null;
|
|
external_raw: string | null;
|
|
scan_status: "pending" | "scanned" | "error";
|
|
scan_error: string | null;
|
|
last_scanned_at: string | null;
|
|
last_executed_at: string | null;
|
|
created_at: string;
|
|
}
|
|
|
|
export interface MediaStream {
|
|
id: number;
|
|
item_id: number;
|
|
stream_index: number;
|
|
type: "Video" | "Audio" | "Subtitle" | "Data" | "EmbeddedImage";
|
|
codec: string | null;
|
|
profile: string | null;
|
|
/** Raw language tag as reported by Jellyfin (e.g. "en", "eng", "ger", null).
|
|
* Not normalized on ingest — callers use normalizeLanguage() for comparison
|
|
* so we can detect non-canonical tags that the pipeline should rewrite. */
|
|
language: string | null;
|
|
language_display: string | null;
|
|
title: string | null;
|
|
is_default: number;
|
|
is_forced: number;
|
|
is_hearing_impaired: number;
|
|
channels: number | null;
|
|
channel_layout: string | null;
|
|
bit_rate: number | null;
|
|
sample_rate: number | null;
|
|
bit_depth: number | null;
|
|
}
|
|
|
|
export interface ReviewPlan {
|
|
id: number;
|
|
item_id: number;
|
|
status: "pending" | "approved" | "skipped" | "done" | "error";
|
|
is_noop: number;
|
|
confidence: "high" | "low";
|
|
apple_compat: "direct_play" | "remux" | "audio_transcode" | null;
|
|
job_type: "copy" | "transcode";
|
|
subs_extracted: number;
|
|
notes: string | null;
|
|
reviewed_at: string | null;
|
|
verified: number;
|
|
created_at: string;
|
|
}
|
|
|
|
export interface SubtitleFile {
|
|
id: number;
|
|
item_id: number;
|
|
file_path: string;
|
|
language: string | null;
|
|
codec: string | null;
|
|
is_forced: number;
|
|
is_hearing_impaired: number;
|
|
file_size: number | null;
|
|
created_at: string;
|
|
}
|
|
|
|
export interface StreamDecision {
|
|
id: number;
|
|
plan_id: number;
|
|
stream_id: number;
|
|
action: "keep" | "remove";
|
|
target_index: number | null;
|
|
custom_title: string | null;
|
|
transcode_codec: string | null;
|
|
}
|
|
|
|
export interface Job {
|
|
id: number;
|
|
item_id: number;
|
|
command: string;
|
|
job_type: "copy" | "transcode";
|
|
status: "pending" | "running" | "done" | "error";
|
|
output: string | null;
|
|
exit_code: number | null;
|
|
created_at: string;
|
|
started_at: string | null;
|
|
completed_at: string | null;
|
|
}
|
|
|
|
// ─── Analyzer types ───────────────────────────────────────────────────────────
|
|
|
|
export interface StreamWithDecision extends MediaStream {
|
|
action: "keep" | "remove";
|
|
target_index: number | null;
|
|
}
|
|
|
|
export interface PlanResult {
|
|
is_noop: boolean;
|
|
has_subs: boolean;
|
|
confidence: "high" | "low";
|
|
apple_compat: "direct_play" | "remux" | "audio_transcode" | null;
|
|
job_type: "copy" | "transcode";
|
|
decisions: Array<{
|
|
stream_id: number;
|
|
action: "keep" | "remove";
|
|
target_index: number | null;
|
|
transcode_codec: string | null;
|
|
}>;
|
|
notes: string[];
|
|
}
|
|
|
|
// ─── Jellyfin API types ───────────────────────────────────────────────────────
|
|
|
|
export interface JellyfinMediaStream {
|
|
Type: string;
|
|
Index: number;
|
|
Codec?: string;
|
|
Profile?: string;
|
|
Language?: string;
|
|
DisplayLanguage?: string;
|
|
Title?: string;
|
|
IsDefault?: boolean;
|
|
IsForced?: boolean;
|
|
IsHearingImpaired?: boolean;
|
|
IsExternal?: boolean;
|
|
Channels?: number;
|
|
ChannelLayout?: string;
|
|
BitRate?: number;
|
|
SampleRate?: number;
|
|
BitDepth?: number;
|
|
}
|
|
|
|
export interface JellyfinItem {
|
|
Id: string;
|
|
Type: string;
|
|
Name: string;
|
|
OriginalTitle?: string;
|
|
SeriesName?: string;
|
|
SeriesId?: string;
|
|
ParentIndexNumber?: number;
|
|
IndexNumber?: number;
|
|
ProductionYear?: number;
|
|
Path?: string;
|
|
Size?: number;
|
|
Container?: string;
|
|
RunTimeTicks?: number;
|
|
DateLastRefreshed?: string;
|
|
MediaStreams?: JellyfinMediaStream[];
|
|
ProviderIds?: Record<string, string>;
|
|
}
|
|
|
|
export interface JellyfinUser {
|
|
Id: string;
|
|
Name: string;
|
|
}
|
|
|
|
// ─── Scan state ───────────────────────────────────────────────────────────────
|
|
|
|
export interface ScanProgress {
|
|
total: number;
|
|
scanned: number;
|
|
current_item: string;
|
|
errors: number;
|
|
running: boolean;
|
|
}
|
|
|
|
// ─── SSE event helpers ────────────────────────────────────────────────────────
|
|
|
|
export type SseEventType = "progress" | "log" | "complete" | "error";
|
|
|
|
export interface SseEvent {
|
|
type: SseEventType;
|
|
data: unknown;
|
|
}
|