Sleazy Fork is available in English.

GBT scroller-coaster (GBTSC)

[BETA] Infinity scrolling for videos (main, search, channels) & photos (photos, search, channels)

  1. // ==UserScript==
  2. // @name GBT scroller-coaster (GBTSC)
  3. // @namespace _pc
  4. // @version 0.96
  5. // @license MIT
  6. // @description [BETA] Infinity scrolling for videos (main, search, channels) & photos (photos, search, channels)
  7. // @author verydelight
  8. // @match *://*.gayboystube.com/*
  9. // @connect gayboystube.com
  10. // @icon https://www.gayboystube.com/favicon.ico
  11. // @run-at document-end
  12. // @grant GM.xmlHttpRequest
  13. // @grant GM_download
  14. // @grant GM_xmlHttpRequest
  15. // @grant GM.download
  16. // @require https://code.jquery.com/jquery-3.6.0.min.js
  17. // ==/UserScript==
  18. 'use strict';
  19. const path = window.location.pathname;
  20. if (
  21. path === "" ||
  22. path === "/" ||
  23. path.startsWith("/channels") ||
  24. path.startsWith("/search/videos") ||
  25. path.startsWith("/photos") ||
  26. path.startsWith("/search/photos")
  27. ){
  28. let area = window.location.pathname.split("/");
  29. let testArea,baseUrl
  30. let videoPage=false;
  31. const identifier = $('div[id]')[2].id;
  32. testArea = area[2] ? `${area[1]}/${area[2]}` : (area[1] || "videos");
  33. switch(testArea) {
  34. case "videos":
  35. baseUrl = "https://www.gayboystube.com/page";
  36. gbtScroller(identifier,baseUrl);
  37. videoPage = true;
  38. break;
  39. case "search/videos":
  40. baseUrl = "https://www.gayboystube.com/search/videos/"+area[area.length - 2]+"/page";
  41. gbtScroller(identifier,baseUrl);
  42. videoPage = true;
  43. break;
  44. case testArea.match(/^channels\/[0-9]{1,3}/)?.input:
  45. baseUrl = "https://www.gayboystube.com/channels/"+area[area.length - 3]+"/"+area[area.length - 2]+"/page";
  46. gbtScroller(identifier,baseUrl);
  47. videoPage = true;
  48. break;
  49. case "photos":
  50. baseUrl = "https://www.gayboystube.com/photos/page";
  51. gbtScroller(identifier,baseUrl);
  52. break;
  53. case "search/photos":
  54. baseUrl = "https://www.gayboystube.com/search/photos/"+area[area.length - 2]+"/page";
  55. gbtScroller(identifier,baseUrl);
  56. break;
  57. case "photos/channels":
  58. baseUrl = "https://www.gayboystube.com/photos/channels/"+area[area.length - 2]+"/page";
  59. gbtScroller(identifier,baseUrl);
  60. break;
  61. default:
  62. }
  63. function gbtScroller(identifier,baseUrl){
  64. const style = document.createElement('style')
  65. style.type = 'text/css';
  66. style.textContent = `.gbtloader {
  67. width: 60px;
  68. display: block;
  69. aspect-ratio: 4;
  70. background: radial-gradient(circle closest-side,#009ec5CC 90%,#0000) 0/calc(100%/3) 100% space;
  71. clip-path: inset(0 100% 0 0);
  72. animation: l1 0.8s steps(4) infinite;
  73. transform: translate(10px, 10px);
  74. }
  75. @keyframes l1 {
  76. to {clip-path: inset(0 -34% 0 0);}
  77. }`;
  78. document.head.appendChild(style);
  79. let videoListing = document.getElementById(identifier);
  80. videoListing.nextElementSibling.remove();
  81. let loadTrigger = document.createElement("postloader");
  82. loadTrigger.id = 'postloader-1';
  83. loadTrigger.classList.add('item','item-col','gbtloader');
  84. //loadTrigger.append("(Loading...)");
  85. videoListing.append(loadTrigger)
  86. let target = videoListing.getElementsByTagName('postloader')[0];
  87. let options = {
  88. root: null,
  89. rootMargin: '0px',
  90. threshold: 0
  91. }
  92. let firstCallImminent = true;
  93. if (target.getBoundingClientRect().top < window.innerHeight && target.getBoundingClientRect().bottom > 0) {
  94. // The target is within the viewport
  95. //callback([{isIntersecting: true}]);
  96. firstCallImminent = false;
  97. }
  98. let observer = new IntersectionObserver(callback, options);
  99. if (target){ observer.observe(target); }
  100. function callback() {
  101. if(!firstCallImminent){
  102. observer.unobserve(target);
  103. var nextPage = parseInt(loadTrigger.id.split("-")[1])+1;
  104. var nextPageUrl = baseUrl+nextPage+".html";
  105. loadNextPage(nextPageUrl,nextPage);
  106. //firstCallImminent = true;
  107. }else{
  108. firstCallImminent = false;
  109. }
  110. }
  111. async function loadNextPage(url,nextPage) {
  112. console.log("loading:",url);
  113. try {
  114. const response = await GM.xmlHttpRequest({
  115. method: 'GET',
  116. responseType: 'document',
  117. url: url,
  118. });
  119. const newResults = response.response;
  120. var childResults = newResults.getElementById(identifier).children;
  121. while (childResults.length > 0)
  122. {
  123. if (videoPage){
  124. if(childResults[0].querySelector('img')){
  125. const firstChildNode = childResults[0].firstElementChild;
  126. const lastChildNode = childResults[0].lastElementChild;
  127. const newWrapper = document.createElement("div");
  128. newWrapper.classList.add("item", "item-col");
  129. const aImg = childResults[0].querySelector('a');
  130. aImg.removeAttribute('class');
  131. aImg.setAttribute("data-title",aImg.title);
  132. aImg.removeAttribute('title');
  133. aImg.classList.add("image");
  134. const videoImage = firstChildNode.querySelector('img');
  135. const videoElement = document.createElement("video");
  136. videoElement.alt = videoImage.getAttribute("alt");
  137. videoElement.poster = videoImage.getAttribute("data-src");
  138. videoElement.controls = false;
  139. videoElement.style.objectFit = "cover";
  140. videoElement.style.position = "absolute";
  141. videoElement.style.left = "0px";
  142. videoElement.style.top = "0px";
  143. videoElement.setAttribute('webkit-playsinline', 'true');
  144. videoElement.setAttribute('playsinline', 'true');
  145. videoElement.preload = "none";
  146. videoElement.height = videoImage.getAttribute("height");
  147. videoElement.width = videoImage.getAttribute("width");
  148. videoElement.addEventListener('mouseover', () => {
  149. if(!videoElement.src){
  150. const previewUrl = videoImage.getAttribute("data-preview");
  151. downloadPreview(previewUrl);
  152. }
  153. async function downloadPreview(url) {
  154. try {
  155. const response = await GM.xmlHttpRequest({
  156. method: 'GET',
  157. responseType: 'blob',
  158. url: url,
  159. headers: {
  160. "Content-Type": "video/mp4", "Accept": "video/mp4"
  161. },
  162. });
  163. const blob = new Blob([response.response],{type: 'video/mp4'});
  164. videoElement.src = URL.createObjectURL(blob);
  165. } catch (err) {
  166. console.error("GBTSC: Error in fetching and downloading preview file:", err);
  167. }
  168. }
  169. videoElement.play();
  170. });
  171. videoElement.addEventListener('mouseleave', () => {
  172. videoElement.style.display = 'none';
  173. videoElement.load();
  174. });
  175. videoImage.addEventListener('mouseenter', () => {
  176. videoElement.style.display = '';
  177. videoElement.load();
  178. });
  179. videoElement.style.display = 'none';
  180. newWrapper.append(firstChildNode);
  181. videoImage.insertAdjacentElement('afterend', videoElement)
  182. newWrapper.append(lastChildNode);
  183. videoListing.appendChild(newWrapper);
  184. }else{
  185. childResults[0].remove()
  186. }
  187. }else{
  188. videoListing.appendChild(childResults[0]);
  189. }
  190. }
  191. //firstCallImminent = true;
  192. loadTrigger.remove();
  193. loadTrigger.id = 'postloader-'+nextPage;
  194. videoListing.append(loadTrigger);
  195. if (target.getBoundingClientRect().top < window.innerHeight && target.getBoundingClientRect().bottom > 0) {
  196. firstCallImminent = false;
  197. }else{
  198. firstCallImminent = true;
  199. }
  200. //console.log("appended");
  201. observer.observe(target);
  202. } catch (err) {
  203. console.error("GBTSC: Error in fetching and downloading next videos:", err);
  204. observer.unobserve(target);
  205. loadTrigger.remove();
  206. }
  207. console.log("loaded: ",url);
  208. }
  209. }
  210. }