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, &current_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, &current_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();