diff --git a/chatgpt-suno-tampermonkey.user.js b/chatgpt-suno-tampermonkey.user.js index 713a6b4..2d22242 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.9.3 +// @version 0.9.4 // @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.9.3"; + const SCRIPT_VERSION = "0.9.4"; const STORAGE_KEY = "chatgptSunoPromptSlots"; const SLOT_COUNT = 2; const isChatGPT = location.hostname.includes("chatgpt.com") || location.hostname.includes("chat.openai.com"); @@ -306,6 +306,35 @@ if (status) status.textContent = text; } + function updateProgress(body, done, total, label = "진행") { + const progressLabel = body?.querySelector?.("[data-cgs-progress-label]"); + const progressFill = body?.querySelector?.("[data-cgs-progress-fill]"); + if (!progressLabel || !progressFill) return; + const safeTotal = Math.max(0, Number(total || 0)); + const safeDone = Math.max(0, Math.min(Number(done || 0), safeTotal || Number(done || 0))); + const percent = safeTotal ? Math.round((safeDone / safeTotal) * 1000) / 10 : 0; + progressLabel.textContent = `${label} ${safeDone}/${safeTotal}${safeTotal ? ` (${percent}%)` : ""}`; + progressFill.style.width = `${safeTotal ? percent : 0}%`; + } + + function formatSyncSummary(data, fallbackRequested = 0) { + const requested = Number(data?.requested ?? data?.received ?? fallbackRequested ?? 0); + const existing = Number(data?.existingRequested ?? data?.existing ?? 0); + const missing = Number(data?.missingRequested ?? data?.missing ?? 0); + const invalid = Number(data?.invalidRequested ?? 0); + return `로컬 확인: 요청 ${requested}곡 / 이미 있음 ${existing}곡 / 새로 필요 ${missing}곡${invalid ? ` / ID 없음 ${invalid}곡` : ""}`; + } + + function showSyncSummary(body, data, fallbackRequested = 0) { + const syncSummary = body?.querySelector?.("[data-cgs-sync-summary]"); + const text = formatSyncSummary(data, fallbackRequested); + const requested = Number(data?.requested ?? data?.received ?? fallbackRequested ?? 0); + const existing = Number(data?.existingRequested ?? data?.existing ?? 0); + updateProgress(body, existing, requested, "로컬 확인"); + if (syncSummary) syncSummary.textContent = text; + return text; + } + function escapeHtml(value) { return String(value ?? "") .replaceAll("&", "&") @@ -1434,7 +1463,7 @@ throw new Error(syncText || `${syncResponse.status} ${syncResponse.statusText}`); } const syncData = tryParseJson(syncText) || {}; - const syncTextSummary = showSyncSummary(syncData, tracks.length); + const syncTextSummary = showSyncSummary(statusTarget, syncData, tracks.length); const missingTracks = Array.isArray(syncData.tracks) ? syncData.tracks : []; if (!missingTracks.length) { setStatus(statusTarget, `동기화 완료. ${syncTextSummary}`); @@ -1447,7 +1476,7 @@ const syncTotal = Number(syncData.requested ?? syncData.received ?? tracks.length); for (let index = 0; index < missingTracks.length; index += 1) { const track = { ...missingTracks[index] }; - updateProgress(syncExisting + enqueued, syncTotal, "WAV 큐 준비"); + updateProgress(statusTarget, syncExisting + enqueued, syncTotal, "WAV 큐 준비"); setStatus(statusTarget, `${index + 1}/${missingTracks.length} 없는 곡 WAV 변환 요청 중: ${track.title || track.id}`); track.wavUrl = await prepareSunoWavCandidate(track); if (!track.wavUrl) { @@ -1471,7 +1500,7 @@ throw new Error(text || `${response.status} ${response.statusText}`); } enqueued += 1; - updateProgress(syncExisting + enqueued, syncTotal, "WAV 큐 준비"); + updateProgress(statusTarget, syncExisting + enqueued, syncTotal, "WAV 큐 준비"); const data = tryParseJson(text) || {}; setStatus(statusTarget, `${enqueued}/${missingTracks.length}곡 큐 추가: ${track.title || track.id} / 대기 ${data.queued ?? "?"}개`); } @@ -1498,7 +1527,7 @@ const syncText = await syncResponse.text(); if (!syncResponse.ok) throw new Error(syncText || `${syncResponse.status} ${syncResponse.statusText}`); const syncData = tryParseJson(syncText) || {}; - const syncTextSummary = showSyncSummary(syncData, 1); + const syncTextSummary = showSyncSummary(statusTarget, syncData, 1); const missingTracks = Array.isArray(syncData.tracks) ? syncData.tracks : []; if (!missingTracks.length) { setStatus(statusTarget, `이미 로컬에 있음, SUNO 다운로드 생략: ${track.title || track.id}. ${syncTextSummary}`); @@ -1557,7 +1586,7 @@ const syncText = await syncResponse.text(); if (!syncResponse.ok) throw new Error(syncText || `${syncResponse.status} ${syncResponse.statusText}`); const syncData = tryParseJson(syncText) || {}; - const syncTextSummary = showSyncSummary(syncData, tracks.length); + const syncTextSummary = showSyncSummary(statusTarget, syncData, tracks.length); const missingTracks = Array.isArray(syncData.tracks) ? syncData.tracks : []; if (!missingTracks.length) { setStatus(statusTarget, `동기화 완료. ${syncTextSummary}`); @@ -1571,7 +1600,7 @@ for (let index = 0; index < missingTracks.length; index += 1) { const track = { ...missingTracks[index] }; try { - updateProgress(syncExisting + uploaded + failed, syncTotal, `${format.toUpperCase()} 다운로드`); + updateProgress(statusTarget, syncExisting + uploaded + failed, syncTotal, `${format.toUpperCase()} 다운로드`); setStatus(statusTarget, `${index + 1}/${missingTracks.length} ${format.toUpperCase()} 처리 중: ${track.title || track.id}`); await uploadTrackToCompanion(track, statusTarget, format); uploaded += 1; @@ -1580,7 +1609,7 @@ console.warn("[ChatGPT to Suno] upload failed", track, error); setStatus(statusTarget, `${track.title || track.id} 실패: ${error.message || error}`); } - updateProgress(syncExisting + uploaded + failed, syncTotal, `${format.toUpperCase()} 다운로드`); + updateProgress(statusTarget, syncExisting + uploaded + failed, syncTotal, `${format.toUpperCase()} 다운로드`); await new Promise((resolve) => setTimeout(resolve, 400)); } let healthText = ""; @@ -1708,9 +1737,6 @@ const summary = body.querySelector("[data-cgs-summary]"); const selectionSummary = body.querySelector("[data-cgs-selection-summary]"); - const syncSummary = body.querySelector("[data-cgs-sync-summary]"); - const progressLabel = body.querySelector("[data-cgs-progress-label]"); - const progressFill = body.querySelector("[data-cgs-progress-fill]"); const slotSelect = body.querySelector("[data-cgs-slot]"); const trackList = body.querySelector("[data-cgs-track-list]"); let libraryTracks = []; @@ -1718,32 +1744,8 @@ const selectedTrackIds = new Set(); let lastSelectedIndex = -1; let autoScanTimer = 0; - let progressState = { total: 0, done: 0, label: "진행" }; const getSelectedTracks = () => libraryTracks.filter((track) => selectedTrackIds.has(track.id)); - const updateProgress = (done, total, label = "진행") => { - const safeTotal = Math.max(0, Number(total || 0)); - const safeDone = Math.max(0, Math.min(Number(done || 0), safeTotal || Number(done || 0))); - progressState = { total: safeTotal, done: safeDone, label }; - const percent = safeTotal ? Math.round((safeDone / safeTotal) * 1000) / 10 : 0; - progressLabel.textContent = `${label} ${safeDone}/${safeTotal}${safeTotal ? ` (${percent}%)` : ""}`; - progressFill.style.width = `${safeTotal ? percent : 0}%`; - }; - const formatSyncSummary = (data, fallbackRequested = 0) => { - const requested = Number(data.requested ?? data.received ?? fallbackRequested ?? 0); - const existing = Number(data.existingRequested ?? data.existing ?? 0); - const missing = Number(data.missingRequested ?? data.missing ?? 0); - const invalid = Number(data.invalidRequested ?? 0); - return `로컬 확인: 요청 ${requested}곡 / 이미 있음 ${existing}곡 / 새로 필요 ${missing}곡${invalid ? ` / ID 없음 ${invalid}곡` : ""}`; - }; - const showSyncSummary = (data, fallbackRequested = 0) => { - const text = formatSyncSummary(data, fallbackRequested); - const requested = Number(data.requested ?? data.received ?? fallbackRequested ?? 0); - const existing = Number(data.existingRequested ?? data.existing ?? 0); - updateProgress(existing, requested, "로컬 확인"); - syncSummary.textContent = text; - return text; - }; const mergeLibraryTracks = (tracks) => { for (const track of tracks) { if (!track.id) continue;