// ==UserScript== // @name YouTube → FreeTube Link Rewriter // @name:en YouTube → FreeTube Link Rewriter // @name:nl YouTube → FreeTube Link Herschrijver // @name:es Reescritor de Enlaces de YouTube a FreeTube // @name:fr Réécriture des Liens YouTube vers FreeTube // @name:de YouTube → FreeTube Link-Umschreiber // @name:zh-CN YouTube → FreeTube 链接重写器 // @name:ja YouTube → FreeTube リンク書き換え // @name:ru Переписчик ссылок YouTube на FreeTube // @name:pt Reescritor de Links do YouTube para FreeTube // @name:it Riscrittore di Link da YouTube a FreeTube // @name:ko YouTube → FreeTube 링크 재작성기 // @namespace https://greasyfork.org/users/1197317-opus-x // @version 1.00 // @description Rewrite YouTube links to use the FreeTube protocol handler for opening in the local FreeTube application. Ensures the current page remains unchanged upon clicking. // @description:en Rewrite YouTube links to use the FreeTube protocol handler for opening in the local FreeTube application. Ensures the current page remains unchanged upon clicking. // @description:nl Herschrijf YouTube-links om de FreeTube-protocolhandler te gebruiken voor het openen in de lokale FreeTube-applicatie. Zorgt ervoor dat de huidige pagina ongewijzigd blijft bij klikken. // @description:es Reescribe los enlaces de YouTube para usar el manejador de protocolo FreeTube y abrir en la aplicación local FreeTube. Asegura que la página actual permanezca sin cambios al hacer clic. // @description:fr Réécrit les liens YouTube pour utiliser le gestionnaire de protocole FreeTube afin d'ouvrir dans l'application locale FreeTube. Assure que la page actuelle reste inchangée lors du clic. // @description:de Schreibe YouTube-Links um, um den FreeTube-Protokollhandler zu verwenden, um in der lokalen FreeTube-Anwendung zu öffnen. Stellt sicher, dass die aktuelle Seite beim Klicken unverändert bleibt. // @description:zh-CN 将 YouTube 链接重写为使用 FreeTube 协议处理程序,以便在本地 FreeTube 应用程序中打开。确保点击时当前页面保持不变。 // @description:ja YouTubeリンクをFreeTubeプロトコルハンドラを使用して書き換え、ローカルFreeTubeアプリケーションで開く。クリック時に現在のページが変更されないようにする。 // @description:ru Переписывает ссылки YouTube для использования обработчика протокола FreeTube для открытия в локальном приложении FreeTube. Обеспечивает, чтобы текущая страница оставалась неизменной при клике. // @description:pt Reescreve links do YouTube para usar o manipulador de protocolo FreeTube para abrir no aplicativo local FreeTube. Garante que a página atual permaneça inalterada ao clicar. // @description:it Riscrive i link di YouTube per utilizzare il gestore di protocollo FreeTube per aprire nell'applicazione locale FreeTube. Assicura che la pagina corrente rimanga invariata al clic. // @description:ko YouTube 링크를 FreeTube 프로토콜 핸들러를 사용하도록 재작성하여 로컬 FreeTube 애플리케이션에서 열기. 클릭 시 현재 페이지가 변경되지 않도록 함. // @author Opus-X // @license MIT // @icon data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABwAAAAcCAMAAABF0y+mAAAAn1BMVEVHcEz/KTr/KTr/KTr/KTr/KTr/KTr/KTr/KTr/KTr/KTr/KTr5KDn7KDnWKDX/KTr////+KTr+IjTxJjf5KDjtpKnrFCn98/T+Gi/pJjX+9/fbIjHiQ0/iIDDCEiLPHi22CBzFSlP8UV3z29y4Mjz9O0r0Fiv8p639i5L5ZW/85efswMPJGCjfn6P+z9L+a3XHY2nNgofWanHTKjevBBkSa/ywAAAAD3RSTlMA1eJM8G8ZywLEjK/Ky+Vq/rygAAABGklEQVQokcWSV3PDMAiAlcRJ7YwzGq1XvGPH8cho+/9/W0Ee59zlpQ+98iA4PoEQwNi/iLna7qy3J7F225VJbLOwX8pig3FLbYrBJcRElyZb99aN994scye6ZhYpt4JrgV5eOFBN1GIGqfIMUF9cW2Q+wHlMbTCOJy/fAcCvXHF00LgWPeU9/CCIMaWGft0/zJk9g9BoCODcNH2GkPuDcRG/g006pP0MFcF5Qd33ScM8jQjOq/UfSaQjm1MUSE7QGGF9T+LgiG92SRtIpZtgadhBjtc9iV95JHFIcdQ+ajyXX3dKpVSYpknsKT40nkbGZdDidWUrL26DkeHI9LCV9DxdgkI9MBr2sCb7g96Pw14b05ogfblZf7e0P1C8J9ljbAzmAAAAAElFTkSuQmCC // @match *://*/* // @run-at document-start // @downloadURL none // ==/UserScript== (function () { 'use strict'; // Convert a YouTube URL to the corresponding FreeTube protocol URL function convertToFreeTubeProtocol(url) { try { const u = new URL(url); const { hostname, pathname } = u; // Match YouTube domains and short links if (hostname.match(/(^|\.)youtube\.com$/) || hostname.match(/(^|\.)youtube-nocookie\.com$/) || hostname === 'youtu.be') { // Supported paths: watch videos, shorts, live, playlists, channels, etc. if ( pathname === '/watch' || pathname.startsWith('/shorts/') || pathname.startsWith('/live/') || pathname === '/playlist' || pathname.startsWith('/@') || // Channels pathname.startsWith('/channel/') || pathname.startsWith('/c/') || pathname.startsWith('/user/') ) { return 'freetube://' + url; } // For youtu.be short links if (hostname === 'youtu.be' && pathname.length > 1) { return 'freetube://' + url; } } } catch (e) { // Invalid URL } return null; } // Trigger the FreeTube protocol using a hidden iframe function triggerFreeTube(protocolUrl) { const iframe = document.createElement('iframe'); iframe.style.display = 'none'; iframe.src = protocolUrl; document.body.appendChild(iframe); setTimeout(() => { if (iframe.parentNode) iframe.parentNode.removeChild(iframe); }, 1000); } // Handle direct loads and SPA navigations: trigger FreeTube and blank the page function handleNavigation() { const protocolUrl = convertToFreeTubeProtocol(location.href); if (protocolUrl) { window.stop(); // Stop loading resources triggerFreeTube(protocolUrl); location.replace('about:blank'); // Blank the page after triggering return true; } return false; } // Check immediately at document-start for direct loads if (handleNavigation()) { return; // Exit early if handled } // Rewrite existing YouTube links to FreeTube protocol function rewriteLinks() { const links = document.querySelectorAll('a[href^="http://youtube.com"], a[href^="https://youtube.com"], a[href^="http://www.youtube.com"], a[href^="https://www.youtube.com"], a[href^="http://youtu.be"], a[href^="https://youtu.be"], a[href^="http://youtube-nocookie.com"], a[href^="https://youtube-nocookie.com"]'); links.forEach(link => { const newUrl = convertToFreeTubeProtocol(link.href); if (newUrl) { link.href = newUrl; } }); } // Initial rewrite after DOMContentLoaded document.addEventListener('DOMContentLoaded', rewriteLinks); // Observe for dynamically added links const observer = new MutationObserver(rewriteLinks); observer.observe(document.body || document.documentElement, { childList: true, subtree: true }); // Intercept clicks to trigger protocol without navigating document.addEventListener('click', e => { if (e.button !== 0 || e.ctrlKey || e.shiftKey || e.altKey || e.metaKey) return; const a = e.target.closest('a[href]'); if (!a) return; const protocolUrl = convertToFreeTubeProtocol(a.href); if (!protocolUrl) return; e.preventDefault(); e.stopImmediatePropagation(); triggerFreeTube(protocolUrl); }, true); // Capture phase // Patch History API to detect SPA navigations const origPushState = history.pushState; const origReplaceState = history.replaceState; history.pushState = function (...args) { origPushState.apply(this, args); handleNavigation(); }; history.replaceState = function (...args) { origReplaceState.apply(this, args); handleNavigation(); }; window.addEventListener('popstate', handleNavigation); // YouTube-specific SPA hooks for extra reliability window.addEventListener('yt-navigate-start', handleNavigation, true); window.addEventListener('yt-page-data-updated', handleNavigation, true); })();