131 lines
4.7 KiB
TypeScript
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)
|
|
})
|
|
}
|