|
| 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.<br><b>Not all tracks are fully free-to-use, read the report after searching.</b>`; |
| 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