tvbox黄色过滤

This commit is contained in:
mtvpls
2026-03-24 10:57:32 +08:00
parent 4437a4c461
commit 5d75491523
3 changed files with 122 additions and 70 deletions

View File

@@ -10,6 +10,7 @@ import {
setCachedMetaInfo,
} from '@/lib/openlist-cache';
import { getTMDBImageUrl } from '@/lib/tmdb.search';
import { yellowWords } from '@/lib/yellow';
export const runtime = 'nodejs';
@@ -22,6 +23,7 @@ export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url);
const apiUrl = searchParams.get('api');
const yellowFilter = searchParams.get('yellowFilter') === 'true';
if (!apiUrl) {
return NextResponse.json(
@@ -96,7 +98,7 @@ export async function GET(request: NextRequest) {
console.log('CMS 代理 origin:', origin);
// 处理返回数据,替换播放链接为代理链接
const processedData = processPlayUrls(data, origin);
const processedData = processCmsResponse(data, origin, yellowFilter);
return NextResponse.json(processedData, {
headers: {
@@ -130,7 +132,7 @@ export async function GET(request: NextRequest) {
/**
* 处理 CMS API 返回数据,将播放链接替换为代理链接
*/
function processPlayUrls(data: any, proxyOrigin: string): any {
function processCmsResponse(data: any, proxyOrigin: string, yellowFilter: boolean): any {
if (!data || typeof data !== 'object') {
return data;
}
@@ -138,6 +140,28 @@ function processPlayUrls(data: any, proxyOrigin: string): any {
// 深拷贝数据,避免修改原始对象
const processedData = JSON.parse(JSON.stringify(data));
if (yellowFilter) {
if (processedData.class && Array.isArray(processedData.class)) {
processedData.class = processedData.class.filter((item: any) => !matchesYellowContent(item?.type_name));
}
if (processedData.list && Array.isArray(processedData.list)) {
processedData.list = processedData.list.filter((item: any) => !matchesYellowContent(
item?.vod_name,
item?.type_name,
item?.vod_remarks,
item?.vod_content,
));
if (typeof processedData.total === 'number') {
processedData.total = processedData.list.length;
}
if (typeof processedData.limit === 'number') {
processedData.limit = processedData.list.length;
}
}
}
// 获取 M3U8 代理 token
const proxyToken = process.env.NEXT_PUBLIC_PROXY_M3U8_TOKEN || '';
const tokenParam = proxyToken ? `&token=${encodeURIComponent(proxyToken)}` : '';
@@ -174,6 +198,19 @@ function processPlayUrls(data: any, proxyOrigin: string): any {
return processedData;
}
function matchesYellowContent(...values: Array<string | undefined>): boolean {
const normalized = values
.filter(Boolean)
.join(' ')
.toLowerCase();
if (!normalized) {
return false;
}
return yellowWords.some((word) => normalized.includes(word.toLowerCase()));
}
/**
* 处理播放地址字符串
* 格式: 第01集$url1#第02集$url2#...

View File

@@ -28,6 +28,7 @@ export async function GET(request: NextRequest) {
const token = searchParams.get('token');
const globalToken = process.env.TVBOX_SUBSCRIBE_TOKEN;
const adFilter = searchParams.get('adFilter') === 'true'; // 获取去广告参数
const yellowFilter = searchParams.get('yellowFilter') === 'true';
if (!token) {
return NextResponse.json(
@@ -89,7 +90,7 @@ export async function GET(request: NextRequest) {
baseUrl = `${proto}://${host}`;
}
console.log('TVBOX 订阅 baseUrl:', baseUrl, 'adFilter:', adFilter);
console.log('TVBOX 订阅 baseUrl:', baseUrl, 'adFilter:', adFilter, 'yellowFilter:', yellowFilter);
// 检查是否配置了 OpenList
const hasOpenList = !!(
@@ -142,9 +143,9 @@ export async function GET(request: NextRequest) {
key: site.key,
name: site.name,
type: 1,
// 如果开启去广告使用 CMS 代理;否则使用原始 API
api: adFilter
? `${baseUrl}/api/cms-proxy?api=${encodeURIComponent(site.api)}`
// 开启去广告或黄色过滤时使用 CMS 代理
api: (adFilter || yellowFilter)
? `${baseUrl}/api/cms-proxy?api=${encodeURIComponent(site.api)}${adFilter ? '&adFilter=true' : ''}${yellowFilter ? '&yellowFilter=true' : ''}`
: site.api,
searchable: 1,
quickSearch: 1,

View File

@@ -78,12 +78,12 @@ export const UserMenu: React.FC = () => {
// 订阅相关状态
const [subscribeEnabled, setSubscribeEnabled] = useState(false);
const [subscribeUrl, setSubscribeUrl] = useState('');
const [subscribeUrlWithAdFilter, setSubscribeUrlWithAdFilter] = useState('');
const [copySuccess, setCopySuccess] = useState(false);
const [copySuccessAdFilter, setCopySuccessAdFilter] = useState(false);
const [tvboxToken, setTvboxToken] = useState('');
const [isResettingToken, setIsResettingToken] = useState(false);
const [isLoadingSubscribeUrl, setIsLoadingSubscribeUrl] = useState(false);
const [subscribeAdFilterEnabled, setSubscribeAdFilterEnabled] = useState(false);
const [subscribeYellowFilterEnabled, setSubscribeYellowFilterEnabled] = useState(false);
// Body 滚动锁定 - 使用 overflow 方式避免布局问题
useEffect(() => {
@@ -156,7 +156,7 @@ export const UserMenu: React.FC = () => {
isOpen: false,
title: '',
message: '',
onConfirm: () => {},
onConfirm: () => undefined,
});
// 折叠面板状态
@@ -323,13 +323,7 @@ export const UserMenu: React.FC = () => {
const token = data.token;
setTvboxToken(token);
// 前端拼接订阅链接
const currentOrigin = window.location.origin;
const standardUrl = `${currentOrigin}/api/tvbox/subscribe?token=${token}`;
const adFilterUrl = `${currentOrigin}/api/tvbox/subscribe?token=${token}&adFilter=true`;
setSubscribeUrl(standardUrl);
setSubscribeUrlWithAdFilter(adFilterUrl);
setSubscribeUrl(buildSubscribeUrl(token, subscribeAdFilterEnabled, subscribeYellowFilterEnabled));
}
} catch (error) {
console.error('获取订阅URL失败:', error);
@@ -359,13 +353,7 @@ export const UserMenu: React.FC = () => {
const token = data.token;
setTvboxToken(token);
// 更新订阅链接
const currentOrigin = window.location.origin;
const standardUrl = `${currentOrigin}/api/tvbox/subscribe?token=${token}`;
const adFilterUrl = `${currentOrigin}/api/tvbox/subscribe?token=${token}&adFilter=true`;
setSubscribeUrl(standardUrl);
setSubscribeUrlWithAdFilter(adFilterUrl);
setSubscribeUrl(buildSubscribeUrl(token, subscribeAdFilterEnabled, subscribeYellowFilterEnabled));
if (messageEl) {
messageEl.textContent = '订阅token已重置';
@@ -398,6 +386,19 @@ export const UserMenu: React.FC = () => {
});
};
const buildSubscribeUrl = (token: string, adFilter: boolean, yellowFilter: boolean) => {
const currentOrigin = window.location.origin;
const url = new URL('/api/tvbox/subscribe', currentOrigin);
url.searchParams.set('token', token);
if (adFilter) {
url.searchParams.set('adFilter', 'true');
}
if (yellowFilter) {
url.searchParams.set('yellowFilter', 'true');
}
return url.toString();
};
// 获取认证信息和存储类型
useEffect(() => {
if (typeof window !== 'undefined') {
@@ -819,7 +820,6 @@ export const UserMenu: React.FC = () => {
const handleCloseSubscribe = () => {
setIsSubscribeOpen(false);
setCopySuccess(false);
setCopySuccessAdFilter(false);
};
const handleCopySubscribeUrl = async () => {
@@ -833,18 +833,11 @@ export const UserMenu: React.FC = () => {
console.error('复制失败:', error);
}
};
const handleCopySubscribeUrlWithAdFilter = async () => {
try {
await navigator.clipboard.writeText(subscribeUrlWithAdFilter);
setCopySuccessAdFilter(true);
setTimeout(() => {
setCopySuccessAdFilter(false);
}, 2000);
} catch (error) {
console.error('复制失败:', error);
}
};
useEffect(() => {
if (!tvboxToken || !isSubscribeOpen) return;
setSubscribeUrl(buildSubscribeUrl(tvboxToken, subscribeAdFilterEnabled, subscribeYellowFilterEnabled));
}, [tvboxToken, subscribeAdFilterEnabled, subscribeYellowFilterEnabled, isSubscribeOpen]);
const handleSubmitChangePassword = async () => {
setPasswordError('');
@@ -2833,18 +2826,18 @@ export const UserMenu: React.FC = () => {
<div className='space-y-4'>
{isLoadingSubscribeUrl ? (
<>
{/* 加载骨架 - 订阅链接(标准) */}
{/* 加载骨架 - 开关 */}
<div>
<div className='h-5 w-32 bg-gray-200 dark:bg-gray-700 rounded mb-2 animate-pulse'></div>
<div className='flex gap-2'>
<div className='flex-1 h-10 bg-gray-200 dark:bg-gray-700 rounded animate-pulse'></div>
<div className='w-20 h-10 bg-gray-200 dark:bg-gray-700 rounded animate-pulse'></div>
<div className='h-5 w-24 bg-gray-200 dark:bg-gray-700 rounded mb-3 animate-pulse'></div>
<div className='space-y-2'>
<div className='h-14 bg-gray-200 dark:bg-gray-700 rounded animate-pulse'></div>
<div className='h-14 bg-gray-200 dark:bg-gray-700 rounded animate-pulse'></div>
</div>
</div>
{/* 加载骨架 - 订阅链接(去广告) */}
{/* 加载骨架 - 订阅链接 */}
<div>
<div className='h-5 w-36 bg-gray-200 dark:bg-gray-700 rounded mb-2 animate-pulse'></div>
<div className='h-5 w-28 bg-gray-200 dark:bg-gray-700 rounded mb-2 animate-pulse'></div>
<div className='flex gap-2'>
<div className='flex-1 h-10 bg-gray-200 dark:bg-gray-700 rounded animate-pulse'></div>
<div className='w-20 h-10 bg-gray-200 dark:bg-gray-700 rounded animate-pulse'></div>
@@ -2860,10 +2853,51 @@ export const UserMenu: React.FC = () => {
</>
) : (
<>
{/* 订阅链接(标准) */}
<div className='space-y-3'>
<h4 className='text-sm font-medium text-gray-700 dark:text-gray-300'>
</h4>
<button
type='button'
onClick={() => setSubscribeAdFilterEnabled((prev) => !prev)}
className='w-full flex items-center justify-between rounded-lg border border-gray-200 dark:border-gray-700 px-4 py-3 text-left bg-gray-50 dark:bg-gray-800/70'
>
<div>
<div className='text-sm font-medium text-gray-800 dark:text-gray-200'>
广
</div>
<div className='text-xs text-gray-500 dark:text-gray-400 mt-1'>
</div>
</div>
<div className={`relative h-6 w-11 rounded-full transition-colors ${subscribeAdFilterEnabled ? 'bg-green-500' : 'bg-gray-300 dark:bg-gray-600'}`}>
<div className={`absolute top-0.5 h-5 w-5 rounded-full bg-white transition-transform ${subscribeAdFilterEnabled ? 'translate-x-5' : 'translate-x-0.5'}`} />
</div>
</button>
<button
type='button'
onClick={() => setSubscribeYellowFilterEnabled((prev) => !prev)}
className='w-full flex items-center justify-between rounded-lg border border-gray-200 dark:border-gray-700 px-4 py-3 text-left bg-gray-50 dark:bg-gray-800/70'
>
<div>
<div className='text-sm font-medium text-gray-800 dark:text-gray-200'>
</div>
<div className='text-xs text-gray-500 dark:text-gray-400 mt-1'>
</div>
</div>
<div className={`relative h-6 w-11 rounded-full transition-colors ${subscribeYellowFilterEnabled ? 'bg-yellow-500' : 'bg-gray-300 dark:bg-gray-600'}`}>
<div className={`absolute top-0.5 h-5 w-5 rounded-full bg-white transition-transform ${subscribeYellowFilterEnabled ? 'translate-x-5' : 'translate-x-0.5'}`} />
</div>
</button>
</div>
<div>
<h4 className='text-sm font-medium text-gray-700 dark:text-gray-300 mb-2'>
</h4>
<div className='flex gap-2'>
<input
@@ -2880,31 +2914,11 @@ export const UserMenu: React.FC = () => {
{copySuccess ? '已复制' : '复制'}
</button>
</div>
</div>
{/* 订阅链接(去广告) */}
<div>
<h4 className='text-sm font-medium text-gray-700 dark:text-gray-300 mb-2'>
广
</h4>
<div className='flex gap-2'>
<input
type='text'
className='flex-1 px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md text-sm bg-gray-50 dark:bg-gray-800 text-gray-900 dark:text-gray-100'
value={subscribeUrlWithAdFilter}
readOnly
/>
<button
onClick={handleCopySubscribeUrlWithAdFilter}
className='px-4 py-2 bg-green-600 hover:bg-green-700 dark:bg-green-700 dark:hover:bg-green-600 text-white text-sm font-medium rounded-md transition-colors flex items-center gap-2 whitespace-nowrap'
>
<Copy className='w-4 h-4' />
{copySuccessAdFilter ? '已复制' : '复制'}
</button>
</div>
<p className='text-xs text-gray-500 dark:text-gray-400 mt-1'>
💡 广
</p>
{(subscribeAdFilterEnabled || subscribeYellowFilterEnabled) && (
<p className='text-xs text-gray-500 dark:text-gray-400 mt-1'>
💡
</p>
)}
</div>
{/* 重置Token按钮 */}