export interface Me { id: number; email: string; display_name: string | null; avatar_url: string | null; role: string; preferences: Record; } export interface Tag { id: number; name: string; color: string | null; category: string; system: boolean; channel_count: number; } export interface Video { id: string; title: string | null; channel_id: string; channel_title: string | null; channel_thumbnail: string | null; channel_url: string; published_at: string | null; thumbnail_url: string | null; duration_seconds: number | null; view_count: number | null; is_short: boolean; live_status: string; status: string; watch_url: string; } export interface FeedResponse { items: Video[]; has_more: boolean; offset: number; limit: number; } export interface FeedFilters { tags: number[]; tagMode: "or" | "and"; q: string; sort: string; includeNormal: boolean; includeShorts: boolean; includeLive: boolean; show: string; channelId?: string; channelName?: string; maxAgeDays?: number; minDuration?: number; maxDuration?: number; dateFrom?: string; dateTo?: string; } class HttpError extends Error { status: number; constructor(status: number) { super(`HTTP ${status}`); this.status = status; } } async function req(url: string, opts: RequestInit = {}): Promise { const r = await fetch(url, { credentials: "include", headers: { "Content-Type": "application/json" }, ...opts, }); if (!r.ok) throw new HttpError(r.status); return r.status === 204 ? null : r.json(); } function feedQuery(f: FeedFilters, offset: number, limit: number): string { const p = new URLSearchParams(); f.tags.forEach((t) => p.append("tags", String(t))); p.set("tag_mode", f.tagMode); if (f.q) p.set("q", f.q); p.set("sort", f.sort); p.set("show_normal", String(f.includeNormal)); p.set("include_shorts", String(f.includeShorts)); p.set("include_live", String(f.includeLive)); p.set("show", f.show); if (f.channelId) p.set("channel_id", f.channelId); if (f.maxAgeDays) p.set("max_age_days", String(f.maxAgeDays)); if (f.dateFrom) p.set("published_after", f.dateFrom); if (f.dateTo) p.set("published_before", f.dateTo); if (f.minDuration != null) p.set("min_duration", String(f.minDuration)); if (f.maxDuration != null) p.set("max_duration", String(f.maxDuration)); p.set("offset", String(offset)); p.set("limit", String(limit)); return p.toString(); } export const api = { me: (): Promise => req("/api/me"), tags: (): Promise => req("/api/tags"), status: (): Promise => req("/api/sync/status"), feed: (f: FeedFilters, offset: number, limit: number): Promise => req(`/api/feed?${feedQuery(f, offset, limit)}`), setState: (id: string, status: string) => req(`/api/videos/${id}/state`, { method: "POST", body: JSON.stringify({ status }) }), savePrefs: (p: Record) => req("/api/me/preferences", { method: "PUT", body: JSON.stringify(p) }), }; export { HttpError };