動畫瘋評分顯示

簡單粗暴地為每個動畫封面添加評分顯示,並排序

  1. // ==UserScript==
  2. // @name 動畫瘋評分顯示
  3. // @namespace Anong0u0
  4. // @version 0.2.2
  5. // @description 簡單粗暴地為每個動畫封面添加評分顯示,並排序
  6. // @author Anong0u0
  7. // @match https://ani.gamer.com.tw/animeList.php*
  8. // @match https://ani.gamer.com.tw/mygather.php*
  9. // @match https://ani.gamer.com.tw/*
  10. // @icon https://i.imgur.com/2aijUa9.png
  11. // @grant GM_xmlhttpRequest
  12. // @license MIT
  13. // ==/UserScript==
  14.  
  15. const delay = (ms = 0) => {return new Promise((r)=>{setTimeout(r, ms)})}
  16.  
  17. const waitElementLoad = (elementSelector, selectCount = 1, tryTimes = 1, interval = 0) =>
  18. {
  19. return new Promise(async (resolve, reject)=>
  20. {
  21. let t = 1, result;
  22. while(true)
  23. {
  24. if(selectCount != 1) {if((result = document.querySelectorAll(elementSelector)).length >= selectCount) break;}
  25. else {if(result = document.querySelector(elementSelector)) break;}
  26.  
  27. if(tryTimes>0 && ++t>tryTimes) return reject(new Error("Wait Timeout"));
  28. await delay(interval);
  29. }
  30. resolve(result);
  31. })
  32. }
  33.  
  34. let lastRequestTime = 0;
  35. const requests = (method, url, data = null, headers = {}) => {
  36. return new Promise(async (resolve) => {
  37. //console.log(`Requesting ${url}`);
  38. const timeSinceLastRequest = Date.now() - lastRequestTime;
  39. if (timeSinceLastRequest < 400) {
  40. //console.log(`Delaying request by ${400 - timeSinceLastRequest}ms`);
  41. await delay(400 - timeSinceLastRequest); // avoid 429
  42. }
  43. lastRequestTime = Date.now();
  44.  
  45. GM_xmlhttpRequest({
  46. method: method,
  47. url: url,
  48. headers: headers,
  49. data: data,
  50. onload: resolve
  51. });
  52. });
  53. };
  54.  
  55. const ls = localStorage,
  56. cacheTime = 604800000; // 7 days
  57. let debounceID = 0;
  58. const ratinger = {
  59. // ls["ratingCache"]["refID"] = [rating, imgID, cacheTimestamp]
  60. cache: {},
  61. updateCache: function(){this.cache = this.lsGet("ratingCache") || {}},
  62. updateLS: function(){
  63. clearTimeout(debounceID);
  64. debounceID = setTimeout(()=>{this.lsSet("ratingCache", this.cache)}, 300);
  65. },
  66. get: async function(refID) {
  67. if(refID in this.cache && Date.now() - this.cache[refID][2] < cacheTime) return this.cache[refID];
  68. const res = (await requests("get", `https://ani.gamer.com.tw/animeRef.php?sn=${refID}`));
  69. const rating = JSON.parse(`[${res.responseText.match(/(?<='#acg_review', )[^}]+}/)}]`);
  70. const avgRating = ([1,2,3,4,5].reduce((adder, score)=>Number(adder) + rating[1][score]*score, 0) / rating[0]).toFixed(4);
  71. const imgID = res.responseText.match(/(?<=https:\/\/p2\.bahamut\.com\.tw\/B\/ACG\/c\/\d{2}\/)0*([^\.]+)/)[1];
  72. if(avgRating==Infinity)
  73. {
  74. const score = [1,2,3,4,5].reduce((adder, score)=>Number(adder) + rating[1][score]*score, 0),
  75. total = [1,2,3,4,5].reduce((adder, score)=>Number(adder) + rating[1][score], 0),
  76. avg = (score/total).toFixed(4);
  77. this.cache[refID] = [avg, imgID, Date.now()-cacheTime+3600000];
  78. }
  79. else this.cache[refID] = [avgRating, imgID, Date.now()];
  80. this.updateLS();
  81. return this.cache[refID];
  82. },
  83. lsGet: function(key, jsonParse = true) {return jsonParse ? JSON.parse(ls.getItem(key) || "{}") : ls.getItem(key)},
  84. lsSet: function(key, value, stringify = true) {ls.setItem(key, stringify?JSON.stringify(value):value)}
  85. };
  86. ratinger.updateCache();
  87.  
  88. (async ()=>
  89. {
  90. Node.prototype.prependChild = function(element) {return this.insertBefore(element, this.firstChild)}
  91. const lists = await waitElementLoad("div.theme-list-block", 0, 10, 200)
  92. for (const list of lists)
  93. {
  94. let count = 0
  95. const label = document.createElement("label")
  96. label.style="padding-left:1em;"
  97. label.innerHTML=`<input checked="true" type="checkbox" style="width:1.5em;height:1.5em;">依評分排序`
  98. list.parentElement.querySelector(".theme-title-block").firstElementChild.append(label)
  99.  
  100. let debounceID = 0;
  101. const observer = new MutationObserver((m)=>{
  102. if(m.some((e)=>[...e.addedNodes].some((e)=>e.nodeName!="A")))
  103. {
  104. // console.log("in update")
  105. clearTimeout(debounceID)
  106. debounceID = setTimeout(update, 500)
  107. }
  108. })
  109.  
  110. const update = async () => {
  111. observer.disconnect()
  112. // console.log("start update")
  113. const elements = [...list.querySelectorAll("a.theme-list-main[href^='animeRef.php?sn=']")]
  114. const tip = document.createElement("span")
  115. tip.style="padding-left:1em;"
  116. tip.innerText = `讀取評分中(${count}/${elements.length})`
  117. label.insertAdjacentElement("beforebegin", tip)
  118. for (const element of elements)
  119. {
  120. if(element.rating) continue;
  121. const refID = element.href.match(/(?<=sn=)\d+/g);
  122. const [avgRating, imgID] = (await ratinger.get(refID)).map((v)=>Number(v));
  123.  
  124. const ratingBlock = document.createElement("div")
  125. ratingBlock.className = "anime-label-block"
  126. ratingBlock.style = "top: 6px;bottom: unset;"
  127. ratingBlock.innerHTML = `<span class="label-edition color-paid" style="background-color: var(--anime-primary-color);">${avgRating.toFixed(2)}</span>`
  128. element.querySelector(".theme-img-block").append(ratingBlock)
  129. element.rating = avgRating
  130. element.order = count
  131. tip.innerText = `讀取評分中(${++count}/${elements.length})`
  132. }
  133. tip.remove()
  134. label.onclick = ()=>
  135. {
  136. if(label.firstElementChild.checked) elements.sort((a, b)=> a.rating-b.rating).forEach((e)=>{list.prependChild(e)})
  137. else elements.sort((a, b)=> b.order-a.order).forEach((e)=>{list.prependChild(e)})
  138. }
  139. if(label.firstElementChild.checked) label.onclick()
  140. observer.observe(list, {childList: true, subtree: true})
  141. }
  142. await update().then(()=>{observer.observe(list, {childList: true, subtree: true})})
  143. }
  144.  
  145. for(const e of document.querySelectorAll(`a.anime-card-block[href^="animeVideo.php?sn="]`))
  146. { // homepage timeline anime
  147. const refID = e.parentElement.parentElement.parentElement.getAttribute("data-animesn")
  148. const avgRating = Number((await ratinger.get(refID))[0]);
  149. const ratingBlock = document.createElement("div")
  150. ratingBlock.className = "anime-label-block"
  151. ratingBlock.innerHTML = `<span class="label-edition color-paid" style="background-color: var(--anime-primary-color);">${avgRating.toFixed(2)}</span>`;
  152. if(e.querySelector(".anime-hours-block"))
  153. {
  154. ratingBlock.style = "position: absolute;bottom: -2px;right: -40px;"
  155. e.querySelector(".anime-hours-block").append(ratingBlock)
  156. }
  157. else
  158. {
  159. ratingBlock.style = "position: absolute;top: 6px;left: 6px;width: auto;"
  160. e.querySelector(".anime-pic-block").append(ratingBlock)
  161. }
  162. };
  163. // TODO: sort anime by rating (read all page)
  164. })();
  165.  
  166.  
  167.  
  168.  

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址