diff --git a/backend/routers/videos.py b/backend/routers/videos.py index 5504a41..b94f841 100644 --- a/backend/routers/videos.py +++ b/backend/routers/videos.py @@ -666,6 +666,22 @@ def get_available_subs( return ytdlp.fetch_available_subs(youtube_video_id) +@router.post("/by-yt/{youtube_video_id}/download-subs") +def download_subs( + youtube_video_id: str, + body: dict, + current_user: User = Depends(get_current_user), +): + """Download subtitle file(s) only for an already-downloaded video.""" + langs = (body.get("subtitle_langs") or "").strip() + if not langs: + raise HTTPException(status_code=400, detail="subtitle_langs required") + ok = ytdlp.download_subs_only(youtube_video_id, langs) + if not ok: + raise HTTPException(status_code=500, detail="Subtitle download failed") + return {"ok": True} + + @router.get("/by-yt/{youtube_video_id}/subtitle-files") def list_subtitle_files( youtube_video_id: str, diff --git a/backend/services/ytdlp.py b/backend/services/ytdlp.py index 0278bf3..f6cf8a0 100644 --- a/backend/services/ytdlp.py +++ b/backend/services/ytdlp.py @@ -384,6 +384,22 @@ def fetch_channel_links(channel_id: str) -> list[str]: return list(channel_ids) +def download_subs_only(video_id: str, subtitle_langs: str) -> bool: + """Download subtitle files only (no video) for an already-downloaded video.""" + url = f"https://www.youtube.com/watch?v={video_id}" + output_template = str(Path(settings.download_path) / f"{video_id}.%(ext)s") + _, _, code = _run([ + "yt-dlp", url, + "--skip-download", "--no-playlist", + "--write-subs", "--write-auto-subs", + "--sub-langs", subtitle_langs, + "--convert-subs", "vtt", + "-o", output_template, + *_cookie_args(), + ], timeout=60) + return code == 0 + + def fetch_available_subs(video_id: str) -> dict: """Return subtitle languages available on YouTube for a video. diff --git a/frontend/src/api/index.js b/frontend/src/api/index.js index 872a6f3..ee185f3 100644 --- a/frontend/src/api/index.js +++ b/frontend/src/api/index.js @@ -92,6 +92,7 @@ export const createDownload = (youtube_video_id, quality, 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 downloadSubs = (ytId, subtitle_langs) => api.post(`/videos/by-yt/${ytId}/download-subs`, { subtitle_langs }); 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 cbadcc4..7b179f4 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, getSubtitleFiles, + getVideoComments, refreshVideoComments, getAvailableSubs, getSubtitleFiles, downloadSubs, } from "../api"; import VideoCard from "../components/VideoCard"; @@ -809,6 +809,15 @@ export default function Watch() { onSuccess: (res) => setDisliked(res.data.rating === -1), }); + const addSubsMut = useMutation({ + mutationFn: () => downloadSubs(youtubeVideoId, selectedSubLang), + onSuccess: () => { + qc.invalidateQueries({ queryKey: ["subtitle-files", youtubeVideoId] }); + setSubsRequested(false); + setSelectedSubLang(""); + }, + }); + const handlePiP = useCallback(async () => { if (!videoRef.current) return; try { @@ -986,14 +995,17 @@ export default function Watch() { )} {(() => { - // After download: subtitle files on disk are served via in the player - if (fileReady && subtitleFiles.length > 0) return ( - + // Subs already on disk → show indicator (player CC button handles the rest) + if (subtitleFiles.length > 0 && !subsRequested) return ( + ); - // Before download: let user pick a lang to download with - if (dlComplete) return null; + // Not yet asked → show CC chip if (!subsRequested) return ( ); + // Loading YouTube subtitle list if (subsLoading) return ( @@ -1015,16 +1028,29 @@ export default function Watch() { No CC ); return ( - + <> + + {dlComplete && selectedSubLang && ( + + )} + ); })()}