视频源权重
This commit is contained in:
@@ -364,6 +364,7 @@ interface DataSource {
|
|||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
from: 'config' | 'custom';
|
from: 'config' | 'custom';
|
||||||
proxyMode?: boolean;
|
proxyMode?: boolean;
|
||||||
|
weight?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 直播源数据类型
|
// 直播源数据类型
|
||||||
@@ -4500,6 +4501,45 @@ const VideoSourceConfig = ({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleUpdateWeight = (key: string, weight: number) => {
|
||||||
|
// 调用API更新
|
||||||
|
withLoading(`updateWeight_${key}`, async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/admin/source', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
action: 'update_weight',
|
||||||
|
key,
|
||||||
|
weight,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const data = await response.json().catch(() => ({}));
|
||||||
|
throw new Error(data.error || `操作失败: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
await refreshConfig();
|
||||||
|
} catch (error) {
|
||||||
|
// 失败时回滚本地状态到配置中的值
|
||||||
|
const originalWeight = config?.SourceConfig?.find(s => s.key === key)?.weight ?? 0;
|
||||||
|
setSources((prev) =>
|
||||||
|
prev.map((s) =>
|
||||||
|
s.key === key ? { ...s, weight: originalWeight } : s
|
||||||
|
)
|
||||||
|
);
|
||||||
|
showError(
|
||||||
|
error instanceof Error ? error.message : '更新权重失败',
|
||||||
|
showAlert
|
||||||
|
);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}).catch(() => {
|
||||||
|
console.error('操作失败', 'update_weight', key, weight);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const handleAddSource = () => {
|
const handleAddSource = () => {
|
||||||
if (!newSource.name || !newSource.key || !newSource.api) return;
|
if (!newSource.name || !newSource.key || !newSource.api) return;
|
||||||
withLoading('addSource', async () => {
|
withLoading('addSource', async () => {
|
||||||
@@ -4812,6 +4852,36 @@ const VideoSourceConfig = ({
|
|||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
|
<td className='px-6 py-4 whitespace-nowrap'>
|
||||||
|
<input
|
||||||
|
type='number'
|
||||||
|
min='0'
|
||||||
|
max='100'
|
||||||
|
value={source.weight ?? 0}
|
||||||
|
onChange={(e) => {
|
||||||
|
const value = parseInt(e.target.value) || 0;
|
||||||
|
const clampedValue = Math.min(100, Math.max(0, value));
|
||||||
|
// 只更新本地状态,不调用API
|
||||||
|
setSources((prev) =>
|
||||||
|
prev.map((s) =>
|
||||||
|
s.key === source.key ? { ...s, weight: clampedValue } : s
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
onBlur={(e) => {
|
||||||
|
const newValue = parseInt(e.target.value) || 0;
|
||||||
|
const clampedValue = Math.min(100, Math.max(0, newValue));
|
||||||
|
const originalWeight = config?.SourceConfig?.find(s => s.key === source.key)?.weight ?? 0;
|
||||||
|
|
||||||
|
// 只有在值发生变化时才调用API
|
||||||
|
if (clampedValue !== originalWeight) {
|
||||||
|
handleUpdateWeight(source.key, clampedValue);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className='w-16 px-2 py-1 text-sm border border-gray-300 dark:border-gray-600 rounded bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-blue-500 focus:border-transparent'
|
||||||
|
title='权重范围:0-100,用于排序和优选评分'
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
<td className='px-6 py-4 whitespace-nowrap max-w-[1rem]'>
|
<td className='px-6 py-4 whitespace-nowrap max-w-[1rem]'>
|
||||||
{(() => {
|
{(() => {
|
||||||
const status = getValidationStatus(source.key);
|
const status = getValidationStatus(source.key);
|
||||||
@@ -5195,6 +5265,9 @@ const VideoSourceConfig = ({
|
|||||||
<th className='px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider'>
|
<th className='px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider'>
|
||||||
代理模式
|
代理模式
|
||||||
</th>
|
</th>
|
||||||
|
<th className='px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider'>
|
||||||
|
权重
|
||||||
|
</th>
|
||||||
<th className='px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider'>
|
<th className='px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider'>
|
||||||
有效性
|
有效性
|
||||||
</th>
|
</th>
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { db } from '@/lib/db';
|
|||||||
export const runtime = 'nodejs';
|
export const runtime = 'nodejs';
|
||||||
|
|
||||||
// 支持的操作类型
|
// 支持的操作类型
|
||||||
type Action = 'add' | 'disable' | 'enable' | 'delete' | 'sort' | 'batch_disable' | 'batch_enable' | 'batch_delete' | 'toggle_proxy_mode';
|
type Action = 'add' | 'disable' | 'enable' | 'delete' | 'sort' | 'batch_disable' | 'batch_enable' | 'batch_delete' | 'toggle_proxy_mode' | 'update_weight';
|
||||||
|
|
||||||
interface BaseBody {
|
interface BaseBody {
|
||||||
action?: Action;
|
action?: Action;
|
||||||
@@ -37,7 +37,7 @@ export async function POST(request: NextRequest) {
|
|||||||
const username = authInfo.username;
|
const username = authInfo.username;
|
||||||
|
|
||||||
// 基础校验
|
// 基础校验
|
||||||
const ACTIONS: Action[] = ['add', 'disable', 'enable', 'delete', 'sort', 'batch_disable', 'batch_enable', 'batch_delete', 'toggle_proxy_mode'];
|
const ACTIONS: Action[] = ['add', 'disable', 'enable', 'delete', 'sort', 'batch_disable', 'batch_enable', 'batch_delete', 'toggle_proxy_mode', 'update_weight'];
|
||||||
if (!username || !action || !ACTIONS.includes(action)) {
|
if (!username || !action || !ACTIONS.includes(action)) {
|
||||||
return NextResponse.json({ error: '参数格式错误' }, { status: 400 });
|
return NextResponse.json({ error: '参数格式错误' }, { status: 400 });
|
||||||
}
|
}
|
||||||
@@ -254,6 +254,20 @@ export async function POST(request: NextRequest) {
|
|||||||
entry.proxyMode = !entry.proxyMode;
|
entry.proxyMode = !entry.proxyMode;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case 'update_weight': {
|
||||||
|
const { key, weight } = body as { key?: string; weight?: number };
|
||||||
|
if (!key)
|
||||||
|
return NextResponse.json({ error: '缺少 key 参数' }, { status: 400 });
|
||||||
|
if (weight === undefined || weight === null)
|
||||||
|
return NextResponse.json({ error: '缺少 weight 参数' }, { status: 400 });
|
||||||
|
if (typeof weight !== 'number' || weight < 0 || weight > 100)
|
||||||
|
return NextResponse.json({ error: '权重必须是 0-100 之间的数字' }, { status: 400 });
|
||||||
|
const entry = adminConfig.SourceConfig.find((s) => s.key === key);
|
||||||
|
if (!entry)
|
||||||
|
return NextResponse.json({ error: '源不存在' }, { status: 404 });
|
||||||
|
entry.weight = weight;
|
||||||
|
break;
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
return NextResponse.json({ error: '未知操作' }, { status: 400 });
|
return NextResponse.json({ error: '未知操作' }, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,6 +36,12 @@ export async function GET(request: NextRequest) {
|
|||||||
const config = await getConfig();
|
const config = await getConfig();
|
||||||
const apiSites = await getAvailableApiSites(authInfo.username);
|
const apiSites = await getAvailableApiSites(authInfo.username);
|
||||||
|
|
||||||
|
// 创建权重映射表
|
||||||
|
const weightMap = new Map<string, number>();
|
||||||
|
config.SourceConfig.forEach(source => {
|
||||||
|
weightMap.set(source.key, source.weight ?? 0);
|
||||||
|
});
|
||||||
|
|
||||||
// 检查是否配置了 OpenList
|
// 检查是否配置了 OpenList
|
||||||
const hasOpenList = !!(
|
const hasOpenList = !!(
|
||||||
config.OpenListConfig?.Enabled &&
|
config.OpenListConfig?.Enabled &&
|
||||||
@@ -191,6 +197,14 @@ export async function GET(request: NextRequest) {
|
|||||||
return !yellowWords.some((word: string) => typeName.includes(word));
|
return !yellowWords.some((word: string) => typeName.includes(word));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 按权重降序排序
|
||||||
|
flattenedResults.sort((a, b) => {
|
||||||
|
const weightA = weightMap.get(a.source) ?? 0;
|
||||||
|
const weightB = weightMap.get(b.source) ?? 0;
|
||||||
|
return weightB - weightA;
|
||||||
|
});
|
||||||
|
|
||||||
const cacheTime = await getCacheTime();
|
const cacheTime = await getCacheTime();
|
||||||
|
|
||||||
if (flattenedResults.length === 0) {
|
if (flattenedResults.length === 0) {
|
||||||
|
|||||||
@@ -33,6 +33,19 @@ export async function GET(request: NextRequest) {
|
|||||||
const config = await getConfig();
|
const config = await getConfig();
|
||||||
const apiSites = await getAvailableApiSites(authInfo.username);
|
const apiSites = await getAvailableApiSites(authInfo.username);
|
||||||
|
|
||||||
|
// 创建权重映射表
|
||||||
|
const weightMap = new Map<string, number>();
|
||||||
|
config.SourceConfig.forEach(source => {
|
||||||
|
weightMap.set(source.key, source.weight ?? 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 按权重降序排序 apiSites
|
||||||
|
const sortedApiSites = [...apiSites].sort((a, b) => {
|
||||||
|
const weightA = weightMap.get(a.key) ?? 0;
|
||||||
|
const weightB = weightMap.get(b.key) ?? 0;
|
||||||
|
return weightB - weightA;
|
||||||
|
});
|
||||||
|
|
||||||
// 检查是否配置了 OpenList
|
// 检查是否配置了 OpenList
|
||||||
const hasOpenList = !!(
|
const hasOpenList = !!(
|
||||||
config.OpenListConfig?.Enabled &&
|
config.OpenListConfig?.Enabled &&
|
||||||
@@ -89,7 +102,7 @@ export async function GET(request: NextRequest) {
|
|||||||
const startEvent = `data: ${JSON.stringify({
|
const startEvent = `data: ${JSON.stringify({
|
||||||
type: 'start',
|
type: 'start',
|
||||||
query,
|
query,
|
||||||
totalSources: apiSites.length + (hasOpenList ? 1 : 0) + embySourcesCount,
|
totalSources: sortedApiSites.length + (hasOpenList ? 1 : 0) + embySourcesCount,
|
||||||
timestamp: Date.now()
|
timestamp: Date.now()
|
||||||
})}\n\n`;
|
})}\n\n`;
|
||||||
|
|
||||||
@@ -294,7 +307,7 @@ export async function GET(request: NextRequest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 为每个源创建搜索 Promise
|
// 为每个源创建搜索 Promise
|
||||||
const searchPromises = apiSites.map(async (site) => {
|
const searchPromises = sortedApiSites.map(async (site) => {
|
||||||
try {
|
try {
|
||||||
// 添加超时控制
|
// 添加超时控制
|
||||||
const searchPromise = Promise.race([
|
const searchPromise = Promise.race([
|
||||||
@@ -363,7 +376,7 @@ export async function GET(request: NextRequest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 检查是否所有源都已完成
|
// 检查是否所有源都已完成
|
||||||
if (completedSources === apiSites.length + (hasOpenList ? 1 : 0) + embySourcesCount) {
|
if (completedSources === sortedApiSites.length + (hasOpenList ? 1 : 0) + embySourcesCount) {
|
||||||
if (!streamClosed) {
|
if (!streamClosed) {
|
||||||
// 发送最终完成事件
|
// 发送最终完成事件
|
||||||
const completeEvent = `data: ${JSON.stringify({
|
const completeEvent = `data: ${JSON.stringify({
|
||||||
|
|||||||
+31
-4
@@ -1322,6 +1322,22 @@ function PlayPageClient() {
|
|||||||
): Promise<SearchResult> => {
|
): Promise<SearchResult> => {
|
||||||
if (sources.length === 1) return sources[0];
|
if (sources.length === 1) return sources[0];
|
||||||
|
|
||||||
|
// 获取配置以获取权重信息
|
||||||
|
let weightMap = new Map<string, number>();
|
||||||
|
try {
|
||||||
|
const configResponse = await fetch('/api/admin/config');
|
||||||
|
if (configResponse.ok) {
|
||||||
|
const configData = await configResponse.json();
|
||||||
|
if (configData.Config?.SourceConfig) {
|
||||||
|
configData.Config.SourceConfig.forEach((source: any) => {
|
||||||
|
weightMap.set(source.key, source.weight ?? 0);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('获取配置失败,权重将使用默认值0:', error);
|
||||||
|
}
|
||||||
|
|
||||||
// 将播放源均分为两批,并发测速各批,避免一次性过多请求
|
// 将播放源均分为两批,并发测速各批,避免一次性过多请求
|
||||||
const batchSize = Math.ceil(sources.length / 2);
|
const batchSize = Math.ceil(sources.length / 2);
|
||||||
const allResults: Array<{
|
const allResults: Array<{
|
||||||
@@ -1388,9 +1404,15 @@ function PlayPageClient() {
|
|||||||
|
|
||||||
setPrecomputedVideoInfo(newVideoInfoMap);
|
setPrecomputedVideoInfo(newVideoInfoMap);
|
||||||
|
|
||||||
|
// 如果所有测速都失败,仍然按权重排序返回
|
||||||
if (successfulResults.length === 0) {
|
if (successfulResults.length === 0) {
|
||||||
console.warn('所有播放源测速都失败,使用第一个播放源');
|
console.warn('所有播放源测速都失败,按权重排序');
|
||||||
return sources[0];
|
const sortedByWeight = [...sources].sort((a, b) => {
|
||||||
|
const weightA = weightMap.get(a.source) ?? 0;
|
||||||
|
const weightB = weightMap.get(b.source) ?? 0;
|
||||||
|
return weightB - weightA;
|
||||||
|
});
|
||||||
|
return sortedByWeight[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
// 找出所有有效速度的最大值,用于线性映射
|
// 找出所有有效速度的最大值,用于线性映射
|
||||||
@@ -1425,7 +1447,8 @@ function PlayPageClient() {
|
|||||||
result.testResult,
|
result.testResult,
|
||||||
maxSpeed,
|
maxSpeed,
|
||||||
minPing,
|
minPing,
|
||||||
maxPing
|
maxPing,
|
||||||
|
weightMap.get(result.source.source) ?? 0
|
||||||
),
|
),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -1455,7 +1478,8 @@ function PlayPageClient() {
|
|||||||
},
|
},
|
||||||
maxSpeed: number,
|
maxSpeed: number,
|
||||||
minPing: number,
|
minPing: number,
|
||||||
maxPing: number
|
maxPing: number,
|
||||||
|
weight: number = 0
|
||||||
): number => {
|
): number => {
|
||||||
let score = 0;
|
let score = 0;
|
||||||
|
|
||||||
@@ -1513,6 +1537,9 @@ function PlayPageClient() {
|
|||||||
})();
|
})();
|
||||||
score += pingScore * 0.2;
|
score += pingScore * 0.2;
|
||||||
|
|
||||||
|
// 权重加分 - 直接加到总分上(0-100分)
|
||||||
|
score += weight;
|
||||||
|
|
||||||
return Math.round(score * 100) / 100; // 保留两位小数
|
return Math.round(score * 100) / 100; // 保留两位小数
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ export interface AdminConfig {
|
|||||||
from: 'config' | 'custom';
|
from: 'config' | 'custom';
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
proxyMode?: boolean; // 代理模式开关:启用后由服务器代理m3u8和ts分片
|
proxyMode?: boolean; // 代理模式开关:启用后由服务器代理m3u8和ts分片
|
||||||
|
weight?: number; // 权重:用于排序和优选评分,默认0,范围0-100
|
||||||
}[];
|
}[];
|
||||||
CustomCategories: {
|
CustomCategories: {
|
||||||
name?: string;
|
name?: string;
|
||||||
|
|||||||
Reference in New Issue
Block a user