Files
TeleWave/frontend/src/features/admin/media/api.ts
T

131 lines
4.7 KiB
TypeScript

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<PagedList<MediaAssetDto>>(`/admin/media?${query.toString()}`)
}
export function getMediaStats() {
return apiRequest<MediaStatsDto>('/admin/media/stats')
}
/**
* Дозагружает ВСЕ страницы медиа (для пикеров с клиентской фильтрацией/сортировкой — кандидаты серий,
* пул рекламы), чтобы ничего не терялось молча за фиксированным pageSize. Есть предохранитель `cap`:
* если элементов больше — возвращаем `truncated: true`, и UI показывает предупреждение (а не делает вид,
* что список полон).
*/
export async function listAllMedia(
params: Omit<ListMediaParams, 'page' | 'pageSize'> & { 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<ManualInboxListDto>('/admin/media/manual')
}
/**
* Забирает файлы из manual/ в шоу: файлы уходят из каталога, как и из обычного inbox, а спутники
* (субтитры, nfo) удаляются. Номера серий передаются явно — сохранится ровно то, что было показано.
*/
export function importManualInbox(items: ManualImportItem[], showId: string) {
return apiRequest<ImportManualInboxResultDto>('/admin/media/manual/import', {
method: 'POST',
body: { items, showId },
})
}
export function deleteMedia(id: string) {
return apiRequest<void>(`/admin/media/${id}`, { method: 'DELETE' })
}
/**
* Потоковая загрузка файла (сырое тело + fileName в query). Через XHR ради индикатора прогресса.
*/
export function uploadMedia(
file: File,
onProgress?: (percent: number) => void,
signal?: AbortSignal,
): Promise<CreatedIdResponse> {
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)
})
}