Merge pull request #146 from jb-alvarado/master
support frontend v5.0.0
This commit is contained in:
commit
0632e862e5
82
Cargo.lock
generated
82
Cargo.lock
generated
@ -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",
|
||||||
]
|
]
|
||||||
|
13
Cargo.toml
13
Cargo.toml
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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"
|
||||||
|
@ -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
|
||||||
|
378
docs/api.md
378
docs/api.md
@ -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"
|
||||||
|
```
|
||||||
|
@ -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]
|
||||||
|
@ -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:
|
||||||
|
@ -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),
|
||||||
|
@ -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>,
|
||||||
|
@ -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())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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()));
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
|
@ -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(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
@ -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,
|
||||||
|
@ -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())
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
};
|
};
|
||||||
|
@ -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));
|
||||||
|
@ -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"
|
||||||
|
@ -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;
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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!");
|
||||||
|
@ -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
23
scripts/gen_doc.sh
Executable 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"
|
Loading…
x
Reference in New Issue
Block a user