diff --git a/chatgpt-suno-tampermonkey.user.js b/chatgpt-suno-tampermonkey.user.js
index 113fc6c..580ebef 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.8.8
+// @version 0.8.9
// @description Copy structured ChatGPT song prompt versions into Suno fields.
// @author local
// @match https://chatgpt.com/*
@@ -30,7 +30,7 @@
(function () {
"use strict";
- const SCRIPT_VERSION = "0.8.8";
+ const SCRIPT_VERSION = "0.8.9";
const STORAGE_KEY = "chatgptSunoPromptSlots";
const SLOT_COUNT = 2;
const isChatGPT = location.hostname.includes("chatgpt.com") || location.hostname.includes("chat.openai.com");
@@ -115,7 +115,8 @@
right: 16px;
bottom: 16px;
z-index: 2147483647;
- width: min(360px, calc(100vw - 32px));
+ width: min(560px, calc(100vw - 32px));
+ max-height: min(86vh, 860px);
color: #f5f7fb;
background: rgba(16, 18, 24, 0.94);
border: 1px solid rgba(255, 255, 255, 0.14);
@@ -124,6 +125,9 @@
font: 13px/1.45 system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
overflow: hidden;
}
+ #cgs-panel.cgs-suno {
+ width: min(620px, calc(100vw - 32px));
+ }
#cgs-panel.cgs-chatgpt {
right: 18px;
top: 72px;
@@ -144,7 +148,11 @@
background: rgba(255, 255, 255, 0.06);
font-weight: 700;
}
- .cgs-body { padding: 10px 12px 12px; }
+ .cgs-body {
+ padding: 10px 12px 12px;
+ max-height: calc(min(86vh, 860px) - 48px);
+ overflow: auto;
+ }
.cgs-row {
display: flex;
gap: 6px;
@@ -197,6 +205,47 @@
padding: 4px 7px;
font: inherit;
}
+ .cgs-track-list {
+ margin-top: 8px;
+ max-height: 300px;
+ overflow: auto;
+ border: 1px solid rgba(255, 255, 255, 0.14);
+ border-radius: 8px;
+ background: rgba(0, 0, 0, 0.18);
+ }
+ .cgs-track-item {
+ display: grid;
+ grid-template-columns: 28px minmax(0, 1fr) auto;
+ gap: 8px;
+ align-items: center;
+ min-height: 38px;
+ padding: 7px 8px;
+ border-bottom: 1px solid rgba(255, 255, 255, 0.07);
+ cursor: default;
+ user-select: none;
+ }
+ .cgs-track-item:last-child { border-bottom: 0; }
+ .cgs-track-item:hover { background: rgba(255, 255, 255, 0.08); }
+ .cgs-track-item.cgs-selected {
+ background: rgba(47, 124, 246, 0.28);
+ outline: 1px solid rgba(91, 154, 255, 0.55);
+ outline-offset: -1px;
+ }
+ .cgs-track-index { color: #93a4bd; text-align: right; font-variant-numeric: tabular-nums; }
+ .cgs-track-title {
+ min-width: 0;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ color: #f5f7fb;
+ font-weight: 650;
+ }
+ .cgs-track-meta { color: #94a3b8; font-size: 11px; }
+ .cgs-track-badge {
+ color: #b7c9ff;
+ font-size: 11px;
+ white-space: nowrap;
+ }
`);
function createPanel(title, mode = "") {
@@ -718,11 +767,15 @@
}
function trackMatchesVisibleRows(track, rows) {
- if (!rows.length) return false;
+ return getTrackVisibleIndex(track, rows) >= 0;
+ }
+
+ function getTrackVisibleIndex(track, rows) {
+ if (!rows.length) return -1;
const title = normalizeMatchText(track.title);
const style = normalizeMatchText(track.style || track.lyrics || "");
- if (!title || title.length > 160) return false;
- return rows.some((row) => {
+ if (!title || title.length > 160) return -1;
+ return rows.findIndex((row) => {
if (!row.normalized.includes(title)) return false;
if (!style) return true;
const styleNeedle = style.slice(0, Math.min(42, style.length));
@@ -772,7 +825,7 @@
.filter((track) => track.id || track.audioUrl || track.imageUrl)
.filter((track) => trackMatchesCurrentUser(track, currentUser))
.filter((track) => trackMatchesVisibleRows(track, visibleRows))
- .sort((a, b) => String(a.title).localeCompare(String(b.title), "ko"));
+ .sort((a, b) => getTrackVisibleIndex(a, visibleRows) - getTrackVisibleIndex(b, visibleRows));
}
function visibleSunoRowsHint() {
@@ -1505,7 +1558,7 @@
}
async function bootSuno() {
- const body = createPanel("Suno Prompt Paste");
+ const body = createPanel("Suno Prompt Paste", "cgs-suno");
const slots = await readSlots();
body.innerHTML = `
@@ -1521,13 +1574,16 @@
-
+
+
+
+
+
+
@@ -1535,27 +1591,81 @@
-
+
-
+
`;
const summary = body.querySelector("[data-cgs-summary]");
+ const selectionSummary = body.querySelector("[data-cgs-selection-summary]");
const slotSelect = body.querySelector("[data-cgs-slot]");
- const libraryTrackSelect = body.querySelector("[data-cgs-library-track]");
+ const trackList = body.querySelector("[data-cgs-track-list]");
let libraryTracks = [];
- const renderLibraryTrackSelect = () => {
+ const libraryTrackMap = new Map();
+ const selectedTrackIds = new Set();
+ let lastSelectedIndex = -1;
+ let autoScanTimer = 0;
+
+ const getSelectedTracks = () => libraryTracks.filter((track) => selectedTrackIds.has(track.id));
+ const mergeLibraryTracks = (tracks) => {
+ for (const track of tracks) {
+ if (!track.id) continue;
+ if (libraryTrackMap.has(track.id)) {
+ libraryTrackMap.set(track.id, mergeTrack(libraryTrackMap.get(track.id), track));
+ } else {
+ libraryTrackMap.set(track.id, track);
+ }
+ }
+ libraryTracks = [...libraryTrackMap.values()];
+ };
+ const renderLibraryTrackList = () => {
+ const selectedCount = selectedTrackIds.size;
+ selectionSummary.textContent = libraryTracks.length
+ ? `누적 ${libraryTracks.length}곡 / 선택 ${selectedCount}곡`
+ : "누적된 곡 없음. 스크롤하면서 곡 감지를 눌러줘.";
if (!libraryTracks.length) {
- libraryTrackSelect.innerHTML = ``;
+ trackList.innerHTML = ``;
return;
}
- libraryTrackSelect.innerHTML = libraryTracks
- .map((track, index) => ``)
+ trackList.innerHTML = libraryTracks
+ .map((track, index) => {
+ const selected = selectedTrackIds.has(track.id);
+ const badge = [
+ track.audioUrl ? "MP3" : "",
+ track.wavUrl ? "WAV" : "",
+ track.imageUrl ? "IMG" : "",
+ ].filter(Boolean).join(" ");
+ return `
+
+
${index + 1}
+
+
${escapeHtml(track.title || track.id || "제목 없음")}
+
${escapeHtml((track.style || track.lyrics || track.id || "").slice(0, 110))}
+
+
${escapeHtml(badge || "ID")}
+
+ `;
+ })
.join("");
};
+ const setSelectedRange = (from, to) => {
+ const start = Math.max(0, Math.min(from, to));
+ const end = Math.min(libraryTracks.length - 1, Math.max(from, to));
+ for (let index = start; index <= end; index += 1) {
+ if (libraryTracks[index]?.id) selectedTrackIds.add(libraryTracks[index].id);
+ }
+ };
+ const scanAndMergeVisibleTracks = () => {
+ const scannedTracks = scanSunoLibraryTracks();
+ const before = libraryTracks.length;
+ mergeLibraryTracks(scannedTracks);
+ renderLibraryTrackList();
+ updateSummary();
+ return { scannedTracks, added: libraryTracks.length - before };
+ };
const updateSummary = () => {
const payload = slots[Number(slotSelect.value)];
const track = findCurrentSunoTrack();
@@ -1564,7 +1674,7 @@
const trackText = track.id || track.audioUrl || track.imageUrl ? ` / 곡 감지: ${track.title || track.id}` : " / 곡 미감지";
summary.textContent = promptText + trackText + libraryText;
};
- renderLibraryTrackSelect();
+ renderLibraryTrackList();
updateSummary();
slotSelect.addEventListener("change", updateSummary);
@@ -1593,13 +1703,11 @@
}
body.querySelector("[data-cgs-scan-library]").addEventListener("click", () => {
- libraryTracks = scanSunoLibraryTracks();
- renderLibraryTrackSelect();
- updateSummary();
- const sample = libraryTracks.slice(0, 3).map((track) => track.title || track.id).join(", ");
- if (libraryTracks.length) {
+ const { scannedTracks, added } = scanAndMergeVisibleTracks();
+ const sample = scannedTracks.slice(0, 3).map((track) => track.title || track.id).join(", ");
+ if (scannedTracks.length) {
const visibleCount = getVisibleLibraryRows().length;
- setStatus(body, `${libraryTracks.length}곡 감지: ${sample} / 화면 행 ${visibleCount}개 기준`);
+ setStatus(body, `이번 화면 ${scannedTracks.length}곡 감지, 누적 ${libraryTracks.length}곡 (+${added}): ${sample} / 화면 행 ${visibleCount}개 기준`);
return;
}
const visibleHints = visibleSunoRowsHint();
@@ -1607,14 +1715,46 @@
setStatus(body, `${networkHint} 목록 새로고침이나 스크롤 후 다시 눌러봐. 화면 텍스트: ${visibleHints.join(", ")}`);
});
+ const scheduleAutoScan = () => {
+ clearTimeout(autoScanTimer);
+ autoScanTimer = setTimeout(() => {
+ const result = scanAndMergeVisibleTracks();
+ if (result.added > 0) {
+ setStatus(body, `스크롤 감지로 ${result.added}곡 추가, 누적 ${libraryTracks.length}곡`);
+ }
+ }, 450);
+ };
+ window.addEventListener("scroll", scheduleAutoScan, { passive: true });
+ document.addEventListener("scroll", scheduleAutoScan, { passive: true, capture: true });
+
+ trackList.addEventListener("click", (event) => {
+ const item = event.target.closest("[data-cgs-track-index]");
+ if (!item) return;
+ const index = Number(item.getAttribute("data-cgs-track-index"));
+ const track = libraryTracks[index];
+ if (!track?.id) return;
+ if (event.shiftKey && lastSelectedIndex >= 0) {
+ setSelectedRange(lastSelectedIndex, index);
+ } else if (event.ctrlKey || event.metaKey) {
+ if (selectedTrackIds.has(track.id)) selectedTrackIds.delete(track.id);
+ else selectedTrackIds.add(track.id);
+ lastSelectedIndex = index;
+ } else {
+ selectedTrackIds.clear();
+ selectedTrackIds.add(track.id);
+ lastSelectedIndex = index;
+ }
+ renderLibraryTrackList();
+ updateSummary();
+ });
+
const processSelectedTrack = async (format) => {
if (!libraryTracks.length) {
- libraryTracks = scanSunoLibraryTracks();
- renderLibraryTrackSelect();
+ mergeLibraryTracks(scanSunoLibraryTracks());
+ renderLibraryTrackList();
}
updateSummary();
- const index = Number(libraryTrackSelect.value || 0);
- const track = libraryTracks[index];
+ const track = getSelectedTracks()[0] || libraryTracks[0];
if (!track) {
setStatus(body, "선택할 감지 곡이 없어. 곡 감지를 먼저 눌러줘.");
return;
@@ -1635,10 +1775,18 @@
await processSelectedTrack("wav");
});
+ body.querySelector("[data-cgs-process-selected-mp3]").addEventListener("click", async () => {
+ await processSelectedTracks("mp3");
+ });
+
+ body.querySelector("[data-cgs-process-selected-wav]").addEventListener("click", async () => {
+ await processSelectedTracks("wav");
+ });
+
const processLibraryTracks = async (format) => {
if (!libraryTracks.length) {
- libraryTracks = scanSunoLibraryTracks();
- renderLibraryTrackSelect();
+ mergeLibraryTracks(scanSunoLibraryTracks());
+ renderLibraryTrackList();
}
updateSummary();
try {
@@ -1648,6 +1796,24 @@
}
};
+ const processSelectedTracks = async (format) => {
+ if (!libraryTracks.length) {
+ mergeLibraryTracks(scanSunoLibraryTracks());
+ renderLibraryTrackList();
+ }
+ const tracks = getSelectedTracks();
+ if (!tracks.length) {
+ setStatus(body, "선택된 곡이 없어. 목록에서 Ctrl/Shift로 고른 뒤 눌러줘.");
+ return;
+ }
+ updateSummary();
+ try {
+ await uploadTracksToCompanion(tracks, body, format);
+ } catch (error) {
+ setStatus(body, `선택항목 ${format.toUpperCase()} 처리 실패: ${error.message || error}`);
+ }
+ };
+
body.querySelector("[data-cgs-process-library-mp3]").addEventListener("click", async () => {
await processLibraryTracks("mp3");
});
@@ -1662,11 +1828,11 @@
body.querySelector("[data-cgs-copy-diagnostics]").addEventListener("click", async () => {
if (!libraryTracks.length) {
- libraryTracks = scanSunoLibraryTracks();
- renderLibraryTrackSelect();
+ mergeLibraryTracks(scanSunoLibraryTracks());
+ renderLibraryTrackList();
}
- const selectedIndex = Number(libraryTrackSelect.value || 0);
- const selectedTrack = libraryTracks[selectedIndex];
+ const selectedTracks = getSelectedTracks();
+ const selectedTrack = selectedTracks[0] || libraryTracks[0];
let health = {};
let healthError = "";
try {
@@ -1679,7 +1845,8 @@
location: location.href,
detectedTracks: libraryTracks.length,
visibleRows: getVisibleLibraryRows().length,
- selectedIndex,
+ selectedCount: selectedTracks.length,
+ selectedIds: selectedTracks.map((track) => track.id),
selectedTrack: selectedTrack ? {
id: selectedTrack.id || "",
title: selectedTrack.title || "",