From 227d09bce7b2aa46f17f5ca99755767e13c00c7b Mon Sep 17 00:00:00 2001 From: jb-alvarado Date: Mon, 6 Jun 2022 23:07:11 +0200 Subject: [PATCH 01/47] start with API --- .gitignore | 1 + Cargo.lock | 809 ++++++++++++++++++++++++++++++++++++++++- Cargo.toml | 9 + src/api/handles.rs | 102 ++++++ src/api/mod.rs | 3 + src/api/models.rs | 17 + src/api/routes.rs | 41 +++ src/bin/ffpapi.rs | 109 ++++++ src/lib.rs | 1 + src/main.rs | 9 +- src/tests/mod.rs | 17 +- src/tests/utils/mod.rs | 2 +- src/utils/arg_parse.rs | 2 +- src/utils/config.rs | 97 ++--- src/utils/logging.rs | 14 +- src/utils/mod.rs | 2 +- 16 files changed, 1160 insertions(+), 75 deletions(-) create mode 100644 src/api/handles.rs create mode 100644 src/api/mod.rs create mode 100644 src/api/models.rs create mode 100644 src/api/routes.rs create mode 100644 src/bin/ffpapi.rs diff --git a/.gitignore b/.gitignore index d43385d9..3cf88035 100644 --- a/.gitignore +++ b/.gitignore @@ -17,5 +17,6 @@ *tar.gz *.deb *.rpm +/assets/*.db* .vscode/ diff --git a/Cargo.lock b/Cargo.lock index d8935839..a5e4bade 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,12 +2,202 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "actix-codec" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57a7559404a7f3573127aab53c08ce37a6c6a315c374a31070f3c91cd1b4a7fe" +dependencies = [ + "bitflags", + "bytes", + "futures-core", + "futures-sink", + "log", + "memchr", + "pin-project-lite", + "tokio", + "tokio-util 0.7.2", +] + +[[package]] +name = "actix-http" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5885cb81a0d4d0d322864bea1bb6c2a8144626b4fdc625d4c51eba197e7797a" +dependencies = [ + "actix-codec", + "actix-rt", + "actix-service", + "actix-utils", + "ahash", + "base64", + "bitflags", + "brotli", + "bytes", + "bytestring", + "derive_more", + "encoding_rs", + "flate2", + "futures-core", + "h2", + "http", + "httparse", + "httpdate", + "itoa", + "language-tags", + "local-channel", + "log", + "mime", + "percent-encoding", + "pin-project-lite", + "rand", + "sha-1", + "smallvec", + "zstd", +] + +[[package]] +name = "actix-macros" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "465a6172cf69b960917811022d8f29bc0b7fa1398bc4f78b3c466673db1213b6" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "actix-router" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb60846b52c118f2f04a56cc90880a274271c489b2498623d58176f8ca21fa80" +dependencies = [ + "bytestring", + "firestorm", + "http", + "log", + "regex", + "serde", +] + +[[package]] +name = "actix-rt" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ea16c295198e958ef31930a6ef37d0fb64e9ca3b6116e6b93a8bdae96ee1000" +dependencies = [ + "futures-core", + "tokio", +] + +[[package]] +name = "actix-server" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0da34f8e659ea1b077bb4637948b815cd3768ad5a188fdcd74ff4d84240cd824" +dependencies = [ + "actix-rt", + "actix-service", + "actix-utils", + "futures-core", + "futures-util", + "mio 0.8.3", + "num_cpus", + "socket2", + "tokio", + "tracing", +] + +[[package]] +name = "actix-service" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b894941f818cfdc7ccc4b9e60fa7e53b5042a2e8567270f9147d5591893373a" +dependencies = [ + "futures-core", + "paste", + "pin-project-lite", +] + +[[package]] +name = "actix-utils" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e491cbaac2e7fc788dfff99ff48ef317e23b3cf63dbaf7aaab6418f40f92aa94" +dependencies = [ + "local-waker", + "pin-project-lite", +] + +[[package]] +name = "actix-web" +version = "4.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4e5ebffd51d50df56a3ae0de0e59487340ca456f05dd0b90c0a7a6dd6a74d31" +dependencies = [ + "actix-codec", + "actix-http", + "actix-macros", + "actix-router", + "actix-rt", + "actix-server", + "actix-service", + "actix-utils", + "actix-web-codegen", + "ahash", + "bytes", + "bytestring", + "cfg-if 1.0.0", + "cookie", + "derive_more", + "encoding_rs", + "futures-core", + "futures-util", + "itoa", + "language-tags", + "log", + "mime", + "once_cell", + "pin-project-lite", + "regex", + "serde", + "serde_json", + "serde_urlencoded", + "smallvec", + "socket2", + "time 0.3.9", + "url", +] + +[[package]] +name = "actix-web-codegen" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7525bedf54704abb1d469e88d7e7e9226df73778798a69cea5022d53b2ae91bc" +dependencies = [ + "actix-router", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "adler" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "ahash" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" +dependencies = [ + "getrandom", + "once_cell", + "version_check", +] + [[package]] name = "aho-corasick" version = "0.7.18" @@ -17,6 +207,30 @@ dependencies = [ "memchr", ] +[[package]] +name = "alloc-no-stdlib" +version = "2.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35ef4730490ad1c4eae5c4325b2a95f521d023e5c885853ff7aca0a6a1631db3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "697ed7edc0f1711de49ce108c541623a0af97c6c60b2f6e2b65229847ac843c2" +dependencies = [ + "alloc-no-stdlib", +] + +[[package]] +name = "atoi" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616896e05fc0e2649463a93a15183c6a16bf03413a7af88ef1285ddedfa9cda5" +dependencies = [ + "num-traits", +] + [[package]] name = "atty" version = "0.2.14" @@ -46,6 +260,36 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "block-buffer" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf7fe51849ea569fd452f37822f606a5cabb684dc918707a0193fd4664ff324" +dependencies = [ + "generic-array", +] + +[[package]] +name = "brotli" +version = "3.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1a0b1dbcc8ae29329621f8d4f0d835787c1c38bb1401979b49d13b0b305ff68" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ad2d4653bf5ca36ae797b1f4bb4dbddb60ce49ca4aed8a2ce4829f60425b80" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + [[package]] name = "bstr" version = "0.2.17" @@ -61,17 +305,35 @@ version = "3.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4a45a46ab1f2412e53d3a0ade76ffad2025804294569aae387231a0cd6e0899" +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + [[package]] name = "bytes" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8" +[[package]] +name = "bytestring" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90706ba19e97b90786e19dc0d5e2abd80008d99d4c0c5d1ad0b5e72cec7c494d" +dependencies = [ + "bytes", +] + [[package]] name = "cc" version = "1.0.73" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11" +dependencies = [ + "jobserver", +] [[package]] name = "cfg-if" @@ -85,6 +347,18 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chrono" +version = "0.4.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73" +dependencies = [ + "libc", + "num-integer", + "num-traits", + "winapi 0.3.9", +] + [[package]] name = "chrono" version = "0.4.19" @@ -136,6 +410,23 @@ dependencies = [ "os_str_bytes", ] +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + +[[package]] +name = "cookie" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94d4706de1b0fa5b132270cddffa8585166037822e260a944fe161acd137ca05" +dependencies = [ + "percent-encoding", + "time 0.3.9", + "version_check", +] + [[package]] name = "core-foundation" version = "0.9.3" @@ -152,6 +443,30 @@ version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" +[[package]] +name = "cpufeatures" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59a6001667ab124aebae2a495118e11d30984c3a653e99d86d58971708cf5e4b" +dependencies = [ + "libc", +] + +[[package]] +name = "crc" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49fc9a695bca7f35f5f4c15cddc84415f66a74ea78eef08e90c5024f2b540e23" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccaeedb56da03b09f598226e25e80088cb4cd25f316e6e4df7d695f0feeb1403" + [[package]] name = "crc32fast" version = "1.3.2" @@ -171,6 +486,16 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "crossbeam-queue" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f25d8400f4a7a5778f0e4e52384a48cbd9b5c495d110786187fc750075277a2" +dependencies = [ + "cfg-if 1.0.0", + "crossbeam-utils", +] + [[package]] name = "crossbeam-utils" version = "0.8.8" @@ -181,6 +506,51 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "crypto-common" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57952ca27b5e3606ff4dd79b0020231aaf9d6aa76dc05fd30137538c50bd3ce8" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "derive_more" +version = "0.99.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn", +] + +[[package]] +name = "digest" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2fb860ca6fafa5552fb6d0e816a69c8e49f0908bf524e30a90d97c85892d506" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "dotenv" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" + +[[package]] +name = "either" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" + [[package]] name = "email-encoding" version = "0.1.0" @@ -199,6 +569,23 @@ dependencies = [ "cfg-if 1.0.0", ] +[[package]] +name = "event-listener" +version = "2.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77f3309417938f28bf8228fcff79a4a37103981e3e186d2ccd19c74b38f4eb71" + +[[package]] +name = "faccess" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ae66425802d6a903e268ae1a08b8c38ba143520f227a205edf4e9c7e3e26d5" +dependencies = [ + "bitflags", + "libc", + "winapi 0.3.9", +] + [[package]] name = "fastrand" version = "1.7.0" @@ -212,9 +599,11 @@ dependencies = [ name = "ffplayout-engine" version = "0.9.8" dependencies = [ - "chrono", + "actix-web", + "chrono 0.4.19 (git+https://github.com/sbrocket/chrono?branch=parse-error-kind-public)", "clap", "crossbeam-channel", + "faccess", "ffprobe", "file-rotate", "jsonrpc-http-server", @@ -228,8 +617,10 @@ dependencies = [ "serde", "serde_json", "serde_yaml", + "sha-crypt", "shlex", "simplelog", + "sqlx", "time 0.3.9", "walkdir", ] @@ -249,7 +640,7 @@ name = "file-rotate" version = "0.6.0" source = "git+https://github.com/Ploppz/file-rotate.git?branch=timestamp-parse-fix#cb1874a15a7a18de820a57df48d3513e5a4076f4" dependencies = [ - "chrono", + "chrono 0.4.19 (git+https://github.com/sbrocket/chrono?branch=parse-error-kind-public)", "flate2", ] @@ -265,6 +656,12 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "firestorm" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c5f6c2c942da57e2aaaa84b8a521489486f14e75e7fa91dab70aba913975f98" + [[package]] name = "flate2" version = "1.0.24" @@ -275,6 +672,18 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "flume" +version = "0.10.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843c03199d0c0ca54bc1ea90ac0d507274c28abcc4f691ae8b4eaa375087c76a" +dependencies = [ + "futures-core", + "futures-sink", + "pin-project", + "spin", +] + [[package]] name = "fnv" version = "1.0.7" @@ -383,6 +792,17 @@ dependencies = [ "futures-util", ] +[[package]] +name = "futures-intrusive" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62007592ac46aa7c2b6416f7deb9a8a8f63a01e0f1d6e1787d5630170db2b63e" +dependencies = [ + "futures-core", + "lock_api", + "parking_lot 0.11.2", +] + [[package]] name = "futures-io" version = "0.3.21" @@ -430,6 +850,16 @@ dependencies = [ "slab", ] +[[package]] +name = "generic-array" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd48d33ec7f05fbfa152300fdad764757cbded343c1aa1cff2fbaf4134851803" +dependencies = [ + "typenum", + "version_check", +] + [[package]] name = "getrandom" version = "0.2.6" @@ -478,12 +908,27 @@ name = "hashbrown" version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" +dependencies = [ + "ahash", +] + +[[package]] +name = "hashlink" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7249a3129cbc1ffccd74857f81464a323a152173cdb134e0fd81bc803b29facf" +dependencies = [ + "hashbrown", +] [[package]] name = "heck" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" +dependencies = [ + "unicode-segmentation", +] [[package]] name = "hermit-abi" @@ -494,6 +939,12 @@ dependencies = [ "libc", ] +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + [[package]] name = "hostname" version = "0.3.1" @@ -641,12 +1092,30 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "879d54834c8c76457ef4293a689b2a8c59b076067ad77b15efafbb05f92a592b" +[[package]] +name = "itertools" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9a9d19fa1e79b6215ff29b9d6880b706147f16e9b1dbb1e4e5947b5b02bc5e3" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "112c678d4050afce233f4f2852bb2eb519230b3cf12f33585275537d7e41578d" +[[package]] +name = "jobserver" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af25a77299a7f711a01975c35a6a424eb6862092cc2d6c72c4ed6cbc56dfc1fa" +dependencies = [ + "libc", +] + [[package]] name = "js-sys" version = "0.3.57" @@ -683,7 +1152,7 @@ dependencies = [ "jsonrpc-server-utils", "log", "net2", - "parking_lot", + "parking_lot 0.11.2", "unicase", ] @@ -715,6 +1184,12 @@ dependencies = [ "winapi-build", ] +[[package]] +name = "language-tags" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388" + [[package]] name = "lazy_static" version = "1.4.0" @@ -754,12 +1229,41 @@ version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836" +[[package]] +name = "libsqlite3-sys" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "898745e570c7d0453cc1fbc4a701eb6c662ed54e8fec8b7d14be137ebeeb9d14" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + [[package]] name = "linked-hash-map" version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7fb9b38af92608140b86b693604b9ffcc5824240a484d1ecd4795bacb2fe88f3" +[[package]] +name = "local-channel" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f303ec0e94c6c54447f84f3b0ef7af769858a9c4ef56ef2a986d3dcd4c3fc9c" +dependencies = [ + "futures-core", + "futures-sink", + "futures-util", + "local-waker", +] + +[[package]] +name = "local-waker" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e34f76eb3611940e0e7d53a9aaa4e6a3151f69541a282fd0dad5571420c53ff1" + [[package]] name = "lock_api" version = "0.4.7" @@ -1049,7 +1553,17 @@ checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" dependencies = [ "instant", "lock_api", - "parking_lot_core", + "parking_lot_core 0.8.5", +] + +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core 0.9.3", ] [[package]] @@ -1066,12 +1580,51 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "parking_lot_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09a279cbf25cb0757810394fbc1e359949b59e348145c643a939a525692e6929" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "redox_syscall", + "smallvec", + "windows-sys", +] + +[[package]] +name = "paste" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c520e05135d6e763148b6426a837e239041653ba7becd2e538c076c738025fc" + [[package]] name = "percent-encoding" version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" +[[package]] +name = "pin-project" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58ad3879ad3baf4e44784bc6a718a8698867bb991f8ce24d1bcbe2cfb4c3a75e" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "744b6f092ba29c3650faf274db506afd39944f48420f6c86b17cfe0ee1cb36bb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "pin-project-lite" version = "0.2.9" @@ -1245,6 +1798,15 @@ dependencies = [ "winreg", ] +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + [[package]] name = "ryu" version = "1.0.10" @@ -1299,6 +1861,12 @@ dependencies = [ "libc", ] +[[package]] +name = "semver" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8cb243bdfdb5936c8dc3c45762a19d12ab4550cdc753bc247637d4ec35a040fd" + [[package]] name = "serde" version = "1.0.137" @@ -1354,12 +1922,54 @@ dependencies = [ "yaml-rust", ] +[[package]] +name = "sha-1" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "028f48d513f9678cda28f6e4064755b3fbb2af6acd672f2c209b62323f7aea0f" +dependencies = [ + "cfg-if 1.0.0", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha-crypt" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0193e80e8a92aa7173dad160cfd5f5eda57ba146aac9826ea5c9dc3d5492072" +dependencies = [ + "rand", + "sha2", + "subtle", +] + +[[package]] +name = "sha2" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55deaec60f81eefe3cce0dc50bda92d6d8e88f2a27df7c5033b42afeb1ed2676" +dependencies = [ + "cfg-if 1.0.0", + "cpufeatures", + "digest", +] + [[package]] name = "shlex" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43b2853a4d09f215c24cc5489c992ce46052d359b5109343cbafbf26bc62f8a3" +[[package]] +name = "signal-hook-registry" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0" +dependencies = [ + "libc", +] + [[package]] name = "simplelog" version = "0.12.0" @@ -1394,12 +2004,133 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "spin" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c530c2b0d0bf8b69304b39fe2001993e267461948b890cd037d8ad4293fa1a0d" +dependencies = [ + "lock_api", +] + +[[package]] +name = "sqlformat" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4b7922be017ee70900be125523f38bdd644f4f06a1b16e8fa5a8ee8c34bffd4" +dependencies = [ + "itertools", + "nom", + "unicode_categories", +] + +[[package]] +name = "sqlx" +version = "0.5.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "551873805652ba0d912fec5bbb0f8b4cdd96baf8e2ebf5970e5671092966019b" +dependencies = [ + "sqlx-core", + "sqlx-macros", +] + +[[package]] +name = "sqlx-core" +version = "0.5.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e48c61941ccf5ddcada342cd59e3e5173b007c509e1e8e990dafc830294d9dc5" +dependencies = [ + "ahash", + "atoi", + "bitflags", + "byteorder", + "bytes", + "chrono 0.4.19 (registry+https://github.com/rust-lang/crates.io-index)", + "crc", + "crossbeam-queue", + "either", + "event-listener", + "flume", + "futures-channel", + "futures-core", + "futures-executor", + "futures-intrusive", + "futures-util", + "hashlink", + "hex", + "indexmap", + "itoa", + "libc", + "libsqlite3-sys", + "log", + "memchr", + "once_cell", + "paste", + "percent-encoding", + "sha2", + "smallvec", + "sqlformat", + "sqlx-rt", + "stringprep", + "thiserror", + "tokio-stream", + "url", +] + +[[package]] +name = "sqlx-macros" +version = "0.5.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc0fba2b0cae21fc00fe6046f8baa4c7fcb49e379f0f592b04696607f69ed2e1" +dependencies = [ + "dotenv", + "either", + "heck", + "once_cell", + "proc-macro2", + "quote", + "sha2", + "sqlx-core", + "sqlx-rt", + "syn", + "url", +] + +[[package]] +name = "sqlx-rt" +version = "0.5.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4db708cd3e459078f85f39f96a00960bd841f66ee2a669e90bf36907f5a79aae" +dependencies = [ + "actix-rt", + "native-tls", + "once_cell", + "tokio", + "tokio-native-tls", +] + +[[package]] +name = "stringprep" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee348cb74b87454fff4b551cbf727025810a004f88aeacae7f85b87f4e9a1c1" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + [[package]] name = "strsim" version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +[[package]] +name = "subtle" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" + [[package]] name = "syn" version = "1.0.95" @@ -1440,6 +2171,26 @@ version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb" +[[package]] +name = "thiserror" +version = "1.0.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd829fe32373d27f76265620b5309d0340cb8550f523c1dda251d6298069069a" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0396bc89e626244658bef819e22d0cc459e795a5ebe878e6ec336d1674a8d79a" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "time" version = "0.1.44" @@ -1496,7 +2247,9 @@ dependencies = [ "mio 0.8.3", "num_cpus", "once_cell", + "parking_lot 0.12.1", "pin-project-lite", + "signal-hook-registry", "socket2", "winapi 0.3.9", ] @@ -1563,6 +2316,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d0ecdcb44a79f0fe9844f0c4f33a342cbcbb5117de8001e6ba0dc2351327d09" dependencies = [ "cfg-if 1.0.0", + "log", "pin-project-lite", "tracing-attributes", "tracing-core", @@ -1594,6 +2348,12 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" +[[package]] +name = "typenum" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" + [[package]] name = "unicase" version = "2.6.0" @@ -1624,6 +2384,18 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "unicode-segmentation" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e8820f5d777f6224dc4be3632222971ac30164d4a258d595640799554ebfd99" + +[[package]] +name = "unicode_categories" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" + [[package]] name = "url" version = "2.2.2" @@ -1870,3 +2642,32 @@ checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" dependencies = [ "linked-hash-map", ] + +[[package]] +name = "zstd" +version = "0.10.2+zstd.1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f4a6bd64f22b5e3e94b4e238669ff9f10815c27a5180108b849d24174a83847" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "4.1.6+zstd.1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94b61c51bb270702d6167b8ce67340d2754b088d0c091b06e593aa772c3ee9bb" +dependencies = [ + "libc", + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "1.6.3+zstd.1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc49afa5c8d634e75761feda8c592051e7eeb4683ba827211eb0d731d3402ea8" +dependencies = [ + "cc", + "libc", +] diff --git a/Cargo.toml b/Cargo.toml index 6bcee844..3396a03e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,9 +9,11 @@ edition = "2021" default-run = "ffplayout" [dependencies] +actix-web = "4" chrono = { git = "https://github.com/sbrocket/chrono", branch = "parse-error-kind-public" } clap = { version = "3.1", features = ["derive"] } crossbeam-channel = "0.5" +faccess = "0.2" ffprobe = "0.3" file-rotate = { git = "https://github.com/Ploppz/file-rotate.git", branch = "timestamp-parse-fix" } jsonrpc-http-server = "18.0" @@ -21,11 +23,14 @@ notify = "4.0" rand = "0.8" regex = "1" reqwest = { version = "0.11", features = ["blocking"] } +sha-crypt = { version = "0.4", features = ["simple"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" serde_yaml = "0.8" shlex = "1.1" simplelog = { version = "^0.12", features = ["paris"] } + +sqlx = { version = "0.5", features = [ "chrono", "runtime-actix-native-tls", "sqlite" ] } time = { version = "0.3", features = ["formatting", "macros"] } walkdir = "2" @@ -36,6 +41,10 @@ openssl = { version = "0.10", features = ["vendored"] } name = "ffplayout" path = "src/main.rs" +[[bin]] +name = "ffpapi" +path = "src/bin/ffpapi.rs" + [profile.release] opt-level = 3 strip = true diff --git a/src/api/handles.rs b/src/api/handles.rs new file mode 100644 index 00000000..67b9b546 --- /dev/null +++ b/src/api/handles.rs @@ -0,0 +1,102 @@ +use std::path::Path; + +use faccess::PathExt; +use simplelog::*; +use sqlx::{migrate::MigrateDatabase, sqlite::SqliteQueryResult, Pool, Sqlite, SqlitePool}; + +pub fn db_path() -> Result> { + let sys_path = Path::new("/usr/share/ffplayout"); + let mut db_path = String::from("./ffplayout.db"); + + if sys_path.is_dir() && sys_path.writable() { + db_path = String::from("/usr/share/ffplayout/ffplayout.db"); + } else if Path::new("./assets").is_dir() { + db_path = String::from("./assets/ffplayout.db"); + } + + Ok(db_path) +} + +async fn cretea_schema() -> Result { + let pool = db_connection().await?; + let query = "PRAGMA foreign_keys = ON; + CREATE TABLE IF NOT EXISTS groups + ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + UNIQUE(name) + ); + CREATE TABLE IF NOT EXISTS settings + ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + channel_name TEXT NOT NULL, + preview_url TEXT NOT NULL, + settings_path TEXT NOT NULL, + extra_extensions TEXT NOT NULL, + UNIQUE(channel_name) + ); + CREATE TABLE IF NOT EXISTS user + ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + email TEXT NOT NULL, + username TEXT NOT NULL, + password TEXT NOT NULL, + group_id INTEGER NOT NULL DEFAULT 2, + FOREIGN KEY (group_id) REFERENCES groups (id) ON UPDATE SET NULL ON DELETE SET NULL, + UNIQUE(email, username) + );"; + let result = sqlx::query(query).execute(&pool).await; + pool.close().await; + + result +} + +pub async fn db_init() -> Result<&'static str, Box> { + let db_path = db_path()?; + + if !Sqlite::database_exists(&db_path).await.unwrap_or(false) { + Sqlite::create_database(&db_path).await.unwrap(); + match cretea_schema().await { + Ok(_) => info!("Database created Successfully"), + Err(e) => panic!("{e}"), + } + } + let instances = db_connection().await?; + + let query = "INSERT INTO groups(name) VALUES('admin'), ('user'); + INSERT INTO settings(channel_name, preview_url, settings_path, extra_extensions) + VALUES('Channel 1', 'http://localhost/live/preview.m3u8', + '/etc/ffplayout/ffplayout.yml', '.jpg,.jpeg,.png');"; + sqlx::query(query).execute(&instances).await?; + + instances.close().await; + + Ok("Database initialized!") +} + +pub async fn db_connection() -> Result, sqlx::Error> { + let db_path = db_path().unwrap(); + + let pool = SqlitePool::connect(&db_path).await?; + + Ok(pool) +} + +pub async fn add_user( + instances: &SqlitePool, + mail: &str, + user: &str, + pass: &str, + group: &i64, +) -> Result { + let query = "INSERT INTO user (email, username, password, group_id) VALUES($1, $2, $3, $4)"; + let result = sqlx::query(query) + .bind(mail) + .bind(user) + .bind(pass) + .bind(group) + .execute(instances) + .await?; + + Ok(result) +} diff --git a/src/api/mod.rs b/src/api/mod.rs new file mode 100644 index 00000000..2ce89ab9 --- /dev/null +++ b/src/api/mod.rs @@ -0,0 +1,3 @@ +pub mod handles; +pub mod models; +pub mod routes; diff --git a/src/api/models.rs b/src/api/models.rs new file mode 100644 index 00000000..feaad65a --- /dev/null +++ b/src/api/models.rs @@ -0,0 +1,17 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Deserialize, Serialize)] +pub struct User { + pub email: String, + pub username: String, + pub password: String, + pub group_id: i64, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct Settings { + pub channel_name: String, + pub preview_url: String, + pub settings_path: String, + pub extra_extensions: String, +} diff --git a/src/api/routes.rs b/src/api/routes.rs new file mode 100644 index 00000000..39c135ee --- /dev/null +++ b/src/api/routes.rs @@ -0,0 +1,41 @@ +use crate::api::{ + handles::{add_user, db_connection}, + models::User, +}; +use actix_web::{get, post, web, Responder}; +use sha_crypt::{sha512_simple, Sha512Params}; + +#[get("/hello/{name}")] +async fn greet(name: web::Path) -> impl Responder { + format!("Hello {name}!") +} + +/// curl -X POST -H "Content-Type: application/json" -d '{"username": "USER", "password": "abc123", "email":"user@example.org" }' http://127.0.0.1:8080/api/user/ +#[post("/api/user/")] +pub async fn user(user: web::Json) -> impl Responder { + let params = Sha512Params::new(10_000).expect("RandomError!"); + + let hashed_password = sha512_simple(&user.password, ¶ms).expect("Should not fail"); + + // // Verifying a stored password + // assert!(sha512_check("Not so secure password", &hashed_password).is_ok()); + + if let Ok(pool) = db_connection().await { + if let Err(e) = add_user( + &pool, + &user.email, + &user.username, + &hashed_password, + &user.group_id, + ) + .await + { + pool.close().await; + return e.to_string(); + }; + + pool.close().await; + } + + format!("User {} added", user.username) +} diff --git a/src/bin/ffpapi.rs b/src/bin/ffpapi.rs new file mode 100644 index 00000000..2789b987 --- /dev/null +++ b/src/bin/ffpapi.rs @@ -0,0 +1,109 @@ +use std::process::exit; + +use actix_web::{App, HttpServer}; +use clap::Parser; +use sha_crypt::{sha512_simple, Sha512Params}; +use simplelog::*; + +use ffplayout_engine::{ + api::{ + handles::{add_user, db_connection, db_init}, + routes::user, + }, + utils::{init_logging, GlobalConfig}, +}; + +#[derive(Parser, Debug)] +#[clap(version, + name = "ffpapi", + version = "0.1.0", + about = "ffplayout REST API", + long_about = None)] +pub struct Args { + #[clap(short, long, help = "Listen on IP:PORT, like: 127.0.0.1:8080")] + pub listen: Option, + + #[clap(short, long, help = "Initialize Database")] + pub init: bool, + + #[clap(short, long, help = "Create admin user")] + pub username: Option, + + #[clap(short, long, help = "Admin email")] + pub email: Option, + + #[clap(short, long, help = "Admin password")] + pub password: Option, +} + +#[actix_web::main] +async fn main() -> std::io::Result<()> { + let args = Args::parse(); + + if !args.init && args.listen.is_none() && args.username.is_none() { + error!("Wrong number of arguments! Run ffpapi --help for more information."); + + exit(1); + } + + let mut config = GlobalConfig::new(None); + config.mail.recipient = String::new(); + config.logging.log_to_file = false; + + let logging = init_logging(&config, None, None); + CombinedLogger::init(logging).unwrap(); + + if args.init { + if let Err(e) = db_init().await { + panic!("{e}"); + }; + + exit(0); + } + + if let Some(username) = args.username { + if args.email.is_none() || args.password.is_none() { + error!("Email/password missing!"); + exit(1); + } + + let params = Sha512Params::new(10_000).expect("RandomError!"); + + let hashed_password = + sha512_simple(&args.password.unwrap(), ¶ms).expect("Should not fail"); + + match db_connection().await { + Ok(pool) => { + if let Err(e) = + add_user(&pool, &args.email.unwrap(), &username, &hashed_password, &1).await + { + pool.close().await; + error!("{e}"); + exit(1); + }; + + pool.close().await; + info!("Create admin user \"{username}\" done..."); + + exit(0); + } + Err(e) => { + panic!("{e}") + } + } + } + + if let Some(conn) = args.listen { + let ip_port = conn.split(':').collect::>(); + let addr = ip_port[0]; + let port = ip_port[1].parse::().unwrap(); + info!("running ffplayout API, listen on {conn}"); + + HttpServer::new(|| App::new().service(user)) + .bind((addr, port))? + .run() + .await + } else { + panic!("Run ffpapi with listen parameter!") + } +} diff --git a/src/lib.rs b/src/lib.rs index 0738833b..89174366 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,7 @@ extern crate log; extern crate simplelog; +pub mod api; pub mod filter; pub mod input; pub mod macros; diff --git a/src/main.rs b/src/main.rs index 4feba587..da758623 100644 --- a/src/main.rs +++ b/src/main.rs @@ -14,8 +14,8 @@ use ffplayout_engine::{ output::{player, write_hls}, rpc::json_rpc_server, utils::{ - generate_playlist, init_logging, send_mail, validate_ffmpeg, GlobalConfig, PlayerControl, - PlayoutStatus, ProcessControl, + generate_playlist, get_args, init_logging, send_mail, validate_ffmpeg, GlobalConfig, + PlayerControl, PlayoutStatus, ProcessControl, }, }; @@ -56,7 +56,8 @@ fn status_file(stat_file: &str, playout_stat: &PlayoutStatus) { } fn main() { - let config = GlobalConfig::new(); + let args = get_args(); + let config = GlobalConfig::new(Some(args)); let config_clone = config.clone(); let play_control = PlayerControl::new(); let playout_stat = PlayoutStatus::new(); @@ -67,7 +68,7 @@ fn main() { let proc_ctl2 = proc_control.clone(); let messages = Arc::new(Mutex::new(Vec::new())); - let logging = init_logging(&config, proc_ctl1, messages.clone()); + let logging = init_logging(&config, Some(proc_ctl1), Some(messages.clone())); CombinedLogger::init(logging).unwrap(); validate_ffmpeg(&config); diff --git a/src/tests/mod.rs b/src/tests/mod.rs index d12fd964..859b2026 100644 --- a/src/tests/mod.rs +++ b/src/tests/mod.rs @@ -1,5 +1,4 @@ use std::{ - sync::{Arc, Mutex}, thread::{self, sleep}, time::Duration, }; @@ -22,26 +21,24 @@ fn timed_kill(sec: u64, mut proc_ctl: ProcessControl) { #[test] #[ignore] fn playlist_change_at_midnight() { - let mut config = GlobalConfig::new(); + let mut config = GlobalConfig::new(None); config.mail.recipient = "".into(); config.processing.mode = "playlist".into(); config.playlist.day_start = "00:00:00".into(); config.playlist.length = "24:00:00".into(); config.logging.log_to_file = false; - let messages = Arc::new(Mutex::new(Vec::new())); let play_control = PlayerControl::new(); let playout_stat = PlayoutStatus::new(); let proc_control = ProcessControl::new(); let proc_ctl = proc_control.clone(); - let proc_ctl2 = proc_control.clone(); - let logging = init_logging(&config, proc_ctl, messages); + let logging = init_logging(&config, None, None); CombinedLogger::init(logging).unwrap(); mock_time::set_mock_time("2022-05-09T23:59:45"); - thread::spawn(move || timed_kill(30, proc_ctl2)); + thread::spawn(move || timed_kill(30, proc_ctl)); player(&config, play_control, playout_stat, proc_control); } @@ -49,26 +46,24 @@ fn playlist_change_at_midnight() { #[test] #[ignore] fn playlist_change_at_six() { - let mut config = GlobalConfig::new(); + let mut config = GlobalConfig::new(None); config.mail.recipient = "".into(); config.processing.mode = "playlist".into(); config.playlist.day_start = "06:00:00".into(); config.playlist.length = "24:00:00".into(); config.logging.log_to_file = false; - let messages = Arc::new(Mutex::new(Vec::new())); let play_control = PlayerControl::new(); let playout_stat = PlayoutStatus::new(); let proc_control = ProcessControl::new(); let proc_ctl = proc_control.clone(); - let proc_ctl2 = proc_control.clone(); - let logging = init_logging(&config, proc_ctl, messages); + let logging = init_logging(&config, None, None); CombinedLogger::init(logging).unwrap(); mock_time::set_mock_time("2022-05-09T05:59:45"); - thread::spawn(move || timed_kill(30, proc_ctl2)); + thread::spawn(move || timed_kill(30, proc_ctl)); player(&config, play_control, playout_stat, proc_control); } diff --git a/src/tests/utils/mod.rs b/src/tests/utils/mod.rs index 2267bdfe..af2766b7 100644 --- a/src/tests/utils/mod.rs +++ b/src/tests/utils/mod.rs @@ -39,7 +39,7 @@ fn get_date_tomorrow() { #[test] fn test_delta() { - let mut config = GlobalConfig::new(); + let mut config = GlobalConfig::new(None); config.mail.recipient = "".into(); config.processing.mode = "playlist".into(); config.playlist.day_start = "00:00:00".into(); diff --git a/src/utils/arg_parse.rs b/src/utils/arg_parse.rs index f5c28ec2..ef0550ba 100644 --- a/src/utils/arg_parse.rs +++ b/src/utils/arg_parse.rs @@ -1,6 +1,6 @@ use clap::Parser; -#[derive(Parser, Debug)] +#[derive(Parser, Debug, Clone)] #[clap(version, about = "ffplayout, Rust based 24/7 playout solution.", override_usage = "Run without any command to use config file only, or with commands to override parameters:\n\n ffplayout [OPTIONS]", diff --git a/src/utils/config.rs b/src/utils/config.rs index 2593c5bf..8c291c24 100644 --- a/src/utils/config.rs +++ b/src/utils/config.rs @@ -8,7 +8,7 @@ use std::{ use serde::{Deserialize, Serialize}; use shlex::split; -use crate::utils::{get_args, time_to_sec}; +use crate::utils::{time_to_sec, Args}; use crate::vec_strings; /// Global Config @@ -136,11 +136,10 @@ pub struct Out { impl GlobalConfig { /// Read config from YAML file, and set some extra config values. - pub fn new() -> Self { - let args = get_args(); + pub fn new(args: Option) -> Self { let mut config_path = PathBuf::from("/etc/ffplayout/ffplayout.yml"); - if let Some(cfg) = args.config { + if let Some(cfg) = args.clone().and_then(|a| a.config) { config_path = PathBuf::from(cfg); } @@ -219,55 +218,57 @@ impl GlobalConfig { // Read command line arguments, and override the config with them. - if let Some(gen) = args.generate { - config.general.generate = Some(gen); - } - - if let Some(log_path) = args.log { - if Path::new(&log_path).is_dir() { - config.logging.log_to_file = true; + if let Some(arg) = args { + if let Some(gen) = arg.generate { + config.general.generate = Some(gen); } - config.logging.log_path = log_path; - } - if let Some(playlist) = args.playlist { - config.playlist.path = playlist; - } - - if let Some(mode) = args.play_mode { - config.processing.mode = mode; - } - - if let Some(folder) = args.folder { - config.storage.path = folder; - config.processing.mode = "folder".into(); - } - - if let Some(start) = args.start { - config.playlist.day_start = start.clone(); - config.playlist.start_sec = Some(time_to_sec(&start)); - } - - if let Some(length) = args.length { - config.playlist.length = length.clone(); - - if length.contains(':') { - config.playlist.length_sec = Some(time_to_sec(&length)); - } else { - config.playlist.length_sec = Some(86400.0); + if let Some(log_path) = arg.log { + if Path::new(&log_path).is_dir() { + config.logging.log_to_file = true; + } + config.logging.log_path = log_path; } - } - if args.infinit { - config.playlist.infinit = args.infinit; - } + if let Some(playlist) = arg.playlist { + config.playlist.path = playlist; + } - if let Some(output) = args.output { - config.out.mode = output; - } + if let Some(mode) = arg.play_mode { + config.processing.mode = mode; + } - if let Some(volume) = args.volume { - config.processing.volume = volume; + if let Some(folder) = arg.folder { + config.storage.path = folder; + config.processing.mode = "folder".into(); + } + + if let Some(start) = arg.start { + config.playlist.day_start = start.clone(); + config.playlist.start_sec = Some(time_to_sec(&start)); + } + + if let Some(length) = arg.length { + config.playlist.length = length.clone(); + + if length.contains(':') { + config.playlist.length_sec = Some(time_to_sec(&length)); + } else { + config.playlist.length_sec = Some(86400.0); + } + } + + if arg.infinit { + config.playlist.infinit = arg.infinit; + } + + if let Some(output) = arg.output { + config.out.mode = output; + } + + if let Some(volume) = arg.volume { + config.processing.volume = volume; + } } config @@ -276,7 +277,7 @@ impl GlobalConfig { impl Default for GlobalConfig { fn default() -> Self { - Self::new() + Self::new(None) } } diff --git a/src/utils/logging.rs b/src/utils/logging.rs index 88a379f4..2b853d52 100644 --- a/src/utils/logging.rs +++ b/src/utils/logging.rs @@ -167,8 +167,8 @@ fn clean_string(text: &str) -> String { /// - mail logger pub fn init_logging( config: &GlobalConfig, - proc_ctl: ProcessControl, - messages: Arc>>, + proc_ctl: Option, + messages: Option>>>, ) -> Vec> { let config_clone = config.clone(); let app_config = config.logging.clone(); @@ -182,6 +182,8 @@ pub fn init_logging( let mut log_config = ConfigBuilder::new() .set_thread_level(LevelFilter::Off) .set_target_level(LevelFilter::Off) + .add_filter_ignore_str("sqlx") + .add_filter_ignore_str("reqwest") .set_level_padding(LevelPadding::Left) .set_time_level(time_level) .clone(); @@ -247,10 +249,12 @@ pub fn init_logging( // set mail logger only the recipient is set in config if config.mail.recipient.contains('@') && config.mail.recipient.contains('.') { - let messages_clone = messages.clone(); + let messages_clone = messages.clone().unwrap(); let interval = config.mail.interval; - thread::spawn(move || mail_queue(config_clone, proc_ctl, messages_clone, interval)); + thread::spawn(move || { + mail_queue(config_clone, proc_ctl.unwrap(), messages_clone, interval) + }); let mail_config = log_config.build(); @@ -260,7 +264,7 @@ pub fn init_logging( _ => LevelFilter::Error, }; - app_logger.push(LogMailer::new(filter, mail_config, messages)); + app_logger.push(LogMailer::new(filter, mail_config, messages.unwrap())); } app_logger diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 5e01c6f7..232a6b68 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -23,7 +23,7 @@ pub mod json_serializer; mod json_validate; mod logging; -pub use arg_parse::get_args; +pub use arg_parse::{get_args, Args}; pub use config::GlobalConfig; pub use controller::{PlayerControl, PlayoutStatus, ProcessControl, ProcessUnit::*}; pub use generator::generate_playlist; From 49af829221376db3b0403e46ebe3b5147789874d Mon Sep 17 00:00:00 2001 From: jb-alvarado Date: Tue, 7 Jun 2022 18:11:46 +0200 Subject: [PATCH 02/47] work on login route --- src/api/args_parse.rs | 24 +++++++++++++ src/api/handles.rs | 26 ++++++++++++++ src/api/mod.rs | 2 ++ src/api/models.rs | 11 ++++-- src/api/routes.rs | 43 ++++++++++++++++++++--- src/api/utils.rs | 58 ++++++++++++++++++++++++++++++ src/bin/ffpapi.rs | 82 ++++++------------------------------------- 7 files changed, 166 insertions(+), 80 deletions(-) create mode 100644 src/api/args_parse.rs create mode 100644 src/api/utils.rs diff --git a/src/api/args_parse.rs b/src/api/args_parse.rs new file mode 100644 index 00000000..e7eb44f9 --- /dev/null +++ b/src/api/args_parse.rs @@ -0,0 +1,24 @@ +use clap::Parser; + +#[derive(Parser, Debug, Clone)] +#[clap(version, + name = "ffpapi", + version = "0.1.0", + about = "ffplayout REST API", + long_about = None)] +pub struct Args { + #[clap(short, long, help = "Listen on IP:PORT, like: 127.0.0.1:8080")] + pub listen: Option, + + #[clap(short, long, help = "Initialize Database")] + pub init: bool, + + #[clap(short, long, help = "Create admin user")] + pub username: Option, + + #[clap(short, long, help = "Admin email")] + pub email: Option, + + #[clap(short, long, help = "Admin password")] + pub password: Option, +} diff --git a/src/api/handles.rs b/src/api/handles.rs index 67b9b546..6b855c8e 100644 --- a/src/api/handles.rs +++ b/src/api/handles.rs @@ -4,6 +4,8 @@ use faccess::PathExt; use simplelog::*; use sqlx::{migrate::MigrateDatabase, sqlite::SqliteQueryResult, Pool, Sqlite, SqlitePool}; +use crate::api::models::User; + pub fn db_path() -> Result> { let sys_path = Path::new("/usr/share/ffplayout"); let mut db_path = String::from("./ffplayout.db"); @@ -100,3 +102,27 @@ pub async fn add_user( Ok(result) } + +pub async fn get_users( + instances: &SqlitePool, + index: Option, +) -> Result, sqlx::Error> { + let query = match index { + Some(i) => format!("SELECT id, email, username FROM user WHERE id = {i}"), + None => "SELECT id, email, username FROM user".to_string(), + }; + + let result: Vec = sqlx::query_as(&query).fetch_all(instances).await?; + instances.close().await; + + Ok(result) +} + +pub async fn get_login(user: &str) -> Result, sqlx::Error> { + let pool = db_connection().await?; + let query = "SELECT id, username, password FROM user WHERE username = $1"; + let result: Vec = sqlx::query_as(query).bind(user).fetch_all(&pool).await?; + pool.close().await; + + Ok(result) +} diff --git a/src/api/mod.rs b/src/api/mod.rs index 2ce89ab9..293e5f89 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -1,3 +1,5 @@ +pub mod args_parse; pub mod handles; pub mod models; pub mod routes; +pub mod utils; diff --git a/src/api/models.rs b/src/api/models.rs index feaad65a..c0e28ada 100644 --- a/src/api/models.rs +++ b/src/api/models.rs @@ -1,15 +1,20 @@ use serde::{Deserialize, Serialize}; -#[derive(Debug, Deserialize, Serialize)] +#[derive(Debug, Deserialize, Serialize, sqlx::FromRow)] pub struct User { - pub email: String, + pub id: Option, + #[sqlx(default)] + pub email: Option, pub username: String, + #[sqlx(default)] pub password: String, - pub group_id: i64, + #[sqlx(default)] + pub group_id: Option, } #[derive(Debug, Deserialize, Serialize)] pub struct Settings { + pub id: i64, pub channel_name: String, pub preview_url: String, pub settings_path: String, diff --git a/src/api/routes.rs b/src/api/routes.rs index 39c135ee..63e78bde 100644 --- a/src/api/routes.rs +++ b/src/api/routes.rs @@ -1,9 +1,9 @@ use crate::api::{ - handles::{add_user, db_connection}, + handles::{add_user, db_connection, get_login, get_users}, models::User, }; use actix_web::{get, post, web, Responder}; -use sha_crypt::{sha512_simple, Sha512Params}; +use sha_crypt::{sha512_check, sha512_simple, Sha512Params}; #[get("/hello/{name}")] async fn greet(name: web::Path) -> impl Responder { @@ -14,7 +14,6 @@ async fn greet(name: web::Path) -> impl Responder { #[post("/api/user/")] pub async fn user(user: web::Json) -> impl Responder { let params = Sha512Params::new(10_000).expect("RandomError!"); - let hashed_password = sha512_simple(&user.password, ¶ms).expect("Should not fail"); // // Verifying a stored password @@ -23,10 +22,10 @@ pub async fn user(user: web::Json) -> impl Responder { if let Ok(pool) = db_connection().await { if let Err(e) = add_user( &pool, - &user.email, + &user.email.clone().unwrap(), &user.username, &hashed_password, - &user.group_id, + &user.group_id.unwrap(), ) .await { @@ -39,3 +38,37 @@ pub async fn user(user: web::Json) -> impl Responder { format!("User {} added", user.username) } + +#[get("/api/user/{id}")] +pub async fn get_user(id: web::Path) -> impl Responder { + if let Ok(pool) = db_connection().await { + match get_users(&pool, Some(*id)).await { + Ok(r) => { + return web::Json(r); + } + Err(_) => { + return web::Json(vec![]); + } + }; + } + + web::Json(vec![]) +} + +#[post("/auth/login/")] +pub async fn login(credentials: web::Json) -> impl Responder { + let params = Sha512Params::new(10_000).expect("RandomError!"); + let hashed_password = sha512_simple(&credentials.password, ¶ms).expect("Should not fail"); + + println!("{hashed_password}"); + + if let Ok(u) = get_login(&credentials.username).await { + println!("{}", &u[0].password); + println!("{:?}", sha512_check(&u[0].password, &hashed_password)); + if !u.is_empty() && sha512_check(&u[0].password, &hashed_password).is_ok() { + return "login correct!"; + } + }; + + "Login failed!" +} diff --git a/src/api/utils.rs b/src/api/utils.rs new file mode 100644 index 00000000..4f7eafe6 --- /dev/null +++ b/src/api/utils.rs @@ -0,0 +1,58 @@ +use sha_crypt::{sha512_simple, Sha512Params}; +use simplelog::*; + +use crate::api::{ + args_parse::Args, + handles::{add_user, db_connection, db_init}, +}; + +pub async fn run_args(args: Args) -> Result<(), i32> { + if !args.init && args.listen.is_none() && args.username.is_none() { + error!("Wrong number of arguments! Run ffpapi --help for more information."); + + return Err(0); + } + + if args.init { + if let Err(e) = db_init().await { + panic!("{e}"); + }; + + return Err(0); + } + + if let Some(username) = args.username { + if args.email.is_none() || args.password.is_none() { + error!("Email/password missing!"); + return Err(1); + } + + let params = Sha512Params::new(10_000).expect("RandomError!"); + + let hashed_password = + sha512_simple(&args.password.unwrap(), ¶ms).expect("Should not fail"); + + match db_connection().await { + Ok(pool) => { + if let Err(e) = + add_user(&pool, &args.email.unwrap(), &username, &hashed_password, &1).await + { + pool.close().await; + error!("{e}"); + return Err(1); + }; + + pool.close().await; + info!("Create admin user \"{username}\" done..."); + + return Err(0); + } + Err(e) => { + error!("Add admin user failed! Did you init the database?"); + panic!("{e}") + } + } + } + + Ok(()) +} diff --git a/src/bin/ffpapi.rs b/src/bin/ffpapi.rs index 2789b987..7ad3663b 100644 --- a/src/bin/ffpapi.rs +++ b/src/bin/ffpapi.rs @@ -2,95 +2,31 @@ use std::process::exit; use actix_web::{App, HttpServer}; use clap::Parser; -use sha_crypt::{sha512_simple, Sha512Params}; use simplelog::*; use ffplayout_engine::{ api::{ - handles::{add_user, db_connection, db_init}, - routes::user, + args_parse::Args, + routes::{get_user, login, user}, + utils::run_args, }, utils::{init_logging, GlobalConfig}, }; -#[derive(Parser, Debug)] -#[clap(version, - name = "ffpapi", - version = "0.1.0", - about = "ffplayout REST API", - long_about = None)] -pub struct Args { - #[clap(short, long, help = "Listen on IP:PORT, like: 127.0.0.1:8080")] - pub listen: Option, - - #[clap(short, long, help = "Initialize Database")] - pub init: bool, - - #[clap(short, long, help = "Create admin user")] - pub username: Option, - - #[clap(short, long, help = "Admin email")] - pub email: Option, - - #[clap(short, long, help = "Admin password")] - pub password: Option, -} - #[actix_web::main] async fn main() -> std::io::Result<()> { let args = Args::parse(); - if !args.init && args.listen.is_none() && args.username.is_none() { - error!("Wrong number of arguments! Run ffpapi --help for more information."); - - exit(1); - } - let mut config = GlobalConfig::new(None); config.mail.recipient = String::new(); config.logging.log_to_file = false; + config.logging.timestamp = false; let logging = init_logging(&config, None, None); CombinedLogger::init(logging).unwrap(); - if args.init { - if let Err(e) = db_init().await { - panic!("{e}"); - }; - - exit(0); - } - - if let Some(username) = args.username { - if args.email.is_none() || args.password.is_none() { - error!("Email/password missing!"); - exit(1); - } - - let params = Sha512Params::new(10_000).expect("RandomError!"); - - let hashed_password = - sha512_simple(&args.password.unwrap(), ¶ms).expect("Should not fail"); - - match db_connection().await { - Ok(pool) => { - if let Err(e) = - add_user(&pool, &args.email.unwrap(), &username, &hashed_password, &1).await - { - pool.close().await; - error!("{e}"); - exit(1); - }; - - pool.close().await; - info!("Create admin user \"{username}\" done..."); - - exit(0); - } - Err(e) => { - panic!("{e}") - } - } + if let Err(c) = run_args(args.clone()).await { + exit(c); } if let Some(conn) = args.listen { @@ -99,11 +35,13 @@ async fn main() -> std::io::Result<()> { let port = ip_port[1].parse::().unwrap(); info!("running ffplayout API, listen on {conn}"); - HttpServer::new(|| App::new().service(user)) + HttpServer::new(|| App::new().service(get_user).service(login).service(user)) .bind((addr, port))? .run() .await } else { - panic!("Run ffpapi with listen parameter!") + error!("Run ffpapi with listen parameter!"); + + Ok(()) } } From 58b0df57573fe745c35f58c33c80825a64e76105 Mon Sep 17 00:00:00 2001 From: jb-alvarado Date: Tue, 7 Jun 2022 22:05:35 +0200 Subject: [PATCH 03/47] add login --- Cargo.lock | 52 +++++++++++++++++++++++------- Cargo.toml | 3 +- src/api/handles.rs | 8 +++-- src/api/models.rs | 2 ++ src/api/routes.rs | 79 +++++++++++++++++++++++++--------------------- src/api/utils.rs | 29 +++++++++++++---- src/bin/ffpapi.rs | 4 +-- 7 files changed, 118 insertions(+), 59 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a5e4bade..5be73738 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -222,6 +222,17 @@ dependencies = [ "alloc-no-stdlib", ] +[[package]] +name = "argon2" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a27e27b63e4a34caee411ade944981136fdfa535522dc9944d6700196cbd899f" +dependencies = [ + "base64ct", + "blake2", + "password-hash", +] + [[package]] name = "atoi" version = "0.4.0" @@ -254,12 +265,27 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" +[[package]] +name = "base64ct" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dea908e7347a8c64e378c17e30ef880ad73e3b4498346b055c2c00ea342f3179" + [[package]] name = "bitflags" version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "blake2" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9cf849ee05b2ee5fba5e36f97ff8ec2533916700fc0758d40d92136a42f3388" +dependencies = [ + "digest", +] + [[package]] name = "block-buffer" version = "0.10.2" @@ -537,6 +563,7 @@ checksum = "f2fb860ca6fafa5552fb6d0e816a69c8e49f0908bf524e30a90d97c85892d506" dependencies = [ "block-buffer", "crypto-common", + "subtle", ] [[package]] @@ -600,6 +627,7 @@ name = "ffplayout-engine" version = "0.9.8" dependencies = [ "actix-web", + "argon2", "chrono 0.4.19 (git+https://github.com/sbrocket/chrono?branch=parse-error-kind-public)", "clap", "crossbeam-channel", @@ -612,12 +640,12 @@ dependencies = [ "notify", "openssl", "rand", + "rand_core", "regex", "reqwest", "serde", "serde_json", "serde_yaml", - "sha-crypt", "shlex", "simplelog", "sqlx", @@ -1593,6 +1621,17 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "password-hash" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e029e94abc8fb0065241c308f1ac6bc8d20f450e8f7c5f0b25cd9b8d526ba294" +dependencies = [ + "base64ct", + "rand_core", + "subtle", +] + [[package]] name = "paste" version = "1.0.7" @@ -1933,17 +1972,6 @@ dependencies = [ "digest", ] -[[package]] -name = "sha-crypt" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0193e80e8a92aa7173dad160cfd5f5eda57ba146aac9826ea5c9dc3d5492072" -dependencies = [ - "rand", - "sha2", - "subtle", -] - [[package]] name = "sha2" version = "0.10.2" diff --git a/Cargo.toml b/Cargo.toml index 3396a03e..2184aeb3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,7 @@ default-run = "ffplayout" [dependencies] actix-web = "4" +argon2 = "0.4" chrono = { git = "https://github.com/sbrocket/chrono", branch = "parse-error-kind-public" } clap = { version = "3.1", features = ["derive"] } crossbeam-channel = "0.5" @@ -21,9 +22,9 @@ lettre = "0.10.0-rc.6" log = "0.4" notify = "4.0" rand = "0.8" +rand_core = { version = "0.6", features = ["std"] } regex = "1" reqwest = { version = "0.11", features = ["blocking"] } -sha-crypt = { version = "0.4", features = ["simple"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" serde_yaml = "0.8" diff --git a/src/api/handles.rs b/src/api/handles.rs index 6b855c8e..3ee3fecc 100644 --- a/src/api/handles.rs +++ b/src/api/handles.rs @@ -43,6 +43,7 @@ async fn cretea_schema() -> Result { email TEXT NOT NULL, username TEXT NOT NULL, password TEXT NOT NULL, + salt TEXT NOT NULL, group_id INTEGER NOT NULL DEFAULT 2, FOREIGN KEY (group_id) REFERENCES groups (id) ON UPDATE SET NULL ON DELETE SET NULL, UNIQUE(email, username) @@ -89,13 +90,16 @@ pub async fn add_user( mail: &str, user: &str, pass: &str, + salt: &str, group: &i64, ) -> Result { - let query = "INSERT INTO user (email, username, password, group_id) VALUES($1, $2, $3, $4)"; + let query = + "INSERT INTO user (email, username, password, salt, group_id) VALUES($1, $2, $3, $4, $5)"; let result = sqlx::query(query) .bind(mail) .bind(user) .bind(pass) + .bind(salt) .bind(group) .execute(instances) .await?; @@ -120,7 +124,7 @@ pub async fn get_users( pub async fn get_login(user: &str) -> Result, sqlx::Error> { let pool = db_connection().await?; - let query = "SELECT id, username, password FROM user WHERE username = $1"; + let query = "SELECT id, username, password, salt FROM user WHERE username = $1"; let result: Vec = sqlx::query_as(query).bind(user).fetch_all(&pool).await?; pool.close().await; diff --git a/src/api/models.rs b/src/api/models.rs index c0e28ada..026baa2e 100644 --- a/src/api/models.rs +++ b/src/api/models.rs @@ -9,6 +9,8 @@ pub struct User { #[sqlx(default)] pub password: String, #[sqlx(default)] + pub salt: Option, + #[sqlx(default)] pub group_id: Option, } diff --git a/src/api/routes.rs b/src/api/routes.rs index 63e78bde..993d85c4 100644 --- a/src/api/routes.rs +++ b/src/api/routes.rs @@ -1,44 +1,46 @@ use crate::api::{ - handles::{add_user, db_connection, get_login, get_users}, + handles::{db_connection, get_login, get_users}, models::User, }; use actix_web::{get, post, web, Responder}; -use sha_crypt::{sha512_check, sha512_simple, Sha512Params}; +use argon2::{password_hash::PasswordHash, Argon2, PasswordVerifier}; +use simplelog::*; #[get("/hello/{name}")] async fn greet(name: web::Path) -> impl Responder { format!("Hello {name}!") } -/// curl -X POST -H "Content-Type: application/json" -d '{"username": "USER", "password": "abc123", "email":"user@example.org" }' http://127.0.0.1:8080/api/user/ -#[post("/api/user/")] -pub async fn user(user: web::Json) -> impl Responder { - let params = Sha512Params::new(10_000).expect("RandomError!"); - let hashed_password = sha512_simple(&user.password, ¶ms).expect("Should not fail"); +// /// curl -X POST -H "Content-Type: application/json" -d '{"username": "USER", "password": "abc123", "email":"user@example.org" }' http://127.0.0.1:8080/api/user/ +// #[post("/api/user/")] +// pub async fn user(user: web::Json) -> impl Responder { +// let params = Sha512Params::new(10_000).expect("RandomError!"); +// let hashed_password = sha512_simple(&user.password, ¶ms).expect("Should not fail"); - // // Verifying a stored password - // assert!(sha512_check("Not so secure password", &hashed_password).is_ok()); +// // // Verifying a stored password +// // assert!(sha512_check("Not so secure password", &hashed_password).is_ok()); - if let Ok(pool) = db_connection().await { - if let Err(e) = add_user( - &pool, - &user.email.clone().unwrap(), - &user.username, - &hashed_password, - &user.group_id.unwrap(), - ) - .await - { - pool.close().await; - return e.to_string(); - }; +// if let Ok(pool) = db_connection().await { +// if let Err(e) = add_user( +// &pool, +// &user.email.clone().unwrap(), +// &user.username, +// &hashed_password, +// &user.group_id.unwrap(), +// ) +// .await +// { +// pool.close().await; +// return e.to_string(); +// }; - pool.close().await; - } +// pool.close().await; +// } - format!("User {} added", user.username) -} +// format!("User {} added", user.username) +// } +/// curl -X GET http://127.0.0.1:8080/api/user/1 #[get("/api/user/{id}")] pub async fn get_user(id: web::Path) -> impl Responder { if let Ok(pool) = db_connection().await { @@ -54,21 +56,26 @@ pub async fn get_user(id: web::Path) -> impl Responder { web::Json(vec![]) } - +/// curl -X POST -H "Content-Type: application/json" -d '{"username": "USER", "password": "abc123" }' http://127.0.0.1:8080/auth/login/ #[post("/auth/login/")] pub async fn login(credentials: web::Json) -> impl Responder { - let params = Sha512Params::new(10_000).expect("RandomError!"); - let hashed_password = sha512_simple(&credentials.password, ¶ms).expect("Should not fail"); - - println!("{hashed_password}"); - if let Ok(u) = get_login(&credentials.username).await { - println!("{}", &u[0].password); - println!("{:?}", sha512_check(&u[0].password, &hashed_password)); - if !u.is_empty() && sha512_check(&u[0].password, &hashed_password).is_ok() { - return "login correct!"; + if u.is_empty() { + return "User not found"; } + let pass = u[0].password.clone(); + + if let Ok(hash) = PasswordHash::new(&pass) { + if Argon2::default() + .verify_password(credentials.password.as_bytes(), &hash) + .is_ok() + { + info!("user {} login", credentials.username); + return "login correct!"; + } + }; }; + error!("Login {} failed!", credentials.username); "Login failed!" } diff --git a/src/api/utils.rs b/src/api/utils.rs index 4f7eafe6..1ed94a47 100644 --- a/src/api/utils.rs +++ b/src/api/utils.rs @@ -1,4 +1,7 @@ -use sha_crypt::{sha512_simple, Sha512Params}; +use argon2::{ + password_hash::{rand_core::OsRng, PasswordHasher, SaltString}, + Argon2, +}; use simplelog::*; use crate::api::{ @@ -27,15 +30,29 @@ pub async fn run_args(args: Args) -> Result<(), i32> { return Err(1); } - let params = Sha512Params::new(10_000).expect("RandomError!"); + let salt = SaltString::generate(&mut OsRng); + let argon2 = Argon2::default(); + let password = args.password.unwrap(); - let hashed_password = - sha512_simple(&args.password.unwrap(), ¶ms).expect("Should not fail"); + let password_hash = match argon2.hash_password(password.as_bytes(), &salt) { + Ok(hash) => hash.to_string(), + Err(e) => { + error!("{e}"); + return Err(1); + } + }; match db_connection().await { Ok(pool) => { - if let Err(e) = - add_user(&pool, &args.email.unwrap(), &username, &hashed_password, &1).await + if let Err(e) = add_user( + &pool, + &args.email.unwrap(), + &username, + &password_hash.to_string(), + &salt.to_string(), + &1, + ) + .await { pool.close().await; error!("{e}"); diff --git a/src/bin/ffpapi.rs b/src/bin/ffpapi.rs index 7ad3663b..cc12d75d 100644 --- a/src/bin/ffpapi.rs +++ b/src/bin/ffpapi.rs @@ -7,7 +7,7 @@ use simplelog::*; use ffplayout_engine::{ api::{ args_parse::Args, - routes::{get_user, login, user}, + routes::{get_user, login}, utils::run_args, }, utils::{init_logging, GlobalConfig}, @@ -35,7 +35,7 @@ async fn main() -> std::io::Result<()> { let port = ip_port[1].parse::().unwrap(); info!("running ffplayout API, listen on {conn}"); - HttpServer::new(|| App::new().service(get_user).service(login).service(user)) + HttpServer::new(|| App::new().service(get_user).service(login)) .bind((addr, port))? .run() .await From cca5e24cc9dbd1b38b29255e04bb3fc97ad07fb8 Mon Sep 17 00:00:00 2001 From: jb-alvarado Date: Wed, 8 Jun 2022 18:06:40 +0200 Subject: [PATCH 04/47] cleanup code, reponse object --- Cargo.lock | 76 ++++++++++++++++++++++++++++++++++- Cargo.toml | 8 +++- src/api/handles.rs | 56 ++++++++++++-------------- src/api/models.rs | 9 ++++- src/api/routes.rs | 98 ++++++++++++++++++---------------------------- src/api/utils.rs | 41 +++++++------------ src/bin/ffpapi.rs | 8 +--- 7 files changed, 168 insertions(+), 128 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5be73738..1442b7da 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -635,6 +635,7 @@ dependencies = [ "ffprobe", "file-rotate", "jsonrpc-http-server", + "jsonwebtoken", "lettre", "log", "notify", @@ -709,7 +710,7 @@ dependencies = [ "futures-core", "futures-sink", "pin-project", - "spin", + "spin 0.9.3", ] [[package]] @@ -1202,6 +1203,20 @@ dependencies = [ "unicase", ] +[[package]] +name = "jsonwebtoken" +version = "8.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc9051c17f81bae79440afa041b3a278e1de71bfb96d32454b477fd4703ccb6f" +dependencies = [ + "base64", + "pem", + "ring", + "serde", + "serde_json", + "simple_asn1", +] + [[package]] name = "kernel32-sys" version = "0.2.2" @@ -1462,6 +1477,17 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "num-bigint" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f93ab6289c7b344a8a9f60f88d80aa20032336fe78da341afc91c8a2341fc75f" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + [[package]] name = "num-integer" version = "0.1.45" @@ -1638,6 +1664,15 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c520e05135d6e763148b6426a837e239041653ba7becd2e538c076c738025fc" +[[package]] +name = "pem" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9a3b09a20e374558580a4914d3b7d89bd61b954a5a5e1dcbea98753addb1947" +dependencies = [ + "base64", +] + [[package]] name = "percent-encoding" version = "2.1.0" @@ -1837,6 +1872,21 @@ dependencies = [ "winreg", ] +[[package]] +name = "ring" +version = "0.16.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" +dependencies = [ + "cc", + "libc", + "once_cell", + "spin 0.5.2", + "untrusted", + "web-sys", + "winapi 0.3.9", +] + [[package]] name = "rustc_version" version = "0.4.0" @@ -1998,6 +2048,18 @@ dependencies = [ "libc", ] +[[package]] +name = "simple_asn1" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc4e5204eb1910f40f9cfa375f6f05b68c3abac4b6fd879c8ff5e7ae8a0a085" +dependencies = [ + "num-bigint", + "num-traits", + "thiserror", + "time 0.3.9", +] + [[package]] name = "simplelog" version = "0.12.0" @@ -2032,6 +2094,12 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + [[package]] name = "spin" version = "0.9.3" @@ -2424,6 +2492,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + [[package]] name = "url" version = "2.2.2" diff --git a/Cargo.toml b/Cargo.toml index 2184aeb3..f9c9d48b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,7 @@ faccess = "0.2" ffprobe = "0.3" file-rotate = { git = "https://github.com/Ploppz/file-rotate.git", branch = "timestamp-parse-fix" } jsonrpc-http-server = "18.0" +jsonwebtoken = "8" lettre = "0.10.0-rc.6" log = "0.4" notify = "4.0" @@ -30,8 +31,11 @@ serde_json = "1.0" serde_yaml = "0.8" shlex = "1.1" simplelog = { version = "^0.12", features = ["paris"] } - -sqlx = { version = "0.5", features = [ "chrono", "runtime-actix-native-tls", "sqlite" ] } +sqlx = { version = "0.5", features = [ + "chrono", + "runtime-actix-native-tls", + "sqlite" +] } time = { version = "0.3", features = ["formatting", "macros"] } walkdir = "2" diff --git a/src/api/handles.rs b/src/api/handles.rs index 3ee3fecc..400b16d4 100644 --- a/src/api/handles.rs +++ b/src/api/handles.rs @@ -1,6 +1,7 @@ use std::path::Path; use faccess::PathExt; +use rand::{distributions::Alphanumeric, Rng}; use simplelog::*; use sqlx::{migrate::MigrateDatabase, sqlite::SqliteQueryResult, Pool, Sqlite, SqlitePool}; @@ -20,7 +21,7 @@ pub fn db_path() -> Result> { } async fn cretea_schema() -> Result { - let pool = db_connection().await?; + let conn = db_connection().await?; let query = "PRAGMA foreign_keys = ON; CREATE TABLE IF NOT EXISTS groups ( @@ -35,6 +36,7 @@ async fn cretea_schema() -> Result { preview_url TEXT NOT NULL, settings_path TEXT NOT NULL, extra_extensions TEXT NOT NULL, + secret TEXT NOT NULL, UNIQUE(channel_name) ); CREATE TABLE IF NOT EXISTS user @@ -48,8 +50,8 @@ async fn cretea_schema() -> Result { FOREIGN KEY (group_id) REFERENCES groups (id) ON UPDATE SET NULL ON DELETE SET NULL, UNIQUE(email, username) );"; - let result = sqlx::query(query).execute(&pool).await; - pool.close().await; + let result = sqlx::query(query).execute(&conn).await; + conn.close().await; result } @@ -64,14 +66,19 @@ pub async fn db_init() -> Result<&'static str, Box> { Err(e) => panic!("{e}"), } } + let secret: String = rand::thread_rng() + .sample_iter(&Alphanumeric) + .take(80) + .map(char::from) + .collect(); + let instances = db_connection().await?; let query = "INSERT INTO groups(name) VALUES('admin'), ('user'); - INSERT INTO settings(channel_name, preview_url, settings_path, extra_extensions) + INSERT INTO settings(channel_name, preview_url, settings_path, extra_extensions, secret) VALUES('Channel 1', 'http://localhost/live/preview.m3u8', - '/etc/ffplayout/ffplayout.yml', '.jpg,.jpeg,.png');"; - sqlx::query(query).execute(&instances).await?; - + '/etc/ffplayout/ffplayout.yml', '.jpg,.jpeg,.png', $1);"; + sqlx::query(query).bind(secret).execute(&instances).await?; instances.close().await; Ok("Database initialized!") @@ -79,20 +86,19 @@ pub async fn db_init() -> Result<&'static str, Box> { pub async fn db_connection() -> Result, sqlx::Error> { let db_path = db_path().unwrap(); + let conn = SqlitePool::connect(&db_path).await?; - let pool = SqlitePool::connect(&db_path).await?; - - Ok(pool) + Ok(conn) } pub async fn add_user( - instances: &SqlitePool, mail: &str, user: &str, pass: &str, salt: &str, group: &i64, ) -> Result { + let conn = db_connection().await?; let query = "INSERT INTO user (email, username, password, salt, group_id) VALUES($1, $2, $3, $4, $5)"; let result = sqlx::query(query) @@ -101,32 +107,18 @@ pub async fn add_user( .bind(pass) .bind(salt) .bind(group) - .execute(instances) + .execute(&conn) .await?; + conn.close().await; Ok(result) } -pub async fn get_users( - instances: &SqlitePool, - index: Option, -) -> Result, sqlx::Error> { - let query = match index { - Some(i) => format!("SELECT id, email, username FROM user WHERE id = {i}"), - None => "SELECT id, email, username FROM user".to_string(), - }; - - let result: Vec = sqlx::query_as(&query).fetch_all(instances).await?; - instances.close().await; - - Ok(result) -} - -pub async fn get_login(user: &str) -> Result, sqlx::Error> { - let pool = db_connection().await?; - let query = "SELECT id, username, password, salt FROM user WHERE username = $1"; - let result: Vec = sqlx::query_as(query).bind(user).fetch_all(&pool).await?; - pool.close().await; +pub async fn get_login(user: &str) -> Result { + let conn = db_connection().await?; + let query = "SELECT id, email, username, password, salt FROM user WHERE username = $1"; + let result: User = sqlx::query_as(query).bind(user).fetch_one(&conn).await?; + conn.close().await; Ok(result) } diff --git a/src/api/models.rs b/src/api/models.rs index 026baa2e..3bf15f1d 100644 --- a/src/api/models.rs +++ b/src/api/models.rs @@ -2,23 +2,30 @@ use serde::{Deserialize, Serialize}; #[derive(Debug, Deserialize, Serialize, sqlx::FromRow)] pub struct User { + #[sqlx(default)] pub id: Option, #[sqlx(default)] pub email: Option, pub username: String, #[sqlx(default)] + #[serde(skip_serializing)] pub password: String, #[sqlx(default)] + #[serde(skip_serializing)] pub salt: Option, #[sqlx(default)] + #[serde(skip_serializing)] pub group_id: Option, } -#[derive(Debug, Deserialize, Serialize)] +#[derive(Debug, Deserialize, Serialize, sqlx::FromRow)] pub struct Settings { pub id: i64, pub channel_name: String, pub preview_url: String, pub settings_path: String, pub extra_extensions: String, + #[sqlx(default)] + #[serde(skip_serializing)] + pub secret: String, } diff --git a/src/api/routes.rs b/src/api/routes.rs index 993d85c4..1b3a0c45 100644 --- a/src/api/routes.rs +++ b/src/api/routes.rs @@ -1,81 +1,59 @@ -use crate::api::{ - handles::{db_connection, get_login, get_users}, - models::User, -}; use actix_web::{get, post, web, Responder}; use argon2::{password_hash::PasswordHash, Argon2, PasswordVerifier}; +use serde::Serialize; use simplelog::*; +use crate::api::{handles::get_login, models::User}; + #[get("/hello/{name}")] async fn greet(name: web::Path) -> impl Responder { format!("Hello {name}!") } -// /// curl -X POST -H "Content-Type: application/json" -d '{"username": "USER", "password": "abc123", "email":"user@example.org" }' http://127.0.0.1:8080/api/user/ -// #[post("/api/user/")] -// pub async fn user(user: web::Json) -> impl Responder { -// let params = Sha512Params::new(10_000).expect("RandomError!"); -// let hashed_password = sha512_simple(&user.password, ¶ms).expect("Should not fail"); - -// // // Verifying a stored password -// // assert!(sha512_check("Not so secure password", &hashed_password).is_ok()); - -// if let Ok(pool) = db_connection().await { -// if let Err(e) = add_user( -// &pool, -// &user.email.clone().unwrap(), -// &user.username, -// &hashed_password, -// &user.group_id.unwrap(), -// ) -// .await -// { -// pool.close().await; -// return e.to_string(); -// }; - -// pool.close().await; -// } - -// format!("User {} added", user.username) -// } - -/// curl -X GET http://127.0.0.1:8080/api/user/1 -#[get("/api/user/{id}")] -pub async fn get_user(id: web::Path) -> impl Responder { - if let Ok(pool) = db_connection().await { - match get_users(&pool, Some(*id)).await { - Ok(r) => { - return web::Json(r); - } - Err(_) => { - return web::Json(vec![]); - } - }; - } - - web::Json(vec![]) +#[derive(Serialize)] +struct ResponseObj { + message: String, + status: i32, + data: Option, } + /// curl -X POST -H "Content-Type: application/json" -d '{"username": "USER", "password": "abc123" }' http://127.0.0.1:8080/auth/login/ #[post("/auth/login/")] pub async fn login(credentials: web::Json) -> impl Responder { - if let Ok(u) = get_login(&credentials.username).await { - if u.is_empty() { - return "User not found"; - } - let pass = u[0].password.clone(); + match get_login(&credentials.username).await { + Ok(mut user) => { + let pass = user.password.clone(); + user.password = "".into(); + user.salt = None; - if let Ok(hash) = PasswordHash::new(&pass) { + let hash = PasswordHash::new(&pass).unwrap(); if Argon2::default() .verify_password(credentials.password.as_bytes(), &hash) .is_ok() { info!("user {} login", credentials.username); - return "login correct!"; - } - }; - }; - error!("Login {} failed!", credentials.username); - "Login failed!" + web::Json(ResponseObj { + message: "login correct!".into(), + status: 200, + data: Some(user), + }) + } else { + error!("Wrong password for {}!", credentials.username); + web::Json(ResponseObj { + message: "Wrong password!".into(), + status: 401, + data: None, + }) + } + } + Err(e) => { + error!("Login {} failed! {e}", credentials.username); + return web::Json(ResponseObj { + message: format!("Login {} failed!", credentials.username), + status: 404, + data: None, + }); + } + } } diff --git a/src/api/utils.rs b/src/api/utils.rs index 1ed94a47..bc5b522d 100644 --- a/src/api/utils.rs +++ b/src/api/utils.rs @@ -6,7 +6,7 @@ use simplelog::*; use crate::api::{ args_parse::Args, - handles::{add_user, db_connection, db_init}, + handles::{add_user, db_init}, }; pub async fn run_args(args: Args) -> Result<(), i32> { @@ -42,33 +42,22 @@ pub async fn run_args(args: Args) -> Result<(), i32> { } }; - match db_connection().await { - Ok(pool) => { - if let Err(e) = add_user( - &pool, - &args.email.unwrap(), - &username, - &password_hash.to_string(), - &salt.to_string(), - &1, - ) - .await - { - pool.close().await; - error!("{e}"); - return Err(1); - }; + if let Err(e) = add_user( + &args.email.unwrap(), + &username, + &password_hash.to_string(), + &salt.to_string(), + &1, + ) + .await + { + error!("{e}"); + return Err(1); + }; - pool.close().await; - info!("Create admin user \"{username}\" done..."); + info!("Create admin user \"{username}\" done..."); - return Err(0); - } - Err(e) => { - error!("Add admin user failed! Did you init the database?"); - panic!("{e}") - } - } + return Err(0); } Ok(()) diff --git a/src/bin/ffpapi.rs b/src/bin/ffpapi.rs index cc12d75d..7bb75d55 100644 --- a/src/bin/ffpapi.rs +++ b/src/bin/ffpapi.rs @@ -5,11 +5,7 @@ use clap::Parser; use simplelog::*; use ffplayout_engine::{ - api::{ - args_parse::Args, - routes::{get_user, login}, - utils::run_args, - }, + api::{args_parse::Args, routes::login, utils::run_args}, utils::{init_logging, GlobalConfig}, }; @@ -35,7 +31,7 @@ async fn main() -> std::io::Result<()> { let port = ip_port[1].parse::().unwrap(); info!("running ffplayout API, listen on {conn}"); - HttpServer::new(|| App::new().service(get_user).service(login)) + HttpServer::new(|| App::new().service(login)) .bind((addr, port))? .run() .await From cac4e2562039ab5e247f3c6362210e7cd1461e7e Mon Sep 17 00:00:00 2001 From: jb-alvarado Date: Wed, 8 Jun 2022 18:16:58 +0200 Subject: [PATCH 05/47] add status --- src/api/routes.rs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/api/routes.rs b/src/api/routes.rs index 1b3a0c45..1f4dbcb2 100644 --- a/src/api/routes.rs +++ b/src/api/routes.rs @@ -1,4 +1,4 @@ -use actix_web::{get, post, web, Responder}; +use actix_web::{get, http::StatusCode, post, web, Responder}; use argon2::{password_hash::PasswordHash, Argon2, PasswordVerifier}; use serde::Serialize; use simplelog::*; @@ -38,6 +38,8 @@ pub async fn login(credentials: web::Json) -> impl Responder { status: 200, data: Some(user), }) + .customize() + .with_status(StatusCode::OK) } else { error!("Wrong password for {}!", credentials.username); web::Json(ResponseObj { @@ -45,15 +47,19 @@ pub async fn login(credentials: web::Json) -> impl Responder { status: 401, data: None, }) + .customize() + .with_status(StatusCode::FORBIDDEN) } } Err(e) => { error!("Login {} failed! {e}", credentials.username); return web::Json(ResponseObj { message: format!("Login {} failed!", credentials.username), - status: 404, + status: 400, data: None, - }); + }) + .customize() + .with_status(StatusCode::BAD_REQUEST); } } } From 2c940ca41cbcd07370d75f63779eefaf60bd8239 Mon Sep 17 00:00:00 2001 From: jb-alvarado Date: Thu, 9 Jun 2022 18:59:14 +0200 Subject: [PATCH 06/47] add jwt auth --- Cargo.lock | 39 +++++++++++++++++++++++++++++++ Cargo.toml | 3 +++ src/api/auth.rs | 46 ++++++++++++++++++++++++++++++++++++ src/api/handles.rs | 54 +++++++++++++++++++++++++++++++++++------- src/api/mod.rs | 1 + src/api/models.rs | 12 ++++++++-- src/api/routes.rs | 58 ++++++++++++++++++++++++++++++++++++++-------- src/api/utils.rs | 32 ++++++++++++++++++++++++- src/bin/ffpapi.rs | 52 +++++++++++++++++++++++++++++++++++------ 9 files changed, 268 insertions(+), 29 deletions(-) create mode 100644 src/api/auth.rs diff --git a/Cargo.lock b/Cargo.lock index 1442b7da..3602dc46 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -19,6 +19,17 @@ dependencies = [ "tokio-util 0.7.2", ] +[[package]] +name = "actix-grants-proc-macro" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48df3c1e0019e6f46bf3277e9b339688f476ccd9f5d5161419fdd305648fcac2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "actix-http" version = "3.0.4" @@ -181,6 +192,31 @@ dependencies = [ "syn", ] +[[package]] +name = "actix-web-grants" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef8ecad01ac8f2e4d0355a825e141c2077bba4d4e7bf740da34810a4aa872aea" +dependencies = [ + "actix-grants-proc-macro", + "actix-web", +] + +[[package]] +name = "actix-web-httpauth" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08c25a48b4684f90520183cd1a688e5f4f7e9905835fa75d02c0fe4f60fcdbe6" +dependencies = [ + "actix-service", + "actix-utils", + "actix-web", + "base64", + "futures-core", + "futures-util", + "pin-project-lite", +] + [[package]] name = "adler" version = "1.0.2" @@ -627,6 +663,8 @@ name = "ffplayout-engine" version = "0.9.8" dependencies = [ "actix-web", + "actix-web-grants", + "actix-web-httpauth", "argon2", "chrono 0.4.19 (git+https://github.com/sbrocket/chrono?branch=parse-error-kind-public)", "clap", @@ -639,6 +677,7 @@ dependencies = [ "lettre", "log", "notify", + "once_cell", "openssl", "rand", "rand_core", diff --git a/Cargo.toml b/Cargo.toml index f9c9d48b..71336f53 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,8 @@ default-run = "ffplayout" [dependencies] actix-web = "4" +actix-web-grants = "3" +actix-web-httpauth = "0.6" argon2 = "0.4" chrono = { git = "https://github.com/sbrocket/chrono", branch = "parse-error-kind-public" } clap = { version = "3.1", features = ["derive"] } @@ -22,6 +24,7 @@ jsonwebtoken = "8" lettre = "0.10.0-rc.6" log = "0.4" notify = "4.0" +once_cell = "1.10" rand = "0.8" rand_core = { version = "0.6", features = ["std"] } regex = "1" diff --git a/src/api/auth.rs b/src/api/auth.rs new file mode 100644 index 00000000..79a86fd2 --- /dev/null +++ b/src/api/auth.rs @@ -0,0 +1,46 @@ +use actix_web::error::ErrorUnauthorized; +use actix_web::Error; +use chrono::{Duration, Utc}; +use jsonwebtoken::{self, DecodingKey, EncodingKey, Header, Validation}; +use serde::{Deserialize, Serialize}; + +use crate::api::utils::GlobalSettings; + +// Token lifetime and Secret key are hardcoded for clarity +const JWT_EXPIRATION_MINUTES: i64 = 15; + +#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] +pub struct Claims { + pub id: i64, + pub username: String, + pub permissions: Vec, + exp: i64, +} + +impl Claims { + pub fn new(id: i64, username: String, permissions: Vec) -> Self { + Self { + id, + username, + permissions, + exp: (Utc::now() + Duration::minutes(JWT_EXPIRATION_MINUTES)).timestamp(), + } + } +} + +/// Create a json web token (JWT) +pub fn create_jwt(claims: Claims) -> Result { + let config = GlobalSettings::global(); + let encoding_key = EncodingKey::from_secret(config.secret.as_bytes()); + jsonwebtoken::encode(&Header::default(), &claims, &encoding_key) + .map_err(|e| ErrorUnauthorized(e.to_string())) +} + +/// Decode a json web token (JWT) +pub async fn decode_jwt(token: &str) -> Result { + let config = GlobalSettings::global(); + let decoding_key = DecodingKey::from_secret(config.secret.as_bytes()); + jsonwebtoken::decode::(token, &decoding_key, &Validation::default()) + .map(|data| data.claims) + .map_err(|e| ErrorUnauthorized(e.to_string())) +} diff --git a/src/api/handles.rs b/src/api/handles.rs index 400b16d4..a4f107ed 100644 --- a/src/api/handles.rs +++ b/src/api/handles.rs @@ -6,6 +6,12 @@ use simplelog::*; use sqlx::{migrate::MigrateDatabase, sqlite::SqliteQueryResult, Pool, Sqlite, SqlitePool}; use crate::api::models::User; +use crate::api::utils::GlobalSettings; + +#[derive(Debug, sqlx::FromRow)] +struct Role { + name: String, +} pub fn db_path() -> Result> { let sys_path = Path::new("/usr/share/ffplayout"); @@ -23,7 +29,13 @@ pub fn db_path() -> Result> { async fn cretea_schema() -> Result { let conn = db_connection().await?; let query = "PRAGMA foreign_keys = ON; - CREATE TABLE IF NOT EXISTS groups + CREATE TABLE IF NOT EXISTS global + ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + secret TEXT NOT NULL, + UNIQUE(secret) + ); + CREATE TABLE IF NOT EXISTS roles ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, @@ -36,7 +48,6 @@ async fn cretea_schema() -> Result { preview_url TEXT NOT NULL, settings_path TEXT NOT NULL, extra_extensions TEXT NOT NULL, - secret TEXT NOT NULL, UNIQUE(channel_name) ); CREATE TABLE IF NOT EXISTS user @@ -46,8 +57,8 @@ async fn cretea_schema() -> Result { username TEXT NOT NULL, password TEXT NOT NULL, salt TEXT NOT NULL, - group_id INTEGER NOT NULL DEFAULT 2, - FOREIGN KEY (group_id) REFERENCES groups (id) ON UPDATE SET NULL ON DELETE SET NULL, + role_id INTEGER NOT NULL DEFAULT 2, + FOREIGN KEY (role_id) REFERENCES roles (id) ON UPDATE SET NULL ON DELETE SET NULL, UNIQUE(email, username) );"; let result = sqlx::query(query).execute(&conn).await; @@ -74,10 +85,17 @@ pub async fn db_init() -> Result<&'static str, Box> { let instances = db_connection().await?; - let query = "INSERT INTO groups(name) VALUES('admin'), ('user'); - INSERT INTO settings(channel_name, preview_url, settings_path, extra_extensions, secret) + let query = "CREATE TRIGGER global_row_count + BEFORE INSERT ON global + WHEN (SELECT COUNT(*) FROM global) >= 1 + BEGIN + SELECT RAISE(FAIL, 'Database is already init!'); + END; + INSERT INTO global(secret) VALUES($1); + INSERT INTO roles(name) VALUES('admin'), ('user'), ('guest'); + INSERT INTO settings(channel_name, preview_url, settings_path, extra_extensions) VALUES('Channel 1', 'http://localhost/live/preview.m3u8', - '/etc/ffplayout/ffplayout.yml', '.jpg,.jpeg,.png', $1);"; + '/etc/ffplayout/ffplayout.yml', '.jpg,.jpeg,.png');"; sqlx::query(query).bind(secret).execute(&instances).await?; instances.close().await; @@ -91,6 +109,24 @@ pub async fn db_connection() -> Result, sqlx::Error> { Ok(conn) } +pub async fn get_global() -> Result { + let conn = db_connection().await?; + let query = "SELECT secret FROM global WHERE id = 1"; + let result: GlobalSettings = sqlx::query_as(query).fetch_one(&conn).await?; + conn.close().await; + + Ok(result) +} + +pub async fn get_role(id: &i64) -> Result { + let conn = db_connection().await?; + let query = "SELECT name FROM roles WHERE id = $1"; + let result: Role = sqlx::query_as(query).bind(id).fetch_one(&conn).await?; + conn.close().await; + + Ok(result.name) +} + pub async fn add_user( mail: &str, user: &str, @@ -100,7 +136,7 @@ pub async fn add_user( ) -> Result { let conn = db_connection().await?; let query = - "INSERT INTO user (email, username, password, salt, group_id) VALUES($1, $2, $3, $4, $5)"; + "INSERT INTO user (email, username, password, salt, role_id) VALUES($1, $2, $3, $4, $5)"; let result = sqlx::query(query) .bind(mail) .bind(user) @@ -116,7 +152,7 @@ pub async fn add_user( pub async fn get_login(user: &str) -> Result { let conn = db_connection().await?; - let query = "SELECT id, email, username, password, salt FROM user WHERE username = $1"; + let query = "SELECT id, email, 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; diff --git a/src/api/mod.rs b/src/api/mod.rs index 293e5f89..d3beadd4 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -1,4 +1,5 @@ pub mod args_parse; +pub mod auth; pub mod handles; pub mod models; pub mod routes; diff --git a/src/api/models.rs b/src/api/models.rs index 3bf15f1d..598f33b3 100644 --- a/src/api/models.rs +++ b/src/api/models.rs @@ -3,7 +3,8 @@ use serde::{Deserialize, Serialize}; #[derive(Debug, Deserialize, Serialize, sqlx::FromRow)] pub struct User { #[sqlx(default)] - pub id: Option, + #[serde(skip_deserializing)] + pub id: i64, #[sqlx(default)] pub email: Option, pub username: String, @@ -15,7 +16,14 @@ pub struct User { pub salt: Option, #[sqlx(default)] #[serde(skip_serializing)] - pub group_id: Option, + pub role_id: Option, + #[sqlx(default)] + pub token: Option, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct LoginUser { + pub id: i64, } #[derive(Debug, Deserialize, Serialize, sqlx::FromRow)] diff --git a/src/api/routes.rs b/src/api/routes.rs index 1f4dbcb2..2c1c7296 100644 --- a/src/api/routes.rs +++ b/src/api/routes.rs @@ -1,14 +1,22 @@ -use actix_web::{get, http::StatusCode, post, web, Responder}; +use std::sync::Mutex; + +use actix_web::{ + get, + http::StatusCode, + post, put, + web::{self, Data}, + Responder, +}; +use actix_web_grants::proc_macro::has_permissions; use argon2::{password_hash::PasswordHash, Argon2, PasswordVerifier}; use serde::Serialize; use simplelog::*; -use crate::api::{handles::get_login, models::User}; - -#[get("/hello/{name}")] -async fn greet(name: web::Path) -> impl Responder { - format!("Hello {name}!") -} +use crate::api::{ + auth::{create_jwt, Claims}, + handles::{get_login, get_role}, + models::{LoginUser, User}, +}; #[derive(Serialize)] struct ResponseObj { @@ -17,9 +25,27 @@ struct ResponseObj { data: Option, } -/// curl -X POST -H "Content-Type: application/json" -d '{"username": "USER", "password": "abc123" }' http://127.0.0.1:8080/auth/login/ +#[get("/settings")] +#[has_permissions("admin")] +async fn settings(data: Data>) -> impl Responder { + println!("{:?}", data.lock()); + "Hello from settings!" +} + +#[put("/user/{user_id}")] +#[has_permissions("admin")] +async fn update_user(user_id: web::Path, data: Data>) -> impl Responder { + if user_id.into_inner() == data.lock().unwrap().id { + return "Update allow!"; + } + + "Wrong user!" +} + +/// curl -X POST -H "Content-Type: application/json" -d '{"username": "USER", "password": "abc123" }' \ +/// http://127.0.0.1:8080/auth/login/ #[post("/auth/login/")] -pub async fn login(credentials: web::Json) -> impl Responder { +pub async fn login(credentials: web::Json, data: Data>) -> impl Responder { match get_login(&credentials.username).await { Ok(mut user) => { let pass = user.password.clone(); @@ -31,7 +57,19 @@ pub async fn login(credentials: web::Json) -> impl Responder { .verify_password(credentials.password.as_bytes(), &hash) .is_ok() { - info!("user {} login", credentials.username); + let role = get_role(&user.role_id.unwrap_or_default()) + .await + .unwrap_or_else(|_| "guest".to_string()); + let claims = Claims::new(user.id, user.username.clone(), vec![role.clone()]); + + if let Ok(token) = create_jwt(claims) { + user.token = Some(token); + }; + + let mut my_data = data.lock().unwrap(); + my_data.id = user.id; + + info!("user {} login, with role: {role}", credentials.username); web::Json(ResponseObj { message: "login correct!".into(), diff --git a/src/api/utils.rs b/src/api/utils.rs index bc5b522d..1e652cac 100644 --- a/src/api/utils.rs +++ b/src/api/utils.rs @@ -2,13 +2,43 @@ use argon2::{ password_hash::{rand_core::OsRng, PasswordHasher, SaltString}, Argon2, }; +use once_cell::sync::OnceCell; use simplelog::*; use crate::api::{ args_parse::Args, - handles::{add_user, db_init}, + handles::{add_user, db_init, get_global}, }; +#[derive(Debug, sqlx::FromRow)] +pub struct GlobalSettings { + pub secret: String, +} + +impl GlobalSettings { + async fn new() -> Self { + let global_settings = get_global(); + + match global_settings.await { + Ok(g) => g, + Err(_) => GlobalSettings { + secret: String::new(), + }, + } + } + + pub fn global() -> &'static GlobalSettings { + INSTANCE.get().expect("Config is not initialized") + } +} + +static INSTANCE: OnceCell = OnceCell::new(); + +pub async fn init_config() { + let config = GlobalSettings::new().await; + INSTANCE.set(config).unwrap(); +} + pub async fn run_args(args: Args) -> Result<(), i32> { if !args.init && args.listen.is_none() && args.username.is_none() { error!("Wrong number of arguments! Run ffpapi --help for more information."); diff --git a/src/bin/ffpapi.rs b/src/bin/ffpapi.rs index 7bb75d55..2c7cd77a 100644 --- a/src/bin/ffpapi.rs +++ b/src/bin/ffpapi.rs @@ -1,14 +1,36 @@ -use std::process::exit; +use std::{process::exit, sync::Mutex}; + +use actix_web::{ + dev::ServiceRequest, + middleware, + web::{self, Data}, + App, Error, HttpServer, +}; +use actix_web_grants::permissions::AttachPermissions; +use actix_web_httpauth::extractors::bearer::BearerAuth; +use actix_web_httpauth::middleware::HttpAuthentication; -use actix_web::{App, HttpServer}; use clap::Parser; use simplelog::*; use ffplayout_engine::{ - api::{args_parse::Args, routes::login, utils::run_args}, + api::{ + args_parse::Args, + auth, + models::LoginUser, + routes::{login, settings, update_user}, + utils::{init_config, run_args}, + }, utils::{init_logging, GlobalConfig}, }; +async fn validator(req: ServiceRequest, credentials: BearerAuth) -> Result { + // We just get permissions from JWT + let claims = auth::decode_jwt(credentials.token()).await?; + req.attach(claims.permissions); + Ok(req) +} + #[actix_web::main] async fn main() -> std::io::Result<()> { let args = Args::parse(); @@ -26,15 +48,31 @@ async fn main() -> std::io::Result<()> { } if let Some(conn) = args.listen { + init_config().await; let ip_port = conn.split(':').collect::>(); let addr = ip_port[0]; let port = ip_port[1].parse::().unwrap(); + let data = Data::new(Mutex::new(LoginUser { id: 0 })); + info!("running ffplayout API, listen on {conn}"); - HttpServer::new(|| App::new().service(login)) - .bind((addr, port))? - .run() - .await + // TODO: add allow origin + HttpServer::new(move || { + let auth = HttpAuthentication::bearer(validator); + App::new() + .wrap(middleware::Logger::default()) + .app_data(Data::clone(&data)) + .service(login) + .service( + web::scope("/api") + .wrap(auth) + .service(settings) + .service(update_user), + ) + }) + .bind((addr, port))? + .run() + .await } else { error!("Run ffpapi with listen parameter!"); From 51e75cb113a7cc8a761cb5bac61b2c76fa5dc947 Mon Sep 17 00:00:00 2001 From: jb-alvarado Date: Thu, 9 Jun 2022 19:20:09 +0200 Subject: [PATCH 07/47] set user data in validator --- src/api/models.rs | 9 ++++++++- src/api/routes.rs | 23 ++++++----------------- src/bin/ffpapi.rs | 14 +++++--------- 3 files changed, 19 insertions(+), 27 deletions(-) diff --git a/src/api/models.rs b/src/api/models.rs index 598f33b3..51133052 100644 --- a/src/api/models.rs +++ b/src/api/models.rs @@ -21,9 +21,16 @@ pub struct User { pub token: Option, } -#[derive(Debug, Deserialize, Serialize)] +#[derive(Debug, Deserialize, Serialize, Clone)] pub struct LoginUser { pub id: i64, + pub username: String, +} + +impl LoginUser { + pub fn new(id: i64, username: String) -> Self { + Self { id, username } + } } #[derive(Debug, Deserialize, Serialize, sqlx::FromRow)] diff --git a/src/api/routes.rs b/src/api/routes.rs index 2c1c7296..1ce99fca 100644 --- a/src/api/routes.rs +++ b/src/api/routes.rs @@ -1,12 +1,4 @@ -use std::sync::Mutex; - -use actix_web::{ - get, - http::StatusCode, - post, put, - web::{self, Data}, - Responder, -}; +use actix_web::{get, http::StatusCode, post, put, web, Responder}; use actix_web_grants::proc_macro::has_permissions; use argon2::{password_hash::PasswordHash, Argon2, PasswordVerifier}; use serde::Serialize; @@ -27,15 +19,15 @@ struct ResponseObj { #[get("/settings")] #[has_permissions("admin")] -async fn settings(data: Data>) -> impl Responder { - println!("{:?}", data.lock()); +async fn settings(user: web::ReqData) -> impl Responder { + println!("{:?}", user); "Hello from settings!" } #[put("/user/{user_id}")] #[has_permissions("admin")] -async fn update_user(user_id: web::Path, data: Data>) -> impl Responder { - if user_id.into_inner() == data.lock().unwrap().id { +async fn update_user(user_id: web::Path, user: web::ReqData) -> impl Responder { + if user_id.into_inner() == user.id { return "Update allow!"; } @@ -45,7 +37,7 @@ async fn update_user(user_id: web::Path, data: Data>) -> i /// curl -X POST -H "Content-Type: application/json" -d '{"username": "USER", "password": "abc123" }' \ /// http://127.0.0.1:8080/auth/login/ #[post("/auth/login/")] -pub async fn login(credentials: web::Json, data: Data>) -> impl Responder { +pub async fn login(credentials: web::Json) -> impl Responder { match get_login(&credentials.username).await { Ok(mut user) => { let pass = user.password.clone(); @@ -66,9 +58,6 @@ pub async fn login(credentials: web::Json, data: Data>) - user.token = Some(token); }; - let mut my_data = data.lock().unwrap(); - my_data.id = user.id; - info!("user {} login, with role: {role}", credentials.username); web::Json(ResponseObj { diff --git a/src/bin/ffpapi.rs b/src/bin/ffpapi.rs index 2c7cd77a..cd289775 100644 --- a/src/bin/ffpapi.rs +++ b/src/bin/ffpapi.rs @@ -1,11 +1,6 @@ -use std::{process::exit, sync::Mutex}; +use std::process::exit; -use actix_web::{ - dev::ServiceRequest, - middleware, - web::{self, Data}, - App, Error, HttpServer, -}; +use actix_web::{dev::ServiceRequest, middleware, web, App, Error, HttpMessage, HttpServer}; use actix_web_grants::permissions::AttachPermissions; use actix_web_httpauth::extractors::bearer::BearerAuth; use actix_web_httpauth::middleware::HttpAuthentication; @@ -28,6 +23,9 @@ async fn validator(req: ServiceRequest, credentials: BearerAuth) -> Result std::io::Result<()> { let ip_port = conn.split(':').collect::>(); let addr = ip_port[0]; let port = ip_port[1].parse::().unwrap(); - let data = Data::new(Mutex::new(LoginUser { id: 0 })); info!("running ffplayout API, listen on {conn}"); @@ -61,7 +58,6 @@ async fn main() -> std::io::Result<()> { let auth = HttpAuthentication::bearer(validator); App::new() .wrap(middleware::Logger::default()) - .app_data(Data::clone(&data)) .service(login) .service( web::scope("/api") From e59b35612848aa55112d693750c3b7a3f2d26af6 Mon Sep 17 00:00:00 2001 From: jb-alvarado Date: Thu, 9 Jun 2022 22:17:03 +0200 Subject: [PATCH 08/47] add errors --- Cargo.lock | 1 + Cargo.toml | 1 + src/api/errors.rs | 27 +++++++++++++++++++++++++++ src/api/mod.rs | 1 + src/api/routes.rs | 17 ++++++++++++----- 5 files changed, 42 insertions(+), 5 deletions(-) create mode 100644 src/api/errors.rs diff --git a/Cargo.lock b/Cargo.lock index 3602dc46..22bab976 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -669,6 +669,7 @@ dependencies = [ "chrono 0.4.19 (git+https://github.com/sbrocket/chrono?branch=parse-error-kind-public)", "clap", "crossbeam-channel", + "derive_more", "faccess", "ffprobe", "file-rotate", diff --git a/Cargo.toml b/Cargo.toml index 71336f53..b15b56d2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,7 @@ argon2 = "0.4" chrono = { git = "https://github.com/sbrocket/chrono", branch = "parse-error-kind-public" } clap = { version = "3.1", features = ["derive"] } crossbeam-channel = "0.5" +derive_more = "0.99" faccess = "0.2" ffprobe = "0.3" file-rotate = { git = "https://github.com/Ploppz/file-rotate.git", branch = "timestamp-parse-fix" } diff --git a/src/api/errors.rs b/src/api/errors.rs new file mode 100644 index 00000000..a515c47a --- /dev/null +++ b/src/api/errors.rs @@ -0,0 +1,27 @@ +use actix_web::{error::ResponseError, HttpResponse}; +use derive_more::Display; + +#[derive(Debug, Display)] +pub enum ServiceError { + #[display(fmt = "Internal Server Error")] + InternalServerError, + + #[display(fmt = "BadRequest: {}", _0)] + BadRequest(String), + + #[display(fmt = "Unauthorized")] + Unauthorized, +} + +// impl ResponseError trait allows to convert our errors into http responses with appropriate data +impl ResponseError for ServiceError { + fn error_response(&self) -> HttpResponse { + match self { + ServiceError::InternalServerError => { + HttpResponse::InternalServerError().json("Internal Server Error. Please try later.") + } + ServiceError::BadRequest(ref message) => HttpResponse::BadRequest().json(message), + ServiceError::Unauthorized => HttpResponse::Unauthorized().json("No Permission!"), + } + } +} diff --git a/src/api/mod.rs b/src/api/mod.rs index d3beadd4..6ecc6917 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -1,5 +1,6 @@ pub mod args_parse; pub mod auth; +pub mod errors; pub mod handles; pub mod models; pub mod routes; diff --git a/src/api/routes.rs b/src/api/routes.rs index 1ce99fca..441ad993 100644 --- a/src/api/routes.rs +++ b/src/api/routes.rs @@ -6,6 +6,7 @@ use simplelog::*; use crate::api::{ auth::{create_jwt, Claims}, + errors::ServiceError, handles::{get_login, get_role}, models::{LoginUser, User}, }; @@ -17,21 +18,27 @@ struct ResponseObj { data: Option, } +/// curl -X GET http://127.0.0.1:8080/api/settings -H "Authorization: Bearer " #[get("/settings")] #[has_permissions("admin")] -async fn settings(user: web::ReqData) -> impl Responder { +async fn settings(user: web::ReqData) -> Result { println!("{:?}", user); - "Hello from settings!" + Ok("Hello from settings!") } #[put("/user/{user_id}")] #[has_permissions("admin")] -async fn update_user(user_id: web::Path, user: web::ReqData) -> impl Responder { +async fn update_user( + user_id: web::Path, + user: web::ReqData, + data: web::Json, +) -> Result { if user_id.into_inner() == user.id { - return "Update allow!"; + println!("{data:?}"); + return Ok("Update allow!"); } - "Wrong user!" + Err(ServiceError::Unauthorized) } /// curl -X POST -H "Content-Type: application/json" -d '{"username": "USER", "password": "abc123" }' \ From dc50f621e05c966dd8b4681e550eaa778165a706 Mon Sep 17 00:00:00 2001 From: jb-alvarado Date: Fri, 10 Jun 2022 16:12:30 +0200 Subject: [PATCH 09/47] update user --- src/api/auth.rs | 2 +- src/api/handles.rs | 15 ++++++++++++--- src/api/models.rs | 6 +++++- src/api/routes.rs | 39 +++++++++++++++++++++++++++++++++------ src/api/utils.rs | 4 ++-- 5 files changed, 53 insertions(+), 13 deletions(-) diff --git a/src/api/auth.rs b/src/api/auth.rs index 79a86fd2..9c2f902d 100644 --- a/src/api/auth.rs +++ b/src/api/auth.rs @@ -7,7 +7,7 @@ use serde::{Deserialize, Serialize}; use crate::api::utils::GlobalSettings; // Token lifetime and Secret key are hardcoded for clarity -const JWT_EXPIRATION_MINUTES: i64 = 15; +const JWT_EXPIRATION_MINUTES: i64 = 60; #[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] pub struct Claims { diff --git a/src/api/handles.rs b/src/api/handles.rs index a4f107ed..db54cc9f 100644 --- a/src/api/handles.rs +++ b/src/api/handles.rs @@ -109,7 +109,7 @@ pub async fn db_connection() -> Result, sqlx::Error> { Ok(conn) } -pub async fn get_global() -> Result { +pub async fn db_global() -> Result { let conn = db_connection().await?; let query = "SELECT secret FROM global WHERE id = 1"; let result: GlobalSettings = sqlx::query_as(query).fetch_one(&conn).await?; @@ -118,7 +118,7 @@ pub async fn get_global() -> Result { Ok(result) } -pub async fn get_role(id: &i64) -> Result { +pub async fn db_role(id: &i64) -> Result { let conn = db_connection().await?; let query = "SELECT name FROM roles WHERE id = $1"; let result: Role = sqlx::query_as(query).bind(id).fetch_one(&conn).await?; @@ -150,7 +150,7 @@ pub async fn add_user( Ok(result) } -pub async fn get_login(user: &str) -> Result { +pub async fn db_login(user: &str) -> Result { let conn = db_connection().await?; let query = "SELECT id, email, username, password, salt, role_id FROM user WHERE username = $1"; let result: User = sqlx::query_as(query).bind(user).fetch_one(&conn).await?; @@ -158,3 +158,12 @@ pub async fn get_login(user: &str) -> Result { Ok(result) } + +pub async fn db_update_user(id: i64, fields: String) -> Result { + let conn = db_connection().await?; + let query = format!("UPDATE user SET {fields} WHERE id = $1"); + let result: SqliteQueryResult = sqlx::query(&query).bind(id).execute(&conn).await?; + conn.close().await; + + Ok(result) +} diff --git a/src/api/models.rs b/src/api/models.rs index 51133052..8b781f61 100644 --- a/src/api/models.rs +++ b/src/api/models.rs @@ -9,7 +9,7 @@ pub struct User { pub email: Option, pub username: String, #[sqlx(default)] - #[serde(skip_serializing)] + #[serde(skip_serializing, default = "empty_string")] pub password: String, #[sqlx(default)] #[serde(skip_serializing)] @@ -21,6 +21,10 @@ pub struct User { pub token: Option, } +fn empty_string() -> String { + "".to_string() +} + #[derive(Debug, Deserialize, Serialize, Clone)] pub struct LoginUser { pub id: i64, diff --git a/src/api/routes.rs b/src/api/routes.rs index 441ad993..41a0e4f3 100644 --- a/src/api/routes.rs +++ b/src/api/routes.rs @@ -1,13 +1,16 @@ use actix_web::{get, http::StatusCode, post, put, web, Responder}; use actix_web_grants::proc_macro::has_permissions; -use argon2::{password_hash::PasswordHash, Argon2, PasswordVerifier}; +use argon2::{ + password_hash::{rand_core::OsRng, PasswordHash, SaltString}, + Argon2, PasswordHasher, PasswordVerifier, +}; use serde::Serialize; use simplelog::*; use crate::api::{ auth::{create_jwt, Claims}, errors::ServiceError, - handles::{get_login, get_role}, + handles::{db_login, db_role, db_update_user}, models::{LoginUser, User}, }; @@ -34,8 +37,32 @@ async fn update_user( data: web::Json, ) -> Result { if user_id.into_inner() == user.id { - println!("{data:?}"); - return Ok("Update allow!"); + let mut fields = String::new(); + + if let Some(email) = data.email.clone() { + fields.push_str(format!("email = '{email}'").as_str()); + } + + if !data.password.is_empty() { + if !fields.is_empty() { + fields.push_str(", "); + } + + let salt = SaltString::generate(&mut OsRng); + let argon2 = Argon2::default(); + + let password_hash = argon2 + .hash_password(data.password.clone().as_bytes(), &salt) + .unwrap(); + + fields.push_str(format!("password = '{}', salt = '{salt}'", password_hash).as_str()); + } + + if db_update_user(user.id, fields).await.is_ok() { + return Ok("Update Success"); + }; + + return Err(ServiceError::InternalServerError); } Err(ServiceError::Unauthorized) @@ -45,7 +72,7 @@ async fn update_user( /// http://127.0.0.1:8080/auth/login/ #[post("/auth/login/")] pub async fn login(credentials: web::Json) -> impl Responder { - match get_login(&credentials.username).await { + match db_login(&credentials.username).await { Ok(mut user) => { let pass = user.password.clone(); user.password = "".into(); @@ -56,7 +83,7 @@ pub async fn login(credentials: web::Json) -> impl Responder { .verify_password(credentials.password.as_bytes(), &hash) .is_ok() { - let role = get_role(&user.role_id.unwrap_or_default()) + let role = db_role(&user.role_id.unwrap_or_default()) .await .unwrap_or_else(|_| "guest".to_string()); let claims = Claims::new(user.id, user.username.clone(), vec![role.clone()]); diff --git a/src/api/utils.rs b/src/api/utils.rs index 1e652cac..93e6aaca 100644 --- a/src/api/utils.rs +++ b/src/api/utils.rs @@ -7,7 +7,7 @@ use simplelog::*; use crate::api::{ args_parse::Args, - handles::{add_user, db_init, get_global}, + handles::{add_user, db_global, db_init}, }; #[derive(Debug, sqlx::FromRow)] @@ -17,7 +17,7 @@ pub struct GlobalSettings { impl GlobalSettings { async fn new() -> Self { - let global_settings = get_global(); + let global_settings = db_global(); match global_settings.await { Ok(g) => g, From ca5ac810bedd726d5225bb4af6f88ac4eb725498 Mon Sep 17 00:00:00 2001 From: jb-alvarado Date: Fri, 10 Jun 2022 16:15:52 +0200 Subject: [PATCH 10/47] add curl command --- src/api/routes.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/api/routes.rs b/src/api/routes.rs index 41a0e4f3..c0eef682 100644 --- a/src/api/routes.rs +++ b/src/api/routes.rs @@ -29,6 +29,8 @@ async fn settings(user: web::ReqData) -> Result", "password": ""}' --header 'Authorization: ' #[put("/user/{user_id}")] #[has_permissions("admin")] async fn update_user( @@ -68,8 +70,8 @@ async fn update_user( Err(ServiceError::Unauthorized) } -/// curl -X POST -H "Content-Type: application/json" -d '{"username": "USER", "password": "abc123" }' \ -/// http://127.0.0.1:8080/auth/login/ +/// curl -X POST http://127.0.0.1:8080/auth/login/ -H "Content-Type: application/json" \ +/// -d '{"username": "USER", "password": "abc123" }' #[post("/auth/login/")] pub async fn login(credentials: web::Json) -> impl Responder { match db_login(&credentials.username).await { From c4d599ff31ebb37dfbc2338222686f58858b4743 Mon Sep 17 00:00:00 2001 From: jb-alvarado Date: Sun, 12 Jun 2022 22:37:29 +0200 Subject: [PATCH 11/47] get and patch settngs --- src/api/handles.rs | 37 ++++++++++++++++++++++++++++++++--- src/api/models.rs | 5 +++-- src/api/routes.rs | 48 ++++++++++++++++++++++++++++++++++------------ src/bin/ffpapi.rs | 5 +++-- 4 files changed, 76 insertions(+), 19 deletions(-) diff --git a/src/api/handles.rs b/src/api/handles.rs index db54cc9f..89f14da3 100644 --- a/src/api/handles.rs +++ b/src/api/handles.rs @@ -1,11 +1,12 @@ use std::path::Path; +use actix_web::web; use faccess::PathExt; use rand::{distributions::Alphanumeric, Rng}; use simplelog::*; use sqlx::{migrate::MigrateDatabase, sqlite::SqliteQueryResult, Pool, Sqlite, SqlitePool}; -use crate::api::models::User; +use crate::api::models::{Settings, User}; use crate::api::utils::GlobalSettings; #[derive(Debug, sqlx::FromRow)] @@ -46,7 +47,7 @@ async fn cretea_schema() -> Result { id INTEGER PRIMARY KEY AUTOINCREMENT, channel_name TEXT NOT NULL, preview_url TEXT NOT NULL, - settings_path TEXT NOT NULL, + config_path TEXT NOT NULL, extra_extensions TEXT NOT NULL, UNIQUE(channel_name) ); @@ -93,7 +94,7 @@ pub async fn db_init() -> Result<&'static str, Box> { END; INSERT INTO global(secret) VALUES($1); INSERT INTO roles(name) VALUES('admin'), ('user'), ('guest'); - INSERT INTO settings(channel_name, preview_url, settings_path, extra_extensions) + INSERT INTO settings(channel_name, preview_url, config_path, extra_extensions) VALUES('Channel 1', 'http://localhost/live/preview.m3u8', '/etc/ffplayout/ffplayout.yml', '.jpg,.jpeg,.png');"; sqlx::query(query).bind(secret).execute(&instances).await?; @@ -118,6 +119,36 @@ pub async fn db_global() -> Result { Ok(result) } +pub async fn db_get_settings(id: &i64) -> Result { + let conn = db_connection().await?; + let query = "SELECT * FROM settings WHERE id = $1"; + let result: Settings = sqlx::query_as(query).bind(id).fetch_one(&conn).await?; + conn.close().await; + + println!("{:#?}", result); + + Ok(result) +} + +pub async fn db_update_settings( + id: i64, + s: web::Json, +) -> Result { + let conn = db_connection().await?; + let query = "UPDATE settings SET channel_name = $2, preview_url = $3, config_path = $4, extra_extensions = $5 WHERE id = $1"; + let result: SqliteQueryResult = sqlx::query(query) + .bind(id) + .bind(s.channel_name.clone()) + .bind(s.preview_url.clone()) + .bind(s.config_path.clone()) + .bind(s.extra_extensions.clone()) + .execute(&conn) + .await?; + conn.close().await; + + Ok(result) +} + pub async fn db_role(id: &i64) -> Result { let conn = db_connection().await?; let query = "SELECT name FROM roles WHERE id = $1"; diff --git a/src/api/models.rs b/src/api/models.rs index 8b781f61..b656d693 100644 --- a/src/api/models.rs +++ b/src/api/models.rs @@ -39,12 +39,13 @@ impl LoginUser { #[derive(Debug, Deserialize, Serialize, sqlx::FromRow)] pub struct Settings { + #[serde(skip_deserializing)] pub id: i64, pub channel_name: String, pub preview_url: String, - pub settings_path: String, + pub config_path: String, pub extra_extensions: String, #[sqlx(default)] - #[serde(skip_serializing)] + #[serde(skip_serializing, skip_deserializing)] pub secret: String, } diff --git a/src/api/routes.rs b/src/api/routes.rs index c0eef682..ed8d3992 100644 --- a/src/api/routes.rs +++ b/src/api/routes.rs @@ -1,4 +1,4 @@ -use actix_web::{get, http::StatusCode, post, put, web, Responder}; +use actix_web::{get, http::StatusCode, patch, post, put, web, Responder}; use actix_web_grants::proc_macro::has_permissions; use argon2::{ password_hash::{rand_core::OsRng, PasswordHash, SaltString}, @@ -10,8 +10,8 @@ use simplelog::*; use crate::api::{ auth::{create_jwt, Claims}, errors::ServiceError, - handles::{db_login, db_role, db_update_user}, - models::{LoginUser, User}, + handles::{db_get_settings, db_login, db_role, db_update_settings, db_update_user}, + models::{LoginUser, Settings, User}, }; #[derive(Serialize)] @@ -21,24 +21,48 @@ struct ResponseObj { data: Option, } -/// curl -X GET http://127.0.0.1:8080/api/settings -H "Authorization: Bearer " -#[get("/settings")] +/// curl -X GET http://127.0.0.1:8080/api/settings/1 -H "Authorization: Bearer " +#[get("/settings/{id}")] +#[has_permissions("admin", "user")] +async fn get_settings(id: web::Path) -> Result { + if let Ok(settings) = db_get_settings(&id).await { + return Ok(web::Json(ResponseObj { + message: format!("Settings from {}", settings.channel_name), + status: 200, + data: Some(settings), + })); + } + + Err(ServiceError::InternalServerError) +} + +/// curl -X PATCH http://127.0.0.1:8080/api/settings/1 -H "Content-Type: application/json" \ +/// --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"}' \ +/// -H "Authorization: Bearer " +#[patch("/settings/{id}")] #[has_permissions("admin")] -async fn settings(user: web::ReqData) -> Result { - println!("{:?}", user); - Ok("Hello from settings!") +async fn patch_settings( + id: web::Path, + data: web::Json, +) -> Result { + if db_update_settings(*id, data).await.is_ok() { + return Ok("Update Success"); + }; + + Err(ServiceError::InternalServerError) } /// curl -X PUT http://localhost:8080/api/user/1 --header 'Content-Type: application/json' \ /// --data '{"email": "", "password": ""}' --header 'Authorization: ' -#[put("/user/{user_id}")] -#[has_permissions("admin")] +#[put("/user/{id}")] +#[has_permissions("admin", "user")] async fn update_user( - user_id: web::Path, + id: web::Path, user: web::ReqData, data: web::Json, ) -> Result { - if user_id.into_inner() == user.id { + if id.into_inner() == user.id { let mut fields = String::new(); if let Some(email) = data.email.clone() { diff --git a/src/bin/ffpapi.rs b/src/bin/ffpapi.rs index cd289775..aca8e375 100644 --- a/src/bin/ffpapi.rs +++ b/src/bin/ffpapi.rs @@ -13,7 +13,7 @@ use ffplayout_engine::{ args_parse::Args, auth, models::LoginUser, - routes::{login, settings, update_user}, + routes::{get_settings, login, patch_settings, update_user}, utils::{init_config, run_args}, }, utils::{init_logging, GlobalConfig}, @@ -62,7 +62,8 @@ async fn main() -> std::io::Result<()> { .service( web::scope("/api") .wrap(auth) - .service(settings) + .service(get_settings) + .service(patch_settings) .service(update_user), ) }) From c5947b352d326b2e113efefbeab6a3bd92d6dae2 Mon Sep 17 00:00:00 2001 From: jb-alvarado Date: Mon, 13 Jun 2022 13:54:36 +0200 Subject: [PATCH 12/47] add user --- src/api/handles.rs | 59 ++++++++++++++++++++++++---------------------- src/api/routes.rs | 31 +++++++++++++++++------- src/api/utils.rs | 35 +++++++++------------------ src/bin/ffpapi.rs | 5 ++-- 4 files changed, 68 insertions(+), 62 deletions(-) diff --git a/src/api/handles.rs b/src/api/handles.rs index 89f14da3..c4f6ce40 100644 --- a/src/api/handles.rs +++ b/src/api/handles.rs @@ -1,6 +1,9 @@ use std::path::Path; -use actix_web::web; +use argon2::{ + password_hash::{rand_core::OsRng, SaltString}, + Argon2, PasswordHasher, +}; use faccess::PathExt; use rand::{distributions::Alphanumeric, Rng}; use simplelog::*; @@ -27,7 +30,7 @@ pub fn db_path() -> Result> { Ok(db_path) } -async fn cretea_schema() -> Result { +async fn create_schema() -> Result { let conn = db_connection().await?; let query = "PRAGMA foreign_keys = ON; CREATE TABLE IF NOT EXISTS global @@ -73,7 +76,7 @@ pub async fn db_init() -> Result<&'static str, Box> { if !Sqlite::database_exists(&db_path).await.unwrap_or(false) { Sqlite::create_database(&db_path).await.unwrap(); - match cretea_schema().await { + match create_schema().await { Ok(_) => info!("Database created Successfully"), Err(e) => panic!("{e}"), } @@ -132,16 +135,17 @@ pub async fn db_get_settings(id: &i64) -> Result { pub async fn db_update_settings( id: i64, - s: web::Json, + settings: Settings, ) -> Result { let conn = db_connection().await?; + let query = "UPDATE settings SET channel_name = $2, preview_url = $3, config_path = $4, extra_extensions = $5 WHERE id = $1"; let result: SqliteQueryResult = sqlx::query(query) .bind(id) - .bind(s.channel_name.clone()) - .bind(s.preview_url.clone()) - .bind(s.config_path.clone()) - .bind(s.extra_extensions.clone()) + .bind(settings.channel_name.clone()) + .bind(settings.preview_url.clone()) + .bind(settings.config_path.clone()) + .bind(settings.extra_extensions.clone()) .execute(&conn) .await?; conn.close().await; @@ -158,33 +162,32 @@ pub async fn db_role(id: &i64) -> Result { Ok(result.name) } -pub async fn add_user( - mail: &str, - user: &str, - pass: &str, - salt: &str, - group: &i64, -) -> Result { +pub async fn db_login(user: &str) -> Result { let conn = db_connection().await?; - let query = - "INSERT INTO user (email, username, password, salt, role_id) VALUES($1, $2, $3, $4, $5)"; - let result = sqlx::query(query) - .bind(mail) - .bind(user) - .bind(pass) - .bind(salt) - .bind(group) - .execute(&conn) - .await?; + let query = "SELECT id, email, 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_login(user: &str) -> Result { +pub async fn db_add_user(user: User) -> Result { let conn = db_connection().await?; - let query = "SELECT id, email, username, password, salt, role_id FROM user WHERE username = $1"; - let result: User = sqlx::query_as(query).bind(user).fetch_one(&conn).await?; + let salt = SaltString::generate(&mut OsRng); + let password_hash = Argon2::default() + .hash_password(user.password.clone().as_bytes(), &salt) + .unwrap(); + + let query = + "INSERT INTO user (email, username, password, salt, role_id) VALUES($1, $2, $3, $4, $5)"; + let result = sqlx::query(query) + .bind(user.email) + .bind(user.username) + .bind(password_hash.to_string()) + .bind(salt.to_string()) + .bind(user.role_id) + .execute(&conn) + .await?; conn.close().await; Ok(result) diff --git a/src/api/routes.rs b/src/api/routes.rs index ed8d3992..889beccb 100644 --- a/src/api/routes.rs +++ b/src/api/routes.rs @@ -10,7 +10,9 @@ use simplelog::*; use crate::api::{ auth::{create_jwt, Claims}, errors::ServiceError, - handles::{db_get_settings, db_login, db_role, db_update_settings, db_update_user}, + handles::{ + db_add_user, db_get_settings, db_login, db_role, db_update_settings, db_update_user, + }, models::{LoginUser, Settings, User}, }; @@ -46,7 +48,7 @@ async fn patch_settings( id: web::Path, data: web::Json, ) -> Result { - if db_update_settings(*id, data).await.is_ok() { + if db_update_settings(*id, data.into_inner()).await.is_ok() { return Ok("Update Success"); }; @@ -75,9 +77,7 @@ async fn update_user( } let salt = SaltString::generate(&mut OsRng); - let argon2 = Argon2::default(); - - let password_hash = argon2 + let password_hash = Argon2::default() .hash_password(data.password.clone().as_bytes(), &salt) .unwrap(); @@ -94,17 +94,32 @@ async fn update_user( Err(ServiceError::Unauthorized) } +/// curl -X POST 'http://localhost:8080/api/user/' --header 'Content-Type: application/json' \ +/// -d '{"email": "", "username": "", "password": "", "role_id": 1}' \ +/// --header 'Authorization: Bearer ' +#[post("/user/")] +#[has_permissions("admin")] +async fn add_user(data: web::Json) -> Result { + match db_add_user(data.into_inner()).await { + Ok(_) => Ok("Add User Success"), + Err(e) => { + error!("{e}"); + Err(ServiceError::InternalServerError) + } + } +} + /// curl -X POST http://127.0.0.1:8080/auth/login/ -H "Content-Type: application/json" \ -/// -d '{"username": "USER", "password": "abc123" }' +/// -d '{"username": "", "password": "" }' #[post("/auth/login/")] pub async fn login(credentials: web::Json) -> impl Responder { match db_login(&credentials.username).await { Ok(mut user) => { let pass = user.password.clone(); + let hash = PasswordHash::new(&pass).unwrap(); user.password = "".into(); user.salt = None; - let hash = PasswordHash::new(&pass).unwrap(); if Argon2::default() .verify_password(credentials.password.as_bytes(), &hash) .is_ok() @@ -131,7 +146,7 @@ pub async fn login(credentials: web::Json) -> impl Responder { error!("Wrong password for {}!", credentials.username); web::Json(ResponseObj { message: "Wrong password!".into(), - status: 401, + status: 403, data: None, }) .customize() diff --git a/src/api/utils.rs b/src/api/utils.rs index 93e6aaca..58017868 100644 --- a/src/api/utils.rs +++ b/src/api/utils.rs @@ -1,13 +1,10 @@ -use argon2::{ - password_hash::{rand_core::OsRng, PasswordHasher, SaltString}, - Argon2, -}; use once_cell::sync::OnceCell; use simplelog::*; use crate::api::{ args_parse::Args, - handles::{add_user, db_global, db_init}, + handles::{db_add_user, db_global, db_init}, + models::User, }; #[derive(Debug, sqlx::FromRow)] @@ -60,27 +57,17 @@ pub async fn run_args(args: Args) -> Result<(), i32> { return Err(1); } - let salt = SaltString::generate(&mut OsRng); - let argon2 = Argon2::default(); - let password = args.password.unwrap(); - - let password_hash = match argon2.hash_password(password.as_bytes(), &salt) { - Ok(hash) => hash.to_string(), - Err(e) => { - error!("{e}"); - return Err(1); - } + let user = User { + id: 0, + email: Some(args.email.unwrap()), + username: username.clone(), + password: args.password.unwrap(), + salt: None, + role_id: Some(1), + token: None, }; - if let Err(e) = add_user( - &args.email.unwrap(), - &username, - &password_hash.to_string(), - &salt.to_string(), - &1, - ) - .await - { + if let Err(e) = db_add_user(user).await { error!("{e}"); return Err(1); }; diff --git a/src/bin/ffpapi.rs b/src/bin/ffpapi.rs index aca8e375..eadbea87 100644 --- a/src/bin/ffpapi.rs +++ b/src/bin/ffpapi.rs @@ -13,7 +13,7 @@ use ffplayout_engine::{ args_parse::Args, auth, models::LoginUser, - routes::{get_settings, login, patch_settings, update_user}, + routes::{add_user, get_settings, login, patch_settings, update_user}, utils::{init_config, run_args}, }, utils::{init_logging, GlobalConfig}, @@ -53,7 +53,7 @@ async fn main() -> std::io::Result<()> { info!("running ffplayout API, listen on {conn}"); - // TODO: add allow origin + // TODO: add allow origin (or give it to the proxy) HttpServer::new(move || { let auth = HttpAuthentication::bearer(validator); App::new() @@ -62,6 +62,7 @@ async fn main() -> std::io::Result<()> { .service( web::scope("/api") .wrap(auth) + .service(add_user) .service(get_settings) .service(patch_settings) .service(update_user), From 019a1631760a245f74144f13482e164943260f27 Mon Sep 17 00:00:00 2001 From: jb-alvarado Date: Mon, 13 Jun 2022 14:20:31 +0200 Subject: [PATCH 13/47] updates --- Cargo.lock | 61 ++++++++++++++++++++++++++++++++---------------------- 1 file changed, 36 insertions(+), 25 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index df7df1dc..dddebab0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -16,7 +16,7 @@ dependencies = [ "memchr", "pin-project-lite", "tokio", - "tokio-util 0.7.2", + "tokio-util 0.7.3", ] [[package]] @@ -32,9 +32,9 @@ dependencies = [ [[package]] name = "actix-http" -version = "3.0.4" +version = "3.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5885cb81a0d4d0d322864bea1bb6c2a8144626b4fdc625d4c51eba197e7797a" +checksum = "bd2e9f6794b5826aff6df65e3a0d0127b271d1c03629c774238f3582e903d4e4" dependencies = [ "actix-codec", "actix-rt", @@ -57,13 +57,13 @@ dependencies = [ "itoa", "language-tags", "local-channel", - "log", "mime", "percent-encoding", "pin-project-lite", "rand", - "sha-1", + "sha1", "smallvec", + "tracing", "zstd", ] @@ -142,9 +142,9 @@ dependencies = [ [[package]] name = "actix-web" -version = "4.0.1" +version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4e5ebffd51d50df56a3ae0de0e59487340ca456f05dd0b90c0a7a6dd6a74d31" +checksum = "a27e8fe9ba4ae613c21f677c2cfaf0696c3744030c6f485b34634e502d6bb379" dependencies = [ "actix-codec", "actix-http", @@ -182,9 +182,9 @@ dependencies = [ [[package]] name = "actix-web-codegen" -version = "4.0.0" +version = "4.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7525bedf54704abb1d469e88d7e7e9226df73778798a69cea5022d53b2ae91bc" +checksum = "5f270541caec49c15673b0af0e9a00143421ad4f118d2df7edcb68b627632f56" dependencies = [ "actix-router", "proc-macro2", @@ -381,9 +381,9 @@ checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8" [[package]] name = "bytestring" -version = "1.0.0" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90706ba19e97b90786e19dc0d5e2abd80008d99d4c0c5d1ad0b5e72cec7c494d" +checksum = "86b6a75fd3048808ef06af5cd79712be8111960adaf89d90250974b38fc3928a" dependencies = [ "bytes", ] @@ -412,7 +412,6 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" version = "0.4.19" -source = "git+https://github.com/sbrocket/chrono?branch=parse-error-kind-public#d9cac30bd0fb46ed410718e6c83753a7c9daa669" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73" dependencies = [ @@ -422,6 +421,18 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "chrono" +version = "0.4.19" +source = "git+https://github.com/sbrocket/chrono?branch=parse-error-kind-public#d9cac30bd0fb46ed410718e6c83753a7c9daa669" +dependencies = [ + "libc", + "num-integer", + "num-traits", + "time 0.1.44", + "winapi 0.3.9", +] + [[package]] name = "clap" version = "3.1.18" @@ -739,9 +750,9 @@ dependencies = [ [[package]] name = "flume" -version = "0.10.12" +version = "0.10.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843c03199d0c0ca54bc1ea90ac0d507274c28abcc4f691ae8b4eaa375087c76a" +checksum = "1ceeb589a3157cac0ab8cc585feb749bd2cea5cb55a6ee802ad72d9fd38303da" dependencies = [ "futures-core", "futures-sink", @@ -1989,9 +2000,9 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.9" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cb243bdfdb5936c8dc3c45762a19d12ab4550cdc753bc247637d4ec35a040fd" +checksum = "a41d061efea015927ac527063765e73601444cdc344ba855bc7bd44578b25e1c" [[package]] name = "serde" @@ -2049,10 +2060,10 @@ dependencies = [ ] [[package]] -name = "sha-1" -version = "0.10.0" +name = "sha1" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "028f48d513f9678cda28f6e4064755b3fbb2af6acd672f2c209b62323f7aea0f" +checksum = "c77f4e7f65455545c2153c1253d25056825e77ee2533f0e41deb65a93a34852f" dependencies = [ "cfg-if 1.0.0", "cpufeatures", @@ -2772,18 +2783,18 @@ dependencies = [ [[package]] name = "zstd" -version = "0.10.2+zstd.1.5.2" +version = "0.11.2+zstd.1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f4a6bd64f22b5e3e94b4e238669ff9f10815c27a5180108b849d24174a83847" +checksum = "20cc960326ece64f010d2d2107537f26dc589a6573a316bd5b1dba685fa5fde4" dependencies = [ "zstd-safe", ] [[package]] name = "zstd-safe" -version = "4.1.6+zstd.1.5.2" +version = "5.0.2+zstd.1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94b61c51bb270702d6167b8ce67340d2754b088d0c091b06e593aa772c3ee9bb" +checksum = "1d2a5585e04f9eea4b2a3d1eca508c4dee9592a89ef6f450c11719da0726f4db" dependencies = [ "libc", "zstd-sys", @@ -2791,9 +2802,9 @@ dependencies = [ [[package]] name = "zstd-sys" -version = "1.6.3+zstd.1.5.2" +version = "2.0.1+zstd.1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc49afa5c8d634e75761feda8c592051e7eeb4683ba827211eb0d731d3402ea8" +checksum = "9fd07cbbc53846d9145dbffdf6dd09a7a0aa52be46741825f5c97bdd4f73f12b" dependencies = [ "cc", "libc", From bd259225af182927e279aacf7a9006e785418771 Mon Sep 17 00:00:00 2001 From: jb-alvarado Date: Mon, 13 Jun 2022 14:25:47 +0200 Subject: [PATCH 14/47] exclude db file --- cross_compile_all.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cross_compile_all.sh b/cross_compile_all.sh index b7e0d4ce..7e0f0a0c 100755 --- a/cross_compile_all.sh +++ b/cross_compile_all.sh @@ -25,7 +25,7 @@ for target in "${targets[@]}"; do fi cp ./target/${target}/release/ffplayout.exe . - zip -r "ffplayout-engine-v${version}_${target}.zip" assets docs LICENSE README.md ffplayout.exe + zip -r "ffplayout-engine-v${version}_${target}.zip" assets docs LICENSE README.md ffplayout.exe -x *.db rm -f ffplayout.exe else if [[ -f "ffplayout-engine-v${version}_${target}.tar.gz" ]]; then @@ -33,7 +33,7 @@ for target in "${targets[@]}"; do fi cp ./target/${target}/release/ffplayout . - tar -czvf "ffplayout-engine-v${version}_${target}.tar.gz" assets docs LICENSE README.md ffplayout + tar -czvf "ffplayout-engine-v${version}_${target}.tar.gz" --exclude='*.db' assets docs LICENSE README.md ffplayout rm -f ffplayout fi From b15eb449dcebaaec6eae116da614df04be462753 Mon Sep 17 00:00:00 2001 From: jb-alvarado Date: Mon, 13 Jun 2022 18:29:37 +0200 Subject: [PATCH 15/47] switch to role based auth --- src/api/auth.rs | 6 +++--- src/api/handles.rs | 2 -- src/api/routes.rs | 25 +++++++++++++++++++------ src/api/utils.rs | 17 +++++++++++++++++ src/bin/ffpapi.rs | 9 ++++++--- 5 files changed, 45 insertions(+), 14 deletions(-) diff --git a/src/api/auth.rs b/src/api/auth.rs index 9c2f902d..14c625a2 100644 --- a/src/api/auth.rs +++ b/src/api/auth.rs @@ -13,16 +13,16 @@ const JWT_EXPIRATION_MINUTES: i64 = 60; pub struct Claims { pub id: i64, pub username: String, - pub permissions: Vec, + pub role: String, exp: i64, } impl Claims { - pub fn new(id: i64, username: String, permissions: Vec) -> Self { + pub fn new(id: i64, username: String, role: String) -> Self { Self { id, username, - permissions, + role, exp: (Utc::now() + Duration::minutes(JWT_EXPIRATION_MINUTES)).timestamp(), } } diff --git a/src/api/handles.rs b/src/api/handles.rs index c4f6ce40..6882c5c7 100644 --- a/src/api/handles.rs +++ b/src/api/handles.rs @@ -128,8 +128,6 @@ pub async fn db_get_settings(id: &i64) -> Result { let result: Settings = sqlx::query_as(query).bind(id).fetch_one(&conn).await?; conn.close().await; - println!("{:#?}", result); - Ok(result) } diff --git a/src/api/routes.rs b/src/api/routes.rs index 889beccb..fcfc32a6 100644 --- a/src/api/routes.rs +++ b/src/api/routes.rs @@ -1,5 +1,5 @@ use actix_web::{get, http::StatusCode, patch, post, put, web, Responder}; -use actix_web_grants::proc_macro::has_permissions; +use actix_web_grants::proc_macro::has_any_role; use argon2::{ password_hash::{rand_core::OsRng, PasswordHash, SaltString}, Argon2, PasswordHasher, PasswordVerifier, @@ -14,6 +14,7 @@ use crate::api::{ db_add_user, db_get_settings, db_login, db_role, db_update_settings, db_update_user, }, models::{LoginUser, Settings, User}, + utils::Role, }; #[derive(Serialize)] @@ -25,7 +26,7 @@ struct ResponseObj { /// curl -X GET http://127.0.0.1:8080/api/settings/1 -H "Authorization: Bearer " #[get("/settings/{id}")] -#[has_permissions("admin", "user")] +#[has_any_role("Role::Admin", "Role::User", type = "Role")] async fn get_settings(id: web::Path) -> Result { if let Ok(settings) = db_get_settings(&id).await { return Ok(web::Json(ResponseObj { @@ -43,7 +44,7 @@ async fn get_settings(id: web::Path) -> Result" #[patch("/settings/{id}")] -#[has_permissions("admin")] +#[has_any_role("Role::Admin", type = "Role")] async fn patch_settings( id: web::Path, data: web::Json, @@ -55,10 +56,22 @@ async fn patch_settings( Err(ServiceError::InternalServerError) } +#[get("/playout/config/{id}")] +#[has_any_role("Role::Admin", "Role::User", type = "Role")] +async fn get_playout_config(id: web::Path) -> Result { + if let Ok(settings) = db_get_settings(&id).await { + println!("{:?}", settings.config_path); + + return Ok("settings"); + }; + + Err(ServiceError::InternalServerError) +} + /// curl -X PUT http://localhost:8080/api/user/1 --header 'Content-Type: application/json' \ /// --data '{"email": "", "password": ""}' --header 'Authorization: ' #[put("/user/{id}")] -#[has_permissions("admin", "user")] +#[has_any_role("Role::Admin", "Role::User", type = "Role")] async fn update_user( id: web::Path, user: web::ReqData, @@ -98,7 +111,7 @@ async fn update_user( /// -d '{"email": "", "username": "", "password": "", "role_id": 1}' \ /// --header 'Authorization: Bearer ' #[post("/user/")] -#[has_permissions("admin")] +#[has_any_role("Role::Admin", type = "Role")] async fn add_user(data: web::Json) -> Result { match db_add_user(data.into_inner()).await { Ok(_) => Ok("Add User Success"), @@ -127,7 +140,7 @@ pub async fn login(credentials: web::Json) -> impl Responder { let role = db_role(&user.role_id.unwrap_or_default()) .await .unwrap_or_else(|_| "guest".to_string()); - let claims = Claims::new(user.id, user.username.clone(), vec![role.clone()]); + let claims = Claims::new(user.id, user.username.clone(), role.clone()); if let Ok(token) = create_jwt(claims) { user.token = Some(token); diff --git a/src/api/utils.rs b/src/api/utils.rs index 58017868..71caa0aa 100644 --- a/src/api/utils.rs +++ b/src/api/utils.rs @@ -7,6 +7,23 @@ use crate::api::{ models::User, }; +#[derive(PartialEq, Clone)] +pub enum Role { + Admin, + User, + Guest, +} + +impl Role { + pub fn set_role(role: &str) -> Self { + match role { + "admin" => Role::Admin, + "user" => Role::User, + _ => Role::Guest, + } + } +} + #[derive(Debug, sqlx::FromRow)] pub struct GlobalSettings { pub secret: String, diff --git a/src/bin/ffpapi.rs b/src/bin/ffpapi.rs index eadbea87..220bef3c 100644 --- a/src/bin/ffpapi.rs +++ b/src/bin/ffpapi.rs @@ -13,8 +13,8 @@ use ffplayout_engine::{ args_parse::Args, auth, models::LoginUser, - routes::{add_user, get_settings, login, patch_settings, update_user}, - utils::{init_config, run_args}, + routes::{add_user, get_playout_config, get_settings, login, patch_settings, update_user}, + utils::{init_config, run_args, Role}, }, utils::{init_logging, GlobalConfig}, }; @@ -22,10 +22,12 @@ use ffplayout_engine::{ async fn validator(req: ServiceRequest, credentials: BearerAuth) -> Result { // We just get permissions from JWT let claims = auth::decode_jwt(credentials.token()).await?; - req.attach(claims.permissions); + req.attach(vec![Role::set_role(&claims.role)]); req.extensions_mut() .insert(LoginUser::new(claims.id, claims.username)); + + println!("{:#?}", req); Ok(req) } @@ -63,6 +65,7 @@ async fn main() -> std::io::Result<()> { web::scope("/api") .wrap(auth) .service(add_user) + .service(get_playout_config) .service(get_settings) .service(patch_settings) .service(update_user), From 34b99ae784bc3ebd42a9dd1a379f671a221495c8 Mon Sep 17 00:00:00 2001 From: jb-alvarado Date: Tue, 14 Jun 2022 09:35:46 +0200 Subject: [PATCH 16/47] remove print --- src/output/stream.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/output/stream.rs b/src/output/stream.rs index 0559e5d5..836fdb59 100644 --- a/src/output/stream.rs +++ b/src/output/stream.rs @@ -44,8 +44,6 @@ pub fn output(config: &GlobalConfig, log_format: &str) -> process::Child { enc_cmd.append(&mut preview_cmd); } - println!("{enc_filter:?}"); - enc_cmd.append(&mut output_cmd); let enc_cmd = prepare_output_cmd(enc_prefix, enc_filter, enc_cmd, &config.out.mode); From 74f968e2840d88fd0462da830e059571de506536 Mon Sep 17 00:00:00 2001 From: jb-alvarado Date: Tue, 14 Jun 2022 12:08:25 +0200 Subject: [PATCH 17/47] support log path none --- src/utils/logging.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/utils/logging.rs b/src/utils/logging.rs index 2b853d52..a8ce2c31 100644 --- a/src/utils/logging.rs +++ b/src/utils/logging.rs @@ -22,10 +22,10 @@ use log::{Level, LevelFilter, Log, Metadata, Record}; use regex::Regex; use simplelog::*; -use crate::utils::{GlobalConfig, ProcessControl}; +use crate::utils::{PlayoutConfig, ProcessControl}; /// send log messages to mail recipient -pub fn send_mail(cfg: &GlobalConfig, msg: String) { +pub fn send_mail(cfg: &PlayoutConfig, msg: String) { let recip = cfg .mail .recipient @@ -68,7 +68,7 @@ pub fn send_mail(cfg: &GlobalConfig, msg: String) { /// /// Check every give seconds for messages and send them. fn mail_queue( - cfg: GlobalConfig, + cfg: PlayoutConfig, proc_ctl: ProcessControl, messages: Arc>>, interval: u64, @@ -166,7 +166,7 @@ fn clean_string(text: &str) -> String { /// - file logger /// - mail logger pub fn init_logging( - config: &GlobalConfig, + config: &PlayoutConfig, proc_ctl: Option, messages: Option>>>, ) -> Vec> { @@ -195,7 +195,7 @@ pub fn init_logging( }; }; - if app_config.log_to_file { + if app_config.log_to_file && &app_config.log_path != "none" { let file_config = log_config .clone() .set_time_format_custom(format_description!( From b97f30c2b4ced0b437b1770832af868d9ccf7187 Mon Sep 17 00:00:00 2001 From: jb-alvarado Date: Tue, 14 Jun 2022 12:09:31 +0200 Subject: [PATCH 18/47] rename GlobalConfig and Playlist struct, get playlist config --- src/api/auth.rs | 6 ++-- src/api/handles.rs | 22 +++--------- src/api/routes.rs | 65 +++++++++++++++++++++++++++++++++--- src/api/utils.rs | 24 +++++++++++++ src/bin/ffpapi.rs | 14 +++++--- src/filter/a_loudnorm.rs | 4 +-- src/filter/ingest_filter.rs | 6 ++-- src/filter/mod.rs | 27 +++++++++------ src/filter/v_drawtext.rs | 4 +-- src/filter/v_overlay.rs | 4 +-- src/input/folder.rs | 8 ++--- src/input/ingest.rs | 8 +++-- src/input/mod.rs | 4 +-- src/input/playlist.rs | 12 +++---- src/main.rs | 6 ++-- src/output/desktop.rs | 4 +-- src/output/hls.rs | 6 ++-- src/output/mod.rs | 5 +-- src/output/stream.rs | 4 +-- src/rpc/mod.rs | 6 ++-- src/tests/mod.rs | 4 +-- src/tests/utils/mod.rs | 2 +- src/utils/config.rs | 8 ++--- src/utils/generator.rs | 6 ++-- src/utils/json_serializer.rs | 24 +++++++------ src/utils/json_validate.rs | 8 +++-- src/utils/mod.rs | 18 +++++----- 27 files changed, 200 insertions(+), 109 deletions(-) diff --git a/src/api/auth.rs b/src/api/auth.rs index 14c625a2..ba34919e 100644 --- a/src/api/auth.rs +++ b/src/api/auth.rs @@ -6,8 +6,8 @@ use serde::{Deserialize, Serialize}; use crate::api::utils::GlobalSettings; -// Token lifetime and Secret key are hardcoded for clarity -const JWT_EXPIRATION_MINUTES: i64 = 60; +// Token lifetime +const JWT_EXPIRATION_DAYS: i64 = 7; #[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] pub struct Claims { @@ -23,7 +23,7 @@ impl Claims { id, username, role, - exp: (Utc::now() + Duration::minutes(JWT_EXPIRATION_MINUTES)).timestamp(), + exp: (Utc::now() + Duration::days(JWT_EXPIRATION_DAYS)).timestamp(), } } } diff --git a/src/api/handles.rs b/src/api/handles.rs index 6882c5c7..2c247f84 100644 --- a/src/api/handles.rs +++ b/src/api/handles.rs @@ -1,35 +1,23 @@ -use std::path::Path; - use argon2::{ password_hash::{rand_core::OsRng, SaltString}, Argon2, PasswordHasher, }; -use faccess::PathExt; + use rand::{distributions::Alphanumeric, Rng}; use simplelog::*; use sqlx::{migrate::MigrateDatabase, sqlite::SqliteQueryResult, Pool, Sqlite, SqlitePool}; -use crate::api::models::{Settings, User}; use crate::api::utils::GlobalSettings; +use crate::api::{ + models::{Settings, User}, + utils::db_path, +}; #[derive(Debug, sqlx::FromRow)] struct Role { name: String, } -pub fn db_path() -> Result> { - let sys_path = Path::new("/usr/share/ffplayout"); - let mut db_path = String::from("./ffplayout.db"); - - if sys_path.is_dir() && sys_path.writable() { - db_path = String::from("/usr/share/ffplayout/ffplayout.db"); - } else if Path::new("./assets").is_dir() { - db_path = String::from("./assets/ffplayout.db"); - } - - Ok(db_path) -} - async fn create_schema() -> Result { let conn = db_connection().await?; let query = "PRAGMA foreign_keys = ON; diff --git a/src/api/routes.rs b/src/api/routes.rs index fcfc32a6..82cd7a54 100644 --- a/src/api/routes.rs +++ b/src/api/routes.rs @@ -1,5 +1,5 @@ use actix_web::{get, http::StatusCode, patch, post, put, web, Responder}; -use actix_web_grants::proc_macro::has_any_role; +use actix_web_grants::{permissions::AuthDetails, proc_macro::has_any_role}; use argon2::{ password_hash::{rand_core::OsRng, PasswordHash, SaltString}, Argon2, PasswordHasher, PasswordVerifier, @@ -14,9 +14,11 @@ use crate::api::{ db_add_user, db_get_settings, db_login, db_role, db_update_settings, db_update_user, }, models::{LoginUser, Settings, User}, - utils::Role, + utils::{read_playout_config, Role}, }; +use crate::utils::playout_config; + #[derive(Serialize)] struct ResponseObj { message: String, @@ -24,6 +26,37 @@ struct ResponseObj { data: Option, } +#[derive(Debug, Serialize, Clone)] +struct ResponsePlayoutConfig { + general: Option, + rpc_server: Option, + mail: Option, + logging: Option, + processing: Option, + ingest: Option, + playlist: Option, + storage: Option, + text: Option, + out: Option, +} + +impl ResponsePlayoutConfig { + fn new() -> Self { + Self { + general: None, + rpc_server: None, + mail: None, + logging: None, + processing: None, + ingest: None, + playlist: None, + storage: None, + text: None, + out: None, + } + } +} + /// curl -X GET http://127.0.0.1:8080/api/settings/1 -H "Authorization: Bearer " #[get("/settings/{id}")] #[has_any_role("Role::Admin", "Role::User", type = "Role")] @@ -56,13 +89,35 @@ async fn patch_settings( Err(ServiceError::InternalServerError) } +/// curl -X GET http://localhost:8080/api/playout/config/1 --header 'Authorization: ' #[get("/playout/config/{id}")] #[has_any_role("Role::Admin", "Role::User", type = "Role")] -async fn get_playout_config(id: web::Path) -> Result { +async fn get_playout_config( + id: web::Path, + details: AuthDetails, +) -> Result { if let Ok(settings) = db_get_settings(&id).await { - println!("{:?}", settings.config_path); + if let Ok(config) = read_playout_config(&settings.config_path) { + let mut playout_cfg = ResponsePlayoutConfig::new(); - return Ok("settings"); + playout_cfg.playlist = Some(config.playlist); + playout_cfg.storage = Some(config.storage); + playout_cfg.text = Some(config.text); + + if details.has_role(&Role::Admin) { + playout_cfg.general = Some(config.general); + playout_cfg.rpc_server = Some(config.rpc_server); + playout_cfg.mail = Some(config.mail); + playout_cfg.logging = Some(config.logging); + playout_cfg.processing = Some(config.processing); + playout_cfg.ingest = Some(config.ingest); + playout_cfg.out = Some(config.out); + + return Ok(web::Json(playout_cfg)); + } + + return Ok(web::Json(playout_cfg)); + } }; Err(ServiceError::InternalServerError) diff --git a/src/api/utils.rs b/src/api/utils.rs index 71caa0aa..1838fc19 100644 --- a/src/api/utils.rs +++ b/src/api/utils.rs @@ -1,3 +1,6 @@ +use std::{error::Error, fs::File, path::Path}; + +use faccess::PathExt; use once_cell::sync::OnceCell; use simplelog::*; @@ -6,6 +9,7 @@ use crate::api::{ handles::{db_add_user, db_global, db_init}, models::User, }; +use crate::utils::PlayoutConfig; #[derive(PartialEq, Clone)] pub enum Role { @@ -53,6 +57,19 @@ pub async fn init_config() { INSTANCE.set(config).unwrap(); } +pub fn db_path() -> Result> { + let sys_path = Path::new("/usr/share/ffplayout"); + let mut db_path = String::from("./ffplayout.db"); + + if sys_path.is_dir() && sys_path.writable() { + db_path = String::from("/usr/share/ffplayout/ffplayout.db"); + } else if Path::new("./assets").is_dir() { + db_path = String::from("./assets/ffplayout.db"); + } + + Ok(db_path) +} + pub async fn run_args(args: Args) -> Result<(), i32> { if !args.init && args.listen.is_none() && args.username.is_none() { error!("Wrong number of arguments! Run ffpapi --help for more information."); @@ -96,3 +113,10 @@ pub async fn run_args(args: Args) -> Result<(), i32> { Ok(()) } + +pub fn read_playout_config(path: &str) -> Result> { + let file = File::open(path)?; + let config: PlayoutConfig = serde_yaml::from_reader(file)?; + + Ok(config) +} diff --git a/src/bin/ffpapi.rs b/src/bin/ffpapi.rs index 220bef3c..007b4bcb 100644 --- a/src/bin/ffpapi.rs +++ b/src/bin/ffpapi.rs @@ -1,4 +1,4 @@ -use std::process::exit; +use std::{path::Path, process::exit}; use actix_web::{dev::ServiceRequest, middleware, web, App, Error, HttpMessage, HttpServer}; use actix_web_grants::permissions::AttachPermissions; @@ -14,9 +14,9 @@ use ffplayout_engine::{ auth, models::LoginUser, routes::{add_user, get_playout_config, get_settings, login, patch_settings, update_user}, - utils::{init_config, run_args, Role}, + utils::{db_path, init_config, run_args, Role}, }, - utils::{init_logging, GlobalConfig}, + utils::{init_logging, PlayoutConfig}, }; async fn validator(req: ServiceRequest, credentials: BearerAuth) -> Result { @@ -35,7 +35,7 @@ async fn validator(req: ServiceRequest, credentials: BearerAuth) -> Result std::io::Result<()> { let args = Args::parse(); - let mut config = GlobalConfig::new(None); + let mut config = PlayoutConfig::new(None); config.mail.recipient = String::new(); config.logging.log_to_file = false; config.logging.timestamp = false; @@ -48,6 +48,12 @@ async fn main() -> std::io::Result<()> { } if let Some(conn) = args.listen { + if let Ok(p) = db_path() { + if !Path::new(&p).is_file() { + error!("Database is not initialized! Init DB first and add admin user."); + exit(1); + } + } init_config().await; let ip_port = conn.split(':').collect::>(); let addr = ip_port[0]; diff --git a/src/filter/a_loudnorm.rs b/src/filter/a_loudnorm.rs index efc6d3b6..57662082 100644 --- a/src/filter/a_loudnorm.rs +++ b/src/filter/a_loudnorm.rs @@ -1,9 +1,9 @@ -use crate::utils::GlobalConfig; +use crate::utils::PlayoutConfig; /// Loudnorm Audio Filter /// /// Add loudness normalization. -pub fn filter_node(config: &GlobalConfig) -> String { +pub fn filter_node(config: &PlayoutConfig) -> String { format!( "loudnorm=I={}:TP={}:LRA={}", config.processing.loud_i, config.processing.loud_tp, config.processing.loud_lra diff --git a/src/filter/ingest_filter.rs b/src/filter/ingest_filter.rs index 9838ee89..577221c7 100644 --- a/src/filter/ingest_filter.rs +++ b/src/filter/ingest_filter.rs @@ -1,10 +1,10 @@ use crate::filter::{a_loudnorm, v_overlay}; -use crate::utils::GlobalConfig; +use crate::utils::PlayoutConfig; /// Audio Filter /// /// If needed we add audio filters to the server instance. -fn audio_filter(config: &GlobalConfig) -> String { +fn audio_filter(config: &PlayoutConfig) -> String { let mut audio_chain = ";[0:a]afade=in:st=0:d=0.5".to_string(); if config.processing.loudnorm_ingest { @@ -22,7 +22,7 @@ fn audio_filter(config: &GlobalConfig) -> String { } /// Create filter nodes for ingest live stream. -pub fn filter_cmd(config: &GlobalConfig) -> Vec { +pub fn filter_cmd(config: &PlayoutConfig) -> Vec { let mut filter = format!( "[0:v]fps={},scale={}:{},setdar=dar={},fade=in:st=0:d=0.5", config.processing.fps, diff --git a/src/filter/mod.rs b/src/filter/mod.rs index 9c99aa68..a8a2009b 100644 --- a/src/filter/mod.rs +++ b/src/filter/mod.rs @@ -7,7 +7,7 @@ pub mod ingest_filter; pub mod v_drawtext; pub mod v_overlay; -use crate::utils::{get_delta, is_close, GlobalConfig, Media}; +use crate::utils::{get_delta, is_close, Media, PlayoutConfig}; #[derive(Debug, Clone)] struct Filters { @@ -72,7 +72,7 @@ fn deinterlace(field_order: &Option, chain: &mut Filters) { } } -fn pad(aspect: f64, chain: &mut Filters, config: &GlobalConfig) { +fn pad(aspect: f64, chain: &mut Filters, config: &PlayoutConfig) { if !is_close(aspect, config.processing.aspect, 0.03) { chain.add_filter( &format!( @@ -84,13 +84,13 @@ fn pad(aspect: f64, chain: &mut Filters, config: &GlobalConfig) { } } -fn fps(fps: f64, chain: &mut Filters, config: &GlobalConfig) { +fn fps(fps: f64, chain: &mut Filters, config: &PlayoutConfig) { if fps != config.processing.fps { chain.add_filter(&format!("fps={}", config.processing.fps), "video") } } -fn scale(v_stream: &ffprobe::Stream, aspect: f64, chain: &mut Filters, config: &GlobalConfig) { +fn scale(v_stream: &ffprobe::Stream, aspect: f64, chain: &mut Filters, config: &PlayoutConfig) { // width: i64, height: i64 if let (Some(w), Some(h)) = (v_stream.width, v_stream.height) { if w != config.processing.width || h != config.processing.height { @@ -137,7 +137,7 @@ fn fade(node: &mut Media, chain: &mut Filters, codec_type: &str) { } } -fn overlay(node: &mut Media, chain: &mut Filters, config: &GlobalConfig) { +fn overlay(node: &mut Media, chain: &mut Filters, config: &PlayoutConfig) { if config.processing.add_logo && Path::new(&config.processing.logo).is_file() && &node.category.clone().unwrap_or_default() != "advertisement" @@ -183,7 +183,7 @@ fn extend_video(node: &mut Media, chain: &mut Filters) { } /// add drawtext filter for lower thirds messages -fn add_text(node: &mut Media, chain: &mut Filters, config: &GlobalConfig) { +fn add_text(node: &mut Media, chain: &mut Filters, config: &PlayoutConfig) { if config.text.add_text && config.text.over_pre { let filter = v_drawtext::filter_node(config, node); @@ -233,7 +233,7 @@ fn extend_audio(node: &mut Media, chain: &mut Filters) { } /// Add single pass loudnorm filter to audio line. -fn add_loudnorm(node: &mut Media, chain: &mut Filters, config: &GlobalConfig) { +fn add_loudnorm(node: &mut Media, chain: &mut Filters, config: &PlayoutConfig) { if config.processing.add_loudnorm && !node .probe @@ -247,13 +247,13 @@ fn add_loudnorm(node: &mut Media, chain: &mut Filters, config: &GlobalConfig) { } } -fn audio_volume(chain: &mut Filters, config: &GlobalConfig) { +fn audio_volume(chain: &mut Filters, config: &PlayoutConfig) { if config.processing.volume != 1.0 { chain.add_filter(&format!("volume={}", config.processing.volume), "audio") } } -fn aspect_calc(aspect_string: &Option, config: &GlobalConfig) -> f64 { +fn aspect_calc(aspect_string: &Option, config: &PlayoutConfig) -> f64 { let mut source_aspect = config.processing.aspect; if let Some(aspect) = aspect_string { @@ -276,7 +276,12 @@ fn fps_calc(r_frame_rate: &str) -> f64 { } /// This realtime filter is important for HLS output to stay in sync. -fn realtime_filter(node: &mut Media, chain: &mut Filters, config: &GlobalConfig, codec_type: &str) { +fn realtime_filter( + node: &mut Media, + chain: &mut Filters, + config: &PlayoutConfig, + codec_type: &str, +) { let mut t = ""; if codec_type == "audio" { @@ -300,7 +305,7 @@ fn realtime_filter(node: &mut Media, chain: &mut Filters, config: &GlobalConfig, } } -pub fn filter_chains(config: &GlobalConfig, node: &mut Media) -> Vec { +pub fn filter_chains(config: &PlayoutConfig, node: &mut Media) -> Vec { let mut filters = Filters::new(); if let Some(probe) = node.probe.as_ref() { diff --git a/src/filter/v_drawtext.rs b/src/filter/v_drawtext.rs index 47c6ae14..8ff6cb6e 100644 --- a/src/filter/v_drawtext.rs +++ b/src/filter/v_drawtext.rs @@ -2,9 +2,9 @@ use std::path::Path; use regex::Regex; -use crate::utils::{GlobalConfig, Media}; +use crate::utils::{Media, PlayoutConfig}; -pub fn filter_node(config: &GlobalConfig, node: &mut Media) -> String { +pub fn filter_node(config: &PlayoutConfig, node: &mut Media) -> String { let mut filter = String::new(); let mut font = String::new(); diff --git a/src/filter/v_overlay.rs b/src/filter/v_overlay.rs index 0607090c..de736a9c 100644 --- a/src/filter/v_overlay.rs +++ b/src/filter/v_overlay.rs @@ -1,11 +1,11 @@ use std::path::Path; -use crate::utils::GlobalConfig; +use crate::utils::PlayoutConfig; /// Overlay Filter /// /// When a logo is set, we create here the filter for the server. -pub fn filter_node(config: &GlobalConfig, add_tail: bool) -> String { +pub fn filter_node(config: &PlayoutConfig, add_tail: bool) -> String { let mut logo_chain = String::new(); if config.processing.add_logo && Path::new(&config.processing.logo).is_file() { diff --git a/src/input/folder.rs b/src/input/folder.rs index 9c9cf69c..7dbd68d9 100644 --- a/src/input/folder.rs +++ b/src/input/folder.rs @@ -19,14 +19,14 @@ use rand::{seq::SliceRandom, thread_rng}; use simplelog::*; use walkdir::WalkDir; -use crate::utils::{get_sec, GlobalConfig, Media}; +use crate::utils::{get_sec, Media, PlayoutConfig}; /// Folder Sources /// /// Like playlist source, we create here a folder list for iterate over it. #[derive(Debug, Clone)] pub struct FolderSource { - config: GlobalConfig, + config: PlayoutConfig, pub nodes: Arc>>, current_node: Media, index: Arc, @@ -34,7 +34,7 @@ pub struct FolderSource { impl FolderSource { pub fn new( - config: &GlobalConfig, + config: &PlayoutConfig, current_list: Arc>>, global_index: Arc, ) -> Self { @@ -163,7 +163,7 @@ fn file_extension(filename: &Path) -> Option<&str> { /// When a change is register, update the current file list. /// This makes it possible, to play infinitely and and always new files to it. pub fn watchman( - config: GlobalConfig, + config: PlayoutConfig, is_terminated: Arc, sources: Arc>>, ) { diff --git a/src/input/ingest.rs b/src/input/ingest.rs index e65b72c2..a5cd4c28 100644 --- a/src/input/ingest.rs +++ b/src/input/ingest.rs @@ -9,7 +9,7 @@ use crossbeam_channel::Sender; use simplelog::*; use crate::filter::ingest_filter::filter_cmd; -use crate::utils::{format_log_line, GlobalConfig, Ingest, ProcessControl}; +use crate::utils::{format_log_line, Ingest, PlayoutConfig, ProcessControl}; use crate::vec_strings; pub fn log_line(line: String, level: &str) { @@ -55,6 +55,10 @@ fn server_monitor( ); } + if line.contains("Address already in use") { + proc_ctl.kill_all(); + } + log_line(line, level); } @@ -65,7 +69,7 @@ fn server_monitor( /// /// Start ffmpeg in listen mode, and wait for input. pub fn ingest_server( - config: GlobalConfig, + config: PlayoutConfig, ingest_sender: Sender<(usize, [u8; 65088])>, mut proc_control: ProcessControl, ) -> Result<(), Error> { diff --git a/src/input/mod.rs b/src/input/mod.rs index a3dff4e5..9c1ffc6e 100644 --- a/src/input/mod.rs +++ b/src/input/mod.rs @@ -9,7 +9,7 @@ use std::{ use simplelog::*; -use crate::utils::{GlobalConfig, Media, PlayoutStatus}; +use crate::utils::{Media, PlayoutConfig, PlayoutStatus}; pub mod folder; pub mod ingest; @@ -21,7 +21,7 @@ pub use playlist::CurrentProgram; /// Create a source iterator from playlist, or from folder. pub fn source_generator( - config: GlobalConfig, + config: PlayoutConfig, current_list: Arc>>, index: Arc, playout_stat: PlayoutStatus, diff --git a/src/input/playlist.rs b/src/input/playlist.rs index c67e71ac..8b2b0ad9 100644 --- a/src/input/playlist.rs +++ b/src/input/playlist.rs @@ -12,7 +12,7 @@ use simplelog::*; use crate::utils::{ check_sync, gen_dummy, get_delta, get_sec, is_close, is_remote, json_serializer::read_json, - modified_time, seek_and_length, valid_source, GlobalConfig, Media, PlayoutStatus, DUMMY_LEN, + modified_time, seek_and_length, valid_source, Media, PlayoutConfig, PlayoutStatus, DUMMY_LEN, }; /// Struct for current playlist. @@ -20,7 +20,7 @@ use crate::utils::{ /// Here we prepare the init clip and build a iterator where we pull our clips. #[derive(Debug)] pub struct CurrentProgram { - config: GlobalConfig, + config: PlayoutConfig, start_sec: f64, json_mod: Option, json_path: Option, @@ -34,7 +34,7 @@ pub struct CurrentProgram { impl CurrentProgram { pub fn new( - config: &GlobalConfig, + config: &PlayoutConfig, playout_stat: PlayoutStatus, is_terminated: Arc, current_list: Arc>>, @@ -390,7 +390,7 @@ impl Iterator for CurrentProgram { /// - return clip only if we are in 24 hours time range fn timed_source( node: Media, - config: &GlobalConfig, + config: &PlayoutConfig, last: bool, playout_stat: &PlayoutStatus, ) -> Media { @@ -440,7 +440,7 @@ fn timed_source( } /// Generate the source CMD, or when clip not exist, get a dummy. -fn gen_source(config: &GlobalConfig, mut node: Media) -> Media { +fn gen_source(config: &PlayoutConfig, mut node: Media) -> Media { if valid_source(&node.source) { node.add_probe(); node.cmd = Some(seek_and_length( @@ -470,7 +470,7 @@ fn gen_source(config: &GlobalConfig, mut node: Media) -> Media { /// Handle init clip, but this clip can be the last one in playlist, /// this we have to figure out and calculate the right length. -fn handle_list_init(config: &GlobalConfig, mut node: Media) -> Media { +fn handle_list_init(config: &PlayoutConfig, mut node: Media) -> Media { debug!("Playlist init"); let (_, total_delta) = get_delta(config, &node.begin.unwrap()); let mut out = node.out; diff --git a/src/main.rs b/src/main.rs index da758623..1e7654c3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -14,8 +14,8 @@ use ffplayout_engine::{ output::{player, write_hls}, rpc::json_rpc_server, utils::{ - generate_playlist, get_args, init_logging, send_mail, validate_ffmpeg, GlobalConfig, - PlayerControl, PlayoutStatus, ProcessControl, + generate_playlist, get_args, init_logging, send_mail, validate_ffmpeg, PlayerControl, + PlayoutConfig, PlayoutStatus, ProcessControl, }, }; @@ -57,7 +57,7 @@ fn status_file(stat_file: &str, playout_stat: &PlayoutStatus) { fn main() { let args = get_args(); - let config = GlobalConfig::new(Some(args)); + let config = PlayoutConfig::new(Some(args)); let config_clone = config.clone(); let play_control = PlayerControl::new(); let playout_stat = PlayoutStatus::new(); diff --git a/src/output/desktop.rs b/src/output/desktop.rs index 5f0925a7..ffe72f37 100644 --- a/src/output/desktop.rs +++ b/src/output/desktop.rs @@ -3,13 +3,13 @@ use std::process::{self, Command, Stdio}; use simplelog::*; use crate::filter::v_drawtext; -use crate::utils::{GlobalConfig, Media}; +use crate::utils::{Media, PlayoutConfig}; use crate::vec_strings; /// Desktop Output /// /// Instead of streaming, we run a ffplay instance and play on desktop. -pub fn output(config: &GlobalConfig, log_format: &str) -> process::Child { +pub fn output(config: &PlayoutConfig, log_format: &str) -> process::Child { let mut enc_filter: Vec = vec![]; let mut enc_cmd = vec_strings!["-hide_banner", "-nostats", "-v", log_format, "-i", "pipe:0"]; diff --git a/src/output/hls.rs b/src/output/hls.rs index e326fab0..62dce871 100644 --- a/src/output/hls.rs +++ b/src/output/hls.rs @@ -30,14 +30,14 @@ use simplelog::*; use crate::filter::ingest_filter::filter_cmd; use crate::input::{ingest::log_line, source_generator}; use crate::utils::{ - prepare_output_cmd, sec_to_time, stderr_reader, Decoder, GlobalConfig, Ingest, PlayerControl, + prepare_output_cmd, sec_to_time, stderr_reader, Decoder, Ingest, PlayerControl, PlayoutConfig, PlayoutStatus, ProcessControl, }; use crate::vec_strings; /// Ingest Server for HLS fn ingest_to_hls_server( - config: GlobalConfig, + config: PlayoutConfig, playout_stat: PlayoutStatus, mut proc_control: ProcessControl, ) -> Result<(), Error> { @@ -131,7 +131,7 @@ fn ingest_to_hls_server( /// /// Write with single ffmpeg instance directly to a HLS playlist. pub fn write_hls( - config: &GlobalConfig, + config: &PlayoutConfig, play_control: PlayerControl, playout_stat: PlayoutStatus, mut proc_control: ProcessControl, diff --git a/src/output/mod.rs b/src/output/mod.rs index f6884e6c..d2e18f68 100644 --- a/src/output/mod.rs +++ b/src/output/mod.rs @@ -17,7 +17,8 @@ pub use hls::write_hls; use crate::input::{ingest_server, source_generator}; use crate::utils::{ - sec_to_time, stderr_reader, Decoder, GlobalConfig, PlayerControl, PlayoutStatus, ProcessControl, + sec_to_time, stderr_reader, Decoder, PlayerControl, PlayoutConfig, PlayoutStatus, + ProcessControl, }; use crate::vec_strings; @@ -31,7 +32,7 @@ use crate::vec_strings; /// When a live ingest arrive, it stops the current playing and switch to the live source. /// When ingest stops, it switch back to playlist/folder mode. pub fn player( - config: &GlobalConfig, + config: &PlayoutConfig, play_control: PlayerControl, playout_stat: PlayoutStatus, mut proc_control: ProcessControl, diff --git a/src/output/stream.rs b/src/output/stream.rs index 836fdb59..a9687f81 100644 --- a/src/output/stream.rs +++ b/src/output/stream.rs @@ -3,13 +3,13 @@ use std::process::{self, Command, Stdio}; use simplelog::*; use crate::filter::v_drawtext; -use crate::utils::{prepare_output_cmd, GlobalConfig, Media}; +use crate::utils::{prepare_output_cmd, Media, PlayoutConfig}; use crate::vec_strings; /// Streaming Output /// /// Prepare the ffmpeg command for streaming output -pub fn output(config: &GlobalConfig, log_format: &str) -> process::Child { +pub fn output(config: &PlayoutConfig, log_format: &str) -> process::Child { let mut enc_cmd = vec![]; let mut enc_filter = vec![]; let mut preview_cmd = config.out.preview_cmd.as_ref().unwrap().clone(); diff --git a/src/rpc/mod.rs b/src/rpc/mod.rs index fadb1834..5d1740ad 100644 --- a/src/rpc/mod.rs +++ b/src/rpc/mod.rs @@ -9,7 +9,7 @@ use serde_json::{json, Map}; use simplelog::*; use crate::utils::{ - get_delta, get_sec, sec_to_time, write_status, GlobalConfig, Media, PlayerControl, + get_delta, get_sec, sec_to_time, write_status, Media, PlayerControl, PlayoutConfig, PlayoutStatus, ProcessControl, }; @@ -25,7 +25,7 @@ fn get_media_map(media: Media) -> Value { } /// prepare json object for response -fn get_data_map(config: &GlobalConfig, media: Media) -> Map { +fn get_data_map(config: &PlayoutConfig, media: Media) -> Map { let mut data_map = Map::new(); let begin = media.begin.unwrap_or(0.0); @@ -56,7 +56,7 @@ fn get_data_map(config: &GlobalConfig, media: Media) -> Map { /// - get last clip /// - reset player state to original clip pub fn json_rpc_server( - config: GlobalConfig, + config: PlayoutConfig, play_control: PlayerControl, playout_stat: PlayoutStatus, proc_control: ProcessControl, diff --git a/src/tests/mod.rs b/src/tests/mod.rs index 859b2026..f629ede5 100644 --- a/src/tests/mod.rs +++ b/src/tests/mod.rs @@ -21,7 +21,7 @@ fn timed_kill(sec: u64, mut proc_ctl: ProcessControl) { #[test] #[ignore] fn playlist_change_at_midnight() { - let mut config = GlobalConfig::new(None); + let mut config = PlayoutConfig::new(None); config.mail.recipient = "".into(); config.processing.mode = "playlist".into(); config.playlist.day_start = "00:00:00".into(); @@ -46,7 +46,7 @@ fn playlist_change_at_midnight() { #[test] #[ignore] fn playlist_change_at_six() { - let mut config = GlobalConfig::new(None); + let mut config = PlayoutConfig::new(None); config.mail.recipient = "".into(); config.processing.mode = "playlist".into(); config.playlist.day_start = "06:00:00".into(); diff --git a/src/tests/utils/mod.rs b/src/tests/utils/mod.rs index af2766b7..f50cd743 100644 --- a/src/tests/utils/mod.rs +++ b/src/tests/utils/mod.rs @@ -39,7 +39,7 @@ fn get_date_tomorrow() { #[test] fn test_delta() { - let mut config = GlobalConfig::new(None); + let mut config = PlayoutConfig::new(None); config.mail.recipient = "".into(); config.processing.mode = "playlist".into(); config.playlist.day_start = "00:00:00".into(); diff --git a/src/utils/config.rs b/src/utils/config.rs index 8c291c24..950cb48b 100644 --- a/src/utils/config.rs +++ b/src/utils/config.rs @@ -15,7 +15,7 @@ use crate::vec_strings; /// /// This we init ones, when ffplayout is starting and use them globally in the hole program. #[derive(Debug, Serialize, Deserialize, Clone)] -pub struct GlobalConfig { +pub struct PlayoutConfig { pub general: General, pub rpc_server: RpcServer, pub mail: Mail, @@ -134,7 +134,7 @@ pub struct Out { pub output_cmd: Option>, } -impl GlobalConfig { +impl PlayoutConfig { /// Read config from YAML file, and set some extra config values. pub fn new(args: Option) -> Self { let mut config_path = PathBuf::from("/etc/ffplayout/ffplayout.yml"); @@ -161,7 +161,7 @@ impl GlobalConfig { } }; - let mut config: GlobalConfig = + let mut config: PlayoutConfig = serde_yaml::from_reader(f).expect("Could not read config file."); config.general.generate = None; config.general.stat_file = env::temp_dir() @@ -275,7 +275,7 @@ impl GlobalConfig { } } -impl Default for GlobalConfig { +impl Default for PlayoutConfig { fn default() -> Self { Self::new(None) } diff --git a/src/utils/generator.rs b/src/utils/generator.rs index b842deda..1ec9d168 100644 --- a/src/utils/generator.rs +++ b/src/utils/generator.rs @@ -17,7 +17,7 @@ use chrono::{Duration, NaiveDate}; use simplelog::*; use crate::input::FolderSource; -use crate::utils::{json_serializer::Playlist, GlobalConfig, Media}; +use crate::utils::{json_serializer::JsonPlaylist, Media, PlayoutConfig}; /// Generate a vector with dates, from given range. fn get_date_range(date_range: &[String]) -> Vec { @@ -50,7 +50,7 @@ fn get_date_range(date_range: &[String]) -> Vec { } /// Generate playlists -pub fn generate_playlist(config: &GlobalConfig, mut date_range: Vec) { +pub fn generate_playlist(config: &PlayoutConfig, mut date_range: Vec) { let total_length = config.playlist.length_sec.unwrap(); let current_list = Arc::new(Mutex::new(vec![Media::new(0, "".to_string(), false)])); let index = Arc::new(AtomicUsize::new(0)); @@ -103,7 +103,7 @@ pub fn generate_playlist(config: &GlobalConfig, mut date_range: Vec) { let mut length = 0.0; let mut round = 0; - let mut playlist = Playlist { + let mut playlist = JsonPlaylist { date, current_file: None, start_sec: None, diff --git a/src/utils/json_serializer.rs b/src/utils/json_serializer.rs index 5408c6c4..a82b530c 100644 --- a/src/utils/json_serializer.rs +++ b/src/utils/json_serializer.rs @@ -9,14 +9,14 @@ use std::{ use simplelog::*; use crate::utils::{ - get_date, is_remote, modified_time, time_from_header, validate_playlist, GlobalConfig, Media, + get_date, is_remote, modified_time, time_from_header, validate_playlist, Media, PlayoutConfig, }; pub const DUMMY_LEN: f64 = 60.0; /// This is our main playlist object, it holds all necessary information for the current day. #[derive(Debug, Serialize, Deserialize, Clone)] -pub struct Playlist { +pub struct JsonPlaylist { pub date: String, #[serde(skip_serializing, skip_deserializing)] @@ -31,7 +31,7 @@ pub struct Playlist { pub program: Vec, } -impl Playlist { +impl JsonPlaylist { fn new(date: String, start: f64) -> Self { let mut media = Media::new(0, String::new(), false); media.begin = Some(start); @@ -47,7 +47,11 @@ impl Playlist { } } -fn set_defaults(mut playlist: Playlist, current_file: String, mut start_sec: f64) -> Playlist { +fn set_defaults( + mut playlist: JsonPlaylist, + current_file: String, + mut start_sec: f64, +) -> JsonPlaylist { playlist.current_file = Some(current_file); playlist.start_sec = Some(start_sec); @@ -66,15 +70,15 @@ fn set_defaults(mut playlist: Playlist, current_file: String, mut start_sec: f64 playlist } -/// Read json playlist file, fills Playlist struct and set some extra values, +/// Read json playlist file, fills JsonPlaylist struct and set some extra values, /// which we need to process. pub fn read_json( - config: &GlobalConfig, + config: &PlayoutConfig, path: Option, is_terminated: Arc, seek: bool, next_start: f64, -) -> Playlist { +) -> JsonPlaylist { let config_clone = config.clone(); let mut playlist_path = Path::new(&config.playlist.path).to_owned(); let start_sec = config.playlist.start_sec.unwrap(); @@ -104,7 +108,7 @@ pub fn read_json( let headers = resp.headers().clone(); if let Ok(body) = resp.text() { - let mut playlist: Playlist = + let mut playlist: JsonPlaylist = serde_json::from_str(&body).expect("Could't read remote json playlist."); if let Some(time) = time_from_header(&headers) { @@ -127,7 +131,7 @@ pub fn read_json( .write(false) .open(¤t_file) .expect("Could not open json playlist file."); - let mut playlist: Playlist = + let mut playlist: JsonPlaylist = serde_json::from_reader(f).expect("Could't read json playlist file."); playlist.modified = modified_time(¤t_file); @@ -140,5 +144,5 @@ pub fn read_json( error!("Read playlist error, on: {current_file}!"); - Playlist::new(date, start_sec) + JsonPlaylist::new(date, start_sec) } diff --git a/src/utils/json_validate.rs b/src/utils/json_validate.rs index f16024bf..5a359917 100644 --- a/src/utils/json_validate.rs +++ b/src/utils/json_validate.rs @@ -5,7 +5,7 @@ use std::sync::{ use simplelog::*; -use crate::utils::{sec_to_time, valid_source, GlobalConfig, MediaProbe, Playlist}; +use crate::utils::{sec_to_time, valid_source, JsonPlaylist, MediaProbe, PlayoutConfig}; /// Validate a given playlist, to check if: /// @@ -14,7 +14,11 @@ use crate::utils::{sec_to_time, valid_source, GlobalConfig, MediaProbe, Playlist /// - total playtime fits target length from config /// /// This function we run in a thread, to don't block the main function. -pub fn validate_playlist(playlist: Playlist, is_terminated: Arc, config: GlobalConfig) { +pub fn validate_playlist( + playlist: JsonPlaylist, + is_terminated: Arc, + config: PlayoutConfig, +) { let date = playlist.date; let mut length = config.playlist.length_sec.unwrap(); let mut begin = config.playlist.start_sec.unwrap(); diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 232a6b68..a73033a9 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -16,7 +16,7 @@ use serde_json::json; use simplelog::*; mod arg_parse; -mod config; +pub mod config; pub mod controller; mod generator; pub mod json_serializer; @@ -24,10 +24,10 @@ mod json_validate; mod logging; pub use arg_parse::{get_args, Args}; -pub use config::GlobalConfig; +pub use config::{self as playout_config, PlayoutConfig}; pub use controller::{PlayerControl, PlayoutStatus, ProcessControl, ProcessUnit::*}; pub use generator::generate_playlist; -pub use json_serializer::{read_json, Playlist, DUMMY_LEN}; +pub use json_serializer::{read_json, JsonPlaylist, DUMMY_LEN}; pub use json_validate::validate_playlist; pub use logging::{init_logging, send_mail}; @@ -123,7 +123,7 @@ impl Media { } } - pub fn add_filter(&mut self, config: &GlobalConfig) { + pub fn add_filter(&mut self, config: &PlayoutConfig) { let mut node = self.clone(); self.filter = Some(filter_chains(config, &mut node)) } @@ -191,7 +191,7 @@ impl MediaProbe { /// Write current status to status file in temp folder. /// /// The status file is init in main function and mostly modified in RPC server. -pub fn write_status(config: &GlobalConfig, date: &str, shift: f64) { +pub fn write_status(config: &PlayoutConfig, date: &str, shift: f64) { let data = json!({ "time_shift": shift, "date": date, @@ -308,7 +308,7 @@ pub fn is_close(a: f64, b: f64, to: f64) -> bool { /// if we still in sync. /// /// We also get here the global delta between clip start and time when a new playlist should start. -pub fn get_delta(config: &GlobalConfig, begin: &f64) -> (f64, f64) { +pub fn get_delta(config: &PlayoutConfig, begin: &f64) -> (f64, f64) { let mut current_time = get_sec(); let start = config.playlist.start_sec.unwrap(); let length = time_to_sec(&config.playlist.length); @@ -339,7 +339,7 @@ pub fn get_delta(config: &GlobalConfig, begin: &f64) -> (f64, f64) { } /// Check if clip in playlist is in sync with global time. -pub fn check_sync(config: &GlobalConfig, delta: f64) -> bool { +pub fn check_sync(config: &PlayoutConfig, delta: f64) -> bool { if delta.abs() > config.general.stop_threshold && config.general.stop_threshold > 0.0 { error!("Clip begin out of sync for {delta:.3} seconds. Stop playout!"); return false; @@ -349,7 +349,7 @@ pub fn check_sync(config: &GlobalConfig, delta: f64) -> bool { } /// Create a dummy clip as a placeholder for missing video files. -pub fn gen_dummy(config: &GlobalConfig, duration: f64) -> (String, Vec) { +pub fn gen_dummy(config: &PlayoutConfig, duration: f64) -> (String, Vec) { let color = "#121212"; let source = format!( "color=c={color}:s={}x{}:d={duration}", @@ -567,7 +567,7 @@ fn ffmpeg_libs_and_filter() -> (Vec, Vec) { /// Validate ffmpeg/ffprobe/ffplay. /// /// Check if they are in system and has all filters and codecs we need. -pub fn validate_ffmpeg(config: &GlobalConfig) { +pub fn validate_ffmpeg(config: &PlayoutConfig) { is_in_system("ffmpeg"); is_in_system("ffprobe"); From c6f81fa8a3a32027eaffaf4bed81fae94b33ffa0 Mon Sep 17 00:00:00 2001 From: jb-alvarado Date: Wed, 15 Jun 2022 17:32:39 +0200 Subject: [PATCH 19/47] write playout config --- Cargo.lock | 2 +- Cargo.toml | 2 +- src/api/routes.rs | 80 ++++++++++++++++----------------------------- src/bin/ffpapi.rs | 7 ++-- src/utils/config.rs | 31 ++++++++++++++++-- 5 files changed, 64 insertions(+), 58 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index dddebab0..e928f6bd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -667,7 +667,7 @@ dependencies = [ [[package]] name = "ffplayout-engine" -version = "0.9.8" +version = "0.9.9" dependencies = [ "actix-web", "actix-web-grants", diff --git a/Cargo.toml b/Cargo.toml index 70c5ecff..f7fd7617 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ description = "24/7 playout based on rust and ffmpeg" license = "GPL-3.0" authors = ["Jonathan Baecker jonbae77@gmail.com"] readme = "README.md" -version = "0.9.8" +version = "0.9.9" edition = "2021" default-run = "ffplayout" diff --git a/src/api/routes.rs b/src/api/routes.rs index 82cd7a54..bb31379b 100644 --- a/src/api/routes.rs +++ b/src/api/routes.rs @@ -17,7 +17,7 @@ use crate::api::{ utils::{read_playout_config, Role}, }; -use crate::utils::playout_config; +use crate::utils::PlayoutConfig; #[derive(Serialize)] struct ResponseObj { @@ -26,37 +26,6 @@ struct ResponseObj { data: Option, } -#[derive(Debug, Serialize, Clone)] -struct ResponsePlayoutConfig { - general: Option, - rpc_server: Option, - mail: Option, - logging: Option, - processing: Option, - ingest: Option, - playlist: Option, - storage: Option, - text: Option, - out: Option, -} - -impl ResponsePlayoutConfig { - fn new() -> Self { - Self { - general: None, - rpc_server: None, - mail: None, - logging: None, - processing: None, - ingest: None, - playlist: None, - storage: None, - text: None, - out: None, - } - } -} - /// curl -X GET http://127.0.0.1:8080/api/settings/1 -H "Authorization: Bearer " #[get("/settings/{id}")] #[has_any_role("Role::Admin", "Role::User", type = "Role")] @@ -94,35 +63,42 @@ async fn patch_settings( #[has_any_role("Role::Admin", "Role::User", type = "Role")] async fn get_playout_config( id: web::Path, - details: AuthDetails, + _details: AuthDetails, ) -> Result { if let Ok(settings) = db_get_settings(&id).await { if let Ok(config) = read_playout_config(&settings.config_path) { - let mut playout_cfg = ResponsePlayoutConfig::new(); - - playout_cfg.playlist = Some(config.playlist); - playout_cfg.storage = Some(config.storage); - playout_cfg.text = Some(config.text); - - if details.has_role(&Role::Admin) { - playout_cfg.general = Some(config.general); - playout_cfg.rpc_server = Some(config.rpc_server); - playout_cfg.mail = Some(config.mail); - playout_cfg.logging = Some(config.logging); - playout_cfg.processing = Some(config.processing); - playout_cfg.ingest = Some(config.ingest); - playout_cfg.out = Some(config.out); - - return Ok(web::Json(playout_cfg)); - } - - return Ok(web::Json(playout_cfg)); + return Ok(web::Json(config)); } }; Err(ServiceError::InternalServerError) } +/// curl -X PUT http://localhost:8080/api/playout/config/1 -H "Content-Type: application/json" \ +/// --data { } --header 'Authorization: ' +#[put("/playout/config/{id}")] +#[has_any_role("Role::Admin", type = "Role")] +async fn update_playout_config( + id: web::Path, + data: web::Json, +) -> Result { + if let Ok(settings) = db_get_settings(&id).await { + if let Ok(f) = std::fs::OpenOptions::new() + .write(true) + .truncate(true) + .open(&settings.config_path) + { + serde_yaml::to_writer(f, &data).unwrap(); + + return Ok("Update playout config success."); + } else { + return Err(ServiceError::InternalServerError); + }; + }; + + Err(ServiceError::InternalServerError) +} + /// curl -X PUT http://localhost:8080/api/user/1 --header 'Content-Type: application/json' \ /// --data '{"email": "", "password": ""}' --header 'Authorization: ' #[put("/user/{id}")] diff --git a/src/bin/ffpapi.rs b/src/bin/ffpapi.rs index 007b4bcb..a24d7574 100644 --- a/src/bin/ffpapi.rs +++ b/src/bin/ffpapi.rs @@ -13,7 +13,10 @@ use ffplayout_engine::{ args_parse::Args, auth, models::LoginUser, - routes::{add_user, get_playout_config, get_settings, login, patch_settings, update_user}, + routes::{ + add_user, get_playout_config, get_settings, login, patch_settings, + update_playout_config, update_user, + }, utils::{db_path, init_config, run_args, Role}, }, utils::{init_logging, PlayoutConfig}, @@ -27,7 +30,6 @@ async fn validator(req: ServiceRequest, credentials: BearerAuth) -> Result std::io::Result<()> { .wrap(auth) .service(add_user) .service(get_playout_config) + .service(update_playout_config) .service(get_settings) .service(patch_settings) .service(update_user), diff --git a/src/utils/config.rs b/src/utils/config.rs index 950cb48b..b6991ab8 100644 --- a/src/utils/config.rs +++ b/src/utils/config.rs @@ -30,7 +30,10 @@ pub struct PlayoutConfig { #[derive(Debug, Serialize, Deserialize, Clone)] pub struct General { + pub help_text: String, pub stop_threshold: f64, + + #[serde(skip_serializing, skip_deserializing)] pub generate: Option>, #[serde(skip_serializing, skip_deserializing)] @@ -39,6 +42,7 @@ pub struct General { #[derive(Debug, Serialize, Deserialize, Clone)] pub struct RpcServer { + pub help_text: String, pub enable: bool, pub address: String, pub authorization: String, @@ -46,6 +50,7 @@ pub struct RpcServer { #[derive(Debug, Serialize, Deserialize, Clone)] pub struct Mail { + pub help_text: String, pub subject: String, pub smtp_server: String, pub starttls: bool, @@ -58,6 +63,7 @@ pub struct Mail { #[derive(Debug, Serialize, Deserialize, Clone)] pub struct Logging { + pub help_text: String, pub log_to_file: bool, pub backup_count: usize, pub local_time: bool, @@ -69,6 +75,7 @@ pub struct Logging { #[derive(Debug, Serialize, Deserialize, Clone)] pub struct Processing { + pub help_text: String, pub mode: String, pub width: i64, pub height: i64, @@ -85,28 +92,41 @@ pub struct Processing { pub loud_tp: f32, pub loud_lra: f32, pub volume: f64, + + #[serde(skip_serializing, skip_deserializing)] pub settings: Option>, } #[derive(Debug, Serialize, Deserialize, Clone)] pub struct Ingest { + pub help_text: String, pub enable: bool, input_param: String, + + #[serde(skip_serializing, skip_deserializing)] pub input_cmd: Option>, } #[derive(Debug, Serialize, Deserialize, Clone)] pub struct Playlist { + pub help_text: String, pub path: String, pub day_start: String, + + #[serde(skip_serializing, skip_deserializing)] pub start_sec: Option, + pub length: String, + + #[serde(skip_serializing, skip_deserializing)] pub length_sec: Option, + pub infinit: bool, } #[derive(Debug, Serialize, Deserialize, Clone)] pub struct Storage { + pub help_text: String, pub path: String, pub filler_clip: String, pub extensions: Vec, @@ -115,6 +135,7 @@ pub struct Storage { #[derive(Debug, Serialize, Deserialize, Clone)] pub struct Text { + pub help_text: String, pub add_text: bool, pub over_pre: bool, pub bind_address: String, @@ -126,11 +147,17 @@ pub struct Text { #[derive(Debug, Serialize, Deserialize, Clone)] pub struct Out { + pub help_text: String, pub mode: String, pub preview: bool, - preview_param: String, + pub preview_param: String, + + #[serde(skip_serializing, skip_deserializing)] pub preview_cmd: Option>, - output_param: String, + + pub output_param: String, + + #[serde(skip_serializing, skip_deserializing)] pub output_cmd: Option>, } From efa6dfa5eacdb69d1388d6e9c8a0b814ce570966 Mon Sep 17 00:00:00 2001 From: jb-alvarado Date: Wed, 15 Jun 2022 21:27:16 +0200 Subject: [PATCH 20/47] set internal random port for zmq --- assets/ffplayout.yml | 10 +++------- src/filter/mod.rs | 4 +++- src/filter/v_drawtext.rs | 8 ++++---- src/output/desktop.rs | 22 ++++++++++++---------- src/output/hls.rs | 2 +- src/output/stream.rs | 22 ++++++++++++---------- src/utils/config.rs | 22 +++++++++++++++++++--- src/utils/json_serializer.rs | 2 +- src/utils/mod.rs | 15 +++++++++++++++ 9 files changed, 70 insertions(+), 37 deletions(-) diff --git a/assets/ffplayout.yml b/assets/ffplayout.yml index e121438a..5dfe30fa 100644 --- a/assets/ffplayout.yml +++ b/assets/ffplayout.yml @@ -105,14 +105,10 @@ text: help_text: Overlay text in combination with libzmq for remote text manipulation. On windows fontfile path need to be like this 'C\:/WINDOWS/fonts/DejaVuSans.ttf'. In a standard environment the filter drawtext node is Parsed_drawtext_2. - 'over_pre' if True text will be overlay in pre processing. Continue same text - over multiple files is in that mode not possible. '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 API will override this. - With 'regex' you can format file names, to get a title from it. + '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 + API will override this. With 'regex' you can format file names, to get a title from it. add_text: false - over_pre: false - bind_address: "127.0.0.1:5555" fontfile: "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf" text_from_filename: false style: "x=(w-tw)/2:y=(h-line_h)*0.9:fontsize=24:fontcolor=#ffffff:box=1:boxcolor=#000000:boxborderw=4" diff --git a/src/filter/mod.rs b/src/filter/mod.rs index a8a2009b..56af4440 100644 --- a/src/filter/mod.rs +++ b/src/filter/mod.rs @@ -184,7 +184,9 @@ fn extend_video(node: &mut Media, chain: &mut Filters) { /// add drawtext filter for lower thirds messages fn add_text(node: &mut Media, chain: &mut Filters, config: &PlayoutConfig) { - if config.text.add_text && config.text.over_pre { + if config.text.add_text + && (config.text.text_from_filename || config.out.mode.to_lowercase() == "hls") + { let filter = v_drawtext::filter_node(config, node); chain.add_filter(&filter, "video"); diff --git a/src/filter/v_drawtext.rs b/src/filter/v_drawtext.rs index 8ff6cb6e..34820ced 100644 --- a/src/filter/v_drawtext.rs +++ b/src/filter/v_drawtext.rs @@ -4,7 +4,7 @@ use regex::Regex; use crate::utils::{Media, PlayoutConfig}; -pub fn filter_node(config: &PlayoutConfig, node: &mut Media) -> String { +pub fn filter_node(config: &PlayoutConfig, node: &Media) -> String { let mut filter = String::new(); let mut font = String::new(); @@ -13,7 +13,7 @@ pub fn filter_node(config: &PlayoutConfig, node: &mut Media) -> String { font = format!(":fontfile='{}'", config.text.fontfile) } - if config.text.over_pre && config.text.text_from_filename { + if config.text.text_from_filename { let source = node.source.clone(); let regex: Regex = Regex::new(&config.text.regex).unwrap(); @@ -27,10 +27,10 @@ pub fn filter_node(config: &PlayoutConfig, node: &mut Media) -> String { .replace('%', "\\\\\\%") .replace(':', "\\:"); filter = format!("drawtext=text='{escape}':{}{font}", config.text.style) - } else { + } else if let Some(socket) = config.text.bind_address.clone() { filter = format!( "zmq=b=tcp\\\\://'{}',drawtext=text=''{font}", - config.text.bind_address.replace(':', "\\:") + socket.replace(':', "\\:") ) } } diff --git a/src/output/desktop.rs b/src/output/desktop.rs index ffe72f37..8ff89ba3 100644 --- a/src/output/desktop.rs +++ b/src/output/desktop.rs @@ -14,17 +14,19 @@ pub fn output(config: &PlayoutConfig, log_format: &str) -> process::Child { let mut enc_cmd = vec_strings!["-hide_banner", "-nostats", "-v", log_format, "-i", "pipe:0"]; - if config.text.add_text && !config.text.over_pre { - info!( - "Using drawtext filter, listening on address: {}", - config.text.bind_address - ); + if config.text.add_text && !config.text.text_from_filename { + if let Some(socket) = config.text.bind_address.clone() { + debug!( + "Using drawtext filter, listening on address: {}", + socket + ); - let mut filter: String = "null,".to_string(); - filter.push_str( - v_drawtext::filter_node(config, &mut Media::new(0, String::new(), false)).as_str(), - ); - enc_filter = vec!["-vf".to_string(), filter]; + let mut filter: String = "null,".to_string(); + filter.push_str( + v_drawtext::filter_node(config, &Media::new(0, String::new(), false)).as_str(), + ); + enc_filter = vec!["-vf".to_string(), filter]; + } } enc_cmd.append(&mut enc_filter); diff --git a/src/output/hls.rs b/src/output/hls.rs index 62dce871..4373bfd9 100644 --- a/src/output/hls.rs +++ b/src/output/hls.rs @@ -13,7 +13,7 @@ out: -hls_time 6 -hls_list_size 600 -hls_flags append_list+delete_segments+omit_endlist+program_date_time - -hls_segment_filename /var/www/html/live/stream-%09d.ts /var/www/html/live/stream.m3u8 + -hls_segment_filename /var/www/html/live/stream-%d.ts /var/www/html/live/stream.m3u8 */ diff --git a/src/output/stream.rs b/src/output/stream.rs index a9687f81..b61af29b 100644 --- a/src/output/stream.rs +++ b/src/output/stream.rs @@ -25,19 +25,21 @@ pub fn output(config: &PlayoutConfig, log_format: &str) -> process::Child { "pipe:0" ]; - if config.text.add_text && !config.text.over_pre { - info!( - "Using drawtext filter, listening on address: {}", - config.text.bind_address - ); + if config.text.add_text && !config.text.text_from_filename { + if let Some(socket) = config.text.bind_address.clone() { + debug!( + "Using drawtext filter, listening on address: {}", + socket + ); - let mut filter = "[0:v]null,".to_string(); + let mut filter = "[0:v]null,".to_string(); - filter.push_str( - v_drawtext::filter_node(config, &mut Media::new(0, String::new(), false)).as_str(), - ); + filter.push_str( + v_drawtext::filter_node(config, &Media::new(0, String::new(), false)).as_str(), + ); - enc_filter = vec!["-filter_complex".to_string(), filter]; + enc_filter = vec!["-filter_complex".to_string(), filter]; + } } if config.out.preview { diff --git a/src/utils/config.rs b/src/utils/config.rs index b6991ab8..9ad00436 100644 --- a/src/utils/config.rs +++ b/src/utils/config.rs @@ -8,7 +8,7 @@ use std::{ use serde::{Deserialize, Serialize}; use shlex::split; -use crate::utils::{time_to_sec, Args}; +use crate::utils::{free_tcp_socket, time_to_sec, Args}; use crate::vec_strings; /// Global Config @@ -137,8 +137,13 @@ pub struct Storage { pub struct Text { pub help_text: String, pub add_text: bool, - pub over_pre: bool, - pub bind_address: String, + + #[serde(skip_serializing, skip_deserializing)] + pub bind_address: Option, + + #[serde(skip_serializing, skip_deserializing)] + pub node_pos: Option, + pub fontfile: String, pub text_from_filename: bool, pub style: String, @@ -243,6 +248,17 @@ impl PlayoutConfig { config.out.preview_cmd = split(config.out.preview_param.as_str()); config.out.output_cmd = split(config.out.output_param.as_str()); + // when text overlay without text_from_filename is on, turn also the RPC server on, + // to get text messages from it + if config.text.add_text && !config.text.text_from_filename { + config.rpc_server.enable = true; + config.text.bind_address = free_tcp_socket(); + config.text.node_pos = Some(2); + } else { + config.text.bind_address = None; + config.text.node_pos = None; + } + // Read command line arguments, and override the config with them. if let Some(arg) = args { diff --git a/src/utils/json_serializer.rs b/src/utils/json_serializer.rs index a82b530c..2c669cb1 100644 --- a/src/utils/json_serializer.rs +++ b/src/utils/json_serializer.rs @@ -142,7 +142,7 @@ pub fn read_json( return set_defaults(playlist, current_file, start_sec); } - error!("Read playlist error, on: {current_file}!"); + error!("Read playlist error, on: {current_file}"); JsonPlaylist::new(date, start_sec) } diff --git a/src/utils/mod.rs b/src/utils/mod.rs index a73033a9..d5790cd1 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -1,6 +1,7 @@ use std::{ fs::{self, metadata}, io::{BufRead, BufReader, Error}, + net::TcpListener, path::Path, process::{exit, ChildStderr, Command, Stdio}, time::{self, UNIX_EPOCH}, @@ -9,6 +10,7 @@ use std::{ use chrono::{prelude::*, Duration}; use ffprobe::{ffprobe, Format, Stream}; use jsonrpc_http_server::hyper::HeaderMap; +use rand::prelude::*; use regex::Regex; use reqwest::header; use serde::{Deserialize, Serialize}; @@ -596,6 +598,19 @@ pub fn validate_ffmpeg(config: &PlayoutConfig) { } } +/// get a free tcp socket +pub fn free_tcp_socket() -> Option { + for _ in 0..100 { + let port = rand::thread_rng().gen_range(45321..54268); + + if TcpListener::bind(("127.0.0.1", port)).is_ok() { + return Some(format!("127.0.0.1:{port}")); + } + } + + None +} + /// Get system time, in non test case. #[cfg(not(test))] pub fn time_now() -> DateTime { From 25e7cb8c0115cfff10ac274b7417134cb38aeffb Mon Sep 17 00:00:00 2001 From: jb-alvarado Date: Wed, 15 Jun 2022 21:37:44 +0200 Subject: [PATCH 21/47] colorize path --- src/filter/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/filter/mod.rs b/src/filter/mod.rs index 56af4440..12d9ec9a 100644 --- a/src/filter/mod.rs +++ b/src/filter/mod.rs @@ -210,7 +210,7 @@ fn add_audio(node: &mut Media, chain: &mut Filters) { .unwrap_or(&vec![]) .is_empty() { - warn!("Clip: '{}' has no audio!", node.source); + warn!("Clip {} has no audio!", node.source); let audio = format!( "aevalsrc=0:channel_layout=stereo:duration={}:sample_rate=48000", node.out - node.seek From bbc5c7b01ba0591969015a74180553a46a841dc0 Mon Sep 17 00:00:00 2001 From: jb-alvarado Date: Thu, 16 Jun 2022 18:14:58 +0200 Subject: [PATCH 22/47] add zmq for text messages through JSON RPC, add preset table --- Cargo.lock | 550 +++++++++++++++++++++++++++++++++++++++++++-- Cargo.toml | 5 + README.md | 20 +- src/api/handles.rs | 23 ++ src/rpc/mod.rs | 27 ++- src/rpc/zmq_cmd.rs | 14 ++ src/utils/mod.rs | 13 ++ 7 files changed, 625 insertions(+), 27 deletions(-) create mode 100644 src/rpc/zmq_cmd.rs diff --git a/Cargo.lock b/Cargo.lock index e928f6bd..1a1eb359 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -40,7 +40,7 @@ dependencies = [ "actix-rt", "actix-service", "actix-utils", - "ahash", + "ahash 0.7.6", "base64", "bitflags", "brotli", @@ -60,7 +60,7 @@ dependencies = [ "mime", "percent-encoding", "pin-project-lite", - "rand", + "rand 0.8.5", "sha1", "smallvec", "tracing", @@ -155,7 +155,7 @@ dependencies = [ "actix-service", "actix-utils", "actix-web-codegen", - "ahash", + "ahash 0.7.6", "bytes", "bytestring", "cfg-if 1.0.0", @@ -223,13 +223,22 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "ahash" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8fd72866655d1904d6b0997d0b07ba561047d070fbe29de039031c641b61217" +dependencies = [ + "const-random", +] + [[package]] name = "ahash" version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" dependencies = [ - "getrandom", + "getrandom 0.2.6", "once_cell", "version_check", ] @@ -269,6 +278,142 @@ dependencies = [ "password-hash", ] +[[package]] +name = "async-attributes" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3203e79f4dd9bdda415ed03cf14dae5a2bf775c683a00f94e9cd1faf0f596e5" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "async-channel" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2114d64672151c0c5eaa5e131ec84a74f06e1e559830dabba01ca30605d66319" +dependencies = [ + "concurrent-queue", + "event-listener", + "futures-core", +] + +[[package]] +name = "async-executor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "871f9bb5e0a22eeb7e8cf16641feb87c9dc67032ccf8ff49e772eb9941d3a965" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "once_cell", + "slab", +] + +[[package]] +name = "async-global-executor" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd8b508d585e01084059b60f06ade4cb7415cd2e4084b71dd1cb44e7d3fb9880" +dependencies = [ + "async-channel", + "async-executor", + "async-io", + "async-lock", + "blocking", + "futures-lite", + "once_cell", +] + +[[package]] +name = "async-io" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5e18f61464ae81cde0a23e713ae8fd299580c54d697a35820cfd0625b8b0e07" +dependencies = [ + "concurrent-queue", + "futures-lite", + "libc", + "log", + "once_cell", + "parking", + "polling", + "slab", + "socket2", + "waker-fn", + "winapi 0.3.9", +] + +[[package]] +name = "async-lock" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e97a171d191782fba31bb902b14ad94e24a68145032b7eedf871ab0bc0d077b6" +dependencies = [ + "event-listener", +] + +[[package]] +name = "async-std" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52580991739c5cdb36cde8b2a516371c0a3b70dda36d916cc08b82372916808c" +dependencies = [ + "async-attributes", + "async-channel", + "async-global-executor", + "async-io", + "async-lock", + "crossbeam-utils 0.8.8", + "futures-channel", + "futures-core", + "futures-io", + "futures-lite", + "gloo-timers", + "kv-log-macro", + "log", + "memchr", + "num_cpus", + "once_cell", + "pin-project-lite", + "pin-utils", + "slab", + "wasm-bindgen-futures", +] + +[[package]] +name = "async-task" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30696a84d817107fc028e049980e09d5e140e8da8f1caeb17e8e950658a3cea9" + +[[package]] +name = "async-trait" +version = "0.1.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96cf8829f67d2eab0b2dfa42c5d0ef737e0724e4a82b01b3e292456202b19716" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "asynchronous-codec" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0de5164e5edbf51c45fb8c2d9664ae1c095cce1b265ecf7569093c0d66ef690" +dependencies = [ + "bytes", + "futures-sink", + "futures-util", + "memchr", + "pin-project-lite", +] + [[package]] name = "atoi" version = "0.4.0" @@ -278,6 +423,12 @@ dependencies = [ "num-traits", ] +[[package]] +name = "atomic-waker" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "065374052e7df7ee4047b1160cca5e1467a12351a40b3da123c870ba0b8eda2a" + [[package]] name = "atty" version = "0.2.14" @@ -331,6 +482,20 @@ dependencies = [ "generic-array", ] +[[package]] +name = "blocking" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6ccb65d468978a086b69884437ded69a90faab3bbe6e67f242173ea728acccc" +dependencies = [ + "async-channel", + "async-task", + "atomic-waker", + "fastrand", + "futures-lite", + "once_cell", +] + [[package]] name = "brotli" version = "3.3.4" @@ -388,6 +553,12 @@ dependencies = [ "bytes", ] +[[package]] +name = "cache-padded" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1db59621ec70f09c5e9b597b220c7a2b43611f4710dc03ceb8748637775692c" + [[package]] name = "cc" version = "1.0.73" @@ -472,6 +643,37 @@ dependencies = [ "os_str_bytes", ] +[[package]] +name = "concurrent-queue" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30ed07550be01594c6026cff2a1d7fe9c8f683caa798e12b68694ac9e88286a3" +dependencies = [ + "cache-padded", +] + +[[package]] +name = "const-random" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f590d95d011aa80b063ffe3253422ed5aa462af4e9867d43ce8337562bac77c4" +dependencies = [ + "const-random-macro", + "proc-macro-hack", +] + +[[package]] +name = "const-random-macro" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "615f6e27d000a2bffbc7f2f6a8669179378fa27ee4d0a509e985dfc0a7defb40" +dependencies = [ + "getrandom 0.2.6", + "lazy_static", + "proc-macro-hack", + "tiny-keccak", +] + [[package]] name = "convert_case" version = "0.4.0" @@ -538,6 +740,30 @@ dependencies = [ "cfg-if 1.0.0", ] +[[package]] +name = "crossbeam" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69323bff1fb41c635347b8ead484a5ca6c3f11914d784170b158d8449ab07f8e" +dependencies = [ + "cfg-if 0.1.10", + "crossbeam-channel 0.4.4", + "crossbeam-deque", + "crossbeam-epoch", + "crossbeam-queue 0.2.3", + "crossbeam-utils 0.7.2", +] + +[[package]] +name = "crossbeam-channel" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b153fe7cbef478c567df0f972e02e6d736db11affe43dfc9c56a9374d1adfb87" +dependencies = [ + "crossbeam-utils 0.7.2", + "maybe-uninit", +] + [[package]] name = "crossbeam-channel" version = "0.5.4" @@ -545,7 +771,44 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5aaa7bd5fb665c6864b5f963dd9097905c54125909c7aa94c9e18507cdbe6c53" dependencies = [ "cfg-if 1.0.0", - "crossbeam-utils", + "crossbeam-utils 0.8.8", +] + +[[package]] +name = "crossbeam-deque" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c20ff29ded3204c5106278a81a38f4b482636ed4fa1e6cfbeef193291beb29ed" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils 0.7.2", + "maybe-uninit", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "058ed274caafc1f60c4997b5fc07bf7dc7cca454af7c6e81edffe5f33f70dace" +dependencies = [ + "autocfg", + "cfg-if 0.1.10", + "crossbeam-utils 0.7.2", + "lazy_static", + "maybe-uninit", + "memoffset", + "scopeguard", +] + +[[package]] +name = "crossbeam-queue" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "774ba60a54c213d409d5353bda12d49cd68d14e45036a285234c8d6f91f92570" +dependencies = [ + "cfg-if 0.1.10", + "crossbeam-utils 0.7.2", + "maybe-uninit", ] [[package]] @@ -555,7 +818,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f25d8400f4a7a5778f0e4e52384a48cbd9b5c495d110786187fc750075277a2" dependencies = [ "cfg-if 1.0.0", - "crossbeam-utils", + "crossbeam-utils 0.8.8", +] + +[[package]] +name = "crossbeam-utils" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3c7c73a2d1e9fc0886a08b93e98eb643461230d5f1925e4036204d5f2e261a8" +dependencies = [ + "autocfg", + "cfg-if 0.1.10", + "lazy_static", ] [[package]] @@ -568,6 +842,12 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + [[package]] name = "crypto-common" version = "0.1.3" @@ -578,6 +858,27 @@ dependencies = [ "typenum", ] +[[package]] +name = "ctor" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f877be4f7c9f246b183111634f75baa039715e3f46ce860677d3b19a69fb229c" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "dashmap" +version = "3.11.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f260e2fc850179ef410018660006951c1b55b79e8087e87111a2c388994b9b5" +dependencies = [ + "ahash 0.3.8", + "cfg-if 0.1.10", + "num_cpus", +] + [[package]] name = "derive_more" version = "0.99.17" @@ -639,6 +940,17 @@ dependencies = [ "cfg-if 1.0.0", ] +[[package]] +name = "enum-primitive-derive" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c375b9c5eadb68d0a6efee2999fef292f45854c3444c86f09d8ab086ba942b0e" +dependencies = [ + "num-traits", + "quote", + "syn", +] + [[package]] name = "event-listener" version = "2.5.2" @@ -675,11 +987,12 @@ dependencies = [ "argon2", "chrono 0.4.19 (git+https://github.com/sbrocket/chrono?branch=parse-error-kind-public)", "clap", - "crossbeam-channel", + "crossbeam-channel 0.5.4", "derive_more", "faccess", "ffprobe", "file-rotate", + "futures", "jsonrpc-http-server", "jsonwebtoken", "lettre", @@ -687,8 +1000,8 @@ dependencies = [ "notify", "once_cell", "openssl", - "rand", - "rand_core", + "rand 0.8.5", + "rand_core 0.6.3", "regex", "reqwest", "serde", @@ -699,6 +1012,7 @@ dependencies = [ "sqlx", "time 0.3.9", "walkdir", + "zeromq", ] [[package]] @@ -885,6 +1199,21 @@ version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc4045962a5a5e935ee2fdedaa4e08284547402885ab326734432bed5d12966b" +[[package]] +name = "futures-lite" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694489acd39452c77daa48516b894c153f192c3578d5a839b62c58099fcbf48" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "memchr", + "parking", + "pin-project-lite", + "waker-fn", +] + [[package]] name = "futures-macro" version = "0.3.21" @@ -936,6 +1265,17 @@ dependencies = [ "version_check", ] +[[package]] +name = "getrandom" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "wasi 0.9.0+wasi-snapshot-preview1", +] + [[package]] name = "getrandom" version = "0.2.6" @@ -960,6 +1300,18 @@ dependencies = [ "regex", ] +[[package]] +name = "gloo-timers" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fb7d06c1c8cc2a29bee7ec961009a0b2caa0793ee4900c2ffb348734ba1c8f9" +dependencies = [ + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", +] + [[package]] name = "h2" version = "0.3.13" @@ -985,7 +1337,7 @@ version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" dependencies = [ - "ahash", + "ahash 0.7.6", ] [[package]] @@ -1274,6 +1626,15 @@ dependencies = [ "winapi-build", ] +[[package]] +name = "kv-log-macro" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de8b303297635ad57c9f5059fd9cee7a47f8e8daa09df0fcd07dd39fb22977f" +dependencies = [ + "log", +] + [[package]] name = "language-tags" version = "0.3.2" @@ -1372,6 +1733,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" dependencies = [ "cfg-if 1.0.0", + "value-bag", ] [[package]] @@ -1386,12 +1748,27 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" +[[package]] +name = "maybe-uninit" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00" + [[package]] name = "memchr" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" +[[package]] +name = "memoffset" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "043175f069eda7b85febe4a74abbaeff828d9f8b448515d3151a14a3542811aa" +dependencies = [ + "autocfg", +] + [[package]] name = "mime" version = "0.3.16" @@ -1647,6 +2024,12 @@ version = "1.5.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2eaf2319cd71dd9ff38c72bebde61b9ea657134abcf26ae4205f54f772a32810" +[[package]] +name = "parking" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "427c3892f9e783d91cc128285287e70a59e206ca452770ece88a76f7a3eddd72" + [[package]] name = "parking_lot" version = "0.11.2" @@ -1702,7 +2085,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e029e94abc8fb0065241c308f1ac6bc8d20f450e8f7c5f0b25cd9b8d526ba294" dependencies = [ "base64ct", - "rand_core", + "rand_core 0.6.3", "subtle", ] @@ -1765,6 +2148,19 @@ version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae" +[[package]] +name = "polling" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "685404d509889fade3e86fe3a5803bca2ec09b0c0778d5ada6ec8bf7a8de5259" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "log", + "wepoll-ffi", + "winapi 0.3.9", +] + [[package]] name = "ppv-lite86" version = "0.2.16" @@ -1795,6 +2191,12 @@ dependencies = [ "version_check", ] +[[package]] +name = "proc-macro-hack" +version = "0.5.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5" + [[package]] name = "proc-macro2" version = "1.0.39" @@ -1819,6 +2221,19 @@ version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fee2dce59f7a43418e3382c766554c614e06a552d53a8f07ef499ea4b332c0f" +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom 0.1.16", + "libc", + "rand_chacha 0.2.2", + "rand_core 0.5.1", + "rand_hc", +] + [[package]] name = "rand" version = "0.8.5" @@ -1826,8 +2241,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ "libc", - "rand_chacha", - "rand_core", + "rand_chacha 0.3.1", + "rand_core 0.6.3", +] + +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core 0.5.1", ] [[package]] @@ -1837,7 +2262,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", - "rand_core", + "rand_core 0.6.3", +] + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom 0.1.16", ] [[package]] @@ -1846,7 +2280,16 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" dependencies = [ - "getrandom", + "getrandom 0.2.6", +] + +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core 0.5.1", ] [[package]] @@ -2184,14 +2627,14 @@ version = "0.5.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e48c61941ccf5ddcada342cd59e3e5173b007c509e1e8e990dafc830294d9dc5" dependencies = [ - "ahash", + "ahash 0.7.6", "atoi", "bitflags", "byteorder", "bytes", "chrono 0.4.19 (registry+https://github.com/rust-lang/crates.io-index)", "crc", - "crossbeam-queue", + "crossbeam-queue 0.3.5", "either", "event-listener", "flume", @@ -2364,6 +2807,15 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42657b1a6f4d817cda8e7a0ace261fe0cc946cf3a80314390b22cc61ae080792" +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + [[package]] name = "tinyvec" version = "1.6.0" @@ -2546,6 +2998,25 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "uuid" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" +dependencies = [ + "getrandom 0.2.6", +] + +[[package]] +name = "value-bag" +version = "1.0.0-alpha.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2209b78d1249f7e6f3293657c9779fe31ced465df091bbd433a1cf88e916ec55" +dependencies = [ + "ctor", + "version_check", +] + [[package]] name = "vcpkg" version = "0.2.15" @@ -2558,6 +3029,12 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +[[package]] +name = "waker-fn" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca" + [[package]] name = "walkdir" version = "2.3.2" @@ -2579,6 +3056,12 @@ dependencies = [ "try-lock", ] +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + [[package]] name = "wasi" version = "0.10.0+wasi-snapshot-preview1" @@ -2667,6 +3150,15 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "wepoll-ffi" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d743fdedc5c64377b5fc2bc036b01c7fd642205a0d96356034ae3404d49eb7fb" +dependencies = [ + "cc", +] + [[package]] name = "winapi" version = "0.2.8" @@ -2781,6 +3273,30 @@ dependencies = [ "linked-hash-map", ] +[[package]] +name = "zeromq" +version = "0.3.3" +source = "git+https://github.com/zeromq/zmq.rs.git#9e0eb7c16950146d285d952939ea8d5a5fc812c9" +dependencies = [ + "async-std", + "async-trait", + "asynchronous-codec", + "bytes", + "crossbeam", + "dashmap", + "enum-primitive-derive", + "futures", + "futures-util", + "lazy_static", + "log", + "num-traits", + "parking_lot 0.11.2", + "rand 0.7.3", + "regex", + "thiserror", + "uuid", +] + [[package]] name = "zstd" version = "0.11.2+zstd.1.5.2" diff --git a/Cargo.toml b/Cargo.toml index f7fd7617..3682ee91 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,7 @@ derive_more = "0.99" faccess = "0.2" ffprobe = "0.3" file-rotate = { git = "https://github.com/Ploppz/file-rotate.git", branch = "timestamp-parse-fix" } +futures = "0.3" jsonrpc-http-server = "18.0" jsonwebtoken = "8" lettre = "0.10.0-rc.7" @@ -42,6 +43,10 @@ sqlx = { version = "0.5", features = [ ] } time = { version = "0.3", features = ["formatting", "macros"] } walkdir = "2" +zeromq = { git = "https://github.com/zeromq/zmq.rs.git", default-features = false, features = [ + "async-std-runtime", + "tcp-transport" +] } [target.x86_64-unknown-linux-musl.dependencies] openssl = { version = "0.10", features = ["vendored"] } diff --git a/README.md b/README.md index 0add514b..e404fa23 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ The main purpose of ffplayout is to provide a 24/7 broadcasting solution that pl - playing clips in [watched](/docs/folder_mode.md) folder mode - send emails with error message - overlay a logo -- overlay text, controllable through [messenger](https://github.com/ffplayout/messenger) or [ffplayout-frontend](https://github.com/ffplayout/ffplayout-frontend) (needs ffmpeg with libzmq) +- overlay text, controllable through [messenger](https://github.com/ffplayout/messenger) or [ffplayout-frontend](https://github.com/ffplayout/ffplayout-frontend) (needs ffmpeg with libzmq and enabled JSON RPC server) - EBU R128 loudness normalization (single pass) - loop playlist infinitely - [remote source](/docs/remote_source.md) @@ -122,20 +122,24 @@ The ffplayout engine can run a JSON RPC server. A request show look like: ```Bash curl -X POST -H "Content-Type: application/json" -H "Authorization: ---auth-key---" \ - -d '{"jsonrpc": "2.0", "method": "player", "params":{"control":"next"}, "id":1 }' \ + -d '{"jsonrpc": "2.0", "id":1, "method": "player", "params":{"control":"next"}}' \ 127.0.0.1:7070 ``` At the moment this comments are possible: ```Bash -'{"jsonrpc": "2.0", "method": "player", "params":{"media":"current"}, "id":1 }' # get infos about current clip -'{"jsonrpc": "2.0", "method": "player", "params":{"media":"next"}, "id":2 }' # get infos about next clip -'{"jsonrpc": "2.0", "method": "player", "params":{"media":"last"}, "id":3 }' # get infos about last clip -'{"jsonrpc": "2.0", "method": "player", "params":{"control":"next"}, "id":4 }' # jump to next clip -'{"jsonrpc": "2.0", "method": "player", "params":{"control":"back"}, "id":5 }' # jump to last clip -'{"jsonrpc": "2.0", "method": "player", "params":{"control":"reset"}, "id":6 }' # reset playlist to old state +'{"jsonrpc": "2.0", "id":1, "method": "player", "params":{"media":"current"}}' # get infos about current clip +'{"jsonrpc": "2.0", "id":2, "method": "player", "params":{"media":"next"}}' # get infos about next clip +'{"jsonrpc": "2.0", "id":3, "method": "player", "params":{"media":"last"}}' # get infos about last clip +'{"jsonrpc": "2.0", "id":4, "method": "player", "params":{"control":"next"}}' # jump to next clip +'{"jsonrpc": "2.0", "id":5, "method": "player", "params":{"control":"back"}}' # jump to last clip +'{"jsonrpc": "2.0", "id":6, "method": "player", "params":{"control":"reset"}}' # reset playlist to old state +'{"jsonrpc": "2.0", "id":7, "method": "player", "params":{"control":"text", \ + "message": {"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}}}' # send text to drawtext filter from ffmpeg ``` Output from `{"media":"current"}` show: diff --git a/src/api/handles.rs b/src/api/handles.rs index 2c247f84..3beea9b8 100644 --- a/src/api/handles.rs +++ b/src/api/handles.rs @@ -33,6 +33,22 @@ async fn create_schema() -> Result { 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 INTEGER NOT NULL DEFAULT 24, + line_spacing INTEGER NOT NULL DEFAULT 4, + fontcolor TEXT NOT NULL, + box INTEGER NOT NULL DEFAULT 1, + boxcolor TEXT NOT NULL, + boxborderw INTEGER NOT NULL DEFAULT 4, + alpha TEXT NOT NULL, + UNIQUE(name) + ); CREATE TABLE IF NOT EXISTS settings ( id INTEGER PRIMARY KEY AUTOINCREMENT, @@ -84,6 +100,13 @@ pub async fn db_init() -> Result<&'static str, Box> { SELECT RAISE(FAIL, 'Database is already init!'); END; INSERT INTO global(secret) VALUES($1); + INSERT INTO presets(name, text, x, y, fontsize, line_spacing, fontcolor, alpha, box, boxcolor, boxborderw) + 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) VALUES('Channel 1', 'http://localhost/live/preview.m3u8', diff --git a/src/rpc/mod.rs b/src/rpc/mod.rs index 5d1740ad..c96adeaf 100644 --- a/src/rpc/mod.rs +++ b/src/rpc/mod.rs @@ -1,5 +1,8 @@ +use futures::executor; use std::sync::atomic::Ordering; +mod zmq_cmd; + use jsonrpc_http_server::{ hyper, jsonrpc_core::{IoHandler, Params, Value}, @@ -9,10 +12,12 @@ use serde_json::{json, Map}; use simplelog::*; use crate::utils::{ - get_delta, get_sec, sec_to_time, write_status, Media, PlayerControl, PlayoutConfig, - PlayoutStatus, ProcessControl, + get_delta, get_filter_from_json, get_sec, sec_to_time, write_status, Media, PlayerControl, + PlayoutConfig, PlayoutStatus, ProcessControl, }; +use zmq_cmd::zmq_send; + /// map media struct to json object fn get_media_map(media: Media) -> Value { json!({ @@ -73,6 +78,24 @@ pub fn json_rpc_server( let mut date = playout_stat.date.lock().unwrap(); let current_list = play_control.current_list.lock().unwrap(); + // forward text message to ffmpeg + if map.contains_key("control") + && &map["control"] == "text" + && map.contains_key("message") + { + let mut filter = get_filter_from_json(map["message"].to_string()); + let socket = config.text.bind_address.clone(); + + if !filter.is_empty() && config.text.bind_address.is_some() { + filter = format!("Parsed_drawtext_2 reinit {filter}"); + if let Ok(reply) = executor::block_on(zmq_send(&filter, &socket.unwrap())) { + return Ok(Value::String(reply)); + }; + } + + return Ok(Value::String("Last clip can not be skipped".to_string())); + } + // get next clip if map.contains_key("control") && &map["control"] == "next" { let index = play_control.index.load(Ordering::SeqCst); diff --git a/src/rpc/zmq_cmd.rs b/src/rpc/zmq_cmd.rs new file mode 100644 index 00000000..9238b3f5 --- /dev/null +++ b/src/rpc/zmq_cmd.rs @@ -0,0 +1,14 @@ +use std::error::Error; + +use zeromq::Socket; +use zeromq::{SocketRecv, SocketSend, ZmqMessage}; + +pub async fn zmq_send(msg: &str, socket_addr: &str) -> Result> { + let mut socket = zeromq::ReqSocket::new(); + socket.connect(&format!("tcp://{socket_addr}")).await?; + socket.send(msg.into()).await?; + let repl: ZmqMessage = socket.recv().await?; + let response = String::from_utf8(repl.into_vec()[0].to_vec())?; + + Ok(response) +} diff --git a/src/utils/mod.rs b/src/utils/mod.rs index d5790cd1..8471cecb 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -190,6 +190,19 @@ impl MediaProbe { } } +/// Covert JSON string to ffmpeg filter command. +pub fn get_filter_from_json(raw_text: String) -> String { + let re1 = Regex::new(r#""|}|\{"#).unwrap(); + let re2 = Regex::new(r#",text:([^,]*)"#).unwrap(); + let text = re1.replace_all(&raw_text, ""); + let filter = re2 + .replace_all(&text, ",text:'$1'") + .replace(':', "=") + .replace(',', ":"); + + filter +} + /// Write current status to status file in temp folder. /// /// The status file is init in main function and mostly modified in RPC server. From 781656ca306dc76723ba8a944640b83ed6d38492 Mon Sep 17 00:00:00 2001 From: jb-alvarado Date: Thu, 16 Jun 2022 19:34:43 +0200 Subject: [PATCH 23/47] get, add, update presets --- src/api/handles.rs | 60 +++++++++++++++++++++++++++++++++++++++++++++- src/api/models.rs | 17 +++++++++++++ src/api/routes.rs | 48 +++++++++++++++++++++++++++++++++++-- src/bin/ffpapi.rs | 7 ++++-- 4 files changed, 127 insertions(+), 5 deletions(-) diff --git a/src/api/handles.rs b/src/api/handles.rs index 3beea9b8..fc2807e1 100644 --- a/src/api/handles.rs +++ b/src/api/handles.rs @@ -9,7 +9,7 @@ use sqlx::{migrate::MigrateDatabase, sqlite::SqliteQueryResult, Pool, Sqlite, Sq use crate::api::utils::GlobalSettings; use crate::api::{ - models::{Settings, User}, + models::{Preset, Settings, User}, utils::db_path, }; @@ -210,3 +210,61 @@ pub async fn db_update_user(id: i64, fields: String) -> Result Result, sqlx::Error> { + let conn = db_connection().await?; + let query = "SELECT * FROM presets"; + let result: Vec = sqlx::query_as(query).fetch_all(&conn).await?; + conn.close().await; + + Ok(result) +} + +pub async fn db_update_preset(id: &i64, preset: Preset) -> Result { + let conn = db_connection().await?; + let query = + "UPDATE presets SET name = $1, text = $2, x = $3, y = $4, fontsize = $5, line_spacing = $6, + fontcolor = $7, alpha = $8, box = $9, boxcolor = $10, boxborderw = 11 WHERE id = $12"; + let result: SqliteQueryResult = sqlx::query(query) + .bind(preset.name) + .bind(preset.text) + .bind(preset.x) + .bind(preset.y) + .bind(preset.fontsize) + .bind(preset.line_spacing) + .bind(preset.fontcolor) + .bind(preset.alpha) + .bind(preset.r#box) + .bind(preset.boxcolor) + .bind(preset.boxborderw) + .bind(id) + .execute(&conn) + .await?; + conn.close().await; + + Ok(result) +} + +pub async fn db_add_preset(preset: Preset) -> Result { + let conn = db_connection().await?; + let query = + "INSERT INTO presets (name, text, x, y, fontsize, line_spacing, fontcolor, alpha, box, boxcolor, boxborderw) + VALUES($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)"; + let result: SqliteQueryResult = sqlx::query(query) + .bind(preset.name) + .bind(preset.text) + .bind(preset.x) + .bind(preset.y) + .bind(preset.fontsize) + .bind(preset.line_spacing) + .bind(preset.fontcolor) + .bind(preset.alpha) + .bind(preset.r#box) + .bind(preset.boxcolor) + .bind(preset.boxborderw) + .execute(&conn) + .await?; + conn.close().await; + + Ok(result) +} diff --git a/src/api/models.rs b/src/api/models.rs index b656d693..ea3cbc24 100644 --- a/src/api/models.rs +++ b/src/api/models.rs @@ -36,6 +36,23 @@ impl LoginUser { Self { id, username } } } +#[derive(Debug, Deserialize, Serialize, Clone, sqlx::FromRow)] +pub struct Preset { + #[sqlx(default)] + #[serde(skip_deserializing)] + pub id: i64, + pub name: String, + pub text: String, + pub x: String, + pub y: String, + pub fontsize: i64, + pub line_spacing: i64, + pub fontcolor: String, + pub r#box: bool, + pub boxcolor: String, + pub boxborderw: i64, + pub alpha: String, +} #[derive(Debug, Deserialize, Serialize, sqlx::FromRow)] pub struct Settings { diff --git a/src/api/routes.rs b/src/api/routes.rs index bb31379b..baeb082e 100644 --- a/src/api/routes.rs +++ b/src/api/routes.rs @@ -11,9 +11,10 @@ use crate::api::{ auth::{create_jwt, Claims}, errors::ServiceError, handles::{ - db_add_user, db_get_settings, db_login, db_role, db_update_settings, db_update_user, + db_add_preset, db_add_user, db_get_presets, db_get_settings, db_login, db_role, + db_update_preset, db_update_settings, db_update_user, }, - models::{LoginUser, Settings, User}, + models::{LoginUser, Preset, Settings, User}, utils::{read_playout_config, Role}, }; @@ -99,6 +100,49 @@ async fn update_playout_config( Err(ServiceError::InternalServerError) } +/// curl -X PUT http://localhost:8080/api/presets/ --header 'Content-Type: application/json' \ +/// --data '{"email": "", "password": ""}' --header 'Authorization: ' +#[get("/presets/")] +#[has_any_role("Role::Admin", "Role::User", type = "Role")] +async fn get_presets() -> Result { + if let Ok(presets) = db_get_presets().await { + return Ok(web::Json(presets)); + } + + Err(ServiceError::InternalServerError) +} + +/// curl -X PUT http://localhost:8080/api/presets/1 --header 'Content-Type: application/json' \ +/// --data '{"name": "", "text": "TEXT>", "x": "", "y": "", "fontsize": 24, \ +/// "line_spacing": 4, "fontcolor": "#ffffff", "box": 1, "boxcolor": "#000000", "boxborderw": 4, "alpha": 1.0}}' \ +/// --header 'Authorization: ' +#[put("/presets/{id}")] +#[has_any_role("Role::Admin", "Role::User", type = "Role")] +async fn update_preset( + id: web::Path, + data: web::Json, +) -> Result { + if db_update_preset(&id, data.into_inner()).await.is_ok() { + return Ok("Update Success"); + } + + Err(ServiceError::InternalServerError) +} + +/// curl -X POST http://localhost:8080/api/presets/ --header 'Content-Type: application/json' \ +/// --data '{"name": "", "text": "TEXT>", "x": "", "y": "", "fontsize": 24, \ +/// "line_spacing": 4, "fontcolor": "#ffffff", "box": 1, "boxcolor": "#000000", "boxborderw": 4, "alpha": 1.0}}' \ +/// --header 'Authorization: ' +#[post("/presets/")] +#[has_any_role("Role::Admin", "Role::User", type = "Role")] +async fn add_preset(data: web::Json) -> Result { + if db_add_preset(data.into_inner()).await.is_ok() { + return Ok("Add preset Success"); + } + + Err(ServiceError::InternalServerError) +} + /// curl -X PUT http://localhost:8080/api/user/1 --header 'Content-Type: application/json' \ /// --data '{"email": "", "password": ""}' --header 'Authorization: ' #[put("/user/{id}")] diff --git a/src/bin/ffpapi.rs b/src/bin/ffpapi.rs index a24d7574..c8e4a935 100644 --- a/src/bin/ffpapi.rs +++ b/src/bin/ffpapi.rs @@ -14,8 +14,8 @@ use ffplayout_engine::{ auth, models::LoginUser, routes::{ - add_user, get_playout_config, get_settings, login, patch_settings, - update_playout_config, update_user, + add_preset, add_user, get_playout_config, get_presets, get_settings, login, + patch_settings, update_playout_config, update_preset, update_user, }, utils::{db_path, init_config, run_args, Role}, }, @@ -75,6 +75,9 @@ async fn main() -> std::io::Result<()> { .service(add_user) .service(get_playout_config) .service(update_playout_config) + .service(add_preset) + .service(get_presets) + .service(update_preset) .service(get_settings) .service(patch_settings) .service(update_user), From 5a8c1e5f43f0f919be778b8c16ea31dc6b49c9bd Mon Sep 17 00:00:00 2001 From: jb-alvarado Date: Fri, 17 Jun 2022 00:18:02 +0200 Subject: [PATCH 24/47] reorder --- assets/ffplayout.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assets/ffplayout.yml b/assets/ffplayout.yml index 5dfe30fa..db2d89c8 100644 --- a/assets/ffplayout.yml +++ b/assets/ffplayout.yml @@ -109,8 +109,8 @@ text: 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. add_text: false - fontfile: "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf" text_from_filename: false + 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" regex: ^.+[/\\](.*)(.mp4|.mkv)$ From f9a82b4f93d914cb009d900e4e49a32c8248828a Mon Sep 17 00:00:00 2001 From: jb-alvarado Date: Fri, 17 Jun 2022 16:21:03 +0200 Subject: [PATCH 25/47] add send messages to api --- Cargo.toml | 2 +- src/api/control.rs | 80 ++++++++++++++++++++++++++++++++++++++++++++++ src/api/handles.rs | 13 +++++--- src/api/mod.rs | 1 + src/api/models.rs | 3 +- src/api/routes.rs | 29 +++++++++++++++-- src/bin/ffpapi.rs | 5 +-- src/utils/mod.rs | 8 +++-- 8 files changed, 126 insertions(+), 15 deletions(-) create mode 100644 src/api/control.rs diff --git a/Cargo.toml b/Cargo.toml index 3682ee91..e75d9c1c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,7 +30,7 @@ once_cell = "1.10" rand = "0.8" rand_core = { version = "0.6", features = ["std"] } regex = "1" -reqwest = { version = "0.11", features = ["blocking"] } +reqwest = { version = "0.11", features = ["blocking", "json"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" serde_yaml = "0.8" diff --git a/src/api/control.rs b/src/api/control.rs new file mode 100644 index 00000000..d07d93a9 --- /dev/null +++ b/src/api/control.rs @@ -0,0 +1,80 @@ +use log::error; +use reqwest::{ + header::{HeaderMap, AUTHORIZATION, CONTENT_TYPE}, + Client, Response, +}; +use serde::{Deserialize, Serialize}; + +use crate::api::{ + errors::ServiceError, handles::db_get_settings, models::TextPreset, utils::read_playout_config, +}; + +#[derive(Debug, Deserialize, Serialize, Clone)] +struct RpcObj { + jsonrpc: String, + id: i64, + method: String, + params: T, +} + +#[derive(Debug, Deserialize, Serialize, Clone)] +struct TextParams { + control: String, + message: TextPreset, +} + +impl RpcObj { + fn new(id: i64, method: String, params: T) -> Self { + Self { + jsonrpc: "2.0".into(), + id, + method, + params, + } + } +} + +fn create_header(content: &str, auth: &str) -> HeaderMap { + let mut headers = HeaderMap::new(); + headers.insert(CONTENT_TYPE, content.parse().unwrap()); + headers.insert(AUTHORIZATION, auth.parse().unwrap()); + + headers +} + +pub async fn send_message(id: i64, msg: TextPreset) -> Result { + let client = Client::new(); + + if let Ok(settings) = db_get_settings(&id).await { + if let Ok(config) = read_playout_config(&settings.config_path) { + let url = format!("http://{}", config.rpc_server.address); + let json_obj = RpcObj::new( + id, + "player".into(), + TextParams { + control: "text".into(), + message: msg, + }, + ); + + match client + .post(&url) + .headers(create_header( + "Content-Type: application/json", + &config.rpc_server.authorization, + )) + .json(&json_obj) + .send() + .await + { + Ok(result) => return Ok(result), + Err(e) => { + error!("{e:?}"); + return Err(ServiceError::BadRequest(e.to_string())); + } + }; + } + }; + + Err(ServiceError::InternalServerError) +} diff --git a/src/api/handles.rs b/src/api/handles.rs index fc2807e1..02101aee 100644 --- a/src/api/handles.rs +++ b/src/api/handles.rs @@ -9,7 +9,7 @@ use sqlx::{migrate::MigrateDatabase, sqlite::SqliteQueryResult, Pool, Sqlite, Sq use crate::api::utils::GlobalSettings; use crate::api::{ - models::{Preset, Settings, User}, + models::{Settings, TextPreset, User}, utils::db_path, }; @@ -211,16 +211,19 @@ pub async fn db_update_user(id: i64, fields: String) -> Result Result, sqlx::Error> { +pub async fn db_get_presets() -> Result, sqlx::Error> { let conn = db_connection().await?; let query = "SELECT * FROM presets"; - let result: Vec = sqlx::query_as(query).fetch_all(&conn).await?; + let result: Vec = sqlx::query_as(query).fetch_all(&conn).await?; conn.close().await; Ok(result) } -pub async fn db_update_preset(id: &i64, preset: Preset) -> Result { +pub async fn db_update_preset( + id: &i64, + preset: TextPreset, +) -> Result { let conn = db_connection().await?; let query = "UPDATE presets SET name = $1, text = $2, x = $3, y = $4, fontsize = $5, line_spacing = $6, @@ -245,7 +248,7 @@ pub async fn db_update_preset(id: &i64, preset: Preset) -> Result Result { +pub async fn db_add_preset(preset: TextPreset) -> Result { let conn = db_connection().await?; let query = "INSERT INTO presets (name, text, x, y, fontsize, line_spacing, fontcolor, alpha, box, boxcolor, boxborderw) diff --git a/src/api/mod.rs b/src/api/mod.rs index 6ecc6917..7bef2175 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -1,5 +1,6 @@ pub mod args_parse; pub mod auth; +pub mod control; pub mod errors; pub mod handles; pub mod models; diff --git a/src/api/models.rs b/src/api/models.rs index ea3cbc24..ea314c4c 100644 --- a/src/api/models.rs +++ b/src/api/models.rs @@ -37,10 +37,11 @@ impl LoginUser { } } #[derive(Debug, Deserialize, Serialize, Clone, sqlx::FromRow)] -pub struct Preset { +pub struct TextPreset { #[sqlx(default)] #[serde(skip_deserializing)] pub id: i64, + #[serde(skip_deserializing)] pub name: String, pub text: String, pub x: String, diff --git a/src/api/routes.rs b/src/api/routes.rs index baeb082e..c4194f8a 100644 --- a/src/api/routes.rs +++ b/src/api/routes.rs @@ -9,12 +9,13 @@ use simplelog::*; use crate::api::{ auth::{create_jwt, Claims}, + control::send_message, errors::ServiceError, handles::{ db_add_preset, db_add_user, db_get_presets, db_get_settings, db_login, db_role, db_update_preset, db_update_settings, db_update_user, }, - models::{LoginUser, Preset, Settings, User}, + models::{LoginUser, Settings, TextPreset, User}, utils::{read_playout_config, Role}, }; @@ -120,7 +121,7 @@ async fn get_presets() -> Result { #[has_any_role("Role::Admin", "Role::User", type = "Role")] async fn update_preset( id: web::Path, - data: web::Json, + data: web::Json, ) -> Result { if db_update_preset(&id, data.into_inner()).await.is_ok() { return Ok("Update Success"); @@ -135,7 +136,7 @@ async fn update_preset( /// --header 'Authorization: ' #[post("/presets/")] #[has_any_role("Role::Admin", "Role::User", type = "Role")] -async fn add_preset(data: web::Json) -> Result { +async fn add_preset(data: web::Json) -> Result { if db_add_preset(data.into_inner()).await.is_ok() { return Ok("Add preset Success"); } @@ -253,3 +254,25 @@ pub async fn login(credentials: web::Json) -> impl Responder { } } } + +/// ---------------------------------------------------------------------------- +/// ffplayout process 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 the the engine, for overlaying it (as lower third etc.) +/// ---------------------------------------------------------------------------- + +#[post("/control/text/{id}")] +#[has_any_role("Role::Admin", "Role::User", type = "Role")] +pub async fn send_text_message( + id: web::Path, + data: web::Json, +) -> Result { + match send_message(*id, data.into_inner()).await { + Ok(res) => return Ok(res.text().await.unwrap_or_else(|_| "Success".into())), + Err(e) => Err(e), + } +} diff --git a/src/bin/ffpapi.rs b/src/bin/ffpapi.rs index c8e4a935..47ee0381 100644 --- a/src/bin/ffpapi.rs +++ b/src/bin/ffpapi.rs @@ -15,7 +15,7 @@ use ffplayout_engine::{ models::LoginUser, routes::{ add_preset, add_user, get_playout_config, get_presets, get_settings, login, - patch_settings, update_playout_config, update_preset, update_user, + patch_settings, send_text_message, update_playout_config, update_preset, update_user, }, utils::{db_path, init_config, run_args, Role}, }, @@ -80,7 +80,8 @@ async fn main() -> std::io::Result<()> { .service(update_preset) .service(get_settings) .service(patch_settings) - .service(update_user), + .service(update_user) + .service(send_text_message), ) }) .bind((addr, port))? diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 8471cecb..fde9de06 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -193,10 +193,12 @@ impl MediaProbe { /// Covert JSON string to ffmpeg filter command. pub fn get_filter_from_json(raw_text: String) -> String { let re1 = Regex::new(r#""|}|\{"#).unwrap(); - let re2 = Regex::new(r#",text:([^,]*)"#).unwrap(); + let re2 = Regex::new(r#"id:[0-9]+,?|name:[^,]?,?"#).unwrap(); + let re3 = Regex::new(r#"text:([^,]*)"#).unwrap(); let text = re1.replace_all(&raw_text, ""); - let filter = re2 - .replace_all(&text, ",text:'$1'") + let text = re2.replace_all(&text, "").clone(); + let filter = re3 + .replace_all(&text, "text:'$1'") .replace(':', "=") .replace(',', ":"); From 8e3194813d0e6e08b08d328b6e02f19b1ba2845d Mon Sep 17 00:00:00 2001 From: jb-alvarado Date: Fri, 17 Jun 2022 19:01:53 +0200 Subject: [PATCH 26/47] generalize get config --- src/api/control.rs | 77 +++++++++++++++++++++++++--------------------- 1 file changed, 42 insertions(+), 35 deletions(-) diff --git a/src/api/control.rs b/src/api/control.rs index d07d93a9..9f600bac 100644 --- a/src/api/control.rs +++ b/src/api/control.rs @@ -1,13 +1,14 @@ -use log::error; use reqwest::{ header::{HeaderMap, AUTHORIZATION, CONTENT_TYPE}, Client, Response, }; use serde::{Deserialize, Serialize}; +use simplelog::*; use crate::api::{ errors::ServiceError, handles::db_get_settings, models::TextPreset, utils::read_playout_config, }; +use crate::utils::PlayoutConfig; #[derive(Debug, Deserialize, Serialize, Clone)] struct RpcObj { @@ -34,47 +35,53 @@ impl RpcObj { } } -fn create_header(content: &str, auth: &str) -> HeaderMap { +fn create_header(auth: &str) -> HeaderMap { let mut headers = HeaderMap::new(); - headers.insert(CONTENT_TYPE, content.parse().unwrap()); + headers.insert( + CONTENT_TYPE, + "Content-Type: application/json".parse().unwrap(), + ); headers.insert(AUTHORIZATION, auth.parse().unwrap()); headers } -pub async fn send_message(id: i64, msg: TextPreset) -> Result { - let client = Client::new(); - - if let Ok(settings) = db_get_settings(&id).await { +async fn playout_config(channel_id: &i64) -> Result { + if let Ok(settings) = db_get_settings(channel_id).await { if let Ok(config) = read_playout_config(&settings.config_path) { - let url = format!("http://{}", config.rpc_server.address); - let json_obj = RpcObj::new( - id, - "player".into(), - TextParams { - control: "text".into(), - message: msg, - }, - ); - - match client - .post(&url) - .headers(create_header( - "Content-Type: application/json", - &config.rpc_server.authorization, - )) - .json(&json_obj) - .send() - .await - { - Ok(result) => return Ok(result), - Err(e) => { - error!("{e:?}"); - return Err(ServiceError::BadRequest(e.to_string())); - } - }; + return Ok(config); } - }; + } - Err(ServiceError::InternalServerError) + Err(ServiceError::BadRequest( + "Error in getting config!".to_string(), + )) +} + +pub async fn send_message(id: i64, message: TextPreset) -> Result { + let config = playout_config(&id).await?; + let url = format!("http://{}", config.rpc_server.address); + let client = Client::new(); + let json_obj = RpcObj::new( + id, + "player".into(), + TextParams { + control: "text".into(), + message, + }, + ); + + match client + .post(&url) + .headers(create_header(&config.rpc_server.authorization)) + .json(&json_obj) + .send() + .await + { + Ok(result) => Ok(result), + Err(e) => { + error!("{e:?}"); + Err(ServiceError::BadRequest(e.to_string())) + } + } } From f929fdcb70b3e598cc5c7adf8f8b9e3c095a1516 Mon Sep 17 00:00:00 2001 From: jb-alvarado Date: Sun, 19 Jun 2022 11:44:47 +0200 Subject: [PATCH 27/47] log error --- src/input/playlist.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/input/playlist.rs b/src/input/playlist.rs index 8b2b0ad9..d6149388 100644 --- a/src/input/playlist.rs +++ b/src/input/playlist.rs @@ -171,8 +171,10 @@ impl CurrentProgram { *self.playout_stat.time_shift.lock().unwrap() = 0.0; let status_data: String = serde_json::to_string(&data).expect("Serialize status data failed"); - fs::write(self.config.general.stat_file.clone(), &status_data) - .expect("Unable to write file"); + + if let Err(e) = fs::write(self.config.general.stat_file.clone(), &status_data) { + error!("Unable to write status file: {e}"); + }; self.json_path = json.current_file.clone(); self.json_mod = json.modified; From 6c9add4dd794f9db9a23be59dac960e06f8a7b02 Mon Sep 17 00:00:00 2001 From: jb-alvarado Date: Sun, 19 Jun 2022 11:46:51 +0200 Subject: [PATCH 28/47] log error --- src/input/playlist.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/input/playlist.rs b/src/input/playlist.rs index d6149388..47513422 100644 --- a/src/input/playlist.rs +++ b/src/input/playlist.rs @@ -56,7 +56,9 @@ impl CurrentProgram { }); let json: String = serde_json::to_string(&data).expect("Serialize status data failed"); - fs::write(config.general.stat_file.clone(), &json).expect("Unable to write file"); + if let Err(e) = fs::write(config.general.stat_file.clone(), &json) { + error!("Unable to write status file: {e}"); + }; } Self { From 5534ca049a5601bbd3b2fd6a7abefb0468a3bd7a Mon Sep 17 00:00:00 2001 From: jb-alvarado Date: Sun, 19 Jun 2022 12:03:00 +0200 Subject: [PATCH 29/47] log write error --- src/main.rs | 4 +++- src/utils/mod.rs | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main.rs b/src/main.rs index 1e7654c3..3d48d8b4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -39,7 +39,9 @@ fn status_file(stat_file: &str, playout_stat: &PlayoutStatus) { }); let json: String = serde_json::to_string(&data).expect("Serialize status data failed"); - fs::write(stat_file, &json).expect("Unable to write file"); + if let Err(e) = fs::write(stat_file, &json) { + error!("Unable to write status file: {e}"); + }; } else { let stat_file = File::options() .read(true) diff --git a/src/utils/mod.rs b/src/utils/mod.rs index fde9de06..c71b5526 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -216,7 +216,7 @@ pub fn write_status(config: &PlayoutConfig, date: &str, shift: f64) { let status_data: String = serde_json::to_string(&data).expect("Serialize status data failed"); if let Err(e) = fs::write(&config.general.stat_file, &status_data) { - error!("Unable to write file: {e:?}") + error!("Unable to write status file: {e:?}") }; } From 03dc21c6a3fb7c1f8afd18321d75390a04719e60 Mon Sep 17 00:00:00 2001 From: jb-alvarado Date: Sun, 19 Jun 2022 22:02:28 +0200 Subject: [PATCH 30/47] ignore hyper logging, check last two messages --- src/utils/logging.rs | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/utils/logging.rs b/src/utils/logging.rs index a8ce2c31..035faf43 100644 --- a/src/utils/logging.rs +++ b/src/utils/logging.rs @@ -92,7 +92,7 @@ pub struct LogMailer { level: LevelFilter, pub config: Config, messages: Arc>>, - last_message: Arc>, + last_messages: Arc>>, } impl LogMailer { @@ -105,7 +105,7 @@ impl LogMailer { level: log_level, config, messages, - last_message: Arc::new(Mutex::new(String::new())), + last_messages: Arc::new(Mutex::new(vec![String::new()])), }) } } @@ -118,12 +118,15 @@ impl Log for LogMailer { fn log(&self, record: &Record<'_>) { if self.enabled(record.metadata()) { let rec = record.args().to_string(); - let mut last_msg = self.last_message.lock().unwrap(); + let mut last_msgs = self.last_messages.lock().unwrap(); // put message only to mail queue when it differs from last message // this we do to prevent spamming the mail box - if *last_msg != rec { - *last_msg = rec.clone(); + if !last_msgs.contains(&rec) { + if last_msgs.len() > 2 { + last_msgs.clear() + } + last_msgs.push(rec.clone()); let local: DateTime = Local::now(); let time_stamp = local.format("[%Y-%m-%d %H:%M:%S%.3f]"); let level = record.level().to_string().to_uppercase(); @@ -182,6 +185,7 @@ pub fn init_logging( let mut log_config = ConfigBuilder::new() .set_thread_level(LevelFilter::Off) .set_target_level(LevelFilter::Off) + .add_filter_ignore_str("hyper") .add_filter_ignore_str("sqlx") .add_filter_ignore_str("reqwest") .set_level_padding(LevelPadding::Left) From 4834b47d74c905d94d60c7c30d04768b02cecb5c Mon Sep 17 00:00:00 2001 From: jb-alvarado Date: Sun, 19 Jun 2022 22:03:17 +0200 Subject: [PATCH 31/47] add controls, next, back, reset and get media info --- src/api/control.rs | 61 +++++++++++++++++++++++++++-------- src/api/handles.rs | 8 ++--- src/api/models.rs | 8 ++--- src/api/routes.rs | 79 ++++++++++++++++++++++++++++++++++++++++++++-- src/bin/ffpapi.rs | 13 ++++++-- 5 files changed, 141 insertions(+), 28 deletions(-) diff --git a/src/api/control.rs b/src/api/control.rs index 9f600bac..d803c6af 100644 --- a/src/api/control.rs +++ b/src/api/control.rs @@ -1,3 +1,5 @@ +use std::collections::HashMap; + use reqwest::{ header::{HeaderMap, AUTHORIZATION, CONTENT_TYPE}, Client, Response, @@ -5,9 +7,7 @@ use reqwest::{ use serde::{Deserialize, Serialize}; use simplelog::*; -use crate::api::{ - errors::ServiceError, handles::db_get_settings, models::TextPreset, utils::read_playout_config, -}; +use crate::api::{errors::ServiceError, handles::db_get_settings, utils::read_playout_config}; use crate::utils::PlayoutConfig; #[derive(Debug, Deserialize, Serialize, Clone)] @@ -21,7 +21,17 @@ struct RpcObj { #[derive(Debug, Deserialize, Serialize, Clone)] struct TextParams { control: String, - message: TextPreset, + message: HashMap, +} + +#[derive(Debug, Deserialize, Serialize, Clone)] +struct ControlParams { + control: String, +} + +#[derive(Debug, Deserialize, Serialize, Clone)] +struct MediaParams { + media: String, } impl RpcObj { @@ -58,23 +68,18 @@ async fn playout_config(channel_id: &i64) -> Result )) } -pub async fn send_message(id: i64, message: TextPreset) -> Result { +async fn post_request(id: i64, obj: RpcObj) -> Result +where + T: Serialize, +{ let config = playout_config(&id).await?; let url = format!("http://{}", config.rpc_server.address); let client = Client::new(); - let json_obj = RpcObj::new( - id, - "player".into(), - TextParams { - control: "text".into(), - message, - }, - ); match client .post(&url) .headers(create_header(&config.rpc_server.authorization)) - .json(&json_obj) + .json(&obj) .send() .await { @@ -85,3 +90,31 @@ pub async fn send_message(id: i64, message: TextPreset) -> Result, +) -> Result { + let json_obj = RpcObj::new( + id, + "player".into(), + TextParams { + control: "text".into(), + message, + }, + ); + + post_request(id, json_obj).await +} + +pub async fn control_state(id: i64, command: String) -> Result { + let json_obj = RpcObj::new(id, "player".into(), ControlParams { control: command }); + + post_request(id, json_obj).await +} + +pub async fn media_info(id: i64, command: String) -> Result { + let json_obj = RpcObj::new(id, "player".into(), MediaParams { media: command }); + + post_request(id, json_obj).await +} diff --git a/src/api/handles.rs b/src/api/handles.rs index 02101aee..5a88a184 100644 --- a/src/api/handles.rs +++ b/src/api/handles.rs @@ -40,12 +40,12 @@ async fn create_schema() -> Result { text TEXT NOT NULL, x TEXT NOT NULL, y TEXT NOT NULL, - fontsize INTEGER NOT NULL DEFAULT 24, - line_spacing INTEGER NOT NULL DEFAULT 4, + fontsize TEXT NOT NULL, + line_spacing TEXT NOT NULL, fontcolor TEXT NOT NULL, - box INTEGER NOT NULL DEFAULT 1, + box TEXT NOT NULL, boxcolor TEXT NOT NULL, - boxborderw INTEGER NOT NULL DEFAULT 4, + boxborderw TEXT NOT NULL, alpha TEXT NOT NULL, UNIQUE(name) ); diff --git a/src/api/models.rs b/src/api/models.rs index ea314c4c..1227c82f 100644 --- a/src/api/models.rs +++ b/src/api/models.rs @@ -46,12 +46,12 @@ pub struct TextPreset { pub text: String, pub x: String, pub y: String, - pub fontsize: i64, - pub line_spacing: i64, + pub fontsize: String, + pub line_spacing: String, pub fontcolor: String, - pub r#box: bool, + pub r#box: String, pub boxcolor: String, - pub boxborderw: i64, + pub boxborderw: String, pub alpha: String, } diff --git a/src/api/routes.rs b/src/api/routes.rs index c4194f8a..3bc14007 100644 --- a/src/api/routes.rs +++ b/src/api/routes.rs @@ -1,3 +1,5 @@ +use std::collections::HashMap; + use actix_web::{get, http::StatusCode, patch, post, put, web, Responder}; use actix_web_grants::{permissions::AuthDetails, proc_macro::has_any_role}; use argon2::{ @@ -9,7 +11,7 @@ use simplelog::*; use crate::api::{ auth::{create_jwt, Claims}, - control::send_message, + control::{control_state, media_info, send_message}, errors::ServiceError, handles::{ db_add_preset, db_add_user, db_get_presets, db_get_settings, db_login, db_role, @@ -265,14 +267,85 @@ pub async fn login(credentials: web::Json) -> impl Responder { /// - send text the the engine, for overlaying it (as lower third etc.) /// ---------------------------------------------------------------------------- -#[post("/control/text/{id}")] +/// curl -X POST http://localhost:8080/api/control/1/text/ \ +/// --header 'Content-Type: application/json' --header 'Authorization: ' \ +/// --data '{"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"}' +#[post("/control/{id}/text/")] #[has_any_role("Role::Admin", "Role::User", type = "Role")] pub async fn send_text_message( id: web::Path, - data: web::Json, + data: web::Json>, ) -> Result { match send_message(*id, data.into_inner()).await { Ok(res) => return Ok(res.text().await.unwrap_or_else(|_| "Success".into())), Err(e) => Err(e), } } + +/// curl -X POST http://localhost:8080/api/control/1/playout/next/ +/// --header 'Content-Type: application/json' --header 'Authorization: ' +#[post("/control/{id}/playout/next/")] +#[has_any_role("Role::Admin", "Role::User", type = "Role")] +pub async fn jump_to_next(id: web::Path) -> Result { + match control_state(*id, "next".into()).await { + Ok(res) => return Ok(res.text().await.unwrap_or_else(|_| "Success".into())), + Err(e) => Err(e), + } +} + +/// curl -X POST http://localhost:8080/api/control/1/playout/back/ +/// --header 'Content-Type: application/json' --header 'Authorization: ' +#[post("/control/{id}/playout/back/")] +#[has_any_role("Role::Admin", "Role::User", type = "Role")] +pub async fn jump_to_last(id: web::Path) -> Result { + match control_state(*id, "back".into()).await { + Ok(res) => return Ok(res.text().await.unwrap_or_else(|_| "Success".into())), + Err(e) => Err(e), + } +} + +/// curl -X POST http://localhost:8080/api/control/1/playout/reset/ +/// --header 'Content-Type: application/json' --header 'Authorization: ' +#[post("/control/{id}/playout/reset/")] +#[has_any_role("Role::Admin", "Role::User", type = "Role")] +pub async fn reset_playout(id: web::Path) -> Result { + match control_state(*id, "reset".into()).await { + Ok(res) => return Ok(res.text().await.unwrap_or_else(|_| "Success".into())), + Err(e) => Err(e), + } +} + +/// curl -X GET http://localhost:8080/api/control/1/media/current/ +/// --header 'Content-Type: application/json' --header 'Authorization: ' +#[get("/control/{id}/media/current/")] +#[has_any_role("Role::Admin", "Role::User", type = "Role")] +pub async fn media_current(id: web::Path) -> Result { + match media_info(*id, "current".into()).await { + Ok(res) => return Ok(res.text().await.unwrap_or_else(|_| "Success".into())), + Err(e) => Err(e), + } +} + +/// curl -X GET http://localhost:8080/api/control/1/media/next/ +/// --header 'Content-Type: application/json' --header 'Authorization: ' +#[get("/control/{id}/media/next/")] +#[has_any_role("Role::Admin", "Role::User", type = "Role")] +pub async fn media_next(id: web::Path) -> Result { + match media_info(*id, "next".into()).await { + Ok(res) => return Ok(res.text().await.unwrap_or_else(|_| "Success".into())), + Err(e) => Err(e), + } +} + +/// curl -X GET http://localhost:8080/api/control/1/media/last/ +/// --header 'Content-Type: application/json' --header 'Authorization: ' +#[get("/control/{id}/media/last/")] +#[has_any_role("Role::Admin", "Role::User", type = "Role")] +pub async fn media_last(id: web::Path) -> Result { + match media_info(*id, "last".into()).await { + Ok(res) => return Ok(res.text().await.unwrap_or_else(|_| "Success".into())), + Err(e) => Err(e), + } +} diff --git a/src/bin/ffpapi.rs b/src/bin/ffpapi.rs index 47ee0381..b66d6bfb 100644 --- a/src/bin/ffpapi.rs +++ b/src/bin/ffpapi.rs @@ -14,8 +14,9 @@ use ffplayout_engine::{ auth, models::LoginUser, routes::{ - add_preset, add_user, get_playout_config, get_presets, get_settings, login, - patch_settings, send_text_message, update_playout_config, update_preset, update_user, + add_preset, add_user, get_playout_config, get_presets, get_settings, jump_to_last, + jump_to_next, login, media_current, media_last, media_next, patch_settings, + reset_playout, send_text_message, update_playout_config, update_preset, update_user, }, utils::{db_path, init_config, run_args, Role}, }, @@ -81,7 +82,13 @@ async fn main() -> std::io::Result<()> { .service(get_settings) .service(patch_settings) .service(update_user) - .service(send_text_message), + .service(send_text_message) + .service(jump_to_next) + .service(jump_to_last) + .service(reset_playout) + .service(media_current) + .service(media_next) + .service(media_last), ) }) .bind((addr, port))? From fccee59d69b005dfe97457f13d8d7b3e69e0a3e3 Mon Sep 17 00:00:00 2001 From: jb-alvarado Date: Sun, 19 Jun 2022 22:55:10 +0200 Subject: [PATCH 32/47] get playlist --- src/api/control.rs | 15 +-------------- src/api/mod.rs | 1 + src/api/playlist.rs | 23 +++++++++++++++++++++++ src/api/routes.rs | 25 ++++++++++++++++++++++--- src/api/utils.rs | 15 ++++++++++++++- src/bin/ffpapi.rs | 10 ++++++---- 6 files changed, 67 insertions(+), 22 deletions(-) create mode 100644 src/api/playlist.rs diff --git a/src/api/control.rs b/src/api/control.rs index d803c6af..4f8b8ad8 100644 --- a/src/api/control.rs +++ b/src/api/control.rs @@ -7,8 +7,7 @@ use reqwest::{ use serde::{Deserialize, Serialize}; use simplelog::*; -use crate::api::{errors::ServiceError, handles::db_get_settings, utils::read_playout_config}; -use crate::utils::PlayoutConfig; +use crate::api::{errors::ServiceError, utils::playout_config}; #[derive(Debug, Deserialize, Serialize, Clone)] struct RpcObj { @@ -56,18 +55,6 @@ fn create_header(auth: &str) -> HeaderMap { headers } -async fn playout_config(channel_id: &i64) -> Result { - if let Ok(settings) = db_get_settings(channel_id).await { - if let Ok(config) = read_playout_config(&settings.config_path) { - return Ok(config); - } - } - - Err(ServiceError::BadRequest( - "Error in getting config!".to_string(), - )) -} - async fn post_request(id: i64, obj: RpcObj) -> Result where T: Serialize, diff --git a/src/api/mod.rs b/src/api/mod.rs index 7bef2175..acc618e1 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -4,5 +4,6 @@ pub mod control; pub mod errors; pub mod handles; pub mod models; +pub mod playlist; pub mod routes; pub mod utils; diff --git a/src/api/playlist.rs b/src/api/playlist.rs new file mode 100644 index 00000000..b2bdbdec --- /dev/null +++ b/src/api/playlist.rs @@ -0,0 +1,23 @@ +use std::{fs::File, path::PathBuf}; + +use crate::api::{errors::ServiceError, utils::playout_config}; +use crate::utils::JsonPlaylist; + +pub async fn read_playlist(id: i64, date: String) -> Result { + let config = playout_config(&id).await?; + let mut playlist_path = PathBuf::from(&config.playlist.path); + let d: Vec<&str> = date.split('-').collect(); + playlist_path = playlist_path + .join(d[0]) + .join(d[1]) + .join(date.clone()) + .with_extension("json"); + + if let Ok(f) = File::options().read(true).write(false).open(&playlist_path) { + if let Ok(p) = serde_json::from_reader(f) { + return Ok(p); + } + }; + + Err(ServiceError::InternalServerError) +} diff --git a/src/api/routes.rs b/src/api/routes.rs index 3bc14007..fcaf0433 100644 --- a/src/api/routes.rs +++ b/src/api/routes.rs @@ -18,6 +18,7 @@ use crate::api::{ db_update_preset, db_update_settings, db_update_user, }, models::{LoginUser, Settings, TextPreset, User}, + playlist::read_playlist, utils::{read_playout_config, Role}, }; @@ -319,7 +320,7 @@ pub async fn reset_playout(id: web::Path) -> Result' -#[get("/control/{id}/media/current/")] +#[get("/control/{id}/media/current")] #[has_any_role("Role::Admin", "Role::User", type = "Role")] pub async fn media_current(id: web::Path) -> Result { match media_info(*id, "current".into()).await { @@ -330,7 +331,7 @@ pub async fn media_current(id: web::Path) -> Result' -#[get("/control/{id}/media/next/")] +#[get("/control/{id}/media/next")] #[has_any_role("Role::Admin", "Role::User", type = "Role")] pub async fn media_next(id: web::Path) -> Result { match media_info(*id, "next".into()).await { @@ -341,7 +342,7 @@ pub async fn media_next(id: web::Path) -> Result' -#[get("/control/{id}/media/last/")] +#[get("/control/{id}/media/last")] #[has_any_role("Role::Admin", "Role::User", type = "Role")] pub async fn media_last(id: web::Path) -> Result { match media_info(*id, "last".into()).await { @@ -349,3 +350,21 @@ pub async fn media_last(id: web::Path) -> Result Err(e), } } + +/// ---------------------------------------------------------------------------- +/// ffplayout playlist operations +/// +/// ---------------------------------------------------------------------------- + +/// curl -X GET http://localhost:8080/api/playlist/1/2022-06-20 +/// --header 'Content-Type: application/json' --header 'Authorization: ' +#[get("/playlist/{id}/{date}")] +#[has_any_role("Role::Admin", "Role::User", type = "Role")] +pub async fn get_playlist( + params: web::Path<(i64, String)>, +) -> Result { + match read_playlist(params.0, params.1.clone()).await { + Ok(playlist) => Ok(web::Json(playlist)), + Err(e) => Err(e), + } +} diff --git a/src/api/utils.rs b/src/api/utils.rs index 1838fc19..231a157b 100644 --- a/src/api/utils.rs +++ b/src/api/utils.rs @@ -6,7 +6,8 @@ use simplelog::*; use crate::api::{ args_parse::Args, - handles::{db_add_user, db_global, db_init}, + errors::ServiceError, + handles::{db_add_user, db_get_settings, db_global, db_init}, models::User, }; use crate::utils::PlayoutConfig; @@ -120,3 +121,15 @@ pub fn read_playout_config(path: &str) -> Result> Ok(config) } + +pub async fn playout_config(channel_id: &i64) -> Result { + if let Ok(settings) = db_get_settings(channel_id).await { + if let Ok(config) = read_playout_config(&settings.config_path) { + return Ok(config); + } + } + + Err(ServiceError::BadRequest( + "Error in getting config!".to_string(), + )) +} diff --git a/src/bin/ffpapi.rs b/src/bin/ffpapi.rs index b66d6bfb..216dfe56 100644 --- a/src/bin/ffpapi.rs +++ b/src/bin/ffpapi.rs @@ -14,9 +14,10 @@ use ffplayout_engine::{ auth, models::LoginUser, routes::{ - add_preset, add_user, get_playout_config, get_presets, get_settings, jump_to_last, - jump_to_next, login, media_current, media_last, media_next, patch_settings, - reset_playout, send_text_message, update_playout_config, update_preset, update_user, + add_preset, add_user, get_playlist, get_playout_config, get_presets, get_settings, + jump_to_last, jump_to_next, login, media_current, media_last, media_next, + patch_settings, reset_playout, send_text_message, update_playout_config, update_preset, + update_user, }, utils::{db_path, init_config, run_args, Role}, }, @@ -88,7 +89,8 @@ async fn main() -> std::io::Result<()> { .service(reset_playout) .service(media_current) .service(media_next) - .service(media_last), + .service(media_last) + .service(get_playlist), ) }) .bind((addr, port))? From 6243c685dc51cd0749f919193d89e58c66f2ca35 Mon Sep 17 00:00:00 2001 From: jb-alvarado Date: Mon, 20 Jun 2022 18:11:12 +0200 Subject: [PATCH 33/47] add folder for deb package, update clap --- Cargo.lock | 148 +++++++++++++++++++++++++----------------------- Cargo.toml | 5 +- debian/.gitkeep | 0 3 files changed, 80 insertions(+), 73 deletions(-) create mode 100644 debian/.gitkeep diff --git a/Cargo.lock b/Cargo.lock index 1a1eb359..2782da06 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -112,7 +112,7 @@ dependencies = [ "actix-utils", "futures-core", "futures-util", - "mio 0.8.3", + "mio 0.8.4", "num_cpus", "socket2", "tokio", @@ -176,7 +176,7 @@ dependencies = [ "serde_urlencoded", "smallvec", "socket2", - "time 0.3.9", + "time 0.3.10", "url", ] @@ -238,7 +238,7 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" dependencies = [ - "getrandom 0.2.6", + "getrandom 0.2.7", "once_cell", "version_check", ] @@ -358,16 +358,16 @@ dependencies = [ [[package]] name = "async-std" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52580991739c5cdb36cde8b2a516371c0a3b70dda36d916cc08b82372916808c" +checksum = "62565bb4402e926b29953c785397c6dc0391b7b446e45008b0049eb43cec6f5d" dependencies = [ "async-attributes", "async-channel", "async-global-executor", "async-io", "async-lock", - "crossbeam-utils 0.8.8", + "crossbeam-utils 0.8.9", "futures-channel", "futures-core", "futures-io", @@ -376,7 +376,6 @@ dependencies = [ "kv-log-macro", "log", "memchr", - "num_cpus", "once_cell", "pin-project-lite", "pin-utils", @@ -606,16 +605,16 @@ dependencies = [ [[package]] name = "clap" -version = "3.1.18" +version = "3.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2dbdf4bdacb33466e854ce889eee8dfd5729abf7ccd7664d0a2d60cd384440b" +checksum = "d53da17d37dba964b9b3ecb5c5a1f193a2762c700e6829201e645b9381c99dc7" dependencies = [ "atty", "bitflags", "clap_derive", "clap_lex", "indexmap", - "lazy_static", + "once_cell", "strsim", "termcolor", "textwrap", @@ -623,9 +622,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "3.1.18" +version = "3.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25320346e922cffe59c0bbc5410c8d8784509efb321488971081313cb1e1a33c" +checksum = "c11d40217d16aee8508cc8e5fde8b4ff24639758608e5374e731b53f85749fb9" dependencies = [ "heck", "proc-macro-error", @@ -636,9 +635,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.2.0" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a37c35f1112dad5e6e0b1adaff798507497a18fceeb30cceb3bae7d1427b9213" +checksum = "5538cd660450ebeb4234cfecf8f2284b844ffc4c50531e66d584ad5b91293613" dependencies = [ "os_str_bytes", ] @@ -668,7 +667,7 @@ version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "615f6e27d000a2bffbc7f2f6a8669179378fa27ee4d0a509e985dfc0a7defb40" dependencies = [ - "getrandom 0.2.6", + "getrandom 0.2.7", "lazy_static", "proc-macro-hack", "tiny-keccak", @@ -687,7 +686,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94d4706de1b0fa5b132270cddffa8585166037822e260a944fe161acd137ca05" dependencies = [ "percent-encoding", - "time 0.3.9", + "time 0.3.10", "version_check", ] @@ -766,12 +765,12 @@ dependencies = [ [[package]] name = "crossbeam-channel" -version = "0.5.4" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5aaa7bd5fb665c6864b5f963dd9097905c54125909c7aa94c9e18507cdbe6c53" +checksum = "4c02a4d71819009c192cf4872265391563fd6a84c81ff2c0f2a7026ca4c1d85c" dependencies = [ "cfg-if 1.0.0", - "crossbeam-utils 0.8.8", + "crossbeam-utils 0.8.9", ] [[package]] @@ -818,7 +817,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f25d8400f4a7a5778f0e4e52384a48cbd9b5c495d110786187fc750075277a2" dependencies = [ "cfg-if 1.0.0", - "crossbeam-utils 0.8.8", + "crossbeam-utils 0.8.9", ] [[package]] @@ -834,12 +833,12 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.8" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf124c720b7686e3c2663cf54062ab0f68a88af2fb6a030e87e30bf721fcb38" +checksum = "8ff1f980957787286a554052d03c7aee98d99cc32e09f6d45f0a814133c87978" dependencies = [ "cfg-if 1.0.0", - "lazy_static", + "once_cell", ] [[package]] @@ -987,7 +986,7 @@ dependencies = [ "argon2", "chrono 0.4.19 (git+https://github.com/sbrocket/chrono?branch=parse-error-kind-public)", "clap", - "crossbeam-channel 0.5.4", + "crossbeam-channel 0.5.5", "derive_more", "faccess", "ffprobe", @@ -1010,7 +1009,7 @@ dependencies = [ "shlex", "simplelog", "sqlx", - "time 0.3.9", + "time 0.3.10", "walkdir", "zeromq", ] @@ -1278,13 +1277,13 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9be70c98951c83b8d2f8f60d7065fa6d5146873094452a1008da8c2f1e4205ad" +checksum = "4eb1a864a501629691edf6c15a593b7a51eebaa1e8468e9ddc623de7c9b58ec6" dependencies = [ "cfg-if 1.0.0", "libc", - "wasi 0.10.0+wasi-snapshot-preview1", + "wasi 0.11.0+wasi-snapshot-preview1", ] [[package]] @@ -1340,13 +1339,19 @@ dependencies = [ "ahash 0.7.6", ] +[[package]] +name = "hashbrown" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db0d4cf898abf0081f964436dc980e96670a0f36863e4b83aaacdb65c9d7ccc3" + [[package]] name = "hashlink" version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7249a3129cbc1ffccd74857f81464a323a152173cdb134e0fd81bc803b29facf" dependencies = [ - "hashbrown", + "hashbrown 0.11.2", ] [[package]] @@ -1468,12 +1473,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "1.8.2" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6012d540c5baa3589337a98ce73408de9b5a25ec9fc2c6fd6be8f0d39e0ca5a" +checksum = "6c6392766afd7964e2531940894cffe4bd8d7d17dbc3c1c4857040fd4b33bdb3" dependencies = [ "autocfg", - "hashbrown", + "hashbrown 0.12.1", ] [[package]] @@ -1546,9 +1551,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.57" +version = "0.3.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "671a26f820db17c2a2750743f1dd03bafd15b98c9f30c7c2628c024c05d73397" +checksum = "c3fac17f7123a73ca62df411b1bf727ccc805daa070338fda671c86dac1bdc27" dependencies = [ "wasm-bindgen", ] @@ -1604,9 +1609,9 @@ dependencies = [ [[package]] name = "jsonwebtoken" -version = "8.1.0" +version = "8.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc9051c17f81bae79440afa041b3a278e1de71bfb96d32454b477fd4703ccb6f" +checksum = "1aa4b4af834c6cfd35d8763d359661b90f2e45d8f750a0849156c7f4671af09c" dependencies = [ "base64", "pem", @@ -1811,9 +1816,9 @@ dependencies = [ [[package]] name = "mio" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "713d550d9b44d89174e066b7a6217ae06234c10cb47819a88290d2b353c31799" +checksum = "57ee1c23c7c63b0c9250c339ffdc69255f110b298b901b9f6c82547b7b87caaf" dependencies = [ "libc", "log", @@ -2199,18 +2204,18 @@ checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5" [[package]] name = "proc-macro2" -version = "1.0.39" +version = "1.0.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c54b25569025b7fc9651de43004ae593a75ad88543b17178aa5e1b9c4f15f56f" +checksum = "dd96a1e8ed2596c337f8eae5f24924ec83f5ad5ab21ea8e455d3566c69fbcaf7" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.18" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1feb54ed693b93a84e14094943b84b7c4eae204c512b7ccb95ab0c66d278ad1" +checksum = "3bcdf212e9776fbcb2d23ab029360416bb1706b1aea2d1a5ba002727cbcab804" dependencies = [ "proc-macro2", ] @@ -2280,7 +2285,7 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" dependencies = [ - "getrandom 0.2.6", + "getrandom 0.2.7", ] [[package]] @@ -2329,9 +2334,9 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.11.10" +version = "0.11.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46a1f7aa4f35e5e8b4160449f51afc758f0ce6454315a9fa7d0d113e958c41eb" +checksum = "b75aa69a3f06bbcc66ede33af2af253c6f7a86b1ca0033f60c580a27074fbf92" dependencies = [ "base64", "bytes", @@ -2356,6 +2361,7 @@ dependencies = [ "serde_urlencoded", "tokio", "tokio-native-tls", + "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", @@ -2548,7 +2554,7 @@ dependencies = [ "num-bigint", "num-traits", "thiserror", - "time 0.3.9", + "time 0.3.10", ] [[package]] @@ -2560,7 +2566,7 @@ dependencies = [ "log", "paris", "termcolor", - "time 0.3.9", + "time 0.3.10", ] [[package]] @@ -2720,9 +2726,9 @@ checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" [[package]] name = "syn" -version = "1.0.96" +version = "1.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0748dd251e24453cb8717f0354206b91557e4ec8703673a4b30208f2abaf1ebf" +checksum = "c50aef8a904de4c23c788f104b7dddc7d6f79c647c7c8ce4cc8f73eb0ca773dd" dependencies = [ "proc-macro2", "quote", @@ -2791,9 +2797,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.9" +version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2702e08a7a860f005826c6815dcac101b19b5eb330c27fe4a5928fec1d20ddd" +checksum = "82501a4c1c0330d640a6e176a3d6a204f5ec5237aca029029d21864a902e27b0" dependencies = [ "itoa", "libc", @@ -2840,7 +2846,7 @@ dependencies = [ "bytes", "libc", "memchr", - "mio 0.8.3", + "mio 0.8.4", "num_cpus", "once_cell", "parking_lot 0.12.1", @@ -2901,9 +2907,9 @@ dependencies = [ [[package]] name = "tower-service" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "360dfd1d6d30e05fda32ace2c8c70e9c0a9da713275777f5a4dbb8a1893930c6" +checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" [[package]] name = "tracing" @@ -2955,9 +2961,9 @@ checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992" [[package]] name = "unicode-ident" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d22af068fba1eb5edcb4aea19d382b2a3deb4c8f9d475c589b6ada9e0fd493ee" +checksum = "5bd2fe26506023ed7b5e1e315add59d6f584c621d037f9368fea9cfb988f368c" [[package]] name = "unicode-normalization" @@ -3004,7 +3010,7 @@ version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" dependencies = [ - "getrandom 0.2.6", + "getrandom 0.2.7", ] [[package]] @@ -3076,9 +3082,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.80" +version = "0.2.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27370197c907c55e3f1a9fbe26f44e937fe6451368324e009cba39e139dc08ad" +checksum = "7c53b543413a17a202f4be280a7e5c62a1c69345f5de525ee64f8cfdbc954994" dependencies = [ "cfg-if 1.0.0", "wasm-bindgen-macro", @@ -3086,9 +3092,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.80" +version = "0.2.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53e04185bfa3a779273da532f5025e33398409573f348985af9a1cbf3774d3f4" +checksum = "5491a68ab4500fa6b4d726bd67408630c3dbe9c4fe7bda16d5c82a1fd8c7340a" dependencies = [ "bumpalo", "lazy_static", @@ -3101,9 +3107,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.30" +version = "0.4.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f741de44b75e14c35df886aff5f1eb73aa114fa5d4d00dcd37b5e01259bf3b2" +checksum = "de9a9cec1733468a8c657e57fa2413d2ae2c0129b95e87c5b72b8ace4d13f31f" dependencies = [ "cfg-if 1.0.0", "js-sys", @@ -3113,9 +3119,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.80" +version = "0.2.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17cae7ff784d7e83a2fe7611cfe766ecf034111b49deb850a3dc7699c08251f5" +checksum = "c441e177922bc58f1e12c022624b6216378e5febc2f0533e41ba443d505b80aa" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -3123,9 +3129,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.80" +version = "0.2.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99ec0dc7a4756fffc231aab1b9f2f578d23cd391390ab27f952ae0c9b3ece20b" +checksum = "7d94ac45fcf608c1f45ef53e748d35660f168490c10b23704c7779ab8f5c3048" dependencies = [ "proc-macro2", "quote", @@ -3136,15 +3142,15 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.80" +version = "0.2.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d554b7f530dee5964d9a9468d95c1f8b8acae4f282807e7d27d4b03099a46744" +checksum = "6a89911bd99e5f3659ec4acf9c4d93b0a90fe4a2a11f15328472058edc5261be" [[package]] name = "web-sys" -version = "0.3.57" +version = "0.3.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b17e741662c70c8bd24ac5c5b18de314a2c26c32bf8346ee1e6f53de919c283" +checksum = "2fed94beee57daf8dd7d51f2b15dc2bcde92d7a72304cdf662a4371008b71b90" dependencies = [ "js-sys", "wasm-bindgen", diff --git a/Cargo.toml b/Cargo.toml index e75d9c1c..b1ccf8e5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,7 +14,7 @@ actix-web-grants = "3" actix-web-httpauth = "0.6" argon2 = "0.4" chrono = { git = "https://github.com/sbrocket/chrono", branch = "parse-error-kind-public" } -clap = { version = "3.1", features = ["derive"] } +clap = { version = "3.2", features = ["derive"] } crossbeam-channel = "0.5" derive_more = "0.99" faccess = "0.2" @@ -84,7 +84,8 @@ assets = [ ["assets/logo.png", "/usr/share/ffplayout/logo.png", "644"], ["README.md", "/usr/share/doc/ffplayout-engine/README", "644"], ] -systemd-units = { unit-name = "ffplayout-engine", unit-scripts = "assets", enable = false } +maintainer-scripts = "debian/" +systemd-units = { enable = false, unit-scripts = "assets" } # REHL RPM PACKAGE [package.metadata.generate-rpm] diff --git a/debian/.gitkeep b/debian/.gitkeep new file mode 100644 index 00000000..e69de29b From 96d781bf59ad90e84b288b583d120a52fb26f0bc Mon Sep 17 00:00:00 2001 From: jb-alvarado Date: Mon, 20 Jun 2022 18:11:27 +0200 Subject: [PATCH 34/47] save, delete playlist, work on file browser --- src/api/errors.rs | 4 ++ src/api/files.rs | 59 +++++++++++++++++++++++++ src/api/mod.rs | 1 + src/api/playlist.rs | 83 +++++++++++++++++++++++++++++++++--- src/api/routes.rs | 75 ++++++++++++++++++++++++++------ src/bin/ffpapi.rs | 13 +++--- src/filter/mod.rs | 2 +- src/input/playlist.rs | 6 +-- src/utils/generator.rs | 1 + src/utils/json_serializer.rs | 10 +++++ src/utils/mod.rs | 19 +++++++-- 11 files changed, 241 insertions(+), 32 deletions(-) create mode 100644 src/api/files.rs diff --git a/src/api/errors.rs b/src/api/errors.rs index a515c47a..8c31bbe0 100644 --- a/src/api/errors.rs +++ b/src/api/errors.rs @@ -9,6 +9,9 @@ pub enum ServiceError { #[display(fmt = "BadRequest: {}", _0)] BadRequest(String), + #[display(fmt = "Conflict: {}", _0)] + Conflict(String), + #[display(fmt = "Unauthorized")] Unauthorized, } @@ -21,6 +24,7 @@ impl ResponseError for ServiceError { HttpResponse::InternalServerError().json("Internal Server Error. Please try later.") } ServiceError::BadRequest(ref message) => HttpResponse::BadRequest().json(message), + ServiceError::Conflict(ref message) => HttpResponse::Conflict().json(message), ServiceError::Unauthorized => HttpResponse::Unauthorized().json("No Permission!"), } } diff --git a/src/api/files.rs b/src/api/files.rs new file mode 100644 index 00000000..9aa4f876 --- /dev/null +++ b/src/api/files.rs @@ -0,0 +1,59 @@ +use log::error; +use serde::{Deserialize, Serialize}; +use std::{ + fs::{self, canonicalize}, + path::{self, PathBuf}, +}; + +use simplelog::*; + +use crate::api::{errors::ServiceError, utils::playout_config}; + +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct PathObject { + root: String, + folders: Vec, + files: Vec, +} + +impl PathObject { + fn new(root: String) -> Self { + Self { + root, + folders: vec![], + files: vec![], + } + } +} + +pub async fn browser(id: i64, path_obj: &PathObject) -> Result { + let config = playout_config(&id).await?; + let path = PathBuf::from(config.storage.path); + let absolute = match canonicalize(path_obj.root.clone()) { + Ok(p) => p, + Err(e) => { + error!("{e}"); + return Err(ServiceError::InternalServerError); + } + }; + let path = path.join(absolute.clone()); + let obj = PathObject::new(path_obj.root.clone()); + + println!("absolute: {:?}", absolute); + + let paths = fs::read_dir(path).unwrap(); + + for path in paths { + println!("Name: {:?}", path); + // if let Ok(p) = path { + // let file_path = p.path().to_owned(); + // if file_path.is_dir() { + // folders.push(file_path.display()) + // } else if file_path.is_file() { + // files.push(file_path.clone().display()) + // } + // } + } + + Ok(obj) +} diff --git a/src/api/mod.rs b/src/api/mod.rs index acc618e1..78826bad 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -2,6 +2,7 @@ pub mod args_parse; pub mod auth; pub mod control; pub mod errors; +pub mod files; pub mod handles; pub mod models; pub mod playlist; diff --git a/src/api/playlist.rs b/src/api/playlist.rs index b2bdbdec..ba8142aa 100644 --- a/src/api/playlist.rs +++ b/src/api/playlist.rs @@ -1,8 +1,32 @@ -use std::{fs::File, path::PathBuf}; +use std::{ + fs::{self, File}, + io::Error, + path::PathBuf, +}; + +use simplelog::*; use crate::api::{errors::ServiceError, utils::playout_config}; use crate::utils::JsonPlaylist; +fn json_reader(path: &PathBuf) -> Result { + let f = File::options().read(true).write(false).open(&path)?; + let p = serde_json::from_reader(f)?; + + Ok(p) +} + +fn json_writer(path: &PathBuf, data: JsonPlaylist) -> Result<(), Error> { + let f = File::options() + .write(true) + .truncate(true) + .create(true) + .open(&path)?; + serde_json::to_writer_pretty(f, &data)?; + + Ok(()) +} + pub async fn read_playlist(id: i64, date: String) -> Result { let config = playout_config(&id).await?; let mut playlist_path = PathBuf::from(&config.playlist.path); @@ -13,11 +37,60 @@ pub async fn read_playlist(id: i64, date: String) -> Result Result { + let config = playout_config(&id).await?; + let date = json_data.date.clone(); + let mut playlist_path = PathBuf::from(&config.playlist.path); + let d: Vec<&str> = date.split('-').collect(); + playlist_path = playlist_path + .join(d[0]) + .join(d[1]) + .join(date.clone()) + .with_extension("json"); + + if playlist_path.is_file() { + if let Ok(existing_data) = json_reader(&playlist_path) { + if json_data == existing_data { + return Err(ServiceError::Conflict(format!( + "Playlist from {date}, already exists!" + ))); + } + } + } + + match json_writer(&playlist_path, json_data) { + Ok(_) => return Ok(format!("Write playlist from {date} success!")), + Err(e) => { + error!("{e}"); + } + } + + Err(ServiceError::InternalServerError) +} + +pub async fn delete_playlist(id: i64, date: &str) -> Result<(), ServiceError> { + let config = playout_config(&id).await?; + let mut playlist_path = PathBuf::from(&config.playlist.path); + let d: Vec<&str> = date.split('-').collect(); + playlist_path = playlist_path + .join(d[0]) + .join(d[1]) + .join(date) + .with_extension("json"); + + if playlist_path.is_file() { + if let Err(e) = fs::remove_file(playlist_path) { + error!("{e}"); + return Err(ServiceError::InternalServerError); + }; + } + + Ok(()) +} diff --git a/src/api/routes.rs b/src/api/routes.rs index fcaf0433..6d152c7b 100644 --- a/src/api/routes.rs +++ b/src/api/routes.rs @@ -1,6 +1,6 @@ use std::collections::HashMap; -use actix_web::{get, http::StatusCode, patch, post, put, web, Responder}; +use actix_web::{delete, get, http::StatusCode, patch, post, put, web, Responder}; use actix_web_grants::{permissions::AuthDetails, proc_macro::has_any_role}; use argon2::{ password_hash::{rand_core::OsRng, PasswordHash, SaltString}, @@ -9,21 +9,23 @@ use argon2::{ use serde::Serialize; use simplelog::*; -use crate::api::{ - auth::{create_jwt, Claims}, - control::{control_state, media_info, send_message}, - errors::ServiceError, - handles::{ - db_add_preset, db_add_user, db_get_presets, db_get_settings, db_login, db_role, - db_update_preset, db_update_settings, db_update_user, +use crate::{ + api::{ + auth::{create_jwt, Claims}, + control::{control_state, media_info, send_message}, + errors::ServiceError, + files::{browser, PathObject}, + handles::{ + db_add_preset, db_add_user, db_get_presets, db_get_settings, db_login, db_role, + db_update_preset, db_update_settings, db_update_user, + }, + models::{LoginUser, Settings, TextPreset, User}, + playlist::{delete_playlist, read_playlist, write_playlist}, + utils::{read_playout_config, Role}, }, - models::{LoginUser, Settings, TextPreset, User}, - playlist::read_playlist, - utils::{read_playout_config, Role}, + utils::{JsonPlaylist, PlayoutConfig}, }; -use crate::utils::PlayoutConfig; - #[derive(Serialize)] struct ResponseObj { message: String, @@ -368,3 +370,50 @@ pub async fn get_playlist( Err(e) => Err(e), } } + +/// curl -X POST http://localhost:8080/api/playlist/1/ +/// --header 'Content-Type: application/json' --header 'Authorization: ' +/// -- data "{}" +#[post("/playlist/{id}/")] +#[has_any_role("Role::Admin", "Role::User", type = "Role")] +pub async fn save_playlist( + id: web::Path, + data: web::Json, +) -> Result { + match write_playlist(*id, data.into_inner()).await { + Ok(res) => Ok(res), + Err(e) => Err(e), + } +} + +/// curl -X DELETE http://localhost:8080/api/playlist/1/2022-06-20 +/// --header 'Content-Type: application/json' --header 'Authorization: ' +#[delete("/playlist/{id}/{date}")] +#[has_any_role("Role::Admin", "Role::User", type = "Role")] +pub async fn del_playlist( + params: web::Path<(i64, String)>, +) -> Result { + match delete_playlist(params.0, ¶ms.1).await { + Ok(_) => Ok(format!("Delete playlist from {} success!", params.1)), + Err(e) => Err(e), + } +} + +/// ---------------------------------------------------------------------------- +/// file operations +/// +/// ---------------------------------------------------------------------------- + +/// curl -X get http://localhost:8080/api/file/1/browse +/// --header 'Content-Type: application/json' --header 'Authorization: ' +#[post("/file/{id}/browse/")] +#[has_any_role("Role::Admin", "Role::User", type = "Role")] +pub async fn file_browser( + id: web::Path, + data: web::Json, +) -> Result { + match browser(*id, &data.into_inner()).await { + Ok(obj) => return Ok(web::Json(obj)), + Err(e) => Err(e), + } +} diff --git a/src/bin/ffpapi.rs b/src/bin/ffpapi.rs index 216dfe56..5309c2e3 100644 --- a/src/bin/ffpapi.rs +++ b/src/bin/ffpapi.rs @@ -14,10 +14,10 @@ use ffplayout_engine::{ auth, models::LoginUser, routes::{ - add_preset, add_user, get_playlist, get_playout_config, get_presets, get_settings, - jump_to_last, jump_to_next, login, media_current, media_last, media_next, - patch_settings, reset_playout, send_text_message, update_playout_config, update_preset, - update_user, + add_preset, add_user, del_playlist, file_browser, get_playlist, get_playout_config, + get_presets, get_settings, jump_to_last, jump_to_next, login, media_current, + media_last, media_next, patch_settings, reset_playout, save_playlist, + send_text_message, update_playout_config, update_preset, update_user, }, utils::{db_path, init_config, run_args, Role}, }, @@ -90,7 +90,10 @@ async fn main() -> std::io::Result<()> { .service(media_current) .service(media_next) .service(media_last) - .service(get_playlist), + .service(get_playlist) + .service(save_playlist) + .service(del_playlist) + .service(file_browser), ) }) .bind((addr, port))? diff --git a/src/filter/mod.rs b/src/filter/mod.rs index 12d9ec9a..c2878839 100644 --- a/src/filter/mod.rs +++ b/src/filter/mod.rs @@ -140,7 +140,7 @@ fn fade(node: &mut Media, chain: &mut Filters, codec_type: &str) { fn overlay(node: &mut Media, chain: &mut Filters, config: &PlayoutConfig) { if config.processing.add_logo && Path::new(&config.processing.logo).is_file() - && &node.category.clone().unwrap_or_default() != "advertisement" + && &node.category != "advertisement" { let mut logo_chain = v_overlay::filter_node(config, false); diff --git a/src/input/playlist.rs b/src/input/playlist.rs index 47513422..e6384822 100644 --- a/src/input/playlist.rs +++ b/src/input/playlist.rs @@ -195,15 +195,13 @@ impl CurrentProgram { let index = self.index.load(Ordering::SeqCst); let current_list = self.nodes.lock().unwrap(); - if index + 1 < current_list.len() - && ¤t_list[index + 1].category.clone().unwrap_or_default() == "advertisement" - { + if index + 1 < current_list.len() && ¤t_list[index + 1].category == "advertisement" { self.current_node.next_ad = Some(true); } if index > 0 && index < current_list.len() - && ¤t_list[index - 1].category.clone().unwrap_or_default() == "advertisement" + && ¤t_list[index - 1].category == "advertisement" { self.current_node.last_ad = Some(true); } diff --git a/src/utils/generator.rs b/src/utils/generator.rs index 1ec9d168..f7f724a6 100644 --- a/src/utils/generator.rs +++ b/src/utils/generator.rs @@ -104,6 +104,7 @@ pub fn generate_playlist(config: &PlayoutConfig, mut date_range: Vec) { let mut round = 0; let mut playlist = JsonPlaylist { + channel: "Channel 1".into(), date, current_file: None, start_sec: None, diff --git a/src/utils/json_serializer.rs b/src/utils/json_serializer.rs index 2c669cb1..6eeca4a9 100644 --- a/src/utils/json_serializer.rs +++ b/src/utils/json_serializer.rs @@ -17,6 +17,7 @@ pub const DUMMY_LEN: f64 = 60.0; /// This is our main playlist object, it holds all necessary information for the current day. #[derive(Debug, Serialize, Deserialize, Clone)] pub struct JsonPlaylist { + pub channel: String, pub date: String, #[serde(skip_serializing, skip_deserializing)] @@ -38,6 +39,7 @@ impl JsonPlaylist { media.duration = DUMMY_LEN; media.out = DUMMY_LEN; Self { + channel: "Channel 1".into(), date, start_sec: Some(start), current_file: None, @@ -47,6 +49,14 @@ impl JsonPlaylist { } } +impl PartialEq for JsonPlaylist { + fn eq(&self, other: &Self) -> bool { + self.channel == other.channel && self.date == other.date && self.program == other.program + } +} + +impl Eq for JsonPlaylist {} + fn set_defaults( mut playlist: JsonPlaylist, current_file: String, diff --git a/src/utils/mod.rs b/src/utils/mod.rs index c71b5526..e1d11302 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -48,8 +48,7 @@ pub struct Media { pub out: f64, pub duration: f64, - #[serde(skip_serializing)] - pub category: Option, + pub category: String, pub source: String, #[serde(skip_serializing, skip_deserializing)] @@ -94,7 +93,7 @@ impl Media { seek: 0.0, out: duration, duration, - category: None, + category: String::new(), source: src.clone(), cmd: Some(vec!["-i".to_string(), src]), filter: Some(vec![]), @@ -131,8 +130,20 @@ impl Media { } } +impl PartialEq for Media { + fn eq(&self, other: &Self) -> bool { + self.seek == other.seek + && self.out == other.out + && self.duration == other.duration + && self.source == other.source + && self.category == other.category + } +} + +impl Eq for Media {} + /// We use the ffprobe crate, but we map the metadata to our needs. -#[derive(Debug, Serialize, Deserialize, Clone)] +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] pub struct MediaProbe { pub format: Option, pub audio_streams: Option>, From 98bcf34ebba89187f5d1a51552aff92a11addb0f Mon Sep 17 00:00:00 2001 From: jb-alvarado Date: Mon, 20 Jun 2022 22:19:41 +0200 Subject: [PATCH 35/47] simple file browser --- Cargo.lock | 7 ++++++ Cargo.toml | 1 + src/api/files.rs | 56 +++++++++++++++++++++++++++------------------ src/api/routes.rs | 2 +- src/input/folder.rs | 7 +----- src/utils/mod.rs | 6 +++++ 6 files changed, 50 insertions(+), 29 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2782da06..cd9a6dce 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1002,6 +1002,7 @@ dependencies = [ "rand 0.8.5", "rand_core 0.6.3", "regex", + "relative-path", "reqwest", "serde", "serde_json", @@ -2323,6 +2324,12 @@ version = "0.6.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49b3de9ec5dc0a3417da371aab17d729997c15010e7fd24ff707773a33bddb64" +[[package]] +name = "relative-path" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4e112eddc95bbf25365df3b5414354ad2fe7ee465eddb9965a515faf8c3b6d9" + [[package]] name = "remove_dir_all" version = "0.5.3" diff --git a/Cargo.toml b/Cargo.toml index b1ccf8e5..60137583 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,6 +29,7 @@ notify = "4.0" once_cell = "1.10" rand = "0.8" rand_core = { version = "0.6", features = ["std"] } +relative-path = "1.6" regex = "1" reqwest = { version = "0.11", features = ["blocking", "json"] } serde = { version = "1.0", features = ["derive"] } diff --git a/src/api/files.rs b/src/api/files.rs index 9aa4f876..578af9aa 100644 --- a/src/api/files.rs +++ b/src/api/files.rs @@ -1,18 +1,18 @@ -use log::error; +use relative_path::RelativePath; use serde::{Deserialize, Serialize}; -use std::{ - fs::{self, canonicalize}, - path::{self, PathBuf}, -}; +use std::{fs, path::PathBuf}; use simplelog::*; use crate::api::{errors::ServiceError, utils::playout_config}; +use crate::utils::file_extension; #[derive(Debug, Deserialize, Serialize, Clone)] pub struct PathObject { root: String, + #[serde(skip_deserializing)] folders: Vec, + #[serde(skip_deserializing)] files: Vec, } @@ -29,30 +29,42 @@ impl PathObject { pub async fn browser(id: i64, path_obj: &PathObject) -> Result { let config = playout_config(&id).await?; let path = PathBuf::from(config.storage.path); - let absolute = match canonicalize(path_obj.root.clone()) { - Ok(p) => p, + let extensions = config.storage.extensions; + let path_component = RelativePath::new(&path_obj.root) + .normalize() + .to_string() + .replace("../", ""); + let path = path.join(path_component.clone()); + let mut obj = PathObject::new(path_component.clone()); + + let mut paths: Vec<_> = match fs::read_dir(path) { + Ok(p) => p.filter_map(|r| r.ok()).collect(), Err(e) => { - error!("{e}"); + error!("{e} in {path_component}"); return Err(ServiceError::InternalServerError); } }; - let path = path.join(absolute.clone()); - let obj = PathObject::new(path_obj.root.clone()); - println!("absolute: {:?}", absolute); - - let paths = fs::read_dir(path).unwrap(); + paths.sort_by_key(|dir| dir.path()); for path in paths { - println!("Name: {:?}", path); - // if let Ok(p) = path { - // let file_path = p.path().to_owned(); - // if file_path.is_dir() { - // folders.push(file_path.display()) - // } else if file_path.is_file() { - // files.push(file_path.clone().display()) - // } - // } + let file_path = path.path().to_owned(); + let path_str = file_path.display().to_string(); + + // ignore hidden files/folders on unix + if path_str.contains("/.") { + continue; + } + + if file_path.is_dir() { + obj.folders.push(path_str); + } else if file_path.is_file() { + if let Some(ext) = file_extension(&file_path) { + if extensions.contains(&ext.to_string().to_lowercase()) { + obj.files.push(path_str); + } + } + } } Ok(obj) diff --git a/src/api/routes.rs b/src/api/routes.rs index 6d152c7b..78dbaabf 100644 --- a/src/api/routes.rs +++ b/src/api/routes.rs @@ -413,7 +413,7 @@ pub async fn file_browser( data: web::Json, ) -> Result { match browser(*id, &data.into_inner()).await { - Ok(obj) => return Ok(web::Json(obj)), + Ok(obj) => Ok(web::Json(obj)), Err(e) => Err(e), } } diff --git a/src/input/folder.rs b/src/input/folder.rs index 7dbd68d9..a1b10ce6 100644 --- a/src/input/folder.rs +++ b/src/input/folder.rs @@ -1,5 +1,4 @@ use std::{ - ffi::OsStr, path::Path, process::exit, sync::{ @@ -19,7 +18,7 @@ use rand::{seq::SliceRandom, thread_rng}; use simplelog::*; use walkdir::WalkDir; -use crate::utils::{get_sec, Media, PlayoutConfig}; +use crate::utils::{file_extension, get_sec, Media, PlayoutConfig}; /// Folder Sources /// @@ -155,10 +154,6 @@ impl Iterator for FolderSource { } } -fn file_extension(filename: &Path) -> Option<&str> { - filename.extension().and_then(OsStr::to_str) -} - /// Create a watcher, which monitor file changes. /// When a change is register, update the current file list. /// This makes it possible, to play infinitely and and always new files to it. diff --git a/src/utils/mod.rs b/src/utils/mod.rs index e1d11302..38ed54e4 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -1,4 +1,5 @@ use std::{ + ffi::OsStr, fs::{self, metadata}, io::{BufRead, BufReader, Error}, net::TcpListener, @@ -322,6 +323,11 @@ pub fn sec_to_time(sec: f64) -> String { date_time.format("%H:%M:%S%.3f").to_string() } +/// get file extension +pub fn file_extension(filename: &Path) -> Option<&str> { + filename.extension().and_then(OsStr::to_str) +} + /// Test if given numbers are close to each other, /// with a third number for setting the maximum range. pub fn is_close(a: f64, b: f64, to: f64) -> bool { From 4080fa73966bfa3429a2f0be98c4b0acb36327f3 Mon Sep 17 00:00:00 2001 From: jb-alvarado Date: Tue, 21 Jun 2022 17:56:10 +0200 Subject: [PATCH 36/47] generate playlist --- src/api/control.rs | 2 +- src/api/files.rs | 2 +- src/api/playlist.rs | 28 +++++++++++++++++++---- src/api/routes.rs | 15 ++++++++++++- src/api/utils.rs | 8 +++---- src/bin/ffpapi.rs | 7 +++--- src/main.rs | 5 ++++- src/utils/generator.rs | 50 ++++++++++++++++++++++++++---------------- 8 files changed, 83 insertions(+), 34 deletions(-) diff --git a/src/api/control.rs b/src/api/control.rs index 4f8b8ad8..42a9ba10 100644 --- a/src/api/control.rs +++ b/src/api/control.rs @@ -59,7 +59,7 @@ async fn post_request(id: i64, obj: RpcObj) -> Result Result { - let config = playout_config(&id).await?; + let (config, _) = playout_config(&id).await?; let path = PathBuf::from(config.storage.path); let extensions = config.storage.extensions; let path_component = RelativePath::new(&path_obj.root) diff --git a/src/api/playlist.rs b/src/api/playlist.rs index ba8142aa..6ee5c11b 100644 --- a/src/api/playlist.rs +++ b/src/api/playlist.rs @@ -7,7 +7,7 @@ use std::{ use simplelog::*; use crate::api::{errors::ServiceError, utils::playout_config}; -use crate::utils::JsonPlaylist; +use crate::utils::{generate_playlist as playlist_generator, JsonPlaylist}; fn json_reader(path: &PathBuf) -> Result { let f = File::options().read(true).write(false).open(&path)?; @@ -28,7 +28,7 @@ fn json_writer(path: &PathBuf, data: JsonPlaylist) -> Result<(), Error> { } pub async fn read_playlist(id: i64, date: String) -> Result { - let config = playout_config(&id).await?; + let (config, _) = playout_config(&id).await?; let mut playlist_path = PathBuf::from(&config.playlist.path); let d: Vec<&str> = date.split('-').collect(); playlist_path = playlist_path @@ -45,7 +45,7 @@ pub async fn read_playlist(id: i64, date: String) -> Result Result { - let config = playout_config(&id).await?; + let (config, _) = playout_config(&id).await?; let date = json_data.date.clone(); let mut playlist_path = PathBuf::from(&config.playlist.path); let d: Vec<&str> = date.split('-').collect(); @@ -75,8 +75,28 @@ pub async fn write_playlist(id: i64, json_data: JsonPlaylist) -> Result Result { + let (config, settings) = playout_config(&id).await?; + + match playlist_generator(&config, vec![date], Some(settings.channel_name)) { + Ok(playlists) => { + if !playlists.is_empty() { + Ok(playlists[0].clone()) + } else { + Err(ServiceError::Conflict( + "Playlist could not be written, possible already exists!".into(), + )) + } + } + Err(e) => { + error!("{e}"); + Err(ServiceError::InternalServerError) + } + } +} + pub async fn delete_playlist(id: i64, date: &str) -> Result<(), ServiceError> { - let config = playout_config(&id).await?; + let (config, _) = playout_config(&id).await?; let mut playlist_path = PathBuf::from(&config.playlist.path); let d: Vec<&str> = date.split('-').collect(); playlist_path = playlist_path diff --git a/src/api/routes.rs b/src/api/routes.rs index 78dbaabf..60622b64 100644 --- a/src/api/routes.rs +++ b/src/api/routes.rs @@ -20,7 +20,7 @@ use crate::{ db_update_preset, db_update_settings, db_update_user, }, models::{LoginUser, Settings, TextPreset, User}, - playlist::{delete_playlist, read_playlist, write_playlist}, + playlist::{delete_playlist, generate_playlist, read_playlist, write_playlist}, utils::{read_playout_config, Role}, }, utils::{JsonPlaylist, PlayoutConfig}, @@ -386,6 +386,19 @@ pub async fn save_playlist( } } +/// curl -X GET http://localhost:8080/api/playlist/1/generate/2022-06-20 +/// --header 'Content-Type: application/json' --header 'Authorization: ' +#[get("/playlist/{id}/generate/{date}")] +#[has_any_role("Role::Admin", "Role::User", type = "Role")] +pub async fn gen_playlist( + params: web::Path<(i64, String)>, +) -> Result { + match generate_playlist(params.0, params.1.clone()).await { + Ok(playlist) => Ok(web::Json(playlist)), + Err(e) => Err(e), + } +} + /// curl -X DELETE http://localhost:8080/api/playlist/1/2022-06-20 /// --header 'Content-Type: application/json' --header 'Authorization: ' #[delete("/playlist/{id}/{date}")] diff --git a/src/api/utils.rs b/src/api/utils.rs index 231a157b..aedc1ee4 100644 --- a/src/api/utils.rs +++ b/src/api/utils.rs @@ -8,7 +8,7 @@ use crate::api::{ args_parse::Args, errors::ServiceError, handles::{db_add_user, db_get_settings, db_global, db_init}, - models::User, + models::{Settings, User}, }; use crate::utils::PlayoutConfig; @@ -122,10 +122,10 @@ pub fn read_playout_config(path: &str) -> Result> Ok(config) } -pub async fn playout_config(channel_id: &i64) -> Result { +pub async fn playout_config(channel_id: &i64) -> Result<(PlayoutConfig, Settings), ServiceError> { if let Ok(settings) = db_get_settings(channel_id).await { - if let Ok(config) = read_playout_config(&settings.config_path) { - return Ok(config); + if let Ok(config) = read_playout_config(&settings.config_path.clone()) { + return Ok((config, settings)); } } diff --git a/src/bin/ffpapi.rs b/src/bin/ffpapi.rs index 5309c2e3..fd1c0db4 100644 --- a/src/bin/ffpapi.rs +++ b/src/bin/ffpapi.rs @@ -14,9 +14,9 @@ use ffplayout_engine::{ auth, models::LoginUser, routes::{ - add_preset, add_user, del_playlist, file_browser, get_playlist, get_playout_config, - get_presets, get_settings, jump_to_last, jump_to_next, login, media_current, - media_last, media_next, patch_settings, reset_playout, save_playlist, + add_preset, add_user, del_playlist, file_browser, gen_playlist, get_playlist, + get_playout_config, get_presets, get_settings, jump_to_last, jump_to_next, login, + media_current, media_last, media_next, patch_settings, reset_playout, save_playlist, send_text_message, update_playout_config, update_preset, update_user, }, utils::{db_path, init_config, run_args, Role}, @@ -92,6 +92,7 @@ async fn main() -> std::io::Result<()> { .service(media_last) .service(get_playlist) .service(save_playlist) + .service(gen_playlist) .service(del_playlist) .service(file_browser), ) diff --git a/src/main.rs b/src/main.rs index 3d48d8b4..1d7394f2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -77,7 +77,10 @@ fn main() { if let Some(range) = config.general.generate.clone() { // run a simple playlist generator and save them to disk - generate_playlist(&config, range); + if let Err(e) = generate_playlist(&config, range, None) { + error!("{e}"); + exit(1); + }; exit(0); } diff --git a/src/utils/generator.rs b/src/utils/generator.rs index f7f724a6..643decc7 100644 --- a/src/utils/generator.rs +++ b/src/utils/generator.rs @@ -8,6 +8,7 @@ /// Beside that it is really very basic, without any logic. use std::{ fs::{create_dir_all, write}, + io::Error, path::Path, process::exit, sync::{atomic::AtomicUsize, Arc, Mutex}, @@ -17,7 +18,7 @@ use chrono::{Duration, NaiveDate}; use simplelog::*; use crate::input::FolderSource; -use crate::utils::{json_serializer::JsonPlaylist, Media, PlayoutConfig}; +use crate::utils::{json_serializer::JsonPlaylist, time_to_sec, Media, PlayoutConfig}; /// Generate a vector with dates, from given range. fn get_date_range(date_range: &[String]) -> Vec { @@ -50,11 +51,30 @@ fn get_date_range(date_range: &[String]) -> Vec { } /// Generate playlists -pub fn generate_playlist(config: &PlayoutConfig, mut date_range: Vec) { - let total_length = config.playlist.length_sec.unwrap(); +pub fn generate_playlist( + config: &PlayoutConfig, + mut date_range: Vec, + channel_name: Option, +) -> Result, Error> { + let total_length = match config.playlist.length_sec { + Some(length) => length, + None => { + if config.playlist.length.contains(':') { + time_to_sec(&config.playlist.length) + } else { + 86400.0 + } + } + }; let current_list = Arc::new(Mutex::new(vec![Media::new(0, "".to_string(), false)])); let index = Arc::new(AtomicUsize::new(0)); let playlist_root = Path::new(&config.playlist.path); + let mut playlists = vec![]; + + let channel = match channel_name { + Some(name) => name, + None => "Channel 1".to_string(), + }; if !playlist_root.is_dir() { error!( @@ -79,10 +99,7 @@ pub fn generate_playlist(config: &PlayoutConfig, mut date_range: Vec) { let playlist_path = playlist_root.join(year).join(month); let playlist_file = &playlist_path.join(format!("{date}.json")); - if let Err(e) = create_dir_all(playlist_path) { - error!("Create folder failed: {e:?}"); - exit(1); - } + create_dir_all(playlist_path)?; if playlist_file.is_file() { warn!( @@ -104,7 +121,7 @@ pub fn generate_playlist(config: &PlayoutConfig, mut date_range: Vec) { let mut round = 0; let mut playlist = JsonPlaylist { - channel: "Channel 1".into(), + channel: channel.clone(), date, current_file: None, start_sec: None, @@ -131,17 +148,12 @@ pub fn generate_playlist(config: &PlayoutConfig, mut date_range: Vec) { } } - let json: String = match serde_json::to_string_pretty(&playlist) { - Ok(j) => j, - Err(e) => { - error!("Unable to serialize data: {e:?}"); - exit(0); - } - }; + playlists.push(playlist.clone()); - if let Err(e) = write(playlist_file, &json) { - error!("Unable to write playlist: {e:?}"); - exit(1) - }; + let json: String = serde_json::to_string_pretty(&playlist)?; + + write(playlist_file, &json)?; } + + Ok(playlists) } From bb60f894eccd26242bdda280702eb5d1f73716ea Mon Sep 17 00:00:00 2001 From: jb-alvarado Date: Tue, 21 Jun 2022 17:57:04 +0200 Subject: [PATCH 37/47] change version --- src/api/args_parse.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/api/args_parse.rs b/src/api/args_parse.rs index e7eb44f9..74b7e234 100644 --- a/src/api/args_parse.rs +++ b/src/api/args_parse.rs @@ -3,7 +3,7 @@ use clap::Parser; #[derive(Parser, Debug, Clone)] #[clap(version, name = "ffpapi", - version = "0.1.0", + version = "0.3.0", about = "ffplayout REST API", long_about = None)] pub struct Args { From 529de7bba8d253cf384d268d6d0ed24696dff88a Mon Sep 17 00:00:00 2001 From: jb-alvarado Date: Tue, 21 Jun 2022 18:09:34 +0200 Subject: [PATCH 38/47] update compiler command, add service for ffpapi --- Cargo.toml | 6 ++++++ assets/ffpapi.service | 14 ++++++++++++++ cross_compile_all.sh | 10 ++++++---- 3 files changed, 26 insertions(+), 4 deletions(-) create mode 100644 assets/ffpapi.service diff --git a/Cargo.toml b/Cargo.toml index 60137583..68c2645c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -81,6 +81,11 @@ assets = [ "/usr/bin/ffplayout", "755" ], + [ + "target/x86_64-unknown-linux-musl/release/ffpapi", + "/usr/bin/ffpapi", + "755" + ], ["assets/ffplayout.yml", "/etc/ffplayout/ffplayout.yml", "644"], ["assets/logo.png", "/usr/share/ffplayout/logo.png", "644"], ["README.md", "/usr/share/doc/ffplayout-engine/README", "644"], @@ -94,6 +99,7 @@ name = "ffplayout-engine" license = "GPL-3.0" assets = [ { source = "target/x86_64-unknown-linux-musl/release/ffplayout", dest = "/usr/bin/ffplayout", mode = "755" }, + { source = "target/x86_64-unknown-linux-musl/release/ffpapi", dest = "/usr/bin/ffpapi", mode = "755" }, { source = "assets/ffplayout.yml", dest = "/etc/ffplayout/ffplayout.yml", mode = "644", config = true }, { source = "assets/ffplayout-engine.service", dest = "/lib/systemd/system/ffplayout-engine.service", mode = "644" }, { source = "README.md", dest = "/usr/share/doc/ffplayout-engine/README", mode = "644", doc = true }, diff --git a/assets/ffpapi.service b/assets/ffpapi.service new file mode 100644 index 00000000..3cd83f38 --- /dev/null +++ b/assets/ffpapi.service @@ -0,0 +1,14 @@ +[Unit] +Description=Rest API for ffplayout +After=network.target remote-fs.target + +[Service] +ExecStart= /usr/bin/ffpapi +ExecReload=/bin/kill -1 $MAINPID +Restart=always +RestartSec=1 +User=www-data +Group=www-data + +[Install] +WantedBy=multi-user.target diff --git a/cross_compile_all.sh b/cross_compile_all.sh index 7e0f0a0c..957d4852 100755 --- a/cross_compile_all.sh +++ b/cross_compile_all.sh @@ -25,16 +25,18 @@ for target in "${targets[@]}"; do fi cp ./target/${target}/release/ffplayout.exe . - zip -r "ffplayout-engine-v${version}_${target}.zip" assets docs LICENSE README.md ffplayout.exe -x *.db - rm -f ffplayout.exe + cp ./target/${target}/release/ffpapi.exe . + zip -r "ffplayout-engine-v${version}_${target}.zip" assets docs LICENSE README.md ffplayout.exe ffpapi.exe -x *.db + rm -f ffplayout.exe ffpapi.exe else if [[ -f "ffplayout-engine-v${version}_${target}.tar.gz" ]]; then rm -f "ffplayout-engine-v${version}_${target}.tar.gz" fi cp ./target/${target}/release/ffplayout . - tar -czvf "ffplayout-engine-v${version}_${target}.tar.gz" --exclude='*.db' assets docs LICENSE README.md ffplayout - rm -f ffplayout + cp ./target/${target}/release/ffpapi . + tar -czvf "ffplayout-engine-v${version}_${target}.tar.gz" --exclude='*.db' assets docs LICENSE README.md ffplayout ffpapi + rm -f ffplayout ffpapi fi echo "" From 07b1bd30aea640a5db3e7c868bbcfd23b4481b6b Mon Sep 17 00:00:00 2001 From: jb-alvarado Date: Tue, 21 Jun 2022 23:10:38 +0200 Subject: [PATCH 39/47] refractor code --- Cargo.lock | 91 +++++++++++---- Cargo.toml | 108 +----------------- build_all.sh | 100 ++++++++++++++++ cross_compile_all.sh | 56 --------- ffplayout-api/Cargo.toml | 76 ++++++++++++ ffplayout-api/README.md | 2 + {debian => ffplayout-api/debian}/.gitkeep | 0 .../ffpapi.rs => ffplayout-api/src/main.rs | 27 ++--- .../src/utils}/args_parse.rs | 4 +- {src/api => ffplayout-api/src/utils}/auth.rs | 2 +- .../src/utils}/control.rs | 2 +- .../api => ffplayout-api/src/utils}/errors.rs | 0 {src/api => ffplayout-api/src/utils}/files.rs | 4 +- .../src/utils}/handles.rs | 6 +- .../src/utils/mod.rs | 14 ++- .../api => ffplayout-api/src/utils}/models.rs | 0 .../src/utils}/playlist.rs | 4 +- .../api => ffplayout-api/src/utils}/routes.rs | 26 ++--- {assets => ffplayout-api/unit}/ffpapi.service | 0 ffplayout-engine/Cargo.toml | 79 +++++++++++++ ffplayout-engine/README.md | 2 + ffplayout-engine/src/input/folder.rs | 76 ++++++++++++ {src => ffplayout-engine/src}/input/ingest.rs | 6 +- {src => ffplayout-engine/src}/input/mod.rs | 6 +- .../src}/input/playlist.rs | 2 +- {src => ffplayout-engine/src}/main.rs | 22 +++- .../src}/output/desktop.rs | 6 +- {src => ffplayout-engine/src}/output/hls.rs | 6 +- {src => ffplayout-engine/src}/output/mod.rs | 4 +- .../src}/output/stream.rs | 6 +- {src => ffplayout-engine/src}/rpc/mod.rs | 2 +- {src => ffplayout-engine/src}/rpc/zmq_cmd.rs | 0 {src => ffplayout-engine/src}/tests/mod.rs | 4 +- .../src}/utils/arg_parse.rs | 0 ffplayout-engine/src/utils/mod.rs | 64 +++++++++++ .../unit/ffplayout.service | 0 lib/Cargo.toml | 34 ++++++ {src => lib/src}/filter/a_loudnorm.rs | 0 {src => lib/src}/filter/ingest_filter.rs | 0 {src => lib/src}/filter/mod.rs | 0 {src => lib/src}/filter/v_drawtext.rs | 0 {src => lib/src}/filter/v_overlay.rs | 0 {src => lib/src}/lib.rs | 7 +- {src => lib/src}/macros/mod.rs | 0 {src/tests/utils => lib/src/tests}/mod.rs | 0 {src => lib/src}/utils/config.rs | 61 +--------- {src => lib/src}/utils/controller.rs | 0 {src/input => lib/src/utils}/folder.rs | 67 +---------- {src => lib/src}/utils/generator.rs | 2 +- {src => lib/src}/utils/json_serializer.rs | 0 {src => lib/src}/utils/json_validate.rs | 0 {src => lib/src}/utils/logging.rs | 0 {src => lib/src}/utils/mod.rs | 3 +- src/api/mod.rs | 10 -- 54 files changed, 603 insertions(+), 388 deletions(-) create mode 100755 build_all.sh delete mode 100755 cross_compile_all.sh create mode 100644 ffplayout-api/Cargo.toml create mode 100644 ffplayout-api/README.md rename {debian => ffplayout-api/debian}/.gitkeep (100%) rename src/bin/ffpapi.rs => ffplayout-api/src/main.rs (84%) rename {src/api => ffplayout-api/src/utils}/args_parse.rs (87%) rename {src/api => ffplayout-api/src/utils}/auth.rs (97%) rename {src/api => ffplayout-api/src/utils}/control.rs (97%) rename {src/api => ffplayout-api/src/utils}/errors.rs (100%) rename {src/api => ffplayout-api/src/utils}/files.rs (94%) rename {src/api => ffplayout-api/src/utils}/handles.rs (99%) rename src/api/utils.rs => ffplayout-api/src/utils/mod.rs (93%) rename {src/api => ffplayout-api/src/utils}/models.rs (100%) rename {src/api => ffplayout-api/src/utils}/playlist.rs (95%) rename {src/api => ffplayout-api/src/utils}/routes.rs (96%) rename {assets => ffplayout-api/unit}/ffpapi.service (100%) create mode 100644 ffplayout-engine/Cargo.toml create mode 100644 ffplayout-engine/README.md create mode 100644 ffplayout-engine/src/input/folder.rs rename {src => ffplayout-engine/src}/input/ingest.rs (96%) rename {src => ffplayout-engine/src}/input/mod.rs (92%) rename {src => ffplayout-engine/src}/input/playlist.rs (99%) rename {src => ffplayout-engine/src}/main.rs (90%) rename {src => ffplayout-engine/src}/output/desktop.rs (92%) rename {src => ffplayout-engine/src}/output/hls.rs (98%) rename {src => ffplayout-engine/src}/output/mod.rs (99%) rename {src => ffplayout-engine/src}/output/stream.rs (92%) rename {src => ffplayout-engine/src}/rpc/mod.rs (99%) rename {src => ffplayout-engine/src}/rpc/zmq_cmd.rs (100%) rename {src => ffplayout-engine/src}/tests/mod.rs (98%) rename {src => ffplayout-engine/src}/utils/arg_parse.rs (100%) create mode 100644 ffplayout-engine/src/utils/mod.rs rename assets/ffplayout-engine.service => ffplayout-engine/unit/ffplayout.service (100%) create mode 100644 lib/Cargo.toml rename {src => lib/src}/filter/a_loudnorm.rs (100%) rename {src => lib/src}/filter/ingest_filter.rs (100%) rename {src => lib/src}/filter/mod.rs (100%) rename {src => lib/src}/filter/v_drawtext.rs (100%) rename {src => lib/src}/filter/v_overlay.rs (100%) rename {src => lib/src}/lib.rs (66%) rename {src => lib/src}/macros/mod.rs (100%) rename {src/tests/utils => lib/src/tests}/mod.rs (100%) rename {src => lib/src}/utils/config.rs (80%) rename {src => lib/src}/utils/controller.rs (100%) rename {src/input => lib/src/utils}/folder.rs (63%) rename {src => lib/src}/utils/generator.rs (99%) rename {src => lib/src}/utils/json_serializer.rs (100%) rename {src => lib/src}/utils/json_validate.rs (100%) rename {src => lib/src}/utils/logging.rs (100%) rename {src => lib/src}/utils/mod.rs (99%) delete mode 100644 src/api/mod.rs diff --git a/Cargo.lock b/Cargo.lock index cd9a6dce..ca8d56a2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -315,9 +315,9 @@ dependencies = [ [[package]] name = "async-global-executor" -version = "2.1.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd8b508d585e01084059b60f06ade4cb7415cd2e4084b71dd1cb44e7d3fb9880" +checksum = "5262ed948da60dd8956c6c5aca4d4163593dddb7b32d73267c93dab7b2e98940" dependencies = [ "async-channel", "async-executor", @@ -325,6 +325,7 @@ dependencies = [ "async-lock", "blocking", "futures-lite", + "num_cpus", "once_cell", ] @@ -588,6 +589,7 @@ dependencies = [ "libc", "num-integer", "num-traits", + "time 0.1.44", "winapi 0.3.9", ] @@ -605,9 +607,9 @@ dependencies = [ [[package]] name = "clap" -version = "3.2.5" +version = "3.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d53da17d37dba964b9b3ecb5c5a1f193a2762c700e6829201e645b9381c99dc7" +checksum = "9f1fe12880bae935d142c8702d500c63a4e8634b6c3c57ad72bf978fc7b6249a" dependencies = [ "atty", "bitflags", @@ -622,9 +624,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "3.2.5" +version = "3.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c11d40217d16aee8508cc8e5fde8b4ff24639758608e5374e731b53f85749fb9" +checksum = "ed6db9e867166a43a53f7199b5e4d1f522a1e5bd626654be263c999ce59df39a" dependencies = [ "heck", "proc-macro-error", @@ -635,9 +637,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.2.2" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5538cd660450ebeb4234cfecf8f2284b844ffc4c50531e66d584ad5b91293613" +checksum = "87eba3c8c7f42ef17f6c659fc7416d0f4758cd3e58861ee63c5fa4a4dde649e4" dependencies = [ "os_str_bytes", ] @@ -977,26 +979,21 @@ dependencies = [ ] [[package]] -name = "ffplayout-engine" -version = "0.9.9" +name = "ffplayout-api" +version = "0.3.0" dependencies = [ "actix-web", "actix-web-grants", "actix-web-httpauth", "argon2", - "chrono 0.4.19 (git+https://github.com/sbrocket/chrono?branch=parse-error-kind-public)", + "chrono 0.4.19 (registry+https://github.com/rust-lang/crates.io-index)", "clap", - "crossbeam-channel 0.5.5", "derive_more", "faccess", + "ffplayout-lib", "ffprobe", - "file-rotate", - "futures", - "jsonrpc-http-server", "jsonwebtoken", - "lettre", "log", - "notify", "once_cell", "openssl", "rand 0.8.5", @@ -1007,14 +1004,68 @@ dependencies = [ "serde", "serde_json", "serde_yaml", - "shlex", "simplelog", "sqlx", +] + +[[package]] +name = "ffplayout-engine" +version = "0.9.9" +dependencies = [ + "chrono 0.4.19 (git+https://github.com/sbrocket/chrono?branch=parse-error-kind-public)", + "clap", + "crossbeam-channel 0.5.5", + "faccess", + "ffplayout-lib", + "ffprobe", + "file-rotate", + "futures", + "jsonrpc-http-server", + "lettre", + "log", + "notify", + "openssl", + "rand 0.8.5", + "regex", + "reqwest", + "serde", + "serde_json", + "serde_yaml", + "shlex", + "simplelog", "time 0.3.10", "walkdir", "zeromq", ] +[[package]] +name = "ffplayout-lib" +version = "0.9.9" +dependencies = [ + "chrono 0.4.19 (git+https://github.com/sbrocket/chrono?branch=parse-error-kind-public)", + "crossbeam-channel 0.5.5", + "faccess", + "ffprobe", + "file-rotate", + "futures", + "jsonrpc-http-server", + "lettre", + "log", + "notify", + "once_cell", + "openssl", + "rand 0.8.5", + "regex", + "reqwest", + "serde", + "serde_json", + "serde_yaml", + "shlex", + "simplelog", + "time 0.3.10", + "walkdir", +] + [[package]] name = "ffprobe" version = "0.3.2" @@ -1474,9 +1525,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "1.9.0" +version = "1.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c6392766afd7964e2531940894cffe4bd8d7d17dbc3c1c4857040fd4b33bdb3" +checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e" dependencies = [ "autocfg", "hashbrown 0.12.1", diff --git a/Cargo.toml b/Cargo.toml index 68c2645c..77e2c4b7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,108 +1,12 @@ -[package] -name = "ffplayout-engine" -description = "24/7 playout based on rust and ffmpeg" -license = "GPL-3.0" -authors = ["Jonathan Baecker jonbae77@gmail.com"] -readme = "README.md" -version = "0.9.9" -edition = "2021" -default-run = "ffplayout" +[workspace] -[dependencies] -actix-web = "4" -actix-web-grants = "3" -actix-web-httpauth = "0.6" -argon2 = "0.4" -chrono = { git = "https://github.com/sbrocket/chrono", branch = "parse-error-kind-public" } -clap = { version = "3.2", features = ["derive"] } -crossbeam-channel = "0.5" -derive_more = "0.99" -faccess = "0.2" -ffprobe = "0.3" -file-rotate = { git = "https://github.com/Ploppz/file-rotate.git", branch = "timestamp-parse-fix" } -futures = "0.3" -jsonrpc-http-server = "18.0" -jsonwebtoken = "8" -lettre = "0.10.0-rc.7" -log = "0.4" -notify = "4.0" -once_cell = "1.10" -rand = "0.8" -rand_core = { version = "0.6", features = ["std"] } -relative-path = "1.6" -regex = "1" -reqwest = { version = "0.11", features = ["blocking", "json"] } -serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" -serde_yaml = "0.8" -shlex = "1.1" -simplelog = { version = "^0.12", features = ["paris"] } -sqlx = { version = "0.5", features = [ - "chrono", - "runtime-actix-native-tls", - "sqlite" -] } -time = { version = "0.3", features = ["formatting", "macros"] } -walkdir = "2" -zeromq = { git = "https://github.com/zeromq/zmq.rs.git", default-features = false, features = [ - "async-std-runtime", - "tcp-transport" -] } - -[target.x86_64-unknown-linux-musl.dependencies] -openssl = { version = "0.10", features = ["vendored"] } - -[[bin]] -name = "ffplayout" -path = "src/main.rs" - -[[bin]] -name = "ffpapi" -path = "src/bin/ffpapi.rs" +members = [ + "ffplayout-api", + "ffplayout-engine", + "lib", +] [profile.release] opt-level = 3 strip = true lto = true - -# DEBIAN DEB PACKAGE -[package.metadata.deb] -name = "ffplayout-engine" -priority = "optional" -section = "net" -license-file = ["LICENSE", "0"] -depends = "" -suggests = "ffmpeg" -copyright = "Copyright (c) 2022, Jonathan Baecker. All rights reserved." -conf-files = ["/etc/ffplayout/ffplayout.yml"] -assets = [ - [ - "target/x86_64-unknown-linux-musl/release/ffplayout", - "/usr/bin/ffplayout", - "755" - ], - [ - "target/x86_64-unknown-linux-musl/release/ffpapi", - "/usr/bin/ffpapi", - "755" - ], - ["assets/ffplayout.yml", "/etc/ffplayout/ffplayout.yml", "644"], - ["assets/logo.png", "/usr/share/ffplayout/logo.png", "644"], - ["README.md", "/usr/share/doc/ffplayout-engine/README", "644"], -] -maintainer-scripts = "debian/" -systemd-units = { enable = false, unit-scripts = "assets" } - -# REHL RPM PACKAGE -[package.metadata.generate-rpm] -name = "ffplayout-engine" -license = "GPL-3.0" -assets = [ - { source = "target/x86_64-unknown-linux-musl/release/ffplayout", dest = "/usr/bin/ffplayout", mode = "755" }, - { source = "target/x86_64-unknown-linux-musl/release/ffpapi", dest = "/usr/bin/ffpapi", mode = "755" }, - { source = "assets/ffplayout.yml", dest = "/etc/ffplayout/ffplayout.yml", mode = "644", config = true }, - { source = "assets/ffplayout-engine.service", dest = "/lib/systemd/system/ffplayout-engine.service", mode = "644" }, - { source = "README.md", dest = "/usr/share/doc/ffplayout-engine/README", mode = "644", doc = true }, - { source = "LICENSE", dest = "/usr/share/doc/ffplayout-engine/LICENSE", mode = "644" }, - { source = "assets/logo.png", dest = "/usr/share/ffplayout/logo.png", mode = "644" }, -] diff --git a/build_all.sh b/build_all.sh new file mode 100755 index 00000000..9052828b --- /dev/null +++ b/build_all.sh @@ -0,0 +1,100 @@ +#!/usr/bin/bash + + +targets=("x86_64-unknown-linux-musl" "x86_64-pc-windows-gnu" "x86_64-apple-darwin" "aarch64-apple-darwin") + +IFS="= " +while read -r name value; do + if [[ $name == "version" ]]; then + version=${value//\"/} + fi +done < ffplayout-engine/Cargo.toml + +echo "Compile ffplayout-engine version is: \"$version\"" +echo "" + +for target in "${targets[@]}"; do + echo "compile static for $target" + echo "" + + cargo build --release --target=$target --bin ffplayout + + if [[ $target == "x86_64-pc-windows-gnu" ]]; then + if [[ -f "ffplayout-engine-v${version}_${target}.zip" ]]; then + rm -f "ffplayout-engine-v${version}_${target}.zip" + fi + + cp ./target/${target}/release/ffplayout.exe . + zip -r "ffplayout-engine-v${version}_${target}.zip" assets docs LICENSE README.md ffplayout.exe -x *.db + rm -f ffplayout.exe + elif [[ $target == "x86_64-apple-darwin" ]] || [[ $target == "aarch64-apple-darwin" ]]; then + if [[ -f "ffplayout-engine-v${version}_${target}.tar.gz" ]]; then + rm -f "ffplayout-engine-v${version}_${target}.tar.gz" + fi + + cp ./target/${target}/release/ffplayout . + tar -czvf "ffplayout-engine-v${version}_${target}.tar.gz" --exclude='*.db' assets docs LICENSE README.md ffplayout + rm -f ffplayout + else + if [[ -f "ffplayout-engine-v${version}_${target}.tar.gz" ]]; then + rm -f "ffplayout-engine-v${version}_${target}.tar.gz" + fi + + cp ./target/${target}/release/ffplayout . + tar -czvf "ffplayout-engine-v${version}_${target}.tar.gz" --exclude='*.db' assets docs LICENSE README.md ffplayout + rm -f ffplayout + fi + + echo "" +done + +cargo deb --target=x86_64-unknown-linux-musl -p ffplayout-engine +mv ./target/x86_64-unknown-linux-musl/debian/ffplayout-engine_${version}_amd64.deb . + +cargo generate-rpm --target=x86_64-unknown-linux-musl -p ffplayout-engine +mv ./target/x86_64-unknown-linux-musl/generate-rpm/ffplayout-engine-${version}-1.x86_64.rpm . + +IFS="= " +while read -r name value; do + if [[ $name == "version" ]]; then + version=${value//\"/} + fi +done < ffplayout-api/Cargo.toml + +echo "Compile ffplayout-api version is: \"$version\"" +echo "" + +for target in "${targets[@]}"; do + echo "compile static for $target" + echo "" + + if [[ $target == "x86_64-pc-windows-gnu" ]]; then + if [[ -f "ffplayout-api-v${version}_${target}.zip" ]]; then + rm -f "ffplayout-api-v${version}_${target}.zip" + fi + + cargo build --release --target=$target --bin ffpapi + + cp ./target/${target}/release/ffpapi.exe . + zip -r "ffplayout-api-v${version}_${target}.zip" assets docs LICENSE README.md ffpapi.exe -x *.db + rm -f ffpapi.exe + elif [[ $target == "x86_64-unknown-linux-musl" ]]; then + if [[ -f "ffplayout-api-v${version}_${target}.tar.gz" ]]; then + rm -f "ffplayout-api-v${version}_${target}.tar.gz" + fi + + cargo build --release --target=$target --bin ffpapi + + cp ./target/${target}/release/ffpapi . + tar -czvf "ffplayout-api-v${version}_${target}.tar.gz" --exclude='*.db' assets docs LICENSE README.md ffpapi + rm -f ffpapi + fi + + echo "" +done + +cargo deb --target=x86_64-unknown-linux-musl -p ffplayout-api +mv ./target/x86_64-unknown-linux-musl/debian/ffplayout-api_${version}_amd64.deb . + +cargo generate-rpm --target=x86_64-unknown-linux-musl -p ffplayout-api +mv ./target/x86_64-unknown-linux-musl/generate-rpm/ffplayout-api-${version}-1.x86_64.rpm . diff --git a/cross_compile_all.sh b/cross_compile_all.sh deleted file mode 100755 index 957d4852..00000000 --- a/cross_compile_all.sh +++ /dev/null @@ -1,56 +0,0 @@ -#!/usr/bin/bash - - -targets=("x86_64-unknown-linux-musl" "x86_64-pc-windows-gnu" "x86_64-apple-darwin" "aarch64-apple-darwin") - -IFS="= " -while read -r name value; do - if [[ $name == "version" ]]; then - version=${value//\"/} - fi -done < Cargo.toml - -echo "Compile ffplayout-engine version is: \"$version\"" -echo "" - -for target in "${targets[@]}"; do - echo "compile static for $target" - echo "" - - cargo build --release --target=$target - - if [[ $target == "x86_64-pc-windows-gnu" ]]; then - if [[ -f "ffplayout-engine-v${version}_${target}.zip" ]]; then - rm -f "ffplayout-engine-v${version}_${target}.zip" - fi - - cp ./target/${target}/release/ffplayout.exe . - cp ./target/${target}/release/ffpapi.exe . - zip -r "ffplayout-engine-v${version}_${target}.zip" assets docs LICENSE README.md ffplayout.exe ffpapi.exe -x *.db - rm -f ffplayout.exe ffpapi.exe - else - if [[ -f "ffplayout-engine-v${version}_${target}.tar.gz" ]]; then - rm -f "ffplayout-engine-v${version}_${target}.tar.gz" - fi - - cp ./target/${target}/release/ffplayout . - cp ./target/${target}/release/ffpapi . - tar -czvf "ffplayout-engine-v${version}_${target}.tar.gz" --exclude='*.db' assets docs LICENSE README.md ffplayout ffpapi - rm -f ffplayout ffpapi - fi - - echo "" -done - -echo "Create debian package" -echo "" - -cargo deb --target=x86_64-unknown-linux-musl -mv ./target/x86_64-unknown-linux-musl/debian/ffplayout-engine_${version}_amd64.deb . - -echo "" -echo "Create rhel package" -echo "" - -cargo generate-rpm --target=x86_64-unknown-linux-musl -mv ./target/x86_64-unknown-linux-musl/generate-rpm/ffplayout-engine-${version}-1.x86_64.rpm . diff --git a/ffplayout-api/Cargo.toml b/ffplayout-api/Cargo.toml new file mode 100644 index 00000000..c4183d13 --- /dev/null +++ b/ffplayout-api/Cargo.toml @@ -0,0 +1,76 @@ +[package] +name = "ffplayout-api" +description = "Rest API for ffplayout" +license = "GPL-3.0" +authors = ["Jonathan Baecker jonbae77@gmail.com"] +readme = "README.md" +version = "0.3.0" +edition = "2021" + +[dependencies] +ffplayout-lib = { path = "../lib" } +actix-web = "4" +actix-web-grants = "3" +actix-web-httpauth = "0.6" +argon2 = "0.4" +chrono = "0.4" +clap = { version = "3.2", features = ["derive"] } +derive_more = "0.99" +faccess = "0.2" +ffprobe = "0.3" +jsonwebtoken = "8" +log = "0.4" +once_cell = "1.10" +rand = "0.8" +rand_core = { version = "0.6", features = ["std"] } +relative-path = "1.6" +regex = "1" +reqwest = { version = "0.11", features = ["blocking", "json"] } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +serde_yaml = "0.8" +simplelog = { version = "^0.12", features = ["paris"] } +sqlx = { version = "0.5", features = [ + "chrono", + "runtime-actix-native-tls", + "sqlite" +] } + +[target.x86_64-unknown-linux-musl.dependencies] +openssl = { version = "0.10", features = ["vendored"] } + +[[bin]] +name = "ffpapi" +path = "src/main.rs" + +# DEBIAN DEB PACKAGE +[package.metadata.deb] +name = "ffplayout-api" +priority = "optional" +section = "net" +license-file = ["../LICENSE", "0"] +depends = "" +suggests = "ffmpeg" +copyright = "Copyright (c) 2022, Jonathan Baecker. All rights reserved." +conf-files = ["/etc/ffplayout/ffplayout.yml"] +assets = [ + [ + "../target/x86_64-unknown-linux-musl/release/ffpapi", + "/usr/bin/ffpapi", + "755" + ], + ["README.md", "/usr/share/doc/ffplayout/README", "644"], +] +maintainer-scripts = "debian/" +systemd-units = { enable = false, unit-scripts = "unit" } + +# REHL RPM PACKAGE +[package.metadata.generate-rpm] +name = "ffplayout-api" +license = "GPL-3.0" +assets = [ + { source = "../target/x86_64-unknown-linux-musl/release/ffpapi", dest = "/usr/bin/ffpapi", mode = "755" }, + { source = "unit/ffpapi.service", dest = "/lib/systemd/system/ffpapi.service", mode = "644" }, + { source = "README.md", dest = "/usr/share/doc/ffplayout/README", mode = "644", doc = true }, + { source = "../LICENSE", dest = "/usr/share/doc/ffplayout/LICENSE", mode = "644" }, +] diff --git a/ffplayout-api/README.md b/ffplayout-api/README.md new file mode 100644 index 00000000..c247225c --- /dev/null +++ b/ffplayout-api/README.md @@ -0,0 +1,2 @@ +**ffplayout-api** +================ diff --git a/debian/.gitkeep b/ffplayout-api/debian/.gitkeep similarity index 100% rename from debian/.gitkeep rename to ffplayout-api/debian/.gitkeep diff --git a/src/bin/ffpapi.rs b/ffplayout-api/src/main.rs similarity index 84% rename from src/bin/ffpapi.rs rename to ffplayout-api/src/main.rs index fd1c0db4..fc1ec396 100644 --- a/src/bin/ffpapi.rs +++ b/ffplayout-api/src/main.rs @@ -8,22 +8,23 @@ use actix_web_httpauth::middleware::HttpAuthentication; use clap::Parser; use simplelog::*; -use ffplayout_engine::{ - api::{ - args_parse::Args, - auth, - models::LoginUser, - routes::{ - add_preset, add_user, del_playlist, file_browser, gen_playlist, get_playlist, - get_playout_config, get_presets, get_settings, jump_to_last, jump_to_next, login, - media_current, media_last, media_next, patch_settings, reset_playout, save_playlist, - send_text_message, update_playout_config, update_preset, update_user, - }, - utils::{db_path, init_config, run_args, Role}, +pub mod utils; + +use utils::{ + args_parse::Args, + auth, db_path, init_config, + models::LoginUser, + routes::{ + add_preset, add_user, del_playlist, file_browser, gen_playlist, get_playlist, + get_playout_config, get_presets, get_settings, jump_to_last, jump_to_next, login, + media_current, media_last, media_next, patch_settings, reset_playout, save_playlist, + send_text_message, update_playout_config, update_preset, update_user, }, - utils::{init_logging, PlayoutConfig}, + run_args, Role, }; +use ffplayout_lib::utils::{init_logging, PlayoutConfig}; + async fn validator(req: ServiceRequest, credentials: BearerAuth) -> Result { // We just get permissions from JWT let claims = auth::decode_jwt(credentials.token()).await?; diff --git a/src/api/args_parse.rs b/ffplayout-api/src/utils/args_parse.rs similarity index 87% rename from src/api/args_parse.rs rename to ffplayout-api/src/utils/args_parse.rs index 74b7e234..84d8efa5 100644 --- a/src/api/args_parse.rs +++ b/ffplayout-api/src/utils/args_parse.rs @@ -2,9 +2,7 @@ use clap::Parser; #[derive(Parser, Debug, Clone)] #[clap(version, - name = "ffpapi", - version = "0.3.0", - about = "ffplayout REST API", + about = "REST API for ffplayout", long_about = None)] pub struct Args { #[clap(short, long, help = "Listen on IP:PORT, like: 127.0.0.1:8080")] diff --git a/src/api/auth.rs b/ffplayout-api/src/utils/auth.rs similarity index 97% rename from src/api/auth.rs rename to ffplayout-api/src/utils/auth.rs index ba34919e..76351923 100644 --- a/src/api/auth.rs +++ b/ffplayout-api/src/utils/auth.rs @@ -4,7 +4,7 @@ use chrono::{Duration, Utc}; use jsonwebtoken::{self, DecodingKey, EncodingKey, Header, Validation}; use serde::{Deserialize, Serialize}; -use crate::api::utils::GlobalSettings; +use crate::utils::GlobalSettings; // Token lifetime const JWT_EXPIRATION_DAYS: i64 = 7; diff --git a/src/api/control.rs b/ffplayout-api/src/utils/control.rs similarity index 97% rename from src/api/control.rs rename to ffplayout-api/src/utils/control.rs index 42a9ba10..44804d85 100644 --- a/src/api/control.rs +++ b/ffplayout-api/src/utils/control.rs @@ -7,7 +7,7 @@ use reqwest::{ use serde::{Deserialize, Serialize}; use simplelog::*; -use crate::api::{errors::ServiceError, utils::playout_config}; +use crate::utils::{errors::ServiceError, playout_config}; #[derive(Debug, Deserialize, Serialize, Clone)] struct RpcObj { diff --git a/src/api/errors.rs b/ffplayout-api/src/utils/errors.rs similarity index 100% rename from src/api/errors.rs rename to ffplayout-api/src/utils/errors.rs diff --git a/src/api/files.rs b/ffplayout-api/src/utils/files.rs similarity index 94% rename from src/api/files.rs rename to ffplayout-api/src/utils/files.rs index e60daa19..4343d4ef 100644 --- a/src/api/files.rs +++ b/ffplayout-api/src/utils/files.rs @@ -4,8 +4,8 @@ use std::{fs, path::PathBuf}; use simplelog::*; -use crate::api::{errors::ServiceError, utils::playout_config}; -use crate::utils::file_extension; +use crate::utils::{errors::ServiceError, playout_config}; +use ffplayout_lib::utils::file_extension; #[derive(Debug, Deserialize, Serialize, Clone)] pub struct PathObject { diff --git a/src/api/handles.rs b/ffplayout-api/src/utils/handles.rs similarity index 99% rename from src/api/handles.rs rename to ffplayout-api/src/utils/handles.rs index 5a88a184..9ee0f1f3 100644 --- a/src/api/handles.rs +++ b/ffplayout-api/src/utils/handles.rs @@ -7,10 +7,10 @@ use rand::{distributions::Alphanumeric, Rng}; use simplelog::*; use sqlx::{migrate::MigrateDatabase, sqlite::SqliteQueryResult, Pool, Sqlite, SqlitePool}; -use crate::api::utils::GlobalSettings; -use crate::api::{ +use crate::utils::{ + db_path, models::{Settings, TextPreset, User}, - utils::db_path, + GlobalSettings, }; #[derive(Debug, sqlx::FromRow)] diff --git a/src/api/utils.rs b/ffplayout-api/src/utils/mod.rs similarity index 93% rename from src/api/utils.rs rename to ffplayout-api/src/utils/mod.rs index aedc1ee4..b3561173 100644 --- a/src/api/utils.rs +++ b/ffplayout-api/src/utils/mod.rs @@ -4,13 +4,23 @@ use faccess::PathExt; use once_cell::sync::OnceCell; use simplelog::*; -use crate::api::{ +pub mod args_parse; +pub mod auth; +pub mod control; +pub mod errors; +pub mod files; +pub mod handles; +pub mod models; +pub mod playlist; +pub mod routes; + +use crate::utils::{ args_parse::Args, errors::ServiceError, handles::{db_add_user, db_get_settings, db_global, db_init}, models::{Settings, User}, }; -use crate::utils::PlayoutConfig; +use ffplayout_lib::utils::PlayoutConfig; #[derive(PartialEq, Clone)] pub enum Role { diff --git a/src/api/models.rs b/ffplayout-api/src/utils/models.rs similarity index 100% rename from src/api/models.rs rename to ffplayout-api/src/utils/models.rs diff --git a/src/api/playlist.rs b/ffplayout-api/src/utils/playlist.rs similarity index 95% rename from src/api/playlist.rs rename to ffplayout-api/src/utils/playlist.rs index 6ee5c11b..9f828024 100644 --- a/src/api/playlist.rs +++ b/ffplayout-api/src/utils/playlist.rs @@ -6,8 +6,8 @@ use std::{ use simplelog::*; -use crate::api::{errors::ServiceError, utils::playout_config}; -use crate::utils::{generate_playlist as playlist_generator, JsonPlaylist}; +use crate::utils::{errors::ServiceError, playout_config}; +use ffplayout_lib::utils::{generate_playlist as playlist_generator, JsonPlaylist}; fn json_reader(path: &PathBuf) -> Result { let f = File::options().read(true).write(false).open(&path)?; diff --git a/src/api/routes.rs b/ffplayout-api/src/utils/routes.rs similarity index 96% rename from src/api/routes.rs rename to ffplayout-api/src/utils/routes.rs index 60622b64..3c98ba09 100644 --- a/src/api/routes.rs +++ b/ffplayout-api/src/utils/routes.rs @@ -9,22 +9,20 @@ use argon2::{ use serde::Serialize; use simplelog::*; -use crate::{ - api::{ - auth::{create_jwt, Claims}, - control::{control_state, media_info, send_message}, - errors::ServiceError, - files::{browser, PathObject}, - handles::{ - db_add_preset, db_add_user, db_get_presets, db_get_settings, db_login, db_role, - db_update_preset, db_update_settings, db_update_user, - }, - models::{LoginUser, Settings, TextPreset, User}, - playlist::{delete_playlist, generate_playlist, read_playlist, write_playlist}, - utils::{read_playout_config, Role}, +use crate::utils::{ + auth::{create_jwt, Claims}, + control::{control_state, media_info, send_message}, + errors::ServiceError, + files::{browser, PathObject}, + handles::{ + db_add_preset, db_add_user, db_get_presets, db_get_settings, db_login, db_role, + db_update_preset, db_update_settings, db_update_user, }, - utils::{JsonPlaylist, PlayoutConfig}, + models::{LoginUser, Settings, TextPreset, User}, + playlist::{delete_playlist, generate_playlist, read_playlist, write_playlist}, + read_playout_config, Role, }; +use ffplayout_lib::utils::{JsonPlaylist, PlayoutConfig}; #[derive(Serialize)] struct ResponseObj { diff --git a/assets/ffpapi.service b/ffplayout-api/unit/ffpapi.service similarity index 100% rename from assets/ffpapi.service rename to ffplayout-api/unit/ffpapi.service diff --git a/ffplayout-engine/Cargo.toml b/ffplayout-engine/Cargo.toml new file mode 100644 index 00000000..7b2cac32 --- /dev/null +++ b/ffplayout-engine/Cargo.toml @@ -0,0 +1,79 @@ +[package] +name = "ffplayout-engine" +description = "24/7 playout based on rust and ffmpeg" +license = "GPL-3.0" +authors = ["Jonathan Baecker jonbae77@gmail.com"] +readme = "README.md" +version = "0.9.9" +edition = "2021" + +[dependencies] +ffplayout-lib = { path = "../lib" } +chrono = { git = "https://github.com/sbrocket/chrono", branch = "parse-error-kind-public" } +clap = { version = "3.2", features = ["derive"] } +crossbeam-channel = "0.5" +faccess = "0.2" +ffprobe = "0.3" +file-rotate = { git = "https://github.com/Ploppz/file-rotate.git", branch = "timestamp-parse-fix" } +futures = "0.3" +jsonrpc-http-server = "18.0" +lettre = "0.10.0-rc.7" +log = "0.4" +notify = "4.0" +rand = "0.8" +regex = "1" +reqwest = { version = "0.11", features = ["blocking", "json"] } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +serde_yaml = "0.8" +shlex = "1.1" +simplelog = { version = "^0.12", features = ["paris"] } +time = { version = "0.3", features = ["formatting", "macros"] } +walkdir = "2" +zeromq = { git = "https://github.com/zeromq/zmq.rs.git", default-features = false, features = [ + "async-std-runtime", + "tcp-transport" +] } + +[target.x86_64-unknown-linux-musl.dependencies] +openssl = { version = "0.10", features = ["vendored"] } + +[[bin]] +name = "ffplayout" +path = "src/main.rs" + +# DEBIAN DEB PACKAGE +[package.metadata.deb] +name = "ffplayout-engine" +priority = "optional" +section = "net" +license-file = ["../LICENSE", "0"] +depends = "" +suggests = "ffmpeg" +copyright = "Copyright (c) 2022, Jonathan Baecker. All rights reserved." +conf-files = ["/etc/ffplayout/ffplayout.yml"] +assets = [ + [ + "../target/x86_64-unknown-linux-musl/release/ffplayout", + "/usr/bin/ffplayout", + "755" + ], + ["../assets/ffplayout.yml", "/etc/ffplayout/ffplayout.yml", "644"], + ["../assets/logo.png", "/usr/share/ffplayout/logo.png", "644"], + ["../README.md", "/usr/share/doc/ffplayout/README", "644"], +] +maintainer-scripts = "debian/" +systemd-units = { enable = false, unit-scripts = "unit" } + +# REHL RPM PACKAGE +[package.metadata.generate-rpm] +name = "ffplayout-engine" +license = "GPL-3.0" +assets = [ + { source = "../target/x86_64-unknown-linux-musl/release/ffplayout", dest = "/usr/bin/ffplayout", mode = "755" }, + { source = "../assets/ffplayout.yml", dest = "/etc/ffplayout/ffplayout.yml", mode = "644", config = true }, + { source = "unit/ffplayout.service", dest = "/lib/systemd/system/ffplayout.service", mode = "644" }, + { source = "../README.md", dest = "/usr/share/doc/ffplayout/README", mode = "644", doc = true }, + { source = "../LICENSE", dest = "/usr/share/doc/ffplayout/LICENSE", mode = "644" }, + { source = "../assets/logo.png", dest = "/usr/share/ffplayout/logo.png", mode = "644" }, +] diff --git a/ffplayout-engine/README.md b/ffplayout-engine/README.md new file mode 100644 index 00000000..29590fe5 --- /dev/null +++ b/ffplayout-engine/README.md @@ -0,0 +1,2 @@ +**ffplayout-engine** +================ diff --git a/ffplayout-engine/src/input/folder.rs b/ffplayout-engine/src/input/folder.rs new file mode 100644 index 00000000..f7809434 --- /dev/null +++ b/ffplayout-engine/src/input/folder.rs @@ -0,0 +1,76 @@ +use std::{ + path::Path, + sync::{ + atomic::{AtomicBool, Ordering}, + mpsc::channel, + {Arc, Mutex}, + }, + thread::sleep, + time::Duration, +}; + +use notify::{ + DebouncedEvent::{Create, Remove, Rename}, + {watcher, RecursiveMode, Watcher}, +}; +use simplelog::*; + +use ffplayout_lib::utils::{Media, PlayoutConfig}; + +/// Create a watcher, which monitor file changes. +/// When a change is register, update the current file list. +/// This makes it possible, to play infinitely and and always new files to it. +pub fn watchman( + config: PlayoutConfig, + is_terminated: Arc, + sources: Arc>>, +) { + let (tx, rx) = channel(); + + let path = config.storage.path; + + if !Path::new(&path).exists() { + error!("Folder path not exists: '{path}'"); + panic!("Folder path not exists: '{path}'"); + } + + let mut watcher = watcher(tx, Duration::from_secs(1)).unwrap(); + watcher.watch(path, RecursiveMode::Recursive).unwrap(); + + while !is_terminated.load(Ordering::SeqCst) { + if let Ok(res) = rx.try_recv() { + match res { + Create(new_path) => { + let index = sources.lock().unwrap().len(); + let media = Media::new(index, new_path.display().to_string(), false); + + sources.lock().unwrap().push(media); + info!("Create new file: {new_path:?}"); + } + Remove(old_path) => { + sources + .lock() + .unwrap() + .retain(|x| x.source != old_path.display().to_string()); + info!("Remove file: {old_path:?}"); + } + Rename(old_path, new_path) => { + let index = sources + .lock() + .unwrap() + .iter() + .position(|x| *x.source == old_path.display().to_string()) + .unwrap(); + + let media = Media::new(index, new_path.display().to_string(), false); + sources.lock().unwrap()[index] = media; + + info!("Rename file: {old_path:?} to {new_path:?}"); + } + _ => (), + } + } + + sleep(Duration::from_secs(5)); + } +} diff --git a/src/input/ingest.rs b/ffplayout-engine/src/input/ingest.rs similarity index 96% rename from src/input/ingest.rs rename to ffplayout-engine/src/input/ingest.rs index a5cd4c28..cc316a30 100644 --- a/src/input/ingest.rs +++ b/ffplayout-engine/src/input/ingest.rs @@ -8,9 +8,9 @@ use std::{ use crossbeam_channel::Sender; use simplelog::*; -use crate::filter::ingest_filter::filter_cmd; -use crate::utils::{format_log_line, Ingest, PlayoutConfig, ProcessControl}; -use crate::vec_strings; +use ffplayout_lib::filter::ingest_filter::filter_cmd; +use ffplayout_lib::utils::{format_log_line, Ingest, PlayoutConfig, ProcessControl}; +use ffplayout_lib::vec_strings; pub fn log_line(line: String, level: &str) { if line.contains("[info]") && level.to_lowercase() == "info" { diff --git a/src/input/mod.rs b/ffplayout-engine/src/input/mod.rs similarity index 92% rename from src/input/mod.rs rename to ffplayout-engine/src/input/mod.rs index 9c1ffc6e..5fcaea62 100644 --- a/src/input/mod.rs +++ b/ffplayout-engine/src/input/mod.rs @@ -9,16 +9,18 @@ use std::{ use simplelog::*; -use crate::utils::{Media, PlayoutConfig, PlayoutStatus}; +use ffplayout_lib::utils::{Media, PlayoutConfig, PlayoutStatus}; pub mod folder; pub mod ingest; pub mod playlist; -pub use folder::{watchman, FolderSource}; +pub use folder::watchman; pub use ingest::ingest_server; pub use playlist::CurrentProgram; +use ffplayout_lib::utils::folder::FolderSource; + /// Create a source iterator from playlist, or from folder. pub fn source_generator( config: PlayoutConfig, diff --git a/src/input/playlist.rs b/ffplayout-engine/src/input/playlist.rs similarity index 99% rename from src/input/playlist.rs rename to ffplayout-engine/src/input/playlist.rs index e6384822..ab8da117 100644 --- a/src/input/playlist.rs +++ b/ffplayout-engine/src/input/playlist.rs @@ -10,7 +10,7 @@ use std::{ use serde_json::json; use simplelog::*; -use crate::utils::{ +use ffplayout_lib::utils::{ check_sync, gen_dummy, get_delta, get_sec, is_close, is_remote, json_serializer::read_json, modified_time, seek_and_length, valid_source, Media, PlayoutConfig, PlayoutStatus, DUMMY_LEN, }; diff --git a/src/main.rs b/ffplayout-engine/src/main.rs similarity index 90% rename from src/main.rs rename to ffplayout-engine/src/main.rs index 1d7394f2..c2db334d 100644 --- a/src/main.rs +++ b/ffplayout-engine/src/main.rs @@ -10,13 +10,23 @@ use serde::{Deserialize, Serialize}; use serde_json::json; use simplelog::*; -use ffplayout_engine::{ +pub mod input; +pub mod output; +pub mod rpc; +// #[cfg(test)] +// mod tests; +pub mod utils; + +use utils::{arg_parse::get_args, get_config}; + +use crate::{ output::{player, write_hls}, rpc::json_rpc_server, - utils::{ - generate_playlist, get_args, init_logging, send_mail, validate_ffmpeg, PlayerControl, - PlayoutConfig, PlayoutStatus, ProcessControl, - }, +}; + +use ffplayout_lib::utils::{ + generate_playlist, init_logging, send_mail, validate_ffmpeg, PlayerControl, PlayoutStatus, + ProcessControl, }; #[derive(Serialize, Deserialize)] @@ -59,7 +69,7 @@ fn status_file(stat_file: &str, playout_stat: &PlayoutStatus) { fn main() { let args = get_args(); - let config = PlayoutConfig::new(Some(args)); + let config = get_config(args); let config_clone = config.clone(); let play_control = PlayerControl::new(); let playout_stat = PlayoutStatus::new(); diff --git a/src/output/desktop.rs b/ffplayout-engine/src/output/desktop.rs similarity index 92% rename from src/output/desktop.rs rename to ffplayout-engine/src/output/desktop.rs index 8ff89ba3..7c88bc06 100644 --- a/src/output/desktop.rs +++ b/ffplayout-engine/src/output/desktop.rs @@ -2,9 +2,9 @@ use std::process::{self, Command, Stdio}; use simplelog::*; -use crate::filter::v_drawtext; -use crate::utils::{Media, PlayoutConfig}; -use crate::vec_strings; +use ffplayout_lib::filter::v_drawtext; +use ffplayout_lib::utils::{Media, PlayoutConfig}; +use ffplayout_lib::vec_strings; /// Desktop Output /// diff --git a/src/output/hls.rs b/ffplayout-engine/src/output/hls.rs similarity index 98% rename from src/output/hls.rs rename to ffplayout-engine/src/output/hls.rs index 4373bfd9..c583625d 100644 --- a/src/output/hls.rs +++ b/ffplayout-engine/src/output/hls.rs @@ -27,13 +27,13 @@ use std::{ use simplelog::*; -use crate::filter::ingest_filter::filter_cmd; use crate::input::{ingest::log_line, source_generator}; -use crate::utils::{ +use ffplayout_lib::filter::ingest_filter::filter_cmd; +use ffplayout_lib::utils::{ prepare_output_cmd, sec_to_time, stderr_reader, Decoder, Ingest, PlayerControl, PlayoutConfig, PlayoutStatus, ProcessControl, }; -use crate::vec_strings; +use ffplayout_lib::vec_strings; /// Ingest Server for HLS fn ingest_to_hls_server( diff --git a/src/output/mod.rs b/ffplayout-engine/src/output/mod.rs similarity index 99% rename from src/output/mod.rs rename to ffplayout-engine/src/output/mod.rs index d2e18f68..337753a1 100644 --- a/src/output/mod.rs +++ b/ffplayout-engine/src/output/mod.rs @@ -16,11 +16,11 @@ mod stream; pub use hls::write_hls; use crate::input::{ingest_server, source_generator}; -use crate::utils::{ +use ffplayout_lib::utils::{ sec_to_time, stderr_reader, Decoder, PlayerControl, PlayoutConfig, PlayoutStatus, ProcessControl, }; -use crate::vec_strings; +use ffplayout_lib::vec_strings; /// Player /// diff --git a/src/output/stream.rs b/ffplayout-engine/src/output/stream.rs similarity index 92% rename from src/output/stream.rs rename to ffplayout-engine/src/output/stream.rs index b61af29b..ae4eec74 100644 --- a/src/output/stream.rs +++ b/ffplayout-engine/src/output/stream.rs @@ -2,9 +2,9 @@ use std::process::{self, Command, Stdio}; use simplelog::*; -use crate::filter::v_drawtext; -use crate::utils::{prepare_output_cmd, Media, PlayoutConfig}; -use crate::vec_strings; +use ffplayout_lib::filter::v_drawtext; +use ffplayout_lib::utils::{prepare_output_cmd, Media, PlayoutConfig}; +use ffplayout_lib::vec_strings; /// Streaming Output /// diff --git a/src/rpc/mod.rs b/ffplayout-engine/src/rpc/mod.rs similarity index 99% rename from src/rpc/mod.rs rename to ffplayout-engine/src/rpc/mod.rs index c96adeaf..9e85bedd 100644 --- a/src/rpc/mod.rs +++ b/ffplayout-engine/src/rpc/mod.rs @@ -11,7 +11,7 @@ use jsonrpc_http_server::{ use serde_json::{json, Map}; use simplelog::*; -use crate::utils::{ +use ffplayout_lib::utils::{ get_delta, get_filter_from_json, get_sec, sec_to_time, write_status, Media, PlayerControl, PlayoutConfig, PlayoutStatus, ProcessControl, }; diff --git a/src/rpc/zmq_cmd.rs b/ffplayout-engine/src/rpc/zmq_cmd.rs similarity index 100% rename from src/rpc/zmq_cmd.rs rename to ffplayout-engine/src/rpc/zmq_cmd.rs diff --git a/src/tests/mod.rs b/ffplayout-engine/src/tests/mod.rs similarity index 98% rename from src/tests/mod.rs rename to ffplayout-engine/src/tests/mod.rs index f629ede5..76b97867 100644 --- a/src/tests/mod.rs +++ b/ffplayout-engine/src/tests/mod.rs @@ -3,12 +3,10 @@ use std::{ time::Duration, }; -mod utils; - #[cfg(test)] use crate::output::player; #[cfg(test)] -use crate::utils::*; +use ffplayout_lib::utils::*; #[cfg(test)] use simplelog::*; diff --git a/src/utils/arg_parse.rs b/ffplayout-engine/src/utils/arg_parse.rs similarity index 100% rename from src/utils/arg_parse.rs rename to ffplayout-engine/src/utils/arg_parse.rs diff --git a/ffplayout-engine/src/utils/mod.rs b/ffplayout-engine/src/utils/mod.rs new file mode 100644 index 00000000..2aa9d0d7 --- /dev/null +++ b/ffplayout-engine/src/utils/mod.rs @@ -0,0 +1,64 @@ +use std::path::Path; + +pub mod arg_parse; + +pub use arg_parse::Args; +use ffplayout_lib::utils::{time_to_sec, PlayoutConfig}; + +pub fn get_config(args: Args) -> PlayoutConfig { + let mut config = PlayoutConfig::new(args.config); + + if let Some(gen) = args.generate { + config.general.generate = Some(gen); + } + + if let Some(log_path) = args.log { + if Path::new(&log_path).is_dir() { + config.logging.log_to_file = true; + } + config.logging.log_path = log_path; + } + + if let Some(playlist) = args.playlist { + config.playlist.path = playlist; + } + + if let Some(mode) = args.play_mode { + config.processing.mode = mode; + } + + if let Some(folder) = args.folder { + config.storage.path = folder; + config.processing.mode = "folder".into(); + } + + if let Some(start) = args.start { + config.playlist.day_start = start.clone(); + config.playlist.start_sec = Some(time_to_sec(&start)); + } + + if let Some(length) = args.length { + config.playlist.length = length.clone(); + + if length.contains(':') { + config.playlist.length_sec = Some(time_to_sec(&length)); + } else { + config.playlist.length_sec = Some(86400.0); + } + } + + if args.infinit { + config.playlist.infinit = args.infinit; + } + + if let Some(output) = args.output { + config.out.mode = output; + } + + if let Some(volume) = args.volume { + config.processing.volume = volume; + } + + config +} +// Read command line arguments, and override the config with them. diff --git a/assets/ffplayout-engine.service b/ffplayout-engine/unit/ffplayout.service similarity index 100% rename from assets/ffplayout-engine.service rename to ffplayout-engine/unit/ffplayout.service diff --git a/lib/Cargo.toml b/lib/Cargo.toml new file mode 100644 index 00000000..c5b76e0d --- /dev/null +++ b/lib/Cargo.toml @@ -0,0 +1,34 @@ +[package] +name = "ffplayout-lib" +description = "Library for ffplayout" +license = "GPL-3.0" +authors = ["Jonathan Baecker jonbae77@gmail.com"] +readme = "README.md" +version = "0.9.9" +edition = "2021" + +[dependencies] +chrono = { git = "https://github.com/sbrocket/chrono", branch = "parse-error-kind-public" } +crossbeam-channel = "0.5" +faccess = "0.2" +ffprobe = "0.3" +file-rotate = { git = "https://github.com/Ploppz/file-rotate.git", branch = "timestamp-parse-fix" } +futures = "0.3" +jsonrpc-http-server = "18.0" +lettre = "0.10.0-rc.7" +log = "0.4" +notify = "4.0" +once_cell = "1.10" +rand = "0.8" +regex = "1" +reqwest = { version = "0.11", features = ["blocking", "json"] } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +serde_yaml = "0.8" +shlex = "1.1" +simplelog = { version = "^0.12", features = ["paris"] } +time = { version = "0.3", features = ["formatting", "macros"] } +walkdir = "2" + +[target.x86_64-unknown-linux-musl.dependencies] +openssl = { version = "0.10", features = ["vendored"] } diff --git a/src/filter/a_loudnorm.rs b/lib/src/filter/a_loudnorm.rs similarity index 100% rename from src/filter/a_loudnorm.rs rename to lib/src/filter/a_loudnorm.rs diff --git a/src/filter/ingest_filter.rs b/lib/src/filter/ingest_filter.rs similarity index 100% rename from src/filter/ingest_filter.rs rename to lib/src/filter/ingest_filter.rs diff --git a/src/filter/mod.rs b/lib/src/filter/mod.rs similarity index 100% rename from src/filter/mod.rs rename to lib/src/filter/mod.rs diff --git a/src/filter/v_drawtext.rs b/lib/src/filter/v_drawtext.rs similarity index 100% rename from src/filter/v_drawtext.rs rename to lib/src/filter/v_drawtext.rs diff --git a/src/filter/v_overlay.rs b/lib/src/filter/v_overlay.rs similarity index 100% rename from src/filter/v_overlay.rs rename to lib/src/filter/v_overlay.rs diff --git a/src/lib.rs b/lib/src/lib.rs similarity index 66% rename from src/lib.rs rename to lib/src/lib.rs index 89174366..da112113 100644 --- a/src/lib.rs +++ b/lib/src/lib.rs @@ -1,12 +1,9 @@ extern crate log; extern crate simplelog; -pub mod api; pub mod filter; -pub mod input; pub mod macros; -pub mod output; -pub mod rpc; +pub mod utils; + #[cfg(test)] mod tests; -pub mod utils; diff --git a/src/macros/mod.rs b/lib/src/macros/mod.rs similarity index 100% rename from src/macros/mod.rs rename to lib/src/macros/mod.rs diff --git a/src/tests/utils/mod.rs b/lib/src/tests/mod.rs similarity index 100% rename from src/tests/utils/mod.rs rename to lib/src/tests/mod.rs diff --git a/src/utils/config.rs b/lib/src/utils/config.rs similarity index 80% rename from src/utils/config.rs rename to lib/src/utils/config.rs index 9ad00436..5371bca9 100644 --- a/src/utils/config.rs +++ b/lib/src/utils/config.rs @@ -8,7 +8,7 @@ use std::{ use serde::{Deserialize, Serialize}; use shlex::split; -use crate::utils::{free_tcp_socket, time_to_sec, Args}; +use crate::utils::{free_tcp_socket, time_to_sec}; use crate::vec_strings; /// Global Config @@ -168,10 +168,10 @@ pub struct Out { impl PlayoutConfig { /// Read config from YAML file, and set some extra config values. - pub fn new(args: Option) -> Self { + pub fn new(cfg_path: Option) -> Self { let mut config_path = PathBuf::from("/etc/ffplayout/ffplayout.yml"); - if let Some(cfg) = args.clone().and_then(|a| a.config) { + if let Some(cfg) = cfg_path { config_path = PathBuf::from(cfg); } @@ -259,61 +259,6 @@ impl PlayoutConfig { config.text.node_pos = None; } - // Read command line arguments, and override the config with them. - - if let Some(arg) = args { - if let Some(gen) = arg.generate { - config.general.generate = Some(gen); - } - - if let Some(log_path) = arg.log { - if Path::new(&log_path).is_dir() { - config.logging.log_to_file = true; - } - config.logging.log_path = log_path; - } - - if let Some(playlist) = arg.playlist { - config.playlist.path = playlist; - } - - if let Some(mode) = arg.play_mode { - config.processing.mode = mode; - } - - if let Some(folder) = arg.folder { - config.storage.path = folder; - config.processing.mode = "folder".into(); - } - - if let Some(start) = arg.start { - config.playlist.day_start = start.clone(); - config.playlist.start_sec = Some(time_to_sec(&start)); - } - - if let Some(length) = arg.length { - config.playlist.length = length.clone(); - - if length.contains(':') { - config.playlist.length_sec = Some(time_to_sec(&length)); - } else { - config.playlist.length_sec = Some(86400.0); - } - } - - if arg.infinit { - config.playlist.infinit = arg.infinit; - } - - if let Some(output) = arg.output { - config.out.mode = output; - } - - if let Some(volume) = arg.volume { - config.processing.volume = volume; - } - } - config } } diff --git a/src/utils/controller.rs b/lib/src/utils/controller.rs similarity index 100% rename from src/utils/controller.rs rename to lib/src/utils/controller.rs diff --git a/src/input/folder.rs b/lib/src/utils/folder.rs similarity index 63% rename from src/input/folder.rs rename to lib/src/utils/folder.rs index a1b10ce6..0aa569f4 100644 --- a/src/input/folder.rs +++ b/lib/src/utils/folder.rs @@ -2,18 +2,11 @@ use std::{ path::Path, process::exit, sync::{ - atomic::{AtomicBool, AtomicUsize, Ordering}, - mpsc::channel, + atomic::{AtomicUsize, Ordering}, {Arc, Mutex}, }, - thread::sleep, - time::Duration, }; -use notify::{ - DebouncedEvent::{Create, Remove, Rename}, - {watcher, RecursiveMode, Watcher}, -}; use rand::{seq::SliceRandom, thread_rng}; use simplelog::*; use walkdir::WalkDir; @@ -153,61 +146,3 @@ impl Iterator for FolderSource { } } } - -/// Create a watcher, which monitor file changes. -/// When a change is register, update the current file list. -/// This makes it possible, to play infinitely and and always new files to it. -pub fn watchman( - config: PlayoutConfig, - is_terminated: Arc, - sources: Arc>>, -) { - let (tx, rx) = channel(); - - let path = config.storage.path; - - if !Path::new(&path).exists() { - error!("Folder path not exists: '{path}'"); - panic!("Folder path not exists: '{path}'"); - } - - let mut watcher = watcher(tx, Duration::from_secs(1)).unwrap(); - watcher.watch(path, RecursiveMode::Recursive).unwrap(); - - while !is_terminated.load(Ordering::SeqCst) { - if let Ok(res) = rx.try_recv() { - match res { - Create(new_path) => { - let index = sources.lock().unwrap().len(); - let media = Media::new(index, new_path.display().to_string(), false); - - sources.lock().unwrap().push(media); - info!("Create new file: {new_path:?}"); - } - Remove(old_path) => { - sources - .lock() - .unwrap() - .retain(|x| x.source != old_path.display().to_string()); - info!("Remove file: {old_path:?}"); - } - Rename(old_path, new_path) => { - let index = sources - .lock() - .unwrap() - .iter() - .position(|x| *x.source == old_path.display().to_string()) - .unwrap(); - - let media = Media::new(index, new_path.display().to_string(), false); - sources.lock().unwrap()[index] = media; - - info!("Rename file: {old_path:?} to {new_path:?}"); - } - _ => (), - } - } - - sleep(Duration::from_secs(5)); - } -} diff --git a/src/utils/generator.rs b/lib/src/utils/generator.rs similarity index 99% rename from src/utils/generator.rs rename to lib/src/utils/generator.rs index 643decc7..c81bcb20 100644 --- a/src/utils/generator.rs +++ b/lib/src/utils/generator.rs @@ -17,7 +17,7 @@ use std::{ use chrono::{Duration, NaiveDate}; use simplelog::*; -use crate::input::FolderSource; +use super::folder::FolderSource; use crate::utils::{json_serializer::JsonPlaylist, time_to_sec, Media, PlayoutConfig}; /// Generate a vector with dates, from given range. diff --git a/src/utils/json_serializer.rs b/lib/src/utils/json_serializer.rs similarity index 100% rename from src/utils/json_serializer.rs rename to lib/src/utils/json_serializer.rs diff --git a/src/utils/json_validate.rs b/lib/src/utils/json_validate.rs similarity index 100% rename from src/utils/json_validate.rs rename to lib/src/utils/json_validate.rs diff --git a/src/utils/logging.rs b/lib/src/utils/logging.rs similarity index 100% rename from src/utils/logging.rs rename to lib/src/utils/logging.rs diff --git a/src/utils/mod.rs b/lib/src/utils/mod.rs similarity index 99% rename from src/utils/mod.rs rename to lib/src/utils/mod.rs index 38ed54e4..3cbc0881 100644 --- a/src/utils/mod.rs +++ b/lib/src/utils/mod.rs @@ -18,15 +18,14 @@ use serde::{Deserialize, Serialize}; use serde_json::json; use simplelog::*; -mod arg_parse; pub mod config; pub mod controller; +pub mod folder; mod generator; pub mod json_serializer; mod json_validate; mod logging; -pub use arg_parse::{get_args, Args}; pub use config::{self as playout_config, PlayoutConfig}; pub use controller::{PlayerControl, PlayoutStatus, ProcessControl, ProcessUnit::*}; pub use generator::generate_playlist; diff --git a/src/api/mod.rs b/src/api/mod.rs deleted file mode 100644 index 78826bad..00000000 --- a/src/api/mod.rs +++ /dev/null @@ -1,10 +0,0 @@ -pub mod args_parse; -pub mod auth; -pub mod control; -pub mod errors; -pub mod files; -pub mod handles; -pub mod models; -pub mod playlist; -pub mod routes; -pub mod utils; From b119849be4e63573d28f71406c46673638f5be34 Mon Sep 17 00:00:00 2001 From: jb-alvarado Date: Wed, 22 Jun 2022 18:00:31 +0200 Subject: [PATCH 40/47] add file move/copy, add file delete and file upload --- Cargo.lock | 47 ++++++ ffplayout-api/Cargo.toml | 3 + ffplayout-api/src/main.rs | 10 +- ffplayout-api/src/utils/errors.rs | 32 +++- ffplayout-api/src/utils/files.rs | 240 ++++++++++++++++++++++++++++-- ffplayout-api/src/utils/routes.rs | 43 +++++- 6 files changed, 355 insertions(+), 20 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ca8d56a2..53720a90 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -77,6 +77,24 @@ dependencies = [ "syn", ] +[[package]] +name = "actix-multipart" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9edfb0e7663d7fe18c8d5b668c9c1bcf79176b1dcc9d4da9592503209a6bfb0" +dependencies = [ + "actix-utils", + "actix-web", + "bytes", + "derive_more", + "futures-core", + "httparse", + "local-waker", + "log", + "mime", + "twoway", +] + [[package]] name = "actix-router" version = "0.5.0" @@ -982,6 +1000,7 @@ dependencies = [ name = "ffplayout-api" version = "0.3.0" dependencies = [ + "actix-multipart", "actix-web", "actix-web-grants", "actix-web-httpauth", @@ -992,6 +1011,7 @@ dependencies = [ "faccess", "ffplayout-lib", "ffprobe", + "futures-util", "jsonwebtoken", "log", "once_cell", @@ -1001,6 +1021,7 @@ dependencies = [ "regex", "relative-path", "reqwest", + "sanitize-filename", "serde", "serde_json", "serde_yaml", @@ -2466,6 +2487,16 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "sanitize-filename" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf18934a12018228c5b55a6dae9df5d0641e3566b3630cb46cc55564068e7c2f" +dependencies = [ + "lazy_static", + "regex", +] + [[package]] name = "schannel" version = "0.1.20" @@ -2996,12 +3027,28 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" +[[package]] +name = "twoway" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c57ffb460d7c24cd6eda43694110189030a3d1dfe418416d9468fd1c1d290b47" +dependencies = [ + "memchr", + "unchecked-index", +] + [[package]] name = "typenum" version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" +[[package]] +name = "unchecked-index" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eeba86d422ce181a719445e51872fa30f1f7413b62becb52e95ec91aa262d85c" + [[package]] name = "unicase" version = "2.6.0" diff --git a/ffplayout-api/Cargo.toml b/ffplayout-api/Cargo.toml index c4183d13..72fc94f9 100644 --- a/ffplayout-api/Cargo.toml +++ b/ffplayout-api/Cargo.toml @@ -9,6 +9,7 @@ edition = "2021" [dependencies] ffplayout-lib = { path = "../lib" } +actix-multipart = "0.4" actix-web = "4" actix-web-grants = "3" actix-web-httpauth = "0.6" @@ -18,6 +19,7 @@ clap = { version = "3.2", features = ["derive"] } derive_more = "0.99" faccess = "0.2" ffprobe = "0.3" +futures-util = { version = "0.3", default-features = false, features = ["std"] } jsonwebtoken = "8" log = "0.4" once_cell = "1.10" @@ -26,6 +28,7 @@ rand_core = { version = "0.6", features = ["std"] } relative-path = "1.6" regex = "1" reqwest = { version = "0.11", features = ["blocking", "json"] } +sanitize-filename = "0.3" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" serde_yaml = "0.8" diff --git a/ffplayout-api/src/main.rs b/ffplayout-api/src/main.rs index fc1ec396..64a5daa5 100644 --- a/ffplayout-api/src/main.rs +++ b/ffplayout-api/src/main.rs @@ -17,8 +17,9 @@ use utils::{ routes::{ add_preset, add_user, del_playlist, file_browser, gen_playlist, get_playlist, get_playout_config, get_presets, get_settings, jump_to_last, jump_to_next, login, - media_current, media_last, media_next, patch_settings, reset_playout, save_playlist, - send_text_message, update_playout_config, update_preset, update_user, + media_current, media_last, media_next, move_rename, patch_settings, remove, reset_playout, + save_file, save_playlist, send_text_message, update_playout_config, update_preset, + update_user, }, run_args, Role, }; @@ -95,7 +96,10 @@ async fn main() -> std::io::Result<()> { .service(save_playlist) .service(gen_playlist) .service(del_playlist) - .service(file_browser), + .service(file_browser) + .service(move_rename) + .service(remove) + .service(save_file), ) }) .bind((addr, port))? diff --git a/ffplayout-api/src/utils/errors.rs b/ffplayout-api/src/utils/errors.rs index 8c31bbe0..2bb20ab9 100644 --- a/ffplayout-api/src/utils/errors.rs +++ b/ffplayout-api/src/utils/errors.rs @@ -1,4 +1,4 @@ -use actix_web::{error::ResponseError, HttpResponse}; +use actix_web::{error::ResponseError, Error, HttpResponse}; use derive_more::Display; #[derive(Debug, Display)] @@ -29,3 +29,33 @@ impl ResponseError for ServiceError { } } } + +impl From for ServiceError { + fn from(err: String) -> ServiceError { + ServiceError::BadRequest(err) + } +} + +impl From for ServiceError { + fn from(err: Error) -> ServiceError { + ServiceError::BadRequest(err.to_string()) + } +} + +impl From for ServiceError { + fn from(err: actix_multipart::MultipartError) -> ServiceError { + ServiceError::BadRequest(err.to_string()) + } +} + +impl From for ServiceError { + fn from(err: std::io::Error) -> ServiceError { + ServiceError::BadRequest(err.to_string()) + } +} + +impl From for ServiceError { + fn from(err: actix_web::error::BlockingError) -> ServiceError { + ServiceError::BadRequest(err.to_string()) + } +} diff --git a/ffplayout-api/src/utils/files.rs b/ffplayout-api/src/utils/files.rs index 4343d4ef..1e04cd2a 100644 --- a/ffplayout-api/src/utils/files.rs +++ b/ffplayout-api/src/utils/files.rs @@ -1,6 +1,15 @@ +use std::{ + fs, + io::Write, + path::{Path, PathBuf}, +}; + +use actix_multipart::Multipart; +use actix_web::{web, HttpResponse}; +use futures_util::TryStreamExt as _; +use rand::{distributions::Alphanumeric, Rng}; use relative_path::RelativePath; use serde::{Deserialize, Serialize}; -use std::{fs, path::PathBuf}; use simplelog::*; @@ -9,28 +18,32 @@ use ffplayout_lib::utils::file_extension; #[derive(Debug, Deserialize, Serialize, Clone)] pub struct PathObject { - root: String, - #[serde(skip_deserializing)] - folders: Vec, - #[serde(skip_deserializing)] - files: Vec, + pub source: String, + folders: Option>, + files: Option>, } impl PathObject { - fn new(root: String) -> Self { + fn new(source: String) -> Self { Self { - root, - folders: vec![], - files: vec![], + source, + folders: Some(vec![]), + files: Some(vec![]), } } } +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct MoveObject { + source: String, + target: String, +} + pub async fn browser(id: i64, path_obj: &PathObject) -> Result { let (config, _) = playout_config(&id).await?; let path = PathBuf::from(config.storage.path); let extensions = config.storage.extensions; - let path_component = RelativePath::new(&path_obj.root) + let path_component = RelativePath::new(&path_obj.source) .normalize() .to_string() .replace("../", ""); @@ -57,11 +70,15 @@ pub async fn browser(id: i64, path_obj: &PathObject) -> Result Result Result { +// match fs::copy(&source, &target) { +// Ok(_) => { +// if let Err(e) = fs::remove_file(source) { +// error!("{e}"); +// return Err(ServiceError::BadRequest( +// "Removing File not possible!".into(), +// )); +// }; + +// return Ok(PathObject::new(target.display().to_string())); +// } +// Err(e) => { +// error!("{e}"); +// Err(ServiceError::BadRequest("Error in file copy!".into())) +// } +// } +// } + +fn rename(source: &PathBuf, target: &PathBuf) -> Result { + match fs::rename(&source, &target) { + Ok(_) => Ok(MoveObject { + source: source.display().to_string(), + target: target.display().to_string(), + }), + Err(e) => { + error!("{e}"); + Err(ServiceError::BadRequest("Rename failed!".into())) + } + } +} + +pub async fn rename_file(id: i64, move_object: &MoveObject) -> Result { + let (config, _) = playout_config(&id).await?; + let path = PathBuf::from(&config.storage.path); + let source = RelativePath::new(&move_object.source) + .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(); + + if !source_path.starts_with(&relativ_path) { + source_path = path.join(source); + } else { + source_path = path.join(source_path.strip_prefix(&relativ_path).unwrap()); + } + + if !target_path.starts_with(&relativ_path) { + target_path = path.join(target); + } else { + target_path = path.join(target_path.strip_prefix(relativ_path).unwrap()); + } + + if !source_path.exists() { + return Err(ServiceError::BadRequest("Source file not exist!".into())); + } + + if (source_path.is_dir() || source_path.is_file()) && source_path.parent() == Some(&target_path) + { + return rename(&source_path, &target_path); + } + + if target_path.is_dir() { + target_path = target_path.join(source_path.file_name().unwrap()); + } + + if target_path.is_file() { + return Err(ServiceError::BadRequest( + "Target file already exists!".into(), + )); + } + + if source_path.is_file() && target_path.parent().is_some() { + return rename(&source_path, &target_path); + } + + Err(ServiceError::InternalServerError) +} + +pub async fn remove_file_or_folder(id: i64, source_path: &str) -> Result<(), ServiceError> { + let (config, _) = playout_config(&id).await?; + let source = PathBuf::from(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() { + return Err(ServiceError::BadRequest("Source does not exists!".into())); + } + + if source.is_dir() { + match fs::remove_dir(source) { + Ok(_) => return Ok(()), + Err(e) => { + error!("{e}"); + return Err(ServiceError::BadRequest( + "Delete folder failed! (Folder must be empty)".into(), + )); + } + }; + } + + if source.is_file() { + match fs::remove_file(source) { + Ok(_) => return Ok(()), + Err(e) => { + error!("{e}"); + return Err(ServiceError::BadRequest("Delete file failed!".into())); + } + }; + } + + Err(ServiceError::InternalServerError) +} + +async fn valid_path(id: i64, path: &str) -> Result<(), ServiceError> { + let (config, _) = playout_config(&id).await?; + + let test_target = RelativePath::new(&path) + .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())); + } + + Ok(()) +} + +pub async fn upload(id: i64, mut payload: Multipart) -> Result { + while let Some(mut field) = payload.try_next().await? { + let content_disposition = field.content_disposition(); + println!("{content_disposition}"); + let rand_string: String = rand::thread_rng() + .sample_iter(&Alphanumeric) + .take(20) + .map(char::from) + .collect(); + let path_name = content_disposition.get_name().unwrap_or(&rand_string); + let filename = content_disposition + .get_filename() + .map_or_else(|| rand_string.to_string(), sanitize_filename::sanitize); + + if let Err(e) = valid_path(id, path_name).await { + return Err(e); + } + + let filepath = PathBuf::from(path_name).join(filename); + + if filepath.is_file() { + return Err(ServiceError::BadRequest("Target already exists!".into())); + } + + // File::create is blocking operation, use threadpool + let mut f = web::block(|| std::fs::File::create(filepath)).await??; + + while let Some(chunk) = field.try_next().await? { + f = web::block(move || f.write_all(&chunk).map(|_| f)).await??; + } + } + + Ok(HttpResponse::Ok().into()) +} diff --git a/ffplayout-api/src/utils/routes.rs b/ffplayout-api/src/utils/routes.rs index 3c98ba09..47088d21 100644 --- a/ffplayout-api/src/utils/routes.rs +++ b/ffplayout-api/src/utils/routes.rs @@ -1,6 +1,7 @@ use std::collections::HashMap; -use actix_web::{delete, get, http::StatusCode, patch, post, put, web, Responder}; +use actix_multipart::Multipart; +use actix_web::{delete, get, http::StatusCode, patch, post, put, web, HttpResponse, Responder}; use actix_web_grants::{permissions::AuthDetails, proc_macro::has_any_role}; use argon2::{ password_hash::{rand_core::OsRng, PasswordHash, SaltString}, @@ -13,7 +14,7 @@ use crate::utils::{ auth::{create_jwt, Claims}, control::{control_state, media_info, send_message}, errors::ServiceError, - files::{browser, PathObject}, + files::{browser, remove_file_or_folder, rename_file, upload, MoveObject, PathObject}, handles::{ db_add_preset, db_add_user, db_get_presets, db_get_settings, db_login, db_role, db_update_preset, db_update_settings, db_update_user, @@ -415,7 +416,7 @@ pub async fn del_playlist( /// /// ---------------------------------------------------------------------------- -/// curl -X get http://localhost:8080/api/file/1/browse +/// curl -X GET http://localhost:8080/api/file/1/browse/ /// --header 'Content-Type: application/json' --header 'Authorization: ' #[post("/file/{id}/browse/")] #[has_any_role("Role::Admin", "Role::User", type = "Role")] @@ -428,3 +429,39 @@ pub async fn file_browser( Err(e) => Err(e), } } + +/// curl -X POST http://localhost:8080/api/file/1/move/ +/// --header 'Content-Type: application/json' --header 'Authorization: ' +/// -d '{"source": "", "target": ""}' +#[post("/file/{id}/move/")] +#[has_any_role("Role::Admin", "Role::User", type = "Role")] +pub async fn move_rename( + id: web::Path, + data: web::Json, +) -> Result { + match rename_file(*id, &data.into_inner()).await { + Ok(obj) => Ok(web::Json(obj)), + Err(e) => Err(e), + } +} + +/// curl -X DELETE http://localhost:8080/api/file/1/remove/ +/// --header 'Content-Type: application/json' --header 'Authorization: ' +/// -d '{"source": "", "target": ""}' +#[delete("/file/{id}/remove/")] +#[has_any_role("Role::Admin", "Role::User", type = "Role")] +pub async fn remove( + id: web::Path, + data: web::Json, +) -> Result { + match remove_file_or_folder(*id, &data.into_inner().source).await { + Ok(obj) => Ok(web::Json(obj)), + Err(e) => Err(e), + } +} + +#[post("/file/{id}/upload/")] +#[has_any_role("Role::Admin", "Role::User", type = "Role")] +async fn save_file(id: web::Path, payload: Multipart) -> Result { + upload(*id, payload).await +} From 7f83b5439360dcf4b35757565e3007c049edd4a6 Mon Sep 17 00:00:00 2001 From: jb-alvarado Date: Wed, 22 Jun 2022 18:02:34 +0200 Subject: [PATCH 41/47] add debug --- ffplayout-api/src/utils/files.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ffplayout-api/src/utils/files.rs b/ffplayout-api/src/utils/files.rs index 1e04cd2a..723f9250 100644 --- a/ffplayout-api/src/utils/files.rs +++ b/ffplayout-api/src/utils/files.rs @@ -252,7 +252,7 @@ async fn valid_path(id: i64, path: &str) -> Result<(), ServiceError> { pub async fn upload(id: i64, mut payload: Multipart) -> Result { while let Some(mut field) = payload.try_next().await? { let content_disposition = field.content_disposition(); - println!("{content_disposition}"); + debug!("{content_disposition}"); let rand_string: String = rand::thread_rng() .sample_iter(&Alphanumeric) .take(20) From 15d635a27b5057f5b39f45a05096d587ef278de7 Mon Sep 17 00:00:00 2001 From: jb-alvarado Date: Wed, 22 Jun 2022 21:25:30 +0200 Subject: [PATCH 42/47] update readme --- README.md | 50 +--------- ffplayout-api/README.md | 186 +++++++++++++++++++++++++++++++++++++ ffplayout-engine/README.md | 40 ++++++++ 3 files changed, 231 insertions(+), 45 deletions(-) diff --git a/README.md b/README.md index e404fa23..80d6ea7f 100644 --- a/README.md +++ b/README.md @@ -3,9 +3,7 @@ [![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0) -The main purpose of ffplayout is to provide a 24/7 broadcasting solution that plays a *json* playlist for every day, while keeping the current playlist editable. - -**Check [ffplayout-frontend](https://github.com/ffplayout/ffplayout-frontend): web-based GUI for ffplayout** +[ffplayout](/ffplayout-engine/README.md) is 24/7 broadcasting solution. It can playout a folder with containing video clips, or play for every day a *JSON* playlist, while keeping the current playlist editable. **Features** ----- @@ -43,6 +41,10 @@ The main purpose of ffplayout is to provide a 24/7 broadcasting solution that pl - JSON RPC server, for getting infos about current playing and controlling - [live ingest](/docs/live_ingest.md) +**ffplayout-api (ffpapi)** +----- +ffpapi is an [REST API](/ffplayout-api/README.md) for controlling the engine, manipulate playlists, add settings etc. + Requirements ----- @@ -166,45 +168,3 @@ Output from `{"media":"current"}` show: } ``` When you are in playlist mode and jumping forward or backwards in time, the time shift will be saved so the playlist is still in sync. But have in mind, that then maybe your playlist gets to short. When you are not resetting the state, it will reset on the next day automatically. - ------ - -Installation under Linux ------ - -- copy the binary to `/usr/bin/` -- copy **assets/ffplayout.yml** to `/etc/ffplayout` -- copy **assets/ffplayout-engine.service** to `/etc/systemd/system` -- activate service and run it: `systemctl enable --now ffplayout-engine` - -You can also install the released ***.deb** or ***.rpm** package. - -Start with Arguments ------ - -ffplayout also allows the passing of parameters: - -``` -OPTIONS: - -c, --config File path to ffplayout.conf - -f, --folder Play folder content - -g, --generate ... Generate playlist for date or date-range, like: 2022-01-01 - 2022-01-10: - -h, --help Print help information - -i, --infinit Loop playlist infinitely - -l, --log File path for logging - -m, --play-mode Playing mode: folder, playlist - -o, --output Set output mode: desktop, hls, stream - -p, --playlist Path from playlist - -s, --start Start time in 'hh:mm:ss', 'now' for start with first - -t, --length Set length in 'hh:mm:ss', 'none' for no length check - -v, --volume Set audio volume - -V, --version Print version information - -``` - - -You can run the command like: - -```Bash -./ffplayout -l none -p ~/playlist.json -o desktop -``` diff --git a/ffplayout-api/README.md b/ffplayout-api/README.md index c247225c..11403090 100644 --- a/ffplayout-api/README.md +++ b/ffplayout-api/README.md @@ -1,2 +1,188 @@ **ffplayout-api** ================ + +ffplayout-api (ffpapi) is a on strict REST API for ffplayout. It makes it possible to control the engine, read and manipulate the config, save playlist, etc. + +To be able to use the API it is necessary to initialize the settings database first. To do that, run: + +```BASH +ffpapi -i +``` + +Then add an admin user: + +```BASH +ffpapi -u -p -e +``` + +Then run the API thru the systemd service, or like: + +```BASH +ffpapi -l 127.0.0.1:8080 +``` + +### Possible endpoints + +For all endpoints an (Bearer) authentication is required.\ +`{id}` represent the channel id, and at default is 1. + +#### Login is + +- **POST** `/auth/login/`\ +JSON Data: `'{"username": "", "password": ""`\ +JSON Response: +```JSON +{ + "message": "login correct!", + "status": 200, + "data": { + "id": 1, + "email": "user@example.org", + "username": "user", + "token": "" + } +} +``` + +From here on all request **must** contain the authorization header:\ +`"Authorization: Bearer "` + +#### User + +- **PUT** `/api/user/{user id}`\ +JSON Data: `'{"email": "", "password": ""}'` + +- **POST** `/api/user/`\ +JSON Data: +```JSON +{ + "email": "", + "username": "", + "password": "", + "role_id": 1 +} +``` + +#### API Settings + +- **GET** `/api/settings/{id}`\ +HEADER: +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 + +- **GET** `/api/playout/config/{id}`\ +Response is in JSON format + +- **PUT** `/api/playout/config/{id}`\ +JSON Data: `{ }`\ +Response is in TEXT format + +#### Text Presets + +- **GET** `/api/presets/`\ +Response is in JSON format + +- **PUT** `/api/playout/presets/{id}`\ +JSON Data: +```JSON +{ + "name": "", + "text": "", + "x": "", + "y": "", + "fontsize": 24, + "line_spacing": 4, + "fontcolor": "#ffffff", + "box": 1, + "boxcolor": "#000000", + "boxborderw": 4, + "alpha": "" +} + +``` +Response is in TEXT format + +- **POST** `/api/playout/presets/`\ +JSON 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/`\ +Response is in TEXT format + +- **POST** `api/control/{id}/playout/back/`\ +Response is in TEXT format + +- **POST** `api/control/{id}/playout/reset/`\ +Response is in TEXT format + +- **GET** `/api/control/{id}/media/current/`\ +Response is in JSON format + +- **GET** `/api/control/{id}/media/next/`\ +Response is in JSON format + +- **GET** `/api/control/{id}/media/last/`\ +Response is in JSON format + +#### Playlist Operations + +- **GET** `/api/playlist/{id}/2022-06-20`\ +Response is in JSON format + +- **POST** `/api/playlist/1/`\ +JSON Data: `{ }`\ +Response is in TEXT format + +- **GET** `/api/playlist/{id}/generate/2022-06-20`\ +Response is in JSON format + +- **DELETE** `/api/playlist/{id}/2022-06-20`\ +Response is in TEXT format + +#### File Operations + +- **GET** `/api/file/{id}/browse/`\ +Response is in JSON format + +- **POST** `/api/file/{id}/move/`\ +JSON Data: `{"source": "", "target": ""}`\ +Response is in JSON format + +- **DELETE** `/api/file/{id}/remove/`\ +JSON Data: `{"source": ""}`\ +Response is in JSON format + +- **POST** `/file/{id}/upload/`\ +Multipart Form: `name=, filename=`\ +Response is in TEXT format diff --git a/ffplayout-engine/README.md b/ffplayout-engine/README.md index 29590fe5..ca3da1f5 100644 --- a/ffplayout-engine/README.md +++ b/ffplayout-engine/README.md @@ -1,2 +1,42 @@ **ffplayout-engine** ================ + +Installation under Linux +----- + +- copy the binary to `/usr/bin/` +- copy **assets/ffplayout.yml** to `/etc/ffplayout` +- copy **assets/ffplayout-engine.service** to `/etc/systemd/system` +- activate service and run it: `systemctl enable --now ffplayout-engine` + +You can also install the [released](https://github.com/ffplayout/ffplayout-engine/releases/latest) ***.deb** or ***.rpm** package. + +Start with Arguments +----- + +ffplayout also allows the passing of parameters: + +``` +OPTIONS: + -c, --config File path to ffplayout.conf + -f, --folder Play folder content + -g, --generate ... Generate playlist for date or date-range, like: 2022-01-01 - 2022-01-10: + -h, --help Print help information + -i, --infinit Loop playlist infinitely + -l, --log File path for logging + -m, --play-mode Playing mode: folder, playlist + -o, --output Set output mode: desktop, hls, stream + -p, --playlist Path from playlist + -s, --start Start time in 'hh:mm:ss', 'now' for start with first + -t, --length Set length in 'hh:mm:ss', 'none' for no length check + -v, --volume Set audio volume + -V, --version Print version information + +``` + + +You can run the command like: + +```Bash +./ffplayout -l none -p ~/playlist.json -o desktop +``` From ca48a093a53a593128eb0f7a84c98fbda5bb2cdd Mon Sep 17 00:00:00 2001 From: jb-alvarado Date: Wed, 22 Jun 2022 21:25:46 +0200 Subject: [PATCH 43/47] spelling, reorganize code --- ffplayout-api/src/utils/routes.rs | 342 +++++++++++++++--------------- 1 file changed, 171 insertions(+), 171 deletions(-) diff --git a/ffplayout-api/src/utils/routes.rs b/ffplayout-api/src/utils/routes.rs index 47088d21..de98af8d 100644 --- a/ffplayout-api/src/utils/routes.rs +++ b/ffplayout-api/src/utils/routes.rs @@ -32,176 +32,6 @@ struct ResponseObj { data: Option, } -/// curl -X GET http://127.0.0.1:8080/api/settings/1 -H "Authorization: Bearer " -#[get("/settings/{id}")] -#[has_any_role("Role::Admin", "Role::User", type = "Role")] -async fn get_settings(id: web::Path) -> Result { - if let Ok(settings) = db_get_settings(&id).await { - return Ok(web::Json(ResponseObj { - message: format!("Settings from {}", settings.channel_name), - status: 200, - data: Some(settings), - })); - } - - Err(ServiceError::InternalServerError) -} - -/// curl -X PATCH http://127.0.0.1:8080/api/settings/1 -H "Content-Type: application/json" \ -/// --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"}' \ -/// -H "Authorization: Bearer " -#[patch("/settings/{id}")] -#[has_any_role("Role::Admin", type = "Role")] -async fn patch_settings( - id: web::Path, - data: web::Json, -) -> Result { - if db_update_settings(*id, data.into_inner()).await.is_ok() { - return Ok("Update Success"); - }; - - Err(ServiceError::InternalServerError) -} - -/// curl -X GET http://localhost:8080/api/playout/config/1 --header 'Authorization: ' -#[get("/playout/config/{id}")] -#[has_any_role("Role::Admin", "Role::User", type = "Role")] -async fn get_playout_config( - id: web::Path, - _details: AuthDetails, -) -> Result { - if let Ok(settings) = db_get_settings(&id).await { - if let Ok(config) = read_playout_config(&settings.config_path) { - return Ok(web::Json(config)); - } - }; - - Err(ServiceError::InternalServerError) -} - -/// curl -X PUT http://localhost:8080/api/playout/config/1 -H "Content-Type: application/json" \ -/// --data { } --header 'Authorization: ' -#[put("/playout/config/{id}")] -#[has_any_role("Role::Admin", type = "Role")] -async fn update_playout_config( - id: web::Path, - data: web::Json, -) -> Result { - if let Ok(settings) = db_get_settings(&id).await { - if let Ok(f) = std::fs::OpenOptions::new() - .write(true) - .truncate(true) - .open(&settings.config_path) - { - serde_yaml::to_writer(f, &data).unwrap(); - - return Ok("Update playout config success."); - } else { - return Err(ServiceError::InternalServerError); - }; - }; - - Err(ServiceError::InternalServerError) -} - -/// curl -X PUT http://localhost:8080/api/presets/ --header 'Content-Type: application/json' \ -/// --data '{"email": "", "password": ""}' --header 'Authorization: ' -#[get("/presets/")] -#[has_any_role("Role::Admin", "Role::User", type = "Role")] -async fn get_presets() -> Result { - if let Ok(presets) = db_get_presets().await { - return Ok(web::Json(presets)); - } - - Err(ServiceError::InternalServerError) -} - -/// curl -X PUT http://localhost:8080/api/presets/1 --header 'Content-Type: application/json' \ -/// --data '{"name": "", "text": "TEXT>", "x": "", "y": "", "fontsize": 24, \ -/// "line_spacing": 4, "fontcolor": "#ffffff", "box": 1, "boxcolor": "#000000", "boxborderw": 4, "alpha": 1.0}}' \ -/// --header 'Authorization: ' -#[put("/presets/{id}")] -#[has_any_role("Role::Admin", "Role::User", type = "Role")] -async fn update_preset( - id: web::Path, - data: web::Json, -) -> Result { - if db_update_preset(&id, data.into_inner()).await.is_ok() { - return Ok("Update Success"); - } - - Err(ServiceError::InternalServerError) -} - -/// curl -X POST http://localhost:8080/api/presets/ --header 'Content-Type: application/json' \ -/// --data '{"name": "", "text": "TEXT>", "x": "", "y": "", "fontsize": 24, \ -/// "line_spacing": 4, "fontcolor": "#ffffff", "box": 1, "boxcolor": "#000000", "boxborderw": 4, "alpha": 1.0}}' \ -/// --header 'Authorization: ' -#[post("/presets/")] -#[has_any_role("Role::Admin", "Role::User", type = "Role")] -async fn add_preset(data: web::Json) -> Result { - if db_add_preset(data.into_inner()).await.is_ok() { - return Ok("Add preset Success"); - } - - Err(ServiceError::InternalServerError) -} - -/// curl -X PUT http://localhost:8080/api/user/1 --header 'Content-Type: application/json' \ -/// --data '{"email": "", "password": ""}' --header 'Authorization: ' -#[put("/user/{id}")] -#[has_any_role("Role::Admin", "Role::User", type = "Role")] -async fn update_user( - id: web::Path, - user: web::ReqData, - data: web::Json, -) -> Result { - if id.into_inner() == user.id { - let mut fields = String::new(); - - if let Some(email) = data.email.clone() { - fields.push_str(format!("email = '{email}'").as_str()); - } - - if !data.password.is_empty() { - if !fields.is_empty() { - fields.push_str(", "); - } - - let salt = SaltString::generate(&mut OsRng); - let password_hash = Argon2::default() - .hash_password(data.password.clone().as_bytes(), &salt) - .unwrap(); - - fields.push_str(format!("password = '{}', salt = '{salt}'", password_hash).as_str()); - } - - if db_update_user(user.id, fields).await.is_ok() { - return Ok("Update Success"); - }; - - return Err(ServiceError::InternalServerError); - } - - Err(ServiceError::Unauthorized) -} - -/// curl -X POST 'http://localhost:8080/api/user/' --header 'Content-Type: application/json' \ -/// -d '{"email": "", "username": "", "password": "", "role_id": 1}' \ -/// --header 'Authorization: Bearer ' -#[post("/user/")] -#[has_any_role("Role::Admin", type = "Role")] -async fn add_user(data: web::Json) -> Result { - match db_add_user(data.into_inner()).await { - Ok(_) => Ok("Add User Success"), - Err(e) => { - error!("{e}"); - Err(ServiceError::InternalServerError) - } - } -} - /// curl -X POST http://127.0.0.1:8080/auth/login/ -H "Content-Type: application/json" \ /// -d '{"username": "", "password": "" }' #[post("/auth/login/")] @@ -259,6 +89,176 @@ pub async fn login(credentials: web::Json) -> impl Responder { } } +/// curl -X PUT http://localhost:8080/api/user/1 --header 'Content-Type: application/json' \ +/// --data '{"email": "", "password": ""}' --header 'Authorization: ' +#[put("/user/{id}")] +#[has_any_role("Role::Admin", "Role::User", type = "Role")] +async fn update_user( + id: web::Path, + user: web::ReqData, + data: web::Json, +) -> Result { + if id.into_inner() == user.id { + let mut fields = String::new(); + + if let Some(email) = data.email.clone() { + fields.push_str(format!("email = '{email}'").as_str()); + } + + if !data.password.is_empty() { + if !fields.is_empty() { + fields.push_str(", "); + } + + let salt = SaltString::generate(&mut OsRng); + let password_hash = Argon2::default() + .hash_password(data.password.clone().as_bytes(), &salt) + .unwrap(); + + fields.push_str(format!("password = '{}', salt = '{salt}'", password_hash).as_str()); + } + + if db_update_user(user.id, fields).await.is_ok() { + return Ok("Update Success"); + }; + + return Err(ServiceError::InternalServerError); + } + + Err(ServiceError::Unauthorized) +} + +/// curl -X POST 'http://localhost:8080/api/user/' --header 'Content-Type: application/json' \ +/// -d '{"email": "", "username": "", "password": "", "role_id": 1}' \ +/// --header 'Authorization: Bearer ' +#[post("/user/")] +#[has_any_role("Role::Admin", type = "Role")] +async fn add_user(data: web::Json) -> Result { + match db_add_user(data.into_inner()).await { + Ok(_) => Ok("Add User Success"), + Err(e) => { + error!("{e}"); + Err(ServiceError::InternalServerError) + } + } +} + +/// curl -X GET http://127.0.0.1:8080/api/settings/1 -H "Authorization: Bearer " +#[get("/settings/{id}")] +#[has_any_role("Role::Admin", "Role::User", type = "Role")] +async fn get_settings(id: web::Path) -> Result { + if let Ok(settings) = db_get_settings(&id).await { + return Ok(web::Json(ResponseObj { + message: format!("Settings from {}", settings.channel_name), + status: 200, + data: Some(settings), + })); + } + + Err(ServiceError::InternalServerError) +} + +/// curl -X PATCH http://127.0.0.1:8080/api/settings/1 -H "Content-Type: application/json" \ +/// --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"}' \ +/// -H "Authorization: Bearer " +#[patch("/settings/{id}")] +#[has_any_role("Role::Admin", type = "Role")] +async fn patch_settings( + id: web::Path, + data: web::Json, +) -> Result { + if db_update_settings(*id, data.into_inner()).await.is_ok() { + return Ok("Update Success"); + }; + + Err(ServiceError::InternalServerError) +} + +/// curl -X GET http://localhost:8080/api/playout/config/1 --header 'Authorization: ' +#[get("/playout/config/{id}")] +#[has_any_role("Role::Admin", "Role::User", type = "Role")] +async fn get_playout_config( + id: web::Path, + _details: AuthDetails, +) -> Result { + if let Ok(settings) = db_get_settings(&id).await { + if let Ok(config) = read_playout_config(&settings.config_path) { + return Ok(web::Json(config)); + } + }; + + Err(ServiceError::InternalServerError) +} + +/// curl -X PUT http://localhost:8080/api/playout/config/1 -H "Content-Type: application/json" \ +/// --data { } --header 'Authorization: ' +#[put("/playout/config/{id}")] +#[has_any_role("Role::Admin", type = "Role")] +async fn update_playout_config( + id: web::Path, + data: web::Json, +) -> Result { + if let Ok(settings) = db_get_settings(&id).await { + if let Ok(f) = std::fs::OpenOptions::new() + .write(true) + .truncate(true) + .open(&settings.config_path) + { + serde_yaml::to_writer(f, &data).unwrap(); + + return Ok("Update playout config success."); + } else { + return Err(ServiceError::InternalServerError); + }; + }; + + Err(ServiceError::InternalServerError) +} + +/// curl -X GET http://localhost:8080/api/presets/ --header 'Content-Type: application/json' \ +/// --data '{"email": "", "password": ""}' --header 'Authorization: ' +#[get("/presets/")] +#[has_any_role("Role::Admin", "Role::User", type = "Role")] +async fn get_presets() -> Result { + if let Ok(presets) = db_get_presets().await { + return Ok(web::Json(presets)); + } + + Err(ServiceError::InternalServerError) +} + +/// curl -X PUT http://localhost:8080/api/presets/1 --header 'Content-Type: application/json' \ +/// --data '{"name": "", "text": "", "x": "", "y": "", "fontsize": 24, \ +/// "line_spacing": 4, "fontcolor": "#ffffff", "box": 1, "boxcolor": "#000000", "boxborderw": 4, "alpha": 1.0}' \ +/// --header 'Authorization: ' +#[put("/presets/{id}")] +#[has_any_role("Role::Admin", "Role::User", type = "Role")] +async fn update_preset( + id: web::Path, + data: web::Json, +) -> Result { + if db_update_preset(&id, data.into_inner()).await.is_ok() { + return Ok("Update Success"); + } + + Err(ServiceError::InternalServerError) +} + +/// curl -X POST http://localhost:8080/api/presets/ --header 'Content-Type: application/json' \ +/// --data '{"name": "", "text": "TEXT>", "x": "", "y": "", "fontsize": 24, \ +/// "line_spacing": 4, "fontcolor": "#ffffff", "box": 1, "boxcolor": "#000000", "boxborderw": 4, "alpha": 1.0}}' \ +/// --header 'Authorization: ' +#[post("/presets/")] +#[has_any_role("Role::Admin", "Role::User", type = "Role")] +async fn add_preset(data: web::Json) -> Result { + if db_add_preset(data.into_inner()).await.is_ok() { + return Ok("Add preset Success"); + } + + Err(ServiceError::InternalServerError) +} + /// ---------------------------------------------------------------------------- /// ffplayout process controlling /// @@ -447,7 +447,7 @@ pub async fn move_rename( /// curl -X DELETE http://localhost:8080/api/file/1/remove/ /// --header 'Content-Type: application/json' --header 'Authorization: ' -/// -d '{"source": "", "target": ""}' +/// -d '{"source": ""}' #[delete("/file/{id}/remove/")] #[has_any_role("Role::Admin", "Role::User", type = "Role")] pub async fn remove( From 727e7d94f0e5dc92193b5a915b37de3ea0a13b2a Mon Sep 17 00:00:00 2001 From: jb-alvarado Date: Wed, 22 Jun 2022 21:28:32 +0200 Subject: [PATCH 44/47] spelling --- ffplayout-api/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ffplayout-api/README.md b/ffplayout-api/README.md index 11403090..df7982ad 100644 --- a/ffplayout-api/README.md +++ b/ffplayout-api/README.md @@ -29,7 +29,7 @@ For all endpoints an (Bearer) authentication is required.\ #### Login is - **POST** `/auth/login/`\ -JSON Data: `'{"username": "", "password": ""`\ +JSON Data: `{"username": "", "password": ""}`\ JSON Response: ```JSON { @@ -50,7 +50,7 @@ From here on all request **must** contain the authorization header:\ #### User - **PUT** `/api/user/{user id}`\ -JSON Data: `'{"email": "", "password": ""}'` +JSON Data: `{"email": "", "password": ""}` - **POST** `/api/user/`\ JSON Data: From 24b115296a0d59427afa55a111502a1d4b7dc412 Mon Sep 17 00:00:00 2001 From: jb-alvarado Date: Wed, 22 Jun 2022 21:29:48 +0200 Subject: [PATCH 45/47] spelling --- ffplayout-api/README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ffplayout-api/README.md b/ffplayout-api/README.md index df7982ad..e4b23ac9 100644 --- a/ffplayout-api/README.md +++ b/ffplayout-api/README.md @@ -125,11 +125,11 @@ JSON Data: { "text": "Hello from ffplayout", "x": "(w-text_w)/2", - "y": "(h-text_h)/2", \ + "y": "(h-text_h)/2", "fontsize": "24", "line_spacing": "4", "fontcolor": "#ffffff", - "box": "1", \ + "box": "1", "boxcolor": "#000000", "boxborderw": "4", "alpha": "1.0" From 46ce7e2135744a58ef6b37cafb6533db2a532bca Mon Sep 17 00:00:00 2001 From: jb-alvarado Date: Wed, 22 Jun 2022 21:39:59 +0200 Subject: [PATCH 46/47] update readme --- README.md | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 80d6ea7f..1f4405a2 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,18 @@ -**ffplayout-engine** +**ffplayout** ================ [![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0) +The ffplayout apps are mostly made to run on Linux as system services. But in general they should run on all platforms which are supported by Rust. At the moment the cross compiled version from *ffpapi* runs on Windows and Linux, and not on Mac. If it is needed there, it should be compile natively. + +Check the [releases](https://github.com/ffplayout/ffplayout-engine/releases/latest) for pre compiled version. + +**ffplayout-engine (ffplayout)** +----- + [ffplayout](/ffplayout-engine/README.md) is 24/7 broadcasting solution. It can playout a folder with containing video clips, or play for every day a *JSON* playlist, while keeping the current playlist editable. -**Features** ------ +### Features - have all values in a separate config file - dynamic playlist From e7bc8fb09610cfbed1e0b0b7777cb34f22468db1 Mon Sep 17 00:00:00 2001 From: jb-alvarado Date: Thu, 23 Jun 2022 22:39:13 +0200 Subject: [PATCH 47/47] reorganize project --- Cargo.lock | 112 ++++++------ {ffplayout-api/unit => assets}/ffpapi.service | 0 .../unit => assets}/ffplayout.service | 0 build_all.sh | 94 +++------- {ffplayout-api/debian => debian}/.gitkeep | 0 docs/api.md | 171 ++++++++++++++++++ ffplayout-api/Cargo.toml | 39 ---- ffplayout-api/README.md | 168 +---------------- ffplayout-engine/Cargo.toml | 24 ++- 9 files changed, 277 insertions(+), 331 deletions(-) rename {ffplayout-api/unit => assets}/ffpapi.service (100%) rename {ffplayout-engine/unit => assets}/ffplayout.service (100%) rename {ffplayout-api/debian => debian}/.gitkeep (100%) create mode 100644 docs/api.md diff --git a/Cargo.lock b/Cargo.lock index 53720a90..41c4b2c1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -194,7 +194,7 @@ dependencies = [ "serde_urlencoded", "smallvec", "socket2", - "time 0.3.10", + "time 0.3.11", "url", ] @@ -386,7 +386,7 @@ dependencies = [ "async-global-executor", "async-io", "async-lock", - "crossbeam-utils 0.8.9", + "crossbeam-utils 0.8.10", "futures-channel", "futures-core", "futures-io", @@ -623,6 +623,17 @@ dependencies = [ "winapi 0.3.9", ] +[[package]] +name = "chrono" +version = "0.4.19" +source = "git+https://github.com/chronotope/chrono.git#b1d74aef688c27fccc738c64746535905903471a" +dependencies = [ + "num-integer", + "num-traits", + "time 0.1.44", + "winapi 0.3.9", +] + [[package]] name = "clap" version = "3.2.6" @@ -706,7 +717,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94d4706de1b0fa5b132270cddffa8585166037822e260a944fe161acd137ca05" dependencies = [ "percent-encoding", - "time 0.3.10", + "time 0.3.11", "version_check", ] @@ -790,7 +801,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c02a4d71819009c192cf4872265391563fd6a84c81ff2c0f2a7026ca4c1d85c" dependencies = [ "cfg-if 1.0.0", - "crossbeam-utils 0.8.9", + "crossbeam-utils 0.8.10", ] [[package]] @@ -837,7 +848,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1f25d8400f4a7a5778f0e4e52384a48cbd9b5c495d110786187fc750075277a2" dependencies = [ "cfg-if 1.0.0", - "crossbeam-utils 0.8.9", + "crossbeam-utils 0.8.10", ] [[package]] @@ -853,9 +864,9 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.9" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ff1f980957787286a554052d03c7aee98d99cc32e09f6d45f0a814133c87978" +checksum = "7d82ee10ce34d7bc12c2122495e7593a9c41347ecdd64185af4ecf72cb1a7f83" dependencies = [ "cfg-if 1.0.0", "once_cell", @@ -997,43 +1008,10 @@ dependencies = [ ] [[package]] -name = "ffplayout-api" -version = "0.3.0" +name = "ffplayout" +version = "0.10.0" dependencies = [ - "actix-multipart", - "actix-web", - "actix-web-grants", - "actix-web-httpauth", - "argon2", - "chrono 0.4.19 (registry+https://github.com/rust-lang/crates.io-index)", - "clap", - "derive_more", - "faccess", - "ffplayout-lib", - "ffprobe", - "futures-util", - "jsonwebtoken", - "log", - "once_cell", - "openssl", - "rand 0.8.5", - "rand_core 0.6.3", - "regex", - "relative-path", - "reqwest", - "sanitize-filename", - "serde", - "serde_json", - "serde_yaml", - "simplelog", - "sqlx", -] - -[[package]] -name = "ffplayout-engine" -version = "0.9.9" -dependencies = [ - "chrono 0.4.19 (git+https://github.com/sbrocket/chrono?branch=parse-error-kind-public)", + "chrono 0.4.19 (git+https://github.com/chronotope/chrono.git)", "clap", "crossbeam-channel 0.5.5", "faccess", @@ -1054,11 +1032,43 @@ dependencies = [ "serde_yaml", "shlex", "simplelog", - "time 0.3.10", + "time 0.3.11", "walkdir", "zeromq", ] +[[package]] +name = "ffplayout-api" +version = "0.3.0" +dependencies = [ + "actix-multipart", + "actix-web", + "actix-web-grants", + "actix-web-httpauth", + "argon2", + "chrono 0.4.19 (registry+https://github.com/rust-lang/crates.io-index)", + "clap", + "derive_more", + "faccess", + "ffplayout-lib", + "ffprobe", + "futures-util", + "jsonwebtoken", + "log", + "once_cell", + "rand 0.8.5", + "rand_core 0.6.3", + "regex", + "relative-path", + "reqwest", + "sanitize-filename", + "serde", + "serde_json", + "serde_yaml", + "simplelog", + "sqlx", +] + [[package]] name = "ffplayout-lib" version = "0.9.9" @@ -1083,7 +1093,7 @@ dependencies = [ "serde_yaml", "shlex", "simplelog", - "time 0.3.10", + "time 0.3.11", "walkdir", ] @@ -2069,9 +2079,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-src" -version = "111.20.0+1.1.1o" +version = "111.21.0+1.1.1p" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92892c4f87d56e376e469ace79f1128fdaded07646ddf73aa0be4706ff712dec" +checksum = "6d0a8313729211913936f1b95ca47a5fc7f2e04cd658c115388287f8a8361008" dependencies = [ "cc", ] @@ -2643,7 +2653,7 @@ dependencies = [ "num-bigint", "num-traits", "thiserror", - "time 0.3.10", + "time 0.3.11", ] [[package]] @@ -2655,7 +2665,7 @@ dependencies = [ "log", "paris", "termcolor", - "time 0.3.10", + "time 0.3.11", ] [[package]] @@ -2886,9 +2896,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.10" +version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "82501a4c1c0330d640a6e176a3d6a204f5ec5237aca029029d21864a902e27b0" +checksum = "72c91f41dcb2f096c05f0873d667dceec1087ce5bcf984ec8ffb19acddbb3217" dependencies = [ "itoa", "libc", diff --git a/ffplayout-api/unit/ffpapi.service b/assets/ffpapi.service similarity index 100% rename from ffplayout-api/unit/ffpapi.service rename to assets/ffpapi.service diff --git a/ffplayout-engine/unit/ffplayout.service b/assets/ffplayout.service similarity index 100% rename from ffplayout-engine/unit/ffplayout.service rename to assets/ffplayout.service diff --git a/build_all.sh b/build_all.sh index 9052828b..524e66db 100755 --- a/build_all.sh +++ b/build_all.sh @@ -10,58 +10,7 @@ while read -r name value; do fi done < ffplayout-engine/Cargo.toml -echo "Compile ffplayout-engine version is: \"$version\"" -echo "" - -for target in "${targets[@]}"; do - echo "compile static for $target" - echo "" - - cargo build --release --target=$target --bin ffplayout - - if [[ $target == "x86_64-pc-windows-gnu" ]]; then - if [[ -f "ffplayout-engine-v${version}_${target}.zip" ]]; then - rm -f "ffplayout-engine-v${version}_${target}.zip" - fi - - cp ./target/${target}/release/ffplayout.exe . - zip -r "ffplayout-engine-v${version}_${target}.zip" assets docs LICENSE README.md ffplayout.exe -x *.db - rm -f ffplayout.exe - elif [[ $target == "x86_64-apple-darwin" ]] || [[ $target == "aarch64-apple-darwin" ]]; then - if [[ -f "ffplayout-engine-v${version}_${target}.tar.gz" ]]; then - rm -f "ffplayout-engine-v${version}_${target}.tar.gz" - fi - - cp ./target/${target}/release/ffplayout . - tar -czvf "ffplayout-engine-v${version}_${target}.tar.gz" --exclude='*.db' assets docs LICENSE README.md ffplayout - rm -f ffplayout - else - if [[ -f "ffplayout-engine-v${version}_${target}.tar.gz" ]]; then - rm -f "ffplayout-engine-v${version}_${target}.tar.gz" - fi - - cp ./target/${target}/release/ffplayout . - tar -czvf "ffplayout-engine-v${version}_${target}.tar.gz" --exclude='*.db' assets docs LICENSE README.md ffplayout - rm -f ffplayout - fi - - echo "" -done - -cargo deb --target=x86_64-unknown-linux-musl -p ffplayout-engine -mv ./target/x86_64-unknown-linux-musl/debian/ffplayout-engine_${version}_amd64.deb . - -cargo generate-rpm --target=x86_64-unknown-linux-musl -p ffplayout-engine -mv ./target/x86_64-unknown-linux-musl/generate-rpm/ffplayout-engine-${version}-1.x86_64.rpm . - -IFS="= " -while read -r name value; do - if [[ $name == "version" ]]; then - version=${value//\"/} - fi -done < ffplayout-api/Cargo.toml - -echo "Compile ffplayout-api version is: \"$version\"" +echo "Compile ffplayout version is: \"$version\"" echo "" for target in "${targets[@]}"; do @@ -69,32 +18,43 @@ for target in "${targets[@]}"; do echo "" if [[ $target == "x86_64-pc-windows-gnu" ]]; then - if [[ -f "ffplayout-api-v${version}_${target}.zip" ]]; then - rm -f "ffplayout-api-v${version}_${target}.zip" + if [[ -f "ffplayout-v${version}_${target}.zip" ]]; then + rm -f "ffplayout-v${version}_${target}.zip" fi - cargo build --release --target=$target --bin ffpapi + cargo build --release --target=$target cp ./target/${target}/release/ffpapi.exe . - zip -r "ffplayout-api-v${version}_${target}.zip" assets docs LICENSE README.md ffpapi.exe -x *.db - rm -f ffpapi.exe - elif [[ $target == "x86_64-unknown-linux-musl" ]]; then - if [[ -f "ffplayout-api-v${version}_${target}.tar.gz" ]]; then - rm -f "ffplayout-api-v${version}_${target}.tar.gz" + cp ./target/${target}/release/ffplayout.exe . + zip -r "ffplayout-v${version}_${target}.zip" assets docs LICENSE README.md ffplayout.exe ffpapi.exe -x *.db + rm -f ffplayout.exe ffpapi.exe + elif [[ $target == "x86_64-apple-darwin" ]] || [[ $target == "aarch64-apple-darwin" ]]; then + if [[ -f "ffplayout-v${version}_${target}.tar.gz" ]]; then + rm -f "ffplayout-v${version}_${target}.tar.gz" fi - cargo build --release --target=$target --bin ffpapi + cargo build --release --target=$target --bin ffplayout + + cp ./target/${target}/release/ffplayout . + tar -czvf "ffplayout-v${version}_${target}.tar.gz" --exclude='*.db' assets docs LICENSE README.md ffplayout + rm -f ffplayout + else + if [[ -f "ffplayout-v${version}_${target}.tar.gz" ]]; then + rm -f "ffplayout-v${version}_${target}.tar.gz" + fi + + cargo build --release --target=$target cp ./target/${target}/release/ffpapi . - tar -czvf "ffplayout-api-v${version}_${target}.tar.gz" --exclude='*.db' assets docs LICENSE README.md ffpapi - rm -f ffpapi + cp ./target/${target}/release/ffplayout . + tar -czvf "ffplayout-v${version}_${target}.tar.gz" --exclude='*.db' assets docs LICENSE README.md ffplayout ffpapi + rm -f ffplayout ffpapi fi echo "" done -cargo deb --target=x86_64-unknown-linux-musl -p ffplayout-api -mv ./target/x86_64-unknown-linux-musl/debian/ffplayout-api_${version}_amd64.deb . +cargo deb --target=x86_64-unknown-linux-musl -p ffplayout --manifest-path=ffplayout-engine/Cargo.toml -o ffplayout_${version}_amd64.deb + +cargo generate-rpm --target=x86_64-unknown-linux-musl -p ffplayout-engine -o ffplayout-${version}-1.x86_64.rpm -cargo generate-rpm --target=x86_64-unknown-linux-musl -p ffplayout-api -mv ./target/x86_64-unknown-linux-musl/generate-rpm/ffplayout-api-${version}-1.x86_64.rpm . diff --git a/ffplayout-api/debian/.gitkeep b/debian/.gitkeep similarity index 100% rename from ffplayout-api/debian/.gitkeep rename to debian/.gitkeep diff --git a/docs/api.md b/docs/api.md new file mode 100644 index 00000000..6d103dd6 --- /dev/null +++ b/docs/api.md @@ -0,0 +1,171 @@ +#### Possible endpoints + +Run the API thru the systemd service, or like: + +```BASH +ffpapi -l 127.0.0.1:8080 +``` + +For all endpoints an (Bearer) authentication is required.\ +`{id}` represent the channel id, and at default is 1. + +#### Login is + +- **POST** `/auth/login/`\ +JSON Data: `{"username": "", "password": ""}`\ +JSON Response: +```JSON +{ + "message": "login correct!", + "status": 200, + "data": { + "id": 1, + "email": "user@example.org", + "username": "user", + "token": "" + } +} +``` + +From here on all request **must** contain the authorization header:\ +`"Authorization: Bearer "` + +#### User + +- **PUT** `/api/user/{user id}`\ +JSON Data: `{"email": "", "password": ""}` + +- **POST** `/api/user/`\ +JSON Data: +```JSON +{ + "email": "", + "username": "", + "password": "", + "role_id": 1 +} +``` + +#### API Settings + +- **GET** `/api/settings/{id}`\ +HEADER: +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 + +- **GET** `/api/playout/config/{id}`\ +Response is in JSON format + +- **PUT** `/api/playout/config/{id}`\ +JSON Data: `{ }`\ +Response is in TEXT format + +#### Text Presets + +- **GET** `/api/presets/`\ +Response is in JSON format + +- **PUT** `/api/playout/presets/{id}`\ +JSON Data: +```JSON +{ + "name": "", + "text": "", + "x": "", + "y": "", + "fontsize": 24, + "line_spacing": 4, + "fontcolor": "#ffffff", + "box": 1, + "boxcolor": "#000000", + "boxborderw": 4, + "alpha": "" +} + +``` +Response is in TEXT format + +- **POST** `/api/playout/presets/`\ +JSON 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/`\ +Response is in TEXT format + +- **POST** `api/control/{id}/playout/back/`\ +Response is in TEXT format + +- **POST** `api/control/{id}/playout/reset/`\ +Response is in TEXT format + +- **GET** `/api/control/{id}/media/current/`\ +Response is in JSON format + +- **GET** `/api/control/{id}/media/next/`\ +Response is in JSON format + +- **GET** `/api/control/{id}/media/last/`\ +Response is in JSON format + +#### Playlist Operations + +- **GET** `/api/playlist/{id}/2022-06-20`\ +Response is in JSON format + +- **POST** `/api/playlist/1/`\ +JSON Data: `{ }`\ +Response is in TEXT format + +- **GET** `/api/playlist/{id}/generate/2022-06-20`\ +Response is in JSON format + +- **DELETE** `/api/playlist/{id}/2022-06-20`\ +Response is in TEXT format + +#### File Operations + +- **GET** `/api/file/{id}/browse/`\ +Response is in JSON format + +- **POST** `/api/file/{id}/move/`\ +JSON Data: `{"source": "", "target": ""}`\ +Response is in JSON format + +- **DELETE** `/api/file/{id}/remove/`\ +JSON Data: `{"source": ""}`\ +Response is in JSON format + +- **POST** `/file/{id}/upload/`\ +Multipart Form: `name=, filename=`\ +Response is in TEXT format diff --git a/ffplayout-api/Cargo.toml b/ffplayout-api/Cargo.toml index 72fc94f9..f79ee65c 100644 --- a/ffplayout-api/Cargo.toml +++ b/ffplayout-api/Cargo.toml @@ -38,42 +38,3 @@ sqlx = { version = "0.5", features = [ "runtime-actix-native-tls", "sqlite" ] } - -[target.x86_64-unknown-linux-musl.dependencies] -openssl = { version = "0.10", features = ["vendored"] } - -[[bin]] -name = "ffpapi" -path = "src/main.rs" - -# DEBIAN DEB PACKAGE -[package.metadata.deb] -name = "ffplayout-api" -priority = "optional" -section = "net" -license-file = ["../LICENSE", "0"] -depends = "" -suggests = "ffmpeg" -copyright = "Copyright (c) 2022, Jonathan Baecker. All rights reserved." -conf-files = ["/etc/ffplayout/ffplayout.yml"] -assets = [ - [ - "../target/x86_64-unknown-linux-musl/release/ffpapi", - "/usr/bin/ffpapi", - "755" - ], - ["README.md", "/usr/share/doc/ffplayout/README", "644"], -] -maintainer-scripts = "debian/" -systemd-units = { enable = false, unit-scripts = "unit" } - -# REHL RPM PACKAGE -[package.metadata.generate-rpm] -name = "ffplayout-api" -license = "GPL-3.0" -assets = [ - { source = "../target/x86_64-unknown-linux-musl/release/ffpapi", dest = "/usr/bin/ffpapi", mode = "755" }, - { source = "unit/ffpapi.service", dest = "/lib/systemd/system/ffpapi.service", mode = "644" }, - { source = "README.md", dest = "/usr/share/doc/ffplayout/README", mode = "644", doc = true }, - { source = "../LICENSE", dest = "/usr/share/doc/ffplayout/LICENSE", mode = "644" }, -] diff --git a/ffplayout-api/README.md b/ffplayout-api/README.md index e4b23ac9..48c7175a 100644 --- a/ffplayout-api/README.md +++ b/ffplayout-api/README.md @@ -1,7 +1,7 @@ **ffplayout-api** ================ -ffplayout-api (ffpapi) is a on strict REST API for ffplayout. It makes it possible to control the engine, read and manipulate the config, save playlist, etc. +ffplayout-api (ffpapi) is a non strict REST API for ffplayout. It makes it possible to control the engine, read and manipulate the config, save playlist, etc. To be able to use the API it is necessary to initialize the settings database first. To do that, run: @@ -21,168 +21,4 @@ Then run the API thru the systemd service, or like: ffpapi -l 127.0.0.1:8080 ``` -### Possible endpoints - -For all endpoints an (Bearer) authentication is required.\ -`{id}` represent the channel id, and at default is 1. - -#### Login is - -- **POST** `/auth/login/`\ -JSON Data: `{"username": "", "password": ""}`\ -JSON Response: -```JSON -{ - "message": "login correct!", - "status": 200, - "data": { - "id": 1, - "email": "user@example.org", - "username": "user", - "token": "" - } -} -``` - -From here on all request **must** contain the authorization header:\ -`"Authorization: Bearer "` - -#### User - -- **PUT** `/api/user/{user id}`\ -JSON Data: `{"email": "", "password": ""}` - -- **POST** `/api/user/`\ -JSON Data: -```JSON -{ - "email": "", - "username": "", - "password": "", - "role_id": 1 -} -``` - -#### API Settings - -- **GET** `/api/settings/{id}`\ -HEADER: -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 - -- **GET** `/api/playout/config/{id}`\ -Response is in JSON format - -- **PUT** `/api/playout/config/{id}`\ -JSON Data: `{ }`\ -Response is in TEXT format - -#### Text Presets - -- **GET** `/api/presets/`\ -Response is in JSON format - -- **PUT** `/api/playout/presets/{id}`\ -JSON Data: -```JSON -{ - "name": "", - "text": "", - "x": "", - "y": "", - "fontsize": 24, - "line_spacing": 4, - "fontcolor": "#ffffff", - "box": 1, - "boxcolor": "#000000", - "boxborderw": 4, - "alpha": "" -} - -``` -Response is in TEXT format - -- **POST** `/api/playout/presets/`\ -JSON 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/`\ -Response is in TEXT format - -- **POST** `api/control/{id}/playout/back/`\ -Response is in TEXT format - -- **POST** `api/control/{id}/playout/reset/`\ -Response is in TEXT format - -- **GET** `/api/control/{id}/media/current/`\ -Response is in JSON format - -- **GET** `/api/control/{id}/media/next/`\ -Response is in JSON format - -- **GET** `/api/control/{id}/media/last/`\ -Response is in JSON format - -#### Playlist Operations - -- **GET** `/api/playlist/{id}/2022-06-20`\ -Response is in JSON format - -- **POST** `/api/playlist/1/`\ -JSON Data: `{ }`\ -Response is in TEXT format - -- **GET** `/api/playlist/{id}/generate/2022-06-20`\ -Response is in JSON format - -- **DELETE** `/api/playlist/{id}/2022-06-20`\ -Response is in TEXT format - -#### File Operations - -- **GET** `/api/file/{id}/browse/`\ -Response is in JSON format - -- **POST** `/api/file/{id}/move/`\ -JSON Data: `{"source": "", "target": ""}`\ -Response is in JSON format - -- **DELETE** `/api/file/{id}/remove/`\ -JSON Data: `{"source": ""}`\ -Response is in JSON format - -- **POST** `/file/{id}/upload/`\ -Multipart Form: `name=, filename=`\ -Response is in TEXT format +**For possible endpoints read: [api endpoints](/docs/api.md)** diff --git a/ffplayout-engine/Cargo.toml b/ffplayout-engine/Cargo.toml index 7b2cac32..65fed720 100644 --- a/ffplayout-engine/Cargo.toml +++ b/ffplayout-engine/Cargo.toml @@ -1,15 +1,15 @@ [package] -name = "ffplayout-engine" +name = "ffplayout" description = "24/7 playout based on rust and ffmpeg" license = "GPL-3.0" authors = ["Jonathan Baecker jonbae77@gmail.com"] readme = "README.md" -version = "0.9.9" +version = "0.10.0" edition = "2021" [dependencies] ffplayout-lib = { path = "../lib" } -chrono = { git = "https://github.com/sbrocket/chrono", branch = "parse-error-kind-public" } +chrono = { git = "https://github.com/chronotope/chrono.git" } clap = { version = "3.2", features = ["derive"] } crossbeam-channel = "0.5" faccess = "0.2" @@ -44,7 +44,7 @@ path = "src/main.rs" # DEBIAN DEB PACKAGE [package.metadata.deb] -name = "ffplayout-engine" +name = "ffplayout" priority = "optional" section = "net" license-file = ["../LICENSE", "0"] @@ -53,26 +53,34 @@ suggests = "ffmpeg" copyright = "Copyright (c) 2022, Jonathan Baecker. All rights reserved." conf-files = ["/etc/ffplayout/ffplayout.yml"] assets = [ + [ + "../target/x86_64-unknown-linux-musl/release/ffpapi", + "/usr/bin/ffpapi", + "755" + ], [ "../target/x86_64-unknown-linux-musl/release/ffplayout", "/usr/bin/ffplayout", "755" ], + ["../assets/ffpapi.service", "/lib/systemd/system/ffpapi.service", "644"], ["../assets/ffplayout.yml", "/etc/ffplayout/ffplayout.yml", "644"], ["../assets/logo.png", "/usr/share/ffplayout/logo.png", "644"], ["../README.md", "/usr/share/doc/ffplayout/README", "644"], ] -maintainer-scripts = "debian/" -systemd-units = { enable = false, unit-scripts = "unit" } +maintainer-scripts = "../debian/" +systemd-units = { enable = false, unit-scripts = "../assets" } # REHL RPM PACKAGE [package.metadata.generate-rpm] -name = "ffplayout-engine" +name = "ffplayout" license = "GPL-3.0" assets = [ + { source = "../target/x86_64-unknown-linux-musl/release/ffpapi", dest = "/usr/bin/ffpapi", mode = "755" }, { source = "../target/x86_64-unknown-linux-musl/release/ffplayout", dest = "/usr/bin/ffplayout", mode = "755" }, { source = "../assets/ffplayout.yml", dest = "/etc/ffplayout/ffplayout.yml", mode = "644", config = true }, - { source = "unit/ffplayout.service", dest = "/lib/systemd/system/ffplayout.service", mode = "644" }, + { source = "../assets/ffpapi.service", dest = "/lib/systemd/system/ffpapi.service", mode = "644" }, + { source = "../assets/ffplayout.service", dest = "/lib/systemd/system/ffplayout.service", mode = "644" }, { source = "../README.md", dest = "/usr/share/doc/ffplayout/README", mode = "644", doc = true }, { source = "../LICENSE", dest = "/usr/share/doc/ffplayout/LICENSE", mode = "644" }, { source = "../assets/logo.png", dest = "/usr/share/ffplayout/logo.png", mode = "644" },