From 1bd3378d9f74458d802943d3cc095441eba6f8a2 Mon Sep 17 00:00:00 2001 From: SharkPool <139097378+SharkPool-SP@users.noreply.github.com> Date: Mon, 13 Nov 2023 22:11:51 -0800 Subject: [PATCH 01/13] Create Spotify.js --- extensions/SharkPool/Spotify.js | 467 ++++++++++++++++++++++++++++++++ 1 file changed, 467 insertions(+) create mode 100644 extensions/SharkPool/Spotify.js diff --git a/extensions/SharkPool/Spotify.js b/extensions/SharkPool/Spotify.js new file mode 100644 index 0000000000..2144e62eae --- /dev/null +++ b/extensions/SharkPool/Spotify.js @@ -0,0 +1,467 @@ +// Name: Spotify API +// ID: SPspotify +// Description: Fetch Statistics and Information and Play Songs from Spotify +// By: SharkPool + +// Version 1.0.0 + +(function (Scratch) { + "use strict"; + if (!Scratch.extensions.unsandboxed) { + throw new Error("Spotify API must run unsandboxed"); + } + + const menuIconURI = "https://upload.wikimedia.org/wikipedia/commons/8/84/Spotify_icon.svg"; + const blockIconURI = "https://uxwing.com/wp-content/themes/uxwing/download/brands-and-social-media/spotify-white-icon.svg"; + + const proxy = "https://api.codetabs.com/v1/proxy?quest="; + let audioInstances = []; + let lastFetchedSong = []; + let lastFetchedArtist = []; + let lastFetchedPlaylist = []; + + class SPspotify { + constructor() { + Scratch.vm.runtime.on("PROJECT_STOP_ALL", () => { + this.stopAll(); + }); + } + getInfo() { + return { + id: "SPspotify", + name: "Spotify API", + menuIconURI, + blockIconURI, + color1: "#1db954", + color2: "#158c3f", + color3: "#106e31", + blocks: [ + { + func: "disclaimer", + blockType: Scratch.BlockType.BUTTON, + text: "Song Disclaimer", + }, + { + opcode: "getID", + blockType: Scratch.BlockType.REPORTER, + text: "get special ID from [URL]", + arguments: { + URL: { + type: Scratch.ArgumentType.STRING, + defaultValue: "https://open.spotify.com" + } + } + }, + { + opcode: "isThingID", + blockType: Scratch.BlockType.BOOLEAN, + text: "is [URL] a [THING]?", + arguments: { + THING: { + type: Scratch.ArgumentType.STRING, + menu: "TYPES", + defaultValue: "song" + }, + URL: { + type: Scratch.ArgumentType.STRING, + defaultValue: "https://open.spotify.com" + } + } + }, + { blockType: Scratch.BlockType.LABEL, text: "Songs" }, + { + opcode: "getSongURL", + blockType: Scratch.BlockType.REPORTER, + text: "get song from ID [URL]", + arguments: { + URL: { + type: Scratch.ArgumentType.STRING, + defaultValue: "1mCsF9Tw4AkIZOjvZbZZdT" + } + } + }, + { + opcode: "playSongURL", + blockType: Scratch.BlockType.COMMAND, + text: "play song from ID [URL]", + arguments: { + URL: { + type: Scratch.ArgumentType.STRING, + defaultValue: "1mCsF9Tw4AkIZOjvZbZZdT" + } + } + }, + { + opcode: "stopAll", + blockType: Scratch.BlockType.COMMAND, + text: "stop all songs" + }, + "---", + { + opcode: "getSongAtt", + blockType: Scratch.BlockType.REPORTER, + text: "get [THING] from song ID [URL]", + arguments: { + THING: { + type: Scratch.ArgumentType.STRING, + menu: "SONG_ATTS", + defaultValue: "name" + }, + URL: { + type: Scratch.ArgumentType.STRING, + defaultValue: "1mCsF9Tw4AkIZOjvZbZZdT" + } + } + }, + { blockType: Scratch.BlockType.LABEL, text: "Artists" }, + { + opcode: "getArtistAtt", + blockType: Scratch.BlockType.REPORTER, + text: "get [THING] from artist ID [URL]", + arguments: { + THING: { + type: Scratch.ArgumentType.STRING, + menu: "ARTIST_ATTS", + defaultValue: "artist" + }, + URL: { + type: Scratch.ArgumentType.STRING, + defaultValue: "3bmFPbLMiLxtR9tFrTcKcP" + } + } + }, + { blockType: Scratch.BlockType.LABEL, text: "Playlists" }, + { + opcode: "getPlaylistAtt", + blockType: Scratch.BlockType.REPORTER, + text: "get [THING] from playlist ID [URL]", + arguments: { + THING: { + type: Scratch.ArgumentType.STRING, + menu: "PLAYLIST_ATTS", + defaultValue: "name" + }, + URL: { + type: Scratch.ArgumentType.STRING, + defaultValue: "6pyzEFjkdLGH8gXowr8Pg7" + } + } + }, + ], + menus: { + TYPES: { + acceptReporters: true, + items: ["song", "artist", "playlist"] + }, + SEARCH_TYPES: { + acceptReporters: true, + items: [ + "song names", + "artist names", + "playlist names", + "song IDs", + "artist IDs", + "playlist IDs" + ] + }, + SONG_ATTS: { + acceptReporters: true, + items: [ + "name", + "artist", + "artist ID", + "cover", + "release date", + "length", + "listens" + ] + }, + ARTIST_ATTS: { + acceptReporters: true, + items: ["artist", "description", "monthly listeners", "top 5 songs"] + }, + PLAYLIST_ATTS: { + acceptReporters: true, + items: [ + "name", + "creator", + "cover", + "description", + "likes", + "song count", + "top 30 songs" + ] + } + }, + }; + } + + disclaimer() { + const disclaimerContainer = document.createElement("div"); + disclaimerContainer.id = "disclaimer-container"; + + const closeButton = document.createElement("button"); + closeButton.textContent = "Close"; + closeButton.style.cssText = "font-weight: bold; display: block; margin: auto; border: 3px solid #106e31; border-radius: 15px; padding: 10px; width: 100px; background-color: #1db954; color: white;"; + closeButton.addEventListener("click", () => { disclaimerContainer.style.display = "none"; }); + const image = document.createElement("img"); + image.src = menuIconURI; + image.alt = "Disclaimer Image"; + image.style.cssText = "width: 50px; margin-right: 10px; margin-bottom: -15px; display: inline-block;"; + + const title = document.createElement("h2"); + title.textContent = "Disclaimer"; + title.style.cssText = "margin-bottom: 10px; text-align: center; color: white; display: inline-block;"; + const disclaimerText = document.createElement("p"); + disclaimerText.textContent = "Unfortunately, the Spotify API can only play 30-second previews of songs."; + disclaimerText.style.cssText = "text-align: center; color: white;"; + + const contentContainer = document.createElement("div"); + contentContainer.appendChild(image); + contentContainer.appendChild(title); + contentContainer.appendChild(disclaimerText); + contentContainer.style.textAlign = "center"; + disclaimerContainer.appendChild(contentContainer); + disclaimerContainer.appendChild(closeButton); + + disclaimerContainer.style.cssText = "position: fixed; width: 250px; top: 50%; left: 50%; transform: translate(-50%, -50%); z-index: 9999; border: 5px outset #1db954; border-radius: 15px; padding: 10px; background-color: #18191A; color: white; font-family: arial;"; + document.body.appendChild(disclaimerContainer); + } + + replaceSpecial(inputArray) { + if (!inputArray) return inputArray; + const replacements = { + "&": "&", + "<": "<", + ">": ">", + "!": "!", + "@": "@", + "#": "#", + "$": "$", + "%": "%", + "^": "^", + "*": "*", + "(": "(", + ")": ")", + "_": "_", + "+": "+", + "=": "=", + "?": "?", + "'": "'", + """: "\"", + "'": "'" + }; + return inputArray.map((inputString) => { + const regex = new RegExp(Object.keys(replacements).join("|"), "g"); + return inputString.replace(regex, (match) => replacements[match]); + }); + } + + getID(args) { + if (args.URL.startsWith("https://open.spotify.com/")) { + const urlParts = args.URL.split("/"); + return urlParts[4] ? urlParts[4] : ""; + } + return ""; + } + + isThingID(args) { + if (args.THING === "song") args.THING = "track"; + if (args.URL.startsWith("https://open.spotify.com/")) { + const urlParts = args.URL.split("/"); + if (urlParts[3]) return urlParts[3] === args.THING; + } + return false; + } + + async getSongURL(args) { + try { + let response; + if (lastFetchedSong[0] === args.URL) { + response = lastFetchedSong[1]; + } else { + response = await Scratch.fetch(`${proxy}https://open.spotify.com/track/${args.URL}`); + if (!response.ok) return "Error: 404"; + response = await response.text(); + lastFetchedSong = [args.URL, response]; + } + const regex = / { + audio.pause(); + audio.currentTime = 0; + }); + audioInstances = []; + } + + async getSongAtt(args) { + try { + let response; + if (lastFetchedSong[0] === args.URL) { + response = lastFetchedSong[1]; + } else { + response = await Scratch.fetch(`${proxy}https://open.spotify.com/track/${args.URL}`); + if (!response.ok) return "Error: 404"; + response = await response.text(); + lastFetchedSong = [args.URL, response]; + } + let regex; + let match; + switch (args.THING) { + case "artist": + regex = /- song and lyrics by ([^|]+) \| Spotify<\/title>/; + match = response.match(regex); + match = this.replaceSpecial(match); + return match && match[1] ? match[1] : ""; + case "artist ID": + regex = //; + match = response.match(regex); + return match && match[1] ? match[1] : ""; + case "cover": + regex = //; + match = response.match(regex); + return match && match[1] ? match[1] : ""; + case "release date": + regex = //; + match = response.match(regex); + return match && match[1] ? match[1] : ""; + case "length": + regex = //; + match = response.match(regex); + return match && match[1] ? match[1] + " seconds" : ""; + case "listens": + regex = /"type">([^<]+)<\/span><\/a><\/div><\/div>