Greasy Fork is available in English.
MIDI Player bot for MPP. (Based off of Teal's MIDI player)
// ==UserScript==
// @name TealMIDIPlayer
// @name:pt-BR Tocador de MIDIs do Teal
// @homepage <gone>
// @version 2.5.0
// @description MIDI Player bot for MPP. (Based off of Teal's MIDI player)
// @description:pt-BR Bot tocador de MIDIs para MPP. (Baseado no tocador de MIDIs do Teal)
// @author sopb b (ccjt)
// @match *://multiplayerpiano.net/*
// @match *://multiplayerpiano.org/*
// @match *://multiplayerpiano.com/*
// @match *://piano.mpp.community/*
// @match *://mpp.8448.space/*
// @match *://mpp.7458.space/*
// @match *://www.multiplayerpiano.dev/*
// @match *://mpp.smp-meow.net/*
// @icon data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==
// @grant GM_info
// @license MIT
// @namespace https://greasyfork.org/users/1459137
// ==/UserScript==
if (!localStorage.getItem('tmp_prefix')) localStorage.tmp_prefix = 'p.'
let prefix = localStorage.tmp_prefix
const sep = '-'
// script info
const SCRIPT = GM_info.script
const name = SCRIPT.name
const version = SCRIPT.version
const author = SCRIPT.author
const link = SCRIPT.homepage
// JMIDIPlayer
// "THE BEER-WARE LICENSE" (Revision 42):
// <[email protected]> wrote this file.
// As long as you retain this notice you can do whatever you want with this stuff.
// If we meet some day, and you think this stuff is worth it, you can buy me a beer in return.
// - James
const HEADER_LENGTH = 14;
const DEFAULT_TEMPO = 500000; // 120 bpm / 500ms/qn
const EVENT_SIZE = 8;
const EVENT_CODE = {
NOTE_ON: 0x09,
NOTE_OFF: 0x08,
CONTROL_CHANGE: 0x0B,
SET_TEMPO: 0x51,
END_OF_TRACK: 0x2F
};
class JMIDIPlayer {
// playback state
#isPlaying = false;
#currentTick = 0;
#currentTempo = DEFAULT_TEMPO;
#playbackWorker = null;
// loading state & file data
#isLoading = false;
#totalEvents = 0;
#totalTicks = 0;
#songTime = 0;
#ppqn = 0;
#numTracks = 0;
#timeMap = [];
// configurable properties
#playbackSpeed = 1; // multiplier
// event listeners
#eventListeners = {};
constructor() {
this.#eventListeners = {};
this.#createWorker();
}
on(event, callback) {
if (!this.#eventListeners[event]) {
this.#eventListeners[event] = [];
}
this.#eventListeners[event].push(callback);
}
off(event, callback) {
if (!this.#eventListeners[event]) return;
const index = this.#eventListeners[event].indexOf(callback);
if (index > -1) {
this.#eventListeners[event].splice(index, 1);
}
}
emit(event, data) {
if (!this.#eventListeners[event]) return;
for (const callback of this.#eventListeners[event]) {
callback(data);
}
}
async loadArrayBuffer(arrbuf) {
const start = performance.now();
this.#isLoading = true;
return new Promise((resolve, reject) => {
const handleMessage = (e) => {
const msg = e.data;
if (msg.type === 'parseComplete') {
this.#playbackWorker.removeEventListener('message', handleMessage);
this.#isLoading = false;
this.#totalEvents = msg.totalEvents;
this.#totalTicks = msg.totalTicks;
this.#songTime = msg.songTime;
this.#ppqn = msg.ppqn;
this.#numTracks = msg.numTracks;
this.#timeMap = msg.timeMap;
const parseTime = performance.now() - start;
this.emit("fileLoaded", { parseTime });
resolve([0, parseTime]); // [readTime, parseTime]
} else if (msg.type === 'parseError') {
this.#playbackWorker.removeEventListener('message', handleMessage);
this.unload();
reject(new Error(msg.error));
}
};
this.#playbackWorker.addEventListener('message', handleMessage);
// transfer the buffer to the worker
this.#playbackWorker.postMessage({
type: 'load',
buffer: arrbuf
}, [arrbuf]);
});
}
async loadFile(file) {
const arrbuf = await file.arrayBuffer();
return this.loadArrayBuffer(arrbuf);
}
unload() {
this.stop();
if (this.#isLoading) {
this.#isLoading = false;
}
this.#numTracks = 0;
this.#ppqn = 0;
this.#totalEvents = 0;
this.#totalTicks = 0;
this.#songTime = 0;
this.#timeMap = [];
this.#currentTick = 0;
this.#currentTempo = DEFAULT_TEMPO / this.#playbackSpeed;
if (this.#playbackWorker) {
this.#playbackWorker.postMessage({
type: 'unload'
});
}
this.emit("unloaded");
}
play() {
if (this.#isPlaying) return;
if (this.#isLoading) return;
if (this.#totalTicks === 0) throw new Error("No MIDI data loaded.");
this.#isPlaying = true;
this.#playbackWorker.postMessage({
type: 'play'
});
this.emit("play");
}
pause() {
if (!this.#isPlaying) return;
this.#isPlaying = false;
this.#playbackWorker.postMessage({
type: 'pause'
});
this.emit("pause");
}
stop() {
if (!this.#isPlaying && this.#currentTick === 0) return;
const needsEmit = this.#currentTick > 0;
this.#isPlaying = false;
this.#currentTick = 0;
this.#currentTempo = DEFAULT_TEMPO / this.#playbackSpeed;
this.#playbackWorker.postMessage({
type: 'stop'
});
if (needsEmit) this.emit("stop");
}
seek(tick) {
if (this.#isLoading || this.#totalTicks === 0) return;
tick = Math.min(Math.max(0, tick), this.#totalTicks);
if (Number.isNaN(tick)) return;
const wasPlaying = this.#isPlaying;
if (wasPlaying) this.pause();
this.#currentTick = tick;
this.#playbackWorker.postMessage({
type: 'seek',
tick
});
this.emit("seek", {
tick
});
if (wasPlaying) this.play();
}
#createWorker() {
const workerCode = `
const EVENT_SIZE = 8;
const DEFAULT_TEMPO = 500000;
const EVENT_CODE = { NOTE_ON: 0x09, NOTE_OFF: 0x08, CONTROL_CHANGE: 0x0B, SET_TEMPO: 0x51, END_OF_TRACK: 0x2F };
const HEADER_LENGTH = 14;
// parsed MIDI data
let tracks = [];
let ppqn = 0;
let tempoEvents = [];
let totalTicks = 0;
let numTracks = 0;
let format = 0;
// playback state
let playbackSpeed = 1;
let isPlaying = false;
let currentTick = 0;
let currentTempo = DEFAULT_TEMPO;
let trackEventPointers = [];
let startTick = 0;
let startTime = 0;
let playLoopInterval = null;
const sampleRate = 5; // ms
function parseVarlen(view, offset) {
let value = 0;
let startOffset = offset;
let checkNextByte = true;
while (checkNextByte) {
const currentByte = view.getUint8(offset);
value = (value << 7) | (currentByte & 0x7F);
++offset;
checkNextByte = !!(currentByte & 0x80);
}
return [value, offset - startOffset];
}
function parseTrack(view, trackOffset) {
let eventIndex = 0;
let capacity = 2048;
let packedBuffer = new ArrayBuffer(capacity * EVENT_SIZE);
let packedView = new DataView(packedBuffer);
const trackTempoEvents = [];
let totalTicks = 0;
let currentTick = 0;
let runningStatus = 0;
const trackLength = view.getUint32(trackOffset + 4);
let offset = trackOffset + 8;
const endOffset = offset + trackLength;
while (offset < endOffset) {
const deltaTimeVarlen = parseVarlen(view, offset);
offset += deltaTimeVarlen[1];
currentTick += deltaTimeVarlen[0];
let statusByte = view.getUint8(offset);
if (statusByte < 0x80) {
statusByte = runningStatus;
} else {
runningStatus = statusByte;
++offset;
}
const eventType = statusByte >> 4;
let ignore = false;
let eventCode, p1, p2, p3;
switch (eventType) {
case 0x8: // note off
case 0x9: // note on
eventCode = eventType;
const note = view.getUint8(offset++);
const velocity = view.getUint8(offset++);
p1 = statusByte & 0x0F; // channel
p2 = note;
p3 = velocity;
break;
case 0xB: // control change
eventCode = eventType;
const ccNum = view.getUint8(offset++);
const ccValue = view.getUint8(offset++);
if (ccNum !== 64) ignore = true;
p1 = statusByte & 0x0F; // channel
p2 = ccNum;
p3 = ccValue;
break;
case 0xA: // polyphonic key pressure
case 0xE: // pitch wheel change
++offset; // fallthrough
case 0xC: // program change
case 0xD: // channel pressure
++offset;
ignore = true;
break;
case 0xF: // system common / meta event
if (statusByte === 0xFF) {
const metaType = view.getUint8(offset++);
const lengthVarlen = parseVarlen(view, offset);
offset += lengthVarlen[1];
switch (metaType) {
case 0x51: // set tempo
if (lengthVarlen[0] !== 3) {
ignore = true;
} else {
p1 = view.getUint8(offset);
p2 = view.getUint8(offset + 1);
p3 = view.getUint8(offset + 2);
const uspq = (p1 << 16) | (p2 << 8) | p3;
trackTempoEvents.push({ tick: currentTick, uspq: uspq });
eventCode = EVENT_CODE.SET_TEMPO;
}
break;
case 0x2F: // end of track
eventCode = EVENT_CODE.END_OF_TRACK;
offset = endOffset;
break;
default:
ignore = true;
break;
}
offset += lengthVarlen[0];
} else if (statusByte === 0xF0 || statusByte === 0xF7) {
ignore = true;
const lengthVarlen = parseVarlen(view, offset);
offset += lengthVarlen[0] + lengthVarlen[1];
} else {
ignore = true;
}
break;
default:
ignore = true;
break;
}
if (!ignore) {
if (eventIndex >= capacity) {
capacity *= 2;
const newBuffer = new ArrayBuffer(capacity * EVENT_SIZE);
new Uint8Array(newBuffer).set(new Uint8Array(packedBuffer));
packedBuffer = newBuffer;
packedView = new DataView(packedBuffer);
}
const byteOffset = eventIndex * EVENT_SIZE;
if (currentTick > 0xFFFFFFFF) {
throw new Error(\`MIDI file too long! Track tick count exceeds maximum.\`);
}
packedView.setUint32(byteOffset, currentTick);
packedView.setUint8(byteOffset + 4, eventCode);
packedView.setUint8(byteOffset + 5, p1 || 0);
packedView.setUint8(byteOffset + 6, p2 || 0);
packedView.setUint8(byteOffset + 7, p3 || 0);
++eventIndex;
}
}
packedBuffer = packedBuffer.slice(0, eventIndex * EVENT_SIZE);
totalTicks = currentTick;
return { packedBuffer, tempoEvents: trackTempoEvents, totalTicks };
}
function parseMIDI(buffer) {
const view = new DataView(buffer);
// HEADER
const magic = view.getUint32(0);
if (magic !== 0x4d546864) {
throw new Error(\`Invalid MIDI magic! Expected 4d546864, got \${magic.toString(16).padStart(8, "0")}.\`);
}
const length = view.getUint32(4);
if (length !== 6) {
throw new Error(\`Invalid header length! Expected 6, got \${length}.\`);
}
format = view.getUint16(8);
numTracks = view.getUint16(10);
if (format === 0 && numTracks > 1) {
throw new Error(\`Invalid track count! Format 0 MIDIs should only have 1 track, got \${numTracks}.\`);
}
if (format >= 2) {
throw new Error(\`Unsupported MIDI format: \${format}.\`);
}
ppqn = view.getUint16(12);
if (ppqn === 0) {
throw new Error(\`Invalid PPQN/division value!\`);
}
if ((ppqn & 0x8000) !== 0) {
throw new Error(\`SMPTE timecode format is not supported!\`);
}
// TRACK OFFSETS
const trackOffsets = new Array(numTracks);
let currentOffset = HEADER_LENGTH;
for (let i = 0; i < numTracks; ++i) {
if (currentOffset >= buffer.byteLength) {
throw new Error(\`Reached EOF while looking for track \${i}. Tracks reported: \${numTracks}.\`);
}
const trackMagic = view.getUint32(currentOffset);
if (trackMagic !== 0x4d54726b) {
throw new Error(\`Invalid track \${i} magic! Expected 4d54726b, got \${trackMagic.toString(16).padStart(8, "0")}.\`);
}
const trackLength = view.getUint32(currentOffset + 4);
trackOffsets[i] = currentOffset;
currentOffset += trackLength + 8;
}
// PARSE TRACKS
tracks = new Array(numTracks);
totalTicks = 0;
tempoEvents = [];
for (let i = 0; i < numTracks; ++i) {
const result = parseTrack(view, trackOffsets[i]);
tracks[i] = {
packedBuffer: result.packedBuffer,
eventCount: result.packedBuffer.byteLength / EVENT_SIZE,
view: new DataView(result.packedBuffer)
};
totalTicks = Math.max(totalTicks, result.totalTicks);
result.tempoEvents.forEach(event => tempoEvents.push(event));
}
tempoEvents.sort((a, b) => a.tick - b.tick);
const tempoMap = [{ tick: 0, uspq: DEFAULT_TEMPO }];
for (const event of tempoEvents) {
const lastTempo = tempoMap[tempoMap.length - 1];
if (event.tick === lastTempo.tick) {
lastTempo.uspq = event.uspq;
} else {
tempoMap.push(event);
}
}
let totalMs = 0;
const timeMap = [{ tick: 0, time: 0, uspq: DEFAULT_TEMPO }];
for (let i = 0; i < tempoMap.length; ++i) {
const lastTimeData = timeMap[timeMap.length-1];
const lastUspq = lastTimeData.uspq;
const currentTempoEvent = tempoMap[i];
const ticksSinceLast = currentTempoEvent.tick - lastTimeData.tick;
const msSinceLast = (ticksSinceLast * (lastUspq / 1000)) / ppqn;
const cumulativeTime = lastTimeData.time + msSinceLast;
timeMap.push({
tick: currentTempoEvent.tick,
time: cumulativeTime,
uspq: currentTempoEvent.uspq
});
}
const lastTimeData = timeMap[timeMap.length - 1];
const ticksInFinalSegment = totalTicks - lastTimeData.tick;
const msInFinalSegment = (ticksInFinalSegment * (lastTimeData.uspq / 1000)) / ppqn;
totalMs = lastTimeData.time + msInFinalSegment;
const songTime = totalMs / 1000;
const totalEvents = tracks.map(t => t?.eventCount || 0).reduce((a, b) => a + b, 0);
return { totalEvents, totalTicks, songTime, ppqn, numTracks, timeMap };
}
function findNextEventIndex(trackIndex, tick) {
const track = tracks[trackIndex];
if (track.eventCount === 0) return 0;
let low = 0;
let high = track.eventCount;
while (low < high) {
const mid = Math.floor(low + (high - low) / 2);
const eventTick = track.view.getUint32(mid * EVENT_SIZE);
if (eventTick < tick) {
low = mid + 1;
} else {
high = mid;
}
}
return low;
}
function getCurrentTick() {
if (!startTime) return startTick;
const tpms = ppqn / (currentTempo / 1000);
const ms = performance.now() - startTime;
return Math.round(tpms * ms) + startTick;
}
function playLoop() {
if (!isPlaying) {
clearInterval(playLoopInterval);
playLoopInterval = null;
return;
}
currentTick = getCurrentTick();
if (tracks.every((track, i) => trackEventPointers[i] >= track.eventCount) || currentTick > totalTicks) {
isPlaying = false;
clearInterval(playLoopInterval);
playLoopInterval = null;
currentTick = 0;
startTick = 0;
startTime = 0;
postMessage({ type: 'endOfFile' });
return;
}
const eventPointers = [];
let totalEventsToPlay = 0;
for (let i = 0; i < tracks.length; ++i) {
const track = tracks[i];
if (!track) continue;
let ptr = trackEventPointers[i];
const startPtr = ptr;
while (ptr < track.eventCount && track.view.getUint32(ptr * EVENT_SIZE) <= currentTick) {
const eventData = track.view.getUint32((ptr * EVENT_SIZE) + 4);
const eventTypeCode = eventData >> 24;
// handle tempo changes immediately
if (eventTypeCode === EVENT_CODE.SET_TEMPO) {
const eventTick = track.view.getUint32(ptr * EVENT_SIZE);
const uspq = eventData & 0xFFFFFF;
const oldTempo = currentTempo * playbackSpeed;
const msAfterTempoEvent = ((currentTick - eventTick) * (oldTempo / 1000)) / ppqn;
startTick = eventTick;
startTime = performance.now() - msAfterTempoEvent;
currentTempo = uspq / playbackSpeed;
}
++ptr;
}
const numEventsInTrack = ptr - startPtr;
if (numEventsInTrack > 0) {
eventPointers.push({ trackIndex: i, start: startPtr, count: numEventsInTrack });
totalEventsToPlay += numEventsInTrack;
}
}
if (totalEventsToPlay > 0) {
const buffer = new ArrayBuffer(totalEventsToPlay * EVENT_SIZE);
const destView = new Uint8Array(buffer);
let destOffset = 0;
for (const pointer of eventPointers) {
const track = tracks[pointer.trackIndex];
const sourceByteOffset = pointer.start * EVENT_SIZE;
const sourceByteLength = pointer.count * EVENT_SIZE;
const sourceView = new Uint8Array(track.packedBuffer, sourceByteOffset, sourceByteLength);
destView.set(sourceView, destOffset);
destOffset += sourceByteLength;
trackEventPointers[pointer.trackIndex] += pointer.count;
}
postMessage({ type: 'events', buffer: buffer, currentTick }, [buffer]);
}
}
self.onmessage = function(e) {
const msg = e.data;
try {
switch (msg.type) {
case 'load':
const result = parseMIDI(msg.buffer);
trackEventPointers = new Array(tracks.length).fill(0);
currentTick = 0;
currentTempo = DEFAULT_TEMPO / playbackSpeed;
postMessage({
type: 'parseComplete',
totalEvents: result.totalEvents,
totalTicks: result.totalTicks,
songTime: result.songTime,
ppqn: result.ppqn,
numTracks: result.numTracks,
timeMap: result.timeMap
});
break;
case 'unload':
tracks = [];
ppqn = 0;
tempoEvents = [];
totalTicks = 0;
numTracks = 0;
trackEventPointers = [];
currentTick = 0;
currentTempo = DEFAULT_TEMPO / playbackSpeed;
isPlaying = false;
if (playLoopInterval) {
clearInterval(playLoopInterval);
playLoopInterval = null;
}
break;
case 'play':
if (isPlaying) return;
if (tracks.length === 0) return;
isPlaying = true;
startTime = performance.now();
playLoopInterval = setInterval(playLoop, sampleRate);
break;
case 'pause':
if (!isPlaying) return;
isPlaying = false;
clearInterval(playLoopInterval);
playLoopInterval = null;
startTick = getCurrentTick();
currentTick = startTick;
startTime = 0;
postMessage({ type: 'tickUpdate', tick: currentTick });
break;
case 'stop':
isPlaying = false;
clearInterval(playLoopInterval);
playLoopInterval = null;
currentTick = 0;
startTick = 0;
startTime = 0;
currentTempo = DEFAULT_TEMPO / playbackSpeed;
break;
case 'seek':
const tick = msg.tick;
// binary search for tempo
if (tempoEvents.length > 0) {
let low = 0;
let high = tempoEvents.length - 1;
let bestMatch = -1;
while (low <= high) {
const mid = Math.floor(low + (high - low) / 2);
if (tempoEvents[mid].tick <= tick) {
bestMatch = mid;
low = mid + 1;
} else {
high = mid - 1;
}
}
currentTempo = ((bestMatch !== -1) ? tempoEvents[bestMatch].uspq : DEFAULT_TEMPO) / playbackSpeed;
}
for (let i = 0; i < tracks.length; ++i) {
trackEventPointers[i] = findNextEventIndex(i, tick);
}
currentTick = tick;
startTick = tick;
postMessage({ type: 'tickUpdate', tick });
break;
case 'setPlaybackSpeed':
const oldSpeed = playbackSpeed;
playbackSpeed = msg.speed;
if (isPlaying) {
const tick = getCurrentTick();
currentTempo = (currentTempo * oldSpeed) / playbackSpeed;
startTick = tick;
startTime = performance.now();
}
break;
}
} catch (error) {
console.error(error);
postMessage({ type: 'parseError', error: error.message });
}
};
`;
const blob = new Blob([workerCode], {
type: 'application/javascript'
});
const workerUrl = URL.createObjectURL(blob);
this.#playbackWorker = new Worker(workerUrl);
this.#playbackWorker.onmessage = (e) => {
const msg = e.data;
switch (msg.type) {
case 'events':
this.#currentTick = msg.currentTick;
const view = new DataView(msg.buffer);
const numEvents = msg.buffer.byteLength / EVENT_SIZE;
for (let i = 0; i < numEvents; i++) {
const byteOffset = i * EVENT_SIZE;
const eventTick = view.getUint32(byteOffset);
const eventData = view.getUint32(byteOffset + 4);
const eventTypeCode = eventData >> 24;
const event = {
tick: eventTick
};
switch (eventTypeCode) {
case EVENT_CODE.NOTE_ON:
case EVENT_CODE.NOTE_OFF:
event.type = eventTypeCode;
event.channel = (eventData >> 16) & 0xFF;
event.note = (eventData >> 8) & 0xFF;
event.velocity = eventData & 0xFF;
break;
case EVENT_CODE.CONTROL_CHANGE:
event.type = 0x0B;
event.channel = (eventData >> 16) & 0xFF;
event.ccNum = (eventData >> 8) & 0xFF;
event.ccValue = eventData & 0xFF;
break;
case EVENT_CODE.SET_TEMPO:
event.type = 0xFF;
event.metaType = 0x51;
event.uspq = eventData & 0xFFFFFF;
this.#currentTempo = event.uspq / this.#playbackSpeed;
this.emit("tempoChange");
break;
case EVENT_CODE.END_OF_TRACK:
event.type = 0xFF;
event.metaType = 0x2F;
break;
}
this.emit("midiEvent", event);
}
break;
case 'endOfFile':
this.#isPlaying = false;
this.#currentTick = 0;
this.emit("endOfFile");
this.emit("stop");
break;
case 'tickUpdate':
this.#currentTick = msg.tick;
break;
}
};
this.#playbackWorker.onerror = (error) => {
console.error('Worker error:', error);
};
}
getTimeAtTick(tick) {
if (!this.#timeMap || this.#timeMap.length === 0 || this.#ppqn === 0) {
return 0;
}
let low = 0;
let high = this.#timeMap.length - 1;
let bestMatchIndex = 0;
while (low <= high) {
const mid = Math.floor(low + (high - low) / 2);
const midTick = this.#timeMap[mid].tick;
if (midTick <= tick) {
bestMatchIndex = mid;
low = mid + 1;
} else {
high = mid - 1;
}
}
const segment = this.#timeMap[bestMatchIndex];
const ticksSinceSegmentStart = tick - segment.tick;
const msSinceSegmentStart = (ticksSinceSegmentStart * (segment.uspq / 1000)) / this.#ppqn;
const totalMs = segment.time + msSinceSegmentStart;
return totalMs / 1000;
}
get isLoading() {
return this.#isLoading;
}
get isPlaying() {
return this.#isPlaying;
}
get trackCount() {
return this.#numTracks;
}
get songTime() {
return this.#songTime;
}
get ppqn() {
return this.#ppqn;
}
get currentTempo() {
return 60_000_000 / this.#currentTempo;
}
get totalEvents() {
return this.#totalEvents;
}
get totalTicks() {
return this.#totalTicks;
}
get currentTick() {
return this.#currentTick;
}
get playbackSpeed() {
return this.#playbackSpeed;
}
set playbackSpeed(speed) {
speed = +speed;
if (Number.isNaN(speed)) throw new Error("Playback speed must be a valid number!");
if (speed <= 0) throw new Error("Playback speed must be a positive number!");
const oldSpeed = this.#playbackSpeed;
if (speed === oldSpeed) return;
this.#playbackSpeed = speed;
if (this.#playbackWorker) {
this.#playbackWorker.postMessage({
type: 'setPlaybackSpeed',
speed
});
}
}
}
let player = new JMIDIPlayer()
player.isLoaded = ()=>!player.isLoading && player.trackCount > 0
const charLimit = 512
function send(...msgs) {
const arrow = '↱ ',
subarrow = "↳ ",
lineConnector = '❘';
const lineLimit = 3;
msgs = msgs.join(` ${sep} `)
msgs = arrow + msgs
msgs = msgs.split('\n')
if (msgs.length > lineLimit)
MPP.chat.send(`${arrow}[long (multi-line) message (${msgs.length} lines)]`)
msgs.forEach((msg, msgI)=>{
// switch functions depending on amount of messages
let sendFunc =
msgs.length > lineLimit ?
(text, bypassArrow = false)=>{
MPP.chat.receive({
t: Date.now(),
p: {
id: MPP.client.getOwnParticipant().id,
_id: MPP.client.getOwnParticipant()._id,
color: MPP.client.getOwnParticipant().color,
name: `(Only visible to you) ${sep} ${MPP.client.getOwnParticipant().name}`,
},
a: ((bypassArrow || msgI === 0) ? '' : subarrow) + text
})
}
:
(text, bypassArrow = false)=>{
MPP.chat.send(((bypassArrow || msgI === 0) ? '' : subarrow) + text);
}
// send connector line if text is empty
if (msg.replaceAll(' ', '').length === 0) {
sendFunc(lineConnector, true)
return
}
if (msg.length > charLimit) {
let totalString = [];
let splitter = msg.split(' ').length <= 8 ? '' : ' '
let split = msg.split(splitter);
if (Math.ceil(msg.length / charLimit) > lineLimit)
MPP.chat.send(`${arrow}[long message (${Math.ceil(msg.length / charLimit)} lines)]`)
split.forEach(subsplit => {
totalString.push(subsplit)
if ((subarrow + totalString.join(splitter)).length > charLimit) {
totalString.pop();
sendFunc(totalString.join(splitter));
totalString = [];
}
})
if (totalString.length > 0) sendFunc(totalString.join(splitter));
} else {
sendFunc(msg)
}
})
}
function midiLoading() {
if (!player.isPlaying) MPP.press('as3', 1);
setTimeout(() => {
MPP.release('as3');
if (!player.isPlaying) MPP.press('cs4', 1)
}, 250);
setTimeout(() => {
MPP.release('cs4');
if (!player.isPlaying) MPP.press('fs4', 1)
}, 500);
setTimeout(() => {
MPP.release('fs4');
}, 750);
setTimeout(() => {
MPP.release('c4')
}, 1000)
}
let loadnotes
function loadNotes(start) {
if (start) {
midiLoading()
loadnotes = setInterval(midiLoading, 1e3)
} else clearInterval(loadnotes)
}
function validUrl(url) {
let result
try {
new URL(url)
result = true
} catch {
result = false
}
return result
}
const signal = new AbortController().signal
async function loadStuff(a) {
function validMidi(arrbuf) {
const decoder = new TextDecoder('utf8');
let textdata = decoder.decode(arrbuf);
if (textdata.startsWith('MThd'))
return true
else
return false
}
let out = await a.arrayBuffer();
if (validMidi(out))
return out
else
throw new Error('The file provided is invalid, as it doesn\'t start with the header "MThd".')
}
let playing = {}
async function playMidiFromUrl(url) {
eventsplayed = 0
let fetchtime
let fetchstart
let parsetime
loadNotes(true)
setTimeout(() => { }, 50)
let result
fetchtime = 0
fetchstart = Date.now()
console.log('trying to play', url)
if (validUrl(url)) {
if (player.isPlaying) {
player.stop();
player.unload()
}
try {
fetch(url, {
method: 'get',
signal: signal
}).then(async (a) => {
try {
let out = await loadStuff(a)
return out
} catch (err) {
send("There was an error when playing the file.", `Error: \`${err.message}\``)
console.log(err)
loadNotes(false)
return false
}
}).then(async a => {
fetchtime = Date.now() - fetchstart
if (a) {
let parsestart = Date.now()
try {
await player.loadArrayBuffer(a)
player.play()
parsetime = Date.now() - parsestart
}
catch (err) {
parsetime = Date.now() - parsestart
result = false
send("There was an error when playing the file.", `Error: \`${err}\``)
console.err(err)
loadNotes(false)
queue.shift()
if (queue.length > 0) {
setTimeout(() => {
send(`Downloading next song on the queue... (\`${getFileName(queue[0])}\`)`)
playMidiFromUrl(queue[0])
}, 1e3)
} else {
send('Queue is now empty')
}
return
}
console.log(`Fetch time: ${fetchtime}ms\nParse time: ${parsetime}ms`)
send(`Fetched MIDI in ${fetchtime}ms.`, `Parsed MIDI in ${parsetime}ms.`, `Now playing \`${decodeURIComponent(url.split("/")[url.split("/").length - 1])}\`.`)
playing.name = getFileName(url)
playing.url = url
loadNotes(false)
} else {
loadNotes(false)
return
}
})
} catch (err) {
result = false
send('Error')
loadNotes(false)
queue.shift()
if (queue.length > 0) {
setTimeout(() => {
send(`Downloading next song on the queue... (\`${getFileName(queue[0])}\`)`)
playMidiFromUrl(queue[0])
}, 1e3)
} else {
send('Queue is now empty')
}
}
} else {
result = false
send("There was an error when playing the file.", "Error: `Invalid URL`")
queue.shift()
if (queue.length > 0) {
setTimeout(() => {
send(`Downloading next song on the queue... (\`${getFileName(queue[0])}\`)`)
playMidiFromUrl(queue[0])
}, 1e3)
} else {
send('Queue is now empty')
}
}
loadNotes(false)
return result
}
let looping = false
let sustain = false
let transpose = 0
let volume = 1
let jevents = {
noteon: 9,
noteoff: 8,
ctrlChange: 0x0B,
setTempo: 0x51,
end: 0x2F,
meta: 0xFF
};
let eventsplayed = 0
let keys = Object.keys(MPP.piano.keys)
let currenttick
player.on('midiEvent', (event) => {
eventsplayed++
currenttick = event.tick
if (event.type == jevents.noteon && event.velocity !== 0 && event.channel !== 9) {
if (volume > 1) {
if (volume % 1 !== 0)
MPP.press(keys[event.note - 21 + transpose], (event.velocity / 127) * volume % 1)
for (let i = 0; i < Math.trunc(volume); i++) {
MPP.press(keys[event.note - 21 + transpose], (event.velocity / 127) * 1)
}
} else {
MPP.press(keys[event.note - 21 + transpose], (event.velocity / 127) * volume)
}
} else if (event.type == jevents.noteoff || event.velocity == 0) {
if (!sustain) MPP.release(keys[event.note - 21 + transpose])
}
})
player.on('endOfFile', async () => {
if (looping) {
setTimeout(() => {
eventsplayed = 0;
player.seek(0);
player.play();
}, 15)
} else {
loadNotes(false)
send(`Finished playing track. Duration: \`${player.songTime.toFixed(2)}s\``, ` Played \`${eventsplayed}\` out of \`${player.totalEvents}\` (${(eventsplayed / player.totalEvents * 100).toFixed(2)}%) events.`)
player.unload()
playing = {}
queue.splice(0, 1)
if (queue.length > 0) {
setTimeout(() => {
send(`Downloading next song on the queue... (\`${getFileName(queue[0])}\`)`)
playMidiFromUrl(queue[0])
}, 500)
} else {
send('Queue is now empty.')
}
eventsplayed = 0
keys.forEach(key => {
MPP.release(key)
})
}
})
function getFileName(url) {
let filename = ""
filename = url.split('/')
filename = filename[filename.length - 1]
filename = decodeURIComponent(filename)
return filename
}
let queue = []
const categories = {
'midi': {
icon: '🎹',
label: 'MIDI'
},
'info': {
icon: '📎',
label: 'Info'
},
'pref': {
icon: '🔧',
label: 'Preferences'
}
}
const cmds = {
help: {
aliases: ['h'],
category: 'info',
about: "Shows a list of categories, a list of commands inside a category, or the info of a specific command.",
func: (...args) => {
let ogcmd = args[0]
if (args.length == 1)
send(`Categories (currently ${public ? 'public' : 'private'})`, createcmdstr(), `Use \`${ogcmd} <categ.>\` to get a list of commands from a specific category.`)
else {
let categs = []
let targetCateg;
const categ = args[1].toLowerCase();
for (let i = 0; i < Object.keys(categories).length; i++) {
const name = Object.keys(categories)[i]
const info = Object.values(categories)[i]
categs.push(name.toLowerCase())
categs.push(info.label.toLowerCase())
if (categ === info.label.toLowerCase() || categ === name.toLowerCase()) {
targetCateg = info.label
}
}
if (categs.includes(categ)) {
send(`Commands in ${targetCateg}`, createcmdstr(targetCateg), `Use \`${ogcmd} <command>\` to get info about a specific command.`)
return
} else if (Object.keys(cmds).includes(args[1])) {
const cmdinfo = cmds[args[1]]
const aliases = cmdinfo.aliases.length > 0 ? `\`${prefix}${cmdinfo.aliases.join("`, `" + prefix)}\`` : "*(none)*"
send(`*\`${prefix}${args[1]}\`*`, `Category: ${categories[cmdinfo.category].label.toLowerCase()}`, `Description: ${cmdinfo.about}`, `Aliases: ${aliases}`, `Owner only? ${cmdinfo.locked ? 'yes' : 'no'}`)
return
} else {
for (let i = 0; i < Object.keys(cmds).length; i++) {
const cmd = Object.keys(cmds)[i]
const info = Object.values(cmds)[i]
if (info.aliases.includes(args[1])) {
let aliases = info.aliases.length > 0 ? `\`${prefix}${info.aliases.join("`, `" + prefix)}\`` : "*(none)*"
send(`*\`${prefix}${cmd}\`* (searched for ${args[1]})`, `Category: ${categories[info.category].label.toLowerCase()}`, `Description: ${info.about}`, `Aliases: ${aliases}`, `Owner only? ${info.locked ? 'yes' : 'no'}`)
return
}
}
}
// fallback
send("That category (or command) does not exist.")
}
}
},
about: {
aliases: ['ab'],
category: 'info',
about: "Shows you basic information about this script.",
func: () => {
send(`${name} v${version} by ${author}`, `ccjt's site: ${link}`, `JMIDIPlayer module originally made by seq.wtf`, `Get this userscript at https://greasyfork.org/en/scripts/554578-tealmidiplayer-jmp-only`)
}
},
play: {
aliases: ['p'],
category: 'midi',
about: "Clears the queue and plays a MIDI file from the internet via a link.",
func: (...args) => {
let ogcmd = args[0]
if (args.length === 1)
send(`Please specify a direct download URL to the desired MIDI file to play.`, `Usage: \`${ogcmd} <URL>\``)
else {
let callback = () => {
queue = []
player.stop()
keys.forEach(key => {
MPP.release(key)
})
send(`Downloading \`${getFileName(args[1])}\`...`)
queue.push(args[1])
playMidiFromUrl(args[1])
}
if (player.isPlaying)
setTimeout(callback, 250);
else
callback();
}
}
},
queueup: {
aliases: ['addtoqueue', 'queuenext', 'next', 'qa', 'qu', 'up', 'addnext', 'nextup', 'nu'],
category: 'midi',
about: "Adds a MIDI file link to the track queue.",
func: (...args) => {
let ogcmd = args[0]
if (args.length === 1)
send(`Please specify a direct download URL to the desired MIDI file to add to the queue.`, `Usage: \`${ogcmd} <URL>\``)
else {
if (queue.length > 0) {
queue.push(args[1])
send(`Added \`${getFileName(args[1])}\` to queue.`, `Amount of queued tracks: ${queue.length}`)
} else {
cmds.play.func(...args)
}
}
}
},
stop: {
aliases: ['s'],
category: 'midi',
about: "Stops the current track and clears the queue.",
func: () => {
if (!player.isLoaded() && !player.isPlaying) {
send("Nothing to stop.")
return
}
queue = []
loadNotes(false)
player.stop()
player.unload()
keys.forEach(key => {
MPP.release(key)
})
send("Stopped playing.")
}
},
skip: {
aliases: ['sk'],
category: 'midi',
about: "Skips to the next track in the queue.",
func: () => {
if (queue.length === 0)
send('The queue is already empty!')
else if (queue.length === 1) {
queue = []
player.stop()
player.unload()
keys.forEach(key => {
MPP.release(key)
})
send('Skipped track.', 'Queue is now empty.')
} else if (queue.length > 1) {
queue.shift();
player.stop()
player.unload()
keys.forEach(key => {
MPP.release(key)
})
playMidiFromUrl(queue[0])
setTimeout(()=>{
send('Skipped track.', `Downloading \`${getFileName(queue[0])}\`...`)
},250)
}
}
},
sustain: {
aliases: ['sus'],
category: 'midi',
about: 'Toggles sustain on or off.',
func: () => {
sustain = !sustain
send(`Sustain is now ${sustain ? 'on' : 'off'}.`)
if (!sustain) {
keys.forEach(key => {
MPP.release(key)
})
}
}
},
volume: {
aliases: ['vol', 'v'],
category: 'midi',
about: "Adjusts the track's volume.",
func: (...args) => {
let ogcmd = args[0]
const minVol = 0, maxVol = 3;
if (args.length === 1) {
send(`Volume is currently set to \`${volume}\`.`, `Please specify a volume to set to.`, `Range: \`${minVol.toFixed(1)} to ${maxVol.toFixed(1)}\``, `Usage: \`${ogcmd} <volume from ${minVol.toFixed(1)} to ${maxVol.toFixed(1)}>\``)
} else {
args[1] = parseFloat(args[1])
if (args[1] >= minVol && args[1] <= maxVol) {
volume = args[1]
send(`Volume set to \`${volume}\`.`)
} else {
send(`That value is out of range. Please specify a value between \`${minVol.toFixed(1)}\` and \`${maxVol.toFixed(1)}\`.`)
}
}
}
},
speed: {
aliases: ['sp', 'ps'],
category: 'midi',
about: "Changes the playback speed.",
func: (...args) => {
let ogcmd = args[0]
const minSpeed = 0.1, maxSpeed = 2;
if (args.length === 1) {
send(`Playback speed is currently set to \`x${player.playbackSpeed} (${Math.trunc(player.playbackSpeed * 100)}%)\`.`, `Please specify a speed to set to.`, `Range: \`x${minSpeed.toFixed(1)} (${minSpeed * 100}%) to ${maxSpeed.toFixed(1)}x (${maxSpeed * 100}%)\``, `Usage: \`${ogcmd} <value from ${minSpeed.toFixed(1)} to ${maxSpeed.toFixed(1)}>\``)
} else {
args[1] = parseFloat(args[1])
if (args[1] >= minSpeed && args[1] <= maxSpeed) {
player.playbackSpeed = args[1]
send(`Playback speed set to \`x${player.playbackSpeed} (${Math.trunc(player.playbackSpeed * 100)}%)\`.`)
} else {
send(`That value is out of range. Please specify a value between \`${minSpeed.toFixed(1)}\` and \`${maxSpeed.toFixed(1)}\`.`)
}
}
}
},
toggle: {
aliases: ['to', 'pause', 'pa'],
category: 'midi',
about: "Switches between pausing and playing a track.",
func: () => {
if (player.isLoaded()) {
if (player.isPlaying) {
player.pause()
keys.forEach(key => {
MPP.release(key)
})
send("Paused track.")
} else {
player.play()
send("Resumed track.")
}
} else send("Nothing loaded yet.")
}
},
resume: {
aliases: ['re'],
category: 'midi',
about: "Resumes a track.",
func: () => {
if (player.isLoaded()) {
player.play()
send("Resumed track.")
} else send("Nothing loaded yet.")
}
},
transpose: {
aliases: ['tr'],
category: 'midi',
about: "Changes the transposition (key) of the current track.",
func: (...args) => {
let ogcmd = args[0]
if (args.length === 1)
send(`Transposition (key) is currently set to \`${transpose}\`.`, `Please specify a value between \`-24\` and \`36\`.`, `Usage: \`${ogcmd} <value>\``)
else {
args[1] = parseInt(args[1])
if (isNaN(args[1]))
send("Please specify a *number*.")
else
if (args[1] > 36 || args[1] < -24)
send("Please specify a value under `36` and above `-24`.")
else {
transpose = args[1]
send(`Transposition (key) is now set to \`${transpose}\`.`)
}
}
}
},
loop: {
aliases: ['l'],
category: 'midi',
about: "Toggles between looping and not looping.",
func: () => {
looping = !looping
if (looping) send("Now looping track.")
else send("Stopped looping track.")
}
},
track: {
aliases: ["t"],
category: 'info',
about: "Shows info about the track that's currently playing.",
func: () => {
if (player.isLoaded()) {
// format progress bar
let remaining = ((currenttick * (60 * 1000 / (player.currentTempo * player.ppqn)) / 1000) / player.songTime) * 100
let progressbar = []
for (let i = 0; i < Math.trunc(remaining / 10); i++) {
progressbar.push('█')
}
// define song time info
let secondsstr = ""
let played = currenttick * (60 * 1000 / (player.currentTempo * player.ppqn)) / 1000
let playedsecondsstr = ""
let songtime = player.songTime
// format song time
if (songtime % 60 > 0) secondsstr = ` ${parseInt(songtime % 60)}s`
if (played % 60 > 0) playedsecondsstr = ` ${parseInt(played % 60)}s`
// format song length
let length = songtime > 60 ? `${parseInt(songtime / 60)}m ${secondsstr}` : `${songtime.toFixed(3)}s`
let lengthplayed = played > 60 ? `${parseInt(played / 60)}m ${playedsecondsstr}` : `${played.toFixed(3)}s`
// aliases
let totalevents = player.totalEvents
let bpm = Math.trunc(player.currentTempo)
let playbackspeed = player.playbackSpeed
// dayum, he chonkeh!
send(
`\`${(player.isLoaded() && !player.isPlaying) ? `PAUSED ${sep} ` : ''}[${progressbar.join('').padEnd(10, " ")}]`,
`${remaining.toFixed(2)}% (${lengthplayed}) played`,
`Track name: ${playing.name}`,
`Track BPM: ${bpm}`,
`Track length: ${length}`,
`Played ${eventsplayed} out of ${totalevents} events (${(eventsplayed / totalevents).toFixed(2)}%)`,
`Looping: ${looping ? 'yes' : 'no'}`,
`Playback speed: x${playbackspeed} (${Math.trunc(playbackspeed * 100)}%)\``
)
} else send("Nothing loaded yet.")
}
},
notequota: {
aliases: ['nq'],
category: 'info',
about: "Shows NoteQuota status.",
func: () => {
send(`Maximum points: ${MPP.noteQuota.max}\nCurrent points: ${MPP.noteQuota.points} (${Math.trunc((MPP.noteQuota.points / MPP.noteQuota.max) * 100)}%)`)
}
},
queue: {
aliases: ['q'],
category: 'info',
about: "Shows the list of queued tracks.",
func: () => {
let qFormatted = [];
for (let i = 0; i < queue.length; i++) {
qFormatted.push(getFileName(queue[i]))
}
send(`[${queue.length}] Queue${looping ? " (currently looping, won't go to next)" : ''}: ${queue.length > 0 ? `\`${qFormatted.join('`, `')}\`` : '*(empty)*'}`)
}
},
prefix: {
locked: true,
aliases: ['pre'],
category: 'pref',
about: "Defines the prefix of the MIDI player.",
func: (...args) => {
if (args.length <= 1) {
send(`Prefix is currently set to \`${prefix}\`.`, "Please specify a prefix to set to. (Note: anything in the prefix that's after a space is ignored.)")
} else {
if (args[1].length >= 40) {
send(`Prefix too long! (wanted less than 40 characters, but got ${args[1].length} characters instead)`)
return
}
localStorage.tmp_prefix = prefix = args[1].toLowerCase()
send(`Prefix is now set to ${prefix === '' ? 'nothing' : `\`${prefix}\``}. Use \`${prefix}help\` to see commands.`)
}
}
},
public: {
locked: true,
category: 'pref',
aliases: ['pub', 'pu'],
about: "Toggles between commands being public or private.",
func: () => {
public = localStorage.tmp_public = !public
send(`Commands are now ${public ? 'public' : 'private'}.`)
}
},
checkupdate: {
locked: true,
category: 'pref',
aliases: ['updatecheck', 'updates', 'update', 'check', 'upd', 'chk'],
about: "Checks for updates.",
func: async () => {
let status = await checkUpdate(false, true)
switch (status) {
case 'stable':
send(`You are using a stable version of ${name} (${version}). `)
break;
case 'outdated':
send(`You are using an outdated version of ${name} (${version}). Please update here: https://greasyfork.org/en/scripts/554578-tealmidiplayer-jmp-only`)
break;
case 'beta':
send(`You are using an unreleased version of ${name} (${version}). Expect instability and bugs.`)
break;
default:
send(`Unknown update status "${status}"`)
break;
}
}
}
}
function createcmdstr(categ) {
if (!categ) {
let result = []
for (let i = 0; i < Object.keys(categories).length; i++) {
const name = Object.keys(categories)[i]
const info = Object.values(categories)[i]
// expected output: "<categ. emoji> <categ. label/name> (`<categ internal name>`)"
result.push(`${info.icon} ${info.label} (\`${name}\`)`)
}
return result.join(', ')
} else {
categ = categ.toLowerCase()
let targetCateg;
let categs = []
for (let i = 0; i < Object.keys(categories).length; i++) {
const name = Object.keys(categories)[i]
const info = Object.values(categories)[i]
categs.push(name.toLowerCase())
categs.push(info.label.toLowerCase())
if (categ === info.label.toLowerCase() || categ === name.toLowerCase())
targetCateg = name
}
if (!categs.includes(categ)) return 'Invalid category';
let result = []
for (let i = 0; i < Object.keys(cmds).length; i++) {
const cmd = Object.keys(cmds)[i]
const cmdinfo = Object.values(cmds)[i]
// console.log(result)
// skip cmd if its category isn't the target category
if (cmdinfo.category !== targetCateg) continue;
// expected output: "`;<cmd>`"
let str = `\`${prefix}${cmd}\``
// expected output: "`;<cmd>` (`;<alias>`)"
// or alternatively: "`;<cmd>` (`;<alias1>`, `;<alias2>`, ...)"
if (cmdinfo.aliases.length > 0) {
str += ` (\`${prefix}${cmdinfo.aliases.join(`\`, \`${prefix}`)}\`)`
}
result.push(str)
}
return result.join(', ')
}
}
if (!localStorage.getItem('tmp_public')) localStorage.tmp_public = 'false'
let public = localStorage.tmp_public === 'true';
MPP.client.on('a', (data) => {
if (!public && data.p._id !== MPP.client.getOwnParticipant()._id) return;
let args = data.a.split(' ')
let cmd = args[0].toLowerCase()
if (cmd.startsWith(prefix)) {
cmd = cmd.substring(prefix.length)
if (Object.keys(cmds).includes(cmd)) {
cmds[cmd].func(...args)
} else {
for (let i = -1; ++i < Object.keys(cmds).length;) {
if (Object.values(cmds)[i].aliases.includes(cmd)) {
if (cmds[Object.keys(cmds)[i]].locked && data.p._id !== MPP.client.getOwnParticipant()._id) {
send('You do not have permission to use this command.')
return
}
cmds[Object.keys(cmds)[i]].func(...args)
}
}
}
}
})
// custom msg handler
MPP.client.sendArray([{ m: "+custom" }])
MPP.client.on('custom', (evtn) => {
const data = evtn.data
if (data.script !== 'tmp') return
switch (data.type) {
case 'ann.':
notif(`<h2>Announcement</h2><br>${typeof data.text === 'string' ? data.text : '<missing>'}<br><small>(click to close)</small>`, true, 60e3)
break;
default:
console.log(`unknown notif "${data.type}"`, data)
break;
}
})
// set to utf-8
let metaTag = document.createElement('meta')
metaTag.charset = 'UTF-8'
document.head.prepend(metaTag)
// notif API
function notif(html, clickable = false, timeout = 15e3) {
let notif = document.createElement('a')
notif.innerHTML = html
notif.style.cssText = `
color: #fff;
background-color: #666a;
box-shadow: 0px 0px 15px 5px #0008;
backdrop-filter: saturate(150%) blur(16px);
font-size: 24px;
font-family: Arial, monospace;
padding: 5px;
margin: 15px;
border-radius: 10px;
position: fixed;
left: 50%;
transform: translate(-50%, -200%);
transition: opacity 1s ease, transform .75s ease;
white-space: pre;
z-index: calc(infinity);
text-align: center;
${clickable ? 'cursor: pointer;' : 'cursor: default;'}
`
const hidenotif = () => {
if (!notif.isConnected) return
notif.style.opacity = "0%"
notif.style.transform = "translate(-50%, -100%)"
setTimeout(() => {
notif.remove()
}, 1.5e3)
}
if (clickable) notif.onmousedown = hidenotif
document.body.prepend(notif)
setTimeout(() => {
notif.style.transform = "translate(-50%, 0%)"
setTimeout(hidenotif, timeout)
}, 50)
return notif
}
// load notif
const loadNotif = notif(
`<b>${name} by ${author}</b> loaded.\nUse <b><i>${prefix}</i>help</b> to see commands${public ? ' <i>(Commands are currently public!)</i>' : ''}.\n<small><i>(click to close popup)</i></small>`,
true,
5e3
)
// update check
async function checkUpdate(isInterval, silent = false) {
let r
try {
// try using main url
r = await fetch(`https://update.greasyfork.org/scripts/554578/JMIDIPlayer.user.js?nocache=${Math.random()}`)
} catch {
// use backup url if blocked
r = await fetch(`https://update.greasyfork.org/scripts/554578/TealMIDIPlayer%20%28JMP%20only%29.user.js?nocache=${Math.random()}`)
}
const code = (await r.text()).split('\n')
for (let i = 0; i < code.length; i++) {
const line = code[i]
if (!line.startsWith('//')) continue
if (line.includes('@version')) {
let versionNum = line.split(' ')
versionNum = versionNum[versionNum.length - 1]
let splitNewNum = versionNum.split('.')
let splitMyNum = version.split('.')
if (silent) {
let results = [];
for (let i = 0; i < splitMyNum.length; i++) {
const newNum = parseInt(splitNewNum[i])
const myNum = parseInt(splitMyNum[i])
if (myNum === newNum)
results.push('=')
else if (myNum > newNum)
results.push('>')
else if (myNum < newNum)
results.push('<')
else
results.push('?')
}
let result = 'j';
let isBeta = results.includes('>') && (results.includes('<') ? (results.indexOf('>') < results.indexOf('<')) : true)
let isOutdated = results.includes('<') && (results.includes('>') ? (results.indexOf('<') < results.indexOf('>')) : true)
let isStable = results.includes('=') && !results.includes('<') && !results.includes('>')
if (isBeta)
result = 'beta'
else if (isOutdated)
result = 'outdated'
else if (isStable)
result = 'stable'
return result
}
for (let i = 0; i < splitMyNum.length; i++) {
if (parseInt(splitNewNum[i]) !== parseInt(splitMyNum[i])) {
const callback2 = () => {
if (isInterval && parseInt(splitNewNum[i]) < parseInt(splitMyNum[i])) return
notif(
parseInt(splitNewNum[i]) > parseInt(splitMyNum[i]) ?
`Your script is <b>outdated!</b>\nNewest version: ${versionNum} - Your version: ${version}\nPlease <a href='https://greasyfork.org/en/scripts/554578-tealmidiplayer-jmp-only' target='_blank'>update your script here</a>.\n<small><i>(This popup will automatically close in 15 seconds.)</i></small>`
:
`You are using an unreleased version of <b>${name}</b>.\nExpect instability and bugs.`,
false,
15e3
)
}
if (loadNotif.isConnected) {
let msgint = setInterval(() => {
if (loadNotif.isConnected) return
callback2()
clearInterval(msgint)
}, .5e3)
} else {
callback2()
}
break
}
}
}
if (line.startsWith('// ==/UserScript==')) break
}
return code
}
checkUpdate()
setInterval(() => checkUpdate(true), 120e3)