@danhachuel/thunderbolt 0.5.97 → 0.5.99
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -1583,7 +1583,7 @@ def _run_task(task: dict[str, Any]) -> dict[str, Any]:
|
|
|
1583
1583
|
failed = [
|
|
1584
1584
|
f"{item.get('route')}: {item.get('message')}"
|
|
1585
1585
|
for item in attempts
|
|
1586
|
-
if isinstance(item, dict) and item.get("status")
|
|
1586
|
+
if isinstance(item, dict) and item.get("status") in {"failed", "skipped"} and item.get("message")
|
|
1587
1587
|
]
|
|
1588
1588
|
if failed:
|
|
1589
1589
|
detail = " Detalhes: " + " | ".join(failed[-4:])
|
|
@@ -142,6 +142,7 @@ def upload_with_default_route(
|
|
|
142
142
|
privacy_status=privacy_status,
|
|
143
143
|
thumbnail_path=thumbnail_path,
|
|
144
144
|
captions_path=captions_path,
|
|
145
|
+
account=account,
|
|
145
146
|
)
|
|
146
147
|
except Exception as exc: # Keep fallback actionable and deterministic.
|
|
147
148
|
official_result = IntegrationResult(False, f"API Oficial falhou: {exc}", {})
|
|
@@ -8,7 +8,7 @@ from pathlib import Path
|
|
|
8
8
|
from typing import Any, Callable
|
|
9
9
|
|
|
10
10
|
from integrations.platforms import IntegrationResult
|
|
11
|
-
from integrations.youtube_batch import loopback_host, loopback_port, loopback_redirect_uri
|
|
11
|
+
from integrations.youtube_batch import loopback_host, loopback_port, loopback_redirect_uri, token_path as batch_token_path
|
|
12
12
|
|
|
13
13
|
|
|
14
14
|
# Mantemos os mesmos escopos usados pelo youtube-automation-agent.
|
|
@@ -89,10 +89,11 @@ def build_agent_video_metadata(
|
|
|
89
89
|
|
|
90
90
|
|
|
91
91
|
class _GoogleYouTubeBase:
|
|
92
|
-
def __init__(self, settings: dict[str, Any], token_path: Path, scopes: list[str], alternate_token_path: Path | None = None):
|
|
92
|
+
def __init__(self, settings: dict[str, Any], token_path: Path, scopes: list[str], alternate_token_path: Path | None = None, account: dict[str, Any] | None = None):
|
|
93
93
|
self.settings = settings
|
|
94
|
-
self.
|
|
95
|
-
self.
|
|
94
|
+
self.account = account if isinstance(account, dict) else None
|
|
95
|
+
self.client_id = _safe_text((self.account or {}).get("client_id")) or _safe_text(settings.get("youtube_client_id")) or os.getenv("YOUTUBE_CLIENT_ID", "")
|
|
96
|
+
self.client_secret = _safe_text((self.account or {}).get("client_secret")) or _safe_text(settings.get("youtube_client_secret")) or os.getenv("YOUTUBE_CLIENT_SECRET", "")
|
|
96
97
|
self.token_path = token_path
|
|
97
98
|
self.alternate_token_path = alternate_token_path
|
|
98
99
|
self.scopes = scopes
|
|
@@ -107,9 +108,11 @@ class _GoogleYouTubeBase:
|
|
|
107
108
|
|
|
108
109
|
def _token_candidates(self) -> list[Path]:
|
|
109
110
|
paths = [self.token_path]
|
|
111
|
+
if self.account:
|
|
112
|
+
paths.append(batch_token_path(self.token_path.parents[1], self.account))
|
|
110
113
|
if self.alternate_token_path and self.alternate_token_path not in paths:
|
|
111
114
|
paths.append(self.alternate_token_path)
|
|
112
|
-
return paths
|
|
115
|
+
return list(dict.fromkeys(paths))
|
|
113
116
|
|
|
114
117
|
def _client_config(self) -> dict[str, Any]:
|
|
115
118
|
return {
|
|
@@ -135,6 +138,10 @@ class _GoogleYouTubeBase:
|
|
|
135
138
|
if isinstance(raw, dict) and isinstance(raw.get("youtube"), dict):
|
|
136
139
|
raw = raw["youtube"]
|
|
137
140
|
credentials = Credentials.from_authorized_user_info(raw, self.scopes)
|
|
141
|
+
granted_scopes = {str(scope).strip() for scope in (credentials.scopes or [])}
|
|
142
|
+
if "https://www.googleapis.com/auth/youtube.upload" in self.scopes and granted_scopes and "https://www.googleapis.com/auth/youtube.upload" not in granted_scopes:
|
|
143
|
+
email = _safe_text((self.account or {}).get("email")) or "a conta seleccionada"
|
|
144
|
+
raise RuntimeError(f"{email} está autorizada apenas para leitura/listagem. Autorize novamente esta conta com o escopo de upload YouTube.")
|
|
138
145
|
if credentials.expired and credentials.refresh_token:
|
|
139
146
|
credentials.refresh(Request())
|
|
140
147
|
self._save_credentials(credentials)
|
|
@@ -292,8 +299,8 @@ class _GoogleYouTubeBase:
|
|
|
292
299
|
class YouTubeAutomationAgentUploader(_GoogleYouTubeBase):
|
|
293
300
|
"""Adaptation of the source agent's PublishingSchedulingAgent inside Thunderbolt."""
|
|
294
301
|
|
|
295
|
-
def __init__(self, settings: dict[str, Any], storage_root: Path):
|
|
296
|
-
super().__init__(settings, storage_root / "state" / "youtube_agent_tokens.json", AGENT_SCOPES)
|
|
302
|
+
def __init__(self, settings: dict[str, Any], storage_root: Path, account: dict[str, Any] | None = None):
|
|
303
|
+
super().__init__(settings, storage_root / "state" / "youtube_agent_tokens.json", AGENT_SCOPES, account=account)
|
|
297
304
|
|
|
298
305
|
def upload(self, **kwargs: Any) -> IntegrationResult:
|
|
299
306
|
result = self._upload_common(**kwargs)
|
|
@@ -304,12 +311,13 @@ class YouTubeAutomationAgentUploader(_GoogleYouTubeBase):
|
|
|
304
311
|
class DirectYouTubeOAuthUploader(_GoogleYouTubeBase):
|
|
305
312
|
"""Independent minimal OAuth upload path used only after the primary path fails."""
|
|
306
313
|
|
|
307
|
-
def __init__(self, settings: dict[str, Any], storage_root: Path):
|
|
314
|
+
def __init__(self, settings: dict[str, Any], storage_root: Path, account: dict[str, Any] | None = None):
|
|
308
315
|
super().__init__(
|
|
309
316
|
settings,
|
|
310
317
|
storage_root / "state" / "youtube_oauth_fallback_token.json",
|
|
311
318
|
FALLBACK_SCOPES,
|
|
312
319
|
alternate_token_path=storage_root / "state" / "youtube_agent_tokens.json",
|
|
320
|
+
account=account,
|
|
313
321
|
)
|
|
314
322
|
|
|
315
323
|
def upload(self, **kwargs: Any) -> IntegrationResult:
|
|
@@ -332,6 +340,7 @@ def upload_youtube_with_fallback(
|
|
|
332
340
|
publish_at: str | None = None,
|
|
333
341
|
thumbnail_path: str | None = None,
|
|
334
342
|
captions_path: str | None = None,
|
|
343
|
+
account: dict[str, Any] | None = None,
|
|
335
344
|
on_attempt: Callable[[UploadAttempt], None] | None = None,
|
|
336
345
|
) -> IntegrationResult:
|
|
337
346
|
"""Run the adapted agent path first and OAuth direct only as redundancy."""
|
|
@@ -348,7 +357,7 @@ def upload_youtube_with_fallback(
|
|
|
348
357
|
"captions_path": captions_path,
|
|
349
358
|
}
|
|
350
359
|
attempts: list[dict[str, Any]] = []
|
|
351
|
-
primary = YouTubeAutomationAgentUploader(settings, storage_root)
|
|
360
|
+
primary = YouTubeAutomationAgentUploader(settings, storage_root, account=account)
|
|
352
361
|
primary_result = primary.upload(**kwargs)
|
|
353
362
|
primary_result.data.setdefault("mechanism", "youtube-automation-agent-adaptado")
|
|
354
363
|
primary_attempt = UploadAttempt("youtube-automation-agent-adaptado", primary_result)
|
|
@@ -359,7 +368,7 @@ def upload_youtube_with_fallback(
|
|
|
359
368
|
primary_result.data["attempts"] = attempts
|
|
360
369
|
return primary_result
|
|
361
370
|
|
|
362
|
-
fallback = DirectYouTubeOAuthUploader(settings, storage_root)
|
|
371
|
+
fallback = DirectYouTubeOAuthUploader(settings, storage_root, account=account)
|
|
363
372
|
fallback_result = fallback.upload(**kwargs)
|
|
364
373
|
fallback_result.data.setdefault("mechanism", "oauth-direct-fallback")
|
|
365
374
|
fallback_attempt = UploadAttempt("oauth-direct-fallback", fallback_result)
|
package/package.json
CHANGED