|
|
|
|
import os, hashlib, subprocess
|
|
|
|
|
from concurrent.futures import ThreadPoolExecutor
|
|
|
|
|
|
|
|
|
|
# pull constants from app.py
|
|
|
|
|
from app import THUMB_DIR, THUMB_WIDTH, FF_QUALITY
|
|
|
|
|
|
|
|
|
|
def _hashed_thumb_path(video_id: str):
|
|
|
|
|
h = hashlib.md5(video_id.encode()).hexdigest()
|
|
|
|
|
sub1, sub2 = h[:2], h[2:4]
|
|
|
|
|
path = os.path.join(THUMB_DIR, sub1, sub2)
|
|
|
|
|
os.makedirs(path, exist_ok=True)
|
|
|
|
|
return os.path.join(path, f"{video_id}.webp")
|
|
|
|
|
|
|
|
|
|
def _gen_thumb_cmd(src: str, dest: str):
|
|
|
|
|
return [
|
|
|
|
|
"ffmpeg", "-y", "-loglevel", "error",
|
|
|
|
|
"-ss", "0", "-i", src,
|
|
|
|
|
"-vframes", "1",
|
|
|
|
|
"-vf", f"thumbnail,scale={THUMB_WIDTH}:-1",
|
|
|
|
|
"-q:v", FF_QUALITY,
|
|
|
|
|
dest
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
def generate_thumbnails_for_videos(videos):
|
|
|
|
|
tasks = []
|
|
|
|
|
|
|
|
|
|
for v in videos:
|
|
|
|
|
video_id = v.get("video_id")
|
|
|
|
|
filepath = v.get("filepath")
|
|
|
|
|
thumb_path = _hashed_thumb_path(video_id)
|
|
|
|
|
|
|
|
|
|
if not filepath:
|
|
|
|
|
print(f"⚠️ Skipping {video_id}: missing filepath")
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
if not os.path.exists(filepath):
|
|
|
|
|
print(f"⚠️ Skipping {video_id}: file not found → {filepath}")
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
|
|
if not os.path.exists(thumb_path):
|
|
|
|
|
tasks.append((filepath, thumb_path))
|
|
|
|
|
|
|
|
|
|
v["thumbnail"] = thumb_path
|
|
|
|
|
|
|
|
|
|
if tasks:
|
|
|
|
|
with ThreadPoolExecutor(max_workers=os.cpu_count() * 2) as exe:
|
|
|
|
|
list(exe.map(
|
|
|
|
|
lambda t: subprocess.run(
|
|
|
|
|
_gen_thumb_cmd(*t),
|
|
|
|
|
stdout=subprocess.DEVNULL,
|
|
|
|
|
stderr=subprocess.DEVNULL
|
|
|
|
|
),
|
|
|
|
|
tasks
|
|
|
|
|
))
|