From b20f2d2533e2ac02a4fdf333959e0bfee60e4bee Mon Sep 17 00:00:00 2001 From: JohnDoe6345789 Date: Tue, 30 Dec 2025 11:40:25 +0000 Subject: [PATCH] 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. --- fakemui/icons/AlertCircle.tsx | 37 ++ fakemui/icons/AlertTriangle.tsx | 37 ++ fakemui/icons/Broadcast.tsx | 12 + fakemui/icons/Camera.tsx | 9 + fakemui/icons/ChartLine.tsx | 38 ++ fakemui/icons/Edit.tsx | 37 ++ fakemui/icons/File.tsx | 9 + fakemui/icons/Grid.tsx | 11 + fakemui/icons/Image.tsx | 10 + fakemui/icons/Inbox.tsx | 36 ++ fakemui/icons/List.tsx | 10 + fakemui/icons/Lock.tsx | 38 ++ fakemui/icons/Maximize.tsx | 11 + fakemui/icons/Minimize.tsx | 11 + fakemui/icons/Pause.tsx | 8 + fakemui/icons/Refresh.tsx | 9 + fakemui/icons/Screen.tsx | 10 + fakemui/icons/ShieldCheck.tsx | 36 ++ fakemui/icons/Smile.tsx | 38 ++ fakemui/icons/Stop.tsx | 8 + fakemui/icons/Video.tsx | 9 + fakemui/icons/Volume.tsx | 11 + fakemui/icons/VolumeOff.tsx | 11 + fakemui/icons/index.ts | 25 ++ .../seed/scripts/cache_operations.lua | 58 ++- .../dbal_demo/seed/scripts/connection.lua | 61 ++- packages/dbal_demo/seed/scripts/init.lua | 25 ++ .../dbal_demo/seed/scripts/kv_operations.lua | 62 ++- .../forum_forge/seed/scripts/can_moderate.lua | 14 + .../forum_forge/seed/scripts/can_post.lua | 9 + .../forum_forge/seed/scripts/flag_post.lua | 33 ++ packages/forum_forge/seed/scripts/init.lua | 36 +- .../forum_forge/seed/scripts/moderation.lua | 42 +- .../forum_forge/seed/scripts/permissions.lua | 27 +- .../forum_forge/seed/scripts/rank_thread.lua | 13 + .../forum_forge/seed/scripts/thread_rank.lua | 20 +- packages/forum_forge/seed/scripts/types.lua | 55 +++ packages/irc_webchat/seed/scripts/init.lua | 16 + packages/irc_webchat/seed/scripts/types.lua | 46 ++ .../seed/scripts/calculate_total.lua | 15 + .../seed/scripts/get_severity_class.lua | 16 + .../notification_center/seed/scripts/init.lua | 40 +- .../notification_center/seed/scripts/list.lua | 63 +-- .../seed/scripts/prepare_summary.lua | 48 +++ .../scripts/render_notification_badge.lua | 15 + .../seed/scripts/render_notification_item.lua | 18 + .../seed/scripts/render_notification_list.lua | 17 + .../seed/scripts/summary.lua | 101 +---- .../seed/scripts/toast.lua | 66 +-- .../seed/scripts/toast_error.lua | 15 + .../seed/scripts/toast_info.lua | 15 + .../seed/scripts/toast_success.lua | 15 + .../seed/scripts/toast_warning.lua | 15 + .../seed/scripts/types.lua | 80 ++++ .../seed/scripts/lua/permissions.lua | 14 + services/media_daemon/Dockerfile | 110 +++++ services/media_daemon/README.md | 115 +++++ .../media_daemon/build-config/CMakeLists.txt | 159 +++++++ .../media_daemon/build-config/conanfile.txt | 15 + .../media_daemon/config/media-daemon.yaml | 197 +++++++++ .../media_daemon/config/nginx-stream.conf | 91 ++++ services/media_daemon/docker-compose.yml | 190 ++++++++ .../include/media/dbal_client.hpp | 251 +++++++++++ .../media_daemon/include/media/job_queue.hpp | 259 +++++++++++ .../media_daemon/include/media/plugin.hpp | 191 ++++++++ .../include/media/plugin_manager.hpp | 174 ++++++++ .../include/media/plugins/ffmpeg_plugin.hpp | 174 ++++++++ .../include/media/radio_engine.hpp | 314 ++++++++++++++ .../media_daemon/include/media/server.hpp | 193 +++++++++ .../media_daemon/include/media/tv_engine.hpp | 396 +++++++++++++++++ services/media_daemon/include/media/types.hpp | 407 ++++++++++++++++++ .../schema/entities/media_job.yaml | 152 +++++++ services/media_daemon/src/main.cpp | 235 ++++++++++ 73 files changed, 4854 insertions(+), 310 deletions(-) create mode 100644 fakemui/icons/AlertCircle.tsx create mode 100644 fakemui/icons/AlertTriangle.tsx create mode 100644 fakemui/icons/Broadcast.tsx create mode 100644 fakemui/icons/Camera.tsx create mode 100644 fakemui/icons/ChartLine.tsx create mode 100644 fakemui/icons/Edit.tsx create mode 100644 fakemui/icons/File.tsx create mode 100644 fakemui/icons/Grid.tsx create mode 100644 fakemui/icons/Image.tsx create mode 100644 fakemui/icons/Inbox.tsx create mode 100644 fakemui/icons/List.tsx create mode 100644 fakemui/icons/Lock.tsx create mode 100644 fakemui/icons/Maximize.tsx create mode 100644 fakemui/icons/Minimize.tsx create mode 100644 fakemui/icons/Pause.tsx create mode 100644 fakemui/icons/Refresh.tsx create mode 100644 fakemui/icons/Screen.tsx create mode 100644 fakemui/icons/ShieldCheck.tsx create mode 100644 fakemui/icons/Smile.tsx create mode 100644 fakemui/icons/Stop.tsx create mode 100644 fakemui/icons/Video.tsx create mode 100644 fakemui/icons/Volume.tsx create mode 100644 fakemui/icons/VolumeOff.tsx create mode 100644 packages/forum_forge/seed/scripts/can_moderate.lua create mode 100644 packages/forum_forge/seed/scripts/can_post.lua create mode 100644 packages/forum_forge/seed/scripts/flag_post.lua create mode 100644 packages/forum_forge/seed/scripts/rank_thread.lua create mode 100644 packages/forum_forge/seed/scripts/types.lua create mode 100644 packages/irc_webchat/seed/scripts/types.lua create mode 100644 packages/notification_center/seed/scripts/calculate_total.lua create mode 100644 packages/notification_center/seed/scripts/get_severity_class.lua create mode 100644 packages/notification_center/seed/scripts/prepare_summary.lua create mode 100644 packages/notification_center/seed/scripts/render_notification_badge.lua create mode 100644 packages/notification_center/seed/scripts/render_notification_item.lua create mode 100644 packages/notification_center/seed/scripts/render_notification_list.lua create mode 100644 packages/notification_center/seed/scripts/toast_error.lua create mode 100644 packages/notification_center/seed/scripts/toast_info.lua create mode 100644 packages/notification_center/seed/scripts/toast_success.lua create mode 100644 packages/notification_center/seed/scripts/toast_warning.lua create mode 100644 packages/notification_center/seed/scripts/types.lua create mode 100644 services/media_daemon/Dockerfile create mode 100644 services/media_daemon/README.md create mode 100644 services/media_daemon/build-config/CMakeLists.txt create mode 100644 services/media_daemon/build-config/conanfile.txt create mode 100644 services/media_daemon/config/media-daemon.yaml create mode 100644 services/media_daemon/config/nginx-stream.conf create mode 100644 services/media_daemon/docker-compose.yml create mode 100644 services/media_daemon/include/media/dbal_client.hpp create mode 100644 services/media_daemon/include/media/job_queue.hpp create mode 100644 services/media_daemon/include/media/plugin.hpp create mode 100644 services/media_daemon/include/media/plugin_manager.hpp create mode 100644 services/media_daemon/include/media/plugins/ffmpeg_plugin.hpp create mode 100644 services/media_daemon/include/media/radio_engine.hpp create mode 100644 services/media_daemon/include/media/server.hpp create mode 100644 services/media_daemon/include/media/tv_engine.hpp create mode 100644 services/media_daemon/include/media/types.hpp create mode 100644 services/media_daemon/schema/entities/media_job.yaml create mode 100644 services/media_daemon/src/main.cpp diff --git a/fakemui/icons/AlertCircle.tsx b/fakemui/icons/AlertCircle.tsx new file mode 100644 index 000000000..9abf77964 --- /dev/null +++ b/fakemui/icons/AlertCircle.tsx @@ -0,0 +1,37 @@ +import React from 'react' +import type { IconProps } from './types' + +export const AlertCircle: React.FC = ({ + size = 24, + color = 'currentColor', + weight = 'regular', + className = '', + ...props +}) => { + const strokeWidths = { + thin: 1, + light: 1.5, + regular: 2, + bold: 2.5, + } + + return ( + + + + + + ) +} diff --git a/fakemui/icons/AlertTriangle.tsx b/fakemui/icons/AlertTriangle.tsx new file mode 100644 index 000000000..687bc5c2a --- /dev/null +++ b/fakemui/icons/AlertTriangle.tsx @@ -0,0 +1,37 @@ +import React from 'react' +import type { IconProps } from './types' + +export const AlertTriangle: React.FC = ({ + size = 24, + color = 'currentColor', + weight = 'regular', + className = '', + ...props +}) => { + const strokeWidths = { + thin: 1, + light: 1.5, + regular: 2, + bold: 2.5, + } + + return ( + + + + + + ) +} diff --git a/fakemui/icons/Broadcast.tsx b/fakemui/icons/Broadcast.tsx new file mode 100644 index 000000000..7f942a40c --- /dev/null +++ b/fakemui/icons/Broadcast.tsx @@ -0,0 +1,12 @@ +import React from 'react' +import { Icon, IconProps } from './Icon' + +export const Broadcast = (props: IconProps) => ( + + + + + + + +) diff --git a/fakemui/icons/Camera.tsx b/fakemui/icons/Camera.tsx new file mode 100644 index 000000000..9f89ea2f3 --- /dev/null +++ b/fakemui/icons/Camera.tsx @@ -0,0 +1,9 @@ +import React from 'react' +import { Icon, IconProps } from './Icon' + +export const Camera = (props: IconProps) => ( + + + + +) diff --git a/fakemui/icons/ChartLine.tsx b/fakemui/icons/ChartLine.tsx new file mode 100644 index 000000000..b929a0fac --- /dev/null +++ b/fakemui/icons/ChartLine.tsx @@ -0,0 +1,38 @@ +import React from 'react' +import type { IconProps } from './types' + +export const ChartLine: React.FC = ({ + size = 24, + color = 'currentColor', + weight = 'regular', + className = '', + ...props +}) => { + const strokeWidths = { + thin: 1, + light: 1.5, + regular: 2, + bold: 2.5, + } + + return ( + + + + + + + ) +} diff --git a/fakemui/icons/Edit.tsx b/fakemui/icons/Edit.tsx new file mode 100644 index 000000000..c67a98d79 --- /dev/null +++ b/fakemui/icons/Edit.tsx @@ -0,0 +1,37 @@ +import React from 'react' +import type { IconProps } from './types' + +export const Edit: React.FC = ({ + size = 24, + color = 'currentColor', + weight = 'regular', + className = '', + ...props +}) => { + const strokeWidths = { + thin: 1, + light: 1.5, + regular: 2, + bold: 2.5, + } + + return ( + + + + + + ) +} diff --git a/fakemui/icons/File.tsx b/fakemui/icons/File.tsx new file mode 100644 index 000000000..a3a434ace --- /dev/null +++ b/fakemui/icons/File.tsx @@ -0,0 +1,9 @@ +import React from 'react' +import { Icon, IconProps } from './Icon' + +export const File = (props: IconProps) => ( + + + + +) diff --git a/fakemui/icons/Grid.tsx b/fakemui/icons/Grid.tsx new file mode 100644 index 000000000..cf155b7e0 --- /dev/null +++ b/fakemui/icons/Grid.tsx @@ -0,0 +1,11 @@ +import React from 'react' +import { Icon, IconProps } from './Icon' + +export const Grid = (props: IconProps) => ( + + + + + + +) diff --git a/fakemui/icons/Image.tsx b/fakemui/icons/Image.tsx new file mode 100644 index 000000000..e79a0c0bd --- /dev/null +++ b/fakemui/icons/Image.tsx @@ -0,0 +1,10 @@ +import React from 'react' +import { Icon, IconProps } from './Icon' + +export const Image = (props: IconProps) => ( + + + + + +) diff --git a/fakemui/icons/Inbox.tsx b/fakemui/icons/Inbox.tsx new file mode 100644 index 000000000..4815b04ad --- /dev/null +++ b/fakemui/icons/Inbox.tsx @@ -0,0 +1,36 @@ +import React from 'react' +import type { IconProps } from './types' + +export const Inbox: React.FC = ({ + size = 24, + color = 'currentColor', + weight = 'regular', + className = '', + ...props +}) => { + const strokeWidths = { + thin: 1, + light: 1.5, + regular: 2, + bold: 2.5, + } + + return ( + + + + + ) +} diff --git a/fakemui/icons/List.tsx b/fakemui/icons/List.tsx new file mode 100644 index 000000000..8fa12bcf5 --- /dev/null +++ b/fakemui/icons/List.tsx @@ -0,0 +1,10 @@ +import React from 'react' +import { Icon, IconProps } from './Icon' + +export const List = (props: IconProps) => ( + + + + + +) diff --git a/fakemui/icons/Lock.tsx b/fakemui/icons/Lock.tsx new file mode 100644 index 000000000..4faba24e5 --- /dev/null +++ b/fakemui/icons/Lock.tsx @@ -0,0 +1,38 @@ +import React from 'react' +import type { IconProps } from './types' + +export const Lock: React.FC = ({ + size = 24, + color = 'currentColor', + weight = 'regular', + className = '', + ...props +}) => { + const strokeWidths = { + thin: 1, + light: 1.5, + regular: 2, + bold: 2.5, + } + + return ( + + + + + + + ) +} diff --git a/fakemui/icons/Maximize.tsx b/fakemui/icons/Maximize.tsx new file mode 100644 index 000000000..e0df73393 --- /dev/null +++ b/fakemui/icons/Maximize.tsx @@ -0,0 +1,11 @@ +import React from 'react' +import { Icon, IconProps } from './Icon' + +export const Maximize = (props: IconProps) => ( + + + + + + +) diff --git a/fakemui/icons/Minimize.tsx b/fakemui/icons/Minimize.tsx new file mode 100644 index 000000000..94c4cee10 --- /dev/null +++ b/fakemui/icons/Minimize.tsx @@ -0,0 +1,11 @@ +import React from 'react' +import { Icon, IconProps } from './Icon' + +export const Minimize = (props: IconProps) => ( + + + + + + +) diff --git a/fakemui/icons/Pause.tsx b/fakemui/icons/Pause.tsx new file mode 100644 index 000000000..e82b85cce --- /dev/null +++ b/fakemui/icons/Pause.tsx @@ -0,0 +1,8 @@ +import React from 'react' +import { Icon, IconProps } from './Icon' + +export const Pause = (props: IconProps) => ( + + + +) diff --git a/fakemui/icons/Refresh.tsx b/fakemui/icons/Refresh.tsx new file mode 100644 index 000000000..65b4b6eab --- /dev/null +++ b/fakemui/icons/Refresh.tsx @@ -0,0 +1,9 @@ +import React from 'react' +import { Icon, IconProps } from './Icon' + +export const Refresh = (props: IconProps) => ( + + + + +) diff --git a/fakemui/icons/Screen.tsx b/fakemui/icons/Screen.tsx new file mode 100644 index 000000000..6d3fda823 --- /dev/null +++ b/fakemui/icons/Screen.tsx @@ -0,0 +1,10 @@ +import React from 'react' +import { Icon, IconProps } from './Icon' + +export const Screen = (props: IconProps) => ( + + + + + +) diff --git a/fakemui/icons/ShieldCheck.tsx b/fakemui/icons/ShieldCheck.tsx new file mode 100644 index 000000000..ec7ec1d1c --- /dev/null +++ b/fakemui/icons/ShieldCheck.tsx @@ -0,0 +1,36 @@ +import React from 'react' +import type { IconProps } from './types' + +export const ShieldCheck: React.FC = ({ + size = 24, + color = 'currentColor', + weight = 'regular', + className = '', + ...props +}) => { + const strokeWidths = { + thin: 1, + light: 1.5, + regular: 2, + bold: 2.5, + } + + return ( + + + + + ) +} diff --git a/fakemui/icons/Smile.tsx b/fakemui/icons/Smile.tsx new file mode 100644 index 000000000..3ef30c91f --- /dev/null +++ b/fakemui/icons/Smile.tsx @@ -0,0 +1,38 @@ +import React from 'react' +import type { IconProps } from './types' + +export const Smile: React.FC = ({ + size = 24, + color = 'currentColor', + weight = 'regular', + className = '', + ...props +}) => { + const strokeWidths = { + thin: 1, + light: 1.5, + regular: 2, + bold: 2.5, + } + + return ( + + + + + + + ) +} diff --git a/fakemui/icons/Stop.tsx b/fakemui/icons/Stop.tsx new file mode 100644 index 000000000..976a5d762 --- /dev/null +++ b/fakemui/icons/Stop.tsx @@ -0,0 +1,8 @@ +import React from 'react' +import { Icon, IconProps } from './Icon' + +export const Stop = (props: IconProps) => ( + + + +) diff --git a/fakemui/icons/Video.tsx b/fakemui/icons/Video.tsx new file mode 100644 index 000000000..9bf20ccaf --- /dev/null +++ b/fakemui/icons/Video.tsx @@ -0,0 +1,9 @@ +import React from 'react' +import { Icon, IconProps } from './Icon' + +export const Video = (props: IconProps) => ( + + + + +) diff --git a/fakemui/icons/Volume.tsx b/fakemui/icons/Volume.tsx new file mode 100644 index 000000000..e54bb29b9 --- /dev/null +++ b/fakemui/icons/Volume.tsx @@ -0,0 +1,11 @@ +import React from 'react' +import { Icon, IconProps } from './Icon' + +export const Volume = (props: IconProps) => ( + + + + + + +) diff --git a/fakemui/icons/VolumeOff.tsx b/fakemui/icons/VolumeOff.tsx new file mode 100644 index 000000000..49b3a98b9 --- /dev/null +++ b/fakemui/icons/VolumeOff.tsx @@ -0,0 +1,11 @@ +import React from 'react' +import { Icon, IconProps } from './Icon' + +export const VolumeOff = (props: IconProps) => ( + + + + + + +) diff --git a/fakemui/icons/index.ts b/fakemui/icons/index.ts index a8dbbc714..f129065e1 100644 --- a/fakemui/icons/index.ts +++ b/fakemui/icons/index.ts @@ -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' diff --git a/packages/dbal_demo/seed/scripts/cache_operations.lua b/packages/dbal_demo/seed/scripts/cache_operations.lua index 7d84016a1..9d0b52402 100644 --- a/packages/dbal_demo/seed/scripts/cache_operations.lua +++ b/packages/dbal_demo/seed/scripts/cache_operations.lua @@ -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 diff --git a/packages/dbal_demo/seed/scripts/connection.lua b/packages/dbal_demo/seed/scripts/connection.lua index 762295b6e..1ef35ad79 100644 --- a/packages/dbal_demo/seed/scripts/connection.lua +++ b/packages/dbal_demo/seed/scripts/connection.lua @@ -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) diff --git a/packages/dbal_demo/seed/scripts/init.lua b/packages/dbal_demo/seed/scripts/init.lua index 2b8562b1b..1c318db94 100644 --- a/packages/dbal_demo/seed/scripts/init.lua +++ b/packages/dbal_demo/seed/scripts/init.lua @@ -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 = "", diff --git a/packages/dbal_demo/seed/scripts/kv_operations.lua b/packages/dbal_demo/seed/scripts/kv_operations.lua index f0174f252..0caa132e8 100644 --- a/packages/dbal_demo/seed/scripts/kv_operations.lua +++ b/packages/dbal_demo/seed/scripts/kv_operations.lua @@ -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" } diff --git a/packages/forum_forge/seed/scripts/can_moderate.lua b/packages/forum_forge/seed/scripts/can_moderate.lua new file mode 100644 index 000000000..5f7d28758 --- /dev/null +++ b/packages/forum_forge/seed/scripts/can_moderate.lua @@ -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 diff --git a/packages/forum_forge/seed/scripts/can_post.lua b/packages/forum_forge/seed/scripts/can_post.lua new file mode 100644 index 000000000..1c77943e8 --- /dev/null +++ b/packages/forum_forge/seed/scripts/can_post.lua @@ -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 diff --git a/packages/forum_forge/seed/scripts/flag_post.lua b/packages/forum_forge/seed/scripts/flag_post.lua new file mode 100644 index 000000000..b303db894 --- /dev/null +++ b/packages/forum_forge/seed/scripts/flag_post.lua @@ -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 diff --git a/packages/forum_forge/seed/scripts/init.lua b/packages/forum_forge/seed/scripts/init.lua index 52e7d9c87..db6299f5b 100644 --- a/packages/forum_forge/seed/scripts/init.lua +++ b/packages/forum_forge/seed/scripts/init.lua @@ -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) diff --git a/packages/forum_forge/seed/scripts/moderation.lua b/packages/forum_forge/seed/scripts/moderation.lua index 0af571c88..a04806e8b 100644 --- a/packages/forum_forge/seed/scripts/moderation.lua +++ b/packages/forum_forge/seed/scripts/moderation.lua @@ -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 diff --git a/packages/forum_forge/seed/scripts/permissions.lua b/packages/forum_forge/seed/scripts/permissions.lua index cae0d6281..edb3c432d 100644 --- a/packages/forum_forge/seed/scripts/permissions.lua +++ b/packages/forum_forge/seed/scripts/permissions.lua @@ -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 diff --git a/packages/forum_forge/seed/scripts/rank_thread.lua b/packages/forum_forge/seed/scripts/rank_thread.lua new file mode 100644 index 000000000..38b19b183 --- /dev/null +++ b/packages/forum_forge/seed/scripts/rank_thread.lua @@ -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 diff --git a/packages/forum_forge/seed/scripts/thread_rank.lua b/packages/forum_forge/seed/scripts/thread_rank.lua index d7fa4d915..7c936101c 100644 --- a/packages/forum_forge/seed/scripts/thread_rank.lua +++ b/packages/forum_forge/seed/scripts/thread_rank.lua @@ -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 diff --git a/packages/forum_forge/seed/scripts/types.lua b/packages/forum_forge/seed/scripts/types.lua new file mode 100644 index 000000000..428d49206 --- /dev/null +++ b/packages/forum_forge/seed/scripts/types.lua @@ -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 {} diff --git a/packages/irc_webchat/seed/scripts/init.lua b/packages/irc_webchat/seed/scripts/init.lua index f47c78f41..46bf33f41 100644 --- a/packages/irc_webchat/seed/scripts/init.lua +++ b/packages/irc_webchat/seed/scripts/init.lua @@ -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) diff --git a/packages/irc_webchat/seed/scripts/types.lua b/packages/irc_webchat/seed/scripts/types.lua new file mode 100644 index 000000000..4eb74f957 --- /dev/null +++ b/packages/irc_webchat/seed/scripts/types.lua @@ -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 {} diff --git a/packages/notification_center/seed/scripts/calculate_total.lua b/packages/notification_center/seed/scripts/calculate_total.lua new file mode 100644 index 000000000..a5d6e5ef4 --- /dev/null +++ b/packages/notification_center/seed/scripts/calculate_total.lua @@ -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 diff --git a/packages/notification_center/seed/scripts/get_severity_class.lua b/packages/notification_center/seed/scripts/get_severity_class.lua new file mode 100644 index 000000000..743158588 --- /dev/null +++ b/packages/notification_center/seed/scripts/get_severity_class.lua @@ -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 diff --git a/packages/notification_center/seed/scripts/init.lua b/packages/notification_center/seed/scripts/init.lua index e3b67c0a9..74fc5c4aa 100644 --- a/packages/notification_center/seed/scripts/init.lua +++ b/packages/notification_center/seed/scripts/init.lua @@ -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 { diff --git a/packages/notification_center/seed/scripts/list.lua b/packages/notification_center/seed/scripts/list.lua index fdd7e6218..a4a21cb3e 100644 --- a/packages/notification_center/seed/scripts/list.lua +++ b/packages/notification_center/seed/scripts/list.lua @@ -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 diff --git a/packages/notification_center/seed/scripts/prepare_summary.lua b/packages/notification_center/seed/scripts/prepare_summary.lua new file mode 100644 index 000000000..bf7fe3242 --- /dev/null +++ b/packages/notification_center/seed/scripts/prepare_summary.lua @@ -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 diff --git a/packages/notification_center/seed/scripts/render_notification_badge.lua b/packages/notification_center/seed/scripts/render_notification_badge.lua new file mode 100644 index 000000000..86b62d327 --- /dev/null +++ b/packages/notification_center/seed/scripts/render_notification_badge.lua @@ -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 diff --git a/packages/notification_center/seed/scripts/render_notification_item.lua b/packages/notification_center/seed/scripts/render_notification_item.lua new file mode 100644 index 000000000..3175e1e70 --- /dev/null +++ b/packages/notification_center/seed/scripts/render_notification_item.lua @@ -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 diff --git a/packages/notification_center/seed/scripts/render_notification_list.lua b/packages/notification_center/seed/scripts/render_notification_list.lua new file mode 100644 index 000000000..1142c813c --- /dev/null +++ b/packages/notification_center/seed/scripts/render_notification_list.lua @@ -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 diff --git a/packages/notification_center/seed/scripts/summary.lua b/packages/notification_center/seed/scripts/summary.lua index ead7b328d..3014d658e 100644 --- a/packages/notification_center/seed/scripts/summary.lua +++ b/packages/notification_center/seed/scripts/summary.lua @@ -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 ----@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 diff --git a/packages/notification_center/seed/scripts/toast.lua b/packages/notification_center/seed/scripts/toast.lua index 545a97cb3..5f7af6028 100644 --- a/packages/notification_center/seed/scripts/toast.lua +++ b/packages/notification_center/seed/scripts/toast.lua @@ -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 diff --git a/packages/notification_center/seed/scripts/toast_error.lua b/packages/notification_center/seed/scripts/toast_error.lua new file mode 100644 index 000000000..d47777e91 --- /dev/null +++ b/packages/notification_center/seed/scripts/toast_error.lua @@ -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 diff --git a/packages/notification_center/seed/scripts/toast_info.lua b/packages/notification_center/seed/scripts/toast_info.lua new file mode 100644 index 000000000..6c65f19a6 --- /dev/null +++ b/packages/notification_center/seed/scripts/toast_info.lua @@ -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 diff --git a/packages/notification_center/seed/scripts/toast_success.lua b/packages/notification_center/seed/scripts/toast_success.lua new file mode 100644 index 000000000..6b59b122f --- /dev/null +++ b/packages/notification_center/seed/scripts/toast_success.lua @@ -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 diff --git a/packages/notification_center/seed/scripts/toast_warning.lua b/packages/notification_center/seed/scripts/toast_warning.lua new file mode 100644 index 000000000..0d5fc57e4 --- /dev/null +++ b/packages/notification_center/seed/scripts/toast_warning.lua @@ -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 diff --git a/packages/notification_center/seed/scripts/types.lua b/packages/notification_center/seed/scripts/types.lua new file mode 100644 index 000000000..67c5fc3dd --- /dev/null +++ b/packages/notification_center/seed/scripts/types.lua @@ -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 {} diff --git a/packages/stream_cast/seed/scripts/lua/permissions.lua b/packages/stream_cast/seed/scripts/lua/permissions.lua index 6e198d573..c2ede8d45 100644 --- a/packages/stream_cast/seed/scripts/lua/permissions.lua +++ b/packages/stream_cast/seed/scripts/lua/permissions.lua @@ -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" diff --git a/services/media_daemon/Dockerfile b/services/media_daemon/Dockerfile new file mode 100644 index 000000000..99fd8e3d8 --- /dev/null +++ b/services/media_daemon/Dockerfile @@ -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"] diff --git a/services/media_daemon/README.md b/services/media_daemon/README.md new file mode 100644 index 000000000..41a5138aa --- /dev/null +++ b/services/media_daemon/README.md @@ -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. diff --git a/services/media_daemon/build-config/CMakeLists.txt b/services/media_daemon/build-config/CMakeLists.txt new file mode 100644 index 000000000..0ed1fb9c1 --- /dev/null +++ b/services/media_daemon/build-config/CMakeLists.txt @@ -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 +) diff --git a/services/media_daemon/build-config/conanfile.txt b/services/media_daemon/build-config/conanfile.txt new file mode 100644 index 000000000..fce0ab3eb --- /dev/null +++ b/services/media_daemon/build-config/conanfile.txt @@ -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 diff --git a/services/media_daemon/config/media-daemon.yaml b/services/media_daemon/config/media-daemon.yaml new file mode 100644 index 000000000..2a1c30dcb --- /dev/null +++ b/services/media_daemon/config/media-daemon.yaml @@ -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"] diff --git a/services/media_daemon/config/nginx-stream.conf b/services/media_daemon/config/nginx-stream.conf new file mode 100644 index 000000000..5fdcfb3c7 --- /dev/null +++ b/services/media_daemon/config/nginx-stream.conf @@ -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; +} diff --git a/services/media_daemon/docker-compose.yml b/services/media_daemon/docker-compose.yml new file mode 100644 index 000000000..ee6240412 --- /dev/null +++ b/services/media_daemon/docker-compose.yml @@ -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 diff --git a/services/media_daemon/include/media/dbal_client.hpp b/services/media_daemon/include/media/dbal_client.hpp new file mode 100644 index 000000000..e5003450f --- /dev/null +++ b/services/media_daemon/include/media/dbal_client.hpp @@ -0,0 +1,251 @@ +#pragma once + +#include "media/types.hpp" +#include +#include +#include + +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 initialize(const DbalClientConfig& config); + + /** + * Check if connected to DBAL + */ + bool is_connected() const; + + /** + * Test connection to DBAL + * @return Result indicating success or failure + */ + Result ping(); + + // ======================================================================== + // Notifications + // ======================================================================== + + /** + * Send a notification to a user + * @param notification Notification details + * @return Result indicating success or failure + */ + Result send_notification(const Notification& notification); + + /** + * Send job started notification + */ + Result 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 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 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 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 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 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 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 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 store_job(const JobInfo& job); + + /** + * Update job record in DBAL + * @param job Updated job info + * @return Result indicating success or failure + */ + Result update_job(const JobInfo& job); + + /** + * Get job record from DBAL + * @param job_id Job ID + * @return Result with job info or error + */ + Result 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> 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 store_radio_channel(const RadioChannelConfig& config); + + /** + * Store TV channel config + */ + Result store_tv_channel(const TvChannelConfig& config); + + /** + * Get radio channels for tenant + */ + Result> get_radio_channels( + const std::string& tenant_id + ); + + /** + * Get TV channels for tenant + */ + Result> get_tv_channels( + const std::string& tenant_id + ); + +private: + /** + * Make HTTP request to DBAL + */ + Result make_request( + const std::string& method, + const std::string& endpoint, + const std::string& body = "" + ); + + /** + * Retry wrapper + */ + template + auto with_retry(Func&& func) -> decltype(func()); + + DbalClientConfig config_; + bool initialized_ = false; + + // HTTP client handle (cpr or similar) + struct Impl; + std::unique_ptr impl_; +}; + +} // namespace media diff --git a/services/media_daemon/include/media/job_queue.hpp b/services/media_daemon/include/media/job_queue.hpp new file mode 100644 index 000000000..507196906 --- /dev/null +++ b/services/media_daemon/include/media/job_queue.hpp @@ -0,0 +1,259 @@ +#pragma once + +#include "media/types.hpp" +#include "media/plugin_manager.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +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 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 submit(const JobRequest& request); + + /** + * Get job status + * @param job_id Job ID + * @return Result with job info or error + */ + Result 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 list_jobs( + const std::string& tenant_id = "", + const std::string& user_id = "", + std::optional 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 cancel(const std::string& job_id); + + /** + * Retry a failed job + * @param job_id Job ID + * @return Result with new job ID or error + */ + Result 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 pending_by_type; + std::map 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 initialized_{false}; + std::atomic running_{false}; + + // Job storage + mutable std::mutex jobs_mutex_; + std::map jobs_; + std::map 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(priority) != static_cast(other.priority)) { + return static_cast(priority) > static_cast(other.priority); + } + // Earlier submission time = higher priority + return submitted_at > other.submitted_at; + } + }; + + mutable std::mutex queues_mutex_; + std::map> queues_; + std::map queue_cvs_; + + // Worker threads + std::vector workers_; + std::map> 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 job_counter_{0}; +}; + +} // namespace media diff --git a/services/media_daemon/include/media/plugin.hpp b/services/media_daemon/include/media/plugin.hpp new file mode 100644 index 000000000..9caf2585a --- /dev/null +++ b/services/media_daemon/include/media/plugin.hpp @@ -0,0 +1,191 @@ +#pragma once + +#include "media/types.hpp" +#include +#include + +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 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 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 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 start_stream( + const std::string& channel_id, + const std::map& source, + const std::map& output + ) { + return Result::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 stop_stream(const std::string& channel_id) { + return Result::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; \ + } \ + } diff --git a/services/media_daemon/include/media/plugin_manager.hpp b/services/media_daemon/include/media/plugin_manager.hpp new file mode 100644 index 000000000..21ef5edd1 --- /dev/null +++ b/services/media_daemon/include/media/plugin_manager.hpp @@ -0,0 +1,174 @@ +#pragma once + +#include "media/types.hpp" +#include "media/plugin.hpp" +#include +#include +#include +#include +#include + +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 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 load_plugin(const std::string& path); + + /** + * Unload a specific plugin + * @param plugin_id Plugin ID to unload + * @return Result indicating success or failure + */ + Result 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 reload_plugin(const std::string& plugin_id); + + /** + * Get list of all loaded plugins + * @return Vector of plugin information + */ + std::vector 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 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 register_builtin(std::unique_ptr 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 health_check() const; + +private: + /** + * Scan directory for plugin files + */ + std::vector scan_plugin_directory( + const std::string& dir + ); + + /** + * Load plugin from shared library + */ + Result 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 plugins_; + + // Built-in plugins (owned) + std::vector> builtin_plugins_; + + // Plugin ID to handle mapping for built-ins + std::map builtin_map_; +}; + +} // namespace media diff --git a/services/media_daemon/include/media/plugins/ffmpeg_plugin.hpp b/services/media_daemon/include/media/plugins/ffmpeg_plugin.hpp new file mode 100644 index 000000000..9e0cd0b6c --- /dev/null +++ b/services/media_daemon/include/media/plugins/ffmpeg_plugin.hpp @@ -0,0 +1,174 @@ +#pragma once + +#include "media/plugin.hpp" +#include +#include +#include + +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 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 process( + const JobRequest& request, + JobProgressCallback progress_callback + ) override; + + Result cancel(const std::string& job_id) override; + + // Streaming support + Result start_stream( + const std::string& channel_id, + const std::map& source, + const std::map& output + ) override; + + Result 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 probe(const std::string& path); + + /** + * Get video duration in seconds + */ + Result get_duration(const std::string& path); + + /** + * Get available hardware encoders + */ + std::vector 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 build_video_command( + const VideoTranscodeParams& params + ); + + /** + * Build FFmpeg command for audio transcoding + */ + std::vector build_audio_command( + const AudioTranscodeParams& params + ); + + /** + * Build FFmpeg command for HLS streaming + */ + std::vector build_hls_command( + const std::string& input, + const std::string& output_dir, + const std::map& options + ); + + /** + * Execute FFmpeg command with progress tracking + */ + Result execute_ffmpeg( + const std::vector& 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 cancelled{false}; + }; + + mutable std::mutex processes_mutex_; + std::map active_processes_; + + // Cached probe data + mutable std::mutex cache_mutex_; + std::map probe_cache_; +}; + +} // namespace plugins +} // namespace media diff --git a/services/media_daemon/include/media/radio_engine.hpp b/services/media_daemon/include/media/radio_engine.hpp new file mode 100644 index 000000000..7eceb5af2 --- /dev/null +++ b/services/media_daemon/include/media/radio_engine.hpp @@ -0,0 +1,314 @@ +#pragma once + +#include "media/types.hpp" +#include "media/plugin_manager.hpp" +#include +#include +#include +#include +#include +#include + +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 playlist; + size_t current_index = 0; + + // Streaming state + std::atomic 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 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 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 create_channel(const RadioChannelConfig& config); + + /** + * Delete a radio channel + * @param channel_id Channel ID + * @return Result indicating success or failure + */ + Result 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 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 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 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 start_channel(const std::string& channel_id); + + /** + * Stop streaming a channel + * @param channel_id Channel ID + * @return Result indicating success or failure + */ + Result 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 set_playlist( + const std::string& channel_id, + const std::vector& 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 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 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 skip_track(const std::string& channel_id); + + /** + * Get current playlist + * @param channel_id Channel ID + * @return Result with playlist or error + */ + Result> 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 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 set_auto_dj( + const std::string& channel_id, + bool enabled, + const std::vector& 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 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 initialized_{false}; + + // Channels + mutable std::mutex channels_mutex_; + std::map> channels_; +}; + +} // namespace media diff --git a/services/media_daemon/include/media/server.hpp b/services/media_daemon/include/media/server.hpp new file mode 100644 index 000000000..a2e438598 --- /dev/null +++ b/services/media_daemon/include/media/server.hpp @@ -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 +#include +#include + +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 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 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 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 initialized_{false}; + std::atomic running_{false}; + + // Components + std::unique_ptr plugin_manager_; + std::unique_ptr job_queue_; + std::unique_ptr radio_engine_; + std::unique_ptr tv_engine_; + std::unique_ptr dbal_client_; + + // Drogon app (pimpl) + struct Impl; + std::unique_ptr impl_; +}; + +} // namespace media diff --git a/services/media_daemon/include/media/tv_engine.hpp b/services/media_daemon/include/media/tv_engine.hpp new file mode 100644 index 000000000..d82978e5e --- /dev/null +++ b/services/media_daemon/include/media/tv_engine.hpp @@ -0,0 +1,396 @@ +#pragma once + +#include "media/types.hpp" +#include "media/plugin_manager.hpp" +#include +#include +#include +#include +#include +#include + +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 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 schedule; + size_t current_program_index = 0; + + // Streaming state + std::atomic 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 encoder_handles; + + // Statistics + std::chrono::system_clock::time_point started_at; + std::atomic 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 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 create_channel(const TvChannelConfig& config); + + /** + * Delete a TV channel + * @param channel_id Channel ID + * @return Result indicating success or failure + */ + Result 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 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 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 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 quality_urls; // resolution -> URL + }; + Result start_channel(const std::string& channel_id); + + /** + * Stop streaming a channel + * @param channel_id Channel ID + * @return Result indicating success or failure + */ + Result 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 set_schedule( + const std::string& channel_id, + const std::vector& entries + ); + + /** + * Add program to schedule + * @param channel_id Channel ID + * @param entry Schedule entry + * @return Result indicating success or failure + */ + Result 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 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> 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 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> 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 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 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 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 set_commercials( + const std::string& channel_id, + const std::vector& 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 initialized_{false}; + + // Channels + mutable std::mutex channels_mutex_; + std::map> channels_; + + // EPG refresh thread + std::thread epg_thread_; + std::atomic epg_running_{false}; +}; + +} // namespace media diff --git a/services/media_daemon/include/media/types.hpp b/services/media_daemon/include/media/types.hpp new file mode 100644 index 000000000..a07cd0332 --- /dev/null +++ b/services/media_daemon/include/media/types.hpp @@ -0,0 +1,407 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +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 +class Result { +public: + static Result ok(T value) { + Result r; + r.value_ = std::move(value); + r.error_code_ = ErrorCode::OK; + return r; + } + + static Result error(ErrorCode code, const std::string& message) { + Result 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 { +public: + static Result ok() { + Result r; + r.error_code_ = ErrorCode::OK; + return r; + } + + static Result error(ErrorCode code, const std::string& message) { + Result 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 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 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 filters; // blur, sharpen, etc. +}; + +using JobParams = std::variant< + VideoTranscodeParams, + AudioTranscodeParams, + DocumentConvertParams, + ImageProcessParams, + std::map // 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 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 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 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 auto_dj_folders; + bool shuffle = true; +}; + +struct RadioChannelStatus { + std::string id; + std::string name; + bool is_live = false; + int listeners = 0; + std::optional now_playing; + std::optional 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 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 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 now_playing; + std::optional 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 supported_formats; + std::vector 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 input_formats; + std::vector 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 data; +}; + +// ============================================================================ +// Callback Types +// ============================================================================ + +using JobProgressCallback = std::function; +using JobCompletionCallback = std::function; +using NotificationCallback = std::function; + +} // namespace media diff --git a/services/media_daemon/schema/entities/media_job.yaml b/services/media_daemon/schema/entities/media_job.yaml new file mode 100644 index 000000000..ff83aeab5 --- /dev/null +++ b/services/media_daemon/schema/entities/media_job.yaml @@ -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] diff --git a/services/media_daemon/src/main.cpp b/services/media_daemon/src/main.cpp new file mode 100644 index 000000000..4770a2ac7 --- /dev/null +++ b/services/media_daemon/src/main.cpp @@ -0,0 +1,235 @@ +#include +#include +#include +#include +#include + +// Cross-platform signal handling +#ifdef _WIN32 + #include + #ifndef SIGTERM + #define SIGTERM SIGBREAK + #endif +#endif + +#include "media/server.hpp" + +namespace { + std::unique_ptr 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 Configuration file (default: /etc/media-daemon/config.yaml)" << std::endl; + std::cout << " --bind
Bind address (default: 0.0.0.0)" << std::endl; + std::cout << " --port Port number (default: 8090)" << std::endl; + std::cout << " --workers Number of HTTP workers (default: 4)" << std::endl; + std::cout << " --dbal-url DBAL daemon URL (default: http://localhost:8080)" << std::endl; + std::cout << " --plugin-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(); + + 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; +}