Merge pull request #386 from Nezreka/feat/static-cache-and-discover-cache-headers
Feat/static cache and discover cache headers
This commit is contained in:
commit
7bc7936371
2 changed files with 59 additions and 8 deletions
|
|
@ -171,12 +171,17 @@ app = Flask(
|
||||||
)
|
)
|
||||||
app.config['TEMPLATES_AUTO_RELOAD'] = DEV_STATIC_NO_CACHE
|
app.config['TEMPLATES_AUTO_RELOAD'] = DEV_STATIC_NO_CACHE
|
||||||
app.jinja_env.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
|
# Static assets (library.js / style.css / etc.) get aggressive browser
|
||||||
# with ETag on every load instead of Flask's default 12-hour browser
|
# caching (1 year). Safe because every static URL is bust-tagged with
|
||||||
# cache. Updates ship live without users having to clear cache.
|
# `?v=static_v` (computed once per process start — see below) so each
|
||||||
# Modern browsers still serve 304 Not Modified when the file hasn't
|
# server restart effectively invalidates every cached asset for every
|
||||||
# changed, so the cost per asset per reload is just a header round-trip.
|
# user. Within a single deploy, repeat page loads hit zero round-trips
|
||||||
app.config['SEND_FILE_MAX_AGE_DEFAULT'] = 0
|
# 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
|
# Cache-bust query string for static assets — appended to every
|
||||||
|
|
@ -306,6 +311,45 @@ def _log_slow_request(response):
|
||||||
|
|
||||||
return 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:
|
def get_current_profile_id() -> int:
|
||||||
"""Get the current profile ID from Flask g context or default to 1"""
|
"""Get the current profile ID from Flask g context or default to 1"""
|
||||||
try:
|
try:
|
||||||
|
|
@ -16099,7 +16143,9 @@ def stream_audio():
|
||||||
response = send_file(file_path, as_attachment=False, mimetype=mimetype)
|
response = send_file(file_path, as_attachment=False, mimetype=mimetype)
|
||||||
response.headers.add('Accept-Ranges', 'bytes')
|
response.headers.add('Accept-Ranges', 'bytes')
|
||||||
response.headers.add('Content-Length', str(file_size))
|
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
|
return response
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|
@ -25137,7 +25183,11 @@ def download_backup_endpoint(filename):
|
||||||
backup_path = os.path.join(os.path.dirname(db_path), filename)
|
backup_path = os.path.join(os.path.dirname(db_path), filename)
|
||||||
if not os.path.exists(backup_path):
|
if not os.path.exists(backup_path):
|
||||||
return jsonify({"success": False, "error": "Backup not found"}), 404
|
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:
|
except Exception as e:
|
||||||
return jsonify({"success": False, "error": str(e)}), 500
|
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: '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: '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: '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': [
|
'2.4.0': [
|
||||||
// --- April 26, 2026 — Search & Artists unification + reorganize queue ---
|
// --- April 26, 2026 — Search & Artists unification + reorganize queue ---
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue