ArabP2P Scroll

ArabP2P Infinite Scroll Extension By Hamza - Supports Listings & Torrents

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

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

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

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

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

You will need to install a user script manager extension to install this script.

(Tôi đã có Trình quản lý tập lệnh người dùng, hãy cài đặt nó!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         ArabP2P Scroll
// @namespace    https://arabp2p.net/
// @version      1.3.1
// @description  ArabP2P Infinite Scroll Extension By Hamza - Supports Listings & Torrents
// @author       Hamza
// @match        https://www.arabp2p.net/index.php*
// @match        https://arabp2p.net/index.php*
// @icon         https://www.arabp2p.net/favicon.ico
// @grant        GM_addStyle
// @run-at       document-end
// ==/UserScript==

(function() {
    'use strict';

    console.log("ArabP2P Enhancer: Script loaded! (Userscript v1.3.1 - Fast Scroll Fix)");

    // --- Inject CSS ---
    GM_addStyle(`
        :root {
            --poster-width: 155px;
            --gap-size: 20px;
            --control-bar-bg: #f8f9fa;
            --resume-bg: #28a745;
            --resume-hover-bg: #218838;
            --stop-bg: #dc3545;
            --stop-hover-bg: #c82333;
            --disabled-bg: #e9ecef;
            --text-color: #212529;
            --border-color: #dee2e6;
            --poster-hover-border: #888;
            --poster-active-border: #007bff;
        }

        /* === Listing Pages Styles === */
        .enhancer-active .listing_div1,
        .enhancer-active form[name="change_pagepages"],
        .enhancer-active form[name="change_page1pages"] {
            display: none !important;
        }

        .enhancer-active .listing_div {
            min-height: 0 !important; height: auto !important;
            padding: 0 20px 20px 20px !important; margin: 0 !important;
            overflow: hidden !important; display: block !important;
            box-sizing: border-box !important;
        }

        #custom-poster-grid {
            display: grid;
            grid-template-columns: repeat(auto-fill, minmax(var(--poster-width), 1fr));
            gap: var(--gap-size);
            width: 100%;
        }

        .poster-card {
            display: block;
            border-radius: 6px;
            overflow: hidden;
            background-color: #fff;
            box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1), 0 0 0 1px transparent;
            transition: box-shadow 0.2s ease;
        }

        .poster-card:hover {
            box-shadow: 0 3px 7px rgba(0, 0, 0, 0.15), 0 0 0 1px var(--poster-hover-border);
        }

        .poster-card:active {
            box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1), 0 0 0 1px var(--poster-active-border);
            transition-duration: 0.05s;
        }

        .poster-card img {
            width: 100%; height: auto; display: block;
            aspect-ratio: 2 / 3; object-fit: cover;
        }

        .poster-card-title {
            display: block;
            padding: 8px 6px;
            font-size: 0.85em;
            font-weight: 500;
            color: #333;
            text-align: center;
            line-height: 1.3;
            overflow: hidden;
            text-overflow: ellipsis;
            white-space: nowrap;
            background: linear-gradient(to bottom, #fff, #f5f5f5);
            font-family: 'arabp2p', tahoma, arial, helvetica, sans-serif;
        }

        .poster-card:hover .poster-card-title {
            white-space: normal;
            word-wrap: break-word;
        }

        /* === Shared Control Styles === */
        #status-container {
            text-align: center; padding: 25px 0 10px 0;
            width: 100%; min-height: 2em;
            font-family: 'arabp2p', tahoma, arial, helvetica, sans-serif;
        }

        #status-container div {
            font-size: 1.1em; color: #6c757d; font-weight: 500;
        }

        #status-controls {
            display: flex; justify-content: center; align-items: center;
            gap: 15px; max-width: 550px;
            margin: 20px auto 30px auto;
            padding: 10px 15px;
            background-color: var(--control-bar-bg);
            border: 1px solid var(--border-color);
            border-radius: 8px;
            box-shadow: 0 4px 10px rgba(0, 0, 0, 0.06);
            box-sizing: border-box;
            font-family: 'arabp2p', tahoma, arial, helvetica, sans-serif;
        }

        #page-indicator {
            color: var(--text-color); font-size: 1.1em;
            font-weight: bold; text-align: center; flex-grow: 1;
        }

        .control-btn {
            color: white; border: none; padding: 8px 20px;
            border-radius: 6px; cursor: pointer; font-weight: bold;
            font-size: 1em; transition: all 0.2s ease-in-out;
            display: flex; align-items: center; gap: 8px;
            box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); font-family: inherit;
        }

        .control-btn:hover:not(:disabled) {
            transform: translateY(-2px);
            box-shadow: 0 3px 6px rgba(0, 0, 0, 0.15);
        }

        .control-btn:active:not(:disabled) {
            transform: translateY(0);
            box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
        }

        .control-btn:disabled {
            background-color: var(--disabled-bg);
            color: #6c757d; cursor: not-allowed;
            box-shadow: none; transform: none;
        }

        .control-btn.resume { background-color: var(--resume-bg); }
        .control-btn.resume:hover:not(:disabled) { background-color: var(--resume-hover-bg); }
        .control-btn.stop { background-color: var(--stop-bg); }
        .control-btn.stop:hover:not(:disabled) { background-color: var(--stop-hover-bg); }

        /* === Torrents Page Styles === */
        .enhancer-torrents-active form[name="change_pagepages"],
        .enhancer-torrents-active form[name="change_page1pages"] {
            display: none !important;
        }

        #torrents-status-container {
            text-align: center; padding: 25px 0 10px 0;
            width: 100%; min-height: 2em;
            font-family: 'arabp2p', tahoma, arial, helvetica, sans-serif;
        }

        #torrents-status-container div {
            font-size: 1.1em; color: #6c757d; font-weight: 500;
        }

        .infinite-scroll-separator {
            background-color: #17a2b8 !important;
            color: #fff !important;
            text-align: center;
            padding: 8px;
            font-weight: bold;
        }
    `);

    // --- Settings & Variables ---
    let currentPage = 1;
    let isLoading = false;
    let hasMore = true;
    let lastPageNumber = Infinity;
    const BASE_URL = window.location.origin;
    let isPaused = false;
    let gridContainer, statusContainer, pageIndicator, stopBtn, resumeBtn;
    let pageMode = null; // 'listing' or 'torrents'
    let prefetchedData = null; // For prefetching next page
    let observer = null; // IntersectionObserver
    const LOAD_DELAY = 300; // Reduced from 750ms

    // --- Functions ---
    function delay(ms) { return new Promise(resolve => setTimeout(resolve, ms)); }

    function showStatusMessage(text) {
        if (statusContainer) {
            statusContainer.innerHTML = text ? `<div>${text}</div>` : "";
        }
    }

    function updateControlsUI() {
        if (!pageIndicator) return;

        let statusText = `الصفحة الحالية: ${currentPage} / ${lastPageNumber === Infinity ? '??' : lastPageNumber}`;
        const isAtEnd = !hasMore || (lastPageNumber !== Infinity && currentPage >= lastPageNumber);

        if (isAtEnd) {
            statusText = `تم الوصول للنهاية عند الصفحة ${lastPageNumber}`;
        }
        pageIndicator.textContent = statusText;

        if (stopBtn && resumeBtn) {
            stopBtn.disabled = isPaused || isAtEnd;
            resumeBtn.disabled = !isPaused || isAtEnd;
        }
    }

    // === LISTING PAGES FUNCTIONS ===
    async function fetchListingData(pageNumber, pageParam, extraParams) {
        const url = `${BASE_URL}/index.php?page=${pageParam}${extraParams}&pages=${pageNumber}`;
        try {
            const response = await fetch(url, { headers: { 'X-Requested-With': 'XMLHttpRequest' } });
            if (!response.ok) throw new Error(`Server responded with status: ${response.status}`);

            const htmlText = await response.text();
            const doc = new DOMParser().parseFromString(htmlText, "text/html");
            const items = doc.querySelectorAll('.listing_div1');

            if (items.length === 0) return [];

            const scrapedData = [];
            items.forEach(item => {
                const linkTag = item.querySelector('a');
                const imgTag = item.querySelector('img.listing_poster');
                if (linkTag && imgTag) {
                    scrapedData.push({
                        name: imgTag.getAttribute('alt').replace(/<br>/g, ' ').trim(),
                        link: `${BASE_URL}/${linkTag.getAttribute('href')}`,
                        imageUrl: imgTag.getAttribute('src')
                    });
                }
            });
            return scrapedData;
        } catch (error) {
            console.error('Fetch error:', error);
            return [];
        }
    }

    async function fetchNextListingPage(pageNumber, pageParam, extraParams) {
        if (isPaused || (lastPageNumber !== Infinity && pageNumber > lastPageNumber)) {
            hasMore = false;
            showStatusMessage(`تم الوصول إلى نهاية القائمة عند الصفحة رقم ${lastPageNumber}`);
            updateControlsUI();
            return [];
        }

        isLoading = true;
        showStatusMessage("جاري التحميل...");

        let scrapedData;

        // Use prefetched data if available
        if (prefetchedData && prefetchedData.page === pageNumber) {
            scrapedData = prefetchedData.data;
            prefetchedData = null;
        } else {
            await delay(LOAD_DELAY);
            scrapedData = await fetchListingData(pageNumber, pageParam, extraParams);
        }

        if (scrapedData.length === 0) {
            hasMore = false;
            currentPage = pageNumber - 1;
            updateControlsUI();
            isLoading = false;
            return [];
        }

        isLoading = false;
        if (hasMore) showStatusMessage("");

        // Prefetch next page in background
        if (lastPageNumber === Infinity || pageNumber < lastPageNumber) {
            prefetchNextPage(pageNumber + 1, pageParam, extraParams);
        }

        return scrapedData;
    }

    // Prefetch next page in background
    async function prefetchNextPage(pageNumber, pageParam, extraParams) {
        if (lastPageNumber !== Infinity && pageNumber > lastPageNumber) return;
        const data = await fetchListingData(pageNumber, pageParam, extraParams);
        if (data.length > 0) {
            prefetchedData = { page: pageNumber, data: data };
            console.log(`Prefetched page ${pageNumber}`);
        }
    }

    function renderPosters(itemList, container) {
        // Batch DOM operations using DocumentFragment
        const fragment = document.createDocumentFragment();

        itemList.forEach(item => {
            const card = document.createElement('a');
            card.href = item.link;
            card.className = 'poster-card';
            card.title = item.name;
            card.target = '_blank';

            const img = document.createElement('img');
            img.src = item.imageUrl;
            img.alt = item.name;
            img.loading = 'lazy';

            const title = document.createElement('span');
            title.className = 'poster-card-title';
            title.textContent = item.name;

            card.appendChild(img);
            card.appendChild(title);
            fragment.appendChild(card);
        });

        container.appendChild(fragment);
    }

    // === TORRENTS PAGE FUNCTIONS ===
    async function fetchNextTorrentsPage(pageNumber, extraParams) {
        if (isPaused || (lastPageNumber !== Infinity && pageNumber > lastPageNumber)) {
            hasMore = false;
            showStatusMessage(`تم الوصول إلى نهاية القائمة عند الصفحة رقم ${lastPageNumber}`);
            updateControlsUI();
            return [];
        }

        isLoading = true;
        showStatusMessage("جاري التحميل...");
        await delay(LOAD_DELAY);

        const url = `${BASE_URL}/index.php?page=torrents${extraParams}&pages=${pageNumber}`;
        try {
            const response = await fetch(url, { headers: { 'X-Requested-With': 'XMLHttpRequest' } });
            if (!response.ok) throw new Error(`Server responded with status: ${response.status}`);

            const htmlText = await response.text();
            const doc = new DOMParser().parseFromString(htmlText, "text/html");

            // Get torrent rows from the table
            const torrentTable = doc.querySelector('table#torrents_list_p tbody');
            if (!torrentTable) {
                hasMore = false;
                currentPage = pageNumber - 1;
                updateControlsUI();
                return [];
            }

            const rows = torrentTable.querySelectorAll('tr');
            if (rows.length === 0) {
                hasMore = false;
                currentPage = pageNumber - 1;
                updateControlsUI();
                return [];
            }

            // Return the HTML of the rows
            const rowsHTML = [];
            rows.forEach(row => {
                rowsHTML.push(row.outerHTML);
            });

            return rowsHTML;
        } catch (error) {
            console.error('Error fetching torrents:', error);
            hasMore = false;
            currentPage = pageNumber - 1;
            updateControlsUI();
            return [];
        } finally {
            isLoading = false;
            if (hasMore) showStatusMessage("");
        }
    }

    function renderTorrentRows(rowsHTML, container) {
        // Add a separator for the new page
        const separatorHTML = `<tr><td colspan="9" class="infinite-scroll-separator">═══ الصفحة ${currentPage + 1} ═══</td></tr>`;
        container.insertAdjacentHTML('beforeend', separatorHTML);

        rowsHTML.forEach(rowHTML => {
            container.insertAdjacentHTML('beforeend', rowHTML);
        });
    }

    // === SHARED FUNCTIONS ===
    async function fetchLastPageNumber(pageParam, extraParams) {
        try {
            const url = `${BASE_URL}/index.php?page=${pageParam}${extraParams}&pages=99999`;
            const response = await fetch(url, { headers: { 'X-Requested-With': 'XMLHttpRequest' } });
            if (!response.ok) throw new Error(`Prefetch failed with status: ${response.status}`);

            const htmlText = await response.text();
            const doc = new DOMParser().parseFromString(htmlText, "text/html");
            const paginationContainer = doc.querySelector('form[name="change_page1pages"]');

            if (!paginationContainer) return Infinity;

            const pageLinks = paginationContainer.querySelectorAll('a, .pagercurrent b');
            let maxPage = 0;
            pageLinks.forEach(link => {
                const pageNum = parseInt(link.textContent);
                if (!isNaN(pageNum) && pageNum > maxPage) maxPage = pageNum;
            });

            console.log(`Prefetch for "${pageParam}" successful. True last page:`, maxPage);
            return maxPage > 0 ? maxPage : Infinity;
        } catch (error) {
            console.error('Could not prefetch the last page number.', error);
            return Infinity;
        }
    }

    function setupIntersectionObserver(pageParam, extraParams) {
        // Create a sentinel element at the bottom
        const sentinel = document.createElement('div');
        sentinel.id = 'scroll-sentinel';
        sentinel.style.height = '10px';

        // Insert sentinel after the grid/table container
        if (pageMode === 'listing') {
            gridContainer.parentElement.insertBefore(sentinel, statusContainer);
        } else {
            gridContainer.closest('table').parentElement.appendChild(sentinel);
        }

        // Load function that can be called repeatedly
        async function loadMore() {
            const canLoadMore = !isLoading && hasMore && !isPaused && (lastPageNumber === Infinity || currentPage < lastPageNumber);
            if (!canLoadMore) return;

            const nextPage = currentPage + 1;

            if (pageMode === 'listing') {
                const newItems = await fetchNextListingPage(nextPage, pageParam, extraParams);
                if (newItems.length > 0) {
                    currentPage = nextPage;
                    renderPosters(newItems, gridContainer);
                }
            } else if (pageMode === 'torrents') {
                const newRows = await fetchNextTorrentsPage(nextPage, extraParams);
                if (newRows.length > 0) {
                    currentPage = nextPage;
                    renderTorrentRows(newRows, gridContainer);
                }
            }

            updateControlsUI();

            // Check if we need to load more (user scrolled fast)
            requestAnimationFrame(() => {
                const rect = sentinel.getBoundingClientRect();
                const isVisible = rect.top < window.innerHeight + 500;
                if (isVisible && hasMore && !isPaused && !isLoading) {
                    loadMore();
                }
            });
        }

        // Use IntersectionObserver for better performance
        observer = new IntersectionObserver((entries) => {
            const entry = entries[0];
            if (entry.isIntersecting) {
                loadMore();
            }
        }, {
            root: null,
            rootMargin: '500px', // Load when 500px away from viewport
            threshold: 0
        });

        observer.observe(sentinel);
    }

    // Fallback scroll listener for older browsers
    function setupScrollListener(pageParam, extraParams) {
        if ('IntersectionObserver' in window) {
            setupIntersectionObserver(pageParam, extraParams);
            return;
        }

        // Throttled scroll handler for older browsers
        let scrollTimeout;
        window.addEventListener('scroll', () => {
            if (scrollTimeout) return;

            scrollTimeout = setTimeout(async () => {
                scrollTimeout = null;

                const isNearBottom = (window.innerHeight + window.scrollY) >= document.documentElement.scrollHeight - 500;
                const canLoadMore = !isLoading && hasMore && !isPaused && (lastPageNumber === Infinity || currentPage < lastPageNumber);

                if (isNearBottom && canLoadMore) {
                    const nextPage = currentPage + 1;

                    if (pageMode === 'listing') {
                        const newItems = await fetchNextListingPage(nextPage, pageParam, extraParams);
                        if (newItems.length > 0) {
                            currentPage = nextPage;
                            renderPosters(newItems, gridContainer);
                        }
                    } else if (pageMode === 'torrents') {
                        const newRows = await fetchNextTorrentsPage(nextPage, extraParams);
                        if (newRows.length > 0) {
                            currentPage = nextPage;
                            renderTorrentRows(newRows, gridContainer);
                        }
                    }

                    updateControlsUI();
                }
            }, 100); // Throttle to 100ms
        });
    }

    function setupControlButtons() {
        stopBtn.addEventListener('click', () => {
            isPaused = true;
            showStatusMessage("التحميل التلقائي متوقف");
            updateControlsUI();
        });

        resumeBtn.addEventListener('click', () => {
            isPaused = false;
            showStatusMessage("");
            updateControlsUI();
            // Trigger check for loading more content
            const sentinel = document.getElementById('scroll-sentinel');
            if (sentinel && observer) {
                observer.unobserve(sentinel);
                observer.observe(sentinel);
            } else {
                window.dispatchEvent(new CustomEvent('scroll'));
            }
        });
    }

    // === LISTING PAGES INITIALIZER ===
    async function initializeListing() {
        const urlParams = new URLSearchParams(window.location.search);
        const pageParam = urlParams.get('page');
        const ALLOWED_LISTING_PAGES = ['anime-listing', 'movies-listing', 'tv-listing', 'turkey-listing'];

        if (!pageParam || !ALLOWED_LISTING_PAGES.includes(pageParam)) return false;

        const allowedKeys = ['page', 'pages', 'play'];
        for (const key of urlParams.keys()) {
            if (!allowedKeys.includes(key)) return false;
        }

        pageMode = 'listing';
        document.body.classList.add('enhancer-active');

        const extraParams = urlParams.has('play') ? '&play' : '';
        lastPageNumber = await fetchLastPageNumber(pageParam, extraParams);

        const currentPageElement = document.querySelector('span.pagercurrent b');
        if (currentPageElement) {
            currentPage = parseInt(currentPageElement.textContent);
        } else {
            currentPage = 1;
        }
        console.log('Listing Mode - Current Page:', currentPage);

        const mainContainer = document.querySelector(".listing_div");
        const searchForm = mainContainer ? mainContainer.querySelector('form[name="anime_search"]') : null;
        if (!mainContainer || !searchForm) return false;

        const initialData = [];
        document.querySelectorAll('.listing_div1').forEach(item => {
            const linkTag = item.querySelector('a');
            const imgTag = item.querySelector('img.listing_poster');
            if (linkTag && imgTag) {
                initialData.push({
                    name: imgTag.getAttribute('alt').replace(/<br>/g, ' ').trim(),
                    link: `${BASE_URL}/${linkTag.getAttribute('href')}`,
                    imageUrl: imgTag.getAttribute('src')
                });
            }
        });

        mainContainer.innerHTML = `
            ${searchForm.outerHTML}
            <div id="custom-poster-grid"></div>
            <div id="status-container"></div>
            <div id="status-controls">
                <button id="resume-btn" class="control-btn resume" title="استئناف التحميل">▶ استئناف</button>
                <div id="page-indicator"></div>
                <button id="stop-btn" class="control-btn stop" title="إيقاف التحميل">❚❚ إيقاف</button>
            </div>
        `;

        gridContainer = document.getElementById('custom-poster-grid');
        statusContainer = document.getElementById('status-container');
        pageIndicator = document.getElementById('page-indicator');
        stopBtn = document.getElementById('stop-btn');
        resumeBtn = document.getElementById('resume-btn');

        renderPosters(initialData, gridContainer);
        updateControlsUI();
        setupControlButtons();
        setupScrollListener(pageParam, extraParams);

        return true;
    }

    // === TORRENTS PAGE INITIALIZER ===
    async function initializeTorrents() {
        const urlParams = new URLSearchParams(window.location.search);
        const pageParam = urlParams.get('page');

        if (pageParam !== 'torrents') return false;

        pageMode = 'torrents';
        document.body.classList.add('enhancer-torrents-active');

        // Build extra params from URL (category, search, active, order, etc.)
        let extraParams = '';
        const preserveKeys = ['category', 'search', 'active', 'internel', 'order', 'by', 'uploader', 'id', 'tag'];
        for (const key of preserveKeys) {
            if (urlParams.has(key)) {
                extraParams += `&${key}=${encodeURIComponent(urlParams.get(key))}`;
            }
        }

        lastPageNumber = await fetchLastPageNumber('torrents', extraParams);

        const currentPageElement = document.querySelector('span.pagercurrent b');
        if (currentPageElement) {
            currentPage = parseInt(currentPageElement.textContent);
        } else {
            currentPage = 1;
        }
        console.log('Torrents Mode - Current Page:', currentPage);

        // Find the torrent table
        const torrentTable = document.querySelector('table#torrents_list_p tbody');
        if (!torrentTable) {
            console.log('Torrents table not found');
            return false;
        }

        // Find the parent container to add controls
        const tableParent = document.querySelector('table#torrents_list_p').parentElement;

        // Create status and control elements
        const controlsHTML = `
            <div id="status-container"></div>
            <div id="status-controls">
                <button id="resume-btn" class="control-btn resume" title="استئناف التحميل">▶ استئناف</button>
                <div id="page-indicator"></div>
                <button id="stop-btn" class="control-btn stop" title="إيقاف التحميل">❚❚ إيقاف</button>
            </div>
        `;

        tableParent.insertAdjacentHTML('beforeend', controlsHTML);

        gridContainer = torrentTable; // We'll append rows directly to tbody
        statusContainer = document.getElementById('status-container');
        pageIndicator = document.getElementById('page-indicator');
        stopBtn = document.getElementById('stop-btn');
        resumeBtn = document.getElementById('resume-btn');

        updateControlsUI();
        setupControlButtons();
        setupScrollListener('torrents', extraParams);

        return true;
    }

    // --- Main Initialize ---
    async function initialize() {
        // Try listing pages first
        if (await initializeListing()) {
            console.log('ArabP2P Enhancer: Listing mode activated');
            return;
        }

        // Try torrents page
        if (await initializeTorrents()) {
            console.log('ArabP2P Enhancer: Torrents mode activated');
            return;
        }

        console.log('ArabP2P Enhancer: No compatible page detected');
    }

    initialize();
})();