diff --git a/CHANGELOG.md b/CHANGELOG.md index 58c5a52..bd86e34 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/Extensions/Firefox b/Extensions/Firefox index 9b215e1..be632f3 160000 --- a/Extensions/Firefox +++ b/Extensions/Firefox @@ -1 +1 @@ -Subproject commit 9b215e1108a885a1335c5bc5513a72fec826fef2 +Subproject commit be632f368cc42e416ecdb94727d68a6d6c5bfeaf diff --git a/README.md b/README.md index 73e67a0..3a83ae8 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ **A fast, focused desktop download manager for macOS, Windows, and Linux.** - [![Version](https://img.shields.io/badge/version-1.0.1-6f42c1?style=flat-square)](https://github.com/nimbold/Firelink/releases) + [![Version](https://img.shields.io/badge/version-1.0.2-6f42c1?style=flat-square)](https://github.com/nimbold/Firelink/releases) [![macOS](https://img.shields.io/badge/macOS-111111?style=flat-square&logo=apple&logoColor=white)](#platforms) [![Windows](.github/badges/windows.svg)](#platforms) [![Linux](https://img.shields.io/badge/Linux-FCC624?style=flat-square&logo=linux&logoColor=black)](#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. diff --git a/package-lock.json b/package-lock.json index 5931759..3fa12ea 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 59f243d..ec51f89 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 4a2ca4e..8d645dd 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -1380,7 +1380,7 @@ checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" [[package]] name = "firelink" -version = "1.0.1" +version = "1.0.2" dependencies = [ "async-trait", "axum", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index dea5346..2439afc 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -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" diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 5af21a7..71a0929 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -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> { - static CACHE: OnceLock>> = 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 { 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, proxy: Option, ) -> Result { - 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); - 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( diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 0a30391..fa8fb92 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -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",