Generalize page API seek overrides

This commit is contained in:
KoalaDev
2026-07-02 09:57:57 +02:00
parent 57cd071d58
commit 52265e84eb
3 changed files with 86 additions and 32 deletions
+56 -26
View File
@@ -4,6 +4,7 @@ import { loadLocale, getMessage, getSystemLanguage } from './i18n.js';
import { sameEpisode } from './episode-utils.js';
import { applyTitlePrivacyToPayload, sanitizeSharedTitle, sanitizeTabTitle, normalizeSendTabTitle, normalizeTitlePrivacyMode } from './title-privacy.js';
import { initTabManager } from './modules/tab-manager.js';
import './page-api-seek-overrides.js';
// --- Uninstall URL Initialization ---
let uninstallURLInitPromise = null;
@@ -1648,62 +1649,87 @@ async function simulateRemoteSeek(delta) {
return { status: 'ok', targetTime };
}
function isNetflixUrl(url) {
try {
const host = new URL(url).hostname.toLowerCase();
return host === 'netflix.com' || host.endsWith('.netflix.com');
} catch (_e) {
return false;
}
async function devRemoteToolsAllowed() {
const data = await chrome.storage.local.get(['username']);
return data.username === 'KoalaDev';
}
function installNetflixSeekBridge() {
if (window.__koalaNetflixSeekBridgeInstalled) return;
window.__koalaNetflixSeekBridgeInstalled = true;
function shouldUsePageApiSeek(url) {
return typeof globalThis.koalaFindPageApiSeekProvider === 'function' &&
!!globalThis.koalaFindPageApiSeekProvider(url);
}
function installPageApiSeekBridge() {
if (window.__koalaPageApiSeekBridgeInstalled) return;
window.__koalaPageApiSeekBridgeInstalled = true;
function seekWithPageApi(time) {
const match = typeof window.koalaFindPageApiSeekProvider === 'function'
? window.koalaFindPageApiSeekProvider(window.location.hostname)
: null;
if (!match) return;
function getNetflixPlayer() {
try {
const videoPlayer = window.netflix?.appContext?.state?.playerApp?.getAPI?.().videoPlayer;
const sessionId = videoPlayer?.getAllPlayerSessionIds?.()[0];
return sessionId ? videoPlayer.getVideoPlayerBySessionId(sessionId) : null;
if (match.provider === 'netflix') {
const videoPlayer = window.netflix?.appContext?.state?.playerApp?.getAPI?.().videoPlayer;
const sessionId = videoPlayer?.getAllPlayerSessionIds?.()[0];
const player = sessionId ? videoPlayer.getVideoPlayerBySessionId(sessionId) : null;
player?.seek(Math.round(time * 1000));
}
} catch (_e) {
return null;
// Player not ready or private API changed; the next sync tick can retry.
}
}
window.addEventListener('message', (event) => {
if (event.source !== window) return;
const data = event.data;
if (!data || data.__koalaNetflixSeek !== 1 || data.kind !== 'seek' || typeof data.time !== 'number') return;
try {
getNetflixPlayer()?.seek(Math.round(data.time * 1000));
} catch (_e) {
// Netflix player not ready or private API changed; the next sync tick can retry.
}
if (!data || data.__koalaPageApiSeek !== 1 || data.kind !== 'seek' || typeof data.time !== 'number') return;
seekWithPageApi(data.time);
});
}
function setPageApiSeekEnabled(enabled) {
window.KOALA_PAGE_API_SEEK_ENABLED = enabled === true;
}
async function injectContentScript(tabId) {
let isNetflix = false;
let needsPageApiSeek = false;
let pageApiSeekReady = false;
try {
const tab = await chrome.tabs.get(tabId);
isNetflix = isNetflixUrl(tab?.url || '');
needsPageApiSeek = shouldUsePageApiSeek(tab?.url || '');
} catch (_e) {
// Fall through to the generic content script injection.
}
if (isNetflix) {
if (needsPageApiSeek) {
try {
await chrome.scripting.executeScript({
target: { tabId },
world: 'MAIN',
func: installNetflixSeekBridge
files: ['page-api-seek-overrides.js']
});
await chrome.scripting.executeScript({
target: { tabId },
world: 'MAIN',
func: installPageApiSeekBridge
});
pageApiSeekReady = true;
} catch (err) {
addLog(`Netflix seek bridge injection failed: ${err.message}`, 'warn');
addLog(`Page API seek bridge injection failed: ${err.message}`, 'warn');
}
}
await chrome.scripting.executeScript({
target: { tabId },
files: ['page-api-seek-overrides.js']
});
await chrome.scripting.executeScript({
target: { tabId },
func: setPageApiSeekEnabled,
args: [pageApiSeekReady]
});
return chrome.scripting.executeScript({
target: { tabId },
files: ['content.js']
@@ -2113,6 +2139,10 @@ async function handleAsyncMessage(message, sender, sendResponse) {
}
});
} else if (message.type === 'DEV_SIMULATE_REMOTE_SEEK') {
if (!(await devRemoteToolsAllowed())) {
sendResponse({ status: 'forbidden' });
return;
}
const delta = Number(message.delta);
if (!Number.isFinite(delta)) {
sendResponse({ status: 'invalid_delta' });
+7 -6
View File
@@ -57,17 +57,18 @@
let seekDebounceTimer = null; // debounce timer for rapid seek events
let expectedSeekTime = null; // strictly track programmatic seeks
const NETFLIX_SEEK_BRIDGE = 1;
const PAGE_API_SEEK_BRIDGE = 1;
function isNetflixHost() {
const host = window.location.hostname.toLowerCase();
return host === 'netflix.com' || host.endsWith('.netflix.com');
function shouldUsePageApiSeek() {
return window.KOALA_PAGE_API_SEEK_ENABLED === true &&
typeof window.koalaFindPageApiSeekProvider === 'function' &&
!!window.koalaFindPageApiSeekProvider(window.location.hostname);
}
function seekVideo(video, targetTime) {
expectedSeekTime = targetTime;
if (isNetflixHost()) {
window.postMessage({ __koalaNetflixSeek: NETFLIX_SEEK_BRIDGE, kind: 'seek', time: targetTime }, '*');
if (shouldUsePageApiSeek()) {
window.postMessage({ __koalaPageApiSeek: PAGE_API_SEEK_BRIDGE, kind: 'seek', time: targetTime }, '*');
return;
}
video.currentTime = targetTime;
+23
View File
@@ -0,0 +1,23 @@
(function(root) {
const PAGE_API_SEEK_PROVIDERS = [
{ domain: 'netflix.com', provider: 'netflix' } // Avoids M7375 when seeking via video.currentTime.
];
function normalizeHost(input) {
try {
return new URL(input).hostname.toLowerCase();
} catch (_e) {
return String(input || '').toLowerCase();
}
}
function matchesDomain(host, domain) {
return host === domain || host.endsWith(`.${domain}`);
}
root.KOALA_PAGE_API_SEEK_PROVIDERS = PAGE_API_SEEK_PROVIDERS;
root.koalaFindPageApiSeekProvider = (input) => {
const host = normalizeHost(input);
return PAGE_API_SEEK_PROVIDERS.find(entry => matchesDomain(host, entry.domain)) || null;
};
})(globalThis);