Memrise Hello 日常 Audio

Automatically plays available audio from JapanesePod101 for Memrise's Hello 日常 course

// ==UserScript==
// @name        Memrise Hello 日常 Audio
// @author      looki
// @namespace   looki
// @description Automatically plays available audio from JapanesePod101 for Memrise's Hello 日常 course
// @include     http*://*memrise.com/course/287112/*/garden/*
// @include     http*://*memrise.com/course/528125/*/garden/*
// @version     1.0.0
// @grant       none
// ==/UserScript==

// Dynamically created - contains the lessons, reviews, etc.
var boxes = null;

// Used to govern lessons/reviews
var gardenObserver = null;

// Cache audio files for reuse
var audioMap = {};

// Main function - waits for the main DIV to be created and then observes lessons/reviews
var boxesFinder = new MutationObserver(function(mutations) {
    boxes = document.getElementById('boxes');
    if (boxes) {
        boxesFinder.disconnect();
        gardenObserver = new MutationObserver(function(mutations) {
            for (var i = 0; i < mutations.length; ++i) {
                // A correct answer was given (hence the sparkles)
                if (mutations[i].target.tagName == 'IMG'
                 && mutations[i].target.classList.contains('sparkles')
                 && mutations[i].attributeName == 'style'
                 && (mutations[i].oldValue == '' || mutations[i].oldValue == null)) {
                    // Note: For some reason, oldValue is null in Chrome and empty in FireFox...
                    playAudio(getReviewKanji(), getReviewKana());
                }

                // A lesson page was opened
                else
                if (mutations[i].target.tagName == 'DIV'
                 && mutations[i].target.classList.contains('garden-box')
                 && mutations[i].target.classList.contains('presentation')
                 && mutations[i].oldValue == 'garden-box presentation show-more choosing-mem') {
                    playAudio(getLessonKanji(), getLessonKana());
                }

                // The answer was correctly re-entered after a failed review
                else
                if (mutations[i].target.tagName == 'INPUT'
                 && mutations[i].attributeName == 'class'
                 && boxes.firstChild.classList.contains('copytyping')
                 && mutations[i].target.classList.contains('correct')
                 && mutations[i].target.value.trim() == getLessonKanji()
                 ){
                    // Copytyping page layout == lesson layout
                    playAudio(getLessonKanji(), getLessonKana());
                }
            }
        });
        gardenObserver.observe(boxes, {'attributes' : true, 'attributeOldValue' : true, 'childList' : true, 'subtree' : true});
    }
});

// Run the whole thing...
boxesFinder.observe(document.getElementById('central-area'), {'attributes' : true});

/**
 * Helpers
 */
function getLessonKanji() {
    var kanji = boxes.querySelector('.row[data-column-index="1"] > .row-value');
    return kanji ? kanji.innerHTML.trim() : '';
    return '';
}
function getLessonKana() {
    var kana = boxes.querySelector('.row[data-column-index="3"] > .row-value');
    return kana ? kana.innerHTML.trim() : '';
}
function getReviewKanji() {
    var correctNode = boxes.querySelector('.correct');
    if (!correctNode)
        return '';

    var kanji;
    // TODO: Check support for other memrise quiz types that were not encountered during development (?)
    if (correctNode.tagName == 'INPUT')
        kanji = correctNode.value;
    else {
        // Find out whether this is a JP->EN quiz or the other way around
        var question = boxes.querySelector('.qquestion').innerHTML;
        question = question.substring(0, question.indexOf('<div'));
        // JP->EN
        if (question.match(/[\u4E00-\u9FAF\u3040-\u3096\u30A1-\u30FA\uFF66-\uFF9D\u31F0-\u31FF]/))
            kanji = question;
        // EN->JP
        else
            kanji = correctNode.querySelector('.val').innerHTML;

    }
    return kanji.trim();
}
function getReviewKana() {
    var kana = boxes.querySelector('.extra-info');
    return kana ? kana.innerHTML.trim() : '';
}

/**
 * Search for, cache and play an audio file using the given spelling and reading
 */
function playAudio(kanji, kana) {
    if (kanji == '' || kana == '')
        return;
    var hash = kanji + '|' + kana;
    // Not cached yet - download and check if audio is actually available
    if (!audioMap[hash]) {
        audioMap[hash] = new Audio('http://assets.languagepod101.com/dictionary/japanese/audiomp3.php?kanji=' + kanji + '&kana=' + kana);
        audioMap[hash].playOnLoad = true;
        audioMap[hash].addEventListener('loadedmetadata', function() {
            // The 'Audio missing' clip is just over 5 seconds long
            if (audioMap[hash].duration < 5.0) {
                audioMap[hash].play();
                audioMap[hash].playOnLoad = false;
            }
            else {
                audioMap[hash] = new Audio();
            }
        });
    }
    // Already cached - however, don't try to play the sound if we're still downloading it (necessary for 'missing audio' check) 
    else if (!audioMap[hash].playOnLoad) {
        audioMap[hash].play();
    }
}