mirror of
https://github.com/myronblair/kino-app
synced 2026-07-29 14:02:50 -05:00
Speed up transcoding without overloading the VM, add bulk retry
- nice level 19 -> 10 (was the absolute lowest CPU priority) and cap ffmpeg to 6 threads on this 8-core host, leaving headroom for Mongo, the *arr stack, and the API itself instead of an unbounded encode. - Add POST /transcode/queue/retry-failed to re-queue every failed/ cancelled job at once; label the per-job Retry button with text (was icon-only) and clarify in the page copy that failed/cancelled jobs never delete the movie — retry just re-queues the same file.
This commit is contained in:
@@ -707,6 +707,16 @@ async def retry_job(job_id: str, user: dict = Depends(require_admin)):
|
|||||||
return {"ok": True, "job_id": new_job["id"]}
|
return {"ok": True, "job_id": new_job["id"]}
|
||||||
|
|
||||||
|
|
||||||
|
@api.post("/transcode/queue/retry-failed")
|
||||||
|
async def retry_all_failed(user: dict = Depends(require_admin)):
|
||||||
|
jobs = await db.transcode_queue.find({"status": {"$in": ["failed", "cancelled"]}}, {"_id": 0}).to_list(500)
|
||||||
|
retried = 0
|
||||||
|
for job in jobs:
|
||||||
|
await _enqueue_transcode(job["movie_id"], job["quality"], triggered_by=job.get("triggered_by", "manual"), content_type=job.get("content_type", "movie"))
|
||||||
|
retried += 1
|
||||||
|
return {"ok": True, "retried": retried}
|
||||||
|
|
||||||
|
|
||||||
@api.post("/transcode/queue/clear")
|
@api.post("/transcode/queue/clear")
|
||||||
async def clear_finished(user: dict = Depends(require_admin)):
|
async def clear_finished(user: dict = Depends(require_admin)):
|
||||||
res = await db.transcode_queue.delete_many({"status": {"$in": ["done", "failed", "cancelled"]}})
|
res = await db.transcode_queue.delete_many({"status": {"$in": ["done", "failed", "cancelled"]}})
|
||||||
|
|||||||
@@ -13,6 +13,11 @@ from typing import Optional, List, Tuple, Callable, Awaitable
|
|||||||
|
|
||||||
logger = logging.getLogger("kino.transcode")
|
logger = logging.getLogger("kino.transcode")
|
||||||
|
|
||||||
|
# Moderate priority (was 19, the absolute lowest) and a thread cap so an ABR encode can't
|
||||||
|
# starve the rest of the stack (Mongo, *arr apps, the API itself) on this 8-core host.
|
||||||
|
TRANSCODE_NICE = 10
|
||||||
|
TRANSCODE_THREADS = 6
|
||||||
|
|
||||||
|
|
||||||
async def probe_video(source: Path) -> Tuple[int, int]:
|
async def probe_video(source: Path) -> Tuple[int, int]:
|
||||||
"""Return (width, height) using ffprobe; (0, 0) on failure."""
|
"""Return (width, height) using ffprobe; (0, 0) on failure."""
|
||||||
@@ -58,7 +63,7 @@ async def transcode_quick(source: Path, out_dir: Path, on_status: Callable[...,
|
|||||||
return
|
return
|
||||||
out_dir.mkdir(parents=True, exist_ok=True)
|
out_dir.mkdir(parents=True, exist_ok=True)
|
||||||
cmd = [
|
cmd = [
|
||||||
"nice", "-n", "19",
|
"nice", "-n", str(TRANSCODE_NICE),
|
||||||
"ffmpeg", "-y", "-i", str(source),
|
"ffmpeg", "-y", "-i", str(source),
|
||||||
"-c:v", "copy", "-c:a", "copy",
|
"-c:v", "copy", "-c:a", "copy",
|
||||||
"-bsf:v", "h264_mp4toannexb",
|
"-bsf:v", "h264_mp4toannexb",
|
||||||
@@ -88,13 +93,14 @@ async def transcode_abr(source: Path, out_dir: Path, on_status: Callable[..., Aw
|
|||||||
fc_parts.append(f"[v{i}]scale=w=-2:h={h}[v{i}out]")
|
fc_parts.append(f"[v{i}]scale=w=-2:h={h}[v{i}out]")
|
||||||
filter_complex = ";".join(fc_parts)
|
filter_complex = ";".join(fc_parts)
|
||||||
|
|
||||||
cmd: List[str] = ["nice", "-n", "19", "ffmpeg", "-y", "-i", str(source), "-filter_complex", filter_complex]
|
cmd: List[str] = ["nice", "-n", str(TRANSCODE_NICE), "ffmpeg", "-y", "-threads", str(TRANSCODE_THREADS), "-i", str(source), "-filter_complex", filter_complex]
|
||||||
|
|
||||||
for i, (_h, vb, maxr, buf, _ab) in enumerate(variants):
|
for i, (_h, vb, maxr, buf, _ab) in enumerate(variants):
|
||||||
cmd += [
|
cmd += [
|
||||||
"-map", f"[v{i}out]",
|
"-map", f"[v{i}out]",
|
||||||
f"-c:v:{i}", "libx264",
|
f"-c:v:{i}", "libx264",
|
||||||
f"-preset:v:{i}", "veryfast",
|
f"-preset:v:{i}", "veryfast",
|
||||||
|
f"-threads:v:{i}", str(TRANSCODE_THREADS),
|
||||||
f"-profile:v:{i}", "main",
|
f"-profile:v:{i}", "main",
|
||||||
f"-pix_fmt:v:{i}", "yuv420p",
|
f"-pix_fmt:v:{i}", "yuv420p",
|
||||||
f"-b:v:{i}", vb,
|
f"-b:v:{i}", vb,
|
||||||
|
|||||||
@@ -44,6 +44,14 @@ export default function TranscodeQueue() {
|
|||||||
catch (err) { toast.error(err.response?.data?.detail || "Could not retry"); }
|
catch (err) { toast.error(err.response?.data?.detail || "Could not retry"); }
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const retryAllFailed = async () => {
|
||||||
|
try {
|
||||||
|
const { data } = await api.post("/transcode/queue/retry-failed");
|
||||||
|
toast.success(`Re-queued ${data.retried} job(s)`);
|
||||||
|
load();
|
||||||
|
} catch (err) { toast.error(err.response?.data?.detail || "Could not retry"); }
|
||||||
|
};
|
||||||
|
|
||||||
const togglePause = async () => {
|
const togglePause = async () => {
|
||||||
try {
|
try {
|
||||||
const { data } = await api.post("/transcode/queue/pause", { paused: !stats.paused });
|
const { data } = await api.post("/transcode/queue/pause", { paused: !stats.paused });
|
||||||
@@ -64,9 +72,9 @@ export default function TranscodeQueue() {
|
|||||||
<span className="text-xs uppercase tracking-[0.3em] text-[#D9381E]">Background</span>
|
<span className="text-xs uppercase tracking-[0.3em] text-[#D9381E]">Background</span>
|
||||||
<h1 className="font-display text-5xl md:text-6xl font-black tracking-tighter text-white mt-3">Transcode Queue</h1>
|
<h1 className="font-display text-5xl md:text-6xl font-black tracking-tighter text-white mt-3">Transcode Queue</h1>
|
||||||
<p className="text-[#8A8A8A] mt-4 max-w-2xl">
|
<p className="text-[#8A8A8A] mt-4 max-w-2xl">
|
||||||
One job runs at a time at low CPU priority. Auto-transcode is currently:{" "}
|
One job runs at a time, moderate CPU priority, capped threads so it can't starve the rest of the stack. Auto-transcode is currently:{" "}
|
||||||
<span className="text-white">{stats.auto_transcode === "off" ? "off" : `auto · ${stats.auto_transcode}`}</span>
|
<span className="text-white">{stats.auto_transcode === "off" ? "off" : `auto · ${stats.auto_transcode}`}</span>
|
||||||
{" — "}<a href="/admin/settings" className="text-[#D9381E] hover:text-[#ED4B32]">change in Settings</a>
|
{" — "}<a href="/admin/settings" className="text-[#D9381E] hover:text-[#ED4B32]">change in Settings</a>. Failed and cancelled jobs never delete the movie — retry re-queues the same file.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="mt-8 grid grid-cols-2 sm:grid-cols-5 gap-3">
|
<div className="mt-8 grid grid-cols-2 sm:grid-cols-5 gap-3">
|
||||||
@@ -86,6 +94,9 @@ export default function TranscodeQueue() {
|
|||||||
{stats.paused ? <PlayIcon size={14} strokeWidth={1.5} /> : <Pause size={14} strokeWidth={1.5} />}
|
{stats.paused ? <PlayIcon size={14} strokeWidth={1.5} /> : <Pause size={14} strokeWidth={1.5} />}
|
||||||
{stats.paused ? "Resume" : "Pause"}
|
{stats.paused ? "Resume" : "Pause"}
|
||||||
</button>
|
</button>
|
||||||
|
<button onClick={retryAllFailed} disabled={!stats.failed && !stats.cancelled} className="flex items-center gap-2 border border-[#222] hover:border-[#D9381E] hover:text-[#D9381E] text-[#8A8A8A] px-4 py-2 text-xs uppercase tracking-[0.2em] transition-colors disabled:opacity-50" data-testid="queue-retry-all">
|
||||||
|
<RotateCcw size={14} strokeWidth={1.5} /> Retry All Failed
|
||||||
|
</button>
|
||||||
<button onClick={clearFinished} className="flex items-center gap-2 border border-[#222] hover:border-[#fca5a5] hover:text-[#fca5a5] text-[#8A8A8A] px-4 py-2 text-xs uppercase tracking-[0.2em] transition-colors" data-testid="queue-clear">
|
<button onClick={clearFinished} className="flex items-center gap-2 border border-[#222] hover:border-[#fca5a5] hover:text-[#fca5a5] text-[#8A8A8A] px-4 py-2 text-xs uppercase tracking-[0.2em] transition-colors" data-testid="queue-clear">
|
||||||
<Trash2 size={14} strokeWidth={1.5} /> Clear Finished
|
<Trash2 size={14} strokeWidth={1.5} /> Clear Finished
|
||||||
</button>
|
</button>
|
||||||
@@ -124,8 +135,8 @@ export default function TranscodeQueue() {
|
|||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
{(j.status === "failed" || j.status === "cancelled") && (
|
{(j.status === "failed" || j.status === "cancelled") && (
|
||||||
<button onClick={() => retry(j.id)} className="text-[#8A8A8A] hover:text-[#D9381E]" data-testid={`retry-${j.id}`} aria-label="Retry">
|
<button onClick={() => retry(j.id)} className="flex items-center gap-1 text-[10px] uppercase tracking-[0.2em] text-[#8A8A8A] hover:text-[#D9381E]" data-testid={`retry-${j.id}`}>
|
||||||
<RotateCcw size={14} strokeWidth={1.5} />
|
<RotateCcw size={13} strokeWidth={1.5} /> Retry
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user