Greasy Fork

[GMT] CD DiscID Lookup

Lookup CD by TOC

目前为 2023-01-29 提交的版本。查看 最新版本

// ==UserScript==
// @name         [GMT] CD DiscID Lookup
// @namespace    https://greasyfork.org/users/321857-anakunda
// @version      1.05
// @description  Lookup CD by TOC
// @match        https://*/torrents.php?id=*
// @match        https://*/torrents.php?page=*&id=*
// @run-at       document-end
// @grant        GM_xmlhttpRequest
// @grant        GM_openInTab
// @connect      musicbrainz.org
// @author       Anakunda
// @iconURL      https://musicbrainz.org/favicon.ico
// @copyright    2023, Anakunda (https://greasyfork.org/users/321857-anakunda)
// @license      GPL-3.0-or-later
// @require      https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.1.1/crypto-js.min.js
// @require      https://openuserjs.org/src/libs/Anakunda/xhrLib.min.js
// ==/UserScript==

{

'use strict';

const requestsCache = new Map, mbRequestsCache = new Map;
const msf = 75, preGap = 2 * msf, dataTrackGap = 11400;
let mbLastRequest = null;

function getTorrentId(tr) {
	if (!(tr instanceof HTMLElement)) throw 'Invalid argument';
	if ((tr = tr.querySelector('a.button_pl')) != null && (tr = parseInt(new URLSearchParams(tr.search).get('torrentid'))) > 0)
		return tr;
	throw 'Failed to get torrent id';
}

function lookupByToc(torrentId, callback) {
	if (!(torrentId > 0) || typeof callback != 'function') return Promise.reject('Invalid argument');
	const msfTime = '(?:(\\d+):)?(\\d+):(\\d+)[\\.\\:](\\d+)';
	const msfToSector = time => Array.isArray(time) || (time = new RegExp('^\\s*' + msfTime + '\\s*$').exec(time)) != null ?
		(((time[1] ? parseInt(time[1]) : 0) * 60 + parseInt(time[2])) * 60 + parseInt(time[3])) * msf + parseInt(time[4]) : NaN;
	// 1211 + 1287
	const rxRR = /^(?:Selected range|Выбранный диапазон|Âûáðàííûé äèàïàçîí|已选择范围|選択された範囲|Gewählter Bereich|Intervallo selezionato|Geselecteerd bereik|Utvalt område|Seleccionar gama|Избран диапазон|Wybrany zakres|Izabrani opseg|Vybraný rozsah)(?:[^\S\r\n]+\((?:Sectors|Секторы|扇区|Sektoren|Settori|Sektorer|Sectores|Сектори|Sektora|Sektory)[^\S\r\n]+(\d+)[^\S\r\n]*-[^\S\r\n]*(\d+)\))?$/m;
	const tocParser = '^\\s*' + ['\\d+', msfTime, msfTime, '\\d+', '\\d+']
		.map(pattern => '(' + pattern + ')').join('\\s+\\|\\s+') + '\\s*$';
	function tocEntriesMapper(tocEntry, trackNdx) {
		if ((tocEntry = new RegExp(tocParser).exec(tocEntry)) == null)
			throw `assertion failed: track ${trackNdx + 1} ToC entry invalid format`;
		console.assert(msfToSector(tocEntry[2]) == parseInt(tocEntry[12]));
		console.assert(msfToSector(tocEntry[7]) == parseInt(tocEntry[13]) + 1 - parseInt(tocEntry[12]));
		return {
			trackNumber: parseInt(tocEntry[1]),
			startSector: parseInt(tocEntry[12]),
			endSector: parseInt(tocEntry[13]),
		};
	};
	let request;
	if (requestsCache.has(torrentId)) request = requestsCache.get(torrentId); else {
		request = localXHR('/torrents.php?' + new URLSearchParams({ action: 'loglist', torrentid: torrentId })).then(document =>
			Array.from(document.body.querySelectorAll(':scope > blockquote > pre:first-child'), pre => pre.textContent).filter(function(logFile) {
				logFile = logFile.trimLeft();
				if (!['Exact Audio Copy', 'EAC', 'X Lossless Decoder'].some(prefix => logFile.startsWith(prefix))) return false;
				const rr = rxRR.exec(logFile);
				if (rr == null) return true;
				// Ditch HTOA logs
				let tocEntries = logFile.match(new RegExp(tocParser, 'gm'));
				if (tocEntries != null) tocEntries = tocEntries.map(tocEntriesMapper); else return true;
				return parseInt(rr[1]) != 0 || parseInt(rr[2]) + 1 != tocEntries[0].startSector;
			}));
		requestsCache.set(torrentId, request);
	}
	return request.then(logfiles => logfiles.length > 0 ? Promise.all(logfiles.map(function(logfile, volumeNdx) {
		const isRangeRip = rxRR.test(logfile);
		let tocEntries = logfile.match(new RegExp(tocParser, 'gm'));
		if (tocEntries != null && tocEntries.length > 0) tocEntries = tocEntries.map(tocEntriesMapper);
			else throw `disc ${volumeNdx + 1} ToC not found`;
		let layoutType = 0;
		for (let index = 0; index < tocEntries.length - 1; ++index) {
			const gap = tocEntries[index + 1].startSector - tocEntries[index].endSector - 1;
			if (gap == 0) continue; else layoutType = gap == dataTrackGap && index == tocEntries.length - 2 ? 1 : -1;
			break;
		}
		if (layoutType == 1) tocEntries.pop();
		return callback(tocEntries);
	}).map(results => results.catch(function(reason) {
		console.log('DiscID lookup failed:', reason);
		return null;
	}))) : Promise.reject('no valid log files found'));
}

for (let tr of Array.prototype.filter.call(document.body.querySelectorAll('table#torrent_details > tbody > tr.torrent_row'),
		tr => (tr = tr.querySelector('td > a')) != null && /\b(?:FLAC)\b.+\b(?:Lossless)\b.+\b(?:Log) \(\-?\d+\s*\%\)/.test(tr.textContent))) {
	function addLookup(caption, callback, tooltip) {
		const a = document.createElement('A');
		a.textContent = caption;
		a.className = 'brackets';
		a.href = '#';
		a.dataset.torrentId = torrentId;
		a.onclick = callback;
		if (tooltip) a.title = tooltip;
		tr.append(' ', a);
	}

	let torrentId = getTorrentId(tr);
	if (!(torrentId > 0)) continue;
	if ((tr = tr.nextElementSibling) == null || !tr.classList.contains('torrentdetails')) continue;
	if ((tr = tr.querySelector('div.linkbox')) == null) continue;
	addLookup('DiscID lookup', function(evt) {
		const target = evt.currentTarget;
		console.assert(target instanceof HTMLElement);
		const baseUrl = 'https://musicbrainz.org/cdtoc/';
		if (!target.disabled) if (Boolean(target.dataset.haveResponse)) {
			if (!('results' in target.dataset)) return false;
			const results = JSON.parse(target.dataset.results);
			for (let result of results.releases) GM_openInTab('https://musicbrainz.org/release/' + result.id, false);
			// if (results.mbDiscID) {
			// 	GM_openInTab(baseUrl + 'attach?toc=' + results.mbTOC.join(' '), false);
			// 	GM_openInTab(baseUrl + results.mbDiscID, false);
			// }
		} else {
			function mbQueryAPI(endPoint, params) {
				if (!endPoint) throw 'Endpoint is missing';
				const url = new URL('/ws/2/' + endPoint.replace(/^\/+|\/+$/g, ''), 'https://musicbrainz.org');
				url.search = new URLSearchParams(Object.assign({ fmt: 'json' }, params));
				const cacheKey = url.pathname.slice(6) + url.search;
				if (mbRequestsCache.has(cacheKey)) return mbRequestsCache.get(cacheKey);
				const request = new Promise((resolve, reject) => { (function request(reqCounter = 1) {
					if (reqCounter > 60) return reject('Request retry limit exceeded');
					if (mbLastRequest == Infinity) return setTimeout(request, 100, reqCounter);
					const now = Date.now();
					if (now <= mbLastRequest + 1000) return setTimeout(request, mbLastRequest + 1000 - now, reqCounter);
					mbLastRequest = Infinity;
					globalXHR(url, { responseType: 'json' }).then(function({response}) {
						mbLastRequest = Date.now();
						resolve(response);
					}, function(reason) {
						mbLastRequest = Date.now();
						if (/^HTTP error (?:429|430)\b/.test(reason)) return setTimeout(request, 1000, reqCounter + 1);
						reject(reason);
					});
				})() });
				mbRequestsCache.set(cacheKey, request);
				return request;
			}
			function mbComputeDiscID(mbTOC) {
				if (!Array.isArray(mbTOC) || mbTOC.length != mbTOC[1] - mbTOC[0] + 4 || mbTOC[1] - mbTOC[0] > 98) return null;
				let tocStr = ([mbTOC[0], mbTOC[1]].map(track => track.toString(16).padStart(2, '0'))
					.concat(mbTOC.slice(2).map(offset => offset.toString(16).padStart(8, '0'))).join('') +
					'0'.repeat(98 + mbTOC[0] - mbTOC[1] << 3)).toUpperCase();
				return CryptoJS.SHA1(tocStr).toString(CryptoJS.enc.Base64)
					.replace(/\=/g, '-').replace(/\+/g, '.').replace(/\//g, '_');
			}
			function mbLookupByDiscID(mbTOC, allowTOCLookup = true) {
				if (!Array.isArray(mbTOC) || mbTOC.length != mbTOC[1] - mbTOC[0] + 4)
					return Promise.reject('mbLookupByDiscID(…): missing or invalid TOC');
				const mbDiscID = mbComputeDiscID(mbTOC), params = { };
				if (!mbDiscID || allowTOCLookup) params.toc = mbTOC.join('+');
				//params['media-format'] = 'all';
				return mbQueryAPI('discid/' + (mbDiscID || '-'), params).then(function(result) {
					if (!('releases' in result) && !/^[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12}$/i.test(result.id))
						return Promise.reject('MusicBrainz: no matches');
					const releases = result.releases || (['id', 'title'].every(key => key in result) ? [result] : null);
					if (!Array.isArray(releases) || releases.length <= 0) return Promise.reject('MusicBrainz: no matches');
					console.log('MusicBrainz lookup by discId/TOC successfull:', mbDiscID, '/', params, 'releases:', releases);
					return { mbDiscID: result.id, mbTOC: mbTOC, releases: releases };
				});
			}

			target.disabled = true;
			target.style.color = null;
			target.textContent = 'Looking up...';
			lookupByToc(parseInt(target.dataset.torrentId), function(tocEntries) {
				const isHTOA = tocEntries[0].startSector > preGap, mbTOC = [tocEntries[0].trackNumber, tocEntries.length];
				mbTOC.push(preGap + tocEntries[tocEntries.length - 1].endSector + 1);
				Array.prototype.push.apply(mbTOC, tocEntries.map(tocEntry => preGap + tocEntry.startSector));
				return mbLookupByDiscID(mbTOC, !evt.ctrlKey);
			}).then(function(results) {
				if (results.length <= 0 || results[0] == null) {
					if (!evt.ctrlKey) target.dataset.haveResponse = true;
					target.textContent = 'No matches';
					target.style.color = 'red';
				} else {
					target.dataset.haveResponse = true;
					let caption = `${results[0].releases.length} ${results[0].mbDiscID ? 'exact' : ' fuzzy'} match`;
					if (results[0].releases.length > 1) caption += 'es';
					target.textContent = caption;
					target.style.color = 'green';
					if (results[0].mbDiscID) {
						GM_openInTab(baseUrl + results[0].mbDiscID, true);
						//GM_openInTab(baseUrl + 'attach?toc=' + results[0].mbTOC.join(' '), true);
					}
					// Fuzzy matching
					if (!results[0].mbDiscID) for (let result of results[0].releases)
						GM_openInTab('https://musicbrainz.org/release/' + result.id, true);
					target.dataset.results = JSON.stringify(results[0]);
				}
			}).catch(function(reason) {
				target.textContent = reason;
				target.style.color = 'red';
			}).then(() => { target.disabled = false });
		}
		return false;
	}, 'Lookup release on MusicBrainz by DiscID/TOC\n(Ctrl + click enforces strict TOC matching)');
	// addLookup('FreeDB lookup', function(evt) {
	// 	const target = evt.currentTarget;
	// 	console.assert(target instanceof HTMLElement);
	// 	if (!target.disabled) if (target.classList.contains('have-response')) {
	// 		if (!('results' in target.dataset)) return false;
	// 		const results = JSON.parse(target.dataset.results);
	// 		//for (let result of results.releases) GM_openInTab('https://', false);
	// 	} else {
	// 		function calculate_cddb_id(tocEntries) {
	// 			function sum_of_digits(n) {
	// 				let sum = 0;
	// 				while (n > 0) {
	// 					sum += n % 10;
	// 					n = Math.floor(n / 10);
	// 				}
	// 				return sum;
	// 			}
	// 			function decimalToHexString(number) {
	// 				if (number < 0) number = 0xFFFFFFFF + number + 1;
	// 				return number.toString(16).padStart(8, '0');
	// 			}
	// 			let checksum = 0
	// 			for (let entry of tocEntries) checksum += sum_of_digits(Math.floor((parseInt(entry.startSector) + preGap) / msf));
	// 			checksum = checksum % 255;
	// 			const length_seconds = Math.floor((tocEntries[tocEntries.length - 1].endSector + 1 - tocEntries[0].startSector) / msf);
	// 			return decimalToHexString((checksum << 24) | (length_seconds << 8) | tocEntries.length);
	// 		}

	// 		target.disabled = true;
	// 		target.textContent = 'Looking up...';
	// 		lookupByToc(parseInt(target.dataset.torrentId), function(tocEntries) {
	// 			const isHTOA = tocEntries[0].startSector > preGap, cddbId = calculate_cddb_id(tocEntries);
	// 			alert('CDDB id: ' + cddbId);
	// 			// search query here
	// 			return Promise.reject('not implamented');
	// 		}).then(function(results) {
	// 			target.classList.add('have-response');
	// 			if (results.length <= 0 || results[0] == null) {
	// 				target.textContent = 'No matches';
	// 				target.style.color = 'red';
	// 			} else {
	// 				target.textContent = results[0].releases.length + ' match(es)';
	// 				target.style.color = 'green';
	// 				target.dataset.results = JSON.stringify(results[0]);
	// 			}
	// 		}).catch(function(reason) {
	// 			target.textContent = reason;
	// 			target.style.color = 'red';
	// 		}).then(() => { target.disabled = false });
	// 	}
	// 	return false;
	// }, 'Lookup release on freeDb');
}

}