RYM Genre/Charts Toolkit

Toolkit for RYM: filtering, search, export, statistics, ad removal & more

Vous devrez installer une extension telle que Tampermonkey, Greasemonkey ou Violentmonkey pour installer ce script.

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

Vous devrez installer une extension telle que Tampermonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Userscripts pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension de gestionnaire de script utilisateur pour installer ce script.

(J'ai déjà un gestionnaire de scripts utilisateur, laissez-moi l'installer !)

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

(J'ai déjà un gestionnaire de style utilisateur, laissez-moi l'installer!)

// ==UserScript==
// @name         RYM Genre/Charts Toolkit
// @namespace    http://tampermonkey.net/
// @version      3.1
// @description  Toolkit for RYM: filtering, search, export, statistics, ad removal & more
// @author       dil83
// @license      MIT
// @match        https://rateyourmusic.com/*
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_deleteValue
// ==/UserScript==

(function() {
    'use strict';

    // ===== AD REMOVAL =====
    const AdBlocker = {
        selectors: [
            'div.page_creative_frame',
            'div.connatix_video',
            '[class*="advertisement"]',
            '[id*="ad_"]',
            '.ad-container'
        ],
        
        remove: () => {
            AdBlocker.selectors.forEach(selector => {
                document.querySelectorAll(selector).forEach(ad => {
                    ad.remove();
                    console.log(`[RYM Toolkit] Removed ad: ${selector}`);
                });
            });
        },
        
        init: () => {
            // Remove ads initially
            AdBlocker.remove();
            
            // Observe DOM changes to remove ads instantly when they appear
            const observer = new MutationObserver(() => {
                AdBlocker.remove();
            });
            
            observer.observe(document.body, { 
                childList: true, 
                subtree: true 
            });
            
            console.log('[RYM Toolkit] Ad blocker initialized');
        }
    };
    
    // Initialize ad blocker immediately (works on all pages)
    AdBlocker.init();

    // ===== UTILITY FUNCTIONS =====
    const Utils = {
        getMonthName: (n) => ["", "January", "February", "March", "April", "May", "June", 
                              "July", "August", "September", "October", "November", "December"][n] || "error",
        
        getLastDayOfMonth: (year, month) => new Date(year, month, 0).getDate(),
        
        formatDate: (date) => date.toISOString().slice(0, 10).replace(/-/g, '.'),
        
        debounce: (func, wait) => {
            let timeout;
            return function(...args) {
                clearTimeout(timeout);
                timeout = setTimeout(() => func.apply(this, args), wait);
            };
        },
        
        parseRating: (text) => parseFloat(text.replace(/[^\d.]/g, '')) || 0,
        
        parseCount: (text) => {
            const match = text.match(/([\d.]+)([kKmM]?)/);
            if (!match) return 0;
            const num = parseFloat(match[1]);
            const mult = match[2].toLowerCase();
            return mult === 'k' ? num * 1000 : mult === 'm' ? num * 1000000 : num;
        }
    };
    const State = {
        currentDate: new Date(),
        currentYear: new Date().getFullYear(),
        currentMonth: new Date().getMonth() + 1,
        currentDay: new Date().getDate(),
        isChartsPage: window.location.pathname.includes('/charts/'),
        isGenrePage: window.location.pathname.includes('/genre/'),
        theme: document.documentElement.className.match(/theme_(\w+)/)?.[1] || 'light',
        scrollInterval: null,
        isScrolling: false,
        highlightedItems: new Set(),
        
        getGenre: () => {
            const match = window.location.href.match(/g:([^/]+)/);
            if (match) return match[1];
            if (State.isGenrePage) {
                return window.location.pathname.split('/genre/')[1].replace('/', '');
            }
            return '';
        },        
        getCurrentFilter: () => {
            const periodPattern = /^(\d{4}s?|\d{4}-\d{4}|all-time|\d{4}\.\d{2}\.\d{2}-\d{4}\.\d{2}\.\d{2})$/;
            const pathParts = window.location.pathname.split('/');
            const periodIdx = pathParts.findIndex((part, idx) => idx === 4 && periodPattern.test(part));
            
            let year = State.currentYear;
            let period = 0;
            
            if (periodIdx !== -1) {
                const periodStr = pathParts[periodIdx];
                if (/^\d{4}$/.test(periodStr)) {
                    year = parseInt(periodStr);
                } else if (/^\d{4}\.\d{2}\.\d{2}-\d{4}\.\d{2}\.\d{2}$/.test(periodStr)) {
                    year = parseInt(periodStr.split('.')[0]);
                    period = parseInt(periodStr.split('.')[1]);
                }
            }
            return { year, period };
        }
    };
    const URLBuilder = {
        buildDateRange: (year, month) => {
            if (month < 1 || month > 12) return '';
            if (year > State.currentYear || (year === State.currentYear && month > State.currentMonth)) return '';
            
            let lastDay = Utils.getLastDayOfMonth(year, month);
            if (year === State.currentYear && month === State.currentMonth) {
                lastDay = Math.min(lastDay, State.currentDay);
            }
            
            const monthStr = month.toString().padStart(2, '0');
            const lastDayStr = lastDay.toString().padStart(2, '0');
            return `${year}.${monthStr}.01-${year}.${monthStr}.${lastDayStr}`;
        },
        
        getLastDaysRange: (days) => {
            const endDate = State.currentDate;
            const startDate = new Date(State.currentDate);
            startDate.setDate(endDate.getDate() - days + 1);
            return `${Utils.formatDate(startDate)}-${Utils.formatDate(endDate)}`;
        },
        
        buildFilterURL: (year, value) => {
            let filterPeriod = '';
            
            if (value > 12) {
                const daysMap = { 13: 7, 14: 14, 15: 30, 16: 90, 17: 180 };
                filterPeriod = URLBuilder.getLastDaysRange(daysMap[value]);
            } else if (value > 0) {
                filterPeriod = URLBuilder.buildDateRange(year, value);
            } else {
                if (year > State.currentYear) return null;
                filterPeriod = `${year}`;
            }
            
            if (!filterPeriod) return null;
            
            if (State.isGenrePage) {
                return `${window.location.origin}/charts/top/album/${filterPeriod}/g:${State.getGenre()}/`;
            }
            
            const path = window.location.pathname.replace(/\/+$/, '');
            const parts = path.split('/');
            const periodPattern = /^(\d{4}s?|\d{4}-\d{4}|all-time|\d{4}\.\d{2}\.\d{2}-\d{4}\.\d{2}\.\d{2})$/;
            let periodIdx = parts.findIndex((part, idx) => idx === 4 && periodPattern.test(part));
            
            if (periodIdx === -1) {
                parts.splice(4, 0, filterPeriod);
            } else {
                parts[periodIdx] = filterPeriod;
            }
            
            return window.location.origin + parts.join('/');
        }
    };

    // ===== DATA EXTRACTION =====
    const DataExtractor = {
        getChartItems: () => {
            let items = document.querySelectorAll('.page_charts_section_charts_item');
            let isCarousel = false;
            if (items.length === 0) {
                items = document.querySelectorAll('.page_section_charts_carousel_item');
                isCarousel = true;
            }
            return { items, isCarousel };
        },
        
        extractItemData: (item, index, isCarousel) => {
            const getData = (selector, attr = 'innerText') => {
                const el = item.querySelector(selector);
                return el ? (attr === 'innerText' ? el.innerText.trim() : el.getAttribute(attr)) : '';
            };
            
            const getMultiple = (selector) => 
                Array.from(item.querySelectorAll(selector)).map(el => el.innerText.trim()).join(', ');
            
            return {
                rank: isCarousel 
                    ? getData('.page_section_charts_carousel_number div') || (index + 1)
                    : getData('.page_charts_section_charts_item_number > div:first-child') || (index + 1),
                artist: getData('.artist a, .page_charts_section_charts_item_credited_links_primary a.artist'),
                release: getData('.release a, .page_charts_section_charts_item_title a .ui_name_locale_original'),
                date: getData('.page_charts_section_charts_item_date span, .chart_stats_release_date .rendered_text'),
                type: getData('.page_charts_section_charts_item_release_type'),
                primaryGenres: getMultiple('.page_charts_section_charts_item_genres_primary a'),
                secondaryGenres: getMultiple('.page_charts_section_charts_item_genres_secondary a'),
                descriptors: getMultiple('.page_charts_section_charts_item_genre_descriptors span'),
                rating: getData('.page_charts_section_charts_item_details_average_num, .chart_stats_rating_text'),
                ratingsCount: getData('.page_charts_section_charts_item_details_ratings .abbr, .chart_stats_ratings .abbr'),
                reviews: getData('.page_charts_section_charts_item_details_reviews .abbr, .chart_stats_reviews .abbr'),
                url: getData('.page_charts_section_charts_item_title a, .release a', 'href')
            };
        }
    };
    const UI = {
        createElement: (tag, props = {}, children = []) => {
            const el = document.createElement(tag);
            Object.entries(props).forEach(([key, value]) => {
                if (key === 'style' && typeof value === 'object') {
                    Object.assign(el.style, value);
                } else if (key.startsWith('on')) {
                    el.addEventListener(key.slice(2).toLowerCase(), value);
                } else {
                    el[key] = value;
                }
            });
            children.forEach(child => {
                if (typeof child === 'string') {
                    el.appendChild(document.createTextNode(child));
                } else if (child) {
                    el.appendChild(child);
                }
            });
            return el;
        },
        
        applyTheme: (element) => {
            if (State.theme !== 'light') {
                element.style.backgroundColor = '#333';
                element.style.color = '#fff';
                element.style.border = '1px solid #555';
            }
            return element;
        },
        
        createButton: (text, onClick, style = {}) => {
            const btn = UI.createElement('button', {
                textContent: text,
                onClick,
                style: { margin: '2px', padding: '5px 10px', cursor: 'pointer', ...style }
            });
            return UI.applyTheme(btn);
        },
        
        createInput: (type, props = {}) => {
            const input = UI.createElement('input', { type, ...props });
            return UI.applyTheme(input);
        },
        
        createSelect: (options, selectedValue, onChange) => {
            const select = UI.createElement('select', { onChange });
            options.forEach(({ value, text, selected }) => {
                const opt = UI.createElement('option', { 
                    value, 
                    textContent: text,
                    selected: selected || value == selectedValue 
                });
                select.appendChild(opt);
            });
            return UI.applyTheme(select);
        }
    };
    const Features = {
        // Year/Month Filter
        createDateFilter: () => {
            const container = UI.createElement('div', { style: { marginBottom: '10px' } });
            const { year, period } = State.getCurrentFilter();
            
            const yearOptions = Array.from({ length: State.currentYear - 1899 }, (_, i) => ({
                value: 1900 + i,
                text: 1900 + i
            }));
            
            const yearSelect = UI.createSelect(yearOptions, year, function() {
                const url = URLBuilder.buildFilterURL(parseInt(this.value), parseInt(monthSelect.value));
                if (url) window.location.href = url;
            });
            
            const monthOptions = [
                { value: 0, text: 'Select month' },
                ...Array.from({ length: 12 }, (_, i) => ({ value: i + 1, text: Utils.getMonthName(i + 1) })),
                { value: 0, text: '-----' },
                { value: 13, text: 'Last week' },
                { value: 14, text: 'Last 2 weeks' },
                { value: 15, text: 'Last 30 days' },
                { value: 16, text: 'Last 90 days' },
                { value: 17, text: 'Last 180 days' }
            ];
            
            const monthSelect = UI.createSelect(monthOptions, period, function() {
                const url = URLBuilder.buildFilterURL(parseInt(yearSelect.value), parseInt(this.value));
                if (url) window.location.href = url;
            });
            
            container.appendChild(document.createTextNode('Year: '));
            container.appendChild(yearSelect);
            container.appendChild(UI.createElement('br'));
            container.appendChild(document.createTextNode('Month/Period: '));
            container.appendChild(monthSelect);
            
            return container;
        },
        createCustomDateRange: () => {
            const container = UI.createElement('div', { style: { marginTop: '10px' } });
            const startInput = UI.createInput('date');
            const endInput = UI.createInput('date');
            
            const applyBtn = UI.createButton('Apply Custom Range', () => {
                const startDate = new Date(startInput.value);
                const endDate = new Date(endInput.value);
                
                if (startDate > State.currentDate || endDate > State.currentDate) {
                    alert("Future dates are not available.");
                    return;
                }
                
                const filterPeriod = `${startInput.value.replace(/-/g, '.')}-${endInput.value.replace(/-/g, '.')}`;
                const path = window.location.pathname.replace(/\/+$/, '').split('/');
                const periodPattern = /^(\d{4}s?|\d{4}-\d{4}|all-time|\d{4}\.\d{2}\.\d{2}-\d{4}\.\d{2}\.\d{2})$/;
                let periodIdx = path.findIndex((part, idx) => idx === 4 && periodPattern.test(part));
                
                if (periodIdx === -1) {
                    path.splice(4, 0, filterPeriod);
                } else {
                    path[periodIdx] = filterPeriod;
                }
                
                window.location.href = window.location.origin + path.join('/');
            });
            
            container.appendChild(document.createTextNode('Custom Start: '));
            container.appendChild(startInput);
            container.appendChild(document.createTextNode(' End: '));
            container.appendChild(endInput);
            container.appendChild(applyBtn);
            
            return container;
        },
        createToggle: () => {
            const isAlbum = window.location.pathname.includes('/album/');
            return UI.createButton(
                isAlbum ? 'Switch to Songs' : 'Switch to Albums',
                () => {
                    const newPath = window.location.pathname.replace(
                        /\/(album|song)\//,
                        `/${isAlbum ? 'song' : 'album'}/`
                    );
                    window.location.href = window.location.origin + newPath;
                }
            );
        },
        createQuickLinks: () => {
            const genre = State.getGenre();
            if (!genre) return null;
            
            const container = UI.createElement('div', { style: { marginTop: '10px' } });
            const links = [
                { text: 'Top Albums All-Time', url: `/charts/top/album/all-time/g:${genre}/` },
                { text: 'Top Songs All-Time', url: `/charts/top/song/all-time/g:${genre}/` },
                { text: `Top Albums ${State.currentYear}`, url: `/charts/top/album/${State.currentYear}/g:${genre}/` },
                { text: 'New Releases', url: `/new-music/g:${genre}/` }
            ];
            
            links.forEach(({ text, url }) => {
                container.appendChild(UI.createButton(text, () => {
                    window.location.href = window.location.origin + url;
                }));
            });
            
            return container;
        },
        createSearch: () => {
            const container = UI.createElement('div', { style: { marginTop: '10px' } });
            const searchInput = UI.createInput('text', { 
                placeholder: 'Search by artist/album/genre...',
                style: { width: '300px', marginRight: '5px' }
            });
            
            const filterSelect = UI.createSelect([
                { value: 'all', text: 'All' },
                { value: 'artist', text: 'Artist' },
                { value: 'album', text: 'Album' },
                { value: 'genre', text: 'Genre' }
            ], 'all', () => performSearch());
            
            const performSearch = Utils.debounce(() => {
                const query = searchInput.value.toLowerCase();
                const filterType = filterSelect.value;
                const { items } = DataExtractor.getChartItems();
                
                items.forEach(item => {
                    let shouldShow = !query;
                    
                    if (query) {
                        const text = item.innerText.toLowerCase();
                        if (filterType === 'all') {
                            shouldShow = text.includes(query);
                        } else if (filterType === 'artist') {
                            const artist = item.querySelector('.artist a')?.innerText.toLowerCase() || '';
                            shouldShow = artist.includes(query);
                        } else if (filterType === 'album') {
                            const album = item.querySelector('.release a')?.innerText.toLowerCase() || '';
                            shouldShow = album.includes(query);
                        } else if (filterType === 'genre') {
                            const genres = item.querySelector('.page_charts_section_charts_item_genres_primary')?.innerText.toLowerCase() || '';
                            shouldShow = genres.includes(query);
                        }
                    }
                    
                    item.style.display = shouldShow ? '' : 'none';
                });
            }, 300);
            
            searchInput.oninput = performSearch;
            
            container.appendChild(searchInput);
            container.appendChild(filterSelect);
            
            return container;
        },
        createExport: () => {
            const container = UI.createElement('div', { style: { marginTop: '10px' } });
            
            const exportMD = UI.createButton('Export to Markdown', () => {
                const { items, isCarousel } = DataExtractor.getChartItems();
                let md = '# RYM Chart\n\n| Rank | Artist | Release | Date | Type | Primary Genres | Secondary Genres | Descriptors | Rating | Ratings | Reviews |\n|------|--------|---------|------|------|----------------|------------------|-------------|--------|---------|--------|\n';
                
                items.forEach((item, index) => {
                    const data = DataExtractor.extractItemData(item, index, isCarousel);
                    md += `| ${data.rank} | ${data.artist} | ${data.release} | ${data.date} | ${data.type} | ${data.primaryGenres} | ${data.secondaryGenres} | ${data.descriptors} | ${data.rating} | ${data.ratingsCount} | ${data.reviews} |\n`;
                });
                
                const blob = new Blob([md], { type: 'text/markdown' });
                const link = document.createElement('a');
                link.href = URL.createObjectURL(blob);
                link.download = `rym_chart_${Date.now()}.md`;
                link.click();
            });
            
            const exportCSV = UI.createButton('Export to CSV', () => {
                const { items, isCarousel } = DataExtractor.getChartItems();
                let csv = 'Rank,Artist,Release,Date,Type,Primary Genres,Secondary Genres,Descriptors,Rating,Ratings,Reviews,URL\n';
                
                items.forEach((item, index) => {
                    const data = DataExtractor.extractItemData(item, index, isCarousel);
                    const row = [
                        data.rank, data.artist, data.release, data.date, data.type,
                        data.primaryGenres, data.secondaryGenres, data.descriptors,
                        data.rating, data.ratingsCount, data.reviews, data.url
                    ].map(v => `"${String(v).replace(/"/g, '""')}"`).join(',');
                    csv += row + '\n';
                });
                
                const blob = new Blob([csv], { type: 'text/csv' });
                const link = document.createElement('a');
                link.href = URL.createObjectURL(blob);
                link.download = `rym_chart_${Date.now()}.csv`;
                link.click();
            });
            
            const exportJSON = UI.createButton('Export to JSON', () => {
                const { items, isCarousel } = DataExtractor.getChartItems();
                const data = Array.from(items).map((item, index) => 
                    DataExtractor.extractItemData(item, index, isCarousel)
                );
                
                const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
                const link = document.createElement('a');
                link.href = URL.createObjectURL(blob);
                link.download = `rym_chart_${Date.now()}.json`;
                link.click();
            });
            
            container.appendChild(exportMD);
            container.appendChild(exportCSV);
            container.appendChild(exportJSON);
            
            return container;
        },
        createStatistics: () => {
            const container = UI.createElement('div', { style: { marginTop: '10px', padding: '10px', border: '1px solid #ccc' } });
            const { items, isCarousel } = DataExtractor.getChartItems();
            
            if (items.length === 0) return null;
            
            const allData = Array.from(items).map((item, index) => 
                DataExtractor.extractItemData(item, index, isCarousel)
            );
            
            const ratings = allData.map(d => Utils.parseRating(d.rating)).filter(r => r > 0);
            const counts = allData.map(d => Utils.parseCount(d.ratingsCount)).filter(c => c > 0);
            
            const avgRating = ratings.length ? (ratings.reduce((a, b) => a + b, 0) / ratings.length).toFixed(2) : 'N/A';
            const avgCount = counts.length ? Math.round(counts.reduce((a, b) => a + b, 0) / counts.length) : 'N/A';
            const totalRatings = counts.reduce((a, b) => a + b, 0);
            
            const genres = {};
            allData.forEach(d => {
                d.primaryGenres.split(', ').forEach(g => {
                    if (g) genres[g] = (genres[g] || 0) + 1;
                });
            });
            
            const topGenre = Object.entries(genres).sort((a, b) => b[1] - a[1])[0];
            
            container.innerHTML = `
                <strong>Chart Statistics:</strong><br>
                Total Items: ${items.length}<br>
                Avg Rating: ${avgRating}<br>
                Avg Rating Count: ${avgCount}<br>
                Total Ratings: ${totalRatings.toLocaleString()}<br>
                Most Common Genre: ${topGenre ? `${topGenre[0]} (${topGenre[1]})` : 'N/A'}
            `;
            
            return container;
        },
        createAutoScroll: () => {
            const container = UI.createElement('div', { style: { marginTop: '10px' } });
            const speedInput = UI.createInput('range', {
                min: 1,
                max: 10,
                value: 5,
                style: { width: '100px', marginLeft: '5px' }
            });
            
            const toggleBtn = UI.createButton('Start Auto-Scroll', () => {
                State.isScrolling = !State.isScrolling;
                toggleBtn.textContent = State.isScrolling ? 'Stop Auto-Scroll' : 'Start Auto-Scroll';
                
                if (State.isScrolling) {
                    const speed = parseInt(speedInput.value);
                    State.scrollInterval = setInterval(() => window.scrollBy(0, speed * 2), 50);
                } else {
                    clearInterval(State.scrollInterval);
                }
            });
            
            container.appendChild(toggleBtn);
            container.appendChild(document.createTextNode(' Speed: '));
            container.appendChild(speedInput);
            document.addEventListener('keydown', (e) => {
                if (e.key === 's' && State.isScrolling) {
                    State.isScrolling = false;
                    toggleBtn.textContent = 'Start Auto-Scroll';
                    clearInterval(State.scrollInterval);
                }
            });
            
            return container;
        },
        createHighlighter: () => {
            const container = UI.createElement('div', { style: { marginTop: '10px' } });
            
            const ratingInput = UI.createInput('number', {
                placeholder: 'Min rating',
                step: '0.1',
                style: { width: '100px', marginRight: '5px' }
            });
            
            const highlightBtn = UI.createButton('Highlight High Rated', () => {
                const minRating = parseFloat(ratingInput.value) || 3.5;
                const { items, isCarousel } = DataExtractor.getChartItems();
                
                items.forEach((item, index) => {
                    const data = DataExtractor.extractItemData(item, index, isCarousel);
                    const rating = Utils.parseRating(data.rating);
                    
                    if (rating >= minRating) {
                        item.style.backgroundColor = State.theme === 'light' ? '#ffffcc' : '#444400';
                        State.highlightedItems.add(item);
                    }
                });
            });
            
            const clearBtn = UI.createButton('Clear Highlights', () => {
                State.highlightedItems.forEach(item => {
                    item.style.backgroundColor = '';
                });
                State.highlightedItems.clear();
            });
            
            container.appendChild(ratingInput);
            container.appendChild(highlightBtn);
            container.appendChild(clearBtn);
            
            return container;
        },
        createQuickFilters: () => {
            const container = UI.createElement('div', { style: { marginTop: '10px' } });
            
            const filters = [
                { text: 'This Month', period: State.currentMonth },
                { text: 'Last Month', period: State.currentMonth - 1 || 12 },
                { text: 'This Year', period: 0 },
                { text: 'Last 30 Days', period: 15 }
            ];
            
            filters.forEach(({ text, period }) => {
                container.appendChild(UI.createButton(text, () => {
                    const year = period === 0 || period >= State.currentMonth ? State.currentYear : State.currentYear;
                    const url = URLBuilder.buildFilterURL(year, period);
                    if (url) window.location.href = url;
                }, { fontSize: '12px' }));
            });
            
            return container;
        }
    };
    const init = () => {
        // Only show toolkit on charts and genre pages
        if (!State.isChartsPage && !State.isGenrePage) {
            console.log('[RYM Toolkit] Toolkit UI not shown (only on charts/genre pages), but ad blocker is active');
            return;
        }
        
        const toolkitDiv = UI.createElement('div', {
            className: 'page_section page_genre_section',
            style: {
                padding: '15px',
                marginBottom: '20px',
                backgroundColor: State.theme === 'light' ? '#f0f0f0' : '#222',
                color: State.theme === 'light' ? '#000' : '#fff',
                border: `1px solid ${State.theme === 'light' ? '#ccc' : '#444'}`
            }
        });
        
        toolkitDiv.appendChild(UI.createElement('h3', { textContent: '🛠️ RYM Toolkit by dil83' }));
        const adStatus = UI.createElement('div', {
            style: {
                fontSize: '11px',
                color: State.theme === 'light' ? '#006600' : '#00ff00',
                marginBottom: '8px',
                fontWeight: 'bold'
            },
            textContent: '✓ Ad Blocker Active (Site-wide)'
        });
        toolkitDiv.appendChild(adStatus);
        toolkitDiv.appendChild(Features.createDateFilter());
        toolkitDiv.appendChild(Features.createCustomDateRange());
        
        if (State.isChartsPage) {
            toolkitDiv.appendChild(UI.createElement('br'));
            toolkitDiv.appendChild(Features.createToggle());
            toolkitDiv.appendChild(Features.createQuickFilters());
            toolkitDiv.appendChild(Features.createSearch());
            toolkitDiv.appendChild(Features.createHighlighter());
            
            const stats = Features.createStatistics();
            if (stats) toolkitDiv.appendChild(stats);
            
            toolkitDiv.appendChild(Features.createExport());
        }
        
        if (State.isGenrePage) {
            const quickLinks = Features.createQuickLinks();
            if (quickLinks) toolkitDiv.appendChild(quickLinks);
        }
        
        toolkitDiv.appendChild(Features.createAutoScroll());
        
        const insertPoint = State.isChartsPage 
            ? document.querySelector('.page_chart_query_advanced')
            : document.querySelector('#page_genre_group_main_info_left');
        
        if (insertPoint) {
            insertPoint.prepend(toolkitDiv);
        }
    };
    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', init);
    } else {
        init();
    }
})();