diff --git a/components/player/desktop/DesktopControls.tsx b/components/player/desktop/DesktopControls.tsx
index 2339f85..f5d0f58 100644
--- a/components/player/desktop/DesktopControls.tsx
+++ b/components/player/desktop/DesktopControls.tsx
@@ -1,9 +1,7 @@
import React from 'react';
-import { Icons } from '@/components/ui/Icon';
import { DesktopProgressBar } from './DesktopProgressBar';
-import { DesktopVolumeControl } from './DesktopVolumeControl';
-import { DesktopSpeedMenu } from './DesktopSpeedMenu';
-import { DesktopMoreMenu } from './DesktopMoreMenu';
+import { DesktopLeftControls } from './DesktopLeftControls';
+import { DesktopRightControls } from './DesktopRightControls';
interface DesktopControlsProps {
showControls: boolean;
@@ -44,44 +42,17 @@ interface DesktopControlsProps {
speeds: number[];
}
-export function DesktopControls({
- showControls,
- isPlaying,
- currentTime,
- duration,
- volume,
- isMuted,
- isFullscreen,
- playbackRate,
- showSpeedMenu,
- showMoreMenu,
- showVolumeBar,
- isPiPSupported,
- isAirPlaySupported,
- progressBarRef,
- volumeBarRef,
- onTogglePlay,
- onSkipForward,
- onSkipBackward,
- onToggleMute,
- onVolumeChange,
- onVolumeMouseDown,
- onToggleFullscreen,
- onTogglePictureInPicture,
- onShowAirPlayMenu,
- onToggleSpeedMenu,
- onToggleMoreMenu,
- onSpeedChange,
- onCopyLink,
- onProgressClick,
- onProgressMouseDown,
- onSpeedMenuMouseEnter,
- onSpeedMenuMouseLeave,
- onMoreMenuMouseEnter,
- onMoreMenuMouseLeave,
- formatTime,
- speeds
-}: DesktopControlsProps) {
+export function DesktopControls(props: DesktopControlsProps) {
+ const {
+ showControls,
+ currentTime,
+ duration,
+ progressBarRef,
+ onProgressClick,
+ onProgressMouseDown,
+ formatTime,
+ } = props;
+
return (
- {/* Left Controls */}
-
- {/* Play/Pause */}
-
- {isPlaying ? : }
-
-
- {/* Skip Backward 10s */}
-
-
-
-
- {/* Skip Forward 10s */}
-
-
-
-
- {/* Volume */}
-
-
- {/* Time */}
-
- {formatTime(currentTime)} / {formatTime(duration)}
-
-
-
- {/* Right Controls */}
-
- {/* Playback Speed */}
-
-
- {/* Picture-in-Picture */}
- {isPiPSupported && (
-
-
-
- )}
-
- {/* AirPlay */}
- {isAirPlaySupported && (
-
-
-
- )}
-
- {/* More Menu */}
-
-
- {/* Fullscreen */}
-
- {isFullscreen ? : }
-
-
+
+
diff --git a/components/player/desktop/DesktopLeftControls.tsx b/components/player/desktop/DesktopLeftControls.tsx
new file mode 100644
index 0000000..674a7ad
--- /dev/null
+++ b/components/player/desktop/DesktopLeftControls.tsx
@@ -0,0 +1,86 @@
+import React from 'react';
+import { Icons } from '@/components/ui/Icon';
+import { DesktopVolumeControl } from './DesktopVolumeControl';
+
+interface DesktopLeftControlsProps {
+ isPlaying: boolean;
+ currentTime: number;
+ duration: number;
+ volume: number;
+ isMuted: boolean;
+ showVolumeBar: boolean;
+ volumeBarRef: React.RefObject;
+ onTogglePlay: () => void;
+ onSkipForward: () => void;
+ onSkipBackward: () => void;
+ onToggleMute: () => void;
+ onVolumeChange: (e: React.MouseEvent) => void;
+ onVolumeMouseDown: (e: React.MouseEvent) => void;
+ formatTime: (seconds: number) => string;
+}
+
+export function DesktopLeftControls({
+ isPlaying,
+ currentTime,
+ duration,
+ volume,
+ isMuted,
+ showVolumeBar,
+ volumeBarRef,
+ onTogglePlay,
+ onSkipForward,
+ onSkipBackward,
+ onToggleMute,
+ onVolumeChange,
+ onVolumeMouseDown,
+ formatTime
+}: DesktopLeftControlsProps) {
+ return (
+
+ {/* Play/Pause */}
+
+ {isPlaying ? : }
+
+
+ {/* Skip Backward 10s */}
+
+
+
+
+ {/* Skip Forward 10s */}
+
+
+
+
+ {/* Volume */}
+
+
+ {/* Time */}
+
+ {formatTime(currentTime)} / {formatTime(duration)}
+
+
+ );
+}
diff --git a/components/player/desktop/DesktopRightControls.tsx b/components/player/desktop/DesktopRightControls.tsx
new file mode 100644
index 0000000..a2b01b2
--- /dev/null
+++ b/components/player/desktop/DesktopRightControls.tsx
@@ -0,0 +1,103 @@
+import React from 'react';
+import { Icons } from '@/components/ui/Icon';
+import { DesktopSpeedMenu } from './DesktopSpeedMenu';
+import { DesktopMoreMenu } from './DesktopMoreMenu';
+
+interface DesktopRightControlsProps {
+ isFullscreen: boolean;
+ playbackRate: number;
+ showSpeedMenu: boolean;
+ showMoreMenu: boolean;
+ isPiPSupported: boolean;
+ isAirPlaySupported: boolean;
+ onToggleFullscreen: () => void;
+ onTogglePictureInPicture: () => void;
+ onShowAirPlayMenu: () => void;
+ onToggleSpeedMenu: () => void;
+ onToggleMoreMenu: () => void;
+ onSpeedChange: (speed: number) => void;
+ onCopyLink: () => void;
+ onSpeedMenuMouseEnter: () => void;
+ onSpeedMenuMouseLeave: () => void;
+ onMoreMenuMouseEnter: () => void;
+ onMoreMenuMouseLeave: () => void;
+ speeds: number[];
+}
+
+export function DesktopRightControls({
+ isFullscreen,
+ playbackRate,
+ showSpeedMenu,
+ showMoreMenu,
+ isPiPSupported,
+ isAirPlaySupported,
+ onToggleFullscreen,
+ onTogglePictureInPicture,
+ onShowAirPlayMenu,
+ onToggleSpeedMenu,
+ onToggleMoreMenu,
+ onSpeedChange,
+ onCopyLink,
+ onSpeedMenuMouseEnter,
+ onSpeedMenuMouseLeave,
+ onMoreMenuMouseEnter,
+ onMoreMenuMouseLeave,
+ speeds
+}: DesktopRightControlsProps) {
+ return (
+
+ {/* Playback Speed */}
+
+
+ {/* Picture-in-Picture */}
+ {isPiPSupported && (
+
+
+
+ )}
+
+ {/* AirPlay */}
+ {isAirPlaySupported && (
+
+
+
+ )}
+
+ {/* More Menu */}
+
+
+ {/* Fullscreen */}
+
+ {isFullscreen ? : }
+
+
+ );
+}
diff --git a/components/search/VideoCard.tsx b/components/search/VideoCard.tsx
new file mode 100644
index 0000000..902a2bb
--- /dev/null
+++ b/components/search/VideoCard.tsx
@@ -0,0 +1,149 @@
+'use client';
+
+import { memo } from 'react';
+import Link from 'next/link';
+import Image from 'next/image';
+import { Card } from '@/components/ui/Card';
+import { Badge } from '@/components/ui/Badge';
+import { Icons } from '@/components/ui/Icon';
+import { LatencyBadge } from '@/components/ui/LatencyBadge';
+
+interface Video {
+ vod_id: string;
+ vod_name: string;
+ vod_pic?: string;
+ vod_remarks?: string;
+ vod_year?: string;
+ type_name?: string;
+ source: string;
+ sourceName?: string;
+ isNew?: boolean;
+ latency?: number;
+}
+
+interface VideoCardProps {
+ video: Video;
+ videoUrl: string;
+ cardId: string;
+ isActive: boolean;
+ onCardClick: (e: React.MouseEvent, cardId: string, videoUrl: string) => void;
+}
+
+export const VideoCard = memo(({
+ video,
+ videoUrl,
+ cardId,
+ isActive,
+ onCardClick
+}) => {
+ return (
+
+
onCardClick(e, cardId, videoUrl)}
+ role="listitem"
+ aria-label={`${video.vod_name}${video.vod_remarks ? ` - ${video.vod_remarks}` : ''}`}
+ prefetch={false}
+ >
+
+ {/* Poster */}
+
+ {video.vod_pic ? (
+
{
+ const target = e.currentTarget as HTMLImageElement;
+ target.style.opacity = '0';
+ }}
+ />
+ ) : (
+
+
+
+ )}
+
+ {/* Fallback Icon */}
+
+
+
+
+ {/* Badge Container */}
+
+ {video.sourceName && (
+
+ {video.sourceName}
+
+ )}
+
+ {video.latency !== undefined && (
+
+ )}
+
+
+ {/* Overlay */}
+
+
+ {isActive && (
+
+ 再次点击播放 →
+
+ )}
+ {video.type_name && (
+
+ {video.type_name}
+
+ )}
+ {video.vod_year && (
+
+
+ {video.vod_year}
+
+ )}
+
+
+
+
+ {/* Info */}
+
+
+ {video.vod_name}
+
+ {video.vod_remarks && (
+
+ {video.vod_remarks}
+
+ )}
+
+
+
+
+ );
+});
+
+VideoCard.displayName = 'VideoCard';
diff --git a/components/search/VideoGrid.tsx b/components/search/VideoGrid.tsx
index 0a07ed7..6ef6009 100644
--- a/components/search/VideoGrid.tsx
+++ b/components/search/VideoGrid.tsx
@@ -1,12 +1,7 @@
'use client';
-import { useState, useRef, useCallback, useMemo, memo, useEffect } from 'react';
-import Link from 'next/link';
-import Image from 'next/image';
-import { Card } from '@/components/ui/Card';
-import { Badge } from '@/components/ui/Badge';
-import { Icons } from '@/components/ui/Icon';
-import { LatencyBadge } from '@/components/ui/LatencyBadge';
+import { useState, useRef, useCallback, useMemo, memo } from 'react';
+import { VideoCard } from './VideoCard';
interface Video {
vod_id: string;
@@ -18,7 +13,7 @@ interface Video {
source: string;
sourceName?: string;
isNew?: boolean;
- latency?: number; // Response time in milliseconds
+ latency?: number;
}
interface VideoGridProps {
@@ -26,146 +21,6 @@ interface VideoGridProps {
className?: string;
}
-// Memoized VideoCard component to prevent unnecessary re-renders
-const VideoCard = memo(({
- video,
- videoUrl,
- cardId,
- isActive,
- onCardClick
-}: {
- video: Video;
- videoUrl: string;
- cardId: string;
- isActive: boolean;
- onCardClick: (e: React.MouseEvent, cardId: string, videoUrl: string) => void;
-}) => {
- return (
-
-
onCardClick(e, cardId, videoUrl)}
- role="listitem"
- aria-label={`${video.vod_name}${video.vod_remarks ? ` - ${video.vod_remarks}` : ''}`}
- prefetch={false}
- >
-
- {/* Poster */}
-
- {video.vod_pic ? (
-
{
- // Fallback for next/image error is tricky because it doesn't expose the img element directly in the same way
- // But we can try to hide it or show a placeholder
- const target = e.currentTarget as HTMLImageElement;
- // Since next/image manages the src, we might need a state or a different approach for fallback
- // For simplicity in this performance fix, we'll rely on the parent div background or a separate placeholder component
- // But actually, we can just use a simple img tag for fallback if next/image fails,
- // or better: use a state to switch to fallback.
- // However, inside a memoized component, adding state might be heavy.
- // Let's stick to a simple CSS hide for now or use the unoptimized prop if it fails? No.
- // Let's just hide it and let the background icon show.
- target.style.opacity = '0';
- }}
- />
- ) : (
-
-
-
- )}
-
- {/* Fallback Icon (always rendered behind image, visible if image fails/loads) */}
-
-
-
-
- {/* Badge Container - Top, spans full width with proper spacing */}
-
- {/* Source Badge - Left */}
- {video.sourceName && (
-
- {video.sourceName}
-
- )}
-
- {/* Latency Badge - Right */}
- {video.latency !== undefined && (
-
- )}
-
-
- {/* Overlay - Show on hover (desktop) or when active (mobile) */}
-
-
- {/* Mobile indicator when active */}
- {isActive && (
-
- 再次点击播放 →
-
- )}
- {video.type_name && (
-
- {video.type_name}
-
- )}
- {video.vod_year && (
-
-
- {video.vod_year}
-
- )}
-
-
-
-
- {/* Info - Fixed height section */}
-
-
- {video.vod_name}
-
- {video.vod_remarks && (
-
- {video.vod_remarks}
-
- )}
-
-
-
-
- );
-});
-
-VideoCard.displayName = 'VideoCard';
-
export const VideoGrid = memo(function VideoGrid({ videos, className = '' }: VideoGridProps) {
const [activeCardId, setActiveCardId] = useState(null);
const [visibleCount, setVisibleCount] = useState(24);
diff --git a/lib/api/client.ts b/lib/api/client.ts
index 5bae31d..4b85bf0 100644
--- a/lib/api/client.ts
+++ b/lib/api/client.ts
@@ -1,68 +1,17 @@
/**
* API Client for fetching video data from multiple sources
- * Handles parallel requests, timeouts, retries, and data normalization
+ * Handles parallel requests and data normalization
*/
import type {
VideoSource,
VideoItem,
VideoDetail,
- Episode,
ApiSearchResponse,
ApiDetailResponse,
} from '@/lib/types';
-
-const REQUEST_TIMEOUT = 15000;
-const MAX_RETRIES = 3;
-const RETRY_DELAY = 1000;
-
-/**
- * Fetch with timeout support
- */
-async function fetchWithTimeout(
- url: string,
- options: RequestInit = {},
- timeout: number = REQUEST_TIMEOUT
-): Promise {
- const controller = new AbortController();
- const timeoutId = setTimeout(() => controller.abort(), timeout);
-
- try {
- const response = await fetch(url, {
- ...options,
- signal: controller.signal,
- });
- clearTimeout(timeoutId);
- return response;
- } catch (error) {
- clearTimeout(timeoutId);
- throw error;
- }
-}
-
-/**
- * Retry logic wrapper
- */
-async function withRetry(
- fn: () => Promise,
- retries: number = MAX_RETRIES
-): Promise {
- let lastError: Error | null = null;
-
- for (let i = 0; i <= retries; i++) {
- try {
- return await fn();
- } catch (error) {
- lastError = error as Error;
-
- if (i < retries) {
- await new Promise(resolve => setTimeout(resolve, RETRY_DELAY * (i + 1)));
- }
- }
- }
-
- throw lastError;
-}
+import { fetchWithTimeout, withRetry } from './http-utils';
+import { parseEpisodes } from './parsers';
/**
* Search videos from a single source
@@ -146,29 +95,7 @@ export async function searchVideos(
return Promise.all(searchPromises);
}
-/**
- * Parse episode URL string into structured array
- */
-function parseEpisodes(playUrl: string): Episode[] {
- if (!playUrl) return [];
- try {
- // Format: "Episode1$url1#Episode2$url2#..."
- const episodes = playUrl.split('#').filter(Boolean);
-
- return episodes.map((episode, index) => {
- const [name, url] = episode.split('$');
- return {
- name: name || `Episode ${index + 1}`,
- url: url || '',
- index,
- };
- });
- } catch (error) {
- console.error('Failed to parse episodes:', error);
- return [];
- }
-}
/**
* Get video detail from a single source
diff --git a/lib/api/http-utils.ts b/lib/api/http-utils.ts
new file mode 100644
index 0000000..26f3db2
--- /dev/null
+++ b/lib/api/http-utils.ts
@@ -0,0 +1,56 @@
+/**
+ * HTTP Utilities for API calls
+ * Handles timeouts and retries
+ */
+
+const REQUEST_TIMEOUT = 15000;
+const MAX_RETRIES = 3;
+const RETRY_DELAY = 1000;
+
+/**
+ * Fetch with timeout support
+ */
+export async function fetchWithTimeout(
+ url: string,
+ options: RequestInit = {},
+ timeout: number = REQUEST_TIMEOUT
+): Promise {
+ const controller = new AbortController();
+ const timeoutId = setTimeout(() => controller.abort(), timeout);
+
+ try {
+ const response = await fetch(url, {
+ ...options,
+ signal: controller.signal,
+ });
+ clearTimeout(timeoutId);
+ return response;
+ } catch (error) {
+ clearTimeout(timeoutId);
+ throw error;
+ }
+}
+
+/**
+ * Retry logic wrapper
+ */
+export async function withRetry(
+ fn: () => Promise,
+ retries: number = MAX_RETRIES
+): Promise {
+ let lastError: Error | null = null;
+
+ for (let i = 0; i <= retries; i++) {
+ try {
+ return await fn();
+ } catch (error) {
+ lastError = error as Error;
+
+ if (i < retries) {
+ await new Promise(resolve => setTimeout(resolve, RETRY_DELAY * (i + 1)));
+ }
+ }
+ }
+
+ throw lastError;
+}
diff --git a/lib/api/parsers.ts b/lib/api/parsers.ts
new file mode 100644
index 0000000..3899b45
--- /dev/null
+++ b/lib/api/parsers.ts
@@ -0,0 +1,29 @@
+/**
+ * API Response Parsers
+ */
+
+import type { Episode } from '@/lib/types';
+
+/**
+ * Parse episode URL string into structured array
+ */
+export function parseEpisodes(playUrl: string): Episode[] {
+ if (!playUrl) return [];
+
+ try {
+ // Format: "Episode1$url1#Episode2$url2#..."
+ const episodes = playUrl.split('#').filter(Boolean);
+
+ return episodes.map((episode, index) => {
+ const [name, url] = episode.split('$');
+ return {
+ name: name || `Episode ${index + 1}`,
+ url: url || '',
+ index,
+ };
+ });
+ } catch (error) {
+ console.error('Failed to parse episodes:', error);
+ return [];
+ }
+}
diff --git a/lib/hooks/useParallelSearch.ts b/lib/hooks/useParallelSearch.ts
index 6e6be43..511fb68 100644
--- a/lib/hooks/useParallelSearch.ts
+++ b/lib/hooks/useParallelSearch.ts
@@ -4,6 +4,7 @@ import { useState, useRef, useCallback } from 'react';
import { getSourceName, SOURCE_IDS } from '@/lib/utils/source-names';
import { calculateRelevanceScore } from '@/lib/utils/search';
import { sortVideos } from '@/lib/utils/sort';
+import { binaryInsertVideos } from '@/lib/utils/sorted-insert';
import type { SortOption } from '@/lib/store/settings-store';
interface Video {
@@ -119,44 +120,8 @@ export function useParallelSearch(
}));
-
- // Optimized: Insert new videos in sorted position instead of re-sorting entire array
- setResults((prev) => {
- if (prev.length === 0) return newVideos;
-
- // Binary insert for better performance with combined sorting
- const combined = [...prev];
- for (const video of newVideos) {
- const relevanceScore = video.relevanceScore || 0;
- const latency = video.latency || 99999; // Default high latency for sorting
-
- // Find insert position using binary search
- // Sort by: 1) relevance score (DESC), 2) latency (ASC)
- let left = 0;
- let right = combined.length;
- while (left < right) {
- const mid = Math.floor((left + right) / 2);
- const midRelevance = combined[mid].relevanceScore || 0;
- const midLatency = combined[mid].latency || 99999;
-
- // Compare by relevance first
- if (midRelevance > relevanceScore) {
- left = mid + 1;
- } else if (midRelevance < relevanceScore) {
- right = mid;
- } else {
- // Same relevance, compare by latency (lower is better)
- if (midLatency < latency) {
- left = mid + 1;
- } else {
- right = mid;
- }
- }
- }
- combined.splice(left, 0, video);
- }
- return combined;
- });
+ // Optimized: Insert new videos in sorted position
+ setResults((prev) => binaryInsertVideos(prev, newVideos));
// Update source stats
if (!sourcesMap.has(data.source)) {
diff --git a/lib/store/settings-store.ts b/lib/store/settings-store.ts
index bac382d..f980c9a 100644
--- a/lib/store/settings-store.ts
+++ b/lib/store/settings-store.ts
@@ -3,6 +3,7 @@
*/
import type { VideoSource } from '@/lib/types';
+import { DEFAULT_SOURCES } from '@/lib/api/default-sources';
export type SortOption =
| 'default'
@@ -36,49 +37,7 @@ export const sortOptions: Record = {
'name-desc': '按名称(Z-A)',
};
-export const getDefaultSources = (): VideoSource[] => {
- // Import from video-sources to get all 38 default sources
- return [
- { id: 'feifan', name: '非凡资源', baseUrl: 'http://ffzy5.tv/api.php/provide/vod', searchPath: '', detailPath: '', enabled: true, priority: 1 },
- { id: 'wolong', name: '卧龙资源', baseUrl: 'https://wolongzyw.com/api.php/provide/vod', searchPath: '', detailPath: '', enabled: true, priority: 2 },
- { id: 'zuida', name: '最大资源', baseUrl: 'https://api.zuidapi.com/api.php/provide/vod', searchPath: '', detailPath: '', enabled: true, priority: 3 },
- { id: 'baiduyun', name: '百度云资源', baseUrl: 'https://api.apibdzy.com/api.php/provide/vod', searchPath: '', detailPath: '', enabled: true, priority: 4 },
- { id: 'baofeng', name: '暴风资源', baseUrl: 'https://bfzyapi.com/api.php/provide/vod', searchPath: '', detailPath: '', enabled: true, priority: 5 },
- { id: 'jisu', name: '极速资源', baseUrl: 'https://jszyapi.com/api.php/provide/vod', searchPath: '', detailPath: '', enabled: true, priority: 6 },
- { id: 'tianya', name: '天涯资源', baseUrl: 'https://tyyszy.com/api.php/provide/vod', searchPath: '', detailPath: '', enabled: true, priority: 7 },
- { id: 'wujin', name: '无尽资源', baseUrl: 'https://api.wujinapi.com/api.php/provide/vod', searchPath: '', detailPath: '', enabled: true, priority: 8 },
- { id: 'modu', name: '魔都资源', baseUrl: 'https://www.mdzyapi.com/api.php/provide/vod', searchPath: '', detailPath: '', enabled: true, priority: 9 },
- { id: 'sanliuling', name: '360资源', baseUrl: 'https://360zy.com/api.php/provide/vod', searchPath: '', detailPath: '', enabled: true, priority: 10 },
- { id: 'dytt', name: '电影天堂', baseUrl: 'http://caiji.dyttzyapi.com/api.php/provide/vod', searchPath: '', detailPath: '', enabled: true, priority: 11 },
- { id: 'ruyi', name: '如意资源', baseUrl: 'https://cj.rycjapi.com/api.php/provide/vod', searchPath: '', detailPath: '', enabled: true, priority: 12 },
- { id: 'wangwang', name: '旺旺资源', baseUrl: 'https://wwzy.tv/api.php/provide/vod', searchPath: '', detailPath: '', enabled: true, priority: 13 },
- { id: 'hongniu', name: '红牛资源', baseUrl: 'https://www.hongniuzy2.com/api.php/provide/vod', searchPath: '', detailPath: '', enabled: true, priority: 14 },
- { id: 'guangsu', name: '光速资源', baseUrl: 'https://api.guangsuapi.com/api.php/provide/vod', searchPath: '', detailPath: '', enabled: true, priority: 15 },
- { id: 'ikun', name: 'iKun资源', baseUrl: 'https://ikunzyapi.com/api.php/provide/vod', searchPath: '', detailPath: '', enabled: true, priority: 16 },
- { id: 'youku', name: '优酷资源', baseUrl: 'https://api.ukuapi.com/api.php/provide/vod', searchPath: '', detailPath: '', enabled: true, priority: 17 },
- { id: 'huya', name: '虎牙资源', baseUrl: 'https://www.huyaapi.com/api.php/provide/vod', searchPath: '', detailPath: '', enabled: true, priority: 18 },
- { id: 'xinlang', name: '新浪资源', baseUrl: 'http://api.xinlangapi.com/xinlangapi.php/provide/vod', searchPath: '', detailPath: '', enabled: true, priority: 19 },
- { id: 'lezi', name: '乐子资源', baseUrl: 'https://cj.lziapi.com/api.php/provide/vod', searchPath: '', detailPath: '', enabled: true, priority: 20 },
- { id: 'haihua', name: '海豚资源', baseUrl: 'https://hhzyapi.com/api.php/provide/vod', searchPath: '', detailPath: '', enabled: true, priority: 21 },
- { id: 'jiangyu', name: '鲸鱼资源', baseUrl: 'https://jyzyapi.com/provide/vod', searchPath: '', detailPath: '', enabled: true, priority: 22 },
- { id: 'yilingba', name: '1080资源', baseUrl: 'https://api.1080zyku.com/inc/api_mac10.php', searchPath: '', detailPath: '', enabled: true, priority: 23 },
- { id: 'aidan', name: '爱蛋资源', baseUrl: 'https://lovedan.net/api.php/provide/vod', searchPath: '', detailPath: '', enabled: true, priority: 24 },
- { id: 'leba', name: '乐播资源', baseUrl: 'https://lbapi9.com/api.php/provide/vod', searchPath: '', detailPath: '', enabled: true, priority: 25 },
- { id: 'moduzy', name: '魔都影视', baseUrl: 'https://www.moduzy.com/api.php/provide/vod', searchPath: '', detailPath: '', enabled: true, priority: 26 },
- { id: 'feifanapi', name: '非凡API', baseUrl: 'https://api.ffzyapi.com/api.php/provide/vod', searchPath: '', detailPath: '', enabled: true, priority: 27 },
- { id: 'feifancj', name: '非凡采集', baseUrl: 'http://cj.ffzyapi.com/api.php/provide/vod', searchPath: '', detailPath: '', enabled: true, priority: 28 },
- { id: 'feifancj2', name: '非凡采集HTTPS', baseUrl: 'https://cj.ffzyapi.com/api.php/provide/vod', searchPath: '', detailPath: '', enabled: true, priority: 29 },
- { id: 'feifan1', name: '非凡线路1', baseUrl: 'http://ffzy1.tv/api.php/provide/vod', searchPath: '', detailPath: '', enabled: true, priority: 30 },
- { id: 'wolong2', name: '卧龙采集', baseUrl: 'https://collect.wolongzyw.com/api.php/provide/vod', searchPath: '', detailPath: '', enabled: true, priority: 31 },
- { id: 'baofeng2', name: '暴风APP', baseUrl: 'https://app.bfzyapi.com/api.php/provide/vod', searchPath: '', detailPath: '', enabled: true, priority: 32 },
- { id: 'wujin2', name: '无尽ME', baseUrl: 'https://api.wujinapi.me/api.php/provide/vod', searchPath: '', detailPath: '', enabled: true, priority: 33 },
- { id: 'tianyazy', name: '天涯海角', baseUrl: 'https://tyyszyapi.com/api.php/provide/vod', searchPath: '', detailPath: '', enabled: true, priority: 34 },
- { id: 'guangsu2', name: '光速HTTP', baseUrl: 'http://api.guangsuapi.com/api.php/provide/vod', searchPath: '', detailPath: '', enabled: true, priority: 35 },
- { id: 'xinlang2', name: '新浪HTTPS', baseUrl: 'https://api.xinlangapi.com/xinlangapi.php/provide/vod', searchPath: '', detailPath: '', enabled: true, priority: 36 },
- { id: 'yilingba2', name: '1080JSON', baseUrl: 'https://api.1080zyku.com/inc/apijson.php', searchPath: '', detailPath: '', enabled: true, priority: 37 },
- { id: 'lezi2', name: '乐子HTTP', baseUrl: 'http://cj.lziapi.com/api.php/provide/vod', searchPath: '', detailPath: '', enabled: true, priority: 38 },
- ];
-};
+export const getDefaultSources = (): VideoSource[] => DEFAULT_SOURCES;
export const settingsStore = {
getSettings(): AppSettings {
diff --git a/lib/utils/sorted-insert.ts b/lib/utils/sorted-insert.ts
new file mode 100644
index 0000000..0831625
--- /dev/null
+++ b/lib/utils/sorted-insert.ts
@@ -0,0 +1,50 @@
+/**
+ * Binary insert utility for sorted arrays
+ */
+
+interface Video {
+ relevanceScore?: number;
+ latency?: number;
+}
+
+/**
+ * Insert videos into sorted array using binary search
+ * Sorts by: 1) relevance score (DESC), 2) latency (ASC)
+ */
+export function binaryInsertVideos(existing: T[], newVideos: T[]): T[] {
+ if (existing.length === 0) return newVideos;
+
+ const combined = [...existing];
+
+ for (const video of newVideos) {
+ const relevanceScore = video.relevanceScore || 0;
+ const latency = video.latency || 99999;
+
+ // Find insert position using binary search
+ let left = 0;
+ let right = combined.length;
+
+ while (left < right) {
+ const mid = Math.floor((left + right) / 2);
+ const midRelevance = combined[mid].relevanceScore || 0;
+ const midLatency = combined[mid].latency || 99999;
+
+ if (midRelevance > relevanceScore) {
+ left = mid + 1;
+ } else if (midRelevance < relevanceScore) {
+ right = mid;
+ } else {
+ // Same relevance, compare by latency
+ if (midLatency < latency) {
+ left = mid + 1;
+ } else {
+ right = mid;
+ }
+ }
+ }
+
+ combined.splice(left, 0, video);
+ }
+
+ return combined;
+}