Created
November 23, 2024 08:15
-
-
Save cf/4a7fd6a7a3e93bdcc7c36d9685320b6f to your computer and use it in GitHub Desktop.
SoundCloud Backend for ShaderToy
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// DO NOT use this for piracy, this is for educational purposes only | |
/* | |
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: | |
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. | |
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. | |
*/ | |
const HEADERS={ | |
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36", | |
"Referer": "https://soundcloud.com/ ", | |
}; | |
function getSoundInfoFromSoundCloudPage(pageHTML){ | |
const startHydrationIndex = pageHTML.indexOf("<script>window.__sc_hydration"); | |
if(startHydrationIndex === -1){ | |
throw new Error("SoundCloud hydration data not found"); | |
} | |
const eqIndex = pageHTML.indexOf("=", startHydrationIndex); | |
if(eqIndex === -1){ | |
throw new Error("SoundCloud hydration data equals not found"); | |
} | |
const firstSemi = pageHTML.indexOf(";", eqIndex); | |
if(firstSemi === -1){ | |
throw new Error("SoundCloud hydration data semicolon not found"); | |
} | |
const endHydrationIndex = pageHTML.indexOf("</script>", firstSemi); | |
if(endHydrationIndex === -1){ | |
throw new Error("SoundCloud hydration end data not found"); | |
} | |
let hydratablesText = pageHTML.substring(eqIndex+1, endHydrationIndex).trim(); | |
if(hydratablesText.charAt(hydratablesText.length-1) === ";"){ | |
hydratablesText = hydratablesText.substring(0, hydratablesText.length-1); | |
} | |
let hydratables = []; | |
try { | |
hydratables = JSON.parse(hydratablesText); | |
}catch(err){ | |
throw new Error("Error parsing sound cloud hydration data") | |
} | |
const soundHydratable = hydratables.filter(x=>x.hydratable==='sound')[0]; | |
if(!soundHydratable || !soundHydratable.data){ | |
throw new Error("SoundCloud sound hydration data not found"); | |
} | |
return soundHydratable.data; | |
} | |
async function getSoundCloudSourceFromData(url, soundHydratableData){ | |
const auth = soundHydratableData.track_authorization; | |
const progressiveTranscoding = soundHydratableData.media.transcodings.filter(x=>x.format.protocol==="progressive")[0]; | |
const realTranscoding = progressiveTranscoding ? progressiveTranscoding : soundHydratableData.media.transcodings[0]; | |
if(!realTranscoding){ | |
throw new Error("could not find transcoding for stream"); | |
} | |
const firstURL = realTranscoding.url+`?client_id=Bzi0o0nRG6RkdZROE3o4Rsq32X0n7J9E&track_authorization=${encodeURIComponent(auth)}`; | |
const firstURLResp = await (await fetch(firstURL, {headers: {...HEADERS, Referer: url}})).json(); | |
const info = { | |
streamable: true, | |
stream_url: firstURLResp.url, | |
protocol: realTranscoding.format.protocol, | |
user: { username: soundHydratableData.user.username }, | |
title: soundHydratableData.title, | |
}; | |
return info; | |
} | |
async function getSoundCloudAudioSourceURL(soundCloudURL){ | |
const pageHTML = await (fetch(soundCloudURL, {headers: HEADERS}).then(x=>x.text())); | |
const soundHydratableData = getSoundInfoFromSoundCloudPage(pageHTML); | |
const source = await getSoundCloudSourceFromData(soundCloudURL, soundHydratableData); | |
return source; | |
} | |
function errorResp(origin, error, errorCode){ | |
return new Response(JSON.stringify({error: error+""}), { | |
status: errorCode || 400, | |
headers: { | |
"Access-Control-Allow-Origin": origin, | |
"Vary": "Origin", | |
"Content-Type": "application/json", | |
} | |
}); | |
} | |
function jsonResp(origin, jsonData){ | |
return new Response(JSON.stringify(jsonData), { | |
status: 200, | |
headers: { | |
"Access-Control-Allow-Origin": origin, | |
"Vary": "Origin", | |
"Content-Type": "application/json", | |
} | |
}); | |
} | |
export default { | |
async fetch(request) { | |
const corsHeaders = { | |
"Access-Control-Allow-Origin": "*", | |
"Access-Control-Allow-Methods": "GET,HEAD,POST,OPTIONS", | |
"Access-Control-Max-Age": "86400", | |
}; | |
// The URL for the remote third party API you want to fetch from | |
// but does not implement CORS | |
// The rest of this snippet for the demo page | |
function rawHtmlResponse(html) { | |
return new Response(html, { | |
headers: { | |
"content-type": "text/html;charset=UTF-8", | |
}, | |
}); | |
} | |
const DEMO_PAGE = ` | |
<!DOCTYPE html> | |
<html> | |
<head><title>Re-enable SoundCloud in Shadertoy</title></head> | |
<body> | |
<h1>Sound Cloud Re-enabled</h1> | |
</body> | |
</html> | |
`; | |
async function handleRequest(request) { | |
const url = new URL(request.url); | |
let apiUrl = url.searchParams.get("url"); | |
if (apiUrl == null || !apiUrl.startsWith("https://soundcloud.com")) { | |
return errorResp(request.headers.get("Origin"), "Please provide a sound cloud url query parameter", 400); | |
} | |
try { | |
const source = await getSoundCloudAudioSourceURL(apiUrl); | |
return jsonResp(request.headers.get("Origin"), source); | |
}catch(err){ | |
return errorResp(request.headers.get("Origin"), err+"", 400); | |
} | |
} | |
async function handleOptions(request) { | |
if ( | |
request.headers.get("Origin") !== null && | |
request.headers.get("Access-Control-Request-Method") !== null && | |
request.headers.get("Access-Control-Request-Headers") !== null | |
) { | |
// Handle CORS preflight requests. | |
return new Response(null, { | |
headers: { | |
...corsHeaders, | |
"Access-Control-Allow-Headers": request.headers.get( | |
"Access-Control-Request-Headers", | |
), | |
}, | |
}); | |
} else { | |
// Handle standard OPTIONS request. | |
return new Response(null, { | |
headers: { | |
Allow: "GET, HEAD, POST, OPTIONS", | |
}, | |
}); | |
} | |
} | |
const url = new URL(request.url); | |
if (url.pathname.startsWith("/sc")) { | |
if (request.method === "OPTIONS") { | |
// Handle CORS preflight requests | |
return handleOptions(request); | |
} else if ( | |
request.method === "GET" || | |
request.method === "HEAD" || | |
request.method === "POST" | |
) { | |
// Handle requests to the API server | |
return handleRequest(request); | |
} else { | |
return new Response(null, { | |
status: 405, | |
statusText: "Method Not Allowed", | |
}); | |
} | |
} else { | |
return rawHtmlResponse(DEMO_PAGE); | |
} | |
}, | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment