どっかの鳥の日常

主に開発についてのブログです。たまに他愛ないことも。

Chrome拡張でYouTubeの関連動画を消す

はじめに

この記事は SLP KBIT Advent Calendar 2019 23日目の記事です。 adventar.org

昨年のアドベントカレンダーの記事も良ければどうぞ。 qiita.com

最近Youtubeの見すぎで時間が溶けていることがよくあります。(なんでや)
原因を自分なりに考察()してみたら、関連動画を押し続けて止め時がないことだという答えに行き着きました。
今回は、そんなお悩みを解決するためにChrome 拡張でYoutubeの関連動画を消してみます。
Chrome拡張をやるにあたって私が詰まったところを書いていくので、Chrome拡張に興味がある人はぜひ見ていって下さい。(私は初心者)
こうしたほうがいいよ〜、ということがあれば教えていただけると助かります。

準備

ファイルの準備

まずはじめに、適当に必要なファイルを作っていきます。
今回の構成はこんな感じです。

.
├─ src
│   ├─ assets
│   │   ├─ css
│   │   │   └─ options.css
│   │   ├─ img
│   │   │   └─ icon.png
│   │   ├─ js
│   │   │   └─ options.js
│   │   └─ options.html
│   ├─ content_scripts
│   │   └─ youtube.js
│   └─ background.js
└─ manifest.json

今後、もう少し拡張したいと思ったので、ディレクトリを結構細かく分割しておきました。(niconicoとかもできたらいいな)
Chrome拡張で(多分)一番重要になるのはmanifest.jsonです。

私的にこの記事が結構わかりやすいと思いました。 qiita.com

↑を参考にmanifest.jsonを書いていきます。

{
  "name": "RelatedVideosBlocker",
  "short_name": "RVB",
  "version": "1.0.0",
  "manifest_version": 2,
  "description": "This Chrome Extension delete related videos",
  "icons": {
    "16": "src/assets/img/icon.png",
    "48": "src/assets/img/icon.png",
    "128": "src/assets/img/icon.png"
  },
  "permissions": [
    "tabs",
    "storage"
  ],
  "background": {
    "scripts": [
      "src/background.js"
    ],
    "persistent": false
  },
  "browser_action": {
    "default_icon": "src/assets/img/icon.png",
    "default_title": "RelatedVideosBlocker",
    "default_popup": "src/assets/options.html"
  },
  "options_ui": {
    "page": "src/assets/options.html",
    "open_in_tab": false
  },
  "content_scripts": [
    {
      "matches": [
        "https://www.youtube.com/watch?v=*"
      ],
      "js": [
        "src/content_scripts/youtube.js"
      ]
    }
  ]
}

参考のページにない項目を説明していきます。

options_ui

以前まではoptions_pageが使われていたのですが、options_uiに変更されました。
options_uiのpageでは、options_pageと同様に、オプション画面で表示する画面を選択します。
open_in_tabは、オプションを開くときに、新しいタブを開くかといったものです。
基本的にfalseでいいと思います。

Chromeの準備

ここまでできたらChrome拡張機能を読み込みます。
Chromeの設定から拡張機能に行きます。
拡張機能の右上にあるディベロッパーモードをオンにします。
そしたら「パッケージ化されていない拡張機能を読み込む」から作成したプロジェクトを選択すれば、読み込みは完了です。
開発中にコードを書き換えた場合は、拡張機能をしっかり更新してあげてください。

f:id:tokutatsu:20191222215027p:plain

ここまでで準備は完了です。

中身の処理を書いていく

ここからは実際に処理を書いていきます。

youtube.js

ここでは、特定のリンク(今回はYouTubeのリンク)に訪れたときの処理を書きます。
今回は、関連動画と広告の削除(非表示)を行います。
基本的に、DOM操作によって要素を消すので、消したいものを探す場合はF12を押してElementsを見てあげましょう。
ここらへんは色々やりようがあると思います。(いいやり方がわからない)
chrome.runtime.sendMessageについては、後のbackground.jsのchrome.runtime.onMessage.addListenerとセットなのでそこで説明します。

// コンテンツ読み込み時に拡張機能が有効か確認して有効なら処理を実行
chrome.runtime.sendMessage({ contents: 'youtube' }, (isAvailable) => {
    if (isAvailable) {
        setTimeout(() => {
            hideRelatedVideos();
            removeAdvertisement();
        }, 1000);

    } else {
        setTimeout(() => {
            showRelatedVideos();
        }, 1000);
    }
});

// 関連動画を表示する
function showRelatedVideos() {
    // 関連動画などが表示されているブロック全体
    const secondary = document.getElementById('secondary');
    // 自動再生のトグル操作を行う要素
    const head = document.getElementById('head');
    // 次の動画と表示されるテキスト
    const upnext = document.getElementById('upnext');
    // 動画終了時に表示される関連動画
    let videos = document.querySelector('.videowall-endscreen');
    // 広告の時間待機する秒数
    const LIMIT = 180;
    // 1秒でカウンタが1増える
    let limit_counter = 0;

    secondary.style.visibility = 'visible';
    head.style.visibility = 'visible';
    upnext.style.visibility = 'visible';

    const interval = setInterval(() => {
        videos = document.querySelector('.videowall-endscreen');
        if (videos != null) {
            videos.style.visibility = 'visible';
            clearInterval(interval);
        }
        if (limit_counter >= LIMIT) {
            clearInterval(interval);
        }
        limit_counter++;
    }, 1000);
}

// 関連動画を非表示にする
function hideRelatedVideos() {
    // 関連動画などが表示されているブロック全体
    const secondary = document.getElementById('secondary');
    // 自動再生のトグル操作を行う要素
    const head = document.getElementById('head');
    // 次の動画と表示されるテキスト
    const upnext = document.getElementById('upnext');
    // 動画終了時に表示される関連動画
    let videos = document.querySelector('.videowall-endscreen');
    // 広告の時間待機する秒数
    const LIMIT = 180;
    // 1秒でカウンタが1増える
    let limit_counter = 0;

    secondary.style.visibility = 'hidden';
    head.style.visibility = 'visible';
    upnext.style.visibility = 'hidden';

    const interval = setInterval(() => {
        videos = document.querySelector('.videowall-endscreen');
        if (videos != null) {
            videos.style.visibility = 'hidden';
            clearInterval(interval);
        }
        if (limit_counter >= LIMIT) {
            clearInterval(interval);
        }
        limit_counter++;
    }, 1000);
}

// 広告の削除
function removeAdvertisement() {
    // 関連動画がある場所の広告
    const playerAds = document.querySelectorAll('#player-ads');

    for (const playerAd of playerAds) {
        playerAd.remove();
    }
}

↓ここの部分、これめっちゃ汚いです。
なぜこのようにしているかというと、YouTubeでは広告があります。
広告動画の間はvideowall-endscreen要素が存在しないため、エラーを吐いてしまいます。
なので、理想は広告動画が終わったときに要素を消してあげたいのですがいい方法が見つかりませんでした。
よって、今回はvideowall-endscreen要素を発見するまで探し続けることにしました。(ザル実装)
一応リミットを設けてあげなければ無限ループしてしまうのでカウンタをつけてあげてます。

const interval = setInterval(() => {
   videos = document.querySelector('.videowall-endscreen');
    if (videos != null) {
        videos.remove();
        clearInterval(interval);
    }
    if (limit_counter >= LIMIT) {
        clearInterval(interval);
    }
    limit_counter++;
}, 1000);

options.htmlとoptions.js

ここでは、オプション画面を作成します。
今回、オプション画面では拡張機能のオンオフを設定します。
CSSはおまけなので省略します。 HTMLではチェックボックスを設置するだけです。(簡単)

<!DOCTYPE html>
<html lang="ja">

<head>
  <meta charset="UTF-8" />
  <link rel="stylesheet" href="css/options.css" />
  <title>Document</title>
</head>

<body>
  <div class="switch">
    <span>youtube</span>
    <label class="switch__label">
      <input id="youtube" type="checkbox" class="switch__input" />
      <span class="switch__content"></span>
      <span class="switch__circle"></span>
    </label>
  </div>
  <script type="text/javascript" src="js/options.js"></script>
</body>

</html>

次にJavaScriptの方を説明します。
まず、HTMLからチェックボックスの要素を持って来ます。
その後、チェックボックスの要素がオンかオフかをChromeのストレージから取り出して、チェックボックスに反映させます。
チェックボックスがクリックされると、Chromeのストレージにクリックされているか否かの状態を保存します。

const youtube = document.getElementById('youtube');

// ストレージから情報を取得
chrome.storage.sync.get(['youtube'], (value) => {
    youtube.checked = value.youtube;
});

// youtubeの有効と無効を切り替え
youtube.addEventListener('click', () => {
    chrome.storage.sync.set({ 'youtube': youtube.checked });
});

CSSも反映させてできたのがこちら。(niconicoは息をしてません) f:id:tokutatsu:20191222184729g:plain

基本は設定に飛ばなくてはいけないのですが、manifest.jsonbrowser_actionにoptions.htmlを指定してるため、アイコンをクリックすれば表示されます。

background.js

background.jsでは、拡張機能の裏で動く処理を記述します。
今回は、オプション画面での拡張機能が有効かを確認して、処理を行うかを判定することを行います。
ここで、youtube.jsで出てきたchrome.runtime.sendMessagechrome.runtime.onMessage.addListenerの関係です。
background.jsのchrome.runtime.onMessage.addListenerは、chrome.runtime.sendMessageで送られるメッセージを受けつけています。
そのため、chrome.runtime.sendMessageの第一引数の値が、requestの中に入ります。
ここでは、そのコンテンツが何かで処理を分けてあげます。
そして、Chromeのストレージから拡張機能が有効であるかをsendResponse()により、chrome.runtime.sendMessageの方に返します。
また、同期的に処理を行う場合は、明示的にtrueを返してあげる必要があります。

// どのコンテンツに飛んだかを確認して有効か無効化を判定する
chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
    if (request.contents == 'youtube') {
        chrome.storage.sync.get('youtube', (value) => {
            sendResponse(value.youtube);
        });
    }
    // 同期的に待つ場合は明示的にtrueを返す
    return true;
});

できた!

モザイクだらけの関連動画達が... f:id:tokutatsu:20191222230712p:plain

スッキリ! f:id:tokutatsu:20191222230720p:plain これで完成!...と思いきや...

見事にかかった罠

よっしゃできたぞ!とYouTubeのページをポチポチ...
あれ、非表示にできないぞ。
リロードしたら消えるのに...
YouTubeはSPAでした。(多分、間違っていたらごめんなさい)
SPAとはSingle Page Applicationの略で単一のWebページでアプリケーションを構成する設計構造のことです。
今まで使っていた、Content ScriptsではSPAに対応することが多分できません。
ページの更新をしなければ切り替えができないという状況になります。
そこで、chrome.tabsを使うとSPAのページでも動くみたいなのでこれを使ってみます。
developer.chrome.com

やはり何事にも先駆者はいるものですね。助かります。 r17n.page

先駆者を参考に、background.jsに以下のコードを追加します。
気を付けポイントは、youtube.jsのパスをプロジェクトのルートにするとこです。(background.jsからのパスにしないように)

chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
    if (changeInfo.status === 'complete' && tab.url.indexOf('https://www.youtube.com/watch?v=') > -1) {
        console.log(`updated: ${tab.url}`);
        chrome.tabs.executeScript(
            tabId,
            {
                file: '/src/content_scripts/youtube.js',
            },
            (result) => {
                console.log(`executed: ${result}`);
            }
        );
    }
});

manifest.jsonも少し書き換えます。

{
  "name": "RelatedVideosBlocker",
  "short_name": "RVB",
  "version": "1.0.0",
  "manifest_version": 2,
  "description": "This Chrome Extension delete related videos",
  "icons": {
    "16": "src/assets/img/icon.png",
    "48": "src/assets/img/icon.png",
    "128": "src/assets/img/icon.png"
  },
  "permissions": [
    "tabs",
    "storage",
    "https://www.youtube.com/watch?v=*"
  ],
  "background": {
    "scripts": [
      "src/background.js"
    ],
    "persistent": false
  },
  "browser_action": {
    "default_icon": "src/assets/img/icon.png",
    "default_title": "RelatedVideosBlocker",
    "default_popup": "src/assets/options.html"
  },
  "options_ui": {
    "page": "src/assets/options.html",
    "open_in_tab": false
  }
}

これでSPAのページにも対応することができたはずです。 完成!(動作は基本同じなのでスクショはなしで)

さいごに

結構ゴリ押しでしたが、なんとか形にはなった気がします。
とりあえずバグを減らしていかなければ...と思いつつ、機能追加もしていければいいなと思っています。
バグがあったら教えて下さい...

作成したもの

分量的に主要な部分しか載せられなかったので、ここが微妙といったところはコードを見てくれればいいと思います。(ほとんど載せましたが...) github.com