mirror of
https://github.com/MoonTechLab/LunaTV.git
synced 2026-02-20 00:14:41 +08:00
feat: add yellow filter
This commit is contained in:
29
README.md
29
README.md
@@ -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 |
|
||||
|
||||
## 配置说明
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
20250801003950
|
||||
20250801131720
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
// 写入数据库
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -6,6 +6,7 @@ export interface AdminConfig {
|
||||
SiteInterfaceCacheTime: number;
|
||||
ImageProxy: string;
|
||||
DoubanProxy: string;
|
||||
DisableYellowFilter: boolean;
|
||||
};
|
||||
UserConfig: {
|
||||
AllowRegister: boolean;
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
'use client';
|
||||
|
||||
const CURRENT_VERSION = '20250801003950';
|
||||
const CURRENT_VERSION = '20250801131720';
|
||||
|
||||
// 版本检查结果枚举
|
||||
export enum UpdateStatus {
|
||||
|
||||
22
src/lib/yellow.ts
Normal file
22
src/lib/yellow.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
export const yellowWords = [
|
||||
'伦理片',
|
||||
'福利',
|
||||
'里番动漫',
|
||||
'门事件',
|
||||
'萝莉少女',
|
||||
'制服诱惑',
|
||||
'国产传媒',
|
||||
'cosplay',
|
||||
'黑丝诱惑',
|
||||
'无码',
|
||||
'日本无码',
|
||||
'有码',
|
||||
'日本有码',
|
||||
'SWAG',
|
||||
'网红主播',
|
||||
'色情片',
|
||||
'同性片',
|
||||
'福利视频',
|
||||
'福利片',
|
||||
'写真热舞',
|
||||
];
|
||||
Reference in New Issue
Block a user