Nonsense Speaker (Not Currently Functional)

Make the Tiktok text-to-speech API say the funny things at https://soybomb.com/tricks/words/

// ==UserScript==
// @name         Nonsense Speaker (Not Currently Functional)
// @namespace    http://michrev.com/
// @version      0.12
// @description  Make the Tiktok text-to-speech API say the funny things at https://soybomb.com/tricks/words/
// @author       StevenRoy
// @match        http://soybomb.com/tricks/words/
// @match        https://soybomb.com/tricks/words/
// @match        http://www.soybomb.com/tricks/words/
// @match        https://www.soybomb.com/tricks/words/
// @icon         https://www.soybomb.com/images/45adapter.png
// @grant        GM_xmlhttpRequest
// @connect      tiktokv.com
// ==/UserScript==

// Old icon: https://www.google.com/s2/favicons?sz=64&domain=soybomb.com
/* Inspired by a tweet by @scanlime
str=`aspell dump master | shuf | head -n 30 | tee /dev/stderr | tr '\n' '+'`;
curl -s -X POST 'https''://api16-normal-useast5.us.tiktokv.com/media/api/text/speech/invoke/?text_speaker=en_us_002&req_text='$str
| jq .data.v_str | base64 -di | ffplay -volume 10 -f mp3 -
*/
(function() { // Is this wrapper necessary? It's usually implicit! But I guess a little reasonable paranoia never hurt anyone... much.
    'use strict';

// Array of bytes from Base64 string decoding
    function atobarr(a){
        var b=atob(a); // DANGER: "Binary string" ahead
        var t=new Uint8Array(b.length); // We have to convert it ourselves otherwise JavaScript will "helpfully" encode it as UTF-8.
        t.forEach((e,i)=>{ t[i]=b.charCodeAt(i); }); // one charCode -> one byte
        return t;
    }
    const waittexts=["Please wait","Working on it","Just a sec","Processing","Downloading","Wait","Hold on","Soon\u2122"];
    if (!("MediaSource" in window)) {
        console.log("Nonsense Speaker userscript can't run here because your browser is illiterate :(");
        return;
    }
    console.log("NS starting");
    var t0,t=document.getElementsByTagName('table');
    if (!(t && t.length>2)) { console.log("NS couldn't find correct table"); return; }
    t=Array.from((t0=t[t.length-1]).getElementsByTagName("td")).map(n=>n.textContent); //.join("+"); // not space because URL parameter
    if (!(t && t.length)) { console.log("NS got empty string table"); return; }
//    console.log(t0);
    var s0=document.createElement("a");
    const dst="<b>Click here to hear this list read aloud</b>";
    s0.innerHTML=dst;
    s0.href="#";
    s0.setAttribute("style","display:block; text-align:center; border:2px solid #789; padding:4px; margin:20px");
    t0.parentNode.appendChild(s0);
    var ae,mediaSrc,msbuf,bl,bp=0; // buffers loading, buffers pending update (in sb)
    s0.onclick=(e)=>{
        if(!e) e=window.event;
//        console.log(e);
        e.preventDefault();
//        s0.innerHTML="Please Wait";
// new Blob([ atob(temp1.match(/"v_str":"([A-Za-z0-9+/]{6,})"/)[0]) ]); // or new TextEncoder().encode(temp2) for uint8array?
// document.links[0].href=URL.createObjectURL(new Blob([atob(temp1.match(/"v_str":"([A-Za-z0-9+/]{16,})"/)[1])],{ type:'application/octet-stream' })) // Creates invalid file?
// document.links[0].href="data:audio/mp3;base64,"+temp2
// DANGER! Conversion to Blob encodes atob() output as UTF-8 and this makes invalid data! encode() has same problem!

/* {"data":{"s_key":"","v_str":"","duration":""},"extra":{"log_id":"2022042512541201011300601210CDF66C"},
//   "message":"Text too long to create speech audio","status_code":1,"status_msg":"Text too long to create speech audio"}
{"data":{"s_key":"f6f77bc9-0262-4258-b920-b367663920d8","v_str":"(DATA GOES HERE)",
"duration":"100"},"extra":{"log_id":"2022042513082801011313513507F1C6A8"},"message":"success","status_code":0,"status_msg":"success"}
*/

// Since it's necessary to split request into multiple parts... Create separate media player per row? Or concat returned MP3 data?
// Or maybe this is a better idea:
        if (ae) return; // Already clicked it!
        ae=document.createElement("audio");
        mediaSrc = new window.MediaSource();
        ae.src = window.URL.createObjectURL(mediaSrc);
        mediaSrc.addEventListener('sourceopen', ()=>{
//          var mediaSource = this; // We don't need "this", do we?
//            console.log("Source open",mediaSrc);
            msbuf = mediaSrc.addSourceBuffer("audio/mpeg"); // NOT "mp3"!
            console.log("Source buffer",msbuf);
            msbuf.mode = 'sequence'; // https://developer.mozilla.org/en-US/docs/Web/API/SourceBuffer/mode
            msbuf.addEventListener('updateend', segmentupdated);
  bl=1; loadsegment(t,0);
        }, false);
    };
    function abortit(){ // In case of error...
        if (msbuf && msbuf.updating) { setTimeout(abortit,100); return; }
        if (bp==0 && bl==0) return; // Abort when complete? TSNH
        if (ae) {
            if (mediaSrc && msbuf) { mediaSrc.removeSourceBuffer(msbuf); }
            msbuf=false;
            mediaSrc=false; // Delete! Delete!
            ae.src="";
            ae=false;
        }
        bp=0; bl=0;
        s0.innerHTML=dst; // The option to try again, starting as much from scratch as possible without a page reload
    }
//    console.log(s0);
    function loadsegment(t,n,e=n+18){
//        var e=n+18; // Because the API doesn't like if we try to get all 50 words at once, try batches of 18+18+14
//        if (!bl) return; // Abort flag (probably redundant here but BSTS)
        if (e>=t.length) {
            e=t.length;
            if (e==n) { bl=0; if (bp || msbuf.updating) { return; } else { return segmentsloaded(); } }
        }
        s0.innerHTML=waittexts[Math.floor(Math.random()*waittexts.length)]+"...";
        console.log("Loading",n,"..",e);
        var r0=GM_xmlhttpRequest({ // Why's that capitalization so different?
            method:"POST",anonymous:true,
/* TODO: Voice select UI!
Valid Voices (tested from 0-30):
en_us_001 = Standard female
en_us_002 = Standard female (same as 001 it seems.)
en_us_006 = British male 1
en_us_007 = Standard male 1
en_us_009 = Standard male 2
en_us_010 = British male 2*/
            url:"https://api16-normal-useast5.us.tiktokv.com/media/api/text/speech/invoke/?text_speaker=en_us_002&req_text="+t.slice(n,e).join("+"),
            onerror:(e)=>{
                console.log(e); window.alert("Error - Web request went bang\n(Details may be in console)"); abortit();
            },
            onload:(a)=>{
//                s0.style.display="none";
//                console.log(a);
                var ab=a.responseText.match(/"v_str":"([A-Za-z0-9+/]{6,})"/);
                if (ab && ab[1]) {
                    ab=atobarr(ab[1]).buffer;
                    bp++; msbuf.appendBuffer(ab); // console.log("appending buffer: Count="+bp);
                    loadsegment(t,e); // start fetching next batch
                } else {
                    console.log(a);
                    ab=a.responseText.match(/"message":"([^"]{8,})"/);
                    window.alert("Error - Web request returned "+((ab && ab[1])?"an error message:\n\""+ab[1]+'"':"invalid data")+"\n(Details may be in console)");
                    abortit();
                }
            }
        });
    }
    function segmentupdated(){
        bp--; // console.log("appended buffer: Count="+bp);
        if (bp==0 && bl==0) return segmentsloaded();
//      ae.play();
      //console.log(mediaSource.readyState); // ended
    }
    function segmentsloaded(){
        console.log("complete");
        mediaSrc.endOfStream();
        s0.parentNode.appendChild(ae);
        s0.parentNode.removeChild(s0); s0=false;
        ae.volume=0.6; ae.controls=true;
    ae.setAttribute("style","display:block; color:#0f0; padding:0; margin:20px; width:100%");
        ae.play(); console.log(ae);
    }
})();