Prepare Suno WAV URLs through studio API
This commit is contained in:
@@ -49,7 +49,7 @@ WAV 다운로드는 SUNO 계정 권한, 변환 준비 상태, CDN 접근 정책
|
|||||||
브라우저는 `\\VaultOfData\AKAMedia\음악\처리됨\SUNO` 같은 로컬/NAS 경로에 직접 파일을 정리하거나 WAV를 FLAC으로 인코딩할 수 없습니다. 그래서 Tampermonkey는 manifest를 만들고, 로컬 PowerShell 스크립트가 실제 다운로드/변환/정리를 담당합니다.
|
브라우저는 `\\VaultOfData\AKAMedia\음악\처리됨\SUNO` 같은 로컬/NAS 경로에 직접 파일을 정리하거나 WAV를 FLAC으로 인코딩할 수 없습니다. 그래서 Tampermonkey는 manifest를 만들고, 로컬 PowerShell 스크립트가 실제 다운로드/변환/정리를 담당합니다.
|
||||||
|
|
||||||
1. SUNO 라이브러리에서 `라이브러리 곡 감지`를 누릅니다.
|
1. SUNO 라이브러리에서 `라이브러리 곡 감지`를 누릅니다.
|
||||||
2. `처리용 manifest 저장`을 눌러 Downloads 폴더에 `suno-library-YYYY-MM-DD.manifest.json`을 저장합니다.
|
2. `처리용 manifest 저장`을 눌러 Downloads 폴더에 `suno-library-YYYY-MM-DD.manifest.json`을 저장합니다. 이 단계에서 브라우저가 SUNO의 `convert_wav/`와 `wav_file/` API를 호출해 WAV URL을 준비합니다.
|
||||||
3. `run_process_suno_library.ps1`을 실행합니다.
|
3. `run_process_suno_library.ps1`을 실행합니다.
|
||||||
|
|
||||||
기본 출력 위치:
|
기본 출력 위치:
|
||||||
@@ -58,4 +58,4 @@ WAV 다운로드는 SUNO 계정 권한, 변환 준비 상태, CDN 접근 정책
|
|||||||
\\VaultOfData\AKAMedia\음악\처리됨\SUNO\YYYY-MM-DD
|
\\VaultOfData\AKAMedia\음악\처리됨\SUNO\YYYY-MM-DD
|
||||||
```
|
```
|
||||||
|
|
||||||
로컬 처리기는 각 곡의 WAV 후보를 다운로드하고, 실패하면 manifest의 audio URL로 fallback합니다. 이후 ffmpeg로 FLAC 변환하면서 title, artist, album, genre/style, lyrics, Suno clip id, cover art를 메타데이터로 넣습니다.
|
로컬 처리기는 manifest에 저장된 WAV URL을 다운로드하고, 실패하면 manifest의 audio URL로 fallback합니다. 이후 ffmpeg로 FLAC 변환하면서 title, artist, album, genre/style, lyrics, Suno clip id, cover art를 메타데이터로 넣습니다.
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// ==UserScript==
|
// ==UserScript==
|
||||||
// @name ChatGPT to Suno Prompt Copier
|
// @name ChatGPT to Suno Prompt Copier
|
||||||
// @namespace local.suno-helper
|
// @namespace local.suno-helper
|
||||||
// @version 0.4.2
|
// @version 0.4.3
|
||||||
// @description Copy structured ChatGPT song prompt versions into Suno fields.
|
// @description Copy structured ChatGPT song prompt versions into Suno fields.
|
||||||
// @author local
|
// @author local
|
||||||
// @match https://chatgpt.com/*
|
// @match https://chatgpt.com/*
|
||||||
@@ -12,6 +12,7 @@
|
|||||||
// @downloadURL https://git.mokaya.org/shkim/suno-helper/raw/branch/master/chatgpt-suno-tampermonkey.user.js
|
// @downloadURL https://git.mokaya.org/shkim/suno-helper/raw/branch/master/chatgpt-suno-tampermonkey.user.js
|
||||||
// @connect cdn*.suno.ai
|
// @connect cdn*.suno.ai
|
||||||
// @connect *.suno.com
|
// @connect *.suno.com
|
||||||
|
// @connect studio-api-prod.suno.com
|
||||||
// @run-at document-start
|
// @run-at document-start
|
||||||
// @grant GM_addStyle
|
// @grant GM_addStyle
|
||||||
// @grant GM_getValue
|
// @grant GM_getValue
|
||||||
@@ -42,7 +43,7 @@
|
|||||||
function rememberSunoResponse(url, data) {
|
function rememberSunoResponse(url, data) {
|
||||||
if (!isSuno || !data) return;
|
if (!isSuno || !data) return;
|
||||||
const text = typeof data === "string" ? data : JSON.stringify(data);
|
const text = typeof data === "string" ? data : JSON.stringify(data);
|
||||||
if (!/clip|audio_url|image_url|song|playlist|metadata|cdn[0-9]*\.suno\.ai/i.test(text)) return;
|
if (!/clip|audio|image_url|song|playlist|metadata|cdn[0-9]*\.suno\.ai|download/i.test(text)) return;
|
||||||
capturedSunoResponses.push({
|
capturedSunoResponses.push({
|
||||||
url: String(url || location.href),
|
url: String(url || location.href),
|
||||||
data,
|
data,
|
||||||
@@ -529,6 +530,15 @@
|
|||||||
return (match?.[1] || fallback).toLowerCase();
|
return (match?.[1] || fallback).toLowerCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeSunoMediaUrl(value) {
|
||||||
|
const raw = String(value || "").trim();
|
||||||
|
if (!raw) return "";
|
||||||
|
if (/^https?:\/\//i.test(raw)) return raw.replace(/\\u002F/g, "/");
|
||||||
|
if (raw.startsWith("//")) return `https:${raw}`;
|
||||||
|
if (raw.startsWith("/")) return `https://suno.com${raw}`;
|
||||||
|
return raw;
|
||||||
|
}
|
||||||
|
|
||||||
function unique(values) {
|
function unique(values) {
|
||||||
return [...new Set(values.filter(Boolean))];
|
return [...new Set(values.filter(Boolean))];
|
||||||
}
|
}
|
||||||
@@ -581,7 +591,7 @@
|
|||||||
if (!object || typeof object !== "object" || Array.isArray(object)) return false;
|
if (!object || typeof object !== "object" || Array.isArray(object)) return false;
|
||||||
const id = firstValue(object, ["id", "clip_id", "clipId", "audio_id", "audioId"]);
|
const id = firstValue(object, ["id", "clip_id", "clipId", "audio_id", "audioId"]);
|
||||||
const hasTrackText = firstValue(object, ["title", "name", "prompt", "gpt_description_prompt", "metadata"]);
|
const hasTrackText = firstValue(object, ["title", "name", "prompt", "gpt_description_prompt", "metadata"]);
|
||||||
const hasMedia = firstValue(object, ["audio_url", "audioUrl", "image_url", "imageUrl", "image_large_url", "wav_url", "wavUrl"]);
|
const hasMedia = firstValue(object, ["audio_url", "audioUrl", "stream_audio_url", "streamAudioUrl", "play_path", "playPath", "image_url", "imageUrl", "image_large_url", "wav_url", "wavUrl"]);
|
||||||
return Boolean(id && (hasTrackText || hasMedia));
|
return Boolean(id && (hasTrackText || hasMedia));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -589,8 +599,10 @@
|
|||||||
const id = String(firstValue(object, ["id", "clip_id", "clipId", "audio_id", "audioId"]) || "").trim();
|
const id = String(firstValue(object, ["id", "clip_id", "clipId", "audio_id", "audioId"]) || "").trim();
|
||||||
const title = String(firstValue(object, ["title", "name"]) || id || "suno-track").trim();
|
const title = String(firstValue(object, ["title", "name"]) || id || "suno-track").trim();
|
||||||
const imageUrl = firstValue(object, ["image_url", "imageUrl", "image_large_url", "imageLargeUrl", "cover_url", "coverUrl"]);
|
const imageUrl = firstValue(object, ["image_url", "imageUrl", "image_large_url", "imageLargeUrl", "cover_url", "coverUrl"]);
|
||||||
const audioUrl = firstValue(object, ["audio_url", "audioUrl", "play_url", "playUrl", "mp3_url", "mp3Url"]);
|
const audioPath = firstValue(object, ["audio_url", "audioUrl", "stream_audio_url", "streamAudioUrl", "play_url", "playUrl", "play_path", "playPath", "mp3_url", "mp3Url", "video_url", "videoUrl"]);
|
||||||
const wavUrl = firstValue(object, ["wav_url", "wavUrl"]) || (id ? `https://cdn1.suno.ai/${id}.wav` : "");
|
const audioUrl = normalizeSunoMediaUrl(audioPath);
|
||||||
|
const wavPath = firstValue(object, ["wav_url", "wavUrl", "download_url", "downloadUrl"]);
|
||||||
|
const wavUrl = normalizeSunoMediaUrl(wavPath) || (id ? `https://cdn1.suno.ai/${id}.wav` : "");
|
||||||
const lyrics = firstValue(object, ["prompt", "lyrics", "lyric", "text"]);
|
const lyrics = firstValue(object, ["prompt", "lyrics", "lyric", "text"]);
|
||||||
const style = firstValue(object, ["gpt_description_prompt", "style", "tags", "description"]);
|
const style = firstValue(object, ["gpt_description_prompt", "style", "tags", "description"]);
|
||||||
const model = firstValue(object, ["model_name", "model", "major_model_version"]);
|
const model = firstValue(object, ["model_name", "model", "major_model_version"]);
|
||||||
@@ -787,7 +799,7 @@
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function downloadUrl(url, name, onerror) {
|
function downloadUrl(url, name, onerror, onload) {
|
||||||
if (!url) {
|
if (!url) {
|
||||||
onerror?.("URL 없음");
|
onerror?.("URL 없음");
|
||||||
return;
|
return;
|
||||||
@@ -796,10 +808,60 @@
|
|||||||
url,
|
url,
|
||||||
name,
|
name,
|
||||||
saveAs: false,
|
saveAs: false,
|
||||||
|
onload: () => onload?.(),
|
||||||
onerror: (error) => onerror?.(error?.error || String(error)),
|
onerror: (error) => onerror?.(error?.error || String(error)),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function callSunoJson(url, options = {}) {
|
||||||
|
const page = typeof unsafeWindow !== "undefined" ? unsafeWindow : window;
|
||||||
|
const response = await page.fetch(url, {
|
||||||
|
credentials: "include",
|
||||||
|
...options,
|
||||||
|
headers: {
|
||||||
|
"accept": "application/json, text/plain, */*",
|
||||||
|
...(options.method && options.method !== "GET" ? { "content-type": "application/json" } : {}),
|
||||||
|
...(options.headers || {}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const text = await response.text();
|
||||||
|
const data = tryParseJson(text) || text;
|
||||||
|
rememberSunoResponse(url, data);
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`${response.status} ${response.statusText}: ${String(text).slice(0, 160)}`);
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function requestSunoWavUrl(track) {
|
||||||
|
if (!track.id) return track.wavUrl || "";
|
||||||
|
const base = `https://studio-api-prod.suno.com/api/gen/${encodeURIComponent(track.id)}`;
|
||||||
|
try {
|
||||||
|
await callSunoJson(`${base}/convert_wav/`, { method: "POST" });
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("[ChatGPT to Suno] convert_wav failed", error);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let attempt = 0; attempt < 8; attempt += 1) {
|
||||||
|
try {
|
||||||
|
const data = await callSunoJson(`${base}/wav_file/`, { method: "GET" });
|
||||||
|
const direct =
|
||||||
|
data?.url ||
|
||||||
|
data?.download_url ||
|
||||||
|
data?.wav_url ||
|
||||||
|
data?.audio_url ||
|
||||||
|
data?.file ||
|
||||||
|
(typeof data === "string" && /^https?:\/\//i.test(data) ? data : "");
|
||||||
|
if (direct) return normalizeSunoMediaUrl(direct);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("[ChatGPT to Suno] wav_file failed", error);
|
||||||
|
}
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 1300));
|
||||||
|
}
|
||||||
|
|
||||||
|
return track.wavUrl || `https://cdn1.suno.ai/${track.id}.wav`;
|
||||||
|
}
|
||||||
|
|
||||||
function downloadTextFile(text, name) {
|
function downloadTextFile(text, name) {
|
||||||
const blob = new Blob([text], { type: "application/json;charset=utf-8" });
|
const blob = new Blob([text], { type: "application/json;charset=utf-8" });
|
||||||
const url = URL.createObjectURL(blob);
|
const url = URL.createObjectURL(blob);
|
||||||
@@ -812,7 +874,7 @@
|
|||||||
setTimeout(() => URL.revokeObjectURL(url), 1000);
|
setTimeout(() => URL.revokeObjectURL(url), 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
function downloadSunoPackage(track, statusTarget) {
|
async function downloadSunoPackage(track, statusTarget) {
|
||||||
const baseName = safeFilename(track.title || track.id);
|
const baseName = safeFilename(track.title || track.id);
|
||||||
const messages = [];
|
const messages = [];
|
||||||
const fail = (label) => (reason) => {
|
const fail = (label) => (reason) => {
|
||||||
@@ -827,8 +889,16 @@
|
|||||||
downloadUrl(track.imageUrl, `${baseName}.cover.${extensionFromUrl(track.imageUrl, "jpg")}`, fail("cover"));
|
downloadUrl(track.imageUrl, `${baseName}.cover.${extensionFromUrl(track.imageUrl, "jpg")}`, fail("cover"));
|
||||||
messages.push("cover 요청");
|
messages.push("cover 요청");
|
||||||
}
|
}
|
||||||
if (track.wavUrl) {
|
const preparedWavUrl = await requestSunoWavUrl(track);
|
||||||
downloadUrl(track.wavUrl, `${baseName}.wav`, fail("wav"));
|
if (preparedWavUrl) {
|
||||||
|
downloadUrl(preparedWavUrl, `${baseName}.wav`, (reason) => {
|
||||||
|
fail("wav")(reason);
|
||||||
|
if (track.audioUrl && track.audioUrl !== preparedWavUrl) {
|
||||||
|
downloadUrl(track.audioUrl, `${baseName}.${extensionFromUrl(track.audioUrl, "audio")}`, fail("audio-fallback"));
|
||||||
|
messages.push("audio fallback 요청");
|
||||||
|
setStatus(statusTarget, messages.join(" / "));
|
||||||
|
}
|
||||||
|
});
|
||||||
messages.push("wav 요청");
|
messages.push("wav 요청");
|
||||||
} else if (track.audioUrl) {
|
} else if (track.audioUrl) {
|
||||||
downloadUrl(track.audioUrl, `${baseName}.${extensionFromUrl(track.audioUrl, "audio")}`, fail("audio"));
|
downloadUrl(track.audioUrl, `${baseName}.${extensionFromUrl(track.audioUrl, "audio")}`, fail("audio"));
|
||||||
@@ -855,25 +925,32 @@
|
|||||||
for (let index = 0; index < tracks.length; index += 1) {
|
for (let index = 0; index < tracks.length; index += 1) {
|
||||||
const track = tracks[index];
|
const track = tracks[index];
|
||||||
setStatus(statusTarget, `${index + 1}/${tracks.length} 요청 중: ${track.title || track.id}`);
|
setStatus(statusTarget, `${index + 1}/${tracks.length} 요청 중: ${track.title || track.id}`);
|
||||||
downloadSunoPackage(track, statusTarget);
|
await downloadSunoPackage(track, statusTarget);
|
||||||
await new Promise((resolve) => setTimeout(resolve, 650));
|
await new Promise((resolve) => setTimeout(resolve, 650));
|
||||||
}
|
}
|
||||||
setStatus(statusTarget, `${tracks.length}곡 다운로드 요청 완료. 실패한 파일은 브라우저 다운로드 목록/metadata에서 확인해줘.`);
|
setStatus(statusTarget, `${tracks.length}곡 다운로드 요청 완료. 실패한 파일은 브라우저 다운로드 목록/metadata에서 확인해줘.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function exportSunoManifest(tracks, statusTarget) {
|
async function exportSunoManifest(tracks, statusTarget) {
|
||||||
if (!tracks.length) {
|
if (!tracks.length) {
|
||||||
setStatus(statusTarget, "내보낼 곡을 못 찾았어. 라이브러리 곡 감지를 먼저 해줘.");
|
setStatus(statusTarget, "내보낼 곡을 못 찾았어. 라이브러리 곡 감지를 먼저 해줘.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const preparedTracks = [];
|
||||||
|
for (let index = 0; index < tracks.length; index += 1) {
|
||||||
|
const track = { ...tracks[index] };
|
||||||
|
setStatus(statusTarget, `${index + 1}/${tracks.length} WAV 준비 중: ${track.title || track.id}`);
|
||||||
|
track.wavUrl = await requestSunoWavUrl(track);
|
||||||
|
preparedTracks.push(track);
|
||||||
|
}
|
||||||
const manifest = {
|
const manifest = {
|
||||||
pageUrl: location.href,
|
pageUrl: location.href,
|
||||||
capturedAt: new Date().toISOString(),
|
capturedAt: new Date().toISOString(),
|
||||||
count: tracks.length,
|
count: preparedTracks.length,
|
||||||
tracks,
|
tracks: preparedTracks,
|
||||||
};
|
};
|
||||||
downloadTextFile(JSON.stringify(manifest, null, 2), `suno-library-${new Date().toISOString().slice(0, 10)}.manifest.json`);
|
downloadTextFile(JSON.stringify(manifest, null, 2), `suno-library-${new Date().toISOString().slice(0, 10)}.manifest.json`);
|
||||||
setStatus(statusTarget, `${tracks.length}곡 manifest 저장. 이제 run_process_suno_library.ps1 실행하면 정리/FLAC 변환돼.`);
|
setStatus(statusTarget, `${preparedTracks.length}곡 manifest 저장. 이제 run_process_suno_library.ps1 실행하면 정리/FLAC 변환돼.`);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function bootSuno() {
|
async function bootSuno() {
|
||||||
@@ -952,13 +1029,13 @@
|
|||||||
setStatus(body, track.id || track.audioUrl ? `감지됨: ${track.title || track.id}` : "현재 화면에서 곡 URL을 못 찾았어.");
|
setStatus(body, track.id || track.audioUrl ? `감지됨: ${track.title || track.id}` : "현재 화면에서 곡 URL을 못 찾았어.");
|
||||||
});
|
});
|
||||||
|
|
||||||
body.querySelector("[data-cgs-download-track]").addEventListener("click", () => {
|
body.querySelector("[data-cgs-download-track]").addEventListener("click", async () => {
|
||||||
const track = findCurrentSunoTrack();
|
const track = findCurrentSunoTrack();
|
||||||
if (!track.id && !track.audioUrl && !track.imageUrl) {
|
if (!track.id && !track.audioUrl && !track.imageUrl) {
|
||||||
setStatus(body, "다운로드할 곡 정보를 못 찾았어. 곡 상세/플레이어 화면에서 다시 눌러봐.");
|
setStatus(body, "다운로드할 곡 정보를 못 찾았어. 곡 상세/플레이어 화면에서 다시 눌러봐.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
downloadSunoPackage(track, body);
|
await downloadSunoPackage(track, body);
|
||||||
});
|
});
|
||||||
|
|
||||||
body.querySelector("[data-cgs-scan-library]").addEventListener("click", () => {
|
body.querySelector("[data-cgs-scan-library]").addEventListener("click", () => {
|
||||||
@@ -981,10 +1058,10 @@
|
|||||||
await downloadSunoBatch(libraryTracks, body);
|
await downloadSunoBatch(libraryTracks, body);
|
||||||
});
|
});
|
||||||
|
|
||||||
body.querySelector("[data-cgs-export-library]").addEventListener("click", () => {
|
body.querySelector("[data-cgs-export-library]").addEventListener("click", async () => {
|
||||||
if (!libraryTracks.length) libraryTracks = scanSunoLibraryTracks();
|
if (!libraryTracks.length) libraryTracks = scanSunoLibraryTracks();
|
||||||
updateSummary();
|
updateSummary();
|
||||||
exportSunoManifest(libraryTracks, body);
|
await exportSunoManifest(libraryTracks, body);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -112,6 +112,17 @@ function Get-UrlExtension {
|
|||||||
return $Fallback
|
return $Fallback
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function Resolve-SunoMediaUrl {
|
||||||
|
param([string]$Value)
|
||||||
|
|
||||||
|
if (-not $Value) { return "" }
|
||||||
|
$raw = $Value.Trim()
|
||||||
|
if ($raw -match '^https?://') { return $raw }
|
||||||
|
if ($raw.StartsWith("//")) { return "https:$raw" }
|
||||||
|
if ($raw.StartsWith("/")) { return "https://suno.com$raw" }
|
||||||
|
return $raw
|
||||||
|
}
|
||||||
|
|
||||||
function Invoke-Download {
|
function Invoke-Download {
|
||||||
param(
|
param(
|
||||||
[string]$Url,
|
[string]$Url,
|
||||||
@@ -180,8 +191,12 @@ foreach ($track in $tracks) {
|
|||||||
$model = Get-DeepValue $track @("model_name", "model", "major_model_version")
|
$model = Get-DeepValue $track @("model_name", "model", "major_model_version")
|
||||||
$createdAt = Get-DeepValue $track @("created_at", "createdAt")
|
$createdAt = Get-DeepValue $track @("created_at", "createdAt")
|
||||||
|
|
||||||
$wavUrl = if ($track.wavUrl) { "$($track.wavUrl)" } elseif ($id) { "https://cdn1.suno.ai/$id.wav" } else { "" }
|
$audioUrl = Resolve-SunoMediaUrl $(if ($track.audioUrl) { "$($track.audioUrl)" } else { Get-DeepValue $track @("audio_url", "audioUrl", "stream_audio_url", "streamAudioUrl", "play_url", "playUrl", "play_path", "playPath", "mp3_url", "mp3Url", "video_url", "videoUrl") })
|
||||||
$audioUrl = if ($track.audioUrl) { "$($track.audioUrl)" } else { "" }
|
$wavUrl = Resolve-SunoMediaUrl $(if ($track.wavUrl) { "$($track.wavUrl)" } else { Get-DeepValue $track @("wav_url", "wavUrl", "download_url", "downloadUrl") })
|
||||||
|
$wavCandidates = @()
|
||||||
|
if ($wavUrl) { $wavCandidates += $wavUrl }
|
||||||
|
if ($id) { $wavCandidates += "https://cdn1.suno.ai/$id.wav" }
|
||||||
|
$wavCandidates = $wavCandidates | Where-Object { $_ } | Select-Object -Unique
|
||||||
$imageUrl = if ($track.imageUrl) { "$($track.imageUrl)" } else { Get-DeepValue $track @("image_url", "imageUrl", "image_large_url", "cover_url") }
|
$imageUrl = if ($track.imageUrl) { "$($track.imageUrl)" } else { Get-DeepValue $track @("image_url", "imageUrl", "image_large_url", "cover_url") }
|
||||||
|
|
||||||
$inputAudio = Join-Path $workRoot "$prefix.input.wav"
|
$inputAudio = Join-Path $workRoot "$prefix.input.wav"
|
||||||
@@ -193,7 +208,12 @@ foreach ($track in $tracks) {
|
|||||||
Write-Host ""
|
Write-Host ""
|
||||||
Write-Host "[$index/$($tracks.Count)] $title"
|
Write-Host "[$index/$($tracks.Count)] $title"
|
||||||
|
|
||||||
$audioOk = Invoke-Download $wavUrl $inputAudio
|
$audioOk = $false
|
||||||
|
foreach ($candidate in $wavCandidates) {
|
||||||
|
Write-Host "WAV 후보 시도: $candidate"
|
||||||
|
$audioOk = Invoke-Download $candidate $inputAudio
|
||||||
|
if ($audioOk) { break }
|
||||||
|
}
|
||||||
if (-not $audioOk -and $audioUrl) {
|
if (-not $audioOk -and $audioUrl) {
|
||||||
Write-Host "WAV 후보 실패. audioUrl로 fallback 시도."
|
Write-Host "WAV 후보 실패. audioUrl로 fallback 시도."
|
||||||
$audioOk = Invoke-Download $audioUrl $fallbackAudio
|
$audioOk = Invoke-Download $audioUrl $fallbackAudio
|
||||||
|
|||||||
Reference in New Issue
Block a user