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