nicovideo Add Stars to Tags

By gifnksm Last update Jul 9, 2009 — Installed 2,424 times. Daily Installs: 7, 6, 2, 6, 9, 4, 4, 3, 8, 6, 2, 5, 7, 8, 3, 2, 4, 4, 4, 3, 4, 5, 6, 3, 4, 5, 5, 3, 3, 4, 3, 10

There are 4 previous versions of this script.

// ==UserScript==
// @name           nicovideo Add Stars to Tags
// @namespace      http://d.hatena.ne.jp/gifnksm/
// @description    Add stars that represents their uploader set the tags unmodifiable.
// @include        http://www.nicovideo.jp/watch/*
// ==/UserScript==

Array.prototype.include = function(x0) {
  return this.some(function(x) { return x == x0; });
};
function hasClassName(elem, className) {
  return elem.className.split(/\s/).indexOf(className) != -1;
}
Array.prototype.toggle = function(x) {
  var idx = this.indexOf(x);
  if(idx == -1) {
    this.push(x);
    return null;
  }
  var temp = [], i;
  for(i = 0; i < idx; i++)
    temp.unshift(this.shift());
  this.shift();
  for(i = 0, len = temp.length; i < len; i++)
    this.unshift(temp.shift());
  return x;
}

function addClassName(elem, className) {
  if(hasClassName(elem))
    return null;
  else
    elem.className += ' ' + className;
}
function removeClassName(elem, className) {
  elem.className = elem.className.split(/\s/).filter(
    function(exist_name) {
      return exist_name != className;
    }).join(' ');
}
function toggleClassName(elem, className) {
  if(hasClassName(elem, className))
    removeClassName(elem, className);
  else
    addClassName(elem, className);
}

function $exp(exp, ownerDocument) {
  if(!ownerDocument) ownerDocument = document;
  var resolver = document.createNSResolver(ownerDocument);
  var def =
    (document.contentType == 'application/xhtml+xml')
    ? 'http://www.w3.org/1999/xhtml'
    : '';
  return ownerDocument.createExpression(
    exp,
    function(prefix) {
      return resolver.lookupNamespaceURI(prefix) || def;
    });
}

const Single = {};
function $X(exp, context, type /* want type */) {
  if (typeof context == 'function') {
    type    = context;
    context = null;
  }
  if(typeof exp == 'string' || exp instanceof String)
    exp = $exp(exp, context.ownerDocument || context);

  var result, ret, i;
  switch (type) {
  case String:  return exp.evaluate(context, XPathResult.STRING_TYPE,  null).stringValue;
  case Number:  return exp.evaluate(context, XPathResult.NUMBER_TYPE,  null).numberValue;
  case Boolean: return exp.evaluate(context, XPathResult.BOOLEAN_TYPE, null).booleanValue;
  case Single:  return exp.evaluate(context, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
  case Array:
    result = exp.evaluate(context, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
    ret = [];
    for (i = 0, len = result.snapshotLength; i < len; i++)
      ret.push(result.snapshotItem(i));
    return ret;
  case undefined:
    result = exp.evaluate(context, XPathResult.ANY_TYPE, null);
    switch (result.resultType) {
    case XPathResult.STRING_TYPE : return result.stringValue;
    case XPathResult.NUMBER_TYPE : return result.numberValue;
    case XPathResult.BOOLEAN_TYPE: return result.booleanValue;
    case XPathResult.UNORDERED_NODE_ITERATOR_TYPE:
      // not ensure the order.
      ret = [];
      i = null;
      while ((i = result.iterateNext()) != null)
        ret.push(i);
      return ret;
    }
    return null;
  default:
    throw(TypeError('$X: specified type is not valid type.'));
  }
}

function $N(elem, attr, children) {
  if(!elem) return null;
  if(typeof elem == 'string' || elem instanceof String)
    elem = document.createElement(elem);
  else
    elem = elem.cloneNode(!children);
  for (key in attr) {
    if (!attr.hasOwnProperty(key)) continue;
    elem.setAttribute(key, attr[key]);
  }
  function recAppend(child) {
    if(typeof child == 'string' || child instanceof String)
      elem.appendChild(document.createTextNode(child));
    else if(child instanceof Array)
    child.forEach(recAppend);
    else if(child)
    elem.appendChild(child);
  }
  recAppend(children);
  return elem;
}



const video_id = unsafeWindow.Video.id;
const request_url = 'http://www.nicovideo.jp/tag_edit/' + video_id;

const categoryTags = ["公式", "総合", "音楽", "エンターテイメント", "アニメ", "ゲーム", "ラジオ", "スポーツ", "科学", "料理", "政治", "動物", "歴史", "自然", "ニコニコ動画講座", "演奏してみた", "歌ってみた", "踊ってみた", "投稿者コメント", "日記", "アンケート", "チャット", "テスト", "台灣", "その他", "R-18"];
const lockedTags = unsafeWindow.Video.lockedTags;
const initialTags = unsafeWindow.Video.tags;

GM_addStyle(
<><![CDATA[
._GM_tag_link_selected_and {
    -moz-outline: 2px solid red;
    -moz-outline-radius: 5px;
}
._GM_tag_link_selected_minus {
    -moz-outline: 2px solid blue;
    -moz-outline-radius: 5px;
}
]]></>);

var TagData = function(tagname) {
  this.name = tagname;
  this.is_locked = lockedTags.include(tagname);
  this.is_category = categoryTags.include(tagname);
};
TagData.create = function(tagname) { return new TagData(tagname);};

TagData.prototype = {
  get mark() {
    if(typeof this._mark != "undefined")
      return this._mark;
    if(this.is_locked) {
      this._mark =  $N('span', {style: 'color:#F90;'}, '★');
      if(this.is_category)
        return this._mark = $N('strong', {style: 'color:#F30;'}, ['[', this._mark, ']']);
      return this._mark;
    }
    if(this.is_category)
      return this._mark = $N('strong', {style: 'color:#F30;'}, '[C]');
    return this._mark = null;
  },
  get link() {
    if(typeof this._link != "undefined")
      return this._link;
    // 以下はinitialTags やサーバから受信したデータから Tags を作った場合とかに必要となる。
    if(!this.link_exp)
      this.link_exp = $exp('//a[@rel = "tag" and text() = "' + this.name + '"]');
    this._link = $X(this.link_exp, Tags.container, Single);
    return this._link;
  },
  set link(link) { this._link = link; return this._link; },
  decorateLink: function() {
    if(this.link == null)
      return;
    if(this.mark != null)
      this.link.parentNode.insertBefore(this.mark, this.link);
  },
  toString: function() {
    return (this.is_locked
            ? (this.is_category ? '[★]' :' ★ ')
            : (this.is_category ? '[C]' : '    ')
           ) + this.name;
  }
};

var selectedAndTags = [];
var selectedMinusTags = [];

document.addEventListener(
  'mousedown',
  function(e) {
    var link = e.target;
    if(link.nodeName != 'A' || link.rel != 'tag')
      return;
    if(e.altKey && e.ctrlKey && e.shiftKey) {
      e.preventDefault();
      return;
    }
  },
  false);
document.addEventListener(
  'click',
  function(e) {
    var link = e.target;
    if(link.nodeName != 'A' || link.rel != 'tag')
      return;
    var tagname = link.href.replace(new RegExp("^http://www.nicovideo.jp/tag/"), "");
    if(e.altKey && e.ctrlKey && e.shiftKey) {
      removeClassName(link, '_GM_tag_link_selected_and');
      toggleClassName(link, '_GM_tag_link_selected_minus');
      selectedAndTags = selectedAndTags.filter(function(x) { return x != tagname; });
      selectedMinusTags.toggle(tagname);
      e.preventDefault();
      GM_log('\n +: ' + selectedAndTags.map(decodeURI) + '\n -: ' + selectedMinusTags.map(decodeURI));
      return;
    }
    if(e.altKey && e.ctrlKey) {
      toggleClassName(link, '_GM_tag_link_selected_and');
      removeClassName(link, '_GM_tag_link_selected_minus');
      selectedMinusTags = selectedMinusTags.filter(function(x) { return x != tagname; });
      selectedAndTags.toggle(tagname);
      e.preventDefault();
      GM_log('\n +: ' + selectedAndTags.map(decodeURI) + '\n -: ' + selectedMinusTags.map(decodeURI));
      return;
    }
    if(selectedAndTags.length == 0 && selectedMinusTags.length == 0)
      return;
    var addr = selectedAndTags.join('+');
    if(!selectedAndTags.include(tagname) && !selectedMinusTags.include(tagname))
      addr += '+' + tagname;
    addr = [addr].concat(selectedMinusTags).join(encodeURI(' -'));
    var oldhref = link.href;
    link.href = 'http://www.nicovideo.jp/tag/' + addr;
    setTimeout(function() {
                 link.href = oldhref;
               }, 500);
  }, false);



//Tags = initialTags.map(TagData.create);
Tags = [];
Tags.reset = function() { this.length = 0; };
Tags.update = function() {
  var len, j = 0, old_len = Tags.length;
  var links = $X('//a[@rel = "tag"]/text()', Tags.container, Array);
  if(links.length == 0) {
    Tags.length = 0;
    return;
  }
  links.forEach(
    function(text, i) {
      // 新しく★を生成したりするのはコスト高そうなので、できるだけ再利用。
      var tagname = text.nodeValue;
      while(j < old_len && tagname != Tags[j].name)
        j++;
      if(j >= old_len)
        Tags[i] = new TagData(tagname);
      else {
        Tags[i] = Tags[j];
        j++;
      }
      Tags[i].link = text.parentNode;
      len = i + 1;
    });
  Tags.length = len;
};

function appendStars(force_update) {
  var div = document.getElementById("video_tags");
  var p = $X('child::p', div, Single);
  if(p != null) {
    Tags.container = p;
    Tags.update();
    Tags.forEach(function(tag) { tag.decorateLink(); });
    var strong = document.createElement('strong');
    strong.appendChild(document.createTextNode('全' + Tags.length + '件'));
    p.insertBefore(strong, p.firstChild);
  }
  else
    p = div.getElementsByTagName('p')[0]; // タグを更新できなかったとき。(混雑しています、など)
  p.appendChild(document.createTextNode(' '));
  var reload_link = $N('a', {style: 'color: #F30;', href: 'javascript: void(0);'}, '【更新】');
  reload_link.addEventListener('click', refreshTagLinks, false);
  p.appendChild(reload_link);
}

var isGlobal = false;

function refreshTagLinks(callback, isAll) {
  isAll = isAll || isGlobal;
  var video_tags = document.getElementById('video_tags');
  video_tags.innerHTML = '<img src="img/watch/tool_loading.gif" alt="処理中">';
  selectedAndTags = [];
  selectedMinusTags = [];
  GM_xmlhttpRequest(
    {
      method: 'POST',
      url: request_url,
      headers: {
        'X-Requested-With': 'XMLHttpRequest',
        'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8'
      },
      data: 'cmd=tags' + (isAll ? '&all=1' : ''),
      onload: function(response) {
        video_tags.innerHTML = response.responseText;
        if(callback instanceof Function)
          callback();
        if (typeof unsafeWindow.Nicopedia != "undefined")
          unsafeWindow.Nicopedia.decorateLinks();
        appendStars();
      }
    });;
}

unsafeWindow.finishTagEdit = function(url, isAll) {
  isGlobal = isAll;
  var edit_form = document.getElementById('tag_edit_form');
  if(edit_form)
    edit_form.innerHTML = '<img src="img/watch/tool_loading.gif" alt="処理中">';
  setTimeout(
    function() {
      refreshTagLinks(
        function() {
          document.getElementById('video_controls').style.display = '';
          if(edit_form)
            edit_form.parentNode.removeChild(edit_form);
        },
        isAll);},
    10);
};

unsafeWindow.showGlobalTags = function(url) {
  unsafeWindow.finishTagEdit(url, true);
};

appendStars(false);

if(!unsafeWindow.User.isPremium)
  GM_addStyle('.nicopedia_dic { display: inline; }');

// unsafeWindow.showGlobalTags('', true);