fix: restore HLS ad filtering

Closes #133

Closes #134
This commit is contained in:
kuekhaoyang
2026-04-02 20:12:59 +08:00
parent ba3a1a7009
commit 48ad9bc252
9 changed files with 258 additions and 107 deletions
+19 -9
View File
@@ -1,22 +1,32 @@
'use client';
import { useEffect, useRef } from 'react';
import { usePlayerSettings } from '@/components/player/hooks/usePlayerSettings';
import { useEffect } from 'react';
import { settingsStore } from '@/lib/store/settings-store';
interface AdKeywordsInjectorProps {
keywords: string[];
}
export function AdKeywordsInjector({ keywords }: AdKeywordsInjectorProps) {
const { setAdKeywords } = usePlayerSettings();
const initialized = useRef(false);
useEffect(() => {
if (!initialized.current && keywords.length > 0) {
setAdKeywords(keywords);
initialized.current = true;
const normalizedKeywords = [...new Set(
keywords
.map((keyword) => keyword.trim())
.filter((keyword) => keyword.length > 0)
)];
const currentSettings = settingsStore.getSettings();
const isUnchanged =
currentSettings.adKeywords.length === normalizedKeywords.length &&
currentSettings.adKeywords.every((keyword, index) => keyword === normalizedKeywords[index]);
if (!isUnchanged) {
settingsStore.saveSettings({
...currentSettings,
adKeywords: normalizedKeywords,
});
}
}, [keywords, setAdKeywords]);
}, [keywords]);
return null;
}
+4 -1
View File
@@ -60,7 +60,7 @@ export function DesktopVideoPlayer({
onResolutionDetected,
}: DesktopVideoPlayerProps) {
const { refs, data, actions } = useDesktopPlayerState();
const { fullscreenType: settingsFullscreenType } = usePlayerSettings();
const { fullscreenType: settingsFullscreenType } = usePlayerSettings(isPremium);
const isIOS = useIsIOS();
const isMobile = useIsMobile();
const [seekStepSeconds, setSeekStepSeconds] = React.useState(DEFAULT_SEEK_STEP_SECONDS);
@@ -160,6 +160,7 @@ export function DesktopVideoPlayer({
useHlsPlayer({
videoRef: refs.videoRef,
src,
isPremium,
autoPlay: shouldAutoPlay
});
@@ -206,6 +207,7 @@ export function DesktopVideoPlayer({
currentTime,
duration,
isPlaying,
isPremium,
totalEpisodes,
currentEpisodeIndex,
onNextEpisode,
@@ -335,6 +337,7 @@ export function DesktopVideoPlayer({
isTransitioningToNextEpisode={isTransitioningToNextEpisode}
// More Menu Props
showMoreMenu={data.showMoreMenu}
isPremium={isPremium}
isProxied={src.includes('/api/proxy')}
onToggleMoreMenu={() => actions.setShowMoreMenu(!data.showMoreMenu)}
onMoreMenuMouseEnter={() => {
@@ -9,6 +9,7 @@ import { createPortal } from 'react-dom';
interface DesktopMoreMenuProps {
showMoreMenu: boolean;
isPremium?: boolean;
isProxied?: boolean;
onToggleMoreMenu: () => void;
onMouseEnter: () => void;
@@ -22,6 +23,7 @@ interface DesktopMoreMenuProps {
export function DesktopMoreMenu({
showMoreMenu,
isPremium = false,
isProxied = false,
onToggleMoreMenu,
onMouseEnter,
@@ -60,7 +62,7 @@ export function DesktopMoreMenu({
setDanmakuFontSize,
danmakuDisplayArea,
setDanmakuDisplayArea,
} = usePlayerSettings();
} = usePlayerSettings(isPremium);
const buttonRef = React.useRef<HTMLButtonElement>(null);
const menuRef = React.useRef<HTMLDivElement>(null);
@@ -23,6 +23,7 @@ interface DesktopOverlayProps {
onSkipForward: () => void;
onSkipBackward: () => void;
showMoreMenu: boolean;
isPremium?: boolean;
isProxied: boolean;
onToggleMoreMenu: () => void;
onMoreMenuMouseEnter: () => void;
@@ -62,6 +63,7 @@ export function DesktopOverlay({
onSkipBackward,
showControls,
showMoreMenu,
isPremium = false,
isProxied,
onToggleMoreMenu,
onMoreMenuMouseEnter,
@@ -89,6 +91,7 @@ export function DesktopOverlay({
<div className={`absolute top-8 left-6 z-40 transition-opacity duration-300 ${showControls ? 'opacity-100' : 'opacity-0'}`} style={{ pointerEvents: showControls ? 'auto' : 'none' }}>
<DesktopMoreMenu
showMoreMenu={showMoreMenu}
isPremium={isPremium}
isProxied={isProxied}
onToggleMoreMenu={onToggleMoreMenu}
onMouseEnter={onMoreMenuMouseEnter}
@@ -13,6 +13,7 @@ interface DesktopOverlayWrapperProps {
onSkipBackward: () => void;
isTransitioningToNextEpisode?: boolean;
showMoreMenu: boolean;
isPremium?: boolean;
isProxied: boolean;
onToggleMoreMenu: () => void;
onMoreMenuMouseEnter: () => void;
@@ -43,6 +44,7 @@ export function DesktopOverlayWrapper({
onSkipBackward,
isTransitioningToNextEpisode = false,
showMoreMenu,
isPremium = false,
isProxied,
onToggleMoreMenu,
onMoreMenuMouseEnter,
@@ -93,6 +95,7 @@ export function DesktopOverlayWrapper({
onSkipForward={onSkipForward}
onSkipBackward={onSkipBackward}
showMoreMenu={showMoreMenu}
isPremium={isPremium}
isProxied={isProxied}
onToggleMoreMenu={onToggleMoreMenu}
onMoreMenuMouseEnter={onMoreMenuMouseEnter}
+3 -1
View File
@@ -9,6 +9,7 @@ interface UseAutoSkipProps {
currentTime: number;
duration: number;
isPlaying: boolean;
isPremium?: boolean;
totalEpisodes?: number;
currentEpisodeIndex?: number;
onNextEpisode?: () => void;
@@ -27,6 +28,7 @@ export function useAutoSkip({
currentTime,
duration,
isPlaying,
isPremium = false,
totalEpisodes = 1,
currentEpisodeIndex = 0,
onNextEpisode,
@@ -39,7 +41,7 @@ export function useAutoSkip({
skipIntroSeconds,
autoSkipOutro,
skipOutroSeconds,
} = usePlayerSettings();
} = usePlayerSettings(isPremium);
// Track if we've already skipped intro for this video session
const hasSkippedIntroRef = useRef(false);
+3 -1
View File
@@ -6,6 +6,7 @@ import { filterM3u8Ad } from '@/lib/utils/m3u8-utils';
interface UseHlsPlayerProps {
videoRef: React.RefObject<HTMLVideoElement | null>;
src: string;
isPremium?: boolean;
autoPlay?: boolean;
onAutoPlayPrevented?: (error: Error) => void;
onError?: (message: string) => void;
@@ -14,12 +15,13 @@ interface UseHlsPlayerProps {
export function useHlsPlayer({
videoRef,
src,
isPremium = false,
autoPlay = false,
onAutoPlayPrevented,
onError
}: UseHlsPlayerProps) {
const hlsRef = useRef<Hls | null>(null);
const { adFilterMode, adKeywords } = usePlayerSettings();
const { adFilterMode, adKeywords } = usePlayerSettings(isPremium);
const isAdFilterEnabled = adFilterMode !== 'off';
useEffect(() => {
+119 -82
View File
@@ -1,135 +1,172 @@
'use client';
import { useState, useEffect, useCallback } from 'react';
import { settingsStore, AdFilterMode } from '@/lib/store/settings-store';
import {
settingsStore,
type AppSettings,
type AdFilterMode,
} from '@/lib/store/settings-store';
import {
premiumModeSettingsStore,
type ModeSettings,
} from '@/lib/store/premium-mode-settings';
interface PlayerSettingsSnapshot {
autoNextEpisode: boolean;
autoSkipIntro: boolean;
skipIntroSeconds: number;
autoSkipOutro: boolean;
skipOutroSeconds: number;
showModeIndicator: boolean;
adFilter: boolean;
adFilterMode: AdFilterMode;
adKeywords: string[];
fullscreenType: 'auto' | 'native' | 'window';
proxyMode: 'retry' | 'none' | 'always';
danmakuEnabled: boolean;
danmakuApiUrl: string;
danmakuOpacity: number;
danmakuFontSize: number;
danmakuDisplayArea: number;
}
function getPlayerSettingsSnapshot(isPremium: boolean): PlayerSettingsSnapshot {
const globalSettings = settingsStore.getSettings();
const modeSettings = isPremium ? premiumModeSettingsStore.getSettings() : globalSettings;
return {
autoNextEpisode: modeSettings.autoNextEpisode,
autoSkipIntro: modeSettings.autoSkipIntro,
skipIntroSeconds: modeSettings.skipIntroSeconds,
autoSkipOutro: modeSettings.autoSkipOutro,
skipOutroSeconds: modeSettings.skipOutroSeconds,
showModeIndicator: modeSettings.showModeIndicator,
adFilter: globalSettings.adFilter,
adFilterMode: modeSettings.adFilterMode,
adKeywords: globalSettings.adKeywords,
fullscreenType: modeSettings.fullscreenType,
proxyMode: modeSettings.proxyMode,
danmakuEnabled: modeSettings.danmakuEnabled,
danmakuApiUrl: modeSettings.danmakuApiUrl,
danmakuOpacity: modeSettings.danmakuOpacity,
danmakuFontSize: modeSettings.danmakuFontSize,
danmakuDisplayArea: modeSettings.danmakuDisplayArea,
};
}
/**
* Hook to access and update player settings from the settings store
* Provides reactive updates when settings change
*/
export function usePlayerSettings() {
const [settings, setSettings] = useState(() => {
const stored = settingsStore.getSettings();
return {
autoNextEpisode: stored.autoNextEpisode,
autoSkipIntro: stored.autoSkipIntro,
skipIntroSeconds: stored.skipIntroSeconds,
autoSkipOutro: stored.autoSkipOutro,
skipOutroSeconds: stored.skipOutroSeconds,
showModeIndicator: stored.showModeIndicator,
adFilter: stored.adFilter,
adFilterMode: stored.adFilterMode,
adKeywords: stored.adKeywords,
fullscreenType: stored.fullscreenType,
proxyMode: stored.proxyMode,
danmakuEnabled: stored.danmakuEnabled,
danmakuApiUrl: stored.danmakuApiUrl,
danmakuOpacity: stored.danmakuOpacity,
danmakuFontSize: stored.danmakuFontSize,
danmakuDisplayArea: stored.danmakuDisplayArea,
};
});
export function usePlayerSettings(isPremium: boolean = false) {
const [settings, setSettings] = useState(() => getPlayerSettingsSnapshot(isPremium));
// Subscribe to settings changes
useEffect(() => {
const unsubscribe = settingsStore.subscribe(() => {
const stored = settingsStore.getSettings();
setSettings({
autoNextEpisode: stored.autoNextEpisode,
autoSkipIntro: stored.autoSkipIntro,
skipIntroSeconds: stored.skipIntroSeconds,
autoSkipOutro: stored.autoSkipOutro,
skipOutroSeconds: stored.skipOutroSeconds,
showModeIndicator: stored.showModeIndicator,
adFilter: stored.adFilter,
adFilterMode: stored.adFilterMode,
adKeywords: stored.adKeywords,
fullscreenType: stored.fullscreenType,
proxyMode: stored.proxyMode,
danmakuEnabled: stored.danmakuEnabled,
danmakuApiUrl: stored.danmakuApiUrl,
danmakuOpacity: stored.danmakuOpacity,
danmakuFontSize: stored.danmakuFontSize,
danmakuDisplayArea: stored.danmakuDisplayArea,
const syncSettings = () => {
setSettings(getPlayerSettingsSnapshot(isPremium));
};
const modeStore = isPremium ? premiumModeSettingsStore : settingsStore;
const unsubscribeModeStore = modeStore.subscribe(syncSettings);
const unsubscribeGlobalStore = isPremium ? settingsStore.subscribe(syncSettings) : null;
syncSettings();
return () => {
unsubscribeModeStore();
unsubscribeGlobalStore?.();
};
}, [isPremium]);
const updateModeSettings = useCallback((partial: Partial<ModeSettings>) => {
if (isPremium) {
const currentSettings = premiumModeSettingsStore.getSettings();
premiumModeSettingsStore.saveSettings({
...currentSettings,
...partial,
});
});
return unsubscribe;
}, []);
return;
}
const updateSetting = useCallback(<K extends keyof typeof settings>(
key: K,
value: typeof settings[K]
) => {
const currentSettings = settingsStore.getSettings();
settingsStore.saveSettings({
...currentSettings,
[key]: value,
...partial,
});
}, [isPremium]);
const updateGlobalSettings = useCallback((partial: Partial<AppSettings>) => {
const currentSettings = settingsStore.getSettings();
settingsStore.saveSettings({
...currentSettings,
...partial,
});
}, []);
const setAutoNextEpisode = useCallback((value: boolean) => {
updateSetting('autoNextEpisode', value);
}, [updateSetting]);
updateModeSettings({ autoNextEpisode: value });
}, [updateModeSettings]);
const setAutoSkipIntro = useCallback((value: boolean) => {
updateSetting('autoSkipIntro', value);
}, [updateSetting]);
updateModeSettings({ autoSkipIntro: value });
}, [updateModeSettings]);
const setSkipIntroSeconds = useCallback((value: number) => {
updateSetting('skipIntroSeconds', Math.max(0, value));
}, [updateSetting]);
updateModeSettings({ skipIntroSeconds: Math.max(0, value) });
}, [updateModeSettings]);
const setAutoSkipOutro = useCallback((value: boolean) => {
updateSetting('autoSkipOutro', value);
}, [updateSetting]);
updateModeSettings({ autoSkipOutro: value });
}, [updateModeSettings]);
const setSkipOutroSeconds = useCallback((value: number) => {
updateSetting('skipOutroSeconds', Math.max(0, value));
}, [updateSetting]);
updateModeSettings({ skipOutroSeconds: Math.max(0, value) });
}, [updateModeSettings]);
const setShowModeIndicator = useCallback((value: boolean) => {
updateSetting('showModeIndicator', value);
}, [updateSetting]);
updateModeSettings({ showModeIndicator: value });
}, [updateModeSettings]);
const setAdFilter = useCallback((value: boolean) => {
updateSetting('adFilter', value);
}, [updateSetting]);
updateGlobalSettings({ adFilter: value });
}, [updateGlobalSettings]);
const setAdFilterMode = useCallback((value: AdFilterMode) => {
updateSetting('adFilterMode', value);
}, [updateSetting]);
updateModeSettings({ adFilterMode: value });
}, [updateModeSettings]);
const setAdKeywords = useCallback((value: string[]) => {
updateSetting('adKeywords', value);
}, [updateSetting]);
updateGlobalSettings({ adKeywords: value });
}, [updateGlobalSettings]);
const setFullscreenType = useCallback((value: 'auto' | 'native' | 'window') => {
updateSetting('fullscreenType', value);
}, [updateSetting]);
updateModeSettings({ fullscreenType: value });
}, [updateModeSettings]);
const setProxyMode = useCallback((value: 'retry' | 'none' | 'always') => {
updateSetting('proxyMode', value);
}, [updateSetting]);
updateModeSettings({ proxyMode: value });
}, [updateModeSettings]);
const setDanmakuEnabled = useCallback((value: boolean) => {
updateSetting('danmakuEnabled', value);
}, [updateSetting]);
updateModeSettings({ danmakuEnabled: value });
}, [updateModeSettings]);
const setDanmakuApiUrl = useCallback((value: string) => {
updateSetting('danmakuApiUrl', value);
}, [updateSetting]);
updateModeSettings({ danmakuApiUrl: value });
}, [updateModeSettings]);
const setDanmakuOpacity = useCallback((value: number) => {
updateSetting('danmakuOpacity', Math.max(0.1, Math.min(1, value)));
}, [updateSetting]);
updateModeSettings({ danmakuOpacity: Math.max(0.1, Math.min(1, value)) });
}, [updateModeSettings]);
const setDanmakuFontSize = useCallback((value: number) => {
updateSetting('danmakuFontSize', value);
}, [updateSetting]);
updateModeSettings({ danmakuFontSize: value });
}, [updateModeSettings]);
const setDanmakuDisplayArea = useCallback((value: number) => {
updateSetting('danmakuDisplayArea', value);
}, [updateSetting]);
updateModeSettings({ danmakuDisplayArea: value });
}, [updateModeSettings]);
return {
...settings,