Major refactoring: Event-driven sermon converter
- 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
This commit is contained in:
parent
c5bb088c79
commit
18bddf4e6f
327
Cargo.lock
generated
327
Cargo.lock
generated
|
@ -47,6 +47,17 @@ version = "1.0.95"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04"
|
||||
|
||||
[[package]]
|
||||
name = "async-trait"
|
||||
version = "0.1.89"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.4.0"
|
||||
|
@ -136,12 +147,53 @@ dependencies = [
|
|||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-deque"
|
||||
version = "0.8.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51"
|
||||
dependencies = [
|
||||
"crossbeam-epoch",
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-epoch"
|
||||
version = "0.9.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
|
||||
dependencies = [
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-utils"
|
||||
version = "0.8.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
|
||||
|
||||
[[package]]
|
||||
name = "either"
|
||||
version = "1.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
|
||||
|
||||
[[package]]
|
||||
name = "errno"
|
||||
version = "0.3.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fastrand"
|
||||
version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
|
||||
|
||||
[[package]]
|
||||
name = "filetime"
|
||||
version = "0.2.25"
|
||||
|
@ -163,6 +215,18 @@ dependencies = [
|
|||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "getrandom"
|
||||
version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"r-efi",
|
||||
"wasi 0.14.4+wasi-0.2.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gimli"
|
||||
version = "0.31.1"
|
||||
|
@ -242,6 +306,12 @@ dependencies = [
|
|||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lazy_static"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.169"
|
||||
|
@ -259,6 +329,12 @@ dependencies = [
|
|||
"redox_syscall",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.9.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12"
|
||||
|
||||
[[package]]
|
||||
name = "lock_api"
|
||||
version = "0.4.12"
|
||||
|
@ -275,6 +351,15 @@ version = "0.4.22"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
|
||||
|
||||
[[package]]
|
||||
name = "matchers"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9"
|
||||
dependencies = [
|
||||
"regex-automata",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.7.4"
|
||||
|
@ -298,7 +383,7 @@ checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c"
|
|||
dependencies = [
|
||||
"libc",
|
||||
"log",
|
||||
"wasi",
|
||||
"wasi 0.11.0+wasi-snapshot-preview1",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
|
@ -309,7 +394,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"wasi",
|
||||
"wasi 0.11.0+wasi-snapshot-preview1",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
|
@ -332,6 +417,24 @@ dependencies = [
|
|||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ntapi"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e8a3895c6391c39d7fe7ebc444a87eb2991b2a0bc718fdabd071eec617fc68e4"
|
||||
dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nu-ansi-term"
|
||||
version = "0.50.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d4a28e057d01f97e61255210fcff094d74ed0466038633e95017f5beb68e4399"
|
||||
dependencies = [
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.19"
|
||||
|
@ -403,6 +506,32 @@ dependencies = [
|
|||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "r-efi"
|
||||
version = "5.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
|
||||
|
||||
[[package]]
|
||||
name = "rayon"
|
||||
version = "1.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f"
|
||||
dependencies = [
|
||||
"either",
|
||||
"rayon-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rayon-core"
|
||||
version = "1.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91"
|
||||
dependencies = [
|
||||
"crossbeam-deque",
|
||||
"crossbeam-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "redox_syscall"
|
||||
version = "0.5.8"
|
||||
|
@ -447,6 +576,19 @@ version = "0.1.24"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
|
||||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "1.0.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8"
|
||||
dependencies = [
|
||||
"bitflags 2.6.0",
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "same-file"
|
||||
version = "1.0.6"
|
||||
|
@ -467,10 +609,23 @@ name = "sermon_converter"
|
|||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"chrono",
|
||||
"notify",
|
||||
"regex",
|
||||
"tempfile",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
"video_processing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sharded-slab"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
|
||||
dependencies = [
|
||||
"lazy_static",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -515,6 +670,43 @@ dependencies = [
|
|||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sysinfo"
|
||||
version = "0.30.13"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0a5b4ddaee55fb2bea2bf0e5000747e5f5c0de765e5a5ff87f4cd106439f4bb3"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"core-foundation-sys",
|
||||
"libc",
|
||||
"ntapi",
|
||||
"once_cell",
|
||||
"rayon",
|
||||
"windows",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tempfile"
|
||||
version = "3.21.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "15b61f8f20e3a6f7e0649d825294eaf317edce30f82cf6026e7e4cb9222a7d1e"
|
||||
dependencies = [
|
||||
"fastrand",
|
||||
"getrandom",
|
||||
"once_cell",
|
||||
"rustix",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thread_local"
|
||||
version = "1.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.42.0"
|
||||
|
@ -544,12 +736,94 @@ dependencies = [
|
|||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing"
|
||||
version = "0.1.41"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0"
|
||||
dependencies = [
|
||||
"pin-project-lite",
|
||||
"tracing-attributes",
|
||||
"tracing-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-attributes"
|
||||
version = "0.1.30"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-core"
|
||||
version = "0.1.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"valuable",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-log"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
|
||||
dependencies = [
|
||||
"log",
|
||||
"once_cell",
|
||||
"tracing-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing-subscriber"
|
||||
version = "0.3.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5"
|
||||
dependencies = [
|
||||
"matchers",
|
||||
"nu-ansi-term",
|
||||
"once_cell",
|
||||
"regex-automata",
|
||||
"sharded-slab",
|
||||
"smallvec",
|
||||
"thread_local",
|
||||
"tracing",
|
||||
"tracing-core",
|
||||
"tracing-log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83"
|
||||
|
||||
[[package]]
|
||||
name = "valuable"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
|
||||
|
||||
[[package]]
|
||||
name = "video_processing"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"chrono",
|
||||
"notify",
|
||||
"regex",
|
||||
"sysinfo",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "walkdir"
|
||||
version = "2.5.0"
|
||||
|
@ -566,6 +840,15 @@ version = "0.11.0+wasi-snapshot-preview1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
|
||||
|
||||
[[package]]
|
||||
name = "wasi"
|
||||
version = "0.14.4+wasi-0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "88a5f4a424faf49c3c2c344f166f0662341d470ea185e939657aaff130f0ec4a"
|
||||
dependencies = [
|
||||
"wit-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.99"
|
||||
|
@ -620,6 +903,22 @@ version = "0.2.99"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "943aab3fdaaa029a6e0271b35ea10b72b943135afe9bffca82384098ad0e06a6"
|
||||
|
||||
[[package]]
|
||||
name = "winapi"
|
||||
version = "0.3.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
|
||||
dependencies = [
|
||||
"winapi-i686-pc-windows-gnu",
|
||||
"winapi-x86_64-pc-windows-gnu",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi-i686-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
||||
|
||||
[[package]]
|
||||
name = "winapi-util"
|
||||
version = "0.1.9"
|
||||
|
@ -629,6 +928,22 @@ dependencies = [
|
|||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi-x86_64-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
||||
|
||||
[[package]]
|
||||
name = "windows"
|
||||
version = "0.52.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be"
|
||||
dependencies = [
|
||||
"windows-core",
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-core"
|
||||
version = "0.52.0"
|
||||
|
@ -785,3 +1100,9 @@ name = "windows_x86_64_msvc"
|
|||
version = "0.52.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen"
|
||||
version = "0.45.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c573471f125075647d03df72e026074b7203790d41351cd6edc96f46bcccd36"
|
||||
|
|
18
Cargo.toml
18
Cargo.toml
|
@ -3,9 +3,23 @@ name = "sermon_converter"
|
|||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[lib]
|
||||
name = "sermon_converter"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "sermon_converter"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
tokio = { version = "1.0", features = ["full"] }
|
||||
tokio = { version = "1.0", features = ["full", "signal"] }
|
||||
anyhow = "1.0"
|
||||
notify = "6.1"
|
||||
chrono = "0.4"
|
||||
regex = "1.5"
|
||||
tracing = "0.1"
|
||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||
async-trait = "0.1"
|
||||
video_processing = { git = "https://git.rockvilletollandsda.church/RTSDA/video-processing-support.git", branch = "main" }
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3.0"
|
||||
|
|
3
src/lib.rs
Normal file
3
src/lib.rs
Normal file
|
@ -0,0 +1,3 @@
|
|||
pub mod services;
|
||||
|
||||
pub use services::sermon_converter::VideoConverter;
|
172
src/main.rs
172
src/main.rs
|
@ -1,14 +1,28 @@
|
|||
use std::path::PathBuf;
|
||||
use std::env;
|
||||
use std::sync::Arc;
|
||||
use anyhow::Result;
|
||||
use notify::{Watcher, RecursiveMode, Event, EventKind};
|
||||
use tokio::sync::mpsc;
|
||||
use tracing::{info, error, warn};
|
||||
use video_processing::{
|
||||
VideoProcessingConfig, StabilityTracker, SystemMonitor, ShutdownHandler,
|
||||
EventDrivenFileWatcher, FileProcessingService
|
||||
};
|
||||
|
||||
mod services;
|
||||
use services::sermon_converter::VideoConverter;
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
// Initialize logging
|
||||
tracing_subscriber::fmt()
|
||||
.with_env_filter(
|
||||
env::var("RUST_LOG")
|
||||
.unwrap_or_else(|_| "sermon_converter=info,video_processing=info".to_string())
|
||||
)
|
||||
.init();
|
||||
|
||||
info!("Starting Sermon Converter v0.1.0");
|
||||
|
||||
let watch_path = PathBuf::from(
|
||||
env::var("SERMON_WATCH_PATH").expect("SERMON_WATCH_PATH environment variable must be set")
|
||||
);
|
||||
|
@ -18,110 +32,94 @@ async fn main() -> Result<()> {
|
|||
|
||||
// Ensure directories exist
|
||||
if !watch_path.exists() {
|
||||
println!("Creating watch directory: {}", watch_path.display());
|
||||
info!("Creating watch directory: {}", watch_path.display());
|
||||
std::fs::create_dir_all(&watch_path)?;
|
||||
}
|
||||
if !output_path.exists() {
|
||||
println!("Creating output directory: {}", output_path.display());
|
||||
info!("Creating output directory: {}", output_path.display());
|
||||
std::fs::create_dir_all(&output_path)?;
|
||||
}
|
||||
|
||||
println!("Starting sermon converter service...");
|
||||
println!("Watching directory: {}", watch_path.display());
|
||||
println!("Output directory: {}", output_path.display());
|
||||
info!("Watch directory: {}", watch_path.display());
|
||||
info!("Output directory: {}", output_path.display());
|
||||
|
||||
let converter = VideoConverter::new(output_path);
|
||||
// Initialize components
|
||||
let config = VideoProcessingConfig::from_env();
|
||||
let converter = Arc::new(VideoConverter::new(output_path.clone()));
|
||||
let stability_tracker = Arc::new(StabilityTracker::new(config));
|
||||
let system_monitor = SystemMonitor::new(5); // 5GB minimum free space
|
||||
let (shutdown_handler, mut shutdown_rx) = ShutdownHandler::new();
|
||||
|
||||
// Process existing files first
|
||||
println!("Checking for existing files...");
|
||||
if let Ok(entries) = std::fs::read_dir(&watch_path) {
|
||||
for entry in entries {
|
||||
if let Ok(entry) = entry {
|
||||
let path = entry.path();
|
||||
if path.to_string_lossy().contains(".syncthing.") {
|
||||
println!("Skipping temporary file: {}", path.display());
|
||||
continue;
|
||||
}
|
||||
if let Some(ext) = path.extension() {
|
||||
if ext == "mp4" {
|
||||
println!("Processing existing MP4 file: {}", path.display());
|
||||
if let Err(e) = converter.process_file(path).await {
|
||||
eprintln!("Error processing existing file: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Check initial disk space
|
||||
if !system_monitor.check_disk_space(&output_path)? {
|
||||
error!("Insufficient disk space to start processing");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Set up file processing service
|
||||
let file_service = Arc::new(FileProcessingService::new(
|
||||
Arc::clone(&converter),
|
||||
Arc::clone(&stability_tracker),
|
||||
));
|
||||
|
||||
// Process existing files
|
||||
if let Err(e) = file_service.process_existing_files(&watch_path).await {
|
||||
error!("Error processing existing files: {}", e);
|
||||
}
|
||||
|
||||
// Set up file watcher
|
||||
let (tx, mut rx) = mpsc::channel(100);
|
||||
|
||||
let mut watcher = notify::recommended_watcher(move |res: Result<Event, notify::Error>| {
|
||||
let tx = tx.clone();
|
||||
match res {
|
||||
Ok(event) => {
|
||||
match &event.kind {
|
||||
EventKind::Create(_) => println!("File created: {:?}", event.paths),
|
||||
EventKind::Modify(notify::event::ModifyKind::Name(mode)) => {
|
||||
match mode {
|
||||
notify::event::RenameMode::From => println!("File renamed from: {:?}", event.paths),
|
||||
notify::event::RenameMode::To => println!("File renamed to: {:?}", event.paths),
|
||||
notify::event::RenameMode::Both => println!("File renamed: {:?}", event.paths),
|
||||
_ => println!("Other rename event: {:?}", event),
|
||||
}
|
||||
},
|
||||
_ => println!("Other file event: {:?}", event),
|
||||
}
|
||||
|
||||
if let Err(e) = tx.blocking_send(event) {
|
||||
eprintln!("Error sending event: {}", e);
|
||||
}
|
||||
let mut file_watcher = EventDrivenFileWatcher::new(watch_path.clone())?;
|
||||
|
||||
// Start shutdown signal handler
|
||||
let shutdown_task = tokio::spawn(async move {
|
||||
shutdown_handler.wait_for_shutdown_signal().await;
|
||||
});
|
||||
|
||||
info!("Sermon converter is running. Press Ctrl+C to stop.");
|
||||
|
||||
// Main event loop
|
||||
loop {
|
||||
tokio::select! {
|
||||
// Handle shutdown signal
|
||||
_ = shutdown_rx.recv() => {
|
||||
info!("Shutdown signal received, stopping...");
|
||||
break;
|
||||
}
|
||||
Err(e) => eprintln!("Watch error: {}", e),
|
||||
}
|
||||
})?;
|
||||
|
||||
watcher.watch(&watch_path, RecursiveMode::NonRecursive)?;
|
||||
println!("Watcher started successfully");
|
||||
|
||||
while let Some(event) = rx.recv().await {
|
||||
match event.kind {
|
||||
EventKind::Create(_) |
|
||||
EventKind::Modify(notify::event::ModifyKind::Name(notify::event::RenameMode::To)) => {
|
||||
for path in event.paths {
|
||||
// Skip temporary and non-MP4 files
|
||||
if path.to_string_lossy().contains(".syncthing.") {
|
||||
println!("Skipping temporary Syncthing file: {}", path.display());
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(ext) = path.extension() {
|
||||
if ext == "mp4" {
|
||||
println!("New MP4 file detected: {}", path.display());
|
||||
// Wait a short time to ensure file is fully written
|
||||
tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
|
||||
|
||||
println!("Starting processing for: {}", path.display());
|
||||
if let Err(e) = converter.process_file(path.clone()).await {
|
||||
eprintln!("Error processing file {}: {}", path.display(), e);
|
||||
} else {
|
||||
println!("Successfully processed: {}", path.display());
|
||||
|
||||
// Handle file events
|
||||
event = file_watcher.next_event() => {
|
||||
if let Some(event) = event {
|
||||
if EventDrivenFileWatcher::should_process_event(&event) {
|
||||
let mp4_paths = EventDrivenFileWatcher::extract_mp4_paths(&event);
|
||||
|
||||
for path in mp4_paths {
|
||||
// Check disk space before processing
|
||||
if !system_monitor.check_disk_space(&output_path)? {
|
||||
warn!("Insufficient disk space, skipping file: {}", path.display());
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
println!("Ignoring non-MP4 file: {}", path.display());
|
||||
|
||||
info!("New MP4 file detected: {}", path.display());
|
||||
|
||||
let service = Arc::clone(&file_service);
|
||||
tokio::spawn(async move {
|
||||
if let Err(e) = service.handle_file_event(path.clone()).await {
|
||||
error!("Failed to process file {}: {}", path.display(), e);
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
println!("Ignoring file without extension: {}", path.display());
|
||||
}
|
||||
} else {
|
||||
warn!("File watcher channel closed, exiting");
|
||||
break;
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
// Log other events at debug level
|
||||
println!("Ignoring event: {:?}", event);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for shutdown to complete
|
||||
shutdown_task.await?;
|
||||
info!("Sermon converter stopped");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -1,56 +1,37 @@
|
|||
use std::path::PathBuf;
|
||||
use anyhow::{Result, anyhow};
|
||||
use chrono::NaiveDate;
|
||||
use tokio::process::Command;
|
||||
use tokio::time::Duration;
|
||||
use regex::Regex;
|
||||
use video_processing::{
|
||||
VideoProcessingConfig, VideoConverter as SharedVideoConverter, NfoGenerator, FileProcessor
|
||||
};
|
||||
|
||||
pub struct VideoConverter {
|
||||
output_path: PathBuf,
|
||||
config: VideoProcessingConfig,
|
||||
video_converter: SharedVideoConverter,
|
||||
nfo_generator: NfoGenerator,
|
||||
}
|
||||
|
||||
impl VideoConverter {
|
||||
pub fn new(output_path: PathBuf) -> Self {
|
||||
let config = VideoProcessingConfig::from_env();
|
||||
let video_converter = SharedVideoConverter::new(config.clone());
|
||||
let nfo_generator = NfoGenerator::new(config.clone());
|
||||
|
||||
VideoConverter {
|
||||
output_path,
|
||||
config,
|
||||
video_converter,
|
||||
nfo_generator,
|
||||
}
|
||||
}
|
||||
|
||||
async fn wait_for_file_ready(&self, path: &PathBuf) -> Result<()> {
|
||||
let mut last_size = 0;
|
||||
let mut unchanged_count = 0;
|
||||
|
||||
println!("Waiting for file to stabilize: {}", path.display());
|
||||
|
||||
// Wait up to 30 minutes for file to stabilize
|
||||
for i in 0..1800 {
|
||||
if let Ok(metadata) = tokio::fs::metadata(path).await {
|
||||
let current_size = metadata.len();
|
||||
if current_size > 0 {
|
||||
if current_size == last_size {
|
||||
unchanged_count += 1;
|
||||
// File size unchanged for 30 seconds
|
||||
if unchanged_count >= 30 {
|
||||
println!("File size stable for 30 seconds");
|
||||
return Ok(());
|
||||
}
|
||||
} else {
|
||||
if i > 0 { // Don't log first check
|
||||
println!("File size changed: {} -> {}", last_size, current_size);
|
||||
}
|
||||
unchanged_count = 0;
|
||||
}
|
||||
last_size = current_size;
|
||||
}
|
||||
}
|
||||
tokio::time::sleep(Duration::from_secs(1)).await;
|
||||
}
|
||||
Err(anyhow!("Timeout waiting for file to finish copying"))
|
||||
}
|
||||
// Note: File stability checking is now handled by the caller using the shared crate
|
||||
|
||||
async fn extract_date_from_filename(&self, filename: &str) -> Result<NaiveDate> {
|
||||
// Example filename: "Sermon Title - Speaker | December 4th 2024.mp4"
|
||||
if let Some(date_part) = filename.split('|').nth(1) {
|
||||
pub async fn extract_date_from_filename(&self, filename: &str) -> Result<NaiveDate> {
|
||||
// Example filename: "Sermon Title - Speaker (December 4th 2024).mp4"
|
||||
if let Some(date_part) = filename.split('(').nth(1).and_then(|part| part.split(')').next()) {
|
||||
let re = Regex::new(r"(?i)(January|February|March|April|May|June|July|August|September|October|November|December)\s+(\d+)(?:st|nd|rd|th)?\s+(\d{4})")?;
|
||||
|
||||
if let Some(caps) = re.captures(date_part) {
|
||||
|
@ -62,16 +43,16 @@ impl VideoConverter {
|
|||
let date = NaiveDate::parse_from_str(&date_str, "%d %B %Y")?;
|
||||
Ok(date)
|
||||
} else {
|
||||
Err(anyhow!("Could not extract date from filename part after |"))
|
||||
Err(anyhow!("Could not extract date from filename part within parentheses"))
|
||||
}
|
||||
} else {
|
||||
Err(anyhow!("No | separator found in filename"))
|
||||
Err(anyhow!("No date found in parentheses in filename"))
|
||||
}
|
||||
}
|
||||
|
||||
async fn extract_title_and_speaker(&self, filename: &str) -> Result<(String, String)> {
|
||||
// Example: "God's People in the Time of the End - Pastor Joseph Piresson | December 28th 2024.mp4"
|
||||
if let Some(main_part) = filename.split('|').next() {
|
||||
pub async fn extract_title_and_speaker(&self, filename: &str) -> Result<(String, String)> {
|
||||
// Example: "God's People in the Time of the End - Pastor Joseph Piresson (December 28th 2024).mp4"
|
||||
if let Some(main_part) = filename.split('(').next() {
|
||||
if let Some((title, speaker)) = main_part.split_once('-') {
|
||||
return Ok((
|
||||
title.trim().to_string(),
|
||||
|
@ -82,46 +63,6 @@ impl VideoConverter {
|
|||
Err(anyhow!("Could not extract title and speaker from filename"))
|
||||
}
|
||||
|
||||
async fn create_nfo_file(&self, video_path: &PathBuf, title: &str, speaker: &str, date: &NaiveDate) -> Result<()> {
|
||||
let nfo_path = video_path.with_extension("nfo");
|
||||
|
||||
// Extract series name if it exists (before the "-" in the title)
|
||||
let series_name = if let Some(idx) = title.find('-') {
|
||||
title[..idx].trim()
|
||||
} else {
|
||||
""
|
||||
};
|
||||
|
||||
// Format the full title with date including year
|
||||
let full_title = format!("{} - {} | {}",
|
||||
title,
|
||||
speaker,
|
||||
date.format("%B %-d %Y") // Format like "December 28 2024"
|
||||
);
|
||||
|
||||
let nfo_content = format!(r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<episodedetails>
|
||||
<title>{}</title>
|
||||
<showtitle>Sermons</showtitle>
|
||||
<season>{}</season>
|
||||
<episode>{}</episode>
|
||||
<aired>{}</aired>
|
||||
<displayseason>{}</displayseason>
|
||||
<displayepisode>{}</displayepisode>
|
||||
<tag>{}</tag>
|
||||
</episodedetails>"#,
|
||||
full_title,
|
||||
date.format("%Y").to_string(),
|
||||
date.format("%m%d").to_string(),
|
||||
date.format("%Y-%m-%d"),
|
||||
date.format("%Y"),
|
||||
date.format("%m%d"),
|
||||
series_name
|
||||
);
|
||||
|
||||
tokio::fs::write(nfo_path, nfo_content).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn process_file(&self, path: PathBuf) -> Result<()> {
|
||||
// Early checks
|
||||
|
@ -137,8 +78,7 @@ impl VideoConverter {
|
|||
return Err(anyhow!("File no longer exists: {}", path.display()));
|
||||
}
|
||||
|
||||
// Wait for file to be fully copied
|
||||
self.wait_for_file_ready(&path).await?;
|
||||
// Note: File stability is now handled by the caller
|
||||
|
||||
println!("Processing sermon: {}", path.display());
|
||||
|
||||
|
@ -167,49 +107,36 @@ impl VideoConverter {
|
|||
|
||||
println!("Converting to AV1 and saving to: {}", output_file.display());
|
||||
|
||||
// Build ffmpeg command for AV1 conversion using QSV
|
||||
let status = Command::new("ffmpeg")
|
||||
.arg("-init_hw_device").arg("qsv=hw")
|
||||
.arg("-filter_hw_device").arg("hw")
|
||||
.arg("-hwaccel").arg("qsv")
|
||||
.arg("-hwaccel_output_format").arg("qsv")
|
||||
.arg("-i").arg(&path)
|
||||
.arg("-c:v").arg("av1_qsv")
|
||||
.arg("-preset").arg("4")
|
||||
.arg("-b:v").arg("6M")
|
||||
.arg("-maxrate").arg("12M")
|
||||
.arg("-bufsize").arg("24M")
|
||||
.arg("-c:a").arg("libopus") // Convert to AAC
|
||||
.arg("-b:a").arg("192k") // Audio bitrate
|
||||
.arg("-y")
|
||||
.arg(&output_file)
|
||||
.status()
|
||||
.await?;
|
||||
// Convert video using shared crate
|
||||
self.video_converter.convert_video(&path, &output_file).await?;
|
||||
|
||||
if !status.success() {
|
||||
return Err(anyhow!("FFmpeg conversion failed"));
|
||||
}
|
||||
|
||||
// Verify conversion succeeded
|
||||
if !output_file.exists() {
|
||||
return Err(anyhow!("Conversion failed - output file not found"));
|
||||
}
|
||||
|
||||
let output_size = tokio::fs::metadata(&output_file).await?.len();
|
||||
if output_size == 0 {
|
||||
return Err(anyhow!("Conversion failed - output file is empty"));
|
||||
}
|
||||
|
||||
// Create NFO file
|
||||
println!("Creating NFO file...");
|
||||
self.create_nfo_file(&output_file, &title, &speaker, &date).await?;
|
||||
// Create NFO file using shared crate
|
||||
let full_title = format!("{} - {}", title, speaker);
|
||||
self.nfo_generator.create_nfo_file(&output_file, &full_title, &date, None).await?;
|
||||
|
||||
println!("Successfully converted {} to AV1 and created NFO", path.display());
|
||||
|
||||
// Delete original file
|
||||
println!("Deleting original file: {}", path.display());
|
||||
tokio::fs::remove_file(path).await?;
|
||||
// Handle original file based on configuration
|
||||
if !self.config.preserve_original_files {
|
||||
println!("Deleting original file: {}", path.display());
|
||||
tokio::fs::remove_file(path).await?;
|
||||
} else {
|
||||
println!("Preserving original file: {}", path.display());
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl FileProcessor for VideoConverter {
|
||||
async fn process_file(&self, path: PathBuf) -> Result<()> {
|
||||
self.process_file(path).await
|
||||
}
|
||||
|
||||
async fn should_skip_existing_file(&self, _path: &PathBuf) -> bool {
|
||||
// For sermons, we don't have a simple duplicate check like livestreams
|
||||
// Let the process_file method handle duplicate detection
|
||||
false
|
||||
}
|
||||
}
|
||||
|
|
223
tests/integration_tests.rs
Normal file
223
tests/integration_tests.rs
Normal file
|
@ -0,0 +1,223 @@
|
|||
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"));
|
||||
}
|
Loading…
Reference in a new issue