This guide is for developers who want to contribute to Auto Clipper or understand its internals.
clipper-rust/
βββ src/
β βββ main.rs # Main application logic
βββ docs/ # Documentation
β βββ README.md # Documentation index
β βββ installation.md # Installation guide
β βββ usage.md # Usage examples
β βββ configuration.md # Configuration options
β βββ api.md # API reference
β βββ development.md # This file
βββ tests/ # Test files (future)
βββ examples/ # Example configurations
βββ .env.example # Environment template
βββ .gitignore # Git ignore rules
βββ Cargo.toml # Rust dependencies
βββ Cargo.lock # Dependency lock file
βββ LICENSE # MIT license
βββ README.md # Project overview
βββ CONTRIBUTING.md # Contributing guidelines
- Rust 1.75.0 or higher
- Git
- FFmpeg, yt-dlp, curl (for testing)
# Clone the repository
git clone https://github.com/Hans02-Neo/clipper-rust.git
cd clipper-rust
# Set up environment
cp .env.example .env
# Add your Gemini API key to .env
# Install development dependencies
cargo install cargo-watch cargo-audit cargo-outdated
# Build and test
cargo build
cargo test# Watch for changes during development
cargo watch -x check -x test -x run
# Format code
cargo fmt
# Lint code
cargo clippy
# Check for security vulnerabilities
cargo audit
# Check for outdated dependencies
cargo outdated// URL detection and validation
if input.starts_with("http") {
// YouTube URL mode
process_youtube_url(input).await?
} else {
// Config file mode
process_config_file(input).await?
}async fn download_video(url: &str) -> Result<String, Box<dyn std::error::Error>> {
let output = Command::new("yt-dlp")
.args(["-f", "best[height<=720]", "-o", "video.%(ext)s", url])
.output()?;
// Error handling and validation
}async fn analyze_with_gemini(
video_info: &str,
duration: f64
) -> Result<String, Box<dyn std::error::Error>> {
// Construct prompt
// Make API request
// Parse response
}// FFmpeg integration for clip generation
let output = Command::new("ffmpeg")
.args(["-i", input, "-ss", start, "-to", end, "-c", "copy", output_file])
.output()?;graph TD
A[CLI Input] --> B{Input Type?}
B -->|URL| C[Download Video]
B -->|Config| D[Load Config]
C --> E[Get Video Info]
E --> F[AI Analysis]
F --> G[Generate JSON Config]
D --> H[Parse Config]
G --> H
H --> I[Process Clips]
I --> J[FFmpeg Processing]
J --> K[Output Files]
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_timestamp_parsing() {
// Test various timestamp formats
assert!(is_valid_timestamp("00:01:30"));
assert!(is_valid_timestamp("01:23:45.500"));
assert!(!is_valid_timestamp("invalid"));
}
#[test]
fn test_config_validation() {
let config = ClipConfig {
input_video: "test.mp4".to_string(),
clips: vec![
Clip {
start: "00:00:10".to_string(),
end: "00:00:20".to_string(),
output: "clip.mp4".to_string(),
}
],
};
assert!(validate_config(&config).is_ok());
}
}#[tokio::test]
async fn test_full_workflow() {
// Test complete workflow with sample data
let result = process_youtube_url("https://www.youtube.com/watch?v=test").await;
// Assertions
}# Create test fixtures
mkdir -p tests/fixtures
# Add sample videos, configs, expected outputs// Use descriptive names
async fn analyze_video_with_gemini_ai() -> Result<ClipSuggestions, AnalysisError>
// Document public functions
/// Analyzes video content using Gemini AI
///
/// # Arguments
/// * `video_info` - Video metadata and title
/// * `duration` - Video duration in seconds
///
/// # Returns
/// * `Result<String, Box<dyn std::error::Error>>` - JSON response
async fn analyze_with_gemini(video_info: &str, duration: f64) -> Result<String, Box<dyn std::error::Error>>
// Use proper error handling
let config = serde_json::from_str(&content)
.map_err(|e| format!("Failed to parse config: {}", e))?;// Custom error types
#[derive(Debug)]
enum AutoClipperError {
VideoDownload(String),
AIAnalysis(String),
VideoProcessing(String),
Configuration(String),
}
// Error conversion
impl From<serde_json::Error> for AutoClipperError {
fn from(err: serde_json::Error) -> Self {
AutoClipperError::Configuration(err.to_string())
}
}// Use async/await consistently
async fn process_clips(config: &ClipConfig) -> Result<(), Box<dyn std::error::Error>> {
for clip in &config.clips {
process_single_clip(clip).await?;
}
Ok(())
}// Use references where possible
fn process_clip(clip: &Clip) -> Result<(), ProcessingError>
// Clean up temporary files
async fn cleanup_temp_files() {
let _ = fs::remove_file("temp_request.json").await;
let _ = fs::remove_file("temp_video.mp4").await;
}// Future enhancement: parallel clip processing
use tokio::task::JoinSet;
async fn process_clips_parallel(clips: &[Clip]) -> Result<(), Box<dyn std::error::Error>> {
let mut set = JoinSet::new();
for clip in clips {
let clip = clip.clone();
set.spawn(async move {
process_single_clip(&clip).await
});
}
while let Some(result) = set.join_next().await {
result??;
}
Ok(())
}// Future enhancement: cache AI responses
use std::collections::HashMap;
struct AICache {
cache: HashMap<String, String>,
}
impl AICache {
fn get_or_analyze(&mut self, video_id: &str) -> Option<&String> {
self.cache.get(video_id)
}
}use log::{info, warn, error, debug};
// Add logging throughout the application
info!("Starting video analysis for: {}", video_url);
debug!("AI response: {}", response);
warn!("Clip duration exceeds recommended length: {}s", duration);
error!("Failed to process clip: {}", error);# Enable debug logging
export RUST_LOG=debug
# Enable trace logging for specific modules
export RUST_LOG=auto_clipper=trace
# Log to file
export RUST_LOG=info
./auto-clipper "url" 2>&1 | tee auto_clipper.log# Use cargo expand to see macro expansions
cargo install cargo-expand
cargo expand
# Use cargo flamegraph for performance profiling
cargo install flamegraph
cargo flamegraph --bin auto-clipper
# Memory profiling with valgrind
valgrind --tool=massif ./target/debug/auto-clipper config.json# Cargo.toml
[profile.release]
opt-level = 3
lto = true
codegen-units = 1
panic = "abort"
[profile.dev]
opt-level = 0
debug = true# Install targets
rustup target add x86_64-pc-windows-gnu
rustup target add x86_64-apple-darwin
# Build for different platforms
cargo build --release --target x86_64-pc-windows-gnu
cargo build --release --target x86_64-apple-darwin# 1. Update version in Cargo.toml
# 2. Update CHANGELOG.md
# 3. Run tests
cargo test --release
# 4. Build release
cargo build --release
# 5. Create git tag
git tag v0.1.0
git push origin v0.1.0
# 6. Create GitHub release with binaries- Create feature branch
git checkout -b feature/new-feature-name- Implement feature with tests
// Add new functionality
// Write comprehensive tests
// Update documentation- Test thoroughly
cargo test
cargo clippy
cargo fmt --check- Create pull request
- Define trait for video platforms
trait VideoPlatform {
async fn download(&self, url: &str) -> Result<String, PlatformError>;
async fn get_info(&self, url: &str) -> Result<VideoInfo, PlatformError>;
}- Implement for new platform
struct VimeoDownloader;
impl VideoPlatform for VimeoDownloader {
async fn download(&self, url: &str) -> Result<String, PlatformError> {
// Implementation
}
}- Update URL detection
fn detect_platform(url: &str) -> Box<dyn VideoPlatform> {
if url.contains("youtube.com") {
Box::new(YouTubeDownloader)
} else if url.contains("vimeo.com") {
Box::new(VimeoDownloader)
} else {
Box::new(GenericDownloader)
}
}trait ClipAnalyzer {
async fn analyze(&self, video: &VideoFile) -> Result<Vec<Clip>, AnalysisError>;
}
struct GeminiAnalyzer {
api_key: String,
}
struct CustomAnalyzer {
model_path: String,
}#[derive(Deserialize)]
struct ExtendedConfig {
#[serde(flatten)]
base: ClipConfig,
// Extensions
quality_settings: Option<QualitySettings>,
platform_settings: Option<PlatformSettings>,
ai_settings: Option<AISettings>,
}/// Processes a single video clip using FFmpeg
///
/// This function uses stream copy mode for optimal performance,
/// avoiding re-encoding when possible.
///
/// # Arguments
/// * `input_file` - Path to the source video file
/// * `clip` - Clip configuration with start/end times
///
/// # Returns
/// * `Ok(())` if clip was created successfully
/// * `Err(ProcessingError)` if FFmpeg failed
///
/// # Examples
/// ```
/// let clip = Clip {
/// start: "00:01:30".to_string(),
/// end: "00:02:00".to_string(),
/// output: "highlight.mp4".to_string(),
/// };
/// process_clip("input.mp4", &clip).await?;
/// ```
async fn process_clip(input_file: &str, clip: &Clip) -> Result<(), ProcessingError>- Keep examples current
- Update feature list
- Maintain installation instructions
- Update performance benchmarks
fn validate_youtube_url(url: &str) -> Result<(), ValidationError> {
if !url.starts_with("https://") {
return Err(ValidationError::InsecureUrl);
}
if !url.contains("youtube.com") && !url.contains("youtu.be") {
return Err(ValidationError::InvalidDomain);
}
Ok(())
}// Never log API keys
debug!("Making API request to: {}", endpoint); // β
Good
debug!("API key: {}", api_key); // β Never do this
// Sanitize error messages
fn sanitize_error(error: &str, api_key: &str) -> String {
error.replace(api_key, "[REDACTED]")
}fn validate_output_path(path: &str) -> Result<(), SecurityError> {
// Prevent directory traversal
if path.contains("..") {
return Err(SecurityError::DirectoryTraversal);
}
// Ensure reasonable file extension
if !path.ends_with(".mp4") {
return Err(SecurityError::InvalidExtension);
}
Ok(())
}- Batch processing multiple videos
- Custom AI model integration
- GUI interface
- Plugin system
- Cloud deployment options
- Real-time processing
- Advanced video filters
- Parallel clip processing
- Caching layer for AI responses
- Configuration validation
- Better error messages
- Progress bars
- Resumable downloads
Ready to contribute? Check out our Contributing Guidelines and start building!