Spaces:
Build error
Build error
| const API_URL = (import.meta.env.VITE_API_URL || "/api").replace(/\/$/, ""); | |
| const STREAM_PROXY = (import.meta.env.VITE_STREAM_PROXY_URL || "/hls").replace(/\/$/, ""); | |
| export const STREAM_KEY = import.meta.env.VITE_STREAM_KEY || ""; | |
| import { getToken } from "./utils/auth.js"; | |
| async function req(path, options = {}) { | |
| const res = await fetch(`${API_URL}${path}`, { | |
| headers: { Accept: "application/json", ...(options.headers || {}) }, | |
| ...options, | |
| }); | |
| if (!res.ok) { | |
| let detail = `HTTP ${res.status}`; | |
| try { | |
| const body = await res.json(); | |
| detail = body.detail?.error || body.detail?.hint || body.detail || detail; | |
| // FastAPI validation errors are an array of { msg } objects — flatten them. | |
| if (Array.isArray(detail)) detail = detail.map((d) => d.msg).join("; "); | |
| } catch { | |
| /* ignore */ | |
| } | |
| const err = new Error(detail); | |
| err.status = res.status; // lets pages distinguish 404 from server errors | |
| throw err; | |
| } | |
| return res.json(); | |
| } | |
| export const api = { | |
| // catalog | |
| search: (q, page = 1, mal = false) => | |
| req(`/anime/search?q=${encodeURIComponent(q)}&page=${page}&mal=${mal ? 1 : 0}`), | |
| trending: (page = 1) => req(`/anime/trending?page=${page}`), | |
| popular: (page = 1) => req(`/anime/popular?page=${page}`), | |
| upcoming: (page = 1) => req(`/anime/upcoming?page=${page}`), | |
| recent: (page = 1) => req(`/anime/recent?page=${page}`), | |
| schedule: (page = 1) => req(`/anime/schedule?page=${page}`), | |
| details: (id) => req(`/anime/${id}`), | |
| mal: (id) => req(`/anime/${id}/mal`), | |
| // streaming | |
| episodes: (id) => req(`/anime/${id}/episodes`), | |
| watch: (episodeId) => req(`/watch/${episodeId}`), | |
| // manga (backed by the MangaVault sidecar) | |
| mangaHome: () => req(`/manga/home`), | |
| mangaSearch: (q) => req(`/manga/search?q=${encodeURIComponent(q)}`), | |
| mangaDetails: (source, id) => req(`/manga/${source}/${encodeURIComponent(id)}/details`), | |
| mangaChapterImages: (path) => req(`/manga/chapter-images?path=${encodeURIComponent(path)}`), | |
| mangaImg: (url, src) => `${API_URL}/manga/img?url=${encodeURIComponent(url)}&src=${src}`, | |
| // auth — local accounts + AniList OAuth | |
| authRegister: (username, email, password) => | |
| req("/auth/register", { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify({ username, email, password }), | |
| }), | |
| authLogin: (identifier, password) => | |
| req("/auth/login", { | |
| method: "POST", | |
| headers: { "Content-Type": "application/json" }, | |
| body: JSON.stringify({ identifier, password }), | |
| }), | |
| authMe: () => req("/auth/me", { headers: { Authorization: `Bearer ${getToken()}` } }), | |
| authLogout: () => | |
| req("/auth/logout", { | |
| method: "POST", | |
| headers: { Authorization: `Bearer ${getToken()}` }, | |
| }), | |
| // movies & tv (backed by the Moviebox-API sidecar) | |
| movieHome: () => req(`/movies/home`), | |
| movieCatalog: (type = "movie", page = 1, sort = "RECOMMEND") => | |
| req(`/movies/catalog?type=${type}&page=${page}&sort=${sort}`), | |
| movieSearch: (q, page = 1) => | |
| req(`/movies/search?q=${encodeURIComponent(q)}&page=${page}`), | |
| movieSuggest: (q) => req(`/movies/suggest?q=${encodeURIComponent(q)}`), | |
| movieDetails: (slug) => req(`/movies/${encodeURIComponent(slug)}`), | |
| movieStream: (slug, se = 0, ep = 0) => | |
| req(`/movies/${encodeURIComponent(slug)}/stream?se=${se}&ep=${ep}`), | |
| // images | |
| img: (url) => (url ? `${API_URL}/img?url=${encodeURIComponent(url)}` : ""), | |
| }; | |
| /** Wrap an upstream m3u8/segment URL so it flows through the HLS proxy worker. | |
| * `ref` (optional) sets the Referer/Origin the worker sends upstream. */ | |
| export function proxiedStreamUrl(url, ref) { | |
| let out = `${STREAM_PROXY}?url=${encodeURIComponent(url)}`; | |
| if (ref) out += `&ref=${encodeURIComponent(ref)}`; | |
| return out; | |
| } | |
| export function streamHeaders() { | |
| return STREAM_KEY ? { "x-stream-key": STREAM_KEY } : {}; | |
| } | |
| /** Parse "watch/<provider>/<anilistId>/<category>/<ref>" → parts. */ | |
| export function parseEpisodeId(episodeId) { | |
| const parts = (episodeId || "").split("/"); | |
| if (parts.length >= 4 && parts[0] === "watch") { | |
| return { | |
| provider: parts[1], | |
| anilistId: Number(parts[2]), | |
| category: parts[3], | |
| ref: parts.slice(4).join("/"), | |
| }; | |
| } | |
| return null; | |
| } | |
| /** Pick the best title from a media object. */ | |
| export function titleOf(media) { | |
| return ( | |
| media?.title?.english || | |
| media?.title?.romaji || | |
| media?.title?.native || | |
| "Untitled" | |
| ); | |
| } | |