feat(downloads): ASCII-slug names, universal browser playability, force re-download

Mini-epic "Downloads correctness & UX" (S1–S3), on top of the iOS-AV1 + 720p fixes.

S1 — naming & titles:
- ASCII-fold on-disk dir/file names AND the served (Content-Disposition) filename: no
  spaces (underscore-joined), no accents (á→a, ő→o, ß→ss), pure ASCII — portable and free
  of the `?` mojibake accented names show in non-UTF-8 tools. (Forward-only; existing files
  clean up via re-download.)
- Strip a leading social engagement prefix ("1.6M views · 66K reactions | …") from titles.

S2 — universal browser playability (generalizes the iOS re-encode):
- ensure_browser_playable guarantees the stored file is H.264 + AAC/MP3 in mp4 — the only
  combo every browser decodes. Re-encode just the offending stream(s) (video for AV1/VP9/HEVC,
  audio for Opus/etc), or remux when only the container is wrong. Gated on the mp4 compat
  profile (an explicit vcodec / non-mp4 custom profile opts out).

S3 — force re-download (preserves share links):
- POST /downloads/{job_id}/redownload keeps the job row so its public DownloadLink survives
  and resolves to the freshly-downloaded file; deletes the old file and re-fetches under the
  current rules. A failed re-download leaves the job in error (the intended broken-link case).
- Library "Re-download" action (icon + confirm), api method, hu/en strings.

Tests: naming/title-strip (test_naming), browser_transcode_flags matrix; existing
download_filename expectation updated for the underscore policy.
This commit is contained in:
2026-07-22 02:04:27 +02:00
parent 8fd0049dce
commit e46f215ada
12 changed files with 272 additions and 66 deletions
+20 -15
View File
@@ -398,31 +398,36 @@ def _download_settings() -> tuple[str, int]:
)
def _ensure_ios_compatible(job_id: int, produced: Path, spec: dict, info: dict) -> Path:
"""Last-ditch iOS net: if the mp4 compat profile still yielded an Apple-undecodable codec
(AV1/VP9 — happens when a source exposes no H.264 stream at all), re-encode to H.264 + AAC so
shared /watch files play on iPad. Gated on the compat profile so an explicit `vcodec` or a
non-mp4 container is respected. Returns the file to store (re-encoded, or the original), and
corrects `info`'s codec fields so the stored asset metadata matches the actual file."""
def _ensure_browser_playable(job_id: int, produced: Path, spec: dict, info: dict) -> Path:
"""Guarantee the stored file plays in EVERY browser: H.264 + AAC/MP3 in mp4. If the download
(whatever the source shipped) isn't already that, re-encode the offending stream(s) — or just
remux when the codecs are fine but the container isn't. Gated on the mp4 compat profile, so an
explicit `vcodec` or non-mp4 custom profile opts out. Returns the file to store (converted, or
the original) and corrects `info`'s codec/container fields to match what's actually on disk."""
if spec.get("container") != "mp4" or spec.get("vcodec"):
return produced
if produced.suffix.lower() != ".mp4":
return produced
vcodec = editmod.probe_vcodec(produced)
if not editmod.needs_ios_transcode(vcodec):
vcodec, acodec = editmod.probe_codecs(produced)
is_mp4 = produced.suffix.lower() == ".mp4"
needs, reenc_v, reenc_a = editmod.browser_transcode_flags(vcodec, acodec, is_mp4)
if not needs:
return produced
dest = produced.with_name(produced.stem + ".ios.mp4")
log.info("job %s: re-encoding %s→h264 for iOS compatibility", job_id, vcodec)
dest = produced.with_name(produced.stem + ".browser.mp4")
action = "re-encode" if (reenc_v or reenc_a) else "remux"
log.info("job %s: %s (v=%s a=%s mp4=%s) -> browser-safe mp4", job_id, action, vcodec, acodec, is_mp4)
_set_job(job_id, phase="processing", progress=0, speed_bps=None, eta_s=None)
editmod.run_ffmpeg(
editmod.build_h264_transcode_cmd(produced, dest),
editmod.build_browser_safe_cmd(produced, dest, reenc_v, reenc_a),
total_s=int(info["duration"]) if info.get("duration") else 0,
on_progress=lambda pct: _set_job(job_id, progress=int(pct), phase="processing"),
should_cancel=lambda: _job_status(job_id) not in ("running", None),
)
produced.unlink(missing_ok=True)
info["vcodec"], info["acodec"] = "h264", "aac" # asset metadata must match the stored file
# Keep the stored asset metadata truthful about what's now on disk.
if reenc_v:
info["vcodec"] = "h264"
if reenc_a:
info["acodec"] = "aac"
return dest
@@ -446,7 +451,7 @@ def _download(job_id: int, asset_id: int, source_kind: str, source_ref: str, spe
raise RuntimeError("yt-dlp produced no output file")
produced = cands[0]
produced = _ensure_ios_compatible(job_id, produced, spec, info)
produced = _ensure_browser_playable(job_id, produced, spec, info)
ext = produced.suffix.lstrip(".").lower()
meta = storage.MediaMeta(
video_id=info.get("id") or source_ref,