// ==UserScript== // @namespace https://github.com/umajho // @name bangumi-episode-ratings-gadget // @version 0.2.7 // @description Bangumi 单集评分的超合金组件 // @license MIT // @website https://github.com/umajho/bangumi-episode-ratings // @match https://bangumi.tv/* // @match https://bgm.tv/* // @match https://chii.in/* // @grant GM_info // @grant unsafeWindow // @grant window.close // @downloadURL none // ==/UserScript== (function() { "use strict"; const env = { get APP_ENTRYPOINT() { return "https://bgm-ep-ratings.deno.dev/"; }, LOCAL_STORAGE_KEY_TOKEN: "bgm_ep_ratings_token", SEARCH_PARAMS_KEY_TOKEN_COUPON: "bgm_ep_ratings_token_coupon" }; const version = "0.2.7"; const ENDPOINT_PATHS = { AUTH: { BANGUMI_PAGE: "bangumi-page", CALLBACK: "callback", REDEEM_TOKEN_COUPON: "redeem-token-coupon" } }; class Client { constructor(opts) { this.subjectEpisodesRatingsCache = {}; this.entrypoint = opts.entrypoint; this.token = opts.token; } get URL_AUTH_BANGUMI_PAGE() { const url = ( // new URL(this.buildFullEndpoint("auth", ENDPOINT_PATHS.AUTH.BANGUMI_PAGE)) ); url.searchParams.set("gadget_version", Global.version); return url.toString(); } async rateEpisode(opts) { if (!this.token) return ["auth_required"]; if (opts.score !== null) { const bodyData = { score: opts.score }; return await this.fetch( "api/v1", `subjects/${opts.subjectID}/episodes/${opts.episodeID}/ratings/mine`, { method: "PUT", body: JSON.stringify(bodyData) } ); } else { return await this.fetch( "api/v1", `subjects/${opts.subjectID}/episodes/${opts.episodeID}/ratings/mine`, { method: "DELETE" } ); } } hasCachedSubjectEpisodesRatings(subjectID) { return !!this.subjectEpisodesRatingsCache[subjectID]; } async mustGetSubjectEpisodesRatings(opts) { if (this.subjectEpisodesRatingsCache[opts.subjectID]) { return this.subjectEpisodesRatingsCache[opts.subjectID]; } return this.subjectEpisodesRatingsCache[opts.subjectID] = this.fetch( "api/v1", `subjects/${opts.subjectID}/episodes/ratings`, { method: "GET" } ).then( (resp) => this.subjectEpisodesRatingsCache[opts.subjectID] = unwrap(resp) ); } async mustGetEpisodeRatings() { const resp = await this.fetch( "api/v1", `subjects/${Global.subjectID}/episodes/${Global.episodeID}/ratings`, { method: "GET" } ); return unwrap(resp); } async getMyEpisodeRating() { return await this.fetch( "api/v1", `subjects/${Global.subjectID}/episodes/${Global.episodeID}/ratings/mine`, { method: "GET" } ); } async fetch(group, endpointPath, opts) { const url = new URL(this.buildFullEndpoint(group, endpointPath)); if (opts.searchParams) { url.search = opts.searchParams.toString(); } const headers = new Headers(); if (this.token) { headers.set("Authorization", `Basic ${this.token}`); } headers.set("X-Gadget-Version", Global.version); if (Global.claimedUserID !== null) { headers.set("X-Claimed-User-ID", Global.claimedUserID.toString()); } try { const resp = await fetch(url, { method: opts.method, headers, body: opts.body }); const respJSON = await resp.json(); if (respJSON[0] === "error" && respJSON[1] === "AUTH_REQUIRED") { if (Global.token.getValueOnce() !== null) { Global.token.setValue(null); } return ["auth_required"]; } return respJSON; } catch (e) { const operation = `fetch \`${opts.method} ${url}\``; console.error(`${operation} \u5931\u8D25`, e); return ["error", "UNKNOWN", `${operation} \u5931\u8D25\uFF1A ${e}`]; } } buildFullEndpoint(group, endpointPath) { return join(join(this.entrypoint, group + "/"), endpointPath); } async mustRedeemTokenCoupon(tokenCoupon) { const data = await this.fetch( "auth", ENDPOINT_PATHS.AUTH.REDEEM_TOKEN_COUPON, { method: "POST", body: JSON.stringify({ tokenCoupon }) } ); return unwrap(data); } clearCache() { this.subjectEpisodesRatingsCache = {}; } } function join(base, url) { return new URL(url, base).href; } function unwrap(resp) { if (!Array.isArray(resp) || resp[0] !== "ok" && resp[0] !== "error") { console.error("Unsupported response", resp); throw new Error(`Unsupported response: ${JSON.stringify(resp)}`); } if (resp[0] === "error") throw new Error(resp[2]); return resp[1]; } class Watched { constructor(_value) { this._value = _value; this._watchers = []; } getValueOnce() { return this._value; } setValue(newValue) { const oldValue = this._value; this._value = newValue; this._watchers.forEach((w) => w(newValue, oldValue)); } watchDeferred(cb) { this._watchers.push(cb); return () => { this._watchers = this._watchers.filter((w) => w !== cb); }; } watch(cb) { cb(this._value, void 0); return this.watchDeferred(cb); } } const global = {}; const Global = global; function initializeGlobal() { Object.assign(global, makeGlobal()); } function makeGlobal() { const { subjectID, episodeID } = (() => { let subjectID2 = null; let episodeID2 = null; const pathParts = window.location.pathname.split("/").filter(Boolean); if (pathParts[0] === "subject") { subjectID2 = Number(pathParts[1]); } else if (pathParts.length === 2 && pathParts[0] === "ep") { episodeID2 = Number(pathParts[1]); const subjectHref = $("#headerSubject > .nameSingle > a").attr("href"); subjectID2 = Number(subjectHref.split("/")[2]); } return { subjectID: subjectID2, episodeID: episodeID2 }; })(); const claimedUserID = (() => { if ("unsafeWindow" in window) { return window.unsafeWindow.CHOBITS_UID || null; } return window.CHOBITS_UID || null; })(); if (claimedUserID === null) { localStorage.removeItem(env.LOCAL_STORAGE_KEY_TOKEN); } const token = new Watched( localStorage.getItem(env.LOCAL_STORAGE_KEY_TOKEN) ); window.addEventListener("storage", (ev) => { if (ev.key !== env.LOCAL_STORAGE_KEY_TOKEN) return; if (ev.newValue === token.getValueOnce()) return; token.setValue(ev.newValue); }); const client = new Client({ entrypoint: env.APP_ENTRYPOINT, token: token.getValueOnce() }); token.watchDeferred((newToken) => { if (newToken) { localStorage.setItem(env.LOCAL_STORAGE_KEY_TOKEN, newToken); } else { localStorage.removeItem(env.LOCAL_STORAGE_KEY_TOKEN); } client.token = newToken; client.clearCache(); }); return { version, subjectID, episodeID, claimedUserID, token, client }; } const scores = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; function describeScore(score) { return [ [9.5, "\u8D85\u795E\u4F5C"], [8.5, "\u795E\u4F5C"], [7.5, "\u529B\u8350"], [6.5, "\u63A8\u8350"], [5.5, "\u8FD8\u884C"], [4.5, "\u4E0D\u8FC7\u4E0D\u5931"], [3.5, "\u8F83\u5DEE"], [2.5, "\u5DEE"], [1.5, "\u5F88\u5DEE"] ].find(([min, _]) => score >= min)?.[1] ?? "\u4E0D\u5FCD\u76F4\u89C6"; } function describeScoreEx(score) { let description = `${describeScore(score)} ${score}`; if (score === 1 || score === 10) { description += " (\u8BF7\u8C28\u614E\u8BC4\u4EF7)"; } return description; } function renderStars(el, props) { el = $( /*html*/ `
` ).replaceAll(el); for (const score of scores) { const starEl = $( /*html*/ `
` ); el.append(starEl); const aEl = starEl.find("a"); aEl.text(score); aEl.attr("title", describeScoreEx(score)); starEl.on("mouseover", () => props.hoveredScore.setValue(score)).on("mouseout", () => props.hoveredScore.setValue(null)).on("click", () => props.onRateEpisode(score)); } function updateStarsContainer(params) { if (params[0] === "invisible") { el.css("display", "none"); return; } el.css("display", ""); const [_, { ratedScore, hoveredScore }] = params; const isHovering = hoveredScore !== null; const maxScoreToHighlight = hoveredScore ?? ratedScore ?? null; { let alarmScore = maxScoreToHighlight; if (alarmScore === "cancel") { alarmScore = ratedScore; } props.onUpdateScoreToAlarm(alarmScore); } const starEls = el.find(".star-rating"); for (const score of scores) { const starEl = starEls.eq(score - 1); starEl.removeClass("star-rating-on").removeClass("star-rating-hover"); if (typeof maxScoreToHighlight === "number" && score <= maxScoreToHighlight) { starEl.addClass(isHovering ? "star-rating-hover" : "star-rating-on"); } } $(el).find(".rating-cancel").removeClass("star-rating-hover"); if (hoveredScore === "cancel") { $(el).find(".rating-cancel").addClass("star-rating-hover"); } } return { updateStarsContainer }; } function renderMyRating(el, props) { const ratedScore = new Watched(props.ratedScore); const hoveredScore = new Watched(null); el = $( /*html*/ `

\u6211\u7684\u8BC4\u4EF7:

` ).replaceAll(el); const starsContainerEl = el.find(".stars-container"); const { updateStarsContainer } = renderStars(starsContainerEl, { hoveredScore, onRateEpisode: rateEpisode, onUpdateScoreToAlarm: (score) => { if (score !== null) { $(el).find(".alarm").text(describeScoreEx(score)); } else { $(el).find(".alarm").text(""); } } }); $(el).find(".rating-cancel").on("mouseover", () => hoveredScore.setValue("cancel")).on("mouseout", () => hoveredScore.setValue(null)).on("click", () => rateEpisode(null)); ratedScore.watchDeferred( (ratedScore2) => updateStarsContainer(["normal", { ratedScore: ratedScore2, hoveredScore: hoveredScore.getValueOnce() }]) ); hoveredScore.watch((hoveredScore2) => { updateStarsContainer(["normal", { ratedScore: ratedScore.getValueOnce(), hoveredScore: hoveredScore2 }]); }); const messageEl = el.find(".message"); function updateMessage(value) { messageEl.attr("style", ""); switch (value[0]) { case "none": { messageEl.text(""); messageEl.css("display", "none"); break; } case "processing": { messageEl.text("\u5904\u7406\u4E2D\u2026"); messageEl.css("color", "gray"); break; } case "loading": { messageEl.text("\u52A0\u8F7D\u4E2D\u2026"); messageEl.css("color", "gray"); break; } case "error": { messageEl.text(value[1]); messageEl.css("color", "red"); break; } case "auth_link": { messageEl.html( /*html*/ ` \u82E5\u8981\u67E5\u770B\u6216\u63D0\u4EA4\u81EA\u5DF1\u7684\u5355\u96C6\u8BC4\u5206\uFF0C
\u8BF7\u5148\u6388\u6743\u6B64\u5E94\u7528\u3002
\u5355\u96C6\u8BC4\u5206\u5E94\u7528\u9700\u8981\u4EE5\u6B64\u6765\u786E\u8BA4\u767B\u5F55\u8005\u3002 ` ); $(messageEl).find("a").attr( "href", Global.client.URL_AUTH_BANGUMI_PAGE ); break; } case "requiring_fetch": { if (props.canRefetchAfterAuth) { messageEl.html( /*html*/ ` \u70B9\u51FB\u6216\u5237\u65B0\u672C\u9875\u4EE5\u83B7\u53D6\u3002 ` ); $(messageEl).find("button").on("click", async () => { updateMessage(["loading"]); const resp = await Global.client.getMyEpisodeRating(); if (resp[0] === "auth_required") { Global.token.setValue(null); } else if (resp[0] === "error") { const [_, _name, message] = resp; updateMessage(["error", message]); } else if (resp[0] === "ok") { const [_, data] = resp; updateMessage(["none"]); ratedScore.setValue(data.score); } else ; }); } else { messageEl.text("\u8BF7\u5237\u65B0\u672C\u9875\u4EE5\u83B7\u53D6\u3002 "); } break; } } } updateMessage(["none"]); Global.token.watch((newToken, oldToken) => { if (newToken) { if (oldToken !== void 0) { if (props.isPrimary) { updateMessage(["requiring_fetch"]); } updateStarsContainer(["invisible"]); } else { updateMessage(["none"]); } } else { if (props.isPrimary) { updateMessage(["auth_link"]); } else { el.css("display", "none"); } updateStarsContainer(["invisible"]); } }); async function rateEpisode(scoreToRate) { if (!Global.token.getValueOnce()) return; updateMessage(["processing"]); const resp = await Global.client.rateEpisode({ subjectID: Global.subjectID, episodeID: props.episodeID, score: scoreToRate }); if (resp[0] === "auth_required") { updateMessage(["auth_link"]); } else if (resp[0] === "error") { const [_, _name, message] = resp; updateMessage(["error", message]); } else if (resp[0] === "ok") { const [_, data] = resp; updateMessage(["none"]); ratedScore.setValue(data.score); } else ; } } function renderScoreboard(el, props) { el = $( /*html*/ `
\u5355\u96C6\u8BC4\u5206
` ).replaceAll(el); function updateNumber(score) { if (Number.isNaN(score)) { $(el).find(".number").text(0 .toFixed(1)); $(el).find(".description").text("--"); } else { $(el).find(".number").text(score.toFixed(4)); $(el).find(".description").text(describeScore(score)); } } updateNumber(props.score); } function renderScoreChart(el, props) { const { votesData } = props; el = $( /*html*/ `
votes
` ).replaceAll(el); $(el).find(".votes").text(votesData.totalVotes); const chartEl = el.find(".horizontalChart"); const totalVotes = votesData.totalVotes; const votesOfMostVotedScore = votesData.votesOfMostVotedScore; for (const score of scores) { const votes = votesData.getScoreVotes(score); const barEl = $("
"); chartEl.prepend(barEl); renderBar(barEl, { score, votes, totalVotes, votesOfMostVotedScore, updateTooltip }); } function updateTooltip(props2) { let tooltipEl = $(chartEl).find(".tooltip"); if (props2.score === null) { tooltipEl.css("display", "none"); return; } tooltipEl.css("display", "block"); const barEl = $(chartEl).find(`li`).eq(10 - props2.score); const barElRelativeOffsetLeft = barEl.offset().left - el.offset().left; tooltipEl.css("left", `${barElRelativeOffsetLeft + barEl.width() / 2}px`); let scoreVotes = votesData.getScoreVotes(props2.score); const percentage = votesData.totalVotes ? scoreVotes / votesData.totalVotes * 100 : 0; $(tooltipEl).find(".tooltip-inner").text( `${percentage.toFixed(2)}% (${scoreVotes}\u4EBA)` ); } updateTooltip({ score: null }); return el; } function renderBar(el, props) { el = $( /*html*/ `
  • ` ).replaceAll(el); const percentage = (props.votes / props.totalVotes * 100).toFixed(2); $(el).find(".textTip").attr( "data-original-title", `${percentage}% (${props.votes}\u4EBA)` ); $(el).find(".label").text(props.score); const height = (props.votes / props.votesOfMostVotedScore * 100).toFixed(2); $(el).find(".count").css("height", `${height}%`); $(el).find(".count").text(`(${props.votes})`); $(el).on("mouseover", () => props.updateTooltip({ score: props.score })).on("mouseout", () => props.updateTooltip({ score: null })); } class VotesData { constructor(data) { this.data = data; this.totalVotesCache = null; this.averageScoreCache = null; this.mostVotedScoreCache = null; } getScoreVotes(score) { return this.data[score] ?? 0; } get totalVotes() { if (this.totalVotesCache) return this.totalVotesCache; let totalVotes = 0; for (const score of scores) { totalVotes += this.getScoreVotes(score); } return this.totalVotesCache = totalVotes; } get averageScore() { if (this.averageScoreCache) return this.averageScoreCache; let totalScore = 0; for (const score of scores) { totalScore += this.getScoreVotes(score) * score; } return this.averageScoreCache = totalScore / this.totalVotes; } get mostVotedScore() { if (this.mostVotedScoreCache) return this.mostVotedScoreCache; let mostVotedScore = scores[0]; for (const score of scores.slice(1)) { if (this.getScoreVotes(score) > this.getScoreVotes(mostVotedScore)) { mostVotedScore = score; } } return this.mostVotedScoreCache = mostVotedScore; } get votesOfMostVotedScore() { return this.getScoreVotes(this.mostVotedScore); } } async function processEpPage() { const scoreboardEl = $( /*html*/ `
    \u5355\u96C6\u8BC4\u5206\u52A0\u8F7D\u4E2D\u2026
    ` ); $("#columnEpA").prepend(scoreboardEl); const ratingsData = await Global.client.mustGetEpisodeRatings(); const votesData = new VotesData( ratingsData.votes ); renderScoreboard(scoreboardEl, { score: votesData.averageScore }); const scoreChartEl = $("
    ").insertBefore("#columnEpA > .epDesc"); renderScoreChart(scoreChartEl, { votesData }); $( /*html*/ `
    ` ).insertAfter("#columnEpA > .epDesc"); const myRatingEl = $("
    ").insertAfter(".singleCommentList > .board"); if (!ratingsData.my_rating) { Global.token.setValue(null); } renderMyRating(myRatingEl, { episodeID: Global.episodeID, ratedScore: ratingsData.my_rating?.score ?? null, isPrimary: true, canRefetchAfterAuth: true }); } function renderRateInfo(el, props) { el = $( /*html*/ `
    ` ).replaceAll(el); const rateInfoEl = el.find(".rateInfo"); const buttonEl = el.find("button"); props.votesData.watch((votesData) => { if (!votesData) { el.css("display", "none"); return; } el.css("display", ""); const score = votesData.averageScore; if (Number.isNaN(score)) { $(rateInfoEl).find(".maybe-starlight").removeClass().addClass("maybe-starlight"); $(rateInfoEl).find("small.fade").text("--"); } else { $(rateInfoEl).find(".maybe-starlight").addClass("starlight").addClass(`stars${Math.round(score)}`); $(rateInfoEl).find("small.fade").text(score.toFixed(4)); } $(rateInfoEl).find(".tip_j").text(`(${votesData.totalVotes}\u4EBA\u8BC4\u5206)`); }); buttonEl.on("click", () => { console.log("111"); rateInfoEl.css("display", ""); buttonEl.css("display", "none"); props.onReveal?.(); }); props.requiresClickToReveal.watch((requiresClickToReveal) => { if (requiresClickToReveal) { rateInfoEl.css("display", "none"); buttonEl.css("display", ""); } else { rateInfoEl.css("display", ""); buttonEl.css("display", "none"); } }); } let isNavigatingAway = false; $(window).on("beforeunload", () => { isNavigatingAway = true; }); function processCluetip() { const el = $("#cluetip"); let counter = 0; const revealed = {}; async function update(opts) { const popupEl = $(el).find(".prg_popup"); if (popupEl.attr("data-bgm-ep-ratings-initialized")) return; popupEl.attr("data-bgm-ep-ratings-initialized", "true"); counter++; const currentCounter = counter; if (!Global.client.hasCachedSubjectEpisodesRatings(opts.subjectID)) { await new Promise((resolve) => setTimeout(resolve, 250)); if (isNavigatingAway || currentCounter !== counter || !popupEl.is(":visible")) { return; } } const loadingEl = $( /*html*/ `
    \u5355\u96C6\u8BC4\u5206\u52A0\u8F7D\u4E2D\u2026
    ` ).insertBefore($(popupEl).find(".tip .board")); const epsRatings = await Global.client.mustGetSubjectEpisodesRatings({ subjectID: opts.subjectID }); loadingEl.remove(); if (currentCounter !== counter) return; const votesData = new Watched( new VotesData( epsRatings.episodes_votes[opts.episodeID] ?? {} ) ); const requiresClickToReveal = new Watched(false); requiresClickToReveal.setValue( !(opts.hasUserWatched || revealed[`${opts.subjectID}:${opts.episodeID}`] || !votesData.getValueOnce().totalVotes) ); function revealScore() { revealed[`${opts.subjectID}:${opts.episodeID}`] = true; requiresClickToReveal.setValue(false); } const rateInfoEl = $("
    ").insertBefore($(popupEl).find(".tip .board")); renderRateInfo(rateInfoEl, { votesData, requiresClickToReveal, onReveal: () => { revealed[`${opts.subjectID}:${opts.episodeID}`] = true; } }); $(popupEl).find(".epStatusTool > a.ep_status").each((_, epStatusEl) => { if (epStatusEl.id.startsWith("Watched")) { $(epStatusEl).on("click", () => revealScore()); } }); } return { update }; } async function processRootPage() { const { update: updateCluetip } = processCluetip(); let isMouseOver = false; $("ul.prg_list > li").each((_, liEl) => { if (!$(liEl).find(".load-epinfo").length) return; $(liEl).on("mouseover", () => { if (isMouseOver) return; isMouseOver = true; const aEl = $(liEl).find("a"); const subjectID = Number($(aEl).attr("subject_id")); const episodeID = (() => { const href = $(aEl).attr("href"); const match = href.match(/^\/ep\/(\d+)/); return Number(match[1]); })(); updateCluetip({ subjectID, episodeID, hasUserWatched: aEl.hasClass("epBtnWatched") }); }).on("mouseout", () => { isMouseOver = false; }); }); } async function processSubjectPage() { const { update: updateCluetip } = processCluetip(); let isMouseOver = false; $("ul.prg_list > li").each((_, liEl) => { $(liEl).on("mouseover", () => { if (isMouseOver) return; isMouseOver = true; const aEl = $(liEl).find("a"); const episodeID = (() => { const href = $(aEl).attr("href"); const match = href.match(/^\/ep\/(\d+)/); return Number(match[1]); })(); updateCluetip({ subjectID: Global.subjectID, episodeID, hasUserWatched: aEl.hasClass("epBtnWatched") }); }).on("mouseout", () => { isMouseOver = false; }); }); } async function processSubjectEpListPage() { const editEpBatchEl = $('[name="edit_ep_batch"]'); let loadingEl; $(editEpBatchEl).find("li").each((_, li) => { if (!$(li).find('[name="ep_mod[]"]').length) return; $( /*html*/ `
    ` ).insertAfter($(li).find("h6")); loadingEl = $( /*html*/ `
    \u5355\u96C6\u8BC4\u5206\u52A0\u8F7D\u4E2D\u2026
    ` ); $(li).append(loadingEl); return false; }); const epsRatings = await Global.client.mustGetSubjectEpisodesRatings({ subjectID: Global.subjectID }); if (loadingEl) { loadingEl.remove(); } if (!epsRatings.my_ratings) { Global.token.setValue(null); } let isFirst_ = true; $(editEpBatchEl).find("li").each((_, li) => { if (!$(li).find('[name="ep_mod[]"]').length) return; const isFirst = isFirst_; isFirst_ = false; if (!isFirst) { $( /*html*/ `
    ` ).insertAfter($(li).find("h6")); } const episodeID = (() => { const href = $(li).find("> h6 > a").attr("href"); const match = /\/ep\/(\d+)/.exec(href); return Number(match[1]); })(); const ratings = epsRatings.episodes_votes[episodeID]; if (!epsRatings.is_certain_that_episodes_votes_are_integral && ratings === void 0) { return; } const votesData = new VotesData(ratings ?? {}); const myRating = epsRatings.my_ratings?.[episodeID]; const hasUserWatched = $(li).find(".statusWatched").length || // 在 “看过” 之类不能修改章节观看状态的情况下,没法确认用户是否看过,但至 // 少可以假设用户在给了某集评分的时候是看过那一集的。 myRating !== void 0; const myRatingEl = $("
    "); $(li).append(myRatingEl); renderMyRating(myRatingEl, { episodeID, ratedScore: myRating ?? null, isPrimary: isFirst, canRefetchAfterAuth: false }); const rateInfoEl = $("
    "); $(li).append(rateInfoEl); renderRateInfo(rateInfoEl, { votesData: new Watched(votesData), requiresClickToReveal: ( // new Watched(!hasUserWatched && !!votesData.totalVotes) ) }); $(li).append($( /*html*/ `
    ` )); }); } function migrate() { const tokenInWrongPlace = localStorage.getItem("bgm_test_app_token"); if (tokenInWrongPlace) { localStorage.setItem(env.LOCAL_STORAGE_KEY_TOKEN, tokenInWrongPlace); localStorage.removeItem("bgm_test_app_token"); } const searchParams = new URLSearchParams(window.location.search); const tokenCouponInWrongPlace = searchParams.get("bgm_test_app_token_coupon"); if (tokenCouponInWrongPlace) { searchParams.set( env.SEARCH_PARAMS_KEY_TOKEN_COUPON, tokenCouponInWrongPlace ); searchParams.delete("bgm_test_app_token_coupon"); let newURL = `${window.location.pathname}?${searchParams.toString()}`; window.history.replaceState(null, "", newURL); } } async function main() { const isInUserScriptRuntime = typeof GM_info !== "undefined"; if ($('meta[name="__bgm_ep_ratings__initialized"]').length) { console.warn( "\u68C0\u6D4B\u5230\u672C\u811A\u672C/\u8D85\u5408\u91D1\u7EC4\u4EF6\uFF08\u5355\u96C6\u8BC4\u5206 by Umajho A.K.A. um\uFF09\u5148\u524D\u5DF2\u7ECF\u521D\u59CB\u5316\u8FC7\uFF0C\u672C\u5B9E\u4F8B\u5C06\u4E0D\u4F1A\u7EE7\u7EED\u8FD0\u884C\u3002", { version: Global.version, isInUserScriptRuntime } ); return; } $('').appendTo("head"); const searchParams = new URLSearchParams(window.location.search); const tokenCoupon = searchParams.get(env.SEARCH_PARAMS_KEY_TOKEN_COUPON); if (tokenCoupon) { searchParams.delete(env.SEARCH_PARAMS_KEY_TOKEN_COUPON); let newURL = `${window.location.pathname}`; if (searchParams.size) { newURL += `?${searchParams.toString()}`; } window.history.replaceState(null, "", newURL); Global.token.setValue( await Global.client.mustRedeemTokenCoupon(tokenCoupon) ); window.close(); } const pathParts = window.location.pathname.split("/").filter(Boolean); if (!pathParts.length) { await processRootPage(); } else if (pathParts.length === 2 && pathParts[0] === "subject") { await processSubjectPage(); } else if (pathParts.length === 3 && pathParts[0] === "subject" && pathParts[2] === "ep") { await processSubjectEpListPage(); } else if (pathParts.length === 2 && pathParts[0] === "ep") { await processEpPage(); } } migrate(); initializeGlobal(); main(); })();