diff --git a/ACKNOWLEDGEMENTS.md b/ACKNOWLEDGEMENTS.md index d77ccd1b..c46906f3 100644 --- a/ACKNOWLEDGEMENTS.md +++ b/ACKNOWLEDGEMENTS.md @@ -31,6 +31,7 @@ MeedyaDL is built on top of many open-source projects. We are grateful to the de | Crate | Version | Licence | Description | |-------|---------|---------|-------------| +| aes-gcm | 0.10 | MIT/Apache-2.0 | AES-GCM authenticated encryption (Profile Bundle export, credential vault) | | arboard | 3.6 | MIT/Apache-2.0 | Cross-platform clipboard access | | base64 | 0.22 | MIT/Apache-2.0 | Base64 encoding/decoding (animated artwork, API payloads) | | chrono | 0.4 | MIT/Apache-2.0 | Date and time library | @@ -43,11 +44,16 @@ MeedyaDL is built on top of many open-source projects. We are grateful to the de | keyring | 3.6 | MIT/Apache-2.0 | OS keychain access | | lofty | 0.22 | MIT/Apache-2.0 | Audio metadata reading/writing (FLAC, MP3, OGG) | | log | 0.4 | MIT/Apache-2.0 | Logging facade | +| meedya-fingerprint | (git, branch=main) | MIT | Shared audio-fingerprint primitives (Chromaprint + ebur128) from [MWBMPartners/MeedyaSuite-core](https://github.com/MWBMPartners/MeedyaSuite-core) | +| meedya-lyrics | (git, branch=main) | MIT | Shared lyrics primitives (TTML parser + classifier + Lyricsfile YAML + LRC offset round-trip) from [MWBMPartners/MeedyaSuite-core](https://github.com/MWBMPartners/MeedyaSuite-core) | | mp4ameta | 0.13 | MIT/Apache-2.0 | M4A metadata reading/writing | +| pbkdf2 | 0.12 | MIT/Apache-2.0 | Password-based key derivation (Profile Bundle export passphrase) | +| rand | 0.8 | MIT/Apache-2.0 | Random number generation (salt + nonce derivation, retry jitter) | | regex | 1.12 | MIT/Apache-2.0 | Regular expression parsing | | reqwest | 0.12-0.13 | MIT/Apache-2.0 | HTTP client | | rookie | 0.5 | — | Browser cookie extraction | | roxmltree | 0.21 | MIT/Apache-2.0 | XML parsing (TTML lyrics) | +| rusqlite | 0.31 | MIT | SQLite bindings (Library Index database + Profile Bundle export manifests) | | rusty-chromaprint | 0.2 | MIT | Audio fingerprinting (AcoustID) | | sentry | 0.46 | MIT | Crash reporting SDK | | sentry-tracing | 0.46 | MIT | Sentry integration for `tracing` events | diff --git a/package-lock.json b/package-lock.json index b3e22856..021767c2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "meedyadl", - "version": "1.11.0-alpha.22", + "version": "1.11.0-alpha.25", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "meedyadl", - "version": "1.11.0-alpha.22", + "version": "1.11.0-alpha.25", "license": "MIT", "dependencies": { "@dnd-kit/core": "^6.3.1", @@ -8138,9 +8138,9 @@ } }, "node_modules/undici": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.25.0.tgz", - "integrity": "sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.28.0.tgz", + "integrity": "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==", "dev": true, "license": "MIT", "engines": { diff --git a/package.json b/package.json index 4dc6d672..1e8ffd09 100644 --- a/package.json +++ b/package.json @@ -83,6 +83,7 @@ }, "overrides": { "basic-ftp": "^5.3.1", - "fast-uri": "^3.1.1" + "fast-uri": "^3.1.1", + "undici": "^7.28.0" } } diff --git a/src-tauri/src/services/download_index/queries.rs b/src-tauri/src/services/download_index/queries.rs index bcfcb5ba..81eb2cf0 100644 --- a/src-tauri/src/services/download_index/queries.rs +++ b/src-tauri/src/services/download_index/queries.rs @@ -313,12 +313,17 @@ mod tests { use super::*; fn temp_db() -> std::path::PathBuf { + // macOS clock resolution can collide if two tests in the same + // process call `temp_db()` in the same nanosecond — observed + // CI failure on macos-14 (run 27811727783) where two tests + // landed on the same DB path, opened it in different modes, + // and one hit `SqliteFailure(Error { code: ReadOnly, + // extended_code: 1032 }, "attempt to write a readonly + // database")`. UUID v4 guarantees per-call uniqueness + // regardless of clock resolution. let dir = std::env::temp_dir().join(format!( "meedyadl_query_test_{}", - std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap() - .as_nanos() + uuid::Uuid::new_v4().simple() )); std::fs::create_dir_all(&dir).unwrap(); dir.join("test.db") diff --git a/src-tauri/src/services/download_queue.rs b/src-tauri/src/services/download_queue.rs index 21b1a99b..645523b8 100644 --- a/src-tauri/src/services/download_queue.rs +++ b/src-tauri/src/services/download_queue.rs @@ -11841,10 +11841,12 @@ async fn run_download_with_events( if should_emit { // Suppress Python traceback noise from the user- // facing activity-log feed when verbose is off - // (#660). The helper unconditionally writes to + // (#660). Same gate for ffprobe demuxing noise + // (#847). The helper unconditionally writes to // disk so support requests stay debuggable. - let is_traceback_noise = - process::is_python_traceback_noise(&clean_line); + let is_known_noise = + process::is_python_traceback_noise(&clean_line) + || process::is_ffprobe_demux_noise(&clean_line); // Phase 3.5h: humanise GAMDL "codec skip" lines // — strip "(media ID: NNN)" and the Python-repr // codec list. Idempotent + safe on non-matching @@ -11855,7 +11857,7 @@ async fn run_download_with_events( &download_id, "stdout", humanised, - verbose || !is_traceback_noise, + verbose || !is_known_noise, ); } } @@ -12035,12 +12037,15 @@ async fn run_download_with_events( set.insert(clean_line.clone()) }; if should_emit { - // Suppress Python traceback noise from the - // user-facing activity-log feed in non-verbose - // mode (#660). The helper unconditionally writes - // to disk so support requests stay debuggable. - let is_traceback_noise = - process::is_python_traceback_noise(&clean_line); + // Suppress Python traceback noise (#660) and + // recurring ffprobe demuxing-error lines (#847) + // from the user-facing activity-log feed in + // non-verbose mode. The helper unconditionally + // writes to disk so support requests stay + // debuggable. + let is_known_noise = + process::is_python_traceback_noise(&clean_line) + || process::is_ffprobe_demux_noise(&clean_line); // Phase 3.5h: humanise GAMDL "codec skip" lines // (strips "(media ID: NNN)" + Python-repr codec // list). @@ -12050,7 +12055,7 @@ async fn run_download_with_events( &download_id, "stderr", humanised, - verbose || !is_traceback_noise, + verbose || !is_known_noise, ); } } diff --git a/src-tauri/src/utils/process.rs b/src-tauri/src/utils/process.rs index 86f84397..b3587b41 100644 --- a/src-tauri/src/utils/process.rs +++ b/src-tauri/src/utils/process.rs @@ -274,6 +274,57 @@ pub fn is_python_traceback_noise(line: &str) -> bool { trimmed.chars().all(|c| c == '^') } +/// Reports whether `line` is a recurring ffprobe demuxing-error noise +/// line that should be suppressed from the user-facing activity log +/// when verbose mode is off (#847). +/// +/// During enrichment, MeedyaDL runs ffprobe several times per track +/// (codec detection, ReplayGain, mediainfo, …). Each invocation against +/// a freshly-written M4A occasionally trips an +/// "Invalid data found when processing input" warning that ffmpeg's +/// stderr emits for the partial-moov-atom case at byte 0. Downloads +/// complete fine; ffprobe falls through to a valid result on retry. +/// But the noise produces ~20 entries per album in the activity log — +/// reported in #847 as the most-aggravating recurring log noise. +/// +/// Modelled exactly on [`is_python_traceback_noise`] (#660): the +/// on-disk activity-log writer still records the line regardless so +/// the forensic record stays complete; this helper just gates the +/// per-line `activity-log` Tauri event when verbose is off. +/// +/// The match is intentionally tight on the recognisable prefix — +/// `[in#0/ @ 0x…] Error during demuxing: ` — to avoid +/// suppressing genuine ffmpeg errors that happen to share the +/// "demuxing" / "invalid data" wording. The hex pointer in the +/// bracket varies per invocation so we match by structure +/// (`[in#0/…@ 0x…]` substring) rather than literal. +#[must_use] +pub fn is_ffprobe_demux_noise(line: &str) -> bool { + let trimmed = line.trim(); + if trimmed.is_empty() { + return false; + } + // Required prefix shape — `[in#/ @ 0x]`. + // `in#0/mov,mp4,m4a,3gp,3g2,mj2` is the typical demuxer-list ffmpeg + // selects for Apple-Music-shipped M4A files, but we keep this + // generic so future demuxer-list shifts don't slip past the gate. + let Some(after_in_marker) = trimmed.strip_prefix("[in#") else { + return false; + }; + let Some(after_at_sign) = after_in_marker.split_once(" @ 0x").map(|(_, r)| r) else { + return false; + }; + let Some(after_close_bracket) = after_at_sign.split_once("] ").map(|(_, r)| r) else { + return false; + }; + // Required tail — both substrings present (order-flexible). Apple's + // ffmpeg has been stable on this exact wording since 5.x; if it + // changes upstream this gate fails open and noise resumes — which + // is the safe default vs accidentally suppressing genuine errors. + after_close_bracket.starts_with("Error during demuxing: ") + && after_close_bracket.contains("Invalid data found when processing input") +} + /// Reports whether `line` matches the Python exception summary pattern /// (e.g. `TypeError: …`, `httpx.ConnectError: Connection refused`). /// @@ -1589,6 +1640,96 @@ mod tests { )); } + // ---------------------------------------------------------- + // ffprobe demuxing-noise detector tests (#847) + // ---------------------------------------------------------- + + #[test] + fn is_ffprobe_demux_noise_recognises_canonical_apple_music_shape() { + // Verbatim from the #847 issue report — fires every track during + // enrichment. + assert!(is_ffprobe_demux_noise( + "[in#0/mov,mp4,m4a,3gp,3g2,mj2 @ 0x954c14000] Error during demuxing: Invalid data found when processing input" + )); + } + + #[test] + fn is_ffprobe_demux_noise_tolerates_leading_whitespace_and_alt_hex_widths() { + // Some ffmpeg builds prefix the line with a single space; some + // hex pointers are wider on 64-bit platforms. + assert!(is_ffprobe_demux_noise( + " [in#0/mov,mp4,m4a,3gp,3g2,mj2 @ 0x7b8c1c000] Error during demuxing: Invalid data found when processing input" + )); + assert!(is_ffprobe_demux_noise( + "[in#0/mov,mp4,m4a,3gp,3g2,mj2 @ 0xff00ff00ff00ff00] Error during demuxing: Invalid data found when processing input" + )); + } + + #[test] + fn is_ffprobe_demux_noise_recognises_alternate_demuxer_lists() { + // Future-proofing — ffmpeg's auto-selected demuxer list may + // change as more formats are added. The match keys on the + // structural `[in#/ @ 0x]` shape, not the + // exact comma-separated demuxer names. + assert!(is_ffprobe_demux_noise( + "[in#0/aac @ 0xabc123] Error during demuxing: Invalid data found when processing input" + )); + assert!(is_ffprobe_demux_noise( + "[in#1/wav,mp3 @ 0x1234] Error during demuxing: Invalid data found when processing input" + )); + } + + #[test] + fn is_ffprobe_demux_noise_rejects_genuine_ffmpeg_errors() { + // Other ffmpeg errors that happen to share words must NOT be + // suppressed — they're rare but actionable when they fire. + // Pattern requires BOTH "Error during demuxing:" prefix AND + // "Invalid data found when processing input" — so e.g. a + // muxer error or a generic "Invalid data" without the demuxing + // prefix passes through. + assert!(!is_ffprobe_demux_noise( + "[in#0/mov,mp4,m4a,3gp,3g2,mj2 @ 0x954c14000] Error muxing packet (track 2)" + )); + assert!(!is_ffprobe_demux_noise( + "[in#0/mov @ 0x1234] Could not find codec parameters for stream 0" + )); + assert!(!is_ffprobe_demux_noise( + "Invalid data found when processing input" + )); + // No `[in#…]` prefix → not the noise we're after. + assert!(!is_ffprobe_demux_noise( + "Error during demuxing: Invalid data found when processing input" + )); + } + + #[test] + fn is_ffprobe_demux_noise_rejects_unrelated_lines() { + assert!(!is_ffprobe_demux_noise("")); + assert!(!is_ffprobe_demux_noise(" ")); + assert!(!is_ffprobe_demux_noise( + "[INFO 12:34:56] [Track 1/12] Downloading \"Hello\"" + )); + assert!(!is_ffprobe_demux_noise( + "Traceback (most recent call last):" + )); + // Line that LOOKS like a bracketed prefix but uses a different + // shape (e.g. structlog's `[level HH:MM:SS]` prefix on + // GAMDL v3.0+ wrapped lines). + assert!(!is_ffprobe_demux_noise( + "[INFO 12:34:56] [in#0/mov,mp4 @ 0x1234] Error during demuxing: Invalid data found when processing input" + )); + } + + #[test] + fn is_ffprobe_demux_noise_requires_zero_x_hex_pointer_form() { + // ffmpeg always prints the pointer as `0x`; if upstream + // ever switches format the gate fails open (noise resumes) + // rather than over-suppressing. + assert!(!is_ffprobe_demux_noise( + "[in#0/mov,mp4,m4a,3gp,3g2,mj2 @ 12345] Error during demuxing: Invalid data found when processing input" + )); + } + // ---------------------------------------------------------- // Storefront-mismatch detector tests (#666) // ---------------------------------------------------------- diff --git a/src/App.tsx b/src/App.tsx index 96dd60cf..40900826 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -948,7 +948,10 @@ function App() { .getState() .addToast(`URL received from external link${codecSuffix}`, 'info'); - console.log(`Deep link received: ${url}${codec ? `, codec=${codec}` : ''}`); + // dev-only diagnostic; the user already gets the toast above + // (#951 — matches the console.debug convention in useTheme.ts + // and main.tsx, suppressed at default browser log levels) + console.debug(`Deep link received: ${url}${codec ? `, codec=${codec}` : ''}`); } catch (err) { console.error('Error in deep-link-download handler:', err); } diff --git a/src/components/download/QueueItem.tsx b/src/components/download/QueueItem.tsx index 3bb17414..487cecc3 100644 --- a/src/components/download/QueueItem.tsx +++ b/src/components/download/QueueItem.tsx @@ -685,7 +685,9 @@ function QueueItemComponent({