// ==UserScript==
// @name Redacted YouTube Searcher
// @license MIT
// @namespace https://redacted.ch/
// @version 1.3
// @description Add YouTube search links that open in a new tab.
// @author x__a
// @match https://*.redacted.ch/*
// @grant GM_xmlhttpRequest
// @downloadURL none
// ==/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 playFirstYouTubeResult(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/results?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', `
|
`);
return;
}
if (addBookmarkButton) {
addBookmarkButton.insertAdjacentHTML('beforebegin', `
`);
}
});
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', () => playFirstYouTubeResult(window.event));
let trackSearchAnchor = document.createElement('a');
trackSearchAnchor.innerHTML = `
`;
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);
});
}
}
};
})();