YouTube playlist support: /api/youtube/playlist-preview za pred-confirm, /api/youtube zaznava 'list=' URL in kreira batch (1 job/video). Qnet auto-match na YT naslovu, Confirm dialog v UI z prvih 5 naslovov.

This commit is contained in:
OpenClaw Agent
2026-05-02 12:34:27 +00:00
parent 7a7d7ea20d
commit 77075795ce
3 changed files with 252 additions and 7 deletions
+139 -2
View File
@@ -1345,17 +1345,154 @@ async def upload_video(
# ────────────────────────────────────────────────────────────────
# YouTube submit
# ────────────────────────────────────────────────────────────────
class YouTubePlaylistPreviewIn(BaseModel):
url: str
@app.post("/api/youtube/playlist-preview")
async def youtube_playlist_preview(payload: YouTubePlaylistPreviewIn, user: str = Depends(check_auth)):
"""Resolve YouTube URL — vrne listo videov če je playlist, ali en item.
Frontend uporabi to za preview pred submit-om: če je playlist, user vidi
koliko komadov je in lahko potrdi.
"""
import sys as _sys
_sys.path.insert(0, str(SCRIPTS_DIR))
try:
from yt_download import get_playlist
except ImportError:
raise HTTPException(500, "yt_download modul ne dostopen")
try:
info = get_playlist(payload.url)
except subprocess.TimeoutExpired:
raise HTTPException(504, "yt-dlp timeout (>120s) — preveri URL")
except Exception as e:
raise HTTPException(500, f"yt-dlp napaka: {e}")
if info.get("error"):
raise HTTPException(400, f"yt-dlp: {info['error']}")
if not info.get("items"):
raise HTTPException(400, "Ni najdenih videov na tem URL-u")
return info
@app.post("/api/youtube")
async def submit_youtube(
payload: YouTubeJobIn,
background: BackgroundTasks,
user: str = Depends(check_auth),
):
"""Submit YouTube URL — single video ali cela playlist.
Če URL vsebuje 'list=' parameter, resolve-amo playlist in kreiramo
batch jobs (en job za vsak video). Single video kreira en job.
"""
url = payload.url.strip()
is_playlist_url = "list=" in url and "watch?" not in url.split("list=")[0].split("&")[0]
# Bolj robustno: če ima list= ampak tudi v= potem je morda single video iz playliste
# → tretiraj kot single (default)
has_list = "list=" in url
has_v = "v=" in url or "/watch?" in url or "youtu.be/" in url
treat_as_playlist = has_list and not has_v # samo "playlist?list=..." URLs
# Pa tudi če je explicit "/playlist?" v URL-u
if "/playlist?" in url:
treat_as_playlist = True
if treat_as_playlist:
# Resolve playlist
import sys as _sys
_sys.path.insert(0, str(SCRIPTS_DIR))
try:
from yt_download import get_playlist
except ImportError:
raise HTTPException(500, "yt_download modul ne dostopen")
try:
info = get_playlist(url)
except Exception as e:
raise HTTPException(500, f"Playlist resolve napaka: {e}")
items = info.get("items", [])
if not items:
raise HTTPException(400, "Playlist je prazen ali ni dostopen")
# Batch ID za grupiranje
batch_id = "yt-batch-" + uuid.uuid4().hex[:8]
playlist_title = info.get("playlist_title", "YouTube Playlist")
created_jobs = []
for item in items:
job_id = uuid.uuid4().hex[:12]
# Probaj match proti Qnet bazi (parsa Artist - Title iz YouTube naslova)
yt_title = item.get("title", "")
qm = qnet_match.match_filename(yt_title)
job = {
"id": job_id,
"source_type": "youtube",
"youtube_url": item["url"],
"youtube_title": yt_title,
"youtube_id": item.get("id"),
"youtube_uploader": item.get("uploader", ""),
"playlist_url": url,
"playlist_title": playlist_title,
"batch_id": batch_id,
"status": "queued",
"current_step": "V vrsti za YouTube prenos",
"created_at": time.time(),
"updated_at": time.time(),
"mode": payload.mode,
"lang": payload.lang,
"auto_chorus": payload.auto_chorus,
"start": payload.start,
"duration": payload.duration,
"no_subs": payload.no_subs,
"subtitle_style": payload.subtitle_style,
"whisper_model": payload.whisper_model,
"quality": payload.quality,
"tv_station": payload.tv_station,
}
# Qnet match — auto-fill clean Artist/Title in tv_station če je matched
if qm["matched"] and qm["confidence"] >= 0.85:
job["parsed_artist"] = qm["artist"]
job["parsed_title"] = qm["title"]
job["has_clean_name"] = True
job["qnet_match"] = {
"method": qm["method"],
"confidence": qm["confidence"],
"matched_file": qm["file"],
"matched_station": qm["station"],
}
# Auto-set tv_station iz Qnet match-a (override default)
job["tv_station"] = qm["station"]
else:
# Fallback: regex parser na YT naslovu
a, t = parse_artist_title(yt_title)
if a: job["parsed_artist"] = a
if t: job["parsed_title"] = t
job["has_clean_name"] = bool(a and t)
save_job(job)
created_jobs.append(job)
print(f"📋 YouTube playlist '{playlist_title}': {len(created_jobs)} jobov v batch {batch_id}", flush=True)
return {
"is_playlist": True,
"batch_id": batch_id,
"playlist_title": playlist_title,
"count": len(created_jobs),
"jobs": created_jobs,
}
# ─── Single video ───
job_id = uuid.uuid4().hex[:12]
job = {
"id": job_id,
"source_type": "youtube",
"youtube_url": payload.url,
"youtube_url": url,
"status": "queued",
"current_step": "V vrsti za YouTube prenos",
"created_at": time.time(),
@@ -1369,9 +1506,9 @@ async def submit_youtube(
"subtitle_style": payload.subtitle_style,
"whisper_model": payload.whisper_model,
"quality": payload.quality,
"tv_station": payload.tv_station,
}
save_job(job)
# Queue worker bo pograbil
return job