Add Suno library batch download
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
// ==UserScript==
|
||||
// @name ChatGPT to Suno Prompt Copier
|
||||
// @namespace local.suno-helper
|
||||
// @version 0.2.0
|
||||
// @version 0.3.0
|
||||
// @description Copy structured ChatGPT song prompt versions into Suno fields.
|
||||
// @author local
|
||||
// @match https://chatgpt.com/*
|
||||
@@ -10,6 +10,8 @@
|
||||
// @match https://*.suno.com/*
|
||||
// @updateURL 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 *.suno.com
|
||||
// @run-at document-idle
|
||||
// @grant GM_addStyle
|
||||
// @grant GM_getValue
|
||||
@@ -495,6 +497,84 @@
|
||||
return output;
|
||||
}
|
||||
|
||||
function firstValue(object, keys) {
|
||||
for (const key of keys) {
|
||||
if (object?.[key]) return object[key];
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function isLikelyTrackObject(object) {
|
||||
if (!object || typeof object !== "object" || Array.isArray(object)) return false;
|
||||
const id = firstValue(object, ["id", "clip_id", "clipId", "audio_id", "audioId"]);
|
||||
const hasTrackText = firstValue(object, ["title", "name", "prompt", "gpt_description_prompt", "metadata"]);
|
||||
const hasMedia = firstValue(object, ["audio_url", "audioUrl", "image_url", "imageUrl", "image_large_url", "wav_url", "wavUrl"]);
|
||||
return Boolean(id && (hasTrackText || hasMedia));
|
||||
}
|
||||
|
||||
function normalizeTrackObject(object) {
|
||||
const id = String(firstValue(object, ["id", "clip_id", "clipId", "audio_id", "audioId"]) || "").trim();
|
||||
const title = String(firstValue(object, ["title", "name"]) || id || "suno-track").trim();
|
||||
const imageUrl = firstValue(object, ["image_url", "imageUrl", "image_large_url", "imageLargeUrl", "cover_url", "coverUrl"]);
|
||||
const audioUrl = firstValue(object, ["audio_url", "audioUrl", "play_url", "playUrl", "mp3_url", "mp3Url"]);
|
||||
const wavUrl = firstValue(object, ["wav_url", "wavUrl"]) || (id ? `https://cdn1.suno.ai/${id}.wav` : "");
|
||||
return {
|
||||
id,
|
||||
title,
|
||||
audioUrl,
|
||||
wavUrl,
|
||||
imageUrl,
|
||||
pageUrl: location.href,
|
||||
metadata: object,
|
||||
capturedAt: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
function mergeTrack(target, source) {
|
||||
if (!target) return source;
|
||||
for (const key of ["title", "audioUrl", "wavUrl", "imageUrl", "pageUrl"]) {
|
||||
if (!target[key] && source[key]) target[key] = source[key];
|
||||
}
|
||||
target.metadata = target.metadata && Object.keys(target.metadata).length ? target.metadata : source.metadata;
|
||||
return target;
|
||||
}
|
||||
|
||||
function scanSunoLibraryTracks() {
|
||||
const scripts = readJsonScripts();
|
||||
const objects = scripts.flatMap((item) => flattenObjects(item));
|
||||
const tracks = new Map();
|
||||
|
||||
for (const object of objects) {
|
||||
if (!isLikelyTrackObject(object)) continue;
|
||||
const track = normalizeTrackObject(object);
|
||||
if (!track.id) continue;
|
||||
tracks.set(track.id, mergeTrack(tracks.get(track.id), track));
|
||||
}
|
||||
|
||||
const htmlText = document.documentElement.innerHTML || "";
|
||||
const idMatches = [...htmlText.matchAll(/[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 fallbackIds = unique(idMatches
|
||||
.filter((match) => /clip|audio|image|title|cdn[0-9]*\.suno\.ai/i.test(htmlText.slice(Math.max(0, match.index - 500), match.index + 500)))
|
||||
.map((match) => match[0]));
|
||||
for (const id of fallbackIds) {
|
||||
if (tracks.has(id)) continue;
|
||||
tracks.set(id, {
|
||||
id,
|
||||
title: id,
|
||||
audioUrl: "",
|
||||
wavUrl: `https://cdn1.suno.ai/${id}.wav`,
|
||||
imageUrl: "",
|
||||
pageUrl: location.href,
|
||||
metadata: {},
|
||||
capturedAt: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
|
||||
return [...tracks.values()]
|
||||
.filter((track) => track.id || track.audioUrl || track.imageUrl)
|
||||
.sort((a, b) => String(a.title).localeCompare(String(b.title), "ko"));
|
||||
}
|
||||
|
||||
function findCurrentSunoTrack() {
|
||||
const pageText = document.body.innerText || "";
|
||||
const htmlText = document.documentElement.innerHTML || "";
|
||||
@@ -595,6 +675,30 @@
|
||||
setStatus(statusTarget, messages.join(" / "));
|
||||
}
|
||||
|
||||
async function downloadSunoBatch(tracks, statusTarget) {
|
||||
if (!tracks.length) {
|
||||
setStatus(statusTarget, "다운로드할 곡을 못 찾았어. 라이브러리에서 스크롤을 좀 내려서 곡을 로드한 뒤 다시 눌러봐.");
|
||||
return;
|
||||
}
|
||||
|
||||
const manifest = {
|
||||
pageUrl: location.href,
|
||||
capturedAt: new Date().toISOString(),
|
||||
count: tracks.length,
|
||||
tracks,
|
||||
};
|
||||
downloadTextFile(JSON.stringify(manifest, null, 2), `suno-library-${new Date().toISOString().slice(0, 10)}.manifest.json`);
|
||||
setStatus(statusTarget, `${tracks.length}곡 감지. manifest 저장 후 순차 다운로드 시작.`);
|
||||
|
||||
for (let index = 0; index < tracks.length; index += 1) {
|
||||
const track = tracks[index];
|
||||
setStatus(statusTarget, `${index + 1}/${tracks.length} 요청 중: ${track.title || track.id}`);
|
||||
downloadSunoPackage(track, statusTarget);
|
||||
await new Promise((resolve) => setTimeout(resolve, 650));
|
||||
}
|
||||
setStatus(statusTarget, `${tracks.length}곡 다운로드 요청 완료. 실패한 파일은 브라우저 다운로드 목록/metadata에서 확인해줘.`);
|
||||
}
|
||||
|
||||
async function bootSuno() {
|
||||
const body = createPanel("Suno Prompt Paste");
|
||||
const slots = await readSlots();
|
||||
@@ -615,17 +719,25 @@
|
||||
<div class="cgs-row">
|
||||
<button class="cgs-btn cgs-primary cgs-wide" type="button" data-cgs-download-track>WAV + 커버 + 메타 다운로드</button>
|
||||
</div>
|
||||
<div class="cgs-row">
|
||||
<button class="cgs-btn cgs-wide" type="button" data-cgs-scan-library>라이브러리 곡 감지</button>
|
||||
</div>
|
||||
<div class="cgs-row">
|
||||
<button class="cgs-btn cgs-primary cgs-wide" type="button" data-cgs-download-library>감지된 라이브러리 일괄 다운로드</button>
|
||||
</div>
|
||||
<div class="cgs-status"></div>
|
||||
`;
|
||||
|
||||
const summary = body.querySelector("[data-cgs-summary]");
|
||||
const slotSelect = body.querySelector("[data-cgs-slot]");
|
||||
let libraryTracks = [];
|
||||
const updateSummary = () => {
|
||||
const payload = slots[Number(slotSelect.value)];
|
||||
const track = findCurrentSunoTrack();
|
||||
const promptText = payload ? `${payload.version || "저장된 프롬프트"}: ${payload.title || "제목 없음"}` : "선택 슬롯 비어있음";
|
||||
const libraryText = libraryTracks.length ? ` / 라이브러리 ${libraryTracks.length}곡` : "";
|
||||
const trackText = track.id || track.audioUrl || track.imageUrl ? ` / 곡 감지: ${track.title || track.id}` : " / 곡 미감지";
|
||||
summary.textContent = promptText + trackText;
|
||||
summary.textContent = promptText + trackText + libraryText;
|
||||
};
|
||||
updateSummary();
|
||||
slotSelect.addEventListener("change", updateSummary);
|
||||
@@ -668,6 +780,19 @@
|
||||
}
|
||||
downloadSunoPackage(track, body);
|
||||
});
|
||||
|
||||
body.querySelector("[data-cgs-scan-library]").addEventListener("click", () => {
|
||||
libraryTracks = scanSunoLibraryTracks();
|
||||
updateSummary();
|
||||
const sample = libraryTracks.slice(0, 3).map((track) => track.title || track.id).join(", ");
|
||||
setStatus(body, libraryTracks.length ? `${libraryTracks.length}곡 감지: ${sample}` : "라이브러리 곡을 못 찾았어. 곡 목록을 스크롤해서 로드한 뒤 다시 눌러봐.");
|
||||
});
|
||||
|
||||
body.querySelector("[data-cgs-download-library]").addEventListener("click", async () => {
|
||||
if (!libraryTracks.length) libraryTracks = scanSunoLibraryTracks();
|
||||
updateSummary();
|
||||
await downloadSunoBatch(libraryTracks, body);
|
||||
});
|
||||
}
|
||||
|
||||
function startApp() {
|
||||
|
||||
Reference in New Issue
Block a user