Annict: Track Broker

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)
})()