Never serve a partial file: it makes Jellyfin transcode

The 12s first-byte grace was the wrong trade and a real play found it
within the hour. A 2-hour upload took 3 minutes to start, played 6
seconds, and stalled. Jellyfin had run ffmpeg with -probesize 1G against
the growing stream and then transcoded to HLS with libx264.

The cause is the container. A fragmented MP4 with empty_moov has no
duration in its header, so the only way to get one is to sum every
fragment -- probing a growing file reads all of it. Jellyfin cannot
establish duration, codec or bitrate, so it abandons direct play and
transcodes a stream it also cannot seek. It was targeting 4.83 Mbps
against a source measured at 3.29: re-encoding a stream that already fit,
because it could not measure it.

The same video once complete reports SupportsDirectPlay with the exact
runtime and bitrate.

So FIRST_BYTE_GRACE defaults to infinite again, with --wait-timeout raised
to 600s for a 2-hour upload. A cold long video is slow to start, which is
accepted: the fetch outlives the request so a retry is instant, and a
retryable stall beats a transcode that wastes a gigabyte and cannot work.
Both failure modes are recorded at the constant in the order measured so
the 12s cap is not reintroduced.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Claude
2026-08-13 11:32:12 +01:00
parent c12837cbb9
commit 22d8828080
10 changed files with 514 additions and 46 deletions
+52
View File
@@ -310,3 +310,55 @@ def test_a_renamed_episode_removes_its_old_files(conn, settings, media_root,
assert not old.exists()
assert (media_root / second["rel_path"]).exists()
assert len(list(media_root.rglob("*.strm"))) == 1
# ------------------------------------- pruning channels with nothing to show
#
# §5: a channel with nothing inside the window must not leave an empty series in
# Jellyfin. runner creates directories lazily to honour that, but
# channels.subscribe() mkdirs to write tvshow.nfo and a poster, so every
# subscription got one regardless. Measured after approving all 119 real
# subscriptions: 57 empty series.
def test_prune_removes_a_channel_with_no_episodes(conn, media_root, channel):
tree = media_root / "clabretro"
tree.mkdir()
(tree / "tvshow.nfo").write_text("<tvshow/>")
(tree / "poster.jpg").write_bytes(b"x" * 100)
assert strm.prune_if_no_episodes(channel) is True
assert not tree.exists()
def test_prune_leaves_a_channel_that_has_episodes(conn, settings, media_root,
channel, video, no_thumbs):
strm.materialise(conn, settings, channel, video)
assert strm.prune_if_no_episodes(channel) is False
assert (media_root / "clabretro").exists()
def test_prune_finds_episodes_in_any_season(conn, media_root, channel):
"""The .strm lives a directory down, so a shallow check would delete it."""
deep = media_root / "clabretro" / "Season 2019"
deep.mkdir(parents=True)
(deep / "clabretro - S2019E1010 - Old [dQw4w9WgXcQ].strm").write_text("url")
assert strm.prune_if_no_episodes(channel) is False
assert deep.exists()
def test_prune_is_a_no_op_when_there_is_no_directory(conn, channel):
assert strm.prune_if_no_episodes(channel) is False
def test_prune_refuses_a_blank_channel_directory(conn, media_root):
from conftest import add_channel
row = add_channel(conn, "UC" + "r" * 22, "Blank", "")
keep = media_root / "keep"
keep.mkdir()
assert strm.prune_if_no_episodes(row) is False
assert keep.exists()