fix(downloads,playback): re-encode undecodable audio and carry the media source through

Work from a parallel session in the same working tree, committed here so the
branch is not left half-written. Attribution note: authored in a concurrent
Claude session, not by the author of the preceding commit.

- DR-171: a downloaded video keeps audio the device can actually decode.
  `original` quality asked for a straight copy, so an E-AC-3/AC-3/DTS/TrueHD
  track came down untouched and the webview had nothing to play it with.
- `get_video_download_url` gains the media source, so the URL is built against
  the source actually chosen rather than the item's default.
- Device profile and repository plumbing updated to match.

Verified green as a whole: 656 Rust tests, 945 frontend tests, svelte-check clean.
This commit is contained in:
2026-08-15 23:54:00 +02:00
parent a5535f2941
commit ac4fccd499
10 changed files with 385 additions and 46 deletions
+42 -11
View File
@@ -120,22 +120,34 @@ pub fn webview_can_decode_audio(codec: &str) -> bool {
/// delegate this decision; it knows what its own renderer can decode and must
/// apply that itself.
///
/// The track that matters is the one the server will actually serve: the
/// default, or the first when none is marked. An unknown codec is left alone —
/// forcing a transcode on a guess would burn server CPU for files that play.
/// The track that matters is the one the server will actually serve (see
/// [`served_audio_codec`]). An unknown codec is left alone — forcing a transcode
/// on a guess would burn server CPU for files that play.
///
/// TRACES: UR-004 | DR-149 | UT-148
pub fn audio_forces_transcode(streams: &[(Option<&str>, bool)]) -> bool {
let served = streams
match served_audio_codec(streams) {
Some(codec) => !webview_can_decode_audio(codec),
// No audio at all, or a codec the server did not name: leave it alone.
None => false,
}
}
/// The codec of the audio track the server will actually serve, given the
/// source's audio streams as `(codec, is_default)` in source order: the default,
/// or the first when none is marked.
///
/// `None` means "nothing to judge" — no audio streams, or the server named no
/// codec for the one it would serve. Both callers of this rule treat that as
/// leave-well-alone, never as a licence to assume compatibility.
///
/// TRACES: UR-004, UR-071 | DR-149, DR-171 | UT-148, UT-166
pub fn served_audio_codec<'a>(streams: &[(Option<&'a str>, bool)]) -> Option<&'a str> {
streams
.iter()
.find(|(_, is_default)| *is_default)
.or_else(|| streams.first());
match served {
Some((Some(codec), _)) => !webview_can_decode_audio(codec),
// No audio at all, or a codec the server did not name: leave it alone.
Some((None, _)) | None => false,
}
.or_else(|| streams.first())
.and_then(|(codec, _)| *codec)
}
#[cfg(test)]
@@ -193,6 +205,25 @@ mod tests {
assert!(!audio_forces_transcode(&[(None, true)]));
}
/// The download path needs the codec itself, not just the verdict, so it can
/// tell the server what to re-encode. It picks the same track the streaming
/// verdict is formed from — one rule, one place.
///
/// TRACES: UR-071 | DR-171 | UT-166
#[test]
fn the_served_codec_is_the_one_the_verdict_is_formed_from() {
assert_eq!(
served_audio_codec(&[(Some("aac"), false), (Some("eac3"), true)]),
Some("eac3")
);
assert_eq!(
served_audio_codec(&[(Some("eac3"), false), (Some("aac"), false)]),
Some("eac3")
);
assert_eq!(served_audio_codec(&[]), None);
assert_eq!(served_audio_codec(&[(None, true)]), None);
}
#[test]
fn a_dolby_device_does_not_advertise_dolby_for_video() {
// The bug: a Motorola reporting c2.dolby.eac3.decoder direct-played