Consigue la lista de todos los vídeos que ha subido un canal (título, duración y enlace) en CSV o texto. Gratis, en tu navegador, sin clave de API.
Abre la lista de subidas del canal (mira los pasos de abajo) y ejecuta allí el script.
▶ Mira cómo funcionaVídeo tutorial de 1 minuto (en inglés)
// Responsive Muse · YouTube Playlist Exporter · v1.2 · 2026-11-06
// Exports any playlist you can open (your own, Watch Later, Liked videos, public lists,
// a channel's uploads or its Shorts) to CSV or TXT. Runs only on youtube.com/playlist.
// The script makes no network requests of its own: YouTube loads the rest of the list
// while the script scrolls. It never reads cookies, passwords or tokens.
(async () => {
const TOOL = 'Exportar lista de reproducción';
const FORMAT = 'csv'; // 'csv' (Excel, Google Sheets) or 'txt' (one URL per line)
if (location.hostname !== 'www.youtube.com' || location.pathname !== '/playlist') {
alert('Abre primero una lista de reproducción, por ejemplo:\nhttps://www.youtube.com/playlist?list=WL');
return;
}
// A small box on the page shows progress and asks before changing anything,
// so you never need to read or edit the console.
const many = (n, word) => `${n} ${n === 1 ? word : ({ canal: 'canales', 'canción': 'canciones', 'publicación': 'publicaciones', 'elemento guardado': 'elementos guardados', lugar: 'lugares' })[word] || word + 's'}`; // 1 vídeo, 2 vídeos
const panel = (() => {
const box = document.createElement('div');
box.style.cssText = 'display:block!important;position:fixed;right:16px;bottom:16px;z-index:2147483647;width:340px;max-width:calc(100vw - 32px);' +
'background:#111318;color:#e6e8ee;font:14px/1.5 system-ui,sans-serif;border-radius:12px;padding:16px;' +
'box-shadow:0 8px 30px rgba(0,0,0,.4);text-align:left';
const head = document.createElement('strong');
head.textContent = TOOL;
const msg = document.createElement('p');
msg.style.cssText = 'margin:8px 0 12px;white-space:pre-line';
const row = document.createElement('div');
row.style.cssText = 'display:flex;flex-wrap:wrap;gap:8px;justify-content:flex-end';
box.append(head, msg, row);
document.body.append(box);
const button = (label, primary, onClick) => {
const b = document.createElement('button');
b.textContent = label;
b.style.cssText = 'font:600 14px system-ui,sans-serif;padding:8px 14px;border-radius:8px;cursor:pointer;' +
(primary ? 'background:#0f766e;color:#fff;border:1px solid #0f766e' : 'background:none;color:#e6e8ee;border:1px solid #3a3f4b');
b.onclick = onClick;
row.append(b);
};
return {
text(t) { msg.textContent = t; },
ask(t, yes) {
msg.textContent = t;
row.replaceChildren();
return new Promise(r => {
button('Cancelar', false, () => { row.replaceChildren(); r(false); });
button(yes, true, () => { row.replaceChildren(); r(true); });
});
},
stoppable(onStop) { row.replaceChildren(); button('Parar', false, onStop); },
actions(t, list) {
msg.textContent = t;
row.replaceChildren();
list.forEach(([label, primary, run]) => button(label, primary, run));
},
finish(t) { this.actions(t, [['Cerrar', true, () => box.remove()]]); },
close() { box.remove(); },
};
})();
// Offer the result as a downloaded file or as clipboard text (a real click, so it always works)
const deliver = (summary, body, file) => {
const save = () => {
const a = document.createElement('a');
a.href = URL.createObjectURL(new Blob([/\.csv$/.test(file) ? '\ufeff' + body : body], { type: 'text/plain;charset=utf-8' }));
a.download = file;
a.click();
setTimeout(() => URL.revokeObjectURL(a.href), 5000);
panel.text(`${summary}\nGuardado como ${file} (mira tu carpeta de Descargas).`);
};
const copy = () => navigator.clipboard.writeText(body)
.then(() => panel.text(`${summary}\nCopiado. Pégalo en una hoja de cálculo o en una nota.`))
.catch(() => panel.text(`${summary}\nEl navegador ha bloqueado el portapapeles: usa Descargar.`));
panel.actions(summary, [['Cerrar', false, () => panel.close()], ['Copiar', false, copy], ['Descargar', true, save]]);
};
const sleep = ms => new Promise(r => setTimeout(r, ms));
// Playlist rows: new layout (yt-lockup-view-model), old layout (ytd-playlist-video-renderer)
// and the Shorts grid of a channel's Shorts list (ytm-shorts-lockup-view-model, links to /shorts/ID)
const rows = () => [...document.querySelectorAll('yt-lockup-view-model, ytd-playlist-video-renderer, ytm-shorts-lockup-view-model')]
.filter(el => el.querySelector('a[href*="/watch?v="][href*="index="], a[href*="/shorts/"]'));
// Channel lists (UU… uploads, UUSH… Shorts) don't show the channel on every row: use the list owner
const owner = /^UU/.test(new URLSearchParams(location.search).get('list') || '')
? ([...document.querySelectorAll('a[href^="/@"]')]
.find(a => a.textContent.trim() && !a.closest('yt-lockup-view-model, ytd-playlist-video-renderer, ytm-shorts-lockup-view-model, #guide, ytd-guide-renderer'))
?.textContent.trim() || '')
: '';
// 1. Scroll to the end until no new videos appear three times in a row
let last = -1, idle = 0;
while (idle < 3) {
window.scrollTo(0, document.documentElement.scrollHeight);
await sleep(1200);
const n = rows().length;
idle = n === last ? idle + 1 : 0;
last = n;
panel.text(`Cargando la lista… ${n} vídeos`);
}
// 2. Read every row
const seen = new Set();
const videos = [];
for (const el of rows()) {
const short = el.querySelector('a[href*="/shorts/"]');
const link = new URL((short || el.querySelector('a[href*="/watch?v="]')).href);
const id = short ? link.pathname.split('/')[2] : link.searchParams.get('v');
const index = short ? String(videos.length + 1) : link.searchParams.get('index') || '';
if (!id || seen.has(id + index)) continue;
seen.add(id + index);
const text = sel => (el.querySelector(sel)?.textContent || '').trim();
videos.push({
index,
title: short ? (el.querySelector('a[title]')?.getAttribute('title') || short.textContent.trim())
: el.querySelector('h3')?.getAttribute('title') || text('#video-title'),
channel: text('a[href^="/@"], a[href^="/channel/"], ytd-channel-name a') || owner,
duration: short ? 'Short' : text('.ytBadgeShapeText, ytd-thumbnail-overlay-time-status-renderer #text'),
url: short ? 'https://www.youtube.com/shorts/' + id : 'https://www.youtube.com/watch?v=' + id,
});
}
if (!videos.length) {
panel.finish('No se han encontrado vídeos. Puede que YouTube haya cambiado su diseño: avísanos en responsive-muse.com.');
return 0;
}
// 3. Build the file and offer it
const cell = v => '"' + String(v).replace(/"/g, '""') + '"';
const body = FORMAT === 'txt'
? videos.map(v => v.url).join('\n')
: ['Posición,Título,Canal,Duración,URL', ...videos.map(v =>
[v.index, v.title, v.channel, v.duration, v.url].map(cell).join(','))].join('\n');
const name = (document.title.replace(/ - YouTube$/, '') || 'playlist').replace(/[\\/:*?"<>|]+/g, '_');
deliver(`Listo: ${many(videos.length, 'vídeo')}.`, body, `${name}.${FORMAT}`);
return videos.length;
})();
🛡️ ¿Chrome te dice «No pegues código…»? Es normal. Escribe allow pasting en la consola (o permitir pegar si tu Chrome está en español), pulsa Intro y vuelve a pegar. Solo hace falta una vez.
¿Por qué avisa Chrome?
Chrome lo muestra la primera vez que alguien pega algo en la consola, para proteger de estafas en las que te piden pegar código desconocido. Hace bien en avisar, y por eso este script está entero a la vista: 131 líneas comentadas que no envían nada a ningún sitio ni tocan tus contraseñas o cookies. Por qué es seguro.
¿Prefieres no usar la consola? Arrastra el botón Exportar vídeos de un canal a tu barra de marcadores y haz clic en él estando en www.youtube.com. Así no sale el aviso.
Marcador: arrastra el botón del medio a tu barra de marcadores, abre www.youtube.com y pulsa el marcador. Ejecuta el mismo script con las opciones de arriba.
- No envía nada a ningún sitio. Sin fetch, XHR ni scripts externos. La única actividad de red es la de www.youtube.com cargando tu lista.
- No toca tu sesión. No lee cookies, contraseñas, tokens ni el almacenamiento del navegador.
- Solo funciona en www.youtube.com. En cualquier otra web se detiene antes de hacer nada.
- Corto y legible. 131 líneas comentadas, sin minificar ni ofuscar.
- Muestra su progreso en la página. Un pequeño recuadro te dice qué está haciendo y al final te ofrece Descargar o Copiar.
- El mismo código para todos. SHA-256 de la versión por defecto:
a5a9d17bc923fc5a…
Compruébalo tú mismo en 30 segundos
- Léelo: todas las líneas están arriba, con comentarios.
- Busca en el código
fetch,XMLHttpRequest,cookie,localStorageoeval. No aparece ninguna. - Abre la pestaña Red (Network) antes de ejecutarlo: solo verás peticiones a www.youtube.com.
Cómo usarlo
- Busca el ID del canal: abre el canal, pulsa Más información → Compartir canal → Copiar ID del canal. Empieza por
UC. - Cambia las dos primeras letras
UCporUUy abreyoutube.com/playlist?list=UU…: es la lista de «todas las subidas» del canal. - Abre la consola: F12 o Ctrl+Mayús+J (Cmd+Opción+J en Mac). Si Chrome te lo pide, escribe
permitir pegar(oallow pastingsi lo tienes en inglés) y pulsa Enter. - Elige las opciones, copia el script, pégalo en la consola y pulsa Enter. Al terminar aparece un recuadro: pulsa Descargar (o Copiar).
¿Por qué la lista de subidas?
La pestaña Vídeos de un canal separa vídeos, Shorts y directos, y los carga por páginas. La lista de subidas los tiene todos en una sola lista ordenada que el script puede leer entera.
Preguntas frecuentes
¿Cómo abro la lista de subidas de un canal?
El ID de todo canal empieza por UC. Cambia UC por UU y abre youtube.com/playlist?list=UU… Esa lista contiene todos los vídeos subidos por el canal.
¿Funciona con los Shorts?
Sí. La lista de subidas (UU…) ya incluye los Shorts del canal. Para exportar solo los Shorts, cambia UC por UUSH y abre youtube.com/playlist?list=UUSH… Cada Short sale con su enlace youtube.com/shorts/.
