tvbox黄色过滤
This commit is contained in:
@@ -10,6 +10,7 @@ import {
|
|||||||
setCachedMetaInfo,
|
setCachedMetaInfo,
|
||||||
} from '@/lib/openlist-cache';
|
} from '@/lib/openlist-cache';
|
||||||
import { getTMDBImageUrl } from '@/lib/tmdb.search';
|
import { getTMDBImageUrl } from '@/lib/tmdb.search';
|
||||||
|
import { yellowWords } from '@/lib/yellow';
|
||||||
|
|
||||||
export const runtime = 'nodejs';
|
export const runtime = 'nodejs';
|
||||||
|
|
||||||
@@ -22,6 +23,7 @@ export async function GET(request: NextRequest) {
|
|||||||
try {
|
try {
|
||||||
const { searchParams } = new URL(request.url);
|
const { searchParams } = new URL(request.url);
|
||||||
const apiUrl = searchParams.get('api');
|
const apiUrl = searchParams.get('api');
|
||||||
|
const yellowFilter = searchParams.get('yellowFilter') === 'true';
|
||||||
|
|
||||||
if (!apiUrl) {
|
if (!apiUrl) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
@@ -96,7 +98,7 @@ export async function GET(request: NextRequest) {
|
|||||||
console.log('CMS 代理 origin:', origin);
|
console.log('CMS 代理 origin:', origin);
|
||||||
|
|
||||||
// 处理返回数据,替换播放链接为代理链接
|
// 处理返回数据,替换播放链接为代理链接
|
||||||
const processedData = processPlayUrls(data, origin);
|
const processedData = processCmsResponse(data, origin, yellowFilter);
|
||||||
|
|
||||||
return NextResponse.json(processedData, {
|
return NextResponse.json(processedData, {
|
||||||
headers: {
|
headers: {
|
||||||
@@ -130,7 +132,7 @@ export async function GET(request: NextRequest) {
|
|||||||
/**
|
/**
|
||||||
* 处理 CMS API 返回数据,将播放链接替换为代理链接
|
* 处理 CMS API 返回数据,将播放链接替换为代理链接
|
||||||
*/
|
*/
|
||||||
function processPlayUrls(data: any, proxyOrigin: string): any {
|
function processCmsResponse(data: any, proxyOrigin: string, yellowFilter: boolean): any {
|
||||||
if (!data || typeof data !== 'object') {
|
if (!data || typeof data !== 'object') {
|
||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
@@ -138,6 +140,28 @@ function processPlayUrls(data: any, proxyOrigin: string): any {
|
|||||||
// 深拷贝数据,避免修改原始对象
|
// 深拷贝数据,避免修改原始对象
|
||||||
const processedData = JSON.parse(JSON.stringify(data));
|
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
|
// 获取 M3U8 代理 token
|
||||||
const proxyToken = process.env.NEXT_PUBLIC_PROXY_M3U8_TOKEN || '';
|
const proxyToken = process.env.NEXT_PUBLIC_PROXY_M3U8_TOKEN || '';
|
||||||
const tokenParam = proxyToken ? `&token=${encodeURIComponent(proxyToken)}` : '';
|
const tokenParam = proxyToken ? `&token=${encodeURIComponent(proxyToken)}` : '';
|
||||||
@@ -174,6 +198,19 @@ function processPlayUrls(data: any, proxyOrigin: string): any {
|
|||||||
return processedData;
|
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#...
|
* 格式: 第01集$url1#第02集$url2#...
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ export async function GET(request: NextRequest) {
|
|||||||
const token = searchParams.get('token');
|
const token = searchParams.get('token');
|
||||||
const globalToken = process.env.TVBOX_SUBSCRIBE_TOKEN;
|
const globalToken = process.env.TVBOX_SUBSCRIBE_TOKEN;
|
||||||
const adFilter = searchParams.get('adFilter') === 'true'; // 获取去广告参数
|
const adFilter = searchParams.get('adFilter') === 'true'; // 获取去广告参数
|
||||||
|
const yellowFilter = searchParams.get('yellowFilter') === 'true';
|
||||||
|
|
||||||
if (!token) {
|
if (!token) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
@@ -89,7 +90,7 @@ export async function GET(request: NextRequest) {
|
|||||||
baseUrl = `${proto}://${host}`;
|
baseUrl = `${proto}://${host}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('TVBOX 订阅 baseUrl:', baseUrl, 'adFilter:', adFilter);
|
console.log('TVBOX 订阅 baseUrl:', baseUrl, 'adFilter:', adFilter, 'yellowFilter:', yellowFilter);
|
||||||
|
|
||||||
// 检查是否配置了 OpenList
|
// 检查是否配置了 OpenList
|
||||||
const hasOpenList = !!(
|
const hasOpenList = !!(
|
||||||
@@ -142,9 +143,9 @@ export async function GET(request: NextRequest) {
|
|||||||
key: site.key,
|
key: site.key,
|
||||||
name: site.name,
|
name: site.name,
|
||||||
type: 1,
|
type: 1,
|
||||||
// 如果开启去广告,使用 CMS 代理;否则使用原始 API
|
// 开启去广告或黄色过滤时使用 CMS 代理
|
||||||
api: adFilter
|
api: (adFilter || yellowFilter)
|
||||||
? `${baseUrl}/api/cms-proxy?api=${encodeURIComponent(site.api)}`
|
? `${baseUrl}/api/cms-proxy?api=${encodeURIComponent(site.api)}${adFilter ? '&adFilter=true' : ''}${yellowFilter ? '&yellowFilter=true' : ''}`
|
||||||
: site.api,
|
: site.api,
|
||||||
searchable: 1,
|
searchable: 1,
|
||||||
quickSearch: 1,
|
quickSearch: 1,
|
||||||
|
|||||||
+78
-64
@@ -78,12 +78,12 @@ export const UserMenu: React.FC = () => {
|
|||||||
// 订阅相关状态
|
// 订阅相关状态
|
||||||
const [subscribeEnabled, setSubscribeEnabled] = useState(false);
|
const [subscribeEnabled, setSubscribeEnabled] = useState(false);
|
||||||
const [subscribeUrl, setSubscribeUrl] = useState('');
|
const [subscribeUrl, setSubscribeUrl] = useState('');
|
||||||
const [subscribeUrlWithAdFilter, setSubscribeUrlWithAdFilter] = useState('');
|
|
||||||
const [copySuccess, setCopySuccess] = useState(false);
|
const [copySuccess, setCopySuccess] = useState(false);
|
||||||
const [copySuccessAdFilter, setCopySuccessAdFilter] = useState(false);
|
|
||||||
const [tvboxToken, setTvboxToken] = useState('');
|
const [tvboxToken, setTvboxToken] = useState('');
|
||||||
const [isResettingToken, setIsResettingToken] = useState(false);
|
const [isResettingToken, setIsResettingToken] = useState(false);
|
||||||
const [isLoadingSubscribeUrl, setIsLoadingSubscribeUrl] = useState(false);
|
const [isLoadingSubscribeUrl, setIsLoadingSubscribeUrl] = useState(false);
|
||||||
|
const [subscribeAdFilterEnabled, setSubscribeAdFilterEnabled] = useState(false);
|
||||||
|
const [subscribeYellowFilterEnabled, setSubscribeYellowFilterEnabled] = useState(false);
|
||||||
|
|
||||||
// Body 滚动锁定 - 使用 overflow 方式避免布局问题
|
// Body 滚动锁定 - 使用 overflow 方式避免布局问题
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -156,7 +156,7 @@ export const UserMenu: React.FC = () => {
|
|||||||
isOpen: false,
|
isOpen: false,
|
||||||
title: '',
|
title: '',
|
||||||
message: '',
|
message: '',
|
||||||
onConfirm: () => {},
|
onConfirm: () => undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
// 折叠面板状态
|
// 折叠面板状态
|
||||||
@@ -323,13 +323,7 @@ export const UserMenu: React.FC = () => {
|
|||||||
const token = data.token;
|
const token = data.token;
|
||||||
setTvboxToken(token);
|
setTvboxToken(token);
|
||||||
|
|
||||||
// 前端拼接订阅链接
|
setSubscribeUrl(buildSubscribeUrl(token, subscribeAdFilterEnabled, subscribeYellowFilterEnabled));
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('获取订阅URL失败:', error);
|
console.error('获取订阅URL失败:', error);
|
||||||
@@ -359,13 +353,7 @@ export const UserMenu: React.FC = () => {
|
|||||||
const token = data.token;
|
const token = data.token;
|
||||||
setTvboxToken(token);
|
setTvboxToken(token);
|
||||||
|
|
||||||
// 更新订阅链接
|
setSubscribeUrl(buildSubscribeUrl(token, subscribeAdFilterEnabled, subscribeYellowFilterEnabled));
|
||||||
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);
|
|
||||||
|
|
||||||
if (messageEl) {
|
if (messageEl) {
|
||||||
messageEl.textContent = '订阅token已重置!';
|
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(() => {
|
useEffect(() => {
|
||||||
if (typeof window !== 'undefined') {
|
if (typeof window !== 'undefined') {
|
||||||
@@ -819,7 +820,6 @@ export const UserMenu: React.FC = () => {
|
|||||||
const handleCloseSubscribe = () => {
|
const handleCloseSubscribe = () => {
|
||||||
setIsSubscribeOpen(false);
|
setIsSubscribeOpen(false);
|
||||||
setCopySuccess(false);
|
setCopySuccess(false);
|
||||||
setCopySuccessAdFilter(false);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCopySubscribeUrl = async () => {
|
const handleCopySubscribeUrl = async () => {
|
||||||
@@ -833,18 +833,11 @@ export const UserMenu: React.FC = () => {
|
|||||||
console.error('复制失败:', error);
|
console.error('复制失败:', error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCopySubscribeUrlWithAdFilter = async () => {
|
useEffect(() => {
|
||||||
try {
|
if (!tvboxToken || !isSubscribeOpen) return;
|
||||||
await navigator.clipboard.writeText(subscribeUrlWithAdFilter);
|
setSubscribeUrl(buildSubscribeUrl(tvboxToken, subscribeAdFilterEnabled, subscribeYellowFilterEnabled));
|
||||||
setCopySuccessAdFilter(true);
|
}, [tvboxToken, subscribeAdFilterEnabled, subscribeYellowFilterEnabled, isSubscribeOpen]);
|
||||||
setTimeout(() => {
|
|
||||||
setCopySuccessAdFilter(false);
|
|
||||||
}, 2000);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('复制失败:', error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSubmitChangePassword = async () => {
|
const handleSubmitChangePassword = async () => {
|
||||||
setPasswordError('');
|
setPasswordError('');
|
||||||
@@ -2833,18 +2826,18 @@ export const UserMenu: React.FC = () => {
|
|||||||
<div className='space-y-4'>
|
<div className='space-y-4'>
|
||||||
{isLoadingSubscribeUrl ? (
|
{isLoadingSubscribeUrl ? (
|
||||||
<>
|
<>
|
||||||
{/* 加载骨架 - 订阅链接(标准) */}
|
{/* 加载骨架 - 开关 */}
|
||||||
<div>
|
<div>
|
||||||
<div className='h-5 w-32 bg-gray-200 dark:bg-gray-700 rounded mb-2 animate-pulse'></div>
|
<div className='h-5 w-24 bg-gray-200 dark:bg-gray-700 rounded mb-3 animate-pulse'></div>
|
||||||
<div className='flex gap-2'>
|
<div className='space-y-2'>
|
||||||
<div className='flex-1 h-10 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 className='w-20 h-10 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>
|
||||||
|
|
||||||
{/* 加载骨架 - 订阅链接(去广告) */}
|
{/* 加载骨架 - 订阅链接 */}
|
||||||
<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 gap-2'>
|
||||||
<div className='flex-1 h-10 bg-gray-200 dark:bg-gray-700 rounded animate-pulse'></div>
|
<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='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>
|
<div>
|
||||||
<h4 className='text-sm font-medium text-gray-700 dark:text-gray-300 mb-2'>
|
<h4 className='text-sm font-medium text-gray-700 dark:text-gray-300 mb-2'>
|
||||||
订阅链接(标准)
|
订阅链接
|
||||||
</h4>
|
</h4>
|
||||||
<div className='flex gap-2'>
|
<div className='flex gap-2'>
|
||||||
<input
|
<input
|
||||||
@@ -2880,31 +2914,11 @@ export const UserMenu: React.FC = () => {
|
|||||||
{copySuccess ? '已复制' : '复制'}
|
{copySuccess ? '已复制' : '复制'}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{(subscribeAdFilterEnabled || subscribeYellowFilterEnabled) && (
|
||||||
|
<p className='text-xs text-gray-500 dark:text-gray-400 mt-1'>
|
||||||
{/* 订阅链接(去广告) */}
|
💡 代理模式已开启,某些源可能因为区域或兼容问题无法播放
|
||||||
<div>
|
</p>
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 重置Token按钮 */}
|
{/* 重置Token按钮 */}
|
||||||
|
|||||||
Reference in New Issue
Block a user