bgm.tv条目页系列索引

在番剧条目下添加系列索引

// ==UserScript==
// @name         bgm.tv条目页系列索引
// @namespace    https://xizo.xyz/
// @version      1.0
// @description  在番剧条目下添加系列索引
// @author       Xizo
// @match        http://bgm.tv/subject/*
// @match        http://bangumi.tv/subject/*
// @match        https://bgm.tv/subject/*
// @match        https://bangumi.tv/subject/*
// @icon         https://bgm.tv/img/favicon.ico
// @license      MIT
// ==/UserScript==

const SeasonListLocalStorageOpenedKey = 'seasonlist-opended';

async function fetchSubjectInfo(subjectId) {
    const response = await fetch(`https://api.bgm.tv/v0/subjects/${subjectId}`);
    const data = await response.json();
    
    const platform = data.platform;
    if (platform !== 'TV' && platform !== '剧场版') {
        return [];
    }

    return {
        id: data.id,
        name: data.name,
        name_cn: data.name_cn || '',
        largeImage: data.images?.large || ''
    };
}

async function fetchRelatedSubjects(subjectId) {
    const response = await fetch(`https://api.bgm.tv/v0/subjects/${subjectId}/subjects`);
    return await response.json();
}

function findMinIdByRelation(items, relation) {
    const filtered = items.filter(item => item.relation === relation);
    if (filtered.length === 0) return null;
    return filtered.reduce((min, curr) => curr.id < min.id ? curr : min, filtered[0]);
}

async function collectSeries(initialId) {
    try {
        const initialInfo = await fetchSubjectInfo(initialId);
        const result = [initialInfo];

        let currentId = initialId;
        const predecessors = [];
        while (true) {
            const items = await fetchRelatedSubjects(currentId);
            const pre = findMinIdByRelation(items, '前传');
            if (!pre) break;
            
            predecessors.unshift({
                id: pre.id,
                name: pre.name,
                name_cn: pre.name_cn || '',
                largeImage: pre.images.common
            });
            currentId = pre.id;
        }
        result.unshift(...predecessors);

        currentId = initialId;
        const successors = [];
        while (true) {
            const items = await fetchRelatedSubjects(currentId);
            const seq = findMinIdByRelation(items, '续集');
            if (!seq) break;
            
            successors.push({
                id: seq.id,
                name: seq.name,
                name_cn: seq.name_cn || '',
                largeImage: seq.images.common
            });
            currentId = seq.id;
        }
        result.push(...successors);

        return result;

    } catch (error) {
        console.error("Error:", error);
        return [];
    }
}

(function(htmlFunc) {
    const match = location.pathname.match(/^\/subject\/(\d+)(\?|$)/);
    if (!match) return;
    const bangumiId = +match[1];

    collectSeries(bangumiId)
        .then(series => {
            const htmlText = htmlFunc({ series });

            let el = document.createElement('div');
            el.innerHTML = htmlText;
            el = el.children[0];

            if (document.querySelector('.subject_section.clearit.anitabi-bangumi-inset-section-box')) {
                let sectionOne = document.querySelector('.subject_section.clearit.anitabi-bangumi-inset-section-box');
                sectionOne.parentNode.insertBefore(el, sectionOne.nextSibling);
            } else {
                let sectionOne = document.querySelector('.subject_section:nth-child(3)');
                if (!sectionOne) sectionOne = document.querySelector('.subject_section');
                if (!sectionOne) return;
                sectionOne.parentNode.insertBefore(el, sectionOne);
            }

            el.querySelector('h2').onclick = () => {
                let opened = el.getAttribute('data-opened') === 'true';
                opened = !opened;
                el.setAttribute('data-opened', opened);
                localStorage.setItem(SeasonListLocalStorageOpenedKey, opened);
            };
            el.setAttribute('data-opened', localStorage.getItem(SeasonListLocalStorageOpenedKey) || 'true');

            $('.seasonlist-bangumi-inset-section-box *[title]').tooltip({
                offset: 0
            });
        })
        .catch(err => console.error(err));
})(({
    series,
}) => `
<div class="subject_section clearit seasonlist-bangumi-inset-section-box">
    <style>
    .seasonlist-bangumi-inset-section-box{
        overflow: hidden;
        --seasonlist-point-cover-width: 142px;
        --seasonlist-point-cover-height: 80px;
        --seasonlist-point-margin: 8px;
    }
    .seasonlist-section-head{
        line-height: 28px;
        padding-bottom: 5px;
    }
    .seasonlist-section-head .chiiBtn{
        margin: 0;
    }
    .seasonlist-section-head h2{
        line-height: inherit;
        cursor: pointer;
    }
    .seasonlist-section-head h2 .fold-btn{
        font-weight: 400;
        font-size: 12px;
        line-height: 24px;
        margin: 2px 0 0 4px;
        display: inline-block;
        vertical-align: top;
        opacity: 0.8;
        background: rgba(128,128,128,.2);
        padding: 0 10px;
        border-radius: 20px;
    }
    .seasonlist-section-head h2 .fold-btn:before{
        content: '显示';
    }
    .seasonlist-bangumi-inset-section-box[data-opened="true"] .seasonlist-section-head h2 .fold-btn:before{
        content: '折叠';
    }
    .seasonlist-bangumi-inset-section-box[data-opened="false"] .seasonlist-bangumi-info-box{
        display: none;
    }
    .seasonlist-point-info-box{
        line-height: 16px;
    }
    .seasonlist-point-info-box span{
        opacity: .5;
    }
    .seasonlist-point-info-box .ep{
        margin-right: 2px;
    }
    .seasonlist-point-info-box .s{
        opacity: .7;
    }
    </style>
    <div class="seasonlist-section-head clearit">
        <h2 class="subtitle ll">
            系列作品
            <a class="fold-btn"></a>
        </h2>
    </div>
    <div class="seasonlist-bangumi-info-box">
        <div class="content_inner">
            <ul class="browserCoverMedium clearit">
                ${series.map(item => `
                    <li style="height: 160px;">
                        <a href="/subject/${item.id}" title="${item.name_cn}" class="avatar thumbTip" data-original-title="${item.name_cn}">
                            <span class="avatarNeue avatarSize75" style="background-image:url('${item.largeImage.slice(6)}'); filter: blur(0.5px)"></span>
                        </a>
                        <a href="/subject/${item.id}" class="title" style="height: 90px !important">${item.name}</a>
                    </li>
                `).join('')}
            </ul>
        </div>
    </div>
</div>`);