// ==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 = "
" + "
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();