Spaces:
Running
Running
Upload main.rs
Browse files- src/main.rs +140 -0
src/main.rs
CHANGED
|
@@ -808,6 +808,145 @@ async fn stream_info(State(state): State<AppState>, Query(q): Query<StreamParams
|
|
| 808 |
}))).into_response()
|
| 809 |
}
|
| 810 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 811 |
async fn stream(
|
| 812 |
State(state): State<AppState>,
|
| 813 |
Query(q): Query<StreamParams>,
|
|
@@ -3351,6 +3490,7 @@ async fn main() {
|
|
| 3351 |
.route("/get_url", post(get_url))
|
| 3352 |
.route("/fetch", get(fetch))
|
| 3353 |
.route("/stream", get(stream))
|
|
|
|
| 3354 |
.route("/stream_info", get(stream_info))
|
| 3355 |
.route("/user_data", post(user_data))
|
| 3356 |
.route("/playlists", post(playlists))
|
|
|
|
| 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(
|
| 814 |
+
State(state): State<AppState>,
|
| 815 |
+
Query(q): Query<StreamParams>,
|
| 816 |
+
) -> impl IntoResponse {
|
| 817 |
+
let formats = match q.format.as_deref().unwrap_or("AUTO") {
|
| 818 |
+
"FLAC" => vec![Format::FLAC, Format::MP3_320, Format::MP3_128],
|
| 819 |
+
"MP3_320" => vec![Format::MP3_320, Format::MP3_128],
|
| 820 |
+
"MP3_MISC" => vec![Format::MP3_MISC, Format::MP3_128],
|
| 821 |
+
"MP3_128" => vec![Format::MP3_128],
|
| 822 |
+
_ => vec![Format::FLAC, Format::MP3_320, Format::MP3_128],
|
| 823 |
+
};
|
| 824 |
+
|
| 825 |
+
let arl_key = q.arl.as_ref().map(|s| s.trim().to_string()).filter(|s| !s.is_empty());
|
| 826 |
+
if arl_key.is_none() {
|
| 827 |
+
return (StatusCode::SERVICE_UNAVAILABLE, "arl required").into_response();
|
| 828 |
+
}
|
| 829 |
+
|
| 830 |
+
// Resolve correct FALLBACK_ID for decryption
|
| 831 |
+
let client = api_client_for_arl(&state, arl_key.clone()).await;
|
| 832 |
+
let target_id = {
|
| 833 |
+
let mut c = client.lock().await;
|
| 834 |
+
resolve_fallback_id(&mut c, q.id).await
|
| 835 |
+
};
|
| 836 |
+
let mut decrypt_id = target_id;
|
| 837 |
+
|
| 838 |
+
let mut url: Option<String> = None;
|
| 839 |
+
let mut used_fmt = String::new();
|
| 840 |
+
let mut last_err: Option<String> = None;
|
| 841 |
+
|
| 842 |
+
// Try DZMEDIA_UPSTREAM first for highest quality
|
| 843 |
+
if url.is_none() {
|
| 844 |
+
if let Ok(text) = upstream_get_url_text(&formats, &vec![target_id]).await {
|
| 845 |
+
if let Ok(v) = serde_json::from_str::<serde_json::Value>(&text) {
|
| 846 |
+
let format_strings: Vec<String> = formats.iter().map(|f| format!("{:?}", f)).collect();
|
| 847 |
+
let format_strs: Vec<&str> = format_strings.iter().map(|s| s.as_str()).collect();
|
| 848 |
+
let (u, f, _) = extract_media_url_and_format(&v, &format_strs);
|
| 849 |
+
if !u.is_empty() {
|
| 850 |
+
url = Some(u);
|
| 851 |
+
used_fmt = f;
|
| 852 |
+
}
|
| 853 |
+
}
|
| 854 |
+
}
|
| 855 |
+
}
|
| 856 |
+
|
| 857 |
+
// Fallback: ARL path
|
| 858 |
+
for attempt in 0..2u8 {
|
| 859 |
+
if url.is_some() { break; }
|
| 860 |
+
if attempt >= 1 {
|
| 861 |
+
if let Some(ref arl) = arl_key {
|
| 862 |
+
state.api_by_arl.write().await.remove(arl);
|
| 863 |
+
}
|
| 864 |
+
}
|
| 865 |
+
let client = api_client_for_arl(&state, arl_key.clone()).await;
|
| 866 |
+
let res = {
|
| 867 |
+
let mut c = client.lock().await;
|
| 868 |
+
if attempt >= 1 {
|
| 869 |
+
if let Err(e) = c.force_renew().await {
|
| 870 |
+
last_err = Some(format!("renew:{}", e));
|
| 871 |
+
continue;
|
| 872 |
+
}
|
| 873 |
+
}
|
| 874 |
+
match timeout(Duration::from_secs(15), media_url_for_track(&mut c, target_id, &formats)).await {
|
| 875 |
+
Ok(v) => v,
|
| 876 |
+
Err(_) => Err("timeout".to_string()),
|
| 877 |
+
}
|
| 878 |
+
};
|
| 879 |
+
match res {
|
| 880 |
+
Ok((u, f, id_used)) => { url = Some(u); used_fmt = f; decrypt_id = id_used; break; }
|
| 881 |
+
Err(e) => { last_err = Some(e); }
|
| 882 |
+
}
|
| 883 |
+
}
|
| 884 |
+
|
| 885 |
+
let cdn_url = match url {
|
| 886 |
+
Some(u) => u,
|
| 887 |
+
None => {
|
| 888 |
+
let err = last_err.unwrap_or_else(|| "download:no_url".to_string());
|
| 889 |
+
tracing::error!("download {} failed: {}", q.id, err);
|
| 890 |
+
return (StatusCode::NOT_FOUND, "Not Found").into_response();
|
| 891 |
+
}
|
| 892 |
+
};
|
| 893 |
+
|
| 894 |
+
// Download the full encrypted file
|
| 895 |
+
let http = reqwest::Client::builder().no_proxy().timeout(Duration::from_secs(120)).build().unwrap();
|
| 896 |
+
let resp = match http.get(&cdn_url).send().await {
|
| 897 |
+
Ok(r) => r,
|
| 898 |
+
Err(e) => {
|
| 899 |
+
tracing::error!("download {} cdn fetch failed: {}", q.id, e);
|
| 900 |
+
return (StatusCode::BAD_GATEWAY, "CDN fetch failed").into_response();
|
| 901 |
+
}
|
| 902 |
+
};
|
| 903 |
+
if !resp.status().is_success() {
|
| 904 |
+
return (StatusCode::BAD_GATEWAY, "CDN error").into_response();
|
| 905 |
+
}
|
| 906 |
+
let content_type = resp.headers()
|
| 907 |
+
.get(reqwest::header::CONTENT_TYPE)
|
| 908 |
+
.and_then(|v| v.to_str().ok())
|
| 909 |
+
.unwrap_or("audio/mpeg")
|
| 910 |
+
.to_string();
|
| 911 |
+
let enc_bytes = match resp.bytes().await {
|
| 912 |
+
Ok(b) => b,
|
| 913 |
+
Err(e) => {
|
| 914 |
+
tracing::error!("download {} cdn read failed: {}", q.id, e);
|
| 915 |
+
return (StatusCode::BAD_GATEWAY, "CDN read failed").into_response();
|
| 916 |
+
}
|
| 917 |
+
};
|
| 918 |
+
|
| 919 |
+
// Decrypt in-memory (every 3rd 2048-byte block)
|
| 920 |
+
let key = blowfish_key(decrypt_id);
|
| 921 |
+
let mut dec = enc_bytes.to_vec();
|
| 922 |
+
let block_size = 2048usize;
|
| 923 |
+
let mut bi = 0usize;
|
| 924 |
+
let mut pos = 0usize;
|
| 925 |
+
while pos + block_size <= dec.len() {
|
| 926 |
+
if bi % 3 == 0 {
|
| 927 |
+
let _ = decrypt_stripe(&mut dec[pos..pos + block_size], &key);
|
| 928 |
+
}
|
| 929 |
+
bi += 1;
|
| 930 |
+
pos += block_size;
|
| 931 |
+
}
|
| 932 |
+
|
| 933 |
+
// Determine file extension
|
| 934 |
+
let ext = if used_fmt.contains("FLAC") || content_type.contains("flac") { "flac" } else { "mp3" };
|
| 935 |
+
let filename = format!("track_{}.{}", q.id, ext);
|
| 936 |
+
let ct = if ext == "flac" { "audio/flac" } else { "audio/mpeg" };
|
| 937 |
+
|
| 938 |
+
axum::response::Response::builder()
|
| 939 |
+
.status(200)
|
| 940 |
+
.header("Content-Type", ct)
|
| 941 |
+
.header("Content-Length", dec.len().to_string())
|
| 942 |
+
.header("Content-Disposition", format!("attachment; filename=\"{}\"", filename))
|
| 943 |
+
.header("Accept-Ranges", "none")
|
| 944 |
+
.header("Access-Control-Allow-Origin", "*")
|
| 945 |
+
.body(axum::body::Body::from(dec))
|
| 946 |
+
.unwrap()
|
| 947 |
+
.into_response()
|
| 948 |
+
}
|
| 949 |
+
|
| 950 |
async fn stream(
|
| 951 |
State(state): State<AppState>,
|
| 952 |
Query(q): Query<StreamParams>,
|
|
|
|
| 3490 |
.route("/get_url", post(get_url))
|
| 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))
|