From 08725094db1fca263f34f53f80f79605fa22ac60 Mon Sep 17 00:00:00 2001 From: Broque Thomas <26755000+Nezreka@users.noreply.github.com> Date: Fri, 15 May 2026 21:06:17 -0700 Subject: [PATCH] get_current_profile_id: catch RuntimeError so background callers don't crash MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reproduced on the personalized playlist pipeline: selecting Fresh Tape (or any kind) and running the automation surfaced "Working outside of application context" in the UI. Root cause: `get_current_profile_id` reads Flask's `g.profile_id` and only catches `AttributeError`. Outside a request — automation engine, sync threads, watchlist scanner — `g` raises `RuntimeError` instead, so the except misses and the handler dies. Mirrored playlist pipeline never hit this because it hardcodes profile_id=1 in its sync call. The personalized pipeline calls `deps.get_current_profile_id()` from a background thread, which is what tripped the bug. Fresh Tape's generator also resolves the profile via the same function — same path, same crash. Fix: broaden the except to `(AttributeError, RuntimeError)` in all three copies of the helper (`web_server.py`, `core/artists/map.py`, `core/discovery/hero.py`). All three now safely degrade to profile_id=1 (admin profile) when called outside a request context — matches the existing intent that single-admin installs Just Work. No test changes — the existing pipeline tests stub the helper, so they never exercised the bug. The fix is in the layer above the stubs. --- core/artists/map.py | 8 ++++++-- core/discovery/hero.py | 8 ++++++-- web_server.py | 10 ++++++++-- 3 files changed, 20 insertions(+), 6 deletions(-) diff --git a/core/artists/map.py b/core/artists/map.py index 5e6f130a..c2ceb0c5 100644 --- a/core/artists/map.py +++ b/core/artists/map.py @@ -20,10 +20,14 @@ logger = logging.getLogger(__name__) def get_current_profile_id() -> int: - """Mirror of web_server.get_current_profile_id — uses Flask g.""" + """Mirror of web_server.get_current_profile_id — uses Flask g. + + Catches RuntimeError too because reading `g` outside a request + context raises that (not AttributeError) — happens when this is + called from background threads (sync, automation, scanners).""" try: return g.profile_id - except AttributeError: + except (AttributeError, RuntimeError): return 1 diff --git a/core/discovery/hero.py b/core/discovery/hero.py index 50800ecb..41143924 100644 --- a/core/discovery/hero.py +++ b/core/discovery/hero.py @@ -17,10 +17,14 @@ logger = logging.getLogger(__name__) def get_current_profile_id() -> int: - """Mirror of web_server.get_current_profile_id — uses Flask g.""" + """Mirror of web_server.get_current_profile_id — uses Flask g. + + Catches RuntimeError too because reading `g` outside a request + context raises that (not AttributeError) — happens when this is + called from background threads (sync, automation, scanners).""" try: return g.profile_id - except AttributeError: + except (AttributeError, RuntimeError): return 1 diff --git a/web_server.py b/web_server.py index 25bec6c7..a29ff3bd 100644 --- a/web_server.py +++ b/web_server.py @@ -474,10 +474,16 @@ def _add_discover_cache_headers(response): 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. + + Background callers (automation engine, sync threads, watchlist + scanner) have no request context, so `g.profile_id` raises + `RuntimeError("Working outside of application context")` rather + than `AttributeError`. Catch both so non-request callers degrade + to the admin profile instead of crashing the handler.""" try: return g.profile_id - except AttributeError: + except (AttributeError, RuntimeError): return 1