Rename date limit setting

This commit is contained in:
Arnaud_Cayrol 2026-02-10 17:39:15 +01:00
parent 0ae09da074
commit 66e7af28eb
6 changed files with 42 additions and 41 deletions

View file

@ -78,10 +78,10 @@
month: config.processing.timestamp.month, month: config.processing.timestamp.month,
day: config.processing.timestamp.day, day: config.processing.timestamp.day,
}, },
photo_limit: { time_interval: {
enabled: config.processing.photo_limit.enabled, enabled: config.processing.time_interval.enabled,
max_photos: Number(config.processing.photo_limit.max_photos), max_photos: Number(config.processing.time_interval.max_photos),
time_range: config.processing.photo_limit.time_range, time_range: config.processing.time_interval.time_range,
}, },
}, },
video: { video: {
@ -413,17 +413,17 @@
<input id="keep-intermediates" type="checkbox" bind:checked={config.processing.output.keep_intermediates} /> <input id="keep-intermediates" type="checkbox" bind:checked={config.processing.output.keep_intermediates} />
</div> </div>
<!-- Photo Limit Section --> <!-- Time Interval Section -->
<div class="setting-section"> <div class="setting-section">
<div class="section-header"> <div class="section-header">
<span class="section-title">Photo Limit</span> <span class="section-title">Time Interval</span>
<input <input
type="checkbox" type="checkbox"
bind:checked={config.processing.photo_limit.enabled} bind:checked={config.processing.time_interval.enabled}
/> />
</div> </div>
{#if config.processing.photo_limit.enabled} {#if config.processing.time_interval.enabled}
<div class="setting-row sub-setting"> <div class="setting-row sub-setting">
<label for="max-photos"> <label for="max-photos">
<span class="setting-label">Max Photos</span> <span class="setting-label">Max Photos</span>
@ -433,12 +433,12 @@
<input <input
id="max-photos" id="max-photos"
type="range" type="range"
bind:value={config.processing.photo_limit.max_photos} bind:value={config.processing.time_interval.max_photos}
min="1" min="1"
max="10" max="10"
step="1" step="1"
/> />
<span class="value">{config.processing.photo_limit.max_photos}</span> <span class="value">{config.processing.time_interval.max_photos}</span>
</div> </div>
</div> </div>
@ -447,7 +447,7 @@
<span class="setting-label">Time Range</span> <span class="setting-label">Time Range</span>
<span class="setting-hint">Group photos by this period</span> <span class="setting-hint">Group photos by this period</span>
</label> </label>
<select id="time-range" bind:value={config.processing.photo_limit.time_range}> <select id="time-range" bind:value={config.processing.time_interval.time_range}>
<option value="day">Day</option> <option value="day">Day</option>
<option value="week">Week</option> <option value="week">Week</option>
<option value="month">Month</option> <option value="month">Month</option>

View file

@ -166,7 +166,7 @@ export const DEFAULT_CONFIG = {
month: false, month: false,
day: false, day: false,
}, },
photo_limit: { time_interval: {
enabled: false, enabled: false,
max_photos: 1, max_photos: 1,
time_range: 'day', time_range: 'day',

View file

@ -455,12 +455,12 @@ pub enum TimeRange {
Month, Month,
} }
/// Photo limit configuration. /// Time interval configuration.
/// ///
/// Limits the number of successfully processed photos per time range /// Limits the number of successfully processed photos per time range
/// (day/week/month) to avoid over-representation of busy periods. /// (day/week/month) to avoid over-representation of busy periods.
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PhotoLimitConfig { pub struct TimeIntervalConfig {
/// Whether photo limiting is enabled. /// Whether photo limiting is enabled.
pub enabled: bool, pub enabled: bool,
@ -471,7 +471,7 @@ pub struct PhotoLimitConfig {
pub time_range: TimeRange, pub time_range: TimeRange,
} }
impl Default for PhotoLimitConfig { impl Default for TimeIntervalConfig {
fn default() -> Self { fn default() -> Self {
Self { Self {
enabled: false, enabled: false,
@ -481,17 +481,17 @@ impl Default for PhotoLimitConfig {
} }
} }
impl PhotoLimitConfig { impl TimeIntervalConfig {
/// Validate the configuration values. /// Validate the configuration values.
pub fn validate(&self) -> Result<()> { pub fn validate(&self) -> Result<()> {
if self.enabled && self.max_photos == 0 { if self.enabled && self.max_photos == 0 {
return Err(Error::Config( return Err(Error::Config(
"Photo limit max_photos must be greater than 0".to_string(), "Time interval max_photos must be greater than 0".to_string(),
)); ));
} }
if self.enabled && self.max_photos > 100 { if self.enabled && self.max_photos > 100 {
return Err(Error::Config( return Err(Error::Config(
"Photo limit max_photos must be at most 100".to_string(), "Time interval max_photos must be at most 100".to_string(),
)); ));
} }
Ok(()) Ok(())
@ -544,9 +544,9 @@ pub struct ProcessingConfig {
#[serde(default)] #[serde(default)]
pub timestamp: TimestampConfig, pub timestamp: TimestampConfig,
/// Photo limit settings. /// Time interval settings.
#[serde(default)] #[serde(default)]
pub photo_limit: PhotoLimitConfig, pub time_interval: TimeIntervalConfig,
} }
impl Default for ProcessingConfig { impl Default for ProcessingConfig {
@ -561,7 +561,7 @@ impl Default for ProcessingConfig {
output: OutputConfig::default(), output: OutputConfig::default(),
alignment: AlignmentConfig::default(), alignment: AlignmentConfig::default(),
timestamp: TimestampConfig::default(), timestamp: TimestampConfig::default(),
photo_limit: PhotoLimitConfig::default(), time_interval: TimeIntervalConfig::default(),
} }
} }
} }
@ -577,7 +577,7 @@ impl ProcessingConfig {
self.output.validate()?; self.output.validate()?;
self.alignment.validate()?; self.alignment.validate()?;
self.timestamp.validate()?; self.timestamp.validate()?;
self.photo_limit.validate()?; self.time_interval.validate()?;
Ok(()) Ok(())
} }
} }

View file

@ -8,7 +8,7 @@
mod processing; mod processing;
use crate::config::{PhotoLimitConfig, TimeRange}; use crate::config::{TimeIntervalConfig, TimeRange};
use crate::error::{Error, Result}; use crate::error::{Error, Result};
use crate::immich_api::{Asset, FaceData, ImmichClient}; use crate::immich_api::{Asset, FaceData, ImmichClient};
use crate::pipeline::Pipeline; use crate::pipeline::Pipeline;
@ -30,15 +30,15 @@ use tokio_util::sync::CancellationToken;
/// ///
/// Thread-safe: uses atomic counters with fetch_add + undo pattern /// Thread-safe: uses atomic counters with fetch_add + undo pattern
/// so concurrent workers can claim slots without races. /// so concurrent workers can claim slots without races.
pub struct PhotoLimitTracker { pub struct TimeIntervalTracker {
max_photos: u32, max_photos: u32,
time_range: TimeRange, time_range: TimeRange,
buckets: RwLock<HashMap<String, Arc<AtomicU32>>>, buckets: RwLock<HashMap<String, Arc<AtomicU32>>>,
} }
impl PhotoLimitTracker { impl TimeIntervalTracker {
/// Create a tracker if photo limiting is enabled, otherwise return None. /// Create a tracker if photo limiting is enabled, otherwise return None.
pub fn new(config: &PhotoLimitConfig) -> Option<Self> { pub fn new(config: &TimeIntervalConfig) -> Option<Self> {
if !config.enabled { if !config.enabled {
return None; return None;
} }
@ -277,7 +277,7 @@ async fn run_job_inner(
tracing::debug!("Pipeline steps: {:?}", pipeline.step_ids()); tracing::debug!("Pipeline steps: {:?}", pipeline.step_ids());
// Create photo limit tracker if enabled // Create photo limit tracker if enabled
let photo_limit_tracker = PhotoLimitTracker::new(&config.processing.photo_limit).map(Arc::new); let time_interval_tracker = TimeIntervalTracker::new(&config.processing.time_interval).map(Arc::new);
// Process images in parallel with concurrency limit // Process images in parallel with concurrency limit
let completed = Arc::new(AtomicU32::new(0)); let completed = Arc::new(AtomicU32::new(0));
@ -317,7 +317,7 @@ async fn run_job_inner(
let skip_stats = skip_stats.clone(); let skip_stats = skip_stats.clone();
// Clone photo limit tracker for this task // Clone photo limit tracker for this task
let photo_limit_tracker = photo_limit_tracker.clone(); let time_interval_tracker = time_interval_tracker.clone();
// Clone person info for this task // Clone person info for this task
let person_id = task_person_id.clone(); let person_id = task_person_id.clone();
@ -341,7 +341,7 @@ async fn run_job_inner(
&task_cancel_token, &task_cancel_token,
&skip_stats, &skip_stats,
&pipeline, &pipeline,
photo_limit_tracker.as_ref(), time_interval_tracker.as_ref(),
) )
.await; .await;

View file

@ -8,7 +8,7 @@ use crate::immich_api::{Asset, FaceData, ImmichClient};
use crate::pipeline::{Pipeline, PipelineContext, PipelineResult}; use crate::pipeline::{Pipeline, PipelineContext, PipelineResult};
use crate::web::AtomicSkipStats; use crate::web::AtomicSkipStats;
use super::PhotoLimitTracker; use super::TimeIntervalTracker;
use bytes::Bytes; use bytes::Bytes;
use image::ImageFormat; use image::ImageFormat;
@ -70,7 +70,7 @@ pub async fn process_single_asset(
cancel_token: &CancellationToken, cancel_token: &CancellationToken,
skip_stats: &Arc<AtomicSkipStats>, skip_stats: &Arc<AtomicSkipStats>,
pipeline: &Pipeline, pipeline: &Pipeline,
photo_limit: Option<&Arc<PhotoLimitTracker>>, time_interval: Option<&Arc<TimeIntervalTracker>>,
) -> AssetProcessResult { ) -> AssetProcessResult {
let asset_id = &asset.id; let asset_id = &asset.id;
@ -123,12 +123,13 @@ pub async fn process_single_asset(
.. ..
} => { } => {
// Check photo limit before saving // Check photo limit before saving
if let Some(tracker) = photo_limit { if let Some(tracker) = time_interval {
if !tracker.try_claim(&timestamp) { if !tracker.try_claim(&timestamp) {
skip_stats.increment("photo_limit"); tracing::debug!("Asset {} skipped: time interval too short (timestamp: {})", asset_id, timestamp);
skip_stats.increment("time_interval");
return AssetProcessResult::Skipped { return AssetProcessResult::Skipped {
asset_id, asset_id,
reason: "Photo limit reached for time range".to_string(), reason: "Time interval too short".to_string(),
}; };
} }
} }

View file

@ -2,7 +2,7 @@
use crate::config::{ use crate::config::{
AlignmentConfig, BlurConfig, BrightnessConfig, EyeFilterConfig, FaceResolutionConfig, AlignmentConfig, BlurConfig, BrightnessConfig, EyeFilterConfig, FaceResolutionConfig,
HeadPoseConfig, OutputConfig, PhotoLimitConfig, ProcessingConfig, TimestampConfig, VideoConfig, HeadPoseConfig, OutputConfig, TimeIntervalConfig, ProcessingConfig, TimestampConfig, VideoConfig,
}; };
use crate::web::state::AppState; use crate::web::state::AppState;
use axum::{extract::State, http::StatusCode, response::Json}; use axum::{extract::State, http::StatusCode, response::Json};
@ -46,7 +46,7 @@ pub struct ProcessingConfigUpdate {
pub output: Option<OutputConfig>, pub output: Option<OutputConfig>,
pub alignment: Option<AlignmentConfig>, pub alignment: Option<AlignmentConfig>,
pub timestamp: Option<TimestampConfig>, pub timestamp: Option<TimestampConfig>,
pub photo_limit: Option<PhotoLimitConfig>, pub time_interval: Option<TimeIntervalConfig>,
} }
/// Video configuration update fields. /// Video configuration update fields.
@ -178,16 +178,16 @@ fn validate_processing_config(proc: &ProcessingConfigUpdate) -> Result<(), Valid
} }
} }
if let Some(ref pl) = proc.photo_limit { if let Some(ref pl) = proc.time_interval {
if pl.enabled && pl.max_photos == 0 { if pl.enabled && pl.max_photos == 0 {
return Err(ValidationError::new( return Err(ValidationError::new(
"processing.photo_limit.max_photos", "processing.time_interval.max_photos",
"must be greater than 0 when enabled", "must be greater than 0 when enabled",
)); ));
} }
if pl.enabled && pl.max_photos > 100 { if pl.enabled && pl.max_photos > 100 {
return Err(ValidationError::new( return Err(ValidationError::new(
"processing.photo_limit.max_photos", "processing.time_interval.max_photos",
format!("must be at most 100, got {}", pl.max_photos), format!("must be at most 100, got {}", pl.max_photos),
)); ));
} }
@ -286,8 +286,8 @@ pub async fn update_config(
if let Some(v) = proc.timestamp { if let Some(v) = proc.timestamp {
config.processing.timestamp = v; config.processing.timestamp = v;
} }
if let Some(v) = proc.photo_limit { if let Some(v) = proc.time_interval {
config.processing.photo_limit = v; config.processing.time_interval = v;
} }
} }