mirror of
https://github.com/nimbold/Firelink.git
synced 2026-07-26 12:08:27 +00:00
chore(release): prepare 1.0.2
Remove reduced yt-dlp info-json reuse so metadata fallback downloads re-extract from the original URL instead of crashing on incomplete extractor metadata. Update README and changelog for the 1.0.2 desktop release and point the bundled extension submodule at Firelink Companion 2.0.2.
This commit is contained in:
@@ -5,6 +5,26 @@ All notable changes to Firelink will be documented in this file.
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [1.0.2] - 2026-07-08
|
||||
|
||||
### New
|
||||
- Add explicit **Fetch media** actions for Firelink Companion so video and audio pages can be sent to Firelink from the extension popup or page context menu.
|
||||
- Add a Chromium extension install path in Firelink's Integrations settings and release documentation, addressing the Chromium support request in [#4](https://github.com/nimbold/Firelink/issues/4).
|
||||
|
||||
### Improved
|
||||
- Improve YouTube and social-media download handling with better proxy support, cleaner diagnostics, safer log redaction, and more reliable metadata fetching based on reports in [#5](https://github.com/nimbold/Firelink/issues/5) and [#8](https://github.com/nimbold/Firelink/issues/8).
|
||||
- Let category subfolders be disabled so files can save directly into a base download folder, addressing [#6](https://github.com/nimbold/Firelink/issues/6).
|
||||
- Refresh bundled media engines and release tooling for the current cross-platform desktop builds.
|
||||
- Strengthen release packaging checks for macOS, Windows, and Linux, including macOS ad-hoc signing verification and Windows installer icon handling.
|
||||
|
||||
### Fixed
|
||||
- Fix YouTube downloads that failed when Chrome's cookie database could not be copied by retrying public media without browser cookies, addressing [#7](https://github.com/nimbold/Firelink/issues/7).
|
||||
- Fix the hidden-sidebar trap on Settings and other pages so the sidebar can always be shown again, addressing [#9](https://github.com/nimbold/Firelink/issues/9).
|
||||
- Fix Windows proxy detection so system proxy schemes are preserved for metadata and media requests, addressing [#5](https://github.com/nimbold/Firelink/issues/5).
|
||||
- Fix captured and auto-captured browser links so they always route through Firelink's Add window before reaching the download list.
|
||||
- Fix media downloads after a metadata fallback so pages without selectable preview formats can still let yt-dlp choose the best downloadable file instead of crashing.
|
||||
- Fix media cleanup and retry paths so failed, canceled, or retried media downloads leave fewer stale temporary files behind.
|
||||
|
||||
## [1.0.1] - 2026-07-04
|
||||
|
||||
### Fixed
|
||||
|
||||
+1
-1
Submodule Extensions/Firefox updated: 9b215e1108...be632f368c
@@ -5,7 +5,7 @@
|
||||
|
||||
**A fast, focused desktop download manager for macOS, Windows, and Linux.**
|
||||
|
||||
[](https://github.com/nimbold/Firelink/releases)
|
||||
[](https://github.com/nimbold/Firelink/releases)
|
||||
[](#platforms)
|
||||
[](#platforms)
|
||||
[](#platforms)
|
||||
@@ -45,7 +45,7 @@ It is now a cross-platform Rust/Tauri app with a React and TypeScript interface.
|
||||
- **Persistent queues** with pause, resume, retry, redownload, sorting, multi-select, and bulk actions.
|
||||
- **Scheduling** with start/stop windows, speed rules, and post-queue actions.
|
||||
- **File organization** with categories, default folders, per-download overrides, and reveal/trash actions.
|
||||
- **Browser handoff** through local pairing, signed requests, replay protection, and server checks.
|
||||
- **Browser handoff** through local pairing, signed requests, Add window review, replay protection, and server checks.
|
||||
- **Desktop integration** with tray controls, notifications, sounds, sleep prevention, and secure credential storage.
|
||||
- **Diagnostics** with engine health checks, structured logs, and package verification.
|
||||
|
||||
@@ -74,12 +74,14 @@ Firelink Companion sends browser links and downloads to the desktop app.
|
||||
What it adds:
|
||||
|
||||
- Automatic capture for regular browser downloads.
|
||||
- Explicit Fetch media actions from the popup and page context menu.
|
||||
- Context-menu actions for links and selected text.
|
||||
- Firefox and Chromium support.
|
||||
- Signed local requests using the token from **Settings -> Integrations**.
|
||||
- Fallback to the browser download when Firelink is closed or rejects a handoff.
|
||||
- Captured links always open Firelink's Add window before anything is added to the download list.
|
||||
|
||||
Install the extension, open Firelink, then pair it from **Settings -> Integrations**. Firefox users can install from Mozilla Add-ons. Chromium users can use the [manual load-unpacked flow](https://github.com/nimbold/Firelink-Extension#manual-chromium-installation) with `firelink-chromium.zip` from the [extension releases](https://github.com/nimbold/Firelink-Extension/releases).
|
||||
Install the extension, open Firelink, then pair it from **Settings -> Integrations**. Firefox users can install from Mozilla Add-ons. Chromium users can use the [manual load-unpacked flow](https://github.com/nimbold/Firelink-Extension#manual-chromium-installation) with `firelink-chromium.zip` from the [extension releases](https://github.com/nimbold/Firelink-Extension/releases). Firelink Companion 2.0.2 is the matching extension release for Firelink 1.0.2.
|
||||
|
||||
The extension lives in [Firelink-Extension](https://github.com/nimbold/Firelink-Extension). This repo also vendors it as the `Extensions/Firefox` submodule.
|
||||
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "firelink",
|
||||
"version": "1.0.1",
|
||||
"version": "1.0.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "firelink",
|
||||
"version": "1.0.1",
|
||||
"version": "1.0.2",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@formkit/auto-animate": "^0.9.0",
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "firelink",
|
||||
"private": true,
|
||||
"version": "1.0.1",
|
||||
"version": "1.0.2",
|
||||
"description": "A fast cross-platform desktop download manager powered by Rust, Tauri, React, aria2, and yt-dlp.",
|
||||
"license": "MIT",
|
||||
"homepage": "https://github.com/nimbold/Firelink",
|
||||
|
||||
Generated
+1
-1
@@ -1380,7 +1380,7 @@ checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
|
||||
|
||||
[[package]]
|
||||
name = "firelink"
|
||||
version = "1.0.1"
|
||||
version = "1.0.2"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"axum",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "firelink"
|
||||
version = "1.0.1"
|
||||
version = "1.0.2"
|
||||
description = "A fast cross-platform desktop download manager powered by Rust and Tauri"
|
||||
authors = ["NimBold"]
|
||||
edition = "2021"
|
||||
|
||||
+19
-175
@@ -12,34 +12,6 @@ use tauri::{Emitter, Manager};
|
||||
use tauri_plugin_deep_link::DeepLinkExt;
|
||||
use ts_rs::TS;
|
||||
|
||||
fn get_metadata_cache() -> &'static std::sync::Mutex<HashMap<String, String>> {
|
||||
static CACHE: OnceLock<std::sync::Mutex<HashMap<String, String>>> = OnceLock::new();
|
||||
CACHE.get_or_init(|| std::sync::Mutex::new(HashMap::new()))
|
||||
}
|
||||
|
||||
fn metadata_info_cache_key(
|
||||
url: &str,
|
||||
cookie_source: Option<&str>,
|
||||
username: Option<&str>,
|
||||
password: Option<&str>,
|
||||
headers: Option<&str>,
|
||||
cookies: Option<&str>,
|
||||
proxy: Option<&str>,
|
||||
user_agent: Option<&str>,
|
||||
) -> String {
|
||||
serde_json::json!([
|
||||
url,
|
||||
cookie_source.unwrap_or(""),
|
||||
username.unwrap_or(""),
|
||||
password.unwrap_or(""),
|
||||
headers.unwrap_or(""),
|
||||
cookies.unwrap_or(""),
|
||||
proxy.unwrap_or(""),
|
||||
user_agent.unwrap_or("")
|
||||
])
|
||||
.to_string()
|
||||
}
|
||||
|
||||
fn sanitize_metadata_filename(filename: &str) -> Option<String> {
|
||||
let normalized = filename.trim().replace('\\', "/");
|
||||
let basename = std::path::Path::new(&normalized)
|
||||
@@ -946,25 +918,6 @@ async fn remove_file_best_effort_with_retry(path: &std::path::Path) {
|
||||
}
|
||||
}
|
||||
|
||||
fn remove_file_best_effort_with_retry_blocking(path: &std::path::Path) {
|
||||
for attempt in 0..=5 {
|
||||
match std::fs::remove_file(path) {
|
||||
Ok(()) => return,
|
||||
Err(error) if error.kind() == std::io::ErrorKind::NotFound => return,
|
||||
Err(error) if attempt == 5 => {
|
||||
log::warn!(
|
||||
"failed to remove temporary media metadata '{}' after retries: {}",
|
||||
path.display(),
|
||||
error
|
||||
);
|
||||
}
|
||||
Err(_) => {
|
||||
std::thread::sleep(std::time::Duration::from_millis(200));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn cleanup_media_artifacts(out_path: &std::path::Path, remove_primary: bool) {
|
||||
let Some(parent) = out_path.parent() else {
|
||||
return;
|
||||
@@ -1499,17 +1452,6 @@ async fn fetch_media_metadata_uncached(
|
||||
cookies: Option<String>,
|
||||
proxy: Option<String>,
|
||||
) -> Result<MediaMetadata, String> {
|
||||
let info_cache_key = metadata_info_cache_key(
|
||||
&url,
|
||||
cookie_browser.as_deref(),
|
||||
username.as_deref(),
|
||||
password.as_deref(),
|
||||
headers.as_deref(),
|
||||
cookies.as_deref(),
|
||||
proxy.as_deref(),
|
||||
user_agent.as_deref(),
|
||||
);
|
||||
|
||||
// Pass bundled tools by absolute path so extraction never depends on
|
||||
// system Python, a user-managed PATH, or auto-detection heuristics.
|
||||
let deno_path = resolve_bundled_binary_path(&app_handle, "deno")
|
||||
@@ -1603,12 +1545,6 @@ async fn fetch_media_metadata_uncached(
|
||||
let value: serde_json::Value = serde_json::from_slice(&output.stdout)
|
||||
.map_err(|e| format!("Failed to parse JSON: {}", e))?;
|
||||
|
||||
if let Ok(mut cache) = get_metadata_cache().lock() {
|
||||
if let Ok(json_str) = String::from_utf8(output.stdout.clone()) {
|
||||
cache.insert(info_cache_key, json_str);
|
||||
}
|
||||
}
|
||||
|
||||
let title = value
|
||||
.get("title")
|
||||
.and_then(|v| v.as_str())
|
||||
@@ -2889,42 +2825,7 @@ pub(crate) async fn start_media_download_internal(
|
||||
}
|
||||
}
|
||||
|
||||
let mut temp_info_path = None;
|
||||
if let Ok(mut cache) = get_metadata_cache().lock() {
|
||||
let info_cache_key = metadata_info_cache_key(
|
||||
&url,
|
||||
effective_cookie_source.as_deref(),
|
||||
username.as_deref(),
|
||||
password.as_deref(),
|
||||
headers.as_deref(),
|
||||
cookies.as_deref(),
|
||||
proxy.as_deref(),
|
||||
user_agent.as_deref(),
|
||||
);
|
||||
if let Some(json_str) = cache.remove(&info_cache_key) {
|
||||
let temp_dir = std::env::temp_dir();
|
||||
let path = temp_dir.join(format!("firelink_ytdlp_{}.info.json", id));
|
||||
if std::fs::write(&path, json_str).is_ok() {
|
||||
temp_info_path = Some(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct CleanupPath(Option<std::path::PathBuf>);
|
||||
impl Drop for CleanupPath {
|
||||
fn drop(&mut self) {
|
||||
if let Some(path) = self.0.take() {
|
||||
remove_file_best_effort_with_retry_blocking(&path);
|
||||
}
|
||||
}
|
||||
}
|
||||
let _cleanup = CleanupPath(temp_info_path.clone());
|
||||
|
||||
if let Some(path) = temp_info_path.as_ref() {
|
||||
cmd = cmd.arg("--load-info-json").arg(path);
|
||||
} else {
|
||||
cmd = cmd.arg("--").arg(&url);
|
||||
}
|
||||
cmd = cmd.arg("--").arg(&url);
|
||||
|
||||
let (mut rx, child) = cmd
|
||||
.spawn()
|
||||
@@ -4585,11 +4486,10 @@ mod tests {
|
||||
collect_download_uris, filename_from_content_disposition,
|
||||
filename_from_url_disposition_query, filename_from_url_path, is_excluded_yt_dlp_format,
|
||||
is_browser_cookie_extraction_error, json_lower, media_metadata_cache_key,
|
||||
metadata_info_cache_key, media_output_template, media_progress_speed,
|
||||
normalize_speed_limit_for_aria2, parse_firelink_deep_link, parse_ffmpeg_version,
|
||||
parse_media_progress_line, redact_log_line, sanitize_ytdlp_config_value,
|
||||
should_cleanup_media_artifacts_after_failure, FirelinkDeepLink, MediaProgress,
|
||||
MEDIA_PROGRESS_PREFIX,
|
||||
media_output_template, media_progress_speed, normalize_speed_limit_for_aria2,
|
||||
parse_firelink_deep_link, parse_ffmpeg_version, parse_media_progress_line,
|
||||
redact_log_line, sanitize_ytdlp_config_value, should_cleanup_media_artifacts_after_failure,
|
||||
FirelinkDeepLink, MediaProgress, MEDIA_PROGRESS_PREFIX,
|
||||
};
|
||||
use serde_json::json;
|
||||
use std::time::{Duration, Instant};
|
||||
@@ -4690,76 +4590,6 @@ mod tests {
|
||||
assert_ne!(base, changed_user_agent);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn metadata_info_cache_key_includes_request_headers_and_cookies() {
|
||||
let base = 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 A"),
|
||||
);
|
||||
let changed_headers = metadata_info_cache_key(
|
||||
"https://example.com/watch?v=1",
|
||||
Some("firefox"),
|
||||
Some("user-one"),
|
||||
Some("pass-one"),
|
||||
Some("User-Agent: Browser B"),
|
||||
Some("session=one"),
|
||||
None,
|
||||
Some("Custom UA A"),
|
||||
);
|
||||
let changed_cookies = 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=two"),
|
||||
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_cookies);
|
||||
assert_ne!(base, changed_username);
|
||||
assert_ne!(base, changed_password);
|
||||
assert_ne!(base, changed_user_agent);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn retryable_media_failures_preserve_resumable_artifacts() {
|
||||
assert!(!should_cleanup_media_artifacts_after_failure(
|
||||
@@ -4995,6 +4825,20 @@ mod tests {
|
||||
assert_eq!(option.filesize_approx, Some(1_468_000_000));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn returns_no_media_options_for_webpage_only_metadata() {
|
||||
let formats = vec![json!({
|
||||
"format_id": "0",
|
||||
"ext": "html",
|
||||
"protocol": "https",
|
||||
"format": "default webpage",
|
||||
"vcodec": "none",
|
||||
"acodec": "none"
|
||||
})];
|
||||
|
||||
assert!(build_media_format_options(&formats, Some(30.0)).is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn classifies_browser_cookie_database_errors_for_fallback() {
|
||||
assert!(is_browser_cookie_extraction_error(
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "Firelink",
|
||||
"version": "1.0.1",
|
||||
"version": "1.0.2",
|
||||
"identifier": "com.nimbold.firelink",
|
||||
"build": {
|
||||
"beforeDevCommand": "node scripts/stage-engines.js && npm run dev",
|
||||
|
||||
Reference in New Issue
Block a user