Greasy Fork 支持简体中文。

Redacted YouTube Searcher

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 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', `
                    | <a href="https://www.youtube.com/results?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/results?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', () => playFirstYouTubeResult(window.event));

                let trackSearchAnchor = document.createElement('a');
                trackSearchAnchor.innerHTML = `
                <a href="https://www.youtube.com/results?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);
                });
            }
        }
    };
})();