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