Capture Suno API responses for library detection

This commit is contained in:
DESKTOP-KSVGT20\shkim
2026-05-01 17:41:51 +09:00
parent 778ec302c0
commit 513db59a53
+96 -9
View File
@@ -1,7 +1,7 @@
// ==UserScript==
// @name ChatGPT to Suno Prompt Copier
// @namespace local.suno-helper
// @version 0.3.0
// @version 0.3.1
// @description Copy structured ChatGPT song prompt versions into Suno fields.
// @author local
// @match https://chatgpt.com/*
@@ -12,12 +12,13 @@
// @downloadURL https://git.mokaya.org/shkim/suno-helper/raw/branch/master/chatgpt-suno-tampermonkey.user.js
// @connect cdn*.suno.ai
// @connect *.suno.com
// @run-at document-idle
// @run-at document-start
// @grant GM_addStyle
// @grant GM_getValue
// @grant GM_download
// @grant GM_setClipboard
// @grant GM_setValue
// @grant unsafeWindow
// ==/UserScript==
(function () {
@@ -27,6 +28,7 @@
const SLOT_COUNT = 2;
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 capturedSunoResponses = [];
let chatRenderTimer = 0;
const FIELD_DEFS = [
@@ -37,6 +39,69 @@
{ key: "extend", label: "Extend Prompt", aliases: ["Extend용 추가 프롬프트", "Extend", "추가 프롬프트"] },
];
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;
capturedSunoResponses.push({
url: String(url || location.href),
data,
capturedAt: new Date().toISOString(),
});
if (capturedSunoResponses.length > 60) capturedSunoResponses.shift();
}
function tryParseJson(text) {
if (!text || typeof text !== "string") return null;
try {
return JSON.parse(text);
} catch {
return null;
}
}
function installSunoNetworkCapture() {
if (!isSuno) return;
const page = typeof unsafeWindow !== "undefined" ? unsafeWindow : window;
if (page.__cgsNetworkCaptureInstalled) return;
page.__cgsNetworkCaptureInstalled = true;
const originalFetch = page.fetch;
if (typeof originalFetch === "function") {
page.fetch = async function (...args) {
const response = await originalFetch.apply(this, args);
const url = args[0]?.url || args[0] || response.url || "";
if (/suno/i.test(String(url))) {
response.clone().text().then((text) => {
rememberSunoResponse(url, tryParseJson(text) || text);
}).catch(() => {});
}
return response;
};
}
const OriginalXHR = page.XMLHttpRequest;
if (typeof OriginalXHR === "function") {
page.XMLHttpRequest = function () {
const xhr = new OriginalXHR();
let requestUrl = "";
const open = xhr.open;
xhr.open = function (method, url, ...rest) {
requestUrl = String(url || "");
return open.call(xhr, method, url, ...rest);
};
xhr.addEventListener("load", () => {
if (!/suno/i.test(requestUrl || xhr.responseURL || "")) return;
const responseText = typeof xhr.responseText === "string" ? xhr.responseText : "";
rememberSunoResponse(requestUrl || xhr.responseURL, tryParseJson(responseText) || responseText);
});
return xhr;
};
}
}
installSunoNetworkCapture();
GM_addStyle(`
#cgs-panel {
position: fixed;
@@ -486,6 +551,14 @@
return results;
}
function readSunoDataSources() {
return [
...readJsonScripts(),
...capturedSunoResponses.map((item) => item.data),
...((typeof unsafeWindow !== "undefined" && unsafeWindow.__NEXT_DATA__) ? [unsafeWindow.__NEXT_DATA__] : []),
];
}
function flattenObjects(value, output = []) {
if (!value || typeof value !== "object") return output;
if (Array.isArray(value)) {
@@ -540,8 +613,8 @@
}
function scanSunoLibraryTracks() {
const scripts = readJsonScripts();
const objects = scripts.flatMap((item) => flattenObjects(item));
const sources = readSunoDataSources();
const objects = sources.flatMap((item) => flattenObjects(item));
const tracks = new Map();
for (const object of objects) {
@@ -575,13 +648,21 @@
.sort((a, b) => String(a.title).localeCompare(String(b.title), "ko"));
}
function visibleSunoRowsHint() {
const titles = [...document.querySelectorAll("a, h1, h2, h3, [role='button'], div, span")]
.map((node) => (node.innerText || node.textContent || "").trim())
.filter((text) => text && text.length < 80 && !/^(Songs|Playlists|Workspaces|Studio Projects|Voices|Cover Art|Hooks|History|Search|Newest|Liked|Public|Uploads)$/i.test(text));
const likelyTitles = titles.filter((text, index) => titles.indexOf(text) === index).slice(0, 5);
return likelyTitles;
}
function findCurrentSunoTrack() {
const pageText = document.body.innerText || "";
const htmlText = document.documentElement.innerHTML || "";
const scripts = readJsonScripts();
const objects = scripts.flatMap((item) => flattenObjects(item));
const sources = readSunoDataSources();
const objects = sources.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 allText = [htmlText, ...sources.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);
@@ -603,7 +684,7 @@
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] || "";
@@ -785,7 +866,13 @@
libraryTracks = scanSunoLibraryTracks();
updateSummary();
const sample = libraryTracks.slice(0, 3).map((track) => track.title || track.id).join(", ");
setStatus(body, libraryTracks.length ? `${libraryTracks.length}곡 감지: ${sample}` : "라이브러리 곡을 못 찾았어. 곡 목록을 스크롤해서 로드한 뒤 다시 눌러봐.");
if (libraryTracks.length) {
setStatus(body, `${libraryTracks.length}곡 감지: ${sample}`);
return;
}
const visibleHints = visibleSunoRowsHint();
const networkHint = capturedSunoResponses.length ? `API 응답 ${capturedSunoResponses.length}개는 잡혔는데 clip 구조를 못 찾았어.` : "아직 SUNO API 응답을 못 잡았어.";
setStatus(body, `${networkHint} 목록 새로고침이나 스크롤 후 다시 눌러봐. 화면 텍스트: ${visibleHints.join(", ")}`);
});
body.querySelector("[data-cgs-download-library]").addEventListener("click", async () => {