support multi channel

This commit is contained in:
jb-alvarado 2022-07-18 22:55:18 +02:00
parent 01a464dce5
commit a7d0a43fdd
11 changed files with 237 additions and 81 deletions

2
Cargo.lock generated
View File

@ -1027,7 +1027,7 @@ dependencies = [
[[package]]
name = "ffplayout-api"
version = "0.4.2"
version = "0.5.0"
dependencies = [
"actix-multipart",
"actix-web",

View File

@ -1,5 +1,5 @@
# give user www-data 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
www-data 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
www-data ALL = NOPASSWD: /bin/systemctl start ffplayout@*, /bin/systemctl stop ffplayout@*, /bin/systemctl reload ffplayout@*, /bin/systemctl restart ffplayout@*, /bin/systemctl status ffplayout@*, /bin/systemctl is-active ffplayout@*, /bin/systemctl enable ffplayout@*, /bin/systemctl disable ffplayout@*
www-data 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

@ -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:**
@ -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" \
curl -X PATCH http://127.0.0.1:8000/api/channel/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 }' \
"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 '{ "channel_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,56 @@
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()));
}
if let Ok(source_channel) = db_get_channel(&1).await {
if fs::copy(&source_channel.config_path, &target_channel.config_path).is_ok() {
match db_add_channel(target_channel).await {
Ok(c) => {
if let Err(e) = control_service(c.id, "enable").await {
return Err(e);
}
return Ok(c);
}
Err(e) => {
return Err(ServiceError::Conflict(e.to_string()));
}
};
}
}
Err(ServiceError::InternalServerError)
}
pub async fn delete_channel(id: i64) -> Result<(), ServiceError> {
if let Ok(channel) = db_get_channel(&id).await {
if control_service(channel.id, "stop").await.is_ok()
&& control_service(channel.id, "disable").await.is_ok()
{
if let Err(e) = fs::remove_file(channel.config_path) {
error!("{e}");
};
match db_delete_channel(&id).await {
Ok(_) => return Ok(()),
Err(e) => return Err(ServiceError::Conflict(e.to_string())),
}
}
}
Err(ServiceError::InternalServerError)
}

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,7 +33,7 @@ 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,
@ -42,7 +42,7 @@ async fn create_schema() -> Result<SqliteQueryResult, sqlx::Error> {
extra_extensions TEXT NOT NULL,
timezone TEXT NOT NULL,
service TEXT NOT NULL,
UNIQUE(channel_name)
UNIQUE(channel_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(channel_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,37 @@ 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 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 channel_name = $2, preview_url = $3, config_path = $4, extra_extensions = $5 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.channel_name.clone())
.bind(channel.preview_url.clone())
.bind(channel.config_path.clone())
.bind(channel.extra_extensions.clone())
.execute(&conn)
.await?;
conn.close().await;
@ -185,6 +185,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 (channel_name, preview_url, config_path, extra_extensions, timezone, service) VALUES($1, $2, $3, $4, $5, $6)";
let result = sqlx::query(query)
.bind(channel.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,7 +59,7 @@ 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,
@ -67,8 +67,5 @@ pub struct Settings {
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

@ -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:**
@ -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" \
/// curl -X PATCH http://127.0.0.1:8000/api/channel/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 }' \
/// "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 '{ "channel_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();