substudy/export/
exporter.rs

1//! Code shared between multiple exporters.
2
3use std::{
4    convert::AsRef,
5    default::Default,
6    ffi::OsStr,
7    fmt::Write as fmt_Write,
8    fs,
9    io::Write,
10    path::{Path, PathBuf},
11};
12
13use anyhow::{anyhow, Context as _};
14
15use crate::{
16    align::align_available_files,
17    lang::Lang,
18    srt::{Subtitle, SubtitleFile},
19    time::{Period, ToTimestamp},
20    video::{Extraction, ExtractionSpec, Id3Metadata, Video},
21    Result,
22};
23
24/// Take a platform-specific pathname fragment and turn it into a regular
25/// Unicode string.
26pub fn os_str_to_string(os_str: &OsStr) -> String {
27    os_str.to_string_lossy().into_owned()
28}
29
30/// Information about a specific language.
31pub struct LanguageResources {
32    /// The subtitles associated with this language.
33    pub subtitles: SubtitleFile,
34
35    /// The language used in our subtitles, if we can figure it out.
36    pub language: Option<Lang>,
37}
38
39impl LanguageResources {
40    /// Create a list of per-language resources.
41    fn new(subtitles: SubtitleFile) -> LanguageResources {
42        let language = subtitles.detect_language();
43        LanguageResources {
44            subtitles: subtitles,
45            language: language,
46        }
47    }
48}
49
50/// Information about media file and associated subtitles that the user
51/// wants to export.
52pub struct Exporter {
53    /// The video file from which to extract images and audio clips.
54    video: Video,
55
56    /// Resources related to the foreign language.
57    foreign: LanguageResources,
58
59    /// Resources related to the native language, if any.
60    native: Option<LanguageResources>,
61
62    /// The base name to use when constructing other filenames.
63    file_stem: String,
64
65    /// The directory into which we want to output files.
66    dir: PathBuf,
67
68    /// A list of media files we want to extract from our video as
69    /// efficiently as possible.
70    extractions: Vec<Extraction>,
71}
72
73impl Exporter {
74    /// Create a new exporter for the specified video and subtitles.  The
75    /// `label` parameter will be used to construct an output directory
76    /// name.
77    pub fn new(
78        video: Video,
79        foreign_subtitles: SubtitleFile,
80        native_subtitles: Option<SubtitleFile>,
81        label: &str,
82    ) -> Result<Exporter> {
83        let foreign = LanguageResources::new(foreign_subtitles);
84        let native = native_subtitles.map(|subs| LanguageResources::new(subs));
85
86        // Construct a path `dir` which we'll use to store our output
87        // files.  This is much uglier than it ought to be because paths
88        // are not necessarily valid Unicode strings on all OSes, so we
89        // need to jump through extra hoops.  We test for a directory's
90        // existence using the `metadata` call, which is the only way to do
91        // it in stable Rust.
92        let file_stem = os_str_to_string(video.file_stem());
93        let dir = Path::new("./").join(format!("{}_{}", &file_stem, label));
94        if fs::metadata(&dir).is_ok() {
95            return Err(anyhow!(
96                "Directory already exists: {}",
97                &dir.to_string_lossy()
98            ));
99        }
100        fs::create_dir_all(&dir)
101            .with_context(|| format!("could not create {}", dir.display()))?;
102
103        Ok(Exporter {
104            video: video,
105            foreign: foreign,
106            native: native,
107            file_stem: file_stem,
108            dir: dir,
109            extractions: vec![],
110        })
111    }
112
113    /// The base name of this file, with the directory and file extension
114    /// removed.
115    pub fn file_stem(&self) -> &str {
116        &self.file_stem
117    }
118
119    /// Return a title for this video.
120    pub fn title(&self) -> &str {
121        &self.file_stem
122    }
123
124    /// Get the video we're exporting.
125    pub fn video(&self) -> &Video {
126        &self.video
127    }
128
129    /// Get data related to the foreign language.
130    pub fn foreign(&self) -> &LanguageResources {
131        &self.foreign
132    }
133
134    /// Get data related to the native language.
135    pub fn native(&self) -> Option<&LanguageResources> {
136        self.native.as_ref()
137    }
138
139    /// Align our two sets of subtitles.
140    pub fn align(&self) -> Vec<(Option<Subtitle>, Option<Subtitle>)> {
141        align_available_files(
142            &self.foreign.subtitles,
143            self.native.as_ref().map(|n| &n.subtitles),
144        )
145    }
146
147    /// Construct a path to an extracted media file, including timestamps
148    /// and language information as appropriate.
149    fn media_path<T: ToTimestamp>(
150        &self,
151        timestamp: T,
152        lang: Option<Lang>,
153        extension: &str,
154    ) -> PathBuf {
155        let mut file_name =
156            format!("{}_{}", &self.file_stem, timestamp.to_file_timestamp());
157        if let Some(l) = lang {
158            write!(&mut file_name, ".{}", l.as_str()).unwrap();
159        }
160        write!(&mut file_name, ".{}", extension).unwrap();
161        self.dir.join(file_name)
162    }
163
164    /// Schedule an export of the image at the specified time code.
165    /// Returns the path to which the image will be written.
166    pub fn schedule_image_export(&mut self, time: f32) -> String {
167        let path = self.media_path(time, None, "jpg");
168        self.extractions.push(Extraction {
169            path: path.clone(),
170            spec: ExtractionSpec::Image(time),
171        });
172        os_str_to_string(path.file_name().unwrap())
173    }
174
175    /// Schedule an export of the audio at the specified time period.
176    /// Returns the path to which the audio will be written.
177    pub fn schedule_audio_export(
178        &mut self,
179        lang: Option<Lang>,
180        period: Period,
181    ) -> String {
182        self.schedule_audio_export_ext(lang, period, Default::default())
183    }
184
185    /// Schedule an export of the audio at the specified time period, using
186    /// the specified metadata.  Returns the path to which the audio will
187    /// be written.
188    pub fn schedule_audio_export_ext(
189        &mut self,
190        lang: Option<Lang>,
191        period: Period,
192        metadata: Id3Metadata,
193    ) -> String {
194        let path = self.media_path(period, lang, "mp3");
195        let stream = lang.and_then(|l| self.video.audio_for(l));
196        self.extractions.push(Extraction {
197            path: path.clone(),
198            spec: ExtractionSpec::Audio(stream, period, metadata),
199        });
200        os_str_to_string(path.file_name().unwrap())
201    }
202
203    /// Write a raw chunk of bytes to a file in our export directory.
204    pub fn export_data_file<P>(&self, rel_path: P, data: &[u8]) -> Result<()>
205    where
206        P: AsRef<Path>,
207    {
208        let path = self.dir.join(rel_path.as_ref());
209        let mut f = fs::File::create(&path)
210            .with_context(|| format!("could not open {}", path.display()))?;
211        f.write_all(data)
212            .with_context(|| format!("could not write to {}", path.display()))?;
213        Ok(())
214    }
215
216    /// Finish all scheduled exports.
217    pub fn finish_exports(&mut self) -> Result<()> {
218        self.video.extract(&self.extractions)?;
219        Ok(())
220    }
221}