Annict: Track Broker

Annict の「記録する」ページの内容を指定されたURLに送信します。

  1. // ==UserScript==
  2. // @name Annict: Track Broker
  3. // @namespace https://rinsuki.net
  4. // @match https://annict.com/*
  5. // @grant GM_getValue
  6. // @grant GM_setValue
  7. // @grant GM_xmlhttpRequest
  8. // @version 1.1
  9. // @author rinsuki
  10. // @description Annict の「記録する」ページの内容を指定されたURLに送信します。
  11. // ==/UserScript==
  12.  
  13. // @ts-check
  14. (() => {
  15. /** @type {MutationObserver | null} */
  16. let observer
  17. const configUI = document.createElement("details")
  18. 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>`
  19. configUI.style.position = "fixed"
  20. configUI.style.bottom = "1em"
  21. configUI.style.right = "1em"
  22. configUI.style.zIndex = "9999"
  23. configUI.style.backgroundColor = "white"
  24. configUI.style.border = "1px solid black"
  25. configUI.style.padding = "0.5em"
  26. const form = configUI.querySelector("form")
  27. if (form == null) return
  28. /** @type {HTMLInputElement | null} */
  29. const urlField = form.querySelector("input[name=url]")
  30. /** @type {HTMLInputElement | null} */
  31. const authField = form.querySelector("input[name=auth]")
  32. /** @type {HTMLSelectElement | null} */
  33. const methodField = form.querySelector("select[name=method]")
  34. if (urlField == null || authField == null || methodField == null) return
  35. form.addEventListener("submit", e => {
  36. e.preventDefault()
  37. GM_setValue("url", urlField.value)
  38. GM_setValue("auth", authField.value)
  39. GM_setValue("method", methodField.value)
  40. alert("Saved")
  41. })
  42. urlField.value = GM_getValue("url", "")
  43. authField.value = GM_getValue("auth", "")
  44. methodField.value = GM_getValue("method", "POST")
  45. async function main() {
  46. if (location.pathname !== "/track") {
  47. configUI.remove()
  48. return
  49. } else {
  50. document.body.appendChild(configUI)
  51. }
  52. const watchTarget = document.querySelector(`[data-reloadable-event-name-value="trackable-episode-list"]`)
  53. if (watchTarget == null) return
  54. function changed() {
  55. const works = document.querySelectorAll("#trackable-episode-list .card > .card-body > :first-child")
  56. let episodes = []
  57. for (const work of works) {
  58. const workName = work.querySelector(".col > .small.u-cursor-pointer")?.textContent
  59. const episodeName = work.querySelector(".col > .fw-bold")?.textContent
  60. const episodeSource = work.querySelector(".col > .mt-1.small > .text-muted")
  61. const episodeTimestamp = work.querySelector(".col > .small:not(.mt-1) > .text-muted")?.textContent
  62. const episodeJSON = {
  63. workName,
  64. episodeName,
  65. episodeSource: episodeSource != null ? {
  66. name: episodeSource.textContent,
  67. url: episodeSource.getAttribute("href"),
  68. } : null,
  69. episodeTimestamp,
  70. }
  71. episodes.push(episodeJSON)
  72. }
  73. const body = {
  74. episodes,
  75. timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
  76. }
  77. console.log("body", body)
  78. const url = GM_getValue("url", "")
  79. const auth = GM_getValue("auth", "")
  80. if (url.length && url.startsWith("http")) {
  81. const urlObj = new URL(url)
  82. GM_xmlhttpRequest({
  83. url,
  84. method: GM_getValue("method", "POST"),
  85. headers: {
  86. "Content-Type": "application/json",
  87. "Authorization": auth,
  88. },
  89. data: JSON.stringify(body),
  90. onload: res => {
  91. console.log(res)
  92. if (res.status >= 300) {
  93. alert(`failed to send data (${res.status})`)
  94. }
  95. }
  96. })
  97. } else {
  98. console.warn("skip sending because url is not set or invalid")
  99. }
  100. }
  101. if (observer != null) {
  102. observer.disconnect()
  103. }
  104. observer = new MutationObserver((mutations) => {
  105. changed()
  106. })
  107. observer.observe(watchTarget, {
  108. childList: true,
  109. })
  110. changed()
  111. }
  112. addEventListener("turbo:load", main)
  113. })()