Merge pull request #810 from jb-alvarado/master

fix user permissions, remove signal term, scroll down on logs, scroll to item on playlist generation
This commit is contained in:
jb-alvarado 2024-10-24 14:16:56 +02:00 committed by GitHub
commit 4e6c33da02
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 180 additions and 124 deletions

49
Cargo.lock generated
View File

@ -341,9 +341,9 @@ dependencies = [
[[package]] [[package]]
name = "actix-web-lab" name = "actix-web-lab"
version = "0.22.0" version = "0.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a965e3e826aa4737af33666aa09ed949aa1837706fda2adee07039347be50d6" checksum = "ee75923689132fc5fb57ccc5bb98d25bb214796a29cd505844eb3b42daf11df0"
dependencies = [ dependencies = [
"actix-http", "actix-http",
"actix-router", "actix-router",
@ -366,7 +366,6 @@ dependencies = [
"local-channel", "local-channel",
"mediatype", "mediatype",
"mime", "mime",
"once_cell",
"pin-project-lite", "pin-project-lite",
"regex", "regex",
"serde", "serde",
@ -380,9 +379,9 @@ dependencies = [
[[package]] [[package]]
name = "actix-web-lab-derive" name = "actix-web-lab-derive"
version = "0.22.0" version = "0.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "008f98f5a68eeacf5e6d44ed74ce03c1b906baa53eabfb41faf0f5f40bd685f8" checksum = "4c221da13534b9352f3f79fcbbd6095f6d8aee63bdf1da8a73d36f9eeea17d5a"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -720,9 +719,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]] [[package]]
name = "bytes" name = "bytes"
version = "1.7.2" version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "428d9aa8fbc0670b7b8d6030a7fadd0f86151cae55e4dbbece15f3780a3dfaf3" checksum = "9ac0150caa2ae65ca5bd83f25c7de183dea78d4d366469f148435e2acfbad0da"
[[package]] [[package]]
name = "bytestring" name = "bytestring"
@ -1233,7 +1232,6 @@ dependencies = [
"ffprobe", "ffprobe",
"flexi_logger", "flexi_logger",
"futures-util", "futures-util",
"home",
"jsonwebtoken", "jsonwebtoken",
"lazy_static", "lazy_static",
"lettre", "lettre",
@ -1259,7 +1257,6 @@ dependencies = [
"serde_json", "serde_json",
"serde_with", "serde_with",
"shlex", "shlex",
"signal-child",
"sqlx", "sqlx",
"static-files", "static-files",
"sysinfo", "sysinfo",
@ -1326,9 +1323,9 @@ dependencies = [
[[package]] [[package]]
name = "flexi_logger" name = "flexi_logger"
version = "0.29.3" version = "0.29.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "719236bdbcf6033a3395165f797076b31056018e6723ccff616eb25fc9c99de1" checksum = "0bc6a1594377eb9de4205e15e33e222c996de8dc047f7c998cc477030bfac48a"
dependencies = [ dependencies = [
"chrono", "chrono",
"log", "log",
@ -2592,9 +2589,9 @@ dependencies = [
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.88" version = "1.0.89"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c3a7fc5db1e57d5a779a352c8cdb57b29aa4c40cc69c3a68a7fedc815fbf2f9" checksum = "f139b0662de085916d1fb67d2b4169d1addddda1919e696f3252b740b629986e"
dependencies = [ dependencies = [
"unicode-ident", "unicode-ident",
] ]
@ -3011,18 +3008,18 @@ checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b"
[[package]] [[package]]
name = "serde" name = "serde"
version = "1.0.210" version = "1.0.213"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" checksum = "3ea7893ff5e2466df8d720bb615088341b295f849602c6956047f8f80f0e9bc1"
dependencies = [ dependencies = [
"serde_derive", "serde_derive",
] ]
[[package]] [[package]]
name = "serde_derive" name = "serde_derive"
version = "1.0.210" version = "1.0.213"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" checksum = "7e85ad2009c50b58e87caa8cd6dac16bdf511bbfb7af6c33df902396aa480fa5"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -3186,12 +3183,6 @@ version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "signal-child"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a3184fa464a0128cbcc353100ae752a848bc0067dd5715a50550f31570051150"
[[package]] [[package]]
name = "signal-hook-registry" name = "signal-hook-registry"
version = "1.4.2" version = "1.4.2"
@ -3715,18 +3706,18 @@ dependencies = [
[[package]] [[package]]
name = "thiserror" name = "thiserror"
version = "1.0.64" version = "1.0.65"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d50af8abc119fb8bb6dbabcfa89656f46f84aa0ac7688088608076ad2b459a84" checksum = "5d11abd9594d9b38965ef50805c5e469ca9cc6f197f883f717e0269a3057b3d5"
dependencies = [ dependencies = [
"thiserror-impl", "thiserror-impl",
] ]
[[package]] [[package]]
name = "thiserror-impl" name = "thiserror-impl"
version = "1.0.64" version = "1.0.65"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08904e7672f5eb876eaaf87e0ce17857500934f4981c4a0ab2b4aa98baac7fc3" checksum = "ae71770322cbd277e69d762a16c444af02aa0575ac0d174f0b9562d3b37f8602"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -3791,9 +3782,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]] [[package]]
name = "tokio" name = "tokio"
version = "1.40.0" version = "1.41.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2b070231665d27ad9ec9b8df639893f46727666c6767db40317fbe920a5d998" checksum = "145f3413504347a2be84393cc8a7d2fb4d863b375909ea59f2158261aa258bbb"
dependencies = [ dependencies = [
"backtrace", "backtrace",
"bytes", "bytes",

View File

@ -18,7 +18,7 @@ actix-multipart = "0.7"
actix-web = "4" actix-web = "4"
actix-web-grants = "4" actix-web-grants = "4"
actix-web-httpauth = "0.8" actix-web-httpauth = "0.8"
actix-web-lab = "0.22" actix-web-lab = "0.23"
actix-web-static-files = "4.0" actix-web-static-files = "4.0"
argon2 = "0.5" argon2 = "0.5"
chrono = { version = "0.4", default-features = false, features = ["clock", "std", "serde"] } chrono = { version = "0.4", default-features = false, features = ["clock", "std", "serde"] }
@ -29,7 +29,6 @@ faccess = "0.2"
ffprobe = "0.4" ffprobe = "0.4"
flexi_logger = { version = "0.29", features = ["kv", "colors"] } flexi_logger = { version = "0.29", features = ["kv", "colors"] }
futures-util = { version = "0.3", default-features = false, features = ["std"] } futures-util = { version = "0.3", default-features = false, features = ["std"] }
home = "0.5"
jsonwebtoken = "9" jsonwebtoken = "9"
lazy_static = "1.4" lazy_static = "1.4"
lettre = { version = "0.11", features = ["builder", "rustls-tls", "smtp-transport", "tokio1", "tokio1-rustls-tls"], default-features = false } lettre = { version = "0.11", features = ["builder", "rustls-tls", "smtp-transport", "tokio1", "tokio1-rustls-tls"], default-features = false }
@ -70,9 +69,6 @@ zeromq = { version = "0.4", default-features = false, features = [
"tcp-transport", "tcp-transport",
] } ] }
[target.'cfg(not(target_arch = "windows"))'.dependencies]
signal-child = "1"
[build-dependencies] [build-dependencies]
static-files = "0.2" static-files = "0.2"

View File

@ -467,8 +467,7 @@ async fn get_all_channels(
/// ``` /// ```
#[patch("/channel/{id}")] #[patch("/channel/{id}")]
#[protect( #[protect(
"Role::GlobalAdmin", any("Role::GlobalAdmin", "Role::ChannelAdmin"),
"Role::ChannelAdmin",
ty = "Role", ty = "Role",
expr = "user.channels.contains(&*id) || role.has_authority(&Role::GlobalAdmin)" expr = "user.channels.contains(&*id) || role.has_authority(&Role::GlobalAdmin)"
)] )]
@ -984,10 +983,10 @@ pub async fn process_control(
} }
ProcessCtl::Stop => { ProcessCtl::Stop => {
manager.channel.lock().unwrap().active = false; manager.channel.lock().unwrap().active = false;
manager.async_stop().await; manager.async_stop().await?;
} }
ProcessCtl::Restart => { ProcessCtl::Restart => {
manager.async_stop().await; manager.async_stop().await?;
tokio::time::sleep(tokio::time::Duration::from_millis(1500)).await; tokio::time::sleep(tokio::time::Duration::from_millis(1500)).await;
if !manager.is_alive.load(Ordering::SeqCst) { if !manager.is_alive.load(Ordering::SeqCst) {

View File

@ -11,9 +11,7 @@ use std::{
time::Duration, time::Duration,
}; };
#[cfg(not(windows))] use actix_web::web;
use signal_child::Signalable;
use log::*; use log::*;
use m3u8_rs::Playlist; use m3u8_rs::Playlist;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -26,7 +24,7 @@ use crate::player::{
}; };
use crate::utils::{ use crate::utils::{
config::{OutputMode::*, PlayoutConfig}, config::{OutputMode::*, PlayoutConfig},
errors::ProcessError, errors::{ProcessError, ServiceError},
}; };
use crate::ARGS; use crate::ARGS;
use crate::{ use crate::{
@ -203,11 +201,6 @@ impl ChannelManager {
match unit { match unit {
Decoder => { Decoder => {
if let Some(proc) = self.decoder.lock()?.as_mut() { if let Some(proc) = self.decoder.lock()?.as_mut() {
#[cfg(not(windows))]
proc.term()
.map_err(|e| ProcessError::Custom(format!("Decoder: {e}")))?;
#[cfg(windows)]
proc.kill() proc.kill()
.map_err(|e| ProcessError::Custom(format!("Decoder: {e}")))?; .map_err(|e| ProcessError::Custom(format!("Decoder: {e}")))?;
} }
@ -258,36 +251,48 @@ impl ChannelManager {
Ok(()) Ok(())
} }
pub async fn async_stop(&self) { pub async fn async_stop(&self) -> Result<(), ServiceError> {
let channel_id = self.channel.lock().unwrap().id;
if self.is_alive.load(Ordering::SeqCst) {
debug!(target: Target::all(), channel = channel_id; "Deactivate playout and stop all child processes from channel: <yellow>{channel_id}</>");
}
self.is_terminated.store(true, Ordering::SeqCst); self.is_terminated.store(true, Ordering::SeqCst);
self.is_alive.store(false, Ordering::SeqCst); self.is_alive.store(false, Ordering::SeqCst);
self.ingest_is_running.store(false, Ordering::SeqCst); self.ingest_is_running.store(false, Ordering::SeqCst);
self.run_count.fetch_sub(1, Ordering::SeqCst); self.run_count.fetch_sub(1, Ordering::SeqCst);
let pool = self.db_pool.clone().unwrap(); let pool = self.db_pool.clone().unwrap();
let channel_id = self.channel.lock().unwrap().id;
debug!(target: Target::all(), channel = channel_id; "Deactivate playout and stop all child processes from channel: <yellow>{channel_id}</>");
if let Err(e) = handles::update_player(&pool, channel_id, false).await { if let Err(e) = handles::update_player(&pool, channel_id, false).await {
error!(target: Target::all(), channel = channel_id; "Unable write to player status: {e}"); error!(target: Target::all(), channel = channel_id; "Unable write to player status: {e}");
}; };
for unit in [Decoder, Encoder, Ingest] { for unit in [Decoder, Encoder, Ingest] {
if let Err(e) = self.stop(unit) { let self_clone = self.clone();
if let Err(e) = web::block(move || self_clone.stop(unit)).await? {
if !e.to_string().contains("exited process") { if !e.to_string().contains("exited process") {
error!(target: Target::all(), channel = channel_id; "{e}") error!(target: Target::all(), channel = channel_id; "{e}")
} }
} }
} }
Ok(())
} }
/// No matter what is running, terminate them all. /// No matter what is running, terminate them all.
pub fn stop_all(&self) { pub fn stop_all(&self) {
let channel_id = self.channel.lock().unwrap().id;
if self.is_alive.load(Ordering::SeqCst) {
debug!(target: Target::all(), channel = channel_id; "Stop all child processes from channel: <yellow>{channel_id}</>");
}
self.is_terminated.store(true, Ordering::SeqCst); self.is_terminated.store(true, Ordering::SeqCst);
self.is_alive.store(false, Ordering::SeqCst); self.is_alive.store(false, Ordering::SeqCst);
self.ingest_is_running.store(false, Ordering::SeqCst); self.ingest_is_running.store(false, Ordering::SeqCst);
self.run_count.fetch_sub(1, Ordering::SeqCst); self.run_count.fetch_sub(1, Ordering::SeqCst);
let channel_id = self.channel.lock().unwrap().id;
debug!(target: Target::all(), channel = channel_id; "Stop all child processes from channel: <yellow>{channel_id}</>");
for unit in [Decoder, Encoder, Ingest] { for unit in [Decoder, Encoder, Ingest] {
if let Err(e) = self.stop(unit) { if let Err(e) = self.stop(unit) {
@ -400,6 +405,7 @@ fn find_m3u8_files(path: &Path) -> io::Result<Vec<String>> {
Ok(m3u8_files) Ok(m3u8_files)
} }
/// Check if segment is in playlist, if not, delete it.
fn delete_old_segments<P: AsRef<Path> + Clone + std::fmt::Debug>( fn delete_old_segments<P: AsRef<Path> + Clone + std::fmt::Debug>(
path: P, path: P,
pl_segments: &[String], pl_segments: &[String],

View File

@ -15,8 +15,9 @@
v-model="channel.name" v-model="channel.name"
type="text" type="text"
placeholder="Type here" placeholder="Type here"
class="input input-bordered w-full" class="input input-bordered w-full !bg-base-100"
@keyup="isChanged" @keyup="isChanged"
:disabled="authStore.role === 'User'"
/> />
</label> </label>
@ -27,8 +28,9 @@
<input <input
v-model="channel.preview_url" v-model="channel.preview_url"
type="text" type="text"
class="input input-bordered w-full" class="input input-bordered w-full !bg-base-100"
@keyup="isChanged" @keyup="isChanged"
:disabled="authStore.role === 'User'"
/> />
</label> </label>
@ -39,8 +41,10 @@
<input <input
v-model="channel.extra_extensions" v-model="channel.extra_extensions"
type="text" type="text"
class="input input-bordered w-full" class="input input-bordered w-full !bg-base-100"
:class="'input-disabled'"
@keyup="isChanged" @keyup="isChanged"
:disabled="authStore.role === 'User'"
/> />
</label> </label>
@ -88,7 +92,7 @@
</label> </label>
</template> </template>
<div class="my-4 flex gap-1"> <div v-if="authStore.role !== 'User'" class="my-4 flex gap-1">
<button class="btn" :class="saved ? 'btn-primary' : 'btn-error'" @click="addUpdateChannel()"> <button class="btn" :class="saved ? 'btn-primary' : 'btn-error'" @click="addUpdateChannel()">
{{ t('config.save') }} {{ t('config.save') }}
</button> </button>
@ -218,7 +222,10 @@ async function addUpdateChannel() {
await updateChannel() await updateChannel()
} }
await configStore.getAdvancedConfig() if (authStore.role === 'GlobalAdmin') {
await configStore.getAdvancedConfig()
}
await configStore.getPlayoutConfig() await configStore.getPlayoutConfig()
await configStore.getUserConfig() await configStore.getUserConfig()
await mediaStore.getTree('') await mediaStore.getTree('')
@ -242,8 +249,12 @@ async function deleteChannel() {
}) })
i.value = configStore.i - 1 i.value = configStore.i - 1
if (authStore.role === 'GlobalAdmin') {
await configStore.getAdvancedConfig()
}
await configStore.getChannelConfig() await configStore.getChannelConfig()
await configStore.getAdvancedConfig()
await configStore.getPlayoutConfig() await configStore.getPlayoutConfig()
await configStore.getUserConfig() await configStore.getUserConfig()
await mediaStore.getTree('') await mediaStore.getTree('')

View File

@ -705,6 +705,11 @@
class="textarea textarea-bordered" class="textarea textarea-bordered"
rows="6" rows="6"
/> />
<div class="label">
<span class="text-sm select-text text-base-content/80">
{{ t('config.outputParam') }}
</span>
</div>
</label> </label>
</div> </div>
<div class="mt-5 mb-10"> <div class="mt-5 mb-10">

View File

@ -149,7 +149,9 @@ const user = ref({
} as User) } as User)
onMounted(() => { onMounted(() => {
getUsers() if (authStore.role === 'GlobalAdmin') {
getUsers()
}
}) })
async function getUsers() { async function getUsers() {

View File

@ -15,18 +15,26 @@
<SvgIcon name="burger" classes="w-5 h-5" /> <SvgIcon name="burger" classes="w-5 h-5" />
</summary> </summary>
<ul class="menu menu-sm dropdown-content mt-1 z-[1] p-2 shadow bg-base-100 rounded-box w-52"> <ul class="menu menu-sm dropdown-content mt-1 z-[1] p-2 shadow bg-base-100 rounded-box w-52">
<li v-for="item in menuItems" :key="item.name" class="bg-base-100 rounded-md"> <template v-for="item in menuItems" :key="item.name">
<NuxtLink <li
:to="item.link" v-if="
class="h-[27px] text-base" item.label !== 'message' ||
exact-active-class="is-active" (configStore.playout.text.add_text && !configStore.playout.text.text_from_filename)
@click="closeMenu()" "
class="bg-base-100 rounded-md"
> >
<span> <NuxtLink
{{ item.name }} :to="item.link"
</span> class="h-[27px] text-base"
</NuxtLink> exact-active-class="is-active"
</li> @click="closeMenu()"
>
<span>
{{ item.name }}
</span>
</NuxtLink>
</li>
</template>
<li v-if="configStore.channels.length > 1"> <li v-if="configStore.channels.length > 1">
<details tabindex="0" @focusout="closeDropdown"> <details tabindex="0" @focusout="closeDropdown">
<summary> <summary>
@ -37,7 +45,9 @@
<ul class="p-2"> <ul class="p-2">
<li v-for="(channel, index) in configStore.channels" :key="index"> <li v-for="(channel, index) in configStore.channels" :key="index">
<span> <span>
<a class="dropdown-item cursor-pointer" @click="selectChannel(index)">{{ channel.name }}</a> <a class="dropdown-item cursor-pointer" @click="selectChannel(index)">{{
channel.name
}}</a>
</span> </span>
</li> </li>
</ul> </ul>
@ -53,17 +63,26 @@
</div> </div>
<div class="navbar-end hidden 2sm:flex w-4/5 min-w-[750px]"> <div class="navbar-end hidden 2sm:flex w-4/5 min-w-[750px]">
<ul class="menu menu-sm menu-horizontal px-1"> <ul class="menu menu-sm menu-horizontal px-1">
<li v-for="item in menuItems" :key="item.name" class="bg-base-100 rounded-md p-0"> <template v-for="item in menuItems" :key="item.name">
<NuxtLink <li
:to="item.link" v-if="
class="px-2 h-[27px] relative text-base text-base-content" item.label !== 'message' ||
active-class="is-active" (configStore.playout.text.add_text && !configStore.playout.text.text_from_filename)
"
class="bg-base-100 rounded-md p-0"
> >
<span> <NuxtLink
{{ item.name }} :to="item.link"
</span> class="px-2 h-[27px] relative text-base text-base-content"
</NuxtLink> active-class="is-active"
</li> >
<span>
{{ item.name }}
</span>
</NuxtLink>
</li>
</template>
<li v-if="configStore.channels.length > 1"> <li v-if="configStore.channels.length > 1">
<details tabindex="0" @focusout="closeDropdown"> <details tabindex="0" @focusout="closeDropdown">
<summary> <summary>
@ -112,12 +131,12 @@ const menuDropdown = ref()
const isOpen = ref(false) const isOpen = ref(false)
const menuItems = ref([ const menuItems = ref([
{ name: t('button.home'), link: localePath({ name: 'index' }) }, { label: 'index', name: t('button.home'), link: localePath({ name: 'index' }) },
{ name: t('button.player'), link: localePath({ name: 'player' }) }, { label: 'player', name: t('button.player'), link: localePath({ name: 'player' }) },
{ name: t('button.media'), link: localePath({ name: 'media' }) }, { label: 'media', name: t('button.media'), link: localePath({ name: 'media' }) },
{ name: t('button.message'), link: localePath({ name: 'message' }) }, { label: 'message', name: t('button.message'), link: localePath({ name: 'message' }) },
{ name: t('button.logging'), link: localePath({ name: 'logging' }) }, { label: 'logging', name: t('button.logging'), link: localePath({ name: 'logging' }) },
{ name: t('button.configure'), link: localePath({ name: 'configure' }) }, { label: 'configure', name: t('button.configure'), link: localePath({ name: 'configure' }) },
]) ])
if (colorMode.value === 'dark') { if (colorMode.value === 'dark') {
@ -162,7 +181,11 @@ function logout() {
function selectChannel(index: number) { function selectChannel(index: number) {
configStore.i = index configStore.i = index
configStore.getAdvancedConfig()
if (authStore.role === 'GlobalAdmin') {
configStore.getAdvancedConfig()
}
configStore.getPlayoutConfig() configStore.getPlayoutConfig()
} }

View File

@ -420,6 +420,7 @@ async function generatePlaylist() {
resetCheckboxes() resetCheckboxes()
resetTemplate() resetTemplate()
playlistStore.scrollToItem = true
playlistStore.isLoading = false playlistStore.isLoading = false
} }
</script> </script>

View File

@ -140,7 +140,7 @@ const playlistContainer = ref()
const sortContainer = ref() const sortContainer = ref()
const todayDate = ref($dayjs().utcOffset(configStore.utcOffset).format('YYYY-MM-DD')) const todayDate = ref($dayjs().utcOffset(configStore.utcOffset).format('YYYY-MM-DD'))
const { i } = storeToRefs(useConfig()) const { i } = storeToRefs(useConfig())
const { currentIndex, listDate, playoutIsRunning } = storeToRefs(usePlaylist()) const { currentIndex, listDate, playoutIsRunning, scrollToItem } = storeToRefs(usePlaylist())
const playlistSortOptions = { const playlistSortOptions = {
group: 'playlist', group: 'playlist',
@ -175,10 +175,14 @@ watch([listDate, i], () => {
}, 800) }, 800)
}) })
watch([playoutIsRunning], () => { watch([playoutIsRunning, scrollToItem], () => {
setTimeout(() => { if (playoutIsRunning.value || scrollToItem.value) {
scrollTo(currentIndex.value) setTimeout(() => {
}, 400) scrollTo(currentIndex.value)
scrollToItem.value = false
}, 400)
}
}) })
defineExpose({ defineExpose({

View File

@ -1,8 +1,8 @@
export default { export default {
ok: 'Ok', ok: 'Ok',
cancel: 'Abbrechen', cancel: 'Abbrechen',
socketConnected: 'Event Stream verbunden', socketConnected: 'Message-Stream verbunden',
socketDisconnected: 'Event Stream nicht verbunden', socketDisconnected: 'Message-Stream nicht verbunden',
alert: { alert: {
wrongLogin: 'Falsche Anmeldedaten!', wrongLogin: 'Falsche Anmeldedaten!',
}, },
@ -206,7 +206,7 @@ export default {
playlistLength: 'Ziel-Länge der Playlist; wenn es leer ist, wird die reale Länge nicht berücksichtigt.', playlistLength: 'Ziel-Länge der Playlist; wenn es leer ist, wird die reale Länge nicht berücksichtigt.',
playlistInfinit: 'Eine einzelne Playlist-Datei endlos wiederholen.', playlistInfinit: 'Eine einzelne Playlist-Datei endlos wiederholen.',
storageHelp: 'Speichereinstellungen, die Standorte sind relativ zum Kanal-Speicher.', storageHelp: 'Speichereinstellungen, die Standorte sind relativ zum Kanal-Speicher.',
storageFiller: 'Verwende Füllmaterial, um anstelle einer fehlenden Datei abzuspielen oder um die verbleibende Zeit zu füllen, um eine Gesamtdauer von 24 Stunden zu erreichen. Es kann eine Datei oder ein Ordner sein und wird bei Bedarf wiederholt.', storageFiller: 'Verwenden Sie einen Platzhalter, um eine fehlende Datei abzuspielen oder um die verbleibende Zeit auf insgesamt 24 Stunden zu füllen. Es kann sich um eine Datei oder einen Ordner mit relativem Pfad handeln, der bei Bedarf wiederholt wird.',
storageExtension: 'Gib an, welche Dateien gesucht und verwendet werden sollen.', storageExtension: 'Gib an, welche Dateien gesucht und verwendet werden sollen.',
storageShuffle: 'Wähle Dateien zufällig aus (im Ordner-Modus und bei der Playlist-Erstellung).', storageShuffle: 'Wähle Dateien zufällig aus (im Ordner-Modus und bei der Playlist-Erstellung).',
textHelp: 'Texteinblendung in Kombination mit libzmq für die Fernmanipulation von Text.', textHelp: 'Texteinblendung in Kombination mit libzmq für die Fernmanipulation von Text.',
@ -217,6 +217,7 @@ export default {
taskHelp: 'Führe ein externes Programm mit einem gegebenen Medienobjekt aus. Das Medienobjekt ist im JSON-Format und enthält alle Informationen über den aktuellen Clip. Das externe Programm kann ein Skript oder eine Binärdatei sein, sollte aber nur für kurze Zeit laufen.', taskHelp: 'Führe ein externes Programm mit einem gegebenen Medienobjekt aus. Das Medienobjekt ist im JSON-Format und enthält alle Informationen über den aktuellen Clip. Das externe Programm kann ein Skript oder eine Binärdatei sein, sollte aber nur für kurze Zeit laufen.',
taskPath: 'Pfad zur ausführbaren Datei.', taskPath: 'Pfad zur ausführbaren Datei.',
outputHelp: `Die endgültige Playout-Codierung, passe die Einstellungen nach deinen Bedürfnissen an. Verwende den 'stream'-Modus und passe den 'Ausgabe-Parameter' an, wenn du zu einem RTMP/RTSP/SRT/...-Server streamen möchtest. Im Produktionsbetrieb verwende kein HLS mit ffplayout; nutze Nginx oder einen anderen Webserver!`, outputHelp: `Die endgültige Playout-Codierung, passe die Einstellungen nach deinen Bedürfnissen an. Verwende den 'stream'-Modus und passe den 'Ausgabe-Parameter' an, wenn du zu einem RTMP/RTSP/SRT/...-Server streamen möchtest. Im Produktionsbetrieb verwende kein HLS mit ffplayout; nutze Nginx oder einen anderen Webserver!`,
outputParam: 'HLS-Segment- und Playlist-Pfade sind relativ.',
restartTile: 'Playout neustarten', restartTile: 'Playout neustarten',
restartText: 'ffplayout neustarten um Einstellungen anzuwenden?', restartText: 'ffplayout neustarten um Einstellungen anzuwenden?',
updatePlayoutSuccess: 'Update der Playout-Konfiguration erfolgreich!', updatePlayoutSuccess: 'Update der Playout-Konfiguration erfolgreich!',

View File

@ -1,8 +1,8 @@
export default { export default {
ok: 'Ok', ok: 'Ok',
cancel: 'Cancel', cancel: 'Cancel',
socketConnected: 'Event stream connected', socketConnected: 'Message stream connected',
socketDisconnected: 'Event stream disconnected', socketDisconnected: 'Message stream disconnected',
alert: { alert: {
wrongLogin: 'Incorrect login data!', wrongLogin: 'Incorrect login data!',
}, },
@ -206,7 +206,7 @@ export default {
playlistLength: 'Target length of the playlist; when it is blank, the real length will not be considered.', playlistLength: 'Target length of the playlist; when it is blank, the real length will not be considered.',
playlistInfinit: 'Loop a single playlist file infinitely.', playlistInfinit: 'Loop a single playlist file infinitely.',
storageHelp: 'Storage settings, locations are relative to channel storage.', storageHelp: 'Storage settings, locations are relative to channel storage.',
storageFiller: 'Use filler to play in place of a missing file or to fill the remaining time to reach a total of 24 hours. It can be a file or folder and will loop when necessary.', storageFiller: 'Use filler to play in place of a missing file or to fill the remaining time to reach a total of 24 hours. It can be a file or folder, with relative path, and will loop when necessary.',
storageExtension: 'Specify which files to search and use.', storageExtension: 'Specify which files to search and use.',
storageShuffle: 'Pick files randomly (in folder mode and playlist generation).', storageShuffle: 'Pick files randomly (in folder mode and playlist generation).',
textHelp: 'Overlay text in combination with libzmq for remote text manipulation.', textHelp: 'Overlay text in combination with libzmq for remote text manipulation.',
@ -218,6 +218,7 @@ export default {
taskPath: 'Path to executable.', taskPath: 'Path to executable.',
outputHelp: `The final playout encoding, set the settings according to your needs. Use 'stream' mode and adjust the 'Output Parameter' when you want to stream to an RTMP/RTSP/SRT/... server. outputHelp: `The final playout encoding, set the settings according to your needs. Use 'stream' mode and adjust the 'Output Parameter' when you want to stream to an RTMP/RTSP/SRT/... server.
In production, don't serve HLS playlists with ffplayout; use Nginx or another web server!`, In production, don't serve HLS playlists with ffplayout; use Nginx or another web server!`,
outputParam: 'HLS segment and playlist paths are relative.',
restartTile: 'Restart Playout', restartTile: 'Restart Playout',
restartText: 'Restart ffplayout to apply changes?', restartText: 'Restart ffplayout to apply changes?',
updatePlayoutSuccess: 'Update playout config success!', updatePlayoutSuccess: 'Update playout config success!',

View File

@ -206,7 +206,7 @@ export default {
playlistLength: 'Duração alvo da playlist; quando estiver em branco, o comprimento real não será considerado.', playlistLength: 'Duração alvo da playlist; quando estiver em branco, o comprimento real não será considerado.',
playlistInfinit: 'Reproduza infinitamente um único arquivo de playlist.', playlistInfinit: 'Reproduza infinitamente um único arquivo de playlist.',
storageHelp: 'Configurações de armazenamento, os locais são relativos ao armazenamento do canal.', storageHelp: 'Configurações de armazenamento, os locais são relativos ao armazenamento do canal.',
storageFiller: 'Use preenchimento para tocar no lugar de um arquivo ausente ou para preencher o tempo restante para atingir um total de 24 horas. Pode ser um arquivo ou pasta e será repetido quando necessário.', storageFiller: 'Use um preenchimento para reproduzir no lugar de um arquivo ausente ou preencher o tempo restante para alcançar um total de 24 horas. Pode ser um arquivo ou uma pasta com caminho relativo, e será repetido quando necessário.',
storageExtension: 'Especifique quais arquivos procurar e usar.', storageExtension: 'Especifique quais arquivos procurar e usar.',
storageShuffle: 'Escolha arquivos aleatoriamente (no modo de pasta e geração de playlist).', storageShuffle: 'Escolha arquivos aleatoriamente (no modo de pasta e geração de playlist).',
textHelp: 'Sobrepor texto em combinação com libzmq para manipulação remota de texto.', textHelp: 'Sobrepor texto em combinação com libzmq para manipulação remota de texto.',
@ -217,6 +217,7 @@ export default {
taskHelp: 'Execute um programa externo com um objeto de mídia fornecido. O objeto de mídia está em formato JSON e contém todas as informações sobre o clipe atual. O programa externo pode ser um script ou binário, mas deve ser executado apenas por um curto período de tempo.', taskHelp: 'Execute um programa externo com um objeto de mídia fornecido. O objeto de mídia está em formato JSON e contém todas as informações sobre o clipe atual. O programa externo pode ser um script ou binário, mas deve ser executado apenas por um curto período de tempo.',
taskPath: 'Caminho para o executável.', taskPath: 'Caminho para o executável.',
outputHelp: `A codificação final do playout, ajuste as configurações de acordo com suas necessidades. Use o modo 'stream' e ajuste o 'Parâmetro de Saída' quando quiser fazer streaming para um servidor RTMP/RTSP/SRT/... No ambiente de produção, não sirva playlists HLS com ffplayout; use Nginx ou outro servidor web!`, outputHelp: `A codificação final do playout, ajuste as configurações de acordo com suas necessidades. Use o modo 'stream' e ajuste o 'Parâmetro de Saída' quando quiser fazer streaming para um servidor RTMP/RTSP/SRT/... No ambiente de produção, não sirva playlists HLS com ffplayout; use Nginx ou outro servidor web!`,
outputParam: 'Os caminhos dos segmentos e playlists HLS são relativos.',
restartTile: 'Reiniciar Playout', restartTile: 'Reiniciar Playout',
restartText: 'Reiniciar o ffplayout para aplicar as alterações?', restartText: 'Reiniciar o ffplayout para aplicar as alterações?',
updatePlayoutSuccess: 'Sucesso na atualização da configuração do playout!', updatePlayoutSuccess: 'Sucesso na atualização da configuração do playout!',

View File

@ -1,8 +1,8 @@
export default { export default {
ok: 'ОК', ok: 'ОК',
cancel: 'Отмена', cancel: 'Отмена',
socketConnected: 'Event stream connected', socketConnected: 'Message stream connected',
socketDisconnected: 'Event stream disconnected', socketDisconnected: 'Message stream disconnected',
alert: { alert: {
wrongLogin: 'Неверные данные для входа!', wrongLogin: 'Неверные данные для входа!',
}, },
@ -206,7 +206,7 @@ export default {
playlistLength: 'Target length of the playlist; when it is blank, the real length will not be considered.', playlistLength: 'Target length of the playlist; when it is blank, the real length will not be considered.',
playlistInfinit: 'Loop a single playlist file infinitely.', playlistInfinit: 'Loop a single playlist file infinitely.',
storageHelp: 'Storage settings, locations are relative to channel storage.', storageHelp: 'Storage settings, locations are relative to channel storage.',
storageFiller: 'Use filler to play in place of a missing file or to fill the remaining time to reach a total of 24 hours. It can be a file or folder and will loop when necessary.', storageFiller: 'Use filler to play in place of a missing file or to fill the remaining time to reach a total of 24 hours. It can be a file or folder, with relative path, and will loop when necessary.',
storageExtension: 'Specify which files to search and use.', storageExtension: 'Specify which files to search and use.',
storageShuffle: 'Pick files randomly (in folder mode and playlist generation).', storageShuffle: 'Pick files randomly (in folder mode and playlist generation).',
textHelp: 'Overlay text in combination with libzmq for remote text manipulation.', textHelp: 'Overlay text in combination with libzmq for remote text manipulation.',
@ -218,6 +218,7 @@ export default {
taskPath: 'Path to executable.', taskPath: 'Path to executable.',
outputHelp: `The final playout encoding, set the settings according to your needs. Use 'stream' mode and adjust the 'Output Parameter' when you want to stream to an RTMP/RTSP/SRT/... server. outputHelp: `The final playout encoding, set the settings according to your needs. Use 'stream' mode and adjust the 'Output Parameter' when you want to stream to an RTMP/RTSP/SRT/... server.
In production, don't serve HLS playlists with ffplayout; use Nginx or another web server!`, In production, don't serve HLS playlists with ffplayout; use Nginx or another web server!`,
outputParam: 'HLS segment and playlist paths are relative.',
restartTile: 'Перезапуск Playout', restartTile: 'Перезапуск Playout',
restartText: 'Перезапустить ffplayout для применения изменений?', restartText: 'Перезапустить ffplayout для применения изменений?',
updatePlayoutSuccess: 'Обновление конфигурации воспроизведения прошло успешно!', updatePlayoutSuccess: 'Обновление конфигурации воспроизведения прошло успешно!',

View File

@ -17,6 +17,7 @@
Advanced Advanced
</button> </button>
<button <button
v-if="authStore.role !== 'User'"
class="join-item btn btn-sm btn-primary mt-1 duration-500" class="join-item btn btn-sm btn-primary mt-1 duration-500"
:class="activeConf === 3 && 'btn-secondary'" :class="activeConf === 3 && 'btn-secondary'"
@click="activeConf = 3" @click="activeConf = 3"

View File

@ -12,17 +12,17 @@
<NuxtLink :to="localePath({ name: 'media' })" class="btn join-item btn-primary px-2"> <NuxtLink :to="localePath({ name: 'media' })" class="btn join-item btn-primary px-2">
{{ t('button.media') }} {{ t('button.media') }}
</NuxtLink> </NuxtLink>
<NuxtLink :to="localePath({ name: 'message' })" class="btn join-item btn-primary px-2"> <NuxtLink
v-if="configStore.playout.text.add_text && !configStore.playout.text.text_from_filename"
:to="localePath({ name: 'message' })"
class="btn join-item btn-primary px-2"
>
{{ t('button.message') }} {{ t('button.message') }}
</NuxtLink> </NuxtLink>
<NuxtLink :to="localePath({ name: 'logging' })" class="btn join-item btn-primary px-2"> <NuxtLink :to="localePath({ name: 'logging' })" class="btn join-item btn-primary px-2">
{{ t('button.logging') }} {{ t('button.logging') }}
</NuxtLink> </NuxtLink>
<NuxtLink <NuxtLink :to="localePath({ name: 'configure' })" class="btn join-item btn-primary px-2">
v-if="authStore.role.toLowerCase().includes('admin')"
:to="localePath({ name: 'configure' })"
class="btn join-item btn-primary px-2"
>
{{ t('button.configure') }} {{ t('button.configure') }}
</NuxtLink> </NuxtLink>
<button class="btn join-item btn-primary px-2" @click="logout()"> <button class="btn join-item btn-primary px-2" @click="logout()">

View File

@ -34,10 +34,9 @@
</div> </div>
</div> </div>
<div class="px-3 inline-block h-[calc(100vh-140px)] text-[13px]"> <div class="px-3 inline-block h-[calc(100vh-140px)] text-[13px]">
<div <div id="log-container" class="bg-base-300 whitespace-pre h-full font-mono overflow-auto p-3">
class="bg-base-300 whitespace-pre h-full font-mono overflow-auto p-3" <div id="log-content" v-html="filterLogsBySeverity(formatLog(currentLog), errorLevel)" />
v-html="filterLogsBySeverity(formatLog(currentLog), errorLevel)" </div>
/>
</div> </div>
</div> </div>
</template> </template>
@ -88,7 +87,16 @@ watch([listDate, i], () => {
}) })
const calendarFormat = (date: Date) => { const calendarFormat = (date: Date) => {
return $dayjs(date).locale(locale.value).format('ddd L') return $dayjs(date).locale(locale.value).format('ddd L')
}
function scrollTo() {
const parent = document.getElementById('log-container')
const child = document.getElementById('log-content')
if (child && parent) {
parent.scrollTop = child.scrollHeight
}
} }
function filterLogsBySeverity(logString: string, minSeverity: string): string { function filterLogsBySeverity(logString: string, minSeverity: string): string {
@ -121,6 +129,10 @@ async function getLog() {
.then((response) => response.text()) .then((response) => response.text())
.then((data) => { .then((data) => {
currentLog.value = data currentLog.value = data
nextTick(() => {
scrollTo()
})
}) })
.catch(() => { .catch(() => {
currentLog.value = '' currentLog.value = ''

View File

@ -330,14 +330,6 @@ useHead({
title: `${t('button.media')} | ffplayout`, title: `${t('button.media')} | ffplayout`,
}) })
watch([width], () => {
if (width.value < 640) {
horizontal.value = true
} else {
horizontal.value = false
}
})
const horizontal = ref(false) const horizontal = ref(false)
const deleteName = ref('') const deleteName = ref('')
const recursive = ref(false) const recursive = ref(false)
@ -384,6 +376,14 @@ onMounted(async () => {
} }
}) })
watch([width], () => {
if (width.value < 640) {
horizontal.value = true
} else {
horizontal.value = false
}
})
watch([i], () => { watch([i], () => {
mediaStore.getTree('') mediaStore.getTree('')
}) })

View File

@ -32,7 +32,7 @@ export const useConfig = defineStore('config', {
await this.getPlayoutConfig() await this.getPlayoutConfig()
await this.getUserConfig() await this.getUserConfig()
if (this.configUser.id === 1) { if (authStore.role === 'GlobalAdmin') {
await this.getAdvancedConfig() await this.getAdvancedConfig()
} }
}) })

View File

@ -24,6 +24,7 @@ export const usePlaylist = defineStore('playlist', {
playoutIsRunning: false, playoutIsRunning: false,
last_channel: 0, last_channel: 0,
firstLoad: true, firstLoad: true,
scrollToItem: false,
}), }),
getters: {}, getters: {},