|
26 | 26 | TOP_VIDEO_LIMIT = 25 |
27 | 27 |
|
28 | 28 | TOKEN_URL = "https://oauth2.googleapis.com/token" |
| 29 | +TOKENINFO_URL = "https://oauth2.googleapis.com/tokeninfo" |
29 | 30 | YT_ANALYTICS_URL = "https://youtubeanalytics.googleapis.com/v2/reports" |
30 | 31 | YT_VIDEOS_URL = "https://www.googleapis.com/youtube/v3/videos" |
31 | 32 |
|
|
38 | 39 | "averageViewPercentage", |
39 | 40 | ] |
40 | 41 | REVENUE_METRICS = ["estimatedRevenue"] |
| 42 | +REQUIRED_SCOPES = { |
| 43 | + "https://www.googleapis.com/auth/yt-analytics.readonly", |
| 44 | + "https://www.googleapis.com/auth/youtube.readonly", |
| 45 | +} |
41 | 46 |
|
42 | 47 |
|
43 | 48 | class ConfigError(RuntimeError): |
@@ -80,6 +85,18 @@ def refresh_access_token(client_id: str, client_secret: str, refresh_token: str) |
80 | 85 | return str(token) |
81 | 86 |
|
82 | 87 |
|
| 88 | +def validate_access_token_scopes(access_token: str) -> None: |
| 89 | + data = api_json(f"{TOKENINFO_URL}?{parse.urlencode({'access_token': access_token})}") |
| 90 | + granted = set(str(data.get("scope", "")).split()) |
| 91 | + missing = sorted(REQUIRED_SCOPES - granted) |
| 92 | + if missing: |
| 93 | + raise RuntimeError( |
| 94 | + "The YouTube refresh token is missing required OAuth scopes. " |
| 95 | + "Re-authorize in OAuth Playground with BOTH scopes and replace YOUTUBE_REFRESH_TOKEN: " |
| 96 | + + ", ".join(missing) |
| 97 | + ) |
| 98 | + |
| 99 | + |
83 | 100 | def metric_names(include_revenue: bool) -> list[str]: |
84 | 101 | return BASE_METRICS + (REVENUE_METRICS if include_revenue else []) |
85 | 102 |
|
@@ -364,6 +381,7 @@ def main() -> int: |
364 | 381 | metrics = metric_names(args.include_revenue) |
365 | 382 |
|
366 | 383 | token = refresh_access_token(client_id, client_secret, refresh_token) |
| 384 | + validate_access_token_scopes(token) |
367 | 385 | daily_cols, daily_rows = rows_from_report(query_report( |
368 | 386 | token, |
369 | 387 | channel_id=args.channel_id, |
|
0 commit comments