Merge pull request #146 from jb-alvarado/master

support frontend v5.0.0
This commit is contained in:
jb-alvarado 2022-07-06 17:11:14 +02:00 committed by GitHub
commit 0632e862e5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
28 changed files with 1000 additions and 499 deletions

82
Cargo.lock generated
View File

@ -32,9 +32,9 @@ dependencies = [
[[package]] [[package]]
name = "actix-http" name = "actix-http"
version = "3.1.0" version = "3.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bd2e9f6794b5826aff6df65e3a0d0127b271d1c03629c774238f3582e903d4e4" checksum = "6f9ffb6db08c1c3a1f4aef540f1a63193adc73c4fbd40b75a95fc8c5258f6e51"
dependencies = [ dependencies = [
"actix-codec", "actix-codec",
"actix-rt", "actix-rt",
@ -626,7 +626,7 @@ dependencies = [
[[package]] [[package]]
name = "chrono" name = "chrono"
version = "0.4.20-beta.1" version = "0.4.20-beta.1"
source = "git+https://github.com/chronotope/chrono.git#39ac80a6a51b2a5081750c451feb261c1cb43960" source = "git+https://github.com/chronotope/chrono.git#686f72038e5cfa41c312cefa8d4569744cbff887"
dependencies = [ dependencies = [
"num-integer", "num-integer",
"num-traits", "num-traits",
@ -636,9 +636,9 @@ dependencies = [
[[package]] [[package]]
name = "clap" name = "clap"
version = "3.2.7" version = "3.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b7b16274bb247b45177db843202209b12191b631a14a9d06e41b3777d6ecf14" checksum = "190814073e85d238f31ff738fcb0bf6910cedeb73376c87cd69291028966fd83"
dependencies = [ dependencies = [
"atty", "atty",
"bitflags", "bitflags",
@ -880,9 +880,9 @@ checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7"
[[package]] [[package]]
name = "crypto-common" name = "crypto-common"
version = "0.1.3" version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57952ca27b5e3606ff4dd79b0020231aaf9d6aa76dc05fd30137538c50bd3ce8" checksum = "5999502d32b9c48d492abe66392408144895020ec4709e549e840799f3bb74c0"
dependencies = [ dependencies = [
"generic-array", "generic-array",
"typenum", "typenum",
@ -941,15 +941,15 @@ checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f"
[[package]] [[package]]
name = "either" name = "either"
version = "1.6.1" version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" checksum = "3f107b87b6afc2a64fd13cac55fe06d6c8859f12d4b14cbcdd2c67d0976781be"
[[package]] [[package]]
name = "email-encoding" name = "email-encoding"
version = "0.1.2" version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "827e1fb86d24d558ab0454ca3fa084f8a6144ade1e3e6982f697c586bf96b41b" checksum = "34dd14c63662e0206599796cd5e1ad0268ab2b9d19b868d6050d688eba2bbf98"
dependencies = [ dependencies = [
"base64", "base64",
"memchr", "memchr",
@ -1027,7 +1027,7 @@ dependencies = [
[[package]] [[package]]
name = "ffplayout-api" name = "ffplayout-api"
version = "0.3.2" version = "0.4.0"
dependencies = [ dependencies = [
"actix-multipart", "actix-multipart",
"actix-web", "actix-web",
@ -1100,14 +1100,14 @@ dependencies = [
[[package]] [[package]]
name = "filetime" name = "filetime"
version = "0.2.16" version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0408e2626025178a6a7f7ffc05a25bc47103229f19c113755de7bf63816290c" checksum = "e94a7bbaa59354bc20dd75b67f23e2797b4490e9d6928203fb105c79e448c86c"
dependencies = [ dependencies = [
"cfg-if 1.0.0", "cfg-if 1.0.0",
"libc", "libc",
"redox_syscall", "redox_syscall",
"winapi 0.3.9", "windows-sys",
] ]
[[package]] [[package]]
@ -1725,9 +1725,9 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
[[package]] [[package]]
name = "lettre" name = "lettre"
version = "0.10.0-rc.7" version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f7e87d9d44162eea7abd87b1a7540fcb10d5e58e8bb4f173178f3dc6e453944" checksum = "5677c78c7c7ede1dd68e8a7078012bc625449fb304e7b509b917eaaedfe6e849"
dependencies = [ dependencies = [
"base64", "base64",
"email-encoding", "email-encoding",
@ -2023,9 +2023,9 @@ dependencies = [
[[package]] [[package]]
name = "once_cell" name = "once_cell"
version = "1.12.0" version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7709cef83f0c1f58f666e746a08b21e0085f7440fa6a29cc194d68aac97a4225" checksum = "18a6dbe30758c9f83eb00cbea4ac95966305f5a7772f3f42ebfc7fc7eddbd8e1"
[[package]] [[package]]
name = "openssl" name = "openssl"
@ -2061,9 +2061,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
[[package]] [[package]]
name = "openssl-src" name = "openssl-src"
version = "111.21.0+1.1.1p" version = "111.22.0+1.1.1q"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d0a8313729211913936f1b95ca47a5fc7f2e04cd658c115388287f8a8361008" checksum = "8f31f0d509d1c1ae9cada2f9539ff8f37933831fd5098879e482aa687d659853"
dependencies = [ dependencies = [
"cc", "cc",
] ]
@ -2182,18 +2182,18 @@ checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e"
[[package]] [[package]]
name = "pin-project" name = "pin-project"
version = "1.0.10" version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "58ad3879ad3baf4e44784bc6a718a8698867bb991f8ce24d1bcbe2cfb4c3a75e" checksum = "78203e83c48cffbe01e4a2d35d566ca4de445d79a85372fc64e378bfc812a260"
dependencies = [ dependencies = [
"pin-project-internal", "pin-project-internal",
] ]
[[package]] [[package]]
name = "pin-project-internal" name = "pin-project-internal"
version = "1.0.10" version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "744b6f092ba29c3650faf274db506afd39944f48420f6c86b17cfe0ee1cb36bb" checksum = "710faf75e1b33345361201d36d04e98ac1ed8909151a017ed384700836104c74"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -2373,9 +2373,9 @@ dependencies = [
[[package]] [[package]]
name = "regex" name = "regex"
version = "1.5.6" version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d83f127d94bdbcda4c8cc2e50f6f84f4b611f69c902699ca385a39c3a75f9ff1" checksum = "4c4eb3267174b8c6c2f654116623910a0fef09c4753f8dd83db29c48a0df988b"
dependencies = [ dependencies = [
"aho-corasick", "aho-corasick",
"memchr", "memchr",
@ -2384,9 +2384,9 @@ dependencies = [
[[package]] [[package]]
name = "regex-syntax" name = "regex-syntax"
version = "0.6.26" version = "0.6.27"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49b3de9ec5dc0a3417da371aab17d729997c15010e7fd24ff707773a33bddb64" checksum = "a3f87b73ce11b1619a3c6332f45341e0047173771e8b8b73f87bfeefb7b56244"
[[package]] [[package]]
name = "relative-path" name = "relative-path"
@ -2542,24 +2542,24 @@ dependencies = [
[[package]] [[package]]
name = "semver" name = "semver"
version = "1.0.11" version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d92beeab217753479be2f74e54187a6aed4c125ff0703a866c3147a02f0c6dd" checksum = "a2333e6df6d6598f2b1974829f853c2b4c5f4a6e503c10af918081aa6f8564e1"
[[package]] [[package]]
name = "serde" name = "serde"
version = "1.0.137" version = "1.0.138"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61ea8d54c77f8315140a05f4c7237403bf38b72704d031543aa1d16abbf517d1" checksum = "1578c6245786b9d168c5447eeacfb96856573ca56c9d68fdcf394be134882a47"
dependencies = [ dependencies = [
"serde_derive", "serde_derive",
] ]
[[package]] [[package]]
name = "serde_derive" name = "serde_derive"
version = "1.0.137" version = "1.0.138"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f26faba0c3959972377d3b2d306ee9f71faee9714294e41bb777f83f88578be" checksum = "023e9b1467aef8a10fb88f25611870ada9800ef7e22afce356bb0d2387b6f27c"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -2568,9 +2568,9 @@ dependencies = [
[[package]] [[package]]
name = "serde_json" name = "serde_json"
version = "1.0.81" version = "1.0.82"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b7ce2b32a1aed03c558dc61a5cd328f15aff2dbc17daad8fb8af04d2100e15c" checksum = "82c2c1fdcd807d1098552c5b9a36e425e42e9fbd7c6a37a8425f390f781f7fa7"
dependencies = [ dependencies = [
"itoa", "itoa",
"ryu", "ryu",
@ -2670,9 +2670,9 @@ checksum = "eb703cfe953bccee95685111adeedb76fabe4e97549a58d16f03ea7b9367bb32"
[[package]] [[package]]
name = "smallvec" name = "smallvec"
version = "1.8.1" version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc88c725d61fc6c3132893370cac4a0200e3fedf5da8331c570664b1987f5ca2" checksum = "2fd0db749597d91ff862fd1d55ea87f7855a744a8425a64695b6fca237d1dad1"
[[package]] [[package]]
name = "socket2" name = "socket2"
@ -3076,9 +3076,9 @@ checksum = "5bd2fe26506023ed7b5e1e315add59d6f584c621d037f9368fea9cfb988f368c"
[[package]] [[package]]
name = "unicode-normalization" name = "unicode-normalization"
version = "0.1.20" version = "0.1.21"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81dee68f85cab8cf68dec42158baf3a79a1cdc065a8b103025965d6ccb7f6cbd" checksum = "854cbdc4f7bc6ae19c820d44abdc3277ac3e1b2b93db20a636825d9322fb60e6"
dependencies = [ dependencies = [
"tinyvec", "tinyvec",
] ]

View File

@ -1,15 +1,6 @@
[workspace] [workspace]
members = ["ffplayout-api", "ffplayout-engine", "lib"]
members = [ default-members = ["ffplayout-api", "ffplayout-engine"]
"ffplayout-api",
"ffplayout-engine",
"lib",
]
default-members = [
"ffplayout-api",
"ffplayout-engine",
]
[profile.release] [profile.release]
opt-level = 3 opt-level = 3

View File

@ -3,7 +3,7 @@ Description=Rest API for ffplayout
After=network.target remote-fs.target After=network.target remote-fs.target
[Service] [Service]
ExecStart=/usr/bin/ffpapi -l 127.0.0.1:8080 ExecStart=/usr/bin/ffpapi -l 127.0.0.1:8000
ExecReload=/bin/kill -1 $MAINPID ExecReload=/bin/kill -1 $MAINPID
Restart=always Restart=always
RestartSec=1 RestartSec=1

View File

@ -3,7 +3,7 @@ Description=Rust and ffmpeg based playout solution
After=network.target remote-fs.target After=network.target remote-fs.target
[Service] [Service]
ExecStart= /usr/bin/ffplayout ExecStart=/usr/bin/ffplayout
ExecReload=/bin/kill -1 $MAINPID ExecReload=/bin/kill -1 $MAINPID
Restart=always Restart=always
RestartSec=1 RestartSec=1

View File

@ -10,7 +10,7 @@ general:
rpc_server: rpc_server:
help_text: Run a JSON RPC server, for getting infos about current playing, and help_text: Run a JSON RPC server, for getting infos about current playing, and
control for some functions. control for some functions.
enable: false enable: true
address: 127.0.0.1:7070 address: 127.0.0.1:7070
authorization: av2Kx8g67lF9qj5wEH3ym1bI4cCs authorization: av2Kx8g67lF9qj5wEH3ym1bI4cCs
@ -108,7 +108,7 @@ text:
'text_from_filename' activate the extraction from text of a filename. With 'style' 'text_from_filename' activate the extraction from text of a filename. With 'style'
you can define the drawtext parameters like position, color, etc. Post Text over you can define the drawtext parameters like position, color, etc. Post Text over
API will override this. With 'regex' you can format file names, to get a title from it. API will override this. With 'regex' you can format file names, to get a title from it.
add_text: false add_text: true
text_from_filename: false text_from_filename: false
fontfile: "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf" fontfile: "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf"
style: "x=(w-tw)/2:y=(h-line_h)*0.9:fontsize=24:fontcolor=#ffffff:box=1:boxcolor=#000000:boxborderw=4" style: "x=(w-tw)/2:y=(h-line_h)*0.9:fontsize=24:fontcolor=#ffffff:box=1:boxcolor=#000000:boxborderw=4"

View File

@ -21,6 +21,9 @@ The different output modes.
Setup and use a preview stream. Setup and use a preview stream.
### **[Remove Sources](/docs/remote_source.md)** ### **[Remote Sources](/docs/remote_source.md)**
Use of remote sources, like https://example.org/video.mp4 Use of remote sources, like https://example.org/video.mp4
### **[ffplayout API](/docs/api.md)**
Control the engine, playlist and config with a ~REST API

View File

@ -1,175 +1,313 @@
#### Possible endpoints ### Possible endpoints
Run the API thru the systemd service, or like: Run the API thru the systemd service, or like:
```BASH ```BASH
ffpapi -l 127.0.0.1:8080 ffpapi -l 127.0.0.1:8000
``` ```
For all endpoints an (Bearer) authentication is required.\ For all endpoints an (Bearer) authentication is required.\
`{id}` represent the channel id, and at default is 1. `{id}` represent the channel id, and at default is 1.
#### Login is #### User Handling
**Login**
```BASH
curl -X POST http://127.0.0.1:8000/auth/login/ -H "Content-Type: application/json" \
-d '{ "username": "<USER>", "password": "<PASS>" }'
```
**Response:**
- **POST** `/auth/login/`\
JSON Data: `{"username": "<USER>", "password": "<PASS>"}`\
JSON Response:
```JSON ```JSON
{ {
"message": "login correct!", "id": 1,
"status": 200, "mail": "user@example.org",
"data": { "username": "<USER>",
"id": 1, "token": "<TOKEN>"
"email": "user@example.org",
"username": "user",
"token": "<TOKEN>"
}
} }
``` ```
From here on all request **must** contain the authorization header:\ From here on all request **must** contain the authorization header:\
`"Authorization: Bearer <TOKEN>"` `"Authorization: Bearer <TOKEN>"`
#### User **Get current User**
- **PUT** `/api/user/{user id}`\ ```BASH
JSON Data: `{"email": "<EMAIL>", "password": "<PASS>"}` curl -X GET 'http://localhost:8000/api/user' -H 'Content-Type: application/json' \
-H 'Authorization: Bearer <TOKEN>'
```
**Update current User**
```BASH
curl -X PUT http://localhost:8000/api/user/1 -H 'Content-Type: application/json' \
-d '{"mail": "<MAIL>", "password": "<PASS>"}' -H 'Authorization: <TOKEN>'
```
**Add User**
```BASH
curl -X POST 'http://localhost:8000/api/user/' -H 'Content-Type: application/json' \
-d '{"mail": "<MAIL>", "username": "<USER>", "password": "<PASS>", "role_id": 1, "channel_id": 1}' \
-H 'Authorization: Bearer <TOKEN>'
```
#### ffpapi Settings
**Get Settings**
```BASH
curl -X GET http://127.0.0.1:8000/api/settings/1 -H "Authorization: Bearer <TOKEN>"
```
**Response:**
- **POST** `/api/user/`\
JSON Data:
```JSON ```JSON
{ {
"email": "<EMAIL>", "id": 1,
"username": "<USER>", "channel_name": "Channel 1",
"password": "<PASS>", "preview_url": "http://localhost/live/preview.m3u8",
"role_id": 1 "config_path": "/etc/ffplayout/ffplayout.yml",
"extra_extensions": "jpg,jpeg,png",
"timezone": "UTC",
"service": "ffplayout.service"
} }
``` ```
#### API Settings **Get all Settings**
- **GET** `/api/settings/{id}`\ ```BASH
HEADER: curl -X GET http://127.0.0.1:8000/api/settings -H "Authorization: Bearer <TOKEN>"
Response is in JSON format
- **PATCH** `/api/settings/{id}`\
JSON Data:
```JSON
"id": 1,
"channel_name": "Channel 1",
"preview_url": "http://localhost/live/stream.m3u8",
"config_path": "/etc/ffplayout/ffplayout.yml",
"extra_extensions": ".jpg,.jpeg,.png"
``` ```
#### Playout Config **Update Settings**
- **GET** `/api/playout/config/{id}`\ ```BASH
Response is in JSON format curl -X PATCH http://127.0.0.1:8000/api/settings/1 -H "Content-Type: application/json" \
-d '{ "id": 1, "channel_name": "Channel 1", "preview_url": "http://localhost/live/stream.m3u8", \
"config_path": "/etc/ffplayout/ffplayout.yml", "extra_extensions": "jpg,jpeg,png",
"role_id": 1, "channel_id": 1 }' \
-H "Authorization: Bearer <TOKEN>"
```
- **PUT** `/api/playout/config/{id}`\ #### ffplayout Config
JSON Data: `{ <CONFIG DATA> }`\
Response is in TEXT format **Get Config**
```BASH
curl -X GET http://localhost:8000/api/playout/config/1 -H 'Authorization: <TOKEN>'
```
Response is a JSON object from the ffplayout.yml
**Update Config**
```BASH
curl -X PUT http://localhost:8000/api/playout/config/1 -H "Content-Type: application/json" \
-d { <CONFIG DATA> } -H 'Authorization: <TOKEN>'
```
#### Text Presets #### Text Presets
- **GET** `/api/presets/`\ Text presets are made for sending text messages to the ffplayout engine, to overlay them as a lower third.
Response is in JSON format
**Get all Presets**
```BASH
curl -X GET http://localhost:8000/api/presets/ -H 'Content-Type: application/json' \
-H 'Authorization: <TOKEN>'
```
**Update Preset**
```BASH
curl -X PUT http://localhost:8000/api/presets/1 -H 'Content-Type: application/json' \
-d '{"name": "<PRESET NAME>", "text": "<TEXT>", "x": "<X>", "y": "<Y>", "fontsize": 24, \
"line_spacing": 4, "fontcolor": "#ffffff", "box": 1, "boxcolor": "#000000", "boxborderw": 4, "alpha": 1.0}' \
-H 'Authorization: <TOKEN>'
```
**Ad new Preset**
```BASH
curl -X POST http://localhost:8000/api/presets/ -H 'Content-Type: application/json' \
-d '{"name": "<PRESET NAME>", "text": "TEXT>", "x": "<X>", "y": "<Y>", "fontsize": 24, \
"line_spacing": 4, "fontcolor": "#ffffff", "box": 1, "boxcolor": "#000000", "boxborderw": 4, "alpha": 1.0}}' \
-H 'Authorization: <TOKEN>'
```
### ffplayout controlling
here we communicate with the engine for:
- jump to last or next clip
- reset playlist state
- get infos about current, next, last clip
- send text to the engine, for overlaying it (as lower third etc.)
**Send Text to ffplayout**
```BASH
curl -X POST http://localhost:8000/api/control/1/text/ \
-H 'Content-Type: application/json' -H 'Authorization: <TOKEN>' \
-d '{"text": "Hello from ffplayout", "x": "(w-text_w)/2", "y": "(h-text_h)/2", \
"fontsize": "24", "line_spacing": "4", "fontcolor": "#ffffff", "box": "1", \
"boxcolor": "#000000", "boxborderw": "4", "alpha": "1.0"}'
```
**Jump to next Clip**
```BASH
curl -X POST http://localhost:8000/api/control/1/playout/next/ -H 'Authorization: <TOKEN>'
```
**Jump to last Clip**
```BASH
curl -X POST http://localhost:8000/api/control/1/playout/back/ -H 'Authorization: <TOKEN>'
```
**Reset ffplayout State**
When before was jumped to next, or last clips, here we go back to the original clip.
```BASH
curl -X POST http://localhost:8000/api/control/1/playout/reset/ -H 'Authorization: <TOKEN>'
```
**Get current Clip**
```BASH
curl -X GET http://localhost:8000/api/control/1/media/current/
-H 'Content-Type: application/json' -H 'Authorization: <TOKEN>'
```
**Response:**
- **PUT** `/api/playout/presets/{id}`\
JSON Data:
```JSON ```JSON
{ {
"name": "<PRESET NAME>", "jsonrpc": "2.0",
"text": "<TEXT>", "result": {
"x": "<X>", "current_media": {
"y": "<Y>", "category": "",
"fontsize": 24, "duration": 154.2,
"line_spacing": 4, "out": 154.2,
"fontcolor": "#ffffff", "seek": 0.0,
"box": 1, "source": "/opt/tv-media/clip.mp4"
"boxcolor": "#000000", },
"boxborderw": 4, "index": 39,
"alpha": "<alpha>" "play_mode": "playlist",
} "played_sec": 67.80771999300123,
"remaining_sec": 86.39228000699876,
``` "start_sec": 24713.631999999998,
Response is in TEXT format "start_time": "06:51:53.631"
},
- **POST** `/api/playout/presets/`\ "id": 1
JSON Data: `{ <PRESET DATA> }`\
Response is in TEXT format
#### Playout Process Control
- **POST** `/api/control/{id}/text/`¸
JSON Data:
```JSON
{
"text": "Hello from ffplayout",
"x": "(w-text_w)/2",
"y": "(h-text_h)/2",
"fontsize": "24",
"line_spacing": "4",
"fontcolor": "#ffffff",
"box": "1",
"boxcolor": "#000000",
"boxborderw": "4",
"alpha": "1.0"
} }
``` ```
Response is in TEXT format
- **POST** `api/control/{id}/playout/next/`\ **Get next Clip**
Response is in TEXT format
- **POST** `api/control/{id}/playout/back/`\ ```BASH
Response is in TEXT format curl -X GET http://localhost:8000/api/control/1/media/next/ -H 'Authorization: <TOKEN>'
```
- **POST** `api/control/{id}/playout/reset/`\ **Get last Clip**
Response is in TEXT format
- **GET** `/api/control/{id}/media/current`\ ```BASH
Response is in JSON format curl -X GET http://localhost:8000/api/control/1/media/last/
-H 'Content-Type: application/json' -H 'Authorization: <TOKEN>'
```
- **GET** `/api/control/{id}/media/next`\ #### ffplayout Process Control
Response is in JSON format
- **GET** `/api/control/{id}/media/last`\ Control ffplayout process, like:
Response is in JSON format - start
- stop
- restart
- status
- **POST** `/api/control/{id}/process/`\ ```BASH
JSON Data: `{"command": "<start/stop/restart/status>"}` curl -X POST http://localhost:8000/api/control/1/process/
Response is in TEXT format -H 'Content-Type: application/json' -H 'Authorization: <TOKEN>'
-d '{"command": "start"}'
```
#### Playlist Operations #### ffplayout Playlist Operations
- **GET** `/api/playlist/{id}/2022-06-20`\ **Get playlist**
Response is in JSON format
- **POST** `/api/playlist/1/`\ ```BASH
JSON Data: `{ <PLAYLIST DATA> }`\ curl -X GET http://localhost:8000/api/playlist/1?date=2022-06-20
Response is in TEXT format -H 'Content-Type: application/json' -H 'Authorization: <TOKEN>'
```
- **GET** `/api/playlist/{id}/generate/2022-06-20`\ **Save playlist**
Response is in JSON format
- **DELETE** `/api/playlist/{id}/2022-06-20`\ ```BASH
Response is in TEXT format curl -X POST http://localhost:8000/api/playlist/1/
-H 'Content-Type: application/json' -H 'Authorization: <TOKEN>'
-- data "{<JSON playlist data>}"
```
#### File Operations **Generate Playlist**
- **GET** `/api/file/{id}/browse/`\ A new playlist will be generated and response.
Response is in JSON format
- **POST** `/api/file/{id}/move/`\ ```BASH
JSON Data: `{"source": "<SOURCE>", "target": "<TARGET>"}`\ curl -X GET http://localhost:8000/api/playlist/1/generate/2022-06-20
Response is in JSON format -H 'Content-Type: application/json' -H 'Authorization: <TOKEN>'
```
- **DELETE** `/api/file/{id}/remove/`\ **Delete Playlist**
JSON Data: `{"source": "<SOURCE>"}`\
Response is in JSON format
- **POST** `/file/{id}/upload/`\ ```BASH
Multipart Form: `name=<TARGET PATH>, filename=<FILENAME>`\ curl -X DELETE http://localhost:8000/api/playlist/1/2022-06-20
Response is in TEXT format -H 'Content-Type: application/json' -H 'Authorization: <TOKEN>'
```
### Log file
**Read Log Life**
```BASH
curl -X Get http://localhost:8000/api/log/1
-H 'Content-Type: application/json' -H 'Authorization: <TOKEN>'
```
### File Operations
**Get File/Folder List**
```BASH
curl -X POST http://localhost:8000/api/file/1/browse/ -H 'Content-Type: application/json'
-d '{ "source": "/" }' -H 'Authorization: <TOKEN>'
```
**Create Folder**
```BASH
curl -X POST http://localhost:8000/api/file/1/create-folder/ -H 'Content-Type: application/json'
-d '{"source": "<FOLDER PATH>"}' -H 'Authorization: <TOKEN>'
```
**Rename File**
```BASH
curl -X POST http://localhost:8000/api/file/1/rename/ -H 'Content-Type: application/json'
-d '{"source": "<SOURCE>", "target": "<TARGET>"}' -H 'Authorization: <TOKEN>'
```
**Remove File/Folder**
```BASH
curl -X POST http://localhost:8000/api/file/1/remove/ -H 'Content-Type: application/json'
-d '{"source": "<SOURCE>"}' -H 'Authorization: <TOKEN>'
```
**Upload File**
```BASH
curl -X POST http://localhost:8000/api/file/1/upload/ -H 'Authorization: <TOKEN>'
-F "file=@file.mp4"
```

View File

@ -4,7 +4,7 @@ description = "Rest API for ffplayout"
license = "GPL-3.0" license = "GPL-3.0"
authors = ["Jonathan Baecker jonbae77@gmail.com"] authors = ["Jonathan Baecker jonbae77@gmail.com"]
readme = "README.md" readme = "README.md"
version = "0.3.2" version = "0.4.0"
edition = "2021" edition = "2021"
[dependencies] [dependencies]

View File

@ -12,7 +12,7 @@ ffpapi -i
Then add an admin user: Then add an admin user:
```BASH ```BASH
ffpapi -u <USERNAME> -p <PASSWORD> -e <EMAIL ADDRESS> ffpapi -u <USERNAME> -p <PASSWORD> -m <MAIL ADDRESS>
``` ```
Then run the API thru the systemd service, or like: Then run the API thru the systemd service, or like:

View File

@ -15,11 +15,11 @@ use utils::{
auth, db_path, init_config, auth, db_path, init_config,
models::LoginUser, models::LoginUser,
routes::{ routes::{
add_preset, add_user, del_playlist, file_browser, gen_playlist, get_playlist, add_dir, add_preset, add_user, del_playlist, file_browser, gen_playlist, get_all_settings,
get_playout_config, get_presets, get_settings, jump_to_last, jump_to_next, login, get_log, get_playlist, get_playout_config, get_presets, get_settings, get_user,
media_current, media_last, media_next, move_rename, patch_settings, process_control, control_playout, login, media_current, media_last, media_next, move_rename,
remove, reset_playout, save_file, save_playlist, send_text_message, update_playout_config, patch_settings, process_control, remove, save_file, save_playlist,
update_preset, update_user, send_text_message, update_playout_config, update_preset, update_user, delete_preset,
}, },
run_args, Role, run_args, Role,
}; };
@ -77,18 +77,19 @@ async fn main() -> std::io::Result<()> {
web::scope("/api") web::scope("/api")
.wrap(auth) .wrap(auth)
.service(add_user) .service(add_user)
.service(get_user)
.service(get_playout_config) .service(get_playout_config)
.service(update_playout_config) .service(update_playout_config)
.service(add_preset) .service(add_preset)
.service(get_presets) .service(get_presets)
.service(update_preset) .service(update_preset)
.service(delete_preset)
.service(get_settings) .service(get_settings)
.service(get_all_settings)
.service(patch_settings) .service(patch_settings)
.service(update_user) .service(update_user)
.service(send_text_message) .service(send_text_message)
.service(jump_to_next) .service(control_playout)
.service(jump_to_last)
.service(reset_playout)
.service(media_current) .service(media_current)
.service(media_next) .service(media_next)
.service(media_last) .service(media_last)
@ -97,7 +98,9 @@ async fn main() -> std::io::Result<()> {
.service(save_playlist) .service(save_playlist)
.service(gen_playlist) .service(gen_playlist)
.service(del_playlist) .service(del_playlist)
.service(get_log)
.service(file_browser) .service(file_browser)
.service(add_dir)
.service(move_rename) .service(move_rename)
.service(remove) .service(remove)
.service(save_file), .service(save_file),

View File

@ -17,8 +17,8 @@ pub struct Args {
#[clap(short, long, help = "Create admin user")] #[clap(short, long, help = "Create admin user")]
pub username: Option<String>, pub username: Option<String>,
#[clap(short, long, help = "Admin email")] #[clap(short, long, help = "Admin mail address")]
pub email: Option<String>, pub mail: Option<String>,
#[clap(short, long, help = "Admin password")] #[clap(short, long, help = "Admin password")]
pub password: Option<String>, pub password: Option<String>,

View File

@ -97,7 +97,7 @@ impl SystemD {
let output = Command::new("sudo").args(self.cmd).output()?; let output = Command::new("sudo").args(self.cmd).output()?;
Ok(String::from_utf8_lossy(&output.stdout).to_string()) Ok(String::from_utf8_lossy(&output.stdout).trim().to_string())
} }
} }

View File

@ -14,6 +14,9 @@ pub enum ServiceError {
#[display(fmt = "Unauthorized")] #[display(fmt = "Unauthorized")]
Unauthorized, Unauthorized,
#[display(fmt = "NoContent: {}", _0)]
NoContent(String),
} }
// impl ResponseError trait allows to convert our errors into http responses with appropriate data // impl ResponseError trait allows to convert our errors into http responses with appropriate data
@ -26,6 +29,7 @@ impl ResponseError for ServiceError {
ServiceError::BadRequest(ref message) => HttpResponse::BadRequest().json(message), ServiceError::BadRequest(ref message) => HttpResponse::BadRequest().json(message),
ServiceError::Conflict(ref message) => HttpResponse::Conflict().json(message), ServiceError::Conflict(ref message) => HttpResponse::Conflict().json(message),
ServiceError::Unauthorized => HttpResponse::Unauthorized().json("No Permission!"), ServiceError::Unauthorized => HttpResponse::Unauthorized().json("No Permission!"),
ServiceError::NoContent(ref message) => HttpResponse::NoContent().json(message),
} }
} }
} }

View File

@ -1,8 +1,4 @@
use std::{ use std::{fs, io::Write, path::PathBuf};
fs,
io::Write,
path::{Path, PathBuf},
};
use actix_multipart::Multipart; use actix_multipart::Multipart;
use actix_web::{web, HttpResponse}; use actix_web::{web, HttpResponse};
@ -14,19 +10,21 @@ use serde::{Deserialize, Serialize};
use simplelog::*; use simplelog::*;
use crate::utils::{errors::ServiceError, playout_config}; use crate::utils::{errors::ServiceError, playout_config};
use ffplayout_lib::utils::file_extension; use ffplayout_lib::utils::{file_extension, MediaProbe};
#[derive(Debug, Deserialize, Serialize, Clone)] #[derive(Debug, Deserialize, Serialize, Clone)]
pub struct PathObject { pub struct PathObject {
pub source: String, pub source: String,
parent: Option<String>,
folders: Option<Vec<String>>, folders: Option<Vec<String>>,
files: Option<Vec<String>>, files: Option<Vec<VideoFile>>,
} }
impl PathObject { impl PathObject {
fn new(source: String) -> Self { fn new(source: String, parent: Option<String>) -> Self {
Self { Self {
source, source,
parent,
folders: Some(vec![]), folders: Some(vec![]),
files: Some(vec![]), files: Some(vec![]),
} }
@ -39,54 +37,123 @@ pub struct MoveObject {
target: String, target: String,
} }
pub async fn browser(id: i64, path_obj: &PathObject) -> Result<PathObject, ServiceError> { #[derive(Debug, Deserialize, Serialize, Clone)]
let (config, _) = playout_config(&id).await?; pub struct VideoFile {
let path = PathBuf::from(config.storage.path); name: String,
let extensions = config.storage.extensions; duration: f64,
let path_component = RelativePath::new(&path_obj.source) }
/// Normalize absolut path
///
/// This function takes care, that it is not possible to break out from root_path.
/// It also gives alway a relative path back.
fn norm_abs_path(root_path: &String, input_path: &String) -> (PathBuf, String, String) {
let mut path = PathBuf::from(root_path.clone());
let path_relative = RelativePath::new(&root_path)
.normalize() .normalize()
.to_string() .to_string()
.replace("../", ""); .replace("../", "");
let path = path.join(path_component.clone()); let mut source_relative = RelativePath::new(input_path)
let mut obj = PathObject::new(path_component.clone()); .normalize()
.to_string()
.replace("../", "");
let path_suffix = path.file_name().unwrap().to_string_lossy().to_string();
if input_path.starts_with(root_path) || source_relative.starts_with(&path_relative) {
source_relative = source_relative
.strip_prefix(&path_relative)
.and_then(|s| s.strip_prefix('/'))
.unwrap_or_default()
.to_string();
} else {
source_relative = source_relative
.strip_prefix(&path_suffix)
.and_then(|s| s.strip_prefix('/'))
.unwrap_or(&source_relative)
.to_string();
}
path = path.join(&source_relative);
(path, path_suffix, source_relative)
}
/// File Browser
///
/// Take input path and give file and folder list from it back.
/// Input should be a relative path segment, but when it is a absolut path, the norm_abs_path function
/// will take care, that user can not break out from given storage path in config.
pub async fn browser(id: i64, path_obj: &PathObject) -> Result<PathObject, ServiceError> {
let (config, _) = playout_config(&id).await?;
let extensions = config.storage.extensions;
let (path, parent, path_component) = norm_abs_path(&config.storage.path, &path_obj.source);
let mut obj = PathObject::new(path_component, Some(parent));
let mut paths: Vec<_> = match fs::read_dir(path) { let mut paths: Vec<_> = match fs::read_dir(path) {
Ok(p) => p.filter_map(|r| r.ok()).collect(), Ok(p) => p.filter_map(|r| r.ok()).collect(),
Err(e) => { Err(e) => {
error!("{e} in {path_component}"); error!("{e} in {}", path_obj.source);
return Err(ServiceError::InternalServerError); return Err(ServiceError::InternalServerError);
} }
}; };
paths.sort_by_key(|dir| dir.path()); paths.sort_by_key(|dir| dir.path().display().to_string().to_lowercase());
let mut files = vec![];
let mut folders = vec![];
for path in paths { for path in paths {
let file_path = path.path().to_owned(); let file_path = path.path().to_owned();
let path_str = file_path.display().to_string(); let path = file_path.clone();
// ignore hidden files/folders on unix // ignore hidden files/folders on unix
if path_str.contains("/.") { if path.display().to_string().contains("/.") {
continue; continue;
} }
if file_path.is_dir() { if file_path.is_dir() {
if let Some(ref mut folders) = obj.folders { folders.push(path.file_name().unwrap().to_string_lossy().to_string());
folders.push(path_str);
}
} else if file_path.is_file() { } else if file_path.is_file() {
if let Some(ext) = file_extension(&file_path) { if let Some(ext) = file_extension(&file_path) {
if extensions.contains(&ext.to_string().to_lowercase()) { if extensions.contains(&ext.to_string().to_lowercase()) {
if let Some(ref mut files) = obj.files { let media = MediaProbe::new(&path.display().to_string());
files.push(path_str); let mut duration = 0.0;
if let Some(dur) = media.format.and_then(|f| f.duration) {
duration = dur.parse().unwrap_or(0.0)
} }
let video = VideoFile {
name: path.file_name().unwrap().to_string_lossy().to_string(),
duration,
};
files.push(video);
} }
} }
} }
} }
obj.folders = Some(folders);
obj.files = Some(files);
Ok(obj) Ok(obj)
} }
pub async fn create_directory(
id: i64,
path_obj: &PathObject,
) -> Result<HttpResponse, ServiceError> {
let (config, _) = playout_config(&id).await?;
let (path, _, _) = norm_abs_path(&config.storage.path, &path_obj.source);
if let Err(e) = fs::create_dir_all(&path) {
return Err(ServiceError::BadRequest(e.to_string()));
}
info!("create folder: <b><magenta>{}</></b>", path.display());
Ok(HttpResponse::Ok().into())
}
// fn copy_and_delete(source: &PathBuf, target: &PathBuf) -> Result<PathObject, ServiceError> { // fn copy_and_delete(source: &PathBuf, target: &PathBuf) -> Result<PathObject, ServiceError> {
// match fs::copy(&source, &target) { // match fs::copy(&source, &target) {
// Ok(_) => { // Ok(_) => {
@ -109,8 +176,16 @@ pub async fn browser(id: i64, path_obj: &PathObject) -> Result<PathObject, Servi
fn rename(source: &PathBuf, target: &PathBuf) -> Result<MoveObject, ServiceError> { fn rename(source: &PathBuf, target: &PathBuf) -> Result<MoveObject, ServiceError> {
match fs::rename(&source, &target) { match fs::rename(&source, &target) {
Ok(_) => Ok(MoveObject { Ok(_) => Ok(MoveObject {
source: source.display().to_string(), source: source
target: target.display().to_string(), .file_name()
.unwrap_or_default()
.to_string_lossy()
.to_string(),
target: target
.file_name()
.unwrap_or_default()
.to_string_lossy()
.to_string(),
}), }),
Err(e) => { Err(e) => {
error!("{e}"); error!("{e}");
@ -121,32 +196,8 @@ fn rename(source: &PathBuf, target: &PathBuf) -> Result<MoveObject, ServiceError
pub async fn rename_file(id: i64, move_object: &MoveObject) -> Result<MoveObject, ServiceError> { pub async fn rename_file(id: i64, move_object: &MoveObject) -> Result<MoveObject, ServiceError> {
let (config, _) = playout_config(&id).await?; let (config, _) = playout_config(&id).await?;
let path = PathBuf::from(&config.storage.path); let (source_path, _, _) = norm_abs_path(&config.storage.path, &move_object.source);
let source = RelativePath::new(&move_object.source) let (mut target_path, _, _) = norm_abs_path(&config.storage.path, &move_object.target);
.normalize()
.to_string()
.replace("../", "");
let target = RelativePath::new(&move_object.target)
.normalize()
.to_string()
.replace("../", "");
let mut source_path = PathBuf::from(source.clone());
let mut target_path = PathBuf::from(target.clone());
let relativ_path = RelativePath::new(&config.storage.path)
.normalize()
.to_string();
source_path = match source_path.starts_with(&relativ_path) {
true => path.join(source_path.strip_prefix(&relativ_path).unwrap()),
false => path.join(source),
};
target_path = match target_path.starts_with(&relativ_path) {
true => path.join(target_path.strip_prefix(relativ_path).unwrap()),
false => path.join(target),
};
if !source_path.exists() { if !source_path.exists() {
return Err(ServiceError::BadRequest("Source file not exist!".into())); return Err(ServiceError::BadRequest("Source file not exist!".into()));
@ -174,24 +225,9 @@ pub async fn rename_file(id: i64, move_object: &MoveObject) -> Result<MoveObject
Err(ServiceError::InternalServerError) Err(ServiceError::InternalServerError)
} }
pub async fn remove_file_or_folder(id: i64, source_path: &str) -> Result<(), ServiceError> { pub async fn remove_file_or_folder(id: i64, source_path: &String) -> Result<(), ServiceError> {
let (config, _) = playout_config(&id).await?; let (config, _) = playout_config(&id).await?;
let source = PathBuf::from(source_path); let (source, _, _) = norm_abs_path(&config.storage.path, source_path);
let test_source = RelativePath::new(&source_path)
.normalize()
.to_string()
.replace("../", "");
let test_path = RelativePath::new(&config.storage.path)
.normalize()
.to_string();
if !test_source.starts_with(&test_path) {
return Err(ServiceError::BadRequest(
"Source file is not in storage!".into(),
));
}
if !source.exists() { if !source.exists() {
return Err(ServiceError::BadRequest("Source does not exists!".into())); return Err(ServiceError::BadRequest("Source does not exists!".into()));
@ -222,32 +258,22 @@ pub async fn remove_file_or_folder(id: i64, source_path: &str) -> Result<(), Ser
Err(ServiceError::InternalServerError) Err(ServiceError::InternalServerError)
} }
async fn valid_path(id: i64, path: &str) -> Result<(), ServiceError> { async fn valid_path(id: i64, path: &String) -> Result<PathBuf, ServiceError> {
let (config, _) = playout_config(&id).await?; let (config, _) = playout_config(&id).await?;
let (test_path, _, _) = norm_abs_path(&config.storage.path, path);
let test_target = RelativePath::new(&path) if !test_path.is_dir() {
.normalize()
.to_string()
.replace("../", "");
let test_path = RelativePath::new(&config.storage.path)
.normalize()
.to_string();
if !test_target.starts_with(&test_path) {
return Err(ServiceError::BadRequest(
"Target folder is not in storage!".into(),
));
}
if !Path::new(path).is_dir() {
return Err(ServiceError::BadRequest("Target folder not exists!".into())); return Err(ServiceError::BadRequest("Target folder not exists!".into()));
} }
Ok(()) Ok(test_path)
} }
pub async fn upload(id: i64, mut payload: Multipart) -> Result<HttpResponse, ServiceError> { pub async fn upload(
id: i64,
mut payload: Multipart,
path: &String,
) -> Result<HttpResponse, ServiceError> {
while let Some(mut field) = payload.try_next().await? { while let Some(mut field) = payload.try_next().await? {
let content_disposition = field.content_disposition(); let content_disposition = field.content_disposition();
debug!("{content_disposition}"); debug!("{content_disposition}");
@ -256,16 +282,12 @@ pub async fn upload(id: i64, mut payload: Multipart) -> Result<HttpResponse, Ser
.take(20) .take(20)
.map(char::from) .map(char::from)
.collect(); .collect();
let path_name = content_disposition.get_name().unwrap_or(&rand_string);
let filename = content_disposition let filename = content_disposition
.get_filename() .get_filename()
.map_or_else(|| rand_string.to_string(), sanitize_filename::sanitize); .map_or_else(|| rand_string.to_string(), sanitize_filename::sanitize);
if let Err(e) = valid_path(id, path_name).await { let target_path = valid_path(id, path).await?;
return Err(e); let filepath = target_path.join(filename);
}
let filepath = PathBuf::from(path_name).join(filename);
if filepath.is_file() { if filepath.is_file() {
return Err(ServiceError::BadRequest("Target already exists!".into())); return Err(ServiceError::BadRequest("Target already exists!".into()));

View File

@ -23,52 +23,57 @@ async fn create_schema() -> Result<SqliteQueryResult, sqlx::Error> {
let query = "PRAGMA foreign_keys = ON; let query = "PRAGMA foreign_keys = ON;
CREATE TABLE IF NOT EXISTS global CREATE TABLE IF NOT EXISTS global
( (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
secret TEXT NOT NULL, secret TEXT NOT NULL,
UNIQUE(secret) UNIQUE(secret)
); );
CREATE TABLE IF NOT EXISTS roles CREATE TABLE IF NOT EXISTS roles
( (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL, name TEXT NOT NULL,
UNIQUE(name)
);
CREATE TABLE IF NOT EXISTS presets
(
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
text TEXT NOT NULL,
x TEXT NOT NULL,
y TEXT NOT NULL,
fontsize TEXT NOT NULL,
line_spacing TEXT NOT NULL,
fontcolor TEXT NOT NULL,
box TEXT NOT NULL,
boxcolor TEXT NOT NULL,
boxborderw TEXT NOT NULL,
alpha TEXT NOT NULL,
UNIQUE(name) UNIQUE(name)
); );
CREATE TABLE IF NOT EXISTS settings CREATE TABLE IF NOT EXISTS settings
( (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
channel_name TEXT NOT NULL, channel_name TEXT NOT NULL,
preview_url TEXT NOT NULL, preview_url TEXT NOT NULL,
config_path TEXT NOT NULL, config_path TEXT NOT NULL,
extra_extensions TEXT NOT NULL, extra_extensions TEXT NOT NULL,
service TEXT NOT NULL, timezone TEXT NOT NULL,
service TEXT NOT NULL,
UNIQUE(channel_name) UNIQUE(channel_name)
); );
CREATE TABLE IF NOT EXISTS presets
(
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
text TEXT NOT NULL,
x TEXT NOT NULL,
y TEXT NOT NULL,
fontsize TEXT NOT NULL,
line_spacing TEXT NOT NULL,
fontcolor TEXT NOT NULL,
box TEXT NOT NULL,
boxcolor TEXT NOT NULL,
boxborderw TEXT NOT NULL,
alpha TEXT NOT NULL,
channel_id INTEGER NOT NULL DEFAULT 1,
FOREIGN KEY (channel_id) REFERENCES settings (id) ON UPDATE SET NULL ON DELETE SET NULL,
UNIQUE(name)
);
CREATE TABLE IF NOT EXISTS user CREATE TABLE IF NOT EXISTS user
( (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
email TEXT NOT NULL, mail TEXT NOT NULL,
username TEXT NOT NULL, username TEXT NOT NULL,
password TEXT NOT NULL, password TEXT NOT NULL,
salt TEXT NOT NULL, salt TEXT NOT NULL,
role_id INTEGER NOT NULL DEFAULT 2, role_id INTEGER NOT NULL DEFAULT 2,
FOREIGN KEY (role_id) REFERENCES roles (id) ON UPDATE SET NULL ON DELETE SET NULL, channel_id INTEGER NOT NULL DEFAULT 1,
UNIQUE(email, username) FOREIGN KEY (role_id) REFERENCES roles (id) ON UPDATE SET NULL ON DELETE SET NULL,
FOREIGN KEY (channel_id) REFERENCES settings (id) ON UPDATE SET NULL ON DELETE SET NULL,
UNIQUE(mail, username)
);"; );";
let result = sqlx::query(query).execute(&conn).await; let result = sqlx::query(query).execute(&conn).await;
conn.close().await; conn.close().await;
@ -98,20 +103,20 @@ pub async fn db_init() -> Result<&'static str, Box<dyn std::error::Error>> {
BEFORE INSERT ON global BEFORE INSERT ON global
WHEN (SELECT COUNT(*) FROM global) >= 1 WHEN (SELECT COUNT(*) FROM global) >= 1
BEGIN BEGIN
SELECT RAISE(FAIL, 'Database is already init!'); SELECT RAISE(FAIL, 'Database is already initialized!');
END; END;
INSERT INTO global(secret) VALUES($1); INSERT INTO global(secret) VALUES($1);
INSERT INTO presets(name, text, x, y, fontsize, line_spacing, fontcolor, alpha, box, boxcolor, boxborderw) INSERT INTO settings(channel_name, preview_url, config_path, extra_extensions, timezone, service)
VALUES('Default', 'Wellcome to ffplayout messenger!', '(w-text_w)/2', '(h-text_h)/2', '24', '4', '#ffffff@0xff', '1.0', '0', '#000000@0x80', '4'),
('Empty Text', '', '0', '0', '24', '4', '#000000', '0', '0', '#000000', '0'),
('Bottom Text fade in', 'The upcoming event will be delayed by a few minutes.', '(w-text_w)/2', '(h-line_h)*0.9', '24', '4', '#ffffff',
'ifnot(ld(1),st(1,t));if(lt(t,ld(1)+1),0,if(lt(t,ld(1)+2),(t-(ld(1)+1))/1,if(lt(t,ld(1)+8),1,if(lt(t,ld(1)+9),(1-(t-(ld(1)+8)))/1,0))))', '1', '#000000@0x80', '4'),
('Scrolling Text', 'We have a very important announcement to make.', 'ifnot(ld(1),st(1,t));if(lt(t,ld(1)+1),w+4,w-w/12*mod(t-ld(1),12*(w+tw)/w))', '(h-line_h)*0.9',
'24', '4', '#ffffff', '1.0', '1', '#000000@0x80', '4');
INSERT INTO roles(name) VALUES('admin'), ('user'), ('guest');
INSERT INTO settings(channel_name, preview_url, config_path, extra_extensions, service)
VALUES('Channel 1', 'http://localhost/live/preview.m3u8', VALUES('Channel 1', 'http://localhost/live/preview.m3u8',
'/etc/ffplayout/ffplayout.yml', '.jpg,.jpeg,.png', 'ffplayout.service');"; '/etc/ffplayout/ffplayout.yml', 'jpg,jpeg,png', 'UTC', 'ffplayout.service');
INSERT INTO roles(name) VALUES('admin'), ('user'), ('guest');
INSERT INTO presets(name, text, x, y, fontsize, line_spacing, fontcolor, box, boxcolor, boxborderw, alpha, channel_id)
VALUES('Default', 'Wellcome to ffplayout messenger!', '(w-text_w)/2', '(h-text_h)/2', '24', '4', '#ffffff@0xff', '0', '#000000@0x80', '4', '1.0', '1'),
('Empty Text', '', '0', '0', '24', '4', '#000000', '0', '#000000', '0', '0', '1'),
('Bottom Text fade in', 'The upcoming event will be delayed by a few minutes.', '(w-text_w)/2', '(h-line_h)*0.9', '24', '4', '#ffffff',
'1', '#000000@0x80', '4', 'ifnot(ld(1),st(1,t));if(lt(t,ld(1)+1),0,if(lt(t,ld(1)+2),(t-(ld(1)+1))/1,if(lt(t,ld(1)+8),1,if(lt(t,ld(1)+9),(1-(t-(ld(1)+8)))/1,0))))', '1'),
('Scrolling Text', 'We have a very important announcement to make.', 'ifnot(ld(1),st(1,t));if(lt(t,ld(1)+1),w+4,w-w/12*mod(t-ld(1),12*(w+tw)/w))', '(h-line_h)*0.9',
'24', '4', '#ffffff', '1', '#000000@0x80', '4', '1.0', '1');";
sqlx::query(query).bind(secret).execute(&instances).await?; sqlx::query(query).bind(secret).execute(&instances).await?;
instances.close().await; instances.close().await;
@ -143,6 +148,15 @@ pub async fn db_get_settings(id: &i64) -> Result<Settings, sqlx::Error> {
Ok(result) Ok(result)
} }
pub async fn db_get_all_settings() -> Result<Vec<Settings>, sqlx::Error> {
let conn = db_connection().await?;
let query = "SELECT * FROM settings";
let result: Vec<Settings> = sqlx::query_as(query).fetch_all(&conn).await?;
conn.close().await;
Ok(result)
}
pub async fn db_update_settings( pub async fn db_update_settings(
id: i64, id: i64,
settings: Settings, settings: Settings,
@ -174,7 +188,16 @@ pub async fn db_role(id: &i64) -> Result<String, sqlx::Error> {
pub async fn db_login(user: &str) -> Result<User, sqlx::Error> { pub async fn db_login(user: &str) -> Result<User, sqlx::Error> {
let conn = db_connection().await?; let conn = db_connection().await?;
let query = "SELECT id, email, username, password, salt, role_id FROM user WHERE username = $1"; let query = "SELECT id, mail, username, password, salt, role_id FROM user WHERE username = $1";
let result: User = sqlx::query_as(query).bind(user).fetch_one(&conn).await?;
conn.close().await;
Ok(result)
}
pub async fn db_get_user(user: &str) -> Result<User, sqlx::Error> {
let conn = db_connection().await?;
let query = "SELECT id, mail, username, role_id FROM user WHERE username = $1";
let result: User = sqlx::query_as(query).bind(user).fetch_one(&conn).await?; let result: User = sqlx::query_as(query).bind(user).fetch_one(&conn).await?;
conn.close().await; conn.close().await;
@ -189,9 +212,9 @@ pub async fn db_add_user(user: User) -> Result<SqliteQueryResult, sqlx::Error> {
.unwrap(); .unwrap();
let query = let query =
"INSERT INTO user (email, username, password, salt, role_id) VALUES($1, $2, $3, $4, $5)"; "INSERT INTO user (mail, username, password, salt, role_id) VALUES($1, $2, $3, $4, $5)";
let result = sqlx::query(query) let result = sqlx::query(query)
.bind(user.email) .bind(user.mail)
.bind(user.username) .bind(user.username)
.bind(password_hash.to_string()) .bind(password_hash.to_string())
.bind(salt.to_string()) .bind(salt.to_string())
@ -212,10 +235,10 @@ pub async fn db_update_user(id: i64, fields: String) -> Result<SqliteQueryResult
Ok(result) Ok(result)
} }
pub async fn db_get_presets() -> Result<Vec<TextPreset>, sqlx::Error> { pub async fn db_get_presets(id: i64) -> Result<Vec<TextPreset>, sqlx::Error> {
let conn = db_connection().await?; let conn = db_connection().await?;
let query = "SELECT * FROM presets"; let query = "SELECT * FROM presets WHERE channel_id = $1";
let result: Vec<TextPreset> = sqlx::query_as(query).fetch_all(&conn).await?; let result: Vec<TextPreset> = sqlx::query_as(query).bind(id).fetch_all(&conn).await?;
conn.close().await; conn.close().await;
Ok(result) Ok(result)
@ -252,9 +275,10 @@ pub async fn db_update_preset(
pub async fn db_add_preset(preset: TextPreset) -> Result<SqliteQueryResult, sqlx::Error> { pub async fn db_add_preset(preset: TextPreset) -> Result<SqliteQueryResult, sqlx::Error> {
let conn = db_connection().await?; let conn = db_connection().await?;
let query = let query =
"INSERT INTO presets (name, text, x, y, fontsize, line_spacing, fontcolor, alpha, box, boxcolor, boxborderw) "INSERT INTO presets (channel_id, name, text, x, y, fontsize, line_spacing, fontcolor, alpha, box, boxcolor, boxborderw)
VALUES($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)"; VALUES($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)";
let result: SqliteQueryResult = sqlx::query(query) let result: SqliteQueryResult = sqlx::query(query)
.bind(preset.channel_id)
.bind(preset.name) .bind(preset.name)
.bind(preset.text) .bind(preset.text)
.bind(preset.x) .bind(preset.x)
@ -272,3 +296,12 @@ pub async fn db_add_preset(preset: TextPreset) -> Result<SqliteQueryResult, sqlx
Ok(result) Ok(result)
} }
pub async fn db_delete_preset(id: &i64) -> Result<SqliteQueryResult, sqlx::Error> {
let conn = db_connection().await?;
let query = "DELETE FROM presets WHERE id = $1;";
let result: SqliteQueryResult = sqlx::query(query).bind(id).execute(&conn).await?;
conn.close().await;
Ok(result)
}

View File

@ -1,6 +1,6 @@
use std::{ use std::{
error::Error, error::Error,
fs::File, fs::{self, File},
io::{stdin, stdout, Write}, io::{stdin, stdout, Write},
path::Path, path::Path,
}; };
@ -129,36 +129,37 @@ pub async fn run_args(mut args: Args) -> Result<(), i32> {
args.password = password.ok(); args.password = password.ok();
let mut email = String::new(); let mut mail = String::new();
print!("EMail: "); print!("Mail: ");
stdout().flush().unwrap(); stdout().flush().unwrap();
stdin() stdin()
.read_line(&mut email) .read_line(&mut mail)
.expect("Did not enter a correct name?"); .expect("Did not enter a correct name?");
if let Some('\n') = email.chars().next_back() { if let Some('\n') = mail.chars().next_back() {
email.pop(); mail.pop();
} }
if let Some('\r') = email.chars().next_back() { if let Some('\r') = mail.chars().next_back() {
email.pop(); mail.pop();
} }
args.email = Some(email); args.mail = Some(mail);
} }
if let Some(username) = args.username { if let Some(username) = args.username {
if args.email.is_none() || args.password.is_none() { if args.mail.is_none() || args.password.is_none() {
error!("Email/password missing!"); error!("Mail/password missing!");
return Err(1); return Err(1);
} }
let user = User { let user = User {
id: 0, id: 0,
email: Some(args.email.unwrap()), mail: Some(args.mail.unwrap()),
username: username.clone(), username: username.clone(),
password: args.password.unwrap(), password: args.password.unwrap(),
salt: None, salt: None,
role_id: Some(1), role_id: Some(1),
channel_id: Some(1),
token: None, token: None,
}; };
@ -193,3 +194,30 @@ pub async fn playout_config(channel_id: &i64) -> Result<(PlayoutConfig, Settings
"Error in getting config!".to_string(), "Error in getting config!".to_string(),
)) ))
} }
pub async fn read_log_file(channel_id: &i64, date: &str) -> Result<String, ServiceError> {
if let Ok(settings) = db_get_settings(channel_id).await {
let mut date_str = "".to_string();
if !date.is_empty() {
date_str.push('.');
date_str.push_str(date);
}
if let Ok(config) = read_playout_config(&settings.config_path) {
let mut log_path = Path::new(&config.logging.log_path)
.join("ffplayout.log")
.display()
.to_string();
log_path.push_str(&date_str);
let file = fs::read_to_string(log_path)?;
return Ok(file);
}
}
Err(ServiceError::BadRequest(
"Requested log file not exists, or not readable.".to_string(),
))
}

View File

@ -6,7 +6,7 @@ pub struct User {
#[serde(skip_deserializing)] #[serde(skip_deserializing)]
pub id: i64, pub id: i64,
#[sqlx(default)] #[sqlx(default)]
pub email: Option<String>, pub mail: Option<String>,
pub username: String, pub username: String,
#[sqlx(default)] #[sqlx(default)]
#[serde(skip_serializing, default = "empty_string")] #[serde(skip_serializing, default = "empty_string")]
@ -18,6 +18,9 @@ pub struct User {
#[serde(skip_serializing)] #[serde(skip_serializing)]
pub role_id: Option<i64>, pub role_id: Option<i64>,
#[sqlx(default)] #[sqlx(default)]
#[serde(skip_serializing)]
pub channel_id: Option<i64>,
#[sqlx(default)]
pub token: Option<String>, pub token: Option<String>,
} }
@ -41,7 +44,7 @@ pub struct TextPreset {
#[sqlx(default)] #[sqlx(default)]
#[serde(skip_deserializing)] #[serde(skip_deserializing)]
pub id: i64, pub id: i64,
#[serde(skip_deserializing)] pub channel_id: i64,
pub name: String, pub name: String,
pub text: String, pub text: String,
pub x: String, pub x: String,
@ -63,6 +66,7 @@ pub struct Settings {
pub preview_url: String, pub preview_url: String,
pub config_path: String, pub config_path: String,
pub extra_extensions: String, pub extra_extensions: String,
pub timezone: String,
#[sqlx(default)] #[sqlx(default)]
#[serde(skip_serializing, skip_deserializing)] #[serde(skip_serializing, skip_deserializing)]
pub secret: String, pub secret: String,

View File

@ -37,11 +37,10 @@ pub async fn read_playlist(id: i64, date: String) -> Result<JsonPlaylist, Servic
.join(date.clone()) .join(date.clone())
.with_extension("json"); .with_extension("json");
if let Ok(p) = json_reader(&playlist_path) { match json_reader(&playlist_path) {
return Ok(p); Ok(p) => Ok(p),
}; Err(e) => Err(ServiceError::NoContent(e.to_string())),
}
Err(ServiceError::InternalServerError)
} }
pub async fn write_playlist(id: i64, json_data: JsonPlaylist) -> Result<String, ServiceError> { pub async fn write_playlist(id: i64, json_data: JsonPlaylist) -> Result<String, ServiceError> {
@ -76,9 +75,10 @@ pub async fn write_playlist(id: i64, json_data: JsonPlaylist) -> Result<String,
} }
pub async fn generate_playlist(id: i64, date: String) -> Result<JsonPlaylist, ServiceError> { pub async fn generate_playlist(id: i64, date: String) -> Result<JsonPlaylist, ServiceError> {
let (config, settings) = playout_config(&id).await?; let (mut config, settings) = playout_config(&id).await?;
config.general.generate = Some(vec![date.clone()]);
match playlist_generator(&config, vec![date], Some(settings.channel_name)) { match playlist_generator(&config, Some(settings.channel_name)) {
Ok(playlists) => { Ok(playlists) => {
if !playlists.is_empty() { if !playlists.is_empty() {
Ok(playlists[0].clone()) Ok(playlists[0].clone())

View File

@ -1,3 +1,14 @@
/// ### Possible endpoints
///
/// Run the API thru the systemd service, or like:
///
/// ```BASH
/// ffpapi -l 127.0.0.1:8000
/// ```
///
/// For all endpoints an (Bearer) authentication is required.\
/// `{id}` represent the channel id, and at default is 1.
use std::collections::HashMap; use std::collections::HashMap;
use actix_multipart::Multipart; use actix_multipart::Multipart;
@ -7,21 +18,25 @@ use argon2::{
password_hash::{rand_core::OsRng, PasswordHash, SaltString}, password_hash::{rand_core::OsRng, PasswordHash, SaltString},
Argon2, PasswordHasher, PasswordVerifier, Argon2, PasswordHasher, PasswordVerifier,
}; };
use serde::Serialize; use serde::{Deserialize, Serialize};
use simplelog::*; use simplelog::*;
use crate::utils::{ use crate::utils::{
auth::{create_jwt, Claims}, auth::{create_jwt, Claims},
control::{control_service, control_state, media_info, send_message, Process}, control::{control_service, control_state, media_info, send_message, Process},
errors::ServiceError, errors::ServiceError,
files::{browser, remove_file_or_folder, rename_file, upload, MoveObject, PathObject}, files::{
browser, create_directory, remove_file_or_folder, rename_file, upload, MoveObject,
PathObject,
},
handles::{ handles::{
db_add_preset, db_add_user, db_get_presets, db_get_settings, db_login, db_role, db_add_preset, db_add_user, db_get_all_settings, db_get_presets, db_get_settings,
db_update_preset, db_update_settings, db_update_user, db_get_user, db_login, db_role, db_update_preset, db_update_settings, db_update_user,
db_delete_preset,
}, },
models::{LoginUser, Settings, TextPreset, User}, models::{LoginUser, Settings, TextPreset, User},
playlist::{delete_playlist, generate_playlist, read_playlist, write_playlist}, playlist::{delete_playlist, generate_playlist, read_playlist, write_playlist},
read_playout_config, Role, read_log_file, read_playout_config, Role,
}; };
use ffplayout_lib::utils::{JsonPlaylist, PlayoutConfig}; use ffplayout_lib::utils::{JsonPlaylist, PlayoutConfig};
@ -32,8 +47,42 @@ struct ResponseObj<T> {
data: Option<T>, data: Option<T>,
} }
/// curl -X POST http://127.0.0.1:8080/auth/login/ -H "Content-Type: application/json" \ #[derive(Serialize)]
/// -d '{"username": "<USER>", "password": "<PASS>" }' struct UserObj<T> {
message: String,
user: Option<T>,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct DateObj {
#[serde(default)]
date: String,
}
#[derive(Debug, Deserialize, Serialize)]
pub struct FileObj {
#[serde(default)]
path: String,
}
/// #### User Handling
///
/// **Login**
///
/// ```BASH
/// curl -X POST http://127.0.0.1:8000/auth/login/ -H "Content-Type: application/json" \
/// -d '{ "username": "<USER>", "password": "<PASS>" }'
/// ```
/// **Response:**
///
/// ```JSON
/// {
/// "id": 1,
/// "mail": "user@example.org",
/// "username": "<USER>",
/// "token": "<TOKEN>"
/// }
/// ```
#[post("/auth/login/")] #[post("/auth/login/")]
pub async fn login(credentials: web::Json<User>) -> impl Responder { pub async fn login(credentials: web::Json<User>) -> impl Responder {
match db_login(&credentials.username).await { match db_login(&credentials.username).await {
@ -58,19 +107,17 @@ pub async fn login(credentials: web::Json<User>) -> impl Responder {
info!("user {} login, with role: {role}", credentials.username); info!("user {} login, with role: {role}", credentials.username);
web::Json(ResponseObj { web::Json(UserObj {
message: "login correct!".into(), message: "login correct!".into(),
status: 200, user: Some(user),
data: Some(user),
}) })
.customize() .customize()
.with_status(StatusCode::OK) .with_status(StatusCode::OK)
} else { } else {
error!("Wrong password for {}!", credentials.username); error!("Wrong password for {}!", credentials.username);
web::Json(ResponseObj { web::Json(UserObj {
message: "Wrong password!".into(), message: "Wrong password!".into(),
status: 403, user: None,
data: None,
}) })
.customize() .customize()
.with_status(StatusCode::FORBIDDEN) .with_status(StatusCode::FORBIDDEN)
@ -78,10 +125,9 @@ pub async fn login(credentials: web::Json<User>) -> impl Responder {
} }
Err(e) => { Err(e) => {
error!("Login {} failed! {e}", credentials.username); error!("Login {} failed! {e}", credentials.username);
return web::Json(ResponseObj { return web::Json(UserObj {
message: format!("Login {} failed!", credentials.username), message: format!("Login {} failed!", credentials.username),
status: 400, user: None,
data: None,
}) })
.customize() .customize()
.with_status(StatusCode::BAD_REQUEST); .with_status(StatusCode::BAD_REQUEST);
@ -89,8 +135,33 @@ pub async fn login(credentials: web::Json<User>) -> impl Responder {
} }
} }
/// curl -X PUT http://localhost:8080/api/user/1 --header 'Content-Type: application/json' \ /// From here on all request **must** contain the authorization header:\
/// --data '{"email": "<EMAIL>", "password": "<PASS>"}' --header 'Authorization: <TOKEN>' /// `"Authorization: Bearer <TOKEN>"`
/// **Get current User**
///
/// ```BASH
/// curl -X GET 'http://localhost:8000/api/user' -H 'Content-Type: application/json' \
/// -H 'Authorization: Bearer <TOKEN>'
/// ```
#[get("/user")]
#[has_any_role("Role::Admin", "Role::User", type = "Role")]
async fn get_user(user: web::ReqData<LoginUser>) -> Result<impl Responder, ServiceError> {
match db_get_user(&user.username).await {
Ok(user) => Ok(web::Json(user)),
Err(e) => {
error!("{e}");
Err(ServiceError::InternalServerError)
}
}
}
/// **Update current User**
///
/// ```BASH
/// curl -X PUT http://localhost:8000/api/user/1 -H 'Content-Type: application/json' \
/// -d '{"mail": "<MAIL>", "password": "<PASS>"}' -H 'Authorization: <TOKEN>'
/// ```
#[put("/user/{id}")] #[put("/user/{id}")]
#[has_any_role("Role::Admin", "Role::User", type = "Role")] #[has_any_role("Role::Admin", "Role::User", type = "Role")]
async fn update_user( async fn update_user(
@ -101,8 +172,8 @@ async fn update_user(
if id.into_inner() == user.id { if id.into_inner() == user.id {
let mut fields = String::new(); let mut fields = String::new();
if let Some(email) = data.email.clone() { if let Some(mail) = data.mail.clone() {
fields.push_str(format!("email = '{email}'").as_str()); fields.push_str(format!("mail = '{mail}'").as_str());
} }
if !data.password.is_empty() { if !data.password.is_empty() {
@ -128,9 +199,13 @@ async fn update_user(
Err(ServiceError::Unauthorized) Err(ServiceError::Unauthorized)
} }
/// curl -X POST 'http://localhost:8080/api/user/' --header 'Content-Type: application/json' \ /// **Add User**
/// -d '{"email": "<EMAIL>", "username": "<USER>", "password": "<PASS>", "role_id": 1}' \ ///
/// --header 'Authorization: Bearer <TOKEN>' /// ```BASH
/// curl -X POST 'http://localhost:8000/api/user/' -H 'Content-Type: application/json' \
/// -d '{"mail": "<MAIL>", "username": "<USER>", "password": "<PASS>", "role_id": 1, "channel_id": 1}' \
/// -H 'Authorization: Bearer <TOKEN>'
/// ```
#[post("/user/")] #[post("/user/")]
#[has_any_role("Role::Admin", type = "Role")] #[has_any_role("Role::Admin", type = "Role")]
async fn add_user(data: web::Json<User>) -> Result<impl Responder, ServiceError> { async fn add_user(data: web::Json<User>) -> Result<impl Responder, ServiceError> {
@ -143,25 +218,61 @@ async fn add_user(data: web::Json<User>) -> Result<impl Responder, ServiceError>
} }
} }
/// curl -X GET http://127.0.0.1:8080/api/settings/1 -H "Authorization: Bearer <TOKEN>" /// #### ffpapi Settings
///
/// **Get Settings**
///
/// ```BASH
/// curl -X GET http://127.0.0.1:8000/api/settings/1 -H "Authorization: Bearer <TOKEN>"
/// ```
///
/// **Response:**
///
/// ```JSON
/// {
/// "id": 1,
/// "channel_name": "Channel 1",
/// "preview_url": "http://localhost/live/preview.m3u8",
/// "config_path": "/etc/ffplayout/ffplayout.yml",
/// "extra_extensions": "jpg,jpeg,png",
/// "timezone": "UTC",
/// "service": "ffplayout.service"
/// }
/// ```
#[get("/settings/{id}")] #[get("/settings/{id}")]
#[has_any_role("Role::Admin", "Role::User", type = "Role")] #[has_any_role("Role::Admin", "Role::User", type = "Role")]
async fn get_settings(id: web::Path<i64>) -> Result<impl Responder, ServiceError> { async fn get_settings(id: web::Path<i64>) -> Result<impl Responder, ServiceError> {
if let Ok(settings) = db_get_settings(&id).await { if let Ok(settings) = db_get_settings(&id).await {
return Ok(web::Json(ResponseObj { return Ok(web::Json(settings));
message: format!("Settings from {}", settings.channel_name),
status: 200,
data: Some(settings),
}));
} }
Err(ServiceError::InternalServerError) Err(ServiceError::InternalServerError)
} }
/// curl -X PATCH http://127.0.0.1:8080/api/settings/1 -H "Content-Type: application/json" \ /// **Get all Settings**
/// --data '{"id":1,"channel_name":"Channel 1","preview_url":"http://localhost/live/stream.m3u8", \ ///
/// "config_path":"/etc/ffplayout/ffplayout.yml","extra_extensions":".jpg,.jpeg,.png"}' \ /// ```BASH
/// curl -X GET http://127.0.0.1:8000/api/settings -H "Authorization: Bearer <TOKEN>"
/// ```
#[get("/settings")]
#[has_any_role("Role::Admin", type = "Role")]
async fn get_all_settings() -> Result<impl Responder, ServiceError> {
if let Ok(settings) = db_get_all_settings().await {
return Ok(web::Json(settings));
}
Err(ServiceError::InternalServerError)
}
/// **Update Settings**
///
/// ```BASH
/// curl -X PATCH http://127.0.0.1:8000/api/settings/1 -H "Content-Type: application/json" \
/// -d '{ "id": 1, "channel_name": "Channel 1", "preview_url": "http://localhost/live/stream.m3u8", \
/// "config_path": "/etc/ffplayout/ffplayout.yml", "extra_extensions": "jpg,jpeg,png",
/// "role_id": 1, "channel_id": 1 }' \
/// -H "Authorization: Bearer <TOKEN>" /// -H "Authorization: Bearer <TOKEN>"
/// ```
#[patch("/settings/{id}")] #[patch("/settings/{id}")]
#[has_any_role("Role::Admin", type = "Role")] #[has_any_role("Role::Admin", type = "Role")]
async fn patch_settings( async fn patch_settings(
@ -175,7 +286,15 @@ async fn patch_settings(
Err(ServiceError::InternalServerError) Err(ServiceError::InternalServerError)
} }
/// curl -X GET http://localhost:8080/api/playout/config/1 --header 'Authorization: <TOKEN>' /// #### ffplayout Config
///
/// **Get Config**
///
/// ```BASH
/// curl -X GET http://localhost:8000/api/playout/config/1 -H 'Authorization: <TOKEN>'
/// ```
///
/// Response is a JSON object from the ffplayout.yml
#[get("/playout/config/{id}")] #[get("/playout/config/{id}")]
#[has_any_role("Role::Admin", "Role::User", type = "Role")] #[has_any_role("Role::Admin", "Role::User", type = "Role")]
async fn get_playout_config( async fn get_playout_config(
@ -191,8 +310,12 @@ async fn get_playout_config(
Err(ServiceError::InternalServerError) Err(ServiceError::InternalServerError)
} }
/// curl -X PUT http://localhost:8080/api/playout/config/1 -H "Content-Type: application/json" \ /// **Update Config**
/// --data { <CONFIG DATA> } --header 'Authorization: <TOKEN>' ///
/// ```BASH
/// curl -X PUT http://localhost:8000/api/playout/config/1 -H "Content-Type: application/json" \
/// -d { <CONFIG DATA> } -H 'Authorization: <TOKEN>'
/// ```
#[put("/playout/config/{id}")] #[put("/playout/config/{id}")]
#[has_any_role("Role::Admin", type = "Role")] #[has_any_role("Role::Admin", type = "Role")]
async fn update_playout_config( async fn update_playout_config(
@ -216,22 +339,34 @@ async fn update_playout_config(
Err(ServiceError::InternalServerError) Err(ServiceError::InternalServerError)
} }
/// curl -X GET http://localhost:8080/api/presets/ --header 'Content-Type: application/json' \ /// #### Text Presets
/// --data '{"email": "<EMAIL>", "password": "<PASS>"}' --header 'Authorization: <TOKEN>' ///
#[get("/presets/")] /// Text presets are made for sending text messages to the ffplayout engine, to overlay them as a lower third.
///
/// **Get all Presets**
///
/// ```BASH
/// curl -X GET http://localhost:8000/api/presets/ -H 'Content-Type: application/json' \
/// -H 'Authorization: <TOKEN>'
/// ```
#[get("/presets/{id}")]
#[has_any_role("Role::Admin", "Role::User", type = "Role")] #[has_any_role("Role::Admin", "Role::User", type = "Role")]
async fn get_presets() -> Result<impl Responder, ServiceError> { async fn get_presets(id: web::Path<i64>) -> Result<impl Responder, ServiceError> {
if let Ok(presets) = db_get_presets().await { if let Ok(presets) = db_get_presets(*id).await {
return Ok(web::Json(presets)); return Ok(web::Json(presets));
} }
Err(ServiceError::InternalServerError) Err(ServiceError::InternalServerError)
} }
/// curl -X PUT http://localhost:8080/api/presets/1 --header 'Content-Type: application/json' \ /// **Update Preset**
/// --data '{"name": "<PRESET NAME>", "text": "<TEXT>", "x": "<X>", "y": "<Y>", "fontsize": 24, \ ///
/// "line_spacing": 4, "fontcolor": "#ffffff", "box": 1, "boxcolor": "#000000", "boxborderw": 4, "alpha": 1.0}' \ /// ```BASH
/// --header 'Authorization: <TOKEN>' /// curl -X PUT http://localhost:8000/api/presets/1 -H 'Content-Type: application/json' \
/// -d '{ "name": "<PRESET NAME>", "text": "<TEXT>", "x": "<X>", "y": "<Y>", "fontsize": 24, \
/// "line_spacing": 4, "fontcolor": "#ffffff", "box": 1, "boxcolor": "#000000", "boxborderw": 4, "alpha": 1.0, "channel_id": 1 }' \
/// -H 'Authorization: <TOKEN>'
/// ```
#[put("/presets/{id}")] #[put("/presets/{id}")]
#[has_any_role("Role::Admin", "Role::User", type = "Role")] #[has_any_role("Role::Admin", "Role::User", type = "Role")]
async fn update_preset( async fn update_preset(
@ -245,10 +380,14 @@ async fn update_preset(
Err(ServiceError::InternalServerError) Err(ServiceError::InternalServerError)
} }
/// curl -X POST http://localhost:8080/api/presets/ --header 'Content-Type: application/json' \ /// **Add new Preset**
/// --data '{"name": "<PRESET NAME>", "text": "TEXT>", "x": "<X>", "y": "<Y>", "fontsize": 24, \ ///
/// "line_spacing": 4, "fontcolor": "#ffffff", "box": 1, "boxcolor": "#000000", "boxborderw": 4, "alpha": 1.0}}' \ /// ```BASH
/// --header 'Authorization: <TOKEN>' /// curl -X POST http://localhost:8000/api/presets/ -H 'Content-Type: application/json' \
/// -d '{ "name": "<PRESET NAME>", "text": "TEXT>", "x": "<X>", "y": "<Y>", "fontsize": 24, \
/// "line_spacing": 4, "fontcolor": "#ffffff", "box": 1, "boxcolor": "#000000", "boxborderw": 4, "alpha": 1.0, "channel_id": 1 }' \
/// -H 'Authorization: <TOKEN>'
/// ```
#[post("/presets/")] #[post("/presets/")]
#[has_any_role("Role::Admin", "Role::User", type = "Role")] #[has_any_role("Role::Admin", "Role::User", type = "Role")]
async fn add_preset(data: web::Json<TextPreset>) -> Result<impl Responder, ServiceError> { async fn add_preset(data: web::Json<TextPreset>) -> Result<impl Responder, ServiceError> {
@ -259,21 +398,39 @@ async fn add_preset(data: web::Json<TextPreset>) -> Result<impl Responder, Servi
Err(ServiceError::InternalServerError) Err(ServiceError::InternalServerError)
} }
/// ---------------------------------------------------------------------------- /// **Delete Preset**
/// ffplayout process controlling ///
/// ```BASH
/// curl -X DELETE http://localhost:8000/api/presets/1 -H 'Content-Type: application/json' \
/// -H 'Authorization: <TOKEN>'
/// ```
#[delete("/presets/{id}")]
#[has_any_role("Role::Admin", "Role::User", type = "Role")]
async fn delete_preset(id: web::Path<i64>) -> Result<impl Responder, ServiceError> {
if db_delete_preset(&id).await.is_ok() {
return Ok("Delete preset Success");
}
Err(ServiceError::InternalServerError)
}
/// ### ffplayout controlling
/// ///
/// here we communicate with the engine for: /// here we communicate with the engine for:
/// - jump to last or next clip /// - jump to last or next clip
/// - reset playlist state /// - reset playlist state
/// - get infos about current, next, last clip /// - get infos about current, next, last clip
/// - send text the the engine, for overlaying it (as lower third etc.) /// - send text to the engine, for overlaying it (as lower third etc.)
/// ---------------------------------------------------------------------------- ///
/// **Send Text to ffplayout**
/// curl -X POST http://localhost:8080/api/control/1/text/ \ ///
/// --header 'Content-Type: application/json' --header 'Authorization: <TOKEN>' \ /// ```BASH
/// --data '{"text": "Hello from ffplayout", "x": "(w-text_w)/2", "y": "(h-text_h)/2", \ /// curl -X POST http://localhost:8000/api/control/1/text/ \
/// -H 'Content-Type: application/json' -H 'Authorization: <TOKEN>' \
/// -d '{"text": "Hello from ffplayout", "x": "(w-text_w)/2", "y": "(h-text_h)/2", \
/// "fontsize": "24", "line_spacing": "4", "fontcolor": "#ffffff", "box": "1", \ /// "fontsize": "24", "line_spacing": "4", "fontcolor": "#ffffff", "box": "1", \
/// "boxcolor": "#000000", "boxborderw": "4", "alpha": "1.0"}' /// "boxcolor": "#000000", "boxborderw": "4", "alpha": "1.0"}'
/// ```
#[post("/control/{id}/text/")] #[post("/control/{id}/text/")]
#[has_any_role("Role::Admin", "Role::User", type = "Role")] #[has_any_role("Role::Admin", "Role::User", type = "Role")]
pub async fn send_text_message( pub async fn send_text_message(
@ -286,41 +443,55 @@ pub async fn send_text_message(
} }
} }
/// curl -X POST http://localhost:8080/api/control/1/playout/next/ /// **Control Playout**
/// --header 'Content-Type: application/json' --header 'Authorization: <TOKEN>' ///
#[post("/control/{id}/playout/next/")] /// - next
/// - back
/// - reset
///
/// ```BASH
/// curl -X POST http://localhost:8000/api/control/1/playout/next/ -H 'Content-Type: application/json'
/// -d '{ "command": "reset" }' -H 'Authorization: <TOKEN>'
/// ```
#[post("/control/{id}/playout/")]
#[has_any_role("Role::Admin", "Role::User", type = "Role")] #[has_any_role("Role::Admin", "Role::User", type = "Role")]
pub async fn jump_to_next(id: web::Path<i64>) -> Result<impl Responder, ServiceError> { pub async fn control_playout(id: web::Path<i64>, control: web::Json<Process>) -> Result<impl Responder, ServiceError> {
match control_state(*id, "next".into()).await { match control_state(*id, control.command.clone()).await {
Ok(res) => return Ok(res.text().await.unwrap_or_else(|_| "Success".into())), Ok(res) => return Ok(res.text().await.unwrap_or_else(|_| "Success".into())),
Err(e) => Err(e), Err(e) => Err(e),
} }
} }
/// curl -X POST http://localhost:8080/api/control/1/playout/back/ /// **Get current Clip**
/// --header 'Content-Type: application/json' --header 'Authorization: <TOKEN>' ///
#[post("/control/{id}/playout/back/")] /// ```BASH
#[has_any_role("Role::Admin", "Role::User", type = "Role")] /// curl -X GET http://localhost:8000/api/control/1/media/current
pub async fn jump_to_last(id: web::Path<i64>) -> Result<impl Responder, ServiceError> { /// -H 'Content-Type: application/json' -H 'Authorization: <TOKEN>'
match control_state(*id, "back".into()).await { /// ```
Ok(res) => return Ok(res.text().await.unwrap_or_else(|_| "Success".into())), ///
Err(e) => Err(e), /// **Response:**
} ///
} /// ```JSON
/// {
/// curl -X POST http://localhost:8080/api/control/1/playout/reset/ /// "jsonrpc": "2.0",
/// --header 'Content-Type: application/json' --header 'Authorization: <TOKEN>' /// "result": {
#[post("/control/{id}/playout/reset/")] /// "current_media": {
#[has_any_role("Role::Admin", "Role::User", type = "Role")] /// "category": "",
pub async fn reset_playout(id: web::Path<i64>) -> Result<impl Responder, ServiceError> { /// "duration": 154.2,
match control_state(*id, "reset".into()).await { /// "out": 154.2,
Ok(res) => return Ok(res.text().await.unwrap_or_else(|_| "Success".into())), /// "seek": 0.0,
Err(e) => Err(e), /// "source": "/opt/tv-media/clip.mp4"
} /// },
} /// "index": 39,
/// "play_mode": "playlist",
/// curl -X GET http://localhost:8080/api/control/1/media/current/ /// "played_sec": 67.80771999300123,
/// --header 'Content-Type: application/json' --header 'Authorization: <TOKEN>' /// "remaining_sec": 86.39228000699876,
/// "start_sec": 24713.631999999998,
/// "start_time": "06:51:53.631"
/// },
/// "id": 1
/// }
/// ```
#[get("/control/{id}/media/current")] #[get("/control/{id}/media/current")]
#[has_any_role("Role::Admin", "Role::User", type = "Role")] #[has_any_role("Role::Admin", "Role::User", type = "Role")]
pub async fn media_current(id: web::Path<i64>) -> Result<impl Responder, ServiceError> { pub async fn media_current(id: web::Path<i64>) -> Result<impl Responder, ServiceError> {
@ -330,8 +501,11 @@ pub async fn media_current(id: web::Path<i64>) -> Result<impl Responder, Service
} }
} }
/// curl -X GET http://localhost:8080/api/control/1/media/next/ /// **Get next Clip**
/// --header 'Content-Type: application/json' --header 'Authorization: <TOKEN>' ///
/// ```BASH
/// curl -X GET http://localhost:8000/api/control/1/media/next/ -H 'Authorization: <TOKEN>'
/// ```
#[get("/control/{id}/media/next")] #[get("/control/{id}/media/next")]
#[has_any_role("Role::Admin", "Role::User", type = "Role")] #[has_any_role("Role::Admin", "Role::User", type = "Role")]
pub async fn media_next(id: web::Path<i64>) -> Result<impl Responder, ServiceError> { pub async fn media_next(id: web::Path<i64>) -> Result<impl Responder, ServiceError> {
@ -341,8 +515,12 @@ pub async fn media_next(id: web::Path<i64>) -> Result<impl Responder, ServiceErr
} }
} }
/// curl -X GET http://localhost:8080/api/control/1/media/last/ /// **Get last Clip**
/// --header 'Content-Type: application/json' --header 'Authorization: <TOKEN>' ///
/// ```BASH
/// curl -X GET http://localhost:8000/api/control/1/media/last/
/// -H 'Content-Type: application/json' -H 'Authorization: <TOKEN>'
/// ```
#[get("/control/{id}/media/last")] #[get("/control/{id}/media/last")]
#[has_any_role("Role::Admin", "Role::User", type = "Role")] #[has_any_role("Role::Admin", "Role::User", type = "Role")]
pub async fn media_last(id: web::Path<i64>) -> Result<impl Responder, ServiceError> { pub async fn media_last(id: web::Path<i64>) -> Result<impl Responder, ServiceError> {
@ -352,9 +530,19 @@ pub async fn media_last(id: web::Path<i64>) -> Result<impl Responder, ServiceErr
} }
} }
/// curl -X GET http://localhost:8080/api/control/1/process/ /// #### ffplayout Process Control
/// --header 'Content-Type: application/json' --header 'Authorization: <TOKEN>' ///
/// Control ffplayout process, like:
/// - start
/// - stop
/// - restart
/// - status
///
/// ```BASH
/// curl -X POST http://localhost:8000/api/control/1/process/
/// -H 'Content-Type: application/json' -H 'Authorization: <TOKEN>'
/// -d '{"command": "start"}' /// -d '{"command": "start"}'
/// ```
#[post("/control/{id}/process/")] #[post("/control/{id}/process/")]
#[has_any_role("Role::Admin", "Role::User", type = "Role")] #[has_any_role("Role::Admin", "Role::User", type = "Role")]
pub async fn process_control( pub async fn process_control(
@ -364,27 +552,33 @@ pub async fn process_control(
control_service(*id, &proc.command).await control_service(*id, &proc.command).await
} }
/// ---------------------------------------------------------------------------- /// #### ffplayout Playlist Operations
/// ffplayout playlist operations
/// ///
/// ---------------------------------------------------------------------------- /// **Get playlist**
///
/// curl -X GET http://localhost:8080/api/playlist/1/2022-06-20 /// ```BASH
/// --header 'Content-Type: application/json' --header 'Authorization: <TOKEN>' /// curl -X GET http://localhost:8000/api/playlist/1?date=2022-06-20
#[get("/playlist/{id}/{date}")] /// -H 'Content-Type: application/json' -H 'Authorization: <TOKEN>'
/// ```
#[get("/playlist/{id}")]
#[has_any_role("Role::Admin", "Role::User", type = "Role")] #[has_any_role("Role::Admin", "Role::User", type = "Role")]
pub async fn get_playlist( pub async fn get_playlist(
params: web::Path<(i64, String)>, id: web::Path<i64>,
obj: web::Query<DateObj>,
) -> Result<impl Responder, ServiceError> { ) -> Result<impl Responder, ServiceError> {
match read_playlist(params.0, params.1.clone()).await { match read_playlist(*id, obj.date.clone()).await {
Ok(playlist) => Ok(web::Json(playlist)), Ok(playlist) => Ok(web::Json(playlist)),
Err(e) => Err(e), Err(e) => Err(e),
} }
} }
/// curl -X POST http://localhost:8080/api/playlist/1/ /// **Save playlist**
/// --header 'Content-Type: application/json' --header 'Authorization: <TOKEN>' ///
/// ```BASH
/// curl -X POST http://localhost:8000/api/playlist/1/
/// -H 'Content-Type: application/json' -H 'Authorization: <TOKEN>'
/// -- data "{<JSON playlist data>}" /// -- data "{<JSON playlist data>}"
/// ```
#[post("/playlist/{id}/")] #[post("/playlist/{id}/")]
#[has_any_role("Role::Admin", "Role::User", type = "Role")] #[has_any_role("Role::Admin", "Role::User", type = "Role")]
pub async fn save_playlist( pub async fn save_playlist(
@ -397,8 +591,14 @@ pub async fn save_playlist(
} }
} }
/// curl -X GET http://localhost:8080/api/playlist/1/generate/2022-06-20 /// **Generate Playlist**
/// --header 'Content-Type: application/json' --header 'Authorization: <TOKEN>' ///
/// A new playlist will be generated and response.
///
/// ```BASH
/// curl -X GET http://localhost:8000/api/playlist/1/generate/2022-06-20
/// -H 'Content-Type: application/json' -H 'Authorization: <TOKEN>'
/// ```
#[get("/playlist/{id}/generate/{date}")] #[get("/playlist/{id}/generate/{date}")]
#[has_any_role("Role::Admin", "Role::User", type = "Role")] #[has_any_role("Role::Admin", "Role::User", type = "Role")]
pub async fn gen_playlist( pub async fn gen_playlist(
@ -410,8 +610,12 @@ pub async fn gen_playlist(
} }
} }
/// curl -X DELETE http://localhost:8080/api/playlist/1/2022-06-20 /// **Delete Playlist**
/// --header 'Content-Type: application/json' --header 'Authorization: <TOKEN>' ///
/// ```BASH
/// curl -X DELETE http://localhost:8000/api/playlist/1/2022-06-20
/// -H 'Content-Type: application/json' -H 'Authorization: <TOKEN>'
/// ```
#[delete("/playlist/{id}/{date}")] #[delete("/playlist/{id}/{date}")]
#[has_any_role("Role::Admin", "Role::User", type = "Role")] #[has_any_role("Role::Admin", "Role::User", type = "Role")]
pub async fn del_playlist( pub async fn del_playlist(
@ -423,13 +627,31 @@ pub async fn del_playlist(
} }
} }
/// ---------------------------------------------------------------------------- /// ### Log file
/// file operations
/// ///
/// ---------------------------------------------------------------------------- /// **Read Log Life**
///
/// ```BASH
/// curl -X Get http://localhost:8000/api/log/1
/// -H 'Content-Type: application/json' -H 'Authorization: <TOKEN>'
/// ```
#[get("/log/{id}")]
#[has_any_role("Role::Admin", "Role::User", type = "Role")]
pub async fn get_log(
id: web::Path<i64>,
log: web::Query<DateObj>,
) -> Result<impl Responder, ServiceError> {
read_log_file(&id, &log.date).await
}
/// curl -X GET http://localhost:8080/api/file/1/browse/ /// ### File Operations
/// --header 'Content-Type: application/json' --header 'Authorization: <TOKEN>' ///
/// **Get File/Folder List**
///
/// ```BASH
/// curl -X POST http://localhost:8000/api/file/1/browse/ -H 'Content-Type: application/json'
/// -d '{ "source": "/" }' -H 'Authorization: <TOKEN>'
/// ```
#[post("/file/{id}/browse/")] #[post("/file/{id}/browse/")]
#[has_any_role("Role::Admin", "Role::User", type = "Role")] #[has_any_role("Role::Admin", "Role::User", type = "Role")]
pub async fn file_browser( pub async fn file_browser(
@ -442,10 +664,28 @@ pub async fn file_browser(
} }
} }
/// curl -X POST http://localhost:8080/api/file/1/move/ /// **Create Folder**
/// --header 'Content-Type: application/json' --header 'Authorization: <TOKEN>' ///
/// -d '{"source": "<SOURCE>", "target": "<TARGET>"}' /// ```BASH
#[post("/file/{id}/move/")] /// curl -X POST http://localhost:8000/api/file/1/create-folder/ -H 'Content-Type: application/json'
/// -d '{"source": "<FOLDER PATH>"}' -H 'Authorization: <TOKEN>'
/// ```
#[post("/file/{id}/create-folder/")]
#[has_any_role("Role::Admin", "Role::User", type = "Role")]
pub async fn add_dir(
id: web::Path<i64>,
data: web::Json<PathObject>,
) -> Result<HttpResponse, ServiceError> {
create_directory(*id, &data.into_inner()).await
}
/// **Rename File**
///
/// ```BASH
/// curl -X POST http://localhost:8000/api/file/1/rename/ -H 'Content-Type: application/json'
/// -d '{"source": "<SOURCE>", "target": "<TARGET>"}' -H 'Authorization: <TOKEN>'
/// ```
#[post("/file/{id}/rename/")]
#[has_any_role("Role::Admin", "Role::User", type = "Role")] #[has_any_role("Role::Admin", "Role::User", type = "Role")]
pub async fn move_rename( pub async fn move_rename(
id: web::Path<i64>, id: web::Path<i64>,
@ -457,10 +697,13 @@ pub async fn move_rename(
} }
} }
/// curl -X DELETE http://localhost:8080/api/file/1/remove/ /// **Remove File/Folder**
/// --header 'Content-Type: application/json' --header 'Authorization: <TOKEN>' ///
/// -d '{"source": "<SOURCE>"}' /// ```BASH
#[delete("/file/{id}/remove/")] /// curl -X POST http://localhost:8000/api/file/1/remove/ -H 'Content-Type: application/json'
/// -d '{"source": "<SOURCE>"}' -H 'Authorization: <TOKEN>'
/// ```
#[post("/file/{id}/remove/")]
#[has_any_role("Role::Admin", "Role::User", type = "Role")] #[has_any_role("Role::Admin", "Role::User", type = "Role")]
pub async fn remove( pub async fn remove(
id: web::Path<i64>, id: web::Path<i64>,
@ -472,8 +715,18 @@ pub async fn remove(
} }
} }
#[post("/file/{id}/upload/")] /// **Upload File**
///
/// ```BASH
/// curl -X POST http://localhost:8000/api/file/1/upload/ -H 'Authorization: <TOKEN>'
/// -F "file=@file.mp4"
/// ```
#[put("/file/{id}/upload/")]
#[has_any_role("Role::Admin", "Role::User", type = "Role")] #[has_any_role("Role::Admin", "Role::User", type = "Role")]
async fn save_file(id: web::Path<i64>, payload: Multipart) -> Result<HttpResponse, ServiceError> { async fn save_file(
upload(*id, payload).await id: web::Path<i64>,
payload: Multipart,
obj: web::Query<FileObj>,
) -> Result<HttpResponse, ServiceError> {
upload(*id, payload, &obj.path).await
} }

View File

@ -85,9 +85,9 @@ fn main() {
validate_ffmpeg(&config); validate_ffmpeg(&config);
if let Some(range) = config.general.generate.clone() { if config.general.generate.is_some() {
// run a simple playlist generator and save them to disk // run a simple playlist generator and save them to disk
if let Err(e) = generate_playlist(&config, range, None) { if let Err(e) = generate_playlist(&config, None) {
error!("{e}"); error!("{e}");
exit(1); exit(1);
}; };

View File

@ -149,6 +149,10 @@ pub fn write_hls(
proc_control.is_terminated.clone(), proc_control.is_terminated.clone(),
); );
if config.out.preview {
warn!("Preview in HLS mode is not supported!");
}
// spawn a thread for ffmpeg ingest server and create a channel for package sending // spawn a thread for ffmpeg ingest server and create a channel for package sending
if config.ingest.enable { if config.ingest.enable {
thread::spawn(move || ingest_to_hls_server(config_clone, play_stat, proc_control_c)); thread::spawn(move || ingest_to_hls_server(config_clone, play_stat, proc_control_c));

View File

@ -13,7 +13,7 @@ crossbeam-channel = "0.5"
ffprobe = "0.3" ffprobe = "0.3"
file-rotate = { git = "https://github.com/Ploppz/file-rotate.git", branch = "timestamp-parse-fix" } file-rotate = { git = "https://github.com/Ploppz/file-rotate.git", branch = "timestamp-parse-fix" }
jsonrpc-http-server = "18.0" jsonrpc-http-server = "18.0"
lettre = "0.10.0-rc.7" lettre = "0.10"
log = "0.4" log = "0.4"
notify = "4.0" notify = "4.0"
rand = "0.8" rand = "0.8"

View File

@ -190,15 +190,6 @@ fn add_text(node: &mut Media, chain: &mut Filters, config: &PlayoutConfig) {
let filter = v_drawtext::filter_node(config, node); let filter = v_drawtext::filter_node(config, node);
chain.add_filter(&filter, "video"); chain.add_filter(&filter, "video");
if let Some(filters) = &chain.video_chain {
for (i, f) in filters.split(',').enumerate() {
if f.contains("drawtext") && !config.text.text_from_filename {
debug!("drawtext node is on index: <yellow>{i}</>");
break;
}
}
}
} }
} }
@ -290,7 +281,7 @@ fn realtime_filter(
t = "a" t = "a"
} }
if &config.out.mode.to_lowercase() == "hls" { if config.general.generate.is_none() && &config.out.mode.to_lowercase() == "hls" {
let mut speed_filter = format!("{t}realtime=speed=1"); let mut speed_filter = format!("{t}realtime=speed=1");
let (delta, _) = get_delta(config, &node.begin.unwrap()); let (delta, _) = get_delta(config, &node.begin.unwrap());
let duration = node.out - node.seek; let duration = node.out - node.seek;

View File

@ -53,7 +53,6 @@ fn get_date_range(date_range: &[String]) -> Vec<String> {
/// Generate playlists /// Generate playlists
pub fn generate_playlist( pub fn generate_playlist(
config: &PlayoutConfig, config: &PlayoutConfig,
mut date_range: Vec<String>,
channel_name: Option<String>, channel_name: Option<String>,
) -> Result<Vec<JsonPlaylist>, Error> { ) -> Result<Vec<JsonPlaylist>, Error> {
let total_length = match config.playlist.length_sec { let total_length = match config.playlist.length_sec {
@ -70,6 +69,7 @@ pub fn generate_playlist(
let index = Arc::new(AtomicUsize::new(0)); let index = Arc::new(AtomicUsize::new(0));
let playlist_root = Path::new(&config.playlist.path); let playlist_root = Path::new(&config.playlist.path);
let mut playlists = vec![]; let mut playlists = vec![];
let mut date_range = vec![];
let channel = match channel_name { let channel = match channel_name {
Some(name) => name, Some(name) => name,
@ -85,6 +85,10 @@ pub fn generate_playlist(
exit(1); exit(1);
} }
if let Some(range) = config.general.generate.clone() {
date_range = range;
}
if date_range.contains(&"-".to_string()) && date_range.len() == 3 { if date_range.contains(&"-".to_string()) && date_range.len() == 3 {
date_range = get_date_range(&date_range) date_range = get_date_range(&date_range)
} }

View File

@ -43,7 +43,7 @@ pub fn send_mail(cfg: &PlayoutConfig, msg: String) {
message = message.to(r.parse().unwrap()); message = message.to(r.parse().unwrap());
} }
if let Ok(email) = message.body(clean_string(&msg)) { if let Ok(mail) = message.body(clean_string(&msg)) {
let credentials = let credentials =
Credentials::new(cfg.mail.sender_addr.clone(), cfg.mail.sender_pass.clone()); Credentials::new(cfg.mail.sender_addr.clone(), cfg.mail.sender_pass.clone());
@ -55,9 +55,9 @@ pub fn send_mail(cfg: &PlayoutConfig, msg: String) {
let mailer = transporter.unwrap().credentials(credentials).build(); let mailer = transporter.unwrap().credentials(credentials).build();
// Send the email // Send the mail
if let Err(e) = mailer.send(&email) { if let Err(e) = mailer.send(&mail) {
error!("Could not send email: {:?}", e); error!("Could not send mail: {:?}", e);
} }
} else { } else {
error!("Mail Message failed!"); error!("Mail Message failed!");

View File

@ -152,7 +152,7 @@ pub struct MediaProbe {
} }
impl MediaProbe { impl MediaProbe {
fn new(input: &str) -> Self { pub fn new(input: &str) -> Self {
let probe = ffprobe(input); let probe = ffprobe(input);
let mut a_stream = vec![]; let mut a_stream = vec![];
let mut v_stream = vec![]; let mut v_stream = vec![];

23
scripts/gen_doc.sh Executable file
View File

@ -0,0 +1,23 @@
#!/usr/bin/bash
input=$1
output=$2
print_block=false
if [ ! "$input" ] || [ ! "$output" ]; then
echo "Run script like: den_doc.sh input.rs output.md"
fi
:> "$output"
while IFS= read -r line; do
if echo $line | grep -Eq "^///"; then
echo "$line" | sed -E "s|^/// ?||g" >> "$output"
print_block=true
fi
if [ -z "$line" ] && [[ $print_block == true ]]; then
echo "" >> "$output"
print_block=false
fi
done < "$input"