LPSG Video Unlocker

Automatically unlocks and enhances your LPSG browsing experience

  1. // ==UserScript==
  2. // @name LPSG Video Unlocker
  3. // @namespace MBing & CurlyWurly
  4. // @version 3.8
  5. // @description Automatically unlocks and enhances your LPSG browsing experience
  6. // @author MBing & CurlyWurly
  7. // @match https://www.lpsg.com/*
  8. // @icon data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiA/Pjxzdmcgdmlld0JveD0iMCAwIDI1NiAyNTYiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHJlY3QgZmlsbD0ibm9uZSIgaGVpZ2h0PSIyNTYiIHdpZHRoPSIyNTYiLz48cGF0aCBkPSJNOTMuMiwxMjIuOEE3MC4zLDcwLjMsMCwwLDEsODgsOTZhNzIsNzIsMCwxLDEsNzIsNzIsNzAuMyw3MC4zLDAsMCwxLTI2LjgtNS4yaDBMMTIwLDE3Nkg5NnYyNEg3MnYyNEgzMlYxODRsNjEuMi02MS4yWiIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjRkZENzAwIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiIHN0cm9rZS13aWR0aD0iMTYiLz48Y2lyY2xlIGN4PSIxODAiIGN5PSI3NiIgcj0iMTIiIGZpbGw9IiNGRkQ3MDAiLz48L3N2Zz4=
  9. // @grant none
  10. // @license MIT
  11. // ==/UserScript==
  12.  
  13. (function () {
  14. 'use strict';
  15.  
  16. const volume = 0.1;
  17. const formats = ['mp4', 'm4v', 'mov'];
  18.  
  19. var easterEggPoster = document.getElementsByClassName("video-easter-egg-poster");
  20. var videoDiv = [];
  21. var imageUrl;
  22. var newDiv;
  23.  
  24. // Create video elements with all format sources
  25. for (var i = easterEggPoster.length - 1; i > -1; i--) {
  26. imageUrl = easterEggPoster[i].children[0].src;
  27. let sourceElements = formats.map(format => {
  28. let videoUrl = imageUrl.replace("attachments/posters", "video")
  29. .replace("/lsvideo/thumbnails", "lsvideo/videos")
  30. .replace(".jpg", `.${format}`);
  31.  
  32. // Set correct MIME type for each format
  33. let mimeType;
  34. switch (format) {
  35. case 'mp4':
  36. mimeType = 'video/mp4';
  37. break;
  38. case 'm4v':
  39. mimeType = 'video/x-m4v';
  40. break;
  41. case 'mov':
  42. mimeType = 'video/quicktime';
  43. break;
  44. }
  45.  
  46. return `<source data-src="${videoUrl}" src="${videoUrl}" type="${mimeType}">`;
  47. }).join('');
  48.  
  49. videoDiv[i] = `<video onloadstart="this.volume=${volume}"
  50. style="width: 100%; height: auto; display: block;"
  51. playsinline
  52. controls=""
  53. data-xf-init="video-init"
  54. data-poster="${imageUrl}"
  55. class="message-cell--main-video"
  56. poster="${imageUrl}">
  57. ${sourceElements}
  58. <div class="bbMediaWrapper-fallback">Your browser is not able to display this video.</div>
  59. </video>`;
  60.  
  61. newDiv = document.createElement("div");
  62. newDiv.setAttribute("class", "newVideoDiv");
  63. newDiv.innerHTML = videoDiv[i];
  64. easterEggPoster[i].parentElement.parentElement.append(newDiv);
  65. }
  66.  
  67. // Remove original elements
  68. for (i = easterEggPoster.length - 1; i > -1; i--) {
  69. easterEggPoster[i].parentElement.parentElement.removeChild(easterEggPoster[i].parentElement);
  70. }
  71.  
  72. // Remove blockers and overlays
  73. ['video-easter-egg-blocker', 'video-easter-egg-overlay'].forEach(className => {
  74. var elements = document.getElementsByClassName(className);
  75. for (var j = elements.length - 1; j > -1; j--) {
  76. elements[j].parentElement.removeChild(elements[j]);
  77. }
  78. });
  79.  
  80. // Set volume for all video players
  81. var allVideoPlayers = document.getElementsByTagName('video');
  82. for (i = allVideoPlayers.length - 1; i > -1; i--) {
  83. allVideoPlayers[i].volume = volume;
  84. }
  85.  
  86. // Enhanced image loading
  87. function unlockAllImages() {
  88. document.querySelectorAll('img[loading="lazy"]').forEach(img => {
  89. img.loading = 'eager';
  90. if (img.dataset.src) {
  91. img.src = img.dataset.src;
  92. }
  93. });
  94. }
  95.  
  96. // Add this function to create the media gallery button and functionality
  97. function createMediaButton() {
  98. // Create button
  99. const mediaBtn = document.createElement("a");
  100. mediaBtn.innerHTML = '<span class="button-text"><i class="fa--xf fas fa-images" aria-hidden="true"></i><span class="u-srOnly">Media</span></span>';
  101. mediaBtn.className = "button--scroll ripple-JsOnly button";
  102. mediaBtn.style.cssText = "margin: 2px;";
  103.  
  104. mediaBtn.addEventListener('click', () => {
  105. // Create gallery container
  106. const gallery = document.createElement("div");
  107. gallery.style.cssText = `
  108. position: fixed;
  109. top: 0;
  110. left: 0;
  111. right: 0;
  112. bottom: 0;
  113. background: rgba(0,0,0,0.9);
  114. z-index: 9999;
  115. overflow-y: auto;
  116. padding: 20px;
  117. display: grid;
  118. grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
  119. gap: 20px;
  120. `;
  121.  
  122. // Collect all media in order of appearance
  123. // Collect all media in order of appearance
  124. const media = [...document.querySelectorAll('.message-cell--main img, .message-cell--main video')]
  125. .filter(element => {
  126. // Only include images with '/attachments/' in URL and all videos
  127. return element.tagName.toLowerCase() === 'video' ||
  128. (element.tagName.toLowerCase() === 'img' && element.src.includes('/attachments/'));
  129. });
  130.  
  131. // Add media to gallery in original order
  132. media.forEach(element => {
  133. const clone = element.cloneNode(true);
  134. clone.style.width = '100%';
  135. clone.style.height = 'auto';
  136. clone.style.maxHeight = '500px';
  137. clone.style.objectFit = 'contain';
  138.  
  139. // If it's an image, wrap it in a clickable link to the full version
  140. if (element.tagName.toLowerCase() === 'img') {
  141. // Find the parent anchor tag that contains the full image URL
  142. const parentAnchor = element.closest('a[href*="/attachments/"]');
  143. if (parentAnchor) {
  144. const wrapper = document.createElement('a');
  145. wrapper.href = parentAnchor.href;
  146. wrapper.target = '_blank';
  147. wrapper.style.cursor = 'pointer';
  148. wrapper.appendChild(clone);
  149. gallery.appendChild(wrapper);
  150. } else {
  151. gallery.appendChild(clone);
  152. }
  153. } else {
  154. gallery.appendChild(clone);
  155. }
  156. });
  157.  
  158.  
  159.  
  160. // Add close button
  161. const closeBtn = document.createElement("button");
  162. closeBtn.innerHTML = "✖️";
  163. closeBtn.style.cssText = `
  164. position: fixed;
  165. top: 10px;
  166. right: 10px;
  167. padding: 10px;
  168. background: white;
  169. border: none;
  170. border-radius: 50%;
  171. cursor: pointer;
  172. z-index: 10000;
  173. `;
  174. closeBtn.onclick = () => document.body.removeChild(gallery);
  175.  
  176. gallery.appendChild(closeBtn);
  177. document.body.appendChild(gallery);
  178. });
  179.  
  180. // Add button to scroll buttons container
  181. const scrollButtons = document.querySelector('.u-scrollButtons');
  182. if (scrollButtons) {
  183. scrollButtons.appendChild(mediaBtn);
  184. }
  185. }
  186.  
  187. // Run image loading immediately and after a delay to catch dynamic content
  188. unlockAllImages();
  189. setTimeout(unlockAllImages, 1000);
  190. createMediaButton();
  191. })();