fix(downloads): second-round review fixes (fork on re-download, title fallbacks)

- 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.)
This commit is contained in:
2026-07-22 02:48:16 +02:00
parent cae5702fbb
commit ebfddf330a
6 changed files with 53 additions and 17 deletions
+10 -5
View File
@@ -25,9 +25,12 @@ import unicodedata
_DROP_CATEGORIES = {"So", "Sk", "Cc", "Cf", "Cs", "Co", "Cn"}
# Social engagement prefix some sources (Facebook) prepend, e.g. "1.6M views · 66K reactions | ".
# Match one-or-more "<count> <label>" tokens (· / , separated) terminated by a pipe, then drop it.
# Deliberately does NOT consume the space after `|`: leaving it lets a hashtag-only remainder
# ("… | #foo #bar") keep the leading whitespace `_TRAILING_HASHTAGS` needs to strip it; the normal
# case's leading space is removed later by the whitespace collapse.
_ENGAGEMENT_PREFIX = re.compile(
r"^\s*(?:\d[\d.,]*\s*[KMB]?\s*"
r"(?:views?|reactions?|likes?|comments?|shares?|plays?|followers?)\s*[·,;]?\s*)+\|\s*",
r"(?:views?|reactions?|likes?|comments?|shares?|plays?|followers?)\s*[·,;]?\s*)+\|",
re.IGNORECASE,
)
_LETTER_RUN = re.compile(r"[^\W\d_]+", re.UNICODE)
@@ -61,6 +64,7 @@ def normalize_title(raw: str | None) -> str | None:
stripped = _ENGAGEMENT_PREFIX.sub("", t)
prefix_removed = stripped != t
t = _TRAILING_HASHTAGS.sub("", stripped)
base = t # after prefix + trailing-hashtag removal — the fallback if de-shouting empties it
multi = [r for r in _LETTER_RUN.findall(t) if len(r) >= 2]
caps = [r for r in multi if r.isupper()]
@@ -88,7 +92,8 @@ def normalize_title(raw: str | None) -> str | None:
if ch.isalpha():
t = t[:i] + ch.upper() + t[i + 1:]
break
# Nothing survived cleaning: fall back to the engagement-stripped text (never resurrect a
# stats prefix). Only use the untouched raw when no prefix was there to strip — e.g. an
# all-emoji title whose glyphs were dropped, where returning the original is the lesser evil.
return t or stripped.strip() or ("" if prefix_removed else raw)
# Nothing survived cleaning: fall back to the prefix+hashtag-stripped text (so neither the
# stats prefix nor the SEO hashtags are resurrected). Only use the untouched raw when no prefix
# was there to strip — e.g. an all-emoji title whose glyphs were dropped, where returning the
# original is the lesser evil.
return t or base.strip() or ("" if prefix_removed else raw)