bruh commit
This commit is contained in:
commit
b7add9a9a4
|
@ -0,0 +1,12 @@
|
||||||
|
# Generated by Cargo
|
||||||
|
# will have compiled files and executables
|
||||||
|
debug/
|
||||||
|
target/
|
||||||
|
|
||||||
|
# These are backup files generated by rustfmt
|
||||||
|
**/*.rs.bk
|
||||||
|
|
||||||
|
# MSVC Windows builds of rustc generate these, which store debugging information
|
||||||
|
*.pdb
|
||||||
|
|
||||||
|
.vscode
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,14 @@
|
||||||
|
[package]
|
||||||
|
name = "dzmedia"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2018"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
rocket = { version = "0.5.0-rc.1", features = ["json"], default-features = false }
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
reqwest = { version = "0.11", features = ["json", "rustls-tls"], default-features = false }
|
||||||
|
serde_json = "1.0"
|
||||||
|
thiserror = "1.0"
|
||||||
|
regex = "1"
|
|
@ -0,0 +1 @@
|
||||||
|
web: ROCKET_PORT=$PORT ROCKET_KEEP_ALIVE=0 ./target/release/dzmedia
|
|
@ -0,0 +1,171 @@
|
||||||
|
use serde::{Serialize, Deserialize, de::DeserializeOwned};
|
||||||
|
use std::{env, marker::Sized, time::Instant};
|
||||||
|
use thiserror::Error;
|
||||||
|
use serde_json::{json, value::from_value};
|
||||||
|
use regex::Regex;
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct DeezerResponse {
|
||||||
|
error: serde_json::Value,
|
||||||
|
results: serde_json::Value,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, Debug)]
|
||||||
|
#[allow(non_camel_case_types)]
|
||||||
|
pub enum Format {
|
||||||
|
AAC_64,
|
||||||
|
AAC_96,
|
||||||
|
FLAC,
|
||||||
|
MP3_MISC,
|
||||||
|
MP3_32,
|
||||||
|
MP3_64,
|
||||||
|
MP3_128,
|
||||||
|
MP3_192,
|
||||||
|
MP3_256,
|
||||||
|
MP3_320,
|
||||||
|
SBC_256,
|
||||||
|
MP4_RA1,
|
||||||
|
MP4_RA2,
|
||||||
|
MP4_RA3,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct DeezerFormat<'a> {
|
||||||
|
cipher: &'a str,
|
||||||
|
format: &'a Format,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
pub enum APIError {
|
||||||
|
#[error("reqwest error")]
|
||||||
|
Reqwest(#[from] reqwest::Error),
|
||||||
|
|
||||||
|
#[error("Deezer API error (code: {code:?}, message: {message:?})")]
|
||||||
|
DeezerError {
|
||||||
|
code: String,
|
||||||
|
message: String,
|
||||||
|
},
|
||||||
|
|
||||||
|
#[error("Couldn't deserialize JSON response")]
|
||||||
|
JSON(#[from] serde_json::error::Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct APIClient {
|
||||||
|
client: reqwest::Client,
|
||||||
|
license_token: String,
|
||||||
|
check_form: String,
|
||||||
|
pub sid: String,
|
||||||
|
arl: String,
|
||||||
|
renew_instant: Option<Instant>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl APIClient {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
if let Ok(arl) = env::var("ARL") {
|
||||||
|
Self {
|
||||||
|
client: reqwest::Client::new(),
|
||||||
|
license_token: String::new(),
|
||||||
|
check_form: String::new(),
|
||||||
|
sid: String::new(),
|
||||||
|
arl,
|
||||||
|
renew_instant: None,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
panic!("ARL environment variable required");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn raw_api_call<T>(&self, method: &str, params: &T) -> Result<reqwest::Response, reqwest::Error>
|
||||||
|
where T: Serialize + ?Sized
|
||||||
|
{
|
||||||
|
let check_form;
|
||||||
|
if method == "deezer.getUserData" {
|
||||||
|
check_form = ""
|
||||||
|
} else {
|
||||||
|
check_form = &self.check_form;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut cookies = format!("arl={}", self.arl);
|
||||||
|
|
||||||
|
if self.sid != "" && method != "deezer.getUserData" {
|
||||||
|
cookies.push_str(&format!("; sid={}", self.sid))
|
||||||
|
}
|
||||||
|
|
||||||
|
self.client.post("https://www.deezer.com/ajax/gw-light.php")
|
||||||
|
.query(&[
|
||||||
|
("method", method),
|
||||||
|
("input", "3"),
|
||||||
|
("api_version", "1.0"),
|
||||||
|
("api_token", check_form)
|
||||||
|
])
|
||||||
|
.json(params)
|
||||||
|
.header("cookie", &cookies)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn api_call<P, T>(&mut self, method: &str, params: &P) -> Result<T, APIError>
|
||||||
|
where P: Serialize + ?Sized,
|
||||||
|
T: DeserializeOwned
|
||||||
|
{
|
||||||
|
if let Some(i) = self.renew_instant {
|
||||||
|
if i.elapsed().as_secs() >= 3600 {
|
||||||
|
self.renew().await?;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self.renew().await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let json = self.raw_api_call(method, params)
|
||||||
|
.await?
|
||||||
|
.json::<DeezerResponse>()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if let Some(error) = json.error.as_object() {
|
||||||
|
for (code, message) in error {
|
||||||
|
return Err(APIError::DeezerError {
|
||||||
|
code: code.clone(),
|
||||||
|
message: message.as_str().unwrap().to_string()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(from_value(json.results)?)
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn renew(&mut self) -> Result<(), reqwest::Error> {
|
||||||
|
let resp = self.raw_api_call("deezer.getUserData", &json!({})).await?;
|
||||||
|
|
||||||
|
let sid = resp.headers().get("set-cookie").unwrap().to_str().unwrap();
|
||||||
|
let sid = Regex::new("^sid=(fr[\\da-f]+)").unwrap().captures(sid).unwrap();
|
||||||
|
self.sid = (&sid[1]).to_string();
|
||||||
|
|
||||||
|
let json = resp.json::<DeezerResponse>().await?.results;
|
||||||
|
|
||||||
|
self.check_form = json["checkForm"].as_str().unwrap().to_string();
|
||||||
|
self.license_token = json["USER"]["OPTIONS"]["license_token"].as_str().unwrap().to_string();
|
||||||
|
|
||||||
|
self.renew_instant = Some(Instant::now());
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn get_media(&self, formats: &Vec<Format>, track_tokens: Vec<&str>) -> Result<reqwest::Response, reqwest::Error> {
|
||||||
|
let formats: Vec<DeezerFormat> = formats.iter().map(|f| DeezerFormat { cipher: "BF_CBC_STRIPE", format: f }).collect();
|
||||||
|
|
||||||
|
let req = json!({
|
||||||
|
"license_token": self.license_token,
|
||||||
|
"media": [{
|
||||||
|
"formats": formats,
|
||||||
|
"type": "FULL"
|
||||||
|
}],
|
||||||
|
"track_tokens": track_tokens
|
||||||
|
});
|
||||||
|
|
||||||
|
self.client.post("https://media.deezer.com/v1/get_url")
|
||||||
|
.json(&req)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,86 @@
|
||||||
|
#[macro_use] extern crate rocket;
|
||||||
|
|
||||||
|
use rocket::{serde::{Deserialize, json::Json}, State, http::Status};
|
||||||
|
use serde_json::json;
|
||||||
|
use std::sync::RwLock;
|
||||||
|
|
||||||
|
mod api;
|
||||||
|
use api::{APIClient, APIError, Format};
|
||||||
|
|
||||||
|
struct StateData {
|
||||||
|
client: APIClient
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct DeezerTrackList {
|
||||||
|
data: Vec<DeezerTrack>
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
#[allow(non_snake_case)]
|
||||||
|
struct DeezerTrack {
|
||||||
|
TRACK_TOKEN: String
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/")]
|
||||||
|
fn index() -> &'static str {
|
||||||
|
"marecchione gay af"
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
struct Request {
|
||||||
|
formats: Vec<Format>,
|
||||||
|
ids: Vec<u32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post("/get_url", data = "<req>")]
|
||||||
|
async fn get_url(req: Json<Request>, state_data: &State<RwLock<StateData>>) -> (Status, String) {
|
||||||
|
if req.formats.is_empty() {
|
||||||
|
return (Status::BadRequest, "Format list cannot be empty".to_string());
|
||||||
|
}
|
||||||
|
if req.ids.is_empty() {
|
||||||
|
return (Status::BadRequest, "ID list cannot be empty".to_string());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut client: APIClient;
|
||||||
|
let old_sid: String;
|
||||||
|
{
|
||||||
|
let state_data_read = state_data.read().unwrap();
|
||||||
|
old_sid = state_data_read.client.sid.clone();
|
||||||
|
client = state_data_read.client.clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
let resp: Result<DeezerTrackList, APIError> = client.api_call("song.getListData", &json!({"sng_ids":req.ids})).await;
|
||||||
|
let track_list;
|
||||||
|
match resp {
|
||||||
|
Ok(t) => track_list = t,
|
||||||
|
Err(e) => return (Status::InternalServerError, e.to_string()),
|
||||||
|
};
|
||||||
|
|
||||||
|
if track_list.data.is_empty() {
|
||||||
|
return (Status::BadRequest, "No valid IDs found".to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
let track_tokens: Vec<&str> = track_list.data.iter().map(|t| t.TRACK_TOKEN.as_str()).collect();
|
||||||
|
|
||||||
|
let media_result = client.get_media(&req.formats, track_tokens).await;
|
||||||
|
let media_resp;
|
||||||
|
match media_result {
|
||||||
|
Ok(r) => media_resp = r,
|
||||||
|
Err(_) => return (Status::InternalServerError, "Error while getting response from media.deezer.com".to_string())
|
||||||
|
};
|
||||||
|
|
||||||
|
if client.sid != old_sid {
|
||||||
|
let mut state_data_write = state_data.write().unwrap();
|
||||||
|
state_data_write.client = client;
|
||||||
|
}
|
||||||
|
|
||||||
|
(Status::Ok, media_resp.text().await.unwrap())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[launch]
|
||||||
|
fn rocket() -> _ {
|
||||||
|
rocket::build()
|
||||||
|
.manage(RwLock::new(StateData { client: APIClient::new() }))
|
||||||
|
.mount("/", routes![index, get_url])
|
||||||
|
}
|
Loading…
Reference in New Issue