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) {