Batch upload Suno MP3s with metadata

This commit is contained in:
DESKTOP-KSVGT20\shkim
2026-05-02 00:55:16 +09:00
parent 9049136574
commit cc9399c37f
3 changed files with 148 additions and 31 deletions
+104 -12
View File
@@ -1,7 +1,7 @@
// ==UserScript==
// @name ChatGPT to Suno Prompt Copier
// @namespace local.suno-helper
// @version 0.8.0
// @version 0.8.1
// @description Copy structured ChatGPT song prompt versions into Suno fields.
// @author local
// @match https://chatgpt.com/*
@@ -28,7 +28,7 @@
(function () {
"use strict";
const SCRIPT_VERSION = "0.8.0";
const SCRIPT_VERSION = "0.8.1";
const STORAGE_KEY = "chatgptSunoPromptSlots";
const SLOT_COUNT = 2;
const isChatGPT = location.hostname.includes("chatgpt.com") || location.hostname.includes("chat.openai.com");
@@ -1095,6 +1095,52 @@
throw new Error("브라우저도 WAV 다운로드를 못 받았어");
}
function getMp3Candidates(track) {
const candidates = [];
if (track.audioUrl && !/\.wav(?:\?|$)/i.test(track.audioUrl)) candidates.push(track.audioUrl);
const metadata = track.metadata || {};
for (const value of [
metadata.audio_url,
metadata.audioUrl,
metadata.stream_audio_url,
metadata.streamAudioUrl,
metadata.play_url,
metadata.playUrl,
metadata.play_path,
metadata.playPath,
metadata.mp3_url,
metadata.mp3Url,
]) {
const normalized = normalizeSunoMediaUrl(value);
if (normalized && !/\.wav(?:\?|$)/i.test(normalized)) candidates.push(normalized);
}
if (track.id) {
candidates.push(`https://cdn1.suno.ai/${encodeURIComponent(track.id)}.mp3`);
candidates.push(`https://cdn2.suno.ai/${encodeURIComponent(track.id)}.mp3`);
}
return unique(candidates);
}
async function downloadSunoMp3Buffer(track, statusTarget) {
const candidates = getMp3Candidates(track);
if (!candidates.length) throw new Error("MP3 후보 URL 없음");
for (const url of candidates) {
try {
setStatus(statusTarget, `브라우저에서 MP3 받는 중: ${track.title || track.id}`);
const response = await callTampermonkeyBytes(url, { method: "GET" });
const size = response.buffer?.byteLength || 0;
if (response.ok && size > 128 * 1024) {
track.audioUrl = url;
return response.buffer;
}
console.warn("[ChatGPT to Suno] MP3 bytes not ready", url, response.status, size);
} catch (error) {
console.warn("[ChatGPT to Suno] MP3 download failed", url, error);
}
}
throw new Error("브라우저도 MP3 다운로드를 못 받았어");
}
function downloadTextFile(text, name) {
const blob = new Blob([text], { type: "application/json;charset=utf-8" });
const url = URL.createObjectURL(blob);
@@ -1247,14 +1293,17 @@
setStatus(statusTarget, `큐 전송 완료: 새 곡 ${enqueued}개, 보류 ${wavPending}개. 로컬 프로그램이 WAV 준비를 기다리며 처리 중.`);
}
async function uploadTrackToCompanion(track, statusTarget) {
const buffer = await downloadSunoWavBuffer(track, statusTarget);
setStatus(statusTarget, `로컬 프로그램으로 WAV 업로드 중: ${track.title || track.id}`);
async function uploadTrackToCompanion(track, statusTarget, format = "mp3") {
const buffer = format === "wav"
? await downloadSunoWavBuffer(track, statusTarget)
: await downloadSunoMp3Buffer(track, statusTarget);
setStatus(statusTarget, `로컬 프로그램으로 ${format.toUpperCase()} 업로드 중: ${track.title || track.id}`);
const payload = {
pageUrl: location.href,
capturedAt: new Date().toISOString(),
track,
wavBase64: arrayBufferToBase64(buffer),
audioExt: format,
audioBase64: arrayBufferToBase64(buffer),
};
const response = await fetch("http://127.0.0.1:17873/upload-track", {
method: "POST",
@@ -1268,6 +1317,50 @@
return tryParseJson(text) || {};
}
async function uploadTracksToCompanion(tracks, statusTarget, format = "mp3") {
if (!tracks.length) {
setStatus(statusTarget, "보낼 곡을 못 찾았어. 곡 감지를 먼저 해줘.");
return;
}
const syncManifest = {
pageUrl: location.href,
capturedAt: new Date().toISOString(),
count: tracks.length,
tracks,
};
setStatus(statusTarget, `${tracks.length}곡 로컬 폴더와 비교 중...`);
const syncResponse = await fetch("http://127.0.0.1:17873/sync-index", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(syncManifest),
});
const syncText = await syncResponse.text();
if (!syncResponse.ok) throw new Error(syncText || `${syncResponse.status} ${syncResponse.statusText}`);
const syncData = tryParseJson(syncText) || {};
const missingTracks = Array.isArray(syncData.tracks) ? syncData.tracks : [];
if (!missingTracks.length) {
setStatus(statusTarget, `동기화 완료 상태: ${tracks.length}곡 모두 로컬에 있음`);
return;
}
let uploaded = 0;
let failed = 0;
for (let index = 0; index < missingTracks.length; index += 1) {
const track = { ...missingTracks[index] };
try {
setStatus(statusTarget, `${index + 1}/${missingTracks.length} ${format.toUpperCase()} 처리 중: ${track.title || track.id}`);
await uploadTrackToCompanion(track, statusTarget, format);
uploaded += 1;
} catch (error) {
failed += 1;
console.warn("[ChatGPT to Suno] upload failed", track, error);
setStatus(statusTarget, `${track.title || track.id} 실패: ${error.message || error}`);
}
await new Promise((resolve) => setTimeout(resolve, 400));
}
setStatus(statusTarget, `${format.toUpperCase()} 일괄 업로드 완료: ${uploaded}곡 성공, ${failed}곡 실패. 로컬에서 메타데이터 후처리 중.`);
}
function trackOptionLabel(track, index) {
const title = track?.title || track?.id || "제목 없음";
const shortId = track?.id ? String(track.id).slice(0, 8) : "";
@@ -1325,7 +1418,7 @@
<option value="">감지된 곡 없음</option>
</select>
<div class="cgs-row">
<button class="cgs-btn cgs-wide" type="button" data-cgs-process-one>선택 1곡만 다운</button>
<button class="cgs-btn cgs-wide" type="button" data-cgs-process-one>선택 1곡 MP3</button>
</div>
<div class="cgs-row">
<button class="cgs-btn cgs-wide" type="button" data-cgs-test-companion>연결 테스트</button>
@@ -1334,7 +1427,7 @@
<button class="cgs-btn cgs-wide" type="button" data-cgs-copy-diagnostics>진단 복사</button>
</div>
<div class="cgs-row">
<button class="cgs-btn cgs-primary cgs-wide" type="button" data-cgs-process-library>WAV + 메타데이터 다운</button>
<button class="cgs-btn cgs-primary cgs-wide" type="button" data-cgs-process-library>MP3 일괄 + 메타데이터</button>
</div>
<div class="cgs-status"></div>
`;
@@ -1416,7 +1509,7 @@
return;
}
try {
const data = await uploadTrackToCompanion(track, body);
const data = await uploadTrackToCompanion(track, body, "mp3");
setStatus(body, `선택 1곡 업로드 완료: ${track.title || track.id} / 대기 ${data.queued ?? "?"}개 / 처리 ${data.active || "대기"}`);
} catch (error) {
setStatus(body, `선택 1곡 처리 실패: ${error.message || error}`);
@@ -1430,10 +1523,9 @@
}
updateSummary();
try {
await postToCompanion(libraryTracks, body);
await uploadTracksToCompanion(libraryTracks, body, "mp3");
} catch (error) {
setStatus(body, `로컬 프로그램 연결 실패. manifest로 저장할게: ${error.message || error}`);
await exportSunoManifest(libraryTracks, body);
setStatus(body, `MP3 일괄 처리 실패: ${error.message || error}`);
}
});