Feature/structured errors landing page (#705)

- structured errors
- landing page
- fix: table header background
This commit is contained in:
euzu
2026-04-06 10:20:52 +02:00
committed by GitHub
parent 1b148d29ae
commit 31a48bc1f3
143 changed files with 1499 additions and 1101 deletions
+5 -5
View File
@@ -35,7 +35,7 @@ use indexmap::IndexMap;
use log::{debug, error, info, log_enabled, warn, Level};
use shared::{
concat_string,
error::{get_errors_notify_message, notify_err, TuliproxError},
error::{get_errors_notify_message, TuliproxError},
foundation::{get_field_value, set_field_value, Filter, ValueAccessor, ValueProvider},
model::{
ClusterSource, CounterModifier, FieldGetAccessor, FieldSetAccessor, InputStats, InputType, ItemField,
@@ -596,7 +596,7 @@ async fn process_source(
if playlist_groups.is_empty() {
broadcast_step("Playlist download", &format!("Input '{}' playlist is empty", input.name));
info!("Source is empty {input_name}");
errors.push(notify_err!("Source is empty {input_name}"));
errors.push(TuliproxError::RepositoryPlaylist(format!("Source is empty {input_name}")));
} else {
source_playlists.push(FetchedPlaylist { input, source: playlist_groups, epg: tvguide });
log_memory_snapshot(
@@ -621,10 +621,10 @@ async fn process_source(
if source_downloaded {
if source_playlists.is_empty() {
debug!("Source at index {source_idx} is empty");
errors.push(notify_err!(
errors.push(TuliproxError::RepositoryPlaylist(format!(
"Source at index {source_idx} is empty: {}",
source.inputs.iter().map(Clone::clone).collect::<Vec<Arc<str>>>().join(", ")
));
)));
} else {
debug_if_enabled!(
"Source has {} groups",
@@ -1542,7 +1542,7 @@ pub async fn exec_processing(
// log errors
for err in &errors {
error!("{}", err.message);
error!("{}", err.message());
}
if !stats.is_empty() {
@@ -52,7 +52,7 @@ pub async fn update_generic_stream_metadata(
// Determine storage file path based on input type
let storage_path = get_input_storage_path(&input.name, storage_dir).await
.map_err(|e| shared::error::info_err!("Storage path error: {e}"))?;
.map_err(|e| TuliproxError::Io(format!("Storage path error: {e}")))?;
let (db_path, storage_kind) = match input.input_type {
InputType::M3u | InputType::M3uBatch => (
@@ -82,7 +82,11 @@ pub async fn update_generic_stream_metadata(
};
if !db_path.exists() {
return Err(shared::error::info_err!("Playlist DB file not found for input {}: {}", input.name, db_path.display()));
return Err(shared::error::TuliproxError::Config(format!(
"Playlist DB file not found for input {input_name}: {db_path_display}",
input_name = input.name,
db_path_display = db_path.display()
)));
}
let needs_provider_connection = requires_provider_connection_for_generic_probe(input.input_type);
@@ -98,7 +102,7 @@ pub async fn update_generic_stream_metadata(
if needs_provider_connection && active_handle.is_none() && acquired_handle.is_none() {
warn!("Skipping probe for generic stream {unique_id} due to connection limits");
return Err(shared::error::info_err!("No connection available"));
return Err(TuliproxError::Probe(format!("Skipping probe for generic stream {unique_id} due to connection limits")));
}
let probe_url = stream_url.to_string();
@@ -143,7 +147,7 @@ pub async fn update_generic_stream_metadata(
ProbeUrlOutcome::Success(_quality, raw_video, raw_audio, stats) => (raw_video, raw_audio, stats),
ProbeUrlOutcome::Failed(ProbeFailureKind::NotFound) => {
warn!("Probe target not found (404) for generic stream: {unique_id}");
return Err(shared::error::info_err!("Probe target returned 404 Not Found for stream {unique_id}"));
return Err(shared::error::TuliproxError::Probe(format!("Probe target returned 404 Not Found for stream {unique_id}")));
}
ProbeUrlOutcome::Failed(ProbeFailureKind::Other) => {
warn!("Probe failed or timed out for generic stream: {unique_id}");
@@ -246,8 +250,8 @@ pub async fn update_generic_stream_metadata(
Ok(updated)
})
.await
.map_err(|e| shared::error::info_err!("Failed to join generic probe DB update task: {e}"))?
.map_err(|e| shared::error::info_err!("{e}"))?;
.map_err(|e| shared::error::TuliproxError::Config(format!("Failed to join generic probe DB update task: {e}")))?
.map_err(shared::error::TuliproxError::Config)?;
drop(file_lock);
if updated {
+1 -1
View File
@@ -250,7 +250,7 @@ impl TraktCategoriesProcessor {
}
}
Err(err) => {
warn!("Failed to fetch Trakt list {cache_key}: {}", err.message);
warn!("Failed to fetch Trakt list {cache_key}: {}", err.message());
}
}
}
+4 -4
View File
@@ -23,7 +23,7 @@ pub async fn update_live_stream_metadata(
) -> Result<Option<LiveStreamProperties>, TuliproxError> {
let storage_dir = &app_config.config.load().storage_dir;
let storage_path = get_input_storage_path(&input.name, storage_dir).await
.map_err(|e| shared::error::info_err!("Storage path error: {e}"))?;
.map_err(|e| TuliproxError::Io(format!("Storage path error: {e}")))?;
// Try to load existing info first to preserve data
let mut props: Option<LiveStreamProperties> = None;
@@ -175,16 +175,16 @@ pub async fn update_live_stream_metadata(
if let Some(stream_id) = stream_id_opt {
persist_input_live_info(app_config, &storage_path, XtreamCluster::Live, &input.name, stream_id, &properties)
.await
.map_err(|e| shared::error::info_err!("Persist error: {e}"))?;
.map_err(|e| shared::error::TuliproxError::Io(format!("Persist error: {e}")))?;
}
}
if !success {
if not_found {
return Err(shared::error::info_err!("Probe failed with 404 Not Found for stream {display_id}"));
return Err(shared::error::TuliproxError::Probe(format!("Probe failed with 404 Not Found for stream {display_id}")));
}
// Return error to propagate failure up to task manager/logs
return Err(shared::error::info_err!("Probe failed for stream {display_id}"));
return Err(shared::error::TuliproxError::Probe(format!("Probe failed for stream {display_id}")));
}
Ok(Some(properties))
@@ -649,7 +649,7 @@ pub async fn update_series_metadata(
let storage_dir = &app_config.config.load().storage_dir;
let storage_path = get_input_storage_path(&input.name, storage_dir)
.await
.map_err(|e| shared::error::info_err!("Storage path error: {e}"))?;
.map_err(|e| TuliproxError::Io(format!("Storage path error: {e}")))?;
if input.has_flag(ConfigInputFlags::XtreamSkipSeries) {
return Ok(None);
@@ -734,12 +734,12 @@ pub async fn update_series_metadata(
if let Some(series_id) = series_id_opt {
// Fetch Info from Provider
let info_url = xtream::get_xtream_player_api_info_url(input, XtreamCluster::Series, series_id)
.ok_or_else(|| shared::error::info_err!("Failed to build info URL"))?;
.ok_or_else(|| shared::error::TuliproxError::Config("Failed to build info URL".to_string()))?;
let input_source = InputSource::from(input).with_url(info_url);
let content = xtream::get_xtream_stream_info_content(app_config, client, &input_source, true)
.await
.map_err(|e| shared::error::info_err!("{e}"))?;
.map_err(|e| shared::error::TuliproxError::Config(format!("{e}")))?;
if content.is_empty() {
debug!("Series {display_id}: provider returned empty content for info fetch");
@@ -763,16 +763,16 @@ pub async fn update_series_metadata(
properties_updated = true;
}
Err(e) => {
return Err(shared::error::info_err!(
return Err(shared::error::TuliproxError::Config(format!(
"Series {display_id}: failed to parse XtreamSeriesInfo: {e}. Raw response: {content}"
));
)));
}
}
}
Err(e) => {
return Err(shared::error::info_err!(
return Err(shared::error::TuliproxError::Config(format!(
"Series {display_id}: failed to parse provider response as JSON: {e}. Raw response: {content}"
));
)));
}
}
}
@@ -789,12 +789,12 @@ pub async fn update_series_metadata(
};
props = Some(new_props);
} else {
return Err(shared::error::info_err!("No Series properties available and no title found for {display_id}"));
return Err(shared::error::TuliproxError::Config(format!("No Series properties available and no title found for {display_id}")));
}
}
let mut properties = props.ok_or_else(|| {
shared::error::info_err!("No Series properties available after fallback creation for {display_id}")
shared::error::TuliproxError::Config(format!("No Series properties available after fallback creation for {display_id}"))
})?;
let resolve_tmdb_enabled = input.has_flag(ConfigInputFlags::ResolveTmdb);
@@ -994,10 +994,10 @@ pub async fn update_series_metadata(
if let Some(kind) = probe_failure {
let err = match kind {
ProbeFailureKind::NotFound => {
shared::error::info_err!("Probe failed with 404 Not Found for Series {display_id}")
shared::error::TuliproxError::Config(format!("Probe failed with 404 Not Found for Series {display_id}"))
}
ProbeFailureKind::Other => shared::error::info_err!("Probe failed for Series {display_id}"),
ProbeFailureKind::Cancelled => shared::error::info_err!("Probe cancelled for Series {display_id}"),
ProbeFailureKind::Other => shared::error::TuliproxError::Config(format!("Probe failed for Series {display_id}")),
ProbeFailureKind::Cancelled => shared::error::TuliproxError::Config(format!("Probe cancelled for Series {display_id}")),
};
return Err(err);
}
@@ -1016,7 +1016,7 @@ pub async fn update_series_metadata(
&properties,
)
.await
.map_err(|e| shared::error::info_err!("Persist error: {e}"))?;
.map_err(|e| shared::error::TuliproxError::Config(format!("Persist error: {e}")))?;
debug_if_enabled!("Successfully updated Series metadata for ID {}", series_id);
}
@@ -625,7 +625,7 @@ pub async fn update_vod_metadata(
if should_fetch_info {
if let Some(stream_id) = stream_id_opt {
let info_url = xtream::get_xtream_player_api_info_url(input, XtreamCluster::Video, stream_id)
.ok_or_else(|| shared::error::info_err!("Failed to build info URL"))?;
.ok_or_else(|| shared::error::TuliproxError::Config("Failed to build info URL".to_string()))?;
let input_source = InputSource::from(input).with_url(info_url);
@@ -670,12 +670,12 @@ pub async fn update_vod_metadata(
props = Some(new_props);
} else {
// We can't proceed without at least a name
return Err(shared::error::info_err!("No VOD properties available and no title found for {display_id}"));
return Err(shared::error::TuliproxError::Config(format!("No VOD properties available and no title found for {display_id}")));
}
}
let Some(mut properties) = props else {
return Err(shared::error::info_err!("No VOD properties available after fallback creation for {display_id}"));
return Err(shared::error::TuliproxError::Config(format!("No VOD properties available after fallback creation for {display_id}")));
};
let resolve_tmdb_enabled = input.has_flag(ConfigInputFlags::ResolveTmdb);
@@ -908,10 +908,10 @@ pub async fn update_vod_metadata(
if let Some(kind) = probe_failure {
let err = match kind {
ProbeFailureKind::NotFound => {
shared::error::info_err!("Probe failed with 404 Not Found for VOD {display_id}")
shared::error::TuliproxError::Config(format!("Probe failed with 404 Not Found for VOD {display_id}"))
}
ProbeFailureKind::Other => shared::error::info_err!("Probe failed for VOD {display_id}"),
ProbeFailureKind::Cancelled => shared::error::info_err!("Probe cancelled for VOD {display_id}"),
ProbeFailureKind::Other => shared::error::TuliproxError::Config(format!("Probe failed for VOD {display_id}")),
ProbeFailureKind::Cancelled => shared::error::TuliproxError::Config(format!("Probe cancelled for VOD {display_id}")),
};
return Err(err);
}
@@ -930,7 +930,7 @@ pub async fn update_vod_metadata(
&properties,
)
.await
.map_err(|e| shared::error::info_err!("Persist error: {e}"))?;
.map_err(|e| shared::error::TuliproxError::Config(format!("Persist error: {e}")))?;
}
debug_if_enabled!("Successfully updated VOD metadata for '{}' (ID: {})", display_title, display_id);