Capture Suno API responses for library detection
This commit is contained in:
@@ -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 () => {
|
||||
|
||||
Reference in New Issue
Block a user