Skip to content

Instantly share code, notes, and snippets.

@minanagehsalalma
Created June 5, 2026 18:34
Show Gist options
  • Select an option

  • Save minanagehsalalma/9b6ac3682e7c37cf311731edf74bb010 to your computer and use it in GitHub Desktop.

Select an option

Save minanagehsalalma/9b6ac3682e7c37cf311731edf74bb010 to your computer and use it in GitHub Desktop.
Scrapes YouTube and video.js embeds on any page and injects duration + metadata cards beneath each player
/**
* yt-duration-scraper.js
*
* Scrapes all YouTube / video.js embeds on any page,
* injects an info card beneath each player showing:
* - video title
* - video URL
* - duration (fetched via the official YT IFrame API — no API key needed)
* - type
* - current page URL
*
* Usage: paste into browser console, or save as a bookmarklet.
*
* Bookmarklet: wrap the whole thing in:
* javascript:(function(){...})();
* and save as a browser bookmark.
*
* Works on any site that embeds YouTube videos via:
* - video.js (data-setup / data-setup-lazy attributes)
* - plain <iframe src="youtube.com/embed/...">
* - <div data-videoid="..."> (common in CMSes)
* - anchor tags pointing to youtube.com/watch?v=...
*/
(function () {
const pageUrl = window.location.href;
// ─── Helpers ────────────────────────────────────────────────────────────────
function formatDuration(seconds) {
if (!seconds || isNaN(seconds)) return 'Unknown';
const h = Math.floor(seconds / 3600);
const m = Math.floor((seconds % 3600) / 60);
const s = Math.floor(seconds % 60);
return [
h ? String(h) : null,
String(m).padStart(h ? 2 : 1, '0'),
String(s).padStart(2, '0')
].filter(Boolean).join(':');
}
function extractVideoId(url) {
if (!url) return null;
return (
url.match(/[?&]v=([a-zA-Z0-9_-]{11})/) ?.[1] || // watch?v=
url.match(/\/embed\/([a-zA-Z0-9_-]{11})/) ?.[1] || // /embed/
url.match(/\/shorts\/([a-zA-Z0-9_-]{11})/) ?.[1] || // /shorts/
url.match(/youtu\.be\/([a-zA-Z0-9_-]{11})/)?.[1] || // youtu.be/
null
);
}
// ─── YouTube IFrame API ──────────────────────────────────────────────────────
function loadYTApi() {
return new Promise((resolve) => {
if (window.YT && window.YT.Player) return resolve();
// If API is already loading, wait for the existing callback
const existing = window.onYouTubeIframeAPIReady;
window.onYouTubeIframeAPIReady = () => {
if (existing) existing();
resolve();
};
if (!document.querySelector('script[src*="youtube.com/iframe_api"]')) {
const tag = document.createElement('script');
tag.src = 'https://www.youtube.com/iframe_api';
document.head.appendChild(tag);
}
});
}
function getDurationViaYTApi(videoId) {
return new Promise((resolve) => {
const container = document.createElement('div');
container.style.cssText =
'position:fixed;top:-9999px;left:-9999px;width:1px;height:1px;';
document.body.appendChild(container);
let resolved = false;
const timeout = setTimeout(() => {
if (!resolved) { resolved = true; cleanup(); resolve('Unavailable'); }
}, 12000);
function cleanup() {
clearTimeout(timeout);
try { player.destroy(); } catch {}
container.remove();
}
function done(dur) {
if (!resolved) { resolved = true; cleanup(); resolve(dur); }
}
let player;
try {
player = new YT.Player(container, {
width: 1,
height: 1,
videoId,
playerVars: { autoplay: 1, mute: 1, controls: 0, playsinline: 1 },
events: {
onReady(e) {
const dur = e.target.getDuration();
if (dur > 0) done(formatDuration(dur));
},
onStateChange(e) {
// YT.PlayerState.PLAYING === 1
if (e.data === 1) {
const dur = e.target.getDuration();
if (dur > 0) done(formatDuration(dur));
}
},
onError() { done('Error'); }
}
});
} catch {
clearTimeout(timeout);
container.remove();
resolve('Error');
}
});
}
// ─── Card UI ─────────────────────────────────────────────────────────────────
function createCard({ index, title, videoUrl, type }) {
const card = document.createElement('div');
card.className = '__video-info-card';
card.style.cssText = `
background: #1e1e2e;
color: #cdd6f4;
font-family: monospace;
font-size: 12px;
border-radius: 0 0 8px 8px;
border-top: 3px solid #cba6f7;
padding: 10px 14px;
margin-bottom: 8px;
box-sizing: border-box;
`;
card.innerHTML = `
<div style="color:#89b4fa;font-weight:bold;margin-bottom:6px;">
🎬 #${index} — ${title}
</div>
<div style="margin-bottom:4px;">
<span style="color:#a6e3a1;">▶ Video: </span>
<a href="${videoUrl}" target="_blank"
style="color:#f9e2af;word-break:break-all;">${videoUrl}</a>
</div>
<div style="margin-bottom:4px;">
<span style="color:#a6e3a1;">⏱ Duration: </span>
<span class="__duration-value" style="color:#fab387;">fetching...</span>
</div>
<div style="margin-bottom:4px;">
<span style="color:#a6e3a1;">📄 Type: </span>
<span>${type}</span>
</div>
<div>
<span style="color:#a6e3a1;">🔗 Page: </span>
<a href="${pageUrl}" target="_blank"
style="color:#cba6f7;word-break:break-all;">${pageUrl}</a>
</div>
`;
return card;
}
// ─── Player Detection ────────────────────────────────────────────────────────
function collectPlayers() {
const seen = new Set(); // deduplicate by videoId
const players = []; // { videoId, title, videoUrl, type, insertAfter }
function add({ videoId, title, videoUrl, type, insertAfter }) {
if (!videoId || seen.has(videoId)) return;
seen.add(videoId);
players.push({ videoId, title, videoUrl, type, insertAfter });
}
// 1. video.js — data-setup-lazy
document.querySelectorAll('[data-setup-lazy]').forEach((el) => {
try {
const config = JSON.parse(el.getAttribute('data-setup-lazy'));
(config.sources || []).forEach(({ src, type }) => {
const videoId = extractVideoId(src);
if (videoId) add({
videoId, type,
title: el.getAttribute('title') || el.getAttribute('aria-label') || 'Unknown',
videoUrl: src,
insertAfter: el
});
});
} catch {}
});
// 2. video.js — data-setup (non-lazy variant)
document.querySelectorAll('[data-setup]').forEach((el) => {
try {
const config = JSON.parse(el.getAttribute('data-setup'));
(config.sources || []).forEach(({ src, type }) => {
const videoId = extractVideoId(src);
if (videoId) add({
videoId, type,
title: el.getAttribute('title') || el.getAttribute('aria-label') || 'Unknown',
videoUrl: src,
insertAfter: el
});
});
} catch {}
});
// 3. Plain YouTube iframes
document.querySelectorAll('iframe[src*="youtube"]').forEach((el) => {
const videoId = extractVideoId(el.getAttribute('src'));
if (!videoId) return;
// Skip if already inside a video.js container caught above
if (el.closest('[data-setup-lazy],[data-setup]')) return;
add({
videoId,
type: 'video/youtube',
title: el.getAttribute('title') || 'Unknown',
videoUrl: `https://www.youtube.com/watch?v=${videoId}`,
insertAfter: el
});
});
// 4. data-videoid divs (used by some CMSes / LMSes)
document.querySelectorAll('[data-videoid]').forEach((el) => {
const videoId = el.getAttribute('data-videoid');
if (!extractVideoId(`?v=${videoId}`)) return;
add({
videoId,
type: 'video/youtube',
title: el.getAttribute('title') || el.getAttribute('aria-label') || 'Unknown',
videoUrl: `https://www.youtube.com/watch?v=${videoId}`,
insertAfter: el
});
});
// 5. Anchor tags linking to YouTube (e.g. linked thumbnails)
document.querySelectorAll('a[href*="youtube.com/watch"],a[href*="youtu.be"]').forEach((el) => {
const videoId = extractVideoId(el.getAttribute('href'));
if (!videoId) return;
add({
videoId,
type: 'video/youtube (link)',
title: el.getAttribute('title') || el.textContent.trim().slice(0, 60) || 'Unknown',
videoUrl: `https://www.youtube.com/watch?v=${videoId}`,
insertAfter: el
});
});
return players;
}
// ─── Main ────────────────────────────────────────────────────────────────────
async function run() {
// Clean up previous run
document.querySelectorAll('.__video-info-card').forEach(el => el.remove());
const players = collectPlayers();
if (players.length === 0) {
console.warn('[yt-scraper] No YouTube videos found on this page.');
return;
}
console.log(`[yt-scraper] Found ${players.length} video(s). Loading YT API...`);
// Inject cards immediately (with "fetching..." placeholder)
const tasks = players.map((p, i) => {
const card = createCard({
index: i + 1,
title: p.title,
videoUrl: p.videoUrl,
type: p.type
});
p.insertAfter.parentNode?.insertBefore(card, p.insertAfter.nextSibling);
return { card, videoId: p.videoId };
});
// Load YT API then fetch durations one by one
await loadYTApi();
console.log('[yt-scraper] YT API ready. Fetching durations...');
for (const { card, videoId } of tasks) {
const duration = await getDurationViaYTApi(videoId);
console.log(`[yt-scraper] ${videoId} → ${duration}`);
const span = card.querySelector('.__duration-value');
if (span) span.textContent = duration;
}
console.log(`[yt-scraper] ✅ Done — ${tasks.length} video(s) processed.`);
}
run();
})();
@minanagehsalalma

minanagehsalalma commented Jun 5, 2026

Copy link
Copy Markdown
Author

Ex
ExampleOutput

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment