// TOTAL DURATION CALCULATOR FOR YOUTUBE PLAYLISTS
Calculate the total length of any YouTube playlist. Paste a playlist URL or ID to instantly see the full runtime at normal and faster playback speeds — no login required.
// supports full URLs, short URLs, and bare playlist IDs (PLxxx…)
Standalone, zero-dependency JavaScript implementation powering this tool. Free to inspect, copy, and build upon.
// ── helpers ──────────────────────────────────────────────────────────────
function extractPlaylistId(input) {
input = input.trim();
const patterns = [
/[?&]list=([a-zA-Z0-9_-]+)/,
/^(PL[a-zA-Z0-9_-]+)$/,
/^([a-zA-Z0-9_-]{34})$/, // bare ID
];
for (const p of patterns) {
const m = input.match(p);
if (m) return m[1];
}
return null;
}
function parseDuration(iso) {
// ISO 8601 duration: PT#H#M#S
const m = iso.match(/PT(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)S)?/);
if (!m) return 0;
return (parseInt(m[1]||0)*3600) + (parseInt(m[2]||0)*60) + parseInt(m[3]||0);
}
function fmtTime(secs) {
const h = Math.floor(secs / 3600);
const m = Math.floor((secs % 3600) / 60);
const s = secs % 60;
return { h, m, s };
}
function pad(n) { return String(n).padStart(2,'0'); }
function setStatus(msg, spin=false) {
const el = document.getElementById('status');
el.innerHTML = spin ? `<span class="spinner"></span>${msg}` : msg;
}
// ── API fetch ─────────────────────────────────────────────────────────────
async function fetchAllItems(apiKey, playlistId) {
let items = [], pageToken = '';
do {
const url = `https://www.googleapis.com/youtube/v3/playlistItems`
+ `?part=contentDetails&maxResults=50&playlistId=${playlistId}`
+ `&key=${apiKey}${pageToken ? '&pageToken='+pageToken : ''}`;
const r = await fetch(url);
const d = await r.json();
if (d.error) throw new Error(d.error.message);
const ids = d.items.map(i => i.contentDetails.videoId).join(',');
// fetch durations
const vr = await fetch(
`https://www.googleapis.com/youtube/v3/videos`
+ `?part=contentDetails&id=${ids}&key=${apiKey}`
);
const vd = await vr.json();
if (vd.error) throw new Error(vd.error.message);
vd.items.forEach(v => items.push({
id: v.id,
duration: parseDuration(v.contentDetails.duration)
}));
pageToken = d.nextPageToken || '';
setStatus(`Fetched ${items.length} videos…`, true);
} while (pageToken);
return items;
}
// ── main ──────────────────────────────────────────────────────────────────
async function calculate() {
const rawUrl = document.getElementById('urlInput').value.trim();
const apiKey = document.getElementById('apiKey').value.trim();
const btn = document.getElementById('calcBtn');
const resultsEl = document.getElementById('results');
resultsEl.style.display = 'none';
resultsEl.innerHTML = '';
if (!rawUrl) { setStatus('// paste a playlist URL or ID'); return; }
if (!apiKey) { showError('API key required. Get one free at console.cloud.google.com → YouTube Data API v3.'); return; }
const listId = extractPlaylistId(rawUrl);
if (!listId) { showError('Could not parse a playlist ID from that URL. Try pasting the full URL.'); return; }
btn.disabled = true;
setStatus('Connecting to YouTube…', true);
try {
const items = await fetchAllItems(apiKey, listId);
if (!items.length) { showError('Playlist appears empty or is private.'); btn.disabled=false; return; }
const totalSecs = items.reduce((a,v) => a + v.duration, 0);
const avgSecs = Math.round(totalSecs / items.length);
renderResults(listId, items.length, totalSecs, avgSecs);
setStatus(`// done — ${items.length} videos analysed`);
} catch(e) {
showError(e.message);
}
btn.disabled = false;
}
function renderResults(listId, count, totalSecs, avgSecs) {
const {h,m,s} = fmtTime(totalSecs);
const resultsEl = document.getElementById('results');
// speeds
const speeds = [1, 1.25, 1.5, 1.75, 2];
const speedCards = speeds.map(sp => {
const secs = Math.round(totalSecs / sp);
const {h:sh,m:sm} = fmtTime(secs);
return `<div class="speed-card">
<span class="speed-label">At ${sp}×</span>
<span class="speed-value">${sh}h ${pad(sm)}m</span>
<span class="speed-sub">${fmtSecsNice(secs)}</span>
</div>`;
});
// avg
const {h:ah,m:am,s:as_} = fmtTime(avgSecs);
const avgLabel = ah ? `${ah}h ${pad(am)}m ${pad(as_)}s` : `${am}m ${pad(as_)}s`;
// "if you watched 1hr/day" readout
const daysAt1hr = (totalSecs / 3600).toFixed(1);
const daysAt2hr = (totalSecs / 7200).toFixed(1);
resultsEl.innerHTML = `
<div class="result-header">
<span class="result-title">TOTAL DURATION</span>
<span class="video-count">${count} VIDEOS</span>
</div>
<div class="time-block">
<div class="time-unit">
<div class="time-value">${pad(h)}</div>
<div class="time-label">Hours</div>
</div>
<div class="time-unit">
<div class="time-value">${pad(m)}</div>
<div class="time-label">Minutes</div>
</div>
<div class="time-unit">
<div class="time-value">${pad(s)}</div>
<div class="time-label">Seconds</div>
</div>
</div>
<div class="speed-grid" style="margin-bottom:10px">
${speedCards.join('')}
<div class="speed-card">
<span class="speed-label">Avg per video</span>
<span class="speed-value" style="font-size:20px">${avgLabel}</span>
<span class="speed-sub">per video average</span>
</div>
</div>
<div class="progress-wrap">
<div class="progress-meta">
<span>BINGE-WATCH ESTIMATE</span>
<span>${daysAt1hr} days @ 1hr/day</span>
</div>
<div class="progress-bar-bg">
<div class="progress-bar-fill" id="progressFill" style="width:0%"></div>
</div>
<div class="progress-meta" style="margin-top:10px;margin-bottom:0">
<span style="color:var(--text)">${daysAt2hr} days @ 2hr/day</span>
<span>${fmtSecsNice(totalSecs)} total</span>
</div>
</div>
`;
resultsEl.style.display = 'block';
// animate progress bar — maps hours logarithmically for visual flair
setTimeout(() => {
const pct = Math.min(100, (h / Math.max(h, 50)) * 100);
const fill = document.getElementById('progressFill');
if (fill) fill.style.width = pct + '%';
}, 100);
}
function fmtSecsNice(secs) {
const h = Math.floor(secs/3600), m = Math.floor((secs%3600)/60), s = secs%60;
if (h) return `${h}h ${pad(m)}m ${pad(s)}s`;
return `${m}m ${pad(s)}s`;
}
function showError(msg) {
const el = document.getElementById('results');
el.innerHTML = `<div class="error-box">// ERROR — ${msg}</div>`;
el.style.display = 'block';
setStatus('');
}
// Enter key shortcut
document.addEventListener('keydown', e => {
if (e.key === 'Enter') calculate();
});
// Auto-calculate when a YouTube playlist URL is pasted
document.getElementById('urlInput').addEventListener('paste', e => {
// Let paste complete first, then check
setTimeout(() => {
const val = document.getElementById('urlInput').value.trim();
const isYTPlaylist = /[?&]list=PL[A-Za-z0-9_-]+/.test(val) ||
/youtu\.?be/.test(val) && /list=/.test(val) ||
/^PL[A-Za-z0-9_-]{10,}$/.test(val);
if (isYTPlaylist) calculate();
}, 0);
});