mirror of
https://github.com/nimbold/Firelink.git
synced 2026-08-31 13:08:17 +00:00
feat(media): support explicit fetch media intent
This commit is contained in:
+1
-1
Submodule Extensions/Firefox updated: 19e4b1b6d1...e2b2ec7b4e
@@ -31,7 +31,7 @@ const CLIENT_NONCE_HEADER: &str = "x-firelink-client-nonce";
|
|||||||
const SERVER_PROOF_HEADER: &str = "x-firelink-server-proof";
|
const SERVER_PROOF_HEADER: &str = "x-firelink-server-proof";
|
||||||
const SERVER_PORT_HEADER: &str = "x-firelink-server-port";
|
const SERVER_PORT_HEADER: &str = "x-firelink-server-port";
|
||||||
const SERVER_PROOF_PREFIX: &[u8] = b"firelink-server-proof\n";
|
const SERVER_PROOF_PREFIX: &[u8] = b"firelink-server-proof\n";
|
||||||
const PROTOCOL_VERSION: &str = "3";
|
const PROTOCOL_VERSION: &str = "4";
|
||||||
|
|
||||||
type HmacSha256 = Hmac<Sha256>;
|
type HmacSha256 = Hmac<Sha256>;
|
||||||
pub type SharedExtensionToken = Arc<RwLock<String>>;
|
pub type SharedExtensionToken = Arc<RwLock<String>>;
|
||||||
@@ -61,6 +61,8 @@ struct ExtensionRequest {
|
|||||||
headers: Option<String>,
|
headers: Option<String>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
cookies: Option<String>,
|
cookies: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
media: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Serialize, TS)]
|
#[derive(Clone, Serialize, TS)]
|
||||||
@@ -72,6 +74,7 @@ pub struct ExtensionDownload {
|
|||||||
filename: Option<String>,
|
filename: Option<String>,
|
||||||
headers: Option<String>,
|
headers: Option<String>,
|
||||||
cookies: Option<String>,
|
cookies: Option<String>,
|
||||||
|
media: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn start_server(
|
pub async fn start_server(
|
||||||
@@ -318,6 +321,7 @@ fn normalize_download(payload: ExtensionRequest) -> Option<ExtensionDownload> {
|
|||||||
filename,
|
filename,
|
||||||
headers: payload.headers.filter(|value| !value.trim().is_empty()),
|
headers: payload.headers.filter(|value| !value.trim().is_empty()),
|
||||||
cookies: payload.cookies.filter(|value| !value.trim().is_empty()),
|
cookies: payload.cookies.filter(|value| !value.trim().is_empty()),
|
||||||
|
media: payload.media,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -478,7 +482,7 @@ mod tests {
|
|||||||
assert_eq!(response.headers().get(SERVER_HEADER).unwrap(), "1");
|
assert_eq!(response.headers().get(SERVER_HEADER).unwrap(), "1");
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
response.headers().get(PROTOCOL_VERSION_HEADER).unwrap(),
|
response.headers().get(PROTOCOL_VERSION_HEADER).unwrap(),
|
||||||
"3"
|
"4"
|
||||||
);
|
);
|
||||||
|
|
||||||
server.abort();
|
server.abort();
|
||||||
|
|||||||
+71
-10
@@ -20,16 +20,22 @@ fn get_metadata_cache() -> &'static std::sync::Mutex<HashMap<String, String>> {
|
|||||||
fn metadata_info_cache_key(
|
fn metadata_info_cache_key(
|
||||||
url: &str,
|
url: &str,
|
||||||
cookie_source: Option<&str>,
|
cookie_source: Option<&str>,
|
||||||
|
username: Option<&str>,
|
||||||
|
password: Option<&str>,
|
||||||
headers: Option<&str>,
|
headers: Option<&str>,
|
||||||
cookies: Option<&str>,
|
cookies: Option<&str>,
|
||||||
proxy: Option<&str>,
|
proxy: Option<&str>,
|
||||||
|
user_agent: Option<&str>,
|
||||||
) -> String {
|
) -> String {
|
||||||
serde_json::json!([
|
serde_json::json!([
|
||||||
url,
|
url,
|
||||||
cookie_source.unwrap_or(""),
|
cookie_source.unwrap_or(""),
|
||||||
|
username.unwrap_or(""),
|
||||||
|
password.unwrap_or(""),
|
||||||
headers.unwrap_or(""),
|
headers.unwrap_or(""),
|
||||||
cookies.unwrap_or(""),
|
cookies.unwrap_or(""),
|
||||||
proxy.unwrap_or("")
|
proxy.unwrap_or(""),
|
||||||
|
user_agent.unwrap_or("")
|
||||||
])
|
])
|
||||||
.to_string()
|
.to_string()
|
||||||
}
|
}
|
||||||
@@ -1320,6 +1326,7 @@ async fn fetch_media_metadata(
|
|||||||
app_handle: tauri::AppHandle,
|
app_handle: tauri::AppHandle,
|
||||||
url: String,
|
url: String,
|
||||||
cookie_browser: Option<String>,
|
cookie_browser: Option<String>,
|
||||||
|
user_agent: Option<String>,
|
||||||
username: Option<String>,
|
username: Option<String>,
|
||||||
password: Option<String>,
|
password: Option<String>,
|
||||||
headers: Option<String>,
|
headers: Option<String>,
|
||||||
@@ -1370,6 +1377,7 @@ async fn fetch_media_metadata(
|
|||||||
app_handle.clone(),
|
app_handle.clone(),
|
||||||
url.clone(),
|
url.clone(),
|
||||||
cookie_browser.clone(),
|
cookie_browser.clone(),
|
||||||
|
user_agent.clone(),
|
||||||
username.clone(),
|
username.clone(),
|
||||||
password.clone(),
|
password.clone(),
|
||||||
headers.clone(),
|
headers.clone(),
|
||||||
@@ -1387,7 +1395,7 @@ async fn fetch_media_metadata(
|
|||||||
browser
|
browser
|
||||||
);
|
);
|
||||||
fetch_media_metadata_uncached(
|
fetch_media_metadata_uncached(
|
||||||
app_handle, url, None, username, password, headers, cookies, proxy,
|
app_handle, url, None, user_agent, username, password, headers, cookies, proxy,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
@@ -1427,6 +1435,7 @@ async fn fetch_media_metadata_uncached(
|
|||||||
app_handle: tauri::AppHandle,
|
app_handle: tauri::AppHandle,
|
||||||
url: String,
|
url: String,
|
||||||
cookie_browser: Option<String>,
|
cookie_browser: Option<String>,
|
||||||
|
user_agent: Option<String>,
|
||||||
username: Option<String>,
|
username: Option<String>,
|
||||||
password: Option<String>,
|
password: Option<String>,
|
||||||
headers: Option<String>,
|
headers: Option<String>,
|
||||||
@@ -1436,9 +1445,12 @@ async fn fetch_media_metadata_uncached(
|
|||||||
let info_cache_key = metadata_info_cache_key(
|
let info_cache_key = metadata_info_cache_key(
|
||||||
&url,
|
&url,
|
||||||
cookie_browser.as_deref(),
|
cookie_browser.as_deref(),
|
||||||
|
username.as_deref(),
|
||||||
|
password.as_deref(),
|
||||||
headers.as_deref(),
|
headers.as_deref(),
|
||||||
cookies.as_deref(),
|
cookies.as_deref(),
|
||||||
proxy.as_deref(),
|
proxy.as_deref(),
|
||||||
|
user_agent.as_deref(),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Pass bundled tools by absolute path so extraction never depends on
|
// Pass bundled tools by absolute path so extraction never depends on
|
||||||
@@ -1489,20 +1501,24 @@ async fn fetch_media_metadata_uncached(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(ua) = user_agent.as_deref().map(str::trim).filter(|s| !s.is_empty()) {
|
||||||
|
cmd = cmd.arg("--user-agent").arg(ua);
|
||||||
|
}
|
||||||
|
|
||||||
let mut config_file = tempfile::Builder::new()
|
let mut config_file = tempfile::Builder::new()
|
||||||
.prefix("ytdlp-")
|
.prefix("ytdlp-")
|
||||||
.suffix(".conf")
|
.suffix(".conf")
|
||||||
.tempfile()
|
.tempfile()
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(|e| e.to_string())?;
|
||||||
let mut config_content = String::new();
|
let mut config_content = String::new();
|
||||||
if let Some(user) = username {
|
if let Some(user) = username.as_deref() {
|
||||||
if !user.is_empty() {
|
if !user.is_empty() {
|
||||||
append_ytdlp_config_option(&mut config_content, "--username", &user);
|
append_ytdlp_config_option(&mut config_content, "--username", user);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some(pass) = password {
|
if let Some(pass) = password.as_deref() {
|
||||||
if !pass.is_empty() {
|
if !pass.is_empty() {
|
||||||
append_ytdlp_config_option(&mut config_content, "--password", &pass);
|
append_ytdlp_config_option(&mut config_content, "--password", pass);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
append_ytdlp_http_headers(&mut config_content, headers.as_deref(), cookies.as_deref())?;
|
append_ytdlp_http_headers(&mut config_content, headers.as_deref(), cookies.as_deref())?;
|
||||||
@@ -2678,14 +2694,14 @@ pub(crate) async fn start_media_download_internal(
|
|||||||
.tempfile()
|
.tempfile()
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(|e| e.to_string())?;
|
||||||
let mut config_content = String::new();
|
let mut config_content = String::new();
|
||||||
if let Some(user) = username {
|
if let Some(user) = username.as_deref() {
|
||||||
if !user.is_empty() {
|
if !user.is_empty() {
|
||||||
append_ytdlp_config_option(&mut config_content, "--username", &user);
|
append_ytdlp_config_option(&mut config_content, "--username", user);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some(pass) = password {
|
if let Some(pass) = password.as_deref() {
|
||||||
if !pass.is_empty() {
|
if !pass.is_empty() {
|
||||||
append_ytdlp_config_option(&mut config_content, "--password", &pass);
|
append_ytdlp_config_option(&mut config_content, "--password", pass);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
append_ytdlp_http_headers(&mut config_content, headers.as_deref(), cookies.as_deref())?;
|
append_ytdlp_http_headers(&mut config_content, headers.as_deref(), cookies.as_deref())?;
|
||||||
@@ -2820,9 +2836,12 @@ pub(crate) async fn start_media_download_internal(
|
|||||||
let info_cache_key = metadata_info_cache_key(
|
let info_cache_key = metadata_info_cache_key(
|
||||||
&url,
|
&url,
|
||||||
effective_cookie_source.as_deref(),
|
effective_cookie_source.as_deref(),
|
||||||
|
username.as_deref(),
|
||||||
|
password.as_deref(),
|
||||||
headers.as_deref(),
|
headers.as_deref(),
|
||||||
cookies.as_deref(),
|
cookies.as_deref(),
|
||||||
proxy.as_deref(),
|
proxy.as_deref(),
|
||||||
|
user_agent.as_deref(),
|
||||||
);
|
);
|
||||||
if let Some(json_str) = cache.remove(&info_cache_key) {
|
if let Some(json_str) = cache.remove(&info_cache_key) {
|
||||||
let temp_dir = std::env::temp_dir();
|
let temp_dir = std::env::temp_dir();
|
||||||
@@ -4604,27 +4623,69 @@ mod tests {
|
|||||||
let base = metadata_info_cache_key(
|
let base = metadata_info_cache_key(
|
||||||
"https://example.com/watch?v=1",
|
"https://example.com/watch?v=1",
|
||||||
Some("firefox"),
|
Some("firefox"),
|
||||||
|
Some("user-one"),
|
||||||
|
Some("pass-one"),
|
||||||
Some("User-Agent: Browser A"),
|
Some("User-Agent: Browser A"),
|
||||||
Some("session=one"),
|
Some("session=one"),
|
||||||
None,
|
None,
|
||||||
|
Some("Custom UA A"),
|
||||||
);
|
);
|
||||||
let changed_headers = metadata_info_cache_key(
|
let changed_headers = metadata_info_cache_key(
|
||||||
"https://example.com/watch?v=1",
|
"https://example.com/watch?v=1",
|
||||||
Some("firefox"),
|
Some("firefox"),
|
||||||
|
Some("user-one"),
|
||||||
|
Some("pass-one"),
|
||||||
Some("User-Agent: Browser B"),
|
Some("User-Agent: Browser B"),
|
||||||
Some("session=one"),
|
Some("session=one"),
|
||||||
None,
|
None,
|
||||||
|
Some("Custom UA A"),
|
||||||
);
|
);
|
||||||
let changed_cookies = metadata_info_cache_key(
|
let changed_cookies = metadata_info_cache_key(
|
||||||
"https://example.com/watch?v=1",
|
"https://example.com/watch?v=1",
|
||||||
Some("firefox"),
|
Some("firefox"),
|
||||||
|
Some("user-one"),
|
||||||
|
Some("pass-one"),
|
||||||
Some("User-Agent: Browser A"),
|
Some("User-Agent: Browser A"),
|
||||||
Some("session=two"),
|
Some("session=two"),
|
||||||
None,
|
None,
|
||||||
|
Some("Custom UA A"),
|
||||||
|
);
|
||||||
|
let changed_username = metadata_info_cache_key(
|
||||||
|
"https://example.com/watch?v=1",
|
||||||
|
Some("firefox"),
|
||||||
|
Some("user-two"),
|
||||||
|
Some("pass-one"),
|
||||||
|
Some("User-Agent: Browser A"),
|
||||||
|
Some("session=one"),
|
||||||
|
None,
|
||||||
|
Some("Custom UA A"),
|
||||||
|
);
|
||||||
|
let changed_password = metadata_info_cache_key(
|
||||||
|
"https://example.com/watch?v=1",
|
||||||
|
Some("firefox"),
|
||||||
|
Some("user-one"),
|
||||||
|
Some("pass-two"),
|
||||||
|
Some("User-Agent: Browser A"),
|
||||||
|
Some("session=one"),
|
||||||
|
None,
|
||||||
|
Some("Custom UA A"),
|
||||||
|
);
|
||||||
|
let changed_user_agent = metadata_info_cache_key(
|
||||||
|
"https://example.com/watch?v=1",
|
||||||
|
Some("firefox"),
|
||||||
|
Some("user-one"),
|
||||||
|
Some("pass-one"),
|
||||||
|
Some("User-Agent: Browser A"),
|
||||||
|
Some("session=one"),
|
||||||
|
None,
|
||||||
|
Some("Custom UA B"),
|
||||||
);
|
);
|
||||||
|
|
||||||
assert_ne!(base, changed_headers);
|
assert_ne!(base, changed_headers);
|
||||||
assert_ne!(base, changed_cookies);
|
assert_ne!(base, changed_cookies);
|
||||||
|
assert_ne!(base, changed_username);
|
||||||
|
assert_ne!(base, changed_password);
|
||||||
|
assert_ne!(base, changed_user_agent);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -478,6 +478,11 @@ pub static SUPPORTED_DOMAINS: &[&str] = &[
|
|||||||
"reddit.com",
|
"reddit.com",
|
||||||
"v.redd.it",
|
"v.redd.it",
|
||||||
"soundcloud.com",
|
"soundcloud.com",
|
||||||
|
"pornhub.com",
|
||||||
|
"redtube.com",
|
||||||
|
"xhamster.com",
|
||||||
|
"xnxx.com",
|
||||||
|
"xvideos.com",
|
||||||
];
|
];
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
|
|||||||
@@ -1,3 +1,3 @@
|
|||||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||||
|
|
||||||
export type ExtensionDownload = { urls: Array<string>, referer: string | null, silent: boolean, filename: string | null, headers: string | null, cookies: string | null, };
|
export type ExtensionDownload = { urls: Array<string>, referer: string | null, silent: boolean, filename: string | null, headers: string | null, cookies: string | null, media: boolean, };
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ export const AddDownloadsModal = () => {
|
|||||||
pendingAddFilename,
|
pendingAddFilename,
|
||||||
pendingAddHeaders,
|
pendingAddHeaders,
|
||||||
pendingAddCookies,
|
pendingAddCookies,
|
||||||
|
pendingAddMediaUrls,
|
||||||
toggleAddModal,
|
toggleAddModal,
|
||||||
addDownload,
|
addDownload,
|
||||||
queues
|
queues
|
||||||
@@ -136,6 +137,7 @@ export const AddDownloadsModal = () => {
|
|||||||
pendingAddReferer,
|
pendingAddReferer,
|
||||||
pendingAddHeaders,
|
pendingAddHeaders,
|
||||||
pendingAddCookies,
|
pendingAddCookies,
|
||||||
|
pendingAddMediaUrls,
|
||||||
baseDownloadFolder,
|
baseDownloadFolder,
|
||||||
perServerConnections
|
perServerConnections
|
||||||
]);
|
]);
|
||||||
@@ -173,10 +175,17 @@ export const AddDownloadsModal = () => {
|
|||||||
}, [saveLocation, isAddModalOpen]);
|
}, [saveLocation, isAddModalOpen]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
const forcedMediaUrls = new Set(pendingAddMediaUrls.map(url => {
|
||||||
|
try {
|
||||||
|
return new URL(url).href;
|
||||||
|
} catch {
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
}));
|
||||||
setParsedItems(current =>
|
setParsedItems(current =>
|
||||||
reconcileDownloadRows(urls, current, pendingAddFilename || undefined)
|
reconcileDownloadRows(urls, current, pendingAddFilename || undefined, forcedMediaUrls)
|
||||||
);
|
);
|
||||||
}, [urls, pendingAddFilename]);
|
}, [urls, pendingAddFilename, pendingAddMediaUrls]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
for (const row of parsedItems) {
|
for (const row of parsedItems) {
|
||||||
@@ -205,6 +214,7 @@ export const AddDownloadsModal = () => {
|
|||||||
const mediaData = await fetchMediaMetadataDeduped({
|
const mediaData = await fetchMediaMetadataDeduped({
|
||||||
url: row.sourceUrl,
|
url: row.sourceUrl,
|
||||||
cookieBrowser: browserArg,
|
cookieBrowser: browserArg,
|
||||||
|
userAgent: settingsStore.customUserAgent.trim() || null,
|
||||||
username: useAuth ? username.trim() || null : login?.username || null,
|
username: useAuth ? username.trim() || null : login?.username || null,
|
||||||
password: useAuth ? password || null : keychainPassword,
|
password: useAuth ? password || null : keychainPassword,
|
||||||
headers: headers?.trim() || null,
|
headers: headers?.trim() || null,
|
||||||
|
|||||||
+1
-1
@@ -21,7 +21,7 @@ type CommandMap = {
|
|||||||
result: MetadataResponse;
|
result: MetadataResponse;
|
||||||
};
|
};
|
||||||
fetch_media_metadata: {
|
fetch_media_metadata: {
|
||||||
args: { url: string; cookieBrowser: string | null; username: string | null; password: string | null; headers: string | null; cookies: string | null; proxy: string | null };
|
args: { url: string; cookieBrowser: string | null; userAgent: string | null; username: string | null; password: string | null; headers: string | null; cookies: string | null; proxy: string | null };
|
||||||
result: MediaMetadata;
|
result: MediaMetadata;
|
||||||
};
|
};
|
||||||
get_aria2_engine_status: { args: undefined; result: EngineStatusItem };
|
get_aria2_engine_status: { args: undefined; result: EngineStatusItem };
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ describe('useDownloadStore', () => {
|
|||||||
pendingAddFilename: '',
|
pendingAddFilename: '',
|
||||||
pendingAddHeaders: '',
|
pendingAddHeaders: '',
|
||||||
pendingAddCookies: '',
|
pendingAddCookies: '',
|
||||||
|
pendingAddMediaUrls: [],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -487,7 +488,8 @@ describe('useDownloadStore', () => {
|
|||||||
silent: false,
|
silent: false,
|
||||||
filename: 'file.bin',
|
filename: 'file.bin',
|
||||||
headers: 'X-Test: value',
|
headers: 'X-Test: value',
|
||||||
cookies: 'session=secret'
|
cookies: 'session=secret',
|
||||||
|
media: false
|
||||||
});
|
});
|
||||||
|
|
||||||
const state = useDownloadStore.getState();
|
const state = useDownloadStore.getState();
|
||||||
@@ -495,53 +497,115 @@ describe('useDownloadStore', () => {
|
|||||||
expect(state.pendingAddUrls).toBe('https://example.com/file.bin');
|
expect(state.pendingAddUrls).toBe('https://example.com/file.bin');
|
||||||
expect(state.pendingAddReferer).toBe('https://example.com/page');
|
expect(state.pendingAddReferer).toBe('https://example.com/page');
|
||||||
expect(state.pendingAddFilename).toBe('file.bin');
|
expect(state.pendingAddFilename).toBe('file.bin');
|
||||||
expect(state.pendingAddHeaders).toBe('X-Test: value');
|
expect(state.pendingAddHeaders).toBe('X-Test: value');
|
||||||
expect(state.pendingAddCookies).toBe('session=secret');
|
expect(state.pendingAddCookies).toBe('session=secret');
|
||||||
});
|
expect(state.pendingAddMediaUrls).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
it('does not reuse stale extension metadata for a later single-link handoff', async () => {
|
it('does not reuse stale extension metadata for a later single-link handoff', async () => {
|
||||||
useDownloadStore.setState({
|
useDownloadStore.setState({
|
||||||
isAddModalOpen: true,
|
isAddModalOpen: true,
|
||||||
pendingAddUrls: '',
|
pendingAddUrls: '',
|
||||||
pendingAddReferer: 'https://old.example/page',
|
pendingAddReferer: 'https://old.example/page',
|
||||||
pendingAddFilename: '7aae36e6-00ec-4e7d-8dec-f14ace170bdb',
|
pendingAddFilename: '7aae36e6-00ec-4e7d-8dec-f14ace170bdb',
|
||||||
pendingAddHeaders: 'X-Old: value',
|
pendingAddHeaders: 'X-Old: value',
|
||||||
pendingAddCookies: 'old=session'
|
pendingAddCookies: 'old=session',
|
||||||
});
|
pendingAddMediaUrls: []
|
||||||
|
});
|
||||||
|
|
||||||
await useDownloadStore.getState().handleExtensionDownload({
|
await useDownloadStore.getState().handleExtensionDownload({
|
||||||
urls: ['https://github.com/center2055/OnionHop/releases/download/v3.5/OnionHop-3.5-macOS-arm64.dmg'],
|
urls: ['https://github.com/center2055/OnionHop/releases/download/v3.5/OnionHop-3.5-macOS-arm64.dmg'],
|
||||||
referer: 'https://github.com/center2055/OnionHop/releases/tag/v3.5',
|
referer: 'https://github.com/center2055/OnionHop/releases/tag/v3.5',
|
||||||
silent: false,
|
silent: false,
|
||||||
filename: null,
|
filename: null,
|
||||||
headers: 'User-Agent: Firefox Test',
|
headers: 'User-Agent: Firefox Test',
|
||||||
cookies: null
|
cookies: null,
|
||||||
});
|
media: false
|
||||||
|
});
|
||||||
|
|
||||||
const state = useDownloadStore.getState();
|
const state = useDownloadStore.getState();
|
||||||
expect(state.pendingAddUrls).toBe('https://github.com/center2055/OnionHop/releases/download/v3.5/OnionHop-3.5-macOS-arm64.dmg');
|
expect(state.pendingAddUrls).toBe('https://github.com/center2055/OnionHop/releases/download/v3.5/OnionHop-3.5-macOS-arm64.dmg');
|
||||||
expect(state.pendingAddReferer).toBe('https://github.com/center2055/OnionHop/releases/tag/v3.5');
|
expect(state.pendingAddReferer).toBe('https://github.com/center2055/OnionHop/releases/tag/v3.5');
|
||||||
expect(state.pendingAddFilename).toBe('');
|
expect(state.pendingAddFilename).toBe('');
|
||||||
expect(state.pendingAddHeaders).toBe('User-Agent: Firefox Test');
|
expect(state.pendingAddHeaders).toBe('User-Agent: Firefox Test');
|
||||||
expect(state.pendingAddCookies).toBe('');
|
expect(state.pendingAddCookies).toBe('');
|
||||||
});
|
expect(state.pendingAddMediaUrls).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
it('routes silent extension captures to the Add Modal instead of queuing immediately', async () => {
|
it('routes silent extension captures to the Add Modal instead of queuing immediately', async () => {
|
||||||
await useDownloadStore.getState().handleExtensionDownload({
|
await useDownloadStore.getState().handleExtensionDownload({
|
||||||
urls: ['https://example.com/downloads/report.pdf'],
|
urls: ['https://example.com/downloads/report.pdf'],
|
||||||
referer: 'https://example.com/page',
|
referer: 'https://example.com/page',
|
||||||
silent: true,
|
silent: true,
|
||||||
filename: 'report.pdf',
|
filename: 'report.pdf',
|
||||||
headers: 'User-Agent: Test',
|
headers: 'User-Agent: Test',
|
||||||
cookies: 'session=secret'
|
cookies: 'session=secret',
|
||||||
});
|
media: false
|
||||||
|
});
|
||||||
|
|
||||||
const state = useDownloadStore.getState();
|
const state = useDownloadStore.getState();
|
||||||
expect(state.isAddModalOpen).toBe(true);
|
expect(state.isAddModalOpen).toBe(true);
|
||||||
expect(state.pendingAddUrls).toBe('https://example.com/downloads/report.pdf');
|
expect(state.pendingAddUrls).toBe('https://example.com/downloads/report.pdf');
|
||||||
expect(state.pendingAddReferer).toBe('https://example.com/page');
|
expect(state.pendingAddReferer).toBe('https://example.com/page');
|
||||||
expect(state.pendingAddFilename).toBe('report.pdf');
|
expect(state.pendingAddFilename).toBe('report.pdf');
|
||||||
expect(state.pendingAddHeaders).toBe('User-Agent: Test');
|
expect(state.pendingAddHeaders).toBe('User-Agent: Test');
|
||||||
expect(state.pendingAddCookies).toBe('session=secret');
|
expect(state.pendingAddCookies).toBe('session=secret');
|
||||||
});
|
expect(state.pendingAddMediaUrls).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('preserves explicit extension media intent for non-allow-listed pages', async () => {
|
||||||
|
await useDownloadStore.getState().handleExtensionDownload({
|
||||||
|
urls: ['https://adult.example/watch/123'],
|
||||||
|
referer: 'https://adult.example/watch/123',
|
||||||
|
silent: false,
|
||||||
|
filename: null,
|
||||||
|
headers: 'User-Agent: Firefox Test',
|
||||||
|
cookies: 'session=secret',
|
||||||
|
media: true
|
||||||
|
});
|
||||||
|
|
||||||
|
const state = useDownloadStore.getState();
|
||||||
|
expect(state.isAddModalOpen).toBe(true);
|
||||||
|
expect(state.pendingAddUrls).toBe('https://adult.example/watch/123');
|
||||||
|
expect(state.pendingAddMediaUrls).toEqual(['https://adult.example/watch/123']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('deduplicates forced media URLs and drops stale media intent when opening fresh', async () => {
|
||||||
|
useDownloadStore.setState({
|
||||||
|
isAddModalOpen: true,
|
||||||
|
pendingAddUrls: 'https://adult.example/watch/123',
|
||||||
|
pendingAddMediaUrls: ['https://adult.example/watch/123']
|
||||||
|
});
|
||||||
|
|
||||||
|
await useDownloadStore.getState().handleExtensionDownload({
|
||||||
|
urls: ['https://adult.example/watch/123'],
|
||||||
|
referer: 'https://adult.example/watch/123',
|
||||||
|
silent: false,
|
||||||
|
filename: null,
|
||||||
|
headers: 'User-Agent: Firefox Test',
|
||||||
|
cookies: 'session=secret',
|
||||||
|
media: true
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(useDownloadStore.getState().pendingAddMediaUrls).toEqual([
|
||||||
|
'https://adult.example/watch/123'
|
||||||
|
]);
|
||||||
|
|
||||||
|
useDownloadStore.setState({
|
||||||
|
isAddModalOpen: false,
|
||||||
|
pendingAddMediaUrls: ['https://stale.example/watch']
|
||||||
|
});
|
||||||
|
|
||||||
|
await useDownloadStore.getState().handleExtensionDownload({
|
||||||
|
urls: ['https://example.com/file.bin'],
|
||||||
|
referer: 'https://example.com/page',
|
||||||
|
silent: false,
|
||||||
|
filename: 'file.bin',
|
||||||
|
headers: 'User-Agent: Firefox Test',
|
||||||
|
cookies: null,
|
||||||
|
media: false
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(useDownloadStore.getState().pendingAddMediaUrls).toEqual([]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -252,6 +252,7 @@ interface DownloadState {
|
|||||||
pendingAddFilename: string;
|
pendingAddFilename: string;
|
||||||
pendingAddHeaders: string;
|
pendingAddHeaders: string;
|
||||||
pendingAddCookies: string;
|
pendingAddCookies: string;
|
||||||
|
pendingAddMediaUrls: string[];
|
||||||
selectedPropertiesDownloadId: string | null;
|
selectedPropertiesDownloadId: string | null;
|
||||||
toggleAddModal: (isOpen: boolean) => void;
|
toggleAddModal: (isOpen: boolean) => void;
|
||||||
openAddModalWithUrls: (
|
openAddModalWithUrls: (
|
||||||
@@ -259,7 +260,8 @@ interface DownloadState {
|
|||||||
referer?: string | null,
|
referer?: string | null,
|
||||||
filename?: string | null,
|
filename?: string | null,
|
||||||
headers?: string | null,
|
headers?: string | null,
|
||||||
cookies?: string | null
|
cookies?: string | null,
|
||||||
|
media?: boolean
|
||||||
) => void;
|
) => void;
|
||||||
handleExtensionDownload: (request: ExtensionDownloadRequest) => Promise<void>;
|
handleExtensionDownload: (request: ExtensionDownloadRequest) => Promise<void>;
|
||||||
deleteModalState: DeleteModalState;
|
deleteModalState: DeleteModalState;
|
||||||
@@ -383,6 +385,7 @@ export const useDownloadStore = create<DownloadState>((set, get) => ({
|
|||||||
pendingAddFilename: '',
|
pendingAddFilename: '',
|
||||||
pendingAddHeaders: '',
|
pendingAddHeaders: '',
|
||||||
pendingAddCookies: '',
|
pendingAddCookies: '',
|
||||||
|
pendingAddMediaUrls: [],
|
||||||
selectedPropertiesDownloadId: null,
|
selectedPropertiesDownloadId: null,
|
||||||
deleteModalState: { isOpen: false },
|
deleteModalState: { isOpen: false },
|
||||||
openDeleteModal: (downloadIds) => set({
|
openDeleteModal: (downloadIds) => set({
|
||||||
@@ -398,18 +401,27 @@ export const useDownloadStore = create<DownloadState>((set, get) => ({
|
|||||||
pendingAddReferer: '',
|
pendingAddReferer: '',
|
||||||
pendingAddFilename: '',
|
pendingAddFilename: '',
|
||||||
pendingAddHeaders: '',
|
pendingAddHeaders: '',
|
||||||
pendingAddCookies: ''
|
pendingAddCookies: '',
|
||||||
|
pendingAddMediaUrls: []
|
||||||
}),
|
}),
|
||||||
openAddModalWithUrls: (urls, referer, filename, headers, cookies) => set((state) => {
|
openAddModalWithUrls: (urls, referer, filename, headers, cookies, media = false) => set((state) => {
|
||||||
const existingUrls = state.isAddModalOpen && state.pendingAddUrls ? state.pendingAddUrls : '';
|
const existingUrls = state.isAddModalOpen && state.pendingAddUrls ? state.pendingAddUrls : '';
|
||||||
const mergedUrls = existingUrls ? `${existingUrls}\n${urls}` : urls;
|
const mergedUrls = existingUrls ? `${existingUrls}\n${urls}` : urls;
|
||||||
|
const existingMediaUrls = state.isAddModalOpen ? state.pendingAddMediaUrls : [];
|
||||||
|
const pendingAddMediaUrls = media
|
||||||
|
? [...new Set([
|
||||||
|
...existingMediaUrls,
|
||||||
|
...urls.split('\n').map(url => url.trim()).filter(Boolean)
|
||||||
|
])]
|
||||||
|
: existingMediaUrls;
|
||||||
return {
|
return {
|
||||||
isAddModalOpen: true,
|
isAddModalOpen: true,
|
||||||
pendingAddUrls: mergedUrls,
|
pendingAddUrls: mergedUrls,
|
||||||
pendingAddReferer: referer?.trim() || '',
|
pendingAddReferer: referer?.trim() || '',
|
||||||
pendingAddFilename: filename?.trim() || '',
|
pendingAddFilename: filename?.trim() || '',
|
||||||
pendingAddHeaders: headers?.trim() || '',
|
pendingAddHeaders: headers?.trim() || '',
|
||||||
pendingAddCookies: cookies?.trim() || ''
|
pendingAddCookies: cookies?.trim() || '',
|
||||||
|
pendingAddMediaUrls
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
handleExtensionDownload: async (request) => {
|
handleExtensionDownload: async (request) => {
|
||||||
@@ -421,7 +433,8 @@ export const useDownloadStore = create<DownloadState>((set, get) => ({
|
|||||||
request.referer,
|
request.referer,
|
||||||
urls.length === 1 ? request.filename : null,
|
urls.length === 1 ? request.filename : null,
|
||||||
request.headers,
|
request.headers,
|
||||||
request.cookies
|
request.cookies,
|
||||||
|
request.media === true
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
setSelectedPropertiesDownloadId: (id) => set({ selectedPropertiesDownloadId: id }),
|
setSelectedPropertiesDownloadId: (id) => set({ selectedPropertiesDownloadId: id }),
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ describe('add download metadata workflow', () => {
|
|||||||
'https://example.com/file.zip\nhttps://example.com/new.zip',
|
'https://example.com/file.zip\nhttps://example.com/new.zip',
|
||||||
[existing],
|
[existing],
|
||||||
undefined,
|
undefined,
|
||||||
|
new Set(),
|
||||||
() => `new-${nextId++}`
|
() => `new-${nextId++}`
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -47,12 +48,55 @@ describe('add download metadata workflow', () => {
|
|||||||
'https://example.com/a\nhttps://example.com/a\nfile:///tmp/private\nnot-a-url',
|
'https://example.com/a\nhttps://example.com/a\nfile:///tmp/private\nnot-a-url',
|
||||||
[],
|
[],
|
||||||
undefined,
|
undefined,
|
||||||
|
new Set(),
|
||||||
() => `row-${nextId++}`
|
() => `row-${nextId++}`
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(rows.map(item => item.status)).toEqual(['loading', 'invalid', 'invalid']);
|
expect(rows.map(item => item.status)).toEqual(['loading', 'invalid', 'invalid']);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('forces explicit extension media fetches through media metadata for any http page', () => {
|
||||||
|
const rows = reconcileDownloadRows(
|
||||||
|
'https://adult.example/watch/123',
|
||||||
|
[],
|
||||||
|
undefined,
|
||||||
|
new Set(['https://adult.example/watch/123'])
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(rows[0]).toMatchObject({
|
||||||
|
sourceUrl: 'https://adult.example/watch/123',
|
||||||
|
isMedia: true,
|
||||||
|
status: 'loading'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('upgrades an existing normal row when the user explicitly fetches it as media', () => {
|
||||||
|
const existing = row({
|
||||||
|
sourceUrl: 'https://adult.example/watch/123',
|
||||||
|
downloadUrl: 'https://adult.example/watch/123',
|
||||||
|
file: '123',
|
||||||
|
status: 'ready',
|
||||||
|
generation: 2,
|
||||||
|
isMedia: false
|
||||||
|
});
|
||||||
|
|
||||||
|
const rows = reconcileDownloadRows(
|
||||||
|
'https://adult.example/watch/123',
|
||||||
|
[existing],
|
||||||
|
undefined,
|
||||||
|
new Set(['https://adult.example/watch/123'])
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(rows[0]).toMatchObject({
|
||||||
|
sourceUrl: 'https://adult.example/watch/123',
|
||||||
|
isMedia: true,
|
||||||
|
status: 'loading',
|
||||||
|
generation: 3,
|
||||||
|
formats: undefined,
|
||||||
|
selectedFormat: undefined
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('refreshes only failed metadata and preserves successful format selection', () => {
|
it('refreshes only failed metadata and preserves successful format selection', () => {
|
||||||
const ready = row({
|
const ready = row({
|
||||||
id: 'ready',
|
id: 'ready',
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ export const reconcileDownloadRows = (
|
|||||||
rawText: string,
|
rawText: string,
|
||||||
currentRows: AddDownloadDraftRow[],
|
currentRows: AddDownloadDraftRow[],
|
||||||
pendingFilename?: string,
|
pendingFilename?: string,
|
||||||
|
forceMediaUrls: ReadonlySet<string> = new Set(),
|
||||||
createId: () => string = () => crypto.randomUUID()
|
createId: () => string = () => crypto.randomUUID()
|
||||||
): AddDownloadDraftRow[] => {
|
): AddDownloadDraftRow[] => {
|
||||||
const inputs = parseInputLines(rawText);
|
const inputs = parseInputLines(rawText);
|
||||||
@@ -78,7 +79,19 @@ export const reconcileDownloadRows = (
|
|||||||
|
|
||||||
return inputs.map(input => {
|
return inputs.map(input => {
|
||||||
const preserved = existing.get(input.sourceUrl);
|
const preserved = existing.get(input.sourceUrl);
|
||||||
if (preserved) return preserved;
|
if (preserved) {
|
||||||
|
if (input.valid && forceMediaUrls.has(input.sourceUrl) && !preserved.isMedia) {
|
||||||
|
return {
|
||||||
|
...preserved,
|
||||||
|
status: 'loading',
|
||||||
|
generation: preserved.generation + 1,
|
||||||
|
isMedia: true,
|
||||||
|
formats: undefined,
|
||||||
|
selectedFormat: undefined
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return preserved;
|
||||||
|
}
|
||||||
|
|
||||||
const fallback = canonicalizeDownloadFileName(
|
const fallback = canonicalizeDownloadFileName(
|
||||||
inputs.length === 1 && pendingFilename
|
inputs.length === 1 && pendingFilename
|
||||||
@@ -93,7 +106,7 @@ export const reconcileDownloadRows = (
|
|||||||
file: fallback,
|
file: fallback,
|
||||||
status: input.valid ? 'loading' : 'invalid',
|
status: input.valid ? 'loading' : 'invalid',
|
||||||
generation: input.valid ? 1 : 0,
|
generation: input.valid ? 1 : 0,
|
||||||
isMedia: input.valid && isMediaUrl(input.sourceUrl)
|
isMedia: input.valid && (forceMediaUrls.has(input.sourceUrl) || isMediaUrl(input.sourceUrl))
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -18,7 +18,12 @@ let MEDIA_DOMAINS = [
|
|||||||
'v.redd.it',
|
'v.redd.it',
|
||||||
'soundcloud.com',
|
'soundcloud.com',
|
||||||
'facebook.com',
|
'facebook.com',
|
||||||
'fb.watch'
|
'fb.watch',
|
||||||
|
'pornhub.com',
|
||||||
|
'redtube.com',
|
||||||
|
'xhamster.com',
|
||||||
|
'xnxx.com',
|
||||||
|
'xvideos.com'
|
||||||
];
|
];
|
||||||
|
|
||||||
const ACTIVE_DOWNLOAD_STATUSES: ReadonlySet<DownloadStatus> = new Set([
|
const ACTIVE_DOWNLOAD_STATUSES: ReadonlySet<DownloadStatus> = new Set([
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import type { MediaMetadata } from '../bindings/MediaMetadata';
|
|||||||
type FetchMediaMetadataArgs = {
|
type FetchMediaMetadataArgs = {
|
||||||
url: string;
|
url: string;
|
||||||
cookieBrowser: string | null;
|
cookieBrowser: string | null;
|
||||||
|
userAgent: string | null;
|
||||||
username: string | null;
|
username: string | null;
|
||||||
password: string | null;
|
password: string | null;
|
||||||
headers: string | null;
|
headers: string | null;
|
||||||
@@ -17,6 +18,7 @@ const metadataKey = (args: FetchMediaMetadataArgs) =>
|
|||||||
JSON.stringify([
|
JSON.stringify([
|
||||||
args.url,
|
args.url,
|
||||||
args.cookieBrowser,
|
args.cookieBrowser,
|
||||||
|
args.userAgent,
|
||||||
args.username,
|
args.username,
|
||||||
args.password,
|
args.password,
|
||||||
args.headers,
|
args.headers,
|
||||||
|
|||||||
Reference in New Issue
Block a user