nicovideo Add Stars to Tags

By gifnksm Last update Aug 27, 2008 — Installed 785 times.

There are 2 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);

    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:
        var result = exp.evaluate(context, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
        var ret = [];
        for (var i = 0, len = result.snapshotLength; i < len; i++)
            ret.push(result.snapshotItem(i));
        return ret;
    case undefined:
        var 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.
            var ret = [];
            var i = null;
            while (i = result.iterateNext())
                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('//p[contains(concat(" ", @class," "), " tag_txt ")]', div, Single);
    if(p != null) {
        Tags.container = p;
        Tags.update();
        Tags.forEach(function(tag) { tag.decorateLink(); });
        var national_len = unsafeWindow.Video.tags.length;
        var global_len = Tags.length - national_len;
        div.getElementsByTagName('strong')[0].appendChild(
            document.createTextNode(national_len + ((global_len > 0) ? ' + ' + global_len : '') + '件'));
    }
    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);