Spaces:
Build error
Build error
File size: 4,478 Bytes
d571830 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 | 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"
);
}
|