Prepare Suno WAV URLs through studio API

This commit is contained in:
DESKTOP-KSVGT20\shkim
2026-05-01 18:03:52 +09:00
parent a7a54d3532
commit cec4ae3d0c
3 changed files with 120 additions and 23 deletions
+95 -18
View File
@@ -1,7 +1,7 @@
// ==UserScript==
// @name ChatGPT to Suno Prompt Copier
// @namespace local.suno-helper
// @version 0.4.2
// @version 0.4.3
// @description Copy structured ChatGPT song prompt versions into Suno fields.
// @author local
// @match https://chatgpt.com/*
@@ -12,6 +12,7 @@
// @downloadURL https://git.mokaya.org/shkim/suno-helper/raw/branch/master/chatgpt-suno-tampermonkey.user.js
// @connect cdn*.suno.ai
// @connect *.suno.com
// @connect studio-api-prod.suno.com
// @run-at document-start
// @grant GM_addStyle
// @grant GM_getValue
@@ -42,7 +43,7 @@
function rememberSunoResponse(url, data) {
if (!isSuno || !data) return;
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({
url: String(url || location.href),
data,
@@ -529,6 +530,15 @@
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) {
return [...new Set(values.filter(Boolean))];
}
@@ -581,7 +591,7 @@
if (!object || typeof object !== "object" || Array.isArray(object)) return false;
const id = firstValue(object, ["id", "clip_id", "clipId", "audio_id", "audioId"]);
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));
}
@@ -589,8 +599,10 @@
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 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 wavUrl = firstValue(object, ["wav_url", "wavUrl"]) || (id ? `https://cdn1.suno.ai/${id}.wav` : "");
const audioPath = firstValue(object, ["audio_url", "audioUrl", "stream_audio_url", "streamAudioUrl", "play_url", "playUrl", "play_path", "playPath", "mp3_url", "mp3Url", "video_url", "videoUrl"]);
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 style = firstValue(object, ["gpt_description_prompt", "style", "tags", "description"]);
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) {
onerror?.("URL 없음");
return;
@@ -796,10 +808,60 @@
url,
name,
saveAs: false,
onload: () => onload?.(),
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) {
const blob = new Blob([text], { type: "application/json;charset=utf-8" });
const url = URL.createObjectURL(blob);
@@ -812,7 +874,7 @@
setTimeout(() => URL.revokeObjectURL(url), 1000);
}
function downloadSunoPackage(track, statusTarget) {
async function downloadSunoPackage(track, statusTarget) {
const baseName = safeFilename(track.title || track.id);
const messages = [];
const fail = (label) => (reason) => {
@@ -827,8 +889,16 @@
downloadUrl(track.imageUrl, `${baseName}.cover.${extensionFromUrl(track.imageUrl, "jpg")}`, fail("cover"));
messages.push("cover 요청");
}
if (track.wavUrl) {
downloadUrl(track.wavUrl, `${baseName}.wav`, fail("wav"));
const preparedWavUrl = await requestSunoWavUrl(track);
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 요청");
} else if (track.audioUrl) {
downloadUrl(track.audioUrl, `${baseName}.${extensionFromUrl(track.audioUrl, "audio")}`, fail("audio"));
@@ -855,25 +925,32 @@
for (let index = 0; index < tracks.length; index += 1) {
const track = tracks[index];
setStatus(statusTarget, `${index + 1}/${tracks.length} 요청 중: ${track.title || track.id}`);
downloadSunoPackage(track, statusTarget);
await downloadSunoPackage(track, statusTarget);
await new Promise((resolve) => setTimeout(resolve, 650));
}
setStatus(statusTarget, `${tracks.length}곡 다운로드 요청 완료. 실패한 파일은 브라우저 다운로드 목록/metadata에서 확인해줘.`);
}
function exportSunoManifest(tracks, statusTarget) {
async function exportSunoManifest(tracks, statusTarget) {
if (!tracks.length) {
setStatus(statusTarget, "내보낼 곡을 못 찾았어. 라이브러리 곡 감지를 먼저 해줘.");
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 = {
pageUrl: location.href,
capturedAt: new Date().toISOString(),
count: tracks.length,
tracks,
count: preparedTracks.length,
tracks: preparedTracks,
};
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() {
@@ -952,13 +1029,13 @@
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();
if (!track.id && !track.audioUrl && !track.imageUrl) {
setStatus(body, "다운로드할 곡 정보를 못 찾았어. 곡 상세/플레이어 화면에서 다시 눌러봐.");
return;
}
downloadSunoPackage(track, body);
await downloadSunoPackage(track, body);
});
body.querySelector("[data-cgs-scan-library]").addEventListener("click", () => {
@@ -981,10 +1058,10 @@
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();
updateSummary();
exportSunoManifest(libraryTracks, body);
await exportSunoManifest(libraryTracks, body);
});
}