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:
@@ -90,6 +90,32 @@ class JellyfinClient:
|
||||
logger.info("Jellyfin returned %s visible users", len(users))
|
||||
return users
|
||||
|
||||
def resolve_user_id(self, identifier: str | None) -> str:
|
||||
"""Resolve a configured user identifier to Jellyfin's internal Id.
|
||||
|
||||
The service ``user_id`` config field accepts either the internal Jellyfin
|
||||
Id (a hash) or a username (e.g. ``'admin'``). Jellyfin's
|
||||
``/Users/{id}/...`` endpoints reject usernames with HTTP 400
|
||||
(``"The value 'admin' is not valid."``), so any caller must resolve
|
||||
usernames to the real Id before hitting user-scoped endpoints.
|
||||
|
||||
Resolution order: exact ``Id`` match → ``Name`` match → first visible
|
||||
user. Raises if the API key cannot see any users.
|
||||
"""
|
||||
users = self.users()
|
||||
if not users:
|
||||
raise RuntimeError("No Jellyfin users visible to this API key")
|
||||
if identifier:
|
||||
if any(str(u.get("Id")) == identifier for u in users):
|
||||
return identifier
|
||||
match = next((u for u in users if str(u.get("Name", "")) == identifier), None)
|
||||
if match:
|
||||
resolved = str(match["Id"])
|
||||
logger.info("Resolved Jellyfin username %r to Id %s", identifier, resolved)
|
||||
return resolved
|
||||
logger.warning("Jellyfin user identifier %r not found; using first user", identifier)
|
||||
return str(users[0]["Id"])
|
||||
|
||||
def libraries(self, user_id: str) -> list[dict[str, Any]]:
|
||||
"""Return top-level library views visible to the selected Jellyfin user."""
|
||||
items = self.get(f"/Users/{user_id}/Views").get("Items", [])
|
||||
|
||||
Reference in New Issue
Block a user