fix: harden sync and image fallback; release 4.9.19

This commit is contained in:
kuekhaoyang
2026-07-31 23:17:50 +08:00
parent 4983a9e2ab
commit f1a0086701
13 changed files with 172 additions and 46 deletions
+30
View File
@@ -0,0 +1,30 @@
// Douban's imgN.doubanio.com hosts mirror the same paths but use different
// network routes. Some routes are unreachable from overseas deployments, so a
// failed request can retry the same path through known reachable mirrors.
const DOUBAN_IMAGE_HOSTS = [
'img9.doubanio.com',
'img3.doubanio.com',
'img2.doubanio.com',
];
export function buildDoubanImageCandidates(rawUrl: string): string[] {
const candidates = [rawUrl];
try {
const parsed = new URL(rawUrl);
if (!/^img\d+\.doubanio\.com$/.test(parsed.hostname)) {
return candidates;
}
for (const host of DOUBAN_IMAGE_HOSTS) {
if (host === parsed.hostname) continue;
const alternate = new URL(parsed.toString());
alternate.hostname = host;
candidates.push(alternate.toString());
}
} catch {
// Leave malformed URLs to fetch so the route returns its normal proxy error.
}
return candidates;
}
+3 -4
View File
@@ -2,7 +2,7 @@ import { Redis } from '@upstash/redis/cloudflare';
import { getRuntimeEnvValue } from '@/lib/server/runtime-env';
let cachedRedis: Redis | null | undefined;
let cachedRedis: Redis | undefined;
/**
* Build the shared Upstash client from whichever environment source is available.
@@ -17,15 +17,14 @@ let cachedRedis: Redis | null | undefined;
* "server-side sync unavailable" rather than as a request failure.
*/
export function getRedisClient(): Redis | null {
if (cachedRedis !== undefined) {
if (cachedRedis) {
return cachedRedis;
}
const url = getRuntimeEnvValue('UPSTASH_REDIS_REST_URL');
const token = getRuntimeEnvValue('UPSTASH_REDIS_REST_TOKEN');
if (!url || !token) {
cachedRedis = null;
return cachedRedis;
return null;
}
cachedRedis = new Redis({
+1 -1
View File
@@ -16,5 +16,5 @@ export function getRuntimeEnvValue(name: string, fallback = ''): string {
// Outside Cloudflare's request runtime, fall back to process.env.
}
return process.env[name] || fallback;
return typeof process !== 'undefined' ? process.env[name] || fallback : fallback;
}
+11 -5
View File
@@ -4,19 +4,25 @@ function hasIdentity(value: unknown): boolean {
if (!value || typeof value !== 'object') return false;
const record = value as { videoId?: unknown; source?: unknown; title?: unknown };
const videoId = record.videoId;
const hasVideoId = typeof videoId === 'string' ? videoId.length > 0 : typeof videoId === 'number';
const hasVideoId = typeof videoId === 'string'
? videoId.trim().length > 0
: typeof videoId === 'number' && Number.isFinite(videoId);
return hasVideoId && typeof record.source === 'string' && typeof record.title === 'string';
return hasVideoId &&
typeof record.source === 'string' && record.source.trim().length > 0 &&
typeof record.title === 'string' && record.title.trim().length > 0;
}
/**
* Records restored from server-side sync are not guaranteed to be well formed —
* they may come from an older schema or a partial write. Rendering one without
* `videoId` throws while building the player URL, which takes down the whole
* page, so anything lacking a usable identity is dropped on the way in.
* `videoId` or `episodeIndex` throws while building the player URL, which takes
* down the whole page, so anything lacking the required URL fields is dropped.
*/
export function isRenderableHistoryItem(value: unknown): value is VideoHistoryItem {
return hasIdentity(value);
if (!hasIdentity(value)) return false;
const episodeIndex = (value as { episodeIndex?: unknown }).episodeIndex;
return typeof episodeIndex === 'number' && Number.isInteger(episodeIndex) && episodeIndex >= 0;
}
export function isRenderableFavoriteItem(value: unknown): value is FavoriteItem {