diff --git a/Cargo.lock b/Cargo.lock index c8e911a..c4553a0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -770,7 +770,7 @@ dependencies = [ [[package]] name = "tcobalt" -version = "1.2.1" +version = "1.3.0" dependencies = [ "futures", "openssl", diff --git a/Cargo.toml b/Cargo.toml index 46f95e1..15f153a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "tcobalt" -version = "1.2.1" +version = "1.3.0" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html diff --git a/src/args/config.rs b/src/args/config.rs index f6d2418..10971cd 100644 --- a/src/args/config.rs +++ b/src/args/config.rs @@ -35,13 +35,17 @@ pub fn load_config_into(args: &mut Vec, instance_list: &mut Vec) args.push("-f".into()); args.push(option[1].into()) }, - "audio-only" => { + "proxy" => { if option[1].to_lowercase().as_str() == "true" { - args.push("-a".into()) + args.push("-x".into()) } - }, - "mute-audio" => { - if option[1].to_lowercase().as_str() == "true" { + } + "download-mode" => { + if option[1].to_lowercase().as_str() == "auto" { + args.push("-=".into()) + } else if option[1].to_lowercase().as_str() == "audio" { + args.push("-a".into()) + } else if option[1].to_lowercase().as_str() == "mute" { args.push("-m".into()) } }, @@ -79,6 +83,10 @@ pub fn load_config_into(args: &mut Vec, instance_list: &mut Vec) args.push("-i".into()); args.push(option[1].into()) }, + "bitrate" => { + args.push("-b".into()); + args.push(option[1].into()) + } _ => () } } diff --git a/src/args/mod.rs b/src/args/mod.rs index 11b4101..69cbd8e 100644 --- a/src/args/mod.rs +++ b/src/args/mod.rs @@ -13,12 +13,11 @@ pub struct Args { pub c_video_codec: types::VideoCodec, pub c_video_quality: u16, pub c_audio_format: types::AudioFormat, - pub c_audio_only: bool, - pub c_audio_muted: bool, + pub c_audio_bitrate: u16, + pub c_download_mode: types::DownloadMode, pub c_twitter_gif: bool, pub c_tt_full_audio: bool, pub c_tt_h265: bool, - pub c_dublang: bool, pub c_disable_metadata: bool, pub accept_language: String, pub out_filename: Option, @@ -26,7 +25,8 @@ pub struct Args { pub same_filenames: bool, pub picker_choice: u8, pub cobalt_instance: String, - pub help_flag: Option + pub help_flag: Option, + pub c_proxy: bool } impl Args { pub fn get() -> Self { @@ -37,11 +37,11 @@ impl Args { c_video_codec: types::VideoCodec::H264, c_video_quality: 1080, c_audio_format: types::AudioFormat::MP3, - c_audio_only: false, - c_audio_muted: false, + c_download_mode: types::DownloadMode::Auto, c_twitter_gif: false, out_filename: None, same_filenames: false, + c_audio_bitrate: 128, help_flag: None, method: None, bulk_array: None, @@ -49,9 +49,9 @@ impl Args { c_fname_style: types::FilenamePattern::Classic, c_tt_full_audio: false, c_tt_h265: false, - c_dublang: false, c_disable_metadata: false, - cobalt_instance: String::from("co.wuk.sh"), + c_proxy: false, + cobalt_instance: String::from("api.cobalt.tools"), accept_language: String::from("en") } } @@ -104,20 +104,20 @@ impl Args { "--vcodec" => expected.push(ExpectedFlags::VideoCodec), "--vquality" => expected.push(ExpectedFlags::VideoQuality), "--aformat" => expected.push(ExpectedFlags::AudioFormat), - "--audio-only" => self.c_audio_only = !self.c_audio_only, - "--mute-audio" => self.c_audio_muted = !self.c_audio_muted, + "--audio-only" => self.c_download_mode = types::DownloadMode::Audio, + "--mute-audio" => self.c_download_mode = types::DownloadMode::Mute, + "--auto" => self.c_download_mode = types::DownloadMode::Auto, "--twitter-gif" => self.c_twitter_gif = !self.c_twitter_gif, "--tt-full-audio" => self.c_tt_full_audio = !self.c_tt_full_audio, "--tt-h265" => self.c_tt_h265 = !self.c_tt_h265, - "--dublang" => { - self.c_dublang = true; - expected.push(ExpectedFlags::Language); - }, + "--dublang" => expected.push(ExpectedFlags::Language), "--no-metadata" => self.c_disable_metadata = !self.c_disable_metadata, "--output" => expected.push(ExpectedFlags::Output), "--fname-style" => expected.push(ExpectedFlags::FilenamePattern), "--pick" => expected.push(ExpectedFlags::Picker), "--instance" => expected.push(ExpectedFlags::Instance), + "--bitrate" => expected.push(ExpectedFlags::Bitrate), + "--proxy" => self.c_proxy = !self.c_proxy, _ => { if self.c_url == None && arg.contains("https://") { self.c_url = Some(arg.clone()); @@ -141,13 +141,12 @@ impl Args { 'c' => expected.push(ExpectedFlags::VideoCodec), 'q' => expected.push(ExpectedFlags::VideoQuality), 'f' => expected.push(ExpectedFlags::AudioFormat), - 'a' => self.c_audio_only = !self.c_audio_only, - 'm' => self.c_audio_muted = !self.c_audio_muted, + 'a' => self.c_download_mode = types::DownloadMode::Audio, + 'm' => self.c_download_mode = types::DownloadMode::Mute, 'g' => self.c_twitter_gif = !self.c_twitter_gif, 'u' => self.c_tt_full_audio = !self.c_tt_full_audio, 'h' => self.c_tt_h265 = !self.c_tt_h265, 'l' => { - self.c_dublang = true; expected.push(ExpectedFlags::Language); }, 'n' => self.c_disable_metadata = !self.c_disable_metadata, @@ -155,6 +154,9 @@ impl Args { 's' => expected.push(ExpectedFlags::FilenamePattern), 'p' => expected.push(ExpectedFlags::Picker), 'i' => expected.push(ExpectedFlags::Instance), + 'x' => self.c_proxy = !self.c_proxy, + '=' => self.c_download_mode = types::DownloadMode::Auto, + 'b' => expected.push(ExpectedFlags::Bitrate), _ => return Err(types::ParseError::throw_invalid(&format!("Invalid character {c} in multi-flag argument: {arg}"))) } } @@ -227,6 +229,13 @@ impl Args { url.truncate(idx); } self.cobalt_instance = url; + }, + ExpectedFlags::Bitrate => { + if arg == "320" || arg == "256" || arg == "128" || arg == "96" || arg == "64" || arg == "8" { + self.c_audio_bitrate = arg.parse::().unwrap(); + } else { + return Err(types::ParseError::throw_invalid("Make sure you select a valid bitrate! (320/256/128/96/64/8)")); + } } } } @@ -326,7 +335,13 @@ impl Args { }, "list" | "l" => self.method = Some(types::Method::List), "version" | "v" | "-v" | "--version" => self.method = Some(types::Method::Version), - "cobalt-version" | "cv" | "c" => self.method = Some(types::Method::CobaltVersion), + "cobalt-version" | "cv" | "c" => { + if self.raw.get(2).is_some() { + self.method = Some(types::Method::CobaltVersion(self.raw[2].clone())) + } else { + self.method = Some(types::Method::CobaltVersion(String::from("api.cobalt.tools"))) + } + }, "gen-config" | "gc" => self.method = Some(types::Method::GenConfig), unknown => return Err(types::ParseError::throw_invalid(&format!("Unrecognized tcobalt method: {}", unknown))) @@ -347,5 +362,5 @@ impl Args { #[derive(Debug)] enum ExpectedFlags { - VideoCodec, VideoQuality, AudioFormat, Output, FilenamePattern, Picker, Language, Instance + VideoCodec, VideoQuality, AudioFormat, Output, FilenamePattern, Picker, Language, Instance, Bitrate } diff --git a/src/args/types.rs b/src/args/types.rs index 5dadd17..47f0012 100644 --- a/src/args/types.rs +++ b/src/args/types.rs @@ -42,6 +42,15 @@ impl FilenamePattern { format!("{self:?}").to_lowercase() } } +#[derive(Debug, PartialEq, Eq, Clone)] +pub enum DownloadMode { + Auto, Audio, Mute +} +impl Default for DownloadMode { + fn default() -> Self { + Self::Auto + } +} #[derive(Debug, PartialEq, Eq, Clone)] pub enum Help { @@ -49,7 +58,7 @@ pub enum Help { } #[derive(Debug, PartialEq, Eq, Clone)] pub enum Method { - Get, List, Bulk, Help, Version, CobaltVersion, GenConfig + Get, List, Bulk, Help, Version, CobaltVersion(String), GenConfig } #[derive(Debug, PartialEq, Eq)] diff --git a/src/main.rs b/src/main.rs index 06384df..a714ab0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -83,12 +83,14 @@ async fn main() -> std::process::ExitCode { args::types::Method::List => println!("{}", strings::get_str("info", "supported")), args::types::Method::Help => unreachable!(), args::types::Method::Version => println!("{}", strings::get_str("info", "version").replace("{}", VERSION.trim())), - args::types::Method::CobaltVersion => { - let request = reqwest::Client::new().get("https://co.wuk.sh/api/serverInfo") + args::types::Method::CobaltVersion(api_url) => { + let request = reqwest::Client::new().get(format!("https://{}/", api_url)) .header("User-Agent", &format!("tcobalt {}", VERSION.trim())); if debug { eprintln!("[DEBUG] Sending GET request to cobalt ...") }; let ver = match request.send().await { - Ok(res) => res.text().await.unwrap_or("{\"version\":\"unknown\",\"commit\":\"unknown\",\"branch\":\"unknown\"}".to_string()), + Ok(res) => { + res.text().await.expect("cobalt returned nothing") + }, Err(e) => { eprintln!("Cobalt server did not respond: {}", e.to_string()); return std::process::ExitCode::FAILURE; @@ -102,10 +104,12 @@ async fn main() -> std::process::ExitCode { return std::process::ExitCode::FAILURE; } }; - let version = stats.get("version").unwrap().get_str().unwrap(); - let commit = stats.get("commit").unwrap().get_str().unwrap(); - let branch = stats.get("branch").unwrap().get_str().unwrap(); - println!("Cobalt (by wukko) version {version}"); + let cobalt = stats.get("cobalt").unwrap().get_object().unwrap(); + let git = stats.get("git").unwrap().get_object().unwrap(); + let version = cobalt.get("version").unwrap().get_str().unwrap(); + let commit = git.get("commit").unwrap().get_str().unwrap(); + let branch = git.get("branch").unwrap().get_str().unwrap(); + println!("Cobalt (by wukko and jj) version {version}"); println!("Latest commit on branch \"{branch}\": {commit}"); }, args::types::Method::GenConfig => { @@ -133,11 +137,10 @@ async fn execute_get_media(args: Args, bulk: u16, debug: bool) -> bool { let json = proc::cobalt_args(&args); let download_url: &str = args.c_url.as_ref().unwrap(); - let request = reqwest::Client::new().post(format!("https://{}/api/json", &args.cobalt_instance)) + let request = reqwest::Client::new().post(format!("https://{}/", &args.cobalt_instance)) .header("User-Agent", &format!("tcobalt {}", VERSION.trim())) .header("Accept", "application/json") .header("Content-Type", "application/json") - .header("Accept-Language", &args.accept_language) .body(json); if debug { eprintln!("[DEBUG {download_url}] Sending POST request to cobalt server ...") }; @@ -150,16 +153,16 @@ async fn execute_get_media(args: Args, bulk: u16, debug: bool) -> bool { let status = json.get("status".into()).unwrap().get_str().unwrap(); match status.as_str() { "error" => { - let text = json.get("text").unwrap().get_str().unwrap(); - eprintln!("Cobalt returned error:\n\"{text}\"\n(when downloading from {download_url})"); + let text = json.get("error".into()).unwrap().get_object().unwrap().get("code".into()).unwrap().get_str().unwrap(); + eprintln!("Cobalt returned error: \"{text}\" (when downloading from {download_url})"); return false; }, - "stream" | "redirect" | "success" | "picker" => { + "tunnel" | "redirect" | "picker" => { if debug { eprintln!("[DEBUG {download_url}] Cobalt returned a response") }; let url = proc::get_url(&args, &status, &json); - let media = if args.c_audio_only { + let media = if args.c_download_mode == tcargs::types::DownloadMode::Audio { "audio" } else { "video" @@ -171,7 +174,13 @@ async fn execute_get_media(args: Args, bulk: u16, debug: bool) -> bool { let res = attempt!(stream_request.send().await, "Live renderer did not respond:\n\"{}\"\n(when downloading from {download_url})"); if debug { eprintln!("[DEBUG {download_url}] Response received from stream") }; - let filename = proc::extract_filename(&args, res.headers(), bulk, debug); + let mut filename = json.get("filename".into()).unwrap().get_str().unwrap(); + if let Some(custom_name) = args.out_filename { + filename = custom_name; + } + if bulk > 0 { + filename = format!("{bulk}_{filename}"); + } println!( "Downloading {} from {} ...", media, @@ -192,10 +201,6 @@ async fn execute_get_media(args: Args, bulk: u16, debug: bool) -> bool { println!("Your {media} is ready! >> {display}") }, - "rate-limit" => { - eprintln!("You are being rate limited by cobalt! Please try again later.\n(when downloading from {download_url})"); - return false; - } _ => unreachable!() } true diff --git a/src/process.rs b/src/process.rs index fc625f4..3ad732e 100644 --- a/src/process.rs +++ b/src/process.rs @@ -1,6 +1,5 @@ use crate::{tcargs, args::Args, json}; use std::io::Write; -use std::hash::{Hash, Hasher}; pub fn print_json_error(error: String, body: String) -> String { let mut text = String::new(); @@ -17,7 +16,7 @@ pub fn print_json_error(error: String, body: String) -> String { } pub fn get_url(args: &Args, status: &str, json: &std::collections::HashMap) -> String { - let media = if args.c_audio_only { + let media = if args.c_download_mode == tcargs::types::DownloadMode::Audio { "audio" } else { "video" @@ -28,7 +27,11 @@ pub fn get_url(args: &Args, status: &str, json: &std::collections::HashMap = Vec::new(); let picker_array = json.get("picker").unwrap().get_array().unwrap(); for picker in picker_array.iter() { - urls.push(picker.get_object().unwrap().get("url").unwrap().get_str().unwrap()); + let picker = picker.get_object().unwrap(); + let cobalt_type = picker.get("type".into()).unwrap().get_str().unwrap(); + if cobalt_type == String::from("video") || cobalt_type == String::from("gif") { + urls.push(picker.get("url".into()).unwrap().get_str().unwrap()); + } } urls }; @@ -56,74 +59,20 @@ pub fn get_url(args: &Args, status: &str, json: &std::collections::HashMap String { - match &args.out_filename { - Some(name) => { - if bulk > 0 { - format!("{bulk}-{name}") - } else { - name.to_string() - } - }, - None => { - let download_url = args.c_url.clone().unwrap(); - if debug { eprintln!("[DEBUG {download_url}] Obtaining filename from headers") }; - match headers.get("Content-Disposition") { - Some(disposition) => { - let disposition = disposition.to_str().unwrap(); - let mut pass: u8 = 0; - let mut filename = String::new(); - for c in disposition.chars() { - if c == ';' || c == '\"' { - pass += 1; - continue; - } - if pass == 2 { - filename.push(c); - } - if pass == 3 { - break; - } - } - filename - }, - None => { - if debug { eprintln!("[DEBUG {download_url}] No filename specified, generating random filename ...") }; - let mut hasher = std::collections::hash_map::DefaultHasher::new(); - download_url.hash(&mut hasher); - let mut hash = format!("{:x}", hasher.finish()); - if args.c_twitter_gif { - hash.push_str(".gif"); - } else { - match args.c_video_codec { - tcargs::types::VideoCodec::AV1 | tcargs::types::VideoCodec::H264 => { - hash.push_str(".mp4"); - } - tcargs::types::VideoCodec::VP9 => { - hash.push_str(".webm"); - } - } - } - hash - } - } - } - } -} - const POST_TEMPLATE: &str = "{ \"url\": \"\", - \"vCodec\": \"\", - \"vQuality\": \"\", - \"aFormat\": \"\", - \"filenamePattern\": \"\", - \"isAudioOnly\": , - \"isTTFullAudio\": , + \"youtubeVideoCodec\": \"\", + \"videoQuality\": \"\", + \"audioFormat\": \"\", + \"audioBitrate\": \"\", + \"filenameStyle\": \"\", + \"downloadMode\": \"\", + \"tiktokFullAudio\": , \"tiktokH265\": , - \"isAudioMuted\": , - \"dubLang\": , + \"youtubeDubLang\": \"\", \"disableMetadata\": , - \"twitterGif\": + \"twitterGif\": , + \"alwaysProxy\": }"; pub fn cobalt_args(args_in: &Args) -> String { POST_TEMPLATE.to_string() @@ -134,11 +83,12 @@ pub fn cobalt_args(args_in: &Args) -> String { .replace("", &args_in.c_fname_style.print()) .replace("", &args_in.c_tt_full_audio.to_string()) .replace("", &args_in.c_tt_h265.to_string()) - .replace("", &args_in.c_audio_only.to_string()) - .replace("", &args_in.c_audio_muted.to_string()) - .replace("", &args_in.c_dublang.to_string()) + .replace("", &format!("{:?}", args_in.c_download_mode).to_lowercase()) + .replace("", &args_in.accept_language) .replace("", &args_in.c_disable_metadata.to_string()) .replace("", &args_in.c_twitter_gif.to_string()) + .replace("", &args_in.c_proxy.to_string()) + .replace("", &args_in.c_audio_bitrate.to_string()) } #[macro_export] diff --git a/src/strings/info.txt b/src/strings/info.txt index bde27a8..1f5f535 100644 --- a/src/strings/info.txt +++ b/src/strings/info.txt @@ -7,12 +7,16 @@ The code and license is available at https://github.com/khyerdev/tcobalt [supported] bilibili.com & bilibili.tv +bluesky dailymotion videos instagram reels, posts & stories (rarely works) +facebook +loom ok video (full video+audio only) pinterest videos & stories reddit videos & gifs rutube videos +snapchat soundcloud (audio only) streamable.com tiktok videos, photos & audio @@ -29,22 +33,21 @@ youtube videos, shorts & music vcodec = h264 vquality = 1080 aformat = mp3 -audio-only = false -mute-audio = false +bitrate = 128 +download-mode = auto twitter-gif = false tt-full-audio = false tt-h265 = false dublang = none no-metadata = false fname-style = classic +proxy = false instance = api.cobalt.tools \[default.instances] api.cobalt.tools -capi.oak.li +dl.khyernet.xyz co.eepy.today -co.tskau.team -cobalt.wither.ing -cobalt.canine.tools -wukko.wolfdo.gg -cobalt.misike.eu +cobalt.api.timelessnesses.me +beta.cobalt.canine.tools +ca.haloz.at diff --git a/src/strings/usage.txt b/src/strings/usage.txt index fd6312f..8468f5c 100644 --- a/src/strings/usage.txt +++ b/src/strings/usage.txt @@ -10,7 +10,7 @@ Main Methods: Misc Methods: gen-config version - cobalt-version + cobalt-version [instance] You can also type the first letter for a method Type "help " for more information about a method and its options @@ -25,11 +25,14 @@ Usage: tcb get [options] Global Options: -q --vquality The quality of the output video. Options: 144, 480, 720, 1080, 1440, 2160, Default: 1080 -f --aformat The format of the audio. Formats: best, mp3, ogg, wav, opus, Default: mp3 - The "best" option takes the format of the original audio, and may not be specified on the website you took the media from. + The "best" option takes the format of the original audio, and may not be specified on the website you took the media from. + -b --bitrate The bitrate of the audio if it was reformatted. Rates: 320, 256, 128, 96, 64, 8, Default: 128 -a --audio-only Tells cobalt to only download and output the audio of the link -m --mute-audio Tells cobalt to mute the audio from the downloaded content + -= --auto Tells cobalt to download a video if possible, otherwise downloading audio instead. + This is the default option, only use this if your config default is something else. -n --no-metadata Prevents the downloaded media from including metadata - -s --fname_style