Add Media Daemon server and TV engine implementation

- Introduced `Server` class for managing the Media Daemon, including configuration, lifecycle, and HTTP route handling.
- Added `TvEngine` class for managing TV channels, scheduling, EPG generation, and streaming functionalities.
- Created `types.hpp` to define various data structures and enums for jobs, radio, TV channels, and plugins.
- Implemented main entry point in `main.cpp` to initialize and run the Media Daemon server with command-line and environment variable configurations.
- Established error handling and result management using a generic `Result` class.
- Included necessary headers and dependencies for media processing and plugin management.
This commit is contained in:
2025-12-30 11:40:25 +00:00
parent 14df5c377d
commit b20f2d2533
73 changed files with 4854 additions and 310 deletions

View File

@@ -0,0 +1,37 @@
import React from 'react'
import type { IconProps } from './types'
export const AlertCircle: React.FC<IconProps> = ({
size = 24,
color = 'currentColor',
weight = 'regular',
className = '',
...props
}) => {
const strokeWidths = {
thin: 1,
light: 1.5,
regular: 2,
bold: 2.5,
}
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width={size}
height={size}
viewBox="0 0 256 256"
fill="none"
stroke={color}
strokeWidth={strokeWidths[weight]}
strokeLinecap="round"
strokeLinejoin="round"
className={className}
{...props}
>
<circle cx="128" cy="128" r="96" />
<line x1="128" y1="80" x2="128" y2="136" />
<circle cx="128" cy="172" r="8" fill={color} stroke="none" />
</svg>
)
}

View File

@@ -0,0 +1,37 @@
import React from 'react'
import type { IconProps } from './types'
export const AlertTriangle: React.FC<IconProps> = ({
size = 24,
color = 'currentColor',
weight = 'regular',
className = '',
...props
}) => {
const strokeWidths = {
thin: 1,
light: 1.5,
regular: 2,
bold: 2.5,
}
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width={size}
height={size}
viewBox="0 0 256 256"
fill="none"
stroke={color}
strokeWidth={strokeWidths[weight]}
strokeLinecap="round"
strokeLinejoin="round"
className={className}
{...props}
>
<path d="M114.5 40.2L22.1 196a16 16 0 0 0 13.9 24h184a16 16 0 0 0 13.9-24L141.5 40.2a16 16 0 0 0-27 0z" />
<line x1="128" y1="104" x2="128" y2="144" />
<circle cx="128" cy="180" r="8" fill={color} stroke="none" />
</svg>
)
}

View File

@@ -0,0 +1,12 @@
import React from 'react'
import { Icon, IconProps } from './Icon'
export const Broadcast = (props: IconProps) => (
<Icon {...props}>
<circle cx="128" cy="128" r="32" />
<path d="M77.1 178.9a80 80 0 0 1 0-101.8" />
<path d="M178.9 77.1a80 80 0 0 1 0 101.8" />
<path d="M44.9 211.1a128 128 0 0 1 0-166.2" />
<path d="M211.1 44.9a128 128 0 0 1 0 166.2" />
</Icon>
)

9
fakemui/icons/Camera.tsx Normal file
View File

@@ -0,0 +1,9 @@
import React from 'react'
import { Icon, IconProps } from './Icon'
export const Camera = (props: IconProps) => (
<Icon {...props}>
<path d="M208 56h-27.7l-14.4-21.6A8 8 0 0 0 159.2 32H96.8a8 8 0 0 0-6.7 3.6L75.7 56H48a24 24 0 0 0-24 24v112a24 24 0 0 0 24 24h160a24 24 0 0 0 24-24V80a24 24 0 0 0-24-24Z" />
<circle cx="128" cy="132" r="44" />
</Icon>
)

View File

@@ -0,0 +1,38 @@
import React from 'react'
import type { IconProps } from './types'
export const ChartLine: React.FC<IconProps> = ({
size = 24,
color = 'currentColor',
weight = 'regular',
className = '',
...props
}) => {
const strokeWidths = {
thin: 1,
light: 1.5,
regular: 2,
bold: 2.5,
}
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width={size}
height={size}
viewBox="0 0 256 256"
fill="none"
stroke={color}
strokeWidth={strokeWidths[weight]}
strokeLinecap="round"
strokeLinejoin="round"
className={className}
{...props}
>
<polyline points="48 208 48 48" />
<polyline points="48 208 208 208" />
<polyline points="48 160 96 112 144 144 208 72" />
<polyline points="176 72 208 72 208 104" />
</svg>
)
}

37
fakemui/icons/Edit.tsx Normal file
View File

@@ -0,0 +1,37 @@
import React from 'react'
import type { IconProps } from './types'
export const Edit: React.FC<IconProps> = ({
size = 24,
color = 'currentColor',
weight = 'regular',
className = '',
...props
}) => {
const strokeWidths = {
thin: 1,
light: 1.5,
regular: 2,
bold: 2.5,
}
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width={size}
height={size}
viewBox="0 0 256 256"
fill="none"
stroke={color}
strokeWidth={strokeWidths[weight]}
strokeLinecap="round"
strokeLinejoin="round"
className={className}
{...props}
>
<path d="M92.7 216H48a8 8 0 0 1-8-8v-44.7a8 8 0 0 1 2.3-5.6l120-120a8 8 0 0 1 11.3 0l44.7 44.7a8 8 0 0 1 0 11.3l-120 120a8 8 0 0 1-5.6 2.3z" />
<line x1="136" y1="64" x2="192" y2="120" />
<line x1="40" y1="216" x2="96" y2="160" />
</svg>
)
}

9
fakemui/icons/File.tsx Normal file
View File

@@ -0,0 +1,9 @@
import React from 'react'
import { Icon, IconProps } from './Icon'
export const File = (props: IconProps) => (
<Icon {...props}>
<path d="M200 224H56a8 8 0 0 1-8-8V40a8 8 0 0 1 8-8h96l56 56v128a8 8 0 0 1-8 8Z" />
<polyline points="152 32 152 88 208 88" />
</Icon>
)

11
fakemui/icons/Grid.tsx Normal file
View File

@@ -0,0 +1,11 @@
import React from 'react'
import { Icon, IconProps } from './Icon'
export const Grid = (props: IconProps) => (
<Icon {...props}>
<rect x="32" y="32" width="80" height="80" rx="8" />
<rect x="144" y="32" width="80" height="80" rx="8" />
<rect x="32" y="144" width="80" height="80" rx="8" />
<rect x="144" y="144" width="80" height="80" rx="8" />
</Icon>
)

10
fakemui/icons/Image.tsx Normal file
View File

@@ -0,0 +1,10 @@
import React from 'react'
import { Icon, IconProps } from './Icon'
export const Image = (props: IconProps) => (
<Icon {...props}>
<rect x="32" y="48" width="192" height="160" rx="8" />
<circle cx="96" cy="112" r="24" />
<path d="m32 168 72-56 48 48 48-48 24 24" />
</Icon>
)

36
fakemui/icons/Inbox.tsx Normal file
View File

@@ -0,0 +1,36 @@
import React from 'react'
import type { IconProps } from './types'
export const Inbox: React.FC<IconProps> = ({
size = 24,
color = 'currentColor',
weight = 'regular',
className = '',
...props
}) => {
const strokeWidths = {
thin: 1,
light: 1.5,
regular: 2,
bold: 2.5,
}
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width={size}
height={size}
viewBox="0 0 256 256"
fill="none"
stroke={color}
strokeWidth={strokeWidths[weight]}
strokeLinecap="round"
strokeLinejoin="round"
className={className}
{...props}
>
<rect x="32" y="48" width="192" height="160" rx="8" />
<path d="M32 144h56a8 8 0 0 1 8 8 32 32 0 0 0 64 0 8 8 0 0 1 8-8h56" />
</svg>
)
}

10
fakemui/icons/List.tsx Normal file
View File

@@ -0,0 +1,10 @@
import React from 'react'
import { Icon, IconProps } from './Icon'
export const List = (props: IconProps) => (
<Icon {...props}>
<line x1="40" y1="64" x2="216" y2="64" />
<line x1="40" y1="128" x2="216" y2="128" />
<line x1="40" y1="192" x2="216" y2="192" />
</Icon>
)

38
fakemui/icons/Lock.tsx Normal file
View File

@@ -0,0 +1,38 @@
import React from 'react'
import type { IconProps } from './types'
export const Lock: React.FC<IconProps> = ({
size = 24,
color = 'currentColor',
weight = 'regular',
className = '',
...props
}) => {
const strokeWidths = {
thin: 1,
light: 1.5,
regular: 2,
bold: 2.5,
}
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width={size}
height={size}
viewBox="0 0 256 256"
fill="none"
stroke={color}
strokeWidth={strokeWidths[weight]}
strokeLinecap="round"
strokeLinejoin="round"
className={className}
{...props}
>
<rect x="48" y="112" width="160" height="112" rx="8" />
<path d="M80 112V80a48 48 0 0 1 96 0v32" />
<circle cx="128" cy="164" r="12" fill={color} stroke="none" />
<line x1="128" y1="176" x2="128" y2="196" />
</svg>
)
}

View File

@@ -0,0 +1,11 @@
import React from 'react'
import { Icon, IconProps } from './Icon'
export const Maximize = (props: IconProps) => (
<Icon {...props}>
<polyline points="168 48 208 48 208 88" />
<line x1="152" y1="104" x2="208" y2="48" />
<polyline points="88 208 48 208 48 168" />
<line x1="104" y1="152" x2="48" y2="208" />
</Icon>
)

View File

@@ -0,0 +1,11 @@
import React from 'react'
import { Icon, IconProps } from './Icon'
export const Minimize = (props: IconProps) => (
<Icon {...props}>
<polyline points="152 104 208 104 208 48" />
<line x1="152" y1="104" x2="208" y2="48" />
<polyline points="104 152 48 152 48 208" />
<line x1="104" y1="152" x2="48" y2="208" />
</Icon>
)

8
fakemui/icons/Pause.tsx Normal file
View File

@@ -0,0 +1,8 @@
import React from 'react'
import { Icon, IconProps } from './Icon'
export const Pause = (props: IconProps) => (
<Icon {...props}>
<path d="M216 48v160a16 16 0 0 1-16 16h-36a16 16 0 0 1-16-16V48a16 16 0 0 1 16-16h36a16 16 0 0 1 16 16ZM92 32H56a16 16 0 0 0-16 16v160a16 16 0 0 0 16 16h36a16 16 0 0 0 16-16V48a16 16 0 0 0-16-16Z" />
</Icon>
)

View File

@@ -0,0 +1,9 @@
import React from 'react'
import { Icon, IconProps } from './Icon'
export const Refresh = (props: IconProps) => (
<Icon {...props}>
<polyline points="176.2 99.7 224.2 99.7 224.2 51.7" />
<path d="M190.2 190.2a88 88 0 1 1 0-124.4l34 34" />
</Icon>
)

10
fakemui/icons/Screen.tsx Normal file
View File

@@ -0,0 +1,10 @@
import React from 'react'
import { Icon, IconProps } from './Icon'
export const Screen = (props: IconProps) => (
<Icon {...props}>
<rect x="32" y="48" width="192" height="144" rx="16" />
<line x1="96" y1="224" x2="160" y2="224" />
<line x1="128" y1="192" x2="128" y2="224" />
</Icon>
)

View File

@@ -0,0 +1,36 @@
import React from 'react'
import type { IconProps } from './types'
export const ShieldCheck: React.FC<IconProps> = ({
size = 24,
color = 'currentColor',
weight = 'regular',
className = '',
...props
}) => {
const strokeWidths = {
thin: 1,
light: 1.5,
regular: 2,
bold: 2.5,
}
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width={size}
height={size}
viewBox="0 0 256 256"
fill="none"
stroke={color}
strokeWidth={strokeWidths[weight]}
strokeLinecap="round"
strokeLinejoin="round"
className={className}
{...props}
>
<path d="M40 114V56a8 8 0 0 1 8-8h160a8 8 0 0 1 8 8v58c0 84-72 106-88 106s-88-22-88-106z" />
<polyline points="88 136 112 160 168 104" />
</svg>
)
}

38
fakemui/icons/Smile.tsx Normal file
View File

@@ -0,0 +1,38 @@
import React from 'react'
import type { IconProps } from './types'
export const Smile: React.FC<IconProps> = ({
size = 24,
color = 'currentColor',
weight = 'regular',
className = '',
...props
}) => {
const strokeWidths = {
thin: 1,
light: 1.5,
regular: 2,
bold: 2.5,
}
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width={size}
height={size}
viewBox="0 0 256 256"
fill="none"
stroke={color}
strokeWidth={strokeWidths[weight]}
strokeLinecap="round"
strokeLinejoin="round"
className={className}
{...props}
>
<circle cx="128" cy="128" r="96" />
<circle cx="92" cy="108" r="8" fill={color} stroke="none" />
<circle cx="164" cy="108" r="8" fill={color} stroke="none" />
<path d="M88 152a48 48 0 0 0 80 0" />
</svg>
)
}

8
fakemui/icons/Stop.tsx Normal file
View File

@@ -0,0 +1,8 @@
import React from 'react'
import { Icon, IconProps } from './Icon'
export const Stop = (props: IconProps) => (
<Icon {...props}>
<rect x="48" y="48" width="160" height="160" rx="8" />
</Icon>
)

9
fakemui/icons/Video.tsx Normal file
View File

@@ -0,0 +1,9 @@
import React from 'react'
import { Icon, IconProps } from './Icon'
export const Video = (props: IconProps) => (
<Icon {...props}>
<rect x="24" y="60" width="176" height="136" rx="8" />
<path d="M200 112l52-32v96l-52-32v-32z" />
</Icon>
)

11
fakemui/icons/Volume.tsx Normal file
View File

@@ -0,0 +1,11 @@
import React from 'react'
import { Icon, IconProps } from './Icon'
export const Volume = (props: IconProps) => (
<Icon {...props}>
<path d="M80 168H32a8 8 0 0 1-8-8V96a8 8 0 0 1 8-8h48l72-56v192Z" />
<line x1="80" y1="88" x2="80" y2="168" />
<path d="M190.5 73.5a72 72 0 0 1 0 109" />
<path d="M166.5 97.5a32 32 0 0 1 0 61" />
</Icon>
)

View File

@@ -0,0 +1,11 @@
import React from 'react'
import { Icon, IconProps } from './Icon'
export const VolumeOff = (props: IconProps) => (
<Icon {...props}>
<path d="M80 168H32a8 8 0 0 1-8-8V96a8 8 0 0 1 8-8h48l72-56v192Z" />
<line x1="80" y1="88" x2="80" y2="168" />
<line x1="240" y1="104" x2="192" y2="152" />
<line x1="240" y1="152" x2="192" y2="104" />
</Icon>
)

View File

@@ -11,9 +11,12 @@ export { Copy } from './Copy'
export { Check } from './Check'
export { X } from './X'
export { Play } from './Play'
export { Pause } from './Pause'
export { Stop } from './Stop'
export { Download } from './Download'
export { Upload } from './Upload'
export { Filter } from './Filter'
export { Refresh } from './Refresh'
// Navigation
export { ArrowUp } from './ArrowUp'
@@ -38,6 +41,20 @@ export { Pencil } from './Pencil'
export { Folder } from './Folder'
export { Code } from './Code'
export { Link } from './Link'
export { File } from './File'
export { Grid } from './Grid'
export { List } from './List'
export { Image } from './Image'
// Media & Display
export { Maximize } from './Maximize'
export { Minimize } from './Minimize'
export { Volume } from './Volume'
export { VolumeOff } from './VolumeOff'
export { Camera } from './Camera'
export { Video } from './Video'
export { Screen } from './Screen'
export { Broadcast } from './Broadcast'
// Communication & Time
export { Calendar } from './Calendar'
@@ -52,8 +69,16 @@ export { Warning } from './Warning'
export { CircleCheck } from './CircleCheck'
export { CircleX } from './CircleX'
export { Lightning } from './Lightning'
export { AlertCircle } from './AlertCircle'
export { AlertTriangle } from './AlertTriangle'
export { ChartLine } from './ChartLine'
export { ShieldCheck } from './ShieldCheck'
export { Lock } from './Lock'
// Social
export { Star } from './Star'
export { Heart } from './Heart'
export { Share } from './Share'
export { Smile } from './Smile'
export { Inbox } from './Inbox'
export { Edit } from './Edit'

View File

@@ -1,7 +1,48 @@
-- Cache Operations
---@class CacheSaveResult
---@field success boolean Whether save succeeded
---@field message string Status message
---@field key string The cache key
---@field ttl number Time to live in seconds
---@field error string? Error message if failed
---@class CacheGetResult
---@field success boolean Whether get succeeded
---@field data any? The cached data
---@field message string Status message (hit/miss)
---@field error string? Error message if failed
---@class CacheClearResult
---@field success boolean Whether clear succeeded
---@field message string Status message
---@field error string? Error message if failed
---@class UserPreferences
---@field theme string Color theme ("dark" | "light")
---@field language string Language code
---@field notifications boolean Whether notifications enabled
---@class PreferencesResult
---@field success boolean Whether operation succeeded
---@field preferences UserPreferences The user preferences
---@field is_default boolean? Whether returning defaults
---@class CacheModule
---@field save fun(key: string, data: any, ttl: number?): CacheSaveResult Save to cache
---@field get fun(key: string): CacheGetResult Get from cache
---@field clear fun(key: string): CacheClearResult Clear cached data
---@field save_preferences fun(prefs: UserPreferences?): CacheSaveResult Save user prefs
---@field get_preferences fun(): PreferencesResult Get user prefs
---@type CacheModule
local M = {}
-- Save data to cache
---Save data to cache
---@param key string The cache key
---@param data any The data to cache
---@param ttl number? Time to live in seconds (default 3600)
---@return CacheSaveResult
function M.save(key, data, ttl)
if not key or key == "" then
return { success = false, error = "Cache key is required" }
@@ -19,7 +60,9 @@ function M.save(key, data, ttl)
}
end
-- Get cached data
---Get cached data
---@param key string The cache key
---@return CacheGetResult
function M.get(key)
if not key or key == "" then
return { success = false, error = "Cache key is required" }
@@ -40,7 +83,9 @@ function M.get(key)
end
end
-- Clear cached data
---Clear cached data
---@param key string The cache key
---@return CacheClearResult
function M.clear(key)
if not key or key == "" then
return { success = false, error = "Cache key is required" }
@@ -54,7 +99,9 @@ function M.clear(key)
}
end
-- Save user preferences (common use case)
---Save user preferences (common use case)
---@param prefs UserPreferences? User preferences to save
---@return CacheSaveResult
function M.save_preferences(prefs)
prefs = prefs or {}
@@ -67,7 +114,8 @@ function M.save_preferences(prefs)
return M.save("user-preferences", data, 3600)
end
-- Get user preferences
---Get user preferences
---@return PreferencesResult
function M.get_preferences()
local result = M.get("user-preferences")
if result.success then

View File

@@ -1,10 +1,53 @@
-- Connection Management
---@class ConnectResult
---@field success boolean Whether connection succeeded
---@field endpoint string? The endpoint connected to
---@field api_key string? Masked API key or status
---@field timestamp string? Connection timestamp
---@field message string Status message
---@field error string? Error message if failed
---@class DisconnectResult
---@field success boolean Whether disconnection succeeded
---@field message string Status message
---@class ConnectionStatus
---@field status string Current status ("connected" | "disconnected")
---@field is_connected boolean Whether currently connected
---@class LogsResult
---@field logs string[] Array of log entries
---@field count number Number of log entries
---@class ClearLogsResult
---@field success boolean Whether logs were cleared
---@field message string Status message
---@class LogEntryResult
---@field success boolean Whether log was added
---@field entry string The formatted log entry
---@class ConnectionModule
---@field logs string[] Connection logs
---@field status string Current connection status
---@field connect fun(endpoint: string, api_key: string?): ConnectResult Connect to DBAL
---@field disconnect fun(): DisconnectResult Disconnect from DBAL
---@field get_status fun(): ConnectionStatus Get connection status
---@field get_logs fun(): LogsResult Get connection logs
---@field clear_logs fun(): ClearLogsResult Clear logs
---@field log fun(message: string): LogEntryResult Add custom log
---@type ConnectionModule
local M = {}
M.logs = {}
M.status = "disconnected"
-- Connect to DBAL endpoint
---Connect to DBAL endpoint
---@param endpoint string The DBAL endpoint URL
---@param api_key string? Optional API key
---@return ConnectResult
function M.connect(endpoint, api_key)
if not endpoint or endpoint == "" then
return { success = false, error = "Endpoint is required" }
@@ -25,7 +68,8 @@ function M.connect(endpoint, api_key)
}
end
-- Disconnect from DBAL
---Disconnect from DBAL
---@return DisconnectResult
function M.disconnect()
local timestamp = os.date("%H:%M:%S")
table.insert(M.logs, timestamp .. ": Disconnected")
@@ -37,7 +81,8 @@ function M.disconnect()
}
end
-- Get connection status
---Get connection status
---@return ConnectionStatus
function M.get_status()
return {
status = M.status,
@@ -45,7 +90,8 @@ function M.get_status()
}
end
-- Get connection logs
---Get connection logs
---@return LogsResult
function M.get_logs()
return {
logs = M.logs,
@@ -53,7 +99,8 @@ function M.get_logs()
}
end
-- Clear logs
---Clear logs
---@return ClearLogsResult
function M.clear_logs()
M.logs = {}
return {
@@ -62,7 +109,9 @@ function M.clear_logs()
}
end
-- Add custom log entry
---Add custom log entry
---@param message string Message to log
---@return LogEntryResult
function M.log(message)
local timestamp = os.date("%H:%M:%S")
table.insert(M.logs, timestamp .. ": " .. message)

View File

@@ -1,9 +1,32 @@
-- DBAL Demo Package Initialization
---@class DbalDemoTab
---@field value string Tab identifier
---@field label string Tab display label
---@class DbalDemoConfig
---@field default_endpoint string Default DBAL endpoint URL
---@field default_api_key string Default API key
---@field tabs DbalDemoTab[] Available demo tabs
---@class DbalDemoInitResult
---@field success boolean Whether initialization succeeded
---@field message string Status message
---@class DbalDemoModule
---@field name string Package name
---@field version string Package version
---@field init fun(): DbalDemoInitResult Initialize the package
---@field get_config fun(): DbalDemoConfig Get configuration
---@type DbalDemoModule
local M = {}
M.name = "dbal_demo"
M.version = "1.0.0"
---Initialize the DBAL demo package
---@return DbalDemoInitResult
function M.init()
return {
success = true,
@@ -11,6 +34,8 @@ function M.init()
}
end
---Get demo configuration
---@return DbalDemoConfig
function M.get_config()
return {
default_endpoint = "",

View File

@@ -1,7 +1,50 @@
-- KV Store Operations
---@class KVSetResult
---@field success boolean Whether set succeeded
---@field message string Status message
---@field ttl number? Time to live in seconds
---@field error string? Error message if failed
---@class KVGetResult
---@field success boolean Whether get succeeded
---@field value any? The retrieved value
---@field message string Status message
---@field error string? Error message if failed
---@class KVDeleteResult
---@field success boolean Whether delete succeeded
---@field message string Status message
---@field error string? Error message if failed
---@class KVListAddResult
---@field success boolean Whether add succeeded
---@field message string Status message
---@field items string[] Items that were added
---@field error string? Error message if failed
---@class KVListGetResult
---@field success boolean Whether get succeeded
---@field items string[] The list items
---@field count number Number of items
---@field message string Status message
---@field error string? Error message if failed
---@class KVModule
---@field set fun(key: string, value: any, ttl: number?): KVSetResult Set a key-value pair
---@field get fun(key: string): KVGetResult Get a value by key
---@field delete fun(key: string): KVDeleteResult Delete a key
---@field list_add fun(key: string, items: string[]?): KVListAddResult Add items to a list
---@field list_get fun(key: string): KVListGetResult Get list items
---@type KVModule
local M = {}
-- Set a key-value pair
---Set a key-value pair
---@param key string The key to set
---@param value any The value to store
---@param ttl number? Optional time to live in seconds
---@return KVSetResult
function M.set(key, value, ttl)
if not key or key == "" then
return { success = false, error = "Key is required" }
@@ -15,7 +58,9 @@ function M.set(key, value, ttl)
}
end
-- Get a value by key
---Get a value by key
---@param key string The key to retrieve
---@return KVGetResult
function M.get(key)
if not key or key == "" then
return { success = false, error = "Key is required" }
@@ -36,7 +81,9 @@ function M.get(key)
end
end
-- Delete a key
---Delete a key
---@param key string The key to delete
---@return KVDeleteResult
function M.delete(key)
if not key or key == "" then
return { success = false, error = "Key is required" }
@@ -56,7 +103,10 @@ function M.delete(key)
end
end
-- Add items to a list
---Add items to a list
---@param key string The list key
---@param items string[]? Items to add (defaults to sample items)
---@return KVListAddResult
function M.list_add(key, items)
if not key or key == "" then
return { success = false, error = "List key is required" }
@@ -72,7 +122,9 @@ function M.list_add(key, items)
}
end
-- Get list items
---Get list items
---@param key string The list key
---@return KVListGetResult
function M.list_get(key)
if not key or key == "" then
return { success = false, error = "List key is required" }

View File

@@ -0,0 +1,14 @@
--- Check if a user can moderate content
---@param user { role?: UserRole } User to check
---@return boolean Whether user can moderate
local function can_moderate(user)
local role = user.role or "public"
local allowed = {
admin = true,
god = true,
supergod = true
}
return allowed[role] == true
end
return can_moderate

View File

@@ -0,0 +1,9 @@
--- Check if a user can create posts
---@param user { role?: UserRole } User to check
---@return boolean Whether user can post
local function can_post(user)
local role = user.role or "public"
return role ~= "public"
end
return can_post

View File

@@ -0,0 +1,33 @@
--- Flag a post for moderation
--- Checks content length and banned terms
---@param post { content?: string } Post to check
---@return FlagResult Flagging result
local function flag_post(post)
local banned_terms = {
"spam",
"scam",
"phish",
"abuse"
}
local content = post.content or ""
local reasons = {}
if #content > 5000 then
table.insert(reasons, "Post exceeds 5000 characters")
end
local lowered = string.lower(content)
for _, term in ipairs(banned_terms) do
if string.find(lowered, term, 1, true) then
table.insert(reasons, "Contains banned term: " .. term)
end
end
return {
flagged = #reasons > 0,
reasons = reasons
}
end
return flag_post

View File

@@ -1,5 +1,21 @@
---@class ForumForge
local M = {}
--- Forum Forge package entry point
--- Provides forum functionality with moderation and permissions
---@class ForumForgePackage : ForumForgeModule
---@field name string Package name
---@field version string Package version
---@field moderation ModerationModule Moderation utilities
---@field permissions PermissionsModule Permission checking
---@field thread_rank ThreadRankModule Thread ranking
---@class ModerationModule
---@field flag_post fun(post: { content?: string }): FlagResult
---@class PermissionsModule
---@field can_post fun(user: { role?: UserRole }): boolean
---@field can_moderate fun(user: { role?: UserRole }): boolean
---@class ThreadRankModule
---@field rank_thread fun(thread: ForumThread): number
---@class InstallContext
---@field version string
@@ -11,6 +27,22 @@ local M = {}
---@class UninstallResult
---@field message string
local M = {}
M.name = "forum_forge"
M.version = "1.0.0"
-- Load sub-modules
M.moderation = require("moderation")
M.permissions = require("permissions")
M.thread_rank = require("thread_rank")
-- Also expose functions directly
M.flag_post = require("flag_post")
M.can_post = require("can_post")
M.can_moderate = require("can_moderate")
M.rank_thread = require("rank_thread")
---@param context InstallContext
---@return InstallResult
function M.on_install(context)

View File

@@ -1,42 +1,8 @@
---@class Moderation
--- Moderation facade for forum_forge
--- Re-exports single-function modules for backward compatibility
local M = {}
---@type string[]
local banned_terms = {
"spam",
"scam",
"phish",
"abuse"
}
---@class Post
---@field content? string
---@class FlagResult
---@field flagged boolean
---@field reasons string[]
---@param post Post
---@return FlagResult
function M.flag_post(post)
local content = post.content or ""
local reasons = {}
if #content > 5000 then
table.insert(reasons, "Post exceeds 5000 characters")
end
local lowered = string.lower(content)
for _, term in ipairs(banned_terms) do
if string.find(lowered, term, 1, true) then
table.insert(reasons, "Contains banned term: " .. term)
end
end
return {
flagged = #reasons > 0,
reasons = reasons
}
end
M.flag_post = require("flag_post")
return M

View File

@@ -1,26 +1,9 @@
---@class ForumPermissions
--- Permissions facade for forum_forge
--- Re-exports single-function modules for backward compatibility
local M = {}
---@class User
---@field role? "public"|"user"|"admin"|"god"|"supergod"
---@param user User
---@return boolean
function M.can_post(user)
local role = user.role or "public"
return role ~= "public"
end
---@param user User
---@return boolean
function M.can_moderate(user)
local role = user.role or "public"
local allowed = {
admin = true,
god = true,
supergod = true
}
return allowed[role] == true
end
M.can_post = require("can_post")
M.can_moderate = require("can_moderate")
return M

View File

@@ -0,0 +1,13 @@
--- Calculate thread ranking score
--- Uses replies, likes, and recency
---@param thread ForumThread Thread to rank
---@return number Ranking score
local function rank_thread(thread)
local replies = thread.replyCount or 0
local likes = thread.likeCount or 0
local recency = thread.lastReplyAt or thread.createdAt or 0
return (replies * 2) + likes + (recency / 1000000)
end
return rank_thread

View File

@@ -1,20 +1,8 @@
---@class ThreadRank
--- Thread ranking facade for forum_forge
--- Re-exports single-function modules for backward compatibility
local M = {}
---@class Thread
---@field replyCount? number
---@field likeCount? number
---@field lastReplyAt? number
---@field createdAt? number
---@param thread Thread
---@return number
function M.rank_thread(thread)
local replies = thread.replyCount or 0
local likes = thread.likeCount or 0
local recency = thread.lastReplyAt or thread.createdAt or 0
return (replies * 2) + likes + (recency / 1000000)
end
M.rank_thread = require("rank_thread")
return M

View File

@@ -0,0 +1,55 @@
---@meta
-- Type definitions for forum_forge package
---@alias UserRole "public" | "user" | "moderator" | "admin" | "god" | "supergod"
---@class ForumUser
---@field id string User ID
---@field name string Username
---@field role UserRole User role level
---@field avatar? string Avatar URL
---@class ForumPost
---@field id string Post ID
---@field threadId string Parent thread ID
---@field authorId string Author user ID
---@field content string Post content
---@field createdAt number Created timestamp
---@field updatedAt? number Last update timestamp
---@field likes number Like count
---@class ForumThread
---@field id string Thread ID
---@field title string Thread title
---@field authorId string Author user ID
---@field forumId string Parent forum ID
---@field replyCount number Number of replies
---@field likeCount number Total likes
---@field lastReplyAt number Last reply timestamp
---@field createdAt number Created timestamp
---@field isPinned? boolean Whether thread is pinned
---@field isLocked? boolean Whether thread is locked
---@class FlagResult
---@field flagged boolean Whether post was flagged
---@field reasons string[] Reasons for flagging
---@class InstallContext
---@field version string Version number
---@class InstallResult
---@field message string Install message
---@field version string Version installed
---@class UninstallResult
---@field message string Uninstall message
---@class ForumForgeModule
---@field on_install fun(context: InstallContext): InstallResult
---@field on_uninstall fun(): UninstallResult
---@field flag_post fun(post: { content?: string }): FlagResult
---@field can_post fun(user: { role?: UserRole }): boolean
---@field can_moderate fun(user: { role?: UserRole }): boolean
---@field rank_thread fun(thread: ForumThread): number
return {}

View File

@@ -1,3 +1,9 @@
--- IRC Webchat package entry point
--- Provides IRC-style chat functionality for web interfaces
---@class IRCWebchatModule : IRCWebchat
---@field name string Package name
---@field version string Package version
---@class InstallContext
---@field version string Version number
@@ -7,6 +13,16 @@
local M = {}
M.name = "irc_webchat"
M.version = "1.0.0"
--- Load utility modules
M.formatTime = require("format_time")
M.handleCommand = require("handle_command")
M.sendMessage = require("send_message")
M.userJoin = require("user_join")
M.userLeave = require("user_leave")
---@param context InstallContext Installation context object
---@return InstallResult Installation result
function M.on_install(context)

View File

@@ -0,0 +1,46 @@
---@meta
-- Type definitions for irc_webchat package
---@alias MessageType "message" | "system" | "command" | "join" | "leave"
---@class IRCMessage
---@field id string Message identifier
---@field username string Username of the sender
---@field userId string User identifier
---@field message string Message content
---@field type MessageType Message type
---@field timestamp number Timestamp in milliseconds
---@field channelId string Channel identifier
---@class ChatMessage : IRCMessage
---@field type "message"
---@class SystemMessage : IRCMessage
---@field type "system"
---@class CommandMessage : IRCMessage
---@field type "command"
---@class JoinMessage : IRCMessage
---@field type "join"
---@class LeaveMessage : IRCMessage
---@field type "leave"
---@class InstallContext
---@field version string Version number
---@class InstallResult
---@field message string Installation message
---@field version? string Optional version number
---@class IRCWebchat
---@field on_install fun(context: InstallContext): InstallResult
---@field on_uninstall fun(): InstallResult
---@field formatTime fun(timestamp: number): string
---@field handleCommand fun(command: string, channelId: string, username: string, onlineUsers: string[]): IRCMessage
---@field sendMessage fun(channelId: string, username: string, userId: string, message: string): ChatMessage
---@field userJoin fun(channelId: string, username: string, userId: string): JoinMessage
---@field userLeave fun(channelId: string, username: string, userId: string): LeaveMessage
return {}

View File

@@ -0,0 +1,15 @@
--- Calculate total count from summary items
---@param items SummaryItem[] Summary items
---@return number Total count
local function calculate_total(items)
local total = 0
for _, item in ipairs(items) do
local count = item.count or 0
if count > 0 then
total = total + count
end
end
return total
end
return calculate_total

View File

@@ -0,0 +1,16 @@
--- Severity class mappings
local severity_classes = {
info = "summary-item--info",
success = "summary-item--success",
warning = "summary-item--warning",
error = "summary-item--error"
}
--- Get CSS class for severity level
---@param severity? NotificationSeverity Severity level
---@return string CSS class name
local function get_severity_class(severity)
return severity_classes[severity or "info"] or severity_classes.info
end
return get_severity_class

View File

@@ -1,18 +1,44 @@
-- Notification Center initialization
--- Notification Center package entry point
--- Provides notification list, toast, and summary components
---@class NotificationCenterPackage
---@field name string Package name
---@field version string Package version
---@field list NotificationListModule List rendering
---@field toast ToastModule Toast utilities
---@field summary SummaryModule Summary utilities
---@field init fun(): NotificationCenterInfo
---@class NotificationCenter
---@field name string
---@field version string
local M = {}
---@class NotificationListModule
---@field render_item fun(notification: Notification): NotificationItemComponent
---@field render_list fun(notifications: Notification[]): NotificationListComponent
---@field render_badge fun(count: number): BadgeComponent|nil
M.name = "notification_center"
M.version = "1.0.0"
---@class ToastModule
---@field success fun(message: string, duration?: number): Toast
---@field error fun(message: string, duration?: number): Toast
---@field warning fun(message: string, duration?: number): Toast
---@field info fun(message: string, duration?: number): Toast
---@class SummaryModule
---@field calculateTotal fun(items: SummaryItem[]): number
---@field getSeverityClass fun(severity?: NotificationSeverity): string
---@field prepareSummary fun(props: SummaryProps): SummaryData
---@class NotificationCenterInfo
---@field name string
---@field version string
---@field loaded boolean
local M = {}
M.name = "notification_center"
M.version = "1.0.0"
-- Load sub-modules
M.list = require("list")
M.toast = require("toast")
M.summary = require("summary")
---@return NotificationCenterInfo
function M.init()
return {

View File

@@ -1,63 +1,10 @@
-- Notification list utilities
--- Notification list facade
--- Re-exports single-function modules for backward compatibility
---@class NotificationListUtils
local M = {}
---@class Notification
---@field id string|number
---@field title string
---@field message string
---@field created_at string|number
---@field read? boolean
---@field icon? string
---@class UIComponent
---@field type string
---@field props? table
---@field children? table
---@field content? string
---@field variant? string
---@param notification Notification
---@return UIComponent
function M.render_item(notification)
return {
type = "notification_item",
props = {
id = notification.id,
title = notification.title,
message = notification.message,
time = notification.created_at,
read = notification.read or false,
icon = notification.icon or "bell"
}
}
end
---@param notifications Notification[]
---@return UIComponent
function M.render_list(notifications)
local items = {}
for _, n in ipairs(notifications) do
table.insert(items, M.render_item(n))
end
return {
type = "notification_list",
children = items
}
end
---@param count number
---@return UIComponent|nil
function M.render_badge(count)
if count > 0 then
return {
type = "badge",
content = count > 99 and "99+" or tostring(count),
variant = "error"
}
end
return nil
end
M.render_item = require("render_notification_item")
M.render_list = require("render_notification_list")
M.render_badge = require("render_notification_badge")
return M

View File

@@ -0,0 +1,48 @@
local calculate_total = require("calculate_total")
local get_severity_class = require("get_severity_class")
--- Default summary items
local default_items = {
{ label = "Info", count = 0, severity = "info" },
{ label = "Success", count = 0, severity = "success" },
{ label = "Warnings", count = 0, severity = "warning" },
{ label = "Errors", count = 0, severity = "error" }
}
--- Prepare summary data with enriched items
---@param props SummaryProps Summary configuration
---@return SummaryData Prepared summary data
local function prepare_summary(props)
local title = props.title or "Notification Summary"
local subtitle = props.subtitle
local totalLabel = props.totalLabel or "Total"
local items = props.items
if not items or #items == 0 then
items = default_items
end
local total = calculate_total(items)
-- Enrich items with severity classes
local enrichedItems = {}
for i, item in ipairs(items) do
enrichedItems[i] = {
label = item.label,
count = math.max(item.count or 0, 0),
severity = item.severity or "info",
hint = item.hint,
classes = get_severity_class(item.severity)
}
end
return {
title = title,
subtitle = subtitle,
totalLabel = totalLabel,
total = total,
items = enrichedItems
}
end
return prepare_summary

View File

@@ -0,0 +1,15 @@
--- Render a notification badge
---@param count number Unread notification count
---@return BadgeComponent|nil Badge component or nil if count is 0
local function render_notification_badge(count)
if count > 0 then
return {
type = "badge",
content = count > 99 and "99+" or tostring(count),
variant = "error"
}
end
return nil
end
return render_notification_badge

View File

@@ -0,0 +1,18 @@
--- Render a notification item component
---@param notification Notification Notification data
---@return NotificationItemComponent Notification item component
local function render_notification_item(notification)
return {
type = "notification_item",
props = {
id = notification.id,
title = notification.title,
message = notification.message,
time = notification.created_at,
read = notification.read or false,
icon = notification.icon or "bell"
}
}
end
return render_notification_item

View File

@@ -0,0 +1,17 @@
local render_notification_item = require("render_notification_item")
--- Render a list of notifications
---@param notifications Notification[] Array of notifications
---@return NotificationListComponent Notification list component
local function render_notification_list(notifications)
local items = {}
for _, n in ipairs(notifications) do
table.insert(items, render_notification_item(n))
end
return {
type = "notification_list",
children = items
}
end
return render_notification_list

View File

@@ -1,97 +1,16 @@
-- Notification summary card component logic
local json = require("json")
--- Notification summary facade
--- Re-exports single-function modules for backward compatibility
---@class NotificationSummary
---@field severityClasses table<string, string>
---@field defaultItems SummaryItem[]
local M = {}
-- Load config from JSON file
M.calculateTotal = require("calculate_total")
M.getSeverityClass = require("get_severity_class")
M.prepareSummary = require("prepare_summary")
-- Keep config for backward compatibility
local json = require("json")
local config = json.load("summary.json")
M.severityClasses = config.severityClasses
M.defaultItems = config.defaultItems
---@class SummaryItem
---@field label string
---@field count? number
---@field severity? "info"|"success"|"warning"|"error"
---@field hint? string
---@class EnrichedSummaryItem
---@field label string
---@field count number
---@field severity "info"|"success"|"warning"|"error"
---@field hint? string
---@field classes string
---@class SummaryProps
---@field title? string
---@field subtitle? string
---@field totalLabel? string
---@field items? SummaryItem[]
---@class SummaryData
---@field title string
---@field subtitle? string
---@field totalLabel string
---@field total number
---@field items EnrichedSummaryItem[]
-- Calculate total from items
---@param items SummaryItem[]
---@return number
function M.calculateTotal(items)
local total = 0
for _, item in ipairs(items) do
local count = item.count or 0
if count > 0 then
total = total + count
end
end
return total
end
-- Get severity class for an item
---@param severity? "info"|"success"|"warning"|"error"
---@return string
function M.getSeverityClass(severity)
return M.severityClasses[severity or "info"] or M.severityClasses.info
end
-- Prepare summary data
---@param props SummaryProps
---@return SummaryData
function M.prepareSummary(props)
local title = props.title or "Notification Summary"
local subtitle = props.subtitle
local totalLabel = props.totalLabel or "Total"
local items = props.items
if not items or #items == 0 then
items = M.defaultItems
end
local total = M.calculateTotal(items)
-- Enrich items with severity classes
local enrichedItems = {}
for i, item in ipairs(items) do
enrichedItems[i] = {
label = item.label,
count = math.max(item.count or 0, 0),
severity = item.severity or "info",
hint = item.hint,
classes = M.getSeverityClass(item.severity)
}
end
return {
title = title,
subtitle = subtitle,
totalLabel = totalLabel,
total = total,
items = enrichedItems
}
end
M.severityClasses = config and config.severityClasses or {}
M.defaultItems = config and config.defaultItems or {}
return M

View File

@@ -1,65 +1,11 @@
-- Toast notification utilities
--- Toast notification facade
--- Re-exports single-function modules for backward compatibility
---@class ToastUtils
local M = {}
---@class Toast
---@field type string
---@field variant "info"|"success"|"warning"|"error"
---@field message string
---@field duration number
---@field icon string
---@param message string
---@param duration? number
---@return Toast
function M.success(message, duration)
return {
type = "toast",
variant = "success",
message = message,
duration = duration or 3000,
icon = "check"
}
end
---@param message string
---@param duration? number
---@return Toast
function M.error(message, duration)
return {
type = "toast",
variant = "error",
message = message,
duration = duration or 5000,
icon = "error"
}
end
---@param message string
---@param duration? number
---@return Toast
function M.warning(message, duration)
return {
type = "toast",
variant = "warning",
message = message,
duration = duration or 4000,
icon = "warning"
}
end
---@param message string
---@param duration? number
---@return Toast
function M.info(message, duration)
return {
type = "toast",
variant = "info",
message = message,
duration = duration or 3000,
icon = "info"
}
end
M.success = require("toast_success")
M.error = require("toast_error")
M.warning = require("toast_warning")
M.info = require("toast_info")
return M

View File

@@ -0,0 +1,15 @@
--- Create an error toast notification
---@param message string Toast message
---@param duration? number Display duration in ms (default 5000)
---@return Toast Toast configuration
local function toast_error(message, duration)
return {
type = "toast",
variant = "error",
message = message,
duration = duration or 5000,
icon = "error"
}
end
return toast_error

View File

@@ -0,0 +1,15 @@
--- Create an info toast notification
---@param message string Toast message
---@param duration? number Display duration in ms (default 3000)
---@return Toast Toast configuration
local function toast_info(message, duration)
return {
type = "toast",
variant = "info",
message = message,
duration = duration or 3000,
icon = "info"
}
end
return toast_info

View File

@@ -0,0 +1,15 @@
--- Create a success toast notification
---@param message string Toast message
---@param duration? number Display duration in ms (default 3000)
---@return Toast Toast configuration
local function toast_success(message, duration)
return {
type = "toast",
variant = "success",
message = message,
duration = duration or 3000,
icon = "check"
}
end
return toast_success

View File

@@ -0,0 +1,15 @@
--- Create a warning toast notification
---@param message string Toast message
---@param duration? number Display duration in ms (default 4000)
---@return Toast Toast configuration
local function toast_warning(message, duration)
return {
type = "toast",
variant = "warning",
message = message,
duration = duration or 4000,
icon = "warning"
}
end
return toast_warning

View File

@@ -0,0 +1,80 @@
---@meta
-- Type definitions for notification_center package
---@alias NotificationSeverity "info" | "success" | "warning" | "error"
---@class Notification
---@field id string|number Notification ID
---@field title string Title text
---@field message string Message content
---@field created_at string|number Creation timestamp
---@field read? boolean Whether notification has been read
---@field icon? string Icon name
---@class UIComponent
---@field type string Component type
---@field props? table Component props
---@field children? table[] Child components
---@field content? string Text content
---@field variant? string Variant name
---@class NotificationItemComponent
---@field type "notification_item"
---@field props NotificationItemProps
---@class NotificationItemProps
---@field id string|number
---@field title string
---@field message string
---@field time string|number
---@field read boolean
---@field icon string
---@class NotificationListComponent
---@field type "notification_list"
---@field children NotificationItemComponent[]
---@class BadgeComponent
---@field type "badge"
---@field content string
---@field variant string
---@class Toast
---@field type "toast"
---@field variant NotificationSeverity
---@field message string
---@field duration number
---@field icon string
---@class SummaryItem
---@field label string Item label
---@field count? number Item count
---@field severity? NotificationSeverity Severity level
---@field hint? string Hint text
---@class EnrichedSummaryItem
---@field label string
---@field count number
---@field severity NotificationSeverity
---@field hint? string
---@field classes string
---@class SummaryProps
---@field title? string Summary title
---@field subtitle? string Summary subtitle
---@field totalLabel? string Total label
---@field items? SummaryItem[] Summary items
---@class SummaryData
---@field title string
---@field subtitle? string
---@field totalLabel string
---@field total number
---@field items EnrichedSummaryItem[]
---@class NotificationCenterInfo
---@field name string
---@field version string
---@field loaded boolean
return {}

View File

@@ -1,10 +1,24 @@
---@alias StreamUserRole "public" | "user" | "moderator" | "admin" | "god" | "supergod"
---@class StreamUser
---@field role? StreamUserRole User role
---@class StreamPermissionsModule
---@field can_publish fun(user: StreamUser): boolean Check if user can publish streams
---@field can_moderate fun(user: StreamUser): boolean Check if user can moderate streams
local M = {}
---Check if user can publish streams
---@param user StreamUser User to check
---@return boolean
function M.can_publish(user)
local role = user.role or "public"
return role == "moderator" or role == "admin" or role == "god" or role == "supergod"
end
---Check if user can moderate streams
---@param user StreamUser User to check
---@return boolean
function M.can_moderate(user)
local role = user.role or "public"
return role == "moderator" or role == "admin" or role == "god" or role == "supergod"

View File

@@ -0,0 +1,110 @@
# Multi-stage build for Media Daemon
# Stage 1: Build environment
FROM ubuntu:22.04 AS builder
ENV DEBIAN_FRONTEND=noninteractive
# Install build dependencies
RUN apt-get update && apt-get install -y \
build-essential \
cmake \
ninja-build \
git \
curl \
pkg-config \
python3 \
python3-pip \
libssl-dev \
libc-ares-dev \
libbrotli-dev \
libcurl4-openssl-dev \
uuid-dev \
zlib1g-dev \
libpq-dev \
libmysqlclient-dev \
libsqlite3-dev \
&& rm -rf /var/lib/apt/lists/*
# Install Conan
RUN pip3 install conan
# Configure Conan
RUN conan profile detect
# Copy source code
WORKDIR /app
COPY . .
# Install dependencies via Conan
WORKDIR /app/build
RUN conan install ../build-config/conanfile.txt \
--output-folder=. \
--build=missing \
-s build_type=Release
# Build
RUN cmake ../build-config \
-G Ninja \
-DCMAKE_BUILD_TYPE=Release \
-DCMAKE_TOOLCHAIN_FILE=conan_toolchain.cmake
RUN ninja
# Stage 2: Runtime image
FROM ubuntu:22.04 AS runtime
ENV DEBIAN_FRONTEND=noninteractive
# Install runtime dependencies
RUN apt-get update && apt-get install -y \
ffmpeg \
imagemagick \
pandoc \
texlive-xetex \
texlive-fonts-recommended \
fonts-liberation \
curl \
ca-certificates \
libssl3 \
libc-ares2 \
libcurl4 \
libpq5 \
libmysqlclient21 \
libsqlite3-0 \
tini \
&& rm -rf /var/lib/apt/lists/*
# Create non-root user
RUN groupadd -r media && useradd -r -g media media
# Create directories
RUN mkdir -p /data/media /data/temp /data/output /data/cache \
/data/hls/radio /data/hls/tv \
/plugins /etc/media-daemon /var/log/media-daemon \
&& chown -R media:media /data /plugins /var/log/media-daemon
# Copy binary
COPY --from=builder /app/build/media_daemon /usr/local/bin/
# Copy default config
COPY config/media-daemon.yaml /etc/media-daemon/config.yaml
# ImageMagick policy (allow PDF processing)
RUN sed -i 's/rights="none" pattern="PDF"/rights="read|write" pattern="PDF"/' /etc/ImageMagick-6/policy.xml || true
# Switch to non-root user
USER media
# Expose ports
EXPOSE 8090
# Health check
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD curl -f http://localhost:8090/health || exit 1
# Volumes
VOLUME ["/data/media", "/data/output", "/data/hls", "/plugins"]
# Entry point
ENTRYPOINT ["/usr/bin/tini", "--"]
CMD ["media_daemon", "--config", "/etc/media-daemon/config.yaml"]

View File

@@ -0,0 +1,115 @@
# MetaBuilder Media Daemon
A C++ Docker daemon for media processing, radio streaming, and TV channel simulation with a modular plugin architecture.
## Features
### 🎬 Job Queue System
- Video transcoding (H.264, H.265, VP9, AV1)
- Audio transcoding (MP3, AAC, FLAC, Opus)
- Document conversion (PDF, Office formats)
- Image processing (resize, convert, optimize)
- Priority queuing with configurable workers
### 📻 Radio Streaming
- Live audio streaming with multiple quality levels
- Playlist scheduling and auto-DJ
- Crossfading and audio normalization
- Metadata injection (artist, title, album art)
- Icecast/Shoutcast compatible output
### 📺 TV Channel Simulation
- Multi-channel scheduling system
- EPG (Electronic Program Guide) generation
- Bumpers, commercials, and interstitials
- Live-to-VOD and VOD-to-Live workflows
- HLS/DASH output for web players
### 🔌 Plugin Architecture
- Dynamic plugin loading (.so/.dll)
- Hot-reload support in development mode
- Plugin API for custom processors
- Built-in plugins: FFmpeg, ImageMagick, Pandoc
### 🔗 DBAL Integration
- Job status notifications to users
- Progress tracking in real-time
- User permission checks
- Multi-tenant isolation
## Architecture
```
┌─────────────────────────────────────────────────────────────┐
│ Media Daemon │
├─────────────────────────────────────────────────────────────┤
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ HTTP API │ │ WebSocket │ │ gRPC │ │
│ │ (Drogon) │ │ (Live) │ │ (Internal) │ │
│ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │
│ └────────────────┼────────────────┘ │
│ ▼ │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ Core Engine │ │
│ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │
│ │ │ Job │ │ Radio │ │ TV │ │ Plugin │ │ │
│ │ │ Queue │ │ Engine │ │ Engine │ │ Manager │ │ │
│ │ └────┬────┘ └────┬────┘ └────┬────┘ └────┬────┘ │ │
│ └───────┼────────────┼────────────┼────────────┼───────┘ │
│ └────────────┴────────────┴────────────┘ │
│ ▼ │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ Plugin System │ │
│ │ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ │ │
│ │ │FFmpeg │ │ImageMag│ │Pandoc │ │Custom │ │ │
│ │ │Plugin │ │Plugin │ │Plugin │ │Plugins │ │ │
│ │ └────────┘ └────────┘ └────────┘ └────────┘ │ │
│ └───────────────────────────────────────────────────────┘ │
│ ▼ │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ DBAL Client (Notifications) │ │
│ └───────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
```
## Quick Start
```bash
# Build
docker build -t metabuilder/media-daemon -f Dockerfile .
# Run
docker run -d \
-p 8090:8090 \
-v /media:/data/media \
-e DBAL_URL=http://dbal:8080 \
metabuilder/media-daemon
```
## API Endpoints
### Job Queue
- `POST /api/jobs` - Submit a new job
- `GET /api/jobs/:id` - Get job status
- `GET /api/jobs` - List jobs (filtered by user/tenant)
- `DELETE /api/jobs/:id` - Cancel a job
### Radio
- `POST /api/radio/channels` - Create radio channel
- `GET /api/radio/channels/:id/stream` - Get stream URL
- `POST /api/radio/channels/:id/playlist` - Update playlist
- `GET /api/radio/channels/:id/now-playing` - Current track info
### TV Channels
- `POST /api/tv/channels` - Create TV channel
- `GET /api/tv/channels/:id/schedule` - Get EPG
- `POST /api/tv/channels/:id/schedule` - Update schedule
- `GET /api/tv/channels/:id/stream.m3u8` - HLS playlist
### Plugins
- `GET /api/plugins` - List loaded plugins
- `POST /api/plugins/:id/reload` - Hot-reload plugin (dev mode)
## Configuration
See `config/media-daemon.yaml` for full configuration options.

View File

@@ -0,0 +1,159 @@
cmake_minimum_required(VERSION 3.20)
project(media_daemon VERSION 1.0.0 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
# Directories
set(MEDIA_ROOT ${CMAKE_CURRENT_LIST_DIR}/..)
set(MEDIA_SRC_DIR ${MEDIA_ROOT}/src)
set(MEDIA_INCLUDE_DIR ${MEDIA_ROOT}/include)
set(MEDIA_TEST_DIR ${MEDIA_ROOT}/tests)
# Find required packages
find_package(Threads REQUIRED)
find_package(Drogon REQUIRED CONFIG)
find_package(cpr REQUIRED CONFIG)
find_package(fmt QUIET)
find_package(spdlog QUIET)
find_package(nlohmann_json QUIET)
# Include directories
include_directories(
${MEDIA_INCLUDE_DIR}
${MEDIA_SRC_DIR}
)
# ============================================================================
# Core Library
# ============================================================================
add_library(media_core STATIC
${MEDIA_SRC_DIR}/types.cpp
${MEDIA_SRC_DIR}/plugin_manager.cpp
${MEDIA_SRC_DIR}/job_queue.cpp
${MEDIA_SRC_DIR}/radio_engine.cpp
${MEDIA_SRC_DIR}/tv_engine.cpp
${MEDIA_SRC_DIR}/dbal_client.cpp
)
target_link_libraries(media_core
Threads::Threads
cpr::cpr
${CMAKE_DL_LIBS} # For dynamic loading
)
if(fmt_FOUND)
target_link_libraries(media_core fmt::fmt)
endif()
if(spdlog_FOUND)
target_link_libraries(media_core spdlog::spdlog)
endif()
# ============================================================================
# Built-in Plugins
# ============================================================================
add_library(media_plugins STATIC
${MEDIA_SRC_DIR}/plugins/ffmpeg_plugin.cpp
${MEDIA_SRC_DIR}/plugins/imagemagick_plugin.cpp
${MEDIA_SRC_DIR}/plugins/pandoc_plugin.cpp
)
target_link_libraries(media_plugins
media_core
)
# ============================================================================
# Server
# ============================================================================
add_library(media_server STATIC
${MEDIA_SRC_DIR}/server.cpp
${MEDIA_SRC_DIR}/routes/health_routes.cpp
${MEDIA_SRC_DIR}/routes/job_routes.cpp
${MEDIA_SRC_DIR}/routes/radio_routes.cpp
${MEDIA_SRC_DIR}/routes/tv_routes.cpp
${MEDIA_SRC_DIR}/routes/plugin_routes.cpp
)
target_link_libraries(media_server
media_core
media_plugins
Drogon::Drogon
)
# ============================================================================
# Main Executable
# ============================================================================
add_executable(media_daemon
${MEDIA_SRC_DIR}/main.cpp
)
target_link_libraries(media_daemon
media_server
media_core
media_plugins
)
# ============================================================================
# Plugin Shared Library Template
# ============================================================================
# Example of how to build an external plugin
# add_library(my_custom_plugin SHARED
# ${MEDIA_SRC_DIR}/plugins/custom/my_plugin.cpp
# )
# target_link_libraries(my_custom_plugin media_core)
# ============================================================================
# Tests
# ============================================================================
enable_testing()
add_executable(job_queue_test
${MEDIA_TEST_DIR}/unit/job_queue_test.cpp
)
target_link_libraries(job_queue_test media_core)
add_executable(plugin_manager_test
${MEDIA_TEST_DIR}/unit/plugin_manager_test.cpp
)
target_link_libraries(plugin_manager_test media_core)
add_executable(radio_engine_test
${MEDIA_TEST_DIR}/unit/radio_engine_test.cpp
)
target_link_libraries(radio_engine_test media_core)
add_executable(tv_engine_test
${MEDIA_TEST_DIR}/unit/tv_engine_test.cpp
)
target_link_libraries(tv_engine_test media_core)
add_test(NAME job_queue_test COMMAND job_queue_test)
add_test(NAME plugin_manager_test COMMAND plugin_manager_test)
add_test(NAME radio_engine_test COMMAND radio_engine_test)
add_test(NAME tv_engine_test COMMAND tv_engine_test)
# ============================================================================
# Installation
# ============================================================================
install(TARGETS media_daemon
RUNTIME DESTINATION bin
)
install(DIRECTORY ${MEDIA_INCLUDE_DIR}/media
DESTINATION include
)
install(FILES ${MEDIA_ROOT}/config/media-daemon.yaml
DESTINATION etc/media-daemon
RENAME config.yaml
)

View File

@@ -0,0 +1,15 @@
[requires]
fmt/10.1.1
spdlog/1.12.0
nlohmann_json/3.11.2
drogon/1.8.7
cpr/1.10.4
[generators]
CMakeToolchain
CMakeDeps
[options]
drogon/*:with_postgres=True
drogon/*:with_mysql=True
drogon/*:with_sqlite=True

View File

@@ -0,0 +1,197 @@
# Media Daemon Configuration
version: "1.0"
# Server settings
server:
bind_address: "0.0.0.0"
port: 8090
workers: 4
development_mode: false
# DBAL connection for notifications
dbal:
url: "${DBAL_URL:-http://localhost:8080}"
api_key: "${DBAL_API_KEY}"
timeout_ms: 5000
retry_attempts: 3
# Job queue configuration
job_queue:
# Maximum concurrent jobs per type
workers:
video: 2
audio: 4
document: 4
image: 8
# Job priority levels (lower = higher priority)
priorities:
urgent: 0
high: 1
normal: 2
low: 3
background: 4
# Job retention
retention:
completed_hours: 24
failed_hours: 168 # 7 days
# Temp storage
temp_dir: "/data/temp"
output_dir: "/data/output"
# Radio streaming configuration
radio:
enabled: true
max_channels: 10
# Audio settings
audio:
sample_rate: 44100
channels: 2
bitrates: [64, 128, 192, 320] # kbps
default_bitrate: 128
codec: "mp3" # mp3, aac, opus
# Crossfade settings
crossfade:
enabled: true
duration_ms: 3000
# Normalization
normalization:
enabled: true
target_lufs: -14.0
# Output formats
outputs:
- type: "icecast"
host: "${ICECAST_HOST:-localhost}"
port: 8000
mount: "/radio"
password: "${ICECAST_PASSWORD}"
- type: "hls"
segment_duration: 6
playlist_size: 5
# TV channel simulation
tv:
enabled: true
max_channels: 5
# Video settings
video:
resolutions:
- name: "1080p"
width: 1920
height: 1080
bitrate: 5000
- name: "720p"
width: 1280
height: 720
bitrate: 2500
- name: "480p"
width: 854
height: 480
bitrate: 1000
codec: "h264" # h264, h265, vp9
preset: "fast"
# Audio settings
audio:
codec: "aac"
bitrate: 128
sample_rate: 48000
# HLS output settings
hls:
segment_duration: 4
playlist_size: 10
output_dir: "/data/hls"
# EPG settings
epg:
lookahead_hours: 24
refresh_interval_minutes: 15
# Plugin system
plugins:
enabled: true
directory: "/plugins"
hot_reload: false # Enable in development mode
# Built-in plugins
builtin:
ffmpeg:
enabled: true
binary: "/usr/bin/ffmpeg"
hardware_accel: "auto" # auto, nvidia, vaapi, none
imagemagick:
enabled: true
binary: "/usr/bin/convert"
memory_limit: "512MB"
pandoc:
enabled: true
binary: "/usr/bin/pandoc"
pdf_engine: "xelatex"
# Storage backends
storage:
# Local filesystem
local:
media_dir: "/data/media"
cache_dir: "/data/cache"
max_cache_size_gb: 50
# S3-compatible storage (optional)
s3:
enabled: false
endpoint: "${S3_ENDPOINT}"
bucket: "${S3_BUCKET}"
access_key: "${S3_ACCESS_KEY}"
secret_key: "${S3_SECRET_KEY}"
region: "us-east-1"
# Monitoring
monitoring:
# Prometheus metrics
prometheus:
enabled: true
endpoint: "/metrics"
# Health checks
health:
endpoint: "/health"
include_details: true
# Logging
logging:
level: "info" # trace, debug, info, warn, error
format: "json" # json, text
output: "stdout" # stdout, file
file:
path: "/var/log/media-daemon.log"
max_size_mb: 100
max_files: 5
# Security
security:
# API authentication
api_keys:
enabled: true
header: "X-API-Key"
# Rate limiting
rate_limit:
enabled: true
requests_per_minute: 100
burst: 20
# CORS
cors:
enabled: true
origins: ["*"]
methods: ["GET", "POST", "PUT", "DELETE"]

View File

@@ -0,0 +1,91 @@
# Nginx configuration for HLS streaming
# Optimized for low-latency video delivery
server {
listen 80;
server_name _;
# Enable CORS
add_header 'Access-Control-Allow-Origin' '*' always;
add_header 'Access-Control-Allow-Methods' 'GET, OPTIONS' always;
add_header 'Access-Control-Allow-Headers' 'Origin, Content-Type, Accept, Range' always;
add_header 'Access-Control-Expose-Headers' 'Content-Length, Content-Range' always;
# Cache settings
add_header Cache-Control 'no-cache';
# HLS/DASH content
location /hls {
alias /data/hls;
# MIME types for HLS
types {
application/vnd.apple.mpegurl m3u8;
video/mp2t ts;
application/dash+xml mpd;
video/mp4 mp4 m4s;
}
# Caching for segments
location ~ \.ts$ {
add_header Cache-Control 'public, max-age=31536000';
}
location ~ \.m4s$ {
add_header Cache-Control 'public, max-age=31536000';
}
# No caching for playlists
location ~ \.m3u8$ {
add_header Cache-Control 'no-cache, no-store, must-revalidate';
add_header Pragma 'no-cache';
add_header Expires '0';
}
location ~ \.mpd$ {
add_header Cache-Control 'no-cache, no-store, must-revalidate';
add_header Pragma 'no-cache';
add_header Expires '0';
}
}
# Radio streams (HLS)
location /radio {
alias /data/hls/radio;
types {
application/vnd.apple.mpegurl m3u8;
video/mp2t ts;
audio/aac aac;
}
location ~ \.m3u8$ {
add_header Cache-Control 'no-cache';
}
}
# TV streams (HLS)
location /tv {
alias /data/hls/tv;
types {
application/vnd.apple.mpegurl m3u8;
video/mp2t ts;
video/mp4 mp4 m4s;
}
location ~ \.m3u8$ {
add_header Cache-Control 'no-cache';
}
}
# Health check
location /nginx-health {
return 200 'OK';
add_header Content-Type text/plain;
}
# Gzip
gzip on;
gzip_types application/vnd.apple.mpegurl application/dash+xml;
}

View File

@@ -0,0 +1,190 @@
# Media Daemon Docker Compose
# Use with: docker-compose -f docker-compose.yml up -d
version: '3.8'
services:
# ============================================================================
# Media Daemon
# ============================================================================
media-daemon:
build:
context: .
dockerfile: Dockerfile
image: metabuilder/media-daemon:latest
container_name: media-daemon
restart: unless-stopped
ports:
- "8090:8090"
environment:
# Server
- MEDIA_BIND_ADDRESS=0.0.0.0
- MEDIA_PORT=8090
- MEDIA_WORKERS=4
- MEDIA_DEV_MODE=false
# DBAL Connection
- DBAL_URL=http://dbal-daemon:8080
- DBAL_API_KEY=${DBAL_API_KEY:-}
# Job Queue
- MEDIA_VIDEO_WORKERS=2
- MEDIA_AUDIO_WORKERS=4
- MEDIA_DOC_WORKERS=4
- MEDIA_IMAGE_WORKERS=8
- MEDIA_TEMP_DIR=/data/temp
- MEDIA_OUTPUT_DIR=/data/output
# Radio
- MEDIA_RADIO_ENABLED=true
- MEDIA_RADIO_MAX_CHANNELS=10
- MEDIA_RADIO_HLS_DIR=/data/hls/radio
# TV
- MEDIA_TV_ENABLED=true
- MEDIA_TV_MAX_CHANNELS=5
- MEDIA_TV_HLS_DIR=/data/hls/tv
# Icecast (optional)
- ICECAST_HOST=${ICECAST_HOST:-icecast}
- ICECAST_PASSWORD=${ICECAST_PASSWORD:-hackme}
volumes:
# Media storage
- media-library:/data/media:ro
- media-output:/data/output
- media-cache:/data/cache
# HLS segments (serve via nginx)
- hls-output:/data/hls
# Temp (consider tmpfs for performance)
- media-temp:/data/temp
# Custom plugins
- ./plugins:/plugins:ro
# Custom config (optional)
# - ./config/media-daemon.yaml:/etc/media-daemon/config.yaml:ro
deploy:
resources:
limits:
cpus: '4'
memory: 4G
reservations:
cpus: '1'
memory: 1G
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8090/health"]
interval: 30s
timeout: 5s
retries: 3
start_period: 10s
networks:
- metabuilder-net
depends_on:
dbal-daemon:
condition: service_healthy
# ============================================================================
# Icecast Server (Radio Output)
# ============================================================================
icecast:
image: libretime/icecast:2.4.4
container_name: icecast
restart: unless-stopped
ports:
- "8000:8000"
environment:
- ICECAST_SOURCE_PASSWORD=${ICECAST_PASSWORD:-hackme}
- ICECAST_ADMIN_PASSWORD=${ICECAST_ADMIN_PASSWORD:-hackme}
- ICECAST_RELAY_PASSWORD=${ICECAST_PASSWORD:-hackme}
- ICECAST_HOSTNAME=${ICECAST_HOSTNAME:-localhost}
- ICECAST_MAX_CLIENTS=100
- ICECAST_MAX_SOURCES=10
volumes:
- icecast-logs:/var/log/icecast2
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/status-json.xsl"]
interval: 30s
timeout: 5s
retries: 3
networks:
- metabuilder-net
# ============================================================================
# Nginx (HLS Streaming Server)
# ============================================================================
nginx-stream:
image: nginx:alpine
container_name: nginx-stream
restart: unless-stopped
ports:
- "8080:80" # HLS streams
volumes:
- hls-output:/data/hls:ro
- ./config/nginx-stream.conf:/etc/nginx/conf.d/default.conf:ro
networks:
- metabuilder-net
# ============================================================================
# Redis (optional - for distributed job queue)
# ============================================================================
redis:
image: redis:7-alpine
container_name: media-redis
restart: unless-stopped
command: redis-server --appendonly yes
volumes:
- redis-data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 3
networks:
- metabuilder-net
# ============================================================================
# Networks
# ============================================================================
networks:
metabuilder-net:
external: true
# ============================================================================
# Volumes
# ============================================================================
volumes:
media-library:
name: metabuilder-media-library
media-output:
name: metabuilder-media-output
media-cache:
name: metabuilder-media-cache
media-temp:
name: metabuilder-media-temp
hls-output:
name: metabuilder-hls-output
icecast-logs:
name: metabuilder-icecast-logs
redis-data:
name: metabuilder-media-redis

View File

@@ -0,0 +1,251 @@
#pragma once
#include "media/types.hpp"
#include <memory>
#include <string>
#include <functional>
namespace media {
/**
* DBAL Client Configuration
*/
struct DbalClientConfig {
std::string url = "http://localhost:8080";
std::string api_key;
int timeout_ms = 5000;
int retry_attempts = 3;
int retry_delay_ms = 1000;
};
/**
* DBAL Client
*
* Handles communication with the DBAL daemon for:
* - Sending user notifications
* - Checking user permissions
* - Storing job metadata
* - Multi-tenant isolation
*/
class DbalClient {
public:
DbalClient();
~DbalClient();
// Disable copying
DbalClient(const DbalClient&) = delete;
DbalClient& operator=(const DbalClient&) = delete;
// ========================================================================
// Initialization
// ========================================================================
/**
* Initialize the DBAL client
* @param config Client configuration
* @return Result indicating success or failure
*/
Result<void> initialize(const DbalClientConfig& config);
/**
* Check if connected to DBAL
*/
bool is_connected() const;
/**
* Test connection to DBAL
* @return Result indicating success or failure
*/
Result<void> ping();
// ========================================================================
// Notifications
// ========================================================================
/**
* Send a notification to a user
* @param notification Notification details
* @return Result indicating success or failure
*/
Result<void> send_notification(const Notification& notification);
/**
* Send job started notification
*/
Result<void> notify_job_started(
const std::string& tenant_id,
const std::string& user_id,
const std::string& job_id,
JobType job_type
);
/**
* Send job progress notification
*/
Result<void> notify_job_progress(
const std::string& tenant_id,
const std::string& user_id,
const std::string& job_id,
const JobProgress& progress
);
/**
* Send job completed notification
*/
Result<void> notify_job_completed(
const std::string& tenant_id,
const std::string& user_id,
const std::string& job_id,
const std::string& output_path
);
/**
* Send job failed notification
*/
Result<void> notify_job_failed(
const std::string& tenant_id,
const std::string& user_id,
const std::string& job_id,
const std::string& error_message
);
/**
* Send stream started notification
*/
Result<void> notify_stream_started(
const std::string& tenant_id,
const std::string& channel_id,
const std::string& channel_name,
const std::string& stream_url
);
/**
* Send stream stopped notification
*/
Result<void> notify_stream_stopped(
const std::string& tenant_id,
const std::string& channel_id,
const std::string& channel_name
);
// ========================================================================
// Permissions
// ========================================================================
/**
* Check if user has permission for an action
* @param tenant_id Tenant ID
* @param user_id User ID
* @param permission Permission to check
* @return Result with boolean or error
*/
Result<bool> check_permission(
const std::string& tenant_id,
const std::string& user_id,
const std::string& permission
);
/**
* Get user's permission level
* @param tenant_id Tenant ID
* @param user_id User ID
* @return Result with level (0-6) or error
*/
Result<int> get_user_level(
const std::string& tenant_id,
const std::string& user_id
);
// ========================================================================
// Job Storage
// ========================================================================
/**
* Store job record in DBAL
* @param job Job info to store
* @return Result indicating success or failure
*/
Result<void> store_job(const JobInfo& job);
/**
* Update job record in DBAL
* @param job Updated job info
* @return Result indicating success or failure
*/
Result<void> update_job(const JobInfo& job);
/**
* Get job record from DBAL
* @param job_id Job ID
* @return Result with job info or error
*/
Result<JobInfo> get_job(const std::string& job_id);
/**
* List jobs from DBAL
* @param tenant_id Tenant ID filter
* @param user_id User ID filter (optional)
* @param limit Max results
* @param offset Pagination offset
* @return Result with job list or error
*/
Result<std::vector<JobInfo>> list_jobs(
const std::string& tenant_id,
const std::string& user_id = "",
size_t limit = 100,
size_t offset = 0
);
// ========================================================================
// Channel Storage
// ========================================================================
/**
* Store radio channel config
*/
Result<void> store_radio_channel(const RadioChannelConfig& config);
/**
* Store TV channel config
*/
Result<void> store_tv_channel(const TvChannelConfig& config);
/**
* Get radio channels for tenant
*/
Result<std::vector<RadioChannelConfig>> get_radio_channels(
const std::string& tenant_id
);
/**
* Get TV channels for tenant
*/
Result<std::vector<TvChannelConfig>> get_tv_channels(
const std::string& tenant_id
);
private:
/**
* Make HTTP request to DBAL
*/
Result<std::string> make_request(
const std::string& method,
const std::string& endpoint,
const std::string& body = ""
);
/**
* Retry wrapper
*/
template<typename Func>
auto with_retry(Func&& func) -> decltype(func());
DbalClientConfig config_;
bool initialized_ = false;
// HTTP client handle (cpr or similar)
struct Impl;
std::unique_ptr<Impl> impl_;
};
} // namespace media

View File

@@ -0,0 +1,259 @@
#pragma once
#include "media/types.hpp"
#include "media/plugin_manager.hpp"
#include <memory>
#include <queue>
#include <vector>
#include <map>
#include <mutex>
#include <condition_variable>
#include <thread>
#include <atomic>
#include <functional>
namespace media {
/**
* Job Queue Configuration
*/
struct JobQueueConfig {
// Worker counts per job type
int video_workers = 2;
int audio_workers = 4;
int document_workers = 4;
int image_workers = 8;
// Directories
std::string temp_dir = "/data/temp";
std::string output_dir = "/data/output";
// Retention (hours)
int completed_retention_hours = 24;
int failed_retention_hours = 168;
// Notification callback
NotificationCallback notification_callback;
};
/**
* Job Queue
*
* Thread-safe priority queue for processing media jobs.
* Supports multiple worker threads per job type.
*/
class JobQueue {
public:
JobQueue();
~JobQueue();
// Disable copying
JobQueue(const JobQueue&) = delete;
JobQueue& operator=(const JobQueue&) = delete;
// ========================================================================
// Initialization
// ========================================================================
/**
* Initialize the job queue
* @param config Queue configuration
* @param plugin_manager Plugin manager for job processing
* @return Result indicating success or failure
*/
Result<void> initialize(
const JobQueueConfig& config,
PluginManager* plugin_manager
);
/**
* Start processing jobs
*/
void start();
/**
* Stop processing and shutdown
* @param wait_for_completion Wait for current jobs to finish
*/
void stop(bool wait_for_completion = true);
// ========================================================================
// Job Management
// ========================================================================
/**
* Submit a new job
* @param request Job request
* @return Result with job ID or error
*/
Result<std::string> submit(const JobRequest& request);
/**
* Get job status
* @param job_id Job ID
* @return Result with job info or error
*/
Result<JobInfo> get_job(const std::string& job_id) const;
/**
* List jobs with optional filtering
* @param tenant_id Filter by tenant (empty for all)
* @param user_id Filter by user (empty for all)
* @param status Filter by status (optional)
* @param limit Maximum results
* @param offset Pagination offset
* @return Vector of job info
*/
std::vector<JobInfo> list_jobs(
const std::string& tenant_id = "",
const std::string& user_id = "",
std::optional<JobStatus> status = std::nullopt,
size_t limit = 100,
size_t offset = 0
) const;
/**
* Cancel a job
* @param job_id Job ID
* @return Result indicating success or failure
*/
Result<void> cancel(const std::string& job_id);
/**
* Retry a failed job
* @param job_id Job ID
* @return Result with new job ID or error
*/
Result<std::string> retry(const std::string& job_id);
// ========================================================================
// Callbacks
// ========================================================================
/**
* Set progress callback
*/
void set_progress_callback(JobProgressCallback callback);
/**
* Set completion callback
*/
void set_completion_callback(JobCompletionCallback callback);
// ========================================================================
// Statistics
// ========================================================================
/**
* Get queue statistics
*/
struct QueueStats {
size_t pending_jobs = 0;
size_t processing_jobs = 0;
size_t completed_jobs = 0;
size_t failed_jobs = 0;
size_t cancelled_jobs = 0;
// Per-type counts
std::map<JobType, size_t> pending_by_type;
std::map<JobType, size_t> processing_by_type;
// Worker status
int total_workers = 0;
int busy_workers = 0;
int idle_workers = 0;
};
QueueStats get_stats() const;
/**
* Check if queue is running
*/
bool is_running() const { return running_.load(); }
private:
/**
* Generate unique job ID
*/
std::string generate_job_id();
/**
* Worker thread function
*/
void worker_thread(JobType type, int worker_id);
/**
* Process a single job
*/
void process_job(JobInfo& job);
/**
* Update job status
*/
void update_job_status(const std::string& job_id, JobStatus status, const std::string& error = "");
/**
* Send notification via DBAL
*/
void send_notification(const JobInfo& job, NotificationType type);
/**
* Cleanup old jobs
*/
void cleanup_old_jobs();
/**
* Cleanup thread function
*/
void cleanup_thread();
// Configuration
JobQueueConfig config_;
PluginManager* plugin_manager_ = nullptr;
// State
std::atomic<bool> initialized_{false};
std::atomic<bool> running_{false};
// Job storage
mutable std::mutex jobs_mutex_;
std::map<std::string, JobInfo> jobs_;
std::map<std::string, JobRequest> job_requests_; // Original requests
// Priority queues per job type
struct JobQueueEntry {
std::string job_id;
JobPriority priority;
std::chrono::system_clock::time_point submitted_at;
bool operator<(const JobQueueEntry& other) const {
// Lower priority value = higher priority
if (static_cast<int>(priority) != static_cast<int>(other.priority)) {
return static_cast<int>(priority) > static_cast<int>(other.priority);
}
// Earlier submission time = higher priority
return submitted_at > other.submitted_at;
}
};
mutable std::mutex queues_mutex_;
std::map<JobType, std::priority_queue<JobQueueEntry>> queues_;
std::map<JobType, std::condition_variable> queue_cvs_;
// Worker threads
std::vector<std::thread> workers_;
std::map<JobType, std::vector<std::thread>> typed_workers_;
// Cleanup thread
std::thread cleanup_thread_;
std::condition_variable cleanup_cv_;
// Callbacks
JobProgressCallback progress_callback_;
JobCompletionCallback completion_callback_;
// Job ID counter
std::atomic<uint64_t> job_counter_{0};
};
} // namespace media

View File

@@ -0,0 +1,191 @@
#pragma once
#include "media/types.hpp"
#include <memory>
#include <string>
namespace media {
/**
* Plugin Interface
*
* Base class for all media processing plugins. Plugins are loaded dynamically
* and can handle various media processing tasks.
*
* To create a custom plugin:
* 1. Inherit from Plugin
* 2. Implement all pure virtual methods
* 3. Export the create_plugin() and destroy_plugin() functions
*/
class Plugin {
public:
virtual ~Plugin() = default;
// ========================================================================
// Plugin Metadata
// ========================================================================
/**
* Get plugin information
*/
virtual PluginInfo info() const = 0;
/**
* Get plugin capabilities
*/
virtual PluginCapabilities capabilities() const = 0;
// ========================================================================
// Lifecycle
// ========================================================================
/**
* Initialize the plugin
* Called once when the plugin is loaded
* @param config_path Path to plugin-specific configuration
* @return Result indicating success or failure
*/
virtual Result<void> initialize(const std::string& config_path) = 0;
/**
* Shutdown the plugin
* Called before the plugin is unloaded
*/
virtual void shutdown() = 0;
/**
* Check if the plugin is healthy
* @return true if plugin is operational
*/
virtual bool is_healthy() const = 0;
// ========================================================================
// Processing
// ========================================================================
/**
* Check if this plugin can handle a specific job
* @param type Job type
* @param params Job parameters
* @return true if plugin can process this job
*/
virtual bool can_handle(JobType type, const JobParams& params) const = 0;
/**
* Process a job
* @param request Job request with all parameters
* @param progress_callback Callback for progress updates
* @return Result with output path or error
*/
virtual Result<std::string> process(
const JobRequest& request,
JobProgressCallback progress_callback
) = 0;
/**
* Cancel an ongoing job
* @param job_id ID of job to cancel
* @return Result indicating success or failure
*/
virtual Result<void> cancel(const std::string& job_id) = 0;
// ========================================================================
// Streaming (optional - for streaming-capable plugins)
// ========================================================================
/**
* Start a stream
* @param channel_id Channel ID
* @param source Source configuration
* @param output Output configuration
* @return Result with stream URL or error
*/
virtual Result<std::string> start_stream(
const std::string& channel_id,
const std::map<std::string, std::string>& source,
const std::map<std::string, std::string>& output
) {
return Result<std::string>::error(
ErrorCode::NOT_FOUND,
"Streaming not supported by this plugin"
);
}
/**
* Stop a stream
* @param channel_id Channel ID
* @return Result indicating success or failure
*/
virtual Result<void> stop_stream(const std::string& channel_id) {
return Result<void>::error(
ErrorCode::NOT_FOUND,
"Streaming not supported by this plugin"
);
}
};
/**
* Plugin factory function type
* Every plugin shared library must export these functions:
*
* extern "C" {
* Plugin* create_plugin();
* void destroy_plugin(Plugin* plugin);
* const char* plugin_api_version();
* }
*/
using CreatePluginFunc = Plugin* (*)();
using DestroyPluginFunc = void (*)(Plugin*);
using PluginApiVersionFunc = const char* (*)();
// Current plugin API version
constexpr const char* PLUGIN_API_VERSION = "1.0.0";
/**
* Plugin handle for managing loaded plugins
*/
struct PluginHandle {
std::string path;
void* library_handle = nullptr;
Plugin* instance = nullptr;
CreatePluginFunc create_func = nullptr;
DestroyPluginFunc destroy_func = nullptr;
PluginApiVersionFunc version_func = nullptr;
bool is_loaded = false;
};
} // namespace media
// ============================================================================
// Plugin Export Macros
// ============================================================================
/**
* Use this macro in your plugin implementation to export required functions
*
* Example:
*
* class MyPlugin : public media::Plugin {
* // ... implementation
* };
*
* MEDIA_PLUGIN_EXPORT(MyPlugin)
*/
#ifdef _WIN32
#define MEDIA_PLUGIN_API __declspec(dllexport)
#else
#define MEDIA_PLUGIN_API __attribute__((visibility("default")))
#endif
#define MEDIA_PLUGIN_EXPORT(PluginClass) \
extern "C" { \
MEDIA_PLUGIN_API media::Plugin* create_plugin() { \
return new PluginClass(); \
} \
MEDIA_PLUGIN_API void destroy_plugin(media::Plugin* plugin) { \
delete plugin; \
} \
MEDIA_PLUGIN_API const char* plugin_api_version() { \
return media::PLUGIN_API_VERSION; \
} \
}

View File

@@ -0,0 +1,174 @@
#pragma once
#include "media/types.hpp"
#include "media/plugin.hpp"
#include <memory>
#include <vector>
#include <map>
#include <mutex>
#include <filesystem>
namespace media {
/**
* Plugin Manager
*
* Handles loading, unloading, and managing media processing plugins.
* Supports both built-in and dynamically loaded plugins.
*/
class PluginManager {
public:
PluginManager();
~PluginManager();
// Disable copying
PluginManager(const PluginManager&) = delete;
PluginManager& operator=(const PluginManager&) = delete;
// ========================================================================
// Initialization
// ========================================================================
/**
* Initialize the plugin manager
* @param plugin_dir Directory to scan for plugins
* @param config_path Path to plugin configuration
* @return Result indicating success or failure
*/
Result<void> initialize(
const std::string& plugin_dir,
const std::string& config_path
);
/**
* Shutdown all plugins and cleanup
*/
void shutdown();
// ========================================================================
// Plugin Management
// ========================================================================
/**
* Load a specific plugin
* @param path Path to plugin shared library
* @return Result with plugin info or error
*/
Result<PluginInfo> load_plugin(const std::string& path);
/**
* Unload a specific plugin
* @param plugin_id Plugin ID to unload
* @return Result indicating success or failure
*/
Result<void> unload_plugin(const std::string& plugin_id);
/**
* Reload a plugin (hot-reload for development)
* @param plugin_id Plugin ID to reload
* @return Result with updated plugin info or error
*/
Result<PluginInfo> reload_plugin(const std::string& plugin_id);
/**
* Get list of all loaded plugins
* @return Vector of plugin information
*/
std::vector<PluginInfo> list_plugins() const;
/**
* Get a specific plugin by ID
* @param plugin_id Plugin ID
* @return Pointer to plugin or nullptr if not found
*/
Plugin* get_plugin(const std::string& plugin_id);
/**
* Get a plugin by ID (const version)
*/
const Plugin* get_plugin(const std::string& plugin_id) const;
// ========================================================================
// Job Routing
// ========================================================================
/**
* Find the best plugin to handle a job
* @param type Job type
* @param params Job parameters
* @return Pointer to plugin or nullptr if none can handle
*/
Plugin* find_plugin_for_job(JobType type, const JobParams& params);
/**
* Get all plugins that can handle a job type
* @param type Job type
* @return Vector of plugin pointers
*/
std::vector<Plugin*> get_plugins_for_type(JobType type);
// ========================================================================
// Built-in Plugins
// ========================================================================
/**
* Register a built-in plugin
* @param plugin Plugin instance (ownership transferred)
* @return Result indicating success or failure
*/
Result<void> register_builtin(std::unique_ptr<Plugin> plugin);
// ========================================================================
// Status
// ========================================================================
/**
* Check if plugin manager is initialized
*/
bool is_initialized() const { return initialized_; }
/**
* Get plugin count
*/
size_t plugin_count() const;
/**
* Run health checks on all plugins
* @return Map of plugin ID to health status
*/
std::map<std::string, bool> health_check() const;
private:
/**
* Scan directory for plugin files
*/
std::vector<std::filesystem::path> scan_plugin_directory(
const std::string& dir
);
/**
* Load plugin from shared library
*/
Result<PluginHandle> load_shared_library(const std::string& path);
/**
* Unload a plugin handle
*/
void unload_handle(PluginHandle& handle);
mutable std::mutex mutex_;
bool initialized_ = false;
std::string plugin_dir_;
std::string config_path_;
// Loaded plugins (by ID)
std::map<std::string, PluginHandle> plugins_;
// Built-in plugins (owned)
std::vector<std::unique_ptr<Plugin>> builtin_plugins_;
// Plugin ID to handle mapping for built-ins
std::map<std::string, Plugin*> builtin_map_;
};
} // namespace media

View File

@@ -0,0 +1,174 @@
#pragma once
#include "media/plugin.hpp"
#include <map>
#include <mutex>
#include <atomic>
namespace media {
namespace plugins {
/**
* FFmpeg Plugin Configuration
*/
struct FFmpegConfig {
std::string ffmpeg_path = "/usr/bin/ffmpeg";
std::string ffprobe_path = "/usr/bin/ffprobe";
// Hardware acceleration
enum class HwAccel {
NONE,
AUTO,
NVIDIA, // NVENC
VAAPI, // Intel/AMD
QSV, // Intel Quick Sync
VIDEOTOOLBOX // macOS
};
HwAccel hardware_accel = HwAccel::AUTO;
// Threading
int threads = 0; // 0 = auto
// Limits
int max_concurrent_jobs = 4;
size_t max_output_size_gb = 50;
};
/**
* FFmpeg Plugin
*
* Built-in plugin for video and audio transcoding using FFmpeg.
* Supports hardware acceleration, multiple codecs, and streaming output.
*/
class FFmpegPlugin : public Plugin {
public:
FFmpegPlugin();
~FFmpegPlugin() override;
// ========================================================================
// Plugin Interface
// ========================================================================
PluginInfo info() const override;
PluginCapabilities capabilities() const override;
Result<void> initialize(const std::string& config_path) override;
void shutdown() override;
bool is_healthy() const override;
bool can_handle(JobType type, const JobParams& params) const override;
Result<std::string> process(
const JobRequest& request,
JobProgressCallback progress_callback
) override;
Result<void> cancel(const std::string& job_id) override;
// Streaming support
Result<std::string> start_stream(
const std::string& channel_id,
const std::map<std::string, std::string>& source,
const std::map<std::string, std::string>& output
) override;
Result<void> stop_stream(const std::string& channel_id) override;
// ========================================================================
// FFmpeg-specific Methods
// ========================================================================
/**
* Get media info using ffprobe
* @param path Path to media file
* @return Result with media info JSON or error
*/
Result<std::string> probe(const std::string& path);
/**
* Get video duration in seconds
*/
Result<double> get_duration(const std::string& path);
/**
* Get available hardware encoders
*/
std::vector<std::string> get_available_encoders();
/**
* Check if codec is available
*/
bool is_codec_available(const std::string& codec);
private:
/**
* Build FFmpeg command for video transcoding
*/
std::vector<std::string> build_video_command(
const VideoTranscodeParams& params
);
/**
* Build FFmpeg command for audio transcoding
*/
std::vector<std::string> build_audio_command(
const AudioTranscodeParams& params
);
/**
* Build FFmpeg command for HLS streaming
*/
std::vector<std::string> build_hls_command(
const std::string& input,
const std::string& output_dir,
const std::map<std::string, std::string>& options
);
/**
* Execute FFmpeg command with progress tracking
*/
Result<void> execute_ffmpeg(
const std::vector<std::string>& args,
const std::string& job_id,
double total_duration,
JobProgressCallback progress_callback
);
/**
* Parse FFmpeg progress output
*/
JobProgress parse_progress(
const std::string& output,
double total_duration
);
/**
* Detect available hardware acceleration
*/
FFmpegConfig::HwAccel detect_hardware_accel();
/**
* Get hardware encoder name for codec
*/
std::string get_hw_encoder(const std::string& codec);
// Configuration
FFmpegConfig config_;
bool initialized_ = false;
// Active jobs/streams
struct ProcessInfo {
int pid = 0;
std::atomic<bool> cancelled{false};
};
mutable std::mutex processes_mutex_;
std::map<std::string, ProcessInfo> active_processes_;
// Cached probe data
mutable std::mutex cache_mutex_;
std::map<std::string, std::string> probe_cache_;
};
} // namespace plugins
} // namespace media

View File

@@ -0,0 +1,314 @@
#pragma once
#include "media/types.hpp"
#include "media/plugin_manager.hpp"
#include <memory>
#include <map>
#include <mutex>
#include <thread>
#include <atomic>
#include <condition_variable>
namespace media {
/**
* Radio Engine Configuration
*/
struct RadioEngineConfig {
// General
int max_channels = 10;
// Default audio settings
int default_bitrate_kbps = 128;
int default_sample_rate = 44100;
int default_channels = 2;
std::string default_codec = "mp3";
// Crossfade
bool crossfade_enabled = true;
int crossfade_duration_ms = 3000;
// Normalization
bool normalization_enabled = true;
double target_lufs = -14.0;
// Output
std::string hls_output_dir = "/data/hls/radio";
int hls_segment_duration = 6;
// Notification callback
NotificationCallback notification_callback;
};
/**
* Internal Radio Channel State
*/
struct RadioChannelState {
RadioChannelConfig config;
RadioChannelStatus status;
// Playlist
std::vector<RadioPlaylistEntry> playlist;
size_t current_index = 0;
// Streaming state
std::atomic<bool> is_running{false};
std::thread stream_thread;
std::condition_variable cv;
std::mutex mutex;
// FFmpeg process handle (or similar)
void* process_handle = nullptr;
// Statistics
std::chrono::system_clock::time_point started_at;
std::atomic<int> listener_count{0};
};
/**
* Radio Engine
*
* Manages radio channel streaming with playlist scheduling,
* crossfading, audio normalization, and multiple output formats.
*/
class RadioEngine {
public:
RadioEngine();
~RadioEngine();
// Disable copying
RadioEngine(const RadioEngine&) = delete;
RadioEngine& operator=(const RadioEngine&) = delete;
// ========================================================================
// Initialization
// ========================================================================
/**
* Initialize the radio engine
* @param config Engine configuration
* @param plugin_manager Plugin manager for audio processing
* @return Result indicating success or failure
*/
Result<void> initialize(
const RadioEngineConfig& config,
PluginManager* plugin_manager
);
/**
* Shutdown all channels and cleanup
*/
void shutdown();
// ========================================================================
// Channel Management
// ========================================================================
/**
* Create a new radio channel
* @param config Channel configuration
* @return Result with channel ID or error
*/
Result<std::string> create_channel(const RadioChannelConfig& config);
/**
* Delete a radio channel
* @param channel_id Channel ID
* @return Result indicating success or failure
*/
Result<void> delete_channel(const std::string& channel_id);
/**
* Update channel configuration
* @param channel_id Channel ID
* @param config New configuration
* @return Result indicating success or failure
*/
Result<void> update_channel(
const std::string& channel_id,
const RadioChannelConfig& config
);
/**
* Get channel status
* @param channel_id Channel ID
* @return Result with channel status or error
*/
Result<RadioChannelStatus> get_channel_status(
const std::string& channel_id
) const;
/**
* List all channels
* @param tenant_id Filter by tenant (empty for all)
* @return Vector of channel statuses
*/
std::vector<RadioChannelStatus> list_channels(
const std::string& tenant_id = ""
) const;
// ========================================================================
// Streaming Control
// ========================================================================
/**
* Start streaming a channel
* @param channel_id Channel ID
* @return Result with stream URL or error
*/
Result<std::string> start_channel(const std::string& channel_id);
/**
* Stop streaming a channel
* @param channel_id Channel ID
* @return Result indicating success or failure
*/
Result<void> stop_channel(const std::string& channel_id);
// ========================================================================
// Playlist Management
// ========================================================================
/**
* Set channel playlist
* @param channel_id Channel ID
* @param tracks List of tracks
* @return Result indicating success or failure
*/
Result<void> set_playlist(
const std::string& channel_id,
const std::vector<RadioTrack>& tracks
);
/**
* Add track to playlist
* @param channel_id Channel ID
* @param track Track to add
* @param position Position in playlist (-1 for end)
* @return Result indicating success or failure
*/
Result<void> add_track(
const std::string& channel_id,
const RadioTrack& track,
int position = -1
);
/**
* Remove track from playlist
* @param channel_id Channel ID
* @param track_id Track ID to remove
* @return Result indicating success or failure
*/
Result<void> remove_track(
const std::string& channel_id,
const std::string& track_id
);
/**
* Skip to next track
* @param channel_id Channel ID
* @return Result indicating success or failure
*/
Result<void> skip_track(const std::string& channel_id);
/**
* Get current playlist
* @param channel_id Channel ID
* @return Result with playlist or error
*/
Result<std::vector<RadioPlaylistEntry>> get_playlist(
const std::string& channel_id
) const;
/**
* Get now playing info
* @param channel_id Channel ID
* @return Result with current track or error
*/
Result<RadioTrack> get_now_playing(
const std::string& channel_id
) const;
// ========================================================================
// Auto-DJ
// ========================================================================
/**
* Enable/disable auto-DJ for a channel
* @param channel_id Channel ID
* @param enabled Enable auto-DJ
* @param folders Folders to scan for music
* @param shuffle Shuffle tracks
* @return Result indicating success or failure
*/
Result<void> set_auto_dj(
const std::string& channel_id,
bool enabled,
const std::vector<std::string>& folders = {},
bool shuffle = true
);
// ========================================================================
// Statistics
// ========================================================================
/**
* Update listener count (called by stream server)
* @param channel_id Channel ID
* @param delta Change in listener count
*/
void update_listener_count(const std::string& channel_id, int delta);
/**
* Get total listener count across all channels
*/
int get_total_listeners() const;
private:
/**
* Stream thread function
*/
void stream_thread(const std::string& channel_id);
/**
* Load next track with crossfade
*/
void load_next_track(RadioChannelState& state);
/**
* Apply audio processing (normalization, effects)
*/
void process_audio(RadioChannelState& state, void* buffer, size_t size);
/**
* Generate HLS segments
*/
void generate_hls_segment(
const std::string& channel_id,
const void* audio_data,
size_t size
);
/**
* Scan folder for audio files
*/
std::vector<RadioTrack> scan_folder(const std::string& folder);
/**
* Get audio metadata from file
*/
RadioTrack get_track_metadata(const std::string& path);
// Configuration
RadioEngineConfig config_;
PluginManager* plugin_manager_ = nullptr;
// State
std::atomic<bool> initialized_{false};
// Channels
mutable std::mutex channels_mutex_;
std::map<std::string, std::unique_ptr<RadioChannelState>> channels_;
};
} // namespace media

View File

@@ -0,0 +1,193 @@
#pragma once
#include "media/types.hpp"
#include "media/plugin_manager.hpp"
#include "media/job_queue.hpp"
#include "media/radio_engine.hpp"
#include "media/tv_engine.hpp"
#include "media/dbal_client.hpp"
#include <memory>
#include <atomic>
#include <string>
namespace media {
/**
* Media Daemon Server Configuration
*/
struct ServerConfig {
// Network
std::string bind_address = "0.0.0.0";
int port = 8090;
int workers = 4;
bool development_mode = false;
// DBAL
DbalClientConfig dbal;
// Job Queue
JobQueueConfig job_queue;
// Radio
bool radio_enabled = true;
RadioEngineConfig radio;
// TV
bool tv_enabled = true;
TvEngineConfig tv;
// Plugins
bool plugins_enabled = true;
std::string plugin_dir = "/plugins";
bool hot_reload = false;
// Security
bool api_keys_enabled = true;
std::string api_key_header = "X-API-Key";
bool rate_limit_enabled = true;
int rate_limit_rpm = 100;
// CORS
bool cors_enabled = true;
std::vector<std::string> cors_origins = {"*"};
// Monitoring
bool prometheus_enabled = true;
std::string prometheus_endpoint = "/metrics";
std::string health_endpoint = "/health";
};
/**
* Media Daemon Server
*
* Main HTTP server for the media processing daemon.
* Uses Drogon framework (same as DBAL daemon).
*/
class Server {
public:
Server();
~Server();
// Disable copying
Server(const Server&) = delete;
Server& operator=(const Server&) = delete;
// ========================================================================
// Lifecycle
// ========================================================================
/**
* Initialize the server
* @param config Server configuration
* @return Result indicating success or failure
*/
Result<void> initialize(const ServerConfig& config);
/**
* Start the server (blocking)
*/
void run();
/**
* Start the server (non-blocking)
*/
void start();
/**
* Stop the server
*/
void stop();
/**
* Check if server is running
*/
bool is_running() const { return running_.load(); }
// ========================================================================
// Component Access
// ========================================================================
PluginManager& plugin_manager() { return *plugin_manager_; }
JobQueue& job_queue() { return *job_queue_; }
RadioEngine& radio_engine() { return *radio_engine_; }
TvEngine& tv_engine() { return *tv_engine_; }
DbalClient& dbal_client() { return *dbal_client_; }
const PluginManager& plugin_manager() const { return *plugin_manager_; }
const JobQueue& job_queue() const { return *job_queue_; }
const RadioEngine& radio_engine() const { return *radio_engine_; }
const TvEngine& tv_engine() const { return *tv_engine_; }
const DbalClient& dbal_client() const { return *dbal_client_; }
private:
/**
* Setup HTTP routes
*/
void setup_routes();
/**
* Setup middleware (auth, rate limiting, CORS)
*/
void setup_middleware();
/**
* Register built-in plugins
*/
Result<void> register_builtin_plugins();
// ========================================================================
// Route Handlers
// ========================================================================
// Health
void handle_health();
void handle_metrics();
// Jobs
void handle_create_job();
void handle_get_job();
void handle_list_jobs();
void handle_cancel_job();
// Radio
void handle_create_radio_channel();
void handle_get_radio_channel();
void handle_list_radio_channels();
void handle_start_radio();
void handle_stop_radio();
void handle_set_playlist();
void handle_get_now_playing();
// TV
void handle_create_tv_channel();
void handle_get_tv_channel();
void handle_list_tv_channels();
void handle_start_tv();
void handle_stop_tv();
void handle_set_schedule();
void handle_get_epg();
// Plugins
void handle_list_plugins();
void handle_reload_plugin();
// Configuration
ServerConfig config_;
// State
std::atomic<bool> initialized_{false};
std::atomic<bool> running_{false};
// Components
std::unique_ptr<PluginManager> plugin_manager_;
std::unique_ptr<JobQueue> job_queue_;
std::unique_ptr<RadioEngine> radio_engine_;
std::unique_ptr<TvEngine> tv_engine_;
std::unique_ptr<DbalClient> dbal_client_;
// Drogon app (pimpl)
struct Impl;
std::unique_ptr<Impl> impl_;
};
} // namespace media

View File

@@ -0,0 +1,396 @@
#pragma once
#include "media/types.hpp"
#include "media/plugin_manager.hpp"
#include <memory>
#include <map>
#include <mutex>
#include <thread>
#include <atomic>
#include <condition_variable>
namespace media {
/**
* TV Engine Configuration
*/
struct TvEngineConfig {
// General
int max_channels = 5;
// Video settings
struct Resolution {
std::string name;
int width;
int height;
int bitrate_kbps;
};
std::vector<Resolution> resolutions = {
{"1080p", 1920, 1080, 5000},
{"720p", 1280, 720, 2500},
{"480p", 854, 480, 1000}
};
std::string default_video_codec = "h264";
std::string video_preset = "fast";
// Audio settings
std::string default_audio_codec = "aac";
int audio_bitrate_kbps = 128;
int audio_sample_rate = 48000;
// HLS settings
std::string hls_output_dir = "/data/hls/tv";
int hls_segment_duration = 4;
int hls_playlist_size = 10;
// EPG settings
int epg_lookahead_hours = 24;
int epg_refresh_interval_minutes = 15;
// Notification callback
NotificationCallback notification_callback;
};
/**
* Internal TV Channel State
*/
struct TvChannelState {
TvChannelConfig config;
TvChannelStatus status;
// Schedule
std::vector<TvScheduleEntry> schedule;
size_t current_program_index = 0;
// Streaming state
std::atomic<bool> is_running{false};
std::thread stream_thread;
std::condition_variable cv;
std::mutex mutex;
// Current playback position
std::chrono::system_clock::time_point playback_position;
// FFmpeg process handles per resolution
std::map<std::string, void*> encoder_handles;
// Statistics
std::chrono::system_clock::time_point started_at;
std::atomic<int> viewer_count{0};
};
/**
* TV Engine
*
* Manages TV channel simulation with scheduling, EPG generation,
* multi-resolution HLS output, and commercial/bumper insertion.
*/
class TvEngine {
public:
TvEngine();
~TvEngine();
// Disable copying
TvEngine(const TvEngine&) = delete;
TvEngine& operator=(const TvEngine&) = delete;
// ========================================================================
// Initialization
// ========================================================================
/**
* Initialize the TV engine
* @param config Engine configuration
* @param plugin_manager Plugin manager for video processing
* @return Result indicating success or failure
*/
Result<void> initialize(
const TvEngineConfig& config,
PluginManager* plugin_manager
);
/**
* Shutdown all channels and cleanup
*/
void shutdown();
// ========================================================================
// Channel Management
// ========================================================================
/**
* Create a new TV channel
* @param config Channel configuration
* @return Result with channel ID or error
*/
Result<std::string> create_channel(const TvChannelConfig& config);
/**
* Delete a TV channel
* @param channel_id Channel ID
* @return Result indicating success or failure
*/
Result<void> delete_channel(const std::string& channel_id);
/**
* Update channel configuration
* @param channel_id Channel ID
* @param config New configuration
* @return Result indicating success or failure
*/
Result<void> update_channel(
const std::string& channel_id,
const TvChannelConfig& config
);
/**
* Get channel status
* @param channel_id Channel ID
* @return Result with channel status or error
*/
Result<TvChannelStatus> get_channel_status(
const std::string& channel_id
) const;
/**
* List all channels
* @param tenant_id Filter by tenant (empty for all)
* @return Vector of channel statuses
*/
std::vector<TvChannelStatus> list_channels(
const std::string& tenant_id = ""
) const;
// ========================================================================
// Streaming Control
// ========================================================================
/**
* Start streaming a channel
* @param channel_id Channel ID
* @return Result with stream URLs or error
*/
struct StreamUrls {
std::string hls_url;
std::string dash_url;
std::map<std::string, std::string> quality_urls; // resolution -> URL
};
Result<StreamUrls> start_channel(const std::string& channel_id);
/**
* Stop streaming a channel
* @param channel_id Channel ID
* @return Result indicating success or failure
*/
Result<void> stop_channel(const std::string& channel_id);
// ========================================================================
// Schedule Management
// ========================================================================
/**
* Set channel schedule
* @param channel_id Channel ID
* @param entries Schedule entries
* @return Result indicating success or failure
*/
Result<void> set_schedule(
const std::string& channel_id,
const std::vector<TvScheduleEntry>& entries
);
/**
* Add program to schedule
* @param channel_id Channel ID
* @param entry Schedule entry
* @return Result indicating success or failure
*/
Result<void> add_program(
const std::string& channel_id,
const TvScheduleEntry& entry
);
/**
* Remove program from schedule
* @param channel_id Channel ID
* @param program_id Program ID
* @return Result indicating success or failure
*/
Result<void> remove_program(
const std::string& channel_id,
const std::string& program_id
);
/**
* Get channel schedule
* @param channel_id Channel ID
* @param start_time Start of time range
* @param end_time End of time range
* @return Result with schedule or error
*/
Result<std::vector<TvScheduleEntry>> get_schedule(
const std::string& channel_id,
std::chrono::system_clock::time_point start_time,
std::chrono::system_clock::time_point end_time
) const;
// ========================================================================
// EPG (Electronic Program Guide)
// ========================================================================
/**
* Generate EPG for all channels
* @param hours_ahead Hours of programming to include
* @return Vector of EPG entries
*/
std::vector<EpgEntry> generate_epg(int hours_ahead = 24) const;
/**
* Generate EPG for specific channel
* @param channel_id Channel ID
* @param hours_ahead Hours of programming
* @return Result with EPG entries or error
*/
Result<std::vector<EpgEntry>> generate_channel_epg(
const std::string& channel_id,
int hours_ahead = 24
) const;
/**
* Export EPG as XMLTV format
* @param hours_ahead Hours of programming
* @return XMLTV formatted string
*/
std::string export_xmltv(int hours_ahead = 24) const;
// ========================================================================
// Now Playing
// ========================================================================
/**
* Get current program
* @param channel_id Channel ID
* @return Result with current program or error
*/
Result<TvProgram> get_now_playing(const std::string& channel_id) const;
/**
* Get next program
* @param channel_id Channel ID
* @return Result with next program or error
*/
Result<TvProgram> get_next_program(const std::string& channel_id) const;
// ========================================================================
// Interstitials (Bumpers/Commercials)
// ========================================================================
/**
* Set channel bumpers
* @param channel_id Channel ID
* @param intro_bumper Path to intro bumper video
* @param outro_bumper Path to outro bumper video
* @return Result indicating success or failure
*/
Result<void> set_bumpers(
const std::string& channel_id,
const std::string& intro_bumper,
const std::string& outro_bumper
);
/**
* Set commercial break playlist
* @param channel_id Channel ID
* @param commercials List of commercial video paths
* @param break_duration_seconds Target break duration
* @return Result indicating success or failure
*/
Result<void> set_commercials(
const std::string& channel_id,
const std::vector<std::string>& commercials,
int break_duration_seconds = 120
);
// ========================================================================
// Statistics
// ========================================================================
/**
* Update viewer count (called by stream server)
* @param channel_id Channel ID
* @param delta Change in viewer count
*/
void update_viewer_count(const std::string& channel_id, int delta);
/**
* Get total viewer count across all channels
*/
int get_total_viewers() const;
private:
/**
* Stream thread function
*/
void stream_thread(const std::string& channel_id);
/**
* Get current program based on schedule
*/
const TvScheduleEntry* get_current_scheduled_program(
const TvChannelState& state
) const;
/**
* Prepare next segment
*/
void prepare_next_segment(TvChannelState& state);
/**
* Encode video segment
*/
void encode_segment(
TvChannelState& state,
const std::string& input_path,
double start_time,
double duration
);
/**
* Generate HLS master playlist
*/
void generate_master_playlist(const std::string& channel_id);
/**
* Update variant playlists
*/
void update_variant_playlist(
const std::string& channel_id,
const std::string& resolution,
const std::string& segment_filename
);
/**
* Insert bumper/commercial
*/
void insert_interstitial(
TvChannelState& state,
const std::string& video_path
);
// Configuration
TvEngineConfig config_;
PluginManager* plugin_manager_ = nullptr;
// State
std::atomic<bool> initialized_{false};
// Channels
mutable std::mutex channels_mutex_;
std::map<std::string, std::unique_ptr<TvChannelState>> channels_;
// EPG refresh thread
std::thread epg_thread_;
std::atomic<bool> epg_running_{false};
};
} // namespace media

View File

@@ -0,0 +1,407 @@
#pragma once
#include <string>
#include <vector>
#include <memory>
#include <functional>
#include <chrono>
#include <optional>
#include <variant>
#include <map>
namespace media {
// Forward declarations
class Plugin;
class Job;
class RadioChannel;
class TvChannel;
// ============================================================================
// Error Handling (following DBAL patterns)
// ============================================================================
enum class ErrorCode {
OK = 0,
NOT_FOUND = 404,
CONFLICT = 409,
UNAUTHORIZED = 401,
FORBIDDEN = 403,
VALIDATION_ERROR = 422,
INTERNAL_ERROR = 500,
SERVICE_UNAVAILABLE = 503,
TIMEOUT = 504,
PLUGIN_ERROR = 1001,
TRANSCODE_ERROR = 1002,
STREAM_ERROR = 1003,
STORAGE_ERROR = 1004
};
template<typename T>
class Result {
public:
static Result<T> ok(T value) {
Result<T> r;
r.value_ = std::move(value);
r.error_code_ = ErrorCode::OK;
return r;
}
static Result<T> error(ErrorCode code, const std::string& message) {
Result<T> r;
r.error_code_ = code;
r.error_message_ = message;
return r;
}
bool is_ok() const { return error_code_ == ErrorCode::OK; }
bool is_error() const { return error_code_ != ErrorCode::OK; }
const T& value() const { return value_; }
T& value() { return value_; }
ErrorCode error_code() const { return error_code_; }
const std::string& error_message() const { return error_message_; }
private:
T value_;
ErrorCode error_code_ = ErrorCode::OK;
std::string error_message_;
};
// Specialization for void
template<>
class Result<void> {
public:
static Result<void> ok() {
Result<void> r;
r.error_code_ = ErrorCode::OK;
return r;
}
static Result<void> error(ErrorCode code, const std::string& message) {
Result<void> r;
r.error_code_ = code;
r.error_message_ = message;
return r;
}
bool is_ok() const { return error_code_ == ErrorCode::OK; }
bool is_error() const { return error_code_ != ErrorCode::OK; }
ErrorCode error_code() const { return error_code_; }
const std::string& error_message() const { return error_message_; }
private:
ErrorCode error_code_ = ErrorCode::OK;
std::string error_message_;
};
// ============================================================================
// Job Types
// ============================================================================
enum class JobType {
VIDEO_TRANSCODE,
AUDIO_TRANSCODE,
DOCUMENT_CONVERT,
IMAGE_PROCESS,
RADIO_INGEST,
TV_SEGMENT,
CUSTOM
};
enum class JobStatus {
PENDING,
QUEUED,
PROCESSING,
COMPLETED,
FAILED,
CANCELLED
};
enum class JobPriority {
URGENT = 0,
HIGH = 1,
NORMAL = 2,
LOW = 3,
BACKGROUND = 4
};
struct JobProgress {
double percent = 0.0; // 0-100
std::string stage; // Current stage (e.g., "encoding", "muxing")
std::string eta; // Estimated time remaining
size_t bytes_processed = 0;
size_t bytes_total = 0;
};
struct VideoTranscodeParams {
std::string input_path;
std::string output_path;
std::string codec = "h264"; // h264, h265, vp9, av1
int width = 0; // 0 = auto
int height = 0;
int bitrate_kbps = 0; // 0 = auto
std::string preset = "fast";
std::string audio_codec = "aac";
int audio_bitrate_kbps = 128;
std::map<std::string, std::string> extra_params;
};
struct AudioTranscodeParams {
std::string input_path;
std::string output_path;
std::string codec = "mp3"; // mp3, aac, flac, opus
int bitrate_kbps = 128;
int sample_rate = 44100;
int channels = 2;
bool normalize = true;
double target_lufs = -14.0;
};
struct DocumentConvertParams {
std::string input_path;
std::string output_path;
std::string output_format; // pdf, docx, html, etc.
std::string template_path;
std::map<std::string, std::string> variables;
};
struct ImageProcessParams {
std::string input_path;
std::string output_path;
std::string format; // jpg, png, webp, avif
int width = 0;
int height = 0;
int quality = 85;
bool preserve_aspect = true;
std::vector<std::string> filters; // blur, sharpen, etc.
};
using JobParams = std::variant<
VideoTranscodeParams,
AudioTranscodeParams,
DocumentConvertParams,
ImageProcessParams,
std::map<std::string, std::string> // Custom params
>;
struct JobRequest {
std::string id; // Auto-generated if empty
std::string tenant_id;
std::string user_id;
JobType type;
JobPriority priority = JobPriority::NORMAL;
JobParams params;
std::string callback_url; // Webhook on completion
bool notify_user = true; // Send DBAL notification
std::map<std::string, std::string> metadata;
};
struct JobInfo {
std::string id;
std::string tenant_id;
std::string user_id;
JobType type;
JobStatus status;
JobPriority priority;
JobProgress progress;
std::chrono::system_clock::time_point created_at;
std::chrono::system_clock::time_point started_at;
std::chrono::system_clock::time_point completed_at;
std::string error_message;
std::string output_path;
std::map<std::string, std::string> metadata;
};
// ============================================================================
// Radio Types
// ============================================================================
struct RadioTrack {
std::string id;
std::string path;
std::string title;
std::string artist;
std::string album;
std::string artwork_url;
int duration_ms = 0;
std::map<std::string, std::string> metadata;
};
struct RadioPlaylistEntry {
RadioTrack track;
std::chrono::system_clock::time_point scheduled_at;
bool played = false;
};
struct RadioChannelConfig {
std::string id;
std::string tenant_id;
std::string name;
std::string description;
std::string artwork_url;
// Audio settings
int bitrate_kbps = 128;
std::string codec = "mp3";
int sample_rate = 44100;
// Crossfade
bool crossfade_enabled = true;
int crossfade_ms = 3000;
// Auto-DJ settings
bool auto_dj_enabled = true;
std::vector<std::string> auto_dj_folders;
bool shuffle = true;
};
struct RadioChannelStatus {
std::string id;
std::string name;
bool is_live = false;
int listeners = 0;
std::optional<RadioTrack> now_playing;
std::optional<RadioTrack> next_track;
int uptime_seconds = 0;
std::string stream_url;
};
// ============================================================================
// TV Channel Types
// ============================================================================
struct TvProgram {
std::string id;
std::string title;
std::string description;
std::string category; // movie, series, news, sports, etc.
std::string content_path; // Video file or playlist
int duration_seconds = 0;
std::string thumbnail_url;
std::string rating; // G, PG, PG-13, R, etc.
std::map<std::string, std::string> metadata;
};
struct TvScheduleEntry {
TvProgram program;
std::chrono::system_clock::time_point start_time;
std::chrono::system_clock::time_point end_time;
bool is_live = false;
std::string bumper_before; // Pre-roll video
std::string bumper_after; // Post-roll video
};
struct TvChannelConfig {
std::string id;
std::string tenant_id;
std::string name;
std::string description;
std::string logo_url;
int channel_number = 0;
// Video settings
std::vector<std::string> resolutions; // 1080p, 720p, 480p
std::string codec = "h264";
// HLS settings
int segment_duration_seconds = 4;
int playlist_size = 10;
// Filler content (when nothing scheduled)
std::string filler_playlist;
std::string offline_image;
};
struct TvChannelStatus {
std::string id;
std::string name;
int channel_number;
bool is_live = false;
std::optional<TvProgram> now_playing;
std::optional<TvProgram> next_program;
int viewers = 0;
std::string hls_url;
std::string dash_url;
};
struct EpgEntry {
std::string channel_id;
std::string channel_name;
TvProgram program;
std::chrono::system_clock::time_point start_time;
std::chrono::system_clock::time_point end_time;
};
// ============================================================================
// Plugin Types
// ============================================================================
enum class PluginType {
TRANSCODER, // Video/audio transcoding
PROCESSOR, // Image/document processing
STREAMER, // Streaming output
ANALYZER, // Media analysis
CUSTOM
};
struct PluginInfo {
std::string id;
std::string name;
std::string version;
std::string author;
std::string description;
PluginType type;
std::vector<std::string> supported_formats;
std::vector<std::string> capabilities;
bool is_loaded = false;
bool is_builtin = false;
};
struct PluginCapabilities {
bool supports_video = false;
bool supports_audio = false;
bool supports_image = false;
bool supports_document = false;
bool supports_streaming = false;
bool supports_hardware_accel = false;
std::vector<std::string> input_formats;
std::vector<std::string> output_formats;
};
// ============================================================================
// Notification Types (for DBAL integration)
// ============================================================================
enum class NotificationType {
JOB_STARTED,
JOB_PROGRESS,
JOB_COMPLETED,
JOB_FAILED,
STREAM_STARTED,
STREAM_STOPPED,
CHANNEL_LIVE,
CHANNEL_OFFLINE
};
struct Notification {
std::string tenant_id;
std::string user_id;
NotificationType type;
std::string title;
std::string message;
std::string icon; // success, error, warning, info
std::map<std::string, std::string> data;
};
// ============================================================================
// Callback Types
// ============================================================================
using JobProgressCallback = std::function<void(const std::string& job_id, const JobProgress& progress)>;
using JobCompletionCallback = std::function<void(const std::string& job_id, bool success, const std::string& result)>;
using NotificationCallback = std::function<void(const Notification& notification)>;
} // namespace media

View File

@@ -0,0 +1,152 @@
# Media Job Entity Schema
# Following DBAL YAML-first design pattern
entity: MediaJob
version: "1.0"
description: "Media processing job for transcoding, conversion, and streaming"
fields:
id:
type: uuid
primary: true
generated: true
description: "Unique job identifier"
tenant_id:
type: uuid
required: true
indexed: true
description: "Tenant for multi-tenancy isolation"
user_id:
type: uuid
required: true
indexed: true
description: "User who submitted the job"
type:
type: enum
values: [video_transcode, audio_transcode, document_convert, image_process, radio_ingest, tv_segment, custom]
required: true
indexed: true
description: "Type of media job"
status:
type: enum
values: [pending, queued, processing, completed, failed, cancelled]
default: pending
indexed: true
description: "Current job status"
priority:
type: enum
values: [urgent, high, normal, low, background]
default: normal
description: "Job priority level"
params:
type: json
required: true
description: "Job-specific parameters (codec, bitrate, etc.)"
progress:
type: json
description: "Progress tracking (percent, stage, eta)"
input_path:
type: string
required: true
max_length: 1024
description: "Path to input file(s)"
output_path:
type: string
max_length: 1024
description: "Path to output file(s)"
error_message:
type: string
max_length: 4096
description: "Error message if job failed"
callback_url:
type: string
max_length: 512
description: "Webhook URL for completion notification"
notify_user:
type: boolean
default: true
description: "Send DBAL notification on completion"
metadata:
type: json
description: "Additional metadata"
created_at:
type: timestamp
auto_now_add: true
indexed: true
description: "Job creation timestamp"
started_at:
type: timestamp
description: "Job start timestamp"
completed_at:
type: timestamp
description: "Job completion timestamp"
duration_ms:
type: integer
description: "Processing duration in milliseconds"
indexes:
- fields: [tenant_id, user_id]
name: idx_job_tenant_user
- fields: [tenant_id, status]
name: idx_job_tenant_status
- fields: [status, priority, created_at]
name: idx_job_queue
acl:
create:
min_level: 1 # User level
read:
self: true
min_level: 2 # Moderator can see all
update:
self: true
min_level: 3 # Admin
delete:
min_level: 3 # Admin
operations:
submit:
description: "Submit a new job"
input:
- type
- params
- priority
- callback_url
- notify_user
- metadata
output: [id, status, created_at]
cancel:
description: "Cancel a pending or processing job"
input: [id]
output: [status]
retry:
description: "Retry a failed job"
input: [id]
output: [id, status]
list_pending:
description: "List pending jobs for a tenant"
input:
- tenant_id
- limit?
- offset?
output: [id, type, status, priority, created_at]

View File

@@ -0,0 +1,235 @@
#include <iostream>
#include <string>
#include <csignal>
#include <memory>
#include <fstream>
// Cross-platform signal handling
#ifdef _WIN32
#include <windows.h>
#ifndef SIGTERM
#define SIGTERM SIGBREAK
#endif
#endif
#include "media/server.hpp"
namespace {
std::unique_ptr<media::Server> server_instance;
void signal_handler(int signal) {
if (signal == SIGINT || signal == SIGTERM) {
std::cout << "\nShutting down Media Daemon..." << std::endl;
if (server_instance) {
server_instance->stop();
}
}
}
void print_banner() {
std::cout << R"(
MetaBuilder Media Daemon
v1.0.0
Job Queue Video, Audio, Document, Image Processing
Radio Streaming, Auto-DJ, Crossfade
TV Channels Schedule, EPG, HLS/DASH Output
Plugins FFmpeg, ImageMagick, Pandoc, Custom
)" << std::endl;
}
void print_help(const char* program) {
std::cout << "Usage: " << program << " [options]" << std::endl;
std::cout << std::endl;
std::cout << "Options:" << std::endl;
std::cout << " --config <file> Configuration file (default: /etc/media-daemon/config.yaml)" << std::endl;
std::cout << " --bind <address> Bind address (default: 0.0.0.0)" << std::endl;
std::cout << " --port <port> Port number (default: 8090)" << std::endl;
std::cout << " --workers <n> Number of HTTP workers (default: 4)" << std::endl;
std::cout << " --dbal-url <url> DBAL daemon URL (default: http://localhost:8080)" << std::endl;
std::cout << " --plugin-dir <dir> Plugin directory (default: /plugins)" << std::endl;
std::cout << " --dev Enable development mode (hot-reload, verbose logging)" << std::endl;
std::cout << " --daemon, -d Run in daemon mode" << std::endl;
std::cout << " --help, -h Show this help message" << std::endl;
std::cout << std::endl;
std::cout << "Environment variables:" << std::endl;
std::cout << " MEDIA_BIND_ADDRESS Bind address" << std::endl;
std::cout << " MEDIA_PORT Port number" << std::endl;
std::cout << " MEDIA_WORKERS HTTP worker threads" << std::endl;
std::cout << " DBAL_URL DBAL daemon URL" << std::endl;
std::cout << " DBAL_API_KEY DBAL API key" << std::endl;
std::cout << " MEDIA_PLUGIN_DIR Plugin directory" << std::endl;
std::cout << " MEDIA_DEV_MODE Development mode (true/false)" << std::endl;
std::cout << std::endl;
}
std::string get_env(const char* name, const std::string& default_value = "") {
const char* value = std::getenv(name);
return value ? value : default_value;
}
int get_env_int(const char* name, int default_value) {
const char* value = std::getenv(name);
if (value) {
try {
return std::stoi(value);
} catch (...) {}
}
return default_value;
}
bool get_env_bool(const char* name, bool default_value = false) {
const char* value = std::getenv(name);
if (value) {
std::string str = value;
return str == "true" || str == "1" || str == "yes";
}
return default_value;
}
}
int main(int argc, char* argv[]) {
print_banner();
// Register signal handlers
std::signal(SIGINT, signal_handler);
std::signal(SIGTERM, signal_handler);
// Default configuration
media::ServerConfig config;
std::string config_file = "/etc/media-daemon/config.yaml";
bool daemon_mode = false;
// Load from environment variables
config.bind_address = get_env("MEDIA_BIND_ADDRESS", "0.0.0.0");
config.port = get_env_int("MEDIA_PORT", 8090);
config.workers = get_env_int("MEDIA_WORKERS", 4);
config.development_mode = get_env_bool("MEDIA_DEV_MODE", false);
config.plugin_dir = get_env("MEDIA_PLUGIN_DIR", "/plugins");
// DBAL settings
config.dbal.url = get_env("DBAL_URL", "http://localhost:8080");
config.dbal.api_key = get_env("DBAL_API_KEY", "");
// Job queue settings
config.job_queue.video_workers = get_env_int("MEDIA_VIDEO_WORKERS", 2);
config.job_queue.audio_workers = get_env_int("MEDIA_AUDIO_WORKERS", 4);
config.job_queue.document_workers = get_env_int("MEDIA_DOC_WORKERS", 4);
config.job_queue.image_workers = get_env_int("MEDIA_IMAGE_WORKERS", 8);
config.job_queue.temp_dir = get_env("MEDIA_TEMP_DIR", "/data/temp");
config.job_queue.output_dir = get_env("MEDIA_OUTPUT_DIR", "/data/output");
// Radio settings
config.radio_enabled = get_env_bool("MEDIA_RADIO_ENABLED", true);
config.radio.max_channels = get_env_int("MEDIA_RADIO_MAX_CHANNELS", 10);
config.radio.hls_output_dir = get_env("MEDIA_RADIO_HLS_DIR", "/data/hls/radio");
// TV settings
config.tv_enabled = get_env_bool("MEDIA_TV_ENABLED", true);
config.tv.max_channels = get_env_int("MEDIA_TV_MAX_CHANNELS", 5);
config.tv.hls_output_dir = get_env("MEDIA_TV_HLS_DIR", "/data/hls/tv");
// Parse command line arguments
for (int i = 1; i < argc; i++) {
std::string arg = argv[i];
if (arg == "--config" && i + 1 < argc) {
config_file = argv[++i];
} else if (arg == "--bind" && i + 1 < argc) {
config.bind_address = argv[++i];
} else if (arg == "--port" && i + 1 < argc) {
config.port = std::stoi(argv[++i]);
} else if (arg == "--workers" && i + 1 < argc) {
config.workers = std::stoi(argv[++i]);
} else if (arg == "--dbal-url" && i + 1 < argc) {
config.dbal.url = argv[++i];
} else if (arg == "--plugin-dir" && i + 1 < argc) {
config.plugin_dir = argv[++i];
} else if (arg == "--dev") {
config.development_mode = true;
config.hot_reload = true;
} else if (arg == "--daemon" || arg == "-d") {
daemon_mode = true;
} else if (arg == "--help" || arg == "-h") {
print_help(argv[0]);
return 0;
} else {
std::cerr << "Unknown option: " << arg << std::endl;
print_help(argv[0]);
return 1;
}
}
// Print configuration summary
std::cout << "Configuration:" << std::endl;
std::cout << " Bind Address: " << config.bind_address << std::endl;
std::cout << " Port: " << config.port << std::endl;
std::cout << " Workers: " << config.workers << std::endl;
std::cout << " DBAL URL: " << config.dbal.url << std::endl;
std::cout << " Plugin Dir: " << config.plugin_dir << std::endl;
std::cout << " Development Mode: " << (config.development_mode ? "yes" : "no") << std::endl;
std::cout << " Radio Enabled: " << (config.radio_enabled ? "yes" : "no") << std::endl;
std::cout << " TV Enabled: " << (config.tv_enabled ? "yes" : "no") << std::endl;
std::cout << std::endl;
// Create and initialize server
server_instance = std::make_unique<media::Server>();
auto result = server_instance->initialize(config);
if (result.is_error()) {
std::cerr << "Failed to initialize server: " << result.error_message() << std::endl;
return 1;
}
std::cout << "Server initialized successfully" << std::endl;
std::cout << "Starting HTTP server on " << config.bind_address << ":" << config.port << std::endl;
std::cout << std::endl;
// Print available endpoints
std::cout << "Endpoints:" << std::endl;
std::cout << " Health: GET /health" << std::endl;
std::cout << " Metrics: GET /metrics" << std::endl;
std::cout << std::endl;
std::cout << " Jobs:" << std::endl;
std::cout << " POST /api/jobs - Submit job" << std::endl;
std::cout << " GET /api/jobs - List jobs" << std::endl;
std::cout << " GET /api/jobs/:id - Get job status" << std::endl;
std::cout << " DELETE /api/jobs/:id - Cancel job" << std::endl;
std::cout << std::endl;
std::cout << " Radio:" << std::endl;
std::cout << " POST /api/radio/channels - Create channel" << std::endl;
std::cout << " GET /api/radio/channels - List channels" << std::endl;
std::cout << " GET /api/radio/channels/:id - Get channel" << std::endl;
std::cout << " POST /api/radio/channels/:id/start - Start streaming" << std::endl;
std::cout << " POST /api/radio/channels/:id/stop - Stop streaming" << std::endl;
std::cout << " GET /api/radio/channels/:id/now - Now playing" << std::endl;
std::cout << std::endl;
std::cout << " TV:" << std::endl;
std::cout << " POST /api/tv/channels - Create channel" << std::endl;
std::cout << " GET /api/tv/channels - List channels" << std::endl;
std::cout << " GET /api/tv/channels/:id - Get channel" << std::endl;
std::cout << " POST /api/tv/channels/:id/start - Start streaming" << std::endl;
std::cout << " POST /api/tv/channels/:id/stop - Stop streaming" << std::endl;
std::cout << " GET /api/tv/channels/:id/schedule - Get EPG" << std::endl;
std::cout << " GET /api/tv/epg - Full EPG (XMLTV)" << std::endl;
std::cout << std::endl;
std::cout << " Plugins:" << std::endl;
std::cout << " GET /api/plugins - List plugins" << std::endl;
std::cout << " POST /api/plugins/:id/reload - Reload plugin (dev)" << std::endl;
std::cout << std::endl;
if (daemon_mode) {
std::cout << "Running in daemon mode..." << std::endl;
} else {
std::cout << "Press Ctrl+C to stop" << std::endl;
}
std::cout << std::endl;
// Run server (blocking)
server_instance->run();
std::cout << "Media Daemon stopped" << std::endl;
return 0;
}