Backend: add refresh JWT (30d, token_type claim), POST /auth/refresh endpoint (rotates token pair), remember_me on login, JWT_REFRESH_EXPIRY_DAYS env var. Extractors now reject refresh tokens on protected routes. Frontend: sessionStorage for non-remembered sessions, localStorage + refresh token for remembered sessions. Transparent 401 recovery in api.ts (retry once after refresh). Remember me checkbox on login page with security note when checked.
294 lines
6.9 KiB
TypeScript
294 lines
6.9 KiB
TypeScript
// API response and request types matching the backend DTOs
|
|
|
|
export interface ActivityEvent {
|
|
id: string;
|
|
timestamp: string;
|
|
event_type: string;
|
|
detail: string;
|
|
channel_id?: string;
|
|
}
|
|
|
|
export interface LogLine {
|
|
level: string;
|
|
target: string;
|
|
message: string;
|
|
timestamp: string;
|
|
}
|
|
|
|
export type ContentType = "movie" | "episode" | "short";
|
|
|
|
export type AccessMode = "public" | "password_protected" | "account_required" | "owner_only";
|
|
|
|
export type LogoPosition = "top_left" | "top_right" | "bottom_left" | "bottom_right";
|
|
|
|
export type FillStrategy = "best_fit" | "sequential" | "random";
|
|
|
|
export interface MediaFilter {
|
|
content_type?: ContentType | null;
|
|
genres: string[];
|
|
decade?: number | null;
|
|
tags: string[];
|
|
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;
|
|
}
|
|
|
|
// Library browsing
|
|
|
|
export interface CollectionResponse {
|
|
id: string;
|
|
name: string;
|
|
collection_type?: string | null;
|
|
}
|
|
|
|
export interface SeriesResponse {
|
|
id: string;
|
|
name: string;
|
|
episode_count: number;
|
|
genres: string[];
|
|
year?: number | null;
|
|
}
|
|
|
|
export interface LibraryItemResponse {
|
|
id: string;
|
|
title: string;
|
|
content_type: ContentType;
|
|
duration_secs: number;
|
|
series_name?: string | null;
|
|
season_number?: number | null;
|
|
episode_number?: number | null;
|
|
year?: number | null;
|
|
genres: string[];
|
|
}
|
|
|
|
export interface RecyclePolicy {
|
|
cooldown_days?: number | null;
|
|
cooldown_generations?: number | null;
|
|
min_available_ratio: number;
|
|
}
|
|
|
|
export type BlockContent =
|
|
| { type: "algorithmic"; filter: MediaFilter; strategy: FillStrategy; provider_id?: string }
|
|
| { type: "manual"; items: string[]; provider_id?: string };
|
|
|
|
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;
|
|
access_mode?: AccessMode;
|
|
/** Plain-text password sent to API; hashed server-side. Only set on write operations. */
|
|
access_password?: string;
|
|
}
|
|
|
|
export type Weekday =
|
|
| 'monday' | 'tuesday' | 'wednesday' | 'thursday'
|
|
| 'friday' | 'saturday' | 'sunday'
|
|
|
|
export const WEEKDAYS: Weekday[] = [
|
|
'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday',
|
|
]
|
|
|
|
export const WEEKDAY_LABELS: Record<Weekday, string> = {
|
|
monday: 'Mon', tuesday: 'Tue', wednesday: 'Wed', thursday: 'Thu',
|
|
friday: 'Fri', saturday: 'Sat', sunday: 'Sun',
|
|
}
|
|
|
|
export interface ScheduleConfig {
|
|
day_blocks: Record<Weekday, ProgrammingBlock[]>
|
|
}
|
|
|
|
export interface ConfigSnapshot {
|
|
id: string
|
|
version_num: number
|
|
label: string | null
|
|
created_at: string
|
|
}
|
|
|
|
export interface ScheduleHistoryEntry {
|
|
id: string
|
|
generation: number
|
|
valid_from: string
|
|
valid_until: string
|
|
}
|
|
|
|
// Config
|
|
|
|
export type StreamingProtocol = "hls" | "direct_file";
|
|
|
|
export interface ProviderCapabilities {
|
|
collections: boolean;
|
|
series: boolean;
|
|
genres: boolean;
|
|
tags: boolean;
|
|
decade: boolean;
|
|
search: boolean;
|
|
streaming_protocol: StreamingProtocol;
|
|
rescan: boolean;
|
|
transcode: boolean;
|
|
}
|
|
|
|
export interface TranscodeSettings {
|
|
cleanup_ttl_hours: number;
|
|
}
|
|
|
|
export interface TranscodeStats {
|
|
cache_size_bytes: number;
|
|
item_count: number;
|
|
}
|
|
|
|
export interface ProviderInfo {
|
|
id: string;
|
|
capabilities: ProviderCapabilities;
|
|
}
|
|
|
|
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[];
|
|
}
|
|
|
|
export interface ProviderConfig {
|
|
provider_type: string;
|
|
config_json: Record<string, string>;
|
|
enabled: boolean;
|
|
}
|
|
|
|
export interface ProviderTestResult {
|
|
ok: boolean;
|
|
message: string;
|
|
}
|
|
|
|
// Auth
|
|
|
|
export interface TokenResponse {
|
|
access_token: string;
|
|
token_type: string;
|
|
expires_in: number;
|
|
refresh_token?: string;
|
|
}
|
|
|
|
export interface UserResponse {
|
|
id: string;
|
|
email: string;
|
|
created_at: string;
|
|
is_admin: boolean;
|
|
}
|
|
|
|
// Channels
|
|
|
|
export interface ChannelResponse {
|
|
id: string;
|
|
owner_id: string;
|
|
name: string;
|
|
description?: string | null;
|
|
timezone: string;
|
|
schedule_config: ScheduleConfig;
|
|
recycle_policy: RecyclePolicy;
|
|
auto_schedule: boolean;
|
|
access_mode: AccessMode;
|
|
logo?: string | null;
|
|
logo_position: LogoPosition;
|
|
logo_opacity: number;
|
|
webhook_url?: string | null;
|
|
webhook_poll_interval_secs?: number;
|
|
webhook_body_template?: string | null;
|
|
webhook_headers?: string | null;
|
|
created_at: string;
|
|
updated_at: string;
|
|
}
|
|
|
|
export interface CreateChannelRequest {
|
|
name: string;
|
|
timezone: string;
|
|
description?: string;
|
|
access_mode?: AccessMode;
|
|
access_password?: string;
|
|
webhook_url?: string;
|
|
webhook_poll_interval_secs?: number;
|
|
webhook_body_template?: string;
|
|
webhook_headers?: string;
|
|
}
|
|
|
|
export interface UpdateChannelRequest {
|
|
name?: string;
|
|
description?: string;
|
|
timezone?: string;
|
|
schedule_config?: ScheduleConfig;
|
|
recycle_policy?: RecyclePolicy;
|
|
auto_schedule?: boolean;
|
|
access_mode?: AccessMode;
|
|
/** Empty string clears the password. */
|
|
access_password?: string;
|
|
/** null = clear logo */
|
|
logo?: string | null;
|
|
logo_position?: LogoPosition;
|
|
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;
|
|
}
|
|
|
|
// Media & Schedule
|
|
|
|
export interface MediaItemResponse {
|
|
id: string;
|
|
title: string;
|
|
content_type: ContentType;
|
|
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;
|
|
}
|
|
|
|
export interface ScheduleResponse {
|
|
id: string;
|
|
channel_id: string;
|
|
generation: number;
|
|
generated_at: string;
|
|
valid_from: string;
|
|
valid_until: string;
|
|
slots: ScheduledSlotResponse[];
|
|
}
|
|
|
|
export interface CurrentBroadcastResponse {
|
|
slot: ScheduledSlotResponse;
|
|
offset_secs: number;
|
|
block_access_mode: AccessMode;
|
|
}
|