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:
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user