// ==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
// @downloadURL 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 =
"
";
return box_aliases;
};
this.make_alias_release = function(alias_id, alias_name) {
let alias_release =
"" +
" as " +
"" +
alias_name +
"" +
"";
return alias_release;
}
this.make_alias_li = function(alias_id, alias_name) {
let alias_li =
"" +
"" + alias_name + "" +
"";
return alias_li;
};
this.make_tag_ul = function(alias_id) {
let tag_ul = "";
return tag_ul;
};
this.make_tag_li = function(alias_name, tag_name, tag_count) {
let href = "torrents.php?taglist=" + tag_name +
"&artistname=" + encodeURIComponent(alias_name) +
"&action=advanced&searchsubmit=1";
let tag_li =
"" +
"" + tag_name + " (" + tag_count.toString() + ")" +
"";
return tag_li;
};
this.make_alias_title = function(artist_name) {
let main = "" + artist_name + "";
let alias = "";
let title =
"" +
main + " " + alias +
"
";
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 =
"" +
"An error occured." +
msg +
"";
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 = "Loading...";
};
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 = "No torrent tags";
}
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();