[GMT] Artist Chronology

Browse artist's releases in chronological order directly from release page

目前為 2024-09-22 提交的版本,檢視 最新版本

// ==UserScript==
// @name         [GMT] Artist Chronology
// @version      1.00
// @description  Browse artist's releases in chronological order directly from release page
// @match        https://*/torrents.php?id=*
// @match        https://*/torrents.php?page=*&id=*
// @run-at       document-end
// @author       Anakunda
// @namespace    https://greasyfork.org/users/321857
// @copyright    2024, Anakunda (https://greasyfork.org/users/321857)
// @license      GPL-3.0-or-later
// @grant        GM_getValue
// @grant        GM_setValue
// @require      https://openuserjs.org/src/libs/Anakunda/libLocks.min.js
// @require      https://openuserjs.org/src/libs/Anakunda/gazelleApiLib.min.js
// ==/UserScript==

'use strict';

const anchor = document.body.querySelector('table#vote_matches')
	|| document.body.querySelector('div.box.torrent_description');
if (anchor == null) return;
const searchParams = new URLSearchParams(document.location.search);
const groupId = parseInt(searchParams.get('id'));
console.assert(groupId > 0);
if (!(groupId > 0)) return;
let chronologyArtistId = parseInt(searchParams.get('chronology_id')) || undefined;
let releaseType = document.body.querySelector('div#content div.header > h2');
if (releaseType != null) releaseType = /\s+\[([^\[\]]+)\]$/.exec(releaseType.lastChild.textContent);
if (releaseType != null) releaseType = {
	'Album': 1, 'Soundtrack': 3, 'EP': 5, 'Anthology': 6, 'Compilation': 7, 'Single': 9, 'Live album': 11,
	'Remix': 13, 'Bootleg': 14, 'Interview': 15, 'Mixtape': 16, 'Demo': 17, 'Concert Recording': 18, 'DJ Mix': 19,
	'Unknown': 21,
}[releaseType[1]];
const getArtistIds = className => Array.from(document.body.querySelectorAll(`ul#artist_list > li.${className} > a`),
	a => parseInt((a = new URLSearchParams(a.search)).get('id'))).filter(artistId => artistId > 0);
const artistIds = {
	main: getArtistIds('artist_main'),
	producer: getArtistIds('artists_producer'),
	composer: getArtistIds('artists_composers'),
	remixer: getArtistIds('artists_remix'),
	DJ: getArtistIds('artists_dj'),
};
const minifyHTML = html => html.replace(/\s*(?:\r?\n)+\s*/g, '');
const forward = (scaleX = 1) => minifyHTML(`
<svg height="20px" viewBox="0 0 82.66 82.66" transform="scale(${scaleX},1)" xmlns="http://www.w3.org/2000/svg">
  <circle fill="#8888" cx="41" cy="41.33" r="41.04"/>
  <polygon fill="white" points="46.27,22.55 59.12,31.94 71.97,41.33 59.12,50.72 46.27,60.12 46.27,47.41 38.39,53.17 22.19,65.02 22.19,41.33 22.19,17.65 38.39,29.49 46.27,35.26 "/>
</svg>
`);
const fastForward = (scaleX = 1) => minifyHTML(`
<svg height="20px" viewBox="0 0 93.33 93.33" transform="scale(${scaleX},1)" xmlns="http://www.w3.org/2000/svg">
  <circle fill="#8888" cx="46.67" cy="46.67" r="46.67"/>
  <path fill="white" d="M58.69 20.71l12.12 0 0 51.91 -12.12 0 0 -51.91zm-0.31 25.96l-17.93 12.97 -17.93 12.98 0 -25.95 0 -25.96 17.93 12.98 17.93 12.98z"/>
</svg>
`);
const artistChronology = 'artistChronology' in sessionStorage ?
	JSON.parse(sessionStorage.getItem('artistChronology')) : { };
const box = Object.assign(document.createElement('div'), { innerHTML: minifyHTML(`
<div class="box chronology">
	<div class="head">
		<a href="#">↑</a>&nbsp;<strong>Chronology</strong>
		<label style="float: right; margin-left: 2rem;">Lock to release type
			<input name="releasetype-lock" type="checkbox" style="margin-left: 0.5rem;">
		</label>
		<a href="#" class="artist-lock" style="float: right; margin-left: 2rem;"></a>
	</div>
	<div class="body" style="padding: 0;"></div>
</div>
`) });
const [body, artistLock, lockState] = ['div.body', 'a.artist-lock','input[type="checkbox"][name="releasetype-lock"]']
	.map(Element.prototype.querySelector.bind(box));
console.assert(body != null && artistLock != null && lockState != null);

function loadChronology(releaseTypeLocked = true, lockedArtistId) {
	while (body.lastChild != null) body.removeChild(body.lastChild);
	let filteredArtistIds = Array.prototype.concat.apply([ ], Object.values(artistIds).map(artistIds =>
		lockedArtistId > 0 ? artistIds.filter(artistId => artistId == lockedArtistId) : artistIds).filter(artistIds =>
			artistIds.length <= GM_getValue('max_artists_load', 5)));
	filteredArtistIds = filteredArtistIds.filter((artistId, index, artistIds) => artistIds.indexOf(artistId) == index);
	filteredArtistIds.forEach(function(artistId) {
		(artistId in artistChronology ? Promise.resolve(artistChronology[artistId]) : queryAjaxAPI('artist', { id: artistId }).then(function(artist) {
			artistChronology[artistId] = {
				name: artist.name,
				torrentGroups: artist.torrentgroup.map(torrentGroup => ({
					id: torrentGroup.groupId,
					name: torrentGroup.groupName,
					year: torrentGroup.groupYear,
					releaseType: torrentGroup.releaseType,
				})).sort((a, b) => (a.year || Infinity) - (b.year || Infinity)),
				timestamp: Date.now(),
			};
			sessionStorage.setItem('artistChronology', JSON.stringify(artistChronology));
			return artistChronology[artistId];
		})).then(function(artistChronology) {
			function chronologyByRole(torrentGroups, role) {
				function addNavigation(className, index, icon) {
					if (torrentGroups[index]) artistNav.querySelectorAll('div.' + className).forEach(function(div) {
						if (div.classList.contains('link')) {
							const link = content => `<a href="/torrents.php?${new URLSearchParams({
								id: torrentGroups[index].id,
								chronology_id: artistId,
							}).toString()}">${content}</a>`;
							if (!div.classList.contains('icon'))
								div.innerHTML = `${link(torrentGroups[index].name)}<br>(${torrentGroups[index].year})`;
							else if (icon) div.innerHTML = link(icon);
						} else {
							if (!div.classList.contains('icon'))
								div.innerHTML = `<span>${torrentGroups[index].name}</span><br>(${torrentGroups[index].year})`;
							else if (icon) div.innerHTML = icon;
						}
					});
				}

				torrentGroups = torrentGroups.filter((tg1, index, torrentGroups) =>
					torrentGroups.findIndex(tg2 => tg2.id == tg1.id) == index);
				const groupIndex = torrentGroups.findIndex(torrentGroup => torrentGroup.id == groupId);
				if (groupIndex < 0) return;
				const artistNav = Object.assign(document.createElement('div'), {
					className: `chronology-${artistId}-${role}`,
					innerHTML: minifyHTML(`
<div style="text-align: center; padding: 5px 10px; background-color: #b0c4de66;">
	<a href="/artist.php?id=${artistId}">${artistChronology.name}</a>'s chronology (as ${role}) [${groupIndex + 1}/${torrentGroups.length}]
</div>
<div style="display: flex; flex-flow: row nowrap; padding: 5px 10px; justify-content: space-between; column-gap: 1rem;">
	<div class="first icon link"></div>
	<div class="previous link" style="text-align: center;"></div>
	<div class="previous icon link"></div>
	<div class="current" style="text-align: center;"></div>
	<div class="next icon link"></div>
	<div class="next link" style="text-align: center;"></div>
	<div class="last icon link"></div>
</div>
`), });
				if (groupIndex > 0) {
					addNavigation('first', 0, fastForward(-1));
					addNavigation('previous', groupIndex - 1, forward(-1));
				}
				addNavigation('current', groupIndex);
				if (groupIndex < torrentGroups.length - 1) {
					addNavigation('next', groupIndex + 1, forward(1));
					addNavigation('last', torrentGroups.length - 1, fastForward(1));
				}
				body.append(artistNav);
			}

			let torrentGroups = artistChronology.torrentGroups;
			if (releaseTypeLocked && releaseType > 0) torrentGroups = torrentGroups.filter(torrentGroup =>
				torrentGroup.releaseType >= 1000 || torrentGroup.releaseType == releaseType);
			chronologyByRole(torrentGroups.filter(torrentGroup => torrentGroup.releaseType < 1000), 'main');
			chronologyByRole(torrentGroups.filter(torrentGroup => torrentGroup.releaseType == 1021), 'producer');
			chronologyByRole(torrentGroups.filter(torrentGroup => torrentGroup.releaseType == 1022), 'composer');
			chronologyByRole(torrentGroups.filter(torrentGroup => torrentGroup.releaseType == 1023), 'remixer');
		}, alert);
	});
}

if (chronologyArtistId > 0) Object.assign(artistLock, {
	innerHTML: '<span class="artist-lock-state">Release</span> artist Id',
	onclick: function(evt) {
		chronologyArtistId = chronologyArtistId > 0 ? undefined : parseInt(searchParams.get('chronology_id'));
		loadChronology(lockState.checked, chronologyArtistId);
		evt.currentTarget.querySelector('span.artist-lock-state').textContent =
			chronologyArtistId > 0 ? 'Release' : 'Lock to';
		return false;
	},
}); else artistLock.remove();
Object.assign(lockState, {
	checked: 'chronologyLock' in sessionStorage ? Boolean(Number(sessionStorage.getItem('chronologyLock'))) : true,
	onchange: function(evt) {
		sessionStorage.setItem('chronologyLock', Number(evt.currentTarget.checked));
		loadChronology(evt.currentTarget.checked, chronologyArtistId);
	},
});
loadChronology(lockState.checked, chronologyArtistId);
anchor.before(box);