RED Artist Aliases Filter

Add a box on artist page to filter based on aliases

当前为 2019-02-01 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name        RED Artist Aliases Filter
// @namespace   PTH Artist Aliases Filter
// @description Add a box on artist page to filter based on aliases
// @include     https://passtheheadphones.me/artist.php?id=*
// @include     https://redacted.ch/artist.php?id=*
// @version     1.3.5
// @grant       GM_registerMenuCommand
// ==/UserScript==

/* Avoid using jQuery in this userscript, prioritize vanilla javascript as a matter of performance on big pages */

"use strict";

function Storage(alias_id) {
    this.key = "red.artists_aliases_filter." + alias_id;

    this.save = function(data) {
        if (typeof data !== 'string') {
            data = JSON.stringify(data);
        }
        sessionStorage.setItem(this.key, data);
    };

    this.load = function() {
        let storage = sessionStorage.getItem(this.key) || "{}";
        return JSON.parse(storage);
    };
};

function ConfigManager() {
    this.key = "red.artists_aliases_filter.CONFIG";

    this.default = {
        "display_nb_groups": 0
    };

    this.save = function(config) {
        if (typeof config !== 'string') {
            config = JSON.stringify(config);
        }
        localStorage.setItem(this.key, config);
    };

    this.load = function() {
        let config = {};
        for (let key in this.default) {
            config[key] = this.default[key];
        }
        let parsed = JSON.parse(localStorage.getItem(this.key) || "{}");
        for (let key in parsed) {
            if (this.default.hasOwnProperty(key)) {
                config[key] = parsed[key];
            }
        }
        return config;
    };
};

function Builder() {
    this.make_box_aliases = function() {
        let box_aliases =
            "<div class='box box_aliases'>" +
                "<div class='head'><strong>Aliases</strong></div>" +
                "<ul class='stats nobullet'></ul>" +
            "</div>";
        return box_aliases;
    };

    this.make_alias_release = function(alias_id, alias_name) {
        let alias_release =
            "<text class='filtering_off'>" +
                " <i>as</i> " +
                "<a href='#content' class='alias_filter' alias_id='" + alias_id + "'>" +
                    alias_name +
                "</a>" +
            "</text>";
        return alias_release;
    };

    this.mousehover_nb_groups = "Number of releases with this alias";

    this.make_alias_li = function(alias_id, alias_name, nb_groups) {
        let nb_groups_str = "";
        if (nb_groups !== undefined) {
            nb_groups_str = "(" + nb_groups.toString() + ")";
        }
        let span_nb_groups =
            "<span title='" + this.mousehover_nb_groups + "' class='alias_list_nb_groups'>" +
                nb_groups_str +
            "</span>";
        let alias_li =
            "<li>" +
                "<a href='#' class='alias_filter' alias_id='" + alias_id.toString() + "'>" + alias_name + "</a>" +
                " " + span_nb_groups +
            "</li>";
        return alias_li;
    };

    this.make_tag_ul = function(alias_id, innerHTML) {
        let tag_ul =
            "<ul class='stats nobullet filtering_on alias_id alias_id_" + alias_id + "'>" +
                innerHTML +
            "</ul>";
        return tag_ul;
    };

    this.make_tag_li = function(alias_name, tag_name, tag_count) {
        let href = "torrents.php?taglist=" + tag_name +
                   "&amp;artistname=" + encodeURIComponent(alias_name) +
                   "&amp;action=advanced&amp;searchsubmit=1";

        // Count do not need formatting (see Bach) on the contrary of stats
        let tag_li =
            "<li>" +
                "<a href='" + href + "'>" + tag_name + "</a> (" + tag_count.toString() + ")" +
            "</li>";
        return tag_li;
    };

    this.make_stats_ul = function(alias_id, innerHTML) {
        let stats_ul =
            "<ul class='stats nobullet filtering_on alias_id alias_id_" + alias_id + "'>" +
                innerHTML +
            "</ul>";
        return stats_ul;
    }

    this.make_stats_li = function(type, number) {
        let stats_li =
            "<li>" +
                "Number of " + type + ": " + number.toLocaleString("en") +
            "</li>";
        return stats_li;
    }

    this.make_alias_title = function(artist_name) {
        let main = "<a href='#' class='alias_filter' alias_id='-1'>" + artist_name + "</a>";
        let alias = "<span id='alias_title'></span>";
        let span_nb_groups =
            "<span id='alias_title_nb_groups' title='" + this.mousehover_nb_groups + "'>" +
            "</span>";
        let title =
            "<h2 class='filtering_on'>" +
                main + " " + alias + " " + span_nb_groups +
            "</h2>";
        return title;
    };

    this.make_css_style = function(id) {
        let style =
            "<style type='text/css' " + (id === undefined ? "" : "id='" + id + "'") + ">" +
            "</style>";
        return style;
    }
};

function Manager() {
    this.builder = new Builder();
    this.config_manager = new ConfigManager();

    this.config = undefined;

    this.current_alias_id = "-1";

    this.try_catch = function(func) {
        let self = this;
        func = func.bind(this);

        function wrapped() {
            try {
                func();
            } catch(err) {
                let err_msg = err.message + " (line " + err.lineNumber + ")";
                console.log("Error in RED AAF: '" + err_msg + "'.");
                self.set_error_message(err_msg);
            }
        }

        return wrapped;
    };

    this.get_aliases_list = function() {
        let aliases_list = document.getElementById("aliases_list");
        return aliases_list;
    };

    this.set_error_message = function(msg) {
        let error_msg =
            "<li>" +
                "<strong>An error occured.</strong></br>" +
                msg +
            "</li>";
        let aliases_list = this.get_aliases_list();
        aliases_list.innerHTML = error_msg;
    };

    this.proceed = function() {
        let start = this.try_catch(this.start);
        start();
    };

    this.start = function() {
        let artist_id = this.get_artist_id();

        this.set_box_aliases();
        this.set_loading_message();

        this.set_style_node();
        this.reset_style();

        let config = this.config_manager.load();
        this.config = config;
        this.apply_config(config);
        this.register_config();

        let hash = this.compute_hash();

        let storage = new Storage(artist_id);
        let storage_data = storage.load();

        let self = this;

        // If cache is not yet set or if it is no longer valid, query the API
        if (storage_data["hash"] !== hash) {
            this.query_api(artist_id, function(json_data) {
                let data = self.parse_json_data(json_data);
                data["hash"] = hash;
                storage.save(data);
                self.set_aliases(data);
            });
        } else {
            this.set_aliases(storage_data);
        }
    };

    this.set_box_aliases = function() {
        let box_search = document.getElementsByClassName("box_search")[0];
        let box_aliases = this.builder.make_box_aliases();
        box_search.insertAdjacentHTML('beforebegin', box_aliases);
        box_aliases = box_search.parentNode.getElementsByClassName("box_aliases")[0];
        box_aliases.getElementsByClassName("stats")[0].id = "aliases_list";
    };

    this.set_loading_message = function() {
        let aliases_list = this.get_aliases_list();
        aliases_list.innerHTML = "<li>Loading...</li>";
    };

    this.get_artist_id = function() {
        let artist_id = window.location.href.match(/id=(\d+)/)[1];
        return artist_id;
    };

    this.set_style_node = function() {
        let head = document.getElementsByTagName('head')[0];
        let style_filter = this.builder.make_css_style("artist_alias_filter_css");
        let style_nb_groups = this.builder.make_css_style("artist_alias_filter_nb_groups_css");
        head.insertAdjacentHTML('beforeend', style_filter);
        head.insertAdjacentHTML('beforeend', style_nb_groups);
    };

    this.set_style_filter = function(css) {
        let style = document.getElementById("artist_alias_filter_css");
        style.innerHTML = css;
    };

    this.set_style_nb_groups = function(css) {
        let style = document.getElementById("artist_alias_filter_nb_groups_css");
        style.innerHTML = css;
    };

    this.reset_style = function() {
        let style =
            ".filtering_on { display: none; }";
        this.set_style_filter(style);
    };

    this.filter_style = function(alias_id) {
        let style =
            ".filtering_off { display: none; } " +
            ".alias_id:not(.alias_id_" + alias_id.toString() + ") { display: none; }";
        this.set_style_filter(style);
    };

    this.register_config = function() {
        let self = this;

        let caption = "RED Artist Alias Filter - Configuration";

        function callback() {
            let new_config = self.configure(self.config);
            self.config = new_config;
            self.config_manager.save(new_config);
            self.apply_config(new_config);
        };

        if (typeof GM_registerMenuCommand != "undefined") {
            GM_registerMenuCommand(caption, callback);
        }
    };

    this.configure = function(current_config) {
        let new_config = {};
        for (var key in current_config) {
            new_config[key] = current_config[key];
        }

        let display_nb_groups = current_config["display_nb_groups"];
        let text =
            "Display of the number of releases per alias:\n" +
            "0. Don't display\n" +
            "1. Display it in the Aliases box\n" +
            "2. Display it in the title while on a filtered page\n" +
            "3. Both";

        let value = prompt(text, display_nb_groups);

        if (value === null) {
            return current_config;
        }

        let val = parseInt(value);

        if ([0, 1, 2, 3].indexOf(val) === -1) {
            alert("Invalid value!");
            return current_config;
        }

        new_config["display_nb_groups"] = val;
        return new_config
    };

    this.apply_config = function(config) {
        let display_nb_groups = config["display_nb_groups"];
        let style = "";

        if (display_nb_groups === 0 || display_nb_groups === 1) {
            style += "#alias_title_nb_groups { display: none; } ";
        }

        if (display_nb_groups === 0 || display_nb_groups === 2) {
            style += ".alias_list_nb_groups { display: none; } ";
        }

        this.set_style_nb_groups(style);
    };

    // Set an array `groups_ids` of all groupid on the current artist page
    // to ensure that cache is still valid (no new group since last visit)
    this.compute_hash = function() {
        let elements = document.querySelectorAll("[id^='showimg_']");
        let groups_ids = [];
        for (let i = 0, len = elements.length; i < len; i++) {
            let group_id = elements[i].id.split("_")[1];
            groups_ids.push(group_id);
        }
        groups_ids.sort();

        let version = GM_info.script.version;
        groups_ids.unshift("version:" + version);

        let hash = groups_ids.toString();
        return hash;
    };

    // Parse JSON response after having queried the API and extract
    // main_alias_id, main_name, aliases, groups, and tags
    this.parse_json_data = function(json_data) {
        json_data = json_data.response;
        let main_name = json_data.name;
        let main_alias_id = undefined;
        let aliases = {};      // alias_id => alias_name
        let groups = {};       // group_id => alias_id
        let aliases_tags = {}; // alias_id => tag_name => count
        let tags = {};         // alias_id => [tag_name, count]

        let main_id = json_data["id"]

        // Iterate through each artists of each group to find those correct (`id` === `main_id`)
        let torrentgroup = json_data.torrentgroup;
        for (let i = 0, len = torrentgroup.length; i < len; i++) {
            let group = torrentgroup[i];

            // Same release can appear twice in different categories
            if (groups.hasOwnProperty(group) && groups[group] !== "-1") {
                continue;
            }

            let extendedArtists = group["extendedArtists"];
            let release_type = group["releaseType"];
            let found = false;

            let alias_id = "-1";
            let group_id = group["groupId"].toString();

            // Search alias_id through the list of artists lists
            for (let id in extendedArtists) {
                let artists = extendedArtists[id];
                if (artists) {
                    for (let j = 0, len_ = artists.length; j < len_; j++) {
                        let artist = artists[j];
                        if (artist["id"] === main_id) {
                            // This is not perfect:
                            // If a release contains references to multiple aliases of the same artist, it keeps only the first one
                            // For example, see group 72607761 of Snoop Dogg
                            // However, it is better for performance not to have to iterate through an array
                            // So let's say 1 group release => 1 artist alias
                            alias_id = artist["aliasid"].toString();
                            aliases[alias_id] = artist["name"];

                            if ((main_alias_id === undefined) && (artist["name"] === main_name)) {
                                // Sometimes, the alias_id associated with the artist main id differs, see artist 24926
                                // But we need it to not display "as Alias" besides releases of main artist name
                                main_alias_id = alias_id;
                            }
                            found = true;
                            break;
                        }
                    }
                }
                if (found) break;
            }

            // Sometimes, release does not contain any artist because of an issue with the API
            // See: https://what.cd/forums.php?action=viewthread&threadid=192517&postid=5290204
            // In such a case (aliasid == -1), the release is not linked to any alias, just the default "[Show All]"
            groups[group_id] = alias_id;

            // Create the dictionary to update tags box
            // Skip compilations and soundtracks
            // See Gazelle code source: https://github.com/WhatCD/Gazelle/blob/2aa4553f7a508e0051cae2249229bfe0f3f99c89/sections/artist/artist.php#L258
            if (release_type !== 7 && release_type !== 3 && alias_id !== "-1") {
                if (!aliases_tags.hasOwnProperty(alias_id)) {
                    aliases_tags[alias_id] = {};
                }
                let artist_tags = aliases_tags[alias_id];
                let group_tags = group["tags"];
                for (let j = 0, len_ = group_tags.length; j < len_; j++) {
                    let tag = group_tags[j];
                    if (!artist_tags.hasOwnProperty(tag)) {
                        artist_tags[tag] = 1;
                    } else {
                        artist_tags[tag] += 1
                    }
                }
            }
        }

        // Sort tags list by count and keep only top 50 (see Gazelle code source)
        for (let artist_id in aliases_tags) {
            let tags_dict = aliases_tags[artist_id];
            let tags_pairs = Object.keys(tags_dict).map(function(tag) {
                return [tag, tags_dict[tag]];
            });
            tags_pairs.sort(function(pair_1, pair_2) {
                return pair_2[1] - pair_1[1];
            });
            tags_pairs = tags_pairs.slice(0, 50);
            tags[artist_id] = tags_pairs;
        }

        let data = {
            "tags": tags,
            "main_name": main_name,
            "main_alias_id": main_alias_id,
            "aliases": aliases,
            "groups": groups
        };

        return data;
    };

    this.query_api = function(artist_id, callback) {
        let self = this;
        let url = "/ajax.php?action=artist&id=" + artist_id;

        let xhr = new XMLHttpRequest();
        xhr.timeout = 20000;

        xhr.ontimeout = this.try_catch(
            function() {
                self.set_error_message("The API query timed out.");
            }
        );

        xhr.onerror = this.try_catch(
            function() {
                self.set_error_message("The API query failed.\n" + xhr.statusText);
            }
        );

        xhr.onload = this.try_catch(
            function() {
                if (xhr.status === 200) {
                    let data = JSON.parse(xhr.responseText);
                    callback(data);
                } else {
                    self.set_error_message("The API query returned an error.\n" + xhr.statusText);
                }
            }
        );

        xhr.open("GET", url, true);
        xhr.send(null);
    };

    this.set_alias_title = function(alias_name, nb_groups) {
        document.getElementById("alias_title").innerHTML = "[" + alias_name + "]";
        document.getElementById("alias_title_nb_groups").innerHTML = "(" + nb_groups.toString() + ")";
    };

    this.append_alias_filter = function(alias_id, alias_name, nb_groups) {
        let li = this.builder.make_alias_li(alias_id, alias_name, nb_groups);
        let aliases_list = this.get_aliases_list();
        aliases_list.insertAdjacentHTML('beforeend', li);
    };

    this.set_aliases = function(data) {
        if (Object.keys(data["aliases"]).length < 2) {
            this.cancel_process();
            return;
        }
        this.init_alias_title(data["main_name"]);
        let stats = this.classify_releases(data["aliases"], data["groups"], data["main_alias_id"]);
        this.populate_tags(data["aliases"], data["tags"]);
        this.compute_stats(stats);
        this.fill_aliases_list(data["aliases"], stats);
        this.bind_filter(data["aliases"], stats);
    };

    this.cancel_process = function() {
        let box_aliases = document.getElementsByClassName("box_aliases")[0];
        box_aliases.style.display = "none";
    };

    this.init_alias_title = function(main_name) {
        let content = document.getElementById("content");
        let header = content.getElementsByClassName("header")[0];
        let h2 = header.getElementsByTagName("h2")[0];
        h2.className += " filtering_off";

        let title = this.builder.make_alias_title(main_name);

        h2.insertAdjacentHTML("afterend", title);
    };

    this.fill_aliases_list = function(aliases, stats) {
        let aliases_list = this.get_aliases_list();
        aliases_list.innerHTML = "";
        this.append_alias_filter("-1", "[Show All]");
        let first = aliases_list.getElementsByTagName("a")[0];
        first.style.fontSize = "80%";
        first.style.fontWeight = "bold";
        for (let alias_id in aliases) {
            let name = aliases[alias_id];
            this.append_alias_filter(alias_id, name, stats[alias_id]["groups"]);
        }
    };

    this.classify_releases = function(aliases, groups, main_alias_id) {
        let stats = {}; // Compute stats while classifying trough torrents
        let torrent_tables = document.getElementsByClassName("torrent_table");
        let categories = document.getElementById("discog_table").getElementsByClassName("box")[0];

        for (let alias_id in aliases) {
            stats[alias_id] = {
                "groups_ids": {},
                "groups": 0,
                "torrents": 0,
                "seeders": 0,
                "leechers": 0,
                "snatches": 0
            }
        }

        for (let i = 0, len = torrent_tables.length; i < len; i++) {
            let table = torrent_tables[i];
            let category_id = table.getAttribute("id");
            let aliases_in_this_category = {};

            let discogs = table.getElementsByClassName("discog");
            let count_stats = false;
            let alias_id = undefined;
            let group_id = undefined;

            for (let j = 0, len_ = discogs.length; j < len_; j++) {
                let discog = discogs[j];
                // The groupid of each torrent row is the same that the previous encountered main release row
                // This avoid having to extract groupid value at each iteration

                if (discog.classList.contains("group")) {
                    // Retrieve group_id and alias_id for this release
                    group_id = discog.querySelector("[id^='showimg_']").id.split("_")[1];
                    alias_id = groups[group_id];
                    aliases_in_this_category[alias_id] = 1;

                    // Append "as Alias" to release artist name if needed
                    if (alias_id !== main_alias_id && alias_id !== "-1") {
                        let group_info = discog.getElementsByClassName("group_info")[0];
                        let strong = group_info.getElementsByTagName("strong")[0];
                        let name = aliases[alias_id];

                        let alias_text = this.builder.make_alias_release(alias_id, name);

                        strong.insertAdjacentHTML("beforeend", alias_text);
                    }

                    // Avoid same torrents in two different categories to be counted twice for stats
                    if (alias_id === "-1" || stats[alias_id]["groups_ids"].hasOwnProperty(group_id)) {
                        count_stats = false;
                    } else {
                        stats[alias_id]["groups_ids"][group_id] = 1;
                        stats[alias_id]["groups"] += 1;
                        count_stats = true;
                    }
                } else if (count_stats && discog.classList.contains("torrent_row") && alias_id !== "-1") {
                    // Update stats with the current torrent
                    let alias_stats = stats[alias_id];
                    let cols = discog.getElementsByClassName("number_column");

                    let seeders = parseInt(cols[2].innerText);
                    let leechers = parseInt(cols[3].innerText);
                    let snatches = parseInt(cols[1].innerText);

                    alias_stats["torrents"] += 1;
                    alias_stats["seeders"] += seeders;
                    alias_stats["leechers"] += leechers;
                    alias_stats["snatches"] += snatches;
                }

                discog.className += " alias_id alias_id_" + alias_id;
            }

            let category_aliases = " alias_id";
            for (let alias_id in aliases_in_this_category) {
                category_aliases += " alias_id_" + alias_id;
            }
            table.className += category_aliases;
            categories.querySelector("[href='#" + category_id + "']").className += category_aliases;
        }

        return stats;
    };

    this.bind_filter = function(aliases, stats) {
        let self = this;
        let filters = document.getElementsByClassName("alias_filter");

        function callback(event) {
            let call = self.try_catch(
                function() {
                    let clicked = event.target;
                    if (clicked.getAttribute("href") === "#") {
                        event.preventDefault();
                    }
                    let alias_id = clicked.getAttribute("alias_id");
                    self.filter_releases(alias_id, aliases, stats);
                }
            );
            call();
        }

        for (let i = 0, len = filters.length; i < len; i++) {
            let filter = filters[i];
            filter.addEventListener("click", callback);
        }
    };

    this.populate_tags = function(aliases, tags) {
        let box_tags = document.getElementsByClassName("box_tags")[0];
        let tag_list = box_tags.getElementsByClassName("stats")[0];
        tag_list.className += " filtering_off";

        for (let alias_id in tags) {
            let alias_tags = tags[alias_id];
            let alias_name = aliases[alias_id];
            let li_list = "";

            for (let i = 0, len = alias_tags.length; i < len; i++) {
                let tag_and_count = alias_tags[i];
                let tag = tag_and_count[0];
                let count = tag_and_count[1];
                let li = this.builder.make_tag_li(alias_name, tag, count);
                li_list += li;
            }

            if (li_list === "") {
                li_list = "<li>No torrent tags</li>";
            }

            let ul = this.builder.make_tag_ul(alias_id, li_list);
            tag_list.insertAdjacentHTML("beforebegin", ul);
        }
    };

    this.compute_stats = function(stats) {
        let box_stats = document.getElementsByClassName("box_statistics_artist")[0];
        let stats_list = box_stats.getElementsByClassName("stats")[0];
        stats_list.className += " filtering_off";

        let builder = this.builder;
        let labels = ["groups", "torrents", "seeders", "leechers", "snatches"];

        for (let alias_id in stats) {
            let alias_stats = stats[alias_id];
            let li_list = "";

            for (let i = 0, len = labels.length; i < len; i++) {
                let label = labels[i];
                li_list += builder.make_stats_li(label, alias_stats[label]);
            }

            let ul = builder.make_stats_ul(alias_id, li_list);
            stats_list.insertAdjacentHTML("beforebegin", ul);
        }
    };

    this.filter_releases = function(alias_id, aliases, stats) {
        let current_alias_id = this.current_alias_id;
        if (alias_id === current_alias_id) return;

        let aliases_list = this.get_aliases_list();
        let current_link = aliases_list.querySelector("[alias_id='" + current_alias_id + "']");
        let new_link = aliases_list.querySelector("[alias_id='" + alias_id + "']");

        current_link.style.fontWeight = "";
        new_link.style.fontWeight = "bold";

        if (alias_id === "-1") {
            this.reset_style();
        } else {
            this.set_alias_title(aliases[alias_id], stats[alias_id]["groups"]);
            this.filter_style(alias_id);
        }

        this.current_alias_id = alias_id;
    };
};

let manager = new Manager();
manager.proceed();