Greasy Fork is available in English.

Memrise Audio Uploader

Automatically generates and uploads Audio from Google TTS (for the first column)

// ==UserScript==
// @name           Memrise Audio Uploader
// @description    Automatically generates and uploads Audio from Google TTS (for the first column)
// @match          https://*.memrise.com/course/*/*/edit/*
// @match          https://*.memrise.com/garden/review/*
// @run-at         document-end
// @version        1.0.6
// @grant          none
// @namespace      https://greasyfork.org/users/213706
// ==/UserScript==

/* jshint esversion:6 */

function main() {

  // Google TTS API
  const GOOGLETTS_LANG = {
    "Afrikaans": "af",
    "Albanian": "sq",
    "Arabic": "ar",
    "Armenian": "hy",
    "Bengali": "bn",
    "Bosnian": "bs",
    "Catalan": "ca",
    "Chinese (Simplified)": "zh-CN",
    "Chinese (Traditional)": "zh-TW",
    "Croatian": "hr",
    "Czech": "cs",
    "Danish": "da",
    "Dutch": "nl",
    "English": "en",
    "Esperanto": "eo",
    "Finnish": "fi",
    "French": "fr",
    "German": "de",
    "Greek": "el",
    "Hindi": "hi",
    "Hungarian": "hu",
    "Icelandic": "is",
    "Indonesian": "id",
    "Italian": "it",
    "Japanese": "ja",
    "Kanji": "ja",
    "Khmer": "km",
    "Korean": "ko",
    "Latin": "la",
    "Latvian": "lv",
    "Macedonian": "mk",
    "Nepali": "ne",
    "Norwegian": "no",
    "Polish": "pl",
    "Portuguese (Brazil)": "pt-BR",
    "Portuguese (Portugal)": "pt-PT",
    "Romanian": "ro",
    "Russian": "ru",
    "Serbian": "sr",
    "Sinhalese": "si",
    "Slovak": "sk",
    "Spanish (Mexico)": "es",
    "Spanish (Spain)": "es",
    "Swahili": "sw",
    "Swedish": "sv",
    "Tamil": "ta",
    "Thai": "th",
    "Turkish": "tr",
    "Ukrainian": "uk",
    "Vietnamese": "vi",
    "Welsh": "cy"
  };
  
  var AudioUploader = {
    
    /**
     * Entrypoint
     */
    init: function() {

      // Add "generate audio" btn
      $(document).ajaxSend(function (e, xhr, settings) {
        var get_lvl = settings.url.match(/^\/ajax\/level\/editing_html\/\?level_id=(\d+)/);

        if(!get_lvl) {
          return;
        }
        xhr.always(function() {
          this.addBtn('l_' + get_lvl[1]);
        }.bind(this));
      }.bind(this));
    },

    /**
     * Append btn "Generate audio" to level options
     * @param string idLvl
     */
    addBtn: function(idLvl) {
      if(!document.getElementById(idLvl).getAttribute('data-pool-id')) {
        return;
      }

      var btn = document.createElement('button');
      btn.setAttribute('type', 'button');
      btn.setAttribute('class', 'generate-audio');
      btn.innerHTML = 'Generate audio';

      setTimeout(function(){
        var parent = document.getElementById(idLvl);
        parent = parent.querySelector('.level-options').firstElementChild;
        parent.appendChild(btn);

        btn.addEventListener('click', this.generateAudio.bind(this));
      }.bind(this), 0);
    },

    //+------------------------------------------------------
    //|
    //| UPLOAD AUDIO
    //|
    //+------------------------------------------------------

    /**
     * Generate audio for the current level
     */
    generateAudio: function(e) {
      var table   = e.target.parentNode.parentNode.nextElementSibling.firstElementChild,
          column1 = table.firstElementChild.querySelector('.column').innerText.trim();

      // Get lang label
      if(typeof GOOGLETTS_LANG[column1] == 'undefined') {
        alert(column1 + " isn't a recognized language");
        return;
      }
      var languageCode = GOOGLETTS_LANG[column1];

      // Get list of words without audio
      var things = table.querySelector('.things').children;

      for(let i=0; i<things.length; i++) {
        let thing   = things[i],
            word    = thing.querySelector('.column').innerText.trim(),
            $column = $('.audio', thing);

        // Already has an audio?
        let listAudio = $('.dropdown-toggle', $column).text().trim();
        if(/^[1-9]/.test(listAudio)) {
          continue;
        }

        // If not: generate the audio from Google TTS and upload it
        this.uploadWord({
          word,
          $column,
          thingId : thing.getAttribute('data-thing-id'),
          cellId  : $column.data('key'),
          url     : this.getGoogleTtsUrl(languageCode, word)
        });
      }
    },
    
    /**
     * Returns Google TTS url
     * for the given word and language
     *
     * @return string
     */
    getGoogleTtsUrl: function(languageCode, word) {
      const url = `https://translate.google.com/translate_tts?ie=UTF-8&tl=${languageCode || "en"}&client=tw-ob&q=${encodeURIComponent(word)}&tk=${Math.floor(Math.random() * 1000000)}&ttsspeed=1`;

      const proxy_url = 'https://cors-anywhere.99901dev.workers.dev/?q=' + encodeURIComponent(url);

      return proxy_url;
      //return `https://google-tts-api-v2.herokuapp.com/?q=${encodeURIComponent(word)}&tl=${languageCode}&download`;
      //return `https://translate.google.com/translate_tts?ie=UTF-8&tl=${languageCode}&client=tw-ob&q=${encodeURIComponent(word)}&tk=${Math.floor(Math.random() * 1000000)}`;
    },

    /**
     * Upload Google TTS to Memrise
     */
    uploadWord: function({url, word, thingId, cellId, $column}) {
      $('.files-add', $column).remove();

      let status = 200;

      fetch(url)
        .then(res => {
          status = res.status;

          if(res.status!=200) {
            return res.text();
          } else {
            return res.blob()
          }
        })
        .then(blob => {
          if(status!=200) {
            console.error(status, blob);
            return;
          }
          let file = new File([blob], word + '.mp3', {type: "audio/mpeg"});

          let fd = new FormData();
          fd.append('thing_id', thingId);
          fd.append('cell_id', cellId);
          fd.append('cell_type', 'column');
          fd.append('csrfmiddlewaretoken', MEMRISE.csrftoken);
          fd.append('f', file);

          $.ajax({
            url: '/ajax/thing/cell/upload_file/',
            data: fd,
            processData: false,
            contentType: false,
            type: 'POST',
            success: function(data){
              if(data.message) {
                alert(data.message);
              }
              $column.replaceWith(data.rendered);
            }
          }); // end ajax
        }); // end fetch
    } // end uploadWord
  };

  AudioUploader.init();
}


// Inject JS directly in page to prevent limitations of access
var script = document.createElement('script');

script.setAttribute("type", "application/javascript");
script.appendChild(document.createTextNode('('+ main +')();'));
document.body.appendChild(script);