Compare commits

..

No commits in common. "16cf5dc79bec3e21e65772938b15c36010c03e14" and "d272f602d834dd77f3e3108a1fd478504252fab4" have entirely different histories.

8 changed files with 732 additions and 506 deletions

View File

@ -1 +0,0 @@
/target

966
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,17 +1,14 @@
[package] [package]
name = "dzmedia" name = "dzmedia"
version = "0.1.0" version = "0.1.0"
edition = "2021" edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
axum = "0.5" rocket = { version = "0.5.0-rc.1", features = ["json"], default-features = false }
http = "0.2"
tower-http = { version = "0.3", features = ["cors", "trace", "compression-br", "compression-deflate", "compression-gzip"] }
tracing-subscriber = "0.3"
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
reqwest = { version = "0.11", features = ["json", "rustls-tls", "cookies", "gzip"], default-features = false } reqwest = { version = "0.11", features = ["json", "rustls-tls"], default-features = false }
serde_json = "1.0" serde_json = "1.0"
thiserror = "1.0" thiserror = "1.0"
regex = "1"

View File

@ -1,31 +0,0 @@
FROM rust:latest as builder
# Make a fake Rust app to keep a cached layer of compiled crates
RUN USER=root cargo new app
WORKDIR /usr/src/app
COPY Cargo.toml Cargo.lock ./
# Needs at least a main.rs file with a main function
RUN mkdir src && echo "fn main(){}" > src/main.rs
# Will build all dependent crates in release mode
RUN --mount=type=cache,target=/usr/local/cargo/registry \
--mount=type=cache,target=/usr/src/app/target \
cargo build --release
# Copy the rest
COPY . .
# Build (install) the actual binaries
RUN cargo install --path .
# Runtime image
FROM debian:bullseye-slim
# Run as "app" user
RUN useradd -ms /bin/bash app
USER app
WORKDIR /app
# Get compiled binaries from builder's cargo install directory
COPY --from=builder /usr/local/cargo/bin/dzmedia /app/dzmedia
# No CMD or ENTRYPOINT, see fly.toml with `cmd` override.

View File

@ -1 +1 @@
web: ./target/release/dzmedia web: ROCKET_ADDRESS=0.0.0.0 ROCKET_PORT=$PORT ROCKET_KEEP_ALIVE=0 ./target/release/dzmedia

View File

@ -1,35 +0,0 @@
app = "dzmedia"
kill_signal = "SIGINT"
kill_timeout = 5
[experimental]
# required because we can't infer your binary's name
cmd = "./dzmedia"
[env]
PORT = "8080"
RUST_LOG = "tower_http=trace"
[[services]]
internal_port = 8080
protocol = "tcp"
[services.concurrency]
hard_limit = 25
soft_limit = 20
[[services.ports]]
handlers = ["http"]
port = 80
[[services.ports]]
handlers = ["tls", "http"]
port = 443
[[services.tcp_checks]]
grace_period = "1s"
interval = "15s"
port = "8080"
restart_limit = 6
timeout = "2s"

View File

@ -1,8 +1,8 @@
use serde::{Serialize, Deserialize, de::DeserializeOwned}; use serde::{Serialize, Deserialize, de::DeserializeOwned};
use std::{env, marker::Sized, time::Instant, sync::Arc}; use std::{env, marker::Sized, time::Instant};
use thiserror::Error; use thiserror::Error;
use serde_json::{json, value::from_value}; use serde_json::{json, value::from_value};
use reqwest::{Client, Response, cookie::Jar, Url, header::ACCEPT}; use regex::Regex;
#[derive(Deserialize)] #[derive(Deserialize)]
struct DeezerResponse { struct DeezerResponse {
@ -52,28 +52,23 @@ pub enum APIError {
#[derive(Clone)] #[derive(Clone)]
pub struct APIClient { pub struct APIClient {
client: Client, client: reqwest::Client,
pub license_token: String, license_token: String,
check_form: String, check_form: String,
pub sid: String,
arl: String,
renew_instant: Option<Instant>, renew_instant: Option<Instant>,
} }
impl APIClient { impl APIClient {
pub fn new() -> Self { pub fn new() -> Self {
if let Ok(arl) = env::var("ARL") { if let Ok(arl) = env::var("ARL") {
let builder = Client::builder();
let cookie = format!("arl={}; Domain=.deezer.com", arl);
let url = "https://www.deezer.com".parse::<Url>().unwrap();
let jar = Jar::default();
jar.add_cookie_str(&cookie, &url);
let builder = builder.cookie_provider(Arc::new(jar));
Self { Self {
client: builder.build().unwrap(), client: reqwest::Client::new(),
license_token: String::new(), license_token: String::new(),
check_form: String::new(), check_form: String::new(),
sid: String::new(),
arl,
renew_instant: None, renew_instant: None,
} }
} else { } else {
@ -81,9 +76,8 @@ impl APIClient {
} }
} }
async fn no_renew_api_call<P, T>(&mut self, method: &str, params: &P) -> Result<T, APIError> async fn raw_api_call<T>(&self, method: &str, params: &T) -> Result<reqwest::Response, reqwest::Error>
where P: Serialize + ?Sized, where T: Serialize + ?Sized
T: DeserializeOwned
{ {
let check_form; let check_form;
if method == "deezer.getUserData" { if method == "deezer.getUserData" {
@ -92,30 +86,23 @@ impl APIClient {
check_form = &self.check_form; check_form = &self.check_form;
} }
let json: DeezerResponse = self.client.post("https://www.deezer.com/ajax/gw-light.php") 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(&[ .query(&[
("method", method), ("method", method),
("input", "3"), ("input", "3"),
("api_version", "1.0"), ("api_version", "1.0"),
("api_token", check_form) ("api_token", check_form)
]) ])
.header(ACCEPT, "*/*")
.json(params) .json(params)
.header("cookie", &cookies)
.send() .send()
.await? .await
.json()
.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)?)
} }
pub async fn api_call<P, T>(&mut self, method: &str, params: &P) -> Result<T, APIError> pub async fn api_call<P, T>(&mut self, method: &str, params: &P) -> Result<T, APIError>
@ -130,21 +117,41 @@ impl APIClient {
self.renew().await?; self.renew().await?;
} }
self.no_renew_api_call(method, params).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<(), APIError> { async fn renew(&mut self) -> Result<(), reqwest::Error> {
let user_data: serde_json::Value = self.no_renew_api_call("deezer.getUserData", &json!({})).await?; let resp = self.raw_api_call("deezer.getUserData", &json!({})).await?;
self.check_form = user_data["checkForm"].as_str().unwrap().to_string(); let sid = resp.headers().get("set-cookie").unwrap().to_str().unwrap();
self.license_token = user_data["USER"]["OPTIONS"]["license_token"].as_str().unwrap().to_string(); 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()); self.renew_instant = Some(Instant::now());
Ok(()) Ok(())
} }
pub async fn get_media(&self, formats: &Vec<Format>, track_tokens: Vec<&str>) -> Result<Response, reqwest::Error> { 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 formats: Vec<DeezerFormat> = formats.iter().map(|f| DeezerFormat { cipher: "BF_CBC_STRIPE", format: f }).collect();
let req = json!({ let req = json!({

View File

@ -1,18 +1,19 @@
use axum::{ #[macro_use] extern crate rocket;
routing::{get, post},
http::StatusCode, use rocket::{serde::{Deserialize, json::Json}, State, http::Status};
response::IntoResponse, use rocket::http::Header;
Json, Router, Extension, use rocket::{Request, Response};
}; use rocket::fairing::{Fairing, Info, Kind};
use tower_http::{cors::{CorsLayer, Any}, compression::CompressionLayer, trace::TraceLayer};
use http::{Method, header::CONTENT_TYPE};
use serde_json::json; use serde_json::json;
use serde::Deserialize; use std::sync::RwLock;
use std::sync::{Arc, RwLock};
mod api; mod api;
use api::{APIClient, APIError, Format}; use api::{APIClient, APIError, Format};
struct StateData {
client: APIClient
}
#[derive(Deserialize)] #[derive(Deserialize)]
struct DeezerTrackList { struct DeezerTrackList {
data: Vec<DeezerTrack> data: Vec<DeezerTrack>
@ -24,7 +25,8 @@ struct DeezerTrack {
TRACK_TOKEN: String TRACK_TOKEN: String
} }
async fn root() -> &'static str { #[get("/")]
fn index() -> &'static str {
"marecchione gay af" "marecchione gay af"
} }
@ -34,26 +36,32 @@ struct RequestParams {
ids: Vec<u32>, ids: Vec<u32>,
} }
async fn get_url(Json(req): Json<RequestParams>, Extension(state): Extension<Arc<RwLock<APIClient>>>) -> impl IntoResponse { #[post("/get_url", format = "json", data = "<req>")]
async fn get_url(req: Json<RequestParams>, state_data: &State<RwLock<StateData>>) -> (Status, String) {
if req.formats.is_empty() { if req.formats.is_empty() {
return (StatusCode::BAD_REQUEST, "Format list cannot be empty".to_string()); return (Status::BadRequest, "Format list cannot be empty".to_string());
} }
if req.ids.is_empty() { if req.ids.is_empty() {
return (StatusCode::BAD_REQUEST, "ID list cannot be empty".to_string()); return (Status::BadRequest, "ID list cannot be empty".to_string());
} }
let mut client = state.read().unwrap().clone(); let mut client: APIClient;
let old_license = client.license_token.clone(); 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,"array_default":["SNG_ID","TRACK_TOKEN"]})).await; let resp: Result<DeezerTrackList, APIError> = client.api_call("song.getListData", &json!({"sng_ids":req.ids,"array_default":["SNG_ID","TRACK_TOKEN"]})).await;
let track_list; let track_list;
match resp { match resp {
Ok(t) => track_list = t, Ok(t) => track_list = t,
Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()), Err(e) => return (Status::InternalServerError, e.to_string()),
}; };
if track_list.data.is_empty() { if track_list.data.is_empty() {
return (StatusCode::BAD_REQUEST, "No valid IDs found".to_string()); 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 track_tokens: Vec<&str> = track_list.data.iter().map(|t| t.TRACK_TOKEN.as_str()).collect();
@ -62,46 +70,43 @@ async fn get_url(Json(req): Json<RequestParams>, Extension(state): Extension<Arc
let media_resp; let media_resp;
match media_result { match media_result {
Ok(r) => media_resp = r, Ok(r) => media_resp = r,
Err(_) => return (StatusCode::INTERNAL_SERVER_ERROR, "Error while getting response from media.deezer.com".to_string()), Err(_) => return (Status::InternalServerError, "Error while getting response from media.deezer.com".to_string())
}; };
if client.license_token != old_license { if client.sid != old_sid {
let mut client_write = state.write().unwrap(); let mut state_data_write = state_data.write().unwrap();
*client_write = client; state_data_write.client = client;
} }
(StatusCode::OK, media_resp.text().await.unwrap()) (Status::Ok, media_resp.text().await.unwrap())
} }
#[tokio::main] #[options("/get_url")]
async fn main() { fn send_options() -> Status {
let bind_addr = std::env::var("BIND_ADDR").unwrap_or("[::]".to_string()); Status::Ok
let port = std::env::var("PORT").unwrap_or("8000".to_string()); }
let port: u16 = port.parse().unwrap_or(8000);
pub struct CORS;
let shared_state = Arc::new(RwLock::new(APIClient::new()));
#[rocket::async_trait]
let cors = CorsLayer::new() impl Fairing for CORS {
.allow_origin(Any) fn info(&self) -> Info {
.allow_methods([Method::GET, Method::POST, Method::OPTIONS, Method::HEAD]) Info {
.allow_headers([CONTENT_TYPE]); name: "Add CORS headers to responses",
kind: Kind::Response
tracing_subscriber::fmt::init(); }
}
let app = Router::new()
.route("/", get(root)) async fn on_response<'r>(&self, _request: &'r Request<'_>, response: &mut Response<'r>) {
.route("/get_url", post(get_url)) response.set_header(Header::new("Access-Control-Allow-Origin", "*"));
.layer(Extension(shared_state)) response.set_header(Header::new("Access-Control-Allow-Headers", "*"));
.layer(cors) }
.layer(CompressionLayer::new()) }
.layer(TraceLayer::new_for_http());
#[launch]
let bind_addr = format!("{bind_addr}:{port}"); fn rocket() -> _ {
println!("Listening on {bind_addr}"); rocket::build()
let bind_addr = bind_addr.parse().unwrap(); .manage(RwLock::new(StateData { client: APIClient::new() }))
.mount("/", routes![index, get_url, send_options])
axum::Server::bind(&bind_addr) .attach(CORS)
.serve(app.into_make_service())
.await
.unwrap();
} }