cleanup code, fix roles, better naming, fix clippy, use ChannelManager
This commit is contained in:
parent
17e728ead1
commit
c5d179b36a
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -1090,7 +1090,6 @@ dependencies = [
|
||||
"ffplayout-lib",
|
||||
"ffprobe",
|
||||
"flexi_logger",
|
||||
"futures",
|
||||
"futures-util",
|
||||
"home",
|
||||
"itertools 0.13.0",
|
||||
|
@ -1,5 +0,0 @@
|
||||
# give user ffpu permission to control the ffplayout systemd service
|
||||
|
||||
ffpu ALL = NOPASSWD: /usr/bin/systemctl start ffplayout.service, /usr/bin/systemctl stop ffplayout.service, /usr/bin/systemctl restart ffplayout.service, /usr/bin/systemctl status ffplayout.service, /usr/bin/systemctl is-active ffplayout.service, /usr/bin/systemctl enable ffplayout.service, /usr/bin/systemctl disable ffplayout.service
|
||||
|
||||
ffpu ALL = NOPASSWD: /usr/bin/systemctl start ffplayout@*, /usr/bin/systemctl stop ffplayout@*, /usr/bin/systemctl restart ffplayout@*, /usr/bin/systemctl status ffplayout@*, /usr/bin/systemctl is-active ffplayout@*, /usr/bin/systemctl enable ffplayout@*, /usr/bin/systemctl disable ffplayout@*
|
@ -1,37 +0,0 @@
|
||||
# Changing these settings is for advanced users only!
|
||||
# There will be no support or guarantee that it will be stable after changing them.
|
||||
|
||||
[decoder]
|
||||
input_param = ""
|
||||
# output_param get also applied to ingest instance.
|
||||
output_param = ""
|
||||
|
||||
[filters]
|
||||
deinterlace = "" # yadif=0:-1:0
|
||||
pad_scale_w = "" # scale={}:-1
|
||||
pad_scale_h = "" # scale=-1:{}
|
||||
pad_video = "" # pad=max(iw\\,ih*({0}/{1})):ow/({0}/{1}):(ow-iw)/2:(oh-ih)/2
|
||||
fps = "" # fps={}
|
||||
scale = "" # scale={}:{}
|
||||
set_dar = "" # setdar=dar={}
|
||||
fade_in = "" # fade=in:st=0:d=0.5
|
||||
fade_out = "" # fade=out:st={}:d=1.0
|
||||
overlay_logo_scale = "" # scale={}
|
||||
overlay_logo_fade_in = "" # fade=in:st=0:d=1.0:alpha=1
|
||||
overlay_logo_fade_out = "" # fade=out:st={}:d=1.0:alpha=1
|
||||
overlay_logo = "" # null[l];[v][l]overlay={}:shortest=1
|
||||
tpad = "" # tpad=stop_mode=add:stop_duration={}
|
||||
drawtext_from_file = "" # drawtext=text='{}':{}{}
|
||||
drawtext_from_zmq = "" # zmq=b=tcp\\\\://'{}',drawtext@dyntext={}
|
||||
aevalsrc = "" # aevalsrc=0:channel_layout=stereo:duration={}:sample_rate=48000
|
||||
afade_in = "" # afade=in:st=0:d=0.5
|
||||
afade_out = "" # afade=out:st={}:d=1.0
|
||||
apad = "" # apad=whole_dur={}
|
||||
volume = "" # volume={}
|
||||
split = "" # split={}{}
|
||||
|
||||
[encoder]
|
||||
input_param = ""
|
||||
|
||||
[ingest]
|
||||
input_param = ""
|
@ -1,12 +0,0 @@
|
||||
[Unit]
|
||||
Description=Rest API for ffplayout
|
||||
After=network.target remote-fs.target
|
||||
|
||||
[Service]
|
||||
ExecStart=/usr/bin/ffpapi -l 0.0.0.0:8787
|
||||
Restart=always
|
||||
RestartSec=1
|
||||
User=ffpu
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
@ -1,168 +0,0 @@
|
||||
[general]
|
||||
help_text = """Sometimes it can happen, that a file is corrupt but still playable, \
|
||||
this can produce an streaming error over all following files. The only way \
|
||||
in this case is, to stop ffplayout and start it again. Here we only say when \
|
||||
it stops, the starting process is in your hand. Best way is a systemd service \
|
||||
on linux.
|
||||
'stop_threshold' stop ffplayout, if it is async in time above this \
|
||||
value. A number below 3 can cause unexpected errors."""
|
||||
stop_threshold = 11
|
||||
stat_file = ".ffp_status"
|
||||
|
||||
[rpc_server]
|
||||
help_text = """Run a JSON RPC server, for getting infos about current playing and for some \
|
||||
control functions."""
|
||||
enable = true
|
||||
address = "127.0.0.1:7070"
|
||||
authorization = "av2Kx8g67lF9qj5wEH3ym1bI4cCs"
|
||||
|
||||
[mail]
|
||||
help_text = """Send error messages to email address, like missing playlist; invalid \
|
||||
json format; missing clip path. Leave recipient blank, if you don't need this.
|
||||
'mail_level' can be INFO, WARNING or ERROR.
|
||||
'interval' means seconds until a new mail will be sended, value must be in increments of 10."""
|
||||
subject = "Playout Error"
|
||||
smtp_server = "mail.example.org"
|
||||
starttls = true
|
||||
sender_addr = "ffplayout@example.org"
|
||||
sender_pass = "abc123"
|
||||
recipient = ""
|
||||
mail_level = "ERROR"
|
||||
interval = 120
|
||||
|
||||
[logging]
|
||||
help_text = """If 'log_to_file' is true, log to file, when is false log to console.
|
||||
'backup_count' says how long log files will be saved in days.
|
||||
'local_time' to false will set log timestamps to UTC. Path to /var/log/ only \
|
||||
if you run this program as daemon.
|
||||
'level' can be DEBUG, INFO, WARNING, ERROR.
|
||||
'ffmpeg_level/ingest_level' can be INFO, WARNING, ERROR.
|
||||
'detect_silence' logs an error message if the audio line is silent for 15 \
|
||||
seconds during the validation process.
|
||||
'ignore_lines' makes logging to ignore strings that contains matched lines, \
|
||||
in frontend is a semicolon separated list."""
|
||||
log_to_file = true
|
||||
backup_count = 7
|
||||
local_time = true
|
||||
timestamp = true
|
||||
path = "/var/log/ffplayout/"
|
||||
level = "DEBUG"
|
||||
ffmpeg_level = "ERROR"
|
||||
ingest_level = "WARNING"
|
||||
detect_silence = false
|
||||
ignore_lines = [
|
||||
"P sub_mb_type 4 out of range at",
|
||||
"error while decoding MB",
|
||||
"negative number of zero coeffs at",
|
||||
"out of range intra chroma pred mode",
|
||||
"non-existing SPS 0 referenced in buffering period",
|
||||
]
|
||||
|
||||
[processing]
|
||||
help_text = """Default processing for all clips, to have them unique. Mode can be playlist \
|
||||
or folder.
|
||||
'aspect' must be a float number.'logo' is only used if the path exist.
|
||||
'logo_scale' scale the logo to target size, leave it blank when no scaling \
|
||||
is needed, format is 'width:height', for example '100:-1' for proportional \
|
||||
scaling. With 'logo_opacity' logo can become transparent.
|
||||
With 'audio_tracks' it is possible to configure how many audio tracks should \
|
||||
be processed. 'audio_channels' can be use, if audio has more channels then only stereo.
|
||||
With 'logo_position' in format 'x:y' you set the logo position.
|
||||
With 'custom_filter' it is possible, to apply further filters. The filter \
|
||||
outputs should end with [c_v_out] for video filter, and [c_a_out] for audio filter."""
|
||||
mode = "playlist"
|
||||
audio_only = false
|
||||
copy_audio = false
|
||||
copy_video = false
|
||||
width = 1024
|
||||
height = 576
|
||||
aspect = 1.778
|
||||
fps = 25
|
||||
add_logo = true
|
||||
logo = "/usr/share/ffplayout/logo.png"
|
||||
logo_scale = ""
|
||||
logo_opacity = 0.7
|
||||
logo_position = "W-w-12:12"
|
||||
audio_tracks = 1
|
||||
audio_track_index = -1
|
||||
audio_channels = 2
|
||||
volume = 1
|
||||
custom_filter = ""
|
||||
|
||||
[ingest]
|
||||
help_text = """Run a server for a ingest stream. This stream will override the normal streaming \
|
||||
until is done. There is only a very simple authentication mechanism, which check if the \
|
||||
stream name is correct.
|
||||
'custom_filter' can be used in the same way then the one in the process section."""
|
||||
enable = false
|
||||
input_param = "-f live_flv -listen 1 -i rtmp://127.0.0.1:1936/live/stream"
|
||||
custom_filter = ""
|
||||
|
||||
[playlist]
|
||||
help_text = """'path' can be a path to a single file, or a directory. For directory put \
|
||||
only the root folder, for example '/playlists', subdirectories are read by the \
|
||||
program. Subdirectories needs this structure '/playlists/2018/01'.
|
||||
'day_start' means at which time the playlist should start, leave day_start \
|
||||
blank when playlist should always start at the begin. 'length' represent the \
|
||||
target length from playlist, when is blank real length will not consider.
|
||||
'infinit: true' works with single playlist file and loops it infinitely. """
|
||||
path = "/var/lib/ffplayout/playlists"
|
||||
day_start = "05:59:25"
|
||||
length = "24:00:00"
|
||||
infinit = false
|
||||
|
||||
[storage]
|
||||
help_text = """'filler' is for playing instead of a missing file or fill the end to reach 24 \
|
||||
hours, can be a file or folder, it will loop when is necessary.
|
||||
'extensions' search only files with this extension. Set 'shuffle' to 'true' \
|
||||
to pick files randomly."""
|
||||
path = "/var/lib/ffplayout/tv-media"
|
||||
filler = "/var/lib/ffplayout/tv-media/filler/filler.mp4"
|
||||
extensions = ["mp4", "mkv", "webm"]
|
||||
shuffle = true
|
||||
|
||||
[text]
|
||||
help_text = """Overlay text in combination with libzmq for remote text manipulation. \
|
||||
On windows fontfile path need to be like this 'C\\:/WINDOWS/fonts/DejaVuSans.ttf'.
|
||||
'text_from_filename' activate the extraction from text of a filename. With 'style' \
|
||||
you can define the drawtext parameters like position, color, etc. Post Text over \
|
||||
API will override this. With 'regex' you can format file names, to get a title from it."""
|
||||
add_text = true
|
||||
text_from_filename = false
|
||||
fontfile = "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf"
|
||||
style = "x=(w-tw)/2:y=(h-line_h)*0.9:fontsize=24:fontcolor=#ffffff:box=1:boxcolor=#000000:boxborderw=4"
|
||||
regex = "^.+[/\\](.*)(.mp4|.mkv|.webm)$"
|
||||
|
||||
[task]
|
||||
help_text = """Run an external program with a given media object. The media object is in json format \
|
||||
and contains all the information about the current clip. The external program can be a script \
|
||||
or a binary, but should only run for a short time."""
|
||||
enable = false
|
||||
path = ""
|
||||
|
||||
[out]
|
||||
help_text = """The final playout compression. Set the settings to your needs. 'mode' \
|
||||
has the options 'desktop', 'hls', 'null', 'stream'. Use 'stream' and adjust \
|
||||
'output_param:' settings when you want to stream to a rtmp/rtsp/srt/... server.
|
||||
In production don't serve hls playlist with ffpapi, use nginx or another web server!"""
|
||||
mode = "hls"
|
||||
output_param = """\
|
||||
-c:v libx264
|
||||
-crf 23
|
||||
-x264-params keyint=50:min-keyint=25:scenecut=-1
|
||||
-maxrate 1300k
|
||||
-bufsize 2600k
|
||||
-preset faster
|
||||
-tune zerolatency
|
||||
-profile:v Main
|
||||
-level 3.1
|
||||
-c:a aac
|
||||
-ar 44100
|
||||
-b:a 128k
|
||||
-flags +cgop
|
||||
-f hls
|
||||
-hls_time 6
|
||||
-hls_list_size 600
|
||||
-hls_flags append_list+delete_segments+omit_endlist
|
||||
-hls_segment_filename /usr/share/ffplayout/public/live/stream-%d.ts
|
||||
/usr/share/ffplayout/public/live/stream.m3u8"""
|
@ -1,14 +0,0 @@
|
||||
[Unit]
|
||||
Description=Rust and ffmpeg based multi channel playout solution
|
||||
After=network.target remote-fs.target
|
||||
|
||||
[Service]
|
||||
ExecStart=/usr/bin/ffplayout %I
|
||||
Restart=always
|
||||
StartLimitInterval=20
|
||||
RestartSec=1
|
||||
KillMode=mixed
|
||||
User=ffpu
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
@ -30,7 +30,6 @@ faccess = "0.2"
|
||||
ffprobe = "0.4"
|
||||
flexi_logger = { version = "0.28", features = ["kv", "colors"] }
|
||||
futures-util = { version = "0.3", default-features = false, features = ["std"] }
|
||||
futures = "0.3"
|
||||
home = "0.5"
|
||||
itertools = "0.13"
|
||||
jsonwebtoken = "9"
|
||||
|
@ -32,7 +32,7 @@ use path_clean::PathClean;
|
||||
use regex::Regex;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::{Pool, Sqlite};
|
||||
use tokio::{fs, task};
|
||||
use tokio::fs;
|
||||
|
||||
use crate::player::utils::{
|
||||
get_data_map, get_date_range, import::import_file, sec_to_time, time_to_sec, JsonPlaylist,
|
||||
@ -160,7 +160,7 @@ pub async fn login(pool: web::Data<Pool<Sqlite>>, credentials: web::Json<User>)
|
||||
.await
|
||||
.unwrap_or(Role::Guest);
|
||||
|
||||
task::spawn_blocking(move || {
|
||||
web::block(move || {
|
||||
let pass = user.password.clone();
|
||||
let hash = PasswordHash::new(&pass).unwrap();
|
||||
user.password = "".into();
|
||||
@ -219,7 +219,7 @@ pub async fn login(pool: web::Data<Pool<Sqlite>>, credentials: web::Json<User>)
|
||||
/// -H 'Authorization: Bearer <TOKEN>'
|
||||
/// ```
|
||||
#[get("/user")]
|
||||
#[protect(any("Role::Admin", "Role::User"), ty = "Role")]
|
||||
#[protect(any("Role::GlobalAdmin", "Role::User"), ty = "Role")]
|
||||
async fn get_user(
|
||||
pool: web::Data<Pool<Sqlite>>,
|
||||
user: web::ReqData<LoginUser>,
|
||||
@ -240,7 +240,7 @@ async fn get_user(
|
||||
/// -H 'Authorization: Bearer <TOKEN>'
|
||||
/// ```
|
||||
#[get("/user/{name}")]
|
||||
#[protect("Role::Admin", ty = "Role")]
|
||||
#[protect("Role::GlobalAdmin", ty = "Role")]
|
||||
async fn get_by_name(
|
||||
pool: web::Data<Pool<Sqlite>>,
|
||||
name: web::Path<String>,
|
||||
@ -261,7 +261,7 @@ async fn get_by_name(
|
||||
/// -H 'Authorization: Bearer <TOKEN>'
|
||||
/// ```
|
||||
#[get("/users")]
|
||||
#[protect("Role::Admin", ty = "Role")]
|
||||
#[protect("Role::GlobalAdmin", ty = "Role")]
|
||||
async fn get_users(pool: web::Data<Pool<Sqlite>>) -> Result<impl Responder, ServiceError> {
|
||||
match handles::select_users(&pool.into_inner()).await {
|
||||
Ok(users) => Ok(web::Json(users)),
|
||||
@ -279,7 +279,7 @@ async fn get_users(pool: web::Data<Pool<Sqlite>>) -> Result<impl Responder, Serv
|
||||
/// -d '{"mail": "<MAIL>", "password": "<PASS>"}' -H 'Authorization: Bearer <TOKEN>'
|
||||
/// ```
|
||||
#[put("/user/{id}")]
|
||||
#[protect(any("Role::Admin", "Role::User"), ty = "Role")]
|
||||
#[protect(any("Role::GlobalAdmin", "Role::User"), ty = "Role")]
|
||||
async fn update_user(
|
||||
pool: web::Data<Pool<Sqlite>>,
|
||||
id: web::Path<i32>,
|
||||
@ -287,7 +287,7 @@ async fn update_user(
|
||||
data: web::Json<User>,
|
||||
role: AuthDetails<Role>,
|
||||
) -> Result<impl Responder, ServiceError> {
|
||||
if *id == user.id || role.has_authority(&Role::Admin) {
|
||||
if *id == user.id || role.has_authority(&Role::GlobalAdmin) {
|
||||
let mut fields = String::new();
|
||||
|
||||
if let Some(mail) = data.mail.clone() {
|
||||
@ -332,7 +332,7 @@ async fn update_user(
|
||||
/// -H 'Authorization: Bearer <TOKEN>'
|
||||
/// ```
|
||||
#[post("/user/")]
|
||||
#[protect("Role::Admin", ty = "Role")]
|
||||
#[protect("Role::GlobalAdmin", ty = "Role")]
|
||||
async fn add_user(
|
||||
pool: web::Data<Pool<Sqlite>>,
|
||||
data: web::Json<User>,
|
||||
@ -353,7 +353,7 @@ async fn add_user(
|
||||
/// -H 'Authorization: Bearer <TOKEN>'
|
||||
/// ```
|
||||
#[delete("/user/{name}")]
|
||||
#[protect("Role::Admin", ty = "Role")]
|
||||
#[protect("Role::GlobalAdmin", ty = "Role")]
|
||||
async fn remove_user(
|
||||
pool: web::Data<Pool<Sqlite>>,
|
||||
name: web::Path<String>,
|
||||
@ -389,7 +389,7 @@ async fn remove_user(
|
||||
/// }
|
||||
/// ```
|
||||
#[get("/channel/{id}")]
|
||||
#[protect(any("Role::Admin", "Role::User"), ty = "Role")]
|
||||
#[protect(any("Role::GlobalAdmin", "Role::User"), ty = "Role")]
|
||||
async fn get_channel(
|
||||
pool: web::Data<Pool<Sqlite>>,
|
||||
id: web::Path<i32>,
|
||||
@ -407,7 +407,7 @@ async fn get_channel(
|
||||
/// curl -X GET http://127.0.0.1:8787/api/channels -H "Authorization: Bearer <TOKEN>"
|
||||
/// ```
|
||||
#[get("/channels")]
|
||||
#[protect(any("Role::Admin", "Role::User"), ty = "Role")]
|
||||
#[protect(any("Role::GlobalAdmin", "Role::User"), ty = "Role")]
|
||||
async fn get_all_channels(pool: web::Data<Pool<Sqlite>>) -> Result<impl Responder, ServiceError> {
|
||||
if let Ok(channel) = handles::select_all_channels(&pool.into_inner()).await {
|
||||
return Ok(web::Json(channel));
|
||||
@ -424,7 +424,7 @@ async fn get_all_channels(pool: web::Data<Pool<Sqlite>>) -> Result<impl Responde
|
||||
/// -H "Authorization: Bearer <TOKEN>"
|
||||
/// ```
|
||||
#[patch("/channel/{id}")]
|
||||
#[protect("Role::Admin", ty = "Role")]
|
||||
#[protect("Role::GlobalAdmin", ty = "Role")]
|
||||
async fn patch_channel(
|
||||
pool: web::Data<Pool<Sqlite>>,
|
||||
id: web::Path<i32>,
|
||||
@ -448,7 +448,7 @@ async fn patch_channel(
|
||||
/// -H "Authorization: Bearer <TOKEN>"
|
||||
/// ```
|
||||
#[post("/channel/")]
|
||||
#[protect("Role::Admin", ty = "Role")]
|
||||
#[protect("Role::GlobalAdmin", ty = "Role")]
|
||||
async fn add_channel(
|
||||
pool: web::Data<Pool<Sqlite>>,
|
||||
data: web::Json<Channel>,
|
||||
@ -465,7 +465,7 @@ async fn add_channel(
|
||||
/// curl -X DELETE http://127.0.0.1:8787/api/channel/2 -H "Authorization: Bearer <TOKEN>"
|
||||
/// ```
|
||||
#[delete("/channel/{id}")]
|
||||
#[protect("Role::Admin", ty = "Role")]
|
||||
#[protect("Role::GlobalAdmin", ty = "Role")]
|
||||
async fn remove_channel(
|
||||
pool: web::Data<Pool<Sqlite>>,
|
||||
id: web::Path<i32>,
|
||||
@ -487,19 +487,18 @@ async fn remove_channel(
|
||||
///
|
||||
/// Response is a JSON object from the ffplayout.toml
|
||||
#[get("/playout/config/{id}")]
|
||||
#[protect(any("Role::Admin", "Role::User"), ty = "Role")]
|
||||
#[protect(any("Role::GlobalAdmin", "Role::User"), ty = "Role")]
|
||||
async fn get_playout_config(
|
||||
pool: web::Data<Pool<Sqlite>>,
|
||||
_pool: web::Data<Pool<Sqlite>>,
|
||||
id: web::Path<i32>,
|
||||
_details: AuthDetails<Role>,
|
||||
controllers: web::Data<Mutex<ChannelController>>,
|
||||
) -> Result<impl Responder, ServiceError> {
|
||||
if let Ok(_channel) = handles::select_channel(&pool.into_inner(), &id).await {
|
||||
// TODO: get config
|
||||
let manager = controllers.lock().unwrap().get(*id).unwrap();
|
||||
let config = manager.config.lock().unwrap().clone();
|
||||
// let config = PlayoutConfig::new(&pool.into_inner(), *id).await;
|
||||
|
||||
return Ok("Update playout config success.");
|
||||
};
|
||||
|
||||
Err(ServiceError::InternalServerError)
|
||||
Ok(web::Json(config))
|
||||
}
|
||||
|
||||
/// **Update Config**
|
||||
@ -509,7 +508,7 @@ async fn get_playout_config(
|
||||
/// -d { <CONFIG DATA> } -H 'Authorization: Bearer <TOKEN>'
|
||||
/// ```
|
||||
#[put("/playout/config/{id}")]
|
||||
#[protect("Role::Admin", ty = "Role")]
|
||||
#[protect("Role::GlobalAdmin", ty = "Role")]
|
||||
async fn update_playout_config(
|
||||
pool: web::Data<Pool<Sqlite>>,
|
||||
id: web::Path<i32>,
|
||||
@ -535,7 +534,7 @@ async fn update_playout_config(
|
||||
/// -H 'Authorization: Bearer <TOKEN>'
|
||||
/// ```
|
||||
#[get("/presets/{id}")]
|
||||
#[protect(any("Role::Admin", "Role::User"), ty = "Role")]
|
||||
#[protect(any("Role::GlobalAdmin", "Role::User"), ty = "Role")]
|
||||
async fn get_presets(
|
||||
pool: web::Data<Pool<Sqlite>>,
|
||||
id: web::Path<i32>,
|
||||
@ -555,7 +554,7 @@ async fn get_presets(
|
||||
/// -H 'Authorization: Bearer <TOKEN>'
|
||||
/// ```
|
||||
#[put("/presets/{id}")]
|
||||
#[protect(any("Role::Admin", "Role::User"), ty = "Role")]
|
||||
#[protect(any("Role::GlobalAdmin", "Role::User"), ty = "Role")]
|
||||
async fn update_preset(
|
||||
pool: web::Data<Pool<Sqlite>>,
|
||||
id: web::Path<i32>,
|
||||
@ -579,7 +578,7 @@ async fn update_preset(
|
||||
/// -H 'Authorization: Bearer <TOKEN>'
|
||||
/// ```
|
||||
#[post("/presets/")]
|
||||
#[protect(any("Role::Admin", "Role::User"), ty = "Role")]
|
||||
#[protect(any("Role::GlobalAdmin", "Role::User"), ty = "Role")]
|
||||
async fn add_preset(
|
||||
pool: web::Data<Pool<Sqlite>>,
|
||||
data: web::Json<TextPreset>,
|
||||
@ -601,7 +600,7 @@ async fn add_preset(
|
||||
/// -H 'Authorization: Bearer <TOKEN>'
|
||||
/// ```
|
||||
#[delete("/presets/{id}")]
|
||||
#[protect(any("Role::Admin", "Role::User"), ty = "Role")]
|
||||
#[protect(any("Role::GlobalAdmin", "Role::User"), ty = "Role")]
|
||||
async fn delete_preset(
|
||||
pool: web::Data<Pool<Sqlite>>,
|
||||
id: web::Path<i32>,
|
||||
@ -632,7 +631,7 @@ async fn delete_preset(
|
||||
/// -d '{"text": "Hello from ffplayout", "x": "(w-text_w)/2", "y": "(h-text_h)/2", fontsize": "24", "line_spacing": "4", "fontcolor": "#ffffff", "box": "1", "boxcolor": "#000000", "boxborderw": "4", "alpha": "1.0"}'
|
||||
/// ```
|
||||
#[post("/control/{id}/text/")]
|
||||
#[protect(any("Role::Admin", "Role::User"), ty = "Role")]
|
||||
#[protect(any("Role::GlobalAdmin", "Role::User"), ty = "Role")]
|
||||
pub async fn send_text_message(
|
||||
id: web::Path<i32>,
|
||||
data: web::Json<TextFilter>,
|
||||
@ -657,7 +656,7 @@ pub async fn send_text_message(
|
||||
/// -d '{ "command": "reset" }' -H 'Authorization: Bearer <TOKEN>'
|
||||
/// ```
|
||||
#[post("/control/{id}/playout/")]
|
||||
#[protect(any("Role::Admin", "Role::User"), ty = "Role")]
|
||||
#[protect(any("Role::GlobalAdmin", "Role::User"), ty = "Role")]
|
||||
pub async fn control_playout(
|
||||
pool: web::Data<Pool<Sqlite>>,
|
||||
id: web::Path<i32>,
|
||||
@ -697,7 +696,7 @@ pub async fn control_playout(
|
||||
/// }
|
||||
/// ```
|
||||
#[get("/control/{id}/media/current")]
|
||||
#[protect(any("Role::Admin", "Role::User"), ty = "Role")]
|
||||
#[protect(any("Role::GlobalAdmin", "Role::User"), ty = "Role")]
|
||||
pub async fn media_current(
|
||||
id: web::Path<i32>,
|
||||
controllers: web::Data<Mutex<ChannelController>>,
|
||||
@ -722,7 +721,7 @@ pub async fn media_current(
|
||||
/// -d '{"command": "start"}'
|
||||
/// ```
|
||||
#[post("/control/{id}/process/")]
|
||||
#[protect(any("Role::Admin", "Role::User"), ty = "Role")]
|
||||
#[protect(any("Role::GlobalAdmin", "Role::User"), ty = "Role")]
|
||||
pub async fn process_control(
|
||||
_id: web::Path<i32>,
|
||||
_proc: web::Json<Process>,
|
||||
@ -740,7 +739,7 @@ pub async fn process_control(
|
||||
/// -H 'Content-Type: application/json' -H 'Authorization: Bearer <TOKEN>'
|
||||
/// ```
|
||||
#[get("/playlist/{id}")]
|
||||
#[protect(any("Role::Admin", "Role::User"), ty = "Role")]
|
||||
#[protect(any("Role::GlobalAdmin", "Role::User"), ty = "Role")]
|
||||
pub async fn get_playlist(
|
||||
id: web::Path<i32>,
|
||||
obj: web::Query<DateObj>,
|
||||
@ -763,7 +762,7 @@ pub async fn get_playlist(
|
||||
/// --data "{<JSON playlist data>}"
|
||||
/// ```
|
||||
#[post("/playlist/{id}/")]
|
||||
#[protect(any("Role::Admin", "Role::User"), ty = "Role")]
|
||||
#[protect(any("Role::GlobalAdmin", "Role::User"), ty = "Role")]
|
||||
pub async fn save_playlist(
|
||||
id: web::Path<i32>,
|
||||
data: web::Json<JsonPlaylist>,
|
||||
@ -797,34 +796,39 @@ pub async fn save_playlist(
|
||||
/// {"start": "10:00:00", "duration": "14:00:00", "shuffle": false, "paths": ["path/3", "path/4"]}]}}'
|
||||
/// ```
|
||||
#[post("/playlist/{id}/generate/{date}")]
|
||||
#[protect(any("Role::Admin", "Role::User"), ty = "Role")]
|
||||
#[protect(any("Role::GlobalAdmin", "Role::User"), ty = "Role")]
|
||||
pub async fn gen_playlist(
|
||||
params: web::Path<(i32, String)>,
|
||||
data: Option<web::Json<PathsObj>>,
|
||||
controllers: web::Data<Mutex<ChannelController>>,
|
||||
) -> Result<impl Responder, ServiceError> {
|
||||
let manager = controllers.lock().unwrap().get(params.0).unwrap();
|
||||
let channel_name = manager.channel.lock().unwrap().name.clone();
|
||||
let mut config = manager.config.lock().unwrap();
|
||||
config.general.generate = Some(vec![params.1.clone()]);
|
||||
manager.config.lock().unwrap().general.generate = Some(vec![params.1.clone()]);
|
||||
let storage_path = manager.config.lock().unwrap().storage.path.clone();
|
||||
|
||||
if let Some(obj) = data {
|
||||
if let Some(paths) = &obj.paths {
|
||||
let mut path_list = vec![];
|
||||
|
||||
for path in paths {
|
||||
let (p, _, _) = norm_abs_path(&config.storage.path, path)?;
|
||||
let (p, _, _) = norm_abs_path(&storage_path, path)?;
|
||||
|
||||
path_list.push(p);
|
||||
}
|
||||
|
||||
config.storage.paths = path_list;
|
||||
manager.config.lock().unwrap().storage.paths = path_list;
|
||||
}
|
||||
|
||||
config.general.template.clone_from(&obj.template);
|
||||
manager
|
||||
.config
|
||||
.lock()
|
||||
.unwrap()
|
||||
.general
|
||||
.template
|
||||
.clone_from(&obj.template);
|
||||
}
|
||||
|
||||
match generate_playlist(config.clone(), channel_name).await {
|
||||
match generate_playlist(manager).await {
|
||||
Ok(playlist) => Ok(web::Json(playlist)),
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
@ -837,7 +841,7 @@ pub async fn gen_playlist(
|
||||
/// -H 'Content-Type: application/json' -H 'Authorization: Bearer <TOKEN>'
|
||||
/// ```
|
||||
#[delete("/playlist/{id}/{date}")]
|
||||
#[protect(any("Role::Admin", "Role::User"), ty = "Role")]
|
||||
#[protect(any("Role::GlobalAdmin", "Role::User"), ty = "Role")]
|
||||
pub async fn del_playlist(
|
||||
params: web::Path<(i32, String)>,
|
||||
controllers: web::Data<Mutex<ChannelController>>,
|
||||
@ -860,7 +864,7 @@ pub async fn del_playlist(
|
||||
/// -H 'Content-Type: application/json' -H 'Authorization: Bearer <TOKEN>'
|
||||
/// ```
|
||||
#[get("/log/{id}")]
|
||||
#[protect(any("Role::Admin", "Role::User"), ty = "Role")]
|
||||
#[protect(any("Role::GlobalAdmin", "Role::User"), ty = "Role")]
|
||||
pub async fn get_log(
|
||||
id: web::Path<i32>,
|
||||
log: web::Query<DateObj>,
|
||||
@ -877,7 +881,7 @@ pub async fn get_log(
|
||||
/// -d '{ "source": "/" }' -H 'Authorization: Bearer <TOKEN>'
|
||||
/// ```
|
||||
#[post("/file/{id}/browse/")]
|
||||
#[protect(any("Role::Admin", "Role::User"), ty = "Role")]
|
||||
#[protect(any("Role::GlobalAdmin", "Role::User"), ty = "Role")]
|
||||
pub async fn file_browser(
|
||||
id: web::Path<i32>,
|
||||
data: web::Json<PathObject>,
|
||||
@ -900,7 +904,7 @@ pub async fn file_browser(
|
||||
/// -d '{"source": "<FOLDER PATH>"}' -H 'Authorization: Bearer <TOKEN>'
|
||||
/// ```
|
||||
#[post("/file/{id}/create-folder/")]
|
||||
#[protect(any("Role::Admin", "Role::User"), ty = "Role")]
|
||||
#[protect(any("Role::GlobalAdmin", "Role::User"), ty = "Role")]
|
||||
pub async fn add_dir(
|
||||
id: web::Path<i32>,
|
||||
data: web::Json<PathObject>,
|
||||
@ -919,7 +923,7 @@ pub async fn add_dir(
|
||||
/// -d '{"source": "<SOURCE>", "target": "<TARGET>"}' -H 'Authorization: Bearer <TOKEN>'
|
||||
/// ```
|
||||
#[post("/file/{id}/rename/")]
|
||||
#[protect(any("Role::Admin", "Role::User"), ty = "Role")]
|
||||
#[protect(any("Role::GlobalAdmin", "Role::User"), ty = "Role")]
|
||||
pub async fn move_rename(
|
||||
id: web::Path<i32>,
|
||||
data: web::Json<MoveObject>,
|
||||
@ -941,7 +945,7 @@ pub async fn move_rename(
|
||||
/// -d '{"source": "<SOURCE>"}' -H 'Authorization: Bearer <TOKEN>'
|
||||
/// ```
|
||||
#[post("/file/{id}/remove/")]
|
||||
#[protect(any("Role::Admin", "Role::User"), ty = "Role")]
|
||||
#[protect(any("Role::GlobalAdmin", "Role::User"), ty = "Role")]
|
||||
pub async fn remove(
|
||||
id: web::Path<i32>,
|
||||
data: web::Json<PathObject>,
|
||||
@ -963,7 +967,7 @@ pub async fn remove(
|
||||
/// -F "file=@file.mp4"
|
||||
/// ```
|
||||
#[put("/file/{id}/upload/")]
|
||||
#[protect(any("Role::Admin", "Role::User"), ty = "Role")]
|
||||
#[protect(any("Role::GlobalAdmin", "Role::User"), ty = "Role")]
|
||||
async fn save_file(
|
||||
id: web::Path<i32>,
|
||||
req: HttpRequest,
|
||||
@ -1051,7 +1055,7 @@ async fn get_public(public: web::Path<String>) -> Result<actix_files::NamedFile,
|
||||
/// -F "file=@list.m3u"
|
||||
/// ```
|
||||
#[put("/file/{id}/import/")]
|
||||
#[protect(any("Role::Admin", "Role::User"), ty = "Role")]
|
||||
#[protect(any("Role::GlobalAdmin", "Role::User"), ty = "Role")]
|
||||
async fn import_playlist(
|
||||
id: web::Path<i32>,
|
||||
req: HttpRequest,
|
||||
@ -1074,10 +1078,9 @@ async fn import_playlist(
|
||||
|
||||
upload(&config, size, payload, &path, true).await?;
|
||||
|
||||
let response = task::spawn_blocking(move || {
|
||||
import_file(&config, &obj.date, Some(channel_name), &path_clone)
|
||||
})
|
||||
.await??;
|
||||
let response =
|
||||
web::block(move || import_file(&config, &obj.date, Some(channel_name), &path_clone))
|
||||
.await??;
|
||||
|
||||
fs::remove_file(path).await?;
|
||||
|
||||
@ -1107,7 +1110,7 @@ async fn import_playlist(
|
||||
/// -H 'Authorization: Bearer <TOKEN>'
|
||||
/// ```
|
||||
#[get("/program/{id}/")]
|
||||
#[protect(any("Role::Admin", "Role::User"), ty = "Role")]
|
||||
#[protect(any("Role::GlobalAdmin", "Role::User"), ty = "Role")]
|
||||
async fn get_program(
|
||||
id: web::Path<i32>,
|
||||
obj: web::Query<ProgramObj>,
|
||||
@ -1195,7 +1198,7 @@ async fn get_program(
|
||||
/// -H 'Content-Type: application/json' -H 'Authorization: Bearer <TOKEN>'
|
||||
/// ```
|
||||
#[get("/system/{id}")]
|
||||
#[protect(any("Role::Admin", "Role::User"), ty = "Role")]
|
||||
#[protect(any("Role::GlobalAdmin", "Role::User"), ty = "Role")]
|
||||
pub async fn get_system_stat(
|
||||
id: web::Path<i32>,
|
||||
controllers: web::Data<Mutex<ChannelController>>,
|
||||
|
@ -18,7 +18,7 @@ pub async fn db_migrate(conn: &Pool<Sqlite>) -> Result<&'static str, Box<dyn std
|
||||
Err(e) => panic!("{e}"),
|
||||
}
|
||||
|
||||
if let Err(_) = select_global(conn).await {
|
||||
if select_global(conn).await.is_err() {
|
||||
let secret: String = rand::thread_rng()
|
||||
.sample_iter(&Alphanumeric)
|
||||
.take(80)
|
||||
@ -85,14 +85,14 @@ pub async fn update_channel(
|
||||
pub async fn update_stat(
|
||||
conn: &Pool<Sqlite>,
|
||||
id: i32,
|
||||
current_date: String,
|
||||
last_date: String,
|
||||
time_shift: f64,
|
||||
) -> Result<SqliteQueryResult, sqlx::Error> {
|
||||
let query = "UPDATE channels SET current_date = $2, time_shift = $3 WHERE id = $1";
|
||||
let query = "UPDATE channels SET last_date = $2, time_shift = $3 WHERE id = $1";
|
||||
|
||||
sqlx::query(query)
|
||||
.bind(id)
|
||||
.bind(current_date)
|
||||
.bind(last_date)
|
||||
.bind(time_shift)
|
||||
.execute(conn)
|
||||
.await
|
||||
|
@ -110,7 +110,7 @@ pub struct Channel {
|
||||
pub preview_url: String,
|
||||
pub extra_extensions: String,
|
||||
pub active: bool,
|
||||
pub current_date: Option<String>,
|
||||
pub last_date: Option<String>,
|
||||
pub time_shift: f64,
|
||||
|
||||
#[sqlx(default)]
|
||||
@ -192,7 +192,6 @@ pub struct Configuration {
|
||||
|
||||
pub text_help: String,
|
||||
pub add_text: bool,
|
||||
|
||||
pub fontfile: String,
|
||||
pub text_from_filename: bool,
|
||||
pub style: String,
|
||||
|
@ -56,7 +56,7 @@ pub struct ChannelManager {
|
||||
pub ingest_is_running: Arc<AtomicBool>,
|
||||
pub is_terminated: Arc<AtomicBool>,
|
||||
pub is_alive: Arc<AtomicBool>,
|
||||
pub chain: Option<Arc<Mutex<Vec<String>>>>,
|
||||
pub filter_chain: Option<Arc<Mutex<Vec<String>>>>,
|
||||
pub current_date: Arc<Mutex<String>>,
|
||||
pub list_init: Arc<AtomicBool>,
|
||||
pub current_media: Arc<Mutex<Option<Media>>>,
|
||||
@ -84,13 +84,13 @@ impl ChannelManager {
|
||||
pub fn update_channel(self, other: &Channel) {
|
||||
let mut channel = self.channel.lock().unwrap();
|
||||
|
||||
channel.name = other.name.clone();
|
||||
channel.preview_url = other.preview_url.clone();
|
||||
channel.extra_extensions = other.extra_extensions.clone();
|
||||
channel.active = other.active.clone();
|
||||
channel.current_date = other.current_date.clone();
|
||||
channel.time_shift = other.time_shift.clone();
|
||||
channel.utc_offset = other.utc_offset.clone();
|
||||
channel.name.clone_from(&other.name);
|
||||
channel.preview_url.clone_from(&other.preview_url);
|
||||
channel.extra_extensions.clone_from(&other.extra_extensions);
|
||||
channel.active.clone_from(&other.active);
|
||||
channel.last_date.clone_from(&other.last_date);
|
||||
channel.time_shift.clone_from(&other.time_shift);
|
||||
channel.utc_offset.clone_from(&other.utc_offset);
|
||||
}
|
||||
|
||||
pub fn stop(&self, unit: ProcessUnit) -> Result<(), ProcessError> {
|
||||
|
@ -24,9 +24,7 @@ pub fn source_generator(
|
||||
) -> Box<dyn Iterator<Item = Media>> {
|
||||
let config = manager.config.lock().unwrap().clone();
|
||||
let is_terminated = manager.is_terminated.clone();
|
||||
let chain = manager.chain.clone();
|
||||
let current_list = manager.current_list.clone();
|
||||
let current_index = manager.current_index.clone();
|
||||
|
||||
match config.processing.mode {
|
||||
Folder => {
|
||||
@ -37,8 +35,7 @@ pub fn source_generator(
|
||||
);
|
||||
|
||||
let config_clone = config.clone();
|
||||
let folder_source =
|
||||
FolderSource::new(&config, chain, current_list.clone(), current_index);
|
||||
let folder_source = FolderSource::new(&config, manager);
|
||||
let list_clone = current_list.clone();
|
||||
|
||||
// Spawn a thread to monitor folder for file changes.
|
||||
|
@ -2,11 +2,10 @@ use std::{
|
||||
path::Path,
|
||||
sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
Arc,
|
||||
Arc, Mutex,
|
||||
},
|
||||
};
|
||||
|
||||
use futures::executor;
|
||||
use simplelog::*;
|
||||
use sqlx::{Pool, Sqlite};
|
||||
|
||||
@ -101,7 +100,7 @@ impl CurrentProgram {
|
||||
.channel
|
||||
.lock()
|
||||
.unwrap()
|
||||
.current_date
|
||||
.last_date
|
||||
.clone()
|
||||
.unwrap_or_default()
|
||||
!= self.json_playlist.date
|
||||
@ -198,7 +197,7 @@ impl CurrentProgram {
|
||||
}
|
||||
|
||||
fn set_status(&mut self, date: String) {
|
||||
if self.manager.channel.lock().unwrap().current_date != Some(date.clone())
|
||||
if self.manager.channel.lock().unwrap().last_date != Some(date.clone())
|
||||
&& self.manager.channel.lock().unwrap().time_shift != 0.0
|
||||
{
|
||||
info!("Reset playout status");
|
||||
@ -209,16 +208,19 @@ impl CurrentProgram {
|
||||
.channel
|
||||
.lock()
|
||||
.unwrap()
|
||||
.current_date
|
||||
.last_date
|
||||
.clone_from(&Some(date.clone()));
|
||||
self.manager.channel.lock().unwrap().time_shift = 0.0;
|
||||
|
||||
if let Err(e) = executor::block_on(handles::update_stat(
|
||||
&self.db_pool,
|
||||
self.config.general.channel_id,
|
||||
date,
|
||||
0.0,
|
||||
)) {
|
||||
if let Err(e) = tokio::runtime::Runtime::new()
|
||||
.unwrap()
|
||||
.block_on(handles::update_stat(
|
||||
&self.db_pool,
|
||||
self.config.general.channel_id,
|
||||
date,
|
||||
0.0,
|
||||
))
|
||||
{
|
||||
error!("Unable to write status: {e}");
|
||||
};
|
||||
}
|
||||
@ -255,7 +257,7 @@ impl CurrentProgram {
|
||||
// On init or reload we need to seek for the current clip.
|
||||
fn get_current_clip(&mut self) {
|
||||
let mut time_sec = self.get_current_time();
|
||||
let shift = self.manager.channel.lock().unwrap().time_shift.clone();
|
||||
let shift = self.manager.channel.lock().unwrap().time_shift;
|
||||
|
||||
if shift != 0.0 {
|
||||
info!("Shift playlist start for <yellow>{shift:.3}</> seconds");
|
||||
@ -306,13 +308,8 @@ impl CurrentProgram {
|
||||
|
||||
self.manager.current_index.fetch_add(1, Ordering::SeqCst);
|
||||
|
||||
self.current_node = handle_list_init(
|
||||
&self.config,
|
||||
node_clone,
|
||||
&self.playout_stat,
|
||||
&self.player_control,
|
||||
last_index,
|
||||
);
|
||||
self.current_node =
|
||||
handle_list_init(&self.config, node_clone, &self.manager, last_index);
|
||||
|
||||
if self
|
||||
.current_node
|
||||
@ -337,15 +334,9 @@ impl CurrentProgram {
|
||||
|
||||
self.last_next_ad(&mut media);
|
||||
|
||||
self.current_node = gen_source(
|
||||
&self.config,
|
||||
media,
|
||||
&self.playout_stat,
|
||||
&self.player_control,
|
||||
0,
|
||||
);
|
||||
self.current_node = gen_source(&self.config, media, &self.manager, 0);
|
||||
|
||||
self.player_control
|
||||
self.manager
|
||||
.current_list
|
||||
.lock()
|
||||
.unwrap()
|
||||
@ -353,11 +344,9 @@ impl CurrentProgram {
|
||||
|
||||
self.current_node.last_ad = self.last_node_ad;
|
||||
self.current_node
|
||||
.add_filter(&self.config, &self.playout_stat.chain);
|
||||
.add_filter(&self.config, &self.manager.filter_chain);
|
||||
|
||||
self.player_control
|
||||
.current_index
|
||||
.fetch_add(1, Ordering::SeqCst);
|
||||
self.manager.current_index.fetch_add(1, Ordering::SeqCst);
|
||||
}
|
||||
|
||||
fn recalculate_begin(&mut self, extend: bool) {
|
||||
@ -371,7 +360,7 @@ impl CurrentProgram {
|
||||
|
||||
self.json_playlist.start_sec = Some(time_sec);
|
||||
set_defaults(&mut self.json_playlist);
|
||||
self.player_control
|
||||
self.manager
|
||||
.current_list
|
||||
.lock()
|
||||
.unwrap()
|
||||
@ -386,9 +375,9 @@ impl Iterator for CurrentProgram {
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
self.last_json_path.clone_from(&self.json_playlist.path);
|
||||
self.last_node_ad = self.current_node.last_ad;
|
||||
self.check_for_playlist(self.playout_stat.list_init.load(Ordering::SeqCst));
|
||||
self.check_for_playlist(self.manager.list_init.load(Ordering::SeqCst));
|
||||
|
||||
if self.playout_stat.list_init.load(Ordering::SeqCst) {
|
||||
if self.manager.list_init.load(Ordering::SeqCst) {
|
||||
trace!("Init playlist, from next iterator");
|
||||
let mut init_clip_is_filler = false;
|
||||
|
||||
@ -396,7 +385,7 @@ impl Iterator for CurrentProgram {
|
||||
init_clip_is_filler = self.init_clip();
|
||||
}
|
||||
|
||||
if self.playout_stat.list_init.load(Ordering::SeqCst) && !init_clip_is_filler {
|
||||
if self.manager.list_init.load(Ordering::SeqCst) && !init_clip_is_filler {
|
||||
// On init load, playlist could be not long enough, or clips are not found
|
||||
// so we fill the gap with a dummy.
|
||||
trace!("Init clip is no filler");
|
||||
@ -409,7 +398,7 @@ impl Iterator for CurrentProgram {
|
||||
}
|
||||
|
||||
let mut last_index = 0;
|
||||
let length = self.player_control.current_list.lock().unwrap().len();
|
||||
let length = self.manager.current_list.lock().unwrap().len();
|
||||
|
||||
if length > 0 {
|
||||
last_index = length - 1;
|
||||
@ -422,26 +411,20 @@ impl Iterator for CurrentProgram {
|
||||
|
||||
self.last_next_ad(&mut media);
|
||||
|
||||
self.current_node = gen_source(
|
||||
&self.config,
|
||||
media,
|
||||
&self.playout_stat,
|
||||
&self.player_control,
|
||||
last_index,
|
||||
);
|
||||
self.current_node = gen_source(&self.config, media, &self.manager, last_index);
|
||||
}
|
||||
|
||||
return Some(self.current_node.clone());
|
||||
}
|
||||
|
||||
if self.player_control.current_index.load(Ordering::SeqCst)
|
||||
< self.player_control.current_list.lock().unwrap().len()
|
||||
if self.manager.current_index.load(Ordering::SeqCst)
|
||||
< self.manager.current_list.lock().unwrap().len()
|
||||
{
|
||||
// get next clip from current playlist
|
||||
|
||||
let mut is_last = false;
|
||||
let index = self.player_control.current_index.load(Ordering::SeqCst);
|
||||
let node_list = self.player_control.current_list.lock().unwrap();
|
||||
let index = self.manager.current_index.load(Ordering::SeqCst);
|
||||
let node_list = self.manager.current_list.lock().unwrap();
|
||||
let mut node = node_list[index].clone();
|
||||
let last_index = node_list.len() - 1;
|
||||
|
||||
@ -453,18 +436,10 @@ impl Iterator for CurrentProgram {
|
||||
|
||||
self.last_next_ad(&mut node);
|
||||
|
||||
self.current_node = timed_source(
|
||||
node,
|
||||
&self.config,
|
||||
is_last,
|
||||
&self.playout_stat,
|
||||
&self.player_control,
|
||||
last_index,
|
||||
);
|
||||
self.current_node =
|
||||
timed_source(node, &self.config, is_last, &self.manager, last_index);
|
||||
|
||||
self.player_control
|
||||
.current_index
|
||||
.fetch_add(1, Ordering::SeqCst);
|
||||
self.manager.current_index.fetch_add(1, Ordering::SeqCst);
|
||||
|
||||
Some(self.current_node.clone())
|
||||
} else {
|
||||
@ -484,7 +459,7 @@ impl Iterator for CurrentProgram {
|
||||
}
|
||||
// Get first clip from next playlist.
|
||||
|
||||
let c_list = self.player_control.current_list.lock().unwrap();
|
||||
let c_list = self.manager.current_list.lock().unwrap();
|
||||
let mut first_node = c_list[0].clone();
|
||||
|
||||
drop(c_list);
|
||||
@ -493,19 +468,13 @@ impl Iterator for CurrentProgram {
|
||||
self.recalculate_begin(false)
|
||||
}
|
||||
|
||||
self.player_control.current_index.store(0, Ordering::SeqCst);
|
||||
self.manager.current_index.store(0, Ordering::SeqCst);
|
||||
self.last_next_ad(&mut first_node);
|
||||
first_node.last_ad = self.last_node_ad;
|
||||
|
||||
self.current_node = gen_source(
|
||||
&self.config,
|
||||
first_node,
|
||||
&self.playout_stat,
|
||||
&self.player_control,
|
||||
0,
|
||||
);
|
||||
self.current_node = gen_source(&self.config, first_node, &self.manager, 0);
|
||||
|
||||
self.player_control.current_index.store(1, Ordering::SeqCst);
|
||||
self.manager.current_index.store(1, Ordering::SeqCst);
|
||||
|
||||
Some(self.current_node.clone())
|
||||
}
|
||||
@ -520,10 +489,12 @@ fn timed_source(
|
||||
node: Media,
|
||||
config: &PlayoutConfig,
|
||||
last: bool,
|
||||
playout_stat: &PlayoutStatus,
|
||||
player_control: &PlayerControl,
|
||||
manager: &ChannelManager,
|
||||
last_index: usize,
|
||||
) -> Media {
|
||||
let time_shift = manager.channel.lock().unwrap().time_shift;
|
||||
let current_date = manager.current_date.lock().unwrap().clone();
|
||||
let last_date = manager.channel.lock().unwrap().last_date.clone();
|
||||
let (delta, total_delta) = get_delta(config, &node.begin.unwrap());
|
||||
let mut shifted_delta = delta;
|
||||
let mut new_node = node.clone();
|
||||
@ -533,12 +504,8 @@ fn timed_source(
|
||||
trace!("timed source is last: {last}");
|
||||
|
||||
if config.playlist.length.contains(':') {
|
||||
let time_shift = playout_stat.time_shift.lock().unwrap();
|
||||
|
||||
if *playout_stat.current_date.lock().unwrap() == *playout_stat.date.lock().unwrap()
|
||||
&& *time_shift != 0.0
|
||||
{
|
||||
shifted_delta = delta - *time_shift;
|
||||
if Some(current_date) == last_date && time_shift != 0.0 {
|
||||
shifted_delta = delta - time_shift;
|
||||
|
||||
debug!("Delta: <yellow>{shifted_delta:.3}</>, shifted: <yellow>{delta:.3}</>");
|
||||
} else {
|
||||
@ -563,26 +530,19 @@ fn timed_source(
|
||||
{
|
||||
// when we are in the 24 hour range, get the clip
|
||||
new_node.process = Some(true);
|
||||
new_node = gen_source(config, node, playout_stat, player_control, last_index);
|
||||
new_node = gen_source(config, node, manager, last_index);
|
||||
} else if total_delta <= 0.0 {
|
||||
info!("Begin is over play time, skip: {}", node.source);
|
||||
} else if total_delta < node.duration - node.seek || last {
|
||||
new_node = handle_list_end(
|
||||
config,
|
||||
node,
|
||||
total_delta,
|
||||
playout_stat,
|
||||
player_control,
|
||||
last_index,
|
||||
);
|
||||
new_node = handle_list_end(config, node, total_delta, manager, last_index);
|
||||
}
|
||||
|
||||
new_node
|
||||
}
|
||||
|
||||
fn duplicate_for_seek_and_loop(node: &mut Media, player_control: &PlayerControl) {
|
||||
fn duplicate_for_seek_and_loop(node: &mut Media, current_list: &Arc<Mutex<Vec<Media>>>) {
|
||||
warn!("Clip loops and has seek value: duplicate clip to separate loop and seek.");
|
||||
let mut nodes = player_control.current_list.lock().unwrap();
|
||||
let mut nodes = current_list.lock().unwrap();
|
||||
let index = node.index.unwrap_or_default();
|
||||
|
||||
let mut node_duplicate = node.clone();
|
||||
@ -617,8 +577,7 @@ fn duplicate_for_seek_and_loop(node: &mut Media, player_control: &PlayerControl)
|
||||
pub fn gen_source(
|
||||
config: &PlayoutConfig,
|
||||
mut node: Media,
|
||||
playout_stat: &PlayoutStatus,
|
||||
player_control: &PlayerControl,
|
||||
manager: &ChannelManager,
|
||||
last_index: usize,
|
||||
) -> Media {
|
||||
let node_index = node.index.unwrap_or_default();
|
||||
@ -658,7 +617,7 @@ pub fn gen_source(
|
||||
node.cmd = Some(loop_image(&node));
|
||||
} else {
|
||||
if node.seek > 0.0 && node.out > node.duration {
|
||||
duplicate_for_seek_and_loop(&mut node, player_control);
|
||||
duplicate_for_seek_and_loop(&mut node, &manager.current_list);
|
||||
}
|
||||
|
||||
node.cmd = Some(seek_and_length(&mut node));
|
||||
@ -671,25 +630,25 @@ pub fn gen_source(
|
||||
error!("Source not found: <b><magenta>{}</></b>", node.source);
|
||||
}
|
||||
|
||||
let mut filler_list = vec![];
|
||||
let mut fillers = vec![];
|
||||
|
||||
match player_control.filler_list.try_lock() {
|
||||
Ok(list) => filler_list = list.to_vec(),
|
||||
match manager.filler_list.try_lock() {
|
||||
Ok(list) => fillers = list.to_vec(),
|
||||
Err(e) => error!("Lock filler list error: {e}"),
|
||||
}
|
||||
|
||||
// Set list_init to true, to stay in sync.
|
||||
playout_stat.list_init.store(true, Ordering::SeqCst);
|
||||
manager.list_init.store(true, Ordering::SeqCst);
|
||||
|
||||
if config.storage.filler.is_dir() && !filler_list.is_empty() {
|
||||
let filler_index = player_control.filler_index.fetch_add(1, Ordering::SeqCst);
|
||||
let mut filler_media = filler_list[filler_index].clone();
|
||||
if config.storage.filler.is_dir() && !fillers.is_empty() {
|
||||
let index = manager.filler_index.fetch_add(1, Ordering::SeqCst);
|
||||
let mut filler_media = fillers[index].clone();
|
||||
|
||||
trace!("take filler: {}", filler_media.source);
|
||||
|
||||
if filler_index == filler_list.len() - 1 {
|
||||
if index == fillers.len() - 1 {
|
||||
// reset index for next round
|
||||
player_control.filler_index.store(0, Ordering::SeqCst)
|
||||
manager.filler_index.store(0, Ordering::SeqCst)
|
||||
}
|
||||
|
||||
if filler_media.probe.is_none() {
|
||||
@ -776,7 +735,7 @@ pub fn gen_source(
|
||||
);
|
||||
}
|
||||
|
||||
node.add_filter(config, &playout_stat.chain);
|
||||
node.add_filter(config, &manager.filter_chain.clone());
|
||||
|
||||
trace!(
|
||||
"return gen_source: {}, seek: {}, out: {}",
|
||||
@ -793,8 +752,7 @@ pub fn gen_source(
|
||||
fn handle_list_init(
|
||||
config: &PlayoutConfig,
|
||||
mut node: Media,
|
||||
playout_stat: &PlayoutStatus,
|
||||
player_control: &PlayerControl,
|
||||
manager: &ChannelManager,
|
||||
last_index: usize,
|
||||
) -> Media {
|
||||
debug!("Playlist init");
|
||||
@ -804,7 +762,7 @@ fn handle_list_init(
|
||||
node.out = total_delta + node.seek;
|
||||
}
|
||||
|
||||
gen_source(config, node, playout_stat, player_control, last_index)
|
||||
gen_source(config, node, manager, last_index)
|
||||
}
|
||||
|
||||
/// when we come to last clip in playlist,
|
||||
@ -814,8 +772,7 @@ fn handle_list_end(
|
||||
config: &PlayoutConfig,
|
||||
mut node: Media,
|
||||
total_delta: f64,
|
||||
playout_stat: &PlayoutStatus,
|
||||
player_control: &PlayerControl,
|
||||
manager: &ChannelManager,
|
||||
last_index: usize,
|
||||
) -> Media {
|
||||
debug!("Last clip from day");
|
||||
@ -844,5 +801,5 @@ fn handle_list_end(
|
||||
|
||||
node.process = Some(true);
|
||||
|
||||
gen_source(config, node, playout_stat, player_control, last_index)
|
||||
gen_source(config, node, manager, last_index)
|
||||
}
|
||||
|
@ -46,7 +46,7 @@ use crate::{
|
||||
fn ingest_to_hls_server(manager: ChannelManager) -> Result<(), ProcessError> {
|
||||
let config = manager.config.lock().unwrap();
|
||||
let playlist_init = manager.list_init.clone();
|
||||
let chain = manager.chain.clone();
|
||||
let chain = manager.filter_chain.clone();
|
||||
|
||||
let mut server_prefix = vec_strings!["-hide_banner", "-nostats", "-v", "level+info"];
|
||||
let stream_input = config.ingest.input_cmd.clone().unwrap();
|
||||
|
@ -1,5 +1,5 @@
|
||||
use std::sync::{
|
||||
atomic::{AtomicUsize, Ordering},
|
||||
atomic::Ordering,
|
||||
{Arc, Mutex},
|
||||
};
|
||||
|
||||
@ -8,27 +8,22 @@ use rand::{seq::SliceRandom, thread_rng};
|
||||
use simplelog::*;
|
||||
use walkdir::WalkDir;
|
||||
|
||||
use crate::player::utils::{include_file_extension, time_in_seconds, Media, PlayoutConfig};
|
||||
use crate::player::{
|
||||
controller::ChannelManager,
|
||||
utils::{include_file_extension, time_in_seconds, Media, PlayoutConfig},
|
||||
};
|
||||
|
||||
/// Folder Sources
|
||||
///
|
||||
/// Like playlist source, we create here a folder list for iterate over it.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct FolderSource {
|
||||
config: PlayoutConfig,
|
||||
filter_chain: Option<Arc<Mutex<Vec<String>>>>,
|
||||
pub current_list: Arc<Mutex<Vec<Media>>>,
|
||||
pub current_index: Arc<AtomicUsize>,
|
||||
manager: ChannelManager,
|
||||
current_node: Media,
|
||||
}
|
||||
|
||||
impl FolderSource {
|
||||
pub fn new(
|
||||
config: &PlayoutConfig,
|
||||
filter_chain: Option<Arc<Mutex<Vec<String>>>>,
|
||||
current_list: Arc<Mutex<Vec<Media>>>,
|
||||
current_index: Arc<AtomicUsize>,
|
||||
) -> Self {
|
||||
pub fn new(config: &PlayoutConfig, manager: ChannelManager) -> Self {
|
||||
let mut path_list = vec![];
|
||||
let mut media_list = vec![];
|
||||
let mut index: usize = 0;
|
||||
@ -78,38 +73,26 @@ impl FolderSource {
|
||||
index += 1;
|
||||
}
|
||||
|
||||
*current_list.lock().unwrap() = media_list;
|
||||
*manager.current_list.lock().unwrap() = media_list;
|
||||
|
||||
Self {
|
||||
config: config.clone(),
|
||||
filter_chain,
|
||||
current_list,
|
||||
current_index,
|
||||
manager,
|
||||
current_node: Media::new(0, "", false),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_list(
|
||||
config: &PlayoutConfig,
|
||||
filter_chain: Option<Arc<Mutex<Vec<String>>>>,
|
||||
current_list: Arc<Mutex<Vec<Media>>>,
|
||||
current_index: Arc<AtomicUsize>,
|
||||
list: Vec<Media>,
|
||||
) -> Self {
|
||||
*current_list.lock().unwrap() = list;
|
||||
pub fn from_list(manager: &ChannelManager, list: Vec<Media>) -> Self {
|
||||
*manager.current_list.lock().unwrap() = list;
|
||||
|
||||
Self {
|
||||
config: config.clone(),
|
||||
filter_chain,
|
||||
current_list,
|
||||
current_index,
|
||||
manager: manager.clone(),
|
||||
current_node: Media::new(0, "", false),
|
||||
}
|
||||
}
|
||||
|
||||
fn shuffle(&mut self) {
|
||||
let mut rng = thread_rng();
|
||||
let mut nodes = self.current_list.lock().unwrap();
|
||||
let mut nodes = self.manager.current_list.lock().unwrap();
|
||||
|
||||
nodes.shuffle(&mut rng);
|
||||
|
||||
@ -119,7 +102,7 @@ impl FolderSource {
|
||||
}
|
||||
|
||||
fn sort(&mut self) {
|
||||
let mut nodes = self.current_list.lock().unwrap();
|
||||
let mut nodes = self.manager.current_list.lock().unwrap();
|
||||
|
||||
nodes.sort_by(|d1, d2| d1.source.cmp(&d2.source));
|
||||
|
||||
@ -134,39 +117,43 @@ impl Iterator for FolderSource {
|
||||
type Item = Media;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if self.current_index.load(Ordering::SeqCst) < self.current_list.lock().unwrap().len() {
|
||||
let i = self.current_index.load(Ordering::SeqCst);
|
||||
self.current_node = self.current_list.lock().unwrap()[i].clone();
|
||||
let config = self.manager.config.lock().unwrap().clone();
|
||||
|
||||
if self.manager.current_index.load(Ordering::SeqCst)
|
||||
< self.manager.current_list.lock().unwrap().len()
|
||||
{
|
||||
let i = self.manager.current_index.load(Ordering::SeqCst);
|
||||
self.current_node = self.manager.current_list.lock().unwrap()[i].clone();
|
||||
let _ = self.current_node.add_probe(false).ok();
|
||||
self.current_node
|
||||
.add_filter(&self.config, &self.filter_chain);
|
||||
.add_filter(&config, &self.manager.filter_chain);
|
||||
self.current_node.begin = Some(time_in_seconds());
|
||||
|
||||
self.current_index.fetch_add(1, Ordering::SeqCst);
|
||||
self.manager.current_index.fetch_add(1, Ordering::SeqCst);
|
||||
|
||||
Some(self.current_node.clone())
|
||||
} else {
|
||||
if self.config.storage.shuffle {
|
||||
if self.config.general.generate.is_none() {
|
||||
if config.storage.shuffle {
|
||||
if config.general.generate.is_none() {
|
||||
info!("Shuffle files");
|
||||
}
|
||||
|
||||
self.shuffle();
|
||||
} else {
|
||||
if self.config.general.generate.is_none() {
|
||||
if config.general.generate.is_none() {
|
||||
info!("Sort files");
|
||||
}
|
||||
|
||||
self.sort();
|
||||
}
|
||||
|
||||
self.current_node = self.current_list.lock().unwrap()[0].clone();
|
||||
self.current_node = self.manager.current_list.lock().unwrap()[0].clone();
|
||||
let _ = self.current_node.add_probe(false).ok();
|
||||
self.current_node
|
||||
.add_filter(&self.config, &self.filter_chain);
|
||||
.add_filter(&config, &self.manager.filter_chain);
|
||||
self.current_node.begin = Some(time_in_seconds());
|
||||
|
||||
self.current_index.store(1, Ordering::SeqCst);
|
||||
self.manager.current_index.store(1, Ordering::SeqCst);
|
||||
|
||||
Some(self.current_node.clone())
|
||||
}
|
||||
|
@ -28,7 +28,7 @@ impl User {
|
||||
/// curl -X GET 'http://127.0.0.1:8787/api/generate-uuid' -H 'Authorization: Bearer <TOKEN>'
|
||||
/// ```
|
||||
#[post("/generate-uuid")]
|
||||
#[protect(any("Role::Admin", "Role::User"), ty = "Role")]
|
||||
#[protect(any("Role::GlobalAdmin", "Role::User"), ty = "Role")]
|
||||
async fn generate_uuid(data: web::Data<AuthState>) -> Result<impl Responder, ServiceError> {
|
||||
let mut uuids = data.uuids.lock().await;
|
||||
let new_uuid = UuidData::new();
|
||||
|
@ -83,10 +83,11 @@ impl FromStr for OutputMode {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Eq, PartialEq)]
|
||||
#[derive(Debug, Default, Clone, Serialize, Deserialize, Eq, PartialEq)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum ProcessMode {
|
||||
Folder,
|
||||
#[default]
|
||||
Playlist,
|
||||
}
|
||||
|
||||
@ -99,12 +100,6 @@ impl ProcessMode {
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ProcessMode {
|
||||
fn default() -> Self {
|
||||
ProcessMode::Playlist
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for ProcessMode {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match *self {
|
||||
@ -144,6 +139,7 @@ pub struct Source {
|
||||
/// This we init ones, when ffplayout is starting and use them globally in the hole program.
|
||||
#[derive(Debug, Default, Clone, Deserialize, Serialize)]
|
||||
pub struct PlayoutConfig {
|
||||
#[serde(skip_serializing)]
|
||||
pub advanced: AdvancedConfig,
|
||||
pub general: General,
|
||||
pub mail: Mail,
|
||||
@ -160,13 +156,20 @@ pub struct PlayoutConfig {
|
||||
#[derive(Debug, Default, Clone, Deserialize, Serialize)]
|
||||
pub struct General {
|
||||
pub help_text: String,
|
||||
#[serde(skip_serializing)]
|
||||
pub channel_id: i32,
|
||||
pub stop_threshold: f64,
|
||||
#[serde(skip_serializing)]
|
||||
pub generate: Option<Vec<String>>,
|
||||
#[serde(skip_serializing)]
|
||||
pub ffmpeg_filters: Vec<String>,
|
||||
#[serde(skip_serializing)]
|
||||
pub ffmpeg_libs: Vec<String>,
|
||||
#[serde(skip_serializing)]
|
||||
pub template: Option<Template>,
|
||||
#[serde(skip_serializing)]
|
||||
pub skip_validation: bool,
|
||||
#[serde(skip_serializing)]
|
||||
pub validate: bool,
|
||||
}
|
||||
|
||||
@ -278,6 +281,7 @@ pub struct Processing {
|
||||
pub audio_channels: u8,
|
||||
pub volume: f64,
|
||||
pub custom_filter: String,
|
||||
#[serde(skip_serializing)]
|
||||
pub cmd: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
@ -314,6 +318,7 @@ pub struct Ingest {
|
||||
pub enable: bool,
|
||||
input_param: String,
|
||||
pub custom_filter: String,
|
||||
#[serde(skip_serializing)]
|
||||
pub input_cmd: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
@ -334,8 +339,10 @@ pub struct Playlist {
|
||||
pub help_text: String,
|
||||
pub path: PathBuf,
|
||||
pub day_start: String,
|
||||
#[serde(skip_serializing)]
|
||||
pub start_sec: Option<f64>,
|
||||
pub length: String,
|
||||
#[serde(skip_serializing)]
|
||||
pub length_sec: Option<f64>,
|
||||
pub infinit: bool,
|
||||
}
|
||||
@ -358,6 +365,7 @@ impl Playlist {
|
||||
pub struct Storage {
|
||||
pub help_text: String,
|
||||
pub path: PathBuf,
|
||||
#[serde(skip_serializing)]
|
||||
pub paths: Vec<PathBuf>,
|
||||
pub filler: PathBuf,
|
||||
pub extensions: Vec<String>,
|
||||
@ -385,8 +393,11 @@ impl Storage {
|
||||
pub struct Text {
|
||||
pub help_text: String,
|
||||
pub add_text: bool,
|
||||
#[serde(skip_serializing)]
|
||||
pub node_pos: Option<usize>,
|
||||
#[serde(skip_serializing)]
|
||||
pub zmq_stream_socket: Option<String>,
|
||||
#[serde(skip_serializing)]
|
||||
pub zmq_server_socket: Option<String>,
|
||||
pub fontfile: String,
|
||||
pub text_from_filename: bool,
|
||||
@ -398,7 +409,7 @@ impl Text {
|
||||
fn new(config: &Configuration) -> Self {
|
||||
Self {
|
||||
help_text: config.text_help.clone(),
|
||||
add_text: config.add_text.clone(),
|
||||
add_text: config.add_text,
|
||||
node_pos: None,
|
||||
zmq_stream_socket: None,
|
||||
zmq_server_socket: None,
|
||||
@ -432,8 +443,11 @@ pub struct Output {
|
||||
pub help_text: String,
|
||||
pub mode: OutputMode,
|
||||
pub output_param: String,
|
||||
#[serde(skip_serializing)]
|
||||
pub output_count: usize,
|
||||
#[serde(skip_serializing)]
|
||||
pub output_filter: Option<String>,
|
||||
#[serde(skip_serializing)]
|
||||
pub output_cmd: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
|
@ -112,7 +112,7 @@ pub async fn send_message(
|
||||
let config = manager.config.lock().unwrap().clone();
|
||||
|
||||
if config.text.zmq_stream_socket.is_some() {
|
||||
if let Some(clips_filter) = manager.chain.clone() {
|
||||
if let Some(clips_filter) = manager.filter_chain.clone() {
|
||||
*clips_filter.lock().unwrap() = vec![filter.clone()];
|
||||
}
|
||||
|
||||
@ -161,8 +161,8 @@ pub async fn control_state(
|
||||
) -> Result<Map<String, Value>, ServiceError> {
|
||||
let config = manager.config.lock().unwrap().clone();
|
||||
let current_date = manager.current_date.lock().unwrap().clone();
|
||||
let current_list = manager.current_list.lock().unwrap();
|
||||
let mut date = manager.current_date.lock().unwrap();
|
||||
let current_list = manager.current_list.lock().unwrap().clone();
|
||||
let mut date = manager.current_date.lock().unwrap().clone();
|
||||
let index = manager.current_index.load(Ordering::SeqCst);
|
||||
|
||||
match command {
|
||||
@ -176,30 +176,29 @@ pub async fn control_state(
|
||||
if let Err(e) = proc.wait() {
|
||||
error!("Decoder {e:?}")
|
||||
};
|
||||
|
||||
info!("Move to last clip");
|
||||
let mut data_map = Map::new();
|
||||
let mut media = current_list[index - 2].clone();
|
||||
manager.current_index.fetch_sub(2, Ordering::SeqCst);
|
||||
|
||||
if let Err(e) = media.add_probe(false) {
|
||||
error!("{e:?}");
|
||||
};
|
||||
|
||||
let (delta, _) = get_delta(&config, &media.begin.unwrap_or(0.0));
|
||||
manager.channel.lock().unwrap().time_shift = delta;
|
||||
date.clone_from(¤t_date);
|
||||
handles::update_stat(conn, config.general.channel_id, current_date, delta)
|
||||
.await?;
|
||||
|
||||
data_map.insert("operation".to_string(), json!("move_to_last"));
|
||||
data_map.insert("shifted_seconds".to_string(), json!(delta));
|
||||
data_map.insert("media".to_string(), get_media_map(media));
|
||||
|
||||
return Ok(data_map);
|
||||
} else {
|
||||
return Err(ServiceError::InternalServerError);
|
||||
}
|
||||
|
||||
return Err(ServiceError::InternalServerError);
|
||||
info!("Move to last clip");
|
||||
let mut data_map = Map::new();
|
||||
let mut media = current_list[index - 2].clone();
|
||||
manager.current_index.fetch_sub(2, Ordering::SeqCst);
|
||||
|
||||
if let Err(e) = media.add_probe(false) {
|
||||
error!("{e:?}");
|
||||
};
|
||||
|
||||
let (delta, _) = get_delta(&config, &media.begin.unwrap_or(0.0));
|
||||
manager.channel.lock().unwrap().time_shift = delta;
|
||||
date.clone_from(¤t_date);
|
||||
handles::update_stat(conn, config.general.channel_id, current_date, delta).await?;
|
||||
|
||||
data_map.insert("operation".to_string(), json!("move_to_last"));
|
||||
data_map.insert("shifted_seconds".to_string(), json!(delta));
|
||||
data_map.insert("media".to_string(), get_media_map(media));
|
||||
|
||||
return Ok(data_map);
|
||||
}
|
||||
}
|
||||
|
||||
@ -213,30 +212,29 @@ pub async fn control_state(
|
||||
if let Err(e) = proc.wait() {
|
||||
error!("Decoder {e:?}")
|
||||
};
|
||||
|
||||
info!("Move to next clip");
|
||||
|
||||
let mut data_map = Map::new();
|
||||
let mut media = current_list[index].clone();
|
||||
|
||||
if let Err(e) = media.add_probe(false) {
|
||||
error!("{e:?}");
|
||||
};
|
||||
|
||||
let (delta, _) = get_delta(&config, &media.begin.unwrap_or(0.0));
|
||||
manager.channel.lock().unwrap().time_shift = delta;
|
||||
date.clone_from(¤t_date);
|
||||
handles::update_stat(conn, config.general.channel_id, current_date, delta)
|
||||
.await?;
|
||||
|
||||
data_map.insert("operation".to_string(), json!("move_to_next"));
|
||||
data_map.insert("shifted_seconds".to_string(), json!(delta));
|
||||
data_map.insert("media".to_string(), get_media_map(media));
|
||||
|
||||
return Ok(data_map);
|
||||
} else {
|
||||
return Err(ServiceError::InternalServerError);
|
||||
}
|
||||
|
||||
return Err(ServiceError::InternalServerError);
|
||||
info!("Move to next clip");
|
||||
|
||||
let mut data_map = Map::new();
|
||||
let mut media = current_list[index].clone();
|
||||
|
||||
if let Err(e) = media.add_probe(false) {
|
||||
error!("{e:?}");
|
||||
};
|
||||
|
||||
let (delta, _) = get_delta(&config, &media.begin.unwrap_or(0.0));
|
||||
manager.channel.lock().unwrap().time_shift = delta;
|
||||
date.clone_from(¤t_date);
|
||||
handles::update_stat(conn, config.general.channel_id, current_date, delta).await?;
|
||||
|
||||
data_map.insert("operation".to_string(), json!("move_to_next"));
|
||||
data_map.insert("shifted_seconds".to_string(), json!(delta));
|
||||
data_map.insert("media".to_string(), get_media_map(media));
|
||||
|
||||
return Ok(data_map);
|
||||
}
|
||||
}
|
||||
|
||||
@ -249,21 +247,21 @@ pub async fn control_state(
|
||||
if let Err(e) = proc.wait() {
|
||||
error!("Decoder {e:?}")
|
||||
};
|
||||
|
||||
info!("Reset playout to original state");
|
||||
let mut data_map = Map::new();
|
||||
manager.channel.lock().unwrap().time_shift = 0.0;
|
||||
date.clone_from(¤t_date);
|
||||
manager.list_init.store(true, Ordering::SeqCst);
|
||||
|
||||
handles::update_stat(conn, config.general.channel_id, current_date, 0.0).await?;
|
||||
|
||||
data_map.insert("operation".to_string(), json!("reset_playout_state"));
|
||||
|
||||
return Ok(data_map);
|
||||
} else {
|
||||
return Err(ServiceError::InternalServerError);
|
||||
}
|
||||
|
||||
return Err(ServiceError::InternalServerError);
|
||||
info!("Reset playout to original state");
|
||||
let mut data_map = Map::new();
|
||||
manager.channel.lock().unwrap().time_shift = 0.0;
|
||||
date.clone_from(¤t_date);
|
||||
manager.list_init.store(true, Ordering::SeqCst);
|
||||
|
||||
handles::update_stat(conn, config.general.channel_id, current_date, 0.0).await?;
|
||||
|
||||
data_map.insert("operation".to_string(), json!("reset_playout_state"));
|
||||
|
||||
return Ok(data_map);
|
||||
}
|
||||
|
||||
"stop_all" => {
|
||||
|
@ -402,7 +402,7 @@ pub async fn upload(
|
||||
let filepath = if abs_path {
|
||||
path.to_path_buf()
|
||||
} else {
|
||||
valid_path(&config, &path.to_string_lossy())
|
||||
valid_path(config, &path.to_string_lossy())
|
||||
.await?
|
||||
.join(filename)
|
||||
};
|
||||
|
@ -16,11 +16,14 @@ use rand::{seq::SliceRandom, thread_rng, Rng};
|
||||
use simplelog::*;
|
||||
use walkdir::WalkDir;
|
||||
|
||||
use crate::player::utils::{
|
||||
folder::{fill_filler_list, FolderSource},
|
||||
gen_dummy, get_date_range, include_file_extension,
|
||||
json_serializer::JsonPlaylist,
|
||||
sum_durations, Media,
|
||||
use crate::player::{
|
||||
controller::ChannelManager,
|
||||
utils::{
|
||||
folder::{fill_filler_list, FolderSource},
|
||||
gen_dummy, get_date_range, include_file_extension,
|
||||
json_serializer::JsonPlaylist,
|
||||
sum_durations, Media,
|
||||
},
|
||||
};
|
||||
use crate::utils::{
|
||||
config::{PlayoutConfig, Template},
|
||||
@ -131,7 +134,7 @@ pub fn filler_list(config: &PlayoutConfig, total_length: f64) -> Vec<Media> {
|
||||
|
||||
pub fn generate_from_template(
|
||||
config: &PlayoutConfig,
|
||||
player_control: &PlayerControl,
|
||||
manager: &ChannelManager,
|
||||
template: Template,
|
||||
) -> FolderSource {
|
||||
let mut media_list = vec![];
|
||||
@ -192,14 +195,14 @@ pub fn generate_from_template(
|
||||
index += 1;
|
||||
}
|
||||
|
||||
FolderSource::from_list(config, None, player_control, media_list)
|
||||
FolderSource::from_list(manager, media_list)
|
||||
}
|
||||
|
||||
/// Generate playlists
|
||||
pub fn generate_playlist(
|
||||
config: &PlayoutConfig,
|
||||
channel_name: Option<String>,
|
||||
) -> Result<Vec<JsonPlaylist>, Error> {
|
||||
pub fn playlist_generator(manager: &ChannelManager) -> Result<Vec<JsonPlaylist>, Error> {
|
||||
let config = manager.config.lock().unwrap();
|
||||
let channel_name = manager.channel.lock().unwrap().name.clone();
|
||||
|
||||
let total_length = match config.playlist.length_sec {
|
||||
Some(length) => length,
|
||||
None => {
|
||||
@ -210,17 +213,11 @@ pub fn generate_playlist(
|
||||
}
|
||||
}
|
||||
};
|
||||
let player_control = PlayerControl::new();
|
||||
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,
|
||||
None => "Channel 1".to_string(),
|
||||
};
|
||||
|
||||
if !playlist_root.is_dir() {
|
||||
error!(
|
||||
"Playlist folder <b><magenta>{:?}</></b> not exists!",
|
||||
@ -242,12 +239,12 @@ pub fn generate_playlist(
|
||||
let folder_iter = if let Some(template) = &config.general.template {
|
||||
from_template = true;
|
||||
|
||||
generate_from_template(config, &player_control, template.clone())
|
||||
generate_from_template(&config, manager, template.clone())
|
||||
} else {
|
||||
FolderSource::new(config, None, &player_control)
|
||||
FolderSource::new(&config, manager.clone())
|
||||
};
|
||||
|
||||
let list_length = player_control.current_list.lock().unwrap().len();
|
||||
let list_length = manager.current_list.lock().unwrap().len();
|
||||
|
||||
for date in date_range {
|
||||
let d: Vec<&str> = date.split('-').collect();
|
||||
@ -275,7 +272,7 @@ pub fn generate_playlist(
|
||||
);
|
||||
|
||||
let mut playlist = JsonPlaylist {
|
||||
channel: channel.clone(),
|
||||
channel: channel_name.clone(),
|
||||
date,
|
||||
path: None,
|
||||
start_sec: None,
|
||||
@ -285,7 +282,7 @@ pub fn generate_playlist(
|
||||
};
|
||||
|
||||
if from_template {
|
||||
let media_list = player_control.current_list.lock().unwrap();
|
||||
let media_list = manager.current_list.lock().unwrap();
|
||||
playlist.program = media_list.to_vec();
|
||||
} else {
|
||||
for item in folder_iter.clone() {
|
||||
@ -306,7 +303,7 @@ pub fn generate_playlist(
|
||||
|
||||
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);
|
||||
let mut fillers = filler_list(&config, time_left);
|
||||
|
||||
playlist.program.append(&mut fillers);
|
||||
}
|
||||
|
@ -1,5 +1,5 @@
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
collections::{hash_map, HashMap},
|
||||
env,
|
||||
io::{self, ErrorKind, Write},
|
||||
path::PathBuf,
|
||||
@ -73,7 +73,7 @@ impl MultiFileLogger {
|
||||
|
||||
fn get_writer(&self, channel: i32) -> io::Result<Arc<Mutex<FileLogWriter>>> {
|
||||
let mut writers = self.writers.lock().unwrap();
|
||||
if !writers.contains_key(&channel) {
|
||||
if let hash_map::Entry::Vacant(e) = writers.entry(channel) {
|
||||
let writer = FileLogWriter::builder(
|
||||
FileSpec::default()
|
||||
.suppress_timestamp()
|
||||
@ -93,8 +93,9 @@ impl MultiFileLogger {
|
||||
)
|
||||
.try_build()
|
||||
.map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))?;
|
||||
writers.insert(channel, Arc::new(Mutex::new(writer)));
|
||||
e.insert(Arc::new(Mutex::new(writer)));
|
||||
}
|
||||
|
||||
Ok(writers.get(&channel).unwrap().clone())
|
||||
}
|
||||
}
|
||||
@ -326,6 +327,7 @@ pub fn mail_queue(mail_queues: Arc<Mutex<Vec<Arc<Mutex<MailQueue>>>>>) {
|
||||
|
||||
loop {
|
||||
interval.tick().await;
|
||||
let mut tasks = vec![];
|
||||
|
||||
// Reset the counter after one day
|
||||
if counter >= 86400 {
|
||||
@ -334,34 +336,40 @@ pub fn mail_queue(mail_queues: Arc<Mutex<Vec<Arc<Mutex<MailQueue>>>>>) {
|
||||
counter += sec;
|
||||
}
|
||||
|
||||
let mut queues = match mail_queues.lock() {
|
||||
Ok(l) => l,
|
||||
Err(e) => {
|
||||
error!("Failed to lock mail_queues {e}");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
// Process mail queues and send emails
|
||||
for queue in queues.iter_mut() {
|
||||
let interval = round_to_nearest_ten(counter);
|
||||
let mut q_lock = queue.lock().unwrap_or_else(|poisoned| {
|
||||
error!("Queue mutex was poisoned");
|
||||
|
||||
poisoned.into_inner()
|
||||
});
|
||||
|
||||
let expire = round_to_nearest_ten(q_lock.config.interval);
|
||||
|
||||
if interval % expire == 0 && !q_lock.is_empty() {
|
||||
if q_lock.config.recipient.contains('@') {
|
||||
if let Err(e) = send_mail(&q_lock.config, q_lock.text()).await {
|
||||
error!(target: "{file}", channel = q_lock.id; "Failed to send mail: {e}");
|
||||
}
|
||||
{
|
||||
let mut queues = match mail_queues.lock() {
|
||||
Ok(l) => l,
|
||||
Err(e) => {
|
||||
error!("Failed to lock mail_queues {e}");
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
// Clear the messages after sending the email
|
||||
q_lock.clear();
|
||||
// Process mail queues and send emails
|
||||
for queue in queues.iter_mut() {
|
||||
let interval = round_to_nearest_ten(counter);
|
||||
let mut q_lock = queue.lock().unwrap_or_else(|poisoned| {
|
||||
error!("Queue mutex was poisoned");
|
||||
|
||||
poisoned.into_inner()
|
||||
});
|
||||
|
||||
let expire = round_to_nearest_ten(q_lock.config.interval);
|
||||
|
||||
if interval % expire == 0 && !q_lock.is_empty() {
|
||||
if q_lock.config.recipient.contains('@') {
|
||||
tasks.push((q_lock.config.clone(), q_lock.text().clone(), q_lock.id));
|
||||
}
|
||||
|
||||
// Clear the messages after sending the email
|
||||
q_lock.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (config, text, id) in tasks {
|
||||
if let Err(e) = send_mail(&config, text).await {
|
||||
error!(target: "{file}", channel = id; "Failed to send mail: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -48,7 +48,8 @@ use crate::utils::{errors::ServiceError, logging::log_file_path};
|
||||
|
||||
#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize, Deserialize)]
|
||||
pub enum Role {
|
||||
Admin,
|
||||
GlobalAdmin,
|
||||
ChannelAdmin,
|
||||
User,
|
||||
Guest,
|
||||
}
|
||||
@ -56,7 +57,8 @@ pub enum Role {
|
||||
impl Role {
|
||||
pub fn set_role(role: &str) -> Self {
|
||||
match role {
|
||||
"admin" => Role::Admin,
|
||||
"global_admin" => Role::GlobalAdmin,
|
||||
"channel_admin" => Role::ChannelAdmin,
|
||||
"user" => Role::User,
|
||||
_ => Role::Guest,
|
||||
}
|
||||
@ -68,7 +70,8 @@ impl FromStr for Role {
|
||||
|
||||
fn from_str(input: &str) -> Result<Self, Self::Err> {
|
||||
match input {
|
||||
"admin" => Ok(Self::Admin),
|
||||
"global_admin" => Ok(Self::GlobalAdmin),
|
||||
"channel_admin" => Ok(Self::ChannelAdmin),
|
||||
"user" => Ok(Self::User),
|
||||
_ => Ok(Self::Guest),
|
||||
}
|
||||
@ -78,7 +81,8 @@ impl FromStr for Role {
|
||||
impl fmt::Display for Role {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match *self {
|
||||
Self::Admin => write!(f, "admin"),
|
||||
Self::GlobalAdmin => write!(f, "global_admin"),
|
||||
Self::ChannelAdmin => write!(f, "channel_admin"),
|
||||
Self::User => write!(f, "user"),
|
||||
Self::Guest => write!(f, "guest"),
|
||||
}
|
||||
@ -101,7 +105,8 @@ where
|
||||
impl FromRow<'_, SqliteRow> for Role {
|
||||
fn from_row(row: &SqliteRow) -> sqlx::Result<Self> {
|
||||
match row.get("name") {
|
||||
"admin" => Ok(Self::Admin),
|
||||
"global_admin" => Ok(Self::GlobalAdmin),
|
||||
"channel_admin" => Ok(Self::ChannelAdmin),
|
||||
"user" => Ok(Self::User),
|
||||
_ => Ok(Self::Guest),
|
||||
}
|
||||
@ -419,7 +424,7 @@ pub async fn read_log_file(channel_id: &i32, date: &str) -> Result<String, Servi
|
||||
fs::read_to_string(log_path)?
|
||||
};
|
||||
|
||||
return Ok(file_content);
|
||||
Ok(file_content)
|
||||
}
|
||||
|
||||
/// get human readable file size
|
||||
|
@ -2,10 +2,11 @@ use std::{fs, path::PathBuf};
|
||||
|
||||
use log::*;
|
||||
|
||||
use crate::player::controller::ChannelManager;
|
||||
use crate::player::utils::{json_reader, json_writer, JsonPlaylist};
|
||||
use crate::utils::{
|
||||
config::PlayoutConfig, errors::ServiceError, files::norm_abs_path,
|
||||
generator::generate_playlist as playlist_generator,
|
||||
generator::playlist_generator,
|
||||
};
|
||||
|
||||
pub async fn read_playlist(
|
||||
@ -83,10 +84,9 @@ pub async fn write_playlist(
|
||||
Err(ServiceError::InternalServerError)
|
||||
}
|
||||
|
||||
pub async fn generate_playlist(
|
||||
mut config: PlayoutConfig,
|
||||
channel: String,
|
||||
) -> Result<JsonPlaylist, ServiceError> {
|
||||
pub async fn generate_playlist(manager: ChannelManager) -> Result<JsonPlaylist, ServiceError> {
|
||||
let mut config = manager.config.lock().unwrap();
|
||||
|
||||
if let Some(mut template) = config.general.template.take() {
|
||||
for source in template.sources.iter_mut() {
|
||||
let mut paths = vec![];
|
||||
@ -103,7 +103,9 @@ pub async fn generate_playlist(
|
||||
config.general.template = Some(template);
|
||||
}
|
||||
|
||||
match playlist_generator(&config, Some(channel)) {
|
||||
drop(config);
|
||||
|
||||
match playlist_generator(&manager) {
|
||||
Ok(playlists) => {
|
||||
if !playlists.is_empty() {
|
||||
Ok(playlists[0].clone())
|
||||
|
@ -16,7 +16,7 @@ CREATE TABLE channels (
|
||||
preview_url TEXT NOT NULL,
|
||||
extra_extensions TEXT NOT NULL,
|
||||
active INTEGER NOT NULL DEFAULT 0,
|
||||
current_date TEXT,
|
||||
last_date TEXT,
|
||||
time_shift REAL NOT NULL DEFAULT 0
|
||||
);
|
||||
CREATE TABLE presets (
|
||||
@ -36,7 +36,7 @@ CREATE TABLE presets (
|
||||
FOREIGN KEY (channel_id) REFERENCES channels (id) ON UPDATE CASCADE ON DELETE CASCADE,
|
||||
UNIQUE(name)
|
||||
);
|
||||
CREATE TABLE users (
|
||||
CREATE TABLE user (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
mail TEXT NOT NULL,
|
||||
username TEXT NOT NULL,
|
||||
@ -105,7 +105,7 @@ CREATE TABLE configurations (
|
||||
extensions TEXT NOT NULL DEFAULT "mp4;mkv;webm",
|
||||
shuffle INTEGER NOT NULL DEFAULT 1,
|
||||
text_help TEXT NOT NULL DEFAULT "Overlay text in combination with libzmq for remote text manipulation. On windows fontfile path need to be like this 'C\\:/WINDOWS/fonts/DejaVuSans.ttf'.\n'text_from_filename' activate the extraction from text of a filename. With 'style' you can define the drawtext parameters like position, color, etc. Post Text over API will override this. With 'regex' you can format file names, to get a title from it.",
|
||||
add_textINTEGER NOT NULL DEFAULT 1,
|
||||
add_text INTEGER NOT NULL DEFAULT 1,
|
||||
text_from_filename INTEGER NOT NULL DEFAULT 0,
|
||||
fontfile TEXT NOT NULL DEFAULT "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
|
||||
style TEXT NOT NULL DEFAULT "x=(w-tw)/2:y=(h-line_h)*0.9:fontsize=24:fontcolor=#ffffff:box=1:boxcolor=#000000:boxborderw=4",
|
||||
|
Loading…
Reference in New Issue
Block a user