Greasy Fork

来自缓存

查询bangumi贴中用户观看状态

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

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