Kamikaze' Script Utils

Custom Functions for Kamikaze's Scripts

이 스크립트는 직접 설치하는 용도가 아닙니다. 다른 스크립트에서 메타 지시문 // @require https://update.greatest.deepsurf.us/scripts/455253/1683990/Kamikaze%27%20Script%20Utils.js을(를) 사용하여 포함하는 라이브러리입니다.

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

  1. // ==UserScript==
  2. // @name Kamikaze' Script Utils
  3. // @namespace https://greatest.deepsurf.us/users/928242
  4. // @description Custom Functions for Kamikaze's Scripts
  5. // @version 1.1.13
  6. // @author Kamikaze (https://github.com/Kamiikaze)
  7. // @license MIT
  8. // ==/UserScript==
  9.  
  10. /* jshint esversion: 11 */
  11.  
  12. /* global Toastify */
  13.  
  14. // https://stackoverflow.com/questions/61964265/getting-error-this-document-requires-trustedhtml-assignment-in-chrome
  15. if (window.trustedTypes && window.trustedTypes.createPolicy && !window.trustedTypes.defaultPolicy) {
  16. window.trustedTypes.createPolicy('default', {
  17. createHTML: string => string
  18. // Optional, only needed for script (url) tags
  19. //,createScriptURL: string => string
  20. //,createScript: string => string,
  21. });
  22. }
  23.  
  24. /**
  25. * @description Custom Logger
  26. */
  27. class Logger {
  28.  
  29. /**
  30. * @param {string} prefix - Prefix for the log output
  31. * @param {number} logLevel - 0: disable, 1: info, 2: debug, 3: warn, 4: all
  32. */
  33. constructor(prefix, logLevel = 1) {
  34. this.prefix = prefix; // Name of Script
  35. this.logLevel = logLevel;
  36. this.defaultStyle = "background: #44adf3; color: #000; font-weight: bold; padding: 5px 15px; border-radius: 10px"
  37. this.resetStyle = "background: unset; color: unest"
  38. this.prefixStyle = this.setPrefixStyle(prefix);
  39.  
  40. this.info(`Logger initialized with prefix "${prefix}" and logLevel ${logLevel}`)
  41. }
  42.  
  43. /*
  44. * @param {number} logLevel - 0: disable, 1: info, 2: debug, 3: warn, 4: all
  45. */
  46. setLogLevel(logLevel) {
  47. this.logLevel = logLevel
  48. }
  49.  
  50. setPrefixStyle(prefix) {
  51. switch (prefix) {
  52. case "sto":
  53. return "background: #000; color: #fff"
  54. default:
  55. return this.defaultStyle
  56. }
  57. }
  58.  
  59. formattedOutput(...args) {
  60. const argsArray = Array.from(args).map(arg => {
  61. if (typeof arg === "object") return JSON.stringify(arg, null, 4)
  62. return arg
  63. })
  64. return `%c${this.prefix}%c ` + argsArray.join(", ")
  65. }
  66.  
  67. info(...args) {
  68. if (this.logLevel >= 1) console.info(this.formattedOutput(...args), this.prefixStyle, this.resetStyle)
  69. }
  70.  
  71. debug(...args) {
  72. if (this.logLevel >= 2) console.debug(this.formattedOutput(...args), this.prefixStyle, this.resetStyle)
  73. }
  74.  
  75. warn(...args) {
  76. if (this.logLevel >= 3) console.warn(this.formattedOutput(...args), this.prefixStyle, this.resetStyle)
  77. }
  78.  
  79. error(...args) {
  80. if (this.logLevel > 0) console.error(this.formattedOutput(...args), this.prefixStyle, this.resetStyle)
  81. }
  82.  
  83. }
  84.  
  85. /**
  86. * @param {string} text - Notification Text
  87. * @param {number} [duration=5000] - Duration for which the toast should be displayed.
  88. * -1 for permanent toast
  89. * @param {string} [type="default"] - Classname for Notification type
  90. * @description Sending Toast Notification
  91. */
  92. function notify(text, duration = 5000, type = "default") {
  93. Toastify({
  94. text: text,
  95. duration: duration,
  96. close: true,
  97. gravity: "top", // `top` or `bottom`
  98. position: "right", // `left`, `center` or `right`
  99. offset: {
  100. x: 0, // horizontal axis - can be a number or a string indicating unity. eg: '2em'
  101. y: 70 // vertical axis - can be a number or a string indicating unity. eg: '2em'
  102. },
  103. className: type,
  104. stopOnFocus: true, // Prevents dismissing of toast on hover
  105. style: {
  106. borderRadius: "50px",
  107. boxShadow: "0px 0px 20px 0px #0f1620",
  108. }
  109. }).showToast();
  110. }
  111.  
  112. /**
  113. * @param {string} css - CSS String
  114. * @param {boolean} important - Add !important to all rules
  115. * @description Adds CSS to the head of the document
  116. */
  117. function addGlobalStyle(css, important = true) {
  118. let head, style;
  119. head = document.getElementsByTagName('head')[0];
  120. if (!head) return;
  121. style = document.createElement('style');
  122. (important) ? style.innerHTML = css.replace(/;/g, ' !important;') : style.innerHTML = css;
  123. head.appendChild(style);
  124. }
  125.  
  126. /**
  127. * @param {string} selector - CSS Selector
  128. * @param {HTMLElement|Document} parent - Parent Element
  129. * @description Waits for an element to be present in the DOM
  130. * @returns {Element | null}
  131. */
  132. function waitForElm(selector, parent = document) {
  133. return new Promise((resolve) => {
  134. if (parent.querySelector(selector)) {
  135. log.debug("Element found", selector)
  136. return resolve(parent.querySelector(selector));
  137. }
  138.  
  139. const observer = new MutationObserver(() => {
  140. if (parent.querySelector(selector)) {
  141. log.debug("Element found", selector)
  142. resolve(parent.querySelector(selector));
  143. observer.disconnect();
  144. }
  145. });
  146.  
  147. observer.observe(document.body, {
  148. childList: true,
  149. subtree: true
  150. });
  151.  
  152. setTimeout(() => {
  153. console.error("Element not found", selector)
  154. return resolve(null)
  155. }, 1000)
  156. });
  157. }
  158.  
  159. /**
  160. * @description Get current Hostname, Season and Episode
  161. * @returns {{host: string, season: (string|number), episode: (string|number)}}
  162. */
  163. function getStreamPageLocation() {
  164. const url = window.location;
  165. const host = url.host;
  166. const path = url.pathname.split("/").slice(3);
  167.  
  168. return {
  169. host: host,
  170. season: path[1]?.split("-")[1] || 0,
  171. episode: path[2]?.split("-")[1] || 0,
  172. }
  173. }
  174.  
  175. /**
  176. * @param {HTMLElement} seasonListEl - Season List Element
  177. * @description Check if the Stream has Movies
  178. * @returns {boolean} - True if the list contains a "Filme" entry
  179. */
  180. function checkHasMovies(seasonListEl) {
  181. if (!seasonListEl) return false
  182. const seasonList = seasonListEl.children
  183. for (let i = 0; i < seasonList.length; i++)
  184. if (seasonList[i].textContent.trim() === "Filme") {
  185. log.debug("Found Movies")
  186. return true
  187. }
  188. return false
  189. }
  190.  
  191. /**
  192. * @description Get Stream Details from the Stream Page
  193. * @returns {Promise<{seasonsCount: number, hasMovies: boolean, episodesCount: number, episodeTitle: {de: string, en: string}, title: string}>}
  194. */
  195. async function getStreamDetails() {
  196. const titleEl = await waitForElm(".series-title > h1 > span")
  197. const seasonListEl = await waitForElm("#stream > ul:nth-child(1)")
  198. const episodeListEl = await waitForElm("#stream > ul:nth-child(4)")
  199. const episodeTitleEl = await waitForElm(".hosterSiteTitle h2")
  200. const episodeTitle = getEpisodeTitle(episodeTitleEl)
  201.  
  202. const hasMovies = checkHasMovies(seasonListEl)
  203.  
  204. const seasonsCount = seasonListEl?.childElementCount - 1 - (hasMovies ? 1 : 0) || 0
  205. const episodesCount = episodeListEl?.childElementCount - 1 || 0
  206.  
  207. log.debug("Elements", titleEl, seasonListEl, episodeListEl)
  208. log.debug("Count", seasonsCount, episodesCount)
  209.  
  210. return {
  211. title: titleEl?.textContent.trim() || "",
  212. seasonsCount: seasonsCount,
  213. episodesCount: episodesCount,
  214. episodeTitle: {
  215. de: episodeTitle.de,
  216. en: episodeTitle.en,
  217. },
  218. hasMovies: hasMovies,
  219. }
  220. }
  221.  
  222. /**
  223. * @description Parsing title for both languages
  224. * @returns {{de: string, en: string}}
  225. */
  226. function getEpisodeTitle(episodeTitleEl) {
  227. let titleDE = ""
  228. let titleEN = ""
  229. if (episodeTitleEl) {
  230. const [episodeTitleDE, episodeTitleEN] = episodeTitleEl.children
  231. titleDE = episodeTitleDE.textContent.trim()
  232. titleEN = episodeTitleEN?.textContent.trim() || ""
  233. }
  234.  
  235. return {de: titleDE, en: titleEN}
  236. }
  237.  
  238. /**
  239. * @description Return Stream Data from the Stream Page
  240. * @returns {Promise<{seasonsCount: number, currentSeason: number, host: string, hasMovies: boolean, episodesCount: number, title: string, currentEpisode: number}>}
  241. */
  242. async function getStreamData() {
  243. const streamLocation = getStreamPageLocation()
  244. const streamDetails = await getStreamDetails()
  245.  
  246. const data = {
  247. host: streamLocation.host,
  248. title: streamDetails.title,
  249. currentSeason: parseInt(streamLocation.season),
  250. seasonsCount: parseInt(streamDetails.seasonsCount),
  251. currentEpisode: parseInt(streamLocation.episode),
  252. episodesCount: parseInt(streamDetails.episodesCount),
  253. episodeTitle: streamDetails.episodeTitle,
  254. hasMovies: streamDetails.hasMovies,
  255. }
  256.  
  257. log.debug("StreamData", data)
  258.  
  259. return data
  260. }