To align input definitions with the SourceEditor, inputs are now defined globally in the inputs section of the config file.

Each source can reference one or more inputs by their name in the inputs attribute.
This commit is contained in:
euzu
2026-01-03 19:09:39 +01:00
parent 02346cd733
commit da8947dd76
7 changed files with 59 additions and 37 deletions
+2 -3
View File
@@ -88,8 +88,7 @@ impl ConfigFile {
Self::load_mapping(app_state)
}
async fn reload_source_file(app_state: &Arc<AppState>, file: &Path) -> Result<(), TuliproxError> {
info!("Loaded sources file {}", file.display());
async fn reload_source_file(app_state: &Arc<AppState>) -> 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
}
}
}
+8
View File
@@ -50,6 +50,7 @@ impl TryFrom<&SourcesConfigDto> for SourcesConfig {
fn try_from(dto: &SourcesConfigDto) -> Result<Self, TuliproxError> {
let mut inputs = Vec::<Arc<ConfigInput>>::new();
let mut batch_files = Vec::<PathBuf>::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)?);
}
+5 -4
View File
@@ -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() {
+39 -28
View File
@@ -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<P>(pli: &mut P, provider_series: &HashMap<String, Vec<ProviderEpisodeKey>>)
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<AppConfig>, 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<AppConfig>, 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<PlaylistGroup> = 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<PlaylistGroup> = serde_json::from_str(&content)
.map_err(|e| info_err!(format!("Failed to parse input playlist cache: {e}")))?;
Ok(playlist)
} else {
Ok(vec![])
}
}
}
}
+3
View File
@@ -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;
}
+1 -1
View File
@@ -302,7 +302,7 @@ pub struct ConfigInputDto {
pub options: Option<ConfigInputOptionsDto>,
#[serde(default, skip_serializing_if = "is_blank_optional_string")]
pub cache_duration: Option<String>,
#[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<Vec<ConfigInputAliasDto>>,
+1 -1
View File
@@ -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<String>,
#[serde(default, skip_serializing_if = "is_false")]
pub markdown: bool,