bilibili_extractor_lib/
combiner.rs

1use crate::{
2    error::Result,
3    metadata::{NormalEpisodeMetadata, SpecialEpisodeMetadata},
4    subtitle::SubtitleType,
5};
6use std::{
7    ffi::OsStr,
8    path::Path,
9    process::{Command, ExitStatus},
10};
11
12pub trait Combinable {
13    /// Combine the audio, video, and subtitle using `ffmpeg`.
14    fn combine(
15        &self,
16        subtitle_path: impl AsRef<Path>,
17        subtitle_language: &str,
18        subtitle_type: SubtitleType,
19    ) -> Result<ExitStatus>;
20}
21
22impl<P: AsRef<Path>> Combinable for NormalEpisodeMetadata<P> {
23    fn combine(
24        &self,
25        subtitle_path: impl AsRef<Path>,
26        subtitle_language: &str,
27        subtitle_type: SubtitleType,
28    ) -> Result<ExitStatus> {
29        let video_path = self
30            .path
31            .as_ref()
32            .ok_or(format!(
33                "Episode {} of {} doesn't have a path.",
34                self.episode, self.title
35            ))?
36            .as_ref()
37            .join(&self.type_tag)
38            .join("video.m4s");
39
40        let audio_path = self
41            .path
42            .as_ref()
43            .unwrap()
44            .as_ref()
45            .join(&self.type_tag)
46            .join("audio.m4s");
47
48        let output_path = self
49            .path
50            .as_ref()
51            .unwrap()
52            .as_ref()
53            .join(&self.type_tag)
54            .join("episode.mkv");
55
56        let mut binding = Command::new("ffmpeg");
57        binding
58            .arg("-y")
59            .args(["-hide_banner", "-loglevel", "error"]) // silent operation
60            .args([OsStr::new("-i"), video_path.as_os_str()])
61            .args([OsStr::new("-i"), audio_path.as_os_str()]);
62
63        match subtitle_type {
64            SubtitleType::Hard => Ok(binding
65                .args([
66                    "-vf",
67                    &format!("subtitles={}", subtitle_path.as_ref().display()),
68                ])
69                .arg(output_path)
70                .status()?),
71            SubtitleType::Soft => Ok(binding
72                .args([OsStr::new("-i"), subtitle_path.as_ref().as_os_str()])
73                .args(["-map", "0"])
74                .args(["-map", "1:a:0"])
75                .args(["-map", "2"])
76                .args(["-metadata:s:s:0", &format!("language={subtitle_language}")])
77                .args(["-codec", "copy"])
78                .arg(output_path)
79                .status()?),
80        }
81    }
82}
83
84impl<P: AsRef<Path>> Combinable for SpecialEpisodeMetadata<P> {
85    fn combine(
86        &self,
87        subtitle_path: impl AsRef<Path>,
88        subtitle_language: &str,
89        subtitle_type: SubtitleType,
90    ) -> Result<ExitStatus> {
91        let video_path = self
92            .path
93            .as_ref()
94            .ok_or(format!(
95                "{} of {} doesn't have a path.",
96                self.episode_name, self.title
97            ))?
98            .as_ref()
99            .join(&self.type_tag)
100            .join("video.m4s");
101
102        let audio_path = self
103            .path
104            .as_ref()
105            .unwrap()
106            .as_ref()
107            .join(&self.type_tag)
108            .join("audio.m4s");
109
110        let output_path = self
111            .path
112            .as_ref()
113            .unwrap()
114            .as_ref()
115            .join(&self.type_tag)
116            .join("episode.mkv");
117
118        let mut binding = Command::new("ffmpeg");
119        binding
120            .arg("-y")
121            .args(["-hide_banner", "-loglevel", "error"]) // silent operation
122            .args([OsStr::new("-i"), video_path.as_os_str()])
123            .args([OsStr::new("-i"), audio_path.as_os_str()]);
124
125        match subtitle_type {
126            SubtitleType::Hard => Ok(binding
127                .args([
128                    "-vf",
129                    &format!("subtitles={}", subtitle_path.as_ref().display()),
130                ])
131                .arg(output_path)
132                .status()?),
133            SubtitleType::Soft => Ok(binding
134                .args([OsStr::new("-i"), subtitle_path.as_ref().as_os_str()])
135                .args(["-map", "0"])
136                .args(["-map", "1:a:0"])
137                .args(["-map", "2"])
138                .args(["-metadata:s:s:0", &format!("language={subtitle_language}")])
139                .args(["-codec", "copy"])
140                .arg(output_path)
141                .status()?),
142        }
143    }
144}