Spaces:
Running
Running
Upload soundcloud.rs
Browse files- src/soundcloud.rs +68 -11
src/soundcloud.rs
CHANGED
|
@@ -134,10 +134,49 @@ pub async fn download(Query(params): Query<DownloadParams>) -> impl IntoResponse
|
|
| 134 |
let quality = params.quality.as_deref().unwrap_or("best");
|
| 135 |
|
| 136 |
// Формируем аргументы для yt-dlp
|
|
|
|
| 137 |
let format_arg = match quality {
|
| 138 |
-
"256" => "bestaudio[abr<=256]",
|
| 139 |
-
"128" => "bestaudio[abr<=128]",
|
| 140 |
-
_ => "bestaudio",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 141 |
};
|
| 142 |
|
| 143 |
// Скачиваем во временный файл БЕЗ конвертации - оригинальный формат
|
|
@@ -146,15 +185,19 @@ pub async fn download(Query(params): Query<DownloadParams>) -> impl IntoResponse
|
|
| 146 |
.unwrap()
|
| 147 |
.as_nanos());
|
| 148 |
|
|
|
|
|
|
|
|
|
|
| 149 |
let output = match Command::new("yt-dlp")
|
| 150 |
.args([
|
| 151 |
"--quiet",
|
| 152 |
"--no-warnings",
|
| 153 |
"-f", format_arg,
|
| 154 |
-
"-o", &
|
|
|
|
| 155 |
¶ms.url,
|
| 156 |
])
|
| 157 |
-
.stdout(Stdio::
|
| 158 |
.stderr(Stdio::piped())
|
| 159 |
.output()
|
| 160 |
.await
|
|
@@ -172,15 +215,22 @@ pub async fn download(Query(params): Query<DownloadParams>) -> impl IntoResponse
|
|
| 172 |
return (StatusCode::INTERNAL_SERVER_ERROR, "Download failed").into_response();
|
| 173 |
}
|
| 174 |
|
| 175 |
-
//
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 176 |
let actual_file = if tokio::fs::metadata(&temp_file).await.is_ok() {
|
| 177 |
temp_file.clone()
|
| 178 |
} else {
|
| 179 |
-
// Пробуем найти с расширением
|
| 180 |
let mut found = None;
|
| 181 |
-
for ext in &["m4a", "
|
| 182 |
let path = format!("{}.{}", temp_file, ext);
|
| 183 |
if tokio::fs::metadata(&path).await.is_ok() {
|
|
|
|
| 184 |
found = Some(path);
|
| 185 |
break;
|
| 186 |
}
|
|
@@ -188,7 +238,7 @@ pub async fn download(Query(params): Query<DownloadParams>) -> impl IntoResponse
|
|
| 188 |
match found {
|
| 189 |
Some(p) => p,
|
| 190 |
None => {
|
| 191 |
-
tracing::error!("Could not find downloaded file");
|
| 192 |
return (StatusCode::INTERNAL_SERVER_ERROR, "File not found").into_response();
|
| 193 |
}
|
| 194 |
}
|
|
@@ -293,14 +343,21 @@ pub async fn download(Query(params): Query<DownloadParams>) -> impl IntoResponse
|
|
| 293 |
|
| 294 |
tracing::info!("Sending SoundCloud file: {} ({} bytes, {})", filename_safe, final_data.len(), final_content_type);
|
| 295 |
|
| 296 |
-
axum::response::Response::builder()
|
| 297 |
.status(200)
|
| 298 |
.header("Content-Type", final_content_type)
|
| 299 |
.header("Content-Length", final_data.len().to_string())
|
| 300 |
.header("Content-Disposition", format!("inline; filename=\"{}\"", filename_safe))
|
| 301 |
.header("Accept-Ranges", "bytes")
|
| 302 |
.header("Access-Control-Allow-Origin", "*")
|
| 303 |
-
.header("Cache-Control", "public, max-age=31536000")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 304 |
.body(axum::body::Body::from(final_data))
|
| 305 |
.unwrap()
|
| 306 |
.into_response()
|
|
|
|
| 134 |
let quality = params.quality.as_deref().unwrap_or("best");
|
| 135 |
|
| 136 |
// Формируем аргументы для yt-dlp
|
| 137 |
+
// Приоритизируем M4A/AAC (256kbps для Go+), затем MP3
|
| 138 |
let format_arg = match quality {
|
| 139 |
+
"256" => "bestaudio[ext=m4a]/bestaudio[ext=aac]/bestaudio[abr<=256]/bestaudio",
|
| 140 |
+
"128" => "bestaudio[abr<=128]/bestaudio",
|
| 141 |
+
_ => "bestaudio[ext=m4a]/bestaudio[ext=aac]/bestaudio", // Приоритет M4A/AAC
|
| 142 |
+
};
|
| 143 |
+
|
| 144 |
+
tracing::info!("Using format selector: {}", format_arg);
|
| 145 |
+
|
| 146 |
+
// Сначала получаем информацию о доступных форматах для отладки
|
| 147 |
+
if let Ok(info_output) = Command::new("yt-dlp")
|
| 148 |
+
.args([
|
| 149 |
+
"--quiet",
|
| 150 |
+
"-F", // Показать все доступные форматы
|
| 151 |
+
¶ms.url,
|
| 152 |
+
])
|
| 153 |
+
.output()
|
| 154 |
+
.await
|
| 155 |
+
{
|
| 156 |
+
let formats_info = String::from_utf8_lossy(&info_output.stdout);
|
| 157 |
+
tracing::info!("Available formats:\n{}", formats_info);
|
| 158 |
+
}
|
| 159 |
+
|
| 160 |
+
// Получаем URL thumbnail'а
|
| 161 |
+
let thumbnail_url = if let Ok(thumb_output) = Command::new("yt-dlp")
|
| 162 |
+
.args([
|
| 163 |
+
"--quiet",
|
| 164 |
+
"--no-warnings",
|
| 165 |
+
"--print", "%(thumbnail)s",
|
| 166 |
+
¶ms.url,
|
| 167 |
+
])
|
| 168 |
+
.output()
|
| 169 |
+
.await
|
| 170 |
+
{
|
| 171 |
+
let thumb = String::from_utf8_lossy(&thumb_output.stdout).trim().to_string();
|
| 172 |
+
if !thumb.is_empty() && thumb.starts_with("http") {
|
| 173 |
+
tracing::info!("Found thumbnail: {}", thumb);
|
| 174 |
+
Some(thumb)
|
| 175 |
+
} else {
|
| 176 |
+
None
|
| 177 |
+
}
|
| 178 |
+
} else {
|
| 179 |
+
None
|
| 180 |
};
|
| 181 |
|
| 182 |
// Скачиваем во временный файл БЕЗ конвертации - оригинальный формат
|
|
|
|
| 185 |
.unwrap()
|
| 186 |
.as_nanos());
|
| 187 |
|
| 188 |
+
// Добавляем расширение .%(ext)s чтобы yt-dlp сам определил правильное расширение
|
| 189 |
+
let output_template = format!("{}.%(ext)s", temp_file);
|
| 190 |
+
|
| 191 |
let output = match Command::new("yt-dlp")
|
| 192 |
.args([
|
| 193 |
"--quiet",
|
| 194 |
"--no-warnings",
|
| 195 |
"-f", format_arg,
|
| 196 |
+
"-o", &output_template,
|
| 197 |
+
"--no-post-overwrites", // Не перезаписываем после обработки
|
| 198 |
¶ms.url,
|
| 199 |
])
|
| 200 |
+
.stdout(Stdio::piped())
|
| 201 |
.stderr(Stdio::piped())
|
| 202 |
.output()
|
| 203 |
.await
|
|
|
|
| 215 |
return (StatusCode::INTERNAL_SERVER_ERROR, "Download failed").into_response();
|
| 216 |
}
|
| 217 |
|
| 218 |
+
// Логируем stdout чтобы увидеть что скачал yt-dlp
|
| 219 |
+
let stdout = String::from_utf8_lossy(&output.stdout);
|
| 220 |
+
if !stdout.is_empty() {
|
| 221 |
+
tracing::info!("yt-dlp stdout: {}", stdout);
|
| 222 |
+
}
|
| 223 |
+
|
| 224 |
+
// Находим скачанный файл (yt-dlp добавил расширение через .%(ext)s)
|
| 225 |
let actual_file = if tokio::fs::metadata(&temp_file).await.is_ok() {
|
| 226 |
temp_file.clone()
|
| 227 |
} else {
|
| 228 |
+
// Пробуем найти с расширением - ВАЖНО: сначала ищем M4A/AAC, потом остальные
|
| 229 |
let mut found = None;
|
| 230 |
+
for ext in &["m4a", "aac", "opus", "webm", "mp3"] {
|
| 231 |
let path = format!("{}.{}", temp_file, ext);
|
| 232 |
if tokio::fs::metadata(&path).await.is_ok() {
|
| 233 |
+
tracing::info!("Found downloaded file with extension: {}", ext);
|
| 234 |
found = Some(path);
|
| 235 |
break;
|
| 236 |
}
|
|
|
|
| 238 |
match found {
|
| 239 |
Some(p) => p,
|
| 240 |
None => {
|
| 241 |
+
tracing::error!("Could not find downloaded file with any extension");
|
| 242 |
return (StatusCode::INTERNAL_SERVER_ERROR, "File not found").into_response();
|
| 243 |
}
|
| 244 |
}
|
|
|
|
| 343 |
|
| 344 |
tracing::info!("Sending SoundCloud file: {} ({} bytes, {})", filename_safe, final_data.len(), final_content_type);
|
| 345 |
|
| 346 |
+
let mut response = axum::response::Response::builder()
|
| 347 |
.status(200)
|
| 348 |
.header("Content-Type", final_content_type)
|
| 349 |
.header("Content-Length", final_data.len().to_string())
|
| 350 |
.header("Content-Disposition", format!("inline; filename=\"{}\"", filename_safe))
|
| 351 |
.header("Accept-Ranges", "bytes")
|
| 352 |
.header("Access-Control-Allow-Origin", "*")
|
| 353 |
+
.header("Cache-Control", "public, max-age=31536000");
|
| 354 |
+
|
| 355 |
+
// Добавляем URL thumbnail'а в заголовок если есть
|
| 356 |
+
if let Some(thumb_url) = thumbnail_url {
|
| 357 |
+
response = response.header("X-Thumbnail-Url", thumb_url);
|
| 358 |
+
}
|
| 359 |
+
|
| 360 |
+
response
|
| 361 |
.body(axum::body::Body::from(final_data))
|
| 362 |
.unwrap()
|
| 363 |
.into_response()
|