NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript== // @name Supraphonline foobar2000 tagging support // @name:cs Supraphonline podpora tagování ve foobar2000 // @name:en Supraphonline foobar2000 tagging support // @namespace https://openuserjs.org/users/Anakunda // @version 1.41 // @description Kopíruje metadata alba ve formátu pro aplikaci tagů ve foobar2000 // @description:cs Kopíruje metadata alba ve formátu pro aplikaci tagů ve foobar2000 // @description:en Copies album metadata to clipboard in machine parseable format // @author Anakunda // @copyright 2020, Anakunda (https://openuserjs.org/users/Anakunda) // @license GPL-3.0-or-later // @iconURL https://www.supraphonline.cz/favicon.ico // @match http*://*.supraphonline.cz/album/* // @grant GM_xmlhttpRequest // @grant GM_setClipboard // @grant GM_getValue // @grant GM_getValue // @grant GM_deleteValue // @require https://openuserjs.org/src/libs/Anakunda/xhrLib.js // ==/UserScript== // Výraz pro 'Automatically Fill Values' funkci ve foobaru2000: // %album artist%%album%%date%%releasedate%%genre%%label%%catalog%%discnumber%%totaldiscs%%discsubtitle%%tracknumber%%totaltracks%%artist%%title%%performer%%composer%%media%%comment%%url% 'use strict'; Array.prototype.includesCaseless = function(str) { if (typeof str != 'string') return false; str = str.toLowerCase(); return this.some(elem => typeof elem == 'string' && elem.toLowerCase() == str); }; Array.prototype.pushUnique = function(...items) { if (Array.isArray(items) && items.length > 0) items.forEach(it => { if (!this.includes(it)) this.push(it) }); return this.length; }; Array.prototype.pushUniqueCaseless = function(...items) { if (Array.isArray(items) && items.length > 0) items.forEach(it => { if (!this.includesCaseless(it)) this.push(it) }); return this.length; }; Array.prototype.equalTo = function(arr) { return Array.isArray(arr) && arr.length == this.length && Array.from(arr).sort().toString() == Array.from(this).sort().toString(); }; Array.prototype.equalCaselessTo = function(arr) { function adjust(elem) { return typeof elem == 'string' ? elem.toLowerCase() : elem } return Array.isArray(arr) && arr.length == this.length && arr.map(adjust).sort().toString() == this.map(adjust).sort().toString(); }; var hTimer = setInterval(function() { var ref = document.querySelector('form.table-action'); if (ref == null) return; clearInterval(hTimer); var child = document.createElement('button'); child.id = 'copy-info-to-clipboard'; child.textContent = 'Kopírovat do schránky'; child.type = 'button'; child.name = 'copy-info-to-clipboard'; child.className = 'btn btn-danger topframe_login'; child.style.marginRight = '10px'; child.onclick = fetchAlbum; ref.prepend(child); }, 1000); function fetchAlbum(evt) { var original_text = evt.target.textContent; evt.target.disabled = true; evt.target.textContent = 'Pracuji...'; let tracks = [], discNumber, discSubtitle, ref, media, encoding, format, bitdepth, trackIdentifiers, releaseDate, totalDiscs, samplerate, catalogue, imgUrl, album, totalTracks, albumYear, label, identifiers = {}; const vaParser = /^(?:Various(?:\s+Artists)?|Varios(?:\s+Artistas)?|V\/?A|\<various\s+artists\>|Různí(?:\s+interpreti)?)$/i; const pseudoArtistParsers = [ /^(?:#??N[\/\-]?A|[JS]r\.?)$/i, /^(?:traditional|trad\.|lidová)$/i, /\b(?:traditional|trad\.|lidová)$/, /^(?:tradiční|lidová)\s+/, /^(?:[Aa]nonym)/, /^(?:[Ll]iturgical\b|[Ll]iturgick[áý])/, /^(?:auditorium|[Oo]becenstvo|[Pp]ublikum)$/, /^(?:Various\s+Composers)$/i, /^(?:Guests|Friends)$/i, ]; const VA = 'Various Artists'; const artistClassParsers = [ /* 0 */ [/^(?:Main\s?Artist)$/i], /* 1 */ [/^(?:Featured\s?Artist)$/i], /* 2 */ [/^(?:Remix)/i], /* 3 */ [/(?:^(?:Composer|(?:Composer)?Lyricist|Author|Writer|music|written[\s\-]by|libreto|music\simprovisation)|\b(?:lyrics))$/i], /* 4 */ [/^(?:Conductor|(?:Chorus|Choir)\s?Master|Director|conducts|(?:conducted|directed)[\s\-]by)$/i], /* 5 */ [/^(?:DJ|Compiler|Compiled[\s\-]By|compiled[\s\-]by)$/], /* 6 */ [/^(?:Producer|produced[\s\-]by)$/i], /* 7 */ [/^(?:Artist|Soloist|Vocals|Ensemble|Orchestra|Choir)$/i], /* 8 */ [ /\b(?:Recorded|Engineer|Producer|Mixer|Programming|Programmer|Arranger|Assistant|Translation)\b/i, /(?:PersonnelMastering)\b/i, ], ]; if (/\/album\/(\d+)\b/i.test(document.URL)) identifiers.SUPRAPHONLINE_ID = parseInt(RegExp.$1); let artist = Array.from(document.querySelectorAll('div.visible-lg-block > h2.album-artist > a')) .map(a => a.title || a.textContent.trim()); let isVA = (ref = document.querySelector('span[itemprop="byArtist"] > meta[itemprop="name"]')) != null ? vaParser.test(ref.content) : artist.length <= 0; if ((ref = document.querySelector('h1[itemprop="name"]')) != null) album = ref.firstChild.data.trim(); if ((ref = document.querySelector('meta[itemprop="numTracks"]')) != null) totalTracks = parseInt(ref.content); let genres = (ref = document.querySelector('meta[itemprop="genre"]')) != null ? ref.content : undefined; if ((ref = document.querySelector('li.album-version > div.selected > div')) != null) { if (/\b(?:MP3)\b/.test(ref.textContent)) { media = 'WEB'; encoding = 'lossy'; format = 'MP3'; } if (/\b(?:FLAC)\b/.test(ref.textContent)) { media = 'WEB'; encoding = 'lossless'; format = 'FLAC'; bitdepth = 16; } if (/\b(?:Hi[\s\-]*Res)\b/.test(ref.textContent)) { media = 'WEB'; encoding = 'lossless'; format = 'FLAC'; bitdepth = 24; } if (/\b(?:CD)\b/.test(ref.textContent)) media = 'CD'; if (/\b(?:LP)\b/.test(ref.textContent)) media = 'Vinyl'; } const copyrightParser = /^(?:\([PC]\)|℗|©)$/i; document.querySelectorAll('ul.summary > li').forEach(function(li) { if (li.childElementCount <= 0) return; let key = li.firstElementChild.textContent, value = li.lastChild.textContent.trim(); if (key.includes('Nosič')) media = value; if (key.includes('Datum vydání')) releaseDate = normalizeDate(value, 'cs'); if (key.includes('První vydání')) albumYear = extractYear(value); if (key.includes('Žánr')) genres = translateGenre(value); if (key.includes('Vydavatel')) label = value; if (key.includes('Katalogové číslo')) catalogue = value; if (key.includes('Formát')) { if (/\b(?:FLAC|WAV|AIFF?)\b/.test(value)) { encoding = 'lossless'; format = 'FLAC' } if (/\b(\d+)[\-\s]?bits?\b/i.test(value)) bitdepth = parseInt(RegExp.$1); if (/\b([\d\.\,]+)[\-\s]?kHz\b/.test(value)) samplerate = parseFloat(RegExp.$1.replace(',', '.')) * 1000; } //if (key.includes('Celková stopáž')) totalTime = timeStringToTime(value); if (copyrightParser.test(key) && !albumYear) albumYear = extractYear(value); }); const creators = ['autoři', 'interpreti', 'tělesa', 'digitalizace']; let artists = [], ndx; for (let i = 0; i < creators.length; ++i) artists[i] = {}; document.querySelectorAll('ul.sidebar-artist > li').forEach(function(it) { if ((ref = it.querySelector('h3')) != null) { ndx = undefined; creators.forEach((it, _ndx) => { if (ref.textContent.includes(it)) ndx = _ndx }); } else { if (typeof ndx != 'number') return; if (ndx == 2) var role = 'ensemble'; else if ((ref = it.querySelector('span')) != null) role = translateRole(ref); if ((ref = it.querySelector('a')) != null) { if (!Array.isArray(artists[ndx][role])) artists[ndx][role] = []; artists[ndx][role].pushUnique([ref.textContent.trim(), document.location.origin + ref.pathname]); } } }); let description = Array.from(document.querySelectorAll('div[itemprop="description"] p')) .map(p => p.textContent.trim()).join('\n\n').replace(/\s+/g, ' '); let performers = [], composer = [], conductor = [], DJs = [], albumGuests = [], volMedia; for (let i = 1; i < 3; ++i) Object.keys(artists[i]).forEach(function(role) { // performers var a = artists[i][role].map(a => a[0]); ([ 'conductor', 'choirmaster', 'director', ].includes(role) ? conductor : role == 'DJ' ? DJs : [ 'FeaturedArtist', ].includes(role) ? albumGuests : artist).pushUnique(...a); }); Object.keys(artists[0]).forEach(function(role) { // composers composer.pushUnique(...artists[0][role].map(it => it[0]) .filter(it => !pseudoArtistParsers.some(rx => rx.test(it)))); }); if ((ref = document.querySelector('meta[itemprop="image"]')) != null) imgUrl = ref.content.replace(/\?.*$/, ''); document.querySelectorAll('table.table-tracklist > tbody > tr').forEach(function(tr, index) { if (tr.classList.contains('cd-header') && (ref = tr.querySelector('td > h3')) != null && /\b(?:(\S*?)\s*)?(\d+)\b/.test(ref.textContent)) { volMedia = RegExp.$1 ? RegExp.lastMatch : undefined; discNumber = parseInt(RegExp.$2) || undefined; } if (tr.classList.contains('song-header') && (ref = tr.querySelector('td')) != null) discSubtitle = ref.title || ref.textContent.trim(); if (tr.classList.contains('track') && tr.id) { trackIdentifiers = { TRACK_ID: /^(?:track)-(\d+)$/i.test(tr.id) ? parseInt(RegExp.$1) : undefined, }; if (volMedia) trackIdentifiers.VOL_MEDIA = volMedia; let track = { artist: isVA ? VA : undefined, artists: !isVA && artist.length > 0 ? artist : undefined, //featured_artists: albumGuests.length > 0 ? albumGuests : undefined, album: album, album_year: /*trackYear || */albumYear || undefined, release_date: releaseDate, label: label, catalog: catalogue, encoding: encoding, codec: format, bitdepth: bitdepth, samplerate: samplerate || undefined, media: media, genre: genres, disc_number: discNumber, total_discs: totalDiscs, disc_subtitle: discSubtitle, track_number: /^\s*(\d+)\.?\s*$/.test(tr.children[0].firstChild.textContent) ? parseInt(RegExp.$1) || RegExp.$1 : undefined, total_tracks: totalTracks, title: (ref = tr.querySelector('meta[itemprop="name"][content]')) != null ? ref.content : (ref = tr.querySelector('td > a.trackdetail')) != null ? ref.textContent.trim() : undefined, performers: performers.length > 0 ? performers : undefined, composers: composer.length > 0 ? composer : undefined, conductors: conductor.length > 0 ? conductor : undefined, compilers: DJs.length > 0 ? DJs : undefined, duration: durationFromMeta(tr), url: document.location.origin + document.location.pathname, description: description, identifiers: mergeIds(), cover_url: imgUrl, }; tracks.push((function() { if ((ref = tr.querySelector('td > a.trackdetail')) == null) return Promise.reject('link not found'); return localXHR(ref.pathname + ref.search).then(function(dom) { var detail = dom.querySelector('div[data-swap="trackdetail-' + track.identifiers.TRACK_ID + '"] > div > div.row'); if (detail == null) return Promise.reject('element not found'); detail.querySelectorAll('div[class]:nth-of-type(1) > ul > li').forEach(function(li) { var key = li.querySelector('span'), value = li.lastChild; if (key == null || value.nodeType != Node.TEXT_NODE) return; key = key.textContent.trim(); value = value.wholeText.trim(); if (!key || !value) return; if (key.startsWith('Žánr')) track.genre = value; if (key.startsWith('Nahrávka dokončena')) track.rec_year = extractYear(value); if (key.startsWith('Místo nahrání')) track.venue = value; if (key.startsWith('Rok prvního vydání')) track.pub_year = extractYear(value); if (copyrightParser.test(key)) track.copyright = value; }); let trackArtists = []; for (let i = 0; i < 8; ++i) trackArtists[i] = []; detail.querySelectorAll('div[class]:nth-of-type(2) > ul > li').forEach(function(li) { var role = li.querySelector('span'); var artists = Array.from(li.getElementsByTagName('a')).map(a => a.textContent.trim()) .filter(artist => !pseudoArtistParsers.some(rx => rx.test(artist))); if (role != null && artists.length > 0) role = translateRole(role); else return; if (artistClassParsers[2].some(rx => rx.test(role))) trackArtists[2].pushUnique(...artists); else if (artistClassParsers[3].some(rx => rx.test(role))) trackArtists[3].pushUnique(...artists); else if (artistClassParsers[5].some(rx => rx.test(role))) trackArtists[5].pushUnique(...artists); else if (artistClassParsers[6].some(rx => rx.test(role))) trackArtists[6].pushUnique(...artists); else if (role.toLowerCase() == 'performer' || !artistClassParsers[8].some(rx => rx.test(role))) { if (artistClassParsers[0].some(rx => rx.test(role))) trackArtists[0].pushUnique(...artists); else if (artistClassParsers[1].some(rx => rx.test(role))) trackArtists[1].pushUnique(...artists); else if (artistClassParsers[4].some(rx => rx.test(role))) trackArtists[4].pushUnique(...artists); else artists.forEach(_artist => { if (artist.includesCaseless(_artist)) trackArtists[0].pushUnique(_artist); else if (artistClassParsers[7].some(rx => rx.test(role))) trackArtists[1].pushUnique(_artist); }); trackArtists[7].pushUnique(...artists.map(artist => artist + ' (' + role + ')')); } }); if (trackArtists[1].length > 0 && trackArtists[0].length <= 0) { trackArtists[0] = trackArtists[1]; trackArtists[1] = []; } if (trackArtists[0].length > 0 && (isVA || !trackArtists[0].equalCaselessTo(artist) || trackArtists[1].length > 0/*!trackArtists[1].equalCaselessTo(albumGuests)*/)) { track.track_artists = trackArtists[0]; if (trackArtists[1].length > 0) track.track_guests = trackArtists[1]; } [ [3, 'composer'], [4, 'conductor'], [2, 'remixer'], [5, 'compiler'], //[6, 'producer'], [7, 'performer'], ].forEach(def => { if (trackArtists[def[0]].length > 0) track[def[1] + 's'] = trackArtists[def[0]] }) return track; }); })().catch(function(reason) { console.error('Supraphonline parser failed to get track', index + 1, 'detail:', reason); return Promise.resolve(track); })); } // track }); Promise.all(tracks).then(tracks => tracks.map(track => { if (Array.isArray(track.track_artists) && track.track_artists.length > 0) { var trackArtist = joinArtists(track.track_artists); if (Array.isArray(track.track_guests) && track.track_guests.length > 0) trackArtist += ' feat. ' + joinArtists(track.track_guests); } return [ isVA ? VA : joinArtists(track.artists) || '', track.album || '', track.album_year || track.pub_year || '', track.release_date || '', track.genre || '', track.label || '', track.catalog || '', track.disc_number || '', track.total_discs > 1 ? track.total_discs : '', track.disc_subtitle || '', track.track_number || '', track.total_tracks || '', trackArtist || (Array.isArray(track.artists) && track.artists.length > 0 ? joinArtists(track.artists) : track.artist) || '', track.title || '', (track.performers || []).join(', '), (track.composers || []).join(', '), track.media, track.description, track.url, ].join('\x1E'); }).join('\n')).then(clipBoard => { GM_setClipboard(clipBoard, 'text') }).catch(e => { alert(e) }).then(function() { evt.target.disabled = false; evt.target.textContent = original_text; }); function translateGenre(genre) { if (!genre || typeof genre != 'string') return undefined; [ ['Orchestrální hudba', 'Orchestral Music'], ['Komorní hudba', 'Chamber Music'], ['Vokální', 'Classical, Vocal'], ['Klasická hudba', 'Classical'], ['Melodram', 'Classical, Melodram'], ['Symfonie', 'Symphony'], ['Vánoční hudba', 'Christmas Music'], [/^(?:Alternativ(?:ní|a))$/i, 'Alternative'], ['Dechová hudba', 'Brass Music'], ['Elektronika', 'Electronic'], ['Folklor', 'Folclore, World Music'], ['Instrumentální hudba', 'Instrumental'], ['Latinské rytmy', 'Latin'], ['Meditační hudba', 'Meditative'], ['Vojenská hudba', 'Military Music'], ['Pro děti', 'Children'], ['Pro dospělé', 'Adult'], ['Mluvené slovo', 'Spoken Word'], ['Audiokniha', 'audiobook'], ['Humor', 'humour'], ['Pohádka', 'Fairy-Tale'], ].forEach(function(subst) { if (typeof subst[0] == 'string' && genre.toLowerCase() == subst[0].toLowerCase() || subst[0] instanceof RegExp && subst[0].test(genre)) genre = subst[1]; }); return genre; } function translateRole(elem) { return elem instanceof HTMLElement ? [ [/\b(?:klavír)\b/ig, 'piano'], [/\b(?:housle)\b/ig, 'violin'], [/\b(?:violoncello)\b/ig, 'cello'], [/\b(?:viola)\b/ig, 'alto'], [/\b(?:varhany)\b/ig, 'organ'], [/\b(?:cembalo)\b/ig, 'harpsichord'], [/\b(?:trubka)\b/ig, 'trumpet'], [/\b(?:soprán)\b/ig, 'soprano'], [/\b(?:alt)\b/ig, 'alto'], [/\b(?:baryton)\b/ig, 'baritone'], [/\b(?:bas)\b/ig, 'basso'], [/\b(?:akordeon)\b/ig, 'accordion'], [/\b(?:syntezátor)\b/ig, 'synthesizer'], [/\b(?:klávesové nástroje)\b/ig, 'keyboards'], [/\b(?:bicí)\b/ig, 'drums'], [/\b(?:kontrabas)\b/ig, 'double-bass'], [/\b(?:zpěv|vokál)\b/ig, 'vocals'], [/\b(?:baskytara)\b/ig, 'bass guitar'], [/\b(?:havajská kytara)\b/ig, 'steel guitar'], [/\b(?:akustická kytara)\b/ig, 'acoustic guitar'], [/\b(?:kytara)\b/ig, 'guitar'], [/\b(?:kytary)\b/ig, 'guitars'], [/(?:čte|četba)\b/ig, 'narration'], [/\b(?:vypravuje)\b/ig, 'narration'], [/\b(?:hudební těleso)\b/ig, 'ensemble'], [/\b(?:Umělec)\b/ig, 'Artist'], [/\b(?:improvizace)\b/ig, 'improvisation'], ['český', 'czech'], ['původní', 'original'], [/\b(?:text)\b/ig, 'lyrics'], [/\b(?:hudba)\b/ig, 'music'], ['hudební', 'music'], [/\b(?:autor)\b/ig, 'author'], [/\b(?:překlad)\b/ig, 'translation'], ['účinkuje', 'participating'], ['hovoří a zpívá', 'speaks and sings'], ['hovoří', 'spoken by'], ['komentář', 'commentary'], [/\b(?:dirigent)\b/ig, 'conductor'], ['řídí', 'director'], [/\b(?:sbormistr)\b/ig, 'choirmaster'], ['programování', 'programming'], [/\b(?:produkce)\b/ig, 'produced by'], ['nahrál', 'recorded by'], [/\b(?:digitální přepis)\b/ig, 'A/D transfer'], ].reduce((r, def) => r.replace(...def), elem.textContent.trim().replace(/\s*:.*$/, '')) : undefined; } function mergeIds() { var r = Object.assign({}, identifiers, trackIdentifiers); trackIdentifiers = {}; return r; } } function joinArtists(arr, decorator = artist => artist) { if (!Array.isArray(arr)) return null; if (arr.some(artist => artist.includes('&'))) return arr.map(decorator).join(', '); if (arr.length < 3) return arr.map(decorator).join(' & '); return arr.slice(0, -1).map(decorator).join(', ') + ' & ' + decorator(arr.slice(-1).pop()); } 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; } function normalizeDate(str, countryCode = undefined) { if (typeof str != 'string') return null; var match; function formatOutput(yearIndex, montHindex, dayIndex) { var year = parseInt(match[yearIndex]), month = parseInt(match[montHindex]), day = parseInt(match[dayIndex]); if (year < 30) year += 2000; else if (year < 100) year += 1900; if (year < 1000 || year > 9999 || month < 1 || month > 12 || day < 0 || day > 31) return null; return year.toString() + '-' + month.toString().padStart(2, '0') + '-' + day.toString().padStart(2, '0'); } if ((match = /\b(\d{4})-(\d{1,2})-(\d{1,2})\b/.exec(str)) != null) return formatOutput(1, 2, 3); // US if ((match = /\b(\d{4})\/(\d{1,2})\/(\d{1,2})\b/.exec(str)) != null) return formatOutput(1, 2, 3); if ((match = /\b(\d{1,2})\/(\d{1,2})\/(\d{2})\b/.exec(str)) != null && (parseInt(match[1]) > 12 || /\b(?:be|it)/.test(countryCode))) return formatOutput(3, 2, 1); // BE, IT if ((match = /\b(\d{1,2})\/(\d{1,2})\/(\d{2})\b/.exec(str)) != null) return formatOutput(3, 1, 2); // US if ((match = /\b(\d{1,2})\/(\d{1,2})\/(\d{4})\b/.exec(str)) != null) return formatOutput(3, 2, 1); // UK, IE, FR, ES if ((match = /\b(\d{1,2})-(\d{1,2})-((?:\d{2}|\d{4}))\b/.exec(str)) != null) return formatOutput(3, 2, 1); // NL if ((match = /\b(\d{1,2})\. *(\d{1,2})\. *(\d{4})\b/.exec(str)) != null) return formatOutput(3, 2, 1); // CZ, DE if ((match = /\b(\d{1,2})\. *(\d{1,2})\. *(\d{2})\b/.exec(str)) != null) return formatOutput(3, 2, 1); // AT, CH, DE, LU if ((match = /\b(\d{4})\. *(\d{1,2})\. *(\d{1,2})\b/.exec(str)) != null) return formatOutput(1, 2, 3); // JP return extractYear(str); } function extractYear(expr) { if (typeof expr == 'number') return Math.round(expr); if (typeof expr != 'string') return null; if (/\b(\d{4})\b/.test(expr)) return parseInt(RegExp.$1); var d = new Date(expr); return parseInt(isNaN(d) ? expr : d.getFullYear()); } function durationFromMeta(elem) { if (!(elem instanceof HTMLElement)) return undefined; let meta = elem.querySelector('meta[itemprop="duration"][content]'); if (meta == null) return undefined; let m = /^PT?(?:(?:(\d+)H)?(\d+)M)?(\d+)S$/i.exec(meta.content); if (m != null) return (parseInt(RegExp.$1) || 0) * 60**2 + (parseInt(RegExp.$2) || 0) * 60 + (parseInt(RegExp.$3) || 0); m = timeStringToTime(meta.content); return m != null ? m : undefined; }