Spaces:
Running
Running
Upload main.rs
Browse files- 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))
|