Merge pull request #159 from jb-alvarado/master

initial multi channel support
This commit is contained in:
jb-alvarado 2022-07-19 12:54:51 +02:00 committed by GitHub
commit efdbf28874
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 328 additions and 147 deletions

63
Cargo.lock generated
View File

@ -626,7 +626,7 @@ dependencies = [
[[package]]
name = "chrono"
version = "0.4.20-beta.1"
source = "git+https://github.com/chronotope/chrono.git#051e1170c41477ce162301c8711110a4577c1a23"
source = "git+https://github.com/chronotope/chrono.git#187819ff43e0e4da351b3ea4ac2d3076e06e8251"
dependencies = [
"num-integer",
"num-traits",
@ -636,9 +636,9 @@ dependencies = [
[[package]]
name = "clap"
version = "3.2.8"
version = "3.2.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "190814073e85d238f31ff738fcb0bf6910cedeb73376c87cd69291028966fd83"
checksum = "ab8b79fe3946ceb4a0b1c080b4018992b8d27e9ff363644c1c9b6387c854614d"
dependencies = [
"atty",
"bitflags",
@ -675,9 +675,9 @@ dependencies = [
[[package]]
name = "concurrent-queue"
version = "1.2.2"
version = "1.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "30ed07550be01594c6026cff2a1d7fe9c8f683caa798e12b68694ac9e88286a3"
checksum = "83827793632c72fa4f73c2edb31e7a997527dd8ffe7077344621fc62c5478157"
dependencies = [
"cache-padded",
]
@ -880,9 +880,9 @@ checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7"
[[package]]
name = "crypto-common"
version = "0.1.5"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2ccfd8c0ee4cce11e45b3fd6f9d5e69e0cc62912aa6a0cb1bf4617b0eba5a12f"
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
dependencies = [
"generic-array",
"typenum",
@ -1009,7 +1009,7 @@ dependencies = [
[[package]]
name = "ffplayout"
version = "0.10.5"
version = "0.11.0"
dependencies = [
"clap",
"crossbeam-channel 0.5.5",
@ -1027,7 +1027,7 @@ dependencies = [
[[package]]
name = "ffplayout-api"
version = "0.4.2"
version = "0.5.0"
dependencies = [
"actix-multipart",
"actix-web",
@ -1056,7 +1056,7 @@ dependencies = [
[[package]]
name = "ffplayout-lib"
version = "0.10.4"
version = "0.10.5"
dependencies = [
"chrono 0.4.20-beta.1",
"crossbeam-channel 0.5.5",
@ -1135,7 +1135,7 @@ dependencies = [
"futures-core",
"futures-sink",
"pin-project",
"spin 0.9.3",
"spin 0.9.4",
]
[[package]]
@ -1406,9 +1406,9 @@ dependencies = [
[[package]]
name = "hashbrown"
version = "0.12.2"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "607c8a29735385251a339424dd462993c0fed8fa09d378f259377df08c126022"
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
[[package]]
name = "hashlink"
@ -1543,7 +1543,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e"
dependencies = [
"autocfg",
"hashbrown 0.12.2",
"hashbrown 0.12.3",
]
[[package]]
@ -2084,9 +2084,9 @@ dependencies = [
[[package]]
name = "os_str_bytes"
version = "6.1.0"
version = "6.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21326818e99cfe6ce1e524c2a805c189a99b5ae555a35d19f9a284b427d86afa"
checksum = "648001efe5d5c0102d8cea768e348da85d90af8ba91f0bea908f157951493cd4"
[[package]]
name = "paris"
@ -2167,9 +2167,9 @@ checksum = "0c520e05135d6e763148b6426a837e239041653ba7becd2e538c076c738025fc"
[[package]]
name = "pem"
version = "1.0.2"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e9a3b09a20e374558580a4914d3b7d89bd61b954a5a5e1dcbea98753addb1947"
checksum = "03c64931a1a212348ec4f3b4362585eca7159d0d09cbdf4a7f74f02173596fd4"
dependencies = [
"base64",
]
@ -2548,18 +2548,18 @@ checksum = "a2333e6df6d6598f2b1974829f853c2b4c5f4a6e503c10af918081aa6f8564e1"
[[package]]
name = "serde"
version = "1.0.138"
version = "1.0.139"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1578c6245786b9d168c5447eeacfb96856573ca56c9d68fdcf394be134882a47"
checksum = "0171ebb889e45aa68b44aee0859b3eede84c6f5f5c228e6f140c0b2a0a46cad6"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.138"
version = "1.0.139"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "023e9b1467aef8a10fb88f25611870ada9800ef7e22afce356bb0d2387b6f27c"
checksum = "dc1d3230c1de7932af58ad8ffbe1d784bd55efd5a9d84ac24f69c72d83543dfb"
dependencies = [
"proc-macro2",
"quote",
@ -2591,9 +2591,9 @@ dependencies = [
[[package]]
name = "serde_yaml"
version = "0.8.25"
version = "0.8.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ec0091e1f5aa338283ce049bd9dfefd55e1f168ac233e85c1ffe0038fb48cbe"
checksum = "578a7433b776b56a35785ed5ce9a7e777ac0598aac5a6dd1b4b18a307c7fc71b"
dependencies = [
"indexmap",
"ryu",
@ -2692,9 +2692,9 @@ checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d"
[[package]]
name = "spin"
version = "0.9.3"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c530c2b0d0bf8b69304b39fe2001993e267461948b890cd037d8ad4293fa1a0d"
checksum = "7f6002a767bff9e83f8eeecf883ecb8011875a21ae8da43bffb817a57e78cc09"
dependencies = [
"lock_api",
]
@ -2932,10 +2932,11 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
[[package]]
name = "tokio"
version = "1.19.2"
version = "1.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c51a52ed6686dd62c320f9b89299e9dfb46f730c7a48e635c19f21d116cb1439"
checksum = "57aec3cfa4c296db7255446efb4928a6be304b431a806216105542a67b6ca82e"
dependencies = [
"autocfg",
"bytes",
"libc",
"memchr",
@ -3070,9 +3071,9 @@ checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992"
[[package]]
name = "unicode-ident"
version = "1.0.1"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5bd2fe26506023ed7b5e1e315add59d6f584c621d037f9368fea9cfb988f368c"
checksum = "15c61ba63f9235225a22310255a29b806b907c9b8c964bcbd0a2c70f3f2deea7"
[[package]]
name = "unicode-normalization"
@ -3391,7 +3392,7 @@ dependencies = [
[[package]]
name = "zeromq"
version = "0.3.3"
source = "git+https://github.com/zeromq/zmq.rs.git#9e0eb7c16950146d285d952939ea8d5a5fc812c9"
source = "git+https://github.com/zeromq/zmq.rs.git#c7cbd1c0d589c9d671626c7fa5ba26843c0ab219"
dependencies = [
"async-std",
"async-trait",

View File

@ -1,3 +1,5 @@
# give user www-data permission to control the ffplayout systemd service
# give user ffpu permission to control the ffplayout systemd service
www-data ALL = NOPASSWD: /bin/systemctl start ffplayout.service, /bin/systemctl stop ffplayout.service, /bin/systemctl reload ffplayout.service, /bin/systemctl restart ffplayout.service, /bin/systemctl status ffplayout.service, /bin/systemctl is-active ffplayout.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@*

View File

@ -6,8 +6,8 @@ After=network.target remote-fs.target
ExecStart=/usr/bin/ffpapi -l 127.0.0.1:8000
Restart=always
RestartSec=1
User=www-data
Group=www-data
User=ffpu
Group=ffpu
[Install]
WantedBy=multi-user.target

View File

@ -7,8 +7,8 @@ ExecStart=/usr/bin/ffplayout
Restart=always
RestartSec=1
KillMode=mixed
User=www-data
Group=www-data
User=ffpu
Group=ffpu
[Install]
WantedBy=multi-user.target

View File

@ -85,7 +85,7 @@ 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: /playlists
path: /var/lib/ffplayout/playlists
day_start: "5:59:25"
length: "24:00:00"
infinit: false
@ -94,8 +94,8 @@ storage:
help_text: Play ordered or randomly files from path. 'filler_clip' is for fill
the end to reach 24 hours, it will loop when is necessary. 'extensions' search
only files with this extension. Set 'shuffle' to 'True' to pick files randomly.
path: "/mediaStorage"
filler_clip: "/mediaStorage/filler/filler.mp4"
path: "/var/lib/ffplayout/tv-media"
filler_clip: "/var/lib/ffplayout/tv-media/filler/filler.mp4"
extensions:
- "mp4"
- "mkv"

14
assets/ffplayout@.service Normal file
View File

@ -0,0 +1,14 @@
[Unit]
Description=Rust and ffmpeg based multi channel playout solution
After=network.target remote-fs.target
[Service]
ExecStart=/usr/bin/ffplayout %I
Restart=always
RestartSec=1
KillMode=mixed
User=ffpu
Group=ffpu
[Install]
WantedBy=multi-user.target

28
debian/postinst vendored
View File

@ -1,27 +1,25 @@
#DEBHELPER#
sysUser="ffpu"
if ! id $sysUser &>/dev/null; then
adduser --system $sysUser
fi
if [ ! -d "/usr/share/ffplayout/db" ]; then
mkdir "/usr/share/ffplayout/db"
chmod 777 "/usr/share/ffplayout/db"
mkdir "/var/lib/ffplayout/playlists"
mkdir "/var/lib/ffplayout/tv-media"
/usr/bin/ffpapi -i
if id "www-data" &>/dev/null; then
chown www-data. "/usr/share/ffplayout/db/ffplayout.db"
else
sed -i "s|www-data|root|g" /lib/systemd/system/ffpapi.service
sed -i "s|www-data|root|g" /lib/systemd/system/ffplayout.service
rm -f /etc/sudoers.d/11-ffplayout
systemctl daemon-reload
fi
chown -R ${sysUser}. "/usr/share/ffplayout"
chown -R ${sysUser}. "/var/lib/ffplayout"
chown -R ${sysUser}. "/etc/ffplayout"
fi
if [ ! -d "/var/log/ffplayout" ]; then
mkdir /var/log/ffplayout
mkdir "/var/log/ffplayout"
if id "www-data" &>/dev/null; then
chown www-data. /var/log/ffplayout
chown -R www-data. /etc/ffplayout
fi
chown ${sysUser}. "/var/log/ffplayout"
fi

View File

@ -55,10 +55,10 @@ curl -X POST 'http://localhost:8000/api/user/' -H 'Content-Type: application/jso
#### ffpapi Settings
**Get Settings**
**Get Settings from Channel**
```BASH
curl -X GET http://127.0.0.1:8000/api/settings/1 -H "Authorization: Bearer <TOKEN>"
curl -X GET http://127.0.0.1:8000/api/channel/1 -H "Authorization: Bearer <TOKEN>"
```
**Response:**
@ -66,7 +66,7 @@ curl -X GET http://127.0.0.1:8000/api/settings/1 -H "Authorization: Bearer <TOKE
```JSON
{
"id": 1,
"channel_name": "Channel 1",
"name": "Channel 1",
"preview_url": "http://localhost/live/preview.m3u8",
"config_path": "/etc/ffplayout/ffplayout.yml",
"extra_extensions": "jpg,jpeg,png",
@ -75,22 +75,37 @@ curl -X GET http://127.0.0.1:8000/api/settings/1 -H "Authorization: Bearer <TOKE
}
```
**Get all Settings**
**Get settings from all Channels**
```BASH
curl -X GET http://127.0.0.1:8000/api/settings -H "Authorization: Bearer <TOKEN>"
curl -X GET http://127.0.0.1:8000/api/channels -H "Authorization: Bearer <TOKEN>"
```
**Update Settings**
**Update Channel**
```BASH
curl -X PATCH http://127.0.0.1:8000/api/settings/1 -H "Content-Type: application/json" \
-d '{ "id": 1, "channel_name": "Channel 1", "preview_url": "http://localhost/live/stream.m3u8", \
"config_path": "/etc/ffplayout/ffplayout.yml", "extra_extensions": "jpg,jpeg,png",
"role_id": 1, "channel_id": 1 }' \
curl -X PATCH http://127.0.0.1:8000/api/channel/1 -H "Content-Type: application/json" \
-d '{ "id": 1, "name": "Channel 1", "preview_url": "http://localhost/live/stream.m3u8", \
"config_path": "/etc/ffplayout/ffplayout.yml", "extra_extensions": "jpg,jpeg,png", "timezone": "Europe/Berlin"}' \
-H "Authorization: Bearer <TOKEN>"
```
**Create new Channel**
```BASH
curl -X POST http://127.0.0.1:8000/api/channel/ -H "Content-Type: application/json" \
-d '{ "name": "Channel 2", "preview_url": "http://localhost/live/channel2.m3u8", \
"config_path": "/etc/ffplayout/channel2.yml", "extra_extensions": "jpg,jpeg,png",
"timezone": "Europe/Berlin", "service": "ffplayout@channel2.service" }' \
-H "Authorization: Bearer <TOKEN>"
```
**Delete Channel**
```BASH
curl -X DELETE http://127.0.0.1:8000/api/channel/2 -H "Authorization: Bearer <TOKEN>"
```
#### ffplayout Config
**Get Config**

View File

@ -4,7 +4,7 @@ description = "Rest API for ffplayout"
license = "GPL-3.0"
authors = ["Jonathan Baecker jonbae77@gmail.com"]
readme = "README.md"
version = "0.4.2"
version = "0.5.0"
edition = "2021"
[dependencies]

View File

@ -15,11 +15,11 @@ use utils::{
auth, db_path, init_config,
models::LoginUser,
routes::{
add_dir, add_preset, add_user, control_playout, del_playlist, delete_preset, file_browser,
gen_playlist, get_all_settings, get_log, get_playlist, get_playout_config, get_presets,
get_settings, get_user, login, media_current, media_last, media_next, move_rename,
patch_settings, process_control, remove, save_file, save_playlist, send_text_message,
update_playout_config, update_preset, update_user,
add_channel, add_dir, add_preset, add_user, control_playout, del_playlist, delete_preset,
file_browser, gen_playlist, get_all_channels, get_channel, get_log, get_playlist,
get_playout_config, get_presets, get_user, login, media_current, media_last, media_next,
move_rename, patch_channel, process_control, remove, remove_channel, save_file,
save_playlist, send_text_message, update_playout_config, update_preset, update_user,
},
run_args, Role,
};
@ -84,9 +84,11 @@ async fn main() -> std::io::Result<()> {
.service(get_presets)
.service(update_preset)
.service(delete_preset)
.service(get_settings)
.service(get_all_settings)
.service(patch_settings)
.service(get_channel)
.service(get_all_channels)
.service(patch_channel)
.service(add_channel)
.service(remove_channel)
.service(update_user)
.service(send_text_message)
.service(control_playout)

View File

@ -0,0 +1,44 @@
use std::fs;
use simplelog::*;
use crate::utils::{
control::control_service,
errors::ServiceError,
handles::{db_add_channel, db_delete_channel, db_get_channel},
models::Channel,
};
pub async fn create_channel(target_channel: Channel) -> Result<Channel, ServiceError> {
if !target_channel.service.starts_with("ffplayout@") {
return Err(ServiceError::BadRequest("Bad service name!".to_string()));
}
if !target_channel.config_path.starts_with("/etc/ffplayout") {
return Err(ServiceError::BadRequest("Bad config path!".to_string()));
}
fs::copy(
"/usr/share/ffplayout/ffplayout.yml.orig",
&target_channel.config_path,
)?;
let new_channel = db_add_channel(target_channel).await?;
control_service(new_channel.id, "enable").await?;
Ok(new_channel)
}
pub async fn delete_channel(id: i64) -> Result<(), ServiceError> {
let channel = db_get_channel(&id).await?;
control_service(channel.id, "stop").await?;
control_service(channel.id, "disable").await?;
if let Err(e) = fs::remove_file(channel.config_path) {
error!("{e}");
};
db_delete_channel(&id).await?;
Ok(())
}

View File

@ -7,7 +7,7 @@ use reqwest::{
use serde::{Deserialize, Serialize};
use simplelog::*;
use crate::utils::{errors::ServiceError, handles::db_get_settings, playout_config};
use crate::utils::{errors::ServiceError, handles::db_get_channel, playout_config};
use ffplayout_lib::vec_strings;
#[derive(Debug, Deserialize, Serialize, Clone)]
@ -57,14 +57,32 @@ struct SystemD {
impl SystemD {
async fn new(id: i64) -> Result<Self, ServiceError> {
let settings = db_get_settings(&id).await?;
let channel = db_get_channel(&id).await?;
Ok(Self {
service: settings.service,
cmd: vec_strings!["systemctl"],
service: channel.service,
cmd: vec_strings!["/usr/bin/systemctl"],
})
}
fn enable(mut self) -> Result<String, ServiceError> {
self.cmd
.append(&mut vec!["enable".to_string(), self.service]);
Command::new("sudo").args(self.cmd).spawn()?;
Ok("Success".to_string())
}
fn disable(mut self) -> Result<String, ServiceError> {
self.cmd
.append(&mut vec!["disable".to_string(), self.service]);
Command::new("sudo").args(self.cmd).spawn()?;
Ok("Success".to_string())
}
fn start(mut self) -> Result<String, ServiceError> {
self.cmd
.append(&mut vec!["start".to_string(), self.service]);
@ -167,6 +185,8 @@ pub async fn control_service(id: i64, command: &str) -> Result<String, ServiceEr
let system_d = SystemD::new(id).await?;
match command {
"enable" => system_d.enable(),
"disable" => system_d.disable(),
"start" => system_d.start(),
"stop" => system_d.stop(),
"restart" => system_d.restart(),

View File

@ -9,7 +9,7 @@ use sqlx::{migrate::MigrateDatabase, sqlite::SqliteQueryResult, Pool, Sqlite, Sq
use crate::utils::{
db_path,
models::{Settings, TextPreset, User},
models::{Channel, TextPreset, User},
GlobalSettings,
};
@ -33,16 +33,16 @@ async fn create_schema() -> Result<SqliteQueryResult, sqlx::Error> {
name TEXT NOT NULL,
UNIQUE(name)
);
CREATE TABLE IF NOT EXISTS settings
CREATE TABLE IF NOT EXISTS channels
(
id INTEGER PRIMARY KEY AUTOINCREMENT,
channel_name TEXT NOT NULL,
name TEXT NOT NULL,
preview_url TEXT NOT NULL,
config_path TEXT NOT NULL,
extra_extensions TEXT NOT NULL,
timezone TEXT NOT NULL,
service TEXT NOT NULL,
UNIQUE(channel_name)
UNIQUE(name, service)
);
CREATE TABLE IF NOT EXISTS presets
(
@ -59,7 +59,7 @@ async fn create_schema() -> Result<SqliteQueryResult, sqlx::Error> {
boxborderw TEXT NOT NULL,
alpha TEXT NOT NULL,
channel_id INTEGER NOT NULL DEFAULT 1,
FOREIGN KEY (channel_id) REFERENCES settings (id) ON UPDATE SET NULL ON DELETE SET NULL,
FOREIGN KEY (channel_id) REFERENCES channels (id) ON UPDATE SET NULL ON DELETE SET NULL,
UNIQUE(name)
);
CREATE TABLE IF NOT EXISTS user
@ -72,7 +72,7 @@ async fn create_schema() -> Result<SqliteQueryResult, sqlx::Error> {
role_id INTEGER NOT NULL DEFAULT 2,
channel_id INTEGER NOT NULL DEFAULT 1,
FOREIGN KEY (role_id) REFERENCES roles (id) ON UPDATE SET NULL ON DELETE SET NULL,
FOREIGN KEY (channel_id) REFERENCES settings (id) ON UPDATE SET NULL ON DELETE SET NULL,
FOREIGN KEY (channel_id) REFERENCES channels (id) ON UPDATE SET NULL ON DELETE SET NULL,
UNIQUE(mail, username)
);";
let result = sqlx::query(query).execute(&conn).await;
@ -111,7 +111,7 @@ pub async fn db_init(domain: Option<String>) -> Result<&'static str, Box<dyn std
SELECT RAISE(FAIL, 'Database is already initialized!');
END;
INSERT INTO global(secret) VALUES($1);
INSERT INTO settings(channel_name, preview_url, config_path, extra_extensions, timezone, service)
INSERT INTO channels(name, preview_url, config_path, extra_extensions, timezone, service)
VALUES('Channel 1', $2, '/etc/ffplayout/ffplayout.yml', 'jpg,jpeg,png', 'UTC', 'ffplayout.service');
INSERT INTO roles(name) VALUES('admin'), ('user'), ('guest');
INSERT INTO presets(name, text, x, y, fontsize, line_spacing, fontcolor, box, boxcolor, boxborderw, alpha, channel_id)
@ -147,37 +147,38 @@ pub async fn db_global() -> Result<GlobalSettings, sqlx::Error> {
Ok(result)
}
pub async fn db_get_settings(id: &i64) -> Result<Settings, sqlx::Error> {
pub async fn db_get_channel(id: &i64) -> Result<Channel, sqlx::Error> {
let conn = db_connection().await?;
let query = "SELECT * FROM settings WHERE id = $1";
let result: Settings = sqlx::query_as(query).bind(id).fetch_one(&conn).await?;
let query = "SELECT * FROM channels WHERE id = $1";
let result: Channel = sqlx::query_as(query).bind(id).fetch_one(&conn).await?;
conn.close().await;
Ok(result)
}
pub async fn db_get_all_settings() -> Result<Vec<Settings>, sqlx::Error> {
pub async fn db_get_all_channels() -> Result<Vec<Channel>, sqlx::Error> {
let conn = db_connection().await?;
let query = "SELECT * FROM settings";
let result: Vec<Settings> = sqlx::query_as(query).fetch_all(&conn).await?;
let query = "SELECT * FROM channels";
let result: Vec<Channel> = sqlx::query_as(query).fetch_all(&conn).await?;
conn.close().await;
Ok(result)
}
pub async fn db_update_settings(
pub async fn db_update_channel(
id: i64,
settings: Settings,
channel: Channel,
) -> Result<SqliteQueryResult, sqlx::Error> {
let conn = db_connection().await?;
let query = "UPDATE settings SET channel_name = $2, preview_url = $3, config_path = $4, extra_extensions = $5 WHERE id = $1";
let query = "UPDATE channels SET name = $2, preview_url = $3, config_path = $4, extra_extensions = $5, timezone = $6 WHERE id = $1";
let result: SqliteQueryResult = sqlx::query(query)
.bind(id)
.bind(settings.channel_name.clone())
.bind(settings.preview_url.clone())
.bind(settings.config_path.clone())
.bind(settings.extra_extensions.clone())
.bind(channel.name)
.bind(channel.preview_url)
.bind(channel.config_path)
.bind(channel.extra_extensions)
.bind(channel.timezone)
.execute(&conn)
.await?;
conn.close().await;
@ -185,6 +186,38 @@ pub async fn db_update_settings(
Ok(result)
}
pub async fn db_add_channel(channel: Channel) -> Result<Channel, sqlx::Error> {
let conn = db_connection().await?;
let query = "INSERT INTO channels (name, preview_url, config_path, extra_extensions, timezone, service) VALUES($1, $2, $3, $4, $5, $6)";
let result = sqlx::query(query)
.bind(channel.name)
.bind(channel.preview_url)
.bind(channel.config_path)
.bind(channel.extra_extensions)
.bind(channel.timezone)
.bind(channel.service)
.execute(&conn)
.await?;
let new_channel: Channel = sqlx::query_as("SELECT * FROM channels WHERE id = $1")
.bind(result.last_insert_rowid())
.fetch_one(&conn)
.await?;
conn.close().await;
Ok(new_channel)
}
pub async fn db_delete_channel(id: &i64) -> Result<SqliteQueryResult, sqlx::Error> {
let conn = db_connection().await?;
let query = "DELETE FROM channels WHERE id = $1";
let result: SqliteQueryResult = sqlx::query(query).bind(id).execute(&conn).await?;
conn.close().await;
Ok(result)
}
pub async fn db_role(id: &i64) -> Result<String, sqlx::Error> {
let conn = db_connection().await?;
let query = "SELECT name FROM roles WHERE id = $1";

View File

@ -12,6 +12,7 @@ use simplelog::*;
pub mod args_parse;
pub mod auth;
pub mod channels;
pub mod control;
pub mod errors;
pub mod files;
@ -23,8 +24,8 @@ pub mod routes;
use crate::utils::{
args_parse::Args,
errors::ServiceError,
handles::{db_add_user, db_get_settings, db_global, db_init},
models::{Settings, User},
handles::{db_add_user, db_get_channel, db_global, db_init},
models::{Channel, User},
};
use ffplayout_lib::utils::PlayoutConfig;
@ -183,10 +184,10 @@ pub fn read_playout_config(path: &str) -> Result<PlayoutConfig, Box<dyn Error>>
Ok(config)
}
pub async fn playout_config(channel_id: &i64) -> Result<(PlayoutConfig, Settings), ServiceError> {
if let Ok(settings) = db_get_settings(channel_id).await {
if let Ok(config) = read_playout_config(&settings.config_path.clone()) {
return Ok((config, settings));
pub async fn playout_config(channel_id: &i64) -> Result<(PlayoutConfig, Channel), ServiceError> {
if let Ok(channel) = db_get_channel(channel_id).await {
if let Ok(config) = read_playout_config(&channel.config_path.clone()) {
return Ok((config, channel));
}
}
@ -196,7 +197,7 @@ pub async fn playout_config(channel_id: &i64) -> Result<(PlayoutConfig, Settings
}
pub async fn read_log_file(channel_id: &i64, date: &str) -> Result<String, ServiceError> {
if let Ok(settings) = db_get_settings(channel_id).await {
if let Ok(channel) = db_get_channel(channel_id).await {
let mut date_str = "".to_string();
if !date.is_empty() {
@ -204,7 +205,7 @@ pub async fn read_log_file(channel_id: &i64, date: &str) -> Result<String, Servi
date_str.push_str(date);
}
if let Ok(config) = read_playout_config(&settings.config_path) {
if let Ok(config) = read_playout_config(&channel.config_path) {
let mut log_path = Path::new(&config.logging.log_path)
.join("ffplayout.log")
.display()

View File

@ -59,16 +59,13 @@ pub struct TextPreset {
}
#[derive(Debug, Deserialize, Serialize, sqlx::FromRow)]
pub struct Settings {
pub struct Channel {
#[serde(skip_deserializing)]
pub id: i64,
pub channel_name: String,
pub name: String,
pub preview_url: String,
pub config_path: String,
pub extra_extensions: String,
pub timezone: String,
#[sqlx(default)]
#[serde(skip_serializing, skip_deserializing)]
pub secret: String,
pub service: String,
}

View File

@ -75,10 +75,10 @@ pub async fn write_playlist(id: i64, json_data: JsonPlaylist) -> Result<String,
}
pub async fn generate_playlist(id: i64, date: String) -> Result<JsonPlaylist, ServiceError> {
let (mut config, settings) = playout_config(&id).await?;
let (mut config, channel) = playout_config(&id).await?;
config.general.generate = Some(vec![date.clone()]);
match playlist_generator(&config, Some(settings.channel_name)) {
match playlist_generator(&config, Some(channel.name)) {
Ok(playlists) => {
if !playlists.is_empty() {
Ok(playlists[0].clone())

View File

@ -22,6 +22,7 @@ use simplelog::*;
use crate::utils::{
auth::{create_jwt, Claims},
channels::{create_channel, delete_channel},
control::{control_service, control_state, media_info, send_message, Process},
errors::ServiceError,
files::{
@ -29,11 +30,11 @@ use crate::utils::{
PathObject,
},
handles::{
db_add_preset, db_add_user, db_delete_preset, db_get_all_settings, db_get_presets,
db_get_settings, db_get_user, db_login, db_role, db_update_preset, db_update_settings,
db_add_preset, db_add_user, db_delete_preset, db_get_all_channels, db_get_channel,
db_get_presets, db_get_user, db_login, db_role, db_update_channel, db_update_preset,
db_update_user,
},
models::{LoginUser, Settings, TextPreset, User},
models::{Channel, LoginUser, TextPreset, User},
playlist::{delete_playlist, generate_playlist, read_playlist, write_playlist},
read_log_file, read_playout_config, Role,
};
@ -219,10 +220,10 @@ async fn add_user(data: web::Json<User>) -> Result<impl Responder, ServiceError>
/// #### ffpapi Settings
///
/// **Get Settings**
/// **Get Settings from Channel**
///
/// ```BASH
/// curl -X GET http://127.0.0.1:8000/api/settings/1 -H "Authorization: Bearer <TOKEN>"
/// curl -X GET http://127.0.0.1:8000/api/channel/1 -H "Authorization: Bearer <TOKEN>"
/// ```
///
/// **Response:**
@ -230,7 +231,7 @@ async fn add_user(data: web::Json<User>) -> Result<impl Responder, ServiceError>
/// ```JSON
/// {
/// "id": 1,
/// "channel_name": "Channel 1",
/// "name": "Channel 1",
/// "preview_url": "http://localhost/live/preview.m3u8",
/// "config_path": "/etc/ffplayout/ffplayout.yml",
/// "extra_extensions": "jpg,jpeg,png",
@ -238,53 +239,85 @@ async fn add_user(data: web::Json<User>) -> Result<impl Responder, ServiceError>
/// "service": "ffplayout.service"
/// }
/// ```
#[get("/settings/{id}")]
#[get("/channel/{id}")]
#[has_any_role("Role::Admin", "Role::User", type = "Role")]
async fn get_settings(id: web::Path<i64>) -> Result<impl Responder, ServiceError> {
if let Ok(settings) = db_get_settings(&id).await {
return Ok(web::Json(settings));
async fn get_channel(id: web::Path<i64>) -> Result<impl Responder, ServiceError> {
if let Ok(channel) = db_get_channel(&id).await {
return Ok(web::Json(channel));
}
Err(ServiceError::InternalServerError)
}
/// **Get all Settings**
/// **Get settings from all Channels**
///
/// ```BASH
/// curl -X GET http://127.0.0.1:8000/api/settings -H "Authorization: Bearer <TOKEN>"
/// curl -X GET http://127.0.0.1:8000/api/channels -H "Authorization: Bearer <TOKEN>"
/// ```
#[get("/settings")]
#[get("/channels")]
#[has_any_role("Role::Admin", type = "Role")]
async fn get_all_settings() -> Result<impl Responder, ServiceError> {
if let Ok(settings) = db_get_all_settings().await {
return Ok(web::Json(settings));
async fn get_all_channels() -> Result<impl Responder, ServiceError> {
if let Ok(channel) = db_get_all_channels().await {
return Ok(web::Json(channel));
}
Err(ServiceError::InternalServerError)
}
/// **Update Settings**
/// **Update Channel**
///
/// ```BASH
/// curl -X PATCH http://127.0.0.1:8000/api/settings/1 -H "Content-Type: application/json" \
/// -d '{ "id": 1, "channel_name": "Channel 1", "preview_url": "http://localhost/live/stream.m3u8", \
/// "config_path": "/etc/ffplayout/ffplayout.yml", "extra_extensions": "jpg,jpeg,png",
/// "role_id": 1, "channel_id": 1 }' \
/// curl -X PATCH http://127.0.0.1:8000/api/channel/1 -H "Content-Type: application/json" \
/// -d '{ "id": 1, "name": "Channel 1", "preview_url": "http://localhost/live/stream.m3u8", \
/// "config_path": "/etc/ffplayout/ffplayout.yml", "extra_extensions": "jpg,jpeg,png", "timezone": "Europe/Berlin"}' \
/// -H "Authorization: Bearer <TOKEN>"
/// ```
#[patch("/settings/{id}")]
#[patch("/channel/{id}")]
#[has_any_role("Role::Admin", type = "Role")]
async fn patch_settings(
async fn patch_channel(
id: web::Path<i64>,
data: web::Json<Settings>,
data: web::Json<Channel>,
) -> Result<impl Responder, ServiceError> {
if db_update_settings(*id, data.into_inner()).await.is_ok() {
if db_update_channel(*id, data.into_inner()).await.is_ok() {
return Ok("Update Success");
};
Err(ServiceError::InternalServerError)
}
/// **Create new Channel**
///
/// ```BASH
/// curl -X POST http://127.0.0.1:8000/api/channel/ -H "Content-Type: application/json" \
/// -d '{ "name": "Channel 2", "preview_url": "http://localhost/live/channel2.m3u8", \
/// "config_path": "/etc/ffplayout/channel2.yml", "extra_extensions": "jpg,jpeg,png",
/// "timezone": "Europe/Berlin", "service": "ffplayout@channel2.service" }' \
/// -H "Authorization: Bearer <TOKEN>"
/// ```
#[post("/channel/")]
#[has_any_role("Role::Admin", type = "Role")]
async fn add_channel(data: web::Json<Channel>) -> Result<impl Responder, ServiceError> {
match create_channel(data.into_inner()).await {
Ok(c) => Ok(web::Json(c)),
Err(e) => Err(e),
}
}
/// **Delete Channel**
///
/// ```BASH
/// curl -X DELETE http://127.0.0.1:8000/api/channel/2 -H "Authorization: Bearer <TOKEN>"
/// ```
#[delete("/channel/{id}")]
#[has_any_role("Role::Admin", type = "Role")]
async fn remove_channel(id: web::Path<i64>) -> Result<impl Responder, ServiceError> {
if delete_channel(*id).await.is_ok() {
return Ok("Delete Channel Success");
}
Err(ServiceError::InternalServerError)
}
/// #### ffplayout Config
///
/// **Get Config**
@ -300,8 +333,8 @@ async fn get_playout_config(
id: web::Path<i64>,
_details: AuthDetails<Role>,
) -> Result<impl Responder, ServiceError> {
if let Ok(settings) = db_get_settings(&id).await {
if let Ok(config) = read_playout_config(&settings.config_path) {
if let Ok(channel) = db_get_channel(&id).await {
if let Ok(config) = read_playout_config(&channel.config_path) {
return Ok(web::Json(config));
}
};
@ -321,11 +354,11 @@ async fn update_playout_config(
id: web::Path<i64>,
data: web::Json<PlayoutConfig>,
) -> Result<impl Responder, ServiceError> {
if let Ok(settings) = db_get_settings(&id).await {
if let Ok(channel) = db_get_channel(&id).await {
if let Ok(f) = std::fs::OpenOptions::new()
.write(true)
.truncate(true)
.open(&settings.config_path)
.open(&channel.config_path)
{
serde_yaml::to_writer(f, &data).unwrap();

View File

@ -4,7 +4,7 @@ description = "24/7 playout based on rust and ffmpeg"
license = "GPL-3.0"
authors = ["Jonathan Baecker jonbae77@gmail.com"]
readme = "README.md"
version = "0.10.5"
version = "0.11.0"
edition = "2021"
[dependencies]
@ -48,9 +48,11 @@ assets = [
"755"
],
["../assets/ffpapi.service", "/lib/systemd/system/", "644"],
["../assets/ffplayout@.service", "/lib/systemd/system/", "644"],
["../assets/11-ffplayout", "/etc/sudoers.d/", "644"],
["../assets/ffplayout.yml", "/etc/ffplayout/", "644"],
["../assets/logo.png", "/usr/share/ffplayout/", "644"],
["../assets/ffplayout.yml", "/usr/share/ffplayout/ffplayout.yml.orig", "644"],
["../README.md", "/usr/share/doc/ffplayout/README", "644"],
]
maintainer-scripts = "../debian/"
@ -66,10 +68,12 @@ assets = [
{ source = "../assets/ffplayout.yml", dest = "/etc/ffplayout/ffplayout.yml", mode = "644", config = true },
{ source = "../assets/ffpapi.service", dest = "/lib/systemd/system/ffpapi.service", mode = "644" },
{ source = "../assets/ffplayout.service", dest = "/lib/systemd/system/ffplayout.service", mode = "644" },
{ source = "../assets/ffplayout@.service", dest = "/lib/systemd/system/ffplayout@.service", mode = "644" },
{ source = "../assets/11-ffplayout", dest = "/etc/sudoers.d/11-ffplayout", mode = "644" },
{ source = "../README.md", dest = "/usr/share/doc/ffplayout/README", mode = "644", doc = true },
{ source = "../LICENSE", dest = "/usr/share/doc/ffplayout/LICENSE", mode = "644" },
{ source = "../assets/logo.png", dest = "/usr/share/ffplayout/logo.png", mode = "644" },
{ source = "../assets/ffplayout.yml", dest = "/usr/share/ffplayout/ffplayout.yml.orig", mode = "644" },
{ source = "../debian/postinst", dest = "/usr/share/ffplayout/postinst", mode = "755" },
]
auto-req = "no"

View File

@ -6,6 +6,9 @@ use clap::Parser;
override_usage = "Run without any command to use config file only, or with commands to override parameters:\n\n ffplayout [OPTIONS]",
long_about = None)]
pub struct Args {
#[clap(index = 1, value_parser)]
pub channel: Option<String>,
#[clap(short, long, help = "File path to ffplayout.yml")]
pub config: Option<String>,
@ -15,7 +18,7 @@ pub struct Args {
#[clap(
short,
long,
help = "Generate playlist for date or date-range, like: 2022-01-01 - 2022-01-10",
help = "Generate playlist for dates, like: 2022-01-01 - 2022-01-10",
name = "YYYY-MM-DD",
multiple_values = true
)]

View File

@ -1,4 +1,4 @@
use std::path::Path;
use std::path::{Path, PathBuf};
pub mod arg_parse;
@ -6,7 +6,20 @@ pub use arg_parse::Args;
use ffplayout_lib::utils::{time_to_sec, PlayoutConfig};
pub fn get_config(args: Args) -> PlayoutConfig {
let mut config = PlayoutConfig::new(args.config);
let cfg_path = match args.channel {
Some(c) => {
let path = PathBuf::from(format!("/etc/ffplayout/{c}.yml"));
if path.is_file() {
Some(path.display().to_string())
} else {
println!("no file");
args.config
}
}
None => args.config,
};
let mut config = PlayoutConfig::new(cfg_path);
if let Some(gen) = args.generate {
config.general.generate = Some(gen);

View File

@ -4,7 +4,7 @@ description = "Library for ffplayout"
license = "GPL-3.0"
authors = ["Jonathan Baecker jonbae77@gmail.com"]
readme = "README.md"
version = "0.10.4"
version = "0.10.5"
edition = "2021"
[dependencies]

View File

@ -188,6 +188,7 @@ pub fn init_logging(
.add_filter_ignore_str("hyper")
.add_filter_ignore_str("sqlx")
.add_filter_ignore_str("reqwest")
.add_filter_ignore_str("rpc")
.set_level_padding(LevelPadding::Left)
.set_time_level(time_level)
.clone();