diff --git a/extension/content.js b/extension/content.js index 2128e7a..adc3e52 100644 --- a/extension/content.js +++ b/extension/content.js @@ -57,13 +57,7 @@ let seekDebounceTimer = null; // debounce timer for rapid seek events let expectedSeekTime = null; // strictly track programmatic seeks - const PAGE_API_SEEK_BRIDGE = 1; - let lastDisneyPlusTimelineCandidates = []; - let lastKnownDisneyPlusDuration = 0; - let lastKnownDisneyPlusScale = 1; - let lastKnownDisneyPlusStart = 0; - let lastDisneyPlusUiCurrent = null; - let lastDisneyPlusNativeAtUi = null; + const PAGE_API_SEEK_BRIDGE = 1; // Accurate Disney+ playhead pushed by the MAIN-world page-API bridge // (background.js installPageApiSeekBridge). The isolated content world // can't read the page's media player directly. @@ -96,284 +90,34 @@ return matchesPlayerUrls(['disneyplus.com']); } - function getSeekableRange(video) { - try { - const ranges = video.seekable; - if (!ranges || ranges.length === 0) return null; - const current = video.currentTime; - let index = ranges.length - 1; - if (Number.isFinite(current)) { - for (let i = 0; i < ranges.length; i++) { - if (current >= ranges.start(i) && current <= ranges.end(i)) { - index = i; - break; - } - } - } - const start = ranges.start(index); - const end = ranges.end(index); - if (!Number.isFinite(start) || !Number.isFinite(end) || end <= start) return null; - return { start, end, duration: end - start }; - } catch (_e) { - return null; - } - } - - function parseClockTime(value) { - const parts = String(value || '').trim().split(':').map(Number); - if (parts.length < 2 || parts.length > 3 || parts.some(n => !Number.isFinite(n))) return null; - return parts.length === 2 - ? parts[0] * 60 + parts[1] - : parts[0] * 3600 + parts[1] * 60 + parts[2]; - } - - function parseTimelineText(text) { - const matches = String(text || '').match(/\b\d{1,3}:\d{2}(?::\d{2})?\b/g); - if (!matches || matches.length < 2) return null; - const current = parseClockTime(matches[0]); - const duration = parseClockTime(matches[matches.length - 1]); - if (!Number.isFinite(current) || !Number.isFinite(duration) || duration < 60 || current > duration + 5) return null; - return { current, duration }; - } - - function getDisneyPlusUiTimeline() { - if (typeof document === 'undefined') return null; - const selectors = [ - '[role="slider"]', - '[role="progressbar"]', - '[aria-valuenow][aria-valuemax]', - '[aria-valuetext]', - '[aria-label]', - '[data-testid]', - '[data-test]', - '[class*="time" i]', - '[class*="progress" i]', - 'time', - 'output', - 'button', - 'span', - 'p', - 'div' - ].join(','); - const nodes = querySelectorAllShadow(selectors).slice(0, 2000); - const textParts = []; - const candidates = []; - let best = null; - let bestScore = -1; - let remainingSeconds = null; - - function considerTimeline(parsed, source, weight = 0) { - if (!parsed) return; - const score = weight + - (parsed.duration >= 20 * 60 ? 200 : 0) + - (parsed.current > 0 ? 50 : 0) - - Math.abs((parsed.duration / 2) - parsed.current) / 100; - candidates.push({ ...parsed, source, score: Math.round(score) }); - if (score > bestScore) { - bestScore = score; - best = parsed; - } - } - - for (const node of nodes) { - const now = Number(node.getAttribute?.('aria-valuenow') ?? node.value); - const max = Number(node.getAttribute?.('aria-valuemax') ?? node.max); - if (Number.isFinite(now) && Number.isFinite(max) && max >= 60 && now >= 0 && now <= max + 5) { - considerTimeline({ current: now, duration: max }, 'aria-value', 300); - } - - for (const attr of ['aria-valuetext', 'aria-label', 'title', 'data-testid', 'data-test']) { - const parsed = parseTimelineText(node.getAttribute?.(attr)); - considerTimeline(parsed, attr, 200); - } - - const text = String(node.textContent || '').trim(); - if (text && text.length < 200) { - const parsed = parseTimelineText(text); - considerTimeline(parsed, 'text', 100); - textParts.push(text); - } - // Disney's scrubber aria-valuenow can lag several seconds behind - // real playback; a "time remaining" indicator updates live, so - // capture it and later derive current = duration - remaining. - const remClass = String(node.getAttribute?.('class') || '') + ' ' + String(node.getAttribute?.('data-testid') || ''); - if (/remain/i.test(remClass)) { - const remMatch = String(node.textContent || '').match(/\d{1,3}:\d{2}(?::\d{2})?/); - const rem = remMatch ? parseClockTime(remMatch[0]) : null; - if (Number.isFinite(rem) && rem >= 0) remainingSeconds = rem; - } - } - - if (best && remainingSeconds !== null && best.duration > 0) { - const liveCurrent = best.duration - remainingSeconds; - if (liveCurrent >= 0 && liveCurrent <= best.duration + 5) { - considerTimeline({ current: Math.min(liveCurrent, best.duration), duration: best.duration }, 'time-remaining', 1000); - } - } - considerTimeline(parseTimelineText(textParts.join(' ')), 'combined-text', 25); - lastDisneyPlusTimelineCandidates = candidates - .sort((a, b) => b.score - a.score) - .slice(0, 8) - .map(({ source, current, duration }) => ({ source, current, duration })); - return best; - } - - function getElementLabel(el) { - return [ - el.getAttribute?.('aria-label'), - el.getAttribute?.('title'), - el.getAttribute?.('data-testid'), - el.getAttribute?.('data-test'), - el.textContent - ].filter(Boolean).join(' ').replace(/\s+/g, ' ').trim(); - } - - function getDisneyPlusSeekButtonLabels() { - if (!isDisneyPlusHost() || typeof document === 'undefined') return []; - return querySelectorAllShadow('button,[role="button"]') - .map(getElementLabel) - .filter(Boolean) - .slice(0, 20); - } - - function clickDisneyPlusRelativeSeek(delta) { - if (!isDisneyPlusHost() || !Number.isFinite(delta) || Math.abs(delta) < 1 || typeof document === 'undefined') return false; - const backward = delta < 0; - const candidates = querySelectorAllShadow('button,[role="button"]'); - const backRe = /rewind|backward|back\b|zurück|zurueck|rück|rueck|retour|recul|retroced|atrás|voltar|indietro/i; - const forwardRe = /forward|ahead|skip|vorwärts|vorwaerts|vorsp|weiter|avancer|adelant|avançar|avancar|avanti/i; - const timeRe = /\b(5|10|15|30)\b|sec|sek|second/i; - - const button = candidates.find(btn => { - const label = getElementLabel(btn); - const matchesDirection = backward ? backRe.test(label) : forwardRe.test(label); - return matchesDirection && timeRe.test(label); - }) || candidates.find(btn => { - const label = getElementLabel(btn); - return backward ? backRe.test(label) : forwardRe.test(label); - }); - - if (button && typeof button.click === 'function') { - // Disney offers no absolute seek on its blob