feat: bundle yt-dlp and improve add modal usability

This commit is contained in:
tongki078
2026-02-26 12:16:51 +09:00
parent d85fdc1101
commit 5d8fb9db55
11 changed files with 399 additions and 35 deletions

View File

@@ -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(추가/수정/삭제, 아이콘/확장자 규칙 편집)

View File

@@ -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
View 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"

Binary file not shown.

Binary file not shown.

View File

@@ -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() {

View File

@@ -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,

View File

@@ -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>

View File

@@ -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 })
} }

View File

@@ -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;

View File

@@ -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 {