Iwara Custom Sort

Automatically sort video results in a page on /videos, /images, /subscriptions, /users, and sidebars using customizable sort function.

当前为 2019-02-14 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Iwara Custom Sort
  3. // @version 0.147
  4. // @grant GM.setValue
  5. // @grant GM.getValue
  6. // @grant GM.deleteValue
  7. // @run-at document-start
  8. // @noframes
  9. // @match https://ecchi.iwara.tv/*
  10. // @match https://www.iwara.tv/*
  11. // @match http://ecchi.iwara.tv/*
  12. // @match http://www.iwara.tv/*
  13. // @description Automatically sort video results in a page on /videos, /images, /subscriptions, /users, and sidebars using customizable sort function.
  14. // @namespace https://gf.qytechs.cn/users/245195
  15. // ==/UserScript==
  16.  
  17. /* jshint esversion: 6 */
  18. /* global GM */
  19.  
  20. 'use strict';
  21.  
  22. const additionalPageCount = 0;
  23.  
  24. const logDebug = (...args) => {
  25. const debugging = true;
  26. if (debugging) {
  27. console.log(...args);
  28. }
  29. };
  30.  
  31. const timeout = delay => new Promise(resolve => setTimeout(resolve, delay));
  32. const parsePrefixed = str => Number.parseFloat(str) * (str.includes('k') ? 1000 : 1);
  33. const getNearbyNumber = element => (element ? parsePrefixed(element.parentElement.textContent) : 0);
  34. const evalSortValue = (item, valueExpression) =>
  35. // eslint-disable-next-line no-new-func
  36. new Function('views', 'likes', 'ratio', 'image', 'private', `return (${valueExpression})`)(
  37. item.viewCount,
  38. item.likeCount,
  39. Math.min(item.likeCount / Math.max(1, item.viewCount), 1),
  40. item.imageFactor,
  41. item.privateFactor,
  42. );
  43.  
  44. const sortVideos = (container, valueExpression) => {
  45. const videoDivs = Array.from(container.querySelectorAll('.clearfix'));
  46. const videoItems = videoDivs
  47. .map(div => ({
  48. div,
  49. viewCount: getNearbyNumber(div.querySelector('.glyphicon-eye-open')),
  50. likeCount: getNearbyNumber(div.querySelector('.glyphicon-heart')),
  51. imageFactor: div.querySelector('.glyphicon-th-large') ? 1 : 0,
  52. privateFactor: div.querySelector('.private-video') ? 1 : 0,
  53. }))
  54. .sort((itemA, itemB) =>
  55. evalSortValue(itemB, valueExpression) - evalSortValue(itemA, valueExpression));
  56. videoDivs
  57. .map((div) => {
  58. const anchor = document.createElement('div');
  59. div.before(anchor);
  60. return anchor;
  61. })
  62. .forEach((div, index) => div.replaceWith(videoItems[index].div));
  63. };
  64.  
  65. const sortAllVideos = (valueExpression) => {
  66. const containers = Array.from(document.querySelectorAll('.views-responsive-grid'));
  67. GM.setValue('sortValue', valueExpression);
  68. let sortedCount = 0;
  69. try {
  70. containers.forEach((container) => {
  71. sortVideos(container, valueExpression);
  72. sortedCount += 1;
  73. });
  74. } catch (message) {
  75. alert(message);
  76. }
  77. logDebug(`${sortedCount} containers sorted at ${window.location}`);
  78. };
  79.  
  80. const addPageEmbeds = (URL, pageCount) => {
  81. const params = URL.searchParams;
  82. let page = params.has('page') ? Number.parseInt(params.get('page')) : 0;
  83. for (let pageLeft = pageCount; pageLeft > 0; pageLeft -= 1) {
  84. page += 1;
  85. params.set('page', page);
  86. const nextPage = document.createElement('embed');
  87. nextPage.src = URL;
  88. nextPage.style.display = 'none';
  89. logDebug('page', nextPage.src, pageLeft);
  90. document.documentElement.append(nextPage);
  91. }
  92. };
  93.  
  94. const createUI = async () => {
  95. const sortValueInput = document.createElement('input');
  96. sortValueInput.maxLength = 120;
  97. sortValueInput.size = 60;
  98. const defaultValue = '(ratio / (private * 2.5 + 1) + Math.sqrt(likes) / 3000) / (image + 3)';
  99. sortValueInput.value = await GM.getValue('sortValue', defaultValue);
  100. const sortButton = document.createElement('button');
  101. sortButton.innerHTML = 'Sort';
  102. sortValueInput.addEventListener('keyup', (event) => {
  103. if (event.key !== 'Enter') {
  104. return;
  105. }
  106. sortButton.click();
  107. event.preventDefault();
  108. });
  109. sortButton.addEventListener('click', () => sortAllVideos(sortValueInput.value));
  110. const resetDefaultButton = document.createElement('button');
  111. resetDefaultButton.innerHTML = 'Default';
  112. resetDefaultButton.addEventListener('click', () => {
  113. sortValueInput.value = defaultValue;
  114. });
  115. return {
  116. sortValueInput,
  117. sortButton,
  118. resetDefaultButton,
  119. };
  120. };
  121.  
  122. const addUI = (UI) => {
  123. const UIDiv = document.createElement('div');
  124. UIDiv.style.display = 'inline';
  125. UIDiv.style.margin = '5px';
  126. UIDiv.append(UI.resetDefaultButton, UI.sortValueInput, UI.sortButton);
  127. document.querySelector('#user-links')
  128. .prepend(UIDiv);
  129. };
  130.  
  131. const addVideosToParent = (videoContainers) => {
  132. const parentContainers = window.parent.document.querySelectorAll('.views-responsive-grid');
  133. videoContainers.forEach((container, index) => {
  134. // eslint-disable-next-line no-param-reassign
  135. container.className = '';
  136. if (parentContainers.length > index) {
  137. parentContainers[index].prepend(container);
  138. }
  139. });
  140.  
  141. window.parent.postMessage('iwara custom sort: videosAdded', window.location.origin);
  142. };
  143.  
  144. const initParent = async () => {
  145. if (!document.querySelector('.views-responsive-grid')) {
  146. return;
  147. }
  148. const UI = await createUI();
  149. addUI(UI);
  150. window.addEventListener('message', (event) => {
  151. const originURL = new URL(event.origin);
  152. if (
  153. originURL.hostname === window.location.hostname &&
  154. event.data === 'iwara custom sort: videosAdded'
  155. ) {
  156. sortAllVideos(UI.sortValueInput.value);
  157. }
  158. });
  159. sortAllVideos(UI.sortValueInput.value);
  160. if (/\/(videos|images|subscriptions)$/.test(window.location.pathname)) {
  161. addPageEmbeds(new URL(window.location), additionalPageCount);
  162. }
  163. };
  164.  
  165. const init = async () => {
  166. const videoContainers = Array.from(document.querySelectorAll('.views-responsive-grid'));
  167. if (videoContainers.length > 0) {
  168. await timeout(500);
  169. addVideosToParent(videoContainers);
  170. }
  171. };
  172.  
  173. (() => {
  174. logDebug(`Parsed:${window.location}, ${document.readyState} Parent:`, window.parent);
  175. if (window === window.parent) {
  176. document.addEventListener('DOMContentLoaded', initParent);
  177. } else {
  178. logDebug('I am a child.');
  179. document.addEventListener('DOMContentLoaded', init);
  180. }
  181. })();
  182. /*
  183. // Console Test:
  184. parsePrefixed = str => Number.parseFloat(str) * (str.includes('k') ? 1000 : 1);
  185. getNearbyNumber = element => (element ? parsePrefixed(element.parentElement.textContent) : 0);
  186. videoDivs = Array.from(document.querySelector('.views-responsive-grid').querySelectorAll('.clearfix'));
  187. logFunc = (mapFunc) => {
  188. const items = videoDivs.map(mapFunc);
  189. console.log(items.reduce((acc, value) => acc + value));
  190. const averages = [];
  191. for (let i = 0; i < items.length / 16; i += 1) {
  192. let sum = 0;
  193. for (let j = 0; j < 16; j += 1) {
  194. sum += items[i * 16 + j];
  195. }
  196. averages.push(sum / 16);
  197. }
  198. return averages;
  199. }
  200. console.log(logFunc(div => getNearbyNumber(div.querySelector('.glyphicon-eye-open'))));
  201. console.log(logFunc(div => div.querySelector('.glyphicon-th-large') ? 1 : 0));
  202. console.log(logFunc(div => div.querySelector('.private-video') ? 1 : 0));
  203. console.log(logFunc(div => getNearbyNumber(div.querySelector('.glyphicon-heart')) / getNearbyNumber(div.querySelector('.glyphicon-eye-open'))));
  204. */

QingJ © 2025

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