- redownload now ALWAYS rebuilds into a fresh private asset instead of ever reusing a shared cached file: the cache sig is salted with job_id + current asset_id, so the fork is distinct from both the dedup pool and the job's own current asset. This resolves the co-held silent no-op (finding #1) — a shared asset is left intact for its other holders while THIS job re-fetches its own copy. Resolving the fork BEFORE releasing the old hold (and the unique salted key) also removes the IntegrityError-rollback-after-file-delete window (finding #2). - format_sig gains an optional `salt` for that fork identity. - titles: don't consume the space after `|`, so a "stats | #hashtags" remainder keeps the leading whitespace _TRAILING_HASHTAGS needs — and fall back to the prefix+hashtag-stripped text, so neither the stats nor the SEO hashtags are resurrected when a title empties (#3). - worker: early-fill asset.title falls back to the video id so a title that cleaned to empty doesn't show a blank card while downloading (#5). - Tests for the salt fork and the hashtag-remainder case. (Finding #4 — quota on re-download — intentionally not changed: a re-download reuses the existing job and re-fetches ~the same bytes, so calling check_enqueue would wrongly reject on the job-count cap while footprint is unchanged.)
79 lines
3.2 KiB
Python
79 lines
3.2 KiB
Python
"""ASCII-slug naming (no spaces, no accents) for on-disk paths and served filenames, and the
|
|
social engagement-prefix strip in title normalization."""
|
|
from app.downloads import storage
|
|
from app.titles import normalize_title
|
|
|
|
|
|
def _is_ascii_slug(s: str) -> bool:
|
|
return s.isascii() and " " not in s
|
|
|
|
|
|
def test_sanitize_folds_accents_to_ascii():
|
|
assert storage.sanitize("Barnabás Skriván") == "Barnabas_Skrivan"
|
|
assert storage.sanitize("Comedy Central Magyarország") == "Comedy_Central_Magyarorszag"
|
|
assert storage.sanitize("Marco Mobili Bútoráruház Veszprém") == "Marco_Mobili_Butoraruhaz_Veszprem"
|
|
# Hungarian double-acute (ő/ű) and German ß.
|
|
assert storage.sanitize("Előző Műsor") == "Elozo_Musor"
|
|
assert storage.sanitize("Straße") == "Strasse"
|
|
|
|
|
|
def test_sanitize_output_is_pure_ascii_without_spaces():
|
|
for name in ("Poénbomba", "Mágáné Zsuzsanna", "café déjà vu", "naïve œuvre"):
|
|
out = storage.sanitize(name)
|
|
assert _is_ascii_slug(out), out
|
|
|
|
|
|
def test_sanitize_non_latin_falls_back_not_crashes():
|
|
# A title with no ASCII base collapses to the untitled fallback rather than an empty name.
|
|
assert storage.sanitize("日本語") == "untitled"
|
|
|
|
|
|
def test_display_filename_uses_underscores_and_ascii():
|
|
out = storage.display_filename("Amikor a férfi éhesen ér haza")
|
|
assert out == "Amikor_a_ferfi_ehesen_er_haza"
|
|
assert _is_ascii_slug(out)
|
|
|
|
|
|
def test_download_filename_appends_single_extension():
|
|
from pathlib import Path
|
|
|
|
assert storage.download_filename("Szép Videó", "mp4", Path("x.mp4")) == "Szep_Video.mp4"
|
|
|
|
|
|
def test_rel_path_is_ascii_everywhere():
|
|
meta = storage.MediaMeta(
|
|
video_id="abc123", title="Áristom Ödön", uploader="Hajdú Balázs", upload_date=None
|
|
)
|
|
rel = storage.rel_path(meta, "mp4", "plex", asset_id=7)
|
|
assert _is_ascii_slug(rel.replace("/", ""))
|
|
assert "Hajdu_Balazs" in rel and "Aristom_Odon" in rel
|
|
|
|
|
|
def test_engagement_prefix_stripped():
|
|
assert normalize_title(
|
|
"1.6M views · 66K reactions | Amikor a férfi éhesen ér haza"
|
|
) == "Amikor a férfi éhesen ér haza"
|
|
assert normalize_title("13M views · 162K reactions |Valami cím") == "Valami cím"
|
|
assert normalize_title("500 views | Rövid") == "Rövid"
|
|
|
|
|
|
def test_engagement_prefix_does_not_touch_normal_titles():
|
|
# A pipe that isn't an engagement prefix must survive.
|
|
assert normalize_title("Vlog 12 | A nagy nap") == "Vlog 12 | A nagy nap"
|
|
assert normalize_title("How I built a boat") == "How I built a boat"
|
|
|
|
|
|
def test_engagement_only_title_does_not_resurrect_the_stats():
|
|
# A title that is ONLY the stats prefix must not fall back to the raw (stats-bearing) string.
|
|
out = normalize_title("1.6M views · 66K reactions |")
|
|
assert out == ""
|
|
assert "views" not in (out or "") and "reactions" not in (out or "")
|
|
|
|
|
|
def test_prefix_plus_only_hashtags_resurrects_neither_stats_nor_hashtags():
|
|
# stats prefix + trailing SEO hashtags → both are removed; the fallback must not bring back the
|
|
# hashtags that rule 2 strips (nor the stats prefix).
|
|
out = normalize_title("5 views | #foo #bar")
|
|
assert out == ""
|
|
assert "#foo" not in (out or "") and "views" not in (out or "")
|