Capture Suno API responses for library detection
This commit is contained in:
@@ -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.3.0
|
// @version 0.3.1
|
||||||
// @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/*
|
||||||
@@ -12,12 +12,13 @@
|
|||||||
// @downloadURL https://git.mokaya.org/shkim/suno-helper/raw/branch/master/chatgpt-suno-tampermonkey.user.js
|
// @downloadURL https://git.mokaya.org/shkim/suno-helper/raw/branch/master/chatgpt-suno-tampermonkey.user.js
|
||||||
// @connect cdn*.suno.ai
|
// @connect cdn*.suno.ai
|
||||||
// @connect *.suno.com
|
// @connect *.suno.com
|
||||||
// @run-at document-idle
|
// @run-at document-start
|
||||||
// @grant GM_addStyle
|
// @grant GM_addStyle
|
||||||
// @grant GM_getValue
|
// @grant GM_getValue
|
||||||
// @grant GM_download
|
// @grant GM_download
|
||||||
// @grant GM_setClipboard
|
// @grant GM_setClipboard
|
||||||
// @grant GM_setValue
|
// @grant GM_setValue
|
||||||
|
// @grant unsafeWindow
|
||||||
// ==/UserScript==
|
// ==/UserScript==
|
||||||
|
|
||||||
(function () {
|
(function () {
|
||||||
@@ -27,6 +28,7 @@
|
|||||||
const SLOT_COUNT = 2;
|
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");
|
||||||
|
const capturedSunoResponses = [];
|
||||||
let chatRenderTimer = 0;
|
let chatRenderTimer = 0;
|
||||||
|
|
||||||
const FIELD_DEFS = [
|
const FIELD_DEFS = [
|
||||||
@@ -37,6 +39,69 @@
|
|||||||
{ key: "extend", label: "Extend Prompt", aliases: ["Extend용 추가 프롬프트", "Extend", "추가 프롬프트"] },
|
{ 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(`
|
GM_addStyle(`
|
||||||
#cgs-panel {
|
#cgs-panel {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
@@ -486,6 +551,14 @@
|
|||||||
return results;
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function readSunoDataSources() {
|
||||||
|
return [
|
||||||
|
...readJsonScripts(),
|
||||||
|
...capturedSunoResponses.map((item) => item.data),
|
||||||
|
...((typeof unsafeWindow !== "undefined" && unsafeWindow.__NEXT_DATA__) ? [unsafeWindow.__NEXT_DATA__] : []),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
function flattenObjects(value, output = []) {
|
function flattenObjects(value, output = []) {
|
||||||
if (!value || typeof value !== "object") return output;
|
if (!value || typeof value !== "object") return output;
|
||||||
if (Array.isArray(value)) {
|
if (Array.isArray(value)) {
|
||||||
@@ -540,8 +613,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function scanSunoLibraryTracks() {
|
function scanSunoLibraryTracks() {
|
||||||
const scripts = readJsonScripts();
|
const sources = readSunoDataSources();
|
||||||
const objects = scripts.flatMap((item) => flattenObjects(item));
|
const objects = sources.flatMap((item) => flattenObjects(item));
|
||||||
const tracks = new Map();
|
const tracks = new Map();
|
||||||
|
|
||||||
for (const object of objects) {
|
for (const object of objects) {
|
||||||
@@ -575,13 +648,21 @@
|
|||||||
.sort((a, b) => String(a.title).localeCompare(String(b.title), "ko"));
|
.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() {
|
function findCurrentSunoTrack() {
|
||||||
const pageText = document.body.innerText || "";
|
const pageText = document.body.innerText || "";
|
||||||
const htmlText = document.documentElement.innerHTML || "";
|
const htmlText = document.documentElement.innerHTML || "";
|
||||||
const scripts = readJsonScripts();
|
const sources = readSunoDataSources();
|
||||||
const objects = scripts.flatMap((item) => flattenObjects(item));
|
const objects = sources.flatMap((item) => flattenObjects(item));
|
||||||
const audioElements = [...document.querySelectorAll("audio, video")].map((node) => node.currentSrc || node.src);
|
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 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 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 cdnAudioUrls = extractUrls(allText, /https:\/\/cdn[0-9]*\.suno\.ai\/[^"' <>)]+?\.(?:mp3|wav|m4a|oga|ogg)(?:\?[^"' <>)]+)?/gi);
|
||||||
@@ -603,7 +684,7 @@
|
|||||||
metadata.name ||
|
metadata.name ||
|
||||||
document.querySelector("h1")?.innerText ||
|
document.querySelector("h1")?.innerText ||
|
||||||
document.querySelector("[data-testid*='title' i]")?.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 id = metadata.id || metadata.clip_id || metadata.clipId || ids[0] || "";
|
||||||
const imageUrl = metadata.image_url || metadata.imageUrl || metadata.image_large_url || imageUrls[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 audioUrl = metadata.audio_url || metadata.audioUrl || audioUrls.find((url) => /\.wav(?:\?|$)/i.test(url)) || audioUrls[0] || "";
|
||||||
@@ -785,7 +866,13 @@
|
|||||||
libraryTracks = scanSunoLibraryTracks();
|
libraryTracks = scanSunoLibraryTracks();
|
||||||
updateSummary();
|
updateSummary();
|
||||||
const sample = libraryTracks.slice(0, 3).map((track) => track.title || track.id).join(", ");
|
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 () => {
|
body.querySelector("[data-cgs-download-library]").addEventListener("click", async () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user