forvo.com - Make download button actually download audios

Fix the download button to actually download the audio instead of opening the login screen

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください。
// ==UserScript==
// @name        forvo.com - Make download button actually download audios
// @namespace   secretx_scripts
// @match       *://forvo.com/word/*
// @match       *://*.forvo.com/word/*
// @version     2023.12.16
// @author      SecretX
// @description Fix the download button to actually download the audio instead of opening the login screen
// @grant       GM.xmlHttpRequest
// @run-at      document-start
// @icon        https://i.imgur.com/3hA6TF1.png
// @license     GNU LGPLv3
// ==/UserScript==

const forvoServerUrl = "https://audio12.forvo.com";

function doRequest(httpMethod, url) {
    return new Promise((resolve, reject) => {
        GM.xmlHttpRequest({
            method: httpMethod.toUpperCase(),
            url: url,
            onload: resolve,
            onerror: reject,
            responseType: "blob",
            timeout: 6000,
        });
    });
}

function extractUrl(element) {
    const play = element.getAttribute("onclick");
    // We are interested in Forvo's javascript Play function which takes in some parameters to play the audio
    // Example: Play(3060224,'OTQyN...','OTQyN..',false,'Yy9wL2NwXzk0MjYzOTZfNzZfMzM1NDkxNS5tcDM=','Yy9wL...','h')
    // Match anything that isn't commas, parentheses or quotes to capture the function arguments
    // Regex will match something like ["Play", "3060224", ...]
    const playArgs = play.match(/([^',\(\)]+)/g);

    // Forvo has two locations for mp3, /audios/mp3 and just /mp3
    // /audios/mp3 is normalized and has the filename in the 5th argument of Play base64 encoded
    // /mp3 is raw and has the filename in the 2nd argument of Play encoded
    try {
        const file = atob(playArgs[5]);  // Something like this: v/p/vp_9478059_76_3731369.mp3
        return `${forvoServerUrl}/audios/mp3/${file}`;
    } catch (e) {
        // Some pronunciations don't have a normalized version so fallback to raw
        const file = atob(playArgs[2]);  // Something like this: 9478059/76/9478059_76_3731369.mp3
        return `${forvoServerUrl}/mp3/${file}`;
    }
}

function getPlayButtonFromDownloadButton(downloadButton) {
    let currentElement = downloadButton;
    for (let i = 0; i < 5; i++) {
        currentElement = currentElement.parentElement;
    }
    console.info("Current element", currentElement);
    return currentElement.querySelector(".play");
}

function makeCopyOfDownloadButton(downloadButton) {
    const innerSpan = downloadButton.querySelector("* > span");
    const text = innerSpan.innerText;

    // Create the new download button, following the structure of the old one
    const newDownloadButton = document.createElement(downloadButton.tagName.toLowerCase());
    newDownloadButton.className = downloadButton.className;
    const newInnerSpan = document.createElement(innerSpan.tagName.toLowerCase());
    newInnerSpan.className = innerSpan.className;
    newInnerSpan.innerText = text;
    newDownloadButton.appendChild(newInnerSpan);

    return newDownloadButton;
}

window.addEventListener("DOMContentLoaded", function () {
    const downloadButtons = Array.from(document.querySelectorAll(".download") ?? []);
    if (downloadButtons.length === 0) {
        console.debug("No download buttons found on this page, so not fixing anything...");
        return;
    }

    for (const downloadButton of downloadButtons) {
        const newDownloadButton = makeCopyOfDownloadButton(downloadButton);

        const playButton = getPlayButtonFromDownloadButton(downloadButton);
        console.info("Play button", playButton);
        const url = extractUrl(playButton);
        const fileName = url.substring(url.lastIndexOf("/") + 1);

        // Add the download functionality to the new download button
        newDownloadButton.addEventListener("click", () => {
            console.info(`Downloading mp3 from: ${url}`);

            doRequest("GET", url).then((response) => {
                if (response.status !== 200) {
                    console.error("Error on downloading mp3", response);
                    return
                }
                const blob = response.response;
                const url = URL.createObjectURL(blob);
                const a = document.createElement("a");
                a.style.display = "none";
                a.href = url;
                a.download = fileName;
                document.body.appendChild(a);
                a.click();
                window.URL.revokeObjectURL(url);

                console.info(`Downloaded '${fileName}'!`);
            }).catch((e) => {
                console.error("Error while downloading mp3!", e);
            });
        });

        // Replace the old download button with the new one
        downloadButton.replaceWith(newDownloadButton);
    }

    console.info(`Fixed ${downloadButtons.length} download buttons!`);
}, false);