// ==UserScript== // @name Qobuz - Copy album info // @version 1.07 // @author Anakunda // @copyright 2020, Anakunda (https://greasyfork.org/cs/users/321857-anakunda) // @license GPL-3.0-or-later // @namespace https://greasyfork.org/users/321857-anakunda // @description Copy metadata to parseable format // @match https://www.qobuz.com/*/album/* // @match https://www.qobuz.com/album/* // @iconURL https://www.qobuz.com/assets-static/img/icons/favicon/favicon-32x32.png // @grant GM_setClipboard // @require https://greasyfork.org/scripts/404642-js-xhr/code/js-xhr.js // @require https://greasyfork.org/scripts/406257-qobuzlib/code/QobuzLib.js // @downloadURL none // ==/UserScript== // patter for 'Automatically Fill Values' in foobaru200: // %album artist%%album%%releasedate%%genre%%label%%discnumber%%discsubtitle%%totaldiscs%%tracknumber%%totaltracks%%artist%%title%%composer%%performer%%media%%url%%comment%%releasetype% createButton(); Array.prototype.pushUnique = function(...items) { items.forEach(it => { if (!this.includes(it)) this.push(it) }); return this.length; }; Array.prototype.pushUniqueCaseless = function(...items) { items.forEach(it => { if (!this.includesCaseless(it)) this.push(it) }); return this.length; }; 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(); }; String.prototype.toASCII = function() { return this.normalize("NFKD").replace(/[\x00-\x1F\u0080-\uFFFF]/g, ''); }; String.prototype.flatten = function() { return this.replace(/\n/g, '\x1C').replace(/\r/g, '\x1D'); }; String.prototype.collapseGaps = function() { return this.replace(/(?:[ \t\xA0]*\r?\n){3,}/g, '\n\n').replace(/\[(\w+)\]\[\/\1\]/ig,'').trim(); }; function copyTracks(evt) { getTracks().then(function(tracks) { GM_setClipboard(tracks.map(track => track.map(field => field !== undefined ? field : '') .join('\x1E')).join('\n'), 'text/plain'); }, reason => { alert(reason) }); return false; } function getTracks() { const discParser = /^(?:CD|DIS[CK]\s+|VOLUME\s+|DISCO\s+|DISQUE\s+)(\d+)(?:\s+of\s+(\d+))?$/i; const vaParser = /^(?:Various(?:\s+Artists)?|VA|\|Různí(?:\s+interpreti)?)$/i; const pseudoArtistParsers = [ /^(?:#??N[\/\-]?A|[JS]r\.?)$/i, /^(?:traditional|lidová)$/i, /\b(?:traditional|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 error = new Error('Failed to parse Qobus release page'); var ref, artist, album, releaseDate, totalDiscs, totalTracks, isVA, label, composer, discSubtitle, discNumber, url, description, tracks = [], genres = [], QOBUZ_ID = document.location.pathname.replace(/^.*\//, ''); if ((ref = document.querySelector('section.album-item[data-gtm]')) != null) try { let gtm = JSON.parse(ref.dataset.gtm); if (gtm.shop.category) genres.push(gtm.shop.category); if (gtm.shop.subCategory && !genres.includes(gtm.shop.subCategory)) genres.push(gtm.shop.subCategory.replace(/-/g, ' ')); //if (gtm.type && gtm.type.toLowerCase() != 'album') trackIdentifiers.RELEASETYPE = gtm.type; } catch(e) { console.warn(e) } if ((ref = document.querySelector('div.album-meta > h2.album-meta__artist')) != null) artist = ref.title || ref.textContent.trim(); isVA = vaParser.test(artist); album = (ref = document.querySelector('div.album-meta > h1.album-meta__title')) != null ? ref.title || ref.textContent.trim() : undefined; releaseDate = (ref = document.querySelector('div.album-meta > ul > li:first-of-type')) != null ? normalizeDate(ref.textContent) : undefined; var mainArtist = (ref = document.querySelector('div.album-meta > ul > li:nth-of-type(2) > a')) != null ? ref.title || ref.textContent.trim() : undefined; //ref = document.querySelector('p.album-about__copyright'); //if (ref != null) albumYear = extractYear(ref.textContent); document.querySelectorAll('section#about > ul > li').forEach(function(it) { function matchLabel(lbl) { return it.textContent.trimLeft().startsWith(lbl) } if (/\b(\d+)\s*(?:dis[ck]|disco|disque)/i.test(it.textContent)) totalDiscs = parseInt(RegExp.$1); if (/\b(\d+)\s*(?:track|pist[ae]|tracce|traccia)/i.test(it.textContent)) totalTracks = parseInt(RegExp.$1); if (['Label', 'Etichetta', 'Sello'].some(l => it.textContent.trimLeft().startsWith(l))) label = it.firstElementChild.textContent.replace(/\s+/g, ' ').trim(); else if (['Composer', 'Compositeur', 'Komponist', 'Compositore', 'Compositor'].some(matchLabel)) { composer = it.firstElementChild.textContent.trim(); //if (pseudoArtistParsers.some(rx => rx.test(composer))) composer = undefined; } else if (['Genre', 'Genere', 'Género'].some(g => it.textContent.startsWith(g)) && it.childElementCount > 0 && genres.length <= 0) { genres = Array.from(it.querySelectorAll('a')).map(elem => elem.textContent.trim()); /* if (genres.length >= 1 && ['Pop/Rock'].includes(genres[0])) genres.shift(); if (genres.length >= 2 && ['Alternative & Indie'].includes(genres[genres.length - 1])) genres.shift(); if (genres.length >= 1 && ['Metal', 'Heavy Metal'].some(genre => genres.includes(genre))) while (genres.length > 1) genres.shift(); */ while (genres.length > 1) genres.shift(); } }); if ((ref = document.querySelector('section#description > p')) != null) description = ref.textContent.trim().flatten(); url = (ref = document.querySelector('meta[property="og:url"]')) != null ? ref.content : document.URL; addTracks(document); if (totalTracks <= 50) return Promise.resolve(finalizeTracks()); var params = new URLSearchParams({ albumId: QOBUZ_ID, offset: 50, limit: 999, store: /\/(\w{2}-\w{2})\/album\//i.test(document.location.pathname) ? RegExp.$1 : 'fr-fr', }); return localFetch('/v4/ajax/album/load-tracks?' + params).then(document => { addTracks(document) }, function(reason) { console.error('globalFetch() failed:', reason); }).then(() => finalizeTracks()); function addTracks(dom) { Array.prototype.push.apply(tracks, Array.from(dom.querySelectorAll('div.player__item > div.player__tracks > div.track > div.track__items')).map(function(tr, index) { var TRACK_ID = tr.parentNode.dataset.track, trackArtists = []; for (let n = 0; n < qobuzArtistLabels.length; ++n) trackArtists[n] = []; if ((ref = tr.parentNode.querySelector('p.track__info[itemprop="byArtist"]')) != null) { ref.textContent.trim().split(/\s+-\s+/).map(it => it.split(/\s*,\s*/)).forEach(function(it) { if (it.length > 1) qobuzArtistLabels.forEach(function(artistLabels, index) { if (artistLabels.some(role => it.slice(1).includes(role))) trackArtists[index].pushUnique(it[0]); }); else trackArtists[0].pushUnique(it[0]); }); } //trackArtists[0] = trackArtists[0].filter(artist => !trackArtists[4].includes(artist)); if ((ref = tr.querySelector('div.track__item--name[itemprop="performer"] > span')) != null) { if (trackArtists[0].length <= 0) trackArtists[0] = [ref.textContent.trim()]; if (trackArtists[2].length <= 0) trackArtists[2] = [ref.textContent.trim()]; } for (let index = 0; index < trackArtists.length; ++index) if (index != 3) trackArtists[index] = trackArtists[index].filter(trackArtist => !pseudoArtistParsers.some(rx => rx.test(trackArtist))); trackArtists[1] = trackArtists[1].filter(artist => ![0, 4].some(index => trackArtists[index].includes(artist))); if (trackArtists[0].length > 0) var trackArtist = joinArtists(trackArtists[0]); if (!trackArtist && !isVA) trackArtist = artist; else console.warn('Qobuz: track main artist missing for track', index + 1, tr); if (trackArtist && trackArtists[1].length > 0) trackArtist += ' feat. ' + joinArtists(trackArtists[1]); //console.debug('\tFiltered:', trackArtists[0], trackArtists[1]); if (tr.parentNode.dataset.gtm) try { let gtm = JSON.parse(tr.parentNode.dataset.gtm); //if (gtm.product.id) QOBUZ_ID = gtm.product.id; if (gtm.product.subCategory) var subCategory = [gtm.product.subCategory]; if (gtm.product.type) var releaseType = gtm.product.type; } catch(e) { console.warn(e) } if ((ref = tr.parentNode.parentNode.parentNode.querySelector('p.player__work:first-child')) != null) { discSubtitle = ref.textContent.replace(/\s+/g, ' ').trim(); guessDiscNumber(); } return [ isVA ? 'Various Artists' : artist, album, releaseDate, genres.map(function(genre) { genre = genre.replace(/-+/g, ' '); qobuzTranslations.forEach(function(it) { if (genre.toASCII().toLowerCase() == it[0].toASCII().toLowerCase()) genre = it[1]; }); return genre.split(/\s+/).map(word => word[0].toUpperCase() + word.slice(1).toLowerCase()).join(' '); }).join(', '), label, totalDiscs > 1 ? discNumber || 1 : undefined, discSubtitle, totalDiscs > 1 ? totalDiscs : undefined, parseInt(tr.querySelector('span[itemprop="position"]').textContent), totalTracks, trackArtist, (tr.querySelector('div.track__item--name[itemprop="name"] > span') || tr.querySelector('span.track__item--name')) .textContent.trim().replace(/\s+/g, ' '), trackArtists[3].length > 0 ? trackArtists[3].join(', ') : composer, trackArtists[2].join(', '), //joinArtists(trackArtists[4]), //joinArtists(trackArtists[5]), //joinArtists(trackArtists[6]), 'WEB', url, description, releaseType || undefined, ]; })); } 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})\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 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 guessDiscNumber() { if (discParser.test(discSubtitle)) { discSubtitle = undefined; discNumber = parseInt(RegExp.$1); } } function finalizeTracks() { if (!isVA && tracks.every(track => track[10] && track[10] == tracks[0][10])) tracks.forEach(function(track) { track[0] = track[10] }); return tracks; } } function createButton() { var button = document.querySelector('button.player-share__button'); if (button != null) button.onclick = copyTracks; }