勉強しようと YouTube を見ると面白い動画を再生してしまう件を解決する

# はじめに
　私は、よく勉強しようと YouTube を見ても、おすすめに流れてくる誘惑をクリックしてしまいます。他のウェブサイトは Norton Family さん（カテゴリーによるウェブサイトの閲覧制限）と Microsoft Family Safety さん（パソコン全体の起動動時間制限）のお力を借りて見れなくしているのですが、YouTube はチャンネルごとに設定することができませんでした...。
　もちろん、自制心をもてという話ですが、少しでも「あ、みたらだめだ💦」と気づけるように警告を表示する Chrome 拡張機能を作りました。学生の皆様は、ぜひご利用ください！
💡 Chrome で勉強用のユーザーを作成してもいいですが、Windows のユーザーアカウントごと分けておくことをおすすめします。

# 作品：Chrome 拡張機能「YouTube Study Focus」
作る過程をご紹介する前に、まずは出来上がったものを紹介します！
![YouTube-Study-Focus_qiita_screenshot.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2669617/339e8c42-3d46-78b7-73ea-b2663fc045ec.jpeg)
こちらからお試しください↓↓

https://chrome.google.com/webstore/detail/bkkemkefdbnjpagjpoimamhlcckbncjm

# 作る過程
2年ほど前に作ろうと思って失敗しただけで、はじめての拡張機能開発だったので、「Chrome 拡張機能 作り方」でググってみました。
すると、[Chrome拡張の作り方 (超概要)](https://go.1470.info/00efca30/)というめっちゃ分かりやすい記事に出会えました！本当にありがとうございました（泣

自分の場合は、ポップアップというよりもページを変えたかったので、少し変えて作りました。（一度作ってから知りました）
## 適当なフォルダを作って「manifest.json」と諸ファイルを作成

拡張機能用にフォルダを作成して、その中に ```src``` フォルダと以下のようなファイルを作成します。
```:ディレクトリ構成図
yt-study-focus/
　└ src/
　　├ manifest.json
　　├ popup-dialog.html
　　├ whiteList.js
　　├ youtube.js
　　├ jquery-3.6.1.min.js
　　└ images/
　　　└（後でアイコンを入れる）
```

### manifest.json
「manifest.json」には、次のように記載します。
```json:manifest.json
{
    "name": "YouTube Study Focus",
    "version": "0.0.1",
    "manifest_version": 3,
    "description": "勉強系以外の YouTube チャンネルの動画を再生する前に警告します",
    "action": {
        "default_popup": "popup-dialog.html"
    },
    "icons": {
        "16": "icon/icon16.png",
        "48": "icon/icon48.png",
        "128": "icon/icon128.png"
    },
    "content_scripts": [
        {
            "js": [
                "jquery-3.6.1.min.js",
                "whiteList.js",
                "youtube.js"
            ],
            "matches": [
                "https://youtube.com/*",
                "https://www.youtube.com/*"
            ]
        }
    ],
    "author": "nao_hanpen"
}
```

### jquery-3.6.1.min.js と whiteList.js
これは jQuery と勉強系チャンネルの配列を書いただけのファイルで、本質ではないので今回は省略します

### youtube.js
ここで、①動画を再生されたら、②チャンネル ID を取得して、③ホワイトリストになければ警告を表示するということをします。
深夜テンションで書いたので汚いです、、書き方勉強します。
```js:youtube.js
$(function() {
    // 読み込んだら、loadCheck() を実行
    loadCheck();
});

// YouTube は Single Page App 的なこと（ページ遷移しないでコンテンツを変更）をするので、タイトルの部分が変更されないかを監視します。
// 変更があったときも、loadCheck() を呼ぶので、このときに２重に監視しないように、監視済みかを設定しておきます。もっとスマートな方法もあると思います。
var is_observed = false;


/**
 * チャンネル ID を取得して、ホワイトリストになければ警告を表示する
 */
function loadCheck() {
    // 現在の URL を取得 (動画ページかどうかの判定に使用)
    var now_url = location.href;
    console.log( now_url );

    // VideoID を取得
    var videoId = getVideoId(now_url);
    if ( videoId === "" ) {
        // VideoID が取得できなければ 動画ページでないと判断して処理を停止
        console.log("このページには動画が含まれません");
        return;
    }
    console.log("VideoID: " + videoId);

    // 処理中は再生を止める
    var is_prevent_play = true; // 停止中かどうか（video が playing されたら停止スクリプトを実行するかどうか）
    // ロード、再生、再生中に
    $('video').on('load play playing', function(e) {
        // 動画を停止
        if( is_prevent_play ) $('video')[0].pause();
    });

    // 処理中にロード画面を追加
    showLoad();

    // チャンネル ID を取得
    // 画面中の要素から取得しているので、YouTube は順にロードするようになっていて時間がかかる
    // 1回目では取得できない。だいたい２～３回必要。
    var channelURL = null; // チャンネル URL
    var channelId = null; // チャンネル ID
    var channelGetCount = 0; // 取得試行回数
    getChannelId();

    /**
     * チャンネル ID を取得
     */
    function getChannelId() {
        channelGetCount++; // 試行回数を加算
        if( $('#channel-name a.yt-simple-endpoint.style-scope.yt-formatted-string')[0] !== undefined ){
            // 要素が存在したら

            // チャンネル URL を取得
            channelURL = $('#channel-name a.yt-simple-endpoint.style-scope.yt-formatted-string')[0].href;
            console.log( "Channel URL:" + channelURL );
            // チャンネル ID を抽出
            channelId = channelURL.replace("https://www.youtube.com/channel/","");
            channelId = channelId.replace("https://www.youtube.com/c/","");
            console.log("ChannelId is #" + channelId);
            // チャンネル ID を検証
            checkChannelId();
            
            // 要素の監視がまだされていなければ
            if( is_observed === false ) {
                // 監視する
                const observer = new MutationObserver(function () {
                    var is_prevent_play = true;
                    loadCheck();
                });
                observer.observe($("ytd-watch-metadata #title")[0], { childList: true, subtree: true });
                // フラグをオンに
                is_observed = true;
            }
        }else{
            // 要素が存在しなかったら

            if( channelGetCount > 30 ) {
                // 30 回以上試行してもできない場合は強制終了
                console.log( "チャンネル ID を取得できませんでした。終了します" );
            }else{
                // 2秒後に再試行
                console.log( "チャンネル ID を取得できませんでした。2秒後に再試行します..." );
                setTimeout(getChannelId,2000);
            }
        }
    }

    /**
     * チャンネル ID を検証 (ホワイトリストになければ警告を表示)
     */
    function checkChannelId() {
        console.log("チャンネル ID を検証します: " + channelId);
        if( whiteChannel.indexOf(channelId) < 0 ) {
            // ホワイトリストになければ、警告を表示
            showBlock();
        }else{
            // ロード画面を閉じて動画を再生
            hideLoad(); // ロード画面を閉じる
            is_prevent_play = false; // 停止中フラグをオフに
            $('video')[0].play(); // 動画を再生
        }
    }

    /**
     * ロード画面を表示
     */
    function showLoad() {
        $("body").append("<div id='loadingView'></div>");
        $("#loadingView")[0].style.cssText = 'background: rgb(72 72 72 / 95%); content: ""; position: fixed; top: 0; left: 0; width: 100%; height: 100%; color: #fff; z-index: 2000; display: flex; justify-content: center; align-items: center; flex-direction: column;';
        $("div#loadingView").append("<h1>Loading...</h1>");
    }

    /**
     * ロード画面を非表示
     */
    function hideLoad() {
        if( $('#loadingView')[0] !== undefined ) {
            $("#loadingView").remove();
        }
    }

    /**
     * 警告を表示
     */
    function showBlock() {
        hideLoad();
        $("body").append("<div id='sendButton'></div>");
        $("#sendButton")[0].style.cssText = 'background: rgb(204 0 0 / 95%); content: ""; position: fixed; top: 0; left: 0; width: 100%; height: 100%; color: #fff; z-index: 2000; display: flex; justify-content: center; align-items: center; flex-direction: column;';
        $("div#sendButton").append("<h1>このコンテンツは時間を浪費する可能性があります</h1>");
        $("div#sendButton").append("<p>時間を取り戻すことはできません</p>");
        $("div#sendButton").append("<div id='agree-btn'>継続して時間を無駄にします</div>");
        $("#agree-btn")[0].style.cssText = 'display: inline-block; margin-top: 20px; padding: 10px; background-color: #fff; color: rgb(88 88 88); border-radius: 6px; font-size: 14px; line-height: 1; font-weight: 600; cursor: pointer;';
        $("#agree-btn").click(function() {
            console.log('無視して動画を再生');
            $("#sendButton").remove();
            is_prevent_play = false;
            $('video')[0].play();
        });
    }
}

/**
 * URL から Video ID を抽出
 * @param {str} url    URL 文字列
 * @return {str} ID または 存在しないときに空
 */
function getVideoId( url ) {
    const { search } = new URL(url);
    const params = new URLSearchParams(search);
    if (params.has('v')) {
        return params.get('v');
    }
    return '';
}
```

# 今後
この拡張機能は、今後、
・ユーザー専用ホワイトリスト
・ユーザー専用ブラックリスト
・保護者向け機能
・ホーム画面（おすすめ）から削除する機能
・コメント非表示機能
を実装する予定です。
「こんな機能あったらいいな」というものがありましたらお気軽にコメントください！

# 参考文献
- 「Chrome拡張の作り方 (超概要)」(2022/10/30 参照) https://qiita.com/RyBB/items/32b2a7b879f21b3edefc
- 「Chrome 拡張機能のマニフェストファイルの書き方」(2022/10/30 参照) https://qiita.com/mdstoy/items/9866544e37987337dc79
- 「Google Developers」(2022/10/30 参照) https://developer.chrome.com/docs/webstore/