Spaces:
Paused
Paused
Upload main.rs
Browse files- src/main.rs +303 -0
src/main.rs
CHANGED
|
@@ -1064,6 +1064,87 @@ async fn gw_light_call(
|
|
| 1064 |
}
|
| 1065 |
}
|
| 1066 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1067 |
fn jar_has_cookie(jar: &Jar, url: &Url, cookie_prefix: &str) -> bool {
|
| 1068 |
let Some(hv) = jar.cookies(url) else { return false };
|
| 1069 |
let Ok(s) = hv.to_str() else { return false };
|
|
@@ -1799,6 +1880,226 @@ async fn oauth_arl(Json(req): Json<OauthArlReq>) -> impl IntoResponse {
|
|
| 1799 |
(StatusCode::OK, Json(json!({ "arl": arl }))).into_response()
|
| 1800 |
}
|
| 1801 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1802 |
#[derive(Debug, Deserialize)]
|
| 1803 |
struct OauthFlowReq {
|
| 1804 |
access_token: String,
|
|
@@ -2023,6 +2324,8 @@ async fn main() {
|
|
| 2023 |
.route("/pair/oauth", post(pair_oauth))
|
| 2024 |
.route("/pair/oauth_cb", get(pair_oauth_cb))
|
| 2025 |
.route("/pair/complete", post(pair_complete))
|
|
|
|
|
|
|
| 2026 |
.route("/oauth/me", post(oauth_me))
|
| 2027 |
.route("/oauth/playlists", post(oauth_playlists))
|
| 2028 |
.route("/oauth/playlist_tracks", post(oauth_playlist_tracks))
|
|
|
|
| 1064 |
}
|
| 1065 |
}
|
| 1066 |
|
| 1067 |
+
async fn gw_light_call_custom(
|
| 1068 |
+
client: &reqwest::Client,
|
| 1069 |
+
method: &str,
|
| 1070 |
+
api_token: &str,
|
| 1071 |
+
gateway_input: Option<&str>,
|
| 1072 |
+
params: &serde_json::Value,
|
| 1073 |
+
user_id: Option<&str>,
|
| 1074 |
+
use_get: bool,
|
| 1075 |
+
) -> Result<serde_json::Value, String> {
|
| 1076 |
+
fn cid() -> String {
|
| 1077 |
+
SystemTime::now()
|
| 1078 |
+
.duration_since(UNIX_EPOCH)
|
| 1079 |
+
.map(|d| d.as_millis().to_string())
|
| 1080 |
+
.unwrap_or_else(|_| "0".to_string())
|
| 1081 |
+
}
|
| 1082 |
+
|
| 1083 |
+
let cid = cid();
|
| 1084 |
+
let mut req = if use_get || method == "user.getArl" {
|
| 1085 |
+
client.get("https://www.deezer.com/ajax/gw-light.php")
|
| 1086 |
+
} else {
|
| 1087 |
+
client
|
| 1088 |
+
.post("https://www.deezer.com/ajax/gw-light.php")
|
| 1089 |
+
.header("Content-Type", "text/plain;charset=UTF-8")
|
| 1090 |
+
.body(params.to_string())
|
| 1091 |
+
};
|
| 1092 |
+
|
| 1093 |
+
if let Some(uid) = user_id.filter(|s| !s.trim().is_empty()) {
|
| 1094 |
+
req = req.header("x-deezer-user", uid);
|
| 1095 |
+
}
|
| 1096 |
+
|
| 1097 |
+
let mut qp: Vec<(&str, &str)> = vec![
|
| 1098 |
+
("method", method),
|
| 1099 |
+
("input", "3"),
|
| 1100 |
+
("api_version", "1.0"),
|
| 1101 |
+
("api_token", api_token),
|
| 1102 |
+
("cid", cid.as_str()),
|
| 1103 |
+
];
|
| 1104 |
+
if let Some(gi) = gateway_input.filter(|s| !s.trim().is_empty()) {
|
| 1105 |
+
qp.push(("gateway_input", gi));
|
| 1106 |
+
}
|
| 1107 |
+
|
| 1108 |
+
let r = req
|
| 1109 |
+
.query(&qp)
|
| 1110 |
+
.header(ACCEPT, "*/*")
|
| 1111 |
+
.header("Cache-Control", "max-age=0")
|
| 1112 |
+
.header("Connection", "keep-alive")
|
| 1113 |
+
.header("Origin", "https://www.deezer.com")
|
| 1114 |
+
.header("Referer", "https://www.deezer.com/")
|
| 1115 |
+
.header("Sec-Fetch-Site", "same-origin")
|
| 1116 |
+
.header("Sec-Fetch-Mode", "cors")
|
| 1117 |
+
.header("Sec-Fetch-Dest", "empty")
|
| 1118 |
+
.header("X-Requested-With", "XMLHttpRequest")
|
| 1119 |
+
.header("Accept-Language", "en-US,en;q=0.9")
|
| 1120 |
+
.header("Content-Language", "en-US")
|
| 1121 |
+
.header("X-User-IP", "1.1.1.1")
|
| 1122 |
+
.header("x-deezer-client-ip", "1.1.1.1")
|
| 1123 |
+
.header(
|
| 1124 |
+
"User-Agent",
|
| 1125 |
+
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.130 Safari/537.36",
|
| 1126 |
+
)
|
| 1127 |
+
.send()
|
| 1128 |
+
.await
|
| 1129 |
+
.map_err(|_| "network".to_string())?;
|
| 1130 |
+
|
| 1131 |
+
let status = r.status();
|
| 1132 |
+
let text = r.text().await.map_err(|_| "read".to_string())?;
|
| 1133 |
+
let json: GwResp = serde_json::from_str(&text).map_err(|_| {
|
| 1134 |
+
let snip: String = text.chars().take(200).collect();
|
| 1135 |
+
format!("parse:{}:{}", status.as_u16(), snip)
|
| 1136 |
+
})?;
|
| 1137 |
+
|
| 1138 |
+
if let Some(error) = json.error.as_object() {
|
| 1139 |
+
for (code, message) in error {
|
| 1140 |
+
let msg = message.as_str().unwrap_or("");
|
| 1141 |
+
return Err(format!("{}:{}", code, msg));
|
| 1142 |
+
}
|
| 1143 |
+
}
|
| 1144 |
+
|
| 1145 |
+
Ok(json.results)
|
| 1146 |
+
}
|
| 1147 |
+
|
| 1148 |
fn jar_has_cookie(jar: &Jar, url: &Url, cookie_prefix: &str) -> bool {
|
| 1149 |
let Some(hv) = jar.cookies(url) else { return false };
|
| 1150 |
let Ok(s) = hv.to_str() else { return false };
|
|
|
|
| 1880 |
(StatusCode::OK, Json(json!({ "arl": arl }))).into_response()
|
| 1881 |
}
|
| 1882 |
|
| 1883 |
+
#[derive(Debug, Deserialize)]
|
| 1884 |
+
struct GwPageReq {
|
| 1885 |
+
arl: String,
|
| 1886 |
+
token: Option<String>,
|
| 1887 |
+
user_id: Option<String>,
|
| 1888 |
+
page: Option<String>,
|
| 1889 |
+
lang: Option<String>,
|
| 1890 |
+
}
|
| 1891 |
+
|
| 1892 |
+
async fn gw_page(Json(req): Json<GwPageReq>) -> impl IntoResponse {
|
| 1893 |
+
let arl = req.arl.trim().to_string();
|
| 1894 |
+
if arl.len() < 20 {
|
| 1895 |
+
return json_error(StatusCode::BAD_REQUEST, "arl required");
|
| 1896 |
+
}
|
| 1897 |
+
let page = req.page.unwrap_or_else(|| "home".to_string()).trim().to_string();
|
| 1898 |
+
if page.is_empty() {
|
| 1899 |
+
return json_error(StatusCode::BAD_REQUEST, "page required");
|
| 1900 |
+
}
|
| 1901 |
+
let lang = req
|
| 1902 |
+
.lang
|
| 1903 |
+
.unwrap_or_else(|| "en".to_string())
|
| 1904 |
+
.trim()
|
| 1905 |
+
.to_string();
|
| 1906 |
+
let lang = if lang.is_empty() { "en".to_string() } else { lang };
|
| 1907 |
+
|
| 1908 |
+
let jar = Arc::new(Jar::default());
|
| 1909 |
+
let url_deezer = "https://www.deezer.com".parse::<Url>().unwrap();
|
| 1910 |
+
jar.add_cookie_str("comeback=1; Domain=.deezer.com", &url_deezer);
|
| 1911 |
+
jar.add_cookie_str(&format!("arl={arl}; Domain=.deezer.com; Path=/"), &url_deezer);
|
| 1912 |
+
|
| 1913 |
+
let client = reqwest::Client::builder()
|
| 1914 |
+
.cookie_provider(jar.clone())
|
| 1915 |
+
.timeout(std::time::Duration::from_secs(20))
|
| 1916 |
+
.build()
|
| 1917 |
+
.unwrap();
|
| 1918 |
+
|
| 1919 |
+
let ud = match gw_light_call_custom(
|
| 1920 |
+
&client,
|
| 1921 |
+
"deezer.getUserData",
|
| 1922 |
+
"null",
|
| 1923 |
+
None,
|
| 1924 |
+
&json!({}),
|
| 1925 |
+
None,
|
| 1926 |
+
true,
|
| 1927 |
+
)
|
| 1928 |
+
.await
|
| 1929 |
+
{
|
| 1930 |
+
Ok(v) => v,
|
| 1931 |
+
Err(e) => return json_error(StatusCode::UNAUTHORIZED, format!("gw:getUserData:{e}")),
|
| 1932 |
+
};
|
| 1933 |
+
|
| 1934 |
+
let token = req
|
| 1935 |
+
.token
|
| 1936 |
+
.unwrap_or_default()
|
| 1937 |
+
.trim()
|
| 1938 |
+
.to_string()
|
| 1939 |
+
.chars()
|
| 1940 |
+
.filter(|c| !c.is_control())
|
| 1941 |
+
.collect::<String>();
|
| 1942 |
+
let api_token = if !token.is_empty() {
|
| 1943 |
+
token
|
| 1944 |
+
} else {
|
| 1945 |
+
ud.get("checkForm")
|
| 1946 |
+
.and_then(|x| x.as_str())
|
| 1947 |
+
.unwrap_or("")
|
| 1948 |
+
.trim()
|
| 1949 |
+
.to_string()
|
| 1950 |
+
};
|
| 1951 |
+
if api_token.is_empty() {
|
| 1952 |
+
return json_error(StatusCode::SERVICE_UNAVAILABLE, "gw:no_checkForm");
|
| 1953 |
+
}
|
| 1954 |
+
|
| 1955 |
+
let user_id = req
|
| 1956 |
+
.user_id
|
| 1957 |
+
.unwrap_or_default()
|
| 1958 |
+
.trim()
|
| 1959 |
+
.to_string()
|
| 1960 |
+
.chars()
|
| 1961 |
+
.filter(|c| c.is_ascii_digit())
|
| 1962 |
+
.collect::<String>();
|
| 1963 |
+
let user_id = if !user_id.is_empty() {
|
| 1964 |
+
Some(user_id)
|
| 1965 |
+
} else {
|
| 1966 |
+
ud.get("USER")
|
| 1967 |
+
.and_then(|u| u.get("USER_ID"))
|
| 1968 |
+
.and_then(|x| x.as_i64())
|
| 1969 |
+
.filter(|n| *n > 0)
|
| 1970 |
+
.map(|n| n.to_string())
|
| 1971 |
+
};
|
| 1972 |
+
|
| 1973 |
+
let support = json!({
|
| 1974 |
+
"ads": [],
|
| 1975 |
+
"deeplink-list": ["deeplink"],
|
| 1976 |
+
"event-card": ["live-event"],
|
| 1977 |
+
"grid-preview-one": ["album","artist","artistLineUp","channel","livestream","flow","playlist","radio","show","smarttracklist","track","user","video-link","external-link"],
|
| 1978 |
+
"grid-preview-two": ["album","artist","artistLineUp","channel","livestream","flow","playlist","radio","show","smarttracklist","track","user","video-link","external-link"],
|
| 1979 |
+
"grid": ["album","artist","artistLineUp","channel","livestream","flow","playlist","radio","show","smarttracklist","track","user","video-link","external-link"],
|
| 1980 |
+
"horizontal-grid": ["album","artist","artistLineUp","channel","livestream","flow","playlist","radio","show","smarttracklist","track","user","video-link","external-link"],
|
| 1981 |
+
"horizontal-list": ["track","song"],
|
| 1982 |
+
"item-highlight": ["radio"],
|
| 1983 |
+
"large-card": ["album","external-link","playlist","show","video-link"],
|
| 1984 |
+
"list": ["episode"],
|
| 1985 |
+
"mini-banner": ["external-link"],
|
| 1986 |
+
"slideshow": ["album","artist","channel","external-link","flow","livestream","playlist","show","smarttracklist","user","video-link"],
|
| 1987 |
+
"small-horizontal-grid": ["flow"],
|
| 1988 |
+
"long-card-horizontal-grid": ["album","artist","artistLineUp","channel","livestream","flow","playlist","radio","show","smarttracklist","track","user","video-link","external-link"],
|
| 1989 |
+
"filterable-grid": ["flow"]
|
| 1990 |
+
});
|
| 1991 |
+
|
| 1992 |
+
let gateway_input = json!({
|
| 1993 |
+
"PAGE": page,
|
| 1994 |
+
"VERSION": "2.5",
|
| 1995 |
+
"SUPPORT": support,
|
| 1996 |
+
"LANG": lang,
|
| 1997 |
+
"OPTIONS": ["deeplink_newsandentertainment", "deeplink_subscribeoffer"]
|
| 1998 |
+
})
|
| 1999 |
+
.to_string();
|
| 2000 |
+
|
| 2001 |
+
let results = match gw_light_call_custom(
|
| 2002 |
+
&client,
|
| 2003 |
+
"page.get",
|
| 2004 |
+
&api_token,
|
| 2005 |
+
Some(&gateway_input),
|
| 2006 |
+
&json!({}),
|
| 2007 |
+
user_id.as_deref(),
|
| 2008 |
+
false,
|
| 2009 |
+
)
|
| 2010 |
+
.await
|
| 2011 |
+
{
|
| 2012 |
+
Ok(v) => v,
|
| 2013 |
+
Err(e) => return json_error(StatusCode::SERVICE_UNAVAILABLE, format!("gw:page.get:{e}")),
|
| 2014 |
+
};
|
| 2015 |
+
|
| 2016 |
+
(StatusCode::OK, Json(json!({ "results": results }))).into_response()
|
| 2017 |
+
}
|
| 2018 |
+
|
| 2019 |
+
#[derive(Debug, Deserialize)]
|
| 2020 |
+
struct GwFlowReq {
|
| 2021 |
+
arl: String,
|
| 2022 |
+
token: Option<String>,
|
| 2023 |
+
user_id: String,
|
| 2024 |
+
config_id: Option<String>,
|
| 2025 |
+
}
|
| 2026 |
+
|
| 2027 |
+
async fn gw_flow(Json(req): Json<GwFlowReq>) -> impl IntoResponse {
|
| 2028 |
+
let arl = req.arl.trim().to_string();
|
| 2029 |
+
if arl.len() < 20 {
|
| 2030 |
+
return json_error(StatusCode::BAD_REQUEST, "arl required");
|
| 2031 |
+
}
|
| 2032 |
+
let user_id = req.user_id.trim().to_string();
|
| 2033 |
+
if user_id.is_empty() {
|
| 2034 |
+
return json_error(StatusCode::BAD_REQUEST, "user_id required");
|
| 2035 |
+
}
|
| 2036 |
+
|
| 2037 |
+
let jar = Arc::new(Jar::default());
|
| 2038 |
+
let url_deezer = "https://www.deezer.com".parse::<Url>().unwrap();
|
| 2039 |
+
jar.add_cookie_str("comeback=1; Domain=.deezer.com", &url_deezer);
|
| 2040 |
+
jar.add_cookie_str(&format!("arl={arl}; Domain=.deezer.com; Path=/"), &url_deezer);
|
| 2041 |
+
|
| 2042 |
+
let client = reqwest::Client::builder()
|
| 2043 |
+
.cookie_provider(jar.clone())
|
| 2044 |
+
.timeout(std::time::Duration::from_secs(20))
|
| 2045 |
+
.build()
|
| 2046 |
+
.unwrap();
|
| 2047 |
+
|
| 2048 |
+
let ud = match gw_light_call_custom(
|
| 2049 |
+
&client,
|
| 2050 |
+
"deezer.getUserData",
|
| 2051 |
+
"null",
|
| 2052 |
+
None,
|
| 2053 |
+
&json!({}),
|
| 2054 |
+
None,
|
| 2055 |
+
true,
|
| 2056 |
+
)
|
| 2057 |
+
.await
|
| 2058 |
+
{
|
| 2059 |
+
Ok(v) => v,
|
| 2060 |
+
Err(e) => return json_error(StatusCode::UNAUTHORIZED, format!("gw:getUserData:{e}")),
|
| 2061 |
+
};
|
| 2062 |
+
|
| 2063 |
+
let token = req.token.unwrap_or_default().trim().to_string();
|
| 2064 |
+
let api_token = if !token.is_empty() {
|
| 2065 |
+
token
|
| 2066 |
+
} else {
|
| 2067 |
+
ud.get("checkForm")
|
| 2068 |
+
.and_then(|x| x.as_str())
|
| 2069 |
+
.unwrap_or("")
|
| 2070 |
+
.trim()
|
| 2071 |
+
.to_string()
|
| 2072 |
+
};
|
| 2073 |
+
if api_token.is_empty() {
|
| 2074 |
+
return json_error(StatusCode::SERVICE_UNAVAILABLE, "gw:no_checkForm");
|
| 2075 |
+
}
|
| 2076 |
+
|
| 2077 |
+
let mut params = json!({ "user_id": user_id });
|
| 2078 |
+
let config_id = req.config_id.unwrap_or_default().trim().to_string();
|
| 2079 |
+
if !config_id.is_empty() && config_id != "default" {
|
| 2080 |
+
if let Some(obj) = params.as_object_mut() {
|
| 2081 |
+
obj.insert("config_id".to_string(), serde_json::Value::String(config_id));
|
| 2082 |
+
}
|
| 2083 |
+
}
|
| 2084 |
+
|
| 2085 |
+
let results = match gw_light_call_custom(
|
| 2086 |
+
&client,
|
| 2087 |
+
"radio.getUserRadio",
|
| 2088 |
+
&api_token,
|
| 2089 |
+
None,
|
| 2090 |
+
¶ms,
|
| 2091 |
+
Some(req.user_id.as_str()),
|
| 2092 |
+
false,
|
| 2093 |
+
)
|
| 2094 |
+
.await
|
| 2095 |
+
{
|
| 2096 |
+
Ok(v) => v,
|
| 2097 |
+
Err(e) => return json_error(StatusCode::SERVICE_UNAVAILABLE, format!("gw:radio.getUserRadio:{e}")),
|
| 2098 |
+
};
|
| 2099 |
+
|
| 2100 |
+
(StatusCode::OK, Json(json!({ "results": results }))).into_response()
|
| 2101 |
+
}
|
| 2102 |
+
|
| 2103 |
#[derive(Debug, Deserialize)]
|
| 2104 |
struct OauthFlowReq {
|
| 2105 |
access_token: String,
|
|
|
|
| 2324 |
.route("/pair/oauth", post(pair_oauth))
|
| 2325 |
.route("/pair/oauth_cb", get(pair_oauth_cb))
|
| 2326 |
.route("/pair/complete", post(pair_complete))
|
| 2327 |
+
.route("/gw/page", post(gw_page))
|
| 2328 |
+
.route("/gw/flow", post(gw_flow))
|
| 2329 |
.route("/oauth/me", post(oauth_me))
|
| 2330 |
.route("/oauth/playlists", post(oauth_playlists))
|
| 2331 |
.route("/oauth/playlist_tracks", post(oauth_playlist_tracks))
|