diff --git a/backend/src/api/config_file.rs b/backend/src/api/config_file.rs index f92624167..1c0e530fa 100644 --- a/backend/src/api/config_file.rs +++ b/backend/src/api/config_file.rs @@ -88,8 +88,7 @@ impl ConfigFile { Self::load_mapping(app_state) } - async fn reload_source_file(app_state: &Arc, file: &Path) -> Result<(), TuliproxError> { - info!("Loaded sources file {}", file.display()); + async fn reload_source_file(app_state: &Arc) -> Result<(), TuliproxError> { // TODO selective update and not complete sources update ? ConfigFile::load_sources(app_state).await } @@ -115,7 +114,7 @@ impl ConfigFile { } ConfigFile::SourceFile => { app_state.event_manager.send_event(EventMessage::ConfigChange(ConfigType::Sources)); - ConfigFile::reload_source_file(app_state, file_path).await + ConfigFile::reload_source_file(app_state).await } } } diff --git a/backend/src/model/config/source.rs b/backend/src/model/config/source.rs index b3434e54a..ca73821e9 100644 --- a/backend/src/model/config/source.rs +++ b/backend/src/model/config/source.rs @@ -50,6 +50,7 @@ impl TryFrom<&SourcesConfigDto> for SourcesConfig { fn try_from(dto: &SourcesConfigDto) -> Result { let mut inputs = Vec::>::new(); let mut batch_files = Vec::::new(); + let mut input_names = HashSet::new(); for input_dto in &dto.inputs { let mut input = ConfigInput::from(input_dto); @@ -57,11 +58,18 @@ impl TryFrom<&SourcesConfigDto> for SourcesConfig { if let Some(path) = input.prepare()? { batch_files.push(path); } + input_names.insert(input.name.clone()); inputs.push(Arc::new(input)); } let mut sources = Vec::new(); for source_dto in &dto.sources { + // Validate that all input references exist + for input_name in &source_dto.inputs { + if !input_names.contains(input_name) { + return create_tuliprox_error_result!(TuliproxErrorKind::Info, "Source references unknown input: {input_name}"); + } + } sources.push(ConfigSource::from_dto(source_dto)?); } diff --git a/backend/src/processing/input_cache.rs b/backend/src/processing/input_cache.rs index 75a101e86..410f75145 100644 --- a/backend/src/processing/input_cache.rs +++ b/backend/src/processing/input_cache.rs @@ -29,13 +29,14 @@ pub struct InputStatus { } pub fn resolve_input_storage_path(working_dir: &str, input_name: &str) -> PathBuf { - match get_input_storage_path(input_name, working_dir) { - Ok(path) => path, - Err(_) => Path::new(working_dir).join("data").join(format!("input_{input_name}")), // Fallback + if let Ok(path) = get_input_storage_path(input_name, working_dir) { path } else { + let sanitized_name: String = input_name.chars() + .map(|c| if c.is_alphanumeric() { c } else { '_' }) + .collect(); + Path::new(working_dir).join("data").join(format!("input_{sanitized_name}")) } } - pub fn load_input_status(path: &Path) -> InputStatus { let status_path = path.join(STATUS_FILE); if status_path.exists() { diff --git a/backend/src/repository/playlist_repository.rs b/backend/src/repository/playlist_repository.rs index 7e8690180..48943ae4e 100644 --- a/backend/src/repository/playlist_repository.rs +++ b/backend/src/repository/playlist_repository.rs @@ -1,6 +1,6 @@ use crate::api::model::{AppState, PlaylistM3uStorage, PlaylistStorage, PlaylistStorageState, PlaylistXtreamStorage}; -use crate::model::{AppConfig, ConfigInput, ConfigTarget, TargetOutput}; use crate::model::Epg; +use crate::model::{AppConfig, ConfigInput, ConfigTarget, TargetOutput}; use crate::processing::processor::playlist::apply_filter_to_playlist; use crate::repository::bplustree::{BPlusTree, BPlusTreeQuery}; use crate::repository::epg_repository::epg_write; @@ -10,17 +10,17 @@ use crate::repository::strm_repository::write_strm_playlist; use crate::repository::target_id_mapping::{TargetIdMapping, VirtualIdRecord}; use crate::repository::xtream_repository::{load_input_xtream_playlist, persist_input_xtream_playlist, xtream_get_file_path, xtream_get_storage_path, xtream_write_playlist}; use crate::utils; +use crate::utils::json_write_documents_to_file; use log::info; use shared::create_tuliprox_error; use shared::error::TuliproxError; use shared::error::{info_err, TuliproxErrorKind}; +use shared::model::xtream_const::XTREAM_CLUSTER; use shared::model::{InputType, M3uPlaylistItem, PlaylistEntry, PlaylistGroup, PlaylistItem, PlaylistItemHeader, PlaylistItemType, StreamProperties, XtreamCluster, XtreamPlaylistItem}; use shared::utils::{is_dash_url, is_hls_url}; use std::collections::HashMap; -use std::path::{Path}; +use std::path::Path; use std::sync::Arc; -use shared::model::xtream_const::XTREAM_CLUSTER; -use crate::utils::json_write_documents_to_file; struct LocalEpisodeKey { path: String, @@ -191,7 +191,8 @@ fn rewrite_local_series_info_episode_virtual_id(pli: &mut PlaylistItem, local_li #[allow(clippy::implicit_hasher)] pub fn rewrite_provider_series_info_episode_virtual_id

(pli: &mut P, provider_series: &HashMap>) -where P: PlaylistEntry +where + P: PlaylistEntry, { if let Some(episode_keys) = provider_series.get(&pli.get_uuid().to_string()) { if let Some(StreamProperties::Series(series)) = pli.get_additional_properties_mut() { @@ -349,20 +350,30 @@ pub async fn persist_input_playlist(app_config: &Arc, input: &ConfigI } _ => { + // TODO what is written and why not as BPlusTree? + // Persist M3U/Other types let working_dir = &app_config.config.load().working_dir; - let storage_path = match get_input_storage_path(&input.name, working_dir) { + let storage_path = match get_input_storage_path(&input.name, working_dir) { Ok(storage_path) => storage_path, Err(err) => { return (playlist, Some(create_tuliprox_error!( TuliproxErrorKind::Info, "Error creating input storage directory for input '{}' failed: {err}", input.name))); } }; - let file_path = storage_path.join("playlist.json"); - match json_write_documents_to_file(&file_path, &playlist) { - Ok(()) => (playlist, None), + let sanitized_input_name: String = input.name.chars() + .map(|c| if c.is_alphanumeric() { c } else { '_' }) + .collect(); + let file_path = storage_path.join(format!("{sanitized_input_name}_playlist.json")); + let file_path_clone = file_path.clone(); + let playlist_clone = playlist.clone(); + match tokio::task::spawn_blocking(move || { + json_write_documents_to_file(&file_path_clone, &playlist_clone) + }).await { + Ok(Ok(())) => (playlist, None), + Ok(Err(e)) => (playlist, Some(info_err!(format!("Failed to persist input playlist: {e}")))), Err(e) => (playlist, Some(info_err!(format!("Failed to persist input playlist: {e}")))), } - }, + } }; (result, err) @@ -375,26 +386,26 @@ pub async fn load_input_playlist(app_config: &Arc, input: &ConfigInpu match input.input_type { InputType::Xtream | InputType::XtreamBatch => { - let clusters_to_load = if let Some(c) = clusters { - c - } else { - &XTREAM_CLUSTER - }; - - load_input_xtream_playlist(app_config, &storage_path, clusters_to_load).await + let clusters_to_load = if let Some(c) = clusters { + c + } else { + &XTREAM_CLUSTER + }; + + load_input_xtream_playlist(app_config, &storage_path, clusters_to_load).await } _ => { - // Load JSON for M3U - let file_path = storage_path.join("playlist.json"); - if file_path.exists() { - let content = tokio::fs::read_to_string(&file_path).await - .map_err(|e| info_err!(format!("Failed to read input playlist cache: {e}")))?; - let playlist: Vec = serde_json::from_str(&content) - .map_err(|e| info_err!(format!("Failed to parse input playlist cache: {e}")))?; - Ok(playlist) - } else { - Ok(vec![]) - } + // Load JSON for M3U + let file_path = storage_path.join("playlist.json"); + if file_path.exists() { + let content = tokio::fs::read_to_string(&file_path).await + .map_err(|e| info_err!(format!("Failed to read input playlist cache: {e}")))?; + let playlist: Vec = serde_json::from_str(&content) + .map_err(|e| info_err!(format!("Failed to parse input playlist cache: {e}")))?; + Ok(playlist) + } else { + Ok(vec![]) + } } } } \ No newline at end of file diff --git a/frontend/scss/_theme.scss b/frontend/scss/_theme.scss index cf08ef4c0..b67bbe8f5 100644 --- a/frontend/scss/_theme.scss +++ b/frontend/scss/_theme.scss @@ -648,5 +648,8 @@ body[data-theme="bright"] { --source-editor-line-color: #000000; --source-editor-pending-line-color: yellow; + + --tooltip-background-color: rgba(250, 252, 255, 0.95); + --tooltip-color: #0a0b10; } \ No newline at end of file diff --git a/shared/src/model/config/input.rs b/shared/src/model/config/input.rs index 609a5e51d..b3d047403 100644 --- a/shared/src/model/config/input.rs +++ b/shared/src/model/config/input.rs @@ -302,7 +302,7 @@ pub struct ConfigInputDto { pub options: Option, #[serde(default, skip_serializing_if = "is_blank_optional_string")] pub cache_duration: Option, - #[serde(skip, skip_serializing_if = "is_zero_u16")] + #[serde(skip)] pub cache_duration_seconds: u64, #[serde(default, skip_serializing_if = "Option::is_none", serialize_with = "serialize_option_vec_flow_map_items")] pub aliases: Option>, diff --git a/shared/src/model/config/messaging.rs b/shared/src/model/config/messaging.rs index d63d1abd9..1c593aaed 100644 --- a/shared/src/model/config/messaging.rs +++ b/shared/src/model/config/messaging.rs @@ -5,7 +5,7 @@ use crate::utils::{is_false, is_blank_optional_string, is_blank_optional_str}; #[serde(deny_unknown_fields)] pub struct TelegramMessagingConfigDto { pub bot_token: String, - #[serde(skip_serializing_if = "Vec::is_empty")] + #[serde(default, skip_serializing_if = "Vec::is_empty")] pub chat_ids: Vec, #[serde(default, skip_serializing_if = "is_false")] pub markdown: bool,