Files
tuliprox/backend/processing/src/playlist_watch.rs
T
euzuandGitHub 9ec775b35b Feature/multi crate (#837)
Refactored to multi crate project
2026-08-27 15:36:23 +02:00

135 lines
5.3 KiB
Rust

use log::error;
use shared::model::PlaylistGroup;
use std::{collections::BTreeSet, path::Path, sync::Arc};
use tuliprox_core::{
model::{AppConfig, MessageContent, WatchChanges},
utils,
utils::{binary_deserialize, binary_serialize, file_exists_async},
};
use tuliprox_messaging::send_message;
const WATCH_NOTIFICATION_LIST_LIMIT: usize = 120;
const WATCH_NOTIFICATION_SUMMARY_THRESHOLD: usize = 500;
pub async fn process_group_watch(
app_config: &Arc<AppConfig>,
client: &reqwest::Client,
target_name: &str,
pl: &PlaylistGroup,
) {
let mut new_tree = BTreeSet::new();
pl.channels.iter().for_each(|chan| {
let header = &chan.header;
let title = if header.title.is_empty() { header.name.clone() } else { header.title.clone() };
new_tree.insert(title);
});
let watch_filename =
format!("{}/{}.bin", utils::sanitize_filename(target_name), utils::sanitize_filename(&pl.title));
let cfg = app_config.config.load();
match utils::get_file_path(&cfg.storage_dir, Some(std::path::PathBuf::from(&watch_filename))) {
Some(path) => {
let save_path = path.as_path();
let mut changed = false;
if file_exists_async(&path).await {
if let Some(loaded_tree) = load_watch_tree(&path).await {
// Find elements in set2 but not in set1
let added_difference: BTreeSet<Arc<str>> = new_tree.difference(&loaded_tree).cloned().collect();
let removed_difference: BTreeSet<Arc<str>> = loaded_tree.difference(&new_tree).cloned().collect();
if !added_difference.is_empty() || !removed_difference.is_empty() {
changed = true;
handle_watch_notification(
app_config,
client,
&added_difference,
&removed_difference,
target_name,
&pl.title,
)
.await;
}
} else {
error!("failed to load watch_file {}", path.to_str().unwrap_or_default());
changed = true;
}
} else {
changed = true;
}
if changed {
match save_watch_tree(save_path, &new_tree).await {
Ok(()) => {}
Err(err) => {
error!("failed to write watch_file {}: {err}", save_path.to_str().unwrap_or_default());
}
}
}
}
None => {
error!("failed to write watch_file {watch_filename}");
}
}
}
async fn handle_watch_notification(
app_config: &Arc<AppConfig>,
client: &reqwest::Client,
added: &BTreeSet<Arc<str>>,
removed: &BTreeSet<Arc<str>>,
target_name: &str,
group_name: &str,
) {
let added_count = added.len();
let removed_count = removed.len();
let total_changed = added_count.saturating_add(removed_count);
let mut added = added.iter().map(std::string::ToString::to_string).collect::<Vec<String>>();
let mut removed = removed.iter().map(std::string::ToString::to_string).collect::<Vec<String>>();
if !added.is_empty() || !removed.is_empty() {
if total_changed > WATCH_NOTIFICATION_SUMMARY_THRESHOLD {
added = if added_count > 0 {
vec![format!(
"{added_count} entries added. Detailed list suppressed for large update ({total_changed} total changes)."
)]
} else {
vec![]
};
removed = if removed_count > 0 {
vec![format!(
"{removed_count} entries removed. Detailed list suppressed for large update ({total_changed} total changes)."
)]
} else {
vec![]
};
} else {
if added.len() > WATCH_NOTIFICATION_LIST_LIMIT {
let omitted = added.len() - WATCH_NOTIFICATION_LIST_LIMIT;
added.truncate(WATCH_NOTIFICATION_LIST_LIMIT);
added.push(format!("... {omitted} more added entries omitted"));
}
if removed.len() > WATCH_NOTIFICATION_LIST_LIMIT {
let omitted = removed.len() - WATCH_NOTIFICATION_LIST_LIMIT;
removed.truncate(WATCH_NOTIFICATION_LIST_LIMIT);
removed.push(format!("... {omitted} more removed entries omitted"));
}
}
let changes = WatchChanges { target: target_name.to_string(), group: group_name.to_string(), added, removed };
send_message(app_config, client, MessageContent::Watch(changes)).await;
}
}
async fn load_watch_tree(path: &Path) -> Option<BTreeSet<Arc<str>>> {
let encoded = tokio::fs::read(path).await.ok()?;
binary_deserialize(&encoded[..]).ok()
}
async fn save_watch_tree(path: &Path, tree: &BTreeSet<Arc<str>>) -> std::io::Result<()> {
// Ensure the parent directory exists unconditionally
if let Some(parent) = path.parent() {
tokio::fs::create_dir_all(parent).await?;
}
let encoded: Vec<u8> = binary_serialize(&tree)?;
tokio::fs::write(path, encoded).await
}