diff --git a/.gitignore b/.gitignore index c637205b..6336f642 100644 --- a/.gitignore +++ b/.gitignore @@ -18,5 +18,6 @@ *tar.gz *.deb *.rpm +/assets/*.db* .vscode/ diff --git a/Cargo.lock b/Cargo.lock index c8eba3d4..53720a90 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,12 +2,265 @@ # 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.3", +] + +[[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.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd2e9f6794b5826aff6df65e3a0d0127b271d1c03629c774238f3582e903d4e4" +dependencies = [ + "actix-codec", + "actix-rt", + "actix-service", + "actix-utils", + "ahash 0.7.6", + "base64", + "bitflags", + "brotli", + "bytes", + "bytestring", + "derive_more", + "encoding_rs", + "flate2", + "futures-core", + "h2", + "http", + "httparse", + "httpdate", + "itoa", + "language-tags", + "local-channel", + "mime", + "percent-encoding", + "pin-project-lite", + "rand 0.8.5", + "sha1", + "smallvec", + "tracing", + "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-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" +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.4", + "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.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a27e8fe9ba4ae613c21f677c2cfaf0696c3744030c6f485b34634e502d6bb379" +dependencies = [ + "actix-codec", + "actix-http", + "actix-macros", + "actix-router", + "actix-rt", + "actix-server", + "actix-service", + "actix-utils", + "actix-web-codegen", + "ahash 0.7.6", + "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.10", + "url", +] + +[[package]] +name = "actix-web-codegen" +version = "4.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f270541caec49c15673b0af0e9a00143421ad4f118d2df7edcb68b627632f56" +dependencies = [ + "actix-router", + "proc-macro2", + "quote", + "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" 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 0.2.7", + "once_cell", + "version_check", +] + [[package]] name = "aho-corasick" version = "0.7.18" @@ -17,6 +270,183 @@ 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 = "argon2" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a27e27b63e4a34caee411ade944981136fdfa535522dc9944d6700196cbd899f" +dependencies = [ + "base64ct", + "blake2", + "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.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5262ed948da60dd8956c6c5aca4d4163593dddb7b32d73267c93dab7b2e98940" +dependencies = [ + "async-channel", + "async-executor", + "async-io", + "async-lock", + "blocking", + "futures-lite", + "num_cpus", + "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.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62565bb4402e926b29953c785397c6dc0391b7b446e45008b0049eb43cec6f5d" +dependencies = [ + "async-attributes", + "async-channel", + "async-global-executor", + "async-io", + "async-lock", + "crossbeam-utils 0.8.9", + "futures-channel", + "futures-core", + "futures-io", + "futures-lite", + "gloo-timers", + "kv-log-macro", + "log", + "memchr", + "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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "616896e05fc0e2649463a93a15183c6a16bf03413a7af88ef1285ddedfa9cda5" +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" @@ -40,12 +470,71 @@ 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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf7fe51849ea569fd452f37822f606a5cabb684dc918707a0193fd4664ff324" +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" +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 +550,41 @@ version = "3.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37ccbd214614c6783386c1af30caf03192f17891059cecc394b4fb119e363de3" +[[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.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b6a75fd3048808ef06af5cd79712be8111960adaf89d90250974b38fc3928a" +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" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11" +dependencies = [ + "jobserver", +] [[package]] name = "cfg-if" @@ -85,6 +598,19 @@ 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", + "time 0.1.44", + "winapi 0.3.9", +] + [[package]] name = "chrono" version = "0.4.19" @@ -99,16 +625,16 @@ dependencies = [ [[package]] name = "clap" -version = "3.1.18" +version = "3.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2dbdf4bdacb33466e854ce889eee8dfd5729abf7ccd7664d0a2d60cd384440b" +checksum = "9f1fe12880bae935d142c8702d500c63a4e8634b6c3c57ad72bf978fc7b6249a" dependencies = [ "atty", "bitflags", "clap_derive", "clap_lex", "indexmap", - "lazy_static", + "once_cell", "strsim", "termcolor", "textwrap", @@ -116,9 +642,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "3.1.18" +version = "3.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25320346e922cffe59c0bbc5410c8d8784509efb321488971081313cb1e1a33c" +checksum = "ed6db9e867166a43a53f7199b5e4d1f522a1e5bd626654be263c999ce59df39a" dependencies = [ "heck", "proc-macro-error", @@ -129,13 +655,61 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.2.0" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a37c35f1112dad5e6e0b1adaff798507497a18fceeb30cceb3bae7d1427b9213" +checksum = "87eba3c8c7f42ef17f6c659fc7416d0f4758cd3e58861ee63c5fa4a4dde649e4" 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.7", + "lazy_static", + "proc-macro-hack", + "tiny-keccak", +] + +[[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.10", + "version_check", +] + [[package]] name = "core-foundation" version = "0.9.3" @@ -152,6 +726,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" @@ -162,25 +760,180 @@ dependencies = [ ] [[package]] -name = "crossbeam-channel" -version = "0.5.4" +name = "crossbeam" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5aaa7bd5fb665c6864b5f963dd9097905c54125909c7aa94c9e18507cdbe6c53" +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.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c02a4d71819009c192cf4872265391563fd6a84c81ff2c0f2a7026ca4c1d85c" dependencies = [ "cfg-if 1.0.0", - "crossbeam-utils", + "crossbeam-utils 0.8.9", +] + +[[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]] +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 0.8.9", ] [[package]] name = "crossbeam-utils" -version = "0.8.8" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf124c720b7686e3c2663cf54062ab0f68a88af2fb6a030e87e30bf721fcb38" +checksum = "c3c7c73a2d1e9fc0886a08b93e98eb643461230d5f1925e4036204d5f2e261a8" dependencies = [ - "cfg-if 1.0.0", + "autocfg", + "cfg-if 0.1.10", "lazy_static", ] +[[package]] +name = "crossbeam-utils" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ff1f980957787286a554052d03c7aee98d99cc32e09f6d45f0a814133c87978" +dependencies = [ + "cfg-if 1.0.0", + "once_cell", +] + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57952ca27b5e3606ff4dd79b0020231aaf9d6aa76dc05fd30137538c50bd3ce8" +dependencies = [ + "generic-array", + "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" +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", + "subtle", +] + +[[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.2" @@ -206,6 +959,34 @@ 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" +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" @@ -216,20 +997,56 @@ dependencies = [ ] [[package]] -name = "ffplayout-engine" -version = "0.9.8" +name = "ffplayout-api" +version = "0.3.0" dependencies = [ - "chrono", + "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", - "crossbeam-channel", + "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)", + "clap", + "crossbeam-channel 0.5.5", + "faccess", + "ffplayout-lib", "ffprobe", "file-rotate", + "futures", "jsonrpc-http-server", "lettre", "log", "notify", "openssl", - "rand", + "rand 0.8.5", "regex", "reqwest", "serde", @@ -237,7 +1054,36 @@ dependencies = [ "serde_yaml", "shlex", "simplelog", - "time 0.3.9", + "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", ] @@ -256,7 +1102,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", ] @@ -272,6 +1118,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" @@ -282,6 +1134,18 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "flume" +version = "0.10.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ceeb589a3157cac0ab8cc585feb749bd2cea5cb55a6ee802ad72d9fd38303da" +dependencies = [ + "futures-core", + "futures-sink", + "pin-project", + "spin 0.9.3", +] + [[package]] name = "fnv" version = "1.0.7" @@ -390,12 +1254,38 @@ 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" 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" @@ -438,14 +1328,35 @@ dependencies = [ ] [[package]] -name = "getrandom" -version = "0.2.6" +name = "generic-array" +version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9be70c98951c83b8d2f8f60d7065fa6d5146873094452a1008da8c2f1e4205ad" +checksum = "fd48d33ec7f05fbfa152300fdad764757cbded343c1aa1cff2fbaf4134851803" +dependencies = [ + "typenum", + "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.10.0+wasi-snapshot-preview1", + "wasi 0.9.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4eb1a864a501629691edf6c15a593b7a51eebaa1e8468e9ddc623de7c9b58ec6" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", ] [[package]] @@ -461,6 +1372,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" @@ -485,12 +1408,33 @@ name = "hashbrown" version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" +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 0.11.2", +] [[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" @@ -501,6 +1445,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" @@ -596,12 +1546,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "1.8.2" +version = "1.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6012d540c5baa3589337a98ce73408de9b5a25ec9fc2c6fd6be8f0d39e0ca5a" +checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e" dependencies = [ "autocfg", - "hashbrown", + "hashbrown 0.12.1", ] [[package]] @@ -648,6 +1598,15 @@ 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" @@ -655,10 +1614,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "112c678d4050afce233f4f2852bb2eb519230b3cf12f33585275537d7e41578d" [[package]] -name = "js-sys" -version = "0.3.57" +name = "jobserver" +version = "0.1.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "671a26f820db17c2a2750743f1dd03bafd15b98c9f30c7c2628c024c05d73397" +checksum = "af25a77299a7f711a01975c35a6a424eb6862092cc2d6c72c4ed6cbc56dfc1fa" +dependencies = [ + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3fac17f7123a73ca62df411b1bf727ccc805daa070338fda671c86dac1bdc27" dependencies = [ "wasm-bindgen", ] @@ -690,7 +1658,7 @@ dependencies = [ "jsonrpc-server-utils", "log", "net2", - "parking_lot", + "parking_lot 0.11.2", "unicase", ] @@ -712,6 +1680,20 @@ dependencies = [ "unicase", ] +[[package]] +name = "jsonwebtoken" +version = "8.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa4b4af834c6cfd35d8763d359661b90f2e45d8f750a0849156c7f4671af09c" +dependencies = [ + "base64", + "pem", + "ring", + "serde", + "serde_json", + "simple_asn1", +] + [[package]] name = "kernel32-sys" version = "0.2.2" @@ -722,6 +1704,21 @@ 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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388" + [[package]] name = "lazy_static" version = "1.4.0" @@ -762,12 +1759,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" @@ -785,6 +1811,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" dependencies = [ "cfg-if 1.0.0", + "value-bag", ] [[package]] @@ -799,12 +1826,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" @@ -847,9 +1889,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", @@ -938,6 +1980,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" @@ -1049,6 +2102,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" @@ -1057,7 +2116,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]] @@ -1074,12 +2143,71 @@ 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 = "password-hash" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e029e94abc8fb0065241c308f1ac6bc8d20f450e8f7c5f0b25cd9b8d526ba294" +dependencies = [ + "base64ct", + "rand_core 0.6.3", + "subtle", +] + +[[package]] +name = "paste" +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" 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" @@ -1098,6 +2226,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" @@ -1129,19 +2270,25 @@ dependencies = [ ] [[package]] -name = "proc-macro2" -version = "1.0.39" +name = "proc-macro-hack" +version = "0.5.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c54b25569025b7fc9651de43004ae593a75ad88543b17178aa5e1b9c4f15f56f" +checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5" + +[[package]] +name = "proc-macro2" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +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", ] @@ -1152,6 +2299,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" @@ -1159,8 +2319,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]] @@ -1170,7 +2340,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]] @@ -1179,7 +2358,16 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" dependencies = [ - "getrandom", + "getrandom 0.2.7", +] + +[[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]] @@ -1208,6 +2396,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" @@ -1219,9 +2413,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", @@ -1246,6 +2440,7 @@ dependencies = [ "serde_urlencoded", "tokio", "tokio-native-tls", + "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", @@ -1253,6 +2448,30 @@ 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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + [[package]] name = "ryu" version = "1.0.10" @@ -1268,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" @@ -1307,6 +2536,12 @@ dependencies = [ "libc", ] +[[package]] +name = "semver" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a41d061efea015927ac527063765e73601444cdc344ba855bc7bd44578b25e1c" + [[package]] name = "serde" version = "1.0.137" @@ -1362,12 +2597,55 @@ dependencies = [ "yaml-rust", ] +[[package]] +name = "sha1" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c77f4e7f65455545c2153c1253d25056825e77ee2533f0e41deb65a93a34852f" +dependencies = [ + "cfg-if 1.0.0", + "cpufeatures", + "digest", +] + +[[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 = "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.10", +] + [[package]] name = "simplelog" version = "0.12.0" @@ -1377,7 +2655,7 @@ dependencies = [ "log", "paris", "termcolor", - "time 0.3.9", + "time 0.3.10", ] [[package]] @@ -1402,6 +2680,127 @@ 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" +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 0.7.6", + "atoi", + "bitflags", + "byteorder", + "bytes", + "chrono 0.4.19 (registry+https://github.com/rust-lang/crates.io-index)", + "crc", + "crossbeam-queue 0.3.5", + "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" @@ -1409,10 +2808,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[package]] -name = "syn" -version = "1.0.96" +name = "subtle" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0748dd251e24453cb8717f0354206b91557e4ec8703673a4b30208f2abaf1ebf" +checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" + +[[package]] +name = "syn" +version = "1.0.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c50aef8a904de4c23c788f104b7dddc7d6f79c647c7c8ce4cc8f73eb0ca773dd" dependencies = [ "proc-macro2", "quote", @@ -1448,6 +2853,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" @@ -1461,9 +2886,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", @@ -1477,6 +2902,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" @@ -1501,10 +2935,12 @@ dependencies = [ "bytes", "libc", "memchr", - "mio 0.8.3", + "mio 0.8.4", "num_cpus", "once_cell", + "parking_lot 0.12.1", "pin-project-lite", + "signal-hook-registry", "socket2", "winapi 0.3.9", ] @@ -1560,9 +2996,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" @@ -1571,6 +3007,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a400e31aa60b9d44a52a8ee0343b5b18566b03a8321e0d321f695cf56e940160" dependencies = [ "cfg-if 1.0.0", + "log", "pin-project-lite", "tracing-core", ] @@ -1590,6 +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" @@ -1607,9 +3066,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" @@ -1620,6 +3079,24 @@ 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 = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + [[package]] name = "url" version = "2.2.2" @@ -1632,6 +3109,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.7", +] + +[[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" @@ -1644,6 +3140,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" @@ -1665,6 +3167,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" @@ -1679,9 +3187,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", @@ -1689,9 +3197,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", @@ -1704,9 +3212,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", @@ -1716,9 +3224,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", @@ -1726,9 +3234,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", @@ -1739,20 +3247,29 @@ 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", ] +[[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" @@ -1866,3 +3383,56 @@ checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" 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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20cc960326ece64f010d2d2107537f26dc589a6573a316bd5b1dba685fa5fde4" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "5.0.2+zstd.1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d2a5585e04f9eea4b2a3d1eca508c4dee9592a89ef6f450c11719da0726f4db" +dependencies = [ + "libc", + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.1+zstd.1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fd07cbbc53846d9145dbffdf6dd09a7a0aa52be46741825f5c97bdd4f73f12b" +dependencies = [ + "cc", + "libc", +] diff --git a/Cargo.toml b/Cargo.toml index e8bd70cb..77e2c4b7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,77 +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.8" -edition = "2021" -default-run = "ffplayout" +[workspace] -[dependencies] -chrono = { git = "https://github.com/sbrocket/chrono", branch = "parse-error-kind-public" } -clap = { version = "3.1", features = ["derive"] } -crossbeam-channel = "0.5" -ffprobe = "0.3" -file-rotate = { git = "https://github.com/Ploppz/file-rotate.git", branch = "timestamp-parse-fix" } -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"] } -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"] } - -[[bin]] -name = "ffplayout" -path = "src/main.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" - ], - ["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"], -] -systemd-units = { unit-name = "ffplayout-engine", unit-scripts = "assets", enable = false } - -# 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 = "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/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/assets/ffplayout.yml b/assets/ffplayout.yml index e121438a..db2d89c8 100644 --- a/assets/ffplayout.yml +++ b/assets/ffplayout.yml @@ -105,16 +105,12 @@ 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 + 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)$ 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 7e0f0a0c..00000000 --- a/cross_compile_all.sh +++ /dev/null @@ -1,54 +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 . - 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 - 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 - -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..72fc94f9 --- /dev/null +++ b/ffplayout-api/Cargo.toml @@ -0,0 +1,79 @@ +[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-multipart = "0.4" +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" +futures-util = { version = "0.3", default-features = false, features = ["std"] } +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"] } +sanitize-filename = "0.3" +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/ffplayout-api/debian/.gitkeep b/ffplayout-api/debian/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/ffplayout-api/src/main.rs b/ffplayout-api/src/main.rs new file mode 100644 index 00000000..64a5daa5 --- /dev/null +++ b/ffplayout-api/src/main.rs @@ -0,0 +1,113 @@ +use std::{path::Path, process::exit}; + +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; + +use clap::Parser; +use simplelog::*; + +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, move_rename, patch_settings, remove, reset_playout, + save_file, save_playlist, send_text_message, update_playout_config, update_preset, + update_user, + }, + 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?; + req.attach(vec![Role::set_role(&claims.role)]); + + req.extensions_mut() + .insert(LoginUser::new(claims.id, claims.username)); + + Ok(req) +} + +#[actix_web::main] +async fn main() -> std::io::Result<()> { + let args = Args::parse(); + + let mut config = PlayoutConfig::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 let Err(c) = run_args(args.clone()).await { + exit(c); + } + + 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]; + let port = ip_port[1].parse::().unwrap(); + + info!("running ffplayout API, listen on {conn}"); + + // TODO: add allow origin (or give it to the proxy) + HttpServer::new(move || { + let auth = HttpAuthentication::bearer(validator); + App::new() + .wrap(middleware::Logger::default()) + .service(login) + .service( + web::scope("/api") + .wrap(auth) + .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) + .service(send_text_message) + .service(jump_to_next) + .service(jump_to_last) + .service(reset_playout) + .service(media_current) + .service(media_next) + .service(media_last) + .service(get_playlist) + .service(save_playlist) + .service(gen_playlist) + .service(del_playlist) + .service(file_browser) + .service(move_rename) + .service(remove) + .service(save_file), + ) + }) + .bind((addr, port))? + .run() + .await + } else { + error!("Run ffpapi with listen parameter!"); + + Ok(()) + } +} diff --git a/ffplayout-api/src/utils/args_parse.rs b/ffplayout-api/src/utils/args_parse.rs new file mode 100644 index 00000000..84d8efa5 --- /dev/null +++ b/ffplayout-api/src/utils/args_parse.rs @@ -0,0 +1,22 @@ +use clap::Parser; + +#[derive(Parser, Debug, Clone)] +#[clap(version, + 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")] + 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/ffplayout-api/src/utils/auth.rs b/ffplayout-api/src/utils/auth.rs new file mode 100644 index 00000000..76351923 --- /dev/null +++ b/ffplayout-api/src/utils/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::utils::GlobalSettings; + +// Token lifetime +const JWT_EXPIRATION_DAYS: i64 = 7; + +#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)] +pub struct Claims { + pub id: i64, + pub username: String, + pub role: String, + exp: i64, +} + +impl Claims { + pub fn new(id: i64, username: String, role: String) -> Self { + Self { + id, + username, + role, + exp: (Utc::now() + Duration::days(JWT_EXPIRATION_DAYS)).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/ffplayout-api/src/utils/control.rs b/ffplayout-api/src/utils/control.rs new file mode 100644 index 00000000..44804d85 --- /dev/null +++ b/ffplayout-api/src/utils/control.rs @@ -0,0 +1,107 @@ +use std::collections::HashMap; + +use reqwest::{ + header::{HeaderMap, AUTHORIZATION, CONTENT_TYPE}, + Client, Response, +}; +use serde::{Deserialize, Serialize}; +use simplelog::*; + +use crate::utils::{errors::ServiceError, 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: HashMap, +} + +#[derive(Debug, Deserialize, Serialize, Clone)] +struct ControlParams { + control: String, +} + +#[derive(Debug, Deserialize, Serialize, Clone)] +struct MediaParams { + media: String, +} + +impl RpcObj { + fn new(id: i64, method: String, params: T) -> Self { + Self { + jsonrpc: "2.0".into(), + id, + method, + params, + } + } +} + +fn create_header(auth: &str) -> HeaderMap { + let mut headers = HeaderMap::new(); + headers.insert( + CONTENT_TYPE, + "Content-Type: application/json".parse().unwrap(), + ); + headers.insert(AUTHORIZATION, auth.parse().unwrap()); + + headers +} + +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(); + + match client + .post(&url) + .headers(create_header(&config.rpc_server.authorization)) + .json(&obj) + .send() + .await + { + Ok(result) => Ok(result), + Err(e) => { + error!("{e:?}"); + Err(ServiceError::BadRequest(e.to_string())) + } + } +} + +pub async fn send_message( + id: i64, + message: HashMap, +) -> 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/ffplayout-api/src/utils/errors.rs b/ffplayout-api/src/utils/errors.rs new file mode 100644 index 00000000..2bb20ab9 --- /dev/null +++ b/ffplayout-api/src/utils/errors.rs @@ -0,0 +1,61 @@ +use actix_web::{error::ResponseError, Error, 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 = "Conflict: {}", _0)] + Conflict(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::Conflict(ref message) => HttpResponse::Conflict().json(message), + ServiceError::Unauthorized => HttpResponse::Unauthorized().json("No Permission!"), + } + } +} + +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 new file mode 100644 index 00000000..723f9250 --- /dev/null +++ b/ffplayout-api/src/utils/files.rs @@ -0,0 +1,285 @@ +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 simplelog::*; + +use crate::utils::{errors::ServiceError, playout_config}; +use ffplayout_lib::utils::file_extension; + +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct PathObject { + pub source: String, + folders: Option>, + files: Option>, +} + +impl PathObject { + fn new(source: String) -> Self { + Self { + 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.source) + .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} in {path_component}"); + return Err(ServiceError::InternalServerError); + } + }; + + paths.sort_by_key(|dir| dir.path()); + + for path in paths { + 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() { + if let Some(ref mut folders) = obj.folders { + 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()) { + if let Some(ref mut files) = obj.files { + files.push(path_str); + } + } + } + } + } + + Ok(obj) +} + +// fn copy_and_delete(source: &PathBuf, target: &PathBuf) -> 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(); + debug!("{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/handles.rs b/ffplayout-api/src/utils/handles.rs new file mode 100644 index 00000000..9ee0f1f3 --- /dev/null +++ b/ffplayout-api/src/utils/handles.rs @@ -0,0 +1,273 @@ +use argon2::{ + password_hash::{rand_core::OsRng, SaltString}, + Argon2, PasswordHasher, +}; + +use rand::{distributions::Alphanumeric, Rng}; +use simplelog::*; +use sqlx::{migrate::MigrateDatabase, sqlite::SqliteQueryResult, Pool, Sqlite, SqlitePool}; + +use crate::utils::{ + db_path, + models::{Settings, TextPreset, User}, + GlobalSettings, +}; + +#[derive(Debug, sqlx::FromRow)] +struct Role { + name: String, +} + +async fn create_schema() -> Result { + let conn = db_connection().await?; + let query = "PRAGMA foreign_keys = ON; + 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, + UNIQUE(name) + ); + CREATE TABLE IF NOT EXISTS presets + ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + text TEXT NOT NULL, + x TEXT NOT NULL, + y TEXT NOT NULL, + fontsize TEXT NOT NULL, + line_spacing TEXT NOT NULL, + fontcolor TEXT NOT NULL, + box TEXT NOT NULL, + boxcolor TEXT NOT NULL, + boxborderw TEXT NOT NULL, + alpha TEXT NOT NULL, + UNIQUE(name) + ); + CREATE TABLE IF NOT EXISTS settings + ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + channel_name TEXT NOT NULL, + preview_url TEXT NOT NULL, + config_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, + salt TEXT NOT 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; + conn.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 create_schema().await { + Ok(_) => info!("Database created Successfully"), + 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 = "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 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', + '/etc/ffplayout/ffplayout.yml', '.jpg,.jpeg,.png');"; + sqlx::query(query).bind(secret).execute(&instances).await?; + instances.close().await; + + Ok("Database initialized!") +} + +pub async fn db_connection() -> Result, sqlx::Error> { + let db_path = db_path().unwrap(); + let conn = SqlitePool::connect(&db_path).await?; + + Ok(conn) +} + +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?; + conn.close().await; + + 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; + + Ok(result) +} + +pub async fn db_update_settings( + id: i64, + 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(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; + + Ok(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?; + conn.close().await; + + Ok(result.name) +} + +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?; + conn.close().await; + + Ok(result) +} + +pub async fn db_add_user(user: User) -> Result { + let conn = db_connection().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) +} + +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) +} + +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?; + conn.close().await; + + Ok(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, + 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: TextPreset) -> 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/ffplayout-api/src/utils/mod.rs b/ffplayout-api/src/utils/mod.rs new file mode 100644 index 00000000..b3561173 --- /dev/null +++ b/ffplayout-api/src/utils/mod.rs @@ -0,0 +1,145 @@ +use std::{error::Error, fs::File, path::Path}; + +use faccess::PathExt; +use once_cell::sync::OnceCell; +use simplelog::*; + +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 ffplayout_lib::utils::PlayoutConfig; + +#[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, +} + +impl GlobalSettings { + async fn new() -> Self { + let global_settings = db_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 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."); + + 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 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) = db_add_user(user).await { + error!("{e}"); + return Err(1); + }; + + info!("Create admin user \"{username}\" done..."); + + return Err(0); + } + + Ok(()) +} + +pub fn read_playout_config(path: &str) -> Result> { + let file = File::open(path)?; + let config: PlayoutConfig = serde_yaml::from_reader(file)?; + + Ok(config) +} + +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.clone()) { + return Ok((config, settings)); + } + } + + Err(ServiceError::BadRequest( + "Error in getting config!".to_string(), + )) +} diff --git a/ffplayout-api/src/utils/models.rs b/ffplayout-api/src/utils/models.rs new file mode 100644 index 00000000..1227c82f --- /dev/null +++ b/ffplayout-api/src/utils/models.rs @@ -0,0 +1,69 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Deserialize, Serialize, sqlx::FromRow)] +pub struct User { + #[sqlx(default)] + #[serde(skip_deserializing)] + pub id: i64, + #[sqlx(default)] + pub email: Option, + pub username: String, + #[sqlx(default)] + #[serde(skip_serializing, default = "empty_string")] + pub password: String, + #[sqlx(default)] + #[serde(skip_serializing)] + pub salt: Option, + #[sqlx(default)] + #[serde(skip_serializing)] + pub role_id: Option, + #[sqlx(default)] + pub token: Option, +} + +fn empty_string() -> String { + "".to_string() +} + +#[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, Clone, sqlx::FromRow)] +pub struct TextPreset { + #[sqlx(default)] + #[serde(skip_deserializing)] + pub id: i64, + #[serde(skip_deserializing)] + pub name: String, + pub text: String, + pub x: String, + pub y: String, + pub fontsize: String, + pub line_spacing: String, + pub fontcolor: String, + pub r#box: String, + pub boxcolor: String, + pub boxborderw: String, + pub alpha: String, +} + +#[derive(Debug, Deserialize, Serialize, sqlx::FromRow)] +pub struct Settings { + #[serde(skip_deserializing)] + pub id: i64, + pub channel_name: String, + pub preview_url: String, + pub config_path: String, + pub extra_extensions: String, + #[sqlx(default)] + #[serde(skip_serializing, skip_deserializing)] + pub secret: String, +} diff --git a/ffplayout-api/src/utils/playlist.rs b/ffplayout-api/src/utils/playlist.rs new file mode 100644 index 00000000..9f828024 --- /dev/null +++ b/ffplayout-api/src/utils/playlist.rs @@ -0,0 +1,116 @@ +use std::{ + fs::{self, File}, + io::Error, + path::PathBuf, +}; + +use simplelog::*; + +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)?; + 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); + 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(p) = json_reader(&playlist_path) { + return Ok(p); + }; + + Err(ServiceError::InternalServerError) +} + +pub async fn write_playlist(id: i64, json_data: JsonPlaylist) -> 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 generate_playlist(id: i64, date: String) -> 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 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/ffplayout-api/src/utils/routes.rs b/ffplayout-api/src/utils/routes.rs new file mode 100644 index 00000000..47088d21 --- /dev/null +++ b/ffplayout-api/src/utils/routes.rs @@ -0,0 +1,467 @@ +use std::collections::HashMap; + +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}, + Argon2, PasswordHasher, PasswordVerifier, +}; +use serde::Serialize; +use simplelog::*; + +use crate::utils::{ + auth::{create_jwt, Claims}, + control::{control_state, media_info, send_message}, + errors::ServiceError, + 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, + }, + 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 { + message: String, + status: i32, + 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/")] +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; + + if Argon2::default() + .verify_password(credentials.password.as_bytes(), &hash) + .is_ok() + { + 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(), role.clone()); + + if let Ok(token) = create_jwt(claims) { + user.token = Some(token); + }; + + info!("user {} login, with role: {role}", credentials.username); + + web::Json(ResponseObj { + message: "login correct!".into(), + status: 200, + data: Some(user), + }) + .customize() + .with_status(StatusCode::OK) + } else { + error!("Wrong password for {}!", credentials.username); + web::Json(ResponseObj { + message: "Wrong password!".into(), + status: 403, + 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: 400, + data: None, + }) + .customize() + .with_status(StatusCode::BAD_REQUEST); + } + } +} + +/// ---------------------------------------------------------------------------- +/// 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.) +/// ---------------------------------------------------------------------------- + +/// 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>, +) -> 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), + } +} + +/// ---------------------------------------------------------------------------- +/// 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), + } +} + +/// 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 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}")] +#[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) => Ok(web::Json(obj)), + 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 +} diff --git a/ffplayout-api/unit/ffpapi.service b/ffplayout-api/unit/ffpapi.service new file mode 100644 index 00000000..3cd83f38 --- /dev/null +++ b/ffplayout-api/unit/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/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 93% rename from src/input/ingest.rs rename to ffplayout-engine/src/input/ingest.rs index e65b72c2..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, GlobalConfig, Ingest, 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" { @@ -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/ffplayout-engine/src/input/mod.rs similarity index 90% rename from src/input/mod.rs rename to ffplayout-engine/src/input/mod.rs index a3dff4e5..5fcaea62 100644 --- a/src/input/mod.rs +++ b/ffplayout-engine/src/input/mod.rs @@ -9,19 +9,21 @@ use std::{ use simplelog::*; -use crate::utils::{GlobalConfig, Media, 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: GlobalConfig, + config: PlayoutConfig, current_list: Arc>>, index: Arc, playout_stat: PlayoutStatus, diff --git a/src/input/playlist.rs b/ffplayout-engine/src/input/playlist.rs similarity index 95% rename from src/input/playlist.rs rename to ffplayout-engine/src/input/playlist.rs index c67e71ac..ab8da117 100644 --- a/src/input/playlist.rs +++ b/ffplayout-engine/src/input/playlist.rs @@ -10,9 +10,9 @@ 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, 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>>, @@ -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 { @@ -171,8 +173,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; @@ -191,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); } @@ -390,7 +392,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 +442,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 +472,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/ffplayout-engine/src/main.rs similarity index 80% rename from src/main.rs rename to ffplayout-engine/src/main.rs index 4feba587..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, init_logging, send_mail, validate_ffmpeg, GlobalConfig, PlayerControl, - PlayoutStatus, ProcessControl, - }, +}; + +use ffplayout_lib::utils::{ + generate_playlist, init_logging, send_mail, validate_ffmpeg, PlayerControl, PlayoutStatus, + ProcessControl, }; #[derive(Serialize, Deserialize)] @@ -39,7 +49,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) @@ -56,7 +68,8 @@ fn status_file(stat_file: &str, playout_stat: &PlayoutStatus) { } fn main() { - let config = GlobalConfig::new(); + let args = get_args(); + let config = get_config(args); let config_clone = config.clone(); let play_control = PlayerControl::new(); let playout_stat = PlayoutStatus::new(); @@ -67,14 +80,17 @@ 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); 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/output/desktop.rs b/ffplayout-engine/src/output/desktop.rs similarity index 54% rename from src/output/desktop.rs rename to ffplayout-engine/src/output/desktop.rs index 5f0925a7..7c88bc06 100644 --- a/src/output/desktop.rs +++ b/ffplayout-engine/src/output/desktop.rs @@ -2,29 +2,31 @@ use std::process::{self, Command, Stdio}; use simplelog::*; -use crate::filter::v_drawtext; -use crate::utils::{GlobalConfig, Media}; -use crate::vec_strings; +use ffplayout_lib::filter::v_drawtext; +use ffplayout_lib::utils::{Media, PlayoutConfig}; +use ffplayout_lib::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"]; - 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/ffplayout-engine/src/output/hls.rs similarity index 94% rename from src/output/hls.rs rename to ffplayout-engine/src/output/hls.rs index e326fab0..c583625d 100644 --- a/src/output/hls.rs +++ b/ffplayout-engine/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 */ @@ -27,17 +27,17 @@ use std::{ 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, +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( - 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/ffplayout-engine/src/output/mod.rs similarity index 97% rename from src/output/mod.rs rename to ffplayout-engine/src/output/mod.rs index f6884e6c..337753a1 100644 --- a/src/output/mod.rs +++ b/ffplayout-engine/src/output/mod.rs @@ -16,10 +16,11 @@ mod stream; 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, +use ffplayout_lib::utils::{ + sec_to_time, stderr_reader, Decoder, PlayerControl, PlayoutConfig, PlayoutStatus, + ProcessControl, }; -use crate::vec_strings; +use ffplayout_lib::vec_strings; /// Player /// @@ -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/ffplayout-engine/src/output/stream.rs similarity index 62% rename from src/output/stream.rs rename to ffplayout-engine/src/output/stream.rs index 836fdb59..ae4eec74 100644 --- a/src/output/stream.rs +++ b/ffplayout-engine/src/output/stream.rs @@ -2,14 +2,14 @@ use std::process::{self, Command, Stdio}; use simplelog::*; -use crate::filter::v_drawtext; -use crate::utils::{prepare_output_cmd, GlobalConfig, Media}; -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 /// /// 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(); @@ -25,19 +25,21 @@ pub fn output(config: &GlobalConfig, 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/rpc/mod.rs b/ffplayout-engine/src/rpc/mod.rs similarity index 88% rename from src/rpc/mod.rs rename to ffplayout-engine/src/rpc/mod.rs index fadb1834..9e85bedd 100644 --- a/src/rpc/mod.rs +++ b/ffplayout-engine/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}, @@ -8,11 +11,13 @@ use jsonrpc_http_server::{ use serde_json::{json, Map}; use simplelog::*; -use crate::utils::{ - get_delta, get_sec, sec_to_time, write_status, GlobalConfig, Media, PlayerControl, - PlayoutStatus, ProcessControl, +use ffplayout_lib::utils::{ + 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!({ @@ -25,7 +30,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 +61,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, @@ -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/ffplayout-engine/src/rpc/zmq_cmd.rs b/ffplayout-engine/src/rpc/zmq_cmd.rs new file mode 100644 index 00000000..9238b3f5 --- /dev/null +++ b/ffplayout-engine/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/tests/mod.rs b/ffplayout-engine/src/tests/mod.rs similarity index 72% rename from src/tests/mod.rs rename to ffplayout-engine/src/tests/mod.rs index d12fd964..76b97867 100644 --- a/src/tests/mod.rs +++ b/ffplayout-engine/src/tests/mod.rs @@ -1,15 +1,12 @@ use std::{ - sync::{Arc, Mutex}, thread::{self, sleep}, time::Duration, }; -mod utils; - #[cfg(test)] use crate::output::player; #[cfg(test)] -use crate::utils::*; +use ffplayout_lib::utils::*; #[cfg(test)] use simplelog::*; @@ -22,26 +19,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 = PlayoutConfig::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 +44,24 @@ fn playlist_change_at_midnight() { #[test] #[ignore] fn playlist_change_at_six() { - let mut config = GlobalConfig::new(); + let mut config = PlayoutConfig::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/utils/arg_parse.rs b/ffplayout-engine/src/utils/arg_parse.rs similarity index 98% rename from src/utils/arg_parse.rs rename to ffplayout-engine/src/utils/arg_parse.rs index f5c28ec2..ef0550ba 100644 --- a/src/utils/arg_parse.rs +++ b/ffplayout-engine/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/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 70% rename from src/filter/a_loudnorm.rs rename to lib/src/filter/a_loudnorm.rs index efc6d3b6..57662082 100644 --- a/src/filter/a_loudnorm.rs +++ b/lib/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/lib/src/filter/ingest_filter.rs similarity index 89% rename from src/filter/ingest_filter.rs rename to lib/src/filter/ingest_filter.rs index 9838ee89..577221c7 100644 --- a/src/filter/ingest_filter.rs +++ b/lib/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/lib/src/filter/mod.rs similarity index 91% rename from src/filter/mod.rs rename to lib/src/filter/mod.rs index 9c99aa68..c2878839 100644 --- a/src/filter/mod.rs +++ b/lib/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,10 +137,10 @@ 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" + && &node.category != "advertisement" { let mut logo_chain = v_overlay::filter_node(config, false); @@ -183,8 +183,10 @@ 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) { - if config.text.add_text && config.text.over_pre { +fn add_text(node: &mut Media, chain: &mut Filters, config: &PlayoutConfig) { + 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"); @@ -208,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 @@ -233,7 +235,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 +249,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 +278,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 +307,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/lib/src/filter/v_drawtext.rs similarity index 77% rename from src/filter/v_drawtext.rs rename to lib/src/filter/v_drawtext.rs index 47c6ae14..34820ced 100644 --- a/src/filter/v_drawtext.rs +++ b/lib/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: &Media) -> String { let mut filter = String::new(); let mut font = String::new(); @@ -13,7 +13,7 @@ pub fn filter_node(config: &GlobalConfig, 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: &GlobalConfig, 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/filter/v_overlay.rs b/lib/src/filter/v_overlay.rs similarity index 87% rename from src/filter/v_overlay.rs rename to lib/src/filter/v_overlay.rs index 0607090c..de736a9c 100644 --- a/src/filter/v_overlay.rs +++ b/lib/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/lib.rs b/lib/src/lib.rs similarity index 72% rename from src/lib.rs rename to lib/src/lib.rs index 0738833b..da112113 100644 --- a/src/lib.rs +++ b/lib/src/lib.rs @@ -2,10 +2,8 @@ extern crate log; extern crate simplelog; 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 98% rename from src/tests/utils/mod.rs rename to lib/src/tests/mod.rs index 2267bdfe..f50cd743 100644 --- a/src/tests/utils/mod.rs +++ b/lib/src/tests/mod.rs @@ -39,7 +39,7 @@ fn get_date_tomorrow() { #[test] fn test_delta() { - let mut config = GlobalConfig::new(); + 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/lib/src/utils/config.rs similarity index 76% rename from src/utils/config.rs rename to lib/src/utils/config.rs index 2593c5bf..5371bca9 100644 --- a/src/utils/config.rs +++ b/lib/src/utils/config.rs @@ -8,14 +8,14 @@ use std::{ use serde::{Deserialize, Serialize}; use shlex::split; -use crate::utils::{get_args, time_to_sec}; +use crate::utils::{free_tcp_socket, time_to_sec}; use crate::vec_strings; /// Global Config /// /// 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, @@ -30,7 +30,10 @@ pub struct GlobalConfig { #[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,9 +135,15 @@ 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, + + #[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, @@ -126,21 +152,26 @@ 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>, } -impl GlobalConfig { +impl PlayoutConfig { /// Read config from YAML file, and set some extra config values. - pub fn new() -> Self { - let args = get_args(); + pub fn new(cfg_path: Option) -> Self { let mut config_path = PathBuf::from("/etc/ffplayout/ffplayout.yml"); - if let Some(cfg) = args.config { + if let Some(cfg) = cfg_path { config_path = PathBuf::from(cfg); } @@ -162,7 +193,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() @@ -217,66 +248,24 @@ impl GlobalConfig { config.out.preview_cmd = split(config.out.preview_param.as_str()); config.out.output_cmd = split(config.out.output_param.as_str()); - // 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; - } - 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; + // 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; } config } } -impl Default for GlobalConfig { +impl Default for PlayoutConfig { fn default() -> Self { - Self::new() + Self::new(None) } } 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 60% rename from src/input/folder.rs rename to lib/src/utils/folder.rs index 9c9cf69c..0aa569f4 100644 --- a/src/input/folder.rs +++ b/lib/src/utils/folder.rs @@ -1,32 +1,24 @@ use std::{ - ffi::OsStr, 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; -use crate::utils::{get_sec, GlobalConfig, Media}; +use crate::utils::{file_extension, 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 +26,7 @@ pub struct FolderSource { impl FolderSource { pub fn new( - config: &GlobalConfig, + config: &PlayoutConfig, current_list: Arc>>, global_index: Arc, ) -> Self { @@ -154,65 +146,3 @@ 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. -pub fn watchman( - config: GlobalConfig, - 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 77% rename from src/utils/generator.rs rename to lib/src/utils/generator.rs index b842deda..c81bcb20 100644 --- a/src/utils/generator.rs +++ b/lib/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}, @@ -16,8 +17,8 @@ use std::{ use chrono::{Duration, NaiveDate}; use simplelog::*; -use crate::input::FolderSource; -use crate::utils::{json_serializer::Playlist, GlobalConfig, Media}; +use super::folder::FolderSource; +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: &GlobalConfig, 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: &GlobalConfig, 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!( @@ -103,7 +120,8 @@ 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 { + channel: channel.clone(), date, current_file: None, start_sec: None, @@ -130,17 +148,12 @@ pub fn generate_playlist(config: &GlobalConfig, 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) } diff --git a/src/utils/json_serializer.rs b/lib/src/utils/json_serializer.rs similarity index 84% rename from src/utils/json_serializer.rs rename to lib/src/utils/json_serializer.rs index 5408c6c4..6eeca4a9 100644 --- a/src/utils/json_serializer.rs +++ b/lib/src/utils/json_serializer.rs @@ -9,14 +9,15 @@ 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 channel: String, pub date: String, #[serde(skip_serializing, skip_deserializing)] @@ -31,13 +32,14 @@ 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); media.duration = DUMMY_LEN; media.out = DUMMY_LEN; Self { + channel: "Channel 1".into(), date, start_sec: Some(start), current_file: None, @@ -47,7 +49,19 @@ impl Playlist { } } -fn set_defaults(mut playlist: Playlist, current_file: String, mut start_sec: f64) -> Playlist { +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, + mut start_sec: f64, +) -> JsonPlaylist { playlist.current_file = Some(current_file); playlist.start_sec = Some(start_sec); @@ -66,15 +80,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 +118,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 +141,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); @@ -138,7 +152,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}"); - Playlist::new(date, start_sec) + JsonPlaylist::new(date, start_sec) } diff --git a/src/utils/json_validate.rs b/lib/src/utils/json_validate.rs similarity index 87% rename from src/utils/json_validate.rs rename to lib/src/utils/json_validate.rs index f16024bf..5a359917 100644 --- a/src/utils/json_validate.rs +++ b/lib/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/logging.rs b/lib/src/utils/logging.rs similarity index 87% rename from src/utils/logging.rs rename to lib/src/utils/logging.rs index 88a379f4..035faf43 100644 --- a/src/utils/logging.rs +++ b/lib/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, @@ -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(); @@ -166,9 +169,9 @@ fn clean_string(text: &str) -> String { /// - file logger /// - mail logger pub fn init_logging( - config: &GlobalConfig, - proc_ctl: ProcessControl, - messages: Arc>>, + config: &PlayoutConfig, + proc_ctl: Option, + messages: Option>>>, ) -> Vec> { let config_clone = config.clone(); let app_config = config.logging.clone(); @@ -182,6 +185,9 @@ 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) .set_time_level(time_level) .clone(); @@ -193,7 +199,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!( @@ -247,10 +253,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 +268,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/lib/src/utils/mod.rs similarity index 89% rename from src/utils/mod.rs rename to lib/src/utils/mod.rs index 5e01c6f7..3cbc0881 100644 --- a/src/utils/mod.rs +++ b/lib/src/utils/mod.rs @@ -1,6 +1,8 @@ use std::{ + ffi::OsStr, fs::{self, metadata}, io::{BufRead, BufReader, Error}, + net::TcpListener, path::Path, process::{exit, ChildStderr, Command, Stdio}, time::{self, UNIX_EPOCH}, @@ -9,25 +11,25 @@ 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}; use serde_json::json; use simplelog::*; -mod arg_parse; -mod config; +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; -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}; @@ -46,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)] @@ -92,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![]), @@ -123,14 +124,26 @@ 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)) } } +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>, @@ -188,10 +201,25 @@ 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#"id:[0-9]+,?|name:[^,]?,?"#).unwrap(); + let re3 = Regex::new(r#"text:([^,]*)"#).unwrap(); + let text = re1.replace_all(&raw_text, ""); + let text = re2.replace_all(&text, "").clone(); + let filter = re3 + .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. -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, @@ -199,7 +227,7 @@ pub fn write_status(config: &GlobalConfig, 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:?}") }; } @@ -294,6 +322,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 { @@ -308,7 +341,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 +372,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 +382,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 +600,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"); @@ -596,6 +629,19 @@ pub fn validate_ffmpeg(config: &GlobalConfig) { } } +/// 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 {