Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions ACKNOWLEDGEMENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand All @@ -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 |
Expand Down
10 changes: 5 additions & 5 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@
},
"overrides": {
"basic-ftp": "^5.3.1",
"fast-uri": "^3.1.1"
"fast-uri": "^3.1.1",
"undici": "^7.28.0"
}
}
13 changes: 9 additions & 4 deletions src-tauri/src/services/download_index/queries.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
27 changes: 16 additions & 11 deletions src-tauri/src/services/download_queue.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -11855,7 +11857,7 @@ async fn run_download_with_events(
&download_id,
"stdout",
humanised,
verbose || !is_traceback_noise,
verbose || !is_known_noise,
);
}
}
Expand Down Expand Up @@ -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).
Expand All @@ -12050,7 +12055,7 @@ async fn run_download_with_events(
&download_id,
"stderr",
humanised,
verbose || !is_traceback_noise,
verbose || !is_known_noise,
);
}
}
Expand Down
141 changes: 141 additions & 0 deletions src-tauri/src/utils/process.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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/<demuxer-list> @ 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#<digit>/<demuxer-list> @ 0x<hex>]`.
// `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`).
///
Expand Down Expand Up @@ -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#<digit>/<list> @ 0x<hex>]` 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<hex>`; 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)
// ----------------------------------------------------------
Expand Down
5 changes: 4 additions & 1 deletion src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
8 changes: 6 additions & 2 deletions src/components/download/QueueItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -685,7 +685,9 @@ function QueueItemComponent({
<button
onClick={() => onCancel(item.id)}
className="p-1.5 rounded-platform text-content-tertiary hover:text-status-error hover:bg-surface-elevated transition-colors"
title="Cancel"
// a11y: title matches aria-label so sighted-hover tooltip
// and screen-reader announcement say the same thing (#950)
title="Cancel download"
aria-label="Cancel download"
>
<X size={14} />
Expand All @@ -695,7 +697,9 @@ function QueueItemComponent({
<button
onClick={() => onRetry(item.id)}
className="p-1.5 rounded-platform text-content-tertiary hover:text-content-primary hover:bg-surface-elevated transition-colors"
title="Retry"
// a11y: title matches aria-label so sighted-hover tooltip
// and screen-reader announcement say the same thing (#950)
title="Retry download"
aria-label="Retry download"
>
<RotateCcw size={14} />
Expand Down