From 2e7c49f7fc3c5538ad8b1cbd0c99f70d2aa61706 Mon Sep 17 00:00:00 2001 From: Stardream Date: Wed, 22 Apr 2026 03:13:12 +1000 Subject: [PATCH] feat(Bangumi_Topic_Share): add API-based episode page support with cover image, episode name, and subject tag - Fetch episode data via /v0/episodes/{id} to get episode name (name_cn fallback to name) - Fetch subject data via /v0/subjects/{subject_id} for cover image and subject name - Display subject cover using person-page card layout; badge shows episode number - Subject name used as tag; cache prevents duplicate API calls - Bump version to 5.2 Co-Authored-By: Claude Sonnet 4.6 --- js/Bangumi_Topic_Share.js | 55 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 53 insertions(+), 2 deletions(-) diff --git a/js/Bangumi_Topic_Share.js b/js/Bangumi_Topic_Share.js index f4e4a0c..d0f1809 100644 --- a/js/Bangumi_Topic_Share.js +++ b/js/Bangumi_Topic_Share.js @@ -1,7 +1,7 @@ // ==UserScript== // @name Bangumi Topic Share // @namespace http://tampermonkey.net/ -// @version 5.1 +// @version 5.2 // @description Bangumi 话题/日志分享工具:生成分享卡片,支持图片复制/下载、一键复制分享文案、可选 AI 标签 // @author Stardream // @contributor Chang ji, Mewtw0 @@ -166,8 +166,18 @@ const contentWin = contentDoc.defaultView || window; const pathname = contentWin.location.pathname; const isBlog = /\/blog\/\d+/.test(pathname); + const isEpisode = /\/ep\/\d+/.test(pathname); const isCharacter = /\/character\/\d+|\/rakuen\/topic\/crt\/\d+/.test(pathname); const isPerson = /\/person\/\d+|\/rakuen\/topic\/prsn\/\d+/.test(pathname); + if (isEpisode) { + const epIdMatch = pathname.match(/\/ep\/(\d+)/); + const replyCount = contentDoc.querySelectorAll('[id^="post_"]').length; + if (epIdMatch) { + const epData = await fetchEpisodeData(epIdMatch[1]); + if (epData?.subjectName) return [epData.subjectName, `${replyCount} 回复`]; + } + return [`${replyCount} 回复`]; + } if (isBlog) { const subjectNames = [...new Set( [...contentDoc.querySelectorAll('a')] @@ -624,16 +634,35 @@ return { imageUrl, name, badgeLabel, id, type }; } + const _episodeDataCache = {}; + async function fetchEpisodeData(episodeId) { + if (_episodeDataCache[episodeId]) return _episodeDataCache[episodeId]; + const ep = await fetchBangumiAPI(`episodes/${episodeId}`); + if (!ep) return null; + const subject = await fetchBangumiAPI(`subjects/${ep.subject_id}`); + const result = { + episodeName: ep.name_cn || ep.name || '', + subjectName: subject?.name_cn || subject?.name || '', + subjectImageUrl: subject?.images?.common || subject?.images?.medium || '', + epNumber: ep.ep + }; + _episodeDataCache[episodeId] = result; + return result; + } + async function createShareImage(contentDoc = document) { const dark = contentDoc.documentElement.getAttribute('data-theme') === 'dark'; const contentWin = contentDoc.defaultView || window; const isBlog = /\/blog\/\d+/.test(contentWin.location.pathname); + const isEpisode = /\/ep\/\d+/.test(contentWin.location.pathname); const isCharacter = /\/character\/\d+|\/rakuen\/topic\/crt\/\d+/.test(contentWin.location.pathname); const isPerson = /\/person\/\d+|\/rakuen\/topic\/prsn\/\d+/.test(contentWin.location.pathname); let username, postTime, avatarUrl, contentEl; - if (isBlog) { + if (isEpisode || isCharacter || isPerson) { + username = ""; postTime = ""; avatarUrl = ""; contentEl = null; + } else if (isBlog) { const authorLink = contentDoc.querySelector('.author.user-card .title p a') || contentDoc.querySelector('.author.user-card a.avatar'); username = authorLink ? authorLink.textContent.trim() : "未知用户"; @@ -678,6 +707,17 @@ if (!badgeLabel || badgeLabel === '人物') badgeLabel = apiData.badgeLabel; } } + if (isEpisode) { + const epIdMatch = contentWin.location.pathname.match(/\/ep\/(\d+)/); + if (epIdMatch) { + const epData = await fetchEpisodeData(epIdMatch[1]); + if (epData) { + if (epData.episodeName) pureTitle = epData.episodeName; + if (epData.subjectImageUrl) charImageUrl = epData.subjectImageUrl; + badgeLabel = epData.epNumber ? `第${epData.epNumber}话` : '章节'; + } + } + } await _doShareCard({ username, postTime, avatarUrl, contentEl, pureTitle, contentDoc, contentWin, dark, charImageUrl, badgeLabel }); } @@ -779,6 +819,17 @@ if (!badgeLabel || badgeLabel === '人物') badgeLabel = apiData.badgeLabel; } } + if (isEpisode) { + const epIdMatch = contentWin.location.pathname.match(/\/ep\/(\d+)/); + if (epIdMatch) { + const epData = await fetchEpisodeData(epIdMatch[1]); + if (epData) { + if (epData.episodeName) pureTitle = epData.episodeName; + if (epData.subjectImageUrl) charImageUrl = epData.subjectImageUrl; + badgeLabel = epData.epNumber ? `第${epData.epNumber}话` : '章节'; + } + } + } await _doShareCard({ username, postTime, avatarUrl, contentEl, pureTitle, contentDoc, contentWin, dark, replies, replyId: replyEl.id, charImageUrl, badgeLabel }); }