Better EdPuzzle

Speed up, allow skipping, and stop auto-pausing on EdPuzzle.com.

  1. // ==UserScript==
  2. // @name Better EdPuzzle
  3. // @namespace https://github.com/Enchoseon/better-edpuzzle-userscript/raw/main/better-edpuzzle-userscript.user.js
  4. // @version 0.7.2
  5. // @description Speed up, allow skipping, and stop auto-pausing on EdPuzzle.com.
  6. // @author Enchoseon
  7. // @include *edpuzzle.com/lti/*
  8. // @include *edpuzzle.com/assignments/*
  9. // @include *edpuzzle.com/media/*
  10. // @include *youtube.com/embed*
  11. // @include *youtube-nocookie.com/embed*
  12. // @run-at document-start
  13. // @grant none
  14. // ==/UserScript==
  15.  
  16. (function() {
  17. 'use strict';
  18. const config = { // Edit this variable to set your own settings!
  19. "speed": 2,
  20. }
  21. // =========
  22. // Speedhack
  23. // =========
  24. function speedhack(mediaSource) {
  25. console.log("Media Source: " + mediaSource);
  26. switch(mediaSource) {
  27. case "youtube":
  28. break;
  29. case "vimeo":
  30. break;
  31. case "vimeo_with_controls":
  32. break;
  33. case "edpuzzle":
  34. break;
  35. case "file":
  36. break;
  37. default:
  38. }
  39. }
  40. if (window != window.top && (window.location.href.includes("youtube.com/embed/") || window.location.href.includes("youtube-nocookie.com/embed/")) && window.location.href.includes("origin=https://edpuzzle.com")) { // YouTube speedhack (sticking this in the speedhack function would require a bunch of postMessage BS to get around CORs. Wish I could just grant myself the permission to access the iframe (e.g. document.getElementsByTagName("iframe")[0].contentWindow.document.getElementsByTagName("video")[0].playbackRate = config.speed;))
  41. window.addEventListener("click", function() {
  42. console.log(window.location.href);
  43. document.getElementsByTagName("video")[0].playbackRate = config.speed;
  44. });
  45. }
  46. // ===============
  47. // Anti Auto-Pause
  48. // ===============
  49. window.addEventListener("load", function() { // Make some properties always return a value of false
  50. const propArr = ["hidden", "mozHidden", "msHidden", "webkitHidden"];
  51. for (var i = 0; i < propArr.length; i++) {
  52. Object.defineProperty(document, propArr[i], function() {
  53. value : false
  54. });
  55. }
  56. });
  57. // =========
  58. // API Stuff
  59. // =========
  60. function interceptAPICall(responseText) { // Read the API call for speedhacking (& debugging).
  61. responseText = JSON.parse(responseText);
  62. console.log("API Call Intercepted:");
  63. console.log(responseText);
  64. var mediaSource = null;
  65. if (responseText.medias) {
  66. mediaSource = responseText.medias[0].source;
  67. } else if (responseText.source) {
  68. mediaSource = responseText.source;
  69. }
  70. window.addEventListener("load", speedhack(mediaSource));
  71. }
  72. function modifyAPICall(responseText) { // Modify the API call to allow skipping.
  73. // Occasionally the API returns a responses w/ stuff in different locations—since there isn't actual documentation, it's kind of hard to understand—so we're going to inefficiently brute force it instead
  74. return responseText.replace(`"allowSkipAhead":false`, `"allowSkipAhead":true`);
  75. }
  76. function getAPIPath() { // Get the API path
  77. var urlPath = window.location.pathname;
  78. var apiPath;
  79. if (urlPath.startsWith("/assignments/")) {
  80. var id = urlPath.replace("/assignments/", "")
  81. .replace("/watch", "");
  82. apiPath = "/api/v3/assignments/" + id;
  83. } else if (urlPath.startsWith("/media/")) {
  84. var id = urlPath.replace("/media/", "");
  85. apiPath = "/api/v3/media/" + id;
  86. }
  87. return apiPath;
  88. }
  89. if (window.location.hostname === "edpuzzle.com") { // Intercept XMLHttpRequests (https://stackoverflow.com/a/41899308) (gets around one-time token for LMS & lets us modify the response)
  90. (function(window) {
  91. var OriginalXHR = XMLHttpRequest;
  92. var XHRProxy = function() {
  93. this.xhr = new OriginalXHR();
  94. function delegate(prop) {
  95. Object.defineProperty(this, prop, {
  96. get: function() {
  97. return this.xhr[prop];
  98. },
  99. set: function(value) {
  100. this.xhr.timeout = value;
  101. }
  102. });
  103. }
  104. delegate.call(this, "timeout");
  105. delegate.call(this, "responseType");
  106. delegate.call(this, "withCredentials");
  107. delegate.call(this, "onerror");
  108. delegate.call(this, "onabort");
  109. delegate.call(this, "onloadstart");
  110. delegate.call(this, "onloadend");
  111. delegate.call(this, "onprogress");
  112. };
  113. XHRProxy.prototype.open = function(method, url, async, username, password) {
  114. var ctx = this;
  115. function applyInterceptors(src) {
  116. ctx.responseText = ctx.xhr.responseText;
  117. for (var i = 0; i < XHRProxy.interceptors.length; i++) {
  118. var applied = XHRProxy.interceptors[i](method, url, ctx.responseText, ctx.xhr.status);
  119. if (applied !== undefined) {
  120. ctx.responseText = applied;
  121. }
  122. }
  123. }
  124. function setProps() {
  125. ctx.readyState = ctx.xhr.readyState;
  126. ctx.responseText = ctx.xhr.responseText;
  127. ctx.responseURL = ctx.xhr.responseURL;
  128. ctx.responseXML = ctx.xhr.responseXML;
  129. ctx.status = ctx.xhr.status;
  130. ctx.statusText = ctx.xhr.statusText;
  131. }
  132. this.xhr.open(method, url, async, username, password);
  133. this.xhr.onload = function(evt) {
  134. if (ctx.onload) {
  135. setProps();
  136.  
  137. if (ctx.xhr.readyState === 4) {
  138. applyInterceptors();
  139. }
  140. return ctx.onload(evt);
  141. }
  142. };
  143. this.xhr.onreadystatechange = function (evt) {
  144. if (ctx.onreadystatechange) {
  145. setProps();
  146.  
  147. if (ctx.xhr.readyState === 4) {
  148. applyInterceptors();
  149. }
  150. return ctx.onreadystatechange(evt);
  151. }
  152. };
  153. };
  154. XHRProxy.prototype.addEventListener = function(event, fn) {
  155. return this.xhr.addEventListener(event, fn);
  156. };
  157. XHRProxy.prototype.send = function(data) {
  158. return this.xhr.send(data);
  159. };
  160. XHRProxy.prototype.abort = function() {
  161. return this.xhr.abort();
  162. };
  163. XHRProxy.prototype.getAllResponseHeaders = function() {
  164. return this.xhr.getAllResponseHeaders();
  165. };
  166. XHRProxy.prototype.getResponseHeader = function(header) {
  167. return this.xhr.getResponseHeader(header);
  168. };
  169. XHRProxy.prototype.setRequestHeader = function(header, value) {
  170. return this.xhr.setRequestHeader(header, value);
  171. };
  172. XHRProxy.prototype.overrideMimeType = function(mimetype) {
  173. return this.xhr.overrideMimeType(mimetype);
  174. };
  175.  
  176. XHRProxy.interceptors = [];
  177. XHRProxy.addInterceptor = function(fn) {
  178. this.interceptors.push(fn);
  179. };
  180.  
  181. window.XMLHttpRequest = XHRProxy;
  182. XHRProxy.addInterceptor(function(method, url, responseText, status) {
  183. if (url === getAPIPath()) {
  184. interceptAPICall(responseText);
  185. return modifyAPICall(responseText);
  186. }
  187. });
  188. })(window);
  189. }
  190. })();

QingJ © 2025

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