- 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.
70 lines
2.0 KiB
Python
70 lines
2.0 KiB
Python
"""Subscribing when YouTube says no — and when "no" means "you already are".
|
|
|
|
The error no longer carries YouTube's body (it is the request quoted back, and it went into a log
|
|
line and a 422), so "already following" is recognised by the reason code and nothing else.
|
|
DB lane.
|
|
"""
|
|
|
|
import pytest
|
|
from fastapi import HTTPException
|
|
from sqlalchemy import select
|
|
|
|
from app.models import Channel, Subscription
|
|
from app.routes import channels
|
|
from app.youtube.client import YouTubeError
|
|
|
|
|
|
def _youtube_refusing(reason: str):
|
|
class _Client:
|
|
def __init__(self, db, user):
|
|
pass
|
|
|
|
def __enter__(self):
|
|
return self
|
|
|
|
def __exit__(self, *exc):
|
|
return False
|
|
|
|
def insert_subscription(self, channel_id):
|
|
raise YouTubeError(f"POST subscriptions -> 400: {reason}", reason=reason)
|
|
|
|
def get_channels(self, ids):
|
|
return []
|
|
|
|
return _Client
|
|
|
|
|
|
@pytest.fixture
|
|
def stub_channel(db):
|
|
db.add(Channel(id="UCdup", title="Already followed"))
|
|
db.commit()
|
|
return "UCdup"
|
|
|
|
|
|
@pytest.mark.parametrize("reason", ["subscriptionDuplicate", "quotaExceeded,subscriptionDuplicate"])
|
|
def test_already_following_on_youtube_is_recorded_not_refused(
|
|
db, make_user, monkeypatch, stub_channel, reason
|
|
):
|
|
user = make_user()
|
|
monkeypatch.setattr(channels, "YouTubeClient", _youtube_refusing(reason))
|
|
|
|
channels.subscribe(stub_channel, user=user, db=db)
|
|
|
|
sub = db.execute(
|
|
select(Subscription).where(
|
|
Subscription.user_id == user.id, Subscription.channel_id == stub_channel
|
|
)
|
|
).scalar_one()
|
|
assert sub is not None
|
|
|
|
|
|
@pytest.mark.parametrize("reason", ["quotaExceeded", "<none>", "<unparseable>"])
|
|
def test_any_other_refusal_is_answered_with_a_422(db, make_user, monkeypatch, stub_channel, reason):
|
|
user = make_user()
|
|
monkeypatch.setattr(channels, "YouTubeClient", _youtube_refusing(reason))
|
|
|
|
with pytest.raises(HTTPException) as caught:
|
|
channels.subscribe(stub_channel, user=user, db=db)
|
|
|
|
assert caught.value.status_code == 422
|