Compare commits

..

10 Commits

8 changed files with 499 additions and 725 deletions

1
.dockerignore Normal file
View File

@ -0,0 +1 @@
/target

954
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,14 +1,17 @@
[package]
name = "dzmedia"
version = "0.1.0"
edition = "2018"
edition = "2021"
# 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 }
axum = "0.5"
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"] }
reqwest = { version = "0.11", features = ["json", "rustls-tls"], default-features = false }
reqwest = { version = "0.11", features = ["json", "rustls-tls", "cookies", "gzip"], default-features = false }
serde_json = "1.0"
thiserror = "1.0"
regex = "1"

31
Dockerfile Normal file
View File

@ -0,0 +1,31 @@
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: ROCKET_ADDRESS=0.0.0.0 ROCKET_PORT=$PORT ROCKET_KEEP_ALIVE=0 ./target/release/dzmedia
web: ./target/release/dzmedia

35
fly.toml Normal file
View File

@ -0,0 +1,35 @@
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 std::{env, marker::Sized, time::Instant};
use std::{env, marker::Sized, time::Instant, sync::Arc};
use thiserror::Error;
use serde_json::{json, value::from_value};
use regex::Regex;
use reqwest::{Client, Response, cookie::Jar, Url, header::ACCEPT};
#[derive(Deserialize)]
struct DeezerResponse {
@ -52,23 +52,28 @@ pub enum APIError {
#[derive(Clone)]
pub struct APIClient {
client: reqwest::Client,
license_token: String,
client: Client,
pub 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") {
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 {
client: reqwest::Client::new(),
client: builder.build().unwrap(),
license_token: String::new(),
check_form: String::new(),
sid: String::new(),
arl,
renew_instant: None,
}
} else {
@ -76,8 +81,9 @@ impl APIClient {
}
}
async fn raw_api_call<T>(&self, method: &str, params: &T) -> Result<reqwest::Response, reqwest::Error>
where T: Serialize + ?Sized
async fn no_renew_api_call<P, T>(&mut self, method: &str, params: &P) -> Result<T, APIError>
where P: Serialize + ?Sized,
T: DeserializeOwned
{
let check_form;
if method == "deezer.getUserData" {
@ -86,40 +92,18 @@ impl APIClient {
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")
let json: DeezerResponse = self.client.post("https://www.deezer.com/ajax/gw-light.php")
.query(&[
("method", method),
("input", "3"),
("api_version", "1.0"),
("api_token", check_form)
])
.header(ACCEPT, "*/*")
.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>()
.json()
.await?;
if let Some(error) = json.error.as_object() {
@ -134,24 +118,33 @@ impl APIClient {
Ok(from_value(json.results)?)
}
async fn renew(&mut self) -> Result<(), reqwest::Error> {
let resp = self.raw_api_call("deezer.getUserData", &json!({})).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 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();
self.no_renew_api_call(method, params).await
}
let json = resp.json::<DeezerResponse>().await?.results;
async fn renew(&mut self) -> Result<(), APIError> {
let user_data: serde_json::Value = self.no_renew_api_call("deezer.getUserData", &json!({})).await?;
self.check_form = json["checkForm"].as_str().unwrap().to_string();
self.license_token = json["USER"]["OPTIONS"]["license_token"].as_str().unwrap().to_string();
self.check_form = user_data["checkForm"].as_str().unwrap().to_string();
self.license_token = user_data["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> {
pub async fn get_media(&self, formats: &Vec<Format>, track_tokens: Vec<&str>) -> Result<Response, reqwest::Error> {
let formats: Vec<DeezerFormat> = formats.iter().map(|f| DeezerFormat { cipher: "BF_CBC_STRIPE", format: f }).collect();
let req = json!({

View File

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