Add prompt slots and Suno download tools
This commit is contained in:
@@ -24,9 +24,19 @@ https://git.mokaya.org/shkim/suno-helper/raw/branch/master/chatgpt-suno-tampermo
|
|||||||
|
|
||||||
## 사용법
|
## 사용법
|
||||||
|
|
||||||
1. ChatGPT 답변 페이지에서 오른쪽 아래 `ChatGPT -> Suno` 패널을 확인합니다.
|
1. ChatGPT 답변 페이지에서 `Version A/B/C` 형식이 감지되면 오른쪽 위에 작은 `ChatGPT -> Suno` 패널이 나타납니다.
|
||||||
2. 원하는 버전의 `저장` 버튼을 누릅니다.
|
2. 원하는 버전의 `S1` 또는 `S2` 버튼을 눌러 슬롯에 저장합니다.
|
||||||
3. SUNO Create 페이지로 이동합니다.
|
3. SUNO Create 페이지로 이동합니다.
|
||||||
4. 오른쪽 아래 `Suno Prompt Paste` 패널에서 `저장값 붙여넣기`를 누릅니다.
|
4. 오른쪽 아래 `Suno Prompt Paste` 패널에서 슬롯을 고르고 `선택 슬롯 붙여넣기`를 누릅니다.
|
||||||
|
|
||||||
SUNO 화면 구조가 바뀌어 자동 입력이 실패하면, 패널의 `Title`, `Style of Music`, `Lyrics`, `Exclude` 버튼으로 개별 값을 클립보드에 복사해서 수동으로 붙여넣을 수 있습니다.
|
SUNO 화면 구조가 바뀌어 자동 입력이 실패하면, 패널의 `Title`, `Style of Music`, `Lyrics`, `Exclude` 버튼으로 개별 값을 클립보드에 복사해서 수동으로 붙여넣을 수 있습니다.
|
||||||
|
|
||||||
|
## SUNO 다운로드
|
||||||
|
|
||||||
|
SUNO 곡 상세/플레이어 화면에서 `WAV + 커버 + 메타 다운로드`를 누르면 현재 페이지에서 감지된 곡 정보를 기준으로 아래 파일을 다운로드합니다.
|
||||||
|
|
||||||
|
- `{title}.wav`: clip id가 감지되면 `https://cdn1.suno.ai/{clipId}.wav` 후보를 내려받습니다.
|
||||||
|
- `{title}.cover.*`: 페이지에서 감지된 앨범아트 URL을 내려받습니다.
|
||||||
|
- `{title}.metadata.json`: title, clip id, audio/image URL, 감지된 원본 메타데이터, 현재 페이지 URL을 저장합니다.
|
||||||
|
|
||||||
|
WAV 다운로드는 SUNO 계정 권한, 변환 준비 상태, CDN 접근 정책에 따라 실패할 수 있습니다. 이 경우 metadata JSON의 `detectedAudioUrls`, `detectedImageUrls`, `detectedIds`를 보고 실제 페이지에서 어떤 URL이 잡혔는지 확인할 수 있습니다.
|
||||||
|
|||||||
@@ -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.1.3
|
// @version 0.2.0
|
||||||
// @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/*
|
||||||
@@ -13,6 +13,7 @@
|
|||||||
// @run-at document-idle
|
// @run-at document-idle
|
||||||
// @grant GM_addStyle
|
// @grant GM_addStyle
|
||||||
// @grant GM_getValue
|
// @grant GM_getValue
|
||||||
|
// @grant GM_download
|
||||||
// @grant GM_setClipboard
|
// @grant GM_setClipboard
|
||||||
// @grant GM_setValue
|
// @grant GM_setValue
|
||||||
// ==/UserScript==
|
// ==/UserScript==
|
||||||
@@ -20,7 +21,8 @@
|
|||||||
(function () {
|
(function () {
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
const STORAGE_KEY = "chatgptSunoPromptPayload";
|
const STORAGE_KEY = "chatgptSunoPromptSlots";
|
||||||
|
const SLOT_COUNT = 2;
|
||||||
const isChatGPT = location.hostname.includes("chatgpt.com") || location.hostname.includes("chat.openai.com");
|
const isChatGPT = location.hostname.includes("chatgpt.com") || location.hostname.includes("chat.openai.com");
|
||||||
const isSuno = location.hostname === "suno.com" || location.hostname.endsWith(".suno.com");
|
const isSuno = location.hostname === "suno.com" || location.hostname.endsWith(".suno.com");
|
||||||
let chatRenderTimer = 0;
|
let chatRenderTimer = 0;
|
||||||
@@ -57,23 +59,6 @@
|
|||||||
#cgs-panel.cgs-error {
|
#cgs-panel.cgs-error {
|
||||||
border-color: rgba(255, 84, 84, 0.7);
|
border-color: rgba(255, 84, 84, 0.7);
|
||||||
}
|
}
|
||||||
#cgs-launcher {
|
|
||||||
position: fixed;
|
|
||||||
right: 18px;
|
|
||||||
top: 18px;
|
|
||||||
z-index: 2147483647;
|
|
||||||
min-width: 76px;
|
|
||||||
min-height: 34px;
|
|
||||||
padding: 7px 10px;
|
|
||||||
color: #ffffff;
|
|
||||||
background: #ef4444;
|
|
||||||
border: 2px solid #ffffff;
|
|
||||||
border-radius: 999px;
|
|
||||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.35);
|
|
||||||
cursor: pointer;
|
|
||||||
font: 700 13px/1 system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
||||||
}
|
|
||||||
#cgs-launcher:hover { background: #dc2626; }
|
|
||||||
#cgs-panel * { box-sizing: border-box; }
|
#cgs-panel * { box-sizing: border-box; }
|
||||||
#cgs-panel.cgs-minimized .cgs-body { display: none; }
|
#cgs-panel.cgs-minimized .cgs-body { display: none; }
|
||||||
.cgs-head {
|
.cgs-head {
|
||||||
@@ -127,9 +112,21 @@
|
|||||||
gap: 6px;
|
gap: 6px;
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
}
|
}
|
||||||
|
.cgs-select {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 30px;
|
||||||
|
margin-top: 8px;
|
||||||
|
color: #f5f7fb;
|
||||||
|
background: rgba(255, 255, 255, 0.09);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.16);
|
||||||
|
border-radius: 7px;
|
||||||
|
padding: 4px 7px;
|
||||||
|
font: inherit;
|
||||||
|
}
|
||||||
`);
|
`);
|
||||||
|
|
||||||
function createPanel(title, mode = "") {
|
function createPanel(title, mode = "") {
|
||||||
|
document.querySelector("#cgs-launcher")?.remove();
|
||||||
const old = document.querySelector("#cgs-panel");
|
const old = document.querySelector("#cgs-panel");
|
||||||
if (old) old.remove();
|
if (old) old.remove();
|
||||||
|
|
||||||
@@ -150,18 +147,6 @@
|
|||||||
return panel.querySelector(".cgs-body");
|
return panel.querySelector(".cgs-body");
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensureLauncher(label, onClick) {
|
|
||||||
let launcher = document.querySelector("#cgs-launcher");
|
|
||||||
if (!launcher) {
|
|
||||||
launcher = document.createElement("button");
|
|
||||||
launcher.id = "cgs-launcher";
|
|
||||||
launcher.type = "button";
|
|
||||||
document.body.appendChild(launcher);
|
|
||||||
}
|
|
||||||
launcher.textContent = label;
|
|
||||||
launcher.onclick = onClick;
|
|
||||||
}
|
|
||||||
|
|
||||||
function createErrorPanel(error) {
|
function createErrorPanel(error) {
|
||||||
if (!document.body) return;
|
if (!document.body) return;
|
||||||
const body = createPanel("Suno Helper Error", "cgs-error");
|
const body = createPanel("Suno Helper Error", "cgs-error");
|
||||||
@@ -260,24 +245,46 @@
|
|||||||
.join("\n\n");
|
.join("\n\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
async function savePayload(payload) {
|
function emptySlots() {
|
||||||
await GM_setValue(STORAGE_KEY, JSON.stringify(payload));
|
return Array.from({ length: SLOT_COUNT }, () => null);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function readSlots() {
|
||||||
|
const raw = await GM_getValue(STORAGE_KEY, "");
|
||||||
|
if (!raw) return emptySlots();
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(raw);
|
||||||
|
if (Array.isArray(parsed)) return emptySlots().map((_, index) => parsed[index] || null);
|
||||||
|
if (parsed && typeof parsed === "object") return [parsed, ...emptySlots()].slice(0, SLOT_COUNT);
|
||||||
|
} catch {
|
||||||
|
return emptySlots();
|
||||||
|
}
|
||||||
|
return emptySlots();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveSlots(slots) {
|
||||||
|
await GM_setValue(STORAGE_KEY, JSON.stringify(emptySlots().map((_, index) => slots[index] || null)));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function savePayload(payload, slotIndex) {
|
||||||
|
const slots = await readSlots();
|
||||||
|
slots[slotIndex] = { ...payload, savedAt: new Date().toISOString() };
|
||||||
|
await saveSlots(slots);
|
||||||
GM_setClipboard(formatPayload(payload), "text");
|
GM_setClipboard(formatPayload(payload), "text");
|
||||||
}
|
}
|
||||||
|
|
||||||
async function readPayload() {
|
async function readPayload(slotIndex = 0) {
|
||||||
const raw = await GM_getValue(STORAGE_KEY, "");
|
const slots = await readSlots();
|
||||||
if (!raw) return null;
|
return slots[slotIndex] || null;
|
||||||
try {
|
|
||||||
return JSON.parse(raw);
|
|
||||||
} catch {
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function slotLabel(payload, index) {
|
||||||
|
if (!payload) return `Slot ${index + 1} - 비어있음`;
|
||||||
|
return `Slot ${index + 1} - ${payload.title || payload.version || "저장됨"}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function bootChatGPT() {
|
function bootChatGPT() {
|
||||||
clearInterval(chatRenderTimer);
|
clearInterval(chatRenderTimer);
|
||||||
ensureLauncher("Suno", () => bootChatGPT());
|
|
||||||
const body = createPanel("ChatGPT -> Suno", "cgs-chatgpt");
|
const body = createPanel("ChatGPT -> Suno", "cgs-chatgpt");
|
||||||
body.innerHTML = `
|
body.innerHTML = `
|
||||||
<div class="cgs-muted">현재 답변에서 Version A/B/C를 찾아서 저장해.</div>
|
<div class="cgs-muted">현재 답변에서 Version A/B/C를 찾아서 저장해.</div>
|
||||||
@@ -288,29 +295,35 @@
|
|||||||
<div class="cgs-status"></div>
|
<div class="cgs-status"></div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const render = () => {
|
const render = async () => {
|
||||||
const list = body.querySelector("[data-cgs-list]");
|
const list = body.querySelector("[data-cgs-list]");
|
||||||
const versions = parseVersions(getConversationText());
|
const versions = parseVersions(getConversationText());
|
||||||
list.innerHTML = "";
|
list.innerHTML = "";
|
||||||
|
|
||||||
if (!versions.length) {
|
if (!versions.length) {
|
||||||
list.innerHTML = `<div class="cgs-status">아직 Version 형식을 못 찾았어.</div>`;
|
document.querySelector("#cgs-panel")?.remove();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const slots = await readSlots();
|
||||||
for (const payload of versions) {
|
for (const payload of versions) {
|
||||||
const row = document.createElement("div");
|
const row = document.createElement("div");
|
||||||
row.className = "cgs-row";
|
row.className = "cgs-row";
|
||||||
row.innerHTML = `
|
row.innerHTML = `
|
||||||
<div class="cgs-version" title="${escapeHtml(payload.version)}">${escapeHtml(payload.version)}</div>
|
<div class="cgs-version" title="${escapeHtml(payload.version)}">${escapeHtml(payload.version)}</div>
|
||||||
<button class="cgs-btn cgs-primary" type="button">저장</button>
|
<button class="cgs-btn cgs-primary" type="button" data-slot="0">S1</button>
|
||||||
|
<button class="cgs-btn cgs-primary" type="button" data-slot="1">S2</button>
|
||||||
<button class="cgs-btn" type="button">복사</button>
|
<button class="cgs-btn" type="button">복사</button>
|
||||||
`;
|
`;
|
||||||
const [saveButton, copyButton] = row.querySelectorAll("button");
|
const copyButton = row.querySelector("button:not([data-slot])");
|
||||||
|
for (const saveButton of row.querySelectorAll("[data-slot]")) {
|
||||||
|
saveButton.title = slotLabel(slots[Number(saveButton.dataset.slot)], Number(saveButton.dataset.slot));
|
||||||
saveButton.addEventListener("click", async () => {
|
saveButton.addEventListener("click", async () => {
|
||||||
await savePayload(payload);
|
const slotIndex = Number(saveButton.dataset.slot);
|
||||||
setStatus(body, `${payload.version} 저장하고 클립보드에도 복사했어.`);
|
await savePayload(payload, slotIndex);
|
||||||
|
setStatus(body, `${payload.version} -> Slot ${slotIndex + 1} 저장하고 복사했어.`);
|
||||||
});
|
});
|
||||||
|
}
|
||||||
copyButton.addEventListener("click", () => {
|
copyButton.addEventListener("click", () => {
|
||||||
GM_setClipboard(formatPayload(payload), "text");
|
GM_setClipboard(formatPayload(payload), "text");
|
||||||
setStatus(body, `${payload.version} 전체 텍스트를 복사했어.`);
|
setStatus(body, `${payload.version} 전체 텍스트를 복사했어.`);
|
||||||
@@ -320,7 +333,7 @@
|
|||||||
setStatus(body, `${versions.length}개 버전을 찾았어.`);
|
setStatus(body, `${versions.length}개 버전을 찾았어.`);
|
||||||
};
|
};
|
||||||
|
|
||||||
body.querySelector("[data-cgs-rescan]").addEventListener("click", render);
|
body.querySelector("[data-cgs-rescan]").addEventListener("click", () => render());
|
||||||
render();
|
render();
|
||||||
chatRenderTimer = setInterval(() => {
|
chatRenderTimer = setInterval(() => {
|
||||||
if (document.body.contains(body)) render();
|
if (document.body.contains(body)) render();
|
||||||
@@ -435,30 +448,192 @@
|
|||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function safeFilename(value, fallback = "suno-track") {
|
||||||
|
return String(value || fallback)
|
||||||
|
.replace(/[\\/:*?"<>|]+/g, " ")
|
||||||
|
.replace(/\s+/g, " ")
|
||||||
|
.trim()
|
||||||
|
.slice(0, 120) || fallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
function extensionFromUrl(url, fallback) {
|
||||||
|
const clean = String(url || "").split("?")[0].split("#")[0];
|
||||||
|
const match = /\.([a-z0-9]{2,5})$/i.exec(clean);
|
||||||
|
return (match?.[1] || fallback).toLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
function unique(values) {
|
||||||
|
return [...new Set(values.filter(Boolean))];
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractUrls(text, pattern) {
|
||||||
|
return unique([...String(text || "").matchAll(pattern)].map((match) => match[0].replace(/\\u002F/g, "/")));
|
||||||
|
}
|
||||||
|
|
||||||
|
function readJsonScripts() {
|
||||||
|
const results = [];
|
||||||
|
for (const script of document.querySelectorAll("script")) {
|
||||||
|
const text = script.textContent || "";
|
||||||
|
if (!text.includes("suno") && !text.includes("audio") && !text.includes("image")) continue;
|
||||||
|
try {
|
||||||
|
results.push(JSON.parse(text));
|
||||||
|
} catch {
|
||||||
|
results.push(text);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
function flattenObjects(value, output = []) {
|
||||||
|
if (!value || typeof value !== "object") return output;
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
for (const item of value) flattenObjects(item, output);
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
output.push(value);
|
||||||
|
for (const item of Object.values(value)) flattenObjects(item, output);
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
function findCurrentSunoTrack() {
|
||||||
|
const pageText = document.body.innerText || "";
|
||||||
|
const htmlText = document.documentElement.innerHTML || "";
|
||||||
|
const scripts = readJsonScripts();
|
||||||
|
const objects = scripts.flatMap((item) => flattenObjects(item));
|
||||||
|
const audioElements = [...document.querySelectorAll("audio, video")].map((node) => node.currentSrc || node.src);
|
||||||
|
const allText = [htmlText, ...scripts.map((item) => typeof item === "string" ? item : JSON.stringify(item))].join("\n");
|
||||||
|
const uuidPattern = /[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}/gi;
|
||||||
|
const ids = unique([...allText.matchAll(uuidPattern)].map((match) => match[0]));
|
||||||
|
const cdnAudioUrls = extractUrls(allText, /https:\/\/cdn[0-9]*\.suno\.ai\/[^"' <>)]+?\.(?:mp3|wav|m4a|oga|ogg)(?:\?[^"' <>)]+)?/gi);
|
||||||
|
const cdnImageUrls = extractUrls(allText, /https:\/\/cdn[0-9]*\.suno\.ai\/[^"' <>)]+?\.(?:png|jpg|jpeg|webp)(?:\?[^"' <>)]+)?/gi);
|
||||||
|
const audioUrls = unique([...audioElements, ...cdnAudioUrls]);
|
||||||
|
const imageUrls = unique([
|
||||||
|
...cdnImageUrls,
|
||||||
|
...[...document.images].map((img) => img.currentSrc || img.src).filter((url) => /suno|cdn/i.test(url)),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const metadataObjects = objects.filter((item) => {
|
||||||
|
const text = JSON.stringify(item);
|
||||||
|
return /audio_url|image_url|clip|prompt|metadata|title|gpt_description_prompt/i.test(text)
|
||||||
|
&& (ids.some((id) => text.includes(id)) || /cdn[0-9]*\.suno\.ai/i.test(text));
|
||||||
|
});
|
||||||
|
const metadata = metadataObjects[0] || {};
|
||||||
|
const title =
|
||||||
|
metadata.title ||
|
||||||
|
metadata.name ||
|
||||||
|
document.querySelector("h1")?.innerText ||
|
||||||
|
document.querySelector("[data-testid*='title' i]")?.innerText ||
|
||||||
|
document.title.replace(/\s*\|\s*Suno.*$/i, "");
|
||||||
|
const id = metadata.id || metadata.clip_id || metadata.clipId || ids[0] || "";
|
||||||
|
const imageUrl = metadata.image_url || metadata.imageUrl || metadata.image_large_url || imageUrls[0] || "";
|
||||||
|
const audioUrl = metadata.audio_url || metadata.audioUrl || audioUrls.find((url) => /\.wav(?:\?|$)/i.test(url)) || audioUrls[0] || "";
|
||||||
|
const wavUrl = metadata.wav_url || metadata.wavUrl || audioUrls.find((url) => /\.wav(?:\?|$)/i.test(url)) || (id ? `https://cdn1.suno.ai/${id}.wav` : "");
|
||||||
|
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
title: String(title || "suno-track").trim(),
|
||||||
|
audioUrl,
|
||||||
|
wavUrl,
|
||||||
|
imageUrl,
|
||||||
|
pageUrl: location.href,
|
||||||
|
detectedIds: ids,
|
||||||
|
detectedAudioUrls: audioUrls,
|
||||||
|
detectedImageUrls: imageUrls,
|
||||||
|
metadata,
|
||||||
|
pageTextPreview: pageText.slice(0, 3000),
|
||||||
|
capturedAt: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function downloadUrl(url, name, onerror) {
|
||||||
|
if (!url) {
|
||||||
|
onerror?.("URL 없음");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
GM_download({
|
||||||
|
url,
|
||||||
|
name,
|
||||||
|
saveAs: false,
|
||||||
|
onerror: (error) => onerror?.(error?.error || String(error)),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function downloadTextFile(text, name) {
|
||||||
|
const blob = new Blob([text], { type: "application/json;charset=utf-8" });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const anchor = document.createElement("a");
|
||||||
|
anchor.href = url;
|
||||||
|
anchor.download = name;
|
||||||
|
document.body.appendChild(anchor);
|
||||||
|
anchor.click();
|
||||||
|
anchor.remove();
|
||||||
|
setTimeout(() => URL.revokeObjectURL(url), 1000);
|
||||||
|
}
|
||||||
|
|
||||||
|
function downloadSunoPackage(track, statusTarget) {
|
||||||
|
const baseName = safeFilename(track.title || track.id);
|
||||||
|
const messages = [];
|
||||||
|
const fail = (label) => (reason) => {
|
||||||
|
messages.push(`${label} 실패: ${reason}`);
|
||||||
|
setStatus(statusTarget, messages.join(" / "));
|
||||||
|
};
|
||||||
|
|
||||||
|
downloadTextFile(JSON.stringify(track, null, 2), `${baseName}.metadata.json`);
|
||||||
|
messages.push("metadata 저장");
|
||||||
|
|
||||||
|
if (track.imageUrl) {
|
||||||
|
downloadUrl(track.imageUrl, `${baseName}.cover.${extensionFromUrl(track.imageUrl, "jpg")}`, fail("cover"));
|
||||||
|
messages.push("cover 요청");
|
||||||
|
}
|
||||||
|
if (track.wavUrl) {
|
||||||
|
downloadUrl(track.wavUrl, `${baseName}.wav`, fail("wav"));
|
||||||
|
messages.push("wav 요청");
|
||||||
|
} else if (track.audioUrl) {
|
||||||
|
downloadUrl(track.audioUrl, `${baseName}.${extensionFromUrl(track.audioUrl, "audio")}`, fail("audio"));
|
||||||
|
messages.push("audio 요청");
|
||||||
|
}
|
||||||
|
setStatus(statusTarget, messages.join(" / "));
|
||||||
|
}
|
||||||
|
|
||||||
async function bootSuno() {
|
async function bootSuno() {
|
||||||
ensureLauncher("Suno", () => bootSuno());
|
|
||||||
const body = createPanel("Suno Prompt Paste");
|
const body = createPanel("Suno Prompt Paste");
|
||||||
const payload = await readPayload();
|
const slots = await readSlots();
|
||||||
body.innerHTML = `
|
body.innerHTML = `
|
||||||
<div class="cgs-muted" data-cgs-summary></div>
|
<div class="cgs-muted" data-cgs-summary></div>
|
||||||
|
<select class="cgs-select" data-cgs-slot>
|
||||||
|
${slots.map((payload, index) => `<option value="${index}">${escapeHtml(slotLabel(payload, index))}</option>`).join("")}
|
||||||
|
</select>
|
||||||
<div class="cgs-row">
|
<div class="cgs-row">
|
||||||
<button class="cgs-btn cgs-primary cgs-wide" type="button" data-cgs-fill>저장값 붙여넣기</button>
|
<button class="cgs-btn cgs-primary cgs-wide" type="button" data-cgs-fill>선택 슬롯 붙여넣기</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="cgs-field-list">
|
<div class="cgs-field-list">
|
||||||
${FIELD_DEFS.map((def) => `<button class="cgs-btn" type="button" data-cgs-copy="${def.key}">${escapeHtml(def.label)}</button>`).join("")}
|
${FIELD_DEFS.map((def) => `<button class="cgs-btn" type="button" data-cgs-copy="${def.key}">${escapeHtml(def.label)}</button>`).join("")}
|
||||||
</div>
|
</div>
|
||||||
|
<div class="cgs-row">
|
||||||
|
<button class="cgs-btn cgs-wide" type="button" data-cgs-refresh-track>곡 감지</button>
|
||||||
|
</div>
|
||||||
|
<div class="cgs-row">
|
||||||
|
<button class="cgs-btn cgs-primary cgs-wide" type="button" data-cgs-download-track>WAV + 커버 + 메타 다운로드</button>
|
||||||
|
</div>
|
||||||
<div class="cgs-status"></div>
|
<div class="cgs-status"></div>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const summary = body.querySelector("[data-cgs-summary]");
|
const summary = body.querySelector("[data-cgs-summary]");
|
||||||
summary.textContent = payload
|
const slotSelect = body.querySelector("[data-cgs-slot]");
|
||||||
? `${payload.version || "저장된 프롬프트"} 준비됨: ${payload.title || "제목 없음"}`
|
const updateSummary = () => {
|
||||||
: "ChatGPT 쪽에서 먼저 Version을 저장해야 해.";
|
const payload = slots[Number(slotSelect.value)];
|
||||||
|
const track = findCurrentSunoTrack();
|
||||||
|
const promptText = payload ? `${payload.version || "저장된 프롬프트"}: ${payload.title || "제목 없음"}` : "선택 슬롯 비어있음";
|
||||||
|
const trackText = track.id || track.audioUrl || track.imageUrl ? ` / 곡 감지: ${track.title || track.id}` : " / 곡 미감지";
|
||||||
|
summary.textContent = promptText + trackText;
|
||||||
|
};
|
||||||
|
updateSummary();
|
||||||
|
slotSelect.addEventListener("change", updateSummary);
|
||||||
|
|
||||||
body.querySelector("[data-cgs-fill]").addEventListener("click", async () => {
|
body.querySelector("[data-cgs-fill]").addEventListener("click", async () => {
|
||||||
const latest = await readPayload();
|
const latest = await readPayload(Number(slotSelect.value));
|
||||||
if (!latest) {
|
if (!latest) {
|
||||||
setStatus(body, "저장된 값이 없어.");
|
setStatus(body, "선택 슬롯에 저장된 값이 없어.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const results = fillSuno(latest);
|
const results = fillSuno(latest);
|
||||||
@@ -467,7 +642,7 @@
|
|||||||
|
|
||||||
for (const button of body.querySelectorAll("[data-cgs-copy]")) {
|
for (const button of body.querySelectorAll("[data-cgs-copy]")) {
|
||||||
button.addEventListener("click", async () => {
|
button.addEventListener("click", async () => {
|
||||||
const latest = await readPayload();
|
const latest = await readPayload(Number(slotSelect.value));
|
||||||
const key = button.getAttribute("data-cgs-copy");
|
const key = button.getAttribute("data-cgs-copy");
|
||||||
const value = latest?.[key] || "";
|
const value = latest?.[key] || "";
|
||||||
if (!value) {
|
if (!value) {
|
||||||
@@ -478,6 +653,21 @@
|
|||||||
setStatus(body, `${button.textContent}만 복사했어.`);
|
setStatus(body, `${button.textContent}만 복사했어.`);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body.querySelector("[data-cgs-refresh-track]").addEventListener("click", () => {
|
||||||
|
updateSummary();
|
||||||
|
const track = findCurrentSunoTrack();
|
||||||
|
setStatus(body, track.id || track.audioUrl ? `감지됨: ${track.title || track.id}` : "현재 화면에서 곡 URL을 못 찾았어.");
|
||||||
|
});
|
||||||
|
|
||||||
|
body.querySelector("[data-cgs-download-track]").addEventListener("click", () => {
|
||||||
|
const track = findCurrentSunoTrack();
|
||||||
|
if (!track.id && !track.audioUrl && !track.imageUrl) {
|
||||||
|
setStatus(body, "다운로드할 곡 정보를 못 찾았어. 곡 상세/플레이어 화면에서 다시 눌러봐.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
downloadSunoPackage(track, body);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function startApp() {
|
function startApp() {
|
||||||
|
|||||||
Reference in New Issue
Block a user