use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
use std::sync::Arc;
use std::time::Instant;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ProcessingStage {
Scanning,
Analyzing,
Processing,
Chapters,
Metadata,
Complete,
}
impl ProcessingStage {
pub fn name(&self) -> &'static str {
match self {
Self::Scanning => "Scanning",
Self::Analyzing => "Analyzing",
Self::Processing => "Processing",
Self::Chapters => "Chapters",
Self::Metadata => "Metadata",
Self::Complete => "Complete",
}
}
}
#[derive(Debug, Clone)]
pub struct BookProgress {
pub name: String,
pub stage: ProcessingStage,
pub progress: f32,
pub start_time: Instant,
pub eta_seconds: Option<f64>,
}
impl BookProgress {
pub fn new(name: String) -> Self {
Self {
name,
stage: ProcessingStage::Scanning,
progress: 0.0,
start_time: Instant::now(),
eta_seconds: None,
}
}
pub fn set_stage(&mut self, stage: ProcessingStage) {
self.stage = stage;
}
pub fn set_progress(&mut self, progress: f32) {
self.progress = progress.clamp(0.0, 100.0);
}
pub fn update_eta(&mut self) {
if self.progress > 0.0 {
let elapsed = self.start_time.elapsed().as_secs_f64();
let total_estimated = elapsed / (self.progress as f64 / 100.0);
self.eta_seconds = Some(total_estimated - elapsed);
}
}
pub fn elapsed_seconds(&self) -> f64 {
self.start_time.elapsed().as_secs_f64()
}
}
#[derive(Debug, Clone)]
pub struct BatchProgress {
total_books: usize,
completed: Arc<AtomicUsize>,
failed: Arc<AtomicUsize>,
bytes_processed: Arc<AtomicU64>,
start_time: Instant,
}
impl BatchProgress {
pub fn new(total_books: usize) -> Self {
Self {
total_books,
completed: Arc::new(AtomicUsize::new(0)),
failed: Arc::new(AtomicUsize::new(0)),
bytes_processed: Arc::new(AtomicU64::new(0)),
start_time: Instant::now(),
}
}
pub fn mark_completed(&self) {
self.completed.fetch_add(1, Ordering::Relaxed);
}
pub fn mark_failed(&self) {
self.failed.fetch_add(1, Ordering::Relaxed);
}
pub fn add_bytes(&self, bytes: u64) {
self.bytes_processed.fetch_add(bytes, Ordering::Relaxed);
}
pub fn completed_count(&self) -> usize {
self.completed.load(Ordering::Relaxed)
}
pub fn failed_count(&self) -> usize {
self.failed.load(Ordering::Relaxed)
}
pub fn total_bytes(&self) -> u64 {
self.bytes_processed.load(Ordering::Relaxed)
}
pub fn total_books(&self) -> usize {
self.total_books
}
pub fn overall_progress(&self) -> f32 {
if self.total_books == 0 {
return 0.0;
}
let processed = self.completed_count() + self.failed_count();
(processed as f32 / self.total_books as f32) * 100.0
}
pub fn eta_seconds(&self) -> Option<f64> {
let completed = self.completed_count();
if completed == 0 {
return None;
}
let elapsed = self.start_time.elapsed().as_secs_f64();
let avg_time_per_book = elapsed / completed as f64;
let remaining_books = self.total_books - completed - self.failed_count();
if remaining_books > 0 {
Some(avg_time_per_book * remaining_books as f64)
} else {
Some(0.0)
}
}
pub fn elapsed_seconds(&self) -> f64 {
self.start_time.elapsed().as_secs_f64()
}
pub fn format_eta(&self) -> String {
match self.eta_seconds() {
Some(seconds) if seconds > 0.0 => {
let hours = (seconds / 3600.0) as u64;
let minutes = ((seconds % 3600.0) / 60.0) as u64;
let secs = (seconds % 60.0) as u64;
if hours > 0 {
format!("{}h {:02}m {:02}s", hours, minutes, secs)
} else if minutes > 0 {
format!("{}m {:02}s", minutes, secs)
} else {
format!("{}s", secs)
}
}
_ => "calculating...".to_string(),
}
}
pub fn format_elapsed(&self) -> String {
let seconds = self.elapsed_seconds();
let hours = (seconds / 3600.0) as u64;
let minutes = ((seconds % 3600.0) / 60.0) as u64;
let secs = (seconds % 60.0) as u64;
if hours > 0 {
format!("{}h {:02}m {:02}s", hours, minutes, secs)
} else if minutes > 0 {
format!("{}m {:02}s", minutes, secs)
} else {
format!("{}s", secs)
}
}
pub fn is_complete(&self) -> bool {
self.completed_count() + self.failed_count() >= self.total_books
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::thread;
use std::time::Duration;
#[test]
fn test_processing_stage_name() {
assert_eq!(ProcessingStage::Scanning.name(), "Scanning");
assert_eq!(ProcessingStage::Analyzing.name(), "Analyzing");
assert_eq!(ProcessingStage::Complete.name(), "Complete");
}
#[test]
fn test_book_progress() {
let mut progress = BookProgress::new("Test Book".to_string());
assert_eq!(progress.name, "Test Book");
assert_eq!(progress.stage, ProcessingStage::Scanning);
assert_eq!(progress.progress, 0.0);
progress.set_stage(ProcessingStage::Processing);
assert_eq!(progress.stage, ProcessingStage::Processing);
progress.set_progress(50.0);
assert_eq!(progress.progress, 50.0);
progress.set_progress(150.0);
assert_eq!(progress.progress, 100.0);
}
#[test]
fn test_batch_progress() {
let progress = BatchProgress::new(10);
assert_eq!(progress.total_books(), 10);
assert_eq!(progress.completed_count(), 0);
assert_eq!(progress.failed_count(), 0);
assert_eq!(progress.overall_progress(), 0.0);
progress.mark_completed();
assert_eq!(progress.completed_count(), 1);
assert_eq!(progress.overall_progress(), 10.0);
progress.mark_failed();
assert_eq!(progress.failed_count(), 1);
assert_eq!(progress.overall_progress(), 20.0);
}
#[test]
fn test_batch_progress_bytes() {
let progress = BatchProgress::new(5);
assert_eq!(progress.total_bytes(), 0);
progress.add_bytes(1024);
assert_eq!(progress.total_bytes(), 1024);
progress.add_bytes(2048);
assert_eq!(progress.total_bytes(), 3072);
}
#[test]
fn test_batch_progress_eta() {
let progress = BatchProgress::new(10);
assert!(progress.eta_seconds().is_none());
thread::sleep(Duration::from_millis(100));
progress.mark_completed();
assert!(progress.eta_seconds().is_some());
}
#[test]
fn test_batch_progress_is_complete() {
let progress = BatchProgress::new(3);
assert!(!progress.is_complete());
progress.mark_completed();
progress.mark_completed();
assert!(!progress.is_complete());
progress.mark_completed();
assert!(progress.is_complete());
}
#[test]
fn test_format_eta() {
let progress = BatchProgress::new(1);
let eta = progress.format_eta();
assert_eq!(eta, "calculating...");
}
#[test]
fn test_format_elapsed() {
let progress = BatchProgress::new(1);
thread::sleep(Duration::from_millis(100));
let elapsed = progress.format_elapsed();
assert!(elapsed.ends_with('s'));
}
}