Fetch all pages, filter out movies rated below 4.3 or with fewer than 500 reviews, sort remainder by review count descending
// ==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();
})();