mirror of
https://github.com/KuekHaoYang/KVideo.git
synced 2026-08-12 23:33:43 +08:00
fix: harden sync and image fallback; release 4.9.19
This commit is contained in:
@@ -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
@@ -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({
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user