优化动漫数据源图片降级策略

This commit is contained in:
mtvpls
2026-07-21 16:13:00 +08:00
parent d54cf65927
commit 08c2ff36f9
3 changed files with 173 additions and 49 deletions
+148
View File
@@ -159,6 +159,24 @@ type AnimeImageSource =
const BANGUMI_IMAGE_FALLBACK_UNTIL_KEY = 'bangumiImageFallbackUntil';
const BANGUMI_IMAGE_FALLBACK_SIGNATURE_KEY = 'bangumiImageFallbackSignature';
const BANGUMI_IMAGE_FALLBACK_DURATION = 60 * 60 * 1000;
/** 探测主源图片域主页超时(404 也算通) */
const BANGUMI_IMAGE_PROBE_TIMEOUT_MS = 5000;
/** 探测成功后的内存缓存,避免列表刷屏时重复探测 */
const BANGUMI_IMAGE_PROBE_OK_TTL_MS = 15 * 60 * 1000;
type BangumiImageProbeCache = {
signature: string;
reachable: boolean | null;
okUntil: number;
inflight: Promise<boolean> | null;
};
let bangumiImageProbeCache: BangumiImageProbeCache = {
signature: '',
reachable: null,
okUntil: 0,
inflight: null,
};
function normalizeBaseUrl(baseUrl: string): string {
return baseUrl.trim().replace(/\/+$/, '');
@@ -239,10 +257,50 @@ function getBangumiImageFallbackSignature(): string {
});
}
function getBangumiImageProbeSignature(): string {
return JSON.stringify({
primary: getPrimaryBangumiImageSource(),
imageBaseUrl: getBangumiImageBaseUrl(),
});
}
/** 当前主源图片域 originserver-proxy 无需探测返回 null */
function getBangumiImageProbeOrigin(): string | null {
const primary = getPrimaryBangumiImageSource();
switch (primary) {
case 'server-proxy':
return null;
case 'sakura':
return 'https://lain.bangumi.lol';
case 'custom-baseurl': {
const base = getBangumiImageBaseUrl();
if (!base) return 'https://lain.bgm.tv';
try {
return new URL(base).origin;
} catch {
return null;
}
}
case 'direct':
default:
return 'https://lain.bgm.tv';
}
}
export function clearBangumiImageProbeCache(): void {
bangumiImageProbeCache = {
signature: '',
reachable: null,
okUntil: 0,
inflight: null,
};
}
export function clearBangumiImageFallbackCache(): void {
if (typeof window === 'undefined') return;
localStorage.removeItem(BANGUMI_IMAGE_FALLBACK_UNTIL_KEY);
localStorage.removeItem(BANGUMI_IMAGE_FALLBACK_SIGNATURE_KEY);
clearBangumiImageProbeCache();
}
export function markBangumiImageFallbackActive(): void {
@@ -257,6 +315,92 @@ export function markBangumiImageFallbackActive(): void {
);
}
/**
* 探测主源图片域是否可达:请求 origin 主页(常见 404),5s 超时 + 防缓存。
* no-cors 下任意完成(含 404)视为可达;超时/DNS/断网视为不可达。
* 失败时写入 sticky 降级(若有备源)。全局单飞 + 成功缓存 15 分钟。
* @returns true=主源可用,false=应走备源
*/
export async function ensureBangumiImagePrimaryProbed(): Promise<boolean> {
if (typeof window === 'undefined') return true;
if (isBangumiImageFallbackActive()) {
return false;
}
const origin = getBangumiImageProbeOrigin();
if (!origin) {
return true;
}
const signature = getBangumiImageProbeSignature();
const now = Date.now();
if (
bangumiImageProbeCache.signature === signature &&
bangumiImageProbeCache.reachable === true &&
bangumiImageProbeCache.okUntil > now
) {
return true;
}
if (
bangumiImageProbeCache.signature === signature &&
bangumiImageProbeCache.inflight
) {
return bangumiImageProbeCache.inflight;
}
const probePromise = (async (): Promise<boolean> => {
const url = `${origin}/?_cb=${Date.now()}_${Math.random()
.toString(36)
.slice(2, 10)}`;
try {
await fetch(url, {
method: 'GET',
mode: 'no-cors',
cache: 'no-store',
credentials: 'omit',
signal: AbortSignal.timeout(BANGUMI_IMAGE_PROBE_TIMEOUT_MS),
});
bangumiImageProbeCache = {
signature,
reachable: true,
okUntil: Date.now() + BANGUMI_IMAGE_PROBE_OK_TTL_MS,
inflight: null,
};
return true;
} catch {
bangumiImageProbeCache = {
signature,
reachable: false,
okUntil: 0,
inflight: null,
};
const primary = getPrimaryBangumiImageSource();
if (getBackupBangumiImageSource(primary)) {
markBangumiImageFallbackActive();
}
return false;
}
})();
bangumiImageProbeCache = {
signature,
reachable: bangumiImageProbeCache.reachable,
okUntil: bangumiImageProbeCache.okUntil,
inflight: probePromise,
};
return probePromise;
}
/** 非阻塞触发主源图片域探测(processImageUrl / 首屏侧调用) */
export function scheduleBangumiImagePrimaryProbe(): void {
if (typeof window === 'undefined') return;
void ensureBangumiImagePrimaryProbed();
}
function normalizeComparableUrl(url: string): string {
if (typeof window === 'undefined') return url;
@@ -441,12 +585,16 @@ export function processImageUrl(originalUrl: string): string {
// 处理 Bangumi 图片代理。直连模式尊重用户选择,不代理图片;
// 仅在服务器代理 / 自定义 Base URL 模式下使用本站图片代理。
// sticky 降级中走备源;否则用主源,并非阻塞探测主源图片域主页(5s,404 算通)。
if (isBangumiImageUrl(originalUrl)) {
const primary = getPrimaryBangumiImageSource();
const backup = getBackupBangumiImageSource(primary);
if (backup && isBangumiImageFallbackActive()) {
return buildBangumiImageUrl(originalUrl, backup);
}
if (primary !== 'server-proxy' && backup) {
scheduleBangumiImagePrimaryProbe();
}
return buildBangumiImageUrl(originalUrl, primary);
}