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>
158 lines
5.5 KiB
TypeScript
158 lines
5.5 KiB
TypeScript
export const SCHEMA = `
|
|
PRAGMA journal_mode = WAL;
|
|
PRAGMA foreign_keys = ON;
|
|
|
|
CREATE TABLE IF NOT EXISTS config (
|
|
key TEXT PRIMARY KEY NOT NULL,
|
|
value TEXT
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS media_items (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
jellyfin_id TEXT NOT NULL UNIQUE,
|
|
type TEXT NOT NULL,
|
|
name TEXT NOT NULL,
|
|
original_title TEXT,
|
|
series_name TEXT,
|
|
series_jellyfin_id TEXT,
|
|
season_number INTEGER,
|
|
episode_number INTEGER,
|
|
year INTEGER,
|
|
file_path TEXT NOT NULL,
|
|
file_size INTEGER,
|
|
container TEXT,
|
|
runtime_ticks INTEGER,
|
|
date_last_refreshed TEXT,
|
|
original_language TEXT,
|
|
orig_lang_source TEXT,
|
|
needs_review INTEGER NOT NULL DEFAULT 1,
|
|
imdb_id TEXT,
|
|
tmdb_id TEXT,
|
|
tvdb_id TEXT,
|
|
jellyfin_raw TEXT,
|
|
external_raw TEXT,
|
|
scan_status TEXT NOT NULL DEFAULT 'pending',
|
|
scan_error TEXT,
|
|
last_scanned_at TEXT,
|
|
last_executed_at TEXT,
|
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS media_streams (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
item_id INTEGER NOT NULL REFERENCES media_items(id) ON DELETE CASCADE,
|
|
stream_index INTEGER NOT NULL,
|
|
type TEXT NOT NULL,
|
|
codec TEXT,
|
|
profile TEXT,
|
|
language TEXT,
|
|
language_display TEXT,
|
|
title TEXT,
|
|
is_default INTEGER NOT NULL DEFAULT 0,
|
|
is_forced INTEGER NOT NULL DEFAULT 0,
|
|
is_hearing_impaired INTEGER NOT NULL DEFAULT 0,
|
|
channels INTEGER,
|
|
channel_layout TEXT,
|
|
bit_rate INTEGER,
|
|
sample_rate INTEGER,
|
|
bit_depth INTEGER,
|
|
UNIQUE(item_id, stream_index)
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS review_plans (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
item_id INTEGER NOT NULL UNIQUE REFERENCES media_items(id) ON DELETE CASCADE,
|
|
status TEXT NOT NULL DEFAULT 'pending',
|
|
is_noop INTEGER NOT NULL DEFAULT 0,
|
|
confidence TEXT NOT NULL DEFAULT 'low',
|
|
apple_compat TEXT,
|
|
job_type TEXT NOT NULL DEFAULT 'copy',
|
|
subs_extracted INTEGER NOT NULL DEFAULT 0,
|
|
notes TEXT,
|
|
reviewed_at TEXT,
|
|
-- An independent post-hoc check has confirmed the on-disk file matches
|
|
-- the plan: either the analyzer saw is_noop=1 on first scan, or after
|
|
-- a job completed we ffprobed the output file and it agreed with the
|
|
-- kept/removed stream decisions. Surfaces as the ✓✓ in the Done column.
|
|
verified INTEGER NOT NULL DEFAULT 0,
|
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS stream_decisions (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
plan_id INTEGER NOT NULL REFERENCES review_plans(id) ON DELETE CASCADE,
|
|
stream_id INTEGER NOT NULL REFERENCES media_streams(id) ON DELETE CASCADE,
|
|
action TEXT NOT NULL,
|
|
target_index INTEGER,
|
|
custom_title TEXT,
|
|
transcode_codec TEXT,
|
|
UNIQUE(plan_id, stream_id)
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS subtitle_files (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
item_id INTEGER NOT NULL REFERENCES media_items(id) ON DELETE CASCADE,
|
|
file_path TEXT NOT NULL UNIQUE,
|
|
language TEXT,
|
|
codec TEXT,
|
|
is_forced INTEGER NOT NULL DEFAULT 0,
|
|
is_hearing_impaired INTEGER NOT NULL DEFAULT 0,
|
|
file_size INTEGER,
|
|
created_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS jobs (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
item_id INTEGER NOT NULL REFERENCES media_items(id) ON DELETE CASCADE,
|
|
command TEXT NOT NULL,
|
|
job_type TEXT NOT NULL DEFAULT 'copy',
|
|
status TEXT NOT NULL DEFAULT 'pending',
|
|
output TEXT,
|
|
exit_code INTEGER,
|
|
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
started_at TEXT,
|
|
completed_at TEXT
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_review_plans_status ON review_plans(status);
|
|
CREATE INDEX IF NOT EXISTS idx_review_plans_is_noop ON review_plans(is_noop);
|
|
CREATE INDEX IF NOT EXISTS idx_stream_decisions_plan_id ON stream_decisions(plan_id);
|
|
CREATE INDEX IF NOT EXISTS idx_media_items_series_jf ON media_items(series_jellyfin_id);
|
|
CREATE INDEX IF NOT EXISTS idx_media_items_series_name ON media_items(series_name);
|
|
CREATE INDEX IF NOT EXISTS idx_media_items_type ON media_items(type);
|
|
CREATE INDEX IF NOT EXISTS idx_media_streams_item_id ON media_streams(item_id);
|
|
CREATE INDEX IF NOT EXISTS idx_media_streams_type ON media_streams(type);
|
|
CREATE INDEX IF NOT EXISTS idx_subtitle_files_item_id ON subtitle_files(item_id);
|
|
CREATE INDEX IF NOT EXISTS idx_jobs_status ON jobs(status);
|
|
CREATE INDEX IF NOT EXISTS idx_jobs_item_id ON jobs(item_id);
|
|
`;
|
|
|
|
export const DEFAULT_CONFIG: Record<string, string> = {
|
|
setup_complete: "0",
|
|
jellyfin_url: "",
|
|
jellyfin_api_key: "",
|
|
jellyfin_user_id: "",
|
|
radarr_url: "",
|
|
radarr_api_key: "",
|
|
radarr_enabled: "0",
|
|
sonarr_url: "",
|
|
sonarr_api_key: "",
|
|
sonarr_enabled: "0",
|
|
audio_languages: "[]",
|
|
|
|
scan_running: "0",
|
|
job_sleep_seconds: "0",
|
|
scan_schedule_enabled: "0",
|
|
scan_schedule_start: "01:00",
|
|
scan_schedule_end: "07:00",
|
|
process_schedule_enabled: "0",
|
|
process_schedule_start: "01:00",
|
|
process_schedule_end: "07:00",
|
|
|
|
mqtt_enabled: "0",
|
|
mqtt_url: "",
|
|
mqtt_topic: "jellyfin/events",
|
|
mqtt_username: "",
|
|
mqtt_password: "",
|
|
};
|