Codec-aware transcode routing, two-stage queue, request candidate picker, admin reorg

- Requests approval now shows a Radarr/Sonarr candidate picker instead of grabbing the first search result (fixes wrong-title/wrong-year matches)
- approve_request reuses an existing Radarr/Sonarr entry instead of erroring when content is already in the library
- New codec-check queue (step 1) probes source codecs and skips transcoding entirely for already browser-native files; only non-native content reaches the transcode queue (step 2)
- Fixes ffmpeg hard-failing on non-H.264 sources via the h264_mp4toannexb bitstream filter (root cause of the fix-audio-all failures)
- Added Codec Check Worker / Transcode Worker to the services health panel with restart
- Reorganized the admin page into Pipeline/Utilities dropdowns; added the missing Sonarr Import link
- Bumped VERSION 1.0.0 -> 1.1.0
This commit is contained in:
root
2026-07-30 20:41:11 -05:00
parent 671074764e
commit b28a49e8fa
13 changed files with 784 additions and 50 deletions
+47
View File
@@ -54,6 +54,53 @@ async def probe_duration(source: Path) -> float:
return 0.0
async def probe_codecs(source: Path) -> Tuple[str, str]:
"""Return (video_codec, audio_codec), lowercased, via ffprobe; ('', '') on failure."""
try:
proc = await asyncio.create_subprocess_exec(
"ffprobe", "-v", "error", "-show_entries", "stream=codec_type,codec_name",
"-of", "json", str(source),
stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE,
)
stdout, _ = await proc.communicate()
if proc.returncode != 0:
return ("", "")
data = json.loads(stdout.decode("utf-8", errors="ignore") or "{}")
video_codec = audio_codec = ""
for s in data.get("streams") or []:
if s.get("codec_type") == "video" and not video_codec:
video_codec = (s.get("codec_name") or "").lower()
elif s.get("codec_type") == "audio" and not audio_codec:
audio_codec = (s.get("codec_name") or "").lower()
return (video_codec, audio_codec)
except Exception:
return ("", "")
BROWSER_NATIVE_AUDIO = {"aac", "mp3"}
async def classify_source(source: Path) -> str:
"""Decide the cheapest correct transcode path for a source file:
- 'native': H.264 video + AAC/MP3 audio — browsers already play this file as-is (the
raw-file stream endpoint serves it directly), so no transcode is needed at all.
- 'audio': H.264 video but non-browser audio (AC3/DTS/EAC3/etc) — cheap audio-only
re-encode, video stream-copied untouched.
- 'abr': anything else (HEVC/VP9/etc, or codec probing failed) — needs a real video
re-encode. Also the safe fallback on probe failure: it's the only mode that doesn't
assume the source is already H.264 — 'audio'/'quick' apply the h264_mp4toannexb
bitstream filter unconditionally, which makes ffmpeg hard-fail on non-H.264 input.
"""
if not source.is_file():
return "abr"
video_codec, audio_codec = await probe_codecs(source)
if not video_codec or video_codec != "h264":
return "abr"
if audio_codec in BROWSER_NATIVE_AUDIO:
return "native"
return "audio"
# (height, video_bitrate, max_bitrate, buffer, audio_bitrate)
ALL_VARIANTS = [
(1080, "5000k", "5500k", "7500k", "192k"),