parlorsky's picture
ShotSplitter: add Download from URL button + yt-dlp route
a4f9f3b verified
Raw
History Blame Contribute Delete
5.08 kB
"""ComfyUI server hooks for Oz ShotSplitter.
Routes:
GET /oz_shotsplitter/active_clients — debug list of active websocket clients
POST /oz_shotsplitter/download_url — yt-dlp a public URL into ComfyUI input/
"""
from __future__ import annotations
import asyncio
import hashlib
import logging
import os
import re
logger = logging.getLogger(__name__)
try:
from server import PromptServer # type: ignore
except Exception as e: # pragma: no cover
PromptServer = None # type: ignore
logger.warning("Oz ShotSplitter: PromptServer unavailable (%s)", e)
DOWNLOAD_TIMEOUT_SEC = 600
DOWNLOAD_MAX_FILESIZE = "500M"
VIDEO_EXTS = ("mp4", "mkv", "webm", "mov", "m4v")
_INPUT_DIR_CACHE: str | None = None
def _resolve_input_dir() -> str:
global _INPUT_DIR_CACHE
if _INPUT_DIR_CACHE:
return _INPUT_DIR_CACHE
try:
import folder_paths # type: ignore
d = folder_paths.get_input_directory()
except Exception:
d = "/workspace/ComfyUI/input"
os.makedirs(d, exist_ok=True)
_INPUT_DIR_CACHE = d
return d
def _find_cached(input_dir: str, stem: str) -> str | None:
for ext in VIDEO_EXTS:
p = os.path.join(input_dir, f"{stem}.{ext}")
if os.path.exists(p) and os.path.getsize(p) > 0:
return p
return None
def register_routes() -> None:
if PromptServer is None:
return
try:
server = PromptServer.instance
except Exception as e:
logger.warning("Oz ShotSplitter: no PromptServer.instance (%s)", e)
return
from aiohttp import web
@server.routes.get("/oz_shotsplitter/active_clients")
async def _list_active_clients(_request):
sids = list(getattr(server, "sockets", {}).keys())
return web.json_response({"client_ids": sids, "count": len(sids)})
@server.routes.post("/oz_shotsplitter/download_url")
async def _download_url(request):
try:
data = await request.json()
except Exception:
return web.json_response({"ok": False, "error": "invalid JSON body"}, status=400)
url = (data.get("url") or "").strip()
if not url or not re.match(r"^https?://", url):
return web.json_response({"ok": False, "error": "invalid URL"}, status=400)
input_dir = _resolve_input_dir()
url_hash = hashlib.md5(url.encode("utf-8")).hexdigest()[:12]
stem = f"yt_{url_hash}"
cached = _find_cached(input_dir, stem)
if cached:
return web.json_response({
"ok": True,
"filename": os.path.basename(cached),
"cached": True,
})
out_template = os.path.join(input_dir, f"{stem}.%(ext)s")
cmd = [
"yt-dlp",
url,
"-o", out_template,
"--no-playlist",
"--max-filesize", DOWNLOAD_MAX_FILESIZE,
"-f", "mp4/best[ext=mp4]/bestvideo*+bestaudio/best",
"--merge-output-format", "mp4",
"--no-warnings",
"--no-progress",
"--quiet",
]
logger.info("Oz ShotSplitter: yt-dlp %s -> %s", url, stem)
try:
proc = await asyncio.create_subprocess_exec(
*cmd,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
)
except FileNotFoundError:
return web.json_response({
"ok": False,
"error": "yt-dlp not installed. Run: /venv/main/bin/pip install yt-dlp",
}, status=500)
except Exception as e:
return web.json_response({"ok": False, "error": f"spawn failed: {e}"}, status=500)
try:
stdout, stderr = await asyncio.wait_for(
proc.communicate(), timeout=DOWNLOAD_TIMEOUT_SEC
)
except asyncio.TimeoutError:
try:
proc.kill()
await proc.wait()
except Exception:
pass
return web.json_response({
"ok": False,
"error": f"yt-dlp timed out after {DOWNLOAD_TIMEOUT_SEC}s",
}, status=504)
if proc.returncode != 0:
err_tail = (stderr or b"").decode("utf-8", errors="replace").strip()
if len(err_tail) > 600:
err_tail = err_tail[-600:]
return web.json_response({
"ok": False,
"error": f"yt-dlp exit {proc.returncode}: {err_tail or 'no stderr'}",
}, status=500)
saved = _find_cached(input_dir, stem)
if not saved:
return web.json_response({
"ok": False,
"error": "yt-dlp succeeded but output file missing",
}, status=500)
return web.json_response({
"ok": True,
"filename": os.path.basename(saved),
"cached": False,
})
logger.info(
"Oz ShotSplitter: registered /oz_shotsplitter/{active_clients,download_url}"
)
register_routes()