archive-backend/utils/update_thumbs.py
2026-11-07 17:27:59 +00:00

80 lines
2.3 KiB
Python

import argparse
import asyncio
from pathlib import Path
from ..core.config import THUMBS_DIR, YT_DIR
async def run (cmd: list[str]) -> tuple[int, bytes]:
proc = await asyncio.create_subprocess_exec(
*cmd,
stdout=asyncio.subprocess.DEVNULL,
stderr=asyncio.subprocess.PIPE,
)
_, err = await proc.communicate()
return proc.returncode, err
async def extract_cover(video: Path, out: Path, sem: asyncio.Semaphore) -> bool:
if ".Trash" in str(video) or any(part.startswith('.') for part in video.parts):
return False
if ".temp." in video.name or ".f" in video.name and video.suffix == ".webm":
return False
async with sem:
out.parent.mkdir(parents=True, exist_ok=True)
if out.exists() and out.stat().st_size > 0:
return True
code, err = await run([
"ffmpeg", "-y", "-loglevel", "error",
"-i", str(video),
"-map", "0:v:1?",
"-c", "copy",
str(out),
])
if code != 0:
code, err = await run([
"ffmpeg", "-y", "-loglevel", "error",
"-i", str(video),
"-map", "0:v:0?",
"-c", "copy",
str(out),
])
if code != 0:
print(f"FAIL {video.name}: {err.decode()[:300]}")
return False
return True
async def main(workers: int):
SEM = asyncio.Semaphore(workers)
videos = [p for p in YT_DIR.rglob("*") if p.is_file() and p.suffix.lower() in {".mp4", ".mkv", ".webm"}]
print(f"videos: {len(videos)}")
tasks = []
for video in videos:
rel = video.relative_to(YT_DIR)
out = THUMBS_DIR / rel.with_suffix(".jpg")
tasks.append(extract_cover(video, out, SEM))
results = await asyncio.gather(*tasks)
ok = sum(1 for r in results if r)
print(f"done: {ok}/{len(results)}")
def parse_args():
parser = argparse.ArgumentParser(description="Extract video previews")
parser.add_argument(
"-w", "--workers",
type=int,
default=8,
help="number of parallel ffmpeg processes (default: 8)",
)
return parser.parse_args()
if __name__ == "__main__":
args = parse_args()
asyncio.run(main(args.workers))