增加客户端去广告配置

This commit is contained in:
mtvpls
2026-07-17 19:14:01 +08:00
parent bd779b237c
commit 1bbea13c62
5 changed files with 332 additions and 46 deletions
+21
View File
@@ -21,6 +21,7 @@ type Action =
| 'toggle_proxy_mode'
| 'toggle_special_source'
| 'set_special_sources'
| 'set_client_ad_sources'
| 'update_weight'
| 'batch_update_weights';
@@ -62,6 +63,7 @@ export async function POST(request: NextRequest) {
'toggle_proxy_mode',
'toggle_special_source',
'set_special_sources',
'set_client_ad_sources',
'update_weight',
'batch_update_weights',
];
@@ -153,6 +155,9 @@ export async function POST(request: NextRequest) {
adminConfig.SpecialSourceApis = (adminConfig.SpecialSourceApis || []).filter(
(api) => api !== key
);
adminConfig.ClientAdSourceApis = (adminConfig.ClientAdSourceApis || []).filter(
(api) => api !== key
);
// 检查并清理用户组和用户的权限数组
// 清理用户组权限
@@ -236,6 +241,9 @@ export async function POST(request: NextRequest) {
adminConfig.SpecialSourceApis = (adminConfig.SpecialSourceApis || []).filter(
(api) => !keysToDelete.includes(api)
);
adminConfig.ClientAdSourceApis = (adminConfig.ClientAdSourceApis || []).filter(
(api) => !keysToDelete.includes(api)
);
// 检查并清理用户组和用户的权限数组
if (keysToDelete.length > 0) {
@@ -334,6 +342,19 @@ export async function POST(request: NextRequest) {
);
break;
}
case 'set_client_ad_sources': {
const { keys } = body as { keys?: string[] };
if (!Array.isArray(keys)) {
return NextResponse.json({ error: 'keys 参数格式错误' }, { status: 400 });
}
const sourceKeySet = new Set(adminConfig.SourceConfig.map((source) => source.key));
adminConfig.ClientAdSourceApis = Array.from(new Set(keys)).filter((key) =>
sourceKeySet.has(key)
);
break;
}
case 'batch_update_weights': {
const { weights, order } = body as {
weights?: Array<{ key?: string; weight?: number }>;
+96
View File
@@ -70,6 +70,78 @@ import {
export const runtime = 'nodejs';
/**
* 解析站点 origin。
* 优先级:SITE_BASE(站点 url 环境变量)> NEXT_PUBLIC_SITE_URL > 请求头 Host。
*/
function getRequestSiteOrigin(request: NextRequest): string {
const fromEnv =
(process.env.SITE_BASE || '').trim() ||
(process.env.NEXT_PUBLIC_SITE_URL || '').trim();
if (fromEnv) {
return fromEnv.replace(/\/$/, '');
}
let host =
request.headers.get('host') || request.headers.get('x-forwarded-host');
if (host && !/^[a-zA-Z0-9.-]+(:\d+)?$/.test(host)) {
host = null;
}
if (!host) {
try {
host = new URL(request.url).host;
} catch {
host = 'localhost';
}
}
const proto =
request.headers.get('x-forwarded-proto') ||
(host.includes('localhost') || host.includes('127.0.0.1')
? 'http'
: 'https');
return `${proto}://${host}`.replace(/\/$/, '');
}
/**
* MoonTVPlus APP / OrionTV 客户端:对配置的视频源 m3u8 套一层去广告代理。
* UA 小写包含 "moontvplus app" 或 "oriontv" 时生效(不匹配仅含 moontvplus 的其它客户端)。
*/
function applyClientAdProxyToEpisodes(
request: NextRequest,
sourceCode: string,
episodes: string[] | undefined,
clientAdSourceApis: string[] | undefined
): string[] | undefined {
if (!episodes || episodes.length === 0) return episodes;
if (!clientAdSourceApis || !clientAdSourceApis.includes(sourceCode)) {
return episodes;
}
const ua = (request.headers.get('user-agent') || '').toLowerCase();
if (!ua.includes('moontvplus app') && !ua.includes('oriontv')) {
return episodes;
}
const origin = getRequestSiteOrigin(request);
return episodes.map((episode) => {
if (!episode || typeof episode !== 'string') return episode;
if (
episode.includes('/api/proxy-m3u8') ||
episode.includes('/api/proxy/vod/m3u8')
) {
return episode;
}
// 仅处理 http(s) 直链 m3u8,站内相对播放地址不改写
if (!/^https?:\/\//i.test(episode)) return episode;
return `${origin}/api/proxy-m3u8?url=${encodeURIComponent(episode)}`;
});
}
function formatNetdiskEpisodeTitle(
parsed: {
season?: number;
@@ -1215,8 +1287,32 @@ export async function GET(request: NextRequest) {
proxyMode: apiSite.proxyMode || false,
};
// 客户端广告配置:指定源 + APP/OrionTV UA 时 m3u8 套 proxy-m3u8
const adminConfig = await getConfig();
const clientAdEnabled = (adminConfig.ClientAdSourceApis || []).includes(
sourceCode
);
resultWithProxy.episodes =
applyClientAdProxyToEpisodes(
request,
sourceCode,
resultWithProxy.episodes,
adminConfig.ClientAdSourceApis
) || resultWithProxy.episodes;
const cacheTime = await getCacheTime();
// 同一源在不同 UA 下 episodes 可能不同,避免 CDN/共享缓存串号
if (clientAdEnabled) {
return NextResponse.json(resultWithProxy, {
headers: {
'Cache-Control': 'private, no-store',
Vary: 'User-Agent',
'Netlify-Vary': 'query',
},
});
}
return NextResponse.json(resultWithProxy, {
headers: {
'Cache-Control': `public, max-age=${cacheTime}, s-maxage=${cacheTime}`,