YOUTUBE_VIDEO_HIDDEN

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

// ==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();

})();