From 960990327a222808796ddb8af8ef5cb7e735a7cb Mon Sep 17 00:00:00 2001 From: ChemaVX Date: Wed, 5 Aug 2026 20:51:13 +0000 Subject: [PATCH] feat(youtube): modo --paste en el script OAuth para autorizar desde otro dispositivo Co-Authored-By: Claude Fable 5 --- README.md | 4 +- scripts/youtube_oauth.py | 85 +++++++++++++++++++++++++++++++--------- 2 files changed, 69 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index cff3cd4..831c5ee 100644 --- a/README.md +++ b/README.md @@ -122,7 +122,9 @@ One-time setup, in [console.cloud.google.com](https://console.cloud.google.com): 3. Credentials → OAuth client ID → **Desktop app**. 4. `python scripts/youtube_oauth.py --client-id … --client-secret …`, which opens a browser, catches the redirect on localhost and prints the refresh - token. + token. Add `--paste` when the browser is on another device (an iPad, say): + the final redirect tab fails to load — nothing listens there, that is + expected — and you paste its full URL back into the terminal. 5. Put `youtube-client-id`, `youtube-client-secret` and `youtube-refresh-token` into Infisical (they arrive as `researchowl-secrets-infisical`). diff --git a/scripts/youtube_oauth.py b/scripts/youtube_oauth.py index af72d6a..6c67073 100644 --- a/scripts/youtube_oauth.py +++ b/scripts/youtube_oauth.py @@ -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 = """ ResearchOwl @@ -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,