Greasy Fork

Qobuz - Copy album info

Copy metadata to parseable format

目前为 2020-07-12 提交的版本。查看 最新版本

// ==UserScript==
// @name         Qobuz - Copy album info
// @version      1.01
// @author       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
// @require      https://greasyfork.org/scripts/404642-js-xhr/code/js-xhr.js
// @require      https://greasyfork.org/scripts/404637-qobuzlib/code/QobuzLib.js
// ==/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%

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;
};

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

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|\<various\s+artists\>|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, ' '));
  } 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();
  url = (ref = document.querySelector('meta[property="og:url"]')) != null ? ref.content : document.URL;
  addTracks(document);
  if (totalTracks <= 50) return Promise.resolve(tracks);
  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(response => { addTracks(response.document) }, function(reason) {
	  console.error('globalFetch() failed:', reason);
	}).then(() => tracks);

  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) {
	  var TRACK_ID = tr.parentNode.dataset.track;
	  var trackArtist, trackGuests = [], trackComposers = [], trackRemixers = [],
		  trackConductors = [], trackProducers = [], trackPerformers = [];
	  if ((ref = tr.parentNode.querySelector('p.track__info:first-of-type')) != null) {
		trackArtist = [];
		ref.textContent.trim().split(/\s+-\s+/).map(it => it.split(/\s*,\s*/)).forEach(function(it) {
		  var roles = it.slice(1);
		  if (['Artist', 'MainArtist'].some(role => roles.includes(role))) trackArtist.pushUnique(it[0]);
		  if (['FeaturedArtist'].some(role => roles.includes(role))) trackGuests.pushUnique(it[0]);
		  if (['Artist', 'MainArtist', 'FeaturedArtist', 'AssociatedPerformer', 'Ensemble'].some(role => roles.includes(role)))
			trackPerformers.pushUnique(it[0]);
		  if (['Composer', 'ComposerLyricist', 'Writer'].some(role => roles.includes(role)))
			trackComposers.pushUnique(it[0]);
		  if (['Conductor'].some(role => roles.includes(role))) trackConductors.pushUnique(it[0]);
		  if (['Remixer'].some(role => roles.includes(role))) trackRemixers.pushUnique(it[0]);
		  if (['Producer'].some(role => roles.includes(role))) trackProducers.pushUnique(it[0]);
		});
		trackArtist = trackArtist.filter(artist => !trackConductors.includes(artist));
		trackGuests = trackGuests.filter(artist => ![trackArtist, trackConductors].some(category => category.includes(artist)));
		if ((trackArtist = joinArtists(trackArtist)) && trackGuests.length > 0)
		  trackArtist += ' feat. ' + joinArtists(trackGuests);
	  } else trackArtist = undefined;
	  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.type) trackIdentifiers.RELEASETYPE = gtm.product.type;
		if (gtm.product.subCategory) var subCategory = [gtm.product.subCategory];
	  } 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 > span') || tr.querySelector('span.track__item--name'))
			.textContent.trim().replace(/\s+/g, ' '),
		trackComposers.length > 0 ? joinArtists(trackComposers) : composer,
		joinArtists(trackPerformers),
		//joinArtists(trackConductors),
		//joinArtists(trackRemixers),
		//trackProducers.length > 0 ? trackProducers : undefined,
		'WEB',
		url,
		description,
	  ];
	}));
  }

  function normalizeDate(str, countryCode = undefined) {
	if (typeof str != 'string') return null;
	if (/\b(\d{4})-(\d{1,2})-(\d{1,2})\b/.test(str)) // US
	  return RegExp.$1 + '-' + RegExp.$2.padStart(2, '0') + '-' + RegExp.$3.padStart(2, '0');
	if (/\b(\d{1,2})\/(\d{1,2})\/(\d{2})\b/.test(str) && (parseInt(RegExp.$1) > 12 || /\b(?:be|it)/.test(countryCode))) // BE, IT
	  return '20' + RegExp.$3 + '-' + RegExp.$2.padStart(2, '0') + '-' + RegExp.$1.padStart(2, '0');
	if (/\b(\d{1,2})\/(\d{1,2})\/(\d{2})\b/.test(str)) // US (clash with BE, IT)
	  return '20' + RegExp.$3 + '-' + RegExp.$1.padStart(2, '0') + '-' + RegExp.$2.padStart(2, '0');
	if (/\b(\d{1,2})\/(\d{1,2})\/(\d{4})\b/.test(str)) // UK, IE, FR, ES
	  return RegExp.$3 + '-' + RegExp.$2.padStart(2, '0') + '-' + RegExp.$1.padStart(2, '0');
	if (/\b(\d{1,2})-(\d{1,2})-(\d{2})\b/.test(str))  // NL
	  return '20' + RegExp.$3 + '-' + RegExp.$2.padStart(2, '0') + '-' + RegExp.$1.padStart(2, '0');
	if (/\b(\d{1,2})\. *(\d{1,2})\. *(\d{4})\b/.test(str))
	  return RegExp.$3 + '-' + RegExp.$2.padStart(2, '0') + '-' + RegExp.$1.padStart(2, '0');
	if (/\b(\d{1,2})\. *(\d{1,2})\. *(\d{2})\b/.test(str))  // AT, CH, DE, LU, CE
	  return '20' + RegExp.$3 + '-' + RegExp.$2.padStart(2, '0') + '-' + RegExp.$1.padStart(2, '0');
	if (/\b(\d{4})\. *(\d{1,2})\. *(\d{1,2})\b/.test(str)) // JP
	  return RegExp.$1 + '-' + RegExp.$2.padStart(2, '0') + '-' + RegExp.$3.padStart(2, '0');
	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 createButton() {
  var button = document.querySelector('button.player-share__button');
  if (button != null) button.onclick = copyTracks;
}