tvbox黄色过滤
This commit is contained in:
@@ -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#...
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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按钮 */}
|
||||
|
||||
Reference in New Issue
Block a user