Optional "opacity" per entry for visual emphasis; values below 0.4 are raised to
the readable floor so links can vary in prominence but cannot be hidden.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Stray extra quote after the milliol.com URL made the file invalid JSON,
so json_decode failed and the footer silently rendered no partner links.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Reads resources/data/partners.json (url/anchor/title/rel), cached by the file's
mtime so edits go live on the next deploy. Skips invalid/non-http entries and
renders nothing when the list is empty.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The player pings /track/watch every 30s with the CUMULATIVE position, and each
ping inserted a new row (~18 rows per viewing, 254k/day). SUM(seconds_watched)
therefore summed cumulative snapshots: a single 58-minute viewing reported as
4238 minutes (~73x), skewing TrendingController's watch-minute ranking and the
admin "total_sec"/"plays" figures.
Upsert per (session_id, episode_id) so SUM is the real watch time and COUNT is
the real play count. Adds the supporting index.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Media responses carried Laravel session cookies (Set-Cookie), which made
Cloudflare BYPASS the cache for every cover/avatar/banner. These are public
files with no user context, so the session/cookie/CSRF chain is unnecessary.
Also skips BotDetector (one analytics_bot_logs INSERT per image) and the SEO
redirect lookup.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Resolve hosts to IPs and reject private/reserved/link-local ranges (incl.
169.254 cloud metadata) instead of matching hostname text, and re-validate
every redirect hop by following them manually. Restrict curl to http(s).
vtt-proxy allowlist now requires an exact host or a real subdomain.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>