Erome Ultimate Suite

Download media, skip age warning, sort/filter albums, infinite scroll, like counts, duration badges, and more.

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==UserScript==
// @name         Erome Ultimate Suite
// @description  Download media, skip age warning, sort/filter albums, infinite scroll, like counts, duration badges, and more.
// @version      1.0.1
// @author       RJ, RMS Carpathia, LisaTurtlesCuck , Claude AI
// @license      MIT
// @icon         https://www.erome.com/android-chrome-192x192.png
// @match        http://erome.com/*
// @match        http://*.erome.com/*
// @match        https://erome.com/*
// @match        https://*.erome.com/*
// @grant        none
// @run-at       document-end
// @namespace https://greasyfork.org/users/1076811
// ==/UserScript==

(function() {
    'use strict';

    // ========== Age Warn Skipper ==========
    let skipWarnCount = 0;
    const skipWarn = () => {
        const buttonEnter = document.getElementsByClassName('enter');

        if (skipWarnCount >= 15) return;

        if (buttonEnter.length === 0) {
            skipWarnCount++;
            setTimeout(skipWarn, 30);
            return;
        }

        buttonEnter[0].click();
        console.log('[AGE WARN SKIPPER]: WARN SKIPPED!');
    };
    console.log('[LOADED]: AGE WARN SKIPPER');

    // ========== Better Erome Media Downloader ==========
    function getMediaSrc(media) {
        if (media.tagName === 'IMG') {
            return media.src || media.getAttribute('data-src') || '';
        }
        if (media.tagName === 'VIDEO') {
            if (media.src) return media.src;
            const source = media.querySelector('source');
            return source ? source.src : '';
        }
        return '';
    }

    function getFileNameFromUrl(url) {
        try {
            const urlObj = new URL(url);
            let filename = urlObj.pathname.split('/').pop();
            if (!filename || filename === '') filename = 'download';
            if (!filename.includes('.')) {
                if (url.includes('.mp4')) filename += '.mp4';
                else if (url.includes('.jpg') || url.includes('.jpeg')) filename += '.jpg';
                else if (url.includes('.png')) filename += '.png';
                else if (url.includes('.webm')) filename += '.webm';
                else if (url.includes('.gif')) filename += '.gif';
            }
            return filename;
        } catch(e) {
            return 'download';
        }
    }

    function addDownloadLink(media) {
        const src = getMediaSrc(media);
        if (!src) return;

        let container = media.closest('.media-group') || media.parentElement;
        if (!container) return;

        if (container.querySelector('.erome-dl-link')) return;

        const link = document.createElement('a');
        link.href = src;
        link.download = getFileNameFromUrl(src);
        link.textContent = `⬇ Download ${media.tagName === 'IMG' ? 'Image' : 'Video'}`;
        link.className = 'erome-dl-link';
        link.target = '_blank';
        link.referrerPolicy = 'unsafe-url';
        link.style.display = 'inline-block';
        link.style.margin = '5px 0';
        link.style.fontSize = '12px';
        link.style.backgroundColor = '#e6f2ff';
        link.style.padding = '4px 8px';
        link.style.borderRadius = '4px';
        link.style.textDecoration = 'none';
        link.style.color = '#0055cc';
        link.style.border = '1px solid #99ccff';
        link.style.cursor = 'pointer';

        container.appendChild(document.createElement('br'));
        container.appendChild(link);
    }

    function processAllMedia() {
        document.querySelectorAll('.media-group .video video, .media-group .img img').forEach(addDownloadLink);
    }

    function observeNewMedia() {
        const observer = new MutationObserver(mutations => {
            for (const mutation of mutations) {
                if (mutation.addedNodes.length) {
                    for (const node of mutation.addedNodes) {
                        if (node.nodeType === Node.ELEMENT_NODE) {
                            if (node.matches && node.matches('.media-group .video video, .media-group .img img')) {
                                addDownloadLink(node);
                            } else if (node.querySelector) {
                                node.querySelectorAll('.media-group .video video, .media-group .img img').forEach(addDownloadLink);
                            }
                        }
                    }
                }
            }
        });
        observer.observe(document.body, { childList: true, subtree: true });
    }

    // ========== Erome Enhancer (alpha) ==========
    // Constants
    const STORAGE_KEY = 'eromeEnhancerSettings';
    const VIEWED_KEY = 'eromeViewedAlbums';
    const DEFAULTS = {
        filterMode: 'videos',
        autoScroll: true,
        hideViewed: false,
        minVideoSeconds: 0,
        showLikes: true,
        enableSorting: true,
    };

    const SELECTORS = {
        albums: '#albums',
        albumLink: '.album-link, a[href*="/a/"]',
        albumThumbnail: '.album-thumbnail-container',
        albumBottomRight: '.album-bottom-right',
        albumVideos: '.album-videos',
        albumImages: '.album-images',
        albumViews: '.album-bottom-views',
        tabs: '#tabs',
        page: '#page',
    };

    // State
    let settings = loadSettings();
    let viewedAlbums = loadViewed();
    let currentPage = 1;
    let loading = false;
    const MAX_PAGES = 100;
    let lastSort = null;
    let pendingFetches = 0;
    let processingQueue = false;

    // Inject CSS once
    const CSS = `
        .ee-duration-badge, .ee-watched-badge, .ee-deleted-overlay {
            position: absolute;
            pointer-events: none;
        }
        .ee-duration-badge {
            top: 8px;
            right: 8px;
            background: rgba(0, 0, 0, 0.85);
            color: white;
            padding: 4px 8px;
            border-radius: 4px;
            font-weight: 600;
            z-index: 12;
            box-shadow: 0 2px 6px rgba(0,0,0,0.4);
            line-height: 1.2;
            font-size: 11px;
        }
        .ee-watched-overlay {
            top: 0; left: 0; right: 0; bottom: 0;
            width: 100%; height: 100%;
            background: rgba(0, 0, 0, 0.5);
            z-index: 10;
        }
        .ee-watched-badge {
            top: 8px;
            left: 8px;
            background: rgba(235, 99, 149, 0.9);
            color: white;
            padding: 4px 8px;
            border-radius: 4px;
            font-size: 11px;
            font-weight: 700;
            letter-spacing: 0.5px;
            z-index: 11;
            box-shadow: 0 2px 4px rgba(0,0,0,0.3);
        }
        .ee-deleted-overlay {
            top: 0; left: 0; right: 0; bottom: 0;
            width: 100%; height: 100%;
            background: rgba(0, 0, 0, 0.7);
            z-index: 15;
            display: flex;
            align-items: center;
            justify-content: center;
            transition: opacity 0.3s ease;
        }
        .ee-page-separator {
            display: flex;
            align-items: center;
            justify-content: center;
            margin: 30px 0;
            height: 40px;
            width: 100%;
            clear: both;
            grid-column: 1 / -1;
        }
        #eromeSortControls {
            display: grid;
            grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
            gap: 8px;
            margin: 16px 0;
            max-width: 600px;
        }
        #eromeSortControls button {
            padding: 8px 12px;
            font-size: 14px;
            font-weight: 600;
            border: 1px solid #d0d0d0;
            border-radius: 6px;
            cursor: pointer;
            background: #fff;
            color: #333;
            transition: all 0.2s ease;
            white-space: nowrap;
            min-width: 120px;
            text-align: center;
        }
        #eromeSortControls button:hover {
            background: #f6f6f6;
            transform: translateY(-1px);
            box-shadow: 0 2px 4px rgba(0,0,0,0.1);
        }
        @keyframes spin { to { transform: rotate(360deg); } }
        .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; 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); }
        .ee-action-buttons {
            display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
            gap: 10px; margin-top: 20px; padding-top: 15px; border-top: 1px solid #444;
        }
        .ee-action-btn { width: 100%; white-space: nowrap; }
        #clearViewed { background: #555 !important; border-color: #666 !important; color: #ccc !important; }
        #clearViewed:hover { background: #666 !important; border-color: #777 !important; }
        #resetDurationFilter { border: 1px solid #eb6395 !important; color: #eb6395 !important; background: transparent !important; }
        #resetDurationFilter:hover { background: rgba(235, 99, 149, 0.15) !important; box-shadow: 0 4px 12px rgba(235, 99, 149, 0.2) !important; }
        #saveEnhancer:hover { background: #d85585 !important; border-color: #d85585 !important; box-shadow: 0 6px 16px rgba(235, 99, 149, 0.4) !important; }
        .modal-content { border-radius: 4px; 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%); }
    `;

    const styleEl = document.createElement('style');
    styleEl.textContent = CSS;
    document.head.appendChild(styleEl);

    /* 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;
        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');
        });
    }

    function parseAbbrevNumber(text) {
        if (!text) return 0;
        const t = text.replace(/\s+/g, ' ').trim();
        const m = t.match(/(\d[\d.,]*)(\s*[KM])?/i);
        if (!m) return 0;
        const num = parseFloat(m[1].replace(/,/g, '').replace(/\.(?=.*\.)/g, ''));
        if (!isFinite(num)) return 0;
        const unit = (m[2] || '').trim().toUpperCase();
        if (unit === 'K') return num * 1000;
        if (unit === 'M') return num * 1000000;
        return num;
    }

    function formatDuration(seconds) {
        const hrs = Math.floor(seconds / 3600);
        const mins = Math.floor((seconds % 3600) / 60);
        const secs = seconds % 60;
        if (hrs > 0) return `${hrs}:${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
        return `${mins}:${secs.toString().padStart(2, '0')}`;
    }

    /* Extraction Functions */
    function extractMetric(album, metricName, extractFn) {
        const cacheKey = `_${metricName}Parsed`;
        if (album.dataset[cacheKey]) return +album.dataset[cacheKey];
        const value = extractFn(album);
        album.dataset[cacheKey] = String(value);
        return value;
    }

    function extractViews(album) {
        return extractMetric(album, 'views', (a) => {
            const viewsEl = a.querySelector(SELECTORS.albumViews);
            if (viewsEl) {
                const text = viewsEl.textContent.replace(/\s*views?\s*/i, '').trim();
                const v = parseAbbrevNumber(text);
                if (v) return v;
            }
            const dataEl = a.querySelector('[data-views],[data-view],[data-count-views]');
            if (dataEl) {
                const val = dataEl.getAttribute('data-views') || dataEl.getAttribute('data-view') || dataEl.getAttribute('data-count-views');
                return parseAbbrevNumber(val);
            }
            const icon = a.querySelector('.fa-eye:not(.fa-eye-slash)');
            if (icon?.parentElement?.classList.contains('album-bottom-views')) {
                return parseAbbrevNumber(icon.parentElement.textContent.replace(/\s*views?\s*/i, '').trim());
            }
            return 0;
        });
    }

    function extractVideos(album) {
        return extractMetric(album, 'videos', (a) => {
            const videoSpan = a.querySelector(SELECTORS.albumVideos);
            if (videoSpan) {
                const clone = videoSpan.cloneNode(true);
                clone.querySelectorAll('.fa-heart, .pink').forEach(el => {
                    if (el.classList.contains('fa-heart')) {
                        el.previousElementSibling?.remove();
                        el.remove();
                    }
                });
                const match = clone.textContent.trim().match(/(\d+)/);
                if (match) return parseInt(match[1]);
            }
            const dataEl = a.querySelector('[data-videos],[data-video-count],[data-count-videos]');
            if (dataEl) {
                const val = dataEl.getAttribute('data-videos') || dataEl.getAttribute('data-video-count') || dataEl.getAttribute('data-count-videos');
                return parseAbbrevNumber(val);
            }
            const icon = a.querySelector('.fa-video,.fa-film');
            if (icon?.parentElement?.classList.contains('album-videos')) {
                const match = icon.parentElement.textContent.trim().match(/(\d+)/);
                if (match) return parseInt(match[1]);
            }
            return 0;
        });
    }

    function extractDuration(album) {
        return extractMetric(album, 'duration', (a) => {
            const totalDuration = a.dataset.totalVideoDuration;
            return totalDuration ? parseInt(totalDuration) : 0;
        });
    }

    /* Album Sorting */
    function tagOriginalOrder(albums) {
        albums.forEach((a, i) => {
            if (!a.hasAttribute('data-original-index')) {
                a.setAttribute('data-original-index', String(i));
            }
        });
    }

    function sortOrResetAlbums(sortFn = null) {
        const container = document.querySelector(SELECTORS.albums);
        if (!container) return;

        const allChildren = Array.from(container.children);
        const albums = [];
        const separatorMap = new Map();

        allChildren.forEach((child, index) => {
            if (child.classList.contains('ee-page-separator')) {
                separatorMap.set(index, child);
            } else if (child.classList.contains('album')) {
                albums.push(child);
            }
        });

        if (!albums.length) return;
        tagOriginalOrder(albums);

        const sorted = sortFn ? [...albums].sort((a, b) => {
            const kb = sortFn(b), ka = sortFn(a);
            if (kb !== ka) return kb - ka;
            return (parseInt(a.getAttribute('data-original-index')) || 0) - (parseInt(b.getAttribute('data-original-index')) || 0);
        }) : [...albums].sort((a, b) =>
            (parseInt(a.getAttribute('data-original-index')) || 0) - (parseInt(b.getAttribute('data-original-index')) || 0)
        );

        container.innerHTML = '';
        let albumIndex = 0;
        for (let i = 0; i < allChildren.length; i++) {
            if (separatorMap.has(i)) {
                container.appendChild(separatorMap.get(i));
            } else if (albumIndex < sorted.length) {
                container.appendChild(sorted[albumIndex++]);
            }
        }
    }

    function sortAlbums(keyFnDesc) { sortOrResetAlbums(keyFnDesc); }
    function resetAlbums() { sortOrResetAlbums(); }

    function addSortingControls() {
        if (!settings.enableSorting || location.pathname.startsWith('/a/')) return;
        const tabsContainer = document.querySelector(SELECTORS.tabs);
        if (!tabsContainer || document.getElementById('eromeSortControls')) return;

        const bar = document.createElement('div');
        bar.id = 'eromeSortControls';

        const buttons = [
            ['↓ Views', 'eromeSortByViews', () => { lastSort = 'views'; sortAlbums(extractViews); }],
            ['↓ Videos', 'eromeSortByVideos', () => { lastSort = 'videos'; sortAlbums(extractVideos); }],
            ['↓ Duration', 'eromeSortByDuration', () => { lastSort = 'duration'; sortAlbums(extractDuration); }],
            ['↺ Reset', 'eromeSortReset', () => { lastSort = null; resetAlbums(); }],
        ];

        buttons.forEach(([text, id, handler]) => {
            const btn = document.createElement('button');
            btn.id = id;
            btn.textContent = text;
            btn.addEventListener('click', (e) => { e.preventDefault(); handler(); });
            bar.appendChild(btn);
        });

        tabsContainer.parentElement.insertBefore(bar, tabsContainer.nextSibling);
    }

    /* Like Count & Duration Display */
    async function addLikeCount(albumEl) {
        if (albumEl.dataset.likesProcessed || location.pathname.startsWith('/a/') || !settings.showLikes) return;
        const link = albumEl.querySelector(SELECTORS.albumLink);
        if (!link) return;

        albumEl.dataset.likesProcessed = 'true';
        pendingFetches++;
        const albumIndex = pendingFetches;
        updateLoadingCount(pendingFetches);

        try {
            const response = await fetchWithRetry(link.href, albumIndex);
            const html = await response.text();
            const doc = new DOMParser().parseFromString(html, 'text/html');

            if (doc.querySelector('h1')?.textContent.includes('Album deleted')) {
                markAsDeleted(albumEl);
                return;
            }

            let count = 0;
            try {
                const likeCountEl = doc.querySelector('#like_count');
                if (likeCountEl?.textContent) {
                    count = likeCountEl.textContent.trim();
                } else {
                    const heartIcon = doc.querySelector('.far.fa-heart.fa-lg');
                    if (heartIcon?.nextElementSibling?.firstChild) {
                        count = heartIcon.nextElementSibling.firstChild.textContent.trim();
                    }
                }
                count = parseInt(count) || 0;
            } catch (likeErr) {
                count = 0;
            }

            const videoDurations = [];
            doc.querySelectorAll('.duration').forEach(durEl => {
                const seconds = parseDurationText(durEl.textContent.trim());
                if (seconds > 0) videoDurations.push(seconds);
            });

            if (videoDurations.length > 0) {
                const totalSeconds = videoDurations.reduce((sum, dur) => sum + dur, 0);
                const avgSeconds = Math.round(totalSeconds / videoDurations.length);
                albumEl.dataset.videoDurations = JSON.stringify(videoDurations);
                albumEl.dataset.avgVideoDuration = avgSeconds;
                albumEl.dataset.totalVideoDuration = totalSeconds;

                addDurationDisplay(albumEl, totalSeconds, avgSeconds, videoDurations.length);

                if (settings.minVideoSeconds > 0 && avgSeconds < settings.minVideoSeconds) {
                    albumEl.remove();
                    return;
                }
            }

            if (count > 0) {
                const bottomRight = albumEl.querySelector(SELECTORS.albumBottomRight);
                if (bottomRight) {
                    const likeDisplay = document.createElement('span');
                    likeDisplay.className = 'album-likes-display';
                    likeDisplay.style.cssText = 'position:relative;display:inline-block;margin-left:4px;';
                    likeDisplay.innerHTML = `<i class="fas fa-heart fa-lg" style="color:#eb6395;margin-right:4px;"></i><span style="font-weight:600;">${count}</span>`;
                    bottomRight.appendChild(likeDisplay);
                }
            }
        } catch (err) {
            if (err.message === 'ALBUM_DELETED') markAsDeleted(albumEl);
        } finally {
            pendingFetches--;
            updateLoadingCount(pendingFetches);
            if (pendingFetches === 0) {
                hideLoadingIndicator();
                processingQueue = false;
            }
        }
    }

    function addDurationDisplay(albumEl, totalSeconds, avgSeconds, videoCount) {
        const container = albumEl.querySelector(SELECTORS.albumThumbnail);
        if (!container || container.querySelector('.ee-duration-badge')) return;
        if (!container.style.position || container.style.position === 'static') {
            container.style.position = 'relative';
        }
        const badge = document.createElement('div');
        badge.className = 'ee-duration-badge';
        badge.title = `${videoCount} video${videoCount > 1 ? 's' : ''}\nTotal: ${formatDuration(totalSeconds)}\nAverage: ${formatDuration(avgSeconds)}`;
        badge.innerHTML = `<div style="opacity: 0.9;"><i class="fa fa-clock" style="margin-right: 3px;"></i>${formatDuration(totalSeconds)}</div>${videoCount > 1 ? `<div style="font-size: 9px; opacity: 0.7; margin-top: 2px;">${videoCount} videos</div>` : ''}`;
        container.appendChild(badge);
    }

    function processLikesForAlbums(container = document) {
        if (location.pathname.startsWith('/a/') || !settings.showLikes) return;
        const albums = container.querySelectorAll('.album');
        if (albums.length === 0) return;

        showLoadingIndicator();
        processingQueue = true;
        albums.forEach((album, index) => {
            if (!album.dataset.likesProcessed) {
                setTimeout(() => addLikeCount(album), index * 100);
            }
        });
    }

    /* Rate Limit Handling */
    async function fetchWithRetry(url, albumIndex, maxRetries = 5, initialDelay = 2000) {
        for (let attempt = 0; attempt < maxRetries; attempt++) {
            try {
                const response = await fetch(url);
                if (response.status === 404 || response.status === 410) throw new Error('ALBUM_DELETED');
                if (response.status === 429) {
                    await new Promise(resolve => setTimeout(resolve, initialDelay * Math.pow(2, attempt)));
                    continue;
                }
                if (!response.ok) throw new Error(`HTTP ${response.status}: ${response.statusText}`);
                return response;
            } catch (err) {
                if (err.message === 'ALBUM_DELETED' || attempt === maxRetries - 1) throw err;
                await new Promise(resolve => setTimeout(resolve, initialDelay * Math.pow(2, attempt)));
            }
        }
    }

    function showLoadingIndicator() {
        if (document.getElementById('ee-loading-indicator')) return;
        const indicator = document.createElement('div');
        indicator.id = 'ee-loading-indicator';
        indicator.innerHTML = `<div style="position: fixed;bottom: 20px;right: 20px;background: rgba(235, 99, 149, 0.95);color: white;padding: 12px 20px;border-radius: 8px;box-shadow: 0 4px 12px rgba(0,0,0,0.3);z-index: 9999;display: flex;align-items: center;gap: 12px;font-weight: 600;font-size: 14px;"><div style="width: 20px;height: 20px;border: 3px solid rgba(255,255,255,0.3);border-top-color: white;border-radius: 50%;animation: spin 0.8s linear infinite;"></div><span>Loading album data...</span><span id="ee-loading-count" style="background: rgba(255,255,255,0.2);padding: 2px 8px;border-radius: 4px;font-size: 12px;">0</span></div>`;
        document.body.appendChild(indicator);
    }

    function updateLoadingCount(count) {
        const countEl = document.getElementById('ee-loading-count');
        if (countEl) countEl.textContent = count;
    }

    function hideLoadingIndicator() {
        document.getElementById('ee-loading-indicator')?.remove();
    }

    /* Overlay Creation */
    function createOverlay(type, albumEl) {
        const container = albumEl.querySelector(SELECTORS.albumThumbnail);
        if (!container || container.querySelector(`.ee-${type}-overlay`)) return;
        if (!container.style.position || container.style.position === 'static') {
            container.style.position = 'relative';
        }

        const overlay = document.createElement('div');
        overlay.className = `ee-${type}-overlay`;

        if (type === 'watched') {
            const badge = document.createElement('div');
            badge.className = 'ee-watched-badge';
            badge.textContent = 'WATCHED';
            container.appendChild(overlay);
            container.appendChild(badge);
        } else if (type === 'deleted') {
            const link = container.querySelector('a');
            if (link) {
                link.addEventListener('click', (e) => e.preventDefault());
                link.style.cursor = 'default';
            }
            const message = document.createElement('div');
            message.style.cssText = 'background: rgba(235, 99, 149, 0.95);color: white;padding: 12px 20px;border-radius: 8px;font-size: 14px;font-weight: 700;letter-spacing: 0.5px;box-shadow: 0 4px 12px rgba(0,0,0,0.5);text-align: center;line-height: 1.4;';
            message.innerHTML = `<i class="fa fa-trash" style="display: block; font-size: 24px; margin-bottom: 8px;"></i>ALBUM DELETED`;
            overlay.appendChild(message);
            container.appendChild(overlay);
            container.addEventListener('mouseenter', () => overlay.style.opacity = '0');
            container.addEventListener('mouseleave', () => overlay.style.opacity = '1');
            const thumbnail = container.querySelector('img');
            if (thumbnail) thumbnail.style.filter = 'grayscale(50%) brightness(0.7)';
        }
    }

    function markAsViewed(albumEl) { createOverlay('watched', albumEl); }
    function markAsDeleted(albumEl) { createOverlay('deleted', albumEl); }

    /* Grid Filtering */
    function matchesFilter(albumEl) {
        const videoSpan = albumEl.querySelector(SELECTORS.albumVideos);
        const imageSpan = albumEl.querySelector(SELECTORS.albumImages);
        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?.href;

        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';

        if (viewedAlbums.includes(link.href)) markAsViewed(albumEl);

        link.addEventListener('mousedown', (event) => {
            if (event.button === 0 || event.button === 1) {
                if (!viewedAlbums.includes(link.href)) {
                    viewedAlbums.push(link.href);
                    saveViewed();
                    markAsViewed(albumEl);
                }
            }
        });
    }

    function applyInitialFilter() {
        const container = document.querySelector(SELECTORS.albums);
        if (!container) return;
        const albums = Array.from(container.querySelectorAll('.album'));
        tagOriginalOrder(albums);
        albums.forEach(album => {
            if (!matchesFilter(album)) {
                album.remove();
            } else {
                fixLazyImages(album);
                markAlbumClick(album);
            }
        });
    }

    /* Infinite Scroll */
    function createPageSeparator(pageNum) {
        const separator = document.createElement('div');
        separator.className = 'ee-page-separator';
        separator.dataset.pageNumber = pageNum;
        separator.innerHTML = `<div style="flex: 1;height: 2px;background: linear-gradient(to right, transparent, #444, #444, transparent);"></div><div style="padding: 8px 20px;background: #2b2b2b;border: 2px solid #444;border-radius: 20px;margin: 0 15px;font-weight: 600;color: #eb6395;font-size: 14px;white-space: nowrap;"><i class="fa fa-arrow-down" style="margin-right: 8px;"></i>Page ${pageNum}<i class="fa fa-arrow-down" style="margin-left: 8px;"></i></div><div style="flex: 1;height: 2px;background: linear-gradient(to left, transparent, #444, #444, transparent);"></div>`;
        return separator;
    }

    function setupInfiniteScroll() {
        disableNativeInfiniteScroll();
        let scrollLocked = false;
        window.addEventListener('scroll', () => {
            if (!settings.autoScroll || scrollLocked || processingQueue || pendingFetches > 0) return;
            if (window.innerHeight + window.scrollY >= document.body.scrollHeight - 500) {
                scrollLocked = true;
                loadNextPage().finally(() => setTimeout(() => scrollLocked = false, 1000));
            }
        });
    }

    function disableNativeInfiniteScroll() {
        if (typeof $ === 'function') {
            const $page = $(SELECTORS.page);
            try {
                if ($page.data('infiniteScroll')) $page.infiniteScroll('destroy');
            } catch (e) {}
            $page.off('append.infiniteScroll load.infiniteScroll request.infiniteScroll last.infiniteScroll error.infiniteScroll append');
        }
        if (typeof window.InfiniteScroll !== 'undefined') {
            window.InfiniteScroll = function() {
                return { destroy: () => {}, on: () => {}, off: () => {}, loadNextPage: () => {} };
            };
        }
        if (typeof $ === 'function' && $.fn) {
            $.fn.infiniteScroll = function() { return this; };
        }
        setTimeout(() => {
            const infiniteScrollScript = Array.from(document.querySelectorAll('script')).find(script =>
                script.textContent.includes('infiniteScroll') || script.textContent.includes('infinite-scroll') || script.textContent.includes('.infiniteScroll(')
            );
            if (infiniteScrollScript) infiniteScrollScript.remove();
        }, 100);
    }

    async function loadNextPage() {
        if (loading || currentPage >= MAX_PAGES || processingQueue || pendingFetches > 0) 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 fetchWithRetry(url, `Page${nextPage}`);
            const html = await res.text();
            const doc = new DOMParser().parseFromString(html, 'text/html');
            fixLazyImages(doc);
            doc.querySelectorAll('.suggested-users, .col-sm-12:has(h2)').forEach(el => el.remove());

            const newAlbums = doc.querySelectorAll('.album');
            const container = document.querySelector(SELECTORS.albums);

            if (container && newAlbums.length > 0) {
                const frag = document.createDocumentFragment();
                frag.appendChild(createPageSeparator(nextPage));

                let addedCount = 0;
                newAlbums.forEach(n => {
                    const clone = document.importNode(n, true);
                    if (matchesFilter(clone)) {
                        fixLazyImages(clone);
                        clone.setAttribute('data-original-index', String(container.querySelectorAll('.album').length));
                        frag.appendChild(clone);
                        addedCount++;
                    }
                });

                container.appendChild(frag);

                setTimeout(() => {
                    document.querySelectorAll('.separator .bubble-mobile').forEach(bubble => {
                        if (bubble.href?.includes('/o/')) bubble.closest('.separator')?.remove();
                    });
                    document.querySelectorAll('.suggested-users').forEach(el => el.remove());
                }, 100);

                const addedAlbums = Array.from(container.querySelectorAll('.album')).slice(-addedCount);
                addedAlbums.forEach(album => markAlbumClick(album));

                showLoadingIndicator();
                processingQueue = true;
                addedAlbums.forEach((album, index) => setTimeout(() => addLikeCount(album), index * 100));

                return new Promise((resolve) => {
                    const waitForProcessing = setInterval(() => {
                        if (!processingQueue && pendingFetches === 0) {
                            clearInterval(waitForProcessing);
                            if (lastSort === 'views') sortAlbums(extractViews);
                            else if (lastSort === 'videos') sortAlbums(extractVideos);
                            else if (lastSort === 'duration') sortAlbums(extractDuration);
                            currentPage = nextPage;
                            loading = false;
                            resolve();
                        }
                    }, 500);
                });
            } else {
                currentPage = nextPage;
                loading = false;
            }
        } catch (err) {
            loading = false;
        }
    }

    /* Album Pages */
    function getMediaGroups() {
        return Array.from(document.querySelectorAll('.media-group, .album-media, [class*="media"]'));
    }

    function isVideoGroup(g) {
        return !!(g.querySelector('.duration, video, [class*="video"], .fa-video'));
    }

    function getGroupDurationSeconds(g) {
        let durationText = '';
        const durationEl = g.querySelector('.duration');
        if (durationEl) durationText = durationEl.textContent || durationEl.innerText || '';
        if (!durationText && g.dataset.duration) durationText = g.dataset.duration;
        if (!durationText && g.getAttribute('data-duration')) durationText = g.getAttribute('data-duration');
        if (!durationText) {
            const durationMatch = g.innerHTML.match(/(\d{1,2}):(\d{2})(?::(\d{2}))?/);
            if (durationMatch) durationText = durationMatch[0];
        }
        return parseDurationText(durationText.trim());
    }

    function updateHiddenCounter(n) {
        const num = document.getElementById('eeCountNum');
        if (num) num.textContent = String(n);
    }

    function applyAlbumEnhancements() {
        if (!location.pathname.startsWith('/a/')) return;
        const groups = getMediaGroups();
        if (!groups.length) {
            updateHiddenCounter(0);
            return;
        }
        let hidden = 0;
        groups.forEach(g => {
            if (isVideoGroup(g)) {
                const secs = getGroupDurationSeconds(g);
                if (settings.minVideoSeconds > 0 && secs > 0 && secs < settings.minVideoSeconds) {
                    g.style.display = 'none';
                    hidden++;
                } else {
                    g.style.display = '';
                }
            } else {
                g.style.display = '';
            }
        });
        updateHiddenCounter(hidden);
    }

    function observeAlbumChanges() {
        if (!location.pathname.startsWith('/a/')) return;
        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 navContainer = document.querySelector('.navbar .container .col-sm-12');
        if (!navContainer) return null;
        if (document.getElementById('enhancerNavItem')) return document.getElementById('enhancerBtn');
        const div = document.createElement('div');
        div.className = 'sp';
        div.id = 'enhancerNavItem';
        div.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>`;
        const orientationDropdown = navContainer.querySelector('.sp.dropdown.sign-in');
        if (orientationDropdown) {
            orientationDropdown.insertAdjacentElement('afterend', div);
        } else {
            navContainer.insertBefore(div, navContainer.querySelector('.navbar-header'));
        }
        return document.getElementById('enhancerBtn');
    }

    function addSettingsUI() {
        const anchor = ensureEnhancerNav();
        if (!anchor || document.getElementById('enhancerModal')) return;
        const modal = document.createElement('div');
        modal.className = 'modal';
        modal.id = 'enhancerModal';
        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;"><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 class="form-group"><div class="checkbox"><label><input type="checkbox" id="showLikes"> Show like counts on albums</label></div></div><div class="form-group"><div class="checkbox"><label><input type="checkbox" id="enableSorting"> Enable album sorting controls</label></div></div></div></div><hr style="border-color:#444;margin:25px 0;"><div class="settings-section"><div class="section-header"><i class="fa fa-clock-o" style="margin-right:8px;"></i>Video Duration Filter</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-filter" style="margin-right:6px;"></i>Minimum Average 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>Hide albums where the average video duration is shorter than this</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>Applies to both grid pages and individual album pages</span></div></div><div class="ee-action-buttons"><button id="clearViewed" class="btn btn-default ee-action-btn"><i class="fa fa-trash" style="margin-right:6px;"></i>Clear Viewed</button><button id="resetDurationFilter" class="btn btn-default ee-action-btn"><i class="fa fa-refresh" style="margin-right:6px;"></i>Reset Duration</button></div></div></div></div><div class="modal-footer" style="border-top:1px solid #444;padding:20px 25px;"><button id="saveEnhancer" class="btn btn-primary" style="background:#eb6395 !important;border-color:#eb6395 !important;color:#fff !important;font-weight:600;padding:10px 20px;width:100%;"><i class="fa fa-check" style="margin-right:8px;"></i>Apply Settings</button></div></div></div>`;
        document.body.appendChild(modal);

        anchor.addEventListener('click', e => {
            e.preventDefault();
            document.getElementById('filterMode').value = settings.filterMode;
            document.getElementById('autoScroll').checked = settings.autoScroll;
            document.getElementById('hideViewed').checked = settings.hideViewed;
            document.getElementById('showLikes').checked = settings.showLikes;
            document.getElementById('enableSorting').checked = settings.enableSorting;
            document.getElementById('minVideoSeconds').value = settings.minVideoSeconds || 0;
            if (typeof $ === 'function') {
                $('#enhancerModal').modal({ show: true, backdrop: true });
            } else {
                modal.style.display = 'block';
                modal.classList.add('show');
            }
        });

        modal.querySelector('#saveEnhancer').addEventListener('click', () => {
            settings.filterMode = document.getElementById('filterMode').value;
            settings.autoScroll = document.getElementById('autoScroll').checked;
            settings.hideViewed = document.getElementById('hideViewed').checked;
            settings.showLikes = document.getElementById('showLikes').checked;
            settings.enableSorting = document.getElementById('enableSorting').checked;
            settings.minVideoSeconds = parseInt(document.getElementById('minVideoSeconds').value) || 0;
            saveSettings();
            if (typeof $ === 'function') {
                $('#enhancerModal').modal('hide');
            } else {
                modal.style.display = 'none';
                modal.classList.remove('show');
            }
            setTimeout(() => {
                location.pathname.startsWith('/a/') ? applyAlbumEnhancements() : location.reload();
            }, 300);
        });

        modal.querySelector('#clearViewed').addEventListener('click', clearViewed);
        modal.querySelector('#resetDurationFilter').addEventListener('click', () => {
            settings.minVideoSeconds = 0;
            saveSettings();
            document.getElementById('minVideoSeconds').value = 0;
            location.pathname.startsWith('/a/') ? applyAlbumEnhancements() : location.reload();
        });

        modal.addEventListener('click', (e) => {
            if (e.target === modal) {
                if (typeof $ === 'function') {
                    $('#enhancerModal').modal('hide');
                } else {
                    modal.style.display = 'none';
                    modal.classList.remove('show');
                }
            }
        });
    }

    /* Cleanup */
    function disableDisclaimer() {
        const disclaimer = document.getElementById('disclaimer');
        if (!disclaimer) return;
        if (typeof $ === 'function') {
            $.ajax({ type: 'POST', url: '/user/disclaimer', async: true });
            $('#disclaimer').remove();
            $('body').css('overflow', 'visible');
        } else {
            fetch('/user/disclaimer', { method: 'POST' }).catch(() => {});
            disclaimer.remove();
            document.body.style.overflow = 'visible';
        }
    }

    function cleanNavbar() {
        const navContainer = document.querySelector('.navbar .container .col-sm-12');
        if (navContainer) {
            navContainer.querySelectorAll('.sp').forEach(div => {
                const link = div.querySelector('a');
                if (link?.href?.includes('/o/menu-')) div.remove();
            });
        }
        document.querySelector('.sp-mob.hidden-sm.hidden-md.hidden-lg')?.remove();
        document.querySelectorAll('.separator .bubble-mobile').forEach(bubble => {
            if (bubble.href?.includes('/o/')) bubble.closest('.separator')?.remove();
        });
        const navbar = document.querySelector('.navbar.navbar-inverse.navbar-static-top');
        if (navbar && !document.getElementById('ee-navbar-fix')) {
            const style = document.createElement('style');
            style.id = 'ee-navbar-fix';
            style.textContent = `.navbar.navbar-inverse.navbar-static-top { top: 0 !important; }`;
            document.head.appendChild(style);
        }
    }

    /* Enhancer Init */
    function init() {
        disableDisclaimer();
        cleanNavbar();
        fixLazyImages();

        const albumsContainer = document.querySelector(SELECTORS.albums);
        if (albumsContainer && !location.pathname.startsWith('/a/')) {
            albumsContainer.style.minHeight = '600px';
        }

        if (location.pathname.startsWith('/a/')) {
            setTimeout(() => {
                applyAlbumEnhancements();
                observeAlbumChanges();
            }, 2000);
        } else {
            applyInitialFilter();
            addSortingControls();
            setTimeout(() => setupInfiniteScroll(), 1000);
            setTimeout(() => processLikesForAlbums(), 500);
        }
        addSettingsUI();
    }

    // ========== Unified Startup ==========
    // Run age warning skipper immediately (it retries if needed)
    skipWarn();

    // Start downloader and enhancer when DOM is ready
    function startAll() {
        processAllMedia();
        observeNewMedia();
        init();
    }

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', startAll);
    } else {
        startAll();
    }
})();