feat: bundle yt-dlp and improve add modal usability
This commit is contained in:
@@ -13,6 +13,10 @@
|
|||||||
- 고급 기능: 40~50%
|
- 고급 기능: 40~50%
|
||||||
|
|
||||||
## In Progress
|
## In Progress
|
||||||
|
- [~] yt-dlp 번들 내장/탐지
|
||||||
|
- [x] 런타임 탐지에 번들 resources 경로 추가 (`src-tauri/resources/engine/**/yt-dlp`)
|
||||||
|
- [x] 번들 동기화 스크립트 추가 (`npm run sync:ytdlp`)
|
||||||
|
- [ ] Linux/Windows용 yt-dlp 바이너리 동기화 확장
|
||||||
- [~] 범주(Category) 기능 단계 구현
|
- [~] 범주(Category) 기능 단계 구현
|
||||||
- [x] Step 1: 범주 기본 데이터/설정 토글/추가 모달 범주 선택 + 저장 경로 자동 반영
|
- [x] Step 1: 범주 기본 데이터/설정 토글/추가 모달 범주 선택 + 저장 경로 자동 반영
|
||||||
- [ ] Step 2: 범주 관리 UI(추가/수정/삭제, 아이콘/확장자 규칙 편집)
|
- [ ] Step 2: 범주 관리 UI(추가/수정/삭제, 아이콘/확장자 규칙 편집)
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
"tauri:build": "bash scripts/version-bump.sh && tauri build",
|
"tauri:build": "bash scripts/version-bump.sh && tauri build",
|
||||||
"version:bump": "bash scripts/version-bump.sh",
|
"version:bump": "bash scripts/version-bump.sh",
|
||||||
"sync:aria2": "bash scripts/sync-aria2-from-motrix.sh",
|
"sync:aria2": "bash scripts/sync-aria2-from-motrix.sh",
|
||||||
|
"sync:ytdlp": "bash scripts/sync-ytdlp-bundle.sh",
|
||||||
"native-host:smoke": "cd tools/native-host && npm run smoke",
|
"native-host:smoke": "cd tools/native-host && npm run smoke",
|
||||||
"native-host:install": "bash tools/native-host/install-macos.sh makoclohjdpempbndoaljeadpngefhcf",
|
"native-host:install": "bash tools/native-host/install-macos.sh makoclohjdpempbndoaljeadpngefhcf",
|
||||||
"native-host:uninstall": "bash tools/native-host/uninstall-macos.sh"
|
"native-host:uninstall": "bash tools/native-host/uninstall-macos.sh"
|
||||||
|
|||||||
28
scripts/sync-ytdlp-bundle.sh
Executable file
28
scripts/sync-ytdlp-bundle.sh
Executable file
@@ -0,0 +1,28 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
ROOT_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
||||||
|
DST_BASE="$ROOT_DIR/src-tauri/resources/engine"
|
||||||
|
|
||||||
|
TMP_DIR="$(mktemp -d)"
|
||||||
|
cleanup() {
|
||||||
|
rm -rf "$TMP_DIR"
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
mkdir -p "$DST_BASE/darwin/arm64" "$DST_BASE/darwin/x64"
|
||||||
|
|
||||||
|
YTDLP_URL="${YTDLP_URL:-https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp_macos}"
|
||||||
|
TARGET_FILE="$TMP_DIR/yt-dlp"
|
||||||
|
|
||||||
|
echo "[sync-ytdlp] downloading: $YTDLP_URL"
|
||||||
|
curl -fL "$YTDLP_URL" -o "$TARGET_FILE"
|
||||||
|
chmod +x "$TARGET_FILE"
|
||||||
|
|
||||||
|
cp "$TARGET_FILE" "$DST_BASE/darwin/arm64/yt-dlp"
|
||||||
|
cp "$TARGET_FILE" "$DST_BASE/darwin/x64/yt-dlp"
|
||||||
|
chmod +x "$DST_BASE/darwin/arm64/yt-dlp" "$DST_BASE/darwin/x64/yt-dlp"
|
||||||
|
|
||||||
|
echo "[sync-ytdlp] copied to:"
|
||||||
|
echo " - $DST_BASE/darwin/arm64/yt-dlp"
|
||||||
|
echo " - $DST_BASE/darwin/x64/yt-dlp"
|
||||||
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>,
|
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)]
|
#[derive(Debug, Deserialize)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct Aria2AddTorrentRequest {
|
pub struct Aria2AddTorrentRequest {
|
||||||
@@ -799,6 +814,207 @@ fn is_gid_not_found_error(message: &str) -> bool {
|
|||||||
lower.contains("not found for gid#")
|
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]
|
#[tauri::command]
|
||||||
pub fn engine_start(
|
pub fn engine_start(
|
||||||
app: tauri::AppHandle,
|
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}"))
|
.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]
|
#[tauri::command]
|
||||||
pub async fn aria2_add_torrent(request: Aria2AddTorrentRequest) -> Result<String, String> {
|
pub async fn aria2_add_torrent(request: Aria2AddTorrentRequest) -> Result<String, String> {
|
||||||
if request.torrent_base64.trim().is_empty() {
|
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,
|
aria2_remove_task_record, aria2_resume_all, aria2_resume_task, detect_aria2_binary,
|
||||||
engine_start, engine_status, engine_stop,
|
engine_start, engine_status, engine_stop,
|
||||||
load_torrent_file,
|
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]
|
#[tauri::command]
|
||||||
@@ -46,6 +46,8 @@ struct ExternalAddRequest {
|
|||||||
cookie: Option<String>,
|
cookie: Option<String>,
|
||||||
proxy: Option<String>,
|
proxy: Option<String>,
|
||||||
split: Option<u32>,
|
split: Option<u32>,
|
||||||
|
extractor: Option<String>,
|
||||||
|
format: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_extension_ids() -> Vec<String> {
|
fn default_extension_ids() -> Vec<String> {
|
||||||
@@ -266,6 +268,7 @@ pub fn run() {
|
|||||||
detect_aria2_binary,
|
detect_aria2_binary,
|
||||||
aria2_add_torrent,
|
aria2_add_torrent,
|
||||||
aria2_add_uri,
|
aria2_add_uri,
|
||||||
|
yt_dlp_add_uri,
|
||||||
aria2_change_global_option,
|
aria2_change_global_option,
|
||||||
aria2_get_task_detail,
|
aria2_get_task_detail,
|
||||||
aria2_list_tasks,
|
aria2_list_tasks,
|
||||||
|
|||||||
53
src/App.vue
53
src/App.vue
@@ -24,6 +24,7 @@ import {
|
|||||||
resumeAria2Task,
|
resumeAria2Task,
|
||||||
startEngine,
|
startEngine,
|
||||||
stopEngine,
|
stopEngine,
|
||||||
|
ytDlpAddUri,
|
||||||
type Aria2Task,
|
type Aria2Task,
|
||||||
type Aria2TaskDetail,
|
type Aria2TaskDetail,
|
||||||
type Aria2TaskSnapshot,
|
type Aria2TaskSnapshot,
|
||||||
@@ -168,6 +169,8 @@ const rpcTestMessage = ref('')
|
|||||||
|
|
||||||
const showAddModal = ref(false)
|
const showAddModal = ref(false)
|
||||||
const addTab = ref<AddTab>('url')
|
const addTab = ref<AddTab>('url')
|
||||||
|
const addExtractor = ref<'aria2' | 'yt-dlp'>('aria2')
|
||||||
|
const addYtdlpFormat = ref('bestvideo*+bestaudio/best')
|
||||||
const addUrl = ref('')
|
const addUrl = ref('')
|
||||||
const addOut = ref('')
|
const addOut = ref('')
|
||||||
const addSplit = ref(64)
|
const addSplit = ref(64)
|
||||||
@@ -277,6 +280,12 @@ function applyExternalAddPayload(payload: ExternalAddPayload) {
|
|||||||
if (payload.cookie) addCookie.value = payload.cookie
|
if (payload.cookie) addCookie.value = payload.cookie
|
||||||
if (payload.proxy) addProxy.value = payload.proxy
|
if (payload.proxy) addProxy.value = payload.proxy
|
||||||
if (payload.split) addSplit.value = payload.split
|
if (payload.split) addSplit.value = payload.split
|
||||||
|
if (payload.extractor === 'yt-dlp') {
|
||||||
|
addExtractor.value = 'yt-dlp'
|
||||||
|
}
|
||||||
|
if (payload.format) {
|
||||||
|
addYtdlpFormat.value = payload.format
|
||||||
|
}
|
||||||
if (payload.out) {
|
if (payload.out) {
|
||||||
applySuggestedCategory(payload.out)
|
applySuggestedCategory(payload.out)
|
||||||
} else {
|
} else {
|
||||||
@@ -1531,14 +1540,27 @@ async function onSubmitAddTask() {
|
|||||||
}
|
}
|
||||||
const gids: string[] = []
|
const gids: string[] = []
|
||||||
for (const uri of uris) {
|
for (const uri of uris) {
|
||||||
const gid = await addAria2Uri({
|
const gid =
|
||||||
rpc: rpcConfig(),
|
addExtractor.value === 'yt-dlp'
|
||||||
uri,
|
? await ytDlpAddUri({
|
||||||
out: uris.length === 1 ? (addOut.value.trim() || undefined) : undefined,
|
rpc: rpcConfig(),
|
||||||
dir: targetDir,
|
url: uri,
|
||||||
split: addSplit.value,
|
out: uris.length === 1 ? (addOut.value.trim() || undefined) : undefined,
|
||||||
options: Object.keys(addOptions).length > 0 ? addOptions : undefined,
|
dir: targetDir,
|
||||||
})
|
split: addSplit.value,
|
||||||
|
format: addYtdlpFormat.value.trim() || undefined,
|
||||||
|
referer: addReferer.value.trim() || undefined,
|
||||||
|
userAgent: addUserAgent.value.trim() || undefined,
|
||||||
|
options: Object.keys(addOptions).length > 0 ? addOptions : undefined,
|
||||||
|
})
|
||||||
|
: await addAria2Uri({
|
||||||
|
rpc: rpcConfig(),
|
||||||
|
uri,
|
||||||
|
out: uris.length === 1 ? (addOut.value.trim() || undefined) : undefined,
|
||||||
|
dir: targetDir,
|
||||||
|
split: addSplit.value,
|
||||||
|
options: Object.keys(addOptions).length > 0 ? addOptions : undefined,
|
||||||
|
})
|
||||||
gids.push(gid)
|
gids.push(gid)
|
||||||
void inspectAddedTask(gid)
|
void inspectAddedTask(gid)
|
||||||
prependPendingTask(gid, uris.length === 1 ? addOut.value || guessFileNameFromUri(uri) : guessFileNameFromUri(uri), uri)
|
prependPendingTask(gid, uris.length === 1 ? addOut.value || guessFileNameFromUri(uri) : guessFileNameFromUri(uri), uri)
|
||||||
@@ -1568,6 +1590,8 @@ async function onSubmitAddTask() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
addUrl.value = ''
|
addUrl.value = ''
|
||||||
|
addExtractor.value = 'aria2'
|
||||||
|
addYtdlpFormat.value = 'bestvideo*+bestaudio/best'
|
||||||
addOut.value = ''
|
addOut.value = ''
|
||||||
addShowAdvanced.value = false
|
addShowAdvanced.value = false
|
||||||
addUserAgent.value = ''
|
addUserAgent.value = ''
|
||||||
@@ -2229,6 +2253,19 @@ onUnmounted(() => {
|
|||||||
<span>URL</span>
|
<span>URL</span>
|
||||||
<textarea v-model="addUrl" placeholder="한 줄에 작업 URL 하나 (HTTP / FTP / Magnet)" rows="4" />
|
<textarea v-model="addUrl" placeholder="한 줄에 작업 URL 하나 (HTTP / FTP / Magnet)" rows="4" />
|
||||||
</label>
|
</label>
|
||||||
|
<div class="modal-row">
|
||||||
|
<label>
|
||||||
|
<span>처리 엔진</span>
|
||||||
|
<select v-model="addExtractor">
|
||||||
|
<option value="aria2">aria2 (직접 URL)</option>
|
||||||
|
<option value="yt-dlp">yt-dlp (페이지 URL 추출)</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label v-if="addExtractor === 'yt-dlp'">
|
||||||
|
<span>yt-dlp 포맷</span>
|
||||||
|
<input v-model="addYtdlpFormat" type="text" placeholder="bestvideo*+bestaudio/best" />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template v-else>
|
<template v-else>
|
||||||
|
|||||||
@@ -105,6 +105,19 @@ export interface AddUriPayload {
|
|||||||
position?: number
|
position?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface YtDlpAddUriPayload {
|
||||||
|
rpc: Aria2RpcConfig
|
||||||
|
url: string
|
||||||
|
out?: string
|
||||||
|
dir?: string
|
||||||
|
split?: number
|
||||||
|
format?: string
|
||||||
|
referer?: string
|
||||||
|
userAgent?: string
|
||||||
|
options?: Record<string, string | string[]>
|
||||||
|
position?: number
|
||||||
|
}
|
||||||
|
|
||||||
export interface AddTorrentPayload {
|
export interface AddTorrentPayload {
|
||||||
rpc: Aria2RpcConfig
|
rpc: Aria2RpcConfig
|
||||||
torrentBase64: string
|
torrentBase64: string
|
||||||
@@ -141,6 +154,8 @@ export interface ExternalAddRequest {
|
|||||||
cookie?: string
|
cookie?: string
|
||||||
proxy?: string
|
proxy?: string
|
||||||
split?: number
|
split?: number
|
||||||
|
extractor?: string
|
||||||
|
format?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getEngineStatus(): Promise<EngineStatus> {
|
export async function getEngineStatus(): Promise<EngineStatus> {
|
||||||
@@ -171,6 +186,10 @@ export async function addAria2Uri(payload: AddUriPayload): Promise<string> {
|
|||||||
return invoke<string>('aria2_add_uri', { request: payload })
|
return invoke<string>('aria2_add_uri', { request: payload })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function ytDlpAddUri(payload: YtDlpAddUriPayload): Promise<string> {
|
||||||
|
return invoke<string>('yt_dlp_add_uri', { request: payload })
|
||||||
|
}
|
||||||
|
|
||||||
export async function addAria2Torrent(payload: AddTorrentPayload): Promise<string> {
|
export async function addAria2Torrent(payload: AddTorrentPayload): Promise<string> {
|
||||||
return invoke<string>('aria2_add_torrent', { request: payload })
|
return invoke<string>('aria2_add_torrent', { request: payload })
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -648,14 +648,16 @@ h1 { margin: 0; font-size: 1.16rem; font-weight: 600; }
|
|||||||
}
|
}
|
||||||
|
|
||||||
.add-modal input,
|
.add-modal input,
|
||||||
|
.add-modal select,
|
||||||
.add-modal textarea {
|
.add-modal textarea {
|
||||||
height: 33px;
|
height: 33px;
|
||||||
border: 1px solid #434955;
|
border: 1px solid #434955;
|
||||||
border-radius: 7px;
|
border-radius: 4px;
|
||||||
padding: 7px 9px;
|
padding: 7px 9px;
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
color: var(--text-main);
|
color: var(--text-main);
|
||||||
background: #242830;
|
background: #20242c;
|
||||||
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.add-modal textarea {
|
.add-modal textarea {
|
||||||
@@ -665,10 +667,11 @@ h1 { margin: 0; font-size: 1.16rem; font-weight: 600; }
|
|||||||
}
|
}
|
||||||
|
|
||||||
.add-modal input:focus,
|
.add-modal input:focus,
|
||||||
.add-modal textarea:focus {
|
.add-modal textarea:focus,
|
||||||
|
.add-modal select:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
border-color: #656cf5;
|
border-color: #656cf5;
|
||||||
box-shadow: 0 0 0 2px rgba(94, 98, 243, 0.18);
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
@@ -714,18 +717,22 @@ button:disabled { opacity: 0.55; cursor: not-allowed; }
|
|||||||
inset: 0;
|
inset: 0;
|
||||||
background: rgba(0, 0, 0, 0.45);
|
background: rgba(0, 0, 0, 0.45);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-start;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding-top: 90px;
|
padding: 18px;
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
}
|
}
|
||||||
|
|
||||||
.add-modal {
|
.add-modal {
|
||||||
width: min(680px, calc(100vw - 32px));
|
width: min(680px, calc(100vw - 32px));
|
||||||
|
max-height: calc(100vh - 36px);
|
||||||
background: var(--bg-card-soft);
|
background: var(--bg-card-soft);
|
||||||
border: 1px solid #434955;
|
border: 1px solid #434955;
|
||||||
border-radius: 9px;
|
border-radius: 9px;
|
||||||
box-shadow: 0 22px 56px rgba(0, 0, 0, 0.44);
|
box-shadow: 0 22px 56px rgba(0, 0, 0, 0.44);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-header {
|
.modal-header {
|
||||||
@@ -767,6 +774,9 @@ button:disabled { opacity: 0.55; cursor: not-allowed; }
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
|
overflow-y: auto;
|
||||||
|
min-height: 0;
|
||||||
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-body label {
|
.modal-body label {
|
||||||
@@ -796,35 +806,18 @@ button:disabled { opacity: 0.55; cursor: not-allowed; }
|
|||||||
}
|
}
|
||||||
|
|
||||||
.category-select-label {
|
.category-select-label {
|
||||||
position: relative;
|
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.category-select-label::after {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
right: 10px;
|
|
||||||
top: 50%;
|
|
||||||
width: 7px;
|
|
||||||
height: 7px;
|
|
||||||
border-right: 1.5px solid #8f98ab;
|
|
||||||
border-bottom: 1.5px solid #8f98ab;
|
|
||||||
transform: translateY(-60%) rotate(45deg);
|
|
||||||
pointer-events: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.category-select-label select {
|
.category-select-label select {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 33px;
|
height: 33px;
|
||||||
border: 1px solid #3f4654;
|
border: 1px solid #3f4654;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
padding: 7px 28px 7px 9px;
|
padding: 7px 9px;
|
||||||
font-size: 0.84rem;
|
font-size: 0.84rem;
|
||||||
color: var(--text-main);
|
color: var(--text-main);
|
||||||
background: #1d2128;
|
background: #20242c;
|
||||||
appearance: none;
|
|
||||||
-webkit-appearance: none;
|
|
||||||
-moz-appearance: none;
|
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -928,6 +921,7 @@ button:disabled { opacity: 0.55; cursor: not-allowed; }
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-footer-actions {
|
.modal-footer-actions {
|
||||||
@@ -972,6 +966,19 @@ button:disabled { opacity: 0.55; cursor: not-allowed; }
|
|||||||
height: 15px;
|
height: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-height: 760px) {
|
||||||
|
.modal-backdrop {
|
||||||
|
align-items: stretch;
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-modal {
|
||||||
|
width: min(680px, calc(100vw - 20px));
|
||||||
|
max-height: calc(100vh - 20px);
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.expand-advanced-enter-active,
|
.expand-advanced-enter-active,
|
||||||
.expand-advanced-leave-active {
|
.expand-advanced-leave-active {
|
||||||
transition: opacity 140ms ease, transform 140ms ease;
|
transition: opacity 140ms ease, transform 140ms ease;
|
||||||
|
|||||||
@@ -55,7 +55,7 @@ async function handleRequest(message) {
|
|||||||
ok: true,
|
ok: true,
|
||||||
version: '0.1.0',
|
version: '0.1.0',
|
||||||
host: 'org.gdown.nativehost',
|
host: 'org.gdown.nativehost',
|
||||||
capabilities: ['ping', 'addUri', 'focus'],
|
capabilities: ['ping', 'addUri', 'focus', 'extractor:yt-dlp'],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,6 +70,8 @@ async function handleRequest(message) {
|
|||||||
const authorization = String(message?.authorization || '').trim()
|
const authorization = String(message?.authorization || '').trim()
|
||||||
const proxy = String(message?.proxy || '').trim()
|
const proxy = String(message?.proxy || '').trim()
|
||||||
const split = Number(message?.split || 0)
|
const split = Number(message?.split || 0)
|
||||||
|
const extractor = String(message?.extractor || '').trim()
|
||||||
|
const format = String(message?.format || '').trim()
|
||||||
|
|
||||||
const parsedCookie = parseCookieHeader(Array.isArray(message?.headers) ? message.headers : [])
|
const parsedCookie = parseCookieHeader(Array.isArray(message?.headers) ? message.headers : [])
|
||||||
const cookieValue = cookie || parsedCookie
|
const cookieValue = cookie || parsedCookie
|
||||||
@@ -84,6 +86,8 @@ async function handleRequest(message) {
|
|||||||
authorization: authorization || undefined,
|
authorization: authorization || undefined,
|
||||||
proxy: proxy || undefined,
|
proxy: proxy || undefined,
|
||||||
split: Number.isFinite(split) && split > 0 ? Math.round(split) : undefined,
|
split: Number.isFinite(split) && split > 0 ? Math.round(split) : undefined,
|
||||||
|
extractor: extractor || undefined,
|
||||||
|
format: format || undefined,
|
||||||
})
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
Reference in New Issue
Block a user