API REST
Todo lo que hace la app con bibliotecas, videos, la cola y las exportaciones también se puede pedir por HTTP, en JSON, bajo http://<equipo>:8765/api/v1. La lista completa de rutas está en Rutas de la API; aquí va lo que necesitas para usarlas.
GET /api/v1 responde {version, doc} con esa misma lista en texto, útil para comprobar que llegas al servidor y que tu clave sirve.
Autenticación
Sección titulada «Autenticación»Cada pedido lleva una clave de API en la cabecera Authorization:
curl -H "Authorization: Bearer vt_…" http://localhost:8765/api/v1/librariesUna clave tiene dos propiedades:
| Opciones | |
|---|---|
| Alcance | Todas las bibliotecas (también las que crees después) o una biblioteca. |
| Permiso | Lectura: ver, buscar y exportar. Escritura: además, importar, borrar, cambiar el glosario y manejar la cola. |
Crear y revocar una clave
Sección titulada «Crear y revocar una clave»- Abre Preferencias → Acceso y API → Claves de API.
- Ponle un nombre que diga dónde la usas (p. ej.
n8n), elige el alcance y Lectura o Escritura. - Pulsa Crear clave y cópiala: empieza por
vt_y no se vuelve a mostrar. El servidor solo guarda su hash.
La lista de claves muestra el nombre, el alcance, el permiso, un resumen de la clave (vt_xxxx…xxxx) y cuándo se usó por última vez (se actualiza como mucho una vez por minuto). Revocar la borra: lo que la use empieza a recibir 401 al instante.
Si borras la biblioteca de una clave de una sola biblioteca, la clave deja de servir.
Sin clave
Sección titulada «Sin clave»- Desde el mismo equipo donde corre la app (ver Acceso), un pedido sin
Authorizationresponde como al dueño. Así puedes probar concurlsin crear una clave. - Con una clave inválida o revocada, la respuesta es
401aunque el pedido venga del mismo equipo: no se cae al acceso de dueño. - Desde otro equipo, sin clave, la API usa lo mismo que el navegador: la cookie de sesión
vt_s, si la manda, y las bibliotecas públicas.
Respuestas y errores
Sección titulada «Respuestas y errores»Las respuestas son JSON (application/json; charset=utf-8), salvo los subtítulos. Los errores traen {"error": "…"}:
| Código | Cuándo |
|---|---|
200 |
Listo. |
202 |
Aceptado y en marcha: importar, mandar a la cola, pedir una exportación. |
400 |
Datos inválidos (p. ej. faltan urls, mode desconocido, kinds vacío). Un cuerpo que no es JSON válido se toma como {}. |
401 |
Sin clave válida, o sin acceso a ninguna biblioteca. |
403 |
La clave es de solo lectura, pausar la cola sin una clave de escritura para todas las bibliotecas, o un pedido que cambia algo desde otro sitio web sin clave (Otros sitios web). |
404 |
La ruta, la biblioteca, el video o la exportación no existe, o la clave no puede verlo: no se distingue a propósito. |
413 |
El cuerpo pasa de 2 MB (respuesta en texto plano). |
Bibliotecas
Sección titulada «Bibliotecas»Todas piden permiso de lectura sobre la biblioteca, salvo donde se indica.
GET /librarieslista las bibliotecas que la clave puede ver:id,name,count(videos),downloaded,main,color,icon,pattern. Solo el dueño recibe ademáspath(la carpeta) yaccess(la visibilidad).GET /libraries/{lib}/collectionsdaid,name,kind,count,urlychannel(estos dos pueden venir ennull). Losidautomáticos sonall,recent,ch-…(canales) ycat-…(categorías).GET /libraries/{lib}/search?q=busca en las transcripciones y responde{q, total, results}.GET /libraries/{lib}/glossaryda{entries}.PUTcon{"entries": [{"src": "prompt", "dst": "instrucción"}]}lo reemplaza entero; pide escritura.
Videos de una biblioteca
Sección titulada «Videos de una biblioteca»GET /libraries/{lib}/videos pagina con offset (desde 0) y limit (100 por omisión, máximo 500: más se recorta a 500). collection elige la colección (all por omisión, del último agregado al primero).
{ "total": 1240, "offset": 0, "videos": [ { "id": "yt_dQw4w9WgXcQ", "title": "…", "progress": { "asr": 1, "dub": 1 }, "queue": { "state": "listo" } } ] }Para recorrerla entera, suma limit a offset hasta pasar total.
Importar
Sección titulada «Importar»POST /libraries/{lib}/import pide escritura.
{ "urls": ["https://www.youtube.com/watch?v=…", "https://www.youtube.com/playlist?list=…"], "mode": "traducir", "priority": false }urlsacepta enlaces de YouTube a videos o playlists (o un id de 11 caracteres). También vale"url": "…"para uno solo. Lo que no es de YouTube se ignora.mode:traducir(traducir y doblar) odescargar(solo bajar el video). Si lo omites, usa la preferencia Al importar de esa biblioteca.priority: truelos pone primero en la cola, y entonces se procesan aunquemodeseadescargar.- Cada playlist se importa como colección de la biblioteca.
Responde 202 con {"videos": 3, "playlists": 1, "elsewhere": {"Ciencia": 2}}: elsewhere cuenta los videos que ya estaban en otra biblioteca y se quedaron allá (un video vive en una sola).
GET /videos/{id}(lectura) da los datos del video (title,duration,channel,categories,added,library), las URL de sus archivos (media,audio,thumb), el avance y su estado en la cola:progress.asr: fracción del video reconocida y traducida (0 a 1).progress.dub: fracción doblada (0 a 1).queue:{state, prio, error, dl}onullsi el video no está en la cola.stateesen cola,procesando,descargado,listooerror;dles el avance de la descarga.
GET /videos/{id}/transcript(lectura) da las frases constart,end,text(inglés),translation(español) yspeaker, másspeaker_namesycomments.GET /videos/{id}/subtitles?lang=es(lectura) devuelve un archivo SRT (application/x-subrip).lang=eses el español (por omisión); cualquier otro valor da el inglés.PATCH /videos/{id}(escritura) con{"title": "…"}le cambia el nombre al video; el nombre se queda aunque el video se vuelva a descargar.DELETE /videos/{id}(escritura) borra el video con sus archivos.
GET /queue?library=(lectura) da{paused, current, blocked_until, jobs}, solo con los trabajos de las bibliotecas que la clave ve;libraryfiltra por una.current(lo que se procesa ahora) solo se muestra al dueño.blocked_untiles distinto de 0 mientras YouTube pide verificación.POST /queue(escritura) con{"video": "yt_…", "priority": true}manda un video que ya está en la biblioteca a la cola; conpriority, primero.DELETE /queue/{id}(escritura) lo quita de la cola.POST /queue/pauseyPOST /queue/resumepausan o reanudan todo el procesamiento. Como afectan a todas las bibliotecas, piden una clave de escritura para todas las bibliotecas.
Exportaciones
Sección titulada «Exportaciones»POST /exports(lectura de cada video) con{"videos": ["yt_…"], "kinds": ["es_video", "srt_es"], "name": "…"}responde202con{id, state}. Los valores dekindsestán en Rutas de la API; los desconocidos se ignoran.namees opcional.GET /exports/{id}da{id, name, state, pct, detail, url, error}, solo si puedes ver todos sus videos (si no,404).statepasa poren espera,exportandooesperando …(si el video aún no termina de procesarse) hastalistooerror.
Al quedar listo, url es una ruta del mismo servidor (empieza por /). Descárgala con la misma cabecera Authorization.
Ejemplos
Sección titulada «Ejemplos»En todos, TOKEN es tu clave y la app corre en localhost:8765.
Listar bibliotecas
Sección titulada «Listar bibliotecas»curl -H "Authorization: Bearer $TOKEN" http://localhost:8765/api/v1/librariesImportar un video y priorizarlo
Sección titulada «Importar un video y priorizarlo»curl -X POST http://localhost:8765/api/v1/libraries/principal/import \ -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \ -d '{"urls": ["https://www.youtube.com/watch?v=dQw4w9WgXcQ"], "priority": true}'Los videos de YouTube quedan con id yt_ seguido del id de YouTube (aquí, yt_dQw4w9WgXcQ).
Consultar el avance de un video
Sección titulada «Consultar el avance de un video»curl -H "Authorization: Bearer $TOKEN" http://localhost:8765/api/v1/videos/yt_dQw4w9WgXcQimport json, os, time, urllib.request
BASE, TOKEN = "http://localhost:8765", os.environ["TOKEN"]
def get(path): req = urllib.request.Request(BASE + path, headers={"Authorization": f"Bearer {TOKEN}"}) with urllib.request.urlopen(req) as r: return json.load(r)
while True: v = get("/api/v1/videos/yt_dQw4w9WgXcQ") state = (v["queue"] or {}).get("state") print(state, v["progress"]) if state in ("listo", "error"): break time.sleep(10)// Node 18 o más nuevo; guárdalo como .mjsconst BASE = "http://localhost:8765";const headers = { Authorization: `Bearer ${process.env.TOKEN}` };
while (true) { const v = await (await fetch(`${BASE}/api/v1/videos/yt_dQw4w9WgXcQ`, { headers })).json(); const state = v.queue?.state; console.log(state, v.progress); if (state === "listo" || state === "error") break; await new Promise((r) => setTimeout(r, 10_000));}Bajar los subtítulos en español
Sección titulada «Bajar los subtítulos en español»curl -H "Authorization: Bearer $TOKEN" -o video.es.srt \ "http://localhost:8765/api/v1/videos/yt_dQw4w9WgXcQ/subtitles?lang=es"Exportar y esperar el archivo
Sección titulada «Exportar y esperar el archivo»# 1. pedir la exportación: responde {"id": "…", "state": "en espera"}curl -X POST http://localhost:8765/api/v1/exports \ -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \ -d '{"videos": ["yt_dQw4w9WgXcQ"], "kinds": ["es_video"]}'
# 2. repetir hasta que "state" sea "listo"curl -H "Authorization: Bearer $TOKEN" http://localhost:8765/api/v1/exports/<id>
# 3. bajar el archivo de "url"curl -H "Authorization: Bearer $TOKEN" -o video.mp4 "http://localhost:8765<url>"import json, os, time, urllib.request
BASE, TOKEN = "http://localhost:8765", os.environ["TOKEN"]AUTH = {"Authorization": f"Bearer {TOKEN}"}
def call(path, body=None): data = json.dumps(body).encode() if body is not None else None req = urllib.request.Request(BASE + path, data=data, headers={**AUTH, "Content-Type": "application/json"}) with urllib.request.urlopen(req) as r: return json.load(r)
job = call("/api/v1/exports", {"videos": ["yt_dQw4w9WgXcQ"], "kinds": ["es_video"]})while (job := call(f"/api/v1/exports/{job['id']}"))["state"] not in ("listo", "error"): time.sleep(5)
if job["state"] == "error": raise SystemExit(job["error"])req = urllib.request.Request(BASE + job["url"], headers=AUTH)with urllib.request.urlopen(req) as r, open("video.mp4", "wb") as f: while chunk := r.read(1 << 20): f.write(chunk)// Node 18 o más nuevo; guárdalo como .mjsimport { writeFile } from "node:fs/promises";
const BASE = "http://localhost:8765";const headers = { Authorization: `Bearer ${process.env.TOKEN}`, "Content-Type": "application/json" };const call = async (path, body) => (await fetch(BASE + path, body ? { method: "POST", headers, body: JSON.stringify(body) } : { headers })).json();
let job = await call("/api/v1/exports", { videos: ["yt_dQw4w9WgXcQ"], kinds: ["es_video"] });while (!["listo", "error"].includes(job.state)) { await new Promise((r) => setTimeout(r, 5000)); job = await call(`/api/v1/exports/${job.id}`);}if (job.state === "error") throw new Error(job.error);
const file = await fetch(BASE + job.url, { headers });await writeFile("video.mp4", Buffer.from(await file.arrayBuffer()));Inicio de sesión de la vista
Sección titulada «Inicio de sesión de la vista»La app web usa otras tres rutas, fuera de /api/v1, pensadas para el navegador:
POST /api/auth/logincon{"password": "…"}(dueño) o{"password": "…", "vault": "<id>"}(biblioteca con contraseña). Si acierta, responde200con la cookievt_s; si no,401.POST /api/auth/logoutborra la sesión.GET /api/auth/medice qué puede la conexión:{owner, perms, local}.
Para integraciones, usa claves: no caducan y se revocan una por una.