nicovideo Thumbinfo popup

By gifnksm Last update Sep 10, 2008 — Installed 1,344 times.

There are 3 previous versions of this script.

Add Syntax Highlighting (this will take a few seconds, probably freezing your browser while it works)

// ==UserScript==
// @name           nicovideo Thumbinfo popup
// @namespace      http://d.hatena.ne.jp/gifnksm/
// @description    Get information about nicovideo movies before going to watch page.
// @include        *
// @exclude        http://ext.nicovideo.jp/thumb/*
// @exclude        http://ext.nicovideo.jp/thumb_mylist/*
// @exclude        http://ichiba.nicovideo.jp/parts/*
// ==/UserScript==

// 設定とか

// ポップアップ表示されるまでの遅延時間(ミリ秒)
const show_delay = 600;
// ポップアップが消えるまでの遅延時間(ミリ秒)
const hide_delay = 600;
// 選択範囲内に動画IDが含まれる時にポップアップを行うかどうか
const enable_selection_popup = true;
// iframeでもポップアップを行うかどうか
const enable_iframe_popup = true;
// 動画再生ページのsmilevideoへのリンクでポップアップを行うかどうか
const enable_smilevideo_popup = true;
// はてなブックマーク数を表示するかどうか
const show_hatena_bookmark = true;
// 投稿者名を表示するかどうか
const show_uploader_name = true;
// 動画のIDのプレフィックス
const video_id_prefix = 'sm|nm|fz|ca|za|zb|zc|zd|ze|ax|nl|yk|om|yo|ig|na|fx|cw|sk';

// 以下,スクリプト本体


const DEBUG = false;
const is_watchpage = location.href.indexOf('http://www.nicovideo.jp/watch/') == 0;


var id_func = function(e) { return e; }
var $A = function(array) { return Array.map(array, id_func); };

Function.prototype.bind = function() {
    var self = this;
    var obj = Array.shift(arguments);
    var args = $A(arguments);
    return function() {
        return self.apply(obj, args.concat($A(arguments)));
    }
};

Number.prototype.fill = function(order) {
    var s = this.toString();
    while(s.length < order)
        s = '0' + s;
    return s;
};
String.prototype.insertComma = new function() {
    var regexp = /(\d{1,3})(?=(?:\d\d\d)+$)/g;
    return function() {
        return this.toString().replace(regexp, "$1,");
    };
};

// innerHTML -> range

String.prototype.encodeEntityReference = new function() {
    var div = document.createElement('div');
    return function() {
        div.textContent = this;
        return div.innerHTML;
    }
}
String.prototype.decodeEntityReference = new function() {
    var span = document.createElement('span');
    return function() {
        span.innerHTML = this;
        if(span.firstChild == null)
            return '';
        return span.firstChild.nodeValue;
    };
};

// 戻り値を innerHTML -> documentFragmentにする
String.prototype.createLink = new function() {
    const domain_strs = '[-_.!~*\'()a-zA-Z0-9;?:@&=+$,%#]';
    const url_strs = '[-_.!~*\'()a-zA-Z0-9;/?:@&=+$,%#]';
    const url_regexp = new RegExp(
    '(^|[^a-z])' +
    '(?:'+ 
        '((?:'+video_id_prefix+')\\d+)|' +
        '((?:co)\\d+)|' + 
        '((?:nc)\\d+)|' + 
        '((?:mylist|myvideo|user|watch)/\\d+(?:/\\d+)?)|' +
        '((?:h?t?t?ps?|ftp)(?:://(' + domain_strs + '+)/?' + url_strs + '*))' +
    ')', 'g');
    const func = function(text, pre, video_id, comu_id, commons_id, mylist, url, domain) {
        if(video_id)
            return pre + '<a href="http://www.nicovideo.jp/watch/' + video_id + '">' + video_id + '</a>';
        if(comu_id)
            return pre + '<a href="http://com.nicovideo.jp/community/' + comu_id + '">' + comu_id + '</a>';
        if(commons_id)
            return pre + '<a href="http://www.niconicommons.jp/material/' + commons_id + '">' + commons_id + '</a>';
        if(mylist)
            return pre + '<a href="http://www.nicovideo.jp/' + mylist + '">' + mylist + '</a>';
        if(url) {
            if(url.indexOf('p') == 0)
                url = 'htt' + url;
            else if(url.indexOf('tp') == 0)
                url = 'ht' + url;
            else if(url.indexOf('ttp') == 0)
                url = 'h' + url;
            return pre + '<a href="' + url + '" title="' + url + '">[' + domain + ']</a>';
        }
    };
    return function() {
        return this.replace(url_regexp, func);
    }
}


Date.prototype.toJpString = function() {
    return this.getFullYear() + '年' +  (this.getMonth()+1).fill(2) + '月' +  this.getDate().fill(2) + '日 ' +
        [this.getHours().fill(2), this.getMinutes().fill(2), this.getSeconds().fill(2)].join(':');
};

Date.fromISO8601 = function(str) {
    var date = new Date();
    date.setISO8601(str);
    return date;
};
Date.prototype.setISO8601 = new function() {
    var regstr = "^([0-9]{4})(?:-([0-9]{2})(?:-([0-9]{2})" +
                 "(?:T([0-9]{2}):([0-9]{2})(?::([0-9]{2})(?:\.([0-9]+))?)?" +
                 "(?:Z|(?:([-+])([0-9]{2}):([0-9]{2})))?)?)?)?$";
    var regexp = new RegExp(regstr);
    return function (string) {
        var d = string.match(regexp);
        if(d == null)
            return;
        var offset = 0;
        var date = new Date(d[1], 0, 1);
        
        if (d[2]) { date.setMonth(d[2] - 1); }
        if (d[3]) { date.setDate(d[3]); }
        if (d[4]) { date.setHours(d[4]); }
        if (d[5]) { date.setMinutes(d[5]); }
        if (d[6]) { date.setSeconds(d[6]); }
        if (d[7]) { date.setMilliseconds(Number("0." + d[7]) * 1000); }
        if (d[8]) {
            offset = (Number(d[9]) * 60) + Number(d[10]);
            offset *= ((d[8] == '-') ? 1 : -1);
        }
        
        offset -= date.getTimezoneOffset();
        time = (Number(date) + (offset * 60 * 1000));
        
        this.setTime(Number(time));
    }
};

function hasClassName(elem, className) {
    return (' ' + elem.className + ' ').indexOf(' ' + className + ' ') != -1;
}
function addClassName(elem, className) {
    if(hasClassName(elem, className))
        return;
    elem.className += ' ' + className;
}
function removeClassName(elem, className) {
    if(!hasClassName(elem, className))
        return;
    elem.className = (' ' + elem.className + ' ').replace(' ' + className + ' ', ' ');
}
function toggleClassName(elem, className, flag) {
    if(typeof flag == 'undefined')
        flag = !hasClassName(elem, className);
    if(flag)
        addClassName(elem, className);
    else
        removeClassName(elem, className);
}


function getPosition(elem) {
    var top = 0;
    var left = 0;
    var current = elem;
    while(current && current != document.body) {
        top += current.offsetTop - current.scrollTop;
        left += current.offsetLeft - current.scrollLeft;
        current = current.offsetParent;
    }
    var bottom = top + elem.clientHeight;
    var right = left +  elem.clientWidth;
    if(elem != null) {
        Array.forEach(elem.childNodes, function(current) {
            var current_offset = getPosition(current);
            if(current_offset.top < top)
                top = current_offset.top;
            if(current_offset.left < left)
                left = current_offset.left;
            if(current_offset.bottom > bottom)
                bottom = current_offset.bottom;
            if(current_offset.right > right)
                right = current_offset.right;
        });
    }
    return {top: top, left: left, bottom: bottom, right: right,
            height: elem.clientHeight, width: elem.clientWidth};
}

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 classname_prefix = '_GM_nicovideo_thumbinfo_popup_'   ;
const shown_link_classname = classname_prefix + 'shown';
const base_classname = classname_prefix + 'base';
const filled_popup_classname = classname_prefix + 'filled';
const fixed_popup_classname = classname_prefix + 'fixed';
const thumbnail_classname = classname_prefix + 'thumbnail';
const hatena_classname = classname_prefix + 'hatena';
const uploader_classname = classname_prefix + 'uploader';
const tags_classname = classname_prefix + 'tags';
const description_classname = classname_prefix + 'description';
const buttons_container_classname = classname_prefix + 'buttons_container';
const closebutton_classname = classname_prefix + 'closebutton';
const movebutton_classname = classname_prefix + 'movebutton';
const resizebutton_classname = classname_prefix + 'resizebutton';
const disable_popup_link_classname = classname_prefix + 'disable_popup_link';

GM_addStyle(<><![CDATA[
    .${created} {
        cursor: progress;
    }
    .${created}:focus {
        outline: 2px solid red;
        -moz-outline-radius: 5px;
    }
    .${created}.${shown} {
        cursor: pointer;
    }
    .${created}.${shown}:focus {
        outline-color: blue;
    }
    ${base} {
        -moz-border-radius: 5px;
        position: absolute;
        overflow: auto;
        border: 1px solid gray;
        background-color: white;
        color: black;
        text-align: left;
        font-size: 11px;
        padding: 8px;
        margin: 0;
        font-family: none;
        z-index: 100000;
    }
    ${base} * {
        font-family: none;
        margin: 0;
        padding: 0;
        border: none;
        text-indent: 0;
        text-align: left;
        background: none;
        background-color: transparent;
        color: black;
        width: auto;
        height: auto;
        max-width: auto;
        max-height: auto;
        min-width: auto;
        min-height: auto;
        line-height: 1.5;
        float: none;
        clear: none;
        -moz-box-sizing: content-box;
        position: static;
    }
    ${base} strong {
        font-weight: bold;
        font-size: inherit;
    }
    ${base} a         {
        text-decoration: underline;
        font-size: inherit;
    }
    ${base} a:link    { color: blue; }
    ${base} a:visited { color: #135; }
    ${base} a:hover,
    ${base} a:active {
        color: red;
        text-decoration: none;
    }
    ${base}.${filled} {
        width: 600px;
    }
    ${base}.${fixed} {
        border: 2px solid black;
        margin: -1px;
        background-color: white;
        color: black;
        text-align: left;
    }
    ${base} img.${thumbnail} {
        float: left;
        width: 130px;
        height: 100px;
        margin: 0 5px 5px 0;
    }
    ${base} h1 {
        font-size: 14px;
        line-height: 22px;
        font-weight: bold;
    }
    ${base} img.${hatena} {
        vertical-align: middle;
    }
    ${base} > p.${tags} {
        border: 1px solid silver;
        border-width: 1px 0;
        margin: 3px 0 3px 0;
        padding: 3px;
        word-spacing: 3px;
        line-height: 1.7;
        background-color: #eee;
    }
    ${base} a.${uploader} {
        font-weight: bold;
    }
    ${base} > p.${tags} a {
        white-space: nowrap;
    }
    ${base} > p.${tags} a:link,
    ${base} a.${uploader}:link {
        color: #222222;
    }
    ${base} > p.${tags} a:visited,
    ${base} a.${uploader}:visited {
        color: #444444;
    }
    ${base} > p.${tags} a:hover, ${base} > p.${tags} a:active,
    ${base} a.${uploader}:hover, ${base} a.${uploader}:active {
        color: #666666;
    }
    
    ${base} > p.${description} {
        clear: left;
        padding: 0 5px;
        line-height: 1.7;
        max-height: 150px;
        overflow-y: auto;
    }
    ${base} > p.${description} a { font-weight: bolder; }
    ${base} > p.${buttons_container} {
        position: absolute;
        top: 0;
        right: 0;
        margin: 0;
        padding: 0;
        color: white;
    }
    ${base} > p.${buttons_container} > span {
        display: block;
        width: 16px;
        height: 16px;
        font-size: 15px;
        font-weight: bold;
        line-height: 16px;
        margin: 0;
        padding: 0;
        text-align: center;
        border: 1px solid gray;
        border-width: 0 0 1px 1px;
        background-color: white;
        color: bkack;
    }
    ${base}.${fixed} > p.${buttons_container} >span {
        border: 2px solid black;
        border-width: 0 0 2px 2px;
    }
    ${base} span.${closebutton} {
        -moz-border-radius-topright: 2px;
        -moz-border-radius-bottomleft: 5px;
        cursor: pointer;
    }
    ${base} span.${closebutton}:hover {
        background-color: red;
        color: white;
    }
]]></>.toString().replace(/\${([^}]+)}/g, function(_, name) {
    if(name == 'base')
        return 'body > div.' + base_classname;
    else
        return classname_prefix + name;
}));

function log() {
    if(!DEBUG)
        return;
    if(unsafeWindow.console)
        unsafeWindow.console.log.apply(unsafeWindow.console, arguments);
    else
        Array.forEach(arguments, GM_log);
}

var InfoGetter = function(url, converter) {
    this.callbacks = [];
    this.url = url;
    this.converter = converter || function(x, callback) { callback(x); };
};
InfoGetter.prototype = {
    loaded: false,
    loading: false,
    response: null,
    data: null,
    reset: function() {
        this.callbacks = [];
        this.loaded = false;
        this.loading = false;
        this.response = null;
        this.data = null;
    },
    get: function(callback) {
        if(typeof callback == 'function')
            this.callbacks.push(callback);
        if(this.loading)
            return;
        if(this.loaded) {
            this.call_callbacks();
            return;
        }
        this.loading = true;
        GM_xmlhttpRequest({
            method: 'GET',
            url: this.url,
            headers: { 'User-Agent': 'Mozilla/5.0 Greasemonkey; nicovideo Thumbinfo popup' },
            onload: (function(response) {
                if(!this.loading)
                    return;
                log('loaded: ', this.url, response);
                this.response = response;
                this.converter(response, (function(converted) {
                    log('converted: ', converted);
                    this.data = converted;
                    this.call_callbacks();
                    this.loading = false;
                    this.loaded = true;
                }).bind(this));
            }).bind(this)
        });
    },
    call_callbacks: function() {
        var callback;
        while(callback = this.callbacks.shift()) {
            callback(this.data);
        }
    }
};


var ThumbInfo = function(video_id) {
    this.video_id = video_id;
    this.watch_url = 'http://www.nicovideo.jp/watch/' + video_id;
    this.thumbnail_url = 'http://tn-skr.smilevideo.jp/smile?i=' + video_id.slice(2, this.video_id.length);
    this.thumbinfo_getter = new InfoGetter(
        'http://www.nicovideo.jp/api/getthumbinfo/' + video_id,
        ThumbInfo.responseConverter.bind(this));
    this.uploader_getter = new InfoGetter(
        'http://www.smilevideo.jp/view/' + video_id.replace(/^[a-z]{2}/, ''),
        function(response, callback) {
            log('convert name: ', response, callback);
            if(/<strong>([^<]+?)<\/strong> が投稿した動画を/.test(response.responseText))
                return callback(RegExp.$1.decodeEntityReference());
            else
                return callback(undefined);
        }
    );
};

ThumbInfo.data = {};
ThumbInfo.get = function(video_id) {
    if(typeof this.data[video_id] == 'undefined')
        this.data[video_id] = new ThumbInfo(video_id);
    return this.data[video_id];
}
ThumbInfo.parser = new DOMParser();
ThumbInfo.createErrorMessage = function(fragment, message, thumbnail_url) {
    var fragment = document.createDocumentFragment();
    fragment.appendChild($N('img', { src: thumbnail_url, 'class': thumbnail_classname, alt:''}));
    fragment.appendChild($N('h1', {}, message));
    return fragment;
};
ThumbInfo.responseConverter = function(response, callback) {
    if(response.responseText == '') {
        callback(ThumbInfo.createErrorMessage(fragment, 'メンテナンス中かサーバが落ちています。', this.thumbnail_url));
        return;
    }
    
    var doc = ThumbInfo.parser.parseFromString(response.responseText, 'text/xml');
    this.status = doc.documentElement.getAttribute('status');
    if(this.status != 'ok') {
        log('response(error):', response.responseText);
        var code = doc.getElementsByTagName('code')[0].textContent;
        this.code = code;
        var fragment = ThumbInfo.createErrorMessage(fragment, 'Error! (' + code + ')', this.thumbnail_url);
        fragment.appendChild($N('p', {}, [
            $N('strong', {}, 'description'), ': ', doc.getElementsByTagName('description')[0].textContent,
            ' (', $N('a', {href: this.watch_url}, this.video_id), ').'
        ]));
        if(code == 'NOT_FOUND' && this.video_id.length > 3) {
            var new_ids = [];
            var i = -1;
            do {
                new_ids.push(this.video_id.slice(0, i));
                i--;
            } while(this.video_id.length + i > 2);
            fragment.appendChild(
                $N('p', {}, ['もしかして:', new_ids.map(function(video_id) {
                    return [' ', $N('a', {href: 'http://www.nicovideo.jp/watch/' + video_id}, video_id)];
                })])
            );
        }
        if(code != 'COMMUNITY') {
            callback(fragment);
            return;
        }
        var flv_getter = new InfoGetter('http://www.nicovideo.jp/api/getflv/' + this.video_id);
        var self = this;
        flv_getter.get(function(response) {
            var data = {};
            response.responseText.split('&').forEach(function(pair) {
                var [key, val] = pair.split('=');
                key = decodeURIComponent(key);
                val = decodeURIComponent(val);
                data[key] = val;
            });
            if(data.hasOwnProperty('error')) {
                var message;
                switch(data.error) {
                case 'invalid_v1':
                    message = '削除済み、または観覧する権限がありません。';
                    break;
                case 'invalid_v2':
                    message = '非表示にされています。';
                    break;
                case 'invalid_v3':
                    message = '権利者削除されています。';
                    break;
                case 'cant_get_detail':
                    message = '削除されています。(詳細不明)';
                    break;
                default:
                    message = '詳細不明なエラーです。';
                }
                fragment.appendChild($N('p', {}, message));
                callback(fragment);
                return;
            }
            if(/smile\?(.)=(\d+)/.test(data.url)) {
                var type = RegExp.$1;
                var postfix = RegExp.$2;
                switch(type) {
                case 'm':
                case 'v':
                    type = 'sm';
                    break;
                case 's':
                    type = 'nm';
                    break;
                default:
                    callback(fragment);
                    return;
                }
                var video_id = type + postfix;
                self.real_id = video_id;
                var thumbinfo = ThumbInfo.get(video_id);
                thumbinfo.getData(function(orig_fragment) {
                    var fragment = orig_fragment.cloneNode(true);
                    self.status = thumbinfo.status;
                    if(thumbinfo.status == 'ok') {
                        if(fragment.firstChild.nodeName == 'A') {
                            fragment.firstChild.href = 'http://www.nicovideo.jp/watch/' + self.video_id;
                        }
                        var p, h1;
                        Array.some(fragment.childNodes, function(x) {
                            if(x.nodeName == 'P')
                                p = x;
                            if(x.nodeName == 'H1') {
                                h1 = x;
                                return true;
                            }
                        });
                        if(p) {
                            var comumark = document.createDocumentFragment();
                            comumark.appendChild(document.createTextNode(' '));
                            comumark.appendChild($N('strong', {}, 'コミュニティ'));
                            comumark.appendChild(document.createTextNode(' '));
                            comumark.appendChild($N('a', {href: 'http://www.nicovideo.jp/watch/' + video_id}, '\u00bb元動画'));
                            comumark.appendChild(document.createTextNode(' '));
                            p.insertBefore(comumark, p.firstChild.nextSibling);
                        }
                        if(h1.getElementsByTagName('a').length)
                            h1.getElementsByTagName('a')[0].href = 'http://www.nicovideo.jp/watch/' + self.video_id;
                    }
                    else if(thumbinfo.code == 'NOT_FOUND') {
                        var new_ids = video_id_prefix.split('|').filter(function(prefix) {
                            return prefix != type;
                        });
                        fragment.appendChild(
                            $N('p', {}, ['もしかして:', new_ids.map(function(prefix) {
                                return [' ', $N('a', {href: 'http://www.nicovideo.jp/watch/' + prefix + postfix}, prefix + postfix)];
                            })])
                        );
                    }
                    callback(fragment);
                });
                return;
            }
            callback(fragment);
        });
        return;
    }
    var fragment = document.createDocumentFragment();
    var data = {};
    var tags_data = {jp: [], tw: [], es: [], de: []};
    Array.forEach(doc.getElementsByTagName('thumb')[0].childNodes, function(node) {
        if(node.nodeType != 1)
            return;
        var name = node.nodeName;
        if(name != 'tags')
            data[name] = node.textContent;
        else {
            var domain = node.getAttribute('domain');
            tags_data[domain] = Array.map(node.getElementsByTagName('tag'), function(tag) {
                return {name: tag.textContent, locked: tag.hasAttribute('lock')};
            });
        }
    });
    log('data: ', data);
    
    this.watch_url = data.watch_url;
    this.thumbnail_url = data.thumbnail_url;
    
    var additional = [];
    if(data.thumb_type == 'mymemory') {
        this.real_id = data.video_id;
        additional.push([' ', $N('strong', {}, 'マイメモリー'),
            ' ', $N('a', {href: 'http://www.nicovideo.jp/watch/' + data.video_id}, '\u00bb元動画')]);
    }
    if(show_uploader_name)
        additional.push([' [up: ', $N('span', {'class': uploader_classname}, 'Loading...'), ']']);
    if(show_hatena_bookmark)
        additional.push([' ', $N('a', {href: 'http://b.hatena.ne.jp/entry/' + data.watch_url},
            $N('img', {src: 'http://b.hatena.ne.jp/entry/image/' + data.watch_url, 'class': hatena_classname})
        )]);
    
    fragment.appendChild(
        $N('a', {href: data.watch_url, 'class': disable_popup_link_classname},
            $N('img', {src: data.thumbnail_url, 'class': thumbnail_classname, alt:''})
         )
    );
    fragment.appendChild($N('p', {}, [Date.fromISO8601(data.first_retrieve).toJpString(), '投稿', additional]));
    fragment.appendChild($N('h1', {},
        $N('a', {href: data.watch_url, 'class': disable_popup_link_classname}, data.title)
    ));
    fragment.appendChild($N('p', {}, [
        '再生時間: ',    $N('strong', {}, data.length.split(':').join('分') + '秒'),
        ' 再生: ',       $N('strong', {}, data.view_counter.insertComma()),
        ' コメント: ',   $N('strong', {}, data.comment_num.insertComma()),
        ' マイリスト: ', $N('strong', {}, data.mylist_counter.insertComma())
    ]));
    var global_length = tags_data['tw'].length + tags_data['es'].length + tags_data['de'].length;
    var make_tag = function(code) {
        if(tags_data[code].length == '0')
            return '';
        return $N('span', {}, [
            (code != 'jp') ? [' ', $N('strong', {}, '[' + code + ']:')] : '',
            tags_data[code].map(function(tag) {
                return [' ',
                    tag.locked? $N('span', {style: 'color:#F90;'}, '★') : '' ,
                    $N('a', {href: 'http://www.nicovideo.jp/tag/'+encodeURI(tag.name), rel: 'tag'},
                        tag.name.decodeEntityReference())];
            })
        ]);
    };
    fragment.appendChild($N('p', {'class': tags_classname}, [
        $N('strong', {}, 'タグ(' + tags_data['jp'].length + (global_length > 0 ? ' + ' + global_length : '') + '):'),
        make_tag('jp'), make_tag('tw'), make_tag('de'), make_tag('es')
    ]));
    var p = $N('p', {'class': description_classname});
    p.innerHTML = data.description.createLink().replace(/\s{3,}/g, '<br/>');
    fragment.appendChild(p);
    callback(fragment);
    return;
};
ThumbInfo.prototype = {
    loading: false,
    uploader_name: null,
    get data_loaded() {
        return this.thumbinfo_getter.loaded;
    },
    getData: function(callback) {
        if(show_uploader_name)
            this.uploader_getter.get();
        this.thumbinfo_getter.get(callback);
    },
    getUploaderName: function(callback) {
        this.uploader_getter.get(callback);
    }
}


const popup_parent_classname = '_GM_nicovideo_thumbinfo_popup_parent_';
var popups = [];

var Popup = function(elem, video_id) {
    this.link_elem = elem;
    this.video_id = video_id;
    this.uniqueID = popups.length;
    this.thumbinfo = ThumbInfo.get(video_id);
    this.parent = null;
    if(elem != null) {
        var self = this;
        // elem.addEventListener('focus', this.showDelay.bind(this, false), false);
        elem.addEventListener('mouseover', function() { self.showDelay(); }, false);
        // elem.addEventListener('blur', this.hideDelay.bind(this, true), false);
        elem.addEventListener('mouseout', function() { self.hideDelay(); }, false);
        // なぜか動作しない
        // elem.addEventListener('DOMNodeRemovedFromDocument', this.hideDelay.bind(this, true), false);
        document.addEventListener('DOMNodeRemoved', function(e) {
            if(e.target.compareDocumentPosition(elem) & 16)
                self.hideDelay(hide_delay * 2);
        }, false);
        if((new RegExp(' ' + popup_parent_classname + '(\\d+) ').test(' ' + elem.className + ' ')))
            this.parent =  popups[parseInt(RegExp.$1, 10)];
    }
    popups.push(this);
}

Popup.prototype = {
    mouseover: false,
    visible: false,
    _popup_div: null,
    disableAutohide: false,
    _addMouseEvents: function() {
        var div = this._popup_div;
        var self = this;
        div.addEventListener('dblclick', function() {
            self.fixed = !self.fixed;
            self.disableAutohide = self.fixed;
            toggleClassName(div, fixed_popup_classname, self.fixed);
        }, false);
        var forEachParent = function(f) { return function(e) {
            var x = self; do { f(x); } while(x = x.parent);
        }};
        var over_time;
        div.addEventListener('mouseover',
            forEachParent(function(popup) {
                popup.disableAutohide = true;
                over_time = new Date();
            }), false);
        div.addEventListener('mouseout', forEachParent(function(popup) {
            popup.disableAutohide = false;
            // 意図せぬマウスの通過は無視。
            if(new Date() - over_time > 50)
                popup.hideDelay();
        }), false);
        div.addEventListener('click', function(e) {
            if(!hasClassName(e.target, closebutton_classname))
                document.getElementsByTagName('body')[0].appendChild(div);
        }, false);
    },
    _addMoveEvents: function() {
        var div = this._popup_div, orig_top, orig_left;
        var self = this;
        var mousemove = function(e) {
            e.preventDefault();
            div.style.top = (e.pageY + orig_top) + 'px';
            div.style.left = (e.pageX + orig_left) + 'px';
        };
        div.addEventListener('mousedown', function(e) {
            if(!e.ctrlKey) return;
            self.moved = true;
            orig_top = parseInt(div.style.top, 10) - e.pageY;
            orig_left = parseInt(div.style.left, 10) - e.pageX;
            e.preventDefault();
            document.addEventListener('mousemove', mousemove, false);
        }, false);
        document.addEventListener('mouseup', function(e) {
            e.preventDefault();
            document.removeEventListener('mousemove', mousemove, false);
        }, false);
    },
    _createButton: function() {
        var buttons_container = document.createElement('p');
        addClassName(buttons_container, buttons_container_classname);
        
        var closebutton = document.createElement('span');
        addClassName(closebutton, closebutton_classname);
        closebutton.textContent = '×';
        closebutton.addEventListener('click', this.hide.bind(this, false), false);
        
        buttons_container.appendChild(closebutton);
        this._popup_div.appendChild(buttons_container);
    },
    _createPopup: function() {
        var div = document.createElement('div');
        this._popup_div = div;
        addClassName(div, base_classname);
        if(is_watchpage)
            div.style.position = 'fixed';
        
        var message = document.createElement('div');
        message.appendChild($N('img', { src: this.thumbinfo.thumbnail_url, 'class': thumbnail_classname, alt:''}));
        message.appendChild($N('h1', {}, 'loading...'));
        div.appendChild(message);
        this._message_div = message;
        
        this._addMouseEvents();
        this._addMoveEvents();
        this._createButton();
    },
    _fillData: function(fragment) {
        var div = this.popup_div;
        div.replaceChild(fragment.cloneNode(true), this._message_div);
        if(show_uploader_name) {
            var name_elem;
            Array.some(div.getElementsByTagName('span'), function(span) {
                if(hasClassName(span, uploader_classname)) {
                    name_elem = span;
                    return true;
                }
            });
            if(typeof name_elem != 'undefined') {
                var parent = name_elem.parentNode;
                if(/^\d/.test(this.video_id) && this.thumbinfo.real_id) {
                    this.thumbinfo.uploader_getter = new InfoGetter('http://www.smilevideo.jp/view/' +
                        this.thumbinfo.real_id.slice(2, this.thumbinfo.real_id.length),
                        this.thumbinfo.uploader_getter.converter);
                }
                log(this.thumbinfo.uploader_getter);
                this.thumbinfo.getUploaderName(function(name) {
                    if(typeof name == 'undefined')
                        name_elem.textContent = 'Not Found.'
                    else
                        parent.replaceChild(
                            $N('a', {href: 'http://www.nicochart.jp/name/' + encodeURI(name),
                                'class': uploader_classname}, name),
                            name_elem);
                });
            }
        }
        var self = this;
        var imgs = div.getElementsByTagName('img');
        if(imgs.length) {
            imgs[0].addEventListener('error', function(e) {
                imgs[0].style.width = '0';
                imgs[0].style.height = '0';
                self.appendPopup();
            }, false);
        }
        Array.forEach(div.getElementsByTagName('a'), function(a) {
            addClassName(a, popup_parent_classname + self.uniqueID);
        });
        if(this.thumbinfo.status == 'ok')
            addClassName(div, filled_popup_classname);
        document.getElementsByTagName('body')[0].appendChild(div);
    },
    get popup_div() {
        if(this._popup_div == null)
            this._createPopup();
        return this._popup_div;
    },
    fillData: function(fragment) {
        if(!this.data_filled)
            this._fillData(fragment);
        this.data_filled = true;
        this.appendPopup();
    },
    appendPopup: function() {
        var div = this.popup_div;
        document.body.appendChild(div);
        if(this.moved)
            return;
        if(this.link_elem != null)
            this.move(getPosition(this.link_elem));
        else
            this.move();
    },
    move: function(position) {
        if(typeof position == 'undefined')
            position = this.position;
        this.position = position;
        var div = this.popup_div;
        var top = position.top - div.offsetHeight - 20;
        if(top < document.documentElement.scrollTop + document.body.scrollTop) {
            top = position.bottom + 20;
        }
        var left = position.left;
        if(left < 50)
            left = 50;
        else if(left + div.offsetWidth > window.innerWidth - 50) {
            left = position.right - div.offsetWidth / 2;
            if(left + div.offsetWidth > window.innerWidth - 50) {
                left = position.right - div.offsetWidth;
            }
        }
        if(is_watchpage && this.parent == null) {
            top -= document.documentElement.scrollTop;
            left -= document.documentElement.scrollLeft;
        }
        div.style.top = top + 'px';
        div.style.left = left + 'px';
    },
    showDelay: function(delay) {
        log('showDelay: ', this.video_id);
        if(typeof delay == 'undefined') {
            delay = show_delay;
        }
        this.mouseover = true;
        if(this.hide_timer)
            clearTimeout(this.hide_timer);
        this.show_timer = setTimeout(this.show.bind(this, false), delay);
    },
    hideDelay: function(delay) {
        log('hideDelay: ', this.video_id);
        if(typeof delay == 'undefined')
            delay = hide_delay;
        this.mouseover = false;
        if(this.show_timer)
            clearTimeout(this.hide_timer);
        this.hide_timer = setTimeout(this.hide.bind(this, true), delay);
    },
    show: function(force_show) {
        if(this.visible || (!this.mouseover && !force_show))
            return;
        this.visible = true;
        if(this.link_elem != null)
            addClassName(this.link_elem, shown_link_classname);
        log('show: ', this.video_id, this.uniqueID);
        if(!this.thumbinfo.data_loaded)
            this.appendPopup();
        this.thumbinfo.getData(this.fillData.bind(this));
    },
    hide: function(autohide) {
        if(autohide && (this.disableAutohide || this.fixed))
            return;
        if(this.link_elem != null)
            removeClassName(this.link_elem, shown_link_classname);
        this.visible = false;
        this.mouseover = false;
        this.disableAutohide = false;
        this.fixed = false;
        this.moved = false;
        removeClassName(this.popup_div, fixed_popup_classname);
        log('hide: ', this.video_id, this.uniqueID);
        document.getElementsByTagName('body')[0].removeChild(this.popup_div);
    }
}


const url_regex = new RegExp('^http://www.nicovideo.jp/watch/((?:\\w\\w)?\\d+)');
const url_regex_tag = new RegExp('^http://www.nicovideo.jp/tag/.*?((?:'+video_id_prefix+')\\d+)');
const thumb_regex = new RegExp('^http://(?:www|ext).nicovideo.jp/thumb/((?:\\w\\w)?\\d+)')
const created_propname = classname_prefix + 'created';
var regist_link = function self(e, elem) {
    if(typeof elem == 'undefined')
        elem = e.target;
    if(hasClassName(elem, disable_popup_link_classname))
        return;
    var video_id;
    if(enable_iframe_popup && elem.nodeName == 'IFRAME' && (thumb_regex.test(elem.src))) {
        video_id = RegExp.$1;
    }
    else if((elem.nodeName == 'A' || elem.nodeName == 'AREA') && (url_regex.test(elem.href) || url_regex_tag.test(elem.href))) {
        video_id = RegExp.$1;
    }
    else {
        if(elem.parentNode)
            self(e, elem.parentNode);
        return;
    }
    if(!elem[created_propname]) {
        elem[created_propname] = true;
        var popup = new Popup(elem, video_id);
        log('create: ', video_id, popup.uniqueID);
        popup.showDelay();
    }
}

if(is_watchpage && enable_smilevideo_popup) {
    var h1 = document.getElementsByTagName('h1');
    if(!h1.length)
        return;
    var links = h1[0].getElementsByTagName('a');
    if(!links.length)
        return;
    var link = links[0];
    var video_id = unsafeWindow.Video.id;
    link[created_propname] = true;
    var popup = new Popup(link, video_id);
    log('create: ', video_id, popup.uniqueID);
}

document.addEventListener('mouseover', regist_link, false);
// document.addEventListener('keyup', function(e) { regist_link(e, document.activeElement);}, false);
if(enable_selection_popup) {
    document.addEventListener('mouseup', new function() {
        const url_regexp = new RegExp('(^|[^a-z])((?:'+video_id_prefix+')\\d+)', 'g');
        var old_str;
        return function(e) {
            var selection =  getSelection();
            if(!selection)
                return;
            var str = selection.toString();
            if(old_str == str)
                return;
            old_str = str;
            var p, i = 0;
            while(p = url_regexp.exec(str)) {
                var video_id = p[2];
                var popup = new Popup(null, video_id);
                log('create_from_selection: ', video_id, popup.uniqueID);
                var top = e.pageY + 10 * i;
                var left = e.pageX + 50 * i;
                popup.move({top: top, left: left, bottom: top, right: left, height: 0, width: 0});
                popup.showDelay();
                i++;
            }
        }
    }, false);
}

if(is_watchpage) {
    var sTop = document.documentElement.scrollTop;
    var sLeft = document.documentElement.scrollLeft;
    window.addEventListener('scroll', function(e) {
        var dx = document.documentElement.scrollLeft - sLeft;
        var dy = document.documentElement.scrollTop - sTop;
        popups.forEach(function(popup) {
            popup.popup_div.style.top = (parseInt(popup.popup_div.style.top, 10) - dy) + 'px';
            popup.popup_div.style.left = (parseInt(popup.popup_div.style.left, 10) - dx) + 'px';
        });
        sTop = document.documentElement.scrollTop;
        sLeft = document.documentElement.scrollLeft;
    }, false);
}