diff --git a/backend/thirdparty/downloader/views.py b/backend/thirdparty/downloader/views.py index 6938835..441bcd6 100644 --- a/backend/thirdparty/downloader/views.py +++ b/backend/thirdparty/downloader/views.py @@ -96,6 +96,13 @@ class Downloader(APIView): child=serializers.CharField(), help_text="List of available audio format options" ), + "subtitle_languages": serializers.ListField( + child=serializers.CharField(), + help_text="List of manually available subtitle language codes (e.g., 'en', 'cs')" + ), + "has_subtitles": serializers.BooleanField( + help_text="True if any subtitles or auto-captions are available" + ), } ), help_text="Array of video information (single video for individual URLs, multiple for playlists)" @@ -177,13 +184,20 @@ class Downloader(APIView): # If thumbnail fetch fails, just continue without it pass + subtitles_data = video_data.get("subtitles") or {} + auto_captions = video_data.get("automatic_captions") or {} + subtitle_languages = sorted(subtitles_data.keys()) + has_subtitles = bool(subtitles_data) or bool(auto_captions) + return { "id": video_data.get("id", ""), "title": video_data.get("title", ""), "duration": video_data.get("duration"), - "thumbnail": thumbnail_blob, # Now a base64 blob instead of URL + "thumbnail": thumbnail_blob, "video_resolutions": video_resolutions, "audio_resolutions": audio_resolutions, + "subtitle_languages": subtitle_languages, + "has_subtitles": has_subtitles, } # Check if this is a playlist diff --git a/frontend/src/api/generated/private/models/videoInfo.ts b/frontend/src/api/generated/private/models/videoInfo.ts index 4f25392..0e270d2 100644 --- a/frontend/src/api/generated/private/models/videoInfo.ts +++ b/frontend/src/api/generated/private/models/videoInfo.ts @@ -23,4 +23,8 @@ export interface VideoInfo { video_resolutions: string[]; /** List of available audio format options */ audio_resolutions: string[]; + /** List of manually available subtitle language codes (e.g., 'en', 'cs') */ + subtitle_languages: string[]; + /** True if any subtitles or auto-captions are available */ + has_subtitles: boolean; } diff --git a/frontend/src/api/generated/public/models/videoInfo.ts b/frontend/src/api/generated/public/models/videoInfo.ts index 4f25392..0e270d2 100644 --- a/frontend/src/api/generated/public/models/videoInfo.ts +++ b/frontend/src/api/generated/public/models/videoInfo.ts @@ -23,4 +23,8 @@ export interface VideoInfo { video_resolutions: string[]; /** List of available audio format options */ audio_resolutions: string[]; + /** List of manually available subtitle language codes (e.g., 'en', 'cs') */ + subtitle_languages: string[]; + /** True if any subtitles or auto-captions are available */ + has_subtitles: boolean; } diff --git a/frontend/src/components/downloader/Downloader.tsx b/frontend/src/components/downloader/Downloader.tsx index 90fb0b0..0ac408c 100644 --- a/frontend/src/components/downloader/Downloader.tsx +++ b/frontend/src/components/downloader/Downloader.tsx @@ -97,6 +97,18 @@ export default function Downloader() { }); }; + const getSubtitleLanguages = () => { + if (!videoInfo?.videos) return []; + const allLangs = new Set(); + videoInfo.videos.forEach(video => { + video.subtitle_languages.forEach(lang => allLangs.add(lang)); + }); + return Array.from(allLangs).sort(); + }; + + const videoHasSubtitles = videoInfo?.videos.some(v => v.has_subtitles) ?? false; + const videoHasThumbnail = videoInfo?.videos.some(v => v.thumbnail !== null) ?? false; + async function retrieveVideoInfo() { setIsLoading(true); setError(null); @@ -479,40 +491,88 @@ export default function Downloader() {
- setSubtitles(e.target.value)} - placeholder="e.g., 'en', 'en,cs', or 'all'" - className="w-full p-2 border rounded" - /> -

- Language codes (e.g., 'en', 'cs') or 'all' for all available -

+ {(() => { + const subtitleLanguages = getSubtitleLanguages(); + if (subtitleLanguages.length > 0) { + return ( + <> + +

+ {subtitleLanguages.length} language{subtitleLanguages.length !== 1 ? 's' : ''} available +

+ + ); + } + return ( + <> + setSubtitles(e.target.value)} + placeholder={videoHasSubtitles ? "e.g., 'en', 'en,cs', or 'all'" : 'No subtitles available'} + disabled={!videoHasSubtitles} + className={`w-full p-2 border rounded ${!videoHasSubtitles ? 'opacity-50 cursor-not-allowed bg-gray-100' : ''}`} + /> + {videoHasSubtitles && ( +

+ Auto-captions available — enter language codes (e.g., 'en', 'cs') or 'all' +

+ )} + + ); + })()}
{/* Checkboxes Row */}
- + {(() => { + const embedSubsDisabled = !subtitles || !['mkv', 'mp4'].includes(selectedExtension); + return ( + + ); + })()} -