mirror of
https://github.com/euzu/tuliprox.git
synced 2026-10-20 06:42:20 +02:00
TMDB retry fixed and added cooldown. (#613)
New Features Background probing for Live streams with configurable interval; new input options to trigger probing and toggle background resolution. Configuration Changes metadata_update reorganized into grouped sections: log, resolve, probe, ffprobe, tmdb. FFprobe controls moved under ffprobe.* and now require explicit time/size units; new live ffprobe settings. Added metadata_update.tmdb.cooldown (default 7d) and grouped TMDB settings. Bug Fixes TMDB no-match cooldown to prevent endless requeues. Documentation README and UI help updated to reflect grouped metadata_update fields and explanations.
This commit is contained in:
@@ -3,6 +3,7 @@ use crate::model::ConfigInput;
|
||||
use crate::model::{AppConfig};
|
||||
use crate::repository::{get_input_m3u_playlist_file_path, get_input_storage_path, get_input_local_library_playlist_file_path, xtream_get_file_path, BPlusTreeUpdate};
|
||||
use crate::utils::{debug_if_enabled, ffmpeg};
|
||||
use crate::utils::ffmpeg::{ProbeFailureKind, ProbeUrlOutcome};
|
||||
use log::{info, warn};
|
||||
use shared::error::TuliproxError;
|
||||
use shared::model::{EpisodeStreamProperties, InputType, PlaylistItemType, StreamProperties, VideoStreamDetailProperties, VideoStreamProperties, LiveStreamProperties, M3uPlaylistItem, XtreamCluster, XtreamPlaylistItem};
|
||||
@@ -98,17 +99,17 @@ pub async fn update_generic_stream_metadata(
|
||||
let probe_url = stream_url.to_string();
|
||||
let config = app_config.config.load();
|
||||
let metadata_update = config.metadata_update.clone().unwrap_or_default();
|
||||
let ffprobe_timeout = metadata_update.ffprobe_timeout.unwrap_or(60);
|
||||
let ffprobe_timeout = metadata_update.ffprobe.timeout.unwrap_or(60);
|
||||
let user_agent = config.default_user_agent.clone();
|
||||
let (analyze_duration, probe_size) = if item_type.is_live() {
|
||||
(
|
||||
metadata_update.ffprobe_live_analyze_duration_micros,
|
||||
metadata_update.ffprobe_live_probe_size_bytes,
|
||||
metadata_update.ffprobe.live_analyze_duration_micros,
|
||||
metadata_update.ffprobe.live_probe_size_bytes,
|
||||
)
|
||||
} else {
|
||||
(
|
||||
metadata_update.ffprobe_analyze_duration_micros,
|
||||
metadata_update.ffprobe_probe_size_bytes,
|
||||
metadata_update.ffprobe.analyze_duration_micros,
|
||||
metadata_update.ffprobe.probe_size_bytes,
|
||||
)
|
||||
};
|
||||
|
||||
@@ -126,9 +127,16 @@ pub async fn update_generic_stream_metadata(
|
||||
active_provider.release_handle(&handle).await;
|
||||
}
|
||||
|
||||
let Some((_quality, raw_video, raw_audio)) = probe_data else {
|
||||
warn!("Probe failed or timed out for generic stream: {unique_id}");
|
||||
return Ok(GenericProbeOutcome::ProbeFailed);
|
||||
let (raw_video, raw_audio) = match probe_data {
|
||||
ProbeUrlOutcome::Success(_quality, raw_video, raw_audio) => (raw_video, raw_audio),
|
||||
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}"));
|
||||
}
|
||||
ProbeUrlOutcome::Failed(ProbeFailureKind::Other) => {
|
||||
warn!("Probe failed or timed out for generic stream: {unique_id}");
|
||||
return Ok(GenericProbeOutcome::ProbeFailed);
|
||||
}
|
||||
};
|
||||
|
||||
// Hold the async file lock while the blocking DB update runs in a blocking thread.
|
||||
|
||||
@@ -5,6 +5,7 @@ use crate::model::{AppConfig, ConfigInput, ConfigInputFlags};
|
||||
use shared::model::{LiveStreamProperties, StreamProperties, XtreamCluster, XtreamPlaylistItem};
|
||||
use crate::repository::{get_input_storage_path, persist_input_live_info, BPlusTreeQuery, xtream_get_file_path};
|
||||
use crate::utils::{debug_if_enabled};
|
||||
use crate::utils::ffmpeg::{ProbeFailureKind, ProbeUrlOutcome};
|
||||
use log::{debug, warn};
|
||||
use crate::processing::parser::xtream::create_xtream_url;
|
||||
use crate::api::model::{ActiveProviderManager, ProviderHandle, ProviderIdType};
|
||||
@@ -117,10 +118,10 @@ pub async fn update_live_stream_metadata(
|
||||
);
|
||||
let config = app_config.config.load();
|
||||
let metadata_update = config.metadata_update.clone().unwrap_or_default();
|
||||
let ffprobe_timeout = metadata_update.ffprobe_timeout.unwrap_or(60);
|
||||
let ffprobe_timeout = metadata_update.ffprobe.timeout.unwrap_or(60);
|
||||
let user_agent = config.default_user_agent.clone();
|
||||
let analyze_duration = metadata_update.ffprobe_live_analyze_duration_micros;
|
||||
let probe_size = metadata_update.ffprobe_live_probe_size_bytes;
|
||||
let analyze_duration = metadata_update.ffprobe.live_analyze_duration_micros;
|
||||
let probe_size = metadata_update.ffprobe.live_probe_size_bytes;
|
||||
|
||||
let display_id = stream_id_opt.map_or_else(|| "StringID".to_string(), |v| v.to_string());
|
||||
debug!("Probing Live Stream ID {} for input {}", display_id, input.name);
|
||||
@@ -131,27 +132,37 @@ pub async fn update_live_stream_metadata(
|
||||
properties.last_probed_timestamp = Some(now);
|
||||
|
||||
let mut success = false;
|
||||
if let Some((_quality, raw_video, raw_audio)) = crate::utils::ffmpeg::probe_url(
|
||||
let mut not_found = false;
|
||||
match crate::utils::ffmpeg::probe_url(
|
||||
&stream_url,
|
||||
user_agent.as_deref(),
|
||||
analyze_duration,
|
||||
probe_size,
|
||||
ffprobe_timeout,
|
||||
).await {
|
||||
// 3. Update properties on success
|
||||
if let Some(v) = raw_video {
|
||||
properties.video = Some(v.to_string().into());
|
||||
)
|
||||
.await
|
||||
{
|
||||
ProbeUrlOutcome::Success(_quality, raw_video, raw_audio) => {
|
||||
// 3. Update properties on success
|
||||
if let Some(v) = raw_video {
|
||||
properties.video = Some(v.to_string().into());
|
||||
}
|
||||
if let Some(a) = raw_audio {
|
||||
properties.audio = Some(a.to_string().into());
|
||||
}
|
||||
properties.last_success_timestamp = Some(now);
|
||||
success = true;
|
||||
|
||||
debug_if_enabled!("Successfully probed Live Stream ID {}", display_id);
|
||||
}
|
||||
if let Some(a) = raw_audio {
|
||||
properties.audio = Some(a.to_string().into());
|
||||
ProbeUrlOutcome::Failed(ProbeFailureKind::NotFound) => {
|
||||
warn!("Live stream probe target returned 404 for ID {} (Input: {})", display_id, input.name);
|
||||
not_found = true;
|
||||
}
|
||||
ProbeUrlOutcome::Failed(ProbeFailureKind::Other) => {
|
||||
warn!("Probe failed for Live Stream ID {} (Input: {})", display_id, input.name);
|
||||
// We still persist the updated last_probed_timestamp so we don't retry immediately
|
||||
}
|
||||
properties.last_success_timestamp = Some(now);
|
||||
success = true;
|
||||
|
||||
debug_if_enabled!("Successfully probed Live Stream ID {}", display_id);
|
||||
} else {
|
||||
warn!("Probe failed for Live Stream ID {} (Input: {})", display_id, input.name);
|
||||
// We still persist the updated last_probed_timestamp so we don't retry immediately
|
||||
}
|
||||
|
||||
// 4. Persist
|
||||
@@ -164,6 +175,9 @@ pub async fn update_live_stream_metadata(
|
||||
}
|
||||
|
||||
if !success {
|
||||
if not_found {
|
||||
return Err(shared::error::info_err!("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}"));
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::api::model::UpdateTask;
|
||||
use crate::api::model::{ActiveProviderManager, ProviderHandle, ProviderIdType, ResolveReason, ResolveReasonSet};
|
||||
use crate::library::MetadataResolver;
|
||||
use crate::library::{MetadataResolver, MetadataStorage};
|
||||
use crate::model::FetchedPlaylist;
|
||||
use crate::model::{AppConfig, ConfigTarget, MetadataUpdateConfig};
|
||||
use crate::model::{ConfigInput, ConfigInputFlags, InputSource};
|
||||
@@ -18,6 +18,7 @@ use crate::repository::{
|
||||
get_input_storage_path, persist_input_series_info_batch, MemoryPlaylistSource, PlaylistSource,
|
||||
};
|
||||
use crate::repository::{xtream_get_file_path, BPlusTreeQuery};
|
||||
use crate::utils::ffmpeg::{ProbeFailureKind, ProbeUrlOutcome};
|
||||
use crate::utils::{debug_if_enabled, xtream};
|
||||
use log::{debug, error, info, log_enabled, trace, warn, Level};
|
||||
use parking_lot::Mutex;
|
||||
@@ -45,11 +46,11 @@ impl SeriesProbeSettings {
|
||||
pub fn from_metadata_update(metadata_update: Option<&MetadataUpdateConfig>) -> Self {
|
||||
let defaults = MetadataUpdateConfig::default();
|
||||
Self {
|
||||
timeout_secs: metadata_update.and_then(|cfg| cfg.ffprobe_timeout).unwrap_or(60),
|
||||
timeout_secs: metadata_update.and_then(|cfg| cfg.ffprobe.timeout).unwrap_or(60),
|
||||
analyze_duration_micros: metadata_update
|
||||
.map_or(defaults.ffprobe_analyze_duration_micros, |cfg| cfg.ffprobe_analyze_duration_micros),
|
||||
.map_or(defaults.ffprobe.analyze_duration_micros, |cfg| cfg.ffprobe.analyze_duration_micros),
|
||||
probe_size_bytes: metadata_update
|
||||
.map_or(defaults.ffprobe_probe_size_bytes, |cfg| cfg.ffprobe_probe_size_bytes),
|
||||
.map_or(defaults.ffprobe.probe_size_bytes, |cfg| cfg.ffprobe.probe_size_bytes),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -695,6 +696,7 @@ pub async fn update_series_metadata(
|
||||
|
||||
let mut fetched_new = false;
|
||||
let mut properties_updated = false;
|
||||
let mut probe_failure: Option<ProbeFailureKind> = None;
|
||||
|
||||
let display_id = series_id_opt.map_or_else(|| "StringID".to_string(), |v| v.to_string());
|
||||
|
||||
@@ -759,7 +761,12 @@ pub async fn update_series_metadata(
|
||||
{
|
||||
let config = app_config.config.load();
|
||||
let library_config = config.library.as_ref();
|
||||
let meta_resolver = MetadataResolver::new(library_config, client.clone());
|
||||
let metadata_update_config = config.metadata_update.as_ref();
|
||||
let tmdb_storage = metadata_update_config
|
||||
.filter(|cfg| cfg.tmdb.enabled)
|
||||
.map(|_| MetadataStorage::new(storage_path.clone()));
|
||||
let meta_resolver =
|
||||
MetadataResolver::from_config(library_config, metadata_update_config, client.clone(), tmdb_storage);
|
||||
|
||||
let mut meta = None;
|
||||
let mut tried_title = false;
|
||||
@@ -856,7 +863,7 @@ pub async fn update_series_metadata(
|
||||
ep.title, ep.season, ep.episode_num, missing_reason
|
||||
);
|
||||
|
||||
if let Some((_quality, raw_video, raw_audio)) = crate::utils::ffmpeg::probe_url(
|
||||
match crate::utils::ffmpeg::probe_url(
|
||||
&episode_url,
|
||||
user_agent.as_deref(),
|
||||
probe_settings.analyze_duration_micros,
|
||||
@@ -865,15 +872,25 @@ pub async fn update_series_metadata(
|
||||
)
|
||||
.await
|
||||
{
|
||||
if let Some(v) = raw_video {
|
||||
ep.video = Some(v.to_string().into());
|
||||
properties_updated = true;
|
||||
ProbeUrlOutcome::Success(_quality, raw_video, raw_audio) => {
|
||||
if let Some(v) = raw_video {
|
||||
ep.video = Some(v.to_string().into());
|
||||
properties_updated = true;
|
||||
}
|
||||
if let Some(a) = raw_audio {
|
||||
ep.audio = Some(a.to_string().into());
|
||||
properties_updated = true;
|
||||
}
|
||||
probed_count += 1;
|
||||
}
|
||||
if let Some(a) = raw_audio {
|
||||
ep.audio = Some(a.to_string().into());
|
||||
properties_updated = true;
|
||||
ProbeUrlOutcome::Failed(ProbeFailureKind::NotFound) => {
|
||||
probe_failure = Some(ProbeFailureKind::NotFound);
|
||||
}
|
||||
ProbeUrlOutcome::Failed(ProbeFailureKind::Other) => {
|
||||
if probe_failure.is_none() {
|
||||
probe_failure = Some(ProbeFailureKind::Other);
|
||||
}
|
||||
}
|
||||
probed_count += 1;
|
||||
}
|
||||
|
||||
if let Some(h) = temp_handle {
|
||||
@@ -897,6 +914,19 @@ pub async fn update_series_metadata(
|
||||
}
|
||||
}
|
||||
|
||||
let probe_only_unresolved = do_probe && !fetch_info && !resolve_tmdb && !properties_updated && !fetched_new;
|
||||
if probe_only_unresolved {
|
||||
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}")
|
||||
}
|
||||
ProbeFailureKind::Other => shared::error::info_err!("Probe failed for Series {display_id}"),
|
||||
};
|
||||
return Err(err);
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Persist
|
||||
if properties_updated || fetched_new {
|
||||
if save {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::api::model::{ActiveProviderManager, ProviderHandle};
|
||||
use crate::api::model::{ProviderIdType, ResolveReason, ResolveReasonSet, UpdateTask};
|
||||
use crate::library::MetadataResolver;
|
||||
use crate::library::{MetadataResolver, MetadataStorage};
|
||||
use crate::model::FetchedPlaylist;
|
||||
use crate::model::InputSource;
|
||||
use crate::model::{AppConfig, ConfigTarget};
|
||||
@@ -16,6 +16,7 @@ use crate::repository::get_input_storage_path;
|
||||
use crate::repository::persist_input_vod_info;
|
||||
use crate::repository::persist_input_vod_info_batch;
|
||||
use crate::repository::{xtream_get_file_path, BPlusTreeQuery};
|
||||
use crate::utils::ffmpeg::{ProbeFailureKind, ProbeUrlOutcome};
|
||||
use crate::utils::{debug_if_enabled, trace_if_enabled, xtream};
|
||||
use log::{debug, error, info, log_enabled, trace, warn, Level};
|
||||
use parking_lot::Mutex;
|
||||
@@ -577,6 +578,7 @@ pub async fn update_vod_metadata(
|
||||
|
||||
let mut fetched_new = false;
|
||||
let mut properties_updated = false;
|
||||
let mut probe_failure: Option<ProbeFailureKind> = None;
|
||||
|
||||
// Determine the title to use for logging and fallback.
|
||||
// Cloning here to satisfy borrow checker when 'props' is mutated later.
|
||||
@@ -676,7 +678,12 @@ pub async fn update_vod_metadata(
|
||||
if missing_tmdb || still_missing_date {
|
||||
let config = app_config.config.load();
|
||||
let library_config = config.library.as_ref();
|
||||
let meta_resolver = MetadataResolver::new(library_config, client.clone());
|
||||
let metadata_update_config = config.metadata_update.as_ref();
|
||||
let tmdb_storage = metadata_update_config
|
||||
.filter(|cfg| cfg.tmdb.enabled)
|
||||
.map(|_| MetadataStorage::new(storage_path.clone()));
|
||||
let meta_resolver =
|
||||
MetadataResolver::from_config(library_config, metadata_update_config, client.clone(), tmdb_storage);
|
||||
|
||||
let mut meta = None;
|
||||
let mut tried_title = false;
|
||||
@@ -767,10 +774,10 @@ pub async fn update_vod_metadata(
|
||||
|
||||
let config = app_config.config.load();
|
||||
let metadata_update = config.metadata_update.clone().unwrap_or_default();
|
||||
let ffprobe_timeout = metadata_update.ffprobe_timeout.unwrap_or(60);
|
||||
let ffprobe_timeout = metadata_update.ffprobe.timeout.unwrap_or(60);
|
||||
let user_agent = config.default_user_agent.clone();
|
||||
let analyze_duration = metadata_update.ffprobe_analyze_duration_micros;
|
||||
let probe_size = metadata_update.ffprobe_probe_size_bytes;
|
||||
let analyze_duration = metadata_update.ffprobe.analyze_duration_micros;
|
||||
let probe_size = metadata_update.ffprobe.probe_size_bytes;
|
||||
|
||||
// Acquire Connection logic
|
||||
let temp_handle = if active_handle.is_some() {
|
||||
@@ -779,28 +786,38 @@ pub async fn update_vod_metadata(
|
||||
active_provider.acquire_connection_for_probe(&input.name).await
|
||||
};
|
||||
|
||||
if active_handle.is_some() || temp_handle.is_some() {
|
||||
debug_if_enabled!("Probing VOD '{}' (ID: {})", display_title, display_id);
|
||||
if let Some((_quality, raw_video, raw_audio)) = crate::utils::ffmpeg::probe_url(
|
||||
&stream_url,
|
||||
user_agent.as_deref(),
|
||||
analyze_duration,
|
||||
probe_size,
|
||||
ffprobe_timeout,
|
||||
)
|
||||
.await
|
||||
{
|
||||
if let Some(details) = properties.details.as_mut() {
|
||||
if let Some(v) = raw_video {
|
||||
details.video = Some(v.to_string().into());
|
||||
properties_updated = true;
|
||||
if active_handle.is_some() || temp_handle.is_some() {
|
||||
debug_if_enabled!("Probing VOD '{}' (ID: {})", display_title, display_id);
|
||||
match crate::utils::ffmpeg::probe_url(
|
||||
&stream_url,
|
||||
user_agent.as_deref(),
|
||||
analyze_duration,
|
||||
probe_size,
|
||||
ffprobe_timeout,
|
||||
)
|
||||
.await
|
||||
{
|
||||
ProbeUrlOutcome::Success(_quality, raw_video, raw_audio) => {
|
||||
if let Some(details) = properties.details.as_mut() {
|
||||
if let Some(v) = raw_video {
|
||||
details.video = Some(v.to_string().into());
|
||||
properties_updated = true;
|
||||
}
|
||||
if let Some(a) = raw_audio {
|
||||
details.audio = Some(a.to_string().into());
|
||||
properties_updated = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(a) = raw_audio {
|
||||
details.audio = Some(a.to_string().into());
|
||||
properties_updated = true;
|
||||
ProbeUrlOutcome::Failed(ProbeFailureKind::NotFound) => {
|
||||
probe_failure = Some(ProbeFailureKind::NotFound);
|
||||
}
|
||||
ProbeUrlOutcome::Failed(ProbeFailureKind::Other) => {
|
||||
if probe_failure.is_none() {
|
||||
probe_failure = Some(ProbeFailureKind::Other);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(h) = temp_handle {
|
||||
active_provider.release_handle(&h).await;
|
||||
}
|
||||
@@ -810,6 +827,19 @@ pub async fn update_vod_metadata(
|
||||
}
|
||||
}
|
||||
|
||||
let probe_only_unresolved = do_probe && !fetch_info && !resolve_tmdb && !properties_updated && !fetched_new;
|
||||
if probe_only_unresolved {
|
||||
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}")
|
||||
}
|
||||
ProbeFailureKind::Other => shared::error::info_err!("Probe failed for VOD {display_id}"),
|
||||
};
|
||||
return Err(err);
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Persist if updated
|
||||
if properties_updated || fetched_new {
|
||||
if save {
|
||||
|
||||
Reference in New Issue
Block a user