Sleazy Fork is available in English.

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

Vous devrez installer une extension telle que Tampermonkey, Greasemonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Violentmonkey pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey ou Userscripts pour installer ce script.

Vous devrez installer une extension telle que Tampermonkey pour installer ce script.

Vous devrez installer une extension de gestionnaire de script utilisateur pour installer ce script.

(J'ai déjà un gestionnaire de scripts utilisateur, laissez-moi l'installer !)

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension telle que Stylus pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

Vous devrez installer une extension du gestionnaire de style pour utilisateur pour installer ce style.

(J'ai déjà un gestionnaire de style utilisateur, laissez-moi l'installer!)

// ==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();

})();