// ==UserScript== // @name Search Hobune for unavailable video IDs // @namespace hobune.stream // @match https://www.youtube.com/* // @grant GM_xmlhttpRequest // @connect hobune.stream // @version 2.1 // @author Somepony // @description 2021-08-28T03:33:00Z // ==/UserScript== const matchURLs = ["https://www.youtube.com/watch", "https://www.youtube.com/shorts/"]; const hobuneEndpoints = ["https://hobune.stream/tpa-h/videos/","https://hobune.stream/videos/"]; // End all other async instances when one returns value in earlyArr // Instance returns undefined when ended early function endEarly(generator,earlyArr) { let isDone = false; return async function(...args) { const iter = generator(...args); let resumeValue; for (;;) { const n = iter.next(resumeValue); if (n.done) { // check if process is done if (earlyArr.indexOf(n.value) >= 0) { isDone = true; // value in earlyArr returned; end other processes } return n.value; // final return value of passed generator } else if (isDone) { return; // one process already returned value in earlyArr } // whatever the generator yielded, _now_ run await on it resumeValue = await n.value; // next loop, we give resumeValue back to the generator } }; } // Check Hobune and returns on 200 or 404 (or if ended early by endEarly()) function* checkHobune(videoId,hobuneEndpoint) { while (true) { let status = yield new Promise((resolve, reject) => { GM_xmlhttpRequest ({ method: 'GET', url: hobuneEndpoint + videoId, onload: function (response) { resolve(response.status); } }); }); if ([200,404].indexOf(status) >= 0) { return status; } yield new Promise(r => setTimeout(r, 1000)); } } // Get videoId function getVideoId() { let url = location.href; if (url.startsWith("https://www.youtube.com/shorts/")) { url = url.replace("https://www.youtube.com/shorts/", "https://www.youtube.com/watch?v="); } url = new URL(url) urlParams = new URLSearchParams(url.search); videoId = urlParams.get('v').replace(/[^a-zA-Z0-9_-]/g, ""); return videoId; } async function main() { // Get videoId let videoId = getVideoId(); // Recursively tries to find element // Returns undefined if element still cannot be found after load (i.e. not a player error) let element = (async function() { while (document.readyState != 'complete') { let x = await document.getElementsByClassName("yt-player-error-message-renderer").info; if (x != null) { return x; } await new Promise(r => setTimeout(r, 500)); } let x = await document.getElementsByClassName("yt-player-error-message-renderer").info; // check one more time on load return x; }()); // End early if not player error element = await element; if (element == null) { return; } // Recursively tries to check Hobune for video let foo = endEarly(checkHobune,[200]); let archivedURL = null; let p = Promise.all(hobuneEndpoints.map(endpoint => { return foo(videoId,endpoint).then( function(status) { if (status == 200) { archivedURL = endpoint + videoId; } } ); })); // Append new tag let tag = document.createElement("yt-formatted-string"); tag.classList.add("style-scope","yt-player-error-message-renderer","hobunecheck"); tag.id = "subreason"; element.appendChild(tag); tag.innerText = "Checking Hobune for this video..."; // Modify tag await p; // wait for archivedURL if (archivedURL != null) { tag.innerHTML = `Watch on Hobune`; } else { tag.innerText = "Couldn't find this video on Hobune."; } } if (window.top == window.self) { // Recursively check for href change and match URL var lastHref = null; (function pageURLCheckTimer() { if (lastHref != location.href && matchURLs.some(x => location.href.startsWith(x))) { lastHref = location.href; try { main(); } catch(e) { console.log(e); } } setTimeout(pageURLCheckTimer, 100); }()); }