跳转到内容

User:Aizag/interlangRefCount.js

维基百科,自由的百科全书
注意:保存之后,你必须清除浏览器缓存才能看到做出的更改。Google Chrome、Firefox、Microsoft Edge及Safari:按住⇧ Shift键并单击工具栏的“刷新”按钮。参阅Help:绕过浏览器缓存以获取更多帮助。
// 未經許可,請勿更改
// 在跨語言連結後面顯示該語言版本條目的引用總數
// 計數方式:該條目渲染 HTML 中 <ol class="references"> 底下的 <li> 數量
// 節流:並發 3、每分鐘最多 150 次請求、429 時依 Retry-After 等候重試;快取 24 小時
( function () {
	'use strict';

	var CACHE_PREFIX = 'ilRefCount:';
	var CACHE_TTL = 24 * 60 * 60 * 1000;     // 快取 24 小時
	var CONCURRENCY = 3;                      // Wikimedia 官方建議 <= 3
	var PER_MINUTE = 150;                     // 匿名上限 200/分,留安全餘量
	var MAX_RETRY = 2;

	if ( mw.config.get( 'wgNamespaceNumber' ) !== 0 ) {
		return;
	}

	function readCache( key ) {
		try {
			var raw = localStorage.getItem( CACHE_PREFIX + key );
			if ( !raw ) {
				return null;
			}
			var obj = JSON.parse( raw );
			if ( Date.now() - obj.t > CACHE_TTL ) {
				return null;
			}
			return obj.n;
		} catch ( e ) {
			return null;
		}
	}

	function writeCache( key, n ) {
		try {
			localStorage.setItem(
				CACHE_PREFIX + key,
				JSON.stringify( { t: Date.now(), n: n } )
			);
		} catch ( e ) {
			// 忽略容量不足
		}
	}

	function sleep( ms ) {
		return new Promise( function ( resolve ) {
			setTimeout( resolve, ms );
		} );
	}

	// 每分鐘請求數閘門
	var stamps = [];
	function acquireSlot() {
		var now = Date.now();
		stamps = stamps.filter( function ( t ) {
			return now - t < 60000;
		} );
		if ( stamps.length < PER_MINUTE ) {
			stamps.push( now );
			return Promise.resolve();
		}
		var wait = 60000 - ( now - stamps[ 0 ] ) + 50;
		return sleep( wait ).then( acquireSlot );
	}

	function countRefs( html ) {
		var doc = new DOMParser().parseFromString( html, 'text/html' );
		var n = 0;
		doc.querySelectorAll( 'ol.references' ).forEach( function ( ol ) {
			n += ol.querySelectorAll( ':scope > li' ).length;
		} );
		return n;
	}

	function buildUrl( host, title ) {
		return 'https://' + host + '/w/api.php?' + $.param( {
			action: 'parse',
			page: title,
			prop: 'text',
			formatversion: 2,
			format: 'json',
			disabletoc: 1,
			disablelimitreport: 1,
			disableeditsection: 1,
			origin: '*'
		} );
	}

	function fetchCount( host, title, attempt ) {
		attempt = attempt || 0;
		var url = buildUrl( host, title );
		return acquireSlot().then( function () {
			return fetch( url );
		} ).then( function ( r ) {
			if ( r.status === 429 ) {
				if ( attempt >= MAX_RETRY ) {
					throw new Error( '429' );
				}
				var ra = parseInt( r.headers.get( 'Retry-After' ), 10 );
				var wait = ( isNaN( ra ) ? 20 : ra + 1 ) * 1000;
				return sleep( wait ).then( function () {
					return fetchCount( host, title, attempt + 1 );
				} );
			}
			if ( !r.ok ) {
				throw new Error( 'HTTP ' + r.status );
			}
			return r.json().then( function ( data ) {
				if ( !data.parse ) {
					throw new Error( 'no parse' );
				}
				return countRefs( data.parse.text );
			} );
		} );
	}

	function addBadge( link, text, title ) {
		var badge = document.createElement( 'span' );
		badge.className = 'il-refcount';
		badge.style.cssText = 'margin-left:0.3em;font-size:85%;color:#54595d;';
		badge.textContent = '(' + text + ')';
		if ( title ) {
			badge.title = title;
		}
		link.appendChild( badge );
		return badge;
	}

	function run() {
		var links = Array.prototype.slice.call(
			document.querySelectorAll( 'a.interlanguage-link-target' )
		);
		if ( !links.length ) {
			return;
		}

		var queue = links.map( function ( link ) {
			var u;
			try {
				u = new URL( link.href );
			} catch ( e ) {
				return null;
			}
			var title = decodeURIComponent( u.pathname.replace( /^\/wiki\//, '' ) );
			return {
				link: link,
				host: u.hostname,
				title: title,
				key: u.hostname + '|' + title
			};
		} ).filter( Boolean );

		queue = queue.filter( function ( item ) {
			var cached = readCache( item.key );
			if ( cached !== null ) {
				addBadge( item.link, cached, '引用數(快取)' );
				return false;
			}
			item.badge = addBadge( item.link, '…', '載入中' );
			return true;
		} );

		var active = 0;
		function next() {
			while ( active < CONCURRENCY && queue.length ) {
				var item = queue.shift();
				active++;
				( function ( it ) {
					fetchCount( it.host, it.title ).then( function ( n ) {
						writeCache( it.key, n );
						it.badge.textContent = '(' + n + ')';
						it.badge.title = '引用數';
					} ).catch( function ( e ) {
						it.badge.textContent = '(?)';
						it.badge.title = '無法取得:' + e.message;
					} ).then( function () {
						active--;
						next();
					} );
				}( item ) );
			}
		}
		next();
	}

	$( run );
}() );