From 96a54a3ecabfe05c768ecd45c5f142d23ca530dd Mon Sep 17 00:00:00 2001 From: "DESKTOP-KSVGT20\\shkim" Date: Fri, 1 May 2026 23:15:19 +0900 Subject: [PATCH] Queue Suno tracks one at a time --- README.md | 2 +- chatgpt-suno-tampermonkey.user.js | 45 ++++++------- suno-companion.ps1 | 104 +++++++++++++++++++++++++++++- 3 files changed, 125 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index 374f615..8e0216a 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,7 @@ WAV 다운로드는 SUNO 계정 권한, 변환 준비 상태, CDN 접근 정책 2. SUNO 라이브러리에서 `곡 감지`를 누릅니다. 3. `WAV + 메타데이터 다운`을 누릅니다. -이 경우 Tampermonkey가 manifest를 `http://127.0.0.1:17873/process`로 보내고, 로컬 프로그램이 자동으로 다운로드/FLAC 변환/정리를 시작합니다. +이 경우 Tampermonkey가 로컬 프로그램의 `sync-index`로 이미 저장된 clip id를 확인한 뒤, 없는 곡만 `enqueue`로 한 곡씩 보냅니다. 로컬 프로그램은 큐에 들어온 곡을 비동기로 순서대로 다운로드/FLAC 변환/정리합니다. 기본 출력 위치: diff --git a/chatgpt-suno-tampermonkey.user.js b/chatgpt-suno-tampermonkey.user.js index 7d84f55..4b60308 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.6.0 +// @version 0.7.0 // @description Copy structured ChatGPT song prompt versions into Suno fields. // @author local // @match https://chatgpt.com/* @@ -975,34 +975,31 @@ return; } - const preparedTracks = []; + let enqueued = 0; for (let index = 0; index < missingTracks.length; index += 1) { const track = { ...missingTracks[index] }; setStatus(statusTarget, `${index + 1}/${missingTracks.length} 없는 곡 WAV 준비 중: ${track.title || track.id}`); track.wavUrl = await requestSunoWavUrl(track); - preparedTracks.push(track); + const enqueueManifest = { + pageUrl: location.href, + capturedAt: new Date().toISOString(), + count: 1, + tracks: [track], + }; + const response = await fetch("http://127.0.0.1:17873/enqueue", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify(enqueueManifest), + }); + const text = await response.text(); + if (!response.ok) { + throw new Error(text || `${response.status} ${response.statusText}`); + } + enqueued += 1; + const data = tryParseJson(text) || {}; + setStatus(statusTarget, `${enqueued}/${missingTracks.length}곡 큐 추가: ${track.title || track.id} / 대기 ${data.queued ?? "?"}개`); } - const manifest = { - pageUrl: location.href, - capturedAt: new Date().toISOString(), - count: preparedTracks.length, - tracks: preparedTracks, - }; - const response = await fetch("http://127.0.0.1:17873/process", { - method: "POST", - headers: { "content-type": "application/json" }, - body: JSON.stringify(manifest), - }); - const text = await response.text(); - if (!response.ok) { - throw new Error(text || `${response.status} ${response.statusText}`); - } - const data = tryParseJson(text) || {}; - if (data.skipped) { - setStatus(statusTarget, "동기화 완료 상태: 처리할 새 곡 없음"); - return; - } - setStatus(statusTarget, `로컬 처리 시작: ${data.jobId || "job"} / 새 곡 ${preparedTracks.length}개`); + setStatus(statusTarget, `큐 전송 완료: 새 곡 ${enqueued}개. 로컬 프로그램이 순서대로 처리 중.`); } async function testCompanion(statusTarget) { diff --git a/suno-companion.ps1 b/suno-companion.ps1 index ea82710..d9d6ff8 100644 --- a/suno-companion.ps1 +++ b/suno-companion.ps1 @@ -12,6 +12,9 @@ $processor = Join-Path $scriptRoot "process-suno-library.ps1" $inbox = Join-Path $scriptRoot "companion-inbox" $logs = Join-Path $scriptRoot "companion-logs" New-Item -ItemType Directory -Force -Path $inbox, $logs | Out-Null +$queue = [System.Collections.Queue]::new() +$queueIds = [System.Collections.Generic.HashSet[string]]::new() +$activeJob = $null function Resolve-LibraryRoot { param( @@ -137,6 +140,80 @@ function Start-SunoProcess { } } +function Save-Manifest { + param($Manifest) + + $firstTrack = @($Manifest.tracks)[0] + $id = "$($firstTrack.id)".Trim() + $safeId = if ($id) { $id } else { Get-Date -Format "yyyyMMdd-HHmmssfff" } + $manifestPath = Join-Path $inbox ("suno-track-{0}.manifest.json" -f $safeId) + $Manifest | ConvertTo-Json -Depth 50 | Set-Content -LiteralPath $manifestPath -Encoding UTF8 + return $manifestPath +} + +function Update-Queue { + if ($script:activeJob -and -not $script:activeJob.Process.HasExited) { + return + } + + if ($script:activeJob -and $script:activeJob.Process.HasExited) { + Write-Host "Finished job $($script:activeJob.JobId) exit=$($script:activeJob.Process.ExitCode)" + $script:activeJob = $null + } + + while (-not $script:activeJob -and $script:queue.Count -gt 0) { + $item = $script:queue.Dequeue() + [void]$script:queueIds.Remove($item.Id) + $manifest = [pscustomobject]@{ + pageUrl = $item.PageUrl + capturedAt = (Get-Date).ToString("o") + count = 1 + tracks = @($item.Track) + } + $manifestPath = Save-Manifest $manifest + $job = Start-SunoProcess $manifestPath + $script:activeJob = [pscustomobject]@{ + JobId = $job.jobId + Process = Get-Process -Id $job.pid + TrackId = $item.Id + Title = $item.Title + Log = $job.log + ErrorLog = $job.errorLog + } + Write-Host "Started queued job $($job.jobId): $($item.Title) [$($item.Id)]" + } +} + +function Add-TracksToQueue { + param($Tracks, [string]$PageUrl) + + $sync = Get-MissingTracks $Tracks + $added = @() + $skipped = 0 + foreach ($track in @($sync.missing)) { + $id = "$($track.id)".Trim() + if (-not $id) { $skipped++; continue } + if ($queueIds.Contains($id)) { $skipped++; continue } + if ($activeJob -and $activeJob.TrackId -eq $id) { $skipped++; continue } + $title = if ($track.title) { "$($track.title)" } else { $id } + $queue.Enqueue([pscustomobject]@{ + Id = $id + Title = $title + Track = $track + PageUrl = $PageUrl + }) + [void]$queueIds.Add($id) + $added += $track + } + Update-Queue + return [pscustomobject]@{ + added = @($added).Count + skipped = $skipped + queued = $queue.Count + active = if ($activeJob) { $activeJob.Title } else { "" } + } +} + function Get-ExistingSunoIds { param([string]$Root) @@ -181,6 +258,7 @@ if (-not (Test-Path -LiteralPath $processor)) { $listener = [System.Net.Sockets.TcpListener]::new([System.Net.IPAddress]::Parse("127.0.0.1"), $Port) $listener.Start() +$listener.Server.ReceiveTimeout = 1000 Write-Host "SUNO companion listening on http://127.0.0.1:$Port" Write-Host "OutputRoot: $OutputRoot" @@ -190,6 +268,11 @@ Write-Host "Press Ctrl+C to stop." try { while ($true) { + Update-Queue + if (-not $listener.Pending()) { + Start-Sleep -Milliseconds 500 + continue + } $client = $listener.AcceptTcpClient() try { $stream = $client.GetStream() @@ -201,8 +284,9 @@ try { } if ($request.Method -eq "GET" -and $request.Path -eq "/health") { + Update-Queue Write-Host "Health check OK: $(Get-Date -Format 'HH:mm:ss')" - Write-Response $stream 200 "OK" (@{ ok = $true; outputRoot = $OutputRoot; libraryRoot = $libraryRoot; artist = $ArtistName } | ConvertTo-Json -Compress) + Write-Response $stream 200 "OK" (@{ ok = $true; outputRoot = $OutputRoot; libraryRoot = $libraryRoot; artist = $ArtistName; queued = $queue.Count; active = if ($activeJob) { $activeJob.Title } else { "" } } | ConvertTo-Json -Compress) continue } @@ -258,6 +342,24 @@ try { continue } + if ($request.Method -eq "POST" -and $request.Path -eq "/enqueue") { + $manifest = $request.Body | ConvertFrom-Json + if (-not $manifest.tracks -or @($manifest.tracks).Count -eq 0) { + Write-Response $stream 400 "Bad Request" (@{ ok = $false; error = "tracks is empty" } | ConvertTo-Json -Compress) + continue + } + $result = Add-TracksToQueue @($manifest.tracks) "$($manifest.pageUrl)" + Write-Host "Queue update: added=$($result.added), queued=$($result.queued), active=$($result.active)" + Write-Response $stream 200 "OK" (@{ + ok = $true + added = $result.added + skipped = $result.skipped + queued = $result.queued + active = $result.active + } | ConvertTo-Json -Compress) + continue + } + Write-Response $stream 404 "Not Found" (@{ ok = $false; error = "not found" } | ConvertTo-Json -Compress) } catch { try {