handbrake 0.0.1

A safe, ergonomic, and asynchronous Rust crate for controlling the HandBrakeCLI video transcoder.
Documentation
## **Developer Specification: `handbrake` Rust Crate**

**Version:** 1.0
**Date:** 16 June 2025

### **1. Introduction & Project Goal**

This document outlines the complete specification for `handbrake`, a Rust crate designed to provide a safe, ergonomic, and robust interface for the `HandBrakeCLI` command-line video transcoding tool.

The primary goal is to allow Rust applications to programmatically start, configure, monitor, and control HandBrake encoding jobs without needing to manually handle command-line arguments or parse raw text output. The crate will feature a modern, asynchronous API suitable for integration into complex applications.

### **2. Core Functional Requirements**

* **Job Configuration**: Allow developers to define an encoding job, including input/output sources and detailed encoding parameters.
* **Process Execution**: Support two modes of execution: a simple "fire-and-forget" mode and a fully monitored mode for real-time feedback.
* **Real-time Monitoring**: In monitored mode, provide a stream of structured events for progress, logs (info, warnings, errors), and job completion.
* **Process Control**: Allow a running encoding job to be gracefully cancelled or forcefully killed.
* **Robust Error Handling**: Implement a "fail-fast" strategy and provide clear, specific errors for both setup and runtime failures.
* **Flexible Setup**: Do not require `HandBrakeCLI` to be in a hardcoded location; search the system `PATH` by default and allow developers to provide a specific path.

### **3. Architectural Design**

The crate's architecture is divided into three main components: the main `HandBrake` factory object, a `JobBuilder` for configuration, and a `JobHandle` for monitoring and control. The entire API will be `async`.

1.  **`HandBrake` (The Factory)**
    * This struct is the main entry point. Its responsibility is to locate and validate the `HandBrakeCLI` executable.
    * It acts as a factory for creating new `JobBuilder` instances.

2.  **`JobBuilder` (The Fluent Configurator)**
    * An instance of `JobBuilder` represents a single, configurable encoding job.
    * It uses the builder pattern with a fluent API (e.g., `job.preset(...).video_codec(...)`) to construct the exact command-line arguments for `HandBrakeCLI`.
    * It is immutable in the sense that each method call can return a new, updated instance of the builder.

3.  **`JobHandle` (The Controller & Monitor)**
    * This struct is returned when a job is started in monitored mode.
    * It represents a running `HandBrakeCLI` process and provides two key functionalities:
        1.  An `async` stream of events parsed from the process's `stderr`.
        2.  Control methods (`cancel`, `kill`) to manage the underlying process.

### **4. API Specification**

Below are the proposed public structs and functions.

#### **4.1. `HandBrake` Struct**

```rust
use std::path::PathBuf;

pub struct HandBrake {
    executable_path: PathBuf,
}

impl HandBrake {
    /// Creates a new HandBrake instance by searching for `HandBrakeCLI` in the system PATH.
    ///
    /// # Errors
    /// Returns an error if `HandBrakeCLI` is not found, is not executable,
    /// or is an unsupported version.
    pub async fn new() -> Result<Self, Error> {
        // ...
    }

    /// Creates a new HandBrake instance using a specific path to `HandBrakeCLI`.
    ///
    /// # Errors
    /// Returns an error if the file at the path does not exist, is not executable,
    /// or is an unsupported version.
    pub async fn new_with_path(path: impl Into<PathBuf>) -> Result<Self, Error> {
        // ...
    }

    /// Creates a new job builder.
    pub fn job(&self, input: InputSource, output: OutputDestination) -> JobBuilder {
        // ...
    }
}
```

#### **4.2. `JobBuilder` Struct & I/O Types**

```rust
use std::process::ExitStatus;

pub enum InputSource {
    File(PathBuf),
    Stdin,
}

pub enum OutputDestination {
    File(PathBuf),
    Stdout,
}

pub struct JobBuilder {
    // internal state for arguments
}

impl JobBuilder {
    /// Sets the HandBrake preset.
    /// e.g., "Fast 1080p30"
    pub fn preset(self, preset: impl Into<String>) -> Self { /* ... */ }

    /// Overrides the video codec.
    /// e.g., "x265", "hevc", "av1"
    pub fn video_codec(self, codec: impl Into<String>) -> Self { /* ... */ }

    /// Overrides the audio codec for a specific track.
    pub fn audio_codec(self, track: u32, codec: impl Into<String>) -> Self { /* ... */ }

    /// Sets the constant quality (RF) for video encoding.
    /// Value typically ranges from 0 to 51 (lower is better quality).
    pub fn quality(self, quality: f32) -> Self { /* ... */ }

    /// Sets the output container format.
    /// e.g., "mp4", "mkv"
    pub fn format(self, format: impl Into<String>) -> Self { /* ... */ }

    // ... other fluent methods for filters, etc.

    /// Executes the job and waits for completion, returning only the final status.
    /// Ideal for "fire-and-forget" scenarios.
    ///
    /// # Errors
    /// Returns an error if the process could not be spawned.
    pub async fn status(self) -> Result<ExitStatus, Error> { /* ... */ }

    /// Starts the job in monitored mode.
    /// Returns a `JobHandle` to monitor and control the running process.
    ///
    /// # Errors
    /// Returns an error if the process could not be spawned.
    pub fn start(self) -> Result<JobHandle, Error> { /* ... */ }
}
```

#### **4.3. `JobHandle` Struct & Event Stream**

```rust
use futures::stream::Stream;
use std::time::Duration;

pub struct JobHandle {
    // handle to the child process
    // receiver for the event stream
}

impl JobHandle {
    /// Attempts to gracefully shut down the HandBrake process.
    /// (Sends SIGINT on Unix, CTRL_C_EVENT on Windows).
    pub async fn cancel(&self) -> Result<(), Error> { /* ... */ }

    /// Forcefully terminates the HandBrake process immediately.
    /// (Sends SIGKILL on Unix, TerminateProcess on Windows).
    pub async fn kill(&self) -> Result<(), Error> { /* ... */ }

    /// Returns an async stream of events from the running job.
    pub fn events(&mut self) -> impl Stream<Item = JobEvent> { /* ... */ }
}

// --- Event Data Structures ---

#[derive(Debug)]
pub enum JobEvent {
    /// Raw, unparsed data from the output stream, typically from `stdout`.
    /// This can contain interleaved video data if streaming to `stdout`.
    Fragment(Vec<u8>),
    /// A progress update from the encoding process.
    Progress(Progress),
    /// A log message from `HandBrakeCLI`.
    Log(Log),
    /// Signals that the `HandBrakeCLI` process has finished.
    Done,
}

#[derive(Debug)]
pub struct Progress {
    pub percentage: f32,
    pub fps: f32,
    pub avg_fps: Option<f32>,
    pub eta: Option<Duration>,
}

#[derive(Debug)]
pub struct Log {
    pub message: String,
}
```

### **5. Data Handling & Output Parsing**

The implementation must correctly handle the `stdout` and `stderr` streams from the `HandBrakeCLI` child process.

*   **`stdout`**: `HandBrakeCLI` writes progress information and, if `OutputDestination` is `Stdout`, raw video data to this stream.
*   **`stderr`**: `HandBrakeCLI` writes all log messages to this stream.
*   **Parsing Logic**: A dedicated asynchronous task must read both `stdout` and `stderr`.
    *   **Progress Lines**: `stdout` is read in chunks, looking for lines matching a pattern like `Encoding: task ..., XX.XX %, ...`. These are parsed into a `Progress` struct.
    *   **Log Lines**: Each line from `stderr` is treated as a `Log` event.
    *   **Raw Data**: Any data from `stdout` that is not a progress line is emitted as a `Fragment` event. This is important for streaming scenarios.
    *   **JSON Job**: The specification requires that `HandBrakeCLI` will print a multiline JSON string with the entire job configuration. This should be parsed into a `JobConfig` struct. (Note: This is a future requirement not yet fully implemented).
    *   **Completion**: When `HandBrakeCLI` prints that it has exited, a final `Done` event must be sent.

### **6. Error Handling Strategy**

A robust error handling strategy is crucial for the crate's usability.

1.  **Fail-Fast Initialization**: As specified, `HandBrake::new()` will fail immediately with a descriptive `Error` if `HandBrakeCLI` is not found or is invalid. This prevents any further configuration attempts on an invalid setup.
2.  **Specific Error Types**: Use a dedicated `Error` enum for the crate, leveraging a library like `thiserror` for clean implementation. Example variants:
    * `ExecutableNotFound { searched_paths: Vec<PathBuf> }`
    * `InvalidExecutable { path: PathBuf, reason: String }`
    * `UnsupportedVersion { version_string: String }`
    * `ProcessSpawnFailed { source: std::io::Error }`
    * `ControlFailed { action: &'static str, source: std::io::Error }`
3.  **Runtime Errors**:
    * Fatal runtime errors from `HandBrakeCLI` (e.g., "Source not found", "Invalid preset") will be captured as `Log` events with `LogLevel::Error`.
    * When the process terminates with a non-zero exit code, the `Done` event will contain `Err(JobFailure)`, providing the exit code and any final error messages captured from the logs.

### **7. Testing Plan**

A multi-layered testing approach is required to ensure correctness and robustness.

1.  **Unit Tests**:
    * Test the `stderr` parsing logic in isolation. Create static string samples of `HandBrakeCLI` output (progress lines, warnings, errors, completion summaries) and assert that the parser correctly transforms them into the appropriate `JobEvent` structs.
    * Test individual argument-building logic within the `JobBuilder`.

2.  **Integration Tests**:
    * These tests will run against a real `HandBrakeCLI` executable and require small, short sample video files. They should be marked as `#[ignore]` by default so they don't run in standard `cargo test` workflows unless explicitly requested (`cargo test -- --ignored`).
    * **Success Case**: Test a full, successful encode from a file to a file. Verify that the output file is created and that the event stream contains `Progress` events and a final `Done(Ok(...))` event.
    * **Failure Case**: Test a job that is designed to fail (e.g., pointing to a non-existent input file). Verify that the event stream emits a `Done(Err(...))` event with the correct failure details.
    * **I/O Streaming**: Test `stdin` to `stdout` piping.
    * **Control Case**: Start a long-running encode and immediately call `cancel()`. Verify that the process terminates and the `Done` event reflects the cancellation. Do the same for `kill()`.

3.  **Mocking**:
    * For CI/CD environments where `HandBrakeCLI` might not be installed, the integration tests should be skipped. Alternatively, a use special class implementation `Command` class that can emulate `HandBrakeCLI` behavior and be used in tests, based on the `stderr` output and exit codes of `HandBrakeCLI` based on its input arguments, allowing for more controlled testing of the crate's interaction logic.