feat: add yellow filter

This commit is contained in:
shinya
2025-08-01 13:17:19 +08:00
parent 52c379cff5
commit 25c699a819
12 changed files with 148 additions and 25 deletions

View File

@@ -207,20 +207,21 @@ networks:
## 环境变量
| 变量 | 说明 | 可选值 | 默认值 |
| --------------------------- | -------------------------------------------- | -------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| USERNAME | 非 localstorage 部署时的管理员账号 | 任意字符串 | (空) |
| PASSWORD | 非 localstorage 部署时为管理员密码 | 任意字符串 | (空) |
| SITE_NAME | 站点名称 | 任意字符串 | MoonTV |
| ANNOUNCEMENT | 站点公告 | 任意字符串 | 本网站仅提供影视信息搜索服务,所有内容均来自第三方网站。本站不存储任何视频资源,不对任何内容的准确性、合法性、完整性负责。 |
| NEXT_PUBLIC_STORAGE_TYPE | 播放记录/收藏的存储方式 | localstorage、redis、d1、upstash | localstorage |
| REDIS_URL | redis 连接 url | 连接 url | 空 |
| UPSTASH_URL | upstash redis 连接 url | 连接 url | 空 |
| UPSTASH_TOKEN | upstash redis 连接 token | 连接 token | 空 |
| NEXT_PUBLIC_ENABLE_REGISTER | 是否开放注册,仅在非 localstorage 部署时生效 | true / false | false |
| NEXT_PUBLIC_SEARCH_MAX_PAGE | 搜索接口可拉取的最大页数 | 1-50 | 5 |
| NEXT_PUBLIC_IMAGE_PROXY | 默认的浏览器端图片代理 | url prefix | (空) |
| NEXT_PUBLIC_DOUBAN_PROXY | 默认的浏览器端豆瓣数据代理 | url prefix | (空) |
| 变量 | 说明 | 可选值 | 默认值 |
| --------------------------------- | -------------------------------------------- | -------------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| USERNAME | 非 localstorage 部署时的管理员账号 | 任意字符串 | (空) |
| PASSWORD | 非 localstorage 部署时为管理员密码 | 任意字符串 | (空) |
| SITE_NAME | 站点名称 | 任意字符串 | MoonTV |
| ANNOUNCEMENT | 站点公告 | 任意字符串 | 本网站仅提供影视信息搜索服务,所有内容均来自第三方网站。本站不存储任何视频资源,不对任何内容的准确性、合法性、完整性负责。 |
| NEXT_PUBLIC_STORAGE_TYPE | 播放记录/收藏的存储方式 | localstorage、redis、d1、upstash | localstorage |
| REDIS_URL | redis 连接 url | 连接 url | 空 |
| UPSTASH_URL | upstash redis 连接 url | 连接 url | 空 |
| UPSTASH_TOKEN | upstash redis 连接 token | 连接 token | 空 |
| NEXT_PUBLIC_ENABLE_REGISTER | 是否开放注册,仅在非 localstorage 部署时生效 | true / false | false |
| NEXT_PUBLIC_SEARCH_MAX_PAGE | 搜索接口可拉取的最大页数 | 1-50 | 5 |
| NEXT_PUBLIC_IMAGE_PROXY | 默认的浏览器端图片代理 | url prefix | (空) |
| NEXT_PUBLIC_DOUBAN_PROXY | 默认的浏览器端豆瓣数据代理 | url prefix | (空) |
| NEXT_PUBLIC_DISABLE_YELLOW_FILTER | 关闭色情内容过滤 | true/false | false |
## 配置说明

View File

@@ -1 +1 @@
20250801003950
20250801131720

View File

@@ -59,6 +59,7 @@ interface SiteConfig {
SiteInterfaceCacheTime: number;
ImageProxy: string;
DoubanProxy: string;
DisableYellowFilter: boolean;
}
// 视频源数据类型
@@ -1356,6 +1357,7 @@ const SiteConfigComponent = ({ config }: { config: AdminConfig | null }) => {
SiteInterfaceCacheTime: 7200,
ImageProxy: '',
DoubanProxy: '',
DisableYellowFilter: false,
});
// 保存状态
const [saving, setSaving] = useState(false);
@@ -1374,6 +1376,7 @@ const SiteConfigComponent = ({ config }: { config: AdminConfig | null }) => {
...config.SiteConfig,
ImageProxy: config.SiteConfig.ImageProxy || '',
DoubanProxy: config.SiteConfig.DoubanProxy || '',
DisableYellowFilter: config.SiteConfig.DisableYellowFilter || false,
});
}
}, [config]);
@@ -1610,6 +1613,59 @@ const SiteConfigComponent = ({ config }: { config: AdminConfig | null }) => {
</p>
</div>
{/* 禁用黄色过滤器 */}
<div>
<label
className={`block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2 ${
isD1Storage || isUpstashStorage ? 'opacity-50' : ''
}`}
>
{isD1Storage && (
<span className='ml-2 text-xs text-gray-500 dark:text-gray-400'>
(D1 )
</span>
)}
{isUpstashStorage && (
<span className='ml-2 text-xs text-gray-500 dark:text-gray-400'>
(Upstash )
</span>
)}
</label>
<div className='flex items-center'>
<input
type='checkbox'
id='disableYellowFilter'
checked={siteSettings.DisableYellowFilter}
onChange={(e) =>
!isD1Storage &&
!isUpstashStorage &&
setSiteSettings((prev) => ({
...prev,
DisableYellowFilter: e.target.checked,
}))
}
disabled={isD1Storage || isUpstashStorage}
className={`w-4 h-4 text-green-600 bg-gray-100 border-gray-300 rounded focus:ring-green-500 dark:focus:ring-green-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600 ${
isD1Storage || isUpstashStorage
? 'opacity-50 cursor-not-allowed'
: ''
}`}
/>
<label
htmlFor='disableYellowFilter'
className={`ml-2 text-sm text-gray-700 dark:text-gray-300 ${
isD1Storage || isUpstashStorage ? 'opacity-50' : ''
}`}
>
</label>
</div>
<p className='mt-1 text-xs text-gray-500 dark:text-gray-400'>
</p>
</div>
{/* 操作按钮 */}
<div className='flex justify-end'>
<button

View File

@@ -35,6 +35,7 @@ export async function POST(request: NextRequest) {
SiteInterfaceCacheTime,
ImageProxy,
DoubanProxy,
DisableYellowFilter,
} = body as {
SiteName: string;
Announcement: string;
@@ -42,6 +43,7 @@ export async function POST(request: NextRequest) {
SiteInterfaceCacheTime: number;
ImageProxy: string;
DoubanProxy: string;
DisableYellowFilter: boolean;
};
// 参数校验
@@ -51,7 +53,8 @@ export async function POST(request: NextRequest) {
typeof SearchDownstreamMaxPage !== 'number' ||
typeof SiteInterfaceCacheTime !== 'number' ||
typeof ImageProxy !== 'string' ||
typeof DoubanProxy !== 'string'
typeof DoubanProxy !== 'string' ||
typeof DisableYellowFilter !== 'boolean'
) {
return NextResponse.json({ error: '参数格式错误' }, { status: 400 });
}
@@ -78,6 +81,7 @@ export async function POST(request: NextRequest) {
SiteInterfaceCacheTime,
ImageProxy,
DoubanProxy,
DisableYellowFilter,
};
// 写入数据库

View File

@@ -1,7 +1,8 @@
import { NextResponse } from 'next/server';
import { getAvailableApiSites, getCacheTime } from '@/lib/config';
import { getCacheTime, getConfig } from '@/lib/config';
import { searchFromApi } from '@/lib/downstream';
import { yellowWords } from '@/lib/yellow';
export const runtime = 'edge';
@@ -25,7 +26,8 @@ export async function GET(request: Request) {
);
}
const apiSites = await getAvailableApiSites();
const config = await getConfig();
const apiSites = config.SourceConfig.filter((site) => !site.disabled);
try {
// 根据 resourceId 查找对应的 API 站点
@@ -41,7 +43,13 @@ export async function GET(request: Request) {
}
const results = await searchFromApi(targetSite, query);
const result = results.filter((r) => r.title === query);
let result = results.filter((r) => r.title === query);
if (!config.SiteConfig.DisableYellowFilter) {
result = result.filter((result) => {
const typeName = result.type_name || '';
return !yellowWords.some((word: string) => typeName.includes(word));
});
}
const cacheTime = await getCacheTime();
if (result.length === 0) {

View File

@@ -1,7 +1,8 @@
import { NextResponse } from 'next/server';
import { getAvailableApiSites, getCacheTime } from '@/lib/config';
import { getCacheTime, getConfig } from '@/lib/config';
import { searchFromApi } from '@/lib/downstream';
import { yellowWords } from '@/lib/yellow';
export const runtime = 'edge';
@@ -23,12 +24,19 @@ export async function GET(request: Request) {
);
}
const apiSites = await getAvailableApiSites();
const config = await getConfig();
const apiSites = config.SourceConfig.filter((site) => !site.disabled);
const searchPromises = apiSites.map((site) => searchFromApi(site, query));
try {
const results = await Promise.all(searchPromises);
const flattenedResults = results.flat();
let flattenedResults = results.flat();
if (!config.SiteConfig.DisableYellowFilter) {
flattenedResults = flattenedResults.filter((result) => {
const typeName = result.type_name || '';
return !yellowWords.some((word: string) => typeName.includes(word));
});
}
const cacheTime = await getCacheTime();
return NextResponse.json(

View File

@@ -49,6 +49,8 @@ export default async function RootLayout({
let enableRegister = process.env.NEXT_PUBLIC_ENABLE_REGISTER === 'true';
let imageProxy = process.env.NEXT_PUBLIC_IMAGE_PROXY || '';
let doubanProxy = process.env.NEXT_PUBLIC_DOUBAN_PROXY || '';
let disableYellowFilter =
process.env.NEXT_PUBLIC_DISABLE_YELLOW_FILTER === 'true';
let customCategories =
(RuntimeConfig as any).custom_category?.map((category: any) => ({
name: 'name' in category ? category.name : '',
@@ -65,6 +67,7 @@ export default async function RootLayout({
enableRegister = config.UserConfig.AllowRegister;
imageProxy = config.SiteConfig.ImageProxy;
doubanProxy = config.SiteConfig.DoubanProxy;
disableYellowFilter = config.SiteConfig.DisableYellowFilter;
customCategories = config.CustomCategories.filter(
(category) => !category.disabled
).map((category) => ({
@@ -80,6 +83,7 @@ export default async function RootLayout({
ENABLE_REGISTER: enableRegister,
IMAGE_PROXY: imageProxy,
DOUBAN_PROXY: doubanProxy,
DISABLE_YELLOW_FILTER: disableYellowFilter,
CUSTOM_CATEGORIES: customCategories,
};

View File

@@ -13,6 +13,7 @@ import {
subscribeToDataUpdates,
} from '@/lib/db.client';
import { SearchResult } from '@/lib/types';
import { yellowWords } from '@/lib/yellow';
import PageLayout from '@/components/PageLayout';
import VideoCard from '@/components/VideoCard';
@@ -165,8 +166,18 @@ function SearchPageClient() {
`/api/search?q=${encodeURIComponent(query.trim())}`
);
const data = await response.json();
let results = data.results;
if (
typeof window !== 'undefined' &&
!(window as any).RUNTIME_CONFIG?.DISABLE_YELLOW_FILTER
) {
results = results.filter((result: SearchResult) => {
const typeName = result.type_name || '';
return !yellowWords.some((word: string) => typeName.includes(word));
});
}
setSearchResults(
data.results.sort((a: SearchResult, b: SearchResult) => {
results.sort((a: SearchResult, b: SearchResult) => {
// 优先排序:标题与搜索词完全一致的排在前面
const aExactMatch = a.title === query.trim();
const bExactMatch = b.title === query.trim();
@@ -311,7 +322,7 @@ function SearchPageClient() {
>
<VideoCard
id={item.id}
title={item.title}
title={item.title + ' ' + item.type_name}
poster={item.poster}
episodes={item.episodes.length}
source={item.source}

View File

@@ -6,6 +6,7 @@ export interface AdminConfig {
SiteInterfaceCacheTime: number;
ImageProxy: string;
DoubanProxy: string;
DisableYellowFilter: boolean;
};
UserConfig: {
AllowRegister: boolean;

View File

@@ -201,6 +201,8 @@ async function initConfig() {
SiteInterfaceCacheTime: fileConfig.cache_time || 7200,
ImageProxy: process.env.NEXT_PUBLIC_IMAGE_PROXY || '',
DoubanProxy: process.env.NEXT_PUBLIC_DOUBAN_PROXY || '',
DisableYellowFilter:
process.env.NEXT_PUBLIC_DISABLE_YELLOW_FILTER === 'true',
},
UserConfig: {
AllowRegister: process.env.NEXT_PUBLIC_ENABLE_REGISTER === 'true',
@@ -247,6 +249,8 @@ async function initConfig() {
SiteInterfaceCacheTime: fileConfig.cache_time || 7200,
ImageProxy: process.env.NEXT_PUBLIC_IMAGE_PROXY || '',
DoubanProxy: process.env.NEXT_PUBLIC_DOUBAN_PROXY || '',
DisableYellowFilter:
process.env.NEXT_PUBLIC_DISABLE_YELLOW_FILTER === 'true',
},
UserConfig: {
AllowRegister: process.env.NEXT_PUBLIC_ENABLE_REGISTER === 'true',
@@ -301,6 +305,8 @@ export async function getConfig(): Promise<AdminConfig> {
process.env.NEXT_PUBLIC_IMAGE_PROXY || '';
adminConfig.SiteConfig.DoubanProxy =
process.env.NEXT_PUBLIC_DOUBAN_PROXY || '';
adminConfig.SiteConfig.DisableYellowFilter =
process.env.NEXT_PUBLIC_DISABLE_YELLOW_FILTER === 'true';
// 合并文件中的源信息
fileConfig = runtimeConfig as unknown as ConfigFileStruct;
@@ -441,6 +447,8 @@ export async function resetConfig() {
SiteInterfaceCacheTime: fileConfig.cache_time || 7200,
ImageProxy: process.env.NEXT_PUBLIC_IMAGE_PROXY || '',
DoubanProxy: process.env.NEXT_PUBLIC_DOUBAN_PROXY || '',
DisableYellowFilter:
process.env.NEXT_PUBLIC_DISABLE_YELLOW_FILTER === 'true',
},
UserConfig: {
AllowRegister: process.env.NEXT_PUBLIC_ENABLE_REGISTER === 'true',

View File

@@ -2,7 +2,7 @@
'use client';
const CURRENT_VERSION = '20250801003950';
const CURRENT_VERSION = '20250801131720';
// 版本检查结果枚举
export enum UpdateStatus {

22
src/lib/yellow.ts Normal file
View File

@@ -0,0 +1,22 @@
export const yellowWords = [
'伦理片',
'福利',
'里番动漫',
'门事件',
'萝莉少女',
'制服诱惑',
'国产传媒',
'cosplay',
'黑丝诱惑',
'无码',
'日本无码',
'有码',
'日本有码',
'SWAG',
'网红主播',
'色情片',
'同性片',
'福利视频',
'福利片',
'写真热舞',
];