Buyee Seller Filter

Add infinite scrolling and options for filtering sellers to the Buyee search results page

当前为 2025-09-27 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         Buyee Seller Filter
// @license      MIT
// @version      1.51
// @description  Add infinite scrolling and options for filtering sellers to the Buyee search results page
// @author       rhgg2
// @match        https://buyee.jp/item/search/*
// @icon         https://www.google.com/s2/favicons?domain=buyee.jp
// @namespace https://greasyfork.org/users/1243343
// @grant        none
// @require     https://unpkg.com/[email protected]/js/smartphoto.min.js
// ==/UserScript==

// highlight interval for newly listed items; highlighted in green when new, slowly
// fading to white over the number of hours specified below
const newlyListedHighlightTime = 12;

// list containing metadata associated to sellers,
// including link to items and blacklist status
var sellersData;

// list containing metadata associated to items
// including seller name and blacklist status
var itemsData;

// should hidden sellers/items actually be hidden?
var hideHidden;

// smartPhoto instance

var smartPhoto = new SmartPhoto(".js-smartPhoto");
window.smartPhoto = smartPhoto

// buyee watchlist

var buyeeWatchlist;

// are we on the desktop site?
var isDesktop = (navigator.userAgent.match(/Android/i)
         || navigator.userAgent.match(/webOS/i)
         || navigator.userAgent.match(/iPhone/i)
         || navigator.userAgent.match(/iPad/i)
         || navigator.userAgent.match(/iPod/i)
         || navigator.userAgent.match(/BlackBerry/i)
         || navigator.userAgent.match(/Windows Phone/i)) ? false : true;

// save state to local storage
function serialiseData() {
   localStorage.hideHidden = JSON.stringify(hideHidden);
   localStorage.sellersData = JSON.stringify(sellersData);
   localStorage.itemsData = JSON.stringify(itemsData);
}

// load state from local storage
function unSerialiseData() {
    sellersData = ("sellersData" in localStorage) ? JSON.parse(localStorage.sellersData) : {};
    itemsData = ("itemsData" in localStorage) ? JSON.parse(localStorage.itemsData) : {};
    hideHidden = ("hideHidden" in localStorage) ? JSON.parse(localStorage.hideHidden) : true;
}

// fetch a URL and return a document containing it
function fetchURL(url) {
    return fetch(url)
    .then(response => response.text())
    .then(html => {
        var parser = new DOMParser();
        return parser.parseFromString(html, "text/html");
    });
}

// make a bullet node
function makeBullet() {
    let node = document.createElement("span");
    node.innerText = ' • ';
    node.classList.add('rg-node');
    node.style.width = '14px';
    node.style['text-align'] = 'center';
    if (!isDesktop) {
        node.classList.add('g-text');
    }
    return node;
}

// RGB to hex
const rgbToHex = (r, g, b) => '#' + [r, g, b].map(x => {
  const hex = x.toString(16)
  return hex.length === 1 ? '0' + hex : hex
}).join('')

// create a node which shows/hides the given seller when clicked
function makeSellerStatusNode(seller,status) {
    let node = document.createElement("a");
    node.href = "javascript:void(0);";
    if (status == "Show") {
        node.onclick = (() => {
            sellersData[seller].hide = false;
            serialiseData();
            processCardsIn(document, false);
        });
    } else {
        node.onclick = (() => {
            sellersData[seller].hide = true;
            serialiseData();
            processCardsIn(document, false);
        });
    }
    node.innerText = status;
    node.classList.add('rg-node');
    if (!isDesktop) { node.classList.add('g-text'); }
    return node;
}

// create a node which shows/hides the given item when clicked
function makeItemStatusNode(url,card,status) {
    let node = document.createElement("a");
    node.href = "javascript:void(0);";
    if (status == "Show") {
        node.onclick = (() => {
            itemsData[url].hide = false;
            serialiseData();
            processCard(card);
        });
    } else {
        node.onclick = (() => {
            itemsData[url].hide = true;
            serialiseData();
            processCard(card);
        });
    }
    node.innerText = status;
    node.classList.add('auctionSearchResult__statusItem');
    node.classList.add('rg-node');
    return node;
}

function clearInfoNodes(card)
{
    let infolist = card.querySelector(".itemCard__infoList");
    while (infolist.childElementCount > 3) {
        infolist.lastElementChild.remove();
    }
    let watchNumNode = card.querySelector(".watchList__watchNum");
    if (watchNumNode) watchNumNode.remove();
}


function addSellerNode(card, sellerData)
{
    let infolist = card.querySelector(".itemCard__infoList");
    let newnode = infolist.firstElementChild.cloneNode(true);
    newnode.querySelector(".g-title").innerText = "Seller";
    newnode.querySelector(".g-text").innerText = "";
    let a = document.createElement("a");
    a.href = sellerData.url;
    a.innerText = sellerData.name;
    newnode.querySelector(".g-text").appendChild(a);
    infolist.lastElementChild.remove();
    infolist.appendChild(newnode);
    return newnode;
}

function addWatchersNode(card, watcherNum)
{
    let starNode = card.querySelector(".g-feather");
    let newNode = document.createElement("span");
    let watchButton = starNode.parentNode;
    newNode.classList.add("watchList__watchNum");
    newNode.innerText = watcherNum;
    watchButton.appendChild(newNode);

    watchButton.addEventListener("click", e => {
        e.preventDefault();
        console.log(newNode);
        if (watchButton.classList.contains('is-active')) {
            newNode.innerText = parseInt(newNode.innerText)-1;
        } else {
            newNode.innerText = parseInt(newNode.innerText)+1;
        }
    });
}


// make a "loading" node for the infinite scrolling
function makeLoadingNode() {
    let card = document.createElement("li");
    card.classList.add('itemCard');
    card.classList.add('rg-loading');

    let innerDiv = document.createElement("div");
    innerDiv.classList.add('imgLoading');
    innerDiv.style.height = '150px';

    card.appendChild(innerDiv);

    return card;
}

// Remove old items from items list if not seen for > 1 week.
function cleanItems() {
    var now = Date.now();
    Object.keys(itemsData).forEach( (url) => {
        if (now - itemsData[url].lastSeen > 604800000) {
            delete itemsData[url];
        }
    });
    serialiseData();
}


// queue of cards to process
const MAX_CONCURRENT = 3;
const DELAY = 250;
var processingQueue = [];
var activeCount = 0;

// load data for next card in processing queue
function sleep(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

function loadCard() {
    if (processingQueue.length == 0 || activeCount >= MAX_CONCURRENT) return;

    const { card, url, item, resolve, reject } = processingQueue.shift();
    activeCount++;

    fetchURL(url)
      .then(doc => {
        fillCardData(card, doc, item);
        processCard(card);
        resolve(doc);
      })
      .catch(err => reject(err))
      .then(() => sleep(DELAY))
      .then(() => {
        activeCount--;
        loadCard();
      });

    loadCard();
}

// add card to processing queue

function addCardToQueue(card, url, item)
{
    return new Promise((resolve, reject) => {
        processingQueue.push({ card, url, item, resolve, reject });
        loadCard();
    });
}

// extract data from card page

function fillCardData(card, doc, item)
{
    let itemData = {};
    var xpath = '//a[contains(@href,"search/customer")]';
    let sellernode = doc.evaluate(xpath, doc, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;

//    let sellernode = doc.querySelector(".no_border.clearfix dd a");
    let seller = sellernode.href.match(/\/item\/search\/customer\/(.*)/);
    if (seller) seller = seller[1];

    itemData.hide = false;
    itemData.seller = seller;
    if (!(seller in sellersData)) {
        sellersData[seller] = {};
        sellersData[seller].name = sellernode.innerText.trim();
        sellersData[seller].url = sellernode.href;
        sellersData[seller].hide = false;
    }

    // get end time
    xpath = '//li[.//*[contains(text(),"Closing Time")]]';
    var result = doc.evaluate(xpath, doc, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
    itemData.endTime = result.querySelector("span:last-child").innerText;

    // get number of watchers

    doc.querySelectorAll('script').forEach(script => {
        const match = script.textContent.match(/buyee\.TOTAL_WATCH_COUNT\s*=\s*(\d+);/);
        if (match) itemData.watchNum = parseInt(match[1], 10);
    });
    if (!("watchNum" in itemData)) {
        itemData.watchNum = doc.querySelector(".watchButton__watchNum").innerText;
    }

    // get image links

    itemData.images = Array.from(doc.querySelectorAll(".js-smartPhoto")).map(node => node.href);

    itemsData[item] = itemData;
    serialiseData();
}

// process changes to a results card; this may be called to refresh the page on a state change,
// so be sure to account for the previous changes
function processCard(card) {

    // find url
    let url = card.querySelector('.itemCard__itemName a').href;
    let item = url.match(/\/item\/jdirectitems\/auction\/(.*)(\?.*)?/);
    if (item) { item = item[1] }

    let thumbnailNode = card.querySelector(".g-thumbnail__outer a");
    //thumbnailNode.href = "javascript:void(0);";

    if (!(item in itemsData)) {
        addCardToQueue(card, url, item);
        return;
    }

    let itemData = itemsData[item];
    let seller = itemData.seller;
    let sellerData = sellersData[seller];

    // update last seen
    itemData.lastSeen = Date.now();

    // clear old cruft
    card.querySelectorAll(".rg-node").forEach(node => { node.parentNode.removeChild(node); });
    clearInfoNodes(card);

    let statusNode = card.querySelector("ul.auctionSearchResult__statusList");
    let sellerNode = addSellerNode(card, sellerData);

    let timeLeftNode = card.querySelector(".itemCard__infoList").firstElementChild;
    timeLeftNode.querySelector(".g-title").innerText = "End Time";
    timeLeftNode.querySelector(".g-text").innerText = itemData.endTime;
    timeLeftNode.querySelector(".g-text").classList.remove("g-text--attention");

    addWatchersNode(card, itemData.watchNum);

    // link images

    if (itemData.images.length > 0) {
        let thumbnailNode = card.querySelector(".g-thumbnail__outer a");
        if (!(thumbnailNode.classList.contains("js-smartPhoto"))) {
            smartPhoto.hidePhoto();
            thumbnailNode.href = itemData.images[0];
            thumbnailNode.dataset.group = item;
            thumbnailNode.classList.add("js-smartPhoto");
            smartPhoto.addNewItem(thumbnailNode);
            thumbnailNode.href = "javascript:void(0);";

            let imageDiv = document.createElement("div");
            thumbnailNode.parentNode.appendChild(imageDiv);
            imageDiv.style.display = "none";
            itemData.images.slice(1).forEach( image => {
                let imgNode = document.createElement("a");
                imgNode.href = image;
                imgNode.dataset.group = item;
                imgNode.classList.add("js-smartPhoto");
                imageDiv.appendChild(imgNode);
                smartPhoto.addNewItem(imgNode);
            });
        }
    }


    if (sellerData.hide) {
        if (hideHidden) {
            // hide the card
            card.style.display = 'none';
            card.style.removeProperty('opacity');
            card.style.removeProperty('background-color');
        } else {
            // show with red background
            card.style.opacity = '0.9';
            card.style['background-color'] = '#ffbfbf';
            card.style.removeProperty('display');

            // add show link
            sellerNode.parentNode.appendChild(makeSellerStatusNode(seller,'Show'));
        }
    } else if (itemData.hide) {
        if (hideHidden) {
            // hide the card
            card.style.display = 'none';
            card.style.removeProperty('opacity');
            card.style.removeProperty('background-color');
        } else {
            // show with red background
            card.style.opacity = '0.9';
            card.style['background-color'] = '#ffbfbf';
            card.style.removeProperty('display');

            // add show/hide links
            sellerNode.parentNode.appendChild(makeSellerStatusNode(seller,'Hide'));
            statusNode.appendChild(makeItemStatusNode(item,card,'Show'));
        }
    } else {
        // unhide card
        card.style.removeProperty('opacity');
        card.style.removeProperty('background-color');
        card.style.removeProperty('order');
        card.style.removeProperty('display');

        // add hide links
        sellerNode.parentNode.appendChild(makeSellerStatusNode(seller,'Hide'));
        statusNode.appendChild(makeItemStatusNode(item,card,'Hide'));
    }
}

// process changes to all results cards in a given element
function processCardsIn(element, addPhotos) {
    element.querySelectorAll("ul.auctionSearchResult li.itemCard:not(.rg-loading)").forEach(card => {
        processCard(card);
    });

    serialiseData();
}

// move all results cards in a given element to the given element
// as soon as one visible card is moved, hide loadingElement
// add cards to observer for lazy loading
function moveCards(elementFrom, elementTo, loadingElement, observer) {
    var movedVisibleCard = (loadingElement === undefined) ? true : false;
    elementFrom.querySelectorAll("ul.auctionSearchResult li.itemCard").forEach(card => {
        // move the card
        elementTo.appendChild(card);

        // if we moved a visible card, hide the loading element
        if (!movedVisibleCard && card.style.display != "none") {
            movedVisibleCard = true;
            loadingElement.style.display = "none";
        }

        // add to lazy loading observer
        observer.observe(card.querySelector('img.lazyLoadV2'));

        // fix favourite star
        let watchButton = card.querySelector('div.watchButton');
        const id = watchButton.dataset.auctionId;

        if (buyeeWatchlist.includes(id)) {
            watchButton.classList.add("is-active");
            watchButton.firstElementChild.classList.remove("g-feather-star");
            watchButton.firstElementChild.classList.add("g-feather-star-active");
        } else {
            watchButton.classList.remove("is-active");
            watchButton.firstElementChild.classList.remove("g-feather-star-active");
            watchButton.firstElementChild.classList.add("g-feather-star");
        }

        watchButton.addEventListener("click", e => {
            e.preventDefault();
            e.stopImmediatePropagation();

            if (watchButton.classList.contains('is-active')) {
                fetch("https://buyee.jp/api/v1/watch_list/remove", {
                    "credentials": "include",
                    "headers": {
                        "X-Requested-With": "XMLHttpRequest",
                        "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8"
                    },
                    "method": "POST",
                    "body": `auctionId=${id}`});
                buyeeWatchlist.splice(buyeeWatchlist.indexOf(id), 1);
                watchButton.classList.remove("is-active");
                watchButton.firstElementChild.classList.remove("g-feather-star-active");
                watchButton.firstElementChild.classList.add("g-feather-star");
            } else {
                fetch("https://buyee.jp/api/v1/watch_list/add", {
                    "credentials": "include",
                    "headers": {
                        "X-Requested-With": "XMLHttpRequest",
                        "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8"
                    },
                    "method": "POST",
                    "body": `auctionId=${id}&buttonType=search&isSignupRedirect=false`});
                buyeeWatchlist.push(id);
                watchButton.classList.add("is-active");
                watchButton.firstElementChild.classList.remove("g-feather-star");
                watchButton.firstElementChild.classList.add("g-feather-star-active");
            }
        });

    });
}

// find the URL for the next page of search results after the given one
function nextPageURL(url) {
    var newURL = new URL(url);
    var currentPage = newURL.searchParams.get('page') ?? '1';
    newURL.searchParams.delete('page');
    newURL.searchParams.append('page', parseInt(currentPage) + 1);
    return newURL;
}

// check that the given HTML document is not the last page of results
function notLastPage(doc) {
    if (isDesktop) {
        let button = doc.querySelector("div.page_navi a:nth-last-child(2)");
        return (button && button.innerText === ">");
    } else {
        let button = doc.querySelector("li.page--arrow:nth-last-child(2)");
        return (button != null);
    }
}

// the main function
function buyeeSellerFilter () {

    // initial load of data
    unSerialiseData();

    // refresh watchlist

    fetch("https://buyee.jp/api/v1/watch_list/find", {
        credentials: "include",
        headers: { "X-Requested-With": "XMLHttpRequest" }
    })
    .then(response => response.json())
    .then(data => {
        buyeeWatchlist = data.data.list;
        1});

    // reload data when tab regains focus
    document.addEventListener("visibilitychange", () => {
        if (!document.hidden) {
            // don't change hideHidden, so save its value first
            let hideHiddenSaved = hideHidden;
            unSerialiseData();
            hideHidden = hideHiddenSaved;
            processCardsIn(document, false);
        }
    });

    if (!isDesktop) {
        // disable the google translate popup (annoying with show/hide buttons)
        var style = `
#goog-gt-tt, .goog-te-balloon-frame{display: none !important;}
.goog-text-highlight { background: none !important; box-shadow: none !important;}
`;
        var styleSheet = document.createElement("style");
        styleSheet.innerText = style;
        document.head.appendChild(styleSheet);
    }

    let container = document.querySelector('.g-main:not(.g-modal)');
    let resultsNode = container.children[0];

    // sometimes the results are broken into two lists of ten; if so, merge them.
    if (container.children.length > 1) {
        container.children[1].querySelectorAll("ul.auctionSearchResult li.itemCard").forEach(card => {
            resultsNode.appendChild(card);
        });
        container.children[1].style.display = "none";
    }

    // make link to show or hide hidden results
    let optionsLink = document.createElement("a");
    optionsLink.href = "javascript:void(0);";
    optionsLink.id = "rg-show-hide-link";
    optionsLink.innerText = hideHidden ? "Show hidden" : "Hide hidden";
    optionsLink.onclick = (function() {
        hideHidden = !hideHidden;
        serialiseData();
        optionsLink.innerText = hideHidden ? "Show hidden" : "Hide hidden";
        processCardsIn(document, false);
    });
    optionsLink.style.display = 'inline-block';
    optionsLink.style.width = '110px';

    // put link in the search options bar
    let optionsNode = document.createElement("span");
    optionsNode.classList.add('result-num');
    if (isDesktop) {
        optionsNode.style.left = '20%';
    }
    optionsNode.appendChild(optionsLink);

    if (isDesktop) {
        document.querySelector(".result-num").parentNode.appendChild(optionsNode);
    } else {
        optionsNode.style.display = 'inline';
        document.querySelector(".result-num").appendChild(optionsNode);
    }

    // perform initial processing of cards
    processCardsIn(document, true);

    // image lazy loader
    const imageObserver = new IntersectionObserver(loadImage);

    function loadImage(entries, observer) {
        entries.forEach(entry => {
            if (!entry.isIntersecting) {
                return;
            }

            const target = entry.target;
            target.src = target.getAttribute('data-src');
            target.removeAttribute('data-src');
            target.style.background = '';

            observer.unobserve(target);
        });
    }

    // load subsequent pages of results if navi bar on screen
    var currentURL = document.location;
    var currentPage = document;
    var naviOnScreen = false;

    const loadingNode = makeLoadingNode();

    function loadPageLoop() {
        // code to add further pages of results; loop over pages,
        // with a minimum 50ms delay between to avoid rampaging
        // robot alerts (unlikely as the loads are pretty slow)
        // stop if the navi bar is no longer on screen (so don't load infinitely)
        setTimeout(() => {
            if (naviOnScreen && notLastPage(currentPage)) {
                // display loading node
                resultsNode.appendChild(loadingNode);
                loadingNode.style.removeProperty('display');

                // get next page of results
                currentURL = nextPageURL(currentURL);
                fetchURL(currentURL)
                .then((page) => {
                    currentPage = page;
                    processCardsIn(currentPage, true);
                    console.log("Moving on up");
                    moveCards(currentPage,resultsNode, loadingNode, imageObserver);
                    loadPageLoop();
                });
            } else if (naviOnScreen) {
                // finished loading pages, hide loading node
                loadingNode.style.display = 'none';
            }
        }, 100);
    }

    // function to handle navi bar appearing/disappearing from screen
    function handleIntersection(entries) {
        entries.map((entry) => {
            naviOnScreen = entry.isIntersecting
        });
        if (naviOnScreen) { loadPageLoop(); }
    }

    // 540 px bottom margin so that next page loads a bit before navi bar appears on screen
    const loadObserver = new IntersectionObserver(handleIntersection, { rootMargin: "0px 0px 540px 0px" });

    if (isDesktop) {
        loadObserver.observe(document.querySelector("div.page_navi"));
    } else {
        loadObserver.observe(document.querySelector("ul.pagination"));
    }

    // clean up old blacklisted items
    cleanItems();
}

// stuff to handle loading stage of page


if (document.querySelector('.g-main:not(.g-modal)')) {
    buyeeSellerFilter();
} else {
    const startupObserver = new MutationObserver(() => {
        if (document.querySelector('.g-main:not(.g-modal)')) {
            startupObserver.disconnect();
            buyeeSellerFilter();
        }
    });

    startupObserver.observe(document.body, { childList: true, subtree: true });
}