Anakunda / GazelleTagManager

// ==UserScript==
// ==UserLibrary==
// @name         GazelleTagManager
// @namespace    https://openuserjs.org/users/Anakunda
// @version      1.00.8
// @author       Anakunda
// @license      GPL-3.0-or-later
// @copyright    2021, Anakunda (https://openuserjs.org/users/Anakunda)
// @exclude      *
// ==/UserScript==
// ==/UserLibrary==

String.prototype.toASCII = function() {
	return this.normalize("NFKD").replace(/[\x00-\x1F\u0080-\uFFFF]/g, '');
};

const tmExcludedCountries = [
	/^(?:United\s+States|USA?|U\.S\.(?:A\.)?)$/i,
	/^(?:United\s+Kingdom|(?:Great\s+)?Britain|England|GB|G\.B\.|UK|U\.K\.)$/i,
	/^(?:Europe|European\s+Union|EU|E\.U\.)$/i,
	/^(?:Unknown)$/i,
];
const tmPresubstitutions = [
	[/\b(?:Singer\/Songwriter)\b/i, 'singer.songwriter'],
	[/\b(?:Pop\/Rock)\b/i, 'pop.rock'],
	[/\b(?:Folk\/Rock)\b/i, 'folk.rock'],
	[/^(?:Psy\/Goa\s+Trance)$/i, 'psytrance, goa.trance'],
	[/\s*,\s*(?:&\s*|and\s+)/i, ' & '],
	[/\b(?:Xmas)\b/i, 'Christmas'],
];
const tmSubstitutions = [
	[/^Pop\s*(?:[\-\−\—\–]\s*)?Rock$/i, 'pop.rock'],
	[/^Rock\s*(?:[\-\−\—\–]\s*)?Pop$/i, 'pop.rock'],
	[/^Rock\s+n\s+Roll$/i, 'rock.and.roll'],
	['AOR', 'album.oriented.rock'],
	[/^(?:Prog)\.?\s*(?:Rock)$/i, 'progressive.rock'],
	[/^Synth[\s\-\−\—\–]+Pop$/i, 'synthpop'],
	//[/^World(?:\s+and\s+|\s*[&+]\s*)Country$/i, 'world.music', 'country'],
	['World', 'world.music'],
	[/^(?:Singer(?:\s+and\s+|\s*[&+]\s*))?Songwriter$/i, 'singer.songwriter'],
	[/^(?:R\s*(?:[\'\’\`][Nn](?:\s+|[\'\’\`]\s*)|&\s*)B|RnB)$/i, 'rhythm.and.blues'],
	[/^(?:Alternat(?:iv)?e)\s+(?:R\s*(?:[\'\’\`][Nn](?:\s+|[\'\’\`]\s*)|&\s*)B|RnB)$/i, 'alternative.rhythm.and.blues'],
	[/\b(?:Soundtracks?)$/i, 'score'],
	['Electro', 'electronic'],
	//['Metal', 'heavy.metal'],
	['NonFiction', 'non.fiction'],
	['Rap', 'hip.hop'],
	['NeoSoul', 'neo.soul'],
	['NuJazz', 'nu.jazz'],
	['DreamPop', 'dream.pop'],
	['IndiePop', 'indie.pop'],
	['IndieRock', 'indie.rock'],
	[/^J[\s\-]Pop$/i, 'jpop'],
	[/^K[\s\-]Pop$/i, 'jpop'],
	[/^J[\s\-]Rock$/i, 'jrock'],
	['Hardcore', 'hardcore.punk'],
	['Garage', 'garage.rock'],
	['Game', 'game.music'],
	[/^(?:Neo[\s\-\−\—\–]+Classical)$/i, 'neoclassical'],
	[/^(?:Bluesy[\s\-\−\—\–]+Rock)$/i, 'blues.rock'],
	[/^(?:Be[\s\-\−\—\–]+Bop)$/i, 'bebop'],
	[/^(?:Chill)[\s\-\−\—\–]+(?:Out)$/i, 'chillout'],
	[/^(?:Atmospheric)[\s\-\−\—\–]+(?:Black)$/i, 'atmospheric.black.metal'],
	['GoaTrance', 'goa.trance'],
	[/^Female\s+Vocal\w*$/i, 'female.vocalist'],
	['Contemporary R&B', 'contemporary.rhythm.and.blues'],
	[/^(?:Gothic[\-\s]Rock)$/i, 'rock, gothic'],
	['Free Jazz & Avant-Garde', 'free.jazz', 'avant.garde'],
	['electro synthwave new wave pop', 'electro', 'synthwave', 'new wave', 'pop'], // BC - STUB Rec.
	['Dans', 'dance'], // RO → EN
	['Alternativa', 'alternative'],
	// Country aliases
	['Canada', 'canadian'],
	['Australia', 'australian'],
	['New Zealand', 'new.zealander'],
	['Japan', 'japanese'],
	['JP', 'japanese'],
	['Taiwan', 'thai'],
	['China', 'chinese'],
	['Singapore', 'singaporean'],
	[/^(?:Russia|Russian\s+Federation|Россия|USSR|СССР)$/i, 'russian'],
	['Turkey', 'turkish'],
	['Israel', 'israeli'],
	['France', 'french'],
	['Germany', 'german'],
	['Spain', 'spanish'],
	['Italy', 'italian'],
	['Sweden', 'swedish'],
	['Norway', 'norwegian'],
	['Finland', 'finnish'],
	['Greece', 'greek'],
	[/^(?:Netherlands|Holland)$/i, 'dutch'],
	['Belgium', 'belgian'],
	['Luxembourg', 'luxembourgish'],
	['Denmark', 'danish'],
	['Switzerland', 'swiss'],
	['Austria', 'austrian'],
	['Portugal', 'portugese'],
	['Ireland', 'irish'],
	['Scotland', 'scotish'],
	['Iceland', 'icelandic'],
	[/^(?:Czech\s+Republic|Czechia)$/i, 'czech'],
	[/^(?:Slovak\s+Republic|Slovakia)$/i, 'slovak'],
	['Hungary', 'hungarian'],
	['Poland', 'polish'],
	['Estonia', 'estonian'],
	['Latvia', 'latvian'],
	['Lithuania', 'lithuanian'],
	['Moldova', 'moldovan'],
	['Armenia', 'armenian'],
	['Belarus', 'belarussian'],
	['Ukraine', 'ukrainian'],
	['Yugoslavia', 'yugoslav'],
	['Serbia', 'serbian'],
	['Slovenia', 'slovenian'],
	['Croatia', 'croatian'],
	['Macedonia', 'macedonian'],
	['Montenegro', 'montenegrin'],
	['Romania', 'romanian'],
	['Malta', 'maltese'],
	['Brazil', 'brazilian'],
	['Mexico', 'mexican'],
	['Argentina', 'argentinean'],
	['Jamaica', 'jamaican'],
	// Books
	['Beletrie', 'fiction'],
	['Satira', 'satire'],
	['Komiks', 'comics'],
	['Komix', 'comics'],
	// Removals
	['Indie Rock/Rock Pop'],
	['Unknown'],
	['Other'],
	['New'],
	['Ostatni'],
	['Knihy'],
	['Audioknihy'],
	['dsbm'],
	[/^(?:Audio\s*kniha|Audio\s*Book)$/i],
].concat(tmExcludedCountries.map(it => [it]));
const tmSplits = [
	['Alternative', 'Indie'],
	['Rock', 'Pop'],
	['Soul', 'Funk'],
	['Ska', 'Rocksteady'],
	['Jazz Fusion', 'Jazz Rock'],
	['Rock', 'Pop'],
	['Jazz', 'Funk'],
];
const tmAdditions = [
	[/^(?:(?:(?:Be|Post|Neo)[\s\-\−\—\–]*)?Bop|Modal|Fusion|Free[\s\-\−\—\–]+Improvisation|Modern\s+Creative|Jazz[\s\-\−\—\–]+Fusion|Big[\s\-\−\—\–]*Band)$/i, 'jazz'],
	[/^(?:(?:Free|Cool|Avant[\s\-\−\—\–]*Garde|Contemporary|Instrumental|Crossover|Modal|Mainstream|Modern|Soul|Smooth|Piano|Afro[\s\-\−\—\–]*Cuban)[\s\-\−\—\–]+Jazz)$/i, 'jazz'],
	[/^(?:Opera)$/i, 'classical'],
	[/\b(?:Chamber[\s\-\−\—\–]+Music)\b/i, 'classical'],
	[/\b(?:Orchestral[\s\-\−\—\–]+Music)\b/i, 'classical'],
	[/^(?:Symphony)$/i, 'classical'],
	[/^(?:Sacred\s+Vocal)\b/i, 'classical'],
	[/\b(?:Soundtracks?|Films?|Games?|Video|Series?|Theatre|Musical)\b/i, 'score'],
];
const tmRemovals = [
	'delete.this.tag',
	'live',
	'vinyl',
	'flac',
];
const tmPostSubstitutions = [
    [/^\.+|\.+$/g, ''],
    [/^(?:Alt\.)\s*(\w+)$/i, 'Alternative $1'],
    [/\b(?:Alt\.)(?=\s+)/i, 'Alternative'],
    [/^[3-9]0s$/i, '19$0'],
    [/^[0-2]0s$/i, '20$0'],
    [/\b(Psy)[\s\-\−\—\–]+(Trance|Core|Chill)\b/i, '$1$2'],
    [/\s*(?:[\'\’\`][Nn](?:\s+|[\'\’\`]\s*)|[\&\+]\s*)/, ' and '],
    [/[\s\-\−\—\–\_\.\,\~]+/g, '.'],
    [/[^\w\.]+/g, ''],
    [/^(?:singer\.and\.songwriter)$/i, 'singer.songwriter'],
];

class TagManager extends Array {
	// static get excludedCountries() {
	// 	return [
	// 		/\b(?:United\s+States|USA?)\b/,
	// 		/\b(?:United\s+Kingdom|(?:Great\s+)?Britain|England|GB|UK)\b/,
	// 		/\b(?:Europe|European\s+Union|EU)\b/,
	// 		/\b(?:Unknown)\b/,
	// 	]
	// }
	// static get presubstitutions() {
	// 	return [
	// 		[/\b(?:Singer\/Songwriter)\b/i, 'singer.songwriter'],
	// 		[/\b(?:Pop\/Rock)\b/i, 'pop.rock'],
	// 		[/\b(?:Folk\/Rock)\b/i, 'folk.rock'],
	// 		[/^(?:Psy\/Goa\s+Trance)$/i, 'psytrance, goa.trance'],
	// 		[/\s*,\s*(?:&\s*|and\s+)/i, ' & '],
	// 	]
	// }
	// static get substitutions() {
	// 	return [
	// 		[/^Pop\s*(?:[\-\−\—\–]\s*)?Rock$/i, 'pop.rock'],
	// 		[/^Rock\s*(?:[\-\−\—\–]\s*)?Pop$/i, 'pop.rock'],
	// 		[/^Rock\s+n\s+Roll$/i, 'rock.and.roll'],
	// 		['AOR', 'album.oriented.rock'],
	// 		[/^(?:Prog)\.?\s*(?:Rock)$/i, 'progressive.rock'],
	// 		[/^Synth[\s\-\−\—\–]+Pop$/i, 'synthpop'],
	// 		[/^World(?:\s+and\s+|\s*[&+]\s*)Country$/i, 'world.music', 'country'],
	// 		['World', 'world.music'],
	// 		[/^(?:Singer(?:\s+and\s+|\s*[&+]\s*))?Songwriter$/i, 'singer.songwriter'],
	// 		[/^(?:R\s*(?:[\'\’\`][Nn](?:\s+|[\'\’\`]\s*)|&\s*)B|RnB)$/i, 'rhythm.and.blues'],
	// 		[/^(?:Alternat(?:iv)?e)\s+(?:R\s*(?:[\'\’\`][Nn](?:\s+|[\'\’\`]\s*)|&\s*)B|RnB)$/i, 'alternative.rhythm.and.blues'],
	// 		[/\b(?:Soundtracks?)$/i, 'score'],
	// 		['Electro', 'electronic'],
	// 		//['Metal', 'heavy.metal'],
	// 		['NonFiction', 'non.fiction'],
	// 		['Rap', 'hip.hop'],
	// 		['NeoSoul', 'neo.soul'],
	// 		['NuJazz', 'nu.jazz'],
	// 		[/^J[\s\-]Pop$/i, 'jpop'],
	// 		[/^K[\s\-]Pop$/i, 'jpop'],
	// 		[/^J[\s\-]Rock$/i, 'jrock'],
	// 		['Hardcore', 'hardcore.punk'],
	// 		['Garage', 'garage.rock'],
	// 		[/^(?:Neo[\s\-\−\—\–]+Classical)$/i, 'neoclassical'],
	// 		[/^(?:Bluesy[\s\-\−\—\–]+Rock)$/i, 'blues.rock'],
	// 		[/^(?:Be[\s\-\−\—\–]+Bop)$/i, 'bebop'],
	// 		[/^(?:Chill)[\s\-\−\—\–]+(?:Out)$/i, 'chillout'],
	// 		[/^(?:Atmospheric)[\s\-\−\—\–]+(?:Black)$/i, 'atmospheric.black.metal'],
	// 		['GoaTrance', 'goa.trance'],
	// 		[/^Female\s+Vocal\w*$/i, 'female.vocalist'],
	// 		['Contemporary R&B', 'contemporary.rhythm.and.blues'],
	// 		[/^(?:Gothic[\-\s]Rock)$/i, 'rock, gothic'],
	// 		['Free Jazz & Avant-Garde', 'free.jazz', 'avant.garde'],
	// 		['electro synthwave new wave pop', 'electro', 'synthwave', 'new wave', 'pop'], // BC - STUB Rec.
	// 		['Dans', 'dance'], // RO → EN
	// 		['Alternativa', 'alternative'],
	// 		// Country aliases
	// 		['Canada', 'canadian'],
	// 		['Australia', 'australian'],
	// 		['New Zealand', 'new.zealander'],
	// 		['Japan', 'japanese'],
	// 		['Taiwan', 'thai'],
	// 		['China', 'chinese'],
	// 		['Singapore', 'singaporean'],
	// 		[/^(?:Russia|Russian\s+Federation|Россия|USSR|СССР)$/i, 'russian'],
	// 		['Turkey', 'turkish'],
	// 		['Israel', 'israeli'],
	// 		['France', 'french'],
	// 		['Germany', 'german'],
	// 		['Spain', 'spanish'],
	// 		['Italy', 'italian'],
	// 		['Sweden', 'swedish'],
	// 		['Norway', 'norwegian'],
	// 		['Finland', 'finnish'],
	// 		['Greece', 'greek'],
	// 		[/^(?:Netherlands|Holland)$/i, 'dutch'],
	// 		['Belgium', 'belgian'],
	// 		['Luxembourg', 'luxembourgish'],
	// 		['Denmark', 'danish'],
	// 		['Switzerland', 'swiss'],
	// 		['Austria', 'austrian'],
	// 		['Portugal', 'portugese'],
	// 		['Ireland', 'irish'],
	// 		['Scotland', 'scotish'],
	// 		['Iceland', 'icelandic'],
	// 		[/^(?:Czech\s+Republic|Czechia)$/i, 'czech'],
	// 		[/^(?:Slovak\s+Republic|Slovakia)$/i, 'slovak'],
	// 		['Hungary', 'hungarian'],
	// 		['Poland', 'polish'],
	// 		['Estonia', 'estonian'],
	// 		['Latvia', 'latvian'],
	// 		['Lithuania', 'lithuanian'],
	// 		['Moldova', 'moldovan'],
	// 		['Armenia', 'armenian'],
	// 		['Belarus', 'belarussian'],
	// 		['Ukraine', 'ukrainian'],
	// 		['Yugoslavia', 'yugoslav'],
	// 		['Serbia', 'serbian'],
	// 		['Slovenia', 'slovenian'],
	// 		['Croatia', 'croatian'],
	// 		['Macedonia', 'macedonian'],
	// 		['Montenegro', 'montenegrin'],
	// 		['Romania', 'romanian'],
	// 		['Malta', 'maltese'],
	// 		['Brazil', 'brazilian'],
	// 		['Mexico', 'mexican'],
	// 		['Argentina', 'argentinean'],
	// 		['Jamaica', 'jamaican'],
	// 		// Books
	// 		['Beletrie', 'fiction'],
	// 		['Satira', 'satire'],
	// 		['Komiks', 'comics'],
	// 		['Komix', 'comics'],
	// 		// Removals
	// 		['Indie Rock/Rock Pop'],
	// 		['Unknown'],
	// 		['Other'],
	// 		['New'],
	// 		['Ostatni'],
	// 		['Knihy'],
	// 		['Audioknihy'],
	// 		['dsbm'],
	// 		[/^(?:Audio\s*kniha|Audio\s*Book)$/i],
	// 	].concat(TagManager.excludedCountries.map(it => [it]))
	// }
	// static get splits() {
	// 	return [
	// 		['Alternative', 'Indie'],
	// 		['Rock', 'Pop'],
	// 		['Soul', 'Funk'],
	// 		['Ska', 'Rocksteady'],
	// 		['Jazz Fusion', 'Jazz Rock'],
	// 		['Rock', 'Pop'],
	// 		['Jazz', 'Funk'],
	// 	]
	// }
	// static get additions() {
	// 	return [
	// 		[/^(?:(?:(?:Be|Post|Neo)[\s\-\−\—\–]*)?Bop|Modal|Fusion|Free[\s\-\−\—\–]+Improvisation|Modern\s+Creative|Jazz[\s\-\−\—\–]+Fusion|Big[\s\-\−\—\–]*Band)$/i, 'jazz'],
	// 		[/^(?:(?:Free|Cool|Avant[\s\-\−\—\–]*Garde|Contemporary|Instrumental|Crossover|Modal|Mainstream|Modern|Soul|Smooth|Piano|Afro[\s\-\−\—\–]*Cuban)[\s\-\−\—\–]+Jazz)$/i, 'jazz'],
	// 		[/^(?:Opera)$/i, 'classical'],
	// 		[/\b(?:Chamber[\s\-\−\—\–]+Music)\b/i, 'classical'],
	// 		[/\b(?:Orchestral[\s\-\−\—\–]+Music)\b/i, 'classical'],
	// 		[/^(?:Symphony)$/i, 'classical'],
	// 		[/^(?:Sacred\s+Vocal)\b/i, 'classical'],
	// 		[/\b(?:Soundtracks?|Films?|Games?|Video|Series?|Theatre|Musical)\b/i, 'score'],
	// 	]
	// }
	// static get removals() {
	// 	return [
	// 		'delete.this.tag',
	// 	]
	// }

	constructor(...tags) {
		super();
		if (tags.length > 0) this.add(...tags);
	}

	add(...tags) {
		let added = 0;
		for (let tag of tags.map(tag => tag.trim()).filter(Boolean)) {
			if (typeof tag == 'string') tag = qbGenreToEnglish(tag); else continue;
			for (var k of tmPresubstitutions) if (k[0].test(tag)) tag = tag.replace(...k);
			tag.split(/(?:[\,\/\;\>\|]|\r?\n)+/).map(tag => tag.trim().toASCII()
					.replace(/\s*(?:\(.*?\)|\[.*?\]|\{.*?\})/g, '').replace(/\s+/g, ' ')).forEach(function(tag) {
				//tag = qbGenreToEnglish(tag);
				if (tag.length <= 0 || tag == '?') return;
				const test = obj => typeof obj == 'string' && tag.toLowerCase() == obj.toLowerCase()
					|| obj instanceof RegExp && obj.test(tag);
				for (k of tmSubstitutions) if (test(k[0])) {
					if (k.length > 1) added += this.add(...k.slice(1));
					return;
				}
				for (k of tmAdditions) if (test(k[0])) added += this.add(...k.slice(1));
				for (k of tmSplits) if ([[0, 1], [1, 0]].some(n =>
						new RegExp('^' + k[n[0]] + '(?:\\s+and\\s+|\\s*[&+]\\s*)' + k[n[1]] + '$', 'i').test(tag))) {
					added += this.add(k[0], k[1]);
					return;
				}
				tag = tmPostSubstitutions.reduce((tag, subst) => tag.replace(...subst), tag).toLowerCase();
				if (tag.length < 2 || tmRemovals.some(test) || this.includes(tag)) return;
				this.push(tag);
				++added;
			}.bind(this));
		}
		return added;
	}
	toString() { return this.join(', ') }
	toStringSorted() { return Array.from(this).sort().join(', ') }
};