Skip to content

Latest commit

 

History

History
3043 lines (2900 loc) · 104 KB

index.md

File metadata and controls

3043 lines (2900 loc) · 104 KB

Essential

Drag maru·nettu from here to the Bookmark Bar in Chrome. Click the bookmarklet once in an open Originator task, then check out the More Actions menu. It should be populated with new entries for extra features. Using them should be self-explanatory with pop-ups where it needs information or issues a warning; of course, feel free to send me questions, concerns, and bug reports. Also beer.

All changes "made" to the Originator are local and will not persist; they will be gone as soon as you refresh or quit the page. All tools provided here can work offline. My code contains no trackers and keeps no usage logs. The latest production version unminified source code is found below and is functionally identical to the minified-code link above.

I do not recommend using Import SRT unless you know the limitations and dangers of importing SRT as raw Originator data. Please contact me if you have questions.

This is an early beta release, which means there may be annoying bugs.

Disclaimer

This GPLv3-released tool contains no trackers, keeps no usage statistics, and does not need an Internet connection to work. There is no warranty and no guarantee of result. Technical support will be provided on a limited basis. Donations are welcome but not required or requested. No liability will be accepted from potential consequences of using the tool. By using the tool it is tacitly understood that any data, files, and other information obtained in the process is by its nature confidential and may not be shared. In all other aspects let common sense and the general spirit of GPLv3 prevail for the good of all.

Source Code

var menu = document.querySelector("div.popup")
var itemButtons = document.getElementsByClassName("item-button")
var dropdown = itemButtons[itemButtons.length -
   1] //this should always yield the More Actions dropdown trigger
//the property is the function name; the value is the button text
//Object.entries()[Object.keys().length-1] is the last button
var marunettuButtons = {
   "marunettuCleanup": "Restore Default View",
   "marunettuExportMP4": "Export Video as .MP4",
   "marunettuDoubleExportSRT": "Export Source/Target .SRT",
   "marunettuBilingualExportHTML": "Export Bilingual .HTML",
   "marunettuImportBetterSrt": "Advanced Import SRT",
   "marunettuRunAutoQC": "Run AutoQC"
}
var clq = document.location.href.split(":")[3]
//marunettu may only work in Originator, so clq is always there
if(!localStorage["lang:" + clq])
{
   var LANG = marunettuGetLang()
}
//there are 3 ways to get language: from meta (best), from title, from previously saved to window.marunettuLang
//incidentally, since a window.attr does not survive reload, its presence this early indicates the bookmarklet
//has been run once already. So let's not risk it. 
var marunettuClickDropdown = ''
//placing it here in scope to pass a reference to the eventListener when removing it
marunettuPopulateDropdown()

function marunettuPopulateDropdown()
{
   var itemButtons = document.getElementsByClassName("item-button")
   var dropdown = itemButtons[itemButtons.length -
      1] //this should always yield the More Actions dropdown trigger
   marunettuClickDropdown = function (event)
   {
      setTimeout(marunettuAddButtonsToMenu,
         30) //it takes about 5ms for div.popup to initialize
   }
   dropdown.addEventListener('click', marunettuClickDropdown,
   {
      passive: false,
      capture: true
   })
}

function deferAddButtonUntil(condition, argument)
{ //fortunately I hope never to need this anymore ever again
   if(eval(condition))
   {
      console.log("Condition: " + condition + " evalued TRUE after " + firedon +
         "ms");
      firedon = 0;
      marunettuAddButtonToMenu(argument)
   }
   else
   {
      firedon++;
      setTimeout(function ()
      {
         deferAddButtonUntil(condition, argument)
      }, 1);
   }
}

function marunettuExportMP4()
{
   (function ()
   {
      function download(videolink)
      {
         var element = document.createElement('a');
         element.setAttribute('href', videolink);
         element.style.display = 'none';
         document.body.appendChild(element);
         element.click();
         document.body.removeChild(element);
      }
      videolink = document.getElementsByClassName("video-holder")[0]
         .children[0].getAttribute("src")
      download(videolink)
   })()
}

function marunettuRunAutoQC(language = window.marunettuLang)
{
   //here the scopes get REALLY interesting
   (function ()
   {
      language ? (LANG = language) : (LANG = marunettuGetLang());
      var outOfTheBox
      var qcMeta = {}
      qcMeta.lang = LANG
      if(document.location.href.includes("originator.backlot.netflix.com"))
      {
         var our_clq = document.location.href.split(":")[3]
         if(our_clq)
         {
            qcMeta.clq = our_clq
         }
         outOfTheBox = false
         console.log("DEBUG: state: inTheBox\nour_clq: from location " +
            our_clq)
      }
      else if(typeof Window.autoQC_safe_meta !== "undefined")
      {
         var qcSavedMeta = JSON.parse(Window.autoQC_safe_meta)
         var our_clq = qcSavedMeta.clq
         console.log(
            "DEBUG: state: outOfTheBox\nour_clq: from qcSavedMeta " +
            our_clq)
      }
      else if(document.body.innerText.split("\n")[1].includes(" --> "))
      {
         var our_clq = document.body.innerText.split("\n")
         console.log("DEBUG: state: in SRT file\nour_clq: split object " + (
            typeof our_clq))
      }

      function setShotChanges()
      {
         if(typeof our_clq !== "object" && document.location.href.includes(
               "originator.backlot.netflix.com"))
         {
            var getJSON = function (url, callback)
            {
               var xhr = new XMLHttpRequest();
               xhr.open('GET', url, true);
               xhr.responseType = 'json';
               xhr.onload = function ()
               {
                  var status = xhr.status;
                  if(status === 200)
                  {
                     callback(null, xhr.response);
                  }
                  else
                  {
                     callback(status, xhr.response);
                  }
               };
               xhr.send();
            }
            if(!localStorage.getItem("shotChanges:" + our_clq))
            {
               if(!outOfTheBox)
               {
                  getJSON(
                     "https://originator.backlot.netflix.com/api/request/shotchanges/clq:origination:" +
                     our_clq,
                     function (err, data)
                     {
                        if(err !== null)
                        {
                           alert('Something went wrong with error status: ' +
                              err +
                              '\nIf it\'s 500-something, please reload the page and try again.'
                              );
                        }
                        else
                        {
                           localStorage.setItem("shotChanges:" + our_clq,
                              data["frameNumbers"])
                           qcMeta.SC = data["frameNumbers"]
                           console.log("DEBUG: Shot changes for " +
                              our_clq + " saved in localStorage")
                        }
                     })
               }
               else
               {
                  localStorage.setItem("shotChanges:" + qcSavedMeta.clq,
                     qcSavedMeta.SC)
               }
            }
            else
            {
               console.log("DEBUG: Shot changes for " + our_clq +
                  " already present in localStorage")
               qcMeta.SC = localStorage.getItem("shotChanges:" + our_clq)
            }
         }
      }

      function defer(method)
      {
         if(localStorage.getItem("shotChanges:" + our_clq) ||
            typeof our_clq == "object")
         {
            setTimeout(function ()
            {
               runAutoQC(our_clq);
            }, 2400);
         }
         else
         {
            setTimeout(function ()
            {
               defer(runAutoQC)
            }, 350);
         }
      }
      (function (global, factory)
      {
         typeof exports === 'object' && typeof module !== 'undefined' ?
            module.exports = factory() : typeof define === 'function' &&
            define.amd ? define(factory) : (global.runAutoQC = factory());
      }(this, (function ()
      {
         'use strict';

         function uid()
         {
            return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(
               /[018]/g, c => (c ^ crypto.getRandomValues(
                  new Uint8Array(1))[0] & 15 >> c / 4).toString(
                  16))
         }

         function runAutoQC()
         {
            var clq_pattern = new RegExp(
               '^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$',
               'i');
            if(typeof our_clq !== "object")
            {
               our_clq = arguments.length > 0 && arguments[0] !==
                  undefined && clq_pattern.test(arguments[0]) ?
                  arguments[0] : '';
            }
            LANG = LANG.toUpperCase()
            console.log("DEBUG: Starting AutoQC with LANG: " + LANG)
            var line_limit = 0
            var max_cps = 0
            var normal_cps = 0
            switch (LANG)
            {
            case "AR":
               line_limit = 42
               normal_cps = 20
               max_cps = 26
               break;
            case "CN":
               line_limit = 16
               normal_cps = 9
               max_cps = 11.7
               break;
            case "RU":
               line_limit = 39
               normal_cps = 17
               max_cps = 22.1
               break;
            case "DE":
            case "MS":
            case "GR":
            case "FR":
            case "PT":
            case "EN":
            default:
               line_limit = 42
               normal_cps = 17
               max_cps = 22.1
            }
            if(typeof our_clq !== "object")
            {
               if(!our_clq || !clq_pattern.test(our_clq))
               {
                  alert(
                     "You must be in a started, saved Originator task!");
                  return null
               }
               if(!clq_pattern.test(our_clq))
               {
                  alert("The CLQ is invalid: " + our_clq + "\n" +
                     "Please report me to the developer.");
                  return null
               }
            }
            var shotChanges = localStorage.getItem("shotChanges:" +
               our_clq)
            try
            {
               shotChanges = shotChanges.split(",");
               console.log("DEBUG: Shot changes validated.")
            }
            catch (e)
            {
               var shotChanges = undefined;
               console.log(
                  "DEBUG: No shot changes found, which is ok if our_clq is object: " +
                  (typeof our_clq))
            }
            var hasSafeBox = document.getElementsByClassName(
               "marketing-border")[0] ? true : false
            try
            {
               var userIp = document.getElementsByClassName(
                  "Watermark")[0].innerHTML;
               var userName = document.getElementsByClassName(
                  "username")[0].textContent;
            }
            catch (e)
            {
               var userIp = "LOCAL ITERATION"
            }
            var reportHtml =
               `<head><meta charset="utf-8">
                        <script>
                        Window.autoQC_safe_meta = METADATATOKEN
                        </script>
            <script>var prevScrollpos = window.pageYOffset;
            window.onscroll = function() {
              var currentScrollPos = window.pageYOffset;
              if (prevScrollpos > currentScrollPos) {
                document.getElementById("navbar").style.top = "0";
              } else {
                document.getElementById("navbar").style.top = "-50px";
              }
              prevScrollpos = currentScrollPos;
            }</script>
                            <script>var z=function(q) {
                var s=document.getElementsByClassName(q);
                for(x of s) {
                    x.className===q ? (x.className=q+" of0"): (x.setAttribute("class", q))
                }
            }

            ;
            </script><style>body {
                background-color: #212121
            }

            #navbar {
              background-color: #333;
              position: fixed; 
              top: 0; 
              width: 100%;
              transition: top 0.5s;

            }

            #navbar a {
              float: left;
              display: block;
              color: white;
              text-align: center;
              padding: 5px;
              text-decoration: none;
            }

            #navbar a:hover {
              background-color: #ddd;
              color: black;
            }

            #background{
                position:fixed;
                z-index:-999;
                background:#212121;
                display:block
                height:100vh; 
                width:100vh;
                color:grey; 
            }

            .bg-text
            {
                font-family: Netflix-Sans;
                pointer-events: none;
                color:lightgrey;
                text-align: center;
                opacity: 0.06;
                font-size:80px;
                transform:rotate(320deg);
                -webkit-transform:rotate(320deg);
                position: relative;
                overflow: visible;
                user-select: none;
                -webkit-user-select: none;
                -khtml-user-select: none;
                -moz-user-select: none;
                -ms-user-select: none;
            }

            .of0 {
                visibility: hidden;
                color: #000!important;
            }

            .username {
               float:left;
            }

            .ipAddress {
               float:right; 
            }

            .cSpace {
                color: #00fa00;
                visibility: visible;
            }

            .StyledTextEditor {
                font-family: Netflix-Sans;
                line-height: 1.3;
                margin: 2px;
                white-space: nowrap;
                overflow: hidden;
                font-variant-ligatures: none;
                position: relative;
                pointer-events: none
            }

            .StyledTextEditor .ltr {
                unicode-bidi: embed;
                direction: ltr
            }

            .TimedTextEvent {
                font-family: Netflix-Sans;
                font-size: 1.3rem;
                font-weight: 400;
                line-height: 2rem;
                margin-top: .5rem;
                display: -webkit-box;
                display: -moz-box;
                display: -webkit-flex;
                display: -ms-flexbox;
                display: box;
                display: flex;
                -webkit-box-orient: vertical;
                -moz-box-orient: vertical;
                -o-box-orient: vertical;
                -webkit-flex-direction: column;
                -ms-flex-direction: column;
                flex-direction: column;
                border: 1px solid transparent;
                -webkit-transition: height 250ms ease, border 150ms ease;
                -moz-transition: height 250ms ease, border 150ms ease;
                -o-transition: height 250ms ease, border 150ms ease;
                -ms-transition: height 250ms ease, border 150ms ease;
                transition: height 250ms ease, border 150ms ease
            }

            .TimedTextEvent .details {
                padding-top: .5rem;
                padding-left: 1rem;
                padding-right: 1rem;
                display: -webkit-box;
                display: -moz-box;
                display: -webkit-flex;
                display: -ms-flexbox;
                display: box;
                display: flex;
                -webkit-box-orient: horizontal;
                -moz-box-orient: horizontal;
                -o-box-orient: horizontal;
                -webkit-flex-direction: row;
                -ms-flex-direction: row;
                flex-direction: row
            }

            .TimedTextEvent .details .timing {
                -webkit-flex-shrink: 0;
                flex-shrink: 0;
                display: -webkit-box;
                display: -moz-box;
                display: -webkit-flex;
                display: -ms-flexbox;
                display: box;
                display: flex;
                -webkit-box-orient: vertical;
                -moz-box-orient: vertical;
                -o-box-orient: vertical;
                -webkit-flex-direction: column;
                -ms-flex-direction: column;
                flex-direction: column;
                width: 8rem
            }

            #titleinfo {
            color: #fffff0;
            font-weight: bolder;
            font-style: normal;
            font-family: Roboto, Netflix-Sans;
            font-size: 150%;
            margin-left: 30px;
            margin-right: 20px;
            position: relative; top: 7px; left: -5px;
            width: 580px;
            flex-shrink: 1;
            white-space: nowrap;
            overflow: hidden;
            text-overflow: ellipsis;
            display: inline-block;
            }



            .issue {
                font-size: 16;
                vertical-align: middle;
                font-family: Netflix-Sans;
                position: relative;
                top: -1px
            }

            .TimedTextEvent .details .timing .TimeCode {
                color: rgba(255, 255, 255, .54);
                padding: 0 .5rem;
                border-radius: 1rem;
                white-space: nowrap
            }

            .TimedTextEvent .details .content {
                -webkit-box-flex: 1;
                -moz-box-flex: 1;
                -o-box-flex: 1;
                -ms-box-flex: 1;
                box-flex: 1;
                -webkit-flex-grow: 1;
                flex-grow: 1;
                display: -webkit-box;
                display: -moz-box;
                display: -webkit-flex;
                display: -ms-flexbox;
                display: box;
                display: flex;
                -webkit-box-orient: vertical;
                -moz-box-orient: vertical;
                -o-box-orient: vertical;
                -webkit-flex-direction: column;
                -ms-flex-direction: column;
                flex-direction: column;
                padding-left: 2rem;
                padding-bottom: .5rem;
                width: 24rem
            }

            .TimedTextEvent .details .content .header {
                display: -webkit-box;
                display: -moz-box;
                display: -webkit-flex;
                display: -ms-flexbox;
                display: box;
                display: flex;
                -webkit-box-orient: horizontal;
                -moz-box-orient: horizontal;
                -o-box-orient: horizontal;
                -webkit-flex-direction: row;
                -ms-flex-direction: row;
                flex-direction: row
            }

            .TimedTextEvent .details .content .header .index {
                color: rgba(255, 255, 255, .66)
            }

            .TimedTextEvent .details .content .header .duration {
                color: rgba(255, 255, 255, .54);
                margin-left: auto;
                border-radius: 1rem
            }

            .TimedTextEvent .details .content .StyledTextEditor {
                color: rgba(255, 255, 255, .87);
                min-height: 3.2rem;
                margin-top: .3rem
            }

            img {
                display: table-cell;
                vertical-align: middle
            }

            #logonetflix {
                margin-left: 12px;
            }

            .TimedTextEvent .footer {
                height: .5rem;
                border-bottom: 2px solid rgba(0, 0, 0, .77);
                margin-left: 1rem;
                margin-right: 1rem
            }

            .outline {
                color: #000;
                position: relative;
                left: 350%;
            }

            button {
                background-color: black;
                color: #39ff1a;
                opacity: 0.5;
            }

            i {
                font-style: italic!important;
                font-family: serif!important
            }

            #feedback {
                height: 0;
                width: 65px;
                position: fixed;
                right: 0;
                top: 50%;
                z-index: 1000;
                transform: rotate(-90deg);
                -webkit-transform: rotate(-90deg);
                -moz-transform: rotate(-90deg);
                -o-transform: rotate(-90deg)
            }

            #feedback a {
                display: block;
                background: #06c;
                height: 15px;
                width: 165px;
                padding: 8px 16px;
                color: #fff;
                font-family: Arial, sans-serif;
                font-size: 17px;
                font-weight: 700;
                text-decoration: none;
                border-bottom: solid 1px #333;
                border-left: solid 1px #333;
                border-right: solid 1px #fff
            }

            #feedback a:hover {
                background: #ccc
            }

            </style><body style="width:70%;height:100%">
            <div id="navbar">
              <a href="https://partnerhelp.netflixstudios.com/hc/en-us/articles/215758617-Timed-Text-Style-Guide-General-Requirements"><img id="logonetflix" width="auto" height="32" src="https://upload.wikimedia.org/wikipedia/commons/0/08/Netflix_2015_logo.svg" title="Netflix TTSG Quality Compliance Initiative"></a>
              <span id="titleinfo" title="TITLEINFOTOKEN">TITLEINFOTOKEN</span>
            </div>
            <div id="background">
            <p class="bg-text" style="top: 0px; left: 100px;">WATERMARKPLACEHOLDER<br>NETFLIX CONFIDENTIAL</p>
            <p class="bg-text" style="top: -100px; left: 540px;">NETFLIX CONFIDENTIAL<br>WATERMARKPLACEHOLDER</p>
                </div>

                <div id="stick" style="position:relative;left:750px;"><button type="button" class="f" onclick="alert(Window.autoQC_safe_meta)">🛈</button><button type="button" class="f" onclick="z(\'cSpace\')" title="Highlight whitespace">🟩</button><button title="Display minor issues" type="button" class="f" onclick="z(\'minor_issue\') ">⚠️</button><button title="Display major issues" type="button" class="f" onclick="z(\'major_issue\')">🚫</button><button title="Shot-change suggestions" type="button" onclick="z(\'shot_change_issue\') ">🎞</button>DLFIXSRTTOKEN<span style="position: relative; top: 3px; left: 165px; font-family: monospace; color: #fdfdf2; opacity: 0.33;">Linguist: LINGUISTNAME</div>`
            if(document.location.href.includes(
                  "originator.backlot.netflix.com"))
            {
               var lsJson = localStorage["clq:origination:" +
                  our_clq]
               if(!lsJson)
               {
                  alert("Timed text events not found in localStorage\nfor CLQ: " +
                     our_clq + "\n" +
                     "If the CLQ is correct and" + "\n" +
                     "\"Save to local storage\" is enabled in Settings," +
                     "\n" + "save the task and try again.");
                  return null
               }
               var json_obj = JSON.parse(lsJson)
               var src = json_obj["events"]
               var fps_ = json_obj["meta"]["fps"];
               var fps = fps_.split("_")[1] / 100
               qcMeta.fps = fps
               qcMeta.fps_ = fps_
            }
            else if(outOfTheBox)
            {
               var fps = qcSavedMeta.fps
               var fps_ = qcSavedMeta.fps_
            }
            else if(typeof our_clq == "object")
            {
               var srt_fps = prompt(
                  "What FPS does this SRT file likely have?\n2397, 2398, 2400, 2500, 2997 are the most likely"
                  )
               if(srt_fps.length < 4)
               {
                  var fps = 24.00;
                  var fps_ = "FPS_2400"
               }
               else
               {
                  var fps_ = "FPS_" + (srt_fps.replace(/[^\d]/g, "")
                     .replace(/(^....)(.+$)/, "$1"))
                  var fps = fps_.split("_")[1] / 100
               }
               alert("Continuing with " + fps_ +
                  " and real fps set to " + fps)
            }
            var majTotal = 0
            var minTotal = 0
            var scTotal = 0
            var evTotal = 0
            var afTotal = 0
            if(typeof our_clq !== "object")
            {
               var proposed_fps = prompt("Framerate set to: " +
                  fps +
                  " fps.\nEnter custom FPS or press Enter to keep current"
                  );
               if(proposed_fps)
               {
                  var int_fps = proposed_fps.substring(0, 2);
                  var decimal_fps = proposed_fps.substring(2,
                     proposed_fps.length);
                  fps_ = "FPS" + "_" + int_fps + decimal_fps
                  fps = (int_fps + "." + decimal_fps) * 1;
                  alert("Continuing with " + fps_ +
                     " and real fps set to " + fps);
               }
            }
            var frms = 1000 / fps
            if(document.location.href.includes(
                  "originator.backlot.netflix.com"))
            {
               var titleInfo = document.getElementsByClassName(
                  "cpe-page-menu-label")[0].innerText.replace(
                  / "/g, ' “').replace(/"/g, "”")
               qcMeta.titleinfo = titleInfo
            }
            else if(typeof our_clq !== "object")
            {
               var titleInfo = qcSavedMeta.titleinfo
            }
            else
            {
               var titleInfo = document.location.href.replace(
                  /(.+?)([^/]+$)/g, "$2")
            }

            function srtName(suffix = "", extension = ".srt")
            {
               var s = titleInfo
               var srtName = (s.replace(/[ ]/g, '_').replace(
                        /[^a-z0-9_]/gi, '') + "_" + suffix +
                     extension
                     ) //This gets rid of all punctuation, spaces and non-English letters
                  .trim() //resulting in a name like 14545_El_Burrito_A_Breaking_Fat_Movie_FPS_2500.srt
               if(!srtName) srtName = our_clq + "_" + suffix +
                  extension
               return srtName
            }

            function frames2tcf(framenumber, framerate = fps)
            { //frames to 00:01:02:24 format
               var seconds_float = framenumber / framerate
               var seconds_int = Math.floor(seconds_float)
               var seconds_frac = seconds_float - seconds_int
               var frames = Math.round(seconds_frac * framerate) +
                  ''
               var date = new Date(0);
               date.setSeconds(seconds_int);
               try
               {
                  var timeString = date.toISOString().substr(11, 8)
                  if(frames.length == 1) frames = "0" + frames
                  var timecodef = timeString + ":" + frames
               }
               catch (e)
               {
                  var timecodef = 'ERROR'
               }
               return timecodef
            }

            function frames2timecode(frames)
            { //frames to 00:01:02,000 format
               var milliseconds = Math.round(frames * frms)
               var srt_timecode = TimeConversion(milliseconds)
               return srt_timecode
            }

            function array2srt(events_object)
            {
               var ordered_events = []
               for(var id in events_object)
               {
                  try
                  {
                     var type_fn = events_object[id]["type"]
                     if(type_fn === "fn")
                     {
                        events_object[id]["txt"] += "<b></b>";
                     }
                  }
                  catch (e)
                  {}
                  ordered_events.push([
                     events_object[id]
                     ["start"],
                     events_object[id]
                     ["end"],
                     events_object[id]
                     ["txt"],
                     events_object[id]
                     ["styles"],
                     events_object[id]
                     ["rgn"]
                  ])
               }
               ordered_events.sort(function (a, b)
               {
                  return a[0] - b[0];
               }); //Array sorted by in_cues, sequentially
               var index = 0
               var srt_txt = ''
               for(event of ordered_events)
               {
                  index++
                  var start = frames2timecode(event[0])
                  var end = frames2timecode(event[1])
                  var content = event[2]
                  try
                  {
                     if(typeof event[3]
                        [0]["type"] !== "undefined")
                     {
                        if(event[3][0]
                           ["type"] == "italic")
                        {
                           content = italicize(content, event[3])
                        }
                     }
                  }
                  catch (e)
                  {}
                  try
                  {
                     if(typeof event[4] !== "undefined")
                     {
                        if(event[4] == "top")
                        {
                           content = "{\\an8}" + content
                        }
                     }
                  }
                  catch (e)
                  {}
                  try
                  {
                     if(event["type"] == "fn")
                     {
                        content += '<b></b>'
                     }
                  }
                  catch (e)
                  {}
                  //                      console.log(content)
                  var current_event = index + "\n" + start +
                     " --> " + end + "\n" + content + "\n"
                  srt_txt += current_event + "\n"
               }
               return srt_txt
            }

            function parsed2srt(parsedSRT)
            {
               var nbspRegex = new RegExp(String.fromCharCode(160),
                  "gi");
               var srt_txt = ''
               for(let event of parsedSRT)
               {
                  var cleanTxt = event["text"].replace(nbspRegex,
                     " ").trim().replace(/[ ][ ]/gi, " ")
                  if(cleanTxt.length < 2)
                  {
                     cleanTxt = "\n"
                  }
                  let text_event = event["id"] + "\n" + event[
                        "start"] + " --> " + event["end"] + "\n" +
                     cleanTxt + "\n\n"
                  srt_txt += text_event
               }
               return srt_txt
            }

            function italicize(content, italics_array)
            {
               var position_offset = 0
               for(var italic of italics_array)
               {
                  var position_from = italic["from"] +
                     position_offset;
                  position_offset += 3
                  content = [content.slice(0, position_from), "<i>",
                     content.slice(position_from)
                  ].join('')
                  var position_to = italic["to"] + position_offset;
                  position_offset += 4
                  content = [content.slice(0, position_to), "</i>",
                     content.slice(position_to)
                  ].join('')
               }
               return content
            }

            function download(filename, text)
            {
               var element = document.createElement('a');
               element.setAttribute('href',
                  'data:text/plain;charset=utf-8,' +
                  encodeURIComponent(text));
               element.setAttribute('download', filename);
               element.style.display = 'none';
               document.body.appendChild(element);
               element.click();
               document.body.removeChild(element);
            }

            function TimeConversion(duration)
            {
               let time = parseDuration(duration)
               return formatTimeHMSS(time)
            }

            function parseDuration(duration)
            {
               let remain = duration
               let hours = Math.floor(remain / (1000 * 60 * 60))
               remain = remain % (1000 * 60 * 60)
               let minutes = Math.floor(remain / (1000 * 60))
               remain = remain % (1000 * 60)
               let seconds = Math.floor(remain / (1000))
               remain = remain % (1000)
               let milliseconds = remain
               return {
                  hours,
                  minutes,
                  seconds,
                  milliseconds
               }
            }

            function formatTimeHMSS(o)
            {
               let hours = o.hours.toString()
               if(hours.length === 1) hours = '0' + hours
               let minutes = o.minutes.toString()
               if(minutes.length === 1) minutes = '0' + minutes
               let seconds = o.seconds.toString()
               if(seconds.length === 1) seconds = '0' + seconds
               let milliseconds = o.milliseconds.toString()
               if(milliseconds.length === 1) milliseconds = '00' +
                  milliseconds
               if(milliseconds.length === 2) milliseconds = '0' +
                  milliseconds
               return hours + ":" + minutes + ":" +
                  //Example: 00:01:02,999 -- note that the SRT spec calls for a comma, not a period!
                  seconds + "," + milliseconds
            }

            function isBetween(distance, a, b)
            {
               var left_lim = a;
               var right_lim = b;
               if(a > b)
               {
                  left_lim = b;
                  right_lim = a
               }
               if(distance >= left_lim && distance <= right_lim)
               {
                  return true
               }
               return false
            }

            function validateScE(distance, framesE, sc, distance2)
            {
               var violation = false
               var V = {}
               var css_cs1 = '<span class="shot_change_issue">';
               var css_cs2 = '</span>';
               var oldTcf = frames2tcf(framesE)
               var scTcf = frames2tcf(sc)
               var distanceNext = distance2
               if(isBetween(distance, 5, 11))
               {
                  var moveTo = 12 - distance
                  var newFramesE = framesE + moveTo
                  var newTcf = frames2tcf(newFramesE)
                  var intTo = (moveTo > 0) ? "+" + moveTo : moveTo;
                  moveTo = moveToSigned(moveTo)
                  violation = '🎞 Move out-cue right ' + moveTo +
                     ' to +12 frames after shot change<br><font style="color: #7DFDFE; font-family: monospace">[OUT-CUE: ' +
                     oldTcf + ' ' + intTo + ' = ' + newTcf +
                     ']</font>'
               }
               if(isBetween(distance, -1, 4))
               {
                  var moveTo = -2 - distance
                  var newFramesE = framesE + moveTo
                  var newTcf = frames2tcf(newFramesE)
                  var intTo = (moveTo > 0) ? "+" + moveTo : moveTo;
                  moveTo = moveToSigned(moveTo)
                  violation = '🎞 Move out-cue left ' + moveTo +
                     ' to -2 frames before shot change<br><font style="color: #7DFDFE; font-family: monospace">[OUT-CUE: ' +
                     oldTcf + ' ' + intTo + ' = ' + newTcf +
                     ']</font>'
               }
               if(isBetween(distance, -3, -7))
               {
                  var moveTo = -2 - distance
                  var newFramesE = framesE + moveTo
                  var newTcf = frames2tcf(newFramesE)
                  var intTo = (moveTo > 0) ? "+" + moveTo : moveTo;
                  moveTo = moveToSigned(moveTo)
                  violation = '🎞 Move out-cue right ' + moveTo +
                     ' to -2 before shot change<br><font style="color: #7DFDFE; font-family: monospace">[OUT-CUE: ' +
                     oldTcf + ' ' + intTo + ' = ' + newTcf +
                     ']</font>'
               }
               if(distanceNext > 2 && distanceNext < 12 &&
                  distanceNext <= Math.abs(distance)
                  ) //fixes nasty, nasty situation!
               {
                  var moveTo = distanceNext - 2
                  var newFramesE = framesE + moveTo
                  var newTcf = frames2tcf(newFramesE)
                  var intTo = (moveTo > 0) ? "+" + moveTo : moveTo;
                  moveTo = moveToSigned(moveTo)
                  violation = '🎞 Move out-cue right ' + moveTo +
                     ' to reduce frame gap from ' + distanceNext +
                     ' to 2.<br><font style="color: #7DFDFE; font-family: monospace">[OUT-CUE: ' +
                     oldTcf + ' ' + intTo + ' = ' + newTcf +
                     ']</font>'
               }
               if(cpsFixable)
               {
                  var moveTo = framesToFixCPS
                  var newFramesE = framesE + framesToFixCPS
                  var newTcf = frames2tcf(newFramesE)
                  var intTo = (moveTo > 0) ? "+" + moveTo : moveTo;
                  moveTo = moveToSigned(moveTo)
                  violation = '📖 Move out-cue right ' + moveTo +
                     ' to reduce CPS to 17.<br><font style="color: #7DDDFF; font-family: monospace">[OUT-CUE: ' +
                     oldTcf + ' ' + intTo + ' = ' + newTcf +
                     ']</font>'
                  cpsFixable = false
               }
               if(cpsAttemptable)
               {
                  var moveTo = framesToFixCPS
                  var newFramesE = framesE + framesToFixCPS
                  var newTcf = frames2tcf(newFramesE)
                  var intTo = (moveTo > 0) ? "+" + moveTo : moveTo;
                  moveTo = moveToSigned(moveTo)
                  cpsAttemptable = false
                  violation = '📖 Move out-cue right ' + moveTo +
                     ' to reduce CPS to ' + bestWorstCPS +
                     '.<br><font style="color: #7DDDFF; font-family: monospace">[OUT-CUE: ' +
                     oldTcf + ' ' + intTo + ' = ' + newTcf +
                     ']</font>'
                  cpsAttemptable = false
               }
               if(Math.abs(distanceNext) < 2)
               {
                  var moveTo = (2 + Math.abs(distanceNext)) * (-1)
                  var newFramesE = framesE + moveTo
                  var newTcf = frames2tcf(newFramesE)
                  var intTo = (moveTo > 0) ? "+" + moveTo : moveTo;
                  moveTo = moveToSigned(moveTo)
                  violation = '🎞 Move out-cue ' + moveTo +
                     ' to -2 before next subtitle event<br><font style="color: #7DFDFE; font-family: monospace">[OUT-CUE: ' +
                     oldTcf + ' ' + intTo + ' = ' + newTcf +
                     ']</font>'
               }
               if(violation)
               {
                  scTotal++
                  afTotal++
                  V = {
                     violation: css_cs1 + violation + css_cs2,
                     start: undefined,
                     end: frames2timecode(newFramesE)
                  }
                  return V;
               }
               return false
            }

            function moveToSigned(moveTo)
            {
               var frames = " frames"
               if(Math.abs(moveTo) == 1)
               {
                  var frames = " frame"
               }
               if(moveTo <= 0)
               {
                  return moveTo + frames
               }
               else
               {
                  return '+' + moveTo + frames
               }
            }

            function validateScS(distance, framesE, sc)
            {
               var violation = false
               var V = {}
               var css_cs1 = '<span class="shot_change_issue">';
               var css_cs2 = '</span>';
               var oldTcf = frames2tcf(framesE)
               var scTcf = frames2tcf(sc)
               if(isBetween(distance, 1, 10))
               {
                  var moveTo = 0 - distance
                  var newFramesE = framesE + moveTo
                  var newTcf = frames2tcf(newFramesE)
                  var intTo = (moveTo > 0) ? "+" + moveTo : moveTo;
                  moveTo = moveToSigned(moveTo)
                  violation = '🎞 Move in-cue left ' + moveTo +
                     ' to snap it to shot change<br><font style="color: #7FFFD4; font-family: monospace">[IN-CUE: ' +
                     oldTcf + ' ' + intTo + ' = ' + newTcf +
                     ']</font>'
               }
               if(isBetween(distance, 11, 11))
               {
                  var moveTo = 1
                  var newFramesE = framesE + moveTo
                  var newTcf = frames2tcf(newFramesE)
                  var intTo = (moveTo > 0) ? "+" + moveTo : moveTo;
                  moveTo = moveToSigned(moveTo)
                  violation = '🎞 Move in-cue right ' + moveTo +
                     ' to put 12 frames after shot change<br><font style="color: #7FFFD4; font-family: monospace">[IN-CUE: ' +
                     oldTcf + ' ' + intTo + ' = ' + newTcf +
                     ']</font>'
               }
               if(isBetween(distance, 10, 10))
               {
                  var moveTo = 2
                  var newFramesE = framesE + moveTo
                  var newTcf = frames2tcf(newFramesE)
                  var intTo = (moveTo > 0) ? "+" + moveTo : moveTo;
                  moveTo = moveToSigned(moveTo)
                  violation = '🎞 Move in-cue right ' + moveTo +
                     ' to +12 frames after shot change<br><font style="color: #7FFFD4; font-family: monospace">[IN-CUE: ' +
                     oldTcf + ' ' + intTo + ' = ' + newTcf +
                     ']</font>'
               }
               if(isBetween(distance, -11, -5))
               {
                  var moveTo = -12 - distance
                  var newFramesE = framesE + moveTo
                  var newTcf = frames2tcf(newFramesE)
                  var intTo = (moveTo > 0) ? "+" + moveTo : moveTo;
                  moveTo = moveToSigned(moveTo)
                  violation = '🎞 Move in-cue left ' + moveTo +
                     ' to -12 frames before shot change<br><font style="color: #7FFFD4; font-family: monospace">[IN-CUE: ' +
                     oldTcf + ' ' + intTo + ' = ' + newTcf +
                     ']</font>'
               }
               if(isBetween(distance, -4, -1))
               {
                  var moveTo = 0 - distance
                  var newFramesE = framesE + moveTo
                  var newTcf = frames2tcf(newFramesE)
                  var intTo = (moveTo > 0) ? "+" + moveTo : moveTo;
                  moveTo = moveToSigned(moveTo)
                  violation = '🎞 Move in-cue right ' + moveTo +
                     ' to snap it to shot change<br><font style="color: #7FFFD4; font-family: monospace">[IN-CUE: ' +
                     oldTcf + ' ' + intTo + ' = ' + newTcf +
                     ']</font>'
               }
               if(violation)
               {
                  scTotal++
                  afTotal++
                  V = {
                     violation: css_cs1 + violation + css_cs2,
                     start: frames2timecode(newFramesE),
                     end: undefined
                  }
                  return V
               }
               return false
            }

            function getNextShotchange(frame)
            {
               if(!shotChanges)
               {
                  return false
               }
               else
               {
                  var smallestDist = Number.POSITIVE_INFINITY
                  var bestCandidate = 0
                  for(let shotChange of shotChanges)
                  {
                     var distance = shotChange - frame
                     if(distance < smallestDist && shotChange >=
                        frame)
                     {
                        smallestDist = distance
                        bestCandidate = shotChange * 1
                     }
                  }
                  return bestCandidate
               }
            }
            //returns distance in frames from a given frame to the nearest shot change
            function getNearestShotchange(frame)
            {
               if(!shotChanges)
               {
                  return false
               }
               else
               {
                  var smallestDist = Number.POSITIVE_INFINITY
                  var bestCandidate = 0
                  for(let shotChange of shotChanges)
                  {
                     var distance = Math.abs(frame - shotChange)
                     if(distance < smallestDist)
                     {
                        smallestDist = distance
                        bestCandidate = shotChange * 1
                     }
                  }
                  return bestCandidate
               }
            }

            function leftFillNum(num, targetLength)
            {
               return num.toString().padStart(targetLength, 0);
            }
            if(src && !outOfTheBox)
            {
               console.log(
                  "DEBUG: In Originator, working with json_src")
               var SRT = array2srt(src)
               SRT = parseSRT(SRT)
            }
            else if(typeof our_clq !== "object" && outOfTheBox)
            {
               console.log(
                  "DEBUG: In AutoQC, working with encoded fixed")
               var SRT = decodeURIComponent(document.getElementById(
                  "FixedTaskSRT").href).replace(
                  "data:text/plain;charset=utf-8,", "")
               SRT = parseSRT(SRT)
            }
            else if(typeof our_clq == "object")
            {
               console.log(
                  "DEBUG: In SRT file, working with document body"
                  )
               var SRT = our_clq.join("\n").trim()
               SRT = parseSRT(SRT)
            }
            var fSRT = SRT
            var issues = []
            var events_detected = 0
            for(var i = 0; i < SRT.length; i++)
            {
               var end_current = SRT[i]["end"]
               if(typeof (SRT[i + 1]) !== "undefined")
               {
                  var start_next = SRT[i + 1]["start"]
               }
               else
               {
                  var start_next = 23976023976
               }
               var distance = Math.round(
                  (start_next - end_current) * fps)
               SRT[i]["frames_to_next"] = distance
            }
            for(event of SRT)
            {
               var start_time = event["start"]
               var end_time = event["end"]
               var delta = Math.round((end_time - start_time) *
                  1000) / 1000
               var delta_fps = Math.floor(delta) + ":" + Math.round(
                  (delta - Math.floor(delta)) * fps)
               var start_frames = Math.round(start_time * fps)
               var end_frames = Math.round(end_time * fps)
               var start_tcf = frames2tcf(start_frames)
               var end_tcf = frames2tcf(end_frames)
               var id = event["id"]
               var srtId = id - 1;
               var distanceNextEvent = event["frames_to_next"]
               var _id = leftFillNum(id, 4)
               var eventIssue = []
               var eventTxt = event["text"]
               var bareTxt = eventTxt.replace(/\n/g, "‣").replace(
                  /<(|\/)(i|b)>/g, "").replace(/\{.an.\}/g, "")
               var totalLength = bareTxt.length - 1
               var cps = (Math.round((totalLength / delta) *
                  1000)) / 1000
               var duration_for_normal_cps = (Math.round((
                  totalLength / normal_cps) * 1000)) / 1000
               var framesToFixCPS = Math.floor(((((
                     duration_for_normal_cps - delta) *
                  1000) * fps) / 1000) + 1)
               var round_int_cps = Math.floor(cps + 1);
               var html_cps = ''
               var eventFixedTxt = event["text"]
               var eventOriginalTxt = event["text"]
               var eventIsFn = (eventTxt.indexOf("<b></b>") !== -1)
               var eventIsFnInCaps = eventIsFn && (LANG !== 'CN') &&
                  (eventTxt.toUpperCase().trim() === eventTxt
                  .trim())
               var scS = getNearestShotchange(start_frames)
               var scE = getNearestShotchange(end_frames)
               var scN = getNextShotchange(end_frames)
               var scND = scN - end_frames
               var scSD = start_frames - scS
               var scED = end_frames - scE
               var cpsFixable = false
               var cpsAttemptable = false
               var bestWorstCPS = false
               if(cps > normal_cps && cps < max_cps && scND -
                  framesToFixCPS > 11 && distanceNextEvent -
                  framesToFixCPS > 11 && framesToFixCPS < 12 && !
                  eventIsFnInCaps)
               {
                  cpsFixable = true
                  //var log = "EVENT: "+id+"\nCPS: "+cps+"\tDuration: "+delta_fps+"\nTAR: //"+normal_cps+".00\tWant_dur: "+duration_for_normal_cps+
                  //"\nEnd_frame: "+end_frames+"\nNex_ev in: "+distanceNextEvent+"\nNex_sc in: "
                  //+scND+"\nADD_FRAM: "+framesToFixCPS+"\nTotal length: "
                  //+totalLength+"\nEND_TIMECODE_1: "+frames2tcf(end_frames+framesToFixCPS)+"\nEND_TIMECODE_2: "+
                  //frames2tcf(Math.round((start_time + duration_for_normal_cps)*fps))
               }
               if(cps >= max_cps && scND - 12 > 11 &&
                  distanceNextEvent - 12 > 11 && !eventIsFnInCaps)
               {
                  cpsAttemptable = true
                  framesToFixCPS = 12
                  var plusDelta = 12 / fps
                  bestWorstCPS = Math.round((totalLength / (delta +
                     plusDelta) * 1000)) / 1000
               }
               var scStcf = frames2tcf(scS)
               var scEtcf = frames2tcf(scE)
               var violS = validateScS(scSD, start_frames, scS)
               var violE = validateScE(scED, end_frames, scE,
                  distanceNextEvent, cps)
               if(violS)
               {
                  eventIssue.push(violS["violation"])
                  fSRT[srtId]["start"] = violS["start"];
               }
               else
               {
                  fSRT[srtId]["start"] = frames2timecode(Math.round(
                     fSRT[srtId]
                     ["start"] * fps))
               }
               if(violE)
               {
                  eventIssue.push(violE["violation"])
                  fSRT[srtId]["end"] = violE["end"];
               }
               else
               {
                  fSRT[srtId]["end"] = frames2timecode(Math.round(
                     fSRT[srtId]
                     ["end"] * fps))
               }
               if(eventTxt.indexOf("<b></b>") !== -1)
               {
                  eventIsFn = true;
                  _id +=
                     '<span class="outline" style="color: purple; position: relative; top: 0px; left: 137px;">🅵🅽</span>'
               }
               eventTxt = eventTxt.replace(/(<([^>]+)>)/ig, "");
               eventTxt = eventTxt.replace(/{.an.}/ig, "");
               if(eventTxt.indexOf("\n") !== -1 && eventTxt.indexOf(
                     "\n-") == -1 && eventTxt.replace("\n", " ")
                  .length <= line_limit && !eventIsFnInCaps && !
                  hasSafeBox)
               {
                  eventIssue.push(
                     "<span class=\"major_issue\">🚫 Can fit on one line (" +
                     line_limit + " for " + LANG + ")")
                  majTotal++
                  eventFixedTxt = eventFixedTxt.replace(/\n/g, " ")
                  eventFixedTxt = eventFixedTxt.replace(/ /gi, " ")
                     .replace(/[ ][ ]/g, ' ');
                  afTotal++
               }
               if(eventTxt.indexOf("...") !== -1)
               {
                  eventIssue.push(
                     "<span class=\"minor_issue\">⚠️ Legacy ellipsis detected, replace with U+2026: …"
                     );
                  minTotal++
                  eventFixedTxt = eventFixedTxt.replace(/\.\.\./g,
                     "…")
                  afTotal++
               }
               if(eventTxt.indexOf("'") !== -1 || eventTxt.indexOf(
                     '"') !== -1)
               {
                  switch (LANG)
                  {
                  case "EN":
                     eventIssue.push(
                        "<span class=\"minor_issue\">⚠️ Smart quotes recommended for EN: <b>“   ”   ’</b>"
                        );
                     minTotal++
                     eventFixedTxt = eventFixedTxt.replace(
                        /"( |\n|$)/g, "”$1")
                     eventFixedTxt = eventFixedTxt.replace(/"/g,
                        "“")
                     eventFixedTxt = eventFixedTxt.replace(/'/g,
                        "’")
                     afTotal++
                     break;
                  case "RU":
                     eventIssue.push(
                        "<span class=\"minor_issue\">⚠️ Proper Russian uses chevrons: <b>« »</b>"
                        );
                     minTotal++
                     eventFixedTxt = eventFixedTxt.replace(
                        /"( |\n|$)/g, "»$1")
                     eventFixedTxt = eventFixedTxt.replace(/"/g,
                        "«")
                     afTotal++
                     break;
                  }
               }
               if(eventTxt.indexOf("!?") !== -1)
               {
                  eventIssue.push(
                     "<span class=\"minor_issue\">⚠️ Unconventional punctuation detected. Did you mean to use '?!'"
                     )
                  minTotal++
                  eventFixedTxt = eventFixedTxt.replace(/[!][?]/g,
                     "?!")
                  afTotal++
               }
               if(eventTxt.replace(/\.\.\./g, "…").search(
                     /(([,.:])(!?([^\d\s\.])))/) !== -1)
               {
                  eventIssue.push(
                     "<span class=\"minor_issue\">⚠️ No space after a punctuation mark. Is this deliberate?"
                     )
                  minTotal++
                  eventFixedTxt = eventFixedTxt.replace(
                        /(([,.:])(!?([^\d\s\.])))/gi, "$2 $4")
                     .replace(/\. \./gi, "..")
                  afTotal++
               }
               if(eventTxt.indexOf(" - ") !== -1)
               {
                  eventIssue.push(
                     "<span class=\"minor_issue\">⚠️ Instead of hyphen, consider en or em dash: – or —"
                     )
                  minTotal++
               }
               var eventSpecial = false
               var hasSpace = /\s$|^\s/;
               var eventTxt_lines = eventTxt.split("\n");
               var nPos = eventTxt.lastIndexOf("\n");
               var hasSpaceIssue = nPos && (typeof eventTxt[nPos +
                  1] == "undefined")
               var overLineLimit = false
               var hasDoubleSpace = eventTxt.replace("\n", "")
                  .split(/\s\s/).length - 1
               var hasThreeLines = typeof eventTxt_lines[2] !==
                  "undefined"
               for(var line of eventTxt_lines)
               {
                  hasSpaceIssue = (hasSpace.test(line) ||
                     hasSpaceIssue)
                  if(line.length > line_limit)
                  {
                     eventSpecial = line
                     overLineLimit = true
                  }
               }
               if(hasDoubleSpace)
               {
                  eventIssue.push(
                     "<span class=\"minor_issue>\"<span class=\"minor_issue\">⚠️ Double spaces detected, replace with single space</span>"
                     );
                  minTotal++
               }
               if(hasThreeLines)
               {
                  eventIssue.push(
                     "<span class=\"major_issue\">🚫 Three lines detected in this event"
                     );
                  majTotal++
               }
               if(hasSpaceIssue)
               {
                  if(eventTxt.length > 1)
                  {
                     eventIssue.push(
                        "<span class=\"minor_issue\">⚠️ One of the lines begins or ends with whitespace"
                        );
                     try
                     {
                        eventFixedTxt = eventFixedTxt.replace(
                           /( |)\n( |)/, "‣").replace(
                           /[ ]{2,}/gi, " ").trim().replace("‣",
                           "\n")
                        eventFixedTxt = eventFixedTxt.replace(
                           ' <b></b>', '<b></b>').replace(
                           ' </i><b></b>', '</i><b></b>')
                        eventFixedTxt = eventFixedTxt.replace(
                           /( )(<(|\/)i>)(\n)/g, "$2$4")
                        eventFixedTxt = eventFixedTxt.replace(
                           '<i></i>', '')
                        afTotal++
                     }
                     catch (e)
                     {
                        eventFixedTxt = eventFixedTxt.replace(
                           /( |)\n( |)/, "×").replace(/ \s+/gi,
                           " ").trim().replace("×", "\n")
                        afTotal++
                     }
                  }
                  else
                  {
                     eventIssue.push(
                        "<span class=\"major_issue\">🚫 Empty event detected!"
                        );
                     majTotal++
                     eventSpecial =
                        "<span style='color:red;'>&lt;text cannot be empty&gt;</span>"
                  }
               }
               if(overLineLimit)
               {
                  eventIssue.push(
                     "<span class=\"major_issue\">🚫 Line over max limit of " +
                     line_limit + " for " + LANG)
                  majTotal++
                  eventSpecial = [
                     eventSpecial.slice(0, line_limit),
                     "<span style='color: red; text-overflow: fade;'>",
                     eventSpecial.slice(line_limit)
                  ].join('')
               }
               if(delta * delta > 49)
               {
                  eventIssue.push(
                     "<span class=\"major_issue\">🚫 Maximum duration exceeded for current FPS!"
                     )
                  majTotal++
               }
               // var halfLength = totalLength - Contrary to official statement,
               // ( eventTxt.replace(/[ ]/g, '') Originator does not count
               // .replace(/[^a-z0-9]/gi Western punctuation and spaces
               // , '').length); as 0.5 characters for CPS.*/
               //
               // totalLength += (halfLength/(-2))
               if(cps > normal_cps && cps < max_cps)
               {
                  html_cps =
                     '<div><span style="color: gold;">&nbsp;&nbsp;&emsp;' +
                     round_int_cps + ' c/s</span></div>'
               }
               else if(cps > (max_cps))
               {
                  eventIssue.push(
                     "<span class=\"major_issue\">🚫 Reading speed above maximum! Either truncate text or extend timings!"
                     )
                  majTotal++
                  html_cps =
                     '<div><span style="color:red;">&nbsp;&nbsp;&emsp;' +
                     round_int_cps + ' c/s</span></div>'
               }
               else
               {
                  html_cps =
                     '<div><span style="color:silver;">&nbsp;&nbsp;&emsp;' +
                     round_int_cps + ' c/s</span></div>'
               }
               var dur_tcf = frames2tcf(delta * fps).substr(6)
               var eventLines = eventTxt.trim().split("\n")
               if(typeof eventIssue !== "undefined" && eventIssue
                  .length > 0)
               {
                  events_detected++;
                  var log_event = (start_tcf + " " + eventTxt_lines[
                     0].slice(0, line_limit).padEnd(
                     line_limit + 4, " ") + _id).padEnd(
                     line_limit + 10, " ")
                  if(typeof eventTxt_lines[1] !== "undefined")
                  {
                     log_event += "\n" + end_tcf + " " +
                        eventTxt_lines[1].padEnd(line_limit + 8,
                           " ")
                  }
                  else
                  {
                     log_event += "\n" + end_tcf + " " + " ".padEnd(
                        line_limit + 6, " ")
                  }
                  console.log('%c' + log_event,
                     'background: #12343b; color: #66ff00')
                  console.log((+_id + "\n" + start_tcf + "\n" +
                        end_tcf + "\n" + eventTxt).padEnd(50),
                     'background: #222; color: #bada55');
                  console.log(('%c' + eventIssue.join("\n")).padEnd(
                        50),
                     'background: #FFF; color: #FF0000; font-style: italic; border:solid 1px #000;'
                     )
                  console.log("┉".repeat(63))
                  var issue = {
                     id: _id,
                     issues: eventIssue,
                     content: eventTxt,
                     contentO: eventOriginalTxt,
                     start_tcf: start_tcf,
                     end_tcf: end_tcf,
                     start_frames: start_frames,
                     end_frames: end_frames,
                     start_time: start_time,
                     end_time: end_time,
                     duration: delta,
                     cps: cps,
                     html_cps: html_cps,
                     dur_tcf: dur_tcf,
                     special: eventSpecial
                  }
                  htmlize(issue)
                  if(eventFixedTxt !== eventOriginalTxt)
                  {
                     fSRT[srtId]["text"] = eventFixedTxt
                  }
               }
            }
            if(!events_detected)
            {
               console.log('%c' + "No issues detected",
                  'background: #12343b; color: #66ff00')
            }

            function htmlize(issue)
            {
               try
               {
                  var original = issue.contentO.replace('<b></b>',
                     '').replace(/{.an.}/g, '')
                  var lines = original
               }
               catch (e)
               {
                  var lines = issue.content;
               }
               lines = lines.replace(/\n/g, '⏎');
               lines = lines.replace(/\s/g,
                  "<span class='cSpace'>⎵</span>");
               lines = lines.replace(//g,
                  "<span class='cSpace' style=\"color: #00fa00;\">⏎</span>\n"
                  );
               lines = lines.split("\n");
               if(issue.special)
               {
                  var check_length = lines[0].length;
                  lines[0] = issue.special
                  if(check_length > line_limit)
                  {
                     var save_l0 = issue.special.substring(
                        line_limit - 1)
                     lines[0] = issue.special.substring(0,
                           line_limit - 1).replace(/\s/g,
                           "<span class='cSpace'>⎵</span>") +
                        save_l0
                  }
               }
               var timedTextEvent =
                  '<div class="TimedTextEvent" style="width: 700px; max-width: 100%;">' +
                  '<div class="details">' +
                  '<div class="timing"><span class="TimeCode">' +
                  issue.start_tcf + '</span>' +
                  '<span class="TimeCode">' + issue.end_tcf +
                  '</span></div><div class="content"><div class="header">' +
                  '<span class="index">' + issue.id + '</span>' +
                  issue.html_cps +
                  '<span class="duration"><span class="TimeCode">' +
                  issue.dur_tcf + '</span></span>' +
                  '</div><div class="StyledTextEditor" dir="ltr" style="position:relative; top:-20px;">' +
                  '<pre><span>' + lines[0];
               if(typeof lines[1] == "undefined")
               {
                  lines[1] = ""
               }
               timedTextEvent += '</span><div><span>' + lines[1] +
                  '</span></div>'
               if(typeof lines[2] !== "undefined")
               {
                  timedTextEvent +=
                     '<div><span style="color:red;">' + lines[2] +
                     '</span></div></pre>'
               }
               else
               {
                  timedTextEvent += "</pre>"
               }
               for(var each_issue of issue.issues)
               {
                  var issueWithImg =
                     '<div style="vertical-align:middle;float:left;">' +
                     "<span class='issue'>" + each_issue +
                     "</span></span></div>"
                  timedTextEvent += issueWithImg + "<br />"
               }
               reportHtml += timedTextEvent +
                  "</div></div></div></div></div>"
               evTotal++
            }
            var withSC = ''
            if(shotChanges)
            {
               withSC = "_withShotChanges"
            }
            var fixesFilename = srtName("Fixes_" + LANG + "_" +
               fps_ + withSC)
            var fixedSrtFull = parsed2srt(fSRT)
            fixedSrtFull = fixedSrtFull.replace(/ <b><\/b>/g,
               '<b></b>')
            var suggestedFixesTxt =
               '<a id="FixedTaskSRT" download="' + fixesFilename +
               '" href="data:text/plain;charset=utf-8,' +
               encodeURIComponent(fixedSrtFull) +
               '"><button>✎</button></a>'
            qcMeta.results = {
               totalEventsAffected: evTotal,
               majorIssuesFound: majTotal,
               minorIssuesFound: minTotal,
               shotChangeViolations_found: scTotal,
               autoFixesDone: afTotal
            }

            function getUnnamed()
            {
               function n(n)
               {
                  for(let e = n.length - 1; e > 0; e--)
                  {
                     let i = Math.floor(Math.random() * (e + 1));
                     [n[e], n[i]] = [n[i], n[e]]
                  }
               }
               var e = ["Beaver", "Koala", "Chipmunk", "Goat",
                  "Duckling", "Cheetah", "Platypus", "Eagle",
                  "Mongoose", "Butterfly"
               ];
               n(e);
               var i = ["An Intrepid", "A Meek", "A Feisty",
                  "A Lazy", "A Sly", "An Unperturbed",
                  "An Amused", "A Rabid", "A Very", "A Really"
               ];
               n(i);
               var t = [" Little ", " Humongous ", " Tiny ",
                  " Vain ", " Finicky ", " Cunning ", " Exotic ",
                  " Weary ", " Opinionated ", " Conniving "
               ];
               return n(t), i[7] + t[7] + e[7]
            }
            localStorage.removeItem("shotChanges:" + qcMeta.clq)
            var qcMetaStr = "`" + JSON.stringify(qcMeta, null, 2) +
               "`"
            if(typeof userName == "undefined")
            {
               userName = getUnnamed()
            }
            reportHtml = reportHtml.replace("DLFIXSRTTOKEN",
               suggestedFixesTxt).replace(
               /WATERMARKPLACEHOLDER/g, userIp).replace(
               "TITLEINFOTOKEN", titleInfo).replace(
               "TITLEINFOTOKEN", titleInfo).replace(
               "LINGUISTNAME", userName).replace("METADATATOKEN",
               qcMetaStr)
            console.log(qcMeta.results)
            download(srtName("Auto_QC_Log_" + LANG + '_' + fps_ +
               withSC, ".html"), reportHtml)
         }
         return runAutoQC
      })));
      (function (global, factory)
      {
         typeof exports === 'object' && typeof module !== 'undefined' ?
            module.exports = factory() : typeof define === 'function' &&
            define.amd ? define(factory) : (global.parseSRT = factory());
      }(this, (function ()
      {
         'use strict';

         function toSeconds(time)
         {
            var t = time.split(':');
            try
            {
               var s = t[2].split(',');
               if(s.length === 1)
               {
                  s = t[2].split('.');
               }
               return parseFloat(t[0], 10) * 3600 + parseFloat(t[1],
                  10) * 60 + parseFloat(s[0], 10) + parseFloat(s[
                  1], 10) / 1000;
            }
            catch (e)
            {
               return 0;
            }
         }

         function nextNonEmptyLine(linesArray, position)
         {
            var idx = position;
            while(!linesArray[idx])
            {
               idx++;
            }
            return idx;
         }

         function lastNonEmptyLine(linesArray)
         {
            var idx = linesArray.length - 1;
            while(idx >= 0 && !linesArray[idx])
            {
               idx--;
            }
            return idx;
         }

         function parseSRT()
         {
            var data = arguments.length > 0 && arguments[0] !==
               undefined ? arguments[0] : '';
            var subs = [];
            var lines = data.split(/(?:\r\n|\r|\n)/gm);
            var endIdx = lastNonEmptyLine(lines) + 1;
            var idx = 0;
            var time = void 0;
            var text = void 0;
            var sub = void 0;
            for(var i = 0; i < endIdx; i++)
            {
               sub = {};
               text = [];
               i = nextNonEmptyLine(lines, i);
               sub.id = parseInt(lines[i++], 10);
               time = lines[i++].split(/[\t ]*-->[\t ]*/);
               sub.start = toSeconds(time[0]);
               idx = time[1].indexOf(' ');
               if(idx !== -1)
               {
                  time[1] = time[1].substr(0, idx);
               }
               sub.end = toSeconds(time[1]);
               while(i < endIdx && lines[i])
               {
                  text.push(lines[i++]);
               }
               sub.text = text.join('\\N').replace(/{.an8}/,
                  'POSToppyTAG').replace(
                  /\{(\\[\w]+\(?([\w\d]+,?)+\)?)+\}/gi, '');
               sub.text = sub.text.replace(/</g, '&lt;').replace(
                  />/g, '&gt;');
               sub.text = sub.text.replace(
                  /&lt;(\/?(font|b|u|i|s))((\s+(\w|\w[\w\-]*\w)(\s*=\s*(?:".*?"|'.*?'|[^'">\s]+))?)+\s*|\s*)(\/?)&gt;/gi,
                  '<$1$3$7>');
               sub.text = sub.text.replace(/\\N/gi, "\n");
               sub.text = sub.text.replace('POSToppyTAG', '{\\an8}')
               subs.push(sub);
            }
            return subs;
         }
         return parseSRT;
      })));
      setShotChanges()
      defer(runAutoQC)
   })()
}

function marunettuDoubleExportSRT()
{
   (function ()
   {
      var our_clq = document.location.href.toString().split("=")[1].split(
         ":")[2]
      var clq_pattern = new RegExp(
         '^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$',
         'i');
      if(!our_clq || !clq_pattern.test(our_clq))
      {
         alert("You must be in a started, saved Originator task!");
         throw new Error
      }
      if(!clq_pattern.test(our_clq))
      {
         alert("The CLQ is invalid: " + our_clq + "\n" +
            "Please report me to the developer.");
         throw new Error
      }
      var bilingualHtml = `<head>
    <script>
        var prevScrollpos=window.pageYOffset;
window.onscroll=function() {
    var currentScrollPos=window.pageYOffset;
    if (prevScrollpos > currentScrollPos) {
        document.getElementById("navbar").style.top="0";
    }
    else {
        document.getElementById("navbar").style.top="-50px";
    }
    prevScrollpos=currentScrollPos;
}

</script>
<style>#navbar {
    background-color: #333;
    top: 0;
    position: fixed;
    width: 80%;
    left: 10%;
    transition: top 0.5s;
    margin-left: auto;
    margin-right: auto;
    opacity: 0.7;
}

#navbar a {
    float: left;
    display: block;
    color: white;
    text-align: center;
    padding: 5px;
    text-decoration: none;
}

#navbar a:hover {
    background-color: #ddd;
    color: black;
}

body {
    background: #212121;
    color: grey;
}

table {
    width: 80%;
    table-layout: fixed;
    text-align: center;
    margin-left: auto;
    margin-right: auto;
    margin-top: 60px;
        border-collapse: collapse;

}

thead,
tbody,
td,
tr
 {
    display: block;
    font-family: monospace; 

}

tr {

background-image: linear-gradient(to right, darkgrey 30%, rgba(255,255,255,0) 0%);
background-position: bottom;
background-size: 3px 1px;
background-repeat: repeat-x;
}

tr:after {
    content: ' ';
    display: block;
    visibility: hidden;
    clear: both;
        border-bottom: 1px solid #ccc;

}

  #titleinfo {
            color: #fffff0;
            font-weight: bolder;
            font-style: normal;
            font-family: Roboto;
            font-size: 150%;
            margin-left: 55px;
            margin-top: 8px;
            flex-shrink: 1;
            width: 800px;
            white-space: nowrap;
            overflow: hidden;
            text-overflow: ellipsis;
            display: inline-block;
            }


thead th {
    height: 30px;
    /*text-align: left;*/
}


thead {
    /* fallback */
}

tbody td,
thead th {
    width: 33%;
    float: left;
}

tr:hover {
    color: darkgrey;
}


    </style>
<head><body>
         <div id="navbar">
              <a href="TITLELINK"><img id="logonetflix" width="auto" height="32" src="https://upload.wikimedia.org/wikipedia/commons/0/08/Netflix_2015_logo.svg" title="Backup made on TITLEDATE"></a>
              <span id="titleinfo" title="TITLEINFO">TITLEINFO (TITLEDATE)</span></div>
<table>
    <thead>
    <tr>
        <th><b><u>In-cue/Out-cue</b></u></th>
        <th><b><u>Source</b></u></th>
        <th><b><u>Translation</b></u></th>
    </tr>
    </thead>
    <tbody>
    `
      var lsJson = localStorage["clq:origination:" + our_clq]
      if(!lsJson)
      {
         alert("Timed text events not found in localStorage\nfor CLQ: " +
            our_clq + "\n" + "If the CLQ is correct and" + "\n" +
            "\"Save to local storage\" is enabled in Settings," + "\n" +
            "save the task and try again.");
         throw new Error
      }
      var json_obj = JSON.parse(lsJson)
      var src = json_obj["events"]
      var fps_ = json_obj["meta"]["fps"]
      var fps = fps_.split("_")[1] / 100
      //From {"fps":"FPS_2500"}
      var proposed_fps = prompt(
         "DOUBLE EXPORT SRT reports…\n\nPress Enter to accept framerate of " +
         fps + " or enter new as 2400 or 2997:");
      if(proposed_fps !== "")
      {
         var int_fps = proposed_fps.substring(0, 2);
         var decimal_fps = proposed_fps.substring(2, proposed_fps.length);
         fps_ = "FPS" + "_" + int_fps + decimal_fps
         fps = (int_fps + "." + decimal_fps) * 1;
      }
      var mid = json_obj["meta"]["movieId"]
      var pid = json_obj["meta"]["packageId"]
      var which_url = prompt(
         "DOUBLE EXPORT SRT reports…\n\nPress Enter to try the template. Enter anything to go for CC."
         )
      if(which_url == "")
      {
         var template_url =
            "https://originator.backlot.netflix.com/api/request/timedText/" +
            our_clq + '/' + pid + '/' + mid + '/en/TEMPLATE/PRIMARY/' +
            fps_ + '?source=ORIGINATOR'
      }
      else
      {
         var template_url =
            "https://originator.backlot.netflix.com/api/request/timedText/" +
            our_clq + '/' + pid + '/' + mid + '/en/CC/PRIMARY/' + fps_ +
            '?source=ARCHIVE'
      }
      var targetFilename = srtName(fps_ + "_TRANSLATION")
      var sourceFilename = srtName(fps_ + "_SOURCE")
      var backupFilename = srtName(fps_ + "_BILINGUAL_TABLE").replace(".srt",
         ".html")
      var frms = 1000 / fps
      async function getSourceColumnEvents()
      {
         var result = await (await fetch(template_url)).json();
         return result;
      }
      async function delayedDownload()
      {
         var result = await getSourceColumnEvents();
         download(sourceFilename, array2srt(result))
         //  download(backupFilename, arrays2html(result, src),"html" )
      }
      delayedDownload()
      download(targetFilename, array2srt(src))

      function srtName(suffix = "")
      {
         var s = document.getElementsByClassName("cpe-page-menu-label")[0]
            .innerText
         var srtName = (s.replace(/[ ]/g, '_').replace(/[^a-z0-9_]/gi, '') +
               suffix + ".srt"
               ) //This gets rid of all punctuation, spaces and non-English letters
            .trim() //resulting in a name like 14545_El_Burrito_A_Breaking_Fat_Movie_FPS_2500.srt
         if(!srtName) srtName = our_clq + "_" + suffix +
            ".srt" //Fallback measure. Useful for debugging later
         return srtName
      }

      function frames2timecode(frames)
      { //frames to 00:01:02,000 format
         var milliseconds = Math.round(frames * frms)
         var srt_timecode = TimeConversion(milliseconds)
         return srt_timecode
      }

      function merge_same(array)
      {
         var merged = []
         var skip_next = false
         for(i = 0; i < array.length - 1; i++)
         {
            if(!skip_next)
            {
               thisEvent = array[i]
               nextEvent = array[i + 1]
               if(thisEvent[5] == nextEvent[5])
               {
                  skip_next = true
                  thisEvent[2] = thisEvent[2] + "\n" + nextEvent[2]
                  thisEvent[1] = nextEvent[1]
               }
               merged.push(thisEvent)
            }
            else
            {
               skip_next = false
            }
         }
         return merged
      }

      function arrays2html(events_object, events_object2)
      {
         var ordered_events = []
         var col = "SRC"
         for(var id in events_object)
         {
            events_object[id]["column"] = col
            try
            {
               var type_fn = events_object[id]["type"]
               if(type_fn === "fn")
               {
                  events_object[id]["txt"] += "<b></b>";
                  type_fn = undefined;
                  delete(type_fn)
               }
            }
            catch (e)
            {}
            ordered_events.push([
               events_object[id]["start"], events_object[id]["end"],
               events_object[id]
               ["txt"],
               events_object[id]
               ["styles"],
               events_object[id]
               ["rgn"],
               events_object[id]
               ["column"],
            ])
         }
         var col = "TRG"
         for(var id in events_object2)
         {
            events_object2[id]["column"] = col
            ordered_events.push([
               events_object2[id]["start"], events_object2[id]["end"],
               events_object2[id]
               ["txt"],
               events_object2[id]
               ["styles"],
               events_object2[id]
               ["rgn"],
               events_object2[id]
               ["column"],
            ])
         }
         ordered_events.sort(function (a, b)
         {
            return a[0] - b[0];
         }); //Array sorted by in_cues, sequentially
         ordered_events = merge_same(ordered_events)
         //    ordered_events.sort(function (a
         //        , b) {
         //        return a[1] - b[1];
         //    }); //Array sorted by out_cues this time, because merge_same changes some, sequentially
         var index = 0
         var srt_txt = bilingualHtml;
         var eol = ""
         var source_content = ''
         for(event of ordered_events)
         {
            var start = frames2timecode(event[0])
            var end = frames2timecode(event[1])
            var startend = start + "\n" + end
            try
            {
               if(typeof event[3][0]["type"] !== "undefined")
               {
                  if(event[3][0]["type"] == "italic")
                  {
                     content = italicize(content, event[3])
                  }
               }
            }
            catch (e)
            {}
            if(event[5] == "SRC")
            {
               source_content += event[2]
            }
            else
            {
               tr = '<tr><td><pre>' + startend + '</pre></td><td><pre>' +
                  source_content + '</pre></td><td><pre>' + event[2] +
                  '</pre></td></tr>' + "\n";
               srt_txt += tr;
               tr = "";
               source_content = ""
            }
         }
         var titleinfo = document.getElementsByClassName(
               "cpe-page-menu-label")[0].innerText.replace(/ "/, " “")
            .replace(/"/, "”")
         var titlelink = document.location.href
         var titledate = new Date().toISOString().slice(0, 10)
         srt_txt = srt_txt.replace(/TITLEINFO/g, titleinfo).replace(
            /TITLELINK/, titlelink).replace(/TITLEDATE/g, titledate)
         return srt_txt
      }

      function array2srt(events_object)
      {
         var ordered_events = []
         for(var id in events_object)
         {
            try
            {
               var type_fn = events_object[id]["type"]
               if(type_fn === "fn")
               {
                  events_object[id]["txt"] += "<b></b>";
                  type_fn = undefined;
                  delete(type_fn)
               }
            }
            catch (e)
            {}
            ordered_events.push([
               events_object[id]["start"], events_object[id]["end"],
               events_object[id]
               ["txt"],
               events_object[id]
               ["styles"],
               events_object[id]
               ["rgn"]
            ])
         }
         ordered_events.sort(function (a, b)
         {
            return a[0] - b[0];
         }); //Array sorted by in_cues, sequentially
         var index = 0
         var srt_txt = ''
         for(event of ordered_events)
         {
            index++
            var start = frames2timecode(event[0])
            var end = frames2timecode(event[1])
            var content = event[2]
            try
            {
               if(typeof event[3][0]["type"] !== "undefined")
               {
                  if(event[3][0]["type"] == "italic")
                  {
                     content = italicize(content, event[3])
                  }
               }
            }
            catch (e)
            {}
            try
            {
               if(typeof event[4] !== "undefined")
               {
                  if(event[4] == "top")
                  {
                     content = "{\\an8}" + content
                  }
               }
            }
            catch (e)
            {}
            try
            {
               if(event["type"] == "fn")
               {
                  content += '<b></b>'
               }
            }
            catch (e)
            {}
            console.log(content)
            var current_event = index + "\n" + start + " --> " + end + "\n" +
               content + "\n"
            srt_txt += current_event + "\n"
         }
         return srt_txt
      }

      function italicize(content, italics_array)
      {
         position_offset = 0
         for(var italic of italics_array)
         {
            var position_from = italic["from"] + position_offset;
            position_offset += 3
            content = [content.slice(0, position_from), "<i>", content.slice(
               position_from)].join('')
            var position_to = italic["to"] + position_offset;
            position_offset += 4
            content = [content.slice(0, position_to), "</i>", content.slice(
               position_to)].join('')
         }
         return content
      }

      function download(filename, text, type = "plain")
      {
         var element = document.createElement('a');
         element.setAttribute('href', 'data:text/' + type +
            ';charset=utf-8,%EF%BB%BF' + encodeURIComponent(text));
         element.setAttribute('download', filename);
         element.style.display = 'none';
         document.body.appendChild(element);
         element.click();
         document.body.removeChild(element);
      }

      function TimeConversion(duration)
      {
         let time = parseDuration(duration)
         return formatTimeHMSS(time)
      }

      function parseDuration(duration)
      {
         let remain = duration
         let hours = Math.floor(remain / (1000 * 60 * 60))
         remain = remain % (1000 * 60 * 60)
         let minutes = Math.floor(remain / (1000 * 60))
         remain = remain % (1000 * 60)
         let seconds = Math.floor(remain / (1000))
         remain = remain % (1000)
         let milliseconds = remain
         return {
            hours,
            minutes,
            seconds,
            milliseconds
         }
      }

      function formatTimeHMSS(o)
      {
         let hours = o.hours.toString()
         if(hours.length === 1) hours = '0' + hours
         let minutes = o.minutes.toString()
         if(minutes.length === 1) minutes = '0' + minutes
         let seconds = o.seconds.toString()
         if(seconds.length === 1) seconds = '0' + seconds
         let milliseconds = o.milliseconds.toString()
         if(milliseconds.length === 1) milliseconds = '00' + milliseconds
         if(milliseconds.length === 2) milliseconds = '0' + milliseconds
         return hours + ":" + minutes + ":" +
            //Example: 00:01:02,999 -- note that the SRT spec calls for a comma, not a period!
            seconds + "," + milliseconds
      }
   })();
}

function marunettuBilingualExportHTML()
{
   (function ()
   {
      var our_clq = document.location.href.toString().split("=")[1].split(
         ":")[2]
      var clq_pattern = new RegExp(
         '^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$',
         'i');
      if(!our_clq || !clq_pattern.test(our_clq))
      {
         alert("You must be in a started, saved Originator task!");
         throw new Error
      }
      if(!clq_pattern.test(our_clq))
      {
         alert("The CLQ is invalid: " + our_clq + "\n" +
            "Please report me to the developer.");
         throw new Error
      }
      var bilingualHtml = `<head>
    <script>
        var prevScrollpos=window.pageYOffset;
window.onscroll=function() {
    var currentScrollPos=window.pageYOffset;
    if (prevScrollpos > currentScrollPos) {
        document.getElementById("navbar").style.top="0";
    }
    else {
        document.getElementById("navbar").style.top="-50px";
    }
    prevScrollpos=currentScrollPos;
}

</script>
<style>#navbar {
    background-color: #333;
    top: 0;
    position: fixed;
    width: 80%;
    left: 10%;
    transition: top 0.5s;
    margin-left: auto;
    margin-right: auto;
    opacity: 0.7;
}

#navbar a {
    float: left;
    display: block;
    color: white;
    text-align: center;
    padding: 5px;
    text-decoration: none;
}

#navbar a:hover {
    background-color: #ddd;
    color: black;
}

body {
    background: #212121;
    color: grey;
}

table {
    width: 80%;
    table-layout: fixed;
    text-align: center;
    margin-left: auto;
    margin-right: auto;
    margin-top: 60px;
        border-collapse: collapse;

}

thead,
tbody,
td,
tr
 {
    display: block;
    font-family: monospace; 

}

tr {

background-image: linear-gradient(to right, darkgrey 30%, rgba(255,255,255,0) 0%);
background-position: bottom;
background-size: 3px 1px;
background-repeat: repeat-x;
}

tr:after {
    content: ' ';
    display: block;
    visibility: hidden;
    clear: both;
        border-bottom: 1px solid #ccc;

}

  #titleinfo {
            color: #fffff0;
            font-weight: bolder;
            font-style: normal;
            font-family: Roboto;
            font-size: 150%;
            margin-left: 55px;
            margin-top: 8px;
            flex-shrink: 1;
            width: 800px;
            white-space: nowrap;
            overflow: hidden;
            text-overflow: ellipsis;
            display: inline-block;
            }


thead th {
    height: 30px;
    /*text-align: left;*/
}


thead {
}

tbody td,
thead th {
    width: 33%;
    float: left;
}

tr:hover {
    color: darkgrey;
}


    </style>
<head><body>
         <div id="navbar">
              <a href="TITLELINK"><img id="logonetflix" width="auto" height="32" src="https://upload.wikimedia.org/wikipedia/commons/0/08/Netflix_2015_logo.svg" title="Backup made on TITLEDATE"></a>
              <span id="titleinfo" title="TITLEINFO">TITLEINFO (TITLEDATE)</span></div>
<table>
    <thead>
    <tr>
        <th><b><u>In-cue/Out-cue</b></u></th>
        <th><b><u>Source</b></u></th>
        <th><b><u>Translation</b></u></th>
    </tr>
    </thead>
    <tbody>
    `
      var lsJson = localStorage["clq:origination:" + our_clq]
      if(!lsJson)
      {
         alert("Timed text events not found in localStorage\nfor CLQ: " +
            our_clq + "\n" + "If the CLQ is correct and" + "\n" +
            "\"Save to local storage\" is enabled in Settings," + "\n" +
            "save the task and try again.");
         throw new Error
      }
      var json_obj = JSON.parse(lsJson)
      var src = json_obj["events"]
      var fps_ = json_obj["meta"]["fps"]
      var fps = fps_.split("_")[1] / 100
      //From {"fps":"FPS_2500"}
      var proposed_fps = prompt("Press Enter to accept framerate of " + fps +
         " or enter new as 2400 or 2997:");
      if(proposed_fps !== "")
      {
         var int_fps = proposed_fps.substring(0, 2);
         var decimal_fps = proposed_fps.substring(2, proposed_fps.length);
         fps_ = "FPS" + "_" + int_fps + decimal_fps
         fps = (int_fps + "." + decimal_fps) * 1;
      }
      var mid = json_obj["meta"]["movieId"]
      var pid = json_obj["meta"]["packageId"]
      var which_url = prompt(
         "Press Enter to try the template. Enter anything to go for CC.")
      if(which_url == "")
      {
         var template_url =
            "https://originator.backlot.netflix.com/api/request/timedText/" +
            our_clq + '/' + pid + '/' + mid + '/en/TEMPLATE/PRIMARY/' +
            fps_ + '?source=ORIGINATOR'
      }
      else
      {
         var template_url =
            "https://originator.backlot.netflix.com/api/request/timedText/" +
            our_clq + '/' + pid + '/' + mid + '/en/CC/PRIMARY/' + fps_ +
            '?source=ARCHIVE'
      }
      var targetFilename = srtName(fps_ + "_TRANSLATION")
      var sourceFilename = srtName(fps_ + "_SOURCE")
      var backupFilename = srtName(fps_ + "_BILINGUAL_TABLE").replace(".srt",
         ".html")
      var frms = 1000 / fps
      async function getSourceColumnEvents()
      {
         var result = await (await fetch(template_url)).json();
         return result;
      }
      async function delayedDownload()
      {
         var result = await getSourceColumnEvents();
         //download(sourceFilename, array2srt(result))
         download(backupFilename, arrays2html(result, src), "html")
      }
      delayedDownload()
      //download(targetFilename,array2srt(src))
      function srtName(suffix = "")
      {
         var s = document.getElementsByClassName("cpe-page-menu-label")[0]
            .innerText
         var srtName = (s.replace(/[ ]/g, '_').replace(/[^a-z0-9_]/gi, '') +
               suffix + ".srt"
               ) //This gets rid of all punctuation, spaces and non-English letters
            .trim() //resulting in a name like 14545_El_Burrito_A_Breaking_Fat_Movie_FPS_2500.srt
         if(!srtName) srtName = our_clq + "_" + suffix +
            ".srt" //Fallback measure. Useful for debugging later
         return srtName
      }

      function frames2timecode(frames)
      { //frames to 00:01:02,000 format
         var milliseconds = Math.round(frames * frms)
         var srt_timecode = TimeConversion(milliseconds)
         return srt_timecode
      }

      function merge_same(array)
      {
         var merged = []
         var skip_next = false
         for(i = 0; i < array.length - 1; i++)
         {
            if(!skip_next)
            {
               thisEvent = array[i]
               nextEvent = array[i + 1]
               if(thisEvent[5] == nextEvent[5])
               {
                  skip_next = true
                  thisEvent[2] = thisEvent[2] + "\n" + nextEvent[2]
                  thisEvent[1] = nextEvent[1]
               }
               merged.push(thisEvent)
            }
            else
            {
               skip_next = false
            }
         }
         return merged
      }

      function arrays2html(events_object, events_object2)
      {
         var ordered_events = []
         var col = "SRC"
         for(var id in events_object)
         {
            events_object[id]["column"] = col
            try
            {
               var type_fn = events_object[id]["type"]
               if(type_fn === "fn")
               {
                  events_object[id]["txt"] += "<b></b>";
                  type_fn = undefined;
                  delete(type_fn)
               }
            }
            catch (e)
            {}
            ordered_events.push([
               events_object[id]["start"], events_object[id]["end"],
               events_object[id]
               ["txt"],
               events_object[id]
               ["styles"],
               events_object[id]
               ["rgn"],
               events_object[id]
               ["column"],
            ])
         }
         var col = "TRG"
         for(var id in events_object2)
         {
            events_object2[id]["column"] = col
            ordered_events.push([
               events_object2[id]["start"], events_object2[id]["end"],
               events_object2[id]
               ["txt"],
               events_object2[id]
               ["styles"],
               events_object2[id]
               ["rgn"],
               events_object2[id]
               ["column"],
            ])
         }
         ordered_events.sort(function (a, b)
         {
            return a[0] - b[0];
         }); //Array sorted by in_cues, sequentially
         ordered_events = merge_same(ordered_events)
         //    ordered_events.sort(function (a
         //        , b) {
         //        return a[1] - b[1];
         //    }); //Array sorted by out_cues this time, because merge_same changes some, sequentially
         var index = 0
         var srt_txt = bilingualHtml;
         var eol = ""
         var source_content = ''
         for(event of ordered_events)
         {
            var start = frames2timecode(event[0])
            var end = frames2timecode(event[1])
            var startend = start + "\n" + end
            try
            {
               if(typeof event[3][0]["type"] !== "undefined")
               {
                  if(event[3][0]["type"] == "italic")
                  {
                     content = italicize(content, event[3])
                  }
               }
            }
            catch (e)
            {}
            if(event[5] == "SRC")
            {
               source_content += event[2]
            }
            else
            {
               tr = '<tr><td><pre>' + startend + '</pre></td><td><pre>' +
                  source_content + '</pre></td><td><pre>' + event[2] +
                  '</pre></td></tr>' + "\n";
               srt_txt += tr;
               tr = "";
               source_content = ""
            }
         }
         var titleinfo = document.getElementsByClassName(
               "cpe-page-menu-label")[0].innerText.replace(/ "/, " “")
            .replace(/"/, "”")
         var titlelink = document.location.href
         var titledate = new Date().toISOString().slice(0, 10)
         srt_txt = srt_txt.replace(/TITLEINFO/g, titleinfo).replace(
            /TITLELINK/, titlelink).replace(/TITLEDATE/g, titledate)
         return srt_txt
      }

      function array2srt(events_object)
      {
         var ordered_events = []
         for(var id in events_object)
         {
            try
            {
               var type_fn = events_object[id]["type"]
               if(type_fn === "fn")
               {
                  events_object[id]["txt"] += "<b></b>";
                  type_fn = undefined;
                  delete(type_fn)
               }
            }
            catch (e)
            {}
            ordered_events.push([
               events_object[id]["start"], events_object[id]["end"],
               events_object[id]
               ["txt"],
               events_object[id]
               ["styles"],
               events_object[id]
               ["rgn"]
            ])
         }
         ordered_events.sort(function (a, b)
         {
            return a[0] - b[0];
         }); //Array sorted by in_cues, sequentially
         var index = 0
         var srt_txt = ''
         for(event of ordered_events)
         {
            index++
            var start = frames2timecode(event[0])
            var end = frames2timecode(event[1])
            var content = event[2]
            try
            {
               if(typeof event[3][0]["type"] !== "undefined")
               {
                  if(event[3][0]["type"] == "italic")
                  {
                     content = italicize(content, event[3])
                  }
               }
            }
            catch (e)
            {}
            try
            {
               if(typeof event[4] !== "undefined")
               {
                  if(event[4] == "top")
                  {
                     content = "{\\an8}" + content
                  }
               }
            }
            catch (e)
            {}
            try
            {
               if(event["type"] == "fn")
               {
                  content += '<b></b>'
               }
            }
            catch (e)
            {}
            console.log(content)
            var current_event = index + "\n" + start + " --> " + end + "\n" +
               content + "\n"
            srt_txt += current_event + "\n"
         }
         return srt_txt
      }

      function italicize(content, italics_array)
      {
         position_offset = 0
         for(var italic of italics_array)
         {
            var position_from = italic["from"] + position_offset;
            position_offset += 3
            content = [content.slice(0, position_from), "<i>", content.slice(
               position_from)].join('')
            var position_to = italic["to"] + position_offset;
            position_offset += 4
            content = [content.slice(0, position_to), "</i>", content.slice(
               position_to)].join('')
         }
         return content
      }

      function download(filename, text, type = "plain")
      {
         var element = document.createElement('a');
         element.setAttribute('href', 'data:text/' + type +
            ';charset=utf-8,%EF%BB%BF' + encodeURIComponent(text));
         element.setAttribute('download', filename);
         element.style.display = 'none';
         document.body.appendChild(element);
         element.click();
         document.body.removeChild(element);
      }

      function TimeConversion(duration)
      {
         let time = parseDuration(duration)
         return formatTimeHMSS(time)
      }

      function parseDuration(duration)
      {
         let remain = duration
         let hours = Math.floor(remain / (1000 * 60 * 60))
         remain = remain % (1000 * 60 * 60)
         let minutes = Math.floor(remain / (1000 * 60))
         remain = remain % (1000 * 60)
         let seconds = Math.floor(remain / (1000))
         remain = remain % (1000)
         let milliseconds = remain
         return {
            hours,
            minutes,
            seconds,
            milliseconds
         }
      }

      function formatTimeHMSS(o)
      {
         let hours = o.hours.toString()
         if(hours.length === 1) hours = '0' + hours
         let minutes = o.minutes.toString()
         if(minutes.length === 1) minutes = '0' + minutes
         let seconds = o.seconds.toString()
         if(seconds.length === 1) seconds = '0' + seconds
         let milliseconds = o.milliseconds.toString()
         if(milliseconds.length === 1) milliseconds = '00' + milliseconds
         if(milliseconds.length === 2) milliseconds = '0' + milliseconds
         return hours + ":" + minutes + ":" +
            //Example: 00:01:02,999 -- note that the SRT spec calls for a comma, not a period!
            seconds + "," + milliseconds
      }
   })();
}

function marunettuImportBetterSrt()
{
   alert(
      "DISABLED FOR NOW.\nPossible fixes: Send everybody to Github?\nStore and open in any report?\nCompletely replace current window? hmm.");
   (function () {})()
}

function marunettuAuxFade(type, ms, el, remove = false)
{
   var isIn = type === 'in',
      opacity = isIn ? 0 : 1,
      interval = 50,
      duration = ms,
      gap = interval / duration
   if(isIn)
   {
      el.style.display = 'inline'
      el.style.opacity = opacity
   }

   function func()
   {
      opacity = isIn ? opacity + gap : opacity - gap
      el.style.opacity = opacity
      if(opacity <= 0)
      {
         remove ? el.remove() : el.style.display = 'none'
      }
      if(opacity <= 0 || opacity >= 1) window.clearInterval(fading)
   }
   var fading = window.setInterval(func, interval)
}

function marunettuCleanup()
{
   var menu = document.querySelector("div.popup")
   var itemButtons = document.getElementsByClassName("item-button")
   var dropdown = itemButtons[itemButtons.length -
      1] //this should always yield the More Actions dropdown trigger
   //if(!menu) {dropdown.click();} 
   for(byeButton of document.getElementsByClassName("mnfkbtn"))
   {
      marunettuAuxFade('out', 100, byeButton, true)
   }
   dropdown.removeEventListener("click", marunettuClickDropdown,
   {
      passive: false,
      capture: true
   })
}

function marunettuAddButtonsToMenu(btnid, disabled = "")
{
   var gonnaBeLast = false
   if(typeof btnid == 'undefined')
   {
      gonnaBeLast = true
      for(btnid in marunettuButtons)
      {
         marunettuAddButtonsToMenu(btnid)
      }
   }
   if(!gonnaBeLast && !document.getElementById(btnid))
   {
      var btntext = marunettuButtons[btnid]
      var menu = document.querySelector("div.popup")
      var button = document.createElement("button");
      button.className = "icon-button mnfkbtn" + disabled
      button.id = btnid
      button.setAttribute("title", btntext)
      button.innerHTML = '<i class="bhicons"></i>'
      var span = document.createElement("span")
      span.className = "label"
      if(btnid == "marunettuRunAutoQC")
      {
         btntext = btntext + " for " + window.marunettuLang
      }
      span.innerText = btntext
      button.onclick = function (event)
      {
         if(btnid == "marunettuCleanup")
         {
            event.stopImmediatePropagation();
            marunettuCleanup();
         }
         else
         {
            var funktion = btnid + '();'
            eval(btnid + "" +
               "();") //Hmm, I'm gonna try storing the principal JS code of the bookmarklets as text strings properties of one big object
         }
      }
      button.append(span)
      menu.append(button)
   }
}

function marunettuGetLang()
{
   if(!window.marunettuLang)
   {
      var taskTitle = document.getElementsByClassName("cpe-page-menu-label")[0]
         .innerText
      taskTitle = taskTitle.split(",")
      var lang = taskTitle[taskTitle.length - 2].trim().toUpperCase()
      window.marunettuLang = lang
   }
   return window.marunettuLang
}