mirror of
https://github.com/johndoe6345789/metabuilder.git
synced 2026-04-24 22:04:56 +00:00
Add Media Daemon server and TV engine implementation
- Introduced `Server` class for managing the Media Daemon, including configuration, lifecycle, and HTTP route handling. - Added `TvEngine` class for managing TV channels, scheduling, EPG generation, and streaming functionalities. - Created `types.hpp` to define various data structures and enums for jobs, radio, TV channels, and plugins. - Implemented main entry point in `main.cpp` to initialize and run the Media Daemon server with command-line and environment variable configurations. - Established error handling and result management using a generic `Result` class. - Included necessary headers and dependencies for media processing and plugin management.
This commit is contained in:
37
fakemui/icons/AlertCircle.tsx
Normal file
37
fakemui/icons/AlertCircle.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import React from 'react'
|
||||
import type { IconProps } from './types'
|
||||
|
||||
export const AlertCircle: React.FC<IconProps> = ({
|
||||
size = 24,
|
||||
color = 'currentColor',
|
||||
weight = 'regular',
|
||||
className = '',
|
||||
...props
|
||||
}) => {
|
||||
const strokeWidths = {
|
||||
thin: 1,
|
||||
light: 1.5,
|
||||
regular: 2,
|
||||
bold: 2.5,
|
||||
}
|
||||
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 256 256"
|
||||
fill="none"
|
||||
stroke={color}
|
||||
strokeWidth={strokeWidths[weight]}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className={className}
|
||||
{...props}
|
||||
>
|
||||
<circle cx="128" cy="128" r="96" />
|
||||
<line x1="128" y1="80" x2="128" y2="136" />
|
||||
<circle cx="128" cy="172" r="8" fill={color} stroke="none" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
37
fakemui/icons/AlertTriangle.tsx
Normal file
37
fakemui/icons/AlertTriangle.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import React from 'react'
|
||||
import type { IconProps } from './types'
|
||||
|
||||
export const AlertTriangle: React.FC<IconProps> = ({
|
||||
size = 24,
|
||||
color = 'currentColor',
|
||||
weight = 'regular',
|
||||
className = '',
|
||||
...props
|
||||
}) => {
|
||||
const strokeWidths = {
|
||||
thin: 1,
|
||||
light: 1.5,
|
||||
regular: 2,
|
||||
bold: 2.5,
|
||||
}
|
||||
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 256 256"
|
||||
fill="none"
|
||||
stroke={color}
|
||||
strokeWidth={strokeWidths[weight]}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className={className}
|
||||
{...props}
|
||||
>
|
||||
<path d="M114.5 40.2L22.1 196a16 16 0 0 0 13.9 24h184a16 16 0 0 0 13.9-24L141.5 40.2a16 16 0 0 0-27 0z" />
|
||||
<line x1="128" y1="104" x2="128" y2="144" />
|
||||
<circle cx="128" cy="180" r="8" fill={color} stroke="none" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
12
fakemui/icons/Broadcast.tsx
Normal file
12
fakemui/icons/Broadcast.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
import React from 'react'
|
||||
import { Icon, IconProps } from './Icon'
|
||||
|
||||
export const Broadcast = (props: IconProps) => (
|
||||
<Icon {...props}>
|
||||
<circle cx="128" cy="128" r="32" />
|
||||
<path d="M77.1 178.9a80 80 0 0 1 0-101.8" />
|
||||
<path d="M178.9 77.1a80 80 0 0 1 0 101.8" />
|
||||
<path d="M44.9 211.1a128 128 0 0 1 0-166.2" />
|
||||
<path d="M211.1 44.9a128 128 0 0 1 0 166.2" />
|
||||
</Icon>
|
||||
)
|
||||
9
fakemui/icons/Camera.tsx
Normal file
9
fakemui/icons/Camera.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import React from 'react'
|
||||
import { Icon, IconProps } from './Icon'
|
||||
|
||||
export const Camera = (props: IconProps) => (
|
||||
<Icon {...props}>
|
||||
<path d="M208 56h-27.7l-14.4-21.6A8 8 0 0 0 159.2 32H96.8a8 8 0 0 0-6.7 3.6L75.7 56H48a24 24 0 0 0-24 24v112a24 24 0 0 0 24 24h160a24 24 0 0 0 24-24V80a24 24 0 0 0-24-24Z" />
|
||||
<circle cx="128" cy="132" r="44" />
|
||||
</Icon>
|
||||
)
|
||||
38
fakemui/icons/ChartLine.tsx
Normal file
38
fakemui/icons/ChartLine.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import React from 'react'
|
||||
import type { IconProps } from './types'
|
||||
|
||||
export const ChartLine: React.FC<IconProps> = ({
|
||||
size = 24,
|
||||
color = 'currentColor',
|
||||
weight = 'regular',
|
||||
className = '',
|
||||
...props
|
||||
}) => {
|
||||
const strokeWidths = {
|
||||
thin: 1,
|
||||
light: 1.5,
|
||||
regular: 2,
|
||||
bold: 2.5,
|
||||
}
|
||||
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 256 256"
|
||||
fill="none"
|
||||
stroke={color}
|
||||
strokeWidth={strokeWidths[weight]}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className={className}
|
||||
{...props}
|
||||
>
|
||||
<polyline points="48 208 48 48" />
|
||||
<polyline points="48 208 208 208" />
|
||||
<polyline points="48 160 96 112 144 144 208 72" />
|
||||
<polyline points="176 72 208 72 208 104" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
37
fakemui/icons/Edit.tsx
Normal file
37
fakemui/icons/Edit.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
import React from 'react'
|
||||
import type { IconProps } from './types'
|
||||
|
||||
export const Edit: React.FC<IconProps> = ({
|
||||
size = 24,
|
||||
color = 'currentColor',
|
||||
weight = 'regular',
|
||||
className = '',
|
||||
...props
|
||||
}) => {
|
||||
const strokeWidths = {
|
||||
thin: 1,
|
||||
light: 1.5,
|
||||
regular: 2,
|
||||
bold: 2.5,
|
||||
}
|
||||
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 256 256"
|
||||
fill="none"
|
||||
stroke={color}
|
||||
strokeWidth={strokeWidths[weight]}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className={className}
|
||||
{...props}
|
||||
>
|
||||
<path d="M92.7 216H48a8 8 0 0 1-8-8v-44.7a8 8 0 0 1 2.3-5.6l120-120a8 8 0 0 1 11.3 0l44.7 44.7a8 8 0 0 1 0 11.3l-120 120a8 8 0 0 1-5.6 2.3z" />
|
||||
<line x1="136" y1="64" x2="192" y2="120" />
|
||||
<line x1="40" y1="216" x2="96" y2="160" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
9
fakemui/icons/File.tsx
Normal file
9
fakemui/icons/File.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import React from 'react'
|
||||
import { Icon, IconProps } from './Icon'
|
||||
|
||||
export const File = (props: IconProps) => (
|
||||
<Icon {...props}>
|
||||
<path d="M200 224H56a8 8 0 0 1-8-8V40a8 8 0 0 1 8-8h96l56 56v128a8 8 0 0 1-8 8Z" />
|
||||
<polyline points="152 32 152 88 208 88" />
|
||||
</Icon>
|
||||
)
|
||||
11
fakemui/icons/Grid.tsx
Normal file
11
fakemui/icons/Grid.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import React from 'react'
|
||||
import { Icon, IconProps } from './Icon'
|
||||
|
||||
export const Grid = (props: IconProps) => (
|
||||
<Icon {...props}>
|
||||
<rect x="32" y="32" width="80" height="80" rx="8" />
|
||||
<rect x="144" y="32" width="80" height="80" rx="8" />
|
||||
<rect x="32" y="144" width="80" height="80" rx="8" />
|
||||
<rect x="144" y="144" width="80" height="80" rx="8" />
|
||||
</Icon>
|
||||
)
|
||||
10
fakemui/icons/Image.tsx
Normal file
10
fakemui/icons/Image.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import React from 'react'
|
||||
import { Icon, IconProps } from './Icon'
|
||||
|
||||
export const Image = (props: IconProps) => (
|
||||
<Icon {...props}>
|
||||
<rect x="32" y="48" width="192" height="160" rx="8" />
|
||||
<circle cx="96" cy="112" r="24" />
|
||||
<path d="m32 168 72-56 48 48 48-48 24 24" />
|
||||
</Icon>
|
||||
)
|
||||
36
fakemui/icons/Inbox.tsx
Normal file
36
fakemui/icons/Inbox.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import React from 'react'
|
||||
import type { IconProps } from './types'
|
||||
|
||||
export const Inbox: React.FC<IconProps> = ({
|
||||
size = 24,
|
||||
color = 'currentColor',
|
||||
weight = 'regular',
|
||||
className = '',
|
||||
...props
|
||||
}) => {
|
||||
const strokeWidths = {
|
||||
thin: 1,
|
||||
light: 1.5,
|
||||
regular: 2,
|
||||
bold: 2.5,
|
||||
}
|
||||
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 256 256"
|
||||
fill="none"
|
||||
stroke={color}
|
||||
strokeWidth={strokeWidths[weight]}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className={className}
|
||||
{...props}
|
||||
>
|
||||
<rect x="32" y="48" width="192" height="160" rx="8" />
|
||||
<path d="M32 144h56a8 8 0 0 1 8 8 32 32 0 0 0 64 0 8 8 0 0 1 8-8h56" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
10
fakemui/icons/List.tsx
Normal file
10
fakemui/icons/List.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import React from 'react'
|
||||
import { Icon, IconProps } from './Icon'
|
||||
|
||||
export const List = (props: IconProps) => (
|
||||
<Icon {...props}>
|
||||
<line x1="40" y1="64" x2="216" y2="64" />
|
||||
<line x1="40" y1="128" x2="216" y2="128" />
|
||||
<line x1="40" y1="192" x2="216" y2="192" />
|
||||
</Icon>
|
||||
)
|
||||
38
fakemui/icons/Lock.tsx
Normal file
38
fakemui/icons/Lock.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import React from 'react'
|
||||
import type { IconProps } from './types'
|
||||
|
||||
export const Lock: React.FC<IconProps> = ({
|
||||
size = 24,
|
||||
color = 'currentColor',
|
||||
weight = 'regular',
|
||||
className = '',
|
||||
...props
|
||||
}) => {
|
||||
const strokeWidths = {
|
||||
thin: 1,
|
||||
light: 1.5,
|
||||
regular: 2,
|
||||
bold: 2.5,
|
||||
}
|
||||
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 256 256"
|
||||
fill="none"
|
||||
stroke={color}
|
||||
strokeWidth={strokeWidths[weight]}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className={className}
|
||||
{...props}
|
||||
>
|
||||
<rect x="48" y="112" width="160" height="112" rx="8" />
|
||||
<path d="M80 112V80a48 48 0 0 1 96 0v32" />
|
||||
<circle cx="128" cy="164" r="12" fill={color} stroke="none" />
|
||||
<line x1="128" y1="176" x2="128" y2="196" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
11
fakemui/icons/Maximize.tsx
Normal file
11
fakemui/icons/Maximize.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import React from 'react'
|
||||
import { Icon, IconProps } from './Icon'
|
||||
|
||||
export const Maximize = (props: IconProps) => (
|
||||
<Icon {...props}>
|
||||
<polyline points="168 48 208 48 208 88" />
|
||||
<line x1="152" y1="104" x2="208" y2="48" />
|
||||
<polyline points="88 208 48 208 48 168" />
|
||||
<line x1="104" y1="152" x2="48" y2="208" />
|
||||
</Icon>
|
||||
)
|
||||
11
fakemui/icons/Minimize.tsx
Normal file
11
fakemui/icons/Minimize.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import React from 'react'
|
||||
import { Icon, IconProps } from './Icon'
|
||||
|
||||
export const Minimize = (props: IconProps) => (
|
||||
<Icon {...props}>
|
||||
<polyline points="152 104 208 104 208 48" />
|
||||
<line x1="152" y1="104" x2="208" y2="48" />
|
||||
<polyline points="104 152 48 152 48 208" />
|
||||
<line x1="104" y1="152" x2="48" y2="208" />
|
||||
</Icon>
|
||||
)
|
||||
8
fakemui/icons/Pause.tsx
Normal file
8
fakemui/icons/Pause.tsx
Normal file
@@ -0,0 +1,8 @@
|
||||
import React from 'react'
|
||||
import { Icon, IconProps } from './Icon'
|
||||
|
||||
export const Pause = (props: IconProps) => (
|
||||
<Icon {...props}>
|
||||
<path d="M216 48v160a16 16 0 0 1-16 16h-36a16 16 0 0 1-16-16V48a16 16 0 0 1 16-16h36a16 16 0 0 1 16 16ZM92 32H56a16 16 0 0 0-16 16v160a16 16 0 0 0 16 16h36a16 16 0 0 0 16-16V48a16 16 0 0 0-16-16Z" />
|
||||
</Icon>
|
||||
)
|
||||
9
fakemui/icons/Refresh.tsx
Normal file
9
fakemui/icons/Refresh.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import React from 'react'
|
||||
import { Icon, IconProps } from './Icon'
|
||||
|
||||
export const Refresh = (props: IconProps) => (
|
||||
<Icon {...props}>
|
||||
<polyline points="176.2 99.7 224.2 99.7 224.2 51.7" />
|
||||
<path d="M190.2 190.2a88 88 0 1 1 0-124.4l34 34" />
|
||||
</Icon>
|
||||
)
|
||||
10
fakemui/icons/Screen.tsx
Normal file
10
fakemui/icons/Screen.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import React from 'react'
|
||||
import { Icon, IconProps } from './Icon'
|
||||
|
||||
export const Screen = (props: IconProps) => (
|
||||
<Icon {...props}>
|
||||
<rect x="32" y="48" width="192" height="144" rx="16" />
|
||||
<line x1="96" y1="224" x2="160" y2="224" />
|
||||
<line x1="128" y1="192" x2="128" y2="224" />
|
||||
</Icon>
|
||||
)
|
||||
36
fakemui/icons/ShieldCheck.tsx
Normal file
36
fakemui/icons/ShieldCheck.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import React from 'react'
|
||||
import type { IconProps } from './types'
|
||||
|
||||
export const ShieldCheck: React.FC<IconProps> = ({
|
||||
size = 24,
|
||||
color = 'currentColor',
|
||||
weight = 'regular',
|
||||
className = '',
|
||||
...props
|
||||
}) => {
|
||||
const strokeWidths = {
|
||||
thin: 1,
|
||||
light: 1.5,
|
||||
regular: 2,
|
||||
bold: 2.5,
|
||||
}
|
||||
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 256 256"
|
||||
fill="none"
|
||||
stroke={color}
|
||||
strokeWidth={strokeWidths[weight]}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className={className}
|
||||
{...props}
|
||||
>
|
||||
<path d="M40 114V56a8 8 0 0 1 8-8h160a8 8 0 0 1 8 8v58c0 84-72 106-88 106s-88-22-88-106z" />
|
||||
<polyline points="88 136 112 160 168 104" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
38
fakemui/icons/Smile.tsx
Normal file
38
fakemui/icons/Smile.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import React from 'react'
|
||||
import type { IconProps } from './types'
|
||||
|
||||
export const Smile: React.FC<IconProps> = ({
|
||||
size = 24,
|
||||
color = 'currentColor',
|
||||
weight = 'regular',
|
||||
className = '',
|
||||
...props
|
||||
}) => {
|
||||
const strokeWidths = {
|
||||
thin: 1,
|
||||
light: 1.5,
|
||||
regular: 2,
|
||||
bold: 2.5,
|
||||
}
|
||||
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width={size}
|
||||
height={size}
|
||||
viewBox="0 0 256 256"
|
||||
fill="none"
|
||||
stroke={color}
|
||||
strokeWidth={strokeWidths[weight]}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
className={className}
|
||||
{...props}
|
||||
>
|
||||
<circle cx="128" cy="128" r="96" />
|
||||
<circle cx="92" cy="108" r="8" fill={color} stroke="none" />
|
||||
<circle cx="164" cy="108" r="8" fill={color} stroke="none" />
|
||||
<path d="M88 152a48 48 0 0 0 80 0" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
8
fakemui/icons/Stop.tsx
Normal file
8
fakemui/icons/Stop.tsx
Normal file
@@ -0,0 +1,8 @@
|
||||
import React from 'react'
|
||||
import { Icon, IconProps } from './Icon'
|
||||
|
||||
export const Stop = (props: IconProps) => (
|
||||
<Icon {...props}>
|
||||
<rect x="48" y="48" width="160" height="160" rx="8" />
|
||||
</Icon>
|
||||
)
|
||||
9
fakemui/icons/Video.tsx
Normal file
9
fakemui/icons/Video.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import React from 'react'
|
||||
import { Icon, IconProps } from './Icon'
|
||||
|
||||
export const Video = (props: IconProps) => (
|
||||
<Icon {...props}>
|
||||
<rect x="24" y="60" width="176" height="136" rx="8" />
|
||||
<path d="M200 112l52-32v96l-52-32v-32z" />
|
||||
</Icon>
|
||||
)
|
||||
11
fakemui/icons/Volume.tsx
Normal file
11
fakemui/icons/Volume.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import React from 'react'
|
||||
import { Icon, IconProps } from './Icon'
|
||||
|
||||
export const Volume = (props: IconProps) => (
|
||||
<Icon {...props}>
|
||||
<path d="M80 168H32a8 8 0 0 1-8-8V96a8 8 0 0 1 8-8h48l72-56v192Z" />
|
||||
<line x1="80" y1="88" x2="80" y2="168" />
|
||||
<path d="M190.5 73.5a72 72 0 0 1 0 109" />
|
||||
<path d="M166.5 97.5a32 32 0 0 1 0 61" />
|
||||
</Icon>
|
||||
)
|
||||
11
fakemui/icons/VolumeOff.tsx
Normal file
11
fakemui/icons/VolumeOff.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import React from 'react'
|
||||
import { Icon, IconProps } from './Icon'
|
||||
|
||||
export const VolumeOff = (props: IconProps) => (
|
||||
<Icon {...props}>
|
||||
<path d="M80 168H32a8 8 0 0 1-8-8V96a8 8 0 0 1 8-8h48l72-56v192Z" />
|
||||
<line x1="80" y1="88" x2="80" y2="168" />
|
||||
<line x1="240" y1="104" x2="192" y2="152" />
|
||||
<line x1="240" y1="152" x2="192" y2="104" />
|
||||
</Icon>
|
||||
)
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 = "",
|
||||
|
||||
@@ -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" }
|
||||
|
||||
14
packages/forum_forge/seed/scripts/can_moderate.lua
Normal file
14
packages/forum_forge/seed/scripts/can_moderate.lua
Normal file
@@ -0,0 +1,14 @@
|
||||
--- Check if a user can moderate content
|
||||
---@param user { role?: UserRole } User to check
|
||||
---@return boolean Whether user can moderate
|
||||
local function can_moderate(user)
|
||||
local role = user.role or "public"
|
||||
local allowed = {
|
||||
admin = true,
|
||||
god = true,
|
||||
supergod = true
|
||||
}
|
||||
return allowed[role] == true
|
||||
end
|
||||
|
||||
return can_moderate
|
||||
9
packages/forum_forge/seed/scripts/can_post.lua
Normal file
9
packages/forum_forge/seed/scripts/can_post.lua
Normal file
@@ -0,0 +1,9 @@
|
||||
--- Check if a user can create posts
|
||||
---@param user { role?: UserRole } User to check
|
||||
---@return boolean Whether user can post
|
||||
local function can_post(user)
|
||||
local role = user.role or "public"
|
||||
return role ~= "public"
|
||||
end
|
||||
|
||||
return can_post
|
||||
33
packages/forum_forge/seed/scripts/flag_post.lua
Normal file
33
packages/forum_forge/seed/scripts/flag_post.lua
Normal file
@@ -0,0 +1,33 @@
|
||||
--- Flag a post for moderation
|
||||
--- Checks content length and banned terms
|
||||
---@param post { content?: string } Post to check
|
||||
---@return FlagResult Flagging result
|
||||
local function flag_post(post)
|
||||
local banned_terms = {
|
||||
"spam",
|
||||
"scam",
|
||||
"phish",
|
||||
"abuse"
|
||||
}
|
||||
|
||||
local content = post.content or ""
|
||||
local reasons = {}
|
||||
|
||||
if #content > 5000 then
|
||||
table.insert(reasons, "Post exceeds 5000 characters")
|
||||
end
|
||||
|
||||
local lowered = string.lower(content)
|
||||
for _, term in ipairs(banned_terms) do
|
||||
if string.find(lowered, term, 1, true) then
|
||||
table.insert(reasons, "Contains banned term: " .. term)
|
||||
end
|
||||
end
|
||||
|
||||
return {
|
||||
flagged = #reasons > 0,
|
||||
reasons = reasons
|
||||
}
|
||||
end
|
||||
|
||||
return flag_post
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
13
packages/forum_forge/seed/scripts/rank_thread.lua
Normal file
13
packages/forum_forge/seed/scripts/rank_thread.lua
Normal file
@@ -0,0 +1,13 @@
|
||||
--- Calculate thread ranking score
|
||||
--- Uses replies, likes, and recency
|
||||
---@param thread ForumThread Thread to rank
|
||||
---@return number Ranking score
|
||||
local function rank_thread(thread)
|
||||
local replies = thread.replyCount or 0
|
||||
local likes = thread.likeCount or 0
|
||||
local recency = thread.lastReplyAt or thread.createdAt or 0
|
||||
|
||||
return (replies * 2) + likes + (recency / 1000000)
|
||||
end
|
||||
|
||||
return rank_thread
|
||||
@@ -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
|
||||
|
||||
55
packages/forum_forge/seed/scripts/types.lua
Normal file
55
packages/forum_forge/seed/scripts/types.lua
Normal file
@@ -0,0 +1,55 @@
|
||||
---@meta
|
||||
-- Type definitions for forum_forge package
|
||||
|
||||
---@alias UserRole "public" | "user" | "moderator" | "admin" | "god" | "supergod"
|
||||
|
||||
---@class ForumUser
|
||||
---@field id string User ID
|
||||
---@field name string Username
|
||||
---@field role UserRole User role level
|
||||
---@field avatar? string Avatar URL
|
||||
|
||||
---@class ForumPost
|
||||
---@field id string Post ID
|
||||
---@field threadId string Parent thread ID
|
||||
---@field authorId string Author user ID
|
||||
---@field content string Post content
|
||||
---@field createdAt number Created timestamp
|
||||
---@field updatedAt? number Last update timestamp
|
||||
---@field likes number Like count
|
||||
|
||||
---@class ForumThread
|
||||
---@field id string Thread ID
|
||||
---@field title string Thread title
|
||||
---@field authorId string Author user ID
|
||||
---@field forumId string Parent forum ID
|
||||
---@field replyCount number Number of replies
|
||||
---@field likeCount number Total likes
|
||||
---@field lastReplyAt number Last reply timestamp
|
||||
---@field createdAt number Created timestamp
|
||||
---@field isPinned? boolean Whether thread is pinned
|
||||
---@field isLocked? boolean Whether thread is locked
|
||||
|
||||
---@class FlagResult
|
||||
---@field flagged boolean Whether post was flagged
|
||||
---@field reasons string[] Reasons for flagging
|
||||
|
||||
---@class InstallContext
|
||||
---@field version string Version number
|
||||
|
||||
---@class InstallResult
|
||||
---@field message string Install message
|
||||
---@field version string Version installed
|
||||
|
||||
---@class UninstallResult
|
||||
---@field message string Uninstall message
|
||||
|
||||
---@class ForumForgeModule
|
||||
---@field on_install fun(context: InstallContext): InstallResult
|
||||
---@field on_uninstall fun(): UninstallResult
|
||||
---@field flag_post fun(post: { content?: string }): FlagResult
|
||||
---@field can_post fun(user: { role?: UserRole }): boolean
|
||||
---@field can_moderate fun(user: { role?: UserRole }): boolean
|
||||
---@field rank_thread fun(thread: ForumThread): number
|
||||
|
||||
return {}
|
||||
@@ -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)
|
||||
|
||||
46
packages/irc_webchat/seed/scripts/types.lua
Normal file
46
packages/irc_webchat/seed/scripts/types.lua
Normal file
@@ -0,0 +1,46 @@
|
||||
---@meta
|
||||
-- Type definitions for irc_webchat package
|
||||
|
||||
---@alias MessageType "message" | "system" | "command" | "join" | "leave"
|
||||
|
||||
---@class IRCMessage
|
||||
---@field id string Message identifier
|
||||
---@field username string Username of the sender
|
||||
---@field userId string User identifier
|
||||
---@field message string Message content
|
||||
---@field type MessageType Message type
|
||||
---@field timestamp number Timestamp in milliseconds
|
||||
---@field channelId string Channel identifier
|
||||
|
||||
---@class ChatMessage : IRCMessage
|
||||
---@field type "message"
|
||||
|
||||
---@class SystemMessage : IRCMessage
|
||||
---@field type "system"
|
||||
|
||||
---@class CommandMessage : IRCMessage
|
||||
---@field type "command"
|
||||
|
||||
---@class JoinMessage : IRCMessage
|
||||
---@field type "join"
|
||||
|
||||
---@class LeaveMessage : IRCMessage
|
||||
---@field type "leave"
|
||||
|
||||
---@class InstallContext
|
||||
---@field version string Version number
|
||||
|
||||
---@class InstallResult
|
||||
---@field message string Installation message
|
||||
---@field version? string Optional version number
|
||||
|
||||
---@class IRCWebchat
|
||||
---@field on_install fun(context: InstallContext): InstallResult
|
||||
---@field on_uninstall fun(): InstallResult
|
||||
---@field formatTime fun(timestamp: number): string
|
||||
---@field handleCommand fun(command: string, channelId: string, username: string, onlineUsers: string[]): IRCMessage
|
||||
---@field sendMessage fun(channelId: string, username: string, userId: string, message: string): ChatMessage
|
||||
---@field userJoin fun(channelId: string, username: string, userId: string): JoinMessage
|
||||
---@field userLeave fun(channelId: string, username: string, userId: string): LeaveMessage
|
||||
|
||||
return {}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -1,97 +1,16 @@
|
||||
-- Notification summary card component logic
|
||||
local json = require("json")
|
||||
--- Notification summary facade
|
||||
--- Re-exports single-function modules for backward compatibility
|
||||
|
||||
---@class NotificationSummary
|
||||
---@field severityClasses table<string, string>
|
||||
---@field defaultItems SummaryItem[]
|
||||
local M = {}
|
||||
|
||||
-- Load config from JSON file
|
||||
M.calculateTotal = require("calculate_total")
|
||||
M.getSeverityClass = require("get_severity_class")
|
||||
M.prepareSummary = require("prepare_summary")
|
||||
|
||||
-- Keep config for backward compatibility
|
||||
local json = require("json")
|
||||
local config = json.load("summary.json")
|
||||
M.severityClasses = config.severityClasses
|
||||
M.defaultItems = config.defaultItems
|
||||
|
||||
---@class SummaryItem
|
||||
---@field label string
|
||||
---@field count? number
|
||||
---@field severity? "info"|"success"|"warning"|"error"
|
||||
---@field hint? string
|
||||
|
||||
---@class EnrichedSummaryItem
|
||||
---@field label string
|
||||
---@field count number
|
||||
---@field severity "info"|"success"|"warning"|"error"
|
||||
---@field hint? string
|
||||
---@field classes string
|
||||
|
||||
---@class SummaryProps
|
||||
---@field title? string
|
||||
---@field subtitle? string
|
||||
---@field totalLabel? string
|
||||
---@field items? SummaryItem[]
|
||||
|
||||
---@class SummaryData
|
||||
---@field title string
|
||||
---@field subtitle? string
|
||||
---@field totalLabel string
|
||||
---@field total number
|
||||
---@field items EnrichedSummaryItem[]
|
||||
|
||||
-- Calculate total from items
|
||||
---@param items SummaryItem[]
|
||||
---@return number
|
||||
function M.calculateTotal(items)
|
||||
local total = 0
|
||||
for _, item in ipairs(items) do
|
||||
local count = item.count or 0
|
||||
if count > 0 then
|
||||
total = total + count
|
||||
end
|
||||
end
|
||||
return total
|
||||
end
|
||||
|
||||
-- Get severity class for an item
|
||||
---@param severity? "info"|"success"|"warning"|"error"
|
||||
---@return string
|
||||
function M.getSeverityClass(severity)
|
||||
return M.severityClasses[severity or "info"] or M.severityClasses.info
|
||||
end
|
||||
|
||||
-- Prepare summary data
|
||||
---@param props SummaryProps
|
||||
---@return SummaryData
|
||||
function M.prepareSummary(props)
|
||||
local title = props.title or "Notification Summary"
|
||||
local subtitle = props.subtitle
|
||||
local totalLabel = props.totalLabel or "Total"
|
||||
local items = props.items
|
||||
|
||||
if not items or #items == 0 then
|
||||
items = M.defaultItems
|
||||
end
|
||||
|
||||
local total = M.calculateTotal(items)
|
||||
|
||||
-- Enrich items with severity classes
|
||||
local enrichedItems = {}
|
||||
for i, item in ipairs(items) do
|
||||
enrichedItems[i] = {
|
||||
label = item.label,
|
||||
count = math.max(item.count or 0, 0),
|
||||
severity = item.severity or "info",
|
||||
hint = item.hint,
|
||||
classes = M.getSeverityClass(item.severity)
|
||||
}
|
||||
end
|
||||
|
||||
return {
|
||||
title = title,
|
||||
subtitle = subtitle,
|
||||
totalLabel = totalLabel,
|
||||
total = total,
|
||||
items = enrichedItems
|
||||
}
|
||||
end
|
||||
M.severityClasses = config and config.severityClasses or {}
|
||||
M.defaultItems = config and config.defaultItems or {}
|
||||
|
||||
return M
|
||||
|
||||
@@ -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
|
||||
|
||||
15
packages/notification_center/seed/scripts/toast_error.lua
Normal file
15
packages/notification_center/seed/scripts/toast_error.lua
Normal file
@@ -0,0 +1,15 @@
|
||||
--- Create an error toast notification
|
||||
---@param message string Toast message
|
||||
---@param duration? number Display duration in ms (default 5000)
|
||||
---@return Toast Toast configuration
|
||||
local function toast_error(message, duration)
|
||||
return {
|
||||
type = "toast",
|
||||
variant = "error",
|
||||
message = message,
|
||||
duration = duration or 5000,
|
||||
icon = "error"
|
||||
}
|
||||
end
|
||||
|
||||
return toast_error
|
||||
15
packages/notification_center/seed/scripts/toast_info.lua
Normal file
15
packages/notification_center/seed/scripts/toast_info.lua
Normal file
@@ -0,0 +1,15 @@
|
||||
--- Create an info toast notification
|
||||
---@param message string Toast message
|
||||
---@param duration? number Display duration in ms (default 3000)
|
||||
---@return Toast Toast configuration
|
||||
local function toast_info(message, duration)
|
||||
return {
|
||||
type = "toast",
|
||||
variant = "info",
|
||||
message = message,
|
||||
duration = duration or 3000,
|
||||
icon = "info"
|
||||
}
|
||||
end
|
||||
|
||||
return toast_info
|
||||
15
packages/notification_center/seed/scripts/toast_success.lua
Normal file
15
packages/notification_center/seed/scripts/toast_success.lua
Normal file
@@ -0,0 +1,15 @@
|
||||
--- Create a success toast notification
|
||||
---@param message string Toast message
|
||||
---@param duration? number Display duration in ms (default 3000)
|
||||
---@return Toast Toast configuration
|
||||
local function toast_success(message, duration)
|
||||
return {
|
||||
type = "toast",
|
||||
variant = "success",
|
||||
message = message,
|
||||
duration = duration or 3000,
|
||||
icon = "check"
|
||||
}
|
||||
end
|
||||
|
||||
return toast_success
|
||||
15
packages/notification_center/seed/scripts/toast_warning.lua
Normal file
15
packages/notification_center/seed/scripts/toast_warning.lua
Normal file
@@ -0,0 +1,15 @@
|
||||
--- Create a warning toast notification
|
||||
---@param message string Toast message
|
||||
---@param duration? number Display duration in ms (default 4000)
|
||||
---@return Toast Toast configuration
|
||||
local function toast_warning(message, duration)
|
||||
return {
|
||||
type = "toast",
|
||||
variant = "warning",
|
||||
message = message,
|
||||
duration = duration or 4000,
|
||||
icon = "warning"
|
||||
}
|
||||
end
|
||||
|
||||
return toast_warning
|
||||
80
packages/notification_center/seed/scripts/types.lua
Normal file
80
packages/notification_center/seed/scripts/types.lua
Normal file
@@ -0,0 +1,80 @@
|
||||
---@meta
|
||||
-- Type definitions for notification_center package
|
||||
|
||||
---@alias NotificationSeverity "info" | "success" | "warning" | "error"
|
||||
|
||||
---@class Notification
|
||||
---@field id string|number Notification ID
|
||||
---@field title string Title text
|
||||
---@field message string Message content
|
||||
---@field created_at string|number Creation timestamp
|
||||
---@field read? boolean Whether notification has been read
|
||||
---@field icon? string Icon name
|
||||
|
||||
---@class UIComponent
|
||||
---@field type string Component type
|
||||
---@field props? table Component props
|
||||
---@field children? table[] Child components
|
||||
---@field content? string Text content
|
||||
---@field variant? string Variant name
|
||||
|
||||
---@class NotificationItemComponent
|
||||
---@field type "notification_item"
|
||||
---@field props NotificationItemProps
|
||||
|
||||
---@class NotificationItemProps
|
||||
---@field id string|number
|
||||
---@field title string
|
||||
---@field message string
|
||||
---@field time string|number
|
||||
---@field read boolean
|
||||
---@field icon string
|
||||
|
||||
---@class NotificationListComponent
|
||||
---@field type "notification_list"
|
||||
---@field children NotificationItemComponent[]
|
||||
|
||||
---@class BadgeComponent
|
||||
---@field type "badge"
|
||||
---@field content string
|
||||
---@field variant string
|
||||
|
||||
---@class Toast
|
||||
---@field type "toast"
|
||||
---@field variant NotificationSeverity
|
||||
---@field message string
|
||||
---@field duration number
|
||||
---@field icon string
|
||||
|
||||
---@class SummaryItem
|
||||
---@field label string Item label
|
||||
---@field count? number Item count
|
||||
---@field severity? NotificationSeverity Severity level
|
||||
---@field hint? string Hint text
|
||||
|
||||
---@class EnrichedSummaryItem
|
||||
---@field label string
|
||||
---@field count number
|
||||
---@field severity NotificationSeverity
|
||||
---@field hint? string
|
||||
---@field classes string
|
||||
|
||||
---@class SummaryProps
|
||||
---@field title? string Summary title
|
||||
---@field subtitle? string Summary subtitle
|
||||
---@field totalLabel? string Total label
|
||||
---@field items? SummaryItem[] Summary items
|
||||
|
||||
---@class SummaryData
|
||||
---@field title string
|
||||
---@field subtitle? string
|
||||
---@field totalLabel string
|
||||
---@field total number
|
||||
---@field items EnrichedSummaryItem[]
|
||||
|
||||
---@class NotificationCenterInfo
|
||||
---@field name string
|
||||
---@field version string
|
||||
---@field loaded boolean
|
||||
|
||||
return {}
|
||||
@@ -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"
|
||||
|
||||
110
services/media_daemon/Dockerfile
Normal file
110
services/media_daemon/Dockerfile
Normal file
@@ -0,0 +1,110 @@
|
||||
# Multi-stage build for Media Daemon
|
||||
# Stage 1: Build environment
|
||||
FROM ubuntu:22.04 AS builder
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
# Install build dependencies
|
||||
RUN apt-get update && apt-get install -y \
|
||||
build-essential \
|
||||
cmake \
|
||||
ninja-build \
|
||||
git \
|
||||
curl \
|
||||
pkg-config \
|
||||
python3 \
|
||||
python3-pip \
|
||||
libssl-dev \
|
||||
libc-ares-dev \
|
||||
libbrotli-dev \
|
||||
libcurl4-openssl-dev \
|
||||
uuid-dev \
|
||||
zlib1g-dev \
|
||||
libpq-dev \
|
||||
libmysqlclient-dev \
|
||||
libsqlite3-dev \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install Conan
|
||||
RUN pip3 install conan
|
||||
|
||||
# Configure Conan
|
||||
RUN conan profile detect
|
||||
|
||||
# Copy source code
|
||||
WORKDIR /app
|
||||
COPY . .
|
||||
|
||||
# Install dependencies via Conan
|
||||
WORKDIR /app/build
|
||||
RUN conan install ../build-config/conanfile.txt \
|
||||
--output-folder=. \
|
||||
--build=missing \
|
||||
-s build_type=Release
|
||||
|
||||
# Build
|
||||
RUN cmake ../build-config \
|
||||
-G Ninja \
|
||||
-DCMAKE_BUILD_TYPE=Release \
|
||||
-DCMAKE_TOOLCHAIN_FILE=conan_toolchain.cmake
|
||||
|
||||
RUN ninja
|
||||
|
||||
# Stage 2: Runtime image
|
||||
FROM ubuntu:22.04 AS runtime
|
||||
|
||||
ENV DEBIAN_FRONTEND=noninteractive
|
||||
|
||||
# Install runtime dependencies
|
||||
RUN apt-get update && apt-get install -y \
|
||||
ffmpeg \
|
||||
imagemagick \
|
||||
pandoc \
|
||||
texlive-xetex \
|
||||
texlive-fonts-recommended \
|
||||
fonts-liberation \
|
||||
curl \
|
||||
ca-certificates \
|
||||
libssl3 \
|
||||
libc-ares2 \
|
||||
libcurl4 \
|
||||
libpq5 \
|
||||
libmysqlclient21 \
|
||||
libsqlite3-0 \
|
||||
tini \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Create non-root user
|
||||
RUN groupadd -r media && useradd -r -g media media
|
||||
|
||||
# Create directories
|
||||
RUN mkdir -p /data/media /data/temp /data/output /data/cache \
|
||||
/data/hls/radio /data/hls/tv \
|
||||
/plugins /etc/media-daemon /var/log/media-daemon \
|
||||
&& chown -R media:media /data /plugins /var/log/media-daemon
|
||||
|
||||
# Copy binary
|
||||
COPY --from=builder /app/build/media_daemon /usr/local/bin/
|
||||
|
||||
# Copy default config
|
||||
COPY config/media-daemon.yaml /etc/media-daemon/config.yaml
|
||||
|
||||
# ImageMagick policy (allow PDF processing)
|
||||
RUN sed -i 's/rights="none" pattern="PDF"/rights="read|write" pattern="PDF"/' /etc/ImageMagick-6/policy.xml || true
|
||||
|
||||
# Switch to non-root user
|
||||
USER media
|
||||
|
||||
# Expose ports
|
||||
EXPOSE 8090
|
||||
|
||||
# Health check
|
||||
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
|
||||
CMD curl -f http://localhost:8090/health || exit 1
|
||||
|
||||
# Volumes
|
||||
VOLUME ["/data/media", "/data/output", "/data/hls", "/plugins"]
|
||||
|
||||
# Entry point
|
||||
ENTRYPOINT ["/usr/bin/tini", "--"]
|
||||
CMD ["media_daemon", "--config", "/etc/media-daemon/config.yaml"]
|
||||
115
services/media_daemon/README.md
Normal file
115
services/media_daemon/README.md
Normal file
@@ -0,0 +1,115 @@
|
||||
# MetaBuilder Media Daemon
|
||||
|
||||
A C++ Docker daemon for media processing, radio streaming, and TV channel simulation with a modular plugin architecture.
|
||||
|
||||
## Features
|
||||
|
||||
### 🎬 Job Queue System
|
||||
- Video transcoding (H.264, H.265, VP9, AV1)
|
||||
- Audio transcoding (MP3, AAC, FLAC, Opus)
|
||||
- Document conversion (PDF, Office formats)
|
||||
- Image processing (resize, convert, optimize)
|
||||
- Priority queuing with configurable workers
|
||||
|
||||
### 📻 Radio Streaming
|
||||
- Live audio streaming with multiple quality levels
|
||||
- Playlist scheduling and auto-DJ
|
||||
- Crossfading and audio normalization
|
||||
- Metadata injection (artist, title, album art)
|
||||
- Icecast/Shoutcast compatible output
|
||||
|
||||
### 📺 TV Channel Simulation
|
||||
- Multi-channel scheduling system
|
||||
- EPG (Electronic Program Guide) generation
|
||||
- Bumpers, commercials, and interstitials
|
||||
- Live-to-VOD and VOD-to-Live workflows
|
||||
- HLS/DASH output for web players
|
||||
|
||||
### 🔌 Plugin Architecture
|
||||
- Dynamic plugin loading (.so/.dll)
|
||||
- Hot-reload support in development mode
|
||||
- Plugin API for custom processors
|
||||
- Built-in plugins: FFmpeg, ImageMagick, Pandoc
|
||||
|
||||
### 🔗 DBAL Integration
|
||||
- Job status notifications to users
|
||||
- Progress tracking in real-time
|
||||
- User permission checks
|
||||
- Multi-tenant isolation
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Media Daemon │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
|
||||
│ │ HTTP API │ │ WebSocket │ │ gRPC │ │
|
||||
│ │ (Drogon) │ │ (Live) │ │ (Internal) │ │
|
||||
│ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │
|
||||
│ └────────────────┼────────────────┘ │
|
||||
│ ▼ │
|
||||
│ ┌───────────────────────────────────────────────────────┐ │
|
||||
│ │ Core Engine │ │
|
||||
│ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │
|
||||
│ │ │ Job │ │ Radio │ │ TV │ │ Plugin │ │ │
|
||||
│ │ │ Queue │ │ Engine │ │ Engine │ │ Manager │ │ │
|
||||
│ │ └────┬────┘ └────┬────┘ └────┬────┘ └────┬────┘ │ │
|
||||
│ └───────┼────────────┼────────────┼────────────┼───────┘ │
|
||||
│ └────────────┴────────────┴────────────┘ │
|
||||
│ ▼ │
|
||||
│ ┌───────────────────────────────────────────────────────┐ │
|
||||
│ │ Plugin System │ │
|
||||
│ │ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ │ │
|
||||
│ │ │FFmpeg │ │ImageMag│ │Pandoc │ │Custom │ │ │
|
||||
│ │ │Plugin │ │Plugin │ │Plugin │ │Plugins │ │ │
|
||||
│ │ └────────┘ └────────┘ └────────┘ └────────┘ │ │
|
||||
│ └───────────────────────────────────────────────────────┘ │
|
||||
│ ▼ │
|
||||
│ ┌───────────────────────────────────────────────────────┐ │
|
||||
│ │ DBAL Client (Notifications) │ │
|
||||
│ └───────────────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
```bash
|
||||
# Build
|
||||
docker build -t metabuilder/media-daemon -f Dockerfile .
|
||||
|
||||
# Run
|
||||
docker run -d \
|
||||
-p 8090:8090 \
|
||||
-v /media:/data/media \
|
||||
-e DBAL_URL=http://dbal:8080 \
|
||||
metabuilder/media-daemon
|
||||
```
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### Job Queue
|
||||
- `POST /api/jobs` - Submit a new job
|
||||
- `GET /api/jobs/:id` - Get job status
|
||||
- `GET /api/jobs` - List jobs (filtered by user/tenant)
|
||||
- `DELETE /api/jobs/:id` - Cancel a job
|
||||
|
||||
### Radio
|
||||
- `POST /api/radio/channels` - Create radio channel
|
||||
- `GET /api/radio/channels/:id/stream` - Get stream URL
|
||||
- `POST /api/radio/channels/:id/playlist` - Update playlist
|
||||
- `GET /api/radio/channels/:id/now-playing` - Current track info
|
||||
|
||||
### TV Channels
|
||||
- `POST /api/tv/channels` - Create TV channel
|
||||
- `GET /api/tv/channels/:id/schedule` - Get EPG
|
||||
- `POST /api/tv/channels/:id/schedule` - Update schedule
|
||||
- `GET /api/tv/channels/:id/stream.m3u8` - HLS playlist
|
||||
|
||||
### Plugins
|
||||
- `GET /api/plugins` - List loaded plugins
|
||||
- `POST /api/plugins/:id/reload` - Hot-reload plugin (dev mode)
|
||||
|
||||
## Configuration
|
||||
|
||||
See `config/media-daemon.yaml` for full configuration options.
|
||||
159
services/media_daemon/build-config/CMakeLists.txt
Normal file
159
services/media_daemon/build-config/CMakeLists.txt
Normal file
@@ -0,0 +1,159 @@
|
||||
cmake_minimum_required(VERSION 3.20)
|
||||
project(media_daemon VERSION 1.0.0 LANGUAGES CXX)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
|
||||
set(CMAKE_POSITION_INDEPENDENT_CODE ON)
|
||||
|
||||
# Directories
|
||||
set(MEDIA_ROOT ${CMAKE_CURRENT_LIST_DIR}/..)
|
||||
set(MEDIA_SRC_DIR ${MEDIA_ROOT}/src)
|
||||
set(MEDIA_INCLUDE_DIR ${MEDIA_ROOT}/include)
|
||||
set(MEDIA_TEST_DIR ${MEDIA_ROOT}/tests)
|
||||
|
||||
# Find required packages
|
||||
find_package(Threads REQUIRED)
|
||||
find_package(Drogon REQUIRED CONFIG)
|
||||
find_package(cpr REQUIRED CONFIG)
|
||||
find_package(fmt QUIET)
|
||||
find_package(spdlog QUIET)
|
||||
find_package(nlohmann_json QUIET)
|
||||
|
||||
# Include directories
|
||||
include_directories(
|
||||
${MEDIA_INCLUDE_DIR}
|
||||
${MEDIA_SRC_DIR}
|
||||
)
|
||||
|
||||
# ============================================================================
|
||||
# Core Library
|
||||
# ============================================================================
|
||||
|
||||
add_library(media_core STATIC
|
||||
${MEDIA_SRC_DIR}/types.cpp
|
||||
${MEDIA_SRC_DIR}/plugin_manager.cpp
|
||||
${MEDIA_SRC_DIR}/job_queue.cpp
|
||||
${MEDIA_SRC_DIR}/radio_engine.cpp
|
||||
${MEDIA_SRC_DIR}/tv_engine.cpp
|
||||
${MEDIA_SRC_DIR}/dbal_client.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(media_core
|
||||
Threads::Threads
|
||||
cpr::cpr
|
||||
${CMAKE_DL_LIBS} # For dynamic loading
|
||||
)
|
||||
|
||||
if(fmt_FOUND)
|
||||
target_link_libraries(media_core fmt::fmt)
|
||||
endif()
|
||||
|
||||
if(spdlog_FOUND)
|
||||
target_link_libraries(media_core spdlog::spdlog)
|
||||
endif()
|
||||
|
||||
# ============================================================================
|
||||
# Built-in Plugins
|
||||
# ============================================================================
|
||||
|
||||
add_library(media_plugins STATIC
|
||||
${MEDIA_SRC_DIR}/plugins/ffmpeg_plugin.cpp
|
||||
${MEDIA_SRC_DIR}/plugins/imagemagick_plugin.cpp
|
||||
${MEDIA_SRC_DIR}/plugins/pandoc_plugin.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(media_plugins
|
||||
media_core
|
||||
)
|
||||
|
||||
# ============================================================================
|
||||
# Server
|
||||
# ============================================================================
|
||||
|
||||
add_library(media_server STATIC
|
||||
${MEDIA_SRC_DIR}/server.cpp
|
||||
${MEDIA_SRC_DIR}/routes/health_routes.cpp
|
||||
${MEDIA_SRC_DIR}/routes/job_routes.cpp
|
||||
${MEDIA_SRC_DIR}/routes/radio_routes.cpp
|
||||
${MEDIA_SRC_DIR}/routes/tv_routes.cpp
|
||||
${MEDIA_SRC_DIR}/routes/plugin_routes.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(media_server
|
||||
media_core
|
||||
media_plugins
|
||||
Drogon::Drogon
|
||||
)
|
||||
|
||||
# ============================================================================
|
||||
# Main Executable
|
||||
# ============================================================================
|
||||
|
||||
add_executable(media_daemon
|
||||
${MEDIA_SRC_DIR}/main.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(media_daemon
|
||||
media_server
|
||||
media_core
|
||||
media_plugins
|
||||
)
|
||||
|
||||
# ============================================================================
|
||||
# Plugin Shared Library Template
|
||||
# ============================================================================
|
||||
|
||||
# Example of how to build an external plugin
|
||||
# add_library(my_custom_plugin SHARED
|
||||
# ${MEDIA_SRC_DIR}/plugins/custom/my_plugin.cpp
|
||||
# )
|
||||
# target_link_libraries(my_custom_plugin media_core)
|
||||
|
||||
# ============================================================================
|
||||
# Tests
|
||||
# ============================================================================
|
||||
|
||||
enable_testing()
|
||||
|
||||
add_executable(job_queue_test
|
||||
${MEDIA_TEST_DIR}/unit/job_queue_test.cpp
|
||||
)
|
||||
target_link_libraries(job_queue_test media_core)
|
||||
|
||||
add_executable(plugin_manager_test
|
||||
${MEDIA_TEST_DIR}/unit/plugin_manager_test.cpp
|
||||
)
|
||||
target_link_libraries(plugin_manager_test media_core)
|
||||
|
||||
add_executable(radio_engine_test
|
||||
${MEDIA_TEST_DIR}/unit/radio_engine_test.cpp
|
||||
)
|
||||
target_link_libraries(radio_engine_test media_core)
|
||||
|
||||
add_executable(tv_engine_test
|
||||
${MEDIA_TEST_DIR}/unit/tv_engine_test.cpp
|
||||
)
|
||||
target_link_libraries(tv_engine_test media_core)
|
||||
|
||||
add_test(NAME job_queue_test COMMAND job_queue_test)
|
||||
add_test(NAME plugin_manager_test COMMAND plugin_manager_test)
|
||||
add_test(NAME radio_engine_test COMMAND radio_engine_test)
|
||||
add_test(NAME tv_engine_test COMMAND tv_engine_test)
|
||||
|
||||
# ============================================================================
|
||||
# Installation
|
||||
# ============================================================================
|
||||
|
||||
install(TARGETS media_daemon
|
||||
RUNTIME DESTINATION bin
|
||||
)
|
||||
|
||||
install(DIRECTORY ${MEDIA_INCLUDE_DIR}/media
|
||||
DESTINATION include
|
||||
)
|
||||
|
||||
install(FILES ${MEDIA_ROOT}/config/media-daemon.yaml
|
||||
DESTINATION etc/media-daemon
|
||||
RENAME config.yaml
|
||||
)
|
||||
15
services/media_daemon/build-config/conanfile.txt
Normal file
15
services/media_daemon/build-config/conanfile.txt
Normal file
@@ -0,0 +1,15 @@
|
||||
[requires]
|
||||
fmt/10.1.1
|
||||
spdlog/1.12.0
|
||||
nlohmann_json/3.11.2
|
||||
drogon/1.8.7
|
||||
cpr/1.10.4
|
||||
|
||||
[generators]
|
||||
CMakeToolchain
|
||||
CMakeDeps
|
||||
|
||||
[options]
|
||||
drogon/*:with_postgres=True
|
||||
drogon/*:with_mysql=True
|
||||
drogon/*:with_sqlite=True
|
||||
197
services/media_daemon/config/media-daemon.yaml
Normal file
197
services/media_daemon/config/media-daemon.yaml
Normal file
@@ -0,0 +1,197 @@
|
||||
# Media Daemon Configuration
|
||||
version: "1.0"
|
||||
|
||||
# Server settings
|
||||
server:
|
||||
bind_address: "0.0.0.0"
|
||||
port: 8090
|
||||
workers: 4
|
||||
development_mode: false
|
||||
|
||||
# DBAL connection for notifications
|
||||
dbal:
|
||||
url: "${DBAL_URL:-http://localhost:8080}"
|
||||
api_key: "${DBAL_API_KEY}"
|
||||
timeout_ms: 5000
|
||||
retry_attempts: 3
|
||||
|
||||
# Job queue configuration
|
||||
job_queue:
|
||||
# Maximum concurrent jobs per type
|
||||
workers:
|
||||
video: 2
|
||||
audio: 4
|
||||
document: 4
|
||||
image: 8
|
||||
|
||||
# Job priority levels (lower = higher priority)
|
||||
priorities:
|
||||
urgent: 0
|
||||
high: 1
|
||||
normal: 2
|
||||
low: 3
|
||||
background: 4
|
||||
|
||||
# Job retention
|
||||
retention:
|
||||
completed_hours: 24
|
||||
failed_hours: 168 # 7 days
|
||||
|
||||
# Temp storage
|
||||
temp_dir: "/data/temp"
|
||||
output_dir: "/data/output"
|
||||
|
||||
# Radio streaming configuration
|
||||
radio:
|
||||
enabled: true
|
||||
max_channels: 10
|
||||
|
||||
# Audio settings
|
||||
audio:
|
||||
sample_rate: 44100
|
||||
channels: 2
|
||||
bitrates: [64, 128, 192, 320] # kbps
|
||||
default_bitrate: 128
|
||||
codec: "mp3" # mp3, aac, opus
|
||||
|
||||
# Crossfade settings
|
||||
crossfade:
|
||||
enabled: true
|
||||
duration_ms: 3000
|
||||
|
||||
# Normalization
|
||||
normalization:
|
||||
enabled: true
|
||||
target_lufs: -14.0
|
||||
|
||||
# Output formats
|
||||
outputs:
|
||||
- type: "icecast"
|
||||
host: "${ICECAST_HOST:-localhost}"
|
||||
port: 8000
|
||||
mount: "/radio"
|
||||
password: "${ICECAST_PASSWORD}"
|
||||
- type: "hls"
|
||||
segment_duration: 6
|
||||
playlist_size: 5
|
||||
|
||||
# TV channel simulation
|
||||
tv:
|
||||
enabled: true
|
||||
max_channels: 5
|
||||
|
||||
# Video settings
|
||||
video:
|
||||
resolutions:
|
||||
- name: "1080p"
|
||||
width: 1920
|
||||
height: 1080
|
||||
bitrate: 5000
|
||||
- name: "720p"
|
||||
width: 1280
|
||||
height: 720
|
||||
bitrate: 2500
|
||||
- name: "480p"
|
||||
width: 854
|
||||
height: 480
|
||||
bitrate: 1000
|
||||
codec: "h264" # h264, h265, vp9
|
||||
preset: "fast"
|
||||
|
||||
# Audio settings
|
||||
audio:
|
||||
codec: "aac"
|
||||
bitrate: 128
|
||||
sample_rate: 48000
|
||||
|
||||
# HLS output settings
|
||||
hls:
|
||||
segment_duration: 4
|
||||
playlist_size: 10
|
||||
output_dir: "/data/hls"
|
||||
|
||||
# EPG settings
|
||||
epg:
|
||||
lookahead_hours: 24
|
||||
refresh_interval_minutes: 15
|
||||
|
||||
# Plugin system
|
||||
plugins:
|
||||
enabled: true
|
||||
directory: "/plugins"
|
||||
hot_reload: false # Enable in development mode
|
||||
|
||||
# Built-in plugins
|
||||
builtin:
|
||||
ffmpeg:
|
||||
enabled: true
|
||||
binary: "/usr/bin/ffmpeg"
|
||||
hardware_accel: "auto" # auto, nvidia, vaapi, none
|
||||
|
||||
imagemagick:
|
||||
enabled: true
|
||||
binary: "/usr/bin/convert"
|
||||
memory_limit: "512MB"
|
||||
|
||||
pandoc:
|
||||
enabled: true
|
||||
binary: "/usr/bin/pandoc"
|
||||
pdf_engine: "xelatex"
|
||||
|
||||
# Storage backends
|
||||
storage:
|
||||
# Local filesystem
|
||||
local:
|
||||
media_dir: "/data/media"
|
||||
cache_dir: "/data/cache"
|
||||
max_cache_size_gb: 50
|
||||
|
||||
# S3-compatible storage (optional)
|
||||
s3:
|
||||
enabled: false
|
||||
endpoint: "${S3_ENDPOINT}"
|
||||
bucket: "${S3_BUCKET}"
|
||||
access_key: "${S3_ACCESS_KEY}"
|
||||
secret_key: "${S3_SECRET_KEY}"
|
||||
region: "us-east-1"
|
||||
|
||||
# Monitoring
|
||||
monitoring:
|
||||
# Prometheus metrics
|
||||
prometheus:
|
||||
enabled: true
|
||||
endpoint: "/metrics"
|
||||
|
||||
# Health checks
|
||||
health:
|
||||
endpoint: "/health"
|
||||
include_details: true
|
||||
|
||||
# Logging
|
||||
logging:
|
||||
level: "info" # trace, debug, info, warn, error
|
||||
format: "json" # json, text
|
||||
output: "stdout" # stdout, file
|
||||
file:
|
||||
path: "/var/log/media-daemon.log"
|
||||
max_size_mb: 100
|
||||
max_files: 5
|
||||
|
||||
# Security
|
||||
security:
|
||||
# API authentication
|
||||
api_keys:
|
||||
enabled: true
|
||||
header: "X-API-Key"
|
||||
|
||||
# Rate limiting
|
||||
rate_limit:
|
||||
enabled: true
|
||||
requests_per_minute: 100
|
||||
burst: 20
|
||||
|
||||
# CORS
|
||||
cors:
|
||||
enabled: true
|
||||
origins: ["*"]
|
||||
methods: ["GET", "POST", "PUT", "DELETE"]
|
||||
91
services/media_daemon/config/nginx-stream.conf
Normal file
91
services/media_daemon/config/nginx-stream.conf
Normal file
@@ -0,0 +1,91 @@
|
||||
# Nginx configuration for HLS streaming
|
||||
# Optimized for low-latency video delivery
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name _;
|
||||
|
||||
# Enable CORS
|
||||
add_header 'Access-Control-Allow-Origin' '*' always;
|
||||
add_header 'Access-Control-Allow-Methods' 'GET, OPTIONS' always;
|
||||
add_header 'Access-Control-Allow-Headers' 'Origin, Content-Type, Accept, Range' always;
|
||||
add_header 'Access-Control-Expose-Headers' 'Content-Length, Content-Range' always;
|
||||
|
||||
# Cache settings
|
||||
add_header Cache-Control 'no-cache';
|
||||
|
||||
# HLS/DASH content
|
||||
location /hls {
|
||||
alias /data/hls;
|
||||
|
||||
# MIME types for HLS
|
||||
types {
|
||||
application/vnd.apple.mpegurl m3u8;
|
||||
video/mp2t ts;
|
||||
application/dash+xml mpd;
|
||||
video/mp4 mp4 m4s;
|
||||
}
|
||||
|
||||
# Caching for segments
|
||||
location ~ \.ts$ {
|
||||
add_header Cache-Control 'public, max-age=31536000';
|
||||
}
|
||||
|
||||
location ~ \.m4s$ {
|
||||
add_header Cache-Control 'public, max-age=31536000';
|
||||
}
|
||||
|
||||
# No caching for playlists
|
||||
location ~ \.m3u8$ {
|
||||
add_header Cache-Control 'no-cache, no-store, must-revalidate';
|
||||
add_header Pragma 'no-cache';
|
||||
add_header Expires '0';
|
||||
}
|
||||
|
||||
location ~ \.mpd$ {
|
||||
add_header Cache-Control 'no-cache, no-store, must-revalidate';
|
||||
add_header Pragma 'no-cache';
|
||||
add_header Expires '0';
|
||||
}
|
||||
}
|
||||
|
||||
# Radio streams (HLS)
|
||||
location /radio {
|
||||
alias /data/hls/radio;
|
||||
|
||||
types {
|
||||
application/vnd.apple.mpegurl m3u8;
|
||||
video/mp2t ts;
|
||||
audio/aac aac;
|
||||
}
|
||||
|
||||
location ~ \.m3u8$ {
|
||||
add_header Cache-Control 'no-cache';
|
||||
}
|
||||
}
|
||||
|
||||
# TV streams (HLS)
|
||||
location /tv {
|
||||
alias /data/hls/tv;
|
||||
|
||||
types {
|
||||
application/vnd.apple.mpegurl m3u8;
|
||||
video/mp2t ts;
|
||||
video/mp4 mp4 m4s;
|
||||
}
|
||||
|
||||
location ~ \.m3u8$ {
|
||||
add_header Cache-Control 'no-cache';
|
||||
}
|
||||
}
|
||||
|
||||
# Health check
|
||||
location /nginx-health {
|
||||
return 200 'OK';
|
||||
add_header Content-Type text/plain;
|
||||
}
|
||||
|
||||
# Gzip
|
||||
gzip on;
|
||||
gzip_types application/vnd.apple.mpegurl application/dash+xml;
|
||||
}
|
||||
190
services/media_daemon/docker-compose.yml
Normal file
190
services/media_daemon/docker-compose.yml
Normal file
@@ -0,0 +1,190 @@
|
||||
# Media Daemon Docker Compose
|
||||
# Use with: docker-compose -f docker-compose.yml up -d
|
||||
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
# ============================================================================
|
||||
# Media Daemon
|
||||
# ============================================================================
|
||||
media-daemon:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
image: metabuilder/media-daemon:latest
|
||||
container_name: media-daemon
|
||||
restart: unless-stopped
|
||||
|
||||
ports:
|
||||
- "8090:8090"
|
||||
|
||||
environment:
|
||||
# Server
|
||||
- MEDIA_BIND_ADDRESS=0.0.0.0
|
||||
- MEDIA_PORT=8090
|
||||
- MEDIA_WORKERS=4
|
||||
- MEDIA_DEV_MODE=false
|
||||
|
||||
# DBAL Connection
|
||||
- DBAL_URL=http://dbal-daemon:8080
|
||||
- DBAL_API_KEY=${DBAL_API_KEY:-}
|
||||
|
||||
# Job Queue
|
||||
- MEDIA_VIDEO_WORKERS=2
|
||||
- MEDIA_AUDIO_WORKERS=4
|
||||
- MEDIA_DOC_WORKERS=4
|
||||
- MEDIA_IMAGE_WORKERS=8
|
||||
- MEDIA_TEMP_DIR=/data/temp
|
||||
- MEDIA_OUTPUT_DIR=/data/output
|
||||
|
||||
# Radio
|
||||
- MEDIA_RADIO_ENABLED=true
|
||||
- MEDIA_RADIO_MAX_CHANNELS=10
|
||||
- MEDIA_RADIO_HLS_DIR=/data/hls/radio
|
||||
|
||||
# TV
|
||||
- MEDIA_TV_ENABLED=true
|
||||
- MEDIA_TV_MAX_CHANNELS=5
|
||||
- MEDIA_TV_HLS_DIR=/data/hls/tv
|
||||
|
||||
# Icecast (optional)
|
||||
- ICECAST_HOST=${ICECAST_HOST:-icecast}
|
||||
- ICECAST_PASSWORD=${ICECAST_PASSWORD:-hackme}
|
||||
|
||||
volumes:
|
||||
# Media storage
|
||||
- media-library:/data/media:ro
|
||||
- media-output:/data/output
|
||||
- media-cache:/data/cache
|
||||
|
||||
# HLS segments (serve via nginx)
|
||||
- hls-output:/data/hls
|
||||
|
||||
# Temp (consider tmpfs for performance)
|
||||
- media-temp:/data/temp
|
||||
|
||||
# Custom plugins
|
||||
- ./plugins:/plugins:ro
|
||||
|
||||
# Custom config (optional)
|
||||
# - ./config/media-daemon.yaml:/etc/media-daemon/config.yaml:ro
|
||||
|
||||
deploy:
|
||||
resources:
|
||||
limits:
|
||||
cpus: '4'
|
||||
memory: 4G
|
||||
reservations:
|
||||
cpus: '1'
|
||||
memory: 1G
|
||||
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:8090/health"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
start_period: 10s
|
||||
|
||||
networks:
|
||||
- metabuilder-net
|
||||
|
||||
depends_on:
|
||||
dbal-daemon:
|
||||
condition: service_healthy
|
||||
|
||||
# ============================================================================
|
||||
# Icecast Server (Radio Output)
|
||||
# ============================================================================
|
||||
icecast:
|
||||
image: libretime/icecast:2.4.4
|
||||
container_name: icecast
|
||||
restart: unless-stopped
|
||||
|
||||
ports:
|
||||
- "8000:8000"
|
||||
|
||||
environment:
|
||||
- ICECAST_SOURCE_PASSWORD=${ICECAST_PASSWORD:-hackme}
|
||||
- ICECAST_ADMIN_PASSWORD=${ICECAST_ADMIN_PASSWORD:-hackme}
|
||||
- ICECAST_RELAY_PASSWORD=${ICECAST_PASSWORD:-hackme}
|
||||
- ICECAST_HOSTNAME=${ICECAST_HOSTNAME:-localhost}
|
||||
- ICECAST_MAX_CLIENTS=100
|
||||
- ICECAST_MAX_SOURCES=10
|
||||
|
||||
volumes:
|
||||
- icecast-logs:/var/log/icecast2
|
||||
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:8000/status-json.xsl"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
|
||||
networks:
|
||||
- metabuilder-net
|
||||
|
||||
# ============================================================================
|
||||
# Nginx (HLS Streaming Server)
|
||||
# ============================================================================
|
||||
nginx-stream:
|
||||
image: nginx:alpine
|
||||
container_name: nginx-stream
|
||||
restart: unless-stopped
|
||||
|
||||
ports:
|
||||
- "8080:80" # HLS streams
|
||||
|
||||
volumes:
|
||||
- hls-output:/data/hls:ro
|
||||
- ./config/nginx-stream.conf:/etc/nginx/conf.d/default.conf:ro
|
||||
|
||||
networks:
|
||||
- metabuilder-net
|
||||
|
||||
# ============================================================================
|
||||
# Redis (optional - for distributed job queue)
|
||||
# ============================================================================
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
container_name: media-redis
|
||||
restart: unless-stopped
|
||||
|
||||
command: redis-server --appendonly yes
|
||||
|
||||
volumes:
|
||||
- redis-data:/data
|
||||
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
|
||||
networks:
|
||||
- metabuilder-net
|
||||
|
||||
# ============================================================================
|
||||
# Networks
|
||||
# ============================================================================
|
||||
networks:
|
||||
metabuilder-net:
|
||||
external: true
|
||||
|
||||
# ============================================================================
|
||||
# Volumes
|
||||
# ============================================================================
|
||||
volumes:
|
||||
media-library:
|
||||
name: metabuilder-media-library
|
||||
media-output:
|
||||
name: metabuilder-media-output
|
||||
media-cache:
|
||||
name: metabuilder-media-cache
|
||||
media-temp:
|
||||
name: metabuilder-media-temp
|
||||
hls-output:
|
||||
name: metabuilder-hls-output
|
||||
icecast-logs:
|
||||
name: metabuilder-icecast-logs
|
||||
redis-data:
|
||||
name: metabuilder-media-redis
|
||||
251
services/media_daemon/include/media/dbal_client.hpp
Normal file
251
services/media_daemon/include/media/dbal_client.hpp
Normal file
@@ -0,0 +1,251 @@
|
||||
#pragma once
|
||||
|
||||
#include "media/types.hpp"
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <functional>
|
||||
|
||||
namespace media {
|
||||
|
||||
/**
|
||||
* DBAL Client Configuration
|
||||
*/
|
||||
struct DbalClientConfig {
|
||||
std::string url = "http://localhost:8080";
|
||||
std::string api_key;
|
||||
int timeout_ms = 5000;
|
||||
int retry_attempts = 3;
|
||||
int retry_delay_ms = 1000;
|
||||
};
|
||||
|
||||
/**
|
||||
* DBAL Client
|
||||
*
|
||||
* Handles communication with the DBAL daemon for:
|
||||
* - Sending user notifications
|
||||
* - Checking user permissions
|
||||
* - Storing job metadata
|
||||
* - Multi-tenant isolation
|
||||
*/
|
||||
class DbalClient {
|
||||
public:
|
||||
DbalClient();
|
||||
~DbalClient();
|
||||
|
||||
// Disable copying
|
||||
DbalClient(const DbalClient&) = delete;
|
||||
DbalClient& operator=(const DbalClient&) = delete;
|
||||
|
||||
// ========================================================================
|
||||
// Initialization
|
||||
// ========================================================================
|
||||
|
||||
/**
|
||||
* Initialize the DBAL client
|
||||
* @param config Client configuration
|
||||
* @return Result indicating success or failure
|
||||
*/
|
||||
Result<void> initialize(const DbalClientConfig& config);
|
||||
|
||||
/**
|
||||
* Check if connected to DBAL
|
||||
*/
|
||||
bool is_connected() const;
|
||||
|
||||
/**
|
||||
* Test connection to DBAL
|
||||
* @return Result indicating success or failure
|
||||
*/
|
||||
Result<void> ping();
|
||||
|
||||
// ========================================================================
|
||||
// Notifications
|
||||
// ========================================================================
|
||||
|
||||
/**
|
||||
* Send a notification to a user
|
||||
* @param notification Notification details
|
||||
* @return Result indicating success or failure
|
||||
*/
|
||||
Result<void> send_notification(const Notification& notification);
|
||||
|
||||
/**
|
||||
* Send job started notification
|
||||
*/
|
||||
Result<void> notify_job_started(
|
||||
const std::string& tenant_id,
|
||||
const std::string& user_id,
|
||||
const std::string& job_id,
|
||||
JobType job_type
|
||||
);
|
||||
|
||||
/**
|
||||
* Send job progress notification
|
||||
*/
|
||||
Result<void> notify_job_progress(
|
||||
const std::string& tenant_id,
|
||||
const std::string& user_id,
|
||||
const std::string& job_id,
|
||||
const JobProgress& progress
|
||||
);
|
||||
|
||||
/**
|
||||
* Send job completed notification
|
||||
*/
|
||||
Result<void> notify_job_completed(
|
||||
const std::string& tenant_id,
|
||||
const std::string& user_id,
|
||||
const std::string& job_id,
|
||||
const std::string& output_path
|
||||
);
|
||||
|
||||
/**
|
||||
* Send job failed notification
|
||||
*/
|
||||
Result<void> notify_job_failed(
|
||||
const std::string& tenant_id,
|
||||
const std::string& user_id,
|
||||
const std::string& job_id,
|
||||
const std::string& error_message
|
||||
);
|
||||
|
||||
/**
|
||||
* Send stream started notification
|
||||
*/
|
||||
Result<void> notify_stream_started(
|
||||
const std::string& tenant_id,
|
||||
const std::string& channel_id,
|
||||
const std::string& channel_name,
|
||||
const std::string& stream_url
|
||||
);
|
||||
|
||||
/**
|
||||
* Send stream stopped notification
|
||||
*/
|
||||
Result<void> notify_stream_stopped(
|
||||
const std::string& tenant_id,
|
||||
const std::string& channel_id,
|
||||
const std::string& channel_name
|
||||
);
|
||||
|
||||
// ========================================================================
|
||||
// Permissions
|
||||
// ========================================================================
|
||||
|
||||
/**
|
||||
* Check if user has permission for an action
|
||||
* @param tenant_id Tenant ID
|
||||
* @param user_id User ID
|
||||
* @param permission Permission to check
|
||||
* @return Result with boolean or error
|
||||
*/
|
||||
Result<bool> check_permission(
|
||||
const std::string& tenant_id,
|
||||
const std::string& user_id,
|
||||
const std::string& permission
|
||||
);
|
||||
|
||||
/**
|
||||
* Get user's permission level
|
||||
* @param tenant_id Tenant ID
|
||||
* @param user_id User ID
|
||||
* @return Result with level (0-6) or error
|
||||
*/
|
||||
Result<int> get_user_level(
|
||||
const std::string& tenant_id,
|
||||
const std::string& user_id
|
||||
);
|
||||
|
||||
// ========================================================================
|
||||
// Job Storage
|
||||
// ========================================================================
|
||||
|
||||
/**
|
||||
* Store job record in DBAL
|
||||
* @param job Job info to store
|
||||
* @return Result indicating success or failure
|
||||
*/
|
||||
Result<void> store_job(const JobInfo& job);
|
||||
|
||||
/**
|
||||
* Update job record in DBAL
|
||||
* @param job Updated job info
|
||||
* @return Result indicating success or failure
|
||||
*/
|
||||
Result<void> update_job(const JobInfo& job);
|
||||
|
||||
/**
|
||||
* Get job record from DBAL
|
||||
* @param job_id Job ID
|
||||
* @return Result with job info or error
|
||||
*/
|
||||
Result<JobInfo> get_job(const std::string& job_id);
|
||||
|
||||
/**
|
||||
* List jobs from DBAL
|
||||
* @param tenant_id Tenant ID filter
|
||||
* @param user_id User ID filter (optional)
|
||||
* @param limit Max results
|
||||
* @param offset Pagination offset
|
||||
* @return Result with job list or error
|
||||
*/
|
||||
Result<std::vector<JobInfo>> list_jobs(
|
||||
const std::string& tenant_id,
|
||||
const std::string& user_id = "",
|
||||
size_t limit = 100,
|
||||
size_t offset = 0
|
||||
);
|
||||
|
||||
// ========================================================================
|
||||
// Channel Storage
|
||||
// ========================================================================
|
||||
|
||||
/**
|
||||
* Store radio channel config
|
||||
*/
|
||||
Result<void> store_radio_channel(const RadioChannelConfig& config);
|
||||
|
||||
/**
|
||||
* Store TV channel config
|
||||
*/
|
||||
Result<void> store_tv_channel(const TvChannelConfig& config);
|
||||
|
||||
/**
|
||||
* Get radio channels for tenant
|
||||
*/
|
||||
Result<std::vector<RadioChannelConfig>> get_radio_channels(
|
||||
const std::string& tenant_id
|
||||
);
|
||||
|
||||
/**
|
||||
* Get TV channels for tenant
|
||||
*/
|
||||
Result<std::vector<TvChannelConfig>> get_tv_channels(
|
||||
const std::string& tenant_id
|
||||
);
|
||||
|
||||
private:
|
||||
/**
|
||||
* Make HTTP request to DBAL
|
||||
*/
|
||||
Result<std::string> make_request(
|
||||
const std::string& method,
|
||||
const std::string& endpoint,
|
||||
const std::string& body = ""
|
||||
);
|
||||
|
||||
/**
|
||||
* Retry wrapper
|
||||
*/
|
||||
template<typename Func>
|
||||
auto with_retry(Func&& func) -> decltype(func());
|
||||
|
||||
DbalClientConfig config_;
|
||||
bool initialized_ = false;
|
||||
|
||||
// HTTP client handle (cpr or similar)
|
||||
struct Impl;
|
||||
std::unique_ptr<Impl> impl_;
|
||||
};
|
||||
|
||||
} // namespace media
|
||||
259
services/media_daemon/include/media/job_queue.hpp
Normal file
259
services/media_daemon/include/media/job_queue.hpp
Normal file
@@ -0,0 +1,259 @@
|
||||
#pragma once
|
||||
|
||||
#include "media/types.hpp"
|
||||
#include "media/plugin_manager.hpp"
|
||||
#include <memory>
|
||||
#include <queue>
|
||||
#include <vector>
|
||||
#include <map>
|
||||
#include <mutex>
|
||||
#include <condition_variable>
|
||||
#include <thread>
|
||||
#include <atomic>
|
||||
#include <functional>
|
||||
|
||||
namespace media {
|
||||
|
||||
/**
|
||||
* Job Queue Configuration
|
||||
*/
|
||||
struct JobQueueConfig {
|
||||
// Worker counts per job type
|
||||
int video_workers = 2;
|
||||
int audio_workers = 4;
|
||||
int document_workers = 4;
|
||||
int image_workers = 8;
|
||||
|
||||
// Directories
|
||||
std::string temp_dir = "/data/temp";
|
||||
std::string output_dir = "/data/output";
|
||||
|
||||
// Retention (hours)
|
||||
int completed_retention_hours = 24;
|
||||
int failed_retention_hours = 168;
|
||||
|
||||
// Notification callback
|
||||
NotificationCallback notification_callback;
|
||||
};
|
||||
|
||||
/**
|
||||
* Job Queue
|
||||
*
|
||||
* Thread-safe priority queue for processing media jobs.
|
||||
* Supports multiple worker threads per job type.
|
||||
*/
|
||||
class JobQueue {
|
||||
public:
|
||||
JobQueue();
|
||||
~JobQueue();
|
||||
|
||||
// Disable copying
|
||||
JobQueue(const JobQueue&) = delete;
|
||||
JobQueue& operator=(const JobQueue&) = delete;
|
||||
|
||||
// ========================================================================
|
||||
// Initialization
|
||||
// ========================================================================
|
||||
|
||||
/**
|
||||
* Initialize the job queue
|
||||
* @param config Queue configuration
|
||||
* @param plugin_manager Plugin manager for job processing
|
||||
* @return Result indicating success or failure
|
||||
*/
|
||||
Result<void> initialize(
|
||||
const JobQueueConfig& config,
|
||||
PluginManager* plugin_manager
|
||||
);
|
||||
|
||||
/**
|
||||
* Start processing jobs
|
||||
*/
|
||||
void start();
|
||||
|
||||
/**
|
||||
* Stop processing and shutdown
|
||||
* @param wait_for_completion Wait for current jobs to finish
|
||||
*/
|
||||
void stop(bool wait_for_completion = true);
|
||||
|
||||
// ========================================================================
|
||||
// Job Management
|
||||
// ========================================================================
|
||||
|
||||
/**
|
||||
* Submit a new job
|
||||
* @param request Job request
|
||||
* @return Result with job ID or error
|
||||
*/
|
||||
Result<std::string> submit(const JobRequest& request);
|
||||
|
||||
/**
|
||||
* Get job status
|
||||
* @param job_id Job ID
|
||||
* @return Result with job info or error
|
||||
*/
|
||||
Result<JobInfo> get_job(const std::string& job_id) const;
|
||||
|
||||
/**
|
||||
* List jobs with optional filtering
|
||||
* @param tenant_id Filter by tenant (empty for all)
|
||||
* @param user_id Filter by user (empty for all)
|
||||
* @param status Filter by status (optional)
|
||||
* @param limit Maximum results
|
||||
* @param offset Pagination offset
|
||||
* @return Vector of job info
|
||||
*/
|
||||
std::vector<JobInfo> list_jobs(
|
||||
const std::string& tenant_id = "",
|
||||
const std::string& user_id = "",
|
||||
std::optional<JobStatus> status = std::nullopt,
|
||||
size_t limit = 100,
|
||||
size_t offset = 0
|
||||
) const;
|
||||
|
||||
/**
|
||||
* Cancel a job
|
||||
* @param job_id Job ID
|
||||
* @return Result indicating success or failure
|
||||
*/
|
||||
Result<void> cancel(const std::string& job_id);
|
||||
|
||||
/**
|
||||
* Retry a failed job
|
||||
* @param job_id Job ID
|
||||
* @return Result with new job ID or error
|
||||
*/
|
||||
Result<std::string> retry(const std::string& job_id);
|
||||
|
||||
// ========================================================================
|
||||
// Callbacks
|
||||
// ========================================================================
|
||||
|
||||
/**
|
||||
* Set progress callback
|
||||
*/
|
||||
void set_progress_callback(JobProgressCallback callback);
|
||||
|
||||
/**
|
||||
* Set completion callback
|
||||
*/
|
||||
void set_completion_callback(JobCompletionCallback callback);
|
||||
|
||||
// ========================================================================
|
||||
// Statistics
|
||||
// ========================================================================
|
||||
|
||||
/**
|
||||
* Get queue statistics
|
||||
*/
|
||||
struct QueueStats {
|
||||
size_t pending_jobs = 0;
|
||||
size_t processing_jobs = 0;
|
||||
size_t completed_jobs = 0;
|
||||
size_t failed_jobs = 0;
|
||||
size_t cancelled_jobs = 0;
|
||||
|
||||
// Per-type counts
|
||||
std::map<JobType, size_t> pending_by_type;
|
||||
std::map<JobType, size_t> processing_by_type;
|
||||
|
||||
// Worker status
|
||||
int total_workers = 0;
|
||||
int busy_workers = 0;
|
||||
int idle_workers = 0;
|
||||
};
|
||||
|
||||
QueueStats get_stats() const;
|
||||
|
||||
/**
|
||||
* Check if queue is running
|
||||
*/
|
||||
bool is_running() const { return running_.load(); }
|
||||
|
||||
private:
|
||||
/**
|
||||
* Generate unique job ID
|
||||
*/
|
||||
std::string generate_job_id();
|
||||
|
||||
/**
|
||||
* Worker thread function
|
||||
*/
|
||||
void worker_thread(JobType type, int worker_id);
|
||||
|
||||
/**
|
||||
* Process a single job
|
||||
*/
|
||||
void process_job(JobInfo& job);
|
||||
|
||||
/**
|
||||
* Update job status
|
||||
*/
|
||||
void update_job_status(const std::string& job_id, JobStatus status, const std::string& error = "");
|
||||
|
||||
/**
|
||||
* Send notification via DBAL
|
||||
*/
|
||||
void send_notification(const JobInfo& job, NotificationType type);
|
||||
|
||||
/**
|
||||
* Cleanup old jobs
|
||||
*/
|
||||
void cleanup_old_jobs();
|
||||
|
||||
/**
|
||||
* Cleanup thread function
|
||||
*/
|
||||
void cleanup_thread();
|
||||
|
||||
// Configuration
|
||||
JobQueueConfig config_;
|
||||
PluginManager* plugin_manager_ = nullptr;
|
||||
|
||||
// State
|
||||
std::atomic<bool> initialized_{false};
|
||||
std::atomic<bool> running_{false};
|
||||
|
||||
// Job storage
|
||||
mutable std::mutex jobs_mutex_;
|
||||
std::map<std::string, JobInfo> jobs_;
|
||||
std::map<std::string, JobRequest> job_requests_; // Original requests
|
||||
|
||||
// Priority queues per job type
|
||||
struct JobQueueEntry {
|
||||
std::string job_id;
|
||||
JobPriority priority;
|
||||
std::chrono::system_clock::time_point submitted_at;
|
||||
|
||||
bool operator<(const JobQueueEntry& other) const {
|
||||
// Lower priority value = higher priority
|
||||
if (static_cast<int>(priority) != static_cast<int>(other.priority)) {
|
||||
return static_cast<int>(priority) > static_cast<int>(other.priority);
|
||||
}
|
||||
// Earlier submission time = higher priority
|
||||
return submitted_at > other.submitted_at;
|
||||
}
|
||||
};
|
||||
|
||||
mutable std::mutex queues_mutex_;
|
||||
std::map<JobType, std::priority_queue<JobQueueEntry>> queues_;
|
||||
std::map<JobType, std::condition_variable> queue_cvs_;
|
||||
|
||||
// Worker threads
|
||||
std::vector<std::thread> workers_;
|
||||
std::map<JobType, std::vector<std::thread>> typed_workers_;
|
||||
|
||||
// Cleanup thread
|
||||
std::thread cleanup_thread_;
|
||||
std::condition_variable cleanup_cv_;
|
||||
|
||||
// Callbacks
|
||||
JobProgressCallback progress_callback_;
|
||||
JobCompletionCallback completion_callback_;
|
||||
|
||||
// Job ID counter
|
||||
std::atomic<uint64_t> job_counter_{0};
|
||||
};
|
||||
|
||||
} // namespace media
|
||||
191
services/media_daemon/include/media/plugin.hpp
Normal file
191
services/media_daemon/include/media/plugin.hpp
Normal file
@@ -0,0 +1,191 @@
|
||||
#pragma once
|
||||
|
||||
#include "media/types.hpp"
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
namespace media {
|
||||
|
||||
/**
|
||||
* Plugin Interface
|
||||
*
|
||||
* Base class for all media processing plugins. Plugins are loaded dynamically
|
||||
* and can handle various media processing tasks.
|
||||
*
|
||||
* To create a custom plugin:
|
||||
* 1. Inherit from Plugin
|
||||
* 2. Implement all pure virtual methods
|
||||
* 3. Export the create_plugin() and destroy_plugin() functions
|
||||
*/
|
||||
class Plugin {
|
||||
public:
|
||||
virtual ~Plugin() = default;
|
||||
|
||||
// ========================================================================
|
||||
// Plugin Metadata
|
||||
// ========================================================================
|
||||
|
||||
/**
|
||||
* Get plugin information
|
||||
*/
|
||||
virtual PluginInfo info() const = 0;
|
||||
|
||||
/**
|
||||
* Get plugin capabilities
|
||||
*/
|
||||
virtual PluginCapabilities capabilities() const = 0;
|
||||
|
||||
// ========================================================================
|
||||
// Lifecycle
|
||||
// ========================================================================
|
||||
|
||||
/**
|
||||
* Initialize the plugin
|
||||
* Called once when the plugin is loaded
|
||||
* @param config_path Path to plugin-specific configuration
|
||||
* @return Result indicating success or failure
|
||||
*/
|
||||
virtual Result<void> initialize(const std::string& config_path) = 0;
|
||||
|
||||
/**
|
||||
* Shutdown the plugin
|
||||
* Called before the plugin is unloaded
|
||||
*/
|
||||
virtual void shutdown() = 0;
|
||||
|
||||
/**
|
||||
* Check if the plugin is healthy
|
||||
* @return true if plugin is operational
|
||||
*/
|
||||
virtual bool is_healthy() const = 0;
|
||||
|
||||
// ========================================================================
|
||||
// Processing
|
||||
// ========================================================================
|
||||
|
||||
/**
|
||||
* Check if this plugin can handle a specific job
|
||||
* @param type Job type
|
||||
* @param params Job parameters
|
||||
* @return true if plugin can process this job
|
||||
*/
|
||||
virtual bool can_handle(JobType type, const JobParams& params) const = 0;
|
||||
|
||||
/**
|
||||
* Process a job
|
||||
* @param request Job request with all parameters
|
||||
* @param progress_callback Callback for progress updates
|
||||
* @return Result with output path or error
|
||||
*/
|
||||
virtual Result<std::string> process(
|
||||
const JobRequest& request,
|
||||
JobProgressCallback progress_callback
|
||||
) = 0;
|
||||
|
||||
/**
|
||||
* Cancel an ongoing job
|
||||
* @param job_id ID of job to cancel
|
||||
* @return Result indicating success or failure
|
||||
*/
|
||||
virtual Result<void> cancel(const std::string& job_id) = 0;
|
||||
|
||||
// ========================================================================
|
||||
// Streaming (optional - for streaming-capable plugins)
|
||||
// ========================================================================
|
||||
|
||||
/**
|
||||
* Start a stream
|
||||
* @param channel_id Channel ID
|
||||
* @param source Source configuration
|
||||
* @param output Output configuration
|
||||
* @return Result with stream URL or error
|
||||
*/
|
||||
virtual Result<std::string> start_stream(
|
||||
const std::string& channel_id,
|
||||
const std::map<std::string, std::string>& source,
|
||||
const std::map<std::string, std::string>& output
|
||||
) {
|
||||
return Result<std::string>::error(
|
||||
ErrorCode::NOT_FOUND,
|
||||
"Streaming not supported by this plugin"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop a stream
|
||||
* @param channel_id Channel ID
|
||||
* @return Result indicating success or failure
|
||||
*/
|
||||
virtual Result<void> stop_stream(const std::string& channel_id) {
|
||||
return Result<void>::error(
|
||||
ErrorCode::NOT_FOUND,
|
||||
"Streaming not supported by this plugin"
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Plugin factory function type
|
||||
* Every plugin shared library must export these functions:
|
||||
*
|
||||
* extern "C" {
|
||||
* Plugin* create_plugin();
|
||||
* void destroy_plugin(Plugin* plugin);
|
||||
* const char* plugin_api_version();
|
||||
* }
|
||||
*/
|
||||
using CreatePluginFunc = Plugin* (*)();
|
||||
using DestroyPluginFunc = void (*)(Plugin*);
|
||||
using PluginApiVersionFunc = const char* (*)();
|
||||
|
||||
// Current plugin API version
|
||||
constexpr const char* PLUGIN_API_VERSION = "1.0.0";
|
||||
|
||||
/**
|
||||
* Plugin handle for managing loaded plugins
|
||||
*/
|
||||
struct PluginHandle {
|
||||
std::string path;
|
||||
void* library_handle = nullptr;
|
||||
Plugin* instance = nullptr;
|
||||
CreatePluginFunc create_func = nullptr;
|
||||
DestroyPluginFunc destroy_func = nullptr;
|
||||
PluginApiVersionFunc version_func = nullptr;
|
||||
bool is_loaded = false;
|
||||
};
|
||||
|
||||
} // namespace media
|
||||
|
||||
// ============================================================================
|
||||
// Plugin Export Macros
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Use this macro in your plugin implementation to export required functions
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* class MyPlugin : public media::Plugin {
|
||||
* // ... implementation
|
||||
* };
|
||||
*
|
||||
* MEDIA_PLUGIN_EXPORT(MyPlugin)
|
||||
*/
|
||||
#ifdef _WIN32
|
||||
#define MEDIA_PLUGIN_API __declspec(dllexport)
|
||||
#else
|
||||
#define MEDIA_PLUGIN_API __attribute__((visibility("default")))
|
||||
#endif
|
||||
|
||||
#define MEDIA_PLUGIN_EXPORT(PluginClass) \
|
||||
extern "C" { \
|
||||
MEDIA_PLUGIN_API media::Plugin* create_plugin() { \
|
||||
return new PluginClass(); \
|
||||
} \
|
||||
MEDIA_PLUGIN_API void destroy_plugin(media::Plugin* plugin) { \
|
||||
delete plugin; \
|
||||
} \
|
||||
MEDIA_PLUGIN_API const char* plugin_api_version() { \
|
||||
return media::PLUGIN_API_VERSION; \
|
||||
} \
|
||||
}
|
||||
174
services/media_daemon/include/media/plugin_manager.hpp
Normal file
174
services/media_daemon/include/media/plugin_manager.hpp
Normal file
@@ -0,0 +1,174 @@
|
||||
#pragma once
|
||||
|
||||
#include "media/types.hpp"
|
||||
#include "media/plugin.hpp"
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
#include <map>
|
||||
#include <mutex>
|
||||
#include <filesystem>
|
||||
|
||||
namespace media {
|
||||
|
||||
/**
|
||||
* Plugin Manager
|
||||
*
|
||||
* Handles loading, unloading, and managing media processing plugins.
|
||||
* Supports both built-in and dynamically loaded plugins.
|
||||
*/
|
||||
class PluginManager {
|
||||
public:
|
||||
PluginManager();
|
||||
~PluginManager();
|
||||
|
||||
// Disable copying
|
||||
PluginManager(const PluginManager&) = delete;
|
||||
PluginManager& operator=(const PluginManager&) = delete;
|
||||
|
||||
// ========================================================================
|
||||
// Initialization
|
||||
// ========================================================================
|
||||
|
||||
/**
|
||||
* Initialize the plugin manager
|
||||
* @param plugin_dir Directory to scan for plugins
|
||||
* @param config_path Path to plugin configuration
|
||||
* @return Result indicating success or failure
|
||||
*/
|
||||
Result<void> initialize(
|
||||
const std::string& plugin_dir,
|
||||
const std::string& config_path
|
||||
);
|
||||
|
||||
/**
|
||||
* Shutdown all plugins and cleanup
|
||||
*/
|
||||
void shutdown();
|
||||
|
||||
// ========================================================================
|
||||
// Plugin Management
|
||||
// ========================================================================
|
||||
|
||||
/**
|
||||
* Load a specific plugin
|
||||
* @param path Path to plugin shared library
|
||||
* @return Result with plugin info or error
|
||||
*/
|
||||
Result<PluginInfo> load_plugin(const std::string& path);
|
||||
|
||||
/**
|
||||
* Unload a specific plugin
|
||||
* @param plugin_id Plugin ID to unload
|
||||
* @return Result indicating success or failure
|
||||
*/
|
||||
Result<void> unload_plugin(const std::string& plugin_id);
|
||||
|
||||
/**
|
||||
* Reload a plugin (hot-reload for development)
|
||||
* @param plugin_id Plugin ID to reload
|
||||
* @return Result with updated plugin info or error
|
||||
*/
|
||||
Result<PluginInfo> reload_plugin(const std::string& plugin_id);
|
||||
|
||||
/**
|
||||
* Get list of all loaded plugins
|
||||
* @return Vector of plugin information
|
||||
*/
|
||||
std::vector<PluginInfo> list_plugins() const;
|
||||
|
||||
/**
|
||||
* Get a specific plugin by ID
|
||||
* @param plugin_id Plugin ID
|
||||
* @return Pointer to plugin or nullptr if not found
|
||||
*/
|
||||
Plugin* get_plugin(const std::string& plugin_id);
|
||||
|
||||
/**
|
||||
* Get a plugin by ID (const version)
|
||||
*/
|
||||
const Plugin* get_plugin(const std::string& plugin_id) const;
|
||||
|
||||
// ========================================================================
|
||||
// Job Routing
|
||||
// ========================================================================
|
||||
|
||||
/**
|
||||
* Find the best plugin to handle a job
|
||||
* @param type Job type
|
||||
* @param params Job parameters
|
||||
* @return Pointer to plugin or nullptr if none can handle
|
||||
*/
|
||||
Plugin* find_plugin_for_job(JobType type, const JobParams& params);
|
||||
|
||||
/**
|
||||
* Get all plugins that can handle a job type
|
||||
* @param type Job type
|
||||
* @return Vector of plugin pointers
|
||||
*/
|
||||
std::vector<Plugin*> get_plugins_for_type(JobType type);
|
||||
|
||||
// ========================================================================
|
||||
// Built-in Plugins
|
||||
// ========================================================================
|
||||
|
||||
/**
|
||||
* Register a built-in plugin
|
||||
* @param plugin Plugin instance (ownership transferred)
|
||||
* @return Result indicating success or failure
|
||||
*/
|
||||
Result<void> register_builtin(std::unique_ptr<Plugin> plugin);
|
||||
|
||||
// ========================================================================
|
||||
// Status
|
||||
// ========================================================================
|
||||
|
||||
/**
|
||||
* Check if plugin manager is initialized
|
||||
*/
|
||||
bool is_initialized() const { return initialized_; }
|
||||
|
||||
/**
|
||||
* Get plugin count
|
||||
*/
|
||||
size_t plugin_count() const;
|
||||
|
||||
/**
|
||||
* Run health checks on all plugins
|
||||
* @return Map of plugin ID to health status
|
||||
*/
|
||||
std::map<std::string, bool> health_check() const;
|
||||
|
||||
private:
|
||||
/**
|
||||
* Scan directory for plugin files
|
||||
*/
|
||||
std::vector<std::filesystem::path> scan_plugin_directory(
|
||||
const std::string& dir
|
||||
);
|
||||
|
||||
/**
|
||||
* Load plugin from shared library
|
||||
*/
|
||||
Result<PluginHandle> load_shared_library(const std::string& path);
|
||||
|
||||
/**
|
||||
* Unload a plugin handle
|
||||
*/
|
||||
void unload_handle(PluginHandle& handle);
|
||||
|
||||
mutable std::mutex mutex_;
|
||||
bool initialized_ = false;
|
||||
std::string plugin_dir_;
|
||||
std::string config_path_;
|
||||
|
||||
// Loaded plugins (by ID)
|
||||
std::map<std::string, PluginHandle> plugins_;
|
||||
|
||||
// Built-in plugins (owned)
|
||||
std::vector<std::unique_ptr<Plugin>> builtin_plugins_;
|
||||
|
||||
// Plugin ID to handle mapping for built-ins
|
||||
std::map<std::string, Plugin*> builtin_map_;
|
||||
};
|
||||
|
||||
} // namespace media
|
||||
174
services/media_daemon/include/media/plugins/ffmpeg_plugin.hpp
Normal file
174
services/media_daemon/include/media/plugins/ffmpeg_plugin.hpp
Normal file
@@ -0,0 +1,174 @@
|
||||
#pragma once
|
||||
|
||||
#include "media/plugin.hpp"
|
||||
#include <map>
|
||||
#include <mutex>
|
||||
#include <atomic>
|
||||
|
||||
namespace media {
|
||||
namespace plugins {
|
||||
|
||||
/**
|
||||
* FFmpeg Plugin Configuration
|
||||
*/
|
||||
struct FFmpegConfig {
|
||||
std::string ffmpeg_path = "/usr/bin/ffmpeg";
|
||||
std::string ffprobe_path = "/usr/bin/ffprobe";
|
||||
|
||||
// Hardware acceleration
|
||||
enum class HwAccel {
|
||||
NONE,
|
||||
AUTO,
|
||||
NVIDIA, // NVENC
|
||||
VAAPI, // Intel/AMD
|
||||
QSV, // Intel Quick Sync
|
||||
VIDEOTOOLBOX // macOS
|
||||
};
|
||||
HwAccel hardware_accel = HwAccel::AUTO;
|
||||
|
||||
// Threading
|
||||
int threads = 0; // 0 = auto
|
||||
|
||||
// Limits
|
||||
int max_concurrent_jobs = 4;
|
||||
size_t max_output_size_gb = 50;
|
||||
};
|
||||
|
||||
/**
|
||||
* FFmpeg Plugin
|
||||
*
|
||||
* Built-in plugin for video and audio transcoding using FFmpeg.
|
||||
* Supports hardware acceleration, multiple codecs, and streaming output.
|
||||
*/
|
||||
class FFmpegPlugin : public Plugin {
|
||||
public:
|
||||
FFmpegPlugin();
|
||||
~FFmpegPlugin() override;
|
||||
|
||||
// ========================================================================
|
||||
// Plugin Interface
|
||||
// ========================================================================
|
||||
|
||||
PluginInfo info() const override;
|
||||
PluginCapabilities capabilities() const override;
|
||||
|
||||
Result<void> initialize(const std::string& config_path) override;
|
||||
void shutdown() override;
|
||||
bool is_healthy() const override;
|
||||
|
||||
bool can_handle(JobType type, const JobParams& params) const override;
|
||||
|
||||
Result<std::string> process(
|
||||
const JobRequest& request,
|
||||
JobProgressCallback progress_callback
|
||||
) override;
|
||||
|
||||
Result<void> cancel(const std::string& job_id) override;
|
||||
|
||||
// Streaming support
|
||||
Result<std::string> start_stream(
|
||||
const std::string& channel_id,
|
||||
const std::map<std::string, std::string>& source,
|
||||
const std::map<std::string, std::string>& output
|
||||
) override;
|
||||
|
||||
Result<void> stop_stream(const std::string& channel_id) override;
|
||||
|
||||
// ========================================================================
|
||||
// FFmpeg-specific Methods
|
||||
// ========================================================================
|
||||
|
||||
/**
|
||||
* Get media info using ffprobe
|
||||
* @param path Path to media file
|
||||
* @return Result with media info JSON or error
|
||||
*/
|
||||
Result<std::string> probe(const std::string& path);
|
||||
|
||||
/**
|
||||
* Get video duration in seconds
|
||||
*/
|
||||
Result<double> get_duration(const std::string& path);
|
||||
|
||||
/**
|
||||
* Get available hardware encoders
|
||||
*/
|
||||
std::vector<std::string> get_available_encoders();
|
||||
|
||||
/**
|
||||
* Check if codec is available
|
||||
*/
|
||||
bool is_codec_available(const std::string& codec);
|
||||
|
||||
private:
|
||||
/**
|
||||
* Build FFmpeg command for video transcoding
|
||||
*/
|
||||
std::vector<std::string> build_video_command(
|
||||
const VideoTranscodeParams& params
|
||||
);
|
||||
|
||||
/**
|
||||
* Build FFmpeg command for audio transcoding
|
||||
*/
|
||||
std::vector<std::string> build_audio_command(
|
||||
const AudioTranscodeParams& params
|
||||
);
|
||||
|
||||
/**
|
||||
* Build FFmpeg command for HLS streaming
|
||||
*/
|
||||
std::vector<std::string> build_hls_command(
|
||||
const std::string& input,
|
||||
const std::string& output_dir,
|
||||
const std::map<std::string, std::string>& options
|
||||
);
|
||||
|
||||
/**
|
||||
* Execute FFmpeg command with progress tracking
|
||||
*/
|
||||
Result<void> execute_ffmpeg(
|
||||
const std::vector<std::string>& args,
|
||||
const std::string& job_id,
|
||||
double total_duration,
|
||||
JobProgressCallback progress_callback
|
||||
);
|
||||
|
||||
/**
|
||||
* Parse FFmpeg progress output
|
||||
*/
|
||||
JobProgress parse_progress(
|
||||
const std::string& output,
|
||||
double total_duration
|
||||
);
|
||||
|
||||
/**
|
||||
* Detect available hardware acceleration
|
||||
*/
|
||||
FFmpegConfig::HwAccel detect_hardware_accel();
|
||||
|
||||
/**
|
||||
* Get hardware encoder name for codec
|
||||
*/
|
||||
std::string get_hw_encoder(const std::string& codec);
|
||||
|
||||
// Configuration
|
||||
FFmpegConfig config_;
|
||||
bool initialized_ = false;
|
||||
|
||||
// Active jobs/streams
|
||||
struct ProcessInfo {
|
||||
int pid = 0;
|
||||
std::atomic<bool> cancelled{false};
|
||||
};
|
||||
|
||||
mutable std::mutex processes_mutex_;
|
||||
std::map<std::string, ProcessInfo> active_processes_;
|
||||
|
||||
// Cached probe data
|
||||
mutable std::mutex cache_mutex_;
|
||||
std::map<std::string, std::string> probe_cache_;
|
||||
};
|
||||
|
||||
} // namespace plugins
|
||||
} // namespace media
|
||||
314
services/media_daemon/include/media/radio_engine.hpp
Normal file
314
services/media_daemon/include/media/radio_engine.hpp
Normal file
@@ -0,0 +1,314 @@
|
||||
#pragma once
|
||||
|
||||
#include "media/types.hpp"
|
||||
#include "media/plugin_manager.hpp"
|
||||
#include <memory>
|
||||
#include <map>
|
||||
#include <mutex>
|
||||
#include <thread>
|
||||
#include <atomic>
|
||||
#include <condition_variable>
|
||||
|
||||
namespace media {
|
||||
|
||||
/**
|
||||
* Radio Engine Configuration
|
||||
*/
|
||||
struct RadioEngineConfig {
|
||||
// General
|
||||
int max_channels = 10;
|
||||
|
||||
// Default audio settings
|
||||
int default_bitrate_kbps = 128;
|
||||
int default_sample_rate = 44100;
|
||||
int default_channels = 2;
|
||||
std::string default_codec = "mp3";
|
||||
|
||||
// Crossfade
|
||||
bool crossfade_enabled = true;
|
||||
int crossfade_duration_ms = 3000;
|
||||
|
||||
// Normalization
|
||||
bool normalization_enabled = true;
|
||||
double target_lufs = -14.0;
|
||||
|
||||
// Output
|
||||
std::string hls_output_dir = "/data/hls/radio";
|
||||
int hls_segment_duration = 6;
|
||||
|
||||
// Notification callback
|
||||
NotificationCallback notification_callback;
|
||||
};
|
||||
|
||||
/**
|
||||
* Internal Radio Channel State
|
||||
*/
|
||||
struct RadioChannelState {
|
||||
RadioChannelConfig config;
|
||||
RadioChannelStatus status;
|
||||
|
||||
// Playlist
|
||||
std::vector<RadioPlaylistEntry> playlist;
|
||||
size_t current_index = 0;
|
||||
|
||||
// Streaming state
|
||||
std::atomic<bool> is_running{false};
|
||||
std::thread stream_thread;
|
||||
std::condition_variable cv;
|
||||
std::mutex mutex;
|
||||
|
||||
// FFmpeg process handle (or similar)
|
||||
void* process_handle = nullptr;
|
||||
|
||||
// Statistics
|
||||
std::chrono::system_clock::time_point started_at;
|
||||
std::atomic<int> listener_count{0};
|
||||
};
|
||||
|
||||
/**
|
||||
* Radio Engine
|
||||
*
|
||||
* Manages radio channel streaming with playlist scheduling,
|
||||
* crossfading, audio normalization, and multiple output formats.
|
||||
*/
|
||||
class RadioEngine {
|
||||
public:
|
||||
RadioEngine();
|
||||
~RadioEngine();
|
||||
|
||||
// Disable copying
|
||||
RadioEngine(const RadioEngine&) = delete;
|
||||
RadioEngine& operator=(const RadioEngine&) = delete;
|
||||
|
||||
// ========================================================================
|
||||
// Initialization
|
||||
// ========================================================================
|
||||
|
||||
/**
|
||||
* Initialize the radio engine
|
||||
* @param config Engine configuration
|
||||
* @param plugin_manager Plugin manager for audio processing
|
||||
* @return Result indicating success or failure
|
||||
*/
|
||||
Result<void> initialize(
|
||||
const RadioEngineConfig& config,
|
||||
PluginManager* plugin_manager
|
||||
);
|
||||
|
||||
/**
|
||||
* Shutdown all channels and cleanup
|
||||
*/
|
||||
void shutdown();
|
||||
|
||||
// ========================================================================
|
||||
// Channel Management
|
||||
// ========================================================================
|
||||
|
||||
/**
|
||||
* Create a new radio channel
|
||||
* @param config Channel configuration
|
||||
* @return Result with channel ID or error
|
||||
*/
|
||||
Result<std::string> create_channel(const RadioChannelConfig& config);
|
||||
|
||||
/**
|
||||
* Delete a radio channel
|
||||
* @param channel_id Channel ID
|
||||
* @return Result indicating success or failure
|
||||
*/
|
||||
Result<void> delete_channel(const std::string& channel_id);
|
||||
|
||||
/**
|
||||
* Update channel configuration
|
||||
* @param channel_id Channel ID
|
||||
* @param config New configuration
|
||||
* @return Result indicating success or failure
|
||||
*/
|
||||
Result<void> update_channel(
|
||||
const std::string& channel_id,
|
||||
const RadioChannelConfig& config
|
||||
);
|
||||
|
||||
/**
|
||||
* Get channel status
|
||||
* @param channel_id Channel ID
|
||||
* @return Result with channel status or error
|
||||
*/
|
||||
Result<RadioChannelStatus> get_channel_status(
|
||||
const std::string& channel_id
|
||||
) const;
|
||||
|
||||
/**
|
||||
* List all channels
|
||||
* @param tenant_id Filter by tenant (empty for all)
|
||||
* @return Vector of channel statuses
|
||||
*/
|
||||
std::vector<RadioChannelStatus> list_channels(
|
||||
const std::string& tenant_id = ""
|
||||
) const;
|
||||
|
||||
// ========================================================================
|
||||
// Streaming Control
|
||||
// ========================================================================
|
||||
|
||||
/**
|
||||
* Start streaming a channel
|
||||
* @param channel_id Channel ID
|
||||
* @return Result with stream URL or error
|
||||
*/
|
||||
Result<std::string> start_channel(const std::string& channel_id);
|
||||
|
||||
/**
|
||||
* Stop streaming a channel
|
||||
* @param channel_id Channel ID
|
||||
* @return Result indicating success or failure
|
||||
*/
|
||||
Result<void> stop_channel(const std::string& channel_id);
|
||||
|
||||
// ========================================================================
|
||||
// Playlist Management
|
||||
// ========================================================================
|
||||
|
||||
/**
|
||||
* Set channel playlist
|
||||
* @param channel_id Channel ID
|
||||
* @param tracks List of tracks
|
||||
* @return Result indicating success or failure
|
||||
*/
|
||||
Result<void> set_playlist(
|
||||
const std::string& channel_id,
|
||||
const std::vector<RadioTrack>& tracks
|
||||
);
|
||||
|
||||
/**
|
||||
* Add track to playlist
|
||||
* @param channel_id Channel ID
|
||||
* @param track Track to add
|
||||
* @param position Position in playlist (-1 for end)
|
||||
* @return Result indicating success or failure
|
||||
*/
|
||||
Result<void> add_track(
|
||||
const std::string& channel_id,
|
||||
const RadioTrack& track,
|
||||
int position = -1
|
||||
);
|
||||
|
||||
/**
|
||||
* Remove track from playlist
|
||||
* @param channel_id Channel ID
|
||||
* @param track_id Track ID to remove
|
||||
* @return Result indicating success or failure
|
||||
*/
|
||||
Result<void> remove_track(
|
||||
const std::string& channel_id,
|
||||
const std::string& track_id
|
||||
);
|
||||
|
||||
/**
|
||||
* Skip to next track
|
||||
* @param channel_id Channel ID
|
||||
* @return Result indicating success or failure
|
||||
*/
|
||||
Result<void> skip_track(const std::string& channel_id);
|
||||
|
||||
/**
|
||||
* Get current playlist
|
||||
* @param channel_id Channel ID
|
||||
* @return Result with playlist or error
|
||||
*/
|
||||
Result<std::vector<RadioPlaylistEntry>> get_playlist(
|
||||
const std::string& channel_id
|
||||
) const;
|
||||
|
||||
/**
|
||||
* Get now playing info
|
||||
* @param channel_id Channel ID
|
||||
* @return Result with current track or error
|
||||
*/
|
||||
Result<RadioTrack> get_now_playing(
|
||||
const std::string& channel_id
|
||||
) const;
|
||||
|
||||
// ========================================================================
|
||||
// Auto-DJ
|
||||
// ========================================================================
|
||||
|
||||
/**
|
||||
* Enable/disable auto-DJ for a channel
|
||||
* @param channel_id Channel ID
|
||||
* @param enabled Enable auto-DJ
|
||||
* @param folders Folders to scan for music
|
||||
* @param shuffle Shuffle tracks
|
||||
* @return Result indicating success or failure
|
||||
*/
|
||||
Result<void> set_auto_dj(
|
||||
const std::string& channel_id,
|
||||
bool enabled,
|
||||
const std::vector<std::string>& folders = {},
|
||||
bool shuffle = true
|
||||
);
|
||||
|
||||
// ========================================================================
|
||||
// Statistics
|
||||
// ========================================================================
|
||||
|
||||
/**
|
||||
* Update listener count (called by stream server)
|
||||
* @param channel_id Channel ID
|
||||
* @param delta Change in listener count
|
||||
*/
|
||||
void update_listener_count(const std::string& channel_id, int delta);
|
||||
|
||||
/**
|
||||
* Get total listener count across all channels
|
||||
*/
|
||||
int get_total_listeners() const;
|
||||
|
||||
private:
|
||||
/**
|
||||
* Stream thread function
|
||||
*/
|
||||
void stream_thread(const std::string& channel_id);
|
||||
|
||||
/**
|
||||
* Load next track with crossfade
|
||||
*/
|
||||
void load_next_track(RadioChannelState& state);
|
||||
|
||||
/**
|
||||
* Apply audio processing (normalization, effects)
|
||||
*/
|
||||
void process_audio(RadioChannelState& state, void* buffer, size_t size);
|
||||
|
||||
/**
|
||||
* Generate HLS segments
|
||||
*/
|
||||
void generate_hls_segment(
|
||||
const std::string& channel_id,
|
||||
const void* audio_data,
|
||||
size_t size
|
||||
);
|
||||
|
||||
/**
|
||||
* Scan folder for audio files
|
||||
*/
|
||||
std::vector<RadioTrack> scan_folder(const std::string& folder);
|
||||
|
||||
/**
|
||||
* Get audio metadata from file
|
||||
*/
|
||||
RadioTrack get_track_metadata(const std::string& path);
|
||||
|
||||
// Configuration
|
||||
RadioEngineConfig config_;
|
||||
PluginManager* plugin_manager_ = nullptr;
|
||||
|
||||
// State
|
||||
std::atomic<bool> initialized_{false};
|
||||
|
||||
// Channels
|
||||
mutable std::mutex channels_mutex_;
|
||||
std::map<std::string, std::unique_ptr<RadioChannelState>> channels_;
|
||||
};
|
||||
|
||||
} // namespace media
|
||||
193
services/media_daemon/include/media/server.hpp
Normal file
193
services/media_daemon/include/media/server.hpp
Normal file
@@ -0,0 +1,193 @@
|
||||
#pragma once
|
||||
|
||||
#include "media/types.hpp"
|
||||
#include "media/plugin_manager.hpp"
|
||||
#include "media/job_queue.hpp"
|
||||
#include "media/radio_engine.hpp"
|
||||
#include "media/tv_engine.hpp"
|
||||
#include "media/dbal_client.hpp"
|
||||
#include <memory>
|
||||
#include <atomic>
|
||||
#include <string>
|
||||
|
||||
namespace media {
|
||||
|
||||
/**
|
||||
* Media Daemon Server Configuration
|
||||
*/
|
||||
struct ServerConfig {
|
||||
// Network
|
||||
std::string bind_address = "0.0.0.0";
|
||||
int port = 8090;
|
||||
int workers = 4;
|
||||
bool development_mode = false;
|
||||
|
||||
// DBAL
|
||||
DbalClientConfig dbal;
|
||||
|
||||
// Job Queue
|
||||
JobQueueConfig job_queue;
|
||||
|
||||
// Radio
|
||||
bool radio_enabled = true;
|
||||
RadioEngineConfig radio;
|
||||
|
||||
// TV
|
||||
bool tv_enabled = true;
|
||||
TvEngineConfig tv;
|
||||
|
||||
// Plugins
|
||||
bool plugins_enabled = true;
|
||||
std::string plugin_dir = "/plugins";
|
||||
bool hot_reload = false;
|
||||
|
||||
// Security
|
||||
bool api_keys_enabled = true;
|
||||
std::string api_key_header = "X-API-Key";
|
||||
bool rate_limit_enabled = true;
|
||||
int rate_limit_rpm = 100;
|
||||
|
||||
// CORS
|
||||
bool cors_enabled = true;
|
||||
std::vector<std::string> cors_origins = {"*"};
|
||||
|
||||
// Monitoring
|
||||
bool prometheus_enabled = true;
|
||||
std::string prometheus_endpoint = "/metrics";
|
||||
std::string health_endpoint = "/health";
|
||||
};
|
||||
|
||||
/**
|
||||
* Media Daemon Server
|
||||
*
|
||||
* Main HTTP server for the media processing daemon.
|
||||
* Uses Drogon framework (same as DBAL daemon).
|
||||
*/
|
||||
class Server {
|
||||
public:
|
||||
Server();
|
||||
~Server();
|
||||
|
||||
// Disable copying
|
||||
Server(const Server&) = delete;
|
||||
Server& operator=(const Server&) = delete;
|
||||
|
||||
// ========================================================================
|
||||
// Lifecycle
|
||||
// ========================================================================
|
||||
|
||||
/**
|
||||
* Initialize the server
|
||||
* @param config Server configuration
|
||||
* @return Result indicating success or failure
|
||||
*/
|
||||
Result<void> initialize(const ServerConfig& config);
|
||||
|
||||
/**
|
||||
* Start the server (blocking)
|
||||
*/
|
||||
void run();
|
||||
|
||||
/**
|
||||
* Start the server (non-blocking)
|
||||
*/
|
||||
void start();
|
||||
|
||||
/**
|
||||
* Stop the server
|
||||
*/
|
||||
void stop();
|
||||
|
||||
/**
|
||||
* Check if server is running
|
||||
*/
|
||||
bool is_running() const { return running_.load(); }
|
||||
|
||||
// ========================================================================
|
||||
// Component Access
|
||||
// ========================================================================
|
||||
|
||||
PluginManager& plugin_manager() { return *plugin_manager_; }
|
||||
JobQueue& job_queue() { return *job_queue_; }
|
||||
RadioEngine& radio_engine() { return *radio_engine_; }
|
||||
TvEngine& tv_engine() { return *tv_engine_; }
|
||||
DbalClient& dbal_client() { return *dbal_client_; }
|
||||
|
||||
const PluginManager& plugin_manager() const { return *plugin_manager_; }
|
||||
const JobQueue& job_queue() const { return *job_queue_; }
|
||||
const RadioEngine& radio_engine() const { return *radio_engine_; }
|
||||
const TvEngine& tv_engine() const { return *tv_engine_; }
|
||||
const DbalClient& dbal_client() const { return *dbal_client_; }
|
||||
|
||||
private:
|
||||
/**
|
||||
* Setup HTTP routes
|
||||
*/
|
||||
void setup_routes();
|
||||
|
||||
/**
|
||||
* Setup middleware (auth, rate limiting, CORS)
|
||||
*/
|
||||
void setup_middleware();
|
||||
|
||||
/**
|
||||
* Register built-in plugins
|
||||
*/
|
||||
Result<void> register_builtin_plugins();
|
||||
|
||||
// ========================================================================
|
||||
// Route Handlers
|
||||
// ========================================================================
|
||||
|
||||
// Health
|
||||
void handle_health();
|
||||
void handle_metrics();
|
||||
|
||||
// Jobs
|
||||
void handle_create_job();
|
||||
void handle_get_job();
|
||||
void handle_list_jobs();
|
||||
void handle_cancel_job();
|
||||
|
||||
// Radio
|
||||
void handle_create_radio_channel();
|
||||
void handle_get_radio_channel();
|
||||
void handle_list_radio_channels();
|
||||
void handle_start_radio();
|
||||
void handle_stop_radio();
|
||||
void handle_set_playlist();
|
||||
void handle_get_now_playing();
|
||||
|
||||
// TV
|
||||
void handle_create_tv_channel();
|
||||
void handle_get_tv_channel();
|
||||
void handle_list_tv_channels();
|
||||
void handle_start_tv();
|
||||
void handle_stop_tv();
|
||||
void handle_set_schedule();
|
||||
void handle_get_epg();
|
||||
|
||||
// Plugins
|
||||
void handle_list_plugins();
|
||||
void handle_reload_plugin();
|
||||
|
||||
// Configuration
|
||||
ServerConfig config_;
|
||||
|
||||
// State
|
||||
std::atomic<bool> initialized_{false};
|
||||
std::atomic<bool> running_{false};
|
||||
|
||||
// Components
|
||||
std::unique_ptr<PluginManager> plugin_manager_;
|
||||
std::unique_ptr<JobQueue> job_queue_;
|
||||
std::unique_ptr<RadioEngine> radio_engine_;
|
||||
std::unique_ptr<TvEngine> tv_engine_;
|
||||
std::unique_ptr<DbalClient> dbal_client_;
|
||||
|
||||
// Drogon app (pimpl)
|
||||
struct Impl;
|
||||
std::unique_ptr<Impl> impl_;
|
||||
};
|
||||
|
||||
} // namespace media
|
||||
396
services/media_daemon/include/media/tv_engine.hpp
Normal file
396
services/media_daemon/include/media/tv_engine.hpp
Normal file
@@ -0,0 +1,396 @@
|
||||
#pragma once
|
||||
|
||||
#include "media/types.hpp"
|
||||
#include "media/plugin_manager.hpp"
|
||||
#include <memory>
|
||||
#include <map>
|
||||
#include <mutex>
|
||||
#include <thread>
|
||||
#include <atomic>
|
||||
#include <condition_variable>
|
||||
|
||||
namespace media {
|
||||
|
||||
/**
|
||||
* TV Engine Configuration
|
||||
*/
|
||||
struct TvEngineConfig {
|
||||
// General
|
||||
int max_channels = 5;
|
||||
|
||||
// Video settings
|
||||
struct Resolution {
|
||||
std::string name;
|
||||
int width;
|
||||
int height;
|
||||
int bitrate_kbps;
|
||||
};
|
||||
std::vector<Resolution> resolutions = {
|
||||
{"1080p", 1920, 1080, 5000},
|
||||
{"720p", 1280, 720, 2500},
|
||||
{"480p", 854, 480, 1000}
|
||||
};
|
||||
std::string default_video_codec = "h264";
|
||||
std::string video_preset = "fast";
|
||||
|
||||
// Audio settings
|
||||
std::string default_audio_codec = "aac";
|
||||
int audio_bitrate_kbps = 128;
|
||||
int audio_sample_rate = 48000;
|
||||
|
||||
// HLS settings
|
||||
std::string hls_output_dir = "/data/hls/tv";
|
||||
int hls_segment_duration = 4;
|
||||
int hls_playlist_size = 10;
|
||||
|
||||
// EPG settings
|
||||
int epg_lookahead_hours = 24;
|
||||
int epg_refresh_interval_minutes = 15;
|
||||
|
||||
// Notification callback
|
||||
NotificationCallback notification_callback;
|
||||
};
|
||||
|
||||
/**
|
||||
* Internal TV Channel State
|
||||
*/
|
||||
struct TvChannelState {
|
||||
TvChannelConfig config;
|
||||
TvChannelStatus status;
|
||||
|
||||
// Schedule
|
||||
std::vector<TvScheduleEntry> schedule;
|
||||
size_t current_program_index = 0;
|
||||
|
||||
// Streaming state
|
||||
std::atomic<bool> is_running{false};
|
||||
std::thread stream_thread;
|
||||
std::condition_variable cv;
|
||||
std::mutex mutex;
|
||||
|
||||
// Current playback position
|
||||
std::chrono::system_clock::time_point playback_position;
|
||||
|
||||
// FFmpeg process handles per resolution
|
||||
std::map<std::string, void*> encoder_handles;
|
||||
|
||||
// Statistics
|
||||
std::chrono::system_clock::time_point started_at;
|
||||
std::atomic<int> viewer_count{0};
|
||||
};
|
||||
|
||||
/**
|
||||
* TV Engine
|
||||
*
|
||||
* Manages TV channel simulation with scheduling, EPG generation,
|
||||
* multi-resolution HLS output, and commercial/bumper insertion.
|
||||
*/
|
||||
class TvEngine {
|
||||
public:
|
||||
TvEngine();
|
||||
~TvEngine();
|
||||
|
||||
// Disable copying
|
||||
TvEngine(const TvEngine&) = delete;
|
||||
TvEngine& operator=(const TvEngine&) = delete;
|
||||
|
||||
// ========================================================================
|
||||
// Initialization
|
||||
// ========================================================================
|
||||
|
||||
/**
|
||||
* Initialize the TV engine
|
||||
* @param config Engine configuration
|
||||
* @param plugin_manager Plugin manager for video processing
|
||||
* @return Result indicating success or failure
|
||||
*/
|
||||
Result<void> initialize(
|
||||
const TvEngineConfig& config,
|
||||
PluginManager* plugin_manager
|
||||
);
|
||||
|
||||
/**
|
||||
* Shutdown all channels and cleanup
|
||||
*/
|
||||
void shutdown();
|
||||
|
||||
// ========================================================================
|
||||
// Channel Management
|
||||
// ========================================================================
|
||||
|
||||
/**
|
||||
* Create a new TV channel
|
||||
* @param config Channel configuration
|
||||
* @return Result with channel ID or error
|
||||
*/
|
||||
Result<std::string> create_channel(const TvChannelConfig& config);
|
||||
|
||||
/**
|
||||
* Delete a TV channel
|
||||
* @param channel_id Channel ID
|
||||
* @return Result indicating success or failure
|
||||
*/
|
||||
Result<void> delete_channel(const std::string& channel_id);
|
||||
|
||||
/**
|
||||
* Update channel configuration
|
||||
* @param channel_id Channel ID
|
||||
* @param config New configuration
|
||||
* @return Result indicating success or failure
|
||||
*/
|
||||
Result<void> update_channel(
|
||||
const std::string& channel_id,
|
||||
const TvChannelConfig& config
|
||||
);
|
||||
|
||||
/**
|
||||
* Get channel status
|
||||
* @param channel_id Channel ID
|
||||
* @return Result with channel status or error
|
||||
*/
|
||||
Result<TvChannelStatus> get_channel_status(
|
||||
const std::string& channel_id
|
||||
) const;
|
||||
|
||||
/**
|
||||
* List all channels
|
||||
* @param tenant_id Filter by tenant (empty for all)
|
||||
* @return Vector of channel statuses
|
||||
*/
|
||||
std::vector<TvChannelStatus> list_channels(
|
||||
const std::string& tenant_id = ""
|
||||
) const;
|
||||
|
||||
// ========================================================================
|
||||
// Streaming Control
|
||||
// ========================================================================
|
||||
|
||||
/**
|
||||
* Start streaming a channel
|
||||
* @param channel_id Channel ID
|
||||
* @return Result with stream URLs or error
|
||||
*/
|
||||
struct StreamUrls {
|
||||
std::string hls_url;
|
||||
std::string dash_url;
|
||||
std::map<std::string, std::string> quality_urls; // resolution -> URL
|
||||
};
|
||||
Result<StreamUrls> start_channel(const std::string& channel_id);
|
||||
|
||||
/**
|
||||
* Stop streaming a channel
|
||||
* @param channel_id Channel ID
|
||||
* @return Result indicating success or failure
|
||||
*/
|
||||
Result<void> stop_channel(const std::string& channel_id);
|
||||
|
||||
// ========================================================================
|
||||
// Schedule Management
|
||||
// ========================================================================
|
||||
|
||||
/**
|
||||
* Set channel schedule
|
||||
* @param channel_id Channel ID
|
||||
* @param entries Schedule entries
|
||||
* @return Result indicating success or failure
|
||||
*/
|
||||
Result<void> set_schedule(
|
||||
const std::string& channel_id,
|
||||
const std::vector<TvScheduleEntry>& entries
|
||||
);
|
||||
|
||||
/**
|
||||
* Add program to schedule
|
||||
* @param channel_id Channel ID
|
||||
* @param entry Schedule entry
|
||||
* @return Result indicating success or failure
|
||||
*/
|
||||
Result<void> add_program(
|
||||
const std::string& channel_id,
|
||||
const TvScheduleEntry& entry
|
||||
);
|
||||
|
||||
/**
|
||||
* Remove program from schedule
|
||||
* @param channel_id Channel ID
|
||||
* @param program_id Program ID
|
||||
* @return Result indicating success or failure
|
||||
*/
|
||||
Result<void> remove_program(
|
||||
const std::string& channel_id,
|
||||
const std::string& program_id
|
||||
);
|
||||
|
||||
/**
|
||||
* Get channel schedule
|
||||
* @param channel_id Channel ID
|
||||
* @param start_time Start of time range
|
||||
* @param end_time End of time range
|
||||
* @return Result with schedule or error
|
||||
*/
|
||||
Result<std::vector<TvScheduleEntry>> get_schedule(
|
||||
const std::string& channel_id,
|
||||
std::chrono::system_clock::time_point start_time,
|
||||
std::chrono::system_clock::time_point end_time
|
||||
) const;
|
||||
|
||||
// ========================================================================
|
||||
// EPG (Electronic Program Guide)
|
||||
// ========================================================================
|
||||
|
||||
/**
|
||||
* Generate EPG for all channels
|
||||
* @param hours_ahead Hours of programming to include
|
||||
* @return Vector of EPG entries
|
||||
*/
|
||||
std::vector<EpgEntry> generate_epg(int hours_ahead = 24) const;
|
||||
|
||||
/**
|
||||
* Generate EPG for specific channel
|
||||
* @param channel_id Channel ID
|
||||
* @param hours_ahead Hours of programming
|
||||
* @return Result with EPG entries or error
|
||||
*/
|
||||
Result<std::vector<EpgEntry>> generate_channel_epg(
|
||||
const std::string& channel_id,
|
||||
int hours_ahead = 24
|
||||
) const;
|
||||
|
||||
/**
|
||||
* Export EPG as XMLTV format
|
||||
* @param hours_ahead Hours of programming
|
||||
* @return XMLTV formatted string
|
||||
*/
|
||||
std::string export_xmltv(int hours_ahead = 24) const;
|
||||
|
||||
// ========================================================================
|
||||
// Now Playing
|
||||
// ========================================================================
|
||||
|
||||
/**
|
||||
* Get current program
|
||||
* @param channel_id Channel ID
|
||||
* @return Result with current program or error
|
||||
*/
|
||||
Result<TvProgram> get_now_playing(const std::string& channel_id) const;
|
||||
|
||||
/**
|
||||
* Get next program
|
||||
* @param channel_id Channel ID
|
||||
* @return Result with next program or error
|
||||
*/
|
||||
Result<TvProgram> get_next_program(const std::string& channel_id) const;
|
||||
|
||||
// ========================================================================
|
||||
// Interstitials (Bumpers/Commercials)
|
||||
// ========================================================================
|
||||
|
||||
/**
|
||||
* Set channel bumpers
|
||||
* @param channel_id Channel ID
|
||||
* @param intro_bumper Path to intro bumper video
|
||||
* @param outro_bumper Path to outro bumper video
|
||||
* @return Result indicating success or failure
|
||||
*/
|
||||
Result<void> set_bumpers(
|
||||
const std::string& channel_id,
|
||||
const std::string& intro_bumper,
|
||||
const std::string& outro_bumper
|
||||
);
|
||||
|
||||
/**
|
||||
* Set commercial break playlist
|
||||
* @param channel_id Channel ID
|
||||
* @param commercials List of commercial video paths
|
||||
* @param break_duration_seconds Target break duration
|
||||
* @return Result indicating success or failure
|
||||
*/
|
||||
Result<void> set_commercials(
|
||||
const std::string& channel_id,
|
||||
const std::vector<std::string>& commercials,
|
||||
int break_duration_seconds = 120
|
||||
);
|
||||
|
||||
// ========================================================================
|
||||
// Statistics
|
||||
// ========================================================================
|
||||
|
||||
/**
|
||||
* Update viewer count (called by stream server)
|
||||
* @param channel_id Channel ID
|
||||
* @param delta Change in viewer count
|
||||
*/
|
||||
void update_viewer_count(const std::string& channel_id, int delta);
|
||||
|
||||
/**
|
||||
* Get total viewer count across all channels
|
||||
*/
|
||||
int get_total_viewers() const;
|
||||
|
||||
private:
|
||||
/**
|
||||
* Stream thread function
|
||||
*/
|
||||
void stream_thread(const std::string& channel_id);
|
||||
|
||||
/**
|
||||
* Get current program based on schedule
|
||||
*/
|
||||
const TvScheduleEntry* get_current_scheduled_program(
|
||||
const TvChannelState& state
|
||||
) const;
|
||||
|
||||
/**
|
||||
* Prepare next segment
|
||||
*/
|
||||
void prepare_next_segment(TvChannelState& state);
|
||||
|
||||
/**
|
||||
* Encode video segment
|
||||
*/
|
||||
void encode_segment(
|
||||
TvChannelState& state,
|
||||
const std::string& input_path,
|
||||
double start_time,
|
||||
double duration
|
||||
);
|
||||
|
||||
/**
|
||||
* Generate HLS master playlist
|
||||
*/
|
||||
void generate_master_playlist(const std::string& channel_id);
|
||||
|
||||
/**
|
||||
* Update variant playlists
|
||||
*/
|
||||
void update_variant_playlist(
|
||||
const std::string& channel_id,
|
||||
const std::string& resolution,
|
||||
const std::string& segment_filename
|
||||
);
|
||||
|
||||
/**
|
||||
* Insert bumper/commercial
|
||||
*/
|
||||
void insert_interstitial(
|
||||
TvChannelState& state,
|
||||
const std::string& video_path
|
||||
);
|
||||
|
||||
// Configuration
|
||||
TvEngineConfig config_;
|
||||
PluginManager* plugin_manager_ = nullptr;
|
||||
|
||||
// State
|
||||
std::atomic<bool> initialized_{false};
|
||||
|
||||
// Channels
|
||||
mutable std::mutex channels_mutex_;
|
||||
std::map<std::string, std::unique_ptr<TvChannelState>> channels_;
|
||||
|
||||
// EPG refresh thread
|
||||
std::thread epg_thread_;
|
||||
std::atomic<bool> epg_running_{false};
|
||||
};
|
||||
|
||||
} // namespace media
|
||||
407
services/media_daemon/include/media/types.hpp
Normal file
407
services/media_daemon/include/media/types.hpp
Normal file
@@ -0,0 +1,407 @@
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <memory>
|
||||
#include <functional>
|
||||
#include <chrono>
|
||||
#include <optional>
|
||||
#include <variant>
|
||||
#include <map>
|
||||
|
||||
namespace media {
|
||||
|
||||
// Forward declarations
|
||||
class Plugin;
|
||||
class Job;
|
||||
class RadioChannel;
|
||||
class TvChannel;
|
||||
|
||||
// ============================================================================
|
||||
// Error Handling (following DBAL patterns)
|
||||
// ============================================================================
|
||||
|
||||
enum class ErrorCode {
|
||||
OK = 0,
|
||||
NOT_FOUND = 404,
|
||||
CONFLICT = 409,
|
||||
UNAUTHORIZED = 401,
|
||||
FORBIDDEN = 403,
|
||||
VALIDATION_ERROR = 422,
|
||||
INTERNAL_ERROR = 500,
|
||||
SERVICE_UNAVAILABLE = 503,
|
||||
TIMEOUT = 504,
|
||||
PLUGIN_ERROR = 1001,
|
||||
TRANSCODE_ERROR = 1002,
|
||||
STREAM_ERROR = 1003,
|
||||
STORAGE_ERROR = 1004
|
||||
};
|
||||
|
||||
template<typename T>
|
||||
class Result {
|
||||
public:
|
||||
static Result<T> ok(T value) {
|
||||
Result<T> r;
|
||||
r.value_ = std::move(value);
|
||||
r.error_code_ = ErrorCode::OK;
|
||||
return r;
|
||||
}
|
||||
|
||||
static Result<T> error(ErrorCode code, const std::string& message) {
|
||||
Result<T> r;
|
||||
r.error_code_ = code;
|
||||
r.error_message_ = message;
|
||||
return r;
|
||||
}
|
||||
|
||||
bool is_ok() const { return error_code_ == ErrorCode::OK; }
|
||||
bool is_error() const { return error_code_ != ErrorCode::OK; }
|
||||
|
||||
const T& value() const { return value_; }
|
||||
T& value() { return value_; }
|
||||
|
||||
ErrorCode error_code() const { return error_code_; }
|
||||
const std::string& error_message() const { return error_message_; }
|
||||
|
||||
private:
|
||||
T value_;
|
||||
ErrorCode error_code_ = ErrorCode::OK;
|
||||
std::string error_message_;
|
||||
};
|
||||
|
||||
// Specialization for void
|
||||
template<>
|
||||
class Result<void> {
|
||||
public:
|
||||
static Result<void> ok() {
|
||||
Result<void> r;
|
||||
r.error_code_ = ErrorCode::OK;
|
||||
return r;
|
||||
}
|
||||
|
||||
static Result<void> error(ErrorCode code, const std::string& message) {
|
||||
Result<void> r;
|
||||
r.error_code_ = code;
|
||||
r.error_message_ = message;
|
||||
return r;
|
||||
}
|
||||
|
||||
bool is_ok() const { return error_code_ == ErrorCode::OK; }
|
||||
bool is_error() const { return error_code_ != ErrorCode::OK; }
|
||||
|
||||
ErrorCode error_code() const { return error_code_; }
|
||||
const std::string& error_message() const { return error_message_; }
|
||||
|
||||
private:
|
||||
ErrorCode error_code_ = ErrorCode::OK;
|
||||
std::string error_message_;
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Job Types
|
||||
// ============================================================================
|
||||
|
||||
enum class JobType {
|
||||
VIDEO_TRANSCODE,
|
||||
AUDIO_TRANSCODE,
|
||||
DOCUMENT_CONVERT,
|
||||
IMAGE_PROCESS,
|
||||
RADIO_INGEST,
|
||||
TV_SEGMENT,
|
||||
CUSTOM
|
||||
};
|
||||
|
||||
enum class JobStatus {
|
||||
PENDING,
|
||||
QUEUED,
|
||||
PROCESSING,
|
||||
COMPLETED,
|
||||
FAILED,
|
||||
CANCELLED
|
||||
};
|
||||
|
||||
enum class JobPriority {
|
||||
URGENT = 0,
|
||||
HIGH = 1,
|
||||
NORMAL = 2,
|
||||
LOW = 3,
|
||||
BACKGROUND = 4
|
||||
};
|
||||
|
||||
struct JobProgress {
|
||||
double percent = 0.0; // 0-100
|
||||
std::string stage; // Current stage (e.g., "encoding", "muxing")
|
||||
std::string eta; // Estimated time remaining
|
||||
size_t bytes_processed = 0;
|
||||
size_t bytes_total = 0;
|
||||
};
|
||||
|
||||
struct VideoTranscodeParams {
|
||||
std::string input_path;
|
||||
std::string output_path;
|
||||
std::string codec = "h264"; // h264, h265, vp9, av1
|
||||
int width = 0; // 0 = auto
|
||||
int height = 0;
|
||||
int bitrate_kbps = 0; // 0 = auto
|
||||
std::string preset = "fast";
|
||||
std::string audio_codec = "aac";
|
||||
int audio_bitrate_kbps = 128;
|
||||
std::map<std::string, std::string> extra_params;
|
||||
};
|
||||
|
||||
struct AudioTranscodeParams {
|
||||
std::string input_path;
|
||||
std::string output_path;
|
||||
std::string codec = "mp3"; // mp3, aac, flac, opus
|
||||
int bitrate_kbps = 128;
|
||||
int sample_rate = 44100;
|
||||
int channels = 2;
|
||||
bool normalize = true;
|
||||
double target_lufs = -14.0;
|
||||
};
|
||||
|
||||
struct DocumentConvertParams {
|
||||
std::string input_path;
|
||||
std::string output_path;
|
||||
std::string output_format; // pdf, docx, html, etc.
|
||||
std::string template_path;
|
||||
std::map<std::string, std::string> variables;
|
||||
};
|
||||
|
||||
struct ImageProcessParams {
|
||||
std::string input_path;
|
||||
std::string output_path;
|
||||
std::string format; // jpg, png, webp, avif
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
int quality = 85;
|
||||
bool preserve_aspect = true;
|
||||
std::vector<std::string> filters; // blur, sharpen, etc.
|
||||
};
|
||||
|
||||
using JobParams = std::variant<
|
||||
VideoTranscodeParams,
|
||||
AudioTranscodeParams,
|
||||
DocumentConvertParams,
|
||||
ImageProcessParams,
|
||||
std::map<std::string, std::string> // Custom params
|
||||
>;
|
||||
|
||||
struct JobRequest {
|
||||
std::string id; // Auto-generated if empty
|
||||
std::string tenant_id;
|
||||
std::string user_id;
|
||||
JobType type;
|
||||
JobPriority priority = JobPriority::NORMAL;
|
||||
JobParams params;
|
||||
std::string callback_url; // Webhook on completion
|
||||
bool notify_user = true; // Send DBAL notification
|
||||
std::map<std::string, std::string> metadata;
|
||||
};
|
||||
|
||||
struct JobInfo {
|
||||
std::string id;
|
||||
std::string tenant_id;
|
||||
std::string user_id;
|
||||
JobType type;
|
||||
JobStatus status;
|
||||
JobPriority priority;
|
||||
JobProgress progress;
|
||||
std::chrono::system_clock::time_point created_at;
|
||||
std::chrono::system_clock::time_point started_at;
|
||||
std::chrono::system_clock::time_point completed_at;
|
||||
std::string error_message;
|
||||
std::string output_path;
|
||||
std::map<std::string, std::string> metadata;
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Radio Types
|
||||
// ============================================================================
|
||||
|
||||
struct RadioTrack {
|
||||
std::string id;
|
||||
std::string path;
|
||||
std::string title;
|
||||
std::string artist;
|
||||
std::string album;
|
||||
std::string artwork_url;
|
||||
int duration_ms = 0;
|
||||
std::map<std::string, std::string> metadata;
|
||||
};
|
||||
|
||||
struct RadioPlaylistEntry {
|
||||
RadioTrack track;
|
||||
std::chrono::system_clock::time_point scheduled_at;
|
||||
bool played = false;
|
||||
};
|
||||
|
||||
struct RadioChannelConfig {
|
||||
std::string id;
|
||||
std::string tenant_id;
|
||||
std::string name;
|
||||
std::string description;
|
||||
std::string artwork_url;
|
||||
|
||||
// Audio settings
|
||||
int bitrate_kbps = 128;
|
||||
std::string codec = "mp3";
|
||||
int sample_rate = 44100;
|
||||
|
||||
// Crossfade
|
||||
bool crossfade_enabled = true;
|
||||
int crossfade_ms = 3000;
|
||||
|
||||
// Auto-DJ settings
|
||||
bool auto_dj_enabled = true;
|
||||
std::vector<std::string> auto_dj_folders;
|
||||
bool shuffle = true;
|
||||
};
|
||||
|
||||
struct RadioChannelStatus {
|
||||
std::string id;
|
||||
std::string name;
|
||||
bool is_live = false;
|
||||
int listeners = 0;
|
||||
std::optional<RadioTrack> now_playing;
|
||||
std::optional<RadioTrack> next_track;
|
||||
int uptime_seconds = 0;
|
||||
std::string stream_url;
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// TV Channel Types
|
||||
// ============================================================================
|
||||
|
||||
struct TvProgram {
|
||||
std::string id;
|
||||
std::string title;
|
||||
std::string description;
|
||||
std::string category; // movie, series, news, sports, etc.
|
||||
std::string content_path; // Video file or playlist
|
||||
int duration_seconds = 0;
|
||||
std::string thumbnail_url;
|
||||
std::string rating; // G, PG, PG-13, R, etc.
|
||||
std::map<std::string, std::string> metadata;
|
||||
};
|
||||
|
||||
struct TvScheduleEntry {
|
||||
TvProgram program;
|
||||
std::chrono::system_clock::time_point start_time;
|
||||
std::chrono::system_clock::time_point end_time;
|
||||
bool is_live = false;
|
||||
std::string bumper_before; // Pre-roll video
|
||||
std::string bumper_after; // Post-roll video
|
||||
};
|
||||
|
||||
struct TvChannelConfig {
|
||||
std::string id;
|
||||
std::string tenant_id;
|
||||
std::string name;
|
||||
std::string description;
|
||||
std::string logo_url;
|
||||
int channel_number = 0;
|
||||
|
||||
// Video settings
|
||||
std::vector<std::string> resolutions; // 1080p, 720p, 480p
|
||||
std::string codec = "h264";
|
||||
|
||||
// HLS settings
|
||||
int segment_duration_seconds = 4;
|
||||
int playlist_size = 10;
|
||||
|
||||
// Filler content (when nothing scheduled)
|
||||
std::string filler_playlist;
|
||||
std::string offline_image;
|
||||
};
|
||||
|
||||
struct TvChannelStatus {
|
||||
std::string id;
|
||||
std::string name;
|
||||
int channel_number;
|
||||
bool is_live = false;
|
||||
std::optional<TvProgram> now_playing;
|
||||
std::optional<TvProgram> next_program;
|
||||
int viewers = 0;
|
||||
std::string hls_url;
|
||||
std::string dash_url;
|
||||
};
|
||||
|
||||
struct EpgEntry {
|
||||
std::string channel_id;
|
||||
std::string channel_name;
|
||||
TvProgram program;
|
||||
std::chrono::system_clock::time_point start_time;
|
||||
std::chrono::system_clock::time_point end_time;
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Plugin Types
|
||||
// ============================================================================
|
||||
|
||||
enum class PluginType {
|
||||
TRANSCODER, // Video/audio transcoding
|
||||
PROCESSOR, // Image/document processing
|
||||
STREAMER, // Streaming output
|
||||
ANALYZER, // Media analysis
|
||||
CUSTOM
|
||||
};
|
||||
|
||||
struct PluginInfo {
|
||||
std::string id;
|
||||
std::string name;
|
||||
std::string version;
|
||||
std::string author;
|
||||
std::string description;
|
||||
PluginType type;
|
||||
std::vector<std::string> supported_formats;
|
||||
std::vector<std::string> capabilities;
|
||||
bool is_loaded = false;
|
||||
bool is_builtin = false;
|
||||
};
|
||||
|
||||
struct PluginCapabilities {
|
||||
bool supports_video = false;
|
||||
bool supports_audio = false;
|
||||
bool supports_image = false;
|
||||
bool supports_document = false;
|
||||
bool supports_streaming = false;
|
||||
bool supports_hardware_accel = false;
|
||||
std::vector<std::string> input_formats;
|
||||
std::vector<std::string> output_formats;
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Notification Types (for DBAL integration)
|
||||
// ============================================================================
|
||||
|
||||
enum class NotificationType {
|
||||
JOB_STARTED,
|
||||
JOB_PROGRESS,
|
||||
JOB_COMPLETED,
|
||||
JOB_FAILED,
|
||||
STREAM_STARTED,
|
||||
STREAM_STOPPED,
|
||||
CHANNEL_LIVE,
|
||||
CHANNEL_OFFLINE
|
||||
};
|
||||
|
||||
struct Notification {
|
||||
std::string tenant_id;
|
||||
std::string user_id;
|
||||
NotificationType type;
|
||||
std::string title;
|
||||
std::string message;
|
||||
std::string icon; // success, error, warning, info
|
||||
std::map<std::string, std::string> data;
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// Callback Types
|
||||
// ============================================================================
|
||||
|
||||
using JobProgressCallback = std::function<void(const std::string& job_id, const JobProgress& progress)>;
|
||||
using JobCompletionCallback = std::function<void(const std::string& job_id, bool success, const std::string& result)>;
|
||||
using NotificationCallback = std::function<void(const Notification& notification)>;
|
||||
|
||||
} // namespace media
|
||||
152
services/media_daemon/schema/entities/media_job.yaml
Normal file
152
services/media_daemon/schema/entities/media_job.yaml
Normal file
@@ -0,0 +1,152 @@
|
||||
# Media Job Entity Schema
|
||||
# Following DBAL YAML-first design pattern
|
||||
|
||||
entity: MediaJob
|
||||
version: "1.0"
|
||||
description: "Media processing job for transcoding, conversion, and streaming"
|
||||
|
||||
fields:
|
||||
id:
|
||||
type: uuid
|
||||
primary: true
|
||||
generated: true
|
||||
description: "Unique job identifier"
|
||||
|
||||
tenant_id:
|
||||
type: uuid
|
||||
required: true
|
||||
indexed: true
|
||||
description: "Tenant for multi-tenancy isolation"
|
||||
|
||||
user_id:
|
||||
type: uuid
|
||||
required: true
|
||||
indexed: true
|
||||
description: "User who submitted the job"
|
||||
|
||||
type:
|
||||
type: enum
|
||||
values: [video_transcode, audio_transcode, document_convert, image_process, radio_ingest, tv_segment, custom]
|
||||
required: true
|
||||
indexed: true
|
||||
description: "Type of media job"
|
||||
|
||||
status:
|
||||
type: enum
|
||||
values: [pending, queued, processing, completed, failed, cancelled]
|
||||
default: pending
|
||||
indexed: true
|
||||
description: "Current job status"
|
||||
|
||||
priority:
|
||||
type: enum
|
||||
values: [urgent, high, normal, low, background]
|
||||
default: normal
|
||||
description: "Job priority level"
|
||||
|
||||
params:
|
||||
type: json
|
||||
required: true
|
||||
description: "Job-specific parameters (codec, bitrate, etc.)"
|
||||
|
||||
progress:
|
||||
type: json
|
||||
description: "Progress tracking (percent, stage, eta)"
|
||||
|
||||
input_path:
|
||||
type: string
|
||||
required: true
|
||||
max_length: 1024
|
||||
description: "Path to input file(s)"
|
||||
|
||||
output_path:
|
||||
type: string
|
||||
max_length: 1024
|
||||
description: "Path to output file(s)"
|
||||
|
||||
error_message:
|
||||
type: string
|
||||
max_length: 4096
|
||||
description: "Error message if job failed"
|
||||
|
||||
callback_url:
|
||||
type: string
|
||||
max_length: 512
|
||||
description: "Webhook URL for completion notification"
|
||||
|
||||
notify_user:
|
||||
type: boolean
|
||||
default: true
|
||||
description: "Send DBAL notification on completion"
|
||||
|
||||
metadata:
|
||||
type: json
|
||||
description: "Additional metadata"
|
||||
|
||||
created_at:
|
||||
type: timestamp
|
||||
auto_now_add: true
|
||||
indexed: true
|
||||
description: "Job creation timestamp"
|
||||
|
||||
started_at:
|
||||
type: timestamp
|
||||
description: "Job start timestamp"
|
||||
|
||||
completed_at:
|
||||
type: timestamp
|
||||
description: "Job completion timestamp"
|
||||
|
||||
duration_ms:
|
||||
type: integer
|
||||
description: "Processing duration in milliseconds"
|
||||
|
||||
indexes:
|
||||
- fields: [tenant_id, user_id]
|
||||
name: idx_job_tenant_user
|
||||
- fields: [tenant_id, status]
|
||||
name: idx_job_tenant_status
|
||||
- fields: [status, priority, created_at]
|
||||
name: idx_job_queue
|
||||
|
||||
acl:
|
||||
create:
|
||||
min_level: 1 # User level
|
||||
read:
|
||||
self: true
|
||||
min_level: 2 # Moderator can see all
|
||||
update:
|
||||
self: true
|
||||
min_level: 3 # Admin
|
||||
delete:
|
||||
min_level: 3 # Admin
|
||||
|
||||
operations:
|
||||
submit:
|
||||
description: "Submit a new job"
|
||||
input:
|
||||
- type
|
||||
- params
|
||||
- priority
|
||||
- callback_url
|
||||
- notify_user
|
||||
- metadata
|
||||
output: [id, status, created_at]
|
||||
|
||||
cancel:
|
||||
description: "Cancel a pending or processing job"
|
||||
input: [id]
|
||||
output: [status]
|
||||
|
||||
retry:
|
||||
description: "Retry a failed job"
|
||||
input: [id]
|
||||
output: [id, status]
|
||||
|
||||
list_pending:
|
||||
description: "List pending jobs for a tenant"
|
||||
input:
|
||||
- tenant_id
|
||||
- limit?
|
||||
- offset?
|
||||
output: [id, type, status, priority, created_at]
|
||||
235
services/media_daemon/src/main.cpp
Normal file
235
services/media_daemon/src/main.cpp
Normal file
@@ -0,0 +1,235 @@
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#include <csignal>
|
||||
#include <memory>
|
||||
#include <fstream>
|
||||
|
||||
// Cross-platform signal handling
|
||||
#ifdef _WIN32
|
||||
#include <windows.h>
|
||||
#ifndef SIGTERM
|
||||
#define SIGTERM SIGBREAK
|
||||
#endif
|
||||
#endif
|
||||
|
||||
#include "media/server.hpp"
|
||||
|
||||
namespace {
|
||||
std::unique_ptr<media::Server> server_instance;
|
||||
|
||||
void signal_handler(int signal) {
|
||||
if (signal == SIGINT || signal == SIGTERM) {
|
||||
std::cout << "\nShutting down Media Daemon..." << std::endl;
|
||||
if (server_instance) {
|
||||
server_instance->stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void print_banner() {
|
||||
std::cout << R"(
|
||||
╔═══════════════════════════════════════════════════════════╗
|
||||
║ MetaBuilder Media Daemon ║
|
||||
║ v1.0.0 ║
|
||||
╠═══════════════════════════════════════════════════════════╣
|
||||
║ Job Queue │ Video, Audio, Document, Image Processing ║
|
||||
║ Radio │ Streaming, Auto-DJ, Crossfade ║
|
||||
║ TV Channels │ Schedule, EPG, HLS/DASH Output ║
|
||||
║ Plugins │ FFmpeg, ImageMagick, Pandoc, Custom ║
|
||||
╚═══════════════════════════════════════════════════════════╝
|
||||
)" << std::endl;
|
||||
}
|
||||
|
||||
void print_help(const char* program) {
|
||||
std::cout << "Usage: " << program << " [options]" << std::endl;
|
||||
std::cout << std::endl;
|
||||
std::cout << "Options:" << std::endl;
|
||||
std::cout << " --config <file> Configuration file (default: /etc/media-daemon/config.yaml)" << std::endl;
|
||||
std::cout << " --bind <address> Bind address (default: 0.0.0.0)" << std::endl;
|
||||
std::cout << " --port <port> Port number (default: 8090)" << std::endl;
|
||||
std::cout << " --workers <n> Number of HTTP workers (default: 4)" << std::endl;
|
||||
std::cout << " --dbal-url <url> DBAL daemon URL (default: http://localhost:8080)" << std::endl;
|
||||
std::cout << " --plugin-dir <dir> Plugin directory (default: /plugins)" << std::endl;
|
||||
std::cout << " --dev Enable development mode (hot-reload, verbose logging)" << std::endl;
|
||||
std::cout << " --daemon, -d Run in daemon mode" << std::endl;
|
||||
std::cout << " --help, -h Show this help message" << std::endl;
|
||||
std::cout << std::endl;
|
||||
std::cout << "Environment variables:" << std::endl;
|
||||
std::cout << " MEDIA_BIND_ADDRESS Bind address" << std::endl;
|
||||
std::cout << " MEDIA_PORT Port number" << std::endl;
|
||||
std::cout << " MEDIA_WORKERS HTTP worker threads" << std::endl;
|
||||
std::cout << " DBAL_URL DBAL daemon URL" << std::endl;
|
||||
std::cout << " DBAL_API_KEY DBAL API key" << std::endl;
|
||||
std::cout << " MEDIA_PLUGIN_DIR Plugin directory" << std::endl;
|
||||
std::cout << " MEDIA_DEV_MODE Development mode (true/false)" << std::endl;
|
||||
std::cout << std::endl;
|
||||
}
|
||||
|
||||
std::string get_env(const char* name, const std::string& default_value = "") {
|
||||
const char* value = std::getenv(name);
|
||||
return value ? value : default_value;
|
||||
}
|
||||
|
||||
int get_env_int(const char* name, int default_value) {
|
||||
const char* value = std::getenv(name);
|
||||
if (value) {
|
||||
try {
|
||||
return std::stoi(value);
|
||||
} catch (...) {}
|
||||
}
|
||||
return default_value;
|
||||
}
|
||||
|
||||
bool get_env_bool(const char* name, bool default_value = false) {
|
||||
const char* value = std::getenv(name);
|
||||
if (value) {
|
||||
std::string str = value;
|
||||
return str == "true" || str == "1" || str == "yes";
|
||||
}
|
||||
return default_value;
|
||||
}
|
||||
}
|
||||
|
||||
int main(int argc, char* argv[]) {
|
||||
print_banner();
|
||||
|
||||
// Register signal handlers
|
||||
std::signal(SIGINT, signal_handler);
|
||||
std::signal(SIGTERM, signal_handler);
|
||||
|
||||
// Default configuration
|
||||
media::ServerConfig config;
|
||||
std::string config_file = "/etc/media-daemon/config.yaml";
|
||||
bool daemon_mode = false;
|
||||
|
||||
// Load from environment variables
|
||||
config.bind_address = get_env("MEDIA_BIND_ADDRESS", "0.0.0.0");
|
||||
config.port = get_env_int("MEDIA_PORT", 8090);
|
||||
config.workers = get_env_int("MEDIA_WORKERS", 4);
|
||||
config.development_mode = get_env_bool("MEDIA_DEV_MODE", false);
|
||||
config.plugin_dir = get_env("MEDIA_PLUGIN_DIR", "/plugins");
|
||||
|
||||
// DBAL settings
|
||||
config.dbal.url = get_env("DBAL_URL", "http://localhost:8080");
|
||||
config.dbal.api_key = get_env("DBAL_API_KEY", "");
|
||||
|
||||
// Job queue settings
|
||||
config.job_queue.video_workers = get_env_int("MEDIA_VIDEO_WORKERS", 2);
|
||||
config.job_queue.audio_workers = get_env_int("MEDIA_AUDIO_WORKERS", 4);
|
||||
config.job_queue.document_workers = get_env_int("MEDIA_DOC_WORKERS", 4);
|
||||
config.job_queue.image_workers = get_env_int("MEDIA_IMAGE_WORKERS", 8);
|
||||
config.job_queue.temp_dir = get_env("MEDIA_TEMP_DIR", "/data/temp");
|
||||
config.job_queue.output_dir = get_env("MEDIA_OUTPUT_DIR", "/data/output");
|
||||
|
||||
// Radio settings
|
||||
config.radio_enabled = get_env_bool("MEDIA_RADIO_ENABLED", true);
|
||||
config.radio.max_channels = get_env_int("MEDIA_RADIO_MAX_CHANNELS", 10);
|
||||
config.radio.hls_output_dir = get_env("MEDIA_RADIO_HLS_DIR", "/data/hls/radio");
|
||||
|
||||
// TV settings
|
||||
config.tv_enabled = get_env_bool("MEDIA_TV_ENABLED", true);
|
||||
config.tv.max_channels = get_env_int("MEDIA_TV_MAX_CHANNELS", 5);
|
||||
config.tv.hls_output_dir = get_env("MEDIA_TV_HLS_DIR", "/data/hls/tv");
|
||||
|
||||
// Parse command line arguments
|
||||
for (int i = 1; i < argc; i++) {
|
||||
std::string arg = argv[i];
|
||||
|
||||
if (arg == "--config" && i + 1 < argc) {
|
||||
config_file = argv[++i];
|
||||
} else if (arg == "--bind" && i + 1 < argc) {
|
||||
config.bind_address = argv[++i];
|
||||
} else if (arg == "--port" && i + 1 < argc) {
|
||||
config.port = std::stoi(argv[++i]);
|
||||
} else if (arg == "--workers" && i + 1 < argc) {
|
||||
config.workers = std::stoi(argv[++i]);
|
||||
} else if (arg == "--dbal-url" && i + 1 < argc) {
|
||||
config.dbal.url = argv[++i];
|
||||
} else if (arg == "--plugin-dir" && i + 1 < argc) {
|
||||
config.plugin_dir = argv[++i];
|
||||
} else if (arg == "--dev") {
|
||||
config.development_mode = true;
|
||||
config.hot_reload = true;
|
||||
} else if (arg == "--daemon" || arg == "-d") {
|
||||
daemon_mode = true;
|
||||
} else if (arg == "--help" || arg == "-h") {
|
||||
print_help(argv[0]);
|
||||
return 0;
|
||||
} else {
|
||||
std::cerr << "Unknown option: " << arg << std::endl;
|
||||
print_help(argv[0]);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Print configuration summary
|
||||
std::cout << "Configuration:" << std::endl;
|
||||
std::cout << " Bind Address: " << config.bind_address << std::endl;
|
||||
std::cout << " Port: " << config.port << std::endl;
|
||||
std::cout << " Workers: " << config.workers << std::endl;
|
||||
std::cout << " DBAL URL: " << config.dbal.url << std::endl;
|
||||
std::cout << " Plugin Dir: " << config.plugin_dir << std::endl;
|
||||
std::cout << " Development Mode: " << (config.development_mode ? "yes" : "no") << std::endl;
|
||||
std::cout << " Radio Enabled: " << (config.radio_enabled ? "yes" : "no") << std::endl;
|
||||
std::cout << " TV Enabled: " << (config.tv_enabled ? "yes" : "no") << std::endl;
|
||||
std::cout << std::endl;
|
||||
|
||||
// Create and initialize server
|
||||
server_instance = std::make_unique<media::Server>();
|
||||
|
||||
auto result = server_instance->initialize(config);
|
||||
if (result.is_error()) {
|
||||
std::cerr << "Failed to initialize server: " << result.error_message() << std::endl;
|
||||
return 1;
|
||||
}
|
||||
|
||||
std::cout << "Server initialized successfully" << std::endl;
|
||||
std::cout << "Starting HTTP server on " << config.bind_address << ":" << config.port << std::endl;
|
||||
std::cout << std::endl;
|
||||
|
||||
// Print available endpoints
|
||||
std::cout << "Endpoints:" << std::endl;
|
||||
std::cout << " Health: GET /health" << std::endl;
|
||||
std::cout << " Metrics: GET /metrics" << std::endl;
|
||||
std::cout << std::endl;
|
||||
std::cout << " Jobs:" << std::endl;
|
||||
std::cout << " POST /api/jobs - Submit job" << std::endl;
|
||||
std::cout << " GET /api/jobs - List jobs" << std::endl;
|
||||
std::cout << " GET /api/jobs/:id - Get job status" << std::endl;
|
||||
std::cout << " DELETE /api/jobs/:id - Cancel job" << std::endl;
|
||||
std::cout << std::endl;
|
||||
std::cout << " Radio:" << std::endl;
|
||||
std::cout << " POST /api/radio/channels - Create channel" << std::endl;
|
||||
std::cout << " GET /api/radio/channels - List channels" << std::endl;
|
||||
std::cout << " GET /api/radio/channels/:id - Get channel" << std::endl;
|
||||
std::cout << " POST /api/radio/channels/:id/start - Start streaming" << std::endl;
|
||||
std::cout << " POST /api/radio/channels/:id/stop - Stop streaming" << std::endl;
|
||||
std::cout << " GET /api/radio/channels/:id/now - Now playing" << std::endl;
|
||||
std::cout << std::endl;
|
||||
std::cout << " TV:" << std::endl;
|
||||
std::cout << " POST /api/tv/channels - Create channel" << std::endl;
|
||||
std::cout << " GET /api/tv/channels - List channels" << std::endl;
|
||||
std::cout << " GET /api/tv/channels/:id - Get channel" << std::endl;
|
||||
std::cout << " POST /api/tv/channels/:id/start - Start streaming" << std::endl;
|
||||
std::cout << " POST /api/tv/channels/:id/stop - Stop streaming" << std::endl;
|
||||
std::cout << " GET /api/tv/channels/:id/schedule - Get EPG" << std::endl;
|
||||
std::cout << " GET /api/tv/epg - Full EPG (XMLTV)" << std::endl;
|
||||
std::cout << std::endl;
|
||||
std::cout << " Plugins:" << std::endl;
|
||||
std::cout << " GET /api/plugins - List plugins" << std::endl;
|
||||
std::cout << " POST /api/plugins/:id/reload - Reload plugin (dev)" << std::endl;
|
||||
std::cout << std::endl;
|
||||
|
||||
if (daemon_mode) {
|
||||
std::cout << "Running in daemon mode..." << std::endl;
|
||||
} else {
|
||||
std::cout << "Press Ctrl+C to stop" << std::endl;
|
||||
}
|
||||
std::cout << std::endl;
|
||||
|
||||
// Run server (blocking)
|
||||
server_instance->run();
|
||||
|
||||
std::cout << "Media Daemon stopped" << std::endl;
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user