/** * The bridge returns cover art and video URLs pointing at its own loopback * media proxy, but the origin it embeds is not always the port we actually * started it on. Rebase those onto the live bridge origin, and leave any * genuinely remote URL untouched. */ const PROXY_ROUTES = new Set(['image', 'video']); const LOOPBACK_HOSTS = new Set(['127.0.0.1', 'localhost', '::1', '[::1]']); function isLoopbackProxyUrl(candidate: URL): boolean { if (candidate.protocol !== 'http:' && candidate.protocol !== 'https:') return false; const host = candidate.hostname.toLowerCase(); if (!LOOPBACK_HOSTS.has(host)) return false; const route = candidate.pathname.split('/').filter(Boolean)[0]; return route !== undefined && PROXY_ROUTES.has(route); } /** * Rewrite a bridge media URL onto `bridgeBaseUrl`, preserving path and query. * Returns the input unchanged when it is not a loopback proxy URL, or when * either URL cannot be parsed. */ export function resolveBridgeMediaUrl(bridgeBaseUrl: string, mediaUrl: string): string { let media: URL; try { media = new URL(mediaUrl); } catch { return mediaUrl; } if (!isLoopbackProxyUrl(media)) return mediaUrl; const normalizedBase = bridgeBaseUrl.includes('://') ? bridgeBaseUrl : `http://${bridgeBaseUrl}`; let base: URL; try { base = new URL(normalizedBase); } catch { return mediaUrl; } if (base.protocol !== 'http:' && base.protocol !== 'https:') return mediaUrl; if (base.hostname.length === 0) return mediaUrl; const rebased = new URL(base.origin); rebased.pathname = media.pathname; rebased.search = media.search; rebased.hash = media.hash; return rebased.toString(); } /** Aniyomi's SAnime status constants. */ export type AnimeStatus = | 'unknown' | 'ongoing' | 'completed' | 'publishing-finished' | 'cancelled' | 'on-hiatus'; export function parseAnimeStatus(status: number | undefined): AnimeStatus { switch (status) { case 1: return 'ongoing'; case 2: return 'completed'; case 4: return 'publishing-finished'; case 5: return 'cancelled'; case 6: return 'on-hiatus'; default: return 'unknown'; } }