Skip to content

Commit

Permalink
rewrite /cobalt_download to support v10 and custom instances
Browse files Browse the repository at this point in the history
  • Loading branch information
jelni committed Dec 27, 2024
1 parent 49442ed commit 0742427
Show file tree
Hide file tree
Showing 19 changed files with 391 additions and 400 deletions.
19 changes: 10 additions & 9 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
USER_AGENT="telegram-bot (t.me/bot; me <[email protected]>)"
TELEGRAM_TOKEN=123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11
API_ID=YOUR_TELEGRAM_API_ID
API_HASH=YOUR_TELEGRAM_API_HASH
DB_ENCRYPTION_KEY=12345678
FAL_API_KEY=YOUR_API_KEY
STABLEHORDE_TOKEN=0000000000
STABLEHORDE_CLIENT=name:version:contact
MAKERSUITE_API_KEY=YOUR_API_KEY
GROQ_API_KEY=YOUR_API_KEY
TELEGRAM_TOKEN="123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11"
API_ID="YOUR_TELEGRAM_API_ID"
API_HASH="YOUR_TELEGRAM_API_HASH"
DB_ENCRYPTION_KEY="12345678"
COBALT_INSTANCES="[{\"name\": \"example.com\", \"url\": \"http://localhost:9000/\"}, {\"name\": \"cobalt.tools\", \"url\": \"https://api.cobalt.tools/\", \"api_key\": \"api_key\"}]"
FAL_API_KEY="YOUR_API_KEY"
STABLEHORDE_TOKEN="0000000000"
STABLEHORDE_CLIENT="name:version:contact"
MAKERSUITE_API_KEY="YOUR_API_KEY"
GROQ_API_KEY="YOUR_API_KEY"
1 change: 1 addition & 0 deletions docker-compose.yml → compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ services:
API_ID:
API_HASH:
DB_ENCRYPTION_KEY:
COBALT_INSTANCES:
FAL_API_KEY:
STABLEHORDE_TOKEN:
STABLEHORDE_CLIENT:
Expand Down
182 changes: 66 additions & 116 deletions src/apis/cobalt.rs
Original file line number Diff line number Diff line change
@@ -1,156 +1,106 @@
use std::collections::HashMap;

use reqwest::header::ACCEPT;
use reqwest::header::{ACCEPT, AUTHORIZATION};
use serde::{Deserialize, Serialize};

use crate::commands::CommandError;
use crate::utilities::api_utils::{DetectServerError, ServerError};

#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct Payload<'a> {
url: &'a str,
v_quality: &'a str,
a_format: &'a str,
is_audio_only: bool,
#[serde(rename = "tiktokH265")]
tiktok_h265: bool,
video_quality: &'a str,
audio_format: &'a str,
download_mode: &'a str,
tiktok_full_audio: bool,
twitter_gif: bool,
}

#[derive(Deserialize)]
struct Response {
status: Status,
text: Option<String>,
url: Option<String>,
picker: Option<Vec<PickerItem>>,
#[serde(rename_all = "kebab-case")]
#[serde(tag = "status")]
pub enum Response {
Redirect(File),
Tunnel(File),
Picker(Picker),
Error(CobaltError),
}

#[derive(Deserialize)]
#[serde(rename_all = "kebab-case")]
enum Status {
Stream,
Redirect,
Picker,
Success,
Error,
RateLimit,
pub struct File {
pub url: String,
pub filename: String,
}

#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Picker {
pub audio: Option<String>,
pub audio_filename: Option<String>,
#[expect(clippy::struct_field_names)]
pub picker: Vec<PickerItem>,
}

#[derive(Deserialize)]
pub struct PickerItem {
pub url: String,
pub thumb: Option<String>,
}

#[derive(Deserialize)]
pub struct CobaltError {
pub error: ErrorContext,
}

#[derive(Deserialize)]
struct PickerItem {
url: String,
pub struct ErrorContext {
pub code: String,
}

pub enum Error {
Cobalt(String),
Server(ServerError),
Network(reqwest::Error),
}

pub async fn query(
http_client: reqwest::Client,
domain: &str,
http_client: &reqwest::Client,
instance: &str,
api_key: Option<&str>,
url: &str,
audio_only: bool,
) -> Result<Vec<String>, Error> {
let response = http_client
.post(format!("https://{domain}/api/json"))
) -> Result<Response, Error> {
let mut request = http_client
.post(instance)
.json(&Payload {
url,
v_quality: "1080",
a_format: "best",
is_audio_only: audio_only,
tiktok_h265: true,
video_quality: "1080",
audio_format: "best",
download_mode: if audio_only { "audio" } else { "auto" },
tiktok_full_audio: true,
twitter_gif: false,
})
.header(ACCEPT, "application/json")
.send()
.await
.map_err(Error::Network)?
.server_error()
.map_err(Error::Server)?
.json::<Response>()
.await
.map_err(Error::Network)?;

match response.status {
Status::Stream | Status::Redirect => Ok(vec![response.url.unwrap()]),
Status::Picker => Ok(response.picker.unwrap().into_iter().map(|i| i.url).collect()),
Status::Success | Status::Error | Status::RateLimit => {
Err(Error::Cobalt(response.text.unwrap()))
}
}
}
.header(ACCEPT, "application/json");

#[derive(Deserialize)]
pub struct Instance {
pub api_online: bool,
pub services: HashMap<Service, bool>,
pub score: f32,
pub protocol: String,
pub api: String,
}
if let Some(api_key) = api_key {
request = request.header(AUTHORIZATION, format!("Api-Key {api_key}"));
}

#[derive(Clone, Copy, PartialEq, Eq, Hash, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Service {
Youtube,
Rutube,
Tumblr,
Bilibili,
Pinterest,
Instagram,
Soundcloud,
YoutubeMusic,
Odnoklassniki,
Dailymotion,
Twitter,
Loom,
Vimeo,
Streamable,
Vk,
Tiktok,
Reddit,
TwitchClips,
YoutubeShorts,
Vine,
#[serde(other)]
Unknown,
}
let response =
request.send().await.map_err(Error::Network)?.server_error().map_err(Error::Server)?;

impl Service {
pub const fn name(self) -> Option<&'static str> {
match self {
Self::Youtube => Some("YouTube"),
Self::Rutube => Some("RUTUBE"),
Self::Tumblr => Some("Tumblr"),
Self::Bilibili => Some("BiliBili"),
Self::Pinterest => Some("Pinterest"),
Self::Instagram => Some("Instagram"),
Self::Soundcloud => Some("SoundCloud"),
Self::YoutubeMusic => Some("YouTube Music"),
Self::Odnoklassniki => Some("Odnoklassniki"),
Self::Dailymotion => Some("Dailymotion"),
Self::Twitter => Some("Twitter"),
Self::Loom => Some("Loom"),
Self::Vimeo => Some("Vimeo"),
Self::Streamable => Some("Streamable"),
Self::Vk => Some("VK"),
Self::Tiktok => Some("TikTok"),
Self::Reddit => Some("Reddit"),
Self::TwitchClips => Some("Twitch (Clips)"),
Self::YoutubeShorts => Some("YouTube (Shorts)"),
Self::Vine => Some("Vine"),
Self::Unknown => None,
}
}
response.json::<Response>().await.map_err(Error::Network)
}

pub async fn instances(http_client: reqwest::Client) -> Result<Vec<Instance>, CommandError> {
let response = http_client
.get("https://instances.hyper.lol/instances.json")
pub async fn get_error_localization(
http_client: &reqwest::Client,
) -> reqwest::Result<HashMap<String, String>> {
let request = http_client
.get(concat!(
"https://raw.githubusercontent.com",
"/imputnet/cobalt/refs/heads/main/web/i18n/en/error.json"
))
.send()
.await?
.server_error()?;
.await?;

Ok(response.json().await?)
request.json().await
}
8 changes: 8 additions & 0 deletions src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use crate::bot::TdError;
use crate::utilities::api_utils::ServerError;
use crate::utilities::command_context::CommandContext;
use crate::utilities::convert_argument::ConversionError;
use crate::utilities::file_download::DownloadError;
use crate::utilities::rate_limit::RateLimiter;

pub mod autocomplete;
Expand Down Expand Up @@ -66,6 +67,7 @@ pub enum CommandError {
Telegram(TdError),
Server(StatusCode),
Reqwest(reqwest::Error),
Download(DownloadError),
}

impl From<String> for CommandError {
Expand Down Expand Up @@ -114,3 +116,9 @@ impl From<reqwest::Error> for CommandError {
Self::Reqwest(value)
}
}

impl From<DownloadError> for CommandError {
fn from(value: DownloadError) -> Self {
Self::Download(value)
}
}
Loading

0 comments on commit 0742427

Please sign in to comment.