recycleactor commited on
Commit
e7d0ccb
·
verified ·
1 Parent(s): 5dab5eb

Upload 2 files

Browse files
Files changed (1) hide show
  1. src/main.rs +241 -148
src/main.rs CHANGED
@@ -1023,12 +1023,163 @@ async fn mobile_login(email: &str, password_md5: &str) -> Result<String, String>
1023
  const DEEZER_APP_SECRET: &str = "a83bf7f38ad2f137e444727cfc3775cf";
1024
 
1025
  struct WebLoginResult {
1026
- arl: String,
1027
  token: String,
1028
  uid: String,
1029
  access_token: String,
1030
  }
1031
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1032
  async fn oauth_access_token(
1033
  client: &reqwest::Client,
1034
  email: &str,
@@ -1081,7 +1232,17 @@ async fn oauth_access_token(
1081
  async fn establish_oauth_session(client: &reqwest::Client, jar: &Jar, access_token: &str) {
1082
  let url_deezer = "https://www.deezer.com".parse::<Url>().unwrap();
1083
  let url_api = "https://api.deezer.com".parse::<Url>().unwrap();
1084
- let ua = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36";
 
 
 
 
 
 
 
 
 
 
1085
 
1086
  let _ = client
1087
  .get("https://api.deezer.com/platform/generic/track/80085")
@@ -1116,7 +1277,19 @@ async fn web_login(email: &str, password_md5: &str) -> Result<WebLoginResult, St
1116
  .build()
1117
  .map_err(|_| "web:client".to_string())?;
1118
 
1119
- // OAuth через connect.deezer.com — не блокируется так же, как gw-light checkCredentials
 
 
 
 
 
 
 
 
 
 
 
 
1120
  let access_token = oauth_access_token(&client, email, password_md5)
1121
  .await
1122
  .map_err(|e| format!("oauth:{e}"))?;
@@ -1127,89 +1300,58 @@ async fn web_login(email: &str, password_md5: &str) -> Result<WebLoginResult, St
1127
  .get("https://www.deezer.com/")
1128
  .header(ACCEPT, "*/*")
1129
  .header("Accept-Language", "en-US,en;q=0.9")
1130
- .header(
1131
- "User-Agent",
1132
- "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
1133
- )
1134
  .send()
1135
  .await;
1136
 
1137
- let ud = gw_light_call_custom(
1138
  &client,
1139
- "deezer.getUserData",
1140
- "null",
1141
- None,
1142
- &json!({}),
1143
- None,
1144
- false,
1145
  )
1146
  .await
1147
- .map_err(|e| format!("web:getUserData:{e}"))?;
1148
-
1149
- let check_form = ud.get("checkForm").and_then(|x| x.as_str()).unwrap_or("").trim().to_string();
1150
- if check_form.is_empty() {
1151
- return Err("web:no_checkForm".to_string());
1152
- }
1153
 
1154
- let uid = ud
1155
- .get("USER")
1156
- .and_then(|u| u.get("USER_ID"))
1157
- .and_then(|x| x.as_i64())
1158
- .map(|n| n.to_string())
1159
- .unwrap_or_default();
1160
-
1161
- let arl_v = match gw_light_call_custom(
1162
  &client,
1163
- "user.getArl",
1164
- &check_form,
1165
- None,
1166
- &json!({}),
1167
- None,
1168
- true,
1169
  )
1170
  .await
1171
  {
1172
  Ok(v) => v,
1173
- Err(e1) => {
1174
- // Последний fallback checkCredentials (часто режется на HF/VPS)
1175
- let _ = gw_light_call_custom(
1176
- &client,
1177
- "user.checkCredentials",
1178
- &check_form,
1179
- None,
1180
- &json!({
1181
- "login": email,
1182
- "password": password_md5,
1183
- "checkFormLogin": check_form
1184
- }),
1185
- None,
1186
- false,
1187
- )
1188
- .await;
1189
-
1190
- gw_light_call_custom(
1191
- &client,
1192
- "user.getArl",
1193
- &check_form,
1194
- None,
1195
- &json!({}),
1196
- None,
1197
- true,
1198
- )
1199
- .await
1200
- .map_err(|e2| format!("web:getArl:{e1}; retry:{e2}"))?
1201
  }
1202
  };
1203
 
1204
- let arl = arl_v.as_str().unwrap_or("").to_string();
1205
- if arl.len() < 100 {
1206
- return Err(format!("web:arl_too_short:{}", arl.len()));
1207
- }
1208
-
1209
  Ok(WebLoginResult {
1210
- arl,
1211
- token: check_form,
1212
- uid,
 
 
 
 
1213
  access_token,
1214
  })
1215
  }
@@ -1868,18 +2010,27 @@ async fn pair_complete(State(state): State<AppState>, Form(f): Form<PairComplete
1868
  Ok(login) => {
1869
  let mut map = state.pair.write().await;
1870
  if let Some(s) = map.get_mut(&code) {
1871
- s.arl = Some(login.arl);
1872
- s.token = Some(login.token);
1873
- s.access_token = Some(login.access_token);
 
 
 
 
1874
  if !login.uid.is_empty() {
1875
- s.user_id = Some(login.uid);
1876
  }
1877
  s.email = Some(email);
1878
  s.error = None;
1879
  }
1880
  drop(map);
1881
  pair_store_save(&state).await;
1882
- Html("<h3>Готово</h3><p>Вернись на ТВ плагин подключится автоматически.</p>".to_string()).into_response()
 
 
 
 
 
1883
  }
1884
  }
1885
  }
@@ -1984,7 +2135,6 @@ async fn oauth_arl(Json(req): Json<OauthArlReq>) -> impl IntoResponse {
1984
 
1985
  let jar = Arc::new(Jar::default());
1986
  let url_deezer = "https://www.deezer.com".parse::<Url>().unwrap();
1987
- let url_api = "https://api.deezer.com".parse::<Url>().unwrap();
1988
  jar.add_cookie_str("comeback=1; Domain=.deezer.com", &url_deezer);
1989
  jar.add_cookie_str("dzr_uniq_id=; Domain=.deezer.com", &url_deezer);
1990
  let client = reqwest::Client::builder()
@@ -1998,86 +2148,29 @@ async fn oauth_arl(Json(req): Json<OauthArlReq>) -> impl IntoResponse {
1998
  .get("https://www.deezer.com/")
1999
  .header(ACCEPT, "*/*")
2000
  .header("Accept-Language", "en-US,en;q=0.9")
2001
- .header(
2002
- "User-Agent",
2003
- "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
2004
- )
2005
  .send()
2006
  .await;
2007
 
2008
- let _ = client
2009
- .get("https://api.deezer.com/platform/generic/track/80085")
2010
- .header("Authorization", format!("Bearer {}", token))
2011
- .header(ACCEPT, "*/*")
2012
- .header("Accept-Language", "en-US,en;q=0.9")
2013
- .header(
2014
- "User-Agent",
2015
- "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
2016
- )
2017
- .send()
2018
- .await;
2019
-
2020
- if !jar_has_cookie(&jar, &url_deezer, "sid=") && !jar_has_cookie(&jar, &url_api, "sid=") {
2021
- let _ = client
2022
- .get("https://api.deezer.com/platform/generic/track/3135556")
2023
- .header("Authorization", format!("Bearer {}", token))
2024
- .header(ACCEPT, "*/*")
2025
- .header("Accept-Language", "en-US,en;q=0.9")
2026
- .header(
2027
- "User-Agent",
2028
- "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
2029
- )
2030
- .send()
2031
- .await;
2032
  }
2033
 
2034
- let mut ud: Option<serde_json::Value> = None;
2035
- let mut ud_err: Option<String> = None;
2036
- for attempt in 0..2 {
2037
- match gw_light_call(&client, &jar, "deezer.getUserData", "null", json!({})).await {
2038
- Ok(v) => {
2039
- ud = Some(v);
2040
- break;
2041
- }
2042
- Err(e) => {
2043
- ud_err = Some(e);
2044
- if attempt == 0 {
2045
- let _ = client
2046
- .get("https://www.deezer.com/")
2047
- .header(ACCEPT, "*/*")
2048
- .header("Accept-Language", "en-US,en;q=0.9")
2049
- .header(
2050
- "User-Agent",
2051
- "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
2052
- )
2053
- .send()
2054
- .await;
2055
- }
2056
- }
2057
- }
2058
- }
2059
- let Some(ud) = ud else {
2060
- return json_error(
2061
- StatusCode::SERVICE_UNAVAILABLE,
2062
- ud_err.unwrap_or_else(|| "gw:getUserData".to_string()),
2063
- );
2064
- };
2065
 
2066
- let check_form = ud["checkForm"].as_str().unwrap_or("").to_string();
2067
- if check_form.is_empty() {
2068
- return json_error(StatusCode::SERVICE_UNAVAILABLE, "no checkForm");
2069
- }
 
 
2070
 
2071
- let arl_v = match gw_light_call(&client, &jar, "user.getArl", &check_form, json!({})).await {
2072
- Ok(v) => v,
2073
- Err(e) => return json_error(StatusCode::SERVICE_UNAVAILABLE, e),
2074
- };
2075
- let arl = arl_v.as_str().unwrap_or("").to_string();
2076
- if arl.len() < 20 {
2077
- return json_error(StatusCode::SERVICE_UNAVAILABLE, "no arl");
2078
  }
2079
-
2080
- (StatusCode::OK, Json(json!({ "arl": arl }))).into_response()
2081
  }
2082
 
2083
  #[derive(Debug, Deserialize)]
 
1023
  const DEEZER_APP_SECRET: &str = "a83bf7f38ad2f137e444727cfc3775cf";
1024
 
1025
  struct WebLoginResult {
1026
+ arl: Option<String>,
1027
  token: String,
1028
  uid: String,
1029
  access_token: String,
1030
  }
1031
 
1032
+ fn gw_browser_ua() -> &'static str {
1033
+ "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36"
1034
+ }
1035
+
1036
+ fn gw_cid() -> String {
1037
+ SystemTime::now()
1038
+ .duration_since(UNIX_EPOCH)
1039
+ .map(|d| d.as_millis().to_string())
1040
+ .unwrap_or_else(|_| "0".to_string())
1041
+ }
1042
+
1043
+ fn capture_sid_from_response(jar: &Jar, url: &Url, headers: &reqwest::header::HeaderMap) {
1044
+ for cookie_hdr in headers.get_all(reqwest::header::SET_COOKIE) {
1045
+ if let Ok(s) = cookie_hdr.to_str() {
1046
+ for part in s.split(',') {
1047
+ let part = part.trim();
1048
+ let name_val = part.split(';').next().unwrap_or("").trim();
1049
+ if name_val.starts_with("sid=") {
1050
+ jar.add_cookie_str(
1051
+ &format!("{name_val}; Domain=.deezer.com; Path=/"),
1052
+ url,
1053
+ );
1054
+ }
1055
+ }
1056
+ }
1057
+ }
1058
+ }
1059
+
1060
+ /// Deezer выдаёт анонимный sid в Set-Cookie ответа на GET user.getArl — нужен до OAuth.
1061
+ async fn ensure_anonymous_sid(client: &reqwest::Client, jar: &Jar) -> Result<(), String> {
1062
+ let url_deezer = "https://www.deezer.com".parse::<Url>().map_err(|_| "sid:url".to_string())?;
1063
+ if jar_has_cookie(jar, &url_deezer, "sid=") {
1064
+ return Ok(());
1065
+ }
1066
+
1067
+ let cid = gw_cid();
1068
+ let r = client
1069
+ .get("https://www.deezer.com/ajax/gw-light.php")
1070
+ .query(&[
1071
+ ("method", "user.getArl"),
1072
+ ("input", "3"),
1073
+ ("output", "3"),
1074
+ ("api_version", "1.0"),
1075
+ ("api_token", "null"),
1076
+ ("cid", cid.as_str()),
1077
+ ])
1078
+ .header(ACCEPT, "*/*")
1079
+ .header("Origin", "https://www.deezer.com")
1080
+ .header("Referer", "https://www.deezer.com/")
1081
+ .header("Sec-Fetch-Site", "same-origin")
1082
+ .header("Sec-Fetch-Mode", "cors")
1083
+ .header("Sec-Fetch-Dest", "empty")
1084
+ .header("User-Agent", gw_browser_ua())
1085
+ .send()
1086
+ .await
1087
+ .map_err(|e| format!("sid:network:{e}"))?;
1088
+
1089
+ capture_sid_from_response(jar, &url_deezer, r.headers());
1090
+
1091
+ if !jar_has_cookie(jar, &url_deezer, "sid=") {
1092
+ return Err("sid:not_set".to_string());
1093
+ }
1094
+ Ok(())
1095
+ }
1096
+
1097
+ async fn gw_fetch_arl(
1098
+ client: &reqwest::Client,
1099
+ email: &str,
1100
+ password_md5: &str,
1101
+ user_id: Option<&str>,
1102
+ ) -> Result<(String, String, String), String> {
1103
+ let mut last_err = String::from("getArl:none");
1104
+
1105
+ for attempt in 0..3u8 {
1106
+ let ud = gw_light_call_custom(
1107
+ client,
1108
+ "deezer.getUserData",
1109
+ "null",
1110
+ None,
1111
+ &json!({}),
1112
+ None,
1113
+ attempt > 0,
1114
+ )
1115
+ .await
1116
+ .map_err(|e| format!("getUserData:{e}"))?;
1117
+
1118
+ let check_form = ud
1119
+ .get("checkForm")
1120
+ .and_then(|x| x.as_str())
1121
+ .unwrap_or("")
1122
+ .trim()
1123
+ .to_string();
1124
+ if check_form.is_empty() {
1125
+ last_err = "no_checkForm".to_string();
1126
+ continue;
1127
+ }
1128
+
1129
+ let uid = ud
1130
+ .get("USER")
1131
+ .and_then(|u| u.get("USER_ID"))
1132
+ .and_then(|x| x.as_i64())
1133
+ .map(|n| n.to_string())
1134
+ .unwrap_or_default();
1135
+
1136
+ let uid_for_hdr = user_id
1137
+ .filter(|s| !s.trim().is_empty())
1138
+ .or_else(|| if uid.is_empty() { None } else { Some(uid.as_str()) });
1139
+
1140
+ match gw_light_call_custom(
1141
+ client,
1142
+ "user.getArl",
1143
+ &check_form,
1144
+ None,
1145
+ &json!({}),
1146
+ uid_for_hdr,
1147
+ true,
1148
+ )
1149
+ .await
1150
+ {
1151
+ Ok(v) => {
1152
+ let arl = v.as_str().unwrap_or("").to_string();
1153
+ if arl.len() >= 100 {
1154
+ return Ok((arl, check_form, uid));
1155
+ }
1156
+ last_err = format!("arl_too_short:{}", arl.len());
1157
+ }
1158
+ Err(e) => {
1159
+ last_err = e.clone();
1160
+ if e.contains("NEED_USER_AUTH") && attempt == 0 {
1161
+ let _ = gw_light_call_custom(
1162
+ client,
1163
+ "user.checkCredentials",
1164
+ &check_form,
1165
+ None,
1166
+ &json!({
1167
+ "login": email,
1168
+ "password": password_md5,
1169
+ "checkFormLogin": check_form
1170
+ }),
1171
+ uid_for_hdr,
1172
+ false,
1173
+ )
1174
+ .await;
1175
+ }
1176
+ }
1177
+ }
1178
+ }
1179
+
1180
+ Err(format!("getArl:{last_err}"))
1181
+ }
1182
+
1183
  async fn oauth_access_token(
1184
  client: &reqwest::Client,
1185
  email: &str,
 
1232
  async fn establish_oauth_session(client: &reqwest::Client, jar: &Jar, access_token: &str) {
1233
  let url_deezer = "https://www.deezer.com".parse::<Url>().unwrap();
1234
  let url_api = "https://api.deezer.com".parse::<Url>().unwrap();
1235
+ let ua = gw_browser_ua();
1236
+
1237
+ let _ = client
1238
+ .get("https://api.deezer.com/user/me")
1239
+ .query(&[("access_token", access_token)])
1240
+ .header("Authorization", format!("Bearer {access_token}"))
1241
+ .header(ACCEPT, "*/*")
1242
+ .header("Accept-Language", "en-US,en;q=0.9")
1243
+ .header("User-Agent", ua)
1244
+ .send()
1245
+ .await;
1246
 
1247
  let _ = client
1248
  .get("https://api.deezer.com/platform/generic/track/80085")
 
1277
  .build()
1278
  .map_err(|_| "web:client".to_string())?;
1279
 
1280
+ let _ = client
1281
+ .get("https://www.deezer.com/")
1282
+ .header(ACCEPT, "*/*")
1283
+ .header("Accept-Language", "en-US,en;q=0.9")
1284
+ .header("User-Agent", gw_browser_ua())
1285
+ .send()
1286
+ .await;
1287
+
1288
+ // sid нужен ДО OAuth (как в официальном клиенте Deezer)
1289
+ ensure_anonymous_sid(&client, &jar)
1290
+ .await
1291
+ .map_err(|e| format!("web:{e}"))?;
1292
+
1293
  let access_token = oauth_access_token(&client, email, password_md5)
1294
  .await
1295
  .map_err(|e| format!("oauth:{e}"))?;
 
1300
  .get("https://www.deezer.com/")
1301
  .header(ACCEPT, "*/*")
1302
  .header("Accept-Language", "en-US,en;q=0.9")
1303
+ .header("User-Agent", gw_browser_ua())
 
 
 
1304
  .send()
1305
  .await;
1306
 
1307
+ let uid_from_api = api_deezer_get(
1308
  &client,
1309
+ "/user/me",
1310
+ &access_token,
1311
+ &[],
 
 
 
1312
  )
1313
  .await
1314
+ .ok()
1315
+ .and_then(|v| v.get("id").and_then(|x| x.as_i64()))
1316
+ .filter(|id| *id > 0)
1317
+ .map(|id| id.to_string());
 
 
1318
 
1319
+ let (arl, token, uid) = match gw_fetch_arl(
 
 
 
 
 
 
 
1320
  &client,
1321
+ email,
1322
+ password_md5,
1323
+ uid_from_api.as_deref(),
 
 
 
1324
  )
1325
  .await
1326
  {
1327
  Ok(v) => v,
1328
+ Err(_e) => {
1329
+ // ARL не обязателен — OAuth уже даёт доступ к библиотеке
1330
+ if let Ok(mobile_arl) = mobile_login(email, password_md5).await {
1331
+ (
1332
+ mobile_arl,
1333
+ String::new(),
1334
+ uid_from_api.clone().unwrap_or_default(),
1335
+ )
1336
+ } else {
1337
+ return Ok(WebLoginResult {
1338
+ arl: None,
1339
+ token: String::new(),
1340
+ uid: uid_from_api.unwrap_or_default(),
1341
+ access_token,
1342
+ });
1343
+ }
 
 
 
 
 
 
 
 
 
 
 
 
1344
  }
1345
  };
1346
 
 
 
 
 
 
1347
  Ok(WebLoginResult {
1348
+ arl: Some(arl),
1349
+ token,
1350
+ uid: if uid.is_empty() {
1351
+ uid_from_api.unwrap_or_default()
1352
+ } else {
1353
+ uid
1354
+ },
1355
  access_token,
1356
  })
1357
  }
 
2010
  Ok(login) => {
2011
  let mut map = state.pair.write().await;
2012
  if let Some(s) = map.get_mut(&code) {
2013
+ s.arl = login.arl.clone();
2014
+ s.token = if login.token.is_empty() {
2015
+ None
2016
+ } else {
2017
+ Some(login.token.clone())
2018
+ };
2019
+ s.access_token = Some(login.access_token.clone());
2020
  if !login.uid.is_empty() {
2021
+ s.user_id = Some(login.uid.clone());
2022
  }
2023
  s.email = Some(email);
2024
  s.error = None;
2025
  }
2026
  drop(map);
2027
  pair_store_save(&state).await;
2028
+ let msg = if login.arl.is_some() {
2029
+ "<h3>Готово</h3><p>Вернись на ТВ — плагин подключится автоматически.</p>"
2030
+ } else {
2031
+ "<h3>Вход выполнен</h3><p>Библиотека доступна. ARL не получен — полные треки могут не работать, попробуй кнопку «Войти через Deezer».</p>"
2032
+ };
2033
+ Html(msg.to_string()).into_response()
2034
  }
2035
  }
2036
  }
 
2135
 
2136
  let jar = Arc::new(Jar::default());
2137
  let url_deezer = "https://www.deezer.com".parse::<Url>().unwrap();
 
2138
  jar.add_cookie_str("comeback=1; Domain=.deezer.com", &url_deezer);
2139
  jar.add_cookie_str("dzr_uniq_id=; Domain=.deezer.com", &url_deezer);
2140
  let client = reqwest::Client::builder()
 
2148
  .get("https://www.deezer.com/")
2149
  .header(ACCEPT, "*/*")
2150
  .header("Accept-Language", "en-US,en;q=0.9")
2151
+ .header("User-Agent", gw_browser_ua())
 
 
 
2152
  .send()
2153
  .await;
2154
 
2155
+ if let Err(e) = ensure_anonymous_sid(&client, &jar).await {
2156
+ return json_error(StatusCode::SERVICE_UNAVAILABLE, format!("oauth_arl:{e}"));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2157
  }
2158
 
2159
+ establish_oauth_session(&client, &jar, &token).await;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2160
 
2161
+ let uid = api_deezer_get(&client, "/user/me", &token, &[])
2162
+ .await
2163
+ .ok()
2164
+ .and_then(|v| v.get("id").and_then(|x| x.as_i64()))
2165
+ .filter(|id| *id > 0)
2166
+ .map(|id| id.to_string());
2167
 
2168
+ match gw_fetch_arl(&client, "", "", uid.as_deref()).await {
2169
+ Ok((arl, _, _)) if arl.len() >= 20 => {
2170
+ (StatusCode::OK, Json(json!({ "arl": arl }))).into_response()
2171
+ }
2172
+ Err(e) => json_error(StatusCode::SERVICE_UNAVAILABLE, e),
 
 
2173
  }
 
 
2174
  }
2175
 
2176
  #[derive(Debug, Deserialize)]