Files
k-tv/k-tv-frontend/lib/api.ts
Gabriel Kaszewski bf07a65dcd feat(library): add media library browsing functionality
- Introduced new `library` module in the API routes to handle media library requests.
- Enhanced `AppState` to include a media provider for library interactions.
- Defined new `IMediaProvider` trait methods for listing collections, series, and genres.
- Implemented Jellyfin media provider methods for fetching collections and series.
- Added frontend components for selecting series and displaying filter previews.
- Created hooks for fetching collections, series, and genres from the library.
- Updated media filter to support series name and search term.
- Enhanced API client to handle new library-related endpoints.
2026-03-12 02:54:30 +01:00

175 lines
4.9 KiB
TypeScript

import type {
TokenResponse,
UserResponse,
ConfigResponse,
ChannelResponse,
CreateChannelRequest,
UpdateChannelRequest,
ScheduleResponse,
ScheduledSlotResponse,
CurrentBroadcastResponse,
CollectionResponse,
SeriesResponse,
LibraryItemResponse,
MediaFilter,
} from "@/lib/types";
const API_BASE =
process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:4000/api/v1";
export class ApiRequestError extends Error {
constructor(
public readonly status: number,
message: string,
) {
super(message);
this.name = "ApiRequestError";
}
}
async function request<T>(
path: string,
options: RequestInit & { token?: string } = {},
): Promise<T> {
const { token, ...init } = options;
const headers = new Headers(init.headers);
if (token) {
headers.set("Authorization", `Bearer ${token}`);
}
if (init.body && !headers.has("Content-Type")) {
headers.set("Content-Type", "application/json");
}
const res = await fetch(`${API_BASE}${path}`, { ...init, headers });
if (!res.ok) {
let message = res.statusText;
try {
const body = await res.json();
message = body.message ?? body.error ?? message;
} catch {
// ignore parse error, use statusText
}
throw new ApiRequestError(res.status, message);
}
if (res.status === 204) return null as T;
return res.json() as Promise<T>;
}
export const api = {
config: {
get: () => request<ConfigResponse>("/config"),
},
auth: {
register: (email: string, password: string) =>
request<TokenResponse>("/auth/register", {
method: "POST",
body: JSON.stringify({ email, password }),
}),
login: (email: string, password: string) =>
request<TokenResponse>("/auth/login", {
method: "POST",
body: JSON.stringify({ email, password }),
}),
logout: (token: string) =>
request<void>("/auth/logout", { method: "POST", token }),
me: (token: string) => request<UserResponse>("/auth/me", { token }),
},
channels: {
list: (token: string) =>
request<ChannelResponse[]>("/channels", { token }),
get: (id: string, token: string) =>
request<ChannelResponse>(`/channels/${id}`, { token }),
create: (data: CreateChannelRequest, token: string) =>
request<ChannelResponse>("/channels", {
method: "POST",
body: JSON.stringify(data),
token,
}),
update: (id: string, data: UpdateChannelRequest, token: string) =>
request<ChannelResponse>(`/channels/${id}`, {
method: "PUT",
body: JSON.stringify(data),
token,
}),
delete: (id: string, token: string) =>
request<void>(`/channels/${id}`, { method: "DELETE", token }),
},
library: {
collections: (token: string) =>
request<CollectionResponse[]>("/library/collections", { token }),
series: (token: string, collectionId?: string) => {
const params = new URLSearchParams();
if (collectionId) params.set("collection", collectionId);
const qs = params.toString();
return request<SeriesResponse[]>(`/library/series${qs ? `?${qs}` : ""}`, { token });
},
genres: (token: string, contentType?: string) => {
const params = new URLSearchParams();
if (contentType) params.set("type", contentType);
const qs = params.toString();
return request<string[]>(`/library/genres${qs ? `?${qs}` : ""}`, { token });
},
items: (
token: string,
filter: Pick<MediaFilter, "content_type" | "series_name" | "collections" | "search_term" | "genres">,
limit = 50,
) => {
const params = new URLSearchParams();
if (filter.search_term) params.set("q", filter.search_term);
if (filter.content_type) params.set("type", filter.content_type);
if (filter.series_name) params.set("series", filter.series_name);
if (filter.collections?.[0]) params.set("collection", filter.collections[0]);
params.set("limit", String(limit));
return request<LibraryItemResponse[]>(`/library/items?${params}`, { token });
},
},
schedule: {
generate: (channelId: string, token: string) =>
request<ScheduleResponse>(`/channels/${channelId}/schedule`, {
method: "POST",
token,
}),
getActive: (channelId: string, token: string) =>
request<ScheduleResponse>(`/channels/${channelId}/schedule`, { token }),
getCurrentBroadcast: (channelId: string, token: string) =>
request<CurrentBroadcastResponse | null>(`/channels/${channelId}/now`, {
token,
}),
getEpg: (
channelId: string,
token: string,
from?: string,
until?: string,
) => {
const params = new URLSearchParams();
if (from) params.set("from", from);
if (until) params.set("until", until);
const qs = params.toString();
return request<ScheduledSlotResponse[]>(
`/channels/${channelId}/epg${qs ? `?${qs}` : ""}`,
{ token },
);
},
},
};