Files
ersatztv/web/src/api/trakt.ts
T
timothy 5c932fdc54 feat(web): Trakt Lists screen for SPA parity (#142)
Adds web/src/api/trakt.ts wrapping the new /api/trakt/* endpoints
(#160) plus URL-assert tests, and web/src/screens/TraktListsScreen.tsx:
a table (slug, name, match count) with add-by-URL, match/refresh,
delete, and a sub-path editor at /app/trakt-lists/{id} (slug read-only,
autoRefresh/generatePlaylist toggles, save via PUT) - same allowSubPaths
pattern as ChannelEditScreen/SettingsScreen. Since add/match/delete are
async background jobs, the screen polls GET /api/trakt/status while
busy (disabling actions) and refreshes the list on the busy -> idle
transition.

"View matched items" links out to the classic Blazor search page
(/search?query=trakt_list:{traktId}) since the SPA has no search
screen yet (#161) - same interim deep-link pattern used elsewhere.
Registered in App.tsx's Media nav group; SettingsScreen's Classic-UI
help text no longer lists trakt now that it's SPA-native.
2026-07-07 15:09:30 +02:00

71 lines
2.5 KiB
TypeScript

import { ApiError, request } from './client';
import type { components } from './generated/v1';
export type TraktList = components['schemas']['TraktListResponseModel'];
export type PagedTraktLists = components['schemas']['PagedTraktListsResponseModel'];
export type TraktStatus = components['schemas']['TraktStatusResponseModel'];
export type AddTraktListRequest = components['schemas']['AddTraktListRequest'];
export type UpdateTraktListRequest = components['schemas']['UpdateTraktListRequest'];
export interface GetTraktListsParams {
pageNum?: number;
pageSize?: number;
}
export function getTraktLists(params: GetTraktListsParams = {}): Promise<PagedTraktLists> {
const searchParams = new URLSearchParams();
if (params.pageNum != null) {
searchParams.set('pageNum', String(params.pageNum));
}
if (params.pageSize != null) {
searchParams.set('pageSize', String(params.pageSize));
}
const queryString = searchParams.toString();
return request<PagedTraktLists>(`/api/trakt/lists${queryString ? `?${queryString}` : ''}`);
}
export function getTraktListById(id: number): Promise<TraktList> {
return request<TraktList>(`/api/trakt/lists/${id}`);
}
// 202 Accepted: the server dispatches to the same background worker channel the classic
// UI's "Add Trakt List" dialog uses. Fetch/save/match all happen asynchronously — poll
// getTraktStatus() and reload the list once it goes idle.
export function addTraktList(url: string): Promise<void> {
return request<void>('/api/trakt/lists', { body: { url } satisfies AddTraktListRequest, method: 'POST' });
}
// 202 Accepted; see addTraktList for the async/poll pattern.
export function matchTraktList(id: number): Promise<void> {
return request<void>(`/api/trakt/lists/${id}/match`, { method: 'POST' });
}
// 202 Accepted; see addTraktList for the async/poll pattern.
export function deleteTraktList(id: number): Promise<void> {
return request<void>(`/api/trakt/lists/${id}`, { method: 'DELETE' });
}
export function updateTraktList(id: number, body: UpdateTraktListRequest): Promise<TraktList> {
return request<TraktList>(`/api/trakt/lists/${id}`, { body, method: 'PUT' });
}
export function getTraktStatus(): Promise<TraktStatus> {
return request<TraktStatus>('/api/trakt/status');
}
export function messageFromTraktError(error: unknown, fallback = 'Unable to load Trakt lists'): string {
if (error instanceof ApiError) {
return error.detail ?? error.message;
}
if (error instanceof Error) {
return error.message;
}
return fallback;
}