增加客户端去广告配置
This commit is contained in:
@@ -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 }>;
|
||||
|
||||
@@ -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}`,
|
||||
|
||||
Reference in New Issue
Block a user