增加动漫数据源配置
This commit is contained in:
@@ -25,6 +25,11 @@ export interface AdminConfig {
|
||||
TMDBApiKey?: string;
|
||||
TMDBProxy?: string;
|
||||
TMDBReverseProxy?: string;
|
||||
// 动漫/Bangumi配置
|
||||
BangumiDataSource?: 'direct' | 'server-proxy' | 'custom-baseurl';
|
||||
BangumiApiBaseUrl?: string;
|
||||
BangumiImageBaseUrl?: string;
|
||||
BangumiProxy?: string;
|
||||
BannerDataSource?: string; // 轮播图数据源:TMDB、TX 或 Douban
|
||||
RecommendationDataSource?: string; // 更多推荐数据源:Douban、TMDB、Mixed、MixedSmart
|
||||
// Pansou配置
|
||||
@@ -101,7 +106,7 @@ export interface AdminConfig {
|
||||
LiveConfig?: {
|
||||
key: string;
|
||||
name: string;
|
||||
url: string; // m3u 地址
|
||||
url: string; // m3u 地址
|
||||
ua?: string;
|
||||
epg?: string; // 节目单
|
||||
from: 'config' | 'custom';
|
||||
|
||||
+151
-3
@@ -1,5 +1,7 @@
|
||||
'use client';
|
||||
|
||||
export type AnimeDataSource = 'direct' | 'server-proxy' | 'custom-baseurl';
|
||||
|
||||
export interface BangumiCalendarData {
|
||||
weekday: {
|
||||
en: string;
|
||||
@@ -22,8 +24,154 @@ export interface BangumiCalendarData {
|
||||
}[];
|
||||
}
|
||||
|
||||
export interface BangumiSubjectData {
|
||||
id?: number;
|
||||
name: string;
|
||||
name_cn?: string;
|
||||
date?: string;
|
||||
images?: {
|
||||
large?: string;
|
||||
common?: string;
|
||||
medium?: string;
|
||||
small?: string;
|
||||
grid?: string;
|
||||
};
|
||||
rating?: {
|
||||
score: number;
|
||||
total: number;
|
||||
};
|
||||
summary?: string;
|
||||
tags?: { name: string }[];
|
||||
eps?: number;
|
||||
}
|
||||
|
||||
const BANGUMI_OFFICIAL_BASE_URL = 'https://api.bgm.tv';
|
||||
const SERVER_PROXY_BASE_URL = '/api/bangumi';
|
||||
|
||||
function normalizeBaseUrl(baseUrl: string): string {
|
||||
return baseUrl.trim().replace(/\/+$/, '');
|
||||
}
|
||||
|
||||
function getRuntimeConfig() {
|
||||
if (typeof window === 'undefined') return {} as any;
|
||||
return (window as any).RUNTIME_CONFIG || {};
|
||||
}
|
||||
|
||||
function getPrimaryAnimeDataSource(): AnimeDataSource {
|
||||
if (typeof window === 'undefined') return 'direct';
|
||||
|
||||
const saved = localStorage.getItem(
|
||||
'animeDataSource'
|
||||
) as AnimeDataSource | null;
|
||||
if (
|
||||
saved === 'direct' ||
|
||||
saved === 'server-proxy' ||
|
||||
saved === 'custom-baseurl'
|
||||
) {
|
||||
return saved;
|
||||
}
|
||||
|
||||
const runtimeValue = getRuntimeConfig().BANGUMI_DATA_SOURCE as
|
||||
| AnimeDataSource
|
||||
| undefined;
|
||||
if (
|
||||
runtimeValue === 'direct' ||
|
||||
runtimeValue === 'server-proxy' ||
|
||||
runtimeValue === 'custom-baseurl'
|
||||
) {
|
||||
return runtimeValue;
|
||||
}
|
||||
|
||||
return 'direct';
|
||||
}
|
||||
|
||||
function getBackupAnimeDataSource(
|
||||
primary: AnimeDataSource
|
||||
): AnimeDataSource | null {
|
||||
if (typeof window === 'undefined')
|
||||
return primary === 'server-proxy' ? null : 'server-proxy';
|
||||
|
||||
const saved = localStorage.getItem(
|
||||
'animeDataSourceBackup'
|
||||
) as AnimeDataSource | null;
|
||||
const backup =
|
||||
saved === 'direct' || saved === 'server-proxy' || saved === 'custom-baseurl'
|
||||
? saved
|
||||
: 'server-proxy';
|
||||
|
||||
return backup === primary ? null : backup;
|
||||
}
|
||||
|
||||
function getCustomAnimeBaseUrl(): string {
|
||||
if (typeof window === 'undefined') return '';
|
||||
return localStorage.getItem('animeCustomBaseUrl') || '';
|
||||
}
|
||||
|
||||
function buildBangumiUrl(source: AnimeDataSource, path: string): string {
|
||||
const normalizedPath = path.startsWith('/') ? path : `/${path}`;
|
||||
|
||||
switch (source) {
|
||||
case 'server-proxy':
|
||||
return `${SERVER_PROXY_BASE_URL}${normalizedPath}`;
|
||||
case 'custom-baseurl': {
|
||||
const customBaseUrl = normalizeBaseUrl(getCustomAnimeBaseUrl());
|
||||
if (!customBaseUrl) {
|
||||
return `${BANGUMI_OFFICIAL_BASE_URL}${normalizedPath}`;
|
||||
}
|
||||
return `${customBaseUrl}${normalizedPath}`;
|
||||
}
|
||||
case 'direct':
|
||||
default:
|
||||
return `${BANGUMI_OFFICIAL_BASE_URL}${normalizedPath}`;
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchBangumiJson<T>(
|
||||
source: AnimeDataSource,
|
||||
path: string
|
||||
): Promise<T> {
|
||||
const response = await fetch(buildBangumiUrl(source, path), {
|
||||
signal: AbortSignal.timeout(15000),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Bangumi 请求失败: ${response.status}`);
|
||||
}
|
||||
|
||||
return response.json() as Promise<T>;
|
||||
}
|
||||
|
||||
async function requestWithFallback<T>(path: string): Promise<T> {
|
||||
const primary = getPrimaryAnimeDataSource();
|
||||
const backup = getBackupAnimeDataSource(primary);
|
||||
|
||||
try {
|
||||
return await fetchBangumiJson<T>(primary, path);
|
||||
} catch (primaryError) {
|
||||
if (!backup) throw primaryError;
|
||||
|
||||
try {
|
||||
return await fetchBangumiJson<T>(backup, path);
|
||||
} catch (backupError) {
|
||||
console.error('Bangumi 主源与备用源均请求失败:', {
|
||||
primary,
|
||||
backup,
|
||||
primaryError,
|
||||
backupError,
|
||||
});
|
||||
throw backupError;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function GetBangumiCalendarData(): Promise<BangumiCalendarData[]> {
|
||||
const response = await fetch('https://api.bgm.tv/calendar');
|
||||
const data = await response.json();
|
||||
return data;
|
||||
return requestWithFallback<BangumiCalendarData[]>('/calendar');
|
||||
}
|
||||
|
||||
export async function getBangumiSubject(
|
||||
id: number | string
|
||||
): Promise<BangumiSubjectData> {
|
||||
return requestWithFallback<BangumiSubjectData>(
|
||||
`/v0/subjects/${encodeURIComponent(String(id))}`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
|
||||
import { HttpsProxyAgent } from 'https-proxy-agent';
|
||||
import nodeFetch from 'node-fetch';
|
||||
|
||||
export type AnimeDataSource = 'direct' | 'server-proxy' | 'custom-baseurl';
|
||||
|
||||
export const DEFAULT_BANGUMI_BASE_URL = 'https://api.bgm.tv';
|
||||
|
||||
function isCloudflareEnvironment(): boolean {
|
||||
return (
|
||||
process.env.CF_PAGES === '1' || process.env.BUILD_TARGET === 'cloudflare'
|
||||
);
|
||||
}
|
||||
|
||||
export function normalizeBangumiBaseUrl(baseUrl?: string): string {
|
||||
const normalized = (baseUrl || DEFAULT_BANGUMI_BASE_URL)
|
||||
.trim()
|
||||
.replace(/\/+$/, '');
|
||||
return normalized || DEFAULT_BANGUMI_BASE_URL;
|
||||
}
|
||||
|
||||
export async function fetchBangumiFromServer(
|
||||
path: string,
|
||||
options?: { baseUrl?: string; proxy?: string }
|
||||
): Promise<Response> {
|
||||
const normalizedPath = path.startsWith('/') ? path : `/${path}`;
|
||||
const url = `${normalizeBangumiBaseUrl(options?.baseUrl)}${normalizedPath}`;
|
||||
const proxy = options?.proxy?.trim();
|
||||
|
||||
if (isCloudflareEnvironment()) {
|
||||
return fetch(url, {
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'User-Agent': 'MoonTVPlus/1.0 (https://github.com)',
|
||||
},
|
||||
signal: AbortSignal.timeout(15000),
|
||||
}) as Promise<Response>;
|
||||
}
|
||||
|
||||
const fetchOptions: any = {
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'User-Agent': 'MoonTVPlus/1.0 (https://github.com)',
|
||||
},
|
||||
signal: AbortSignal.timeout(proxy ? 30000 : 15000),
|
||||
};
|
||||
|
||||
if (proxy) {
|
||||
fetchOptions.agent = new HttpsProxyAgent(proxy, {
|
||||
timeout: 30000,
|
||||
keepAlive: false,
|
||||
});
|
||||
}
|
||||
|
||||
return nodeFetch(url, fetchOptions) as unknown as Promise<Response>;
|
||||
}
|
||||
+165
-96
@@ -7,7 +7,9 @@ import { AdminConfig } from './admin.types';
|
||||
const BUILTIN_DANMAKU_API_BASE = 'https://mtvpls-danmu.netlify.app/87654321';
|
||||
const DEFAULT_LIVE_REFRESH_INTERVAL_HOURS = 12;
|
||||
|
||||
function normalizeLiveRefreshIntervalHours(refreshIntervalHours?: number): number {
|
||||
function normalizeLiveRefreshIntervalHours(
|
||||
refreshIntervalHours?: number
|
||||
): number {
|
||||
const normalizedInterval = Number(refreshIntervalHours);
|
||||
|
||||
if (!Number.isFinite(normalizedInterval) || normalizedInterval <= 0) {
|
||||
@@ -44,7 +46,7 @@ interface ConfigFileStruct {
|
||||
}[];
|
||||
lives?: {
|
||||
[key: string]: LiveCfg;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export const API_CONFIG = {
|
||||
@@ -71,7 +73,6 @@ export const API_CONFIG = {
|
||||
let cachedConfig: AdminConfig;
|
||||
let configInitPromise: Promise<AdminConfig> | null = null;
|
||||
|
||||
|
||||
// 从配置文件补充管理员配置
|
||||
export function refineConfig(adminConfig: AdminConfig): AdminConfig {
|
||||
let fileConfig: ConfigFileStruct;
|
||||
@@ -197,19 +198,22 @@ export function refineConfig(adminConfig: AdminConfig): AdminConfig {
|
||||
return adminConfig;
|
||||
}
|
||||
|
||||
async function getInitConfig(configFile: string, subConfig: {
|
||||
URL: string;
|
||||
AutoUpdate: boolean;
|
||||
LastCheck: string;
|
||||
} = {
|
||||
URL: "",
|
||||
async function getInitConfig(
|
||||
configFile: string,
|
||||
subConfig: {
|
||||
URL: string;
|
||||
AutoUpdate: boolean;
|
||||
LastCheck: string;
|
||||
} = {
|
||||
URL: '',
|
||||
AutoUpdate: false,
|
||||
LastCheck: "",
|
||||
}): Promise<AdminConfig> {
|
||||
LastCheck: '',
|
||||
}
|
||||
): Promise<AdminConfig> {
|
||||
let cfgFile: ConfigFileStruct;
|
||||
|
||||
// 优先从环境变量读取订阅 URL
|
||||
const envSubUrl = process.env.CONFIG_SUBSCRIPTION_URL || "";
|
||||
const envSubUrl = process.env.CONFIG_SUBSCRIPTION_URL || '';
|
||||
|
||||
if (envSubUrl) {
|
||||
try {
|
||||
@@ -228,7 +232,7 @@ async function getInitConfig(configFile: string, subConfig: {
|
||||
}
|
||||
|
||||
// 优先从环境变量读取配置
|
||||
const envConfig = process.env.INIT_CONFIG || "";
|
||||
const envConfig = process.env.INIT_CONFIG || '';
|
||||
const configSource = envConfig || configFile;
|
||||
|
||||
try {
|
||||
@@ -254,23 +258,37 @@ async function getInitConfig(configFile: string, subConfig: {
|
||||
process.env.NEXT_PUBLIC_DOUBAN_PROXY_TYPE || 'cmliussss-cdn-tencent',
|
||||
DoubanProxy: process.env.NEXT_PUBLIC_DOUBAN_PROXY || '',
|
||||
DoubanImageProxyType:
|
||||
process.env.NEXT_PUBLIC_DOUBAN_IMAGE_PROXY_TYPE || 'cmliussss-cdn-tencent',
|
||||
process.env.NEXT_PUBLIC_DOUBAN_IMAGE_PROXY_TYPE ||
|
||||
'cmliussss-cdn-tencent',
|
||||
DoubanImageProxy: process.env.NEXT_PUBLIC_DOUBAN_IMAGE_PROXY || '',
|
||||
DisableYellowFilter:
|
||||
process.env.NEXT_PUBLIC_DISABLE_YELLOW_FILTER === 'true',
|
||||
FluidSearch:
|
||||
process.env.NEXT_PUBLIC_FLUID_SEARCH !== 'false',
|
||||
FluidSearch: process.env.NEXT_PUBLIC_FLUID_SEARCH !== 'false',
|
||||
// 弹幕配置
|
||||
DanmakuSourceType: hasCustomDanmakuEnv ? 'custom' : 'builtin',
|
||||
DanmakuApiBase:
|
||||
process.env.DANMAKU_API_BASE ||
|
||||
(hasCustomDanmakuEnv ? 'http://localhost:9321' : BUILTIN_DANMAKU_API_BASE),
|
||||
(hasCustomDanmakuEnv
|
||||
? 'http://localhost:9321'
|
||||
: BUILTIN_DANMAKU_API_BASE),
|
||||
DanmakuApiToken: process.env.DANMAKU_API_TOKEN || '87654321',
|
||||
DanmakuAutoLoadDefault: true,
|
||||
// TMDB配置
|
||||
TMDBApiKey: process.env.TMDB_API_KEY || '',
|
||||
TMDBProxy: process.env.TMDB_PROXY || '',
|
||||
TMDBReverseProxy: process.env.TMDB_REVERSE_PROXY || '',
|
||||
// 动漫/Bangumi配置
|
||||
BangumiDataSource:
|
||||
(process.env.NEXT_PUBLIC_BANGUMI_DATA_SOURCE as any) || 'direct',
|
||||
BangumiApiBaseUrl:
|
||||
process.env.BANGUMI_API_BASE_URL ||
|
||||
process.env.NEXT_PUBLIC_BANGUMI_API_BASE_URL ||
|
||||
'https://api.bgm.tv',
|
||||
BangumiImageBaseUrl:
|
||||
process.env.BANGUMI_IMAGE_BASE_URL ||
|
||||
process.env.NEXT_PUBLIC_BANGUMI_IMAGE_BASE_URL ||
|
||||
'',
|
||||
BangumiProxy: process.env.BANGUMI_PROXY || '',
|
||||
// Pansou配置
|
||||
PansouApiUrl: '',
|
||||
PansouUsername: '',
|
||||
@@ -365,7 +383,7 @@ export async function getConfig(): Promise<AdminConfig> {
|
||||
// localStorage 模式下直接从环境变量初始化
|
||||
if (storageType === 'localstorage') {
|
||||
console.log('localStorage 模式:从环境变量初始化配置');
|
||||
const adminConfig = await getInitConfig("");
|
||||
const adminConfig = await getInitConfig('');
|
||||
cachedConfig = configSelfCheck(adminConfig);
|
||||
configInitPromise = null;
|
||||
return cachedConfig;
|
||||
@@ -386,19 +404,20 @@ export async function getConfig(): Promise<AdminConfig> {
|
||||
if (dbReadFailed) {
|
||||
// 数据库读取失败,使用默认配置但不保存,避免覆盖数据库
|
||||
console.warn('数据库读取失败,使用临时默认配置(不会保存到数据库)');
|
||||
adminConfig = await getInitConfig("");
|
||||
adminConfig = await getInitConfig('');
|
||||
} else {
|
||||
// 数据库中确实没有配置,首次初始化并保存
|
||||
console.log('首次初始化配置');
|
||||
adminConfig = await getInitConfig("");
|
||||
adminConfig = await getInitConfig('');
|
||||
await db.saveAdminConfig(adminConfig);
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否有旧格式Emby配置需要迁移
|
||||
const needsEmbyMigration = adminConfig.EmbyConfig &&
|
||||
adminConfig.EmbyConfig.ServerURL &&
|
||||
!adminConfig.EmbyConfig.Sources;
|
||||
const needsEmbyMigration =
|
||||
adminConfig.EmbyConfig &&
|
||||
adminConfig.EmbyConfig.ServerURL &&
|
||||
!adminConfig.EmbyConfig.Sources;
|
||||
|
||||
adminConfig = configSelfCheck(adminConfig);
|
||||
cachedConfig = adminConfig;
|
||||
@@ -544,19 +563,27 @@ export function configSelfCheck(adminConfig: AdminConfig): AdminConfig {
|
||||
if (!adminConfig.UserConfig) {
|
||||
adminConfig.UserConfig = { Users: [] };
|
||||
}
|
||||
if (!adminConfig.UserConfig.Users || !Array.isArray(adminConfig.UserConfig.Users)) {
|
||||
if (
|
||||
!adminConfig.UserConfig.Users ||
|
||||
!Array.isArray(adminConfig.UserConfig.Users)
|
||||
) {
|
||||
adminConfig.UserConfig.Users = [];
|
||||
}
|
||||
if (!adminConfig.SourceConfig || !Array.isArray(adminConfig.SourceConfig)) {
|
||||
adminConfig.SourceConfig = [];
|
||||
}
|
||||
if (!adminConfig.CustomCategories || !Array.isArray(adminConfig.CustomCategories)) {
|
||||
if (
|
||||
!adminConfig.CustomCategories ||
|
||||
!Array.isArray(adminConfig.CustomCategories)
|
||||
) {
|
||||
adminConfig.CustomCategories = [];
|
||||
}
|
||||
if (!adminConfig.LiveConfig || !Array.isArray(adminConfig.LiveConfig)) {
|
||||
adminConfig.LiveConfig = [];
|
||||
}
|
||||
adminConfig.LiveRefreshIntervalHours = normalizeLiveRefreshIntervalHours(adminConfig.LiveRefreshIntervalHours);
|
||||
adminConfig.LiveRefreshIntervalHours = normalizeLiveRefreshIntervalHours(
|
||||
adminConfig.LiveRefreshIntervalHours
|
||||
);
|
||||
|
||||
if (adminConfig.OpenListConfig) {
|
||||
if (!adminConfig.OpenListConfig.RootPaths) {
|
||||
@@ -567,7 +594,9 @@ export function configSelfCheck(adminConfig: AdminConfig): AdminConfig {
|
||||
if (!adminConfig.OpenListConfig.OfflineDownloadPath) {
|
||||
adminConfig.OpenListConfig.OfflineDownloadPath = '/';
|
||||
}
|
||||
if (adminConfig.OpenListConfig.OfflineDownloadUseCustomSource === undefined) {
|
||||
if (
|
||||
adminConfig.OpenListConfig.OfflineDownloadUseCustomSource === undefined
|
||||
) {
|
||||
adminConfig.OpenListConfig.OfflineDownloadUseCustomSource = false;
|
||||
}
|
||||
if (adminConfig.OpenListConfig.OfflineDownloadURL === undefined) {
|
||||
@@ -584,11 +613,13 @@ export function configSelfCheck(adminConfig: AdminConfig): AdminConfig {
|
||||
// 用户信息已迁移到新版数据库
|
||||
// 这里只保留站长用户用于兼容性,其他用户从数据库读取
|
||||
const ownerUser = process.env.USERNAME;
|
||||
adminConfig.UserConfig.Users = [{
|
||||
username: ownerUser!,
|
||||
role: 'owner',
|
||||
banned: false,
|
||||
}];
|
||||
adminConfig.UserConfig.Users = [
|
||||
{
|
||||
username: ownerUser!,
|
||||
role: 'owner',
|
||||
banned: false,
|
||||
},
|
||||
];
|
||||
|
||||
// 采集源去重
|
||||
const seenSourceKeys = new Set<string>();
|
||||
@@ -602,13 +633,15 @@ export function configSelfCheck(adminConfig: AdminConfig): AdminConfig {
|
||||
|
||||
// 自定义分类去重
|
||||
const seenCustomCategoryKeys = new Set<string>();
|
||||
adminConfig.CustomCategories = adminConfig.CustomCategories.filter((category) => {
|
||||
if (seenCustomCategoryKeys.has(category.query + category.type)) {
|
||||
return false;
|
||||
adminConfig.CustomCategories = adminConfig.CustomCategories.filter(
|
||||
(category) => {
|
||||
if (seenCustomCategoryKeys.has(category.query + category.type)) {
|
||||
return false;
|
||||
}
|
||||
seenCustomCategoryKeys.add(category.query + category.type);
|
||||
return true;
|
||||
}
|
||||
seenCustomCategoryKeys.add(category.query + category.type);
|
||||
return true;
|
||||
});
|
||||
);
|
||||
|
||||
// 直播源去重
|
||||
const seenLiveKeys = new Set<string>();
|
||||
@@ -627,42 +660,52 @@ export function configSelfCheck(adminConfig: AdminConfig): AdminConfig {
|
||||
console.log('[Config] 检测到旧格式Emby配置,自动迁移到新格式');
|
||||
const oldConfig = adminConfig.EmbyConfig;
|
||||
adminConfig.EmbyConfig = {
|
||||
Sources: [{
|
||||
key: 'default',
|
||||
name: 'Emby',
|
||||
enabled: oldConfig.Enabled ?? false,
|
||||
ServerURL: oldConfig.ServerURL || '',
|
||||
ApiKey: oldConfig.ApiKey,
|
||||
Username: oldConfig.Username,
|
||||
Password: oldConfig.Password,
|
||||
UserId: oldConfig.UserId,
|
||||
AuthToken: oldConfig.AuthToken,
|
||||
Libraries: oldConfig.Libraries,
|
||||
LastSyncTime: oldConfig.LastSyncTime,
|
||||
ItemCount: oldConfig.ItemCount,
|
||||
isDefault: true,
|
||||
}],
|
||||
Sources: [
|
||||
{
|
||||
key: 'default',
|
||||
name: 'Emby',
|
||||
enabled: oldConfig.Enabled ?? false,
|
||||
ServerURL: oldConfig.ServerURL || '',
|
||||
ApiKey: oldConfig.ApiKey,
|
||||
Username: oldConfig.Username,
|
||||
Password: oldConfig.Password,
|
||||
UserId: oldConfig.UserId,
|
||||
AuthToken: oldConfig.AuthToken,
|
||||
Libraries: oldConfig.Libraries,
|
||||
LastSyncTime: oldConfig.LastSyncTime,
|
||||
ItemCount: oldConfig.ItemCount,
|
||||
isDefault: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
// Emby源去重
|
||||
if (adminConfig.EmbyConfig?.Sources) {
|
||||
const seenEmbyKeys = new Set<string>();
|
||||
adminConfig.EmbyConfig.Sources = adminConfig.EmbyConfig.Sources.filter((source) => {
|
||||
if (seenEmbyKeys.has(source.key)) {
|
||||
return false;
|
||||
adminConfig.EmbyConfig.Sources = adminConfig.EmbyConfig.Sources.filter(
|
||||
(source) => {
|
||||
if (seenEmbyKeys.has(source.key)) {
|
||||
return false;
|
||||
}
|
||||
seenEmbyKeys.add(source.key);
|
||||
return true;
|
||||
}
|
||||
seenEmbyKeys.add(source.key);
|
||||
return true;
|
||||
});
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (!adminConfig.SuwayomiConfig) {
|
||||
adminConfig.SuwayomiConfig = {
|
||||
Enabled: process.env.SUWAYOMI_ENABLED === 'true',
|
||||
ServerURL: process.env.SUWAYOMI_URL || process.env.NEXT_PUBLIC_SUWAYOMI_URL || '',
|
||||
AuthMode: (process.env.SUWAYOMI_AUTH_MODE as 'none' | 'basic_auth' | 'simple_login' | undefined) || 'none',
|
||||
ServerURL:
|
||||
process.env.SUWAYOMI_URL || process.env.NEXT_PUBLIC_SUWAYOMI_URL || '',
|
||||
AuthMode:
|
||||
(process.env.SUWAYOMI_AUTH_MODE as
|
||||
| 'none'
|
||||
| 'basic_auth'
|
||||
| 'simple_login'
|
||||
| undefined) || 'none',
|
||||
Username: process.env.SUWAYOMI_USERNAME || '',
|
||||
Password: process.env.SUWAYOMI_PASSWORD || '',
|
||||
DefaultLang: process.env.SUWAYOMI_DEFAULT_LANG || 'zh',
|
||||
@@ -694,7 +737,10 @@ export function configSelfCheck(adminConfig: AdminConfig): AdminConfig {
|
||||
if (!Array.isArray(adminConfig.SuwayomiConfig.SourceIds)) {
|
||||
adminConfig.SuwayomiConfig.SourceIds = [];
|
||||
}
|
||||
if (adminConfig.SuwayomiConfig.MaxSources === undefined || Number.isNaN(adminConfig.SuwayomiConfig.MaxSources)) {
|
||||
if (
|
||||
adminConfig.SuwayomiConfig.MaxSources === undefined ||
|
||||
Number.isNaN(adminConfig.SuwayomiConfig.MaxSources)
|
||||
) {
|
||||
adminConfig.SuwayomiConfig.MaxSources = 10;
|
||||
}
|
||||
|
||||
@@ -715,19 +761,26 @@ export function configSelfCheck(adminConfig: AdminConfig): AdminConfig {
|
||||
const envUrl = process.env.OPDS_URL || process.env.NEXT_PUBLIC_OPDS_URL;
|
||||
if (!envUrl) return [];
|
||||
|
||||
return [{
|
||||
id: 'default',
|
||||
name: process.env.OPDS_NAME || '默认书源',
|
||||
type: 'opds',
|
||||
url: envUrl,
|
||||
enabled: true,
|
||||
authMode: (process.env.OPDS_AUTH_MODE as 'none' | 'basic' | 'header' | undefined) || 'none',
|
||||
username: process.env.OPDS_USERNAME || '',
|
||||
password: process.env.OPDS_PASSWORD || '',
|
||||
headerName: process.env.OPDS_HEADER_NAME || '',
|
||||
headerValue: process.env.OPDS_HEADER_VALUE || '',
|
||||
searchTemplate: process.env.OPDS_SEARCH_TEMPLATE || '',
|
||||
}];
|
||||
return [
|
||||
{
|
||||
id: 'default',
|
||||
name: process.env.OPDS_NAME || '默认书源',
|
||||
type: 'opds',
|
||||
url: envUrl,
|
||||
enabled: true,
|
||||
authMode:
|
||||
(process.env.OPDS_AUTH_MODE as
|
||||
| 'none'
|
||||
| 'basic'
|
||||
| 'header'
|
||||
| undefined) || 'none',
|
||||
username: process.env.OPDS_USERNAME || '',
|
||||
password: process.env.OPDS_PASSWORD || '',
|
||||
headerName: process.env.OPDS_HEADER_NAME || '',
|
||||
headerValue: process.env.OPDS_HEADER_VALUE || '',
|
||||
searchTemplate: process.env.OPDS_SEARCH_TEMPLATE || '',
|
||||
},
|
||||
];
|
||||
})(),
|
||||
CacheTTL: Number(process.env.OPDS_CACHE_TTL_MS || 10 * 60 * 1000),
|
||||
};
|
||||
@@ -738,15 +791,22 @@ export function configSelfCheck(adminConfig: AdminConfig): AdminConfig {
|
||||
if (!Array.isArray(adminConfig.OPDSConfig.Sources)) {
|
||||
adminConfig.OPDSConfig.Sources = [];
|
||||
}
|
||||
adminConfig.OPDSConfig.Sources = adminConfig.OPDSConfig.Sources.filter((source: any) => (source?.type || 'opds') === 'opds').map((source: any) => {
|
||||
adminConfig.OPDSConfig.Sources = adminConfig.OPDSConfig.Sources.filter(
|
||||
(source: any) => (source?.type || 'opds') === 'opds'
|
||||
).map((source: any) => {
|
||||
const { legado: _legado, ...rest } = source || {};
|
||||
return { ...rest, type: 'opds' };
|
||||
});
|
||||
if (!Array.isArray(adminConfig.OPDSConfig.LegadoSubscriptions)) {
|
||||
adminConfig.OPDSConfig.LegadoSubscriptions = [];
|
||||
}
|
||||
if (adminConfig.OPDSConfig.CacheTTL === undefined || Number.isNaN(adminConfig.OPDSConfig.CacheTTL)) {
|
||||
adminConfig.OPDSConfig.CacheTTL = Number(process.env.OPDS_CACHE_TTL_MS || 10 * 60 * 1000);
|
||||
if (
|
||||
adminConfig.OPDSConfig.CacheTTL === undefined ||
|
||||
Number.isNaN(adminConfig.OPDSConfig.CacheTTL)
|
||||
) {
|
||||
adminConfig.OPDSConfig.CacheTTL = Number(
|
||||
process.env.OPDS_CACHE_TTL_MS || 10 * 60 * 1000
|
||||
);
|
||||
}
|
||||
|
||||
if (!adminConfig.NetDiskConfig) {
|
||||
@@ -888,7 +948,10 @@ export async function resetConfig() {
|
||||
if (!originConfig) {
|
||||
originConfig = {} as AdminConfig;
|
||||
}
|
||||
const adminConfig = await getInitConfig(originConfig.ConfigFile, originConfig.ConfigSubscribtion);
|
||||
const adminConfig = await getInitConfig(
|
||||
originConfig.ConfigFile,
|
||||
originConfig.ConfigSubscribtion
|
||||
);
|
||||
cachedConfig = adminConfig;
|
||||
await db.saveAdminConfig(adminConfig);
|
||||
|
||||
@@ -923,13 +986,15 @@ export async function getAvailableApiSites(user?: string): Promise<ApiSite[]> {
|
||||
// 优先根据用户自己的 enabledApis 配置查找
|
||||
if (userInfoV2.enabledApis && userInfoV2.enabledApis.length > 0) {
|
||||
const userApiSitesSet = new Set(userInfoV2.enabledApis);
|
||||
return allApiSites.filter((s) => userApiSitesSet.has(s.key)).map((s) => ({
|
||||
key: s.key,
|
||||
name: s.name,
|
||||
api: s.api,
|
||||
detail: s.detail,
|
||||
proxyMode: s.proxyMode,
|
||||
}));
|
||||
return allApiSites
|
||||
.filter((s) => userApiSitesSet.has(s.key))
|
||||
.map((s) => ({
|
||||
key: s.key,
|
||||
name: s.name,
|
||||
api: s.api,
|
||||
detail: s.detail,
|
||||
proxyMode: s.proxyMode,
|
||||
}));
|
||||
}
|
||||
|
||||
// 如果没有 enabledApis 配置,则根据 tags 查找
|
||||
@@ -937,21 +1002,25 @@ export async function getAvailableApiSites(user?: string): Promise<ApiSite[]> {
|
||||
const enabledApisFromTags = new Set<string>();
|
||||
|
||||
// 遍历用户的所有 tags,收集对应的 enabledApis
|
||||
userInfoV2.tags.forEach(tagName => {
|
||||
const tagConfig = config.UserConfig.Tags?.find(t => t.name === tagName);
|
||||
userInfoV2.tags.forEach((tagName) => {
|
||||
const tagConfig = config.UserConfig.Tags?.find((t) => t.name === tagName);
|
||||
if (tagConfig && tagConfig.enabledApis) {
|
||||
tagConfig.enabledApis.forEach(apiKey => enabledApisFromTags.add(apiKey));
|
||||
tagConfig.enabledApis.forEach((apiKey) =>
|
||||
enabledApisFromTags.add(apiKey)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
if (enabledApisFromTags.size > 0) {
|
||||
return allApiSites.filter((s) => enabledApisFromTags.has(s.key)).map((s) => ({
|
||||
key: s.key,
|
||||
name: s.name,
|
||||
api: s.api,
|
||||
detail: s.detail,
|
||||
proxyMode: s.proxyMode,
|
||||
}));
|
||||
return allApiSites
|
||||
.filter((s) => enabledApisFromTags.has(s.key))
|
||||
.map((s) => ({
|
||||
key: s.key,
|
||||
name: s.name,
|
||||
api: s.api,
|
||||
detail: s.detail,
|
||||
proxyMode: s.proxyMode,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+250
-27
@@ -55,9 +55,13 @@ function buildDoubanImageUrl(
|
||||
'img.doubanio.cmliussss.com'
|
||||
);
|
||||
case 'baidu':
|
||||
return `https://image.baidu.com/search/down?url=${encodeURIComponent(originalUrl)}`;
|
||||
return `https://image.baidu.com/search/down?url=${encodeURIComponent(
|
||||
originalUrl
|
||||
)}`;
|
||||
case 'custom':
|
||||
return proxyUrl ? `${proxyUrl}${encodeURIComponent(originalUrl)}` : originalUrl;
|
||||
return proxyUrl
|
||||
? `${proxyUrl}${encodeURIComponent(originalUrl)}`
|
||||
: originalUrl;
|
||||
case 'direct':
|
||||
default:
|
||||
return originalUrl;
|
||||
@@ -89,8 +93,9 @@ function getDoubanImageProxyConfig(): {
|
||||
(window as any).RUNTIME_CONFIG?.DOUBAN_IMAGE_PROXY ||
|
||||
'';
|
||||
const doubanImageProxyBackupType =
|
||||
(localStorage.getItem('doubanImageProxyTypeBackup') as DoubanImageProxyType | null) ||
|
||||
'server';
|
||||
(localStorage.getItem(
|
||||
'doubanImageProxyTypeBackup'
|
||||
) as DoubanImageProxyType | null) || 'server';
|
||||
const doubanImageProxyBackupUrl =
|
||||
localStorage.getItem('doubanImageProxyUrlBackup') || '';
|
||||
const primaryConfig = normalizeDoubanImageProxyConfig(
|
||||
@@ -130,6 +135,162 @@ export function getDoubanImageFallbackUrl(originalUrl: string): string | null {
|
||||
return backupUrl;
|
||||
}
|
||||
|
||||
function isBangumiImageUrl(url: string): boolean {
|
||||
try {
|
||||
const hostname = new URL(url).hostname.toLowerCase();
|
||||
return (
|
||||
hostname === 'lain.bgm.tv' ||
|
||||
hostname === 'r.bgm.tv' ||
|
||||
hostname.endsWith('.bgm.tv') ||
|
||||
hostname.endsWith('.bangumi.tv')
|
||||
);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
type AnimeImageSource = 'direct' | 'server-proxy' | 'custom-baseurl';
|
||||
|
||||
const BANGUMI_IMAGE_FALLBACK_UNTIL_KEY = 'bangumiImageFallbackUntil';
|
||||
const BANGUMI_IMAGE_FALLBACK_SIGNATURE_KEY = 'bangumiImageFallbackSignature';
|
||||
const BANGUMI_IMAGE_FALLBACK_DURATION = 60 * 60 * 1000;
|
||||
|
||||
function normalizeBaseUrl(baseUrl: string): string {
|
||||
return baseUrl.trim().replace(/\/+$/, '');
|
||||
}
|
||||
|
||||
function normalizeAnimeImageSource(
|
||||
value: string | null | undefined
|
||||
): AnimeImageSource {
|
||||
return value === 'server-proxy' ||
|
||||
value === 'custom-baseurl' ||
|
||||
value === 'direct'
|
||||
? value
|
||||
: 'direct';
|
||||
}
|
||||
|
||||
function getPrimaryBangumiImageSource(): AnimeImageSource {
|
||||
if (typeof window === 'undefined') {
|
||||
return 'direct';
|
||||
}
|
||||
|
||||
return normalizeAnimeImageSource(
|
||||
localStorage.getItem('animeDataSource') ||
|
||||
(window as any).RUNTIME_CONFIG?.BANGUMI_DATA_SOURCE ||
|
||||
'direct'
|
||||
);
|
||||
}
|
||||
|
||||
function getBackupBangumiImageSource(
|
||||
primary: AnimeImageSource
|
||||
): AnimeImageSource | null {
|
||||
if (typeof window === 'undefined') {
|
||||
return primary === 'server-proxy' ? null : 'server-proxy';
|
||||
}
|
||||
|
||||
const backup = normalizeAnimeImageSource(
|
||||
localStorage.getItem('animeDataSourceBackup') || 'server-proxy'
|
||||
);
|
||||
|
||||
return backup === primary ? null : backup;
|
||||
}
|
||||
|
||||
function getBangumiImageBaseUrl(): string {
|
||||
if (typeof window === 'undefined') {
|
||||
return '';
|
||||
}
|
||||
|
||||
return normalizeBaseUrl(localStorage.getItem('animeImageBaseUrl') || '');
|
||||
}
|
||||
|
||||
function getBangumiImageFallbackSignature(): string {
|
||||
if (typeof window === 'undefined') return '';
|
||||
|
||||
return JSON.stringify({
|
||||
primary: getPrimaryBangumiImageSource(),
|
||||
backup: normalizeAnimeImageSource(
|
||||
localStorage.getItem('animeDataSourceBackup') || 'server-proxy'
|
||||
),
|
||||
imageBaseUrl: getBangumiImageBaseUrl(),
|
||||
});
|
||||
}
|
||||
|
||||
export function clearBangumiImageFallbackCache(): void {
|
||||
if (typeof window === 'undefined') return;
|
||||
localStorage.removeItem(BANGUMI_IMAGE_FALLBACK_UNTIL_KEY);
|
||||
localStorage.removeItem(BANGUMI_IMAGE_FALLBACK_SIGNATURE_KEY);
|
||||
}
|
||||
|
||||
export function markBangumiImageFallbackActive(): void {
|
||||
if (typeof window === 'undefined') return;
|
||||
localStorage.setItem(
|
||||
BANGUMI_IMAGE_FALLBACK_UNTIL_KEY,
|
||||
String(Date.now() + BANGUMI_IMAGE_FALLBACK_DURATION)
|
||||
);
|
||||
localStorage.setItem(
|
||||
BANGUMI_IMAGE_FALLBACK_SIGNATURE_KEY,
|
||||
getBangumiImageFallbackSignature()
|
||||
);
|
||||
}
|
||||
|
||||
function isBangumiImageFallbackActive(): boolean {
|
||||
if (typeof window === 'undefined') return false;
|
||||
|
||||
const until = Number(localStorage.getItem(BANGUMI_IMAGE_FALLBACK_UNTIL_KEY));
|
||||
if (!until || Date.now() >= until) {
|
||||
clearBangumiImageFallbackCache();
|
||||
return false;
|
||||
}
|
||||
|
||||
const signature = localStorage.getItem(BANGUMI_IMAGE_FALLBACK_SIGNATURE_KEY);
|
||||
if (signature !== getBangumiImageFallbackSignature()) {
|
||||
clearBangumiImageFallbackCache();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function buildBangumiImageUrl(
|
||||
originalUrl: string,
|
||||
source: AnimeImageSource
|
||||
): string {
|
||||
switch (source) {
|
||||
case 'server-proxy':
|
||||
return `/api/image-proxy?url=${encodeURIComponent(
|
||||
originalUrl
|
||||
)}&source=bangumi`;
|
||||
case 'custom-baseurl': {
|
||||
const imageBaseUrl = getBangumiImageBaseUrl();
|
||||
return imageBaseUrl ? `${imageBaseUrl}/${originalUrl}` : originalUrl;
|
||||
}
|
||||
case 'direct':
|
||||
default:
|
||||
return originalUrl;
|
||||
}
|
||||
}
|
||||
|
||||
export function getBangumiImageFallbackUrl(originalUrl: string): string | null {
|
||||
if (!originalUrl || !isBangumiImageUrl(originalUrl)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const primary = getPrimaryBangumiImageSource();
|
||||
const backup = getBackupBangumiImageSource(primary);
|
||||
if (!backup) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const primaryUrl = buildBangumiImageUrl(originalUrl, primary);
|
||||
const backupUrl = buildBangumiImageUrl(originalUrl, backup);
|
||||
|
||||
if (backupUrl === primaryUrl) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return backupUrl;
|
||||
}
|
||||
|
||||
export function tryApplyDoubanImageFallback(
|
||||
target: HTMLImageElement,
|
||||
originalUrl: string
|
||||
@@ -143,7 +304,11 @@ export function tryApplyDoubanImageFallback(
|
||||
}
|
||||
|
||||
const fallbackUrl = getDoubanImageFallbackUrl(originalUrl);
|
||||
if (!fallbackUrl || fallbackUrl === target.currentSrc || fallbackUrl === target.src) {
|
||||
if (
|
||||
!fallbackUrl ||
|
||||
fallbackUrl === target.currentSrc ||
|
||||
fallbackUrl === target.src
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -152,6 +317,33 @@ export function tryApplyDoubanImageFallback(
|
||||
return true;
|
||||
}
|
||||
|
||||
export function tryApplyBangumiImageFallback(
|
||||
target: HTMLImageElement,
|
||||
originalUrl: string
|
||||
): boolean {
|
||||
if (!originalUrl || !isBangumiImageUrl(originalUrl)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (target.dataset.bangumiBackupTried === 'true') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const fallbackUrl = getBangumiImageFallbackUrl(originalUrl);
|
||||
if (
|
||||
!fallbackUrl ||
|
||||
fallbackUrl === target.currentSrc ||
|
||||
fallbackUrl === target.src
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
target.dataset.bangumiBackupTried = 'true';
|
||||
markBangumiImageFallbackActive();
|
||||
target.src = fallbackUrl;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理图片 URL,根据用户设置使用相应的代理
|
||||
*/
|
||||
@@ -166,7 +358,8 @@ export function processImageUrl(originalUrl: string): string {
|
||||
// 处理 TMDB 图片 URL 替换
|
||||
if (originalUrl.includes('image.tmdb.org')) {
|
||||
if (typeof window !== 'undefined') {
|
||||
const tmdbImageBaseUrl = localStorage.getItem('tmdbImageBaseUrl') || 'https://image.tmdb.org';
|
||||
const tmdbImageBaseUrl =
|
||||
localStorage.getItem('tmdbImageBaseUrl') || 'https://image.tmdb.org';
|
||||
// 只有当用户设置了不同的 baseUrl 时才进行替换
|
||||
if (tmdbImageBaseUrl !== 'https://image.tmdb.org') {
|
||||
return originalUrl.replace('https://image.tmdb.org', tmdbImageBaseUrl);
|
||||
@@ -175,6 +368,17 @@ export function processImageUrl(originalUrl: string): string {
|
||||
return originalUrl;
|
||||
}
|
||||
|
||||
// 处理 Bangumi 图片代理。直连模式尊重用户选择,不代理图片;
|
||||
// 仅在服务器代理 / 自定义 Base URL 模式下使用本站图片代理。
|
||||
if (isBangumiImageUrl(originalUrl)) {
|
||||
const primary = getPrimaryBangumiImageSource();
|
||||
const backup = getBackupBangumiImageSource(primary);
|
||||
if (backup && isBangumiImageFallbackActive()) {
|
||||
return buildBangumiImageUrl(originalUrl, backup);
|
||||
}
|
||||
return buildBangumiImageUrl(originalUrl, primary);
|
||||
}
|
||||
|
||||
// 处理豆瓣图片代理
|
||||
if (!originalUrl.includes('doubanio.com')) {
|
||||
return originalUrl;
|
||||
@@ -229,7 +433,10 @@ export function processVideoUrl(originalUrl: string): string {
|
||||
case 'custom':
|
||||
// 使用自定义代理
|
||||
if (proxyUrl) {
|
||||
return originalUrl.replace(/https?:\/\/img\d\.doubanio\.com/g, proxyUrl);
|
||||
return originalUrl.replace(
|
||||
/https?:\/\/img\d\.doubanio\.com/g,
|
||||
proxyUrl
|
||||
);
|
||||
}
|
||||
return originalUrl;
|
||||
|
||||
@@ -291,22 +498,23 @@ export async function getVideoResolutionFromM3u8(
|
||||
width >= 3840
|
||||
? '4K'
|
||||
: width >= 2560
|
||||
? '2K'
|
||||
: width >= 1920
|
||||
? '1080p'
|
||||
: width >= 1280
|
||||
? '720p'
|
||||
: width >= 854
|
||||
? '480p'
|
||||
: width > 0
|
||||
? 'SD'
|
||||
: '未知';
|
||||
? '2K'
|
||||
: width >= 1920
|
||||
? '1080p'
|
||||
: width >= 1280
|
||||
? '720p'
|
||||
: width >= 854
|
||||
? '480p'
|
||||
: width > 0
|
||||
? 'SD'
|
||||
: '未知';
|
||||
|
||||
const bitrateStr = estimatedBitrate > 0
|
||||
? estimatedBitrate >= 1000000
|
||||
? `${(estimatedBitrate / 1000000).toFixed(1)} Mbps`
|
||||
: `${Math.round(estimatedBitrate / 1000)} Kbps`
|
||||
: '未知';
|
||||
const bitrateStr =
|
||||
estimatedBitrate > 0
|
||||
? estimatedBitrate >= 1000000
|
||||
? `${(estimatedBitrate / 1000000).toFixed(1)} Mbps`
|
||||
: `${Math.round(estimatedBitrate / 1000)} Kbps`
|
||||
: '未知';
|
||||
|
||||
hls.destroy();
|
||||
video.remove();
|
||||
@@ -385,9 +593,17 @@ export async function getVideoResolutionFromM3u8(
|
||||
const fragmentSize = size; // 分片大小(字节)
|
||||
|
||||
// 码率 = (分片大小 × 8 bits) / 分片时长
|
||||
estimatedBitrate = Math.round((fragmentSize * 8) / fragmentDuration);
|
||||
estimatedBitrate = Math.round(
|
||||
(fragmentSize * 8) / fragmentDuration
|
||||
);
|
||||
|
||||
console.log(`[测速] 估算码率: ${(estimatedBitrate / 1000000).toFixed(2)} Mbps (分片: ${(fragmentSize / 1024 / 1024).toFixed(2)} MB, 时长: ${fragmentDuration.toFixed(1)}s)`);
|
||||
console.log(
|
||||
`[测速] 估算码率: ${(estimatedBitrate / 1000000).toFixed(
|
||||
2
|
||||
)} Mbps (分片: ${(fragmentSize / 1024 / 1024).toFixed(
|
||||
2
|
||||
)} MB, 时长: ${fragmentDuration.toFixed(1)}s)`
|
||||
);
|
||||
}
|
||||
|
||||
checkAndResolve(); // 尝试返回结果
|
||||
@@ -412,8 +628,14 @@ export async function getVideoResolutionFromM3u8(
|
||||
if (data.fatal) {
|
||||
const statusCode = data.response?.code || data.response?.status;
|
||||
// 防止 415 代理兜底熔断导致正常的二进制源在优选逻辑中被剔除
|
||||
if (statusCode === 415 && (m3u8Url.includes('/api/proxy-m3u8') || m3u8Url.includes('/api/proxy/vod/m3u8'))) {
|
||||
console.log('[测速] 测速通道嗅探到这是底层的媒体流文件,免测速通过');
|
||||
if (
|
||||
statusCode === 415 &&
|
||||
(m3u8Url.includes('/api/proxy-m3u8') ||
|
||||
m3u8Url.includes('/api/proxy/vod/m3u8'))
|
||||
) {
|
||||
console.log(
|
||||
'[测速] 测速通道嗅探到这是底层的媒体流文件,免测速通过'
|
||||
);
|
||||
clearTimeout(timeout);
|
||||
hls.destroy();
|
||||
video.remove();
|
||||
@@ -441,7 +663,8 @@ export async function getVideoResolutionFromM3u8(
|
||||
});
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`Error getting video resolution: ${error instanceof Error ? error.message : String(error)
|
||||
`Error getting video resolution: ${
|
||||
error instanceof Error ? error.message : String(error)
|
||||
}`
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user