RED Artist Aliases Filter

Add a box on artist page to filter based on aliases

当前为 2017-04-04 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴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.1
// @grant       none
// ==/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 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='alias_id'>" +
                " <i>as</i> " +
                "<a href='#content' alias_id='" + alias_id + "'>" +
                    alias_name +
                "</a>" +
            "</text>";
        return alias_release;
    }

    this.make_alias_li = function(alias_id, alias_name) {
        let alias_li = 
            "<li>" +
                "<a href='#' alias_id='" + alias_id.toString() + "'>" + alias_name + "</a>" +
            "</li>";
        return alias_li;
    };

    this.make_tag_ul = function(alias_id) {
        let tag_ul = "<ul class='stats nobullet tag_filtering alias_id alias_id_" + alias_id + "'></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";
        let tag_li =
            "<li>" +
                "<a href='" + href + "'>" + tag_name + "</a> (" + tag_count.toString() + ")" +
            "</li>";
        return tag_li;
    };

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

function Manager() {
    this.builder = new Builder();
    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() {
        this.set_box_aliases();
        this.set_loading_message();

        let artist_id = this.get_artist_id();

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

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

        let hash = this.compute_hash();

        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 = document.createElement('style');
        style.type = 'text/css';
        style.id = "artist_alias_filter_css";
        head.appendChild(style);
    };

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

    this.reset_style = function() {
        let style = 
            "#title_filtering, .tag_filtering { display: none; }";
        this.set_style(style);
    };

    this.filter_style = function(alias_id) {
        let style =
            "#default_title, #default_taglist { display: none; } " +
            ".alias_id:not(.alias_id_" + alias_id.toString() + ") { display: none; }";
        this.set_style(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();

            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) {
        document.getElementById("alias_title").innerHTML = "[" + alias_name + "]";
    };

    this.append_alias_filter = function(alias_id, alias_name) {
        let li = this.builder.make_alias_li(alias_id, alias_name);
        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"]);
        this.classify_releases(data["aliases"], data["groups"], data["main_alias_id"]);
        this.fill_aliases_list(data["aliases"]);
        this.bind_filter(data["aliases"]);
        this.populate_tags(data["aliases"], data["tags"]);
    };

    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.id = "default_title";
        
        let title = this.builder.make_alias_title(main_name);

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

    this.fill_aliases_list = function(aliases) {
        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);
        }
    };

    this.classify_releases = function(aliases, groups, main_alias_id) {
        let torrent_tables = document.getElementsByClassName("torrent_table");
        let categories = document.getElementById("discog_table").getElementsByClassName("box")[0];

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

            let discogs = table.getElementsByClassName("discog");
            let alias_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")) {
                    let group_id = discog.querySelector("[id^='showimg_']").id.split("_")[1];
                    alias_id = groups[group_id];
                    aliases_in_this_category[alias_id] = 1;

                    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);
                    }
                }

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

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

    this.bind_filter = function(aliases) {
        let self = this;
        let filters = document.querySelectorAll("[alias_id]");

        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);
                }
            );
            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.id = "default_taglist";
        tag_list.className += " alias_id";
        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);
            tag_list.insertAdjacentHTML("beforebegin", ul);
            let ul_node = box_tags.getElementsByClassName("alias_id_" + alias_id)[0];
            ul_node.insertAdjacentHTML("afterbegin", li_list);
        }
    };

    this.filter_releases = function(alias_id, aliases) {
        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]);
            this.filter_style(alias_id);
        }

        this.current_alias_id = alias_id;
    };
};

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