feat: implement IPTV channel search debouncing and pagination, add video source auto-fallback, and improve type normalization.

This commit is contained in:
kuekhaoyang
2026-02-19 14:27:17 +08:00
parent 9ea9f85507
commit c0fcfcc4ac
11 changed files with 120 additions and 29 deletions
+12 -3
View File
@@ -17,12 +17,17 @@ import type { TypeBadge } from '@/lib/types';
// Normalize type names to merge near-duplicates
function normalizeTypeName(type: string): string {
let t = type.trim();
// Remove trailing 片/剧 suffix for grouping (e.g., "动作片" → "动作", "喜剧片" → "喜剧")
// Collapse whitespace and trim
let t = type.replace(/\s+/g, '').trim();
// Apply NFC unicode normalization
t = t.normalize('NFC');
// Remove trailing 片/剧/类 suffix for grouping (e.g., "动作片" → "动作", "喜剧片" → "喜剧")
// But keep standalone names like "电影", "电视剧" etc.
if (t.length > 2 && t.endsWith('片')) {
if (t.length > 2 && (t.endsWith('片') || t.endsWith('剧') || t.endsWith('类'))) {
t = t.slice(0, -1);
}
// Lowercase for English name normalization (e.g., "Action" vs "action")
t = t.toLowerCase();
return t;
}
@@ -40,6 +45,10 @@ export function useTypeBadges<T extends { type_name?: string }>(videos: T[]) {
const existing = typeMap.get(normalized);
if (existing) {
existing.count++;
// Prefer shorter display name (e.g., "动作" over "动作片")
if (raw.length < existing.display.length) {
existing.display = raw;
}
} else {
typeMap.set(normalized, { display: raw, count: 1 });
}
+8 -1
View File
@@ -32,7 +32,8 @@ export function useVideoPlayer(
videoId: string | null,
source: string | null,
episodeParam: string | null,
isReversed: boolean = false
isReversed: boolean = false,
onSourceUnavailable?: () => void
): UseVideoPlayerReturn {
const [videoData, setVideoData] = useState<VideoData | null>(null);
// Initialize loading to true if we have the necessary params to start fetching
@@ -45,6 +46,7 @@ export function useVideoPlayer(
// This solves the stale closure problem while keeping fetchVideoDetails stable for the player
const episodeParamRef = useRef(episodeParam);
const isReversedRef = useRef(isReversed);
const onSourceUnavailableRef = useRef(onSourceUnavailable);
useEffect(() => {
episodeParamRef.current = episodeParam;
@@ -54,6 +56,10 @@ export function useVideoPlayer(
isReversedRef.current = isReversed;
}, [isReversed]);
useEffect(() => {
onSourceUnavailableRef.current = onSourceUnavailable;
}, [onSourceUnavailable]);
const fetchVideoDetails = useCallback(async () => {
@@ -93,6 +99,7 @@ export function useVideoPlayer(
if (response.status === 404) {
setVideoError(data.error || '该视频源不可用。请返回并尝试其他来源。');
setLoading(false);
onSourceUnavailableRef.current?.();
return;
}
throw new Error(data.error || `HTTP ${response.status}: ${response.statusText}`);