Bunkr Universal Suite (Auto-Sort & Scan)

A "set and forget" script. Sorts by Size (descending) and scans Media Quality on ALL Bunkr domains (present and future).

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Bunkr Universal Suite (Auto-Sort & Scan)
// @namespace    http://tampermonkey.net/
// @version      5.4
// @description  A "set and forget" script. Sorts by Size (descending) and scans Media Quality on ALL Bunkr domains (present and future).
// @author       You
// @license      MIT
// @match        *://bunkr.cr/*
// @match        *://bunkr.is/*
// @match        *://bunkr.to/*
// @match        *://bunkr.ru/*
// @match        *://bunkr.site/*
// @match        *://bunkr.ph/*
// @match        *://bunkr.media/*
// @match        *://bunkr.black/*
// @include      *://bunkr.*/*
// @include      *://*.bunkr.*/*
// @include      *://bunkrr.*/*
// @include      *://*.bunkrr.*/*
// @include      *://bunkr-albums.io/*
// @include      *://*.bunkr-albums.io/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=bunkr.cr
// @grant        GM_xmlhttpRequest
// ==/UserScript==

// --- CHANGELOG ---
// v5.4 (2026-02-17): Added @include headers to support wildcard TLDs for broader compatibility per user feedback. (Coded by Gemini)
// v5.3 (2026-02-15): Added support for 'bunkr-albums.io' and ensuring all TLD variations are caught. (Coded by Gemini)
// v5.2 (2026-02-15): Added Changelog and License per user request. (Coded by Gemini)
// v5.1 (2026-02-15): Added MIT License and fixed namespace. (Coded by Gemini)
// v5.0 (2026-02-15): Rewrote Scanner to use Metadata Method (scrapes og:video tags) for reliability. Added Universal Domain Support. (Coded by Gemini)
// v4.0 (2026-02-15): Attempted Metadata scraping method. (Coded by Gemini)
// v3.1 (2026-02-15): Enhanced button injection for SPA navigation. (Coded by Gemini)
// v3.0 (2026-02-15): Attempted Download Link Method for probing. (Coded by Gemini)
// v2.0 (2026-02-15): Attempted Active Probe method. (Coded by Gemini)
// v1.1 (2026-02-15): Added Strict Mode for resolution detection. (Coded by Gemini)
// v1.0 (2026-02-15): Initial separate Quality Scanner script. (Coded by Gemini)
// v0.x (2026-02-15): Initial sorting prototypes. (Coded by Gemini)
// -----------------

(function() {
    'use strict';

    // --- CONFIGURATION ---
    const CONFIG = {
        SORT_DELAY: 500,        // Delay between sort clicks
        BUTTON_CHECK: 1000,     // How often to check if "Scan" button is missing (ms)
    };

    // --- PART 1: AUTO-SORT LOGIC ---
    let hasSorted = false;
    let lastUrl = location.href;

    function trySorting() {
        // Reset sort trigger if URL changes (SPA navigation)
        if (location.href !== lastUrl) {
            lastUrl = location.href;
            hasSorted = false;
        }
        if (hasSorted) return;

        // Find the "Size" sort button by text content
        const allElements = document.getElementsByTagName("*");
        for (let el of allElements) {
            if (el.textContent.trim() === "Size" && el.offsetParent !== null && el.tagName !== 'SCRIPT') {
                const style = window.getComputedStyle(el);
                // Check if already active (usually blue text or bg)
                const isActive = el.classList.contains('active') ||
                                 el.classList.contains('bg-blue-600') ||
                                 el.classList.contains('text-blue-600') ||
                                 (style.backgroundColor !== 'rgba(0, 0, 0, 0)' && style.backgroundColor !== 'transparent');

                if (!isActive) {
                    el.click(); // Click once (Ascending)
                    setTimeout(() => el.click(), CONFIG.SORT_DELAY); // Click again (Descending)
                    hasSorted = true;
                    console.log("[Bunkr Suite] Sorted by Size Descending");
                } else {
                    hasSorted = true; // Already active
                }
                break;
            }
        }
    }

    // --- PART 2: SCANNER UI ---
    function createScanButton() {
        if (document.getElementById('bunkr-scan-btn')) return;

        const btn = document.createElement('button');
        btn.id = 'bunkr-scan-btn';
        btn.innerHTML = "🔍 Scan Quality";
        btn.style.cssText = `
            position: fixed;
            top: 15px;
            right: 80px;
            z-index: 2147483647;
            background: #e11d48;
            color: white;
            padding: 10px 16px;
            border: 2px solid #ffffff;
            border-radius: 8px;
            cursor: pointer;
            font-weight: bold;
            font-size: 14px;
            box-shadow: 0 4px 15px rgba(0,0,0,0.5);
            transition: all 0.2s;
        `;
        
        btn.onclick = startScan;
        document.body.appendChild(btn);
    }

    // --- PART 3: SCANNER ENGINE (Metadata Method) ---
    async function startScan() {
        const btn = document.getElementById('bunkr-scan-btn');
        // Select file links (usually contain /v/ for video or /f/ for file)
        const links = Array.from(document.querySelectorAll('a[href*="/v/"], a[href*="/f/"]'));
        
        if (links.length === 0) {
            btn.innerText = "❌ No Files";
            setTimeout(() => btn.innerText = "🔍 Scan Quality", 2000);
            return;
        }

        btn.disabled = true;
        btn.style.backgroundColor = "#555";
        let successCount = 0;

        for (let i = 0; i < links.length; i++) {
            const link = links[i];
            btn.innerText = `Scanning ${i+1}/${links.length}...`;
            
            // Skip if already scanned
            if (link.getAttribute('data-scanned') === 'true') continue;

            try {
                // Fetch page metadata (Fastest method, bypasses download blocks)
                const pageData = await fetchPageMetadata(link.href);
                
                if (pageData) {
                    injectInfo(link, pageData);
                    successCount++;
                }
                link.setAttribute('data-scanned', 'true');
            } catch (e) {
                console.warn("[Bunkr Suite] Scan error:", e);
            }
            
            // Throttle requests slightly to be safe
            await new Promise(r => setTimeout(r, 150)); 
        }

        btn.innerText = `✅ Done (${successCount} found)`;
        btn.style.backgroundColor = "#10b981";
        setTimeout(() => {
            btn.disabled = false;
            btn.innerText = "🔍 Scan Quality";
            btn.style.backgroundColor = "#e11d48";
        }, 4000);
    }

    function fetchPageMetadata(pageUrl) {
        return new Promise((resolve) => {
            GM_xmlhttpRequest({
                method: "GET",
                url: pageUrl,
                onload: function(res) {
                    const html = res.responseText;
                    
                    // Regex for og:video tags (Standard Bunkr metadata)
                    const wMatch = html.match(/property="og:video:width"\s+content="(\d+)"/i);
                    const hMatch = html.match(/property="og:video:height"\s+content="(\d+)"/i);
                    
                    // Regex for Duration (Used for bitrate calc)
                    const durMatch = html.match(/Duration:\s*(\d{1,2}):(\d{2})(?::(\d{2}))?/i);
                    
                    let durationSec = 0;
                    if (durMatch) {
                        if (durMatch[3]) { // HH:MM:SS
                            durationSec = (parseInt(durMatch[1]) * 3600) + (parseInt(durMatch[2]) * 60) + parseInt(durMatch[3]);
                        } else { // MM:SS
                            durationSec = (parseInt(durMatch[1]) * 60) + parseInt(durMatch[2]);
                        }
                    }

                    if (wMatch && hMatch) {
                        resolve({ 
                            w: parseInt(wMatch[1]), 
                            h: parseInt(hMatch[2]), 
                            dur: durationSec 
                        });
                    } else {
                        resolve(null);
                    }
                },
                onerror: () => resolve(null)
            });
        });
    }

    function injectInfo(linkElement, meta) {
        // Find the "card" container
        const card = linkElement.closest('div.relative') || linkElement.parentElement;
        
        // Extract File Size from the card text
        let sizeMB = 0;
        const sizeMatch = card.innerText.match(/(\d+(\.\d+)?)\s*(MB|GB)/i);
        if (sizeMatch) {
            let val = parseFloat(sizeMatch[1]);
            if (sizeMatch[3].toUpperCase() === 'GB') val *= 1024;
            sizeMB = val;
        }

        // Calculate Bitrate
        let bitrateStr = "";
        let color = "#4ade80"; // Default Green
        if (sizeMB > 0 && meta.dur > 0) {
            const kbps = Math.round((sizeMB * 8192) / meta.dur);
            bitrateStr = ` | ${kbps} kbps`;
            
            // Color Coding
            if (kbps > 5000) color = "#4ade80";      // Green (Excellent)
            else if (kbps > 2000) color = "#facc15"; // Yellow (Good)
            else color = "#f87171";                  // Red (Low)
        }

        // Format Resolution
        let res = `${meta.w}x${meta.h}`;
        if ((meta.w===1920 || meta.h===1920) && (meta.w===1080 || meta.h===1080)) res = "1080p";
        else if ((meta.w===1280 || meta.h===1280) && (meta.w===720 || meta.h===720)) res = "720p";
        else if (meta.w>=3840 || meta.h>=3840) res = "4K";

        // Create Badge
        const badge = document.createElement('div');
        badge.style.cssText = `
            position: absolute;
            bottom: 50px;
            left: 5px;
            background: rgba(0,0,0,0.95);
            color: ${color};
            font-size: 11px;
            padding: 4px 6px;
            border-radius: 4px;
            z-index: 999;
            font-family: monospace;
            font-weight: bold;
            border: 1px solid ${color};
            pointer-events: none;
        `;
        badge.innerText = `📺 ${res}${bitrateStr}`;

        if (getComputedStyle(card).position === 'static') card.style.position = 'relative';
        card.appendChild(badge);
    }

    // --- MAIN LOOPS ---
    setInterval(trySorting, 2000);        // Run Sort Check
    setInterval(createScanButton, 1000);  // Run Button Check

})();