長い動画では、視聴者がコメントにチャプターをまとめていることがある。目当てのシーンに飛びたいときに役立つが、コメントが多いと埋もれて見つけにくい。このブックマークレットを使えば、読み込まれたコメントからタイムスタンプだけを抽出して右パネルに一覧表示できる。

ブックマークレット

次のコードをブックマークのURL欄に設定する。

javascript:(function(){if(!location.href.includes('youtube.com/watch')){alert('YouTubeの動画ページで実行してください');return;}var c=document.querySelectorAll('ytd-comment-renderer #content-text');if(!c.length)c=document.querySelectorAll('ytd-comment-view-model #content-text');if(!c.length){var cs=document.querySelector('#comments');if(cs)c=cs.querySelectorAll('#content-text');}if(!c.length){alert('コメントが見つかりません。動画ページを開き、コメント欄まで画面をスクロールして読み込んでから実行してください。');return;}function toSec(t){var p=t.split(':').map(Number);return p.length===3?p[0]*3600+p[1]*60+p[2]:p[0]*60+p[1];}var re=/^[ \t]*(\d{1,2}:\d{2}(?::\d{2})?)[ \t]*[-::]?[ \t]*([^\r\n]{0,60})/gm,seen={},res=[];c.forEach(function(el){var t=el.innerText,m;re.lastIndex=0;while((m=re.exec(t))!==null){var secs=toSec(m[1]),matched=-1;for(var s=secs-1;s<=secs+1;s++){if(seen[s]!==undefined){matched=s;break;}}if(matched===-1){var label=m[2].trim();if(!label){var after=t.slice(m.index+m[0].length);var nl=after.match(/^[\r\n]+[ \t]*([^\r\n]+)/);if(nl&&!/^\d{1,2}:\d{2}/.test(nl[1].trim())){label=nl[1].trim().slice(0,60);}}seen[secs]=res.length;res.push({time:m[1],label:label,secs:secs,count:1});}else{res[seen[matched]].count++;}}});if(!res.length){alert('タイムスタンプが見つかりませんでした。コメント欄をもっとスクロールしてから試してください。');return;}res.sort(function(a,b){return a.secs-b.secs;});var o=document.getElementById('yt-ts-panel');if(o){o.remove();return;}var lt=res.map(function(r){return r.time+(r.label?' '+r.label:'');}).join('\n');var p=document.createElement('div');p.id='yt-ts-panel';p.style.cssText='position:fixed;top:60px;right:16px;width:360px;max-height:80vh;overflow-y:auto;background:#fff;border:1px solid #ddd;border-top:3px solid #f00;border-radius:4px;z-index:99999;padding:12px 16px;font-size:13px;font-family:sans-serif;box-shadow:0 4px 16px rgba(0,0,0,.2)';var hdr=document.createElement('div');hdr.style.cssText='display:flex;justify-content:space-between;align-items:center;margin-bottom:10px';var ttl=document.createElement('span');ttl.style.cssText='color:#f00;font-weight:bold';ttl.textContent='タイムスタンプ ('+res.length+'件)';var btns=document.createElement('div');var cp=document.createElement('button');cp.style.cssText='padding:3px 10px;background:#f00;color:#fff;border:none;border-radius:3px;cursor:pointer;font-size:12px;margin-right:6px';cp.textContent='コピー';var cl=document.createElement('button');cl.style.cssText='background:none;border:none;font-size:16px;cursor:pointer';cl.textContent='×';btns.appendChild(cp);btns.appendChild(cl);hdr.appendChild(ttl);hdr.appendChild(btns);p.appendChild(hdr);var maxCount=Math.max.apply(null,res.map(function(r){return r.count;}));var rd=document.createElement('div');res.forEach(function(r){var pct=(r.count/maxCount*100).toFixed(1);var bg='linear-gradient(to right,rgba(255,0,0,0.1) '+pct+'%,transparent '+pct+'%)';var bgHov='linear-gradient(to right,rgba(255,0,0,0.2) '+pct+'%,rgba(255,245,245,1) '+pct+'%)';var row=document.createElement('div');row.style.cssText='display:flex;align-items:center;padding:5px 0;border-bottom:1px solid #f0f0f0;line-height:1.5;cursor:pointer;background:'+bg;row.onmouseover=function(){row.style.background=bgHov;};row.onmouseout=function(){row.style.background=bg;};row.onclick=function(){var vid=document.querySelector('video');if(vid)vid.currentTime=r.secs;};var ts=document.createElement('span');ts.style.cssText='color:#f00;font-weight:bold;flex-shrink:0;min-width:52px';ts.textContent=r.time;var lb=document.createElement('span');lb.style.cssText='color:#333;flex:1';lb.textContent=r.label;var badge=document.createElement('span');badge.style.cssText='color:#aaa;font-size:11px;flex-shrink:0;min-width:18px;text-align:right;padding-left:4px';badge.textContent=r.count;row.appendChild(ts);row.appendChild(lb);row.appendChild(badge);rd.appendChild(row);});p.appendChild(rd);document.body.appendChild(p);cl.onclick=function(){p.remove();};cp.onclick=function(){navigator.clipboard.writeText(lt).then(function(){cp.textContent='コピーしました';setTimeout(function(){cp.textContent='コピー';},2000);}).catch(function(){cp.textContent='失敗';setTimeout(function(){cp.textContent='コピー';},2000);});};})();

登録手順

  1. ブックマークバーを右クリックして、新しいブックマークを作る
  2. 名前を「TSを抽出」など分かりやすいものにする
  3. URL欄に上のコードを貼り付けて保存する

使い方

  1. YouTubeの動画ページを開く
  2. コメント欄までスクロールして、コメントを読み込む
  3. ブックマークレットをクリックする
  4. 右側にタイムスタンプ一覧のパネルが表示される(背景の塗りが広い行ほど多くのコメントで言及された時間、右端の数字は言及数)
  5. 行をクリックすると、その時間に動画がジャンプする
  6. コピーボタンで全件をクリップボードに取得できる
  7. もう一度クリックするとパネルが閉じる

コメントをさらにスクロールして読み込んでから再実行すると、検出数が増える。

タイムスタンプ抽出パネルの表示例。背景の塗りが広い行ほど多くのコメントで言及されている
抽出パネルの表示例。背景の塗りが広い行が人気のシーン

どう動いているか

まずコメント欄の要素を取得する。YouTubeはレイアウトが変わることがあるため、いくつかのセレクタを順に試している。

let comments = document.querySelectorAll('ytd-comment-renderer #content-text');
if (!comments.length) {
  comments = document.querySelectorAll('ytd-comment-view-model #content-text');
}
if (!comments.length) {
  const section = document.querySelector('#comments');
  if (section) comments = section.querySelectorAll('#content-text');
}

各コメントのテキストに対して、行頭に時刻形式(0:001:23:45)が来るパターンを正規表現で抽出する。m フラグを使って ^ を各行の先頭にマッチさせているため、1:23コメント のようにスペースなしで続く書き方も拾える。文中に書かれた時刻(「3:45あたりが好き」のような書き方)は行頭判定で弾かれる。抽出した時刻は秒数に変換しておく。

// 行頭の "0:00" や "1:23:45" のような時刻表記にマッチする
const re = /^[ \t]*(\d{1,2}:\d{2}(?::\d{2})?)[ \t]*[-::]?[ \t]*([^\r\n]{0,60})/gm;

function toSec(time) {
  const p = time.split(':').map(Number);
  return p.length === 3 ? p[0] * 3600 + p[1] * 60 + p[2] : p[0] * 60 + p[1];
}

00:010:01 のように表記が異なっても秒数に正規化して同一視し、1秒差の重複もまとめてカウントを集計する。タイムスタンプだけで同じ行にラベルがない場合は、次の行のテキストをラベルとして採用する(次の行もタイムスタンプなら無視)。

const seen = {};
const results = [];

comments.forEach((el) => {
  const text = el.innerText;
  let m;
  re.lastIndex = 0;
  while ((m = re.exec(text)) !== null) {
    const secs = toSec(m[1]);

    // 前後1秒以内に同じ時刻があれば、それと同一視してカウントだけ増やす
    let matched = -1;
    for (let s = secs - 1; s <= secs + 1; s++) {
      if (seen[s] !== undefined) { matched = s; break; }
    }

    if (matched === -1) {
      let label = m[2].trim();
      if (!label) {
        // 同じ行にラベルがなければ次の行をラベルとして使う
        const after = text.slice(m.index + m[0].length);
        const nextLine = after.match(/^[\r\n]+[ \t]*([^\r\n]+)/);
        if (nextLine && !/^\d{1,2}:\d{2}/.test(nextLine[1].trim())) {
          label = nextLine[1].trim().slice(0, 60);
        }
      }
      seen[secs] = results.length;
      results.push({ time: m[1], label, secs, count: 1 });
    } else {
      results[seen[matched]].count++;
    }
  }
});

最後に、各行の背景グラデーション幅を count / maxCount の割合で塗り、行をクリックすると video.currentTime をセットして動画をシークするようにする。

const maxCount = Math.max(...results.map((r) => r.count));

results.forEach((r) => {
  // 言及数が多いほど背景の塗りを広くする
  const pct = (r.count / maxCount * 100).toFixed(1);
  row.style.background = `linear-gradient(to right, rgba(255,0,0,0.1) ${pct}%, transparent ${pct}%)`;

  // クリックでその時刻にシーク
  row.onclick = () => {
    const video = document.querySelector('video');
    if (video) video.currentTime = r.secs;
  };
});

注意

  • ページに読み込まれたコメントだけが対象。スクロールして表示させないと拾えない。
  • YouTubeのDOM構造が変わると動作しなくなる可能性がある。
  • タイムスタンプを文の途中に書いているコメントは検出しない。
  • 全コメントから取得したい場合は YouTube Data API を使うスクリプトが必要になる。

関連記事

よくある質問

YouTubeのコメントからタイムスタンプを取得するには?
ブックマークレットを使うと、動画ページでコメント欄に表示されているタイムスタンプを一覧で取得できる。コメント欄までスクロールして読み込んだ状態でクリックするだけ。
タイムスタンプが見つからないと表示される場合は?
コメントが読み込まれていない場合がある。ページを下にスクロールしてコメントを表示させてから、もう一度実行する。さらにスクロールするほど検出数が増える。
全コメントからタイムスタンプを取得できる?
ページに読み込まれたコメントのみが対象。まだ表示されていないコメントは対象外。全件取得したい場合はYouTube Data APIを使うスクリプトが別途必要になる。