diff --git a/chatgpt-suno-tampermonkey.user.js b/chatgpt-suno-tampermonkey.user.js index 0f42027..21f07c7 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.0 +// @version 0.9.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.9.0"; + const SCRIPT_VERSION = "0.9.1"; const STORAGE_KEY = "chatgptSunoPromptSlots"; const SLOT_COUNT = 2; const isChatGPT = location.hostname.includes("chatgpt.com") || location.hostname.includes("chat.openai.com"); @@ -726,6 +726,46 @@ .trim(); } + function isBadTrackTitle(value) { + const text = String(value || "").trim(); + if (!text) return true; + if (/^v\d+(?:\.\d+)*$/i.test(text)) return true; + if (/^\d+:\d{2}$/.test(text)) return true; + if (/^(liked|public|uploads|newest|songs|playlists)$/i.test(text)) return true; + return false; + } + + function extractIdsFromText(value) { + return unique([...String(value || "").matchAll(/[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}/gi)].map((match) => match[0])); + } + + function readNodeMediaText(node) { + const values = []; + let current = node; + for (let depth = 0; depth < 5 && current; depth += 1) { + if (current.currentSrc || current.src) values.push(current.currentSrc || current.src); + const style = current.getAttribute?.("style") || ""; + if (style) values.push(style); + current = current.parentElement; + } + return values.join("\n"); + } + + function inferVisibleRowTitle(text) { + const lines = String(text || "") + .split("\n") + .map((line) => line.trim()) + .filter(Boolean); + for (const line of lines) { + if (isBadTrackTitle(line)) continue; + if (/^(play|like|liked|public|uploads|newest|filters?)$/i.test(line)) continue; + if (/^\d+$/.test(line)) continue; + if (line.length > 90) continue; + return line; + } + return ""; + } + function getCurrentSunoUsername() { const text = document.body.innerText || ""; const match = /([A-Za-z0-9_.-]{3,40})\s+\d+\s+credits/i.exec(text); @@ -753,9 +793,12 @@ const key = normalizeMatchText(text); if (!seen.has(key)) { seen.add(key); + const mediaText = `${readNodeMediaText(node)}\n${current.innerHTML || ""}`; rows.push({ text, normalized: key, + title: inferVisibleRowTitle(text), + ids: extractIdsFromText(mediaText), }); } break; @@ -774,7 +817,10 @@ if (!rows.length) return -1; const title = normalizeMatchText(track.title); const style = normalizeMatchText(track.style || track.lyrics || ""); - if (!title || title.length > 160) return -1; + const id = String(track.id || ""); + const idIndex = rows.findIndex((row) => id && row.ids?.includes(id)); + if (idIndex >= 0) return idIndex; + if (!title || title.length > 160 || isBadTrackTitle(track.title)) return -1; return rows.findIndex((row) => { if (!row.normalized.includes(title)) return false; if (!style) return true; @@ -820,6 +866,10 @@ } const visibleRows = getVisibleLibraryRows(); + for (const track of tracks.values()) { + const row = visibleRows.find((item) => track.id && item.ids?.includes(track.id)); + if (row?.title && isBadTrackTitle(track.title)) track.title = row.title; + } const currentUser = getCurrentSunoUsername(); return [...tracks.values()] .filter((track) => track.id || track.audioUrl || track.imageUrl) @@ -1543,6 +1593,24 @@ return tryParseJson(text) || {}; } + async function stopCompanionWork(statusTarget) { + try { + const response = await fetch("http://127.0.0.1:17873/stop", { + method: "POST", + cache: "no-store", + }); + const text = await response.text(); + if (!response.ok) { + setStatus(statusTarget, `중단 실패: ${response.status} ${response.statusText}`); + return; + } + const data = tryParseJson(text) || {}; + setStatus(statusTarget, `작업 중단: 실행 ${data.stopped || 0}개 종료, 대기 ${data.dropped || 0}개 제거`); + } catch (error) { + setStatus(statusTarget, `중단 실패: companion 연결 확인 필요. ${error.message || error}`); + } + } + function formatCompanionStatus(data) { if (!data || typeof data !== "object") return "상태 정보 없음"; const active = Number(data.activeCount || 0); @@ -1586,6 +1654,7 @@
+
@@ -1851,6 +1920,10 @@ await testCompanion(body); }); + body.querySelector("[data-cgs-stop-companion]").addEventListener("click", async () => { + await stopCompanionWork(body); + }); + body.querySelector("[data-cgs-copy-diagnostics]").addEventListener("click", async () => { if (!libraryTracks.length) { mergeLibraryTracks(scanSunoLibraryTracks()); diff --git a/suno-companion.ps1 b/suno-companion.ps1 index 7088d65..9b91d54 100644 --- a/suno-companion.ps1 +++ b/suno-companion.ps1 @@ -251,6 +251,28 @@ function Get-QueueSnapshot { } } +function Stop-AllWork { + $stopped = 0 + foreach ($job in @($script:activeJobs)) { + try { + if ($job.Process -and -not $job.Process.HasExited) { + Stop-Process -Id $job.Process.Id -Force -ErrorAction Stop + $stopped++ + } + } catch {} + [void]$script:activeIds.Remove($job.TrackId) + } + $dropped = $script:queue.Count + $script:queue.Clear() + $script:queueIds.Clear() + $script:activeJobs.Clear() + Show-Dashboard -Force + return [pscustomobject]@{ + stopped = $stopped + dropped = $dropped + } +} + function Show-Dashboard { param([switch]$Force) @@ -496,6 +518,24 @@ try { continue } + if ($request.Method -eq "POST" -and $request.Path -eq "/stop") { + $result = Stop-AllWork + Write-Host "Stopped work: active=$($result.stopped), queued=$($result.dropped)" + $snapshot = Get-QueueSnapshot + Write-Response $stream 200 "OK" (@{ + ok = $true + stopped = $result.stopped + dropped = $result.dropped + queued = $snapshot.queued + activeCount = $snapshot.activeCount + total = $snapshot.total + completed = $snapshot.completed + failed = $snapshot.failed + percent = $snapshot.percent + } | ConvertTo-Json -Compress) + continue + } + if ($request.Method -eq "POST" -and $request.Path -eq "/sync-index") { $manifest = $request.Body | ConvertFrom-Json $result = Get-MissingTracks $manifest.tracks