Add TV bumpers functionality: introduce configuration options for bumpers in .env.example and appsettings.json, enhance ChannelEndpoints to manage jingles and bumper assets, and update Channel and ScheduleEntry models to support bumper logic. Implement validation for bumper settings and integrate bumper handling in scheduling logic.

This commit is contained in:
Leonid Pershin
2026-07-25 00:45:22 +03:00
parent 622bf1e440
commit 0cfc72166a
60 changed files with 5073 additions and 33 deletions
+58 -1
View File
@@ -1,7 +1,8 @@
import { apiRequest } from '@/shared/api/client'
import { apiRequest, getAccessToken, HttpError } from '@/shared/api/client'
import type {
AdInsertion,
BlockMode,
BumperSettings,
ChannelDto,
ChannelSummaryDto,
CreatedIdResponse,
@@ -26,6 +27,8 @@ export type ChannelSettingsBody = {
isEnabled: boolean
adInsertion: AdInsertion
adsPerBreak: number
bumpersEnabled: boolean
bumper: BumperSettings
fillerAssetId: string | null
}
@@ -67,6 +70,60 @@ export function removeChannelAd(id: string, channelAdId: string) {
return apiRequest<void>(`/admin/channels/${id}/ads/${channelAdId}`, { method: 'DELETE' })
}
export function addChannelJingle(id: string, mediaAssetId: string) {
return apiRequest<CreatedIdResponse>(`/admin/channels/${id}/jingles`, {
method: 'POST',
body: { mediaAssetId },
})
}
export function removeChannelJingle(id: string, channelJingleId: string) {
return apiRequest<void>(`/admin/channels/${id}/jingles/${channelJingleId}`, { method: 'DELETE' })
}
/** Загрузка сырого файла заставки (фон/музыка): тело — файл, имя — в query (как в uploadMedia). */
function uploadBumperFile(id: string, kind: 'background' | 'music', file: File): Promise<void> {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest()
const query = new URLSearchParams({ fileName: file.name })
xhr.open('PUT', `/api/admin/channels/${id}/bumper/${kind}?${query.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 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)
})
}
export function uploadBumperBackground(id: string, file: File) {
return uploadBumperFile(id, 'background', file)
}
export function uploadBumperMusic(id: string, file: File) {
return uploadBumperFile(id, 'music', file)
}
export function clearBumperBackground(id: string) {
return apiRequest<void>(`/admin/channels/${id}/bumper/background`, { method: 'DELETE' })
}
export function clearBumperMusic(id: string) {
return apiRequest<void>(`/admin/channels/${id}/bumper/music`, { method: 'DELETE' })
}
export type OverrideBody = {
mode: OverrideMode
startsAtUtc: string