fix(logs): keep polling reads and a channel-less account out of the warnings

The reads the frontend polls (unread counts, /api/me, the download index,
sync status, subtitle progress) are logged at DEBUG: at INFO they were ~95%
of the stored log on prod. A scheduler job failure and a playlist sync
failure carry event codes, and a Google account without a YouTube channel
has an empty playlist list instead of a warning every six hours.
This commit is contained in:
2026-09-19 04:51:31 +02:00
parent 29094f5f51
commit 3ee00fb051
6 changed files with 136 additions and 10 deletions
+30 -5
View File
@@ -6,6 +6,7 @@ so they don't depend on a specific user's token; subscriptions.list?mine=true al
uses OAuth.
"""
import logging
from collections.abc import Iterator
from datetime import datetime, timedelta, timezone
@@ -50,7 +51,9 @@ def _error_reason(resp: httpx.Response) -> str:
return reasons or "<none>"
def _upstream_failure(method: str, path: str, resp: httpx.Response) -> YouTubeError:
def _upstream_failure(
method: str, path: str, resp: httpx.Response, expected: frozenset[str] = frozenset()
) -> YouTubeError:
"""Report one upstream failure and build the error to raise. Every YouTube response that is
not a success goes through here — the Data API's reads, writes and deletes and the OAuth
token refresh — so what may appear in a log line cannot drift apart between them, and the
@@ -59,7 +62,11 @@ def _upstream_failure(method: str, path: str, resp: httpx.Response) -> YouTubeEr
response at all is `_send`'s, with the reason `transport`.) The body is logged only under
the requesting user's own Teljes consent: it is the request quoted back."""
reason = _error_reason(resp)
log.warning(
# A reason the caller named as an answer rather than a failure (`channelNotFound` on a `mine`
# read: the account simply has no channel) is still raised, but it is not a warning — it came
# back every six hours for an account that will never have one.
log.log(
logging.DEBUG if reason in expected else logging.WARNING,
"YouTube %s %s -> %s (%s)",
method,
path,
@@ -83,6 +90,10 @@ def _upstream_failure(method: str, path: str, resp: httpx.Response) -> YouTubeEr
return YouTubeError(f"{method} {path} -> {resp.status_code}: {reason}", reason=reason)
# The `mine` reads' answer for a Google account that has no YouTube channel.
_NO_CHANNEL = frozenset({"channelNotFound"})
def _chunks(items: list, size: int) -> Iterator[list]:
for i in range(0, len(items), size):
yield items[i : i + size]
@@ -167,7 +178,14 @@ class YouTubeClient:
return access
# --- core request ---
def _get(self, path: str, params: dict, cost: int = 1, allow_key: bool = True) -> dict:
def _get(
self,
path: str,
params: dict,
cost: int = 1,
allow_key: bool = True,
expected: frozenset[str] = frozenset(),
) -> dict:
p = dict(params)
headers = {}
api_key = sysconfig.get_str(self.db, "youtube_api_key")
@@ -178,7 +196,7 @@ class YouTubeClient:
resp = self._send("GET", f"{API_BASE}/{path}", params=p, headers=headers)
quota.record_usage(cost)
if resp.status_code != 200:
raise _upstream_failure("GET", path, resp)
raise _upstream_failure("GET", path, resp, expected)
return resp.json()
# --- endpoints ---
@@ -218,7 +236,14 @@ class YouTubeClient:
params = {"part": "snippet,contentDetails", "mine": "true", "maxResults": 50}
if page_token:
params["pageToken"] = page_token
data = self._get("playlists", params, cost=1, allow_key=False)
try:
data = self._get("playlists", params, cost=1, allow_key=False, expected=_NO_CHANNEL)
except YouTubeError as exc:
# A Google account without a YouTube channel has no playlists, and YouTube says
# so as a 404. That is an empty list, not a failed sync.
if exc.reason in _NO_CHANNEL:
return
raise
for item in data.get("items", []):
sn = item.get("snippet", {})
yield {