班固米日志列表显示相关条目

在 Bangumi 用户页面显示相关作品评价,处理封面图片,显示加载圈,并支持缓存和重试

// ==UserScript==
// @name         班固米日志列表显示相关条目
// @namespace    https://bgm.tv/group/topic/417756
// @version      0.1
// @description  在 Bangumi 用户页面显示相关作品评价,处理封面图片,显示加载圈,并支持缓存和重试
// @author       odeepseeko
// @match        https://bgm.tv/user/*
// @match        https://bgm.tv/user/*/blog*
// @grant        GM_xmlhttpRequest
// @license      MIT
// ==/UserScript==

(function () {
    'use strict';

    // 最大并发请求数
    const MAX_CONCURRENT_REQUESTS = 3;

    // 请求队列
    const requestQueue = [];
    let activeRequests = 0;

    // 获取所有博客条目
    const entries = document.querySelectorAll('#entry_list .item.clearit');

    // 加载圈样式
    const loaderStyle = `
        <style>
            .loader {
                margin-left: 5px;
                border: 2px solid transparent;
                border-top: 2px solid #F09199;
                border-radius: 50%;
                width: 10px;
                height: 10px;
                animation: spin 2s linear infinite;
                display: block;
            }
            @keyframes spin {
                0% { transform: rotate(0deg); }
                100% { transform: rotate(360deg); }
            }
            .evaluation {
                color: #999;
                padding-left: 5px;
                padding-top: 5px;
            }
            .retry {
                color: #999;
                cursor: pointer;
                padding: 0;
                padding-left: 5px;
                padding-top: 5px;
                background: none;
                border: none;
            }
        </style>
    `;
    document.head.insertAdjacentHTML('beforeend', loaderStyle);

    function processRequest(entry, entryID) {
        activeRequests++;

        const loader = document.createElement('span');
        loader.className = 'loader';
        entry.querySelector('.title').appendChild(loader);

        const cacheKey = `entry_${entryID}`;
        const cachedData = sessionStorage.getItem(cacheKey);

        if (cachedData) {
            const data = JSON.parse(cachedData);
            updateEntry(entry, entryID, data);
            loader.remove();
            activeRequests--;
            processNextRequest();
            return;
        }

        GM_xmlhttpRequest({
            method: 'GET',
            url: `https://next.bgm.tv/p1/blogs/${entryID}/subjects`,
            onload: function (response) {
                if (response.status === 200) {
                    try {
                        const data = JSON.parse(response.responseText);

                        updateEntry(entry, entryID, data);
                        sessionStorage.setItem(cacheKey, JSON.stringify(data));
                    } catch (error) {
                        console.error(entryID, ' 解析响应失败: ', error);
                        showError(entry, entryID);
                    }
                } else {
                    console.error(entryID, ' 请求失败,状态码: ', response.status);
                    showError(entry, entryID);
                }

                loader.remove();
                activeRequests--;
                processNextRequest();
            },
            onerror: function (error) {
                console.error(entryID, ' 请求失败: ', error);
                showError(entry, entryID);
                loader.remove();
                activeRequests--;
                processNextRequest();
            }
        });
    }

    function updateEntry(entry, entryID, data) {
        if (!data.length) return;
        const subjects = data.map(subject => {
            const name = subject.nameCN || subject.name;
            const url = `https://bgm.tv/subject/${subject.id}`;
            return `<a class="l" href="${url}">${name}</a>`;
        }).join('、');

        const evaluationHTML = `<div class="evaluation">${subjects} 的评价</div>`;

        const timeDiv = entry.querySelector('div.time');
        if (timeDiv) {
            timeDiv.insertAdjacentHTML('beforebegin', evaluationHTML);
        }

        if (data.length === 1 && data[0].images && data[0].images.large) {
            const subject = data[0];
            const coverDiv = entry.querySelector('.cover.ll');
            const largeImageUrl = subject.images.large;

            if (!coverDiv) {
                // 如果没有封面,创建封面和 div.entry
                entry.querySelector('.loader')?.remove();
                const content = entry.innerHTML;
                entry.innerHTML = /* HTML */`
                    <p class="cover ll">
                        <a href="/blog/${entryID}" title="${entry.querySelector('.title a').textContent}">
                            <span class="pictureFrameGroup">
                                <span class="image"><img src="${largeImageUrl}"></span>
                                <span class="overlay"></span>
                            </span>
                        </a>
                    </p>
                    <div class="entry">
                        ${content}
                    </div>
                `;
            } else {
                // 如果有封面,检查是否需要替换图片
                const img = coverDiv.querySelector('img');
                if (img && img.src.endsWith('no_photo.png')) {
                    img.src = largeImageUrl;
                }
            }
        }
    }

    function showError(entry, entryID) {
        const errorHTML = `<button class="retry" onclick="retryLoad('${entryID}')">获取关联失败,点击重试</button>`;
        entry.querySelector('.title').insertAdjacentHTML('beforeend', errorHTML);
    }

    window.retryLoad = function (entryID) {
        const entry = document.querySelector(`.item.clearit a[href*="/blog/${entryID}"]`)?.closest('.item.clearit');
        if (entry) {
            requestQueue.push({ entry, entryID });
            processNextRequest();
        }
    };

    function processNextRequest() {
        if (requestQueue.length > 0 && activeRequests < MAX_CONCURRENT_REQUESTS) {
            const { entry, entryID } = requestQueue.shift();
            processRequest(entry, entryID);
        }
    }

    entries.forEach(entry => {
        const titleLink = entry.querySelector('.title a');
        if (titleLink && titleLink.href.includes('/blog/')) {
            const entryID = titleLink.href.split('/').pop();
            requestQueue.push({ entry, entryID });
        }
    });

    for (let i = 0; i < MAX_CONCURRENT_REQUESTS; i++) {
        processNextRequest();
    }
})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址