Show sync counts and progress bars

This commit is contained in:
DESKTOP-KSVGT20\shkim
2026-05-02 01:48:35 +09:00
parent 8efd24a50a
commit c79d11aa47
2 changed files with 103 additions and 10 deletions
+72 -7
View File
@@ -1,7 +1,7 @@
// ==UserScript== // ==UserScript==
// @name ChatGPT to Suno Prompt Copier // @name ChatGPT to Suno Prompt Copier
// @namespace local.suno-helper // @namespace local.suno-helper
// @version 0.9.1 // @version 0.9.3
// @description Copy structured ChatGPT song prompt versions into Suno fields. // @description Copy structured ChatGPT song prompt versions into Suno fields.
// @author local // @author local
// @match https://chatgpt.com/* // @match https://chatgpt.com/*
@@ -30,7 +30,7 @@
(function () { (function () {
"use strict"; "use strict";
const SCRIPT_VERSION = "0.9.1"; const SCRIPT_VERSION = "0.9.3";
const STORAGE_KEY = "chatgptSunoPromptSlots"; const STORAGE_KEY = "chatgptSunoPromptSlots";
const SLOT_COUNT = 2; const SLOT_COUNT = 2;
const isChatGPT = location.hostname.includes("chatgpt.com") || location.hostname.includes("chat.openai.com"); const isChatGPT = location.hostname.includes("chatgpt.com") || location.hostname.includes("chat.openai.com");
@@ -246,6 +246,28 @@
font-size: 11px; font-size: 11px;
white-space: nowrap; 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 = "") { function createPanel(title, mode = "") {
@@ -1412,16 +1434,20 @@
throw new Error(syncText || `${syncResponse.status} ${syncResponse.statusText}`); throw new Error(syncText || `${syncResponse.status} ${syncResponse.statusText}`);
} }
const syncData = tryParseJson(syncText) || {}; const syncData = tryParseJson(syncText) || {};
const syncTextSummary = showSyncSummary(syncData, tracks.length);
const missingTracks = Array.isArray(syncData.tracks) ? syncData.tracks : []; const missingTracks = Array.isArray(syncData.tracks) ? syncData.tracks : [];
if (!missingTracks.length) { if (!missingTracks.length) {
setStatus(statusTarget, `동기화 완료 상태: ${tracks.length}곡 모두 로컬에 있음`); setStatus(statusTarget, `동기화 완료. ${syncTextSummary}`);
return; return;
} }
let enqueued = 0; let enqueued = 0;
let wavPending = 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) { for (let index = 0; index < missingTracks.length; index += 1) {
const track = { ...missingTracks[index] }; const track = { ...missingTracks[index] };
updateProgress(syncExisting + enqueued, syncTotal, "WAV 큐 준비");
setStatus(statusTarget, `${index + 1}/${missingTracks.length} 없는 곡 WAV 변환 요청 중: ${track.title || track.id}`); setStatus(statusTarget, `${index + 1}/${missingTracks.length} 없는 곡 WAV 변환 요청 중: ${track.title || track.id}`);
track.wavUrl = await prepareSunoWavCandidate(track); track.wavUrl = await prepareSunoWavCandidate(track);
if (!track.wavUrl) { if (!track.wavUrl) {
@@ -1445,6 +1471,7 @@
throw new Error(text || `${response.status} ${response.statusText}`); throw new Error(text || `${response.status} ${response.statusText}`);
} }
enqueued += 1; enqueued += 1;
updateProgress(syncExisting + enqueued, syncTotal, "WAV 큐 준비");
const data = tryParseJson(text) || {}; const data = tryParseJson(text) || {};
setStatus(statusTarget, `${enqueued}/${missingTracks.length}곡 큐 추가: ${track.title || track.id} / 대기 ${data.queued ?? "?"}`); setStatus(statusTarget, `${enqueued}/${missingTracks.length}곡 큐 추가: ${track.title || track.id} / 대기 ${data.queued ?? "?"}`);
} }
@@ -1452,7 +1479,7 @@
setStatus(statusTarget, `큐에 넣은 곡 없음. 새 곡 ${missingTracks.length}개 중 ${wavPending}개가 WAV 후보 URL 생성 실패.`); setStatus(statusTarget, `큐에 넣은 곡 없음. 새 곡 ${missingTracks.length}개 중 ${wavPending}개가 WAV 후보 URL 생성 실패.`);
return; return;
} }
setStatus(statusTarget, `큐 전송 완료: 새 곡 ${enqueued}개, 보류 ${wavPending}개. 로컬 프로그램이 WAV 준비를 기다리며 처리 중.`); setStatus(statusTarget, `큐 전송 완료: 새 곡 ${enqueued}개, 보류 ${wavPending}개. ${syncTextSummary}`);
} }
async function uploadTrackToCompanion(track, statusTarget, format = "mp3") { async function uploadTrackToCompanion(track, statusTarget, format = "mp3") {
@@ -1471,9 +1498,10 @@
const syncText = await syncResponse.text(); const syncText = await syncResponse.text();
if (!syncResponse.ok) throw new Error(syncText || `${syncResponse.status} ${syncResponse.statusText}`); if (!syncResponse.ok) throw new Error(syncText || `${syncResponse.status} ${syncResponse.statusText}`);
const syncData = tryParseJson(syncText) || {}; const syncData = tryParseJson(syncText) || {};
const syncTextSummary = showSyncSummary(syncData, 1);
const missingTracks = Array.isArray(syncData.tracks) ? syncData.tracks : []; const missingTracks = Array.isArray(syncData.tracks) ? syncData.tracks : [];
if (!missingTracks.length) { if (!missingTracks.length) {
setStatus(statusTarget, `이미 로컬에 있음, SUNO 다운로드 생략: ${track.title || track.id}`); setStatus(statusTarget, `이미 로컬에 있음, SUNO 다운로드 생략: ${track.title || track.id}. ${syncTextSummary}`);
return { return {
ok: true, ok: true,
skipped: true, skipped: true,
@@ -1529,17 +1557,21 @@
const syncText = await syncResponse.text(); const syncText = await syncResponse.text();
if (!syncResponse.ok) throw new Error(syncText || `${syncResponse.status} ${syncResponse.statusText}`); if (!syncResponse.ok) throw new Error(syncText || `${syncResponse.status} ${syncResponse.statusText}`);
const syncData = tryParseJson(syncText) || {}; const syncData = tryParseJson(syncText) || {};
const syncTextSummary = showSyncSummary(syncData, tracks.length);
const missingTracks = Array.isArray(syncData.tracks) ? syncData.tracks : []; const missingTracks = Array.isArray(syncData.tracks) ? syncData.tracks : [];
if (!missingTracks.length) { if (!missingTracks.length) {
setStatus(statusTarget, `동기화 완료 상태: ${tracks.length}곡 모두 로컬에 있음`); setStatus(statusTarget, `동기화 완료. ${syncTextSummary}`);
return; return;
} }
let uploaded = 0; let uploaded = 0;
let failed = 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) { for (let index = 0; index < missingTracks.length; index += 1) {
const track = { ...missingTracks[index] }; const track = { ...missingTracks[index] };
try { try {
updateProgress(syncExisting + uploaded + failed, syncTotal, `${format.toUpperCase()} 다운로드`);
setStatus(statusTarget, `${index + 1}/${missingTracks.length} ${format.toUpperCase()} 처리 중: ${track.title || track.id}`); setStatus(statusTarget, `${index + 1}/${missingTracks.length} ${format.toUpperCase()} 처리 중: ${track.title || track.id}`);
await uploadTrackToCompanion(track, statusTarget, format); await uploadTrackToCompanion(track, statusTarget, format);
uploaded += 1; uploaded += 1;
@@ -1548,13 +1580,14 @@
console.warn("[ChatGPT to Suno] upload failed", track, error); console.warn("[ChatGPT to Suno] upload failed", track, error);
setStatus(statusTarget, `${track.title || track.id} 실패: ${error.message || error}`); setStatus(statusTarget, `${track.title || track.id} 실패: ${error.message || error}`);
} }
updateProgress(syncExisting + uploaded + failed, syncTotal, `${format.toUpperCase()} 다운로드`);
await new Promise((resolve) => setTimeout(resolve, 400)); await new Promise((resolve) => setTimeout(resolve, 400));
} }
let healthText = ""; let healthText = "";
try { try {
healthText = ` / ${formatCompanionStatus(await readCompanionHealth())}`; healthText = ` / ${formatCompanionStatus(await readCompanionHealth())}`;
} catch {} } catch {}
setStatus(statusTarget, `${format.toUpperCase()} 일괄 업로드 완료: ${uploaded}곡 성공, ${failed}곡 실패. 로컬에서 메타데이터 후처리 중${healthText}`); setStatus(statusTarget, `${format.toUpperCase()} 일괄 업로드 완료: ${uploaded}곡 성공, ${failed}곡 실패. ${syncTextSummary}. 로컬에서 메타데이터 후처리 중${healthText}`);
} }
function trackOptionLabel(track, index) { function trackOptionLabel(track, index) {
@@ -1643,6 +1676,11 @@
<button class="cgs-btn cgs-wide" type="button" data-cgs-scan-library>곡 감지</button> <button class="cgs-btn cgs-wide" type="button" data-cgs-scan-library>곡 감지</button>
</div> </div>
<div class="cgs-muted" data-cgs-selection-summary></div> <div class="cgs-muted" data-cgs-selection-summary></div>
<div class="cgs-muted" data-cgs-sync-summary></div>
<div class="cgs-progress-wrap">
<div class="cgs-progress-label" data-cgs-progress-label>진행 0/0</div>
<div class="cgs-progress"><div class="cgs-progress-fill" data-cgs-progress-fill></div></div>
</div>
<div class="cgs-track-list" data-cgs-track-list></div> <div class="cgs-track-list" data-cgs-track-list></div>
<div class="cgs-row"> <div class="cgs-row">
<button class="cgs-btn cgs-wide" type="button" data-cgs-process-one-mp3>선택 1곡 MP3</button> <button class="cgs-btn cgs-wide" type="button" data-cgs-process-one-mp3>선택 1곡 MP3</button>
@@ -1670,6 +1708,9 @@
const summary = body.querySelector("[data-cgs-summary]"); const summary = body.querySelector("[data-cgs-summary]");
const selectionSummary = body.querySelector("[data-cgs-selection-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 slotSelect = body.querySelector("[data-cgs-slot]");
const trackList = body.querySelector("[data-cgs-track-list]"); const trackList = body.querySelector("[data-cgs-track-list]");
let libraryTracks = []; let libraryTracks = [];
@@ -1677,8 +1718,32 @@
const selectedTrackIds = new Set(); const selectedTrackIds = new Set();
let lastSelectedIndex = -1; let lastSelectedIndex = -1;
let autoScanTimer = 0; let autoScanTimer = 0;
let progressState = { total: 0, done: 0, label: "진행" };
const getSelectedTracks = () => libraryTracks.filter((track) => selectedTrackIds.has(track.id)); 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) => { const mergeLibraryTracks = (tracks) => {
for (const track of tracks) { for (const track of tracks) {
if (!track.id) continue; if (!track.id) continue;
+31 -3
View File
@@ -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 { function Stop-AllWork {
$stopped = 0 $stopped = 0
foreach ($job in @($script:activeJobs)) { foreach ($job in @($script:activeJobs)) {
@@ -288,6 +303,7 @@ function Show-Dashboard {
Write-Host "Logs: $logs" Write-Host "Logs: $logs"
Write-Host "" 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} 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 "" Write-Host ""
if ($snapshot.activeJobs.Count) { if ($snapshot.activeJobs.Count) {
@@ -381,7 +397,8 @@ function Add-TracksToQueue {
[void]$queueIds.Add($id) [void]$queueIds.Add($id)
$added += $track $added += $track
} }
$script:stats.total += @($added).Count $script:stats.total += (@($added).Count + $skipped)
$script:stats.completed += $skipped
$script:stats.skipped += $skipped $script:stats.skipped += $skipped
Update-Queue Update-Queue
$snapshot = Get-QueueSnapshot $snapshot = Get-QueueSnapshot
@@ -446,17 +463,23 @@ function Get-MissingTracks {
$existingIds = Get-ExistingSunoIds $libraryRoot $existingIds = Get-ExistingSunoIds $libraryRoot
$missing = @() $missing = @()
$existingRequested = @()
$invalid = 0
foreach ($track in @($Tracks)) { foreach ($track in @($Tracks)) {
$id = "$($track.id)".Trim() $id = "$($track.id)".Trim()
if (-not $id) { continue } if (-not $id) { $invalid++; continue }
if (-not $existingIds.Contains($id)) { if (-not $existingIds.Contains($id)) {
$missing += $track $missing += $track
} else {
$existingRequested += $track
} }
} }
return [pscustomobject]@{ return [pscustomobject]@{
existingIds = @($existingIds) existingIds = @($existingIds)
existingRequested = $existingRequested
missing = $missing missing = $missing
invalid = $invalid
} }
} }
@@ -545,8 +568,13 @@ try {
outputRoot = $OutputRoot outputRoot = $OutputRoot
libraryRoot = $libraryRoot libraryRoot = $libraryRoot
artist = $ArtistName artist = $ArtistName
requested = @($manifest.tracks).Count
received = @($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 missing = @($result.missing).Count
tracks = @($result.missing) tracks = @($result.missing)
} | ConvertTo-Json -Depth 50 -Compress) } | ConvertTo-Json -Depth 50 -Compress)