Files
netfelix-audio-fix/server/types.ts
Felix Förtsch d05e037bbc
All checks were successful
Build and Push Docker Image / build (push) Successful in 1m56s
webhook: PascalCase payload + ItemAdded only, switch ✓✓ signal to ffprobe
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>
2026-04-14 17:27:22 +02:00

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;
}