mirror of
https://github.com/KuekHaoYang/KVideo.git
synced 2026-08-14 16:23:43 +08:00
feat: Implement search stream auto-completion timeout, filter irrelevant video results, and enhance search history dropdown animation with new image domains.
This commit is contained in:
@@ -3,7 +3,6 @@
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
background: var(--bg-color);
|
||||
opacity: 0.98;
|
||||
border-radius: var(--radius-2xl);
|
||||
box-shadow: var(--shadow-md);
|
||||
border: 1px solid var(--glass-border);
|
||||
@@ -83,4 +82,17 @@
|
||||
|
||||
.search-history-remove:active {
|
||||
transform: scale(0.95);
|
||||
}
|
||||
|
||||
/* Animations */
|
||||
@keyframes search-dropdown-appear {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px) scale(0.95);
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Video } from '@/lib/types';
|
||||
import { getSourceName } from '@/lib/utils/source-names';
|
||||
import { calculateRelevanceScore } from '@/lib/utils/search';
|
||||
import { calculateRelevanceScore, hasMinimumMatch } from '@/lib/utils/search';
|
||||
import { binaryInsertVideos } from '@/lib/utils/sorted-insert';
|
||||
|
||||
interface StreamHandlerParams {
|
||||
@@ -24,8 +24,27 @@ export async function processSearchStream({
|
||||
}: StreamHandlerParams) {
|
||||
const decoder = new TextDecoder();
|
||||
let buffer = '';
|
||||
let lastProgressTime = Date.now();
|
||||
let timeoutId: NodeJS.Timeout | null = null;
|
||||
let isCompleted = false;
|
||||
|
||||
// Auto-complete if no progress for 3 seconds
|
||||
const resetTimeout = () => {
|
||||
if (timeoutId) clearTimeout(timeoutId);
|
||||
lastProgressTime = Date.now();
|
||||
|
||||
timeoutId = setTimeout(() => {
|
||||
if (!isCompleted) {
|
||||
console.log('Search timeout: No progress for 3 seconds, auto-completing');
|
||||
isCompleted = true;
|
||||
onComplete();
|
||||
}
|
||||
}, 3000);
|
||||
};
|
||||
|
||||
try {
|
||||
resetTimeout(); // Start initial timeout
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
@@ -42,17 +61,24 @@ export async function processSearchStream({
|
||||
|
||||
if (data.type === 'start') {
|
||||
onStart(data.totalSources);
|
||||
resetTimeout();
|
||||
} else if (data.type === 'videos') {
|
||||
const newVideos: Video[] = data.videos.map((video: any) => ({
|
||||
...video,
|
||||
sourceName: video.sourceDisplayName || getSourceName(video.source),
|
||||
isNew: true,
|
||||
relevanceScore: calculateRelevanceScore(video, currentQuery),
|
||||
}));
|
||||
const newVideos: Video[] = data.videos
|
||||
.filter((video: any) => hasMinimumMatch(video.vod_name, currentQuery))
|
||||
.map((video: any) => ({
|
||||
...video,
|
||||
sourceName: video.sourceDisplayName || getSourceName(video.source),
|
||||
isNew: true,
|
||||
relevanceScore: calculateRelevanceScore(video, currentQuery),
|
||||
}));
|
||||
onVideos(newVideos, data.source);
|
||||
resetTimeout();
|
||||
} else if (data.type === 'progress') {
|
||||
onProgress(data.completedSources, data.totalVideosFound);
|
||||
resetTimeout();
|
||||
} else if (data.type === 'complete') {
|
||||
if (timeoutId) clearTimeout(timeoutId);
|
||||
isCompleted = true;
|
||||
onComplete();
|
||||
} else if (data.type === 'error') {
|
||||
onError(data.message);
|
||||
@@ -63,6 +89,9 @@ export async function processSearchStream({
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
if (timeoutId) clearTimeout(timeoutId);
|
||||
throw error;
|
||||
} finally {
|
||||
if (timeoutId) clearTimeout(timeoutId);
|
||||
}
|
||||
}
|
||||
|
||||
+24
-5
@@ -5,6 +5,25 @@
|
||||
|
||||
import type { VideoItem } from '@/lib/types';
|
||||
|
||||
/**
|
||||
* Check if title contains at least 2 consecutive characters from search query
|
||||
* This filters out irrelevant results
|
||||
*/
|
||||
export function hasMinimumMatch(title: string, query: string): boolean {
|
||||
const normalizedTitle = title.toLowerCase();
|
||||
const normalizedQuery = query.toLowerCase().trim();
|
||||
|
||||
// Extract all 2+ character substrings from query
|
||||
for (let i = 0; i <= normalizedQuery.length - 2; i++) {
|
||||
const substring = normalizedQuery.slice(i, i + 2);
|
||||
if (normalizedTitle.includes(substring)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate search relevance score
|
||||
* Higher score = more relevant to the search query
|
||||
@@ -16,7 +35,7 @@ export function calculateRelevanceScore(item: VideoItem, query: string): number
|
||||
|
||||
// Split query into words for partial matching
|
||||
const queryWords = normalizedQuery.split(/\s+/);
|
||||
|
||||
|
||||
// 1. Exact title match (highest priority)
|
||||
if (normalizedTitle === normalizedQuery) {
|
||||
score += 1000;
|
||||
@@ -31,14 +50,14 @@ export function calculateRelevanceScore(item: VideoItem, query: string): number
|
||||
// 3. Title contains full query as substring
|
||||
if (normalizedTitle.includes(normalizedQuery)) {
|
||||
score += 200;
|
||||
|
||||
|
||||
// Bonus for query appearing earlier in title
|
||||
const position = normalizedTitle.indexOf(normalizedQuery);
|
||||
score += Math.max(0, 50 - position * 2);
|
||||
}
|
||||
|
||||
// 4. All query words present in title
|
||||
const allWordsPresent = queryWords.every(word =>
|
||||
const allWordsPresent = queryWords.every(word =>
|
||||
normalizedTitle.includes(word)
|
||||
);
|
||||
if (allWordsPresent && queryWords.length > 1) {
|
||||
@@ -48,10 +67,10 @@ export function calculateRelevanceScore(item: VideoItem, query: string): number
|
||||
// 5. Individual word matches
|
||||
queryWords.forEach(word => {
|
||||
if (word.length < 2) return; // Skip very short words
|
||||
|
||||
|
||||
if (normalizedTitle.includes(word)) {
|
||||
score += 30;
|
||||
|
||||
|
||||
// Bonus if word is at the start
|
||||
if (normalizedTitle.startsWith(word)) {
|
||||
score += 20;
|
||||
|
||||
@@ -94,6 +94,14 @@ const nextConfig: NextConfig = {
|
||||
protocol: 'https',
|
||||
hostname: '**.online',
|
||||
},
|
||||
{
|
||||
protocol: 'http',
|
||||
hostname: '**.top',
|
||||
},
|
||||
{
|
||||
protocol: 'https',
|
||||
hostname: '**.top',
|
||||
},
|
||||
],
|
||||
// Add image optimization for better performance
|
||||
formats: ['image/webp'],
|
||||
|
||||
Reference in New Issue
Block a user