eslint fix
This commit is contained in:
@@ -11224,7 +11224,7 @@ function AdminPageClient() {
|
|||||||
const userLimit = 10;
|
const userLimit = 10;
|
||||||
|
|
||||||
// 获取新版本用户列表
|
// 获取新版本用户列表
|
||||||
const fetchUsersV2 = useCallback(async (page: number = 1) => {
|
const fetchUsersV2 = useCallback(async (page = 1) => {
|
||||||
try {
|
try {
|
||||||
setUserListLoading(true);
|
setUserListLoading(true);
|
||||||
const response = await fetch(`/api/admin/users?page=${page}&limit=${userLimit}`);
|
const response = await fetch(`/api/admin/users?page=${page}&limit=${userLimit}`);
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
|
||||||
import { getAuthInfoFromCookie } from '@/lib/auth';
|
|
||||||
import { getStorage } from '@/lib/db';
|
|
||||||
import { getConfig } from '@/lib/config';
|
|
||||||
import { EmailService } from '@/lib/email.service';
|
|
||||||
import type { AdminConfig } from '@/lib/admin.types';
|
import type { AdminConfig } from '@/lib/admin.types';
|
||||||
|
import { getAuthInfoFromCookie } from '@/lib/auth';
|
||||||
|
import { getConfig } from '@/lib/config';
|
||||||
|
import { getStorage } from '@/lib/db';
|
||||||
|
import { EmailService } from '@/lib/email.service';
|
||||||
|
|
||||||
export const runtime = 'nodejs';
|
export const runtime = 'nodejs';
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
|
||||||
import { getAuthInfoFromCookie } from '@/lib/auth';
|
import { getAuthInfoFromCookie } from '@/lib/auth';
|
||||||
import { getConfig } from '@/lib/config';
|
import { getConfig } from '@/lib/config';
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
|
||||||
import { getAuthInfoFromCookie } from '@/lib/auth';
|
import { getAuthInfoFromCookie } from '@/lib/auth';
|
||||||
import { getConfig } from '@/lib/config';
|
import { getConfig } from '@/lib/config';
|
||||||
import { db } from '@/lib/db';
|
import { db } from '@/lib/db';
|
||||||
@@ -37,7 +38,6 @@ export async function POST(request: NextRequest) {
|
|||||||
// 追加和覆盖:合并Sources数组
|
// 追加和覆盖:合并Sources数组
|
||||||
if (data.Sources && Array.isArray(data.Sources)) {
|
if (data.Sources && Array.isArray(data.Sources)) {
|
||||||
const existingSources = adminConfig.EmbyConfig?.Sources || [];
|
const existingSources = adminConfig.EmbyConfig?.Sources || [];
|
||||||
const existingKeys = new Set(existingSources.map(s => s.key));
|
|
||||||
|
|
||||||
// 覆盖已存在的,追加新的
|
// 覆盖已存在的,追加新的
|
||||||
const mergedSources = [...existingSources];
|
const mergedSources = [...existingSources];
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ export async function POST(request: NextRequest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 验证扫描间隔
|
// 验证扫描间隔
|
||||||
let scanInterval = parseInt(ScanInterval) || 0;
|
const scanInterval = parseInt(ScanInterval) || 0;
|
||||||
if (scanInterval > 0 && scanInterval < 60) {
|
if (scanInterval > 0 && scanInterval < 60) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: '定时扫描间隔最低为 60 分钟' },
|
{ error: '定时扫描间隔最低为 60 分钟' },
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
|
||||||
import { getAuthInfoFromCookie } from '@/lib/auth';
|
import { getAuthInfoFromCookie } from '@/lib/auth';
|
||||||
import { getConfig } from '@/lib/config';
|
|
||||||
import { db } from '@/lib/db';
|
import { db } from '@/lib/db';
|
||||||
|
|
||||||
export const runtime = 'nodejs';
|
export const runtime = 'nodejs';
|
||||||
@@ -24,9 +23,6 @@ export async function GET(request: NextRequest) {
|
|||||||
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取配置
|
|
||||||
const adminConfig = await getConfig();
|
|
||||||
|
|
||||||
// 判定操作者角色
|
// 判定操作者角色
|
||||||
let operatorRole: 'owner' | 'admin' | 'user' = 'user';
|
let operatorRole: 'owner' | 'admin' | 'user' = 'user';
|
||||||
if (authInfo.username === process.env.USERNAME) {
|
if (authInfo.username === process.env.USERNAME) {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
|
||||||
import { getAuthInfoFromCookie } from '@/lib/auth';
|
import { getAuthInfoFromCookie } from '@/lib/auth';
|
||||||
import { getConfig } from '@/lib/config';
|
import { getConfig } from '@/lib/config';
|
||||||
import { db } from '@/lib/db';
|
import { db } from '@/lib/db';
|
||||||
|
|||||||
@@ -2,11 +2,11 @@
|
|||||||
|
|
||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
|
||||||
import { getAuthInfoFromCookie } from '@/lib/auth';
|
|
||||||
import {
|
import {
|
||||||
orchestrateDataSources,
|
orchestrateDataSources,
|
||||||
VideoContext,
|
VideoContext,
|
||||||
} from '@/lib/ai-orchestrator';
|
} from '@/lib/ai-orchestrator';
|
||||||
|
import { getAuthInfoFromCookie } from '@/lib/auth';
|
||||||
import { getConfig } from '@/lib/config';
|
import { getConfig } from '@/lib/config';
|
||||||
import { db } from '@/lib/db';
|
import { db } from '@/lib/db';
|
||||||
|
|
||||||
@@ -35,7 +35,7 @@ async function streamOpenAIChat(
|
|||||||
temperature: number;
|
temperature: number;
|
||||||
maxTokens: number;
|
maxTokens: number;
|
||||||
},
|
},
|
||||||
enableStreaming: boolean = true
|
enableStreaming = true
|
||||||
): Promise<ReadableStream | Response> {
|
): Promise<ReadableStream | Response> {
|
||||||
const response = await fetch(`${config.baseURL}/chat/completions`, {
|
const response = await fetch(`${config.baseURL}/chat/completions`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -61,45 +61,6 @@ async function streamOpenAIChat(
|
|||||||
return enableStreaming ? response.body! : response;
|
return enableStreaming ? response.body! : response;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Claude API流式聊天请求
|
|
||||||
*/
|
|
||||||
async function streamClaudeChat(
|
|
||||||
messages: ChatMessage[],
|
|
||||||
systemPrompt: string,
|
|
||||||
config: {
|
|
||||||
apiKey: string;
|
|
||||||
model: string;
|
|
||||||
temperature: number;
|
|
||||||
maxTokens: number;
|
|
||||||
}
|
|
||||||
): Promise<ReadableStream> {
|
|
||||||
const response = await fetch('https://api.anthropic.com/v1/messages', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'x-api-key': config.apiKey,
|
|
||||||
'anthropic-version': '2023-06-01',
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
model: config.model,
|
|
||||||
max_tokens: config.maxTokens,
|
|
||||||
temperature: config.temperature,
|
|
||||||
system: systemPrompt,
|
|
||||||
messages: messages,
|
|
||||||
stream: true,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(
|
|
||||||
`Claude API error: ${response.status} ${response.statusText}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return response.body!;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 转换流为SSE格式
|
* 转换流为SSE格式
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -123,7 +123,7 @@ export async function POST(request: NextRequest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 检查用户名是否已存在(优先使用新版本)
|
// 检查用户名是否已存在(优先使用新版本)
|
||||||
let userExists = await db.checkUserExistV2(username);
|
const userExists = await db.checkUserExistV2(username);
|
||||||
if (userExists) {
|
if (userExists) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ error: '用户名已存在' },
|
{ error: '用户名已存在' },
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { NextRequest, NextResponse } from 'next/server';
|
|||||||
|
|
||||||
import { getConfig } from '@/lib/config';
|
import { getConfig } from '@/lib/config';
|
||||||
import { db } from '@/lib/db';
|
import { db } from '@/lib/db';
|
||||||
import { OpenListClient } from '@/lib/openlist.client';
|
|
||||||
import {
|
import {
|
||||||
getCachedMetaInfo,
|
getCachedMetaInfo,
|
||||||
MetaInfo,
|
MetaInfo,
|
||||||
@@ -260,13 +259,6 @@ async function handleOpenListProxy(request: NextRequest) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const rootPath = openListConfig.RootPath || '/';
|
|
||||||
const client = new OpenListClient(
|
|
||||||
openListConfig.URL,
|
|
||||||
openListConfig.Username,
|
|
||||||
openListConfig.Password
|
|
||||||
);
|
|
||||||
|
|
||||||
// 读取 metainfo (从数据库或缓存)
|
// 读取 metainfo (从数据库或缓存)
|
||||||
let metaInfo: MetaInfo | null = getCachedMetaInfo();
|
let metaInfo: MetaInfo | null = getCachedMetaInfo();
|
||||||
|
|
||||||
@@ -296,7 +288,7 @@ async function handleOpenListProxy(request: NextRequest) {
|
|||||||
if (wd) {
|
if (wd) {
|
||||||
const results = Object.entries(metaInfo.folders)
|
const results = Object.entries(metaInfo.folders)
|
||||||
.filter(
|
.filter(
|
||||||
([key, info]) =>
|
([_key, info]) =>
|
||||||
info.folderName.toLowerCase().includes(wd.toLowerCase()) ||
|
info.folderName.toLowerCase().includes(wd.toLowerCase()) ||
|
||||||
info.title.toLowerCase().includes(wd.toLowerCase())
|
info.title.toLowerCase().includes(wd.toLowerCase())
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -4,12 +4,12 @@ import { NextRequest, NextResponse } from 'next/server';
|
|||||||
|
|
||||||
import { getConfig, refineConfig } from '@/lib/config';
|
import { getConfig, refineConfig } from '@/lib/config';
|
||||||
import { db, getStorage } from '@/lib/db';
|
import { db, getStorage } from '@/lib/db';
|
||||||
|
import { EmailService } from '@/lib/email.service';
|
||||||
|
import { FavoriteUpdate,getBatchFavoriteUpdateEmailTemplate } from '@/lib/email.templates';
|
||||||
import { fetchVideoDetail } from '@/lib/fetchVideoDetail';
|
import { fetchVideoDetail } from '@/lib/fetchVideoDetail';
|
||||||
import { refreshLiveChannels } from '@/lib/live';
|
import { refreshLiveChannels } from '@/lib/live';
|
||||||
import { startOpenListRefresh } from '@/lib/openlist-refresh';
|
import { startOpenListRefresh } from '@/lib/openlist-refresh';
|
||||||
import { SearchResult } from '@/lib/types';
|
import { SearchResult } from '@/lib/types';
|
||||||
import { EmailService } from '@/lib/email.service';
|
|
||||||
import { getBatchFavoriteUpdateEmailTemplate, FavoriteUpdate } from '@/lib/email.templates';
|
|
||||||
|
|
||||||
export const runtime = 'nodejs';
|
export const runtime = 'nodejs';
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
|
||||||
import { getAuthInfoFromCookie } from '@/lib/auth';
|
import { getAuthInfoFromCookie } from '@/lib/auth';
|
||||||
import { getConfig } from '@/lib/config';
|
|
||||||
import { db } from '@/lib/db';
|
import { db } from '@/lib/db';
|
||||||
import { DanmakuFilterConfig } from '@/lib/types';
|
import { DanmakuFilterConfig } from '@/lib/types';
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server';
|
|
||||||
import * as cheerio from 'cheerio';
|
import * as cheerio from 'cheerio';
|
||||||
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
|
||||||
import { fetchDoubanWithVerification } from '@/lib/douban-anti-crawler';
|
import { fetchDoubanWithVerification } from '@/lib/douban-anti-crawler';
|
||||||
|
|
||||||
export const runtime = 'nodejs';
|
export const runtime = 'nodejs';
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server';
|
|
||||||
import * as cheerio from 'cheerio';
|
import * as cheerio from 'cheerio';
|
||||||
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
|
||||||
import { fetchDoubanData } from '@/lib/douban';
|
import { fetchDoubanData } from '@/lib/douban';
|
||||||
import { fetchDoubanWithVerification } from '@/lib/douban-anti-crawler';
|
import { fetchDoubanWithVerification } from '@/lib/douban-anti-crawler';
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
|
||||||
import { embyManager } from '@/lib/emby-manager';
|
|
||||||
import { getCachedEmbyList, setCachedEmbyList } from '@/lib/emby-cache';
|
import { getCachedEmbyList, setCachedEmbyList } from '@/lib/emby-cache';
|
||||||
|
import { embyManager } from '@/lib/emby-manager';
|
||||||
|
|
||||||
export const runtime = 'nodejs';
|
export const runtime = 'nodejs';
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
|
||||||
import { embyManager } from '@/lib/emby-manager';
|
|
||||||
import { getCachedEmbyViews, setCachedEmbyViews } from '@/lib/emby-cache';
|
import { getCachedEmbyViews, setCachedEmbyViews } from '@/lib/emby-cache';
|
||||||
|
import { embyManager } from '@/lib/emby-manager';
|
||||||
|
|
||||||
export const runtime = 'nodejs';
|
export const runtime = 'nodejs';
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
|
||||||
import { getAuthInfoFromCookie } from '@/lib/auth';
|
import { getAuthInfoFromCookie } from '@/lib/auth';
|
||||||
import { getConfig } from '@/lib/config';
|
|
||||||
import { db } from '@/lib/db';
|
import { db } from '@/lib/db';
|
||||||
import { Favorite } from '@/lib/types';
|
import { Favorite } from '@/lib/types';
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server';
|
|
||||||
import { nanoid } from 'nanoid';
|
import { nanoid } from 'nanoid';
|
||||||
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
|
||||||
import { getAuthInfoFromCookie } from '@/lib/auth';
|
import { getAuthInfoFromCookie } from '@/lib/auth';
|
||||||
import { getStorage } from '@/lib/db';
|
import { getStorage } from '@/lib/db';
|
||||||
|
|||||||
+3
-2
@@ -3,11 +3,12 @@
|
|||||||
* 路径格式: /api/offline-download/local/[source]/[videoId]/[episodeIndex]/[file]
|
* 路径格式: /api/offline-download/local/[source]/[videoId]/[episodeIndex]/[file]
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { NextRequest, NextResponse } from 'next/server';
|
|
||||||
import { getAuthInfoFromCookie } from '@/lib/auth';
|
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
|
|
||||||
|
import { getAuthInfoFromCookie } from '@/lib/auth';
|
||||||
|
|
||||||
// 检查是否启用离线下载功能
|
// 检查是否启用离线下载功能
|
||||||
const OFFLINE_DOWNLOAD_ENABLED = process.env.NEXT_PUBLIC_ENABLE_OFFLINE_DOWNLOAD === 'true';
|
const OFFLINE_DOWNLOAD_ENABLED = process.env.NEXT_PUBLIC_ENABLE_OFFLINE_DOWNLOAD === 'true';
|
||||||
const OFFLINE_DOWNLOAD_DIR = process.env.OFFLINE_DOWNLOAD_DIR || '/data';
|
const OFFLINE_DOWNLOAD_DIR = process.env.OFFLINE_DOWNLOAD_DIR || '/data';
|
||||||
|
|||||||
@@ -2,11 +2,12 @@
|
|||||||
* 本地下载视频播放代理 API
|
* 本地下载视频播放代理 API
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { NextRequest, NextResponse } from 'next/server';
|
|
||||||
import { getAuthInfoFromCookie } from '@/lib/auth';
|
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
|
|
||||||
|
import { getAuthInfoFromCookie } from '@/lib/auth';
|
||||||
|
|
||||||
// 检查是否启用离线下载功能
|
// 检查是否启用离线下载功能
|
||||||
const OFFLINE_DOWNLOAD_ENABLED = process.env.NEXT_PUBLIC_ENABLE_OFFLINE_DOWNLOAD === 'true';
|
const OFFLINE_DOWNLOAD_ENABLED = process.env.NEXT_PUBLIC_ENABLE_OFFLINE_DOWNLOAD === 'true';
|
||||||
const OFFLINE_DOWNLOAD_DIR = process.env.OFFLINE_DOWNLOAD_DIR || '/data';
|
const OFFLINE_DOWNLOAD_DIR = process.env.OFFLINE_DOWNLOAD_DIR || '/data';
|
||||||
|
|||||||
@@ -2,11 +2,12 @@
|
|||||||
* 离线下载任务管理 API
|
* 离线下载任务管理 API
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import * as fs from 'fs';
|
||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
import * as path from 'path';
|
||||||
|
|
||||||
import { getAuthInfoFromCookie } from '@/lib/auth';
|
import { getAuthInfoFromCookie } from '@/lib/auth';
|
||||||
import { OfflineDownloader, OfflineDownloadTask } from '@/lib/offline-downloader';
|
import { OfflineDownloader, OfflineDownloadTask } from '@/lib/offline-downloader';
|
||||||
import * as fs from 'fs';
|
|
||||||
import * as path from 'path';
|
|
||||||
|
|
||||||
// 检查是否启用离线下载功能
|
// 检查是否启用离线下载功能
|
||||||
const OFFLINE_DOWNLOAD_ENABLED = process.env.NEXT_PUBLIC_ENABLE_OFFLINE_DOWNLOAD === 'true';
|
const OFFLINE_DOWNLOAD_ENABLED = process.env.NEXT_PUBLIC_ENABLE_OFFLINE_DOWNLOAD === 'true';
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import { getAuthInfoFromCookie } from '@/lib/auth';
|
|||||||
import { getConfig } from '@/lib/config';
|
import { getConfig } from '@/lib/config';
|
||||||
import { db } from '@/lib/db';
|
import { db } from '@/lib/db';
|
||||||
import {
|
import {
|
||||||
getCachedMetaInfo,
|
|
||||||
invalidateMetaInfoCache,
|
invalidateMetaInfoCache,
|
||||||
MetaInfo,
|
MetaInfo,
|
||||||
setCachedMetaInfo,
|
setCachedMetaInfo,
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
|
||||||
import { getAuthInfoFromCookie } from '@/lib/auth';
|
import { getAuthInfoFromCookie } from '@/lib/auth';
|
||||||
import { getConfig } from '@/lib/config';
|
|
||||||
import { db } from '@/lib/db';
|
import { db } from '@/lib/db';
|
||||||
import { PlayRecord } from '@/lib/types';
|
import { PlayRecord } from '@/lib/types';
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
|
||||||
import { getAuthInfoFromCookie } from '@/lib/auth';
|
import { getAuthInfoFromCookie } from '@/lib/auth';
|
||||||
import { getConfig } from '@/lib/config';
|
|
||||||
import { db } from '@/lib/db';
|
import { db } from '@/lib/db';
|
||||||
|
|
||||||
export const runtime = 'nodejs';
|
export const runtime = 'nodejs';
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
|
||||||
import { getAuthInfoFromCookie } from '@/lib/auth';
|
import { getAuthInfoFromCookie } from '@/lib/auth';
|
||||||
import { getConfig } from '@/lib/config';
|
|
||||||
import { db } from '@/lib/db';
|
import { db } from '@/lib/db';
|
||||||
import { SkipConfig } from '@/lib/types';
|
import { SkipConfig } from '@/lib/types';
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
|
||||||
import { getConfig } from '@/lib/config';
|
import { getConfig } from '@/lib/config';
|
||||||
|
|
||||||
import { getThemeCSS } from '@/styles/themes';
|
import { getThemeCSS } from '@/styles/themes';
|
||||||
|
|
||||||
export const runtime = 'nodejs';
|
export const runtime = 'nodejs';
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
|
||||||
|
import { getConfig } from '@/lib/config';
|
||||||
import {
|
import {
|
||||||
searchTMDBMulti,
|
getTMDBImageUrl,
|
||||||
getTMDBMovieDetails,
|
getTMDBMovieDetails,
|
||||||
getTMDBTVDetails,
|
getTMDBTVDetails,
|
||||||
getTMDBImageUrl,
|
searchTMDBMulti,
|
||||||
} from '@/lib/tmdb.client';
|
} from '@/lib/tmdb.client';
|
||||||
import { getConfig } from '@/lib/config';
|
|
||||||
|
|
||||||
// 服务器端缓存(内存)
|
// 服务器端缓存(内存)
|
||||||
const searchCache = new Map<
|
const searchCache = new Map<
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
|
||||||
|
import { getConfig } from '@/lib/config';
|
||||||
import {
|
import {
|
||||||
searchTMDBMulti,
|
getTMDBImageUrl,
|
||||||
getTMDBMovieRecommendations,
|
getTMDBMovieRecommendations,
|
||||||
getTMDBTVRecommendations,
|
getTMDBTVRecommendations,
|
||||||
getTMDBImageUrl,
|
searchTMDBMulti,
|
||||||
} from '@/lib/tmdb.client';
|
} from '@/lib/tmdb.client';
|
||||||
import { getConfig } from '@/lib/config';
|
|
||||||
|
|
||||||
// 服务器端缓存(1天)
|
// 服务器端缓存(1天)
|
||||||
const searchCache = new Map<string, { data: any; timestamp: number }>();
|
const searchCache = new Map<string, { data: any; timestamp: number }>();
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any, no-console */
|
/* eslint-disable @typescript-eslint/no-explicit-any, no-console */
|
||||||
|
|
||||||
|
import { HttpsProxyAgent } from 'https-proxy-agent';
|
||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
import nodeFetch from 'node-fetch';
|
||||||
|
|
||||||
import { getAuthInfoFromCookie } from '@/lib/auth';
|
import { getAuthInfoFromCookie } from '@/lib/auth';
|
||||||
import { getConfig } from '@/lib/config';
|
import { getConfig } from '@/lib/config';
|
||||||
import { getNextApiKey } from '@/lib/tmdb.client';
|
import { getNextApiKey } from '@/lib/tmdb.client';
|
||||||
import { HttpsProxyAgent } from 'https-proxy-agent';
|
|
||||||
import nodeFetch from 'node-fetch';
|
|
||||||
|
|
||||||
export const runtime = 'nodejs';
|
export const runtime = 'nodejs';
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any, no-console */
|
/* eslint-disable @typescript-eslint/no-explicit-any, no-console */
|
||||||
|
|
||||||
|
import { HttpsProxyAgent } from 'https-proxy-agent';
|
||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
import nodeFetch from 'node-fetch';
|
||||||
|
|
||||||
import { getAuthInfoFromCookie } from '@/lib/auth';
|
import { getAuthInfoFromCookie } from '@/lib/auth';
|
||||||
import { getConfig } from '@/lib/config';
|
import { getConfig } from '@/lib/config';
|
||||||
import { HttpsProxyAgent } from 'https-proxy-agent';
|
|
||||||
import nodeFetch from 'node-fetch';
|
|
||||||
import { getNextApiKey } from '@/lib/tmdb.client';
|
import { getNextApiKey } from '@/lib/tmdb.client';
|
||||||
|
|
||||||
export const runtime = 'nodejs';
|
export const runtime = 'nodejs';
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any, no-console */
|
/* eslint-disable @typescript-eslint/no-explicit-any, no-console */
|
||||||
|
|
||||||
|
import { HttpsProxyAgent } from 'https-proxy-agent';
|
||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
import nodeFetch from 'node-fetch';
|
||||||
|
|
||||||
import { getAuthInfoFromCookie } from '@/lib/auth';
|
import { getAuthInfoFromCookie } from '@/lib/auth';
|
||||||
import { getConfig } from '@/lib/config';
|
import { getConfig } from '@/lib/config';
|
||||||
import { getNextApiKey } from '@/lib/tmdb.client';
|
import { getNextApiKey } from '@/lib/tmdb.client';
|
||||||
import { HttpsProxyAgent } from 'https-proxy-agent';
|
|
||||||
import nodeFetch from 'node-fetch';
|
|
||||||
|
|
||||||
export const runtime = 'nodejs';
|
export const runtime = 'nodejs';
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
|
||||||
import { NextResponse } from 'next/server';
|
import { NextResponse } from 'next/server';
|
||||||
import { getTMDBTrendingContent, getTMDBVideos } from '@/lib/tmdb.client';
|
|
||||||
import { getConfig } from '@/lib/config';
|
import { getConfig } from '@/lib/config';
|
||||||
import { fetchDoubanData } from '@/lib/douban';
|
import { fetchDoubanData } from '@/lib/douban';
|
||||||
|
import { getTMDBTrendingContent, getTMDBVideos } from '@/lib/tmdb.client';
|
||||||
|
|
||||||
// 缓存配置 - 服务器内存缓存3小时
|
// 缓存配置 - 服务器内存缓存3小时
|
||||||
const CACHE_DURATION = 3 * 60 * 60 * 1000; // 3小时
|
const CACHE_DURATION = 3 * 60 * 60 * 1000; // 3小时
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
import { getTMDBUpcomingContent } from '@/lib/tmdb.client';
|
|
||||||
import { getConfig } from '@/lib/config';
|
import { getConfig } from '@/lib/config';
|
||||||
|
import { getTMDBUpcomingContent } from '@/lib/tmdb.client';
|
||||||
|
|
||||||
// 内存缓存对象
|
// 内存缓存对象
|
||||||
interface CacheItem {
|
interface CacheItem {
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server';
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
|
||||||
import { getConfig } from '@/lib/config';
|
import { getConfig } from '@/lib/config';
|
||||||
|
|
||||||
export const dynamic = 'force-dynamic'; // 禁用缓存
|
export const dynamic = 'force-dynamic'; // 禁用缓存
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { NextRequest, NextResponse } from 'next/server';
|
|
||||||
import crypto from 'crypto';
|
import crypto from 'crypto';
|
||||||
|
import { NextRequest, NextResponse } from 'next/server';
|
||||||
|
|
||||||
function getAntiCode(oldAntiCode: string, streamName: string): string {
|
function getAntiCode(oldAntiCode: string, streamName: string): string {
|
||||||
const paramsT = 100;
|
const paramsT = 100;
|
||||||
|
|||||||
+6
-6
@@ -7,16 +7,16 @@ import './globals.css';
|
|||||||
|
|
||||||
import { getConfig } from '@/lib/config';
|
import { getConfig } from '@/lib/config';
|
||||||
|
|
||||||
|
import { DanmakuCacheCleanup } from '../components/DanmakuCacheCleanup';
|
||||||
|
import { DownloadBubble } from '../components/DownloadBubble';
|
||||||
|
import { DownloadPanel } from '../components/DownloadPanel';
|
||||||
import { GlobalErrorIndicator } from '../components/GlobalErrorIndicator';
|
import { GlobalErrorIndicator } from '../components/GlobalErrorIndicator';
|
||||||
import { SiteProvider } from '../components/SiteProvider';
|
import { SiteProvider } from '../components/SiteProvider';
|
||||||
import { ThemeProvider } from '../components/ThemeProvider';
|
import { ThemeProvider } from '../components/ThemeProvider';
|
||||||
import { WatchRoomProvider } from '../components/WatchRoomProvider';
|
|
||||||
import ChatFloatingWindow from '../components/watch-room/ChatFloatingWindow';
|
|
||||||
import { DownloadProvider } from '../contexts/DownloadContext';
|
|
||||||
import { DownloadBubble } from '../components/DownloadBubble';
|
|
||||||
import { DownloadPanel } from '../components/DownloadPanel';
|
|
||||||
import { DanmakuCacheCleanup } from '../components/DanmakuCacheCleanup';
|
|
||||||
import TopProgressBar from '../components/TopProgressBar';
|
import TopProgressBar from '../components/TopProgressBar';
|
||||||
|
import ChatFloatingWindow from '../components/watch-room/ChatFloatingWindow';
|
||||||
|
import { WatchRoomProvider } from '../components/WatchRoomProvider';
|
||||||
|
import { DownloadProvider } from '../contexts/DownloadContext';
|
||||||
|
|
||||||
const inter = Inter({ subsets: ['latin'] });
|
const inter = Inter({ subsets: ['latin'] });
|
||||||
export const dynamic = 'force-dynamic';
|
export const dynamic = 'force-dynamic';
|
||||||
|
|||||||
@@ -6,8 +6,6 @@ import { Heart, Radio, Tv } from 'lucide-react';
|
|||||||
import { useRouter, useSearchParams } from 'next/navigation';
|
import { useRouter, useSearchParams } from 'next/navigation';
|
||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
|
|
||||||
import { useLiveSync } from '@/hooks/useLiveSync';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
deleteFavorite,
|
deleteFavorite,
|
||||||
generateStorageKey,
|
generateStorageKey,
|
||||||
@@ -17,6 +15,7 @@ import {
|
|||||||
subscribeToDataUpdates,
|
subscribeToDataUpdates,
|
||||||
} from '@/lib/db.client';
|
} from '@/lib/db.client';
|
||||||
import { parseCustomTimeFormat } from '@/lib/time';
|
import { parseCustomTimeFormat } from '@/lib/time';
|
||||||
|
import { useLiveSync } from '@/hooks/useLiveSync';
|
||||||
|
|
||||||
import EpgScrollableRow from '@/components/EpgScrollableRow';
|
import EpgScrollableRow from '@/components/EpgScrollableRow';
|
||||||
import PageLayout from '@/components/PageLayout';
|
import PageLayout from '@/components/PageLayout';
|
||||||
@@ -1150,7 +1149,7 @@ function LivePageClient() {
|
|||||||
if (!selectedGroup) return;
|
if (!selectedGroup) return;
|
||||||
|
|
||||||
// 先在当前分组搜索
|
// 先在当前分组搜索
|
||||||
let filtered = filterChannels(selectedGroup, keyword);
|
const filtered = filterChannels(selectedGroup, keyword);
|
||||||
|
|
||||||
// 如果当前分组没有匹配的频道,且有搜索关键词,轮询所有分组
|
// 如果当前分组没有匹配的频道,且有搜索关键词,轮询所有分组
|
||||||
if (filtered.length === 0 && keyword.trim() && groupedChannels) {
|
if (filtered.length === 0 && keyword.trim() && groupedChannels) {
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState, useEffect } from 'react';
|
import { AlertCircle, CheckCircle } from 'lucide-react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
|
import { useEffect,useState } from 'react';
|
||||||
import { createPortal } from 'react-dom';
|
import { createPortal } from 'react-dom';
|
||||||
import { CheckCircle, AlertCircle, Plus } from 'lucide-react';
|
|
||||||
|
|
||||||
import PageLayout from '@/components/PageLayout';
|
|
||||||
import { getTMDBImageUrl } from '@/lib/tmdb.client';
|
import { getTMDBImageUrl } from '@/lib/tmdb.client';
|
||||||
import { processImageUrl } from '@/lib/utils';
|
import { processImageUrl } from '@/lib/utils';
|
||||||
|
|
||||||
|
import PageLayout from '@/components/PageLayout';
|
||||||
|
|
||||||
interface TMDBResult {
|
interface TMDBResult {
|
||||||
id: number;
|
id: number;
|
||||||
title?: string;
|
title?: string;
|
||||||
|
|||||||
+9
-7
@@ -2,9 +2,9 @@
|
|||||||
|
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { ChevronRight, Bot, ListVideo } from 'lucide-react';
|
import { Bot, ChevronRight, ListVideo } from 'lucide-react';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { Suspense, useCallback, useEffect, useRef, useState } from 'react';
|
import { Suspense, useEffect, useState } from 'react';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
BangumiCalendarData,
|
BangumiCalendarData,
|
||||||
@@ -15,15 +15,15 @@ import { getTMDBImageUrl, TMDBItem } from '@/lib/tmdb.client';
|
|||||||
import { DoubanItem } from '@/lib/types';
|
import { DoubanItem } from '@/lib/types';
|
||||||
import { processImageUrl } from '@/lib/utils';
|
import { processImageUrl } from '@/lib/utils';
|
||||||
|
|
||||||
|
import AIChatPanel from '@/components/AIChatPanel';
|
||||||
|
import BannerCarousel from '@/components/BannerCarousel';
|
||||||
import ContinueWatching from '@/components/ContinueWatching';
|
import ContinueWatching from '@/components/ContinueWatching';
|
||||||
|
import FireworksCanvas from '@/components/FireworksCanvas';
|
||||||
|
import HttpWarningDialog from '@/components/HttpWarningDialog';
|
||||||
import PageLayout from '@/components/PageLayout';
|
import PageLayout from '@/components/PageLayout';
|
||||||
import ScrollableRow from '@/components/ScrollableRow';
|
import ScrollableRow from '@/components/ScrollableRow';
|
||||||
import { useSite } from '@/components/SiteProvider';
|
import { useSite } from '@/components/SiteProvider';
|
||||||
import VideoCard from '@/components/VideoCard';
|
import VideoCard from '@/components/VideoCard';
|
||||||
import HttpWarningDialog from '@/components/HttpWarningDialog';
|
|
||||||
import BannerCarousel from '@/components/BannerCarousel';
|
|
||||||
import AIChatPanel from '@/components/AIChatPanel';
|
|
||||||
import FireworksCanvas from '@/components/FireworksCanvas';
|
|
||||||
|
|
||||||
// 首页模块配置接口
|
// 首页模块配置接口
|
||||||
interface HomeModule {
|
interface HomeModule {
|
||||||
@@ -151,7 +151,9 @@ function HomeClient() {
|
|||||||
const setCache = (key: string, data: any) => {
|
const setCache = (key: string, data: any) => {
|
||||||
try {
|
try {
|
||||||
localStorage.setItem(key, JSON.stringify({ data, timestamp: Date.now() }));
|
localStorage.setItem(key, JSON.stringify({ data, timestamp: Date.now() }));
|
||||||
} catch {}
|
} catch {
|
||||||
|
// Ignore localStorage errors
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const moviesCache = getCache('homepage_movies');
|
const moviesCache = getCache('homepage_movies');
|
||||||
|
|||||||
+55
-54
@@ -2,70 +2,69 @@
|
|||||||
|
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { Heart, Search, X, Cloud, Sparkles, AlertCircle } from 'lucide-react';
|
import { AlertCircle,Cloud, Heart, Sparkles, X } from 'lucide-react';
|
||||||
import { useRouter, useSearchParams } from 'next/navigation';
|
import { useRouter, useSearchParams } from 'next/navigation';
|
||||||
import { Suspense, useEffect, useRef, useState } from 'react';
|
import { Suspense, useEffect, useRef, useState } from 'react';
|
||||||
|
|
||||||
import { usePlaySync } from '@/hooks/usePlaySync';
|
|
||||||
import { getDoubanDetail } from '@/lib/douban.client';
|
|
||||||
import { useDownload } from '@/contexts/DownloadContext';
|
|
||||||
import { getAuthInfoFromBrowserCookie } from '@/lib/auth';
|
import { getAuthInfoFromBrowserCookie } from '@/lib/auth';
|
||||||
import { useSite } from '@/components/SiteProvider';
|
import {
|
||||||
|
convertDanmakuFormat,
|
||||||
|
getDanmakuById,
|
||||||
|
getDanmakuFromCache,
|
||||||
|
getEpisodes,
|
||||||
|
initDanmakuModule,
|
||||||
|
loadDanmakuDisplayState,
|
||||||
|
loadDanmakuSettings,
|
||||||
|
saveDanmakuDisplayState,
|
||||||
|
saveDanmakuSettings,
|
||||||
|
searchAnime,
|
||||||
|
} from '@/lib/danmaku/api';
|
||||||
|
import {
|
||||||
|
getDanmakuAnimeId,
|
||||||
|
getDanmakuSearchKeyword,
|
||||||
|
getDanmakuSourceIndex,
|
||||||
|
getManualDanmakuSelection,
|
||||||
|
saveDanmakuAnimeId,
|
||||||
|
saveDanmakuSearchKeyword,
|
||||||
|
saveDanmakuSourceIndex,
|
||||||
|
saveManualDanmakuSelection,
|
||||||
|
} from '@/lib/danmaku/selection-memory';
|
||||||
|
import type { DanmakuAnime, DanmakuComment,DanmakuSelection, DanmakuSettings } from '@/lib/danmaku/types';
|
||||||
import {
|
import {
|
||||||
deleteFavorite,
|
deleteFavorite,
|
||||||
deletePlayRecord,
|
deletePlayRecord,
|
||||||
deleteSkipConfig,
|
deleteSkipConfig,
|
||||||
generateStorageKey,
|
generateStorageKey,
|
||||||
getAllPlayRecords,
|
getAllPlayRecords,
|
||||||
|
getDanmakuFilterConfig,
|
||||||
|
getEpisodeFilterConfig,
|
||||||
getSkipConfig,
|
getSkipConfig,
|
||||||
isFavorited,
|
isFavorited,
|
||||||
saveFavorite,
|
saveFavorite,
|
||||||
savePlayRecord,
|
savePlayRecord,
|
||||||
saveSkipConfig,
|
saveSkipConfig,
|
||||||
subscribeToDataUpdates,
|
subscribeToDataUpdates,
|
||||||
getDanmakuFilterConfig,
|
|
||||||
getEpisodeFilterConfig,
|
|
||||||
} from '@/lib/db.client';
|
} from '@/lib/db.client';
|
||||||
import {
|
import { getDoubanDetail } from '@/lib/douban.client';
|
||||||
convertDanmakuFormat,
|
|
||||||
getDanmakuById,
|
|
||||||
getEpisodes,
|
|
||||||
loadDanmakuSettings,
|
|
||||||
saveDanmakuSettings,
|
|
||||||
searchAnime,
|
|
||||||
initDanmakuModule,
|
|
||||||
getDanmakuFromCache,
|
|
||||||
saveDanmakuDisplayState,
|
|
||||||
loadDanmakuDisplayState,
|
|
||||||
} from '@/lib/danmaku/api';
|
|
||||||
import {
|
|
||||||
getDanmakuSourceIndex,
|
|
||||||
saveDanmakuSourceIndex,
|
|
||||||
getManualDanmakuSelection,
|
|
||||||
saveManualDanmakuSelection,
|
|
||||||
saveDanmakuSearchKeyword,
|
|
||||||
getDanmakuSearchKeyword,
|
|
||||||
saveDanmakuAnimeId,
|
|
||||||
getDanmakuAnimeId,
|
|
||||||
} from '@/lib/danmaku/selection-memory';
|
|
||||||
import type { DanmakuAnime, DanmakuSelection, DanmakuSettings, DanmakuComment } from '@/lib/danmaku/types';
|
|
||||||
import { SearchResult, DanmakuFilterConfig, EpisodeFilterConfig } from '@/lib/types';
|
|
||||||
import { getVideoResolutionFromM3u8, processImageUrl } from '@/lib/utils';
|
|
||||||
import { getTMDBImageUrl } from '@/lib/tmdb.search';
|
import { getTMDBImageUrl } from '@/lib/tmdb.search';
|
||||||
|
import { DanmakuFilterConfig, EpisodeFilterConfig,SearchResult } from '@/lib/types';
|
||||||
import EpisodeSelector from '@/components/EpisodeSelector';
|
import { getVideoResolutionFromM3u8, processImageUrl } from '@/lib/utils';
|
||||||
import DownloadEpisodeSelector from '@/components/DownloadEpisodeSelector';
|
|
||||||
import PageLayout from '@/components/PageLayout';
|
|
||||||
import DoubanComments from '@/components/DoubanComments';
|
|
||||||
import SmartRecommendations from '@/components/SmartRecommendations';
|
|
||||||
import DanmakuFilterSettings from '@/components/DanmakuFilterSettings';
|
|
||||||
import Toast, { ToastProps } from '@/components/Toast';
|
|
||||||
import AIChatPanel from '@/components/AIChatPanel';
|
|
||||||
import { useEnableComments } from '@/hooks/useEnableComments';
|
import { useEnableComments } from '@/hooks/useEnableComments';
|
||||||
import PansouSearch from '@/components/PansouSearch';
|
import { usePlaySync } from '@/hooks/usePlaySync';
|
||||||
import CustomHeatmap from '@/components/CustomHeatmap';
|
|
||||||
|
import AIChatPanel from '@/components/AIChatPanel';
|
||||||
import CorrectDialog from '@/components/CorrectDialog';
|
import CorrectDialog from '@/components/CorrectDialog';
|
||||||
|
import DanmakuFilterSettings from '@/components/DanmakuFilterSettings';
|
||||||
|
import DoubanComments from '@/components/DoubanComments';
|
||||||
|
import DownloadEpisodeSelector from '@/components/DownloadEpisodeSelector';
|
||||||
|
import EpisodeSelector from '@/components/EpisodeSelector';
|
||||||
|
import PageLayout from '@/components/PageLayout';
|
||||||
|
import PansouSearch from '@/components/PansouSearch';
|
||||||
|
import { useSite } from '@/components/SiteProvider';
|
||||||
|
import SmartRecommendations from '@/components/SmartRecommendations';
|
||||||
|
import Toast, { ToastProps } from '@/components/Toast';
|
||||||
|
|
||||||
|
import { useDownload } from '@/contexts/DownloadContext';
|
||||||
|
|
||||||
// 扩展 HTMLVideoElement 类型以支持 hls 属性
|
// 扩展 HTMLVideoElement 类型以支持 hls 属性
|
||||||
declare global {
|
declare global {
|
||||||
@@ -545,7 +544,7 @@ function PlayPageClient() {
|
|||||||
// 只有用户主动点击推荐时才会添加 _reload 参数
|
// 只有用户主动点击推荐时才会添加 _reload 参数
|
||||||
if (reloadParam && urlTitle && urlTitle !== videoTitle && !isSourceChangingRef.current) {
|
if (reloadParam && urlTitle && urlTitle !== videoTitle && !isSourceChangingRef.current) {
|
||||||
console.log('[PlayPage] User clicked recommendation, reloading page');
|
console.log('[PlayPage] User clicked recommendation, reloading page');
|
||||||
window.location.href = window.location.href;
|
window.location.reload();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 重置换源标记
|
// 重置换源标记
|
||||||
@@ -1526,7 +1525,7 @@ function PlayPageClient() {
|
|||||||
const refreshXiaoyaUrl = async (
|
const refreshXiaoyaUrl = async (
|
||||||
hls: any,
|
hls: any,
|
||||||
video: HTMLVideoElement,
|
video: HTMLVideoElement,
|
||||||
isScheduled: boolean = false
|
isScheduled = false
|
||||||
) => {
|
) => {
|
||||||
// 防抖:距离上次刷新不足3秒则不刷新
|
// 防抖:距离上次刷新不足3秒则不刷新
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
@@ -3911,7 +3910,7 @@ function PlayPageClient() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 处理弹幕选择
|
// 处理弹幕选择
|
||||||
const handleDanmakuSelect = async (selection: DanmakuSelection, isManual: boolean = false) => {
|
const handleDanmakuSelect = async (selection: DanmakuSelection, isManual = false) => {
|
||||||
console.log(`[弹幕选择] isManual=${isManual}, selection:`, selection);
|
console.log(`[弹幕选择] isManual=${isManual}, selection:`, selection);
|
||||||
setCurrentDanmakuSelection(selection);
|
setCurrentDanmakuSelection(selection);
|
||||||
|
|
||||||
@@ -3944,7 +3943,7 @@ function PlayPageClient() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 处理用户选择弹幕源
|
// 处理用户选择弹幕源
|
||||||
const handleDanmakuSourceSelect = async (selectedAnime: DanmakuAnime, selectedIndex?: number, isManualSearch: boolean = false) => {
|
const handleDanmakuSourceSelect = async (selectedAnime: DanmakuAnime, selectedIndex?: number, isManualSearch = false) => {
|
||||||
setShowDanmakuSourceSelector(false);
|
setShowDanmakuSourceSelector(false);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -4863,12 +4862,14 @@ function PlayPageClient() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// 检查其他 HTTP 错误状态码
|
// 检查其他 HTTP 错误状态码
|
||||||
const statusCode = data.response?.code || data.response?.status;
|
{
|
||||||
if (statusCode && statusCode >= 400) {
|
const statusCode = data.response?.code || data.response?.status;
|
||||||
console.log(`HTTP ${statusCode} 错误`);
|
if (statusCode && statusCode >= 400) {
|
||||||
hls.destroy();
|
console.log(`HTTP ${statusCode} 错误`);
|
||||||
setVideoError(`HTTP ${statusCode} 错误`);
|
hls.destroy();
|
||||||
return;
|
setVideoError(`HTTP ${statusCode} 错误`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
console.log('网络错误,尝试恢复...');
|
console.log('网络错误,尝试恢复...');
|
||||||
hls.startLoad();
|
hls.startLoad();
|
||||||
|
|||||||
@@ -2,15 +2,16 @@
|
|||||||
|
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
import { ArrowDownWideNarrow, ArrowUpNarrowWide,Film } from 'lucide-react';
|
||||||
import { useRouter, useSearchParams } from 'next/navigation';
|
import { useRouter, useSearchParams } from 'next/navigation';
|
||||||
import { useEffect, useState, useRef, useMemo } from 'react';
|
import { useEffect, useMemo,useRef, useState } from 'react';
|
||||||
import { Film, ArrowUpDown, ArrowDownWideNarrow, ArrowUpNarrowWide } from 'lucide-react';
|
|
||||||
import { createPortal } from 'react-dom';
|
import { createPortal } from 'react-dom';
|
||||||
|
|
||||||
|
import { base58Encode } from '@/lib/utils';
|
||||||
|
|
||||||
import CapsuleSwitch from '@/components/CapsuleSwitch';
|
import CapsuleSwitch from '@/components/CapsuleSwitch';
|
||||||
import PageLayout from '@/components/PageLayout';
|
import PageLayout from '@/components/PageLayout';
|
||||||
import VideoCard from '@/components/VideoCard';
|
import VideoCard from '@/components/VideoCard';
|
||||||
import { base58Encode } from '@/lib/utils';
|
|
||||||
|
|
||||||
type LibrarySourceType = 'openlist' | 'emby' | 'xiaoya' | `emby:${string}` | `emby_${string}`;
|
type LibrarySourceType = 'openlist' | 'emby' | 'xiaoya' | `emby:${string}` | `emby_${string}`;
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps, @typescript-eslint/no-explicit-any,@typescript-eslint/no-non-null-assertion,no-empty */
|
/* eslint-disable react-hooks/exhaustive-deps, @typescript-eslint/no-explicit-any,@typescript-eslint/no-non-null-assertion,no-empty */
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { ChevronUp, RefreshCw, Search, X, Film, HardDrive, Magnet } from 'lucide-react';
|
import { ChevronUp, Film, HardDrive, Magnet,RefreshCw, Search, X } from 'lucide-react';
|
||||||
import { useRouter, useSearchParams } from 'next/navigation';
|
import { useRouter, useSearchParams } from 'next/navigation';
|
||||||
import React, { startTransition, Suspense, useEffect, useMemo, useRef, useState } from 'react';
|
import React, { startTransition, Suspense, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
|
||||||
|
import { getAuthInfoFromBrowserCookie } from '@/lib/auth';
|
||||||
import {
|
import {
|
||||||
addSearchHistory,
|
addSearchHistory,
|
||||||
clearSearchHistory,
|
clearSearchHistory,
|
||||||
@@ -13,15 +14,14 @@ import {
|
|||||||
subscribeToDataUpdates,
|
subscribeToDataUpdates,
|
||||||
} from '@/lib/db.client';
|
} from '@/lib/db.client';
|
||||||
import { SearchResult } from '@/lib/types';
|
import { SearchResult } from '@/lib/types';
|
||||||
import { getAuthInfoFromBrowserCookie } from '@/lib/auth';
|
|
||||||
|
|
||||||
|
import AcgSearch from '@/components/AcgSearch';
|
||||||
|
import CapsuleSwitch from '@/components/CapsuleSwitch';
|
||||||
import PageLayout from '@/components/PageLayout';
|
import PageLayout from '@/components/PageLayout';
|
||||||
|
import PansouSearch from '@/components/PansouSearch';
|
||||||
import SearchResultFilter, { SearchFilterCategory } from '@/components/SearchResultFilter';
|
import SearchResultFilter, { SearchFilterCategory } from '@/components/SearchResultFilter';
|
||||||
import SearchSuggestions from '@/components/SearchSuggestions';
|
import SearchSuggestions from '@/components/SearchSuggestions';
|
||||||
import VideoCard, { VideoCardHandle } from '@/components/VideoCard';
|
import VideoCard, { VideoCardHandle } from '@/components/VideoCard';
|
||||||
import PansouSearch from '@/components/PansouSearch';
|
|
||||||
import AcgSearch from '@/components/AcgSearch';
|
|
||||||
import CapsuleSwitch from '@/components/CapsuleSwitch';
|
|
||||||
|
|
||||||
function SearchPageClient() {
|
function SearchPageClient() {
|
||||||
// 搜索历史
|
// 搜索历史
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
// 观影室首页 - 选项卡式界面
|
// 观影室首页 - 选项卡式界面
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState, useEffect } from 'react';
|
import { List as ListIcon, Lock, RefreshCw,UserPlus, Users } from 'lucide-react';
|
||||||
import { Users, UserPlus, List as ListIcon, Lock, RefreshCw } from 'lucide-react';
|
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { useWatchRoomContext } from '@/components/WatchRoomProvider';
|
import { useEffect,useState } from 'react';
|
||||||
import PageLayout from '@/components/PageLayout';
|
|
||||||
import { getAuthInfoFromBrowserCookie } from '@/lib/auth';
|
import { getAuthInfoFromBrowserCookie } from '@/lib/auth';
|
||||||
|
|
||||||
|
import PageLayout from '@/components/PageLayout';
|
||||||
|
import { useWatchRoomContext } from '@/components/WatchRoomProvider';
|
||||||
|
|
||||||
import type { Room } from '@/types/watch-room';
|
import type { Room } from '@/types/watch-room';
|
||||||
|
|
||||||
type TabType = 'create' | 'join' | 'list';
|
type TabType = 'create' | 'join' | 'list';
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
import { AlertTriangle,Radio } from 'lucide-react';
|
||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
|
|
||||||
import PageLayout from '@/components/PageLayout';
|
import PageLayout from '@/components/PageLayout';
|
||||||
import { Radio, AlertTriangle } from 'lucide-react';
|
|
||||||
import Head from 'next/head';
|
|
||||||
|
|
||||||
let Artplayer: any = null;
|
let Artplayer: any = null;
|
||||||
let Hls: any = null;
|
let Hls: any = null;
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import React, { useState, useRef, useEffect } from 'react';
|
import { Bot, Loader2, Send, Sparkles, Trash2,X } from 'lucide-react';
|
||||||
|
import React, { useEffect,useRef, useState } from 'react';
|
||||||
import { createPortal } from 'react-dom';
|
import { createPortal } from 'react-dom';
|
||||||
import { X, Send, Bot, Loader2, Sparkles, Trash2 } from 'lucide-react';
|
|
||||||
import ReactMarkdown from 'react-markdown';
|
import ReactMarkdown from 'react-markdown';
|
||||||
import remarkGfm from 'remark-gfm';
|
import remarkGfm from 'remark-gfm';
|
||||||
|
|
||||||
import { VideoContext } from '@/lib/ai-orchestrator';
|
import { VideoContext } from '@/lib/ai-orchestrator';
|
||||||
|
|
||||||
interface ChatMessage {
|
interface ChatMessage {
|
||||||
|
|||||||
@@ -2,10 +2,10 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { AlertCircle, Download, ExternalLink, Loader2 } from 'lucide-react';
|
import { AlertCircle, Download, ExternalLink, Loader2 } from 'lucide-react';
|
||||||
import { useEffect, useState, useRef, useCallback } from 'react';
|
import { useCallback,useEffect, useRef, useState } from 'react';
|
||||||
|
|
||||||
import Toast, { ToastProps } from '@/components/Toast';
|
|
||||||
import CapsuleSwitch from '@/components/CapsuleSwitch';
|
import CapsuleSwitch from '@/components/CapsuleSwitch';
|
||||||
|
import Toast, { ToastProps } from '@/components/Toast';
|
||||||
|
|
||||||
interface AcgSearchItem {
|
interface AcgSearchItem {
|
||||||
title: string;
|
title: string;
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useEffect, useState, useCallback, useRef } from 'react';
|
import { ChevronLeft, ChevronRight, Play } from 'lucide-react';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { getTMDBImageUrl, getGenreNames, type TMDBItem } from '@/lib/tmdb.client';
|
import { useCallback, useEffect, useRef,useState } from 'react';
|
||||||
|
|
||||||
|
import { type TMDBItem,getGenreNames, getTMDBImageUrl } from '@/lib/tmdb.client';
|
||||||
import { processImageUrl } from '@/lib/utils';
|
import { processImageUrl } from '@/lib/utils';
|
||||||
import { ChevronLeft, ChevronRight, Play } from 'lucide-react';
|
|
||||||
|
|
||||||
interface BannerCarouselProps {
|
interface BannerCarouselProps {
|
||||||
autoPlayInterval?: number; // 自动播放间隔(毫秒)
|
autoPlayInterval?: number; // 自动播放间隔(毫秒)
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
/* eslint-disable no-console */
|
/* eslint-disable no-console */
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
import { AlertTriangle } from 'lucide-react';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { createPortal } from 'react-dom';
|
import { createPortal } from 'react-dom';
|
||||||
import { AlertTriangle } from 'lucide-react';
|
|
||||||
|
|
||||||
import type { PlayRecord } from '@/lib/db.client';
|
import type { PlayRecord } from '@/lib/db.client';
|
||||||
import {
|
import {
|
||||||
|
|||||||
@@ -225,7 +225,7 @@ export default function CorrectDialog({
|
|||||||
try {
|
try {
|
||||||
// 构建标题和ID:如果是第二季及以后,在标题后加上季度名称,并使用季度ID
|
// 构建标题和ID:如果是第二季及以后,在标题后加上季度名称,并使用季度ID
|
||||||
let finalTitle = result.title || result.name;
|
let finalTitle = result.title || result.name;
|
||||||
let finalTmdbId = result.id;
|
const finalTmdbId = result.id;
|
||||||
|
|
||||||
if (season && season.season_number > 1) {
|
if (season && season.season_number > 1) {
|
||||||
finalTitle = `${finalTitle} ${season.name}`;
|
finalTitle = `${finalTitle} ${season.name}`;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import React, { useEffect, useRef, useState, useCallback } from 'react';
|
import React, { useCallback,useEffect, useRef, useState } from 'react';
|
||||||
|
|
||||||
interface DanmakuData {
|
interface DanmakuData {
|
||||||
time: number;
|
time: number;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
import { initDanmakuModule } from '@/lib/danmaku/api';
|
import { initDanmakuModule } from '@/lib/danmaku/api';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState, useEffect, useRef } from 'react';
|
import { Plus, ToggleLeft, ToggleRight,Trash2, X } from 'lucide-react';
|
||||||
import { X, Plus, Trash2, ToggleLeft, ToggleRight } from 'lucide-react';
|
import { useEffect, useRef,useState } from 'react';
|
||||||
import { DanmakuFilterConfig, DanmakuFilterRule } from '@/lib/types';
|
|
||||||
import { getDanmakuFilterConfig, saveDanmakuFilterConfig } from '@/lib/db.client';
|
import { getDanmakuFilterConfig, saveDanmakuFilterConfig } from '@/lib/db.client';
|
||||||
|
import { DanmakuFilterConfig, DanmakuFilterRule } from '@/lib/types';
|
||||||
|
|
||||||
interface DanmakuFilterSettingsProps {
|
interface DanmakuFilterSettingsProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { MagnifyingGlassIcon } from '@heroicons/react/24/outline';
|
import { MagnifyingGlassIcon } from '@heroicons/react/24/outline';
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
|
|
||||||
import { getEpisodes, searchAnime } from '@/lib/danmaku/api';
|
import { getEpisodes, searchAnime } from '@/lib/danmaku/api';
|
||||||
import type {
|
import type {
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { X, Calendar, Star, Clock, Tag, Users, Globe, Film } from 'lucide-react';
|
import { Calendar, Clock, Film,Globe, Star, Tag, Users, X } from 'lucide-react';
|
||||||
import Image from 'next/image';
|
import Image from 'next/image';
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect, useState } from 'react';
|
||||||
import { createPortal } from 'react-dom';
|
import { createPortal } from 'react-dom';
|
||||||
|
|
||||||
import { getTMDBImageUrl } from '@/lib/tmdb.client';
|
import { getTMDBImageUrl } from '@/lib/tmdb.client';
|
||||||
import { processImageUrl } from '@/lib/utils';
|
import { processImageUrl } from '@/lib/utils';
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useEffect, useState, useCallback } from 'react';
|
import { useCallback,useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { useEnableComments } from '@/hooks/useEnableComments';
|
import { useEnableComments } from '@/hooks/useEnableComments';
|
||||||
|
|
||||||
interface DoubanComment {
|
interface DoubanComment {
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useEffect, useState, useCallback } from 'react';
|
import { useCallback,useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { useEnableComments } from '@/hooks/useEnableComments';
|
import { useEnableComments } from '@/hooks/useEnableComments';
|
||||||
import VideoCard from '@/components/VideoCard';
|
|
||||||
import ScrollableRow from '@/components/ScrollableRow';
|
import ScrollableRow from '@/components/ScrollableRow';
|
||||||
|
import VideoCard from '@/components/VideoCard';
|
||||||
|
|
||||||
interface DoubanRecommendation {
|
interface DoubanRecommendation {
|
||||||
doubanId: string;
|
doubanId: string;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { useDownload } from '@/contexts/DownloadContext';
|
import { useDownload } from '@/contexts/DownloadContext';
|
||||||
|
|
||||||
export function DownloadBubble() {
|
export function DownloadBubble() {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/* eslint-disable @next/next/no-img-element */
|
/* eslint-disable @next/next/no-img-element */
|
||||||
|
|
||||||
import React, { useState, useMemo } from 'react';
|
import React, { useMemo,useState } from 'react';
|
||||||
|
|
||||||
interface DownloadEpisodeSelectorProps {
|
interface DownloadEpisodeSelectorProps {
|
||||||
/** 是否显示弹窗 */
|
/** 是否显示弹窗 */
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useDownload } from '@/contexts/DownloadContext';
|
|
||||||
import { M3U8DownloadTask } from '@/lib/m3u8-downloader';
|
import { M3U8DownloadTask } from '@/lib/m3u8-downloader';
|
||||||
|
|
||||||
|
import { useDownload } from '@/contexts/DownloadContext';
|
||||||
|
|
||||||
export function DownloadPanel() {
|
export function DownloadPanel() {
|
||||||
const { tasks, showDownloadPanel, setShowDownloadPanel, startTask, pauseTask, cancelTask, retryFailedSegments, getProgress } = useDownload();
|
const { tasks, showDownloadPanel, setShowDownloadPanel, startTask, pauseTask, cancelTask, retryFailedSegments, getProgress } = useDownload();
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
|
|
||||||
import { Clock, Target, Tv, List, BarChart3 } from 'lucide-react';
|
import { BarChart3,Clock, List, Target, Tv } from 'lucide-react';
|
||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
|
|
||||||
import { formatTimeToHHMM, parseCustomTimeFormat } from '@/lib/time';
|
import { formatTimeToHHMM, parseCustomTimeFormat } from '@/lib/time';
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState, useEffect, useRef } from 'react';
|
import { Plus, ToggleLeft, ToggleRight,Trash2, X } from 'lucide-react';
|
||||||
import { X, Plus, Trash2, ToggleLeft, ToggleRight } from 'lucide-react';
|
import { useEffect, useRef,useState } from 'react';
|
||||||
import { EpisodeFilterConfig, EpisodeFilterRule } from '@/lib/types';
|
|
||||||
import { getEpisodeFilterConfig, saveEpisodeFilterConfig } from '@/lib/db.client';
|
import { getEpisodeFilterConfig, saveEpisodeFilterConfig } from '@/lib/db.client';
|
||||||
|
import { EpisodeFilterConfig, EpisodeFilterRule } from '@/lib/types';
|
||||||
|
|
||||||
interface EpisodeFilterSettingsProps {
|
interface EpisodeFilterSettingsProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
/* eslint-disable @next/next/no-img-element */
|
/* eslint-disable @next/next/no-img-element */
|
||||||
|
|
||||||
|
import { Settings } from 'lucide-react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import React, {
|
import React, {
|
||||||
useCallback,
|
useCallback,
|
||||||
@@ -8,13 +9,13 @@ import React, {
|
|||||||
useRef,
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import { Settings } from 'lucide-react';
|
|
||||||
|
import type { DanmakuComment,DanmakuSelection } from '@/lib/danmaku/types';
|
||||||
|
import { EpisodeFilterConfig,SearchResult } from '@/lib/types';
|
||||||
|
import { getVideoResolutionFromM3u8, processImageUrl } from '@/lib/utils';
|
||||||
|
|
||||||
import DanmakuPanel from '@/components/DanmakuPanel';
|
import DanmakuPanel from '@/components/DanmakuPanel';
|
||||||
import EpisodeFilterSettings from '@/components/EpisodeFilterSettings';
|
import EpisodeFilterSettings from '@/components/EpisodeFilterSettings';
|
||||||
import type { DanmakuSelection, DanmakuComment } from '@/lib/danmaku/types';
|
|
||||||
import { SearchResult, EpisodeFilterConfig } from '@/lib/types';
|
|
||||||
import { getVideoResolutionFromM3u8, processImageUrl } from '@/lib/utils';
|
|
||||||
|
|
||||||
// 定义视频信息类型
|
// 定义视频信息类型
|
||||||
interface VideoInfo {
|
interface VideoInfo {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { Star, X, AlertTriangle } from 'lucide-react';
|
import { AlertTriangle,Star, X } from 'lucide-react';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { createPortal } from 'react-dom';
|
import { createPortal } from 'react-dom';
|
||||||
|
|
||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
getAllPlayRecords,
|
getAllPlayRecords,
|
||||||
subscribeToDataUpdates,
|
subscribeToDataUpdates,
|
||||||
} from '@/lib/db.client';
|
} from '@/lib/db.client';
|
||||||
|
|
||||||
import VideoCard from '@/components/VideoCard';
|
import VideoCard from '@/components/VideoCard';
|
||||||
|
|
||||||
interface FavoriteItem {
|
interface FavoriteItem {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import { AlertTriangle } from 'lucide-react';
|
import { AlertTriangle } from 'lucide-react';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { createPortal } from 'react-dom';
|
import { createPortal } from 'react-dom';
|
||||||
|
|
||||||
import { getAuthInfoFromBrowserCookie } from '@/lib/auth';
|
import { getAuthInfoFromBrowserCookie } from '@/lib/auth';
|
||||||
|
|
||||||
interface HttpWarningDialogProps {
|
interface HttpWarningDialogProps {
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ const MobileBottomNav = ({ activePath }: MobileBottomNavProps) => {
|
|||||||
const runtimeConfig = (window as any).RUNTIME_CONFIG;
|
const runtimeConfig = (window as any).RUNTIME_CONFIG;
|
||||||
|
|
||||||
// 基础导航项(不包括观影室)
|
// 基础导航项(不包括观影室)
|
||||||
let items = [
|
const items = [
|
||||||
{ icon: Home, label: '首页', href: '/' },
|
{ icon: Home, label: '首页', href: '/' },
|
||||||
{
|
{
|
||||||
icon: Film,
|
icon: Film,
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { Bell, Check, Trash2, X } from 'lucide-react';
|
import { Bell, Check, Trash2, X } from 'lucide-react';
|
||||||
import { useEffect, useState } from 'react';
|
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { Notification } from '@/lib/types';
|
import { Notification } from '@/lib/types';
|
||||||
|
|
||||||
|
|||||||
@@ -158,7 +158,7 @@ const Sidebar = ({ onToggle, activePath = '/' }: SidebarProps) => {
|
|||||||
const runtimeConfig = (window as any).RUNTIME_CONFIG;
|
const runtimeConfig = (window as any).RUNTIME_CONFIG;
|
||||||
|
|
||||||
// 基础菜单项(不包括观影室)
|
// 基础菜单项(不包括观影室)
|
||||||
let items = [
|
const items = [
|
||||||
{
|
{
|
||||||
icon: Film,
|
icon: Film,
|
||||||
label: '电影',
|
label: '电影',
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useEffect, useState, useCallback } from 'react';
|
import { useCallback,useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { useEnableComments } from '@/hooks/useEnableComments';
|
import { useEnableComments } from '@/hooks/useEnableComments';
|
||||||
import { useRecommendationDataSource } from '@/hooks/useRecommendationDataSource';
|
import { useRecommendationDataSource } from '@/hooks/useRecommendationDataSource';
|
||||||
import VideoCard from '@/components/VideoCard';
|
|
||||||
import ScrollableRow from '@/components/ScrollableRow';
|
import ScrollableRow from '@/components/ScrollableRow';
|
||||||
|
import VideoCard from '@/components/VideoCard';
|
||||||
|
|
||||||
interface Recommendation {
|
interface Recommendation {
|
||||||
doubanId?: string;
|
doubanId?: string;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
import { CheckCircle, Info, X,XCircle } from 'lucide-react';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { CheckCircle, XCircle, Info, X } from 'lucide-react';
|
|
||||||
|
|
||||||
export interface ToastProps {
|
export interface ToastProps {
|
||||||
message: string;
|
message: string;
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useEffect, useRef } from 'react';
|
import { usePathname, useRouter,useSearchParams } from 'next/navigation';
|
||||||
import { usePathname, useSearchParams, useRouter } from 'next/navigation';
|
|
||||||
import NProgress from 'nprogress';
|
import NProgress from 'nprogress';
|
||||||
|
import { useEffect, useRef } from 'react';
|
||||||
|
|
||||||
// 创建全局钩子来拦截 router
|
// 创建全局钩子来拦截 router
|
||||||
let globalRouterRef: any = null;
|
let globalRouterRef: any = null;
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ import {
|
|||||||
X,
|
X,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { createPortal } from 'react-dom';
|
import { createPortal } from 'react-dom';
|
||||||
|
|
||||||
import { getAuthInfoFromBrowserCookie } from '@/lib/auth';
|
import { getAuthInfoFromBrowserCookie } from '@/lib/auth';
|
||||||
@@ -38,11 +38,11 @@ import { clearAllDanmakuCache } from '@/lib/danmaku/api';
|
|||||||
import { CURRENT_VERSION } from '@/lib/version';
|
import { CURRENT_VERSION } from '@/lib/version';
|
||||||
import { UpdateStatus } from '@/lib/version_check';
|
import { UpdateStatus } from '@/lib/version_check';
|
||||||
|
|
||||||
|
import { FavoritesPanel } from './FavoritesPanel';
|
||||||
|
import { NotificationPanel } from './NotificationPanel';
|
||||||
|
import { OfflineDownloadPanel } from './OfflineDownloadPanel';
|
||||||
import { useVersionCheck } from './VersionCheckProvider';
|
import { useVersionCheck } from './VersionCheckProvider';
|
||||||
import { VersionPanel } from './VersionPanel';
|
import { VersionPanel } from './VersionPanel';
|
||||||
import { OfflineDownloadPanel } from './OfflineDownloadPanel';
|
|
||||||
import { NotificationPanel } from './NotificationPanel';
|
|
||||||
import { FavoritesPanel } from './FavoritesPanel';
|
|
||||||
|
|
||||||
interface AuthInfo {
|
interface AuthInfo {
|
||||||
username?: string;
|
username?: string;
|
||||||
|
|||||||
@@ -24,10 +24,10 @@ import {
|
|||||||
import { processImageUrl } from '@/lib/utils';
|
import { processImageUrl } from '@/lib/utils';
|
||||||
import { useLongPress } from '@/hooks/useLongPress';
|
import { useLongPress } from '@/hooks/useLongPress';
|
||||||
|
|
||||||
import { ImagePlaceholder } from '@/components/ImagePlaceholder';
|
|
||||||
import MobileActionSheet from '@/components/MobileActionSheet';
|
|
||||||
import AIChatPanel from '@/components/AIChatPanel';
|
import AIChatPanel from '@/components/AIChatPanel';
|
||||||
import DetailPanel from '@/components/DetailPanel';
|
import DetailPanel from '@/components/DetailPanel';
|
||||||
|
import { ImagePlaceholder } from '@/components/ImagePlaceholder';
|
||||||
|
import MobileActionSheet from '@/components/MobileActionSheet';
|
||||||
|
|
||||||
export interface VideoCardProps {
|
export interface VideoCardProps {
|
||||||
id?: string;
|
id?: string;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useRef, useEffect, useState } from 'react';
|
|
||||||
import { ChevronLeft, ChevronRight } from 'lucide-react';
|
import { ChevronLeft, ChevronRight } from 'lucide-react';
|
||||||
|
import { useEffect, useRef, useState } from 'react';
|
||||||
|
|
||||||
interface VirtualScrollableRowProps {
|
interface VirtualScrollableRowProps {
|
||||||
children: React.ReactNode[];
|
children: React.ReactNode[];
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
// WatchRoom 全局状态管理 Provider
|
// WatchRoom 全局状态管理 Provider
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import React, { createContext, useContext, useEffect, useState, useCallback } from 'react';
|
import React, { createContext, useCallback,useContext, useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { useWatchRoom } from '@/hooks/useWatchRoom';
|
import { useWatchRoom } from '@/hooks/useWatchRoom';
|
||||||
import type { Room, Member, ChatMessage, WatchRoomConfig } from '@/types/watch-room';
|
|
||||||
import Toast, { ToastProps } from '@/components/Toast';
|
import Toast, { ToastProps } from '@/components/Toast';
|
||||||
|
|
||||||
|
import type { ChatMessage, Member, Room, WatchRoomConfig } from '@/types/watch-room';
|
||||||
|
|
||||||
// Import type from watch-room-socket
|
// Import type from watch-room-socket
|
||||||
type WatchRoomSocket = import('@/lib/watch-room-socket').WatchRoomSocket;
|
type WatchRoomSocket = import('@/lib/watch-room-socket').WatchRoomSocket;
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
// 全局聊天悬浮窗和房间信息按钮
|
// 全局聊天悬浮窗和房间信息按钮
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState, useEffect, useRef } from 'react';
|
import { AlertCircle,Info, LogOut, Maximize2, MessageCircle, Mic, MicOff, Minimize2, Send, Smile, Users, Volume2, VolumeX, X, XCircle } from 'lucide-react';
|
||||||
import { MessageCircle, X, Send, Smile, Minimize2, Maximize2, Info, Users, LogOut, XCircle, Mic, MicOff, Volume2, VolumeX, AlertCircle } from 'lucide-react';
|
import { useEffect, useRef,useState } from 'react';
|
||||||
import { useWatchRoomContextSafe } from '@/components/WatchRoomProvider';
|
|
||||||
import { useVoiceChat } from '@/hooks/useVoiceChat';
|
import { useVoiceChat } from '@/hooks/useVoiceChat';
|
||||||
|
|
||||||
|
import { useWatchRoomContextSafe } from '@/components/WatchRoomProvider';
|
||||||
|
|
||||||
const EMOJI_LIST = ['😀', '😂', '😍', '🥰', '😎', '🤔', '👍', '👏', '🎉', '❤️', '🔥', '⭐'];
|
const EMOJI_LIST = ['😀', '😂', '😍', '🥰', '😎', '🤔', '👍', '👏', '🎉', '❤️', '🔥', '⭐'];
|
||||||
|
|
||||||
export default function ChatFloatingWindow() {
|
export default function ChatFloatingWindow() {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import React, { createContext, useContext, useState, useCallback, useEffect } from 'react';
|
import React, { createContext, useCallback, useContext,useState } from 'react';
|
||||||
|
|
||||||
import { M3U8Downloader, M3U8DownloadTask } from '@/lib/m3u8-downloader';
|
import { M3U8Downloader, M3U8DownloadTask } from '@/lib/m3u8-downloader';
|
||||||
|
|
||||||
interface DownloadContextType {
|
interface DownloadContextType {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState, useEffect } from 'react';
|
import { useEffect,useState } from 'react';
|
||||||
|
|
||||||
interface RuntimeConfig {
|
interface RuntimeConfig {
|
||||||
EnableComments: boolean;
|
EnableComments: boolean;
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
// React Hook for Live Page Synchronization
|
// React Hook for Live Page Synchronization
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useEffect, useRef, useCallback } from 'react';
|
import { useCallback,useEffect, useRef } from 'react';
|
||||||
|
|
||||||
import { useWatchRoomContextSafe } from '@/components/WatchRoomProvider';
|
import { useWatchRoomContextSafe } from '@/components/WatchRoomProvider';
|
||||||
|
|
||||||
import type { LiveState } from '@/types/watch-room';
|
import type { LiveState } from '@/types/watch-room';
|
||||||
|
|
||||||
interface UseLiveSyncOptions {
|
interface UseLiveSyncOptions {
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
// React Hook for Play Page Synchronization
|
// React Hook for Play Page Synchronization
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useEffect, useRef, useCallback } from 'react';
|
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
|
import { useCallback,useEffect, useRef } from 'react';
|
||||||
|
|
||||||
import { useWatchRoomContextSafe } from '@/components/WatchRoomProvider';
|
import { useWatchRoomContextSafe } from '@/components/WatchRoomProvider';
|
||||||
|
|
||||||
import type { PlayState } from '@/types/watch-room';
|
import type { PlayState } from '@/types/watch-room';
|
||||||
|
|
||||||
interface UsePlaySyncOptions {
|
interface UsePlaySyncOptions {
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
// React Hook for Voice Chat in Watch Room
|
// React Hook for Voice Chat in Watch Room
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useEffect, useRef, useCallback, useState } from 'react';
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
|
|
||||||
import type { WatchRoomSocket } from '@/lib/watch-room-socket';
|
import type { WatchRoomSocket } from '@/lib/watch-room-socket';
|
||||||
|
|
||||||
import type { Member } from '@/types/watch-room';
|
import type { Member } from '@/types/watch-room';
|
||||||
|
|
||||||
interface UseVoiceChatOptions {
|
interface UseVoiceChatOptions {
|
||||||
@@ -520,7 +522,7 @@ export function useVoiceChat({
|
|||||||
}, [switchToServerRelay]);
|
}, [switchToServerRelay]);
|
||||||
|
|
||||||
// 播放服务器中转的音频 - 使用Web Audio API播放PCM数据
|
// 播放服务器中转的音频 - 使用Web Audio API播放PCM数据
|
||||||
const playServerRelayAudio = useCallback(async (userId: string, audioData: number[], sampleRate: number = 16000) => {
|
const playServerRelayAudio = useCallback(async (userId: string, audioData: number[], sampleRate = 16000) => {
|
||||||
if (!isSpeakerEnabled) return;
|
if (!isSpeakerEnabled) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -1,16 +1,18 @@
|
|||||||
// React Hook for Watch Room
|
// React Hook for Watch Room
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useEffect, useState, useCallback, useRef } from 'react';
|
import { useCallback, useEffect, useRef,useState } from 'react';
|
||||||
import { watchRoomSocketManager, type WatchRoomSocket } from '@/lib/watch-room-socket';
|
|
||||||
|
import { type WatchRoomSocket,watchRoomSocketManager } from '@/lib/watch-room-socket';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
Room,
|
ChatMessage,
|
||||||
|
LiveState,
|
||||||
Member,
|
Member,
|
||||||
PlayState,
|
PlayState,
|
||||||
LiveState,
|
Room,
|
||||||
ChatMessage,
|
|
||||||
WatchRoomConfig,
|
|
||||||
StoredRoomInfo,
|
StoredRoomInfo,
|
||||||
|
WatchRoomConfig,
|
||||||
} from '@/types/watch-room';
|
} from '@/types/watch-room';
|
||||||
|
|
||||||
const STORAGE_KEY = 'watch_room_info';
|
const STORAGE_KEY = 'watch_room_info';
|
||||||
|
|||||||
@@ -139,7 +139,7 @@ export class AESDecryptor {
|
|||||||
const invSubMix2 = invSubMix[2];
|
const invSubMix2 = invSubMix[2];
|
||||||
const invSubMix3 = invSubMix[3];
|
const invSubMix3 = invSubMix[3];
|
||||||
|
|
||||||
let prev: number = 0;
|
let prev = 0;
|
||||||
let t: number;
|
let t: number;
|
||||||
|
|
||||||
for (ksRow = 0; ksRow < ksRows; ksRow++) {
|
for (ksRow = 0; ksRow < ksRows; ksRow++) {
|
||||||
|
|||||||
@@ -5,7 +5,6 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { fetchDoubanData as fetchDoubanAPI } from '@/lib/douban';
|
import { fetchDoubanData as fetchDoubanAPI } from '@/lib/douban';
|
||||||
import { searchTMDB, getTVSeasons } from '@/lib/tmdb.search';
|
|
||||||
import { getNextApiKey } from '@/lib/tmdb.client';
|
import { getNextApiKey } from '@/lib/tmdb.client';
|
||||||
|
|
||||||
export interface VideoContext {
|
export interface VideoContext {
|
||||||
|
|||||||
+10
-12
@@ -1,6 +1,14 @@
|
|||||||
// 弹幕 API 服务封装(通过本地代理转发)
|
// 弹幕 API 服务封装(通过本地代理转发)
|
||||||
|
import {
|
||||||
|
clearAllDanmakuCache,
|
||||||
|
clearDanmakuCache,
|
||||||
|
clearExpiredDanmakuCache,
|
||||||
|
generateCacheKey,
|
||||||
|
getDanmakuCacheStats,
|
||||||
|
getDanmakuFromCache,
|
||||||
|
saveDanmakuToCache,
|
||||||
|
} from './cache';
|
||||||
import type {
|
import type {
|
||||||
DanmakuAnime,
|
|
||||||
DanmakuComment,
|
DanmakuComment,
|
||||||
DanmakuCommentsResponse,
|
DanmakuCommentsResponse,
|
||||||
DanmakuEpisodesResponse,
|
DanmakuEpisodesResponse,
|
||||||
@@ -10,16 +18,6 @@ import type {
|
|||||||
DanmakuSettings,
|
DanmakuSettings,
|
||||||
} from './types';
|
} from './types';
|
||||||
|
|
||||||
import {
|
|
||||||
getDanmakuFromCache,
|
|
||||||
saveDanmakuToCache,
|
|
||||||
clearExpiredDanmakuCache,
|
|
||||||
clearAllDanmakuCache,
|
|
||||||
clearDanmakuCache,
|
|
||||||
getDanmakuCacheStats,
|
|
||||||
generateCacheKey,
|
|
||||||
} from './cache';
|
|
||||||
|
|
||||||
// 初始化弹幕模块(清理过期缓存)
|
// 初始化弹幕模块(清理过期缓存)
|
||||||
let _cacheCleanupInitialized = false;
|
let _cacheCleanupInitialized = false;
|
||||||
|
|
||||||
@@ -46,8 +44,8 @@ export {
|
|||||||
clearAllDanmakuCache,
|
clearAllDanmakuCache,
|
||||||
clearDanmakuCache,
|
clearDanmakuCache,
|
||||||
clearExpiredDanmakuCache,
|
clearExpiredDanmakuCache,
|
||||||
getDanmakuCacheStats,
|
|
||||||
generateCacheKey,
|
generateCacheKey,
|
||||||
|
getDanmakuCacheStats,
|
||||||
getDanmakuFromCache,
|
getDanmakuFromCache,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { getAuthInfoFromBrowserCookie } from './auth';
|
import { getAuthInfoFromBrowserCookie } from './auth';
|
||||||
import { SkipConfig, DanmakuFilterConfig, EpisodeFilterConfig } from './types';
|
import { DanmakuFilterConfig, EpisodeFilterConfig,SkipConfig } from './types';
|
||||||
|
|
||||||
// 全局错误触发函数
|
// 全局错误触发函数
|
||||||
function triggerGlobalError(message: string) {
|
function triggerGlobalError(message: string) {
|
||||||
|
|||||||
+3
-3
@@ -3,7 +3,7 @@
|
|||||||
import { AdminConfig } from './admin.types';
|
import { AdminConfig } from './admin.types';
|
||||||
import { KvrocksStorage } from './kvrocks.db';
|
import { KvrocksStorage } from './kvrocks.db';
|
||||||
import { RedisStorage } from './redis.db';
|
import { RedisStorage } from './redis.db';
|
||||||
import { Favorite, IStorage, PlayRecord, SkipConfig, DanmakuFilterConfig } from './types';
|
import { DanmakuFilterConfig,Favorite, IStorage, PlayRecord, SkipConfig } from './types';
|
||||||
import { UpstashRedisStorage } from './upstash.db';
|
import { UpstashRedisStorage } from './upstash.db';
|
||||||
|
|
||||||
// storage type 常量: 'localstorage' | 'redis' | 'upstash',默认 'localstorage'
|
// storage type 常量: 'localstorage' | 'redis' | 'upstash',默认 'localstorage'
|
||||||
@@ -224,8 +224,8 @@ export class DbManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async getUserListV2(
|
async getUserListV2(
|
||||||
offset: number = 0,
|
offset = 0,
|
||||||
limit: number = 20,
|
limit = 20,
|
||||||
ownerUsername?: string
|
ownerUsername?: string
|
||||||
): Promise<{
|
): Promise<{
|
||||||
users: Array<{
|
users: Array<{
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import nodemailer from 'nodemailer';
|
import nodemailer from 'nodemailer';
|
||||||
|
|
||||||
import type { AdminConfig } from './admin.types';
|
import type { AdminConfig } from './admin.types';
|
||||||
|
|
||||||
export interface EmailOptions {
|
export interface EmailOptions {
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ export function clearEmbyCache(): { cleared: number } {
|
|||||||
/**
|
/**
|
||||||
* 获取缓存的 Emby 媒体库列表
|
* 获取缓存的 Emby 媒体库列表
|
||||||
*/
|
*/
|
||||||
export function getCachedEmbyViews(embyKey: string = 'default'): any | null {
|
export function getCachedEmbyViews(embyKey = 'default'): any | null {
|
||||||
const cacheKey = `${EMBY_VIEWS_CACHE_KEY}:${embyKey}`;
|
const cacheKey = `${EMBY_VIEWS_CACHE_KEY}:${embyKey}`;
|
||||||
const entry = EMBY_CACHE.get(cacheKey);
|
const entry = EMBY_CACHE.get(cacheKey);
|
||||||
if (!entry) return null;
|
if (!entry) return null;
|
||||||
@@ -89,7 +89,7 @@ export function getCachedEmbyViews(embyKey: string = 'default'): any | null {
|
|||||||
/**
|
/**
|
||||||
* 设置缓存的 Emby 媒体库列表
|
* 设置缓存的 Emby 媒体库列表
|
||||||
*/
|
*/
|
||||||
export function setCachedEmbyViews(embyKey: string = 'default', data: any): void {
|
export function setCachedEmbyViews(embyKey = 'default', data: any): void {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const cacheKey = `${EMBY_VIEWS_CACHE_KEY}:${embyKey}`;
|
const cacheKey = `${EMBY_VIEWS_CACHE_KEY}:${embyKey}`;
|
||||||
EMBY_CACHE.set(cacheKey, {
|
EMBY_CACHE.set(cacheKey, {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
|
||||||
import { EmbyClient } from './emby.client';
|
|
||||||
import { getConfig } from './config';
|
|
||||||
import { AdminConfig } from './admin.types';
|
import { AdminConfig } from './admin.types';
|
||||||
|
import { getConfig } from './config';
|
||||||
|
import { EmbyClient } from './emby.client';
|
||||||
|
|
||||||
interface EmbySourceConfig {
|
interface EmbySourceConfig {
|
||||||
key: string;
|
key: string;
|
||||||
|
|||||||
+8
-12
@@ -165,19 +165,15 @@ export class EmbyClient {
|
|||||||
const url = `${this.serverUrl}/Users/Me`;
|
const url = `${this.serverUrl}/Users/Me`;
|
||||||
const headers = this.getHeaders();
|
const headers = this.getHeaders();
|
||||||
|
|
||||||
try {
|
const response = await fetch(url, { headers });
|
||||||
const response = await fetch(url, { headers });
|
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const errorText = await response.text();
|
const errorText = await response.text();
|
||||||
throw new Error(`获取当前用户信息失败 (${response.status}): ${errorText}`);
|
throw new Error(`获取当前用户信息失败 (${response.status}): ${errorText}`);
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
return data;
|
|
||||||
} catch (error) {
|
|
||||||
throw error;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
async getUserViews(): Promise<EmbyView[]> {
|
async getUserViews(): Promise<EmbyView[]> {
|
||||||
@@ -467,7 +463,7 @@ export class EmbyClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async getStreamUrl(itemId: string, direct: boolean = true, forceDirectUrl: boolean = false): Promise<string> {
|
async getStreamUrl(itemId: string, direct = true, forceDirectUrl = false): Promise<string> {
|
||||||
const token = this.apiKey || this.authToken;
|
const token = this.apiKey || this.authToken;
|
||||||
|
|
||||||
// 如果启用了代理播放且不是强制获取直接URL,返回代理URL
|
// 如果启用了代理播放且不是强制获取直接URL,返回代理URL
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/* eslint-disable no-console, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */
|
/* eslint-disable no-console, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */
|
||||||
|
|
||||||
import { BaseRedisStorage, createRedisClient, createRetryWrapper } from './redis-base.db';
|
|
||||||
import { StandardRedisAdapter } from './redis-adapter';
|
import { StandardRedisAdapter } from './redis-adapter';
|
||||||
|
import { BaseRedisStorage, createRedisClient, createRetryWrapper } from './redis-base.db';
|
||||||
|
|
||||||
export class KvrocksStorage extends BaseRedisStorage {
|
export class KvrocksStorage extends BaseRedisStorage {
|
||||||
constructor() {
|
constructor() {
|
||||||
|
|||||||
@@ -3,10 +3,11 @@
|
|||||||
* 基于 M3U8Download 项目改造为 TypeScript 版本
|
* 基于 M3U8Download 项目改造为 TypeScript 版本
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { AESDecryptor } from './aes-decryptor';
|
|
||||||
// @ts-ignore - mux.js 没有类型定义
|
// @ts-ignore - mux.js 没有类型定义
|
||||||
import * as muxjs from 'mux.js';
|
import * as muxjs from 'mux.js';
|
||||||
|
|
||||||
|
import { AESDecryptor } from './aes-decryptor';
|
||||||
|
|
||||||
export interface M3U8DownloadTask {
|
export interface M3U8DownloadTask {
|
||||||
id: string;
|
id: string;
|
||||||
url: string;
|
url: string;
|
||||||
|
|||||||
@@ -4,11 +4,11 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import * as path from 'path';
|
|
||||||
import * as https from 'https';
|
|
||||||
import * as http from 'http';
|
import * as http from 'http';
|
||||||
import * as zlib from 'zlib';
|
import * as https from 'https';
|
||||||
|
import * as path from 'path';
|
||||||
import { URL } from 'url';
|
import { URL } from 'url';
|
||||||
|
import * as zlib from 'zlib';
|
||||||
|
|
||||||
export interface OfflineDownloadTask {
|
export interface OfflineDownloadTask {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -467,12 +467,16 @@ export class OfflineDownloader {
|
|||||||
});
|
});
|
||||||
|
|
||||||
fileStream.on('error', (err) => {
|
fileStream.on('error', (err) => {
|
||||||
fs.unlink(savePath, () => {});
|
fs.unlink(savePath, () => {
|
||||||
|
// Ignore unlink errors
|
||||||
|
});
|
||||||
reject(err);
|
reject(err);
|
||||||
});
|
});
|
||||||
|
|
||||||
stream.on('error', (err) => {
|
stream.on('error', (err) => {
|
||||||
fs.unlink(savePath, () => {});
|
fs.unlink(savePath, () => {
|
||||||
|
// Ignore unlink errors
|
||||||
|
});
|
||||||
reject(err);
|
reject(err);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any, no-console */
|
/* eslint-disable @typescript-eslint/no-explicit-any, no-console */
|
||||||
|
|
||||||
|
import parseTorrentName from 'parse-torrent-name';
|
||||||
|
|
||||||
|
import type { AdminConfig } from '@/lib/admin.types';
|
||||||
import { getConfig } from '@/lib/config';
|
import { getConfig } from '@/lib/config';
|
||||||
import { generateFolderKey } from '@/lib/crypto';
|
import { generateFolderKey } from '@/lib/crypto';
|
||||||
import { db } from '@/lib/db';
|
import { db } from '@/lib/db';
|
||||||
import { OpenListClient } from '@/lib/openlist.client';
|
import { OpenListClient } from '@/lib/openlist.client';
|
||||||
import {
|
import {
|
||||||
getCachedMetaInfo,
|
|
||||||
invalidateMetaInfoCache,
|
invalidateMetaInfoCache,
|
||||||
MetaInfo,
|
MetaInfo,
|
||||||
setCachedMetaInfo,
|
setCachedMetaInfo,
|
||||||
@@ -18,9 +20,7 @@ import {
|
|||||||
updateScanTaskProgress,
|
updateScanTaskProgress,
|
||||||
} from '@/lib/scan-task';
|
} from '@/lib/scan-task';
|
||||||
import { parseSeasonFromTitle } from '@/lib/season-parser';
|
import { parseSeasonFromTitle } from '@/lib/season-parser';
|
||||||
import { searchTMDB, getTVSeasonDetails } from '@/lib/tmdb.search';
|
import { getTVSeasonDetails,searchTMDB } from '@/lib/tmdb.search';
|
||||||
import parseTorrentName from 'parse-torrent-name';
|
|
||||||
import type { AdminConfig } from '@/lib/admin.types';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取根目录列表(兼容新旧配置)
|
* 获取根目录列表(兼容新旧配置)
|
||||||
@@ -58,7 +58,7 @@ async function migrateToMultiRoot(openListConfig: NonNullable<AdminConfig['OpenL
|
|||||||
const metaInfo: MetaInfo = JSON.parse(metainfoContent);
|
const metaInfo: MetaInfo = JSON.parse(metainfoContent);
|
||||||
|
|
||||||
// 2. 迁移 folderName:加上原根路径前缀
|
// 2. 迁移 folderName:加上原根路径前缀
|
||||||
for (const [key, info] of Object.entries(metaInfo.folders)) {
|
for (const [_key, info] of Object.entries(metaInfo.folders)) {
|
||||||
const oldFolderName = info.folderName;
|
const oldFolderName = info.folderName;
|
||||||
const newFolderName = `${oldRootPath}${oldRootPath.endsWith('/') ? '' : '/'}${oldFolderName}`;
|
const newFolderName = `${oldRootPath}${oldRootPath.endsWith('/') ? '' : '/'}${oldFolderName}`;
|
||||||
info.folderName = newFolderName;
|
info.folderName = newFolderName;
|
||||||
@@ -83,7 +83,7 @@ async function migrateToMultiRoot(openListConfig: NonNullable<AdminConfig['OpenL
|
|||||||
/**
|
/**
|
||||||
* 启动 OpenList 刷新任务
|
* 启动 OpenList 刷新任务
|
||||||
*/
|
*/
|
||||||
export async function startOpenListRefresh(clearMetaInfo: boolean = false): Promise<{ taskId: string }> {
|
export async function startOpenListRefresh(clearMetaInfo = false): Promise<{ taskId: string }> {
|
||||||
const config = await getConfig();
|
const config = await getConfig();
|
||||||
const openListConfig = config.OpenListConfig;
|
const openListConfig = config.OpenListConfig;
|
||||||
|
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ export interface OpenListGetResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class OpenListClient {
|
export class OpenListClient {
|
||||||
private token: string = '';
|
private token = '';
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private baseURL: string,
|
private baseURL: string,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
|
||||||
import { RedisClientType } from 'redis';
|
|
||||||
import { Redis } from '@upstash/redis';
|
import { Redis } from '@upstash/redis';
|
||||||
|
import { RedisClientType } from 'redis';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 统一的 Redis 适配器接口
|
* 统一的 Redis 适配器接口
|
||||||
|
|||||||
@@ -796,8 +796,8 @@ export abstract class BaseRedisStorage implements IStorage {
|
|||||||
|
|
||||||
// 获取用户列表(分页,新版本)
|
// 获取用户列表(分页,新版本)
|
||||||
async getUserListV2(
|
async getUserListV2(
|
||||||
offset: number = 0,
|
offset = 0,
|
||||||
limit: number = 20,
|
limit = 20,
|
||||||
ownerUsername?: string
|
ownerUsername?: string
|
||||||
): Promise<{
|
): Promise<{
|
||||||
users: Array<{
|
users: Array<{
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
/* eslint-disable no-console, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */
|
/* eslint-disable no-console, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */
|
||||||
|
|
||||||
import { BaseRedisStorage, createRedisClient, createRetryWrapper } from './redis-base.db';
|
|
||||||
import { StandardRedisAdapter } from './redis-adapter';
|
import { StandardRedisAdapter } from './redis-adapter';
|
||||||
|
import { BaseRedisStorage, createRedisClient, createRetryWrapper } from './redis-base.db';
|
||||||
|
|
||||||
export class RedisStorage extends BaseRedisStorage {
|
export class RedisStorage extends BaseRedisStorage {
|
||||||
constructor() {
|
constructor() {
|
||||||
|
|||||||
@@ -85,8 +85,8 @@ interface TMDBTVAiringTodayResponse {
|
|||||||
*/
|
*/
|
||||||
export async function getTMDBUpcomingMovies(
|
export async function getTMDBUpcomingMovies(
|
||||||
apiKey: string,
|
apiKey: string,
|
||||||
page: number = 1,
|
page = 1,
|
||||||
region: string = 'CN',
|
region = 'CN',
|
||||||
proxy?: string,
|
proxy?: string,
|
||||||
reverseProxyBaseUrl?: string
|
reverseProxyBaseUrl?: string
|
||||||
): Promise<{ code: number; list: TMDBMovie[] }> {
|
): Promise<{ code: number; list: TMDBMovie[] }> {
|
||||||
@@ -140,7 +140,7 @@ export async function getTMDBUpcomingMovies(
|
|||||||
*/
|
*/
|
||||||
export async function getTMDBUpcomingTVShows(
|
export async function getTMDBUpcomingTVShows(
|
||||||
apiKey: string,
|
apiKey: string,
|
||||||
page: number = 1,
|
page = 1,
|
||||||
proxy?: string,
|
proxy?: string,
|
||||||
reverseProxyBaseUrl?: string
|
reverseProxyBaseUrl?: string
|
||||||
): Promise<{ code: number; list: TMDBTVShow[] }> {
|
): Promise<{ code: number; list: TMDBTVShow[] }> {
|
||||||
@@ -400,7 +400,7 @@ export async function getTMDBTrendingContent(
|
|||||||
*/
|
*/
|
||||||
export function getTMDBImageUrl(
|
export function getTMDBImageUrl(
|
||||||
path: string | null,
|
path: string | null,
|
||||||
size: string = 'w500'
|
size = 'w500'
|
||||||
): string {
|
): string {
|
||||||
if (!path) return '';
|
if (!path) return '';
|
||||||
const baseUrl = typeof window !== 'undefined'
|
const baseUrl = typeof window !== 'undefined'
|
||||||
@@ -450,7 +450,7 @@ export const TMDB_GENRES: Record<number, string> = {
|
|||||||
* @param limit - 最多返回几个类型,默认2个
|
* @param limit - 最多返回几个类型,默认2个
|
||||||
* @returns 类型名称数组
|
* @returns 类型名称数组
|
||||||
*/
|
*/
|
||||||
export function getGenreNames(genreIds: number[] = [], limit: number = 2): string[] {
|
export function getGenreNames(genreIds: number[] = [], limit = 2): string[] {
|
||||||
return genreIds
|
return genreIds
|
||||||
.map(id => TMDB_GENRES[id])
|
.map(id => TMDB_GENRES[id])
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
import { HttpsProxyAgent } from 'https-proxy-agent';
|
import { HttpsProxyAgent } from 'https-proxy-agent';
|
||||||
import nodeFetch from 'node-fetch';
|
import nodeFetch from 'node-fetch';
|
||||||
|
|
||||||
import { getNextApiKey } from './tmdb.client';
|
import { getNextApiKey } from './tmdb.client';
|
||||||
|
|
||||||
// TMDB API 默认 Base URL(不包含 /3/,由程序拼接)
|
// TMDB API 默认 Base URL(不包含 /3/,由程序拼接)
|
||||||
@@ -226,7 +227,7 @@ export async function getTVSeasonDetails(
|
|||||||
*/
|
*/
|
||||||
export function getTMDBImageUrl(
|
export function getTMDBImageUrl(
|
||||||
path: string | null,
|
path: string | null,
|
||||||
size: string = 'w500'
|
size = 'w500'
|
||||||
): string {
|
): string {
|
||||||
if (!path) return '';
|
if (!path) return '';
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
|
|
||||||
import { Redis } from '@upstash/redis';
|
import { Redis } from '@upstash/redis';
|
||||||
|
|
||||||
import { BaseRedisStorage } from './redis-base.db';
|
|
||||||
import { UpstashRedisAdapter } from './redis-adapter';
|
import { UpstashRedisAdapter } from './redis-adapter';
|
||||||
|
import { BaseRedisStorage } from './redis-base.db';
|
||||||
|
|
||||||
// 添加Upstash Redis操作重试包装器
|
// 添加Upstash Redis操作重试包装器
|
||||||
async function withRetry<T>(
|
async function withRetry<T>(
|
||||||
|
|||||||
+2
-2
@@ -1,7 +1,7 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any,no-console */
|
/* eslint-disable @typescript-eslint/no-explicit-any,no-console */
|
||||||
|
import bs58 from 'bs58';
|
||||||
import he from 'he';
|
import he from 'he';
|
||||||
import Hls from 'hls.js';
|
import Hls from 'hls.js';
|
||||||
import bs58 from 'bs58';
|
|
||||||
|
|
||||||
function getDoubanImageProxyConfig(): {
|
function getDoubanImageProxyConfig(): {
|
||||||
proxyType:
|
proxyType:
|
||||||
@@ -155,7 +155,7 @@ export function processVideoUrl(originalUrl: string): string {
|
|||||||
*/
|
*/
|
||||||
export async function getVideoResolutionFromM3u8(
|
export async function getVideoResolutionFromM3u8(
|
||||||
m3u8Url: string,
|
m3u8Url: string,
|
||||||
timeoutMs: number = 4000
|
timeoutMs = 4000
|
||||||
): Promise<{
|
): Promise<{
|
||||||
quality: string; // 如720p、1080p等
|
quality: string; // 如720p、1080p等
|
||||||
loadSpeed: string; // 自动转换为KB/s或MB/s
|
loadSpeed: string; // 自动转换为KB/s或MB/s
|
||||||
|
|||||||
@@ -34,13 +34,13 @@ export function parseVideoFileName(fileName: string): ParsedVideoInfo {
|
|||||||
// S01E01, s01e01, S01E1234, S01E01.5 (支持1-4位数字和小数) - 最具体
|
// S01E01, s01e01, S01E1234, S01E01.5 (支持1-4位数字和小数) - 最具体
|
||||||
{ pattern: /[Ss](\d+)[Ee](\d{1,4}(?:\.\d+)?)/, extractSeason: true },
|
{ pattern: /[Ss](\d+)[Ee](\d{1,4}(?:\.\d+)?)/, extractSeason: true },
|
||||||
// [01], (01), [01.5], (01.5) (支持小数,但要排除中文括号内容) - 很具体
|
// [01], (01), [01.5], (01.5) (支持小数,但要排除中文括号内容) - 很具体
|
||||||
{ pattern: /[\[\(](\d+(?:\.\d+)?)[\]\)]/ },
|
{ pattern: /[[(](\d+(?:\.\d+)?)[)\]]/ },
|
||||||
// E01, E1, e01, e1, E01.5 (支持小数)
|
// E01, E1, e01, e1, E01.5 (支持小数)
|
||||||
{ pattern: /[Ee](\d+(?:\.\d+)?)/ },
|
{ pattern: /[Ee](\d+(?:\.\d+)?)/ },
|
||||||
// 第01集, 第1集, 第01话, 第1话, 第1.5集 (支持小数)
|
// 第01集, 第1集, 第01话, 第1话, 第1.5集 (支持小数)
|
||||||
{ pattern: /第(\d+(?:\.\d+)?)[集话]/ },
|
{ pattern: /第(\d+(?:\.\d+)?)[集话]/ },
|
||||||
// _01_, -01-, _01.5_, -01.5- (支持小数)
|
// _01_, -01-, _01.5_, -01.5- (支持小数)
|
||||||
{ pattern: /[_\-](\d+(?:\.\d+)?)[_\-]/ },
|
{ pattern: /[_-](\d+(?:\.\d+)?)[_-]/ },
|
||||||
// 01.mp4, 001.mp4, 01.5.mp4 (纯数字开头,支持小数) - 最不具体
|
// 01.mp4, 001.mp4, 01.5.mp4 (纯数字开头,支持小数) - 最不具体
|
||||||
{ pattern: /^(\d+(?:\.\d+)?)[^\d.]/ },
|
{ pattern: /^(\d+(?:\.\d+)?)[^\d.]/ },
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
// Socket.IO 观影室服务器逻辑(共享代码)
|
// Socket.IO 观影室服务器逻辑(共享代码)
|
||||||
import { Server as SocketIOServer, Socket } from 'socket.io';
|
import { Server as SocketIOServer, Socket } from 'socket.io';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
Room,
|
|
||||||
Member,
|
|
||||||
PlayState,
|
|
||||||
LiveState,
|
|
||||||
ChatMessage,
|
ChatMessage,
|
||||||
ServerToClientEvents,
|
|
||||||
ClientToServerEvents,
|
ClientToServerEvents,
|
||||||
|
Member,
|
||||||
|
Room,
|
||||||
RoomMemberInfo,
|
RoomMemberInfo,
|
||||||
|
ServerToClientEvents,
|
||||||
} from '@/types/watch-room';
|
} from '@/types/watch-room';
|
||||||
|
|
||||||
type TypedSocket = Socket<ClientToServerEvents, ServerToClientEvents>;
|
type TypedSocket = Socket<ClientToServerEvents, ServerToClientEvents>;
|
||||||
|
|||||||
@@ -153,7 +153,7 @@ class WatchRoomSocketManager {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// 监听心跳响应
|
// 监听心跳响应
|
||||||
this.socket.on('heartbeat:pong', (data: { timestamp: number }) => {
|
this.socket.on('heartbeat:pong', (_data: { timestamp: number }) => {
|
||||||
this.lastHeartbeatResponse = Date.now();
|
this.lastHeartbeatResponse = Date.now();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
|
||||||
import { XiaoyaClient } from './xiaoya.client';
|
import { NFOMetadata,parseNFO } from './nfo-parser';
|
||||||
import { parseNFO, NFOMetadata } from './nfo-parser';
|
|
||||||
import { parseVideoFileName } from './video-parser';
|
import { parseVideoFileName } from './video-parser';
|
||||||
|
import { XiaoyaClient } from './xiaoya.client';
|
||||||
|
|
||||||
export interface XiaoyaMetadata {
|
export interface XiaoyaMetadata {
|
||||||
tmdbId?: number;
|
tmdbId?: number;
|
||||||
@@ -208,7 +208,7 @@ export async function getXiaoyaMetadata(
|
|||||||
const fileName = pathParts[pathParts.length - 1];
|
const fileName = pathParts[pathParts.length - 1];
|
||||||
const searchQuery = fileName
|
const searchQuery = fileName
|
||||||
.replace(/\.(mp4|mkv|avi|m3u8|flv|ts)$/i, '')
|
.replace(/\.(mp4|mkv|avi|m3u8|flv|ts)$/i, '')
|
||||||
.replace(/[\[\]()]/g, ' ')
|
.replace(/[[\]()]/g, ' ')
|
||||||
.trim();
|
.trim();
|
||||||
|
|
||||||
// 如果文件名是纯数字(可能带小数点)或者是 SxxExx 格式,跳过文件名搜索,直接使用文件夹名
|
// 如果文件名是纯数字(可能带小数点)或者是 SxxExx 格式,跳过文件名搜索,直接使用文件夹名
|
||||||
@@ -240,7 +240,7 @@ export async function getXiaoyaMetadata(
|
|||||||
// 优先级 4: 实时搜索 TMDb(使用文件夹名)
|
// 优先级 4: 实时搜索 TMDb(使用文件夹名)
|
||||||
if (tmdbApiKey) {
|
if (tmdbApiKey) {
|
||||||
const searchQuery = folderName
|
const searchQuery = folderName
|
||||||
.replace(/[\[\](){}]/g, ' ')
|
.replace(/[[\](){}]/g, ' ')
|
||||||
.replace(/\d{4}/g, '')
|
.replace(/\d{4}/g, '')
|
||||||
.trim();
|
.trim();
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ export interface XiaoyaListResponse {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class XiaoyaClient {
|
export class XiaoyaClient {
|
||||||
private token: string = '';
|
private token = '';
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private baseURL: string,
|
private baseURL: string,
|
||||||
|
|||||||
Reference in New Issue
Block a user