Skip to content

Commit 914aef9

Browse files
committed
review: preserve staging and OAuth polling
1 parent 955d68f commit 914aef9

4 files changed

Lines changed: 92 additions & 10 deletions

File tree

bin/youtube-autoencoder

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -691,7 +691,7 @@ def reconcile_command_args() -> list[str]:
691691
"reconcile-broadcast",
692692
"--allow-create",
693693
"--privacy",
694-
env("YTA_YOUTUBE_STAGING_PRIVACY", env("YTA_YOUTUBE_PRIVACY", "unlisted")),
694+
env("YTA_YOUTUBE_STAGING_PRIVACY", "unlisted"),
695695
"--title-prefix",
696696
env("YTA_YOUTUBE_TITLE_PREFIX", "AutoEncoder Live"),
697697
]

bin/youtube-autoencoder-api

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -237,11 +237,18 @@ def http_json(
237237
parsed = json.loads(raw)
238238
except json.JSONDecodeError:
239239
parsed = {"error": raw}
240-
details = parsed.get("error") if isinstance(parsed, dict) else None
241-
details = details if isinstance(details, dict) else {}
242-
errors = details.get("errors") or []
243-
reasons = tuple(str(item.get("reason")) for item in errors if isinstance(item, dict) and item.get("reason"))
244-
message = str(details.get("message") or exc.reason or "YouTube API request failed")
240+
parsed_object = parsed if isinstance(parsed, dict) else {}
241+
error_value = parsed_object.get("error")
242+
if isinstance(error_value, dict):
243+
errors = error_value.get("errors") or []
244+
reasons = tuple(str(item.get("reason")) for item in errors if isinstance(item, dict) and item.get("reason"))
245+
message = str(error_value.get("message") or exc.reason or "YouTube API request failed")
246+
elif isinstance(error_value, str):
247+
reasons = (error_value,)
248+
message = str(parsed_object.get("error_description") or error_value)
249+
else:
250+
reasons = ()
251+
message = str(exc.reason or "YouTube API request failed")
245252
retry_after = None
246253
retry_after_value = exc.headers.get("Retry-After") if exc.headers else None
247254
if retry_after_value:
@@ -295,11 +302,10 @@ def authorize(_args: argparse.Namespace) -> int:
295302
"grant_type": "urn:ietf:params:oauth:grant-type:device_code",
296303
},
297304
)
298-
except RuntimeError as exc:
299-
text = str(exc)
300-
if "authorization_pending" in text:
305+
except YouTubeApiError as exc:
306+
if "authorization_pending" in exc.reasons:
301307
continue
302-
if "slow_down" in text:
308+
if "slow_down" in exc.reasons:
303309
interval += 5
304310
continue
305311
raise

tests/test_api.py

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,72 @@ def fail_request(_request, timeout):
149149
}
150150

151151

152+
def test_http_json_preserves_oauth_device_error_reason(load_script, monkeypatch):
153+
api = load_script("youtube-autoencoder-api", "yta_api_oauth_error")
154+
body = json.dumps(
155+
{
156+
"error": "authorization_pending",
157+
"error_description": "The user has not completed authorization.",
158+
}
159+
).encode()
160+
161+
def fail_request(_request, timeout):
162+
assert timeout == 30
163+
raise urllib.error.HTTPError(
164+
"https://oauth2.googleapis.com/token",
165+
400,
166+
"Bad Request",
167+
{},
168+
io.BytesIO(body),
169+
)
170+
171+
monkeypatch.setattr(api.urllib.request, "urlopen", fail_request)
172+
173+
with pytest.raises(api.YouTubeApiError) as raised:
174+
api.http_json("POST", "https://oauth2.googleapis.com/token", form={"device_code": "code"})
175+
176+
assert raised.value.reasons == ("authorization_pending",)
177+
assert str(raised.value) == "The user has not completed authorization."
178+
179+
180+
def test_authorize_retries_pending_device_flow(load_script, monkeypatch, tmp_path):
181+
api = load_script("youtube-autoencoder-api", "yta_api_authorize_pending")
182+
calls = []
183+
responses = iter(
184+
[
185+
{
186+
"verification_url": "https://example.test/device",
187+
"user_code": "ABCD-EFGH",
188+
"device_code": "device-code",
189+
"interval": 1,
190+
"expires_in": 600,
191+
},
192+
api.YouTubeApiError(
193+
status=400,
194+
reasons=("authorization_pending",),
195+
message="authorization_pending",
196+
),
197+
{"access_token": "access", "refresh_token": "refresh", "expires_in": 3600},
198+
]
199+
)
200+
201+
def fake_http(*_args, **_kwargs):
202+
calls.append(True)
203+
response = next(responses)
204+
if isinstance(response, Exception):
205+
raise response
206+
return response
207+
208+
monkeypatch.setattr(api, "TOKEN_FILE", tmp_path / "youtube-token.json")
209+
monkeypatch.setattr(api, "client_config", lambda: {"client_id": "id", "client_secret": "secret"})
210+
monkeypatch.setattr(api, "http_json", fake_http)
211+
monkeypatch.setattr(api.time, "sleep", lambda _seconds: None)
212+
213+
assert api.authorize(argparse.Namespace()) == 0
214+
assert len(calls) == 3
215+
assert json.loads(api.TOKEN_FILE.read_text(encoding="utf-8"))["refresh_token"] == "refresh"
216+
217+
152218
def test_corrupt_state_is_quarantined(load_script, monkeypatch, tmp_path):
153219
api = load_script("youtube-autoencoder-api", "yta_api_corrupt_state")
154220
state = tmp_path / "youtube-live-state.json"

tests/test_supervisor.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -616,6 +616,16 @@ def publish(_runtime, state):
616616
assert state["privacy"] == "public"
617617

618618

619+
def test_legacy_public_privacy_never_changes_staging_default(load_script, monkeypatch):
620+
supervisor = load_script("youtube-autoencoder", "yta_staging_legacy_privacy")
621+
monkeypatch.delenv("YTA_YOUTUBE_STAGING_PRIVACY", raising=False)
622+
monkeypatch.setenv("YTA_YOUTUBE_PRIVACY", "public")
623+
624+
args = supervisor.reconcile_command_args()
625+
626+
assert args[args.index("--privacy") + 1] == "unlisted"
627+
628+
619629
def test_unattended_supervisor_has_no_completion_helper(load_script):
620630
supervisor = load_script("youtube-autoencoder", "yta_no_auto_complete")
621631

0 commit comments

Comments
 (0)