blob_dl/assembling/
youtube.rs

1pub mod yt_playlist;
2pub mod yt_video;
3pub mod config;
4
5use crate::error::{BlobdlError, BlobResult};
6use dialoguer::console::Term;
7use dialoguer::{theme::ColorfulTheme, Select, Input};
8use serde::{Deserialize, Serialize};
9use serde_json;
10use std::{env, fmt};
11use colored::Colorize;
12
13// Functions used both in yt_video.rs and yt_playlist.rs
14/// Asks the user whether they want to download video files or audio-only
15fn get_media_selection(term: &Term) -> Result<MediaSelection, std::io::Error> {
16    let download_formats = &[
17        "Normal Video",
18        "Audio-only",
19        "Video-only"
20    ];
21
22    // Ask the user which format they want the downloaded files to be in
23    let media_selection = Select::with_theme(&ColorfulTheme::default())
24        .with_prompt("What kind of file(s) do you want to download?")
25        .default(0)
26        .items(download_formats)
27        .interact_on(term)?;
28
29    match media_selection {
30        0 => Ok(MediaSelection::FullVideo),
31        1 => Ok(MediaSelection::AudioOnly),
32        _ => Ok(MediaSelection::VideoOnly),
33    }
34}
35
36/// Asks for an directory to store downloaded file(s) in
37///
38/// The current directory can be selected or one can be typed in
39fn get_output_path(term: &Term) -> BlobResult<String> {
40    let output_path_options = &[
41        "Current directory",
42        "Other [specify]",
43    ];
44
45    let output_path = Select::with_theme(&ColorfulTheme::default())
46        .with_prompt("Where do you want the downloaded file(s) to be saved?")
47        .default(0)
48        .items(output_path_options)
49        .interact_on(term)?;
50
51    match output_path {
52        // Return the current directory
53        0 => Ok(env::current_dir()?
54            .as_path()
55            .display()
56            .to_string()),
57
58        // Return a directory typed in by the user
59        _ => Ok(Input::with_theme(&ColorfulTheme::default())
60            .with_prompt("Output path:")
61            .interact_text()?),
62    }
63}
64
65
66use spinoff;
67use std::process;
68// Running yt-dlp -j <...>
69use execute::Execute;
70
71/// Returns the output of <yt-dlp -j url>: a JSON dump of all the available format information for a video
72fn get_ytdlp_formats(url: &str) -> Result<process::Output, std::io::Error> {
73    // Neat animation to entertain the user while the information is being downloaded
74    let mut sp = spinoff::Spinner::new(spinoff::spinners::Dots10, "Fetching available formats...", spinoff::Color::Cyan);
75
76    let mut command = process::Command::new("yt-dlp");
77    // Get a JSON dump of all the available formats related to this url
78    command.arg("-j");
79    // Continue even if you get errors
80    command.arg("-i");
81    command.arg(url);
82
83    // Redirect the output to a variable instead of to the screen
84    command.stdout(process::Stdio::piped());
85    // Don't show errors and warnings
86    command.stderr(process::Stdio::piped());
87    let output = command.execute_output();
88
89    // Stop the ui spinner
90    sp.success("Formats downloaded successfully".bold().to_string().as_str());
91
92    output
93}
94
95/// Ask the user what format they want the downloaded file to be recoded to (yt-dlp postprocessor) REQUIRES FFMPEG
96fn convert_to_format(term: &Term, media_selected: &MediaSelection)
97                     -> BlobResult<VideoQualityAndFormatPreferences>
98{
99    // Available formats for recoding
100    let format_options = match *media_selected {
101        // Only show audio-only formats
102        MediaSelection::AudioOnly => vec!["mp3", "m4a", "wav", "aac", "alac", "flac", "opus", "vorbis"],
103        // Only show formats which aren't audio-only
104        MediaSelection::VideoOnly => vec!["mp4", "mkv", "mov", "avi", "flv", "gif", "webm", "aiff", "mka", "ogg"],
105        // Show all the available formats
106        MediaSelection::FullVideo => vec!["mp4", "mkv", "mov", "avi", "flv", "gif", "webm", "aac", "aiff",
107                                          "alac", "flac", "m4a", "mka", "mp3", "ogg", "opus", "vorbis", "wav"],
108    };
109
110    let user_selection = Select::with_theme(&ColorfulTheme::default())
111        .with_prompt("Which container do you want the final file to be in?")
112        .default(0)
113        .items(&format_options)
114        .interact_on(term)?;
115
116    Ok(VideoQualityAndFormatPreferences::ConvertTo(format_options[user_selection].to_string()))
117}
118
119/// Serializes the information about all the formats available for 1 video
120fn serialize_formats(json_dump: &str) -> BlobResult<VideoSpecs> {
121    let result = serde_json::from_str(json_dump);
122    match result {
123        Ok(cool) => Ok(cool),
124        Err(err) => Err(BlobdlError::SerdeError(err))
125    }
126}
127
128/// Checks if format has conflicts with media_selected (like a video only format and an audio-only media_selection
129///
130/// Returns true format and media_selected are compatible
131fn check_format(format: &VideoFormat, media_selected: &MediaSelection) -> bool {
132    // Skip image and weird formats (examples of strange formats ids: 233, 234, sb2, sb1, sb0)
133    if format.filesize.is_none() {
134        return false;
135    }
136    // Skip audio-only files if the user wants full video
137    if *media_selected == MediaSelection::FullVideo && format.resolution == "audio only" {
138        return false;
139    }
140    // Skip video files if the user wants audio-only
141    if *media_selected == MediaSelection::AudioOnly && format.resolution != "audio only" {
142        return false;
143    }
144    if let Some(acodec) = &format.acodec {
145        // Skip video-only files if the user doesn't want video-only
146        if *media_selected == MediaSelection::FullVideo && acodec == "none" {
147            return false;
148        }
149        //Skip normal video if the user wants video-only
150        if *media_selected == MediaSelection::VideoOnly && acodec != "none" {
151            return false;
152        }
153    }
154    true
155}
156
157// Common enums and structs
158/// Whether the user wants to download video files or audio-only
159#[derive(Debug, Eq, PartialEq, Clone)]
160pub(crate) enum MediaSelection {
161    FullVideo,
162    VideoOnly,
163    AudioOnly,
164}
165
166/// All the information about a particular video format
167#[derive(Deserialize, Serialize, Debug, PartialOrd, PartialEq)]
168struct VideoFormat {
169    format_id: String,
170    // File extension
171    ext: String,
172    // Fps count, is null for audio-only formats
173    fps: Option<f64>,
174    // How many audio channels are available, is null for video-only formats. Unavailable on weird sb* formats
175    audio_channels: Option<u64>,
176    // Video resolution, is "audio only" for audio-only formats
177    resolution: String,
178    // Measured in MB. Unavailable on sb* formats
179    filesize: Option<u64>,
180    // Video codec, can be "none"
181    vcodec: String,
182    // Audio codec, can be "none" or straight up not exist (like in mp4 audio-only formats)
183    acodec: Option<String>,
184    // Codec container
185    container: Option<String>,
186    // Total average bitrate
187    tbr: Option<f64>,
188    // When filesize is null, this may be available
189    filesize_approx: Option<u64>,
190}
191
192// A list of all the formats available for a single video
193#[derive(Deserialize, Serialize, Debug)]
194struct VideoSpecs {
195    formats: Vec<VideoFormat>,
196}
197
198#[derive(Debug, Clone)]
199/// What quality and format the user wants a specific video to be downloaded in
200pub(crate) enum VideoQualityAndFormatPreferences {
201    // Code of the selected format
202    UniqueFormat(String),
203    // Recode the downloaded file to this format (post-processor)
204    ConvertTo(String),
205    BestQuality,
206    SmallestSize,
207}
208
209impl fmt::Display for VideoFormat {
210    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
211        let mut result;
212
213        if let Some(tbr) = self.tbr {
214            // Skip picture formats
215            // Add container
216            result = format!("{:<6} ", self.ext);
217
218            if self.resolution != "audio only" {
219                result = format!("{}| {:<13} ", result, self.resolution);
220            }
221
222            // This isn't a picture format so unwrap() is safe
223            let filesize = self.filesize.unwrap_or(0);
224
225            // filesize is converted from bytes to MB
226            let filesize_section = format!("| filesize: {:<.2}MB", filesize as f32 * 0.000001);
227            result = format!("{}{:<24}", result, filesize_section);
228
229            // If available, add audio channels
230            if let Some(ch) = self.audio_channels {
231                result = format!("{}| {} audio ch ", result, ch);
232            }
233
234            result = format!("{}| tbr: {:<8.2} ", result, tbr);
235
236            if self.vcodec != "none" {
237                result = format!("{}| vcodec: {:<13} ", result, self.vcodec);
238            }
239
240            if let Some(acodec) = &self.acodec {
241                if acodec != "none" {
242                    result = format!("{}| acodec: {:<13} ", result, acodec);
243                }
244            }
245
246            #[cfg(debug_assertions)]
247            return {
248                result = format!("[[DEBUG code: {:<3}]] {} ", self.format_id, result);
249                write!(f, "{}", result)
250            };
251
252            #[cfg(not(debug_assertions))]
253            write!(f, "{}", result)
254        } else {
255            write!(f, "I shouldn't show up because I am a picture format")
256        }
257    }
258}
259
260impl VideoSpecs {
261    fn formats(&self) -> &Vec<VideoFormat> {
262        &self.formats
263    }
264}