Add YouTube search links that open in a new tab.
// ==UserScript== // @name Redacted YouTube Searcher // @license MIT // @namespace https://redacted.sh/ // @version 1.3.1 // @description Add YouTube search links that open in a new tab. // @author x__a // @match https://*.redacted.sh/* // @grant GM_xmlhttpRequest // ==/UserScript== (function () { 'use strict'; if (document.getElementById('redacted-youtube')) { return; } main(); let activeTrack = null; function slugify(string) { return string .toLowerCase() .trim() .replace(/[^\w\s-]/g, '') .replace(/[\s_-]+/g, '-') .replace(/^-+|-+$/g, ''); } function onLoading(target) { const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); svg.id = 'redacted-youtube-spinner'; svg.setAttribute('viewBox', '0 0 100 100'); const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle'); circle.id = 'redacted-youtube-spinner-circle'; circle.setAttribute('cx', '50'); circle.setAttribute('cy', '50'); circle.setAttribute('r', '45'); svg.appendChild(circle); target.appendChild(svg); } function setYoutubePlayerVisibility(state) { localStorage.setItem('redacted-youtube-player-visibility', state); const trackList = document.getElementById('redacted-youtube-track-list'); trackList.style.display = state === 'hidden' ? 'none' : ''; } function onToggleTrackListVisibility(event) { event.preventDefault(); const currentState = localStorage.getItem('redacted-youtube-player-visibility'); const newState = currentState === 'hidden' ? 'visible' : 'hidden'; setYoutubePlayerVisibility(newState); } function playFirstYouTubeR###lt(event) { event.preventDefault(); let parent = event.target.parentElement; let query = parent.getAttribute('data-query'); let existingPlayer = document.getElementById('redacted-youtube-player'); if (existingPlayer && activeTrack === parent.id) { activeTrack = null; existingPlayer.remove(); return } onLoading(parent); GM_xmlhttpRequest({ method: 'GET', url: `https://www.youtube.com/r###lts?search_query=${encodeURIComponent(query)}`, onload: function (response) { if (response.readyState === 4 && response.status === 200) { if (existingPlayer) { existingPlayer.remove(); } let videoIds = response.responseText.match('"videoId"\s*:\s*"([^"]+)'); if (videoIds.length > 0) { let player = document.createElement('iframe'); player.id = 'redacted-youtube-player'; player.src = `https://www.youtube-nocookie.com/embed/${videoIds[1]}?autoplay=1` parent.appendChild(player); document.getElementById('redacted-youtube-spinner').remove(); activeTrack = parent.id; } } } }); } function main() { const head = document.head || document.getElementsByTagName('head')[0]; const style = document.createElement('style'); style.id = 'redacted-youtube'; style.innerHTML = ` .redacted-youtube-link { transition: all 0.15s ease !important; line-height: 0 !important; color: #c4302b !important; } .redacted-youtube-link:hover { color: #ed5651 !important; } .redacted-youtube-link>svg { width: 12px !important; height: 12px !important; } .redacted-youtube-svg { width: 12px !important; height: 12px !important; } #redacted-youtube-player { display: block; border: none; border-radius: 0.5rem; margin-top: 0.5rem; aspect-ratio: 16/9; width: 100%; } #redacted-youtube-spinner { animation: 2s linear infinite svg-animation; max-width: 10px; margin-left: 5px; } @keyframes svg-animation { 0% { transform: rotateZ(0deg); } 100% { transform: rotateZ(360deg); } } #redacted-youtube-spinner-circle { animation: 1.4s ease-in-out infinite both circle-animation; display: block; fill: transparent; stroke: #ed5651; stroke-linecap: round; stroke-dasharray: 283; stroke-dashoffset: 280; stroke-width: 10px; transform-origin: 50% 50%; } @keyframes circle-animation { 0%, 25% { stroke-dashoffset: 280; transform: rotate(0); } 50%, 75% { stroke-dashoffset: 75; transform: rotate(45deg); } 100% { stroke-dashoffset: 280; transform: rotate(360deg); } } `; head.appendChild(style); const urlParams = new URLSearchParams(window.location.search); document.querySelectorAll('table.torrent_table > tbody > tr').forEach((torrent) => { const artistLink = torrent.querySelector('a[href*="artist.php?id"]'); const releaseLink = torrent.querySelector('a[href*="torrents.php?id"]'); if (!artistLink && !releaseLink) { return; } let artist = artistLink ? artistLink.textContent : null; if (/\/artist.php/.test(window.location.pathname) && urlParams.has('id')) { artist = document.querySelector('.header > h2').textContent; } const release = releaseLink.textContent; const query = encodeURIComponent(artist ? `${artist} - ${release}` : release); const actionButtons = torrent.querySelector('span.torrent_action_buttons'); const addBookmarkButton = torrent.querySelector('span.add_bookmark'); if (actionButtons) { actionButtons.insertAdjacentHTML('beforeend', ` | <a href="https://www.youtube.com/r###lts?search_query=${query}" class="tooltip redacted-youtube-link" rel="noopener" target="_blank" title="Search YouTube"> <svg role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"> <path d="M23.495 6.205a3.007 3.007 0 0 0-2.088-2.088c-1.87-.501-9.396-.501-9.396-.501s-7.507-.01-9.396.501A3.007 3.007 0 0 0 .527 6.205a31.247 31.247 0 0 0-.522 5.805 31.247 31.247 0 0 0 .522 5.783 3.007 3.007 0 0 0 2.088 2.088c1.868.502 9.396.502 9.396.502s7.506 0 9.396-.502a3.007 3.007 0 0 0 2.088-2.088 31.247 31.247 0 0 0 .5-5.783 31.247 31.247 0 0 0-.5-5.805zM9.609 15.601V8.408l6.264 3.602z"/> </svg> </a> `); return; } if (addBookmarkButton) { addBookmarkButton.insertAdjacentHTML('beforebegin', ` <span title="Search YouTube" class="tooltip" style="margin-left: 4px"> <a href="https://www.youtube.com/r###lts?search_query=${query}" class="redacted-youtube-link" rel="noopener" target="_blank"> <svg role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor"> <path d="M23.495 6.205a3.007 3.007 0 0 0-2.088-2.088c-1.87-.501-9.396-.501-9.396-.501s-7.507-.01-9.396.501A3.007 3.007 0 0 0 .527 6.205a31.247 31.247 0 0 0-.522 5.805 31.247 31.247 0 0 0 .522 5.783 3.007 3.007 0 0 0 2.088 2.088c1.868.502 9.396.502 9.396.502s7.506 0 9.396-.502a3.007 3.007 0 0 0 2.088-2.088 31.247 31.247 0 0 0 .5-5.783 31.247 31.247 0 0 0-.5-5.805zM9.609 15.601V8.408l6.264 3.602z"/> </svg> </a> </span> `); } }); if (/\/torrents.php/.test(window.location.pathname) && urlParams.has('id')) { const trackLinks = []; const artist = Array.from(document.querySelectorAll('h2 a[href*="artist.php"')) .map((link) => link.textContent) .join(' & '); let fileRows = document.querySelectorAll('table.filelist_table > tbody > tr:not(.colhead_dark) > td:not(.number_column)'); fileRows.forEach((item, index) => { let file = item.textContent; let regex = /^\d+\W* (.*)\.(flac|mp3)$/g; let matches = [...file.matchAll(regex)]; let track = matches[0] ? matches[0][1] : null; if (!track) { return; } let artistAndTrack = track.includes(artist) ? track : `${artist} - ${track}`; let trackId = slugify(track); let trackLinkElement = document.createElement('tr'); let trackLinkTableData = document.createElement('td'); trackLinkTableData.id = trackId; trackLinkTableData.setAttribute('data-query', artistAndTrack); let trackLinkAnchor = document.createElement('a'); trackLinkAnchor.href = '#' trackLinkAnchor.innerHTML = track; trackLinkAnchor.addEventListener('click', () => playFirstYouTubeR###lt(window.event)); let trackSearchAnchor = document.createElement('a'); trackSearchAnchor.innerHTML = ` <a href="https://www.youtube.com/r###lts?search_query=${encodeURIComponent(artistAndTrack)}" title="Search on YouTube" rel="noopener" target="_blank"> <svg width="12" height="12" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" class="w-5 h-5"> <path fill-rule="evenodd" d="M4.25 5.5a.75.75 0 00-.75.75v8.5c0 .414.336.75.75.75h8.5a.75.75 0 00.75-.75v-4a.75.75 0 011.5 0v4A2.25 2.25 0 0112.75 17h-8.5A2.25 2.25 0 012 14.75v-8.5A2.25 2.25 0 014.25 4h5a.75.75 0 010 1.5h-5z" clip-rule="evenodd" /> <path fill-rule="evenodd" d="M6.194 12.753a.75.75 0 001.06.053L16.5 4.44v2.81a.75.75 0 001.5 0v-4.5a.75.75 0 00-.75-.75h-4.5a.75.75 0 000 1.5h2.553l-9.056 8.194a.75.75 0 00-.053 1.06z" clip-rule="evenodd" /> </svg> </a>`; trackLinkElement.appendChild(trackLinkTableData); trackLinkTableData.appendChild(trackLinkAnchor); trackLinkTableData.appendChild(trackSearchAnchor); if (trackLinks.findIndex(trackLink => trackLink.id === trackId) === -1) { trackLinks.push({ id: trackId, element: trackLinkElement }); } }); if (trackLinks.length > 0) { const table = document.createElement('table'); table.id = 'redacted-youtube-tracks-table'; table.className = 'collage_table'; const thead = document.createElement('thead'); const headerRow = document.createElement('tr'); headerRow.className = 'colhead'; const headerCell = document.createElement('td'); const upLink = document.createElement('a'); upLink.href = '#'; upLink.textContent = '↑'; const trackSearchText = document.createTextNode(' YouTube Track Search '); const showLink = document.createElement('a'); showLink.href = '#'; showLink.textContent = '(Show)'; showLink.onclick = onToggleTrackListVisibility; headerCell.appendChild(upLink); headerCell.appendChild(trackSearchText); headerCell.appendChild(showLink); headerRow.appendChild(headerCell); thead.appendChild(headerRow); const tbody = document.createElement('tbody'); tbody.id = 'redacted-youtube-track-list'; tbody.style = localStorage.getItem('redacted-youtube-player-visibility') === 'hidden' ? 'display: none' : ''; table.appendChild(thead); table.appendChild(tbody); document.querySelector('div.box.torrent_description').insertAdjacentElement('beforebegin', table); trackLinks.forEach(track => { document.querySelector('table#redacted-youtube-tracks-table > tbody').appendChild(track.element); }); } } }; })();