feat: 添加成人内容过滤功能
- 新增用户设置系统支持内容过滤开关 - 扩展类型定义支持成人内容标记 - 实现用户设置API端点(GET/PATCH/PUT) - 增强搜索API支持内容分组和过滤 - 创建AdultContentFilter UI组件 - 添加用户设置页面和认证检查 - 更新配置示例和README文档 - 实现LocalStorage和Redis存储后端 - 默认启用过滤确保安全性
This commit is contained in:
@@ -39,6 +39,7 @@
|
||||
- **📖 播放历史**:自动记录观看历史,快速找回看过的内容
|
||||
- **👥 多用户支持**:独立的用户系统,每个用户独享个人数据
|
||||
- **🔄 数据同步**:支持多种存储后端(LocalStorage、Redis、D1、Upstash)
|
||||
- **🔒 内容过滤**:智能成人内容过滤系统,默认开启安全保护
|
||||
|
||||
### 🚀 部署特性
|
||||
|
||||
@@ -586,6 +587,54 @@ docker run -d \
|
||||
|
||||
## 📱 高级功能使用指南
|
||||
|
||||
### 🔒 成人内容过滤
|
||||
|
||||
**功能介绍**:
|
||||
- 智能识别和过滤成人内容资源站
|
||||
- 用户可自主选择开启或关闭过滤功能
|
||||
- 默认开启过滤,确保安全浏览体验
|
||||
- 支持资源分组显示,避免误触
|
||||
|
||||
**使用方法**:
|
||||
|
||||
1. **访问用户设置**:
|
||||
- 登录后访问 `/settings` 页面
|
||||
- 或在用户菜单中点击「用户设置」
|
||||
|
||||
2. **配置过滤选项**:
|
||||
- 在「内容过滤」部分找到「成人内容过滤」开关
|
||||
- **开启**:完全隐藏成人内容资源站和搜索结果
|
||||
- **关闭**:成人内容在搜索结果中单独分组显示
|
||||
|
||||
3. **搜索结果展示**:
|
||||
- **过滤开启时**:只显示常规内容
|
||||
- **过滤关闭时**:显示两个标签页「常规结果」和「成人内容」
|
||||
|
||||
**配置文件格式**:
|
||||
|
||||
```json
|
||||
// config.json 中的资源站配置
|
||||
{
|
||||
"api_site": {
|
||||
"regular_site": {
|
||||
"api": "https://example.com/api.php/provide/vod",
|
||||
"name": "常规影视站",
|
||||
"is_adult": false // 或省略此字段,默认为 false
|
||||
},
|
||||
"adult_site": {
|
||||
"api": "https://adult.example.com/api.php/provide/vod",
|
||||
"name": "成人内容站",
|
||||
"is_adult": true // 标记为成人内容
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**安全提示**:
|
||||
- 默认情况下,所有新用户和未登录用户的成人内容过滤均为开启状态
|
||||
- 关闭过滤功能需要用户主动操作,确保使用意图明确
|
||||
- 建议管理员在配置资源站时准确标记 `is_adult` 字段
|
||||
|
||||
### 🎯 跳过片头片尾
|
||||
|
||||
**功能介绍**:
|
||||
|
||||
+8
-1
@@ -4,7 +4,14 @@
|
||||
"example_test": {
|
||||
"api": "https://example.com/api.php/provide/vod",
|
||||
"name": "示例视频源",
|
||||
"detail": "https://example.com"
|
||||
"detail": "https://example.com",
|
||||
"is_adult": false
|
||||
},
|
||||
"adult_example": {
|
||||
"api": "https://adult-example.com/api.php/provide/vod",
|
||||
"name": "成人内容源(仅供示例)",
|
||||
"detail": "https://adult-example.com",
|
||||
"is_adult": true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+43
-10
@@ -1,7 +1,8 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
|
||||
import { getAvailableApiSites, getCacheTime } from '@/lib/config';
|
||||
import { getAdultApiSites, getAvailableApiSites, getCacheTime } from '@/lib/config';
|
||||
import { addCorsHeaders, handleOptionsRequest } from '@/lib/cors';
|
||||
import { getStorage } from '@/lib/db';
|
||||
import { searchFromApi } from '@/lib/downstream';
|
||||
|
||||
export const runtime = 'edge';
|
||||
@@ -14,11 +15,16 @@ export async function OPTIONS() {
|
||||
export async function GET(request: Request) {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const query = searchParams.get('q');
|
||||
const includeAdult = searchParams.get('include_adult') === 'true';
|
||||
const userName = searchParams.get('user'); // 用于获取用户设置
|
||||
|
||||
if (!query) {
|
||||
const cacheTime = await getCacheTime();
|
||||
const response = NextResponse.json(
|
||||
{ results: [] },
|
||||
{
|
||||
regular_results: [],
|
||||
adult_results: []
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
'Cache-Control': `public, max-age=${cacheTime}, s-maxage=${cacheTime}`,
|
||||
@@ -30,16 +36,36 @@ export async function GET(request: Request) {
|
||||
return addCorsHeaders(response);
|
||||
}
|
||||
|
||||
const apiSites = await getAvailableApiSites();
|
||||
const searchPromises = apiSites.map((site) => searchFromApi(site, query));
|
||||
|
||||
try {
|
||||
const results = await Promise.all(searchPromises);
|
||||
const flattenedResults = results.flat();
|
||||
const cacheTime = await getCacheTime();
|
||||
// 获取用户设置以确定是否需要过滤成人内容
|
||||
let shouldFilterAdult = true; // 默认过滤成人内容
|
||||
|
||||
if (userName) {
|
||||
const storage = getStorage();
|
||||
const userSettings = await storage.getUserSettings(userName);
|
||||
shouldFilterAdult = userSettings?.filter_adult_content !== false;
|
||||
}
|
||||
|
||||
// 获取常规资源站
|
||||
const regularSites = await getAvailableApiSites(true); // 总是过滤成人内容
|
||||
const regularSearchPromises = regularSites.map((site) => searchFromApi(site, query));
|
||||
const regularResults = (await Promise.all(regularSearchPromises)).flat();
|
||||
|
||||
let adultResults: unknown[] = [];
|
||||
|
||||
// 如果用户设置允许且明确请求包含成人内容,则搜索成人资源站
|
||||
if (!shouldFilterAdult && includeAdult) {
|
||||
const adultSites = await getAdultApiSites();
|
||||
const adultSearchPromises = adultSites.map((site) => searchFromApi(site, query));
|
||||
adultResults = (await Promise.all(adultSearchPromises)).flat();
|
||||
}
|
||||
|
||||
const cacheTime = await getCacheTime();
|
||||
const response = NextResponse.json(
|
||||
{ results: flattenedResults },
|
||||
{
|
||||
regular_results: regularResults,
|
||||
adult_results: adultResults
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
'Cache-Control': `public, max-age=${cacheTime}, s-maxage=${cacheTime}`,
|
||||
@@ -50,7 +76,14 @@ export async function GET(request: Request) {
|
||||
);
|
||||
return addCorsHeaders(response);
|
||||
} catch (error) {
|
||||
const response = NextResponse.json({ error: '搜索失败' }, { status: 500 });
|
||||
const response = NextResponse.json(
|
||||
{
|
||||
regular_results: [],
|
||||
adult_results: [],
|
||||
error: '搜索失败'
|
||||
},
|
||||
{ status: 500 }
|
||||
);
|
||||
return addCorsHeaders(response);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
import { headers } from 'next/headers';
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
|
||||
import { getStorage } from '@/lib/db';
|
||||
import { UserSettings } from '@/lib/types';
|
||||
|
||||
// 获取用户设置
|
||||
export async function GET(_request: NextRequest) {
|
||||
try {
|
||||
const headersList = headers();
|
||||
const authorization = headersList.get('Authorization');
|
||||
|
||||
if (!authorization) {
|
||||
return NextResponse.json({ error: '未授权访问' }, { status: 401 });
|
||||
}
|
||||
|
||||
const userName = authorization.split(' ')[1]; // 假设格式为 "Bearer username"
|
||||
|
||||
if (!userName) {
|
||||
return NextResponse.json({ error: '用户名不能为空' }, { status: 400 });
|
||||
}
|
||||
|
||||
const storage = getStorage();
|
||||
const settings = await storage.getUserSettings(userName);
|
||||
|
||||
return NextResponse.json({
|
||||
settings: settings || {
|
||||
filter_adult_content: true, // 默认开启成人内容过滤
|
||||
theme: 'auto',
|
||||
language: 'zh-CN',
|
||||
auto_play: true,
|
||||
video_quality: 'auto'
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('Error getting user settings:', error);
|
||||
return NextResponse.json({ error: '获取用户设置失败' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
// 更新用户设置
|
||||
export async function PATCH(request: NextRequest) {
|
||||
try {
|
||||
const headersList = headers();
|
||||
const authorization = headersList.get('Authorization');
|
||||
|
||||
if (!authorization) {
|
||||
return NextResponse.json({ error: '未授权访问' }, { status: 401 });
|
||||
}
|
||||
|
||||
const userName = authorization.split(' ')[1];
|
||||
|
||||
if (!userName) {
|
||||
return NextResponse.json({ error: '用户名不能为空' }, { status: 400 });
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
const { settings } = body as { settings: Partial<UserSettings> };
|
||||
|
||||
if (!settings) {
|
||||
return NextResponse.json({ error: '设置数据不能为空' }, { status: 400 });
|
||||
}
|
||||
|
||||
const storage = getStorage();
|
||||
|
||||
// 验证用户存在
|
||||
const userExists = await storage.checkUserExist(userName);
|
||||
if (!userExists) {
|
||||
return NextResponse.json({ error: '用户不存在' }, { status: 404 });
|
||||
}
|
||||
|
||||
await storage.updateUserSettings(userName, settings);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: '设置更新成功'
|
||||
});
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('Error updating user settings:', error);
|
||||
return NextResponse.json({ error: '更新用户设置失败' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
|
||||
// 重置用户设置
|
||||
export async function PUT(request: NextRequest) {
|
||||
try {
|
||||
const headersList = headers();
|
||||
const authorization = headersList.get('Authorization');
|
||||
|
||||
if (!authorization) {
|
||||
return NextResponse.json({ error: '未授权访问' }, { status: 401 });
|
||||
}
|
||||
|
||||
const userName = authorization.split(' ')[1];
|
||||
|
||||
if (!userName) {
|
||||
return NextResponse.json({ error: '用户名不能为空' }, { status: 400 });
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
const { settings } = body as { settings: UserSettings };
|
||||
|
||||
if (!settings) {
|
||||
return NextResponse.json({ error: '设置数据不能为空' }, { status: 400 });
|
||||
}
|
||||
|
||||
const storage = getStorage();
|
||||
|
||||
// 验证用户存在
|
||||
const userExists = await storage.checkUserExist(userName);
|
||||
if (!userExists) {
|
||||
return NextResponse.json({ error: '用户不存在' }, { status: 404 });
|
||||
}
|
||||
|
||||
await storage.setUserSettings(userName, settings);
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
message: '设置已重置'
|
||||
});
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('Error resetting user settings:', error);
|
||||
return NextResponse.json({ error: '重置用户设置失败' }, { status: 500 });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useRouter } from 'next/navigation';
|
||||
|
||||
import { ArrowLeft, Settings, User } from 'lucide-react';
|
||||
|
||||
import AdultContentFilter from '@/components/AdultContentFilter';
|
||||
import { getAuthInfoFromBrowserCookie } from '@/lib/auth';
|
||||
|
||||
export default function UserSettingsPage() {
|
||||
const router = useRouter();
|
||||
const [authInfo, setAuthInfo] = useState<{ userName: string } | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const auth = getAuthInfoFromBrowserCookie();
|
||||
if (!auth || !auth.username) {
|
||||
// 如果用户未登录,重定向到登录页面
|
||||
router.push('/login');
|
||||
return;
|
||||
}
|
||||
setAuthInfo({ userName: auth.username });
|
||||
setIsLoading(false);
|
||||
}, [router]);
|
||||
|
||||
const handleFilterUpdate = (_enabled: boolean) => {
|
||||
// 可以在这里添加一些全局状态更新或通知逻辑
|
||||
// console.log('成人内容过滤状态已更新:', enabled);
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 flex items-center justify-center">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-blue-600"></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!authInfo) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 dark:bg-gray-900">
|
||||
<div className="max-w-4xl mx-auto p-6">
|
||||
{/* 页面头部 */}
|
||||
<div className="flex items-center justify-between mb-8">
|
||||
<div className="flex items-center space-x-4">
|
||||
<button
|
||||
onClick={() => router.back()}
|
||||
className="flex items-center justify-center w-10 h-10 rounded-full bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
|
||||
>
|
||||
<ArrowLeft className="w-5 h-5 text-gray-600 dark:text-gray-400" />
|
||||
</button>
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold text-gray-900 dark:text-white flex items-center">
|
||||
<Settings className="w-8 h-8 mr-3 text-blue-600" />
|
||||
用户设置
|
||||
</h1>
|
||||
<p className="text-gray-600 dark:text-gray-400 mt-2">
|
||||
管理您的个人偏好设置和隐私选项
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-3 px-4 py-2 bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700">
|
||||
<User className="w-5 h-5 text-gray-600 dark:text-gray-400" />
|
||||
<span className="text-sm font-medium text-gray-900 dark:text-white">
|
||||
{authInfo.userName}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 设置区域 */}
|
||||
<div className="space-y-6">
|
||||
{/* 内容过滤设置 */}
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4">
|
||||
内容过滤
|
||||
</h2>
|
||||
<AdultContentFilter
|
||||
userName={authInfo.userName}
|
||||
onUpdate={handleFilterUpdate}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* 其他设置部分预留 */}
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold text-gray-900 dark:text-white mb-4">
|
||||
其他设置
|
||||
</h2>
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-6">
|
||||
<p className="text-gray-500 dark:text-gray-400 text-center py-8">
|
||||
更多设置选项即将推出...
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 底部信息 */}
|
||||
<div className="mt-8 text-center text-sm text-gray-500 dark:text-gray-400">
|
||||
<p>设置会自动保存并在所有设备间同步</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { Shield, ShieldOff } from 'lucide-react';
|
||||
|
||||
interface AdultContentFilterProps {
|
||||
userName: string;
|
||||
onUpdate?: (enabled: boolean) => void;
|
||||
}
|
||||
|
||||
const AdultContentFilter: React.FC<AdultContentFilterProps> = ({
|
||||
userName,
|
||||
onUpdate
|
||||
}) => {
|
||||
const [isEnabled, setIsEnabled] = useState(true); // 默认开启过滤
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
// 获取用户设置
|
||||
useEffect(() => {
|
||||
const fetchUserSettings = async () => {
|
||||
if (!userName) return;
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/user/settings', {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${userName}`,
|
||||
},
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setIsEnabled(data.settings.filter_adult_content);
|
||||
} else {
|
||||
setError('获取用户设置失败');
|
||||
}
|
||||
} catch (err) {
|
||||
setError('网络连接失败');
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('Failed to fetch user settings:', err);
|
||||
}
|
||||
};
|
||||
|
||||
fetchUserSettings();
|
||||
}, [userName]);
|
||||
|
||||
// 更新用户设置
|
||||
const handleToggle = async () => {
|
||||
if (!userName || isLoading) return;
|
||||
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/user/settings', {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${userName}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
settings: {
|
||||
filter_adult_content: !isEnabled,
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const newState = !isEnabled;
|
||||
setIsEnabled(newState);
|
||||
onUpdate?.(newState);
|
||||
} else {
|
||||
const errorData = await response.json();
|
||||
setError(errorData.error || '更新设置失败');
|
||||
}
|
||||
} catch (err) {
|
||||
setError('网络连接失败');
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('Failed to update user settings:', err);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow-sm border border-gray-200 dark:border-gray-700 p-6">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex items-center space-x-3">
|
||||
<div className="flex items-center justify-center w-10 h-10 rounded-full bg-blue-100 dark:bg-blue-900">
|
||||
{isEnabled ? (
|
||||
<Shield className="w-5 h-5 text-blue-600 dark:text-blue-400" />
|
||||
) : (
|
||||
<ShieldOff className="w-5 h-5 text-gray-600 dark:text-gray-400" />
|
||||
)}
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h3 className="text-lg font-medium text-gray-900 dark:text-white">
|
||||
成人内容过滤
|
||||
</h3>
|
||||
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
|
||||
{isEnabled
|
||||
? '已开启过滤,将自动隐藏所有标记为"成人"的资源站及其内容'
|
||||
: '已关闭过滤,成人内容将在搜索结果中单独分组显示'
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-3">
|
||||
<button
|
||||
onClick={handleToggle}
|
||||
disabled={isLoading || !userName}
|
||||
className={`
|
||||
relative inline-flex h-6 w-11 items-center rounded-full transition-colors focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 focus:ring-offset-white dark:focus:ring-offset-gray-800 disabled:opacity-50 disabled:cursor-not-allowed
|
||||
${isEnabled
|
||||
? 'bg-blue-600'
|
||||
: 'bg-gray-200 dark:bg-gray-700'
|
||||
}
|
||||
`}
|
||||
>
|
||||
<span
|
||||
className={`
|
||||
inline-block h-4 w-4 transform rounded-full bg-white transition-transform
|
||||
${isEnabled ? 'translate-x-6' : 'translate-x-1'}
|
||||
`}
|
||||
/>
|
||||
</button>
|
||||
|
||||
{isLoading && (
|
||||
<div className="w-5 h-5">
|
||||
<div className="animate-spin rounded-full h-5 w-5 border-b-2 border-blue-600"></div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="mt-4 p-3 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-md">
|
||||
<p className="text-sm text-red-600 dark:text-red-400">{error}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mt-4 p-4 bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800 rounded-md">
|
||||
<div className="flex items-start">
|
||||
<div className="flex-shrink-0">
|
||||
<Shield className="w-5 h-5 text-amber-600 dark:text-amber-400" />
|
||||
</div>
|
||||
<div className="ml-3">
|
||||
<h4 className="text-sm font-medium text-amber-800 dark:text-amber-200">
|
||||
安全提示
|
||||
</h4>
|
||||
<p className="mt-1 text-sm text-amber-700 dark:text-amber-300">
|
||||
为了确保良好的使用体验和遵守相关法规,建议保持成人内容过滤开启。如需访问相关内容,请确保您已年满18周岁并承担相应法律责任。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdultContentFilter;
|
||||
@@ -22,6 +22,7 @@ export interface AdminConfig {
|
||||
detail?: string;
|
||||
from: 'config' | 'custom';
|
||||
disabled?: boolean;
|
||||
is_adult?: boolean; // 新增:是否为成人内容资源站
|
||||
}[];
|
||||
}
|
||||
|
||||
|
||||
+23
-2
@@ -378,9 +378,30 @@ export async function getCacheTime(): Promise<number> {
|
||||
return config.SiteConfig.SiteInterfaceCacheTime || 7200;
|
||||
}
|
||||
|
||||
export async function getAvailableApiSites(): Promise<ApiSite[]> {
|
||||
export async function getAvailableApiSites(filterAdult = false): Promise<ApiSite[]> {
|
||||
const config = await getConfig();
|
||||
return config.SourceConfig.filter((s) => !s.disabled).map((s) => ({
|
||||
let sites = config.SourceConfig.filter((s) => !s.disabled);
|
||||
|
||||
// 如果需要过滤成人内容,则排除标记为成人内容的资源站
|
||||
if (filterAdult) {
|
||||
sites = sites.filter((s) => !s.is_adult);
|
||||
}
|
||||
|
||||
return sites.map((s) => ({
|
||||
key: s.key,
|
||||
name: s.name,
|
||||
api: s.api,
|
||||
detail: s.detail,
|
||||
}));
|
||||
}
|
||||
|
||||
// 获取成人内容资源站
|
||||
export async function getAdultApiSites(): Promise<ApiSite[]> {
|
||||
const config = await getConfig();
|
||||
const adultSites = config.SourceConfig
|
||||
.filter((s) => !s.disabled && s.is_adult);
|
||||
|
||||
return adultSites.map((s) => ({
|
||||
key: s.key,
|
||||
name: s.name,
|
||||
api: s.api,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* eslint-disable no-console */
|
||||
import { AdminConfig } from './admin.types';
|
||||
import { EpisodeSkipConfig, Favorite, IStorage, PlayRecord } from './types';
|
||||
import { EpisodeSkipConfig, Favorite, IStorage, PlayRecord, UserSettings } from './types';
|
||||
|
||||
/**
|
||||
* LocalStorage 存储实现
|
||||
@@ -290,6 +290,56 @@ export class LocalStorage implements IStorage {
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- 用户设置 ----------
|
||||
async getUserSettings(userName: string): Promise<UserSettings | null> {
|
||||
if (typeof window === 'undefined') return null;
|
||||
|
||||
try {
|
||||
const storageKey = this.getStorageKey('settings', userName);
|
||||
const data = localStorage.getItem(storageKey);
|
||||
if (data) {
|
||||
return JSON.parse(data);
|
||||
}
|
||||
|
||||
// 如果用户设置不存在,返回默认设置
|
||||
const defaultSettings: UserSettings = {
|
||||
filter_adult_content: true, // 默认开启成人内容过滤
|
||||
theme: 'auto',
|
||||
language: 'zh-CN',
|
||||
auto_play: true,
|
||||
video_quality: 'auto'
|
||||
};
|
||||
|
||||
return defaultSettings;
|
||||
} catch (error) {
|
||||
console.error('Error getting user settings:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async setUserSettings(userName: string, settings: UserSettings): Promise<void> {
|
||||
if (typeof window === 'undefined') return;
|
||||
|
||||
try {
|
||||
const storageKey = this.getStorageKey('settings', userName);
|
||||
localStorage.setItem(storageKey, JSON.stringify(settings));
|
||||
} catch (error) {
|
||||
console.error('Error setting user settings:', error);
|
||||
}
|
||||
}
|
||||
|
||||
async updateUserSettings(userName: string, settings: Partial<UserSettings>): Promise<void> {
|
||||
if (typeof window === 'undefined') return;
|
||||
|
||||
try {
|
||||
const currentSettings = await this.getUserSettings(userName);
|
||||
const updatedSettings = { ...currentSettings, ...settings };
|
||||
await this.setUserSettings(userName, updatedSettings as UserSettings);
|
||||
} catch (error) {
|
||||
console.error('Error updating user settings:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- 管理员功能 ----------
|
||||
async getAllUsers(): Promise<string[]> {
|
||||
if (typeof window === 'undefined') return [];
|
||||
@@ -365,7 +415,7 @@ export class LocalStorage implements IStorage {
|
||||
localStorage.removeItem(userKey);
|
||||
|
||||
// 删除用户相关的所有数据
|
||||
const prefixes = ['playrecord', 'favorite', 'searchhistory', 'skipconfig'];
|
||||
const prefixes = ['playrecord', 'favorite', 'searchhistory', 'skipconfig', 'settings'];
|
||||
|
||||
for (const prefix of prefixes) {
|
||||
const dataPrefix = this.getStorageKey(prefix, userName);
|
||||
|
||||
+45
-1
@@ -3,7 +3,7 @@
|
||||
import { createClient, RedisClientType } from 'redis';
|
||||
|
||||
import { AdminConfig } from './admin.types';
|
||||
import { EpisodeSkipConfig, Favorite, IStorage, PlayRecord } from './types';
|
||||
import { EpisodeSkipConfig, Favorite, IStorage, PlayRecord, UserSettings } from './types';
|
||||
|
||||
// 搜索历史最大条数
|
||||
const SEARCH_HISTORY_LIMIT = 20;
|
||||
@@ -223,6 +223,50 @@ export class RedisStorage implements IStorage {
|
||||
if (favoriteKeys.length > 0) {
|
||||
await withRetry(() => this.client.del(favoriteKeys));
|
||||
}
|
||||
|
||||
// 删除用户设置
|
||||
await withRetry(() => this.client.del(this.userSettingsKey(userName)));
|
||||
}
|
||||
|
||||
// ---------- 用户设置 ----------
|
||||
private userSettingsKey(user: string) {
|
||||
return `u:${user}:settings`; // u:username:settings
|
||||
}
|
||||
|
||||
async getUserSettings(userName: string): Promise<UserSettings | null> {
|
||||
const data = await withRetry(() =>
|
||||
this.client.get(this.userSettingsKey(userName))
|
||||
);
|
||||
|
||||
if (data) {
|
||||
return JSON.parse(ensureString(data));
|
||||
}
|
||||
|
||||
// 如果用户设置不存在,返回默认设置
|
||||
const defaultSettings: UserSettings = {
|
||||
filter_adult_content: true, // 默认开启成人内容过滤
|
||||
theme: 'auto',
|
||||
language: 'zh-CN',
|
||||
auto_play: true,
|
||||
video_quality: 'auto'
|
||||
};
|
||||
|
||||
return defaultSettings;
|
||||
}
|
||||
|
||||
async setUserSettings(userName: string, settings: UserSettings): Promise<void> {
|
||||
await withRetry(() =>
|
||||
this.client.set(
|
||||
this.userSettingsKey(userName),
|
||||
JSON.stringify(settings)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
async updateUserSettings(userName: string, settings: Partial<UserSettings>): Promise<void> {
|
||||
const currentSettings = await this.getUserSettings(userName);
|
||||
const updatedSettings = { ...currentSettings, ...settings };
|
||||
await this.setUserSettings(userName, updatedSettings as UserSettings);
|
||||
}
|
||||
|
||||
// ---------- 搜索历史 ----------
|
||||
|
||||
@@ -70,6 +70,11 @@ export interface IStorage {
|
||||
// 删除用户(包括密码、搜索历史、播放记录、收藏夹)
|
||||
deleteUser(userName: string): Promise<void>;
|
||||
|
||||
// 用户设置相关
|
||||
getUserSettings(userName: string): Promise<UserSettings | null>;
|
||||
setUserSettings(userName: string, settings: UserSettings): Promise<void>;
|
||||
updateUserSettings(userName: string, settings: Partial<UserSettings>): Promise<void>;
|
||||
|
||||
// 搜索历史相关
|
||||
getSearchHistory(userName: string): Promise<string[]>;
|
||||
addSearchHistory(userName: string, keyword: string): Promise<void>;
|
||||
@@ -119,6 +124,38 @@ export interface DoubanResult {
|
||||
list: DoubanItem[];
|
||||
}
|
||||
|
||||
// 资源站配置
|
||||
export interface ApiSite {
|
||||
api: string;
|
||||
name: string;
|
||||
detail?: string;
|
||||
type?: number;
|
||||
playMode?: 'parse' | 'direct';
|
||||
is_adult?: boolean; // 新增:是否为成人内容资源站
|
||||
}
|
||||
|
||||
// 配置文件结构
|
||||
export interface Config {
|
||||
cache_time: number;
|
||||
api_site: { [key: string]: ApiSite };
|
||||
}
|
||||
|
||||
// 用户设置
|
||||
export interface UserSettings {
|
||||
filter_adult_content: boolean; // 是否过滤成人内容,默认为 true
|
||||
theme: 'light' | 'dark' | 'auto';
|
||||
language: string;
|
||||
auto_play: boolean;
|
||||
video_quality: string;
|
||||
[key: string]: string | boolean | number; // 允许其他设置
|
||||
}
|
||||
|
||||
// 搜索结果(支持成人内容分组)
|
||||
export interface GroupedSearchResults {
|
||||
regular_results: SearchResult[];
|
||||
adult_results?: SearchResult[];
|
||||
}
|
||||
|
||||
// Runtime配置类型
|
||||
export interface RuntimeConfig {
|
||||
STORAGE_TYPE?: string;
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { Redis } from '@upstash/redis';
|
||||
|
||||
import { AdminConfig } from './admin.types';
|
||||
import { EpisodeSkipConfig, Favorite, IStorage, PlayRecord } from './types';
|
||||
import { EpisodeSkipConfig, Favorite, IStorage, PlayRecord, UserSettings } from './types';
|
||||
|
||||
// 搜索历史最大条数
|
||||
const SEARCH_HISTORY_LIMIT = 20;
|
||||
|
||||
Reference in New Issue
Block a user