▶ Utility Tool

PLAYLIST
CLOCK

// 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…)

Developer Reference

Core Algorithm & Standalone Script

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);
  });