Spaces:
Running
Running
Upload main.rs
Browse files- src/main.rs +133 -2
src/main.rs
CHANGED
|
@@ -24,6 +24,12 @@ use cbc::Decryptor;
|
|
| 24 |
use reqwest::{cookie::Jar, Url};
|
| 25 |
use reqwest::cookie::CookieStore;
|
| 26 |
use reqwest::header::ACCEPT;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
use std::time::{SystemTime, UNIX_EPOCH};
|
| 28 |
use tokio::sync::{Mutex, RwLock};
|
| 29 |
use tokio::time::{timeout, Duration};
|
|
@@ -709,7 +715,38 @@ struct StreamParams {
|
|
| 709 |
id: u64,
|
| 710 |
format: Option<String>,
|
| 711 |
arl: Option<String>,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 712 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 713 |
|
| 714 |
async fn stream_info(State(state): State<AppState>, Query(q): Query<StreamParams>) -> impl IntoResponse {
|
| 715 |
let _ = std::fs::remove_file("debug_dzmedia.log");
|
|
@@ -832,7 +869,97 @@ struct SendAudioParams {
|
|
| 832 |
title: Option<String>,
|
| 833 |
performer: Option<String>,
|
| 834 |
duration: Option<u64>,
|
|
|
|
|
|
|
| 835 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 836 |
|
| 837 |
async fn send_audio(
|
| 838 |
State(state): State<AppState>,
|
|
@@ -939,7 +1066,9 @@ async fn send_audio(
|
|
| 939 |
bi += 1; pos += bs;
|
| 940 |
}
|
| 941 |
|
| 942 |
-
let ext = if used_fmt.contains("FLAC") || content_type.contains("flac") { "flac" } else { "mp3" };
|
|
|
|
|
|
|
| 943 |
let mime = if ext == "flac" { "audio/flac" } else { "audio/mpeg" };
|
| 944 |
let filename = format!("track_{}.{}", q.id, ext);
|
| 945 |
|
|
@@ -1153,7 +1282,9 @@ async fn download(
|
|
| 1153 |
}
|
| 1154 |
|
| 1155 |
// Determine file extension
|
| 1156 |
-
let ext = if used_fmt.contains("FLAC") || content_type.contains("flac") { "flac" } else { "mp3" };
|
|
|
|
|
|
|
| 1157 |
let filename = format!("track_{}.{}", q.id, ext);
|
| 1158 |
let ct = if ext == "flac" { "audio/flac" } else { "audio/mpeg" };
|
| 1159 |
|
|
|
|
| 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};
|
|
|
|
| 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(¶ms.title),
|
| 744 |
+
performer: normalized_opt(¶ms.performer),
|
| 745 |
+
album: normalized_opt(¶ms.album),
|
| 746 |
+
cover_url: normalized_opt(¶ms.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(¶ms.title),
|
| 880 |
+
performer: normalized_opt(¶ms.performer),
|
| 881 |
+
album: normalized_opt(¶ms.album),
|
| 882 |
+
cover_url: normalized_opt(¶ms.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 |
|
|
|
|
| 1282 |
}
|
| 1283 |
|
| 1284 |
// Determine file extension
|
| 1285 |
+
let ext = if used_fmt.contains("FLAC") || content_type.contains("flac") { "flac" } else { "mp3" };
|
| 1286 |
+
let meta = AudioTagMeta::from(&q);
|
| 1287 |
+
dec = inject_audio_metadata(dec, ext, &meta).await;
|
| 1288 |
let filename = format!("track_{}.{}", q.id, ext);
|
| 1289 |
let ct = if ext == "flac" { "audio/flac" } else { "audio/mpeg" };
|
| 1290 |
|