Skip to content

Commit 791db74

Browse files
authored
Add files via upload
1 parent a91e6a9 commit 791db74

File tree

2 files changed

+255
-0
lines changed

2 files changed

+255
-0
lines changed
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/* generated by pull.js */
2+
const manifest = {
3+
"name": "Newgrounds Audio Import",
4+
"description": "Import audio directly from Newgrounds into the sound editor."
5+
"credits": [
6+
{
7+
"name": "SharkPool",
8+
"link": "https://github.com/SharkPool-SP/"
9+
}
10+
],
11+
"info": [
12+
{
13+
"type": "notice",
14+
"text": "Some audio on Newgrounds may not be licensed for public or commercial use. This addon will try to warn you or block downloads when restrictions apply.",
15+
"id": "copyright-notice"
16+
}
17+
],
18+
"userscripts": [
19+
{
20+
"url": "userscript.js"
21+
}
22+
],
23+
"tags": ["editor", "new"],
24+
"enabledByDefault": true,
25+
"dynamicEnable": true,
26+
"dynamicDisable": false
27+
};
28+
export default manifest;
Lines changed: 227 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,227 @@
1+
// Newgrounds Audio Button
2+
// By: SharkPool
3+
// Thanks Tom Fulp! :)
4+
5+
export default async function() {
6+
const ngIcon = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9Ii0yLjUgLTIuNSAyNSAyNSIgd2lkdGg9IjIwIiBoZWlnaHQ9IjIwIj48cGF0aCBkPSJNNy4wNjkgMS43NzhhMi4wMiAyLjAyIDAgMCAxIDIuMDIgMi4wMnYxMy44MDZhLjg0Ljg0IDAgMCAxLS44NDIuODRINi4zOTZhLjg0Ljg0IDAgMCAxLS44NDItLjg0MlY2LjExOWEuODQuODQgMCAwIDAtLjg0Mi0uODQyaC0uMzM3YS44NC44NCAwIDAgMC0uODQxLjg0MnYxMS40ODRhLjg0Ljg0IDAgMCAxLS44NDEuODQxSC44NDJBLjg0Ljg0IDAgMCAxIDAgMTcuNjAzVjIuNjE5YS44NC44NCAwIDAgMSAuODQyLS44NDF6bTEwLjkxMiAwQTIuMDIgMi4wMiAwIDAgMSAyMCAzLjc5N3YzLjQ3NGEuNjczLjY3MyAwIDAgMS0uNjczLjY3M2gtMi4zNTZhLjY3My42NzMgMCAwIDEtLjY3My0uNjczVjUuNzgzYS41MDQuNTA0IDAgMCAwLS41MDUtLjUwNWgtLjg0MmEuNTA0LjUwNCAwIDAgMC0uNTA1LjUwNXY4LjY1OGMwIC4xODYuMTUxLjMzNy4zMzcuMzM3aDEuMzQ2YS4zMzYuMzM2IDAgMCAwIC4zMzYtLjMzN3YtMS42NjNoLS4zMzZhLjY3My42NzMgMCAwIDEtLjY3My0uNjczVjkuOTUyYS42NzMuNjczIDAgMCAxIC42NzMtLjY3M2gzLjE5OGEuNjczLjY3MyAwIDAgMSAuNjczLjY3MnY2LjQ3NGEyLjAyIDIuMDIgMCAwIDEtMi4wMiAyLjAxOWgtNS4wNDlhMi4wMiAyLjAyIDAgMCAxLTIuMDItMi4wMlYzLjc5N2EyLjAyIDIuMDIgMCAwIDEgMi4wMi0yLjAxOWg1LjA0OVoiIGZpbGw9IiNmZmYiLz48L3N2Zz4=";
7+
const safeIcon = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMjAuNzc0IiBoZWlnaHQ9IjEwNS45MDUiIHZpZXdCb3g9IjAgMCAxMjAuNzc0IDEwNS45MDUiPjxkZWZzPjxsaW5lYXJHcmFkaWVudCB4MT0iMjM5Ljg2IiB5MT0iMTMwLjA2IiB4Mj0iMjM5Ljg2IiB5Mj0iMjMyLjQ2NSIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiIGlkPSJhIj48c3RvcCBvZmZzZXQ9IjAiIHN0b3AtY29sb3I9IiMwMGZjMWQiLz48c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiMwMGI0MTYiLz48L2xpbmVhckdyYWRpZW50PjxsaW5lYXJHcmFkaWVudCB4MT0iMjM5Ljg2IiB5MT0iMTMwLjA2IiB4Mj0iMjM5Ljg2IiB5Mj0iMjMyLjQ2NSIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiIGlkPSJiIj48c3RvcCBvZmZzZXQ9IjAiIHN0b3AtY29sb3I9IiMwMGFlMTQiLz48c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiMwMDZkMGQiLz48L2xpbmVhckdyYWRpZW50PjwvZGVmcz48cGF0aCBkPSJtMjAwLjMgMTc0LjEwOCAyMi40NDYgMjAuNDgxIDU0LjcwOS02NC41MjkgMjEuMDQyIDE3LjM5NS03Mi45NDYgODUuMDEtNDQuMzI5LTM4Ljk5OHoiIGZpbGw9InVybCgjYSkiIHN0cm9rZT0idXJsKCNiKSIgc3Ryb2tlLXdpZHRoPSIzLjUiIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgc3Ryb2tlLW1pdGVybGltaXQ9IjEwIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtMTc5LjQ3MiAtMTI4LjMxKSIvPjwvc3ZnPg==";
8+
const warnIcon = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMjMuODYxIiBoZWlnaHQ9IjEwMC4yOTQiIHZpZXdCb3g9IjAgMCAxMjMuODYxIDEwMC4yOTQiPjxkZWZzPjxsaW5lYXJHcmFkaWVudCB4MT0iMjQwIiB5MT0iMTMwLjM0MSIgeDI9IjI0MCIgeTI9IjIyNy4xMzQiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIiBpZD0iYSI+PHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjZmZlYzEwIi8+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjZmZhYzBjIi8+PC9saW5lYXJHcmFkaWVudD48bGluZWFyR3JhZGllbnQgeDE9IjI0MCIgeTE9IjEzMC4zNDEiIHgyPSIyNDAiIHkyPSIyMjcuMTM0IiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgaWQ9ImIiPjxzdG9wIG9mZnNldD0iMCIgc3RvcC1jb2xvcj0iIzhiNGUwMiIvPjxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iIzgyMjcwMCIvPjwvbGluZWFyR3JhZGllbnQ+PGxpbmVhckdyYWRpZW50IHgxPSIyNDAiIHkxPSIxNTAuODkyIiB4Mj0iMjQwIiB5Mj0iMjIxLjQ1MyIgZ3JhZGllbnRVbml0cz0idXNlclNwYWNlT25Vc2UiIGlkPSJjIj48c3RvcCBvZmZzZXQ9IjAiIHN0b3AtY29sb3I9IiM4OTQ1MDEiLz48c3RvcCBvZmZzZXQ9IjEiIHN0b3AtY29sb3I9IiM4MjI5MDAiLz48L2xpbmVhckdyYWRpZW50PjwvZGVmcz48ZyBzdHJva2Utd2lkdGg9IjMuNSIgc3Ryb2tlLW1pdGVybGltaXQ9IjEwIj48cGF0aCBkPSJtMjQwIDEzMC4zNCA2MC4xOCA5Ni43OTRIMTc5LjgyeiIgZmlsbD0idXJsKCNhKSIgc3Ryb2tlPSJ1cmwoI2IpIiBzdHJva2UtbGluZWpvaW49InJvdW5kIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtMTc4LjA3IC0xMjguNTkpIi8+PHBhdGggZD0iTTIzMi41NjUgMjE0LjAxOGE3LjQzNSA3LjQzNSAwIDEgMSAxNC44NyAwIDcuNDM1IDcuNDM1IDAgMCAxLTE0Ljg3IDBtNy4xMjItMTIuOTA2YTYgNiAwIDAgMS02LTZ2LTM4LjIyYTYgNiAwIDAgMSA2LTZoLjYyNmE2IDYgMCAwIDEgNiA2djM4LjIyYTYgNiAwIDAgMS02IDZ6IiBmaWxsPSJ1cmwoI2MpIiB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtMTc4LjA3IC0xMjguNTkpIi8+PC9nPjwvc3ZnPg==";
9+
const unsafeIcon = "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxMjUuNTc4IiBoZWlnaHQ9IjEyNS44ODMiIHZpZXdCb3g9IjAgMCAxMjUuNTc4IDEyNS44ODMiPjxkZWZzPjxsaW5lYXJHcmFkaWVudCB4MT0iMjM5Ljg2IiB5MT0iMTE5LjY3OSIgeDI9IjIzOS44NiIgeTI9IjI0MC42MDEiIGdyYWRpZW50VW5pdHM9InVzZXJTcGFjZU9uVXNlIiBpZD0iYSI+PHN0b3Agb2Zmc2V0PSIwIiBzdG9wLWNvbG9yPSIjZmUwMDUwIi8+PHN0b3Agb2Zmc2V0PSIxIiBzdG9wLWNvbG9yPSIjOWIwMDA3Ii8+PC9saW5lYXJHcmFkaWVudD48bGluZWFyR3JhZGllbnQgeDE9IjIzOS44NiIgeTE9IjExOS42NzkiIHgyPSIyMzkuODYiIHkyPSIyNDAuNjAxIiBncmFkaWVudFVuaXRzPSJ1c2VyU3BhY2VPblVzZSIgaWQ9ImIiPjxzdG9wIG9mZnNldD0iMCIgc3RvcC1jb2xvcj0iIzdlMDAyNyIvPjxzdG9wIG9mZnNldD0iMSIgc3RvcC1jb2xvcj0iIzNiMDAwMSIvPjwvbGluZWFyR3JhZGllbnQ+PC9kZWZzPjxwYXRoIGQ9Im0xOTcuMjE0IDEyMC44MDIgNDIuNjQ2IDQxLjgwMyA0MS41MjMtNDIuOTI2IDE3Ljk1NiAxNy42NzYtNDEuNTIzIDQyLjM2NCA0Mi4zNjQgNDAuOTYyLTE3LjM5NCAxOC41MTctNDIuMDg1LTQxLjI0MkwxOTkuNDYgMjQwLjZsLTE4LjIzNy0xNy42NzVMMjIyLjE4NCAxODBsLTQyLjY0NS00MC45NjJ6IiBmaWxsPSJ1cmwoI2EpIiBzdHJva2U9InVybCgjYikiIHN0cm9rZS13aWR0aD0iMy41IiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1taXRlcmxpbWl0PSIxMCIgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoLTE3Ny4wNTkgLTExNy4xOTQpIi8+PC9zdmc+";
10+
11+
const proxy1 = "https://corsproxy.io?url=";
12+
const proxy2 = "https://api.codetabs.com/v1/proxy?quest=";
13+
14+
let ngButtonElement;
15+
16+
async function safeFetch(url, respondType) {
17+
const proxies = [proxy1, proxy2];
18+
for (const proxy of proxies) {
19+
try {
20+
const response = await fetch(proxy + url);
21+
22+
if (response.ok) return await response[respondType]();
23+
if (response.status === 400) return undefined;
24+
continue;
25+
} catch (e) {
26+
console.warn(`Failed to fetch ${url} with proxy: ${proxy}`, e);
27+
}
28+
}
29+
return undefined;
30+
}
31+
32+
async function addTrack2Editor(url, name) {
33+
const buffer = await safeFetch(url, "arrayBuffer");
34+
if (!buffer) {
35+
alert("Failed to Fetch Song!");
36+
return;
37+
}
38+
39+
const storage = vm.runtime.storage;
40+
const asset = storage.createAsset(
41+
storage.AssetType.Sound, storage.DataFormat.MP3,
42+
new Uint8Array(buffer), null, true
43+
);
44+
45+
try {
46+
await vm.addSound(
47+
{
48+
asset, name,
49+
md5: asset.assetId + "." + asset.dataFormat,
50+
},
51+
vm.editingTarget.id
52+
);
53+
} catch (e) {
54+
console.warn(e);
55+
}
56+
}
57+
58+
async function openNewgroundsPopup() {
59+
let url, name, songURL;
60+
let infoBox = undefined;
61+
62+
/* ScratchBlocks is availiable when this is called */
63+
const modal = await ScratchBlocks.customPrompt(
64+
{ title: "Newgrounds Audio" }, { content: { width: "500px" } },
65+
[
66+
{
67+
name: "Add Track", role: "ok", callback: () => {
68+
if (url && name && songURL) addTrack2Editor(songURL, `${name} -- ${author}`);
69+
}
70+
},
71+
{ name: "Cancel", role: "close", callback: () => {} }
72+
]
73+
);
74+
75+
const okayButton = modal.parentNode.querySelector(`button[class^="prompt_ok-button"]`);
76+
okayButton.style.filter = "brightness(70%)";
77+
okayButton.style.pointerEvents = "none";
78+
79+
const label = document.createElement("div");
80+
label.innerHTML = `Import <a href="https://www.newgrounds.com/audio" target="_blank">Newgrounds</a> audio directly into your Project. Not all tracks are fully free-to-use, read the report after searching.`;
81+
label.setAttribute("style", "text-align: center; font-size: .85rem;");
82+
83+
const idInputDiv = document.createElement("div");
84+
idInputDiv.setAttribute("style", "width: 100%; margin: 15px 0; padding: 10px 20px; border-radius: 15px; border: dashed 2px grey; text-align: center; font-size: .85rem;");
85+
86+
const idLabel = document.createElement("b");
87+
idLabel.textContent = "Track ID/URL: ";
88+
const idInput = document.createElement("input");
89+
90+
idInput.setAttribute("style", "margin-left: 5px; width: 70%; height: 25px; text-align: center; background: #ffffff20; border-radius: 15px; border: solid grey 1px;");
91+
idInput.type = "text";
92+
idInput.placeholder = "https://www.newgrounds.com/audio/listen/1395716";
93+
idInput.value = "1395716";
94+
url = idInput.placeholder;
95+
idInput.addEventListener("change", (e) => {
96+
url = String(e.target.value);
97+
if (!url.startsWith("https://www.newgrounds.com/audio/listen/")) url = "https://www.newgrounds.com/audio/listen/" + url;
98+
e.stopPropagation();
99+
});
100+
101+
const searchBtn = document.createElement("button");
102+
searchBtn.setAttribute("style", "border: none; border-radius: 5px; padding: 10px 20px; margin: 10px 0 0; background: hsla(194, 100%, 50%, 1); cursor: pointer; font-weight: 600; font-size: 0.85rem; color: white;");
103+
searchBtn.textContent = "Search";
104+
searchBtn.addEventListener("click", async (e) => {
105+
// unfortunately we have to scrape html here since the Newgrounds API is hidden
106+
const htmlText = await safeFetch(url, "text");
107+
if (!htmlText) {
108+
alert("Failed to Fetch Track URL!");
109+
return;
110+
}
111+
112+
/* extract info */
113+
author = htmlText.match(/"artist":"([^"]+)"/)?.[1] || "";
114+
name = htmlText.match(/<title>(.*?)<\/title>/i)?.[1] || "";
115+
116+
let songMatch = htmlText.match(
117+
/<meta property="og:audio" content="(https:\/\/audio\.ngfiles\.com\/[^"]+\.mp3\?[^"]+)">/
118+
);
119+
if (!songMatch) {
120+
const regex = new RegExp(
121+
`"params":\\{"filename":"(https:\\/\\/audio\\.ngfiles\\.com\\/[^"]+\\.mp3\\?[^"]+)"}`
122+
);
123+
songMatch = htmlText.match(regex);
124+
};
125+
songURL = songMatch ? songMatch[1].replace(/\\/g, "") : "";
126+
127+
/* song usage */
128+
// fun fact: we can check if a user is scouted if 'downloads' shows in their song!
129+
const isScouted = htmlText.match(/<dt>\s*Listens\s*<\/dt>[\s\S]*?<dd>\d+<\/dd>[\s\S]*?<dt>\s*Downloads\s*<\/dt>[\s\S]*?<dd>(\d+)<\/dd>[\s\S]*?<dt>\s*Score\s*<\/dt>/i);
130+
const ccLicense = htmlText.match(/<div class="pod-body creative-commons">[\s\S]*?<p>\s*([\s\S]*?)\s*<\/p>/i)?.[1] || "";
131+
const type = isScouted ? ccLicense2Rating(ccLicense) : "unwhitelisted";
132+
133+
if (infoBox) infoBox.remove();
134+
infoBox = genCopyrightInfoBox(type, name, author);
135+
if (type === "bad" || type === "unwhitelisted") {
136+
name = undefined;
137+
okayButton.style.filter = "brightness(70%)";
138+
okayButton.style.pointerEvents = "none";
139+
} else {
140+
okayButton.style.filter = "";
141+
okayButton.style.pointerEvents = "";
142+
}
143+
modal.appendChild(infoBox);
144+
e.stopPropagation();
145+
});
146+
147+
idInputDiv.append(idLabel, idInput, searchBtn);
148+
modal.append(label, idInputDiv);
149+
}
150+
151+
function ccLicense2Rating(licence) {
152+
licence = String(licence).toLowerCase().trim();
153+
const goodTexts = [
154+
`you may only use this piece for commercial purposes if your work is a web-based game or animation,`,
155+
];
156+
for (const text of goodTexts) {
157+
if (licence.startsWith(text)) return "good";
158+
}
159+
160+
const badTexts = [
161+
`you may not use this work for any purposes`,
162+
];
163+
for (const text of badTexts) {
164+
if (licence.startsWith(text)) return "bad";
165+
}
166+
167+
const warnTexts = [
168+
`you are free to copy, distribute and transmit this work under the following conditions:`,
169+
`please contact me if you would like to use this in a project. we can discuss the details.`,
170+
];
171+
for (const text of warnTexts) {
172+
if (licence.startsWith(text)) return "warn";
173+
}
174+
return "warn"; // warn is the default
175+
}
176+
177+
function genCopyrightInfoBox(type, name, author) {
178+
const color = type === "good" ? "#00ff00" : type === "bad" || type === "unwhitelisted" ? "#ff0000" : "#ffc400";
179+
const box = document.createElement("div");
180+
box.setAttribute("style", `display: flex; width: 100%; margin: 15px 0; padding: 10px 20px 10px 30px; border-radius: 15px; border: solid 2px ${color}; background: ${color}30; text-align: center; font-size: .9rem; font-weight: bold;`);
181+
182+
const img = document.createElement("img");
183+
img.setAttribute("style", "width:35px; margin-right: 5px;");
184+
img.src = type === "good" ? safeIcon : type === "bad" || type === "unwhitelisted" ? unsafeIcon : warnIcon;
185+
186+
const label = document.createElement("span");
187+
if (type === "good") label.innerHTML = `The Track: ${name} by ${author}, can freely be used for web-based games`;
188+
else if (type === "bad") label.innerHTML = `The Track: ${name} by ${author}, is not allowed for use!`;
189+
else if (type === "unwhitelisted") label.innerHTML = `The Track: ${name} by ${author}, is not allowed for use. ${author} is not scouted on Newgrounds!`;
190+
else label.innerHTML = `The Track: ${name} by ${author}, can only be used for non-profit web-based games WITH credit. Further use requires permission from ${author}`;
191+
192+
box.append(img, label);
193+
return box;
194+
}
195+
196+
function addButtonNG() {
197+
// TODO add a tooltip maybe
198+
const itemDiv = document.querySelector(`div[class^="action-menu_menu-container"] div[class^="action-menu_more-buttons-outer"] div[class^="action-menu_more-buttons"]`);
199+
200+
ngButtonElement = itemDiv.children[0].cloneNode(true);
201+
const innerButton = ngButtonElement.firstChild;
202+
innerButton.setAttribute("data-tip", "Newgrounds Sound");
203+
innerButton.setAttribute("aria-label", "Newgrounds Sound");
204+
/* cleanup */
205+
for (var i = 1; i < innerButton.children.length; i++) {
206+
const child = innerButton.children[i];
207+
if (child) child.remove();
208+
}
209+
innerButton.firstChild.src = ngIcon;
210+
ngButtonElement.addEventListener("click", openNewgroundsPopup);
211+
itemDiv.insertBefore(ngButtonElement, itemDiv.children[0]);
212+
}
213+
214+
function startListenerWorker() {
215+
ReduxStore.subscribe(() => queueMicrotask(() => {
216+
const reduxState = ReduxStore.getState().scratchGui;
217+
/* sound tab */
218+
if (!reduxState.mode.isPlayerOnly && reduxState.editorTab.activeTabIndex === 2) {
219+
if (!ngButtonElement) addButtonNG();
220+
} else {
221+
ngButtonElement = undefined;
222+
}
223+
}));
224+
}
225+
226+
if (typeof scaffolding === "undefined") startListenerWorker();
227+
}

0 commit comments

Comments
 (0)