Greasy Fork

Greasy Fork is available in English.

Avistaz+

Enhancements for Avistaz

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Avistaz+
// @version      1.0
// @description  Enhancements for Avistaz
// @author       Mio.
// @namespace    https://github.com/dear-clouds/mio-userscripts
// @supportURL   https://github.com/dear-clouds/mio-userscripts/issues
// @icon         https://www.google.com/s2/favicons?sz=64&domain=avistaz.to
// @license      GPL-3.0
// @match        *://*.avistaz.to/*
// @require      https://cdnjs.cloudflare.com/ajax/libs/jszip/3.7.1/jszip.min.js
// @require      https://cdnjs.cloudflare.com/ajax/libs/FileSaver.js/2.0.5/FileSaver.min.js
// @grant        none
// ==/UserScript==

(function() {
    'use strict';
    
    const username = document.querySelector("a[href*='/profile/']").getAttribute('href').split('/').pop();
    const STORAGE_KEY_FULL_SCAN_COMPLETED = `avistaz+_${username}_full_scan_completed`;
    let stopFetching = false;
    
    // Function to simulate a click on the "Thank Uploader" button
    function clickThankButton(torrentId) {
        const thankButton = document.querySelector(`button#btn-torrent-thank[data-id='${torrentId}']`);
        if (thankButton) {
            thankButton.click();
            console.log("Thanked uploader for torrent ID: " + torrentId);
        }
    }
    
    // Function to delete full scan completed state from localStorage
    function resetFullScanState() {
        localStorage.removeItem(STORAGE_KEY_FULL_SCAN_COMPLETED);
        console.log("Full scan state has been reset. You can now retry a full scan.");
    }
    
    // Add a click event listener to each download link
    document.querySelectorAll("a.btn.btn-xs.btn-primary").forEach(downloadLink => {
        downloadLink.addEventListener("click", function(event) {
            const torrentIdMatch = this.href.match(/\/download\/torrent\/(\d+)/);
            if (torrentIdMatch && torrentIdMatch[1]) {
                const torrentId = torrentIdMatch[1];
                // Set a small timeout to ensure the "Thank Uploader" button is available
                setTimeout(() => {
                    clickThankButton(torrentId);
                }, 500);
            }
        });
    });

        // Function to bypass anon.to redirects
        function bypassAnonRedirects() {
            const allLinks = document.querySelectorAll('a');
    
            allLinks.forEach(link => {
                if (link.href.startsWith('https://anon.to/?')) {
                    const realURL = link.href.split('?')[1];
    
                    if (realURL) {
                        link.href = decodeURIComponent(realURL);
    
                        if (window.location.href.startsWith('https://anon.to/')) {
                            window.location.href = decodeURIComponent(realURL);
                        }
                    }
                }
            });
        }

        bypassAnonRedirects();
    
    // Function to gather all Hit & Run entries from history pages
    async function collectHitAndRuns() {
        let currentPage = 1;
        const collectedHitAndRuns = [];
        let isFullScan = false;
        
        const fullScanCompleted = localStorage.getItem(STORAGE_KEY_FULL_SCAN_COMPLETED) === 'true';
        let totalPages = null;
        
        const noHnrMessage = document.querySelector('.table-responsive h3');
        if (noHnrMessage) {
            noHnrMessage.innerHTML = '<div class="loading-icon"><i class="fa fa-spinner fa-spin"></i> Searching for Hit & Run torrents... <strong>(01/??)</strong></div>';
        } else {
            console.log("No 'No Hit & Run Torrents' message found, skipping Hit & Run collection.");
            return;
        }
        
        function updateProgress(current, total) {
            const loadingIcon = document.querySelector('.loading-icon');
            if (loadingIcon) {
                loadingIcon.innerHTML = `<i class="fa fa-spinner fa-spin"></i> Searching for Hit & Run torrents... <strong>(${current}/${total})</strong>`;
            }
        }
        
        async function fetchPage(pageNumber) {
            if (stopFetching) {
                console.log(`Skipping page ${pageNumber} as stop condition was met.`);
                return;
            }
            
            console.log(`Fetching page ${pageNumber}`);
            try {
                const response = await fetch(`https://avistaz.to/profile/${username}/history?page=${pageNumber}`);
                const html = await response.text();
                const parser = new DOMParser();
                const doc = parser.parseFromString(html, 'text/html');
                const hitAndRunRows = doc.querySelectorAll('tr');
                
                // Get total number of pages from pagination element
                if (totalPages === null) {
                    const pagination = doc.querySelector('ul.pagination');
                    if (pagination) {
                        const pageNumbers = Array.from(pagination.querySelectorAll('a[href*="?page="]'))
                        .map(link => parseInt(link.textContent.trim(), 10))
                        .filter(page => !isNaN(page));
                        if (pageNumbers.length > 0) {
                            totalPages = Math.max(...pageNumbers);
                            console.log(`Total pages detected: ${totalPages}`);
                        }
                    }
                    if (!totalPages) {
                        totalPages = 1; // Fallback if pagination is missing
                    }
                }
                
                for (const row of hitAndRunRows) {
                    if (stopFetching) {
                        console.log(`Stopping processing of rows on page ${pageNumber} because stop condition was met.`);
                        return;
                    }
                    
                    // Specifically select the "Added" column to find the date
                    const addedDateCell = row.querySelectorAll('td')[8];
                    if (addedDateCell) {
                        const addedDateElement = addedDateCell.querySelector('span[data-toggle="tooltip"]');
                        if (addedDateElement) {
                            const addedDateText = addedDateElement.textContent.trim().toLowerCase();
                            const originalTitleText = addedDateElement.getAttribute('data-original-title')?.toLowerCase().trim() || "";
                            // console.log(`Checking date text: "${addedDateText}" and data-original-title: "${originalTitleText}" on page: ${pageNumber}`);
                            
                            // Check if either the text content or the attribute contains "1 month"
                            if (fullScanCompleted && (addedDateText.includes("1 month") || originalTitleText.includes("1 month"))) {
                                stopFetching = true;
                                console.log(`Stopping fetch, found torrent added over a month ago on page: ${pageNumber}`);
                                removeLoadingIcon();
                                break;
                            }
                        }
                    }
                    if (row.classList.contains('danger')) {
                        collectedHitAndRuns.push(row.outerHTML);
                        updateHitAndRunTable(row);
                    }
                }
                
                if (!fullScanCompleted) {
                    isFullScan = true; // Indicate this is a full scan
                }
                
                updateProgress(pageNumber, totalPages);
                currentPage++;
            } catch (err) {
                console.error(`Error fetching history page ${pageNumber}: `, err);
            }
        }
        
        // Fetch pages in parallel but with proper stopping control
        async function fetchNextPages(startPage, pagesToFetch = 5) {
            if (stopFetching) {
                removeLoadingIcon();
                return;
            }
            
            const fetchPromises = [];
            for (let i = 0; i < pagesToFetch; i++) {
                if (startPage + i <= (totalPages ?? 1) && !stopFetching) {
                    fetchPromises.push(fetchPage(startPage + i));
                }
            }
            
            await Promise.all(fetchPromises);
            
            if (!stopFetching && currentPage <= totalPages) {
                console.log(`Finished batch, continuing from page ${currentPage}`);
                await fetchNextPages(currentPage, pagesToFetch);
            } else {
                if (isFullScan && !stopFetching) {
                    console.log("Reached the last page, updating cache as full scan completed.");
                    localStorage.setItem(STORAGE_KEY_FULL_SCAN_COMPLETED, 'true');
                }
                removeLoadingIcon();
            }
        }
        
        console.log(`Starting Hit & Run collection from page ${currentPage}`);
        await fetchNextPages(currentPage, 5);
    }
    
    // Function to update Hit & Run entries as they are found
    function updateHitAndRunTable(hitAndRunRow) {
        const tableContainer = document.querySelector('.table-responsive');
        if (tableContainer) {
            // Check if we need to replace the "No Hit & Run Torrents" message
            const noHnrMessage = tableContainer.querySelector('h3');
            if (noHnrMessage) {
                noHnrMessage.remove();
            }
            // If the table doesn't already exist, create it
            let table = tableContainer.querySelector('table');
            if (!table) {
                let tableHtml = '<table class="table table-bordered"><thead><tr><th>Type</th><th>Torrent</th><th>Download</th><th>Seeding</th><th>Completed</th><th>Uploaded</th><th>Downloaded</th><th>Ratio</th><th>Added</th><th>Updated</th><th>Seed Time</th><th>Hit & Run</th></tr></thead><tbody></tbody></table>';
                tableContainer.insertAdjacentHTML('beforeend', tableHtml);
            }
            const tableBody = tableContainer.querySelector('tbody');
            if (tableBody) {
                const newHitAndRunRow = document.createElement('tr');
                newHitAndRunRow.className = hitAndRunRow.className;
                newHitAndRunRow.innerHTML = hitAndRunRow.innerHTML;
                
                // Ensure the button triggers the original modal using PopAlert.warning
                const clearButton = newHitAndRunRow.querySelector('.btn_clear_hitnrun');
                if (clearButton) {
                    clearButton.addEventListener('click', function(event) {
                        event.preventDefault();
                        
                        // Fetch the necessary data for PopAlert
                        const id = clearButton.getAttribute('data-id');
                        const hnrPoints = clearButton.getAttribute('data-hnr');
                        
                        if (typeof PopAlert !== 'undefined' && PopAlert.warning) {
                            PopAlert.warning(
                                `Clear this H&R for ${hnrPoints} BP?`,
                                `Are you sure, you want to clear this Hit & Run for ${hnrPoints} Bonus Points?`,
                                BASEURL + `/profile/${username}/hnr/clear`,
                                {
                                    id: id,
                                    action: "clear_hnr"
                                }
                            );
                        } else {
                            console.error("PopAlert is not defined. Cannot trigger modal.");
                        }
                    });
                    
                    if (typeof $ !== 'undefined' && $.fn.tooltip) {
                        $(clearButton).tooltip();
                    }
                    
                    const dataTitle = clearButton.getAttribute('data-title');
                    if (dataTitle) {
                        clearButton.setAttribute('title', dataTitle);
                    }
                }
                
                tableBody.appendChild(newHitAndRunRow);
                console.log(`Added a new Hit & Run entry to the table.`);
            }
            // Keep the loading message visible below the table while fetching
            if (!document.querySelector('.loading-icon')) {
                tableContainer.insertAdjacentHTML('beforeend', '<div class="loading-icon"><i class="fa fa-spinner fa-spin"></i> Searching for Hit & Run torrents... <strong>(${current}/${total})</strong> <small>(This will take a while the first time it runs. Just keep the tab open.)</small></div>');
            }
        }
    }
    
    // Function to remove the loading icon
    function removeLoadingIcon() {
        const loadingIcon = document.querySelector('.loading-icon');
        if (loadingIcon) {
            loadingIcon.remove();
            console.log("Removed loading icon after completing history search.");
        }
    }
    
    // Automatically collect Hit & Runs when navigating to the H&R page
    if (window.location.href.includes(`/profile/${username}/history?hnr=1`)) {
        const noHnrMessage = document.querySelector('.table-responsive h3');
        if (noHnrMessage && noHnrMessage.textContent.includes('No Hit & Run Torrents')) {
            console.log("Detected H&R history page with no existing H&R, starting Hit & Run collection.");
            collectHitAndRuns();
        } else {
            console.log("No 'No Hit & Run Torrents' message found, skipping Hit & Run collection.");
        }
    }
    
    else if (window.location.href.includes(`/torrent`)) {
        // Function to calculate the seeding time based on file size
        function seedingTimeInHours(sizeGB) {
            if (sizeGB <= 1) {
                return 72; // Minimum seeding time for sizes less than or equal to 1GB
            } else if (sizeGB < 50) {
                return 72 + 2 * sizeGB; // For sizes between 1GB and 50GB
            } else {
                return 100 * Math.log(sizeGB) - 219.2023; // For sizes greater than or equal to 50GB
            }
        }
        
        // Function to format hours into days, hours, and minutes
        function formatTime(hours) {
            const totalMinutes = Math.round(hours * 60);
            const days = Math.floor(totalMinutes / (24 * 60));
            const remainingMinutes = totalMinutes % (24 * 60);
            const hoursPart = Math.floor(remainingMinutes / 60);
            const minutesPart = remainingMinutes % 60;
            
            return `${days} days, ${hoursPart} hours, ${minutesPart} minutes`;
        }
        
        // Get the file size element from the page
        const fileSizeRow = document.evaluate(
            "//tr[td/strong[text()='File Size']]",
            document,
            null,
            XPathResult.FIRST_ORDERED_NODE_TYPE,
            null
        ).singleNodeValue;
        
        if (fileSizeRow) {
            const sizeText = fileSizeRow.children[1].textContent.trim();
            const sizeGB = parseFloat(sizeText.split(" ")[0]);
            
            if (!isNaN(sizeGB)) {
                // Calculate the required seeding time in hours
                const seedingTimeHours = seedingTimeInHours(sizeGB);
                
                // Format the seeding time into days, hours, and minutes
                const formattedTime = formatTime(seedingTimeHours);
                
                const seedingTimeContainer = document.createElement("span");
                seedingTimeContainer.style.fontWeight = "bold";
                seedingTimeContainer.style.marginLeft = "10px";
                
                const staticText = document.createElement("span");
                staticText.textContent = "(You need to seed this for ";
                
                // Have the formatted time with the class `text-green`
                const timeSpan = document.createElement("span");
                timeSpan.className = "text-green";
                timeSpan.textContent = formattedTime;
                
                const closingText = document.createElement("span");
                closingText.textContent = ")";
                
                seedingTimeContainer.appendChild(staticText);
                seedingTimeContainer.appendChild(timeSpan);
                seedingTimeContainer.appendChild(closingText);
                
                // Append the seeding time next to the file size
                fileSizeRow.children[1].appendChild(seedingTimeContainer);
            }
        }
    }
    
    // Adding manual reset functionality
    window.resetFullScanState = resetFullScanState;
})();