feat(youtube): modo --paste en el script OAuth para autorizar desde otro dispositivo
Build & Deploy ResearchOwl / build-and-push (push) Successful in 10s

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
ChemaVX
2026-08-05 20:51:13 +00:00
co-authored by Claude Fable 5
parent cc1f0cab93
commit 960990327a
2 changed files with 69 additions and 20 deletions
+66 -19
View File
@@ -6,6 +6,14 @@
Abre el navegador, te pide permiso para subir vídeos a tu canal, y escupe el
refresh token para meterlo en Infisical como `youtube-refresh-token`.
Con `--paste` no levanta servidor local: da permiso desde CUALQUIER dispositivo
(un iPad vale), deja que la pestaña de redirección falle al cargar — no hay
nadie escuchando, es lo esperado — y pega aquí la URL completa de la barra de
direcciones, que lleva el `code=` dentro. También se puede canalizar por stdin:
echo 'http://localhost:8765/?code=4/0Ab...' | \\
python scripts/youtube_oauth.py --paste --client-id XXX --client-secret YYY
Sólo stdlib: esto corre fuera del contenedor, en el portátil de quien lo lance,
y no debería exigir instalar nada.
@@ -44,6 +52,10 @@ AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth"
TOKEN_URL = "https://oauth2.googleapis.com/token"
SCOPE = "https://www.googleapis.com/auth/youtube.upload"
# En modo --paste nadie escucha en el puerto, pero el redirect_uri del
# intercambio tiene que ser IDÉNTICO al de la autorización, así que va fijo.
PASTE_PORT = 8765
_PAGE = """<!doctype html><meta charset="utf-8">
<title>ResearchOwl</title>
<body style="font-family:system-ui;max-width:32rem;margin:6rem auto;line-height:1.6">
@@ -81,6 +93,18 @@ def _free_port() -> int:
return s.getsockname()[1]
def _code_from_paste(text: str) -> str:
"""Saca el `code` de lo que pegue el usuario: la URL de la redirección
entera, o el código a secas si fue más listo que el script."""
text = text.strip().strip('"').strip("'")
if not text:
return ""
if "?" in text or text.startswith(("http", "localhost")):
query = urllib.parse.urlparse(text).query
return urllib.parse.parse_qs(query).get("code", [""])[0]
return text
def _post_form(url: str, data: dict) -> dict:
payload = urllib.parse.urlencode(data).encode()
request = urllib.request.Request(
@@ -102,13 +126,17 @@ def main() -> int:
default=os.environ.get("YOUTUBE_CLIENT_SECRET"))
parser.add_argument("--no-browser", action="store_true",
help="no abrir el navegador, sólo imprimir la URL")
parser.add_argument("--paste", action="store_true",
help="sin servidor local: autoriza desde cualquier "
"dispositivo y pega aquí la URL de la redirección "
"(la pestaña dará error de conexión; es normal)")
args = parser.parse_args()
if not args.client_id or not args.client_secret:
parser.error("hacen falta --client-id y --client-secret "
"(o YOUTUBE_CLIENT_ID / YOUTUBE_CLIENT_SECRET en el entorno)")
port = _free_port()
port = PASTE_PORT if args.paste else _free_port()
redirect_uri = f"http://localhost:{port}"
auth_url = f"{AUTH_URL}?" + urllib.parse.urlencode({
"client_id": args.client_id,
@@ -122,27 +150,46 @@ def main() -> int:
"prompt": "consent",
})
server = http.server.HTTPServer(("127.0.0.1", port), _Catcher)
waiter = threading.Thread(target=server.handle_request, daemon=True)
waiter.start()
if args.paste:
print(f"\nAbre esto EN CUALQUIER DISPOSITIVO y da permiso (verás un "
f"aviso de app no verificada; «Advanced» → continuar):\n\n"
f" {auth_url}\n\n"
f"La pestaña final fallará al cargar (localhost:{port} no existe "
f"ahí) — es lo esperado. Copia la URL COMPLETA de la barra de "
f"direcciones. El código caduca en unos 10 minutos.")
try:
pasted = input("\nPega aquí la URL de la redirección: ")
except EOFError:
pasted = ""
code = _code_from_paste(pasted)
if not code:
print("\n❌ Ahí no venía ningún `code`. Vuelve a lanzar y pega la "
"URL entera de la barra de direcciones (empieza por "
"http://localhost:…).", file=sys.stderr)
return 1
else:
server = http.server.HTTPServer(("127.0.0.1", port), _Catcher)
waiter = threading.Thread(target=server.handle_request, daemon=True)
waiter.start()
print(f"\nAbre esto y da permiso (verás un aviso de app no verificada; "
f"«Advanced» → continuar):\n\n {auth_url}\n")
if not args.no_browser:
webbrowser.open(auth_url)
print("Esperando la redirección… (Ctrl-C para abortar)")
print(f"\nAbre esto y da permiso (verás un aviso de app no verificada; "
f"«Advanced» → continuar):\n\n {auth_url}\n")
if not args.no_browser:
webbrowser.open(auth_url)
print("Esperando la redirección… (Ctrl-C para abortar)")
waiter.join(timeout=300)
server.server_close()
if waiter.is_alive():
print("\n❌ Nadie llegó a la redirección en 5 minutos.", file=sys.stderr)
return 1
waiter.join(timeout=300)
server.server_close()
if waiter.is_alive():
print("\n❌ Nadie llegó a la redirección en 5 minutos.",
file=sys.stderr)
return 1
code = _Catcher.result.get("code")
if not code:
print(f"\n❌ Sin código. Google dijo: "
f"{_Catcher.result.get('error', '(nada)')}", file=sys.stderr)
return 1
code = _Catcher.result.get("code")
if not code:
print(f"\n❌ Sin código. Google dijo: "
f"{_Catcher.result.get('error', '(nada)')}", file=sys.stderr)
return 1
tokens = _post_form(TOKEN_URL, {
"code": code,