YOUTUBE_VIDEO_HIDDEN

YouTubeの見たくない動画を非表示

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         YOUTUBE_VIDEO_HIDDEN
// @namespace    http://tampermonkey.net/
// @version      0.4
// @description  YouTubeの見たくない動画を非表示
// @author       Your Name
// @match        *://www.youtube.com/*
// @match        *://www.youtube.com/results?search_query=*
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_deleteValue
// @grant        GM.getValue
// @grant        GM.setValue
// @grant        GM.deleteValue
// @license      MIT License
// ==/UserScript==
(function() {

    'use strict';

    // 検索動画
    const RECOMMEND_VIDEO_SELECTOR = 'ytd-video-renderer[is-search][use-search-ui][use-bigger-thumbs][inline-title-icon]';

    // おすすめ動画
    const SEARCH_VIDEO_SELECTOR = 'ytd-rich-grid-media.style-scope.ytd-rich-item-renderer';

    // チャンネル
    const CHANNEL_SELECTOR = '.ytd-channel-renderer';
    // 関連動画
    const RELATED_VIDEO = 'ytd-compact-video-renderer.style-scope.ytd-item-section-renderer';

    // ショート
    const SHORT_VIDEO_SELECTOR = 'ytd-reel-shelf-renderer';
    // 広告
    const AD_SELECTOR = 'ytd-ad-slot-renderer';
    // ミックスリスト
    const MIXLIST_SELECTOR = 'ytd-radio-renderer';
    // アイコン
    const CHANNEL_ICON_SELECTOR = 'ytd-channel-renderer';

    // スタイルシートを追加してサムネイルを非表示にする
    var style = document.createElement('style');

    // 現在URLによって処理を分岐
    function handlePageSpecificTasks() {
        const currentUrl = window.location.href;
        let nodes;

        if (currentUrl === 'https://www.youtube.com/') {
            // YouTubeのホームページの場合の処理
            nodes = document.querySelectorAll(SEARCH_VIDEO_SELECTOR);
        } else if (currentUrl.startsWith('https://www.youtube.com/results?search_query=')) {
            // YouTubeの検索結果ページの場合の処理
            nodes = document.querySelectorAll(RECOMMEND_VIDEO_SELECTOR);
        } else {
            nodes = null;
        }
        return nodes;
    }

    // 非表示にするチャンネル名の配列
    let hiddenChannels = [];

    // 初期化関数
    async function initialize() {
        hiddenCheckBox();
        addShowPopupButton();
        hiddenChannels = await gmGetValue('hiddenChannels', []);
        observeDOMChanges();
        handleExistingNodes();
    }

    // DOMの変更を監視する
    function observeDOMChanges() {
        const observer = new MutationObserver(mutations => {
            mutations.forEach(mutation => {
                if (mutation.type === 'childList') {
                    mutation.addedNodes.forEach(node => {
                        if (node instanceof HTMLElement){
                            handleNewNode(node);
                        }
                    });
                }
            });
        });
        observer.observe(document.body, { childList: true, subtree: true });
    }

    // ページ読み込み時に既存の要素を処理する
    function handleExistingNodes() {
        handleThumbnails(handlePageSpecificTasks());
    }

    // 新しい要素が追加されたときに処理する
    function handleNewNode(node) {
        if (node.matches(RECOMMEND_VIDEO_SELECTOR+ ', ' +SEARCH_VIDEO_SELECTOR)) {
            handleThumbnails([node]);
        }
    }

    // 指定されたビデオのサムネイルを処理する
    function handleThumbnails(thumbnails) {
        thumbnails.forEach(thumbnail => {
            const channelElement = thumbnail.querySelector('#text a');
            if (channelElement) {
                const channelName = channelElement.textContent.trim();
                const isHidden = hiddenChannels.includes(channelName);
                toggleThumbnail(thumbnail, isHidden);
                if (!isHidden) {
                    addButtonToThumbnail(thumbnail, channelName);
                }
            }
        });
    }

    // サムネイルの表示を切り替える
    function toggleThumbnail(thumbnail, hide) {
        thumbnail.style.display = hide ? 'none' : '';
    }

    // サムネイルに非表示ボタンを追加する
    function addButtonToThumbnail(thumbnail, channelName) {
        if (!thumbnail.querySelector('.custom-button')) {
            const button = document.createElement('button');
            button.textContent = '非表示: ' + channelName;
            button.style.marginTop = '5px';
            button.addEventListener('click', async () => {
                await addChannelToHiddenList(channelName);
                handleThumbnails(handlePageSpecificTasks());
                updatePopupContent();
            });
            button.classList.add('custom-button');
            thumbnail.appendChild(button);
        }
    }

    // 非表示にするチャンネル名をリストに追加する
    async function addChannelToHiddenList(channelName) {
        hiddenChannels.push(channelName);
        await gmSetValue('hiddenChannels', hiddenChannels);
    }

    // 非表示チャンネルリスト表示用のボタンを追加する
    function addShowPopupButton() {
        const button = document.createElement('button');
        button.textContent = '設定';
        button.addEventListener('click', () => {

            const existingPopup = document.querySelector('#ng-channel-popup');
            if (existingPopup) {
                existingPopup.remove();
            }else{
                createPopup();
            }
        });
        const searchForm = document.querySelector('ytd-masthead');
        if (searchForm) {
            const searchButton = searchForm.querySelector('button#search-icon-legacy');
            if (searchButton) {
                searchButton.insertAdjacentElement('afterend', button);
            }
        }
    }

    // 非表示チャンネルリスト用のポップアップを作成する
    function createPopup() {
        const existingPopup = document.querySelector('#ng-channel-popup');
        if (existingPopup) {
            existingPopup.remove();
        }

        const popupContainer = document.createElement('div');
        popupContainer.id = 'ng-channel-popup';
        popupContainer.style.cssText = `
                position: fixed;
                top: 50%;
                left: 50%;
                transform: translate(-50%, -50%);
                background-color: #ffffff;
                padding: 20px;
                border: 1px solid #cccccc;
                border-radius: 10px;
                box-shadow: 0 4px 8px rgba(0,0,0,0.1);
                z-index: 9999;
                font-weight: bold;
                font-size: 24px;
                overflow: auto;
                max-height: 400px;
            `;

        const sectionMenuButton = document.createElement('div');
        sectionMenuButton.style.marginBottom = '20px';

        const sectionHiddenCheckBox = document.createElement('div');
        sectionHiddenCheckBox.style.marginBottom = '20px';

        const sectionHiddenChannelList = document.createElement('div');
        sectionHiddenChannelList.style.marginBottom = '20px';


        const closeButton = document.createElement('button');
        closeButton.textContent = '閉じる';
        closeButton.style.fontWeight = 'bold';
        closeButton.addEventListener('click', () => {
            popupContainer.remove();
        });

        const importButton = document.createElement('button');
        importButton.textContent = 'インポート';
        importButton.style.fontWeight = 'bold';
        importButton.addEventListener('click', function() {
            const fileInput = document.createElement('input');
            fileInput.type = 'file';
            fileInput.accept = '.json'; // JSONファイルのみ受け入れる

            // ファイルが選択されたときの処理
            fileInput.addEventListener('change', function(event) {
                const file = event.target.files[0]; // 選択されたファイルを取得

                if (!file) {
                    console.error('ファイルが選択されていません。');
                    return;
                }

                const reader = new FileReader(); // ファイルを読み込むためのFileReaderオブジェクトを作成

                // ファイル読み込み完了時の処理
                reader.onload = async function(e) {
                    const content = e.target.result; // 読み込んだファイルの内容を取得
                    try {
                        const jsonData = JSON.parse(content); // JSONを解析

                        await gmDeleteValue('hiddenChannels');
                        await gmSetValue('hiddenChannels', jsonData);
                        hiddenChannels = await gmGetValue('hiddenChannels', []);
                        handleThumbnails(handlePageSpecificTasks());
                        updatePopupContent();
                    } catch (error) {
                        console.error('JSONファイルの読み込みに失敗しました:', error);
                    }
                };

                reader.readAsText(file); // ファイルをテキストとして読み込む
            });

            // ファイル選択ダイアログを開く
            fileInput.click();
        });

        const exportButton = document.createElement('button');
        exportButton.textContent = 'エクスポート';
        exportButton.style.fontWeight = 'bold';
        exportButton.addEventListener('click', async () => {

            // 保存されたデータを取得
            const savedData = await gmGetValue('hiddenChannels', []); // 'hiddenChannels' は保存時に使ったキーです

            // JSON文字列に変換
            const jsonDataString = JSON.stringify(savedData);

            // Blobオブジェクトを作成
            const blob = new Blob([jsonDataString], { type: 'application/json' });

            // ダウンロード用リンクを作成
            const url = URL.createObjectURL(blob);
            const a = document.createElement('a');
            a.href = url;
            a.download = 'exported_data.json'; // ダウンロードするファイル名を指定

            // リンクをクリックしてダウンロードをトリガー
            document.body.appendChild(a);
            a.click();

            // リンク要素を削除
            document.body.removeChild(a);

            // URLオブジェクトを解放
            URL.revokeObjectURL(url);

        });

        sectionMenuButton.appendChild(closeButton);
        sectionMenuButton.appendChild(importButton);
        sectionMenuButton.appendChild(exportButton);
        popupContainer.appendChild(sectionMenuButton);

        const hiddenLabel = document.createElement('h2');
        hiddenLabel.textContent = '非表示チェックボックス一覧';
        hiddenLabel.style.fontSize = '14px';
        hiddenLabel.style.verticalAlign = 'bottom';
        popupContainer.appendChild(hiddenLabel);

        const shortCheckBox = document.createElement('input');
        shortCheckBox.type = 'checkbox';
        shortCheckBox.id = 'shortCheckBoxID';
        shortCheckBox.style.fontWeight = 'bold';
        shortCheckBox.style.marginLeft = '4px';
        shortCheckBox.checked = gmGetValue('shortVideoHidden', false);
        shortCheckBox.addEventListener('change',async (event) => {
            if (event.target.checked) {
                await gmSetValue('shortVideoHidden', true);
            }else{
                await gmSetValue('shortVideoHidden', false);
            }
            hiddenCheckBox();
        });

        // チェックボックスのラベルを作成する
        const shortLabel = document.createElement('label');
        shortLabel.textContent = 'ショート';
        shortLabel.style.fontWeight = 'bold';
        shortLabel.style.marginLeft = '4px';
        shortLabel.style.fontSize = '10px';
        shortLabel.htmlFor = 'shortCheckBoxID';

        const mixListCheckBox = document.createElement('input');
        mixListCheckBox.type = 'checkbox';
        mixListCheckBox.id = 'mixListCheckBoxID';
        mixListCheckBox.style.fontWeight = 'bold';
        mixListCheckBox.style.marginLeft = '4px';
        mixListCheckBox.checked = gmGetValue('mixListHidden', false);
        mixListCheckBox.addEventListener('change', async (event) => {
            if (event.target.checked) {
                await gmSetValue('mixListHidden', true );
            }else{
                await gmSetValue('mixListHidden', false);
            }
            hiddenCheckBox();
        });

        // チェックボックスのラベルを作成する
        const mixListLabel = document.createElement('label');
        mixListLabel.textContent = 'ミックスリスト';
        mixListLabel.style.fontWeight = 'bold';
        mixListLabel.style.marginLeft = '4px';
        mixListLabel.style.fontSize = '10px';
        mixListLabel.htmlFor = 'mixListCheckBoxID';

        const channelIconCheckBox = document.createElement('input');
        channelIconCheckBox.type = 'checkbox';
        channelIconCheckBox.id = 'channelIconCheckBoxID';
        channelIconCheckBox.style.fontWeight = 'bold';
        channelIconCheckBox.style.marginLeft = '4px';
        channelIconCheckBox.checked = gmGetValue('channelIconHidden', false);
        channelIconCheckBox.addEventListener('change', async (event) => {
            if (event.target.checked) {
                await gmSetValue('channelIconHidden', true );
            }else{
                await gmSetValue('channelIconHidden', false);
            }
            hiddenCheckBox();
        });

        // チェックボックスのラベルを作成する
        const channelIconLabel = document.createElement('label');
        channelIconLabel.textContent = 'チャンネルアイコン';
        channelIconLabel.style.fontWeight = 'bold';
        channelIconLabel.style.marginLeft = '4px';
        channelIconLabel.style.fontSize = '10px';
        channelIconLabel.htmlFor = 'channelIconCheckBoxID';

        const adCheckBox = document.createElement('input');
        adCheckBox.type = 'checkbox';
        adCheckBox.id = 'adCheckBoxID';
        adCheckBox.style.fontWeight = 'bold';
        adCheckBox.style.marginLeft = '4px';
        adCheckBox.checked = gmGetValue('adHidden', false);
        adCheckBox.addEventListener('change', async (event) => {
            if (event.target.checked) {
                await gmSetValue('adHidden', true);
            }else{
                await gmSetValue('adHidden', false);
            }
            hiddenCheckBox();
        });

        // チェックボックスのラベルを作成する
        const adLabel = document.createElement('label');
        adLabel.textContent = '広告';
        adLabel.style.fontWeight = 'bold';
        adLabel.style.marginLeft = '4px';
        adLabel.style.fontSize = '10px';
        adLabel.htmlFor = 'adCheckBoxID';

        sectionHiddenCheckBox.appendChild(shortCheckBox);
        sectionHiddenCheckBox.appendChild(shortLabel);
        sectionHiddenCheckBox.appendChild(mixListCheckBox);
        sectionHiddenCheckBox.appendChild(mixListLabel);
        sectionHiddenCheckBox.appendChild(channelIconCheckBox);
        sectionHiddenCheckBox.appendChild(channelIconLabel);
        sectionHiddenCheckBox.appendChild(adCheckBox);
        sectionHiddenCheckBox.appendChild(adLabel);
        popupContainer.appendChild(sectionHiddenCheckBox);

        const heading = document.createElement('h2');
        heading.textContent = '非表示チャンネル一覧';
        heading.style.fontSize = '14px';
        heading.style.verticalAlign = 'bottom';
        popupContainer.appendChild(heading);

        const ngChannelList = document.createElement('ul');
        ngChannelList.style.listStyleType = 'none';
        ngChannelList.style.padding = '0';
        //ngChannelList.innerHTML = '';
        ngChannelList.textContent = '';

        hiddenChannels.forEach(channelName => {
            const listItem = createNgChannelList(channelName);
            ngChannelList.appendChild(listItem);
        });

        sectionHiddenChannelList.appendChild(ngChannelList);
        popupContainer.appendChild(sectionHiddenChannelList);

        document.body.appendChild(popupContainer);
    }



    // 非表示チャンネルリストのアイテムを作成する
    function hiddenCheckBox() {

        var shortVideoDisplay = gmGetValue('shortVideoHidden', false) ? 'none': '';
        var mixListDisplay = gmGetValue('mixListHidden', false) ? 'none': '';
        var channelIconDisplay = gmGetValue('channelIconHidden', false) ? 'none': '';
        var adDisplay = gmGetValue('adHidden', false) ? 'none': '';

        style.textContent = `
    ${SHORT_VIDEO_SELECTOR} { display: ${shortVideoDisplay} ; }
    ${MIXLIST_SELECTOR} { display: ${mixListDisplay} ; }
    ${CHANNEL_ICON_SELECTOR} { display: ${channelIconDisplay} ; }
    ${AD_SELECTOR} { display: ${adDisplay} ; }
    `;
        // ヘッド要素にスタイル要素を追加
        document.head.appendChild(style);

    }


    // 非表示チャンネルリストのアイテムを作成する
    function createNgChannelList(channelName) {
        const listItem = document.createElement('li');
        listItem.style.display = 'flex';

        const unhideButton = createUnhideButton(channelName);
        listItem.appendChild(unhideButton);

        // const textElement = document.createElement('span');
        const textElement = document.createElement('span');
        textElement.textContent = channelName;
        textElement.style.display = 'flex';
        textElement.style.marginLeft = '4px';
        textElement.style.fontWeight = 'bold';
        textElement.style.fontSize = '16px';
        textElement.style.alignItems = 'center';
        listItem.appendChild(textElement);

        return listItem;
    }

    // 非表示解除ボタンを作成する
    function createUnhideButton(channelName) {
        const button = document.createElement('button');
        button.textContent = '解除 ';
        button.style.marginRight = '10px';
        button.style.alignItems = 'center';
        button.addEventListener('click', async () => {
            hiddenChannels = hiddenChannels.filter(name => name !== channelName);
            await gmSetValue('hiddenChannels', hiddenChannels);
            handleThumbnails(handlePageSpecificTasks());
            updatePopupContent();
        });
        return button;
    }

    // 非表示チャンネルリストの内容を更新する
    function updatePopupContent() {
        const popupContainer = document.querySelector('#ng-channel-popup');
        if (popupContainer) {
            const ngChannelList = popupContainer.querySelector('ul');
            if (ngChannelList) {
                //ngChannelList.innerHTML = '';
                ngChannelList.textContent = '';
                hiddenChannels.forEach(channelName => {
                    const listItem = createNgChannelList(channelName);
                    ngChannelList.appendChild(listItem);
                });
            }
        }
    }

    // GM_getValueのラッパー関数(環境に応じて処理を切り替える)
    function gmGetValue(key, value) {
        if (typeof GM_getValue !== 'undefined') {
            return GM_getValue(key, value);
        } else {
            return GM.getValue(key, value);
        }
    }

    // GM_setValueのラッパー関数(環境に応じて処理を切り替える)
    function gmSetValue(key, value) {
        if (typeof GM_setValue !== 'undefined') {
            return GM_setValue(key, value);
        } else {
            return GM.setValue(key, value);
        }
    }

    // GM_setValueのラッパー関数(環境に応じて処理を切り替える)
    function gmDeleteValue(key) {
        if (typeof GM_deleteValue !== 'undefined') {
            return GM_deleteValue(key);
        } else {
            return GM.deleteValue(key);
        }
    }

    // 初期化関数を呼び出す
    initialize();

})();