[Userscript] KaniWani audio


Right you are: merged PR. @henshin5 try updating the script and see if it works for you now.


Unfortunately this script seems to no longer work. :confused:


Yeah, this stopped working like right after I started using it in the past two weeks or so. I think I heard it working once and now it no longer works.

Anyone know why maybe?


Depends on how it’s trying to pull audio from Wanikani.
Once API V2 is stable and WK have released their audio publicly (technically this script is using WK’s resources without permission) we’ll be adding it to Kaniwani anyway.


FYI this definitely won’t work at the moment on KW 2.0 since all the markup/html content has changed.


Aw man, I just added it to the API and Third Party Apps list yesterday


Sorry for not replying sooner, I’ve been travelling. I’ll have a look when I get a chance (and when I’ve finished my reviews :-O).


Hi, thanks for creating this script, I was really glad it was available. :slight_smile:
I tried getting it to work again but I think I’m stuck at a point where you would have to look into it. It generates a URL that seems to be valid, such as https://wanikaniaudio.herokuapp.com/url/王 and returns an AWS URL but I get an AccessDenied exception for that AWS content.

Changing the script wasn’t much work but since you are busy too I thought maybe it helps to share my changes. Not sure if that was all that had to be changed since I didn’t look any further after being unable to load the mp3 file.

They also switched to https (or maybe it has always been that way) which causes problems at least for the loading image.

// ==UserScript==
// @name         KaniWani audio
// @namespace    http://tampermonkey.net/
// @version      0.22 alpha
// @description  Play audio in KaniWani
// @author       CometZero
// @match        https://kaniwani.com/reviews/session
// @match        https://www.kaniwani.com/reviews/session
// @grant        GM_xmlhttpRequest
// ==/UserScript==


var buttonHtml =
`<a id="playAudio" class="button -addsynonym" href="#">Play Audio</a>`;
var colorDisabled = "hsl(0, 0%, 65%)";

var loadingImageHtml = `<img src="http://img.etimg.com/photo/45627788.cms"
alt="Loading ..." style="margin-left:5px;width:15px;height:15px;display:none;">`;

var playSoundButton;
var loadingImage;

var audio = null;
var isPendingPlay = false;
var isLoadingAudio = false;

var onAudioReady = function(){
    isLoading = false;
    loadingImage.style.display = "none"; // hide loading image
    playSoundButton.style.color = ""; // set default text color

        isPendingPlay = false;

var onAudioLoading = function(){
    loadingImage.style.display = "inline"; // show loading image
    playSoundButton.style.color = colorDisabled; // dimm play button
    isLoading = true;

(function() {
    'use strict';

    // TODO test if my service is still working (wanikaniaudio.herokuapp.com)
    // and notify the user to motivate me to enable the service



// wait until the first word has been loaded so that we can complete the setup
function initWhenReady(){
    console.log("Trying to initialize KaniWani audio... ");
    var wordDom = getWordDom();
        //wait a bit and then try again, assuming the initial loading will be complete soon
        setTimeout(function(){ initWhenReady(); }, 500);

function init(){

    // loads audio for the first time;

    onNewWordObserver(function(mutations, observer) {
        // loads audio everytime the word DOM changes

    playSoundButton.onclick = function(){

    document.getElementById('submitAnswer').onclick = function(){

// plays the audio
// finds the word than loads the audo if needed and plays it
function playAudio(){
    var word = getWord();

    if(word === null) {
        console.log("Cannot get word :(");

    // if audio is available just play it
    // audio is not available we need to load it

    // make sure audio is not already loading

    // set pendingPlay true so when it loads it will play the audio
    isPendingPlay = true;

// accepts function that is triggered when new word is shown
function onNewWordObserver(f){
    MutationObserver = window.MutationObserver || window.WebKitMutationObserver;

    var observer = new MutationObserver(f);

    // configuration of the observer:
    var config = { attributes: true, childList: true, characterData: true };

    // select the target node
    var target = getWordDom();

    // pass in the target node, as well as the observer options
    observer.observe(target, config);

// accepts function that is triggered when user has answered correctly
function onCorrectAnswerObserver(f){
    MutationObserver = window.MutationObserver || window.WebKitMutationObserver;

    var observer = new MutationObserver(f);

    // configuration of the observer:
    var config = { attributes: true, childList: true, characterData: true };

    // TODO find target and change the config
    // select the target node
    var target = null;
    // pass in the target node, as well as the observer options
    observer.observe(target, config);

// adds all the buttons and loading images to the webpage
function initElements(){
    // create "play audio button"
    playSoundButton = htmlToElement(buttonHtml);
    loadingImage = htmlToElement(loadingImageHtml);
    //buttonWraper.innerHTML = buttonHtml;

    // insert
    var answerPanel = document.getElementById('answer');
    answerPanel.parentNode.insertBefore(playSoundButton, answerPanel.nextSibling);    

// get the dom that is containg word that it has to play
function getWordDom(){
    //var detailKanjiDiv = document.getElementById("detailKanji");
    //var kanjisDom = detailKanjiDiv.getElementsByClassName("text");
    var kanjisDom = document.querySelectorAll('div[lang=ja] div');
    if(kanjisDom && kanjisDom.length >= 1){
        return kanjisDom[0];
    return null;

// finds the word that it has to play
function getWord(){
    var kanjisDom = getWordDom();

    if(kanjisDom != null){
        // get just the first kanji
        var kanjis =  kanjisDom.innerHTML;
        var splitKanjis = kanjis.split("<br>");
        return splitKanjis[0];;
    } else {
        return null;

// get audio url for a word and play it
function loadAudio(){
    vocubKanji = getWord();
    if(isEmpty(vocubKanji)) throw "vocubKanji cannot be empty!";

    audio = null;

    GM_xmlhttpRequest ( {
        method: 'GET',
        url:    'https://wanikaniaudio.herokuapp.com/url/' + vocubKanji,
        accept: 'text/xml',
        onreadystatechange: function (response) {

            if (response.readyState != 4)

            // get responseTxt
            var responseTxt = response.responseText;

            // check if responseTxt is valid
            if (!isEmpty(responseTxt) && !responseTxt.startsWith("Cannot")){
                audio = new Audio(responseTxt);
            } else {
                console.log("Invalid response " + responseTxt);
    } );

// check if string is empty
function isEmpty(str) {
    return (!str || 0 === str.length);

 * Creates dom element from string.
 * @param {String} HTML representing a single element
 * @return {Element}
function htmlToElement(html) {
    var template = document.createElement('template');
    template.innerHTML = html;
    return template.content.firstChild;


Following this thread for updates


Sorry, I’ve had several goes at trying to get the script to work after the changes to both KaniWani and WaniKani and I haven’t been able to figure anything out. The script will probably stay broken indefinitely, unless there’s yet more major changes.


Can you get the audio from jisho.org instead of wanikani?