
- Complete removal of polling loops for better performance - Event-driven file processing with stability tracking - Proper structured logging with tracing - Graceful shutdown handling (SIGTERM/SIGINT) - Disk space monitoring (5GB minimum threshold) - Shared video_processing crate integration - Filename compatibility improvements (| → () separator) - Environment-based configuration - Comprehensive error handling and logging Features: - Intelligent sermon filename parsing (Title - Speaker (Date).mp4) - Date extraction with full month name support - Title and speaker metadata extraction - Environment variable configuration (SERMON_WATCH_PATH, SERMON_OUTPUT_PATH) - Hardware-accelerated video conversion (Intel QSV) - NFO file generation for Jellyfin compatibility - Syncthing temporary file filtering - AV1 video encoding with configurable settings Performance improvements: - No more wasteful 1-2 second polling - Only processes files when filesystem events occur - Proper resource management and cleanup
223 lines
8.5 KiB
Rust
223 lines
8.5 KiB
Rust
use std::env;
|
|
use std::fs;
|
|
use std::path::PathBuf;
|
|
use tempfile::TempDir;
|
|
use chrono::{NaiveDate, Datelike};
|
|
use sermon_converter::VideoConverter;
|
|
|
|
#[tokio::test]
|
|
async fn test_sermon_converter_creation() {
|
|
let temp_output = TempDir::new().unwrap();
|
|
|
|
// Test that we can create a new converter
|
|
let converter = VideoConverter::new(temp_output.path().to_path_buf());
|
|
|
|
// Test that it was created successfully (converter doesn't expose output_path directly)
|
|
// We'll test this indirectly through other methods
|
|
assert!(true); // Basic smoke test
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_date_extraction_from_sermon_filename() {
|
|
let temp_output = TempDir::new().unwrap();
|
|
let converter = VideoConverter::new(temp_output.path().to_path_buf());
|
|
|
|
// Test valid sermon filename format
|
|
let filename = "God's Love and Mercy - Pastor John Smith (December 25th 2024).mp4";
|
|
let result = converter.extract_date_from_filename(filename).await;
|
|
assert!(result.is_ok());
|
|
|
|
let date = result.unwrap();
|
|
assert_eq!(date.year(), 2024);
|
|
assert_eq!(date.month(), 12);
|
|
assert_eq!(date.day(), 25);
|
|
|
|
// Test different date formats
|
|
let test_cases = vec![
|
|
("Title - Speaker (January 1st 2024).mp4", (2024, 1, 1)),
|
|
("Title - Speaker (February 29th 2024).mp4", (2024, 2, 29)), // Leap year
|
|
("Title - Speaker (March 3rd 2023).mp4", (2023, 3, 3)),
|
|
("Title - Speaker (April 22nd 2025).mp4", (2025, 4, 22)),
|
|
("Title - Speaker (May 11th 2024).mp4", (2024, 5, 11)),
|
|
("Title - Speaker (June 30th 2024).mp4", (2024, 6, 30)),
|
|
("Title - Speaker (July 4th 2024).mp4", (2024, 7, 4)),
|
|
("Title - Speaker (August 15th 2024).mp4", (2024, 8, 15)),
|
|
("Title - Speaker (September 22nd 2024).mp4", (2024, 9, 22)),
|
|
("Title - Speaker (October 31st 2024).mp4", (2024, 10, 31)),
|
|
("Title - Speaker (November 11th 2024).mp4", (2024, 11, 11)),
|
|
];
|
|
|
|
for (filename, (expected_year, expected_month, expected_day)) in test_cases {
|
|
let result = converter.extract_date_from_filename(filename).await;
|
|
assert!(result.is_ok(), "Failed to parse filename: {}", filename);
|
|
|
|
let date = result.unwrap();
|
|
assert_eq!(date.year(), expected_year, "Wrong year for: {}", filename);
|
|
assert_eq!(date.month(), expected_month, "Wrong month for: {}", filename);
|
|
assert_eq!(date.day(), expected_day, "Wrong day for: {}", filename);
|
|
}
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_invalid_sermon_filenames() {
|
|
let temp_output = TempDir::new().unwrap();
|
|
let converter = VideoConverter::new(temp_output.path().to_path_buf());
|
|
|
|
let invalid_cases = vec![
|
|
"no-date-info.mp4",
|
|
"Title - Speaker.mp4",
|
|
"Title - Speaker (Invalid Date).mp4",
|
|
"Title - Speaker (February 30th 2024).mp4", // Invalid date
|
|
"Title - Speaker (13th Month 2024).mp4",
|
|
"No separator here 2024.mp4",
|
|
];
|
|
|
|
for filename in invalid_cases {
|
|
let result = converter.extract_date_from_filename(filename).await;
|
|
assert!(result.is_err(), "Should fail to parse invalid filename: {}", filename);
|
|
}
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_title_and_speaker_extraction() {
|
|
let temp_output = TempDir::new().unwrap();
|
|
let converter = VideoConverter::new(temp_output.path().to_path_buf());
|
|
|
|
let test_cases = vec![
|
|
(
|
|
"The Power of Prayer - Dr. Sarah Johnson (December 25th 2024).mp4",
|
|
("The Power of Prayer", "Dr. Sarah Johnson")
|
|
),
|
|
(
|
|
"Faith in Difficult Times - Pastor Michael Brown (January 1st 2024).mp4",
|
|
("Faith in Difficult Times", "Pastor Michael Brown")
|
|
),
|
|
(
|
|
"God's Amazing Grace - Elder Mary Wilson (March 15th 2024).mp4",
|
|
("God's Amazing Grace", "Elder Mary Wilson")
|
|
),
|
|
];
|
|
|
|
for (filename, (expected_title, expected_speaker)) in test_cases {
|
|
let result = converter.extract_title_and_speaker(filename).await;
|
|
assert!(result.is_ok(), "Failed to extract title and speaker from: {}", filename);
|
|
|
|
let (title, speaker) = result.unwrap();
|
|
assert_eq!(title, expected_title);
|
|
assert_eq!(speaker, expected_speaker);
|
|
}
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_invalid_title_speaker_formats() {
|
|
let temp_output = TempDir::new().unwrap();
|
|
let converter = VideoConverter::new(temp_output.path().to_path_buf());
|
|
|
|
let invalid_cases = vec![
|
|
"No dash separators here (December 25th 2024).mp4", // No dash separator
|
|
];
|
|
|
|
for filename in invalid_cases {
|
|
let result = converter.extract_title_and_speaker(filename).await;
|
|
assert!(result.is_err(), "Should fail to parse invalid format: {}", filename);
|
|
}
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_environment_variable_integration() {
|
|
// Test with custom environment variables
|
|
env::set_var("SHOW_TITLE", "Custom Sermon Series");
|
|
env::set_var("CREATE_NFO_FILES", "true");
|
|
env::set_var("PRESERVE_ORIGINAL_FILES", "false");
|
|
env::set_var("AUDIO_CODEC", "libopus");
|
|
env::set_var("AUDIO_BITRATE", "128k");
|
|
|
|
let temp_output = TempDir::new().unwrap();
|
|
let converter = VideoConverter::new(temp_output.path().to_path_buf());
|
|
|
|
// Test that the converter was created with custom configuration
|
|
// (We test this indirectly since the config is internal)
|
|
assert!(true); // Smoke test that creation succeeded
|
|
|
|
// Clean up
|
|
env::remove_var("SHOW_TITLE");
|
|
env::remove_var("CREATE_NFO_FILES");
|
|
env::remove_var("PRESERVE_ORIGINAL_FILES");
|
|
env::remove_var("AUDIO_CODEC");
|
|
env::remove_var("AUDIO_BITRATE");
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_directory_structure_creation() {
|
|
let temp_output = TempDir::new().unwrap();
|
|
let converter = VideoConverter::new(temp_output.path().to_path_buf());
|
|
|
|
let test_cases = vec![
|
|
("Title - Speaker (January 15th 2024).mp4", "2024", "01-January"),
|
|
("Title - Speaker (December 31st 2024).mp4", "2024", "12-December"),
|
|
("Title - Speaker (July 4th 2023).mp4", "2023", "07-July"),
|
|
];
|
|
|
|
for (filename, expected_year, expected_month) in test_cases {
|
|
let date = converter.extract_date_from_filename(filename).await.unwrap();
|
|
|
|
// The directory structure should match what the converter expects
|
|
let year_dir = temp_output.path().join(expected_year);
|
|
let month_dir = year_dir.join(expected_month);
|
|
|
|
// Create the directory structure as the converter would
|
|
fs::create_dir_all(&month_dir).unwrap();
|
|
|
|
// Verify structure exists
|
|
assert!(year_dir.exists());
|
|
assert!(month_dir.exists());
|
|
|
|
// Test that output file would go in the right place
|
|
let output_file = month_dir.join(filename);
|
|
assert!(!output_file.exists()); // Shouldn't exist initially
|
|
}
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_syncthing_temp_file_rejection() {
|
|
let temp_output = TempDir::new().unwrap();
|
|
let temp_input = TempDir::new().unwrap();
|
|
let converter = VideoConverter::new(temp_output.path().to_path_buf());
|
|
|
|
// Create a syncthing temp file
|
|
let syncthing_file = temp_input.path().join("test.syncthing.tmp.mp4");
|
|
fs::write(&syncthing_file, "temporary content").unwrap();
|
|
|
|
// Process file should reject syncthing files
|
|
let result = converter.process_file(syncthing_file).await;
|
|
assert!(result.is_err());
|
|
assert!(result.unwrap_err().to_string().contains("Syncthing"));
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_non_mp4_file_rejection() {
|
|
let temp_output = TempDir::new().unwrap();
|
|
let temp_input = TempDir::new().unwrap();
|
|
let converter = VideoConverter::new(temp_output.path().to_path_buf());
|
|
|
|
// Create a non-MP4 file
|
|
let avi_file = temp_input.path().join("Title - Speaker (December 25th 2024).avi");
|
|
fs::write(&avi_file, "avi content").unwrap();
|
|
|
|
// Process file should reject non-MP4 files
|
|
let result = converter.process_file(avi_file).await;
|
|
assert!(result.is_err());
|
|
assert!(result.unwrap_err().to_string().contains("non-MP4"));
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn test_missing_file_handling() {
|
|
let temp_output = TempDir::new().unwrap();
|
|
let converter = VideoConverter::new(temp_output.path().to_path_buf());
|
|
|
|
// Try to process a file that doesn't exist
|
|
let missing_file = PathBuf::from("/nonexistent/path/Title - Speaker (December 25th 2024).mp4");
|
|
let result = converter.process_file(missing_file).await;
|
|
assert!(result.is_err());
|
|
assert!(result.unwrap_err().to_string().contains("no longer exists"));
|
|
} |