// ==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();
}
})();