recycleactor commited on
Commit
df27c84
·
verified ·
1 Parent(s): 3f7f2c1

Upload main.rs

Browse files
Files changed (1) hide show
  1. src/main.rs +171 -0
src/main.rs CHANGED
@@ -808,6 +808,176 @@ async fn stream_info(State(state): State<AppState>, Query(q): Query<StreamParams
808
  }))).into_response()
809
  }
810
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
811
  /// /download — like /stream but downloads full track into memory and returns with Content-Length.
812
  /// Telegram requires Content-Length for sendAudio by URL; this endpoint ensures it.
813
  async fn download(
@@ -3491,6 +3661,7 @@ async fn main() {
3491
  .route("/fetch", get(fetch))
3492
  .route("/stream", get(stream))
3493
  .route("/download", get(download))
 
3494
  .route("/stream_info", get(stream_info))
3495
  .route("/user_data", post(user_data))
3496
  .route("/playlists", post(playlists))
 
808
  }))).into_response()
809
  }
810
 
811
+ /// /send_audio — HuggingFace скачивает трек и сам загружает его в Telegram через multipart.
812
+ /// Render к аудио не прикасается, только получает обратно file_id для кэша.
813
+ /// Требует: BOT_TOKEN env var на HuggingFace Space.
814
+ #[derive(Debug, Deserialize)]
815
+ struct SendAudioParams {
816
+ id: u64,
817
+ format: Option<String>,
818
+ arl: Option<String>,
819
+ /// Telegram chat_id куда отправить аудио
820
+ chat_id: String,
821
+ /// Опциональные метаданные
822
+ title: Option<String>,
823
+ performer: Option<String>,
824
+ duration: Option<u64>,
825
+ }
826
+
827
+ async fn send_audio(
828
+ State(state): State<AppState>,
829
+ Query(q): Query<SendAudioParams>,
830
+ ) -> impl IntoResponse {
831
+ let bot_token = match std::env::var("BOT_TOKEN").ok().filter(|s| !s.is_empty()) {
832
+ Some(t) => t,
833
+ None => return json_error(StatusCode::SERVICE_UNAVAILABLE, "BOT_TOKEN not configured"),
834
+ };
835
+
836
+ let fmt_str = q.format.as_deref().unwrap_or("MP3_320");
837
+ let formats = match fmt_str {
838
+ "FLAC" => vec![Format::FLAC, Format::MP3_320, Format::MP3_128],
839
+ "MP3_320" => vec![Format::MP3_320, Format::MP3_128],
840
+ "MP3_128" => vec![Format::MP3_128],
841
+ _ => vec![Format::FLAC, Format::MP3_320, Format::MP3_128],
842
+ };
843
+
844
+ let arl_key = q.arl.as_ref().map(|s| s.trim().to_string()).filter(|s| !s.is_empty())
845
+ .or_else(|| std::env::var("DEEZER_ARL").ok().filter(|s| !s.is_empty()));
846
+ if arl_key.is_none() {
847
+ return json_error(StatusCode::SERVICE_UNAVAILABLE, "arl required");
848
+ }
849
+
850
+ // Resolve FALLBACK_ID
851
+ let client = api_client_for_arl(&state, arl_key.clone()).await;
852
+ let target_id = {
853
+ let mut c = client.lock().await;
854
+ resolve_fallback_id(&mut c, q.id).await
855
+ };
856
+ let mut decrypt_id = target_id;
857
+
858
+ let mut cdn_url_opt: Option<String> = None;
859
+ let mut used_fmt = String::new();
860
+ let mut last_err: Option<String> = None;
861
+
862
+ // Try upstream first
863
+ if let Ok(text) = upstream_get_url_text(&formats, &vec![target_id]).await {
864
+ if let Ok(v) = serde_json::from_str::<serde_json::Value>(&text) {
865
+ let fstrs: Vec<String> = formats.iter().map(|f| format!("{:?}", f)).collect();
866
+ let fstrs_ref: Vec<&str> = fstrs.iter().map(|s| s.as_str()).collect();
867
+ let (u, f, _) = extract_media_url_and_format(&v, &fstrs_ref);
868
+ if !u.is_empty() { cdn_url_opt = Some(u); used_fmt = f; }
869
+ }
870
+ }
871
+
872
+ // Fallback: ARL
873
+ for attempt in 0..2u8 {
874
+ if cdn_url_opt.is_some() { break; }
875
+ if attempt >= 1 {
876
+ if let Some(ref arl) = arl_key { state.api_by_arl.write().await.remove(arl); }
877
+ }
878
+ let client = api_client_for_arl(&state, arl_key.clone()).await;
879
+ let res = {
880
+ let mut c = client.lock().await;
881
+ if attempt >= 1 {
882
+ if let Err(e) = c.force_renew().await { last_err = Some(format!("renew:{}", e)); continue; }
883
+ }
884
+ match timeout(Duration::from_secs(15), media_url_for_track(&mut c, target_id, &formats)).await {
885
+ Ok(v) => v,
886
+ Err(_) => Err("timeout".to_string()),
887
+ }
888
+ };
889
+ match res {
890
+ Ok((u, f, id_used)) => { cdn_url_opt = Some(u); used_fmt = f; decrypt_id = id_used; break; }
891
+ Err(e) => { last_err = Some(e); }
892
+ }
893
+ }
894
+
895
+ let cdn_url = match cdn_url_opt {
896
+ Some(u) => u,
897
+ None => {
898
+ let err = last_err.unwrap_or_else(|| "send_audio:no_url".to_string());
899
+ tracing::error!("send_audio {} failed: {}", q.id, err);
900
+ return json_error(StatusCode::NOT_FOUND, "track not found");
901
+ }
902
+ };
903
+
904
+ // Скачиваем зашифрованный файл с CDN
905
+ let http = reqwest::Client::builder().no_proxy().timeout(Duration::from_secs(120)).build().unwrap();
906
+ let resp = match http.get(&cdn_url).send().await {
907
+ Ok(r) if r.status().is_success() => r,
908
+ Ok(r) => return json_error(StatusCode::BAD_GATEWAY, format!("CDN {}", r.status())),
909
+ Err(e) => return json_error(StatusCode::BAD_GATEWAY, format!("CDN fetch: {}", e)),
910
+ };
911
+ let content_type = resp.headers()
912
+ .get(reqwest::header::CONTENT_TYPE)
913
+ .and_then(|v| v.to_str().ok())
914
+ .unwrap_or("audio/mpeg")
915
+ .to_string();
916
+ let enc = match resp.bytes().await {
917
+ Ok(b) => b,
918
+ Err(e) => return json_error(StatusCode::BAD_GATEWAY, format!("CDN read: {}", e)),
919
+ };
920
+
921
+ // Расшифровываем в памяти
922
+ let key = blowfish_key(decrypt_id);
923
+ let mut dec = enc.to_vec();
924
+ let bs = 2048usize;
925
+ let mut bi = 0usize;
926
+ let mut pos = 0usize;
927
+ while pos + bs <= dec.len() {
928
+ if bi % 3 == 0 { let _ = decrypt_stripe(&mut dec[pos..pos + bs], &key); }
929
+ bi += 1; pos += bs;
930
+ }
931
+
932
+ let ext = if used_fmt.contains("FLAC") || content_type.contains("flac") { "flac" } else { "mp3" };
933
+ let mime = if ext == "flac" { "audio/flac" } else { "audio/mpeg" };
934
+ let filename = format!("track_{}.{}", q.id, ext);
935
+
936
+ // Загружаем в Telegram через multipart
937
+ let tg_url = format!("https://api.telegram.org/bot{}/sendAudio", bot_token);
938
+ let part = reqwest::multipart::Part::bytes(dec)
939
+ .file_name(filename)
940
+ .mime_str(mime)
941
+ .unwrap();
942
+ let mut form = reqwest::multipart::Form::new()
943
+ .text("chat_id", q.chat_id.clone())
944
+ .part("audio", part);
945
+ if let Some(ref t) = q.title { form = form.text("title", t.clone()); }
946
+ if let Some(ref p) = q.performer { form = form.text("performer", p.clone()); }
947
+ if let Some(d) = q.duration { form = form.text("duration", d.to_string()); }
948
+
949
+ let tg_resp = match http.post(&tg_url).multipart(form).send().await {
950
+ Ok(r) => r,
951
+ Err(e) => return json_error(StatusCode::BAD_GATEWAY, format!("Telegram upload: {}", e)),
952
+ };
953
+
954
+ let tg_json: serde_json::Value = match tg_resp.json().await {
955
+ Ok(j) => j,
956
+ Err(e) => return json_error(StatusCode::BAD_GATEWAY, format!("Telegram parse: {}", e)),
957
+ };
958
+
959
+ if tg_json.get("ok").and_then(|v| v.as_bool()) != Some(true) {
960
+ let desc = tg_json.get("description").and_then(|v| v.as_str()).unwrap_or("unknown");
961
+ tracing::error!("Telegram sendAudio error: {}", desc);
962
+ return json_error(StatusCode::BAD_GATEWAY, format!("Telegram: {}", desc));
963
+ }
964
+
965
+ let file_id = tg_json.pointer("/result/audio/file_id")
966
+ .and_then(|v| v.as_str())
967
+ .unwrap_or("")
968
+ .to_string();
969
+ let message_id = tg_json.pointer("/result/message_id")
970
+ .and_then(|v| v.as_i64())
971
+ .unwrap_or(0);
972
+
973
+ (StatusCode::OK, Json(json!({
974
+ "ok": true,
975
+ "file_id": file_id,
976
+ "message_id": message_id,
977
+ "format": ext,
978
+ }))).into_response()
979
+ }
980
+
981
  /// /download — like /stream but downloads full track into memory and returns with Content-Length.
982
  /// Telegram requires Content-Length for sendAudio by URL; this endpoint ensures it.
983
  async fn download(
 
3661
  .route("/fetch", get(fetch))
3662
  .route("/stream", get(stream))
3663
  .route("/download", get(download))
3664
+ .route("/send_audio", get(send_audio))
3665
  .route("/stream_info", get(stream_info))
3666
  .route("/user_data", post(user_data))
3667
  .route("/playlists", post(playlists))