recycleactor commited on
Commit
f59f4a0
·
verified ·
1 Parent(s): d9836fe

Upload 2 files

Browse files
Files changed (1) hide show
  1. src/main.rs +144 -7
src/main.rs CHANGED
@@ -123,6 +123,55 @@ async fn media_url_for_track(
123
  Ok(url)
124
  }
125
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
126
  #[derive(Deserialize)]
127
  struct DeezerTrackList {
128
  data: Vec<DeezerTrack>
@@ -153,6 +202,13 @@ async fn get_url(State(state): State<AppState>, Json(req): Json<RequestParams>)
153
  return (StatusCode::BAD_REQUEST, "ID list cannot be empty".to_string());
154
  }
155
 
 
 
 
 
 
 
 
156
  let client = api_client_for_arl(&state, req.arl.clone()).await;
157
  let media_resp = {
158
  let mut client = client.lock().await;
@@ -277,7 +333,18 @@ async fn stream(State(state): State<AppState>, Query(q): Query<StreamParams>) ->
277
 
278
  let mut url: Option<String> = None;
279
  let mut last_err: Option<String> = None;
 
 
 
 
 
 
 
 
280
  for attempt in 0..2 {
 
 
 
281
  let client = api_client_for_arl(&state, arl_key.clone()).await;
282
  let res = {
283
  let mut client = client.lock().await;
@@ -304,12 +371,27 @@ async fn stream(State(state): State<AppState>, Query(q): Query<StreamParams>) ->
304
  }
305
  }
306
 
307
- let Some(url) = url else {
308
- return (
309
- StatusCode::SERVICE_UNAVAILABLE,
310
- last_err.unwrap_or_else(|| "stream:error".to_string()),
311
- )
312
- .into_response();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
313
  };
314
 
315
  let http = reqwest::Client::builder()
@@ -829,6 +911,7 @@ async fn pair_page(Query(q): Query<PairPageQuery>) -> impl IntoResponse {
829
  .box{{max-width:420px;margin:0 auto;background:#151515;padding:18px;border-radius:12px}}\
830
  input{{width:100%;padding:12px 14px;margin:10px 0;border-radius:10px;border:1px solid #333;background:#0f0f0f;color:#fff;font-size:16px}}\
831
  button{{width:100%;padding:12px 14px;border-radius:10px;border:0;background:#1db954;color:#000;font-weight:700;font-size:16px}}\
 
832
  .muted{{color:#aaa;font-size:13px;line-height:1.35}}</style>\
833
  </head><body><div class=\"box\">\
834
  <h2 style=\"margin:0 0 6px\">Вход Deezer</h2>\
@@ -840,6 +923,14 @@ async fn pair_page(Query(q): Query<PairPageQuery>) -> impl IntoResponse {
840
  <button type=\"submit\">Войти</button>\
841
  </form>\
842
  <p class=\"muted\" style=\"margin:12px 0 0\">Пароль используется только для по��учения токена доступа и не сохраняется.</p>\
 
 
 
 
 
 
 
 
843
  </div></body></html>"
844
  );
845
  Html(html)
@@ -852,6 +943,12 @@ struct PairCompleteForm {
852
  password: String,
853
  }
854
 
 
 
 
 
 
 
855
  fn htmlesc(s: &str) -> String {
856
  s.replace('&', "&amp;")
857
  .replace('<', "&lt;")
@@ -860,6 +957,27 @@ fn htmlesc(s: &str) -> String {
860
  .replace('\'', "&#39;")
861
  }
862
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
863
  async fn api_deezer_get(
864
  client: &reqwest::Client,
865
  path: &str,
@@ -999,7 +1117,25 @@ async fn pair_complete(State(state): State<AppState>, Form(f): Form<PairComplete
999
  }
1000
  }
1001
 
1002
- Html("<h3>Готово</h3><p>Вернись на ТВ — плагин подключится автоматически.</p>".to_string()).into_response()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1003
  }
1004
 
1005
  #[derive(Debug, Deserialize)]
@@ -1226,6 +1362,7 @@ async fn main() {
1226
  .route("/pair/status", get(pair_status))
1227
  .route("/pair", get(pair_page))
1228
  .route("/pair/complete", post(pair_complete))
 
1229
  .route("/oauth/me", post(oauth_me))
1230
  .route("/oauth/playlists", post(oauth_playlists))
1231
  .route("/oauth/playlist_tracks", post(oauth_playlist_tracks))
 
123
  Ok(url)
124
  }
125
 
126
+ fn upstream_dzmedia_base() -> Option<String> {
127
+ let env = std::env::var("DZMEDIA_UPSTREAM").ok().unwrap_or_default();
128
+ let base = env.trim();
129
+ if base.is_empty() {
130
+ return Some("https://lufts-dzmedia.fly.dev".to_string());
131
+ }
132
+ if base == "-" {
133
+ return None;
134
+ }
135
+ Some(base.trim_end_matches('/').to_string())
136
+ }
137
+
138
+ async fn upstream_get_url_text(formats: &Vec<Format>, ids: &Vec<u32>) -> Result<String, String> {
139
+ let Some(base) = upstream_dzmedia_base() else {
140
+ return Err("upstream:disabled".to_string());
141
+ };
142
+ let url = format!("{base}/get_url");
143
+
144
+ let client = reqwest::Client::builder()
145
+ .timeout(std::time::Duration::from_secs(20))
146
+ .build()
147
+ .map_err(|_| "upstream:client".to_string())?;
148
+
149
+ let r = client
150
+ .post(url)
151
+ .json(&json!({ "formats": formats, "ids": ids }))
152
+ .send()
153
+ .await
154
+ .map_err(|_| "upstream:network".to_string())?;
155
+
156
+ let status = r.status();
157
+ let text = r.text().await.map_err(|_| "upstream:read".to_string())?;
158
+ if !status.is_success() {
159
+ let snip: String = text.chars().take(200).collect();
160
+ return Err(format!("upstream:{}:{}", status.as_u16(), snip));
161
+ }
162
+ Ok(text)
163
+ }
164
+
165
+ async fn upstream_media_url(id: u32, formats: &Vec<Format>) -> Result<String, String> {
166
+ let text = upstream_get_url_text(formats, &vec![id]).await?;
167
+ let v: serde_json::Value = serde_json::from_str(&text).map_err(|_| "upstream:parse".to_string())?;
168
+ let url = extract_media_url(&v);
169
+ if url.is_empty() {
170
+ return Err("upstream:no_url".to_string());
171
+ }
172
+ Ok(url)
173
+ }
174
+
175
  #[derive(Deserialize)]
176
  struct DeezerTrackList {
177
  data: Vec<DeezerTrack>
 
202
  return (StatusCode::BAD_REQUEST, "ID list cannot be empty".to_string());
203
  }
204
 
205
+ let arl = req.arl.clone().unwrap_or_default().trim().to_string();
206
+ if arl.is_empty() {
207
+ if let Ok(t) = upstream_get_url_text(&req.formats, &req.ids).await {
208
+ return (StatusCode::OK, t);
209
+ }
210
+ }
211
+
212
  let client = api_client_for_arl(&state, req.arl.clone()).await;
213
  let media_resp = {
214
  let mut client = client.lock().await;
 
333
 
334
  let mut url: Option<String> = None;
335
  let mut last_err: Option<String> = None;
336
+
337
+ if arl_key.is_none() {
338
+ match upstream_media_url(q.id, &formats).await {
339
+ Ok(u) => url = Some(u),
340
+ Err(e) => last_err = Some(e),
341
+ }
342
+ }
343
+
344
  for attempt in 0..2 {
345
+ if url.is_some() {
346
+ break;
347
+ }
348
  let client = api_client_for_arl(&state, arl_key.clone()).await;
349
  let res = {
350
  let mut client = client.lock().await;
 
371
  }
372
  }
373
 
374
+ let url = match url {
375
+ Some(u) => u,
376
+ None => {
377
+ if arl_key.is_some() {
378
+ if let Ok(u) = upstream_media_url(q.id, &formats).await {
379
+ u
380
+ } else {
381
+ return (
382
+ StatusCode::SERVICE_UNAVAILABLE,
383
+ last_err.unwrap_or_else(|| "stream:error".to_string()),
384
+ )
385
+ .into_response();
386
+ }
387
+ } else {
388
+ return (
389
+ StatusCode::SERVICE_UNAVAILABLE,
390
+ last_err.unwrap_or_else(|| "stream:error".to_string()),
391
+ )
392
+ .into_response();
393
+ }
394
+ }
395
  };
396
 
397
  let http = reqwest::Client::builder()
 
911
  .box{{max-width:420px;margin:0 auto;background:#151515;padding:18px;border-radius:12px}}\
912
  input{{width:100%;padding:12px 14px;margin:10px 0;border-radius:10px;border:1px solid #333;background:#0f0f0f;color:#fff;font-size:16px}}\
913
  button{{width:100%;padding:12px 14px;border-radius:10px;border:0;background:#1db954;color:#000;font-weight:700;font-size:16px}}\
914
+ .secondary{{background:#2b2b2b;color:#fff}}\
915
  .muted{{color:#aaa;font-size:13px;line-height:1.35}}</style>\
916
  </head><body><div class=\"box\">\
917
  <h2 style=\"margin:0 0 6px\">Вход Deezer</h2>\
 
923
  <button type=\"submit\">Войти</button>\
924
  </form>\
925
  <p class=\"muted\" style=\"margin:12px 0 0\">Пароль используется только для по��учения токена доступа и не сохраняется.</p>\
926
+ <hr style=\"border:0;border-top:1px solid #2a2a2a;margin:16px 0\">\
927
+ <h3 style=\"margin:0 0 6px;font-size:16px\">ARL (для полного аудио)</h3>\
928
+ <div class=\"muted\">Если сервер не может автоматически получить ARL (ошибка 503/502), вставь ARL сюда — с телефона это быстрее, чем на ТВ.</div>\
929
+ <form method=\"POST\" action=\"/pair/arl\">\
930
+ <input type=\"hidden\" name=\"code\" value=\"{safe_code}\">\
931
+ <input name=\"arl\" type=\"text\" placeholder=\"arl=...\" autocomplete=\"off\">\
932
+ <button class=\"secondary\" type=\"submit\">Отправить ARL</button>\
933
+ </form>\
934
  </div></body></html>"
935
  );
936
  Html(html)
 
943
  password: String,
944
  }
945
 
946
+ #[derive(Deserialize)]
947
+ struct PairArlForm {
948
+ code: String,
949
+ arl: String,
950
+ }
951
+
952
  fn htmlesc(s: &str) -> String {
953
  s.replace('&', "&amp;")
954
  .replace('<', "&lt;")
 
957
  .replace('\'', "&#39;")
958
  }
959
 
960
+ async fn pair_arl(State(state): State<AppState>, Form(f): Form<PairArlForm>) -> impl IntoResponse {
961
+ let code = f.code.trim().to_string();
962
+ let arl = f.arl.trim().to_string();
963
+ if code.is_empty() {
964
+ return Html("<h3>Нужен code</h3>".to_string()).into_response();
965
+ }
966
+ if arl.len() < 20 {
967
+ return Html("<h3>ARL выглядит неверно</h3>".to_string()).into_response();
968
+ }
969
+
970
+ let mut map = state.pair.write().await;
971
+ let Some(sess) = map.get_mut(&code) else {
972
+ return Html("<h3>Код не найден или истёк</h3>".to_string()).into_response();
973
+ };
974
+ sess.arl = Some(arl);
975
+ sess.error = None;
976
+
977
+ Html("<h3>ARL сохранён</h3><p>Вернись на ТВ — треки должны стать полными.</p>".to_string())
978
+ .into_response()
979
+ }
980
+
981
  async fn api_deezer_get(
982
  client: &reqwest::Client,
983
  path: &str,
 
1117
  }
1118
  }
1119
 
1120
+ let map = state.pair.read().await;
1121
+ let sess_arl = map.get(&code).and_then(|s| s.arl.clone()).unwrap_or_default();
1122
+ if sess_arl.len() > 20 {
1123
+ return Html("<h3>Готово</h3><p>Вернись на ТВ — плагин подключится автоматически.</p>".to_string())
1124
+ .into_response();
1125
+ }
1126
+
1127
+ let safe_code = htmlesc(&code);
1128
+ Html(format!(
1129
+ "<h3>Вход выполнен</h3>\
1130
+ <p>Токен получен, но ARL сервер автоматически не вытащил (из‑за блокировки/ошибок gw-light на хостинге).</p>\
1131
+ <p>Чтобы включить полные треки, вставь ARL (с телефона):</p>\
1132
+ <form method=\"POST\" action=\"/pair/arl\">\
1133
+ <input type=\"hidden\" name=\"code\" value=\"{safe_code}\">\
1134
+ <input name=\"arl\" type=\"text\" placeholder=\"arl=...\" autocomplete=\"off\" style=\"width:100%;padding:12px 14px;border-radius:10px;border:1px solid #333;background:#0f0f0f;color:#fff;font-size:16px\">\
1135
+ <button type=\"submit\" style=\"margin-top:10px;width:100%;padding:12px 14px;border-radius:10px;border:0;background:#2b2b2b;color:#fff;font-weight:700;font-size:16px\">Отправить ARL</button>\
1136
+ </form>"
1137
+ ))
1138
+ .into_response()
1139
  }
1140
 
1141
  #[derive(Debug, Deserialize)]
 
1362
  .route("/pair/status", get(pair_status))
1363
  .route("/pair", get(pair_page))
1364
  .route("/pair/complete", post(pair_complete))
1365
+ .route("/pair/arl", post(pair_arl))
1366
  .route("/oauth/me", post(oauth_me))
1367
  .route("/oauth/playlists", post(oauth_playlists))
1368
  .route("/oauth/playlist_tracks", post(oauth_playlist_tracks))