mirror of
https://github.com/euzu/tuliprox.git
synced 2026-10-01 13:32:04 +02:00
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:
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)?);
|
||||
}
|
||||
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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![])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
}
|
||||
@@ -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>>,
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user