// ==UserScript== // @name Qobuz - Copy album info // @version 1.20.1 // @author Anakunda // @license GPL-3.0-or-later // @copyright 2019, Anakunda (https://greasyfork.org/cs/users/321857-anakunda) // @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 // @grant GM_xmlhttpRequest // @grant GM_registerMenuCommand // @grant GM_getValue // @grant GM_setValue // @connect play.qobuz.com // @connect www.qobuz.com // @require https://openuserjs.org/src/libs/Anakunda/xhrLib.min.js // @require https://openuserjs.org/src/libs/Anakunda/QobuzLib.min.js // @downloadURL none // ==/UserScript== // expression for 'Automatically Fill Values' in foobaru200: // %album artist%%album%%releasedate%%genre%%label%%discnumber%%discsubtitle%%totaldiscs%%tracknumber%%totaltracks%%artist%%title%%composer%%performer%%conductor%%media%%url%%comment%%releasetype%%upc%%isrc%%explicit% 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) { 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(); }; Array.prototype.distinctValues = function() { return this.filter((elem, index, arrRef) => arrRef.indexOf(elem) == index); }; Array.prototype.flatten = function() { return this.reduce(function(flat, toFlatten) { return flat.concat(Array.isArray(toFlatten) ? toFlatten.flatten() : toFlatten); }, [ ]); }; 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 queryQobuzAPI(endPoint, params) { if (!endPoint) return Promise.reject('No API endpoint'); const qobuzApi = (function() { try { let qobuzApis = JSON.parse(window.sessionStorage.qobuzApis); if (qobuzApis.length > 0) return Promise.resolve(qobuzApis[qobuzApis.length - 1]); } catch(e) { } return globalXHR('https://play.qobuz.com/login').then(function({document}) { let script = document.body.querySelector('script[src]:last-of-type'); if (script == null) return Promise.reject('invalid document structure'); let url = new URL(script.src); url.hostname = 'play.qobuz.com'; return globalXHR(url, { responseType: 'text' }); }).then(function({responseText}) { let qobuzApis = responseText.match(/\b(?:n\.qobuzapi)=(\{.*?\})/g) .map(s => eval('(' + /\b(?:n\.qobuzapi)=(\{.*?\})/.exec(s)[1] + ')')); if (qobuzApis.length <= 0) return Promise.reject('invalid format (bundle.js)'); window.sessionStorage.qobuzApis = JSON.stringify(qobuzApis); return qobuzApis[qobuzApis.length - 1]; }); })(); function getUser() { try { let userInfo = JSON.parse(window.localStorage.qobuzUserInfo); if (!userInfo.user_auth_token) throw 'User info invalid'; return Promise.resolve(userInfo); } catch(e) { let userId = GM_getValue('userid'), password = GM_getValue('password'); if (!userId || !password) return Promise.reject('insufficient user credentials'); return qobuzApi.then(qobuzApi => globalXHR(qobuzApi.base_url + qobuzApi.base_method + 'user/login', { responseType: 'json', headers: { 'X-App-Id': qobuzApi.app_id } }, new URLSearchParams({ username: userId, password: password }))).then(function({response}) { window.localStorage.qobuzUserInfo = JSON.stringify(response); if (!response.user_auth_token) throw 'User info invalid'; return response; }); } } return getUser().then(user => qobuzApi.then(function(qobuzApi) { let url = new URL(qobuzApi.base_url + qobuzApi.base_method + endPoint); if (params && typeof params == 'object') url.search = new URLSearchParams(params); return globalXHR(url, { responseType: 'json', headers: { 'X-App-Id': qobuzApi.app_id, 'X-User-Auth-Token': user.user_auth_token }, }).then(({response}) => response); })); } function copyTracks(evt) { getTracks().then(function(tracks) { GM_setClipboard(tracks.map(track => track.map(field => field !== undefined ? field : '') .join('\x1E')).join('\n'), 'text'); let img = document.createElement('img'); img.src = 'data:image/png;base64,' + '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'; img.style.height = '18px'; evt.target.textContent = ''; evt.target.append(img); }, 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)?|Varios(?:\s+Artistas)?|V\/?A|\|Různí(?:\s+interpreti)?)$/i; const VA = 'Various Artists'; const multiArtistParsers = [ /\s*[\,\;\u3001](?!\s*(?:[JjSs]r)\b)(?:\s*(?:[Aa]nd|\&)\s+)?\s*/, /\s+[\/\|\×|meets]\s+/i, ]; const ampersandParsers = [ /\s+(?:meets|vs\.?|X)\s+(?!\s*(?:[\&\/\+\,\;]|and))/i, /\s*[;\/\|\×]\s*(?!\s*(?:\s*[\&\/\+\,\;]|and))/i, /(?:\s*,)?\s+(?:[\&\+]|and)\s+(?!his\b|her\b|Friends$|Strings$)/i, // /\s+(?:[\&\+]|and)\s+(?!(?:The|his|her|Friends)\b)/i, /\s*\+\s*(?!(?:his\b|her\b|Friends$|Strings$))/i, ]; const featArtistParsers = [ ///\s+(?:meets)\s+(.+?)\s*$/i, /* 0 */ /\s+(?:[Ww](?:ith|\.?\/)|[Aa]vec)\s+(?!his\b|her\b|Friends$|Strings$)(.+?)\s*$/, /* 1 */ /(?:\s+[\-\−\—\–\_])?\s+(?:[Ff]eaturing\s+|(?:[Ff]eat|[Ff]t|FT)\.\s*|[Ff]\.?\/\s+)([^\(\)\[\]\{\}]+?)(?=\s*(?:[\(\[\{].*)?$)/, /* 2 */ /\s+\[\s*f(?:eat(?:\.|uring)|t\.|\.?\/)\s+([^\[\]]+?)\s*\]/i, /* 3 */ /\s+\(\s*f(?:eat(?:\.|uring)|t\.|\.?\/)\s+([^\(\)]+?)\s*\)/i, /* 4 */ /\s+\[\s*(?:(?:en\s+)?duo\s+)?avec\s+([^\[\]]+?)\s*\]/i, /* 5 */ /\s+\(\s*(?:(?:en\s+)?duo\s+)?avec\s+([^\(\)]+?)\s*\)/i, /* 6 */ /\s+\[\s*(?:with|[Ww]\.?\/)\s+(?![Hh]is\b|[Hh]er\b|Friends$|Strings$)([^\[\]]+?)\s*\]/, /* 7 */ /\s+\(\s*(?:with|[Ww]\.?\/)\s+(?![Hh]is\b|[Hh]er\b|Friends$|Strings$)([^\(\)]+?)\s*\)/, ]; const featTest = /\b(?:feat(?:uring|\.)|ft\.)/i; const pseudoArtistParsers = [ /* 0 */ vaParser, /* 1 */ /^(?:#??N[\/\-]?A|[JS]r\.?)$/i, /* 2 */ /^(?:auditorium|[Oo]becenstvo|[Pp]ublikum)$/, /* 3 */ /^(?:(Special\s+)??Guests?|Friends|(?:Studio\s+)?Orchestra)$/i, /* 4 */ /^(?:Various\s+Composers)$/i, /* 5 */ /^(?:[Aa]nonym)/, /* 6 */ /^(?:traditional|trad\.|lidová)$/i, /* 7 */ /\b(?:traditional|trad\.|lidová)$/, /* 8 */ /^(?:tradiční|lidová)\s+/, /* 9 */ /^(?:[Ll]iturgical\b|[Ll]iturgick[áý])/, ]; const tailingBracketStripper = /(?:\s+(?:\([^\(\)]+\)|\[[^\[\]]+\]|\{[^\{\}]+\}))+\s*$/; const error = new Error('Failed to parse Qobus release page'); function normalizeDate(str, countryCode = undefined) { if (typeof str != 'string') return null; let match; function formatOutput(yearIndex, montHindex, dayIndex) { let 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, SE 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|au|nz)\b/i.test(countryCode))) return formatOutput(3, 2, 1); // BE, IT, AU, NZ if ((match = /\b(\d{1,2})\/(\d{1,2})\/(\d{2})\b/.exec(str)) != null) return formatOutput(3, 1, 2); // US, MO if ((match = /\b(\d{1,2})\/(\d{1,2})\/(\d{4})\b/.exec(str)) != null) return formatOutput(3, 2, 1); // UK, IE, FR, ES, FI, DK 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); let d = new Date(expr); return parseInt(isNaN(d) ? expr : d.getFullYear()); } function twoOrMore(artist) { return artist.length >= 2 && !pseudoArtistParsers.some(rx => rx.test(artist)) }; function looksLikeTrueName(artist, index = 0) { return twoOrMore(artist) && (index == 0 || !/^(?:his\b|her\b|Friends$|Strings$)/i.test(artist)) && artist.split(/\s+/).length >= 2 && !pseudoArtistParsers.some(rx => rx.test(artist)); } function realArtistName(artist) { return ![ pseudoArtistParsers[0], pseudoArtistParsers[1], pseudoArtistParsers[4], ].some(rx => rx.test(artist)); } function splitArtists(str, parsers = multiArtistParsers) { var result = [str]; parsers.forEach(function(parser) { for (let i = result.length; i > 0; --i) { let j = result[i - 1].split(parser).map(strip); if (j.length > 1 && j.every(twoOrMore) && !j.some(artist => pseudoArtistParsers.some(rx => rx.test(artist)))) result.splice(i - 1, 1, ...j); } }); return result; } function splitAmpersands(artists) { if (typeof artists == 'string') var result = splitArtists(artists); else if (Array.isArray(artists)) result = Array.from(artists); else return []; ampersandParsers.forEach(function(ampersandParser) { for (let i = result.length; i > 0; --i) { let j = result[i - 1].split(ampersandParser).map(strip); if (j.length <= 1 || !j.every(looksLikeTrueName)) continue; result.splice(i - 1, 1, ...j.filter(function(artist) { return !result.includesCaseless(artist) && !pseudoArtistParsers.some(rx => rx.test(artist)); })); } }); return result; } function strip(art) { return [ ///\s+(?:aka|AKA)\.?\s+(.*)$/g, tailingBracketStripper, ].reduce((acc, rx, ndx) => ndx != 1 || rx.test(acc)/* && !notMonospaced(RegExp.$1)*/ ? acc.replace(rx, '') : acc, art); } 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); } } let ref, artist, album, releaseDate, totalDiscs, totalTracks, isVA, label, composer, discSubtitle, discNumber, title, url, tracks = [ ], genres = [ ], featArtists = [ ], description, releaseType, trackArtist, personnel, domParser = new DOMParser, QOBUZ_ID = document.location.pathname.replace(/^.*\//, ''); function getTrackArtists(performers, defaultPerformer) { let result = [ ]; for (let ndx = 0; ndx <= qobuzArtistLabels.length; ++ndx) result[ndx] = [ ]; if (performers) for (let it of performers.split(' - ').map(x => x.split(', '))) { // ========================================== EXPERIMENTAL ========================================== if (it.length > 2) { let ndx = it.findIndex((s, ndx) => ndx > 0 && qobuzArtistLabels.some(artistLabels => artistLabels.includes(s))); if (ndx > 1) it.splice(0, ndx, it.slice(0, ndx).join(', ')); //else if (ndx < 0) it = [it.join(', ')]; } // ================================================================================================== if (it.length > 1) for (let ndx of it.slice(1).map(getCategoryIndex)) result[ndx >= 0 ? ndx : 13].pushUniqueCaseless(it[0]); else { result[qobuzArtistLabels.length].pushUniqueCaseless(it[0]); console.warn('Qobuz rolesless performer:', it[0]); } } //Array.prototype.push.apply(result[0], result[1]); for (let ndx of [1, 2, 3, 4, 5, qobuzArtistLabels.length]) if (result[0].length <= 0 && result[ndx].length > 0) result[0] = result[ndx]; //result[0] = result[0].filter(artist => ![9, 14].some(index => result[index].includes(artist))); // (feat. ....) featArtistParsers.slice(1, 6).forEach(function(rx, index) { let matches = rx.exec(title); if (matches == null) return; Array.prototype.pushUniqueCaseless.apply(result[6], splitAmpersands(matches[2])); title = title.replace(rx, ''); }); // (with ...) featArtistParsers.slice(6, 8).forEach(function(rx, index) { let matches = rx.exec(title); if (matches == null) return; let withArtists = splitAmpersands(matches[2]); if (!withArtists.every(artist => result.some(result => result.includes(artist)))) return; Array.prototype.pushUniqueCaseless.apply(result[6], withArtists); title = title.replace(rx, ''); }); if (defaultPerformer) for (let ndx of [0, 7]) if (result[ndx].length <= 0) result[ndx] = [defaultPerformer]; result[6] = result[6].filter(artist => ![0, 9, 14].some(index => result[index].includes(artist))); // for (let ndx = 0; ndx < result.length; ++ndx) if (ndx != 8) result[ndx] = result[ndx] // .filter(trackArtist => ![0, 1, 4].some(ndx => pseudoArtistParsers[ndx].test(trackArtist))); for (let ndx = 0; ndx < result.length; ++ndx) if (ndx != 8) result[ndx] = result[ndx].filter(realArtistName); //console.debug('\tFiltered:', personnel[0], personnel[6]); return result; } return queryQobuzAPI('album/get', { album_id: QOBUZ_ID }).then(function(response) { if (response.tracks_count > response.tracks.limit) throw 'Tracklist length exceeding batch size'; switch (response.release_type || response.product_type) { //case 'album': releaseType = 'Album'; break; case 'single': releaseType = 'Single'; break; case 'ep': case 'epmini': releaseType = 'EP'; break; } isVA = vaParser.test(response.artist.name); album = response.title.replace(/\s+/g, ' '); if (response.version) { let version = ' (' + response.version + ')'; if (!album.toLowerCase().endsWith(version.toLowerCase())) album += version; } let albumArtists = [ ]; for (let ndx = 0; ndx < qobuzArtistLabels.length; ++ndx) albumArtists[ndx] = [ ]; if (response.artists) for (let _artist of response.artists) for (let ndx of _artist.roles.map(getCategoryIndex)) albumArtists[ndx >= 0 ? ndx : 13].pushUniqueCaseless(_artist.name); for (let ndx of [1, 2, 3, 4, 5]) if (albumArtists[0].length <= 0 && albumArtists[ndx].length > 0) albumArtists[0] = albumArtists[ndx]; //albumArtists[0] = albumArtists[0].filter(_artist => ![9, 14].some(index => albumArtists[index].includes(_artist))); if (albumArtists[0].length <= 0) albumArtists[0] = response.artists.map(albumArtists => albumArtists.name); albumArtists[6] = albumArtists[6].filter(_artist => ![0, 9, 14].some(index => albumArtists[index].includes(_artist))); // for (let ndx = 0; ndx < albumArtists.length; ++ndx) if (ndx != 8) albumArtists[ndx] = albumArtists[ndx] // .filter(albumArtists => ![0, 1, 4].some(ndx => pseudoArtistParsers[ndx].test(albumArtists))); artist = response.artists ? joinArtists(response.artists.filter(artist => artist.roles.some(role => [0, 1, 2, 3, 4, 5/*, 7*/].includes(getCategoryIndex(role)))).map(artist => artist.name)) : response.artist.name.replace(/\s+/g, ' '); featArtists = Array.from(albumArtists[6]); featArtistParsers.slice(1, 6).forEach(function(rx, index) { var matches = rx.exec(album); if (matches == null) return; Array.prototype.pushUniqueCaseless.apply(featArtists, splitAmpersands(matches[1])); album = album.replace(rx, ''); }); featArtistParsers.slice(6, 8).forEach(function(rx, index) { let matches = rx.exec(album); if (matches == null) return; let withArtists = splitAmpersands(matches[2]); if (!withArtists.every(artist => albumArtists.some(albumArtist => albumArtist.includes(artist)))) return; Array.prototype.pushUniqueCaseless.apply(featArtists, withArtists); album = album.replace(rx, ''); }); if ((featArtists = featArtists.filter(realArtistName)).length > 0 && !featTest.test(artist)) artist += ' feat. ' + joinArtists(featArtists); if (response.description) description = domParser.parseFromString(response.description, 'text/html') .body.textContent.trim().flatten(); response.tracks.items.forEach(function(track, index) { title = track.title; if (track.version) title += ' (' + track.version + ')'; personnel = getTrackArtists(track.performers, track.performer && track.performer.name); trackArtist = joinArtists(personnel[0]); //if (trackArtist && personnel[9].length > 0) trackArtist += ' under ' + joinArtists(personnel[9]); if (trackArtist && personnel[6].length > 0) trackArtist += ' feat. ' + joinArtists(personnel[6]); tracks.push([ /* 00 */ isVA ? VA : artist, /* 01 */ album, /* 02 */ response.release_date_original, /* 03 */ response.genre ? response.genre.name.replace(/\s+/g, ' ') : undefined, /* 04 */ response.label ? response.label.name.replace(/\s+/g, ' ') : undefined, /* 05 */ response.media_count > 1 ? track.media_number || 1 : undefined, /* 06 */ track.work || undefined, /* 07 */ response.media_count > 1 ? response.media_count : undefined, /* 08 */ track.track_number || index + 1, /* 09 */ response.tracks_count || response.tracks.total, /* 10 */ trackArtist, /* 11 */ title.replace(/\s+/g, ' '), /* 12 */ personnel[8].length > 0 ? personnel[8].join(', ') : track.composer ? track.composer.name : response.composer ? response.composer.name : undefined, /* 13 */ [personnel[0], personnel[qobuzArtistLabels.length], personnel.slice(1, 8)] .flatten().distinctValues().join(', ') || trackArtist || !isVA && artist, /* 14 */ joinArtists(personnel[9]), // conductors //joinArtists(personnel[10]), //joinArtists(personnel[11]), /* 15 */ 'Digital Media', // WEB /* 16 */ response.url, /* 17 */ description, /* 18 */ releaseType || undefined, /* 19 */ response.upc || undefined, /* 20 */ track.isrc || undefined, /* 21 */ track.parental_warning ? 1 : undefined, ]); }); return finalizeTracks(); }).catch(function(reason) { console.info('Qobuz API method failed for the reason', reason); 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) var subCategory = gtm.shop.subCategory.replace(/-/g, ' '); //if (gtm.type) var 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; featArtistParsers.slice(1, 6).forEach(function(rx, index) { var matches = rx.exec(album); if (matches == null) return; Array.prototype.pushUniqueCaseless.apply(featArtists, splitAmpersands(matches[1])); album = album.replace(rx, ''); }); featArtistParsers.slice(6, 8).forEach(function(rx, index) { let matches = rx.exec(album); if (matches == null) return; let withArtists = splitAmpersands(matches[2]); if (!withArtists.every(artist => false)) return; // TODO: verify if all (with ...) items are artists Array.prototype.pushUniqueCaseless.apply(featArtists, withArtists); album = album.replace(rx, ''); }); if ((featArtists = featArtists.filter(realArtistName)).length > 0 && !featTest.test(artist)) artist += ' feat. ' + joinArtists(featArtists); releaseDate = (ref = document.querySelector('div.album-meta > ul > li:first-of-type')) != null ? normalizeDate(ref.textContent) : undefined; let mainArtist = (ref = document.querySelector('div.album-meta > ul > li:nth-of-type(2) > a')) != null ? ref.title || ref.textContent.trim() : undefined; if (mainArtist && featArtists.length > 0 && !featTest.test(mainArtist)) mainArtist += ' feat. ' + joinArtists(featArtists); //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(); } }); description = Array.from(document.querySelectorAll('section#description > p')) .map(p => p.textContent.trim()).filter(Boolean).join('\n\n').flatten(); url = (ref = document.querySelector('meta[property="og:url"]')) != null ? ref.content : document.URL; addTracks(document); if (totalTracks <= 50) return Promise.resolve(finalizeTracks()); let 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 localXHR('/v4/ajax/album/load-tracks?' + params).then(dom => { addTracks(dom) }, function(reason) { console.error('localXHR() 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(div, index) { let TRACK_ID = div.parentNode.dataset.track; title = (ref = [ 'div.track__item--name > span', 'div.track__item--name--track > span', 'span.track__item--name', ].reduce((acc, sel) => acc || div.querySelector(sel), null)) != null ? ref.textContent.trim().replace(/\s+/g, ' ') : undefined; ref = div.parentNode.querySelector('p.track__info:first-of-type'); personnel = div.querySelector('div.track__item--performer > span') || div.querySelector('div.track__item--name[itemprop="performer"] > span'); personnel = getTrackArtists(ref != null && ref.textContent.trim(), personnel != null && personnel.textContent.trim()); trackArtist = personnel[0].length > 0 ? joinArtists(personnel[0]) : undefined; if (!trackArtist) if (!isVA) trackArtist = artist.replace(/\s+/g, ' '); else console.warn('Qobuz: track main artist missing for track', index + 1, div); //if (trackArtist && personnel[9].length > 0) trackArtist += ' under ' + joinArtists(personnel[9]); if (trackArtist && personnel[6].length > 0) trackArtist += ' feat. ' + joinArtists(personnel[6]); let trackGenres = [ ]; if (div.parentNode.dataset.gtm) try { let gtm = JSON.parse(div.parentNode.dataset.gtm); //if (gtm.product.id) QOBUZ_ID = gtm.product.id; if (gtm.product.subCategory) trackGenres.pushUniqueCaseless(gtm.product.subCategory.replace(/-/g, ' ')); if (gtm.product.type) releaseType = gtm.product.type; } catch(e) { console.warn(e) } trackGenres = trackGenres.map(function(genre) { genre = qbGenreToEnglish(genre.replace(/-/g, ' ')) return genre.split(/\s+/).map(word => word[0].toUpperCase() + word.slice(1).toLowerCase()).join(' '); }); if ((ref = div.parentNode.parentNode.parentNode.querySelector('p.player__work:first-child')) != null) { discSubtitle = ref.textContent.replace(/\s+/g, ' ').trim(); guessDiscNumber(); } return [ /* 00 */ isVA ? VA : artist.replace(/\s+/g, ' '), /* 01 */ album.replace(/\s+/g, ' '), /* 02 */ releaseDate, /* 03 */ genres.map(qbGenreToEnglish).join(', ').replace(/\s+/g, ' '), /* 04 */ label, /* 05 */ totalDiscs > 1 ? discNumber || 1 : undefined, /* 06 */ discSubtitle, /* 07 */ totalDiscs > 1 ? totalDiscs : undefined, /* 08 */ (ref = div.querySelector('div.track__item--number > span') || div.querySelector('span[itemprop="position"]')) != null ? parseInt(ref.textContent) : undefined, /* 09 */ totalTracks, /* 10 */ trackArtist, /* 11 */ title.replace(/\s+/g, ' '), /* 12 */ personnel[8].length > 0 ? personnel[8].join(', ') : composer, /* 13 */ [personnel[0], personnel[qobuzArtistLabels.length], personnel.slice(1, 8)] .flatten().distinctValues().join(', ') || trackArtist || !isVA && artist, /* 14 */ joinArtists(personnel[9]), // conductors //joinArtists(personnel[10]), //joinArtists(personnel[11]), /* 15 */ 'Digital Media', // WEB /* 16 */ url, /* 17 */ description, /* 18 */ releaseType && releaseType.toLowerCase() != 'album' ? releaseType : undefined, ]; })); } }); function finalizeTracks() { if (!isVA && tracks.every(track => track[10] && track[10] == tracks[0][10])) tracks.forEach(track => { track[0] = track[10] }); return tracks; } } let button = document.querySelector('button.player-share__button'); if (button != null) { button.onclick = copyTracks; button.classList.remove('pct-share'); button.style = 'font: 700 small "Segoe UI", Tahome, sans-serif; padding: 3px; background-color: lightgray; width: 12em;'; button.textContent = 'Copy album metadata'; } if (typeof GM_registerMenuCommand == 'function' && typeof GM_setClipboard == 'function') GM_registerMenuCommand('Store foobar2000\'s parsing string to clipboard', function() { GM_setClipboard([ /* 00 */ 'album artist', 'album', 'releasedate', 'genre', 'label', 'discnumber', 'discsubtitle', 'totaldiscs', /* 08 */ 'tracknumber', 'totaltracks', 'artist', 'title', 'composer', 'performer', 'conductor', 'media', 'url', /* 17 */ 'comment', 'releasetype', 'upc', 'isrc', 'explicit', ].map(tagName => '%' + tagName + '%').join('\x1E'), 'text'); });