fix(downloads): a conversion's commit, a stopped upload, the fast join and a clip's copy hold up
- The fast join gives each ffconcat entry its own media-only format whitelist, so a library file
can no longer be opened as a nested concat list (the list's whitelist was handed down to it).
- A file the guarded probe cannot read is a download-only file, not a guess from yt-dlp's info.
- A conversion's or an ingest's commit that raises is re-read in a fresh session before any file
is removed: a commit that landed keeps the file its row now names.
- An original that cannot be removed after conversion is logged; the upload stays ready.
- An upload whose conversion is cancelled or deleted midway is freed once nothing holds it; a
paused one is kept.
- A shared link says "preparing" only while its video's job is queued or running.
- The admin storage view counts every file on disk ("Files on disk"), not only ready ones.
- A clip's failure title, unknown failure and cancel dialog, and a clip's and an upload's delete
dialog speak of what the job made; an upload's quota refusal explains that the original is kept.
- /watch announces "preparing" as an alert and moves focus to the new Retry after an action.
This commit is contained in:
@@ -803,10 +803,10 @@ def test_ingest_rejects_over_max_jobs(db, make_user, _root, monkeypatch):
|
||||
|
||||
def test_ingest_cleans_up_placed_files_on_commit_failure(db, make_user, _root, monkeypatch):
|
||||
"""A failure AFTER storage.place_file has moved the file onto disk must roll back AND unlink the
|
||||
placed file + any (even partial) poster — otherwise the orphan is permanent (the per-user delete
|
||||
keys off the rolled-back asset.rel_path, and the GC orphan sweep skips the dot-prefixed .uploads/
|
||||
tree). Exercises the rollback/cleanup branch that IS the leak remediation, incl. the poster case
|
||||
where ensure_poster left a partial file but returned None."""
|
||||
placed file + any (even partial) poster — otherwise nothing names it (the per-user delete keys
|
||||
off the rolled-back asset.rel_path) and it sits on disk until the GC's `.uploads` sweep. Exercises
|
||||
the rollback/cleanup branch, incl. the poster case where ensure_poster left a partial file but
|
||||
returned None."""
|
||||
_probe(monkeypatch, ("h264", "aac", 320, 240, 5)) # video → the poster path runs
|
||||
u = make_user()
|
||||
|
||||
@@ -837,6 +837,28 @@ def test_ingest_cleans_up_placed_files_on_commit_failure(db, make_user, _root, m
|
||||
assert db.query(MediaAsset).count() == 0 # the flushed asset rolled back — no dangling row
|
||||
|
||||
|
||||
def test_ingest_keeps_the_file_when_its_commit_landed_but_raised(db, make_user, _root, monkeypatch):
|
||||
# The commit reached the server and lost its answer: the row names the file now, so removing it
|
||||
# as "rolled back" would leave a ready upload pointing at nothing.
|
||||
_probe(monkeypatch, ("h264", "aac", 320, 240, 5))
|
||||
monkeypatch.setattr(uploadmod.editmod, "ensure_poster", lambda *_a: None)
|
||||
u = make_user()
|
||||
real_commit = db.commit
|
||||
|
||||
def _landed_then_lost():
|
||||
real_commit()
|
||||
raise RuntimeError("connection lost after COMMIT")
|
||||
|
||||
monkeypatch.setattr(db, "commit", _landed_then_lost)
|
||||
src = _root / "clip.mp4"
|
||||
src.write_bytes(b"fake-video-bytes")
|
||||
with pytest.raises(RuntimeError):
|
||||
uploadmod.ingest_upload(db, u.id, filename="clip.mp4", src_path=src, size=16)
|
||||
|
||||
(asset,) = db.query(MediaAsset).all()
|
||||
assert storage.abs_path(str(_root), asset.rel_path).exists()
|
||||
|
||||
|
||||
# --- classifier: the REAL probe_media parsing (stubs ffprobe one level down) ------------------
|
||||
# The ingest/route tests above stub probe_media wholesale; these exercise its OWN logic — image
|
||||
# detection, the cover-art skip, dimension/duration parsing — by faking only run_ffprobe_json, so a
|
||||
@@ -1022,22 +1044,16 @@ class TestResolveMediaFacts:
|
||||
)
|
||||
assert v is None and mediakind.derive(v, a) == mediakind.FILE
|
||||
|
||||
def test_info_fills_in_only_when_ffprobe_could_not_read_the_file(self, monkeypatch):
|
||||
# ffprobe failed entirely (None) → fall back to info so we never regress the working path.
|
||||
def test_a_file_the_guarded_probe_cannot_read_is_a_download_only_file(self, monkeypatch):
|
||||
# ffprobe (media demuxers only) failed entirely: the player and the editor cannot open it
|
||||
# either, so info's codecs must not make it an editable 'video'. Dimensions still gap-fill.
|
||||
self._ffprobe(monkeypatch, None)
|
||||
v, a, w, h, _d = probemod.resolve_media_facts(
|
||||
Path("x"), {"vcodec": "vp9", "acodec": "opus", "width": 1920, "height": 1080}
|
||||
Path("x"),
|
||||
{"vcodec": "avc1.64001f", "acodec": "mp4a.40.2", "width": 1920, "height": 1080},
|
||||
)
|
||||
assert (v, a, w, h) == ("vp9", "opus", 1920, 1080)
|
||||
|
||||
def test_info_none_codec_string_is_treated_as_absent(self, monkeypatch):
|
||||
# yt-dlp writes the literal "none" for a missing stream — it must not become a codec name.
|
||||
self._ffprobe(monkeypatch, None) # probe failed → info path, where the "none" scrub applies
|
||||
v, a, w, h, _d = probemod.resolve_media_facts(
|
||||
Path("x"), {"vcodec": "none", "acodec": "aac"}
|
||||
)
|
||||
assert (v, a) == (None, "aac")
|
||||
assert mediakind.derive(v, a) == mediakind.AUDIO
|
||||
assert (v, a, w, h) == (None, None, 1920, 1080)
|
||||
assert mediakind.derive(v, a) == mediakind.FILE
|
||||
|
||||
|
||||
# --- the phone-safe conversion (an iPhone's HEVC, a webm, an mkv) ------------------------------
|
||||
|
||||
Reference in New Issue
Block a user