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:
@@ -44,8 +44,16 @@ class QbittorrentClient:
|
||||
def _login(self) -> None:
|
||||
"""POST username/password to ``/auth/login``; store the SID cookie.
|
||||
|
||||
qBittorrent returns the plain text ``"Ok."`` on success. The
|
||||
``Referer`` header is required by some qBittorrent CSRF protections.
|
||||
qBittorrent replies with the plain text ``"Ok."`` and a ``SID`` cookie
|
||||
on success, ``"Fails."`` on bad credentials, and ``403 Forbidden`` when
|
||||
the source IP is banned (too many failed attempts). The ``Referer``
|
||||
header is required by qBittorrent's CSRF protection.
|
||||
|
||||
Any other body — in particular an *empty* 200 — means the request did not
|
||||
reach qBittorrent's login handler, almost always because ``base_url`` is
|
||||
wrong (wrong host/port/path) or a reverse proxy is misrouting
|
||||
``/api/v2/auth/login``. We surface a diagnostic error in that case
|
||||
instead of the useless ``"login failed: "`` message.
|
||||
"""
|
||||
resp = self._session.post(
|
||||
f"{self.base_url}/auth/login",
|
||||
@@ -54,10 +62,23 @@ class QbittorrentClient:
|
||||
headers={"Referer": self.base_url},
|
||||
)
|
||||
resp.raise_for_status()
|
||||
if resp.text.strip() != "Ok.":
|
||||
raise RuntimeError(f"qBittorrent login failed: {resp.text.strip()}")
|
||||
self._logged_in = True
|
||||
logger.info("qBittorrent login successful for %s", self.base_url)
|
||||
body = resp.text.strip()
|
||||
# Some reverse proxies forward the SID cookie but mangle the text body;
|
||||
# accept either success signal. Guard the cookie read behind an empty
|
||||
# body so a mocked response never accidentally reads as success.
|
||||
sid_ok = body == "" and bool(resp.cookies.get("SID"))
|
||||
if body == "Ok." or sid_ok:
|
||||
self._logged_in = True
|
||||
logger.info("qBittorrent login successful for %s", self.base_url)
|
||||
return
|
||||
if body == "Fails.":
|
||||
raise RuntimeError(f"qBittorrent login failed (HTTP {resp.status_code}): invalid username or password")
|
||||
raise RuntimeError(
|
||||
f"qBittorrent login failed (HTTP {resp.status_code}, body={body!r}). "
|
||||
"Expected the text 'Ok.' from /api/v2/auth/login — this usually means "
|
||||
"base_url does not reach the qBittorrent Web API (check the URL, path, "
|
||||
"and any reverse proxy in front of qBittorrent)."
|
||||
)
|
||||
|
||||
def _get(self, path: str, **params: Any) -> dict[str, Any]:
|
||||
"""GET an endpoint with auto-login on first call and re-login on 403."""
|
||||
|
||||
Reference in New Issue
Block a user