needle/
lib.rs

1#![deny(missing_docs)]
2
3//! # needle
4//!
5//! needle detects openings/intros and endings/credits across video files. needle can be used standalone
6//! via a dedicated CLI, or as a library to implement higher-level tools or plugins (e.g., for intro skipping).
7//!
8//! The library exposes two central structs:
9//!
10//! 1. [Analyzer](crate::audio::Analyzer): Decodes one or more videos and converts them into a set of [FrameHashes](crate::audio::FrameHashes).
11//! 2. [Comparator](crate::audio::Comparator): Searches for openings and endings across two or more videos.
12//!
13//! ## Basic Usage
14//!
15//! First, you need to create and run an [Analyzer](crate::audio::Analyzer).
16//!
17//! This will decode the audio streams for all provided video files and return a list of [FrameHashes](audio::FrameHashes), one per video. The structure
18//! stores a compressed representation of the audio stream that contains _only_ the data we need to search for openings and endings.
19//!
20//! ```
21//! use std::path::PathBuf;
22//! use needle::audio::Analyzer;
23//! # fn get_sample_paths() -> Vec<PathBuf> {
24//! #     let resources = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("resources");
25//! #     vec![
26//! #         resources.join("sample-5s.mp4"),
27//! #         resources.join("sample-shifted-4s.mp4"),
28//! #     ]
29//! # }
30//!
31//! let video_paths: Vec<PathBuf> = get_sample_paths();
32//! let analyzer = Analyzer::from_files(video_paths, false, false);
33//!
34//! // Use a `hash_period` of 1.0, `hash_duration` of 3.0, do not `persist` frame hash data
35//! // and enable `threading`.
36//! let frame_hashes = analyzer.run(1.0, 3.0, false, true).unwrap();
37//! ```
38//!
39//! Now you need to create and run a [Comparator](crate::audio::Comparator) using the output [FrameHashes](audio::FrameHashes). You can re-use
40//! the videos by constructing an instance from the [Analyzer](crate::audio::Analyzer).
41//!
42//! ```
43//! use std::path::PathBuf;
44//! use needle::audio::Comparator;
45//! # use needle::audio::Analyzer;
46//! # fn get_sample_paths() -> Vec<PathBuf> {
47//! #     let resources = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("resources");
48//! #     vec![
49//! #         resources.join("sample-5s.mp4"),
50//! #         resources.join("sample-shifted-4s.mp4"),
51//! #     ]
52//! # }
53//! # let video_paths: Vec<PathBuf> = get_sample_paths();
54//! # let analyzer = Analyzer::from_files(video_paths, false, false);
55//! # let frame_hashes = analyzer.run(1.0, 3.0, false, true).unwrap();
56//!
57//! let comparator: Comparator = analyzer.into();
58//! let results = comparator.run_with_frame_hashes(frame_hashes, true, false, false, true).unwrap();
59//!
60//! dbg!(results);
61//! // {
62//! //     "/tmp/land-of-lustrous-ep1.mkv": SearchResult {
63//! //         opening: None,
64//! //         ending: Some(
65//! //             (
66//! //                 1331.664387072s,
67//! //                 1419.024930474s,
68//! //             ),
69//! //         ),
70//! //     },
71//! //     "/tmp/land-of-lustrous-ep2.mkv": SearchResult {
72//! //         opening: Some(
73//! //             (
74//! //                 44.718820458s,
75//! //                 131.995463634s,
76//! //             ),
77//! //         ),
78//! //         ending: Some(
79//! //             (
80//! //                 1331.664387072s,
81//! //                 1436.560077708s,
82//! //             ),
83//! //         ),
84//! //     },
85//! //     "/tmp/land-of-lustrous-ep3.mkv": SearchResult {
86//! //         opening: Some(
87//! //             (
88//! //                 41.11111074s,
89//! //                 127.800452334s,
90//! //             ),
91//! //         ),
92//! //         ending: Some(
93//! //             (
94//! //                 1331.664387072s,
95//! //                 1436.560077708s,
96//! //             ),
97//! //         ),
98//! //     },
99//! // },
100//! ```
101//!
102//! [Comparator::run_with_frame_hashes](crate::audio::Comparator::run_with_frame_hashes) runs a search for openings and endings
103//! using the provided frame hash data. Note that there is an equivalent method that can read existing frame hash data files from disk
104//! ([Comparator::run](crate::audio::Comparator::run)).
105//!
106//! The output of this method is a map from each video file to a [SearchResult](crate::audio::SearchResult). This
107//! structure contains the actual times for any detected openings and endings.
108
109use std::path::PathBuf;
110
111/// Detects opening and endings across videos using just audio streams.
112pub mod audio;
113#[cfg(feature = "video")]
114/// Detects opening and endings across videos using just video streams.
115pub mod video;
116/// Common utility functions.
117pub mod util;
118
119/// Common error type.
120#[derive(thiserror::Error, Debug)]
121pub enum Error {
122    /// Frame hash data was not found on disk.
123    #[error("frame hash data not found at: {0:?}")]
124    FrameHashDataNotFound(PathBuf),
125    /// No paths were provided to the [crate::audio::Analyzer].
126    #[error("no paths provided to analyzer")]
127    AnalyzerMissingPaths,
128    /// Invalid path.
129    #[error("path does not exist: {0:?}")]
130    PathNotFound(PathBuf),
131    /// Wraps [ffmpeg_next::Error].
132    #[error("FFmpeg error: {0}")]
133    FFmpegError(#[from] ffmpeg_next::Error),
134    /// Wraps [bincode::Error].
135    #[error("bincode error: {0}")]
136    BincodeError(#[from] bincode::Error),
137    /// Wraps [serde_json::Error].
138    #[error("serde_json error: {0}")]
139    SerdeJSONError(#[from] serde_json::Error),
140    /// Wraps [std::io::Error].
141    #[error("IO error: {0}")]
142    IOError(#[from] std::io::Error),
143}
144
145/// Common result type.
146pub type Result<T> = std::result::Result<T, Error>;