aniqove / frontend /src /api.js
mwask's picture
Upload 59 files
d571830 verified
Raw
History Blame Contribute Delete
4.48 kB
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"
);
}