fix(logs): deep-review round 1 — say each failure once, and scrub it everywhere
- yt-dlp's text is scrubbed wherever it is logged, not only in the adapter:
`formats.log_safe` takes out the URL, the `[extractor] <id>:` prefix and
file paths, and the job-failed, refused, edit-failed and backfill lines use
it. Done lines log the asset id, not the title-built path. The PII contract
now bans `rel`/`rel_path`/`title`.
- A traceback goes with a download/edit failure only when it is our own code
failing; for yt-dlp's DownloadError, our RuntimeErrors and OSErrors its foot
is the unscrubbed message.
- yt-dlp's `error` maps to WARNING: it fires on attempts the fallback client
set recovers from; the job's verdict is `download.job.failed`.
- The sink groups a bare `%s` pass-through by its text (numbers folded), and
a process's last batch closes the repeat window, so a short-lived download
child still reports what it dropped.
- Worker-side failures (stall, wall-clock, no asset, cached asset error) log
`download.job.failed` with a cause; they left no line at all before. The
crash, supervisor-error and non-zero-exit lines carry the code too.
- An edit failure is one coded line in `_fail_edit`, for every path.
- `logs.get_logger("__main__")` resolves to the `-m` module, so the worker
and the download child keep their names.
- TRACE lines carrying user content are gated on `logs.consented_trace()`,
not on the process level; a contract test pins it.
- The OAuth token refresh goes through `_upstream_failure`; transport errors
carry reason `transport`. Subscribe recognises a duplicate by reason code
only (the prose fallback was dead).
- Once-only guards for a stale sysconfig override and a refusing HLS session
directory; item_detail splits Plex failures from our own parse bugs.
- The playlist breaker line names the playlist and the action; comments that
were false (runner noqa, `_merge_vtt`, sink grouping) are corrected.
This commit is contained in:
@@ -418,8 +418,11 @@ class _Breaker:
|
||||
write site can't accidentally implement two thirds of the protocol (forgetting to record a
|
||||
failure would silently disable the breaker for that operation)."""
|
||||
|
||||
def __init__(self, limit: int = _MAX_CONSECUTIVE_FAILURES) -> None:
|
||||
def __init__(
|
||||
self, limit: int = _MAX_CONSECUTIVE_FAILURES, playlist_id: int | None = None
|
||||
) -> None:
|
||||
self.limit = limit
|
||||
self.playlist_id = playlist_id # for the failure line only
|
||||
self.streak = 0
|
||||
self.tripped = False
|
||||
|
||||
@@ -440,7 +443,9 @@ class _Breaker:
|
||||
self.limit,
|
||||
)
|
||||
|
||||
def run(self, op: Callable[[], T], on_fail: Callable[[], None]) -> T | _Failed:
|
||||
def run(
|
||||
self, op: Callable[[], T], on_fail: Callable[[], None], action: str = "write"
|
||||
) -> T | _Failed:
|
||||
"""Attempt one YouTube write. Returns the op's result, or `FAILED` when the breaker is
|
||||
already tripped (op not called) or the op raised YouTubeError. `on_fail` records the item
|
||||
in the caller's failure list either way."""
|
||||
@@ -450,11 +455,17 @@ class _Breaker:
|
||||
try:
|
||||
out = op()
|
||||
except YouTubeError as exc:
|
||||
# The client logged the call, but not WHICH item it was for — and the caller's failure
|
||||
# list only ever reaches the user as a count.
|
||||
# The client logged the call, but not which playlist it was for or what kind of write
|
||||
# — and the caller's failure list only ever reaches the user as a count. Not the video:
|
||||
# what is in someone's playlist is a row of their history, the same as a download.
|
||||
log.debug(
|
||||
"a playlist write to YouTube failed",
|
||||
extra=logs.event("sync.playlist.item_push_failed", reason=exc.reason),
|
||||
extra=logs.event(
|
||||
"sync.playlist.item_push_failed",
|
||||
playlist_id=self.playlist_id,
|
||||
action=action,
|
||||
reason=exc.reason,
|
||||
),
|
||||
)
|
||||
on_fail()
|
||||
self.hit(False)
|
||||
@@ -474,7 +485,7 @@ def merge_playlist(db: Session, user: User, pl: Playlist, *, live=None) -> dict:
|
||||
local = _local_video_ids(db, pl)
|
||||
inserted = deleted = reordered = created = pulled = removed_local = 0
|
||||
failures: list[str] = []
|
||||
breaker = _Breaker()
|
||||
breaker = _Breaker(playlist_id=pl.id)
|
||||
with YouTubeClient(db, user) as yt:
|
||||
if not pl.yt_playlist_id:
|
||||
pl.yt_playlist_id = yt.create_playlist(pl.name)
|
||||
@@ -485,6 +496,7 @@ def merge_playlist(db: Session, user: User, pl: Playlist, *, live=None) -> dict:
|
||||
added = breaker.run(
|
||||
lambda v=vid: yt.add_playlist_item(pl.yt_playlist_id, v),
|
||||
lambda v=vid: failures.append(v),
|
||||
"add",
|
||||
)
|
||||
if not isinstance(added, _Failed):
|
||||
inserted += 1
|
||||
@@ -567,6 +579,7 @@ def merge_playlist(db: Session, user: User, pl: Playlist, *, live=None) -> dict:
|
||||
dropped = breaker.run(
|
||||
lambda i=item_id: yt.delete_playlist_item(i),
|
||||
lambda v=vid: failures.append(v),
|
||||
"delete",
|
||||
)
|
||||
if not isinstance(dropped, _Failed):
|
||||
deleted += 1
|
||||
@@ -576,6 +589,7 @@ def merge_playlist(db: Session, user: User, pl: Playlist, *, live=None) -> dict:
|
||||
item_id = breaker.run(
|
||||
lambda v=vid: yt.add_playlist_item(pl.yt_playlist_id, v),
|
||||
lambda v=vid: failures.append(v),
|
||||
"add",
|
||||
)
|
||||
if not isinstance(item_id, _Failed):
|
||||
item_id_by_vid[vid] = item_id
|
||||
@@ -595,6 +609,7 @@ def merge_playlist(db: Session, user: User, pl: Playlist, *, live=None) -> dict:
|
||||
item_id_by_vid[w], pl.yt_playlist_id, w, pos
|
||||
),
|
||||
lambda w=want: failures.append(w),
|
||||
"move",
|
||||
)
|
||||
if isinstance(moved, _Failed):
|
||||
continue
|
||||
|
||||
Reference in New Issue
Block a user