增加动漫数据源配置
This commit is contained in:
+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