// ==UserScript==
// @name Erome Enhancer (beta)
// @namespace http://violentmonkey.net/
// @version 2.9
// @license MIT
// @author LisaTurtlesCuck
// @description Filters albums, infinite scroll, duration filtering on album pages, hide viewed albums
// @match https://www.erome.com/a
// @match https://www.erome.com/explore*
// @match https://www.erome.com/search*
// @match https://www.erome.com/user/feed*
// @match https://www.erome.com/user/liked*
// @match https://www.erome.com/user/saved*
// @match https://www.erome.com/*
// @grant none
// ==/UserScript==
(function () {
'use strict';
const STORAGE_KEY = 'eromeEnhancerSettings';
const VIEWED_KEY = 'eromeViewedAlbums';
const DEFAULTS = {
filterMode: 'videos', // 'videos' | 'images' | 'all'
autoScroll: true,
hideViewed: false,
minVideoSeconds: 0, // album: hide videos shorter than this (0 = off)
};
let settings = loadSettings();
let viewedAlbums = loadViewed();
let currentPage = 1;
let loading = false;
const MAX_PAGES = 50;
/* ---------- Storage ---------- */
function loadSettings() {
try {
return Object.assign({}, DEFAULTS, JSON.parse(localStorage.getItem(STORAGE_KEY) || '{}'));
} catch {
return Object.assign({}, DEFAULTS);
}
}
function saveSettings() {
localStorage.setItem(STORAGE_KEY, JSON.stringify(settings));
}
function loadViewed() {
try {
return JSON.parse(localStorage.getItem(VIEWED_KEY) || '[]');
} catch {
return [];
}
}
function saveViewed() {
localStorage.setItem(VIEWED_KEY, JSON.stringify(viewedAlbums));
}
function clearViewed() {
viewedAlbums = [];
saveViewed();
alert('Viewed albums cleared!');
location.reload();
}
/* ---------- Utilities ---------- */
function parseDurationText(text) {
if (!text) return 0;
console.log('Parsing duration text:', text);
const parts = text.trim().split(':').map(Number);
if (parts.length === 3) return parts[0] * 3600 + parts[1] * 60 + parts[2];
if (parts.length === 2) return parts[0] * 60 + parts[1];
return Number(parts[0]) || 0;
}
function fixLazyImages(root = document) {
root.querySelectorAll('img').forEach(img => {
if (img.dataset.src) img.src = img.dataset.src;
if (img.dataset.srcset) img.srcset = img.dataset.srcset;
if (img.getAttribute('data-src')) img.src = img.getAttribute('data-src');
if (img.getAttribute('data-srcset')) img.srcset = img.getAttribute('data-srcset');
img.classList.remove('lazy', 'lazyload', 'lozad');
});
}
/* ---------- Grid Filtering (FIXED) ---------- */
function matchesFilter(albumEl) {
const videoSpan = albumEl.querySelector('.album-videos');
const imageSpan = albumEl.querySelector('.album-images');
const vCount = videoSpan ? Number((videoSpan.textContent.match(/\d+/) || [0])[0]) : 0;
const iCount = imageSpan ? Number((imageSpan.textContent.match(/\d+/) || [0])[0]) : 0;
const anchor = albumEl.querySelector('a');
const url = anchor ? anchor.href : null;
if (settings.hideViewed && url && viewedAlbums.includes(url)) return false;
switch (settings.filterMode) {
case 'videos': return vCount > 0;
case 'images': return iCount > 0 && vCount === 0;
default: return true;
}
}
function markAlbumClick(albumEl) {
const link = albumEl.querySelector('a');
if (!link || link.dataset.eeBound) return;
link.dataset.eeBound = '1';
// FIX: Listen for 'mousedown' and include all buttons (0, 1, 2)
link.addEventListener('mousedown', (event) => {
// Button 0: Left-click (direct navigation)
// Button 1: Middle-click / Scroll Wheel (open in new tab)
// Button 2: Right-click (used to open context menu, where 'open in new tab' is selected)
if (event.button === 0 || event.button === 1 || event.button === 2) {
if (!viewedAlbums.includes(link.href)) {
viewedAlbums.push(link.href);
saveViewed();
console.log(`Album marked as viewed: ${link.href} (Button: ${event.button})`);
}
}
});
}
function applyInitialFilter() {
const container = document.querySelector('#albums');
if (!container) return;
Array.from(container.querySelectorAll('.album')).forEach(album => {
if (!matchesFilter(album)) album.remove();
else {
fixLazyImages(album);
markAlbumClick(album);
}
});
}
/* ---------- Infinite Scroll ---------- */
async function loadNextPage() {
if (loading || currentPage >= MAX_PAGES) return;
loading = true;
const nextPage = currentPage + 1;
const path = location.pathname;
let url = `?page=${nextPage}`;
if (path.startsWith('/explore')) url = `/explore?page=${nextPage}`;
else if (path.startsWith('/search')) {
const p = new URLSearchParams(location.search);
p.set('page', nextPage);
url = `/search?${p.toString()}`;
} else if (path.startsWith('/user/feed')) url = `/user/feed?page=${nextPage}`;
else if (path.startsWith('/user/liked')) url = `/user/liked?page=${nextPage}`;
else if (path.startsWith('/user/saved')) url = `/user/saved?page=${nextPage}`;
else if (/^\/[^/]+$/.test(path)) url = `${path}?page=${nextPage}`;
try {
const res = await fetch(url);
const html = await res.text();
const doc = new DOMParser().parseFromString(html, 'text/html');
fixLazyImages(doc);
const newAlbums = doc.querySelectorAll('.album');
const container = document.querySelector('#albums');
if (container) {
newAlbums.forEach(n => {
const clone = document.importNode(n, true);
if (matchesFilter(clone)) {
fixLazyImages(clone);
markAlbumClick(clone);
container.appendChild(clone);
}
});
}
currentPage = nextPage;
} catch (err) {
console.error('loadNextPage error', err);
}
loading = false;
}
function setupInfiniteScroll() {
window.addEventListener('scroll', () => {
if (!settings.autoScroll) return;
if (window.innerHeight + window.scrollY >= document.body.scrollHeight - 800) {
loadNextPage();
}
});
}
/* ---------- Album Pages (duration filtering) ---------- */
function getMediaGroups() {
// FIXED: Use correct selectors for album pages
return Array.from(document.querySelectorAll('.media-group, .album-media, [class*="media"]'));
}
function isVideoGroup(g) {
// FIXED: Better video detection
return !!(g.querySelector('.duration, video, [class*="video"], .fa-video'));
}
function getGroupDurationSeconds(g) {
// FIXED: More robust duration extraction
let durationText = '';
// Try direct duration element first
const durationEl = g.querySelector('.duration');
if (durationEl) {
durationText = durationEl.textContent || durationEl.innerText || '';
console.log('Found duration element:', durationText);
}
// Try data attributes
if (!durationText && g.dataset.duration) {
durationText = g.dataset.duration;
}
// Try HTML data attribute
if (!durationText && g.getAttribute('data-duration')) {
durationText = g.getAttribute('data-duration');
}
// Fallback: search in inner HTML
if (!durationText) {
const html = g.innerHTML;
const durationMatch = html.match(/(\d{1,2}):(\d{2})(?::(\d{2}))?/);
if (durationMatch) {
durationText = durationMatch[0];
}
}
// Clean up the text
durationText = (durationText || '').trim();
console.log('Final duration text:', durationText, 'for element:', g);
return parseDurationText(durationText);
}
function updateHiddenCounter(n) {
const num = document.getElementById('eeCountNum');
if (num) num.textContent = String(n);
}
function applyAlbumEnhancements() {
// FIXED: Don't use #albums on album pages
if (!location.pathname.startsWith('/a/')) return;
const groups = getMediaGroups();
console.log('Found media groups:', groups.length);
if (!groups.length) {
console.log('No media groups found');
updateHiddenCounter(0);
return;
}
let hidden = 0;
groups.forEach(g => {
if (isVideoGroup(g)) {
const secs = getGroupDurationSeconds(g);
console.log('Video duration:', secs, 'seconds');
if (settings.minVideoSeconds > 0 && secs > 0 && secs < settings.minVideoSeconds) {
g.style.display = 'none';
hidden++;
console.log('Hiding video with duration:', secs, 'seconds');
} else {
g.style.display = '';
}
} else {
g.style.display = '';
}
});
updateHiddenCounter(hidden);
console.log('Duration filter applied:', hidden, 'videos hidden');
}
function observeAlbumChanges() {
if (!location.pathname.startsWith('/a/')) return;
// FIXED: Use body or main container for album pages
const container = document.body;
if (!container) return;
const mo = new MutationObserver(() => {
clearTimeout(window.__ee_album_timeout);
window.__ee_album_timeout = setTimeout(() => {
applyAlbumEnhancements();
}, 500);
});
mo.observe(container, { childList: true, subtree: true });
}
/* ---------- UI ---------- */
function ensureEnhancerNav() {
const nav = document.querySelector('.nav.navbar-nav.navbar-right') || document.querySelector('.navbar-right .nav');
if (!nav) return null;
if (document.getElementById('enhancerNavItem')) return document.getElementById('enhancerBtn');
const li = document.createElement('li');
li.id = 'enhancerNavItem';
li.innerHTML = `
<a href="#" id="enhancerBtn" style="display:inline-flex;align-items:center;gap:8px;">
<i class="fa fa-sliders"></i>
<span>Enhancer</span>
<span style="display:inline-flex;align-items:center;gap:4px;color:#eb6395;margin-left:8px;font-weight:600;font-size:13px;">
<i class="fa fa-eye-slash"></i>
<span id="eeCountNum">0</span>
</span>
</a>`;
nav.insertBefore(li, nav.firstChild);
return document.getElementById('enhancerBtn');
}
function addSettingsUI() {
const anchor = ensureEnhancerNav();
if (!anchor) return;
if (document.getElementById('enhancerModal')) return;
const modal = document.createElement('div');
modal.className = 'modal fade';
modal.id = 'enhancerModal';
// Option 1 - Comprehensive Premium Design
modal.innerHTML = `
<div class="modal-dialog">
<div class="modal-content" style="background:#2b2b2b;color:#fff;">
<div class="modal-header" style="border-bottom:1px solid #444;padding:20px 25px 15px;">
<button type="button" class="close" data-dismiss="modal" style="color:#fff;opacity:0.8;font-size:24px;margin-top:-5px;">×</button>
<h4 class="modal-title" style="font-weight:600;font-size:18px;">
<i class="fa fa-sliders" style="margin-right:10px;color:#eb6395;"></i>Erome Enhancer Settings
</h4>
</div>
<div class="modal-body" style="padding:25px;">
<!-- Main Content Filters -->
<div class="settings-section">
<div class="section-header">
<i class="fa fa-th-large" style="margin-right:8px;"></i>Grid View Filters
</div>
<div class="section-content">
<div class="form-group">
<label class="control-label">Content Filter</label>
<select id="filterMode" class="form-control">
<option value="all">Show All Albums</option>
<option value="videos">Videos Only</option>
<option value="images">Images Only (No Videos)</option>
</select>
</div>
<div class="form-group">
<div class="checkbox">
<label>
<input type="checkbox" id="autoScroll"> Auto-load pages (infinite scroll)
</label>
</div>
</div>
<div class="form-group">
<div class="checkbox">
<label>
<input type="checkbox" id="hideViewed"> Hide viewed albums
</label>
</div>
</div>
</div>
</div>
<hr style="border-color:#444;margin:25px 0;">
<!-- Album Page Filters -->
<div class="settings-section">
<div class="section-header">
<i class="fa fa-film" style="margin-right:8px;"></i>Album Page Filters
</div>
<div class="section-content" style="background:#333;padding:20px;border-radius:8px;margin-top:12px;border:1px solid #444;">
<div class="form-group" style="margin-bottom:20px;">
<label class="control-label" style="font-size:14px;color:#ddd;font-weight:500;">
<i class="fa fa-clock-o" style="margin-right:6px;"></i>Minimum Video Duration
</label>
<div style="display:flex;align-items:center;gap:12px;margin-top:8px;">
<input type="number" id="minVideoSeconds" class="form-control"
min="0" placeholder="0 = disabled"
style="flex:1;background:#444;border:1px solid #555;color:#fff;">
<span style="color:#888;font-size:13px;white-space:nowrap;font-weight:500;">seconds</span>
</div>
<div style="font-size:12px;color:#777;margin-top:8px;line-height:1.4;">
<i class="fa fa-info-circle" style="margin-right:5px;"></i>
Videos shorter than this duration will be hidden on album pages
</div>
</div>
<div style="background:#3a3a3a;padding:12px 15px;border-radius:6px;margin-top:15px;border-left:3px solid #eb6395;">
<div style="font-size:12px;color:#999;display:flex;align-items:center;">
<i class="fa fa-exclamation-circle" style="margin-right:8px;font-size:14px;"></i>
<span>These settings only apply when viewing individual albums</span>
</div>
</div>
</div>
</div>
<!-- Debug Info
<div style="margin-top:20px;padding:12px 15px;background:#333;border-radius:6px;border:1px solid #444;">
<div style="font-size:12px;color:#888;text-align:center;">
<i class="fa fa-bug" style="margin-right:6px;"></i>
Check browser console (F12) for detailed debug information
</div>
</div> -->
</div>
<div class="modal-footer" style="border-top:1px solid #444;padding:20px 25px;">
<div style="display:flex;justify-content:space-between;align-items:center;width:100%;">
<div style="display:flex;gap:10px;">
<button id="clearViewed" class="btn btn-default"
style="background:#555;border-color:#666;color:#ccc;padding:8px 16px;">
<i class="fa fa-trash" style="margin-right:6px;"></i>Clear Viewed
</button>
<button id="resetAlbumFilters" class="btn btn-default"
style="border-color:#eb6395;color:#eb6395;background:transparent;padding:8px 16px;">
<i class="fa fa-refresh" style="margin-right:6px;"></i>Reset Filters
</button>
</div>
<button id="saveEnhancer" class="btn btn-primary"
style="background:#eb6395;border-color:#eb6395;font-weight:600;padding:10px 20px;">
<i class="fa fa-check" style="margin-right:8px;"></i>Apply Settings
</button>
</div>
</div>
</div>
</div>`;
document.body.appendChild(modal);
// Add comprehensive CSS styles
const style = document.createElement('style');
style.textContent = `
.settings-section {
margin-bottom: 25px;
}
.section-header {
font-weight: 600;
color: #eb6395;
font-size: 15px;
margin-bottom: 15px;
padding-bottom: 8px;
border-bottom: 2px solid #444;
display: flex;
align-items: center;
}
.section-content {
padding-left: 10px;
}
.form-group {
margin-bottom: 20px;
}
.control-label {
display: block;
margin-bottom: 8px;
font-size: 14px;
font-weight: 500;
color: #ddd;
}
.form-control {
background: #444;
border: 1px solid #555;
color: #fff;
border-radius: 6px;
padding: 10px 14px;
font-size: 14px;
transition: all 0.3s ease;
}
.form-control:focus {
border-color: #eb6395;
box-shadow: 0 0 0 3px rgba(235, 99, 149, 0.2);
background: #444;
color: #fff;
outline: none;
}
.checkbox {
margin-bottom: 12px;
}
.checkbox label {
display: flex;
align-items: center;
font-size: 14px;
color: #ddd;
cursor: pointer;
transition: color 0.2s ease;
}
.checkbox label:hover {
color: #fff;
}
.checkbox input[type="checkbox"] {
margin-right: 10px;
transform: scale(1.2);
accent-color: #eb6395;
}
.btn {
border-radius: 6px;
font-size: 13px;
padding: 10px 18px;
transition: all 0.3s ease;
border: none;
cursor: pointer;
font-weight: 500;
}
.btn:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0,0,0,0.4);
}
#clearViewed:hover {
background: #666 !important;
border-color: #777 !important;
}
#resetAlbumFilters:hover {
background: rgba(235, 99, 149, 0.15) !important;
box-shadow: 0 4px 12px rgba(235, 99, 149, 0.2);
}
#saveEnhancer:hover {
background: #d85585 !important;
border-color: #d85585 !important;
box-shadow: 0 6px 16px rgba(235, 99, 149, 0.4);
}
.modal-content {
border-radius: 12px;
box-shadow: 0 15px 40px rgba(0,0,0,0.6);
border: 1px solid #444;
}
.modal-header {
background: linear-gradient(135deg, #2b2b2b 0%, #333 100%);
}
.modal-body {
background: linear-gradient(135deg, #2b2b2b 0%, #2f2f2f 100%);
}
`;
document.head.appendChild(style);
anchor.addEventListener('click', e => {
e.preventDefault();
// Initialize modal values
document.getElementById('filterMode').value = settings.filterMode;
document.getElementById('autoScroll').checked = settings.autoScroll;
document.getElementById('hideViewed').checked = settings.hideViewed;
document.getElementById('minVideoSeconds').value = settings.minVideoSeconds || 0;
if (typeof $ === 'function') {
$('#enhancerModal').modal('show');
} else {
modal.style.display = 'block';
}
});
modal.querySelector('#saveEnhancer').addEventListener('click', () => {
settings.filterMode = document.getElementById('filterMode').value;
settings.autoScroll = document.getElementById('autoScroll').checked;
settings.hideViewed = document.getElementById('hideViewed').checked;
settings.minVideoSeconds = parseInt(document.getElementById('minVideoSeconds').value) || 0;
saveSettings();
if (typeof $ === 'function') {
$('#enhancerModal').modal('hide');
} else {
modal.style.display = 'none';
}
setTimeout(() => {
if (location.pathname.startsWith('/a/')) {
applyAlbumEnhancements();
} else {
location.reload();
}
}, 300);
});
modal.querySelector('#clearViewed').addEventListener('click', clearViewed);
modal.querySelector('#resetAlbumFilters').addEventListener('click', () => {
settings.minVideoSeconds = 0;
saveSettings();
document.getElementById('minVideoSeconds').value = 0;
if (location.pathname.startsWith('/a/')) {
applyAlbumEnhancements();
}
});
// Close modal when clicking outside
modal.addEventListener('click', (e) => {
if (e.target === modal) {
if (typeof $ === 'function') {
$('#enhancerModal').modal('hide');
} else {
modal.style.display = 'none';
}
}
});
}
/* ---------- Init ---------- */
function init() {
fixLazyImages();
if (location.pathname.startsWith('/a/')) {
// FIXED: Longer delay for album pages to ensure media is loaded
setTimeout(() => {
console.log('Applying album enhancements...');
applyAlbumEnhancements();
observeAlbumChanges();
}, 2000);
} else {
applyInitialFilter();
setupInfiniteScroll();
}
addSettingsUI();
console.log('Erome Enhancer v2.8.8 loaded - Premium UI');
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();