Sort channels by viewcount

Reorders the followed channels list in the sidebar based on viewcount.

目前為 2021-01-15 提交的版本,檢視 最新版本

// ==UserScript==
// @name         Sort channels by viewcount
// @namespace    https://github.com/tomasz13nocon
// @version      1.1
// @description  Reorders the followed channels list in the sidebar based on viewcount.
// @author       Tomasz Nocoń
// @match        https://www.twitch.tv/*
// @grant        none
// @homepageURL  https://github.com/tomasz13nocon/twitch-sort-by-viewcount
// @license      MIT
// ==/UserScript==

(async function() {
	'use strict';

	// Returns a float parsed from a localized viewcount string for a given language.
	// The values for text representing million are untested or flat out not present,
	// since it's impossible to test them without a million viewer Andy. And those are rare.
	// TODO: Access internal react data to get viewcounts as numbers if it's even possible,
	// and avoid this ugly parsing.
	function parseViewcount(str, lang) {
		// IIFE spreading keys containing commas to seperate keys in the following object
		const langs = (obj => {
			let keys = Object.keys(obj);
			for (let i = 0; i < keys.length; ++i) {
				let key = keys[i],
						subkeys = key.split(", "),
						target = obj[key];
				delete obj[key];
				subkeys.forEach(key => { obj[key] = target; });
			}
			return obj;
		})({ // mappings for determining multiplier text based on language
			"en-US, en-GB, es-MX, fr-FR, nl-NL, no-NO, ro-RO, th-TH": {
				k: "k",
				m: "m",
			},
			"pl-PL": {
				k: "tys",
				m: "mil",
			},
			"da-DK": {
				k: "t",
				m: "m",
			},
			"es-ES, pt-PT, pt-BR": {
				k: "mil",
			},
			"hu-HU": {
				k: "e",
				m: "m",
			},
			"sk-SK, cs-CZ": {
				k: "tis",
				m: "mil",
			},
			"fi-FI": {
				k: "t",
				m: "m",
			},
			"sv-SE": {
				k: "tn",
				m: "mn",
			},
			"vi-VN": {
				k: "n",
				m: "t",
			},
			"tr-TR": {
				k: "b",
				m: "m",
			},
			"el-GR": {
				k: "χιλ",
				m: "εκα",
			},
			"bg-BG": {
				k: "хил",
				m: "млн",
			},
			"ru-RU": {
				k: "тыс",
				m: "млн",
			},
			// These ones are actually 10k modifiers:
			"zh-CN, ja-JP": {
				k: "万",
			},
			"zh-TW": {
				k: "萬",
			},
			"ko-KR": {
				k: "만",
			},
		});

		// Some languages use commas as decimals
		str = str.replace(",", ".");
		// These languages use dot (or comma) as seperator (123.456 == 123456)
		if (["de-DE", "it-IT", "zh-CN", "zh-TW", "ja-JP", "ko-KR"].includes(lang)) {
			str = str.replace(".", "");
		}

		let viewcount = parseFloat(str);
		if (langs[lang]) {
			if (str.toLowerCase().includes(langs[lang].k)) {
				if (["zh-CN", "zh-TW", "ja-JP", "ko-KR"].includes(lang))
					viewcount *= 10000;
				else
					viewcount *= 1000;
			}
			else if (str.toLowerCase().includes(langs[lang].m))
				viewcount *= 1000000;
		}
		return viewcount;
	}

	// Returns viewcount of a given side-nav-card element
	function viewcount(element) {
		let viewcountString = element.getElementsByClassName("side-nav-card__live-status")[0].textContent;
		return parseViewcount(
			viewcountString,
			document.getElementsByTagName("html")[0].getAttribute("lang")
		);
	}


	let sidebar;
	// Delay querying the element until it loads
	while ((sidebar = document.getElementsByClassName("side-bar-contents")[0]) === undefined) {
		console.log("side-bar-contents not found. retrying in 0.5 seconds");
		await new Promise(r => setTimeout(r, 500));
	}
	// We're mutating DOM inside a DOM mutation event callback. This flag prevents infinite loop.
	let weMutatedDom = false;

	new MutationObserver((mutations) => {

		if (weMutatedDom) {
			weMutatedDom = false;
			return;
		}

		// We're only interested in "new nodes added" and "text changed" mutations.
		let relevantMutation = false;
		for (let mutation of mutations) {
			if (mutation.addedNodes[0] || mutation.type === "characterData") {
				relevantMutation = true;
				break;
			}
		}
		if (!relevantMutation)
			return;

		// If the compact version of the followed channels list is being displayed, then there is no way to sort.
		if (document.getElementsByClassName("side-nav--expanded").length === 0)
			return;

		let sections = sidebar.getElementsByClassName("side-nav-section");
		let followedSection = sections[0];

		// Mapping to 2 parents up, as that's the outermost element for a single channel
		let streams = [...followedSection.getElementsByClassName("side-nav-card")].map(el => el.parentNode.parentNode);

		streams.sort((a, b) => {
			let av = viewcount(a), bv = viewcount(b);
			if (av > bv)
				return -1;
			if (av < bv)
				return 1;
			return 0;
		});

		weMutatedDom = true;
		streams[0].parentNode.append(...streams);

	}).observe(sidebar, { childList: true, subtree: true, characterData: true });

})();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址