Compare commits
10 Commits
d272f602d8
...
16cf5dc79b
Author | SHA1 | Date |
---|---|---|
uh wot | 16cf5dc79b | |
uh wot | 6a4bec9e70 | |
uh wot | ae75a24129 | |
uh wot | e72779c338 | |
uh wot | 2429be10e0 | |
uh wot | af1be2bd2a | |
uh wot | 21b10a1957 | |
uh wot | d2a7090033 | |
uh wot | 89a9303321 | |
uh wot | 4773b190b2 |
|
@ -0,0 +1 @@
|
||||||
|
/target
|
File diff suppressed because it is too large
Load Diff
13
Cargo.toml
13
Cargo.toml
|
@ -1,14 +1,17 @@
|
||||||
[package]
|
[package]
|
||||||
name = "dzmedia"
|
name = "dzmedia"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2018"
|
edition = "2021"
|
||||||
|
|
||||||
# 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]
|
||||||
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"] }
|
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"
|
serde_json = "1.0"
|
||||||
thiserror = "1.0"
|
thiserror = "1.0"
|
||||||
regex = "1"
|
|
|
@ -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.
|
2
Procfile
2
Procfile
|
@ -1 +1 @@
|
||||||
web: ROCKET_ADDRESS=0.0.0.0 ROCKET_PORT=$PORT ROCKET_KEEP_ALIVE=0 ./target/release/dzmedia
|
web: ./target/release/dzmedia
|
|
@ -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"
|
83
src/api.rs
83
src/api.rs
|
@ -1,8 +1,8 @@
|
||||||
use serde::{Serialize, Deserialize, de::DeserializeOwned};
|
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 thiserror::Error;
|
||||||
use serde_json::{json, value::from_value};
|
use serde_json::{json, value::from_value};
|
||||||
use regex::Regex;
|
use reqwest::{Client, Response, cookie::Jar, Url, header::ACCEPT};
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
struct DeezerResponse {
|
struct DeezerResponse {
|
||||||
|
@ -52,23 +52,28 @@ pub enum APIError {
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct APIClient {
|
pub struct APIClient {
|
||||||
client: reqwest::Client,
|
client: Client,
|
||||||
license_token: String,
|
pub 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: reqwest::Client::new(),
|
client: builder.build().unwrap(),
|
||||||
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 {
|
||||||
|
@ -76,8 +81,9 @@ impl APIClient {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn raw_api_call<T>(&self, method: &str, params: &T) -> Result<reqwest::Response, reqwest::Error>
|
async fn no_renew_api_call<P, T>(&mut self, method: &str, params: &P) -> Result<T, APIError>
|
||||||
where T: Serialize + ?Sized
|
where P: Serialize + ?Sized,
|
||||||
|
T: DeserializeOwned
|
||||||
{
|
{
|
||||||
let check_form;
|
let check_form;
|
||||||
if method == "deezer.getUserData" {
|
if method == "deezer.getUserData" {
|
||||||
|
@ -86,40 +92,18 @@ impl APIClient {
|
||||||
check_form = &self.check_form;
|
check_form = &self.check_form;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut cookies = format!("arl={}", self.arl);
|
let json: DeezerResponse = self.client.post("https://www.deezer.com/ajax/gw-light.php")
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
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?
|
.await?
|
||||||
.json::<DeezerResponse>()
|
.json()
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
if let Some(error) = json.error.as_object() {
|
if let Some(error) = json.error.as_object() {
|
||||||
|
@ -134,24 +118,33 @@ impl APIClient {
|
||||||
Ok(from_value(json.results)?)
|
Ok(from_value(json.results)?)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn renew(&mut self) -> Result<(), reqwest::Error> {
|
pub async fn api_call<P, T>(&mut self, method: &str, params: &P) -> Result<T, APIError>
|
||||||
let resp = self.raw_api_call("deezer.getUserData", &json!({})).await?;
|
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();
|
self.no_renew_api_call(method, params).await
|
||||||
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;
|
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.check_form = user_data["checkForm"].as_str().unwrap().to_string();
|
||||||
self.license_token = json["USER"]["OPTIONS"]["license_token"].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());
|
self.renew_instant = Some(Instant::now());
|
||||||
|
|
||||||
Ok(())
|
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 formats: Vec<DeezerFormat> = formats.iter().map(|f| DeezerFormat { cipher: "BF_CBC_STRIPE", format: f }).collect();
|
||||||
|
|
||||||
let req = json!({
|
let req = json!({
|
||||||
|
|
105
src/main.rs
105
src/main.rs
|
@ -1,19 +1,18 @@
|
||||||
#[macro_use] extern crate rocket;
|
use axum::{
|
||||||
|
routing::{get, post},
|
||||||
use rocket::{serde::{Deserialize, json::Json}, State, http::Status};
|
http::StatusCode,
|
||||||
use rocket::http::Header;
|
response::IntoResponse,
|
||||||
use rocket::{Request, Response};
|
Json, Router, Extension,
|
||||||
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 std::sync::RwLock;
|
use serde::Deserialize;
|
||||||
|
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>
|
||||||
|
@ -25,8 +24,7 @@ struct DeezerTrack {
|
||||||
TRACK_TOKEN: String
|
TRACK_TOKEN: String
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/")]
|
async fn root() -> &'static str {
|
||||||
fn index() -> &'static str {
|
|
||||||
"marecchione gay af"
|
"marecchione gay af"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -36,32 +34,26 @@ struct RequestParams {
|
||||||
ids: Vec<u32>,
|
ids: Vec<u32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/get_url", format = "json", data = "<req>")]
|
async fn get_url(Json(req): Json<RequestParams>, Extension(state): Extension<Arc<RwLock<APIClient>>>) -> impl IntoResponse {
|
||||||
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 (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() {
|
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 mut client = state.read().unwrap().clone();
|
||||||
let old_sid: String;
|
let old_license = client.license_token.clone();
|
||||||
{
|
|
||||||
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 (Status::InternalServerError, e.to_string()),
|
Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()),
|
||||||
};
|
};
|
||||||
|
|
||||||
if track_list.data.is_empty() {
|
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();
|
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;
|
let media_resp;
|
||||||
match media_result {
|
match media_result {
|
||||||
Ok(r) => media_resp = r,
|
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 {
|
if client.license_token != old_license {
|
||||||
let mut state_data_write = state_data.write().unwrap();
|
let mut client_write = state.write().unwrap();
|
||||||
state_data_write.client = client;
|
*client_write = client;
|
||||||
}
|
}
|
||||||
|
|
||||||
(Status::Ok, media_resp.text().await.unwrap())
|
(StatusCode::OK, media_resp.text().await.unwrap())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[options("/get_url")]
|
#[tokio::main]
|
||||||
fn send_options() -> Status {
|
async fn main() {
|
||||||
Status::Ok
|
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);
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn on_response<'r>(&self, _request: &'r Request<'_>, response: &mut Response<'r>) {
|
tracing_subscriber::fmt::init();
|
||||||
response.set_header(Header::new("Access-Control-Allow-Origin", "*"));
|
|
||||||
response.set_header(Header::new("Access-Control-Allow-Headers", "*"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[launch]
|
let app = Router::new()
|
||||||
fn rocket() -> _ {
|
.route("/", get(root))
|
||||||
rocket::build()
|
.route("/get_url", post(get_url))
|
||||||
.manage(RwLock::new(StateData { client: APIClient::new() }))
|
.layer(Extension(shared_state))
|
||||||
.mount("/", routes![index, get_url, send_options])
|
.layer(cors)
|
||||||
.attach(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();
|
||||||
}
|
}
|
Loading…
Reference in New Issue