Skip to content

Instantly share code, notes, and snippets.

@defaultcf
Last active August 17, 2020 15:10
Show Gist options
  • Save defaultcf/238c78a54fa8d39536fd3ca2f816947a to your computer and use it in GitHub Desktop.
Save defaultcf/238c78a54fa8d39536fd3ca2f816947a to your computer and use it in GitHub Desktop.
Nico Excluder
// ==UserScript==
// @name Nico Excluder
// @namespace https://i544c.github.io
// @version 1.1.2
// @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';
const { version } = GM_info.script;
const _debug = (...msg) => {
console.log('[Nico Excluder]', ...msg);
};
const _fetch = url => new Promise((resolve, _reject) => {
GM_xmlhttpRequest({
url,
method: 'GET',
headers: {
'User-Agent': `nico_excluder/${version}`,
},
onload: res => resolve(res.responseText),
});
});
function* _counter() {
let i = 0;
while(true) yield ++i;
}
const init = () => {
const userList = GM_getValue('denyUserList', null);
if (userList === null) {
_debug('Initialize!');
GM_setValue('denyUserListUrl', 'https://gist.github.com/i544c/238c78a54fa8d39536fd3ca2f816947a/raw/z_deny_user_list_example.json');
}
};
init();
class ApiCache {
constructor() {
this.badContents = GM_getValue('cacheBadContents', []);
this.badContentsMax = 100;
}
addBadContents(contentId, userId) {
if (this.findBadContent(contentId)) return;
this.badContents.push({ contentId, userId });
this.badContents.splice(0, this.badContents.length - this.badContentsMax);
GM_setValue('cacheBadContents', this.badContents);
}
findBadContent(contentId) {
return this.badContents.find(item => item.contentId === contentId);
}
}
class NicoApi {
static endpointGetThumbInfo(contentId) {
return `https://ext.nicovideo.jp/api/getthumbinfo/${contentId}`;
}
static async getMeta(contentId) {
const url = this.endpointGetThumbInfo(contentId);
const rawBody = await _fetch(url);
const domparser = new DOMParser();
const body = domparser.parseFromString(rawBody, 'text/xml');
const userId = body.getElementsByTagName('user_id')[0].textContent;
const tags = body.getElementsByTagName('tags')[0];
return { userId, tags };
}
static removeVideo(contentId) {
_debug('Goodbye!', this.endpointGetThumbInfo(contentId));
const badContent = document.querySelector(`div.MediaObject[data-video-id=${contentId}`);
badContent.remove();
}
}
class Excluder {
constructor() {
this.userList = GM_getValue('denyUserList', []);
this.userListUrl = GM_getValue('denyUserListUrl', null);
this.tagList = GM_getValue('denyTagList', []);
this.tagListUrl = GM_getValue('denyTagListUrl', null);
}
canUpdate() {
const now = new Date();
const lastUpdatedAt = new Date(GM_getValue('updatedAt', 0));
lastUpdatedAt.setHours(lastUpdatedAt.getHours() + 1);
return now.getTime() > lastUpdatedAt.getTime();
}
async update() {
if (!this.canUpdate()) return;
let body, array;
if (this.userListUrl) {
body = await _fetch(this.userListUrl);
array = JSON.parse(body);
this.userList = array;
GM_setValue('denyUserList', array);
}
if (this.tagListUrl) {
body = await _fetch(this.tagListUrl);
array = JSON.parse(body);
this.tagList = array;
GM_setValue('denyTagList', array);
}
const now = new Date();
GM_setValue('updatedAt', now.getTime());
_debug('Updated');
}
shouldExclude(userId, tags) {
const badTags = Array
.from(tags.children, tag => tag.textContent)
.filter(tag => this.tagList.includes(tag));
return this.userList.includes(userId) || badTags.length > 0;
}
}
class Job {
constructor(excluder, apiCache) {
this.excluder = excluder;
this.apiCache = apiCache;
this.timer = null;
this.interval = 1000;
this.queue = [];
}
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() {
const contentId = this.dequeue()
if (!contentId) return;
const { userId, tags } = await NicoApi.getMeta(contentId);
_debug(contentId, userId);
if (!excluder.shouldExclude(userId, tags)) return;
NicoApi.removeVideo(contentId);
apiCache.addBadContents(contentId, userId);
}
}
const excluder = new Excluder;
await excluder.update();
const apiCache = new ApiCache;
const job = new Job(excluder, 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-watchlater-item-id');
const userId = apiCache.findBadContent(contentId);
userId
? NicoApi.removeVideo(contentId)
: job.enqueue(contentId);
}
});
});
const thumbs = document.querySelectorAll('.RankingVideoListContainer div.Thumbnail-image');
thumbs.forEach(thumb => {
observer.observe(thumb, { attributes: true });
});
})();
[
"13168592",
"41533745",
"55763480",
"56035381",
"60703427",
"77100498",
"85520155",
"86475133",
"88359306",
"89578105",
"90808743",
"90987038",
"91829394",
"92490088",
"95946827",
"96021098",
"96709305",
"96906278",
"96997648",
"97164420",
"97220609",
"97295717",
"97318028",
"97498063",
"97505190",
"98046312",
"98401640",
"98459004",
"98657482",
"98680990"
]

Nico Excluder

Support Tampermonkey Support Violentmonkey

ニコ動のホロライブタグのランキングが煽りとかで溢れててキレた

特定の投稿者の動画を非表示にするスクリプト書いたhttps://t.co/WQkIarlGU0

— Isaac (@_leo_isaac) June 16, 2020
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

ニコニコ動画のランキングページにて、指定した投稿者の動画を非表示にするスクリプト。視界から消す、ただそれだけ。

本スクリプトは、拒否したい投稿者のUserIDの配列をdenyUserListというkeyでstorageに設定することで動作する。 また、この配列を外部に保存しておき、denyUserListUrlにそのURLを設定することで、denyUserListを更新することができる。

なお本スクリプトでは、広く知られているニコニコ動画のWebAPIであるgetthumbsinfoを使用している。

https://dic.nicovideo.jp/a/ニコニコ動画API

サービスに過度な負荷をかけないよう、下記の工夫を行っている。

  • 動画のサムネイルが画面の表示領域に来た時に、初めてWebAPIを叩いている
  • 同時に1つのWebAPIしか叩かず、また1秒のインターバルを置いている
  • 一部の結果をキャッシュしている

この工夫は、増えたり減ったりするかもしれない。

簡単な使い方

1. UserScript用のアドオンをインストールする

お使いのブラウザにいずれかのUserScript用のアドオンを入れる。有名なものとして下記のアドオンがあり、いずれも本スクリプトが正常に動作することを確認している。

私個人としては、開発がより盛んであるViolentmonkeyの使用を薦める。

2. 本スクリプトをインストールする

このページでスクリプトをインストールする。

一度、 https://www.nicovideo.jp/ranking/genre/all にアクセスしておく。この時、下記の状況が起こる。

  • 自動的にStorageがセットアップされる
  • Tampermonkeyの場合、「A userscript wants to access a cross-origin resource.」というウィンドウが表示される
    • これは本スクリプトが動画IDからユーザIDを引くために外部のWebAPIを利用しているためである
    • 「Always allow」をクリックして許可してほしい

3. 以上

インストール後に特に設定をしなければ、 私が勝手に作成した 非表示ユーザリストに基づいて動画を非表示にしていきます。 これは、私がエンターテイメントの動画ランキングを見ていて「あ、これ不快」と思った時に、投稿者を調べて追加しています。

勿論、ご自分でカスタマイズすることもできます↓

カスタマイズ方法

1. 拒否したい投稿者のUserIDを確認する

ニコニコ動画上で、拒否したい投稿者のユーザページを開く。 URLはこんな感じのはず。

https://www.nicovideo.jp/user/xxxxxxxx

UserIDはURL上の/user/の後の数字として現れる他、ページ内のID:の後に表示される。このUserIDをクリップボードにコピーするなどしておく。

2. 拒否したい投稿者のUserIDを設定する

ここではViolentmonkeyでのストレージの設定方法について記す。 Tampermonkeyでストレージを編集したい場合は、コチラを参考にしてほしい。

Violentmonkeyの設定画面を開き、本スクリプトの「Edit」ボタンをクリックする。 上のメニュータブに「Values」タブがあるので、クリックする。

「No value is stored」を表示されていれば、「+」(プラス)ボタンをクリックし、次のように入力する。

  • Keyには、「denyUserList」と入れる
  • Valueには、拒否したい投稿者のUserID(String)を配列にしたものを入れる

つまり、

[
    "xxxxxxxx",
    "yyyyyyyy",
    "zzzzzzzz"
]

のように入力する。これはJSONの記述であるから、UserIDはダブルクオーテーション(")で囲み、文字列の最後にはカンマ(,)を付ける(最後の行にカンマを付けてはいけない)。これを拒否したい投稿者のUserIDの分だけ入力する。

なお、このJSONをウェブ上に保存しておき、先程の要領でストレージに下記のように設定することもできる。一度取得すると1時間キャッシュされる。

  • keyには「denyUserListUrl」と入れる
  • Valueには、JSONで保存した先のURLを入力する
    • ダブルクオーテーションなどで囲うとエラーになります

終わりに

何か質問などあれば、フィードバックに書き込んで頂くか、個別に連絡される場合はTwitterのDMまでご連絡ください。

@defaultcf
Copy link
Author

Greasyforkに載せているので、是非コチラからインストールしてください!
https://greasyfork.org/ja/scripts/405548-nico-excluder

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment