// ==UserScript==
// @name DMM Top Filter Buttons (Stable + Cached)
// @namespace http://tampermonkey.net/
// @version 1.61
// @description Adds filter buttons to DMM with robust InstantRD and Cached filtering. Persistent active background for all buttons. Buttons remain first children in correct order.
// @author Waseem
// @match https://debridmediamanager.com/*
// @grant none
// @run-at document-idle
// ==/UserScript==
(function() {
'use strict';
function setReactValue(element, value) {
const nativeSetter = Object.getOwnPropertyDescriptor(
window.HTMLInputElement.prototype, 'value'
).set;
nativeSetter.call(element, value);
element.dispatchEvent(new Event('input', { bubbles: true }));
}
function normalizeText(txt) {
return (txt || '').toLowerCase().replace(/\s+/g, ' ').trim();
}
function escapeRegex(str) {
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
let instantRDActive = false;
let cachedActive = false;
const buttons = [
{ text: '⚡instantRD', value: '__bgonly__', class: 'dmm-bgonly', instantRD: true, color: '#166534' },
{ text: 'Cached', class: 'dmm-cached', cached: true, color: '#f59e0b' }, // amber
{ text: '4K', value: '2160p|4k', class: 'dmm-4k', color: '#1e40af' },
{ text: '1080p', value: '1080p', class: 'dmm-1080p', color: '#1e40af' },
{ text: '720p', value: '720p', class: 'dmm-720p', color: '#1e40af' },
{ text: 'Dolby Vision', value: 'dovi|dv|dolby|vision', class: 'dmm-dolbyvision', color: '#1e40af' },
{ text: 'HDR', value: 'hdr', class: 'dmm-hdr', color: '#1e40af' },
{ text: 'Remux', value: 'remux', class: 'dmm-remux', color: '#1e40af' }
];
const qualityValues = ['2160p|4k', '1080p', '720p'];
function getGrid() {
return document.querySelector(
'#__next > div > div.mx-1.my-1.grid.grid-cols-1.gap-2.overflow-x-auto.sm\\:grid-cols-2.md\\:grid-cols-3.lg\\:grid-cols-4.xl\\:grid-cols-6'
);
}
function cardHasInstantRD(card) {
const btns = card.querySelectorAll('button');
for (const b of btns) {
const t = normalizeText(b.textContent);
if (t.includes('instant rd')) return true;
if (t.includes('rd (100%)')) return true;
}
return false;
}
function cardHasCached(card) {
const btns = card.querySelectorAll('button');
for (const b of btns) {
const t = normalizeText(b.textContent);
if (t.includes('rd (100%)')) return true;
}
return false;
}
function applyFilters() {
const grid = getGrid();
if (!grid) return;
const cards = grid.querySelectorAll(':scope > div');
cards.forEach(card => {
const instantRDKeep = !instantRDActive || cardHasInstantRD(card);
const cachedKeep = !cachedActive || cardHasCached(card);
card.style.display = (instantRDKeep && cachedKeep) ? '' : 'none';
});
updateButtonActiveStates();
}
function showAllCards() {
const grid = getGrid();
if (!grid) return;
const cards = grid.querySelectorAll(':scope > div');
cards.forEach(card => (card.style.display = ''));
}
function updateButtonActiveStates() {
const input = document.querySelector('#query');
if (!input) return;
const val = input.value.trim();
buttons.forEach(btn => {
const span = document.querySelector(`span.${btn.class}`);
if (!span) return;
let active = false;
if (btn.instantRD) active = instantRDActive;
else if (btn.cached) active = cachedActive;
else if (val.includes(btn.value)) active = true;
if (active) {
span.classList.add('active');
span.style.setProperty('background-color', btn.color, 'important');
} else {
span.classList.remove('active');
span.style.removeProperty('background-color');
}
});
}
function addFilterButtons() {
const container = document.querySelector(
'#__next > div > div.mb-2.flex.items-center.gap-2.overflow-x-auto.p-2 > div'
);
const input = document.querySelector('#query');
if (!container || !input) return;
const reference = container.firstChild;
buttons.forEach(btn => {
if (container.querySelector(`span.${btn.class}`)) return;
const span = document.createElement('span');
span.textContent = btn.text;
span.className =
`${btn.class} cursor-pointer whitespace-nowrap rounded border border-blue-500 bg-blue-900/30 px-2 py-0.5 text-xs text-blue-100 transition-colors hover:bg-blue-800/50`;
span.dataset.color = btn.color;
if (reference) container.insertBefore(span, reference);
else container.appendChild(span);
span.addEventListener('click', () => {
if (btn.instantRD) {
instantRDActive = !instantRDActive;
if (instantRDActive) applyFilters();
else {
showAllCards();
applyFilters();
}
updateButtonActiveStates();
return;
}
if (btn.cached) {
cachedActive = !cachedActive;
applyFilters();
return;
}
let current = input.value.trim();
if (qualityValues.includes(btn.value)) {
const clickedQuality = btn.value;
const escaped = escapeRegex(clickedQuality);
const regex = new RegExp(`(^|\\||\\s)${escaped}($|\\||\\s)`);
if (current.match(regex)) {
const parts = current.split(' ');
const filteredParts = parts.map(part => {
const qualityRegex = new RegExp(`(^|\\|)${escaped}($|\\|)`);
let cleaned = part.replace(qualityRegex, (match, before, after) => {
if (before === '|' && after === '|') return '|';
return '';
});
cleaned = cleaned.replace(/\|{2,}/g, '|').replace(/^\|+|\|+$/g, '');
return cleaned;
}).filter(Boolean);
current = filteredParts.join(' ').trim();
setReactValue(input, current);
applyFilters();
updateButtonActiveStates();
return;
}
let lastFound = null;
qualityValues.forEach(q => {
const idx = current.lastIndexOf(q);
if (idx !== -1) lastFound = { value: q, index: idx };
});
if (lastFound) {
const before = current.slice(0, lastFound.index + lastFound.value.length);
const after = current.slice(lastFound.index + lastFound.value.length);
current = before + '|' + clickedQuality + after;
} else current = current ? current + ' ' + clickedQuality : clickedQuality;
current = current.replace(/\|{2,}/g, '|').replace(/^\|+|\|+$/g, '');
current = current.replace(/\s+/g, ' ').trim();
setReactValue(input, current);
applyFilters();
updateButtonActiveStates();
return;
}
if (current.includes(btn.value)) {
const regex = new RegExp(`\\s*\\b${escapeRegex(btn.value)}\\b\\s*`, 'g');
current = current.replace(regex, ' ').trim();
current = current.replace(/\s+/g, ' ');
setReactValue(input, current);
} else {
current = current ? current + ' ' + btn.value : btn.value;
setReactValue(input, current);
}
applyFilters();
updateButtonActiveStates();
});
});
updateButtonActiveStates();
applyFilters();
}
function setupGlobalListeners() {
const input = document.querySelector('#query');
if (!input) return;
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') setReactValue(input, '');
});
input.addEventListener('input', () => {
applyFilters();
});
}
const observer = new MutationObserver(() => {
addFilterButtons();
setupGlobalListeners();
applyFilters();
});
observer.observe(document.body, { childList: true, subtree: true });
addFilterButtons();
setupGlobalListeners();
applyFilters();
})();