diff --git a/.gitignore b/.gitignore index bd26c0c9..f70d6280 100644 --- a/.gitignore +++ b/.gitignore @@ -26,3 +26,4 @@ ffpapi.1.gz /public/ tmp/ .vscode/ +assets/playlist_template.json diff --git a/Cargo.lock b/Cargo.lock index 7df33b52..53aaa62f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -55,17 +55,17 @@ dependencies = [ [[package]] name = "actix-http" -version = "3.3.1" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2079246596c18b4a33e274ae10c0e50613f4d32a4198e09c7b93771013fed74" +checksum = "a92ef85799cba03f76e4f7c10f533e66d87c9a7e7055f3391f09000ad8351bc9" dependencies = [ "actix-codec", "actix-rt", "actix-service", "actix-utils", - "ahash 0.8.3", - "base64 0.21.2", - "bitflags 1.3.2", + "ahash", + "base64 0.21.3", + "bitflags 2.4.0", "brotli", "bytes", "bytestring", @@ -94,30 +94,50 @@ dependencies = [ [[package]] name = "actix-macros" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "465a6172cf69b960917811022d8f29bc0b7fa1398bc4f78b3c466673db1213b6" +checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb" dependencies = [ "quote", - "syn 1.0.109", + "syn 2.0.31", ] [[package]] name = "actix-multipart" -version = "0.5.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5011f5be460e35a5b82e1745c0ea0c6293e8f8d38bbaa1f0455afcff5b454ad6" +checksum = "3b960e2aea75f49c8f069108063d12a48d329fc8b60b786dfc7552a9d5918d2d" dependencies = [ + "actix-multipart-derive", "actix-utils", "actix-web", "bytes", "derive_more", "futures-core", + "futures-util", "httparse", "local-waker", "log", "memchr", "mime", + "serde", + "serde_json", + "serde_plain", + "tempfile", + "tokio", +] + +[[package]] +name = "actix-multipart-derive" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a0a77f836d869f700e5b47ac7c3c8b9c8bc82e4aec861954c6198abee3ebd4d" +dependencies = [ + "darling", + "parse-size", + "proc-macro2", + "quote", + "syn 2.0.31", ] [[package]] @@ -135,9 +155,9 @@ dependencies = [ [[package]] name = "actix-rt" -version = "2.8.0" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15265b6b8e2347670eb363c47fc8c75208b4a4994b27192f345fcbe707804f3e" +checksum = "28f32d40287d3f402ae0028a9d54bef51af15c8769492826a69d28f81893151d" dependencies = [ "futures-core", "tokio", @@ -145,9 +165,9 @@ dependencies = [ [[package]] name = "actix-server" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e8613a75dd50cc45f473cee3c34d59ed677c0f7b44480ce3b8247d7dc519327" +checksum = "3eb13e7eef0423ea6eab0e59f6c72e7cb46d33691ad56a726b3cd07ddec2c2d4" dependencies = [ "actix-rt", "actix-service", @@ -155,8 +175,7 @@ dependencies = [ "futures-core", "futures-util", "mio", - "num_cpus", - "socket2", + "socket2 0.5.3", "tokio", "tracing", ] @@ -184,9 +203,9 @@ dependencies = [ [[package]] name = "actix-web" -version = "4.3.1" +version = "4.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd3cb42f9566ab176e1ef0b8b3a896529062b4efc6be0123046095914c4c1c96" +checksum = "0e4a5b5e29603ca8c94a77c65cf874718ceb60292c5a5c3e5f4ace041af462b9" dependencies = [ "actix-codec", "actix-http", @@ -197,7 +216,7 @@ dependencies = [ "actix-service", "actix-utils", "actix-web-codegen", - "ahash 0.7.6", + "ahash", "bytes", "bytestring", "cfg-if", @@ -206,7 +225,6 @@ dependencies = [ "encoding_rs", "futures-core", "futures-util", - "http", "itoa", "language-tags", "log", @@ -218,21 +236,21 @@ dependencies = [ "serde_json", "serde_urlencoded", "smallvec", - "socket2", - "time 0.3.23", + "socket2 0.5.3", + "time", "url", ] [[package]] name = "actix-web-codegen" -version = "4.2.0" +version = "4.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2262160a7ae29e3415554a3f1fc04c764b1540c116aa524683208078b7a75bc9" +checksum = "eb1f50ebbb30eca122b188319a4398b3f7bb4a8cdf50ecfb73bfc6a3c3ce54f5" dependencies = [ "actix-router", "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.31", ] [[package]] @@ -247,24 +265,24 @@ dependencies = [ [[package]] name = "actix-web-httpauth" -version = "0.6.0" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08c25a48b4684f90520183cd1a688e5f4f7e9905835fa75d02c0fe4f60fcdbe6" +checksum = "6dda62cf04bc3a9ad2ea8f314f721951cfdb4cdacec4e984d20e77c7bb170991" dependencies = [ - "actix-service", "actix-utils", "actix-web", "base64 0.13.1", "futures-core", "futures-util", + "log", "pin-project-lite", ] [[package]] name = "addr2line" -version = "0.20.0" +version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4fa78e18c64fce05e902adecd7a5eed15a5e0a3439f7b0e169f0252214865e3" +checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" dependencies = [ "gimli", ] @@ -275,17 +293,6 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" -[[package]] -name = "ahash" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" -dependencies = [ - "getrandom", - "once_cell", - "version_check", -] - [[package]] name = "ahash" version = "0.8.3" @@ -300,9 +307,9 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.0.2" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43f6cb1bf222025340178f382c426f13757b2960e89779dfcb319c32542a5a41" +checksum = "0c378d78423fdad8089616f827526ee33c19f2fddbd5de1629152c9593ba4783" dependencies = [ "memchr", ] @@ -345,24 +352,23 @@ dependencies = [ [[package]] name = "anstream" -version = "0.3.2" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ca84f3628370c59db74ee214b3263d58f9aadd9b4fe7e711fd87dc452b7f163" +checksum = "b1f58811cfac344940f1a400b6e6231ce35171f614f26439e80f8c1465c5cc0c" dependencies = [ "anstyle", "anstyle-parse", "anstyle-query", "anstyle-wincon", "colorchoice", - "is-terminal", "utf8parse", ] [[package]] name = "anstyle" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a30da5c5f2d5e72842e00bcb57657162cdabef0931f40e2deb9b4140440cecd" +checksum = "15c4c2c83f81532e5845a733998b6971faca23490340a418e9b72a3ec9de12ea" [[package]] name = "anstyle-parse" @@ -379,17 +385,17 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" dependencies = [ - "windows-sys 0.48.0", + "windows-sys", ] [[package]] name = "anstyle-wincon" -version = "1.0.1" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "180abfa45703aebe0093f79badacc01b8fd4ea2e35118747e5811127f926e188" +checksum = "58f54d10c6dfa51283a066ceab3ec1ab78d13fae00aa49243a45e4571fb79dfd" dependencies = [ "anstyle", - "windows-sys 0.48.0", + "windows-sys", ] [[package]] @@ -400,12 +406,13 @@ checksum = "70033777eb8b5124a81a1889416543dddef2de240019b674c81285a2635a7e1e" [[package]] name = "argon2" -version = "0.4.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db4ce4441f99dbd377ca8a8f57b698c44d0d6e712d8329b5040da5a64aa1ce73" +checksum = "17ba4cac0a46bc1d2912652a751c47f2a9f3a7fe89bcae2275d418f5270402f9" dependencies = [ "base64ct", "blake2", + "cpufeatures", "password-hash", ] @@ -451,7 +458,7 @@ dependencies = [ "async-lock", "async-task", "concurrent-queue", - "fastrand", + "fastrand 1.9.0", "futures-lite", "slab", ] @@ -487,15 +494,15 @@ dependencies = [ "polling", "rustix 0.37.23", "slab", - "socket2", + "socket2 0.4.9", "waker-fn", ] [[package]] name = "async-lock" -version = "2.7.0" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa24f727524730b077666307f2734b4a1a1c57acb79193127dcc8914d5242dd7" +checksum = "287272293e9d8c41773cec55e365490fe034813a2f172f502d6ddcf75b2f582b" dependencies = [ "event-listener", ] @@ -535,20 +542,20 @@ checksum = "ecc7ab41815b3c653ccd2978ec3255c81349336702dfdf62ee6f7069b12a3aae" [[package]] name = "async-trait" -version = "0.1.71" +version = "0.1.73" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a564d521dd56509c4c47480d00b80ee55f7e385ae48db5744c67ad50c92d2ebf" +checksum = "bc00ceb34980c03614e35a3a4e218276a0a824e911d07651cd0d858a51e8c0f0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.26", + "syn 2.0.31", ] [[package]] name = "asynchronous-codec" -version = "0.6.1" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06a0daa378f5fd10634e44b0a29b2a87b890657658e072a30d6f26e57ddee182" +checksum = "4057f2c32adbb2fc158e22fb38433c8e9bbf76b75a4732c7c0cbaf695fb65568" dependencies = [ "bytes", "futures-sink", @@ -580,9 +587,9 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "backtrace" -version = "0.3.68" +version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4319208da049c43661739c5fade2ba182f09d1dc2299b32298d3a31692b17e12" +checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" dependencies = [ "addr2line", "cc", @@ -601,9 +608,9 @@ checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" [[package]] name = "base64" -version = "0.21.2" +version = "0.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "604178f6c5c21f02dc555784810edfb88d34ac2c73b2eae109655649ee73ce3d" +checksum = "414dcefbc63d77c526a76b3afcf6fbb9b5e2791c19c3aa2297733208750c6e53" [[package]] name = "base64ct" @@ -619,9 +626,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.3.3" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "630be753d4e58660abd17930c71b647fe46c27ea6b63cc59e1e3851406972e42" +checksum = "b4682ae6287fcf752ecaabbfcc7b6f9b72aa33933dc23a554d853aea8eea8635" dependencies = [ "serde", ] @@ -654,7 +661,7 @@ dependencies = [ "async-lock", "async-task", "atomic-waker", - "fastrand", + "fastrand 1.9.0", "futures-lite", "log", ] @@ -694,9 +701,9 @@ checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" [[package]] name = "bytes" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" +checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" [[package]] name = "bytestring" @@ -709,11 +716,12 @@ dependencies = [ [[package]] name = "cc" -version = "1.0.79" +version = "1.0.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" +checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" dependencies = [ "jobserver", + "libc", ] [[package]] @@ -724,17 +732,17 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.26" +version = "0.4.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec837a71355b28f6556dbd569b37b3f363091c0bd4b2e735674521b4c5fd9bc5" +checksum = "defd4e7873dbddba6c7c91e199c7fcb946abc4a6a4ac3195400bcfb01b5de877" dependencies = [ "android-tzdata", "iana-time-zone", "js-sys", "num-traits", - "time 0.1.45", + "serde", "wasm-bindgen", - "winapi", + "windows-targets", ] [[package]] @@ -745,20 +753,19 @@ checksum = "cca491388666e04d7248af3f60f0c40cfb0991c72205595d7c396e3510207d1a" [[package]] name = "clap" -version = "4.3.12" +version = "4.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3eab9e8ceb9afdade1ab3f0fd8dbce5b1b2f468ad653baf10e771781b2b67b73" +checksum = "6a13b88d2c62ff462f88e4a121f17a82c1af05693a2f192b5c38d14de73c19f6" dependencies = [ "clap_builder", "clap_derive", - "once_cell", ] [[package]] name = "clap_builder" -version = "4.3.12" +version = "4.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f2763db829349bf00cfc06251268865ed4363b93a943174f638daf3ecdba2cd" +checksum = "2bb9faaa7c2ef94b2743a21f5a29e6f0010dff4caa69ac8e9d6cf8b6fa74da08" dependencies = [ "anstream", "anstyle", @@ -768,21 +775,21 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.3.12" +version = "4.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54a9bb5758fc5dfe728d1019941681eccaf0cf8a4189b692a0ee2f2ecf90a050" +checksum = "0862016ff20d69b84ef8247369fabf5c008a7417002411897d40ee1f4532b873" dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.26", + "syn 2.0.31", ] [[package]] name = "clap_lex" -version = "0.5.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b" +checksum = "cd7cc57abe963c6d3b9d8be5b06ba7c8957a930305ca90304f24ef040aa6f961" [[package]] name = "colorchoice" @@ -801,9 +808,9 @@ dependencies = [ [[package]] name = "const-oid" -version = "0.9.4" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "795bc6e66a8e340f075fcf6227e417a2dc976b92b91f3cdc778bb858778b6747" +checksum = "28c122c3980598d243d63d9a704629a2d748d101f278052ff068be5a4423ab6f" [[package]] name = "convert_case" @@ -818,7 +825,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb" dependencies = [ "percent-encoding", - "time 0.3.23", + "time", "version_check", ] @@ -911,10 +918,45 @@ dependencies = [ ] [[package]] -name = "dashmap" -version = "5.5.0" +name = "darling" +version = "0.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6943ae99c34386c84a470c499d3414f66502a41340aa895406e0d2e4a207b91d" +checksum = "0209d94da627ab5605dcccf08bb18afa5009cfbef48d8a8b7d7bdbc79be25c5e" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "177e3443818124b357d8e76f53be906d60937f0d3a90773a664fa63fa253e621" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.31", +] + +[[package]] +name = "darling_macro" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "836a9bbc7ad63342d6d6e7b815ccab164bc77a2d95d84bc3117a8c0d5c98e2d5" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.31", +] + +[[package]] +name = "dashmap" +version = "5.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" dependencies = [ "cfg-if", "hashbrown 0.14.0", @@ -925,15 +967,21 @@ dependencies = [ [[package]] name = "der" -version = "0.7.7" +version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c7ed52955ce76b1554f509074bb357d3fb8ac9b51288a65a3fd480d1dfba946" +checksum = "fffa369a668c8af7dbf8b5e56c9f744fbd399949ed171606040001947de40b1c" dependencies = [ "const-oid", "pem-rfc7468", "zeroize", ] +[[package]] +name = "deranged" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2696e8a945f658fd14dc3b87242e6b80cd0f36ff04ea560fa39082368847946" + [[package]] name = "derive_more" version = "0.99.17" @@ -967,9 +1015,9 @@ checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" [[package]] name = "either" -version = "1.8.1" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" +checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" dependencies = [ "serde", ] @@ -980,7 +1028,7 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dbfb21b9878cf7a348dcb8559109aabc0ec40d69924bd706fa5149846c4fef75" dependencies = [ - "base64 0.21.2", + "base64 0.21.3", "memchr", ] @@ -992,9 +1040,9 @@ checksum = "e2153bd83ebc09db15bcbdc3e2194d901804952e3dc96967e1cd3b0c5c32d112" [[package]] name = "encoding_rs" -version = "0.8.32" +version = "0.8.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071a31f4ee85403370b58aca746f01041ede6f0da2730960ad001edc2b71b394" +checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1" dependencies = [ "cfg-if", ] @@ -1007,13 +1055,13 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" -version = "0.3.1" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bcfec3a70f97c962c307b2d2c56e358cf1d00b558d74262b5f929ee8cc7e73a" +checksum = "136526188508e25c6fef639d7927dfb3e0e3084488bf202267829cf7fc23dbdd" dependencies = [ "errno-dragonfly", "libc", - "windows-sys 0.48.0", + "windows-sys", ] [[package]] @@ -1034,7 +1082,7 @@ checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943" dependencies = [ "cfg-if", "home", - "windows-sys 0.48.0", + "windows-sys", ] [[package]] @@ -1063,6 +1111,12 @@ dependencies = [ "instant", ] +[[package]] +name = "fastrand" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6999dc1837253364c2ebb0704ba97994bd874e8f195d665c50b7548f6ea92764" + [[package]] name = "ffplayout" version = "0.20.0" @@ -1072,9 +1126,11 @@ dependencies = [ "crossbeam-channel", "ffplayout-lib", "futures", + "itertools", "notify", "notify-debouncer-full", "openssl", + "rand", "regex", "reqwest", "serde", @@ -1126,6 +1182,7 @@ dependencies = [ "ffprobe", "file-rotate", "lettre", + "lexical-sort", "log", "openssl", "rand", @@ -1135,8 +1192,9 @@ dependencies = [ "serde_json", "serde_yaml", "shlex", + "signal-child", "simplelog", - "time 0.3.23", + "time", "walkdir", "winapi", ] @@ -1153,11 +1211,11 @@ dependencies = [ [[package]] name = "file-id" -version = "0.1.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13be71e6ca82e91bc0cb862bebaac0b2d1924a5a1d970c822b2f98b63fda8c3" +checksum = "6584280525fb2059cba3db2c04abf947a1a29a45ddae89f3870f8281704fafc9" dependencies = [ - "winapi-util", + "windows-sys", ] [[package]] @@ -1172,21 +1230,27 @@ dependencies = [ [[package]] name = "filetime" -version = "0.2.21" +version = "0.2.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cbc844cecaee9d4443931972e1289c8ff485cb4cc2767cb03ca139ed6885153" +checksum = "d4029edd3e734da6fe05b6cd7bd2960760a616bd2ddd0d59a0124746d6272af0" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.2.16", - "windows-sys 0.48.0", + "redox_syscall", + "windows-sys", ] [[package]] -name = "flate2" -version = "1.0.26" +name = "finl_unicode" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b9429470923de8e8cbd4d2dc513535400b4b3fef0319fb5c4e1f520a7bef743" +checksum = "8fcfdc7a0362c9f4444381a9e697c79d435fe65b52a37466fc2c1184cee9edc6" + +[[package]] +name = "flate2" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6c98ee8095e9d1dcbf2fcc6d95acccb90d1c81db1e44725c6a984b1dbdfb010" dependencies = [ "crc32fast", "miniz_oxide", @@ -1308,7 +1372,7 @@ version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce" dependencies = [ - "fastrand", + "fastrand 1.9.0", "futures-core", "futures-io", "memchr", @@ -1325,7 +1389,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" dependencies = [ "proc-macro2", "quote", - "syn 2.0.26", + "syn 2.0.31", ] [[package]] @@ -1376,14 +1440,14 @@ checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" dependencies = [ "cfg-if", "libc", - "wasi 0.11.0+wasi-snapshot-preview1", + "wasi", ] [[package]] name = "gimli" -version = "0.27.3" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c80984affa11d98d1b88b66ac8853f143217b399d3c74116778ff8fdb4ed2e" +checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0" [[package]] name = "gloo-timers" @@ -1399,9 +1463,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.3.20" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97ec8491ebaf99c8eaa73058b045fe58073cd6be7f596ac993ced0b0a0c01049" +checksum = "91fc23aa11be92976ef4729127f1a74adf36d8436f7816b185d18df956790833" dependencies = [ "bytes", "fnv", @@ -1428,15 +1492,15 @@ version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a" dependencies = [ - "ahash 0.8.3", + "ahash", "allocator-api2", ] [[package]] name = "hashlink" -version = "0.8.3" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "312f66718a2d7789ffef4f4b7b213138ed9f1eb3aa1d0d82fc99f88fb3ffd26f" +checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7" dependencies = [ "hashbrown 0.14.0", ] @@ -1486,7 +1550,7 @@ version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5444c27eef6923071f7ebcc33e3444508466a76f7a2b93da00ed6e19f30c1ddb" dependencies = [ - "windows-sys 0.48.0", + "windows-sys", ] [[package]] @@ -1536,9 +1600,9 @@ checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" [[package]] name = "httpdate" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "hyper" @@ -1557,7 +1621,7 @@ dependencies = [ "httpdate", "itoa", "pin-project-lite", - "socket2", + "socket2 0.4.9", "tokio", "tower-service", "tracing", @@ -1600,6 +1664,12 @@ dependencies = [ "cc", ] +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "idna" version = "0.3.0" @@ -1677,7 +1747,7 @@ checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" dependencies = [ "hermit-abi", "libc", - "windows-sys 0.48.0", + "windows-sys", ] [[package]] @@ -1686,22 +1756,11 @@ version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28b29a3cd74f0f4598934efe3aeba42bae0eb4680554128851ebbecb02af14e6" -[[package]] -name = "is-terminal" -version = "0.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" -dependencies = [ - "hermit-abi", - "rustix 0.38.4", - "windows-sys 0.48.0", -] - [[package]] name = "itertools" -version = "0.10.5" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" dependencies = [ "either", ] @@ -1736,7 +1795,7 @@ version = "8.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6971da4d9c3aa03c3d8f3ff0f4155b534aad021292003895a469716b2a230378" dependencies = [ - "base64 0.21.2", + "base64 0.21.3", "pem", "ring", "serde", @@ -1746,9 +1805,9 @@ dependencies = [ [[package]] name = "kqueue" -version = "1.0.7" +version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c8fc60ba15bf51257aa9807a48a61013db043fcf3a78cb0d916e8e396dcad98" +checksum = "7447f1ca1b7b563588a205fe93dea8df60fd981423a768bc1c0ded35ed147d0c" dependencies = [ "kqueue-sys", "libc", @@ -1756,9 +1815,9 @@ dependencies = [ [[package]] name = "kqueue-sys" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8367585489f01bc55dd27404dcf56b95e6da061a256a666ab23be9ba96a2e587" +checksum = "ed9625ffda8729b85e45cf04090035ac368927b8cebc34898e7c120f52e4838b" dependencies = [ "bitflags 1.3.2", "libc", @@ -1794,10 +1853,10 @@ version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76bd09637ae3ec7bd605b8e135e757980b3968430ff2b1a4a94fb7769e50166d" dependencies = [ - "base64 0.21.2", + "base64 0.21.3", "email-encoding", "email_address", - "fastrand", + "fastrand 1.9.0", "futures-util", "hostname", "httpdate", @@ -1807,7 +1866,7 @@ dependencies = [ "nom", "once_cell", "quoted_printable", - "socket2", + "socket2 0.4.9", "tokio", ] @@ -1851,9 +1910,9 @@ checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" [[package]] name = "linux-raw-sys" -version = "0.4.3" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09fc20d2ca12cb9f044c93e3bd6d32d523e6e2ec3db4f7b2939cd99026ecd3f0" +checksum = "57bcfdad1b858c2db7c38303a6d2ad4dfaf5eb53dfeb0910128b2c26d6158503" [[package]] name = "local-channel" @@ -1885,9 +1944,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.19" +version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b06a4cde4c0f271a446782e3eff8de789548ce57dbc8eca9292c27f4a42004b4" +checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" dependencies = [ "value-bag", ] @@ -1909,9 +1968,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.5.0" +version = "2.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" +checksum = "8f232d6ef707e1956a43342693d2a31e72989554d58299d7a88738cc95b0d35c" [[package]] name = "mime" @@ -1952,8 +2011,8 @@ checksum = "927a765cd3fc26206e66b296465fa9d3e5ab003e651c1b3c060e7956d96b19d2" dependencies = [ "libc", "log", - "wasi 0.11.0+wasi-snapshot-preview1", - "windows-sys 0.48.0", + "wasi", + "windows-sys", ] [[package]] @@ -1986,29 +2045,31 @@ dependencies = [ [[package]] name = "notify" -version = "6.0.1" +version = "6.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5738a2795d57ea20abec2d6d76c6081186709c0024187cd5977265eda6598b51" +checksum = "6205bd8bb1e454ad2e27422015fb5e4f2bcc7e08fa8f27058670d208324a4d2d" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.4.0", "crossbeam-channel", "filetime", "fsevent-sys", "inotify", "kqueue", "libc", + "log", "mio", "walkdir", - "windows-sys 0.45.0", + "windows-sys", ] [[package]] name = "notify-debouncer-full" -version = "0.2.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "416969970ec751a5d702a88c6cd19ac1332abe997fce43f96db0418550426241" +checksum = "49f5dab59c348b9b50cf7f261960a20e389feb2713636399cd9082cd4b536154" dependencies = [ "file-id", + "log", "notify", "parking_lot", "walkdir", @@ -2016,9 +2077,9 @@ dependencies = [ [[package]] name = "num-bigint" -version = "0.4.3" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f93ab6289c7b344a8a9f60f88d80aa20032336fe78da341afc91c8a2341fc75f" +checksum = "608e7659b5c3d7cba262d894801b9ec9d00de989e8a82bd4bef91d08da45cdc0" dependencies = [ "autocfg", "num-integer", @@ -2065,9 +2126,9 @@ dependencies = [ [[package]] name = "num-traits" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +checksum = "f30b0abd723be7e2ffca1272140fac1a2f084c77ec3e123c192b66af1ee9e6c2" dependencies = [ "autocfg", "libm", @@ -2094,9 +2155,9 @@ dependencies = [ [[package]] name = "object" -version = "0.31.1" +version = "0.32.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bda667d9f2b5051b8833f59f3bf748b28ef54f850f4fcb389a252aa383866d1" +checksum = "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0" dependencies = [ "memchr", ] @@ -2109,11 +2170,11 @@ checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" [[package]] name = "openssl" -version = "0.10.55" +version = "0.10.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "345df152bc43501c5eb9e4654ff05f794effb78d4efe3d53abc158baddc0703d" +checksum = "bac25ee399abb46215765b1cb35bc0212377e58a061560d8b29b024fd0430e7c" dependencies = [ - "bitflags 1.3.2", + "bitflags 2.4.0", "cfg-if", "foreign-types", "libc", @@ -2130,7 +2191,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.26", + "syn 2.0.31", ] [[package]] @@ -2141,18 +2202,18 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-src" -version = "111.26.0+1.1.1u" +version = "300.1.3+3.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "efc62c9f12b22b8f5208c23a7200a442b2e5999f8bdf80233852122b5a4f6f37" +checksum = "cd2c101a165fff9935e34def4669595ab1c7847943c42be86e21503e482be107" dependencies = [ "cc", ] [[package]] name = "openssl-sys" -version = "0.9.90" +version = "0.9.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "374533b0e45f3a7ced10fcaeccca020e66656bc03dac384f852e4e5a7a8104a6" +checksum = "db4d56a4c0478783083cfafcc42493dd4a981d41669da64b4572a2a089b51b1d" dependencies = [ "cc", "libc", @@ -2191,16 +2252,22 @@ checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447" dependencies = [ "cfg-if", "libc", - "redox_syscall 0.3.5", + "redox_syscall", "smallvec", - "windows-targets 0.48.1", + "windows-targets", ] [[package]] -name = "password-hash" -version = "0.4.2" +name = "parse-size" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7676374caaee8a325c9e7a2ae557f216c5563a171d6997b0ef8a65af35147700" +checksum = "944553dd59c802559559161f9816429058b869003836120e262e8caec061b7ae" + +[[package]] +name = "password-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" dependencies = [ "base64ct", "rand_core", @@ -2239,29 +2306,29 @@ checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" [[package]] name = "pin-project" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "030ad2bc4db10a8944cb0d837f158bdfec4d4a4873ab701a95046770d11f8842" +checksum = "fda4ed1c6c173e3fc7a83629421152e01d7b1f9b7f65fb301e490e8cfc656422" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec2e072ecce94ec471b13398d5402c188e76ac03cf74dd1a975161b23a3f6d9c" +checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" dependencies = [ "proc-macro2", "quote", - "syn 2.0.26", + "syn 2.0.31", ] [[package]] name = "pin-project-lite" -version = "0.2.10" +version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c40d25201921e5ff0c862a505c6557ea88568a4e3ace775ab55e93f2f4f9d57" +checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" [[package]] name = "pin-utils" @@ -2309,7 +2376,7 @@ dependencies = [ "libc", "log", "pin-project-lite", - "windows-sys 0.48.0", + "windows-sys", ] [[package]] @@ -2329,9 +2396,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.31" +version = "1.0.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fe8a65d69dd0808184ebb5f836ab526bb259db23c657efa38711b1072ee47f0" +checksum = "5267fca4496028628a95160fc423a33e8b2e6af8a5302579e322e4b520293cae" dependencies = [ "proc-macro2", ] @@ -2372,15 +2439,6 @@ dependencies = [ "getrandom", ] -[[package]] -name = "redox_syscall" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" -dependencies = [ - "bitflags 1.3.2", -] - [[package]] name = "redox_syscall" version = "0.3.5" @@ -2392,9 +2450,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.9.1" +version = "1.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2eae68fc220f7cf2532e4494aded17545fce192d59cd996e0fe7887f4ceb575" +checksum = "697061221ea1b4a94a624f67d0ae2bfe4e22b8a17b6a192afb11046542cc8c47" dependencies = [ "aho-corasick", "memchr", @@ -2404,9 +2462,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.3.3" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39354c10dd07468c2e73926b23bb9c2caca74c5501e38a35da70406f1d923310" +checksum = "c2f401f4955220693b56f8ec66ee9c78abffd8d1c4f23dc41a23839eb88f0795" dependencies = [ "aho-corasick", "memchr", @@ -2415,23 +2473,23 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5ea92a5b6195c6ef2a0295ea818b312502c6fc94dde986c5553242e18fd4ce2" +checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" [[package]] name = "relative-path" -version = "1.8.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bf2521270932c3c7bed1a59151222bd7643c79310f2916f01925e1e16255698" +checksum = "c707298afce11da2efef2f600116fa93ffa7a032b5d7b628aa17711ec81383ca" [[package]] name = "reqwest" -version = "0.11.18" +version = "0.11.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cde824a14b7c14f85caff81225f411faacc04a2013f41670f41443742b1c1c55" +checksum = "3e9ad3fe7488d7e34558a2033d45a0c90b72d97b4f80705666fea71472e2e6a1" dependencies = [ - "base64 0.21.2", + "base64 0.21.3", "bytes", "encoding_rs", "futures-core", @@ -2479,13 +2537,12 @@ dependencies = [ [[package]] name = "rpassword" -version = "6.0.1" +version = "7.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bf099a1888612545b683d2661a1940089f6c2e5a8e38979b2159da876bfd956" +checksum = "6678cf63ab3491898c0d021b493c94c9b221d91295294a2a5746eacbe5928322" dependencies = [ "libc", - "serde", - "serde_json", + "rtoolbox", "winapi", ] @@ -2511,6 +2568,16 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rtoolbox" +version = "0.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "034e22c514f5c0cb8a10ff341b9b048b5ceb21591f31c8f44c43b960f9b3524a" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "rustc-demangle" version = "0.1.23" @@ -2537,20 +2604,20 @@ dependencies = [ "io-lifetimes", "libc", "linux-raw-sys 0.3.8", - "windows-sys 0.48.0", + "windows-sys", ] [[package]] name = "rustix" -version = "0.38.4" +version = "0.38.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a962918ea88d644592894bc6dc55acc6c0956488adcebbfb6e273506b7fd6e5" +checksum = "c0c3dde1fc030af041adc40e79c0e7fbcf431dd24870053d187d7c66e4b87453" dependencies = [ - "bitflags 2.3.3", + "bitflags 2.4.0", "errno", "libc", - "linux-raw-sys 0.4.3", - "windows-sys 0.48.0", + "linux-raw-sys 0.4.5", + "windows-sys", ] [[package]] @@ -2570,9 +2637,9 @@ dependencies = [ [[package]] name = "sanitize-filename" -version = "0.3.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf18934a12018228c5b55a6dae9df5d0641e3566b3630cb46cc55564068e7c2f" +checksum = "2ed72fbaf78e6f2d41744923916966c4fbe3d7c74e3037a8ee482f1115572603" dependencies = [ "lazy_static", "regex", @@ -2584,20 +2651,20 @@ version = "0.1.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c3733bf4cf7ea0880754e19cb5a462007c4a8c1914bff372ccc95b464f1df88" dependencies = [ - "windows-sys 0.48.0", + "windows-sys", ] [[package]] name = "scopeguard" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "security-framework" -version = "2.9.1" +version = "2.9.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fc758eb7bffce5b308734e9b0c1468893cae9ff70ebf13e7090be8dcbcc83a8" +checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de" dependencies = [ "bitflags 1.3.2", "core-foundation", @@ -2608,9 +2675,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.9.0" +version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f51d0c0d83bec45f16480d0ce0058397a69e48fcdc52d1dc8855fb68acbd31a7" +checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a" dependencies = [ "core-foundation-sys", "libc", @@ -2624,35 +2691,44 @@ checksum = "b0293b4b29daaf487284529cc2f5675b8e57c61f70167ba415a463651fd6a918" [[package]] name = "serde" -version = "1.0.171" +version = "1.0.188" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30e27d1e4fd7659406c492fd6cfaf2066ba8773de45ca75e855590f856dc34a9" +checksum = "cf9e0fcba69a370eed61bcf2b728575f726b50b55cba78064753d708ddc7549e" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.171" +version = "1.0.188" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "389894603bd18c46fa56231694f8d827779c0951a667087194cf9de94ed24682" +checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.26", + "syn 2.0.31", ] [[package]] name = "serde_json" -version = "1.0.103" +version = "1.0.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d03b412469450d4404fe8499a268edd7f8b79fecb074b0d812ad64ca21f4031b" +checksum = "693151e1ac27563d6dbcec9dee9fbd5da8539b20fa14ad3752b2e6d363ace360" dependencies = [ "itoa", "ryu", "serde", ] +[[package]] +name = "serde_plain" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce1fc6db65a611022b23a0dec6975d63fb80a302cb3388835ff02c097258d50" +dependencies = [ + "serde", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -2667,9 +2743,9 @@ dependencies = [ [[package]] name = "serde_yaml" -version = "0.9.23" +version = "0.9.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da6075b41c7e3b079e5f246eb6094a44850d3a4c25a67c581c80796c80134012" +checksum = "1a49e178e4452f45cb61d0cd8cebc1b0fafd3e41929e996cef79aa3aca91f574" dependencies = [ "indexmap 2.0.0", "itoa", @@ -2700,7 +2776,7 @@ checksum = "91d129178576168c589c9ec973feedf7d3126c01ac2bf08795109aa35b69fb8f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.26", + "syn 2.0.31", ] [[package]] @@ -2727,9 +2803,15 @@ dependencies = [ [[package]] name = "shlex" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43b2853a4d09f215c24cc5489c992ce46052d359b5109343cbafbf26bc62f8a3" +checksum = "a7cee0529a6d40f580e7a5e6c495c8fbfe21b7b52795ed4bb5e62cdf92bc6380" + +[[package]] +name = "signal-child" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a4eed4c5ae38438470ab8e0108bb751012f786f44ff585cfd837c9a5fe426f" [[package]] name = "signal-hook-registry" @@ -2759,7 +2841,7 @@ dependencies = [ "num-bigint", "num-traits", "thiserror", - "time 0.3.23", + "time", ] [[package]] @@ -2771,14 +2853,14 @@ dependencies = [ "log", "paris", "termcolor", - "time 0.3.23", + "time", ] [[package]] name = "slab" -version = "0.4.8" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" dependencies = [ "autocfg", ] @@ -2799,6 +2881,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "socket2" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2538b18701741680e0322a2302176d3253a35388e2e62f172f64f4f16605f877" +dependencies = [ + "libc", + "windows-sys", +] + [[package]] name = "spin" version = "0.5.2" @@ -2826,9 +2918,9 @@ dependencies = [ [[package]] name = "sqlformat" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c12bc9199d1db8234678b7051747c07f517cdcf019262d1847b94ec8b1aee3e" +checksum = "6b7b278788e7be4d0d29c0f39497a0eef3fba6bbc8e70d8bf7fde46edeaa9e85" dependencies = [ "itertools", "nom", @@ -2854,7 +2946,7 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd4cef4251aabbae751a3710927945901ee1d97ee96d757f6880ebb9a79bfd53" dependencies = [ - "ahash 0.8.3", + "ahash", "atoi", "byteorder", "bytes", @@ -2933,8 +3025,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ca69bf415b93b60b80dc8fda3cb4ef52b2336614d8da2de5456cc942a110482" dependencies = [ "atoi", - "base64 0.21.2", - "bitflags 2.3.3", + "base64 0.21.3", + "bitflags 2.4.0", "byteorder", "bytes", "crc", @@ -2975,8 +3067,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a0db2df1b8731c3651e204629dd55e52adbae0462fa1bdcbed56a2302c18181e" dependencies = [ "atoi", - "base64 0.21.2", - "bitflags 2.3.3", + "base64 0.21.3", + "bitflags 2.4.0", "byteorder", "crc", "dotenvy", @@ -3031,10 +3123,11 @@ dependencies = [ [[package]] name = "stringprep" -version = "0.1.3" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db3737bde7edce97102e0e2b15365bf7a20bfdb5f60f4f9e8d7004258a51a8da" +checksum = "bb41d74e231a107a1b4ee36bd1214b11285b77768d2e3824aedafa988fd36ee6" dependencies = [ + "finl_unicode", "unicode-bidi", "unicode-normalization", ] @@ -3064,9 +3157,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.26" +version = "2.0.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45c3457aacde3c65315de5031ec191ce46604304d2446e803d71ade03308d970" +checksum = "718fa2415bcb8d8bd775917a1bf12a7931b6dfa890753378538118181e0cb398" dependencies = [ "proc-macro2", "quote", @@ -3075,16 +3168,15 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.6.0" +version = "3.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31c0432476357e58790aaa47a8efb0c5138f137343f3b5f23bd36a27e3b0a6d6" +checksum = "cb94d2f3cc536af71caac6b6fcebf65860b347e7ce0cc9ebe8f70d3e521054ef" dependencies = [ - "autocfg", "cfg-if", - "fastrand", - "redox_syscall 0.3.5", - "rustix 0.37.23", - "windows-sys 0.48.0", + "fastrand 2.0.0", + "redox_syscall", + "rustix 0.38.11", + "windows-sys", ] [[package]] @@ -3117,47 +3209,37 @@ dependencies = [ "serial_test", "shlex", "simplelog", - "time 0.3.23", + "time", "walkdir", ] [[package]] name = "thiserror" -version = "1.0.43" +version = "1.0.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a35fc5b8971143ca348fa6df4f024d4d55264f3468c71ad1c2f365b0a4d58c42" +checksum = "9d6d7a740b8a666a7e828dd00da9c0dc290dff53154ea77ac109281de90589b7" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.43" +version = "1.0.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "463fe12d7993d3b327787537ce8dd4dfa058de32fc2b195ef3cde03dc4771e8f" +checksum = "49922ecae66cc8a249b77e68d1d0623c1b2c514f0060c27cdc68bd62a1219d35" dependencies = [ "proc-macro2", "quote", - "syn 2.0.26", + "syn 2.0.31", ] [[package]] name = "time" -version = "0.1.45" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b797afad3f312d1c66a56d11d0316f916356d11bd158fbc6ca6389ff6bf805a" -dependencies = [ - "libc", - "wasi 0.10.0+wasi-snapshot-preview1", - "winapi", -] - -[[package]] -name = "time" -version = "0.3.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59e399c068f43a5d116fedaf73b203fa4f9c519f17e2b34f63221d3792f81446" +checksum = "17f6bb557fd245c28e6411aa56b6403c689ad95061f50e4be16c274e70a17e48" dependencies = [ + "deranged", "itoa", "libc", "num_threads", @@ -3174,9 +3256,9 @@ checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb" [[package]] name = "time-macros" -version = "0.2.10" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96ba15a897f3c86766b757e5ac7221554c6750054d74d5b28844fce5fb36a6c4" +checksum = "1a942f44339478ef67935ab2bbaec2fb0322496cf3cbe84b261e06ac3814c572" dependencies = [ "time-core", ] @@ -3210,11 +3292,10 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.29.1" +version = "1.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "532826ff75199d5833b9d2c5fe410f29235e25704ee5f0ef599fb51c21f4a4da" +checksum = "17ed6077ed6cd6c74735e21f37eb16dc3935f96878b1fe961074089cc80893f9" dependencies = [ - "autocfg", "backtrace", "bytes", "libc", @@ -3223,9 +3304,9 @@ dependencies = [ "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2", + "socket2 0.5.3", "tokio-macros", - "windows-sys 0.48.0", + "windows-sys", ] [[package]] @@ -3236,7 +3317,7 @@ checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.26", + "syn 2.0.31", ] [[package]] @@ -3301,7 +3382,7 @@ checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab" dependencies = [ "proc-macro2", "quote", - "syn 2.0.26", + "syn 2.0.31", ] [[package]] @@ -3327,9 +3408,9 @@ checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" [[package]] name = "unicase" -version = "2.6.0" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" +checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" dependencies = [ "version_check", ] @@ -3381,9 +3462,9 @@ checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" [[package]] name = "url" -version = "2.4.0" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50bff7831e19200a85b17131d085c25d7811bc4e186efdaf54bbd132994a88cb" +checksum = "143b538f18257fac9cad154828a57c6bf5157e1aa604d4816b5995bf6de87ae5" dependencies = [ "form_urlencoded", "idna 0.4.0", @@ -3431,9 +3512,9 @@ checksum = "9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca" [[package]] name = "walkdir" -version = "2.3.3" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36df944cda56c7d8d8b7496af378e6b16de9284591917d307c9b4d313c44e698" +checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee" dependencies = [ "same-file", "winapi-util", @@ -3448,12 +3529,6 @@ dependencies = [ "try-lock", ] -[[package]] -name = "wasi" -version = "0.10.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" - [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -3481,7 +3556,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.26", + "syn 2.0.31", "wasm-bindgen-shared", ] @@ -3515,7 +3590,7 @@ checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.26", + "syn 2.0.31", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -3579,16 +3654,7 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" dependencies = [ - "windows-targets 0.48.1", -] - -[[package]] -name = "windows-sys" -version = "0.45.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" -dependencies = [ - "windows-targets 0.42.2", + "windows-targets", ] [[package]] @@ -3597,130 +3663,74 @@ version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ - "windows-targets 0.48.1", + "windows-targets", ] [[package]] name = "windows-targets" -version = "0.42.2" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" dependencies = [ - "windows_aarch64_gnullvm 0.42.2", - "windows_aarch64_msvc 0.42.2", - "windows_i686_gnu 0.42.2", - "windows_i686_msvc 0.42.2", - "windows_x86_64_gnu 0.42.2", - "windows_x86_64_gnullvm 0.42.2", - "windows_x86_64_msvc 0.42.2", -] - -[[package]] -name = "windows-targets" -version = "0.48.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05d4b17490f70499f20b9e791dcf6a299785ce8af4d709018206dc5b4953e95f" -dependencies = [ - "windows_aarch64_gnullvm 0.48.0", - "windows_aarch64_msvc 0.48.0", - "windows_i686_gnu 0.48.0", - "windows_i686_msvc 0.48.0", - "windows_x86_64_gnu 0.48.0", - "windows_x86_64_gnullvm 0.48.0", - "windows_x86_64_msvc 0.48.0", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", ] [[package]] name = "windows_aarch64_gnullvm" -version = "0.42.2" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_msvc" -version = "0.42.2" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_i686_gnu" -version = "0.42.2" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" - -[[package]] -name = "windows_i686_gnu" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_msvc" -version = "0.42.2" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" - -[[package]] -name = "windows_i686_msvc" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_x86_64_gnu" -version = "0.42.2" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnullvm" -version = "0.42.2" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_msvc" -version = "0.42.2" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "winreg" -version = "0.10.1" +version = "0.50.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" dependencies = [ - "winapi", + "cfg-if", + "windows-sys", ] [[package]] @@ -3756,18 +3766,18 @@ dependencies = [ [[package]] name = "zstd" -version = "0.12.3+zstd.1.5.2" +version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76eea132fb024e0e13fd9c2f5d5d595d8a967aa72382ac2f9d39fcc95afd0806" +checksum = "1a27595e173641171fc74a1232b7b1c7a7cb6e18222c11e9dfb9888fa424c53c" dependencies = [ "zstd-safe", ] [[package]] name = "zstd-safe" -version = "6.0.5+zstd.1.5.4" +version = "6.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d56d9e60b4b1758206c238a10165fbcae3ca37b01744e394c463463f6529d23b" +checksum = "ee98ffd0b48ee95e6c5168188e44a54550b1564d9d530ee21d5f0eaed1069581" dependencies = [ "libc", "zstd-sys", diff --git a/Cargo.toml b/Cargo.toml index 0a44d18e..588555df 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,7 @@ [workspace] members = ["ffplayout-api", "ffplayout-engine", "lib", "tests"] default-members = ["ffplayout-api", "ffplayout-engine", "tests"] +resolver = "2" [workspace.package] version = "0.20.0" diff --git a/README.md b/README.md index 0516687d..7c15a09a 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -### This project is taking a summer break, during this time the Issues and Discussions are closed. In October both will be open again. Until then have a good time! +### This project is taking a summer break, during this time the Issues and Discussions are closed. In October both will be open again. Until then have a good time! **ffplayout** ================ @@ -54,6 +54,7 @@ Check the [releases](https://github.com/ffplayout/ffplayout/releases/latest) for - import playlist from text or m3u file, with CLI or frontend - audio only, for radio mode (experimental *) - [Piggyback Mode](/ffplayout-api/README.md#piggyback-mode), mostly for non Linux systems (experimental *) +- generate playlist based on [template](/docs/playlist_gen.md) (experimental *) For preview stream, read: [/docs/preview_stream.md](/docs/preview_stream.md) @@ -176,14 +177,12 @@ Output from `{"media":"current"}` show: ```JSON { - "jsonrpc": "2.0", - "result": { "current_media": { - "category": "", - "duration": 154.2, - "out": 154.2, - "seek": 0.0, - "source": "/opt/tv-media/clip.mp4" + "category": "", + "duration": 154.2, + "out": 154.2, + "seek": 0.0, + "source": "/opt/tv-media/clip.mp4" }, "index": 39, "play_mode": "playlist", @@ -191,8 +190,6 @@ Output from `{"media":"current"}` show: "remaining_sec": 86.39228000699876, "start_sec": 24713.631999999998, "start_time": "06:51:53.631" - }, - "id": 1 } ``` diff --git a/docs/README.md b/docs/README.md index 70985615..53bf1be1 100644 --- a/docs/README.md +++ b/docs/README.md @@ -21,6 +21,10 @@ Using live ingest to inject a live stream. The different output modes. +### **[Playlist Generation](/docs/playlist_gen.md)** + +Generate playlists based on template. + ### **[Multi Audio Tracks](/docs/multi_audio.md)** Output multiple audio tracks. diff --git a/docs/playlist_gen.md b/docs/playlist_gen.md new file mode 100644 index 00000000..80e7d893 --- /dev/null +++ b/docs/playlist_gen.md @@ -0,0 +1,71 @@ + +## Playlist generation template + +It is possible to generate playlists based on templates. A template could look like: + +```JSON +{ + "sources": [ + { + "start": "00:00:00", + "duration": "02:00:00", + "shuffle": true, + "paths": [ + "/path/to/folder/1" + ] + }, + { + "start": "02:00:00", + "duration": "04:00:00", + "shuffle": false, + "paths": [ + "/path/to/folder/2", + "/path/to/folder/3", + "/path/to/folder/4" + ] + }, + { + "start": "06:00:00", + "duration": "10:00:00", + "shuffle": true, + "paths": [ + "/path/to/folder/5" + ] + }, + { + "start": "16:00:00", + "duration": "06:00:00", + "shuffle": false, + "paths": [ + "/path/to/folder/6", + "/path/to/folder/7" + ] + }, + { + "start": "22:00:00", + "duration": "02:00:00", + "shuffle": true, + "paths": [ + "/path/to/folder/8" + ] + } + ] +} +``` + +This can be used as file and run through CLI: + +```BASH +ffplayout -g 2023-09-04 - 2023-09-10 --template 'path/to/playlist_template.json' +``` + +Or through API: + +```BASH +curl -X POST http://127.0.0.1:8787/api/playlist/1/generate/2023-00-05 + -H 'Content-Type: application/json' -H 'Authorization: Bearer <TOKEN>' + --data '{ "paths": "template": {"sources": [\ + {"start": "00:00:00", "duration": "10:00:00", "shuffle": true, "paths": ["path/1", "path/2"]}, \ + {"start": "10:00:00", "duration": "14:00:00", "shuffle": false, "paths": ["path/3", "path/4"]}]}}' +``` + diff --git a/ffplayout-api/Cargo.toml b/ffplayout-api/Cargo.toml index 937bb770..9c4b3063 100644 --- a/ffplayout-api/Cargo.toml +++ b/ffplayout-api/Cargo.toml @@ -11,11 +11,11 @@ edition.workspace = true [dependencies] ffplayout-lib = { path = "../lib" } actix-files = "0.6" -actix-multipart = "0.5" +actix-multipart = "0.6" actix-web = "4" actix-web-grants = "3" -actix-web-httpauth = "0.6" -argon2 = "0.4" +actix-web-httpauth = "0.8" +argon2 = "0.5" chrono = { version = "0.4", default-features = false, features = ["clock", "std"] } clap = { version = "4.3", features = ["derive"] } derive_more = "0.99" @@ -23,19 +23,19 @@ faccess = "0.2" futures-util = { version = "0.3", default-features = false, features = ["std"] } jsonwebtoken = "8" lexical-sort = "0.3" -once_cell = "1.10" +once_cell = "1.18" rand = "0.8" regex = "1" -relative-path = "1.6" +relative-path = "1.8" reqwest = { version = "0.11", features = ["blocking", "json"] } -rpassword = "6.0" -sanitize-filename = "0.3" +rpassword = "7.2" +sanitize-filename = "0.5" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" serde_yaml = "0.9" -simplelog = { version = "^0.12", features = ["paris"] } +simplelog = { version = "0.12", features = ["paris"] } sqlx = { version = "0.7", features = ["runtime-tokio", "sqlite"] } -tokio = { version = "1.25", features = ["full"] } +tokio = { version = "1.29", features = ["full"] } [[bin]] name = "ffpapi" diff --git a/ffplayout-api/src/api/routes.rs b/ffplayout-api/src/api/routes.rs index 9215c710..45601d18 100644 --- a/ffplayout-api/src/api/routes.rs +++ b/ffplayout-api/src/api/routes.rs @@ -8,7 +8,7 @@ /// /// For all endpoints an (Bearer) authentication is required.\ /// `{id}` represent the channel id, and at default is 1. -use std::{collections::HashMap, env, fs, path::Path}; +use std::{collections::HashMap, env, fs, path::PathBuf}; use actix_multipart::Multipart; use actix_web::{delete, get, http::StatusCode, patch, post, put, web, HttpResponse, Responder}; @@ -46,6 +46,7 @@ use crate::{ use ffplayout_lib::{ utils::{ get_date_range, import::import_file, sec_to_time, time_to_sec, JsonPlaylist, PlayoutConfig, + Template, }, vec_strings, }; @@ -72,19 +73,20 @@ pub struct DateObj { #[derive(Debug, Deserialize, Serialize)] struct FileObj { #[serde(default)] - path: String, + path: PathBuf, } #[derive(Debug, Default, Deserialize, Serialize)] pub struct PathsObj { #[serde(default)] paths: Vec<String>, + template: Option<Template>, } #[derive(Debug, Deserialize, Serialize)] pub struct ImportObj { #[serde(default)] - file: String, + file: PathBuf, #[serde(default)] date: String, } @@ -724,7 +726,7 @@ pub async fn get_playlist( /// ```BASH /// curl -X POST http://127.0.0.1:8787/api/playlist/1/ /// -H 'Content-Type: application/json' -H 'Authorization: Bearer <TOKEN>' -/// -- data "{<JSON playlist data>}" +/// --data "{<JSON playlist data>}" /// ``` #[post("/playlist/{id}/")] #[has_any_role("Role::Admin", "Role::User", type = "Role")] @@ -746,7 +748,7 @@ pub async fn save_playlist( /// ```BASH /// curl -X POST http://127.0.0.1:8787/api/playlist/1/generate/2022-06-20 /// -H 'Content-Type: application/json' -H 'Authorization: Bearer <TOKEN>' -/// /// -- data '{ "paths": [<list of paths>] }' # <- data is optional +/// /// --data '{ "paths": [<list of paths>] }' # <- data is optional /// ``` #[post("/playlist/{id}/generate/{date}")] #[has_any_role("Role::Admin", "Role::User", type = "Role")] @@ -764,10 +766,11 @@ pub async fn gen_playlist( for path in &obj.paths { let (p, _, _) = norm_abs_path(&config.storage.path, path); - path_list.push(p.to_string_lossy().to_string()); + path_list.push(p); } config.storage.paths = path_list; + config.general.template = obj.template.clone(); } match generate_playlist(config, channel.name).await { @@ -921,8 +924,8 @@ async fn import_playlist( payload: Multipart, obj: web::Query<ImportObj>, ) -> Result<HttpResponse, ServiceError> { - let file = Path::new(&obj.file).file_name().unwrap_or_default(); - let path = env::temp_dir().join(file).to_string_lossy().to_string(); + let file = obj.file.file_name().unwrap_or_default(); + let path = env::temp_dir().join(file); let (config, _) = playout_config(&pool.clone().into_inner(), &id).await?; let channel = handles::select_channel(&pool.clone().into_inner(), &id).await?; diff --git a/ffplayout-api/src/db/handles.rs b/ffplayout-api/src/db/handles.rs index b037bc02..9d62eeea 100644 --- a/ffplayout-api/src/db/handles.rs +++ b/ffplayout-api/src/db/handles.rs @@ -82,8 +82,8 @@ async fn create_schema(conn: &Pool<Sqlite>) -> Result<SqliteQueryResult, sqlx::E pub async fn db_init(domain: Option<String>) -> Result<&'static str, Box<dyn std::error::Error>> { let db_path = db_path()?; - if !Sqlite::database_exists(&db_path).await.unwrap_or(false) { - Sqlite::create_database(&db_path).await.unwrap(); + if !Sqlite::database_exists(db_path).await.unwrap_or(false) { + Sqlite::create_database(db_path).await.unwrap(); let pool = db_pool().await?; diff --git a/ffplayout-api/src/db/mod.rs b/ffplayout-api/src/db/mod.rs index bd0c27ff..fdf6adbd 100644 --- a/ffplayout-api/src/db/mod.rs +++ b/ffplayout-api/src/db/mod.rs @@ -7,7 +7,7 @@ use crate::utils::db_path; pub async fn db_pool() -> Result<Pool<Sqlite>, sqlx::Error> { let db_path = db_path().unwrap(); - let conn = SqlitePool::connect(&db_path).await?; + let conn = SqlitePool::connect(db_path).await?; Ok(conn) } diff --git a/ffplayout-api/src/main.rs b/ffplayout-api/src/main.rs index a0dab153..11b6820d 100644 --- a/ffplayout-api/src/main.rs +++ b/ffplayout-api/src/main.rs @@ -3,8 +3,7 @@ use std::{path::Path, process::exit}; use actix_files::Files; 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 actix_web_httpauth::{extractors::bearer::BearerAuth, middleware::HttpAuthentication}; use clap::Parser; use simplelog::*; @@ -29,15 +28,22 @@ use utils::{args_parse::Args, control::ProcessControl, db_path, init_config, run use ffplayout_lib::utils::{init_logging, PlayoutConfig}; -async fn validator(req: ServiceRequest, credentials: BearerAuth) -> Result<ServiceRequest, Error> { +async fn validator( + req: ServiceRequest, + credentials: BearerAuth, +) -> Result<ServiceRequest, (Error, ServiceRequest)> { // We just get permissions from JWT - let claims = auth::decode_jwt(credentials.token()).await?; - req.attach(vec![Role::set_role(&claims.role)]); + match auth::decode_jwt(credentials.token()).await { + Ok(claims) => { + req.attach(vec![Role::set_role(&claims.role)]); - req.extensions_mut() - .insert(LoginUser::new(claims.id, claims.username)); + req.extensions_mut() + .insert(LoginUser::new(claims.id, claims.username)); - Ok(req) + Ok(req) + } + Err(e) => Err((e, req)), + } } fn public_path() -> &'static str { @@ -49,7 +55,7 @@ fn public_path() -> &'static str { return "./public/"; } - "./ffplayout-frontend/dist" + "./ffplayout-frontend/.output/public/" } #[actix_web::main] @@ -77,11 +83,9 @@ async fn main() -> std::io::Result<()> { }; if let Some(conn) = args.listen { - if let Ok(p) = db_path() { - if !Path::new(&p).is_file() { - error!("Database is not initialized! Init DB first and add admin user."); - exit(1); - } + if db_path().is_err() { + error!("Database is not initialized! Init DB first and add admin user."); + exit(1); } init_config(&pool).await; let ip_port = conn.split(':').collect::<Vec<&str>>(); diff --git a/ffplayout-api/src/utils/channels.rs b/ffplayout-api/src/utils/channels.rs index 55a061c6..0437536b 100644 --- a/ffplayout-api/src/utils/channels.rs +++ b/ffplayout-api/src/utils/channels.rs @@ -1,4 +1,4 @@ -use std::{fs, path::Path}; +use std::{fs, path::PathBuf}; use rand::prelude::*; use simplelog::*; @@ -31,22 +31,14 @@ pub async fn create_channel( Err(_) => rand::thread_rng().gen_range(71..99), }; - let mut config = - PlayoutConfig::new(Some("/usr/share/ffplayout/ffplayout.yml.orig".to_string())); + let mut config = PlayoutConfig::new(Some(PathBuf::from( + "/usr/share/ffplayout/ffplayout.yml.orig", + ))); config.general.stat_file = format!(".ffp_{channel_name}",); - - config.logging.path = Path::new(&config.logging.path) - .join(&channel_name) - .to_string_lossy() - .to_string(); - + config.logging.path = config.logging.path.join(&channel_name); config.rpc_server.address = format!("127.0.0.1:70{:7>2}", channel_num); - - config.playlist.path = Path::new(&config.playlist.path) - .join(channel_name) - .to_string_lossy() - .to_string(); + config.playlist.path = config.playlist.path.join(channel_name); config.out.output_param = config .out diff --git a/ffplayout-api/src/utils/files.rs b/ffplayout-api/src/utils/files.rs index 91c87375..11e2fadf 100644 --- a/ffplayout-api/src/utils/files.rs +++ b/ffplayout-api/src/utils/files.rs @@ -1,4 +1,8 @@ -use std::{fs, io::Write, path::PathBuf}; +use std::{ + fs, + io::Write, + path::{Path, PathBuf}, +}; use actix_multipart::Multipart; use actix_web::{web, HttpResponse}; @@ -51,10 +55,9 @@ pub struct VideoFile { /// Normalize absolut path /// /// This function takes care, that it is not possible to break out from root_path. -/// It also gives alway a relative path back. -pub fn norm_abs_path(root_path: &str, input_path: &str) -> (PathBuf, String, String) { - let mut path = PathBuf::from(root_path); - let path_relative = RelativePath::new(root_path) +/// It also gives always a relative path back. +pub fn norm_abs_path(root_path: &Path, input_path: &str) -> (PathBuf, String, String) { + let path_relative = RelativePath::new(&root_path.to_string_lossy()) .normalize() .to_string() .replace("../", ""); @@ -62,13 +65,15 @@ pub fn norm_abs_path(root_path: &str, input_path: &str) -> (PathBuf, String, Str .normalize() .to_string() .replace("../", ""); - let path_suffix = path + let path_suffix = root_path .file_name() .unwrap_or_default() .to_string_lossy() .to_string(); - if input_path.starts_with(root_path) || source_relative.starts_with(&path_relative) { + if input_path.starts_with(&*root_path.to_string_lossy()) + || source_relative.starts_with(&path_relative) + { source_relative = source_relative .strip_prefix(&path_relative) .and_then(|s| s.strip_prefix('/')) @@ -82,9 +87,9 @@ pub fn norm_abs_path(root_path: &str, input_path: &str) -> (PathBuf, String, Str .to_string(); } - path = path.join(&source_relative); + let path = &root_path.join(&source_relative); - (path, path_suffix, source_relative) + (path.to_path_buf(), path_suffix, source_relative) } /// File Browser @@ -300,7 +305,7 @@ pub async fn upload( conn: &Pool<Sqlite>, id: i32, mut payload: Multipart, - path: &str, + path: &Path, abs_path: bool, ) -> Result<HttpResponse, ServiceError> { while let Some(mut field) = payload.try_next().await? { @@ -318,9 +323,9 @@ pub async fn upload( let filepath; if abs_path { - filepath = PathBuf::from(path); + filepath = path.to_path_buf(); } else { - let target_path = valid_path(conn, id, path).await?; + let target_path = valid_path(conn, id, &path.to_string_lossy()).await?; filepath = target_path.join(filename); } diff --git a/ffplayout-api/src/utils/mod.rs b/ffplayout-api/src/utils/mod.rs index f9bd3de8..3c456ee2 100644 --- a/ffplayout-api/src/utils/mod.rs +++ b/ffplayout-api/src/utils/mod.rs @@ -74,21 +74,25 @@ pub async fn init_config(conn: &Pool<Sqlite>) { INSTANCE.set(config).unwrap(); } -pub fn db_path() -> Result<String, Box<dyn std::error::Error>> { +pub fn db_path() -> Result<&'static str, Box<dyn std::error::Error>> { let sys_path = Path::new("/usr/share/ffplayout/db"); - let mut db_path = "./ffplayout.db".to_string(); + let mut db_path = "./ffplayout.db"; if sys_path.is_dir() && !sys_path.writable() { error!("Path {} is not writable!", sys_path.display()); } if sys_path.is_dir() && sys_path.writable() { - db_path = "/usr/share/ffplayout/db/ffplayout.db".to_string(); + db_path = "/usr/share/ffplayout/db/ffplayout.db"; } else if Path::new("./assets").is_dir() { - db_path = "./assets/ffplayout.db".to_string(); + db_path = "./assets/ffplayout.db"; } - Ok(db_path) + if Path::new(db_path).is_file() { + return Ok(db_path); + } + + Err(format!("DB path {db_path} not exists!").into()) } pub async fn run_args(mut args: Args) -> Result<(), i32> { @@ -190,6 +194,7 @@ pub fn read_playout_config(path: &str) -> Result<PlayoutConfig, Box<dyn Error>> let mut config: PlayoutConfig = serde_yaml::from_reader(file)?; config.playlist.start_sec = Some(time_to_sec(&config.playlist.day_start)); + config.playlist.length_sec = Some(time_to_sec(&config.playlist.length)); Ok(config) } diff --git a/ffplayout-api/src/utils/playlist.rs b/ffplayout-api/src/utils/playlist.rs index 65cee698..fcbd8b96 100644 --- a/ffplayout-api/src/utils/playlist.rs +++ b/ffplayout-api/src/utils/playlist.rs @@ -3,7 +3,7 @@ use std::{fs, path::PathBuf}; use simplelog::*; use sqlx::{Pool, Sqlite}; -use crate::utils::{errors::ServiceError, playout_config}; +use crate::utils::{errors::ServiceError, files::norm_abs_path, playout_config}; use ffplayout_lib::utils::{ generate_playlist as playlist_generator, json_reader, json_writer, JsonPlaylist, PlayoutConfig, }; @@ -78,16 +78,32 @@ pub async fn write_playlist( } pub async fn generate_playlist( - config: PlayoutConfig, + mut config: PlayoutConfig, channel: String, ) -> Result<JsonPlaylist, ServiceError> { + if let Some(mut template) = config.general.template.take() { + for source in template.sources.iter_mut() { + let mut paths = vec![]; + + for path in &source.paths { + let (safe_path, _, _) = + norm_abs_path(&config.storage.path, &path.to_string_lossy()); + paths.push(safe_path); + } + + source.paths = paths; + } + + config.general.template = Some(template); + } + match playlist_generator(&config, Some(channel)) { Ok(playlists) => { if !playlists.is_empty() { Ok(playlists[0].clone()) } else { Err(ServiceError::Conflict( - "Playlist could not be written, possible already exists!".into(), + "The playlist could not be written, maybe it already exists!".into(), )) } } diff --git a/ffplayout-engine/Cargo.toml b/ffplayout-engine/Cargo.toml index 381594f1..3b8a2ffe 100644 --- a/ffplayout-engine/Cargo.toml +++ b/ffplayout-engine/Cargo.toml @@ -16,13 +16,15 @@ chrono = { version = "0.4", default-features = false, features = ["clock", "std" clap = { version = "4.3", features = ["derive"] } crossbeam-channel = "0.5" futures = "0.3" +itertools = "0.11" notify = "6.0" notify-debouncer-full = { version = "*", default-features = false } +rand = "0.8" regex = "1" reqwest = { version = "0.11", features = ["blocking", "json"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" -simplelog = { version = "^0.12", features = ["paris"] } +simplelog = { version = "0.12", features = ["paris"] } tiny_http = { version = "0.12", default-features = false } zeromq = { git = "https://github.com/zeromq/zmq.rs.git", default-features = false, features = [ "async-std-runtime", diff --git a/ffplayout-engine/src/input/mod.rs b/ffplayout-engine/src/input/mod.rs index a69f1148..d003eb02 100644 --- a/ffplayout-engine/src/input/mod.rs +++ b/ffplayout-engine/src/input/mod.rs @@ -28,8 +28,8 @@ pub fn source_generator( Folder => { info!("Playout in folder mode"); debug!( - "Monitor folder: <b><magenta>{}</></b>", - &config.storage.path + "Monitor folder: <b><magenta>{:?}</></b>", + config.storage.path ); let config_clone = config.clone(); diff --git a/ffplayout-engine/src/input/playlist.rs b/ffplayout-engine/src/input/playlist.rs index 4ddc3a7b..d58d5131 100644 --- a/ffplayout-engine/src/input/playlist.rs +++ b/ffplayout-engine/src/input/playlist.rs @@ -40,7 +40,7 @@ impl CurrentProgram { is_terminated: Arc<AtomicBool>, player_control: &PlayerControl, ) -> Self { - let json = read_json(config, None, is_terminated.clone(), true, 0.0); + let json = read_json(config, None, is_terminated.clone(), true, false); if let Some(file) = &json.current_file { info!("Read Playlist: <b><magenta>{}</></b>", file); @@ -78,7 +78,7 @@ impl CurrentProgram { fn check_update(&mut self, seek: bool) { if self.json_path.is_none() { // If the playlist was missing, we check here to see if it came back. - let json = read_json(&self.config, None, self.is_terminated.clone(), seek, 0.0); + let json = read_json(&self.config, None, self.is_terminated.clone(), seek, false); if let Some(file) = &json.current_file { info!("Read Playlist: <b><magenta>{file}</></b>"); @@ -105,7 +105,7 @@ impl CurrentProgram { self.json_path.clone(), self.is_terminated.clone(), false, - 0.0, + false, ); self.json_mod = json.modified; @@ -133,17 +133,20 @@ impl CurrentProgram { } // Check if day is past and it is time for a new playlist. - fn check_for_next_playlist(&mut self) { + fn check_for_next_playlist(&mut self) -> bool { let current_time = get_sec(); let start_sec = self.config.playlist.start_sec.unwrap(); let target_length = self.config.playlist.length_sec.unwrap(); let (delta, total_delta) = get_delta(&self.config, ¤t_time); let mut duration = self.current_node.out; + let mut next = false; if self.current_node.duration > self.current_node.out { duration = self.current_node.duration } + trace!("delta: {delta}, total_delta: {total_delta}"); + let mut next_start = self.current_node.begin.unwrap_or_default() - start_sec + duration + delta; @@ -153,21 +156,20 @@ impl CurrentProgram { next_start += self.config.general.stop_threshold; } + trace!("next_start: {next_start}, target_length: {target_length}"); + // Check if we over the target length or we are close to it, if so we load the next playlist. if next_start >= target_length || is_close(total_delta, 0.0, 2.0) || is_close(total_delta, target_length, 2.0) { - let json = read_json( - &self.config, - None, - self.is_terminated.clone(), - false, - next_start, - ); + trace!("get next day"); + next = true; + + let json = read_json(&self.config, None, self.is_terminated.clone(), false, true); if let Some(file) = &json.current_file { - info!("Read Playlist: <b><magenta>{}</></b>", file); + info!("Read next Playlist: <b><magenta>{}</></b>", file); } let data = json!({ @@ -192,8 +194,12 @@ impl CurrentProgram { if json.current_file.is_none() { self.playout_stat.list_init.store(true, Ordering::SeqCst); + } else { + self.playout_stat.list_init.store(false, Ordering::SeqCst); } } + + next } // Check if last and/or next clip is a advertisement. @@ -257,6 +263,7 @@ impl CurrentProgram { // Prepare init clip. fn init_clip(&mut self) { + trace!("init_clip"); self.get_current_clip(); if !self.playout_stat.list_init.load(Ordering::SeqCst) { @@ -307,47 +314,48 @@ impl Iterator for CurrentProgram { let new_length = new_node.begin.unwrap_or_default() + new_node.duration; trace!("Init playlist after playlist end"); - self.check_for_next_playlist(); + let next_playlist = self.check_for_next_playlist(); if new_length >= self.config.playlist.length_sec.unwrap() + self.config.playlist.start_sec.unwrap() { self.init_clip(); + } else if next_playlist + && self.player_control.current_list.lock().unwrap().len() > 1 + { + let index = self + .player_control + .current_index + .fetch_add(1, Ordering::SeqCst); + + self.current_node = gen_source( + &self.config, + self.player_control.current_list.lock().unwrap()[index].clone(), + &self.playout_stat.chain, + &self.player_control, + 0, + ); + + return Some(self.current_node.clone()); } else { // fill missing length from playlist let mut current_time = get_sec(); let (_, total_delta) = get_delta(&self.config, ¤t_time); - let mut out = total_delta.abs(); - let mut duration = out + 0.001; trace!("Total delta on list init: {total_delta}"); - let filler_index = self.player_control.filler_index.load(Ordering::SeqCst); - let mut filler = - self.player_control.filler_list.lock().unwrap()[filler_index].clone(); - - filler.add_probe(); - - // If there is no filler, the duration of the dummy clip should not be too long. - // This would take away the ability to restart the playlist when the playout registers a change. - if filler.duration > 0.0 { - if duration > filler.duration { - out = filler.duration; - } - - duration = filler.duration; - } else if DUMMY_LEN > total_delta { - duration = total_delta; - out = total_delta; + let out = if DUMMY_LEN > total_delta { + total_delta } else { - duration = DUMMY_LEN; - out = DUMMY_LEN; - } + DUMMY_LEN + }; + + let duration = out + 0.001; if self.json_path.is_some() { // When playlist is missing, we always need to init the playlist the next iteration. - self.playout_stat.list_init.store(false, Ordering::SeqCst); + self.playout_stat.list_init.store(true, Ordering::SeqCst); } if self.config.playlist.start_sec.unwrap() > current_time { @@ -429,28 +437,14 @@ impl Iterator for CurrentProgram { let index = self.player_control.current_index.load(Ordering::SeqCst); self.current_node = Media::new(index, "", false); self.current_node.begin = Some(get_sec()); - let mut out = total_delta.abs(); - let mut duration = out + 0.001; - let filler_index = self.player_control.filler_index.load(Ordering::SeqCst); - let mut filler = - self.player_control.filler_list.lock().unwrap()[filler_index].clone(); - - filler.add_probe(); - - if filler.duration > 0.0 { - if duration > filler.duration { - out = filler.duration; - } - - duration = filler.duration; - } else if DUMMY_LEN > total_delta { - duration = total_delta; - out = total_delta; + let out = if DUMMY_LEN > total_delta { + total_delta } else { - duration = DUMMY_LEN; - out = DUMMY_LEN; - } + DUMMY_LEN + }; + + let duration = out + 0.001; self.current_node.duration = duration; self.current_node.out = out; @@ -605,7 +599,7 @@ pub fn gen_source( error!("Source not found: <b><magenta>\"{}\"</></b>", node.source); } - let filler_source = Path::new(&config.storage.filler); + let filler_source = &config.storage.filler; if filler_source.is_dir() && !player_control.filler_list.lock().unwrap().is_empty() { let filler_index = player_control.filler_index.fetch_add(1, Ordering::SeqCst); @@ -629,17 +623,19 @@ pub fn gen_source( node.cmd = Some(loop_filler(&node)); node.probe = filler_media.probe; } else if filler_source.is_file() { - let probe = MediaProbe::new(&config.storage.filler); + let probe = MediaProbe::new(&config.storage.filler.to_string_lossy()); if config .storage .filler + .to_string_lossy() + .to_string() .rsplit_once('.') .map(|(_, e)| e.to_lowercase()) .filter(|c| IMAGE_FORMAT.contains(&c.as_str())) .is_some() { - node.source = config.storage.filler.clone(); + node.source = config.storage.filler.clone().to_string_lossy().to_string(); node.cmd = Some(loop_image(&node)); node.probe = Some(probe); } else if let Some(filler_duration) = probe @@ -649,7 +645,7 @@ pub fn gen_source( .and_then(|d| d.parse::<f64>().ok()) { // Create placeholder from config filler. - node.source = config.storage.filler.clone(); + node.source = config.storage.filler.clone().to_string_lossy().to_string(); node.out = if node.duration > duration && filler_duration > duration { duration diff --git a/ffplayout-engine/src/main.rs b/ffplayout-engine/src/main.rs index 624066f8..b0265d79 100644 --- a/ffplayout-engine/src/main.rs +++ b/ffplayout-engine/src/main.rs @@ -1,6 +1,6 @@ use std::{ fs::{self, File}, - path::{Path, PathBuf}, + path::PathBuf, process::exit, sync::{atomic::AtomicBool, Arc, Mutex}, thread, @@ -107,7 +107,7 @@ fn main() { let messages = Arc::new(Mutex::new(Vec::new())); // try to create logging folder, if not exist - if config.logging.log_to_file && !Path::new(&config.logging.path).is_dir() { + if config.logging.log_to_file && config.logging.path.is_dir() { if let Err(e) = fs::create_dir_all(&config.logging.path) { println!("Logging path not exists! {e}"); @@ -163,11 +163,11 @@ fn main() { } if args.validate { - let mut playlist_path = Path::new(&config.playlist.path).to_owned(); + let mut playlist_path = config.playlist.path.clone(); let start_sec = config.playlist.start_sec.unwrap(); - let date = get_date(false, start_sec, 0.0); + let date = get_date(false, start_sec, false); - if playlist_path.is_dir() || is_remote(&config.playlist.path) { + if playlist_path.is_dir() || is_remote(&playlist_path.to_string_lossy()) { let d: Vec<&str> = date.split('-').collect(); playlist_path = playlist_path .join(d[0]) @@ -213,7 +213,9 @@ fn main() { ); // Fill filler list, can also be a single file. - thread::spawn(move || fill_filler_list(config_clone2, play_ctl2)); + thread::spawn(move || { + fill_filler_list(&config_clone2, Some(play_ctl2)); + }); match config.out.mode { // write files/playlist to HLS m3u8 playlist diff --git a/ffplayout-engine/src/output/mod.rs b/ffplayout-engine/src/output/mod.rs index 1b17a247..11fcc74f 100644 --- a/ffplayout-engine/src/output/mod.rs +++ b/ffplayout-engine/src/output/mod.rs @@ -1,6 +1,5 @@ use std::{ io::{prelude::*, BufReader, BufWriter, Read}, - path::Path, process::{Command, Stdio}, sync::atomic::Ordering, thread::{self, sleep}, @@ -101,11 +100,11 @@ pub fn player( let task_node = node.clone(); let server_running = proc_control.server_is_running.load(Ordering::SeqCst); - if Path::new(&config.task.path).is_file() { + if config.task.path.is_file() { thread::spawn(move || task_runner::run(task_config, task_node, server_running)); } else { error!( - "<bright-blue>{}</> executable not exists!", + "<bright-blue>{:?}</> executable not exists!", config.task.path ); } @@ -159,15 +158,11 @@ pub fn player( thread::spawn(move || stderr_reader(dec_err, Decoder, dec_p_ctl)); loop { - // when server is running, read from channel + // when server is running, read from it if proc_control.server_is_running.load(Ordering::SeqCst) { if !live_on { info!("Switch from {} to live ingest", config.processing.mode); - if let Err(e) = enc_writer.flush() { - error!("Encoder error: {e}") - } - if let Err(e) = proc_control.stop(Decoder) { error!("{e}") } @@ -188,11 +183,8 @@ pub fn player( if live_on { info!("Switch from live ingest to {}", config.processing.mode); - if let Err(e) = enc_writer.flush() { - error!("Encoder error: {e}") - } - live_on = false; + break; } let dec_bytes_len = match dec_reader.read(&mut buffer[..]) { diff --git a/ffplayout-engine/src/utils/arg_parse.rs b/ffplayout-engine/src/utils/arg_parse.rs index 9d4f7f22..21ff05b8 100644 --- a/ffplayout-engine/src/utils/arg_parse.rs +++ b/ffplayout-engine/src/utils/arg_parse.rs @@ -1,3 +1,5 @@ +use std::path::PathBuf; + use clap::Parser; use ffplayout_lib::utils::{OutputMode, ProcessMode}; @@ -13,10 +15,24 @@ pub struct Args { pub channel: Option<String>, #[clap(short, long, help = "File path to ffplayout.yml")] - pub config: Option<String>, + pub config: Option<PathBuf>, #[clap(short, long, help = "File path for logging")] - pub log: Option<String>, + pub log: Option<PathBuf>, + + #[clap( + short, + long, + help = "Target date (YYYY-MM-DD) for text/m3u to playlist import" + )] + pub date: Option<String>, + + #[cfg(debug_assertions)] + #[clap(long, help = "fake date time, for debugging")] + pub fake_time: Option<String>, + + #[clap(short, long, help = "Play folder content")] + pub folder: Option<PathBuf>, #[clap( short, @@ -27,37 +43,14 @@ pub struct Args { )] pub generate: Option<Vec<String>>, - #[clap(long, help = "Optional path list for playlist generations", num_args = 1..)] - pub paths: Option<Vec<String>>, - - #[clap(short = 'm', long, help = "Playing mode: folder, playlist")] - pub play_mode: Option<ProcessMode>, - - #[clap(short, long, help = "Play folder content")] - pub folder: Option<String>, - - #[clap( - short, - long, - help = "Target date (YYYY-MM-DD) for text/m3u to playlist import" - )] - pub date: Option<String>, - #[clap( long, help = "Import a given text/m3u file and create a playlist from it" )] - pub import: Option<String>, + pub import: Option<PathBuf>, - #[clap(short, long, help = "Path to playlist, or playlist root folder.")] - pub playlist: Option<String>, - - #[clap( - short, - long, - help = "Start time in 'hh:mm:ss', 'now' for start with first" - )] - pub start: Option<String>, + #[clap(short, long, help = "Loop playlist infinitely")] + pub infinit: bool, #[clap( short = 't', @@ -69,8 +62,24 @@ pub struct Args { #[clap(long, help = "Override logging level")] pub level: Option<String>, - #[clap(short, long, help = "Loop playlist infinitely")] - pub infinit: bool, + #[clap(long, help = "Optional path list for playlist generations", num_args = 1..)] + pub paths: Option<Vec<PathBuf>>, + + #[clap(short = 'm', long, help = "Playing mode: folder, playlist")] + pub play_mode: Option<ProcessMode>, + + #[clap(short, long, help = "Path to playlist, or playlist root folder.")] + pub playlist: Option<PathBuf>, + + #[clap( + short, + long, + help = "Start time in 'hh:mm:ss', 'now' for start with first" + )] + pub start: Option<String>, + + #[clap(short = 'T', long, help = "JSON Template file for generating playlist")] + pub template: Option<PathBuf>, #[clap(short, long, help = "Set output mode: desktop, hls, null, stream")] pub output: Option<OutputMode>, @@ -78,10 +87,6 @@ pub struct Args { #[clap(short, long, help = "Set audio volume")] pub volume: Option<f64>, - #[cfg(debug_assertions)] - #[clap(long, help = "fake date time, for debugging")] - pub fake_time: Option<String>, - #[clap(long, help = "validate given playlist")] pub validate: bool, } diff --git a/ffplayout-engine/src/utils/mod.rs b/ffplayout-engine/src/utils/mod.rs index f5ddcc4c..97a2ceda 100644 --- a/ffplayout-engine/src/utils/mod.rs +++ b/ffplayout-engine/src/utils/mod.rs @@ -1,7 +1,4 @@ -use std::{ - path::{Path, PathBuf}, - process::exit, -}; +use std::{fs::File, path::PathBuf, process::exit}; use regex::Regex; use serde_json::{json, Map, Value}; @@ -14,8 +11,8 @@ pub use arg_parse::Args; use ffplayout_lib::{ filter::Filters, utils::{ - get_sec, parse_log_level_filter, sec_to_time, time_to_sec, Media, OutputMode::*, - PlayoutConfig, ProcessMode::*, + config::Template, get_sec, parse_log_level_filter, sec_to_time, time_to_sec, Media, + OutputMode::*, PlayoutConfig, ProcessMode::*, }, vec_strings, }; @@ -33,7 +30,7 @@ pub fn get_config(args: Args) -> PlayoutConfig { exit(1) } - Some(path.display().to_string()) + Some(path) } None => args.config, }; @@ -48,12 +45,33 @@ pub fn get_config(args: Args) -> PlayoutConfig { config.general.validate = true; } + if let Some(template_file) = args.template { + let f = File::options() + .read(true) + .write(false) + .open(template_file) + .expect("JSON template file"); + + let mut template: Template = match serde_json::from_reader(f) { + Ok(p) => p, + Err(e) => { + error!("Template file not readable! {e}"); + + exit(1) + } + }; + + template.sources.sort_by(|d1, d2| d1.start.cmp(&d2.start)); + + config.general.template = Some(template); + } + if let Some(paths) = args.paths { config.storage.paths = paths; } if let Some(log_path) = args.log { - if Path::new(&log_path).is_dir() { + if log_path.is_dir() { config.logging.log_to_file = true; } config.logging.path = log_path; diff --git a/ffplayout-frontend b/ffplayout-frontend index 2f323422..2ca3aa73 160000 --- a/ffplayout-frontend +++ b/ffplayout-frontend @@ -1 +1 @@ -Subproject commit 2f3234221a0aef8e70d9e2b5e9bbfb1fe51921fc +Subproject commit 2ca3aa73b2992f6997276f5c8fb906f9ee703bf2 diff --git a/lib/Cargo.toml b/lib/Cargo.toml index 607b7114..72f85125 100644 --- a/lib/Cargo.toml +++ b/lib/Cargo.toml @@ -9,11 +9,12 @@ repository.workspace = true edition.workspace = true [dependencies] -chrono = { version = "0.4", default-features = false, features = ["clock", "std"] } +chrono = { version = "0.4", default-features = false, features = ["clock", "serde", "std"] } crossbeam-channel = "0.5" ffprobe = "0.3" -file-rotate = "0.7.0" +file-rotate = "0.7" lettre = "0.10" +lexical-sort = "0.3" log = "0.4" rand = "0.8" regex = "1" @@ -22,7 +23,7 @@ serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" serde_yaml = "0.9" shlex = "1.1" -simplelog = { version = "^0.12", features = ["paris"] } +simplelog = { version = "0.12", features = ["paris"] } time = { version = "0.3", features = ["formatting", "macros"] } walkdir = "2" @@ -32,3 +33,6 @@ features = ["shlobj", "std", "winerror"] [target.x86_64-unknown-linux-musl.dependencies] openssl = { version = "0.10", features = ["vendored"] } + +[target.'cfg(not(target_arch = "windows"))'.dependencies] +signal-child = "1" diff --git a/lib/src/utils/config.rs b/lib/src/utils/config.rs index c8172432..0b2bbbc5 100644 --- a/lib/src/utils/config.rs +++ b/lib/src/utils/config.rs @@ -6,6 +6,7 @@ use std::{ str::FromStr, }; +use chrono::NaiveTime; use log::LevelFilter; use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; use shlex::split; @@ -124,6 +125,19 @@ where } } +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct Template { + pub sources: Vec<Source>, +} + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct Source { + pub start: NaiveTime, + pub duration: NaiveTime, + pub shuffle: bool, + pub paths: Vec<PathBuf>, +} + /// Global Config /// /// This we init ones, when ffplayout is starting and use them globally in the hole program. @@ -163,6 +177,9 @@ pub struct General { #[serde(skip_serializing, skip_deserializing)] pub ffmpeg_libs: Vec<String>, + #[serde(skip_serializing, skip_deserializing)] + pub template: Option<Template>, + #[serde(default, skip_serializing, skip_deserializing)] pub validate: bool, } @@ -196,7 +213,7 @@ pub struct Logging { pub local_time: bool, pub timestamp: bool, #[serde(alias = "log_path")] - pub path: String, + pub path: PathBuf, #[serde( alias = "log_level", serialize_with = "log_level_to_string", @@ -255,7 +272,7 @@ pub struct Ingest { #[derive(Debug, Serialize, Deserialize, Clone)] pub struct Playlist { pub help_text: String, - pub path: String, + pub path: PathBuf, pub day_start: String, #[serde(skip_serializing, skip_deserializing)] @@ -272,11 +289,11 @@ pub struct Playlist { #[derive(Debug, Serialize, Deserialize, Clone)] pub struct Storage { pub help_text: String, - pub path: String, + pub path: PathBuf, #[serde(skip_serializing, skip_deserializing)] - pub paths: Vec<String>, + pub paths: Vec<PathBuf>, #[serde(alias = "filler_clip")] - pub filler: String, + pub filler: PathBuf, pub extensions: Vec<String>, pub shuffle: bool, } @@ -304,7 +321,7 @@ pub struct Text { #[derive(Debug, Default, Serialize, Deserialize, Clone)] pub struct Task { pub enable: bool, - pub path: String, + pub path: PathBuf, } #[derive(Debug, Serialize, Deserialize, Clone)] @@ -335,11 +352,11 @@ fn default_channels() -> u8 { impl PlayoutConfig { /// Read config from YAML file, and set some extra config values. - pub fn new(cfg_path: Option<String>) -> Self { + pub fn new(cfg_path: Option<PathBuf>) -> Self { let mut config_path = PathBuf::from("/etc/ffplayout/ffplayout.yml"); if let Some(cfg) = cfg_path { - config_path = PathBuf::from(cfg); + config_path = cfg; } if !config_path.is_file() { diff --git a/lib/src/utils/controller.rs b/lib/src/utils/controller.rs index 6c06ce33..f7623759 100644 --- a/lib/src/utils/controller.rs +++ b/lib/src/utils/controller.rs @@ -7,6 +7,9 @@ use std::{ }, }; +#[cfg(not(windows))] +use signal_child::Signalable; + use serde::{Deserialize, Serialize}; use simplelog::*; @@ -71,9 +74,15 @@ impl ProcessControl { match unit { Decoder => { if let Some(proc) = self.decoder_term.lock().unwrap().as_mut() { + #[cfg(not(windows))] + if let Err(e) = proc.term() { + return Err(format!("Decoder {e:?}")); + } + + #[cfg(windows)] if let Err(e) = proc.kill() { return Err(format!("Decoder {e:?}")); - }; + } } } Encoder => { @@ -177,7 +186,7 @@ impl PlayerControl { Self { current_media: Arc::new(Mutex::new(None)), current_list: Arc::new(Mutex::new(vec![Media::new(0, "", false)])), - filler_list: Arc::new(Mutex::new(vec![Media::new(0, "", false)])), + filler_list: Arc::new(Mutex::new(vec![])), current_index: Arc::new(AtomicUsize::new(0)), filler_index: Arc::new(AtomicUsize::new(0)), } diff --git a/lib/src/utils/folder.rs b/lib/src/utils/folder.rs index 74a459b7..0489a617 100644 --- a/lib/src/utils/folder.rs +++ b/lib/src/utils/folder.rs @@ -1,9 +1,6 @@ -use std::{ - path::Path, - sync::{ - atomic::Ordering, - {Arc, Mutex}, - }, +use std::sync::{ + atomic::Ordering, + {Arc, Mutex}, }; use rand::{seq::SliceRandom, thread_rng}; @@ -37,18 +34,18 @@ impl FolderSource { if config.general.generate.is_some() && !config.storage.paths.is_empty() { for path in &config.storage.paths { - path_list.push(path.clone()) + path_list.push(path) } } else { - path_list.push(config.storage.path.clone()) + path_list.push(&config.storage.path) } for path in &path_list { - if !Path::new(path).is_dir() { - error!("Path not exists: <b><magenta>{path}</></b>"); + if !path.is_dir() { + error!("Path not exists: <b><magenta>{path:?}</></b>"); } - for entry in WalkDir::new(path.clone()) + for entry in WalkDir::new(path) .into_iter() .flat_map(|e| e.ok()) .filter(|f| f.path().is_file()) @@ -90,6 +87,22 @@ impl FolderSource { } } + pub fn from_list( + config: &PlayoutConfig, + filter_chain: Option<Arc<Mutex<Vec<String>>>>, + player_control: &PlayerControl, + list: Vec<Media>, + ) -> Self { + *player_control.current_list.lock().unwrap() = list; + + Self { + config: config.clone(), + filter_chain, + player_control: player_control.clone(), + current_node: Media::new(0, "", false), + } + } + fn shuffle(&mut self) { let mut rng = thread_rng(); let mut nodes = self.player_control.current_list.lock().unwrap(); @@ -160,23 +173,30 @@ impl Iterator for FolderSource { } } -pub fn fill_filler_list(config: PlayoutConfig, player_control: PlayerControl) { +pub fn fill_filler_list( + config: &PlayoutConfig, + player_control: Option<PlayerControl>, +) -> Vec<Media> { let mut filler_list = vec![]; + let filler_path = &config.storage.filler; - if Path::new(&config.storage.filler).is_dir() { - debug!( - "Fill filler list from: <b><magenta>{}</></b>", - config.storage.filler - ); - + if filler_path.is_dir() { for (index, entry) in WalkDir::new(&config.storage.filler) .into_iter() .flat_map(|e| e.ok()) .filter(|f| f.path().is_file()) - .filter(|f| include_file_extension(&config, f.path())) + .filter(|f| include_file_extension(config, f.path())) .enumerate() { - filler_list.push(Media::new(index, &entry.path().to_string_lossy(), false)); + let mut media = Media::new(index, &entry.path().to_string_lossy(), false); + + if let Some(control) = player_control.as_ref() { + control.filler_list.lock().unwrap().push(media); + } else { + media.add_probe(); + + filler_list.push(media); + } } if config.storage.shuffle { @@ -190,9 +210,17 @@ pub fn fill_filler_list(config: PlayoutConfig, player_control: PlayerControl) { for (index, item) in filler_list.iter_mut().enumerate() { item.index = Some(index); } - } else { - filler_list.push(Media::new(0, &config.storage.filler, false)); + } else if filler_path.is_file() { + let mut media = Media::new(0, &config.storage.filler.to_string_lossy(), false); + + if let Some(control) = player_control.as_ref() { + control.filler_list.lock().unwrap().push(media); + } else { + media.add_probe(); + + filler_list.push(media); + } } - *player_control.filler_list.lock().unwrap() = filler_list; + filler_list } diff --git a/lib/src/utils/generator.rs b/lib/src/utils/generator.rs index 19bc6492..a0b8449e 100644 --- a/lib/src/utils/generator.rs +++ b/lib/src/utils/generator.rs @@ -4,22 +4,192 @@ /// /// The generator takes the files from storage, which are set in config. /// It also respect the shuffle/sort mode. -/// -/// Beside that it is really very basic, without any logic. use std::{ fs::{create_dir_all, write}, io::Error, - path::Path, process::exit, }; +use chrono::Timelike; +use lexical_sort::{natural_lexical_cmp, StringSort}; +use rand::{seq::SliceRandom, thread_rng, Rng}; use simplelog::*; +use walkdir::WalkDir; use super::{folder::FolderSource, PlayerControl}; use crate::utils::{ - get_date_range, json_serializer::JsonPlaylist, time_to_sec, Media, PlayoutConfig, + folder::fill_filler_list, gen_dummy, get_date_range, include_file_extension, + json_serializer::JsonPlaylist, sum_durations, time_to_sec, Media, PlayoutConfig, Template, }; +pub fn random_list(clip_list: Vec<Media>, total_length: f64) -> Vec<Media> { + let mut max_attempts = 10000; + let mut randomized_clip_list: Vec<Media> = vec![]; + let mut target_duration = 0.0; + let clip_list_length = clip_list.len(); + let usage_limit = (total_length / sum_durations(&clip_list)).floor() + 1.0; + let mut last_clip = Media::new(0, "", false); + + while target_duration < total_length && max_attempts > 0 { + let index = rand::thread_rng().gen_range(0..clip_list_length); + let selected_clip = clip_list[index].clone(); + let selected_clip_count = randomized_clip_list + .iter() + .filter(|&n| *n == selected_clip) + .count() as f64; + + if selected_clip_count == usage_limit + || last_clip == selected_clip + || target_duration + selected_clip.duration > total_length + { + max_attempts -= 1; + continue; + } + + target_duration += selected_clip.duration; + randomized_clip_list.push(selected_clip.clone()); + max_attempts -= 1; + last_clip = selected_clip; + } + + randomized_clip_list +} + +pub fn ordered_list(clip_list: Vec<Media>, total_length: f64) -> Vec<Media> { + let mut index = 0; + let mut skip_count = 0; + let mut ordered_clip_list: Vec<Media> = vec![]; + let mut target_duration = 0.0; + let clip_list_length = clip_list.len(); + + while target_duration < total_length && skip_count < clip_list_length { + if index == clip_list_length { + index = 0; + } + + let selected_clip = clip_list[index].clone(); + + if sum_durations(&ordered_clip_list) + selected_clip.duration > total_length + || (!ordered_clip_list.is_empty() + && selected_clip == ordered_clip_list[ordered_clip_list.len() - 1]) + { + skip_count += 1; + index += 1; + continue; + } + + target_duration += selected_clip.duration; + ordered_clip_list.push(selected_clip); + index += 1; + } + + ordered_clip_list +} + +pub fn filler_list(config: &PlayoutConfig, total_length: f64) -> Vec<Media> { + let filler_list = fill_filler_list(config, None); + let mut index = 0; + let mut filler_clip_list: Vec<Media> = vec![]; + let mut target_duration = 0.0; + let clip_list_length = filler_list.len(); + + if clip_list_length > 0 { + while target_duration < total_length { + if index == clip_list_length { + index = 0; + } + + let selected_clip = filler_list[index].clone(); + + target_duration += selected_clip.duration; + filler_clip_list.push(selected_clip); + index += 1; + } + + let over_length = target_duration - total_length; + let last_index = filler_clip_list.len() - 1; + + filler_clip_list[last_index].out = filler_clip_list[last_index].duration - over_length; + } else { + let mut dummy = Media::new(0, "", false); + let (source, cmd) = gen_dummy(config, total_length); + dummy.source = source; + dummy.cmd = Some(cmd); + dummy.duration = total_length; + dummy.out = total_length; + + filler_clip_list.push(dummy); + } + + filler_clip_list +} + +pub fn generate_from_template( + config: &PlayoutConfig, + player_control: &PlayerControl, + template: Template, +) -> FolderSource { + let mut media_list = vec![]; + let mut rng = thread_rng(); + let mut index: usize = 0; + + for source in template.sources { + let mut source_list = vec![]; + let duration = (source.duration.hour() as f64 * 3600.0) + + (source.duration.minute() as f64 * 60.0) + + source.duration.second() as f64; + + debug!("Generating playlist block with <yellow>{duration:.2}</> seconds length"); + + for path in source.paths { + debug!("Search files in <b><magenta>{path:?}</></b>"); + + let mut file_list = WalkDir::new(path.clone()) + .into_iter() + .flat_map(|e| e.ok()) + .filter(|f| f.path().is_file()) + .filter(|f| include_file_extension(config, f.path())) + .map(|p| p.path().to_string_lossy().to_string()) + .collect::<Vec<String>>(); + + if !source.shuffle { + file_list.string_sort_unstable(natural_lexical_cmp); + } + + for entry in file_list { + let media = Media::new(0, &entry, true); + source_list.push(media); + } + } + + let mut timed_list = if source.shuffle { + source_list.shuffle(&mut rng); + + random_list(source_list, duration) + } else { + ordered_list(source_list, duration) + }; + + let total_length = sum_durations(&timed_list); + + if duration > total_length { + let mut filler = filler_list(config, duration - total_length); + + timed_list.append(&mut filler); + } + + media_list.append(&mut timed_list); + } + + for item in media_list.iter_mut() { + item.index = Some(index); + + index += 1; + } + + FolderSource::from_list(config, None, player_control, media_list) +} + /// Generate playlists pub fn generate_playlist( config: &PlayoutConfig, @@ -36,9 +206,10 @@ pub fn generate_playlist( } }; let player_control = PlayerControl::new(); - let playlist_root = Path::new(&config.playlist.path); + let playlist_root = &config.playlist.path; let mut playlists = vec![]; let mut date_range = vec![]; + let mut from_template = false; let channel = match channel_name { Some(name) => name, @@ -47,8 +218,8 @@ pub fn generate_playlist( if !playlist_root.is_dir() { error!( - "Playlist folder <b><magenta>{}</></b> not exists!", - &config.playlist.path + "Playlist folder <b><magenta>{:?}</></b> not exists!", + config.playlist.path ); exit(1); @@ -62,8 +233,16 @@ pub fn generate_playlist( date_range = get_date_range(&date_range) } - let media_list = FolderSource::new(config, None, &player_control); - let list_length = media_list.player_control.current_list.lock().unwrap().len(); + // gives an iterator with infinit length + let folder_iter = if let Some(template) = &config.general.template { + from_template = true; + + generate_from_template(config, &player_control, template.clone()) + } else { + FolderSource::new(config, None, &player_control) + }; + + let list_length = player_control.current_list.lock().unwrap().len(); for date in date_range { let d: Vec<&str> = date.split('-').collect(); @@ -71,6 +250,8 @@ pub fn generate_playlist( let month = d[1]; let playlist_path = playlist_root.join(year).join(month); let playlist_file = &playlist_path.join(format!("{date}.json")); + let mut length = 0.0; + let mut round = 0; create_dir_all(playlist_path)?; @@ -88,12 +269,6 @@ pub fn generate_playlist( playlist_file.display() ); - // TODO: handle filler folder - let mut filler = Media::new(0, &config.storage.filler, true); - let filler_length = filler.duration; - let mut length = 0.0; - let mut round = 0; - let mut playlist = JsonPlaylist { channel: channel.clone(), date, @@ -103,30 +278,38 @@ pub fn generate_playlist( program: vec![], }; - for item in media_list.clone() { - let duration = item.duration; + if from_template { + let media_list = player_control.current_list.lock().unwrap(); + playlist.program = media_list.to_vec(); + } else { + for item in folder_iter.clone() { + let duration = item.duration; - if total_length > length + duration { - playlist.program.push(item); + if total_length >= length + duration { + playlist.program.push(item); - length += duration; - } else if filler_length > 0.0 && filler_length > total_length - length { - filler.out = total_length - length; - playlist.program.push(filler); + length += duration; + } else if round == list_length - 1 { + break; + } else { + round += 1; + } + } - break; - } else if round == list_length - 1 { - break; - } else { - round += 1; + let list_duration = sum_durations(&playlist.program); + + if config.playlist.length_sec.unwrap() > list_duration { + let time_left = config.playlist.length_sec.unwrap() - list_duration; + let mut fillers = filler_list(config, time_left); + + playlist.program.append(&mut fillers); } } - playlists.push(playlist.clone()); - let json: String = serde_json::to_string_pretty(&playlist)?; - write(playlist_file, json)?; + + playlists.push(playlist); } Ok(playlists) diff --git a/lib/src/utils/import.rs b/lib/src/utils/import.rs index 7367473a..ce2c6ce5 100644 --- a/lib/src/utils/import.rs +++ b/lib/src/utils/import.rs @@ -12,7 +12,7 @@ pub fn import_file( config: &PlayoutConfig, date: &str, channel_name: Option<String>, - path: &str, + path: &Path, ) -> Result<String, Error> { let file = File::open(path)?; let reader = BufReader::new(file); @@ -25,13 +25,13 @@ pub fn import_file( program: vec![], }; - let playlist_root = Path::new(&config.playlist.path); + let playlist_root = &config.playlist.path; if !playlist_root.is_dir() { return Err(Error::new( ErrorKind::Other, format!( - "Playlist folder <b><magenta>{}</></b> not exists!", - &config.playlist.path, + "Playlist folder <b><magenta>{:?}</></b> not exists!", + config.playlist.path, ), )); } diff --git a/lib/src/utils/json_serializer.rs b/lib/src/utils/json_serializer.rs index bd0d7295..9aac1425 100644 --- a/lib/src/utils/json_serializer.rs +++ b/lib/src/utils/json_serializer.rs @@ -142,14 +142,14 @@ pub fn read_json( path: Option<String>, is_terminated: Arc<AtomicBool>, seek: bool, - next_start: f64, + get_next: bool, ) -> JsonPlaylist { let config_clone = config.clone(); - let mut playlist_path = Path::new(&config.playlist.path).to_owned(); + let mut playlist_path = config.playlist.path.clone(); let start_sec = config.playlist.start_sec.unwrap(); - let date = get_date(seek, start_sec, next_start); + let date = get_date(seek, start_sec, get_next); - if playlist_path.is_dir() || is_remote(&config.playlist.path) { + if playlist_path.is_dir() || is_remote(&config.playlist.path.to_string_lossy()) { let d: Vec<&str> = date.split('-').collect(); playlist_path = playlist_path .join(d[0]) diff --git a/lib/src/utils/json_validate.rs b/lib/src/utils/json_validate.rs index 3ccab158..803a24bd 100644 --- a/lib/src/utils/json_validate.rs +++ b/lib/src/utils/json_validate.rs @@ -165,7 +165,7 @@ pub fn validate_playlist( begin += item.out - item.seek; } - if !config.playlist.infinit && length > begin + 1.0 { + if !config.playlist.infinit && length > begin + 1.2 { error!( "Playlist from <yellow>{date}</> not long enough, <yellow>{}</> needed!", sec_to_time(length - begin), diff --git a/lib/src/utils/logging.rs b/lib/src/utils/logging.rs index f5114679..3614e865 100644 --- a/lib/src/utils/logging.rs +++ b/lib/src/utils/logging.rs @@ -2,7 +2,7 @@ extern crate log; extern crate simplelog; use std::{ - path::Path, + path::PathBuf, sync::{atomic::Ordering, Arc, Mutex}, thread::{self, sleep}, time::Duration, @@ -199,21 +199,18 @@ pub fn init_logging( }; }; - if app_config.log_to_file && &app_config.path != "none" { + if app_config.log_to_file && app_config.path.exists() { let file_config = log_config .clone() .set_time_format_custom(format_description!( "[[[year]-[month]-[day] [hour]:[minute]:[second].[subsecond digits:5]]" )) .build(); - let mut log_path = "logs/ffplayout.log".to_string(); + let mut log_path = PathBuf::from("logs/ffplayout.log"); - if Path::new(&app_config.path).is_dir() { - log_path = Path::new(&app_config.path) - .join("ffplayout.log") - .display() - .to_string(); - } else if Path::new(&app_config.path).is_file() { + if app_config.path.is_dir() { + log_path = app_config.path.join("ffplayout.log"); + } else if app_config.path.is_file() { log_path = app_config.path } else { println!("Logging path not exists!") diff --git a/lib/src/utils/mod.rs b/lib/src/utils/mod.rs index c96913bb..8a185b5e 100644 --- a/lib/src/utils/mod.rs +++ b/lib/src/utils/mod.rs @@ -13,7 +13,7 @@ use std::{ use std::env; use chrono::{prelude::*, Duration}; -use ffprobe::{ffprobe, Format, Stream}; +use ffprobe::{ffprobe, Format, Stream as FFStream}; use rand::prelude::*; use regex::Regex; use reqwest::header; @@ -24,7 +24,7 @@ use simplelog::*; pub mod config; pub mod controller; pub mod folder; -mod generator; +pub mod generator; pub mod import; pub mod json_serializer; mod json_validate; @@ -38,7 +38,7 @@ pub use config::{ OutputMode::{self, *}, PlayoutConfig, ProcessMode::{self, *}, - DUMMY_LEN, FFMPEG_IGNORE_ERRORS, FFMPEG_UNRECOVERABLE_ERRORS, IMAGE_FORMAT, + Template, DUMMY_LEN, FFMPEG_IGNORE_ERRORS, FFMPEG_UNRECOVERABLE_ERRORS, IMAGE_FORMAT, }; pub use controller::{ PlayerControl, PlayoutStatus, ProcessControl, @@ -148,14 +148,15 @@ impl Media { pub fn add_probe(&mut self) { if self.probe.is_none() { let probe = MediaProbe::new(&self.source); - self.probe = Some(probe.clone()); if let Some(dur) = probe .format + .clone() .and_then(|f| f.duration) .map(|d| d.parse().unwrap()) .filter(|d| !is_close(*d, self.duration, 0.5)) { + self.probe = Some(probe); self.duration = dur; if self.out == 0.0 { @@ -205,8 +206,8 @@ fn is_empty_string(st: &String) -> bool { #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] pub struct MediaProbe { pub format: Option<Format>, - pub audio_streams: Vec<Stream>, - pub video_streams: Vec<Stream>, + pub audio_streams: Vec<FFStream>, + pub video_streams: Vec<FFStream>, } impl MediaProbe { @@ -321,14 +322,14 @@ pub fn get_sec() -> f64 { /// /// - When time is before playlist start, get date from yesterday. /// - When given next_start is over target length (normally a full day), get date from tomorrow. -pub fn get_date(seek: bool, start: f64, next_start: f64) -> String { +pub fn get_date(seek: bool, start: f64, get_next: bool) -> String { let local: DateTime<Local> = time_now(); if seek && start > get_sec() { return (local - Duration::days(1)).format("%Y-%m-%d").to_string(); } - if start == 0.0 && next_start >= 86400.0 { + if start == 0.0 && get_next && get_sec() > 86397.9 { return (local + Duration::days(1)).format("%Y-%m-%d").to_string(); } @@ -409,6 +410,17 @@ pub fn is_close(a: f64, b: f64, to: f64) -> bool { false } +/// add duration from all media clips +pub fn sum_durations(clip_list: &Vec<Media>) -> f64 { + let mut list_duration = 0.0; + + for item in clip_list { + list_duration += item.out + } + + list_duration +} + /// Get delta between clip start and current time. This value we need to check, /// if we still in sync. /// diff --git a/tests/Cargo.toml b/tests/Cargo.toml index 402caeca..7b6a2355 100644 --- a/tests/Cargo.toml +++ b/tests/Cargo.toml @@ -41,3 +41,7 @@ path = "src/engine_playlist.rs" [[test]] name = "engine_cmd" path = "src/engine_cmd.rs" + +[[test]] +name = "engine_generator" +path = "src/engine_generator.rs" diff --git a/tests/src/engine_cmd.rs b/tests/src/engine_cmd.rs index 098bff08..0c0f33d7 100644 --- a/tests/src/engine_cmd.rs +++ b/tests/src/engine_cmd.rs @@ -1,4 +1,4 @@ -use std::fs; +use std::{fs, path::PathBuf}; use ffplayout::{input::playlist::gen_source, utils::prepare_output_cmd}; use ffplayout_lib::{ @@ -8,7 +8,7 @@ use ffplayout_lib::{ #[test] fn video_audio_input() { - let mut config = PlayoutConfig::new(Some("../assets/ffplayout.yml".to_string())); + let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml"))); let player_control = PlayerControl::new(); config.out.mode = Stream; config.processing.add_logo = true; @@ -36,7 +36,7 @@ fn video_audio_input() { #[test] fn video_audio_custom_filter1_input() { - let mut config = PlayoutConfig::new(Some("../assets/ffplayout.yml".to_string())); + let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml"))); let player_control = PlayerControl::new(); config.out.mode = Stream; config.processing.add_logo = false; @@ -62,7 +62,7 @@ fn video_audio_custom_filter1_input() { #[test] fn video_audio_custom_filter2_input() { - let mut config = PlayoutConfig::new(Some("../assets/ffplayout.yml".to_string())); + let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml"))); let player_control = PlayerControl::new(); config.out.mode = Stream; config.processing.add_logo = false; @@ -90,7 +90,7 @@ fn video_audio_custom_filter2_input() { #[test] fn video_audio_custom_filter3_input() { - let mut config = PlayoutConfig::new(Some("../assets/ffplayout.yml".to_string())); + let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml"))); let player_control = PlayerControl::new(); config.out.mode = Stream; config.processing.add_logo = false; @@ -117,7 +117,7 @@ fn video_audio_custom_filter3_input() { #[test] fn dual_audio_aevalsrc_input() { - let mut config = PlayoutConfig::new(Some("../assets/ffplayout.yml".to_string())); + let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml"))); let player_control = PlayerControl::new(); config.out.mode = Stream; config.processing.audio_tracks = 2; @@ -144,7 +144,7 @@ fn dual_audio_aevalsrc_input() { #[test] fn dual_audio_input() { - let mut config = PlayoutConfig::new(Some("../assets/ffplayout.yml".to_string())); + let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml"))); let player_control = PlayerControl::new(); config.out.mode = Stream; config.processing.audio_tracks = 2; @@ -170,7 +170,7 @@ fn dual_audio_input() { #[test] fn video_separate_audio_input() { - let mut config = PlayoutConfig::new(Some("../assets/ffplayout.yml".to_string())); + let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml"))); let player_control = PlayerControl::new(); config.out.mode = Stream; config.processing.audio_tracks = 1; @@ -204,7 +204,7 @@ fn video_separate_audio_input() { #[test] fn video_audio_stream() { - let mut config = PlayoutConfig::new(Some("../assets/ffplayout.yml".to_string())); + let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml"))); config.out.mode = Stream; config.processing.add_logo = false; config.out.output_cmd = Some(vec_strings![ @@ -263,7 +263,7 @@ fn video_audio_stream() { #[test] fn video_audio_filter1_stream() { - let mut config = PlayoutConfig::new(Some("../assets/ffplayout.yml".to_string())); + let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml"))); config.out.mode = Stream; config.processing.add_logo = false; config.text.add_text = false; @@ -338,7 +338,7 @@ fn video_audio_filter1_stream() { #[test] fn video_audio_filter2_stream() { - let mut config = PlayoutConfig::new(Some("../assets/ffplayout.yml".to_string())); + let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml"))); config.out.mode = Stream; config.processing.add_logo = false; config.text.add_text = true; @@ -421,7 +421,7 @@ fn video_audio_filter2_stream() { #[test] fn video_audio_filter3_stream() { - let mut config = PlayoutConfig::new(Some("../assets/ffplayout.yml".to_string())); + let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml"))); config.out.mode = Stream; config.processing.add_logo = false; config.text.add_text = true; @@ -507,7 +507,7 @@ fn video_audio_filter3_stream() { #[test] fn video_audio_filter4_stream() { - let mut config = PlayoutConfig::new(Some("../assets/ffplayout.yml".to_string())); + let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml"))); config.out.mode = Stream; config.processing.add_logo = false; config.text.add_text = true; @@ -593,7 +593,7 @@ fn video_audio_filter4_stream() { #[test] fn video_dual_audio_stream() { - let mut config = PlayoutConfig::new(Some("../assets/ffplayout.yml".to_string())); + let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml"))); config.out.mode = Stream; config.processing.add_logo = false; config.processing.audio_tracks = 2; @@ -664,7 +664,7 @@ fn video_dual_audio_stream() { #[test] fn video_dual_audio_filter_stream() { - let mut config = PlayoutConfig::new(Some("../assets/ffplayout.yml".to_string())); + let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml"))); config.out.mode = Stream; config.processing.add_logo = false; config.processing.audio_tracks = 2; @@ -744,7 +744,7 @@ fn video_dual_audio_filter_stream() { #[test] fn video_audio_multi_stream() { - let mut config = PlayoutConfig::new(Some("../assets/ffplayout.yml".to_string())); + let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml"))); config.out.mode = Stream; config.processing.add_logo = false; config.out.output_cmd = Some(vec_strings![ @@ -833,7 +833,7 @@ fn video_audio_multi_stream() { #[test] fn video_dual_audio_multi_stream() { - let mut config = PlayoutConfig::new(Some("../assets/ffplayout.yml".to_string())); + let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml"))); config.out.mode = Stream; config.processing.add_logo = false; config.processing.audio_tracks = 2; @@ -947,7 +947,7 @@ fn video_dual_audio_multi_stream() { #[test] fn video_audio_text_multi_stream() { - let mut config = PlayoutConfig::new(Some("../assets/ffplayout.yml".to_string())); + let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml"))); config.out.mode = Stream; config.processing.add_logo = false; config.text.add_text = true; @@ -1060,7 +1060,7 @@ fn video_audio_text_multi_stream() { #[test] fn video_dual_audio_multi_filter_stream() { - let mut config = PlayoutConfig::new(Some("../assets/ffplayout.yml".to_string())); + let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml"))); config.out.mode = Stream; config.processing.add_logo = false; config.processing.audio_tracks = 2; @@ -1189,7 +1189,7 @@ fn video_dual_audio_multi_filter_stream() { #[test] fn video_audio_text_filter_stream() { - let mut config = PlayoutConfig::new(Some("../assets/ffplayout.yml".to_string())); + let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml"))); config.out.mode = Stream; config.processing.add_logo = false; config.processing.audio_tracks = 1; @@ -1311,7 +1311,7 @@ fn video_audio_text_filter_stream() { #[test] fn video_audio_hls() { - let mut config = PlayoutConfig::new(Some("../assets/ffplayout.yml".to_string())); + let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml"))); let player_control = PlayerControl::new(); config.out.mode = HLS; config.processing.add_logo = false; @@ -1397,7 +1397,7 @@ fn video_audio_hls() { #[test] fn video_audio_sub_meta_hls() { - let mut config = PlayoutConfig::new(Some("../assets/ffplayout.yml".to_string())); + let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml"))); let player_control = PlayerControl::new(); config.out.mode = HLS; config.processing.add_logo = false; @@ -1491,7 +1491,7 @@ fn video_audio_sub_meta_hls() { #[test] fn video_multi_audio_hls() { - let mut config = PlayoutConfig::new(Some("../assets/ffplayout.yml".to_string())); + let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml"))); let player_control = PlayerControl::new(); config.out.mode = HLS; config.processing.add_logo = false; @@ -1580,7 +1580,7 @@ fn video_multi_audio_hls() { #[test] fn multi_video_audio_hls() { - let mut config = PlayoutConfig::new(Some("../assets/ffplayout.yml".to_string())); + let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml"))); let player_control = PlayerControl::new(); config.out.mode = HLS; config.processing.add_logo = false; @@ -1694,7 +1694,7 @@ fn multi_video_audio_hls() { #[test] fn multi_video_multi_audio_hls() { - let mut config = PlayoutConfig::new(Some("../assets/ffplayout.yml".to_string())); + let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml"))); let player_control = PlayerControl::new(); config.out.mode = HLS; config.processing.add_logo = false; diff --git a/tests/src/engine_generator.rs b/tests/src/engine_generator.rs new file mode 100644 index 00000000..71e4de55 --- /dev/null +++ b/tests/src/engine_generator.rs @@ -0,0 +1,148 @@ +use std::{ + fs, + path::{Path, PathBuf}, +}; + +use chrono::NaiveTime; +use simplelog::*; + +use ffplayout_lib::utils::{ + config::{Source, Template}, + generator::*, + *, +}; + +#[test] +#[ignore] +fn test_random_list() { + let clip_list = vec![ + Media::new(0, "./assets/with_audio.mp4", true), // 30 seconds + Media::new(0, "./assets/dual_audio.mp4", true), // 30 seconds + Media::new(0, "./assets/av_sync.mp4", true), // 30 seconds + Media::new(0, "./assets/ad.mp4", true), // 25 seconds + ]; + + let r_list = random_list(clip_list.clone(), 200.0); + let r_duration = sum_durations(&r_list); + + assert!(200.0 >= r_duration, "duration is {r_duration}"); + assert!(r_duration >= 170.0); +} + +#[test] +#[ignore] +fn test_ordered_list() { + let clip_list = vec![ + Media::new(0, "./assets/with_audio.mp4", true), // 30 seconds + Media::new(0, "./assets/dual_audio.mp4", true), // 30 seconds + Media::new(0, "./assets/av_sync.mp4", true), // 30 seconds + Media::new(0, "./assets/ad.mp4", true), // 25 seconds + ]; + + let o_list = ordered_list(clip_list.clone(), 85.0); + + assert_eq!(o_list.len(), 3); + assert_eq!(o_list[2].duration, 25.0); + assert_eq!(sum_durations(&o_list), 85.0); + + let o_list = ordered_list(clip_list, 120.0); + + assert_eq!(o_list.len(), 4); + assert_eq!(o_list[2].duration, 30.0); + assert_eq!(sum_durations(&o_list), 115.0); +} + +#[test] +#[ignore] +fn test_filler_list() { + let mut config = PlayoutConfig::new(None); + config.storage.filler = "assets/".into(); + + let f_list = filler_list(&config, 2440.0); + + assert_eq!(sum_durations(&f_list), 2440.0); +} + +#[test] +#[ignore] +fn test_generate_playlist_from_folder() { + let mut config = PlayoutConfig::new(None); + config.general.generate = Some(vec!["2023-09-11".to_string()]); + config.processing.mode = Playlist; + config.logging.log_to_file = false; + config.logging.timestamp = false; + config.logging.level = LevelFilter::Error; + config.storage.filler = "assets/".into(); + config.playlist.length_sec = Some(86400.0); + config.playlist.path = "assets/playlists".into(); + + let logging = init_logging(&config, None, None); + CombinedLogger::init(logging).unwrap_or_default(); + + let playlist = generate_playlist(&config, Some("Channel 1".to_string())); + + assert!(playlist.is_ok()); + + let playlist_file = Path::new("assets/playlists/2023/09/2023-09-11.json"); + + assert!(playlist_file.is_file()); + + fs::remove_file(playlist_file).unwrap(); + + let total_duration = sum_durations(&playlist.unwrap()[0].program); + + assert!( + total_duration > 86399.0 && total_duration < 86401.0, + "total_duration is {total_duration}" + ); +} + +#[test] +#[ignore] +fn test_generate_playlist_from_template() { + let mut config = PlayoutConfig::new(None); + config.general.generate = Some(vec!["2023-09-12".to_string()]); + config.general.template = Some(Template { + sources: vec![ + Source { + start: NaiveTime::from_hms_opt(0, 0, 0).unwrap(), + duration: NaiveTime::from_hms_opt(12, 0, 0).unwrap(), + shuffle: false, + paths: vec![PathBuf::from("assets/")], + }, + Source { + start: NaiveTime::from_hms_opt(12, 0, 0).unwrap(), + duration: NaiveTime::from_hms_opt(12, 0, 0).unwrap(), + shuffle: true, + paths: vec![PathBuf::from("assets/")], + }, + ], + }); + config.processing.mode = Playlist; + config.logging.log_to_file = false; + config.logging.timestamp = false; + config.logging.level = LevelFilter::Error; + config.storage.filler = "assets/".into(); + config.playlist.length_sec = Some(86400.0); + config.playlist.path = "assets/playlists".into(); + + let logging = init_logging(&config, None, None); + CombinedLogger::init(logging).unwrap_or_default(); + + let playlist = generate_playlist(&config, Some("Channel 1".to_string())); + + assert!(playlist.is_ok()); + + let playlist_file = Path::new("assets/playlists/2023/09/2023-09-12.json"); + + assert!(playlist_file.is_file()); + + fs::remove_file(playlist_file).unwrap(); + + let total_duration = sum_durations(&playlist.unwrap()[0].program); + + assert!( + total_duration > 86399.0 && total_duration < 86401.0, + "total_duration is {total_duration}" + ); +} diff --git a/tests/src/engine_playlist.rs b/tests/src/engine_playlist.rs index c54ae0c0..18afc3f3 100644 --- a/tests/src/engine_playlist.rs +++ b/tests/src/engine_playlist.rs @@ -17,6 +17,48 @@ fn timed_stop(sec: u64, proc_ctl: ProcessControl) { proc_ctl.stop_all(); } +#[test] +#[serial] +#[ignore] +fn playlist_missing() { + let mut config = PlayoutConfig::new(None); + config.mail.recipient = "".into(); + config.processing.mode = Playlist; + config.ingest.enable = false; + config.text.add_text = false; + config.playlist.day_start = "00:00:00".into(); + config.playlist.start_sec = Some(0.0); + config.playlist.length = "24:00:00".into(); + config.playlist.length_sec = Some(86400.0); + config.playlist.path = "assets/playlists".into(); + config.storage.filler = "assets/with_audio.mp4".into(); + config.logging.log_to_file = false; + config.logging.timestamp = false; + config.logging.level = LevelFilter::Trace; + config.out.mode = Null; + config.out.output_count = 1; + config.out.output_filter = None; + config.out.output_cmd = Some(vec_strings!["-f", "null", "-"]); + + let play_control = PlayerControl::new(); + let playout_stat = PlayoutStatus::new(); + let proc_control = ProcessControl::new(); + let proc_ctl = proc_control.clone(); + + let logging = init_logging(&config, None, None); + CombinedLogger::init(logging).unwrap_or_default(); + + mock_time::set_mock_time("2023-02-07T23:59:45"); + + thread::spawn(move || timed_stop(28, proc_ctl)); + + player(&config, &play_control, playout_stat.clone(), proc_control); + + let playlist_date = &*playout_stat.current_date.lock().unwrap(); + + assert_eq!(playlist_date, "2023-02-08"); +} + #[test] #[serial] #[ignore] @@ -34,6 +76,7 @@ fn playlist_change_at_midnight() { config.storage.filler = "assets/with_audio.mp4".into(); config.logging.log_to_file = false; config.logging.timestamp = false; + config.logging.level = LevelFilter::Trace; config.out.mode = Null; config.out.output_count = 1; config.out.output_filter = None; @@ -58,6 +101,48 @@ fn playlist_change_at_midnight() { assert_eq!(playlist_date, "2023-02-09"); } +#[test] +#[serial] +#[ignore] +fn playlist_change_before_midnight() { + let mut config = PlayoutConfig::new(None); + config.mail.recipient = "".into(); + config.processing.mode = Playlist; + config.ingest.enable = false; + config.text.add_text = false; + config.playlist.day_start = "23:59:45".into(); + config.playlist.start_sec = Some(0.0); + config.playlist.length = "24:00:00".into(); + config.playlist.length_sec = Some(86400.0); + config.playlist.path = "assets/playlists".into(); + config.storage.filler = "assets/with_audio.mp4".into(); + config.logging.log_to_file = false; + config.logging.timestamp = false; + config.logging.level = LevelFilter::Trace; + config.out.mode = Null; + config.out.output_count = 1; + config.out.output_filter = None; + config.out.output_cmd = Some(vec_strings!["-f", "null", "-"]); + + let play_control = PlayerControl::new(); + let playout_stat = PlayoutStatus::new(); + let proc_control = ProcessControl::new(); + let proc_ctl = proc_control.clone(); + + let logging = init_logging(&config, None, None); + CombinedLogger::init(logging).unwrap_or_default(); + + mock_time::set_mock_time("2023-02-08T23:59:30"); + + thread::spawn(move || timed_stop(35, proc_ctl)); + + player(&config, &play_control, playout_stat.clone(), proc_control); + + let playlist_date = &*playout_stat.current_date.lock().unwrap(); + + assert_eq!(playlist_date, "2023-02-09"); +} + #[test] #[serial] #[ignore] diff --git a/tests/src/lib_utils.rs b/tests/src/lib_utils.rs index 360e6540..2e97442e 100644 --- a/tests/src/lib_utils.rs +++ b/tests/src/lib_utils.rs @@ -1,3 +1,5 @@ +use std::path::PathBuf; + #[cfg(test)] use chrono::prelude::*; @@ -22,23 +24,23 @@ fn mock_date_time() { fn get_date_yesterday() { mock_time::set_mock_time("2022-05-20T05:59:24"); - let date = get_date(true, 21600.0, 86400.0); + let date = get_date(true, 21600.0, false); assert_eq!("2022-05-19".to_string(), date); } #[test] fn get_date_tomorrow() { - mock_time::set_mock_time("2022-05-20T23:59:30"); + mock_time::set_mock_time("2022-05-20T23:59:58"); - let date = get_date(false, 0.0, 86400.01); + let date = get_date(false, 0.0, true); assert_eq!("2022-05-21".to_string(), date); } #[test] fn test_delta() { - let mut config = PlayoutConfig::new(Some("../assets/ffplayout.yml".to_string())); + let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml"))); config.mail.recipient = "".into(); config.processing.mode = Playlist; config.playlist.day_start = "00:00:00".into();