Inject popup ad for xprime sources

Xprime's own site has ads, but people have found pstream (which doesnt have ads) and moves here since there are no ads. Xprime is losing money and is finding it difficult to support the proxies and servers.

clean up ad for xprime when switching sources or destroying player

new method for tracking if ad is shown

Revert "Track XPrime ad script load state in player"

This reverts commit c50bdd9ad8.

Track XPrime ad script load state in player

Adds xprimeAdScriptLoaded state and setter to the player store. Updates XPrimeAdOverlay to only show when the ad script is loaded, and base display logic to set the load state based on script events. This ensures the overlay only appears when the ad script is ready.

remove infinite loop

When conditions are met → show becomes true → timer starts
Timer fires after 5s → show becomes false
Effect re-runs (because show changed) → show becomes true again → new timer starts
Loop repeats infinitely

Refactor XPrime ad injection so it loads when the source changes
This commit is contained in:
Pas 2025-11-29 15:09:43 -07:00
parent 1f53701bab
commit b6be227ab3
11 changed files with 200 additions and 2 deletions

View file

@ -1126,6 +1126,9 @@
"doubleClickToSeek": "Double tap to seek",
"doubleClickToSeekDescription": "Double tap on the left or right side of the player to seek 10 seconds forward or backward.",
"doubleClickToSeekLabel": "Enable double tap to seek",
"disableXPrimeAds": "XPrime ads",
"disableXPrimeAdsDescription": "Disable popup ads and notifications when watching content from XPrime sources. XPrime uses ads to support their service and keep it free for you and us to use.",
"disableXPrimeAdsLabel": "Disable XPrime ads",
"sourceOrder": "Reordering sources",
"sourceOrderDescription": "Drag and drop to reorder sources. This will determine the order in which sources are checked for the media you are trying to watch. If a source is greyed out, it means the <bold>extension</bold> is required for that source. <br><br> <strong>(The default order is best for most users)</strong>",
"sourceOrderEnableLabel": "Custom source order",

View file

@ -35,6 +35,7 @@ export interface SettingsInput {
homeSectionOrder?: string[] | null;
manualSourceSelection?: boolean;
enableDoubleClickToSeek?: boolean;
disableXPrimeAds?: boolean;
}
export interface SettingsResponse {
@ -69,6 +70,7 @@ export interface SettingsResponse {
homeSectionOrder?: string[] | null;
manualSourceSelection?: boolean;
enableDoubleClickToSeek?: boolean;
disableXPrimeAds?: boolean;
}
export function updateSettings(

View file

@ -0,0 +1,69 @@
import { useEffect, useState } from "react";
import { Icon, Icons } from "@/components/Icon";
import { Flare } from "@/components/utils/Flare";
import { Transition } from "@/components/utils/Transition";
import { usePlayerStore } from "@/stores/player/store";
import { usePreferencesStore } from "@/stores/preferences";
export function XPrimeAdOverlay() {
const { sourceId, status } = usePlayerStore((s) => ({
sourceId: s.sourceId,
status: s.status,
}));
const disableXPrimeAds = usePreferencesStore((s) => s.disableXPrimeAds);
const [show, setShow] = useState(false);
useEffect(() => {
// Only show overlay when all conditions are met
const scriptExists = !!document.querySelector(
'script[data-cfasync="false"][src*="jg.prisagedibbuk.com"]',
);
const shouldShow =
sourceId === "xprimetv" &&
status === "playing" &&
!disableXPrimeAds &&
scriptExists;
if (shouldShow && !show) {
setShow(true);
const timer = setTimeout(() => {
setShow(false);
}, 5000); // Hide after 5 seconds
return () => clearTimeout(timer);
}
if (!shouldShow && show) {
setShow(false);
}
}, [sourceId, status, disableXPrimeAds]); // eslint-disable-line react-hooks/exhaustive-deps
if (!show) {
return null;
}
return (
<Transition
animation="slide-down"
show
className="absolute inset-x-0 top-4 flex justify-center pointer-events-none"
>
<Flare.Base className="hover:flare-enabled pointer-events-auto bg-video-context-background pl-4 pr-6 py-3 group w-96 h-full rounded-lg transition-colors text-video-context-type-main">
<Flare.Light
enabled
flareSize={200}
cssColorVar="--colors-video-context-light"
backgroundClass="bg-video-context-background duration-100"
className="rounded-lg"
/>
<Flare.Child className="grid grid-cols-[auto,1fr] gap-3 pointer-events-auto relative transition-transform">
<Icon className="text-xl" icon={Icons.CIRCLE_EXCLAMATION} />
<div className="w-full flex items-center">
<span className="text-sm text-center">
XPrime uses ads, but they can be disabled from settings!
</span>
</div>
</Flare.Child>
</Flare.Base>
</Transition>
);
}

View file

@ -1,3 +1,4 @@
/* eslint-disable no-console */
import fscreen from "fscreen";
import Hls, { Level } from "hls.js";
@ -18,11 +19,13 @@ import {
isUrlAlreadyProxied,
} from "@/components/player/utils/proxy";
import { useLanguageStore } from "@/stores/language";
import { usePlayerStore } from "@/stores/player/store";
import {
LoadableSource,
SourceQuality,
getPreferredQuality,
} from "@/stores/player/utils/qualities";
import { usePreferencesStore } from "@/stores/preferences";
import { processCdnLink } from "@/utils/cdn";
import {
canChangeVolume,
@ -103,6 +106,44 @@ export function makeVideoElementDisplayInterface(): DisplayInterface {
(value: void | PromiseLike<void>) => void
>();
function removeXPrimeAd() {
const script = document.querySelector(
'script[data-cfasync="false"][src*="jg.prisagedibbuk.com"]',
);
if (script) {
console.log("removing XPrime ad script");
script.remove();
}
}
function injectXPrimeAd(sourceId?: string | null) {
const currentSourceId = sourceId ?? usePlayerStore.getState().sourceId;
console.log("currentSourceId", currentSourceId);
const disableXPrimeAds = usePreferencesStore.getState().disableXPrimeAds;
// Remove script if not playing XPrime content
if (currentSourceId !== "xprimetv") {
removeXPrimeAd();
return;
}
// Inject script if playing XPrime content and ads are enabled
if (
!disableXPrimeAds &&
!document.querySelector(
'script[data-cfasync="false"][src*="jg.prisagedibbuk.com"]',
)
) {
console.log("injecting XPrime ad");
const script = document.createElement("script");
script.setAttribute("data-cfasync", "false");
script.async = true;
script.type = "text/javascript";
script.src = "//jg.prisagedibbuk.com/r47OViiCQMeGnyQ/131974";
document.head.appendChild(script);
}
}
function reportLevels() {
if (!hls) return;
const levels = hls.levels;
@ -354,6 +395,7 @@ export function makeVideoElementDisplayInterface(): DisplayInterface {
videoElement.addEventListener("play", () => {
emit("play", undefined);
emit("loading", false);
injectXPrimeAd();
});
videoElement.addEventListener("error", () => {
const err = videoElement?.error ?? null;
@ -547,6 +589,8 @@ export function makeVideoElementDisplayInterface(): DisplayInterface {
"leavepictureinpicture",
pictureInPictureChange,
);
// Clean up XPrime ad script when destroying the display
removeXPrimeAd();
},
load(ops) {
if (!ops.source) unloadSource();
@ -558,6 +602,10 @@ export function makeVideoElementDisplayInterface(): DisplayInterface {
// Set autoplay flag if starting from beginning (indicates autoplay transition)
shouldAutoplayAfterLoad = ops.startAt === 0;
setSource();
// Inject ads after source change if conditions are met
if (ops.source) {
injectXPrimeAd();
}
},
changeQuality(newAutomaticQuality, newPreferredQuality) {
if (source?.type !== "hls") return;

View file

@ -79,6 +79,7 @@ export function useSettingsState(
homeSectionOrder: string[],
manualSourceSelection: boolean,
enableDoubleClickToSeek: boolean,
disableXPrimeAds: boolean,
) {
const [proxyUrlsState, setProxyUrls, resetProxyUrls, proxyUrlsChanged] =
useDerived(proxyUrls);
@ -262,6 +263,12 @@ export function useSettingsState(
resetEnableDoubleClickToSeek,
enableDoubleClickToSeekChanged,
] = useDerived(enableDoubleClickToSeek);
const [
disableXPrimeAdsState,
setDisableXPrimeAdsState,
resetDisableXPrimeAds,
disableXPrimeAdsChanged,
] = useDerived(disableXPrimeAds);
function reset() {
resetTheme();
@ -299,6 +306,7 @@ export function useSettingsState(
resetHomeSectionOrder();
resetManualSourceSelection();
resetEnableDoubleClickToSeek();
resetDisableXPrimeAds();
}
const changed =
@ -336,7 +344,8 @@ export function useSettingsState(
enableHoldToBoostChanged ||
homeSectionOrderChanged ||
manualSourceSelectionChanged ||
enableDoubleClickToSeekChanged;
enableDoubleClickToSeekChanged ||
disableXPrimeAdsChanged;
return {
reset,
@ -516,5 +525,10 @@ export function useSettingsState(
set: setEnableDoubleClickToSeekState,
changed: enableDoubleClickToSeekChanged,
},
disableXPrimeAds: {
state: disableXPrimeAdsState,
set: setDisableXPrimeAdsState,
changed: disableXPrimeAdsChanged,
},
};
}

View file

@ -486,6 +486,9 @@ export function SettingsPage() {
(s) => s.setEnableDoubleClickToSeek,
);
const disableXPrimeAds = usePreferencesStore((s) => s.disableXPrimeAds);
const setDisableXPrimeAds = usePreferencesStore((s) => s.setDisableXPrimeAds);
const account = useAuthStore((s) => s.account);
const updateProfile = useAuthStore((s) => s.setAccountProfile);
const updateDeviceName = useAuthStore((s) => s.updateDeviceName);
@ -557,6 +560,7 @@ export function SettingsPage() {
homeSectionOrder,
manualSourceSelection,
enableDoubleClickToSeek,
disableXPrimeAds,
);
const availableSources = useMemo(() => {
@ -622,7 +626,8 @@ export function SettingsPage() {
state.enableHoldToBoost.changed ||
state.homeSectionOrder.changed ||
state.manualSourceSelection.changed ||
state.enableDoubleClickToSeek
state.enableDoubleClickToSeek ||
state.disableXPrimeAds.changed
) {
await updateSettings(backendUrl, account, {
applicationLanguage: state.appLanguage.state,
@ -651,6 +656,7 @@ export function SettingsPage() {
homeSectionOrder: state.homeSectionOrder.state,
manualSourceSelection: state.manualSourceSelection.state,
enableDoubleClickToSeek: state.enableDoubleClickToSeek.state,
disableXPrimeAds: state.disableXPrimeAds.state,
});
}
if (state.deviceName.changed) {
@ -705,6 +711,7 @@ export function SettingsPage() {
setHomeSectionOrder(state.homeSectionOrder.state);
setManualSourceSelection(state.manualSourceSelection.state);
setEnableDoubleClickToSeek(state.enableDoubleClickToSeek.state);
setDisableXPrimeAds(state.disableXPrimeAds.state);
if (state.profile.state) {
updateProfile(state.profile.state);
@ -757,6 +764,7 @@ export function SettingsPage() {
setHomeSectionOrder,
setManualSourceSelection,
setEnableDoubleClickToSeek,
setDisableXPrimeAds,
]);
return (
<SubPageLayout>
@ -838,6 +846,8 @@ export function SettingsPage() {
setManualSourceSelection={state.manualSourceSelection.set}
enableDoubleClickToSeek={state.enableDoubleClickToSeek.state}
setEnableDoubleClickToSeek={state.enableDoubleClickToSeek.set}
disableXPrimeAds={state.disableXPrimeAds.state}
setDisableXPrimeAds={state.disableXPrimeAds.set}
/>
</div>
)}

View file

@ -5,6 +5,7 @@ import { Player } from "@/components/player";
import { SkipIntroButton } from "@/components/player/atoms/SkipIntroButton";
import { UnreleasedEpisodeOverlay } from "@/components/player/atoms/UnreleasedEpisodeOverlay";
import { WatchPartyStatus } from "@/components/player/atoms/WatchPartyStatus";
import { XPrimeAdOverlay } from "@/components/player/atoms/XPrimeAdOverlay";
import { useShouldShowControls } from "@/components/player/hooks/useShouldShowControls";
import { useSkipTime } from "@/components/player/hooks/useSkipTime";
import { useIsMobile } from "@/hooks/useIsMobile";
@ -227,6 +228,7 @@ export function PlayerPart(props: PlayerPartProps) {
<Player.SubtitleDelayPopout />
<Player.SpeedChangedPopout />
<UnreleasedEpisodeOverlay />
<XPrimeAdOverlay />
<Player.NextEpisodeButton
controlsShowing={showTargets}

View file

@ -10,6 +10,7 @@ import { FlagIcon } from "@/components/FlagIcon";
import { Dropdown } from "@/components/form/Dropdown";
import { SortableListWithToggles } from "@/components/form/SortableListWithToggles";
import { Heading1 } from "@/components/utils/Text";
import { conf } from "@/setup/config";
import { appLanguageOptions } from "@/setup/i18n";
import { isAutoplayAllowed } from "@/utils/autoplay";
import { getLocaleInfo, sortLangCodes } from "@/utils/language";
@ -39,6 +40,8 @@ export function PreferencesPart(props: {
setManualSourceSelection: (v: boolean) => void;
enableDoubleClickToSeek: boolean;
setEnableDoubleClickToSeek: (v: boolean) => void;
disableXPrimeAds: boolean;
setDisableXPrimeAds: (v: boolean) => void;
}) {
const { t } = useTranslation();
const sorted = sortLangCodes(appLanguageOptions.map((item) => item.code));
@ -184,6 +187,7 @@ export function PreferencesPart(props: {
</div>
)}
</div>
{/* Low Performance Mode */}
<div>
<p className="text-white font-bold mb-3">
@ -244,6 +248,29 @@ export function PreferencesPart(props: {
</p>
</div>
</div>
{/* Disable XPrime Ads */}
{conf().XPRIME_ADS && (
<div>
<p className="text-white font-bold mb-3">
{t("settings.preferences.disableXPrimeAds")}
</p>
<p className="max-w-[25rem] font-medium">
{t("settings.preferences.disableXPrimeAdsDescription")}
</p>
<div
onClick={() =>
props.setDisableXPrimeAds(!props.disableXPrimeAds)
}
className="bg-dropdown-background hover:bg-dropdown-hoverBackground select-none my-4 cursor-pointer space-x-3 flex items-center max-w-[25rem] py-3 px-4 rounded-lg"
>
<Toggle enabled={props.disableXPrimeAds} />
<p className="flex-1 text-white font-bold">
{t("settings.preferences.disableXPrimeAdsLabel")}
</p>
</div>
</div>
)}
</div>
{/* Column */}

View file

@ -33,6 +33,7 @@ interface Config {
BANNER_MESSAGE: string;
BANNER_ID: string;
USE_TRAKT: boolean;
XPRIME_ADS: boolean;
}
export interface RuntimeConfig {
@ -62,6 +63,7 @@ export interface RuntimeConfig {
BANNER_MESSAGE: string | null;
BANNER_ID: string | null;
USE_TRAKT: boolean;
XPRIME_ADS: boolean;
}
const env: Record<keyof Config, undefined | string> = {
@ -94,6 +96,7 @@ const env: Record<keyof Config, undefined | string> = {
BANNER_MESSAGE: import.meta.env.VITE_BANNER_MESSAGE,
BANNER_ID: import.meta.env.VITE_BANNER_ID,
USE_TRAKT: import.meta.env.VITE_USE_TRAKT,
XPRIME_ADS: import.meta.env.VITE_XPRIME_ADS,
};
function coerceUndefined(value: string | null | undefined): string | undefined {
@ -169,5 +172,6 @@ export function conf(): RuntimeConfig {
BANNER_MESSAGE: getKey("BANNER_MESSAGE"),
BANNER_ID: getKey("BANNER_ID"),
USE_TRAKT: getKey("USE_TRAKT", "false") === "true",
XPRIME_ADS: getKey("XPRIME_ADS", "false") === "true",
};
}

View file

@ -150,6 +150,17 @@ export const createSourceSlice: MakeSlice<SourceSlice> = (set, get) => ({
s.status = playerStatus.PLAYING;
s.sourceId = id;
});
// Remove XPrime ad script if not playing XPrime content
if (id !== "xprimetv") {
const script = document.querySelector(
'script[data-cfasync="false"][src*="jg.prisagedibbuk.com"]',
);
if (script) {
console.log("removing XPrime ad script due to source change");
script.remove();
}
}
},
setEmbedId(id) {
set((s) => {

View file

@ -30,6 +30,7 @@ export interface PreferencesStore {
homeSectionOrder: string[];
manualSourceSelection: boolean;
enableDoubleClickToSeek: boolean;
disableXPrimeAds: boolean;
setEnableThumbnails(v: boolean): void;
setEnableAutoplay(v: boolean): void;
@ -58,6 +59,7 @@ export interface PreferencesStore {
setHomeSectionOrder(v: string[]): void;
setManualSourceSelection(v: boolean): void;
setEnableDoubleClickToSeek(v: boolean): void;
setDisableXPrimeAds(v: boolean): void;
}
export const usePreferencesStore = create(
@ -90,6 +92,7 @@ export const usePreferencesStore = create(
homeSectionOrder: ["watching", "bookmarks"],
manualSourceSelection: false,
enableDoubleClickToSeek: false,
disableXPrimeAds: false,
setEnableThumbnails(v) {
set((s) => {
s.enableThumbnails = v;
@ -230,6 +233,11 @@ export const usePreferencesStore = create(
s.enableDoubleClickToSeek = v;
});
},
setDisableXPrimeAds(v) {
set((s) => {
s.disableXPrimeAds = v;
});
},
})),
{
name: "__MW::preferences",