Minyami

Get Minyami download commands for supported sites

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

Advertisement:

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

Advertisement:

// ==UserScript==
// @name Minyami
// @description Get Minyami download commands for supported sites
// @namespace Violentmonkey Scripts
// @include *
// @exclude https://*.paypal.com/*
// @grant GM_notification
// @grant GM_registerMenuCommand
// @grant GM_unregisterMenuCommand
// @grant GM_setClipboard
// @grant GM_setValue
// @grant GM_getValue
// @version 0.0.1.20260614164700
// ==/UserScript==

(async () => {
    let stream = {};
    let completed = false;

    const serverValue = GM_getValue("server", "");
    if (!(serverValue === "none" || /^https?:\/\//.test(serverValue))) {
        await GM_setValue(
            "server",
            prompt("Enter your download Server URL (to disable this feature input: none)", "")
        );
    }
    const server = GM_getValue("server", "");

    const resolutionValue = GM_getValue("resolution", "")
    if (!(resolutionValue === "none" || /^(240p|360p|480p|720p)$/.test(resolutionValue))) {
        await GM_setValue(
            "resolution",
            prompt("Enter a target resolution override (e.g. 720p) (to disable this feature input: none)", ""));
    }
    const resolution = GM_getValue("resolution", "");

    if (sessionStorage.getItem("streams") === null) {
        sessionStorage.setItem("streams", "[]");
    }

    function buildCommand(url, stream) {
        return `minyami -d "${url}" --output "${stream.title}.ts" --key "${stream.key}" --live --threads 50`;
    }

    async function createCommand(stream) {
        let url = stream.url;

        if (resolution !== "none") {
            const replacedUrl = stream.url.replace(/(240p|360p|480p|720p)/g, resolution);

            try {
                const response = await fetch(replacedUrl, { method: "HEAD" });
                if (response.ok) {
                    url = replacedUrl;
                } else {
                    console.log("override resolution not found, using original");
                }
            } catch (err) {
                console.log("resolution override check failed, using original", err);
            }
        }

        return buildCommand(url, stream);
    }

    async function sendToServer(stream) {
        if (server !== "none") {
            const command = await createCommand(stream);
            fetch(server, {
                method: "POST",
                body: command,
                headers: { "Content-type": "text/plain; charset=UTF-8" }
            }).catch(console.error);
        }
    }

    function getDate(offset) {
        const date = new Date();
        date.setHours(date.getHours() - offset);

        const year = date.getFullYear();
        const month = String(date.getMonth() + 1).padStart(2, "0");
        const day = String(date.getDate()).padStart(2, "0");

        return `${year}-${month}-${day}`;
    }

    async function registerDownloadAllMenu() {
        const streams = JSON.parse(sessionStorage.getItem("streams"));

        if (streams.length > 1) {
            const commands = (await Promise.all(streams.map((stream) => createCommand(stream)))).join("\n");

            GM_registerMenuCommand(
                "📋🔄️ Copy and clear all Minyami commands",
                async () => {
                    await GM_setClipboard(commands);
                    GM_notification("All Minyami commands copied!", "Minyami");
                    sessionStorage.setItem("streams", "[]");
                },
                {
                    title: `${streams.length} Minyami commands`
                }
            );
        }
    }

    await registerDownloadAllMenu();

    const notify = async function(object) {
        if (completed === false) {
            switch (object.type) {
                case "chunklist": {
                    const timestamp = document.querySelectorAll(
                        "div#root div#app-layout div.VideoDescription-infoArea span.MuiTypography-root"
                    )[0].innerHTML;

                    if (/^20[0-9]{2}\/[0-9]{2}\/[0-9]{2}$/.test(timestamp)) {
                        stream.title = timestamp.replaceAll("/", "-") + " - " + object.title;
                    } else if (/^[0-9]日前/.test(timestamp)) {
                        stream.title = getDate(Number(timestamp.replace("日前", "")) * 24) + " - " + object.title;
                    } else if (/.*時間前/.test(timestamp)) {
                        stream.title = getDate(Number(timestamp.replace("時間前", ""))) + " - " + object.title;
                    } else if (/.*分前/.test(timestamp)) {
                        stream.title = getDate(0) + " - " + object.title;
                    } else {
                        stream.title = object.title;
                    }

                    stream.url = object.url;
                    stream.m3u8 = new URL(object.url).pathname.split("/").pop();
                    break;
                }

                case "key":
                    stream.key = object.key;
                    completed = true;
                    break;
            }

            if (completed === true) {
                let streams = JSON.parse(sessionStorage.getItem("streams"));
                streams = streams.filter((obj) => obj.m3u8 !== stream.m3u8);
                streams.push(stream);
                sessionStorage.setItem("streams", JSON.stringify(streams));

                await registerDownloadAllMenu();

                GM_registerMenuCommand(`📋 ${stream.title}`, async () => {
                    const command = await createCommand(stream);
                    await GM_setClipboard(command);
                    GM_notification("Minyami command copied!", "Minyami");
                });

                await sendToServer(stream);
            }
        }
    };
// ========================================== unmodified Code below (from https://raw.githubusercontent.com/Last-Order/Minyami-chrome-extension/refs/heads/master/assets/scripts/inject_core.js) ==========================================

    window.addEventListener("unload", notify({ type: "page_url", url: window.location.href }), false);
    const escapeFilename = (filename) => {
        return filename.replace(/[\/\*\\\:|\?<>"!]/gi, "_");
    };
    let key = "";
    if (window.fetch) {
        const _fetch = fetch;
        fetch = (url, ...fargs) => {
            return new Promise((resolve, reject) => {
                _fetch(url, ...fargs)
                    .then(async (res) => {
                        resolve(res);
                        return res.clone();
                    })
                    .then(async (r) => {
                        if (r.url.match(/\.m3u8(\?|$)/)) {
                            const responseText = await r.text();
                            let title = escapeFilename(document.title);
                            let streamName;
                            switch (location.host) {
                                case "abema.tv": {
                                    if (document.querySelector(".c-tv-SwitchAngleButton-current-angle-name")) {
                                        streamName = document.querySelector(
                                            ".c-tv-SwitchAngleButton-current-angle-name"
                                        ).innerText;
                                    }
                                }
                            }
                            if (responseText.match(/#EXT-X-STREAM-INF/) !== null) {
                                notify({
                                    type: "playlist",
                                    content: responseText,
                                    url: r.url,
                                    title,
                                    streamName
                                });
                            } else {
                                const keyUrlMatch = responseText.match(/#EXT-X-KEY:.*URI="(.*)"/);
                                notify({
                                    type: "chunklist",
                                    content: responseText,
                                    url: r.url,
                                    title,
                                    ...(keyUrlMatch && { keyUrl: keyUrlMatch[1] })
                                });
                            }
                            switch (location.host) {
                                case "spwn.jp": {
                                    spwn();
                                }
                            }
                        }
                    })
                    .catch((e) => {
                        reject(e);
                    });
            });
        };
    }
    XMLHttpRequest.prototype._open = XMLHttpRequest.prototype.open;
    Object.defineProperty(XMLHttpRequest.prototype, "open", {
        get: function() {
            return this._open;
        },
        set: function(f) {
            this._open = new Proxy(f, {
                apply: function(f, instance, fargs) {
                    listen.call(instance, ...fargs);
                    return f.call(instance, ...fargs);
                }
            });
        }
    });
    XMLHttpRequest.prototype.open = XMLHttpRequest.prototype.open;
    const listen = function() {
        this.addEventListener("load", function() {
            if (this.readyState === 4 && new URL(this.responseURL).pathname.endsWith("m3u8")) {
                let title;
                switch (location.host) {
                    case "nogidoga.com": {
                        title = escapeFilename(document.querySelector(".EpisodePage__Title").innerText);
                        break;
                    }
                    case "www.dmm.co.jp": {
                        title = escapeFilename(document.querySelector(".title")?.innerText);
                        break;
                    }
                }
                if (this.responseText.match(/#EXT-X-STREAM-INF/) !== null) {
                    notify({
                        type: "playlist",
                        content: this.responseText,
                        url: this.responseURL,
                        title: title || escapeFilename(document.title)
                    });
                } else {
                    const keyUrlMatch = this.responseText.match(/#EXT-X-KEY:.*URI="(.*)"/);
                    notify({
                        type: "chunklist",
                        content: this.responseText,
                        url: this.responseURL,
                        title: title || escapeFilename(document.title),
                        ...(keyUrlMatch && { keyUrl: keyUrlMatch[1] })
                    });
                }
                // Execute after m3u8 loads
                switch (location.host) {
                    case "live2.nicovideo.jp":
                    case "live.nicovideo.jp": {
                        nico(this);
                        break;
                    }
                    case "www.dmm.com":
                    case "www.dmm.co.jp": {
                        dmm(this);
                        break;
                    }
                    case "spwn.jp": {
                        spwn(this);
                        break;
                    }
                }
            }
            // Execute when first AJAX request finished
            switch (location.host) {
                case "www.dmm.com":
                case "www.dmm.co.jp": {
                    dmm(this);
                    break;
                }
                case "www.360ch.tv": {
                    ch360(this);
                    break;
                }
                case "hibiki-radio.jp": {
                    matchurl(this, "datakey");
                    break;
                }
                case "www.onsen.ag": {
                    matchurl(this, "key.m3u8key");
                    break;
                }
                case "www.showroom-live.com": {
                    showroom(this);
                    break;
                }
                case "nicochannel.jp":
                case "sapocia.com":
                case "gs-ch.com":
                case "qlover.jp":
                case "pizzaradio.jp":
                case "kemomimirefle.net":
                case "tenshi-nano.com":
                case "ado-dokidokihimitsukichi-daigakuimo.com":
                case "canan8181.com":
                case "keisuke-ueda.jp":
                case "p-jinriki-fc.com":
                case "rnqq.jp":
                case "ryogomatsumaru.com":
                case "takahashifumiya.com":
                case "yamingfc.net": {
                    matchurl(this, "https://hls-auth.cloud.stream.co.jp/key");
                    break;
                }
            }
        });
    };
    /**
     * Site Scripts
     */
    /**
     * Get key for Abema!
     */
    const abema = () => {
        Object.defineProperty(__CLIENT_REGION__, "isAllowed", {
            get: () => true
        });
        Object.defineProperty(__CLIENT_REGION__, "status", {
            get: () => false
        });
        const _Uint8Array = Uint8Array;
        Uint8Array = class extends _Uint8Array {
            constructor(...args) {
                super(...args);
                if (this.length === 16) {
                    const key = Array.from(new _Uint8Array(this))
                        .map((i) => (i.toString(16).length === 1 ? "0" + i.toString(16) : i.toString(16)))
                        .join("");
                    if (key !== "00000000000000000000000000000000") {
                        notify({
                            type: "key",
                            key: key
                        });
                    }
                }
                return this;
            }
        };
    };

    const matchurl = (xhr, keyword) => {
        if (xhr.readyState === 4 && xhr.responseURL.includes(keyword)) {
            const key = Array.from(new Uint8Array(xhr.response))
                .map((i) => (i.toString(16).length === 1 ? "0" + i.toString(16) : i.toString(16)))
                .join("");
            notify({
                type: "key",
                key: key,
                url: xhr.responseURL
            });
        }
    };

    const nico = (xhr) => {
        try {
            const liveData = JSON.parse(document.querySelector("#embedded-data").getAttribute("data-props"));
            const websocketUrl = liveData.site.relive.webSocketUrl;
            if (websocketUrl.match(/audience_token=(.+)/)[1]) {
                key = websocketUrl.match(/audience_token=(.+)/)[1];
            }
            if (liveData.program?.stream?.maxQuality) {
                key += `,${liveData.program.stream.maxQuality}`;
            }
            notify({
                type: "key",
                key: key,
            });
        } catch {}
    };

    const spwn = () => {
        notify({
            type: "cookies",
            cookies:
                "CloudFront-Policy=" +
                document.cookie.match(/CloudFront-Policy\=(.+?)(;|$)/)[1] +
                "; " +
                "CloudFront-Signature=" +
                document.cookie.match(/CloudFront-Signature\=(.+?)(;|$)/)[1] +
                "; " +
                "CloudFront-Key-Pair-Id=" +
                document.cookie.match(/CloudFront-Key-Pair-Id\=(.+?)(;|$)/)[1] +
                "; "
        });
    };

    const dmm = (xhr) => {
        if (
            xhr.readyState === 4 &&
            (xhr.responseURL.match(new RegExp("https://www.dmm.(com|co.jp)/service/-/drm_iphone")) ||
                xhr.responseURL.startsWith("https://mlic.dmm.co.jp/drm/hlsaes/key/"))
        ) {
            const key = Array.from(new Uint8Array(xhr.response))
                .map((i) => (i.toString(16).length === 1 ? "0" + i.toString(16) : i.toString(16)))
                .join("");
            notify({
                type: "cookies",
                cookies: "licenseUID=" + document.cookie.match(/licenseUID\=(.+?)(;|$)/)[1]
            });
            notify({
                type: "key",
                key: key,
                url: xhr.responseURL
            });
        }
    };

    const ch360 = (xhr) => {
        notify({
            type: "cookies",
            cookies: "ch360pt=" + document.cookie.match(/ch360pt\=(.+?)(;|$)/)[1]
        });
    };

    const twicas = async () => {
        const userName = location.href.match(/https:\/\/twitcasting.tv\/(.+?)(\/|$)/);
        if (userName && !location.href.includes("/movie/")) {
            await fetch(`https://twitcasting.tv/${userName[1]}/metastream.m3u8`);
        }
        if (location.href.includes("/movie/")) {
            const playlistInfo = document.querySelector("video").getAttribute("data-movie-playlist");
            if (playlistInfo) {
                const parsedPlaylistInfo = JSON.parse(playlistInfo)[2][0];
                const url = parsedPlaylistInfo.source.url;
                const title = escapeFilename(document.querySelector("#movie_title_content").innerText);
                notify({
                    type: "playlist_chunklist",
                    content: "",
                    url,
                    title,
                    chunkLists: [
                        {
                            type: "video",
                            resolution: {
                                x: "Unknown",
                                y: "Unknown"
                            },
                            url
                        }
                    ]
                });
            }
        }
    };

    const youtube = async () => {
        const playerResponse = ytplayer.config.args.raw_player_response;
        if (playerResponse) {
            const HlsManifestUrl = playerResponse.streamingData.hlsManifestUrl;
            await fetch(HlsManifestUrl);
        }
        notify({
            type: "cookies",
            cookies: "PREF=" + document.cookie.match(/PREF\=(.+?)(;|$)/)[1]
        });
    };

    const showroom = (xhr) => {
        if (xhr.responseURL.includes("api/live/streaming_url")) {
            const response = JSON.parse(xhr.responseText);
            if (response.streaming_url_list.some((i) => i.url.endsWith("chunklist.m3u8"))) {
                notify({
                    type: "playlist_chunklist",
                    content: xhr.responseText,
                    url: xhr.responseURL,
                    title: escapeFilename(document.title),
                    chunkLists: response.streaming_url_list
                        .filter((i) => i.url.endsWith("chunklist.m3u8"))
                        .map((i) => {
                            return {
                                type: "video",
                                bandwidth: i.quality * 1024,
                                resolution: {
                                    x: "Unknown",
                                    y: "Unknown"
                                },
                                url: i.url
                            };
                        })
                });
            }
        }
    };

    const bilibili = async () => {
        if (!location.href.match(/live\.bilibili\.com\/(?:blanc\/)*(\d+)/)) {
            return;
        }
        const roomId = location.href.match(/live\.bilibili\.com\/(?:blanc\/)*(\d+)/)[1];
        const api = `https://api.live.bilibili.com/xlive/web-room/v2/index/getRoomPlayInfo?room_id=${roomId}&protocol=0,1&format=0,1,2&codec=0,1,2&qn=10000&platform=web&ptype=16`;
        const roomLiveInfo = await (await fetch(api)).json();
        if (!roomLiveInfo?.data?.playurl_info?.playurl?.stream) {
            return;
        }
        const hlsInfo = roomLiveInfo.data.playurl_info.playurl.stream.find((i) => i.protocol_name === "http_hls");
        const streamFormats = hlsInfo.format;
        const chunkLists = [];
        for (const format of streamFormats) {
            const codec = format.codec[0];
            if (codec.url_info[0].host && codec.base_url) {
                const chunkListUrl = codec.url_info[0].host + codec.base_url + (codec.url_info[0].extra || "");
                fetch(chunkListUrl);
            }
        }
    };

    const asobistore = () => {
        const url = document.querySelector("#embed_placeholder source").getAttribute("src");
        fetch(url);
    };

    // Execute when load
    switch (location.host) {
        case "abema.tv": {
            abema();
            break;
        }
        case "twitcasting.tv": {
            twicas();
            break;
        }
        case "www.youtube.com": {
            youtube();
            break;
        }
        case "live.bilibili.com": {
            bilibili();
            break;
        }
        case "playervspf.channel.or.jp": {
            asobistore();
            break;
        }
    }
})();