From 5a913521fa28d0e99b33926acf4bf38cb3f1c883 Mon Sep 17 00:00:00 2001 From: winoffrg Date: Mon, 23 Mar 2026 16:46:57 +0530 Subject: [PATCH 1/2] fix: prime video patches, log setting and race condition --- .changeset/calm-deserts-deny.md | 5 + src/components/App.tsx | 33 +-- src/components/OTTModal.tsx | 68 ++++-- src/entrypoints/background.ts | 55 ++++- src/entrypoints/content/index.tsx | 16 +- src/entrypoints/primevideo-bootstrap.ts | 217 ++++++++++++++++++ .../primevideo-interceptor.content.ts | 13 ++ src/entrypoints/script.ts | 12 +- src/hooks/useStore.tsx | 61 ++++- src/lib/apps/primevideo/config.ts | 5 +- .../apps/primevideo/script-patch-shared.ts | 27 +++ src/lib/apps/primevideo/script-watcher.ts | 75 +----- src/lib/logger.ts | 41 +++- src/lib/messaging.ts | 8 + src/lib/posthog-enabled.ts | 171 ++++++++++++++ src/lib/posthog-impl.ts | 1 + src/lib/posthog-provider-disabled.tsx | 9 + src/lib/posthog-provider-enabled.tsx | 23 ++ src/lib/posthog-provider-impl.tsx | 1 + src/lib/posthog-provider.tsx | 1 + src/lib/posthog.ts | 165 +------------ src/lib/storage.ts | 15 +- wxt.config.ts | 28 +++ 23 files changed, 749 insertions(+), 301 deletions(-) create mode 100644 .changeset/calm-deserts-deny.md create mode 100644 src/entrypoints/primevideo-bootstrap.ts create mode 100644 src/entrypoints/primevideo-interceptor.content.ts create mode 100644 src/lib/apps/primevideo/script-patch-shared.ts create mode 100644 src/lib/posthog-enabled.ts create mode 100644 src/lib/posthog-impl.ts create mode 100644 src/lib/posthog-provider-disabled.tsx create mode 100644 src/lib/posthog-provider-enabled.tsx create mode 100644 src/lib/posthog-provider-impl.tsx create mode 100644 src/lib/posthog-provider.tsx diff --git a/.changeset/calm-deserts-deny.md b/.changeset/calm-deserts-deny.md new file mode 100644 index 0000000..179d55a --- /dev/null +++ b/.changeset/calm-deserts-deny.md @@ -0,0 +1,5 @@ +--- +"OTTPRO": minor +--- + +fix: prime video blocking regex and race condition diff --git a/src/components/App.tsx b/src/components/App.tsx index 2d49e2b..a1a2d9a 100644 --- a/src/components/App.tsx +++ b/src/components/App.tsx @@ -1,6 +1,10 @@ import { useEffect } from "react"; import type { AppConfig } from "@/lib/shared/types"; -import { setCurrentAppContext } from "@/lib/posthog"; +import { DEFAULT_LOG_LEVEL } from "@/lib/logger"; +import { + PRODUCT_INSIGHTS_AVAILABLE, + setCurrentAppContext, +} from "@/lib/posthog"; import { RootStoreProvider, useRootStore } from "../hooks/useStore"; import { OTTModal } from "./OTTModal"; @@ -29,19 +33,20 @@ function AppContent() { } function App({ root, app }: { root: HTMLElement; app: AppConfig | undefined }) { - return ( - - - - ); + return ( + + + + ); } export default App; diff --git a/src/components/OTTModal.tsx b/src/components/OTTModal.tsx index 7fb2f8d..6faf4d1 100644 --- a/src/components/OTTModal.tsx +++ b/src/components/OTTModal.tsx @@ -1,8 +1,12 @@ -import { useEffect, useState } from "react"; +import { type ChangeEvent, useEffect, useState } from "react"; +import { LOG_LEVEL_OPTIONS, type LogLevel } from "@/lib/logger"; +import { PRODUCT_INSIGHTS_AVAILABLE } from "@/lib/posthog"; import { useAppEnabled, + useLogLevel, useProductInsightsEnabled, useRootStore, + useSetLogLevel, useToggleApp, useToggleProductInsights, } from "../hooks/useStore"; @@ -30,6 +34,8 @@ export function OTTModal() { const currentApp = useRootStore((state) => state.currentApp); const toggleApp = useToggleApp(); const productInsightsEnabled = useProductInsightsEnabled(); + const logLevel = useLogLevel(); + const setLogLevel = useSetLogLevel(); const toggleProductInsights = useToggleProductInsights(); const isAppEnabled = useAppEnabled(currentApp?.id || ""); const [hotstarView, setHotstarView] = useState<"main" | "login-transfer">( @@ -66,6 +72,12 @@ export function OTTModal() { await toggleProductInsights(); }; + const handleLogLevelChange = async ( + event: ChangeEvent, + ) => { + await setLogLevel(Number(event.target.value) as LogLevel); + }; + const handleOpenLoginTransfer = () => { setHotstarView("login-transfer"); setTransferMessage(""); @@ -289,7 +301,7 @@ export function OTTModal() { onClick={handleImportSession} type="button" > - {isTransferBusy ? "Importing..." : "Import Session"} + {isTransferBusy ? "Importing..." : "Import Session"} )} {loginTransferMode === "import" && transferMessage && ( @@ -361,22 +373,50 @@ export function OTTModal() { )} - -
+ {PRODUCT_INSIGHTS_AVAILABLE && ( + +
+
+

+ Share diagnostics +

+

+ Sends usage logs to improve product quality +

+
+ +
+
+ )} + + +

- Share diagnostics -

-

- Sends usage logs to improve product quality + Log level

- +
diff --git a/src/entrypoints/background.ts b/src/entrypoints/background.ts index 1413261..a206fad 100644 --- a/src/entrypoints/background.ts +++ b/src/entrypoints/background.ts @@ -1,8 +1,12 @@ import { onMessage, StorageMessageType } from "@/lib/messaging"; import { AppStorageManager } from "@/lib/storage"; import { syncAllDynamicRules, syncDynamicRule } from "@/lib/dnr-rules"; -import { logger } from "@/lib/logger"; -import { initPostHog, setProductInsightsEnabled } from "@/lib/posthog"; +import { logger, setGlobalLogLevel } from "@/lib/logger"; +import { + initPostHog, + PRODUCT_INSIGHTS_AVAILABLE, + setProductInsightsEnabled, +} from "@/lib/posthog"; import { isSessionOnlyRule } from "@/lib/session-rules"; import { exportCookies, importCookies } from "@/lib/cookie-transfer-background"; @@ -10,13 +14,17 @@ import { WELCOME_URL } from "@/lib/shared/constants"; export default defineBackground(() => { const storageManager = new AppStorageManager(); - storageManager - .getProductInsightsEnabled() - .then((enabled) => { - if (enabled) { + Promise.all([ + storageManager.getProductInsightsEnabled(), + storageManager.getLogLevel(), + ]) + .then(([enabled, logLevel]) => { + setGlobalLogLevel(logLevel); + const insightsEnabled = PRODUCT_INSIGHTS_AVAILABLE && enabled; + if (insightsEnabled) { initPostHog(); } - setProductInsightsEnabled(enabled); + setProductInsightsEnabled(insightsEnabled); }) .catch((error) => { logger.error("Failed to load product insights setting", { error }); @@ -57,9 +65,17 @@ export default defineBackground(() => { }); onMessage(StorageMessageType.GET_PRODUCT_INSIGHTS_ENABLED, async () => { + if (!PRODUCT_INSIGHTS_AVAILABLE) { + return false; + } + return await storageManager.getProductInsightsEnabled(); }); + onMessage(StorageMessageType.GET_LOG_LEVEL, async () => { + return await storageManager.getLogLevel(); + }); + onMessage(StorageMessageType.SET_RULE_ENABLED, async (message) => { const { appId, ruleId, enabled } = message.data; @@ -93,7 +109,7 @@ export default defineBackground(() => { onMessage( StorageMessageType.SET_PRODUCT_INSIGHTS_ENABLED, async (message) => { - const { enabled } = message.data; + const enabled = PRODUCT_INSIGHTS_AVAILABLE && message.data.enabled; await storageManager.setProductInsightsEnabled(enabled); if (enabled) { initPostHog(); @@ -119,6 +135,29 @@ export default defineBackground(() => { }, ); + onMessage(StorageMessageType.SET_LOG_LEVEL, async (message) => { + const { level } = message.data; + await storageManager.setLogLevel(level); + setGlobalLogLevel(level); + + const tabs = await browser.tabs.query({}); + for (const tab of tabs) { + if (tab.id) { + browser.tabs + .sendMessage(tab.id, { + type: StorageMessageType.STORAGE_CHANGED, + data: { logLevel: level }, + }) + .catch((error) => { + logger.error("Failed to send log level message to tab", { + tabId: tab.id, + error, + }); + }); + } + } + }); + onMessage(StorageMessageType.GET_APP_CONFIG, async (message) => { const config = await storageManager.getAppConfig(message.data); if (!config) { diff --git a/src/entrypoints/content/index.tsx b/src/entrypoints/content/index.tsx index 88b7a59..f4d67a3 100644 --- a/src/entrypoints/content/index.tsx +++ b/src/entrypoints/content/index.tsx @@ -1,13 +1,12 @@ import ReactDOM from "react-dom/client"; import type { ContentScriptContext } from "#imports"; import App from "@/components/App"; -import { PostHogProvider } from "posthog-js/react"; import { initPostHog, - POSTHOG_API_KEY, - posthogOptions, + PRODUCT_INSIGHTS_AVAILABLE, setProductInsightsEnabled, } from "@/lib/posthog"; +import { ProductInsightsProvider } from "@/lib/posthog-provider"; import "@/assets/global.css"; import { StorageMessageType, sendMessage } from "@/lib/messaging"; @@ -103,8 +102,9 @@ async function createUi(ctx: ContentScriptContext) { sendMessage(StorageMessageType.GET_PRODUCT_INSIGHTS_ENABLED), ]); const configsWithSupport = withRuleSupport(allAppConfigs); - setProductInsightsEnabled(productInsightsEnabled); - if (productInsightsEnabled) { + const insightsEnabled = PRODUCT_INSIGHTS_AVAILABLE && productInsightsEnabled; + setProductInsightsEnabled(insightsEnabled); + if (insightsEnabled) { initPostHog(); } const app = configsWithSupport.find((config) => @@ -126,11 +126,11 @@ async function createUi(ctx: ContentScriptContext) { onMount: (uiContainer, _, shadowHost) => { if (!root) { root = ReactDOM.createRoot(uiContainer); - if (productInsightsEnabled) { + if (insightsEnabled) { root.render( - + - , + , ); } else { root.render(); diff --git a/src/entrypoints/primevideo-bootstrap.ts b/src/entrypoints/primevideo-bootstrap.ts new file mode 100644 index 0000000..f9da985 --- /dev/null +++ b/src/entrypoints/primevideo-bootstrap.ts @@ -0,0 +1,217 @@ +import { + isPrimeVideoTargetScriptUrl, + patchPrimeVideoScriptContent, +} from "@/lib/apps/primevideo/script-patch-shared"; + +const PRIMEVIDEO_PAGE_BOOTSTRAP_FLAG = + "__OTT_PRO_PRIMEVIDEO_PAGE_BOOTSTRAP__"; +const PENDING_URL_KEY = "__ottProPrimevideoPendingSrc"; +type PrimeVideoScriptElement = HTMLScriptElement & { + __ottProPrimevideoPendingSrc?: string; +}; + +export default defineUnlistedScript(() => { + if (!window.location.hostname.endsWith("primevideo.com")) { + return; + } + + const pageWindow = window as Window & { + [PRIMEVIDEO_PAGE_BOOTSTRAP_FLAG]?: boolean; + }; + + if (pageWindow[PRIMEVIDEO_PAGE_BOOTSTRAP_FLAG]) { + return; + } + pageWindow[PRIMEVIDEO_PAGE_BOOTSTRAP_FLAG] = true; + + const processedScripts = new WeakSet(); + const scriptCache = new Map>(); + + const setStatus = (status: string, url?: string) => { + if (!document.documentElement) { + return; + } + + if (url) { + document.documentElement.dataset.ottProPrimevideoInterceptor = url; + } + document.documentElement.dataset.ottProPrimevideoInterceptorStatus = status; + }; + + const fetchPatchedScript = (url: string) => { + let pendingScript = scriptCache.get(url); + if (!pendingScript) { + pendingScript = fetch(url) + .then((response) => response.text()) + .then((content) => patchPrimeVideoScriptContent(content).content); + scriptCache.set(url, pendingScript); + } + + return pendingScript; + }; + + const replaceScript = ( + node: Node | null | undefined, + forcedUrl?: string, + ): void => { + if (!(node instanceof HTMLScriptElement)) { + return; + } + + const scriptNode = node as PrimeVideoScriptElement; + const scriptUrl = + forcedUrl ?? + scriptNode[PENDING_URL_KEY] ?? + scriptNode.getAttribute("src") ?? + scriptNode.src; + if ( + !scriptUrl || + !isPrimeVideoTargetScriptUrl(scriptUrl) || + processedScripts.has(scriptNode) + ) { + return; + } + + processedScripts.add(scriptNode); + delete scriptNode[PENDING_URL_KEY]; + + const originalOnload = scriptNode.onload; + const originalOnerror = scriptNode.onerror; + setStatus("intercepting", scriptUrl); + + scriptNode.removeAttribute("src"); + scriptNode.removeAttribute("defer"); + scriptNode.removeAttribute("async"); + + fetchPatchedScript(scriptUrl) + .then((patchedContent) => { + const replacement = document.createElement("script"); + for (const { name, value } of Array.from(scriptNode.attributes)) { + if (name === "src" || name === "async" || name === "defer") { + continue; + } + replacement.setAttribute(name, value); + } + + replacement.textContent = `${patchedContent}\n//# sourceURL=${scriptUrl}`; + replacement.dataset.ottProPrimevideoIntercepted = "page"; + scriptNode.replaceWith(replacement); + setStatus("patched", scriptUrl); + + if (originalOnload) { + originalOnload.call(replacement, new Event("load")); + } + }) + .catch((error) => { + setStatus("failed", scriptUrl); + if (originalOnerror) { + originalOnerror.call(scriptNode, new Event("error")); + } + console.error("OTT Pro Prime Video bootstrap failed", error); + }); + }; + + const inspectNode = (node: Node) => { + if (node instanceof HTMLScriptElement) { + replaceScript(node); + return; + } + + if (node instanceof Element) { + node + .querySelectorAll("script[src]") + .forEach((scriptNode) => { + replaceScript(scriptNode); + }); + } + }; + + const wrapMethod = ( + proto: typeof Node.prototype, + methodName: "appendChild" | "insertBefore" | "replaceChild", + ) => { + const original = proto[methodName] as (...args: unknown[]) => unknown; + if (typeof original !== "function") { + return; + } + + Object.defineProperty(proto, methodName, { + configurable: true, + value: function (...args: unknown[]) { + const node = args[0]; + if (node instanceof Node) { + inspectNode(node); + } + return original.apply(this, args as never[]); + }, + }); + }; + + wrapMethod(Node.prototype, "appendChild"); + wrapMethod(Node.prototype, "insertBefore"); + wrapMethod(Node.prototype, "replaceChild"); + + const originalSetAttribute = HTMLScriptElement.prototype.setAttribute; + HTMLScriptElement.prototype.setAttribute = function (name, value) { + if (name === "src") { + const nextUrl = String(value); + if (isPrimeVideoTargetScriptUrl(nextUrl)) { + (this as PrimeVideoScriptElement)[PENDING_URL_KEY] = nextUrl; + queueMicrotask(() => { + replaceScript(this, nextUrl); + }); + return; + } + } + + return originalSetAttribute.call(this, name, value); + }; + + const srcDescriptor = Object.getOwnPropertyDescriptor( + HTMLScriptElement.prototype, + "src", + ); + if (srcDescriptor?.configurable && srcDescriptor.get && srcDescriptor.set) { + const originalGet = srcDescriptor.get; + const originalSet = srcDescriptor.set; + + Object.defineProperty(HTMLScriptElement.prototype, "src", { + configurable: true, + enumerable: srcDescriptor.enumerable ?? true, + get(this: HTMLScriptElement) { + return originalGet.call(this); + }, + set(this: HTMLScriptElement, value) { + const nextUrl = String(value); + if (isPrimeVideoTargetScriptUrl(nextUrl)) { + (this as PrimeVideoScriptElement)[PENDING_URL_KEY] = nextUrl; + queueMicrotask(() => { + replaceScript(this, nextUrl); + }); + return; + } + + return originalSet.call(this, value); + }, + }); + } + + const observer = new MutationObserver((mutations) => { + for (const mutation of mutations) { + for (const node of mutation.addedNodes) { + inspectNode(node); + } + } + }); + + observer.observe(document.documentElement ?? document, { + childList: true, + subtree: true, + }); + + document + .querySelectorAll("script[src]") + .forEach((scriptNode) => { + replaceScript(scriptNode); + }); +}); diff --git a/src/entrypoints/primevideo-interceptor.content.ts b/src/entrypoints/primevideo-interceptor.content.ts new file mode 100644 index 0000000..ff40c76 --- /dev/null +++ b/src/entrypoints/primevideo-interceptor.content.ts @@ -0,0 +1,13 @@ +export default defineContentScript({ + matches: ["*://*.primevideo.com/*"], + runAt: "document_start", + main() { + const isDev = import.meta.env.MODE === "development"; + const bootstrapPath = + "/primevideo-bootstrap.js" as Parameters[0]; + + void injectScript(bootstrapPath, { + keepInDom: isDev, + }); + }, +}); diff --git a/src/entrypoints/script.ts b/src/entrypoints/script.ts index 72c5a39..d556ef0 100644 --- a/src/entrypoints/script.ts +++ b/src/entrypoints/script.ts @@ -29,7 +29,9 @@ export default defineUnlistedScript(() => { return; } - const rule = staticConfig.rules.find((configRule) => configRule.id === ruleId); + const rule = staticConfig.rules.find( + (configRule) => configRule.id === ruleId, + ); if (!rule?.sessionOnly || !rule.onInit) { return; } @@ -58,15 +60,15 @@ export default defineUnlistedScript(() => { } } + if (middlewares.length > 0) { + fetchApiPolyfill(middlewares); + } + for (const rule of staticConfig.rules) { if (enabledRuleIds.includes(rule.id) && rule.onInit) { rule.onInit(); } } - - if (middlewares.length > 0) { - fetchApiPolyfill(middlewares); - } } catch (error) { console.error("Script: Error during execution:", error); } diff --git a/src/hooks/useStore.tsx b/src/hooks/useStore.tsx index 093ed6f..e0daea9 100644 --- a/src/hooks/useStore.tsx +++ b/src/hooks/useStore.tsx @@ -8,8 +8,16 @@ import { import { useStore } from "zustand"; import { createStore } from "zustand/vanilla"; import { StorageMessageType, sendMessage } from "@/lib/messaging"; -import { logger } from "@/lib/logger"; -import { setProductInsightsEnabled } from "@/lib/posthog"; +import { + DEFAULT_LOG_LEVEL, + logger, + setGlobalLogLevel, + type LogLevel, +} from "@/lib/logger"; +import { + PRODUCT_INSIGHTS_AVAILABLE, + setProductInsightsEnabled, +} from "@/lib/posthog"; import { getSessionOnlyRuleDefault, isSessionOnlyRule, @@ -24,13 +32,16 @@ export interface RootState { appStates: Map; ruleStates: Map; productInsightsEnabled: boolean; + logLevel: LogLevel; } export interface RootActions { toggleApp: (appId: string) => Promise; toggleRule: (appId: string, ruleId: string) => Promise; toggleProductInsights: () => Promise; + setLogLevel: (level: LogLevel) => Promise; updateProductInsightsFromStorage: (enabled: boolean) => void; + updateLogLevelFromStorage: (level: LogLevel) => void; updateFromStorage: ( appId: string, appEnabled: boolean, @@ -46,7 +57,8 @@ export const defaultInitState: RootState = { currentApp: undefined, appStates: new Map(), ruleStates: new Map(), - productInsightsEnabled: true, + productInsightsEnabled: PRODUCT_INSIGHTS_AVAILABLE, + logLevel: DEFAULT_LOG_LEVEL, }; export const createRootStore = (initState: RootState = defaultInitState) => { @@ -103,6 +115,11 @@ export const createRootStore = (initState: RootState = defaultInitState) => { }, toggleProductInsights: async () => { + if (!PRODUCT_INSIGHTS_AVAILABLE) { + set({ productInsightsEnabled: false }); + return; + } + const { productInsightsEnabled } = get(); const enabled = !productInsightsEnabled; @@ -114,9 +131,21 @@ export const createRootStore = (initState: RootState = defaultInitState) => { set({ productInsightsEnabled: enabled }); }, + setLogLevel: async (level: LogLevel) => { + await sendMessage(StorageMessageType.SET_LOG_LEVEL, { level }); + setGlobalLogLevel(level); + set({ logLevel: level }); + }, + updateProductInsightsFromStorage: (enabled: boolean) => { - setProductInsightsEnabled(enabled); - set({ productInsightsEnabled: enabled }); + const nextEnabled = PRODUCT_INSIGHTS_AVAILABLE && enabled; + setProductInsightsEnabled(nextEnabled); + set({ productInsightsEnabled: nextEnabled }); + }, + + updateLogLevelFromStorage: (level: LogLevel) => { + setGlobalLogLevel(level); + set({ logLevel: level }); }, updateFromStorage: ( @@ -143,9 +172,10 @@ export const createRootStore = (initState: RootState = defaultInitState) => { initializeFromStorage: async () => { try { - const [appConfigs, productInsightsEnabled] = await Promise.all([ + const [appConfigs, productInsightsEnabled, logLevel] = await Promise.all([ sendMessage(StorageMessageType.GET_ALL_APP_CONFIGS), sendMessage(StorageMessageType.GET_PRODUCT_INSIGHTS_ENABLED), + sendMessage(StorageMessageType.GET_LOG_LEVEL), ]); const newAppStates = new Map(); @@ -161,11 +191,15 @@ export const createRootStore = (initState: RootState = defaultInitState) => { } } - setProductInsightsEnabled(productInsightsEnabled); + const insightsEnabled = + PRODUCT_INSIGHTS_AVAILABLE && productInsightsEnabled; + setProductInsightsEnabled(insightsEnabled); + setGlobalLogLevel(logLevel); set({ appStates: newAppStates, ruleStates: newRuleStates, - productInsightsEnabled, + productInsightsEnabled: insightsEnabled, + logLevel, }); } catch (error) { logger.error("Failed to initialize from storage:", { error }); @@ -245,6 +279,9 @@ export const RootStoreProvider = ({ } else if ("productInsightsEnabled" in message.data) { const enabled = message.data.productInsightsEnabled as boolean; store.getState().updateProductInsightsFromStorage(enabled); + } else if ("logLevel" in message.data) { + const logLevel = message.data.logLevel as LogLevel; + store.getState().updateLogLevelFromStorage(logLevel); } } }; @@ -356,3 +393,11 @@ export const useProductInsightsEnabled = () => { export const useToggleProductInsights = () => { return useRootStore((state) => state.toggleProductInsights); }; + +export const useLogLevel = () => { + return useRootStore((state) => state.logLevel); +}; + +export const useSetLogLevel = () => { + return useRootStore((state) => state.setLogLevel); +}; diff --git a/src/lib/apps/primevideo/config.ts b/src/lib/apps/primevideo/config.ts index f198496..31e62b2 100644 --- a/src/lib/apps/primevideo/config.ts +++ b/src/lib/apps/primevideo/config.ts @@ -1,7 +1,7 @@ import type { AppConfig } from "@/lib/shared/types"; import { blockAds } from "./block-ads"; import { blockTelemetry } from "./block-telemetry"; -import { patchScripts, startScriptTagInterceptor } from "./script-watcher"; +import { patchScripts } from "./script-watcher"; /** * Prime Video App Configuration @@ -20,9 +20,6 @@ export const config: AppConfig = { description: "With a Lite plan you can watch 1080p FHD content on desktop as well.", middleware: patchScripts, - onInit: () => { - startScriptTagInterceptor(); - }, }, { id: "block-ads", diff --git a/src/lib/apps/primevideo/script-patch-shared.ts b/src/lib/apps/primevideo/script-patch-shared.ts new file mode 100644 index 0000000..cb09778 --- /dev/null +++ b/src/lib/apps/primevideo/script-patch-shared.ts @@ -0,0 +1,27 @@ +export const PRIMEVIDEO_SCRIPT_TARGET_URL_PATTERN = + /^https:\/\/m\.media-amazon\.com\/images\/.*\.js/i; + +export const PRIMEVIDEO_SCRIPT_FIND_PATTERN = + "e=>(null==e?void 0:e.isBonus)||(null==e?void 0:e.sequenceNumber)&&e.sequenceNumber>=1&&e.sequenceNumber<=3"; +export const PRIMEVIDEO_SCRIPT_REPLACE_WITH = "e=>true"; + +export function isPrimeVideoTargetScriptUrl(url: string) { + return PRIMEVIDEO_SCRIPT_TARGET_URL_PATTERN.test(url); +} + +export function patchPrimeVideoScriptContent(content: string) { + if (!content.includes(PRIMEVIDEO_SCRIPT_FIND_PATTERN)) { + return { + changed: false, + content, + }; + } + + return { + changed: true, + content: content.replace( + PRIMEVIDEO_SCRIPT_FIND_PATTERN, + PRIMEVIDEO_SCRIPT_REPLACE_WITH, + ), + }; +} diff --git a/src/lib/apps/primevideo/script-watcher.ts b/src/lib/apps/primevideo/script-watcher.ts index 9e23891..916dc55 100644 --- a/src/lib/apps/primevideo/script-watcher.ts +++ b/src/lib/apps/primevideo/script-watcher.ts @@ -1,28 +1,24 @@ import type { Middleware } from "@/lib/shared/middleware"; import { logger } from "@/lib/logger"; +import { + isPrimeVideoTargetScriptUrl, + patchPrimeVideoScriptContent, +} from "./script-patch-shared"; /** * Script Patcher - Patches Prime Video scripts via fetch interception * - * For scripts loaded via fetch/XHR: middleware patches response directly - * For scripts loaded via