fix: resolve Jellyfin usernames to internal Id and harden qBittorrent login
Jellyfin: get_user_id() returned the configured user_id verbatim, so a
username like "admin" hit /Users/admin/Views and got HTTP 400 ("The value
'admin' is not valid."). The index worker already had username->Id
resolution, but the live API paths (dashboard counts, media query) did not.
Route all user-scoped paths through the new JellyfinClient.resolve_user_id()
(exact Id match -> Name match -> first user), cached per service/credentials
in get_user_id() so repeated requests don't re-list users. The worker is
simplified to call the same method.
qBittorrent: _login() raised "qBittorrent login failed: " (empty) on a 200
with an empty body, which happens when base_url doesn't reach the qBittorrent
login handler (wrong URL/path or a reverse proxy misroute) — not a credentials
issue. Now accepts the SID cookie as a success signal (reverse proxies that
mangle the body), returns a clear "invalid username or password" for "Fails.",
and surfaces a diagnostic error (HTTP status + body + base_url/proxy hint) for
any other/empty body.
Tests: new tests/test_jellyfin_client.py (5) + 3 qBittorrent login tests.
Full backend suite (384) passes; ruff clean.
This commit is contained in:
@@ -263,17 +263,41 @@ def get_settings_store() -> SettingsStore:
|
||||
|
||||
|
||||
def get_user_id(request: Request = None) -> str:
|
||||
"""Return the configured Jellyfin user ID or discover the first available one."""
|
||||
"""Return the Jellyfin user Id, resolving a configured username if needed.
|
||||
|
||||
The service ``user_id`` config field accepts either the internal Jellyfin Id
|
||||
or a username (e.g. ``'admin'``). Jellyfin's ``/Users/{id}/...`` endpoints
|
||||
reject usernames with HTTP 400 (``"The value 'admin' is not valid."``), so
|
||||
always resolve to the internal Id before use. Resolution is cached per
|
||||
(service, base_url, api_key, configured) so repeated dashboard/media requests
|
||||
don't re-list users on every call.
|
||||
"""
|
||||
store = get_settings_store()
|
||||
service_id = _request_jellyfin_service_id(request)
|
||||
service = _service_record(store, "jellyfin", service_id)
|
||||
if service and service.get("config", {}).get("user_id"):
|
||||
return str(service["config"]["user_id"])
|
||||
client = get_jellyfin_client(request)
|
||||
users = client.users()
|
||||
if not users:
|
||||
if service is None:
|
||||
raise HTTPException(
|
||||
status_code=503,
|
||||
detail="No Jellyfin users found and no user_id configured on the service",
|
||||
detail="No Jellyfin service is configured. Add a Jellyfin service on the Services page.",
|
||||
)
|
||||
return users[0]["Id"]
|
||||
configured = str(service.get("config", {}).get("user_id") or "").strip()
|
||||
base_url = str(service.get("config", {}).get("base_url") or "")
|
||||
api_key = str(service.get("secrets", {}).get("api_key") or "")
|
||||
if not base_url or not api_key:
|
||||
raise HTTPException(
|
||||
status_code=503,
|
||||
detail="Jellyfin service is missing base_url or api_key. Edit it on the Services page.",
|
||||
)
|
||||
return _resolved_user_id((service["id"], base_url, api_key, configured))
|
||||
|
||||
|
||||
@lru_cache(maxsize=64)
|
||||
def _resolved_user_id(cache_key: tuple[str, str, str, str]) -> str:
|
||||
"""Resolve a configured Jellyfin identifier (Id or username) to the internal Id.
|
||||
|
||||
Keyed by (service_id, base_url, api_key, configured) so a credentials change
|
||||
or a different configured user busts the cache automatically.
|
||||
"""
|
||||
service_id, base_url, api_key, configured = cache_key
|
||||
client = _jellyfin_client_for((service_id, base_url, api_key))
|
||||
return client.resolve_user_id(configured or None)
|
||||
|
||||
Reference in New Issue
Block a user