Frontend Masters – Timestamp Duration Enhancer

Appends (duration) to timestamp ranges like "00:53:09 - 01:06:06".

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Frontend Masters – Timestamp Duration Enhancer
// @namespace    https://tampermonkey.net/
// @version      1.0.0
// @description  Appends (duration) to timestamp ranges like "00:53:09 - 01:06:06".
// @match        https://frontendmasters.com/*
// @match        https://*.frontendmasters.com/*
// @run-at       document-idle
// @grant        none
// @license      MIT
// ==/UserScript==

(() => {
  "use strict";

  // Matches:
  // 00:53:09 - 01:06:06
  // 53:09 - 1:06:06
  // 5:09 - 6:06
  const RANGE_RE = /^\s*(\d{1,2}:\d{2}(?::\d{2})?)\s*-\s*(\d{1,2}:\d{2}(?::\d{2})?)\s*$/;

  function timeToSeconds(t) {
    const parts = t.split(":").map(Number);
    if (parts.some(n => Number.isNaN(n))) return null;

    if (parts.length === 2) {
      const [m, s] = parts;
      return m * 60 + s;
    }
    if (parts.length === 3) {
      const [h, m, s] = parts;
      return h * 3600 + m * 60 + s;
    }
    return null;
  }

  function secondsToHMS(totalSeconds) {
    totalSeconds = Math.max(0, Math.floor(totalSeconds));

    const h = Math.floor(totalSeconds / 3600);
    const m = Math.floor((totalSeconds % 3600) / 60);
    const s = totalSeconds % 60;

    const mm = String(m).padStart(2, "0");
    const ss = String(s).padStart(2, "0");

    if (h > 0) return `${h}:${mm}:${ss}`;
    return `${m}:${ss}`; // keep it compact if < 1 hour
  }

  function updateOne(el) {
    if (!(el instanceof HTMLElement)) return;

    const raw = (el.textContent || "").trim();

    // Avoid double-appending if script runs multiple times.
    // If it already ends with "(...)", skip.
    if (/\)\s*$/.test(raw) && /\([^)]+\)\s*$/.test(raw)) return;

    const m = raw.match(RANGE_RE);
    if (!m) return;

    const start = m[1];
    const end = m[2];

    const startSec = timeToSeconds(start);
    const endSec = timeToSeconds(end);
    if (startSec == null || endSec == null) return;

    const dur = endSec - startSec;
    if (dur < 0) return; // don't guess across day boundaries

    const durStr = secondsToHMS(dur);
    el.textContent = `${start} - ${end} (${durStr})`;
  }

  function scan(root = document) {
    root
      .querySelectorAll('div[data-timestamp]')
      .forEach(updateOne);
  }

  // Initial scan
  scan();

  // For pages that load/replace content dynamically
  const obs = new MutationObserver((mutations) => {
    for (const mu of mutations) {
      if (mu.type === "childList") {
        mu.addedNodes.forEach((n) => {
          if (n.nodeType !== 1) return;
          // Update the node itself if it matches, plus any descendants.
          updateOne(n);
          scan(n);
        });
      } else if (mu.type === "characterData") {
        const parent = mu.target && mu.target.parentElement;
        if (parent) updateOne(parent);
      }
    }
  });

  obs.observe(document.documentElement, {
    subtree: true,
    childList: true,
    characterData: true
  });
})();