Greasy Fork

Greasy Fork is available in English.

Qobuz - Copy album info

Copy metadata to parseable format

当前为 2020-07-18 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name         Qobuz - Copy album info
// @version      1.01
// @author       Anakunda
// @namespace    http://greasyfork.icu/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      http://greasyfork.icu/scripts/404642-js-xhr/code/js-xhr.js
// @require      http://greasyfork.icu/scripts/406257-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, 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));
		trackArtists[1] = trackArtists[0].filter(artist => ![0, 4].some(index => trackArtists[index].includes(artist)));
		var trackArtist = joinArtists(trackArtists[0]);
		if (trackArtist && trackArtists[1].length > 0) trackArtist += ' feat. ' + joinArtists(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.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, ' '),
		trackArtists[3].length > 0 ? trackArtists[3].join(', ') : composer,
		trackArtists[2].join(', '),
		//joinArtists(trackArtists[4]),
		//joinArtists(trackArtists[5]),
		//joinArtists(trackArtists[6]),
		'WEB',
		url,
		description,
	  ];
	}));
  }

  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 createButton() {
  var button = document.querySelector('button.player-share__button');
  if (button != null) button.onclick = copyTracks;
}