generate playlists based on template, serialize paths to PathBuf
This commit is contained in:
parent
d2c72d56fe
commit
0c51f8303c
1
.gitignore
vendored
1
.gitignore
vendored
@ -26,3 +26,4 @@ ffpapi.1.gz
|
||||
/public/
|
||||
tmp/
|
||||
.vscode/
|
||||
assets/playlist_template.json
|
||||
|
684
Cargo.lock
generated
684
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,7 @@
|
||||
[workspace]
|
||||
members = ["ffplayout-api", "ffplayout-engine", "lib", "tests"]
|
||||
default-members = ["ffplayout-api", "ffplayout-engine", "tests"]
|
||||
resolver = "2"
|
||||
|
||||
[workspace.package]
|
||||
version = "0.20.0"
|
||||
|
@ -54,6 +54,7 @@ Check the [releases](https://github.com/ffplayout/ffplayout/releases/latest) for
|
||||
- import playlist from text or m3u file, with CLI or frontend
|
||||
- audio only, for radio mode (experimental *)
|
||||
- [Piggyback Mode](/ffplayout-api/README.md#piggyback-mode), mostly for non Linux systems (experimental *)
|
||||
- generate playlist based on [template](/docs/playlist_gen.md) (experimental *)
|
||||
|
||||
For preview stream, read: [/docs/preview_stream.md](/docs/preview_stream.md)
|
||||
|
||||
|
@ -21,6 +21,10 @@ Using live ingest to inject a live stream.
|
||||
|
||||
The different output modes.
|
||||
|
||||
### **[Playlist Generation](/docs/playlist_gen.md)**
|
||||
|
||||
Generate playlists based on template.
|
||||
|
||||
### **[Multi Audio Tracks](/docs/multi_audio.md)**
|
||||
|
||||
Output multiple audio tracks.
|
||||
|
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"]}]}}'
|
||||
```
|
||||
|
@ -8,7 +8,7 @@
|
||||
///
|
||||
/// For all endpoints an (Bearer) authentication is required.\
|
||||
/// `{id}` represent the channel id, and at default is 1.
|
||||
use std::{collections::HashMap, env, fs, path::Path};
|
||||
use std::{collections::HashMap, env, fs, path::PathBuf};
|
||||
|
||||
use actix_multipart::Multipart;
|
||||
use actix_web::{delete, get, http::StatusCode, patch, post, put, web, HttpResponse, Responder};
|
||||
@ -46,6 +46,7 @@ use crate::{
|
||||
use ffplayout_lib::{
|
||||
utils::{
|
||||
get_date_range, import::import_file, sec_to_time, time_to_sec, JsonPlaylist, PlayoutConfig,
|
||||
Template,
|
||||
},
|
||||
vec_strings,
|
||||
};
|
||||
@ -72,19 +73,20 @@ pub struct DateObj {
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
struct FileObj {
|
||||
#[serde(default)]
|
||||
path: String,
|
||||
path: PathBuf,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Deserialize, Serialize)]
|
||||
pub struct PathsObj {
|
||||
#[serde(default)]
|
||||
paths: Vec<String>,
|
||||
template: Option<Template>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize)]
|
||||
pub struct ImportObj {
|
||||
#[serde(default)]
|
||||
file: String,
|
||||
file: PathBuf,
|
||||
#[serde(default)]
|
||||
date: String,
|
||||
}
|
||||
@ -724,7 +726,7 @@ pub async fn get_playlist(
|
||||
/// ```BASH
|
||||
/// curl -X POST http://127.0.0.1:8787/api/playlist/1/
|
||||
/// -H 'Content-Type: application/json' -H 'Authorization: Bearer <TOKEN>'
|
||||
/// -- data "{<JSON playlist data>}"
|
||||
/// --data "{<JSON playlist data>}"
|
||||
/// ```
|
||||
#[post("/playlist/{id}/")]
|
||||
#[has_any_role("Role::Admin", "Role::User", type = "Role")]
|
||||
@ -746,7 +748,7 @@ pub async fn save_playlist(
|
||||
/// ```BASH
|
||||
/// curl -X POST http://127.0.0.1:8787/api/playlist/1/generate/2022-06-20
|
||||
/// -H 'Content-Type: application/json' -H 'Authorization: Bearer <TOKEN>'
|
||||
/// /// -- data '{ "paths": [<list of paths>] }' # <- data is optional
|
||||
/// /// --data '{ "paths": [<list of paths>] }' # <- data is optional
|
||||
/// ```
|
||||
#[post("/playlist/{id}/generate/{date}")]
|
||||
#[has_any_role("Role::Admin", "Role::User", type = "Role")]
|
||||
@ -764,10 +766,11 @@ pub async fn gen_playlist(
|
||||
for path in &obj.paths {
|
||||
let (p, _, _) = norm_abs_path(&config.storage.path, path);
|
||||
|
||||
path_list.push(p.to_string_lossy().to_string());
|
||||
path_list.push(p);
|
||||
}
|
||||
|
||||
config.storage.paths = path_list;
|
||||
config.general.template = obj.template.clone();
|
||||
}
|
||||
|
||||
match generate_playlist(config, channel.name).await {
|
||||
@ -921,8 +924,8 @@ async fn import_playlist(
|
||||
payload: Multipart,
|
||||
obj: web::Query<ImportObj>,
|
||||
) -> Result<HttpResponse, ServiceError> {
|
||||
let file = Path::new(&obj.file).file_name().unwrap_or_default();
|
||||
let path = env::temp_dir().join(file).to_string_lossy().to_string();
|
||||
let file = obj.file.file_name().unwrap_or_default();
|
||||
let path = env::temp_dir().join(file);
|
||||
let (config, _) = playout_config(&pool.clone().into_inner(), &id).await?;
|
||||
let channel = handles::select_channel(&pool.clone().into_inner(), &id).await?;
|
||||
|
||||
|
@ -82,8 +82,8 @@ async fn create_schema(conn: &Pool<Sqlite>) -> Result<SqliteQueryResult, sqlx::E
|
||||
pub async fn db_init(domain: Option<String>) -> Result<&'static str, Box<dyn std::error::Error>> {
|
||||
let db_path = db_path()?;
|
||||
|
||||
if !Sqlite::database_exists(&db_path).await.unwrap_or(false) {
|
||||
Sqlite::create_database(&db_path).await.unwrap();
|
||||
if !Sqlite::database_exists(db_path).await.unwrap_or(false) {
|
||||
Sqlite::create_database(db_path).await.unwrap();
|
||||
|
||||
let pool = db_pool().await?;
|
||||
|
||||
|
@ -7,7 +7,7 @@ use crate::utils::db_path;
|
||||
|
||||
pub async fn db_pool() -> Result<Pool<Sqlite>, sqlx::Error> {
|
||||
let db_path = db_path().unwrap();
|
||||
let conn = SqlitePool::connect(&db_path).await?;
|
||||
let conn = SqlitePool::connect(db_path).await?;
|
||||
|
||||
Ok(conn)
|
||||
}
|
||||
|
@ -83,11 +83,9 @@ async fn main() -> std::io::Result<()> {
|
||||
};
|
||||
|
||||
if let Some(conn) = args.listen {
|
||||
if let Ok(p) = db_path() {
|
||||
if !Path::new(&p).is_file() {
|
||||
error!("Database is not initialized! Init DB first and add admin user.");
|
||||
exit(1);
|
||||
}
|
||||
if db_path().is_err() {
|
||||
error!("Database is not initialized! Init DB first and add admin user.");
|
||||
exit(1);
|
||||
}
|
||||
init_config(&pool).await;
|
||||
let ip_port = conn.split(':').collect::<Vec<&str>>();
|
||||
|
@ -1,4 +1,4 @@
|
||||
use std::{fs, path::Path};
|
||||
use std::{fs, path::PathBuf};
|
||||
|
||||
use rand::prelude::*;
|
||||
use simplelog::*;
|
||||
@ -31,22 +31,14 @@ pub async fn create_channel(
|
||||
Err(_) => rand::thread_rng().gen_range(71..99),
|
||||
};
|
||||
|
||||
let mut config =
|
||||
PlayoutConfig::new(Some("/usr/share/ffplayout/ffplayout.yml.orig".to_string()));
|
||||
let mut config = PlayoutConfig::new(Some(PathBuf::from(
|
||||
"/usr/share/ffplayout/ffplayout.yml.orig",
|
||||
)));
|
||||
|
||||
config.general.stat_file = format!(".ffp_{channel_name}",);
|
||||
|
||||
config.logging.path = Path::new(&config.logging.path)
|
||||
.join(&channel_name)
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
|
||||
config.logging.path = config.logging.path.join(&channel_name);
|
||||
config.rpc_server.address = format!("127.0.0.1:70{:7>2}", channel_num);
|
||||
|
||||
config.playlist.path = Path::new(&config.playlist.path)
|
||||
.join(channel_name)
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
config.playlist.path = config.playlist.path.join(channel_name);
|
||||
|
||||
config.out.output_param = config
|
||||
.out
|
||||
|
@ -1,4 +1,8 @@
|
||||
use std::{fs, io::Write, path::PathBuf};
|
||||
use std::{
|
||||
fs,
|
||||
io::Write,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use actix_multipart::Multipart;
|
||||
use actix_web::{web, HttpResponse};
|
||||
@ -51,10 +55,9 @@ pub struct VideoFile {
|
||||
/// Normalize absolut path
|
||||
///
|
||||
/// This function takes care, that it is not possible to break out from root_path.
|
||||
/// It also gives alway a relative path back.
|
||||
pub fn norm_abs_path(root_path: &str, input_path: &str) -> (PathBuf, String, String) {
|
||||
let mut path = PathBuf::from(root_path);
|
||||
let path_relative = RelativePath::new(root_path)
|
||||
/// It also gives always a relative path back.
|
||||
pub fn norm_abs_path(root_path: &Path, input_path: &str) -> (PathBuf, String, String) {
|
||||
let path_relative = RelativePath::new(&root_path.to_string_lossy())
|
||||
.normalize()
|
||||
.to_string()
|
||||
.replace("../", "");
|
||||
@ -62,13 +65,15 @@ pub fn norm_abs_path(root_path: &str, input_path: &str) -> (PathBuf, String, Str
|
||||
.normalize()
|
||||
.to_string()
|
||||
.replace("../", "");
|
||||
let path_suffix = path
|
||||
let path_suffix = root_path
|
||||
.file_name()
|
||||
.unwrap_or_default()
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
|
||||
if input_path.starts_with(root_path) || source_relative.starts_with(&path_relative) {
|
||||
if input_path.starts_with(&*root_path.to_string_lossy())
|
||||
|| source_relative.starts_with(&path_relative)
|
||||
{
|
||||
source_relative = source_relative
|
||||
.strip_prefix(&path_relative)
|
||||
.and_then(|s| s.strip_prefix('/'))
|
||||
@ -82,9 +87,9 @@ pub fn norm_abs_path(root_path: &str, input_path: &str) -> (PathBuf, String, Str
|
||||
.to_string();
|
||||
}
|
||||
|
||||
path = path.join(&source_relative);
|
||||
let path = &root_path.join(&source_relative);
|
||||
|
||||
(path, path_suffix, source_relative)
|
||||
(path.to_path_buf(), path_suffix, source_relative)
|
||||
}
|
||||
|
||||
/// File Browser
|
||||
@ -300,7 +305,7 @@ pub async fn upload(
|
||||
conn: &Pool<Sqlite>,
|
||||
id: i32,
|
||||
mut payload: Multipart,
|
||||
path: &str,
|
||||
path: &Path,
|
||||
abs_path: bool,
|
||||
) -> Result<HttpResponse, ServiceError> {
|
||||
while let Some(mut field) = payload.try_next().await? {
|
||||
@ -318,9 +323,9 @@ pub async fn upload(
|
||||
let filepath;
|
||||
|
||||
if abs_path {
|
||||
filepath = PathBuf::from(path);
|
||||
filepath = path.to_path_buf();
|
||||
} else {
|
||||
let target_path = valid_path(conn, id, path).await?;
|
||||
let target_path = valid_path(conn, id, &path.to_string_lossy()).await?;
|
||||
filepath = target_path.join(filename);
|
||||
}
|
||||
|
||||
|
@ -74,21 +74,25 @@ pub async fn init_config(conn: &Pool<Sqlite>) {
|
||||
INSTANCE.set(config).unwrap();
|
||||
}
|
||||
|
||||
pub fn db_path() -> Result<String, Box<dyn std::error::Error>> {
|
||||
pub fn db_path() -> Result<&'static str, Box<dyn std::error::Error>> {
|
||||
let sys_path = Path::new("/usr/share/ffplayout/db");
|
||||
let mut db_path = "./ffplayout.db".to_string();
|
||||
let mut db_path = "./ffplayout.db";
|
||||
|
||||
if sys_path.is_dir() && !sys_path.writable() {
|
||||
error!("Path {} is not writable!", sys_path.display());
|
||||
}
|
||||
|
||||
if sys_path.is_dir() && sys_path.writable() {
|
||||
db_path = "/usr/share/ffplayout/db/ffplayout.db".to_string();
|
||||
db_path = "/usr/share/ffplayout/db/ffplayout.db";
|
||||
} else if Path::new("./assets").is_dir() {
|
||||
db_path = "./assets/ffplayout.db".to_string();
|
||||
db_path = "./assets/ffplayout.db";
|
||||
}
|
||||
|
||||
Ok(db_path)
|
||||
if Path::new(db_path).is_file() {
|
||||
return Ok(db_path);
|
||||
}
|
||||
|
||||
Err(format!("DB path {db_path} not exists!").into())
|
||||
}
|
||||
|
||||
pub async fn run_args(mut args: Args) -> Result<(), i32> {
|
||||
|
@ -3,7 +3,7 @@ use std::{fs, path::PathBuf};
|
||||
use simplelog::*;
|
||||
use sqlx::{Pool, Sqlite};
|
||||
|
||||
use crate::utils::{errors::ServiceError, playout_config};
|
||||
use crate::utils::{errors::ServiceError, files::norm_abs_path, playout_config};
|
||||
use ffplayout_lib::utils::{
|
||||
generate_playlist as playlist_generator, json_reader, json_writer, JsonPlaylist, PlayoutConfig,
|
||||
};
|
||||
@ -78,16 +78,32 @@ pub async fn write_playlist(
|
||||
}
|
||||
|
||||
pub async fn generate_playlist(
|
||||
config: PlayoutConfig,
|
||||
mut config: PlayoutConfig,
|
||||
channel: String,
|
||||
) -> Result<JsonPlaylist, ServiceError> {
|
||||
if let Some(mut template) = config.general.template.take() {
|
||||
for source in template.sources.iter_mut() {
|
||||
let mut paths = vec![];
|
||||
|
||||
for path in &source.paths {
|
||||
let (safe_path, _, _) =
|
||||
norm_abs_path(&config.storage.path, &path.to_string_lossy());
|
||||
paths.push(safe_path);
|
||||
}
|
||||
|
||||
source.paths = paths;
|
||||
}
|
||||
|
||||
config.general.template = Some(template);
|
||||
}
|
||||
|
||||
match playlist_generator(&config, Some(channel)) {
|
||||
Ok(playlists) => {
|
||||
if !playlists.is_empty() {
|
||||
Ok(playlists[0].clone())
|
||||
} else {
|
||||
Err(ServiceError::Conflict(
|
||||
"Playlist could not be written, possible already exists!".into(),
|
||||
"The playlist could not be written, maybe it already exists!".into(),
|
||||
))
|
||||
}
|
||||
}
|
||||
|
@ -16,8 +16,10 @@ chrono = { version = "0.4", default-features = false, features = ["clock", "std"
|
||||
clap = { version = "4.3", features = ["derive"] }
|
||||
crossbeam-channel = "0.5"
|
||||
futures = "0.3"
|
||||
itertools = "0.11"
|
||||
notify = "6.0"
|
||||
notify-debouncer-full = { version = "*", default-features = false }
|
||||
rand = "0.8"
|
||||
regex = "1"
|
||||
reqwest = { version = "0.11", features = ["blocking", "json"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
|
@ -28,8 +28,8 @@ pub fn source_generator(
|
||||
Folder => {
|
||||
info!("Playout in folder mode");
|
||||
debug!(
|
||||
"Monitor folder: <b><magenta>{}</></b>",
|
||||
&config.storage.path
|
||||
"Monitor folder: <b><magenta>{:?}</></b>",
|
||||
config.storage.path
|
||||
);
|
||||
|
||||
let config_clone = config.clone();
|
||||
|
@ -40,7 +40,7 @@ impl CurrentProgram {
|
||||
is_terminated: Arc<AtomicBool>,
|
||||
player_control: &PlayerControl,
|
||||
) -> Self {
|
||||
let json = read_json(config, None, is_terminated.clone(), true, 0.0);
|
||||
let json = read_json(config, None, is_terminated.clone(), true, false);
|
||||
|
||||
if let Some(file) = &json.current_file {
|
||||
info!("Read Playlist: <b><magenta>{}</></b>", file);
|
||||
@ -78,7 +78,7 @@ impl CurrentProgram {
|
||||
fn check_update(&mut self, seek: bool) {
|
||||
if self.json_path.is_none() {
|
||||
// If the playlist was missing, we check here to see if it came back.
|
||||
let json = read_json(&self.config, None, self.is_terminated.clone(), seek, 0.0);
|
||||
let json = read_json(&self.config, None, self.is_terminated.clone(), seek, false);
|
||||
|
||||
if let Some(file) = &json.current_file {
|
||||
info!("Read Playlist: <b><magenta>{file}</></b>");
|
||||
@ -105,7 +105,7 @@ impl CurrentProgram {
|
||||
self.json_path.clone(),
|
||||
self.is_terminated.clone(),
|
||||
false,
|
||||
0.0,
|
||||
false,
|
||||
);
|
||||
|
||||
self.json_mod = json.modified;
|
||||
@ -133,17 +133,20 @@ impl CurrentProgram {
|
||||
}
|
||||
|
||||
// Check if day is past and it is time for a new playlist.
|
||||
fn check_for_next_playlist(&mut self) {
|
||||
fn check_for_next_playlist(&mut self) -> bool {
|
||||
let current_time = get_sec();
|
||||
let start_sec = self.config.playlist.start_sec.unwrap();
|
||||
let target_length = self.config.playlist.length_sec.unwrap();
|
||||
let (delta, total_delta) = get_delta(&self.config, ¤t_time);
|
||||
let mut duration = self.current_node.out;
|
||||
let mut next = false;
|
||||
|
||||
if self.current_node.duration > self.current_node.out {
|
||||
duration = self.current_node.duration
|
||||
}
|
||||
|
||||
trace!("delta: {delta}, total_delta: {total_delta}");
|
||||
|
||||
let mut next_start =
|
||||
self.current_node.begin.unwrap_or_default() - start_sec + duration + delta;
|
||||
|
||||
@ -153,21 +156,20 @@ impl CurrentProgram {
|
||||
next_start += self.config.general.stop_threshold;
|
||||
}
|
||||
|
||||
trace!("next_start: {next_start}, target_length: {target_length}");
|
||||
|
||||
// Check if we over the target length or we are close to it, if so we load the next playlist.
|
||||
if next_start >= target_length
|
||||
|| is_close(total_delta, 0.0, 2.0)
|
||||
|| is_close(total_delta, target_length, 2.0)
|
||||
{
|
||||
let json = read_json(
|
||||
&self.config,
|
||||
None,
|
||||
self.is_terminated.clone(),
|
||||
false,
|
||||
next_start,
|
||||
);
|
||||
trace!("get next day");
|
||||
next = true;
|
||||
|
||||
let json = read_json(&self.config, None, self.is_terminated.clone(), false, true);
|
||||
|
||||
if let Some(file) = &json.current_file {
|
||||
info!("Read Playlist: <b><magenta>{}</></b>", file);
|
||||
info!("Read next Playlist: <b><magenta>{}</></b>", file);
|
||||
}
|
||||
|
||||
let data = json!({
|
||||
@ -192,8 +194,12 @@ impl CurrentProgram {
|
||||
|
||||
if json.current_file.is_none() {
|
||||
self.playout_stat.list_init.store(true, Ordering::SeqCst);
|
||||
} else {
|
||||
self.playout_stat.list_init.store(false, Ordering::SeqCst);
|
||||
}
|
||||
}
|
||||
|
||||
next
|
||||
}
|
||||
|
||||
// Check if last and/or next clip is a advertisement.
|
||||
@ -257,6 +263,7 @@ impl CurrentProgram {
|
||||
|
||||
// Prepare init clip.
|
||||
fn init_clip(&mut self) {
|
||||
trace!("init_clip");
|
||||
self.get_current_clip();
|
||||
|
||||
if !self.playout_stat.list_init.load(Ordering::SeqCst) {
|
||||
@ -307,47 +314,48 @@ impl Iterator for CurrentProgram {
|
||||
let new_length = new_node.begin.unwrap_or_default() + new_node.duration;
|
||||
trace!("Init playlist after playlist end");
|
||||
|
||||
self.check_for_next_playlist();
|
||||
let next_playlist = self.check_for_next_playlist();
|
||||
|
||||
if new_length
|
||||
>= self.config.playlist.length_sec.unwrap()
|
||||
+ self.config.playlist.start_sec.unwrap()
|
||||
{
|
||||
self.init_clip();
|
||||
} else if next_playlist
|
||||
&& self.player_control.current_list.lock().unwrap().len() > 1
|
||||
{
|
||||
let index = self
|
||||
.player_control
|
||||
.current_index
|
||||
.fetch_add(1, Ordering::SeqCst);
|
||||
|
||||
self.current_node = gen_source(
|
||||
&self.config,
|
||||
self.player_control.current_list.lock().unwrap()[index].clone(),
|
||||
&self.playout_stat.chain,
|
||||
&self.player_control,
|
||||
0,
|
||||
);
|
||||
|
||||
return Some(self.current_node.clone());
|
||||
} else {
|
||||
// fill missing length from playlist
|
||||
let mut current_time = get_sec();
|
||||
let (_, total_delta) = get_delta(&self.config, ¤t_time);
|
||||
let mut out = total_delta.abs();
|
||||
let mut duration = out + 0.001;
|
||||
|
||||
trace!("Total delta on list init: {total_delta}");
|
||||
|
||||
let filler_index = self.player_control.filler_index.load(Ordering::SeqCst);
|
||||
let mut filler =
|
||||
self.player_control.filler_list.lock().unwrap()[filler_index].clone();
|
||||
|
||||
filler.add_probe();
|
||||
|
||||
// If there is no filler, the duration of the dummy clip should not be too long.
|
||||
// This would take away the ability to restart the playlist when the playout registers a change.
|
||||
if filler.duration > 0.0 {
|
||||
if duration > filler.duration {
|
||||
out = filler.duration;
|
||||
}
|
||||
|
||||
duration = filler.duration;
|
||||
} else if DUMMY_LEN > total_delta {
|
||||
duration = total_delta;
|
||||
out = total_delta;
|
||||
let out = if DUMMY_LEN > total_delta {
|
||||
total_delta
|
||||
} else {
|
||||
duration = DUMMY_LEN;
|
||||
out = DUMMY_LEN;
|
||||
}
|
||||
DUMMY_LEN
|
||||
};
|
||||
|
||||
let duration = out + 0.001;
|
||||
|
||||
if self.json_path.is_some() {
|
||||
// When playlist is missing, we always need to init the playlist the next iteration.
|
||||
self.playout_stat.list_init.store(false, Ordering::SeqCst);
|
||||
self.playout_stat.list_init.store(true, Ordering::SeqCst);
|
||||
}
|
||||
|
||||
if self.config.playlist.start_sec.unwrap() > current_time {
|
||||
@ -429,28 +437,14 @@ impl Iterator for CurrentProgram {
|
||||
let index = self.player_control.current_index.load(Ordering::SeqCst);
|
||||
self.current_node = Media::new(index, "", false);
|
||||
self.current_node.begin = Some(get_sec());
|
||||
let mut out = total_delta.abs();
|
||||
let mut duration = out + 0.001;
|
||||
|
||||
let filler_index = self.player_control.filler_index.load(Ordering::SeqCst);
|
||||
let mut filler =
|
||||
self.player_control.filler_list.lock().unwrap()[filler_index].clone();
|
||||
|
||||
filler.add_probe();
|
||||
|
||||
if filler.duration > 0.0 {
|
||||
if duration > filler.duration {
|
||||
out = filler.duration;
|
||||
}
|
||||
|
||||
duration = filler.duration;
|
||||
} else if DUMMY_LEN > total_delta {
|
||||
duration = total_delta;
|
||||
out = total_delta;
|
||||
let out = if DUMMY_LEN > total_delta {
|
||||
total_delta
|
||||
} else {
|
||||
duration = DUMMY_LEN;
|
||||
out = DUMMY_LEN;
|
||||
}
|
||||
DUMMY_LEN
|
||||
};
|
||||
|
||||
let duration = out + 0.001;
|
||||
|
||||
self.current_node.duration = duration;
|
||||
self.current_node.out = out;
|
||||
@ -605,7 +599,7 @@ pub fn gen_source(
|
||||
error!("Source not found: <b><magenta>\"{}\"</></b>", node.source);
|
||||
}
|
||||
|
||||
let filler_source = Path::new(&config.storage.filler);
|
||||
let filler_source = &config.storage.filler;
|
||||
|
||||
if filler_source.is_dir() && !player_control.filler_list.lock().unwrap().is_empty() {
|
||||
let filler_index = player_control.filler_index.fetch_add(1, Ordering::SeqCst);
|
||||
@ -629,17 +623,19 @@ pub fn gen_source(
|
||||
node.cmd = Some(loop_filler(&node));
|
||||
node.probe = filler_media.probe;
|
||||
} else if filler_source.is_file() {
|
||||
let probe = MediaProbe::new(&config.storage.filler);
|
||||
let probe = MediaProbe::new(&config.storage.filler.to_string_lossy());
|
||||
|
||||
if config
|
||||
.storage
|
||||
.filler
|
||||
.to_string_lossy()
|
||||
.to_string()
|
||||
.rsplit_once('.')
|
||||
.map(|(_, e)| e.to_lowercase())
|
||||
.filter(|c| IMAGE_FORMAT.contains(&c.as_str()))
|
||||
.is_some()
|
||||
{
|
||||
node.source = config.storage.filler.clone();
|
||||
node.source = config.storage.filler.clone().to_string_lossy().to_string();
|
||||
node.cmd = Some(loop_image(&node));
|
||||
node.probe = Some(probe);
|
||||
} else if let Some(filler_duration) = probe
|
||||
@ -649,7 +645,7 @@ pub fn gen_source(
|
||||
.and_then(|d| d.parse::<f64>().ok())
|
||||
{
|
||||
// Create placeholder from config filler.
|
||||
node.source = config.storage.filler.clone();
|
||||
node.source = config.storage.filler.clone().to_string_lossy().to_string();
|
||||
|
||||
node.out = if node.duration > duration && filler_duration > duration {
|
||||
duration
|
||||
|
@ -1,6 +1,6 @@
|
||||
use std::{
|
||||
fs::{self, File},
|
||||
path::{Path, PathBuf},
|
||||
path::PathBuf,
|
||||
process::exit,
|
||||
sync::{atomic::AtomicBool, Arc, Mutex},
|
||||
thread,
|
||||
@ -107,7 +107,7 @@ fn main() {
|
||||
let messages = Arc::new(Mutex::new(Vec::new()));
|
||||
|
||||
// try to create logging folder, if not exist
|
||||
if config.logging.log_to_file && !Path::new(&config.logging.path).is_dir() {
|
||||
if config.logging.log_to_file && config.logging.path.is_dir() {
|
||||
if let Err(e) = fs::create_dir_all(&config.logging.path) {
|
||||
println!("Logging path not exists! {e}");
|
||||
|
||||
@ -163,11 +163,11 @@ fn main() {
|
||||
}
|
||||
|
||||
if args.validate {
|
||||
let mut playlist_path = Path::new(&config.playlist.path).to_owned();
|
||||
let mut playlist_path = config.playlist.path.clone();
|
||||
let start_sec = config.playlist.start_sec.unwrap();
|
||||
let date = get_date(false, start_sec, 0.0);
|
||||
let date = get_date(false, start_sec, false);
|
||||
|
||||
if playlist_path.is_dir() || is_remote(&config.playlist.path) {
|
||||
if playlist_path.is_dir() || is_remote(&playlist_path.to_string_lossy()) {
|
||||
let d: Vec<&str> = date.split('-').collect();
|
||||
playlist_path = playlist_path
|
||||
.join(d[0])
|
||||
@ -213,7 +213,9 @@ fn main() {
|
||||
);
|
||||
|
||||
// Fill filler list, can also be a single file.
|
||||
thread::spawn(move || fill_filler_list(config_clone2, play_ctl2));
|
||||
thread::spawn(move || {
|
||||
fill_filler_list(&config_clone2, Some(play_ctl2));
|
||||
});
|
||||
|
||||
match config.out.mode {
|
||||
// write files/playlist to HLS m3u8 playlist
|
||||
|
@ -1,6 +1,5 @@
|
||||
use std::{
|
||||
io::{prelude::*, BufReader, BufWriter, Read},
|
||||
path::Path,
|
||||
process::{Command, Stdio},
|
||||
sync::atomic::Ordering,
|
||||
thread::{self, sleep},
|
||||
@ -101,11 +100,11 @@ pub fn player(
|
||||
let task_node = node.clone();
|
||||
let server_running = proc_control.server_is_running.load(Ordering::SeqCst);
|
||||
|
||||
if Path::new(&config.task.path).is_file() {
|
||||
if config.task.path.is_file() {
|
||||
thread::spawn(move || task_runner::run(task_config, task_node, server_running));
|
||||
} else {
|
||||
error!(
|
||||
"<bright-blue>{}</> executable not exists!",
|
||||
"<bright-blue>{:?}</> executable not exists!",
|
||||
config.task.path
|
||||
);
|
||||
}
|
||||
|
@ -1,3 +1,5 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use clap::Parser;
|
||||
|
||||
use ffplayout_lib::utils::{OutputMode, ProcessMode};
|
||||
@ -13,10 +15,24 @@ pub struct Args {
|
||||
pub channel: Option<String>,
|
||||
|
||||
#[clap(short, long, help = "File path to ffplayout.yml")]
|
||||
pub config: Option<String>,
|
||||
pub config: Option<PathBuf>,
|
||||
|
||||
#[clap(short, long, help = "File path for logging")]
|
||||
pub log: Option<String>,
|
||||
pub log: Option<PathBuf>,
|
||||
|
||||
#[clap(
|
||||
short,
|
||||
long,
|
||||
help = "Target date (YYYY-MM-DD) for text/m3u to playlist import"
|
||||
)]
|
||||
pub date: Option<String>,
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
#[clap(long, help = "fake date time, for debugging")]
|
||||
pub fake_time: Option<String>,
|
||||
|
||||
#[clap(short, long, help = "Play folder content")]
|
||||
pub folder: Option<PathBuf>,
|
||||
|
||||
#[clap(
|
||||
short,
|
||||
@ -27,37 +43,14 @@ pub struct Args {
|
||||
)]
|
||||
pub generate: Option<Vec<String>>,
|
||||
|
||||
#[clap(long, help = "Optional path list for playlist generations", num_args = 1..)]
|
||||
pub paths: Option<Vec<String>>,
|
||||
|
||||
#[clap(short = 'm', long, help = "Playing mode: folder, playlist")]
|
||||
pub play_mode: Option<ProcessMode>,
|
||||
|
||||
#[clap(short, long, help = "Play folder content")]
|
||||
pub folder: Option<String>,
|
||||
|
||||
#[clap(
|
||||
short,
|
||||
long,
|
||||
help = "Target date (YYYY-MM-DD) for text/m3u to playlist import"
|
||||
)]
|
||||
pub date: Option<String>,
|
||||
|
||||
#[clap(
|
||||
long,
|
||||
help = "Import a given text/m3u file and create a playlist from it"
|
||||
)]
|
||||
pub import: Option<String>,
|
||||
pub import: Option<PathBuf>,
|
||||
|
||||
#[clap(short, long, help = "Path to playlist, or playlist root folder.")]
|
||||
pub playlist: Option<String>,
|
||||
|
||||
#[clap(
|
||||
short,
|
||||
long,
|
||||
help = "Start time in 'hh:mm:ss', 'now' for start with first"
|
||||
)]
|
||||
pub start: Option<String>,
|
||||
#[clap(short, long, help = "Loop playlist infinitely")]
|
||||
pub infinit: bool,
|
||||
|
||||
#[clap(
|
||||
short = 't',
|
||||
@ -69,8 +62,24 @@ pub struct Args {
|
||||
#[clap(long, help = "Override logging level")]
|
||||
pub level: Option<String>,
|
||||
|
||||
#[clap(short, long, help = "Loop playlist infinitely")]
|
||||
pub infinit: bool,
|
||||
#[clap(long, help = "Optional path list for playlist generations", num_args = 1..)]
|
||||
pub paths: Option<Vec<PathBuf>>,
|
||||
|
||||
#[clap(short = 'm', long, help = "Playing mode: folder, playlist")]
|
||||
pub play_mode: Option<ProcessMode>,
|
||||
|
||||
#[clap(short, long, help = "Path to playlist, or playlist root folder.")]
|
||||
pub playlist: Option<PathBuf>,
|
||||
|
||||
#[clap(
|
||||
short,
|
||||
long,
|
||||
help = "Start time in 'hh:mm:ss', 'now' for start with first"
|
||||
)]
|
||||
pub start: Option<String>,
|
||||
|
||||
#[clap(short = 'T', long, help = "JSON Template file for generating playlist")]
|
||||
pub template: Option<PathBuf>,
|
||||
|
||||
#[clap(short, long, help = "Set output mode: desktop, hls, null, stream")]
|
||||
pub output: Option<OutputMode>,
|
||||
@ -78,10 +87,6 @@ pub struct Args {
|
||||
#[clap(short, long, help = "Set audio volume")]
|
||||
pub volume: Option<f64>,
|
||||
|
||||
#[cfg(debug_assertions)]
|
||||
#[clap(long, help = "fake date time, for debugging")]
|
||||
pub fake_time: Option<String>,
|
||||
|
||||
#[clap(long, help = "validate given playlist")]
|
||||
pub validate: bool,
|
||||
}
|
||||
|
@ -1,7 +1,4 @@
|
||||
use std::{
|
||||
path::{Path, PathBuf},
|
||||
process::exit,
|
||||
};
|
||||
use std::{fs::File, path::PathBuf, process::exit};
|
||||
|
||||
use regex::Regex;
|
||||
use serde_json::{json, Map, Value};
|
||||
@ -14,8 +11,8 @@ pub use arg_parse::Args;
|
||||
use ffplayout_lib::{
|
||||
filter::Filters,
|
||||
utils::{
|
||||
get_sec, parse_log_level_filter, sec_to_time, time_to_sec, Media, OutputMode::*,
|
||||
PlayoutConfig, ProcessMode::*,
|
||||
config::Template, get_sec, parse_log_level_filter, sec_to_time, time_to_sec, Media,
|
||||
OutputMode::*, PlayoutConfig, ProcessMode::*,
|
||||
},
|
||||
vec_strings,
|
||||
};
|
||||
@ -33,7 +30,7 @@ pub fn get_config(args: Args) -> PlayoutConfig {
|
||||
exit(1)
|
||||
}
|
||||
|
||||
Some(path.display().to_string())
|
||||
Some(path)
|
||||
}
|
||||
None => args.config,
|
||||
};
|
||||
@ -48,12 +45,33 @@ pub fn get_config(args: Args) -> PlayoutConfig {
|
||||
config.general.validate = true;
|
||||
}
|
||||
|
||||
if let Some(template_file) = args.template {
|
||||
let f = File::options()
|
||||
.read(true)
|
||||
.write(false)
|
||||
.open(template_file)
|
||||
.expect("JSON template file");
|
||||
|
||||
let mut template: Template = match serde_json::from_reader(f) {
|
||||
Ok(p) => p,
|
||||
Err(e) => {
|
||||
error!("Template file not readable! {e}");
|
||||
|
||||
exit(1)
|
||||
}
|
||||
};
|
||||
|
||||
template.sources.sort_by(|d1, d2| d1.start.cmp(&d2.start));
|
||||
|
||||
config.general.template = Some(template);
|
||||
}
|
||||
|
||||
if let Some(paths) = args.paths {
|
||||
config.storage.paths = paths;
|
||||
}
|
||||
|
||||
if let Some(log_path) = args.log {
|
||||
if Path::new(&log_path).is_dir() {
|
||||
if log_path.is_dir() {
|
||||
config.logging.log_to_file = true;
|
||||
}
|
||||
config.logging.path = log_path;
|
||||
|
@ -9,7 +9,7 @@ repository.workspace = true
|
||||
edition.workspace = true
|
||||
|
||||
[dependencies]
|
||||
chrono = { version = "0.4", default-features = false, features = ["clock", "std"] }
|
||||
chrono = { version = "0.4", default-features = false, features = ["clock", "serde", "std"] }
|
||||
crossbeam-channel = "0.5"
|
||||
ffprobe = "0.3"
|
||||
file-rotate = "0.7"
|
||||
|
@ -6,6 +6,7 @@ use std::{
|
||||
str::FromStr,
|
||||
};
|
||||
|
||||
use chrono::NaiveTime;
|
||||
use log::LevelFilter;
|
||||
use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
|
||||
use shlex::split;
|
||||
@ -124,6 +125,19 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct Template {
|
||||
pub sources: Vec<Source>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct Source {
|
||||
pub start: NaiveTime,
|
||||
pub duration: NaiveTime,
|
||||
pub shuffle: bool,
|
||||
pub paths: Vec<PathBuf>,
|
||||
}
|
||||
|
||||
/// Global Config
|
||||
///
|
||||
/// This we init ones, when ffplayout is starting and use them globally in the hole program.
|
||||
@ -163,6 +177,9 @@ pub struct General {
|
||||
#[serde(skip_serializing, skip_deserializing)]
|
||||
pub ffmpeg_libs: Vec<String>,
|
||||
|
||||
#[serde(skip_serializing, skip_deserializing)]
|
||||
pub template: Option<Template>,
|
||||
|
||||
#[serde(default, skip_serializing, skip_deserializing)]
|
||||
pub validate: bool,
|
||||
}
|
||||
@ -196,7 +213,7 @@ pub struct Logging {
|
||||
pub local_time: bool,
|
||||
pub timestamp: bool,
|
||||
#[serde(alias = "log_path")]
|
||||
pub path: String,
|
||||
pub path: PathBuf,
|
||||
#[serde(
|
||||
alias = "log_level",
|
||||
serialize_with = "log_level_to_string",
|
||||
@ -255,7 +272,7 @@ pub struct Ingest {
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct Playlist {
|
||||
pub help_text: String,
|
||||
pub path: String,
|
||||
pub path: PathBuf,
|
||||
pub day_start: String,
|
||||
|
||||
#[serde(skip_serializing, skip_deserializing)]
|
||||
@ -272,11 +289,11 @@ pub struct Playlist {
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct Storage {
|
||||
pub help_text: String,
|
||||
pub path: String,
|
||||
pub path: PathBuf,
|
||||
#[serde(skip_serializing, skip_deserializing)]
|
||||
pub paths: Vec<String>,
|
||||
pub paths: Vec<PathBuf>,
|
||||
#[serde(alias = "filler_clip")]
|
||||
pub filler: String,
|
||||
pub filler: PathBuf,
|
||||
pub extensions: Vec<String>,
|
||||
pub shuffle: bool,
|
||||
}
|
||||
@ -304,7 +321,7 @@ pub struct Text {
|
||||
#[derive(Debug, Default, Serialize, Deserialize, Clone)]
|
||||
pub struct Task {
|
||||
pub enable: bool,
|
||||
pub path: String,
|
||||
pub path: PathBuf,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
@ -335,11 +352,11 @@ fn default_channels() -> u8 {
|
||||
|
||||
impl PlayoutConfig {
|
||||
/// Read config from YAML file, and set some extra config values.
|
||||
pub fn new(cfg_path: Option<String>) -> Self {
|
||||
pub fn new(cfg_path: Option<PathBuf>) -> Self {
|
||||
let mut config_path = PathBuf::from("/etc/ffplayout/ffplayout.yml");
|
||||
|
||||
if let Some(cfg) = cfg_path {
|
||||
config_path = PathBuf::from(cfg);
|
||||
config_path = cfg;
|
||||
}
|
||||
|
||||
if !config_path.is_file() {
|
||||
|
@ -184,7 +184,7 @@ impl PlayerControl {
|
||||
Self {
|
||||
current_media: Arc::new(Mutex::new(None)),
|
||||
current_list: Arc::new(Mutex::new(vec![Media::new(0, "", false)])),
|
||||
filler_list: Arc::new(Mutex::new(vec![Media::new(0, "", false)])),
|
||||
filler_list: Arc::new(Mutex::new(vec![])),
|
||||
current_index: Arc::new(AtomicUsize::new(0)),
|
||||
filler_index: Arc::new(AtomicUsize::new(0)),
|
||||
}
|
||||
|
@ -1,9 +1,6 @@
|
||||
use std::{
|
||||
path::Path,
|
||||
sync::{
|
||||
atomic::Ordering,
|
||||
{Arc, Mutex},
|
||||
},
|
||||
use std::sync::{
|
||||
atomic::Ordering,
|
||||
{Arc, Mutex},
|
||||
};
|
||||
|
||||
use rand::{seq::SliceRandom, thread_rng};
|
||||
@ -37,18 +34,18 @@ impl FolderSource {
|
||||
|
||||
if config.general.generate.is_some() && !config.storage.paths.is_empty() {
|
||||
for path in &config.storage.paths {
|
||||
path_list.push(path.clone())
|
||||
path_list.push(path)
|
||||
}
|
||||
} else {
|
||||
path_list.push(config.storage.path.clone())
|
||||
path_list.push(&config.storage.path)
|
||||
}
|
||||
|
||||
for path in &path_list {
|
||||
if !Path::new(path).is_dir() {
|
||||
error!("Path not exists: <b><magenta>{path}</></b>");
|
||||
if !path.is_dir() {
|
||||
error!("Path not exists: <b><magenta>{path:?}</></b>");
|
||||
}
|
||||
|
||||
for entry in WalkDir::new(path.clone())
|
||||
for entry in WalkDir::new(path)
|
||||
.into_iter()
|
||||
.flat_map(|e| e.ok())
|
||||
.filter(|f| f.path().is_file())
|
||||
@ -90,6 +87,22 @@ impl FolderSource {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_list(
|
||||
config: &PlayoutConfig,
|
||||
filter_chain: Option<Arc<Mutex<Vec<String>>>>,
|
||||
player_control: &PlayerControl,
|
||||
list: Vec<Media>,
|
||||
) -> Self {
|
||||
*player_control.current_list.lock().unwrap() = list;
|
||||
|
||||
Self {
|
||||
config: config.clone(),
|
||||
filter_chain,
|
||||
player_control: player_control.clone(),
|
||||
current_node: Media::new(0, "", false),
|
||||
}
|
||||
}
|
||||
|
||||
fn shuffle(&mut self) {
|
||||
let mut rng = thread_rng();
|
||||
let mut nodes = self.player_control.current_list.lock().unwrap();
|
||||
@ -160,23 +173,30 @@ impl Iterator for FolderSource {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn fill_filler_list(config: PlayoutConfig, player_control: PlayerControl) {
|
||||
pub fn fill_filler_list(
|
||||
config: &PlayoutConfig,
|
||||
player_control: Option<PlayerControl>,
|
||||
) -> Vec<Media> {
|
||||
let mut filler_list = vec![];
|
||||
let filler_path = &config.storage.filler;
|
||||
|
||||
if Path::new(&config.storage.filler).is_dir() {
|
||||
debug!(
|
||||
"Fill filler list from: <b><magenta>{}</></b>",
|
||||
config.storage.filler
|
||||
);
|
||||
|
||||
if filler_path.is_dir() {
|
||||
for (index, entry) in WalkDir::new(&config.storage.filler)
|
||||
.into_iter()
|
||||
.flat_map(|e| e.ok())
|
||||
.filter(|f| f.path().is_file())
|
||||
.filter(|f| include_file_extension(&config, f.path()))
|
||||
.filter(|f| include_file_extension(config, f.path()))
|
||||
.enumerate()
|
||||
{
|
||||
filler_list.push(Media::new(index, &entry.path().to_string_lossy(), false));
|
||||
let mut media = Media::new(index, &entry.path().to_string_lossy(), false);
|
||||
|
||||
if let Some(control) = player_control.as_ref() {
|
||||
control.filler_list.lock().unwrap().push(media);
|
||||
} else {
|
||||
media.add_probe();
|
||||
|
||||
filler_list.push(media);
|
||||
}
|
||||
}
|
||||
|
||||
if config.storage.shuffle {
|
||||
@ -190,9 +210,17 @@ pub fn fill_filler_list(config: PlayoutConfig, player_control: PlayerControl) {
|
||||
for (index, item) in filler_list.iter_mut().enumerate() {
|
||||
item.index = Some(index);
|
||||
}
|
||||
} else {
|
||||
filler_list.push(Media::new(0, &config.storage.filler, false));
|
||||
} else if filler_path.is_file() {
|
||||
let mut media = Media::new(0, &config.storage.filler.to_string_lossy(), false);
|
||||
|
||||
if let Some(control) = player_control.as_ref() {
|
||||
control.filler_list.lock().unwrap().push(media);
|
||||
} else {
|
||||
media.add_probe();
|
||||
|
||||
filler_list.push(media);
|
||||
}
|
||||
}
|
||||
|
||||
*player_control.filler_list.lock().unwrap() = filler_list;
|
||||
filler_list
|
||||
}
|
||||
|
@ -4,22 +4,196 @@
|
||||
///
|
||||
/// The generator takes the files from storage, which are set in config.
|
||||
/// It also respect the shuffle/sort mode.
|
||||
///
|
||||
/// Beside that it is really very basic, without any logic.
|
||||
use std::{
|
||||
fs::{create_dir_all, write},
|
||||
io::Error,
|
||||
path::Path,
|
||||
process::exit,
|
||||
};
|
||||
|
||||
use chrono::Timelike;
|
||||
use rand::{seq::SliceRandom, thread_rng, Rng};
|
||||
use simplelog::*;
|
||||
use walkdir::WalkDir;
|
||||
|
||||
use super::{folder::FolderSource, PlayerControl};
|
||||
use crate::utils::{
|
||||
get_date_range, json_serializer::JsonPlaylist, time_to_sec, Media, PlayoutConfig,
|
||||
folder::fill_filler_list, gen_dummy, get_date_range, include_file_extension,
|
||||
json_serializer::JsonPlaylist, time_to_sec, Media, PlayoutConfig, Template,
|
||||
};
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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 full_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>");
|
||||
|
||||
for entry in WalkDir::new(path)
|
||||
.into_iter()
|
||||
.flat_map(|e| e.ok())
|
||||
.filter(|f| f.path().is_file())
|
||||
.filter(|f| include_file_extension(config, f.path()))
|
||||
{
|
||||
let media = Media::new(0, &entry.path().to_string_lossy(), true);
|
||||
full_list.push(media);
|
||||
}
|
||||
}
|
||||
|
||||
let mut timed_list = if source.shuffle {
|
||||
full_list.shuffle(&mut rng);
|
||||
|
||||
random_list(full_list, duration)
|
||||
} else {
|
||||
full_list.sort_by(|d1, d2| d1.source.cmp(&d2.source));
|
||||
|
||||
ordered_list(full_list, duration)
|
||||
};
|
||||
|
||||
let total_length = sum_durations(&timed_list);
|
||||
|
||||
if duration > total_length {
|
||||
let mut filler = filler_list(config, duration - total_length);
|
||||
|
||||
timed_list.append(&mut filler);
|
||||
}
|
||||
|
||||
media_list.append(&mut timed_list);
|
||||
}
|
||||
|
||||
for item in media_list.iter_mut() {
|
||||
item.index = Some(index);
|
||||
|
||||
index += 1;
|
||||
}
|
||||
|
||||
FolderSource::from_list(config, None, player_control, media_list)
|
||||
}
|
||||
|
||||
/// Generate playlists
|
||||
pub fn generate_playlist(
|
||||
config: &PlayoutConfig,
|
||||
@ -36,9 +210,10 @@ pub fn generate_playlist(
|
||||
}
|
||||
};
|
||||
let player_control = PlayerControl::new();
|
||||
let playlist_root = Path::new(&config.playlist.path);
|
||||
let playlist_root = &config.playlist.path;
|
||||
let mut playlists = vec![];
|
||||
let mut date_range = vec![];
|
||||
let mut from_template = false;
|
||||
|
||||
let channel = match channel_name {
|
||||
Some(name) => name,
|
||||
@ -47,8 +222,8 @@ pub fn generate_playlist(
|
||||
|
||||
if !playlist_root.is_dir() {
|
||||
error!(
|
||||
"Playlist folder <b><magenta>{}</></b> not exists!",
|
||||
&config.playlist.path
|
||||
"Playlist folder <b><magenta>{:?}</></b> not exists!",
|
||||
config.playlist.path
|
||||
);
|
||||
|
||||
exit(1);
|
||||
@ -62,8 +237,16 @@ pub fn generate_playlist(
|
||||
date_range = get_date_range(&date_range)
|
||||
}
|
||||
|
||||
let media_list = FolderSource::new(config, None, &player_control);
|
||||
let list_length = media_list.player_control.current_list.lock().unwrap().len();
|
||||
// gives an iterator with infinit length
|
||||
let folder_iter = if let Some(template) = &config.general.template {
|
||||
from_template = true;
|
||||
|
||||
generate_from_template(config, &player_control, template.clone())
|
||||
} else {
|
||||
FolderSource::new(config, None, &player_control)
|
||||
};
|
||||
|
||||
let list_length = player_control.current_list.lock().unwrap().len();
|
||||
|
||||
for date in date_range {
|
||||
let d: Vec<&str> = date.split('-').collect();
|
||||
@ -71,6 +254,8 @@ pub fn generate_playlist(
|
||||
let month = d[1];
|
||||
let playlist_path = playlist_root.join(year).join(month);
|
||||
let playlist_file = &playlist_path.join(format!("{date}.json"));
|
||||
let mut length = 0.0;
|
||||
let mut round = 0;
|
||||
|
||||
create_dir_all(playlist_path)?;
|
||||
|
||||
@ -88,12 +273,6 @@ pub fn generate_playlist(
|
||||
playlist_file.display()
|
||||
);
|
||||
|
||||
// TODO: handle filler folder
|
||||
let mut filler = Media::new(0, &config.storage.filler, true);
|
||||
let filler_length = filler.duration;
|
||||
let mut length = 0.0;
|
||||
let mut round = 0;
|
||||
|
||||
let mut playlist = JsonPlaylist {
|
||||
channel: channel.clone(),
|
||||
date,
|
||||
@ -103,30 +282,38 @@ pub fn generate_playlist(
|
||||
program: vec![],
|
||||
};
|
||||
|
||||
for item in media_list.clone() {
|
||||
let duration = item.duration;
|
||||
if from_template {
|
||||
let media_list = player_control.current_list.lock().unwrap();
|
||||
playlist.program = media_list.to_vec();
|
||||
} else {
|
||||
for item in folder_iter.clone() {
|
||||
let duration = item.duration;
|
||||
|
||||
if total_length > length + duration {
|
||||
playlist.program.push(item);
|
||||
if total_length >= length + duration {
|
||||
playlist.program.push(item);
|
||||
|
||||
length += duration;
|
||||
} else if filler_length > 0.0 && filler_length > total_length - length {
|
||||
filler.out = total_length - length;
|
||||
playlist.program.push(filler);
|
||||
length += duration;
|
||||
} else if round == list_length - 1 {
|
||||
break;
|
||||
} else {
|
||||
round += 1;
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
} else if round == list_length - 1 {
|
||||
break;
|
||||
} else {
|
||||
round += 1;
|
||||
let list_duration = sum_durations(&playlist.program);
|
||||
|
||||
if config.playlist.length_sec.unwrap() > list_duration {
|
||||
let time_left = config.playlist.length_sec.unwrap() - list_duration;
|
||||
let mut fillers = filler_list(config, time_left);
|
||||
|
||||
playlist.program.append(&mut fillers);
|
||||
}
|
||||
}
|
||||
|
||||
playlists.push(playlist.clone());
|
||||
|
||||
let json: String = serde_json::to_string_pretty(&playlist)?;
|
||||
|
||||
write(playlist_file, json)?;
|
||||
|
||||
playlists.push(playlist);
|
||||
}
|
||||
|
||||
Ok(playlists)
|
||||
|
@ -12,7 +12,7 @@ pub fn import_file(
|
||||
config: &PlayoutConfig,
|
||||
date: &str,
|
||||
channel_name: Option<String>,
|
||||
path: &str,
|
||||
path: &Path,
|
||||
) -> Result<String, Error> {
|
||||
let file = File::open(path)?;
|
||||
let reader = BufReader::new(file);
|
||||
@ -25,13 +25,13 @@ pub fn import_file(
|
||||
program: vec![],
|
||||
};
|
||||
|
||||
let playlist_root = Path::new(&config.playlist.path);
|
||||
let playlist_root = &config.playlist.path;
|
||||
if !playlist_root.is_dir() {
|
||||
return Err(Error::new(
|
||||
ErrorKind::Other,
|
||||
format!(
|
||||
"Playlist folder <b><magenta>{}</></b> not exists!",
|
||||
&config.playlist.path,
|
||||
"Playlist folder <b><magenta>{:?}</></b> not exists!",
|
||||
config.playlist.path,
|
||||
),
|
||||
));
|
||||
}
|
||||
|
@ -142,14 +142,14 @@ pub fn read_json(
|
||||
path: Option<String>,
|
||||
is_terminated: Arc<AtomicBool>,
|
||||
seek: bool,
|
||||
next_start: f64,
|
||||
get_next: bool,
|
||||
) -> JsonPlaylist {
|
||||
let config_clone = config.clone();
|
||||
let mut playlist_path = Path::new(&config.playlist.path).to_owned();
|
||||
let mut playlist_path = config.playlist.path.clone();
|
||||
let start_sec = config.playlist.start_sec.unwrap();
|
||||
let date = get_date(seek, start_sec, next_start);
|
||||
let date = get_date(seek, start_sec, get_next);
|
||||
|
||||
if playlist_path.is_dir() || is_remote(&config.playlist.path) {
|
||||
if playlist_path.is_dir() || is_remote(&config.playlist.path.to_string_lossy()) {
|
||||
let d: Vec<&str> = date.split('-').collect();
|
||||
playlist_path = playlist_path
|
||||
.join(d[0])
|
||||
|
@ -165,7 +165,7 @@ pub fn validate_playlist(
|
||||
begin += item.out - item.seek;
|
||||
}
|
||||
|
||||
if !config.playlist.infinit && length > begin + 1.0 {
|
||||
if !config.playlist.infinit && length > begin + 1.2 {
|
||||
error!(
|
||||
"Playlist from <yellow>{date}</> not long enough, <yellow>{}</> needed!",
|
||||
sec_to_time(length - begin),
|
||||
|
@ -2,7 +2,7 @@ extern crate log;
|
||||
extern crate simplelog;
|
||||
|
||||
use std::{
|
||||
path::Path,
|
||||
path::PathBuf,
|
||||
sync::{atomic::Ordering, Arc, Mutex},
|
||||
thread::{self, sleep},
|
||||
time::Duration,
|
||||
@ -199,21 +199,18 @@ pub fn init_logging(
|
||||
};
|
||||
};
|
||||
|
||||
if app_config.log_to_file && &app_config.path != "none" {
|
||||
if app_config.log_to_file && !app_config.path.exists() {
|
||||
let file_config = log_config
|
||||
.clone()
|
||||
.set_time_format_custom(format_description!(
|
||||
"[[[year]-[month]-[day] [hour]:[minute]:[second].[subsecond digits:5]]"
|
||||
))
|
||||
.build();
|
||||
let mut log_path = "logs/ffplayout.log".to_string();
|
||||
let mut log_path = PathBuf::from("logs/ffplayout.log");
|
||||
|
||||
if Path::new(&app_config.path).is_dir() {
|
||||
log_path = Path::new(&app_config.path)
|
||||
.join("ffplayout.log")
|
||||
.display()
|
||||
.to_string();
|
||||
} else if Path::new(&app_config.path).is_file() {
|
||||
if app_config.path.is_dir() {
|
||||
log_path = app_config.path.join("ffplayout.log");
|
||||
} else if app_config.path.is_file() {
|
||||
log_path = app_config.path
|
||||
} else {
|
||||
println!("Logging path not exists!")
|
||||
|
@ -13,7 +13,7 @@ use std::{
|
||||
use std::env;
|
||||
|
||||
use chrono::{prelude::*, Duration};
|
||||
use ffprobe::{ffprobe, Format, Stream};
|
||||
use ffprobe::{ffprobe, Format, Stream as FFStream};
|
||||
use rand::prelude::*;
|
||||
use regex::Regex;
|
||||
use reqwest::header;
|
||||
@ -38,7 +38,7 @@ pub use config::{
|
||||
OutputMode::{self, *},
|
||||
PlayoutConfig,
|
||||
ProcessMode::{self, *},
|
||||
DUMMY_LEN, FFMPEG_IGNORE_ERRORS, FFMPEG_UNRECOVERABLE_ERRORS, IMAGE_FORMAT,
|
||||
Template, DUMMY_LEN, FFMPEG_IGNORE_ERRORS, FFMPEG_UNRECOVERABLE_ERRORS, IMAGE_FORMAT,
|
||||
};
|
||||
pub use controller::{
|
||||
PlayerControl, PlayoutStatus, ProcessControl,
|
||||
@ -205,8 +205,8 @@ fn is_empty_string(st: &String) -> bool {
|
||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
|
||||
pub struct MediaProbe {
|
||||
pub format: Option<Format>,
|
||||
pub audio_streams: Vec<Stream>,
|
||||
pub video_streams: Vec<Stream>,
|
||||
pub audio_streams: Vec<FFStream>,
|
||||
pub video_streams: Vec<FFStream>,
|
||||
}
|
||||
|
||||
impl MediaProbe {
|
||||
@ -321,14 +321,14 @@ pub fn get_sec() -> f64 {
|
||||
///
|
||||
/// - When time is before playlist start, get date from yesterday.
|
||||
/// - When given next_start is over target length (normally a full day), get date from tomorrow.
|
||||
pub fn get_date(seek: bool, start: f64, next_start: f64) -> String {
|
||||
pub fn get_date(seek: bool, start: f64, get_next: bool) -> String {
|
||||
let local: DateTime<Local> = time_now();
|
||||
|
||||
if seek && start > get_sec() {
|
||||
return (local - Duration::days(1)).format("%Y-%m-%d").to_string();
|
||||
}
|
||||
|
||||
if start == 0.0 && next_start >= 86400.0 {
|
||||
if start == 0.0 && get_next && get_sec() > 86398.0 {
|
||||
return (local + Duration::days(1)).format("%Y-%m-%d").to_string();
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
use std::fs;
|
||||
use std::{fs, path::PathBuf};
|
||||
|
||||
use ffplayout::{input::playlist::gen_source, utils::prepare_output_cmd};
|
||||
use ffplayout_lib::{
|
||||
@ -8,7 +8,7 @@ use ffplayout_lib::{
|
||||
|
||||
#[test]
|
||||
fn video_audio_input() {
|
||||
let mut config = PlayoutConfig::new(Some("../assets/ffplayout.yml".to_string()));
|
||||
let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml")));
|
||||
let player_control = PlayerControl::new();
|
||||
config.out.mode = Stream;
|
||||
config.processing.add_logo = true;
|
||||
@ -36,7 +36,7 @@ fn video_audio_input() {
|
||||
|
||||
#[test]
|
||||
fn video_audio_custom_filter1_input() {
|
||||
let mut config = PlayoutConfig::new(Some("../assets/ffplayout.yml".to_string()));
|
||||
let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml")));
|
||||
let player_control = PlayerControl::new();
|
||||
config.out.mode = Stream;
|
||||
config.processing.add_logo = false;
|
||||
@ -62,7 +62,7 @@ fn video_audio_custom_filter1_input() {
|
||||
|
||||
#[test]
|
||||
fn video_audio_custom_filter2_input() {
|
||||
let mut config = PlayoutConfig::new(Some("../assets/ffplayout.yml".to_string()));
|
||||
let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml")));
|
||||
let player_control = PlayerControl::new();
|
||||
config.out.mode = Stream;
|
||||
config.processing.add_logo = false;
|
||||
@ -90,7 +90,7 @@ fn video_audio_custom_filter2_input() {
|
||||
|
||||
#[test]
|
||||
fn video_audio_custom_filter3_input() {
|
||||
let mut config = PlayoutConfig::new(Some("../assets/ffplayout.yml".to_string()));
|
||||
let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml")));
|
||||
let player_control = PlayerControl::new();
|
||||
config.out.mode = Stream;
|
||||
config.processing.add_logo = false;
|
||||
@ -117,7 +117,7 @@ fn video_audio_custom_filter3_input() {
|
||||
|
||||
#[test]
|
||||
fn dual_audio_aevalsrc_input() {
|
||||
let mut config = PlayoutConfig::new(Some("../assets/ffplayout.yml".to_string()));
|
||||
let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml")));
|
||||
let player_control = PlayerControl::new();
|
||||
config.out.mode = Stream;
|
||||
config.processing.audio_tracks = 2;
|
||||
@ -144,7 +144,7 @@ fn dual_audio_aevalsrc_input() {
|
||||
|
||||
#[test]
|
||||
fn dual_audio_input() {
|
||||
let mut config = PlayoutConfig::new(Some("../assets/ffplayout.yml".to_string()));
|
||||
let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml")));
|
||||
let player_control = PlayerControl::new();
|
||||
config.out.mode = Stream;
|
||||
config.processing.audio_tracks = 2;
|
||||
@ -170,7 +170,7 @@ fn dual_audio_input() {
|
||||
|
||||
#[test]
|
||||
fn video_separate_audio_input() {
|
||||
let mut config = PlayoutConfig::new(Some("../assets/ffplayout.yml".to_string()));
|
||||
let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml")));
|
||||
let player_control = PlayerControl::new();
|
||||
config.out.mode = Stream;
|
||||
config.processing.audio_tracks = 1;
|
||||
@ -204,7 +204,7 @@ fn video_separate_audio_input() {
|
||||
|
||||
#[test]
|
||||
fn video_audio_stream() {
|
||||
let mut config = PlayoutConfig::new(Some("../assets/ffplayout.yml".to_string()));
|
||||
let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml")));
|
||||
config.out.mode = Stream;
|
||||
config.processing.add_logo = false;
|
||||
config.out.output_cmd = Some(vec_strings![
|
||||
@ -263,7 +263,7 @@ fn video_audio_stream() {
|
||||
|
||||
#[test]
|
||||
fn video_audio_filter1_stream() {
|
||||
let mut config = PlayoutConfig::new(Some("../assets/ffplayout.yml".to_string()));
|
||||
let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml")));
|
||||
config.out.mode = Stream;
|
||||
config.processing.add_logo = false;
|
||||
config.text.add_text = false;
|
||||
@ -338,7 +338,7 @@ fn video_audio_filter1_stream() {
|
||||
|
||||
#[test]
|
||||
fn video_audio_filter2_stream() {
|
||||
let mut config = PlayoutConfig::new(Some("../assets/ffplayout.yml".to_string()));
|
||||
let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml")));
|
||||
config.out.mode = Stream;
|
||||
config.processing.add_logo = false;
|
||||
config.text.add_text = true;
|
||||
@ -421,7 +421,7 @@ fn video_audio_filter2_stream() {
|
||||
|
||||
#[test]
|
||||
fn video_audio_filter3_stream() {
|
||||
let mut config = PlayoutConfig::new(Some("../assets/ffplayout.yml".to_string()));
|
||||
let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml")));
|
||||
config.out.mode = Stream;
|
||||
config.processing.add_logo = false;
|
||||
config.text.add_text = true;
|
||||
@ -507,7 +507,7 @@ fn video_audio_filter3_stream() {
|
||||
|
||||
#[test]
|
||||
fn video_audio_filter4_stream() {
|
||||
let mut config = PlayoutConfig::new(Some("../assets/ffplayout.yml".to_string()));
|
||||
let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml")));
|
||||
config.out.mode = Stream;
|
||||
config.processing.add_logo = false;
|
||||
config.text.add_text = true;
|
||||
@ -593,7 +593,7 @@ fn video_audio_filter4_stream() {
|
||||
|
||||
#[test]
|
||||
fn video_dual_audio_stream() {
|
||||
let mut config = PlayoutConfig::new(Some("../assets/ffplayout.yml".to_string()));
|
||||
let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml")));
|
||||
config.out.mode = Stream;
|
||||
config.processing.add_logo = false;
|
||||
config.processing.audio_tracks = 2;
|
||||
@ -664,7 +664,7 @@ fn video_dual_audio_stream() {
|
||||
|
||||
#[test]
|
||||
fn video_dual_audio_filter_stream() {
|
||||
let mut config = PlayoutConfig::new(Some("../assets/ffplayout.yml".to_string()));
|
||||
let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml")));
|
||||
config.out.mode = Stream;
|
||||
config.processing.add_logo = false;
|
||||
config.processing.audio_tracks = 2;
|
||||
@ -744,7 +744,7 @@ fn video_dual_audio_filter_stream() {
|
||||
|
||||
#[test]
|
||||
fn video_audio_multi_stream() {
|
||||
let mut config = PlayoutConfig::new(Some("../assets/ffplayout.yml".to_string()));
|
||||
let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml")));
|
||||
config.out.mode = Stream;
|
||||
config.processing.add_logo = false;
|
||||
config.out.output_cmd = Some(vec_strings![
|
||||
@ -833,7 +833,7 @@ fn video_audio_multi_stream() {
|
||||
|
||||
#[test]
|
||||
fn video_dual_audio_multi_stream() {
|
||||
let mut config = PlayoutConfig::new(Some("../assets/ffplayout.yml".to_string()));
|
||||
let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml")));
|
||||
config.out.mode = Stream;
|
||||
config.processing.add_logo = false;
|
||||
config.processing.audio_tracks = 2;
|
||||
@ -947,7 +947,7 @@ fn video_dual_audio_multi_stream() {
|
||||
|
||||
#[test]
|
||||
fn video_audio_text_multi_stream() {
|
||||
let mut config = PlayoutConfig::new(Some("../assets/ffplayout.yml".to_string()));
|
||||
let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml")));
|
||||
config.out.mode = Stream;
|
||||
config.processing.add_logo = false;
|
||||
config.text.add_text = true;
|
||||
@ -1060,7 +1060,7 @@ fn video_audio_text_multi_stream() {
|
||||
|
||||
#[test]
|
||||
fn video_dual_audio_multi_filter_stream() {
|
||||
let mut config = PlayoutConfig::new(Some("../assets/ffplayout.yml".to_string()));
|
||||
let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml")));
|
||||
config.out.mode = Stream;
|
||||
config.processing.add_logo = false;
|
||||
config.processing.audio_tracks = 2;
|
||||
@ -1189,7 +1189,7 @@ fn video_dual_audio_multi_filter_stream() {
|
||||
|
||||
#[test]
|
||||
fn video_audio_text_filter_stream() {
|
||||
let mut config = PlayoutConfig::new(Some("../assets/ffplayout.yml".to_string()));
|
||||
let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml")));
|
||||
config.out.mode = Stream;
|
||||
config.processing.add_logo = false;
|
||||
config.processing.audio_tracks = 1;
|
||||
@ -1311,7 +1311,7 @@ fn video_audio_text_filter_stream() {
|
||||
|
||||
#[test]
|
||||
fn video_audio_hls() {
|
||||
let mut config = PlayoutConfig::new(Some("../assets/ffplayout.yml".to_string()));
|
||||
let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml")));
|
||||
let player_control = PlayerControl::new();
|
||||
config.out.mode = HLS;
|
||||
config.processing.add_logo = false;
|
||||
@ -1397,7 +1397,7 @@ fn video_audio_hls() {
|
||||
|
||||
#[test]
|
||||
fn video_audio_sub_meta_hls() {
|
||||
let mut config = PlayoutConfig::new(Some("../assets/ffplayout.yml".to_string()));
|
||||
let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml")));
|
||||
let player_control = PlayerControl::new();
|
||||
config.out.mode = HLS;
|
||||
config.processing.add_logo = false;
|
||||
@ -1491,7 +1491,7 @@ fn video_audio_sub_meta_hls() {
|
||||
|
||||
#[test]
|
||||
fn video_multi_audio_hls() {
|
||||
let mut config = PlayoutConfig::new(Some("../assets/ffplayout.yml".to_string()));
|
||||
let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml")));
|
||||
let player_control = PlayerControl::new();
|
||||
config.out.mode = HLS;
|
||||
config.processing.add_logo = false;
|
||||
@ -1580,7 +1580,7 @@ fn video_multi_audio_hls() {
|
||||
|
||||
#[test]
|
||||
fn multi_video_audio_hls() {
|
||||
let mut config = PlayoutConfig::new(Some("../assets/ffplayout.yml".to_string()));
|
||||
let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml")));
|
||||
let player_control = PlayerControl::new();
|
||||
config.out.mode = HLS;
|
||||
config.processing.add_logo = false;
|
||||
@ -1694,7 +1694,7 @@ fn multi_video_audio_hls() {
|
||||
|
||||
#[test]
|
||||
fn multi_video_multi_audio_hls() {
|
||||
let mut config = PlayoutConfig::new(Some("../assets/ffplayout.yml".to_string()));
|
||||
let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml")));
|
||||
let player_control = PlayerControl::new();
|
||||
config.out.mode = HLS;
|
||||
config.processing.add_logo = false;
|
||||
|
@ -17,6 +17,48 @@ fn timed_stop(sec: u64, proc_ctl: ProcessControl) {
|
||||
proc_ctl.stop_all();
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
#[ignore]
|
||||
fn playlist_missing() {
|
||||
let mut config = PlayoutConfig::new(None);
|
||||
config.mail.recipient = "".into();
|
||||
config.processing.mode = Playlist;
|
||||
config.ingest.enable = false;
|
||||
config.text.add_text = false;
|
||||
config.playlist.day_start = "00:00:00".into();
|
||||
config.playlist.start_sec = Some(0.0);
|
||||
config.playlist.length = "24:00:00".into();
|
||||
config.playlist.length_sec = Some(86400.0);
|
||||
config.playlist.path = "assets/playlists".into();
|
||||
config.storage.filler = "assets/with_audio.mp4".into();
|
||||
config.logging.log_to_file = false;
|
||||
config.logging.timestamp = false;
|
||||
config.logging.level = LevelFilter::Trace;
|
||||
config.out.mode = Null;
|
||||
config.out.output_count = 1;
|
||||
config.out.output_filter = None;
|
||||
config.out.output_cmd = Some(vec_strings!["-f", "null", "-"]);
|
||||
|
||||
let play_control = PlayerControl::new();
|
||||
let playout_stat = PlayoutStatus::new();
|
||||
let proc_control = ProcessControl::new();
|
||||
let proc_ctl = proc_control.clone();
|
||||
|
||||
let logging = init_logging(&config, None, None);
|
||||
CombinedLogger::init(logging).unwrap_or_default();
|
||||
|
||||
mock_time::set_mock_time("2023-02-07T23:59:45");
|
||||
|
||||
thread::spawn(move || timed_stop(28, proc_ctl));
|
||||
|
||||
player(&config, &play_control, playout_stat.clone(), proc_control);
|
||||
|
||||
let playlist_date = &*playout_stat.current_date.lock().unwrap();
|
||||
|
||||
assert_eq!(playlist_date, "2023-02-08");
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
#[ignore]
|
||||
@ -34,6 +76,7 @@ fn playlist_change_at_midnight() {
|
||||
config.storage.filler = "assets/with_audio.mp4".into();
|
||||
config.logging.log_to_file = false;
|
||||
config.logging.timestamp = false;
|
||||
config.logging.level = LevelFilter::Trace;
|
||||
config.out.mode = Null;
|
||||
config.out.output_count = 1;
|
||||
config.out.output_filter = None;
|
||||
|
@ -1,3 +1,5 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[cfg(test)]
|
||||
use chrono::prelude::*;
|
||||
|
||||
@ -22,7 +24,7 @@ fn mock_date_time() {
|
||||
fn get_date_yesterday() {
|
||||
mock_time::set_mock_time("2022-05-20T05:59:24");
|
||||
|
||||
let date = get_date(true, 21600.0, 86400.0);
|
||||
let date = get_date(true, 21600.0, false);
|
||||
|
||||
assert_eq!("2022-05-19".to_string(), date);
|
||||
}
|
||||
@ -31,14 +33,14 @@ fn get_date_yesterday() {
|
||||
fn get_date_tomorrow() {
|
||||
mock_time::set_mock_time("2022-05-20T23:59:30");
|
||||
|
||||
let date = get_date(false, 0.0, 86400.01);
|
||||
let date = get_date(false, 0.0, true);
|
||||
|
||||
assert_eq!("2022-05-21".to_string(), date);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delta() {
|
||||
let mut config = PlayoutConfig::new(Some("../assets/ffplayout.yml".to_string()));
|
||||
let mut config = PlayoutConfig::new(Some(PathBuf::from("../assets/ffplayout.yml")));
|
||||
config.mail.recipient = "".into();
|
||||
config.processing.mode = Playlist;
|
||||
config.playlist.day_start = "00:00:00".into();
|
||||
|
Loading…
Reference in New Issue
Block a user