Files
siftlode/backend/tests/test_formats.py
T
peter e795d191c5 fix(boot): review round 1 — reconcile the tab pin, one loading clock, honest probe failures
Deep-review round 1 over main..improvement/watch-background-audio.

BootGate
- The tab's account pin is corrected to the account the server answered as.
  The server honours the pin only for an account still signed in to this
  browser and otherwise answers as the default, so a tab pinned to an account
  signed out in another tab ran as the default on the server while reading
  and writing the gone account's local state.
- A mounted tab whose identity changes (its session ended, or the case above)
  reloads instead of rebuilding the tree in place, like every account switch;
  that also drops the previous account's query cache. The account key on the
  tree goes: the readyFor gate already unmounted it on every change.
- An unanswered setup probe shows the error with Retry, like an unanswered
  account probe, instead of the signed-out landing to a signed-in user.
- Comments: /api/me never 401s; the ended-session path is identity → nobody.

Loading states
- LoadingState's reveal delay is one clock across loading states that hand
  over to each other (boot screen → page-chunk fallback → the page's own
  fetching state), instead of a CSS delay restarted by each, which blanked a
  visible spinner for another 300 ms at every hand-off. BootScreen is now the
  shared loading visual, full screen, counting from navigation start.

Plex shared link
- Persists only the dimensions the link carries again. Writing the default
  filters for an omitted `pf` created the stored-filters key, which is what
  tells SavedViewsProvider the account picked filters, so the account's
  default Plex saved view was never applied again.

Watch page
- The Repeat toggle uses the shared pressed-pill paint (a solid fill when on)
  instead of Button variants that differ only in colour, and its flag goes
  through storage.ts readJSON/writeJSON.
- The actions wrap under the title when the row is too narrow, instead of
  squeezing the title into a column a word wide on a phone; the title breaks
  long words.

Comments that still justified code by the removed adopt effects (usePref,
PrefsProvider, FeedViewProvider) are rewritten against the seed-once model.

Tests: BootGate error/retry for both probes, setup mode, a failed background
refetch keeping the app, the pin correction and the reload on an identity
change; the shared reveal clock; the Plex link persist; the audio signature
actually moved off version 4; a video download (av and v) still remuxed to
an mp4. Each mutation-checked RED for its own reason.
2026-09-15 00:53:15 +02:00

182 lines
8.6 KiB
Python

"""build_ydl_opts: EVERY download is clamped to the mp4/H.264 compat profile (E7 browser-
playability guarantee) and must SELECT H.264 (not merely prefer it), so shared downloads play in
every browser incl. iOS/Safari. Non-mp4 containers and non-H.264 vcodecs are coerced, not honoured.
See the iPad AV1 broken-player fix + E7 format narrowing."""
from app.downloads import edit as editmod
from app.downloads import formats
def _opts(spec):
return formats.build_ydl_opts(spec, "/tmp/%(id)s.%(ext)s", lambda d: None)
def test_adapter_extra_opts_are_merged_in():
# The SourceAdapter contributes source-specific opts (e.g. YouTube's extractor_args) via `extra`.
extra = {"extractor_args": {"youtube": {"player_client": ["web"]}}}
opts = formats.build_ydl_opts({}, "/tmp/%(id)s.%(ext)s", lambda d: None, extra=extra)
assert opts["extractor_args"]["youtube"]["player_client"] == ["web"]
# No extra → no extractor_args (a generic source contributes nothing).
assert "extractor_args" not in formats.build_ydl_opts({}, "/tmp/x", lambda d: None)
def test_mp4_default_selects_avc1_with_progressive_fallback():
fmt = _opts({"mode": "av", "container": "mp4"})["format"]
# avc1 video-only first, then the avc1 progressive, then an unrestricted fallback.
assert fmt.startswith("bestvideo[vcodec^=avc1]+bestaudio[acodec^=mp4a]")
assert "/best[vcodec^=avc1]" in fmt
assert fmt.endswith("bestvideo+bestaudio/best")
def test_format_sig_salt_forks_a_distinct_identity():
spec = {"mode": "av", "container": "mp4", "max_height": 720}
base = formats.format_sig(spec)
# A salt yields a distinct cache identity (force-redownload's private-asset fork); different
# salts differ from each other and from the unsalted sig, same salt is stable.
assert formats.format_sig(spec, salt="7:12") != base
assert formats.format_sig(spec, salt="7:12") != formats.format_sig(spec, salt="7:13")
assert formats.format_sig(spec, salt="7:12") == formats.format_sig(spec, salt="7:12")
def test_mp4_default_keeps_h264_sort_tiebreaker():
sort = _opts({"mode": "av", "container": "mp4"})["format_sort"]
assert "vcodec:h264" in sort and "acodec:aac" in sort
def test_video_only_mp4_selects_avc1():
fmt = _opts({"mode": "v", "container": "mp4"})["format"]
assert fmt.startswith("bestvideo[vcodec^=avc1]")
assert "+bestaudio" not in fmt # video-only never merges audio
# A video-only download stays audio-free: every `bestvideo` (video-only) option is preferred
# over any `best` (progressive, audio-bearing) fallback.
assert fmt.index("bestvideo") < fmt.index("best[vcodec^=avc1]") < fmt.rindex("/best")
def test_max_height_caps_every_branch():
fmt = _opts({"mode": "av", "container": "mp4", "max_height": 720})["format"]
assert fmt.count("[height<=?720]") >= 3 # applied to each avc1/fallback branch
def test_explicit_vcodec_is_clamped_to_the_compat_selector():
# E7 browser-playability guarantee: a non-H.264 vcodec no longer opts out — normalize() coerces
# vcodec→None, so the avc1 selector (and mp4 merge) is used regardless of what was requested.
opts = _opts({"mode": "av", "container": "mp4", "vcodec": "vp9"})
assert opts["format"].startswith("bestvideo[vcodec^=avc1]")
assert opts["merge_output_format"] == "mp4"
assert "vcodec:vp9" not in opts["format_sort"]
def test_non_mp4_container_is_clamped_to_mp4():
# A webm/mkv request is coerced to the mp4/H.264 compat profile (nothing plays mkv in-browser;
# webm/VP9 fails iOS), so the avc1 selector is used and the merge target is mp4.
opts = _opts({"mode": "av", "container": "webm"})
assert opts["format"].startswith("bestvideo[vcodec^=avc1]")
assert opts["merge_output_format"] == "mp4"
def test_normalize_clamps_to_browser_playable():
n = formats.normalize({"container": "mkv", "vcodec": "av1", "audio_format": "opus"})
assert n["container"] == "mp4"
assert n["vcodec"] is None
assert n["audio_format"] == "m4a"
# A playable audio choice is preserved.
assert formats.normalize({"audio_format": "mp3"})["audio_format"] == "mp3"
def test_non_playable_specs_dedup_to_the_compat_identity():
# Two requests that differ only in (now-clamped) non-playable fields share ONE cache asset.
a = formats.format_sig({"mode": "av", "max_height": 720, "container": "webm", "vcodec": "av1"})
b = formats.format_sig({"mode": "av", "max_height": 720, "container": "mp4"})
assert a == b
def test_audio_only_unaffected():
assert _opts({"mode": "a", "audio_format": "m4a"})["format"] == "bestaudio/best"
def test_browser_transcode_flags_universal_playability():
F = editmod.browser_transcode_flags
# Already universal: H.264 + AAC in mp4 → no change.
assert F("h264", "aac", True) == (False, False, False)
assert F("h264", "mp3", True) == (False, False, False)
# Non-H.264 video → full video re-encode (AV1/VP9 fail iOS; HEVC fails Chrome/FF).
assert F("av1", "aac", True) == (True, True, False)
assert F("vp9", "opus", True) == (True, True, True)
assert F("hevc", "aac", True) == (True, True, False)
# H.264 but bad audio → audio-only transcode (video copied).
assert F("h264", "opus", True) == (True, False, True)
# Codecs fine but wrong container → remux only (no re-encode).
assert F("h264", "aac", False) == (True, False, False)
# A failed probe / missing stream is never re-encoded.
assert F(None, None, True) == (False, False, False)
# --- SEC-13 · one enqueue is one bounded download -------------------------------------------
def test_a_playlist_url_can_only_yield_one_item():
# `noplaylist` alone only demotes a video-inside-a-playlist URL to that video; a URL that is
# purely a playlist or channel has no such video, so without this it expands into every entry.
opts = _opts({})
assert opts["noplaylist"] is True
assert opts["playlist_items"] == "1"
def test_the_remaining_quota_becomes_a_hard_size_ceiling():
opts = formats.build_ydl_opts({}, "/tmp/x", lambda d: None, max_bytes=5_000_000)
assert opts["max_filesize"] == 5_000_000
def test_no_budget_means_no_size_filter():
# Unlimited users pass None; a non-positive remainder is the quota layer's refusal to make at
# enqueue, not an impossible yt-dlp filter to express here.
for budget in (None, 0, -1):
assert "max_filesize" not in formats.build_ydl_opts(
{}, "/tmp/x", lambda d: None, max_bytes=budget
)
def test_a_source_adapter_cannot_raise_the_size_ceiling():
# The adapter merge happens after max_filesize is set, so this pins that no adapter option can
# widen a storage cap — the mirror of the cookiefile rule, in the opposite direction.
opts = formats.build_ydl_opts(
{}, "/tmp/x", lambda d: None, extra={"max_filesize": 999_999_999}, max_bytes=5_000_000
)
assert opts["max_filesize"] == 5_000_000
def test_a_join_cut_list_is_capped():
# An accurate join emits two filters per segment into one ffmpeg filter_complex, so an unbounded
# list is an unbounded command line. The surplus is dropped; the legitimate head survives.
segs = [{"start_s": i, "end_s": i + 1} for i in range(500)]
out = editmod.normalize_edit_spec({"segments": segs})
assert len(out["segments"]) == editmod._MAX_SEGMENTS
assert out["segments"][0] == {"start_s": 0.0, "end_s": 1.0}
def test_a_normal_cut_list_is_untouched():
segs = [{"start_s": 0, "end_s": 5}, {"start_s": 10, "end_s": 12}]
out = editmod.normalize_edit_spec({"segments": segs})
assert len(out["segments"]) == 2
def test_audio_versions_its_cache_identity_apart_from_video(monkeypatch):
# The .mp3/.m4a-stays-itself fix must re-derive cached remuxed audio WITHOUT re-downloading every
# cached video: bumping one version moves only its own mode's signature.
audio, video = {"mode": "a", "audio_format": "mp3"}, {"mode": "av"}
before = formats.format_sig(audio), formats.format_sig(video)
monkeypatch.setattr(formats, "_AUDIO_SIG_VERSION", formats._AUDIO_SIG_VERSION + 1)
assert formats.format_sig(audio) != before[0]
assert formats.format_sig(video) == before[1]
monkeypatch.setattr(formats, "_SIG_VERSION", formats._SIG_VERSION + 1)
assert formats.format_sig(video) != before[1]
def test_the_mp3_fix_moved_the_audio_cache_identity(monkeypatch):
# Before the fix every mode hashed with version 4, so an audio signature still at 4 would find the
# cached mp3-remuxed-into-mp4 assets iPad refuses (prod job 268) instead of re-deriving them.
audio = {"mode": "a", "audio_format": "mp3"}
current = formats.format_sig(audio)
monkeypatch.setattr(formats, "_AUDIO_SIG_VERSION", 4)
assert formats.format_sig(audio) != current