您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Plays MIDI files!
// ==UserScript== // @name MIDI Player Bot // @namespace https://thealiendrew.github.io/ // @version 2.5.3 // @description Plays MIDI files! // @author AlienDrew // @license GPL-3.0-or-later // @match *://multiplayerpiano.com/* // @match *://mppclone.com/* // @match *://mpp.terrium.net/* // @match *://piano.ourworldofpixels.com/* // @match *://multiplayerpiano.net/* // @icon https://raw.githubusercontent.com/TheAlienDrew/Tampermonkey-Scripts/master/Multiplayer%20Piano/MPP-MIDI-Player-Bot/favicon.png // @grant GM_info // @grant GM_getResourceText // @grant GM_getResourceURL // @resource MIDIPlayerJS https://raw.githubusercontent.com/grimmdude/MidiPlayerJS/master/browser/midiplayer.js // @run-at document-end // ==/UserScript== /* Copyright (C) 2020 Andrew Larson ([email protected]) * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <https://www.gnu.org/licenses/>. */ /* globals MPP, MidiPlayer */ // =============================================== FILES // midiplayer.js via https://github.com/grimmdude/MidiPlayerJS // (but I should maybe switch to https://github.com/mudcube/MIDI.js OR https://github.com/Tonejs/Midi) var stringMIDIPlayerJS = GM_getResourceText("MIDIPlayerJS"); var scriptMIDIPlayerJS = document.createElement("script"); scriptMIDIPlayerJS.type = 'text/javascript'; scriptMIDIPlayerJS.appendChild(document.createTextNode(stringMIDIPlayerJS)); (document.body || document.head || document.documentElement).appendChild(scriptMIDIPlayerJS); // =============================================== CONSTANTS // Script constants const SCRIPT = GM_info.script; const NAME = SCRIPT.name; const NAMESPACE = SCRIPT.namespace; const VERSION = SCRIPT.version; const DESCRIPTION = SCRIPT.description; const AUTHOR = SCRIPT.author; const DOWNLOAD_URL = SCRIPT.downloadURL; // Time constants (in milliseconds) const TENTH_OF_SECOND = 100; // mainly for repeating loops const SECOND = 10 * TENTH_OF_SECOND; const CHAT_DELAY = 5 * TENTH_OF_SECOND; // needed since the chat is limited to 10 messages within less delay const SLOW_CHAT_DELAY = 2 * SECOND // when you are not the owner, your chat quota is lowered const REPEAT_DELAY = 2 * TENTH_OF_SECOND; // makes transitioning songs in repeat feel better const SONG_NAME_TIMEOUT = 10 * SECOND; // if a file doesn't play, then forget about showing the song name it after this time // URLs const FEEDBACK_URL = "https://forms.gle/x4nqjynmRMEN2GSG7"; // Players listed by IDs (these are the _id strings) const BANNED_PLAYERS = []; // empty for now const LIMITED_PLAYERS = ["8c81505ab941e0760697d777"]; // Bot constants const CHAT_MAX_CHARS = 512; // there is a limit of this amount of characters for each message sent (DON'T CHANGE) const PERCUSSION_CHANNEL = 10; // (DON'T CHANGE) const MPP_ROOM_SETTINGS_ID = "room-settings-btn"; // (DON'T CHANGE) const MIDI_FILE_SIZE_LIMIT_BYTES = 5242880; // Maximum is roughly somewhere around 150 MB, but only black midi's get to that point // Bot constant settings const ALLOW_ALL_INTRUMENTS = false; // removes percussion instruments (turning this on makes a lot of MIDIs sound bad) const BOT_SOLO_PLAY = true; // sets what play mode when the bot boots up on an owned room // Bot custom constants const PREFIX = "/"; const PREFIX_LENGTH = PREFIX.length; const BOT_KEYWORD = "MIDI"; // this is used for auto enabling the public commands in a room that contains the keyword (character case doesn't matter) const BOT_ACTIVATOR = BOT_KEYWORD.toLowerCase(); const BOT_USERNAME = NAME + " [" + PREFIX + "help]"; const BOT_NAMESPACE = '(' + NAMESPACE + ')'; const BOT_DESCRIPTION = DESCRIPTION + " Made with JS via Tampermonkey, and thanks to grimmdude for the MIDIPlayerJS library." const BOT_AUTHOR = "Created by " + AUTHOR + '.'; const BASE_COMMANDS = [ ["help (command)", "displays info about command, but no command entered shows the commands"], ["about", "get information about this bot"], ["link", "get the download link for this bot"], ["feedback", "shows link to send feedback about the bot to the developer"], ["ping", "gets the milliseconds response time"] ]; const BOT_COMMANDS = [ ["play [MIDI URL]", "plays a specific song (URL must be a direct link to a MIDI file)"], ["stop", "stops all music from playing"], ["pause", "pauses the music at that moment in the song"], ["resume", "plays music right where pause left off"], ["song", "shows the current song playing and at what moment in time"], ["repeat", "toggles repeating current song on or off"], ["sustain", "toggles how sustain is controlled via either MIDI or by MPP"] ]; const BOT_OWNER_COMMANDS = [ ["loading", "toggles the MIDI loading progress audio, or text, on or off"], [BOT_ACTIVATOR, "toggles the public bot commands on or off"] ]; const PRE_MSG = NAME + " (v" + VERSION + "): "; const PRE_HELP = PRE_MSG + "[Help]"; const PRE_ABOUT = PRE_MSG + "[About]"; const PRE_LINK = PRE_MSG + "[Link]"; const PRE_FEEDBACK = PRE_MSG + "[Feedback]"; const PRE_PING = PRE_MSG + "[Ping]"; const PRE_PLAY = PRE_MSG + "[Play]"; const PRE_STOP = PRE_MSG + "[Stop]"; const PRE_PAUSE = PRE_MSG + "[Pause]"; const PRE_RESUME = PRE_MSG + "[Resume]"; const PRE_SONG = PRE_MSG + "[Song]"; const PRE_REPEAT = PRE_MSG + "[Repeat]"; const PRE_SUSTAIN = PRE_MSG + "[Sustain]"; const PRE_DOWNLOADING = PRE_MSG + "[Downloading]"; const PRE_LOAD_MUSIC = PRE_MSG + "[Load Music]"; const PRE_PUBLIC = PRE_MSG + "[Public]"; const PRE_LIMITED = PRE_MSG + "Limited!"; const PRE_ERROR = PRE_MSG + "Error!"; const WHERE_TO_FIND_MIDIS = "You can find some good MIDIs to upload from https://bitmidi.com/ and https://midiworld.com/, or you can use your own MIDI files via Google Drive/Dropbox/etc. with a direct download link"; const NOT_OWNER = "The bot isn't the owner of the room"; const NO_SONG = "Not currently playing anything"; const LIST_BULLET = "• "; const DESCRIPTION_SEPARATOR = " - "; const CONSOLE_IMPORTANT_STYLE = "background-color: red; color: white; font-weight: bold"; // Element constants const CSS_VARIABLE_X_DISPLACEMENT = "--xDisplacement"; const PRE_ELEMENT_ID = "aliendrew-midi-player-bot"; // buttons have some constant styles/classes const ELEM_ON = "display:block;"; const ELEM_OFF = "display:none;"; const ELEM_POS = "position:absolute;"; const BTN_PAD_LEFT = 8; // pixels const BTN_PAD_TOP = 4; // pixels const BTN_WIDTH = 112; // pixels const BTN_HEIGHT = 24; // pixels const BTN_SPACER_X = BTN_PAD_LEFT + BTN_WIDTH; //pixels const BTN_SPACER_Y = BTN_PAD_TOP + BTN_HEIGHT; //pixels const BTNS_START_X = 300; //pixels const BTNS_END_X = BTNS_START_X + 4 * BTN_SPACER_X; //pixels const BTNS_TOP_0 = BTN_PAD_TOP; //pixels const BTNS_TOP_1 = BTN_PAD_TOP + BTN_SPACER_Y; //pixels const BTN_STYLE = ELEM_POS + ELEM_OFF; // Gets the correct note from MIDIPlayer to play on MPP const MIDIPlayerToMPPNote = { "A0": "a-1", "Bb0": "as-1", "B0": "b-1", "C1": "c0", "Db1": "cs0", "D1": "d0", "Eb1": "ds0", "E1": "e0", "F1": "f0", "Gb1": "fs0", "G1": "g0", "Ab1": "gs0", "A1": "a0", "Bb1": "as0", "B1": "b0", "C2": "c1", "Db2": "cs1", "D2": "d1", "Eb2": "ds1", "E2": "e1", "F2": "f1", "Gb2": "fs1", "G2": "g1", "Ab2": "gs1", "A2": "a1", "Bb2": "as1", "B2": "b1", "C3": "c2", "Db3": "cs2", "D3": "d2", "Eb3": "ds2", "E3": "e2", "F3": "f2", "Gb3": "fs2", "G3": "g2", "Ab3": "gs2", "A3": "a2", "Bb3": "as2", "B3": "b2", "C4": "c3", "Db4": "cs3", "D4": "d3", "Eb4": "ds3", "E4": "e3", "F4": "f3", "Gb4": "fs3", "G4": "g3", "Ab4": "gs3", "A4": "a3", "Bb4": "as3", "B4": "b3", "C5": "c4", "Db5": "cs4", "D5": "d4", "Eb5": "ds4", "E5": "e4", "F5": "f4", "Gb5": "fs4", "G5": "g4", "Ab5": "gs4", "A5": "a4", "Bb5": "as4", "B5": "b4", "C6": "c5", "Db6": "cs5", "D6": "d5", "Eb6": "ds5", "E6": "e5", "F6": "f5", "Gb6": "fs5", "G6": "g5", "Ab6": "gs5", "A6": "a5", "Bb6": "as5", "B6": "b5", "C7": "c6", "Db7": "cs6", "D7": "d6", "Eb7": "ds6", "E7": "e6", "F7": "f6", "Gb7": "fs6", "G7": "g6", "Ab7": "gs6", "A7": "a6", "Bb7": "as6", "B7": "b6", "C8": "c7" } // =============================================== VARIABLES var publicOption = false; // turn off the public bot commands if needed var pinging = false; // helps aid in getting response time var pingTime = 0; // changes after each ping var currentRoom = null; // updates when it connects to room var chatDelay = CHAT_DELAY; // for how long to wait until posting another message var endDelay; // used in multiline chats send commands var loadingOption = false; // controls if loading music should be on or not var loadingProgress = 0; // updates when loading files var loadingMusicLoop = null; // this is to play notes while a song is (down)loading var loadingMusicPrematureStop = false; // this is used when we need to stop the music after errors var ended = true; var stopped = false; var paused = false; var uploadButton = null; // this gets an element after it's loaded var currentSongElapsedFormatted = "00:00"; // changes with the amount of song being played var currentSongDurationFormatted = "00:00"; // gets updated when currentSongDuration is updated var currentSongDuration = 0; // this changes after each song is loaded var currentSongData = null; // this contains the song as a data URI var currentFileLocation = null; // this leads to the MIDI location (local or by URL) var currentSongName = null; // extracted from the file name/end of URL var previousSongData = null; // grabs current when changing successfully var previousSongName = null; // grabs current when changing successfully var repeatOption = false; // allows for repeat of one song var sustainOption = true; // makes notes end according to the midi file var mppRoomSettingsBtn = null; // tracks "Room Settings" element var xDisplacement = ""; // tracks xDisplacement value from CSS variables // =============================================== PAGE VISIBILITY var pageVisible = true; document.addEventListener('visibilitychange', function () { if (document.hidden) { pageVisible = false; } else { pageVisible = true; } }); // =============================================== OBJECTS // The MIDIPlayer var Player = new MidiPlayer.Player(function(event) { if (MPP.client.preventsPlaying()) { if (Player.isPlaying()) pause(); return; } var currentEvent = event.name; if (!exists(currentEvent) || currentEvent == "") return; if (currentEvent.indexOf("Note") == 0 && (ALLOW_ALL_INTRUMENTS || event.channel != PERCUSSION_CHANNEL)) { var currentNote = (exists(event.noteName) ? MIDIPlayerToMPPNote[event.noteName] : null); if (currentEvent == "Note on" && event.velocity > 0) { // start note MPP.press(currentNote, (event.velocity/100)); if (!sustainOption) MPP.release(currentNote); } else if (sustainOption && (currentEvent == "Note off" || event.velocity == 0)) MPP.release(currentNote); // end note } if (!ended && !Player.isPlaying()) { ended = true; paused = false; if (!repeatOption) { currentSongData = null; currentSongName = null; } } else { var timeRemaining = Player.getSongTimeRemaining(); var timeElapsed = currentSongDuration - (timeRemaining > 0 ? timeRemaining : 0); // BELOW TEMP: helps mitigate duration calculation issue, but still not fully fixed, see https://github.com/grimmdude/MidiPlayerJS/issues/64 currentSongDuration = Player.getSongTime(); currentSongDurationFormatted = timeClearZeros(secondsToHms(currentSongDuration)); // ABOVE TEMP currentSongElapsedFormatted = timeSizeFormat(secondsToHms(timeElapsed), currentSongDurationFormatted); } }); // see https://github.com/grimmdude/MidiPlayerJS/issues/25 Player.sampleRate = 0; // this allows sequential notes that are supposed to play at the same time, do so when using fast MIDIs (e.g. some black MIDIs) // =============================================== FUNCTIONS // CORS Anywhere (allows downloading files where JS can't) var useCorsUrl = function(url) { var newUrl = null; // send null back if it's already a cors url var cors_api_url = 'https://cors-proxy.htmldriven.com/?url='; // prevents cors-anywhere-ifing a cors-anywhere link if (url.indexOf(cors_api_url) == -1) newUrl = cors_api_url + url; return newUrl; } // Get visual loading progress, just enter the current progressing number (usually time elapsed in seconds) var getProgress = function(intProgress) { var progress = intProgress % 20; switch(progress) { case 0: return " █░░░░░░░░░░"; break; case 1: case 19: return " ░█░░░░░░░░░"; break; case 2: case 18: return " ░░█░░░░░░░░"; break; case 3: case 17: return " ░░░█░░░░░░░"; break; case 4: case 16: return " ░░░░█░░░░░░"; break; case 5: case 15: return " ░░░░░█░░░░░"; break; case 6: case 14: return " ░░░░░░█░░░░"; break; case 7: case 13: return " ░░░░░░░█░░░"; break; case 8: case 12: return " ░░░░░░░░█░░"; break; case 9: case 11: return " ░░░░░░░░░█░"; break; case 10: return " ░░░░░░░░░░█"; break; } } // Checks if loading music should play var preventsLoadingMusic = function() { return !loadingMusicPrematureStop && !Player.isPlaying() && !MPP.client.preventsPlaying(); } // This is used when loading a song in the midi player, if it's been turned on var humanMusic = function() { setTimeout(function() { if (preventsLoadingMusic()) MPP.press("c5", 1); if (preventsLoadingMusic()) MPP.release("c5"); }, 200); setTimeout(function() { if (preventsLoadingMusic()) MPP.press("d5", 1); if (preventsLoadingMusic()) MPP.release("d5"); }, 700); setTimeout(function() { if (preventsLoadingMusic()) MPP.press("c5", 1); if (preventsLoadingMusic()) MPP.release("c5"); loadingMusicPrematureStop = false; }, 1200); } // Starts the loading music var startLoadingMusic = function() { if (loadingMusicLoop == null) { humanMusic(); loadingMusicLoop = setInterval(function() { humanMusic(); }, 2200); } } // Stops the loading music var stopLoadingMusic = function() { if (loadingMusicLoop != null) { loadingMusicPrematureStop = true; clearInterval(loadingMusicLoop); loadingMusicLoop = null; } } // Check to make sure variable is initialized with something var exists = function(element) { if (typeof(element) != "undefined" && element != null) return true; return false; } // Format time to HH:MM:SS from seconds var secondsToHms = function(d) { d = Number(d); var h, m, s; var hDisplay = "00"; var mDisplay = hDisplay; var sDisplay = hDisplay; if (d != null && d > 0) { h = Math.floor(d / 3600); m = Math.floor((d % 3600) / 60); s = Math.floor((d % 3600) % 60); hDisplay = (h < 10 ? "0" : "") + h; mDisplay = (m < 10 ? "0" : "") + m; sDisplay = (s < 10 ? "0" : "") + s; } return hDisplay + ':' + mDisplay + ':' + sDisplay; } // Takes formatted time and removed preceeding zeros (only before minutes) var timeClearZeros = function(formattedHms) { var newTime = formattedHms; while (newTime.length > 5 && newTime.indexOf("00:") == 0) { newTime = newTime.substring(3); } return newTime; } // Resizes a formatted HH:MM:SS time to the second formatted time var timeSizeFormat = function(timeCurrent, timeEnd) { var newTimeFormat = timeCurrent; var timeCurrentLength = timeCurrent.length; var timeEndLength = timeEnd.length; // lose or add 00's if (timeCurrentLength > timeEndLength) newTimeFormat = timeCurrent.substring(timeCurrentLength - timeEndLength); while (newTimeFormat.length < timeEndLength) { newTimeFormat = "00:" + newTimeFormat; } return newTimeFormat; } // Generate a random number var randomNumber = function(min, max) { min = Math.ceil(min); max = Math.floor(max); return Math.floor(Math.random() * (max - min + 1)) + min; } // Puts quotes around string var quoteString = function(string) { var newString = string; if (exists(string) && string != "") newString = '"' + string + '"'; return newString } // Gets file as a blob (data URI) var urlToBlob = function(url, callback) { // show file download progress var downloading = null; mppChatSend(PRE_DOWNLOADING + ' ' + url); if (loadingOption) startLoadingMusic(); else { var progress = 0; downloading = setInterval(function() { mppChatSend(PRE_DOWNLOADING + getProgress(progress)); progress++; }, chatDelay); } fetch(url, { headers: { "Content-Disposition": "attachment" // this might not be doing anything } }).then(response => { stopLoadingMusic(); clearInterval(downloading); if (!response.ok) { throw new Error("Network response was not ok"); } return response.blob(); }).then(blob => { stopLoadingMusic(); clearInterval(downloading); callback(blob); }).catch(error => { console.error("Normal fetch couldn't get the file:", error); var corsUrl = useCorsUrl(url); if (corsUrl != null) { if (loadingOption) startLoadingMusic(); fetch(corsUrl, { headers: { "Content-Disposition": "attachment" // this might not be doing anything } }).then(response => { stopLoadingMusic(); clearInterval(downloading); if (!response.ok) { throw new Error("Network response was not ok"); } return response.blob(); }).then(blob => { stopLoadingMusic(); clearInterval(downloading); callback(blob); }).catch(error => { console.error("CORS Anywhere API fetch couldn't get the file:", error); stopLoadingMusic(); clearInterval(downloading); callback(null); }); } // callback(null); // disabled since the second fetch already should call the call back }); } // Converts files/blobs to base64 (data URI) var fileOrBlobToBase64 = function(raw, callback) { if (raw == null) { stopLoadingMusic(); callback(null); } // continue if we have a blob var reader = new FileReader(); reader.readAsDataURL(raw); reader.onloadend = function() { var base64data = reader.result; callback(base64data); } } // Validates file or blob is a MIDI var isMidi = function(raw) { if (exists(raw)) { var mimetype = raw.type; // acceptable mimetypes for midi files switch(mimetype) { case "@file/mid": case "@file/midi": case "application/mid": case "application/midi": case "application/x-mid": case "application/x-midi": case "audio/mid": case "audio/midi": case "audio/x-mid": case "audio/x-midi": case "music/crescendo": case "x-music/mid": case "x-music/midi": case "x-music/x-mid": case "x-music/x-midi": return true; break; } } return false; } // Validates file or blob is application/octet-stream ... when using CORS var isOctetStream = function(raw) { if (exists(raw) && raw.type == "application/octet-stream") return true; else return false; } // Makes all commands into one string var formattedCommands = function(commandsArray, prefix, spacing) { // needs to be 2D array with commands before descriptions if (!exists(prefix)) prefix = ''; var commands = ''; var i; for(i = 0; i < commandsArray.length; ++i) { commands += (spacing ? ' ' : '') + prefix + commandsArray[i][0]; } return commands; } // Gets 1 command and info about it into a string var formatCommandInfo = function(commandsArray, commandIndex) { return LIST_BULLET + PREFIX + commandsArray[commandIndex][0] + DESCRIPTION_SEPARATOR + commandsArray[commandIndex][1]; } // Send messages without worrying about timing var mppChatSend = function(str, delay) { setTimeout(function(){MPP.chat.send(str)}, (exists(delay) ? delay : 0)); } // Send multiline chats, and return final delay to make things easier for timings var mppChatMultiSend = function(strArray, optionalPrefix, initialDelay) { if (!exists(optionalPrefix)) optionalPrefix = ''; var newDelay = 0; var i; for (i = 0; i < strArray.length; ++i) { var currentString = strArray[i]; if (currentString != "") { ++newDelay; mppChatSend(optionalPrefix + strArray[i], chatDelay * newDelay); } } return chatDelay * newDelay; } // Stops the current song if any are playing var stopSong = function() { stopped = true; if (!ended) { Player.stop(); currentSongElapsedFormatted = timeSizeFormat(secondsToHms(0), currentSongDurationFormatted); ended = true; } if (paused) paused = false; } // Gets song from data URI and plays it var playSong = function(songFileName, songData) { // stop any current songs from playing stopSong(); // play song if it loaded correctly try { // load song Player.loadDataUri(songData); // play song Player.play(); ended = false; stopped = false; var timeoutRecorder = 0; var showSongName = setInterval(function() { if (Player.isPlaying()) { clearInterval(showSongName); // changes song //var hasExtension = songFileName.lastIndexOf('.'); previousSongData = currentSongData; previousSongName = currentSongName; currentSongData = songData; currentSongName = /*(hasExtension > 0) ? songFileName.substring(0, hasExtension) :*/ songFileName; currentSongElapsedFormatted = timeSizeFormat(secondsToHms(0), currentSongDurationFormatted); currentSongDuration = Player.getSongTime(); currentSongDurationFormatted = timeClearZeros(secondsToHms(currentSongDuration)); mppChatSend(PRE_PLAY + ' ' + getSongTimesFormatted(currentSongElapsedFormatted, currentSongDurationFormatted) + " Now playing " + quoteString(currentSongName)); } else if (timeoutRecorder == SONG_NAME_TIMEOUT) { clearInterval(showSongName); } else timeoutRecorder++; }, 1); } catch(error) { stopLoadingMusic(); // reload the previous working file if there is one if (previousSongData != null) Player.loadDataUri(previousSongData); mppChatSend(PRE_ERROR + " (play) " + error); } } // Plays the song from a URL if it's a MIDI var playURL = function(songUrl, songData) { currentFileLocation = songUrl; var songFileName = decodeURIComponent(currentFileLocation.substring(currentFileLocation.lastIndexOf('/') + 1)); playSong(songFileName, songData); } // Plays the song from an uploaded file if it's a MIDI var playFile = function(songFile) { var songFileName = null; var error = PRE_ERROR + " (play)"; // load in the file if (exists(songFile)) { // check and limit file size, mainly to prevent browser tab crashing (not enough RAM to load) and deter black midi songFileName = songFile.name.split(/(\\|\/)/g).pop(); if (songFile.size <= MIDI_FILE_SIZE_LIMIT_BYTES) { if (isMidi(songFile)) { fileOrBlobToBase64(songFile, function(base64data) { // play song only if we got data if (exists(base64data)) { currentFileLocation = songFile.name; playSong(songFileName, base64data); uploadButton.value = ""; // reset file input } else mppChatSend(error + " Unexpected result, MIDI file couldn't load"); }); } else mppChatSend(error + " The file choosen, \"" + songFileName + "\", is either corrupted, or it's not really a MIDI file"); } else mppChatSend(error + " The file choosen, \"" + songFileName + "\", is too big (larger than " + MIDI_FILE_SIZE_LIMIT_BYTES + " bytes), please choose a file with a smaller size"); } else mppChatSend(error + " MIDI file not found"); } // Creates the play, pause, resume, and stop button for the bot var createButtons = function() { // need the bottom area to append buttons to var buttonContainer = document.querySelector("#bottom div"); // we need to keep track of the next button locations var nextLocationX = BTNS_END_X; // need to initialize CSS_VARIABLE_X_DISPLACEMENT document.documentElement.style.setProperty(CSS_VARIABLE_X_DISPLACEMENT, "0px"); // play needs the div like all the other buttons // PLAY var playDiv = document.createElement("div"); playDiv.id = PRE_ELEMENT_ID + "-play"; playDiv.style = BTN_STYLE + "top:" + BTNS_TOP_0 + "px;left:calc(" + nextLocationX + "px + var(" + CSS_VARIABLE_X_DISPLACEMENT + "));"; playDiv.classList.add("ugly-button"); buttonContainer.appendChild(playDiv); // since we need upload files, there also needs to be an input element inside the play div var uploadBtn = document.createElement("input"); var uploadBtnId = PRE_ELEMENT_ID + "-upload"; uploadBtn.id = uploadBtnId; uploadBtn.style = "opacity:0;filter:alpha(opacity=0);position:absolute;top:0;left:0;width:110px;height:22px;border-radius:3px;-webkit-border-radius:3px;-moz-border-radius:3px;"; uploadBtn.title = " "; // removes the "No file choosen" tooltip uploadBtn.type = "file"; uploadBtn.accept = ".mid,.midi"; uploadBtn.onchange = function() { if (!MPP.client.preventsPlaying() && uploadBtn.files.length > 0) playFile(uploadBtn.files[0]); else console.log("No MIDI file selected"); } // fix cursor on upload file button var head = document.getElementsByTagName('HEAD')[0]; var uploadFileBtnFix = this.document.createElement('link'); uploadFileBtnFix.setAttribute('rel', 'stylesheet'); uploadFileBtnFix.setAttribute('type', 'text/css'); uploadFileBtnFix.setAttribute('href', 'data:text/css;charset=UTF-8,' + encodeURIComponent('#' + uploadBtnId + ", #" + uploadBtnId + "::-webkit-file-upload-button {cursor:pointer}")); head.appendChild(uploadFileBtnFix); // continue with other html for play button var playTxt = document.createTextNode("Play"); playDiv.appendChild(uploadBtn); playDiv.appendChild(playTxt); // then we need to let the rest of the script know it so it can reset it after loading files uploadButton = uploadBtn; // other buttons can work fine without major adjustments // STOP nextLocationX += BTN_SPACER_X; var stopDiv = document.createElement("div"); stopDiv.id = PRE_ELEMENT_ID + "-stop"; stopDiv.style = BTN_STYLE + "top:" + BTNS_TOP_0 + "px;left:calc(" + nextLocationX + "px + var(" + CSS_VARIABLE_X_DISPLACEMENT + "));"; stopDiv.classList.add("ugly-button"); stopDiv.onclick = function() { if (!MPP.client.preventsPlaying()) stop(); } var stopTxt = document.createTextNode("Stop"); stopDiv.appendChild(stopTxt); buttonContainer.appendChild(stopDiv); // REPEAT nextLocationX += BTN_SPACER_X; var repeatDiv = document.createElement("div"); repeatDiv.id = PRE_ELEMENT_ID + "-repeat"; repeatDiv.style = BTN_STYLE + "top:" + BTNS_TOP_0 + "px;left:calc(" + nextLocationX + "px + var(" + CSS_VARIABLE_X_DISPLACEMENT + "));"; repeatDiv.classList.add("ugly-button"); repeatDiv.onclick = function() { if (!MPP.client.preventsPlaying()) repeat(); } var repeatTxt = document.createTextNode("Repeat"); repeatDiv.appendChild(repeatTxt); buttonContainer.appendChild(repeatDiv); // SONG nextLocationX += BTN_SPACER_X; var songDiv = document.createElement("div"); songDiv.id = PRE_ELEMENT_ID + "-song"; songDiv.style = BTN_STYLE + "top:" + BTNS_TOP_0 + "px;left:calc(" + nextLocationX + "px + var(" + CSS_VARIABLE_X_DISPLACEMENT + "));"; songDiv.classList.add("ugly-button"); songDiv.onclick = function() { if (!MPP.client.preventsPlaying()) song(); } var songTxt = document.createTextNode("Song"); songDiv.appendChild(songTxt); buttonContainer.appendChild(songDiv); // PAUSE nextLocationX = BTNS_END_X; var pauseDiv = document.createElement("div"); pauseDiv.id = PRE_ELEMENT_ID + "-pause"; pauseDiv.style = BTN_STYLE + "top:" + BTNS_TOP_1 + "px;left:calc(" + nextLocationX + "px + var(" + CSS_VARIABLE_X_DISPLACEMENT + "));"; pauseDiv.classList.add("ugly-button"); pauseDiv.onclick = function() { if (!MPP.client.preventsPlaying()) pause(); } var pauseTxt = document.createTextNode("Pause"); pauseDiv.appendChild(pauseTxt); buttonContainer.appendChild(pauseDiv); // RESUME nextLocationX += BTN_SPACER_X; var resumeDiv = document.createElement("div"); resumeDiv.id = PRE_ELEMENT_ID + "-resume"; resumeDiv.style = BTN_STYLE + "top:" + BTNS_TOP_1 + "px;left:calc(" + nextLocationX + "px + var(" + CSS_VARIABLE_X_DISPLACEMENT + "));"; resumeDiv.classList.add("ugly-button"); resumeDiv.onclick = function() { if (!MPP.client.preventsPlaying()) resume(); } var resumeTxt = document.createTextNode("Resume"); resumeDiv.appendChild(resumeTxt); buttonContainer.appendChild(resumeDiv); // SUSTAIN nextLocationX += BTN_SPACER_X; var sustainDiv = document.createElement("div"); sustainDiv.id = PRE_ELEMENT_ID + "-sustain"; sustainDiv.style = BTN_STYLE + "top:" + BTNS_TOP_1 + "px;left:calc(" + nextLocationX + "px + var(" + CSS_VARIABLE_X_DISPLACEMENT + "));"; sustainDiv.classList.add("ugly-button"); sustainDiv.onclick = function() { if (!MPP.client.preventsPlaying()) sustain(); } var sustainTxt = document.createTextNode("Sustain"); sustainDiv.appendChild(sustainTxt); buttonContainer.appendChild(sustainDiv); // PUBLIC nextLocationX += BTN_SPACER_X; var publicDiv = document.createElement("div"); publicDiv.id = PRE_ELEMENT_ID + '-' + BOT_ACTIVATOR; publicDiv.style = BTN_STYLE + "top:" + BTNS_TOP_1 + "px;left:calc(" + nextLocationX + "px + var(" + CSS_VARIABLE_X_DISPLACEMENT + "));"; publicDiv.classList.add("ugly-button"); publicDiv.onclick = function() { public(true, true) } var publicTxt = document.createTextNode("Public"); publicDiv.appendChild(publicTxt); buttonContainer.appendChild(publicDiv); // one more button to toggle the visibility of the other buttons nextLocationX = BTNS_END_X - BTN_SPACER_X; var buttonsOn = false; var togglerDiv = document.createElement("div"); togglerDiv.id = PRE_ELEMENT_ID + "-toggler"; togglerDiv.style = ELEM_POS + ELEM_ON + "top:" + BTNS_TOP_0 + "px;left:calc(" + nextLocationX + "px + var(" + CSS_VARIABLE_X_DISPLACEMENT + "));"; // normally BTNS_TOP_1, but had to be changed to work with mppclone togglerDiv.classList.add("ugly-button"); togglerDiv.onclick = function() { if (buttonsOn) { // if on, then turn off, else turn on playDiv.style.display = stopDiv.style.display = repeatDiv.style.display = songDiv.style.display = pauseDiv.style.display = resumeDiv.style.display = sustainDiv.style.display = publicDiv.style.display = "none"; buttonsOn = false; } else { playDiv.style.display = stopDiv.style.display = repeatDiv.style.display = songDiv.style.display = pauseDiv.style.display = resumeDiv.style.display = sustainDiv.style.display = publicDiv.style.display = "block"; buttonsOn = true; } } var togglerTxt = document.createTextNode(NAME); togglerDiv.appendChild(togglerTxt); buttonContainer.appendChild(togglerDiv); } // Sends back the current time in the song against total time var getSongTimesFormatted = function(elapsed, duration) { return '[' + elapsed + " / " + duration + ']'; } // Shows limited message for user var playerLimited = function(username) { // displays message with their name about being limited mppChatSend(PRE_LIMITED + " You must of done something to earn this " + quoteString(username) + " as you are no longer allowed to use the bot"); } // When there is an incorrect command, show this error var cmdNotFound = function(cmd) { var error = PRE_ERROR + " Invalid command, " + quoteString(cmd) + " doesn't exist"; if (publicOption) mppChatSend(error); else console.log(error); } // Commands var help = function(command, userId, yourId) { var isOwner = MPP.client.isOwner(); if (!exists(command) || command == "") { var publicCommands = formattedCommands(BOT_COMMANDS, LIST_BULLET + PREFIX, true); mppChatSend(PRE_HELP + " Commands: " + formattedCommands(BASE_COMMANDS, LIST_BULLET + PREFIX, true) + (publicOption ? ' ' + publicCommands : '') + (userId == yourId ? " | Bot Owner Commands: " + (publicOption ? '' : publicCommands + ' ') + formattedCommands(BOT_OWNER_COMMANDS, LIST_BULLET + PREFIX, true) : '')); } else { var valid = null; var commandIndex = null; var commandArray = null; command = command.toLowerCase(); // check commands arrays var i; for(i = 0; i < BASE_COMMANDS.length; i++) { if (BASE_COMMANDS[i][0].indexOf(command) == 0) { valid = command; commandArray = BASE_COMMANDS; commandIndex = i; } } var j; for(j = 0; j < BOT_COMMANDS.length; j++) { if (BOT_COMMANDS[j][0].indexOf(command) == 0) { valid = command; commandArray = BOT_COMMANDS; commandIndex = j; } } var k; for(k = 0; k < BOT_OWNER_COMMANDS.length; k++) { if (BOT_OWNER_COMMANDS[k][0].indexOf(command) == 0) { valid = command; commandArray = BOT_OWNER_COMMANDS; commandIndex = k; } } // display info on command if it exists if (exists(valid)) mppChatSend(PRE_HELP + ' ' + formatCommandInfo(commandArray, commandIndex),); else cmdNotFound(command); } } var about = function() { mppChatSend(PRE_ABOUT + ' ' + BOT_DESCRIPTION + ' ' + BOT_AUTHOR + ' ' + BOT_NAMESPACE); } var link = function() { mppChatSend(PRE_LINK + " You can download this bot from " + DOWNLOAD_URL); } var feedback = function() { mppChatSend(PRE_FEEDBACK + " Please go to " + FEEDBACK_URL + " in order to submit feedback."); } var ping = function() { // get a response back in milliseconds pinging = true; pingTime = Date.now(); mppChatSend(PRE_PING); setTimeout(function() { if (pinging) mppChatSend("Pong! [within 1 second]"); pinging = false; }, SECOND); } var play = function(url) { var error = PRE_ERROR + " (play)"; // URL needs to be entered to play a song if (!exists(url) || url == "") { stopLoadingMusic(); mppChatSend(error + " No MIDI url entered... " + WHERE_TO_FIND_MIDIS); } else { // downloads file if possible and then plays it if it's a MIDI urlToBlob(url, function(blob) { if (blob == null) mppChatSend(error + " Invalid URL, this is not a MIDI file, or the file requires a manual download from " + quoteString(' ' + url + ' ') + "... " + WHERE_TO_FIND_MIDIS); else if (isMidi(blob) || isOctetStream(blob)) { // check and limit file size, mainly to prevent browser tab crashing (not enough RAM to load) and deter black midi if (blob.size <= MIDI_FILE_SIZE_LIMIT_BYTES) { fileOrBlobToBase64(blob, function(base64data) { // play song only if we got data if (exists(base64data)) { if (isOctetStream(blob)) { // when download with CORS, need to replace mimetype, but it doesn't guarantee it's a MIDI file base64data = base64data.replace("application/octet-stream", "audio/midi"); } playURL(url, base64data); } else mppChatSend(error + " Unexpected result, MIDI file couldn't load... " + WHERE_TO_FIND_MIDIS); }); } else mppChatSend(error + " The file choosen, \"" + decodeURIComponent(url.substring(url.lastIndexOf('/') + 1)) + "\", is too big (larger than " + MIDI_FILE_SIZE_LIMIT_BYTES + " bytes), please choose a file with a smaller size"); } else mppChatSend(error + " Invalid URL, this is not a MIDI file... " + WHERE_TO_FIND_MIDIS); }); } } var stop = function() { // stops the current song if (ended) mppChatSend(PRE_STOP + ' ' + NO_SONG); else { stopSong(); paused = false; mppChatSend(PRE_STOP + " Stopped playing " + quoteString(currentSongName)); currentFileLocation = currentSongName = null; } } var pause = function() { // pauses the current song if (ended) mppChatSend(PRE_PAUSE + ' ' + NO_SONG); else { var title = PRE_PAUSE + ' ' + getSongTimesFormatted(currentSongElapsedFormatted, currentSongDurationFormatted); if (paused) mppChatSend(title + " The song is already paused"); else { Player.pause(); paused = true; mppChatSend(title + " Paused " + quoteString(currentSongName)); } } } var resume = function() { // resumes the current song if (ended) mppChatSend(PRE_RESUME + ' ' + NO_SONG); else { var title = PRE_RESUME + ' ' + getSongTimesFormatted(currentSongElapsedFormatted, currentSongDurationFormatted); if (paused) { Player.play(); paused = false; mppChatSend(title + " Resumed " + quoteString(currentSongName)); } else mppChatSend(title + " The song is already playing"); } } var song = function() { // shows current song playing if (exists(currentSongName) && currentSongName != "") { mppChatSend(PRE_SONG + ' ' + getSongTimesFormatted(currentSongElapsedFormatted, currentSongDurationFormatted) + " Currently " + (paused ? "paused on" : "playing") + ' ' + quoteString(currentSongName)); } else mppChatSend(PRE_SONG + ' ' + NO_SONG); } var repeat = function() { // turns on or off repeat repeatOption = !repeatOption; mppChatSend(PRE_REPEAT + " Repeat set to " + (repeatOption ? "" : "not") + " repeating"); } var sustain = function() { // turns on or off sustain sustainOption = !sustainOption; mppChatSend(PRE_SUSTAIN + " Sustain set to " + (sustainOption ? "MIDI controlled" : "MPP controlled")); } var loading = function(userId, yourId) { // only let the bot owner set if loading music should be on or not if (userId != yourId) return; loadingOption = !loadingOption; mppChatSend(PRE_LOAD_MUSIC + " The MIDI loading progress is now set to " + (loadingOption ? "audio" : "text")); } var public = function(userId, yourId) { // only let the bot owner set if public bot commands should be on or not if (userId != yourId) return; publicOption = !publicOption; mppChatSend(PRE_PUBLIC + " Public bot commands were turned " + (publicOption ? "on" : "off")); } // =============================================== MAIN Player.on('fileLoaded', function() { // Do something when file is loaded stopLoadingMusic(); }); MPP.client.on('a', function (msg) { // if user switches to VPN, these need to update var yourParticipant = MPP.client.getOwnParticipant(); var yourId = yourParticipant._id; var yourUsername = yourParticipant.name; // get the message as string var input = msg.a.trim(); var participant = msg.p; var username = participant.name; var userId = participant._id; // check if ping if (userId == yourId && pinging && input == PRE_PING) { pinging = false; pingTime = Date.now() - pingTime; mppChatSend("Pong! [" + pingTime + "ms]", 0 ); } // make sure the start of the input matches prefix if (input.startsWith(PREFIX)) { // don't allow banned or limited users to use the bot var bannedPlayers = BANNED_PLAYERS.length; if (bannedPlayers > 0) { var i; for(i = 0; i < BANNED_PLAYERS.length; ++i) { if (BANNED_PLAYERS[i] == userId) { playerLimited(username); return; } } } var limitedPlayers = LIMITED_PLAYERS.length; if (limitedPlayers > 0) { var j; for(j = 0; j < LIMITED_PLAYERS.length; ++j) { if (LIMITED_PLAYERS[j] == userId) { playerLimited(username); return; } } } // evaluate input into command and possible arguments var message = input.substring(PREFIX_LENGTH).trim(); var hasArgs = message.indexOf(' '); var command = (hasArgs != -1) ? message.substring(0, hasArgs) : message; var argumentsString = (hasArgs != -1) ? message.substring(hasArgs + 1).trim() : null; // look through commands var isBotOwner = userId == yourId; var preventsPlaying = MPP.client.preventsPlaying(); switch (command.toLowerCase()) { case "help": case "h": if ((isBotOwner || publicOption) && !preventsPlaying) help(argumentsString, userId, yourId); break; case "about": case "ab": if ((isBotOwner || publicOption) && !preventsPlaying) about(); break; case "link": case "li": if ((isBotOwner || publicOption) && !preventsPlaying) link(); break; case "feedback": case "fb": if (isBotOwner || publicOption) feedback(); break; case "ping": case "pi": if (isBotOwner || publicOption) ping(); break; case "play": case "p": if ((isBotOwner || publicOption) && !preventsPlaying) play(argumentsString); break; case "stop": case "s": if ((isBotOwner || publicOption) && !preventsPlaying) stop(); break; case "pause": case "pa": if ((isBotOwner || publicOption) && !preventsPlaying) pause(); break; case "resume": case "r": if ((isBotOwner || publicOption) && !preventsPlaying) resume(); break; case "song": case "so": if ((isBotOwner || publicOption) && !preventsPlaying) song(); break; case "repeat": case "re": if ((isBotOwner || publicOption) && !preventsPlaying) repeat(); break; case "sustain": case "ss": if ((isBotOwner || publicOption) && !preventsPlaying) sustain(); break; case "loading": case "lo": loading(userId, yourId); break; case BOT_ACTIVATOR: public(userId, yourId); break; } } }); MPP.client.on("ch", function(msg) { // set new chat delay based on room ownership after changing rooms if (!MPP.client.isOwner()) chatDelay = SLOW_CHAT_DELAY; else chatDelay = CHAT_DELAY; // update current room info var newRoom = MPP.client.channel._id; if (currentRoom != newRoom) { currentRoom = MPP.client.channel._id; // stop any songs that might have been playing before changing rooms if (currentRoom.toUpperCase().indexOf(BOT_KEYWORD) == -1) stopSong(); } }); MPP.client.on('p', function(msg) { var userId = msg._id; // kick ban all the banned players var bannedPlayers = BANNED_PLAYERS.length; if (bannedPlayers > 0) { var i; for(i = 0; i < BANNED_PLAYERS.length; ++i) { var bannedPlayer = BANNED_PLAYERS[i]; if (userId == bannedPlayer) MPP.client.sendArray([{m: "kickban", _id: bannedPlayer, ms: 3600000}]); } } }); // =============================================== INTERVALS // Stuff that needs to be done by intervals (e.g. repeat) var repeatingTasks = setInterval(function() { if (MPP.client.preventsPlaying()) return; // do repeat if (repeatOption && ended && !stopped && exists(currentSongName) && exists(currentSongData)) { ended = false; // nice delay before playing song again setTimeout(function() {Player.play()}, REPEAT_DELAY); } }, 1); var dynamicButtonDisplacement = setInterval(function() { // required when "Room Settings" button shows up mppRoomSettingsBtn = document.getElementById(MPP_ROOM_SETTINGS_ID); xDisplacement = getComputedStyle(document.documentElement).getPropertyValue(CSS_VARIABLE_X_DISPLACEMENT); // if "Room Settings" button exists and is visible, enable displacement, else revert only when not already changed if (xDisplacement == "0px" && (mppRoomSettingsBtn && (!mppRoomSettingsBtn.style || (!mppRoomSettingsBtn.style.display || (mppRoomSettingsBtn.style.display == "block"))))) { document.documentElement.style.setProperty(CSS_VARIABLE_X_DISPLACEMENT, BTN_SPACER_X + "px"); } else if (xDisplacement != "0px" && (!mppRoomSettingsBtn || (mppRoomSettingsBtn.style && mppRoomSettingsBtn.style.display && mppRoomSettingsBtn.style.display != "block"))) { document.documentElement.style.setProperty(CSS_VARIABLE_X_DISPLACEMENT, "0px"); } }, TENTH_OF_SECOND); var slowRepeatingTasks = setInterval(function() { // do background tab fix if (!pageVisible) { var note = MPP.piano.keys["a-1"].note; var participantId = MPP.client.getOwnParticipant().id; MPP.piano.audio.play(note, 0.01, 0, participantId); MPP.piano.audio.stop(note, 0, participantId); } }, SECOND); // Automatically turns off the sound warning (mainly for autoplay) var clearSoundWarning = setInterval(function() { var playButton = document.querySelector("#sound-warning button"); if (exists(playButton)) { clearInterval(clearSoundWarning); playButton.click(); // wait for the client to come online var waitForMPP = setInterval(function() { if (exists(MPP) && exists(MPP.client) && exists(MPP.client.channel) && exists(MPP.client.channel._id) && MPP.client.channel._id != "") { clearInterval(waitForMPP); currentRoom = MPP.client.channel._id; if (currentRoom.toUpperCase().indexOf(BOT_KEYWORD) >= 0) { loadingOption = publicOption = true; } createButtons(); console.log(PRE_MSG + " Online!"); } }, TENTH_OF_SECOND); } }, TENTH_OF_SECOND);