Greasy Fork is available in English.

REDLib

RED LIB!

このスクリプトは単体で利用できません。右のようなメタデータを含むスクリプトから、ライブラリとして読み込まれます: // @require https://greasyfork.org/scripts/388413-redlib/code/REDLib.js?version=757524

質問やレビューの投稿はこちらへ、スクリプトの通報はこちらへどうぞ。
// ==UserScript==
// @name         REDLib
// @namespace    https://greasyfork.org/cs/users/321857-anakunda
// @version      1.88
// @author       Anakunda
// @iconURL      https://redacted.ch/favicon.ico
// @grant        RegExp
// @grant        GM_xmlhttpRequest
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_deleteValue
// @grant        GM_log
// @require      https://greasyfork.org/scripts/388280-xpathlib/code/XPathLib.js
// @connect      www.whatsmyip.org
// @connect      whatsmyip.org
// ==/UserScript==

const red_url_base = 'https://redacted.ch/';
const size_tolerance = 5; // maximum deviation of torrent size in % that is considered identical
const use_api = false;
const qobuzGenresRating = {
  0: [
	'Acid Jazz', 'Acid jazz',
	'Alternative & Indie', 'Alternatif et Indé', 'Alternativ und Indie', 'Alternativa & Indie', 'Musica alternativa e indie', 'Alternative en Indie',
	'Ambient', 'Ambientes',
	'Blues',
	'Blues/Country/Folk', 'Blues/country/folk',
	'Chill-out', 'Downtempo',
	'Crossover',
	'Dance',
	'Disco',
	'Dub',
	'Electronic', 'Electronic/Dance', 'Électronique', 'Electrónica', 'Elettronica',
	'Folk', 'Folk/Americana',
	'Funk',
	'Hard Rock', 'Hard rock', 'Hardrock',
	'Indie Pop', 'Pop indé', 'Indie-Pop', 'Pop indie', 'Indie pop', 'Indiepop',
	'Lounge',
	'Metal',
	'Pop',
	'Pop/Rock',
	'Progressive Rock', 'Rock progressif', 'Rock progresivo', 'Rock progressivo', 'Progressieve rock',
	'Punk / New Wave', 'Punk - New Wave', 'Punk – New Wave', 'Punk/New wave', 'Punk en New Wave',
	'R&B',
	'Reggae',
	'Rock',
	'Rockabilly',
	'Ska & Rocksteady', 'Ska e rocksteady', 'Ska en Rocksteady',
	'Soul',
	'Soul/Funk/R&B', 'R&B/Soul',
  ],
  0.10: [
	//'Afrobeat',
	'Contemporary Jazz', 'Jazz contemporain', 'Modern Jazz', 'Jazz contemporáneo', 'Jazz contemporaneo', 'Moderne jazz',
	'Dancehall',
	'French Artists', 'Interprètes de chanson française', 'Französische Chanson-Sänger', 'Intérpretes de chanson francesa', 'Artisti francesi', 'Zangers van Franse chansons',
	'French Music', 'Chanson française', 'Französischer Chanson', 'Chanson francesa', 'Musica francese', 'Franse chansons', 'Variété francophone',
	'French Rock', 'Rock français', 'Französischer Rock', 'Rock francés', 'Rock francese', 'Franse rock',
	'Irish Celtic', 'Irish celtic', 'Irisch-keltische Musik', 'Música celta irlandesa', 'Musica celtica irlandese', 'Iers Keltisch',
	'Irish Pop Music', 'Irish popmusic', 'Irische Popmusik', 'Música pop irlandesa', 'Musica pop irlandese', 'Ierse popmuziek',
	'Jazz Fusion & Jazz Rock', 'Jazz fusion & Jazz rock', 'Jazz Fusion & Jazzrock', 'Jazz fusión & Jazz rock', 'Fusion & Jazz rock', 'Jazz fusion en jazz rock',
	'Jazz',
	'Latin Jazz', 'Latin jazz',
	'Vocal Jazz', 'Jazz vocal', 'Jazzgesang', 'Vocal jazz', 'Vocale jazz',
  ],
  0.20: [
	'Africa', 'Afrique', 'Afrika', 'África',
	'Asia', 'Asie', 'Asien', 'Azië',
	'Bebop', 'Be Bop',
	'Bossa Nova & Brazil', 'Bossa Nova & Brésil', 'Bossa Nova & brasilianische Musik', 'Bossa nova & Brasil', 'Bossa nova e musica brasiliana ', 'Bossanova en Brazilië',
	'Celtic', 'Celtique', 'Keltische Musik', 'Celta', 'Musica celtica', 'Keltisch',
	'Cool Jazz', 'Cool jazz', 'Cooljazz',
	'Country',
	'Crooners', 'Crooner', 'Musica crooner',
	'Dixieland', 'Dixie',
	'Drum & Bass', 'Drum \'n\' bass',
	'Eastern Europe', 'Europe de l\'Est', 'Osteuropa', 'Europa del Este', 'Europa dell\'est', 'Oost-Europa',
	//'Europe', 'Europa',
	'Fado',
	'Film Soundtracks', 'Bandes originales de films', 'Original Soundtrack', 'Bandas sonoras de cine', 'Colonne sonore', 'Originele soundtracks',
	'Flamenco',
	'Free Jazz & Avant-Garde', 'Free jazz & Avant-garde', 'Free Jazz & Avantgarde', 'Free jazz & Vanguardia', 'Free jazz et jazz d\'avanguardia', 'Free jazz & Avant-garde jazz',
	'Greece', 'Grèce', 'Griechenland', 'Grecia', 'Griekenland',
	'Gypsy', 'Gipsy', 'Musik der Roma', 'Gitano', 'Zigeunermuziek',
	'Gypsy Jazz', 'Jazz manouche', 'Gypsy-Jazz', 'Gipsy jazz',
	'House',
	'Indian Music', 'Musique indienne', 'Indische Musik', 'Música india', 'Musica indiana', 'Indiase muziek',
	'Ireland', 'Irlande', 'Irland', 'Irlanda', 'Ierland',
	'Italy', 'Italie', 'Italien', 'Italia', 'Italië',
	'Latin America', 'Amérique latine', 'Lateinamerika', 'Latinoamérica', 'America latina', 'Latijns-Amerika',
	'Maghreb', 'Magreb', 'Noord-Afrika',
	'North America', 'Amérique du Nord', 'Nordamerika', 'Norteamérica', 'Amercia del nord', 'Noord-Amerika',
	'Oriental Music', 'Orient', 'Oriente', 'Musica orientale', 'Oosters',
	'Portugal', 'Portogallo',
	'Ragtime',
	'Raï',
	'Russia', 'Russie', 'Russland', 'Rusia', 'Rusland',
	'Salsa',
	'Scottish', 'Ecosse', 'Schottland', 'Escocia', 'Scozia', 'Schotland',
	'Soundtracks', 'Film', 'Cine', 'Cinema', 'Soundtrack',
	'Spain', 'Espagne', 'Spanien', 'España', 'Spagna', 'Spanje',
	'Swiss Folk Music', 'Musique folklorique Suisse', 'Schweizer Volksmusik', 'Música folclórica suiza', 'Musica folclorica svizzera', 'Zwitserse volksmuziek',
	'Tango',
	'Traditional Jazz & New Orleans', 'Jazz traditionnel & New Orleans', 'Klassischer Jazz & New-Orleans-Jazz', 'Jazz tradicional & Nueva Orleans', 'Jazz tradizionale & New Orleans', 'Traditionele jazz en dixieland',
	'World', 'Musiques du monde', 'Aus aller Welt', 'World music', 'Wereldmuziek',
	'Yiddish & Klezmer', 'Jiddische Musik & Klezmer', 'Musica yiddish e klezmer', 'Jiddisch en klezmer',
	'Zouk & Antilles', 'Zouk & Musik von den Antillen', 'Zouk & Antillas', 'Musica zouk e Antille', 'Zouk en Antilliaans',
  ],
  0.30: [
	'Ambient/New Age', 'Ambiance', /*'Lounge', 'Ambientes', */'Musica d\'ambiente/New Age', 'Ambient / New Age / Easy Listening',
	'Christmas Music', 'Musiques de Noël', 'Weihnachtsmusik', 'Músicas navideñas', 'Canzoni di Natale', 'Kerstmuziek',
	'International Pop', 'Variété internationale', 'Internationaler Pop', 'Variété internacional', 'Pop internazionale', 'Internationaal variété',
	'Musical Theatre', 'Comédies musicales', 'Musical', 'Comedias musicales', 'Musicals',
	'New Age', 'Musica new Age', 'New age',
	'Relaxation', 'Entspannung', 'Relajación', 'Musica rilassante', 'Ontspanning',
	'Retro French Music', 'Chanson française rétro', 'Französisches Retro-Chanson', 'Chanson francesa retro', 'Musica francese retrò', 'Oude Franse chansons',
	'Trance',
	'Turkey', 'Turquie', 'Türkei', 'Turquía', 'Turchia', 'Turkije',
	'Trip Hop', 'Triphop',
	'TV Series', 'Séries TV', 'TV-Serien', 'Series de televisión', 'Serie TV', 'Tv-series',
	'Video Games', 'Jeux vidéo', 'Computerspiele', 'Vídeojuegos', 'Video Giochi', 'Videogames',
  ],
  0.40: [
	'Gospel',
	'Military Music', 'Musique militaire', 'Militärmusik', 'Música militar', 'Musica militare', 'Militaire muziek',
  ],
  // Hide these
  0.50: [
	'Accordion', 'Accordéon', 'Akkordeon', 'Acordeón', 'Fisarmonica', 'Accordeon',
	'Art Songs', 'Lieder', 'Kunstlieder', 'Liederen',
	'Art Songs, Mélodies & Lieder', 'Mélodies & Lieder', 'Französische Mélodies und Kunstlieder', 'Liederen',
	'Ballets', 'Ballett', 'Balletti', 'Balletten',
	'Bawdy songs', 'Chansons paillardes', 'Canciones gamberras', 'Canzoni licenziose', 'Schuine liedjes',
	'Cantatas (sacred)', 'Cantates sacrées', 'Geistliche Kantaten', 'Cantatas sacras', 'Cantate sacre', 'Religieuze cantates',
	'Cantatas (secular)', 'Cantates (profanes)', 'Kantaten (weltlich)', 'Cantatas (profanas)', 'Cantate (profane)', 'Cantates (wereldlijk)',
	'Cello Concertos', 'Concertos pour violoncelle', 'Cellokonzerte', 'Conciertos para violonchelo', 'Concerti per violoncello', 'Concerten voor cello',
	'Cello Solos', 'Violoncelle solo', 'Cellosolo', 'Violonchelo solo', 'Assoli per violoncello', 'Cello solo',
	'Chamber Music', 'Musique de chambre', 'Kammermusik', 'Música de cámara', 'Musica da camera', 'Kamermuziek',
	'Children', 'Enfants', 'Kinder', 'Infantil', 'Infanzia', 'Kinderen',
	'Choirs (sacred)', 'Chœurs sacrés', 'Geistliche Chormusik', 'Coros sacros', 'Cori sacri', 'Religieuze koormuziek',
	'Choral Music (Choirs)', 'Musique chorale (pour chœur)', 'Chorwerk (für den Chor)', 'Música coral (para coro)', 'Musica corale', 'Koormuziek',
	'Cinema Music', 'Musiques pour le cinéma', 'Filmmusik', 'Bandas sonoras', 'Musiche per il cinema', 'Soundtrack',
	'Classical', 'Classique', 'Klassik', 'Clásica', 'Classica', 'Klassiek',
	'Concertos for trumpet', 'Concertos pour trompette', 'Trompetenkonzerte', 'Conciertos para trompeta', 'Concerti per tromba', 'Concerten voor trompet',
	'Concertos for wind instruments', 'Concertos pour instruments à vent', 'BläserKonzerte', 'Conciertos para instrumentos de viento', 'Concerti per strumenti a fiato', 'Concerten voor blaasinstrumenten',
	'Concertos', 'Musique concertante', 'Instrumentalmusik', 'Música concertante', 'Musica concertante', 'Concertmuziek',
	'Duets', 'Duos', 'Duette', 'Dúos', 'Duetti', 'Duo´s',
	'Dutch', 'Néerlandais', 'Niederländisch', 'Neerlandés', 'Olandese', 'Nederlands',
	'Educational', 'Educatif', 'Bildung', 'Educativa', 'Musica educativa', 'Educatief',
	'Educational', 'Pédagogie', 'Pädagogik', 'Pedagogía', 'Musica educativa', 'Pedagogiek',
	//'Electronic', 'Musique électronique', 'Elektronische Musik', 'Música electrónica', 'Musica elettronica', 'Elektronische muziek',
	'English', 'Anglais', 'Englisch', 'Inglés', 'Inglese', 'Engels',
	//'Experimental', 'Électronique ou concrète', 'Elektronische Musik oder Musique concrète', 'Electrónica o musique concrète', 'Musica elettronica/concreta', 'Elektronische muziek of Musique Concrète',
	'French', 'Français', 'Französisch', 'Francés', 'Francese', 'Frans',
	'Full Operas', 'Intégrales d\'opéra', 'Gesamtaufnahmen von Opern', 'Integrales de ópera', 'Opere integrali', 'Volledige opera\'s',
	'German', 'Allemand', 'Deutsch', 'Alemán', 'Tedesco', 'Duits',
	'Germany', 'Allemagne', 'Deutsche Musik', 'Alemania', 'Germania', 'Duitsland',
	'Historical Documents', 'Documents historiques', 'Historische Dokumente', 'Documentos históricos', 'Documenti storici', 'Historische documenten',
	'Humour', 'Humor', 'Umorismo',
	'Humour/Spoken Word', 'Comedy/Other', 'Diction', 'Hörbücher', 'Audiolibros', 'Spoken Word', 'Cabaret/ Komedie / Luisterboek',
	'Karaoke', 'Karaoké',
	'Keyboard Concertos', 'Concertos pour clavier', 'Klavierkonzerte', 'Conciertos para tecla', 'Concerti per tastiera', 'Concerten voor klavier',
	'Lieder (German)', 'Lieder (Allemagne)', 'Kunstlieder (Deutschland)', 'Lieder (Alemania)', 'Lieder (Germania)', 'Liederen (Duitsland)',
	'Literature', 'Littérature', 'Literatur', 'Literatura', 'Letteratura', 'Literatuur',
	'Masses, Passions, Requiems', 'Messes, Passions, Requiems', 'Messen, Passionen, Requiems', 'Misas, Pasiones, Réquiems', 'Messe, Passioni, Requiem', 'Missen, passies, requiems',
	'Mélodies (England)', 'Mélodies (Angleterre)', 'Mélodies (Inglaterra)', 'Mélodies (Inghilterra)', 'Liederen (Engeland)',
	'Mélodies (French)', 'Mélodies (France)', 'Französische Mélodies (Frankreich)', 'Mélodies (Francia)', 'Liederen (Frankrijk)',
	'Mélodies (Northern Europe)', 'Mélodies (Europe du Nord)', 'Mélodies (Nordeuropa)', 'Mélodies (Europa del Norte)', 'Mélodies (Europa del nord)', 'Liederen (Noord-Europa)',
	'Mélodies', 'Liederen',
	//'Minimal Music', 'Musique minimaliste', 'Música minimalista', 'Musica minimalista', 'Minimalistische muziek',
	'Music by vocal ensembles', 'Musique pour ensembles vocaux', 'Musik für Vokalensembles', 'Música para conjuntos vocales', 'Musica per insiemi vocali', 'Muziek voor vocale ensembles',
	'Musique Concrète', 'Musique concrète', 'Musique concréte', 'Musica concreta',
	'Opera Extracts', 'Extraits d\'opéra', 'Opernauszüge', 'Fragmentos de ópera', 'Estratti d\'opera', 'Operafragmenten',
	'Opera', 'Opéra', 'Oper', 'Ópera',
	'Operettas', 'Opérette', 'Operette', 'Opereta', 'Operetta',
	'Oratorios (secular)', 'Oratorios profanes', 'Weltliche Oratorien', 'Oratorios profanos', 'Oratori profani', 'Wereldlijke oratoria',
	'Overtures', 'Ouvertures', 'Ouvertüren', 'Oberturas', 'Overture',
	'Quartets', 'Quatuors', 'Quartette', 'Cuartetos', 'Quartetti', 'Kwartetten',
	'Quintets', 'Quintettes', 'Quintette', 'Quintetos', 'Quintetti', 'Kwintetten',
	'Rap', 'Hip-Hop', 'Rap/Hip-Hop',
	'Sacred Oratorios', 'Oratorios sacrés', 'Geistliche Oratorien', 'Oratorios sacros', 'Oratori sacri',
	'Sacred Vocal Music', 'Musique vocale sacrée', 'Geistliche Vokalmusik', 'Música vocal sacra', 'Musica vocale sacra', 'Religieuze vocale muziek',
	'Secular Vocal Music', 'Musique vocale profane', 'Weltliche Vokalmusik', 'Música vocal profana', 'Musica vocale profana', 'Wereldlijke vocale muziek',
	'Schlager',
	'Solo Piano', 'Piano solo', 'Klaviersolo', 'Assoli per pianoforte',
	'Stimmungsmusik ', 'Stimmungsmusik',
	'Stories and Nursery Rhymes', 'Contes et comptines', 'Märchen und Kinderreime', 'Cuentos & canciones infantiles', 'Racconti e filastrocche', 'Sprookjes en vertellingen',
	'Symphonic Music', 'Musique symphonique', 'Symphonieorchester', 'Música sinfónica', 'Musica sinfonica', 'Symfonische muziek',
	'Symphonic Poems', 'Poèmes symphoniques', 'Symphonische Dichtung', 'Poemas sinfónicos', 'Poemi sinfonici', 'Symfonische gedichten',
	'Symphonies', 'Symphonien', 'Sinfonías', 'Sinfonie', 'Symfonieën',
	'Techno',
	'Theatre Music', 'Musique de scène', 'Intermezzi', 'Música escénica', 'Musiche di scena', 'Toneelmuziek',
	'Trios', 'Tríos', 'Trii', 'Trio´s',
	'Violin Concertos', 'Concertos pour violon', 'Violinkonzerte', 'Conciertos para violín', 'Concerti per violino', 'Concerten voor viool',
	'Violin Solos', 'Violon solo', 'Violinensolo', 'Violín solo', 'Assoli per violino', 'Viool solo',
	'Vocal Music (Secular and Sacred)', 'Musique vocale (profane et sacrée)', 'Vokalmusik (weltlich und geistlich)', 'Música vocal (profana y sacra)', 'Musica vocale (sacra e profana)', 'Vocale muziek (wereldlijk en religieus)',
	'Vocal Recitals', 'Récitals vocaux', 'Gesangsrezitale', 'Recitales vocales', 'Recital vocali', 'Vocale recitals',
	'Volksmusik',
  ],
};

function compute_rls_quality(article) {
  var this_year = new Date().getFullYear();
  article.quality = 1;
  var genres = [];
  if (article.genre) genres.push(article.genre);
  if (article.style) genres.push(article.style);
  if (Array.isArray(article.genres)) genres.push(...article.genres);
  if (Array.isArray(article.styles)) genres.push(...article.styles);
  for (var discount in qobuzGenresRating) {
	if (Array.isArray(qobuzGenresRating[discount]) && discount != 0
		&& genres.some(genre => qobuzGenresRating[discount].includes(genre))) article.quality -= discount;
  }
  if (article.quality >= 1) {
	if (genres.some(g => /\b(?:French)\b/i.test(g))) article.quality -= 0.15;
	if (/\b(?:World)\b/.test(article.category)
		|| genres.some(g => /\b(?:World|Latin|African|Asia|Flamenco|Tango|Bossa\s*Nova|Brazil(?:ian)?)\b/.test(g))) {
	  article.quality -= 0.15;
	}
	if (genres.some(g => /\b(?:Country)\b/.test(g))) article.quality -= 0.20;
	if (/^New\s*Age$/i.test(article.category)
		|| genres.some(g => /\b(?:New\s*Age|Easy\s+Listening|Meditation|Relax\w*|Spiritual|Gospel|Worship|Holiday)\b/i.test(g))) {
	  article.quality -= 0.30;
	}
	if (genres.some(g => /\b(?:Kids|Children)\b/i.test(g))) article.quality -= 0.40;
	if (genres.some(g => /\b(?:Humour|Spoken\s+Word)\b/i.test(g))) article.quality -= 0.60;
	if (genres.some(g => /\b(?:Hip[\-\s]?Hop|T?Rap)\b/i.test(g))) article.quality -= 0.60;
	if (genres.some(g => /\b(?:Techno|Tech(?:\.\s*|-)House)\b/i.test(g))) {
	  article.quality -= 0.50
	} else if (genres.some(g => /\b(?:House)\b/i.test(g))) {
	  article.quality -= 0.20;
	}
	if (genres.some(function(genre) {
	  return /\b(?:Classical|Symphonic|Concertos?|Concerten|Chamber|Solo\s+Piano|Choral|Opera|Symphonies|Cantatas|Classique|Klaviersolo|Opéra|Klassik|Duets)\b/i.test(genre)
	  && !/\b(?:Modern\s+Classical|Symphonic\s+Metal)\b/i.test(genre);
	})) {
	  article.quality -= 0.50;
	}
	if (article.release_type == 3) article.quality -= 0.25; // soundtrack
  }
  if (article.bd <= 16 && article.category == 'Jazz') article.quality -= 0.15;
  if (article.release_type == 9) article.quality -= 0.10; // single
  if (is_va(article)) article.quality -= 0.30; // VA compilation
  if (article.size) {
	if (article.size < 100) article.quality -= 0.05;
	//else if (article.size < 150) { article.quality -= 0.20; }
  }
  if (article.album_date && article.album_date.getFullYear() > 0
	  && article.album_date.getFullYear() < this_year) {
	if (article.album_date.getFullYear() >= this_year - 1) { article.quality -= 0.08 }
	else if (article.album_date.getFullYear() >= this_year - 2) { article.quality -= 0.20 }
	else { article.quality -= 0.30 }
  }
  if (['CD'].includes(article.media)) article.quality -= 0.30;
  if (['Vinyl', 'SACD', 'DVD', 'Blu-Ray'].includes(article.media)) article.quality -= 0.40;
  if (!article.album_date || isNaN(article.album_date)) article.quality -= 0.60;
  if (/\s*\[[^\[\]]*\bMQA\b[^\[\]]*\]$/.test(article.title)) article.quality = -1;
  if (/\b(?:discography|vinyl\s+collection)\b/i.test(article.title)) article.quality = -1;
}

function build_basic_red_url(article, safe = false) {
  var m, result = red_url_base + 'torrents.php?';
  //var leftouts = '\b(?:Live|Remastered|Remaster|Remasterizado|Musique originale|Soundtrack|Anniversary|Deluxe|Limited)\b';
  if (article.artist && !is_va(article)) {
	var artist = article.get_minimal_artist();
	if (safe) artist = artist.replace(/[\&\/].*$/, '');
	result += '&artistname=' + encodeURIComponent(artist);
  }
  if (article.album) {
	var album = article.album;
	if (safe) {
	  if (/\b(\d{4}-\d{2}-\d{2})\b/.test(article.album)) album = RegExp.$1;
	  album = album.replace(/\s+\([^\(\)]+\)\s*$/, '').replace(/\s+\[[^\[\]]+\]\s*$/, '')
	  	.replace(/\s+\{[^\{\}]+\}\s*$/, '');
	}
	result += '&groupname=' + encodeURIComponent(album);
  }
  if (!safe && article.album_date) result += '&year=' + article.album_date.getFullYear();
  if (!safe && article.release_date) result += '&remasteryear=' + article.release_date.getFullYear();
  if (!safe && article.release_type) result += '&releasetype=' + article.release_type;
  if (!safe && article.media) result += '&media=' + article.media;
  if (!safe && article.bd) result += '&format=FLAC';
  if (!safe && article.bd == 16) result += '&encoding=Lossless';
  if (!safe && article.bd == 24) result += '&encoding=24bit+Lossless';
  return result.concat('&order_by=time&order_way=desc&action=advanced&group_results=1');
}

function query_red(response) {
  if (response.status != 200 || response.readyState != 4) return;
  var parser = new DOMParser();
  var html = parser.parseFromString(response.responseText, "text/html");
  if (html == null || html.querySelector('body#torrents') == null) return; // not having access to site (not login)
  var article = response.context;
  if (findNode('//h2[text()="Your search did not match anything."]', html) != null) {
	article.redacted_status = 3;
	render_red_status(article);
	return;
  }
  var same_format_present = false, group_exists = false, node;
  const edition_parser = /\−(?:\s+(?:(\d{4})\s+-|Unconfirmed Release\s+\/))?(?:\s+(.*)\s+\/)?\s+(CD|DVD|Vinyl|Soundboard|SACD|DAT|Cassette|WEB|Blu-Ray)\s*$/;
  const format_parser = /\bFLAC\s*\/\s*(?:(\d+)[\s\-]?bits?\s+)?Lossless\b/i;
  var results = findNodes('//table[@id="torrent_table"]/tbody/tr[@class][td[@class="edition_info"]/strong]', html);
  //var results = html.querySelectorAll('table#torrent_table > tbody > tr[class]:has(:scope > td.edition_info > strong)');
  outer_block: while (article.redacted_status == undefined && (node = results != null && results.iterateNext()) != null) {
  //for (node of results) {
	//if (article.redacted_status != undefined) break;
	group_exists = true;
	let matches = node.textContent.match(edition_parser);
	if (matches == null) {
	  console.log('WARNING: unhandled edition header> "' + node.textContent + '"');
	  continue;
	}
	if (article.media ? matches[3] != article.media : !['CD', 'WEB'].includes(matches[3])) continue;
	if (article.release_date && article.release_date.getFullYear() > 0
		&& matches[1] && parseInt(matches[1]) != article.release_date.getFullYear()) continue;
	var upload = node;
	while (article.redacted_status == undefined && (upload = upload.nextElementSibling) != null
		   && upload.className != 'group' && upload.children[0].className != 'edition_info') {
	  if (upload.childElementCount != 7) {
		console.log('WARNING: unexpected structure of release> childElementCount=' +
			upload.childElementCount + ', row="' + node.textContent + '"');
		break;
	  }
	  let upload_format = upload.children[0].children[1].textContent;
	  let upload_size = get_size_from_string(upload.children[3].textContent);
	  if (upload_size <= 0) {
		console.log('Unexpected release size for torrents view: ' + upload.children[3].textContent);
	  }
	  matches = upload_format.match(format_parser);
	  if (matches == null) continue;
	  function test_size(status) {
		if (article.size && upload_size > 0) {
		  let deviation = Math.abs(article.size / upload_size - 1) * 100;
		  let difference = Math.abs(article.size - upload_size);
		  if (deviation < size_tolerance) {
			article.redacted_status = status;
			article.size_difference = difference;
			article.size_deviation = deviation;
			return true;
		  }
		}
		return false;
	  }
	  if (matches[1] ? article.bd == matches[1] : article.bd <= 16) { // same format & BD
		same_format_present = true;
		if (test_size(-3)) break outer_block;
	  } else if (matches[1] && (!article.bd || matches[1] > article.bd)) { // RED higher resolution than SP
		article.redacted_status = -3; // always consider uploaded if higher quality exists
		break outer_block;
	  } else if ((!article.bd || article.bd > 16) && test_size(-1)) break outer_block;
	}
  }
  if (article.redacted_status == undefined) article.redacted_status = same_format_present ? -1 : group_exists ? 1 : 2;
  if (article.redacted_status != undefined) render_red_status(article);
}

function build_api_url(article) {
  var m, result = red_url_base + 'ajax.php?action=browse';
  //var leftouts = '\b(?:Live|Remastered|Remaster|Remasterizado|Musique originale|Soundtrack|Anniversary|Deluxe|Limited)\b';
  if (!is_va(article)) {
	var artist = article.get_minimal_artist();
	artist = artist.replace(/[\&\/].*$/, '');
	result += '&artistname=' + encodeURIComponent(artist);
  }
  if (article.album) {
	var album = article.album;
	if ((m = article.album.match(/\b(\d{4}-\d{2}-\d{2})\b/)) != null) album = m[1];
	result += '&groupname=' + encodeURIComponent(album);
  }
  return result.concat('&order_by=time&order_way=desc');
}

function query_red_api(response) {
  if (response.status != 200 || response.readyState != 4 || response.response.status != 'success') return;
  var article = response.context;
  var same_format_present = false, group_exists = false, node;
  outer_block: for (var result of response.response.response.results) {
	//if (article.redacted_status != undefined) break;
	group_exists = true;
	for (var torrent of result.torrents) {
	  if (article.media ? torrent.media != article.media : !['CD', 'WEB'].includes(torrent.media)) continue;
	  if (article.release_date && article.release_date.getFullYear() > 0
		  && torrent.remasterYear && parseInt(torrent.remasterYear) != article.release_date.getFullYear()) continue;
	  if (torrent.format != 'FLAC') continue;
	  let upload_size = torrent.size / 1024 ** 2;
	  let bd = /^(\d+)bit Lossless/i.exec(torrent.encoding) ? parseInt(RegExp.$1) : 16;
	  function test_size(status) {
		if (article.size && upload_size > 0) {
		  let deviation = Math.abs(article.size / upload_size - 1) * 100;
		  let difference = Math.abs(article.size - upload_size);
		  if (deviation < size_tolerance) {
			article.redacted_status = status;
			article.size_difference = difference;
			article.size_deviation = deviation;
			return true;
		  }
		}
		return false;
	  }
	  if (bd > 16 ? article.bd == bd : article.bd <= 16) { // same format & BD
		same_format_present = true;
		if (test_size(-3)) break outer_block;
	  } else if (bd > 16 && (!article.bd || bd > article.bd)) { // RED higher resolution than SP
		article.redacted_status = -3; // always consider uploaded if higher quality exists
		break outer_block;
	  } else if ((!article.bd || article.bd > 16) && test_size(-1)) break outer_block;
	}
  }
  if (article.redacted_status == undefined) article.redacted_status = same_format_present ? -1 : group_exists ? 1 : 2;
  if (article.redacted_status != undefined) render_red_status(article);
}

function have_own_host(response) {
  if (response.status != 200 || response.readyState != 4) return;
  var parser = new DOMParser();
  var html = parser.parseFromString(response.responseText, "text/html");
  //var hostname = html.getElementById('hostname');
  //if (hostname == null || !is_safe_domain(hostname.textContent.trim())) return; // don't visit RED if on VPN or proxy
  var result = findString('//li[b/text()="Hostname"]/text()', html);
  if (result == null) return;
  var matches = result.match(/^\s*:\s*(.*?)\s*$/);
  if (!matches || !safe_domains.some(k => matches[1].toLowerCase().includes(k.toLowerCase()))) return; // don't visit RED if on VPN or proxy
  for (var article of response.context) {
	if (article.isRead || article.quality < 0.50) continue; // skip uninteresting stuff
	let red_url = use_api ? build_api_url(article) : build_basic_red_url(article, true);
// 	if (article.album_date && article.album_date.getFullYear() > 0 && !article.is_rm()) {
// 	  red_url += '&year=' + article.album_date.getFullYear()
// 	}
	GM_xmlhttpRequest({
	  method: 'GET',
	  url: red_url,
	  context: article,
	  onload: use_api ? query_red_api : query_red,
	  responseType: query_red_api ? 'json' : undefined,
	});
  }
}

function query_red_safe(articles) {
  GM_xmlhttpRequest({
	method: 'GET',
	url: 'https://www.whatsmyip.org/more-info-about-you/',
	onload: have_own_host,
	context: articles,
  });
}

function is_va(param) {
  var rx = /^(?:Various(?: Artists?)?|VA|\<various artists\>)$/;
  return typeof param == 'string' && param.search(rx) >= 0
  || typeof param == 'object' && typeof param.artist == 'string' && param.artist.search(rx) >= 0;
}

function get_size_from_string(str) {
  var matches = str.replace(',', '.').toUpperCase().match(/\b(\d+(?:\.\d+)?)\s*([KMGT]?B)/);
  if (!matches) return null;
  var size = matches[1];
  if (matches[2] == 'B') { size /= Math.pow(1024, 2) }
  else if (matches[2] == 'KB') { size /= Math.pow(1024, 1) }
  else if (matches[2] == 'GB') { size *= Math.pow(1024, 1) }
  else if (matches[2] == 'TB') { size *= Math.pow(1024, 2) }
  return parseFloat(size);
}

function extract_year(expr) {
  if (typeof expr != 'string') return null;
  var year = parseInt(expr);
  if (year > 0) return year;
  var m = expr.match(/\b(\d{4})\b/);
  return m && (year = parseInt(m[1])) > 0 ? year : null;
}

function search_red_external() {
  var red_url = this.href;
  console.log(red_url + "\n");
  open('firefox ' + red_url);
}

function parse_title(article) {
  if (typeof article.title != 'string') return;
  if (article.title.search(/\[[^\[\]]*\bVinyl\b/) >= 0) article.media = 'Vinyl';
  if (article.title.search(/\([^\(\)]*\bVinyl\b/) >= 0) article.media = 'Vinyl';
  if (article.title.search(/\[[^\[\]]*\bCD\b/) >= 0) article.media = 'CD';
  if (article.title.search(/\([^\(\)]*\bCD\b/) >= 0) article.media = 'CD';
  if (article.title.search(/\[[^\[\]]*\bSACD\b/) >= 0) article.media = 'SACD';
  if (article.title.search(/\([^\(\)]*\bSACD\b/) >= 0) article.media = 'SACD';
  if (article.title.search(/\[[^\[\]]*\bBlu[ \-]?Ray\b/) >= 0) article.media = 'Blu-Ray';
  if (article.title.search(/\([^\(\)]*\bBlu[ \-]?Ray\b/) >= 0) article.media = 'Blu-Ray';
  if (article.title.search(/\s+(?:(?:-\s+)?Single|\[Single\]|\(Single\})$/i) >= 0) article.release_type = 9;
  if (article.title.search(/\s+(?:(?:-\s+)?EP|\[EP\]|\(EP\})$/) >= 0) article.release_type = 5;
  if (!article.bd && article.title && article.title.search(/\[[^\[\]]\b(?:24\s*bits?|Hi[\-\s]?Res)\b/) >= 0) {
	article.bd = 24;
  }
}

function parse_album(article) {
  if (!article.album && article.title) {
	article.album = article.title
	  .replace(/^.*?\s+-\s+/, '').replace(/\s*\(Lossless [^\(\)]*\(\d{4}\)$/, '')
	  .replace(/\s*\[[^\[\]]*\b(?:Single|Vinyl Rip|24bit|Hi-Res|MQA)\b[^\[\]]*\]$/, '');
  }
  if (typeof article.album != 'string') return;
  // EP
  var rx = /\s+(?:(?:-\s+)?EP|\[EP\]|\(EP\))$/;
  if (article.album.search(rx) >= 0) {
	article.release_type = 5;
	article.album = article.album.replace(rx, '');
  }
  // Single
  rx = /\s+(?:-\s+Single|\[Single\]|\(Single\))$/i;
  if (article.album.search(rx) >= 0) {
	article.release_type = 9;
	article.album = article.album.replace(rx, '');
  }
  guess_release('Soundtrack|Score|Motion Picture|Series|Television|Original(?: \w+)? Cast|Music from|Musique originale|Bandes? originales?', 3); // Soundtrack & score
  guess_release('Live|Ao Vivo|En Directo?', 11); // live album
  if (article.album.search(/(?:^Live|^Directo? [Ee]n|\bUnplugged|\bAcoustic Stage)\b/) >= 0 && !article.release_type) {
	article.release_type = 11; // live album
  }
  // Remasters and editions
  rx = '\\b(?:Remaster\\w*|Reissue)/\\b';
  if (new RegExp('\\s+\\[[^\\[\\]]*' + rx + '[^\\[\\]]*\\]', 'i').test(article.album)
	  || new RegExp('\\s+\\([^\\(\\)]*' + rx + '[^\\(\\)]*\\)', 'i').test(article.album))
		  { article.is_remaster = true }
  guess_release('Anniversary|Deluxe|Limited|Edition|Remaster\\w*|Reissue');
  article.album = article.album.replace(/\s+\([^\(\)]*\s+(?:Version|Edition)\)$/i, '');
  article.album = article.album.replace(/\s+\[[^\[\]]*\s+(?:Version|Edition)\]$/i, '');
  // strip confusing shit
  var rx1 = /\s+\((?:\d+\s*CDs?\s+(?:Compilation|Set)?|Anthology|Compilation)\)$/i;
  var rx2 = /\s+\[(?:\d+\s*CDs?\s+(?:Compilation|Set)?|Anthology|Compilation)\]$/i;
  if (article.album.search(rx1) >= 0 || article.album.search(rx2) >= 0 && !article.release_type) {
	article.release_type = is_va(article) ? 7 : 6;
  }
  article.album = article.album.replace(rx1, '');
  article.album = article.album.replace(rx2, '');
  rx1 = /\s+\(\d+\s*CDs?\)$/;
  if (article.album.search(rx1) >= 0) article.album = article.album.replace(rx1, '');
  rx2 = /\s+\[\d+\s*CDs?\]$/;
  if (article.album.search(rx2) >= 0) article.album = article.album.replace(rx2, '');
  article.album = article.album.replace(/\s+feat\.\s.*/i, '');
  article.album = article.album.replace(/\s+\(feat\.\s[^\(\)]+\)/i, '');
  article.album = article.album.replace(/\s+\[feat\.\s[^\(\)]+\]/i, '');

  function guess_release(expressions, release_type = 0) {
	rx = '\\b(?:' + expressions + ')\\b';
	if (reInParenthesis(rx).test(article.album) || reInBrackets(rx).test(article.album)) {
	  if (release_type > 0 && !article.release_type) article.release_type = release_type;
	  article.album = article.album.replace(reInParenthesis(rx), '');
	  article.album = article.album.replace(reInBrackets(rx), '');
	}
  }
}

function parse_artist(article) {
  if (typeof article.artist != 'string') return;
  article.artist = article.artist.replace(/\s+feat\.\s.*/i, '');
  article.artist = article.artist.replace(/\s+\(feat\.\s[^\(\)]+\)/i, '');
  article.artist = article.artist.replace(/\s+\[feat\.\s[^\(\)]+\]/i, '');
}

function reInParenthesis(expr) { return new RegExp('\\s+\\([^\\(\\)]*'.concat(expr, '[^\\(\\)]*\\)$'), 'i') }
function reInBrackets(expr) { return new RegExp('\\s+\\[[^\\[\\]]*'.concat(expr, '[^\\[\\]]*\\]$'), 'i') }

function makeTimeString(duration) {
  let t = Math.abs(Math.round(duration));
  let H = Math.floor(t / 60 ** 2);
  let M = Math.floor(t / 60 % 60);
  let S = t % 60;
  return (duration < 0 ? '-' : '') + (H > 0 ? H + ':' + M.toString().padStart(2, '0') : M.toString()) +
	':' + S.toString().padStart(2, '0');
}

function timeStringToTime(str) {
  if (!/(-\s*)?\b(\d+(?::\d{2})*(?:\.\d+)?)\b/.test(str)) return null;
  var t = 0, a = RegExp.$2.split(':');
  while (a.length > 0) t = t * 60 + parseFloat(a.shift());
  return RegExp.$1 ? -t : t;
}