feat(api): #286 — mount the whole /api surface at /api/v1
Build ErsatzTV Image / decisions.md append-only (pull_request) Successful in 10s
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 10s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 1m12s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 3m4s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 8m17s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 10m36s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Build ErsatzTV Image / decisions.md append-only (pull_request) Successful in 10s
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 10s
Build ErsatzTV Image / Formatting (changed .cs conform to .editorconfig) (pull_request) Successful in 1m12s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 3m4s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 8m17s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 10m36s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Version every /api route to /api/v1 (251 controller routes + ~24 Location
headers + the scanner callback URL + the Startup request-log literal),
uniform across the machine API, auth, scanner and scripted-build surfaces.
Add ApiVersionRewriteMiddleware: a legacy unversioned /api/* request is
rewritten (NOT redirected) to /api/v1/* in-pipeline — method, body, auth
headers and query survive — carrying RFC 8594 Deprecation/Sunset headers,
so curl / the future MCP server / bookmarks keep working. An already-
versioned path passes through; a future /api/v2 is never forced to v1.
Standardize the route convention (leading-slash absolute route per method,
no class-[Route] — except the two Scanner/Scripted controllers whose ~all
actions share a parametrized {id} prefix), enforced by ApiRouteVersioningTests
(^/api/v\d+/ over the whole Controllers.Api surface; browser-nav
/auth/oidc/login is out of scope).
Regenerate v1.json (160 paths, all /api/v1)/endpoint-index/v1.d.ts; sweep 945
SPA request literals + the test mocks (regex + positional URL parsers). /api/v1
is additive-only after freeze; the legacy-rewrite shim sunsets in ~2 releases
(owner decision) with removal tracked as a Phase-3 follow-up.
Docs: decisions.md 2026-07-13, api-conventions §1/§9, rest-api/spa-conventions/
blazor-route-parity/e2e-local/domain-model.
fixes #286
refs #197
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -9,7 +9,7 @@ export function uploadArtwork(file: File, target: ArtworkUploadTarget): Promise<
|
||||
body.append('file', file);
|
||||
body.append('target', target);
|
||||
|
||||
return request<ArtworkUpload>('/api/artwork/uploads', {
|
||||
return request<ArtworkUpload>('/api/v1/artwork/uploads', {
|
||||
body,
|
||||
method: 'POST'
|
||||
});
|
||||
|
||||
+15
-15
@@ -48,7 +48,7 @@ describe('auth endpoints', () => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('GET /api/auth/config', async () => {
|
||||
it('GET /api/v1/auth/config', async () => {
|
||||
const fetchMock = vi
|
||||
.spyOn(window, 'fetch')
|
||||
.mockResolvedValue(jsonResponse({ oidcEnabled: true, localLoginEnabled: true, setupRequired: false }));
|
||||
@@ -56,10 +56,10 @@ describe('auth endpoints', () => {
|
||||
const config = await getAuthConfig();
|
||||
|
||||
expect(config).toEqual({ oidcEnabled: true, localLoginEnabled: true, setupRequired: false });
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/auth/config', expect.objectContaining({ method: 'GET' }));
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/v1/auth/config', expect.objectContaining({ method: 'GET' }));
|
||||
});
|
||||
|
||||
it('GET /api/auth/session', async () => {
|
||||
it('GET /api/v1/auth/session', async () => {
|
||||
const fetchMock = vi
|
||||
.spyOn(window, 'fetch')
|
||||
.mockResolvedValue(jsonResponse({ authenticated: true, username: 'admin', method: 'local' }));
|
||||
@@ -67,10 +67,10 @@ describe('auth endpoints', () => {
|
||||
const session = await getAuthSession();
|
||||
|
||||
expect(session).toEqual({ authenticated: true, username: 'admin', method: 'local' });
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/auth/session', expect.objectContaining({ method: 'GET' }));
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/v1/auth/session', expect.objectContaining({ method: 'GET' }));
|
||||
});
|
||||
|
||||
it('GET /api/auth/session for an anonymous caller (username/method omitted)', async () => {
|
||||
it('GET /api/v1/auth/session for an anonymous caller (username/method omitted)', async () => {
|
||||
// The server serializes anonymous sessions as `{ "authenticated": false }` — username/method are
|
||||
// dropped by Newtonsoft's global NullValueHandling.Ignore, so AuthSession must treat them as
|
||||
// optional/undefined rather than present-but-null.
|
||||
@@ -83,7 +83,7 @@ describe('auth endpoints', () => {
|
||||
expect(session.method).toBeUndefined();
|
||||
});
|
||||
|
||||
it('POST /api/auth/login with credentials in the body', async () => {
|
||||
it('POST /api/v1/auth/login with credentials in the body', async () => {
|
||||
const fetchMock = vi
|
||||
.spyOn(window, 'fetch')
|
||||
.mockResolvedValue(jsonResponse({ authenticated: true, username: 'admin', method: 'local' }));
|
||||
@@ -91,7 +91,7 @@ describe('auth endpoints', () => {
|
||||
await login('admin', 'hunter2');
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
'/api/auth/login',
|
||||
'/api/v1/auth/login',
|
||||
expect.objectContaining({ body: JSON.stringify({ username: 'admin', password: 'hunter2' }), method: 'POST' })
|
||||
);
|
||||
});
|
||||
@@ -107,7 +107,7 @@ describe('auth endpoints', () => {
|
||||
unsubscribe();
|
||||
});
|
||||
|
||||
it('POST /api/auth/setup with credentials in the body', async () => {
|
||||
it('POST /api/v1/auth/setup with credentials in the body', async () => {
|
||||
const fetchMock = vi
|
||||
.spyOn(window, 'fetch')
|
||||
.mockResolvedValue(jsonResponse({ authenticated: true, username: 'admin', method: 'local' }));
|
||||
@@ -115,37 +115,37 @@ describe('auth endpoints', () => {
|
||||
await setup('admin', 'hunter2');
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
'/api/auth/setup',
|
||||
'/api/v1/auth/setup',
|
||||
expect.objectContaining({ body: JSON.stringify({ username: 'admin', password: 'hunter2' }), method: 'POST' })
|
||||
);
|
||||
});
|
||||
|
||||
it('POST /api/auth/logout', async () => {
|
||||
it('POST /api/v1/auth/logout', async () => {
|
||||
const fetchMock = vi.spyOn(window, 'fetch').mockResolvedValue(new Response(null, { status: 204 }));
|
||||
|
||||
await logout();
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/auth/logout', expect.objectContaining({ method: 'POST' }));
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/v1/auth/logout', expect.objectContaining({ method: 'POST' }));
|
||||
});
|
||||
|
||||
it('POST /api/auth/password with both passwords, suppressing the 401 banner', async () => {
|
||||
it('POST /api/v1/auth/password with both passwords, suppressing the 401 banner', async () => {
|
||||
const fetchMock = vi.spyOn(window, 'fetch').mockResolvedValue(new Response(null, { status: 204 }));
|
||||
|
||||
await changePassword('old', 'new');
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
'/api/auth/password',
|
||||
'/api/v1/auth/password',
|
||||
expect.objectContaining({ body: JSON.stringify({ currentPassword: 'old', newPassword: 'new' }), method: 'POST' })
|
||||
);
|
||||
});
|
||||
|
||||
it('GET /api/auth/machine-key', async () => {
|
||||
it('GET /api/v1/auth/machine-key', async () => {
|
||||
const fetchMock = vi.spyOn(window, 'fetch').mockResolvedValue(jsonResponse({ apiKey: 'abc123' }));
|
||||
|
||||
const result = await getMachineKey();
|
||||
|
||||
expect(result).toEqual({ apiKey: 'abc123' });
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/auth/machine-key', expect.objectContaining({ method: 'GET' }));
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/v1/auth/machine-key', expect.objectContaining({ method: 'GET' }));
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
+11
-11
@@ -1,20 +1,20 @@
|
||||
import { request } from './client';
|
||||
|
||||
// --- wire types -------------------------------------------------------------
|
||||
// The auth surface (`/api/auth/*`) is deliberately excluded from the OpenAPI document (server-side
|
||||
// The auth surface (`/api/v1/auth/*`) is deliberately excluded from the OpenAPI document (server-side
|
||||
// [IgnoreApi]), so — unlike every other domain module (spa-conventions §4) — these DTOs are NOT in the
|
||||
// generated types and are hand-written here. Keep the field names camelCase to match the server records
|
||||
// exactly (the JSON the auth endpoints emit). This deviation is intentional; do not try to source these
|
||||
// from `./generated/v1`.
|
||||
|
||||
/** `GET /api/auth/config` — PUBLIC; drives the boot gate. */
|
||||
/** `GET /api/v1/auth/config` — PUBLIC; drives the boot gate. */
|
||||
export interface AuthConfig {
|
||||
oidcEnabled: boolean;
|
||||
localLoginEnabled: boolean;
|
||||
setupRequired: boolean;
|
||||
}
|
||||
|
||||
/** `GET /api/auth/session` — anonymous callers get 200 with `authenticated: false` (never 401). */
|
||||
/** `GET /api/v1/auth/session` — anonymous callers get 200 with `authenticated: false` (never 401). */
|
||||
export interface AuthSession {
|
||||
authenticated: boolean;
|
||||
// Omitted (undefined) for anonymous callers — the server serializes `{ "authenticated": false }` and
|
||||
@@ -24,7 +24,7 @@ export interface AuthSession {
|
||||
}
|
||||
|
||||
/**
|
||||
* `GET /api/auth/machine-key` — the server-generated machine API key.
|
||||
* `GET /api/v1/auth/machine-key` — the server-generated machine API key.
|
||||
* The wire field is `apiKey` (server record `MachineKeyResponse(string ApiKey)`), not `key`.
|
||||
*/
|
||||
export interface MachineKey {
|
||||
@@ -78,16 +78,16 @@ export function notifyUnauthorized(): void {
|
||||
// --- auth endpoints ---------------------------------------------------------
|
||||
|
||||
export function getAuthConfig(): Promise<AuthConfig> {
|
||||
return request<AuthConfig>('/api/auth/config');
|
||||
return request<AuthConfig>('/api/v1/auth/config');
|
||||
}
|
||||
|
||||
export function getAuthSession(): Promise<AuthSession> {
|
||||
return request<AuthSession>('/api/auth/session');
|
||||
return request<AuthSession>('/api/v1/auth/session');
|
||||
}
|
||||
|
||||
export function login(username: string, password: string): Promise<AuthSession> {
|
||||
// A wrong-password 401 is an expected inline answer here — it must NOT trip the global 401 banner.
|
||||
return request<AuthSession>('/api/auth/login', {
|
||||
return request<AuthSession>('/api/v1/auth/login', {
|
||||
body: { username, password },
|
||||
method: 'POST',
|
||||
suppressUnauthorizedSignal: true
|
||||
@@ -95,19 +95,19 @@ export function login(username: string, password: string): Promise<AuthSession>
|
||||
}
|
||||
|
||||
export function setup(username: string, password: string): Promise<AuthSession> {
|
||||
return request<AuthSession>('/api/auth/setup', {
|
||||
return request<AuthSession>('/api/v1/auth/setup', {
|
||||
body: { username, password },
|
||||
method: 'POST'
|
||||
});
|
||||
}
|
||||
|
||||
export function logout(): Promise<void> {
|
||||
return request<void>('/api/auth/logout', { method: 'POST' });
|
||||
return request<void>('/api/v1/auth/logout', { method: 'POST' });
|
||||
}
|
||||
|
||||
export function changePassword(currentPassword: string, newPassword: string): Promise<void> {
|
||||
// A wrong current-password 401 is shown inline, not via the global banner.
|
||||
return request<void>('/api/auth/password', {
|
||||
return request<void>('/api/v1/auth/password', {
|
||||
body: { currentPassword, newPassword },
|
||||
method: 'POST',
|
||||
suppressUnauthorizedSignal: true
|
||||
@@ -115,5 +115,5 @@ export function changePassword(currentPassword: string, newPassword: string): Pr
|
||||
}
|
||||
|
||||
export function getMachineKey(): Promise<MachineKey> {
|
||||
return request<MachineKey>('/api/auth/machine-key');
|
||||
return request<MachineKey>('/api/v1/auth/machine-key');
|
||||
}
|
||||
|
||||
+15
-15
@@ -58,14 +58,14 @@ describe('blocks api client', () => {
|
||||
it('getBlockGroups fetches all block groups', async () => {
|
||||
const fetchMock = vi.spyOn(window, 'fetch').mockResolvedValue(jsonResponse([{ id: 1, name: 'Prime' }]));
|
||||
await expect(getBlockGroups()).resolves.toHaveLength(1);
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/blocks/groups', expect.objectContaining({ method: 'GET' }));
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/v1/blocks/groups', expect.objectContaining({ method: 'GET' }));
|
||||
});
|
||||
|
||||
it('createBlockGroup POSTs the name', async () => {
|
||||
const fetchMock = vi.spyOn(window, 'fetch').mockResolvedValue(jsonResponse({ id: 3, name: 'Prime' }, 201));
|
||||
await createBlockGroup({ name: 'Prime' });
|
||||
const [url, init] = fetchMock.mock.calls[0];
|
||||
expect(url).toBe('/api/blocks/groups');
|
||||
expect(url).toBe('/api/v1/blocks/groups');
|
||||
expect(init).toMatchObject({ method: 'POST' });
|
||||
expect(JSON.parse(String(init?.body)).name).toBe('Prime');
|
||||
});
|
||||
@@ -73,26 +73,26 @@ describe('blocks api client', () => {
|
||||
it('deleteBlockGroup DELETEs by id', async () => {
|
||||
const fetchMock = vi.spyOn(window, 'fetch').mockResolvedValue(noContent());
|
||||
await expect(deleteBlockGroup(3)).resolves.toBeUndefined();
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/blocks/groups/3', expect.objectContaining({ method: 'DELETE' }));
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/v1/blocks/groups/3', expect.objectContaining({ method: 'DELETE' }));
|
||||
});
|
||||
|
||||
it('getBlocks fetches all blocks', async () => {
|
||||
const fetchMock = vi.spyOn(window, 'fetch').mockResolvedValue(jsonResponse([]));
|
||||
await getBlocks();
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/blocks', expect.objectContaining({ method: 'GET' }));
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/v1/blocks', expect.objectContaining({ method: 'GET' }));
|
||||
});
|
||||
|
||||
it('getBlock fetches a block by id', async () => {
|
||||
const fetchMock = vi.spyOn(window, 'fetch').mockResolvedValue(jsonResponse({ id: 4 }));
|
||||
await getBlock(4);
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/blocks/4', expect.objectContaining({ method: 'GET' }));
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/v1/blocks/4', expect.objectContaining({ method: 'GET' }));
|
||||
});
|
||||
|
||||
it('createBlock POSTs group id and name', async () => {
|
||||
const fetchMock = vi.spyOn(window, 'fetch').mockResolvedValue(jsonResponse({ id: 8 }, 201));
|
||||
await createBlock({ blockGroupId: 2, name: 'Morning' });
|
||||
const [url, init] = fetchMock.mock.calls[0];
|
||||
expect(url).toBe('/api/blocks');
|
||||
expect(url).toBe('/api/v1/blocks');
|
||||
expect(init).toMatchObject({ method: 'POST' });
|
||||
expect(JSON.parse(String(init?.body))).toMatchObject({ blockGroupId: 2, name: 'Morning' });
|
||||
});
|
||||
@@ -100,20 +100,20 @@ describe('blocks api client', () => {
|
||||
it('deleteBlock DELETEs by id', async () => {
|
||||
const fetchMock = vi.spyOn(window, 'fetch').mockResolvedValue(noContent());
|
||||
await deleteBlock(4);
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/blocks/4', expect.objectContaining({ method: 'DELETE' }));
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/v1/blocks/4', expect.objectContaining({ method: 'DELETE' }));
|
||||
});
|
||||
|
||||
it('getBlockItems fetches items for a block', async () => {
|
||||
const fetchMock = vi.spyOn(window, 'fetch').mockResolvedValue(jsonResponse([]));
|
||||
await getBlockItems(4);
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/blocks/4/items', expect.objectContaining({ method: 'GET' }));
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/v1/blocks/4/items', expect.objectContaining({ method: 'GET' }));
|
||||
});
|
||||
|
||||
it('replaceBlock PUTs the full block body', async () => {
|
||||
const fetchMock = vi.spyOn(window, 'fetch').mockResolvedValue(jsonResponse({ id: 4, items: [] }));
|
||||
await replaceBlock(4, sampleReplace);
|
||||
const [url, init] = fetchMock.mock.calls[0];
|
||||
expect(url).toBe('/api/blocks/4');
|
||||
expect(url).toBe('/api/v1/blocks/4');
|
||||
expect(init).toMatchObject({ method: 'PUT' });
|
||||
const body = JSON.parse(String(init?.body));
|
||||
expect(body.minutes).toBe(60);
|
||||
@@ -162,7 +162,7 @@ describe('blocks api client', () => {
|
||||
const fetchMock = vi.spyOn(window, 'fetch').mockResolvedValue(jsonResponse([]));
|
||||
await previewBlock(4, sampleReplace);
|
||||
const [url, init] = fetchMock.mock.calls[0];
|
||||
expect(url).toBe('/api/blocks/4/preview');
|
||||
expect(url).toBe('/api/v1/blocks/4/preview');
|
||||
expect(init).toMatchObject({ method: 'POST' });
|
||||
});
|
||||
|
||||
@@ -170,7 +170,7 @@ describe('blocks api client', () => {
|
||||
const fetchMock = vi.spyOn(window, 'fetch').mockResolvedValue(jsonResponse({ id: 9 }, 201));
|
||||
await copyBlock(4, { blockGroupId: 3, name: 'Morning Copy' });
|
||||
const [url, init] = fetchMock.mock.calls[0];
|
||||
expect(url).toBe('/api/blocks/4/copy');
|
||||
expect(url).toBe('/api/v1/blocks/4/copy');
|
||||
expect(init).toMatchObject({ method: 'POST' });
|
||||
expect(JSON.parse(String(init?.body))).toMatchObject({ blockGroupId: 3, name: 'Morning Copy' });
|
||||
});
|
||||
@@ -182,10 +182,10 @@ describe('blocks api client', () => {
|
||||
await searchTelevisionSeasons('season');
|
||||
await searchSmartCollections('smart');
|
||||
expect(fetchMock.mock.calls.map((c) => c[0])).toEqual([
|
||||
'/api/search/collections?query=a%20b',
|
||||
'/api/search/television-shows?query=show',
|
||||
'/api/search/television-seasons?query=season',
|
||||
'/api/search/smart-collections?query=smart'
|
||||
'/api/v1/search/collections?query=a%20b',
|
||||
'/api/v1/search/television-shows?query=show',
|
||||
'/api/v1/search/television-seasons?query=season',
|
||||
'/api/v1/search/smart-collections?query=smart'
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
+18
-18
@@ -16,42 +16,42 @@ export type SchedulingPickerOption = components['schemas']['SchedulingPickerOpti
|
||||
// Block groups
|
||||
|
||||
export function getBlockGroups(): Promise<BlockGroup[]> {
|
||||
return request<BlockGroup[]>('/api/blocks/groups');
|
||||
return request<BlockGroup[]>('/api/v1/blocks/groups');
|
||||
}
|
||||
|
||||
export function createBlockGroup(body: CreateBlockGroupRequest): Promise<BlockGroup> {
|
||||
return request<BlockGroup>('/api/blocks/groups', { body, method: 'POST' });
|
||||
return request<BlockGroup>('/api/v1/blocks/groups', { body, method: 'POST' });
|
||||
}
|
||||
|
||||
export function deleteBlockGroup(id: number): Promise<void> {
|
||||
return request<void>(`/api/blocks/groups/${id}`, { method: 'DELETE' });
|
||||
return request<void>(`/api/v1/blocks/groups/${id}`, { method: 'DELETE' });
|
||||
}
|
||||
|
||||
// Blocks
|
||||
|
||||
export function getBlocks(): Promise<Block[]> {
|
||||
return request<Block[]>('/api/blocks');
|
||||
return request<Block[]>('/api/v1/blocks');
|
||||
}
|
||||
|
||||
export function getBlock(id: number): Promise<Block> {
|
||||
return request<Block>(`/api/blocks/${id}`);
|
||||
return request<Block>(`/api/v1/blocks/${id}`);
|
||||
}
|
||||
|
||||
export function createBlock(body: CreateBlockRequest): Promise<Block> {
|
||||
return request<Block>('/api/blocks', { body, method: 'POST' });
|
||||
return request<Block>('/api/v1/blocks', { body, method: 'POST' });
|
||||
}
|
||||
|
||||
export function deleteBlock(id: number): Promise<void> {
|
||||
return request<void>(`/api/blocks/${id}`, { method: 'DELETE' });
|
||||
return request<void>(`/api/v1/blocks/${id}`, { method: 'DELETE' });
|
||||
}
|
||||
|
||||
export function getBlockItems(id: number): Promise<BlockItem[]> {
|
||||
return request<BlockItem[]>(`/api/blocks/${id}/items`);
|
||||
return request<BlockItem[]>(`/api/v1/blocks/${id}/items`);
|
||||
}
|
||||
|
||||
/** Load block items together with the block's concurrency ETag (issue #253). */
|
||||
export function getBlockItemsWithMeta(id: number): Promise<ResponseWithMeta<BlockItem[]>> {
|
||||
return requestWithMeta<BlockItem[]>(`/api/blocks/${id}/items`);
|
||||
return requestWithMeta<BlockItem[]>(`/api/v1/blocks/${id}/items`);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -63,7 +63,7 @@ export function replaceBlock(
|
||||
body: ReplaceBlockRequest,
|
||||
ifMatch?: string | null
|
||||
): Promise<ResponseWithMeta<BlockWithItems>> {
|
||||
return requestWithMeta<BlockWithItems>(`/api/blocks/${id}`, {
|
||||
return requestWithMeta<BlockWithItems>(`/api/v1/blocks/${id}`, {
|
||||
body,
|
||||
method: 'PUT',
|
||||
headers: ifMatch ? { 'If-Match': ifMatch } : undefined
|
||||
@@ -71,11 +71,11 @@ export function replaceBlock(
|
||||
}
|
||||
|
||||
export function previewBlock(id: number, body: ReplaceBlockRequest): Promise<BlockPreviewItem[]> {
|
||||
return request<BlockPreviewItem[]>(`/api/blocks/${id}/preview`, { body, method: 'POST' });
|
||||
return request<BlockPreviewItem[]>(`/api/v1/blocks/${id}/preview`, { body, method: 'POST' });
|
||||
}
|
||||
|
||||
export function copyBlock(id: number, body: CopyBlockRequest): Promise<Block> {
|
||||
return request<Block>(`/api/blocks/${id}/copy`, { body, method: 'POST' });
|
||||
return request<Block>(`/api/v1/blocks/${id}/copy`, { body, method: 'POST' });
|
||||
}
|
||||
|
||||
// Scheduling search pickers
|
||||
@@ -85,27 +85,27 @@ function searchOptions(path: string, query: string): Promise<SchedulingPickerOpt
|
||||
}
|
||||
|
||||
export function searchCollections(query: string): Promise<SchedulingPickerOption[]> {
|
||||
return searchOptions('/api/search/collections', query);
|
||||
return searchOptions('/api/v1/search/collections', query);
|
||||
}
|
||||
|
||||
export function searchTelevisionShows(query: string): Promise<SchedulingPickerOption[]> {
|
||||
return searchOptions('/api/search/television-shows', query);
|
||||
return searchOptions('/api/v1/search/television-shows', query);
|
||||
}
|
||||
|
||||
export function searchTelevisionSeasons(query: string): Promise<SchedulingPickerOption[]> {
|
||||
return searchOptions('/api/search/television-seasons', query);
|
||||
return searchOptions('/api/v1/search/television-seasons', query);
|
||||
}
|
||||
|
||||
export function searchSmartCollections(query: string): Promise<SchedulingPickerOption[]> {
|
||||
return searchOptions('/api/search/smart-collections', query);
|
||||
return searchOptions('/api/v1/search/smart-collections', query);
|
||||
}
|
||||
|
||||
export function searchArtists(query: string): Promise<SchedulingPickerOption[]> {
|
||||
return searchOptions('/api/search/artists', query);
|
||||
return searchOptions('/api/v1/search/artists', query);
|
||||
}
|
||||
|
||||
export function searchMultiCollections(query: string): Promise<SchedulingPickerOption[]> {
|
||||
return searchOptions('/api/search/multi-collections', query);
|
||||
return searchOptions('/api/v1/search/multi-collections', query);
|
||||
}
|
||||
|
||||
export function messageFromBlockError(error: unknown, fallback = 'Unable to load blocks'): string {
|
||||
|
||||
@@ -5,12 +5,12 @@ export type ChannelTemplate = components['schemas']['ChannelTemplateResponseMode
|
||||
export type CreateChannelTemplateRequest = components['schemas']['CreateChannelTemplateRequest'];
|
||||
|
||||
export function getChannelTemplates(): Promise<ChannelTemplate[]> {
|
||||
return request<ChannelTemplate[]>('/api/channel-templates');
|
||||
return request<ChannelTemplate[]>('/api/v1/channel-templates');
|
||||
}
|
||||
|
||||
export async function getDefaultChannelTemplate(): Promise<ChannelTemplate | null> {
|
||||
try {
|
||||
return await request<ChannelTemplate>('/api/channel-templates/default');
|
||||
return await request<ChannelTemplate>('/api/v1/channel-templates/default');
|
||||
} catch (error) {
|
||||
if (error instanceof ApiError && error.status === 404) {
|
||||
return null;
|
||||
@@ -21,7 +21,7 @@ export async function getDefaultChannelTemplate(): Promise<ChannelTemplate | nul
|
||||
}
|
||||
|
||||
export function createChannelTemplate(body: CreateChannelTemplateRequest): Promise<ChannelTemplate> {
|
||||
return request<ChannelTemplate>('/api/channel-templates', {
|
||||
return request<ChannelTemplate>('/api/v1/channel-templates', {
|
||||
body,
|
||||
method: 'POST'
|
||||
});
|
||||
|
||||
@@ -58,7 +58,7 @@ describe('getChannelById', () => {
|
||||
await expect(getChannelById(5)).resolves.toMatchObject({ id: 5, name: 'Cartoons' });
|
||||
|
||||
const [url, init] = fetchSpy.mock.calls[0];
|
||||
expect(url).toBe('/api/channels/5');
|
||||
expect(url).toBe('/api/v1/channels/5');
|
||||
expect((init?.method ?? 'GET').toUpperCase()).toBe('GET');
|
||||
});
|
||||
|
||||
@@ -93,7 +93,7 @@ describe('updateChannel', () => {
|
||||
await expect(updateChannel(5, body)).resolves.toMatchObject({ name: 'Renamed' });
|
||||
|
||||
const [url, init] = fetchSpy.mock.calls[0];
|
||||
expect(url).toBe('/api/channels/5');
|
||||
expect(url).toBe('/api/v1/channels/5');
|
||||
expect((init?.method ?? '').toUpperCase()).toBe('PUT');
|
||||
expect(JSON.parse(init?.body as string)).toMatchObject({ name: 'Renamed', number: '5' });
|
||||
});
|
||||
@@ -105,7 +105,7 @@ describe('createChannel', () => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('POSTs the request body to /api/channels and returns the created view model', async () => {
|
||||
it('POSTs the request body to /api/v1/channels and returns the created view model', async () => {
|
||||
const fetchSpy = vi.spyOn(window, 'fetch').mockResolvedValue(
|
||||
new Response(JSON.stringify(sampleChannel), {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
@@ -118,7 +118,7 @@ describe('createChannel', () => {
|
||||
await expect(createChannel(body)).resolves.toMatchObject({ id: 5 });
|
||||
|
||||
const [url, init] = fetchSpy.mock.calls[0];
|
||||
expect(url).toBe('/api/channels');
|
||||
expect(url).toBe('/api/v1/channels');
|
||||
expect((init?.method ?? '').toUpperCase()).toBe('POST');
|
||||
});
|
||||
});
|
||||
@@ -129,7 +129,7 @@ describe('getMusicVideoCreditsTemplates', () => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('GETs /api/channels/music-video-credits-templates', async () => {
|
||||
it('GETs /api/v1/channels/music-video-credits-templates', async () => {
|
||||
const fetchSpy = vi.spyOn(window, 'fetch').mockResolvedValue(
|
||||
new Response(JSON.stringify(['default']), {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
@@ -140,7 +140,7 @@ describe('getMusicVideoCreditsTemplates', () => {
|
||||
await expect(getMusicVideoCreditsTemplates()).resolves.toEqual(['default']);
|
||||
|
||||
const [url] = fetchSpy.mock.calls[0];
|
||||
expect(url).toBe('/api/channels/music-video-credits-templates');
|
||||
expect(url).toBe('/api/v1/channels/music-video-credits-templates');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -150,7 +150,7 @@ describe('getChannelStreamSelectors', () => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('GETs /api/channels/stream-selectors', async () => {
|
||||
it('GETs /api/v1/channels/stream-selectors', async () => {
|
||||
const fetchSpy = vi.spyOn(window, 'fetch').mockResolvedValue(
|
||||
new Response(JSON.stringify(['selector.py']), {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
@@ -161,6 +161,6 @@ describe('getChannelStreamSelectors', () => {
|
||||
await expect(getChannelStreamSelectors()).resolves.toEqual(['selector.py']);
|
||||
|
||||
const [url] = fetchSpy.mock.calls[0];
|
||||
expect(url).toBe('/api/channels/stream-selectors');
|
||||
expect(url).toBe('/api/v1/channels/stream-selectors');
|
||||
});
|
||||
});
|
||||
|
||||
+12
-12
@@ -29,37 +29,37 @@ type ChannelsScreenState =
|
||||
| { data: null; error: null; status: 'loading' };
|
||||
|
||||
export function getChannels(): Promise<ChannelSummary[]> {
|
||||
return request<ChannelSummary[]>('/api/channels');
|
||||
return request<ChannelSummary[]>('/api/v1/channels');
|
||||
}
|
||||
|
||||
export function getChannelStates(): Promise<ChannelState[]> {
|
||||
return request<ChannelState[]>('/api/channels/state');
|
||||
return request<ChannelState[]>('/api/v1/channels/state');
|
||||
}
|
||||
|
||||
export function getChannelById(channelId: number): Promise<Channel> {
|
||||
return request<Channel>(`/api/channels/${channelId}`);
|
||||
return request<Channel>(`/api/v1/channels/${channelId}`);
|
||||
}
|
||||
|
||||
export function updateChannel(channelId: number, body: UpdateChannelRequest): Promise<Channel> {
|
||||
return request<Channel>(`/api/channels/${channelId}`, {
|
||||
return request<Channel>(`/api/v1/channels/${channelId}`, {
|
||||
body,
|
||||
method: 'PUT'
|
||||
});
|
||||
}
|
||||
|
||||
export function createChannel(body: CreateChannelRequest): Promise<Channel> {
|
||||
return request<Channel>('/api/channels', {
|
||||
return request<Channel>('/api/v1/channels', {
|
||||
body,
|
||||
method: 'POST'
|
||||
});
|
||||
}
|
||||
|
||||
export function getMusicVideoCreditsTemplates(): Promise<string[]> {
|
||||
return request<string[]>('/api/channels/music-video-credits-templates');
|
||||
return request<string[]>('/api/v1/channels/music-video-credits-templates');
|
||||
}
|
||||
|
||||
export function getChannelStreamSelectors(): Promise<string[]> {
|
||||
return request<string[]>('/api/channels/stream-selectors');
|
||||
return request<string[]>('/api/v1/channels/stream-selectors');
|
||||
}
|
||||
|
||||
export async function getChannelsScreenData(): Promise<ChannelsScreenData> {
|
||||
@@ -72,28 +72,28 @@ export async function getChannelsScreenData(): Promise<ChannelsScreenData> {
|
||||
}
|
||||
|
||||
export function bulkRenumberChannels(body: BulkRenumberChannelsRequest): Promise<void> {
|
||||
return request<void>('/api/channels/bulk/renumber', {
|
||||
return request<void>('/api/v1/channels/bulk/renumber', {
|
||||
body,
|
||||
method: 'POST'
|
||||
});
|
||||
}
|
||||
|
||||
export function bulkMoveChannelsToGroup(body: BulkMoveChannelsToGroupRequest): Promise<void> {
|
||||
return request<void>('/api/channels/bulk/group', {
|
||||
return request<void>('/api/v1/channels/bulk/group', {
|
||||
body,
|
||||
method: 'POST'
|
||||
});
|
||||
}
|
||||
|
||||
export function bulkDeleteChannels(body: BulkDeleteChannelsRequest): Promise<void> {
|
||||
return request<void>('/api/channels/bulk/delete', {
|
||||
return request<void>('/api/v1/channels/bulk/delete', {
|
||||
body,
|
||||
method: 'POST'
|
||||
});
|
||||
}
|
||||
|
||||
export function deleteChannel(channelId: number): Promise<void> {
|
||||
return request<void>(`/api/channels/${channelId}`, {
|
||||
return request<void>(`/api/v1/channels/${channelId}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
}
|
||||
@@ -101,7 +101,7 @@ export function deleteChannel(channelId: number): Promise<void> {
|
||||
export function createChannelFromLineup(
|
||||
body: CreateChannelFromLineupRequest
|
||||
): Promise<CreateChannelFromLineupResponseModel> {
|
||||
return request<CreateChannelFromLineupResponseModel>('/api/channels/from-lineup', {
|
||||
return request<CreateChannelFromLineupResponseModel>('/api/v1/channels/from-lineup', {
|
||||
body,
|
||||
method: 'POST'
|
||||
});
|
||||
|
||||
+16
-16
@@ -19,10 +19,10 @@ describe('API request client', () => {
|
||||
async (method) => {
|
||||
const fetchMock = vi.spyOn(window, 'fetch').mockResolvedValue(new Response(null, { status: 204 }));
|
||||
|
||||
await request('/api/channels/1', { method });
|
||||
await request('/api/v1/channels/1', { method });
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
'/api/channels/1',
|
||||
'/api/v1/channels/1',
|
||||
expect.objectContaining({
|
||||
headers: expect.objectContaining({ 'X-Csrf': '1' }),
|
||||
method
|
||||
@@ -39,10 +39,10 @@ describe('API request client', () => {
|
||||
})
|
||||
);
|
||||
|
||||
await request('/api/channels');
|
||||
await request('/api/v1/channels');
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
'/api/channels',
|
||||
'/api/v1/channels',
|
||||
expect.objectContaining({
|
||||
headers: expect.not.objectContaining({ 'X-Csrf': expect.anything() })
|
||||
})
|
||||
@@ -54,8 +54,8 @@ describe('API request client', () => {
|
||||
const fetchMock = vi.spyOn(window, 'fetch').mockResolvedValue(new Response(null, { status: 204 }));
|
||||
window.localStorage.setItem('ctv-api-key', 'legacy-secret');
|
||||
|
||||
await request('/api/channels');
|
||||
await request('/api/channels/1', { method: 'DELETE' });
|
||||
await request('/api/v1/channels');
|
||||
await request('/api/v1/channels/1', { method: 'DELETE' });
|
||||
|
||||
for (const call of fetchMock.mock.calls) {
|
||||
const headers = (call[1] as RequestInit | undefined)?.headers as Record<string, string> | undefined;
|
||||
@@ -71,7 +71,7 @@ describe('API request client', () => {
|
||||
})
|
||||
);
|
||||
|
||||
await expect(request('/api/channels', { method: 'POST', body: { name: 'News' } })).rejects.toMatchObject({
|
||||
await expect(request('/api/v1/channels', { method: 'POST', body: { name: 'News' } })).rejects.toMatchObject({
|
||||
detail: 'Number exists',
|
||||
message: 'Validation failed',
|
||||
status: 422
|
||||
@@ -81,7 +81,7 @@ describe('API request client', () => {
|
||||
it('returns undefined for successful responses without a JSON body', async () => {
|
||||
vi.spyOn(window, 'fetch').mockResolvedValue(new Response(null, { status: 200 }));
|
||||
|
||||
await expect(request('/api/channels', { method: 'POST' })).resolves.toBeUndefined();
|
||||
await expect(request('/api/v1/channels', { method: 'POST' })).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it('signals unauthorized subscribers on a 401 response', async () => {
|
||||
@@ -95,7 +95,7 @@ describe('API request client', () => {
|
||||
const onUnauthorized = vi.fn();
|
||||
const unsubscribe = subscribeUnauthorized(onUnauthorized);
|
||||
|
||||
await expect(request('/api/channels')).rejects.toMatchObject({ status: 401 });
|
||||
await expect(request('/api/v1/channels')).rejects.toMatchObject({ status: 401 });
|
||||
|
||||
expect(onUnauthorized).toHaveBeenCalledTimes(1);
|
||||
unsubscribe();
|
||||
@@ -113,7 +113,7 @@ describe('API request client', () => {
|
||||
const unsubscribe = subscribeUnauthorized(onUnauthorized);
|
||||
|
||||
await expect(
|
||||
request('/api/auth/login', { body: { username: 'x', password: 'y' }, method: 'POST', suppressUnauthorizedSignal: true })
|
||||
request('/api/v1/auth/login', { body: { username: 'x', password: 'y' }, method: 'POST', suppressUnauthorizedSignal: true })
|
||||
).rejects.toMatchObject({ status: 401 });
|
||||
|
||||
expect(onUnauthorized).not.toHaveBeenCalled();
|
||||
@@ -131,7 +131,7 @@ describe('API request client', () => {
|
||||
const onUnauthorized = vi.fn();
|
||||
const unsubscribe = subscribeUnauthorized(onUnauthorized);
|
||||
|
||||
await expect(request('/api/channels')).rejects.toMatchObject({ status: 403 });
|
||||
await expect(request('/api/v1/channels')).rejects.toMatchObject({ status: 403 });
|
||||
|
||||
expect(onUnauthorized).not.toHaveBeenCalled();
|
||||
unsubscribe();
|
||||
@@ -140,14 +140,14 @@ describe('API request client', () => {
|
||||
it('does not add a duplicate JSON content type when callers provide lowercase content-type', async () => {
|
||||
const fetchMock = vi.spyOn(window, 'fetch').mockResolvedValue(new Response(null, { status: 204 }));
|
||||
|
||||
await request('/api/channels', {
|
||||
await request('/api/v1/channels', {
|
||||
body: { name: 'News' },
|
||||
headers: { 'content-type': 'application/merge-patch+json' },
|
||||
method: 'PATCH'
|
||||
});
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
'/api/channels',
|
||||
'/api/v1/channels',
|
||||
expect.objectContaining({
|
||||
headers: expect.not.objectContaining({ 'Content-Type': 'application/json' })
|
||||
})
|
||||
@@ -162,7 +162,7 @@ describe('API request client', () => {
|
||||
})
|
||||
);
|
||||
|
||||
const result = await requestWithMeta<{ id: number }[]>('/api/blocks/1/items');
|
||||
const result = await requestWithMeta<{ id: number }[]>('/api/v1/blocks/1/items');
|
||||
|
||||
expect(result.etag).toBe('"7"');
|
||||
expect(result.data).toEqual([{ id: 1 }]);
|
||||
@@ -176,7 +176,7 @@ describe('API request client', () => {
|
||||
})
|
||||
);
|
||||
|
||||
const result = await requestWithMeta('/api/blocks/1/items');
|
||||
const result = await requestWithMeta('/api/v1/blocks/1/items');
|
||||
|
||||
expect(result.etag).toBeNull();
|
||||
});
|
||||
@@ -184,7 +184,7 @@ describe('API request client', () => {
|
||||
it('requestWithMeta surfaces the ETag on a 204 (no body) response', async () => {
|
||||
vi.spyOn(window, 'fetch').mockResolvedValue(new Response(null, { headers: { ETag: '"3"' }, status: 204 }));
|
||||
|
||||
const result = await requestWithMeta('/api/blocks/1', { method: 'PUT' });
|
||||
const result = await requestWithMeta('/api/v1/blocks/1', { method: 'PUT' });
|
||||
|
||||
expect(result.data).toBeUndefined();
|
||||
expect(result.etag).toBe('"3"');
|
||||
|
||||
@@ -61,7 +61,7 @@ describe('collections api client', () => {
|
||||
.mockResolvedValue(jsonResponse([{ id: 1, name: 'Movies', useCustomPlaybackOrder: false }]));
|
||||
|
||||
await expect(getCollections()).resolves.toHaveLength(1);
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/collections', expect.objectContaining({ method: 'GET' }));
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/v1/collections', expect.objectContaining({ method: 'GET' }));
|
||||
});
|
||||
|
||||
it('createCollection POSTs the name and returns the created collection', async () => {
|
||||
@@ -72,7 +72,7 @@ describe('collections api client', () => {
|
||||
await expect(createCollection({ name: 'New' })).resolves.toMatchObject({ id: 7 });
|
||||
|
||||
const [url, init] = fetchMock.mock.calls[0];
|
||||
expect(url).toBe('/api/collections');
|
||||
expect(url).toBe('/api/v1/collections');
|
||||
expect(init).toMatchObject({ method: 'POST' });
|
||||
expect(JSON.parse(String(init?.body))).toEqual({ name: 'New' });
|
||||
});
|
||||
@@ -85,7 +85,7 @@ describe('collections api client', () => {
|
||||
await updateCollection(3, { name: 'Renamed', useCustomPlaybackOrder: true });
|
||||
|
||||
const [url, init] = fetchMock.mock.calls[0];
|
||||
expect(url).toBe('/api/collections/3');
|
||||
expect(url).toBe('/api/v1/collections/3');
|
||||
expect(init).toMatchObject({ method: 'PUT' });
|
||||
expect(JSON.parse(String(init?.body))).toEqual({ name: 'Renamed', useCustomPlaybackOrder: true });
|
||||
});
|
||||
@@ -94,7 +94,7 @@ describe('collections api client', () => {
|
||||
const fetchMock = vi.spyOn(window, 'fetch').mockResolvedValue(noContent());
|
||||
|
||||
await expect(deleteCollection(9)).resolves.toBeUndefined();
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/collections/9', expect.objectContaining({ method: 'DELETE' }));
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/v1/collections/9', expect.objectContaining({ method: 'DELETE' }));
|
||||
});
|
||||
|
||||
it('addItemsToCollection POSTs the bucketed request', async () => {
|
||||
@@ -104,7 +104,7 @@ describe('collections api client', () => {
|
||||
await addItemsToCollection(5, body);
|
||||
|
||||
const [url, init] = fetchMock.mock.calls[0];
|
||||
expect(url).toBe('/api/collections/5/items');
|
||||
expect(url).toBe('/api/v1/collections/5/items');
|
||||
expect(init).toMatchObject({ method: 'POST' });
|
||||
expect(JSON.parse(String(init?.body)).movieIds).toEqual([11, 12]);
|
||||
});
|
||||
@@ -113,7 +113,7 @@ describe('collections api client', () => {
|
||||
const fetchMock = vi.spyOn(window, 'fetch').mockResolvedValue(noContent());
|
||||
|
||||
await removeItemFromCollection(5, 42);
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/collections/5/items/42', expect.objectContaining({ method: 'DELETE' }));
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/v1/collections/5/items/42', expect.objectContaining({ method: 'DELETE' }));
|
||||
});
|
||||
|
||||
it('rethrows API errors (e.g. 422 on delete)', async () => {
|
||||
@@ -129,19 +129,19 @@ describe('collections api client', () => {
|
||||
const url = input.toString();
|
||||
const method = (init?.method ?? 'GET').toUpperCase();
|
||||
|
||||
if (url === '/api/smart-collections' && method === 'GET') {
|
||||
if (url === '/api/v1/smart-collections' && method === 'GET') {
|
||||
return Promise.resolve(jsonResponse([{ id: 1, name: 'Action', query: 'genre:action' }]));
|
||||
}
|
||||
|
||||
if (url === '/api/smart-collections' && method === 'POST') {
|
||||
if (url === '/api/v1/smart-collections' && method === 'POST') {
|
||||
return Promise.resolve(jsonResponse({ id: 2, name: 'Sci-Fi', query: 'genre:scifi' }, 201));
|
||||
}
|
||||
|
||||
if (url === '/api/smart-collections/2' && method === 'PUT') {
|
||||
if (url === '/api/v1/smart-collections/2' && method === 'PUT') {
|
||||
return Promise.resolve(jsonResponse({ id: 2, name: 'Sci-Fi', query: 'genre:"science fiction"' }));
|
||||
}
|
||||
|
||||
if (url === '/api/smart-collections/2' && method === 'DELETE') {
|
||||
if (url === '/api/v1/smart-collections/2' && method === 'DELETE') {
|
||||
return Promise.resolve(noContent());
|
||||
}
|
||||
|
||||
@@ -167,7 +167,7 @@ describe('collections api client', () => {
|
||||
const result = await getCollectionItems(7, 2, 50);
|
||||
|
||||
const url = new URL(String(fetchMock.mock.calls[0][0]), 'http://localhost');
|
||||
expect(url.pathname).toBe('/api/collections/7/items');
|
||||
expect(url.pathname).toBe('/api/v1/collections/7/items');
|
||||
expect(url.searchParams.get('pageNum')).toBe('2');
|
||||
expect(url.searchParams.get('pageSize')).toBe('50');
|
||||
expect(result.totalCount).toBe(1);
|
||||
@@ -181,7 +181,7 @@ describe('collections api client', () => {
|
||||
await getCollectionItems(9);
|
||||
|
||||
const url = new URL(String(fetchMock.mock.calls[0][0]), 'http://localhost');
|
||||
expect(url.pathname).toBe('/api/collections/9/items');
|
||||
expect(url.pathname).toBe('/api/v1/collections/9/items');
|
||||
expect(url.searchParams.get('pageNum')).toBe('0');
|
||||
expect(url.searchParams.get('pageSize')).toBe('100');
|
||||
});
|
||||
@@ -192,7 +192,7 @@ describe('collections api client', () => {
|
||||
await updateCollectionCustomOrder(7, [30, 10, 20]);
|
||||
|
||||
const [url, init] = fetchMock.mock.calls[0];
|
||||
expect(url).toBe('/api/collections/7/custom-order');
|
||||
expect(url).toBe('/api/v1/collections/7/custom-order');
|
||||
expect(init).toMatchObject({ method: 'PUT' });
|
||||
expect(JSON.parse(String(init?.body))).toEqual({ mediaItemIds: [30, 10, 20] });
|
||||
});
|
||||
|
||||
+16
-16
@@ -14,35 +14,35 @@ export type UpdateSmartCollectionRequest = components['schemas']['UpdateSmartCol
|
||||
/* ---------- manual collections ---------- */
|
||||
|
||||
export function getCollections(): Promise<MediaCollection[]> {
|
||||
return request<MediaCollection[]>('/api/collections');
|
||||
return request<MediaCollection[]>('/api/v1/collections');
|
||||
}
|
||||
|
||||
export function getCollection(id: number): Promise<MediaCollection> {
|
||||
return request<MediaCollection>(`/api/collections/${id}`);
|
||||
return request<MediaCollection>(`/api/v1/collections/${id}`);
|
||||
}
|
||||
|
||||
export function createCollection(body: CreateCollectionRequest): Promise<MediaCollection> {
|
||||
return request<MediaCollection>('/api/collections', { body, method: 'POST' });
|
||||
return request<MediaCollection>('/api/v1/collections', { body, method: 'POST' });
|
||||
}
|
||||
|
||||
export function updateCollection(id: number, body: UpdateCollectionRequest): Promise<MediaCollection> {
|
||||
return request<MediaCollection>(`/api/collections/${id}`, { body, method: 'PUT' });
|
||||
return request<MediaCollection>(`/api/v1/collections/${id}`, { body, method: 'PUT' });
|
||||
}
|
||||
|
||||
export function deleteCollection(id: number): Promise<void> {
|
||||
return request<void>(`/api/collections/${id}`, { method: 'DELETE' });
|
||||
return request<void>(`/api/v1/collections/${id}`, { method: 'DELETE' });
|
||||
}
|
||||
|
||||
export function addItemsToCollection(id: number, body: AddItemsToCollectionRequest): Promise<void> {
|
||||
return request<void>(`/api/collections/${id}/items`, { body, method: 'POST' });
|
||||
return request<void>(`/api/v1/collections/${id}/items`, { body, method: 'POST' });
|
||||
}
|
||||
|
||||
export function removeItemFromCollection(id: number, mediaItemId: number): Promise<void> {
|
||||
return request<void>(`/api/collections/${id}/items/${mediaItemId}`, { method: 'DELETE' });
|
||||
return request<void>(`/api/v1/collections/${id}/items/${mediaItemId}`, { method: 'DELETE' });
|
||||
}
|
||||
|
||||
// Lists a manual collection's full contents (all media kinds), paged. Backed by
|
||||
// GET /api/collections/{id}/items (#155), which reuses the library-browse item shape. When the
|
||||
// GET /api/v1/collections/{id}/items (#155), which reuses the library-browse item shape. When the
|
||||
// collection's useCustomPlaybackOrder is true, items come back ordered by CustomIndex (nulls
|
||||
// last, then title); otherwise title order.
|
||||
export function getCollectionItems(
|
||||
@@ -54,7 +54,7 @@ export function getCollectionItems(
|
||||
pageNum: String(pageNum),
|
||||
pageSize: String(pageSize)
|
||||
});
|
||||
return request<PagedLibraryBrowseItems>(`/api/collections/${id}/items?${params.toString()}`);
|
||||
return request<PagedLibraryBrowseItems>(`/api/v1/collections/${id}/items?${params.toString()}`);
|
||||
}
|
||||
|
||||
/** Load a page of collection items together with the collection's concurrency ETag (issue #253). */
|
||||
@@ -67,7 +67,7 @@ export function getCollectionItemsWithMeta(
|
||||
pageNum: String(pageNum),
|
||||
pageSize: String(pageSize)
|
||||
});
|
||||
return requestWithMeta<PagedLibraryBrowseItems>(`/api/collections/${id}/items?${params.toString()}`);
|
||||
return requestWithMeta<PagedLibraryBrowseItems>(`/api/v1/collections/${id}/items?${params.toString()}`);
|
||||
}
|
||||
|
||||
// Replaces a manual collection's custom order wholesale: the CustomIndex of each media item is
|
||||
@@ -82,7 +82,7 @@ export function updateCollectionCustomOrder(
|
||||
ifMatch?: string | null
|
||||
): Promise<ResponseWithMeta<void>> {
|
||||
const body: UpdateCollectionCustomOrderRequest = { mediaItemIds };
|
||||
return requestWithMeta<void>(`/api/collections/${id}/custom-order`, {
|
||||
return requestWithMeta<void>(`/api/v1/collections/${id}/custom-order`, {
|
||||
body,
|
||||
method: 'PUT',
|
||||
headers: ifMatch ? { 'If-Match': ifMatch } : undefined
|
||||
@@ -92,23 +92,23 @@ export function updateCollectionCustomOrder(
|
||||
/* ---------- smart collections ---------- */
|
||||
|
||||
export function getSmartCollections(): Promise<SmartCollection[]> {
|
||||
return request<SmartCollection[]>('/api/smart-collections');
|
||||
return request<SmartCollection[]>('/api/v1/smart-collections');
|
||||
}
|
||||
|
||||
export function getSmartCollection(id: number): Promise<SmartCollection> {
|
||||
return request<SmartCollection>(`/api/smart-collections/${id}`);
|
||||
return request<SmartCollection>(`/api/v1/smart-collections/${id}`);
|
||||
}
|
||||
|
||||
export function createSmartCollection(body: CreateSmartCollectionRequest): Promise<SmartCollection> {
|
||||
return request<SmartCollection>('/api/smart-collections', { body, method: 'POST' });
|
||||
return request<SmartCollection>('/api/v1/smart-collections', { body, method: 'POST' });
|
||||
}
|
||||
|
||||
export function updateSmartCollection(id: number, body: UpdateSmartCollectionRequest): Promise<SmartCollection> {
|
||||
return request<SmartCollection>(`/api/smart-collections/${id}`, { body, method: 'PUT' });
|
||||
return request<SmartCollection>(`/api/v1/smart-collections/${id}`, { body, method: 'PUT' });
|
||||
}
|
||||
|
||||
export function deleteSmartCollection(id: number): Promise<void> {
|
||||
return request<void>(`/api/smart-collections/${id}`, { method: 'DELETE' });
|
||||
return request<void>(`/api/v1/smart-collections/${id}`, { method: 'DELETE' });
|
||||
}
|
||||
|
||||
/* ---------- add-items bucket mapping ---------- */
|
||||
|
||||
@@ -38,21 +38,21 @@ type DashboardHealthState =
|
||||
|
||||
export async function getDashboardData(): Promise<DashboardData> {
|
||||
const [channels, channelStates, mediaSources, playouts] = await Promise.all([
|
||||
request<DashboardChannel[]>('/api/channels'),
|
||||
request<DashboardChannelState[]>('/api/channels/state'),
|
||||
request<DashboardMediaSource[]>('/api/media-sources'),
|
||||
request<DashboardPlayouts>('/api/playouts')
|
||||
request<DashboardChannel[]>('/api/v1/channels'),
|
||||
request<DashboardChannelState[]>('/api/v1/channels/state'),
|
||||
request<DashboardMediaSource[]>('/api/v1/media-sources'),
|
||||
request<DashboardPlayouts>('/api/v1/playouts')
|
||||
]);
|
||||
|
||||
return { channels, channelStates, mediaSources, playouts };
|
||||
}
|
||||
|
||||
export function getDashboardHealth(): Promise<DashboardHealthCheck[]> {
|
||||
return request<DashboardHealthCheck[]>('/api/health');
|
||||
return request<DashboardHealthCheck[]>('/api/v1/health');
|
||||
}
|
||||
|
||||
export function getDashboardVersion(): Promise<DashboardVersion> {
|
||||
return request<DashboardVersion>('/api/version');
|
||||
return request<DashboardVersion>('/api/v1/version');
|
||||
}
|
||||
|
||||
export function useDashboardQuery(): DashboardQueryState {
|
||||
|
||||
@@ -37,7 +37,7 @@ describe('decoTemplates api client', () => {
|
||||
.mockResolvedValue(jsonResponse([{ id: 1, name: 'Weekday', decoTemplateCount: 2 }]));
|
||||
await expect(getDecoTemplateGroups()).resolves.toHaveLength(1);
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
'/api/deco-templates/groups',
|
||||
'/api/v1/deco-templates/groups',
|
||||
expect.objectContaining({ method: 'GET' })
|
||||
);
|
||||
});
|
||||
@@ -48,7 +48,7 @@ describe('decoTemplates api client', () => {
|
||||
.mockResolvedValue(jsonResponse({ id: 3, name: 'Weekday', decoTemplateCount: 0 }, 201));
|
||||
await createDecoTemplateGroup({ name: 'Weekday' });
|
||||
const [url, init] = fetchMock.mock.calls[0];
|
||||
expect(url).toBe('/api/deco-templates/groups');
|
||||
expect(url).toBe('/api/v1/deco-templates/groups');
|
||||
expect(init).toMatchObject({ method: 'POST' });
|
||||
expect(JSON.parse(String(init?.body)).name).toBe('Weekday');
|
||||
});
|
||||
@@ -57,7 +57,7 @@ describe('decoTemplates api client', () => {
|
||||
const fetchMock = vi.spyOn(window, 'fetch').mockResolvedValue(noContent());
|
||||
await expect(deleteDecoTemplateGroup(3)).resolves.toBeUndefined();
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
'/api/deco-templates/groups/3',
|
||||
'/api/v1/deco-templates/groups/3',
|
||||
expect.objectContaining({ method: 'DELETE' })
|
||||
);
|
||||
});
|
||||
@@ -65,20 +65,20 @@ describe('decoTemplates api client', () => {
|
||||
it('getDecoTemplates fetches all deco templates', async () => {
|
||||
const fetchMock = vi.spyOn(window, 'fetch').mockResolvedValue(jsonResponse([]));
|
||||
await getDecoTemplates();
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/deco-templates', expect.objectContaining({ method: 'GET' }));
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/v1/deco-templates', expect.objectContaining({ method: 'GET' }));
|
||||
});
|
||||
|
||||
it('getDecoTemplate fetches a deco template by id', async () => {
|
||||
const fetchMock = vi.spyOn(window, 'fetch').mockResolvedValue(jsonResponse({ id: 4 }));
|
||||
await getDecoTemplate(4);
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/deco-templates/4', expect.objectContaining({ method: 'GET' }));
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/v1/deco-templates/4', expect.objectContaining({ method: 'GET' }));
|
||||
});
|
||||
|
||||
it('createDecoTemplate POSTs group id and name', async () => {
|
||||
const fetchMock = vi.spyOn(window, 'fetch').mockResolvedValue(jsonResponse({ id: 8 }, 201));
|
||||
await createDecoTemplate({ decoTemplateGroupId: 2, name: 'Weekdays' });
|
||||
const [url, init] = fetchMock.mock.calls[0];
|
||||
expect(url).toBe('/api/deco-templates');
|
||||
expect(url).toBe('/api/v1/deco-templates');
|
||||
expect(init).toMatchObject({ method: 'POST' });
|
||||
expect(JSON.parse(String(init?.body))).toMatchObject({ decoTemplateGroupId: 2, name: 'Weekdays' });
|
||||
});
|
||||
@@ -86,14 +86,14 @@ describe('decoTemplates api client', () => {
|
||||
it('deleteDecoTemplate DELETEs by id', async () => {
|
||||
const fetchMock = vi.spyOn(window, 'fetch').mockResolvedValue(noContent());
|
||||
await deleteDecoTemplate(4);
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/deco-templates/4', expect.objectContaining({ method: 'DELETE' }));
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/v1/deco-templates/4', expect.objectContaining({ method: 'DELETE' }));
|
||||
});
|
||||
|
||||
it('getDecoTemplateItems fetches items for a deco template', async () => {
|
||||
const fetchMock = vi.spyOn(window, 'fetch').mockResolvedValue(jsonResponse([]));
|
||||
await getDecoTemplateItems(4);
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
'/api/deco-templates/4/items',
|
||||
'/api/v1/deco-templates/4/items',
|
||||
expect.objectContaining({ method: 'GET' })
|
||||
);
|
||||
});
|
||||
@@ -102,7 +102,7 @@ describe('decoTemplates api client', () => {
|
||||
const fetchMock = vi.spyOn(window, 'fetch').mockResolvedValue(jsonResponse({ id: 4, items: [] }));
|
||||
await replaceDecoTemplate(4, sampleReplace);
|
||||
const [url, init] = fetchMock.mock.calls[0];
|
||||
expect(url).toBe('/api/deco-templates/4');
|
||||
expect(url).toBe('/api/v1/deco-templates/4');
|
||||
expect(init).toMatchObject({ method: 'PUT' });
|
||||
const body = JSON.parse(String(init?.body));
|
||||
expect(body.name).toBe('Weekdays');
|
||||
|
||||
@@ -13,42 +13,42 @@ export type DecoTemplateItemRequest = components['schemas']['DecoTemplateItemReq
|
||||
// Deco template groups
|
||||
|
||||
export function getDecoTemplateGroups(): Promise<DecoTemplateGroup[]> {
|
||||
return request<DecoTemplateGroup[]>('/api/deco-templates/groups');
|
||||
return request<DecoTemplateGroup[]>('/api/v1/deco-templates/groups');
|
||||
}
|
||||
|
||||
export function createDecoTemplateGroup(body: CreateDecoTemplateGroupRequest): Promise<DecoTemplateGroup> {
|
||||
return request<DecoTemplateGroup>('/api/deco-templates/groups', { body, method: 'POST' });
|
||||
return request<DecoTemplateGroup>('/api/v1/deco-templates/groups', { body, method: 'POST' });
|
||||
}
|
||||
|
||||
export function deleteDecoTemplateGroup(id: number): Promise<void> {
|
||||
return request<void>(`/api/deco-templates/groups/${id}`, { method: 'DELETE' });
|
||||
return request<void>(`/api/v1/deco-templates/groups/${id}`, { method: 'DELETE' });
|
||||
}
|
||||
|
||||
// Deco templates
|
||||
|
||||
export function getDecoTemplates(): Promise<DecoTemplate[]> {
|
||||
return request<DecoTemplate[]>('/api/deco-templates');
|
||||
return request<DecoTemplate[]>('/api/v1/deco-templates');
|
||||
}
|
||||
|
||||
export function getDecoTemplate(id: number): Promise<DecoTemplate> {
|
||||
return request<DecoTemplate>(`/api/deco-templates/${id}`);
|
||||
return request<DecoTemplate>(`/api/v1/deco-templates/${id}`);
|
||||
}
|
||||
|
||||
export function createDecoTemplate(body: CreateDecoTemplateRequest): Promise<DecoTemplate> {
|
||||
return request<DecoTemplate>('/api/deco-templates', { body, method: 'POST' });
|
||||
return request<DecoTemplate>('/api/v1/deco-templates', { body, method: 'POST' });
|
||||
}
|
||||
|
||||
export function deleteDecoTemplate(id: number): Promise<void> {
|
||||
return request<void>(`/api/deco-templates/${id}`, { method: 'DELETE' });
|
||||
return request<void>(`/api/v1/deco-templates/${id}`, { method: 'DELETE' });
|
||||
}
|
||||
|
||||
export function getDecoTemplateItems(id: number): Promise<DecoTemplateItem[]> {
|
||||
return request<DecoTemplateItem[]>(`/api/deco-templates/${id}/items`);
|
||||
return request<DecoTemplateItem[]>(`/api/v1/deco-templates/${id}/items`);
|
||||
}
|
||||
|
||||
/** Load deco template items together with the deco template's concurrency ETag (issue #253). */
|
||||
export function getDecoTemplateItemsWithMeta(id: number): Promise<ResponseWithMeta<DecoTemplateItem[]>> {
|
||||
return requestWithMeta<DecoTemplateItem[]>(`/api/deco-templates/${id}/items`);
|
||||
return requestWithMeta<DecoTemplateItem[]>(`/api/v1/deco-templates/${id}/items`);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -60,7 +60,7 @@ export function replaceDecoTemplate(
|
||||
body: ReplaceDecoTemplateRequest,
|
||||
ifMatch?: string | null
|
||||
): Promise<ResponseWithMeta<DecoTemplateWithItems>> {
|
||||
return requestWithMeta<DecoTemplateWithItems>(`/api/deco-templates/${id}`, {
|
||||
return requestWithMeta<DecoTemplateWithItems>(`/api/v1/deco-templates/${id}`, {
|
||||
body,
|
||||
method: 'PUT',
|
||||
headers: ifMatch ? { 'If-Match': ifMatch } : undefined
|
||||
|
||||
+10
-10
@@ -65,49 +65,49 @@ describe('decos api client', () => {
|
||||
it('getDecoGroups fetches all deco groups', async () => {
|
||||
const fetchMock = vi.spyOn(window, 'fetch').mockResolvedValue(jsonResponse([{ id: 1, name: 'Bumpers', decoCount: 0 }]));
|
||||
await expect(getDecoGroups()).resolves.toHaveLength(1);
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/decos/groups', expect.objectContaining({ method: 'GET' }));
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/v1/decos/groups', expect.objectContaining({ method: 'GET' }));
|
||||
});
|
||||
|
||||
it('createDecoGroup POSTs to the groups route', async () => {
|
||||
const fetchMock = vi.spyOn(window, 'fetch').mockResolvedValue(jsonResponse({ id: 5, name: 'Bumpers', decoCount: 0 }, 201));
|
||||
await createDecoGroup({ name: 'Bumpers' });
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/decos/groups', expect.objectContaining({ method: 'POST' }));
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/v1/decos/groups', expect.objectContaining({ method: 'POST' }));
|
||||
});
|
||||
|
||||
it('deleteDecoGroup DELETEs the group route', async () => {
|
||||
const fetchMock = vi.spyOn(window, 'fetch').mockResolvedValue(noContent());
|
||||
await deleteDecoGroup(3);
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/decos/groups/3', expect.objectContaining({ method: 'DELETE' }));
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/v1/decos/groups/3', expect.objectContaining({ method: 'DELETE' }));
|
||||
});
|
||||
|
||||
it('getDecos fetches the flat list', async () => {
|
||||
const fetchMock = vi.spyOn(window, 'fetch').mockResolvedValue(jsonResponse([]));
|
||||
await getDecos();
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/decos', expect.objectContaining({ method: 'GET' }));
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/v1/decos', expect.objectContaining({ method: 'GET' }));
|
||||
});
|
||||
|
||||
it('getDeco fetches a single deco', async () => {
|
||||
const fetchMock = vi.spyOn(window, 'fetch').mockResolvedValue(jsonResponse({ id: 4 }));
|
||||
await getDeco(4);
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/decos/4', expect.objectContaining({ method: 'GET' }));
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/v1/decos/4', expect.objectContaining({ method: 'GET' }));
|
||||
});
|
||||
|
||||
it('createDeco POSTs to the decos route', async () => {
|
||||
const fetchMock = vi.spyOn(window, 'fetch').mockResolvedValue(jsonResponse({ id: 8 }, 201));
|
||||
await createDeco({ decoGroupId: 2, name: 'Movie Night' });
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/decos', expect.objectContaining({ method: 'POST' }));
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/v1/decos', expect.objectContaining({ method: 'POST' }));
|
||||
});
|
||||
|
||||
it('deleteDeco DELETEs the deco route', async () => {
|
||||
const fetchMock = vi.spyOn(window, 'fetch').mockResolvedValue(noContent());
|
||||
await deleteDeco(4);
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/decos/4', expect.objectContaining({ method: 'DELETE' }));
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/v1/decos/4', expect.objectContaining({ method: 'DELETE' }));
|
||||
});
|
||||
|
||||
it('replaceDeco PUTs the full state', async () => {
|
||||
const fetchMock = vi.spyOn(window, 'fetch').mockResolvedValue(jsonResponse({ id: 4 }));
|
||||
await replaceDeco(4, sampleReplace);
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/decos/4', expect.objectContaining({ method: 'PUT' }));
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/v1/decos/4', expect.objectContaining({ method: 'PUT' }));
|
||||
const init = fetchMock.mock.calls[0][1];
|
||||
const sent = JSON.parse(String(init?.body));
|
||||
expect(sent.breakContent[0].playlistId).toBe(9);
|
||||
@@ -117,9 +117,9 @@ describe('decos api client', () => {
|
||||
const fetchMock = vi.spyOn(window, 'fetch').mockImplementation(() => Promise.resolve(jsonResponse([])));
|
||||
await searchArtists('bowie');
|
||||
await searchMultiCollections('mix');
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/search/artists?query=bowie', expect.objectContaining({ method: 'GET' }));
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/v1/search/artists?query=bowie', expect.objectContaining({ method: 'GET' }));
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
'/api/search/multi-collections?query=mix',
|
||||
'/api/v1/search/multi-collections?query=mix',
|
||||
expect.objectContaining({ method: 'GET' })
|
||||
);
|
||||
});
|
||||
|
||||
@@ -13,37 +13,37 @@ export type DecoBreakContentRequest = components['schemas']['DecoBreakContentReq
|
||||
// Deco groups
|
||||
|
||||
export function getDecoGroups(): Promise<DecoGroup[]> {
|
||||
return request<DecoGroup[]>('/api/decos/groups');
|
||||
return request<DecoGroup[]>('/api/v1/decos/groups');
|
||||
}
|
||||
|
||||
export function createDecoGroup(body: CreateDecoGroupRequest): Promise<DecoGroup> {
|
||||
return request<DecoGroup>('/api/decos/groups', { body, method: 'POST' });
|
||||
return request<DecoGroup>('/api/v1/decos/groups', { body, method: 'POST' });
|
||||
}
|
||||
|
||||
export function deleteDecoGroup(id: number): Promise<void> {
|
||||
return request<void>(`/api/decos/groups/${id}`, { method: 'DELETE' });
|
||||
return request<void>(`/api/v1/decos/groups/${id}`, { method: 'DELETE' });
|
||||
}
|
||||
|
||||
// Decos
|
||||
|
||||
export function getDecos(): Promise<DecoListItem[]> {
|
||||
return request<DecoListItem[]>('/api/decos');
|
||||
return request<DecoListItem[]>('/api/v1/decos');
|
||||
}
|
||||
|
||||
export function getDeco(id: number): Promise<Deco> {
|
||||
return request<Deco>(`/api/decos/${id}`);
|
||||
return request<Deco>(`/api/v1/decos/${id}`);
|
||||
}
|
||||
|
||||
export function createDeco(body: CreateDecoRequest): Promise<Deco> {
|
||||
return request<Deco>('/api/decos', { body, method: 'POST' });
|
||||
return request<Deco>('/api/v1/decos', { body, method: 'POST' });
|
||||
}
|
||||
|
||||
export function deleteDeco(id: number): Promise<void> {
|
||||
return request<void>(`/api/decos/${id}`, { method: 'DELETE' });
|
||||
return request<void>(`/api/v1/decos/${id}`, { method: 'DELETE' });
|
||||
}
|
||||
|
||||
export function replaceDeco(id: number, body: ReplaceDecoRequest): Promise<Deco> {
|
||||
return request<Deco>(`/api/decos/${id}`, { body, method: 'PUT' });
|
||||
return request<Deco>(`/api/v1/decos/${id}`, { body, method: 'PUT' });
|
||||
}
|
||||
|
||||
export function messageFromDecoError(error: unknown, fallback = 'Unable to load decos'): string {
|
||||
|
||||
@@ -62,7 +62,7 @@ describe('ffmpeg profiles api client', () => {
|
||||
const fetchMock = vi.spyOn(window, 'fetch').mockResolvedValue(jsonResponse({ id: 4, name: 'Default' }));
|
||||
|
||||
await expect(getFFmpegProfile(4)).resolves.toMatchObject({ id: 4 });
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/ffmpeg/profiles/4', expect.objectContaining({ method: 'GET' }));
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/v1/ffmpeg/profiles/4', expect.objectContaining({ method: 'GET' }));
|
||||
});
|
||||
|
||||
it('createFFmpegProfile POSTs the request body', async () => {
|
||||
@@ -71,7 +71,7 @@ describe('ffmpeg profiles api client', () => {
|
||||
await expect(createFFmpegProfile(sampleRequest)).resolves.toMatchObject({ id: 7 });
|
||||
|
||||
const [url, init] = fetchMock.mock.calls[0];
|
||||
expect(url).toBe('/api/ffmpeg/profiles');
|
||||
expect(url).toBe('/api/v1/ffmpeg/profiles');
|
||||
expect(init).toMatchObject({ method: 'POST' });
|
||||
expect(JSON.parse(String(init?.body)).resolutionId).toBe(1);
|
||||
});
|
||||
@@ -82,7 +82,7 @@ describe('ffmpeg profiles api client', () => {
|
||||
await updateFFmpegProfile(3, sampleRequest);
|
||||
|
||||
const [url, init] = fetchMock.mock.calls[0];
|
||||
expect(url).toBe('/api/ffmpeg/profiles/3');
|
||||
expect(url).toBe('/api/v1/ffmpeg/profiles/3');
|
||||
expect(init).toMatchObject({ method: 'PUT' });
|
||||
});
|
||||
|
||||
@@ -90,7 +90,7 @@ describe('ffmpeg profiles api client', () => {
|
||||
const fetchMock = vi.spyOn(window, 'fetch').mockResolvedValue(noContent());
|
||||
|
||||
await expect(deleteFFmpegProfile(9)).resolves.toBeUndefined();
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/ffmpeg/profiles/9', expect.objectContaining({ method: 'DELETE' }));
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/v1/ffmpeg/profiles/9', expect.objectContaining({ method: 'DELETE' }));
|
||||
});
|
||||
|
||||
it('getHardwareAccelerationKinds fetches the lookup', async () => {
|
||||
@@ -98,7 +98,7 @@ describe('ffmpeg profiles api client', () => {
|
||||
|
||||
await expect(getHardwareAccelerationKinds()).resolves.toEqual(['None', 'Nvenc']);
|
||||
expect(fetchMock).toHaveBeenCalledWith(
|
||||
'/api/ffmpeg/hardware-acceleration-kinds',
|
||||
'/api/v1/ffmpeg/hardware-acceleration-kinds',
|
||||
expect.objectContaining({ method: 'GET' })
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ApiError, request } from './client';
|
||||
import type { components } from './generated/v1';
|
||||
|
||||
// The list endpoint (GET /api/ffmpeg/profiles) is already provided by pickers.ts as
|
||||
// The list endpoint (GET /api/v1/ffmpeg/profiles) is already provided by pickers.ts as
|
||||
// getFFmpegProfiles() returning FFmpegProfile (= FFmpegFullProfileResponseModel). This
|
||||
// module adds the by-id read, the mutations, and the hardware-acceleration lookup.
|
||||
|
||||
@@ -11,23 +11,23 @@ export type UpdateFFmpegProfileRequest = components['schemas']['UpdateFFmpegProf
|
||||
export type HardwareAccelerationKind = components['schemas']['HardwareAccelerationKind'];
|
||||
|
||||
export function getFFmpegProfile(id: number): Promise<FFmpegProfileDetail> {
|
||||
return request<FFmpegProfileDetail>(`/api/ffmpeg/profiles/${id}`);
|
||||
return request<FFmpegProfileDetail>(`/api/v1/ffmpeg/profiles/${id}`);
|
||||
}
|
||||
|
||||
export function createFFmpegProfile(body: CreateFFmpegProfileRequest): Promise<FFmpegProfileDetail> {
|
||||
return request<FFmpegProfileDetail>('/api/ffmpeg/profiles', { body, method: 'POST' });
|
||||
return request<FFmpegProfileDetail>('/api/v1/ffmpeg/profiles', { body, method: 'POST' });
|
||||
}
|
||||
|
||||
export function updateFFmpegProfile(id: number, body: UpdateFFmpegProfileRequest): Promise<FFmpegProfileDetail> {
|
||||
return request<FFmpegProfileDetail>(`/api/ffmpeg/profiles/${id}`, { body, method: 'PUT' });
|
||||
return request<FFmpegProfileDetail>(`/api/v1/ffmpeg/profiles/${id}`, { body, method: 'PUT' });
|
||||
}
|
||||
|
||||
export function deleteFFmpegProfile(id: number): Promise<void> {
|
||||
return request<void>(`/api/ffmpeg/profiles/${id}`, { method: 'DELETE' });
|
||||
return request<void>(`/api/v1/ffmpeg/profiles/${id}`, { method: 'DELETE' });
|
||||
}
|
||||
|
||||
export function getHardwareAccelerationKinds(): Promise<HardwareAccelerationKind[]> {
|
||||
return request<HardwareAccelerationKind[]>('/api/ffmpeg/hardware-acceleration-kinds');
|
||||
return request<HardwareAccelerationKind[]>('/api/v1/ffmpeg/hardware-acceleration-kinds');
|
||||
}
|
||||
|
||||
export function messageFromFFmpegProfileError(error: unknown, fallback = 'Unable to load FFmpeg profiles'): string {
|
||||
|
||||
@@ -46,7 +46,7 @@ describe('filler presets api client', () => {
|
||||
const fetchMock = vi.spyOn(window, 'fetch').mockResolvedValue(jsonResponse({ id: 4, name: 'Intro' }));
|
||||
|
||||
await expect(getFillerPreset(4)).resolves.toMatchObject({ id: 4 });
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/filler-presets/4', expect.objectContaining({ method: 'GET' }));
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/v1/filler-presets/4', expect.objectContaining({ method: 'GET' }));
|
||||
});
|
||||
|
||||
it('createFillerPreset POSTs the request body', async () => {
|
||||
@@ -55,7 +55,7 @@ describe('filler presets api client', () => {
|
||||
await expect(createFillerPreset(sampleRequest)).resolves.toMatchObject({ id: 7 });
|
||||
|
||||
const [url, init] = fetchMock.mock.calls[0];
|
||||
expect(url).toBe('/api/filler-presets');
|
||||
expect(url).toBe('/api/v1/filler-presets');
|
||||
expect(init).toMatchObject({ method: 'POST' });
|
||||
expect(JSON.parse(String(init?.body)).fillerKind).toBe('PreRoll');
|
||||
});
|
||||
@@ -66,7 +66,7 @@ describe('filler presets api client', () => {
|
||||
await updateFillerPreset(3, sampleRequest);
|
||||
|
||||
const [url, init] = fetchMock.mock.calls[0];
|
||||
expect(url).toBe('/api/filler-presets/3');
|
||||
expect(url).toBe('/api/v1/filler-presets/3');
|
||||
expect(init).toMatchObject({ method: 'PUT' });
|
||||
});
|
||||
|
||||
@@ -74,6 +74,6 @@ describe('filler presets api client', () => {
|
||||
const fetchMock = vi.spyOn(window, 'fetch').mockResolvedValue(noContent());
|
||||
|
||||
await expect(deleteFillerPreset(9)).resolves.toBeUndefined();
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/filler-presets/9', expect.objectContaining({ method: 'DELETE' }));
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/v1/filler-presets/9', expect.objectContaining({ method: 'DELETE' }));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ApiError, request } from './client';
|
||||
import type { components } from './generated/v1';
|
||||
|
||||
// The list endpoint (GET /api/filler-presets) is picker-grade (id + name) and is already
|
||||
// The list endpoint (GET /api/v1/filler-presets) is picker-grade (id + name) and is already
|
||||
// provided by pickers.ts as getFillerPresets(). This module adds the by-id read (full
|
||||
// model) and the mutations.
|
||||
|
||||
@@ -10,19 +10,19 @@ export type CreateFillerPresetRequest = components['schemas']['CreateFillerPrese
|
||||
export type UpdateFillerPresetRequest = components['schemas']['UpdateFillerPresetRequest'];
|
||||
|
||||
export function getFillerPreset(id: number): Promise<FillerPresetDetail> {
|
||||
return request<FillerPresetDetail>(`/api/filler-presets/${id}`);
|
||||
return request<FillerPresetDetail>(`/api/v1/filler-presets/${id}`);
|
||||
}
|
||||
|
||||
export function createFillerPreset(body: CreateFillerPresetRequest): Promise<FillerPresetDetail> {
|
||||
return request<FillerPresetDetail>('/api/filler-presets', { body, method: 'POST' });
|
||||
return request<FillerPresetDetail>('/api/v1/filler-presets', { body, method: 'POST' });
|
||||
}
|
||||
|
||||
export function updateFillerPreset(id: number, body: UpdateFillerPresetRequest): Promise<FillerPresetDetail> {
|
||||
return request<FillerPresetDetail>(`/api/filler-presets/${id}`, { body, method: 'PUT' });
|
||||
return request<FillerPresetDetail>(`/api/v1/filler-presets/${id}`, { body, method: 'PUT' });
|
||||
}
|
||||
|
||||
export function deleteFillerPreset(id: number): Promise<void> {
|
||||
return request<void>(`/api/filler-presets/${id}`, { method: 'DELETE' });
|
||||
return request<void>(`/api/v1/filler-presets/${id}`, { method: 'DELETE' });
|
||||
}
|
||||
|
||||
export function messageFromFillerPresetError(error: unknown, fallback = 'Unable to load filler presets'): string {
|
||||
|
||||
@@ -43,7 +43,7 @@ export function getGuide(start: Date, end: Date): Promise<ChannelGuide> {
|
||||
end: end.toISOString()
|
||||
});
|
||||
|
||||
return request<ChannelGuide>(`/api/guide?${query.toString()}`);
|
||||
return request<ChannelGuide>(`/api/v1/guide?${query.toString()}`);
|
||||
}
|
||||
|
||||
export async function getGuideScreenData(start: Date, end: Date): Promise<GuideScreenData> {
|
||||
|
||||
@@ -14,7 +14,7 @@ describe('image folder clients', () => {
|
||||
const fetchMock = vi.spyOn(window, 'fetch').mockResolvedValue(jsonResponse([]));
|
||||
await getImageFolders();
|
||||
const url = new URL(String(fetchMock.mock.calls[0][0]), 'http://localhost');
|
||||
expect(url.pathname).toBe('/api/images/folders');
|
||||
expect(url.pathname).toBe('/api/v1/images/folders');
|
||||
expect(url.searchParams.has('parentId')).toBe(false);
|
||||
});
|
||||
|
||||
@@ -30,7 +30,7 @@ describe('image folder clients', () => {
|
||||
const result = await updateImageFolderDuration(7, 3);
|
||||
|
||||
const [input, init] = fetchMock.mock.calls[0];
|
||||
expect(new URL(String(input), 'http://localhost').pathname).toBe('/api/images/folders/7/duration');
|
||||
expect(new URL(String(input), 'http://localhost').pathname).toBe('/api/v1/images/folders/7/duration');
|
||||
expect((init as RequestInit).method).toBe('PUT');
|
||||
expect(JSON.parse(String((init as RequestInit).body))).toEqual({ durationSeconds: 3 });
|
||||
expect(result.durationSeconds).toBe(3);
|
||||
|
||||
@@ -6,14 +6,14 @@ export type UpdateImageFolderDurationResponse = components['schemas']['UpdateIma
|
||||
|
||||
export function getImageFolders(parentId?: number): Promise<ImageFolder[]> {
|
||||
const queryString = parentId != null ? `?parentId=${parentId}` : '';
|
||||
return request<ImageFolder[]>(`/api/images/folders${queryString}`);
|
||||
return request<ImageFolder[]>(`/api/v1/images/folders${queryString}`);
|
||||
}
|
||||
|
||||
export function updateImageFolderDuration(
|
||||
id: number,
|
||||
durationSeconds: number | null
|
||||
): Promise<UpdateImageFolderDurationResponse> {
|
||||
return request<UpdateImageFolderDurationResponse>(`/api/images/folders/${id}/duration`, {
|
||||
return request<UpdateImageFolderDurationResponse>(`/api/v1/images/folders/${id}/duration`, {
|
||||
body: { durationSeconds },
|
||||
method: 'PUT'
|
||||
});
|
||||
|
||||
@@ -11,7 +11,7 @@ describe('getLanguages', () => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('GETs /api/languages and returns the list', async () => {
|
||||
it('GETs /api/v1/languages and returns the list', async () => {
|
||||
const fetchSpy = vi.spyOn(window, 'fetch').mockResolvedValue(
|
||||
jsonResponse([{ code: 'eng', englishName: 'English' }])
|
||||
);
|
||||
@@ -19,7 +19,7 @@ describe('getLanguages', () => {
|
||||
await expect(getLanguages()).resolves.toEqual([{ code: 'eng', englishName: 'English' }]);
|
||||
|
||||
const [url, init] = fetchSpy.mock.calls[0];
|
||||
expect(url).toBe('/api/languages');
|
||||
expect(url).toBe('/api/v1/languages');
|
||||
expect((init?.method ?? 'GET').toUpperCase()).toBe('GET');
|
||||
});
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import type { components } from './generated/v1';
|
||||
export type LanguageCode = components['schemas']['LanguageCodeResponseModel'];
|
||||
|
||||
export function getLanguages(): Promise<LanguageCode[]> {
|
||||
return request<LanguageCode[]>('/api/languages');
|
||||
return request<LanguageCode[]>('/api/v1/languages');
|
||||
}
|
||||
|
||||
export function messageFromLanguagesError(error: unknown, fallback = 'Unable to load languages'): string {
|
||||
|
||||
@@ -35,11 +35,11 @@ function mockCollectionsBackend(opts: { deferPost?: boolean; postStatus?: number
|
||||
const url = String(input);
|
||||
const method = (init?.method ?? 'GET').toUpperCase();
|
||||
|
||||
if (url.includes('/api/media-sources/collections-scan-status')) {
|
||||
if (url.includes('/api/v1/media-sources/collections-scan-status')) {
|
||||
return json([...active].map((family) => ({ family })));
|
||||
}
|
||||
|
||||
const match = url.match(/\/api\/media-sources\/(\w+)\/\d+\/scan-collections/);
|
||||
const match = url.match(/\/api\/v1\/media-sources\/(\w+)\/\d+\/scan-collections/);
|
||||
if (match && method === 'POST') {
|
||||
await postGate;
|
||||
const status = opts.postStatus ?? 202;
|
||||
@@ -70,33 +70,33 @@ describe('libraries api client', () => {
|
||||
it('scanLibrary POSTs to the library scan endpoint', async () => {
|
||||
const fetchMock = vi.spyOn(window, 'fetch').mockResolvedValue(noContent());
|
||||
await scanLibrary(4);
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/libraries/4/scan', expect.objectContaining({ method: 'POST' }));
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/v1/libraries/4/scan', expect.objectContaining({ method: 'POST' }));
|
||||
});
|
||||
|
||||
it('scanLibrary appends ?deep=true for a deep scan', async () => {
|
||||
const fetchMock = vi.spyOn(window, 'fetch').mockResolvedValue(noContent());
|
||||
await scanLibrary(4, true);
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/libraries/4/scan?deep=true', expect.objectContaining({ method: 'POST' }));
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/v1/libraries/4/scan?deep=true', expect.objectContaining({ method: 'POST' }));
|
||||
});
|
||||
|
||||
it('scanCollections POSTs to the media-source scan-collections endpoint', async () => {
|
||||
const fetchMock = vi.spyOn(window, 'fetch').mockResolvedValue(noContent());
|
||||
await scanCollections('jellyfin', 7);
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/media-sources/jellyfin/7/scan-collections', expect.objectContaining({ method: 'POST' }));
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/v1/media-sources/jellyfin/7/scan-collections', expect.objectContaining({ method: 'POST' }));
|
||||
});
|
||||
|
||||
it('scanCollections appends ?deep=true for a deep scan', async () => {
|
||||
const fetchMock = vi.spyOn(window, 'fetch').mockResolvedValue(noContent());
|
||||
await scanCollections('plex', 3, true);
|
||||
const { url } = lastCall(fetchMock);
|
||||
expect(url).toBe('/api/media-sources/plex/3/scan-collections?deep=true');
|
||||
expect(url).toBe('/api/v1/media-sources/plex/3/scan-collections?deep=true');
|
||||
});
|
||||
|
||||
it('scanShow POSTs the show id and deepScan flag', async () => {
|
||||
const fetchMock = vi.spyOn(window, 'fetch').mockResolvedValue(noContent());
|
||||
await scanShow(4, { deepScan: true, showId: 42 });
|
||||
const { init, url } = lastCall(fetchMock);
|
||||
expect(url).toBe('/api/libraries/4/scan-show');
|
||||
expect(url).toBe('/api/v1/libraries/4/scan-show');
|
||||
expect(init?.method).toBe('POST');
|
||||
expect(JSON.parse(String(init?.body))).toEqual({ deepScan: true, showId: 42 });
|
||||
});
|
||||
@@ -111,7 +111,7 @@ describe('libraries api client', () => {
|
||||
it('getCollectionsScanStatus GETs the collections scan-status endpoint', async () => {
|
||||
const fetchMock = vi.spyOn(window, 'fetch').mockResolvedValue(json([{ family: 'plex' }]));
|
||||
const result = await getCollectionsScanStatus();
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/media-sources/collections-scan-status', expect.anything());
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/v1/media-sources/collections-scan-status', expect.anything());
|
||||
expect(result).toEqual([{ family: 'plex' }]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -30,7 +30,7 @@ type LibrariesScreenState =
|
||||
| { data: null; error: null; status: 'loading' };
|
||||
|
||||
export function getMediaSources(): Promise<MediaSource[]> {
|
||||
return request<MediaSource[]>('/api/media-sources');
|
||||
return request<MediaSource[]>('/api/v1/media-sources');
|
||||
}
|
||||
|
||||
export function getLibraryScanStatus(): Promise<LibraryScanStatus[]> {
|
||||
@@ -38,7 +38,7 @@ export function getLibraryScanStatus(): Promise<LibraryScanStatus[]> {
|
||||
// never multiplies by 100 (known backend wart, tracked in the handoff backlog).
|
||||
// Normalize to a 0-100 percentage once, here at the API boundary, so every
|
||||
// consumer (ProgressBar, percent labels) works in ordinary percentage terms.
|
||||
return request<LibraryScanStatus[]>('/api/libraries/scan-status').then((scanStatuses) =>
|
||||
return request<LibraryScanStatus[]>('/api/v1/libraries/scan-status').then((scanStatuses) =>
|
||||
scanStatuses.map((scanStatus) => ({ ...scanStatus, percent: scanStatus.percent * 100 }))
|
||||
);
|
||||
}
|
||||
@@ -48,7 +48,7 @@ export function getLibraryScanStatus(): Promise<LibraryScanStatus[]> {
|
||||
// 202 when queued, 404 (missing), 409 (already scanning), 422 (sync disabled) — see §3b.
|
||||
export function scanLibrary(libraryId: number, deep = false): Promise<void> {
|
||||
const query = deep ? '?deep=true' : '';
|
||||
return request<void>(`/api/libraries/${libraryId}/scan${query}`, { method: 'POST' });
|
||||
return request<void>(`/api/v1/libraries/${libraryId}/scan${query}`, { method: 'POST' });
|
||||
}
|
||||
|
||||
// Media-source families that support an external-collections scan (#235 F9). Matches the three
|
||||
@@ -60,7 +60,7 @@ export type CollectionsScanSource = 'emby' | 'jellyfin' | 'plex';
|
||||
// when queued, 404 (source missing), 409 (a collections scan is already running for that source).
|
||||
export function scanCollections(source: CollectionsScanSource, sourceId: number, deep = false): Promise<void> {
|
||||
const query = deep ? '?deep=true' : '';
|
||||
return request<void>(`/api/media-sources/${source}/${sourceId}/scan-collections${query}`, { method: 'POST' });
|
||||
return request<void>(`/api/v1/media-sources/${source}/${sourceId}/scan-collections${query}`, { method: 'POST' });
|
||||
}
|
||||
|
||||
export interface ScanShowParams {
|
||||
@@ -74,7 +74,7 @@ export interface ScanShowParams {
|
||||
// error bodies are ProblemDetails; #235 normalized the old conflated 400). Body keys are `showId`
|
||||
// and `deepScan` (see LibrariesController.ScanShowRequest).
|
||||
export function scanShow(libraryId: number, params: ScanShowParams): Promise<void> {
|
||||
return request<void>(`/api/libraries/${libraryId}/scan-show`, {
|
||||
return request<void>(`/api/v1/libraries/${libraryId}/scan-show`, {
|
||||
body: { deepScan: params.deepScan ?? false, showId: params.showId },
|
||||
method: 'POST'
|
||||
});
|
||||
@@ -407,9 +407,9 @@ export function useLibrariesScreenQuery(pollMs = 10000): LibrariesScreenQuerySta
|
||||
|
||||
// Fetch the media-source families whose external-collections scan is currently running. Collections
|
||||
// locks are family-global, so this returns at most one entry per family and only for active ones -
|
||||
// the authoritative counterpart to /api/libraries/scan-status for collections (#271, #91b F9).
|
||||
// the authoritative counterpart to /api/v1/libraries/scan-status for collections (#271, #91b F9).
|
||||
export function getCollectionsScanStatus(): Promise<CollectionsScanStatus[]> {
|
||||
return request<CollectionsScanStatus[]>('/api/media-sources/collections-scan-status');
|
||||
return request<CollectionsScanStatus[]>('/api/v1/media-sources/collections-scan-status');
|
||||
}
|
||||
|
||||
export interface CollectionsScanState {
|
||||
|
||||
@@ -29,7 +29,7 @@ describe('getLibraryBrowseItems', () => {
|
||||
});
|
||||
|
||||
const url = browseUrl(fetchMock);
|
||||
expect(url.pathname).toBe('/api/library/browse');
|
||||
expect(url.pathname).toBe('/api/v1/library/browse');
|
||||
expect(url.searchParams.get('query')).toBe('star');
|
||||
expect(url.searchParams.get('libraryId')).toBe('5');
|
||||
expect(url.searchParams.get('mediaType')).toBe('TelevisionShow');
|
||||
|
||||
@@ -44,7 +44,7 @@ export function getLibraryBrowseItems(params: GetLibraryBrowseItemsParams = {}):
|
||||
|
||||
const queryString = searchParams.toString();
|
||||
|
||||
return request<PagedLibraryBrowseItems>(`/api/library/browse${queryString ? `?${queryString}` : ''}`);
|
||||
return request<PagedLibraryBrowseItems>(`/api/v1/library/browse${queryString ? `?${queryString}` : ''}`);
|
||||
}
|
||||
|
||||
export function messageFromLibraryBrowseError(error: unknown, fallback = 'Unable to load library items'): string {
|
||||
|
||||
@@ -15,7 +15,7 @@ describe('getLogs', () => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('GETs /api/logs with no query string when called without params', async () => {
|
||||
it('GETs /api/v1/logs with no query string when called without params', async () => {
|
||||
const fetchSpy = vi.spyOn(window, 'fetch').mockResolvedValue(
|
||||
new Response(JSON.stringify(samplePage), {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
@@ -26,7 +26,7 @@ describe('getLogs', () => {
|
||||
await expect(getLogs()).resolves.toMatchObject({ totalCount: 2 });
|
||||
|
||||
const [url, init] = fetchSpy.mock.calls[0];
|
||||
expect(url).toBe('/api/logs');
|
||||
expect(url).toBe('/api/v1/logs');
|
||||
expect((init?.method ?? 'GET').toUpperCase()).toBe('GET');
|
||||
});
|
||||
|
||||
@@ -41,7 +41,7 @@ describe('getLogs', () => {
|
||||
await getLogs({ filter: 'boom', pageNum: 2, pageSize: 50 });
|
||||
|
||||
const [url] = fetchSpy.mock.calls[0];
|
||||
expect(url).toBe('/api/logs?filter=boom&pageNum=2&pageSize=50');
|
||||
expect(url).toBe('/api/v1/logs?filter=boom&pageNum=2&pageSize=50');
|
||||
});
|
||||
|
||||
it('builds the query string from sortField and sortDirection', async () => {
|
||||
@@ -55,7 +55,7 @@ describe('getLogs', () => {
|
||||
await getLogs({ sortDirection: 'asc', sortField: 'level' });
|
||||
|
||||
const [url] = fetchSpy.mock.calls[0];
|
||||
expect(url).toBe('/api/logs?sortField=level&sortDirection=asc');
|
||||
expect(url).toBe('/api/v1/logs?sortField=level&sortDirection=asc');
|
||||
});
|
||||
|
||||
it('rejects with the ApiError status on failure', async () => {
|
||||
|
||||
+1
-1
@@ -40,7 +40,7 @@ export function getLogs(params: GetLogsParams = {}): Promise<PagedLogEntries> {
|
||||
|
||||
const queryString = searchParams.toString();
|
||||
|
||||
return request<PagedLogEntries>(`/api/logs${queryString ? `?${queryString}` : ''}`);
|
||||
return request<PagedLogEntries>(`/api/v1/logs${queryString ? `?${queryString}` : ''}`);
|
||||
}
|
||||
|
||||
export function messageFromLogsError(error: unknown, fallback = 'Unable to load logs'): string {
|
||||
|
||||
@@ -7,13 +7,13 @@ describe('emptyTrash', () => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('POSTs /api/maintenance/empty_trash', async () => {
|
||||
it('POSTs /api/v1/maintenance/empty_trash', async () => {
|
||||
const fetchSpy = vi.spyOn(window, 'fetch').mockResolvedValue(new Response(null, { status: 200 }));
|
||||
|
||||
await emptyTrash();
|
||||
|
||||
const [url, init] = fetchSpy.mock.calls[0];
|
||||
expect(url).toBe('/api/maintenance/empty_trash');
|
||||
expect(url).toBe('/api/v1/maintenance/empty_trash');
|
||||
expect((init?.method ?? 'GET').toUpperCase()).toBe('POST');
|
||||
});
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ApiError, request } from './client';
|
||||
|
||||
export function emptyTrash(): Promise<void> {
|
||||
return request<void>('/api/maintenance/empty_trash', { method: 'POST' });
|
||||
return request<void>('/api/v1/maintenance/empty_trash', { method: 'POST' });
|
||||
}
|
||||
|
||||
export function messageFromMaintenanceError(error: unknown, fallback = 'Maintenance action failed'): string {
|
||||
|
||||
@@ -16,11 +16,11 @@ describe('media detail clients', () => {
|
||||
|
||||
it('requests the right detail routes', async () => {
|
||||
const cases: Array<[() => Promise<unknown>, string]> = [
|
||||
[() => getMovie(5), '/api/movies/5'],
|
||||
[() => getShow(6), '/api/shows/6'],
|
||||
[() => getSeason(7), '/api/seasons/7'],
|
||||
[() => getArtist(8), '/api/artists/8'],
|
||||
[() => getMediaItemInfo(9), '/api/media-items/9/info']
|
||||
[() => getMovie(5), '/api/v1/movies/5'],
|
||||
[() => getShow(6), '/api/v1/shows/6'],
|
||||
[() => getSeason(7), '/api/v1/seasons/7'],
|
||||
[() => getArtist(8), '/api/v1/artists/8'],
|
||||
[() => getMediaItemInfo(9), '/api/v1/media-items/9/info']
|
||||
];
|
||||
|
||||
for (const [call, path] of cases) {
|
||||
|
||||
@@ -11,23 +11,23 @@ export type MediaItemInfoStream = components['schemas']['MediaItemInfoStreamResp
|
||||
export type MediaItemInfoChapter = components['schemas']['MediaItemInfoChapterResponseModel'];
|
||||
|
||||
export function getMovie(id: number): Promise<MovieDetail> {
|
||||
return request<MovieDetail>(`/api/movies/${id}`);
|
||||
return request<MovieDetail>(`/api/v1/movies/${id}`);
|
||||
}
|
||||
|
||||
export function getShow(id: number): Promise<ShowDetail> {
|
||||
return request<ShowDetail>(`/api/shows/${id}`);
|
||||
return request<ShowDetail>(`/api/v1/shows/${id}`);
|
||||
}
|
||||
|
||||
export function getSeason(id: number): Promise<SeasonDetail> {
|
||||
return request<SeasonDetail>(`/api/seasons/${id}`);
|
||||
return request<SeasonDetail>(`/api/v1/seasons/${id}`);
|
||||
}
|
||||
|
||||
export function getArtist(id: number): Promise<ArtistDetail> {
|
||||
return request<ArtistDetail>(`/api/artists/${id}`);
|
||||
return request<ArtistDetail>(`/api/v1/artists/${id}`);
|
||||
}
|
||||
|
||||
export function getMediaItemInfo(id: number): Promise<MediaItemInfo> {
|
||||
return request<MediaItemInfo>(`/api/media-items/${id}/info`);
|
||||
return request<MediaItemInfo>(`/api/v1/media-items/${id}/info`);
|
||||
}
|
||||
|
||||
export function messageFromMediaDetailError(error: unknown, fallback = 'Unable to load media details'): string {
|
||||
|
||||
@@ -7,13 +7,13 @@ describe('deleteMediaItems', () => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('DELETEs /api/media-items with the ids in the body', async () => {
|
||||
it('DELETEs /api/v1/media-items with the ids in the body', async () => {
|
||||
const fetchSpy = vi.spyOn(window, 'fetch').mockResolvedValue(new Response(null, { status: 204 }));
|
||||
|
||||
await deleteMediaItems([1, 2, 3]);
|
||||
|
||||
const [url, init] = fetchSpy.mock.calls[0];
|
||||
expect(url).toBe('/api/media-items');
|
||||
expect(url).toBe('/api/v1/media-items');
|
||||
expect((init?.method ?? 'GET').toUpperCase()).toBe('DELETE');
|
||||
expect(JSON.parse(init?.body as string)).toEqual({ ids: [1, 2, 3] });
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ApiError, request } from './client';
|
||||
|
||||
export function deleteMediaItems(ids: number[]): Promise<void> {
|
||||
return request<void>('/api/media-items', { body: { ids }, method: 'DELETE' });
|
||||
return request<void>('/api/v1/media-items', { body: { ids }, method: 'DELETE' });
|
||||
}
|
||||
|
||||
export function messageFromMediaItemsError(error: unknown, fallback = 'Unable to delete media items'): string {
|
||||
|
||||
@@ -46,108 +46,108 @@ describe('mediaSources API client', () => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('local library reads and mutations hit /api/libraries/local with the right verbs', async () => {
|
||||
it('local library reads and mutations hit /api/v1/libraries/local with the right verbs', async () => {
|
||||
const fetchSpy = vi.spyOn(window, 'fetch').mockImplementation(() => Promise.resolve(jsonResponse({})));
|
||||
|
||||
await getLocalLibraries();
|
||||
expect(lastCall(fetchSpy)).toMatchObject({ url: '/api/libraries/local', method: 'GET' });
|
||||
expect(lastCall(fetchSpy)).toMatchObject({ url: '/api/v1/libraries/local', method: 'GET' });
|
||||
|
||||
await getLocalLibrary(7);
|
||||
expect(lastCall(fetchSpy)).toMatchObject({ url: '/api/libraries/local/7', method: 'GET' });
|
||||
expect(lastCall(fetchSpy)).toMatchObject({ url: '/api/v1/libraries/local/7', method: 'GET' });
|
||||
|
||||
await createLocalLibrary({ name: 'Movies', mediaKind: 'Movies', paths: ['/media/movies'] });
|
||||
expect(lastCall(fetchSpy)).toMatchObject({
|
||||
url: '/api/libraries/local',
|
||||
url: '/api/v1/libraries/local',
|
||||
method: 'POST',
|
||||
body: { name: 'Movies', mediaKind: 'Movies', paths: ['/media/movies'] }
|
||||
});
|
||||
|
||||
await updateLocalLibrary(7, { name: 'Films', paths: [{ id: 1, path: '/media/movies' }] });
|
||||
expect(lastCall(fetchSpy)).toMatchObject({
|
||||
url: '/api/libraries/local/7',
|
||||
url: '/api/v1/libraries/local/7',
|
||||
method: 'PUT',
|
||||
body: { name: 'Films', paths: [{ id: 1, path: '/media/movies' }] }
|
||||
});
|
||||
|
||||
await deleteLocalLibrary(7);
|
||||
expect(lastCall(fetchSpy)).toMatchObject({ url: '/api/libraries/local/7', method: 'DELETE' });
|
||||
expect(lastCall(fetchSpy)).toMatchObject({ url: '/api/v1/libraries/local/7', method: 'DELETE' });
|
||||
|
||||
await moveLocalLibraryPath(42, { targetLibraryId: 9 });
|
||||
expect(lastCall(fetchSpy)).toMatchObject({
|
||||
url: '/api/libraries/local/paths/42/move',
|
||||
url: '/api/v1/libraries/local/paths/42/move',
|
||||
method: 'POST',
|
||||
body: { targetLibraryId: 9 }
|
||||
});
|
||||
|
||||
await checkLocalPathExists({ path: '/media/movies' });
|
||||
expect(lastCall(fetchSpy)).toMatchObject({
|
||||
url: '/api/libraries/local/path-exists',
|
||||
url: '/api/v1/libraries/local/path-exists',
|
||||
method: 'POST',
|
||||
body: { path: '/media/movies' }
|
||||
});
|
||||
});
|
||||
|
||||
it('Plex helpers map to /api/media-sources/plex', async () => {
|
||||
it('Plex helpers map to /api/v1/media-sources/plex', async () => {
|
||||
const fetchSpy = vi.spyOn(window, 'fetch').mockImplementation(() => Promise.resolve(jsonResponse({})));
|
||||
|
||||
await getPlexState();
|
||||
expect(lastCall(fetchSpy)).toMatchObject({ url: '/api/media-sources/plex', method: 'GET' });
|
||||
expect(lastCall(fetchSpy)).toMatchObject({ url: '/api/v1/media-sources/plex', method: 'GET' });
|
||||
|
||||
await startPlexPinFlow();
|
||||
expect(lastCall(fetchSpy)).toMatchObject({ url: '/api/media-sources/plex/pin-flow', method: 'POST' });
|
||||
expect(lastCall(fetchSpy)).toMatchObject({ url: '/api/v1/media-sources/plex/pin-flow', method: 'POST' });
|
||||
|
||||
await signOutOfPlex();
|
||||
expect(lastCall(fetchSpy)).toMatchObject({ url: '/api/media-sources/plex/sign-out', method: 'POST' });
|
||||
expect(lastCall(fetchSpy)).toMatchObject({ url: '/api/v1/media-sources/plex/sign-out', method: 'POST' });
|
||||
});
|
||||
|
||||
it('the shared remote surface varies only by the family segment', async () => {
|
||||
const fetchSpy = vi.spyOn(window, 'fetch').mockImplementation(() => Promise.resolve(jsonResponse({})));
|
||||
|
||||
await getRemoteState('jellyfin');
|
||||
expect(lastCall(fetchSpy)).toMatchObject({ url: '/api/media-sources/jellyfin', method: 'GET' });
|
||||
expect(lastCall(fetchSpy)).toMatchObject({ url: '/api/v1/media-sources/jellyfin', method: 'GET' });
|
||||
|
||||
await getRemoteState('emby');
|
||||
expect(lastCall(fetchSpy)).toMatchObject({ url: '/api/media-sources/emby', method: 'GET' });
|
||||
expect(lastCall(fetchSpy)).toMatchObject({ url: '/api/v1/media-sources/emby', method: 'GET' });
|
||||
|
||||
await getRemoteLibraries('plex', 3);
|
||||
expect(lastCall(fetchSpy)).toMatchObject({ url: '/api/media-sources/plex/3/libraries', method: 'GET' });
|
||||
expect(lastCall(fetchSpy)).toMatchObject({ url: '/api/v1/media-sources/plex/3/libraries', method: 'GET' });
|
||||
|
||||
await replaceRemoteLibraryPreferences('emby', 3, { libraries: [{ id: 5, shouldSyncItems: true }] });
|
||||
expect(lastCall(fetchSpy)).toMatchObject({
|
||||
url: '/api/media-sources/emby/3/libraries',
|
||||
url: '/api/v1/media-sources/emby/3/libraries',
|
||||
method: 'PUT',
|
||||
body: { libraries: [{ id: 5, shouldSyncItems: true }] }
|
||||
});
|
||||
|
||||
await getPathReplacements('jellyfin', 3);
|
||||
expect(lastCall(fetchSpy)).toMatchObject({ url: '/api/media-sources/jellyfin/3/path-replacements', method: 'GET' });
|
||||
expect(lastCall(fetchSpy)).toMatchObject({ url: '/api/v1/media-sources/jellyfin/3/path-replacements', method: 'GET' });
|
||||
|
||||
await replacePathReplacements('plex', 3, { items: [{ id: 0, remotePath: '/r', localPath: '/l' }] });
|
||||
expect(lastCall(fetchSpy)).toMatchObject({
|
||||
url: '/api/media-sources/plex/3/path-replacements',
|
||||
url: '/api/v1/media-sources/plex/3/path-replacements',
|
||||
method: 'PUT',
|
||||
body: { items: [{ id: 0, remotePath: '/r', localPath: '/l' }] }
|
||||
});
|
||||
|
||||
await refreshRemoteLibraries('emby', 3);
|
||||
expect(lastCall(fetchSpy)).toMatchObject({ url: '/api/media-sources/emby/3/refresh-libraries', method: 'POST' });
|
||||
expect(lastCall(fetchSpy)).toMatchObject({ url: '/api/v1/media-sources/emby/3/refresh-libraries', method: 'POST' });
|
||||
});
|
||||
|
||||
it('connection endpoints (Jellyfin/Emby) round-trip address + apiKey', async () => {
|
||||
const fetchSpy = vi.spyOn(window, 'fetch').mockImplementation(() => Promise.resolve(jsonResponse({})));
|
||||
|
||||
await getRemoteConnection('jellyfin');
|
||||
expect(lastCall(fetchSpy)).toMatchObject({ url: '/api/media-sources/jellyfin/connection', method: 'GET' });
|
||||
expect(lastCall(fetchSpy)).toMatchObject({ url: '/api/v1/media-sources/jellyfin/connection', method: 'GET' });
|
||||
|
||||
await saveRemoteConnection('emby', { address: 'http://emby:8096', apiKey: '' });
|
||||
expect(lastCall(fetchSpy)).toMatchObject({
|
||||
url: '/api/media-sources/emby/connection',
|
||||
url: '/api/v1/media-sources/emby/connection',
|
||||
method: 'PUT',
|
||||
body: { address: 'http://emby:8096', apiKey: '' }
|
||||
});
|
||||
|
||||
await disconnectRemote('jellyfin');
|
||||
expect(lastCall(fetchSpy)).toMatchObject({ url: '/api/media-sources/jellyfin/disconnect', method: 'POST' });
|
||||
expect(lastCall(fetchSpy)).toMatchObject({ url: '/api/v1/media-sources/jellyfin/disconnect', method: 'POST' });
|
||||
});
|
||||
|
||||
it('204 responses resolve to undefined', async () => {
|
||||
|
||||
+21
-21
@@ -28,74 +28,74 @@ export type SaveRemoteConnectionRequest = components['schemas']['SaveRemoteConne
|
||||
|
||||
export type { RemoteFamily } from '../mediaSources/familyMeta';
|
||||
|
||||
// --- Local libraries (/api/libraries/local) ---
|
||||
// --- Local libraries (/api/v1/libraries/local) ---
|
||||
|
||||
export function getLocalLibraries(): Promise<LocalLibrary[]> {
|
||||
return request<LocalLibrary[]>('/api/libraries/local');
|
||||
return request<LocalLibrary[]>('/api/v1/libraries/local');
|
||||
}
|
||||
|
||||
export function getLocalLibrary(id: number): Promise<LocalLibraryDetail> {
|
||||
return request<LocalLibraryDetail>(`/api/libraries/local/${id}`);
|
||||
return request<LocalLibraryDetail>(`/api/v1/libraries/local/${id}`);
|
||||
}
|
||||
|
||||
export function createLocalLibrary(body: CreateLocalLibraryRequest): Promise<LocalLibrary> {
|
||||
return request<LocalLibrary>('/api/libraries/local', { body, method: 'POST' });
|
||||
return request<LocalLibrary>('/api/v1/libraries/local', { body, method: 'POST' });
|
||||
}
|
||||
|
||||
export function updateLocalLibrary(id: number, body: UpdateLocalLibraryRequest): Promise<LocalLibraryDetail> {
|
||||
return request<LocalLibraryDetail>(`/api/libraries/local/${id}`, { body, method: 'PUT' });
|
||||
return request<LocalLibraryDetail>(`/api/v1/libraries/local/${id}`, { body, method: 'PUT' });
|
||||
}
|
||||
|
||||
export function deleteLocalLibrary(id: number): Promise<void> {
|
||||
return request<void>(`/api/libraries/local/${id}`, { method: 'DELETE' });
|
||||
return request<void>(`/api/v1/libraries/local/${id}`, { method: 'DELETE' });
|
||||
}
|
||||
|
||||
export function moveLocalLibraryPath(pathId: number, body: MoveLocalLibraryPathRequest): Promise<void> {
|
||||
return request<void>(`/api/libraries/local/paths/${pathId}/move`, { body, method: 'POST' });
|
||||
return request<void>(`/api/v1/libraries/local/paths/${pathId}/move`, { body, method: 'POST' });
|
||||
}
|
||||
|
||||
export function checkLocalPathExists(body: LocalPathCheckRequest): Promise<LocalPathCheckResult> {
|
||||
return request<LocalPathCheckResult>('/api/libraries/local/path-exists', { body, method: 'POST' });
|
||||
return request<LocalPathCheckResult>('/api/v1/libraries/local/path-exists', { body, method: 'POST' });
|
||||
}
|
||||
|
||||
// --- Plex-specific convenience over the shared remote surface (/api/media-sources/plex) ---
|
||||
// --- Plex-specific convenience over the shared remote surface (/api/v1/media-sources/plex) ---
|
||||
|
||||
export function getPlexState(): Promise<RemoteMediaSourceState> {
|
||||
return getRemoteState('plex');
|
||||
}
|
||||
|
||||
export function startPlexPinFlow(): Promise<PlexPinFlow> {
|
||||
return request<PlexPinFlow>('/api/media-sources/plex/pin-flow', { method: 'POST' });
|
||||
return request<PlexPinFlow>('/api/v1/media-sources/plex/pin-flow', { method: 'POST' });
|
||||
}
|
||||
|
||||
export function signOutOfPlex(): Promise<void> {
|
||||
return request<void>('/api/media-sources/plex/sign-out', { method: 'POST' });
|
||||
return request<void>('/api/v1/media-sources/plex/sign-out', { method: 'POST' });
|
||||
}
|
||||
|
||||
// --- Shared remote surface (/api/media-sources/{family}); family is the only URL variance ---
|
||||
// --- Shared remote surface (/api/v1/media-sources/{family}); family is the only URL variance ---
|
||||
|
||||
export function getRemoteState(family: RemoteFamily): Promise<RemoteMediaSourceState> {
|
||||
return request<RemoteMediaSourceState>(`/api/media-sources/${family}`);
|
||||
return request<RemoteMediaSourceState>(`/api/v1/media-sources/${family}`);
|
||||
}
|
||||
|
||||
// Connection endpoints exist only for the api-key families (Jellyfin / Emby); Plex uses the pin flow.
|
||||
export function getRemoteConnection(family: 'jellyfin' | 'emby'): Promise<RemoteConnection> {
|
||||
return request<RemoteConnection>(`/api/media-sources/${family}/connection`);
|
||||
return request<RemoteConnection>(`/api/v1/media-sources/${family}/connection`);
|
||||
}
|
||||
|
||||
export function saveRemoteConnection(
|
||||
family: 'jellyfin' | 'emby',
|
||||
body: SaveRemoteConnectionRequest
|
||||
): Promise<RemoteConnection> {
|
||||
return request<RemoteConnection>(`/api/media-sources/${family}/connection`, { body, method: 'PUT' });
|
||||
return request<RemoteConnection>(`/api/v1/media-sources/${family}/connection`, { body, method: 'PUT' });
|
||||
}
|
||||
|
||||
export function disconnectRemote(family: 'jellyfin' | 'emby'): Promise<void> {
|
||||
return request<void>(`/api/media-sources/${family}/disconnect`, { method: 'POST' });
|
||||
return request<void>(`/api/v1/media-sources/${family}/disconnect`, { method: 'POST' });
|
||||
}
|
||||
|
||||
export function getRemoteLibraries(family: RemoteFamily, sourceId: number): Promise<RemoteLibrary[]> {
|
||||
return request<RemoteLibrary[]>(`/api/media-sources/${family}/${sourceId}/libraries`);
|
||||
return request<RemoteLibrary[]>(`/api/v1/media-sources/${family}/${sourceId}/libraries`);
|
||||
}
|
||||
|
||||
export function replaceRemoteLibraryPreferences(
|
||||
@@ -103,11 +103,11 @@ export function replaceRemoteLibraryPreferences(
|
||||
sourceId: number,
|
||||
body: ReplaceRemoteLibraryPreferencesRequest
|
||||
): Promise<RemoteLibrary[]> {
|
||||
return request<RemoteLibrary[]>(`/api/media-sources/${family}/${sourceId}/libraries`, { body, method: 'PUT' });
|
||||
return request<RemoteLibrary[]>(`/api/v1/media-sources/${family}/${sourceId}/libraries`, { body, method: 'PUT' });
|
||||
}
|
||||
|
||||
export function getPathReplacements(family: RemoteFamily, sourceId: number): Promise<PathReplacement[]> {
|
||||
return request<PathReplacement[]>(`/api/media-sources/${family}/${sourceId}/path-replacements`);
|
||||
return request<PathReplacement[]>(`/api/v1/media-sources/${family}/${sourceId}/path-replacements`);
|
||||
}
|
||||
|
||||
export function replacePathReplacements(
|
||||
@@ -115,14 +115,14 @@ export function replacePathReplacements(
|
||||
sourceId: number,
|
||||
body: ReplacePathReplacementsRequest
|
||||
): Promise<PathReplacement[]> {
|
||||
return request<PathReplacement[]>(`/api/media-sources/${family}/${sourceId}/path-replacements`, {
|
||||
return request<PathReplacement[]>(`/api/v1/media-sources/${family}/${sourceId}/path-replacements`, {
|
||||
body,
|
||||
method: 'PUT'
|
||||
});
|
||||
}
|
||||
|
||||
export function refreshRemoteLibraries(family: RemoteFamily, sourceId: number): Promise<void> {
|
||||
return request<void>(`/api/media-sources/${family}/${sourceId}/refresh-libraries`, { method: 'POST' });
|
||||
return request<void>(`/api/v1/media-sources/${family}/${sourceId}/refresh-libraries`, { method: 'POST' });
|
||||
}
|
||||
|
||||
export function messageFromMediaSourcesError(error: unknown, fallback = 'Unable to load media sources'): string {
|
||||
|
||||
@@ -32,7 +32,7 @@ describe('multiCollections api client', () => {
|
||||
await getMultiCollections({ pageNum: 2, pageSize: 25, query: 'kids' });
|
||||
|
||||
const url = new URL(String(fetchMock.mock.calls[0][0]), 'http://localhost');
|
||||
expect(url.pathname).toBe('/api/multi-collections');
|
||||
expect(url.pathname).toBe('/api/v1/multi-collections');
|
||||
expect(url.searchParams.get('pageNum')).toBe('2');
|
||||
expect(url.searchParams.get('pageSize')).toBe('25');
|
||||
expect(url.searchParams.get('query')).toBe('kids');
|
||||
@@ -43,7 +43,7 @@ describe('multiCollections api client', () => {
|
||||
|
||||
await getMultiCollections();
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/multi-collections', expect.objectContaining({ method: 'GET' }));
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/v1/multi-collections', expect.objectContaining({ method: 'GET' }));
|
||||
});
|
||||
|
||||
it('getMultiCollection fetches a single multi-collection by id', async () => {
|
||||
@@ -52,7 +52,7 @@ describe('multiCollections api client', () => {
|
||||
.mockResolvedValue(jsonResponse({ id: 4, items: [], name: 'Bundle' }));
|
||||
|
||||
await expect(getMultiCollection(4)).resolves.toMatchObject({ id: 4 });
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/multi-collections/4', expect.objectContaining({ method: 'GET' }));
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/v1/multi-collections/4', expect.objectContaining({ method: 'GET' }));
|
||||
});
|
||||
|
||||
it('createMultiCollection POSTs the name and items', async () => {
|
||||
@@ -65,7 +65,7 @@ describe('multiCollections api client', () => {
|
||||
await expect(createMultiCollection(body)).resolves.toMatchObject({ id: 9 });
|
||||
|
||||
const [url, init] = fetchMock.mock.calls[0];
|
||||
expect(url).toBe('/api/multi-collections');
|
||||
expect(url).toBe('/api/v1/multi-collections');
|
||||
expect(init).toMatchObject({ method: 'POST' });
|
||||
expect(JSON.parse(String(init?.body))).toEqual(body);
|
||||
});
|
||||
@@ -80,7 +80,7 @@ describe('multiCollections api client', () => {
|
||||
await updateMultiCollection(5, body);
|
||||
|
||||
const [url, init] = fetchMock.mock.calls[0];
|
||||
expect(url).toBe('/api/multi-collections/5');
|
||||
expect(url).toBe('/api/v1/multi-collections/5');
|
||||
expect(init).toMatchObject({ method: 'PUT' });
|
||||
expect(JSON.parse(String(init?.body))).toEqual(body);
|
||||
});
|
||||
@@ -89,7 +89,7 @@ describe('multiCollections api client', () => {
|
||||
const fetchMock = vi.spyOn(window, 'fetch').mockResolvedValue(noContent());
|
||||
|
||||
await expect(deleteMultiCollection(6)).resolves.toBeUndefined();
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/multi-collections/6', expect.objectContaining({ method: 'DELETE' }));
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/v1/multi-collections/6', expect.objectContaining({ method: 'DELETE' }));
|
||||
});
|
||||
|
||||
it('messageFromMultiCollectionError narrows ApiError, Error, and unknown', () => {
|
||||
|
||||
@@ -31,20 +31,20 @@ export function getMultiCollections(params: GetMultiCollectionsParams = {}): Pro
|
||||
|
||||
const queryString = searchParams.toString();
|
||||
|
||||
return request<PagedMultiCollections>(`/api/multi-collections${queryString ? `?${queryString}` : ''}`);
|
||||
return request<PagedMultiCollections>(`/api/v1/multi-collections${queryString ? `?${queryString}` : ''}`);
|
||||
}
|
||||
|
||||
export function getMultiCollection(id: number): Promise<MultiCollection> {
|
||||
return request<MultiCollection>(`/api/multi-collections/${id}`);
|
||||
return request<MultiCollection>(`/api/v1/multi-collections/${id}`);
|
||||
}
|
||||
|
||||
/** Load a single multi-collection together with its concurrency ETag (issue #253). */
|
||||
export function getMultiCollectionWithMeta(id: number): Promise<ResponseWithMeta<MultiCollection>> {
|
||||
return requestWithMeta<MultiCollection>(`/api/multi-collections/${id}`);
|
||||
return requestWithMeta<MultiCollection>(`/api/v1/multi-collections/${id}`);
|
||||
}
|
||||
|
||||
export function createMultiCollection(body: CreateMultiCollectionRequest): Promise<MultiCollection> {
|
||||
return request<MultiCollection>('/api/multi-collections', { body, method: 'POST' });
|
||||
return request<MultiCollection>('/api/v1/multi-collections', { body, method: 'POST' });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -56,7 +56,7 @@ export function updateMultiCollection(
|
||||
body: UpdateMultiCollectionRequest,
|
||||
ifMatch?: string | null
|
||||
): Promise<ResponseWithMeta<MultiCollection>> {
|
||||
return requestWithMeta<MultiCollection>(`/api/multi-collections/${id}`, {
|
||||
return requestWithMeta<MultiCollection>(`/api/v1/multi-collections/${id}`, {
|
||||
body,
|
||||
method: 'PUT',
|
||||
headers: ifMatch ? { 'If-Match': ifMatch } : undefined
|
||||
@@ -64,7 +64,7 @@ export function updateMultiCollection(
|
||||
}
|
||||
|
||||
export function deleteMultiCollection(id: number): Promise<void> {
|
||||
return request<void>(`/api/multi-collections/${id}`, { method: 'DELETE' });
|
||||
return request<void>(`/api/v1/multi-collections/${id}`, { method: 'DELETE' });
|
||||
}
|
||||
|
||||
export function messageFromMultiCollectionError(
|
||||
|
||||
@@ -11,12 +11,12 @@ describe('getGraphicsElements', () => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('GETs /api/graphics-elements without refresh by default', async () => {
|
||||
it('GETs /api/v1/graphics-elements without refresh by default', async () => {
|
||||
const fetchSpy = vi.spyOn(window, 'fetch').mockResolvedValue(jsonResponse([]));
|
||||
|
||||
await getGraphicsElements();
|
||||
|
||||
expect(fetchSpy.mock.calls[0][0]).toBe('/api/graphics-elements');
|
||||
expect(fetchSpy.mock.calls[0][0]).toBe('/api/v1/graphics-elements');
|
||||
});
|
||||
|
||||
it('passes refresh=true to re-sync on-disk definitions first', async () => {
|
||||
@@ -24,6 +24,6 @@ describe('getGraphicsElements', () => {
|
||||
|
||||
await getGraphicsElements(true);
|
||||
|
||||
expect(fetchSpy.mock.calls[0][0]).toBe('/api/graphics-elements?refresh=true');
|
||||
expect(fetchSpy.mock.calls[0][0]).toBe('/api/v1/graphics-elements?refresh=true');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,22 +7,22 @@ export type GraphicsElement = components['schemas']['GraphicsElementResponseMode
|
||||
export type FFmpegProfile = components['schemas']['FFmpegFullProfileResponseModel'];
|
||||
|
||||
export function getFillerPresets(): Promise<FillerPreset[]> {
|
||||
return request<FillerPreset[]>('/api/filler-presets').then(sortByName);
|
||||
return request<FillerPreset[]>('/api/v1/filler-presets').then(sortByName);
|
||||
}
|
||||
|
||||
export function getWatermarks(): Promise<Watermark[]> {
|
||||
return request<Watermark[]>('/api/watermarks').then(sortByName);
|
||||
return request<Watermark[]>('/api/v1/watermarks').then(sortByName);
|
||||
}
|
||||
|
||||
// Pass refresh=true to first re-sync the on-disk graphics element definitions into the database
|
||||
// (matching legacy Blazor's RefreshGraphicsElements-before-list) so newly added files appear.
|
||||
export function getGraphicsElements(refresh = false): Promise<GraphicsElement[]> {
|
||||
const url = refresh ? '/api/graphics-elements?refresh=true' : '/api/graphics-elements';
|
||||
const url = refresh ? '/api/v1/graphics-elements?refresh=true' : '/api/v1/graphics-elements';
|
||||
return request<GraphicsElement[]>(url).then(sortByName);
|
||||
}
|
||||
|
||||
export function getFFmpegProfiles(): Promise<FFmpegProfile[]> {
|
||||
return request<FFmpegProfile[]>('/api/ffmpeg/profiles').then(sortByName);
|
||||
return request<FFmpegProfile[]>('/api/v1/ffmpeg/profiles').then(sortByName);
|
||||
}
|
||||
|
||||
function sortByName<T extends { name: null | string }>(items: T[]): T[] {
|
||||
|
||||
@@ -54,14 +54,14 @@ describe('playlists api client', () => {
|
||||
.spyOn(window, 'fetch')
|
||||
.mockResolvedValue(jsonResponse([{ id: 1, isSystem: false, name: 'Idents', playlistCount: 2 }]));
|
||||
await expect(getPlaylistGroups()).resolves.toHaveLength(1);
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/playlists/groups', expect.objectContaining({ method: 'GET' }));
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/v1/playlists/groups', expect.objectContaining({ method: 'GET' }));
|
||||
});
|
||||
|
||||
it('createPlaylistGroup POSTs the name', async () => {
|
||||
const fetchMock = vi.spyOn(window, 'fetch').mockResolvedValue(jsonResponse({ id: 5, isSystem: false, name: 'New', playlistCount: 0 }, 201));
|
||||
await createPlaylistGroup({ name: 'New' });
|
||||
const { init, url } = lastCall(fetchMock);
|
||||
expect(url).toBe('/api/playlists/groups');
|
||||
expect(url).toBe('/api/v1/playlists/groups');
|
||||
expect(init?.method).toBe('POST');
|
||||
expect(JSON.parse(String(init?.body))).toEqual({ name: 'New' });
|
||||
});
|
||||
@@ -70,7 +70,7 @@ describe('playlists api client', () => {
|
||||
const fetchMock = vi.spyOn(window, 'fetch').mockResolvedValue(jsonResponse({ id: 5, isSystem: false, name: 'Renamed', playlistCount: 0 }));
|
||||
await updatePlaylistGroup(5, { name: 'Renamed' });
|
||||
const { init, url } = lastCall(fetchMock);
|
||||
expect(url).toBe('/api/playlists/groups/5');
|
||||
expect(url).toBe('/api/v1/playlists/groups/5');
|
||||
expect(init?.method).toBe('PUT');
|
||||
expect(JSON.parse(String(init?.body))).toEqual({ name: 'Renamed' });
|
||||
});
|
||||
@@ -78,32 +78,32 @@ describe('playlists api client', () => {
|
||||
it('deletePlaylistGroup DELETEs the group', async () => {
|
||||
const fetchMock = vi.spyOn(window, 'fetch').mockResolvedValue(noContent());
|
||||
await deletePlaylistGroup(9);
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/playlists/groups/9', expect.objectContaining({ method: 'DELETE' }));
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/v1/playlists/groups/9', expect.objectContaining({ method: 'DELETE' }));
|
||||
});
|
||||
|
||||
it('getPlaylists filters by playlist group id', async () => {
|
||||
const fetchMock = vi.spyOn(window, 'fetch').mockResolvedValue(jsonResponse([]));
|
||||
await getPlaylists(3);
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/playlists?playlistGroupId=3', expect.objectContaining({ method: 'GET' }));
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/v1/playlists?playlistGroupId=3', expect.objectContaining({ method: 'GET' }));
|
||||
});
|
||||
|
||||
it('getPlaylistById fetches a single playlist', async () => {
|
||||
const fetchMock = vi.spyOn(window, 'fetch').mockResolvedValue(jsonResponse({ id: 4, isSystem: false, name: 'Bumps', playlistGroupId: 3 }));
|
||||
await expect(getPlaylistById(4)).resolves.toMatchObject({ id: 4 });
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/playlists/4', expect.objectContaining({ method: 'GET' }));
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/v1/playlists/4', expect.objectContaining({ method: 'GET' }));
|
||||
});
|
||||
|
||||
it('getPlaylistItems fetches the items of a playlist', async () => {
|
||||
const fetchMock = vi.spyOn(window, 'fetch').mockResolvedValue(jsonResponse([]));
|
||||
await getPlaylistItems(4);
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/playlists/4/items', expect.objectContaining({ method: 'GET' }));
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/v1/playlists/4/items', expect.objectContaining({ method: 'GET' }));
|
||||
});
|
||||
|
||||
it('createPlaylist POSTs the group id and name', async () => {
|
||||
const fetchMock = vi.spyOn(window, 'fetch').mockResolvedValue(jsonResponse({ id: 8, isSystem: false, name: 'Bumps', playlistGroupId: 3 }, 201));
|
||||
await createPlaylist({ name: 'Bumps', playlistGroupId: 3 });
|
||||
const { init, url } = lastCall(fetchMock);
|
||||
expect(url).toBe('/api/playlists');
|
||||
expect(url).toBe('/api/v1/playlists');
|
||||
expect(init?.method).toBe('POST');
|
||||
expect(JSON.parse(String(init?.body))).toEqual({ name: 'Bumps', playlistGroupId: 3 });
|
||||
});
|
||||
@@ -112,7 +112,7 @@ describe('playlists api client', () => {
|
||||
const fetchMock = vi.spyOn(window, 'fetch').mockResolvedValue(jsonResponse([]));
|
||||
await updatePlaylist(4, { items: [sampleItem], name: 'Bumps' });
|
||||
const { init, url } = lastCall(fetchMock);
|
||||
expect(url).toBe('/api/playlists/4');
|
||||
expect(url).toBe('/api/v1/playlists/4');
|
||||
expect(init?.method).toBe('PUT');
|
||||
expect(JSON.parse(String(init?.body))).toEqual({ items: [sampleItem], name: 'Bumps' });
|
||||
});
|
||||
@@ -120,14 +120,14 @@ describe('playlists api client', () => {
|
||||
it('deletePlaylist DELETEs the playlist', async () => {
|
||||
const fetchMock = vi.spyOn(window, 'fetch').mockResolvedValue(noContent());
|
||||
await deletePlaylist(4);
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/playlists/4', expect.objectContaining({ method: 'DELETE' }));
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/v1/playlists/4', expect.objectContaining({ method: 'DELETE' }));
|
||||
});
|
||||
|
||||
it('previewPlaylist POSTs the draft to the preview endpoint', async () => {
|
||||
const fetchMock = vi.spyOn(window, 'fetch').mockResolvedValue(jsonResponse([]));
|
||||
await previewPlaylist({ items: [sampleItem], name: 'Draft' });
|
||||
const { init, url } = lastCall(fetchMock);
|
||||
expect(url).toBe('/api/playlists/preview');
|
||||
expect(url).toBe('/api/v1/playlists/preview');
|
||||
expect(init?.method).toBe('POST');
|
||||
expect(JSON.parse(String(init?.body))).toEqual({ items: [sampleItem], name: 'Draft' });
|
||||
});
|
||||
@@ -148,7 +148,7 @@ describe('playlists api client', () => {
|
||||
};
|
||||
await addItemsToPlaylist(6, body);
|
||||
const { init, url } = lastCall(fetchMock);
|
||||
expect(url).toBe('/api/playlists/6/items');
|
||||
expect(url).toBe('/api/v1/playlists/6/items');
|
||||
expect(init?.method).toBe('POST');
|
||||
expect(JSON.parse(String(init?.body))).toEqual(body);
|
||||
});
|
||||
|
||||
+13
-13
@@ -14,40 +14,40 @@ export type CreatePlaylistRequest = components['schemas']['CreatePlaylistRequest
|
||||
export type ReplacePlaylistRequest = components['schemas']['ReplacePlaylistRequest'];
|
||||
|
||||
export function getPlaylistGroups(): Promise<PlaylistGroup[]> {
|
||||
return request<PlaylistGroup[]>('/api/playlists/groups');
|
||||
return request<PlaylistGroup[]>('/api/v1/playlists/groups');
|
||||
}
|
||||
|
||||
export function createPlaylistGroup(body: CreatePlaylistGroupRequest): Promise<PlaylistGroup> {
|
||||
return request<PlaylistGroup>('/api/playlists/groups', { body, method: 'POST' });
|
||||
return request<PlaylistGroup>('/api/v1/playlists/groups', { body, method: 'POST' });
|
||||
}
|
||||
|
||||
export function updatePlaylistGroup(id: number, body: UpdatePlaylistGroupRequest): Promise<PlaylistGroup> {
|
||||
return request<PlaylistGroup>(`/api/playlists/groups/${id}`, { body, method: 'PUT' });
|
||||
return request<PlaylistGroup>(`/api/v1/playlists/groups/${id}`, { body, method: 'PUT' });
|
||||
}
|
||||
|
||||
export function deletePlaylistGroup(id: number): Promise<void> {
|
||||
return request<void>(`/api/playlists/groups/${id}`, { method: 'DELETE' });
|
||||
return request<void>(`/api/v1/playlists/groups/${id}`, { method: 'DELETE' });
|
||||
}
|
||||
|
||||
export function getPlaylists(playlistGroupId: number): Promise<Playlist[]> {
|
||||
return request<Playlist[]>(`/api/playlists?playlistGroupId=${playlistGroupId}`);
|
||||
return request<Playlist[]>(`/api/v1/playlists?playlistGroupId=${playlistGroupId}`);
|
||||
}
|
||||
|
||||
export function getPlaylistById(id: number): Promise<Playlist> {
|
||||
return request<Playlist>(`/api/playlists/${id}`);
|
||||
return request<Playlist>(`/api/v1/playlists/${id}`);
|
||||
}
|
||||
|
||||
export function getPlaylistItems(id: number): Promise<PlaylistItem[]> {
|
||||
return request<PlaylistItem[]>(`/api/playlists/${id}/items`);
|
||||
return request<PlaylistItem[]>(`/api/v1/playlists/${id}/items`);
|
||||
}
|
||||
|
||||
/** Load playlist items together with the playlist's concurrency ETag (issue #253). */
|
||||
export function getPlaylistItemsWithMeta(id: number): Promise<ResponseWithMeta<PlaylistItem[]>> {
|
||||
return requestWithMeta<PlaylistItem[]>(`/api/playlists/${id}/items`);
|
||||
return requestWithMeta<PlaylistItem[]>(`/api/v1/playlists/${id}/items`);
|
||||
}
|
||||
|
||||
export function createPlaylist(body: CreatePlaylistRequest): Promise<Playlist> {
|
||||
return request<Playlist>('/api/playlists', { body, method: 'POST' });
|
||||
return request<Playlist>('/api/v1/playlists', { body, method: 'POST' });
|
||||
}
|
||||
|
||||
// PUT = rename + replace the full item list; returns the persisted (re-indexed) items. Pass the
|
||||
@@ -58,7 +58,7 @@ export function updatePlaylist(
|
||||
body: ReplacePlaylistRequest,
|
||||
ifMatch?: string | null
|
||||
): Promise<ResponseWithMeta<PlaylistItem[]>> {
|
||||
return requestWithMeta<PlaylistItem[]>(`/api/playlists/${id}`, {
|
||||
return requestWithMeta<PlaylistItem[]>(`/api/v1/playlists/${id}`, {
|
||||
body,
|
||||
method: 'PUT',
|
||||
headers: ifMatch ? { 'If-Match': ifMatch } : undefined
|
||||
@@ -66,11 +66,11 @@ export function updatePlaylist(
|
||||
}
|
||||
|
||||
export function deletePlaylist(id: number): Promise<void> {
|
||||
return request<void>(`/api/playlists/${id}`, { method: 'DELETE' });
|
||||
return request<void>(`/api/v1/playlists/${id}`, { method: 'DELETE' });
|
||||
}
|
||||
|
||||
export function previewPlaylist(body: ReplacePlaylistRequest): Promise<PlaylistPreviewItem[]> {
|
||||
return request<PlaylistPreviewItem[]>('/api/playlists/preview', { body, method: 'POST' });
|
||||
return request<PlaylistPreviewItem[]>('/api/v1/playlists/preview', { body, method: 'POST' });
|
||||
}
|
||||
|
||||
// Adds media items (bucketed by kind) to an existing playlist. The request body is the same
|
||||
@@ -79,7 +79,7 @@ export function previewPlaylist(body: ReplacePlaylistRequest): Promise<PlaylistP
|
||||
// for this endpoint's body, but the collection-shaped request is structurally identical, so
|
||||
// callers can pipe toAddItemsRequest / toAddItemsRequestFromSearch output straight in.
|
||||
export function addItemsToPlaylist(playlistId: number, body: AddItemsToPlaylistRequest): Promise<void> {
|
||||
return request<void>(`/api/playlists/${playlistId}/items`, { body, method: 'POST' });
|
||||
return request<void>(`/api/v1/playlists/${playlistId}/items`, { body, method: 'POST' });
|
||||
}
|
||||
|
||||
export function messageFromPlaylistError(error: unknown, fallback = 'Unable to load playlists'): string {
|
||||
|
||||
@@ -16,13 +16,13 @@ describe('getPlayoutBlocks', () => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('GETs /api/playouts/{id}/blocks', async () => {
|
||||
it('GETs /api/v1/playouts/{id}/blocks', async () => {
|
||||
const fetchSpy = mockJson([{ id: 1, name: 'Morning' }]);
|
||||
|
||||
await expect(getPlayoutBlocks(7)).resolves.toHaveLength(1);
|
||||
|
||||
const [url, init] = fetchSpy.mock.calls[0];
|
||||
expect(url).toBe('/api/playouts/7/blocks');
|
||||
expect(url).toBe('/api/v1/playouts/7/blocks');
|
||||
expect((init?.method ?? 'GET').toUpperCase()).toBe('GET');
|
||||
});
|
||||
});
|
||||
@@ -39,7 +39,7 @@ describe('getPlayoutBlockHistory', () => {
|
||||
await getPlayoutBlockHistory(7, 3);
|
||||
|
||||
const [url] = fetchSpy.mock.calls[0];
|
||||
expect(url).toBe('/api/playouts/7/blocks/3/history');
|
||||
expect(url).toBe('/api/v1/playouts/7/blocks/3/history');
|
||||
});
|
||||
|
||||
it('builds the query string from pageNum and pageSize', async () => {
|
||||
@@ -48,7 +48,7 @@ describe('getPlayoutBlockHistory', () => {
|
||||
await getPlayoutBlockHistory(7, 3, { pageNum: 2, pageSize: 25 });
|
||||
|
||||
const [url] = fetchSpy.mock.calls[0];
|
||||
expect(url).toBe('/api/playouts/7/blocks/3/history?pageNum=2&pageSize=25');
|
||||
expect(url).toBe('/api/v1/playouts/7/blocks/3/history?pageNum=2&pageSize=25');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -58,7 +58,7 @@ describe('getPlayoutHistoryDetails', () => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('GETs /api/playouts/history/{id}', async () => {
|
||||
it('GETs /api/v1/playouts/history/{id}', async () => {
|
||||
const fetchSpy = mockJson({
|
||||
playbackOrder: 'Shuffle',
|
||||
collectionType: 'Collection',
|
||||
@@ -70,7 +70,7 @@ describe('getPlayoutHistoryDetails', () => {
|
||||
await expect(getPlayoutHistoryDetails(42)).resolves.toMatchObject({ name: 'Cartoons' });
|
||||
|
||||
const [url] = fetchSpy.mock.calls[0];
|
||||
expect(url).toBe('/api/playouts/history/42');
|
||||
expect(url).toBe('/api/v1/playouts/history/42');
|
||||
});
|
||||
|
||||
it('rejects with the ApiError status on failure', async () => {
|
||||
|
||||
@@ -12,7 +12,7 @@ export interface GetBlockHistoryParams {
|
||||
}
|
||||
|
||||
export function getPlayoutBlocks(playoutId: number): Promise<PlayoutBlock[]> {
|
||||
return request<PlayoutBlock[]>(`/api/playouts/${playoutId}/blocks`);
|
||||
return request<PlayoutBlock[]>(`/api/v1/playouts/${playoutId}/blocks`);
|
||||
}
|
||||
|
||||
export function getPlayoutBlockHistory(
|
||||
@@ -33,12 +33,12 @@ export function getPlayoutBlockHistory(
|
||||
const queryString = searchParams.toString();
|
||||
|
||||
return request<PagedPlayoutHistory>(
|
||||
`/api/playouts/${playoutId}/blocks/${blockId}/history${queryString ? `?${queryString}` : ''}`
|
||||
`/api/v1/playouts/${playoutId}/blocks/${blockId}/history${queryString ? `?${queryString}` : ''}`
|
||||
);
|
||||
}
|
||||
|
||||
export function getPlayoutHistoryDetails(historyId: number): Promise<PlayoutHistoryDetails> {
|
||||
return request<PlayoutHistoryDetails>(`/api/playouts/history/${historyId}`);
|
||||
return request<PlayoutHistoryDetails>(`/api/v1/playouts/history/${historyId}`);
|
||||
}
|
||||
|
||||
export function messageFromPlayoutHistoryError(
|
||||
|
||||
@@ -45,7 +45,7 @@ describe('playouts api client', () => {
|
||||
await expect(createPlayout(body)).resolves.toMatchObject({ id: 7 });
|
||||
|
||||
const [url, init] = fetchMock.mock.calls[0];
|
||||
expect(url).toBe('/api/playouts');
|
||||
expect(url).toBe('/api/v1/playouts');
|
||||
expect(init).toMatchObject({ method: 'POST' });
|
||||
expect(JSON.parse(String(init?.body))).toMatchObject({
|
||||
channelId: 1,
|
||||
@@ -80,7 +80,7 @@ describe('playouts api client', () => {
|
||||
await updatePlayoutDetails(3, { dailyRebuildTime: '04:00:00', scheduleFile: null });
|
||||
|
||||
const [url, init] = fetchMock.mock.calls[0];
|
||||
expect(url).toBe('/api/playouts/3');
|
||||
expect(url).toBe('/api/v1/playouts/3');
|
||||
expect(init).toMatchObject({ method: 'PUT' });
|
||||
expect(JSON.parse(String(init?.body))).toMatchObject({ dailyRebuildTime: '04:00:00' });
|
||||
});
|
||||
@@ -100,7 +100,7 @@ describe('playouts api client', () => {
|
||||
await updatePlayoutDefaultDeco(3, 7);
|
||||
|
||||
const [url, init] = fetchMock.mock.calls[0];
|
||||
expect(url).toBe('/api/playouts/3/deco');
|
||||
expect(url).toBe('/api/v1/playouts/3/deco');
|
||||
expect(init).toMatchObject({ method: 'PUT' });
|
||||
expect(JSON.parse(String(init?.body))).toMatchObject({ decoId: 7 });
|
||||
});
|
||||
@@ -111,7 +111,7 @@ describe('playouts api client', () => {
|
||||
await expect(deletePlayout(4)).resolves.toBeUndefined();
|
||||
|
||||
const [url, init] = fetchMock.mock.calls[0];
|
||||
expect(url).toBe('/api/playouts/4');
|
||||
expect(url).toBe('/api/v1/playouts/4');
|
||||
expect(init).toMatchObject({ method: 'DELETE' });
|
||||
});
|
||||
|
||||
@@ -121,7 +121,7 @@ describe('playouts api client', () => {
|
||||
await resetChannelPlayout(20);
|
||||
|
||||
const [url, init] = fetchMock.mock.calls[0];
|
||||
expect(url).toBe('/api/channels/20/playout/reset');
|
||||
expect(url).toBe('/api/v1/channels/20/playout/reset');
|
||||
expect(init).toMatchObject({ method: 'POST' });
|
||||
});
|
||||
|
||||
@@ -131,7 +131,7 @@ describe('playouts api client', () => {
|
||||
await erasePlayoutItems(6);
|
||||
|
||||
const [url, init] = fetchMock.mock.calls[0];
|
||||
expect(url).toBe('/api/playouts/6/erase-items');
|
||||
expect(url).toBe('/api/v1/playouts/6/erase-items');
|
||||
expect(init).toMatchObject({ method: 'POST' });
|
||||
});
|
||||
|
||||
@@ -141,7 +141,7 @@ describe('playouts api client', () => {
|
||||
await erasePlayoutItemsAndHistory(6);
|
||||
|
||||
const [url, init] = fetchMock.mock.calls[0];
|
||||
expect(url).toBe('/api/playouts/6/erase-items-and-history');
|
||||
expect(url).toBe('/api/v1/playouts/6/erase-items-and-history');
|
||||
expect(init).toMatchObject({ method: 'POST' });
|
||||
});
|
||||
|
||||
@@ -151,7 +151,7 @@ describe('playouts api client', () => {
|
||||
await expect(getPlayoutItemSchedulingContext(42)).resolves.toMatchObject({ context: '{ "x": 1 }' });
|
||||
|
||||
const [url, init] = fetchMock.mock.calls[0];
|
||||
expect(url).toBe('/api/playouts/items/42/scheduling-context');
|
||||
expect(url).toBe('/api/v1/playouts/items/42/scheduling-context');
|
||||
expect(init?.method ?? 'GET').toBe('GET');
|
||||
});
|
||||
|
||||
@@ -161,7 +161,7 @@ describe('playouts api client', () => {
|
||||
await getAlternateSchedules(5);
|
||||
|
||||
const [url, init] = fetchMock.mock.calls[0];
|
||||
expect(url).toBe('/api/playouts/5/alternate-schedules');
|
||||
expect(url).toBe('/api/v1/playouts/5/alternate-schedules');
|
||||
expect(init?.method ?? 'GET').toBe('GET');
|
||||
});
|
||||
|
||||
@@ -188,7 +188,7 @@ describe('playouts api client', () => {
|
||||
});
|
||||
|
||||
const [url, init] = fetchMock.mock.calls[0];
|
||||
expect(url).toBe('/api/playouts/5/alternate-schedules');
|
||||
expect(url).toBe('/api/v1/playouts/5/alternate-schedules');
|
||||
expect(init).toMatchObject({ method: 'PUT' });
|
||||
const body = JSON.parse(String(init?.body));
|
||||
expect(body.items[0].daysOfWeek).toEqual(['Monday', 'Tuesday']);
|
||||
@@ -201,7 +201,7 @@ describe('playouts api client', () => {
|
||||
await getPlayoutTemplates(5);
|
||||
|
||||
const [url] = fetchMock.mock.calls[0];
|
||||
expect(url).toBe('/api/playouts/5/templates');
|
||||
expect(url).toBe('/api/v1/playouts/5/templates');
|
||||
});
|
||||
|
||||
it('replacePlayoutTemplates PUTs template items', async () => {
|
||||
@@ -228,7 +228,7 @@ describe('playouts api client', () => {
|
||||
});
|
||||
|
||||
const [url, init] = fetchMock.mock.calls[0];
|
||||
expect(url).toBe('/api/playouts/5/templates');
|
||||
expect(url).toBe('/api/v1/playouts/5/templates');
|
||||
expect(init).toMatchObject({ method: 'PUT' });
|
||||
const body = JSON.parse(String(init?.body));
|
||||
expect(body.items[0].templateId).toBe(3);
|
||||
|
||||
+21
-21
@@ -86,23 +86,23 @@ type PlayoutsScreenState =
|
||||
| { data: null; error: null; status: 'loading' };
|
||||
|
||||
export function getPlayouts(): Promise<PlayoutsPage> {
|
||||
return request<PlayoutsPage>('/api/playouts');
|
||||
return request<PlayoutsPage>('/api/v1/playouts');
|
||||
}
|
||||
|
||||
export function getPlayout(playoutId: number): Promise<PlayoutDetail> {
|
||||
return request<PlayoutDetail>(`/api/playouts/${playoutId}`);
|
||||
return request<PlayoutDetail>(`/api/v1/playouts/${playoutId}`);
|
||||
}
|
||||
|
||||
export function getPlayoutItems(playoutId: number, showFiller = false): Promise<PlayoutItemsPage> {
|
||||
return request<PlayoutItemsPage>(`/api/playouts/${playoutId}/items${showFiller ? '?showFiller=true' : ''}`);
|
||||
return request<PlayoutItemsPage>(`/api/v1/playouts/${playoutId}/items${showFiller ? '?showFiller=true' : ''}`);
|
||||
}
|
||||
|
||||
export function getPlayoutWarningsCount(): Promise<number> {
|
||||
return request<number>('/api/playouts/warnings/count');
|
||||
return request<number>('/api/v1/playouts/warnings/count');
|
||||
}
|
||||
|
||||
export function getPlayoutChannelStates(): Promise<PlayoutChannelState[]> {
|
||||
return request<PlayoutChannelState[]>('/api/channels/state');
|
||||
return request<PlayoutChannelState[]>('/api/v1/channels/state');
|
||||
}
|
||||
|
||||
export type ResetAllPlayoutsResult = components['schemas']['ResetAllPlayoutsResponseModel'];
|
||||
@@ -111,62 +111,62 @@ export type ResetAllPlayoutsResult = components['schemas']['ResetAllPlayoutsResp
|
||||
// queued vs skipped (locked, or an unsupported ExternalJson/None schedule kind) — #235 replaced the
|
||||
// old silent skip. The caller may surface `skipped*` to explain why some playouts didn't reset.
|
||||
export function resetAllPlayouts(): Promise<ResetAllPlayoutsResult> {
|
||||
return request<ResetAllPlayoutsResult>('/api/playouts/reset-all', { method: 'POST' });
|
||||
return request<ResetAllPlayoutsResult>('/api/v1/playouts/reset-all', { method: 'POST' });
|
||||
}
|
||||
|
||||
export function deletePlayout(playoutId: number): Promise<void> {
|
||||
return request<void>(`/api/playouts/${playoutId}`, { method: 'DELETE' });
|
||||
return request<void>(`/api/v1/playouts/${playoutId}`, { method: 'DELETE' });
|
||||
}
|
||||
|
||||
// Resets the given channel's playout. The server picks the correct default mode per schedule kind
|
||||
// (Classic → Refresh, others → Reset), matching the Blazor "Reset Playout" action — so no mode is sent.
|
||||
// Keyed on the immutable channel id (not the user-mutable number), matching the /api/channels/{id} contract.
|
||||
// Keyed on the immutable channel id (not the user-mutable number), matching the /api/v1/channels/{id} contract.
|
||||
export function resetChannelPlayout(channelId: number): Promise<void> {
|
||||
return request<void>(`/api/channels/${channelId}/playout/reset`, { method: 'POST' });
|
||||
return request<void>(`/api/v1/channels/${channelId}/playout/reset`, { method: 'POST' });
|
||||
}
|
||||
|
||||
export function erasePlayoutItems(playoutId: number): Promise<void> {
|
||||
return request<void>(`/api/playouts/${playoutId}/erase-items`, { method: 'POST' });
|
||||
return request<void>(`/api/v1/playouts/${playoutId}/erase-items`, { method: 'POST' });
|
||||
}
|
||||
|
||||
export function erasePlayoutItemsAndHistory(playoutId: number): Promise<void> {
|
||||
return request<void>(`/api/playouts/${playoutId}/erase-items-and-history`, { method: 'POST' });
|
||||
return request<void>(`/api/v1/playouts/${playoutId}/erase-items-and-history`, { method: 'POST' });
|
||||
}
|
||||
|
||||
export function getPlayoutItemSchedulingContext(itemId: number): Promise<PlayoutItemSchedulingContext> {
|
||||
return request<PlayoutItemSchedulingContext>(`/api/playouts/items/${itemId}/scheduling-context`);
|
||||
return request<PlayoutItemSchedulingContext>(`/api/v1/playouts/items/${itemId}/scheduling-context`);
|
||||
}
|
||||
|
||||
export function createPlayout(body: CreatePlayoutRequest): Promise<PlayoutDetail> {
|
||||
return request<PlayoutDetail>('/api/playouts', {
|
||||
return request<PlayoutDetail>('/api/v1/playouts', {
|
||||
body,
|
||||
method: 'POST'
|
||||
});
|
||||
}
|
||||
|
||||
export function updatePlayoutDetails(playoutId: number, body: UpdatePlayoutDetailsRequest): Promise<PlayoutDetail> {
|
||||
return request<PlayoutDetail>(`/api/playouts/${playoutId}`, {
|
||||
return request<PlayoutDetail>(`/api/v1/playouts/${playoutId}`, {
|
||||
body,
|
||||
method: 'PUT'
|
||||
});
|
||||
}
|
||||
|
||||
export function updatePlayoutDefaultDeco(playoutId: number, decoId: number | null): Promise<PlayoutDetail> {
|
||||
return request<PlayoutDetail>(`/api/playouts/${playoutId}/deco`, {
|
||||
return request<PlayoutDetail>(`/api/v1/playouts/${playoutId}/deco`, {
|
||||
body: { decoId },
|
||||
method: 'PUT'
|
||||
});
|
||||
}
|
||||
|
||||
export function getAlternateSchedules(playoutId: number): Promise<PlayoutAlternateSchedule[]> {
|
||||
return request<PlayoutAlternateSchedule[]>(`/api/playouts/${playoutId}/alternate-schedules`);
|
||||
return request<PlayoutAlternateSchedule[]>(`/api/v1/playouts/${playoutId}/alternate-schedules`);
|
||||
}
|
||||
|
||||
/** Load alternate schedules together with the playout's concurrency ETag (issue #253). */
|
||||
export function getAlternateSchedulesWithMeta(
|
||||
playoutId: number
|
||||
): Promise<ResponseWithMeta<PlayoutAlternateSchedule[]>> {
|
||||
return requestWithMeta<PlayoutAlternateSchedule[]>(`/api/playouts/${playoutId}/alternate-schedules`);
|
||||
return requestWithMeta<PlayoutAlternateSchedule[]>(`/api/v1/playouts/${playoutId}/alternate-schedules`);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -178,7 +178,7 @@ export function replaceAlternateSchedules(
|
||||
body: ReplacePlayoutAlternateSchedulesRequest,
|
||||
ifMatch?: string | null
|
||||
): Promise<ResponseWithMeta<PlayoutAlternateSchedule[]>> {
|
||||
return requestWithMeta<PlayoutAlternateSchedule[]>(`/api/playouts/${playoutId}/alternate-schedules`, {
|
||||
return requestWithMeta<PlayoutAlternateSchedule[]>(`/api/v1/playouts/${playoutId}/alternate-schedules`, {
|
||||
body,
|
||||
method: 'PUT',
|
||||
headers: ifMatch ? { 'If-Match': ifMatch } : undefined
|
||||
@@ -186,12 +186,12 @@ export function replaceAlternateSchedules(
|
||||
}
|
||||
|
||||
export function getPlayoutTemplates(playoutId: number): Promise<PlayoutTemplate[]> {
|
||||
return request<PlayoutTemplate[]>(`/api/playouts/${playoutId}/templates`);
|
||||
return request<PlayoutTemplate[]>(`/api/v1/playouts/${playoutId}/templates`);
|
||||
}
|
||||
|
||||
/** Load playout templates together with the playout's concurrency ETag (issue #253). */
|
||||
export function getPlayoutTemplatesWithMeta(playoutId: number): Promise<ResponseWithMeta<PlayoutTemplate[]>> {
|
||||
return requestWithMeta<PlayoutTemplate[]>(`/api/playouts/${playoutId}/templates`);
|
||||
return requestWithMeta<PlayoutTemplate[]>(`/api/v1/playouts/${playoutId}/templates`);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -203,7 +203,7 @@ export function replacePlayoutTemplates(
|
||||
body: ReplacePlayoutTemplatesRequest,
|
||||
ifMatch?: string | null
|
||||
): Promise<ResponseWithMeta<PlayoutTemplate[]>> {
|
||||
return requestWithMeta<PlayoutTemplate[]>(`/api/playouts/${playoutId}/templates`, {
|
||||
return requestWithMeta<PlayoutTemplate[]>(`/api/v1/playouts/${playoutId}/templates`, {
|
||||
body,
|
||||
method: 'PUT',
|
||||
headers: ifMatch ? { 'If-Match': ifMatch } : undefined
|
||||
|
||||
@@ -32,7 +32,7 @@ describe('rerunCollections api client', () => {
|
||||
await getRerunCollections({ pageNum: 1, pageSize: 50, query: 'noir' });
|
||||
|
||||
const url = new URL(String(fetchMock.mock.calls[0][0]), 'http://localhost');
|
||||
expect(url.pathname).toBe('/api/rerun-collections');
|
||||
expect(url.pathname).toBe('/api/v1/rerun-collections');
|
||||
expect(url.searchParams.get('pageNum')).toBe('1');
|
||||
expect(url.searchParams.get('pageSize')).toBe('50');
|
||||
expect(url.searchParams.get('query')).toBe('noir');
|
||||
@@ -43,7 +43,7 @@ describe('rerunCollections api client', () => {
|
||||
|
||||
await getRerunCollections();
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/rerun-collections', expect.objectContaining({ method: 'GET' }));
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/v1/rerun-collections', expect.objectContaining({ method: 'GET' }));
|
||||
});
|
||||
|
||||
it('getRerunCollection fetches a single rerun collection by id', async () => {
|
||||
@@ -60,7 +60,7 @@ describe('rerunCollections api client', () => {
|
||||
);
|
||||
|
||||
await expect(getRerunCollection(3)).resolves.toMatchObject({ id: 3, selectedName: 'Favorites' });
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/rerun-collections/3', expect.objectContaining({ method: 'GET' }));
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/v1/rerun-collections/3', expect.objectContaining({ method: 'GET' }));
|
||||
});
|
||||
|
||||
it('createRerunCollection POSTs the full tagged-union body', async () => {
|
||||
@@ -76,7 +76,7 @@ describe('rerunCollections api client', () => {
|
||||
await expect(createRerunCollection(body)).resolves.toMatchObject({ id: 12 });
|
||||
|
||||
const [url, init] = fetchMock.mock.calls[0];
|
||||
expect(url).toBe('/api/rerun-collections');
|
||||
expect(url).toBe('/api/v1/rerun-collections');
|
||||
expect(init).toMatchObject({ method: 'POST' });
|
||||
expect(JSON.parse(String(init?.body))).toEqual(body);
|
||||
});
|
||||
@@ -94,7 +94,7 @@ describe('rerunCollections api client', () => {
|
||||
await updateRerunCollection(5, body);
|
||||
|
||||
const [url, init] = fetchMock.mock.calls[0];
|
||||
expect(url).toBe('/api/rerun-collections/5');
|
||||
expect(url).toBe('/api/v1/rerun-collections/5');
|
||||
expect(init).toMatchObject({ method: 'PUT' });
|
||||
expect(JSON.parse(String(init?.body))).toEqual(body);
|
||||
});
|
||||
@@ -103,7 +103,7 @@ describe('rerunCollections api client', () => {
|
||||
const fetchMock = vi.spyOn(window, 'fetch').mockResolvedValue(noContent());
|
||||
|
||||
await expect(deleteRerunCollection(7)).resolves.toBeUndefined();
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/rerun-collections/7', expect.objectContaining({ method: 'DELETE' }));
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/v1/rerun-collections/7', expect.objectContaining({ method: 'DELETE' }));
|
||||
});
|
||||
|
||||
it('messageFromRerunCollectionError narrows ApiError, Error, and unknown', () => {
|
||||
|
||||
@@ -29,20 +29,20 @@ export function getRerunCollections(params: GetRerunCollectionsParams = {}): Pro
|
||||
|
||||
const queryString = searchParams.toString();
|
||||
|
||||
return request<PagedRerunCollections>(`/api/rerun-collections${queryString ? `?${queryString}` : ''}`);
|
||||
return request<PagedRerunCollections>(`/api/v1/rerun-collections${queryString ? `?${queryString}` : ''}`);
|
||||
}
|
||||
|
||||
export function getRerunCollection(id: number): Promise<RerunCollection> {
|
||||
return request<RerunCollection>(`/api/rerun-collections/${id}`);
|
||||
return request<RerunCollection>(`/api/v1/rerun-collections/${id}`);
|
||||
}
|
||||
|
||||
/** Load a single rerun collection together with its concurrency ETag (issue #253). */
|
||||
export function getRerunCollectionWithMeta(id: number): Promise<ResponseWithMeta<RerunCollection>> {
|
||||
return requestWithMeta<RerunCollection>(`/api/rerun-collections/${id}`);
|
||||
return requestWithMeta<RerunCollection>(`/api/v1/rerun-collections/${id}`);
|
||||
}
|
||||
|
||||
export function createRerunCollection(body: CreateRerunCollectionRequest): Promise<RerunCollection> {
|
||||
return request<RerunCollection>('/api/rerun-collections', { body, method: 'POST' });
|
||||
return request<RerunCollection>('/api/v1/rerun-collections', { body, method: 'POST' });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -54,7 +54,7 @@ export function updateRerunCollection(
|
||||
body: UpdateRerunCollectionRequest,
|
||||
ifMatch?: string | null
|
||||
): Promise<ResponseWithMeta<RerunCollection>> {
|
||||
return requestWithMeta<RerunCollection>(`/api/rerun-collections/${id}`, {
|
||||
return requestWithMeta<RerunCollection>(`/api/v1/rerun-collections/${id}`, {
|
||||
body,
|
||||
method: 'PUT',
|
||||
headers: ifMatch ? { 'If-Match': ifMatch } : undefined
|
||||
@@ -62,7 +62,7 @@ export function updateRerunCollection(
|
||||
}
|
||||
|
||||
export function deleteRerunCollection(id: number): Promise<void> {
|
||||
return request<void>(`/api/rerun-collections/${id}`, { method: 'DELETE' });
|
||||
return request<void>(`/api/v1/rerun-collections/${id}`, { method: 'DELETE' });
|
||||
}
|
||||
|
||||
export function messageFromRerunCollectionError(
|
||||
|
||||
+11
-11
@@ -21,40 +21,40 @@ export type FillerKind = components['schemas']['FillerKind'];
|
||||
// ---- Schedule CRUD -------------------------------------------------------
|
||||
|
||||
export function getSchedules(): Promise<ProgramSchedule[]> {
|
||||
return request<ProgramSchedule[]>('/api/schedules');
|
||||
return request<ProgramSchedule[]>('/api/v1/schedules');
|
||||
}
|
||||
|
||||
export function getScheduleById(id: number): Promise<ProgramSchedule> {
|
||||
return request<ProgramSchedule>(`/api/schedules/${id}`);
|
||||
return request<ProgramSchedule>(`/api/v1/schedules/${id}`);
|
||||
}
|
||||
|
||||
export function createSchedule(body: CreateScheduleRequest): Promise<ProgramSchedule> {
|
||||
return request<ProgramSchedule>('/api/schedules', { body, method: 'POST' });
|
||||
return request<ProgramSchedule>('/api/v1/schedules', { body, method: 'POST' });
|
||||
}
|
||||
|
||||
export function updateSchedule(id: number, body: UpdateScheduleRequest): Promise<ProgramSchedule> {
|
||||
return request<ProgramSchedule>(`/api/schedules/${id}`, { body, method: 'PUT' });
|
||||
return request<ProgramSchedule>(`/api/v1/schedules/${id}`, { body, method: 'PUT' });
|
||||
}
|
||||
|
||||
export function deleteSchedule(id: number): Promise<void> {
|
||||
return request<void>(`/api/schedules/${id}`, { method: 'DELETE' });
|
||||
return request<void>(`/api/v1/schedules/${id}`, { method: 'DELETE' });
|
||||
}
|
||||
|
||||
// ---- Schedule items ------------------------------------------------------
|
||||
|
||||
export function getScheduleItems(scheduleId: number): Promise<ScheduleItemsResponse> {
|
||||
return request<ScheduleItemsResponse>(`/api/schedules/${scheduleId}/items`);
|
||||
return request<ScheduleItemsResponse>(`/api/v1/schedules/${scheduleId}/items`);
|
||||
}
|
||||
|
||||
/** Load schedule items together with the schedule's concurrency ETag (issue #253). */
|
||||
export function getScheduleItemsWithMeta(
|
||||
scheduleId: number
|
||||
): Promise<ResponseWithMeta<ScheduleItemsResponse>> {
|
||||
return requestWithMeta<ScheduleItemsResponse>(`/api/schedules/${scheduleId}/items`);
|
||||
return requestWithMeta<ScheduleItemsResponse>(`/api/v1/schedules/${scheduleId}/items`);
|
||||
}
|
||||
|
||||
export function addScheduleItem(scheduleId: number, body: ScheduleItemRequest): Promise<ScheduleItem> {
|
||||
return request<ScheduleItem>(`/api/schedules/${scheduleId}/items`, { body, method: 'POST' });
|
||||
return request<ScheduleItem>(`/api/v1/schedules/${scheduleId}/items`, { body, method: 'POST' });
|
||||
}
|
||||
|
||||
// Id-based reconcile (#259): each item's `id` (round-tripped from the GET response by the editor;
|
||||
@@ -70,7 +70,7 @@ export function replaceScheduleItems(
|
||||
body: ReplaceScheduleItemsRequest,
|
||||
ifMatch?: string | null
|
||||
): Promise<ResponseWithMeta<ScheduleItem[]>> {
|
||||
return requestWithMeta<ScheduleItem[]>(`/api/schedules/${scheduleId}/items`, {
|
||||
return requestWithMeta<ScheduleItem[]>(`/api/v1/schedules/${scheduleId}/items`, {
|
||||
body,
|
||||
method: 'PUT',
|
||||
headers: ifMatch ? { 'If-Match': ifMatch } : undefined
|
||||
@@ -78,14 +78,14 @@ export function replaceScheduleItems(
|
||||
}
|
||||
|
||||
export function deleteScheduleItem(scheduleId: number, itemId: number): Promise<void> {
|
||||
return request<void>(`/api/schedules/${scheduleId}/items/${itemId}`, { method: 'DELETE' });
|
||||
return request<void>(`/api/v1/schedules/${scheduleId}/items/${itemId}`, { method: 'DELETE' });
|
||||
}
|
||||
|
||||
// ---- Pickers -------------------------------------------------------------
|
||||
// getLanguages lives in ./languages (imported above); re-exported implicitly via api/index.ts.
|
||||
|
||||
export function getFillerPresetsByKind(fillerKind: FillerKind): Promise<FillerPreset[]> {
|
||||
return request<FillerPreset[]>(`/api/filler-presets?fillerKind=${encodeURIComponent(fillerKind)}`);
|
||||
return request<FillerPreset[]>(`/api/v1/filler-presets?fillerKind=${encodeURIComponent(fillerKind)}`);
|
||||
}
|
||||
|
||||
// The search-backed source pickers (searchTelevisionShows/…/searchMultiCollections) already live in
|
||||
|
||||
@@ -21,7 +21,7 @@ describe('getSearchResults', () => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('GETs /api/search with the query', async () => {
|
||||
it('GETs /api/v1/search with the query', async () => {
|
||||
const fetchSpy = vi.spyOn(window, 'fetch').mockResolvedValue(
|
||||
new Response(JSON.stringify(sampleResults), {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
@@ -32,7 +32,7 @@ describe('getSearchResults', () => {
|
||||
await getSearchResults({ query: 'star' });
|
||||
|
||||
const [url, init] = fetchSpy.mock.calls[0];
|
||||
expect(url).toBe('/api/search?query=star');
|
||||
expect(url).toBe('/api/v1/search?query=star');
|
||||
expect((init?.method ?? 'GET').toUpperCase()).toBe('GET');
|
||||
});
|
||||
|
||||
@@ -47,7 +47,7 @@ describe('getSearchResults', () => {
|
||||
await getSearchResults({ query: 'star wars', pageSize: 50 });
|
||||
|
||||
const [url] = fetchSpy.mock.calls[0];
|
||||
expect(url).toBe('/api/search?query=star+wars&pageSize=50');
|
||||
expect(url).toBe('/api/v1/search?query=star+wars&pageSize=50');
|
||||
});
|
||||
|
||||
it('rejects with the ApiError status on failure', async () => {
|
||||
@@ -68,7 +68,7 @@ describe('getSearchAllItems', () => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('GETs /api/search/all-items with the query', async () => {
|
||||
it('GETs /api/v1/search/all-items with the query', async () => {
|
||||
const fetchSpy = vi.spyOn(window, 'fetch').mockResolvedValue(
|
||||
new Response(JSON.stringify({ movieIds: [1], showIds: [2] }), {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
@@ -79,7 +79,7 @@ describe('getSearchAllItems', () => {
|
||||
await getSearchAllItems('star wars');
|
||||
|
||||
const [url, init] = fetchSpy.mock.calls[0];
|
||||
expect(url).toBe('/api/search/all-items?query=star+wars');
|
||||
expect(url).toBe('/api/v1/search/all-items?query=star+wars');
|
||||
expect((init?.method ?? 'GET').toUpperCase()).toBe('GET');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -23,7 +23,7 @@ export function getSearchResults(params: GetSearchResultsParams): Promise<Search
|
||||
searchParams.set('pageSize', String(params.pageSize));
|
||||
}
|
||||
|
||||
return request<SearchResults>(`/api/search?${searchParams.toString()}`);
|
||||
return request<SearchResults>(`/api/v1/search?${searchParams.toString()}`);
|
||||
}
|
||||
|
||||
// Resolves a search query to the full set of matching media-item ids, bucketed by kind. Backs the
|
||||
@@ -31,7 +31,7 @@ export function getSearchResults(params: GetSearchResultsParams): Promise<Search
|
||||
export function getSearchAllItems(query: string): Promise<SearchAllItemIds> {
|
||||
const searchParams = new URLSearchParams();
|
||||
searchParams.set('query', query);
|
||||
return request<SearchAllItemIds>(`/api/search/all-items?${searchParams.toString()}`);
|
||||
return request<SearchAllItemIds>(`/api/v1/search/all-items?${searchParams.toString()}`);
|
||||
}
|
||||
|
||||
// Normalizes a SearchAllItemIds result (nullable arrays) into a full AddItemsToCollectionRequest
|
||||
|
||||
@@ -38,23 +38,23 @@ describe('settings API module', () => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('fetches ffmpeg settings from GET /api/settings/ffmpeg', async () => {
|
||||
it('fetches ffmpeg settings from GET /api/v1/settings/ffmpeg', async () => {
|
||||
const settings = { fFmpegPath: '/usr/bin/ffmpeg' };
|
||||
vi.spyOn(window, 'fetch').mockImplementation(() => Promise.resolve(jsonResponse(settings)));
|
||||
|
||||
await expect(getFfmpegSettings()).resolves.toMatchObject(settings);
|
||||
const [path] = lastFetchCall();
|
||||
expect(path.toString()).toBe('/api/settings/ffmpeg');
|
||||
expect(path.toString()).toBe('/api/v1/settings/ffmpeg');
|
||||
});
|
||||
|
||||
it('PUTs ffmpeg settings to /api/settings/ffmpeg', async () => {
|
||||
it('PUTs ffmpeg settings to /api/v1/settings/ffmpeg', async () => {
|
||||
const body = { fFmpegPath: '/usr/bin/ffmpeg', hlsDirectOutputFormat: 'MpegTs' as const };
|
||||
vi.spyOn(window, 'fetch').mockResolvedValue(jsonResponse(body));
|
||||
|
||||
await updateFfmpegSettings(body as never);
|
||||
|
||||
const [path, init] = lastFetchCall();
|
||||
expect(path.toString()).toBe('/api/settings/ffmpeg');
|
||||
expect(path.toString()).toBe('/api/v1/settings/ffmpeg');
|
||||
expect(init?.method).toBe('PUT');
|
||||
expect(JSON.parse(init?.body as string)).toMatchObject({ fFmpegPath: '/usr/bin/ffmpeg' });
|
||||
});
|
||||
@@ -64,11 +64,11 @@ describe('settings API module', () => {
|
||||
vi.spyOn(window, 'fetch').mockImplementation(() => Promise.resolve(jsonResponse(settings)));
|
||||
|
||||
await expect(getPlayoutSettings()).resolves.toMatchObject(settings);
|
||||
expect(lastFetchCall()[0].toString()).toBe('/api/settings/playout');
|
||||
expect(lastFetchCall()[0].toString()).toBe('/api/v1/settings/playout');
|
||||
|
||||
await updatePlayoutSettings(settings);
|
||||
const [path, init] = lastFetchCall();
|
||||
expect(path.toString()).toBe('/api/settings/playout');
|
||||
expect(path.toString()).toBe('/api/v1/settings/playout');
|
||||
expect(init?.method).toBe('PUT');
|
||||
});
|
||||
|
||||
@@ -77,11 +77,11 @@ describe('settings API module', () => {
|
||||
vi.spyOn(window, 'fetch').mockImplementation(() => Promise.resolve(jsonResponse(settings)));
|
||||
|
||||
await expect(getXmltvSettings()).resolves.toMatchObject(settings);
|
||||
expect(lastFetchCall()[0].toString()).toBe('/api/settings/xmltv');
|
||||
expect(lastFetchCall()[0].toString()).toBe('/api/v1/settings/xmltv');
|
||||
|
||||
await updateXmltvSettings(settings);
|
||||
const [path, init] = lastFetchCall();
|
||||
expect(path.toString()).toBe('/api/settings/xmltv');
|
||||
expect(path.toString()).toBe('/api/v1/settings/xmltv');
|
||||
expect(init?.method).toBe('PUT');
|
||||
});
|
||||
|
||||
@@ -90,11 +90,11 @@ describe('settings API module', () => {
|
||||
vi.spyOn(window, 'fetch').mockImplementation(() => Promise.resolve(jsonResponse(settings)));
|
||||
|
||||
await expect(getScannerSettings()).resolves.toMatchObject(settings);
|
||||
expect(lastFetchCall()[0].toString()).toBe('/api/settings/scanner');
|
||||
expect(lastFetchCall()[0].toString()).toBe('/api/v1/settings/scanner');
|
||||
|
||||
await updateScannerSettings(settings);
|
||||
const [path, init] = lastFetchCall();
|
||||
expect(path.toString()).toBe('/api/settings/scanner');
|
||||
expect(path.toString()).toBe('/api/v1/settings/scanner');
|
||||
expect(init?.method).toBe('PUT');
|
||||
});
|
||||
|
||||
@@ -110,11 +110,11 @@ describe('settings API module', () => {
|
||||
vi.spyOn(window, 'fetch').mockImplementation(() => Promise.resolve(jsonResponse(settings)));
|
||||
|
||||
await expect(getLoggingSettings()).resolves.toMatchObject(settings);
|
||||
expect(lastFetchCall()[0].toString()).toBe('/api/settings/logging');
|
||||
expect(lastFetchCall()[0].toString()).toBe('/api/v1/settings/logging');
|
||||
|
||||
await updateLoggingSettings(settings);
|
||||
const [path, init] = lastFetchCall();
|
||||
expect(path.toString()).toBe('/api/settings/logging');
|
||||
expect(path.toString()).toBe('/api/v1/settings/logging');
|
||||
expect(init?.method).toBe('PUT');
|
||||
});
|
||||
|
||||
@@ -123,11 +123,11 @@ describe('settings API module', () => {
|
||||
vi.spyOn(window, 'fetch').mockImplementation(() => Promise.resolve(jsonResponse(settings)));
|
||||
|
||||
await expect(getUiSettings()).resolves.toMatchObject(settings);
|
||||
expect(lastFetchCall()[0].toString()).toBe('/api/settings/ui');
|
||||
expect(lastFetchCall()[0].toString()).toBe('/api/v1/settings/ui');
|
||||
|
||||
await updateUiSettings(settings);
|
||||
const [path, init] = lastFetchCall();
|
||||
expect(path.toString()).toBe('/api/settings/ui');
|
||||
expect(path.toString()).toBe('/api/v1/settings/ui');
|
||||
expect(init?.method).toBe('PUT');
|
||||
});
|
||||
|
||||
@@ -136,41 +136,41 @@ describe('settings API module', () => {
|
||||
vi.spyOn(window, 'fetch').mockImplementation(() => Promise.resolve(jsonResponse(settings)));
|
||||
|
||||
await expect(getHdhrSettings()).resolves.toMatchObject(settings);
|
||||
expect(lastFetchCall()[0].toString()).toBe('/api/settings/hdhr');
|
||||
expect(lastFetchCall()[0].toString()).toBe('/api/v1/settings/hdhr');
|
||||
|
||||
await updateHdhrSettings({ tunerCount: 2 });
|
||||
const [path, init] = lastFetchCall();
|
||||
expect(path.toString()).toBe('/api/settings/hdhr');
|
||||
expect(path.toString()).toBe('/api/v1/settings/hdhr');
|
||||
expect(init?.method).toBe('PUT');
|
||||
expect(JSON.parse(init?.body as string)).toMatchObject({ tunerCount: 2 });
|
||||
});
|
||||
|
||||
it('fetches resolutions from GET /api/settings/resolutions', async () => {
|
||||
it('fetches resolutions from GET /api/v1/settings/resolutions', async () => {
|
||||
const resolutions = [{ height: 1080, id: 1, isCustom: false, name: '1920x1080', width: 1920 }];
|
||||
vi.spyOn(window, 'fetch').mockResolvedValue(jsonResponse(resolutions));
|
||||
|
||||
await expect(getResolutions()).resolves.toMatchObject(resolutions);
|
||||
expect(lastFetchCall()[0].toString()).toBe('/api/settings/resolutions');
|
||||
expect(lastFetchCall()[0].toString()).toBe('/api/v1/settings/resolutions');
|
||||
});
|
||||
|
||||
it('creates a custom resolution via POST /api/settings/resolutions', async () => {
|
||||
it('creates a custom resolution via POST /api/v1/settings/resolutions', async () => {
|
||||
const created = { height: 1080, id: 3, isCustom: true, name: '2560x1080', width: 2560 };
|
||||
vi.spyOn(window, 'fetch').mockResolvedValue(jsonResponse(created, 201));
|
||||
|
||||
await expect(createResolution({ height: 1080, width: 2560 })).resolves.toMatchObject(created);
|
||||
const [path, init] = lastFetchCall();
|
||||
expect(path.toString()).toBe('/api/settings/resolutions');
|
||||
expect(path.toString()).toBe('/api/v1/settings/resolutions');
|
||||
expect(init?.method).toBe('POST');
|
||||
expect(JSON.parse(init?.body as string)).toMatchObject({ height: 1080, width: 2560 });
|
||||
});
|
||||
|
||||
it('deletes a custom resolution via DELETE /api/settings/resolutions/{id}', async () => {
|
||||
it('deletes a custom resolution via DELETE /api/v1/settings/resolutions/{id}', async () => {
|
||||
vi.spyOn(window, 'fetch').mockResolvedValue(new Response(null, { status: 204 }));
|
||||
|
||||
await deleteResolution(3);
|
||||
|
||||
const [path, init] = lastFetchCall();
|
||||
expect(path.toString()).toBe('/api/settings/resolutions/3');
|
||||
expect(path.toString()).toBe('/api/v1/settings/resolutions/3');
|
||||
expect(init?.method).toBe('DELETE');
|
||||
});
|
||||
|
||||
|
||||
+17
-17
@@ -28,71 +28,71 @@ export type OutputFormatKind = components['schemas']['OutputFormatKind'];
|
||||
export type HealthCheck = components['schemas']['HealthCheckResponseModel'];
|
||||
|
||||
export function getFfmpegSettings(): Promise<FfmpegSettings> {
|
||||
return request<FfmpegSettings>('/api/settings/ffmpeg');
|
||||
return request<FfmpegSettings>('/api/v1/settings/ffmpeg');
|
||||
}
|
||||
|
||||
export function updateFfmpegSettings(body: UpdateFfmpegSettingsRequest): Promise<FfmpegSettings> {
|
||||
return request<FfmpegSettings>('/api/settings/ffmpeg', { body, method: 'PUT' });
|
||||
return request<FfmpegSettings>('/api/v1/settings/ffmpeg', { body, method: 'PUT' });
|
||||
}
|
||||
|
||||
export function getPlayoutSettings(): Promise<PlayoutSettings> {
|
||||
return request<PlayoutSettings>('/api/settings/playout');
|
||||
return request<PlayoutSettings>('/api/v1/settings/playout');
|
||||
}
|
||||
|
||||
export function updatePlayoutSettings(body: UpdatePlayoutSettingsRequest): Promise<PlayoutSettings> {
|
||||
return request<PlayoutSettings>('/api/settings/playout', { body, method: 'PUT' });
|
||||
return request<PlayoutSettings>('/api/v1/settings/playout', { body, method: 'PUT' });
|
||||
}
|
||||
|
||||
export function getXmltvSettings(): Promise<XmltvSettings> {
|
||||
return request<XmltvSettings>('/api/settings/xmltv');
|
||||
return request<XmltvSettings>('/api/v1/settings/xmltv');
|
||||
}
|
||||
|
||||
export function updateXmltvSettings(body: UpdateXmltvSettingsRequest): Promise<XmltvSettings> {
|
||||
return request<XmltvSettings>('/api/settings/xmltv', { body, method: 'PUT' });
|
||||
return request<XmltvSettings>('/api/v1/settings/xmltv', { body, method: 'PUT' });
|
||||
}
|
||||
|
||||
export function getScannerSettings(): Promise<ScannerSettings> {
|
||||
return request<ScannerSettings>('/api/settings/scanner');
|
||||
return request<ScannerSettings>('/api/v1/settings/scanner');
|
||||
}
|
||||
|
||||
export function updateScannerSettings(body: UpdateScannerSettingsRequest): Promise<ScannerSettings> {
|
||||
return request<ScannerSettings>('/api/settings/scanner', { body, method: 'PUT' });
|
||||
return request<ScannerSettings>('/api/v1/settings/scanner', { body, method: 'PUT' });
|
||||
}
|
||||
|
||||
export function getLoggingSettings(): Promise<LoggingSettings> {
|
||||
return request<LoggingSettings>('/api/settings/logging');
|
||||
return request<LoggingSettings>('/api/v1/settings/logging');
|
||||
}
|
||||
|
||||
export function updateLoggingSettings(body: UpdateLoggingSettingsRequest): Promise<LoggingSettings> {
|
||||
return request<LoggingSettings>('/api/settings/logging', { body, method: 'PUT' });
|
||||
return request<LoggingSettings>('/api/v1/settings/logging', { body, method: 'PUT' });
|
||||
}
|
||||
|
||||
export function getUiSettings(): Promise<UiSettings> {
|
||||
return request<UiSettings>('/api/settings/ui');
|
||||
return request<UiSettings>('/api/v1/settings/ui');
|
||||
}
|
||||
|
||||
export function updateUiSettings(body: UpdateUiSettingsRequest): Promise<UiSettings> {
|
||||
return request<UiSettings>('/api/settings/ui', { body, method: 'PUT' });
|
||||
return request<UiSettings>('/api/v1/settings/ui', { body, method: 'PUT' });
|
||||
}
|
||||
|
||||
export function getHdhrSettings(): Promise<HdhrSettings> {
|
||||
return request<HdhrSettings>('/api/settings/hdhr');
|
||||
return request<HdhrSettings>('/api/v1/settings/hdhr');
|
||||
}
|
||||
|
||||
export function updateHdhrSettings(body: UpdateHdhrSettingsRequest): Promise<HdhrSettings> {
|
||||
return request<HdhrSettings>('/api/settings/hdhr', { body, method: 'PUT' });
|
||||
return request<HdhrSettings>('/api/v1/settings/hdhr', { body, method: 'PUT' });
|
||||
}
|
||||
|
||||
export function getResolutions(): Promise<Resolution[]> {
|
||||
return request<Resolution[]>('/api/settings/resolutions');
|
||||
return request<Resolution[]>('/api/v1/settings/resolutions');
|
||||
}
|
||||
|
||||
export function createResolution(body: CreateResolutionRequest): Promise<Resolution> {
|
||||
return request<Resolution>('/api/settings/resolutions', { body, method: 'POST' });
|
||||
return request<Resolution>('/api/v1/settings/resolutions', { body, method: 'POST' });
|
||||
}
|
||||
|
||||
export function deleteResolution(id: number): Promise<void> {
|
||||
return request<void>(`/api/settings/resolutions/${id}`, { method: 'DELETE' });
|
||||
return request<void>(`/api/v1/settings/resolutions/${id}`, { method: 'DELETE' });
|
||||
}
|
||||
|
||||
export interface SettingsScreenData {
|
||||
|
||||
@@ -35,14 +35,14 @@ describe('templates api client', () => {
|
||||
it('getTemplateGroups fetches all template groups', async () => {
|
||||
const fetchMock = vi.spyOn(window, 'fetch').mockResolvedValue(jsonResponse([{ id: 1, name: 'Prime', templateCount: 2 }]));
|
||||
await expect(getTemplateGroups()).resolves.toHaveLength(1);
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/templates/groups', expect.objectContaining({ method: 'GET' }));
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/v1/templates/groups', expect.objectContaining({ method: 'GET' }));
|
||||
});
|
||||
|
||||
it('createTemplateGroup POSTs the name', async () => {
|
||||
const fetchMock = vi.spyOn(window, 'fetch').mockResolvedValue(jsonResponse({ id: 3, name: 'Prime', templateCount: 0 }, 201));
|
||||
await createTemplateGroup({ name: 'Prime' });
|
||||
const [url, init] = fetchMock.mock.calls[0];
|
||||
expect(url).toBe('/api/templates/groups');
|
||||
expect(url).toBe('/api/v1/templates/groups');
|
||||
expect(init).toMatchObject({ method: 'POST' });
|
||||
expect(JSON.parse(String(init?.body)).name).toBe('Prime');
|
||||
});
|
||||
@@ -50,26 +50,26 @@ describe('templates api client', () => {
|
||||
it('deleteTemplateGroup DELETEs by id', async () => {
|
||||
const fetchMock = vi.spyOn(window, 'fetch').mockResolvedValue(noContent());
|
||||
await expect(deleteTemplateGroup(3)).resolves.toBeUndefined();
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/templates/groups/3', expect.objectContaining({ method: 'DELETE' }));
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/v1/templates/groups/3', expect.objectContaining({ method: 'DELETE' }));
|
||||
});
|
||||
|
||||
it('getTemplates fetches all templates', async () => {
|
||||
const fetchMock = vi.spyOn(window, 'fetch').mockResolvedValue(jsonResponse([]));
|
||||
await getTemplates();
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/templates', expect.objectContaining({ method: 'GET' }));
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/v1/templates', expect.objectContaining({ method: 'GET' }));
|
||||
});
|
||||
|
||||
it('getTemplate fetches a template by id', async () => {
|
||||
const fetchMock = vi.spyOn(window, 'fetch').mockResolvedValue(jsonResponse({ id: 4 }));
|
||||
await getTemplate(4);
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/templates/4', expect.objectContaining({ method: 'GET' }));
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/v1/templates/4', expect.objectContaining({ method: 'GET' }));
|
||||
});
|
||||
|
||||
it('createTemplate POSTs group id and name', async () => {
|
||||
const fetchMock = vi.spyOn(window, 'fetch').mockResolvedValue(jsonResponse({ id: 8 }, 201));
|
||||
await createTemplate({ templateGroupId: 2, name: 'Weekdays' });
|
||||
const [url, init] = fetchMock.mock.calls[0];
|
||||
expect(url).toBe('/api/templates');
|
||||
expect(url).toBe('/api/v1/templates');
|
||||
expect(init).toMatchObject({ method: 'POST' });
|
||||
expect(JSON.parse(String(init?.body))).toMatchObject({ templateGroupId: 2, name: 'Weekdays' });
|
||||
});
|
||||
@@ -77,20 +77,20 @@ describe('templates api client', () => {
|
||||
it('deleteTemplate DELETEs by id', async () => {
|
||||
const fetchMock = vi.spyOn(window, 'fetch').mockResolvedValue(noContent());
|
||||
await deleteTemplate(4);
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/templates/4', expect.objectContaining({ method: 'DELETE' }));
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/v1/templates/4', expect.objectContaining({ method: 'DELETE' }));
|
||||
});
|
||||
|
||||
it('getTemplateItems fetches items for a template', async () => {
|
||||
const fetchMock = vi.spyOn(window, 'fetch').mockResolvedValue(jsonResponse([]));
|
||||
await getTemplateItems(4);
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/templates/4/items', expect.objectContaining({ method: 'GET' }));
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/v1/templates/4/items', expect.objectContaining({ method: 'GET' }));
|
||||
});
|
||||
|
||||
it('replaceTemplate PUTs the full template body', async () => {
|
||||
const fetchMock = vi.spyOn(window, 'fetch').mockResolvedValue(jsonResponse({ id: 4, items: [] }));
|
||||
await replaceTemplate(4, sampleReplace);
|
||||
const [url, init] = fetchMock.mock.calls[0];
|
||||
expect(url).toBe('/api/templates/4');
|
||||
expect(url).toBe('/api/v1/templates/4');
|
||||
expect(init).toMatchObject({ method: 'PUT' });
|
||||
const body = JSON.parse(String(init?.body));
|
||||
expect(body.name).toBe('Weekdays');
|
||||
@@ -102,7 +102,7 @@ describe('templates api client', () => {
|
||||
const fetchMock = vi.spyOn(window, 'fetch').mockResolvedValue(jsonResponse({ id: 9 }, 201));
|
||||
await copyTemplate(4, { templateGroupId: 3, name: 'Weekdays Copy' });
|
||||
const [url, init] = fetchMock.mock.calls[0];
|
||||
expect(url).toBe('/api/templates/4/copy');
|
||||
expect(url).toBe('/api/v1/templates/4/copy');
|
||||
expect(init).toMatchObject({ method: 'POST' });
|
||||
expect(JSON.parse(String(init?.body))).toMatchObject({ templateGroupId: 3, name: 'Weekdays Copy' });
|
||||
});
|
||||
|
||||
+11
-11
@@ -14,42 +14,42 @@ export type CopyTemplateRequest = components['schemas']['CopyTemplateRequest'];
|
||||
// Template groups
|
||||
|
||||
export function getTemplateGroups(): Promise<TemplateGroup[]> {
|
||||
return request<TemplateGroup[]>('/api/templates/groups');
|
||||
return request<TemplateGroup[]>('/api/v1/templates/groups');
|
||||
}
|
||||
|
||||
export function createTemplateGroup(body: CreateTemplateGroupRequest): Promise<TemplateGroup> {
|
||||
return request<TemplateGroup>('/api/templates/groups', { body, method: 'POST' });
|
||||
return request<TemplateGroup>('/api/v1/templates/groups', { body, method: 'POST' });
|
||||
}
|
||||
|
||||
export function deleteTemplateGroup(id: number): Promise<void> {
|
||||
return request<void>(`/api/templates/groups/${id}`, { method: 'DELETE' });
|
||||
return request<void>(`/api/v1/templates/groups/${id}`, { method: 'DELETE' });
|
||||
}
|
||||
|
||||
// Templates
|
||||
|
||||
export function getTemplates(): Promise<Template[]> {
|
||||
return request<Template[]>('/api/templates');
|
||||
return request<Template[]>('/api/v1/templates');
|
||||
}
|
||||
|
||||
export function getTemplate(id: number): Promise<Template> {
|
||||
return request<Template>(`/api/templates/${id}`);
|
||||
return request<Template>(`/api/v1/templates/${id}`);
|
||||
}
|
||||
|
||||
export function createTemplate(body: CreateTemplateRequest): Promise<Template> {
|
||||
return request<Template>('/api/templates', { body, method: 'POST' });
|
||||
return request<Template>('/api/v1/templates', { body, method: 'POST' });
|
||||
}
|
||||
|
||||
export function deleteTemplate(id: number): Promise<void> {
|
||||
return request<void>(`/api/templates/${id}`, { method: 'DELETE' });
|
||||
return request<void>(`/api/v1/templates/${id}`, { method: 'DELETE' });
|
||||
}
|
||||
|
||||
export function getTemplateItems(id: number): Promise<TemplateItem[]> {
|
||||
return request<TemplateItem[]>(`/api/templates/${id}/items`);
|
||||
return request<TemplateItem[]>(`/api/v1/templates/${id}/items`);
|
||||
}
|
||||
|
||||
/** Load template items together with the template's concurrency ETag (issue #253). */
|
||||
export function getTemplateItemsWithMeta(id: number): Promise<ResponseWithMeta<TemplateItem[]>> {
|
||||
return requestWithMeta<TemplateItem[]>(`/api/templates/${id}/items`);
|
||||
return requestWithMeta<TemplateItem[]>(`/api/v1/templates/${id}/items`);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -61,7 +61,7 @@ export function replaceTemplate(
|
||||
body: ReplaceTemplateRequest,
|
||||
ifMatch?: string | null
|
||||
): Promise<ResponseWithMeta<TemplateWithItems>> {
|
||||
return requestWithMeta<TemplateWithItems>(`/api/templates/${id}`, {
|
||||
return requestWithMeta<TemplateWithItems>(`/api/v1/templates/${id}`, {
|
||||
body,
|
||||
method: 'PUT',
|
||||
headers: ifMatch ? { 'If-Match': ifMatch } : undefined
|
||||
@@ -69,7 +69,7 @@ export function replaceTemplate(
|
||||
}
|
||||
|
||||
export function copyTemplate(id: number, body: CopyTemplateRequest): Promise<Template> {
|
||||
return request<Template>(`/api/templates/${id}/copy`, { body, method: 'POST' });
|
||||
return request<Template>(`/api/v1/templates/${id}/copy`, { body, method: 'POST' });
|
||||
}
|
||||
|
||||
export function messageFromTemplateError(error: unknown, fallback = 'Unable to load templates'): string {
|
||||
|
||||
@@ -33,7 +33,7 @@ describe('trakt api client', () => {
|
||||
|
||||
await getTraktLists();
|
||||
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/trakt/lists', expect.objectContaining({ method: 'GET' }));
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/v1/trakt/lists', expect.objectContaining({ method: 'GET' }));
|
||||
});
|
||||
|
||||
it('getTraktLists forwards pageNum/pageSize as query params', async () => {
|
||||
@@ -44,7 +44,7 @@ describe('trakt api client', () => {
|
||||
await getTraktLists({ pageNum: 2, pageSize: 25 });
|
||||
|
||||
const url = new URL(String(fetchMock.mock.calls[0][0]), 'http://localhost');
|
||||
expect(url.pathname).toBe('/api/trakt/lists');
|
||||
expect(url.pathname).toBe('/api/v1/trakt/lists');
|
||||
expect(url.searchParams.get('pageNum')).toBe('2');
|
||||
expect(url.searchParams.get('pageSize')).toBe('25');
|
||||
});
|
||||
@@ -64,7 +64,7 @@ describe('trakt api client', () => {
|
||||
);
|
||||
|
||||
await expect(getTraktListById(1)).resolves.toMatchObject({ id: 1, slug: 'my-list' });
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/trakt/lists/1', expect.objectContaining({ method: 'GET' }));
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/v1/trakt/lists/1', expect.objectContaining({ method: 'GET' }));
|
||||
});
|
||||
|
||||
it('addTraktList POSTs the url and resolves on 202', async () => {
|
||||
@@ -73,7 +73,7 @@ describe('trakt api client', () => {
|
||||
await expect(addTraktList('https://trakt.tv/users/someuser/lists/some-list')).resolves.toBeUndefined();
|
||||
|
||||
const [url, init] = fetchMock.mock.calls[0];
|
||||
expect(url).toBe('/api/trakt/lists');
|
||||
expect(url).toBe('/api/v1/trakt/lists');
|
||||
expect(init).toMatchObject({ method: 'POST' });
|
||||
expect(JSON.parse(String(init?.body))).toEqual({ url: 'https://trakt.tv/users/someuser/lists/some-list' });
|
||||
});
|
||||
@@ -88,14 +88,14 @@ describe('trakt api client', () => {
|
||||
const fetchMock = vi.spyOn(window, 'fetch').mockResolvedValue(accepted());
|
||||
|
||||
await expect(matchTraktList(5)).resolves.toBeUndefined();
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/trakt/lists/5/match', expect.objectContaining({ method: 'POST' }));
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/v1/trakt/lists/5/match', expect.objectContaining({ method: 'POST' }));
|
||||
});
|
||||
|
||||
it('deleteTraktList issues a DELETE and resolves on 202', async () => {
|
||||
const fetchMock = vi.spyOn(window, 'fetch').mockResolvedValue(accepted());
|
||||
|
||||
await expect(deleteTraktList(9)).resolves.toBeUndefined();
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/trakt/lists/9', expect.objectContaining({ method: 'DELETE' }));
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/v1/trakt/lists/9', expect.objectContaining({ method: 'DELETE' }));
|
||||
});
|
||||
|
||||
it('updateTraktList PUTs autoRefresh/generatePlaylist', async () => {
|
||||
@@ -115,7 +115,7 @@ describe('trakt api client', () => {
|
||||
await updateTraktList(3, { autoRefresh: true, generatePlaylist: true });
|
||||
|
||||
const [url, init] = fetchMock.mock.calls[0];
|
||||
expect(url).toBe('/api/trakt/lists/3');
|
||||
expect(url).toBe('/api/v1/trakt/lists/3');
|
||||
expect(init).toMatchObject({ method: 'PUT' });
|
||||
expect(JSON.parse(String(init?.body))).toEqual({ autoRefresh: true, generatePlaylist: true });
|
||||
});
|
||||
@@ -124,6 +124,6 @@ describe('trakt api client', () => {
|
||||
const fetchMock = vi.spyOn(window, 'fetch').mockResolvedValue(jsonResponse({ busy: true }));
|
||||
|
||||
await expect(getTraktStatus()).resolves.toEqual({ busy: true });
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/trakt/status', expect.objectContaining({ method: 'GET' }));
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/v1/trakt/status', expect.objectContaining({ method: 'GET' }));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -25,36 +25,36 @@ export function getTraktLists(params: GetTraktListsParams = {}): Promise<PagedTr
|
||||
|
||||
const queryString = searchParams.toString();
|
||||
|
||||
return request<PagedTraktLists>(`/api/trakt/lists${queryString ? `?${queryString}` : ''}`);
|
||||
return request<PagedTraktLists>(`/api/v1/trakt/lists${queryString ? `?${queryString}` : ''}`);
|
||||
}
|
||||
|
||||
export function getTraktListById(id: number): Promise<TraktList> {
|
||||
return request<TraktList>(`/api/trakt/lists/${id}`);
|
||||
return request<TraktList>(`/api/v1/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' });
|
||||
return request<void>('/api/v1/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' });
|
||||
return request<void>(`/api/v1/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' });
|
||||
return request<void>(`/api/v1/trakt/lists/${id}`, { method: 'DELETE' });
|
||||
}
|
||||
|
||||
export function updateTraktList(id: number, body: UpdateTraktListRequest): Promise<TraktList> {
|
||||
return request<TraktList>(`/api/trakt/lists/${id}`, { body, method: 'PUT' });
|
||||
return request<TraktList>(`/api/v1/trakt/lists/${id}`, { body, method: 'PUT' });
|
||||
}
|
||||
|
||||
export function getTraktStatus(): Promise<TraktStatus> {
|
||||
return request<TraktStatus>('/api/trakt/status');
|
||||
return request<TraktStatus>('/api/v1/trakt/status');
|
||||
}
|
||||
|
||||
export function messageFromTraktError(error: unknown, fallback = 'Unable to load Trakt lists'): string {
|
||||
|
||||
@@ -39,7 +39,7 @@ describe('getTroubleshootingInfo', () => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('GETs /api/troubleshoot/info and returns the info payload', async () => {
|
||||
it('GETs /api/v1/troubleshoot/info and returns the info payload', async () => {
|
||||
const fetchSpy = vi.spyOn(window, 'fetch').mockResolvedValue(
|
||||
new Response(JSON.stringify(sampleInfo), {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
@@ -50,7 +50,7 @@ describe('getTroubleshootingInfo', () => {
|
||||
await expect(getTroubleshootingInfo()).resolves.toMatchObject({ nvidiaCapabilities: 'nvidia output' });
|
||||
|
||||
const [url, init] = fetchSpy.mock.calls[0];
|
||||
expect(url).toBe('/api/troubleshoot/info');
|
||||
expect(url).toBe('/api/v1/troubleshoot/info');
|
||||
expect((init?.method ?? 'GET').toUpperCase()).toBe('GET');
|
||||
});
|
||||
|
||||
@@ -74,7 +74,7 @@ describe('validateSequentialSchedule', () => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('POSTs /api/troubleshoot/validate-schedule with the yaml and isImport body', async () => {
|
||||
it('POSTs /api/v1/troubleshoot/validate-schedule with the yaml and isImport body', async () => {
|
||||
const fetchSpy = vi.spyOn(window, 'fetch').mockResolvedValue(
|
||||
new Response(JSON.stringify(sampleResult), {
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
@@ -85,7 +85,7 @@ describe('validateSequentialSchedule', () => {
|
||||
await expect(validateSequentialSchedule('content: []', true)).resolves.toMatchObject({ isValid: true });
|
||||
|
||||
const [url, init] = fetchSpy.mock.calls[0];
|
||||
expect(url).toBe('/api/troubleshoot/validate-schedule');
|
||||
expect(url).toBe('/api/v1/troubleshoot/validate-schedule');
|
||||
expect((init?.method ?? 'GET').toUpperCase()).toBe('POST');
|
||||
expect(JSON.parse(String(init?.body))).toEqual({ yaml: 'content: []', isImport: true });
|
||||
});
|
||||
@@ -112,7 +112,7 @@ describe('troubleshooting playback wrappers', () => {
|
||||
const fetchSpy = vi.spyOn(window, 'fetch').mockResolvedValue(jsonResponse(['a', 'b']));
|
||||
|
||||
await expect(getTroubleshootingStreamSelectors()).resolves.toEqual(['a', 'b']);
|
||||
expect(fetchSpy.mock.calls[0][0]).toBe('/api/troubleshoot/playback/stream-selectors');
|
||||
expect(fetchSpy.mock.calls[0][0]).toBe('/api/v1/troubleshoot/playback/stream-selectors');
|
||||
});
|
||||
|
||||
it('GETs the subtitle list for a media item id', async () => {
|
||||
@@ -121,7 +121,7 @@ describe('troubleshooting playback wrappers', () => {
|
||||
.mockResolvedValue(jsonResponse([{ id: 3, language: 'eng', title: 'English', codec: 'subrip' }]));
|
||||
|
||||
await expect(getTroubleshootingSubtitles(42)).resolves.toMatchObject([{ id: 3 }]);
|
||||
expect(fetchSpy.mock.calls[0][0]).toBe('/api/troubleshoot/playback/subtitles/42');
|
||||
expect(fetchSpy.mock.calls[0][0]).toBe('/api/v1/troubleshoot/playback/subtitles/42');
|
||||
});
|
||||
|
||||
it('GETs the playback status', async () => {
|
||||
@@ -130,7 +130,7 @@ describe('troubleshooting playback wrappers', () => {
|
||||
.mockResolvedValue(jsonResponse({ state: 'completed', exitCode: 0, speed: 1.2, logs: 'ok' }));
|
||||
|
||||
await expect(getTroubleshootingPlaybackStatus()).resolves.toMatchObject({ state: 'completed', speed: 1.2 });
|
||||
expect(fetchSpy.mock.calls[0][0]).toBe('/api/troubleshoot/playback/status');
|
||||
expect(fetchSpy.mock.calls[0][0]).toBe('/api/v1/troubleshoot/playback/status');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -139,7 +139,7 @@ describe('startTroubleshootingPlayback', () => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('POSTs /api/troubleshoot/playback/start with the mapped body and a CSRF header, returning the url', async () => {
|
||||
it('POSTs /api/v1/troubleshoot/playback/start with the mapped body and a CSRF header, returning the url', async () => {
|
||||
const fetchSpy = vi.spyOn(window, 'fetch').mockResolvedValue(
|
||||
jsonResponse({ url: '/iptv/session/.troubleshooting/live.m3u8' })
|
||||
);
|
||||
@@ -164,7 +164,7 @@ describe('startTroubleshootingPlayback', () => {
|
||||
});
|
||||
|
||||
const [url, init] = fetchSpy.mock.calls[0];
|
||||
expect(url).toBe('/api/troubleshoot/playback/start');
|
||||
expect(url).toBe('/api/v1/troubleshoot/playback/start');
|
||||
expect((init?.method ?? 'GET').toUpperCase()).toBe('POST');
|
||||
expect(headerValue(init, 'X-Csrf')).toBe('1');
|
||||
expect(JSON.parse(String(init?.body))).toEqual(body);
|
||||
@@ -233,7 +233,7 @@ describe('troubleshooting download helpers', () => {
|
||||
await downloadTroubleshootingArchive();
|
||||
|
||||
const [url, init] = fetchSpy.mock.calls[0];
|
||||
expect(url).toBe('/api/troubleshoot/playback/archive');
|
||||
expect(url).toBe('/api/v1/troubleshoot/playback/archive');
|
||||
expect((init?.method ?? 'GET').toUpperCase()).toBe('POST');
|
||||
expect(headerValue(init, 'X-Csrf')).toBe('1');
|
||||
expect(createObjectURL).toHaveBeenCalledTimes(1);
|
||||
@@ -248,7 +248,7 @@ describe('troubleshooting download helpers', () => {
|
||||
await downloadTroubleshootingMediaSample(42);
|
||||
|
||||
const [url, init] = fetchSpy.mock.calls[0];
|
||||
expect(url).toBe('/api/troubleshoot/playback/sample/42');
|
||||
expect(url).toBe('/api/v1/troubleshoot/playback/sample/42');
|
||||
expect((init?.method ?? 'GET').toUpperCase()).toBe('POST');
|
||||
expect(headerValue(init, 'X-Csrf')).toBe('1');
|
||||
});
|
||||
|
||||
@@ -10,25 +10,25 @@ export type StartTroubleshootingPlaybackBody = components['schemas']['StartTroub
|
||||
export type TroubleshootingPlaybackStarted = components['schemas']['TroubleshootingPlaybackStartedResponseModel'];
|
||||
|
||||
export function getTroubleshootingInfo(): Promise<TroubleshootingInfo> {
|
||||
return request<TroubleshootingInfo>('/api/troubleshoot/info');
|
||||
return request<TroubleshootingInfo>('/api/v1/troubleshoot/info');
|
||||
}
|
||||
|
||||
// The stream selectors available for a troubleshooting playback (channel "smart" selectors).
|
||||
export function getTroubleshootingStreamSelectors(): Promise<string[]> {
|
||||
return request<string[]>('/api/troubleshoot/playback/stream-selectors');
|
||||
return request<string[]>('/api/v1/troubleshoot/playback/stream-selectors');
|
||||
}
|
||||
|
||||
// Subtitle streams that can be burned in for a media item. Each item's `id` is the value to pass
|
||||
// back as the playback.m3u8 endpoint's `subtitleId` query param.
|
||||
export function getTroubleshootingSubtitles(mediaItemId: number): Promise<TroubleshootingSubtitle[]> {
|
||||
return request<TroubleshootingSubtitle[]>(`/api/troubleshoot/playback/subtitles/${mediaItemId}`);
|
||||
return request<TroubleshootingSubtitle[]>(`/api/v1/troubleshoot/playback/subtitles/${mediaItemId}`);
|
||||
}
|
||||
|
||||
// Status of the current or last troubleshooting playback session. `state` is idle|running|
|
||||
// completed|failed; exitCode/speed are null until completed/failed; logs is the log tail (null
|
||||
// until written).
|
||||
export function getTroubleshootingPlaybackStatus(): Promise<TroubleshootingPlaybackStatus> {
|
||||
return request<TroubleshootingPlaybackStatus>('/api/troubleshoot/playback/status');
|
||||
return request<TroubleshootingPlaybackStatus>('/api/v1/troubleshoot/playback/status');
|
||||
}
|
||||
|
||||
// Start a troubleshooting playback session (#295 — was a GET on `playback.m3u8` that started the
|
||||
@@ -38,7 +38,7 @@ export function getTroubleshootingPlaybackStatus(): Promise<TroubleshootingPlayb
|
||||
export function startTroubleshootingPlayback(
|
||||
body: StartTroubleshootingPlaybackBody
|
||||
): Promise<TroubleshootingPlaybackStarted> {
|
||||
return request<TroubleshootingPlaybackStarted>('/api/troubleshoot/playback/start', {
|
||||
return request<TroubleshootingPlaybackStarted>('/api/v1/troubleshoot/playback/start', {
|
||||
body,
|
||||
method: 'POST'
|
||||
});
|
||||
@@ -49,11 +49,11 @@ export function startTroubleshootingPlayback(
|
||||
// #295), read the blob, and trigger a browser download. Same-origin fetch can read Content-Disposition
|
||||
// so we use the server-provided filename when present.
|
||||
export function downloadTroubleshootingArchive(): Promise<void> {
|
||||
return downloadViaPost('/api/troubleshoot/playback/archive', 'ersatztv-troubleshooting.zip');
|
||||
return downloadViaPost('/api/v1/troubleshoot/playback/archive', 'ersatztv-troubleshooting.zip');
|
||||
}
|
||||
|
||||
export function downloadTroubleshootingMediaSample(id: number): Promise<void> {
|
||||
return downloadViaPost(`/api/troubleshoot/playback/sample/${id}`, 'ersatztv-media-sample.zip');
|
||||
return downloadViaPost(`/api/v1/troubleshoot/playback/sample/${id}`, 'ersatztv-media-sample.zip');
|
||||
}
|
||||
|
||||
async function downloadViaPost(path: string, fallbackName: string): Promise<void> {
|
||||
@@ -120,7 +120,7 @@ function filenameFromContentDisposition(header: null | string, fallback: string)
|
||||
}
|
||||
|
||||
export function validateSequentialSchedule(yaml: string, isImport: boolean): Promise<ValidateScheduleResult> {
|
||||
return request<ValidateScheduleResult>('/api/troubleshoot/validate-schedule', {
|
||||
return request<ValidateScheduleResult>('/api/v1/troubleshoot/validate-schedule', {
|
||||
body: { yaml, isImport },
|
||||
method: 'POST'
|
||||
});
|
||||
|
||||
@@ -48,7 +48,7 @@ describe('watermarks api client', () => {
|
||||
const fetchMock = vi.spyOn(window, 'fetch').mockResolvedValue(jsonResponse({ id: 4, name: 'Corner Logo' }));
|
||||
|
||||
await expect(getWatermark(4)).resolves.toMatchObject({ id: 4 });
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/watermarks/4', expect.objectContaining({ method: 'GET' }));
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/v1/watermarks/4', expect.objectContaining({ method: 'GET' }));
|
||||
});
|
||||
|
||||
it('createWatermark POSTs the request body', async () => {
|
||||
@@ -57,7 +57,7 @@ describe('watermarks api client', () => {
|
||||
await expect(createWatermark(sampleRequest)).resolves.toMatchObject({ id: 7 });
|
||||
|
||||
const [url, init] = fetchMock.mock.calls[0];
|
||||
expect(url).toBe('/api/watermarks');
|
||||
expect(url).toBe('/api/v1/watermarks');
|
||||
expect(init).toMatchObject({ method: 'POST' });
|
||||
expect(JSON.parse(String(init?.body)).mode).toBe('Permanent');
|
||||
});
|
||||
@@ -68,7 +68,7 @@ describe('watermarks api client', () => {
|
||||
await updateWatermark(3, sampleRequest);
|
||||
|
||||
const [url, init] = fetchMock.mock.calls[0];
|
||||
expect(url).toBe('/api/watermarks/3');
|
||||
expect(url).toBe('/api/v1/watermarks/3');
|
||||
expect(init).toMatchObject({ method: 'PUT' });
|
||||
});
|
||||
|
||||
@@ -76,7 +76,7 @@ describe('watermarks api client', () => {
|
||||
const fetchMock = vi.spyOn(window, 'fetch').mockResolvedValue(noContent());
|
||||
|
||||
await expect(deleteWatermark(9)).resolves.toBeUndefined();
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/watermarks/9', expect.objectContaining({ method: 'DELETE' }));
|
||||
expect(fetchMock).toHaveBeenCalledWith('/api/v1/watermarks/9', expect.objectContaining({ method: 'DELETE' }));
|
||||
});
|
||||
|
||||
it('watermarkImageUrl builds a path-only URL (no reflected ?contentType=)', () => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ApiError, request } from './client';
|
||||
import type { components } from './generated/v1';
|
||||
|
||||
// The list endpoint (GET /api/watermarks) is picker-grade (id + name) and is already
|
||||
// The list endpoint (GET /api/v1/watermarks) is picker-grade (id + name) and is already
|
||||
// provided by pickers.ts as getWatermarks(). This module adds the by-id read (full model)
|
||||
// and the mutations. Image upload is handled by artwork.ts (uploadArtwork(file,'watermark')).
|
||||
|
||||
@@ -10,19 +10,19 @@ export type CreateWatermarkRequest = components['schemas']['CreateWatermarkReque
|
||||
export type UpdateWatermarkRequest = components['schemas']['UpdateWatermarkRequest'];
|
||||
|
||||
export function getWatermark(id: number): Promise<WatermarkDetail> {
|
||||
return request<WatermarkDetail>(`/api/watermarks/${id}`);
|
||||
return request<WatermarkDetail>(`/api/v1/watermarks/${id}`);
|
||||
}
|
||||
|
||||
export function createWatermark(body: CreateWatermarkRequest): Promise<WatermarkDetail> {
|
||||
return request<WatermarkDetail>('/api/watermarks', { body, method: 'POST' });
|
||||
return request<WatermarkDetail>('/api/v1/watermarks', { body, method: 'POST' });
|
||||
}
|
||||
|
||||
export function updateWatermark(id: number, body: UpdateWatermarkRequest): Promise<WatermarkDetail> {
|
||||
return request<WatermarkDetail>(`/api/watermarks/${id}`, { body, method: 'PUT' });
|
||||
return request<WatermarkDetail>(`/api/v1/watermarks/${id}`, { body, method: 'PUT' });
|
||||
}
|
||||
|
||||
export function deleteWatermark(id: number): Promise<void> {
|
||||
return request<void>(`/api/watermarks/${id}`, { method: 'DELETE' });
|
||||
return request<void>(`/api/v1/watermarks/${id}`, { method: 'DELETE' });
|
||||
}
|
||||
|
||||
// Preview URL for a custom watermark image. The serve route sniffs the content type from the
|
||||
|
||||
Reference in New Issue
Block a user