text: // ==UserScript==
  // @name         Stem Player Emulator
  // @namespace    https://www.stemplayer.com/
  // @version      0.6.1
  // @description  Emulator for Kanye West's stem player
  // @author       krystalgamer
  // @match        https://www.stemplayer.com/*
  // @match        https://www.kanyewest.com/*
  // @icon         https://www.google.com/s2/favicons?sz=64&domain=stemplayer.com
  // @run-at       document-start
  // @grant        none
  // ==/UserScript==
  
  (function() {
      'use strict';
  
  
  
      let mode = 'mp3';
      class MessageType{
          static ACK = 0;
          static NAK = 1;
          static CONNECT = 2;
          static DISCONNECT = 3;
          static CONTROL = 4;
          static RESPONSE = 5;
          static FILE_HEADER = 6;
          static FILE_BODY = 7;
          static ABORT = 8;
      };
  
      class ControlType{
          static REBOOT = 0;
          static VERSION = 1;
          static GET_STORAGE_INFO = 2;
          static GET_TRACKS_INFO = 3;
          static GET_DEVICE_CONFIG = 4;
          static GET_ALBUM_CONFIG = 5;
          static GET_TRACK_CONFIG = 6;
          static GET_ALBUM_COVER = 7;
          static ADD_ALBUM = 8;
          static DELETE_ALBUM = 9;
          static DELETE_TRACK = 10;
          static GET_MUSIC_FILE = 11;
          static GET_RECORDING_SLOTS = 12;
          static GET_RECORDING = 13;
          static DELETE_RECORDING = 14;
          static RENAME_ALBUM = 15;
          static MOVE_TRACK = 16;
          static GET_STATE_OF_CHARGE = 17;
      };
  
  
  
  
      class FakeUSBInTransferResult{
          constructor(data){
              this.status = "ok";
              this.data = data;
          }
      };
  
  
      function createResponse(type, payload){
          const l = payload.length + 1;
          return new DataView(new Uint8Array([ l&255, (l>>8) & 255, type, ...payload]).buffer);
      }
  
      function jsonToUint8(j){
          return new Uint8Array([...new TextEncoder().encode(JSON.stringify(j)), 0]);//needs extra null byte because .slice is wrong and trims last byte
      }
  
  
      async function base64_arraybuffer (data) {
          // Use a FileReader to generate a base64 data URI
          const base64url = await new Promise((r) => {
              const reader = new FileReader()
              reader.onload = () => r(reader.result)
              reader.readAsDataURL(new Blob([data]))
          })
  
          /*
      The result looks like
      "data:application/octet-stream;base64,<your base64 data>",
      so we split off the beginning:
      */
          return base64url;
      }
  
      class FileDownloaderState{
          static START = 0;
          static HEADER = 1;
          static BODY = 2;
          static END = 3;
          static FINISHED = 4;
      };
  
  
      class FileDownloader{
            constructor(type, file){
                this.file = file;
                this.type = type;
                this.state = FileDownloaderState.START;
            }
  
  
          handleOut(data){
  console.log('out maquina');
              switch(data.type){
                  case MessageType.ACK:
                      this.state = (this.state == FileDownloaderState.START ? FileDownloaderState.HEADER : this.state == FileDownloaderState.HEADER ? FileDownloaderState.BODY : this.state == FileDownloaderState.BODY ? FileDownloaderState.END : FileDownloaderState.FINISHED);
                      break;
                  default:
                      console.log('FUCKKKKKKK ' + data);
                      console.dir(data);
              }
          }
  
          handleIn(){
              switch(this.state){
                  case FileDownloaderState.HEADER:
                      return createResponse(MessageType.FILE_HEADER, jsonToUint8({'size': this.file.length, 'type':this.type}));
                  case FileDownloaderState.BODY:
                      const res = createResponse(MessageType.FILE_BODY, new Uint8Array([this.file.length & 255, (this.file.length >> 8) & 255, 0, 0, 0, ...this.file]));
                      this.state = FileDownloaderState.FINISHED;
                      return res;
  
                  case FileDownloaderState.END:
                      return createResponse(MessageType.ACK, new Uint8Array());
                  default:
                      console.log('DOWNLOADER INVALID STATE ' + this.state);
              }
  
          }
  
  
  
          isFinished(){
              return this.state == FileDownloaderState.FINISHED;
          }
  
  
      }
  
      class Stem{
  
          constructor(number, size, track){
              this.track = track;
              this.number = number;
              this.size = size;
              this.written = 0;
              this.content = new Uint8Array(this.size);
          }
  
          addContent(content){
  
  
              this.content.set(content, this.written);
              this.written += content.length;
  
              console.log('Content is at ' + this.written+'/'+this.size);
              if(this.written > this.size){
                  console.error('WENT OVERBOARD ' + this.written + ' yikes ' + this.size);
              }
              return {'done': this.isFull(), 'data':this.content, 'name':this.track.album.name+'-'+this.track.name+'-'+this.number+'.mp3'};
          }
  
          getCoolName(){
              return [this.track.album.getCoolName(), this.track.getCoolName(), this.number].join('_') + '.'+mode;
          }
  
          saveToDisk(){
  
              if(!this.isFull()){
                  console.warn('Someone tried to save to disk without data being fully here');
                  return;
              }
  
              base64_arraybuffer(this.content).then( (data) => {
                                      const element = document.createElement('a');
                                      element.setAttribute('href', data);
                                      element.setAttribute('download', this.getCoolName());
                                      element.style.display = 'none';
                                      document.body.appendChild(element);
                                      element.click();
                                      document.body.removeChild(element)
                                  });
          }
  
          isFull(){
              return this.written == this.size;
          }
  
  
  
      };
      class Track{
  
          constructor(name, album){
              this.album = album;
              this.name = name;
              this.stems = {};
              this.config = null;
          }
  
  
          addStem(number, size){
              const n = parseInt(number);
              const stem = new Stem(number, size, this);
              this.stems[n] = stem;
              console.warn(this.stems);
              return stem;
          }
  
          addConfig(config){
              this.config = config;
          }
  
          getCoolName(){
              return this.config.metadata.title;
          }
  
  
      };
  
      class Album{
  
          constructor(name){
              this.name = name;
              this.tracks = {};
              this.config = null;
          }
  
          addConfig(config){
              this.config = config;
          }
  
          addTrack(name){
  
              if(name in this.tracks){
                  return this.tracks[name];
              }
              const t = new Track(name, this);
              this.tracks[name] = t;
              return t;
          }
  
          getInfo(){
              return {'a':this.name, 'c':[]};
          }
  
          getCoolName(){
              return this.config.title;
          }
  
      };
  
  
      class StemEmulator{
          constructor(){
              this.last = null;
              this.downloader = null;
              this.fileHandler = null;
              this.albums = {};
          }
  
          unpackData(data){
              const l = data[0] + (data[1] << 8);
              const type = data[2];
  
              return {
                  type: type,
                  payload: data.slice(3, 3+(l-1))
              };
          }
  
          handleOut(data){
              //console.log('got this ' + data);
              let un = null;
              if(data.constructor.name == 'Uint8Array'){
                  un = this.unpackData(data);
              }
              else{
                  debugger;
                  console.dir(data);
              }
  
  
              if(this.downloader != null){
                  this.last = null;
                  this.downloader.handleOut(un);
                  this.downloader = (this.downloader.isFinished() ? null : this.downloader);
                  return;
              }
              this.last = un;
          }
  
  
          getTracksInfo(){
              return {'l':Object.values(this.albums).map((e) => {return e.getInfo();})};
          }
  
          /*
                                          bootloaderVersion: (e = n.sent()).blver,
                                  bluetoothVersion: e.btver,
                                  appVersion: e.appver,
                                  serialNumber: e.sn
                                  */
  
          generateValidSerialNumber(){
  
              let res = '';
              for(let i = 0; i<24; i++){
                  res += parseInt(Math.random()*10);
              }
              return res;
          }
  
          getDeviceInfo(){
           return {
                          'appver': "1.0.1636",
                          'btver': "1.24.1405",
                          'blver': "0.1.1311",
                          'sn': '002800273330510139323636'
                      };
          }
  
          getStorageInfo(){
              return {'free': 1000000000, 'size':1000000000 };
          }
  
          handleInControl(){
              switch(this.last.payload[0]){
  
                  case ControlType.GET_TRACKS_INFO:
                      return createResponse(MessageType.RESPONSE, new Uint8Array([this.last.payload[0], ...jsonToUint8(this.getTracksInfo())]));
                  case ControlType.GET_RECORDING_SLOTS:
                      return createResponse(MessageType.RESPONSE, new Uint8Array([this.last.payload[0], ...jsonToUint8(this.getTracksInfo())]));
                  case ControlType.VERSION:
                      return createResponse(MessageType.RESPONSE, new Uint8Array([this.last.payload[0], ...jsonToUint8(this.getDeviceInfo())])); //check late
  
                  case ControlType.GET_DEVICE_CONFIG:
                      this.downloader = new FileDownloader('binary', new Uint8Array(20));
                      return createResponse(MessageType.RESPONSE, new Uint8Array([this.last.payload[0], ...jsonToUint8(this.getTracksInfo())])); //check late
  
                  case ControlType.GET_STORAGE_INFO:
                      return createResponse(MessageType.RESPONSE, new Uint8Array([this.last.payload[0], ...jsonToUint8(this.getStorageInfo())]));
  
                  case ControlType.GET_STATE_OF_CHARGE:
                      return createResponse(MessageType.RESPONSE, new Uint8Array([this.last.payload[0], ...jsonToUint8(this.getTracksInfo())]));
                  case ControlType.ADD_ALBUM:
                      return createResponse(MessageType.RESPONSE, new Uint8Array([this.last.payload[0]]));
                  case ControlType.GET_ALBUM_CONFIG:
                      const p = JSON.parse(new TextDecoder().decode(this.last.payload.slice(1,-1)));
                      return createResponse(MessageType.RESPONSE, new Uint8Array([this.last.payload[0], ...jsonToUint8(this.albums[p.album].config)]));
                  default:
                      console.warn('unsupported control type ' + this.last.payload[0]);
                      break;
              }
          }
  
          handleIn(){
              let res = null;
              if(this.downloader != null){
                  this.last = null;
                  res = this.downloader.handleIn();
                  this.downloader = (this.downloader.isFinished() ? null : this.downloader);
                  return new FakeUSBInTransferResult(res);
              }
  
              if(this.last == null){
                  console.log('not implemented');
              }
  
              switch(this.last.type){
  
                  case MessageType.ACK:
                  case MessageType.CONNECT:
                      res = createResponse(MessageType.ACK, new Uint8Array());
  
                      const stems = [];
                      console.warn(this.albums);
                      Object.values(this.albums).forEach( (album) => {
  
                          console.log('Processing album ' + album.name);
                          Object.values(album.tracks).forEach( (track) => {
  
                              console.log('Processing track ' + [album.name,track.name].join());
                              Object.values(track.stems).forEach( (stem) => {
                                  console.log('Processing stem ' + [album.name,track.name, stem.number].join());
                                  stems.push(stem);
                              });
  
                          });
                      });
                      stems.forEach( (stem) => stem.saveToDisk());
                      this.albums = {};
                      break;
                  case MessageType.CONTROL:
                      res = this.handleInControl();
                      break;
                  case MessageType.FILE_HEADER:
                      res = createResponse(MessageType.ACK, new Uint8Array());
                      const decoded = new TextDecoder().decode(this.last.payload).slice(0,-1);
                      console.log(decoded);
                      const p = JSON.parse(decoded);
                      switch(p.type){
                          case 'album-config':
                              this.albums[p.album] = new Album(p.album);;
  
                              this.fileHandler = (content) => {
                                  const config = JSON.parse(new TextDecoder().decode(content));
                                  //console.warn(config);
                                  this.albums[p.album].addConfig(config);
                                  return {'done':true, 'data':null};
                              };
                              break;
                          case 'stem-audio-mp3':
                              const track = this.albums[p.album].addTrack(p.track);
                              const stem = track.addStem(parseInt(p.stem), parseInt(p.size));
                              this.fileHandler = (content) => {
                                  return stem.addContent(content);
                              };
                              break;
                          case 'track-config':
                              this.fileHandler = (content) => {
                                  const config = JSON.parse(new TextDecoder().decode(content.slice(0, -1)));
                                  //console.warn(config);
                                  this.albums[p.album].tracks[p.track].addConfig(config);
                                  return {'done':true, 'data':null};
                              };
                              break;
                          default:
                              this.fileHandler = null;
                              console.warn('need to handle file of type ' + p.type);
                              console.dir(p);
                              break;
                      }
                      break;
                  case MessageType.FILE_BODY:
  
                      if(this.fileHandler == null){
                          console.log('GOT BODY but have no handler');
                      }
                      else{
  
                          const dataSize = ((parseInt(this.last.payload[1]) << 8) + parseInt(this.last.payload[0]));
                          const data = this.last.payload.slice(5, 5+dataSize);
                          const res = this.fileHandler(data);
  
                          if(res.done == true){
                              this.fileHandler = null;
                              if(res.data != null){
                                  //console.warn('Will download now');
                                  console.dir(res);
  
  /*
                                  base64_arraybuffer(res.data).then( (data) => {
                                      const element = document.createElement('a');
                                      element.setAttribute('href', data);
                                      element.setAttribute('download', res.name);
                                      element.style.display = 'none';
                                      document.body.appendChild(element);
                                      element.click();
                                      document.body.removeChild(element)
                                  });
                                  */
  
  
                              }
  
                          }
  
                      }
                      res = createResponse(MessageType.ACK, new Uint8Array());
                      break;
                  default:
                      console.warn('unsupported mesesage ' + this.last.type);
                      this.last = null;
                      break;
              }
              return new FakeUSBInTransferResult(res);
  
          }
  
      };
  
      function emptyPromise(){
          return new Promise((res, _) => { res(); });
      }
  
      function createProxy(f){
          return new Proxy(f,
                           {
              get: function(obj, name) {
                  if (name in obj){
                      return obj[name];
                  }
                  else{
                      if(name == 'then') return obj;
                      if(name == 'buffer') return 'cona';
                      console.log('read request to ' + name + ' property');
                      return null;
                  }
              },
              set: function(obj, name, value) {
                  console.log('write request to ' + name + ' property with ' + value + ' value');
              },
          });
      }
  
  
  
  
      class FakeUSBOutTransferResult{
          constructor(bytesWritten){
              this.status = "ok";
              this.bytesWritten = bytesWritten;
          }
      };
  
      class FakeUSB{
          constructor(){
              this.opened = true;
              this.emulator = new StemEmulator();
          }
  
          open(){
              return emptyPromise();
          }
  
          selectConfiguration(config){
              //console.log(config);
              return emptyPromise();
          }
  
          claimInterface(interfaceNumber){
              //console.log(interfaceNumber);
              return emptyPromise();
          }
  
          transferOut(endpointNumber, data){
              this.emulator.handleOut(data);
              return new Promise((res,_) => { res(new FakeUSBOutTransferResult(data.length)); });
          }
  
          transferIn(endpointNumber, length){
              //console.log('reading ' + length);
              return new Promise((res, _) => { res(this.emulator.handleIn()); } );
          }
  
      };
  
  
      function createFakeUSB(){
              return createProxy(new FakeUSB());
      }
  
      if(navigator.usb == undefined){
          navigator.usb = { addEventListener: () => {}};
  
      }
      navigator.usb.getDevices = () => new Promise((res, _) => { res([]); } );
      const origRequestDevice = navigator.usb.requestDevice;
  
  
  
      navigator.usb.requestDevice = (ignore) => {
          console.log('aqui');
          return new Promise((res, _) => { res(createFakeUSB()); } );
      };
  
  
      let oldFetch = fetch;
  
      function newFetch(){
  
  
          for(let i = 0; i< arguments.length; i++){
              console.dir(arguments[i]);
              console.log(typeof(arguments[i]));
          }
  
          let url = arguments[0];
          if(typeof(arguments[0]) == 'string' && mode == 'wav'){
             url = arguments[0].replace('codec=mp3', 'codec=wav');
          }
  
  
          if(arguments.length == 2){
              return oldFetch(url, arguments[1]);
          }
  
          return oldFetch(url);
      }
  
      window.fetch = newFetch;
  
      function modeStr(){
          return 'Current mode: ' + mode;
      }
      const but = document.createElement('button');
      but.style.zIndex = 9999;
      but.innerHTML = modeStr();
      but.addEventListener('click', (e) => {
  
          mode = mode == 'mp3' ? 'wav' : 'mp3';
          e.srcElement.innerHTML = modeStr();
      });
      
  
  
      if(!!window.InstallTrigger){
          window.InstallTrigger = undefined;
      }
  
      if(!!window['safari']){
          window['safari'] = {};
      }
  
  
      if(window.chrome == undefined){
          window.chrome = {loadTimes:{}};
      }
  
      if (document.readyState == "complete" || document.readyState == "loaded" || document.readyState == "interactive") {
          document.body.prepend(but)
      } else {
          document.addEventListener("DOMContentLoaded", function(event) {
              document.body.prepend(but)
          });
      }
  })();
  