Add Suno UI download click fallback

This commit is contained in:
DESKTOP-KSVGT20\shkim
2026-05-02 02:03:07 +09:00
parent 0e09a422bc
commit 12a27381e3
+122 -1
View File
@@ -30,7 +30,7 @@
(function () { (function () {
"use strict"; "use strict";
const SCRIPT_VERSION = "0.9.6"; const SCRIPT_VERSION = "0.9.7";
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");
@@ -861,6 +861,7 @@
normalized: key, normalized: key,
title: inferVisibleRowTitle(text), title: inferVisibleRowTitle(text),
ids: extractIdsFromText(mediaText), ids: extractIdsFromText(mediaText),
element: current,
}); });
} }
break; break;
@@ -940,6 +941,89 @@
.sort((a, b) => getTrackVisibleIndex(a, visibleRows) - getTrackVisibleIndex(b, visibleRows)); .sort((a, b) => getTrackVisibleIndex(a, visibleRows) - getTrackVisibleIndex(b, visibleRows));
} }
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
function clickLikeUser(node) {
if (!node) return false;
node.scrollIntoView?.({ block: "center", inline: "center" });
for (const type of ["pointerover", "mouseover", "mousemove", "pointerdown", "mousedown", "pointerup", "mouseup", "click"]) {
node.dispatchEvent(new MouseEvent(type, { bubbles: true, cancelable: true, view: window }));
}
return true;
}
function visibleClickableText(node) {
return String(node?.getAttribute?.("aria-label") || node?.innerText || node?.textContent || "").replace(/\s+/g, " ").trim();
}
function findVisibleButtonByText(labels, root = document) {
const needles = labels.map((label) => normalizeMatchText(label));
const buttons = [...root.querySelectorAll("button, [role='button'], [data-context-menu-trigger='true']")];
return buttons.find((button) => {
const rect = button.getBoundingClientRect();
if (rect.width < 8 || rect.height < 8 || rect.bottom < 0 || rect.top > window.innerHeight) return false;
const text = normalizeMatchText(visibleClickableText(button));
return needles.some((needle) => text === needle || text.includes(needle));
}) || null;
}
function findTrackRowElement(track) {
const rows = getVisibleLibraryRows();
const index = getTrackVisibleIndex(track, rows);
if (index >= 0) return rows[index].element;
const title = normalizeMatchText(track.title);
if (!title) return null;
return rows.find((row) => row.normalized.includes(title))?.element || null;
}
function findMoreButtonInRow(row) {
if (!row) return null;
const buttons = [...row.querySelectorAll("button, [role='button']")];
const labeled = buttons.find((button) => /more|options|menu|ellipsis|actions|더보기|옵션/i.test(visibleClickableText(button)));
if (labeled) return labeled;
const compact = buttons
.map((button) => ({ button, rect: button.getBoundingClientRect(), text: visibleClickableText(button) }))
.filter((item) => item.rect.width >= 18 && item.rect.width <= 70 && item.rect.height >= 18 && item.rect.height <= 70)
.sort((a, b) => b.rect.left - a.rect.left);
return compact[0]?.button || null;
}
async function waitForMenuButton(labels, timeoutMs = 2500) {
const started = Date.now();
while (Date.now() - started < timeoutMs) {
const button = findVisibleButtonByText(labels);
if (button) return button;
await sleep(100);
}
return null;
}
async function clickSunoUiDownload(track, format, statusTarget) {
const row = findTrackRowElement(track);
if (!row) throw new Error("현재 화면에서 곡 행을 못 찾았어. SUNO 목록에서 그 곡이 보이게 스크롤한 뒤 다시 눌러줘.");
const moreButton = findMoreButtonInRow(row);
if (!moreButton) throw new Error("곡의 ... 메뉴 버튼을 못 찾았어.");
setStatus(statusTarget, `SUNO UI 메뉴 여는 중: ${track.title || track.id}`);
clickLikeUser(moreButton);
const downloadButton = await waitForMenuButton(["Download", "다운로드"]);
if (!downloadButton) throw new Error("SUNO 메뉴에서 Download 항목을 못 찾았어.");
setStatus(statusTarget, `SUNO UI Download 여는 중: ${track.title || track.id}`);
clickLikeUser(downloadButton);
const labels = format === "wav"
? ["WAV Audio", "WAV", "Download WAV", "wav audio"]
: ["MP3 Audio", "MP3", "Download MP3", "mp3 audio"];
const formatButton = await waitForMenuButton(labels, 3000);
if (!formatButton) throw new Error(`SUNO Download 하위 메뉴에서 ${format.toUpperCase()} 항목을 못 찾았어.`);
setStatus(statusTarget, `SUNO UI ${format.toUpperCase()} 클릭: ${track.title || track.id}`);
clickLikeUser(formatButton);
return true;
}
function visibleSunoRowsHint() { function visibleSunoRowsHint() {
return getVisibleLibraryRows() return getVisibleLibraryRows()
.map((row) => row.text.split("\n").map((line) => line.trim()).filter(Boolean)[0]) .map((row) => row.text.split("\n").map((line) => line.trim()).filter(Boolean)[0])
@@ -1737,6 +1821,10 @@
<button class="cgs-btn cgs-primary cgs-wide" type="button" data-cgs-process-selected-mp3>선택항목 MP3</button> <button class="cgs-btn cgs-primary cgs-wide" type="button" data-cgs-process-selected-mp3>선택항목 MP3</button>
<button class="cgs-btn cgs-primary cgs-wide" type="button" data-cgs-process-selected-wav>선택항목 WAV</button> <button class="cgs-btn cgs-primary cgs-wide" type="button" data-cgs-process-selected-wav>선택항목 WAV</button>
</div> </div>
<div class="cgs-row">
<button class="cgs-btn cgs-wide" type="button" data-cgs-ui-download-mp3>SUNO UI MP3 클릭</button>
<button class="cgs-btn cgs-wide" type="button" data-cgs-ui-download-wav>SUNO UI WAV 클릭</button>
</div>
<div class="cgs-row"> <div class="cgs-row">
<button class="cgs-btn cgs-wide" type="button" data-cgs-test-companion>연결 테스트</button> <button class="cgs-btn cgs-wide" type="button" data-cgs-test-companion>연결 테스트</button>
<button class="cgs-btn cgs-wide" type="button" data-cgs-stop-companion>작업 중단</button> <button class="cgs-btn cgs-wide" type="button" data-cgs-stop-companion>작업 중단</button>
@@ -1962,6 +2050,39 @@
await processSelectedTracks("wav"); await processSelectedTracks("wav");
}); });
const clickSelectedSunoUiDownloads = async (format) => {
if (!libraryTracks.length) {
mergeLibraryTracks(scanSunoLibraryTracks());
renderLibraryTrackList();
}
const tracks = getSelectedTracks();
if (!tracks.length) {
setStatus(body, "선택된 곡이 없어. 목록에서 현재 화면에 보이는 곡을 먼저 선택해줘.");
return;
}
let clicked = 0;
for (const track of tracks) {
try {
await clickSunoUiDownload(track, format, body);
clicked += 1;
updateProgress(body, clicked, tracks.length, `SUNO UI ${format.toUpperCase()} 클릭`);
await sleep(1200);
} catch (error) {
setStatus(body, `SUNO UI ${format.toUpperCase()} 클릭 실패: ${track.title || track.id} / ${error.message || error}`);
return;
}
}
setStatus(body, `SUNO UI ${format.toUpperCase()} 클릭 완료: ${clicked}/${tracks.length}`);
};
body.querySelector("[data-cgs-ui-download-mp3]").addEventListener("click", async () => {
await clickSelectedSunoUiDownloads("mp3");
});
body.querySelector("[data-cgs-ui-download-wav]").addEventListener("click", async () => {
await clickSelectedSunoUiDownloads("wav");
});
const processLibraryTracks = async (format) => { const processLibraryTracks = async (format) => {
if (!libraryTracks.length) { if (!libraryTracks.length) {
mergeLibraryTracks(scanSunoLibraryTracks()); mergeLibraryTracks(scanSunoLibraryTracks());