Created
June 5, 2026 18:34
-
-
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /** | |
| * 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(); | |
| })(); |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Ex
