特定の文字列を含む動画を非表示にするChrome拡張機能を作った

## 作ったもの

設定した文字列を含むタイトルの動画を非表示にするchrome拡張機能です。

拡張機能のアイコンをクリックするとテキストエリアが現れるので、そこにフィルタリングしたい文字列を入力して保存ボタンを押すだけで設定は完了です。

あとは自動で画面遷移時に対象動画を非表示にしてくれます。

ソースコードは[こちら](https://github.com/sayuprc/youtube_filter)

## デモ

### 設定画面
![f.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/271796/1a674801-37ed-958a-62fc-809dc6a2e177.png)

### 設定前
![前.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/271796/d672483d-5142-2745-10e6-8d5a07b54539.png)

### 設定後
![あと.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/271796/98178450-0a18-bf90-6d6b-e2b739e01f2d.png)

## 作った動機

夏と言えばホラー！
私はホラーが大の苦手なので怖い動画は見たくない！
ならば消してしまえ！という感じで作りました。

## 仕組み

Event Pageでタブの変更を検知、Content ScriptでDOMの削除と監視を行っています。

```event.ts
chrome.tabs.onUpdated.addListener((_, info, tab) => {
  const url: string = tab.url ?? '';

  // タブが読み込み完了かつurlに`www.youtube.com`を含む
  if (info.status === 'complete' && 0 < url.indexOf('www.youtube.com')) {
    chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
      const currentUrl: string | undefined = tabs[0]?.url;
      const tabid: number | undefined = tabs[0]?.id;
      if (currentUrl != null && tabid != null) {
        // 指定タブにurlを送信(content scriptが受け取る)
        chrome.tabs.sendMessage(tabid, { message: currentUrl });
      }
    });
  }
});
```

```content.ts

chrome.runtime.onMessage.addListener((request, _, sendResonse) => {
  const url: string = request.message;

  // ストレージに保存してある設定した文字列を取得
  chrome.storage.local.get(['filter'], (storage) => {
    if (storage.filter != null) {
      const filters: string[] = storage.filter
        .split('\n')
        .filter((v: string) => v !== '');
      const querySelector: string = '#video-title';

      let targetElement: HTMLElement | null | undefined = null;

      // 監視対象の要素を取得
      if (
        ~url.indexOf('/feed/subscriptions') ||
        url === 'https://www.youtube.com/'
      ) {
        targetElement = document.getElementById('contents');
      } else if (~url.indexOf('/watch?')) {
        targetElement = document
          .getElementById('related')
          ?.querySelector('#items');
      }

      if (targetElement != null) {
        const removeElement = remove(querySelector)(filters);
        // 初回のみすでに読み込んである動画要素に対してフィルター処理を実行
        removeElement(targetElement.childNodes as NodeListOf<HTMLElement>);

        // 監視対象要素を監視
        observe(removeElement, targetElement);
      }
    }
  });

  sendResonse();

  return;
});

const remove: (
  querySelector: string
) => (filters: string[]) => (targetNodes: NodeListOf<HTMLElement>) => void =
  (querySelector) => (filtres) => (targetNodes) => {
    targetNodes.forEach((node) => {
      node.querySelectorAll(querySelector).forEach((titleElement) => {
        filtres.forEach((filter) => {
          if (~titleElement.innerHTML.indexOf(filter)) {
            node.remove();
          }
        });
      });
    });
  };

function observe(
  func: (nodeList: NodeListOf<HTMLElement>) => void,
  targetElement: Node
) {
  const mutationObserver = new MutationObserver((mr) => {
    func(mr[0].addedNodes as NodeListOf<HTMLElement>);
  });

  const observeConfig: object = {
    attributes: false,
    attributeOldValue: false,
    characterData: false,
    characterDataOldValue: false,
    childList: true,
    subtree: false,
  };

  mutationObserver.observe(targetElement, observeConfig);
}
```

## 最後に

この拡張機能はストアに出す予定はないので、試してみたい方は[リポジトリ](https://github.com/sayuprc/youtube_filter)からDLしてみてください。

今回初めてTypeScriptを使っての開発をしてみました。
型のおかげで開発速度がそれなりに早くできたかと思います。(コードはイケてないのでもっと勉強が必要)

## 参考

[API Reference - Chrome Developers](https://developer.chrome.com/docs/extensions/reference/)

[Chrome Extension の作り方 (その1: 3つの世界)](https://qiita.com/sakaimo/items/416f36db1aa982d8d00c)