recycleactor commited on
Commit
40ed42d
·
verified ·
1 Parent(s): 835983b

Upload main.rs

Browse files
Files changed (1) hide show
  1. src/main.rs +96 -69
src/main.rs CHANGED
@@ -63,6 +63,11 @@ struct AppState {
63
  arl_store_path: String,
64
  /// CDN URL кэш для предзагрузки. Ключ: track_id. TTL 20 мин.
65
  cdn_cache: Arc<RwLock<HashMap<u64, CdnCacheEntry>>>,
 
 
 
 
 
66
  }
67
 
68
  fn license_token_from_ud(ud: &serde_json::Value) -> String {
@@ -151,10 +156,7 @@ async fn api_client_for_arl(state: &AppState, arl: Option<String>) -> Arc<Mutex<
151
  }
152
 
153
  fn extract_media_url_and_format(json: &serde_json::Value, formats: &Vec<&str>) -> (String, String, usize) {
154
- use std::io::Write;
155
- let mut log_file = std::fs::OpenOptions::new().create(true).append(true).open("debug_dzmedia.log").unwrap();
156
- let _ = writeln!(log_file, "--- MEDIA JSON RESPONSE ---");
157
- let _ = writeln!(log_file, "{}", serde_json::to_string_pretty(json).unwrap_or_default());
158
 
159
  let data = json.get("data").and_then(|v| v.as_array());
160
  let Some(data_arr) = data else {
@@ -339,25 +341,23 @@ async fn media_url_for_track(
339
  id: u64,
340
  formats: &Vec<Format>,
341
  ) -> Result<(String, String, u64), String> {
342
- use std::io::Write;
343
- let mut log_file = std::fs::OpenOptions::new().create(true).append(true).open("debug_dzmedia.log").unwrap();
344
- let _ = writeln!(log_file, "--- NEW REQUEST FOR ID {} ---", id);
345
 
346
  if client.license_token.is_empty() {
347
  if let Err(e) = client.force_renew().await {
348
- let _ = writeln!(log_file, "force_renew err: {}", e);
349
  return Err(format!("renew:{e}"));
350
  }
351
  }
352
 
353
  let q_json = serde_json::json!({"sng_ids":[id],"array_default":["SNG_ID","TRACK_TOKEN","FALLBACK"]});
354
  let r: serde_json::Value = client.api_call("song.getListData", &q_json).await.map_err(|e| {
355
- let _ = writeln!(log_file, "api_call err: {}", e);
356
  e.to_string()
357
  })?;
358
 
359
  let item = r.pointer("/data/0").ok_or_else(|| {
360
- let _ = writeln!(log_file, "No valid ID");
361
  "No valid ID".to_string()
362
  })?;
363
 
@@ -373,33 +373,24 @@ async fn media_url_for_track(
373
  })
374
  }).unwrap_or(0);
375
 
376
- let _ = writeln!(log_file, "track_token: {}, fallback_id: {}, fallback_token: {}", track_token, fallback_id, fallback_token);
377
 
378
  if fallback_id > 0 {
379
  let q_fb = serde_json::json!({"sng_ids":[fallback_id],"array_default":["SNG_ID","TRACK_TOKEN","FALLBACK"]});
380
- let _ = writeln!(log_file, "Querying fallback ID: {}", fallback_id);
381
  if let Ok(r_fb) = client.api_call::<serde_json::Value, serde_json::Value>("song.getListData", &q_fb).await {
382
  if let Some(item_fb) = r_fb.pointer("/data/0") {
383
  if let Some(real_tk) = item_fb.get("TRACK_TOKEN").and_then(|v| v.as_str()) {
384
- let _ = writeln!(log_file, "Fetched real_tk for fallback: {}", real_tk);
385
  let tokens = vec![real_tk];
386
  match fetch_media_url(client, formats, tokens).await {
387
  Ok((url, fmt, _)) => {
388
- let _ = writeln!(log_file, "fetch_media_url SUCCESS for fallback: fmt={}", fmt);
389
  return Ok((url, fmt, fallback_id));
390
  },
391
  Err(e) => {
392
- let _ = writeln!(log_file, "fetch_media_url FAILED for fallback: {}", e);
393
  }
394
  }
395
- } else {
396
- let _ = writeln!(log_file, "NO TRACK_TOKEN in fallback response");
397
  }
398
- } else {
399
- let _ = writeln!(log_file, "Empty data array in fallback response");
400
  }
401
- } else {
402
- let _ = writeln!(log_file, "api_call for fallback ID FAILED");
403
  }
404
  }
405
 
@@ -416,20 +407,18 @@ async fn media_url_for_track(
416
  }
417
 
418
  if tokens.is_empty() {
419
- let _ = writeln!(log_file, "empty TRACK_TOKEN and FALLBACK");
420
  return Err("empty TRACK_TOKEN and FALLBACK".to_string());
421
  }
422
 
423
- let _ = writeln!(log_file, "Falling back to old logic. Tokens: {:?}", tokens);
424
  let res = fetch_media_url(client, formats, tokens).await;
425
  match res {
426
  Ok((url, fmt, idx)) => {
427
- let _ = writeln!(log_file, "Old logic SUCCESS: fmt={}", fmt);
428
  let id_to_use = *token_ids.get(idx).unwrap_or(&id);
429
  Ok((url, fmt, id_to_use))
430
  },
431
  Err(e) => {
432
- let _ = writeln!(log_file, "Old logic FAILED: {}", e);
433
  Err(e)
434
  }
435
  }
@@ -749,7 +738,6 @@ impl From<&StreamParams> for AudioTagMeta {
749
  }
750
 
751
  async fn stream_info(State(state): State<AppState>, Query(q): Query<StreamParams>) -> impl IntoResponse {
752
- let _ = std::fs::remove_file("debug_dzmedia.log");
753
  let formats = match q.format.as_deref().unwrap_or("AUTO") {
754
  "FLAC" => vec![Format::FLAC, Format::MP3_320, Format::MP3_128],
755
  "MP3_320" => vec![Format::MP3_320, Format::MP3_128],
@@ -841,8 +829,6 @@ async fn stream_info(State(state): State<AppState>, Query(q): Query<StreamParams
841
  ((total_bytes as f64) * 8.0 / (duration as f64) / 1000.0).round() as u64
842
  } else { 0 };
843
 
844
- let log_data = std::fs::read_to_string("debug_dzmedia.log").unwrap_or_default();
845
-
846
  (StatusCode::OK, Json(json!({
847
  "id": q.id,
848
  "requested": requested,
@@ -851,7 +837,7 @@ async fn stream_info(State(state): State<AppState>, Query(q): Query<StreamParams
851
  "bytes": total_bytes,
852
  "bitrate_kbps": bitrate_kbps,
853
  "mime": mime,
854
- "log": log_data
855
  }))).into_response()
856
  }
857
 
@@ -899,7 +885,7 @@ async fn fetch_cover_picture(cover_url: &str) -> Option<Picture> {
899
  Some(picture)
900
  }
901
 
902
- async fn inject_audio_metadata(mut bytes: Vec<u8>, ext: &str, meta: &AudioTagMeta) -> Vec<u8> {
903
  let has_text = meta.title.is_some() || meta.performer.is_some() || meta.album.is_some();
904
  let cover = match meta.cover_url.as_deref() {
905
  Some(url) => fetch_cover_picture(url).await,
@@ -910,12 +896,6 @@ async fn inject_audio_metadata(mut bytes: Vec<u8>, ext: &str, meta: &AudioTagMet
910
  return bytes;
911
  }
912
 
913
- // Временно отключаем добавление метаданных для FLAC - возможно lofty ломает файл
914
- if ext.eq_ignore_ascii_case("flac") {
915
- tracing::warn!("Skipping metadata injection for FLAC to avoid corruption");
916
- return bytes;
917
- }
918
-
919
  let tag_type = if ext.eq_ignore_ascii_case("flac") {
920
  TagType::VorbisComments
921
  } else {
@@ -936,35 +916,55 @@ async fn inject_audio_metadata(mut bytes: Vec<u8>, ext: &str, meta: &AudioTagMet
936
  tag.push_picture(picture);
937
  }
938
 
939
- let unique = SystemTime::now()
940
- .duration_since(UNIX_EPOCH)
941
- .map(|d| d.as_nanos())
942
- .unwrap_or(0);
943
- let temp_path = std::env::temp_dir().join(format!("dzmedia_tagged_{}.{}", unique, ext));
944
- if let Err(e) = std::fs::write(&temp_path, &bytes) {
945
- tracing::warn!("metadata temp write failed: {}", e);
946
- return bytes;
947
- }
948
 
949
- let write_options = if ext.eq_ignore_ascii_case("mp3") {
950
- WriteOptions::new().use_id3v23(true)
951
- } else {
952
- WriteOptions::new()
953
- };
 
 
 
 
 
 
 
 
 
 
 
954
 
955
- let write_result = tag.save_to_path(&temp_path, write_options);
956
- if let Err(e) = write_result {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
957
  let _ = std::fs::remove_file(&temp_path);
958
- tracing::warn!("metadata tagging failed: {}", e);
959
- return bytes;
960
- }
 
 
 
 
961
 
962
- match std::fs::read(&temp_path) {
963
- Ok(tagged) => bytes = tagged,
964
- Err(e) => tracing::warn!("metadata temp read failed: {}", e),
965
- }
966
- let _ = std::fs::remove_file(&temp_path);
967
- bytes
968
  }
969
 
970
  async fn send_audio(
@@ -1078,15 +1078,13 @@ async fn send_audio(
1078
  let mime = if ext == "flac" { "audio/flac" } else { "audio/mpeg" };
1079
  let filename = format!("track_{}.{}", q.id, ext);
1080
 
1081
- // Загружаем в Telegram через отдельный клиент.
 
 
 
 
1082
  let tg_url = format!("https://api.telegram.org/bot{}/sendAudio", bot_token);
1083
- let tg_http = reqwest::Client::builder()
1084
- .https_only(true)
1085
- .connect_timeout(Duration::from_secs(10))
1086
- .timeout(Duration::from_secs(30))
1087
- .tcp_keepalive(Duration::from_secs(20))
1088
- .build()
1089
- .unwrap();
1090
 
1091
  let mut tg_json: Option<serde_json::Value> = None;
1092
  let mut last_tg_err: Option<String> = None;
@@ -3991,6 +3989,17 @@ async fn main() {
3991
  let port = std::env::var("PORT").unwrap_or("7860".to_string());
3992
  let port: u16 = port.parse().unwrap_or(7860);
3993
 
 
 
 
 
 
 
 
 
 
 
 
3994
  let state = AppState {
3995
  api: Arc::new(Mutex::new(APIClient::new())),
3996
  api_by_arl: Arc::new(RwLock::new(HashMap::new())),
@@ -3999,10 +4008,28 @@ async fn main() {
3999
  pair_store_path: std::env::var("PAIR_STORE_PATH").ok().unwrap_or_else(|| "pair_store.json".to_string()),
4000
  arl_store_path: std::env::var("ARL_STORE_PATH").ok().unwrap_or_else(|| "arl_store.json".to_string()),
4001
  cdn_cache: Arc::new(RwLock::new(HashMap::new())),
 
4002
  };
4003
  arl_store_load(&state).await;
4004
  pair_store_load(&state).await;
4005
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4006
  let cors = CorsLayer::new()
4007
  .allow_origin(Any)
4008
  .allow_methods([Method::GET, Method::POST, Method::OPTIONS, Method::HEAD])
 
63
  arl_store_path: String,
64
  /// CDN URL кэш для предзагрузки. Ключ: track_id. TTL 20 мин.
65
  cdn_cache: Arc<RwLock<HashMap<u64, CdnCacheEntry>>>,
66
+ /// Общий переиспользуемый клиент для аплоада в Telegram.
67
+ /// Живёт весь процесс — держит keep-alive соединение и DNS-кэш к
68
+ /// api.telegram.org "тёплыми", вместо пересоздания клиента (а с ним
69
+ /// DNS+TLS handshake с нуля) на каждый /send_audio.
70
+ tg_http: Arc<reqwest::Client>,
71
  }
72
 
73
  fn license_token_from_ud(ud: &serde_json::Value) -> String {
 
156
  }
157
 
158
  fn extract_media_url_and_format(json: &serde_json::Value, formats: &Vec<&str>) -> (String, String, usize) {
159
+ tracing::trace!(target: "dzmedia::media_json", "{}", json);
 
 
 
160
 
161
  let data = json.get("data").and_then(|v| v.as_array());
162
  let Some(data_arr) = data else {
 
341
  id: u64,
342
  formats: &Vec<Format>,
343
  ) -> Result<(String, String, u64), String> {
344
+ tracing::trace!("media_url_for_track: new request for id {}", id);
 
 
345
 
346
  if client.license_token.is_empty() {
347
  if let Err(e) = client.force_renew().await {
348
+ tracing::warn!("media_url_for_track: force_renew err: {}", e);
349
  return Err(format!("renew:{e}"));
350
  }
351
  }
352
 
353
  let q_json = serde_json::json!({"sng_ids":[id],"array_default":["SNG_ID","TRACK_TOKEN","FALLBACK"]});
354
  let r: serde_json::Value = client.api_call("song.getListData", &q_json).await.map_err(|e| {
355
+ tracing::warn!("media_url_for_track: api_call err: {}", e);
356
  e.to_string()
357
  })?;
358
 
359
  let item = r.pointer("/data/0").ok_or_else(|| {
360
+ tracing::warn!("media_url_for_track: no valid id");
361
  "No valid ID".to_string()
362
  })?;
363
 
 
373
  })
374
  }).unwrap_or(0);
375
 
376
+ tracing::trace!("media_url_for_track: track_token={}, fallback_id={}, fallback_token={}", track_token, fallback_id, fallback_token);
377
 
378
  if fallback_id > 0 {
379
  let q_fb = serde_json::json!({"sng_ids":[fallback_id],"array_default":["SNG_ID","TRACK_TOKEN","FALLBACK"]});
 
380
  if let Ok(r_fb) = client.api_call::<serde_json::Value, serde_json::Value>("song.getListData", &q_fb).await {
381
  if let Some(item_fb) = r_fb.pointer("/data/0") {
382
  if let Some(real_tk) = item_fb.get("TRACK_TOKEN").and_then(|v| v.as_str()) {
 
383
  let tokens = vec![real_tk];
384
  match fetch_media_url(client, formats, tokens).await {
385
  Ok((url, fmt, _)) => {
 
386
  return Ok((url, fmt, fallback_id));
387
  },
388
  Err(e) => {
389
+ tracing::trace!("media_url_for_track: fetch_media_url failed for fallback: {}", e);
390
  }
391
  }
 
 
392
  }
 
 
393
  }
 
 
394
  }
395
  }
396
 
 
407
  }
408
 
409
  if tokens.is_empty() {
410
+ tracing::warn!("media_url_for_track: empty TRACK_TOKEN and FALLBACK");
411
  return Err("empty TRACK_TOKEN and FALLBACK".to_string());
412
  }
413
 
 
414
  let res = fetch_media_url(client, formats, tokens).await;
415
  match res {
416
  Ok((url, fmt, idx)) => {
 
417
  let id_to_use = *token_ids.get(idx).unwrap_or(&id);
418
  Ok((url, fmt, id_to_use))
419
  },
420
  Err(e) => {
421
+ tracing::warn!("media_url_for_track: old logic failed: {}", e);
422
  Err(e)
423
  }
424
  }
 
738
  }
739
 
740
  async fn stream_info(State(state): State<AppState>, Query(q): Query<StreamParams>) -> impl IntoResponse {
 
741
  let formats = match q.format.as_deref().unwrap_or("AUTO") {
742
  "FLAC" => vec![Format::FLAC, Format::MP3_320, Format::MP3_128],
743
  "MP3_320" => vec![Format::MP3_320, Format::MP3_128],
 
829
  ((total_bytes as f64) * 8.0 / (duration as f64) / 1000.0).round() as u64
830
  } else { 0 };
831
 
 
 
832
  (StatusCode::OK, Json(json!({
833
  "id": q.id,
834
  "requested": requested,
 
837
  "bytes": total_bytes,
838
  "bitrate_kbps": bitrate_kbps,
839
  "mime": mime,
840
+ "log": ""
841
  }))).into_response()
842
  }
843
 
 
885
  Some(picture)
886
  }
887
 
888
+ async fn inject_audio_metadata(bytes: Vec<u8>, ext: &str, meta: &AudioTagMeta) -> Vec<u8> {
889
  let has_text = meta.title.is_some() || meta.performer.is_some() || meta.album.is_some();
890
  let cover = match meta.cover_url.as_deref() {
891
  Some(url) => fetch_cover_picture(url).await,
 
896
  return bytes;
897
  }
898
 
 
 
 
 
 
 
899
  let tag_type = if ext.eq_ignore_ascii_case("flac") {
900
  TagType::VorbisComments
901
  } else {
 
916
  tag.push_picture(picture);
917
  }
918
 
919
+ let ext_owned = ext.to_string();
920
+ let bytes_for_write = bytes.clone();
 
 
 
 
 
 
 
921
 
922
+ // Пишем/сохраняем/читаем temp-файл в spawn_blocking: на 20+MB FLAC
923
+ // это не микросекунды, а реальный синхронный I/O. Выполненный прямо
924
+ // в async fn, он держит tokio worker thread занятым и на это время
925
+ // не даёт прогрессировать вообще никаким другим задачам на этом
926
+ // воркере — включая коннект другого параллельного запроса в Telegram.
927
+ let tagged: Option<Vec<u8>> = tokio::task::spawn_blocking(move || {
928
+ let unique = SystemTime::now()
929
+ .duration_since(UNIX_EPOCH)
930
+ .map(|d| d.as_nanos())
931
+ .unwrap_or(0);
932
+ let temp_path = std::env::temp_dir().join(format!("dzmedia_tagged_{}.{}", unique, ext_owned));
933
+
934
+ if let Err(e) = std::fs::write(&temp_path, &bytes_for_write) {
935
+ tracing::warn!("metadata temp write failed: {}", e);
936
+ return None;
937
+ }
938
 
939
+ let write_options = if ext_owned.eq_ignore_ascii_case("mp3") {
940
+ WriteOptions::new().use_id3v23(true)
941
+ } else {
942
+ WriteOptions::new()
943
+ };
944
+
945
+ if let Err(e) = tag.save_to_path(&temp_path, write_options) {
946
+ let _ = std::fs::remove_file(&temp_path);
947
+ tracing::warn!("metadata tagging failed: {}", e);
948
+ return None;
949
+ }
950
+
951
+ let result = match std::fs::read(&temp_path) {
952
+ Ok(tagged) => Some(tagged),
953
+ Err(e) => {
954
+ tracing::warn!("metadata temp read failed: {}", e);
955
+ None
956
+ }
957
+ };
958
  let _ = std::fs::remove_file(&temp_path);
959
+ result
960
+ })
961
+ .await
962
+ .unwrap_or_else(|e| {
963
+ tracing::warn!("metadata blocking task failed: {}", e);
964
+ None
965
+ });
966
 
967
+ tagged.unwrap_or(bytes)
 
 
 
 
 
968
  }
969
 
970
  async fn send_audio(
 
1078
  let mime = if ext == "flac" { "audio/flac" } else { "audio/mpeg" };
1079
  let filename = format!("track_{}.{}", q.id, ext);
1080
 
1081
+ // Загружаем в Telegram через общий, уже прогретый клиент (см. AppState::tg_http).
1082
+ // Раньше клиент создавался заново на каждый вызов — из-за этого каждый
1083
+ // /send_audio платил полный DNS+TLS handshake с нуля, и на "холодных"
1084
+ // контейнерах это не укладывалось в connect_timeout (лог показывал
1085
+ // ровно 10.0с — таймаут коннекта, а не медленную передачу данных).
1086
  let tg_url = format!("https://api.telegram.org/bot{}/sendAudio", bot_token);
1087
+ let tg_http = state.tg_http.clone();
 
 
 
 
 
 
1088
 
1089
  let mut tg_json: Option<serde_json::Value> = None;
1090
  let mut last_tg_err: Option<String> = None;
 
3989
  let port = std::env::var("PORT").unwrap_or("7860".to_string());
3990
  let port: u16 = port.parse().unwrap_or(7860);
3991
 
3992
+ let tg_http = Arc::new(
3993
+ reqwest::Client::builder()
3994
+ .https_only(true)
3995
+ .connect_timeout(Duration::from_secs(20))
3996
+ .timeout(Duration::from_secs(60))
3997
+ .tcp_keepalive(Duration::from_secs(30))
3998
+ .pool_idle_timeout(Duration::from_secs(90))
3999
+ .build()
4000
+ .unwrap(),
4001
+ );
4002
+
4003
  let state = AppState {
4004
  api: Arc::new(Mutex::new(APIClient::new())),
4005
  api_by_arl: Arc::new(RwLock::new(HashMap::new())),
 
4008
  pair_store_path: std::env::var("PAIR_STORE_PATH").ok().unwrap_or_else(|| "pair_store.json".to_string()),
4009
  arl_store_path: std::env::var("ARL_STORE_PATH").ok().unwrap_or_else(|| "arl_store.json".to_string()),
4010
  cdn_cache: Arc::new(RwLock::new(HashMap::new())),
4011
+ tg_http,
4012
  };
4013
  arl_store_load(&state).await;
4014
  pair_store_load(&state).await;
4015
 
4016
+ // Прогреваем DNS+TLS до api.telegram.org сразу при старте контейнера,
4017
+ // а не на первом реальном /send_audio. Именно на "холодном" первом
4018
+ // запросе после старта конекты к Telegram чаще всего не укладываются
4019
+ // в connect_timeout.
4020
+ if let Ok(bot_token) = std::env::var("BOT_TOKEN") {
4021
+ if !bot_token.is_empty() {
4022
+ let warm_client = state.tg_http.clone();
4023
+ tokio::spawn(async move {
4024
+ let url = format!("https://api.telegram.org/bot{}/getMe", bot_token);
4025
+ match warm_client.get(&url).send().await {
4026
+ Ok(_) => tracing::info!("Telegram connection pre-warmed"),
4027
+ Err(e) => tracing::warn!("Telegram warm-up failed (non-fatal): {}", e),
4028
+ }
4029
+ });
4030
+ }
4031
+ }
4032
+
4033
  let cors = CorsLayer::new()
4034
  .allow_origin(Any)
4035
  .allow_methods([Method::GET, Method::POST, Method::OPTIONS, Method::HEAD])