Spaces:
Running
Running
Upload main.rs
Browse files- 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 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
|
| 356 |
e.to_string()
|
| 357 |
})?;
|
| 358 |
|
| 359 |
let item = r.pointer("/data/0").ok_or_else(|| {
|
| 360 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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":
|
| 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(
|
| 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
|
| 940 |
-
|
| 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 |
-
|
| 950 |
-
|
| 951 |
-
|
| 952 |
-
|
| 953 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 954 |
|
| 955 |
-
|
| 956 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 957 |
let _ = std::fs::remove_file(&temp_path);
|
| 958 |
-
|
| 959 |
-
|
| 960 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 961 |
|
| 962 |
-
|
| 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 =
|
| 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])
|