From 73f409a01b08c379ed1e8793d6bfc07d9116e7be Mon Sep 17 00:00:00 2001 From: "DESKTOP-KSVGT20\\shkim" Date: Sat, 2 May 2026 01:37:00 +0900 Subject: [PATCH] Add cumulative selectable Suno track list --- chatgpt-suno-tampermonkey.user.js | 241 +++++++++++++++++++++++++----- 1 file changed, 204 insertions(+), 37 deletions(-) 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 || "",