Busca programas y episodios de iVoox, y súbelos directamente a tus Archivos de Pocket Casts — sin descargar nada a mano, sin conversiones, sin pasos intermedios.
PodBridge es una herramienta personal, pensada para gente que sigue podcasts en iVoox pero prefiere Pocket Casts como reproductor. Busca un programa, elige el episodio que quieras y pulsa un botón: el audio (y su portada) aparecen en tus Archivos de Pocket Casts, listos para escuchar en cualquier dispositivo.
No es una app oficial de iVoox ni de Pocket Casts, ni tiene afiliación con ninguna de las dos.
- Características
- Cómo funciona (arquitectura)
- Instalación y despliegue
- Uso
- Instalar como app
- Persistencia y seguridad
- Cómo funciona el Worker por dentro
- Limitaciones y cosas a tener en cuenta
- Estructura del repositorio
- Licencia
- Aviso de uso
Búsqueda
- Buscador de programas de iVoox, con portada, insignia de iVoox Originals y descripción (con "Leer más" para la versión completa).
- Buscador propio dentro de la lista de episodios de un programa ya abierto, para encontrar uno concreto sin bajar a mano.
- Episodios favoritos: todos los episodios de tus programas favoritos, juntos y ordenados por fecha — para ver de un vistazo si hay algo nuevo sin entrar programa por programa. (Antes había un "buscador de episodios sueltos por palabra clave" — se quitó porque iVoox no indexa episodios individuales, así que en la práctica casi nunca encontraba el episodio que buscabas; esto resuelve mejor el caso real de uso.)
Programas y episodios
- Vista de programa con portada, descripción, enlace directo a iVoox (nueva pestaña) e insignia de iVoox Originals cuando corresponde.
- Lista de episodios con scroll infinito (cargan por páginas a medida que bajas, no solo la primera).
- Cada episodio muestra portada, fecha, duración y un botón de "más información" con la ficha completa (descripción entera, insignias, enlace a iVoox).
- Los episodios exclusivos (contenido de pago de iVoox) se identifican y quedan bloqueados: esta herramienta nunca los descarga.
Descarga y subida
- Un solo botón por episodio: el Worker descarga el audio de iVoox y lo sube a Archivos de Pocket Casts de servidor a servidor — el audio en sí nunca pasa por tu navegador ni por tu conexión.
- Los episodios muy largos (más de 100 MB) van por una vía distinta — Cloudflare Workers no consigue mantener fiable esa subida con ficheros tan grandes — usando un servicio de relevo aparte, opcional de desplegar. Ver Episodios muy grandes.
- Sube también la portada del episodio como imagen personalizada del fichero en Pocket Casts (si falla, el episodio se sube igualmente, solo que sin portada propia).
- Se pueden lanzar varias descargas/subidas a la vez (no hay cola: cada episodio va por su cuenta) — un indicador en la cabecera muestra cuántas hay en curso y, al pasar el ratón o pulsarlo, el progreso individual de cada una.
- Si una descarga o subida se queda sin responder (red inestable, algo se cuelga a medio camino…), se detecta sola a los 45 segundos sin actividad y pasa a error lista para reintentar — y en cualquier momento se puede cancelar a mano pulsando el propio botón de progreso. Antes un atasco se quedaba así para siempre, sin más salida que recargar la página entera.
Cuenta y estado
- Conexión con Pocket Casts mediante email y contraseña; las credenciales viajan directas a tu propio Worker y de ahí a Pocket Casts, nunca a un servidor de terceros. La contraseña nunca se guarda en ningún sitio.
- "Recordarme en este dispositivo" (opcional): mantiene la sesión de Pocket Casts entre visitas sin volver a pedir la contraseña, con el token cifrado en el navegador — ver Persistencia y seguridad.
- Espacio usado en Archivos de Pocket Casts (barra con porcentaje, y cuánto ocupa cada episodio en su propia fila), para saber cuánto sitio te queda antes de que se llene.
- Indicador de estado combinado en la cabecera: al pasar el ratón o pulsarlo se ve el detalle del Worker, de Pocket Casts y del servicio de relevo por separado (si lo tienes configurado) — de un vistazo sabes cuál de los tres falla si algo no funciona. El del relevo distingue, cuando no responde, si ha sido justo tras esperar el minuto entero (puede seguir dormido o estar caído de verdad — no hay forma de saberlo sin reintentar), si ha respondido pero con error (está despierto, algo va mal en el propio servicio) o si ni siquiera se ha podido contactar (URL mal puesta, DNS, sin conexión) — y "activo" de "estaba dormido, ya despierto" cuando sí responde, para que un arranque en frío del plan gratuito de Render no parezca un error.
Favoritos e historial
- Marca programas como favoritos con un toque (desde la tarjeta de búsqueda o desde la propia ficha del programa) y encuéntralos luego en la pestaña "Favoritos" del buscador, que además tiene su propio filtro por título para cuando la lista crece. Si quitas uno sin querer, el aviso trae un botón de "Deshacer".
- Los episodios ya subidos se recuerdan entre sesiones: si vuelves más tarde, siguen marcados como subidos en vez de ofrecerte subirlos otra vez.
- Exportar/importar favoritos, historial, la URL del Worker y la configuración del relevo (URL y secreto) como un archivo JSON, para hacer copia de seguridad o pasarlos a otro navegador — nunca incluye contraseñas ni el token de Pocket Casts. También se puede importar desde el portapapeles o pegando el JSON a mano, y si la descarga del archivo no funciona (pasa en algunos WebViews que envuelven la web como app, como Hermit) queda siempre disponible copiarlo a mano como alternativa.
- Junto a esos botones, un aviso sencillo de cuándo fue la última exportación y la última importación (cada una por separado, si ha habido) y si ha habido cambios desde entonces — en un color más llamativo cuando toca exportar de nuevo, para no descubrir meses después que la última copia se quedó vieja.
Interfaz
- Diseño responsive (escritorio y móvil), con tema claro/oscuro automático según el sistema.
- Instalable como app (PWA) tanto en escritorio como en móvil — icono propio, sin barra de direcciones. Ver Instalar como app.
- Mientras haya una descarga o subida en curso, se evita que el propio sistema apague la pantalla por inactividad (Screen Wake Lock API) — sobre todo útil en móvil con episodios largos.
- Botón de mostrar/ocultar en los campos de contraseña y secretos (cuenta de Pocket Casts, servicio de relevo).
- Estados de carga (esqueletos con la forma real de lo que va a cargar), vacío y error cuidados en toda la app, con opción de reintentar.
- Aviso antes de cerrar o recargar la pestaña si hay una descarga o subida en curso.
GitHub Pages solo sirve ficheros estáticos (HTML/CSS/JS). Pero hacer funcionar esta app de verdad requiere dos cosas que un navegador no puede hacer por sí solo:
- Buscar en iVoox. iVoox no tiene una API JSON pública — su búsqueda
son páginas HTML normales, hay que descargarlas e interpretarlas
(scraping), y su servidor no manda cabeceras CORS pensadas para que
un origen externo (como
tu-usuario.github.io) las lea confetch. - Subir a Pocket Casts. Su API (
api.pocketcasts.com) es privada, usa mensajes protobuf binarios para subir ficheros, y tampoco habilita CORS para orígenes de terceros.
La solución es un pequeño Worker de Cloudflare — gratis, tuyo, lo despliegas tú — que hace de puente: el frontend estático en GitHub Pages le habla a tu Worker, y tu Worker habla con iVoox y con Pocket Casts.
Navegador (GitHub Pages)
│ búsqueda, progreso, ajustes
▼
Tu Worker (Cloudflare)
│ parsea el HTML de iVoox ────────────▶ iVoox (scraping)
│ habla el protobuf de Pocket Casts ──▶ Pocket Casts (API privada)
Cada usuario despliega su propio Worker: es una decisión de diseño, no una limitación accidental. Así las credenciales de Pocket Casts pasan siempre por infraestructura que tú controlas, nunca por un servidor compartido con otros usuarios de la app.
cd worker
npx wrangler login # una vez, abre el navegador para autorizar
npx wrangler deployLa primera vez, wrangler puede pedirte registrar un subdominio
workers.dev (gratis, di que sí) y verificar el email de tu cuenta de
Cloudflare si aún no lo has hecho. Al terminar te da una URL del tipo:
https://podbridge-proxy.tu-subdominio.workers.dev
Guárdala, la necesitas en el paso 3.
Opcional pero recomendado: una vez tengas la URL de tu GitHub Pages,
edita worker/wrangler.toml y cambia ALLOWED_ORIGIN = "*" por tu
dominio real (https://tu-usuario.github.io), y vuelve a desplegar.
Sube este repositorio a GitHub y activa Pages en
Settings → Pages → Build and deployment → GitHub Actions. El workflow
en .github/workflows/deploy.yml publica la raíz del repo en cada push a
main — no hay paso de compilación, es HTML/CSS/JS plano.
Nota: GitHub Pages con repositorio privado requiere un plan GitHub Pro, Team o Enterprise; en una cuenta gratuita el repo tiene que ser público para poder publicar Pages. Y aunque el repo sea privado, la web publicada sigue siendo accesible por su URL para cualquiera que la tenga — GitHub Pages no restringe el acceso a la página en sí salvo en planes Enterprise.
Abre la web publicada → icono de ajustes:
- URL del Worker: pega la URL de
wranglerdel paso 1. - Cuenta de Pocket Casts: tu email y contraseña.
Sin este paso, todo funciona igual salvo que los episodios de más de 100 MB no se pueden subir. Paso a paso completo en Episodios muy grandes → Desplegar el servicio de relevo.
- Busca un programa desde la portada.
- Abre un programa para ver su lista completa de episodios — baja con scroll para cargar más, o usa el buscador propio de esa lista para encontrar uno concreto.
- Pulsa el botón de descarga en el episodio que quieras: se descarga de iVoox y se sube a Pocket Casts automáticamente, con su portada.
- El botón de información (ⓘ) abre la ficha completa de un episodio sin salir de la lista.
- Marca programas como favoritos (☆) y usa la pestaña "Episodios favoritos" para ver, todos juntos y ordenados por fecha, los episodios de todo lo que sigues — sin entrar programa por programa.
Los episodios marcados como exclusivos no tienen botón de descarga: es contenido de pago de iVoox y esta herramienta no lo toca.
PodBridge es una PWA (Progressive Web App) — se puede instalar y usar como una app normal, con su propio icono, sin barra de direcciones ni pestañas del navegador alrededor:
- Android (Chrome): menú (⋮) → "Instalar app" o "Añadir a pantalla de inicio".
- iOS (Safari): botón de compartir → "Añadir a pantalla de inicio".
- Escritorio (Chrome, Edge): icono de instalación en la barra de direcciones, o menú → "Instalar PodBridge…".
No tiene modo sin conexión a propósito (deliberadamente sin service worker): la app depende del Worker y, para episodios grandes, del relevo en todo momento — búsqueda, login, descargas, subidas... — así que no hay nada útil que se pudiera hacer sin internet. Instalarla solo cambia cómo se abre, no qué hace.
Todo lo que guarda PodBridge vive solo en tu navegador — no hay base de datos ni backend propio. Qué se guarda, dónde y por qué:
| Dato | Dónde | Cifrado | Notas |
|---|---|---|---|
| URL del Worker | localStorage |
— | No es sensible |
| URL y secreto del servicio de relevo | localStorage |
— | No es una credencial de Pocket Casts, es configuración tuya — sí se incluye en la exportación de datos |
| Favoritos | localStorage |
— | No es sensible |
| Episodios ya subidos | localStorage |
— | No es sensible |
| Fecha de la última exportación/importación | localStorage |
— | Solo para el aviso de "cambios sin exportar" — nunca sale de este navegador |
| Token de Pocket Casts (sesión activa) | sessionStorage |
— | Se borra al cerrar la pestaña |
| Token de Pocket Casts ("recordarme") | localStorage |
Sí (AES-GCM) | Solo si activas la casilla |
| Contraseña de Pocket Casts | (no se guarda) | — | Ni siquiera cifrada; viaja una vez y se descarta |
"Recordarme en este dispositivo" es opcional y desactivado por
defecto. Al activarlo, el token de sesión (no la contraseña) se cifra con
AES-GCM usando una clave que se genera con Web Crypto y se guarda en
IndexedDB marcada como no exportable: ni la propia app puede sacar
los bytes de la clave, solo pedirle al navegador que cifre o descifre con
ella. Eso protege el token si alguien copia los ficheros de
localStorage/IndexedDB fuera de este navegador — sin la clave, que
nunca sale de tu perfil de navegador, el texto cifrado no sirve de nada.
Lo que esto no protege es un script malicioso corriendo en la propia página (XSS): con acceso de ejecución en el origen, podría pedir al navegador que descifre igual que hace la app. Es defensa en profundidad frente a acceso pasivo a los datos guardados, no una garantía absoluta — por eso sigue siendo opcional, y por eso no se te pide activarlo. En un ordenador compartido, mejor no activarlo — el mismo interruptor sirve para olvidar la sesión recordada en cualquier momento, basta con desactivarlo.
worker/proxy-worker.js expone estos endpoints:
| Endpoint | Qué hace |
|---|---|
GET /health |
Comprobación de vida, la usa la app para el indicador de estado |
GET /ivoox/search?q= |
Busca programas en iVoox |
GET /ivoox/program?url=&page= |
Info de un programa + una página de sus episodios |
GET /ivoox/image?url= |
Retransmite una portada (programa o episodio) |
GET /ivoox/raw?url= |
HTML crudo de una URL de iVoox, solo para depurar el scraper |
POST /pocketcasts/login |
Login contra Pocket Casts, devuelve el token de sesión |
GET /pocketcasts/usage |
Espacio usado/disponible en Archivos (bytes) |
POST /pocketcasts/upload-episode |
Episodios de hasta 100 MB: descarga el audio de iVoox y lo sube a Archivos, del todo en el Worker |
POST /pocketcasts/upload-episode-init |
Episodios de más de 100 MB: solo consigue la URL de subida, para el servicio de relevo (ver "Episodios muy grandes") |
POST /pocketcasts/upload-episode-cancel |
Borra en Pocket Casts el registro de un fichero cuya subida por el relevo ha fallado o se ha cancelado |
POST /pocketcasts/upload-image |
Sube la portada de un episodio ya subido |
El tamaño de cada episodio que se ve en la lista sale de una petición
HEAD en paralelo por episodio (solo mira Content-Length, no descarga
nada) al listar los episodios de un programa — si alguna falla o el CDN no
manda ese encabezado, ese episodio simplemente se queda sin tamaño en vez
de romper el listado entero.
iVoox no tiene API pública. El Worker interpreta directamente el HTML
de sus páginas (dos plantillas distintas: la búsqueda usa microdatos
itemprop, la ficha de programa es una SPA Nuxt con enlaces relativos).
La exclusividad de un episodio se detecta por la clase real de su botón
de reproducción (round-play btn-fans frente a btn-primary), y el mp3
real se resuelve de forma determinista a partir del id del episodio
(listen_mn_<id>_1.mp3), sin necesidad de re-scrapear nada. La lista de
programas Originals se obtiene del catálogo dedicado de iVoox y se cachea
6 horas, porque no hay ninguna marca de "Originals" fiable ni en la ficha
de un programa ni en su tarjeta de búsqueda.
Pocket Casts tampoco tiene API pública. El Worker reproduce el flujo
que usan sus apps oficiales (reverse-engineered): POST /user/login para
el token, y POST /files/upload/request con un mensaje protobuf
(uuid, title, size, contentType, hasCustomImage_p) que devuelve
una URL de S3 pre-firmada donde subir el audio. Si el episodio tiene
portada, hay un segundo paso independiente — POST /files/upload/image
con el mismo uuid — que da otra URL pre-firmada para la imagen; si ese
segundo paso falla no bloquea nada, el episodio se queda sin portada
propia.
Importante: en cuanto /files/upload/request responde, Pocket Casts ya
ha creado el registro del fichero en la cuenta del usuario (se ve en la
app como "Procesando…"), aunque el audio en sí todavía no le haya
llegado. Si la subida falla después de ese punto sin borrar ese
registro, se queda huérfano ahí para siempre — así se descubrió esto en
la práctica: una cuenta llena de duplicados sin terminar de subir nunca,
uno por cada intento fallido. Por eso tanto
handlePocketCastsUploadEpisode como el flujo del relevo externo
llaman a DELETE /files/<uuid> (Files_FileDeleteRequest, campo 1 =
uuid) en cuanto detectan que la subida ha fallado o se ha cancelado —
es limpieza best-effort (nunca bloquea ni tapa el error real) pero evita
que cada reintento deje basura en la cuenta. Si tienes entradas antiguas
de antes de este cambio, bórralas a mano desde la app o la web de Pocket
Casts — son inofensivas (nunca llegaron a tener contenido real), solo
ocupan sitio en la lista.
El audio en sí no llega a pasar por el navegador: /pocketcasts/upload-episode
recibe solo la URL del episodio y el título (una petición pequeña), y es
el propio Worker quien descarga el mp3 de iVoox y lo sube a Pocket Casts,
retransmitiendo en streaming directo de un sitio a otro sin bufferizar
nada en memoria. Esto se decidió a base de descartar dos alternativas más
obvias que se probaron antes y que fallan con episodios largos (~5h,
260-300 MB):
- Navegador descarga y se lo reenvía al Worker para que lo suba. Cloudflare Workers limita a 100 MB el cuerpo de las peticiones que RECIBE (plan gratuito/Pro) — se atascaba justo al llegar a esa fase.
- Navegador sube directo a la URL pre-firmada de Pocket Casts, sin Worker de por medio (que es justo lo que hace la app oficial: el dispositivo sube el fichero directo al almacenamiento). Bloqueado por CORS — ese almacenamiento no está pensado para que le hable un navegador, solo las apps nativas, que no tienen ese problema porque CORS es una restricción exclusiva de navegadores.
Haciendo la descarga+subida del todo dentro del Worker se evita el primer límite (que solo aplica a lo que el Worker recibe, no a las peticiones que él mismo hace hacia fuera) y el problema de CORS no existe entre servidores. Quedaban tres detalles nada evidentes por resolver dentro de esto, cada uno descubierto al chocar con el siguiente episodio de prueba grande (~260-270 MB):
- Encadenar directamente el cuerpo de la respuesta de iVoox como cuerpo
de la petición a Pocket Casts (
body: audioRes.body) provoca un 413 aleatorio en ficheros grandes. - Intercalar un
TransformStreamnormal en medio arregla el 413, pero el runtime de Workers manda entonces el cuerpo comoTransfer-Encoding: chunkeden cuanto es un stream de longitud desconocida — y las URLs pre-firmadas de S3 (que es lo que hay detrás de Pocket Casts) no soportan chunked, lo rechazan con un 501.FixedLengthStream(unTransformStreamal que se le dice de antemano cuántos bytes va a dejar pasar) arregla esto: el runtime lo usa comoContent-Lengthreal en vez de recurrir a chunked. - Con eso arreglado, conectar el readable de iVoox con el writable de
FixedLengthStreammediante.pipeTo()(la forma "normal" de unir dos streams) seguía rompiendo con ficheros grandes — no con un error HTTP limpio, sino tirando la conexión entera a medias, algo que en el navegador se ve como un CORS/Failed to fetchespurio porque nunca llega a haber una respuesta real. La sospecha es que el runtime de Workers no propaga bien la contrapresión al encadenar streams conpipeTo(), así que se sustituyó por un bombeo manual (un bucle que espera a que cada escritura se resuelva antes de pedir el siguiente trozo) — mejora las cosas pero no las arregla del todo: ver "Episodios muy grandes" más abajo.
La contrapartida de todo este diseño es que el navegador ya no puede pintar un progreso real en bytes durante esta fase (es una única petición de principio a fin): la UI la muestra como indeterminada en vez de fingir un porcentaje. La portada, que siempre es pequeña, sigue subiendo desde el navegador con progreso real.
Con episodios de varias horas (~250-300 MB) la subida automática de
servidor a servidor desde el Worker de Cloudflare no es fiable. Se
probaron varios ajustes de streaming sucesivos (TransformStream,
FixedLengthStream, bombeo manual respetando contrapresión, agrupar los
trozos en bloques más grandes antes de escribir) y todos fallaban en el
mismo punto o muy cerca. El diagnóstico definitivo llegó con
wrangler tail en producción: la respuesta del PUT fallido traía
server: cloudflare y una cabecera cf-ray, con el cuerpo vacío — es
decir, la propia red de Cloudflare corta la petición saliente antes de
que llegue a Pocket Casts, no un rechazo de Pocket Casts ni un fallo de
nuestro código. Cloudflare Workers limita a 100 MB el cuerpo de las
peticiones en las que participa, y esto confirma que el límite aplica
también a lo que un Worker manda hacia fuera con fetch(), no solo a lo
que recibe — ningún ajuste de streaming dentro del Worker puede evitarlo
porque el corte pasa en la red de Cloudflare, no en el código.
Por eso, para episodios de más de LARGE_EPISODE_BYTES (100 MB, en
js/utils.js) el audio se sube por una vía distinta que no pasa por
Cloudflare en ningún momento. Esa decisión se toma con isLargeEpisode()
(también en js/utils.js): normalmente por el tamaño exacto que el
Worker obtuvo con un HEAD al listar el programa, pero si ese HEAD
falla o la CDN no da Content-Length — pasa más a menudo justo en
episodios muy largos, los que más importa clasificar bien — se estima a
partir de la duración en vez de asumir "sin tamaño = pequeño", que es lo
que hacía antes y colaba episodios grandes por la vía del Worker con un
413 a media subida.
- El navegador le pide al Worker una URL de subida ya autorizada
(
POST /pocketcasts/upload-episode-init) — una petición pequeña, solo metadatos: el Worker resuelve la URL real del mp3, consulta su tamaño con unHEAD(sin descargar nada) y le pide a Pocket Casts una URL de subida pre-firmada, igual que para un episodio normal. - El navegador le pasa esa URL (más la del mp3 de iVoox) a un servicio de relevo aparte, desplegado fuera de Cloudflare (ver más abajo) — es él quien hace el streaming real de iVoox a Pocket Casts, en una plataforma con un servidor HTTP normal detrás, sin el límite de tamaño de petición que tiene Cloudflare Workers.
Ese servicio nunca ve tu email, tu contraseña ni tu token de Pocket Casts — solo la URL pública del mp3 en iVoox y una URL de subida ya autorizada y de un solo uso. Es opcional: sin configurarlo, los episodios grandes simplemente no se pueden subir. Con la subida normal (episodios de hasta 100 MB) esto no cambia nada — sigue yendo por el Worker, tal y como se explica arriba.
El código está en /relay-service (Node normal, sin dependencias de
npm). Pensado para Render.com, que tiene un plan
gratuito con despliegue automático desde GitHub:
- En el dashboard de Render, New → Web Service y conecta tu fork de este repositorio.
- Root Directory:
relay-service. - Runtime: Node. Build Command:
npm install(Render exige algo en este campo, aunque no hay ninguna dependencia que instalar). Start Command:npm start. - Instance Type: Free.
- En Environment, añade dos variables:
RELAY_SECRET: una cadena aleatoria larga que te inventes (por ejemplo, generada conopenssl rand -hex 32) — protege el servicio para que no lo pueda usar cualquiera que descubra la URL.ALLOWED_ORIGIN: la URL de tu GitHub Pages (p. ej.https://tu-usuario.github.io).
- Despliega. Cuando termine, Render te da una URL tipo
https://tu-relay.onrender.com. - En PodBridge → Ajustes → "Relevo para episodios grandes", pega esa
URL y el mismo
RELAY_SECRETque le pusiste a Render.
El plan gratuito de Render "duerme" el servicio tras ~15 minutos sin uso — la primera petición después de eso tarda unos 30-50 segundos en arrancar (el navegador esperará ese tiempo en la fase de subida antes de que empiece a moverse nada; no hace falta hacer nada especial, solo tener paciencia esa primera vez). Si prefieres no esperar a mitad de una subida, el botón "Despertar ahora" junto al de Guardar hace un ping y avisa de si estaba dormido o ya activo — útil pulsarlo un minuto antes de ponerte con un episodio largo.
Como ninguna de las dos es una API pública documentada, pueden cambiar
sin aviso. Si algo deja de funcionar (títulos raros, episodios que no
aparecen, login o subida rotos), el sitio donde mirar es siempre
worker/proxy-worker.js:
- Para iVoox: abre
https://TU-WORKER.workers.dev/ivoox/raw?url=<la-url-que-falla>para ver el HTML real que ve el Worker, y ajustaparseProgramCards/parseEpisodeCards/resolveAudioUrlsegún lo que haya cambiado. - Para Pocket Casts: la sección "Pocket Casts" del archivo, con el login y las dos subidas (audio e imagen).
- Scraping best-effort. No hay contrato estable con iVoox: un cambio en su web puede romper la búsqueda o la lista de episodios hasta que se actualice el Worker (ver sección anterior).
- Solo contenido público. Los episodios exclusivos (Premium/Fans) no se pueden descargar, a propósito.
- El orden de "Episodios favoritos" es aproximado. iVoox no da una fecha exacta en el HTML, solo texto relativo ("Hoy", "5 días", "2 semanas"...) que se convierte a una fecha aproximada para poder ordenar episodios de programas distintos entre sí — de sobra para ver qué es más reciente, pero no una fecha exacta al día.
- Cada usuario necesita su propio Worker. No es una limitación técnica sino de diseño: así ningún dato ni credencial pasa por un servidor compartido.
- Límites del plan gratuito de Cloudflare Workers: 100.000 peticiones al día y un tope de tiempo de CPU activo por petición (no de tiempo de espera de red, que es la mayor parte de lo que hace este Worker). De sobra para uso personal; si algún día se superan, Cloudflare empieza a devolver error hasta el día siguiente.
- Riesgo de bloqueo o incumplimiento de términos de servicio. Tanto raspar la web de iVoox como hablar la API privada de Pocket Casts probablemente incumple los términos de uso de ambos servicios, aunque el uso sea personal y legítimo. Un uso intensivo (muchas búsquedas o descargas seguidas) aumenta el riesgo de que iVoox limite o bloquee el tráfico.
- La subida de Archivos a Pocket Casts requiere una suscripción Pocket Casts Plus. Sin ella, Pocket Casts rechaza la solicitud.
- Sin progreso en bytes durante la subida del audio. Al hacerse entera dentro del Worker (o del servicio de relevo, para episodios grandes), esa fase se muestra como indeterminada en vez de un porcentaje — sí se cancela igual (a mano o sola a los 45s sin señal de vida) y tiene un límite de seguridad de 15 minutos.
- El servicio de relevo para episodios grandes es opcional y es una pieza más que mantener. Sin desplegarlo, esos episodios (más de 100 MB) no se pueden subir. Desplegado en el plan gratuito de Render, además "duerme" tras ~15 min sin uso y la primera petición después tarda 30-50s en arrancar.
- Persistencia solo local, sin sincronizar. Favoritos, historial de
subidas y la sesión recordada viven en
localStorage/IndexedDBde este navegador — no se sincronizan entre dispositivos ni navegadores; en cada uno hay que marcar los favoritos y activar "recordarme" por separado. Borrar los datos del sitio en el navegador los elimina.
index.html Esqueleto de la SPA (una sola página)
css/styles.css Diseño (claro/oscuro automático, responsive)
manifest.webmanifest Metadatos de instalación como PWA
icons/ Iconos de la app (fuente .svg + .png generados)
js/main.js Cableado de la UI y orquestación de vistas
js/state.js Store (búsqueda, programa, sesión, favoritos, historial, jobs)
js/crypto-store.js Cifrado AES-GCM del token recordado (clave no exportable en IndexedDB)
js/download.js Flujo descargar → subir por episodio
js/wakelock.js Mantiene la pantalla encendida mientras hay algo en curso
js/utils.js Formateo, helpers varios
js/api/ivoox.js Cliente contra los endpoints /ivoox/* del Worker
js/api/pocketcasts.js Cliente contra los endpoints /pocketcasts/* del Worker
js/api/relay.js Cliente del servicio de relevo (episodios grandes)
js/api/proxy.js fetch genérico + comprobación de salud del Worker
js/components/cards.js Tarjetas de programa/episodio, botones de acción
js/components/episodeModal.js Popup de "más información" de un episodio
js/components/overlay.js Apertura/cierre de paneles y popups
js/components/states.js Estados de carga, vacío y error
js/components/toast.js Notificaciones flotantes
worker/proxy-worker.js El puente: scraping de iVoox + protobuf de Pocket Casts
worker/wrangler.toml Configuración de despliegue del Worker
relay-service/server.js Streaming de audio para episodios grandes (fuera de Cloudflare)
relay-service/package.json Sin dependencias — solo Node 18+
.github/workflows/deploy.yml Publicación automática en GitHub Pages
MIT.
PodBridge es una herramienta personal, no oficial y sin afiliación con iVoox ni con Pocket Casts. Solo descarga audio de acceso público en iVoox — los episodios exclusivos quedan bloqueados a propósito — y solo sube contenido a la cuenta de Pocket Casts con la que te identifiques tú mismo. Úsala solo con tu propia cuenta y con contenido al que ya tengas acceso legítimo.