import { API_ENDPOINT } from "./endpoint"; import { ApiError, apiFetch } from "./client"; import type { ArchiveItem, ArchiveResult, DanimeEpisode, ScheduleResponse, ShowItem, TimeResponse } from "./types"; export type { DanimeEpisode, ScheduleResponse, ShowItem, TimeResponse, ArchiveItem } from "./types"; function asNumber(v: unknown, fallback = 0) { const n = typeof v === "number" ? v : Number(v); return Number.isFinite(n) ? n : fallback; } function asString(v: unknown, fallback = "") { return typeof v === "string" ? v : fallback; } function normalizeSchedule(data: unknown): ScheduleResponse { if (data == null || typeof data !== "object") { throw new ApiError("Bad schedule payload", { url: API_ENDPOINT.v1.CURRENT, data }); } const obj = data as Record; return { ep_num: asNumber(obj.ep_num, 0), ep_title: asString(obj.ep_title, "未設定"), season_name: asString(obj.season_name, "未設定"), start_time: obj.start_time ? asString(obj.start_time) : undefined, playback_length: obj.playback_length ? asString(obj.playback_length) : undefined, }; } function normalizeShows(data: unknown): ShowItem[] { if (!Array.isArray(data)) { throw new ApiError("Shows payload is not an array", { url: API_ENDPOINT.v1.SHOWS, data }); } return data.map((item) => { if (!item || typeof item !== "object") { throw new ApiError("Bad show item", { url: API_ENDPOINT.v1.SHOWS, data: item }); } const obj = item as Record; const id = asNumber(obj.id, NaN); if (!Number.isFinite(id)) { throw new ApiError("Show item missing id", { url: API_ENDPOINT.v1.SHOWS, data: item }); } return { id, ep_num: asNumber(obj.ep_num, 0), ep_title: asString(obj.ep_title, "不明"), season_name: asString(obj.season_name, "不明"), start_time: asString(obj.start_time, ""), playback_length: asString(obj.playback_length, ""), date_created: asString(obj.date_created, ""), }; }); } function normalizeArchive(data: unknown): ArchiveItem[] { if (!Array.isArray(data)) { throw new ApiError("Archive payload is not an array", { url: API_ENDPOINT.v1.ARCHIVE, data }); } return data.map((item) => { if (!item || typeof item !== "object") { throw new ApiError("Bad archive item", { url: API_ENDPOINT.v1.ARCHIVE, data: item }); } const obj = item as Record; const id = asNumber(obj.id, NaN); if (!Number.isFinite(id)) { throw new ApiError("Archive item missing id", { url: API_ENDPOINT.v1.ARCHIVE, data: item }); } return { id, ep_num: asNumber(obj.ep_num, 0), ep_title: asString(obj.ep_title, "不明"), season_name: asString(obj.season_name, "不明"), start_time: asString(obj.start_time, ""), playback_length: asString(obj.playback_length, ""), current_ep: Boolean(obj.current_ep), date_created: asString(obj.date_created, ""), date_archived: asString(obj.date_archived, ""), }; }); } function normalizeDanimeEpisode(data: unknown): DanimeEpisode { if (!data || typeof data !== "object") { throw new ApiError("Bad danime payload", { url: API_ENDPOINT.v1.DANIME, data }); } const obj = data as Record; return { ep_num: asNumber(obj.ep_num, 0), ep_title: asString(obj.ep_title, ""), season_name: asString(obj.season_name, ""), playback_length: asString(obj.playback_length, ""), }; } export async function fetchSchedule(signal?: AbortSignal) { const data = await apiFetch(API_ENDPOINT.v1.CURRENT, { signal, timeoutMs: 12_000, logLabel: "load schedule", }); return normalizeSchedule(data); } export async function fetchShows(signal?: AbortSignal) { const data = await apiFetch(API_ENDPOINT.v1.SHOWS, { signal, timeoutMs: 12_000, logLabel: "fetch shows", }); return normalizeShows(data); } export async function postCurrentEpisode(payload: { id: number; start_time?: string }, signal?: AbortSignal) { await apiFetch(API_ENDPOINT.v1.CURRENT, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), signal, timeoutMs: 12_000, expect: "void", logLabel: "set current episode", }); } export async function fetchServerNow(signal?: AbortSignal): Promise { const data = await apiFetch(API_ENDPOINT.v1.TIME, { signal, timeoutMs: 8_000, logLabel: "server time", }); let n = typeof data?.now === "number" ? data.now : Number(data?.now); if (!Number.isFinite(n)) { throw new ApiError("Bad time payload", { url: API_ENDPOINT.v1.TIME, data }); } if (n < 1e12) n = n * 1000; // seconds → ms return n; } export async function fetchDanimeEpisode(partId: string, signal?: AbortSignal): Promise { const url = `${API_ENDPOINT.v1.DANIME}?part_id=${encodeURIComponent(partId)}`; const data = await apiFetch(url, { signal, timeoutMs: 12_000, logLabel: "fetch danime episode", }); return normalizeDanimeEpisode(data); } export async function createShow(payload: { ep_num: number; ep_title: string; season_name: string; playback_length: string; start_time?: string; }, signal?: AbortSignal) { await apiFetch(API_ENDPOINT.v1.SHOWS, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), signal, timeoutMs: 12_000, expect: "void", logLabel: "create show", }); } export async function deleteArchiveShow(id: number, idToken: string) { if (!idToken) { throw new ApiError("Missing auth token for delete"); } const url = `${API_ENDPOINT.v1.SHOWS}?id=${encodeURIComponent(id)}`; await apiFetch(url, { method: "DELETE", headers: { "Authorization": `Bearer ${idToken}`, }, timeoutMs: 10_000, expect: "void", logLabel: "delete archived show", }); } export async function archiveShow(id: number, idToken: string) { if (!idToken) { throw new ApiError("Missing auth token for archive"); } await apiFetch(API_ENDPOINT.v1.ARCHIVE, { method: "POST", headers: { "Authorization": `Bearer ${idToken}`, "Content-Type": "application/json", }, body: JSON.stringify({ ids: [id] }), timeoutMs: 10_000, expect: "json", logLabel: "archive show", }); } export async function fetchArchive(idToken: string) { if (!idToken) { throw new ApiError("Missing auth token for archive list"); } const data = await apiFetch(API_ENDPOINT.v1.ARCHIVE, { method: "GET", headers: { "Authorization": `Bearer ${idToken}`, }, timeoutMs: 12_000, logLabel: "fetch archive", }); return normalizeArchive(data); }