From 46b9be652da7a74d1d54c6827ac59227ac5d2063 Mon Sep 17 00:00:00 2001 From: "DESKTOP-KSVGT20\\shkim" Date: Sat, 2 May 2026 02:14:17 +0900 Subject: [PATCH] Add manual download watch mode --- chatgpt-suno-tampermonkey.user.js | 64 ++++++++++++++--- suno-companion.ps1 | 110 +++++++++++++++++++++++++++++- 2 files changed, 162 insertions(+), 12 deletions(-) diff --git a/chatgpt-suno-tampermonkey.user.js b/chatgpt-suno-tampermonkey.user.js index ce0dd26..bde8848 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.10.0 +// @version 0.10.1 // @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.10.0"; + const SCRIPT_VERSION = "0.10.1"; const STORAGE_KEY = "chatgptSunoPromptSlots"; const SLOT_COUNT = 2; const isChatGPT = location.hostname.includes("chatgpt.com") || location.hostname.includes("chat.openai.com"); @@ -1760,6 +1760,30 @@ setStatus(statusTarget, `${format.toUpperCase()} 일괄 업로드 완료: ${uploaded}곡 성공, ${failed}곡 실패. ${syncTextSummary}. 로컬에서 메타데이터 후처리 중${healthText}`); } + async function expectManualDownloads(tracks, statusTarget, format = "mp3") { + const selected = tracks.filter((track) => track?.id); + if (!selected.length) { + setStatus(statusTarget, "등록할 곡이 없어. 곡 감지 후 목록에서 선택해줘."); + return; + } + setStatus(statusTarget, `로컬 다운로드 감시 등록 중: ${selected.length}곡 ${format.toUpperCase()}`); + const response = await fetch("http://127.0.0.1:17873/expect-downloads", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ + pageUrl: location.href, + capturedAt: new Date().toISOString(), + format, + tracks: selected, + }), + }); + const text = await response.text(); + if (!response.ok) throw new Error(text || `${response.status} ${response.statusText}`); + const data = tryParseJson(text) || {}; + const root = data.downloadsRoot ? ` / 감시 폴더: ${data.downloadsRoot}` : ""; + setStatus(statusTarget, `${format.toUpperCase()} 수동 다운로드 대기 등록: 새 ${data.added || 0}곡, 이미 있음 ${data.existingRequested || 0}곡, 대기 ${data.pendingDownloads || 0}곡.${root} 이제 SUNO 원래 메뉴에서 ${format.toUpperCase()}를 직접 받아줘.`); + } + function trackOptionLabel(track, index) { const title = track?.title || track?.id || "제목 없음"; const shortId = track?.id ? String(track.id).slice(0, 8) : ""; @@ -1818,6 +1842,7 @@ if (!data || typeof data !== "object") return "상태 정보 없음"; const active = Number(data.activeCount || 0); const queued = Number(data.queued || 0); + const pendingDownloads = Number(data.pendingDownloads || 0); const done = Number(data.done ?? ((data.completed || 0) + (data.failed || 0))); const total = Number(data.total || 0); const failed = Number(data.failed || 0); @@ -1825,7 +1850,7 @@ const workerText = data.maxWorkers ? `${active}/${data.maxWorkers}` : `${active}`; const title = data.active || ""; const titleText = title ? ` / 처리중: ${title}` : ""; - return `작업 ${done}/${total} (${percent}%), 실행 ${workerText}, 대기 ${queued}, 실패 ${failed}${titleText}`; + return `작업 ${done}/${total} (${percent}%), 실행 ${workerText}, 큐 ${queued}, 다운로드대기 ${pendingDownloads}, 실패 ${failed}${titleText}`; } async function bootSuno() { @@ -1853,12 +1878,12 @@
- - + +
- - + +
@@ -2074,21 +2099,38 @@ }; body.querySelector("[data-cgs-process-one-mp3]").addEventListener("click", async () => { - await clickSelectedSunoUiDownloads("mp3", { one: true }); + await registerSelectedManualDownloads("mp3", { one: true }); }); body.querySelector("[data-cgs-process-one-wav]").addEventListener("click", async () => { - await clickSelectedSunoUiDownloads("wav", { one: true }); + await registerSelectedManualDownloads("wav", { one: true }); }); body.querySelector("[data-cgs-process-selected-mp3]").addEventListener("click", async () => { - await clickSelectedSunoUiDownloads("mp3"); + await registerSelectedManualDownloads("mp3"); }); body.querySelector("[data-cgs-process-selected-wav]").addEventListener("click", async () => { - await clickSelectedSunoUiDownloads("wav"); + await registerSelectedManualDownloads("wav"); }); + const registerSelectedManualDownloads = async (format, options = {}) => { + if (!libraryTracks.length) { + mergeLibraryTracks(scanSunoLibraryTracks()); + renderLibraryTrackList(); + } + const tracks = options.one ? (getSelectedTracks().slice(0, 1)) : getSelectedTracks(); + if (!tracks.length) { + setStatus(body, "선택된 곡이 없어. 목록에서 받을 곡을 먼저 선택해줘."); + return; + } + try { + await expectManualDownloads(tracks, body, format); + } catch (error) { + setStatus(body, `${format.toUpperCase()} 다운로드 대기 등록 실패: ${error.message || error}`); + } + }; + const clickSelectedSunoUiDownloads = async (format, options = {}) => { if (!libraryTracks.length) { mergeLibraryTracks(scanSunoLibraryTracks()); diff --git a/suno-companion.ps1 b/suno-companion.ps1 index e65812f..fe838bf 100644 --- a/suno-companion.ps1 +++ b/suno-companion.ps1 @@ -17,6 +17,8 @@ $queue = [System.Collections.Queue]::new() $queueIds = [System.Collections.Generic.HashSet[string]]::new() $activeJobs = [System.Collections.ArrayList]::new() $activeIds = [System.Collections.Generic.HashSet[string]]::new() +$pendingDownloads = [System.Collections.ArrayList]::new() +$claimedDownloadPaths = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) $stats = [ordered]@{ total = 0 completed = 0 @@ -238,6 +240,7 @@ function Get-QueueSnapshot { return [pscustomobject]@{ maxWorkers = $MaxWorkers queued = $script:queue.Count + pendingDownloads = $script:pendingDownloads.Count activeCount = $active.Count active = if ($active.Count) { ($active | Select-Object -First 1).title } else { "" } activeJobs = $active @@ -302,7 +305,7 @@ function Show-Dashboard { Write-Host "Library: $libraryRoot" 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 ("Workers: {0}/{1} Queue: {2} Waiting downloads: {3} Done: {4}/{5} Failed: {6} Progress: {7}%" -f $snapshot.activeCount, $snapshot.maxWorkers, $snapshot.queued, $snapshot.pendingDownloads, $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 "" @@ -439,6 +442,93 @@ function Save-UploadedTrack { return Add-TracksToQueue @($track) "$($Payload.pageUrl)" } +function Get-DownloadsRoot { + $path = Join-Path $env:USERPROFILE "Downloads" + if (Test-Path -LiteralPath $path) { return $path } + return "" +} + +function Test-StableDownloadFile { + param([System.IO.FileInfo]$File, [string]$Ext) + + if (-not $File -or -not $File.Exists) { return $false } + if ($File.Name -like "*.crdownload" -or $File.Name -like "*.tmp") { return $false } + if ($File.Extension.TrimStart(".").ToLowerInvariant() -ne $Ext) { return $false } + if ($File.Length -lt 131072) { return $false } + return ((Get-Date) - $File.LastWriteTime).TotalSeconds -ge 2 +} + +function Find-ExpectedDownloadFile { + param($Expected) + + $downloads = Get-DownloadsRoot + if (-not $downloads) { return $null } + $ext = "$($Expected.Format)".TrimStart(".").ToLowerInvariant() + $registeredAt = [DateTime]$Expected.RegisteredAt + $candidates = Get-ChildItem -LiteralPath $downloads -File -Filter "*.$ext" -ErrorAction SilentlyContinue | + Where-Object { + -not $script:claimedDownloadPaths.Contains($_.FullName) -and + $_.CreationTime -ge $registeredAt.AddSeconds(-10) -and + (Test-StableDownloadFile $_ $ext) + } | + Sort-Object CreationTime + + return @($candidates)[0] +} + +function Register-ExpectedDownloads { + param($Payload) + + $format = "$($Payload.format)".TrimStart(".").ToLowerInvariant() + if ($format -notin @("mp3", "wav")) { throw "format must be mp3 or wav" } + $tracks = @($Payload.tracks) + if (-not $tracks.Count) { throw "tracks is empty" } + + $missing = Get-MissingTracks $tracks + $added = 0 + foreach ($track in @($missing.missing)) { + $id = "$($track.id)".Trim() + if (-not $id) { continue } + $alreadyPending = @($script:pendingDownloads | Where-Object { $_.Id -eq $id -and $_.Format -eq $format }).Count -gt 0 + if ($alreadyPending -or $script:queueIds.Contains($id) -or $script:activeIds.Contains($id)) { continue } + [void]$script:pendingDownloads.Add([pscustomobject]@{ + Id = $id + Title = "$($track.title)" + Track = $track + Format = $format + PageUrl = "$($Payload.pageUrl)" + RegisteredAt = Get-Date + }) + $added++ + } + + return [pscustomobject]@{ + added = $added + pending = $script:pendingDownloads.Count + existingRequested = @($missing.existingRequested).Count + missingRequested = @($missing.missing).Count + invalidRequested = $missing.invalid + } +} + +function Update-ExpectedDownloads { + if (-not $script:pendingDownloads.Count) { return } + + for ($index = $script:pendingDownloads.Count - 1; $index -ge 0; $index--) { + $expected = $script:pendingDownloads[$index] + $file = Find-ExpectedDownloadFile $expected + if (-not $file) { continue } + + [void]$script:claimedDownloadPaths.Add($file.FullName) + $track = $expected.Track + $track | Add-Member -NotePropertyName "localAudioPath" -NotePropertyValue $file.FullName -Force + $track | Add-Member -NotePropertyName "localAudioExt" -NotePropertyValue $expected.Format -Force + $result = Add-TracksToQueue @($track) "$($expected.PageUrl)" + $script:pendingDownloads.RemoveAt($index) + Write-Host "Picked downloaded $($expected.Format): $($expected.Title) <- $($file.Name) queued=$($result.queued)" + } +} + function Get-ExistingSunoIds { param([string]$Root) @@ -501,6 +591,7 @@ Show-Dashboard -Force try { while ($true) { + Update-ExpectedDownloads Update-Queue if (-not $listener.Pending()) { Start-Sleep -Milliseconds 500 @@ -526,6 +617,7 @@ try { artist = $ArtistName queued = $snapshot.queued active = $snapshot.active + pendingDownloads = $snapshot.pendingDownloads activeCount = $snapshot.activeCount activeJobs = $snapshot.activeJobs next = $snapshot.next @@ -581,6 +673,22 @@ try { continue } + if ($request.Method -eq "POST" -and $request.Path -eq "/expect-downloads") { + $payload = $request.Body | ConvertFrom-Json + $result = Register-ExpectedDownloads $payload + Write-Host "Waiting for browser downloads: added=$($result.added), pending=$($result.pending), format=$($payload.format)" + Write-Response $stream 200 "OK" (@{ + ok = $true + added = $result.added + pendingDownloads = $result.pending + existingRequested = $result.existingRequested + missingRequested = $result.missingRequested + invalidRequested = $result.invalidRequested + downloadsRoot = Get-DownloadsRoot + } | ConvertTo-Json -Compress) + continue + } + if ($request.Method -eq "POST" -and $request.Path -eq "/process") { $manifest = $request.Body | ConvertFrom-Json if (-not $manifest.tracks -or @($manifest.tracks).Count -eq 0) {