feat: bundle yt-dlp and improve add modal usability
This commit is contained in:
BIN
src-tauri/resources/engine/darwin/arm64/yt-dlp
Executable file
BIN
src-tauri/resources/engine/darwin/arm64/yt-dlp
Executable file
Binary file not shown.
BIN
src-tauri/resources/engine/darwin/x64/yt-dlp
Executable file
BIN
src-tauri/resources/engine/darwin/x64/yt-dlp
Executable file
Binary file not shown.
@@ -42,6 +42,21 @@ pub struct Aria2AddUriRequest {
|
||||
pub position: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct YtDlpAddUriRequest {
|
||||
pub rpc: Aria2RpcConfig,
|
||||
pub url: String,
|
||||
pub out: Option<String>,
|
||||
pub dir: Option<String>,
|
||||
pub split: Option<u16>,
|
||||
pub format: Option<String>,
|
||||
pub referer: Option<String>,
|
||||
pub user_agent: Option<String>,
|
||||
pub options: Option<BTreeMap<String, Value>>,
|
||||
pub position: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Aria2AddTorrentRequest {
|
||||
@@ -799,6 +814,207 @@ fn is_gid_not_found_error(message: &str) -> bool {
|
||||
lower.contains("not found for gid#")
|
||||
}
|
||||
|
||||
fn default_ytdlp_binary() -> String {
|
||||
if cfg!(target_os = "windows") {
|
||||
"yt-dlp.exe".to_string()
|
||||
} else {
|
||||
"yt-dlp".to_string()
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_ytdlp_binary(app: &tauri::AppHandle) -> Option<String> {
|
||||
let binary_name = default_ytdlp_binary();
|
||||
let mut candidates: Vec<String> = Vec::new();
|
||||
|
||||
for key in ["YTDLP_BIN", "YTDLP_PATH"] {
|
||||
if let Ok(raw) = env::var(key) {
|
||||
let trimmed = raw.trim();
|
||||
if !trimmed.is_empty() {
|
||||
candidates.push(trimmed.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Ok(path_var) = env::var("PATH") {
|
||||
for dir in env::split_paths(&path_var) {
|
||||
candidate_push(&dir.join(&binary_name), &mut candidates);
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
if !binary_name.to_ascii_lowercase().ends_with(".exe") {
|
||||
candidate_push(&dir.join(format!("{binary_name}.exe")), &mut candidates);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let source_engine_base = Path::new(env!("CARGO_MANIFEST_DIR")).join("resources").join("engine");
|
||||
let mut engine_bases = vec![source_engine_base];
|
||||
if let Ok(resource_dir) = app.path().resource_dir() {
|
||||
engine_bases.push(resource_dir.join("engine"));
|
||||
}
|
||||
|
||||
let platforms = platform_aliases();
|
||||
let arches = arch_aliases();
|
||||
for base in engine_bases {
|
||||
for platform in &platforms {
|
||||
for arch in &arches {
|
||||
candidate_push(&base.join(platform).join(arch).join(&binary_name), &mut candidates);
|
||||
}
|
||||
candidate_push(&base.join(platform).join(&binary_name), &mut candidates);
|
||||
}
|
||||
candidate_push(&base.join(&binary_name), &mut candidates);
|
||||
}
|
||||
|
||||
if cfg!(target_os = "macos") {
|
||||
for candidate in ["/opt/homebrew/bin/yt-dlp", "/usr/local/bin/yt-dlp", "/usr/bin/yt-dlp"] {
|
||||
candidates.push(candidate.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
let mut deduped: Vec<String> = Vec::with_capacity(candidates.len());
|
||||
for candidate in candidates {
|
||||
if !deduped.contains(&candidate) {
|
||||
deduped.push(candidate);
|
||||
}
|
||||
}
|
||||
|
||||
deduped.into_iter().find(|path| Path::new(path).is_file())
|
||||
}
|
||||
|
||||
fn run_ytdlp_get_url(
|
||||
binary: &str,
|
||||
page_url: &str,
|
||||
format: Option<&str>,
|
||||
referer: Option<&str>,
|
||||
user_agent: Option<&str>,
|
||||
) -> Result<String, String> {
|
||||
let mut cmd = Command::new(binary);
|
||||
cmd.arg("--no-playlist");
|
||||
cmd.arg("--get-url");
|
||||
if let Some(fmt) = format {
|
||||
let trimmed = fmt.trim();
|
||||
if !trimmed.is_empty() {
|
||||
cmd.arg("-f");
|
||||
cmd.arg(trimmed);
|
||||
}
|
||||
}
|
||||
if let Some(value) = referer {
|
||||
let trimmed = value.trim();
|
||||
if !trimmed.is_empty() {
|
||||
cmd.arg("--referer");
|
||||
cmd.arg(trimmed);
|
||||
}
|
||||
}
|
||||
if let Some(value) = user_agent {
|
||||
let trimmed = value.trim();
|
||||
if !trimmed.is_empty() {
|
||||
cmd.arg("--user-agent");
|
||||
cmd.arg(trimmed);
|
||||
}
|
||||
}
|
||||
cmd.arg(page_url.trim());
|
||||
cmd.stdout(Stdio::piped());
|
||||
cmd.stderr(Stdio::piped());
|
||||
|
||||
let output = cmd
|
||||
.output()
|
||||
.map_err(|err| format!("yt-dlp 실행 실패: {err}"))?;
|
||||
if !output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
|
||||
return Err(format!(
|
||||
"yt-dlp 처리 실패(status={}): {}",
|
||||
output.status,
|
||||
if stderr.is_empty() { "unknown error" } else { &stderr }
|
||||
));
|
||||
}
|
||||
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
let direct = stdout
|
||||
.lines()
|
||||
.map(|line| line.trim())
|
||||
.find(|line| !line.is_empty() && (line.starts_with("http://") || line.starts_with("https://")))
|
||||
.map(|line| line.to_string())
|
||||
.ok_or_else(|| "yt-dlp 결과에서 다운로드 URL을 찾지 못했습니다.".to_string())?;
|
||||
|
||||
Ok(direct)
|
||||
}
|
||||
|
||||
fn sanitize_output_filename(raw: &str) -> String {
|
||||
let mut value = raw.trim().replace(['\r', '\n', '\t'], " ");
|
||||
value = value
|
||||
.chars()
|
||||
.map(|ch| match ch {
|
||||
'/' | '\\' | ':' | '*' | '?' | '"' | '<' | '>' | '|' => '_',
|
||||
_ => ch,
|
||||
})
|
||||
.collect::<String>();
|
||||
while value.contains(" ") {
|
||||
value = value.replace(" ", " ");
|
||||
}
|
||||
value.trim_matches('.').trim().to_string()
|
||||
}
|
||||
|
||||
fn run_ytdlp_get_filename(
|
||||
binary: &str,
|
||||
page_url: &str,
|
||||
format: Option<&str>,
|
||||
referer: Option<&str>,
|
||||
user_agent: Option<&str>,
|
||||
) -> Result<Option<String>, String> {
|
||||
let mut cmd = Command::new(binary);
|
||||
cmd.arg("--no-playlist");
|
||||
cmd.arg("--get-filename");
|
||||
cmd.arg("-o");
|
||||
cmd.arg("%(title).200B.%(ext)s");
|
||||
if let Some(fmt) = format {
|
||||
let trimmed = fmt.trim();
|
||||
if !trimmed.is_empty() {
|
||||
cmd.arg("-f");
|
||||
cmd.arg(trimmed);
|
||||
}
|
||||
}
|
||||
if let Some(value) = referer {
|
||||
let trimmed = value.trim();
|
||||
if !trimmed.is_empty() {
|
||||
cmd.arg("--referer");
|
||||
cmd.arg(trimmed);
|
||||
}
|
||||
}
|
||||
if let Some(value) = user_agent {
|
||||
let trimmed = value.trim();
|
||||
if !trimmed.is_empty() {
|
||||
cmd.arg("--user-agent");
|
||||
cmd.arg(trimmed);
|
||||
}
|
||||
}
|
||||
cmd.arg(page_url.trim());
|
||||
cmd.stdout(Stdio::piped());
|
||||
cmd.stderr(Stdio::piped());
|
||||
|
||||
let output = cmd
|
||||
.output()
|
||||
.map_err(|err| format!("yt-dlp 파일명 추출 실패: {err}"))?;
|
||||
if !output.status.success() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
let raw = stdout
|
||||
.lines()
|
||||
.map(|line| line.trim())
|
||||
.find(|line| !line.is_empty())
|
||||
.unwrap_or("");
|
||||
if raw.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let sanitized = sanitize_output_filename(raw);
|
||||
if sanitized.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
Ok(Some(sanitized))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub fn engine_start(
|
||||
app: tauri::AppHandle,
|
||||
@@ -968,6 +1184,51 @@ pub async fn aria2_add_uri(request: Aria2AddUriRequest) -> Result<String, String
|
||||
.ok_or_else(|| format!("aria2.addUri returned unexpected result: {result}"))
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn yt_dlp_add_uri(app: tauri::AppHandle, request: YtDlpAddUriRequest) -> Result<String, String> {
|
||||
let page_url = request.url.trim();
|
||||
if page_url.is_empty() {
|
||||
return Err("url is required".to_string());
|
||||
}
|
||||
|
||||
let binary = resolve_ytdlp_binary(&app).ok_or_else(|| {
|
||||
"yt-dlp 실행 파일을 찾을 수 없습니다. 번들(resources/engine) 또는 시스템 경로를 확인하세요."
|
||||
.to_string()
|
||||
})?;
|
||||
|
||||
let direct_url = run_ytdlp_get_url(
|
||||
&binary,
|
||||
page_url,
|
||||
request.format.as_deref(),
|
||||
request.referer.as_deref(),
|
||||
request.user_agent.as_deref(),
|
||||
)?;
|
||||
let suggested_out = run_ytdlp_get_filename(
|
||||
&binary,
|
||||
page_url,
|
||||
request.format.as_deref(),
|
||||
request.referer.as_deref(),
|
||||
request.user_agent.as_deref(),
|
||||
)?;
|
||||
let final_out = if request.out.as_ref().map(|v| v.trim().is_empty()).unwrap_or(true) {
|
||||
suggested_out
|
||||
} else {
|
||||
request.out.clone()
|
||||
};
|
||||
|
||||
let aria_request = Aria2AddUriRequest {
|
||||
rpc: request.rpc,
|
||||
uri: direct_url,
|
||||
out: final_out,
|
||||
dir: request.dir,
|
||||
split: request.split,
|
||||
options: request.options,
|
||||
position: request.position,
|
||||
};
|
||||
|
||||
aria2_add_uri(aria_request).await
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
pub async fn aria2_add_torrent(request: Aria2AddTorrentRequest) -> Result<String, String> {
|
||||
if request.torrent_base64.trim().is_empty() {
|
||||
|
||||
@@ -13,7 +13,7 @@ use engine::{
|
||||
aria2_remove_task_record, aria2_resume_all, aria2_resume_task, detect_aria2_binary,
|
||||
engine_start, engine_status, engine_stop,
|
||||
load_torrent_file,
|
||||
open_path_in_file_manager, stop_engine_for_exit, EngineState,
|
||||
open_path_in_file_manager, stop_engine_for_exit, yt_dlp_add_uri, EngineState,
|
||||
};
|
||||
|
||||
#[tauri::command]
|
||||
@@ -46,6 +46,8 @@ struct ExternalAddRequest {
|
||||
cookie: Option<String>,
|
||||
proxy: Option<String>,
|
||||
split: Option<u32>,
|
||||
extractor: Option<String>,
|
||||
format: Option<String>,
|
||||
}
|
||||
|
||||
fn default_extension_ids() -> Vec<String> {
|
||||
@@ -266,6 +268,7 @@ pub fn run() {
|
||||
detect_aria2_binary,
|
||||
aria2_add_torrent,
|
||||
aria2_add_uri,
|
||||
yt_dlp_add_uri,
|
||||
aria2_change_global_option,
|
||||
aria2_get_task_detail,
|
||||
aria2_list_tasks,
|
||||
|
||||
Reference in New Issue
Block a user