1mod cutlist;
6mod ffmpeg;
7mod info;
8mod interval;
9
10use anyhow::{anyhow, Context};
11use log::*;
12use std::{
13 error::Error,
14 fmt::{self, Debug, Display},
15 path::Path,
16 str,
17};
18use which::which;
19
20use super::dirs::tmp_dir;
21use cutlist::Cutlist;
22use info::Metadata;
23
24pub use cutlist::{
25 AccessType as CutlistAccessType, Ctrl as CutlistCtrl, Rating as CutlistRating, ID as CutlistID,
26};
27
28#[derive(Debug, Default)]
31pub enum CutError {
32 Any(anyhow::Error),
33 #[default]
34 Default,
35 NoCutlist,
36 CutlistSubmissionFailed(anyhow::Error),
37}
38impl fmt::Display for CutError {
40 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
41 match *self {
42 CutError::Any(ref source) => write!(f, "Error: {}", source),
43 CutError::Default => write!(f, "Default cut error"),
44 CutError::NoCutlist => write!(f, "No cut list exists"),
45 CutError::CutlistSubmissionFailed(ref source) => {
46 write!(f, "Submission of cut list to cutlist.at failed: {}", source)
47 }
48 }
49 }
50}
51impl Error for CutError {}
53impl From<anyhow::Error> for CutError {
55 fn from(err: anyhow::Error) -> CutError {
56 CutError::Any(err)
57 }
58}
59
60pub fn cut<P, Q>(in_video: P, out_video: Q, cutlist_ctrl: &CutlistCtrl) -> Result<(), CutError>
76where
77 P: AsRef<Path>,
78 Q: AsRef<Path>,
79{
80 check_dependencies()?;
82
83 let tmp_dir = tmp_dir()?;
84
85 match cutlist_ctrl.access_type {
88 cutlist::AccessType::Direct(intervals) => cut_with_cutlist_from_intervals(
89 in_video,
90 out_video,
91 tmp_dir,
92 intervals,
93 cutlist_ctrl.submit,
94 cutlist_ctrl.access_token,
95 cutlist_ctrl.rating,
96 ),
97 cutlist::AccessType::File(file) => {
98 cut_with_cutlist_from_file(in_video, out_video, tmp_dir, file)
99 }
100 cutlist::AccessType::ID(id) => {
101 cut_with_cutlist_from_provider_by_id(in_video, out_video, tmp_dir, id)
102 }
103 _ => cut_with_cutlist_from_provider_auto_select(
104 in_video,
105 out_video,
106 tmp_dir,
107 cutlist_ctrl.min_rating,
108 ),
109 }
110}
111
112fn check_dependencies() -> anyhow::Result<()> {
116 if which("ffmpeg").is_err() {
118 return Err(anyhow!("ffmpeg must be installed and in the path"));
119 }
120
121 if which("ffmsindex").is_err() {
123 return Err(anyhow!("ffmsindex must be installed and in the path"));
124 }
125
126 Ok(())
127}
128
129fn cut_with_cutlist_from_file<P, Q, R, T>(
132 in_video: P,
133 out_video: Q,
134 tmp_dir: T,
135 cutlist_path: R,
136) -> Result<(), CutError>
137where
138 P: AsRef<Path>,
139 Q: AsRef<Path>,
140 R: AsRef<Path>,
141 T: AsRef<Path>,
142{
143 trace!(
144 "Cutting \"{}\" with cut list from \"{}\"",
145 in_video.as_ref().display(),
146 cutlist_path.as_ref().display()
147 );
148
149 let cutlist = Cutlist::try_from(cutlist_path.as_ref())?;
150
151 match cut_with_cutlist(in_video, out_video, tmp_dir, &cutlist).context(format!(
152 "Could not cut video with cut list from \"{}\"",
153 cutlist_path.as_ref().display()
154 )) {
155 Err(err) => Err(CutError::Any(err)),
156 _ => Ok(()),
157 }
158}
159
160fn cut_with_cutlist_from_intervals<P, Q, T, I>(
165 in_video: P,
166 out_video: Q,
167 tmp_dir: T,
168 intervals: I,
169 submit_cutlists: bool,
170 cutlist_at_access_token: Option<&str>,
171 rating: CutlistRating,
172) -> Result<(), CutError>
173where
174 P: AsRef<Path>,
175 Q: AsRef<Path>,
176 T: AsRef<Path>,
177 I: AsRef<str> + Display,
178{
179 trace!("Cutting \"{}\" with intervals", in_video.as_ref().display());
180
181 let mut cutlist = Cutlist::try_from_intervals(intervals.as_ref())?;
182
183 if let Err(err) = cut_with_cutlist(
184 in_video.as_ref(),
185 out_video.as_ref(),
186 tmp_dir.as_ref(),
187 &cutlist,
188 ) {
189 return Err(CutError::Any(
190 err.context(format!("Could not cut video with {}", intervals)),
191 ));
192 }
193
194 if submit_cutlists {
196 return match cutlist_at_access_token {
197 Some(access_token) => {
199 if let Err(err) = cutlist.submit(in_video, tmp_dir, access_token, rating) {
200 Err(CutError::CutlistSubmissionFailed(err))
201 } else {
202 Ok(())
203 }
204 }
205 None => Err(CutError::CutlistSubmissionFailed(anyhow!(
206 "No access token for cutlist.at maintained in configuration file"
207 ))),
208 };
209 }
210
211 Ok(())
212}
213
214fn cut_with_cutlist_from_provider_by_id<P, Q, T>(
218 in_video: P,
219 out_video: Q,
220 tmp_dir: T,
221 id: u64,
222) -> Result<(), CutError>
223where
224 P: AsRef<Path>,
225 Q: AsRef<Path>,
226 T: AsRef<Path>,
227{
228 trace!(
229 "Cutting \"{}\" with cut list id {} from provider",
230 in_video.as_ref().display(),
231 id
232 );
233
234 match Cutlist::try_from(id) {
236 Ok(cutlist) => match cut_with_cutlist(in_video, out_video, tmp_dir, &cutlist) {
237 Ok(_) => Ok(()),
238 Err(err) => Err(CutError::Any(
239 anyhow!(err).context(format!("Could not cut video with cut list {}", id)),
240 )),
241 },
242 Err(err) => Err(CutError::Any(
243 anyhow!(err).context(format!("Could not retrieve cut list ID={}", id)),
244 )),
245 }
246}
247
248fn cut_with_cutlist_from_provider_auto_select<P, Q, T>(
254 in_video: P,
255 out_video: Q,
256 tmp_dir: T,
257 min_cutlist_rating: Option<CutlistRating>,
258) -> Result<(), CutError>
259where
260 P: AsRef<Path>,
261 Q: AsRef<Path>,
262 T: AsRef<Path>,
263{
264 let file_name = in_video.as_ref().file_name().unwrap().to_str().unwrap();
265
266 let headers: Vec<cutlist::ProviderHeader> =
268 match cutlist::headers_from_provider(file_name, min_cutlist_rating)
269 .context("Could not retrieve cut lists")
270 {
271 Ok(hdrs) => hdrs,
272 _ => return Err(CutError::NoCutlist),
273 };
274
275 let mut is_cut = false;
277 for header in headers {
278 match Cutlist::try_from(header.id()) {
279 Ok(cutlist) => {
280 match cut_with_cutlist(
281 in_video.as_ref(),
282 out_video.as_ref(),
283 tmp_dir.as_ref(),
284 &cutlist,
285 ) {
286 Ok(_) => {
287 is_cut = true;
288 break;
289 }
290 Err(err) => {
291 error!(
292 "{:?}",
293 anyhow!(err).context(format!(
294 "Could not cut video with cut list ID={}",
295 header.id()
296 ))
297 );
298 }
299 }
300 }
301 Err(err) => {
302 error!(
303 "{:?}",
304 anyhow!(err)
305 .context(format!("Could not retrieve cut list ID={}", header.id(),))
306 );
307 }
308 }
309 }
310
311 if !is_cut {
312 return Err(CutError::Any(anyhow!(
313 "No cut list could be successfully applied to cut video"
314 )));
315 }
316
317 Ok(())
318}
319
320fn cut_with_cutlist<I, O, T>(
323 in_video: I,
324 out_video: O,
325 tmp_dir: T,
326 cutlist: &Cutlist,
327) -> anyhow::Result<()>
328where
329 I: AsRef<Path>,
330 O: AsRef<Path>,
331 T: AsRef<Path>,
332{
333 trace!("Cutting video with ffmpeg ...");
334
335 let metadata = Metadata::new(&in_video)?;
337
338 if cutlist.has_frame_intervals() {
343 if !metadata.has_frames() {
344 trace!("Since video has no frames, frame-based cut intervals cannot be used");
345 } else if let Err(err) = ffmpeg::cut(
346 &in_video,
347 &out_video,
348 &tmp_dir,
349 cutlist.frame_intervals()?,
350 &metadata,
351 ) {
352 warn!(
353 "Could not cut \"{}\" with frame intervals: {:?}",
354 in_video.as_ref().display(),
355 err
356 );
357 } else {
358 trace!("Cut video with ffmpeg");
359
360 return Ok(());
361 }
362 }
363
364 if cutlist.has_time_intervals() {
366 if let Err(err) = ffmpeg::cut(
367 &in_video,
368 &out_video,
369 &tmp_dir,
370 cutlist.time_intervals()?,
371 &metadata,
372 ) {
373 warn!(
374 "Could not cut \"{}\" with time intervals: {:?}",
375 in_video.as_ref().display(),
376 err
377 );
378 } else {
379 trace!("Cut video with ffmpeg");
380 return Ok(());
381 }
382 }
383
384 Err(anyhow!("Could not cut video with ffmpeg"))
385}