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////" → 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" ); }