Greasy Fork is available in English.

MyAnimeList (MAL) Tags Updater

Adds type, genres and other info to entries tags. Can also delete all current tags.

Από την 31/05/2016. Δείτε την τελευταία έκδοση.

// ==UserScript==
// @name        MyAnimeList (MAL) Tags Updater
// @namespace   https://greasyfork.org/users/7517
// @description Adds type, genres and other info to entries tags. Can also delete all current tags.
// @icon        http://i.imgur.com/b7Fw8oH.png
// @version     5.0
// @author      akarin
// @include     /^http:\/\/myanimelist\.net\/(anime|manga)list\//
// @include     /^http:\/\/myanimelist\.net\/panel\.php\?go=(add|edit)/
// @include     /^http:\/\/myanimelist\.net\/editlist\.php\?type=anime/
// @include     /^http:\/\/myanimelist\.net\/ownlist\/(anime|manga)\/(\d+\/)?(add|edit)/
// @grant       none
// ==/UserScript==

/*jslint fudge, maxerr: 10, browser, devel, this, white, for */
/*global jQuery */

(function($) {
    "use strict";

var T_PAGE = {
    M_LIST: 1, M_POPUP: 2
};

var T_RUN = {
    M_FULL: 1, M_EMPTY: 2, M_CLEAR: 3
};

var mal = {
    version: '5.0', // cache
    ajax: { delay: 300, timeout: 10000 },
    page: document.URL.match(/^http:\/\/myanimelist\.net\/(anime|manga)list\//) ? T_PAGE.M_LIST : T_PAGE.M_POPUP,
    type: '', // anime or manga
    status: '',
    entries: { total: 0, done: 0, fail: 0 },
    content: {
        done: $('<span id="tu_status_done">'),
        fail: $('<span id="tu_status_fail">')
    }
};

$.fn.myfancybox = function(onstart) {
    return $(this).click(function() {
        mal.fancybox.start(onstart);
    });
};

mal.fancybox = {
    body: $('<div id="tu_fancybox_inner">'),
    outer: $('<div id="tu_fancybox_outer">'),
    wrapper: $('<div id="tu_fancybox_wrapper">'),

    init: function(el) {
        mal.fancybox.outer.hide()
            .append(mal.fancybox.body)
            .insertAfter(el);

        mal.fancybox.wrapper.hide()
            .insertAfter(el);

        mal.fancybox.wrapper.click(function() {
            mal.fancybox.close();
        });
    },

    start: function(onstart) {
        mal.fancybox.body.children().hide();
        if (onstart()) {
            mal.fancybox.wrapper.show();
            mal.fancybox.outer.show();
        } else {
            mal.fancybox.close();
        }
    },

    close: function() {
        mal.fancybox.outer.hide();
        mal.fancybox.wrapper.hide();
    }
};

var T_ = {
    TYPE:           1,
    STATUS:         2,
    AIRED:          3,
    PRODUCERS:      4,
    LICENSORS:      5,
    STUDIOS:        6,
    AUTHORS:        7,
    SERIALIZATION:  8,
    GENRES:         9,
    RATING:         11,
    BROADCAST:      12,
    SOURCE:         13,
    SCORE:          14,
    RANK:           15,
    POPULARITY:     16,
    MEMBERS:        17,
    FAVORITES:      18,
    JAPANESE:       19,
    ENGLISH:        20,
    DURATION:       101,
    YEAR:           102,
    SEASON:         103,
    PERIOD:         104,
    RND_SCORE:      105/*,
    EP_COUNT:       106,
    CH_COUNT:       107,
    VL_COUNT:       108*/
};

var TAGS_ARRAY = {
    anime: [
        { id: T_.TYPE, text: 'Type', has_prefix: true, prefix: '', def: true },
        { id: T_.GENRES, text: 'Genres', has_prefix: false, prefix: '', def: true },
        { id: T_.STUDIOS, text: 'Studios', has_prefix: false, prefix: '', def: true },
        { id: T_.LICENSORS, text: 'Licensors', has_prefix: false, prefix: '', def: true },
        { id: T_.PRODUCERS, text: 'Producers', has_prefix: false, prefix: '', def: false },
        { id: T_.DURATION, text: 'Episode Length', has_prefix: true, prefix: '', def: true },
        //{ id: T_.EP_COUNT, text: 'Episode Count', has_prefix: true, prefix: '', def: false },
        { id: T_.RATING, text: 'Rating', has_prefix: true, prefix: 'Rating: ', def: false },
        { id: T_.BROADCAST, text: 'Broadcast', has_prefix: true, prefix: 'Broadcast: ', def: false },
        { id: T_.SOURCE, text: 'Source', has_prefix: true, prefix: 'Source: ', def: false },
        { id: T_.STATUS, text: 'Status', has_prefix: true, prefix: 'Status: ', def: false },
        { id: T_.YEAR, text: 'Year', has_prefix: true, prefix: '', def: false },
        { id: T_.SEASON, text: 'Season', has_prefix: true, prefix: '', def: true },
        { id: T_.PERIOD, text: 'Time Period', has_prefix: true, prefix: '', def: false },
        { id: T_.JAPANESE, text: 'Japanese Title', has_prefix: true, prefix: '', def: false },
        { id: T_.ENGLISH, text: 'English Title', has_prefix: true, prefix: '', def: false },
        { id: T_.SCORE, text: 'Score', has_prefix: true, prefix: 'Score: ', def: false },
        { id: T_.RND_SCORE, text: 'Rounded Score', has_prefix: true, prefix: 'Score: ', def: false },
        { id: T_.RANK, text: 'Rank', has_prefix: true, prefix: 'Ranked: ', def: false },
        { id: T_.POPULARITY, text: 'Popularity', has_prefix: true, prefix: 'Popularity: ', def: false },
        { id: T_.MEMBERS, text: 'Members', has_prefix: true, prefix: 'Members: ', def: false },
        { id: T_.FAVORITES, text: 'Favorites', has_prefix: true, prefix: 'Favorites: ', def: false }
    ],
    manga: [
        { id: T_.TYPE, text: 'Type', has_prefix: true, prefix: '', def: true },
        { id: T_.GENRES, text: 'Genres', has_prefix: false, prefix: '', def: true },
        { id: T_.AUTHORS, text: 'Authors', has_prefix: false, prefix: '', def: true },
        { id: T_.SERIALIZATION, text: 'Serialization', has_prefix: true, prefix: '', def: true },
        //{ id: T_.CH_COUNT, text: 'Chapter Count', has_prefix: true, prefix: '', def: false },
        //{ id: T_.VL_COUNT, text: 'Volume Count', has_prefix: true, prefix: '', def: false },
        { id: T_.STATUS, text: 'Status', has_prefix: true, prefix: 'Status: ', def: false },
        { id: T_.YEAR, text: 'Year', has_prefix: true, prefix: '', def: false },
        { id: T_.SEASON, text: 'Season', has_prefix: true, prefix: '', def: false },
        { id: T_.PERIOD, text: 'Time Period', has_prefix: true, prefix: '', def: false },
        { id: T_.JAPANESE, text: 'Japanese Title', has_prefix: true, prefix: '', def: false },
        { id: T_.ENGLISH, text: 'English Title', has_prefix: true, prefix: '', def: false },
        { id: T_.SCORE, text: 'Score', has_prefix: true, prefix: 'Score: ', def: false },
        { id: T_.RND_SCORE, text: 'Rounded Score', has_prefix: true, prefix: 'Score: ', def: false },
        { id: T_.RANK, text: 'Rank', has_prefix: true, prefix: 'Ranked: ', def: false },
        { id: T_.POPULARITY, text: 'Popularity', has_prefix: true, prefix: 'Popularity: ', def: false },
        { id: T_.MEMBERS, text: 'Members', has_prefix: true, prefix: 'Members: ', def: false },
        { id: T_.FAVORITES, text: 'Favorites', has_prefix: true, prefix: 'Favorites: ', def: false }
    ]
};

var TAGS_ARRAY_SORTED = {
    anime: [], manga: []
};

TAGS_ARRAY_SORTED.update = function() {
    ['anime', 'manga'].forEach(function(type) {
        var order_map = mal.settings.order[type];
        TAGS_ARRAY_SORTED[type] = TAGS_ARRAY[type].slice().sort(function(a, b) {
            var a_order = order_map.hasOwnProperty(a.id) ? order_map[a.id] : 0;
            var b_order = order_map.hasOwnProperty(b.id) ? order_map[b.id] : 0;
            return a_order - b_order;
        });
    });
};

mal.loadValue = function(key, value) {
    try {
        value = JSON.parse(localStorage.getItem(mal.version + '#' + key)) || value;
    }
    finally {
        return value;
    }
};

mal.saveValue = function(key, value) {
    localStorage.setItem(mal.version + '#' + key, JSON.stringify(value));
};

mal.settings = {
    body: $('<div id="tu_settings">'),
    tags: { anime: [], manga: [] },
    order: { anime: {}, manga: {} },
    prefix: { anime: {}, manga: {} },

    load: function() {
        mal.settings.reset();
        ['anime', 'manga'].forEach(function(type) {
            mal.settings.tags[type] = mal.loadValue('mal.settings.tags.' + type, mal.settings.tags[type]);
            mal.settings.order[type] = mal.loadValue('mal.settings.order.' + type, mal.settings.order[type]);
            mal.settings.prefix[type] = mal.loadValue('mal.settings.prefix.' + type, mal.settings.prefix[type]);
        });
        TAGS_ARRAY_SORTED.update();
    },

    save: function() {
        ['anime', 'manga'].forEach(function(type) {
            mal.saveValue('mal.settings.tags.' + type, mal.settings.tags[type]);
            mal.saveValue('mal.settings.order.' + type, mal.settings.order[type]);
            mal.saveValue('mal.settings.prefix.' + type, mal.settings.prefix[type]);
        });
        TAGS_ARRAY_SORTED.update();
    },

    reset: function() {
        ['anime', 'manga'].forEach(function(type) {
            mal.settings.tags[type] = [];
            mal.settings.order[type] = {};
            mal.settings.prefix[type] = {};
            TAGS_ARRAY[type].forEach(function(tag, index) {
                if (tag.def) {
                    mal.settings.tags[type].push(tag.id);
                }
                if (tag.has_prefix) {
                    mal.settings.prefix[type][tag.id] = tag.prefix;
                }
                mal.settings.order[type][tag.id] = index + 1;
            });
        });
    },

    update: function() {
        mal.settings.body.empty();

        var table = $('<table class="tu_table" border="0" cellpadding="0" cellspacing="0" width="100%">' +
                        '<thead><tr>' +
                            '<th>Anime Tags <span>(Order / Prefix / Status)</span></th>' +
                            '<th>Manga Tags <span>(Order / Prefix / Status)</span></th>' +
                        '</tr></thead></table>');
        var tbody = $('<tbody>').appendTo(table);

        var tags_re = {
            anime: new RegExp('^(' + mal.settings.tags.anime.join('|') + ')$'),
            manga: new RegExp('^(' + mal.settings.tags.manga.join('|') + ')$')
        };

        var addSettingsRow = function(i) {
            var tr = $('<tr>').appendTo(tbody);

            ['anime', 'manga'].forEach(function(type) {
                if (i < TAGS_ARRAY[type].length) {
                    var tag = TAGS_ARRAY[type][i];
                    var order_map = mal.settings.order[type];
                    var prefix_map = mal.settings.prefix[type];

                    var el = $('<div class="tu_checkbox">')
                        .append('<input type="number" min="0" max="999">')
                        .append('<input type="text" value="">')
                        .append('<input name="tu_cb' + type[0] + '_' + tag.id +
                                       '" id="tu_cb' + type[0] + '_' + tag.id + '" type="checkbox">')
                        .append('<label for="tu_cb' + type[0] + '_' + tag.id + '">' + tag.text + '</label>');

                    $('input[type=number]', el).val(order_map.hasOwnProperty(tag.id) ? order_map[tag.id] : 0);
                    $('input[type=checkbox]', el).prop('checked', tag.id.toString().match(tags_re[type]));

                    var prefix = $('input[type=text]', el);
                    if (tag.has_prefix) {
                        prefix.val(prefix_map.hasOwnProperty(tag.id) ? prefix_map[tag.id] : '');
                    } else {
                        prefix.prop('disabled', true);
                    }

                    $('<td>').append(el).appendTo(tr);
                } else {
                    $('<td>').appendTo(tr);
                }
            });
        };

        var i;
        var max_length = Math.max(TAGS_ARRAY.anime.length, TAGS_ARRAY.manga.length);
        for (i = 0; i < max_length; i += 1) {
            addSettingsRow(i);
        }

        var buttons = $('<div class="tu_buttons">')
            .append($('<input class="tu_button" value="Save" type="button">').click(function() {
                ['anime', 'manga'].forEach(function(type) {
                    mal.settings.tags[type] = [];
                    mal.settings.order[type] = {};

                    $('input[type=checkbox][id^="tu_cb' + type[0] + '_"]', mal.settings.body).each(function() {
                        var id = this.id.match(/\d+/)[0];
                        if ($(this).prop('checked')) {
                            mal.settings.tags[type].push(id);
                        }

                        var order = parseInt($(this).parent().find('input[type=number]').val()) || 0;
                        order = Math.max(order, 0);
                        order = Math.min(order, 999);
                        mal.settings.order[type][id] = order;

                        var prefix = $(this).parent().find('input[type=text]').val();
                        mal.settings.prefix[type][id] = prefix.replace(/^\s+$/, '');
                    });
                });

                mal.settings.save();
                mal.fancybox.close();
            }))
            .append($('<input class="tu_button" value="Cancel" type="button">').click(function() {
                mal.fancybox.close();
            }))
            .append($('<input class="tu_button" value="Reset" type="button">').click(function() {
                mal.settings.reset();
                mal.settings.save();
                mal.fancybox.close();
            }));

        mal.settings.body
            .append('<div class="tu_title">Tags Settings</div>')
            .append($('<div class="tu_table_div">').append(table))
            .append(buttons);
    }
};

String.prototype.formatProducers = function() {
    return this
        .replace(/None\ found,\ <a\ href="[^\"]*?\/dbchanges\.php\?[^>]*?>add\ some<\/a>\.?/i, '')
        .replace(/<sup>[\s\S]*?<\/sup>/g, '')
        .replace(/,/g, '')
        .replace(/<\/a>\s*?<a/g, '</a>, <a');
};

String.prototype.formatJapTitle = function() {
    return this
        .replace(/\s+/g, ' ')
        .replace(/[\uff01-\uff5e]/g, function(ch) { return String.fromCharCode(ch.charCodeAt(0) - 0xfee0); })
        .replace(/\s?,\s?/g, '、')
        .replace(/\s?\+\s?/g, '+')
        .replace(/\s?=\s?/g, '=')
        .replace(/[“”]/g, '"')
        .replace(/\s?-\s?/g, '—').replace(/(\w)—(\w)/g, '$1-$2')
        .replace(/\s?:\s?/g, ':').replace(/(\d):(\d)/g, '$1:$2')
        .replace(/\s?[\(〈]/g, '(').replace(/[\)〉]\s?/g, ')')
        .replace(/\s?[\[【]/g, '[').replace(/[\]】]\s?/g, ']')
        .replace(/\s?『/g, '「').replace(/』\s?/g, '」')
        .replace(/\//g, '/').replace(////g, '//').replace(/\s?/\s?/g, '/')
        .replace(/[\.。]/g, '.').replace(/\s?.\s?/g, '.')
        .replace(/;/g, ';').replace(/\s?;\s?/g, ';')
        .replace(/["“”]/g, '"').replace(/\s?"\s?/g, '"')
        .replace(/[.・]{3}/g, '…').replace(/^./g, '.')
        .replace(/([^\w])./g, '$1。').replace(/(\d).(\d)/g, '$1.$2')
        .replace(/!/g, '!').replace(/\s?!\s?/g, '!')
        .replace(/\?/g, '?').replace(/\s??\s?/g, '?')
        .replace(/[~〜]/g, '~').replace(/\s?~\s?/g, '~')
        .replace(/([^\w]|[\sA-Z])x([^\w]|[\sA-Z])/g, '$1 × $2')
        .replace(/\s?×\s?/g, ' × ').replace(/\s?x\s?/g, ' × ');
};

function getTagsFromDuration(type, duration) {
    var reMin = duration.match(/(\d+)\ min./);
    var reHour = duration.match(/(\d+)\ hr./);

    duration = reHour ? (parseInt(reHour[1]) * 60) : 0;
    duration = reMin ? (parseInt(duration) + parseInt(reMin[1])) : -1;
    
    if (type.match(/(Movie|Music|Unknown)/) || duration < 0) {
        return '';
    }
    if (duration > 32) {
        return 'Long-ep';
    }
    if (duration < 10) {
        return 'Short-ep';
    }
    if (duration <= 16) {
        return 'Half-ep';
    }
    
    return '';
}

function getDateFromString(str) {
    var result = { year: '', season: '', period: '' };
    var date = str.replace(/to(.*)$/, '').trim();
    var m_year = date.match(/\d{4}/);
    var m_month = date.match(/^[a-zA-Z]{3}/);

    if (!m_year) {
        return result;
    }

    result.year = m_year[0];

    if (m_month) {
        result.season = m_month[0]
            .replace(/^(Jan|Feb|Mar)$/i, 'Winter')
            .replace(/^(Apr|May|Jun)$/i, 'Spring')
            .replace(/^(Jul|Aug|Sep)$/i, 'Summer')
            .replace(/^(Oct|Nov|Dec)$/i, 'Fall');
        result.season += ' ' + result.year;
    }

    var i;
    var years = [ [1917, 1949], [1950, 1969], [1970, 1989], [1990, 1999], [2000, 2004],
                  [2005, 2009], [2010, 2014], [2015, 2019], [2020, 2024], [2025, 2029] ];
    for (i = years.length - 1; i >= 0; i -= 1) {
        if (result.year >= years[i][0] && result.year <= years[i][1]) {
            result.period = years[i][0] + '-' + years[i][1];
            break;
        }
    }

    return result;
}

function getTags(data) {
    var result = [];
    var tags_re = new RegExp('^(' + mal.settings.tags[mal.type].join('|') + ')$');
    var re = data.match(/<div\ id="editdiv"([\s\S]*?)<h2>Information<\/h2>([\s\S]*?)<h2>Statistics<\/h2>([\s\S]*?)<\/td>/);

    if (!re) {
        return null;
    }

    var titles = re[1];
    var info = re[2];
    var stats = re[3];

    re = info.match(/[\s\S]*?>(Aired|Published):<\/span>([\s\S]*?)<\/div>/);
    var date = re ? getDateFromString(re[2]) : null;
    var textarea = $('<textarea>');
    var prefix_map = mal.settings.prefix[mal.type];

    TAGS_ARRAY_SORTED[mal.type].forEach(function(tag) {
        if (!tag.id.toString().match(tags_re)) {
            return;
        }

        var prefix = '';
        if (tag.has_prefix) {
            prefix = prefix_map.hasOwnProperty(tag.id) ? prefix_map[tag.id] : '';
        }

        switch (tag.id)
        {
            case T_.JAPANESE:
                re = titles.match(/[\s\S]*?>Japanese:<\/span>([\s\S]*?)<\/div>/);
                if (re) {
                    textarea.html(re[1].trim());
                    re = textarea.val().formatJapTitle();
                    if (re.length > 0) {
                        result.push(prefix + re);
                    }
                }
                break;

            case T_.ENGLISH:
                re = titles.match(/[\s\S]*?>English:<\/span>([\s\S]*?)<\/div>/);
                if (re) {
                    textarea.html(re[1].trim());
                    re = textarea.val();
                    if (re.length > 0) {
                        result.push(prefix + re);
                    }
                }
                break;

            case T_.TYPE:
                re = info.match(/[\s\S]*?>Type:<\/span>([\s\S]*?)<\/div>/);
                re = re ? $(re[1].replace('Unknown', '<a>N/A</a>')).text().trim() : 'N/A';
                if (re !== 'N/A' || prefix.length > 0) {
                    result.push(prefix + re);
                }
                break;

            case T_.GENRES:
                if (mal.type === 'anime') {
                    re = info.match(/[\s\S]*?>Genres:<\/span>([\s\S]*?)<\/div>[\s\S]*?>Rating:<\/span>([\s\S]*?)(\ -|None)/);
                } else {
                    re = info.match(/[\s\S]*?>Genres:<\/span>([\s\S]*?)<\/div>/);
                }

                if (re) {
                    result.push($(re[1].replace('No genres have been added yet.', '')).text());
                    if (mal.type === 'anime' && re[2].match('Rx')) {
                        result.push('Hentai');
                    }
                }
                break;

            case T_.STUDIOS:
                re = info.match(/[\s\S]*?>Studios:<\/span>([\s\S]*?)<\/div>/);
                if (re) {
                    result.push($(re[1].formatProducers()).text());
                }
                break;

            case T_.LICENSORS:
                re = info.match(/[\s\S]*?>Licensors:<\/span>([\s\S]*?)<\/div>/);
                if (re) {
                    result.push($(re[1].formatProducers()).text());
                }
                break;

            case T_.PRODUCERS:
                re = info.match(/[\s\S]*?>Producers:<\/span>([\s\S]*?)<\/div>/);
                if (re) {
                    result.push($(re[1].formatProducers()).text());
                }
                break;

            case T_.AUTHORS:
                re = info.match(/[\s\S]*?>Authors:<\/span>([\s\S]*?)<\/div>/);
                if (re) {
                    re = $(re[1]
                        .replace(/,/g, '')
                        .replace(/\((Art|Story|Story\ &\ Art)\)/g, '')
                        .replace(/<\/a>\s*?<a/g, '</a>, <a')
                    ).text();
                    if (re.length > 0) {
                        result.push(prefix + re);
                    }
                }
                break;

            case T_.SERIALIZATION:
                re = info.match(/[\s\S]*?>Serialization:<\/span>([\s\S]*?)<\/div>/);
                if (re) {
                    re = $(re[1].replace(/None\s*?$/, '<a>N/A</a>')).text().trim();
                    if (re !== 'N/A' || prefix.length > 0) {
                        result.push(prefix + re);
                    }
                }
                break;

            case T_.DURATION:
                re = info.match(/[\s\S]*?>Type:<\/span>([\s\S]*?)<\/div>[\s\S]*?>Duration:<\/span>([\s\S]*?)<\/div>/);
                if (re) {
                    re = getTagsFromDuration(re[1], re[2]);
                    if (re.length > 0) {
                        result.push(prefix + re);
                    }
                }
                break;

            /*case T_.EP_COUNT:

                break;

            case T_.CH_COUNT:

                break;

            case T_.VL_COUNT:

                break;*/

            case T_.RATING:
                re = info.match(/[\s\S]*?>Rating:<\/span>([\s\S]*?)(\ -|None)/);
                re = re ? re[1].trim().replace(/^\s*?$/, 'N/A') : 'N/A';
                if (re !== 'N/A' || prefix.length > 0) {
                    result.push(prefix + re);
                }
                break;

            case T_.STATUS:
                re = info.match(/[\s\S]*?>Status:<\/span>([\s\S]*?)<\/div>/);
                re = re ? re[1].trim()
                    .replace('Finished Airing', 'Finished')
                    .replace('Currently Airing', 'Airing') : 'N/A';
                if (re !== 'N/A' || prefix.length > 0) {
                    result.push(prefix + re);
                }
                break;

            case T_.BROADCAST:
                re = info.match(/[\s\S]*?>Broadcast:<\/span>([\s\S]*?)<\/div>/);
                if (re) {
                    re = re[1].trim()
                        .replace(/s\s[\s\S]*?$/, '')
                        .replace('Unknown', 'N/A');
                    if (re !== 'N/A' || prefix.length > 0) {
                        result.push(prefix + re);
                    }
                }
                break;

            case T_.SOURCE:
                re = info.match(/[\s\S]*?>Source:<\/span>([\s\S]*?)<\/div>/);
                if (re) {
                    re = re[1].trim().replace('Unknown', 'N/A');
                    if (re !== 'N/A' || prefix.length > 0) {
                        result.push(prefix + re);
                    }
                }
                break;

            case T_.YEAR:
                if (date && date.year.length > 0) {
                    result.push(prefix + date.year);
                }
                break;

            case T_.SEASON:
                if (date && date.season.length > 0) {
                    result.push(prefix + date.season);
                }
                break;

            case T_.PERIOD:
                if (date && date.period.length > 0) {
                    result.push(prefix + date.period);
                }
                break;

            case T_.SCORE:
            case T_.RND_SCORE:
                re = stats.match(/itemprop="ratingValue">([\d\.]+?)<\/span>/);
                re = re ? (tag.id === T_.RND_SCORE ? Math.round(re[1]) : re[1]) : 'N/A';
                if (re !== 'N/A' || prefix.length > 0) {
                    result.push(prefix + re);
                }
                break;

            case T_.RANK:
                re = stats.match(/[\s\S]*?>Ranked:<\/span>\s*?#(\d+?)\s*?</);
                re = re ? re[1] : 'N/A';
                if (re !== 'N/A' || prefix.length > 0) {
                    result.push(prefix + re);
                }
                break;

            case T_.POPULARITY:
                re = stats.match(/[\s\S]*?>Popularity:<\/span>\s*?#(\d+?)\s*?</);
                re = re ? re[1] : 'N/A';
                if (re !== 'N/A' || prefix.length > 0) {
                    result.push(prefix + re);
                }
                break;

            case T_.MEMBERS:
                re = stats.match(/[\s\S]*?>Members:<\/span>\s*?([\d,]+?)\s*?</);
                re = re ? re[1].replace(',', '') : 'N/A';
                if (re !== 'N/A' || prefix.length > 0) {
                    result.push(prefix + re);
                }
                break;

            case T_.FAVORITES:
                re = stats.match(/[\s\S]*?>Favorites:<\/span>\s*?([\d,]+?)\s*?</);
                re = re ? re[1].replace(',', '') : 'N/A';
                if (re !== 'N/A' || prefix.length > 0) {
                    result.push(prefix + re);
                }
                break;
        }
    });

    return result;
}

function setTags(id, tags, done_callback, fail_callback) {
    if (!tags) {
        if (fail_callback) {
            fail_callback(id);
        }
        return;
    }

    var cache = {};
    tags = $.map(tags.join(',').split(','), function(tag) {
            return tag.trim().replace(/'/g, '’').replace(/\s+/g, ' ');
        })
        .filter(function(tag) {
            if (tag.length === 0 || cache.hasOwnProperty(tag)) {
                return false;
            } else {
                cache[tag] = true;
                return true;
            }
        })
        .join(', ');

    if (mal.page === T_PAGE.M_POPUP) {
        $('textarea#add_' + mal.type + '_tags').prop('value', tags);
        if (done_callback) {
            done_callback();
        }
        return;
    }

    $.ajax(mal.tagsUrl + id + '&tags=' + encodeURIComponent(tags))
        .fail(function() {
            if (fail_callback) {
                fail_callback(id);
            }
        })
        .done(function(data) {
            if (!mal.modern && mal.page === T_PAGE.M_LIST &&
                    $('#list_surround .table_header[width="125"]').length > 0) {
                data = data.replace(/[\?&]status=\d/g, '').replace(/&tag=/g, mal.status + '&tag=');
                $('#list_surround #tagLinks' + id).html(data);
                $('#list_surround #tagRow' + id).text($(data).text());
            }
            if (done_callback) {
                done_callback();
            }
        });
}

function updateTags(id, mode) {
    var done_callback = function() {
        if (mal.page === T_PAGE.M_LIST) {
            mal.entries.done += 1;
            mal.content.done.text('    Done: ' + mal.entries.done + '/' + mal.entries.total);
        }
    };

    var fail_callback = function(id) {
        setTags(id, [], null, null);
        if (mal.page === T_PAGE.M_LIST) {
            mal.entries.fail += 1;
            mal.content.fail.text('    Failed: ' + mal.entries.fail);
        }
    };

    if (mode === T_RUN.M_CLEAR) {
        setTags(id, [], done_callback, fail_callback);
        return;
    }

    $.ajax('/' + mal.type + '/' + id + '/_/pics')
        .fail(function() {
            fail_callback(id);
        })
        .done(function(data) {
            setTags(id, getTags(data), done_callback, fail_callback);
        });
}

function updateAllTags(username, mode) {
    if (mal.page !== T_PAGE.M_LIST) {
        return;
    }

    mal.content.done.text('    Loading...');
    mal.content.fail.text('');
    mal.entries.total = 0;
    mal.entries.done = 0;
    mal.entries.fail = 0;

    if (mal.settings.tags[mal.type].length === 0) {
        mode = T_RUN.M_CLEAR;
    }

    $.get('/malappinfo.php?u=' + username + '&status=all&type=' + mal.type, function(data) {
        var links = $(mal.type, data);
        mal.entries.total = links.length;

        if (mal.entries.total === 0) {
            mal.content.done.text('');
            mal.content.fail.text('');
            return;
        }

        var index = 0;
        links.each(function() {
            var id = $('series_' + mal.type + 'db_id', this).text().trim();
            var tags = $('my_tags', this).text();

            if ((mode === T_RUN.M_EMPTY && tags !== '') || (mode === T_RUN.M_CLEAR && tags === '')) {
                mal.entries.done += 1;
            } else {
                setTimeout(function() {
                    updateTags(id, mode);
                }, mal.ajax.delay * index);
                index += 1;
            }
        });

        if (mal.page === T_PAGE.M_LIST) {
            mal.content.done.text('    Done: ' + mal.entries.done + '/' + mal.entries.total);
        }
    });
}

if ($('#malLogin').length === 0 && $('a[href$="/login.php"]').length === 0) {
    $.ajaxSetup({ timeout: mal.ajax.timeout });
    mal.settings.load();
    mal.modern = false;

    if (mal.page === T_PAGE.M_LIST) {
        mal.type = document.URL.match(/^http:\/\/myanimelist\.net\/(anime|manga)list\//)[1];
        mal.modern = $('.header .header-menu .btn-menu > span.username').length > 0;

        var el;
        var username;

        if (mal.modern) {
            if ($('.header .header-info').length === 0) {
                $('.header .header-menu').addClass('other').append('<div class="header-info">');
            }
            el = $('.header .header-info');
            username = $('.list-menu-float .icon-menu.profile').prop('href').match(/\/profile\/(.*)$/)[1];
            mal.status = $('.status-menu-container .status-menu .status-button.on').prop('href').match(/[\?&]status=\d/)[0];
            mal.fancybox.init('.list-container');
        } else {
            if (!$('#mal_cs_otherlinks div:first strong').text().match('You are viewing your')) {
                return;
            }
            el = $('<span id="tu_links">').appendTo('#mal_cs_otherlinks div:last');
            username = $('#mal_cs_listinfo strong a strong').text();
            mal.status = $('.status_selected a').prop('href').match(/[\?&]status=\d/)[0];
            mal.fancybox.init('#list_surround');
        }

        mal.fancybox.body.append(mal.settings.body);

        el.append((mal.modern ? '' : '&nbsp;|') + '&nbsp;&nbsp;Update Tags: ')
            .append($('<a href="javascript:void(0);" title="Update all tags">All</a>').click(function() {
                if (confirm('Are you sure you want to update all tags?')) {
                    updateAllTags(username, T_RUN.M_FULL);
                }
            }))
            .append(',&nbsp;')
            .append($('<a href="javascript:void(0);" title="Update only empty tags">Empty</a>').click(function() {
                if (confirm('Are you sure you want to update empty tags?')) {
                    updateAllTags(username, T_RUN.M_EMPTY);
                }
            }))
            .append('&nbsp;-&nbsp;')
            .append($('<a href="javascript:void(0);" title="Clear all tags">Clear</a>').click(function() {
                if (confirm('Are you sure you want to clear all tags?')) {
                    updateAllTags(username, T_RUN.M_CLEAR);
                }
            }))
            .append('&nbsp;-&nbsp;')
            .append($('<a href="javascript:void(0);" title="Change Tags Updater settings">Settings</a>').myfancybox(function() {
                mal.settings.update();
                mal.settings.body.show();
                return true;
            }))
            .append(mal.content.done)
            .append(mal.content.fail);

            $('<style type="text/css">').html(
                'div#tu_fancybox_wrapper { position: fixed; width: 100%; height: 100%; top: 0; left: 0; background: rgba(102, 102, 102, 0.3); z-index: 99990; }' +
                'div#tu_fancybox_inner { width: 600px !important; height: 650px !important; overflow: hidden; color: #000; }' +
                'div#tu_fancybox_outer { position: absolute; display: block; width: auto; height: auto; padding: 10px; border-radius: 8px; top: 80px; left: 50%; margin-top: 0 !important; margin-left: -310px !important; background: #fff; box-shadow: 0 0 15px rgba(32, 32, 32, 0.4); z-index: 99991; }' +
                'div#tu_settings { width: 100%; height: 100%; text-align: center; padding: 40px 0 35px; box-sizing: border-box; }' +
                'div#tu_settings .tu_title { position: absolute; top: 10px; left: 10px; width: 600px; font-size: 16px; font-weight: normal; text-align: center; margin: 0; border: 0; }' +
                'div#tu_settings .tu_title:after { content: ""; display: block; position: relative; width: 100%; height: 8px; margin: 0.5em 0 0;	padding: 0;	border-top: 1px solid #ebebeb; background: center bottom no-repeat radial-gradient(#f6f6f6, #fff 70%); background-size: 100% 16px; }' +
                'div#tu_settings .tu_table_div { width: 100%; height: 100%; overflow-x: hidden; overflow-y: auto; border: 1px solid #eee; box-sizing: border-box; }' +
                'div#tu_settings .tu_table thead { background-color: #f5f5f5; }' +
                'div#tu_settings .tu_table th { background-color: transparent; width: 50%; padding: 5px 0 5px 5px; color: #222; font-size: 13px; font-weight: bold; text-align: left; line-height: 20px !important; box-shadow: none; }' +
                'div#tu_settings .tu_table th > span { font-size: 11px; font-weight: normal; }' +
                'div#tu_settings .tu_table tbody { background-color: #fff; }' +
                'div#tu_settings .tu_table td { text-align: left !important; }' +
                'div#tu_settings .tu_table .tu_checkbox > * { vertical-align: middle; }' +
                'div#tu_settings .tu_table .tu_checkbox > input[type=number], div#tu_settings .tu_table .tu_checkbox > input[type=text] { width: 40px !important; margin: 1px 2px 1px 5px !important; padding: 1px 0 2px 2px !important; border: 1px solid #bbb !important; font-size: 11px !important; }' +
                'div#tu_settings .tu_table .tu_checkbox > input[type=text] { width: 70px !important; margin: 1px 2px !important; text-align: right; }' +
                'div#tu_settings .tu_table .tu_checkbox > input[type=checkbox] + label { font-weight: normal; color: #666; }' +
                'div#tu_settings .tu_table .tu_checkbox > input[type=checkbox]:checked + label { font-weight: bold; color: #222; }' +
                'div#tu_settings .tu_buttons { position: absolute; bottom: 10px; width: 600px; text-align: center; padding: 0; }' +
                'div#tu_settings .tu_buttons > .tu_button { margin: 2px 5px !important; font-size: 12px; }'
            ).appendTo('head');
    } else {
        mal.type = document.URL.match(/(\?go=(add|edit)&|\?type=anime&|ownlist\/anime\/)/) ? 'anime' : 'manga';

        var id = $('#main-form > table td.borderClass:contains(Title) + td > strong > a').prop('href').match(/\d+/)[0];
        $('#main-form > table.advanced td.borderClass:contains(Tags)').append('&nbsp;').append(
            $('<a href="javascript:void(0)">').click(function() {
                updateTags(id, T_RUN.M_FULL);
            })
            .append('<small>update</small>')
        );
    }

    mal.tagsUrl = '/includes/ajax.inc.php?' + (mal.type === 'anime' ? 't=22&a' : 't=30&m') + 'id=';
}

}(jQuery));