-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcontent.js
More file actions
128 lines (109 loc) · 4.5 KB
/
Copy pathcontent.js
File metadata and controls
128 lines (109 loc) · 4.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
// content.js — runs on https://suno.com/create*
// Finds the "Styles" textarea, the Instrumental toggle and the "Create song"
// button, fills the prompt in and clicks Create. Controlled by messages sent
// from background.js.
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
function setNativeValue(el, value) {
const proto = Object.getPrototypeOf(el);
const descriptor = Object.getOwnPropertyDescriptor(proto, "value");
const setter = descriptor && descriptor.set;
if (setter) {
setter.call(el, value);
} else {
el.value = value;
}
el.dispatchEvent(new Event("input", { bubbles: true }));
el.dispatchEvent(new Event("change", { bubbles: true }));
}
function findFieldByLabel(labelText) {
// Find the leaf element that is exactly the section header ("Styles",
// "Lyrics", ...), then walk up through its ancestors until one of them
// contains a <textarea> — that's the section's own input box. This is
// resilient to hashed/rotating CSS class names.
const header = [...document.querySelectorAll("*")].find(
(el) => el.children.length === 0 && el.textContent.trim() === labelText
);
if (!header) return null;
let node = header;
for (let i = 0; i < 10 && node; i += 1) {
const ta = node.querySelector && node.querySelector("textarea");
if (ta) return ta;
node = node.parentElement;
}
return null;
}
function findStyleBox() {
// Primary: walk up from the "Styles" section header — robust regardless of
// the (rotating, sometimes bpm-less) example placeholder text.
const byLabel = findFieldByLabel("Styles");
if (byLabel) return byLabel;
// Fallback: the example placeholder usually mentions "bpm".
const textareas = [...document.querySelectorAll("textarea")];
return textareas.find((t) => (t.placeholder || "").toLowerCase().includes("bpm")) || null;
}
function findInstrumentalRadio() {
return [...document.querySelectorAll('[role="radio"]')].find(
(r) => r.textContent.trim().toLowerCase() === "instrumental"
);
}
function findCreateButton() {
return (
[...document.querySelectorAll("button")].find(
(b) => (b.getAttribute("aria-label") || "").trim().toLowerCase() === "create song"
) || [...document.querySelectorAll("button")].find((b) => b.textContent.trim().toLowerCase() === "create")
);
}
function getCreditsRemaining() {
const btn = [...document.querySelectorAll("button")].find((b) =>
(b.getAttribute("aria-label") || "").toLowerCase().startsWith("credits remaining")
);
if (!btn) return null;
const match = (btn.getAttribute("aria-label") || "").match(/(\d+)/);
return match ? parseInt(match[1], 10) : null;
}
async function handleGenerate(prompt, instrumental) {
const styleBox = findStyleBox();
if (!styleBox) {
throw new Error('Не знайдено поле "Styles" на сторінці. Переконайтесь, що відкрита вкладка Advanced.');
}
setNativeValue(styleBox, prompt);
await sleep(200);
if (instrumental) {
const radio = findInstrumentalRadio();
if (radio && radio.getAttribute("aria-checked") !== "true") {
radio.click();
await sleep(300);
}
}
await sleep(300); // give React time to enable the Create button
const createBtn = findCreateButton();
if (!createBtn) {
throw new Error('Не знайдено кнопку "Create".');
}
if (createBtn.disabled) {
throw new Error('Кнопка "Create" неактивна (можливо, забракло кредитів або поле Styles не прийнялось).');
}
createBtn.click();
await sleep(200);
return { credits: getCreditsRemaining() };
}
// background.js may inject this file again on every generation (to cover tabs
// that were already open before the extension was installed/reloaded, which
// never get the declarative content script). Guard against registering the
// message listener twice, which would otherwise double-click Create.
if (!window.__sunoAutoQueueLoaded) {
window.__sunoAutoQueueLoaded = true;
chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
if (message.action === "ping") {
sendResponse({ ok: true });
return undefined;
}
if (message.action !== "sunoGenerate") return undefined;
handleGenerate(message.prompt, message.instrumental)
.then((result) => sendResponse({ ok: true, credits: result.credits }))
.catch((err) => sendResponse({ ok: false, error: err.message }));
return true; // keep the message channel open for the async response
});
}