Skip to content

Instantly share code, notes, and snippets.

@kor-bim
Last active February 5, 2026 23:21
Show Gist options
  • Select an option

  • Save kor-bim/a9ec68cdce4ea56a2be4d176e01d7b9e to your computer and use it in GitHub Desktop.

Select an option

Save kor-bim/a9ec68cdce4ea56a2be4d176e01d7b9e to your computer and use it in GitHub Desktop.
YouTube - Jump to Most Replayed Button
// ==UserScript==
// @name YouTube 가장 많이 다시 본 장면 이동 버튼
// @name:el YouTube Μετάβαση στο πιο επαναλαμβανόμενο σημείο
// @name:nl YouTube Ga naar meest herhaalde segment
// @name:nb YouTube Gå til mest gjenspilte segment
// @name:da YouTube Gå til mest genspillede segment
// @name:de YouTube Zum meistgesehenen Abschnitt springen
// @name:ru YouTube Переход к самому просматриваемому фрагменту
// @name:ro YouTube Salt la segmentul cel mai reluat
// @name:mr YouTube सर्वाधिक पुन्हा पाहिलेल्या भागावर जा
// @name:vi YouTube Chuyển đến đoạn được xem lại nhiều nhất
// @name:be YouTube Пераход да найбольш прагляданага фрагмента
// @name:bg YouTube Преход към най-гледания сегмент
// @name:sr YouTube Прелазак на најгледанији сегмент
// @name:sv YouTube Hoppa till mest sedda segment
// @name:es YouTube Ir al segmento más repetido
// @name:es-419 YouTube Ir al segmento más repetido
// @name:sk YouTube Prejsť na najviac prehrávaný segment
// @name:ar YouTube الانتقال إلى الجزء الأكثر إعادة
// @name:eo YouTube Salti al plej reludita segmento
// @name:en YouTube Jump to Most Replayed Button
// @name:uk YouTube Перехід до найчастіше переглядуваного фрагмента
// @name:ug YouTube ئەڭ كۆپ قايتا كۆرۈلگەن بۆلەككە ئۆتۈش
// @name:it YouTube Vai al segmento più riprodotto
// @name:id YouTube Lompat ke segmen paling sering diputar
// @name:ja YouTube 最も再生されたシーンへ移動
// @name:ka YouTube ყველაზე ხშირად ნანახ ნაწილზე გადასვლა
// @name:zh-CN YouTube 跳转到最常重播片段
// @name:zh-TW YouTube 跳至最常重播片段
// @name:cs YouTube Přejít na nejčastěji přehrávaný segment
// @name:hr YouTube Prijeđi na najgledaniji segment
// @name:th YouTube ไปยังช่วงที่ถูกเล่นซ้ำมากที่สุด
// @name:tr YouTube En Çok Tekrar İzlenen Bölüme Git
// @name:pt-BR YouTube Ir para o trecho mais reproduzido
// @name:pl YouTube Przejdź do najczęściej odtwarzanego segmentu
// @name:fr YouTube Aller au segment le plus rejoué
// @name:fr-CA YouTube Aller au segment le plus rejoué
// @name:fi YouTube Siirry eniten toistettuun kohtaan
// @name:ko YouTube 가장 많이 다시 본 장면 이동 버튼
// @name:hu YouTube Ugrás a leggyakrabban visszajátszott részhez
// @name:he YouTube מעבר לקטע הנצפה ביותר
// @name:ckb YouTube بڕۆ بۆ زۆرترین بەشی دووبارەبینراو
// @description 영상에서 가장 많이 다시 본 장면으로 이동하는 버튼을 추가합니다
// @description:el Προσθέτει ένα κουμπί που μεταβαίνει στο πιο επαναλαμβανόμενο σημείο του βίντεο
// @description:nl Voegt een knop toe om naar het meest herhaalde segment van de video te springen
// @description:nb Legger til en knapp som går til det mest gjenspilte segmentet i videoen
// @description:da Tilføjer en knap der hopper til det mest genspillede segment i videoen
// @description:de Fügt eine Schaltfläche hinzu um zum meistgesehenen Abschnitt des Videos zu springen
// @description:ru Добавляет кнопку для перехода к самому просматриваемому фрагменту видео
// @description:ro Adaugă un buton pentru a sări la segmentul cel mai reluat al videoclipului
// @description:mr व्हिडिओमधील सर्वाधिक पुन्हा पाहिलेल्या भागावर जाण्यासाठी बटण जोडते
// @description:vi Thêm nút để chuyển đến đoạn được xem lại nhiều nhất trong video
// @description:be Дадае кнопку для пераходу да найбольш прагляданага фрагмента відэа
// @description:bg Добавя бутон за преминаване към най-гледания сегмент на видеото
// @description:sr Додаје дугме за прелазак на најгледанији сегмент видеа
// @description:sv Lägger till en knapp för att hoppa till det mest sedda segmentet i videon
// @description:es Añade un botón para saltar al segmento más reproducido del video
// @description:es-419 Agrega un botón para saltar al segmento más reproducido del video
// @description:sk Pridáva tlačidlo na preskočenie na najviac prehrávaný segment videa
// @description:ar يضيف زرًا للانتقال إلى الجزء الأكثر إعادة في الفيديو
// @description:eo Aldonas butonon por salti al plej reludita segmento de la video
// @description:en Adds a button that jumps to the most replayed segment of the video
// @description:uk Додає кнопку для переходу до найчастіше переглядуваного фрагмента відео
// @description:ug سىننىڭ ئەڭ كۆپ قايتا كۆرۈلگەن بۆلىكىگە ئۆتۈش كۇنۇپكىسىنى قوشىدۇ
// @description:it Aggiunge un pulsante per saltare al segmento più riprodotto del video
// @description:id Menambahkan tombol untuk melompat ke segmen yang paling sering diputar dalam video
// @description:ja 動画内で最も再生されたシーンへ移動するボタンを追加します
// @description:ka ამატებს ღილაკს ვიდეოში ყველაზე ხშირად ნანახ ნაწილზე გადასასვლელად
// @description:zh-CN 添加一个按钮可跳转到视频中最常被重播的片段
// @description:zh-TW 新增一個按鈕可跳至影片中最常被重播的片段
// @description:cs Přidá tlačítko pro přechod na nejčastěji přehrávaný segment videa
// @description:hr Dodaje gumb za prelazak na najgledaniji segment videozapisa
// @description:th เพิ่มปุ่มสำหรับไปยังช่วงของวิดีโอที่ถูกเล่นซ้ำมากที่สุด
// @description:tr Videodaki en çok tekrar izlenen bölüme gitmek için bir düğme ekler
// @description:pt-BR Adiciona um botão para pular para o trecho mais reproduzido do vídeo
// @description:pl Dodaje przycisk do przejścia do najczęściej odtwarzanego segmentu wideo
// @description:fr Ajoute un bouton pour accéder au segment le plus rejoué de la vidéo
// @description:fr-CA Ajoute un bouton pour accéder au segment le plus rejoué de la vidéo
// @description:fi Lisää painikkeen siirtymiseen videon eniten toistettuun kohtaan
// @description:ko 영상에서 가장 많이 다시 본 장면으로 이동하는 버튼을 추가합니다
// @description:hu Hozzáad egy gombot a videó leggyakrabban visszajátszott részére ugráshoz
// @description:he מוסיף כפתור למעבר לקטע הנצפה ביותר בסרטון
// @description:ckb دوگمەیەک زیاد دەکات بۆ چوون بۆ زۆرترین بەشی دووبارەبینراوی ڤیدیۆ
// @author kor-bim
// @namespace http://tampermonkey.net/
// @version 1.1.5
// @match https://www.youtube.com/*
// @icon https://www.youtube.com/s/desktop/aaaab8bf/img/favicon_144x144.png
// @run-at document-idle
// @grant unsafeWindow
// @license MIT
// ==/UserScript==
(() => {
'use strict';
class MostReplayedJump {
static CONFIG = Object.freeze({
btnId: 'gf-mostreplayed-jump',
peakOffsetMs: 0,
wait: Object.freeze({
maxMs: 8000,
pollMs: 200,
}),
seg: Object.freeze({
dedupMs: 500,
nextThresholdMs: 250,
}),
ui: Object.freeze({
fallbackEnsureMs: 1500,
toast: Object.freeze({
id: 'gf-mostreplayed-toast',
removeAfterMs: 900,
cssText: `
position:absolute;
left:50%;
bottom:72px;
transform:translateX(-50%);
background:rgba(0,0,0,.78);
color:#fff;
padding:6px 10px;
border-radius:10px;
font-size:12px;
z-index:999999;
pointer-events:none;
white-space:nowrap;
line-height:1;
`,
}),
}),
net: Object.freeze({
urlIncludes: ['/youtubei/v1/'],
likelyEndpoints: ['/player', '/next', '/get_watch_next', '/browse'],
}),
i18n: Object.freeze({
// 'auto' | 'ko' | 'en'
lang: 'auto',
dict: Object.freeze({
ko: Object.freeze({
btnTitle: '가장 많이 다시 본 장면으로 이동',
btnAria: '가장 많이 다시 본 장면으로 이동',
toastSearching: '찾는 중…',
toastNotFound: '가장 많이 다시 본 장면 없음',
toastAccessDenied: 'Data 접근 불가',
}),
en: Object.freeze({
btnTitle: 'Jump to Most Replayed',
btnAria: 'Jump to Most Replayed',
toastSearching: 'Searching…',
toastNotFound: 'Most Replayed Not Found',
toastAccessDenied: 'Cannot access Data',
}),
}),
}),
});
#cache = {
netByVid: new Map(), // videoId -> segments[]
parsedByVid: new Map(), // videoId -> segments[]
domTried: new Set(), // videoId
};
#timers = { fallbackEnsure: null };
#observer = null;
run() {
this.#hookNetwork();
this.#bindNavigation();
this.#boot();
}
/* ------------------------------- i18n ------------------------------- */
#lang() {
const cfg = MostReplayedJump.CONFIG.i18n;
if (cfg.lang === 'ko' || cfg.lang === 'en') return cfg.lang;
const htmlLang = (document.documentElement.getAttribute('lang') || '').toLowerCase();
if (htmlLang.startsWith('ko')) return 'ko';
const navLang = (navigator.language || '').toLowerCase();
return navLang.startsWith('ko') ? 'ko' : 'en';
}
#t(key) {
const { dict } = MostReplayedJump.CONFIG.i18n;
const lang = this.#lang();
return dict[lang]?.[key] ?? dict.en?.[key] ?? String(key);
}
/* ----------------------------- Env helpers ----------------------------- */
#win() {
try {
if (typeof unsafeWindow !== 'undefined') return unsafeWindow;
} catch {}
return window;
}
#isWatch() {
return location.pathname.startsWith('/watch');
}
#qs(sel, root = document) {
return root.querySelector(sel);
}
#player() {
return document.getElementById('movie_player');
}
#video() {
return document.querySelector('video.html5-main-video') || document.querySelector('video');
}
#videoId() {
const p = this.#player();
const id = p?.getVideoData?.()?.video_id;
if (id) return id;
const u = new URL(location.href);
return u.searchParams.get('v') || null;
}
#sleep(ms) {
return new Promise((r) => setTimeout(r, ms));
}
/* --------------------------------- UI --------------------------------- */
#toast(text) {
const player = this.#player();
if (!player) return;
const { id, removeAfterMs, cssText } = MostReplayedJump.CONFIG.ui.toast;
document.getElementById(id)?.remove();
const el = document.createElement('div');
el.id = id;
el.textContent = text;
el.style.cssText = cssText;
player.appendChild(el);
setTimeout(() => el.remove(), removeAfterMs);
}
#makeIconSvg() {
const svgNS = 'http://www.w3.org/2000/svg';
const svg = document.createElementNS(svgNS, 'svg');
const isOldUI = !document.querySelector('.ytp-right-controls-left');
svg.setAttribute('viewBox', '0 0 24 24');
svg.setAttribute('fill', 'none');
if (isOldUI) {
svg.setAttribute('width', '100%');
svg.setAttribute('height', '100%');
svg.setAttribute('preserveAspectRatio', 'xMidYMid meet');
} else {
svg.setAttribute('width', '24');
svg.setAttribute('height', '24');
}
// 아이콘 그룹
const g = document.createElementNS(svgNS, 'g');
// 옛 UI에서만 크기 보정
if (isOldUI) {
const scale = 0.75;
const offset = (24 * (1 - scale)) / 2;
g.setAttribute(
'transform',
`translate(${offset} ${offset}) scale(${scale})`
);
}
const path = document.createElementNS(svgNS, 'path');
path.setAttribute('d', 'M4 16l6-6 4 4 6-8');
path.setAttribute('stroke', 'white');
path.setAttribute('stroke-width', '2');
path.setAttribute('stroke-linecap', 'round');
path.setAttribute('stroke-linejoin', 'round');
const dot = (cx, cy) => {
const c = document.createElementNS(svgNS, 'circle');
c.setAttribute('cx', cx);
c.setAttribute('cy', cy);
c.setAttribute('r', '1.6');
c.setAttribute('fill', 'white');
return c;
};
g.appendChild(path);
g.appendChild(dot(4, 16));
g.appendChild(dot(10, 10));
g.appendChild(dot(14, 14));
g.appendChild(dot(20, 6));
svg.appendChild(g);
return svg;
}
#ensureButton() {
// 1) 기존(신 UI에서 존재할 수 있음)
const preferred =
this.#qs('.ytp-right-controls .ytp-right-controls-left') ||
// 2) 구 UI: right-controls에 바로 버튼들이 있음
this.#qs('.ytp-right-controls') ||
// 3) 최후: 전체 컨트롤 영역
this.#qs('.ytp-chrome-controls .ytp-right-controls') ||
this.#qs('.ytp-chrome-controls');
if (!preferred) return false;
if (document.getElementById(MostReplayedJump.CONFIG.btnId)) return true;
const btn = document.createElement('button');
btn.id = MostReplayedJump.CONFIG.btnId;
btn.className = 'ytp-button';
btn.type = 'button';
btn.title = this.#t('btnTitle');
btn.setAttribute('aria-label', this.#t('btnAria'));
btn.appendChild(this.#makeIconSvg());
btn.addEventListener('click', (e) => this.#onClick(e), true);
// 구 UI에서 우측 버튼들 사이 “자연스러운 위치”에 끼워넣기(전체화면 버튼 앞)
const fullscreen = preferred.querySelector('.ytp-fullscreen-button');
if (fullscreen && fullscreen.parentElement === preferred) {
preferred.insertBefore(btn, fullscreen);
} else {
preferred.appendChild(btn);
}
return true;
}
#removeButton() {
document.getElementById(MostReplayedJump.CONFIG.btnId)?.remove();
}
/* ------------------------------- Parsing ------------------------------- */
#normalizeLabel(dec) {
const t = dec?.label?.runs?.[0]?.text;
return typeof t === 'string' ? t.trim() : '';
}
// 언어 무관 판별: Most Replayed timed marker는 보통 (start/end + decorationTimeMillis) 조합을 가진다.
#isMostReplayedDecoration(dec) {
const startMs = Number(dec?.visibleTimeRangeStartMillis);
const endMs = Number(dec?.visibleTimeRangeEndMillis);
const decoMs = Number(dec?.decorationTimeMillis);
if (!Number.isFinite(decoMs)) return false;
// start/end가 둘 다 존재하는 경우를 우선 신뢰
if (Number.isFinite(startMs) && Number.isFinite(endMs) && endMs > startMs) {
// deco가 범위 밖인 경우도 있을 수 있어 너무 빡세게 제한하지 않음
return true;
}
// 일부 케이스에서 start/end가 비거나 구조가 다를 수 있으니 라벨 기반 보조(있으면)
const label = this.#normalizeLabel(dec);
if (!label) return false;
const low = label.toLowerCase();
return low.includes('most') && low.includes('replay');
}
#dedupByJumpMs(segs) {
const out = [];
let last = null;
const { dedupMs } = MostReplayedJump.CONFIG.seg;
for (const s of segs) {
const jm = s?.jumpMs;
if (!Number.isFinite(jm)) continue;
if (last == null || Math.abs(jm - last) > dedupMs) {
out.push(s);
last = jm;
}
}
return out;
}
#mergePreferNew(a, b) {
const all = [...(a || []), ...(b || [])].filter(Boolean);
all.sort((x, y) => (x.jumpMs || 0) - (y.jumpMs || 0));
return this.#dedupByJumpMs(all);
}
#extractSegmentsFromRoot(root, currentVid) {
const muts = root?.frameworkUpdates?.entityBatchUpdate?.mutations;
if (!Array.isArray(muts)) return [];
const out = [];
for (const mut of muts) {
const ent = mut?.payload?.macroMarkersListEntity;
if (!ent) continue;
const extVid = ent?.externalVideoId;
if (currentVid && extVid && extVid !== currentVid) continue;
const timed = ent?.markersList?.markersDecoration?.timedMarkerDecorations;
if (!Array.isArray(timed) || timed.length === 0) continue;
for (const d of timed) {
// ✅ 라벨 문자열 의존 제거 (언어 무관)
if (!this.#isMostReplayedDecoration(d)) continue;
const startMs = Number(d.visibleTimeRangeStartMillis);
const endMs = Number(d.visibleTimeRangeEndMillis);
const decoMs = Number(d.decorationTimeMillis);
if (!Number.isFinite(decoMs)) continue;
const s = Number.isFinite(startMs) ? startMs : null;
const e = Number.isFinite(endMs) ? endMs : null;
const jumpMs = decoMs + MostReplayedJump.CONFIG.peakOffsetMs;
out.push({ startMs: s, endMs: e, decoMs, jumpMs, videoId: extVid || null });
}
}
out.sort((a, b) => a.jumpMs - b.jumpMs);
return this.#dedupByJumpMs(out);
}
#extractVarObjectFromScriptText(text, varName) {
const idx = text.indexOf(`var ${varName} =`);
if (idx < 0) return null;
const startBrace = text.indexOf('{', idx);
if (startBrace < 0) return null;
let depth = 0;
let inStr = false;
let strCh = '';
let esc = false;
for (let i = startBrace; i < text.length; i++) {
const ch = text[i];
if (inStr) {
if (esc) esc = false;
else if (ch === '\\') esc = true;
else if (ch === strCh) {
inStr = false;
strCh = '';
}
continue;
}
if (ch === '"' || ch === "'") {
inStr = true;
strCh = ch;
continue;
}
if (ch === '{') depth++;
else if (ch === '}') {
depth--;
if (depth === 0) return text.slice(startBrace, i + 1);
}
}
return null;
}
#parseYtInitialDataFromDomScriptsOnce(vid) {
if (!vid) return null;
if (this.#cache.domTried.has(vid)) return null;
this.#cache.domTried.add(vid);
for (const s of document.querySelectorAll('script')) {
const txt = s.textContent || '';
if (!txt.includes('var ytInitialData =')) continue;
const objText = this.#extractVarObjectFromScriptText(txt, 'ytInitialData');
if (!objText) continue;
try {
return JSON.parse(objText);
} catch {}
}
return null;
}
#refreshParsedCache() {
const vid = this.#videoId();
if (!vid) return;
const yid = this.#win()?.ytInitialData;
if (yid) {
const segs = this.#extractSegmentsFromRoot(yid, vid);
if (segs.length) {
this.#cache.parsedByVid.set(vid, this.#mergePreferNew(this.#cache.parsedByVid.get(vid), segs));
return;
}
}
const parsed = this.#parseYtInitialDataFromDomScriptsOnce(vid);
if (parsed) {
const segs = this.#extractSegmentsFromRoot(parsed, vid);
if (segs.length) {
this.#cache.parsedByVid.set(vid, this.#mergePreferNew(this.#cache.parsedByVid.get(vid), segs));
}
}
}
#segmentsForCurrentVid() {
const vid = this.#videoId();
if (!vid) return [];
return this.#mergePreferNew(this.#cache.netByVid.get(vid), this.#cache.parsedByVid.get(vid));
}
/* ------------------------------- Jumping ------------------------------- */
#pickNext(segs, curMs) {
const { nextThresholdMs } = MostReplayedJump.CONFIG.seg;
const inSeg = segs.find(
(s) => s.startMs != null && s.endMs != null && curMs >= s.startMs && curMs <= s.endMs
);
const base = inSeg?.endMs ?? curMs;
const next = segs.find((s) => s.jumpMs > base + nextThresholdMs);
return next || segs[0] || null;
}
#seekToMs(ms) {
const player = this.#player();
const video = this.#video();
if (!player || !video) return false;
const sec = ms / 1000;
try {
player.seekTo(sec, true);
player.playVideo?.();
return true;
} catch {
if (typeof video.fastSeek === 'function') video.fastSeek(sec);
else video.currentTime = sec;
video.play?.().catch(() => {});
return true;
}
}
async #getSegmentsOnClickWait() {
const start = Date.now();
const { maxMs, pollMs } = MostReplayedJump.CONFIG.wait;
this.#refreshParsedCache();
let segs = this.#segmentsForCurrentVid();
if (segs.length) return segs;
while (Date.now() - start < maxMs) {
await this.#sleep(pollMs);
this.#refreshParsedCache();
segs = this.#segmentsForCurrentVid();
if (segs.length) return segs;
}
return [];
}
/* ------------------------------- Network ------------------------------- */
#shouldParseYoutubeiUrl(url) {
const { urlIncludes, likelyEndpoints } = MostReplayedJump.CONFIG.net;
if (!url) return false;
if (!urlIncludes.some((t) => url.includes(t))) return false;
return likelyEndpoints.some((t) => url.includes(t));
}
#storeFromResponseJson(json) {
try {
const currentVid = this.#videoId();
const segs = this.#extractSegmentsFromRoot(json, currentVid);
if (!segs.length) return;
const vid = segs.find((s) => s.videoId)?.videoId || currentVid;
if (!vid) return;
this.#cache.netByVid.set(vid, this.#mergePreferNew(this.#cache.netByVid.get(vid), segs));
} catch {}
}
#hookFetch() {
const win = this.#win();
const orig = win.fetch;
if (!orig || orig.__gf_hooked) return;
const self = this;
const wrapped = function (...args) {
return orig.apply(this, args).then((res) => {
try {
const url = typeof args[0] === 'string' ? args[0] : (args[0]?.url || '');
if (!self.#shouldParseYoutubeiUrl(url)) return res;
res
.clone()
.json()
.then((j) => self.#storeFromResponseJson(j))
.catch(() => {});
} catch {}
return res;
});
};
wrapped.__gf_hooked = true;
win.fetch = wrapped;
}
#hookXHR() {
const win = this.#win();
const XHR = win.XMLHttpRequest;
if (!XHR || XHR.__gf_hooked) return;
const self = this;
const origOpen = XHR.prototype.open;
const origSend = XHR.prototype.send;
XHR.prototype.open = function (method, url, ...rest) {
this.__gf_url = url;
return origOpen.call(this, method, url, ...rest);
};
XHR.prototype.send = function (...args) {
this.addEventListener('load', function () {
try {
const url = this.__gf_url || '';
if (!self.#shouldParseYoutubeiUrl(url)) return;
const ct = this.getResponseHeader('content-type') || '';
if (!ct.includes('application/json')) return;
const txt = this.responseText;
if (!txt || txt.length < 2) return;
self.#storeFromResponseJson(JSON.parse(txt));
} catch {}
});
return origSend.apply(this, args);
};
XHR.__gf_hooked = true;
}
#hookNetwork() {
this.#hookFetch();
this.#hookXHR();
}
/* ------------------------------ Lifecycle ------------------------------ */
#startObserver() {
this.#observer?.disconnect();
this.#observer = new MutationObserver(() => {
if (!this.#isWatch()) return;
this.#ensureButton();
});
this.#observer.observe(document.documentElement, { childList: true, subtree: true });
}
#startFallbackEnsure() {
this.#stopFallbackEnsure();
this.#timers.fallbackEnsure = setInterval(() => {
if (!this.#isWatch()) return;
this.#ensureButton();
}, MostReplayedJump.CONFIG.ui.fallbackEnsureMs);
}
#stopFallbackEnsure() {
if (this.#timers.fallbackEnsure) clearInterval(this.#timers.fallbackEnsure);
this.#timers.fallbackEnsure = null;
}
#boot() {
if (!this.#isWatch()) {
this.#removeButton();
this.#stopFallbackEnsure();
return;
}
this.#ensureButton();
this.#refreshParsedCache();
this.#startObserver();
this.#startFallbackEnsure();
}
#bindNavigation() {
window.addEventListener('yt-navigate-finish', () => this.#boot(), true);
window.addEventListener('popstate', () => this.#boot(), true);
}
/* -------------------------------- Events ------------------------------ */
async #onClick(e) {
e.preventDefault();
e.stopPropagation();
const video = this.#video();
if (!video) return;
if (!this.#isWatch()) return;
this.#toast(this.#t('toastSearching'));
const segs = await this.#getSegmentsOnClickWait();
if (!segs.length) {
const win = this.#win();
this.#toast(win?.ytInitialData ? this.#t('toastNotFound') : this.#t('toastAccessDenied'));
return;
}
const curMs = video.currentTime * 1000;
const target = this.#pickNext(segs, curMs);
if (!target) return;
this.#seekToMs(target.jumpMs);
}
}
new MostReplayedJump().run();
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment