mirror of
https://github.com/Shik3i/KoalaSync.git
synced 2026-08-31 13:08:15 +00:00
Remove Disney+ DOM scraping fallback
This commit is contained in:
+7
-350
@@ -58,12 +58,6 @@
|
|||||||
|
|
||||||
// --- SHARED_EVENTS_INJECT_END ---
|
// --- SHARED_EVENTS_INJECT_END ---
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Suppresses native event reporting after a programmatic action.
|
|
||||||
|
|
||||||
// Each entry is a per-type timer (key = 'playing'|'paused'|'seek').
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Suppresses native event reporting after a programmatic action.
|
// Suppresses native event reporting after a programmatic action.
|
||||||
@@ -96,281 +90,31 @@
|
|||||||
|
|
||||||
if (_suppressTimers[state]) {
|
if (_suppressTimers[state]) {
|
||||||
|
|
||||||
}
|
clearTimeout(_suppressTimers[state]);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// --- Seek Relay Filtering ---
|
|
||||||
|
|
||||||
// Minimum seek delta (seconds) to report. Prevents HLS/DASH buffering micro-seeks
|
|
||||||
|
|
||||||
// from being relayed to peers as user-initiated seeks.
|
|
||||||
|
|
||||||
const MIN_SEEK_DELTA = 2.0;
|
|
||||||
|
|
||||||
let lastReportedSeekTime = null; // last currentTime we relayed as a SEEK
|
|
||||||
|
|
||||||
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;
|
|
||||||
// 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.
|
|
||||||
let disneyPageApiTime = null;
|
|
||||||
if (typeof window !== 'undefined' && typeof window.addEventListener === 'function') {
|
|
||||||
window.addEventListener('message', (event) => {
|
|
||||||
if (event.source !== window) return;
|
|
||||||
const data = event.data;
|
|
||||||
if (data && data.__koalaPlayerTime === 1 && data.provider === 'disney'
|
|
||||||
&& Number.isFinite(data.position) && Number.isFinite(data.duration) && data.duration > 0) {
|
|
||||||
disneyPageApiTime = { position: data.position, duration: data.duration, at: Date.now() };
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function hostMatchesUrl(host, url) {
|
|
||||||
const normalized = String(url || '')
|
|
||||||
.replace(/^https?:\/\//i, '')
|
|
||||||
.split('/')[0]
|
|
||||||
.toLowerCase();
|
|
||||||
return normalized && (host === normalized || host.endsWith(`.${normalized}`));
|
|
||||||
}
|
|
||||||
|
|
||||||
function matchesPlayerUrls(urls) {
|
|
||||||
const host = window.location.hostname.toLowerCase();
|
|
||||||
return Array.isArray(urls) && urls.some(url => hostMatchesUrl(host, url));
|
|
||||||
}
|
|
||||||
|
|
||||||
function isDisneyPlusHost() {
|
|
||||||
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"]')
|
|
||||||
|
|
||||||
.filter(Boolean)
|
delete _suppressTimers[state];
|
||||||
.slice(0, 20);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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 <video>, so we click its
|
|
||||||
// relative skip button. Derive the step from the button's own label
|
|
||||||
// (usually 10s; some UIs 5/15/30) and issue enough clicks to cover the
|
|
||||||
// full delta. The previous 120s cap truncated any larger seek, which
|
|
||||||
// broke absolute-position sync (e.g. seeking to 5:00 from far away).
|
|
||||||
const stepMatch = getElementLabel(button).match(/\b(5|10|15|30)\b/);
|
|
||||||
const step = stepMatch ? Number(stepMatch[1]) : 10;
|
|
||||||
const clicks = Math.max(1, Math.min(90, Math.round(Math.abs(delta) / step)));
|
|
||||||
for (let i = 0; i < clicks; i++) {
|
|
||||||
setTimeout(() => button.click(), i * 60);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback: Simulate ArrowLeft / ArrowRight keyboard events
|
|
||||||
const clicks = Math.max(1, Math.min(12, Math.round(Math.abs(delta) / 10)));
|
|
||||||
const eventInit = {
|
|
||||||
key: backward ? 'ArrowLeft' : 'ArrowRight',
|
|
||||||
code: backward ? 'ArrowLeft' : 'ArrowRight',
|
|
||||||
keyCode: backward ? 37 : 39,
|
|
||||||
which: backward ? 37 : 39,
|
|
||||||
bubbles: true,
|
|
||||||
cancelable: true,
|
|
||||||
view: window
|
|
||||||
};
|
|
||||||
const target = document.querySelector('.hive-video') || document.activeElement || document.body;
|
|
||||||
for (let i = 0; i < clicks; i++) {
|
|
||||||
setTimeout(() => {
|
|
||||||
target.dispatchEvent(new window.KeyboardEvent('keydown', eventInit));
|
|
||||||
target.dispatchEvent(new window.KeyboardEvent('keyup', eventInit));
|
|
||||||
}, i * 60);
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getDisneyPlusTimeline(video) {
|
|
||||||
if (!isDisneyPlusHost()) return null;
|
|
||||||
// Preferred: exact playhead/duration from the page media player,
|
|
||||||
// relayed by the MAIN-world bridge. Authoritative while fresh.
|
|
||||||
|
|
||||||
// --- Seek Relay Filtering ---
|
// --- Seek Relay Filtering ---
|
||||||
|
|
||||||
// Minimum seek delta (seconds) to report. Prevents HLS/DASH buffering micro-seeks
|
// Minimum seek delta (seconds) to report. Prevents HLS/DASH buffering micro-seeks
|
||||||
|
|
||||||
const range = getSeekableRange(video);
|
// from being relayed to peers as user-initiated seeks.
|
||||||
|
|
||||||
const MIN_SEEK_DELTA = 2.0;
|
const MIN_SEEK_DELTA = 2.0;
|
||||||
|
|
||||||
let lastReportedSeekTime = null; // last currentTime we relayed as a SEEK
|
let lastReportedSeekTime = null; // last currentTime we relayed as a SEEK
|
||||||
lastKnownDisneyPlusDuration = ui.duration;
|
|
||||||
|
|
||||||
let seekDebounceTimer = null; // debounce timer for rapid seek events
|
let seekDebounceTimer = null; // debounce timer for rapid seek events
|
||||||
|
|
||||||
let expectedSeekTime = null; // strictly track programmatic seeks
|
let expectedSeekTime = null; // strictly track programmatic seeks
|
||||||
|
|
||||||
}
|
|
||||||
const nativeStart = Number.isFinite(current)
|
|
||||||
? current - ui.current * nativeScale
|
|
||||||
|
|
||||||
const PAGE_API_SEEK_BRIDGE = 1;
|
const PAGE_API_SEEK_BRIDGE = 1;
|
||||||
// Accurate Disney+ playhead pushed by the MAIN-world page-API bridge
|
// Accurate Disney+ playhead pushed by the MAIN-world page-API bridge
|
||||||
@@ -419,7 +163,7 @@
|
|||||||
// Site-specific player exceptions live here. The default HTML5 path stays below.
|
// Site-specific player exceptions live here. The default HTML5 path stays below.
|
||||||
function getSiteQuirkAdapters() {
|
function getSiteQuirkAdapters() {
|
||||||
return [{
|
return [{
|
||||||
matches() { return matchesPlayerUrls(this.urls); },
|
name: 'disneyplus-page-api',
|
||||||
key: 'disneyPlus',
|
key: 'disneyPlus',
|
||||||
urls: ['disneyplus.com'],
|
urls: ['disneyplus.com'],
|
||||||
matches() { return matchesPlayerUrls(this.urls); },
|
matches() { return matchesPlayerUrls(this.urls); },
|
||||||
@@ -427,18 +171,6 @@
|
|||||||
getDebug(video) {
|
getDebug(video) {
|
||||||
return {
|
return {
|
||||||
name: this.name,
|
name: this.name,
|
||||||
timeline: getDisneyPlusTimeline(video),
|
|
||||||
timelineCandidates: lastDisneyPlusTimelineCandidates,
|
|
||||||
seekButtons: getDisneyPlusSeekButtonLabels()
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}];
|
|
||||||
}
|
|
||||||
|
|
||||||
function getActiveSiteQuirk() {
|
|
||||||
return getSiteQuirkAdapters().find(adapter => adapter.matches()) || null;
|
|
||||||
}
|
|
||||||
|
|
||||||
key: 'disneyPlus',
|
key: 'disneyPlus',
|
||||||
urls: this.urls,
|
urls: this.urls,
|
||||||
timeline: getDisneyPlusTimeline(video)
|
timeline: getDisneyPlusTimeline(video)
|
||||||
@@ -1503,7 +1235,6 @@
|
|||||||
|
|
||||||
function applyAudioSettings(videoEl, settings) {
|
function applyAudioSettings(videoEl, settings) {
|
||||||
|
|
||||||
const mergedSettings = mergeAudioSettings(settings);
|
|
||||||
const mergedSettings = mergeAudioSettings(settings);
|
const mergedSettings = mergeAudioSettings(settings);
|
||||||
|
|
||||||
if (!mergedSettings.enabled || !mergedSettings.compressor?.enabled) {
|
if (!mergedSettings.enabled || !mergedSettings.compressor?.enabled) {
|
||||||
@@ -1515,7 +1246,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const chain = setupAudioChain(videoEl);
|
|
||||||
|
|
||||||
const chain = setupAudioChain(videoEl);
|
const chain = setupAudioChain(videoEl);
|
||||||
|
|
||||||
@@ -1733,79 +1463,6 @@
|
|||||||
|
|
||||||
const video = findVideo();
|
const video = findVideo();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if (video && currentTitle && sameEpisode(currentTitle, expectedTitle)
|
|
||||||
|
|
||||||
&& video.currentTime < 5 && video.readyState >= 1) {
|
|
||||||
|
|
||||||
// Match! Pause at start and report ready.
|
|
||||||
|
|
||||||
if (!video.paused) {
|
|
||||||
|
|
||||||
_setSuppress('paused');
|
|
||||||
|
|
||||||
video.pause();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
stopLobbyPoll();
|
|
||||||
|
|
||||||
chrome.runtime.sendMessage({
|
|
||||||
|
|
||||||
type: 'EPISODE_READY_LOCAL',
|
|
||||||
|
|
||||||
payload: { title: currentTitle }
|
|
||||||
|
|
||||||
}).catch(() => {});
|
|
||||||
|
|
||||||
reportLog(`Episode lobby: Ready for "${currentTitle}"`, 'success');
|
|
||||||
|
|
||||||
return true;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function startLobbyPoll(expectedTitle) {
|
|
||||||
|
|
||||||
stopLobbyPoll();
|
|
||||||
|
|
||||||
_pendingLobbyTitle = expectedTitle;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// NOTE: Do NOT pause here. Three callers reach this function:
|
|
||||||
|
|
||||||
// 1. PAUSE_FOR_LOBBY (initiator): already paused by that handler before calling us.
|
|
||||||
|
|
||||||
// 2. EPISODE_LOBBY (non-initiator): peer may still be on the PREVIOUS episode — pausing
|
|
||||||
|
|
||||||
// would freeze them mid-episode. The pause happens inside checkAndReportLobbyReady()
|
|
||||||
|
|
||||||
// only once their title actually matches.
|
|
||||||
|
|
||||||
// 3. CONTENT_BOOT recovery: same reasoning as (2).
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Check immediately
|
|
||||||
|
|
||||||
if (checkAndReportLobbyReady(expectedTitle)) return;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Poll every 2 seconds — no log spam, internal only
|
|
||||||
|
|
||||||
lobbyPollTimer = setInterval(() => {
|
|
||||||
|
|
||||||
checkAndReportLobbyReady(expectedTitle);
|
|
||||||
|
|
||||||
}, 2000);
|
|
||||||
const currentTitle = getMediaTitle();
|
const currentTitle = getMediaTitle();
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -700,12 +700,6 @@
|
|||||||
<button id="remoteSeekForward" class="secondary" style="flex:1; font-size:12px;">+30s</button>
|
<button id="remoteSeekForward" class="secondary" style="flex:1; font-size:12px;">+30s</button>
|
||||||
</div>
|
</div>
|
||||||
<button id="remoteSeekFiveMin" class="secondary" style="width:100%; font-size:12px; margin-bottom:15px;">Seek 5:00</button>
|
<button id="remoteSeekFiveMin" class="secondary" style="width:100%; font-size:12px; margin-bottom:15px;">Seek 5:00</button>
|
||||||
<div style="margin-top: 15px;">
|
|
||||||
<label style="font-size:10px; font-weight:700; text-transform:uppercase; letter-spacing:0.5px;">DOM Timestamps (Scraped)</label>
|
|
||||||
<div class="info-card" id="devtoolsDomScraperList" style="font-size:10px; max-height:100px; overflow-y:auto; font-family:monospace; line-height:1.2; padding:6px; background:rgba(0,0,0,0.2); white-space:pre-wrap; border: 1px solid #334155;">
|
|
||||||
No timestamps scraped.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div style="font-size:10px; color:var(--text-muted); text-align:center; margin-top:10px; opacity:0.7;">
|
<div style="font-size:10px; color:var(--text-muted); text-align:center; margin-top:10px; opacity:0.7;">
|
||||||
Build: <span id="buildIdentifier">__BUILD_TIMESTAMP__</span>
|
Build: <span id="buildIdentifier">__BUILD_TIMESTAMP__</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2292,13 +2292,6 @@ function refreshDebugInfo() {
|
|||||||
const buttons = Array.isArray(quirk.seekButtons) ? quirk.seekButtons.slice(0, 4).join(' | ') : '';
|
const buttons = Array.isArray(quirk.seekButtons) ? quirk.seekButtons.slice(0, 4).join(' | ') : '';
|
||||||
if (buttons) addField(`${label} Buttons`, buttons);
|
if (buttons) addField(`${label} Buttons`, buttons);
|
||||||
}
|
}
|
||||||
const domScraperEl = document.getElementById('devtoolsDomScraperList');
|
|
||||||
if (domScraperEl && state.scrapedTimestamps) {
|
|
||||||
domScraperEl.textContent = state.scrapedTimestamps.length > 0
|
|
||||||
? state.scrapedTimestamps.join('\n')
|
|
||||||
: 'No timestamps scraped.';
|
|
||||||
}
|
|
||||||
|
|
||||||
addSection('Properties');
|
addSection('Properties');
|
||||||
addField('Seeking', String(state.seeking));
|
addField('Seeking', String(state.seeking));
|
||||||
addField('Ended', String(state.ended));
|
addField('Ended', String(state.ended));
|
||||||
|
|||||||
@@ -77,38 +77,15 @@ function makeDocument(nodes = []) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function makeNode(attrs = {}, textContent = '') {
|
function loadTimelineFns(hostname, document = makeDocument(), pageApiTime = null) {
|
||||||
return {
|
const disneyPageApiTime = pageApiTime
|
||||||
textContent,
|
? `let disneyPageApiTime = { position: ${pageApiTime.position}, duration: ${pageApiTime.duration}, at: Date.now() - ${pageApiTime.ageMs || 0} };`
|
||||||
value: attrs.value,
|
: 'let disneyPageApiTime = null;';
|
||||||
max: attrs.max,
|
|
||||||
clicked: 0,
|
|
||||||
click() { this.clicked += 1; },
|
|
||||||
getAttribute(name) { return attrs[name] ?? null; }
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function loadTimelineFns(hostname, document = makeDocument()) {
|
|
||||||
return Function('window', 'document', [
|
return Function('window', 'document', [
|
||||||
'let lastDisneyPlusTimelineCandidates = [];',
|
disneyPageApiTime,
|
||||||
'let lastKnownDisneyPlusDuration = 0;',
|
|
||||||
'let lastKnownDisneyPlusScale = 1;',
|
|
||||||
'let lastKnownDisneyPlusStart = 0;',
|
|
||||||
'let lastDisneyPlusUiCurrent = null;',
|
|
||||||
'let lastDisneyPlusNativeAtUi = null;',
|
|
||||||
'let disneyPageApiTime = null;',
|
|
||||||
extractFunction('scanShadowDom', source),
|
|
||||||
extractFunction('querySelectorAllShadow', source),
|
|
||||||
extractFunction('hostMatchesUrl', source),
|
extractFunction('hostMatchesUrl', source),
|
||||||
extractFunction('matchesPlayerUrls', source),
|
extractFunction('matchesPlayerUrls', source),
|
||||||
extractFunction('isDisneyPlusHost', source),
|
extractFunction('isDisneyPlusHost', source),
|
||||||
extractFunction('getSeekableRange', source),
|
|
||||||
extractFunction('parseClockTime', source),
|
|
||||||
extractFunction('parseTimelineText', source),
|
|
||||||
extractFunction('getDisneyPlusUiTimeline', source),
|
|
||||||
extractFunction('getElementLabel', source),
|
|
||||||
extractFunction('getDisneyPlusSeekButtonLabels', source),
|
|
||||||
extractFunction('clickDisneyPlusRelativeSeek', source),
|
|
||||||
extractFunction('getDisneyPlusTimeline', source),
|
extractFunction('getDisneyPlusTimeline', source),
|
||||||
extractFunction('getSiteQuirkAdapters', source),
|
extractFunction('getSiteQuirkAdapters', source),
|
||||||
extractFunction('getActiveSiteQuirk', source),
|
extractFunction('getActiveSiteQuirk', source),
|
||||||
@@ -117,7 +94,7 @@ function loadTimelineFns(hostname, document = makeDocument()) {
|
|||||||
extractFunction('getSyncCurrentTime', source),
|
extractFunction('getSyncCurrentTime', source),
|
||||||
extractFunction('getSyncDuration', source),
|
extractFunction('getSyncDuration', source),
|
||||||
extractFunction('toNativeSeekTime', source),
|
extractFunction('toNativeSeekTime', source),
|
||||||
'return { getActiveSiteQuirk, getSyncCurrentTime, getSyncDuration, toNativeSeekTime, clickDisneyPlusRelativeSeek };'
|
'return { getActiveSiteQuirk, getSyncCurrentTime, getSyncDuration, toNativeSeekTime };'
|
||||||
].join('\n'))({ location: { hostname } }, document);
|
].join('\n'))({ location: { hostname } }, document);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,11 +108,11 @@ function loadPlayerFixFns(hostname) {
|
|||||||
].join('\n'))({ location: { hostname } });
|
].join('\n'))({ location: { hostname } });
|
||||||
}
|
}
|
||||||
|
|
||||||
const disneyUiDocument = makeDocument([
|
const disneyFns = loadTimelineFns('www.disneyplus.com', makeDocument(), {
|
||||||
makeNode({ 'aria-valuetext': '0:09 / 180:00' })
|
position: 9,
|
||||||
]);
|
duration: 10800
|
||||||
const disneyFns = loadTimelineFns('www.disneyplus.com', disneyUiDocument);
|
});
|
||||||
assert.equal(disneyFns.getActiveSiteQuirk().name, 'disneyplus-timeline-and-buttons');
|
assert.equal(disneyFns.getActiveSiteQuirk().name, 'disneyplus-page-api');
|
||||||
assert.deepEqual(disneyFns.getActiveSiteQuirk().urls, ['disneyplus.com']);
|
assert.deepEqual(disneyFns.getActiveSiteQuirk().urls, ['disneyplus.com']);
|
||||||
const disneyVideo = makeVideo('disney-offset', 1920, 1080, {
|
const disneyVideo = makeVideo('disney-offset', 1920, 1080, {
|
||||||
currentTime: 29,
|
currentTime: 29,
|
||||||
@@ -144,17 +121,17 @@ const disneyVideo = makeVideo('disney-offset', 1920, 1080, {
|
|||||||
});
|
});
|
||||||
assert.equal(disneyFns.getSyncCurrentTime(disneyVideo), 9);
|
assert.equal(disneyFns.getSyncCurrentTime(disneyVideo), 9);
|
||||||
assert.equal(disneyFns.getSyncDuration(disneyVideo), 10800);
|
assert.equal(disneyFns.getSyncDuration(disneyVideo), 10800);
|
||||||
assert.equal(disneyFns.toNativeSeekTime(disneyVideo, 39), 119);
|
assert.equal(disneyFns.toNativeSeekTime(disneyVideo, 39), 39);
|
||||||
|
|
||||||
const disneySeekableFallbackFns = loadTimelineFns('www.disneyplus.com');
|
const disneyNoPageApiFns = loadTimelineFns('www.disneyplus.com');
|
||||||
const disneyOffsetVideo = makeVideo('disney-offset', 1920, 1080, {
|
const disneyOffsetVideo = makeVideo('disney-offset', 1920, 1080, {
|
||||||
currentTime: 29,
|
currentTime: 29,
|
||||||
duration: 0,
|
duration: 0,
|
||||||
seekable: makeSeekable([[20, 10820]])
|
seekable: makeSeekable([[20, 10820]])
|
||||||
});
|
});
|
||||||
assert.equal(disneySeekableFallbackFns.getSyncCurrentTime(disneyOffsetVideo), 9);
|
assert.equal(disneyNoPageApiFns.getSyncCurrentTime(disneyOffsetVideo), 29);
|
||||||
assert.equal(disneySeekableFallbackFns.getSyncDuration(disneyOffsetVideo), 10800);
|
assert.equal(disneyNoPageApiFns.getSyncDuration(disneyOffsetVideo), 0);
|
||||||
assert.equal(disneySeekableFallbackFns.toNativeSeekTime(disneyOffsetVideo, 39), 59);
|
assert.equal(disneyNoPageApiFns.toNativeSeekTime(disneyOffsetVideo, 39), 39);
|
||||||
|
|
||||||
const genericFns = loadTimelineFns('example.com');
|
const genericFns = loadTimelineFns('example.com');
|
||||||
assert.equal(genericFns.getActiveSiteQuirk(), null);
|
assert.equal(genericFns.getActiveSiteQuirk(), null);
|
||||||
@@ -162,12 +139,6 @@ assert.equal(genericFns.getSyncCurrentTime(disneyVideo), 29);
|
|||||||
assert.equal(genericFns.getSyncDuration(disneyVideo), 0);
|
assert.equal(genericFns.getSyncDuration(disneyVideo), 0);
|
||||||
assert.equal(genericFns.toNativeSeekTime(disneyVideo, 39), 39);
|
assert.equal(genericFns.toNativeSeekTime(disneyVideo, 39), 39);
|
||||||
|
|
||||||
const backButton = makeNode({ 'aria-label': '10 Sekunden zurück' });
|
|
||||||
const forwardButton = makeNode({ 'aria-label': '10 Sekunden vorspulen' });
|
|
||||||
const disneyButtonFns = loadTimelineFns('www.disneyplus.com', makeDocument([backButton, forwardButton]));
|
|
||||||
assert.equal(disneyButtonFns.clickDisneyPlusRelativeSeek(-30), true);
|
|
||||||
assert.equal(disneyButtonFns.clickDisneyPlusRelativeSeek(30), true);
|
|
||||||
|
|
||||||
const twitchFixFns = loadPlayerFixFns('player.twitch.tv');
|
const twitchFixFns = loadPlayerFixFns('player.twitch.tv');
|
||||||
assert.equal(twitchFixFns.getActivePlayerActionFix().name, 'twitch-player-buttons');
|
assert.equal(twitchFixFns.getActivePlayerActionFix().name, 'twitch-player-buttons');
|
||||||
assert.deepEqual(twitchFixFns.getActivePlayerActionFix().urls, ['twitch.tv']);
|
assert.deepEqual(twitchFixFns.getActivePlayerActionFix().urls, ['twitch.tv']);
|
||||||
|
|||||||
Reference in New Issue
Block a user