Dynamic Include Sites Script (Protocol-Independent)

Run on default & user-defined sites using wildcard patterns (ignores protocols), with full management features.

This script should not be not be installed directly. It is a library for other scripts to include with the meta directive // @require https://update.greatest.deepsurf.us/scripts/526770/1585275/Dynamic%20Include%20Sites%20Script%20%28Protocol-Independent%29.js

  1. // ==UserScript==
  2. // @name Site Filter (Protocol-Independent)
  3. // @namespace http://tampermonkey.net/
  4. // @version 2.6
  5. // @description Manage allowed sites dynamically and reference this in other scripts.
  6. // @author blvdmd
  7. // @match *://*/*
  8. // @grant GM_getValue
  9. // @grant GM_setValue
  10. // @grant GM_registerMenuCommand
  11. // @grant GM_download
  12. // @run-at document-start
  13. // ==/UserScript==
  14.  
  15. (function () {
  16. 'use strict';
  17.  
  18. // Exit if the script is running in an iframe or embedded context
  19. if (window.self !== window.top) {
  20. return; // Stop execution for embedded pages
  21. }
  22.  
  23. const USE_EMOJI_FOR_STATUS = true; // Configurable flag to use emoji for true/false status
  24. const SHOW_STATUS_ONLY_IF_TRUE = true; // Configurable flag to show status only if any value is true
  25.  
  26. // ✅ Wait for `SCRIPT_STORAGE_KEY` to be set
  27. function waitForScriptStorageKey(maxWait = 1000) {
  28. return new Promise(resolve => {
  29. const startTime = Date.now();
  30. const interval = setInterval(() => {
  31. if (typeof window.SCRIPT_STORAGE_KEY !== 'undefined') {
  32. clearInterval(interval);
  33. resolve(window.SCRIPT_STORAGE_KEY);
  34. } else if (Date.now() - startTime > maxWait) {
  35. clearInterval(interval);
  36. console.error("🚨 SCRIPT_STORAGE_KEY is not set! Make sure your script sets it **before** @require.");
  37. resolve(null);
  38. }
  39. }, 50);
  40. });
  41. }
  42.  
  43. (async function initialize() {
  44. async function waitForDocumentReady() {
  45. if (document.readyState === "complete") return;
  46. return new Promise(resolve => {
  47. window.addEventListener("load", resolve, { once: true });
  48. });
  49. }
  50. // ✅ Wait for the script storage key
  51. const key = await waitForScriptStorageKey();
  52. if (!key) return;
  53.  
  54. // ✅ Ensure the document is fully loaded before setting `shouldRunOnThisSite`
  55. await waitForDocumentReady();
  56.  
  57. const STORAGE_KEY = `additionalSites_${key}`;
  58.  
  59. function getDefaultList() {
  60. return typeof window.GET_DEFAULT_LIST === "function" ? window.GET_DEFAULT_LIST() : [];
  61. }
  62.  
  63. function normalizeUrl(url) {
  64. if (typeof url !== 'string') {
  65. url = String(url);
  66. }
  67. return url.replace(/^https?:\/\//, '');
  68. }
  69.  
  70. let additionalSites = GM_getValue(STORAGE_KEY, []);
  71.  
  72. let mergedSites = [...new Set([...getDefaultList(), ...additionalSites])].map(item => {
  73. if (typeof item === 'string') {
  74. return { pattern: normalizeUrl(item), preProcessingRequired: false, postProcessingRequired: false };
  75. }
  76. return { ...item, pattern: normalizeUrl(item.pattern) };
  77. });
  78.  
  79. GM_registerMenuCommand("➕ Add Current Site to Include List", addCurrentSiteMenu);
  80. GM_registerMenuCommand("📜 View Included Sites", viewIncludedSites);
  81. GM_registerMenuCommand("🗑️ Delete Specific Entries", deleteEntries);
  82. GM_registerMenuCommand("✏️ Edit an Entry", editEntry);
  83. GM_registerMenuCommand("🚨 Clear All Entries", clearAllEntries);
  84. GM_registerMenuCommand("📤 Export Site List as JSON", exportAdditionalSites);
  85. GM_registerMenuCommand("📥 Import Site List from JSON", importAdditionalSites);
  86.  
  87. async function shouldRunOnThisSite() {
  88. const currentFullPath = normalizeUrl(`${window.top.location.href}`);
  89. return mergedSites.some(item => wildcardToRegex(normalizeUrl(item.pattern)).test(currentFullPath));
  90. }
  91.  
  92. function wildcardToRegex(pattern) {
  93. return new RegExp("^" + pattern
  94. .replace(/[-[\]{}()+^$|#\s]/g, '\\$&')
  95. .replace(/\./g, '\\.')
  96. .replace(/\?/g, '\\?')
  97. .replace(/\*/g, '.*')
  98. + "$");
  99. }
  100.  
  101. function addCurrentSiteMenu() {
  102. const currentHost = window.top.location.hostname; // Use window.top to get the top-level hostname
  103. const currentPath = window.top.location.pathname; // Use window.top to get the top-level pathname
  104. const domainParts = currentHost.split('.');
  105. const baseDomain = domainParts.length > 2 ? domainParts.slice(-2).join('.') : domainParts.join('.');
  106. const secondLevelDomain = domainParts.length > 2 ? domainParts.slice(-2, -1)[0] : domainParts[0];
  107. const options = [
  108. { name: `Preferred Domain Match (*${secondLevelDomain}.*)`, pattern: `*${secondLevelDomain}.*` },
  109. { name: `Base Hostname (*.${baseDomain}*)`, pattern: `*.${baseDomain}*` },
  110. { name: `Base Domain (*.${secondLevelDomain}.*)`, pattern: `*.${secondLevelDomain}.*` },
  111. { name: `Host Contains (*${secondLevelDomain}*)`, pattern: `*${secondLevelDomain}*` },
  112. { name: `Exact Path (${currentHost}${currentPath})`, pattern: normalizeUrl(`${window.top.location.href}`) },
  113. { name: "Custom Wildcard Pattern", pattern: normalizeUrl(`${window.top.location.href}`) }
  114. ];
  115.  
  116. const userChoice = prompt(
  117. "Select an option to add the site:\n" +
  118. options.map((opt, index) => `${index + 1}. ${opt.name}`).join("\n") +
  119. "\nEnter a number or cancel."
  120. );
  121.  
  122. if (!userChoice) return;
  123. const selectedIndex = parseInt(userChoice, 10) - 1;
  124. if (selectedIndex >= 0 && selectedIndex < options.length) {
  125. let pattern = normalizeUrl(options[selectedIndex].pattern);
  126. if (options[selectedIndex].name === "Custom Wildcard Pattern") {
  127. pattern = normalizeUrl(prompt("Edit custom wildcard pattern:", pattern));
  128. if (!pattern.trim()) return alert("Invalid pattern. Operation canceled.");
  129. }
  130.  
  131. const preProcessingRequired = prompt("Is pre-processing required? (y/n)", "n").toLowerCase() === 'y';
  132. const postProcessingRequired = prompt("Is post-processing required? (y/n)", "n").toLowerCase() === 'y';
  133. const onDemandFloatingButtonRequired = prompt("Is on-demand floating button required? (y/n)", "n").toLowerCase() === 'y';
  134. const backgroundChangeObserverRequired = prompt("Is background change observer required? (y/n)", "n").toLowerCase() === 'y';
  135.  
  136. const entry = {
  137. pattern,
  138. preProcessingRequired,
  139. postProcessingRequired,
  140. onDemandFloatingButtonRequired,
  141. backgroundChangeObserverRequired
  142. };
  143. if (!additionalSites.some(item => item.pattern === pattern)) {
  144. additionalSites.push(entry);
  145. GM_setValue(STORAGE_KEY, additionalSites);
  146. mergedSites = [...new Set([...getDefaultList(), ...additionalSites])].map(item => {
  147. if (typeof item === 'string') {
  148. return {
  149. pattern: normalizeUrl(item),
  150. preProcessingRequired: false,
  151. postProcessingRequired: false,
  152. onDemandFloatingButtonRequired: false,
  153. backgroundChangeObserverRequired: false
  154. };
  155. }
  156. return {
  157. ...item,
  158. pattern: normalizeUrl(item.pattern),
  159. onDemandFloatingButtonRequired: item.onDemandFloatingButtonRequired || false,
  160. backgroundChangeObserverRequired: item.backgroundChangeObserverRequired || false
  161. };
  162. });
  163. alert(`✅ Added site with pattern: ${pattern}`);
  164. } else {
  165. alert(`⚠️ Pattern "${pattern}" is already in the list.`);
  166. }
  167. }
  168. }
  169.  
  170. function viewIncludedSites() {
  171. const siteList = additionalSites.map(item => {
  172. const status = formatStatus(item.preProcessingRequired, item.postProcessingRequired, item.onDemandFloatingButtonRequired, item.backgroundChangeObserverRequired);
  173. return `${item.pattern}${status ? ` (${status})` : ''}`;
  174. }).join("\n");
  175. alert(`🔍 Included Sites:\n${siteList || "No sites added yet."}`);
  176. }
  177.  
  178. function deleteEntries() {
  179. if (additionalSites.length === 0) return alert("⚠️ No user-defined entries to delete.");
  180. const userChoice = prompt("Select entries to delete (comma-separated numbers):\n" +
  181. additionalSites.map((item, index) => `${index + 1}. ${item.pattern}`).join("\n"));
  182. if (!userChoice) return;
  183. const indicesToRemove = userChoice.split(',').map(num => parseInt(num.trim(), 10) - 1);
  184. additionalSites = additionalSites.filter((_, index) => !indicesToRemove.includes(index));
  185. GM_setValue(STORAGE_KEY, additionalSites);
  186. mergedSites = [...new Set([...getDefaultList(), ...additionalSites])].map(item => {
  187. if (typeof item === 'string') {
  188. return { pattern: normalizeUrl(item), preProcessingRequired: false, postProcessingRequired: false };
  189. }
  190. return { ...item, pattern: normalizeUrl(item.pattern) };
  191. });
  192. alert("✅ Selected entries have been deleted.");
  193. }
  194.  
  195. function editEntry() {
  196. if (additionalSites.length === 0) return alert("⚠️ No user-defined entries to edit.");
  197. const userChoice = prompt("Select an entry to edit:\n" +
  198. additionalSites.map((item, index) => {
  199. const status = formatStatus(item.preProcessingRequired, item.postProcessingRequired, item.onDemandFloatingButtonRequired, item.backgroundChangeObserverRequired);
  200. return `${index + 1}. ${item.pattern}${status ? ` (${status})` : ''}`;
  201. }).join("\n"));
  202. if (!userChoice) return;
  203. const selectedIndex = parseInt(userChoice, 10) - 1;
  204. if (selectedIndex < 0 || selectedIndex >= additionalSites.length) return alert("❌ Invalid selection.");
  205. const entry = additionalSites[selectedIndex];
  206. const newPattern = normalizeUrl(prompt("Edit the pattern:", entry.pattern));
  207. if (!newPattern || !newPattern.trim()) return;
  208. const preProcessingRequired = prompt("Is pre-processing required? (y/n)", entry.preProcessingRequired ? "y" : "n").toLowerCase() === 'y';
  209. const postProcessingRequired = prompt("Is post-processing required? (y/n)", entry.postProcessingRequired ? "y" : "n").toLowerCase() === 'y';
  210. const onDemandFloatingButtonRequired = prompt("Is on-demand floating button required? (y/n)", entry.onDemandFloatingButtonRequired ? "y" : "n").toLowerCase() === 'y';
  211. const backgroundChangeObserverRequired = prompt("Is background change observer required? (y/n)", entry.backgroundChangeObserverRequired ? "y" : "n").toLowerCase() === 'y';
  212. entry.pattern = newPattern.trim();
  213. entry.preProcessingRequired = preProcessingRequired;
  214. entry.postProcessingRequired = postProcessingRequired;
  215. entry.onDemandFloatingButtonRequired = onDemandFloatingButtonRequired;
  216. entry.backgroundChangeObserverRequired = backgroundChangeObserverRequired;
  217. GM_setValue(STORAGE_KEY, additionalSites);
  218. mergedSites = [...new Set([...getDefaultList(), ...additionalSites])].map(item => {
  219. if (typeof item === 'string') {
  220. return {
  221. pattern: normalizeUrl(item),
  222. preProcessingRequired: false,
  223. postProcessingRequired: false,
  224. onDemandFloatingButtonRequired: false,
  225. backgroundChangeObserverRequired: false
  226. };
  227. }
  228. return {
  229. ...item,
  230. pattern: normalizeUrl(item.pattern),
  231. onDemandFloatingButtonRequired: item.onDemandFloatingButtonRequired || false,
  232. backgroundChangeObserverRequired: item.backgroundChangeObserverRequired || false
  233. };
  234. });
  235. alert("✅ Entry updated.");
  236. }
  237.  
  238. function clearAllEntries() {
  239. if (additionalSites.length === 0) return alert("⚠️ No user-defined entries to clear.");
  240. if (confirm(`🚨 You have ${additionalSites.length} entries. Clear all?`)) {
  241. additionalSites = [];
  242. GM_setValue(STORAGE_KEY, additionalSites);
  243. mergedSites = [...getDefaultList()].map(item => {
  244. if (typeof item === 'string') {
  245. return { pattern: normalizeUrl(item), preProcessingRequired: false, postProcessingRequired: false };
  246. }
  247. return { ...item, pattern: normalizeUrl(item.pattern) };
  248. });
  249. alert("✅ All user-defined entries cleared.");
  250. }
  251. }
  252.  
  253. // function exportAdditionalSites() {
  254. // GM_download("data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(additionalSites, null, 2)), "additionalSites_backup.json");
  255. // alert("📤 Additional sites exported as JSON.");
  256. // }
  257.  
  258. function exportAdditionalSites() {
  259. const data = JSON.stringify(additionalSites, null, 2);
  260. const blob = new Blob([data], { type: 'application/json' });
  261. const url = URL.createObjectURL(blob);
  262. const a = document.createElement('a');
  263. a.href = url;
  264. a.download = 'additionalSites_backup.json';
  265. document.body.appendChild(a);
  266. a.click();
  267. document.body.removeChild(a);
  268. URL.revokeObjectURL(url);
  269. alert("📤 Additional sites exported as JSON.");
  270. }
  271.  
  272. // function importAdditionalSites() {
  273. // const input = document.createElement("input");
  274. // input.type = "file";
  275. // input.accept = ".json";
  276. // input.onchange = event => {
  277. // const reader = new FileReader();
  278. // reader.onload = e => {
  279. // additionalSites = JSON.parse(e.target.result);
  280. // GM_setValue(STORAGE_KEY, additionalSites);
  281. // mergedSites = [...new Set([...getDefaultList(), ...additionalSites])].map(item => {
  282. // if (typeof item === 'string') {
  283. // return { pattern: normalizeUrl(item), preProcessingRequired: false, postProcessingRequired: false };
  284. // }
  285. // return { ...item, pattern: normalizeUrl(item.pattern) };
  286. // });
  287. // alert("📥 Sites imported successfully.");
  288. // };
  289. // reader.readAsText(event.target.files[0]);
  290. // };
  291. // input.click();
  292. // }
  293.  
  294. function importAdditionalSites() {
  295. const input = document.createElement('input');
  296. input.type = 'file';
  297. input.accept = '.json';
  298. input.style.display = 'none';
  299. input.onchange = event => {
  300. const reader = new FileReader();
  301. reader.onload = e => {
  302. try {
  303. const importedData = JSON.parse(e.target.result);
  304. if (Array.isArray(importedData)) {
  305. additionalSites = importedData.map(item => {
  306. if (typeof item === 'string') {
  307. return normalizeUrl(item);
  308. } else if (typeof item === 'object' && item.pattern) {
  309. return { ...item, pattern: normalizeUrl(item.pattern) };
  310. }
  311. throw new Error('Invalid data format');
  312. });
  313. GM_setValue(STORAGE_KEY, additionalSites);
  314. mergedSites = [...new Set([...getDefaultList(), ...additionalSites])].map(item => {
  315. if (typeof item === 'string') {
  316. return normalizeUrl(item);
  317. }
  318. return { ...item, pattern: normalizeUrl(item.pattern) };
  319. });
  320. alert('📥 Sites imported successfully.');
  321. } else {
  322. throw new Error('Invalid data format');
  323. }
  324. } catch (error) {
  325. alert('❌ Failed to import sites: ' + error.message);
  326. }
  327. };
  328. reader.readAsText(event.target.files[0]);
  329. };
  330. document.body.appendChild(input);
  331. input.click();
  332. document.body.removeChild(input);
  333. }
  334.  
  335. function formatStatus(preProcessingRequired, postProcessingRequired, onDemandFloatingButtonRequired, backgroundChangeObserverRequired) {
  336. if (SHOW_STATUS_ONLY_IF_TRUE && !preProcessingRequired && !postProcessingRequired && !onDemandFloatingButtonRequired && !backgroundChangeObserverRequired) {
  337. return '';
  338. }
  339. const preStatus = USE_EMOJI_FOR_STATUS ? (preProcessingRequired ? '✅' : '✖️') : (preProcessingRequired ? 'true' : 'false');
  340. const postStatus = USE_EMOJI_FOR_STATUS ? (postProcessingRequired ? '✅' : '✖️') : (postProcessingRequired ? 'true' : 'false');
  341. const floatingButtonStatus = USE_EMOJI_FOR_STATUS ? (onDemandFloatingButtonRequired ? '✅' : '✖️') : (onDemandFloatingButtonRequired ? 'true' : 'false');
  342. const backgroundObserverStatus = USE_EMOJI_FOR_STATUS ? (backgroundChangeObserverRequired ? '✅' : '✖️') : (backgroundChangeObserverRequired ? 'true' : 'false');
  343. return `Pre: ${preStatus}, Post: ${postStatus}, Floating Button: ${floatingButtonStatus}, Background Observer: ${backgroundObserverStatus}`;
  344. }
  345.  
  346. window.shouldRunOnThisSite = shouldRunOnThisSite;
  347. window.isPreProcessingRequired = function() {
  348. const currentFullPath = normalizeUrl(`${window.top.location.href}`);
  349. const entry = mergedSites.find(item => wildcardToRegex(normalizeUrl(item.pattern)).test(currentFullPath));
  350. return entry ? entry.preProcessingRequired : false;
  351. };
  352. window.isPostProcessingRequired = function() {
  353. const currentFullPath = normalizeUrl(`${window.top.location.href}`);
  354. const entry = mergedSites.find(item => wildcardToRegex(normalizeUrl(item.pattern)).test(currentFullPath));
  355. return entry ? entry.postProcessingRequired : false;
  356. };
  357. // Expose isOnDemandFloatingButtonRequired
  358. window.isOnDemandFloatingButtonRequired = function() {
  359. const currentFullPath = normalizeUrl(`${window.top.location.href}`);
  360. const entry = mergedSites.find(item => wildcardToRegex(normalizeUrl(item.pattern)).test(currentFullPath));
  361. return entry ? entry.onDemandFloatingButtonRequired : false;
  362. };
  363.  
  364. // Expose isBackgroundChangeObserverRequired
  365. window.isBackgroundChangeObserverRequired = function() {
  366. const currentFullPath = normalizeUrl(`${window.top.location.href}`);
  367. const entry = mergedSites.find(item => wildcardToRegex(normalizeUrl(item.pattern)).test(currentFullPath));
  368. return entry ? entry.backgroundChangeObserverRequired : false;
  369. };
  370. })();
  371. })();
  372.  
  373. //To use this in another script use @require
  374.  
  375. // // @run-at document-end
  376. // // ==/UserScript==
  377.  
  378. // window.SCRIPT_STORAGE_KEY = "magnetLinkHashChecker"; // UNIQUE STORAGE KEY
  379.  
  380.  
  381. // window.GET_DEFAULT_LIST = function() {
  382. // return [
  383. // { pattern: "*1337x.*", preProcessingRequired: false, postProcessingRequired: false, onDemandFloatingButtonRequired: false, backgroundChangeObserverRequired: false },
  384. // { pattern: "*yts.*", preProcessingRequired: true, postProcessingRequired: true, onDemandFloatingButtonRequired: false, backgroundChangeObserverRequired: false },
  385. // { pattern: "*torrentgalaxy.*", preProcessingRequired: false, postProcessingRequired: true, onDemandFloatingButtonRequired: false, backgroundChangeObserverRequired: false },
  386. // { pattern: "*bitsearch.*", preProcessingRequired: false, postProcessingRequired: false, onDemandFloatingButtonRequired: false, backgroundChangeObserverRequired: false },
  387. // { pattern: "*thepiratebay.*", preProcessingRequired: false, postProcessingRequired: false, onDemandFloatingButtonRequired: false, backgroundChangeObserverRequired: false },
  388. // { pattern: "*ext.*", preProcessingRequired: false, postProcessingRequired: false, onDemandFloatingButtonRequired: false, backgroundChangeObserverRequired: false }
  389. // ];
  390. // };
  391.  
  392.  
  393. // (async function () {
  394. // 'use strict';
  395.  
  396. // // ✅ Wait until `shouldRunOnThisSite` is available
  397. // while (typeof shouldRunOnThisSite === 'undefined') {
  398. // await new Promise(resolve => setTimeout(resolve, 50));
  399. // }
  400.  
  401. // if (!(await shouldRunOnThisSite())) return;
  402. // //alert("running");
  403.  
  404. // console.log("Pre-Customization enabled for this site: " + isPreProcessingRequired() );
  405. // console.log("Post-Customization enabled for this site: " + isPostProcessingRequired() );
  406.  
  407. // const OFFCLOUD_CACHE_API_URL = 'https://offcloud.com/api/cache';