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
13fn 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 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
36fn 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 0 => Ok(env::current_dir()?
54 .as_path()
55 .display()
56 .to_string()),
57
58 _ => Ok(Input::with_theme(&ColorfulTheme::default())
60 .with_prompt("Output path:")
61 .interact_text()?),
62 }
63}
64
65
66use spinoff;
67use std::process;
68use execute::Execute;
70
71fn get_ytdlp_formats(url: &str) -> Result<process::Output, std::io::Error> {
73 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 command.arg("-j");
79 command.arg("-i");
81 command.arg(url);
82
83 command.stdout(process::Stdio::piped());
85 command.stderr(process::Stdio::piped());
87 let output = command.execute_output();
88
89 sp.success("Formats downloaded successfully".bold().to_string().as_str());
91
92 output
93}
94
95fn convert_to_format(term: &Term, media_selected: &MediaSelection)
97 -> BlobResult<VideoQualityAndFormatPreferences>
98{
99 let format_options = match *media_selected {
101 MediaSelection::AudioOnly => vec!["mp3", "m4a", "wav", "aac", "alac", "flac", "opus", "vorbis"],
103 MediaSelection::VideoOnly => vec!["mp4", "mkv", "mov", "avi", "flv", "gif", "webm", "aiff", "mka", "ogg"],
105 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
119fn 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
128fn check_format(format: &VideoFormat, media_selected: &MediaSelection) -> bool {
132 if format.filesize.is_none() {
134 return false;
135 }
136 if *media_selected == MediaSelection::FullVideo && format.resolution == "audio only" {
138 return false;
139 }
140 if *media_selected == MediaSelection::AudioOnly && format.resolution != "audio only" {
142 return false;
143 }
144 if let Some(acodec) = &format.acodec {
145 if *media_selected == MediaSelection::FullVideo && acodec == "none" {
147 return false;
148 }
149 if *media_selected == MediaSelection::VideoOnly && acodec != "none" {
151 return false;
152 }
153 }
154 true
155}
156
157#[derive(Debug, Eq, PartialEq, Clone)]
160pub(crate) enum MediaSelection {
161 FullVideo,
162 VideoOnly,
163 AudioOnly,
164}
165
166#[derive(Deserialize, Serialize, Debug, PartialOrd, PartialEq)]
168struct VideoFormat {
169 format_id: String,
170 ext: String,
172 fps: Option<f64>,
174 audio_channels: Option<u64>,
176 resolution: String,
178 filesize: Option<u64>,
180 vcodec: String,
182 acodec: Option<String>,
184 container: Option<String>,
186 tbr: Option<f64>,
188 filesize_approx: Option<u64>,
190}
191
192#[derive(Deserialize, Serialize, Debug)]
194struct VideoSpecs {
195 formats: Vec<VideoFormat>,
196}
197
198#[derive(Debug, Clone)]
199pub(crate) enum VideoQualityAndFormatPreferences {
201 UniqueFormat(String),
203 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 result = format!("{:<6} ", self.ext);
217
218 if self.resolution != "audio only" {
219 result = format!("{}| {:<13} ", result, self.resolution);
220 }
221
222 let filesize = self.filesize.unwrap_or(0);
224
225 let filesize_section = format!("| filesize: {:<.2}MB", filesize as f32 * 0.000001);
227 result = format!("{}{:<24}", result, filesize_section);
228
229 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}