diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 45296b2..fcfccab 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -3974,6 +3974,7 @@ name = "stream-capture" version = "1.1.0" dependencies = [ "anyhow", + "base64 0.22.1", "chrono", "futures-util", "regex", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index b26c61a..b16a605 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -19,6 +19,7 @@ tauri-plugin-dialog = "2" serde = { version = "1", features = ["derive"] } serde_json = "1" reqwest = { version = "0.12", features = ["json", "rustls-tls", "stream"] } +base64 = "0.22" tokio = { version = "1", features = ["full"] } anyhow = "1.0" chrono = { version = "0.4", features = ["serde"] } diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index 1b6b524..321b501 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -49,6 +49,28 @@ pub fn get_ffmpeg_version(app: AppHandle) -> Result { binary_manager::get_ffmpeg_version(&app).map_err(|e| e.to_string()) } +#[tauri::command] +pub async fn fetch_image(url: String) -> Result { + use base64::{Engine as _, engine::general_purpose}; + + let client = reqwest::Client::new(); + let res = client.get(&url) + .header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36") + .send() + .await + .map_err(|e| e.to_string())?; + + let bytes = res.bytes().await.map_err(|e| e.to_string())?; + + // Convert to base64 + let b64 = general_purpose::STANDARD.encode(&bytes); + + // Simple heuristic for mime type + let mime = if url.to_lowercase().ends_with(".png") { "image/png" } else { "image/jpeg" }; + + Ok(format!("data:{};base64,{}", mime, b64)) +} + #[tauri::command] pub async fn fetch_metadata(app: AppHandle, url: String, parse_mix_playlist: bool) -> Result { downloader::fetch_metadata(&app, &url, parse_mix_playlist).await.map_err(|e| e.to_string()) diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 6e31ad5..6d567cd 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -17,6 +17,7 @@ pub fn run() { commands::get_ytdlp_version, commands::get_quickjs_version, commands::get_ffmpeg_version, + commands::fetch_image, commands::fetch_metadata, commands::start_download, commands::get_settings, diff --git a/src/views/Home.vue b/src/views/Home.vue index 72a2de7..d8885db 100644 --- a/src/views/Home.vue +++ b/src/views/Home.vue @@ -61,6 +61,38 @@ watch(() => analysisStore.options.is_audio_only, () => { analysisStore.options.output_format = 'original' }) +async function processThumbnail(url: string | undefined): Promise { + if (!url) return undefined; + // Check if it's an Instagram URL or similar that needs proxying + if (url.includes('instagram.com') || url.includes('fbcdn.net') || url.includes('cdninstagram.com')) { + try { + return await invoke('fetch_image', { url }); + } catch (e) { + console.warn('Thumbnail fetch failed, falling back to URL', e); + return url; + } + } + return url; +} + +async function processMetadataThumbnails(metadata: any) { + if (!metadata) return; + + // Process single video thumbnail + if (metadata.thumbnail) { + metadata.thumbnail = await processThumbnail(metadata.thumbnail); + } + + // Process playlist entries + if (metadata.entries && Array.isArray(metadata.entries)) { + await Promise.all(metadata.entries.map(async (entry: any) => { + if (entry.thumbnail) { + entry.thumbnail = await processThumbnail(entry.thumbnail); + } + })); + } +} + async function analyze() { if (!analysisStore.url) return analysisStore.loading = true @@ -122,6 +154,9 @@ async function analyze() { throw new Error("所有链接解析失败或均为播放列表。"); } + // Process thumbnails for batch results + await Promise.all(results.map(r => processMetadataThumbnails(r))); + // Construct synthetic playlist analysisStore.metadata = { id: 'batch_download_' + Date.now(), @@ -155,6 +190,8 @@ async function analyze() { const res = await invoke('fetch_metadata', { url: urlToScan, parseMixPlaylist: parseMix }) + await processMetadataThumbnails(res); + // Initialize selected state for playlist entries if (res.entries) { res.entries = res.entries.map((e: any) => ({ ...e, selected: true }))