From 41b58a6f70307a261336e37e35918bf49f0de76e Mon Sep 17 00:00:00 2001 From: Nexmoe <16796652+nexmoe@users.noreply.github.com> Date: Mon, 3 Nov 2025 20:13:18 +0800 Subject: [PATCH] feat(format-selector): exclude HLS, sort and improve labels for formats (#13) --- src/main/lib/download-engine.ts | 26 ++++ .../src/components/video/AudioExtractor.tsx | 70 ++++----- .../src/components/video/FormatSelector.tsx | 136 ++++++++++++++---- .../src/components/video/VideoInfoCard.tsx | 72 ++++++---- src/shared/types/index.ts | 1 + 5 files changed, 218 insertions(+), 87 deletions(-) diff --git a/src/main/lib/download-engine.ts b/src/main/lib/download-engine.ts index a9da7a3..7807a39 100644 --- a/src/main/lib/download-engine.ts +++ b/src/main/lib/download-engine.ts @@ -53,6 +53,11 @@ class DownloadEngine extends EventEmitter { // Add encoding support for proper handling of non-ASCII characters args.push('--encoding', 'utf-8') + // Note: Some sites (e.g., YouTube) may not provide filesize information + // in the initial request. This is normal behavior and filesize may be null/undefined + // for many formats. File size information might require additional HTTP HEAD requests + // which would significantly slow down info extraction, so yt-dlp doesn't fetch it by default. + // Add proxy if configured if (settings.proxy) { args.push('--proxy', settings.proxy) @@ -92,6 +97,27 @@ class DownloadEngine extends EventEmitter { if (code === 0 && stdout) { try { const info = JSON.parse(stdout) + + // Calculate estimated file size for formats missing filesize information + // Using tbr (total bitrate in kbps) and duration (in seconds) + // Formula: (tbr * 1000) / 8 * duration = size in bytes + if (info.formats && Array.isArray(info.formats) && info.duration) { + const duration = info.duration + for (const format of info.formats) { + if ( + !format.filesize && + !format.filesize_approx && + format.tbr && + typeof format.tbr === 'number' && + duration > 0 + ) { + // Calculate estimated size: tbr (kbps) * 1000 / 8 bits per byte * duration (seconds) + const estimatedSize = Math.round(((format.tbr * 1000) / 8) * duration) + format.filesize_approx = estimatedSize + } + } + } + scopedLoggers.download.info('Successfully retrieved video info for:', url) resolve(info) } catch (error) { diff --git a/src/renderer/src/components/video/AudioExtractor.tsx b/src/renderer/src/components/video/AudioExtractor.tsx index 89b71e2..6bcd48f 100644 --- a/src/renderer/src/components/video/AudioExtractor.tsx +++ b/src/renderer/src/components/video/AudioExtractor.tsx @@ -41,44 +41,46 @@ export function AudioExtractor({ onExtract }: AudioExtractorProps) { ] return ( - - - {t('audioExtract.title')} + + + {t('audioExtract.title')} -
- - +
+
+ + +
+ +
+ + +
-
- - -
- - diff --git a/src/renderer/src/components/video/FormatSelector.tsx b/src/renderer/src/components/video/FormatSelector.tsx index aeaafae..c9c4631 100644 --- a/src/renderer/src/components/video/FormatSelector.tsx +++ b/src/renderer/src/components/video/FormatSelector.tsx @@ -73,9 +73,22 @@ export function FormatSelector({ useEffect(() => { // Filter and sort formats - const videos = formats.filter((f) => f.video_ext !== 'none' && f.vcodec && f.vcodec !== 'none') + // Exclude m3u8/HLS formats as they are streaming formats not suitable for direct download + const videos = formats.filter( + (f) => + f.video_ext !== 'none' && + f.vcodec && + f.vcodec !== 'none' && + f.protocol !== 'm3u8' && + f.protocol !== 'm3u8_native' + ) const audios = formats.filter( - (f) => f.acodec && f.acodec !== 'none' && (f.video_ext === 'none' || !f.video_ext) + (f) => + f.acodec && + f.acodec !== 'none' && + (f.video_ext === 'none' || !f.video_ext) && + f.protocol !== 'm3u8' && + f.protocol !== 'm3u8_native' ) // Apply showMoreFormats filter @@ -87,6 +100,48 @@ export function FormatSelector({ ? audios : audios.filter((f) => f.ext !== 'webm') + // Sort formats by quality (best first) + const sortVideoFormatsByQuality = (a: VideoFormat, b: VideoFormat) => { + // Sort by height (higher is better) + const aHeight = a.height ?? 0 + const bHeight = b.height ?? 0 + if (aHeight !== bHeight) { + return bHeight - aHeight + } + // If same height, sort by fps (higher is better) + const aFps = a.fps ?? 0 + const bFps = b.fps ?? 0 + if (aFps !== bFps) { + return bFps - aFps + } + // If same quality, prefer formats with file size information + const aHasSize = !!(a.filesize || a.filesize_approx) + const bHasSize = !!(b.filesize || b.filesize_approx) + if (aHasSize !== bHasSize) { + return bHasSize ? 1 : -1 + } + return 0 + } + + const sortAudioFormatsByQuality = (a: VideoFormat, b: VideoFormat) => { + // Sort by bitrate/quality if available + const aQuality = a.tbr ?? a.quality ?? 0 + const bQuality = b.tbr ?? b.quality ?? 0 + if (aQuality !== bQuality) { + return bQuality - aQuality + } + // If same quality, prefer formats with file size information + const aHasSize = !!(a.filesize || a.filesize_approx) + const bHasSize = !!(b.filesize || b.filesize_approx) + if (aHasSize !== bHasSize) { + return bHasSize ? 1 : -1 + } + return 0 + } + + filteredVideos.sort(sortVideoFormatsByQuality) + filteredAudios.sort(sortAudioFormatsByQuality) + setVideoFormats(filteredVideos) setAudioFormats(filteredAudios) @@ -121,25 +176,50 @@ export function FormatSelector({ } const formatVideoLabel = (format: VideoFormat) => { - const quality = `${format.height || '???'}p${format.fps === 60 ? '60' : ''}` - const codec = settings.showMoreFormats ? ` | ${format.vcodec?.split('.')[0]}` : '' + const parts: string[] = [] + // Resolution + if (format.height) { + parts.push(`${format.height}p${format.fps === 60 ? '60' : ''}`) + } + // Format extension + parts.push(format.ext.toUpperCase()) + // Codec (if showMoreFormats is enabled) + if (settings.showMoreFormats && format.vcodec) { + parts.push(format.vcodec.split('.')[0]) + } + // Audio indicator + if (format.acodec !== 'none') { + parts.push('🔊') + } + // File size const size = formatSize(format.filesize || format.filesize_approx) - const hasAudio = format.acodec !== 'none' ? ' 🔊' : '' - return `${quality} | ${format.ext} ${codec} | ${size}${hasAudio}` + if (size !== t('download.unknownSize')) { + parts.push(size) + } + return parts.join(' • ') } const formatAudioLabel = (format: VideoFormat) => { + const parts: string[] = [] + // Quality const quality = format.format_note || t('download.unknownQuality') + parts.push(quality) + // Format extension const ext = format.ext === 'webm' ? 'opus' : format.ext + parts.push(ext.toUpperCase()) + // File size const size = formatSize(format.filesize || format.filesize_approx) - return `${quality} | ${ext} | ${size}` + if (size !== t('download.unknownSize')) { + parts.push(size) + } + return parts.join(' • ') } if (type === 'video') { return ( -
-
- +
+
+
-
- +
+ { @@ -205,17 +287,17 @@ export function FormatSelector({ onAudioFormatChange?.(value) }} > - + - + {audioFormats.map((format) => ( - {formatAudioLabel(format)} + {formatAudioLabel(format)} ))} diff --git a/src/renderer/src/components/video/VideoInfoCard.tsx b/src/renderer/src/components/video/VideoInfoCard.tsx index ffc63a1..befc4fc 100644 --- a/src/renderer/src/components/video/VideoInfoCard.tsx +++ b/src/renderer/src/components/video/VideoInfoCard.tsx @@ -115,53 +115,59 @@ export function VideoInfoCard({ videoInfo }: VideoInfoCardProps) { } return ( -
- - - + +
{/* Thumbnail */}
} />
{/* Video Metadata */} -
-
- {t('download.videoInfo')} +
+
+ {t('download.videoInfo')} {videoInfo.duration && ( - - - {formatDuration(videoInfo.duration)} + + + {formatDuration(videoInfo.duration)} )} {videoInfo.view_count && ( - - - {formatViews(videoInfo.view_count)} + + + {formatViews(videoInfo.view_count)} + + )} + {videoInfo.uploader && ( + + {videoInfo.uploader} )} - {videoInfo.uploader && {videoInfo.uploader}}
-
- +
+ setTitle(e.target.value)} - className="font-medium" + className="font-medium h-10" />
@@ -170,14 +176,18 @@ export function VideoInfoCard({ videoInfo }: VideoInfoCardProps) { - + setActiveTab(v as 'video' | 'audio')}> - - {t('download.video')} - {t('download.audio')} + + + {t('download.video')} + + + {t('download.audio')} + - + - - + - diff --git a/src/shared/types/index.ts b/src/shared/types/index.ts index 66c9a2a..7910611 100644 --- a/src/shared/types/index.ts +++ b/src/shared/types/index.ts @@ -17,6 +17,7 @@ export interface VideoFormat { audio_ext?: string tbr?: number quality?: number + protocol?: string // http, https, m3u8, m3u8_native, etc. } export interface VideoInfo {