javdb Re-sort

Fetch all pages, filter out movies rated below 4.3 or with fewer than 500 reviews, sort remainder by review count descending

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         javdb Re-sort
// @namespace    http://tampermonkey.net/
// @version      1.3
// @description  Fetch all pages, filter out movies rated below 4.3 or with fewer than 500 reviews, sort remainder by review count descending
// @author       You
// @match        https://javdb.com/*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=javdb.com
// @grant        none
// @license      MIT
// ==/UserScript==

(function () {
    'use strict';

    // ── Regex patterns ──
    const peoplePattern = /由(\d+)人/;   // Extract review count from Chinese text "由123人"
    const scorePattern  = /(\d+\.\d+)/;  // Extract numeric rating e.g. "4.56"
    const pagePattern   = /[?&]page=(\d+)/;

    // ── Filter thresholds (edit these to adjust) ──
    const MIN_SCORE  = 4.3;   // Minimum rating
    const MIN_PEOPLE = 500;   // Minimum number of reviews

    // ── Skip favourites / collection pages ──
    if (/\/(users\/[^/]+\/)?(collected|favourites|watchlist|want_watches|watched|reviews|history)/i.test(window.location.pathname)) return;

    const movieList = $('.movie-list')[0];
    if (!movieList) return; // Not a list page, exit early

    // ── Get total number of pages from pagination ──
    function getTotalPages() {
        // Primary: read last pagination link
        const lastPageLink = $('.pagination-list .pagination-link:last-child, .pagination a:last-of-type');
        if (lastPageLink && lastPageLink.length) {
            const href = lastPageLink.last().attr('href') || '';
            const m = pagePattern.exec(href);
            if (m) return parseInt(m[1], 10);
        }
        // Fallback: scan all pagination links for the highest page number
        const links = $('.pagination-link');
        let max = 1;
        links.each(function () {
            const m = pagePattern.exec($(this).attr('href') || '');
            if (m) max = Math.max(max, parseInt(m[1], 10));
        });
        return max;
    }

    // ── Read the current page number from the URL ──
    function getCurPage() {
        const m = pagePattern.exec(window.location.href);
        return m ? parseInt(m[1], 10) : 1;
    }

    // ── Build a URL for the given page number ──
    function getPageUrl(page) {
        const url = window.location.href;
        if (pagePattern.test(url)) {
            return url.replace(pagePattern, (match) => match.replace(/\d+$/, page));
        }
        return url + (url.includes('?') ? `&page=${page}` : `?page=${page}`);
    }

    // ── Parse review count and rating from a .item element ──
    function parseItem(item) {
        const scoreEl = $('.score', item)[0];
        if (!scoreEl) return null;

        const text        = scoreEl.textContent;
        const peopleMatch = peoplePattern.exec(text);
        const scoreMatch  = scorePattern.exec(text);

        if (!peopleMatch || !scoreMatch) return null;

        return {
            peopleCnt: parseInt(peopleMatch[1], 10),
            rating:    parseFloat(scoreMatch[1]),
        };
    }

    // ── Fetch a page via jQuery $.get, return a Promise of .item elements ──
    function fetchPage(page) {
        return new Promise((resolve) => {
            $.get(getPageUrl(page), function (data) {
                const container = document.createElement('div');
                container.innerHTML = data;
                resolve(Array.from($('.movie-list .item', container)));
            }).fail(() => resolve([]));
        });
    }

    // ── Highlight rating (green) and review count (red) in the score element ──
    function highlightScore(item) {
        const scoreEl = $('.score', item)[0];
        if (!scoreEl) return;
        scoreEl.innerHTML = scoreEl.innerHTML
            .replace(peoplePattern, `由<span style="color:#e74c3c;font-weight:bold">$1</span>人`)
            .replace(scorePattern,  `<span style="color:#27ae60;font-weight:bold">$1</span>`);
    }

    // ── Render the filtered list into the movie list container ──
    function renderList(items) {
        movieList.innerHTML = '';
        items.forEach(item => movieList.append(item));
    }

    // ── Fixed status bar shown at the top of the page ──
    function createStatusBar() {
        const bar = document.createElement('div');
        bar.id = 'reorder-status';
        bar.style.cssText = `
            position: fixed; top: 0; left: 0; right: 0; z-index: 99999;
            background: rgba(0,0,0,0.82); color: #fff;
            font-size: 14px; padding: 8px 16px;
            display: flex; align-items: center; gap: 12px;
            backdrop-filter: blur(4px);
        `;
        bar.innerHTML = `
            <span id="rs-spinner" style="display:inline-block;width:14px;height:14px;border:2px solid #fff;
                border-top-color:transparent;border-radius:50%;animation:rs-spin 0.7s linear infinite;"></span>
            <span id="rs-text">Initializing...</span>
            <style>@keyframes rs-spin{to{transform:rotate(360deg)}}</style>
        `;
        document.body.prepend(bar);
        return {
            update(msg) { document.getElementById('rs-text').textContent = msg; },
            done(msg) {
                document.getElementById('rs-spinner').style.display = 'none';
                document.getElementById('rs-text').textContent = msg;
                setTimeout(() => bar.remove(), 4000);
            }
        };
    }

    // ── Hide the original pagination to avoid confusion ──
    function hidePagination() {
        $('.pagination, nav.pagination').hide();
    }

    // ══════════════════════════════
    //  Main flow
    // ══════════════════════════════
    async function main() {
        const status = createStatusBar();
        hidePagination();

        const curPage    = getCurPage();
        const totalPages = getTotalPages();

        status.update(`Detected ${totalPages} page(s). Starting fetch...`);

        // Seed with items already on the current page
        let allItems = Array.from($('.item', movieList));

        // Build list of pages to fetch (skip current page, already loaded)
        const pagesToFetch = [];
        for (let p = 1; p <= totalPages; p++) {
            if (p !== curPage) pagesToFetch.push(p);
        }

        // Fetch in batches of 5 to avoid rate limiting
        let loaded = 1;
        const BATCH = 5;
        for (let i = 0; i < pagesToFetch.length; i += BATCH) {
            const batch   = pagesToFetch.slice(i, i + BATCH);
            const results = await Promise.all(batch.map(p => fetchPage(p)));
            results.forEach(items => allItems = allItems.concat(items));
            loaded += batch.length;
            status.update(`Loaded ${Math.min(loaded, totalPages)} / ${totalPages} pages — ${allItems.length} items so far...`);
        }

        // Parse metadata for each item.
        // Items whose score/count can't be read (e.g. favorites page uses a
        // different layout) are kept as-is and sorted to the bottom, rather
        // than being silently discarded.
        allItems.forEach(item => {
            const data = parseItem(item);
            if (data) {
                item._peopleCnt = data.peopleCnt;
                item._rating    = data.rating;
                item._parsed    = true;
            } else {
                item._peopleCnt = 0;
                item._rating    = Infinity; // bypass rating filter for unreadable items
                item._parsed    = false;
            }
        });

        // Apply filters only to items whose metadata could be read
        const filtered = allItems.filter(item =>
            !item._parsed || (item._rating >= MIN_SCORE && item._peopleCnt >= MIN_PEOPLE)
        );

        // Sort: parsed items by review count descending; unparsed items fall to the bottom
        filtered.sort((a, b) => {
            if (a._parsed && b._parsed) return b._peopleCnt - a._peopleCnt;
            if (a._parsed) return -1;
            if (b._parsed) return 1;
            return 0;
        });

        // Highlight scores and render
        filtered.forEach(item => { if (item._parsed) highlightScore(item); });
        renderList(filtered);

        const removedCount   = allItems.length - filtered.length;
        const unparsedCount  = filtered.filter(i => !i._parsed).length;
        const unparsedNote   = unparsedCount ? ` (${unparsedCount} item(s) kept without score data)` : '';
        status.done(
            `✅ All ${totalPages} pages loaded — ${allItems.length} total → removed ${removedCount} (rating < ${MIN_SCORE} or reviews < ${MIN_PEOPLE}) → ${filtered.length} remaining, sorted by review count${unparsedNote}`
        );
    }

    main();

})();