Sleazy Fork is available in English.

Xhamster - Video Speed Button (Tweaked)

Add speed buttons to Xhamster - Fork of Video Speed Button (by Braden Best) v.1.0.10 - Adaptation (2024.08)

질문, 리뷰하거나, 이 스크립트를 신고하세요.
  1. // ==UserScript==
  2. // @name Xhamster - Video Speed Button (Tweaked)
  3. // @description Add speed buttons to Xhamster - Fork of Video Speed Button (by Braden Best) v.1.0.10 - Adaptation (2024.08)
  4. // @namespace bradenscode
  5. // @version 1.0.10
  6. // @copyright 2017, Braden Best
  7. // @run-at document-end
  8. // @author Braden Best - tweak Janvier57
  9. // @grant none
  10. //
  11. // @match *://*.youtube.com/*
  12. // @match *://youtube.com/*
  13. // @match *://*.vimeo.com/*
  14. // @match *://vimeo.com/*
  15.  
  16.  
  17. // @match https://*xhamster.com/videos/*
  18. // @exclude https://*xhamster.com/videos/latest*
  19. // @exclude https://*xhamster.com/videos/recommended*
  20.  
  21. // ==/UserScript==
  22.  
  23. // To add a new site: add a @match above, and modify loader_data.container_candidates near the bottom
  24.  
  25. const CONTROLLER_VSB = 0; // normal controller (video speed buttons). Uses lots of loops to enforce speed
  26. const CONTROLLER_VSC = 1; // alternative controller (video speed controller). Keyboard-only, minimalistic, no loops
  27.  
  28. const controller_type = CONTROLLER_VSB;
  29. // change this to use the experimental CONTROLLER_VSC, which is keyboard-only
  30. // and extremely minimalistic, but is also fast, lightweight on memory usage,
  31. // and doesn't use any loops. Try it out, see if it works better for you.
  32. // The controls are the same as VSB. + to increase the speed, - to decrease,
  33. // * to reset to 1.
  34.  
  35. function video_speed_buttons(anchor, video_el){
  36. if(!anchor || !video_el)
  37. return null;
  38.  
  39. const COLOR_SELECTED = "#FF5500",
  40. COLOR_NORMAL = "grey",
  41. BUTTON_SIZE = "120%",
  42. DEFAULT_SPEED = 1.0,
  43. LABEL_TEXT = "Video Speed: ",
  44. ALLOW_EXTERNAL_ACCESS = false;
  45.  
  46. const BUTTON_TEMPLATES = [
  47. ["25%", 0.25],
  48. ["50%", 0.5],
  49. ["Normal", 1],
  50. ["1.5x", 1.5],
  51. ["2x", 2],
  52. ["3x", 3],
  53. ["4x", 4],
  54. ["8x", 8],
  55. ["16x", 16]
  56. ];
  57.  
  58. const buttons = {
  59. head: null,
  60. selected: null,
  61. last: null
  62. };
  63.  
  64. const keyboard_controls = [
  65. ["-", "Speed Down", function(ev){
  66. if(is_comment_box(ev.target))
  67. return false;
  68.  
  69. (buttons.selected || buttons.head)
  70. .getprev()
  71. .el
  72. .dispatchEvent(new MouseEvent("click"));
  73. }],
  74. ["+", "Speed Up", function(ev){
  75. if(is_comment_box(ev.target))
  76. return false;
  77.  
  78. (buttons.selected || buttons.head)
  79. .getnext()
  80. .el
  81. .dispatchEvent(new MouseEvent("click"));
  82. }],
  83. ["*", "Reset Speed", function(ev){
  84. let selbtn = buttons.head;
  85. let result = null;
  86.  
  87. if(is_comment_box(ev.target))
  88. return false;
  89.  
  90. while(selbtn !== null && result === null)
  91. if(selbtn.speed === DEFAULT_SPEED)
  92. result = selbtn;
  93. else
  94. selbtn = selbtn.next;
  95.  
  96. if(result === null)
  97. result = buttons.head;
  98.  
  99. result.el.dispatchEvent(new MouseEvent("click"));
  100. }],
  101. ["?", "Show Help", function(ev){
  102. let infobox;
  103.  
  104. if(is_comment_box(ev.target))
  105. return false;
  106.  
  107. (infobox = Infobox(container))
  108. .log("Keyboard Controls (click to close)<br>");
  109.  
  110. keyboard_controls.forEach(function([key, description]){
  111. infobox.log(` [${key}] ${description}<br>`);
  112. });
  113. }]
  114. ];
  115.  
  116. const container = (function(){
  117. let div = document.createElement("div");
  118. let prev_node = null;
  119.  
  120. div.className = "vsb-container";
  121. div.style.borderBottom = "1px solid #ccc";
  122. div.style.marginBottom = "10px";
  123. div.style.paddingBottom = "10px";
  124. div.appendChild(SpeedButtonLabel(LABEL_TEXT));
  125.  
  126. BUTTON_TEMPLATES.forEach(function(button){
  127. let speedButton = SpeedButton(...button, div);
  128.  
  129. if(buttons.head === null)
  130. buttons.head = speedButton;
  131.  
  132. if(prev_node !== null){
  133. speedButton.prev = prev_node;
  134. prev_node.next = speedButton;
  135. }
  136.  
  137. prev_node = speedButton;
  138.  
  139. if(speedButton.speed == DEFAULT_SPEED)
  140. speedButton.select();
  141. });
  142.  
  143. return div;
  144. })();
  145.  
  146. function is_comment_box(el){
  147. const candidate = [
  148. ".comment-simplebox-text",
  149. "textarea"
  150. ].map(c => document.querySelector(c))
  151. .find(el => el !== null);
  152.  
  153. if(candidate === null){
  154. logvsb("video_speed_buttons::is_comment_box", "no candidate for comment box. Assuming false.");
  155. return 0;
  156. }
  157.  
  158. return el === candidate;
  159. }
  160.  
  161. function Infobox(parent){
  162. let el = document.createElement("pre");
  163.  
  164. el.style.font = "1em monospace";
  165. el.style.borderTop = "1px solid #ccc";
  166. el.style.marginTop = "10px";
  167. el.style.paddingTop = "10px";
  168.  
  169. el.addEventListener("click", function(){
  170. parent.removeChild(el);
  171. });
  172.  
  173. parent.appendChild(el);
  174.  
  175. function log(msg){
  176. el.innerHTML += msg;
  177. }
  178.  
  179. return {
  180. el,
  181. log
  182. };
  183. }
  184.  
  185. let playbackRate_data = {
  186. rate: 1,
  187. video: null,
  188. };
  189.  
  190. function setPlaybackRate(el, rate){
  191. if(el) {
  192. el.playbackRate = rate;
  193. playbackRate_data.rate = rate;
  194. playbackRate_data.video = el;
  195. }
  196. else
  197. logvsb("video_speed_buttons::setPlaybackRate", "video element is null or undefined", 1);
  198. }
  199.  
  200. function SpeedButtonLabel(text){
  201. let el = document.createElement("span");
  202.  
  203. el.innerHTML = text;
  204. el.style.marginRight = "10px";
  205. el.style.fontWeight = "bold";
  206. el.style.fontSize = BUTTON_SIZE;
  207. el.style.color = COLOR_NORMAL;
  208.  
  209. return el;
  210. }
  211.  
  212. function SpeedButton(text, speed, parent){
  213. let el = SpeedButtonLabel(text);
  214. let self;
  215.  
  216. el.style.cursor = "pointer";
  217.  
  218. el.addEventListener("click", function(){
  219. setPlaybackRate(video_el, speed);
  220. self.select();
  221. });
  222.  
  223. parent.appendChild(el);
  224.  
  225. function select(){
  226. if(buttons.last !== null)
  227. buttons.last.el.style.color = COLOR_NORMAL;
  228.  
  229. buttons.last = self;
  230. buttons.selected = self;
  231. el.style.color = COLOR_SELECTED;
  232. }
  233.  
  234. function getprev(){
  235. if(self.prev === null)
  236. return self;
  237.  
  238. return buttons.selected = self.prev;
  239. }
  240.  
  241. function getnext(){
  242. if(self.next === null)
  243. return self;
  244.  
  245. return buttons.selected = self.next;
  246. }
  247.  
  248. return self = {
  249. el,
  250. text,
  251. speed,
  252. prev: null,
  253. next: null,
  254. select,
  255. getprev,
  256. getnext
  257. };
  258. }
  259.  
  260. function kill(){
  261. anchor.removeChild(container);
  262. document.body.removeEventListener("keydown", ev_keyboard);
  263. }
  264.  
  265. function set_video_el(new_video_el){
  266. video_el = new_video_el;
  267. }
  268.  
  269. function ev_keyboard(ev){
  270. let match = keyboard_controls.find(([key, unused, callback]) => key === ev.key);
  271. let callback = (match || {2: ()=>null})[2];
  272.  
  273. callback(ev);
  274. }
  275.  
  276. setPlaybackRate(video_el, DEFAULT_SPEED);
  277. anchor.insertBefore(container, anchor.firstChild);
  278. document.body.addEventListener("keydown", ev_keyboard);
  279.  
  280. return {
  281. controls: keyboard_controls,
  282. buttons,
  283. kill,
  284. SpeedButton,
  285. Infobox,
  286. setPlaybackRate,
  287. is_comment_box,
  288. set_video_el,
  289. playbackRate_data,
  290. ALLOW_EXTERNAL_ACCESS,
  291. };
  292. }
  293.  
  294. video_speed_buttons.from_query = function(anchor_q, video_q){
  295. return video_speed_buttons(
  296. document.querySelector(anchor_q),
  297. document.querySelector(video_q));
  298. }
  299.  
  300. // Multi-purpose Loader (defaults to floating on top right)
  301. /*const loader_data = {
  302. container_candidates: [
  303. // YouTube
  304. "div#above-the-fold",
  305. "div#title.style-scope.ytd-watch-metadata",
  306. "div#container.ytd-video-primary-info-renderer",
  307. "div#watch-header",
  308. "div#watch7-headline",
  309. "div#watch-headline-title",
  310. // Vimeo
  311. ".clip_info-wrapper",
  312. ],
  313. */
  314. // janvier57 TWEAK
  315. const loader_data = {
  316. container_candidates: [
  317. // YouTube
  318. // YouTube
  319. "div#container.ytd-video-primary-info-renderer",
  320. "div#watch-header",
  321. "div#watch7-headline",
  322. "div#watch-headline-title",
  323. // Vimeo
  324. ".clip_info-wrapper",
  325. // Xhamster (OLD)
  326. // "#playerBox >.head.gr" ,
  327. // Xhamster (NEW)
  328. // ".entity-info-container__title" ,
  329. // Xhamster Chrome (new)
  330. // ".video-page .categories-container" ,
  331. // Xhamster NEW DESIGN 2022.05
  332. "#video-tags-list-container" ,
  333. ],
  334.  
  335. css_div: [
  336. "position: fixed",
  337. "top: 0",
  338. "right: 0",
  339. "zIndex: 100000",
  340. "background: rgba(0, 0, 0, 0.8)",
  341. "color: #eeeeee",
  342. "padding: 10px"
  343. ].map(rule => rule.split(/: */)),
  344.  
  345. css_vsb_container: [
  346. "borderBottom: none",
  347. "marginBottom: 0",
  348. "paddingBottom: 0",
  349. ].map(rule => rule.split(/: */))
  350. };
  351.  
  352. function logvsb(where, msg, lvl = 0){
  353. let logf = (["info", "error"])[lvl];
  354.  
  355. console[logf](`[vsb::${where}] ${msg}`);
  356. }
  357.  
  358. function loader_loop(){
  359. let vsbc = () => document.querySelector(".vsb-container");
  360. let candidate;
  361. let default_candidate;
  362. let vsb_handle;
  363.  
  364. if(vsbc() !== null)
  365. return;
  366.  
  367. candidate = loader_data
  368. .container_candidates
  369. .map(candidate => document.querySelector(candidate))
  370. .find(candidate => candidate !== null);
  371.  
  372. default_candidate = (function(){
  373. let el = document.createElement("div");
  374.  
  375. loader_data.css_div.forEach(function([name, value]){
  376. el.style[name] = value; });
  377.  
  378. return el;
  379. }());
  380.  
  381. vsb_handle = video_speed_buttons(candidate || default_candidate, document.querySelector("video"));
  382.  
  383. if(candidate === null){
  384. logvsb("loader_loop", "no candidates for title section. Defaulting to top of page.");
  385. document.body.appendChild(default_candidate);
  386.  
  387. loader_data.css_vsb_container.forEach(function([name, value]){
  388. vsbc().style[name] = value;
  389. });
  390. }
  391.  
  392. // ugly hack to address vimeo automatically resetting the speed
  393. vsb_handle.enforcer_loop_iid = setInterval(function(){
  394. let prdata = vsb_handle.playbackRate_data;
  395.  
  396. if (prdata.video !== null)
  397. prdata.video.playbackRate = prdata.rate;
  398. }, 500);
  399.  
  400. if(vsb_handle.ALLOW_EXTERNAL_ACCESS)
  401. window.vsb = vsb_handle;
  402. }
  403.  
  404. const vsc = {
  405. name: "Video Speed Controller",
  406. getvideo: _ => document.querySelector("video"), // Yep, it's really that simple.
  407. rates: [0.25, 0.5, 1, 1.5, 2, 3, 4, 8, 16],
  408. selrate: 2,
  409. ev_keydown: function(ev) {
  410. let change = 0;
  411.  
  412. if (vsc.getvideo() === null)
  413. return true;
  414.  
  415. if (ev.key === "+")
  416. change = +1;
  417.  
  418. if (ev.key === "-")
  419. change = -1;
  420.  
  421. if (ev.key === "*")
  422. change = -(vsc.selrate - 2);
  423.  
  424. vsc.selrate = (vsc.rates.length + vsc.selrate + change) % vsc.rates.length;
  425. vsc.getvideo().playbackRate = vsc.rates[vsc.selrate];
  426. console.log(`[${vsc.name}] Speed set to ${vsc.rates[vsc.selrate]}`);
  427. }
  428. };
  429.  
  430. if (controller_type === CONTROLLER_VSB) {
  431. setInterval(function(){
  432. if(document.readyState === "complete")
  433. setTimeout(loader_loop, 1000);
  434. }, 1000); // Blame YouTube for this
  435. }
  436. else if (controller_type === CONTROLLER_VSC) {
  437. document.body.addEventListener("keydown", vsc.ev_keydown);
  438. console.clear();
  439. console.log(`[${vsc.name}] loaded`);
  440. }