mirror of
https://github.com/Shik3i/KoalaSync.git
synced 2026-08-28 11:37:16 +00:00
Merge pull request #19 from Shik3i/codex/netflix-seek-fix
[codex] Fix Netflix and Disney+ seek handling
This commit is contained in:
+16
-1
@@ -60,6 +60,22 @@
|
|||||||
- **Legal/moderation:** Unclear what moderation requirements would apply if users can exchange chat messages. Could be relevant depending on jurisdiction.
|
- **Legal/moderation:** Unclear what moderation requirements would apply if users can exchange chat messages. Could be relevant depending on jurisdiction.
|
||||||
- **Status:** Under evaluation, may come later.
|
- **Status:** Under evaluation, may come later.
|
||||||
|
|
||||||
|
### Cross-frame video detection and control
|
||||||
|
|
||||||
|
- **Priority:** P3
|
||||||
|
- **Category:** Compatibility / Embedded Players
|
||||||
|
- **Background:** KoalaSync currently injects on demand into the selected tab's top frame. This works for normal top-frame players, including current Emby/Jellyfin usage, but does not cover cases where the real `<video>` lives inside a cross-origin iframe or an `about:blank`/`srcdoc` player frame.
|
||||||
|
- **Possible approach:** Add an opt-in frame bridge where child frames announce detected videos to the top frame, and the top frame routes remote play/pause/seek commands to the active child video.
|
||||||
|
- **Status:** Future compatibility work, not needed for current Emby behavior.
|
||||||
|
|
||||||
|
### Local extension E2E smoke tests
|
||||||
|
|
||||||
|
- **Priority:** P2
|
||||||
|
- **Category:** Testing / Release Confidence
|
||||||
|
- **Background:** The release verification covers unit tests, server integration, syntax, lint, audits, and builds, but it does not currently run a real browser extension flow. A small local E2E smoke suite would catch regressions in content-script injection, tab navigation reinjection, remote seek handling, and iframe player support.
|
||||||
|
- **Possible approach:** Add a separate local-only Playwright smoke command that loads the unpacked extension, opens two controlled video pages, and verifies play/pause/seek through the actual extension path. Keep it outside `npm run verify` until it is stable enough for CI.
|
||||||
|
- **Status:** Backlog, recommended before larger content-script or frame-bridge changes.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## ❌ Rejected
|
## ❌ Rejected
|
||||||
@@ -70,4 +86,3 @@
|
|||||||
|---|---|
|
|---|---|
|
||||||
| *(none yet)* | |
|
| *(none yet)* | |
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+216
-8
@@ -4,6 +4,7 @@ import { loadLocale, getMessage, getSystemLanguage } from './i18n.js';
|
|||||||
import { sameEpisode } from './episode-utils.js';
|
import { sameEpisode } from './episode-utils.js';
|
||||||
import { applyTitlePrivacyToPayload, sanitizeSharedTitle, sanitizeTabTitle, normalizeSendTabTitle, normalizeTitlePrivacyMode } from './title-privacy.js';
|
import { applyTitlePrivacyToPayload, sanitizeSharedTitle, sanitizeTabTitle, normalizeSendTabTitle, normalizeTitlePrivacyMode } from './title-privacy.js';
|
||||||
import { initTabManager } from './modules/tab-manager.js';
|
import { initTabManager } from './modules/tab-manager.js';
|
||||||
|
import './page-api-seek-overrides.js';
|
||||||
|
|
||||||
// --- Uninstall URL Initialization ---
|
// --- Uninstall URL Initialization ---
|
||||||
let uninstallURLInitPromise = null;
|
let uninstallURLInitPromise = null;
|
||||||
@@ -1592,6 +1593,188 @@ async function routeToContent(action, payload) {
|
|||||||
_routeToContentInternal(tabId, action, payload, actionTimestamp, commandSenderId, 0);
|
_routeToContentInternal(tabId, action, payload, actionTimestamp, commandSenderId, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getTabVideoState(tabId) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
chrome.tabs.sendMessage(tabId, { type: 'GET_VIDEO_STATE' }, (res) => {
|
||||||
|
if (chrome.runtime.lastError) {
|
||||||
|
resolve({ error: chrome.runtime.lastError.message });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
resolve(res);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getReadyTabVideoState(tabId) {
|
||||||
|
let state = await getTabVideoState(tabId);
|
||||||
|
if (!state || state.error) {
|
||||||
|
await injectContentScript(tabId);
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 250));
|
||||||
|
state = await getTabVideoState(tabId);
|
||||||
|
}
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function simulateRemoteSeek(delta, explicitTargetTime = null) {
|
||||||
|
if (!currentTabId) return { status: 'no_tab' };
|
||||||
|
const tabId = parseInt(currentTabId);
|
||||||
|
if (isNaN(tabId)) return { status: 'no_tab' };
|
||||||
|
|
||||||
|
const state = await getReadyTabVideoState(tabId);
|
||||||
|
if (!state || state.error) return { status: 'error', message: state?.error || 'No video state' };
|
||||||
|
if (!state.found || !Number.isFinite(state.currentTime)) return { status: 'no_video' };
|
||||||
|
|
||||||
|
let targetTime = explicitTargetTime !== null ? explicitTargetTime : Math.max(0, state.currentTime + (delta || 0));
|
||||||
|
if (Number.isFinite(state.duration) && state.duration > 0) {
|
||||||
|
targetTime = Math.min(targetTime, Math.max(0, state.duration - 0.1));
|
||||||
|
}
|
||||||
|
|
||||||
|
const senderId = 'KoalaDev';
|
||||||
|
const timestamp = Date.now();
|
||||||
|
const payload = {
|
||||||
|
senderId,
|
||||||
|
actionTimestamp: timestamp,
|
||||||
|
currentTime: targetTime,
|
||||||
|
targetTime
|
||||||
|
};
|
||||||
|
|
||||||
|
addToHistory(EVENTS.SEEK, senderId);
|
||||||
|
showNotification(senderId, EVENTS.SEEK);
|
||||||
|
updateLastAction(EVENTS.SEEK, senderId, timestamp);
|
||||||
|
lastActionState.targetTime = targetTime;
|
||||||
|
if (storageInitialized) chrome.storage.session.set({ lastActionState });
|
||||||
|
updateLocalPeerState(senderId, { currentTime: targetTime });
|
||||||
|
routeToContent(EVENTS.SEEK, payload);
|
||||||
|
|
||||||
|
return { status: 'ok', targetTime };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function devRemoteToolsAllowed() {
|
||||||
|
const data = await chrome.storage.local.get(['username']);
|
||||||
|
return data.username === 'KoalaDev';
|
||||||
|
}
|
||||||
|
|
||||||
|
function shouldUsePageApiSeek(url) {
|
||||||
|
return typeof globalThis.koalaFindPageApiSeekProvider === 'function' &&
|
||||||
|
!!globalThis.koalaFindPageApiSeekProvider(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
function installPageApiSeekBridge() {
|
||||||
|
if (window.__koalaPageApiSeekBridgeInstalled) return;
|
||||||
|
window.__koalaPageApiSeekBridgeInstalled = true;
|
||||||
|
|
||||||
|
function currentMatch() {
|
||||||
|
return typeof window.koalaFindPageApiSeekProvider === 'function'
|
||||||
|
? window.koalaFindPageApiSeekProvider(window.location.hostname)
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disney+ ("hive"/BAM) player: the real media player hangs off the
|
||||||
|
// <disney-web-player> custom element as `.mediaPlayer`, exposing precise
|
||||||
|
// seek(ms) and timeline.info (playhead/duration in ms).
|
||||||
|
function disneyMediaPlayer() {
|
||||||
|
const el = document.querySelector('disney-web-player');
|
||||||
|
return el && el.mediaPlayer ? el.mediaPlayer : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function seekWithPageApi(time) {
|
||||||
|
const match = currentMatch();
|
||||||
|
if (!match) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (match.provider === 'netflix') {
|
||||||
|
const videoPlayer = window.netflix?.appContext?.state?.playerApp?.getAPI?.().videoPlayer;
|
||||||
|
const ids = videoPlayer?.getAllPlayerSessionIds?.();
|
||||||
|
const sessionId = ids ? ids[0] : null;
|
||||||
|
const player = sessionId ? videoPlayer.getVideoPlayerBySessionId(sessionId) : null;
|
||||||
|
player?.seek(Math.round(time * 1000));
|
||||||
|
} else if (match.provider === 'disney') {
|
||||||
|
const mp = disneyMediaPlayer();
|
||||||
|
if (mp && typeof mp.seek === 'function') mp.seek(Math.round(time * 1000));
|
||||||
|
}
|
||||||
|
} catch (_e) {
|
||||||
|
// 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.__koalaPageApiSeek !== 1 || data.kind !== 'seek' || typeof data.time !== 'number') return;
|
||||||
|
seekWithPageApi(data.time);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Disney+'s <video> currentTime is blob-relative and its scrubber lags, so
|
||||||
|
// the isolated-world content script can't read an accurate position. Push
|
||||||
|
// the real playhead/duration (seconds) from the page's media player.
|
||||||
|
setInterval(() => {
|
||||||
|
try {
|
||||||
|
const match = currentMatch();
|
||||||
|
if (!match || match.provider !== 'disney') return;
|
||||||
|
const mp = disneyMediaPlayer();
|
||||||
|
const info = mp && mp.timeline && mp.timeline.info;
|
||||||
|
if (!info || typeof info.playheadPositionMs !== 'number' || typeof info.programDurationMs !== 'number') return;
|
||||||
|
if (info.programDurationMs <= 0) return;
|
||||||
|
window.postMessage({
|
||||||
|
__koalaPlayerTime: 1,
|
||||||
|
provider: 'disney',
|
||||||
|
position: info.playheadPositionMs / 1000,
|
||||||
|
duration: info.programDurationMs / 1000
|
||||||
|
}, '*');
|
||||||
|
} catch (_e) {
|
||||||
|
// Ignore transient errors (player teardown / element swap).
|
||||||
|
}
|
||||||
|
}, 250);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setPageApiSeekEnabled(enabled) {
|
||||||
|
window.KOALA_PAGE_API_SEEK_ENABLED = enabled === true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function injectContentScript(tabId) {
|
||||||
|
let needsPageApiSeek = false;
|
||||||
|
let pageApiSeekReady = false;
|
||||||
|
try {
|
||||||
|
const tab = await chrome.tabs.get(tabId);
|
||||||
|
const url = tab?.url || '';
|
||||||
|
needsPageApiSeek = shouldUsePageApiSeek(url);
|
||||||
|
} catch (_e) {
|
||||||
|
// Fall through to the generic content script injection.
|
||||||
|
}
|
||||||
|
|
||||||
|
if (needsPageApiSeek) {
|
||||||
|
try {
|
||||||
|
await chrome.scripting.executeScript({
|
||||||
|
target: { tabId },
|
||||||
|
world: 'MAIN',
|
||||||
|
files: ['page-api-seek-overrides.js']
|
||||||
|
});
|
||||||
|
await chrome.scripting.executeScript({
|
||||||
|
target: { tabId },
|
||||||
|
world: 'MAIN',
|
||||||
|
func: installPageApiSeekBridge
|
||||||
|
});
|
||||||
|
pageApiSeekReady = true;
|
||||||
|
} catch (err) {
|
||||||
|
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']
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function _routeToContentInternal(tabId, action, payload, actionTimestamp, commandSenderId, retries) {
|
function _routeToContentInternal(tabId, action, payload, actionTimestamp, commandSenderId, retries) {
|
||||||
chrome.tabs.sendMessage(tabId, {
|
chrome.tabs.sendMessage(tabId, {
|
||||||
type: 'SERVER_COMMAND',
|
type: 'SERVER_COMMAND',
|
||||||
@@ -1606,10 +1789,7 @@ function _routeToContentInternal(tabId, action, payload, actionTimestamp, comman
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (err.message.includes('Receiving end does not exist') || err.message.includes('Extension context invalidated')) {
|
if (err.message.includes('Receiving end does not exist') || err.message.includes('Extension context invalidated')) {
|
||||||
chrome.scripting.executeScript({
|
injectContentScript(tabId).then(() => {
|
||||||
target: { tabId },
|
|
||||||
files: ['content.js']
|
|
||||||
}).then(() => {
|
|
||||||
setTimeout(() => _routeToContentInternal(tabId, action, payload, actionTimestamp, commandSenderId, retries + 1), 500);
|
setTimeout(() => _routeToContentInternal(tabId, action, payload, actionTimestamp, commandSenderId, retries + 1), 500);
|
||||||
}).catch(_err => {
|
}).catch(_err => {
|
||||||
addLog(`Auto-reinject failed for tab ${tabId}`, 'warn');
|
addLog(`Auto-reinject failed for tab ${tabId}`, 'warn');
|
||||||
@@ -1997,6 +2177,22 @@ async function handleAsyncMessage(message, sender, sendResponse) {
|
|||||||
sendResponse(res);
|
sendResponse(res);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
} else if (message.type === 'DEV_SIMULATE_REMOTE_SEEK') {
|
||||||
|
if (!(await devRemoteToolsAllowed())) {
|
||||||
|
sendResponse({ status: 'forbidden' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const delta = message.delta !== null && message.delta !== undefined ? Number(message.delta) : null;
|
||||||
|
const targetTime = message.targetTime !== null && message.targetTime !== undefined ? Number(message.targetTime) : null;
|
||||||
|
|
||||||
|
if (delta === null && targetTime === null) {
|
||||||
|
sendResponse({ status: 'invalid_params' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
simulateRemoteSeek(delta, targetTime).then(sendResponse).catch(err => {
|
||||||
|
addLog(`Remote seek simulation failed: ${err.message}`, 'warn');
|
||||||
|
sendResponse({ status: 'error', message: err.message });
|
||||||
|
});
|
||||||
} else if (message.type === 'CONTENT_EVENT') {
|
} else if (message.type === 'CONTENT_EVENT') {
|
||||||
const processEvent = async () => {
|
const processEvent = async () => {
|
||||||
// Host Control Mode (sender-side): a non-controller in host-only mode must
|
// Host Control Mode (sender-side): a non-controller in host-only mode must
|
||||||
@@ -2196,6 +2392,20 @@ async function handleAsyncMessage(message, sender, sendResponse) {
|
|||||||
addLog('Heartbeat settings error: ' + err.message, 'error');
|
addLog('Heartbeat settings error: ' + err.message, 'error');
|
||||||
sendResponse({ status: 'ok' });
|
sendResponse({ status: 'ok' });
|
||||||
});
|
});
|
||||||
|
} else if (message.type === 'INJECT_CONTENT_SCRIPT') {
|
||||||
|
const tabId = Number(message.tabId);
|
||||||
|
if (!Number.isInteger(tabId)) {
|
||||||
|
sendResponse({ status: 'invalid_tab' });
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
injectContentScript(tabId).then(() => {
|
||||||
|
sendResponse({ status: 'ok' });
|
||||||
|
}).catch(err => {
|
||||||
|
addLog(`Failed to inject into tab: ${err.message}`, 'warn');
|
||||||
|
sendResponse({ status: 'error', message: err.message });
|
||||||
|
});
|
||||||
|
return true;
|
||||||
} else if (message.type === 'SET_TARGET_TAB') {
|
} else if (message.type === 'SET_TARGET_TAB') {
|
||||||
const previousTabId = currentTabId;
|
const previousTabId = currentTabId;
|
||||||
currentTabId = message.tabId;
|
currentTabId = message.tabId;
|
||||||
@@ -2213,10 +2423,7 @@ async function handleAsyncMessage(message, sender, sendResponse) {
|
|||||||
|
|
||||||
if (currentTabId) {
|
if (currentTabId) {
|
||||||
const selectedTabId = currentTabId;
|
const selectedTabId = currentTabId;
|
||||||
chrome.scripting.executeScript({
|
injectContentScript(selectedTabId)
|
||||||
target: { tabId: selectedTabId },
|
|
||||||
files: ['content.js']
|
|
||||||
})
|
|
||||||
.then(() => applyAudioSettingsToTab(selectedTabId))
|
.then(() => applyAudioSettingsToTab(selectedTabId))
|
||||||
.catch(err => {
|
.catch(err => {
|
||||||
addLog(`Failed to inject into tab: ${err.message}`, 'warn');
|
addLog(`Failed to inject into tab: ${err.message}`, 'warn');
|
||||||
@@ -2399,6 +2606,7 @@ initTabManager({
|
|||||||
getSettings,
|
getSettings,
|
||||||
emit,
|
emit,
|
||||||
applyAudioSettingsToTab,
|
applyAudioSettingsToTab,
|
||||||
|
injectContentScript,
|
||||||
ensureState,
|
ensureState,
|
||||||
EVENTS
|
EVENTS
|
||||||
});
|
});
|
||||||
|
|||||||
+1639
-1513
File diff suppressed because it is too large
Load Diff
@@ -12,6 +12,7 @@ export function initTabManager({
|
|||||||
getSettings,
|
getSettings,
|
||||||
emit,
|
emit,
|
||||||
applyAudioSettingsToTab,
|
applyAudioSettingsToTab,
|
||||||
|
injectContentScript,
|
||||||
ensureState,
|
ensureState,
|
||||||
EVENTS
|
EVENTS
|
||||||
}) {
|
}) {
|
||||||
@@ -83,10 +84,7 @@ export function initTabManager({
|
|||||||
await ensureState();
|
await ensureState();
|
||||||
const curTabId = getCurrentTabId();
|
const curTabId = getCurrentTabId();
|
||||||
if (curTabId && tabId === parseInt(curTabId) && changeInfo.status === 'complete') {
|
if (curTabId && tabId === parseInt(curTabId) && changeInfo.status === 'complete') {
|
||||||
chrome.scripting.executeScript({
|
injectContentScript(tabId)
|
||||||
target: { tabId },
|
|
||||||
files: ['content.js']
|
|
||||||
})
|
|
||||||
.then(() => applyAudioSettingsToTab(tabId))
|
.then(() => applyAudioSettingsToTab(tabId))
|
||||||
.catch(() => {});
|
.catch(() => {});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
(function(root) {
|
||||||
|
const PAGE_API_SEEK_FIXES = [
|
||||||
|
{
|
||||||
|
name: 'netflix-page-api-seek',
|
||||||
|
urls: ['netflix.com'],
|
||||||
|
provider: 'netflix'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'disney-page-api-seek',
|
||||||
|
urls: ['disneyplus.com'],
|
||||||
|
provider: 'disney'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
function normalizeHost(input) {
|
||||||
|
try {
|
||||||
|
return new URL(input).hostname.toLowerCase();
|
||||||
|
} catch (_e) {
|
||||||
|
return String(input || '').toLowerCase();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function matchesDomain(host, domain) {
|
||||||
|
const normalizedDomain = normalizeHost(domain);
|
||||||
|
return normalizedDomain && (host === normalizedDomain || host.endsWith(`.${normalizedDomain}`));
|
||||||
|
}
|
||||||
|
|
||||||
|
root.KOALA_PAGE_API_SEEK_FIXES = PAGE_API_SEEK_FIXES;
|
||||||
|
root.KOALA_PAGE_API_SEEK_PROVIDERS = PAGE_API_SEEK_FIXES;
|
||||||
|
root.koalaFindPageApiSeekProvider = (input) => {
|
||||||
|
const host = normalizeHost(input);
|
||||||
|
return PAGE_API_SEEK_FIXES.find(entry =>
|
||||||
|
Array.isArray(entry.urls) && entry.urls.some(url => matchesDomain(host, url))
|
||||||
|
) || null;
|
||||||
|
};
|
||||||
|
})(globalThis);
|
||||||
@@ -391,6 +391,7 @@
|
|||||||
<button class="tab-btn" data-tab="tab-sync" data-i18n="TAB_SYNC" data-i18n-title="TAB_SYNC_TOOLTIP" title="Video sync controls and remote actions">Sync</button>
|
<button class="tab-btn" data-tab="tab-sync" data-i18n="TAB_SYNC" data-i18n-title="TAB_SYNC_TOOLTIP" title="Video sync controls and remote actions">Sync</button>
|
||||||
<button class="tab-btn" data-tab="tab-settings" data-i18n="TAB_SETTINGS" data-i18n-title="TAB_SETTINGS_TOOLTIP" title="Extension preferences">Settings</button>
|
<button class="tab-btn" data-tab="tab-settings" data-i18n="TAB_SETTINGS" data-i18n-title="TAB_SETTINGS_TOOLTIP" title="Extension preferences">Settings</button>
|
||||||
<button class="tab-btn" data-tab="tab-dev" data-i18n="TAB_STATUS" data-i18n-title="TAB_STATUS_TOOLTIP" title="Advanced Diagnostics & Logs">Status</button>
|
<button class="tab-btn" data-tab="tab-dev" data-i18n="TAB_STATUS" data-i18n-title="TAB_STATUS_TOOLTIP" title="Advanced Diagnostics & Logs">Status</button>
|
||||||
|
<button id="devToolsTabBtn" class="tab-btn" data-tab="tab-devtools" style="display:none;">Dev</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Room Tab -->
|
<!-- Room Tab -->
|
||||||
@@ -692,6 +693,18 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div id="tab-devtools" class="tab-content">
|
||||||
|
<label>Remote Seek</label>
|
||||||
|
<div class="info-card" style="display:flex; gap:8px; margin-bottom:15px;">
|
||||||
|
<button id="remoteSeekBack" class="secondary" style="flex:1; font-size:12px;">-30s</button>
|
||||||
|
<button id="remoteSeekForward" class="secondary" style="flex:1; font-size:12px;">+30s</button>
|
||||||
|
</div>
|
||||||
|
<button id="remoteSeekFiveMin" class="secondary" style="width:100%; font-size:12px; margin-bottom:15px;">Seek 5:00</button>
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script src="popup.js" type="module"></script>
|
<script src="popup.js" type="module"></script>
|
||||||
|
|
||||||
<!-- Onboarding Overlay -->
|
<!-- Onboarding Overlay -->
|
||||||
|
|||||||
+86
-15
@@ -2,7 +2,7 @@ import { EVENTS, OFFICIAL_LANDING_PAGE_URL, SUPPORT_URL, getReviewUrl } from './
|
|||||||
import { BLACKLIST_DOMAINS } from './shared/blacklist.js';
|
import { BLACKLIST_DOMAINS } from './shared/blacklist.js';
|
||||||
import { getAvatarForName, generateUsername, USERNAME_ADJECTIVES, USERNAME_NOUNS } from './shared/names.js';
|
import { getAvatarForName, generateUsername, USERNAME_ADJECTIVES, USERNAME_NOUNS } from './shared/names.js';
|
||||||
import { loadLocale, translateDOM, getMessage, getSystemLanguage } from './i18n.js';
|
import { loadLocale, translateDOM, getMessage, getSystemLanguage } from './i18n.js';
|
||||||
import { TITLE_PRIVACY_MODES, normalizeSendTabTitle } from './title-privacy.js';
|
import { TITLE_PRIVACY_MODES, normalizeSendTabTitle, normalizeTabTitle } from './title-privacy.js';
|
||||||
|
|
||||||
|
|
||||||
const elements = {
|
const elements = {
|
||||||
@@ -71,7 +71,11 @@ const elements = {
|
|||||||
settingsVersion: document.getElementById('settingsVersion'),
|
settingsVersion: document.getElementById('settingsVersion'),
|
||||||
devSupportLink: document.getElementById('devSupportLink'),
|
devSupportLink: document.getElementById('devSupportLink'),
|
||||||
devReviewLink: document.getElementById('devReviewLink'),
|
devReviewLink: document.getElementById('devReviewLink'),
|
||||||
syncTabCopyInvite: document.getElementById('syncTabCopyInvite')
|
syncTabCopyInvite: document.getElementById('syncTabCopyInvite'),
|
||||||
|
devToolsTabBtn: document.getElementById('devToolsTabBtn'),
|
||||||
|
remoteSeekBack: document.getElementById('remoteSeekBack'),
|
||||||
|
remoteSeekForward: document.getElementById('remoteSeekForward'),
|
||||||
|
remoteSeekFiveMin: document.getElementById('remoteSeekFiveMin')
|
||||||
};
|
};
|
||||||
|
|
||||||
let localPeerId = null;
|
let localPeerId = null;
|
||||||
@@ -88,6 +92,19 @@ let errorToken = 0;
|
|||||||
let forceSyncDone = false;
|
let forceSyncDone = false;
|
||||||
let connectionErrorTimer = null;
|
let connectionErrorTimer = null;
|
||||||
let pendingConnectionErrorMsg = null;
|
let pendingConnectionErrorMsg = null;
|
||||||
|
|
||||||
|
function devToolsEnabled() {
|
||||||
|
return elements.username && elements.username.value.trim() === 'KoalaDev';
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncDevToolsVisibility() {
|
||||||
|
if (!elements.devToolsTabBtn) return;
|
||||||
|
const enabled = devToolsEnabled();
|
||||||
|
elements.devToolsTabBtn.style.display = enabled ? '' : 'none';
|
||||||
|
if (!enabled && document.getElementById('tab-devtools')?.classList.contains('active')) {
|
||||||
|
document.querySelector('.tab-btn[data-tab="tab-settings"]')?.click();
|
||||||
|
}
|
||||||
|
}
|
||||||
let roomListRefreshTimer = null;
|
let roomListRefreshTimer = null;
|
||||||
let roomListRefreshInterval = null;
|
let roomListRefreshInterval = null;
|
||||||
const ROOM_LIST_REFRESH_COOLDOWN_MS = 11000;
|
const ROOM_LIST_REFRESH_COOLDOWN_MS = 11000;
|
||||||
@@ -210,6 +227,7 @@ async function init() {
|
|||||||
elements.roomId.value = localData.roomId || '';
|
elements.roomId.value = localData.roomId || '';
|
||||||
elements.password.value = localData.password || '';
|
elements.password.value = localData.password || '';
|
||||||
elements.username.value = username;
|
elements.username.value = username;
|
||||||
|
syncDevToolsVisibility();
|
||||||
if (elements.filterNoise) elements.filterNoise.checked = localData.filterNoise !== false;
|
if (elements.filterNoise) elements.filterNoise.checked = localData.filterNoise !== false;
|
||||||
if (elements.autoSyncNextEpisode) elements.autoSyncNextEpisode.checked = localData.autoSyncNextEpisode !== false;
|
if (elements.autoSyncNextEpisode) elements.autoSyncNextEpisode.checked = localData.autoSyncNextEpisode !== false;
|
||||||
const legacyTitlePrivacyMode = Object.values(TITLE_PRIVACY_MODES).includes(localData.titlePrivacyMode) ? localData.titlePrivacyMode : TITLE_PRIVACY_MODES.FULL;
|
const legacyTitlePrivacyMode = Object.values(TITLE_PRIVACY_MODES).includes(localData.titlePrivacyMode) ? localData.titlePrivacyMode : TITLE_PRIVACY_MODES.FULL;
|
||||||
@@ -277,13 +295,13 @@ async function init() {
|
|||||||
const syncTabBtn = document.querySelector('.tab-btn[data-tab="tab-sync"]');
|
const syncTabBtn = document.querySelector('.tab-btn[data-tab="tab-sync"]');
|
||||||
if (syncTabBtn) syncTabBtn.click();
|
if (syncTabBtn) syncTabBtn.click();
|
||||||
showSelectVideoHint();
|
showSelectVideoHint();
|
||||||
} else if (localData.activeTab) {
|
} else if (localData.activeTab && (localData.activeTab !== 'tab-devtools' || devToolsEnabled())) {
|
||||||
const btn = document.querySelector(`.tab-btn[data-tab="${localData.activeTab}"]`);
|
const btn = document.querySelector(`.tab-btn[data-tab="${localData.activeTab}"]`);
|
||||||
if (btn) btn.click();
|
if (btn) btn.click();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
await populateTabs();
|
await populateTabs();
|
||||||
if (localData.activeTab) {
|
if (localData.activeTab && (localData.activeTab !== 'tab-devtools' || devToolsEnabled())) {
|
||||||
const btn = document.querySelector(`.tab-btn[data-tab="${localData.activeTab}"]`);
|
const btn = document.querySelector(`.tab-btn[data-tab="${localData.activeTab}"]`);
|
||||||
if (btn) btn.click();
|
if (btn) btn.click();
|
||||||
}
|
}
|
||||||
@@ -903,7 +921,7 @@ async function populateTabs(providedPeers = null, providedTargetTabId = null) {
|
|||||||
// Smart Matching Logic — exclude own tabTitle to prevent self-match (computed once)
|
// Smart Matching Logic — exclude own tabTitle to prevent self-match (computed once)
|
||||||
const cleanTitle = (rawTitle) => {
|
const cleanTitle = (rawTitle) => {
|
||||||
if (!rawTitle) return '';
|
if (!rawTitle) return '';
|
||||||
return rawTitle
|
return (normalizeTabTitle(rawTitle) || '')
|
||||||
.replace(/(?:\s*[-\|•]\s*(?:YouTube|Twitch|Jellyfin|Emby|Netflix|Vimeo|Dailymotion).*)$/i, '')
|
.replace(/(?:\s*[-\|•]\s*(?:YouTube|Twitch|Jellyfin|Emby|Netflix|Vimeo|Dailymotion).*)$/i, '')
|
||||||
.replace(/^(?:Netflix|Twitch|YouTube|Emby|Jellyfin)\s*[-\|•]\s*/i, '')
|
.replace(/^(?:Netflix|Twitch|YouTube|Emby|Jellyfin)\s*[-\|•]\s*/i, '')
|
||||||
.trim();
|
.trim();
|
||||||
@@ -1275,8 +1293,10 @@ elements.serverUrl.addEventListener('input', () => {
|
|||||||
chrome.storage.local.set({ serverUrl: elements.serverUrl.value });
|
chrome.storage.local.set({ serverUrl: elements.serverUrl.value });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
elements.username.addEventListener('input', syncDevToolsVisibility);
|
||||||
elements.username.addEventListener('change', () => {
|
elements.username.addEventListener('change', () => {
|
||||||
chrome.storage.local.set({ username: elements.username.value });
|
chrome.storage.local.set({ username: elements.username.value });
|
||||||
|
syncDevToolsVisibility();
|
||||||
});
|
});
|
||||||
|
|
||||||
if (elements.langSelector) {
|
if (elements.langSelector) {
|
||||||
@@ -1609,10 +1629,14 @@ elements.forceSyncBtn.addEventListener('click', async () => {
|
|||||||
if (mode === 'jump-to-me') {
|
if (mode === 'jump-to-me') {
|
||||||
chrome.tabs.sendMessage(tabId, { action: 'get_current_time' }, (response) => {
|
chrome.tabs.sendMessage(tabId, { action: 'get_current_time' }, (response) => {
|
||||||
if (chrome.runtime.lastError || !response || response.currentTime === undefined) {
|
if (chrome.runtime.lastError || !response || response.currentTime === undefined) {
|
||||||
chrome.scripting.executeScript({
|
chrome.runtime.sendMessage({ type: 'INJECT_CONTENT_SCRIPT', tabId }, (injectResponse) => {
|
||||||
target: { tabId },
|
if (chrome.runtime.lastError || !injectResponse || injectResponse.status !== 'ok') {
|
||||||
files: ['content.js']
|
showError(getMessage('ERR_NO_VIDEO_TAB'));
|
||||||
}).then(() => {
|
forceSyncDone = true;
|
||||||
|
elements.forceSyncBtn.disabled = false;
|
||||||
|
elements.forceSyncBtn.textContent = originalText;
|
||||||
|
return;
|
||||||
|
}
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
chrome.tabs.sendMessage(tabId, { action: 'get_current_time' }, (retryResponse) => {
|
chrome.tabs.sendMessage(tabId, { action: 'get_current_time' }, (retryResponse) => {
|
||||||
if (chrome.runtime.lastError) return;
|
if (chrome.runtime.lastError) return;
|
||||||
@@ -1621,11 +1645,6 @@ elements.forceSyncBtn.addEventListener('click', async () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}, 500);
|
}, 500);
|
||||||
}).catch(() => {
|
|
||||||
showError(getMessage('ERR_NO_VIDEO_TAB'));
|
|
||||||
forceSyncDone = true;
|
|
||||||
elements.forceSyncBtn.disabled = false;
|
|
||||||
elements.forceSyncBtn.textContent = originalText;
|
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1690,6 +1709,27 @@ elements.pauseBtn.addEventListener('click', () => {
|
|||||||
}, 2500);
|
}, 2500);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function simulateRemoteSeek({ delta = null, targetTime = null }) {
|
||||||
|
chrome.runtime.sendMessage({ type: 'DEV_SIMULATE_REMOTE_SEEK', delta, targetTime }, (res) => {
|
||||||
|
if (chrome.runtime.lastError || !res || res.status !== 'ok') {
|
||||||
|
showToast(res?.message || 'Remote seek failed', 'error');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
showToast(`Remote seek -> ${formatTime(res.targetTime)}`, 'success', 1200);
|
||||||
|
refreshDebugInfo();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (elements.remoteSeekBack) {
|
||||||
|
elements.remoteSeekBack.addEventListener('click', () => simulateRemoteSeek({ delta: -30 }));
|
||||||
|
}
|
||||||
|
if (elements.remoteSeekForward) {
|
||||||
|
elements.remoteSeekForward.addEventListener('click', () => simulateRemoteSeek({ delta: 30 }));
|
||||||
|
}
|
||||||
|
if (elements.remoteSeekFiveMin) {
|
||||||
|
elements.remoteSeekFiveMin.addEventListener('click', () => simulateRemoteSeek({ targetTime: 300 }));
|
||||||
|
}
|
||||||
|
|
||||||
elements.clearLogs.addEventListener('click', () => {
|
elements.clearLogs.addEventListener('click', () => {
|
||||||
chrome.runtime.sendMessage({ type: 'CLEAR_LOGS' }, () => {
|
chrome.runtime.sendMessage({ type: 'CLEAR_LOGS' }, () => {
|
||||||
elements.logList.innerHTML = '';
|
elements.logList.innerHTML = '';
|
||||||
@@ -2047,6 +2087,26 @@ elements.copyLogs.addEventListener('click', () => {
|
|||||||
lines.push(`- **ReadyState:** ${readyOk ? '\u2705' : '\u26A0\uFE0F'} ${safe(vs.readyState, '?')} (${readyLabel})`);
|
lines.push(`- **ReadyState:** ${readyOk ? '\u2705' : '\u26A0\uFE0F'} ${safe(vs.readyState, '?')} (${readyLabel})`);
|
||||||
lines.push(`- **Network:** ${safe(vs.networkState, '?')} (${netLabel})`);
|
lines.push(`- **Network:** ${safe(vs.networkState, '?')} (${netLabel})`);
|
||||||
lines.push(`- **Buffered:** ${safe(vs.buffered, '?')}`);
|
lines.push(`- **Buffered:** ${safe(vs.buffered, '?')}`);
|
||||||
|
if (vs.nativeCurrentTime != null || vs.nativeDuration != null) {
|
||||||
|
lines.push(`- **Native Time:** ${safe(vs.nativeCurrentTime, '?')}s / ${safe(vs.nativeDuration, '?')}s`);
|
||||||
|
}
|
||||||
|
if (vs.siteQuirk) {
|
||||||
|
const quirk = vs.siteQuirk;
|
||||||
|
const label = quirk.name || quirk.key || 'site';
|
||||||
|
if (quirk.timeline) {
|
||||||
|
lines.push(`- **${label} Timeline:** ${safe(quirk.timeline.current, '?')}s / ${safe(quirk.timeline.duration, '?')}s`);
|
||||||
|
}
|
||||||
|
const candidates = Array.isArray(quirk.timelineCandidates) ? quirk.timelineCandidates : [];
|
||||||
|
if (candidates.length > 0) {
|
||||||
|
lines.push(`- **${label} Timeline Candidates:**`);
|
||||||
|
candidates.forEach(c => lines.push(` - ${safe(c.source, '?')}: ${safe(c.current, '?')}s / ${safe(c.duration, '?')}s`));
|
||||||
|
}
|
||||||
|
const buttons = Array.isArray(quirk.seekButtons) ? quirk.seekButtons : [];
|
||||||
|
if (buttons.length > 0) {
|
||||||
|
lines.push(`- **${label} Buttons:**`);
|
||||||
|
buttons.forEach(label => lines.push(` - ${label}`));
|
||||||
|
}
|
||||||
|
}
|
||||||
lines.push(`- **Dimensions:** ${safe(vs.videoWidth, '?')}x${safe(vs.videoHeight, '?')}${dimOk ? '' : ' \u26A0\uFE0F 0x0'}`);
|
lines.push(`- **Dimensions:** ${safe(vs.videoWidth, '?')}x${safe(vs.videoHeight, '?')}${dimOk ? '' : ' \u26A0\uFE0F 0x0'}`);
|
||||||
lines.push(`- **Muted:** ${safe(vs.muted, '?')} | **Volume:** ${safe(vs.volume, '?')} | **Speed:** ${safe(vs.playbackRate, '?')}x`);
|
lines.push(`- **Muted:** ${safe(vs.muted, '?')} | **Volume:** ${safe(vs.volume, '?')} | **Speed:** ${safe(vs.playbackRate, '?')}x`);
|
||||||
lines.push(`- **Seeking:** ${safe(vs.seeking, '?')} | **Ended:** ${safe(vs.ended, '?')} | **Loop:** ${safe(vs.loop, '?')}`);
|
lines.push(`- **Seeking:** ${safe(vs.seeking, '?')} | **Ended:** ${safe(vs.ended, '?')} | **Loop:** ${safe(vs.loop, '?')}`);
|
||||||
@@ -2220,7 +2280,18 @@ function refreshDebugInfo() {
|
|||||||
addField('Network', `${state.networkState} (${state.networkStateLabel || '?'})`,
|
addField('Network', `${state.networkState} (${state.networkStateLabel || '?'})`,
|
||||||
state.networkState === 1 ? '#22c55e' : state.networkState === 3 ? '#ef4444' : 'var(--text-muted)');
|
state.networkState === 1 ? '#22c55e' : state.networkState === 3 ? '#ef4444' : 'var(--text-muted)');
|
||||||
addField('Buffered', state.buffered || '?');
|
addField('Buffered', state.buffered || '?');
|
||||||
|
if (state.nativeCurrentTime != null || state.nativeDuration != null) {
|
||||||
|
addField('Native Time', `${state.nativeCurrentTime ?? '?'}s / ${state.nativeDuration ?? '?'}s`);
|
||||||
|
}
|
||||||
|
if (state.siteQuirk) {
|
||||||
|
const quirk = state.siteQuirk;
|
||||||
|
const label = quirk.name || quirk.key || 'Site';
|
||||||
|
if (quirk.timeline) {
|
||||||
|
addField(`${label} Timeline`, `${quirk.timeline.current ?? '?'}s / ${quirk.timeline.duration ?? '?'}s`);
|
||||||
|
}
|
||||||
|
const buttons = Array.isArray(quirk.seekButtons) ? quirk.seekButtons.slice(0, 4).join(' | ') : '';
|
||||||
|
if (buttons) addField(`${label} Buttons`, buttons);
|
||||||
|
}
|
||||||
addSection('Properties');
|
addSection('Properties');
|
||||||
addField('Seeking', String(state.seeking));
|
addField('Seeking', String(state.seeking));
|
||||||
addField('Ended', String(state.ended));
|
addField('Ended', String(state.ended));
|
||||||
|
|||||||
@@ -17,9 +17,15 @@ export function normalizeSendTabTitle(sendTabTitle, legacyMode = TITLE_PRIVACY_M
|
|||||||
return normalizeTitlePrivacyMode(legacyMode) === TITLE_PRIVACY_MODES.FULL;
|
return normalizeTitlePrivacyMode(legacyMode) === TITLE_PRIVACY_MODES.FULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function normalizeTabTitle(title) {
|
||||||
|
if (typeof title !== 'string') return null;
|
||||||
|
const normalized = title.replace(/^\s*(?:\(\d{1,2}\)|\[\d{1,2}\])\s+/, '').trim();
|
||||||
|
return normalized.length > 0 ? normalized : null;
|
||||||
|
}
|
||||||
|
|
||||||
export function sanitizeTabTitle(title, sendTabTitle) {
|
export function sanitizeTabTitle(title, sendTabTitle) {
|
||||||
if (!sendTabTitle) return null;
|
if (!sendTabTitle) return null;
|
||||||
return typeof title === 'string' && title.length > 0 ? title : null;
|
return normalizeTabTitle(title);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function sanitizeSharedTitle(title, mode) {
|
export function sanitizeSharedTitle(title, mode) {
|
||||||
|
|||||||
@@ -140,6 +140,12 @@ function copyExtensionFiles(targetDir, browserName) {
|
|||||||
|
|
||||||
fs.writeFileSync(destPath, content);
|
fs.writeFileSync(destPath, content);
|
||||||
console.log(`✓ Injected uninstall URL constants for ${browserName} into background.js`);
|
console.log(`✓ Injected uninstall URL constants for ${browserName} into background.js`);
|
||||||
|
} else if (item === 'popup.html') {
|
||||||
|
let content = fs.readFileSync(srcPath, 'utf8');
|
||||||
|
const timestamp = new Date().toISOString().replace('T', ' ').substring(0, 19) + ' UTC';
|
||||||
|
content = content.replace(/__BUILD_TIMESTAMP__/g, timestamp);
|
||||||
|
fs.writeFileSync(destPath, content);
|
||||||
|
console.log(`✓ Injected build timestamp into popup.html: ${timestamp}`);
|
||||||
} else {
|
} else {
|
||||||
fs.copyFileSync(srcPath, destPath);
|
fs.copyFileSync(srcPath, destPath);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,14 @@ function extractFunction(name, text) {
|
|||||||
throw new Error(`${name} body did not terminate`);
|
throw new Error(`${name} body did not terminate`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function makeSeekable(ranges = []) {
|
||||||
|
return {
|
||||||
|
length: ranges.length,
|
||||||
|
start(i) { return ranges[i][0]; },
|
||||||
|
end(i) { return ranges[i][1]; }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function makeVideo(name, width, height, options = {}) {
|
function makeVideo(name, width, height, options = {}) {
|
||||||
return {
|
return {
|
||||||
name,
|
name,
|
||||||
@@ -28,7 +36,9 @@ function makeVideo(name, width, height, options = {}) {
|
|||||||
offsetWidth: width,
|
offsetWidth: width,
|
||||||
offsetHeight: height,
|
offsetHeight: height,
|
||||||
muted: options.muted ?? true,
|
muted: options.muted ?? true,
|
||||||
duration: options.duration ?? 0
|
duration: options.duration ?? 0,
|
||||||
|
currentTime: options.currentTime ?? 0,
|
||||||
|
seekable: options.seekable ?? makeSeekable()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,4 +71,79 @@ assert.strictEqual(
|
|||||||
'findVideo should score Shadow DOM videos together with light DOM videos'
|
'findVideo should score Shadow DOM videos together with light DOM videos'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
function makeDocument(nodes = []) {
|
||||||
|
return {
|
||||||
|
querySelectorAll() { return nodes; }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadTimelineFns(hostname, document = makeDocument(), pageApiTime = null) {
|
||||||
|
const disneyPageApiTime = pageApiTime
|
||||||
|
? `let disneyPageApiTime = { position: ${pageApiTime.position}, duration: ${pageApiTime.duration}, at: Date.now() - ${pageApiTime.ageMs || 0} };`
|
||||||
|
: 'let disneyPageApiTime = null;';
|
||||||
|
return Function('window', 'document', [
|
||||||
|
disneyPageApiTime,
|
||||||
|
extractFunction('hostMatchesUrl', source),
|
||||||
|
extractFunction('matchesPlayerUrls', source),
|
||||||
|
extractFunction('isDisneyPlusHost', source),
|
||||||
|
extractFunction('getDisneyPlusTimeline', source),
|
||||||
|
extractFunction('getSiteQuirkAdapters', source),
|
||||||
|
extractFunction('getActiveSiteQuirk', source),
|
||||||
|
extractFunction('getSiteQuirkTimeline', source),
|
||||||
|
extractFunction('getSiteQuirkDebug', source),
|
||||||
|
extractFunction('getSyncCurrentTime', source),
|
||||||
|
extractFunction('getSyncDuration', source),
|
||||||
|
extractFunction('toNativeSeekTime', source),
|
||||||
|
'return { getActiveSiteQuirk, getSyncCurrentTime, getSyncDuration, toNativeSeekTime };'
|
||||||
|
].join('\n'))({ location: { hostname } }, document);
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadPlayerFixFns(hostname) {
|
||||||
|
return Function('window', [
|
||||||
|
extractFunction('hostMatchesUrl', source),
|
||||||
|
extractFunction('matchesPlayerUrls', source),
|
||||||
|
extractFunction('getPlayerActionFixes', source),
|
||||||
|
extractFunction('getActivePlayerActionFix', source),
|
||||||
|
'return { getPlayerActionFixes, getActivePlayerActionFix };'
|
||||||
|
].join('\n'))({ location: { hostname } });
|
||||||
|
}
|
||||||
|
|
||||||
|
const disneyFns = loadTimelineFns('www.disneyplus.com', makeDocument(), {
|
||||||
|
position: 9,
|
||||||
|
duration: 10800
|
||||||
|
});
|
||||||
|
assert.equal(disneyFns.getActiveSiteQuirk().name, 'disneyplus-page-api');
|
||||||
|
assert.deepEqual(disneyFns.getActiveSiteQuirk().urls, ['disneyplus.com']);
|
||||||
|
const disneyVideo = makeVideo('disney-offset', 1920, 1080, {
|
||||||
|
currentTime: 29,
|
||||||
|
duration: 0,
|
||||||
|
seekable: makeSeekable([[0, 32400]])
|
||||||
|
});
|
||||||
|
assert.equal(disneyFns.getSyncCurrentTime(disneyVideo), 9);
|
||||||
|
assert.equal(disneyFns.getSyncDuration(disneyVideo), 10800);
|
||||||
|
assert.equal(disneyFns.toNativeSeekTime(disneyVideo, 39), 39);
|
||||||
|
|
||||||
|
const disneyNoPageApiFns = loadTimelineFns('www.disneyplus.com');
|
||||||
|
const disneyOffsetVideo = makeVideo('disney-offset', 1920, 1080, {
|
||||||
|
currentTime: 29,
|
||||||
|
duration: 0,
|
||||||
|
seekable: makeSeekable([[20, 10820]])
|
||||||
|
});
|
||||||
|
assert.equal(disneyNoPageApiFns.getSyncCurrentTime(disneyOffsetVideo), 29);
|
||||||
|
assert.equal(disneyNoPageApiFns.getSyncDuration(disneyOffsetVideo), 0);
|
||||||
|
assert.equal(disneyNoPageApiFns.toNativeSeekTime(disneyOffsetVideo, 39), 39);
|
||||||
|
|
||||||
|
const genericFns = loadTimelineFns('example.com');
|
||||||
|
assert.equal(genericFns.getActiveSiteQuirk(), null);
|
||||||
|
assert.equal(genericFns.getSyncCurrentTime(disneyVideo), 29);
|
||||||
|
assert.equal(genericFns.getSyncDuration(disneyVideo), 0);
|
||||||
|
assert.equal(genericFns.toNativeSeekTime(disneyVideo, 39), 39);
|
||||||
|
|
||||||
|
const twitchFixFns = loadPlayerFixFns('player.twitch.tv');
|
||||||
|
assert.equal(twitchFixFns.getActivePlayerActionFix().name, 'twitch-player-buttons');
|
||||||
|
assert.deepEqual(twitchFixFns.getActivePlayerActionFix().urls, ['twitch.tv']);
|
||||||
|
|
||||||
|
const genericFixFns = loadPlayerFixFns('example.com');
|
||||||
|
assert.equal(genericFixFns.getActivePlayerActionFix(), null);
|
||||||
|
|
||||||
console.log('content video finder tests passed');
|
console.log('content video finder tests passed');
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import {
|
|||||||
TITLE_PRIVACY_MODES,
|
TITLE_PRIVACY_MODES,
|
||||||
applyTitlePrivacyToPayload,
|
applyTitlePrivacyToPayload,
|
||||||
normalizeSendTabTitle,
|
normalizeSendTabTitle,
|
||||||
|
normalizeTabTitle,
|
||||||
normalizeTitlePrivacyMode,
|
normalizeTitlePrivacyMode,
|
||||||
sanitizeSharedTitle,
|
sanitizeSharedTitle,
|
||||||
sanitizeTabTitle
|
sanitizeTabTitle
|
||||||
@@ -15,8 +16,13 @@ assert.equal(normalizeSendTabTitle(undefined, TITLE_PRIVACY_MODES.FULL), true);
|
|||||||
assert.equal(normalizeSendTabTitle(undefined, TITLE_PRIVACY_MODES.EPISODE), false);
|
assert.equal(normalizeSendTabTitle(undefined, TITLE_PRIVACY_MODES.EPISODE), false);
|
||||||
assert.equal(normalizeSendTabTitle(true, TITLE_PRIVACY_MODES.HIDDEN), true);
|
assert.equal(normalizeSendTabTitle(true, TITLE_PRIVACY_MODES.HIDDEN), true);
|
||||||
assert.equal(normalizeSendTabTitle(false, TITLE_PRIVACY_MODES.FULL), false);
|
assert.equal(normalizeSendTabTitle(false, TITLE_PRIVACY_MODES.FULL), false);
|
||||||
|
assert.equal(normalizeTabTitle('(12) Testvideo - YouTube'), 'Testvideo - YouTube');
|
||||||
|
assert.equal(normalizeTabTitle('[7] Testvideo - YouTube'), 'Testvideo - YouTube');
|
||||||
|
assert.equal(normalizeTabTitle('(500) Days of Summer'), '(500) Days of Summer');
|
||||||
|
assert.equal(normalizeTabTitle(' '), null);
|
||||||
|
|
||||||
assert.equal(sanitizeTabTitle('Private Tab', true), 'Private Tab');
|
assert.equal(sanitizeTabTitle('Private Tab', true), 'Private Tab');
|
||||||
|
assert.equal(sanitizeTabTitle('(12) Private Tab', true), 'Private Tab');
|
||||||
assert.equal(sanitizeTabTitle('Private Tab', false), null);
|
assert.equal(sanitizeTabTitle('Private Tab', false), null);
|
||||||
assert.equal(sanitizeTabTitle('', true), null);
|
assert.equal(sanitizeTabTitle('', true), null);
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
Reference in New Issue
Block a user