ニコニコ動画の動画再生ページに広告コメントを表示させるGMスクリプト: NicoAds Tab を作った

ダウンロードはこちら
NicoAds Tab for Greasemonkey


先日作ったライブラリを使いがてらつくってみた。スクリーンショットを見て分かるとおり、かなりのやっつけ実装。最初はちゃんとしたUI作ろうと思ってたんだけど、思いの他面倒だったから結局やっつけた。

2010/10/29追記

ニコニコ動画(原宿)で広告ページのレイアウトが変更されたので,それにあわせて修正しました。

2010/11/10追記

ニコニ広告のHTML変更の関係で動作がおかしくなっていたのを修正しました。

ニコニコ動画の動画再生ページのタブをGreasemonkeyから追加する方法

さっき書いた関数 (Nicopedia Tabで使ったやつ)を使えば,Greasemonkeyからタブが追加できたりしていろいろ便利になるんじゃないかな,と思ったので公開します。

function createPanel(label, id, action) {
  id = 'itab_' + id;

  let itab = document.querySelector('#itab td');
  let link = document.createElement('a');
  link.href = '#'+id;
  // a の中に div いれるとかちょっとどうにかしてほしい
  link.innerHTML = '<div>' + label + '</div>';
  itab.appendChild(link);

  let info_frm = document.querySelector('.info_frm');
  let panel = document.createElement('div');
  panel.id = id;
  panel.className = 'info';
  info_frm.appendChild(panel);

  unsafeWindow.cont = unsafeWindow.$$('.info_frm .info');

  const active = 'in';
  link.addEventListener('click', function(e) {
    e.preventDefault();
    Array.forEach(document.querySelectorAll('#itab td a'),
                  function(elm) elm.classList.remove(active));
    link.classList.add(active);
    Array.forEach(document.querySelectorAll('.info_frm .info'),
                  function(elm) elm.id === id
                  ? elm.classList.add(active)
                  : elm.classList.remove(active));
  }, false);

  if (typeof action === 'function')
    link.addEventListener('click', function(e) action(panel), false);

  return panel;
}

使い方

※createPanelの第三引数は省略可能です

  // ニコニコ大百科という名前のタブを作る (panel.id == nicopedia)
  let panel = createPanel('ニコニコ大百科', 'nicopedia', function onActivate() {
    // タブが表示された時(ラベルがクリックされた時)の処理
    alert('clicked!!');
  });
  // タブのパネルに表示する内容を指定する
  panel.appendChild(...);

追記

gistにアップしました。ニコニコ動画のwatchページのタブを追加する関数
GMスクリプトのヘッダ部分に

// @require http://gist.github.com/raw/636265/11243a0856cc32f4d5633b95d9411aecb1bd820b/nicovideo_createPanel.js

を書き加えるだけで使えるようになります。

新Greasemonkeyスクリプト:Nicopedia Tab を作った

ニコニコ動画の動画再生ページに,ニコニコ大百科の動画記事(の概要?)を埋め込むGreasemonkeyスクリプトです。今回のリニューアルで消えちゃったやつをタブとして復活させました。

インストールはこちら
Nicopedia Tab for Greasemonkey


運営が本気出せば数行のHTML+スクリプトで書ける内容なんだから,わざわざユーザースクリプトを書かせることなく,公式でやればいいのになー。と思いました。せっかくのタブインターフェースなんだしタブ増やそうよ。

nicovideo Add Thumbnailを更新

ニコニコ動画の動画再生ページにサムネイルを表示する"nicovideo add Thumbnail"を更新して新インターフェースでも動くようにしました。

インストールはこちら
nicovideo Add Thumbnail for Greasemonkey

今回動画タイトルが<h1>じゃなくなったのもそうだけど,ニコニコ動画はテーブル使いまくりなのとID/クラス振らなさすぎ(振ってもclass="TXT12"とかいうふざけた名前だったりする)なのをもう少しなんとかしてくれるとGMスクリプト作者としてはいろいろうれしい…。

ニコニコ動画の「もったいないスペース」を減らすユーザースタイル書いた

ニコニコ動画の動画再生ページが新インターフェースになりましたね!これまで目立たなかったユーザー情報が目立つようになったのが良いですね!
そんな新インターフェースなんですが,画説明文が長かったりする場合に下の図のように「もったいないスペース」が生まれてしまいます(旧インターフェースの時もそうでしたが)。

このもったいないスペースを解消するユーザーCSSを書きました。

インストールはこちら
ニコニコ動画 - watchページの余分なスペースを無くす - Themes and Skins for Nicovideo - userstyles.org

このスタイルを適用すると,動画説明文が以下のように縮んで表示されます。

マウスカーソルを載っけると全文が表示されます。

便利かどうかはこれから使い込んでみないと分かりませんが,見た目はかなりスッキリしていい感じです。

ニコニコ動画の「キーワードを含むタグ検索」に大百科アイコンを表示させるGreasemonkeyスクリプト

久しぶりに新Greasemonkeyスクリプトを作りました。
userscripts.orgが落ちてたのでソースコードを貼り付けておきます。適当にコピペして,保存したファイルFirefoxにドラッグすればインストールできるかと。できないかも。
userscripts.orgにアップロートしました:Nicopedia Existence Checker for Greasemonkey
機能は非常に単純で,タグ名の横に大百科アイコンを表示させるだけ(スクリーンショット参照。)

画面内に表示されているタグのみ大百科アイコンを表示させるようにしたので,余計な通信が発生せずサーバにやさしい仕様になっているかと思います。
以下ソースコード。LazyLoader クラスは何かと使い回しがききそうです。

// ==UserScript==
// @name           Nicopedia Existence Checker
// @namespace      http://mfp.xrea.jp/
// @include        http://www.nicovideo.jp/related_tag/*
// ==/UserScript==

const LOAD_WAIT = 1000;
const SHOW_MARGIN = 20;

const JSONP_CALLBACK = 'gm_nicopedia_existence_checker';
const NICOPEDIA_API_URL = 'http://api.nicodic.jp/e/';
const NICOPEDIA_URL = 'http://dic.nicovideo.jp/a/';
const UA_STRING = 'Mozilla/5.0 Greasemonkey; Nicopedia Existence Checker';
const NICOPEDIA_ON_ICON_URL = 'http://res.nimg.jp/img/common/icon/dic_on.gif';
const NICOPEDIA_OFF_ICON_URL = 'http://res.nimg.jp/img/common/icon/dic_off.gif';


// 2つのソート済み配列をマージする
// x < y => comparer(x, y) < 0
function mergeSorted(a, b, comparer) {
  if (comparer === undefined)
    comparer = function(x, y) x - y;

  let ia = 0, ib = 0, la = a.length, lb = b.length;
  let array = new Array(la + lb);
  while (true) {
    if (ia === la) {
      for (; ib < lb; ib++)
        array[ia+ib] = b[ib];
      break;
    }
    if (ib === lb) {
      for (; ia < la; ia++)
        array[ia+ib] = a[ia];
      break;
    }
    if (comparer(a[ia], b[ib]) <= 0) {
      array[ia+ib] = a[ia];
      ia++;
    } else {
      array[ia+ib] = b[ib];
      ib++;
    }
  }
  return array;
}


// ソート済みの配列中から,指定した値以上の大きさを持つ最小の要素のインデックスを返す
// x < (検索する値) => comparer(x) < 0
function binarySearch(array, comparer) {
  if (array.length === 0)
    return 0;

  if (typeof comparer !== 'function') {
    let n = comparer;
    comparer = function(x) x - n;
  }

  let min = 0, max = array.length - 1;
  if (comparer(array[max]) < 0)
    return max + 1;

  while (min < max) {
    let mid = min + ((max - min) >>> 1);
    if (comparer(array[mid]) >= 0) {
      max = mid;
    } else {
      min = mid + 1;
    }
  }
  return min;
}


// 要素の相対位置を取得する
function getOffset(elem, root) {
  if (root === undefined)
    root = document.documentElement;

  if (elem === null)
    return { top: 0, left: 0 };

  let cmp = root.compareDocumentPosition(elem);
  // elem isn't contained by root
  if ((cmp & document.DOCUMENT_POSITION_CONTAINED_BY) === 0) {
    return { top: 0, left: 0 };
  }

  let pos = { top: elem.offsetTop, left: elem.offsetLeft };
  let p = getOffset(elem.offsetParent, root);
  pos.top += p.top;
  pos.left += p.left;
  return pos;
}


// 現在表示されている要素を取得するためのクラス
let LazyLoader = function(root) {
  if (root === undefined)
    this.root = document.documentElement;
  else
    this.root = root;
  this._elemPool = [];
  this._scrollCallback = [];

  let self = this;
  window.addEventListener('scroll', function() self._updateScrollPos(), false);
  window.addEventListener('resize', function() self._updateScrollPos(), false);
  this._updateScrollPos();
};

LazyLoader.prototype = {
  _elemPool: null,
  _scrollCallback: null,
  _pointer: NaN,
  _scrollTop: NaN, _scrollBottom: NaN,
  _cnt: 0,
  addScrollCallback: function(callback) {
    this._scrollCallback.push(callback);
  },
  removeScrollCallback: function(callback) {
    let idx = this._scrollCallback.indexOf(callback);
    if (idx !== -1)
      this._scrollCallback.splice(idx, 1);
  },
  pushElements: function(elems) {
    let objs = Array.map(elems, function(e) {
                           let pos = getOffset(e, this.root);
                           return { top: pos.top, elem: e };
                         });
    let comparer = function(a, b) a.top - b.top;
    this._elemPool = mergeSorted(this._elemPool, objs.sort(comparer), comparer);
    this._pointer = -1;
  },
  _cleanup: function() {
    let root = this.root;
    // 要素を追加した際にoffsetが変わる可能性があるので再計算しなおす.
    // 順序は変わらないものとする
    this._elemPool = this._elemPool
      .filter(function(e) e.elem !== null)
      .map(function(e) {
             e.top = getOffset(e.elem, root).top;
             return e;
           });
  },
  pop: function() {
    let top = this._scrollTop - SHOW_MARGIN;
    if (this._pointer === -1) {
      this._cnt = 0;
      this._pointer = binarySearch(this._elemPool,
        function(e) e.top - top);
    }

    let len = this._elemPool.length;
    while (this._pointer < len && this._elemPool[this._pointer].elem === null)
      this._pointer++;
    if (this._pointer === len)
      return null;

    let obj = this._elemPool[this._pointer];
    if (obj.top > this._scrollBottom + SHOW_MARGIN)
      return null;

    let elem = obj.elem;
    obj.elem = null;
    this._pointer++;

    // 30回に1回,popされた要素を配列から削除する
    this._cnt++;
    if (this._cnt >= 30) {
      this._cnt = 0;
      this._cleanup();
      this._pointer = -1;
    }

    return elem;
  },
  _updateScrollPos: function() {
    this._pointer = -1;
    this._scrollTop = this.root.scrollTop;
    this._scrollBottom = this.root.scrollTop + this.root.offsetHeight;
    this._scrollCallback.forEach(function(c) c());
  }
};


let NicopediaDecorator = {
  _queue: new LazyLoader(),
  _cache: {},
  _loading: false,
  _load: function(name, callback) {
    GM_xmlhttpRequest({
      method: 'GET',
      url: NICOPEDIA_API_URL + JSONP_CALLBACK + '/' + encodeURIComponent(name),
      headers: { 'User-Agent': UA_STRING },
      onload: function({responseText: text}) {
        let rep = text.replace(
          new RegExp('^' + JSONP_CALLBACK + '\\(\\[(\\d)\\]\\);$'), '$1');
        switch (rep) {
        case '0': callback(false); return;
        case '1': callback(true); return;
        default:  callback(null); return;
        }
      },
      onerror: function() {
        callback(null);
      }
    });
  },
  _insertIcon: function(link) {
    let name = link.textContent;
    let url = NICOPEDIA_URL + encodeURIComponent(name);
    let exist = this._cache[name];

    let icon = document.createElement('a');
    icon.href = url;
    let img = document.createElement('img');
    img.className = 'txticon';

    if (exist === null) {
      img.src = '';
      img.alt = '×';
      icon.title = '読み込み失敗';
    } else if (exist) {
      img.src = NICOPEDIA_ON_ICON_URL;
      img.alt = '百';
      icon.title = '大百科 ' + name + ' の記事を読む';
    } else {
      img.src = NICOPEDIA_OFF_ICON_URL;
      img.alt = '?';
      icon.title = '大百科 ' + name + ' の記事を書く';
    }

    icon.appendChild(img);
    link.parentNode.insertBefore(icon, link);
  },
  _update: function() {
    if (this._loading)
      return;

    this._loading = true;

    // 既に読み込み済みでキャッシュに入っているタグは即座に表示する
    let link = this._queue.pop();
    while (link !== null && link.textContent in this._cache) {
      this._insertIcon(link);
      link = this._queue.pop();
    }

    if (link === null) {
      this._loading = false;
      return;
    }

    let self = this;
    let name = link.textContent;
    this._load(name, function(exist) {
                 self._cache[name] = exist;
                 self._insertIcon(link);
                 setTimeout(function() {
                              self._loading = false;
                              self._update();
                            }, LOAD_WAIT);
               });
  },
  init: function() {
    this._cache.__proto__ = null;
    let self = this;
    this._queue.addScrollCallback(function() { self._update(); });
  },
  decorate: function(links) {
    this._queue.pushElements(links);
    this._update();
  }
};


(function main() {
   function handle(target) {
     let links = target.querySelectorAll(
       'a[href^="tag/"], a[href^="http://www.nicovideo.jp/tag/"]');
     NicopediaDecorator.decorate(links);
   }

   NicopediaDecorator.init();
   document.addEventListener('AutoPagerize_DOMNodeInserted',
                             function({target}) handle(target), false);
   handle(document);
 })();