recycleactor commited on
Commit
d35d103
·
verified ·
1 Parent(s): b7d9b1b

Upload main.rs

Browse files
Files changed (1) hide show
  1. src/main.rs +227 -206
src/main.rs CHANGED
@@ -24,12 +24,12 @@ use cbc::Decryptor;
24
  use reqwest::{cookie::Jar, Url};
25
  use reqwest::cookie::CookieStore;
26
  use reqwest::header::ACCEPT;
27
- use lofty::{
28
- config::WriteOptions,
29
- picture::{Picture, PictureType},
30
- tag::{Accessor, Tag, TagExt, TagType},
31
- };
32
- use std::io::Cursor;
33
  use std::time::{SystemTime, UNIX_EPOCH};
34
  use tokio::sync::{Mutex, RwLock};
35
  use tokio::time::{timeout, Duration};
@@ -53,8 +53,8 @@ struct CdnCacheEntry {
53
  expires_at: std::time::Instant,
54
  }
55
 
56
- #[derive(Clone)]
57
- struct AppState {
58
  api: Arc<Mutex<APIClient>>,
59
  api_by_arl: Arc<RwLock<HashMap<String, Arc<Mutex<APIClient>>>>>,
60
  arl_sessions: Arc<RwLock<HashMap<String, ArlSession>>>,
@@ -715,38 +715,38 @@ struct StreamParams {
715
  id: u64,
716
  format: Option<String>,
717
  arl: Option<String>,
718
- title: Option<String>,
719
- performer: Option<String>,
720
- duration: Option<u64>,
721
- album: Option<String>,
722
- cover_url: Option<String>,
723
- }
724
-
725
- #[derive(Debug, Clone, Default)]
726
- struct AudioTagMeta {
727
- title: Option<String>,
728
- performer: Option<String>,
729
- album: Option<String>,
730
- cover_url: Option<String>,
731
- }
732
-
733
- fn normalized_opt(value: &Option<String>) -> Option<String> {
734
- value
735
- .as_ref()
736
- .map(|s| s.trim().to_string())
737
- .filter(|s| !s.is_empty())
738
- }
739
-
740
- impl From<&StreamParams> for AudioTagMeta {
741
- fn from(params: &StreamParams) -> Self {
742
- Self {
743
- title: normalized_opt(&params.title),
744
- performer: normalized_opt(&params.performer),
745
- album: normalized_opt(&params.album),
746
- cover_url: normalized_opt(&params.cover_url),
747
- }
748
- }
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");
@@ -869,97 +869,97 @@ struct SendAudioParams {
869
  title: Option<String>,
870
  performer: Option<String>,
871
  duration: Option<u64>,
872
- album: Option<String>,
873
- cover_url: Option<String>,
874
- }
875
-
876
- impl From<&SendAudioParams> for AudioTagMeta {
877
- fn from(params: &SendAudioParams) -> Self {
878
- Self {
879
- title: normalized_opt(&params.title),
880
- performer: normalized_opt(&params.performer),
881
- album: normalized_opt(&params.album),
882
- cover_url: normalized_opt(&params.cover_url),
883
- }
884
- }
885
- }
886
-
887
- async fn fetch_cover_picture(cover_url: &str) -> Option<Picture> {
888
- let http = reqwest::Client::builder()
889
- .timeout(Duration::from_secs(20))
890
- .build()
891
- .ok()?;
892
- let resp = http.get(cover_url).send().await.ok()?;
893
- if !resp.status().is_success() {
894
- return None;
895
- }
896
- let bytes = resp.bytes().await.ok()?;
897
- let mut picture = Picture::from_reader(&mut Cursor::new(bytes.to_vec())).ok()?;
898
- picture.set_pic_type(PictureType::CoverFront);
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,
906
- None => None,
907
- };
908
-
909
- if !has_text && cover.is_none() {
910
- return bytes;
911
- }
912
-
913
- let tag_type = if ext.eq_ignore_ascii_case("flac") {
914
- TagType::VorbisComments
915
- } else {
916
- TagType::Id3v2
917
- };
918
-
919
- let mut tag = Tag::new(tag_type);
920
- if let Some(title) = meta.title.clone() {
921
- tag.set_title(title);
922
- }
923
- if let Some(performer) = meta.performer.clone() {
924
- tag.set_artist(performer);
925
- }
926
- if let Some(album) = meta.album.clone() {
927
- tag.set_album(album);
928
- }
929
- if let Some(picture) = cover {
930
- tag.push_picture(picture);
931
- }
932
-
933
- let unique = SystemTime::now()
934
- .duration_since(UNIX_EPOCH)
935
- .map(|d| d.as_nanos())
936
- .unwrap_or(0);
937
- let temp_path = std::env::temp_dir().join(format!("dzmedia_tagged_{}.{}", unique, ext));
938
- if let Err(e) = std::fs::write(&temp_path, &bytes) {
939
- tracing::warn!("metadata temp write failed: {}", e);
940
- return bytes;
941
- }
942
-
943
- let write_options = if ext.eq_ignore_ascii_case("mp3") {
944
- WriteOptions::new().use_id3v23(true)
945
- } else {
946
- WriteOptions::new()
947
- };
948
-
949
- let write_result = tag.save_to_path(&temp_path, write_options);
950
- if let Err(e) = write_result {
951
- let _ = std::fs::remove_file(&temp_path);
952
- tracing::warn!("metadata tagging failed: {}", e);
953
- return bytes;
954
- }
955
-
956
- match std::fs::read(&temp_path) {
957
- Ok(tagged) => bytes = tagged,
958
- Err(e) => tracing::warn!("metadata temp read failed: {}", e),
959
- }
960
- let _ = std::fs::remove_file(&temp_path);
961
- bytes
962
- }
963
 
964
  async fn send_audio(
965
  State(state): State<AppState>,
@@ -1066,81 +1066,81 @@ async fn send_audio(
1066
  bi += 1; pos += bs;
1067
  }
1068
 
1069
- let ext = if used_fmt.contains("FLAC") || content_type.contains("flac") { "flac" } else { "mp3" };
1070
- let meta = AudioTagMeta::from(&q);
1071
- dec = inject_audio_metadata(dec, ext, &meta).await;
1072
  let mime = if ext == "flac" { "audio/flac" } else { "audio/mpeg" };
1073
  let filename = format!("track_{}.{}", q.id, ext);
1074
 
1075
- // Загружаем в Telegram через отдельный клиент.
1076
- // Если аплоад виснет по сети, важно быстро вернуть ошибку боту,
1077
- // чтобы он раньше переключился на fallback по URL.
1078
  let tg_url = format!("https://api.telegram.org/bot{}/sendAudio", bot_token);
1079
- let tg_http = reqwest::Client::builder()
1080
- .https_only(true)
1081
- .connect_timeout(Duration::from_secs(8))
1082
- .timeout(Duration::from_secs(18))
1083
- .tcp_keepalive(Duration::from_secs(30))
1084
- .build()
1085
- .unwrap();
1086
-
1087
- let mut tg_json: Option<serde_json::Value> = None;
1088
- let mut last_tg_err: Option<String> = None;
1089
-
1090
- for attempt in 1..=2u8 {
1091
- let part = reqwest::multipart::Part::bytes(dec.clone())
1092
- .file_name(filename.clone())
1093
- .mime_str(mime)
1094
- .unwrap();
1095
- let mut form = reqwest::multipart::Form::new()
1096
- .text("chat_id", q.chat_id.clone())
1097
- .part("audio", part);
1098
- if let Some(ref t) = q.title { form = form.text("title", t.clone()); }
1099
- if let Some(ref p) = q.performer { form = form.text("performer", p.clone()); }
1100
- if let Some(d) = q.duration { form = form.text("duration", d.to_string()); }
1101
-
1102
- match tg_http.post(&tg_url).multipart(form).send().await {
1103
- Ok(resp) => {
1104
- let status = resp.status();
1105
- match resp.json::<serde_json::Value>().await {
1106
- Ok(j) => {
1107
- if status.is_success() && j.get("ok").and_then(|v| v.as_bool()) == Some(true) {
1108
- tg_json = Some(j);
1109
- break;
1110
- }
1111
- let desc = j
1112
- .get("description")
1113
- .and_then(|v| v.as_str())
1114
- .unwrap_or("unknown")
1115
- .to_string();
1116
- tracing::warn!("Telegram sendAudio attempt {} failed: status={} desc={}", attempt, status, desc);
1117
- last_tg_err = Some(format!("Telegram {}: {}", status, desc));
1118
- }
1119
- Err(e) => {
1120
- tracing::warn!("Telegram sendAudio attempt {} parse failed: {}", attempt, e);
1121
- last_tg_err = Some(format!("Telegram parse: {}", e));
1122
- }
1123
- }
1124
- }
1125
- Err(e) => {
1126
- tracing::warn!("Telegram sendAudio attempt {} upload failed: {}", attempt, e);
1127
- last_tg_err = Some(format!("Telegram upload: {}", e));
1128
- }
1129
- }
1130
-
1131
- if attempt < 2 {
1132
- tokio::time::sleep(Duration::from_secs(1)).await;
1133
- }
1134
- }
1135
-
1136
- let tg_json = match tg_json {
1137
- Some(j) => j,
1138
- None => {
1139
- let err = last_tg_err.unwrap_or_else(|| "Telegram upload failed".to_string());
1140
- tracing::error!("Telegram sendAudio final failure: {}", err);
1141
- return json_error(StatusCode::BAD_GATEWAY, err);
1142
- }
1143
- };
1144
 
1145
  let file_id = tg_json.pointer("/result/audio/file_id")
1146
  .and_then(|v| v.as_str())
@@ -1281,10 +1281,31 @@ async fn download(
1281
  }
1282
 
1283
  // Determine file extension
1284
- let ext = if used_fmt.contains("FLAC") || content_type.contains("flac") { "flac" } else { "mp3" };
1285
- let meta = AudioTagMeta::from(&q);
1286
- dec = inject_audio_metadata(dec, ext, &meta).await;
1287
- let filename = format!("track_{}.{}", q.id, ext);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1288
  let ct = if ext == "flac" { "audio/flac" } else { "audio/mpeg" };
1289
 
1290
  axum::response::Response::builder()
 
24
  use reqwest::{cookie::Jar, Url};
25
  use reqwest::cookie::CookieStore;
26
  use reqwest::header::ACCEPT;
27
+ use lofty::{
28
+ config::WriteOptions,
29
+ picture::{Picture, PictureType},
30
+ tag::{Accessor, Tag, TagExt, TagType},
31
+ };
32
+ use std::io::Cursor;
33
  use std::time::{SystemTime, UNIX_EPOCH};
34
  use tokio::sync::{Mutex, RwLock};
35
  use tokio::time::{timeout, Duration};
 
53
  expires_at: std::time::Instant,
54
  }
55
 
56
+ #[derive(Clone)]
57
+ struct AppState {
58
  api: Arc<Mutex<APIClient>>,
59
  api_by_arl: Arc<RwLock<HashMap<String, Arc<Mutex<APIClient>>>>>,
60
  arl_sessions: Arc<RwLock<HashMap<String, ArlSession>>>,
 
715
  id: u64,
716
  format: Option<String>,
717
  arl: Option<String>,
718
+ title: Option<String>,
719
+ performer: Option<String>,
720
+ duration: Option<u64>,
721
+ album: Option<String>,
722
+ cover_url: Option<String>,
723
+ }
724
+
725
+ #[derive(Debug, Clone, Default)]
726
+ struct AudioTagMeta {
727
+ title: Option<String>,
728
+ performer: Option<String>,
729
+ album: Option<String>,
730
+ cover_url: Option<String>,
731
+ }
732
+
733
+ fn normalized_opt(value: &Option<String>) -> Option<String> {
734
+ value
735
+ .as_ref()
736
+ .map(|s| s.trim().to_string())
737
+ .filter(|s| !s.is_empty())
738
+ }
739
+
740
+ impl From<&StreamParams> for AudioTagMeta {
741
+ fn from(params: &StreamParams) -> Self {
742
+ Self {
743
+ title: normalized_opt(&params.title),
744
+ performer: normalized_opt(&params.performer),
745
+ album: normalized_opt(&params.album),
746
+ cover_url: normalized_opt(&params.cover_url),
747
+ }
748
+ }
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");
 
869
  title: Option<String>,
870
  performer: Option<String>,
871
  duration: Option<u64>,
872
+ album: Option<String>,
873
+ cover_url: Option<String>,
874
+ }
875
+
876
+ impl From<&SendAudioParams> for AudioTagMeta {
877
+ fn from(params: &SendAudioParams) -> Self {
878
+ Self {
879
+ title: normalized_opt(&params.title),
880
+ performer: normalized_opt(&params.performer),
881
+ album: normalized_opt(&params.album),
882
+ cover_url: normalized_opt(&params.cover_url),
883
+ }
884
+ }
885
+ }
886
+
887
+ async fn fetch_cover_picture(cover_url: &str) -> Option<Picture> {
888
+ let http = reqwest::Client::builder()
889
+ .timeout(Duration::from_secs(20))
890
+ .build()
891
+ .ok()?;
892
+ let resp = http.get(cover_url).send().await.ok()?;
893
+ if !resp.status().is_success() {
894
+ return None;
895
+ }
896
+ let bytes = resp.bytes().await.ok()?;
897
+ let mut picture = Picture::from_reader(&mut Cursor::new(bytes.to_vec())).ok()?;
898
+ picture.set_pic_type(PictureType::CoverFront);
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,
906
+ None => None,
907
+ };
908
+
909
+ if !has_text && cover.is_none() {
910
+ return bytes;
911
+ }
912
+
913
+ let tag_type = if ext.eq_ignore_ascii_case("flac") {
914
+ TagType::VorbisComments
915
+ } else {
916
+ TagType::Id3v2
917
+ };
918
+
919
+ let mut tag = Tag::new(tag_type);
920
+ if let Some(title) = meta.title.clone() {
921
+ tag.set_title(title);
922
+ }
923
+ if let Some(performer) = meta.performer.clone() {
924
+ tag.set_artist(performer);
925
+ }
926
+ if let Some(album) = meta.album.clone() {
927
+ tag.set_album(album);
928
+ }
929
+ if let Some(picture) = cover {
930
+ tag.push_picture(picture);
931
+ }
932
+
933
+ let unique = SystemTime::now()
934
+ .duration_since(UNIX_EPOCH)
935
+ .map(|d| d.as_nanos())
936
+ .unwrap_or(0);
937
+ let temp_path = std::env::temp_dir().join(format!("dzmedia_tagged_{}.{}", unique, ext));
938
+ if let Err(e) = std::fs::write(&temp_path, &bytes) {
939
+ tracing::warn!("metadata temp write failed: {}", e);
940
+ return bytes;
941
+ }
942
+
943
+ let write_options = if ext.eq_ignore_ascii_case("mp3") {
944
+ WriteOptions::new().use_id3v23(true)
945
+ } else {
946
+ WriteOptions::new()
947
+ };
948
+
949
+ let write_result = tag.save_to_path(&temp_path, write_options);
950
+ if let Err(e) = write_result {
951
+ let _ = std::fs::remove_file(&temp_path);
952
+ tracing::warn!("metadata tagging failed: {}", e);
953
+ return bytes;
954
+ }
955
+
956
+ match std::fs::read(&temp_path) {
957
+ Ok(tagged) => bytes = tagged,
958
+ Err(e) => tracing::warn!("metadata temp read failed: {}", e),
959
+ }
960
+ let _ = std::fs::remove_file(&temp_path);
961
+ bytes
962
+ }
963
 
964
  async fn send_audio(
965
  State(state): State<AppState>,
 
1066
  bi += 1; pos += bs;
1067
  }
1068
 
1069
+ let ext = if used_fmt.contains("FLAC") || content_type.contains("flac") { "flac" } else { "mp3" };
1070
+ let meta = AudioTagMeta::from(&q);
1071
+ dec = inject_audio_metadata(dec, ext, &meta).await;
1072
  let mime = if ext == "flac" { "audio/flac" } else { "audio/mpeg" };
1073
  let filename = format!("track_{}.{}", q.id, ext);
1074
 
1075
+ // Загружаем в Telegram через отдельный клиент.
1076
+ // Если аплоад виснет по сети, важно быстро вернуть ошибку боту,
1077
+ // чтобы он раньше переключился на fallback по URL.
1078
  let tg_url = format!("https://api.telegram.org/bot{}/sendAudio", bot_token);
1079
+ let tg_http = reqwest::Client::builder()
1080
+ .https_only(true)
1081
+ .connect_timeout(Duration::from_secs(8))
1082
+ .timeout(Duration::from_secs(18))
1083
+ .tcp_keepalive(Duration::from_secs(30))
1084
+ .build()
1085
+ .unwrap();
1086
+
1087
+ let mut tg_json: Option<serde_json::Value> = None;
1088
+ let mut last_tg_err: Option<String> = None;
1089
+
1090
+ for attempt in 1..=2u8 {
1091
+ let part = reqwest::multipart::Part::bytes(dec.clone())
1092
+ .file_name(filename.clone())
1093
+ .mime_str(mime)
1094
+ .unwrap();
1095
+ let mut form = reqwest::multipart::Form::new()
1096
+ .text("chat_id", q.chat_id.clone())
1097
+ .part("audio", part);
1098
+ if let Some(ref t) = q.title { form = form.text("title", t.clone()); }
1099
+ if let Some(ref p) = q.performer { form = form.text("performer", p.clone()); }
1100
+ if let Some(d) = q.duration { form = form.text("duration", d.to_string()); }
1101
+
1102
+ match tg_http.post(&tg_url).multipart(form).send().await {
1103
+ Ok(resp) => {
1104
+ let status = resp.status();
1105
+ match resp.json::<serde_json::Value>().await {
1106
+ Ok(j) => {
1107
+ if status.is_success() && j.get("ok").and_then(|v| v.as_bool()) == Some(true) {
1108
+ tg_json = Some(j);
1109
+ break;
1110
+ }
1111
+ let desc = j
1112
+ .get("description")
1113
+ .and_then(|v| v.as_str())
1114
+ .unwrap_or("unknown")
1115
+ .to_string();
1116
+ tracing::warn!("Telegram sendAudio attempt {} failed: status={} desc={}", attempt, status, desc);
1117
+ last_tg_err = Some(format!("Telegram {}: {}", status, desc));
1118
+ }
1119
+ Err(e) => {
1120
+ tracing::warn!("Telegram sendAudio attempt {} parse failed: {}", attempt, e);
1121
+ last_tg_err = Some(format!("Telegram parse: {}", e));
1122
+ }
1123
+ }
1124
+ }
1125
+ Err(e) => {
1126
+ tracing::warn!("Telegram sendAudio attempt {} upload failed: {}", attempt, e);
1127
+ last_tg_err = Some(format!("Telegram upload: {}", e));
1128
+ }
1129
+ }
1130
+
1131
+ if attempt < 2 {
1132
+ tokio::time::sleep(Duration::from_secs(1)).await;
1133
+ }
1134
+ }
1135
+
1136
+ let tg_json = match tg_json {
1137
+ Some(j) => j,
1138
+ None => {
1139
+ let err = last_tg_err.unwrap_or_else(|| "Telegram upload failed".to_string());
1140
+ tracing::error!("Telegram sendAudio final failure: {}", err);
1141
+ return json_error(StatusCode::BAD_GATEWAY, err);
1142
+ }
1143
+ };
1144
 
1145
  let file_id = tg_json.pointer("/result/audio/file_id")
1146
  .and_then(|v| v.as_str())
 
1281
  }
1282
 
1283
  // Determine file extension
1284
+ let ext = if used_fmt.contains("FLAC") || content_type.contains("flac") { "flac" } else { "mp3" };
1285
+ let meta = AudioTagMeta::from(&q);
1286
+ dec = inject_audio_metadata(dec, ext, &meta).await;
1287
+
1288
+ // Generate filename from metadata
1289
+ let filename = if let Some(ref title) = meta.title {
1290
+ let safe_title = title
1291
+ .replace(['/', '\\', ':', '*', '?', '"', '<', '>', '|'], "_")
1292
+ .chars()
1293
+ .take(100)
1294
+ .collect::<String>();
1295
+ if let Some(ref performer) = meta.performer {
1296
+ let safe_performer = performer
1297
+ .replace(['/', '\\', ':', '*', '?', '"', '<', '>', '|'], "_")
1298
+ .chars()
1299
+ .take(50)
1300
+ .collect::<String>();
1301
+ format!("{} - {}.{}", safe_performer, safe_title, ext)
1302
+ } else {
1303
+ format!("{}.{}", safe_title, ext)
1304
+ }
1305
+ } else {
1306
+ format!("track_{}.{}", q.id, ext)
1307
+ };
1308
+
1309
  let ct = if ext == "flac" { "audio/flac" } else { "audio/mpeg" };
1310
 
1311
  axum::response::Response::builder()