frontend: align types to backend, playout HLS, rm Jellyfin proxy (#12)

This commit is contained in:
2026-07-12 15:11:50 +02:00
parent 711f0e4411
commit 5bc1e5e44b
19 changed files with 128 additions and 334 deletions

View File

@@ -6,7 +6,7 @@ export function exportChannel(channel: ChannelResponse): void {
description: channel.description ?? undefined,
timezone: channel.timezone,
day_blocks: channel.schedule_config.day_blocks,
recycle_policy: channel.recycle_policy,
rotation_policy: channel.rotation_policy,
};
const blob = new Blob([JSON.stringify(payload, null, 2)], {
type: "application/json",

View File

@@ -26,9 +26,7 @@ export const mediaFilterSchema = z.object({
export const accessModeSchema = z.enum([
"public",
"password_protected",
"account_required",
"owner_only",
"private",
]);
export const blockSchema = z.object({
@@ -40,7 +38,7 @@ export const blockSchema = z.object({
z.object({
type: z.literal("algorithmic"),
filter: mediaFilterSchema,
strategy: z.enum(["best_fit", "sequential", "random"]),
strategy: z.enum(["best_fit", "sequential", "random", "alternating", "weighted", "marathon"]),
provider_id: z.string().optional(),
}),
z.object({
@@ -50,7 +48,7 @@ export const blockSchema = z.object({
}),
]),
loop_on_finish: z.boolean().optional(),
ignore_recycle_policy: z.boolean().optional(),
ignore_rotation_policy: z.boolean().optional(),
access_mode: accessModeSchema.optional(),
access_password: z.string().optional(),
});
@@ -63,7 +61,7 @@ export const channelFormSchema = z.object({
.default(() =>
Object.fromEntries(WEEKDAYS.map(d => [d, []])) as unknown as Record<Weekday, z.infer<typeof blockSchema>[]>
),
recycle_policy: z.object({
rotation_policy: z.object({
cooldown_days: z.number().int().min(0).nullable().optional(),
cooldown_generations: z.number().int().min(0).nullable().optional(),
min_available_ratio: z

View File

@@ -1,5 +1,3 @@
// API response and request types matching the backend DTOs
export interface ActivityEvent {
id: string;
timestamp: string;
@@ -17,11 +15,11 @@ export interface LogLine {
export type ContentType = "movie" | "episode" | "short";
export type AccessMode = "public" | "password_protected" | "account_required" | "owner_only";
export type AccessMode = "public" | "private";
export type LogoPosition = "top_left" | "top_right" | "bottom_left" | "bottom_right";
export type FillStrategy = "best_fit" | "sequential" | "random";
export type FillStrategy = "best_fit" | "sequential" | "random" | "alternating" | "weighted" | "marathon";
export interface MediaFilter {
content_type?: ContentType | null;
@@ -31,9 +29,7 @@ export interface MediaFilter {
min_duration_secs?: number | null;
max_duration_secs?: number | null;
collections: string[];
/** Filter to one or more TV series by name. OR-combined: any listed show is eligible. */
series_names?: string[];
/** Free-text search, used for library browsing only. */
search_term?: string | null;
}
@@ -55,22 +51,43 @@ export interface SeriesResponse {
export interface LibraryItemResponse {
id: string;
provider_id: string;
external_id: string;
title: string;
content_type: ContentType;
content_type: string;
duration_secs: number;
series_name?: string | null;
season_number?: number | null;
episode_number?: number | null;
year?: number | null;
genres: string[];
tags: string[];
collection_id?: string | null;
collection_name?: string | null;
collection_type?: string | null;
thumbnail_url?: string | null;
synced_at?: string | null;
}
export interface RecyclePolicy {
export interface RotationPolicy {
cooldown_days?: number | null;
cooldown_generations?: number | null;
min_available_ratio: number;
}
export interface InterstitialRule {
pool_filter: MediaFilter;
strategy: FillStrategy;
min_gap_secs: number;
}
export interface MidRollRule {
prefer_chapters: boolean;
fallback_interval_mins: number;
break_duration_secs: number;
pool_filter: MediaFilter;
}
export type BlockContent =
| { type: "algorithmic"; filter: MediaFilter; strategy: FillStrategy; provider_id?: string }
| { type: "manual"; items: string[]; provider_id?: string };
@@ -78,16 +95,14 @@ export type BlockContent =
export interface ProgrammingBlock {
id: string;
name: string;
/** "HH:MM:SS" */
start_time: string;
duration_mins: number;
content: BlockContent;
/** Sequential only: loop back to episode 1 after the last episode. Default true on backend. */
loop_on_finish?: boolean;
/** When true, skip the channel-level recycle policy for this block. Default false on backend. */
ignore_recycle_policy?: boolean;
ignore_rotation_policy?: boolean;
interstitial_rule?: InterstitialRule | null;
mid_roll_rule?: MidRollRule | null;
access_mode?: AccessMode;
/** Plain-text password sent to API; hashed server-side. Only set on write operations. */
access_password?: string;
}
@@ -154,9 +169,7 @@ export interface ProviderInfo {
export interface ConfigResponse {
allow_registration: boolean;
/** All registered providers. Added in multi-provider update. */
providers: ProviderInfo[];
/** Primary provider capabilities — kept for backward compat. */
provider_capabilities: ProviderCapabilities;
available_provider_types: string[];
}
@@ -198,14 +211,14 @@ export interface ChannelResponse {
description?: string | null;
timezone: string;
schedule_config: ScheduleConfig;
recycle_policy: RecyclePolicy;
rotation_policy: RotationPolicy;
auto_schedule: boolean;
access_mode: AccessMode;
access_mode: string;
logo?: string | null;
logo_position: LogoPosition;
logo_position: string;
logo_opacity: number;
webhook_url?: string | null;
webhook_poll_interval_secs?: number;
webhook_poll_interval_secs: number;
webhook_body_template?: string | null;
webhook_headers?: string | null;
created_at: string;
@@ -229,21 +242,15 @@ export interface UpdateChannelRequest {
description?: string;
timezone?: string;
schedule_config?: ScheduleConfig;
recycle_policy?: RecyclePolicy;
rotation_policy?: RotationPolicy;
auto_schedule?: boolean;
access_mode?: AccessMode;
/** Empty string clears the password. */
access_password?: string;
/** null = clear logo */
access_mode?: string;
logo?: string | null;
logo_position?: LogoPosition;
logo_position?: string;
logo_opacity?: number;
/** null = clear webhook */
webhook_url?: string | null;
webhook_poll_interval_secs?: number;
/** null = clear template */
webhook_body_template?: string | null;
/** null = clear headers */
webhook_headers?: string | null;
}
@@ -252,56 +259,40 @@ export interface UpdateChannelRequest {
export interface MediaItemResponse {
id: string;
title: string;
content_type: ContentType;
content_type: string;
duration_secs: number;
description?: string | null;
genres: string[];
tags: string[];
year?: number | null;
/** Episodes only: the parent TV show name. */
series_name?: string | null;
/** Episodes only: season number (1-based). */
season_number?: number | null;
/** Episodes only: episode number within the season (1-based). */
episode_number?: number | null;
}
export interface ScheduledSlotResponse {
id: string;
block_id: string;
item: MediaItemResponse;
/** RFC3339 */
start_at: string;
/** RFC3339 */
end_at: string;
block_access_mode: AccessMode;
item: MediaItemResponse;
source_block_id: string;
}
export interface ScheduleResponse {
id: string;
channel_id: string;
generation: number;
generated_at: string;
valid_from: string;
valid_until: string;
generation: number;
slots: ScheduledSlotResponse[];
}
export interface CurrentBroadcastResponse {
slot: ScheduledSlotResponse;
offset_secs: number;
block_access_mode: AccessMode;
}
// Library management
// Note: LibraryItemResponse is already defined in this file (search for it above).
// LibraryItemFull extends it with the extra fields returned by the DB-backed endpoint.
export interface LibraryItemFull extends LibraryItemResponse {
thumbnail_url?: string | null;
collection_id?: string | null;
collection_name?: string | null;
}
export type LibraryItemFull = LibraryItemResponse;
export interface ShowSummary {
series_name: string;