您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Annict の「記録する」ページの内容を指定されたURLに送信します。
// ==UserScript== // @name Annict: Track Broker // @namespace https://rinsuki.net // @match https://annict.com/* // @grant GM_getValue // @grant GM_setValue // @grant GM_xmlhttpRequest // @version 1.1 // @author rinsuki // @description Annict の「記録する」ページの内容を指定されたURLに送信します。 // ==/UserScript== // @ts-check (() => { /** @type {MutationObserver | null} */ let observer const configUI = document.createElement("details") configUI.innerHTML = `<summary>Track Broker Config</summary><form><div>URL: <input name="url"></div><div>Authorization Header: <input name="auth" placeholder="Bearer ..."></div><div>method: <select name="method"><option>POST</option><option>PUT</option></select></div><button>Save</button></form>` configUI.style.position = "fixed" configUI.style.bottom = "1em" configUI.style.right = "1em" configUI.style.zIndex = "9999" configUI.style.backgroundColor = "white" configUI.style.border = "1px solid black" configUI.style.padding = "0.5em" const form = configUI.querySelector("form") if (form == null) return /** @type {HTMLInputElement | null} */ const urlField = form.querySelector("input[name=url]") /** @type {HTMLInputElement | null} */ const authField = form.querySelector("input[name=auth]") /** @type {HTMLSelectElement | null} */ const methodField = form.querySelector("select[name=method]") if (urlField == null || authField == null || methodField == null) return form.addEventListener("submit", e => { e.preventDefault() GM_setValue("url", urlField.value) GM_setValue("auth", authField.value) GM_setValue("method", methodField.value) alert("Saved") }) urlField.value = GM_getValue("url", "") authField.value = GM_getValue("auth", "") methodField.value = GM_getValue("method", "POST") async function main() { if (location.pathname !== "/track") { configUI.remove() return } else { document.body.appendChild(configUI) } const watchTarget = document.querySelector(`[data-reloadable-event-name-value="trackable-episode-list"]`) if (watchTarget == null) return function changed() { const works = document.querySelectorAll("#trackable-episode-list .card > .card-body > :first-child") let episodes = [] for (const work of works) { const workName = work.querySelector(".col > .small.u-cursor-pointer")?.textContent const episodeName = work.querySelector(".col > .fw-bold")?.textContent const episodeSource = work.querySelector(".col > .mt-1.small > .text-muted") const episodeTimestamp = work.querySelector(".col > .small:not(.mt-1) > .text-muted")?.textContent const episodeJSON = { workName, episodeName, episodeSource: episodeSource != null ? { name: episodeSource.textContent, url: episodeSource.getAttribute("href"), } : null, episodeTimestamp, } episodes.push(episodeJSON) } const body = { episodes, timezone: Intl.DateTimeFormat().resolvedOptions().timeZone, } console.log("body", body) const url = GM_getValue("url", "") const auth = GM_getValue("auth", "") if (url.length && url.startsWith("http")) { const urlObj = new URL(url) GM_xmlhttpRequest({ url, method: GM_getValue("method", "POST"), headers: { "Content-Type": "application/json", "Authorization": auth, }, data: JSON.stringify(body), onload: res => { console.log(res) if (res.status >= 300) { alert(`failed to send data (${res.status})`) } } }) } else { console.warn("skip sending because url is not set or invalid") } } if (observer != null) { observer.disconnect() } observer = new MutationObserver((mutations) => { changed() }) observer.observe(watchTarget, { childList: true, }) changed() } addEventListener("turbo:load", main) })()