import { apiRequest, getAccessToken, HttpError } from '@/shared/api/client' import type { CreatedIdResponse, ImportManualInboxResultDto, ManualImportItem, ManualInboxListDto, MediaAssetDto, MediaAssetStatus, MediaStatsDto, PagedList, } from '@/shared/api/types' export type ListMediaParams = { page: number pageSize: number statuses?: MediaAssetStatus[] search?: string sort?: string desc?: boolean } export function listMedia(params: ListMediaParams) { const query = new URLSearchParams({ page: String(params.page), pageSize: String(params.pageSize), }) for (const status of params.statuses ?? []) query.append('status', status) if (params.search) query.set('search', params.search) if (params.sort) query.set('sort', params.sort) if (params.desc) query.set('desc', 'true') return apiRequest>(`/admin/media?${query.toString()}`) } export function getMediaStats() { return apiRequest('/admin/media/stats') } /** * Дозагружает ВСЕ страницы медиа (для пикеров с клиентской фильтрацией/сортировкой — кандидаты серий, * пул рекламы), чтобы ничего не терялось молча за фиксированным pageSize. Есть предохранитель `cap`: * если элементов больше — возвращаем `truncated: true`, и UI показывает предупреждение (а не делает вид, * что список полон). */ export async function listAllMedia( params: Omit & { cap?: number }, ): Promise<{ items: MediaAssetDto[]; total: number; truncated: boolean }> { const pageSize = 200 const cap = params.cap ?? 5000 const items: MediaAssetDto[] = [] let total = 0 for (let page = 1; ; page++) { const res = await listMedia({ page, pageSize, statuses: params.statuses, search: params.search, }) total = res.total items.push(...res.items) if (res.items.length === 0 || items.length >= total || items.length >= cap) break } return { items, total, truncated: items.length < total } } /** Что лежит в ручном inbox (manual/) и ждёт разбора. */ export function listManualInbox() { return apiRequest('/admin/media/manual') } /** * Забирает файлы из manual/ в шоу: файлы уходят из каталога, как и из обычного inbox, а спутники * (субтитры, nfo) удаляются. Номера серий передаются явно — сохранится ровно то, что было показано. */ export function importManualInbox(items: ManualImportItem[], showId: string) { return apiRequest('/admin/media/manual/import', { method: 'POST', body: { items, showId }, }) } export function deleteMedia(id: string) { return apiRequest(`/admin/media/${id}`, { method: 'DELETE' }) } /** * Потоковая загрузка файла (сырое тело + fileName в query). Через XHR ради индикатора прогресса. */ export function uploadMedia( file: File, onProgress?: (percent: number) => void, signal?: AbortSignal, ): Promise { return new Promise((resolve, reject) => { if (signal?.aborted) { reject(new DOMException('Aborted', 'AbortError')) return } const xhr = new XMLHttpRequest() const query = new URLSearchParams({ fileName: file.name }) xhr.open('POST', `/api/admin/media?${query.toString()}`) const token = getAccessToken() if (token) xhr.setRequestHeader('Authorization', `Bearer ${token}`) signal?.addEventListener('abort', () => xhr.abort()) xhr.onabort = () => reject(new DOMException('Aborted', 'AbortError')) xhr.upload.onprogress = (event) => { if (event.lengthComputable && onProgress) onProgress(Math.round((event.loaded / event.total) * 100)) } xhr.onload = () => { if (xhr.status >= 200 && xhr.status < 300) { resolve(JSON.parse(xhr.responseText) as CreatedIdResponse) } else { let detail = `HTTP ${xhr.status}` try { const problem = JSON.parse(xhr.responseText) as { detail?: string; title?: string } detail = problem.detail ?? problem.title ?? detail } catch { /* пусто */ } reject(new HttpError({ detail }, xhr.status)) } } xhr.onerror = () => reject(new HttpError({ title: 'Network error' }, 0)) xhr.send(file) }) }