Osu! Beatmap Star Filter

Integrates star rating, length, and ranked date filters into osu!'s built-in site filters.

// ==UserScript==
// @name         Osu! Beatmap Star Filter
// @namespace    http://tampermonkey.net/
// @version      2.4
// @description  Integrates star rating, length, and ranked date filters into osu!'s built-in site filters.
// @author       YourName
// @match        https://osu.ppy.sh/*
// @grant        none
// ==/UserScript==

(function() {
    'use strict';

    console.log("Osu! Beatmap Star Filter script is running...");

    function convertToMinutes(seconds) {
        let min = Math.floor(seconds / 60);
        let sec = seconds % 60;
        return `${min}:${sec.toString().padStart(2, '0')}`;
    }

    function convertToRankedFormat(dateString) {
        let parts = dateString.split("-");
        return `${parts[1]}/${parts[0]}`; // Convert from YYYY-MM to MM/YYYY
    }

    function integrateFilters() {
        let filters = document.querySelectorAll(".beatmapsets-search-filter");
        let explicitContentFilter = null;

        filters.forEach(filter => {
            let header = filter.querySelector(".beatmapsets-search-filter__header");
            if (header && header.textContent.trim() === "Explicit Content") {
                explicitContentFilter = filter;
            }
        });

        if (!explicitContentFilter || document.getElementById("osu-custom-filters")) return;

        console.log("Explicit content filter found, adding custom filters below...");

        let customFilterDiv = document.createElement("div");
        customFilterDiv.id = "osu-custom-filters";
        customFilterDiv.className = "beatmapsets-search-filter beatmapsets-search-filter--grid";

        let filterHeader = document.createElement("span");
        filterHeader.className = "beatmapsets-search-filter__header";
        filterHeader.textContent = "Custom Filters";

        let filterItems = document.createElement("div");
        filterItems.className = "beatmapsets-search-filter__items";
        filterItems.style.display = "flex";
        filterItems.style.gap = "10px";
        filterItems.style.alignItems = "center";

        function createFilterInput(labelText) {
            let container = document.createElement("div");
            container.style.display = "flex";
            container.style.alignItems = "center";
            container.style.gap = "5px";

            let label = document.createElement("span");
            label.textContent = labelText + ":";
            label.style.color = "white";

            let minInput = document.createElement("input");
            let maxInput = document.createElement("input");
            minInput.type = maxInput.type = "text";
            minInput.placeholder = "Min";
            maxInput.placeholder = "Max";
            minInput.className = maxInput.className = "beatmapsets-search-input";
            minInput.style.width = maxInput.style.width = "60px";
            minInput.style.backgroundColor = maxInput.style.backgroundColor = "hsl(200, 10%, 30%)";
            minInput.style.border = maxInput.style.border = "1px solid hsl(200, 10%, 20%)";
            minInput.style.padding = maxInput.style.padding = "5px";
            minInput.style.borderRadius = maxInput.style.borderRadius = "5px";

            [minInput, maxInput].forEach(input => {
                input.addEventListener("keypress", function(event) {
                    if (event.key === "Enter") {
                        applyFilters();
                    }
                });
            });

            container.appendChild(label);
            container.appendChild(minInput);
            container.appendChild(maxInput);
            return { container, minInput, maxInput };
        }

        let starFilter = createFilterInput("Stars");
        let lengthFilter = createFilterInput("Length");
        let rankedFilter = createFilterInput("Ranked Date");

        let applyButton = document.createElement("button");
        applyButton.textContent = "Apply";
        applyButton.style.padding = "5px 10px";
        applyButton.style.border = "none";
        applyButton.style.background = "#ff66aa";
        applyButton.style.color = "white";
        applyButton.style.borderRadius = "5px";
        applyButton.style.cursor = "pointer";
        applyButton.style.marginLeft = "10px";
        applyButton.addEventListener("click", applyFilters);

        function populateFiltersFromURL() {
            let queryParams = new URLSearchParams(window.location.search);
            let query = queryParams.get("q");
            if (!query) return;

            let starMatch = query.match(/stars>=(\d+(\.\d+)?)/);
            if (starMatch) starFilter.minInput.value = starMatch[1];

            let starMaxMatch = query.match(/stars<=(\d+(\.\d+)?)/);
            if (starMaxMatch) starFilter.maxInput.value = starMaxMatch[1];

            let lengthMinMatch = query.match(/length>=(\d+)/);
            if (lengthMinMatch) lengthFilter.minInput.value = convertToMinutes(lengthMinMatch[1]);

            let lengthMaxMatch = query.match(/length<=(\d+)/);
            if (lengthMaxMatch) lengthFilter.maxInput.value = convertToMinutes(lengthMaxMatch[1]);

            let rankedMinMatch = query.match(/ranked>=(\d{4}-\d{2})/);
            if (rankedMinMatch) rankedFilter.minInput.value = convertToRankedFormat(rankedMinMatch[1]);

            let rankedMaxMatch = query.match(/ranked<(\d{4}-\d{2})/);
            if (rankedMaxMatch) rankedFilter.maxInput.value = convertToRankedFormat(rankedMaxMatch[1]);
        }
        function applyFilters() {
            let queryParams = new URLSearchParams(window.location.search);
            let query = "";

            let minStars = starFilter.minInput.value.trim();
            let maxStars = starFilter.maxInput.value.trim();
            if (minStars) query += `stars>=${minStars} `;
            if (maxStars) query += `stars<=${maxStars} `;

            let minLength = lengthFilter.minInput.value.trim();
            let maxLength = lengthFilter.maxInput.value.trim();
            if (minLength) query += `length>=${convertToSeconds(minLength)} `;
            if (maxLength) query += `length<=${convertToSeconds(maxLength)} `;

            let minRanked = rankedFilter.minInput.value.trim();
            let maxRanked = rankedFilter.maxInput.value.trim();
            if (minRanked) query += `ranked>=${convertToRankedDate(minRanked)} `;
            if (maxRanked) query += `ranked<${convertToRankedDate(maxRanked, true)} `;

            queryParams.set("q", query.trim());
            window.location.search = queryParams.toString();
        }

        function convertToSeconds(time) {
            let parts = time.split(":");
            if (parts.length === 1) {
            // If there's no colon, treat it as seconds
                return parseInt(parts[0]) || 0;
            } else {
            // Otherwise, convert minutes:seconds
                let minutes = parseInt(parts[0]) || 0;
                let seconds = parseInt(parts[1]) || 0;
                return minutes * 60 + seconds;
            }
        }

        function convertToRankedDate(date, isUpperBound = false) {
            let parts = date.split("/"), month = parts[0].padStart(2, "0"), year = parts[1] || new Date().getFullYear();
            return `${year}-${month}${isUpperBound ? "-01" : ""}`;
        }

        filterItems.appendChild(starFilter.container);
        filterItems.appendChild(lengthFilter.container);
        filterItems.appendChild(rankedFilter.container);
        filterItems.appendChild(applyButton);

        customFilterDiv.appendChild(filterHeader);
        customFilterDiv.appendChild(filterItems);

        explicitContentFilter.parentNode.insertBefore(customFilterDiv, explicitContentFilter.nextSibling);

        populateFiltersFromURL();

    }

    function handleNavigation() {
        window.addEventListener("popstate", () => {
            setTimeout(integrateFilters, 100);
        });
    }

    let observer = new MutationObserver(() => {
        if (document.querySelector(".beatmapsets-search-filter")) {
            observer.disconnect();
            integrateFilters();
            handleNavigation();
        }
    });

    observer.observe(document.body, { childList: true, subtree: true });
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址