Merge pull request #722 from jb-alvarado/master

move storage paths to channel
This commit is contained in:
jb-alvarado 2024-08-22 12:11:47 +02:00 committed by GitHub
commit 1e8adcc777
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
28 changed files with 446 additions and 188 deletions

158
Cargo.lock generated
View File

@ -348,6 +348,12 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
[[package]]
name = "adler2"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
[[package]] [[package]]
name = "ahash" name = "ahash"
version = "0.8.11" version = "0.8.11"
@ -528,7 +534,7 @@ dependencies = [
"cc", "cc",
"cfg-if", "cfg-if",
"libc", "libc",
"miniz_oxide", "miniz_oxide 0.7.4",
"object", "object",
"rustc-demangle", "rustc-demangle",
] ]
@ -649,6 +655,12 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "cfg_aliases"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
[[package]] [[package]]
name = "change-detection" name = "change-detection"
version = "1.2.0" version = "1.2.0"
@ -1097,7 +1109,7 @@ checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a"
[[package]] [[package]]
name = "ffplayout" name = "ffplayout"
version = "0.24.0-beta1" version = "0.24.0-beta2"
dependencies = [ dependencies = [
"actix-files", "actix-files",
"actix-multipart", "actix-multipart",
@ -1122,6 +1134,7 @@ dependencies = [
"lexical-sort", "lexical-sort",
"local-ip-address", "local-ip-address",
"log", "log",
"nix",
"notify", "notify",
"notify-debouncer-full", "notify-debouncer-full",
"num-traits", "num-traits",
@ -1195,12 +1208,12 @@ dependencies = [
[[package]] [[package]]
name = "flate2" name = "flate2"
version = "1.0.31" version = "1.0.32"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f211bbe8e69bbd0cfdea405084f128ae8b4aaa6b0b522fc8f2b009084797920" checksum = "9c0596c1eac1f9e04ed902702e9878208b336edc9d6fddc8a48387349bab3666"
dependencies = [ dependencies = [
"crc32fast", "crc32fast",
"miniz_oxide", "miniz_oxide 0.8.0",
] ]
[[package]] [[package]]
@ -1818,9 +1831,9 @@ dependencies = [
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.157" version = "0.2.158"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "374af5f94e54fa97cf75e945cce8a6b201e88a1a07e688b47dfd2a59c66dbd86" checksum = "d8adc4bb1803a324070e64a98ae98f38934d91957a99cfb3a43dcbc01bc56439"
[[package]] [[package]]
name = "libm" name = "libm"
@ -1960,6 +1973,15 @@ dependencies = [
"adler", "adler",
] ]
[[package]]
name = "miniz_oxide"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2d80299ef12ff69b16a84bb182e3b9df68b5a91574d3d4fa6e41b65deec4df1"
dependencies = [
"adler2",
]
[[package]] [[package]]
name = "mio" name = "mio"
version = "0.8.11" version = "0.8.11"
@ -2010,6 +2032,18 @@ dependencies = [
"syn 1.0.109", "syn 1.0.109",
] ]
[[package]]
name = "nix"
version = "0.29.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46"
dependencies = [
"bitflags 2.6.0",
"cfg-if",
"cfg_aliases",
"libc",
]
[[package]] [[package]]
name = "nom" name = "nom"
version = "7.1.3" version = "7.1.3"
@ -2530,9 +2564,9 @@ checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2"
[[package]] [[package]]
name = "reqwest" name = "reqwest"
version = "0.12.5" version = "0.12.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7d6d2a27d57148378eb5e111173f4276ad26340ecc5c49a4a2152167a2d6a37" checksum = "f8f4955649ef5c38cc7f9e8aa41761d48fb9677197daea9984dc54f56aad5e63"
dependencies = [ dependencies = [
"base64 0.22.1", "base64 0.22.1",
"bytes", "bytes",
@ -2568,7 +2602,7 @@ dependencies = [
"wasm-bindgen-futures", "wasm-bindgen-futures",
"web-sys", "web-sys",
"webpki-roots", "webpki-roots",
"winreg", "windows-registry",
] ]
[[package]] [[package]]
@ -3222,15 +3256,15 @@ dependencies = [
[[package]] [[package]]
name = "stacker" name = "stacker"
version = "0.1.15" version = "0.1.16"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c886bd4480155fd3ef527d45e9ac8dd7118a898a46530b7b94c3e21866259fce" checksum = "95a5daa25ea337c85ed954c0496e3bdd2c7308cc3b24cf7b50d04876654c579f"
dependencies = [ dependencies = [
"cc", "cc",
"cfg-if", "cfg-if",
"libc", "libc",
"psm", "psm",
"winapi", "windows-sys 0.36.1",
] ]
[[package]] [[package]]
@ -3372,6 +3406,9 @@ name = "sync_wrapper"
version = "1.0.1" version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394"
dependencies = [
"futures-core",
]
[[package]] [[package]]
name = "sysinfo" name = "sysinfo"
@ -3402,7 +3439,7 @@ dependencies = [
[[package]] [[package]]
name = "tests" name = "tests"
version = "0.24.0-beta1" version = "0.24.0-beta2"
dependencies = [ dependencies = [
"chrono", "chrono",
"crossbeam-channel", "crossbeam-channel",
@ -3687,15 +3724,15 @@ dependencies = [
[[package]] [[package]]
name = "unicode-properties" name = "unicode-properties"
version = "0.1.1" version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e4259d9d4425d9f0661581b804cb85fe66a4c631cadd8f490d1c13a35d5d9291" checksum = "52ea75f83c0137a9b98608359a5f1af8144876eb67bcb1ce837368e906a9f524"
[[package]] [[package]]
name = "unicode-xid" name = "unicode-xid"
version = "0.2.4" version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" checksum = "229730647fbc343e3a80e463c1db7f78f3855d3f3739bee0dda773c9a037c90a"
[[package]] [[package]]
name = "unicode_categories" name = "unicode_categories"
@ -3974,7 +4011,7 @@ checksum = "d2ed2439a290666cd67ecce2b0ffaad89c2a56b976b736e6ece670297897832d"
dependencies = [ dependencies = [
"windows-implement", "windows-implement",
"windows-interface", "windows-interface",
"windows-result", "windows-result 0.1.2",
"windows-targets 0.52.6", "windows-targets 0.52.6",
] ]
@ -4000,6 +4037,17 @@ dependencies = [
"syn 2.0.75", "syn 2.0.75",
] ]
[[package]]
name = "windows-registry"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0"
dependencies = [
"windows-result 0.2.0",
"windows-strings",
"windows-targets 0.52.6",
]
[[package]] [[package]]
name = "windows-result" name = "windows-result"
version = "0.1.2" version = "0.1.2"
@ -4009,6 +4057,38 @@ dependencies = [
"windows-targets 0.52.6", "windows-targets 0.52.6",
] ]
[[package]]
name = "windows-result"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e"
dependencies = [
"windows-targets 0.52.6",
]
[[package]]
name = "windows-strings"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10"
dependencies = [
"windows-result 0.2.0",
"windows-targets 0.52.6",
]
[[package]]
name = "windows-sys"
version = "0.36.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2"
dependencies = [
"windows_aarch64_msvc 0.36.1",
"windows_i686_gnu 0.36.1",
"windows_i686_msvc 0.36.1",
"windows_x86_64_gnu 0.36.1",
"windows_x86_64_msvc 0.36.1",
]
[[package]] [[package]]
name = "windows-sys" name = "windows-sys"
version = "0.48.0" version = "0.48.0"
@ -4079,6 +4159,12 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_msvc"
version = "0.36.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47"
[[package]] [[package]]
name = "windows_aarch64_msvc" name = "windows_aarch64_msvc"
version = "0.48.5" version = "0.48.5"
@ -4091,6 +4177,12 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_i686_gnu"
version = "0.36.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6"
[[package]] [[package]]
name = "windows_i686_gnu" name = "windows_i686_gnu"
version = "0.48.5" version = "0.48.5"
@ -4109,6 +4201,12 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_msvc"
version = "0.36.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024"
[[package]] [[package]]
name = "windows_i686_msvc" name = "windows_i686_msvc"
version = "0.48.5" version = "0.48.5"
@ -4121,6 +4219,12 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_x86_64_gnu"
version = "0.36.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1"
[[package]] [[package]]
name = "windows_x86_64_gnu" name = "windows_x86_64_gnu"
version = "0.48.5" version = "0.48.5"
@ -4145,6 +4249,12 @@ version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_msvc"
version = "0.36.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680"
[[package]] [[package]]
name = "windows_x86_64_msvc" name = "windows_x86_64_msvc"
version = "0.48.5" version = "0.48.5"
@ -4166,16 +4276,6 @@ dependencies = [
"memchr", "memchr",
] ]
[[package]]
name = "winreg"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a277a57398d4bfa075df44f501a17cfdf8542d224f0d36095a2adc7aee4ef0a5"
dependencies = [
"cfg-if",
"windows-sys 0.48.0",
]
[[package]] [[package]]
name = "zerocopy" name = "zerocopy"
version = "0.7.35" version = "0.7.35"

View File

@ -4,7 +4,7 @@ default-members = ["ffplayout", "tests"]
resolver = "2" resolver = "2"
[workspace.package] [workspace.package]
version = "0.24.0-beta1" version = "0.24.0-beta2"
license = "GPL-3.0" license = "GPL-3.0"
repository = "https://github.com/ffplayout/ffplayout" repository = "https://github.com/ffplayout/ffplayout"
authors = ["Jonathan Baecker <jonbae77@gmail.com>"] authors = ["Jonathan Baecker <jonbae77@gmail.com>"]

View File

@ -9,13 +9,13 @@ ffplayout provides ***.deb** and ***.rpm** packages, which makes it more easy to
- `systemctl enable ffplayout` - `systemctl enable ffplayout`
5. initial defaults and add global admin user: 5. initial defaults and add global admin user:
- `sudo -u ffpu ffplayout -i` - `sudo -u ffpu ffplayout -i`
6. use a revers proxy for SSL, Port is **8787**. 6. start ffplayout:
7. login with your browser, address without proxy would be: **http://[IP ADDRESS]:8787** - `systemctl start ffplayout`
7. use a revers proxy for SSL, Port is **8787**.
8. login with your browser, address without proxy would be: **http://[IP ADDRESS]:8787**
Default location for playlists and media files are: **/var/lib/ffplayout/**. Default location for playlists and media files are: **/var/lib/ffplayout/**.
When playlists are created and the ffplayout output is configured, you can start the process: `systemctl start ffplayout`, or click start in frontend.
### Manual Install ### Manual Install
----- -----

View File

@ -24,7 +24,7 @@ argon2 = "0.5"
chrono = { version = "0.4", default-features = false, features = ["clock", "std", "serde"] } chrono = { version = "0.4", default-features = false, features = ["clock", "std", "serde"] }
clap = { version = "4.3", features = ["derive", "env"] } clap = { version = "4.3", features = ["derive", "env"] }
crossbeam-channel = "0.5" crossbeam-channel = "0.5"
derive_more = "1" derive_more = { version = "1", features = ["display"] }
faccess = "0.2" faccess = "0.2"
ffprobe = "0.4" ffprobe = "0.4"
flexi_logger = { version = "0.28", features = ["kv", "colors"] } flexi_logger = { version = "0.28", features = ["kv", "colors"] }
@ -36,6 +36,7 @@ lettre = { version = "0.11", features = ["builder", "rustls-tls", "smtp-transpor
lexical-sort = "0.3" lexical-sort = "0.3"
local-ip-address = "0.6" local-ip-address = "0.6"
log = { version = "0.4", features = ["std", "serde", "kv", "kv_std", "kv_sval", "kv_serde"] } log = { version = "0.4", features = ["std", "serde", "kv", "kv_std", "kv_sval", "kv_serde"] }
nix = { version = "0.29", features = ["user", "fs"] }
notify = "6.0" notify = "6.0"
notify-debouncer-full = { version = "*", default-features = false } notify-debouncer-full = { version = "*", default-features = false }
num-traits = "0.2" num-traits = "0.2"

View File

@ -309,6 +309,7 @@ async fn update_user(
role: AuthDetails<Role>, role: AuthDetails<Role>,
user: web::ReqData<UserMeta>, user: web::ReqData<UserMeta>,
) -> Result<impl Responder, ServiceError> { ) -> Result<impl Responder, ServiceError> {
let channel_ids = data.channel_ids.clone().unwrap_or_default();
let mut fields = String::new(); let mut fields = String::new();
if let Some(mail) = data.mail.clone() { if let Some(mail) = data.mail.clone() {
@ -319,21 +320,6 @@ async fn update_user(
fields.push_str(&format!("mail = '{mail}'")); fields.push_str(&format!("mail = '{mail}'"));
} }
if let Some(channel_ids) = &data.channel_ids {
if !fields.is_empty() {
fields.push_str(", ");
}
fields.push_str(&format!(
"channel_ids = '{}'",
channel_ids
.iter()
.map(|i| i.to_string())
.collect::<Vec<String>>()
.join(",")
));
}
if !data.password.is_empty() { if !data.password.is_empty() {
if !fields.is_empty() { if !fields.is_empty() {
fields.push_str(", "); fields.push_str(", ");
@ -354,11 +340,19 @@ async fn update_user(
fields.push_str(&format!("password = '{password_hash}'")); fields.push_str(&format!("password = '{password_hash}'"));
} }
if handles::update_user(&pool, *id, fields).await.is_ok() { handles::update_user(&pool, *id, fields).await?;
return Ok("Update Success");
};
Err(ServiceError::InternalServerError) let related_channels = handles::select_related_channels(&pool, Some(*id)).await?;
for channel in related_channels {
if !channel_ids.contains(&channel.id) {
handles::delete_user_channel(&pool, *id, channel.id).await?;
}
}
handles::insert_user_channel(&pool, *id, channel_ids).await?;
Ok("Update Success")
} }
/// **Add User** /// **Add User**
@ -484,10 +478,17 @@ async fn patch_channel(
role: AuthDetails<Role>, role: AuthDetails<Role>,
user: web::ReqData<UserMeta>, user: web::ReqData<UserMeta>,
) -> Result<impl Responder, ServiceError> { ) -> Result<impl Responder, ServiceError> {
if handles::update_channel(&pool, *id, data.into_inner()) let mut data = data.into_inner();
.await
.is_ok() if !role.has_authority(&Role::GlobalAdmin) {
{ let channel = handles::select_channel(&pool, &id).await?;
data.hls_path = channel.hls_path;
data.playlist_path = channel.playlist_path;
data.storage_path = channel.storage_path;
}
if handles::update_channel(&pool, *id, data).await.is_ok() {
return Ok("Update Success"); return Ok("Update Success");
}; };
@ -1028,7 +1029,7 @@ pub async fn gen_playlist(
) -> Result<impl Responder, ServiceError> { ) -> Result<impl Responder, ServiceError> {
let manager = controllers.lock().unwrap().get(params.0).unwrap(); let manager = controllers.lock().unwrap().get(params.0).unwrap();
manager.config.lock().unwrap().general.generate = Some(vec![params.1.clone()]); manager.config.lock().unwrap().general.generate = Some(vec![params.1.clone()]);
let storage_path = manager.config.lock().unwrap().global.storage_path.clone(); let storage_path = manager.config.lock().unwrap().channel.storage_path.clone();
if let Some(obj) = data { if let Some(obj) = data {
if let Some(paths) = &obj.paths { if let Some(paths) = &obj.paths {
@ -1270,7 +1271,7 @@ async fn get_file(
let id: i32 = req.match_info().query("id").parse()?; let id: i32 = req.match_info().query("id").parse()?;
let manager = controllers.lock().unwrap().get(id).unwrap(); let manager = controllers.lock().unwrap().get(id).unwrap();
let config = manager.config.lock().unwrap(); let config = manager.config.lock().unwrap();
let storage_path = config.global.storage_path.clone(); let storage_path = config.channel.storage_path.clone();
let file_path = req.match_info().query("filename"); let file_path = req.match_info().query("filename");
let (path, _, _) = norm_abs_path(&storage_path, file_path)?; let (path, _, _) = norm_abs_path(&storage_path, file_path)?;
let file = actix_files::NamedFile::open(path)?; let file = actix_files::NamedFile::open(path)?;
@ -1301,7 +1302,7 @@ async fn get_public(
let absolute_path = if file_stem.ends_with(".ts") || file_stem.ends_with(".m3u8") { let absolute_path = if file_stem.ends_with(".ts") || file_stem.ends_with(".m3u8") {
let manager = controllers.lock().unwrap().get(id).unwrap(); let manager = controllers.lock().unwrap().get(id).unwrap();
let config = manager.config.lock().unwrap(); let config = manager.config.lock().unwrap();
config.global.hls_path.join(public) config.channel.hls_path.join(public)
} else if public_path.is_absolute() { } else if public_path.is_absolute() {
public_path.to_path_buf() public_path.to_path_buf()
} else { } else {

View File

@ -40,7 +40,7 @@ pub async fn db_migrate(conn: &Pool<Sqlite>) -> Result<&'static str, Box<dyn std
} }
pub async fn select_global(conn: &Pool<Sqlite>) -> Result<GlobalSettings, sqlx::Error> { pub async fn select_global(conn: &Pool<Sqlite>) -> Result<GlobalSettings, sqlx::Error> {
let query = "SELECT id, secret, hls_path, logging_path, playlist_path, storage_path, shared_storage FROM global WHERE id = 1"; let query = "SELECT id, secret, logging_path, playlist_root, public_root, storage_root, shared_storage FROM global WHERE id = 1";
sqlx::query_as(query).fetch_one(conn).await sqlx::query_as(query).fetch_one(conn).await
} }
@ -49,14 +49,14 @@ pub async fn update_global(
conn: &Pool<Sqlite>, conn: &Pool<Sqlite>,
global: GlobalSettings, global: GlobalSettings,
) -> Result<SqliteQueryResult, sqlx::Error> { ) -> Result<SqliteQueryResult, sqlx::Error> {
let query = "UPDATE global SET hls_path = $2, playlist_path = $3, storage_path = $4, logging_path = $5, shared_storage = $6 WHERE id = 1"; let query = "UPDATE global SET logging_path = $2, playlist_root = $3, public_root = $4, storage_root = $5, shared_storage = $6 WHERE id = 1";
sqlx::query(query) sqlx::query(query)
.bind(global.id) .bind(global.id)
.bind(global.hls_path)
.bind(global.playlist_path)
.bind(global.storage_path)
.bind(global.logging_path) .bind(global.logging_path)
.bind(global.playlist_root)
.bind(global.public_root)
.bind(global.storage_root)
.bind(global.shared_storage) .bind(global.shared_storage)
.execute(conn) .execute(conn)
.await .await
@ -77,7 +77,7 @@ pub async fn select_related_channels(
) -> Result<Vec<Channel>, sqlx::Error> { ) -> Result<Vec<Channel>, sqlx::Error> {
let query = match user_id { let query = match user_id {
Some(id) => format!( Some(id) => format!(
"SELECT c.id, c.name, c.preview_url, c.extra_extensions, c.active, c.last_date, c.time_shift FROM channels c "SELECT c.id, c.name, c.preview_url, c.extra_extensions, c.active, c.hls_path, c.playlist_path, c.storage_path, c.last_date, c.time_shift FROM channels c
left join user_channels uc on uc.channel_id = c.id left join user_channels uc on uc.channel_id = c.id
left join user u on u.id = uc.user_id left join user u on u.id = uc.user_id
WHERE u.id = {id} ORDER BY c.id ASC;" WHERE u.id = {id} ORDER BY c.id ASC;"
@ -94,19 +94,36 @@ pub async fn select_related_channels(
Ok(results) Ok(results)
} }
pub async fn delete_user_channel(
conn: &Pool<Sqlite>,
user_id: i32,
channel_id: i32,
) -> Result<SqliteQueryResult, sqlx::Error> {
let query = "DELETE FROM user_channels WHERE user_id = $1 AND channel_id = $2";
sqlx::query(query)
.bind(user_id)
.bind(channel_id)
.execute(conn)
.await
}
pub async fn update_channel( pub async fn update_channel(
conn: &Pool<Sqlite>, conn: &Pool<Sqlite>,
id: i32, id: i32,
channel: Channel, channel: Channel,
) -> Result<SqliteQueryResult, sqlx::Error> { ) -> Result<SqliteQueryResult, sqlx::Error> {
let query = let query =
"UPDATE channels SET name = $2, preview_url = $3, extra_extensions = $4 WHERE id = $1"; "UPDATE channels SET name = $2, preview_url = $3, extra_extensions = $4, hls_path = $5, playlist_path = $6, storage_path = $7 WHERE id = $1";
sqlx::query(query) sqlx::query(query)
.bind(id) .bind(id)
.bind(channel.name) .bind(channel.name)
.bind(channel.preview_url) .bind(channel.preview_url)
.bind(channel.extra_extensions) .bind(channel.extra_extensions)
.bind(channel.hls_path)
.bind(channel.playlist_path)
.bind(channel.storage_path)
.execute(conn) .execute(conn)
.await .await
} }
@ -138,11 +155,14 @@ pub async fn update_player(
} }
pub async fn insert_channel(conn: &Pool<Sqlite>, channel: Channel) -> Result<Channel, sqlx::Error> { pub async fn insert_channel(conn: &Pool<Sqlite>, channel: Channel) -> Result<Channel, sqlx::Error> {
let query = "INSERT INTO channels (name, preview_url, extra_extensions) VALUES($1, $2, $3)"; let query = "INSERT INTO channels (name, preview_url, extra_extensions, hls_path, playlist_path, storage_path) VALUES($1, $2, $3, $4, $5, $6)";
let result = sqlx::query(query) let result = sqlx::query(query)
.bind(channel.name) .bind(channel.name)
.bind(channel.preview_url) .bind(channel.preview_url)
.bind(channel.extra_extensions) .bind(channel.extra_extensions)
.bind(channel.hls_path)
.bind(channel.playlist_path)
.bind(channel.storage_path)
.execute(conn) .execute(conn)
.await?; .await?;

View File

@ -16,10 +16,10 @@ use crate::utils::config::PlayoutConfig;
pub struct GlobalSettings { pub struct GlobalSettings {
pub id: i32, pub id: i32,
pub secret: Option<String>, pub secret: Option<String>,
pub hls_path: String,
pub logging_path: String, pub logging_path: String,
pub playlist_path: String, pub playlist_root: String,
pub storage_path: String, pub public_root: String,
pub storage_root: String,
pub shared_storage: bool, pub shared_storage: bool,
} }
@ -32,10 +32,10 @@ impl GlobalSettings {
Err(_) => GlobalSettings { Err(_) => GlobalSettings {
id: 0, id: 0,
secret: None, secret: None,
hls_path: String::new(),
logging_path: String::new(), logging_path: String::new(),
playlist_path: String::new(), playlist_root: String::new(),
storage_path: String::new(), public_root: String::new(),
storage_root: String::new(),
shared_storage: false, shared_storage: false,
}, },
} }
@ -240,6 +240,9 @@ pub struct Channel {
pub preview_url: String, pub preview_url: String,
pub extra_extensions: String, pub extra_extensions: String,
pub active: bool, pub active: bool,
pub hls_path: String,
pub playlist_path: String,
pub storage_path: String,
pub last_date: Option<String>, pub last_date: Option<String>,
pub time_shift: f64, pub time_shift: f64,

View File

@ -2,7 +2,7 @@ use std::{
collections::HashSet, collections::HashSet,
env, env,
fs::File, fs::File,
io, io::{self, stdin, stdout, Write},
process::exit, process::exit,
sync::{atomic::AtomicBool, Arc, Mutex}, sync::{atomic::AtomicBool, Arc, Mutex},
thread, thread,
@ -14,6 +14,7 @@ use actix_web::{
}; };
use actix_web_grants::authorities::AttachAuthorities; use actix_web_grants::authorities::AttachAuthorities;
use actix_web_httpauth::{extractors::bearer::BearerAuth, middleware::HttpAuthentication}; use actix_web_httpauth::{extractors::bearer::BearerAuth, middleware::HttpAuthentication};
use sqlx::{migrate::MigrateDatabase, Sqlite};
#[cfg(all(not(debug_assertions), feature = "embed_frontend"))] #[cfg(all(not(debug_assertions), feature = "embed_frontend"))]
use actix_web_static_files::ResourceFiles; use actix_web_static_files::ResourceFiles;
@ -35,6 +36,7 @@ use ffplayout::{
utils::{ utils::{
args_parse::run_args, args_parse::run_args,
config::get_config, config::get_config,
db_path,
logging::{init_logging, MailQueue}, logging::{init_logging, MailQueue},
playlist::generate_playlist, playlist::generate_playlist,
}, },
@ -263,7 +265,7 @@ async fn main() -> std::io::Result<()> {
exit(1); exit(1);
}; };
} else if ARGS.validate { } else if ARGS.validate {
let mut playlist_path = config.global.playlist_path.clone(); let mut playlist_path = config.channel.playlist_path.clone();
let start_sec = config.playlist.start_sec.unwrap(); let start_sec = config.playlist.start_sec.unwrap();
let date = get_date(false, start_sec, false); let date = get_date(false, start_sec, false);
@ -289,7 +291,25 @@ async fn main() -> std::io::Result<()> {
playlist, playlist,
Arc::new(AtomicBool::new(false)), Arc::new(AtomicBool::new(false)),
); );
} else { } else if ARGS.drop_db {
let mut drop_answer = String::new();
print!("Drop Database [Y/n]: ");
stdout().flush().unwrap();
stdin()
.read_line(&mut drop_answer)
.expect("Did not enter a yes or no?");
let drop = drop_answer.trim().to_lowercase().starts_with('y');
if drop {
match Sqlite::drop_database(db_path().unwrap()).await {
Ok(_) => info!("Successfully dropped DB"),
Err(e) => error!("{e}"),
};
}
} else if !ARGS.init {
error!("Run ffplayout with parameters! Run ffplayout -h for more information."); error!("Run ffplayout with parameters! Run ffplayout -h for more information.");
} }
} }

View File

@ -337,7 +337,7 @@ pub fn start_channel(manager: ChannelManager) -> Result<(), ProcessError> {
let channel_id = config.general.channel_id; let channel_id = config.general.channel_id;
drain_hls_path( drain_hls_path(
&config.global.hls_path, &config.channel.hls_path,
&config.output.output_cmd.clone().unwrap_or_default(), &config.output.output_cmd.clone().unwrap_or_default(),
channel_id, channel_id,
)?; )?;

View File

@ -29,7 +29,7 @@ pub fn watchman(
sources: Arc<Mutex<Vec<Media>>>, sources: Arc<Mutex<Vec<Media>>>,
) { ) {
let id = config.general.channel_id; let id = config.general.channel_id;
let path = Path::new(&config.global.storage_path); let path = Path::new(&config.channel.storage_path);
if !path.exists() { if !path.exists() {
error!("Folder path not exists: '{path:?}'"); error!("Folder path not exists: '{path:?}'");

View File

@ -28,7 +28,7 @@ pub fn source_generator(manager: ChannelManager) -> Box<dyn Iterator<Item = Medi
info!(target: Target::file_mail(), channel = id; "Playout in folder mode"); info!(target: Target::file_mail(), channel = id; "Playout in folder mode");
debug!(target: Target::file_mail(), channel = id; debug!(target: Target::file_mail(), channel = id;
"Monitor folder: <b><magenta>{:?}</></b>", "Monitor folder: <b><magenta>{:?}</></b>",
config.global.storage_path config.channel.storage_path
); );
let config_clone = config.clone(); let config_clone = config.clone();

View File

@ -317,7 +317,7 @@ impl CurrentProgram {
if self.current_node.source.contains( if self.current_node.source.contains(
&self &self
.config .config
.global .channel
.storage_path .storage_path
.to_string_lossy() .to_string_lossy()
.to_string(), .to_string(),

View File

@ -40,7 +40,7 @@ impl FolderSource {
path_list.push(path) path_list.push(path)
} }
} else { } else {
path_list.push(&config.global.storage_path) path_list.push(&config.channel.storage_path)
} }
for path in &path_list { for path in &path_list {

View File

@ -28,13 +28,13 @@ pub fn import_file(
program: vec![], program: vec![],
}; };
let playlist_root = &config.global.playlist_path; let playlist_root = &config.channel.playlist_path;
if !playlist_root.is_dir() { if !playlist_root.is_dir() {
return Err(Error::new( return Err(Error::new(
ErrorKind::Other, ErrorKind::Other,
format!( format!(
"Playlist folder <b><magenta>{:?}</></b> not exists!", "Playlist folder <b><magenta>{:?}</></b> not exists!",
config.global.playlist_path, config.channel.playlist_path,
), ),
)); ));
} }

View File

@ -100,11 +100,11 @@ pub fn read_json(
) -> JsonPlaylist { ) -> JsonPlaylist {
let id = config.general.channel_id; let id = config.general.channel_id;
let config_clone = config.clone(); let config_clone = config.clone();
let mut playlist_path = config.global.playlist_path.clone(); let mut playlist_path = config.channel.playlist_path.clone();
let start_sec = config.playlist.start_sec.unwrap(); let start_sec = config.playlist.start_sec.unwrap();
let date = get_date(seek, start_sec, get_next); let date = get_date(seek, start_sec, get_next);
if playlist_path.is_dir() || is_remote(&config.global.playlist_path.to_string_lossy()) { if playlist_path.is_dir() || is_remote(&config.channel.playlist_path.to_string_lossy()) {
let d: Vec<&str> = date.split('-').collect(); let d: Vec<&str> = date.split('-').collect();
playlist_path = playlist_path playlist_path = playlist_path
.join(d[0]) .join(d[0])

View File

@ -1,8 +1,11 @@
use std::{ use std::{
io::{stdin, stdout, Write}, io::{stdin, stdout, Write},
path::PathBuf, path::{Path, PathBuf},
}; };
#[cfg(target_family = "unix")]
use std::{fs, process::exit};
use clap::Parser; use clap::Parser;
use rpassword::read_password; use rpassword::read_password;
use sqlx::{Pool, Sqlite}; use sqlx::{Pool, Sqlite};
@ -17,6 +20,9 @@ use crate::utils::{
}; };
use crate::ARGS; use crate::ARGS;
#[cfg(target_family = "unix")]
use crate::utils::db_path;
#[derive(Parser, Debug, Clone)] #[derive(Parser, Debug, Clone)]
#[clap(version, #[clap(version,
about = "ffplayout - 24/7 broadcasting solution", about = "ffplayout - 24/7 broadcasting solution",
@ -32,9 +38,16 @@ pub struct Args {
#[clap(short, long, help = "Add a global admin user")] #[clap(short, long, help = "Add a global admin user")]
pub add: bool, pub add: bool,
#[clap(long, env, help = "path to database file")] #[clap(long, env, help = "Path to database file")]
pub db: Option<PathBuf>, pub db: Option<PathBuf>,
#[clap(
long,
env,
help = "Drop database. WARNING: this will delete all configurations!"
)]
pub drop_db: bool,
#[clap( #[clap(
short, short,
long, long,
@ -110,16 +123,20 @@ pub struct Args {
#[clap(long, env, help = "Log to console")] #[clap(long, env, help = "Log to console")]
pub log_to_console: bool, pub log_to_console: bool,
#[clap(long, env, help = "HLS output path")] #[clap(long, env, help = "Public (HLS) output path")]
pub hls_path: Option<String>, pub public_root: Option<String>,
#[clap(long, env, help = "Playlist root path")] #[clap(long, env, help = "Playlist root path")]
pub playlist_path: Option<String>, pub playlist_root: Option<String>,
#[clap(long, env, help = "Storage root path")] #[clap(long, env, help = "Storage root path")]
pub storage_path: Option<String>, pub storage_root: Option<String>,
#[clap(long, env, help = "Share storage across channels")] #[clap(
long,
env,
help = "Share storage root across channels, important for running in Container"
)]
pub shared_storage: bool, pub shared_storage: bool,
#[clap(short, long, help = "Create admin user")] #[clap(short, long, help = "Create admin user")]
@ -202,6 +219,43 @@ pub async fn run_args(pool: &Pool<Sqlite>) -> Result<(), i32> {
let mut error_code = -1; let mut error_code = -1;
if args.init { if args.init {
#[cfg(target_family = "unix")]
let process_user = nix::unistd::User::from_name("ffpu").unwrap_or_default();
#[cfg(target_family = "unix")]
let mut fix_permission = false;
#[cfg(target_family = "unix")]
{
let uid = nix::unistd::Uid::current();
let current_user = nix::unistd::User::from_uid(uid).unwrap_or_default();
if current_user != process_user {
let user_name = current_user.unwrap().name;
let mut fix_perm = String::new();
println!(
"\nYou run the initialization as user {}.\nFix permissions after initialization?\n",
user_name
);
print!("Fix permission [Y/n]: ");
stdout().flush().unwrap();
stdin()
.read_line(&mut fix_perm)
.expect("Did not enter a yes or no?");
fix_permission = fix_perm.trim().to_lowercase().starts_with('y');
if fix_permission && user_name != "root" {
println!("\nYou do not have permission to change DB file ownership!\nRun as proper process user or root.");
exit(1);
}
}
}
let check_user = handles::select_users(pool).await; let check_user = handles::select_users(pool).await;
let mut storage = String::new(); let mut storage = String::new();
@ -212,10 +266,10 @@ pub async fn run_args(pool: &Pool<Sqlite>) -> Result<(), i32> {
let mut global = GlobalSettings { let mut global = GlobalSettings {
id: 0, id: 0,
secret: None, secret: None,
hls_path: String::new(),
playlist_path: String::new(),
storage_path: String::new(),
logging_path: String::new(), logging_path: String::new(),
playlist_root: String::new(),
public_root: String::new(),
storage_root: String::new(),
shared_storage: false, shared_storage: false,
}; };
@ -231,9 +285,9 @@ pub async fn run_args(pool: &Pool<Sqlite>) -> Result<(), i32> {
.expect("Did not enter a correct path?"); .expect("Did not enter a correct path?");
if storage.trim().is_empty() { if storage.trim().is_empty() {
global.storage_path = "/var/lib/ffplayout/tv-media".to_string(); global.storage_root = "/var/lib/ffplayout/tv-media".to_string();
} else { } else {
global.storage_path = storage global.storage_root = storage
.trim() .trim()
.trim_matches(|c| c == '"' || c == '\'') .trim_matches(|c| c == '"' || c == '\'')
.to_string(); .to_string();
@ -247,9 +301,9 @@ pub async fn run_args(pool: &Pool<Sqlite>) -> Result<(), i32> {
.expect("Did not enter a correct path?"); .expect("Did not enter a correct path?");
if playlist.trim().is_empty() { if playlist.trim().is_empty() {
global.playlist_path = "/var/lib/ffplayout/playlists".to_string(); global.playlist_root = "/var/lib/ffplayout/playlists".to_string();
} else { } else {
global.playlist_path = playlist global.playlist_root = playlist
.trim() .trim()
.trim_matches(|c| c == '"' || c == '\'') .trim_matches(|c| c == '"' || c == '\'')
.to_string(); .to_string();
@ -271,7 +325,7 @@ pub async fn run_args(pool: &Pool<Sqlite>) -> Result<(), i32> {
.to_string(); .to_string();
} }
print!("HLS path [/usr/share/ffplayout/public]: "); print!("Public (HLS) path [/usr/share/ffplayout/public]: ");
stdout().flush().unwrap(); stdout().flush().unwrap();
stdin() stdin()
@ -279,9 +333,9 @@ pub async fn run_args(pool: &Pool<Sqlite>) -> Result<(), i32> {
.expect("Did not enter a correct path?"); .expect("Did not enter a correct path?");
if hls.trim().is_empty() { if hls.trim().is_empty() {
global.hls_path = "/usr/share/ffplayout/public".to_string(); global.public_root = "/usr/share/ffplayout/public".to_string();
} else { } else {
global.hls_path = hls global.public_root = hls
.trim() .trim()
.trim_matches(|c| c == '"' || c == '\'') .trim_matches(|c| c == '"' || c == '\'')
.to_string(); .to_string();
@ -292,7 +346,7 @@ pub async fn run_args(pool: &Pool<Sqlite>) -> Result<(), i32> {
stdin() stdin()
.read_line(&mut shared_store) .read_line(&mut shared_store)
.expect("Did not enter a correct path?"); .expect("Did not enter a yes or no?");
global.shared_storage = shared_store.trim().to_lowercase().starts_with('y'); global.shared_storage = shared_store.trim().to_lowercase().starts_with('y');
@ -301,14 +355,52 @@ pub async fn run_args(pool: &Pool<Sqlite>) -> Result<(), i32> {
error_code = 1; error_code = 1;
}; };
if !global.shared_storage {
let mut channel = handles::select_channel(pool, &1).await.unwrap(); let mut channel = handles::select_channel(pool, &1).await.unwrap();
channel.preview_url = "http://127.0.0.1:8787/1/stream.m3u8".to_string(); channel.hls_path = global.public_root;
channel.playlist_path = global.playlist_root;
channel.storage_path = global.storage_root;
handles::update_channel(pool, 1, channel).await.unwrap(); if global.shared_storage {
channel.preview_url = "http://127.0.0.1:8787/1/stream.m3u8".to_string();
channel.hls_path = Path::new(&channel.hls_path)
.join("1")
.to_string_lossy()
.to_string();
channel.playlist_path = Path::new(&channel.playlist_path)
.join("1")
.to_string_lossy()
.to_string();
channel.storage_path = Path::new(&channel.storage_path)
.join("1")
.to_string_lossy()
.to_string();
}; };
println!("Set global settings..."); handles::update_channel(pool, 1, channel).await.unwrap();
#[cfg(target_family = "unix")]
if fix_permission {
let db_path = Path::new(db_path().unwrap()).with_extension("");
let user = process_user.unwrap();
let db = fs::canonicalize(db_path.with_extension("db")).unwrap();
let shm = fs::canonicalize(db_path.with_extension("db-shm")).unwrap();
let wal = fs::canonicalize(db_path.with_extension("db-wal")).unwrap();
nix::unistd::chown(&db, Some(user.uid), Some(user.gid)).expect("Change DB owner");
if shm.is_file() {
nix::unistd::chown(&shm, Some(user.uid), Some(user.gid))
.expect("Change DB-SHM owner");
}
if wal.is_file() {
nix::unistd::chown(&wal, Some(user.uid), Some(user.gid))
.expect("Change DB-WAL owner");
}
}
println!("\nSet global settings done...");
} }
if args.add { if args.add {
@ -344,9 +436,9 @@ pub async fn run_args(pool: &Pool<Sqlite>) -> Result<(), i32> {
} }
if !args.init if !args.init
&& args.storage_path.is_some() && args.storage_root.is_some()
&& args.playlist_path.is_some() && args.playlist_root.is_some()
&& args.hls_path.is_some() && args.public_root.is_some()
&& args.log_path.is_some() && args.log_path.is_some()
{ {
error_code = 0; error_code = 0;
@ -354,18 +446,19 @@ pub async fn run_args(pool: &Pool<Sqlite>) -> Result<(), i32> {
let global = GlobalSettings { let global = GlobalSettings {
id: 0, id: 0,
secret: None, secret: None,
hls_path: args.hls_path.unwrap(),
playlist_path: args.playlist_path.unwrap(),
storage_path: args.storage_path.unwrap(),
logging_path: args.log_path.unwrap().to_string_lossy().to_string(), logging_path: args.log_path.unwrap().to_string_lossy().to_string(),
playlist_root: args.playlist_root.unwrap(),
public_root: args.public_root.unwrap(),
storage_root: args.storage_root.unwrap(),
shared_storage: args.shared_storage, shared_storage: args.shared_storage,
}; };
if let Err(e) = handles::update_global(pool, global.clone()).await { match handles::update_global(pool, global.clone()).await {
Ok(_) => println!("Update global paths..."),
Err(e) => {
eprintln!("{e}"); eprintln!("{e}");
error_code = 1; error_code = 1;
} else { }
println!("Update global paths...");
}; };
} }

View File

@ -61,10 +61,26 @@ pub async fn create_channel(
queue: Arc<Mutex<Vec<Arc<Mutex<MailQueue>>>>>, queue: Arc<Mutex<Vec<Arc<Mutex<MailQueue>>>>>,
target_channel: Channel, target_channel: Channel,
) -> Result<Channel, ServiceError> { ) -> Result<Channel, ServiceError> {
let global = handles::select_global(conn).await?;
let mut channel = handles::insert_channel(conn, target_channel).await?; let mut channel = handles::insert_channel(conn, target_channel).await?;
channel.preview_url = preview_url(&channel.preview_url, channel.id); channel.preview_url = preview_url(&channel.preview_url, channel.id);
if global.shared_storage {
channel.hls_path = Path::new(&channel.hls_path)
.join(channel.id.to_string())
.to_string_lossy()
.to_string();
channel.playlist_path = Path::new(&channel.playlist_path)
.join(channel.id.to_string())
.to_string_lossy()
.to_string();
channel.storage_path = Path::new(&channel.storage_path)
.join(channel.id.to_string())
.to_string_lossy()
.to_string();
}
handles::update_channel(conn, channel.id, channel.clone()).await?; handles::update_channel(conn, channel.id, channel.clone()).await?;
let output_param = "-c:v libx264 -crf 23 -x264-params keyint=50:min-keyint=25:scenecut=-1 -maxrate 1300k -bufsize 2600k -preset faster -tune zerolatency -profile:v Main -level 3.1 -c:a aac -ar 44100 -b:a 128k -flags +cgop -f hls -hls_time 6 -hls_list_size 600 -hls_flags append_list+delete_segments+omit_endlist -hls_segment_filename live/stream-%d.ts live/stream.m3u8".to_string(); let output_param = "-c:v libx264 -crf 23 -x264-params keyint=50:min-keyint=25:scenecut=-1 -maxrate 1300k -bufsize 2600k -preset faster -tune zerolatency -profile:v Main -level 3.1 -c:a aac -ar 44100 -b:a 128k -flags +cgop -f hls -hls_time 6 -hls_list_size 600 -hls_flags append_list+delete_segments+omit_endlist -hls_segment_filename live/stream-%d.ts live/stream.m3u8".to_string();

View File

@ -26,12 +26,13 @@ pub const IMAGE_FORMAT: [&str; 21] = [
]; ];
// Some well known errors can be safely ignore // Some well known errors can be safely ignore
pub const FFMPEG_IGNORE_ERRORS: [&str; 12] = [ pub const FFMPEG_IGNORE_ERRORS: [&str; 13] = [
"ac-tex damaged", "ac-tex damaged",
"codec s302m, is muxed as a private data stream", "codec s302m, is muxed as a private data stream",
"corrupt decoded frame in stream", "corrupt decoded frame in stream",
"corrupt input packet in stream", "corrupt input packet in stream",
"end mismatch left", "end mismatch left",
"Invalid mb type in I-frame at",
"Packet corrupt", "Packet corrupt",
"Referenced QT chapter track not found", "Referenced QT chapter track not found",
"skipped MB in I-frame at", "skipped MB in I-frame at",
@ -151,13 +152,13 @@ pub struct Source {
pub paths: Vec<PathBuf>, pub paths: Vec<PathBuf>,
} }
/// Global Config /// Channel Config
/// ///
/// This we init ones, when ffplayout is starting and use them globally in the hole program. /// This we init ones, when ffplayout is starting and use them globally in the hole program.
#[derive(Debug, Default, Clone, Deserialize, Serialize)] #[derive(Debug, Default, Clone, Deserialize, Serialize)]
pub struct PlayoutConfig { pub struct PlayoutConfig {
#[serde(skip_serializing, skip_deserializing)] #[serde(skip_serializing, skip_deserializing)]
pub global: Global, pub channel: Channel,
#[serde(skip_serializing, skip_deserializing)] #[serde(skip_serializing, skip_deserializing)]
pub advanced: AdvancedConfig, pub advanced: AdvancedConfig,
pub general: General, pub general: General,
@ -174,21 +175,21 @@ pub struct PlayoutConfig {
} }
#[derive(Debug, Default, Clone, Deserialize, Serialize)] #[derive(Debug, Default, Clone, Deserialize, Serialize)]
pub struct Global { pub struct Channel {
pub logging_path: PathBuf,
pub hls_path: PathBuf, pub hls_path: PathBuf,
pub playlist_path: PathBuf, pub playlist_path: PathBuf,
pub storage_path: PathBuf, pub storage_path: PathBuf,
pub logging_path: PathBuf,
pub shared_storage: bool, pub shared_storage: bool,
} }
impl Global { impl Channel {
pub fn new(config: &models::GlobalSettings) -> Self { pub fn new(config: &models::GlobalSettings) -> Self {
Self { Self {
hls_path: PathBuf::from(config.hls_path.clone()),
playlist_path: PathBuf::from(config.playlist_path.clone()),
storage_path: PathBuf::from(config.storage_path.clone()),
logging_path: PathBuf::from(config.logging_path.clone()), logging_path: PathBuf::from(config.logging_path.clone()),
hls_path: PathBuf::from(config.public_root.clone()),
playlist_path: PathBuf::from(config.playlist_root.clone()),
storage_path: PathBuf::from(config.storage_root.clone()),
shared_storage: config.shared_storage, shared_storage: config.shared_storage,
} }
} }
@ -559,7 +560,7 @@ impl PlayoutConfig {
.await .await
.expect("Can't read advanced config"); .expect("Can't read advanced config");
let mut global = Global::new(&global); let mut channel = Channel::new(&global);
let advanced = AdvancedConfig::new(adv_config); let advanced = AdvancedConfig::new(adv_config);
let general = General::new(&config); let general = General::new(&config);
let mail = Mail::new(&config); let mail = Mail::new(&config);
@ -571,42 +572,42 @@ impl PlayoutConfig {
let task = Task::new(&config); let task = Task::new(&config);
let mut output = Output::new(&config); let mut output = Output::new(&config);
if !global.shared_storage { if global.shared_storage {
global.storage_path = global.storage_path.join(channel_id.to_string()); channel.storage_path = channel.storage_path.join(channel_id.to_string());
} }
if !global.storage_path.is_dir() { if !channel.storage_path.is_dir() {
tokio::fs::create_dir_all(&global.storage_path) tokio::fs::create_dir_all(&channel.storage_path)
.await .await
.unwrap_or_else(|_| { .unwrap_or_else(|_| {
panic!("Can't create storage folder: {:#?}", global.storage_path) panic!("Can't create storage folder: {:#?}", channel.storage_path)
}); });
} }
let mut storage = Storage::new(&config, global.storage_path.clone()); let mut storage = Storage::new(&config, channel.storage_path.clone());
if channel_id > 1 || !global.shared_storage { if channel_id > 1 || !global.shared_storage {
global.playlist_path = global.playlist_path.join(channel_id.to_string()); channel.playlist_path = channel.playlist_path.join(channel_id.to_string());
global.hls_path = global.hls_path.join(channel_id.to_string()); channel.hls_path = channel.hls_path.join(channel_id.to_string());
} }
if !global.playlist_path.is_dir() { if !channel.playlist_path.is_dir() {
tokio::fs::create_dir_all(&global.playlist_path) tokio::fs::create_dir_all(&channel.playlist_path)
.await .await
.unwrap_or_else(|_| { .unwrap_or_else(|_| {
panic!("Can't create playlist folder: {:#?}", global.playlist_path) panic!("Can't create playlist folder: {:#?}", channel.playlist_path)
}); });
} }
if !global.logging_path.is_dir() { if !channel.logging_path.is_dir() {
tokio::fs::create_dir_all(&global.logging_path) tokio::fs::create_dir_all(&channel.logging_path)
.await .await
.unwrap_or_else(|_| { .unwrap_or_else(|_| {
panic!("Can't create logging folder: {:#?}", global.logging_path) panic!("Can't create logging folder: {:#?}", channel.logging_path)
}); });
} }
let (filler_path, _, _) = norm_abs_path(&global.storage_path, &config.storage_filler) let (filler_path, _, _) = norm_abs_path(&channel.storage_path, &config.storage_filler)
.expect("Can't get filler path"); .expect("Can't get filler path");
storage.filler = filler_path; storage.filler = filler_path;
@ -700,7 +701,7 @@ impl PlayoutConfig {
for item in cmd.iter_mut() { for item in cmd.iter_mut() {
if item.ends_with(".ts") || (item.ends_with(".m3u8") && item != "master.m3u8") { if item.ends_with(".ts") || (item.ends_with(".m3u8") && item != "master.m3u8") {
if let Ok((hls_path, _, _)) = norm_abs_path(&global.hls_path, item) { if let Ok((hls_path, _, _)) = norm_abs_path(&channel.hls_path, item) {
let parent = hls_path.parent().expect("HLS parent path"); let parent = hls_path.parent().expect("HLS parent path");
if !parent.is_dir() { if !parent.is_dir() {
@ -728,7 +729,7 @@ impl PlayoutConfig {
} }
Self { Self {
global, channel,
advanced, advanced,
general, general,
mail, mail,
@ -749,7 +750,7 @@ impl PlayoutConfig {
&config &config
.storage .storage
.filler .filler
.strip_prefix(config.global.storage_path.clone()) .strip_prefix(config.channel.storage_path.clone())
.unwrap_or(&config.storage.filler) .unwrap_or(&config.storage.filler)
.to_path_buf(), .to_path_buf(),
); );
@ -849,11 +850,11 @@ pub async fn get_config(pool: &Pool<Sqlite>, channel_id: i32) -> Result<PlayoutC
} }
if let Some(playlist) = args.playlist { if let Some(playlist) = args.playlist {
config.global.playlist_path = playlist; config.channel.playlist_path = playlist;
} }
if let Some(folder) = args.folder { if let Some(folder) = args.folder {
config.global.storage_path = folder; config.channel.storage_path = folder;
config.processing.mode = ProcessMode::Folder; config.processing.mode = ProcessMode::Folder;
} }

View File

@ -116,12 +116,12 @@ pub async fn browser(
extensions.append(&mut channel_extensions); extensions.append(&mut channel_extensions);
let (path, parent, path_component) = let (path, parent, path_component) =
norm_abs_path(&config.global.storage_path, &path_obj.source)?; norm_abs_path(&config.channel.storage_path, &path_obj.source)?;
let parent_path = if !path_component.is_empty() { let parent_path = if !path_component.is_empty() {
path.parent().unwrap() path.parent().unwrap()
} else { } else {
&config.global.storage_path &config.channel.storage_path
}; };
let mut obj = PathObject::new(path_component, Some(parent)); let mut obj = PathObject::new(path_component, Some(parent));
@ -212,7 +212,7 @@ pub async fn create_directory(
config: &PlayoutConfig, config: &PlayoutConfig,
path_obj: &PathObject, path_obj: &PathObject,
) -> Result<HttpResponse, ServiceError> { ) -> Result<HttpResponse, ServiceError> {
let (path, _, _) = norm_abs_path(&config.global.storage_path, &path_obj.source)?; let (path, _, _) = norm_abs_path(&config.channel.storage_path, &path_obj.source)?;
if let Err(e) = fs::create_dir_all(&path).await { if let Err(e) = fs::create_dir_all(&path).await {
return Err(ServiceError::BadRequest(e.to_string())); return Err(ServiceError::BadRequest(e.to_string()));
@ -281,8 +281,8 @@ pub async fn rename_file(
config: &PlayoutConfig, config: &PlayoutConfig,
move_object: &MoveObject, move_object: &MoveObject,
) -> Result<MoveObject, ServiceError> { ) -> Result<MoveObject, ServiceError> {
let (source_path, _, _) = norm_abs_path(&config.global.storage_path, &move_object.source)?; let (source_path, _, _) = norm_abs_path(&config.channel.storage_path, &move_object.source)?;
let (mut target_path, _, _) = norm_abs_path(&config.global.storage_path, &move_object.target)?; let (mut target_path, _, _) = norm_abs_path(&config.channel.storage_path, &move_object.target)?;
if !source_path.exists() { if !source_path.exists() {
return Err(ServiceError::BadRequest("Source file not exist!".into())); return Err(ServiceError::BadRequest("Source file not exist!".into()));
@ -314,7 +314,7 @@ pub async fn remove_file_or_folder(
config: &PlayoutConfig, config: &PlayoutConfig,
source_path: &str, source_path: &str,
) -> Result<(), ServiceError> { ) -> Result<(), ServiceError> {
let (source, _, _) = norm_abs_path(&config.global.storage_path, source_path)?; let (source, _, _) = norm_abs_path(&config.channel.storage_path, source_path)?;
if !source.exists() { if !source.exists() {
return Err(ServiceError::BadRequest("Source does not exists!".into())); return Err(ServiceError::BadRequest("Source does not exists!".into()));
@ -346,7 +346,7 @@ pub async fn remove_file_or_folder(
} }
async fn valid_path(config: &PlayoutConfig, path: &str) -> Result<PathBuf, ServiceError> { async fn valid_path(config: &PlayoutConfig, path: &str) -> Result<PathBuf, ServiceError> {
let (test_path, _, _) = norm_abs_path(&config.global.storage_path, path)?; let (test_path, _, _) = norm_abs_path(&config.channel.storage_path, path)?;
if !test_path.is_dir() { if !test_path.is_dir() {
return Err(ServiceError::BadRequest("Target folder not exists!".into())); return Err(ServiceError::BadRequest("Target folder not exists!".into()));

View File

@ -215,7 +215,7 @@ pub fn playlist_generator(manager: &ChannelManager) -> Result<Vec<JsonPlaylist>,
} }
} }
}; };
let playlist_root = &config.global.playlist_path; let playlist_root = &config.channel.playlist_path;
let mut playlists = vec![]; let mut playlists = vec![];
let mut date_range = vec![]; let mut date_range = vec![];
let mut from_template = false; let mut from_template = false;
@ -224,7 +224,7 @@ pub fn playlist_generator(manager: &ChannelManager) -> Result<Vec<JsonPlaylist>,
error!( error!(
target: Target::all(), channel = id; target: Target::all(), channel = id;
"Playlist folder <b><magenta>{:?}</></b> not exists!", "Playlist folder <b><magenta>{:?}</></b> not exists!",
config.global.playlist_path config.channel.playlist_path
); );
} }

View File

@ -14,7 +14,7 @@ pub async fn read_playlist(
date: String, date: String,
) -> Result<JsonPlaylist, ServiceError> { ) -> Result<JsonPlaylist, ServiceError> {
let d: Vec<&str> = date.split('-').collect(); let d: Vec<&str> = date.split('-').collect();
let mut playlist_path = config.global.playlist_path.clone(); let mut playlist_path = config.channel.playlist_path.clone();
playlist_path = playlist_path playlist_path = playlist_path
.join(d[0]) .join(d[0])
@ -34,7 +34,7 @@ pub async fn write_playlist(
) -> Result<String, ServiceError> { ) -> Result<String, ServiceError> {
let date = json_data.date.clone(); let date = json_data.date.clone();
let d: Vec<&str> = date.split('-').collect(); let d: Vec<&str> = date.split('-').collect();
let mut playlist_path = config.global.playlist_path.clone(); let mut playlist_path = config.channel.playlist_path.clone();
if !playlist_path if !playlist_path
.extension() .extension()
@ -93,7 +93,7 @@ pub fn generate_playlist(manager: ChannelManager) -> Result<JsonPlaylist, Servic
for path in &source.paths { for path in &source.paths {
let (safe_path, _, _) = let (safe_path, _, _) =
norm_abs_path(&config.global.storage_path, &path.to_string_lossy())?; norm_abs_path(&config.channel.storage_path, &path.to_string_lossy())?;
paths.push(safe_path); paths.push(safe_path);
} }
@ -124,7 +124,7 @@ pub fn generate_playlist(manager: ChannelManager) -> Result<JsonPlaylist, Servic
pub async fn delete_playlist(config: &PlayoutConfig, date: &str) -> Result<String, ServiceError> { pub async fn delete_playlist(config: &PlayoutConfig, date: &str) -> Result<String, ServiceError> {
let d: Vec<&str> = date.split('-').collect(); let d: Vec<&str> = date.split('-').collect();
let mut playlist_path = PathBuf::from(&config.global.playlist_path); let mut playlist_path = PathBuf::from(&config.channel.playlist_path);
playlist_path = playlist_path playlist_path = playlist_path
.join(d[0]) .join(d[0])

View File

@ -118,7 +118,7 @@ pub fn stat(config: PlayoutConfig) -> SystemStat {
for disk in &*disks { for disk in &*disks {
if disk.mount_point().to_string_lossy().len() > 1 if disk.mount_point().to_string_lossy().len() > 1
&& config.global.storage_path.starts_with(disk.mount_point()) && config.channel.storage_path.starts_with(disk.mount_point())
{ {
storage.path = disk.name().to_string_lossy().to_string(); storage.path = disk.name().to_string_lossy().to_string();
storage.total = disk.total_space(); storage.total = disk.total_space();

@ -1 +1 @@
Subproject commit 2045e47ab5e9f42ff73e03a7c4c604821a73f7ca Subproject commit e183320e13bd972d632b841bde722fa7bbed70e5

View File

@ -5,11 +5,11 @@ CREATE TABLE
global ( global (
id INTEGER PRIMARY KEY, id INTEGER PRIMARY KEY,
secret TEXT NOT NULL, secret TEXT NOT NULL,
hls_path TEXT NOT NULL DEFAULT "/usr/share/ffplayout/public",
logging_path TEXT NOT NULL DEFAULT "/var/log/ffplayout", logging_path TEXT NOT NULL DEFAULT "/var/log/ffplayout",
playlist_path TEXT NOT NULL DEFAULT "/var/lib/ffplayout/playlists", playlist_root TEXT NOT NULL DEFAULT "/var/lib/ffplayout/playlists",
storage_path TEXT NOT NULL DEFAULT "/var/lib/ffplayout/tv-media", public_root TEXT NOT NULL DEFAULT "/usr/share/ffplayout/public",
shared_storage INTEGER NOT NULL DEFAULT 1, storage_root TEXT NOT NULL DEFAULT "/var/lib/ffplayout/tv-media",
shared_storage INTEGER NOT NULL DEFAULT 0,
UNIQUE (secret) UNIQUE (secret)
); );
@ -27,6 +27,9 @@ CREATE TABLE
preview_url TEXT NOT NULL, preview_url TEXT NOT NULL,
extra_extensions TEXT NOT NULL DEFAULT 'jpg,jpeg,png', extra_extensions TEXT NOT NULL DEFAULT 'jpg,jpeg,png',
active INTEGER NOT NULL DEFAULT 0, active INTEGER NOT NULL DEFAULT 0,
hls_path TEXT NOT NULL DEFAULT "/usr/share/ffplayout/public",
playlist_path TEXT NOT NULL DEFAULT "/var/lib/ffplayout/playlists",
storage_path TEXT NOT NULL DEFAULT "/var/lib/ffplayout/tv-media",
last_date TEXT, last_date TEXT,
time_shift REAL NOT NULL DEFAULT 0 time_shift REAL NOT NULL DEFAULT 0
); );

View File

@ -22,8 +22,8 @@ async fn prepare_config() -> (PlayoutConfig, ChannelManager) {
sqlx::query( sqlx::query(
r#" r#"
UPDATE global SET hls_path = "assets/hls", logging_path = "assets/log", UPDATE global SET public_root = "assets/hls", logging_path = "assets/log", playlist_root = "assets/playlists", storage_root = "assets/storage";
playlist_path = "assets/playlists", storage_path = "assets/storage"; UPDATE channels SET hls_path = "assets/hls", playlist_path = "assets/playlists", storage_path = "assets/storage";
UPDATE configurations SET processing_width = 1024, processing_height = 576; UPDATE configurations SET processing_width = 1024, processing_height = 576;
"#, "#,
) )

View File

@ -25,8 +25,8 @@ async fn prepare_config() -> (PlayoutConfig, ChannelManager) {
sqlx::query( sqlx::query(
r#" r#"
UPDATE global SET hls_path = "assets/hls", logging_path = "assets/log", UPDATE global SET public_root = "assets/hls", logging_path = "assets/log", playlist_root = "assets/playlists", storage_root = "assets/storage";
playlist_path = "assets/playlists", storage_path = "assets/storage"; UPDATE channels SET hls_path = "assets/hls", playlist_path = "assets/playlists", storage_path = "assets/storage";
UPDATE configurations SET processing_width = 1024, processing_height = 576; UPDATE configurations SET processing_width = 1024, processing_height = 576;
"#, "#,
) )
@ -104,7 +104,7 @@ fn test_generate_playlist_from_folder() {
config.processing.mode = Playlist; config.processing.mode = Playlist;
config.storage.filler = "assets/".into(); config.storage.filler = "assets/".into();
config.playlist.length_sec = Some(86400.0); config.playlist.length_sec = Some(86400.0);
config.global.playlist_path = "assets/playlists".into(); config.channel.playlist_path = "assets/playlists".into();
let playlist = generate_playlist(manager); let playlist = generate_playlist(manager);
@ -149,7 +149,7 @@ fn test_generate_playlist_from_template() {
config.processing.mode = Playlist; config.processing.mode = Playlist;
config.storage.filler = "assets/".into(); config.storage.filler = "assets/".into();
config.playlist.length_sec = Some(86400.0); config.playlist.length_sec = Some(86400.0);
config.global.playlist_path = "assets/playlists".into(); config.channel.playlist_path = "assets/playlists".into();
let playlist = generate_playlist(manager); let playlist = generate_playlist(manager);

View File

@ -24,8 +24,8 @@ async fn prepare_config() -> (PlayoutConfig, ChannelManager) {
sqlx::query( sqlx::query(
r#" r#"
UPDATE global SET hls_path = "assets/hls", logging_path = "assets/log", UPDATE global SET public_root = "assets/hls", logging_path = "assets/log", playlist_root = "assets/playlists", storage_root = "assets/storage";
playlist_path = "assets/playlists", storage_path = "assets/storage"; UPDATE channels SET hls_path = "assets/hls", playlist_path = "assets/playlists", storage_path = "assets/storage";
UPDATE configurations SET processing_width = 1024, processing_height = 576; UPDATE configurations SET processing_width = 1024, processing_height = 576;
"#, "#,
) )
@ -66,7 +66,7 @@ fn test_gen_source() {
config.playlist.start_sec = Some(0.0); config.playlist.start_sec = Some(0.0);
config.playlist.length = "24:00:00".into(); config.playlist.length = "24:00:00".into();
config.playlist.length_sec = Some(86400.0); config.playlist.length_sec = Some(86400.0);
config.global.playlist_path = "assets/playlists".into(); config.channel.playlist_path = "assets/playlists".into();
config.storage.filler = "assets/media_filler/filler_0.mp4".into(); config.storage.filler = "assets/media_filler/filler_0.mp4".into();
let mut valid_source_with_probe = Media::new(0, "assets/media_mix/av_sync.mp4", true); let mut valid_source_with_probe = Media::new(0, "assets/media_mix/av_sync.mp4", true);
@ -113,7 +113,7 @@ fn playlist_missing() {
config.playlist.start_sec = Some(0.0); config.playlist.start_sec = Some(0.0);
config.playlist.length = "24:00:00".into(); config.playlist.length = "24:00:00".into();
config.playlist.length_sec = Some(86400.0); config.playlist.length_sec = Some(86400.0);
config.global.playlist_path = "assets/playlists".into(); config.channel.playlist_path = "assets/playlists".into();
config.storage.filler = "assets/media_filler/filler_0.mp4".into(); config.storage.filler = "assets/media_filler/filler_0.mp4".into();
config.output.mode = Null; config.output.mode = Null;
config.output.output_count = 1; config.output.output_count = 1;
@ -147,7 +147,7 @@ fn playlist_next_missing() {
config.playlist.start_sec = Some(0.0); config.playlist.start_sec = Some(0.0);
config.playlist.length = "24:00:00".into(); config.playlist.length = "24:00:00".into();
config.playlist.length_sec = Some(86400.0); config.playlist.length_sec = Some(86400.0);
config.global.playlist_path = "assets/playlists".into(); config.channel.playlist_path = "assets/playlists".into();
config.storage.filler = "assets/media_filler/filler_0.mp4".into(); config.storage.filler = "assets/media_filler/filler_0.mp4".into();
config.output.mode = Null; config.output.mode = Null;
config.output.output_count = 1; config.output.output_count = 1;
@ -181,7 +181,7 @@ fn playlist_to_short() {
config.playlist.start_sec = Some(21600.0); config.playlist.start_sec = Some(21600.0);
config.playlist.length = "24:00:00".into(); config.playlist.length = "24:00:00".into();
config.playlist.length_sec = Some(86400.0); config.playlist.length_sec = Some(86400.0);
config.global.playlist_path = "assets/playlists".into(); config.channel.playlist_path = "assets/playlists".into();
config.storage.filler = "assets/media_filler/filler_0.mp4".into(); config.storage.filler = "assets/media_filler/filler_0.mp4".into();
config.output.mode = Null; config.output.mode = Null;
config.output.output_count = 1; config.output.output_count = 1;
@ -215,7 +215,7 @@ fn playlist_init_after_list_end() {
config.playlist.start_sec = Some(21600.0); config.playlist.start_sec = Some(21600.0);
config.playlist.length = "24:00:00".into(); config.playlist.length = "24:00:00".into();
config.playlist.length_sec = Some(86400.0); config.playlist.length_sec = Some(86400.0);
config.global.playlist_path = "assets/playlists".into(); config.channel.playlist_path = "assets/playlists".into();
config.storage.filler = "assets/media_filler/filler_0.mp4".into(); config.storage.filler = "assets/media_filler/filler_0.mp4".into();
config.output.mode = Null; config.output.mode = Null;
config.output.output_count = 1; config.output.output_count = 1;
@ -249,7 +249,7 @@ fn playlist_change_at_midnight() {
config.playlist.start_sec = Some(0.0); config.playlist.start_sec = Some(0.0);
config.playlist.length = "24:00:00".into(); config.playlist.length = "24:00:00".into();
config.playlist.length_sec = Some(86400.0); config.playlist.length_sec = Some(86400.0);
config.global.playlist_path = "assets/playlists".into(); config.channel.playlist_path = "assets/playlists".into();
config.storage.filler = "assets/media_filler/filler_0.mp4".into(); config.storage.filler = "assets/media_filler/filler_0.mp4".into();
config.output.mode = Null; config.output.mode = Null;
config.output.output_count = 1; config.output.output_count = 1;
@ -283,7 +283,7 @@ fn playlist_change_before_midnight() {
config.playlist.start_sec = Some(0.0); config.playlist.start_sec = Some(0.0);
config.playlist.length = "24:00:00".into(); config.playlist.length = "24:00:00".into();
config.playlist.length_sec = Some(86400.0); config.playlist.length_sec = Some(86400.0);
config.global.playlist_path = "assets/playlists".into(); config.channel.playlist_path = "assets/playlists".into();
config.storage.filler = "assets/media_filler/filler_0.mp4".into(); config.storage.filler = "assets/media_filler/filler_0.mp4".into();
config.output.mode = Null; config.output.mode = Null;
config.output.output_count = 1; config.output.output_count = 1;
@ -317,7 +317,7 @@ fn playlist_change_at_six() {
config.playlist.start_sec = Some(21600.0); config.playlist.start_sec = Some(21600.0);
config.playlist.length = "24:00:00".into(); config.playlist.length = "24:00:00".into();
config.playlist.length_sec = Some(86400.0); config.playlist.length_sec = Some(86400.0);
config.global.playlist_path = "assets/playlists".into(); config.channel.playlist_path = "assets/playlists".into();
config.storage.filler = "assets/media_filler/filler_0.mp4".into(); config.storage.filler = "assets/media_filler/filler_0.mp4".into();
config.output.mode = Null; config.output.mode = Null;
config.output.output_count = 1; config.output.output_count = 1;

View File

@ -18,8 +18,8 @@ async fn prepare_config() -> (PlayoutConfig, ChannelManager) {
sqlx::query( sqlx::query(
r#" r#"
UPDATE global SET hls_path = "assets/hls", logging_path = "assets/log", UPDATE global SET public_root = "assets/hls", logging_path = "assets/log", playlist_root = "assets/playlists", storage_root = "assets/storage";
playlist_path = "assets/playlists", storage_path = "assets/storage"; UPDATE channels SET hls_path = "assets/hls", playlist_path = "assets/playlists", storage_path = "assets/storage";
UPDATE configurations SET processing_width = 1024, processing_height = 576; UPDATE configurations SET processing_width = 1024, processing_height = 576;
"#, "#,
) )