// ==UserScript==
// @name Redacted YouTube Searcher
// @license MIT
// @namespace https://redacted.sh/
// @version 1.4.2
// @description Add YouTube search links that play in an embedded player or 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;
const CONFIG = {
STORAGE_KEY: "redacted-youtube-player-visibility",
YOUTUBE_SEARCH_URL: "https://www.youtube.com/results?search_query=",
YOUTUBE_EMBED_URL: "https://www.youtube-nocookie.com/embed/",
VIDEO_ID_REGEX: /"videoId"\s*:\s*"([^"]+)"/,
};
let activeTrack = null;
const utils = {
slugify(string) {
return string
.toLowerCase()
.trim()
.replace(/[^\w\s-]/g, "")
.replace(/[\s_-]+/g, "-")
.replace(/^-+|-+$/g, "");
},
};
const UI = {
createLoadingSpinner() {
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);
return svg;
},
createYouTubeIcon() {
return `<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>`;
},
createExternalLinkIcon() {
return `<svg width="12" height="12" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor"><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>`;
},
};
const YouTubeAPI = {
async searchVideo(query) {
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: "GET",
url: `${CONFIG.YOUTUBE_SEARCH_URL}${encodeURIComponent(query)}`,
onload: function (response) {
if (response.readyState === 4 && response.status === 200) {
const videoIds = response.responseText.match(
CONFIG.VIDEO_ID_REGEX,
);
if (videoIds && videoIds.length > 0) {
resolve(videoIds[1]);
} else {
reject(new Error("No video ID found"));
}
} else {
reject(new Error(`HTTP ${response.status}`));
}
},
onerror: () => reject(new Error("Network error")),
});
});
},
createEmbedPlayer(videoId) {
const player = document.createElement("iframe");
player.id = "redacted-youtube-player";
player.src = `${CONFIG.YOUTUBE_EMBED_URL}${videoId}?autoplay=1`;
return player;
},
};
const PlayerManager = {
setVisibility(state) {
localStorage.setItem(CONFIG.STORAGE_KEY, state);
const trackList = document.getElementById("redacted-youtube-track-list");
if (trackList) {
trackList.style.display = state === "hidden" ? "none" : "";
}
},
toggleVisibility(event) {
event.preventDefault();
const currentState = localStorage.getItem(CONFIG.STORAGE_KEY);
const newState = currentState === "hidden" ? "visible" : "hidden";
this.setVisibility(newState);
const showLink = event.target;
showLink.textContent = newState === "hidden" ? "(Show)" : "(Hide)";
},
async playFirstResult(event) {
event.preventDefault();
const parent = event.target.closest("[data-query]");
if (!parent) return;
const query = parent.getAttribute("data-query");
const existingPlayer = document.getElementById("redacted-youtube-player");
if (existingPlayer && activeTrack === parent.id) {
activeTrack = null;
existingPlayer.remove();
return;
}
const spinner = UI.createLoadingSpinner();
parent.appendChild(spinner);
try {
if (existingPlayer) existingPlayer.remove();
const videoId = await YouTubeAPI.searchVideo(query);
const player = YouTubeAPI.createEmbedPlayer(videoId);
parent.appendChild(player);
spinner.remove();
activeTrack = parent.id;
} catch (error) {
console.error("YouTube search failed:", error);
spinner.remove();
const errorMsg = document.createElement("span");
errorMsg.textContent = "Search failed";
errorMsg.style = "color: #ed5651; font-size: 12px; margin-left: 5px;";
parent.appendChild(errorMsg);
setTimeout(() => errorMsg.remove(), 3000);
}
},
};
const TrackProcessor = {
AUDIO_EXTENSIONS: [".mp3", ".flac", ".wav", ".aac", ".opus"],
TRACK_NUMBER_PATTERNS: [
/^(\d{1,2})\.(\d{1,2})[.\s-]+/,
/^(\d{1,2})-(\d{1,2})[.\s-]+/,
/^(\d{1,2})[.\s-]+/,
],
UNWANTED_PATTERNS: [
/\([^)]*\)/g,
/\[[^\]]*\]/g,
/\{[^}]*\}/g,
/\bfeat(?:\.|uring)?\b/gi,
/\bft\.?\b/gi,
/extended/gi,
/radio edit/gi,
/clean version/gi,
/explicit/gi,
/instrumental/gi,
/demo/gi,
/live/gi,
/acoustic/gi,
/original mix/gi,
/club mix/gi,
/dub mix/gi,
],
isAudioFile(filename) {
const extension = filename.slice(filename.lastIndexOf("."));
return this.AUDIO_EXTENSIONS.includes(extension.toLowerCase());
},
cleanText(text) {
let cleaned = text;
this.UNWANTED_PATTERNS.forEach(
(pattern) => (cleaned = cleaned.replace(pattern, "")),
);
return cleaned
.replace(/^\s+|\s+$/g, "")
.replace(/[-\s.]+$/g, "")
.replace(/^[-\s.]+/g, "")
.replace(/\s+/g, " ")
.trim();
},
extractTrackNumber(filename) {
for (const pattern of this.TRACK_NUMBER_PATTERNS) {
const match = filename.match(pattern);
if (match) {
const trackNumber = match[1];
const discNumber = match[2] || null;
const remaining = filename.replace(match[0], "");
return {
trackNumber: parseInt(trackNumber, 10),
discNumber: discNumber ? parseInt(discNumber, 10) : null,
remaining: remaining.trim(),
};
}
}
return { trackNumber: null, discNumber: null, remaining: filename };
},
removeArtistName(trackTitle, artist) {
if (!artist) return trackTitle;
const artistNames = artist
.split(/[&and]/i)
.map((name) => name.trim())
.filter(Boolean);
for (const artistName of artistNames) {
const escapedArtist = artistName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const patterns = [
new RegExp(`^\\s*${escapedArtist}\\s*[-–—]\\s*`, "i"),
new RegExp(`^\\s*${escapedArtist}\\s*:\\s*`, "i"),
new RegExp(`^\\s*${escapedArtist}\\s+`, "i"),
];
for (const pattern of patterns) {
if (pattern.test(trackTitle)) {
return trackTitle.replace(pattern, "").trim();
}
}
}
return trackTitle;
},
extractTrackInfo(fileText, artist) {
if (!this.isAudioFile(fileText)) return null;
try {
const lastDotIndex = fileText.lastIndexOf(".");
let trackTitle =
lastDotIndex > 0 ? fileText.slice(0, lastDotIndex) : fileText;
const numberInfo = this.extractTrackNumber(trackTitle);
trackTitle = numberInfo.remaining;
if (artist) trackTitle = this.removeArtistName(trackTitle, artist);
trackTitle = this.cleanText(trackTitle);
if (!trackTitle)
trackTitle =
lastDotIndex > 0 ? fileText.slice(0, lastDotIndex) : fileText;
const artistInTrack =
artist && trackTitle.toLowerCase().includes(artist.toLowerCase());
const artistAndTrack = artistInTrack
? trackTitle
: `${artist} - ${trackTitle}`;
return {
trackName: trackTitle,
artistAndTrack,
trackId: utils.slugify(trackTitle),
trackNumber: numberInfo.trackNumber,
discNumber: numberInfo.discNumber,
};
} catch (error) {
console.error("Error processing track:", fileText, error);
return null;
}
},
createTrackElement(trackInfo) {
const trackLinkElement = document.createElement("tr");
const trackLinkTableData = document.createElement("td");
trackLinkTableData.id = trackInfo.trackId;
trackLinkTableData.setAttribute("data-query", trackInfo.artistAndTrack);
const trackLinkAnchor = document.createElement("a");
trackLinkAnchor.href = "#";
trackLinkAnchor.innerHTML = trackInfo.trackName;
trackLinkAnchor.addEventListener("click", (e) =>
PlayerManager.playFirstResult(e),
);
const trackSearchAnchor = document.createElement("a");
trackSearchAnchor.href = `${CONFIG.YOUTUBE_SEARCH_URL}${encodeURIComponent(trackInfo.artistAndTrack)}`;
trackSearchAnchor.title = "Open YouTube";
trackSearchAnchor.rel = "noopener";
trackSearchAnchor.target = "_blank";
trackSearchAnchor.innerHTML = UI.createExternalLinkIcon();
trackSearchAnchor.style = "margin-left: 4px";
trackLinkTableData.appendChild(trackLinkAnchor);
trackLinkTableData.appendChild(trackSearchAnchor);
trackLinkElement.appendChild(trackLinkTableData);
return trackLinkElement;
},
};
const App = {
injectStyles() {
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);
},
addTorrentLinks() {
const urlParams = new URLSearchParams(window.location.search);
document
.querySelectorAll("table.torrent_table > tbody > tr")
.forEach((torrent) => {
if (torrent.querySelector(".redacted-youtube-link")) return;
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 || artist;
}
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");
const youtubeLink = `| <a href="${CONFIG.YOUTUBE_SEARCH_URL}${query}" class="tooltip redacted-youtube-link" rel="noopener" target="_blank" title="Search YouTube">${UI.createYouTubeIcon()}</a>`;
if (actionButtons) {
actionButtons.insertAdjacentHTML("beforeend", youtubeLink);
} else if (addBookmarkButton) {
addBookmarkButton.insertAdjacentHTML(
"beforebegin",
`<span title="Search YouTube" class="tooltip" style="margin-left: 4px"><a href="${CONFIG.YOUTUBE_SEARCH_URL}${query}" class="redacted-youtube-link" rel="noopener" target="_blank">${UI.createYouTubeIcon()}</a></span>`,
);
}
});
},
observeDynamicContent() {
const observer = new MutationObserver((mutations) => {
let shouldUpdate = false;
mutations.forEach((mutation) => {
if (mutation.type === "childList") {
mutation.addedNodes.forEach((node) => {
if (node.nodeType === Node.ELEMENT_NODE) {
if (
node.matches &&
(node.matches("table.torrent_table") ||
node.matches("table.torrent_table *"))
) {
shouldUpdate = true;
}
if (
node.querySelector &&
node.querySelector("table.torrent_table")
) {
shouldUpdate = true;
}
}
});
}
});
if (shouldUpdate) {
setTimeout(() => this.addTorrentLinks(), 100);
}
});
observer.observe(document.body, {
childList: true,
subtree: true,
});
},
createTrackList() {
const urlParams = new URLSearchParams(window.location.search);
if (
!/\/torrents.php/.test(window.location.pathname) ||
!urlParams.has("id")
)
return;
const artist = Array.from(
document.querySelectorAll('h2 a[href*="artist.php"]'),
)
.map((link) => link.textContent)
.join(" & ");
const fileTables = Array.from(
document.querySelectorAll("table.filelist_table"),
).filter((table) => {
const releaseRow =
table.closest("tr.releases_1")?.previousElementSibling;
const firstTdText = releaseRow?.querySelector("td")?.textContent || "";
return !/\/\s*Scene/i.test(firstTdText);
});
const fileRows = fileTables.flatMap((table) =>
Array.from(
table.querySelectorAll(
"tbody > tr:not(.colhead_dark) > td:not(.number_column)",
),
),
);
// const fileRows = document.querySelectorAll(
// "table.filelist_table > tbody > tr:not(.colhead_dark) > td:not(.number_column)",
// );
const trackLinks = [];
fileRows.forEach((item) => {
const file = item.textContent;
const trackInfo = TrackProcessor.extractTrackInfo(file, artist);
if (!trackInfo) return;
const trackElement = TrackProcessor.createTrackElement(trackInfo);
const normalizedTrackName = trackInfo.trackName.toLowerCase().trim();
const isDuplicate = trackLinks.some(
(trackLink) =>
trackLink.id === trackInfo.trackId ||
trackLink.artistAndTrack === trackInfo.artistAndTrack ||
trackLink.normalizedName === normalizedTrackName,
);
if (!isDuplicate) {
trackLinks.push({
id: trackInfo.trackId,
element: trackElement,
artistAndTrack: trackInfo.artistAndTrack,
normalizedName: normalizedTrackName,
trackNumber: trackInfo.trackNumber,
discNumber: trackInfo.discNumber,
});
}
});
if (trackLinks.length === 0) return;
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 =
localStorage.getItem(CONFIG.STORAGE_KEY) === "hidden"
? "(Show)"
: "(Hide)";
showLink.onclick = (e) => PlayerManager.toggleVisibility(e);
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(CONFIG.STORAGE_KEY) === "hidden"
? "display: none"
: "";
table.appendChild(thead);
table.appendChild(tbody);
trackLinks.sort((a, b) => {
if (a.discNumber !== b.discNumber)
return (a.discNumber || 0) - (b.discNumber || 0);
return (a.trackNumber || 0) - (b.trackNumber || 0);
});
const descriptionBox = document.querySelector(
"div.box.torrent_description",
);
if (descriptionBox) {
descriptionBox.insertAdjacentElement("beforebegin", table);
trackLinks.forEach((track) => tbody.appendChild(track.element));
}
},
init() {
this.injectStyles();
this.addTorrentLinks();
this.createTrackList();
this.observeDynamicContent();
},
};
App.init();
})();