recycleactor commited on
Commit
1839eb5
·
verified ·
1 Parent(s): be509e5

Upload 2 files

Browse files
Files changed (2) hide show
  1. src/api.rs +39 -8
  2. src/main.rs +193 -64
src/api.rs CHANGED
@@ -1,6 +1,6 @@
1
  use serde::{Serialize, Deserialize, de::DeserializeOwned};
2
  use std::{env, marker::Sized, time::Instant, sync::Arc};
3
- use std::time::{SystemTime, UNIX_EPOCH};
4
  use thiserror::Error;
5
  use serde_json::{json, value::from_value};
6
  use reqwest::{Client, Response, cookie::Jar, Url, header::ACCEPT};
@@ -73,22 +73,48 @@ impl APIClient {
73
  }
74
 
75
  pub fn new_with_arl(arl: String) -> Self {
 
 
 
 
 
 
 
 
76
  let builder = Client::builder().no_proxy();
77
 
78
- let cookie = format!("arl={}; Domain=.deezer.com", arl);
79
- let comeback = "comeback=1; Domain=.deezer.com";
80
  let url = "https://www.deezer.com".parse::<Url>().unwrap();
81
 
82
  let jar = Jar::default();
83
  jar.add_cookie_str(&cookie, &url);
84
  jar.add_cookie_str(comeback, &url);
85
- let builder = builder.cookie_provider(Arc::new(jar));
 
 
 
 
 
 
 
 
86
 
87
  Self {
88
  client: builder.build().unwrap(),
89
- license_token: String::new(),
90
- check_form: String::new(),
91
- renew_instant: None,
 
 
 
 
 
 
 
 
 
 
92
  }
93
  }
94
 
@@ -222,7 +248,12 @@ impl APIClient {
222
  "track_tokens": track_tokens
223
  });
224
 
225
- self.client.post("https://media.deezer.com/v1/get_url")
 
 
 
 
 
226
  .json(&req)
227
  .send()
228
  .await
 
1
  use serde::{Serialize, Deserialize, de::DeserializeOwned};
2
  use std::{env, marker::Sized, time::Instant, sync::Arc};
3
+ use std::time::{Duration, SystemTime, UNIX_EPOCH};
4
  use thiserror::Error;
5
  use serde_json::{json, value::from_value};
6
  use reqwest::{Client, Response, cookie::Jar, Url, header::ACCEPT};
 
73
  }
74
 
75
  pub fn new_with_arl(arl: String) -> Self {
76
+ Self::new_with_arl_session(arl, String::new(), String::new())
77
+ }
78
+
79
+ pub fn new_with_arl_session(
80
+ arl: String,
81
+ check_form: String,
82
+ license_token: String,
83
+ ) -> Self {
84
  let builder = Client::builder().no_proxy();
85
 
86
+ let cookie = format!("arl={}; Domain=.deezer.com; Path=/", arl);
87
+ let comeback = "comeback=1; Domain=.deezer.com; Path=/";
88
  let url = "https://www.deezer.com".parse::<Url>().unwrap();
89
 
90
  let jar = Jar::default();
91
  jar.add_cookie_str(&cookie, &url);
92
  jar.add_cookie_str(comeback, &url);
93
+ let builder = builder
94
+ .cookie_provider(Arc::new(jar))
95
+ .timeout(std::time::Duration::from_secs(30));
96
+
97
+ let renew_instant = if !license_token.is_empty() && !check_form.is_empty() {
98
+ Some(Instant::now())
99
+ } else {
100
+ None
101
+ };
102
 
103
  Self {
104
  client: builder.build().unwrap(),
105
+ license_token,
106
+ check_form,
107
+ renew_instant,
108
+ }
109
+ }
110
+
111
+ pub fn seed_session(&mut self, check_form: String, license_token: String) {
112
+ if !check_form.is_empty() {
113
+ self.check_form = check_form;
114
+ }
115
+ if !license_token.is_empty() {
116
+ self.license_token = license_token;
117
+ self.renew_instant = Some(Instant::now());
118
  }
119
  }
120
 
 
248
  "track_tokens": track_tokens
249
  });
250
 
251
+ self.client
252
+ .post("https://media.deezer.com/v1/get_url")
253
+ .header(ACCEPT, "application/json")
254
+ .header("Content-Type", "application/json")
255
+ .header("Origin", "https://www.deezer.com")
256
+ .header("Referer", "https://www.deezer.com/")
257
  .json(&req)
258
  .send()
259
  .await
src/main.rs CHANGED
@@ -30,14 +30,46 @@ type BoxErr = Box<dyn std::error::Error + Send + Sync + 'static>;
30
  mod api;
31
  use api::{APIClient, APIError, Format};
32
 
 
 
 
 
 
 
33
  #[derive(Clone)]
34
  struct AppState {
35
  api: Arc<Mutex<APIClient>>,
36
  api_by_arl: Arc<RwLock<HashMap<String, Arc<Mutex<APIClient>>>>>,
 
37
  pair: Arc<RwLock<HashMap<String, PairSession>>>,
38
  pair_store_path: String,
39
  }
40
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
41
  fn json_error(status: StatusCode, message: impl Into<String>) -> axum::response::Response {
42
  (status, Json(json!({ "error": message.into() }))).into_response()
43
  }
@@ -61,7 +93,13 @@ async fn api_client_for_arl(state: &AppState, arl: Option<String>) -> Arc<Mutex<
61
  if map.len() >= 32 {
62
  map.clear();
63
  }
64
- let c = Arc::new(Mutex::new(APIClient::new_with_arl(arl.clone())));
 
 
 
 
 
 
65
  map.insert(arl, c.clone());
66
  c
67
  }
@@ -139,11 +177,62 @@ fn total_from_content_range(v: &str) -> Option<u64> {
139
  rest.trim().parse::<u64>().ok()
140
  }
141
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
142
  async fn media_url_for_track(
143
  client: &mut APIClient,
144
  id: u32,
145
  formats: &Vec<Format>,
146
  ) -> Result<String, String> {
 
 
 
 
 
 
147
  let resp: Result<DeezerTrackList, APIError> = client
148
  .api_call(
149
  "song.getListData",
@@ -157,21 +246,26 @@ async fn media_url_for_track(
157
  }
158
 
159
  let track_token = track_list.data[0].TRACK_TOKEN.as_str();
160
- let media_resp = client
161
- .get_media(formats, vec![track_token])
162
- .await
163
- .map_err(|e| e.to_string())?;
164
 
165
- let media_json: serde_json::Value = media_resp
166
- .json()
167
- .await
168
- .map_err(|_| "Bad media response".to_string())?;
169
 
170
- let url = extract_media_url(&media_json);
171
- if url.is_empty() {
172
- return Err("No url in media response".to_string());
 
 
173
  }
174
- Ok(url)
 
 
 
 
 
 
 
175
  }
176
 
177
  fn upstream_dzmedia_base() -> Option<String> {
@@ -417,28 +511,12 @@ async fn stream_info(State(state): State<AppState>, Query(q): Query<StreamParams
417
  state.api_by_arl.write().await.remove(&arl);
418
  }
419
  let client = api_client_for_arl(&state, Some(arl.clone())).await;
420
- let res = async {
421
  let mut c = client.lock().await;
422
- if attempt == 1 { let _ = c.force_renew().await; }
423
- let resp: Result<DeezerTrackList, APIError> = c
424
- .api_call(
425
- "song.getListData",
426
- &json!({"sng_ids":[q.id],"array_default":["SNG_ID","TRACK_TOKEN","DURATION"]}),
427
- )
428
- .await;
429
- let track_list = resp.map_err(|e| e.to_string())?;
430
- if track_list.data.is_empty() { return Err("No valid ID".to_string()); }
431
- let token = track_list.data[0].TRACK_TOKEN.as_str();
432
- let media_resp = c.get_media(&formats, vec![token]).await.map_err(|e| e.to_string())?;
433
- let media_json: serde_json::Value = media_resp.json().await
434
- .map_err(|_| "Bad media response".to_string())?;
435
- let u = extract_media_url(&media_json);
436
- let f = extract_media_format(&media_json);
437
- if u.is_empty() { return Err("No url in media response".to_string()); }
438
- Ok::<(String, String), String>((u, f))
439
- }.await;
440
  match res {
441
- Ok((u, f)) => { url = u; used = f; break 'arl; }
442
  Err(e) => { last_err = Some(e); }
443
  }
444
  }
@@ -525,12 +603,11 @@ async fn stream(
525
  }
526
  }
527
 
528
- // До 3 попыток: 0 — обычная, 1 — после сброса кэша клиента, 2 — принудительный renew внутри
529
  for attempt in 0..3u8 {
530
  if url.is_some() { break; }
531
 
532
- // На попытке 1 — выбрасываем кэшированный клиент чтобы пересоздать его
533
- if attempt == 1 {
534
  if let Some(ref arl) = arl_key {
535
  state.api_by_arl.write().await.remove(arl);
536
  } else {
@@ -541,11 +618,10 @@ async fn stream(
541
  let client = api_client_for_arl(&state, arl_key.clone()).await;
542
  let res = {
543
  let mut c = client.lock().await;
544
- // На попытке 2 — принудительно обновляем токены перед запросом
545
- if attempt == 2 {
546
  if let Err(e) = c.force_renew().await {
547
  last_err = Some(format!("renew:{}", e));
548
- break;
549
  }
550
  }
551
  media_url_for_track(&mut c, q.id, &formats).await
@@ -1027,6 +1103,7 @@ struct WebLoginResult {
1027
  token: String,
1028
  uid: String,
1029
  access_token: String,
 
1030
  }
1031
 
1032
  fn gw_browser_ua() -> &'static str {
@@ -1099,7 +1176,7 @@ async fn gw_fetch_arl(
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 {
@@ -1151,7 +1228,8 @@ async fn gw_fetch_arl(
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
  }
@@ -1183,11 +1261,11 @@ async fn gw_fetch_arl(
1183
  async fn try_fetch_arl_for_token(
1184
  access_token: &str,
1185
  user_id: Option<&str>,
1186
- ) -> (Option<String>, Option<String>) {
1187
  let jar = Arc::new(Jar::default());
1188
  let url_deezer = match "https://www.deezer.com".parse::<Url>() {
1189
  Ok(u) => u,
1190
- Err(_) => return (None, None),
1191
  };
1192
  jar.add_cookie_str("comeback=1; Domain=.deezer.com; Path=/", &url_deezer);
1193
 
@@ -1198,7 +1276,7 @@ async fn try_fetch_arl_for_token(
1198
  .build()
1199
  {
1200
  Ok(c) => c,
1201
- Err(_) => return (None, None),
1202
  };
1203
 
1204
  let _ = client
@@ -1209,7 +1287,7 @@ async fn try_fetch_arl_for_token(
1209
  .await;
1210
 
1211
  if ensure_anonymous_sid(&client, &jar).await.is_err() {
1212
- return (None, None);
1213
  }
1214
 
1215
  establish_oauth_session(&client, &jar, access_token).await;
@@ -1226,11 +1304,12 @@ async fn try_fetch_arl_for_token(
1226
  };
1227
 
1228
  match gw_fetch_arl(&client, "", "", uid.as_deref()).await {
1229
- Ok((arl, token, _)) if arl.len() >= 20 => {
1230
  let gw_token = if token.is_empty() { None } else { Some(token) };
1231
- (Some(arl), gw_token)
 
1232
  }
1233
- _ => (None, None),
1234
  }
1235
  }
1236
 
@@ -1370,7 +1449,7 @@ async fn web_login(email: &str, password_md5: &str) -> Result<WebLoginResult, St
1370
  .filter(|id| *id > 0)
1371
  .map(|id| id.to_string());
1372
 
1373
- let (arl, token, uid) = match gw_fetch_arl(
1374
  &client,
1375
  email,
1376
  password_md5,
@@ -1380,12 +1459,12 @@ async fn web_login(email: &str, password_md5: &str) -> Result<WebLoginResult, St
1380
  {
1381
  Ok(v) => v,
1382
  Err(_e) => {
1383
- // ARL не обязателен — OAuth уже даёт доступ к библиотеке
1384
  if let Ok(mobile_arl) = mobile_login(email, password_md5).await {
1385
  (
1386
  mobile_arl,
1387
  String::new(),
1388
  uid_from_api.clone().unwrap_or_default(),
 
1389
  )
1390
  } else {
1391
  return Ok(WebLoginResult {
@@ -1393,13 +1472,14 @@ async fn web_login(email: &str, password_md5: &str) -> Result<WebLoginResult, St
1393
  token: String::new(),
1394
  uid: uid_from_api.unwrap_or_default(),
1395
  access_token,
 
1396
  });
1397
  }
1398
  }
1399
  };
1400
 
1401
  Ok(WebLoginResult {
1402
- arl: Some(arl),
1403
  token,
1404
  uid: if uid.is_empty() {
1405
  uid_from_api.unwrap_or_default()
@@ -1407,6 +1487,7 @@ async fn web_login(email: &str, password_md5: &str) -> Result<WebLoginResult, St
1407
  uid
1408
  },
1409
  access_token,
 
1410
  })
1411
  }
1412
 
@@ -1521,7 +1602,6 @@ async fn gw_light_call(
1521
  ])
1522
  .header(ACCEPT, "*/*")
1523
  .header("Cache-Control", "max-age=0")
1524
- .header("Connection", "keep-alive")
1525
  .header("Origin", "https://www.deezer.com")
1526
  .header("Referer", "https://www.deezer.com/")
1527
  .header("Sec-Fetch-Site", "same-origin")
@@ -1624,7 +1704,6 @@ async fn gw_light_call_custom(
1624
  .query(&qp)
1625
  .header(ACCEPT, "*/*")
1626
  .header("Cache-Control", "max-age=0")
1627
- .header("Connection", "keep-alive")
1628
  .header("Origin", "https://www.deezer.com")
1629
  .header("Referer", "https://www.deezer.com/")
1630
  .header("Sec-Fetch-Site", "same-origin")
@@ -1667,7 +1746,7 @@ fn jar_has_cookie(jar: &Jar, url: &Url, cookie_prefix: &str) -> bool {
1667
  s.split(';').any(|p| p.trim_start().starts_with(cookie_prefix))
1668
  }
1669
 
1670
- async fn login(Json(req): Json<LoginReq>) -> impl IntoResponse {
1671
  let email = req.email.trim().to_string();
1672
  let pass = req.password_md5.trim().to_string();
1673
 
@@ -1676,12 +1755,18 @@ async fn login(Json(req): Json<LoginReq>) -> impl IntoResponse {
1676
  }
1677
 
1678
  match web_login(&email, &pass).await {
1679
- Ok(res) => (StatusCode::OK, Json(json!({
1680
- "arl": res.arl,
1681
- "token": res.token,
1682
- "uid": res.uid,
1683
- "access_token": res.access_token
1684
- }))).into_response(),
 
 
 
 
 
 
1685
  Err(e) => json_error(
1686
  StatusCode::UNAUTHORIZED,
1687
  format!("login_failed; {e}"),
@@ -1894,12 +1979,20 @@ async fn pair_oauth(State(state): State<AppState>, Json(req): Json<PairOauthReq>
1894
  Some(user_id.clone())
1895
  };
1896
 
1897
- let (arl_opt, token_opt) = try_fetch_arl_for_token(
1898
  &access_token,
1899
  uid_from_api.as_deref(),
1900
  )
1901
  .await;
1902
 
 
 
 
 
 
 
 
 
1903
  let mut map = state.pair.write().await;
1904
  let Some(sess) = map.get_mut(&code) else {
1905
  return json_error(StatusCode::NOT_FOUND, "no such code");
@@ -2131,6 +2224,10 @@ async fn pair_complete(State(state): State<AppState>, Form(f): Form<PairComplete
2131
  }
2132
  drop(map);
2133
  pair_store_save(&state).await;
 
 
 
 
2134
  let msg = if login.arl.is_some() {
2135
  "<h3>Готово</h3><p>Вернись на ТВ — плагин подключится автоматически.</p>"
2136
  } else {
@@ -2233,7 +2330,7 @@ struct OauthArlReq {
2233
  access_token: String,
2234
  }
2235
 
2236
- async fn oauth_arl(Json(req): Json<OauthArlReq>) -> impl IntoResponse {
2237
  let token = req.access_token.trim().to_string();
2238
  if token.is_empty() {
2239
  return json_error(StatusCode::BAD_REQUEST, "access_token required");
@@ -2272,10 +2369,12 @@ async fn oauth_arl(Json(req): Json<OauthArlReq>) -> impl IntoResponse {
2272
  .map(|id| id.to_string());
2273
 
2274
  match gw_fetch_arl(&client, "", "", uid.as_deref()).await {
2275
- Ok((arl, _, _)) if arl.len() >= 20 => {
 
 
2276
  (StatusCode::OK, Json(json!({ "arl": arl }))).into_response()
2277
  }
2278
- Ok((arl, _, _)) => {
2279
  json_error(
2280
  StatusCode::SERVICE_UNAVAILABLE,
2281
  format!("oauth_arl:arl_too_short:{}", arl.len()),
@@ -2701,6 +2800,34 @@ async fn oauth_history(Json(req): Json<OauthHistoryReq>) -> impl IntoResponse {
2701
  }
2702
  }
2703
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2704
  #[tokio::main]
2705
  async fn main() {
2706
  let bind_addr = std::env::var("BIND_ADDR").unwrap_or("[::]".to_string());
@@ -2710,6 +2837,7 @@ async fn main() {
2710
  let state = AppState {
2711
  api: Arc::new(Mutex::new(APIClient::new())),
2712
  api_by_arl: Arc::new(RwLock::new(HashMap::new())),
 
2713
  pair: Arc::new(RwLock::new(HashMap::new())),
2714
  pair_store_path: std::env::var("PAIR_STORE_PATH").ok().unwrap_or_else(|| "pair_store.json".to_string()),
2715
  };
@@ -2745,6 +2873,7 @@ async fn main() {
2745
  .route("/oauth/playlists", post(oauth_playlists))
2746
  .route("/oauth/playlist_tracks", post(oauth_playlist_tracks))
2747
  .route("/oauth/arl", post(oauth_arl))
 
2748
  .route("/oauth/flow", post(oauth_flow))
2749
  .route("/oauth/reco_playlists", post(oauth_reco_playlists))
2750
  .route("/oauth/reco_albums", post(oauth_reco_albums))
 
30
  mod api;
31
  use api::{APIClient, APIError, Format};
32
 
33
+ #[derive(Clone)]
34
+ struct ArlSession {
35
+ check_form: String,
36
+ license_token: String,
37
+ }
38
+
39
  #[derive(Clone)]
40
  struct AppState {
41
  api: Arc<Mutex<APIClient>>,
42
  api_by_arl: Arc<RwLock<HashMap<String, Arc<Mutex<APIClient>>>>>,
43
+ arl_sessions: Arc<RwLock<HashMap<String, ArlSession>>>,
44
  pair: Arc<RwLock<HashMap<String, PairSession>>>,
45
  pair_store_path: String,
46
  }
47
 
48
+ fn license_token_from_ud(ud: &serde_json::Value) -> String {
49
+ ud.pointer("/USER/OPTIONS/license_token")
50
+ .and_then(|v| v.as_str())
51
+ .unwrap_or("")
52
+ .trim()
53
+ .to_string()
54
+ }
55
+
56
+ async fn store_arl_session(state: &AppState, arl: &str, check_form: &str, license_token: &str) {
57
+ if arl.len() < 20 {
58
+ return;
59
+ }
60
+ let mut map = state.arl_sessions.write().await;
61
+ if map.len() >= 64 {
62
+ map.clear();
63
+ }
64
+ map.insert(
65
+ arl.to_string(),
66
+ ArlSession {
67
+ check_form: check_form.to_string(),
68
+ license_token: license_token.to_string(),
69
+ },
70
+ );
71
+ }
72
+
73
  fn json_error(status: StatusCode, message: impl Into<String>) -> axum::response::Response {
74
  (status, Json(json!({ "error": message.into() }))).into_response()
75
  }
 
93
  if map.len() >= 32 {
94
  map.clear();
95
  }
96
+ let session = state.arl_sessions.read().await.get(&arl).cloned();
97
+ let client = if let Some(s) = session {
98
+ APIClient::new_with_arl_session(arl.clone(), s.check_form, s.license_token)
99
+ } else {
100
+ APIClient::new_with_arl(arl.clone())
101
+ };
102
+ let c = Arc::new(Mutex::new(client));
103
  map.insert(arl, c.clone());
104
  c
105
  }
 
177
  rest.trim().parse::<u64>().ok()
178
  }
179
 
180
+ String::new()
181
+ }
182
+
183
+ fn media_response_err(json: &serde_json::Value) -> String {
184
+ if let Some(errs) = json.get("errors").and_then(|v| v.as_array()) {
185
+ if let Some(first) = errs.first() {
186
+ let s = first.to_string();
187
+ return s.chars().take(160).collect();
188
+ }
189
+ }
190
+ json.to_string().chars().take(160).collect()
191
+ }
192
+
193
+ async fn fetch_media_url(
194
+ client: &APIClient,
195
+ formats: &[Format],
196
+ track_token: &str,
197
+ ) -> Result<String, String> {
198
+ if client.license_token.is_empty() {
199
+ return Err("license_token empty".to_string());
200
+ }
201
+
202
+ let media_resp = client
203
+ .get_media(&formats.to_vec(), vec![track_token])
204
+ .await
205
+ .map_err(|e| e.to_string())?;
206
+
207
+ let status = media_resp.status();
208
+ let media_json: serde_json::Value = media_resp
209
+ .json()
210
+ .await
211
+ .map_err(|_| "Bad media response".to_string())?;
212
+
213
+ let url = extract_media_url(&media_json);
214
+ if !url.is_empty() {
215
+ return Ok(url);
216
+ }
217
+
218
+ Err(format!(
219
+ "media:{}:{}",
220
+ status.as_u16(),
221
+ media_response_err(&media_json)
222
+ ))
223
+ }
224
+
225
  async fn media_url_for_track(
226
  client: &mut APIClient,
227
  id: u32,
228
  formats: &Vec<Format>,
229
  ) -> Result<String, String> {
230
+ if client.license_token.is_empty() {
231
+ if let Err(e) = client.force_renew().await {
232
+ return Err(format!("renew:{e}"));
233
+ }
234
+ }
235
+
236
  let resp: Result<DeezerTrackList, APIError> = client
237
  .api_call(
238
  "song.getListData",
 
246
  }
247
 
248
  let track_token = track_list.data[0].TRACK_TOKEN.as_str();
249
+ if track_token.is_empty() {
250
+ return Err("empty TRACK_TOKEN".to_string());
251
+ }
 
252
 
253
+ let mut last_err = String::from("no_url");
 
 
 
254
 
255
+ for fmt in formats {
256
+ match fetch_media_url(client, std::slice::from_ref(fmt), track_token).await {
257
+ Ok(url) => return Ok(url),
258
+ Err(e) => last_err = e,
259
+ }
260
  }
261
+
262
+ if formats.len() > 1 {
263
+ if let Ok(url) = fetch_media_url(client, formats, track_token).await {
264
+ return Ok(url);
265
+ }
266
+ }
267
+
268
+ Err(last_err)
269
  }
270
 
271
  fn upstream_dzmedia_base() -> Option<String> {
 
511
  state.api_by_arl.write().await.remove(&arl);
512
  }
513
  let client = api_client_for_arl(&state, Some(arl.clone())).await;
514
+ let res = {
515
  let mut c = client.lock().await;
516
+ media_url_for_track(&mut c, q.id, &formats).await
517
+ };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
518
  match res {
519
+ Ok(u) => { url = u; break 'arl; }
520
  Err(e) => { last_err = Some(e); }
521
  }
522
  }
 
603
  }
604
  }
605
 
606
+ // До 3 попыток: 0 — renew+запрос, 1 — сброс кэша клиента, 2 — ещё раз renew
607
  for attempt in 0..3u8 {
608
  if url.is_some() { break; }
609
 
610
+ if attempt >= 1 {
 
611
  if let Some(ref arl) = arl_key {
612
  state.api_by_arl.write().await.remove(arl);
613
  } else {
 
618
  let client = api_client_for_arl(&state, arl_key.clone()).await;
619
  let res = {
620
  let mut c = client.lock().await;
621
+ if attempt >= 1 {
 
622
  if let Err(e) = c.force_renew().await {
623
  last_err = Some(format!("renew:{}", e));
624
+ continue;
625
  }
626
  }
627
  media_url_for_track(&mut c, q.id, &formats).await
 
1103
  token: String,
1104
  uid: String,
1105
  access_token: String,
1106
+ license_token: String,
1107
  }
1108
 
1109
  fn gw_browser_ua() -> &'static str {
 
1176
  email: &str,
1177
  password_md5: &str,
1178
  user_id: Option<&str>,
1179
+ ) -> Result<(String, String, String, String), String> {
1180
  let mut last_err = String::from("getArl:none");
1181
 
1182
  for attempt in 0..3u8 {
 
1228
  Ok(v) => {
1229
  let arl = v.as_str().unwrap_or("").to_string();
1230
  if arl.len() >= 100 {
1231
+ let license_token = license_token_from_ud(&ud);
1232
+ return Ok((arl, check_form, uid, license_token));
1233
  }
1234
  last_err = format!("arl_too_short:{}", arl.len());
1235
  }
 
1261
  async fn try_fetch_arl_for_token(
1262
  access_token: &str,
1263
  user_id: Option<&str>,
1264
+ ) -> (Option<String>, Option<String>, Option<String>) {
1265
  let jar = Arc::new(Jar::default());
1266
  let url_deezer = match "https://www.deezer.com".parse::<Url>() {
1267
  Ok(u) => u,
1268
+ Err(_) => return (None, None, None),
1269
  };
1270
  jar.add_cookie_str("comeback=1; Domain=.deezer.com; Path=/", &url_deezer);
1271
 
 
1276
  .build()
1277
  {
1278
  Ok(c) => c,
1279
+ Err(_) => return (None, None, None),
1280
  };
1281
 
1282
  let _ = client
 
1287
  .await;
1288
 
1289
  if ensure_anonymous_sid(&client, &jar).await.is_err() {
1290
+ return (None, None, None);
1291
  }
1292
 
1293
  establish_oauth_session(&client, &jar, access_token).await;
 
1304
  };
1305
 
1306
  match gw_fetch_arl(&client, "", "", uid.as_deref()).await {
1307
+ Ok((arl, token, _, license_token)) if arl.len() >= 20 => {
1308
  let gw_token = if token.is_empty() { None } else { Some(token) };
1309
+ let lic = if license_token.is_empty() { None } else { Some(license_token) };
1310
+ (Some(arl), gw_token, lic)
1311
  }
1312
+ _ => (None, None, None),
1313
  }
1314
  }
1315
 
 
1449
  .filter(|id| *id > 0)
1450
  .map(|id| id.to_string());
1451
 
1452
+ let (arl, token, uid, license_token) = match gw_fetch_arl(
1453
  &client,
1454
  email,
1455
  password_md5,
 
1459
  {
1460
  Ok(v) => v,
1461
  Err(_e) => {
 
1462
  if let Ok(mobile_arl) = mobile_login(email, password_md5).await {
1463
  (
1464
  mobile_arl,
1465
  String::new(),
1466
  uid_from_api.clone().unwrap_or_default(),
1467
+ String::new(),
1468
  )
1469
  } else {
1470
  return Ok(WebLoginResult {
 
1472
  token: String::new(),
1473
  uid: uid_from_api.unwrap_or_default(),
1474
  access_token,
1475
+ license_token: String::new(),
1476
  });
1477
  }
1478
  }
1479
  };
1480
 
1481
  Ok(WebLoginResult {
1482
+ arl: Some(arl.clone()),
1483
  token,
1484
  uid: if uid.is_empty() {
1485
  uid_from_api.unwrap_or_default()
 
1487
  uid
1488
  },
1489
  access_token,
1490
+ license_token,
1491
  })
1492
  }
1493
 
 
1602
  ])
1603
  .header(ACCEPT, "*/*")
1604
  .header("Cache-Control", "max-age=0")
 
1605
  .header("Origin", "https://www.deezer.com")
1606
  .header("Referer", "https://www.deezer.com/")
1607
  .header("Sec-Fetch-Site", "same-origin")
 
1704
  .query(&qp)
1705
  .header(ACCEPT, "*/*")
1706
  .header("Cache-Control", "max-age=0")
 
1707
  .header("Origin", "https://www.deezer.com")
1708
  .header("Referer", "https://www.deezer.com/")
1709
  .header("Sec-Fetch-Site", "same-origin")
 
1746
  s.split(';').any(|p| p.trim_start().starts_with(cookie_prefix))
1747
  }
1748
 
1749
+ async fn login(State(state): State<AppState>, Json(req): Json<LoginReq>) -> impl IntoResponse {
1750
  let email = req.email.trim().to_string();
1751
  let pass = req.password_md5.trim().to_string();
1752
 
 
1755
  }
1756
 
1757
  match web_login(&email, &pass).await {
1758
+ Ok(res) => {
1759
+ if let Some(ref arl) = res.arl {
1760
+ store_arl_session(&state, arl, &res.token, &res.license_token).await;
1761
+ state.api_by_arl.write().await.remove(arl);
1762
+ }
1763
+ (StatusCode::OK, Json(json!({
1764
+ "arl": res.arl,
1765
+ "token": res.token,
1766
+ "uid": res.uid,
1767
+ "access_token": res.access_token
1768
+ }))).into_response()
1769
+ }
1770
  Err(e) => json_error(
1771
  StatusCode::UNAUTHORIZED,
1772
  format!("login_failed; {e}"),
 
1979
  Some(user_id.clone())
1980
  };
1981
 
1982
+ let (arl_opt, token_opt, license_opt) = try_fetch_arl_for_token(
1983
  &access_token,
1984
  uid_from_api.as_deref(),
1985
  )
1986
  .await;
1987
 
1988
+ if let (Some(ref arl), Some(ref token), Some(ref lic)) = (&arl_opt, &token_opt, &license_opt) {
1989
+ store_arl_session(&state, arl, token, lic).await;
1990
+ state.api_by_arl.write().await.remove(arl);
1991
+ } else if let (Some(ref arl), Some(ref token)) = (&arl_opt, &token_opt) {
1992
+ store_arl_session(&state, arl, token, "").await;
1993
+ state.api_by_arl.write().await.remove(arl);
1994
+ }
1995
+
1996
  let mut map = state.pair.write().await;
1997
  let Some(sess) = map.get_mut(&code) else {
1998
  return json_error(StatusCode::NOT_FOUND, "no such code");
 
2224
  }
2225
  drop(map);
2226
  pair_store_save(&state).await;
2227
+ if let Some(ref arl) = login.arl {
2228
+ store_arl_session(&state, arl, &login.token, &login.license_token).await;
2229
+ state.api_by_arl.write().await.remove(arl);
2230
+ }
2231
  let msg = if login.arl.is_some() {
2232
  "<h3>Готово</h3><p>Вернись на ТВ — плагин подключится автоматически.</p>"
2233
  } else {
 
2330
  access_token: String,
2331
  }
2332
 
2333
+ async fn oauth_arl(State(state): State<AppState>, Json(req): Json<OauthArlReq>) -> impl IntoResponse {
2334
  let token = req.access_token.trim().to_string();
2335
  if token.is_empty() {
2336
  return json_error(StatusCode::BAD_REQUEST, "access_token required");
 
2369
  .map(|id| id.to_string());
2370
 
2371
  match gw_fetch_arl(&client, "", "", uid.as_deref()).await {
2372
+ Ok((arl, check_form, _, license_token)) if arl.len() >= 20 => {
2373
+ store_arl_session(&state, &arl, &check_form, &license_token).await;
2374
+ state.api_by_arl.write().await.remove(&arl);
2375
  (StatusCode::OK, Json(json!({ "arl": arl }))).into_response()
2376
  }
2377
+ Ok((arl, _, _, _)) => {
2378
  json_error(
2379
  StatusCode::SERVICE_UNAVAILABLE,
2380
  format!("oauth_arl:arl_too_short:{}", arl.len()),
 
2800
  }
2801
  }
2802
 
2803
+ #[derive(Debug, Deserialize)]
2804
+ struct WarmArlReq {
2805
+ arl: String,
2806
+ }
2807
+
2808
+ async fn warm_arl(State(state): State<AppState>, Json(req): Json<WarmArlReq>) -> impl IntoResponse {
2809
+ let arl = req.arl.trim().to_string();
2810
+ if arl.len() < 20 {
2811
+ return json_error(StatusCode::BAD_REQUEST, "arl required");
2812
+ }
2813
+
2814
+ state.api_by_arl.write().await.remove(&arl);
2815
+ let client = api_client_for_arl(&state, Some(arl.clone())).await;
2816
+ let session = {
2817
+ let mut c = client.lock().await;
2818
+ if c.license_token.is_empty() {
2819
+ if let Err(e) = c.force_renew().await {
2820
+ return json_error(StatusCode::SERVICE_UNAVAILABLE, format!("warm:renew:{e}"));
2821
+ }
2822
+ }
2823
+ (c.check_form.clone(), c.license_token.clone())
2824
+ };
2825
+
2826
+ store_arl_session(&state, &arl, &session.0, &session.1).await;
2827
+ state.api_by_arl.write().await.remove(&arl);
2828
+ (StatusCode::OK, Json(json!({ "ok": true }))).into_response()
2829
+ }
2830
+
2831
  #[tokio::main]
2832
  async fn main() {
2833
  let bind_addr = std::env::var("BIND_ADDR").unwrap_or("[::]".to_string());
 
2837
  let state = AppState {
2838
  api: Arc::new(Mutex::new(APIClient::new())),
2839
  api_by_arl: Arc::new(RwLock::new(HashMap::new())),
2840
+ arl_sessions: Arc::new(RwLock::new(HashMap::new())),
2841
  pair: Arc::new(RwLock::new(HashMap::new())),
2842
  pair_store_path: std::env::var("PAIR_STORE_PATH").ok().unwrap_or_else(|| "pair_store.json".to_string()),
2843
  };
 
2873
  .route("/oauth/playlists", post(oauth_playlists))
2874
  .route("/oauth/playlist_tracks", post(oauth_playlist_tracks))
2875
  .route("/oauth/arl", post(oauth_arl))
2876
+ .route("/session/warm", post(warm_arl))
2877
  .route("/oauth/flow", post(oauth_flow))
2878
  .route("/oauth/reco_playlists", post(oauth_reco_playlists))
2879
  .route("/oauth/reco_albums", post(oauth_reco_albums))