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 ( / " a r t i s t " : " ( [ ^ " ] + ) " / ) ?. [ 1 ] || "" ;
114
+ name = htmlText . match ( / < t i t l e > ( .* ?) < \/ t i t l e > / i) ?. [ 1 ] || "" ;
115
+
116
+ let songMatch = htmlText . match (
117
+ / < m e t a p r o p e r t y = " o g : a u d i o " c o n t e n t = " ( h t t p s : \/ \/ a u d i o \. n g f i l e s \. c o m \/ [ ^ " ] + \. m p 3 \? [ ^ " ] + ) " > /
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 ( / < d t > \s * L i s t e n s \s * < \/ d t > [ \s \S ] * ?< d d > \d + < \/ d d > [ \s \S ] * ?< d t > \s * D o w n l o a d s \s * < \/ d t > [ \s \S ] * ?< d d > ( \d + ) < \/ d d > [ \s \S ] * ?< d t > \s * S c o r e \s * < \/ d t > / i) ;
130
+ const ccLicense = htmlText . match ( / < d i v c l a s s = " p o d - b o d y c r e a t i v e - c o m m o n s " > [ \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