Nico Excluder

ユーザ拒否リストに引っかかった動画を非表示にする

目前为 2020-06-20 提交的版本。查看 最新版本

// ==UserScript==
// @name         Nico Excluder
// @namespace    https://i544c.github.io
// @version      1.0.0
// @description  ユーザ拒否リストに引っかかった動画を非表示にする
// @author       i544c
// @match        https://www.nicovideo.jp/ranking/*
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_xmlhttpRequest
// @run-at       document-end
// @license      MIT
// ==/UserScript==

(async () => {
  'use strict';

  GM_setValue();

  const _debug = (...msg) => {
    console.log(`[Nico Excluder] ${msg.join(' ')}`);
  };

  const _fetch = url => new Promise((resolve, _reject) => {
    GM_xmlhttpRequest({
      url,
      method: 'GET',
      headers: {
        'User-Agent': 'nico_excluder/0.1.6',
      },
      onload: res => resolve(res.responseText),
    });
  });

  class ApiCache {
    constructor() {
      this.badContents = GM_getValue('cacheBadContents', []);
      this.badContentsMax = 100;
    }

    setBadContents(contentId, userId) {
      if (this.getBadContents(contentId)) return;

      this.badContents.push({ contentId, userId });
      if (this.badContents.length > this.badContentsMax) {
        this.badContents.splice(0, this.badContentsMax - this.badContents.length);
      }
      GM_setValue('cacheBadContents', this.badContents);
    }

    getBadContents(contentId) {
      return this.badContents.find(item => item.contentId === contentId);
    }
  }

  class NicoApi {
    static endpointGetThumbInfo(contentId) {
      return `https://ext.nicovideo.jp/api/getthumbinfo/${contentId}`;
    }

    static async getUserId(contentId) {
      const url = this.endpointGetThumbInfo(contentId);
      const body = await _fetch(url);
      const domparser = new DOMParser();
      const response = domparser.parseFromString(body, 'text/xml');
      const userId = response.getElementsByTagName('user_id')[0].textContent;
      return userId;
    }
  }

  class DenyUserList {
    constructor() {
      this.array = GM_getValue('denyUserList', []);
      this.url = GM_getValue('denyUserListUrl', null);
    }

    canUpdate() {
      const now = new Date();
      const lastUpdatedAt = new Date(GM_getValue('updatedAt', 0));
      lastUpdatedAt.setHours(lastUpdatedAt.getHours() + 1);
      return this.url && now.getTime() > lastUpdatedAt.getTime();
    }

    async update() {
      if (!this.canUpdate()) return;

      const body = await _fetch(this.url);
      const array = JSON.parse(body);
      const now = new Date();
      this.array = array;
      GM_setValue('denyUserList', array);
      GM_setValue('updatedAt', now.getTime());
      _debug('Updated');
    }
  }

  class Job {
    constructor(denyUserList, apiCache) {
      this.denyUserList = denyUserList;
      this.timer = null;
      this.interval = 1000;
      this.queue = [];
      this.apiCache = apiCache;
    }

    check(contentId) {
      const cachedContent = this.apiCache.getBadContents(contentId);
      cachedContent
        ? this.run(contentId, cachedContent.userId)
        : this.enqueue(contentId)
    }

    enqueue(contentId) {
      this.queue.push(contentId);
    }

    dequeue() {
      return this.queue.shift();
    }

    start() {
      if (this.timer) {
        console.warn('Already running');
        return;
      }

      this.timer = window.setInterval(() => this.run(), this.interval);
    }

    stop() {
      window.clearInterval(this.timer);
    }

    async run(contentId = this.dequeue(), userId = null) {
      if (!userId) { // Called from queue
        if (this.queue.length === 0) return;
        userId = await NicoApi.getUserId(contentId);
      }
      _debug(contentId, userId);
      if (!this.denyUserList.includes(userId)) return;

      _debug('Goodbye!', NicoApi.endpointGetThumbInfo(contentId));
      document.querySelector(`div.MediaObject[data-video-id=${contentId}`).remove();
      apiCache.setBadContents(contentId, userId);
    }
  }

  const list = new DenyUserList;
  await list.update();

  const apiCache = new ApiCache;
  const job = new Job(list.array, apiCache);
  job.start();

  const observer = new MutationObserver(mutations => {
    mutations.forEach(mutation => {
      if (mutation.type === 'attributes' && mutation.attributeName === 'data-loaded') {
        const contentId = mutation.target.parentElement.getAttribute('data-deflist-item-id');
        job.check(contentId);
      }
    });
  });

  const thumbs = document.querySelectorAll('.RankingVideoListContainer div.Thumbnail-image');
  thumbs.forEach(thumb => {
    observer.observe(thumb, { attributes: true });
  });
})();