From 5d7549152300197a3cd8b217053275366cd54c4d Mon Sep 17 00:00:00 2001 From: mtvpls Date: Tue, 24 Mar 2026 10:57:32 +0800 Subject: [PATCH] =?UTF-8?q?tvbox=E9=BB=84=E8=89=B2=E8=BF=87=E6=BB=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/app/api/cms-proxy/route.ts | 41 +++++++- src/app/api/tvbox/subscribe/route.ts | 9 +- src/components/UserMenu.tsx | 142 +++++++++++++++------------ 3 files changed, 122 insertions(+), 70 deletions(-) diff --git a/src/app/api/cms-proxy/route.ts b/src/app/api/cms-proxy/route.ts index 224df4d..4db9bc8 100644 --- a/src/app/api/cms-proxy/route.ts +++ b/src/app/api/cms-proxy/route.ts @@ -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): boolean { + const normalized = values + .filter(Boolean) + .join(' ') + .toLowerCase(); + + if (!normalized) { + return false; + } + + return yellowWords.some((word) => normalized.includes(word.toLowerCase())); +} + /** * 处理播放地址字符串 * 格式: 第01集$url1#第02集$url2#... diff --git a/src/app/api/tvbox/subscribe/route.ts b/src/app/api/tvbox/subscribe/route.ts index 686ada6..0a3165c 100644 --- a/src/app/api/tvbox/subscribe/route.ts +++ b/src/app/api/tvbox/subscribe/route.ts @@ -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, diff --git a/src/components/UserMenu.tsx b/src/components/UserMenu.tsx index 00a3a1c..26ed7dc 100644 --- a/src/components/UserMenu.tsx +++ b/src/components/UserMenu.tsx @@ -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 = () => {
{isLoadingSubscribeUrl ? ( <> - {/* 加载骨架 - 订阅链接(标准) */} + {/* 加载骨架 - 开关 */}
-
-
-
-
+
+
+
+
- {/* 加载骨架 - 订阅链接(去广告) */} + {/* 加载骨架 - 订阅链接 */}
-
+
@@ -2860,10 +2853,51 @@ export const UserMenu: React.FC = () => { ) : ( <> - {/* 订阅链接(标准) */} +
+

+ 订阅选项 +

+ + + + +
+

- 订阅链接(标准) + 订阅链接

{ {copySuccess ? '已复制' : '复制'}
-
- - {/* 订阅链接(去广告) */} -
-

- 订阅链接(去广告) -

-
- - -
-

- 💡 去广告需要经过服务器代理,某些源可能因为区域或兼容问题无法播放 -

+ {(subscribeAdFilterEnabled || subscribeYellowFilterEnabled) && ( +

+ 💡 代理模式已开启,某些源可能因为区域或兼容问题无法播放 +

+ )}
{/* 重置Token按钮 */}