Add metadata management for shows: implement metadata retrieval, application, and updates in the API and UI. Enhance Show and ShowDto models to include metadata fields, and update the database schema accordingly. Introduce new endpoints for metadata operations and integrate metadata display in the ShowDetail component.

This commit is contained in:
Leonid Pershin
2026-07-25 09:07:55 +03:00
parent ba023bc416
commit 0d2dee815e
42 changed files with 2271 additions and 4 deletions
+65 -2
View File
@@ -1,5 +1,11 @@
import { apiRequest } from '@/shared/api/client'
import type { CreatedIdResponse, ShowDto, ShowKind, ShowSummaryDto } from '@/shared/api/types'
import { apiRequest, getAccessToken, HttpError } from '@/shared/api/client'
import type {
CreatedIdResponse,
MetadataCandidate,
ShowDto,
ShowKind,
ShowSummaryDto,
} from '@/shared/api/types'
export function listShows() {
return apiRequest<ShowSummaryDto[]>('/admin/shows')
@@ -27,3 +33,60 @@ export function addEpisode(showId: string, mediaAssetId: string) {
export function removeEpisode(showId: string, episodeId: string) {
return apiRequest<void>(`/admin/shows/${showId}/episodes/${episodeId}`, { method: 'DELETE' })
}
// ── Метаданные ────────────────────────────────────────────────────────────
export function getMetadataProviders() {
return apiRequest<string[]>('/admin/metadata/providers')
}
export function searchMetadata(provider: string, query: string) {
const q = new URLSearchParams({ provider, query })
return apiRequest<MetadataCandidate[]>(`/admin/metadata/search?${q.toString()}`)
}
export function applyMetadata(showId: string, provider: string, externalId: string) {
return apiRequest<void>(`/admin/metadata/shows/${showId}/apply`, {
method: 'POST',
body: { provider, externalId },
})
}
export function updateMetadata(showId: string, body: { description: string | null; year: number | null }) {
return apiRequest<void>(`/admin/metadata/shows/${showId}`, { method: 'PUT', body })
}
export function clearMetadata(showId: string) {
return apiRequest<void>(`/admin/metadata/shows/${showId}`, { method: 'DELETE' })
}
/** Ссылка на локальный постер шоу (публичный эндпоинт; cache-buster — по флагу наличия). */
export function showPosterUrl(showId: string, bust?: string) {
return `/api/metadata/shows/${showId}/poster${bust ? `?v=${encodeURIComponent(bust)}` : ''}`
}
/** Загрузка постера вручную (сырое тело, имя в query — как uploadMedia). */
export function uploadPoster(showId: string, file: File): Promise<void> {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest()
const q = new URLSearchParams({ fileName: file.name })
xhr.open('PUT', `/api/admin/metadata/shows/${showId}/poster?${q.toString()}`)
const token = getAccessToken()
if (token) xhr.setRequestHeader('Authorization', `Bearer ${token}`)
xhr.onload = () => {
if (xhr.status >= 200 && xhr.status < 300) resolve()
else {
let detail = `HTTP ${xhr.status}`
try {
const p = JSON.parse(xhr.responseText) as { detail?: string; title?: string }
detail = p.detail ?? p.title ?? detail
} catch {
/* пусто */
}
reject(new HttpError({ detail }, xhr.status))
}
}
xhr.onerror = () => reject(new HttpError({ title: 'Network error' }, 0))
xhr.send(file)
})
}