osu! my download

osu beatmap download from mirror. osu beatmap镜像站下载。支持的镜像站点:inso.link、osu.sayobot.cn、osu.direct、nerinyan.moe。也可以自行添加。

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 or Violentmonkey 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!)

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!)

// ==UserScript==
// @name           osu! my download
// @description    osu beatmap download from mirror. osu beatmap镜像站下载。支持的镜像站点:inso.link、osu.sayobot.cn、osu.direct、nerinyan.moe。也可以自行添加。
// @author         dazzulay
// @version        2.5
// @license        GPLv3
// @icon           http://osu.ppy.sh/favicon.ico
// @match          http*://osu.ppy.sh/*
// @match          http*://old.ppy.sh/*
// @grant          GM_registerMenuCommand
// @grant          GM_setValue
// @grant          GM_getValue
// @grant          GM_addStyle
// @namespace      https://greasyfork.org/scripts/3916
// @homepageURL    https://greasyfork.org/scripts/3916
// ==/UserScript==

(function () {
    function init() {
        const has_init = GM_getValue("has_init", null);
        if (has_init === null) {
            GM_setValue("mirros", {
                sayobot: {
                    url: 'https://osu.sayobot.cn/home?search={bmid}',
                    text: 'DOWNLOAD SAYOBOT',
                    class: 'my_green',
                    style: '',
                    target: '_blank'
                },
                insolink: {
                    url: 'https://inso.link/?source=osu_my_download&m={bmid}',
                    text: 'DOWNLOAD INSO.LINK',
                    class: 'my_pink',
                    style: '',
                    target: '_blank'
                },
                osu_direct: {
                    url: 'https://osu.direct/api/d/{bmid}',
                    text: 'DOWNLOAD OSU.DIRECT',
                    class: 'my_purpule',
                    style: '',
                    target: '_blank'
                },
                nerinyan: {
                    url: 'https://api.nerinyan.moe/d/{bmid}',
                    text: 'DOWNLOAD NERINYAN',
                    class: 'my_orange',
                    style: '',
                    target: '_blank'
                }
            });
            GM_setValue("has_init", 1);
        }
    }
    init()

    function settingBox() {
        // css样式 设置
        GM_addStyle(`
#my_setting_box{
    max-width: 800px;
    position: fixed;
    top: 100px;
    bottom: 100px;
    left: 0;
    right: 0;
    margin: auto;
    padding: 20px;
    display: flex;
    gap: 20px;
    flex-direction: column;
    background: #fff;
    color: #000;
}
#my_setting_mirros{
    flex: 1;
}
.my_setting_buttons{
    display: flex;
    gap: 20px;
    height: 50px;
}
.my_setting_buttons > *{
    display: block;
}
#my_setting_save{
    flex: 2;
}
#my_setting_reset{
    flex: 1;
}
#my_setting_cancel{
    flex: 1;
}
`);

        GM_registerMenuCommand("Setting", function () {
            if (!document.getElementById("my_setting_box")) {
                let mirros = GM_getValue("mirros");
                mirros = JSON.stringify(mirros, null, "\t");

                const boxHtml = `
<div id="my_setting_box" style="position: fixed; top:10%; left:10%; width:80%; height:80%; background: white; z-index: 99999; border: 1px solid #000; padding: 10px; overflow: auto;">
    <div style="font-weight: bold; margin-bottom: 10px;">osu! my download Setting</div>
    <textarea id="my_setting_mirros" style="width:100%; height:60%;">${mirros}</textarea>
    <div class="my_setting_buttons" style="margin-top:10px;">
        <button id="my_setting_save">保存 Save</button>
        <button id="my_setting_reset">重置 Reset</button>
        <button id="my_setting_cancel">取消 Cancel</button>
    </div>
</div>
`;
                document.body.insertAdjacentHTML('beforeend', boxHtml);

                const btnCancel = document.getElementById("my_setting_cancel");
                btnCancel.addEventListener('click', () => {
                    const box = document.getElementById("my_setting_box");
                    if (box) box.remove();
                });

                const btnReset = document.getElementById("my_setting_reset");
                btnReset.addEventListener('click', () => {
                    GM_setValue("has_init", null);
                    window.location.reload();
                });

                const btnSave = document.getElementById("my_setting_save");
                btnSave.addEventListener('click', () => {
                    try {
                        const val = document.getElementById("my_setting_mirros").value;
                        const parsed = JSON.parse(val);
                        GM_setValue("mirros", parsed);
                        window.location.reload();
                    } catch (e) {
                        alert("Error:" + e);
                    }
                });
            }
        });

    }
    settingBox()

    var domain = document.domain;
    var drive = domain.replace(/\./g, '_');

    function osu_my_downoad() {
        var self = this;
        this.mirros = GM_getValue("mirros")
        /*
            渲染mirro模板,返回渲染的字符串。暂时只渲染url的bmid
             */
        this.mirros_parse = function (bmid) {
            // 深拷贝 this.mirros
            const return_mirros = JSON.parse(JSON.stringify(self.mirros));
            const _param = 'url';
            Object.keys(return_mirros).forEach(function(k) {
                const v = return_mirros[k];
                if (v.hasOwnProperty(_param)) {
                    v[_param] = str_render(v[_param], { bmid: bmid });
                }
            });
            return return_mirros;
        };

        this.drives = {
            osu_ppy_sh: function () {
                // 判断 `.osu-layout` 元素是否存在
                const is_new = document.querySelectorAll('.osu-layout').length;

                if (is_new) {
                    // css样式 设置
                    GM_addStyle(`
.my_container .btn-osu-big__text-top {
white-space: normal;
}
.my_container .btn-osu-big{
position: relative;
}
.my_container .btn-osu-big__content{
position: relative;
}
.my_container a:before {
content: " ";
position: absolute;
left: 0;
right: 0;
top: 0;
bottom: 0;
border-radius: 4px;
}

.my_orange:before {
background-color: rgba(255,141,0,.5)
}

.my_green:before {
background-color: rgba(0,101,0,.5)
}

.my_pink:before {
background-color: rgba(255, 102, 170,.5)
}

.my_purpule:before {
background-color: rgba(169, 10, 165,0.5);
}
`);

                    function addButtons() {
                        // 如果已存在容器,则不再添加
                        if (document.querySelector('.my_container')) {
                            return;
                        }

                        // 获取 beatmap id
                        var audioElem = document.querySelector('.js-audio--play');
                        if (!audioElem) {
                            return false;
                        }
                        var bmsrc = audioElem.getAttribute('data-audio-url');
                        if (!bmsrc) {
                            return false;
                        }
                        var bmid = bmsrc.substring(bmsrc.lastIndexOf("/") + 1, bmsrc.lastIndexOf("."));

                        // 解析镜像/url
                        var parsed_mirros = self.mirros_parse(bmid);

                        // 创建按钮容器
                        var container = document.createElement('div');
                        container.className = 'my_container';

                        // 模板字符串
                        var btnTpl = '<a href="{url}" class="btn-osu-big btn-osu-big--beatmapset-header {class}" style="{style}" target="{target}">' +
                            '<div class="btn-osu-big__content">' +
                            '<div class="btn-osu-big__left">' +
                            '<span class="btn-osu-big__text-top">{text}</span>' +
                            '</div>' +
                            '<div class="btn-osu-big__icon"><span class="fa fa-download"></span></div>' +
                            '</div>' +
                            '</a>';

                        // 遍历 parsed_mirros,插入按钮
                        Object.keys(parsed_mirros).forEach(function(k) {
                            var v = parsed_mirros[k];
                            var html = str_render(btnTpl, v);
                            // 将 html 插入 DOM
                            container.insertAdjacentHTML('beforeend', html);
                        });

                        // 将容器添加到目标元素中
                        var headerButtons = document.querySelector('.beatmapset-header__buttons');
                        if (headerButtons) {
                            headerButtons.appendChild(container);
                        }
                    }

                    // 匹配你关心的 URL 模式
                    const beatmapsetUrlRegex = /^\/beatmapsets\/\d+/;

                    let lastPath = null;

                    // 当 URL 变化时触发
                    function checkUrlChange() {
                        const path = window.location.pathname;
                        if (path !== lastPath) {
                            lastPath = path;
                            if ( beatmapsetUrlRegex.test(path) ) {
                                // 匹配到目标 URL
                                onBeatmapsetPage();
                            }
                        }
                    }

                    // 目标页面触发后要做的事情
                    function onBeatmapsetPage() {
                        console.log("Detected beatmapset page:", window.location.href);

                        // 等待 .beatmapset-header__buttons 元素出现
                        waitForElement('.beatmapset-header__buttons', element => {
                            console.log(".beatmapset-header__buttons 已加载", element);
                            // 在这里执行你的自定义方法
                            // myCustomMethod(element);
                            addButtons();
                        });
                    }

                    // // 自定义方法(你需要修改这里)
                    // function myCustomMethod(buttonsElement) {
                    //     // 举例:在按钮区域插入一个提示
                    //     const tip = document.createElement('div');
                    //     tip.textContent = "自定义功能已激活!";
                    //     tip.style.color = "red";
                    //     tip.style.margin = "8px";
                    //     buttonsElement.parentElement.insertBefore(tip, buttonsElement);
                    // }

                    // 等待元素出现的通用函数(使用 MutationObserver)
                    function waitForElement(selector, callback) {
                        const el = document.querySelector(selector);
                        if (el) {
                            callback(el);
                            return;
                        }
                        const observer = new MutationObserver((mutations, obs) => {
                            const el2 = document.querySelector(selector);
                            if (el2) {
                                obs.disconnect();
                                callback(el2);
                            }
                        });
                        observer.observe(document.body, {
                            childList: true,
                            subtree: true
                        });
                    }

                    // 监听历史记录 API(pushState/replaceState)以捕捉 SPA 导航
                    (function(history) {
                        const pushState = history.pushState;
                        history.pushState = function(...args) {
                            const ret = pushState.apply(this, args);
                            checkUrlChange();
                            return ret;
                        };
                        const replaceState = history.replaceState;
                        history.replaceState = function(...args) {
                            const ret = replaceState.apply(this, args);
                            checkUrlChange();
                            return ret;
                        };
                    })(window.history);

                    // 监听 popstate(后退/前进)
                    window.addEventListener('popstate', () => {
                        checkUrlChange();
                    });

                    // 初次运行
                    checkUrlChange();

                } else {
                    // 获取beatmapid
                    var elem = document.querySelector('.bmt');
                    var bmsrc = elem ? elem.getAttribute('src') : null;
                    if (!bmsrc) {
                        return false;
                    }
                    var bmid = bmsrc.substring(bmsrc.indexOf("thumb/") + 6, bmsrc.lastIndexOf("l"));

                    // css样式 设置
                    GM_addStyle(`
.my_container {
position: fixed;
top: 20px;
right: 0px;
}

.my_btn {
text-align: center;
width: 150px;
height: 111px;
display: table-cell;
vertical-align: middle;
margin: 0 0 10px 0;
padding: 10px;
font-family: Haettenschweiler,Impact,"Arial Grande",Tahoma,Helvetica,Arial,sans-serif;
font-size: 32px;
font-weight: normal;
color: #fff;
border: 4px solid #fff;
border-radius: 6px;
}

.my_btn:hover {
text-shadow: 0 0 20px floralwhite;
color: #fff;
}

.my_btn span {
display: inline-block;
vertical-align: middle;
text-align: center
}

.my_orange {
background: linear-gradient(to bottom,darkorange,wheat,darkorange);
}

.my_blue {
background: linear-gradient(to bottom,darkblue,lightblue,darkblue);
}

.my_green {
background: linear-gradient(to bottom,darkgreen,lightgreen,darkgreen);
}

.my_pink {
background: linear-gradient(to bottom,HotPink,pink,HotPink);
}
.my_purpule {
background: linear-gradient(to bottom,#261326,#E064E0,#261326);
}
`);


                    // 设置url
                    var parsed_mirros = self.mirros_parse(bmid);

                    // 添加按钮
                    // 创建容器
                    var container = document.createElement('div');
                    container.className = 'my_container';

                    // 模板字符串
                    var btnTpl = '<a class="my_btn {class}" style="{style}" href="{url}" target="{target}"><span>{text}</span></a><br/>';

                    // 遍历 parsed_mirros 并插入按钮
                    Object.keys(parsed_mirros).forEach(function(k) {
                        var v = parsed_mirros[k];
                        var html = str_render(btnTpl, v);
                        // 将 html 插入 DOM
                        container.insertAdjacentHTML('beforeend', html);
                    });

                    // 把 container 插入 body
                    document.body.appendChild(container);
                }
            }
        };
        this.init = function () {
            // var domain = document.domain;
            // var drive = domain.replace(/\./g, '_');
            // self.drives[drive]();
            self.drives.osu_ppy_sh();
        };
        self.init();
    }
    osu_my_downoad();


    function str_render(template, context) {

        var tokenReg = /(\\)?\{([^\{\}\\]+)(\\)?\}/g;

        return template.replace(tokenReg, function (word, slash1, token, slash2) {
            if (slash1 || slash2) {
                return word.replace('\\', '');
            }

            var variables = token.replace(/\s/g, '').split('.');
            var currentObject = context;
            var i, length, variable;

            for (i = 0, length = variables.length; i < length; ++i) {
                variable = variables[i];
                currentObject = currentObject[variable];
                if (currentObject === undefined || currentObject === null) return '';
            }
            return currentObject;
        });
    }
})();