From 513db59a53337047dc4caa16b1ca0bbeef07baac Mon Sep 17 00:00:00 2001 From: "DESKTOP-KSVGT20\\shkim" Date: Fri, 1 May 2026 17:41:51 +0900 Subject: [PATCH] Capture Suno API responses for library detection --- chatgpt-suno-tampermonkey.user.js | 105 +++++++++++++++++++++++++++--- 1 file changed, 96 insertions(+), 9 deletions(-) diff --git a/chatgpt-suno-tampermonkey.user.js b/chatgpt-suno-tampermonkey.user.js index dc5e189..5c2049b 100644 --- a/chatgpt-suno-tampermonkey.user.js +++ b/chatgpt-suno-tampermonkey.user.js @@ -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 () => {