Add Suno UI download click fallback
This commit is contained in:
@@ -30,7 +30,7 @@
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
const SCRIPT_VERSION = "0.9.6";
|
||||
const SCRIPT_VERSION = "0.9.7";
|
||||
const STORAGE_KEY = "chatgptSunoPromptSlots";
|
||||
const SLOT_COUNT = 2;
|
||||
const isChatGPT = location.hostname.includes("chatgpt.com") || location.hostname.includes("chat.openai.com");
|
||||
@@ -861,6 +861,7 @@
|
||||
normalized: key,
|
||||
title: inferVisibleRowTitle(text),
|
||||
ids: extractIdsFromText(mediaText),
|
||||
element: current,
|
||||
});
|
||||
}
|
||||
break;
|
||||
@@ -940,6 +941,89 @@
|
||||
.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() {
|
||||
return getVisibleLibraryRows()
|
||||
.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-wav>선택항목 WAV</button>
|
||||
</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">
|
||||
<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>
|
||||
@@ -1962,6 +2050,39 @@
|
||||
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) => {
|
||||
if (!libraryTracks.length) {
|
||||
mergeLibraryTracks(scanSunoLibraryTracks());
|
||||
|
||||
Reference in New Issue
Block a user