Files
ersatztv/web/src/api/mediaSources.ts
T
timothyandClaude Opus 4.8 ef2bd65c27
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
feat(api): #286 — mount the whole /api surface at /api/v1
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>
2026-07-13 00:30:20 +02:00

139 lines
6.3 KiB
TypeScript

import { ApiError, request } from './client';
import type { components } from './generated/v1';
import type { RemoteFamily } from '../mediaSources/familyMeta';
// --- Response DTOs (re-exported from the generated spec) ---
export type LocalLibrary = components['schemas']['LocalLibraryResponseModel'];
export type LocalLibraryPath = components['schemas']['LocalLibraryPathResponseModel'];
export type LocalLibraryDetail = components['schemas']['LocalLibraryDetailResponseModel'];
export type LocalPathCheckResult = components['schemas']['LocalPathCheckResponseModel'];
export type RemoteMediaSourceItem = components['schemas']['RemoteMediaSourceItemResponseModel'];
export type RemoteMediaSourceState = components['schemas']['RemoteMediaSourceStateResponseModel'];
export type RemoteLibrary = components['schemas']['RemoteLibraryResponseModel'];
export type PathReplacement = components['schemas']['PathReplacementResponseModel'];
export type RemoteConnection = components['schemas']['RemoteConnectionResponseModel'];
export type PlexPinFlow = components['schemas']['PlexPinFlowResponseModel'];
// --- Request DTOs (re-exported from the generated spec) ---
export type CreateLocalLibraryRequest = components['schemas']['CreateLocalLibraryRequest'];
export type UpdateLocalLibraryRequest = components['schemas']['UpdateLocalLibraryRequest'];
export type UpdateLocalLibraryPathRequest = components['schemas']['UpdateLocalLibraryPathRequest'];
export type MoveLocalLibraryPathRequest = components['schemas']['MoveLocalLibraryPathRequest'];
export type LocalPathCheckRequest = components['schemas']['LocalPathCheckRequest'];
export type ReplaceRemoteLibraryPreferencesRequest = components['schemas']['ReplaceRemoteLibraryPreferencesRequest'];
export type RemoteLibraryPreferenceRequest = components['schemas']['RemoteLibraryPreferenceRequest'];
export type ReplacePathReplacementsRequest = components['schemas']['ReplacePathReplacementsRequest'];
export type PathReplacementItemRequest = components['schemas']['PathReplacementItemRequest'];
export type SaveRemoteConnectionRequest = components['schemas']['SaveRemoteConnectionRequest'];
export type { RemoteFamily } from '../mediaSources/familyMeta';
// --- Local libraries (/api/v1/libraries/local) ---
export function getLocalLibraries(): Promise<LocalLibrary[]> {
return request<LocalLibrary[]>('/api/v1/libraries/local');
}
export function getLocalLibrary(id: number): Promise<LocalLibraryDetail> {
return request<LocalLibraryDetail>(`/api/v1/libraries/local/${id}`);
}
export function createLocalLibrary(body: CreateLocalLibraryRequest): Promise<LocalLibrary> {
return request<LocalLibrary>('/api/v1/libraries/local', { body, method: 'POST' });
}
export function updateLocalLibrary(id: number, body: UpdateLocalLibraryRequest): Promise<LocalLibraryDetail> {
return request<LocalLibraryDetail>(`/api/v1/libraries/local/${id}`, { body, method: 'PUT' });
}
export function deleteLocalLibrary(id: number): Promise<void> {
return request<void>(`/api/v1/libraries/local/${id}`, { method: 'DELETE' });
}
export function moveLocalLibraryPath(pathId: number, body: MoveLocalLibraryPathRequest): Promise<void> {
return request<void>(`/api/v1/libraries/local/paths/${pathId}/move`, { body, method: 'POST' });
}
export function checkLocalPathExists(body: LocalPathCheckRequest): Promise<LocalPathCheckResult> {
return request<LocalPathCheckResult>('/api/v1/libraries/local/path-exists', { body, method: 'POST' });
}
// --- 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/v1/media-sources/plex/pin-flow', { method: 'POST' });
}
export function signOutOfPlex(): Promise<void> {
return request<void>('/api/v1/media-sources/plex/sign-out', { method: 'POST' });
}
// --- 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/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/v1/media-sources/${family}/connection`);
}
export function saveRemoteConnection(
family: 'jellyfin' | 'emby',
body: SaveRemoteConnectionRequest
): Promise<RemoteConnection> {
return request<RemoteConnection>(`/api/v1/media-sources/${family}/connection`, { body, method: 'PUT' });
}
export function disconnectRemote(family: 'jellyfin' | 'emby'): Promise<void> {
return request<void>(`/api/v1/media-sources/${family}/disconnect`, { method: 'POST' });
}
export function getRemoteLibraries(family: RemoteFamily, sourceId: number): Promise<RemoteLibrary[]> {
return request<RemoteLibrary[]>(`/api/v1/media-sources/${family}/${sourceId}/libraries`);
}
export function replaceRemoteLibraryPreferences(
family: RemoteFamily,
sourceId: number,
body: ReplaceRemoteLibraryPreferencesRequest
): Promise<RemoteLibrary[]> {
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/v1/media-sources/${family}/${sourceId}/path-replacements`);
}
export function replacePathReplacements(
family: RemoteFamily,
sourceId: number,
body: ReplacePathReplacementsRequest
): Promise<PathReplacement[]> {
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/v1/media-sources/${family}/${sourceId}/refresh-libraries`, { method: 'POST' });
}
export function messageFromMediaSourcesError(error: unknown, fallback = 'Unable to load media sources'): string {
if (error instanceof ApiError) {
return error.detail ?? error.message;
}
if (error instanceof Error) {
return error.message;
}
return fallback;
}