Merge pull request #386 from Nezreka/feat/static-cache-and-discover-cache-headers

Feat/static cache and discover cache headers
pull/387/head
BoulderBadgeDad 4 weeks ago committed by GitHub
commit 7bc7936371
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -171,12 +171,17 @@ app = Flask(
)
app.config['TEMPLATES_AUTO_RELOAD'] = DEV_STATIC_NO_CACHE
app.jinja_env.auto_reload = DEV_STATIC_NO_CACHE
# Force static assets (library.js / style.css / etc.) to revalidate
# with ETag on every load instead of Flask's default 12-hour browser
# cache. Updates ship live without users having to clear cache.
# Modern browsers still serve 304 Not Modified when the file hasn't
# changed, so the cost per asset per reload is just a header round-trip.
app.config['SEND_FILE_MAX_AGE_DEFAULT'] = 0
# Static assets (library.js / style.css / etc.) get aggressive browser
# caching (1 year). Safe because every static URL is bust-tagged with
# `?v=static_v` (computed once per process start — see below) so each
# server restart effectively invalidates every cached asset for every
# user. Within a single deploy, repeat page loads hit zero round-trips
# on static files — was a 304 round-trip per asset under the old
# max-age=0 setting.
#
# In dev, DEV_STATIC_NO_CACHE flips this back to 0 so iterating on JS
# / CSS doesn't require a server restart between edits.
app.config['SEND_FILE_MAX_AGE_DEFAULT'] = 0 if DEV_STATIC_NO_CACHE else 31536000
# Cache-bust query string for static assets — appended to every
@ -306,6 +311,45 @@ def _log_slow_request(response):
return response
@app.after_request
def _add_discover_cache_headers(response):
"""Browser-cache discover GETs for 5 minutes.
The discover surface (hero, similar artists, recent releases, release
radar, deep cuts, etc.) returns semi-stable data that's expensive to
compute and not user-action-driven within a session. A short browser
cache eliminates redundant fetches when the user toggles between
Discover sections or navigates back.
Scope: only `/api/discover/` and `/api/discovery/` paths, only GET,
only successful 2xx responses. Any endpoint that explicitly sets
its own Cache-Control wins (we don't override).
Uses `private` not `public` because discover data is user-specific
(hero artists from your watchlist, similar artists from your taste,
etc.). `private` keeps it browser-only intermediate proxies
(corporate caching proxies, Cloudflare with cache rules, Nginx
proxy_cache) won't store one user's response and serve it to another.
"""
try:
if request.method != 'GET':
return response
if not (request.path.startswith('/api/discover/')
or request.path.startswith('/api/discovery/')):
return response
if not (200 <= response.status_code < 300):
return response
if response.headers.get('Cache-Control'):
return response
response.headers['Cache-Control'] = 'private, max-age=300'
except Exception as exc:
# Don't let a header-tagging bug turn a successful response into
# a 500 — log and ship the response without the cache header.
logger.warning(f"[discover-cache-headers] failed for {request.path}: {exc}")
return response
def get_current_profile_id() -> int:
"""Get the current profile ID from Flask g context or default to 1"""
try:
@ -16099,7 +16143,9 @@ def stream_audio():
response = send_file(file_path, as_attachment=False, mimetype=mimetype)
response.headers.add('Accept-Ranges', 'bytes')
response.headers.add('Content-Length', str(file_size))
response.headers.add('Cache-Control', 'no-cache')
# Override the default static-cache max-age — streaming media
# bypasses caching (range requests, mid-track seeks).
response.headers['Cache-Control'] = 'no-cache'
return response
except Exception as e:
@ -25137,7 +25183,11 @@ def download_backup_endpoint(filename):
backup_path = os.path.join(os.path.dirname(db_path), filename)
if not os.path.exists(backup_path):
return jsonify({"success": False, "error": "Backup not found"}), 404
return send_file(backup_path, as_attachment=True, download_name=filename)
# Override the default static-cache max-age — this is a sensitive
# DB backup, browsers should never cache it.
response = send_file(backup_path, as_attachment=True, download_name=filename)
response.headers['Cache-Control'] = 'no-store'
return response
except Exception as e:
return jsonify({"success": False, "error": str(e)}), 500

@ -3447,6 +3447,7 @@ const WHATS_NEW = {
{ title: 'Lock Down Socket.IO CORS', desc: 'socket.io was accepting websocket connections from any origin (cors=*). now defaults to same-origin only. if your websocket fails after updating, the server logs a clear warning with the rejected origin — add it to settings → security → allowed websocket origins.', page: 'settings' },
{ title: 'Faster Docker Startup — yt-dlp Pinned', desc: 'docker startup used to run `pip install -U yt-dlp` on every container start. removed that — yt-dlp is now pinned in requirements.txt so startup is fast and reproducible. tradeoff: youtube fixes ship via soulsync releases now instead of next container restart.' },
{ title: 'Settings Endpoints: Admin-Only', desc: 'the /api/settings endpoints (read, write, log-level, config-status, verify) had no auth gate — any logged-in profile could read or change service tokens, oauth secrets, api keys. now admin-only. single-admin setups (no multi-profile config) work transparently as before.', page: 'settings' },
{ title: 'Browser Caching for Static Assets + Discover Pages', desc: 'static assets (js/css/icons) now get a 1-year browser cache instead of revalidating on every page load. safe because the existing ?v=static_v cache-bust query changes every server restart, so deploys still ship live. discover pages (hero, similar artists, recent releases, deep cuts, etc.) now cache 5 minutes browser-side so toggling between sections doesn\'t re-fetch everything. faster repeat loads, fewer round-trips.', page: 'discover' },
],
'2.4.0': [
// --- April 26, 2026 — Search & Artists unification + reorganize queue ---

Loading…
Cancel
Save