增加动漫数据源配置

This commit is contained in:
mtvpls
2026-05-28 16:00:07 +08:00
parent dc31a788fc
commit cc98b21d66
16 changed files with 4448 additions and 2153 deletions
+250 -27
View File
@@ -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)
}`
);
}