diff --git a/chatgpt-suno-tampermonkey.user.js b/chatgpt-suno-tampermonkey.user.js index 21f07c7..713a6b4 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.1 +// @version 0.9.3 // @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.1"; + const SCRIPT_VERSION = "0.9.3"; const STORAGE_KEY = "chatgptSunoPromptSlots"; const SLOT_COUNT = 2; const isChatGPT = location.hostname.includes("chatgpt.com") || location.hostname.includes("chat.openai.com"); @@ -246,6 +246,28 @@ font-size: 11px; white-space: nowrap; } + .cgs-progress-wrap { + margin-top: 8px; + display: grid; + gap: 4px; + } + .cgs-progress-label { + color: #b7c9ff; + font-size: 12px; + } + .cgs-progress { + height: 10px; + overflow: hidden; + border-radius: 999px; + background: rgba(255, 255, 255, 0.12); + border: 1px solid rgba(255, 255, 255, 0.12); + } + .cgs-progress-fill { + height: 100%; + width: 0%; + background: linear-gradient(90deg, #2f7cf6, #5db6ff); + transition: width 180ms ease; + } `); function createPanel(title, mode = "") { @@ -1412,16 +1434,20 @@ throw new Error(syncText || `${syncResponse.status} ${syncResponse.statusText}`); } const syncData = tryParseJson(syncText) || {}; + const syncTextSummary = showSyncSummary(syncData, tracks.length); const missingTracks = Array.isArray(syncData.tracks) ? syncData.tracks : []; if (!missingTracks.length) { - setStatus(statusTarget, `동기화 완료 상태: ${tracks.length}곡 모두 로컬에 있음`); + setStatus(statusTarget, `동기화 완료. ${syncTextSummary}`); return; } let enqueued = 0; let wavPending = 0; + const syncExisting = Number(syncData.existingRequested ?? syncData.existing ?? 0); + 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 큐 준비"); setStatus(statusTarget, `${index + 1}/${missingTracks.length} 없는 곡 WAV 변환 요청 중: ${track.title || track.id}`); track.wavUrl = await prepareSunoWavCandidate(track); if (!track.wavUrl) { @@ -1445,6 +1471,7 @@ throw new Error(text || `${response.status} ${response.statusText}`); } enqueued += 1; + updateProgress(syncExisting + enqueued, syncTotal, "WAV 큐 준비"); const data = tryParseJson(text) || {}; setStatus(statusTarget, `${enqueued}/${missingTracks.length}곡 큐 추가: ${track.title || track.id} / 대기 ${data.queued ?? "?"}개`); } @@ -1452,7 +1479,7 @@ setStatus(statusTarget, `큐에 넣은 곡 없음. 새 곡 ${missingTracks.length}개 중 ${wavPending}개가 WAV 후보 URL 생성 실패.`); return; } - setStatus(statusTarget, `큐 전송 완료: 새 곡 ${enqueued}개, 보류 ${wavPending}개. 로컬 프로그램이 WAV 준비를 기다리며 처리 중.`); + setStatus(statusTarget, `큐 전송 완료: 새 곡 ${enqueued}개, 보류 ${wavPending}개. ${syncTextSummary}`); } async function uploadTrackToCompanion(track, statusTarget, format = "mp3") { @@ -1471,9 +1498,10 @@ 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 missingTracks = Array.isArray(syncData.tracks) ? syncData.tracks : []; if (!missingTracks.length) { - setStatus(statusTarget, `이미 로컬에 있음, SUNO 다운로드 생략: ${track.title || track.id}`); + setStatus(statusTarget, `이미 로컬에 있음, SUNO 다운로드 생략: ${track.title || track.id}. ${syncTextSummary}`); return { ok: true, skipped: true, @@ -1529,17 +1557,21 @@ 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 missingTracks = Array.isArray(syncData.tracks) ? syncData.tracks : []; if (!missingTracks.length) { - setStatus(statusTarget, `동기화 완료 상태: ${tracks.length}곡 모두 로컬에 있음`); + setStatus(statusTarget, `동기화 완료. ${syncTextSummary}`); return; } let uploaded = 0; let failed = 0; + const syncExisting = Number(syncData.existingRequested ?? syncData.existing ?? 0); + const syncTotal = Number(syncData.requested ?? syncData.received ?? tracks.length); for (let index = 0; index < missingTracks.length; index += 1) { const track = { ...missingTracks[index] }; try { + updateProgress(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; @@ -1548,13 +1580,14 @@ 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()} 다운로드`); await new Promise((resolve) => setTimeout(resolve, 400)); } let healthText = ""; try { healthText = ` / ${formatCompanionStatus(await readCompanionHealth())}`; } catch {} - setStatus(statusTarget, `${format.toUpperCase()} 일괄 업로드 완료: ${uploaded}곡 성공, ${failed}곡 실패. 로컬에서 메타데이터 후처리 중${healthText}`); + setStatus(statusTarget, `${format.toUpperCase()} 일괄 업로드 완료: ${uploaded}곡 성공, ${failed}곡 실패. ${syncTextSummary}. 로컬에서 메타데이터 후처리 중${healthText}`); } function trackOptionLabel(track, index) { @@ -1643,6 +1676,11 @@
+
+
+
진행 0/0
+
+
@@ -1670,6 +1708,9 @@ 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 = []; @@ -1677,8 +1718,32 @@ 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; diff --git a/suno-companion.ps1 b/suno-companion.ps1 index 9b91d54..e65812f 100644 --- a/suno-companion.ps1 +++ b/suno-companion.ps1 @@ -251,6 +251,21 @@ function Get-QueueSnapshot { } } +function Format-ProgressBar { + param( + [int]$Done, + [int]$Total, + [int]$Width = 28 + ) + + if ($Total -le 0) { + return "[" + ("-" * $Width) + "]" + } + $ratio = [Math]::Max(0, [Math]::Min(1, $Done / $Total)) + $filled = [int][Math]::Round($ratio * $Width) + return "[" + ("#" * $filled) + ("-" * ($Width - $filled)) + "]" +} + function Stop-AllWork { $stopped = 0 foreach ($job in @($script:activeJobs)) { @@ -288,6 +303,7 @@ function Show-Dashboard { Write-Host "Logs: $logs" Write-Host "" Write-Host ("Workers: {0}/{1} Queue: {2} Done: {3}/{4} Failed: {5} Progress: {6}%" -f $snapshot.activeCount, $snapshot.maxWorkers, $snapshot.queued, $snapshot.done, $snapshot.total, $snapshot.failed, $snapshot.percent) + Write-Host ("{0} {1}/{2}" -f (Format-ProgressBar $snapshot.done $snapshot.total), $snapshot.done, $snapshot.total) -ForegroundColor Cyan Write-Host "" if ($snapshot.activeJobs.Count) { @@ -381,7 +397,8 @@ function Add-TracksToQueue { [void]$queueIds.Add($id) $added += $track } - $script:stats.total += @($added).Count + $script:stats.total += (@($added).Count + $skipped) + $script:stats.completed += $skipped $script:stats.skipped += $skipped Update-Queue $snapshot = Get-QueueSnapshot @@ -446,17 +463,23 @@ function Get-MissingTracks { $existingIds = Get-ExistingSunoIds $libraryRoot $missing = @() + $existingRequested = @() + $invalid = 0 foreach ($track in @($Tracks)) { $id = "$($track.id)".Trim() - if (-not $id) { continue } + if (-not $id) { $invalid++; continue } if (-not $existingIds.Contains($id)) { $missing += $track + } else { + $existingRequested += $track } } return [pscustomobject]@{ existingIds = @($existingIds) + existingRequested = $existingRequested missing = $missing + invalid = $invalid } } @@ -545,8 +568,13 @@ try { outputRoot = $OutputRoot libraryRoot = $libraryRoot artist = $ArtistName + requested = @($manifest.tracks).Count received = @($manifest.tracks).Count - existing = @($result.existingIds).Count + libraryExistingTotal = @($result.existingIds).Count + existingRequested = @($result.existingRequested).Count + missingRequested = @($result.missing).Count + invalidRequested = $result.invalid + existing = @($result.existingRequested).Count missing = @($result.missing).Count tracks = @($result.missing) } | ConvertTo-Json -Depth 50 -Compress)