mirror of
https://github.com/myronblair/kino-app
synced 2026-07-31 21:12:43 -05:00
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:
@@ -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"),
|
||||
|
||||
Reference in New Issue
Block a user