diff --git a/backend/routers/videos.py b/backend/routers/videos.py index e4fd2ea..5504a41 100644 --- a/backend/routers/videos.py +++ b/backend/routers/videos.py @@ -662,10 +662,31 @@ def get_available_subs( youtube_video_id: str, current_user: User = Depends(get_current_user), ): - """Return subtitle languages available on YouTube for a video.""" + """Return subtitle languages available on YouTube for a video (yt-dlp call, slow).""" return ytdlp.fetch_available_subs(youtube_video_id) +@router.get("/by-yt/{youtube_video_id}/subtitle-files") +def list_subtitle_files( + youtube_video_id: str, + current_user: User = Depends(get_current_user), +): + """List .vtt subtitle files already on disk for a downloaded video (instant).""" + import re as _re + from pathlib import Path + from ..config import settings as _cfg + pat = _re.compile(rf'^{_re.escape(youtube_video_id)}\.(.+)\.vtt$') + subs = [] + try: + for f in Path(_cfg.download_path).iterdir(): + m = pat.match(f.name) + if m: + subs.append({"lang": m.group(1), "url": f"/files/{f.name}"}) + except Exception: + pass + return sorted(subs, key=lambda s: s["lang"]) + + @router.get("/by-yt/{youtube_video_id}/comments") def get_comments( youtube_video_id: str, diff --git a/backend/services/ytdlp.py b/backend/services/ytdlp.py index 50d3967..0278bf3 100644 --- a/backend/services/ytdlp.py +++ b/backend/services/ytdlp.py @@ -662,7 +662,7 @@ def start_download( fmt = QUALITY_FORMATS.get(quality, QUALITY_FORMATS["best"]) subtitle_args = ( - ["--write-subs", "--write-auto-subs", "--sub-langs", subtitle_langs, "--convert-subs", "srt"] + ["--write-subs", "--write-auto-subs", "--sub-langs", subtitle_langs, "--convert-subs", "vtt"] if subtitle_langs else [] ) diff --git a/frontend/src/api/index.js b/frontend/src/api/index.js index 94d6612..872a6f3 100644 --- a/frontend/src/api/index.js +++ b/frontend/src/api/index.js @@ -91,6 +91,7 @@ export const createDownload = (youtube_video_id, quality, subtitle_langs) => api.post("/downloads", { youtube_video_id, ...(quality ? { quality } : {}), ...(subtitle_langs ? { subtitle_langs } : {}) }); export const getDownloads = () => api.get("/downloads"); export const getAvailableSubs = (ytId) => api.get(`/videos/by-yt/${ytId}/subs`); +export const getSubtitleFiles = (ytId) => api.get(`/videos/by-yt/${ytId}/subtitle-files`); export const getDownload = (id) => api.get(`/downloads/${id}`); export const deleteDownload = (id) => api.delete(`/downloads/${id}`); export const deleteAllDownloads = () => api.delete("/downloads/all"); diff --git a/frontend/src/pages/Watch.jsx b/frontend/src/pages/Watch.jsx index ec2a6b2..cbadcc4 100644 --- a/frontend/src/pages/Watch.jsx +++ b/frontend/src/pages/Watch.jsx @@ -7,7 +7,7 @@ import { getSettings, updateSettings, getRelatedVideos, getDownloads, rateVideo, getBookmarks, createBookmark, updateBookmark, deleteBookmark, importChapters, clearChapters, getCollections, addToCollection, getQueue, - getVideoComments, refreshVideoComments, getAvailableSubs, + getVideoComments, refreshVideoComments, getAvailableSubs, getSubtitleFiles, } from "../api"; import VideoCard from "../components/VideoCard"; @@ -668,6 +668,12 @@ export default function Watch() { enabled: subsRequested && !!youtubeVideoId, staleTime: 30 * 60_000, }); + const { data: subtitleFiles = [] } = useQuery({ + queryKey: ["subtitle-files", youtubeVideoId], + queryFn: () => getSubtitleFiles(youtubeVideoId).then(r => r.data), + enabled: fileReady, + staleTime: Infinity, + }); const { data: dlStatus } = useQuery({ queryKey: ["download-status", downloadId], @@ -890,7 +896,18 @@ export default function Watch() { if (video?.watch_progress_seconds > 10) v.currentTime = video.watch_progress_seconds; v.play().catch(() => {}); }} - /> + > + {subtitleFiles.map((s, i) => ( + + ))} + ) : ( )} - {!dlComplete && (() => { + {(() => { + // After download: subtitle files on disk are served via in the player + if (fileReady && subtitleFiles.length > 0) return ( + + CC ✓ + + ); + // Before download: let user pick a lang to download with + if (dlComplete) return null; if (!subsRequested) return ( @@ -994,7 +1019,7 @@ export default function Watch() { value={selectedSubLang} onChange={(e) => setSelectedSubLang(e.target.value)} className="bg-zinc-800 text-zinc-300 text-xs rounded-full px-3 py-2 border border-zinc-700 focus:outline-none focus:border-accent" - title="Subtitle language" + title="Subtitle language to download" > {[...manual].map(l => )}