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