From dafd8c538e3f79fc6263092ea8effaa342cb401f Mon Sep 17 00:00:00 2001 From: Max Litruv Boonzaayer Date: Fri, 20 Feb 2026 16:28:00 +1100 Subject: [PATCH] feat: add YouTube streaming support using yt-dlp and update CSP for frame sources --- cinny | 2 +- src-tauri/src/lib.rs | 68 +++++++++++++++++++++++++++++++++++++++ src-tauri/tauri.conf.json | 2 +- 3 files changed, 70 insertions(+), 2 deletions(-) diff --git a/cinny b/cinny index d5ca19f..c55e311 160000 --- a/cinny +++ b/cinny @@ -1 +1 @@ -Subproject commit d5ca19f77043c468408dee3a6cfe2ed2df26c0ed +Subproject commit c55e3112415c25f664bd43d37c941c94dca0a144 diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index e9656df..ada02a2 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -76,6 +76,72 @@ fn open_external_url(url: String) -> Result<(), String> { tauri_plugin_opener::open_url(&url, None::<&str>).map_err(|e| e.to_string()) } +/// YouTube stream info returned by yt-dlp +#[derive(serde::Serialize)] +struct YouTubeStreamInfo { + /// Direct video stream URL (may include video+audio or video only) + video_url: String, + /// Title of the video + title: String, +} + +/// Extract direct YouTube stream URL using yt-dlp +/// Requires yt-dlp to be installed and available in PATH +#[cfg(not(any(target_os = "android", target_os = "ios")))] +#[tauri::command] +async fn get_youtube_stream(url: String) -> Result { + use std::process::Command; + + // First get the title + let title_output = Command::new("yt-dlp") + .args(["--get-title", &url]) + .output() + .map_err(|e| format!("Failed to run yt-dlp (is it installed?): {}", e))?; + + let title = if title_output.status.success() { + String::from_utf8_lossy(&title_output.stdout).trim().to_string() + } else { + "YouTube Video".to_string() + }; + + // Get the best format with video+audio combined (up to 1080p) + // -f "best[height<=1080]" gets combined format + // Fallback to "bestvideo[height<=1080]+bestaudio/best" for separate streams + let output = Command::new("yt-dlp") + .args([ + "-g", // Get URL only + "-f", "best[height<=1080]/bestvideo[height<=1080]+bestaudio/best", + &url + ]) + .output() + .map_err(|e| format!("Failed to run yt-dlp: {}", e))?; + + if !output.status.success() { + let stderr = String::from_utf8_lossy(&output.stderr); + return Err(format!("yt-dlp error: {}", stderr)); + } + + let video_url = String::from_utf8_lossy(&output.stdout) + .lines() + .next() + .unwrap_or("") + .trim() + .to_string(); + + if video_url.is_empty() { + return Err("yt-dlp returned empty URL".to_string()); + } + + Ok(YouTubeStreamInfo { video_url, title }) +} + +/// Stub for mobile platforms - YouTube streaming not supported +#[cfg(any(target_os = "android", target_os = "ios"))] +#[tauri::command] +async fn get_youtube_stream(_url: String) -> Result { + Err("YouTube streaming not supported on mobile".to_string()) +} + /// Start background Matrix sync with the given credentials (mobile only) #[cfg(any(target_os = "android", target_os = "ios"))] #[tauri::command] @@ -300,6 +366,7 @@ pub fn run() { .invoke_handler(tauri::generate_handler![ read_clipboard_image, open_external_url, + get_youtube_stream, start_background_sync, stop_background_sync, get_background_sync_state @@ -344,6 +411,7 @@ pub fn run() { .invoke_handler(tauri::generate_handler![ read_clipboard_image, open_external_url, + get_youtube_stream, start_background_sync, stop_background_sync, get_background_sync_state diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index f946dca..4d6ac1c 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -25,7 +25,7 @@ } ], "security": { - "csp": "script-src blob: data: filesystem: ws: wss: http: https: tauri: 'unsafe-eval' 'unsafe-inline' 'self'; img-src 'self' https: http: data: blob:" + "csp": "script-src blob: data: filesystem: ws: wss: http: https: tauri: 'unsafe-eval' 'unsafe-inline' 'self'; img-src 'self' https: http: data: blob:; frame-src https: http:" } }, "plugins": {