Repository: nathanbabcock/ffmpeg-sidecar (language: rust)

Issue:
Consider adding `anyhow` as a dependency
I have been using this crate for a project and it is fantastic! However, there is one problem: it doesn't play nicely with `anyhow`. This is mainly because the error doesn't implement `Send` and `Sync`. While looking through the code I realized that there is an error module that does things very similarly to anyhow. In fact, it would be very easy to change all errors and results to the anyhow equivalents. That would also bring along all other benefits of anyhow such as the size of struct being only a `usize`, better ergonomics, etc.

I understand that this project has a "no dependency" rule. I don't know if that's a hard rule for you, but I would consider changing it. Currently it seems that the `error` module is just a worse version of anyhow, so why not add actual anyhow as a dependency? The only concrete reason I can think of for not using it as a dependency is because of compile time concerns, but it's usually faster because crates can be compiled in parallel and compilations shared between subdependencies. Rust has little of the disadvantages that some other languages have with deps. Re-inventing the wheel in this way is very rarely a good idea. 

I understand that sometimes open source contributors get a lot of often unreasonable demands, so I want to reiterate that I really appreciate your work! This is just a thing I think is good to consider to make the crate even better 😁 

I would be more than glad to do the migration to `anyhow` myself if I get the approval. 


Discussion / hints:
Hey @Odilf!

> I understand that this project has a "no dependency" rule. I don't know if that's a hard rule for you, but I would consider changing it.

I suppose I would not call it a "hard rule", maybe just a personal aesthetic preference for minimalism. The points you make in favor of `anyhow` are compelling, though. In particular I think it is a problem if you can't use the two libraries together without jumping through extra hoops. I would say that ease-of-use is the most important goal. And you are right that the `error` module was inspired by a stripped-down version of `anyhow`, anyways.

> I would be more than glad to do the migration to anyhow myself if I get the approval.

If you have a good idea of what this refactor would look like, then go for it — and I will help review & test. Ideally it would be backwards compatible, but it's not the end of the world if that isn't possible. Hopefully the introduction of `anyhow` would make this crate even easier to use.

(And if anyone stumbling across this issue disagrees with that,  make it known!)
[thiserror](https://lib.rs/crates/thiserror) is typically used for lib crates. However sidecar seems to be somewhere in the middle.

From thiserror docs:

>Comparison to anyhow:
>
>Use thiserror if you care about designing your own dedicated error type(s) so that the caller receives exactly the information that you choose in the event of failure. This most often applies to library-like code. Use [Anyhow](https://github.com/dtolnay/anyhow) if you don't care what error type your functions return, you just want it to be easy. This is common in application-like code.
That sounds great!

> Ideally it would be backwards compatible, but it's not the end of the world if that isn't possible. 

So... I don't think there is a way to make it strictly backwards compatible. The `Error` struct in this crate has two public fields that `anyhow::Error` just does not have. However, in practice I suspect it's going to be almost always compatible, because people usually just use the error to `unwrap`, `expect` or display. I think it should be fine.

Maybe we want to keep the error module tho? I.e., make it re-export the anyhow Error so people using that module have less things to change. I'd rather remove it too since it's a breaking change anyway, but I don't have strong feelings about it. 


> [thiserror](https://lib.rs/crates/thiserror) is typically used for lib crates. However sidecar seems to be somewhere in the middle.

Well, I would say thiserror is better when you can have structure to your errors and you're able to enumerate all the failure modes. Here we forward various ffmpeg errors which are unstructured so might as well just use anyhow, imo. 

Also, using anyhow is a small change because as @nathanbabcock said the current `Error` is very similar to `anyhow::Error`. Using thiserror would entail more knowledge of the codebase and more design decisions. If we wanted to use thiserror, I think it should be a separate issue.

Oh and lastly I believe a lot (if not most) downstream consumers would/are using anyhow regardless, so it's kinda the same either way. 

Fix patch:
diff --git a/Cargo.toml b/Cargo.toml
index d2a5846..04bfe56 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -14,4 +14,4 @@ license = "MIT"
 crate-type = ["lib"]
 
 [dependencies]
-# there are none!
+anyhow = "1.0.79"
diff --git a/README.md b/README.md
index 3683663..e27d4de 100644
--- a/README.md
+++ b/README.md
@@ -8,7 +8,7 @@
 
 ## Features
 
-- ✨ Zero dependencies
+- ✨ Minimal dependencies
 - ⚡ Automatic FFmpeg CLI download (if needed)
 - 🤗 Support for Windows, MacOS, and Linux
 - 🧪 Thoroughly unit tested
diff --git a/examples/download_ffmpeg.rs b/examples/download_ffmpeg.rs
index 4c38dc4..b7a8441 100644
--- a/examples/download_ffmpeg.rs
+++ b/examples/download_ffmpeg.rs
@@ -1,12 +1,11 @@
 use ffmpeg_sidecar::{
   command::ffmpeg_is_installed,
   download::{check_latest_version, download_ffmpeg_package, ffmpeg_download_url, unpack_ffmpeg},
-  error::Result,
   paths::sidecar_dir,
   version::ffmpeg_version,
 };
 
-fn main() -> Result<()> {
+fn main() -> anyhow::Result<()> {
   if ffmpeg_is_installed() {
     println!("FFmpeg is already installed! 🎉");
     println!("For demo purposes, we'll re-download and unpack it anyway.");
diff --git a/examples/hello_world.rs b/examples/hello_world.rs
index 895854e..6723faa 100644
--- a/examples/hello_world.rs
+++ b/examples/hello_world.rs
@@ -1,11 +1,11 @@
-use ffmpeg_sidecar::{command::FfmpegCommand, error::Result, event::FfmpegEvent};
+use ffmpeg_sidecar::{command::FfmpegCommand, event::FfmpegEvent};
 
 /// Iterates over the frames of a `testsrc`.
 ///
 /// ```console
 /// cargo run --example hello_world
 /// ```
-fn main() -> Result<()> {
+fn main() -> anyhow::Result<()> {
   FfmpegCommand::new() // <- Builder API like `std::process::Command`
     .testsrc() // <- Discoverable aliases for FFmpeg args
     .rawvideo() // <- Convenient argument presets
diff --git a/src/child.rs b/src/child.rs
index 9f49f1e..5226574 100644
--- a/src/child.rs
+++ b/src/child.rs
@@ -3,7 +3,8 @@ use std::{
   process::{Child, ChildStderr, ChildStdin, ChildStdout, ExitStatus},
 };
 
-use crate::error::{Error, Result};
+use anyhow::Context;
+
 use crate::iter::FfmpegIterator;
 
 /// A wrapper around [`std::process::Child`] containing a spawned FFmpeg command.
@@ -22,7 +23,7 @@ impl FfmpegChild {
   /// - Progress updates
   /// - Errors and warnings
   /// - Raw output frames
-  pub fn iter(&mut self) -> Result<FfmpegIterator> {
+  pub fn iter(&mut self) -> anyhow::Result<FfmpegIterator> {
     FfmpegIterator::new(self)
   }
 
@@ -66,12 +67,12 @@ impl FfmpegChild {
   /// q      quit
   /// s      Show QP histogram
   /// ```
-  pub fn send_stdin_command(&mut self, command: &[u8]) -> Result<()> {
+  pub fn send_stdin_command(&mut self, command: &[u8]) -> anyhow::Result<()> {
     let mut stdin = self
       .inner
       .stdin
       .take()
-      .ok_or_else(|| Error::msg("Missing child stdin"))?;
+      .context("Missing child stdin")?;
     stdin.write_all(command)?;
     self.inner.stdin.replace(stdin);
     Ok(())
@@ -83,7 +84,7 @@ impl FfmpegChild {
   /// This method returns after the command has been sent; the actual shut down
   /// may take a few more frames as ffmpeg flushes its buffers and writes the
   /// trailer, if applicable.
-  pub fn quit(&mut self) -> Result<()> {
+  pub fn quit(&mut self) -> anyhow::Result<()> {
     self.send_stdin_command(b"q")
   }
 
diff --git a/src/download.rs b/src/download.rs
index d4901fb..4121b2b 100644
--- a/src/download.rs
+++ b/src/download.rs
@@ -5,21 +5,17 @@ use std::{
   process::{Command, ExitStatus, Stdio},
 };
 
-use crate::{
-  command::ffmpeg_is_installed,
-  error::{Error, Result},
-  paths::sidecar_dir,
-};
+use anyhow::Context;
+
+use crate::{command::ffmpeg_is_installed, paths::sidecar_dir};
 
 pub const UNPACK_DIRNAME: &str = "ffmpeg_release_temp";
 
 /// URL of a manifest file containing the latest published build of FFmpeg. The
 /// correct URL for the target platform is baked in at compile time.
-pub fn ffmpeg_manifest_url() -> Result<&'static str> {
+pub fn ffmpeg_manifest_url() -> anyhow::Result<&'static str> {
   if cfg!(not(target_arch = "x86_64")) {
-    return Err(Error::msg(
-      "Downloads must be manually provided for non-x86_64 architectures",
-    ));
+    anyhow::bail!("Downloads must be manually provided for non-x86_64 architectures");
   }
 
   if cfg!(target_os = "windows") {
@@ -29,13 +25,13 @@ pub fn ffmpeg_manifest_url() -> Result<&'static str> {
   } else if cfg!(target_os = "linux") {
     Ok("https://johnvansickle.com/ffmpeg/release-readme.txt")
   } else {
-    Err(Error::msg("Unsupported platform"))
+    anyhow::bail!("Unsupported platform")
   }
 }
 
 /// URL for the latest published FFmpeg release. The correct URL for the target
 /// platform is baked in at compile time.
-pub fn ffmpeg_download_url() -> Result<&'static str> {
+pub fn ffmpeg_download_url() -> anyhow::Result<&'static str> {
   if cfg!(all(target_os = "windows", target_arch = "x86_64")) {
     Ok("https://www.gyan.dev/ffmpeg/builds/ffmpeg-release-essentials.zip")
   } else if cfg!(all(target_os = "linux", target_arch = "x86_64")) {
@@ -45,9 +41,7 @@ pub fn ffmpeg_download_url() -> Result<&'static str> {
   } else if cfg!(all(target_os = "macos", target_arch = "aarch64")) {
     Ok("https://www.osxexperts.net/ffmpeg6arm.zip") // Mac M1
   } else {
-    Err(Error::msg(
-      "Unsupported platform; you can provide your own URL instead and call download_ffmpeg_package directly.",
-    ))
+    anyhow::bail!("Unsupported platform; you can provide your own URL instead and call download_ffmpeg_package directly.")
   }
 }
 
@@ -57,7 +51,7 @@ pub fn ffmpeg_download_url() -> Result<&'static str> {
 ///
 /// If FFmpeg is already installed, the method exits early without downloading
 /// anything.
-pub fn auto_download() -> Result<()> {
+pub fn auto_download() -> anyhow::Result<()> {
   if ffmpeg_is_installed() {
     return Ok(());
   }
@@ -67,12 +61,11 @@ pub fn auto_download() -> Result<()> {
   let archive_path = download_ffmpeg_package(download_url, &destination)?;
   unpack_ffmpeg(&archive_path, &destination)?;
 
-  match ffmpeg_is_installed() {
-    false => Err(Error::msg(
-      "FFmpeg failed to install, please install manually.",
-    )),
-    true => Ok(()),
+  if !ffmpeg_is_installed() {
+    anyhow::bail!("FFmpeg failed to install, please install manually.");
   }
+
+  Ok(())
 }
 
 /// Parse the the MacOS version number from a JSON string manifest file.
@@ -115,7 +108,7 @@ pub fn parse_linux_version(version: &str) -> Option<String> {
 }
 
 /// Invoke cURL on the command line to download a file, returning it as a string.
-pub fn curl(url: &str) -> Result<String> {
+pub fn curl(url: &str) -> anyhow::Result<String> {
   let mut child = Command::new("curl")
     .args(["-L", url])
     .stderr(Stdio::null())
@@ -125,7 +118,7 @@ pub fn curl(url: &str) -> Result<String> {
   let stdout = child
     .stdout
     .take()
-    .ok_or_else(|| Error::msg("Failed to get stdout"))?;
+    .context("Failed to get stdout")?;
 
   let mut string = String::new();
   std::io::BufReader::new(stdout).read_to_string(&mut string)?;
@@ -133,45 +126,47 @@ pub fn curl(url: &str) -> Result<String> {
 }
 
 /// Invoke cURL on the command line to download a file, writing to a file.
-pub fn curl_to_file(url: &str, destination: &str) -> Result<ExitStatus> {
+pub fn curl_to_file(url: &str, destination: &str) -> anyhow::Result<ExitStatus> {
   Command::new("curl")
     .args(["-L", url])
     .args(["-o", destination])
     .status()
-    .map_err(Error::from)
+    .map_err(Into::into)
 }
 
 /// Makes an HTTP request to obtain the latest version available online,
 /// automatically choosing the correct URL for the current platform.
-pub fn check_latest_version() -> Result<String> {
+pub fn check_latest_version() -> anyhow::Result<String> {
   let string = curl(ffmpeg_manifest_url()?)?;
 
   if cfg!(target_os = "windows") {
     Ok(string)
   } else if cfg!(target_os = "macos") {
-    Ok(parse_macos_version(&string).ok_or("failed to parse version number (macos variant)")?)
+    parse_macos_version(&string).context("failed to parse version number (macos variant)")
   } else if cfg!(target_os = "linux") {
-    Ok(parse_linux_version(&string).ok_or("failed to parse version number (linux variant)")?)
+    parse_linux_version(&string).context("failed to parse version number (linux variant)")
   } else {
-    Err(Error::msg("Unsupported platform"))
+    Err(anyhow::Error::msg("Unsupported platform"))
   }
 }
 
 /// Invoke `curl` to download an archive (ZIP on windows, TAR on linux and mac)
 /// from the latest published release online.
-pub fn download_ffmpeg_package(url: &str, download_dir: &Path) -> Result<PathBuf> {
+pub fn download_ffmpeg_package(url: &str, download_dir: &Path) -> anyhow::Result<PathBuf> {
   let filename = Path::new(url)
     .file_name()
-    .ok_or_else(|| Error::msg("Failed to get filename"))?;
+    .context("Failed to get filename")?;
 
   let archive_path = download_dir.join(filename);
 
-  let archive_filename = archive_path.to_str().ok_or("invalid download path")?;
+  let archive_filename = archive_path
+    .to_str()
+    .context("invalid download path")?;
 
   let exit_status = curl_to_file(url, archive_filename)?;
 
   if !exit_status.success() {
-    return Err(Error::msg("Failed to download ffmpeg"));
+    anyhow::bail!("Failed to download ffmpeg");
   }
 
   Ok(archive_path)
@@ -179,7 +174,7 @@ pub fn download_ffmpeg_package(url: &str, download_dir: &Path) -> Result<PathBuf
 
 /// After downloading, unpacks the archive to a folder, moves the binaries to
 /// their final location, and deletes the archive and temporary folder.
-pub fn unpack_ffmpeg(from_archive: &PathBuf, binary_folder: &Path) -> Result<()> {
+pub fn unpack_ffmpeg(from_archive: &PathBuf, binary_folder: &Path) -> anyhow::Result<()> {
   let temp_dirname = UNPACK_DIRNAME;
   let temp_folder = binary_folder.join(temp_dirname);
   create_dir_all(&temp_folder)?;
@@ -192,13 +187,13 @@ pub fn unpack_ffmpeg(from_archive: &PathBuf, binary_folder: &Path) -> Result<()>
     .status()?
     .success()
     .then_some(())
-    .ok_or("Failed to unpack ffmpeg")?;
+    .context("Failed to unpack ffmpeg")?;
 
   // Move binaries
   let (ffmpeg, ffplay, ffprobe) = if cfg!(target_os = "windows") {
     let inner_folder = read_dir(&temp_folder)?
       .next()
-      .ok_or("Failed to get inner folder")??;
+      .context("Failed to get inner folder")??;
     (
       inner_folder.path().join("bin/ffmpeg.exe"),
       inner_folder.path().join("bin/ffplay.exe"),
@@ -207,7 +202,7 @@ pub fn unpack_ffmpeg(from_archive: &PathBuf, binary_folder: &Path) -> Result<()>
   } else if cfg!(target_os = "linux") {
     let inner_folder = read_dir(&temp_folder)?
       .next()
-      .ok_or("Failed to get inner folder")??;
+      .context("Failed to get inner folder")??;
     (
       inner_folder.path().join("./ffmpeg"),
       inner_folder.path().join("./ffplay"), // <- no ffplay on linux
@@ -220,18 +215,28 @@ pub fn unpack_ffmpeg(from_archive: &PathBuf, binary_folder: &Path) -> Result<()>
       temp_folder.join("ffprobe"), // <-- no ffprobe on mac
     )
   } else {
-    return Err(Error::msg("Unsupported platform"));
+    anyhow::bail!("Unsupported platform");
   };
 
   // Move binaries
-  rename(&ffmpeg, binary_folder.join(ffmpeg.file_name().ok_or(())?))?;
+  let move_bin = |path: &Path| {
+    let file_name = binary_folder.join(
+      path
+        .file_name()
+        .with_context(|| format!("Path {} does not have a file_name", path.to_string_lossy()))?,
+    );
+    rename(path, file_name)?;
+    anyhow::Ok(())
+  };
+
+  move_bin(&ffmpeg)?;
 
   if ffprobe.exists() {
-    rename(&ffprobe, binary_folder.join(ffprobe.file_name().ok_or(())?))?;
+    move_bin(&ffprobe)?;
   }
 
   if ffplay.exists() {
-    rename(&ffplay, binary_folder.join(ffplay.file_name().ok_or(())?))?;
+    move_bin(&ffplay)?;
   }
 
   // Delete archive and unpacked files
diff --git a/src/error.rs b/src/error.rs
deleted file mode 100644
index 8b1e90f..0000000
--- a/src/error.rs
+++ /dev/null
@@ -1,97 +0,0 @@
-use std::error::Error as StdError;
-use std::fmt::{Display, Formatter};
-use std::io;
-use std::result::Result as StdResult;
-use std::str::Utf8Error;
-use std::string::FromUtf8Error;
-
-/// Shorthand alias for `Result<T, Error>` using `ffmpeg_sidecar` error type.
-pub type Result<T> = StdResult<T, Error>;
-
-/// A generic error type for the `ffmpeg-sidecar` crate.
-#[derive(Debug)]
-pub struct Error {
-  pub message: String,
-  pub source: Option<Box<dyn StdError + 'static>>,
-}
-
-impl Display for Error {
-  fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
-    write!(f, "{}", self.message)
-  }
-}
-
-impl StdError for Error {
-  fn source(&self) -> Option<&(dyn StdError + 'static)> {
-    self.source.as_deref()
-  }
-}
-
-impl Error {
-  /// Wrap any standard Error into a library Error.
-  /// Similar to [`anyhow`](https://github.com/dtolnay/anyhow/blob/master/src/error.rs#L88).
-  pub fn from_std<E>(e: E) -> Self
-  where
-    E: StdError + 'static,
-  {
-    Error {
-      message: e.to_string(),
-      source: Some(Box::new(e)),
-    }
-  }
-
-  /// Wrap any Display into a library Error.
-  pub fn from_display<E>(e: E) -> Self
-  where
-    E: Display,
-  {
-    Error {
-      message: e.to_string(),
-      source: None,
-    }
-  }
-
-  /// Create an error message from a string.
-  pub fn msg<S: AsRef<str>>(message: S) -> Self {
-    Error {
-      message: message.as_ref().to_string(),
-      source: None,
-    }
-  }
-}
-
-impl From<io::Error> for Error {
-  fn from(e: io::Error) -> Self {
-    Error::from_std(e)
-  }
-}
-
-impl From<Utf8Error> for Error {
-  fn from(e: Utf8Error) -> Self {
-    Error::from_std(e)
-  }
-}
-
-impl From<FromUtf8Error> for Error {
-  fn from(e: FromUtf8Error) -> Self {
-    Error::from_std(e)
-  }
-}
-
-impl From<&str> for Error {
-  fn from(e: &str) -> Self {
-    Error::from_display(e)
-  }
-}
-
-impl From<String> for Error {
-  fn from(e: String) -> Self {
-    Error::from_display(e)
-  }
-}
-
-impl From<()> for Error {
-  fn from(_: ()) -> Self {
-    Error::from_display("empty error")
-  }
-}
diff --git a/src/ffprobe.rs b/src/ffprobe.rs
index 2ccccc6..4ff2fbd 100644
--- a/src/ffprobe.rs
+++ b/src/ffprobe.rs
@@ -1,10 +1,11 @@
-use crate::error::{Error, Result};
 use std::{env::current_exe, ffi::OsStr, path::PathBuf};
 use std::{
   path::Path,
   process::{Command, Stdio},
 };
 
+use anyhow::Context;
+
 /// Returns the path of the downloaded FFprobe executable, or falls back to
 /// assuming its installed in the system path. Note that not all FFmpeg
 /// distributions include FFprobe.
@@ -23,10 +24,10 @@ pub fn ffprobe_path() -> PathBuf {
 ///
 /// The extension between platforms, with Windows using `.exe`, while Mac and
 /// Linux have no extension.
-pub fn ffprobe_sidecar_path() -> Result<PathBuf> {
+pub fn ffprobe_sidecar_path() -> anyhow::Result<PathBuf> {
   let mut path = current_exe()?
     .parent()
-    .ok_or("Can't get parent of current_exe")?
+    .context("Can't get parent of current_exe")?
     .join("ffprobe");
   if cfg!(windows) {
     path.set_extension("exe");
@@ -35,18 +36,18 @@ pub fn ffprobe_sidecar_path() -> Result<PathBuf> {
 }
 
 /// Alias for `ffprobe -version`, parsing the version number and returning it.
-pub fn ffprobe_version() -> Result<String> {
+pub fn ffprobe_version() -> anyhow::Result<String> {
   ffprobe_version_with_path(ffprobe_path())
 }
 
 /// Lower level variant of `ffprobe_version` that exposes a customized the path
 /// to the ffmpeg binary.
-pub fn ffprobe_version_with_path<S: AsRef<OsStr>>(path: S) -> Result<String> {
+pub fn ffprobe_version_with_path<S: AsRef<OsStr>>(path: S) -> anyhow::Result<String> {
   let output = Command::new(&path).arg("-version").output()?;
 
   // note:version parsing is not implemented for ffprobe
 
-  String::from_utf8(output.stdout).map_err(Error::from)
+  Ok(String::from_utf8(output.stdout)?)
 }
 
 /// Verify whether ffprobe is installed on the system. This will return true if
diff --git a/src/iter.rs b/src/iter.rs
index a771cb0..db0a889 100644
--- a/src/iter.rs
+++ b/src/iter.rs
@@ -5,9 +5,10 @@ use std::{
   thread::JoinHandle,
 };
 
+use anyhow::Context;
+
 use crate::{
   child::FfmpegChild,
-  error::{Error, Result},
   event::{AVStream, FfmpegEvent, FfmpegOutput, FfmpegProgress, LogLevel, OutputVideoFrame},
   log_parser::FfmpegLogParser,
   metadata::FfmpegMetadata,
@@ -23,8 +24,8 @@ pub struct FfmpegIterator {
 }
 
 impl FfmpegIterator {
-  pub fn new(child: &mut FfmpegChild) -> Result<Self> {
-    let stderr = child.take_stderr().ok_or_else(|| Error::msg("No stderr channel\n - Did you call `take_stderr` elsewhere?\n - Did you forget to call `.stderr(Stdio::piped)` on the `ChildProcess`?"))?;
+  pub fn new(child: &mut FfmpegChild) -> anyhow::Result<Self> {
+    let stderr = child.take_stderr().context("No stderr channel\n - Did you call `take_stderr` elsewhere?\n - Did you forget to call `.stderr(Stdio::piped)` on the `ChildProcess`?")?;
     let (tx, rx) = sync_channel::<FfmpegEvent>(0);
     spawn_stderr_thread(stderr, tx.clone());
     let stdout = child.take_stdout();
@@ -40,19 +41,19 @@ impl FfmpegIterator {
   /// Called after all metadata has been obtained to spawn the thread that will
   /// handle output. The metadata is needed to determine the output format and
   /// other parameters.
-  fn start_stdout(&mut self) -> Result<()> {
+  fn start_stdout(&mut self) -> anyhow::Result<()> {
     // No output detected
     if self.metadata.output_streams.is_empty() || self.metadata.outputs.is_empty() {
-      let err = Error::msg("No output streams found");
+      let err = "No output streams found";
       self.tx.take(); // drop the tx so that the channel closes
-      return Err(err);
+      anyhow::bail!(err)
     }
 
     // Handle stdout
     if let Some(stdout) = self.stdout.take() {
       spawn_stdout_thread(
         stdout,
-        self.tx.take().ok_or("missing channel tx")?,
+        self.tx.take().context("missing channel tx")?,
         self.metadata.output_streams.clone(),
         self.metadata.outputs.clone(),
       );
@@ -62,7 +63,7 @@ impl FfmpegIterator {
   }
 
   /// Advance the iterator until all metadata has been collected, returning it.
-  pub fn collect_metadata(&mut self) -> Result<FfmpegMetadata> {
+  pub fn collect_metadata(&mut self) -> anyhow::Result<FfmpegMetadata> {
     let mut event_queue: Vec<FfmpegEvent> = Vec::new();
 
     while !self.metadata.is_completed() {
@@ -78,11 +79,10 @@ impl FfmpegIterator {
             })
             .collect::<Vec<String>>()
             .join("");
-          let msg = format!(
-            "Iterator ran out before metadata was gathered. The following errors occurred: {}",
-            errors
-          );
-          return Err(Error::msg(msg));
+
+          anyhow::bail!(
+            "Iterator ran out before metadata was gathered. The following errors occurred: {errors}",
+          )
         }
       }
     }
diff --git a/src/lib.rs b/src/lib.rs
index b8d09e7..170b0a0 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -3,9 +3,9 @@
 //! ## Example
 //!
 //! ```rust
-//! use ffmpeg_sidecar::{command::FfmpegCommand, error::Result, event::FfmpegEvent};
+//! use ffmpeg_sidecar::{command::FfmpegCommand, event::FfmpegEvent};
 //!
-//! fn main() -> Result<()> {
+//! fn main() -> anyhow::Result<()> {
 //!   FfmpegCommand::new() // <- Builder API like `std::process::Command`
 //!     .testsrc()  // <- Discoverable aliases for FFmpeg args
 //!     .rawvideo() // <- Convenient argument presets
@@ -38,7 +38,6 @@ pub mod child;
 pub mod comma_iter;
 pub mod command;
 pub mod download;
-pub mod error;
 pub mod event;
 pub mod ffprobe;
 pub mod iter;
diff --git a/src/log_parser.rs b/src/log_parser.rs
index 1ea6aa8..6698636 100644
--- a/src/log_parser.rs
+++ b/src/log_parser.rs
@@ -5,7 +5,6 @@ use std::{
 
 use crate::{
   comma_iter::CommaIter,
-  error::{Error, Result},
   event::{
     AVStream, FfmpegConfiguration, FfmpegDuration, FfmpegEvent, FfmpegInput, FfmpegOutput,
     FfmpegProgress, FfmpegVersion, LogLevel,
@@ -38,14 +37,14 @@ impl<R: Read> FfmpegLogParser<R> {
   /// - `\n` (MacOS),
   /// - `\r\n` (Windows)
   /// - `\r` (Windows, progress updates which overwrite the previous line)
-  pub fn parse_next_event(&mut self) -> Result<FfmpegEvent> {
+  pub fn parse_next_event(&mut self) -> anyhow::Result<FfmpegEvent> {
     let mut buf = Vec::<u8>::new();
     let bytes_read = read_until_any(&mut self.reader, &[b'\r', b'\n'], &mut buf);
     let line = from_utf8(buf.as_slice())?.trim();
     let raw_log_message = line.to_string();
-    match bytes_read {
-      Ok(0) => Ok(FfmpegEvent::LogEOF),
-      Ok(_) => {
+    match bytes_read? {
+      0 => Ok(FfmpegEvent::LogEOF),
+      _ => {
         // Track log section
         if let Some(input_number) = try_parse_input(line) {
           self.cur_section = LogSection::Input(input_number);
@@ -87,7 +86,7 @@ impl<R: Read> FfmpegLogParser<R> {
           match self.cur_section {
             LogSection::Input(_) => Ok(FfmpegEvent::ParsedInputStream(stream)),
             LogSection::Output(_) => Ok(FfmpegEvent::ParsedOutputStream(stream)),
-            LogSection::Other | LogSection::StreamMapping => Err(Error::msg(format!(
+            LogSection::Other | LogSection::StreamMapping => Err(anyhow::Error::msg(format!(
               "Unexpected stream specification: {}",
               line
             ))),
@@ -107,7 +106,6 @@ impl<R: Read> FfmpegLogParser<R> {
           Ok(FfmpegEvent::Log(LogLevel::Unknown, line.to_string()))
         }
       }
-      Err(e) => Err(Error::from_std(e)),
     }
   }
 
diff --git a/src/metadata.rs b/src/metadata.rs
index 6eeca7b..8a5bb08 100644
--- a/src/metadata.rs
+++ b/src/metadata.rs
@@ -1,4 +1,3 @@
-use crate::error::Result;
 use crate::event::{AVStream, FfmpegEvent, FfmpegInput, FfmpegOutput};
 
 #[derive(Debug, Clone, PartialEq)]
@@ -44,9 +43,9 @@ impl FfmpegMetadata {
     self.inputs[0].duration
   }
 
-  pub fn handle_event(&mut self, item: &Option<FfmpegEvent>) -> Result<()> {
+  pub fn handle_event(&mut self, item: &Option<FfmpegEvent>) -> anyhow::Result<()> {
     if self.is_completed() {
-      return Err("Metadata is already completed".into());
+      anyhow::bail!("Metadata is already completed")
     }
 
     match item {
diff --git a/src/paths.rs b/src/paths.rs
index 0721584..c36adfb 100644
--- a/src/paths.rs
+++ b/src/paths.rs
@@ -1,9 +1,10 @@
-use crate::error::Result;
 use std::{
   env::current_exe,
   path::{Path, PathBuf},
 };
 
+use anyhow::Context;
+
 /// Returns the default path of the FFmpeg executable, to be used as the
 /// argument to `Command::new`. It should first attempt to locate an FFmpeg
 /// binary adjacent to the Rust executable. If that fails, it should invoke
@@ -25,10 +26,10 @@ pub fn ffmpeg_path() -> PathBuf {
 ///
 /// The extension between platforms, with Windows using `.exe`, while Mac and
 /// Linux have no extension.
-pub fn sidecar_path() -> Result<PathBuf> {
+pub fn sidecar_path() -> anyhow::Result<PathBuf> {
   let mut path = current_exe()?
     .parent()
-    .ok_or("Can't get parent of current_exe")?
+    .context("Can't get parent of current_exe")?
     .join("ffmpeg");
   if cfg!(windows) {
     path.set_extension("exe");
@@ -37,11 +38,11 @@ pub fn sidecar_path() -> Result<PathBuf> {
 }
 
 /// By default, downloads all temporary files to the same directory as the Rust executable.
-pub fn sidecar_dir() -> Result<PathBuf> {
+pub fn sidecar_dir() -> anyhow::Result<PathBuf> {
   Ok(
     sidecar_path()?
       .parent()
-      .ok_or("invalid sidecar path")?
+      .context("invalid sidecar path")?
       .to_path_buf(),
   )
 }
diff --git a/src/version.rs b/src/version.rs
index 4066fe6..0216f67 100644
--- a/src/version.rs
+++ b/src/version.rs
@@ -1,5 +1,6 @@
+use anyhow::Context;
+
 use crate::{
-  error::{Error, Result},
   event::FfmpegEvent,
   log_parser::FfmpegLogParser,
   paths::ffmpeg_path,
@@ -8,18 +9,18 @@ use std::ffi::OsStr;
 use std::process::{Command, Stdio};
 
 /// Alias for `ffmpeg -version`, parsing the version number and returning it.
-pub fn ffmpeg_version() -> Result<String> {
+pub fn ffmpeg_version() -> anyhow::Result<String> {
   ffmpeg_version_with_path(ffmpeg_path())
 }
 
 /// Lower level variant of `ffmpeg_version` that exposes a customized the path
 /// to the ffmpeg binary.
-pub fn ffmpeg_version_with_path<S: AsRef<OsStr>>(path: S) -> Result<String> {
+pub fn ffmpeg_version_with_path<S: AsRef<OsStr>>(path: S) -> anyhow::Result<String> {
   let mut cmd = Command::new(&path)
     .arg("-version")
     .stdout(Stdio::piped()) // not stderr when calling `-version`
     .spawn()?;
-  let stdout = cmd.stdout.take().ok_or("No standard output channel")?;
+  let stdout = cmd.stdout.take().context("No standard output channel")?;
   let mut parser = FfmpegLogParser::new(stdout);
 
   let mut version: Option<String> = None;
@@ -32,7 +33,7 @@ pub fn ffmpeg_version_with_path<S: AsRef<OsStr>>(path: S) -> Result<String> {
   }
   let exit_status = cmd.wait()?;
   if !exit_status.success() {
-    return Err(Error::msg("ffmpeg -version exited with non-zero status"));
+    anyhow::bail!("ffmpeg -version exited with non-zero status");
   }
-  version.ok_or_else(|| Error::msg("Failed to parse ffmpeg version"))
+  version.context("Failed to parse ffmpeg version")
 }


Test patch: