Greasy Fork

Greasy Fork is available in English.

[GMT] CD DiscID Lookup

Lookup CD by TOC

当前为 2023-01-29 提交的版本,查看 最新版本

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         [GMT] CD DiscID Lookup
// @namespace    http://greasyfork.icu/users/321857-anakunda
// @version      1.02
// @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
// @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) {
					const releases = Array.isArray(result.releases) ? result.releases
						: 'id' in result && 'title' 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.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, true);
			}).then(function(results) {
				target.dataset.haveResponse = true;
				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';
					if (results[0].mbDiscID) {
						GM_openInTab(baseUrl + results[0].mbDiscID, true);
						//GM_openInTab(baseUrl + 'attach?toc=' + results[0].mbTOC.join(' '), true);
					}
					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');
	// 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');
}

}