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 <noreply@anthropic.com>
This commit is contained in:
2026-04-22 03:13:12 +10:00
parent 943fb422cf
commit 2e7c49f7fc
+53 -2
View File
@@ -1,7 +1,7 @@
// ==UserScript== // ==UserScript==
// @name Bangumi Topic Share // @name Bangumi Topic Share
// @namespace http://tampermonkey.net/ // @namespace http://tampermonkey.net/
// @version 5.1 // @version 5.2
// @description Bangumi 话题/日志分享工具:生成分享卡片,支持图片复制/下载、一键复制分享文案、可选 AI 标签 // @description Bangumi 话题/日志分享工具:生成分享卡片,支持图片复制/下载、一键复制分享文案、可选 AI 标签
// @author Stardream // @author Stardream
// @contributor Chang ji, Mewtw0 // @contributor Chang ji, Mewtw0
@@ -166,8 +166,18 @@
const contentWin = contentDoc.defaultView || window; const contentWin = contentDoc.defaultView || window;
const pathname = contentWin.location.pathname; const pathname = contentWin.location.pathname;
const isBlog = /\/blog\/\d+/.test(pathname); const isBlog = /\/blog\/\d+/.test(pathname);
const isEpisode = /\/ep\/\d+/.test(pathname);
const isCharacter = /\/character\/\d+|\/rakuen\/topic\/crt\/\d+/.test(pathname); const isCharacter = /\/character\/\d+|\/rakuen\/topic\/crt\/\d+/.test(pathname);
const isPerson = /\/person\/\d+|\/rakuen\/topic\/prsn\/\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) { if (isBlog) {
const subjectNames = [...new Set( const subjectNames = [...new Set(
[...contentDoc.querySelectorAll('a')] [...contentDoc.querySelectorAll('a')]
@@ -624,16 +634,35 @@
return { imageUrl, name, badgeLabel, id, type }; 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) { async function createShareImage(contentDoc = document) {
const dark = contentDoc.documentElement.getAttribute('data-theme') === 'dark'; const dark = contentDoc.documentElement.getAttribute('data-theme') === 'dark';
const contentWin = contentDoc.defaultView || window; const contentWin = contentDoc.defaultView || window;
const isBlog = /\/blog\/\d+/.test(contentWin.location.pathname); 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 isCharacter = /\/character\/\d+|\/rakuen\/topic\/crt\/\d+/.test(contentWin.location.pathname);
const isPerson = /\/person\/\d+|\/rakuen\/topic\/prsn\/\d+/.test(contentWin.location.pathname); const isPerson = /\/person\/\d+|\/rakuen\/topic\/prsn\/\d+/.test(contentWin.location.pathname);
let username, postTime, avatarUrl, contentEl; 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') const authorLink = contentDoc.querySelector('.author.user-card .title p a')
|| contentDoc.querySelector('.author.user-card a.avatar'); || contentDoc.querySelector('.author.user-card a.avatar');
username = authorLink ? authorLink.textContent.trim() : "未知用户"; username = authorLink ? authorLink.textContent.trim() : "未知用户";
@@ -678,6 +707,17 @@
if (!badgeLabel || badgeLabel === '人物') badgeLabel = apiData.badgeLabel; 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 }); await _doShareCard({ username, postTime, avatarUrl, contentEl, pureTitle, contentDoc, contentWin, dark, charImageUrl, badgeLabel });
} }
@@ -779,6 +819,17 @@
if (!badgeLabel || badgeLabel === '人物') badgeLabel = apiData.badgeLabel; 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, await _doShareCard({ username, postTime, avatarUrl, contentEl, pureTitle, contentDoc, contentWin, dark,
replies, replyId: replyEl.id, charImageUrl, badgeLabel }); replies, replyId: replyEl.id, charImageUrl, badgeLabel });
} }