Merge pull request #389 from jb-alvarado/master
This commit is contained in:
commit
887dabff8c
1
.gitignore
vendored
1
.gitignore
vendored
@ -26,3 +26,4 @@ ffpapi.1.gz
|
|||||||
/public/
|
/public/
|
||||||
tmp/
|
tmp/
|
||||||
.vscode/
|
.vscode/
|
||||||
|
assets/playlist_template.json
|
||||||
|
826
Cargo.lock
generated
826
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,7 @@
|
|||||||
[workspace]
|
[workspace]
|
||||||
members = ["ffplayout-api", "ffplayout-engine", "lib", "tests"]
|
members = ["ffplayout-api", "ffplayout-engine", "lib", "tests"]
|
||||||
default-members = ["ffplayout-api", "ffplayout-engine", "tests"]
|
default-members = ["ffplayout-api", "ffplayout-engine", "tests"]
|
||||||
|
resolver = "2"
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "0.20.0"
|
version = "0.20.0"
|
||||||
|
17
README.md
17
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**
|
**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
|
- import playlist from text or m3u file, with CLI or frontend
|
||||||
- audio only, for radio mode (experimental *)
|
- audio only, for radio mode (experimental *)
|
||||||
- [Piggyback Mode](/ffplayout-api/README.md#piggyback-mode), mostly for non Linux systems (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)
|
For preview stream, read: [/docs/preview_stream.md](/docs/preview_stream.md)
|
||||||
|
|
||||||
@ -176,14 +177,12 @@ Output from `{"media":"current"}` show:
|
|||||||
|
|
||||||
```JSON
|
```JSON
|
||||||
{
|
{
|
||||||
"jsonrpc": "2.0",
|
|
||||||
"result": {
|
|
||||||
"current_media": {
|
"current_media": {
|
||||||
"category": "",
|
"category": "",
|
||||||
"duration": 154.2,
|
"duration": 154.2,
|
||||||
"out": 154.2,
|
"out": 154.2,
|
||||||
"seek": 0.0,
|
"seek": 0.0,
|
||||||
"source": "/opt/tv-media/clip.mp4"
|
"source": "/opt/tv-media/clip.mp4"
|
||||||
},
|
},
|
||||||
"index": 39,
|
"index": 39,
|
||||||
"play_mode": "playlist",
|
"play_mode": "playlist",
|
||||||
@ -191,8 +190,6 @@ Output from `{"media":"current"}` show:
|
|||||||
"remaining_sec": 86.39228000699876,
|
"remaining_sec": 86.39228000699876,
|
||||||
"start_sec": 24713.631999999998,
|
"start_sec": 24713.631999999998,
|
||||||
"start_time": "06:51:53.631"
|
"start_time": "06:51:53.631"
|
||||||
},
|
|
||||||
"id": 1
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -21,6 +21,10 @@ Using live ingest to inject a live stream.
|
|||||||
|
|
||||||
The different output modes.
|
The different output modes.
|
||||||
|
|
||||||
|
### **[Playlist Generation](/docs/playlist_gen.md)**
|
||||||
|
|
||||||
|
Generate playlists based on template.
|
||||||
|
|
||||||
### **[Multi Audio Tracks](/docs/multi_audio.md)**
|
### **[Multi Audio Tracks](/docs/multi_audio.md)**
|
||||||
|
|
||||||
Output multiple audio tracks.
|
Output multiple audio tracks.
|
||||||
|
71
docs/playlist_gen.md
Normal file
71
docs/playlist_gen.md
Normal file
@ -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"]}]}}'
|
||||||
|
```
|
||||||
|
|
@ -11,11 +11,11 @@ edition.workspace = true
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
ffplayout-lib = { path = "../lib" }
|
ffplayout-lib = { path = "../lib" }
|
||||||
actix-files = "0.6"
|
actix-files = "0.6"
|
||||||
actix-multipart = "0.5"
|
actix-multipart = "0.6"
|
||||||
actix-web = "4"
|
actix-web = "4"
|
||||||
actix-web-grants = "3"
|
actix-web-grants = "3"
|
||||||
actix-web-httpauth = "0.6"
|
actix-web-httpauth = "0.8"
|
||||||
argon2 = "0.4"
|
argon2 = "0.5"
|
||||||
chrono = { version = "0.4", default-features = false, features = ["clock", "std"] }
|
chrono = { version = "0.4", default-features = false, features = ["clock", "std"] }
|
||||||
clap = { version = "4.3", features = ["derive"] }
|
clap = { version = "4.3", features = ["derive"] }
|
||||||
derive_more = "0.99"
|
derive_more = "0.99"
|
||||||
@ -23,19 +23,19 @@ faccess = "0.2"
|
|||||||
futures-util = { version = "0.3", default-features = false, features = ["std"] }
|
futures-util = { version = "0.3", default-features = false, features = ["std"] }
|
||||||
jsonwebtoken = "8"
|
jsonwebtoken = "8"
|
||||||
lexical-sort = "0.3"
|
lexical-sort = "0.3"
|
||||||
once_cell = "1.10"
|
once_cell = "1.18"
|
||||||
rand = "0.8"
|
rand = "0.8"
|
||||||
regex = "1"
|
regex = "1"
|
||||||
relative-path = "1.6"
|
relative-path = "1.8"
|
||||||
reqwest = { version = "0.11", features = ["blocking", "json"] }
|
reqwest = { version = "0.11", features = ["blocking", "json"] }
|
||||||
rpassword = "6.0"
|
rpassword = "7.2"
|
||||||
sanitize-filename = "0.3"
|
sanitize-filename = "0.5"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
serde_yaml = "0.9"
|
serde_yaml = "0.9"
|
||||||
simplelog = { version = "^0.12", features = ["paris"] }
|
simplelog = { version = "0.12", features = ["paris"] }
|
||||||
sqlx = { version = "0.7", features = ["runtime-tokio", "sqlite"] }
|
sqlx = { version = "0.7", features = ["runtime-tokio", "sqlite"] }
|
||||||
tokio = { version = "1.25", features = ["full"] }
|
tokio = { version = "1.29", features = ["full"] }
|
||||||
|
|
||||||
[[bin]]
|
[[bin]]
|
||||||
name = "ffpapi"
|
name = "ffpapi"
|
||||||
|
@ -8,7 +8,7 @@
|
|||||||
///
|
///
|
||||||
/// For all endpoints an (Bearer) authentication is required.\
|
/// For all endpoints an (Bearer) authentication is required.\
|
||||||
/// `{id}` represent the channel id, and at default is 1.
|
/// `{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_multipart::Multipart;
|
||||||
use actix_web::{delete, get, http::StatusCode, patch, post, put, web, HttpResponse, Responder};
|
use actix_web::{delete, get, http::StatusCode, patch, post, put, web, HttpResponse, Responder};
|
||||||
@ -46,6 +46,7 @@ use crate::{
|
|||||||
use ffplayout_lib::{
|
use ffplayout_lib::{
|
||||||
utils::{
|
utils::{
|
||||||
get_date_range, import::import_file, sec_to_time, time_to_sec, JsonPlaylist, PlayoutConfig,
|
get_date_range, import::import_file, sec_to_time, time_to_sec, JsonPlaylist, PlayoutConfig,
|
||||||
|
Template,
|
||||||
},
|
},
|
||||||
vec_strings,
|
vec_strings,
|
||||||
};
|
};
|
||||||
@ -72,19 +73,20 @@ pub struct DateObj {
|
|||||||
#[derive(Debug, Deserialize, Serialize)]
|
#[derive(Debug, Deserialize, Serialize)]
|
||||||
struct FileObj {
|
struct FileObj {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
path: String,
|
path: PathBuf,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default, Deserialize, Serialize)]
|
#[derive(Debug, Default, Deserialize, Serialize)]
|
||||||
pub struct PathsObj {
|
pub struct PathsObj {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
paths: Vec<String>,
|
paths: Vec<String>,
|
||||||
|
template: Option<Template>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Serialize)]
|
#[derive(Debug, Deserialize, Serialize)]
|
||||||
pub struct ImportObj {
|
pub struct ImportObj {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
file: String,
|
file: PathBuf,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
date: String,
|
date: String,
|
||||||
}
|
}
|
||||||
@ -724,7 +726,7 @@ pub async fn get_playlist(
|
|||||||
/// ```BASH
|
/// ```BASH
|
||||||
/// curl -X POST http://127.0.0.1:8787/api/playlist/1/
|
/// curl -X POST http://127.0.0.1:8787/api/playlist/1/
|
||||||
/// -H 'Content-Type: application/json' -H 'Authorization: Bearer <TOKEN>'
|
/// -H 'Content-Type: application/json' -H 'Authorization: Bearer <TOKEN>'
|
||||||
/// -- data "{<JSON playlist data>}"
|
/// --data "{<JSON playlist data>}"
|
||||||
/// ```
|
/// ```
|
||||||
#[post("/playlist/{id}/")]
|
#[post("/playlist/{id}/")]
|
||||||
#[has_any_role("Role::Admin", "Role::User", type = "Role")]
|
#[has_any_role("Role::Admin", "Role::User", type = "Role")]
|
||||||
@ -746,7 +748,7 @@ pub async fn save_playlist(
|
|||||||
/// ```BASH
|
/// ```BASH
|
||||||
/// curl -X POST http://127.0.0.1:8787/api/playlist/1/generate/2022-06-20
|
/// 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>'
|
/// -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}")]
|
#[post("/playlist/{id}/generate/{date}")]
|
||||||
#[has_any_role("Role::Admin", "Role::User", type = "Role")]
|
#[has_any_role("Role::Admin", "Role::User", type = "Role")]
|
||||||
@ -764,10 +766,11 @@ pub async fn gen_playlist(
|
|||||||
for path in &obj.paths {
|
for path in &obj.paths {
|
||||||
let (p, _, _) = norm_abs_path(&config.storage.path, path);
|
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.storage.paths = path_list;
|
||||||
|
config.general.template = obj.template.clone();
|
||||||
}
|
}
|
||||||
|
|
||||||
match generate_playlist(config, channel.name).await {
|
match generate_playlist(config, channel.name).await {
|
||||||
@ -921,8 +924,8 @@ async fn import_playlist(
|
|||||||
payload: Multipart,
|
payload: Multipart,
|
||||||
obj: web::Query<ImportObj>,
|
obj: web::Query<ImportObj>,
|
||||||
) -> Result<HttpResponse, ServiceError> {
|
) -> Result<HttpResponse, ServiceError> {
|
||||||
let file = Path::new(&obj.file).file_name().unwrap_or_default();
|
let file = obj.file.file_name().unwrap_or_default();
|
||||||
let path = env::temp_dir().join(file).to_string_lossy().to_string();
|
let path = env::temp_dir().join(file);
|
||||||
let (config, _) = playout_config(&pool.clone().into_inner(), &id).await?;
|
let (config, _) = playout_config(&pool.clone().into_inner(), &id).await?;
|
||||||
let channel = handles::select_channel(&pool.clone().into_inner(), &id).await?;
|
let channel = handles::select_channel(&pool.clone().into_inner(), &id).await?;
|
||||||
|
|
||||||
|
@ -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>> {
|
pub async fn db_init(domain: Option<String>) -> Result<&'static str, Box<dyn std::error::Error>> {
|
||||||
let db_path = db_path()?;
|
let db_path = db_path()?;
|
||||||
|
|
||||||
if !Sqlite::database_exists(&db_path).await.unwrap_or(false) {
|
if !Sqlite::database_exists(db_path).await.unwrap_or(false) {
|
||||||
Sqlite::create_database(&db_path).await.unwrap();
|
Sqlite::create_database(db_path).await.unwrap();
|
||||||
|
|
||||||
let pool = db_pool().await?;
|
let pool = db_pool().await?;
|
||||||
|
|
||||||
|
@ -7,7 +7,7 @@ use crate::utils::db_path;
|
|||||||
|
|
||||||
pub async fn db_pool() -> Result<Pool<Sqlite>, sqlx::Error> {
|
pub async fn db_pool() -> Result<Pool<Sqlite>, sqlx::Error> {
|
||||||
let db_path = db_path().unwrap();
|
let db_path = db_path().unwrap();
|
||||||
let conn = SqlitePool::connect(&db_path).await?;
|
let conn = SqlitePool::connect(db_path).await?;
|
||||||
|
|
||||||
Ok(conn)
|
Ok(conn)
|
||||||
}
|
}
|
||||||
|
@ -3,8 +3,7 @@ use std::{path::Path, process::exit};
|
|||||||
use actix_files::Files;
|
use actix_files::Files;
|
||||||
use actix_web::{dev::ServiceRequest, middleware, web, App, Error, HttpMessage, HttpServer};
|
use actix_web::{dev::ServiceRequest, middleware, web, App, Error, HttpMessage, HttpServer};
|
||||||
use actix_web_grants::permissions::AttachPermissions;
|
use actix_web_grants::permissions::AttachPermissions;
|
||||||
use actix_web_httpauth::extractors::bearer::BearerAuth;
|
use actix_web_httpauth::{extractors::bearer::BearerAuth, middleware::HttpAuthentication};
|
||||||
use actix_web_httpauth::middleware::HttpAuthentication;
|
|
||||||
|
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use simplelog::*;
|
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};
|
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
|
// We just get permissions from JWT
|
||||||
let claims = auth::decode_jwt(credentials.token()).await?;
|
match auth::decode_jwt(credentials.token()).await {
|
||||||
req.attach(vec![Role::set_role(&claims.role)]);
|
Ok(claims) => {
|
||||||
|
req.attach(vec![Role::set_role(&claims.role)]);
|
||||||
|
|
||||||
req.extensions_mut()
|
req.extensions_mut()
|
||||||
.insert(LoginUser::new(claims.id, claims.username));
|
.insert(LoginUser::new(claims.id, claims.username));
|
||||||
|
|
||||||
Ok(req)
|
Ok(req)
|
||||||
|
}
|
||||||
|
Err(e) => Err((e, req)),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn public_path() -> &'static str {
|
fn public_path() -> &'static str {
|
||||||
@ -49,7 +55,7 @@ fn public_path() -> &'static str {
|
|||||||
return "./public/";
|
return "./public/";
|
||||||
}
|
}
|
||||||
|
|
||||||
"./ffplayout-frontend/dist"
|
"./ffplayout-frontend/.output/public/"
|
||||||
}
|
}
|
||||||
|
|
||||||
#[actix_web::main]
|
#[actix_web::main]
|
||||||
@ -77,11 +83,9 @@ async fn main() -> std::io::Result<()> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if let Some(conn) = args.listen {
|
if let Some(conn) = args.listen {
|
||||||
if let Ok(p) = db_path() {
|
if db_path().is_err() {
|
||||||
if !Path::new(&p).is_file() {
|
error!("Database is not initialized! Init DB first and add admin user.");
|
||||||
error!("Database is not initialized! Init DB first and add admin user.");
|
exit(1);
|
||||||
exit(1);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
init_config(&pool).await;
|
init_config(&pool).await;
|
||||||
let ip_port = conn.split(':').collect::<Vec<&str>>();
|
let ip_port = conn.split(':').collect::<Vec<&str>>();
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
use std::{fs, path::Path};
|
use std::{fs, path::PathBuf};
|
||||||
|
|
||||||
use rand::prelude::*;
|
use rand::prelude::*;
|
||||||
use simplelog::*;
|
use simplelog::*;
|
||||||
@ -31,22 +31,14 @@ pub async fn create_channel(
|
|||||||
Err(_) => rand::thread_rng().gen_range(71..99),
|
Err(_) => rand::thread_rng().gen_range(71..99),
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut config =
|
let mut config = PlayoutConfig::new(Some(PathBuf::from(
|
||||||
PlayoutConfig::new(Some("/usr/share/ffplayout/ffplayout.yml.orig".to_string()));
|
"/usr/share/ffplayout/ffplayout.yml.orig",
|
||||||
|
)));
|
||||||
|
|
||||||
config.general.stat_file = format!(".ffp_{channel_name}",);
|
config.general.stat_file = format!(".ffp_{channel_name}",);
|
||||||
|
config.logging.path = config.logging.path.join(&channel_name);
|
||||||
config.logging.path = Path::new(&config.logging.path)
|
|
||||||
.join(&channel_name)
|
|
||||||
.to_string_lossy()
|
|
||||||
.to_string();
|
|
||||||
|
|
||||||
config.rpc_server.address = format!("127.0.0.1:70{:7>2}", channel_num);
|
config.rpc_server.address = format!("127.0.0.1:70{:7>2}", channel_num);
|
||||||
|
config.playlist.path = config.playlist.path.join(channel_name);
|
||||||
config.playlist.path = Path::new(&config.playlist.path)
|
|
||||||
.join(channel_name)
|
|
||||||
.to_string_lossy()
|
|
||||||
.to_string();
|
|
||||||
|
|
||||||
config.out.output_param = config
|
config.out.output_param = config
|
||||||
.out
|
.out
|
||||||
|
@ -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_multipart::Multipart;
|
||||||
use actix_web::{web, HttpResponse};
|
use actix_web::{web, HttpResponse};
|
||||||
@ -51,10 +55,9 @@ pub struct VideoFile {
|
|||||||
/// Normalize absolut path
|
/// Normalize absolut path
|
||||||
///
|
///
|
||||||
/// This function takes care, that it is not possible to break out from root_path.
|
/// This function takes care, that it is not possible to break out from root_path.
|
||||||
/// It also gives alway a relative path back.
|
/// It also gives always a relative path back.
|
||||||
pub fn norm_abs_path(root_path: &str, input_path: &str) -> (PathBuf, String, String) {
|
pub fn norm_abs_path(root_path: &Path, input_path: &str) -> (PathBuf, String, String) {
|
||||||
let mut path = PathBuf::from(root_path);
|
let path_relative = RelativePath::new(&root_path.to_string_lossy())
|
||||||
let path_relative = RelativePath::new(root_path)
|
|
||||||
.normalize()
|
.normalize()
|
||||||
.to_string()
|
.to_string()
|
||||||
.replace("../", "");
|
.replace("../", "");
|
||||||
@ -62,13 +65,15 @@ pub fn norm_abs_path(root_path: &str, input_path: &str) -> (PathBuf, String, Str
|
|||||||
.normalize()
|
.normalize()
|
||||||
.to_string()
|
.to_string()
|
||||||
.replace("../", "");
|
.replace("../", "");
|
||||||
let path_suffix = path
|
let path_suffix = root_path
|
||||||
.file_name()
|
.file_name()
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
.to_string_lossy()
|
.to_string_lossy()
|
||||||
.to_string();
|
.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
|
source_relative = source_relative
|
||||||
.strip_prefix(&path_relative)
|
.strip_prefix(&path_relative)
|
||||||
.and_then(|s| s.strip_prefix('/'))
|
.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();
|
.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
|
/// File Browser
|
||||||
@ -300,7 +305,7 @@ pub async fn upload(
|
|||||||
conn: &Pool<Sqlite>,
|
conn: &Pool<Sqlite>,
|
||||||
id: i32,
|
id: i32,
|
||||||
mut payload: Multipart,
|
mut payload: Multipart,
|
||||||
path: &str,
|
path: &Path,
|
||||||
abs_path: bool,
|
abs_path: bool,
|
||||||
) -> Result<HttpResponse, ServiceError> {
|
) -> Result<HttpResponse, ServiceError> {
|
||||||
while let Some(mut field) = payload.try_next().await? {
|
while let Some(mut field) = payload.try_next().await? {
|
||||||
@ -318,9 +323,9 @@ pub async fn upload(
|
|||||||
let filepath;
|
let filepath;
|
||||||
|
|
||||||
if abs_path {
|
if abs_path {
|
||||||
filepath = PathBuf::from(path);
|
filepath = path.to_path_buf();
|
||||||
} else {
|
} 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);
|
filepath = target_path.join(filename);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -74,21 +74,25 @@ pub async fn init_config(conn: &Pool<Sqlite>) {
|
|||||||
INSTANCE.set(config).unwrap();
|
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 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() {
|
if sys_path.is_dir() && !sys_path.writable() {
|
||||||
error!("Path {} is not writable!", sys_path.display());
|
error!("Path {} is not writable!", sys_path.display());
|
||||||
}
|
}
|
||||||
|
|
||||||
if sys_path.is_dir() && sys_path.writable() {
|
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() {
|
} 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> {
|
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)?;
|
let mut config: PlayoutConfig = serde_yaml::from_reader(file)?;
|
||||||
|
|
||||||
config.playlist.start_sec = Some(time_to_sec(&config.playlist.day_start));
|
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)
|
Ok(config)
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,7 @@ use std::{fs, path::PathBuf};
|
|||||||
use simplelog::*;
|
use simplelog::*;
|
||||||
use sqlx::{Pool, Sqlite};
|
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::{
|
use ffplayout_lib::utils::{
|
||||||
generate_playlist as playlist_generator, json_reader, json_writer, JsonPlaylist, PlayoutConfig,
|
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(
|
pub async fn generate_playlist(
|
||||||
config: PlayoutConfig,
|
mut config: PlayoutConfig,
|
||||||
channel: String,
|
channel: String,
|
||||||
) -> Result<JsonPlaylist, ServiceError> {
|
) -> 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)) {
|
match playlist_generator(&config, Some(channel)) {
|
||||||
Ok(playlists) => {
|
Ok(playlists) => {
|
||||||
if !playlists.is_empty() {
|
if !playlists.is_empty() {
|
||||||
Ok(playlists[0].clone())
|
Ok(playlists[0].clone())
|
||||||
} else {
|
} else {
|
||||||
Err(ServiceError::Conflict(
|
Err(ServiceError::Conflict(
|
||||||
"Playlist could not be written, possible already exists!".into(),
|
"The playlist could not be written, maybe it already exists!".into(),
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,13 +16,15 @@ chrono = { version = "0.4", default-features = false, features = ["clock", "std"
|
|||||||
clap = { version = "4.3", features = ["derive"] }
|
clap = { version = "4.3", features = ["derive"] }
|
||||||
crossbeam-channel = "0.5"
|
crossbeam-channel = "0.5"
|
||||||
futures = "0.3"
|
futures = "0.3"
|
||||||
|
itertools = "0.11"
|
||||||
notify = "6.0"
|
notify = "6.0"
|
||||||
notify-debouncer-full = { version = "*", default-features = false }
|
notify-debouncer-full = { version = "*", default-features = false }
|
||||||
|
rand = "0.8"
|
||||||
regex = "1"
|
regex = "1"
|
||||||
reqwest = { version = "0.11", features = ["blocking", "json"] }
|
reqwest = { version = "0.11", features = ["blocking", "json"] }
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
simplelog = { version = "^0.12", features = ["paris"] }
|
simplelog = { version = "0.12", features = ["paris"] }
|
||||||
tiny_http = { version = "0.12", default-features = false }
|
tiny_http = { version = "0.12", default-features = false }
|
||||||
zeromq = { git = "https://github.com/zeromq/zmq.rs.git", default-features = false, features = [
|
zeromq = { git = "https://github.com/zeromq/zmq.rs.git", default-features = false, features = [
|
||||||
"async-std-runtime",
|
"async-std-runtime",
|
||||||
|
@ -28,8 +28,8 @@ pub fn source_generator(
|
|||||||
Folder => {
|
Folder => {
|
||||||
info!("Playout in folder mode");
|
info!("Playout in folder mode");
|
||||||
debug!(
|
debug!(
|
||||||
"Monitor folder: <b><magenta>{}</></b>",
|
"Monitor folder: <b><magenta>{:?}</></b>",
|
||||||
&config.storage.path
|
config.storage.path
|
||||||
);
|
);
|
||||||
|
|
||||||
let config_clone = config.clone();
|
let config_clone = config.clone();
|
||||||
|
@ -40,7 +40,7 @@ impl CurrentProgram {
|
|||||||
is_terminated: Arc<AtomicBool>,
|
is_terminated: Arc<AtomicBool>,
|
||||||
player_control: &PlayerControl,
|
player_control: &PlayerControl,
|
||||||
) -> Self {
|
) -> 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 {
|
if let Some(file) = &json.current_file {
|
||||||
info!("Read Playlist: <b><magenta>{}</></b>", file);
|
info!("Read Playlist: <b><magenta>{}</></b>", file);
|
||||||
@ -78,7 +78,7 @@ impl CurrentProgram {
|
|||||||
fn check_update(&mut self, seek: bool) {
|
fn check_update(&mut self, seek: bool) {
|
||||||
if self.json_path.is_none() {
|
if self.json_path.is_none() {
|
||||||
// If the playlist was missing, we check here to see if it came back.
|
// 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 {
|
if let Some(file) = &json.current_file {
|
||||||
info!("Read Playlist: <b><magenta>{file}</></b>");
|
info!("Read Playlist: <b><magenta>{file}</></b>");
|
||||||
@ -105,7 +105,7 @@ impl CurrentProgram {
|
|||||||
self.json_path.clone(),
|
self.json_path.clone(),
|
||||||
self.is_terminated.clone(),
|
self.is_terminated.clone(),
|
||||||
false,
|
false,
|
||||||
0.0,
|
false,
|
||||||
);
|
);
|
||||||
|
|
||||||
self.json_mod = json.modified;
|
self.json_mod = json.modified;
|
||||||
@ -133,17 +133,20 @@ impl CurrentProgram {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check if day is past and it is time for a new playlist.
|
// 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 current_time = get_sec();
|
||||||
let start_sec = self.config.playlist.start_sec.unwrap();
|
let start_sec = self.config.playlist.start_sec.unwrap();
|
||||||
let target_length = self.config.playlist.length_sec.unwrap();
|
let target_length = self.config.playlist.length_sec.unwrap();
|
||||||
let (delta, total_delta) = get_delta(&self.config, ¤t_time);
|
let (delta, total_delta) = get_delta(&self.config, ¤t_time);
|
||||||
let mut duration = self.current_node.out;
|
let mut duration = self.current_node.out;
|
||||||
|
let mut next = false;
|
||||||
|
|
||||||
if self.current_node.duration > self.current_node.out {
|
if self.current_node.duration > self.current_node.out {
|
||||||
duration = self.current_node.duration
|
duration = self.current_node.duration
|
||||||
}
|
}
|
||||||
|
|
||||||
|
trace!("delta: {delta}, total_delta: {total_delta}");
|
||||||
|
|
||||||
let mut next_start =
|
let mut next_start =
|
||||||
self.current_node.begin.unwrap_or_default() - start_sec + duration + delta;
|
self.current_node.begin.unwrap_or_default() - start_sec + duration + delta;
|
||||||
|
|
||||||
@ -153,21 +156,20 @@ impl CurrentProgram {
|
|||||||
next_start += self.config.general.stop_threshold;
|
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.
|
// 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
|
if next_start >= target_length
|
||||||
|| is_close(total_delta, 0.0, 2.0)
|
|| is_close(total_delta, 0.0, 2.0)
|
||||||
|| is_close(total_delta, target_length, 2.0)
|
|| is_close(total_delta, target_length, 2.0)
|
||||||
{
|
{
|
||||||
let json = read_json(
|
trace!("get next day");
|
||||||
&self.config,
|
next = true;
|
||||||
None,
|
|
||||||
self.is_terminated.clone(),
|
let json = read_json(&self.config, None, self.is_terminated.clone(), false, true);
|
||||||
false,
|
|
||||||
next_start,
|
|
||||||
);
|
|
||||||
|
|
||||||
if let Some(file) = &json.current_file {
|
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!({
|
let data = json!({
|
||||||
@ -192,8 +194,12 @@ impl CurrentProgram {
|
|||||||
|
|
||||||
if json.current_file.is_none() {
|
if json.current_file.is_none() {
|
||||||
self.playout_stat.list_init.store(true, Ordering::SeqCst);
|
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.
|
// Check if last and/or next clip is a advertisement.
|
||||||
@ -257,6 +263,7 @@ impl CurrentProgram {
|
|||||||
|
|
||||||
// Prepare init clip.
|
// Prepare init clip.
|
||||||
fn init_clip(&mut self) {
|
fn init_clip(&mut self) {
|
||||||
|
trace!("init_clip");
|
||||||
self.get_current_clip();
|
self.get_current_clip();
|
||||||
|
|
||||||
if !self.playout_stat.list_init.load(Ordering::SeqCst) {
|
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;
|
let new_length = new_node.begin.unwrap_or_default() + new_node.duration;
|
||||||
trace!("Init playlist after playlist end");
|
trace!("Init playlist after playlist end");
|
||||||
|
|
||||||
self.check_for_next_playlist();
|
let next_playlist = self.check_for_next_playlist();
|
||||||
|
|
||||||
if new_length
|
if new_length
|
||||||
>= self.config.playlist.length_sec.unwrap()
|
>= self.config.playlist.length_sec.unwrap()
|
||||||
+ self.config.playlist.start_sec.unwrap()
|
+ self.config.playlist.start_sec.unwrap()
|
||||||
{
|
{
|
||||||
self.init_clip();
|
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 {
|
} else {
|
||||||
// fill missing length from playlist
|
// fill missing length from playlist
|
||||||
let mut current_time = get_sec();
|
let mut current_time = get_sec();
|
||||||
let (_, total_delta) = get_delta(&self.config, ¤t_time);
|
let (_, total_delta) = get_delta(&self.config, ¤t_time);
|
||||||
let mut out = total_delta.abs();
|
|
||||||
let mut duration = out + 0.001;
|
|
||||||
|
|
||||||
trace!("Total delta on list init: {total_delta}");
|
trace!("Total delta on list init: {total_delta}");
|
||||||
|
|
||||||
let filler_index = self.player_control.filler_index.load(Ordering::SeqCst);
|
let out = if DUMMY_LEN > total_delta {
|
||||||
let mut filler =
|
total_delta
|
||||||
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;
|
|
||||||
} else {
|
} else {
|
||||||
duration = DUMMY_LEN;
|
DUMMY_LEN
|
||||||
out = DUMMY_LEN;
|
};
|
||||||
}
|
|
||||||
|
let duration = out + 0.001;
|
||||||
|
|
||||||
if self.json_path.is_some() {
|
if self.json_path.is_some() {
|
||||||
// When playlist is missing, we always need to init the playlist the next iteration.
|
// 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 {
|
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);
|
let index = self.player_control.current_index.load(Ordering::SeqCst);
|
||||||
self.current_node = Media::new(index, "", false);
|
self.current_node = Media::new(index, "", false);
|
||||||
self.current_node.begin = Some(get_sec());
|
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 out = if DUMMY_LEN > total_delta {
|
||||||
let mut filler =
|
total_delta
|
||||||
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;
|
|
||||||
} else {
|
} else {
|
||||||
duration = DUMMY_LEN;
|
DUMMY_LEN
|
||||||
out = DUMMY_LEN;
|
};
|
||||||
}
|
|
||||||
|
let duration = out + 0.001;
|
||||||
|
|
||||||
self.current_node.duration = duration;
|
self.current_node.duration = duration;
|
||||||
self.current_node.out = out;
|
self.current_node.out = out;
|
||||||
@ -605,7 +599,7 @@ pub fn gen_source(
|
|||||||
error!("Source not found: <b><magenta>\"{}\"</></b>", node.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() {
|
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);
|
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.cmd = Some(loop_filler(&node));
|
||||||
node.probe = filler_media.probe;
|
node.probe = filler_media.probe;
|
||||||
} else if filler_source.is_file() {
|
} 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
|
if config
|
||||||
.storage
|
.storage
|
||||||
.filler
|
.filler
|
||||||
|
.to_string_lossy()
|
||||||
|
.to_string()
|
||||||
.rsplit_once('.')
|
.rsplit_once('.')
|
||||||
.map(|(_, e)| e.to_lowercase())
|
.map(|(_, e)| e.to_lowercase())
|
||||||
.filter(|c| IMAGE_FORMAT.contains(&c.as_str()))
|
.filter(|c| IMAGE_FORMAT.contains(&c.as_str()))
|
||||||
.is_some()
|
.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.cmd = Some(loop_image(&node));
|
||||||
node.probe = Some(probe);
|
node.probe = Some(probe);
|
||||||
} else if let Some(filler_duration) = probe
|
} else if let Some(filler_duration) = probe
|
||||||
@ -649,7 +645,7 @@ pub fn gen_source(
|
|||||||
.and_then(|d| d.parse::<f64>().ok())
|
.and_then(|d| d.parse::<f64>().ok())
|
||||||
{
|
{
|
||||||
// Create placeholder from config filler.
|
// 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 {
|
node.out = if node.duration > duration && filler_duration > duration {
|
||||||
duration
|
duration
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
use std::{
|
use std::{
|
||||||
fs::{self, File},
|
fs::{self, File},
|
||||||
path::{Path, PathBuf},
|
path::PathBuf,
|
||||||
process::exit,
|
process::exit,
|
||||||
sync::{atomic::AtomicBool, Arc, Mutex},
|
sync::{atomic::AtomicBool, Arc, Mutex},
|
||||||
thread,
|
thread,
|
||||||
@ -107,7 +107,7 @@ fn main() {
|
|||||||
let messages = Arc::new(Mutex::new(Vec::new()));
|
let messages = Arc::new(Mutex::new(Vec::new()));
|
||||||
|
|
||||||
// try to create logging folder, if not exist
|
// 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) {
|
if let Err(e) = fs::create_dir_all(&config.logging.path) {
|
||||||
println!("Logging path not exists! {e}");
|
println!("Logging path not exists! {e}");
|
||||||
|
|
||||||
@ -163,11 +163,11 @@ fn main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if args.validate {
|
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 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();
|
let d: Vec<&str> = date.split('-').collect();
|
||||||
playlist_path = playlist_path
|
playlist_path = playlist_path
|
||||||
.join(d[0])
|
.join(d[0])
|
||||||
@ -213,7 +213,9 @@ fn main() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Fill filler list, can also be a single file.
|
// 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 {
|
match config.out.mode {
|
||||||
// write files/playlist to HLS m3u8 playlist
|
// write files/playlist to HLS m3u8 playlist
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
use std::{
|
use std::{
|
||||||
io::{prelude::*, BufReader, BufWriter, Read},
|
io::{prelude::*, BufReader, BufWriter, Read},
|
||||||
path::Path,
|
|
||||||
process::{Command, Stdio},
|
process::{Command, Stdio},
|
||||||
sync::atomic::Ordering,
|
sync::atomic::Ordering,
|
||||||
thread::{self, sleep},
|
thread::{self, sleep},
|
||||||
@ -101,11 +100,11 @@ pub fn player(
|
|||||||
let task_node = node.clone();
|
let task_node = node.clone();
|
||||||
let server_running = proc_control.server_is_running.load(Ordering::SeqCst);
|
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));
|
thread::spawn(move || task_runner::run(task_config, task_node, server_running));
|
||||||
} else {
|
} else {
|
||||||
error!(
|
error!(
|
||||||
"<bright-blue>{}</> executable not exists!",
|
"<bright-blue>{:?}</> executable not exists!",
|
||||||
config.task.path
|
config.task.path
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -159,15 +158,11 @@ pub fn player(
|
|||||||
thread::spawn(move || stderr_reader(dec_err, Decoder, dec_p_ctl));
|
thread::spawn(move || stderr_reader(dec_err, Decoder, dec_p_ctl));
|
||||||
|
|
||||||
loop {
|
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 proc_control.server_is_running.load(Ordering::SeqCst) {
|
||||||
if !live_on {
|
if !live_on {
|
||||||
info!("Switch from {} to live ingest", config.processing.mode);
|
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) {
|
if let Err(e) = proc_control.stop(Decoder) {
|
||||||
error!("{e}")
|
error!("{e}")
|
||||||
}
|
}
|
||||||
@ -188,11 +183,8 @@ pub fn player(
|
|||||||
if live_on {
|
if live_on {
|
||||||
info!("Switch from live ingest to {}", config.processing.mode);
|
info!("Switch from live ingest to {}", config.processing.mode);
|
||||||
|
|
||||||
if let Err(e) = enc_writer.flush() {
|
|
||||||
error!("Encoder error: {e}")
|
|
||||||
}
|
|
||||||
|
|
||||||
live_on = false;
|
live_on = false;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
let dec_bytes_len = match dec_reader.read(&mut buffer[..]) {
|
let dec_bytes_len = match dec_reader.read(&mut buffer[..]) {
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
|
|
||||||
use ffplayout_lib::utils::{OutputMode, ProcessMode};
|
use ffplayout_lib::utils::{OutputMode, ProcessMode};
|
||||||
@ -13,10 +15,24 @@ pub struct Args {
|
|||||||
pub channel: Option<String>,
|
pub channel: Option<String>,
|
||||||
|
|
||||||
#[clap(short, long, help = "File path to ffplayout.yml")]
|
#[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")]
|
#[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(
|
#[clap(
|
||||||
short,
|
short,
|
||||||
@ -27,37 +43,14 @@ pub struct Args {
|
|||||||
)]
|
)]
|
||||||
pub generate: Option<Vec<String>>,
|
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(
|
#[clap(
|
||||||
long,
|
long,
|
||||||
help = "Import a given text/m3u file and create a playlist from it"
|
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.")]
|
#[clap(short, long, help = "Loop playlist infinitely")]
|
||||||
pub playlist: Option<String>,
|
pub infinit: bool,
|
||||||
|
|
||||||
#[clap(
|
|
||||||
short,
|
|
||||||
long,
|
|
||||||
help = "Start time in 'hh:mm:ss', 'now' for start with first"
|
|
||||||
)]
|
|
||||||
pub start: Option<String>,
|
|
||||||
|
|
||||||
#[clap(
|
#[clap(
|
||||||
short = 't',
|
short = 't',
|
||||||
@ -69,8 +62,24 @@ pub struct Args {
|
|||||||
#[clap(long, help = "Override logging level")]
|
#[clap(long, help = "Override logging level")]
|
||||||
pub level: Option<String>,
|
pub level: Option<String>,
|
||||||
|
|
||||||
#[clap(short, long, help = "Loop playlist infinitely")]
|
#[clap(long, help = "Optional path list for playlist generations", num_args = 1..)]
|
||||||
pub infinit: bool,
|
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")]
|
#[clap(short, long, help = "Set output mode: desktop, hls, null, stream")]
|
||||||
pub output: Option<OutputMode>,
|
pub output: Option<OutputMode>,
|
||||||
@ -78,10 +87,6 @@ pub struct Args {
|
|||||||
#[clap(short, long, help = "Set audio volume")]
|
#[clap(short, long, help = "Set audio volume")]
|
||||||
pub volume: Option<f64>,
|
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")]
|
#[clap(long, help = "validate given playlist")]
|
||||||
pub validate: bool,
|
pub validate: bool,
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,4 @@
|
|||||||
use std::{
|
use std::{fs::File, path::PathBuf, process::exit};
|
||||||
path::{Path, PathBuf},
|
|
||||||
process::exit,
|
|
||||||
};
|
|
||||||
|
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use serde_json::{json, Map, Value};
|
use serde_json::{json, Map, Value};
|
||||||
@ -14,8 +11,8 @@ pub use arg_parse::Args;
|
|||||||
use ffplayout_lib::{
|
use ffplayout_lib::{
|
||||||
filter::Filters,
|
filter::Filters,
|
||||||
utils::{
|
utils::{
|
||||||
get_sec, parse_log_level_filter, sec_to_time, time_to_sec, Media, OutputMode::*,
|
config::Template, get_sec, parse_log_level_filter, sec_to_time, time_to_sec, Media,
|
||||||
PlayoutConfig, ProcessMode::*,
|
OutputMode::*, PlayoutConfig, ProcessMode::*,
|
||||||
},
|
},
|
||||||
vec_strings,
|
vec_strings,
|
||||||
};
|
};
|
||||||
@ -33,7 +30,7 @@ pub fn get_config(args: Args) -> PlayoutConfig {
|
|||||||
exit(1)
|
exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
Some(path.display().to_string())
|
Some(path)
|
||||||
}
|
}
|
||||||
None => args.config,
|
None => args.config,
|
||||||
};
|
};
|
||||||
@ -48,12 +45,33 @@ pub fn get_config(args: Args) -> PlayoutConfig {
|
|||||||
config.general.validate = true;
|
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 {
|
if let Some(paths) = args.paths {
|
||||||
config.storage.paths = paths;
|
config.storage.paths = paths;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(log_path) = args.log {
|
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.log_to_file = true;
|
||||||
}
|
}
|
||||||
config.logging.path = log_path;
|
config.logging.path = log_path;
|
||||||
|
@ -1 +1 @@
|
|||||||
Subproject commit 2f3234221a0aef8e70d9e2b5e9bbfb1fe51921fc
|
Subproject commit 2ca3aa73b2992f6997276f5c8fb906f9ee703bf2
|
@ -9,11 +9,12 @@ repository.workspace = true
|
|||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[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"
|
crossbeam-channel = "0.5"
|
||||||
ffprobe = "0.3"
|
ffprobe = "0.3"
|
||||||
file-rotate = "0.7.0"
|
file-rotate = "0.7"
|
||||||
lettre = "0.10"
|
lettre = "0.10"
|
||||||
|
lexical-sort = "0.3"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
rand = "0.8"
|
rand = "0.8"
|
||||||
regex = "1"
|
regex = "1"
|
||||||
@ -22,7 +23,7 @@ serde = { version = "1.0", features = ["derive"] }
|
|||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
serde_yaml = "0.9"
|
serde_yaml = "0.9"
|
||||||
shlex = "1.1"
|
shlex = "1.1"
|
||||||
simplelog = { version = "^0.12", features = ["paris"] }
|
simplelog = { version = "0.12", features = ["paris"] }
|
||||||
time = { version = "0.3", features = ["formatting", "macros"] }
|
time = { version = "0.3", features = ["formatting", "macros"] }
|
||||||
walkdir = "2"
|
walkdir = "2"
|
||||||
|
|
||||||
@ -32,3 +33,6 @@ features = ["shlobj", "std", "winerror"]
|
|||||||
|
|
||||||
[target.x86_64-unknown-linux-musl.dependencies]
|
[target.x86_64-unknown-linux-musl.dependencies]
|
||||||
openssl = { version = "0.10", features = ["vendored"] }
|
openssl = { version = "0.10", features = ["vendored"] }
|
||||||
|
|
||||||
|
[target.'cfg(not(target_arch = "windows"))'.dependencies]
|
||||||
|
signal-child = "1"
|
||||||
|
@ -6,6 +6,7 @@ use std::{
|
|||||||
str::FromStr,
|
str::FromStr,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use chrono::NaiveTime;
|
||||||
use log::LevelFilter;
|
use log::LevelFilter;
|
||||||
use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
|
use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
|
||||||
use shlex::split;
|
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
|
/// Global 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.
|
||||||
@ -163,6 +177,9 @@ pub struct General {
|
|||||||
#[serde(skip_serializing, skip_deserializing)]
|
#[serde(skip_serializing, skip_deserializing)]
|
||||||
pub ffmpeg_libs: Vec<String>,
|
pub ffmpeg_libs: Vec<String>,
|
||||||
|
|
||||||
|
#[serde(skip_serializing, skip_deserializing)]
|
||||||
|
pub template: Option<Template>,
|
||||||
|
|
||||||
#[serde(default, skip_serializing, skip_deserializing)]
|
#[serde(default, skip_serializing, skip_deserializing)]
|
||||||
pub validate: bool,
|
pub validate: bool,
|
||||||
}
|
}
|
||||||
@ -196,7 +213,7 @@ pub struct Logging {
|
|||||||
pub local_time: bool,
|
pub local_time: bool,
|
||||||
pub timestamp: bool,
|
pub timestamp: bool,
|
||||||
#[serde(alias = "log_path")]
|
#[serde(alias = "log_path")]
|
||||||
pub path: String,
|
pub path: PathBuf,
|
||||||
#[serde(
|
#[serde(
|
||||||
alias = "log_level",
|
alias = "log_level",
|
||||||
serialize_with = "log_level_to_string",
|
serialize_with = "log_level_to_string",
|
||||||
@ -255,7 +272,7 @@ pub struct Ingest {
|
|||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
pub struct Playlist {
|
pub struct Playlist {
|
||||||
pub help_text: String,
|
pub help_text: String,
|
||||||
pub path: String,
|
pub path: PathBuf,
|
||||||
pub day_start: String,
|
pub day_start: String,
|
||||||
|
|
||||||
#[serde(skip_serializing, skip_deserializing)]
|
#[serde(skip_serializing, skip_deserializing)]
|
||||||
@ -272,11 +289,11 @@ pub struct Playlist {
|
|||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
pub struct Storage {
|
pub struct Storage {
|
||||||
pub help_text: String,
|
pub help_text: String,
|
||||||
pub path: String,
|
pub path: PathBuf,
|
||||||
#[serde(skip_serializing, skip_deserializing)]
|
#[serde(skip_serializing, skip_deserializing)]
|
||||||
pub paths: Vec<String>,
|
pub paths: Vec<PathBuf>,
|
||||||
#[serde(alias = "filler_clip")]
|
#[serde(alias = "filler_clip")]
|
||||||
pub filler: String,
|
pub filler: PathBuf,
|
||||||
pub extensions: Vec<String>,
|
pub extensions: Vec<String>,
|
||||||
pub shuffle: bool,
|
pub shuffle: bool,
|
||||||
}
|
}
|
||||||
@ -304,7 +321,7 @@ pub struct Text {
|
|||||||
#[derive(Debug, Default, Serialize, Deserialize, Clone)]
|
#[derive(Debug, Default, Serialize, Deserialize, Clone)]
|
||||||
pub struct Task {
|
pub struct Task {
|
||||||
pub enable: bool,
|
pub enable: bool,
|
||||||
pub path: String,
|
pub path: PathBuf,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
@ -335,11 +352,11 @@ fn default_channels() -> u8 {
|
|||||||
|
|
||||||
impl PlayoutConfig {
|
impl PlayoutConfig {
|
||||||
/// Read config from YAML file, and set some extra config values.
|
/// 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");
|
let mut config_path = PathBuf::from("/etc/ffplayout/ffplayout.yml");
|
||||||
|
|
||||||
if let Some(cfg) = cfg_path {
|
if let Some(cfg) = cfg_path {
|
||||||
config_path = PathBuf::from(cfg);
|
config_path = cfg;
|
||||||
}
|
}
|
||||||
|
|
||||||
if !config_path.is_file() {
|
if !config_path.is_file() {
|
||||||
|
@ -7,6 +7,9 @@ use std::{
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
#[cfg(not(windows))]
|
||||||
|
use signal_child::Signalable;
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use simplelog::*;
|
use simplelog::*;
|
||||||
|
|
||||||
@ -71,9 +74,15 @@ impl ProcessControl {
|
|||||||
match unit {
|
match unit {
|
||||||
Decoder => {
|
Decoder => {
|
||||||
if let Some(proc) = self.decoder_term.lock().unwrap().as_mut() {
|
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() {
|
if let Err(e) = proc.kill() {
|
||||||
return Err(format!("Decoder {e:?}"));
|
return Err(format!("Decoder {e:?}"));
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Encoder => {
|
Encoder => {
|
||||||
@ -177,7 +186,7 @@ impl PlayerControl {
|
|||||||
Self {
|
Self {
|
||||||
current_media: Arc::new(Mutex::new(None)),
|
current_media: Arc::new(Mutex::new(None)),
|
||||||
current_list: Arc::new(Mutex::new(vec![Media::new(0, "", false)])),
|
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)),
|
current_index: Arc::new(AtomicUsize::new(0)),
|
||||||
filler_index: Arc::new(AtomicUsize::new(0)),
|
filler_index: Arc::new(AtomicUsize::new(0)),
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,6 @@
|
|||||||
use std::{
|
use std::sync::{
|
||||||
path::Path,
|
atomic::Ordering,
|
||||||
sync::{
|
{Arc, Mutex},
|
||||||
atomic::Ordering,
|
|
||||||
{Arc, Mutex},
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use rand::{seq::SliceRandom, thread_rng};
|
use rand::{seq::SliceRandom, thread_rng};
|
||||||
@ -37,18 +34,18 @@ impl FolderSource {
|
|||||||
|
|
||||||
if config.general.generate.is_some() && !config.storage.paths.is_empty() {
|
if config.general.generate.is_some() && !config.storage.paths.is_empty() {
|
||||||
for path in &config.storage.paths {
|
for path in &config.storage.paths {
|
||||||
path_list.push(path.clone())
|
path_list.push(path)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
path_list.push(config.storage.path.clone())
|
path_list.push(&config.storage.path)
|
||||||
}
|
}
|
||||||
|
|
||||||
for path in &path_list {
|
for path in &path_list {
|
||||||
if !Path::new(path).is_dir() {
|
if !path.is_dir() {
|
||||||
error!("Path not exists: <b><magenta>{path}</></b>");
|
error!("Path not exists: <b><magenta>{path:?}</></b>");
|
||||||
}
|
}
|
||||||
|
|
||||||
for entry in WalkDir::new(path.clone())
|
for entry in WalkDir::new(path)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.flat_map(|e| e.ok())
|
.flat_map(|e| e.ok())
|
||||||
.filter(|f| f.path().is_file())
|
.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) {
|
fn shuffle(&mut self) {
|
||||||
let mut rng = thread_rng();
|
let mut rng = thread_rng();
|
||||||
let mut nodes = self.player_control.current_list.lock().unwrap();
|
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 mut filler_list = vec![];
|
||||||
|
let filler_path = &config.storage.filler;
|
||||||
|
|
||||||
if Path::new(&config.storage.filler).is_dir() {
|
if filler_path.is_dir() {
|
||||||
debug!(
|
|
||||||
"Fill filler list from: <b><magenta>{}</></b>",
|
|
||||||
config.storage.filler
|
|
||||||
);
|
|
||||||
|
|
||||||
for (index, entry) in WalkDir::new(&config.storage.filler)
|
for (index, entry) in WalkDir::new(&config.storage.filler)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.flat_map(|e| e.ok())
|
.flat_map(|e| e.ok())
|
||||||
.filter(|f| f.path().is_file())
|
.filter(|f| f.path().is_file())
|
||||||
.filter(|f| include_file_extension(&config, f.path()))
|
.filter(|f| include_file_extension(config, f.path()))
|
||||||
.enumerate()
|
.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 {
|
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() {
|
for (index, item) in filler_list.iter_mut().enumerate() {
|
||||||
item.index = Some(index);
|
item.index = Some(index);
|
||||||
}
|
}
|
||||||
} else {
|
} else if filler_path.is_file() {
|
||||||
filler_list.push(Media::new(0, &config.storage.filler, false));
|
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
|
||||||
}
|
}
|
||||||
|
@ -4,22 +4,192 @@
|
|||||||
///
|
///
|
||||||
/// The generator takes the files from storage, which are set in config.
|
/// The generator takes the files from storage, which are set in config.
|
||||||
/// It also respect the shuffle/sort mode.
|
/// It also respect the shuffle/sort mode.
|
||||||
///
|
|
||||||
/// Beside that it is really very basic, without any logic.
|
|
||||||
use std::{
|
use std::{
|
||||||
fs::{create_dir_all, write},
|
fs::{create_dir_all, write},
|
||||||
io::Error,
|
io::Error,
|
||||||
path::Path,
|
|
||||||
process::exit,
|
process::exit,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use chrono::Timelike;
|
||||||
|
use lexical_sort::{natural_lexical_cmp, StringSort};
|
||||||
|
use rand::{seq::SliceRandom, thread_rng, Rng};
|
||||||
use simplelog::*;
|
use simplelog::*;
|
||||||
|
use walkdir::WalkDir;
|
||||||
|
|
||||||
use super::{folder::FolderSource, PlayerControl};
|
use super::{folder::FolderSource, PlayerControl};
|
||||||
use crate::utils::{
|
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
|
/// Generate playlists
|
||||||
pub fn generate_playlist(
|
pub fn generate_playlist(
|
||||||
config: &PlayoutConfig,
|
config: &PlayoutConfig,
|
||||||
@ -36,9 +206,10 @@ pub fn generate_playlist(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
let player_control = PlayerControl::new();
|
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 playlists = vec![];
|
||||||
let mut date_range = vec![];
|
let mut date_range = vec![];
|
||||||
|
let mut from_template = false;
|
||||||
|
|
||||||
let channel = match channel_name {
|
let channel = match channel_name {
|
||||||
Some(name) => name,
|
Some(name) => name,
|
||||||
@ -47,8 +218,8 @@ pub fn generate_playlist(
|
|||||||
|
|
||||||
if !playlist_root.is_dir() {
|
if !playlist_root.is_dir() {
|
||||||
error!(
|
error!(
|
||||||
"Playlist folder <b><magenta>{}</></b> not exists!",
|
"Playlist folder <b><magenta>{:?}</></b> not exists!",
|
||||||
&config.playlist.path
|
config.playlist.path
|
||||||
);
|
);
|
||||||
|
|
||||||
exit(1);
|
exit(1);
|
||||||
@ -62,8 +233,16 @@ pub fn generate_playlist(
|
|||||||
date_range = get_date_range(&date_range)
|
date_range = get_date_range(&date_range)
|
||||||
}
|
}
|
||||||
|
|
||||||
let media_list = FolderSource::new(config, None, &player_control);
|
// gives an iterator with infinit length
|
||||||
let list_length = media_list.player_control.current_list.lock().unwrap().len();
|
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 {
|
for date in date_range {
|
||||||
let d: Vec<&str> = date.split('-').collect();
|
let d: Vec<&str> = date.split('-').collect();
|
||||||
@ -71,6 +250,8 @@ pub fn generate_playlist(
|
|||||||
let month = d[1];
|
let month = d[1];
|
||||||
let playlist_path = playlist_root.join(year).join(month);
|
let playlist_path = playlist_root.join(year).join(month);
|
||||||
let playlist_file = &playlist_path.join(format!("{date}.json"));
|
let playlist_file = &playlist_path.join(format!("{date}.json"));
|
||||||
|
let mut length = 0.0;
|
||||||
|
let mut round = 0;
|
||||||
|
|
||||||
create_dir_all(playlist_path)?;
|
create_dir_all(playlist_path)?;
|
||||||
|
|
||||||
@ -88,12 +269,6 @@ pub fn generate_playlist(
|
|||||||
playlist_file.display()
|
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 {
|
let mut playlist = JsonPlaylist {
|
||||||
channel: channel.clone(),
|
channel: channel.clone(),
|
||||||
date,
|
date,
|
||||||
@ -103,30 +278,38 @@ pub fn generate_playlist(
|
|||||||
program: vec![],
|
program: vec![],
|
||||||
};
|
};
|
||||||
|
|
||||||
for item in media_list.clone() {
|
if from_template {
|
||||||
let duration = item.duration;
|
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 {
|
if total_length >= length + duration {
|
||||||
playlist.program.push(item);
|
playlist.program.push(item);
|
||||||
|
|
||||||
length += duration;
|
length += duration;
|
||||||
} else if filler_length > 0.0 && filler_length > total_length - length {
|
} else if round == list_length - 1 {
|
||||||
filler.out = total_length - length;
|
break;
|
||||||
playlist.program.push(filler);
|
} else {
|
||||||
|
round += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
break;
|
let list_duration = sum_durations(&playlist.program);
|
||||||
} else if round == list_length - 1 {
|
|
||||||
break;
|
if config.playlist.length_sec.unwrap() > list_duration {
|
||||||
} else {
|
let time_left = config.playlist.length_sec.unwrap() - list_duration;
|
||||||
round += 1;
|
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)?;
|
let json: String = serde_json::to_string_pretty(&playlist)?;
|
||||||
|
|
||||||
write(playlist_file, json)?;
|
write(playlist_file, json)?;
|
||||||
|
|
||||||
|
playlists.push(playlist);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(playlists)
|
Ok(playlists)
|
||||||
|
@ -12,7 +12,7 @@ pub fn import_file(
|
|||||||
config: &PlayoutConfig,
|
config: &PlayoutConfig,
|
||||||
date: &str,
|
date: &str,
|
||||||
channel_name: Option<String>,
|
channel_name: Option<String>,
|
||||||
path: &str,
|
path: &Path,
|
||||||
) -> Result<String, Error> {
|
) -> Result<String, Error> {
|
||||||
let file = File::open(path)?;
|
let file = File::open(path)?;
|
||||||
let reader = BufReader::new(file);
|
let reader = BufReader::new(file);
|
||||||
@ -25,13 +25,13 @@ pub fn import_file(
|
|||||||
program: vec![],
|
program: vec![],
|
||||||
};
|
};
|
||||||
|
|
||||||
let playlist_root = Path::new(&config.playlist.path);
|
let playlist_root = &config.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.playlist.path,
|
config.playlist.path,
|
||||||
),
|
),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
@ -142,14 +142,14 @@ pub fn read_json(
|
|||||||
path: Option<String>,
|
path: Option<String>,
|
||||||
is_terminated: Arc<AtomicBool>,
|
is_terminated: Arc<AtomicBool>,
|
||||||
seek: bool,
|
seek: bool,
|
||||||
next_start: f64,
|
get_next: bool,
|
||||||
) -> JsonPlaylist {
|
) -> JsonPlaylist {
|
||||||
let config_clone = config.clone();
|
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 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();
|
let d: Vec<&str> = date.split('-').collect();
|
||||||
playlist_path = playlist_path
|
playlist_path = playlist_path
|
||||||
.join(d[0])
|
.join(d[0])
|
||||||
|
@ -165,7 +165,7 @@ pub fn validate_playlist(
|
|||||||
begin += item.out - item.seek;
|
begin += item.out - item.seek;
|
||||||
}
|
}
|
||||||
|
|
||||||
if !config.playlist.infinit && length > begin + 1.0 {
|
if !config.playlist.infinit && length > begin + 1.2 {
|
||||||
error!(
|
error!(
|
||||||
"Playlist from <yellow>{date}</> not long enough, <yellow>{}</> needed!",
|
"Playlist from <yellow>{date}</> not long enough, <yellow>{}</> needed!",
|
||||||
sec_to_time(length - begin),
|
sec_to_time(length - begin),
|
||||||
|
@ -2,7 +2,7 @@ extern crate log;
|
|||||||
extern crate simplelog;
|
extern crate simplelog;
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
path::Path,
|
path::PathBuf,
|
||||||
sync::{atomic::Ordering, Arc, Mutex},
|
sync::{atomic::Ordering, Arc, Mutex},
|
||||||
thread::{self, sleep},
|
thread::{self, sleep},
|
||||||
time::Duration,
|
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
|
let file_config = log_config
|
||||||
.clone()
|
.clone()
|
||||||
.set_time_format_custom(format_description!(
|
.set_time_format_custom(format_description!(
|
||||||
"[[[year]-[month]-[day] [hour]:[minute]:[second].[subsecond digits:5]]"
|
"[[[year]-[month]-[day] [hour]:[minute]:[second].[subsecond digits:5]]"
|
||||||
))
|
))
|
||||||
.build();
|
.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() {
|
if app_config.path.is_dir() {
|
||||||
log_path = Path::new(&app_config.path)
|
log_path = app_config.path.join("ffplayout.log");
|
||||||
.join("ffplayout.log")
|
} else if app_config.path.is_file() {
|
||||||
.display()
|
|
||||||
.to_string();
|
|
||||||
} else if Path::new(&app_config.path).is_file() {
|
|
||||||
log_path = app_config.path
|
log_path = app_config.path
|
||||||
} else {
|
} else {
|
||||||
println!("Logging path not exists!")
|
println!("Logging path not exists!")
|
||||||
|
@ -13,7 +13,7 @@ use std::{
|
|||||||
use std::env;
|
use std::env;
|
||||||
|
|
||||||
use chrono::{prelude::*, Duration};
|
use chrono::{prelude::*, Duration};
|
||||||
use ffprobe::{ffprobe, Format, Stream};
|
use ffprobe::{ffprobe, Format, Stream as FFStream};
|
||||||
use rand::prelude::*;
|
use rand::prelude::*;
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use reqwest::header;
|
use reqwest::header;
|
||||||
@ -24,7 +24,7 @@ use simplelog::*;
|
|||||||
pub mod config;
|
pub mod config;
|
||||||
pub mod controller;
|
pub mod controller;
|
||||||
pub mod folder;
|
pub mod folder;
|
||||||
mod generator;
|
pub mod generator;
|
||||||
pub mod import;
|
pub mod import;
|
||||||
pub mod json_serializer;
|
pub mod json_serializer;
|
||||||
mod json_validate;
|
mod json_validate;
|
||||||
@ -38,7 +38,7 @@ pub use config::{
|
|||||||
OutputMode::{self, *},
|
OutputMode::{self, *},
|
||||||
PlayoutConfig,
|
PlayoutConfig,
|
||||||
ProcessMode::{self, *},
|
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::{
|
pub use controller::{
|
||||||
PlayerControl, PlayoutStatus, ProcessControl,
|
PlayerControl, PlayoutStatus, ProcessControl,
|
||||||
@ -148,14 +148,15 @@ impl Media {
|
|||||||
pub fn add_probe(&mut self) {
|
pub fn add_probe(&mut self) {
|
||||||
if self.probe.is_none() {
|
if self.probe.is_none() {
|
||||||
let probe = MediaProbe::new(&self.source);
|
let probe = MediaProbe::new(&self.source);
|
||||||
self.probe = Some(probe.clone());
|
|
||||||
|
|
||||||
if let Some(dur) = probe
|
if let Some(dur) = probe
|
||||||
.format
|
.format
|
||||||
|
.clone()
|
||||||
.and_then(|f| f.duration)
|
.and_then(|f| f.duration)
|
||||||
.map(|d| d.parse().unwrap())
|
.map(|d| d.parse().unwrap())
|
||||||
.filter(|d| !is_close(*d, self.duration, 0.5))
|
.filter(|d| !is_close(*d, self.duration, 0.5))
|
||||||
{
|
{
|
||||||
|
self.probe = Some(probe);
|
||||||
self.duration = dur;
|
self.duration = dur;
|
||||||
|
|
||||||
if self.out == 0.0 {
|
if self.out == 0.0 {
|
||||||
@ -205,8 +206,8 @@ fn is_empty_string(st: &String) -> bool {
|
|||||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
|
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
|
||||||
pub struct MediaProbe {
|
pub struct MediaProbe {
|
||||||
pub format: Option<Format>,
|
pub format: Option<Format>,
|
||||||
pub audio_streams: Vec<Stream>,
|
pub audio_streams: Vec<FFStream>,
|
||||||
pub video_streams: Vec<Stream>,
|
pub video_streams: Vec<FFStream>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MediaProbe {
|
impl MediaProbe {
|
||||||
@ -321,14 +322,14 @@ pub fn get_sec() -> f64 {
|
|||||||
///
|
///
|
||||||
/// - When time is before playlist start, get date from yesterday.
|
/// - 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.
|
/// - 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();
|
let local: DateTime<Local> = time_now();
|
||||||
|
|
||||||
if seek && start > get_sec() {
|
if seek && start > get_sec() {
|
||||||
return (local - Duration::days(1)).format("%Y-%m-%d").to_string();
|
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();
|
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
|
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,
|
/// Get delta between clip start and current time. This value we need to check,
|
||||||
/// if we still in sync.
|
/// if we still in sync.
|
||||||
///
|
///
|
||||||
|
@ -41,3 +41,7 @@ path = "src/engine_playlist.rs"
|
|||||||
[[test]]
|
[[test]]
|
||||||
name = "engine_cmd"
|
name = "engine_cmd"
|
||||||
path = "src/engine_cmd.rs"
|
path = "src/engine_cmd.rs"
|
||||||
|
|
||||||
|
[[test]]
|
||||||
|
name = "engine_generator"
|
||||||
|
path = "src/engine_generator.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::{input::playlist::gen_source, utils::prepare_output_cmd};
|
||||||
use ffplayout_lib::{
|
use ffplayout_lib::{
|
||||||
@ -8,7 +8,7 @@ use ffplayout_lib::{
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn video_audio_input() {
|
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();
|
let player_control = PlayerControl::new();
|
||||||
config.out.mode = Stream;
|
config.out.mode = Stream;
|
||||||
config.processing.add_logo = true;
|
config.processing.add_logo = true;
|
||||||
@ -36,7 +36,7 @@ fn video_audio_input() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn video_audio_custom_filter1_input() {
|
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();
|
let player_control = PlayerControl::new();
|
||||||
config.out.mode = Stream;
|
config.out.mode = Stream;
|
||||||
config.processing.add_logo = false;
|
config.processing.add_logo = false;
|
||||||
@ -62,7 +62,7 @@ fn video_audio_custom_filter1_input() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn video_audio_custom_filter2_input() {
|
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();
|
let player_control = PlayerControl::new();
|
||||||
config.out.mode = Stream;
|
config.out.mode = Stream;
|
||||||
config.processing.add_logo = false;
|
config.processing.add_logo = false;
|
||||||
@ -90,7 +90,7 @@ fn video_audio_custom_filter2_input() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn video_audio_custom_filter3_input() {
|
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();
|
let player_control = PlayerControl::new();
|
||||||
config.out.mode = Stream;
|
config.out.mode = Stream;
|
||||||
config.processing.add_logo = false;
|
config.processing.add_logo = false;
|
||||||
@ -117,7 +117,7 @@ fn video_audio_custom_filter3_input() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn dual_audio_aevalsrc_input() {
|
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();
|
let player_control = PlayerControl::new();
|
||||||
config.out.mode = Stream;
|
config.out.mode = Stream;
|
||||||
config.processing.audio_tracks = 2;
|
config.processing.audio_tracks = 2;
|
||||||
@ -144,7 +144,7 @@ fn dual_audio_aevalsrc_input() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn dual_audio_input() {
|
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();
|
let player_control = PlayerControl::new();
|
||||||
config.out.mode = Stream;
|
config.out.mode = Stream;
|
||||||
config.processing.audio_tracks = 2;
|
config.processing.audio_tracks = 2;
|
||||||
@ -170,7 +170,7 @@ fn dual_audio_input() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn video_separate_audio_input() {
|
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();
|
let player_control = PlayerControl::new();
|
||||||
config.out.mode = Stream;
|
config.out.mode = Stream;
|
||||||
config.processing.audio_tracks = 1;
|
config.processing.audio_tracks = 1;
|
||||||
@ -204,7 +204,7 @@ fn video_separate_audio_input() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn video_audio_stream() {
|
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.out.mode = Stream;
|
||||||
config.processing.add_logo = false;
|
config.processing.add_logo = false;
|
||||||
config.out.output_cmd = Some(vec_strings![
|
config.out.output_cmd = Some(vec_strings![
|
||||||
@ -263,7 +263,7 @@ fn video_audio_stream() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn video_audio_filter1_stream() {
|
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.out.mode = Stream;
|
||||||
config.processing.add_logo = false;
|
config.processing.add_logo = false;
|
||||||
config.text.add_text = false;
|
config.text.add_text = false;
|
||||||
@ -338,7 +338,7 @@ fn video_audio_filter1_stream() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn video_audio_filter2_stream() {
|
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.out.mode = Stream;
|
||||||
config.processing.add_logo = false;
|
config.processing.add_logo = false;
|
||||||
config.text.add_text = true;
|
config.text.add_text = true;
|
||||||
@ -421,7 +421,7 @@ fn video_audio_filter2_stream() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn video_audio_filter3_stream() {
|
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.out.mode = Stream;
|
||||||
config.processing.add_logo = false;
|
config.processing.add_logo = false;
|
||||||
config.text.add_text = true;
|
config.text.add_text = true;
|
||||||
@ -507,7 +507,7 @@ fn video_audio_filter3_stream() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn video_audio_filter4_stream() {
|
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.out.mode = Stream;
|
||||||
config.processing.add_logo = false;
|
config.processing.add_logo = false;
|
||||||
config.text.add_text = true;
|
config.text.add_text = true;
|
||||||
@ -593,7 +593,7 @@ fn video_audio_filter4_stream() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn video_dual_audio_stream() {
|
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.out.mode = Stream;
|
||||||
config.processing.add_logo = false;
|
config.processing.add_logo = false;
|
||||||
config.processing.audio_tracks = 2;
|
config.processing.audio_tracks = 2;
|
||||||
@ -664,7 +664,7 @@ fn video_dual_audio_stream() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn video_dual_audio_filter_stream() {
|
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.out.mode = Stream;
|
||||||
config.processing.add_logo = false;
|
config.processing.add_logo = false;
|
||||||
config.processing.audio_tracks = 2;
|
config.processing.audio_tracks = 2;
|
||||||
@ -744,7 +744,7 @@ fn video_dual_audio_filter_stream() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn video_audio_multi_stream() {
|
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.out.mode = Stream;
|
||||||
config.processing.add_logo = false;
|
config.processing.add_logo = false;
|
||||||
config.out.output_cmd = Some(vec_strings![
|
config.out.output_cmd = Some(vec_strings![
|
||||||
@ -833,7 +833,7 @@ fn video_audio_multi_stream() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn video_dual_audio_multi_stream() {
|
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.out.mode = Stream;
|
||||||
config.processing.add_logo = false;
|
config.processing.add_logo = false;
|
||||||
config.processing.audio_tracks = 2;
|
config.processing.audio_tracks = 2;
|
||||||
@ -947,7 +947,7 @@ fn video_dual_audio_multi_stream() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn video_audio_text_multi_stream() {
|
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.out.mode = Stream;
|
||||||
config.processing.add_logo = false;
|
config.processing.add_logo = false;
|
||||||
config.text.add_text = true;
|
config.text.add_text = true;
|
||||||
@ -1060,7 +1060,7 @@ fn video_audio_text_multi_stream() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn video_dual_audio_multi_filter_stream() {
|
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.out.mode = Stream;
|
||||||
config.processing.add_logo = false;
|
config.processing.add_logo = false;
|
||||||
config.processing.audio_tracks = 2;
|
config.processing.audio_tracks = 2;
|
||||||
@ -1189,7 +1189,7 @@ fn video_dual_audio_multi_filter_stream() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn video_audio_text_filter_stream() {
|
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.out.mode = Stream;
|
||||||
config.processing.add_logo = false;
|
config.processing.add_logo = false;
|
||||||
config.processing.audio_tracks = 1;
|
config.processing.audio_tracks = 1;
|
||||||
@ -1311,7 +1311,7 @@ fn video_audio_text_filter_stream() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn video_audio_hls() {
|
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();
|
let player_control = PlayerControl::new();
|
||||||
config.out.mode = HLS;
|
config.out.mode = HLS;
|
||||||
config.processing.add_logo = false;
|
config.processing.add_logo = false;
|
||||||
@ -1397,7 +1397,7 @@ fn video_audio_hls() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn video_audio_sub_meta_hls() {
|
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();
|
let player_control = PlayerControl::new();
|
||||||
config.out.mode = HLS;
|
config.out.mode = HLS;
|
||||||
config.processing.add_logo = false;
|
config.processing.add_logo = false;
|
||||||
@ -1491,7 +1491,7 @@ fn video_audio_sub_meta_hls() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn video_multi_audio_hls() {
|
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();
|
let player_control = PlayerControl::new();
|
||||||
config.out.mode = HLS;
|
config.out.mode = HLS;
|
||||||
config.processing.add_logo = false;
|
config.processing.add_logo = false;
|
||||||
@ -1580,7 +1580,7 @@ fn video_multi_audio_hls() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn multi_video_audio_hls() {
|
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();
|
let player_control = PlayerControl::new();
|
||||||
config.out.mode = HLS;
|
config.out.mode = HLS;
|
||||||
config.processing.add_logo = false;
|
config.processing.add_logo = false;
|
||||||
@ -1694,7 +1694,7 @@ fn multi_video_audio_hls() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn multi_video_multi_audio_hls() {
|
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();
|
let player_control = PlayerControl::new();
|
||||||
config.out.mode = HLS;
|
config.out.mode = HLS;
|
||||||
config.processing.add_logo = false;
|
config.processing.add_logo = false;
|
||||||
|
148
tests/src/engine_generator.rs
Normal file
148
tests/src/engine_generator.rs
Normal file
@ -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}"
|
||||||
|
);
|
||||||
|
}
|
@ -17,6 +17,48 @@ fn timed_stop(sec: u64, proc_ctl: ProcessControl) {
|
|||||||
proc_ctl.stop_all();
|
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]
|
#[test]
|
||||||
#[serial]
|
#[serial]
|
||||||
#[ignore]
|
#[ignore]
|
||||||
@ -34,6 +76,7 @@ fn playlist_change_at_midnight() {
|
|||||||
config.storage.filler = "assets/with_audio.mp4".into();
|
config.storage.filler = "assets/with_audio.mp4".into();
|
||||||
config.logging.log_to_file = false;
|
config.logging.log_to_file = false;
|
||||||
config.logging.timestamp = false;
|
config.logging.timestamp = false;
|
||||||
|
config.logging.level = LevelFilter::Trace;
|
||||||
config.out.mode = Null;
|
config.out.mode = Null;
|
||||||
config.out.output_count = 1;
|
config.out.output_count = 1;
|
||||||
config.out.output_filter = None;
|
config.out.output_filter = None;
|
||||||
@ -58,6 +101,48 @@ fn playlist_change_at_midnight() {
|
|||||||
assert_eq!(playlist_date, "2023-02-09");
|
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]
|
#[test]
|
||||||
#[serial]
|
#[serial]
|
||||||
#[ignore]
|
#[ignore]
|
||||||
|
@ -1,3 +1,5 @@
|
|||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
use chrono::prelude::*;
|
use chrono::prelude::*;
|
||||||
|
|
||||||
@ -22,23 +24,23 @@ fn mock_date_time() {
|
|||||||
fn get_date_yesterday() {
|
fn get_date_yesterday() {
|
||||||
mock_time::set_mock_time("2022-05-20T05:59:24");
|
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);
|
assert_eq!("2022-05-19".to_string(), date);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn get_date_tomorrow() {
|
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);
|
assert_eq!("2022-05-21".to_string(), date);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_delta() {
|
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.mail.recipient = "".into();
|
||||||
config.processing.mode = Playlist;
|
config.processing.mode = Playlist;
|
||||||
config.playlist.day_start = "00:00:00".into();
|
config.playlist.day_start = "00:00:00".into();
|
||||||
|
Loading…
x
Reference in New Issue
Block a user