feat: yt-dlp support added and playback issues fixed

This commit is contained in:
YusufB5 2026-06-21 10:09:57 +03:00
parent 6047f8ed22
commit 0df68b97fe
3 changed files with 28 additions and 9 deletions

3
app.js
View file

@ -206,6 +206,7 @@ function connectWebSocket() {
if (seekPlayed) seekPlayed.style.transform = 'scaleX(0)';
audioOffset = 0;
scrubMeta = null; // reset so new video gets fresh thumbnails
// Lazy-load hover thumbnails: only fetch on first hover
const qIdx = currentQueueIdx;
if (seekWrap && !scrubMeta) {
@ -558,7 +559,7 @@ function skip(delta) {
function setupScrub(v) {
scrubMeta = null;
if (seekPreviewImg) seekPreviewImg.style.backgroundImage = '';
fetch('/scrub?v=' + (v || 0)).then(r => r.json()).then(m => {
fetch('/scrub?v=' + (v || 0) + '&t=' + Date.now()).then(r => r.json()).then(m => {
if (!m || !m.available || !seekPreviewImg) return;
scrubMeta = m;
seekPreviewImg.style.width = m.cellW + 'px';

View file

@ -42,17 +42,29 @@ def get_video_dimensions(path: str) -> tuple[int, int]:
return w, h
def calc_auto_rows(cols: int, vid_w: int, vid_h: int, pixel_mode: bool) -> int:
def calc_auto_dimensions(cols: int, vid_w: int, vid_h: int, pixel_mode: bool) -> tuple[int, int]:
"""
Calculate rows from video aspect ratio.
Calculate (cols, rows) from video aspect ratio.
ASCII mode: characters are ~2x taller than wide, so divide by 2.
Pixel mode: cells are square (CSS stretches), no correction needed.
"""
# Pixel mode uses GPU-accelerated fillRect → generous cap
# ASCII mode uses CPU fillText per cell → tight cap to prevent stutter on vertical videos
MAX_ROWS = 350 if pixel_mode else 100
ratio = vid_w / max(vid_h, 1)
if pixel_mode:
return max(1, round(cols / ratio))
rows = max(1, round(cols / ratio))
else:
return max(1, round(cols / ratio / 2))
rows = max(1, round(cols / ratio / 2))
if rows > MAX_ROWS:
# Scale down BOTH cols and rows to preserve aspect ratio
scale = MAX_ROWS / rows
rows = MAX_ROWS
cols = max(1, round(cols * scale))
return cols, rows
# Serve only whitelisted static files (security: prevents directory traversal)
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
@ -350,7 +362,8 @@ async def scrub_meta(v: int | None = None):
if not built:
return Response(content='{"available": false}', media_type="application/json")
meta = dict(built["meta"])
meta["sprite"] = f"/scrub_sprite?v={v if v is not None else 0}"
vid_id = os.path.basename(video_path)
meta["sprite"] = f"/scrub_sprite?v={v if v is not None else 0}&id={vid_id}"
return Response(content=_json.dumps(meta), media_type="application/json")
@ -461,7 +474,7 @@ async def websocket_endpoint(websocket: WebSocket):
continue
if rows_cfg == 0:
rows = calc_auto_rows(cols, vid_w, vid_h, pixel_mode)
cols, rows = calc_auto_dimensions(cols, vid_w, vid_h, pixel_mode)
print(f"[AUTO] {vid_w}x{vid_h} → grid {cols}x{rows}")
else:
rows = rows_cfg
@ -860,6 +873,8 @@ if __name__ == "__main__":
# ── High FPS Warning ──
high_fps_videos = []
for entry in queue:
if ytdl.is_url(entry['video']):
continue # skip remote URLs; yt-dlp normalizes to 30 FPS
cap = cv2.VideoCapture(entry['video'])
if cap.isOpened():
fps = cap.get(cv2.CAP_PROP_FPS)
@ -893,9 +908,12 @@ if __name__ == "__main__":
print(f" \033[32m▶\033[0m \033[1mResolution\033[0m: {res_str}")
print(f" \033[32m▶\033[0m \033[1mDefault\033[0m : mode={args.mode} | pixel={'ON' if args.pixel else 'OFF'} | vol={args.vol}")
print(f"\033[1;37m{''*55}\033[0m")
for i, entry in enumerate(queue, 1):
MAX_DISPLAY = 10
for i, entry in enumerate(queue[:MAX_DISPLAY], 1):
px = ' \033[35m[PIXEL]\033[0m' if entry.get('pixel') else ''
print(f" {i:2}. \033[36m{entry['video']}\033[0m (mode={entry['mode']}{px} vol={entry['vol']})")
if len(queue) > MAX_DISPLAY:
print(f" \033[90m... and {len(queue) - MAX_DISPLAY} more\033[0m")
print(f"\033[1;37m{''*55}\033[0m\n")
print(f" \033[1;32m🚀 Server live →\033[0m \033[4;36mhttp://localhost:{args.port}\033[0m\n")

View file

@ -73,7 +73,7 @@ def test_normalize_rejects_audio_only(tmp_path):
audio = tmp_path / "audio_only.mp4"
r = subprocess.run(
["ffmpeg", "-y", "-f", "lavfi", "-i", "sine=frequency=440:duration=1",
"-c:a", "aac", "-loglevel", "error", str(audio)],
"-c:a", "aac", "-strict", "-2", "-loglevel", "error", str(audio)],
capture_output=True, text=True)
assert r.returncode == 0, r.stderr
with pytest.raises(RuntimeError, match="no video stream"):