feat(youtube): modo --paste en el script OAuth para autorizar desde otro dispositivo
Build & Deploy ResearchOwl / build-and-push (push) Successful in 10s
Build & Deploy ResearchOwl / build-and-push (push) Successful in 10s
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
cc1f0cab93
commit
960990327a
+66
-19
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user