Greasy Fork

Greasy Fork is available in English.

查询bangumi贴中用户观看状态

查询贴中所有用户观看某部作品的状态

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         查询bangumi贴中用户观看状态
// @namespace    http://tampermonkey.net/
// @version      1.5.7
// @description  查询贴中所有用户观看某部作品的状态
// @author       Hirasawa Yui
// @run-at       document-idle
// @match        https://bangumi.tv/group/topic/*
// @match        https://bangumi.tv/subject/topic/*
// @match        https://bangumi.tv/blog/*
// @match        https://bangumi.tv/ep/*
// @match        https://bgm.tv/group/topic/*
// @match        https://bgm.tv/subject/topic/*
// @match        https://bgm.tv/blog/*
// @match        https://bgm.tv/ep/*
// @match        https://chii.in/group/topic/*
// @match        https://chii.in/subject/topic/*
// @match        https://chii.in/blog/*
// @match        https://chii.in/ep/*
// @match        https://bangumi.tv/subject_search/*
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_addStyle
// @license      MIT
// ==/UserScript==

(function() {
    'use strict';

    let colorful = true; // 采用五彩配色方案
    let userInfoMap = new Map(); // To store results for each user ID
    let validSubjectId = false; // Flag to track if the subject ID is valid
    let autoload = false;
    let stats = {
        "want": 0,
        "norate": 0,
        "geq7": 0,
        "leq6": 0,
        "watching": 0,
        "postpone": 0,
        "gaveup": 0,
        "nowatch": 0
    }; // stats for the whole discussion board

    const ongoingFetches = new Set(); // Lock set to keep track of ongoing fetches

    // clear stats for new query
    function clearStats() {
      for (let key in stats) {
          stats[key] = 0;
      }
    }

    // Function to create or update an element with the fetched data
    function insertOrUpdateAdjacentToAnchor(anchor, siblingClass, userId, subjectId) {
        let key = `${userId}-${subjectId}`;
        let text = userInfoMap.get(key) || "No info available";

        // Find existing element or create new one
        let newElement = anchor.parentNode.querySelector('.userData');
        if (!newElement) {
            newElement = anchor.parentNode.parentNode.querySelector('.userData');
        }
        if (!newElement) {
            newElement = document.createElement('span');
            newElement.className = 'userData'; // Assign a class for easy identification
            let inserted = false;
            let nextElement = anchor.parentNode.nextSibling;
            while (nextElement) {
                if (nextElement.nodeType === 1 && nextElement.matches(siblingClass)) {
                    nextElement.parentNode.insertBefore(newElement, nextElement.nextSibling);
                    inserted = true;
                    break;
                }
                nextElement = nextElement.nextSibling;
            }
            if (!inserted) {
                anchor.parentNode.insertBefore(newElement, anchor.nextSibling);
            }
        }

        // Update text content and color
        newElement.textContent = `【${text}】`;
        newElement.style.fontSize = '12px';
        if (colorful) {
            let type = getDataFromInfoText(text).type;
            if (type === 1) { // 想看
                newElement.style.color = '#eea1cd';
            } else if (type === 2) { // 看过
                newElement.style.color = '#3ac657';
            } else if (type === 3) { // 在看
                newElement.style.color = '#24a2e6';
            } else if (type === 4) { // 搁置
                newElement.style.color = '#bf7d1f';
            } else if (type === 5) { // 抛弃
                newElement.style.color = '#b22f9c';
            } else {
                newElement.style.color = '#999';
            }
        } else {
            newElement.style.color = '#999';
        }
        newElement.style.fontWeight = 'bold';
    }

    // Fetch collection data for a specific user ID and subject ID
    async function fetchUserInfo(userId, subjectId) {
      const userSubjectKey = `${userId}-${subjectId}`;
      const url = `https://api.bgm.tv/v0/users/${userId}/collections/${subjectId}`;

      // Skip fetching if the user is already in the map with the same subjectId or subject ID is invalid
      if (!validSubjectId || userInfoMap.has(userSubjectKey) || ongoingFetches.has(userSubjectKey)) return;
      console.log(userInfoMap);

      // Add to ongoing fetches
      ongoingFetches.add(userSubjectKey);

      try {
          const response = await fetch(url);

          if (!response.ok) {
              userInfoMap.set(userSubjectKey, "TA未看过/未公开收藏该作");
              stats.nowatch += 1;
              // Remove from ongoing fetches
              ongoingFetches.delete(userSubjectKey);
              return;
          }

          const data = await response.json();

            // update stats
            switch (data.type) {
                case 1:
                    stats.want += 1;
                    break;
                case 2:
                    if (data.rate) {
                        if (data.rate >= 7) stats.geq7 += 1;
                        else stats.leq6 += 1;
                    } else {
                        stats.norate += 1;
                    }
                    break;
                case 3:
                    stats.watching += 1;
                    break;
                case 4:
                    stats.postpone += 1;
                    break;
                case 5:
                    stats.gaveup += 1;
                    break;
            }

            let infoText = getInfoTextFromData(data);
            userInfoMap.set(userSubjectKey, infoText);
        } catch (error) {
            console.error('Error fetching or processing data', error);
            userInfoMap.set(userSubjectKey, "Error fetching data");
        } finally {
            // Remove from ongoing fetches in finally block to ensure it's always cleaned up
            ongoingFetches.delete(userSubjectKey);
        }
    }

    // Convert fetched data to a user-friendly text
    function getInfoTextFromData(data) {
        if (data.type === 1) return "TA想看这部作品";
        else if (data.type === 2) return data.rate ? `TA打了${data.rate}分` : 'TA看过这部作品';
        else if (data.type === 3) return data.ep_status ? `TA看到了${data.ep_status}集` : 'TA在看这部作品';
        else if (data.type === 4) return "TA搁置了这部作品";
        else if (data.type === 5) return "TA抛弃了这部作品";
        else return "未知状态";
    }

    // convert text back to data type
    function getDataFromInfoText(text) {
        let data = { type: 0, rate: null, ep_status: null }; // default unknown data

        if (text === "TA想看这部作品") {
            data.type = 1;
        } else if (text.startsWith("TA打了") && text.endsWith("分")) {
            data.type = 2;
            data.rate = parseFloat(text.slice(3, -1)); // Extracting number from "TA打了X分"
        } else if (text.startsWith("TA看到了") && text.endsWith("集")) {
            data.type = 3;
            data.ep_status = parseInt(text.slice(4, -1)); // Extracting number from "TA看到了X集"
        } else if (text === "TA搁置了这部作品") {
            data.type = 4;
        } else if (text === "TA抛弃了这部作品") {
            data.type = 5;
        } else if (text === 'TA看过这部作品') {
            data.type = 2; // Assuming this corresponds to having seen the work without a rating
        } else if (text === 'TA在看这部作品') {
            data.type = 3; // Assuming this corresponds to watching the work without a specific episode status
        } else {
            // Unknown status, data.type remains 0
        }

        return data;
    }

    // generate stats summary text
    function generateSummary() {
        // Calculate the total of all categories
        let total = stats.want + stats.norate + stats.geq7 + stats.leq6 + stats.watching + stats.postpone + stats.gaveup + stats.nowatch;

        // Select the paragraph element
        let p = document.querySelector('#statsSummary');

        // Function to calculate percentage
        const calcPercent = (value, total) => (total > 0 ? (value / total * 100).toFixed(1) : 0);

        // Create an array of category objects with name, count, and percent
        let categories = [
            { name: "想看这部作品", count: stats.want },
            { name: "看过,未打分", count: stats.norate },
            { name: "打了7分及以上", count: stats.geq7 },
            { name: "打了6分及以下", count: stats.leq6 },
            { name: "正在看这部作品", count: stats.watching },
            { name: "搁置了这部作品", count: stats.postpone },
            { name: "抛弃了这部作品", count: stats.gaveup },
            { name: "未看过/未公开收藏这部作品", count: stats.nowatch }
        ].map(category => ({
            ...category,
            percent: calcPercent(category.count, total)
        }));

        // Sort categories by percent, descending
        categories.sort((a, b) => b.percent - a.percent);

        // Constructing the inner HTML from the sorted categories
        // Prepend the total count line
        let totalPeopleLine = `总计参与人数: ${total}人<br>`;
        p.innerHTML = totalPeopleLine + categories.map(category =>
            `${category.percent}%(${category.count}人)的人${category.name}`).join('<br>');
    }

    // Fetch subject info by subject ID
    async function fetchSubjectInfo(subjectId) {
        const url = `https://api.bgm.tv/v0/subjects/${subjectId}`;
        try {
            const response = await fetch(url);
            const div = document.querySelector('.subjectInfo');

            if (!response.ok) {
                div.textContent = "无效条目";
                validSubjectId = false; // Mark subject ID as invalid
                userInfoMap.clear(); // Clear previous info if any
                return;
            }

            const data = await response.json();
            div.textContent = `你当前正在查询所有用户观看 ${data.name} 的状态`;
            div.style.color = 'green';
            validSubjectId = true; // Mark subject ID as valid
        } catch (error) {
            console.error('Error fetching subject data', error);
        }
    }

    // insert input elements below the specified div
    function insertInputElements(postTopicDiv) {
        if (postTopicDiv) {
            const placeholder = document.createElement('div');
            const title = document.createElement('h3');
            title.textContent = '查询观看状态';
            const input = document.createElement('input');
            input.type = 'text';
            input.className = 'searchInputL';
            input.placeholder = '输入条目ID';
            input.style.maxWidth = '200px';
            const button = document.createElement('button');
            button.type = 'button';
            button.textContent = '获取信息';

            button.style.fontSize = '13px';
            button.style.border = 'none';
            button.style.background = '#4EB1D4';
            button.style.color = '#FFF';
            button.style.padding = '6px 15px';
            button.style.borderRadius = '5px';
            button.style.cursor = 'pointer';

            const div = document.createElement('div');
            div.className = 'subjectInfo'; // For displaying subject info
            div.id = 'current_queried_subject_info';

            // Create a paragraph element for stats summary
            let statsSummary = document.createElement('p');
            statsSummary.id = 'statsSummary';

            button.onclick = async function() {
                const subjectId = input.value.trim();
                if (!subjectId) return; // Do nothing if the subject ID is empty
                clearStats();
                await fetchSubjectInfo(subjectId);
                if (validSubjectId) { // Only process users if the subject ID is valid
                    processAllUsers(subjectId);
                }
            };

            // Create a checkbox
            let checkbox = document.createElement('input');
            checkbox.type = 'checkbox';
            checkbox.checked = autoload; // Set the initial state of the checkbox
            let check_tag = document.createElement('span');
            check_tag.textContent = '自动加载';

            // Change listener for the checkbox
            checkbox.addEventListener('change', function() {
                autoload = checkbox.checked; // Update the switch variable
                GM_setValue('autoload', autoload); // Save the new state
            });

            const quickSearch = document.createElement('a');
            quickSearch.href = 'https://bangumi.tv/subject_search';
            quickSearch.textContent = '快速查询条目ID';
            quickSearch.style.color = 'pink';
            quickSearch.cursor = 'pointer';
            quickSearch.target = '_blank';

            const container = document.createElement('div');
            container.appendChild(placeholder);
            container.appendChild(title);
            container.appendChild(input);
            container.appendChild(button);
            container.appendChild(checkbox);
            container.appendChild(check_tag);
            container.appendChild(quickSearch);
            container.appendChild(div); // Append the div for subject info
            container.appendChild(statsSummary);
            postTopicDiv.appendChild(container);
        }
    }
    // Initialize input box and button, and handle click event
    async function init() {
        autoload = GM_getValue('autoload', false);
        let container = document.querySelector('#columnInSubjectB');
        if (!container) {
            container = document.querySelector('#columnB');
        }
        if (!container) {
            container = document.querySelector('#columnEpB');
        }
        insertInputElements(container);

        // auto load watching status for all users on page 'ep' and 'subject/topic'
        if (autoload && ['https://bangumi.tv/subject/topic/',
             'https://bgm.tv/subject/topic/',
             'https://chii.in/subject/topic/',
             'https://bangumi.tv/ep/',
             'https://bgm.tv/ep/',
             'https://chii.in/ep'].some(prefix => document.URL.startsWith(prefix))) {

            let anchor = document.querySelector('#subject_inner_info .avatar');
            const subjectId = anchor.href.split('/subject/')[1].split('/')[0];
            clearStats();
            await fetchSubjectInfo(subjectId);
            processAllUsers(subjectId);
        }

        // search page
        if (document.URL.startsWith('https://bangumi.tv/subject_search/')){
            const anchorElements = document.querySelectorAll('a.l');
            anchorElements.forEach(anchor => {
                if(anchor.href.includes('/subject/')) {
                    const subjectId = anchor.href.split('/subject/')[1].split('/')[0];
                    let newElement = document.createElement('span');
                    newElement.style.cursor = 'pointer';

                    // Add an event listener for the click event
                    newElement.addEventListener('click', function() {
                        // Copy text to clipboard logic
                        navigator.clipboard.writeText(newElement.textContent.replace(/^\(|\)$/g, '')).then(() => {
                            console.log('Text copied to clipboard');
                        }).catch(err => {
                            console.error('Error in copying text: ', err);
                        });
                    });

                    newElement.textContent = `(${subjectId})`;
                    newElement.style.color = 'red';
                    newElement.style.fontWeight = 'bold';
                    anchor.parentNode.insertBefore(newElement, anchor.nextSibling);
                }
            });
        }
        // blog page
        if (['https://bangumi.tv/blog/',
        'https://bgm.tv/blog/',
        'https://chii.in/blog/'].some(prefix => document.URL.startsWith(prefix))){
            let anchorElements = document.querySelectorAll('#related_subject_list .ll .avatar');
            anchorElements.forEach(anchor => {
                if(anchor.href.includes('/subject/')) {
                    const subjectId = anchor.href.split('/subject/')[1].split('/')[0];
                    let newElement = document.createElement('p');
                    newElement.style.cursor = 'pointer';

                    // Add an event listener for the click event
                    newElement.addEventListener('click', function() {
                        // Copy text to clipboard logic
                        navigator.clipboard.writeText(newElement.textContent.replace(/^\(|\)$/g, '')).then(() => {
                            console.log('Text copied to clipboard');
                        }).catch(err => {
                            console.error('Error in copying text: ', err);
                        });
                    });

                    newElement.textContent = `(${subjectId})`;
                    newElement.style.color = 'red';
                    newElement.style.fontWeight = 'bold';
                    anchor.parentNode.insertBefore(newElement, anchor.nextSibling);
                }
            });
        }

    }

    // Fetch user info for all users and then process anchor tags
    async function processAllUsers(subjectId) {
        const anchorElements = document.querySelectorAll('a.l');
        let fetchPromises = [];

        anchorElements.forEach(anchor => {
            if(anchor.href.includes('/user/')) {
                const userId = anchor.href.split('/user/')[1].split('/')[0];
                if (!userInfoMap.has(`${userId}-${subjectId}`)) {
                    fetchPromises.push(fetchUserInfo(userId, subjectId));
                }
            }
        });

        await Promise.all(fetchPromises);

        anchorElements.forEach(anchor => {
            if(anchor.href.includes('/user/')) {
                const userId = anchor.href.split('/user/')[1].split('/')[0];
                insertOrUpdateAdjacentToAnchor(anchor, 'span.sign.tip_j', userId, subjectId);
            }
        });

        generateSummary();

    }

    init(); // Initialize and append input box and button
})();