// ==UserScript== // @name Twitch Pinned Streamers - twitch.tv // @description Pin Twitch streamers on sidebar without being logged in. // @namespace https://github.com/vekvoid/UserScripts // @homepageURL https://github.com/vekvoid/UserScripts/ // @supportURL https://github.com/vekvoid/UserScripts/issues // @match *://*.twitch.tv/* // @grant none // @version 1.0.0 // @downloadURL none // ==/UserScript== const logLevels = { trace: 10, debug: 20, info: 30, warn: 40, error: 50, fatal: 60, }; const NAME = 'Twitch Pinned Streamers'; const CURRENT_LOG_LEVEL = logLevels.debug; const DETECT_PAGE_CHANGE_INTERVAL = 1000; const ALL_RELEVANT_CONTENT_SELECTOR = '.hVqkZv'; const TWITCH_GRAPHQL = 'https://gql.twitch.tv/gql'; const CLIENT_ID = 'kimne78kx3ncx6brgo4mv6wki5h1ko'; // From Alternate Player for Twitch.tv const logger = { /* eslint-disable no-console */ trace: (...args) => (logLevels.trace >= CURRENT_LOG_LEVEL) && console.trace(`${NAME}:`, ...args), debug: (...args) => (logLevels.debug >= CURRENT_LOG_LEVEL) && console.log(`${NAME}:`, ...args), info: (...args) => (logLevels.info >= CURRENT_LOG_LEVEL) && console.info(`${NAME}:`, ...args), warn: (...args) => (logLevels.warn >= CURRENT_LOG_LEVEL) && console.warn(`${NAME}:`, ...args), error: (...args) => (logLevels.error >= CURRENT_LOG_LEVEL) && console.error(`${NAME}:`, ...args), fatal: (...args) => (logLevels.fatal >= CURRENT_LOG_LEVEL) && console.fatal(`${NAME}:`, ...args), /* eslint-enable no-console */ }; const styles = ` `; document.head.innerHTML += styles; let currentPage = window.top.location.href; let previousPage = ''; let isWorking = false; (() => { logger.info('Started'); setInterval(() => { [currentPage] = window.top.location.href.split('#'); if (currentPage === previousPage) { return; } previousPage = currentPage; main(); }, DETECT_PAGE_CHANGE_INTERVAL); })(); let waitForMainContainer; const main = () => { let relevantContent; if (waitForMainContainer) { clearInterval(waitForMainContainer); } waitForMainContainer = setInterval(() => { relevantContent = document.querySelector(ALL_RELEVANT_CONTENT_SELECTOR); if (!relevantContent) { return; } if (relevantContent.childElementCount < 2) { return; } clearInterval(waitForMainContainer); logger.debug('Main content found.'); const observer = new MutationObserver(async () => { if (isWorking) { return; } isWorking = true; if (document.getElementById('anon-followed')) { return; } const sidebar = relevantContent.querySelector('.side-nav.side-nav--expanded .Layout-sc-nxg1ff-0.SVxtW'); logger.debug(sidebar); if (!sidebar) { return; } const sidebarContent = sidebar.querySelector( '.InjectLayout-sc-588ddc-0 .simplebar-content .side-bar-contents nav div > div > div', ); const anonFollowedElement = document.createElement('div'); anonFollowedElement.id = 'anon-followed'; anonFollowedElement.innerHTML += pinnedHeader(); anonFollowedElement.innerHTML += '
'; sidebarContent.insertBefore(anonFollowedElement, sidebarContent.childNodes[0]); await renderPinnedStreamers(); document.getElementById('tps-add-streamer').onclick = addStreamer; const mainSection = relevantContent.querySelector('main'); logger.debug(sidebar, mainSection); isWorking = false; observer.disconnect(); }); observer.observe(document.body, { childList: true, subtree: true }); }, 500); }; const addStreamer = async () => { // eslint-disable-next-line no-alert const streamerUser = prompt('Streamer username:'); if (!streamerUser) { return; } const pinned = localStorageGetPinned(); const found = pinned.find((user) => user.user.toLowerCase() === streamerUser.toLowerCase()); if (found) { logger.info(`Streamer '${streamerUser}' already pinned.`); return; } const user = await getTwitchUser(streamerUser); logger.debug(user); if (!user.id) { const message = `Streamer '${streamerUser}' not found.`; logger.warn(message); // eslint-disable-next-line no-alert alert(message); return; } pinned.push(user); localStorageSetPinned(pinned); logger.debug(localStorage['tps:pinned']); const prevHeight = document.querySelector('.tps-pinned-container').getBoundingClientRect().height; const nextHeight = prevHeight + document.querySelector('.tps-pinned-container > div').getBoundingClientRect().height; document.querySelector('.tps-pinned-container').style.height = `${prevHeight}px`; await renderPinnedStreamers(); document.querySelector('.tps-pinned-container').style.height = `${nextHeight}px`; setTimeout(() => { document.querySelector('.tps-pinned-container').style.height = ''; }, 500); }; const removeStreamer = async (id) => { const filtered = localStorageGetPinned().filter((p) => p.id !== id && p.id); localStorageSetPinned(filtered); const prevHeight = document.querySelector('.tps-pinned-container').getBoundingClientRect().height; const nextHeight = prevHeight - document.querySelector('.tps-pinned-container > div').getBoundingClientRect().height; document.querySelector('.tps-pinned-container').style.height = `${prevHeight}px`; await renderPinnedStreamers(); document.querySelector('.tps-pinned-container').style.height = `${nextHeight}px`; setTimeout(() => { document.querySelector('.tps-pinned-container').style.height = ''; }, 500); }; const renderPinnedStreamers = async () => { const promises = localStorageGetPinned().map(async (streamer) => { const streamerInfo = await getTwitchStreamInfo(streamer.id); return { ...streamer, ...streamerInfo, }; }); const pinnedStreamers = await Promise.all(promises); document.getElementById('anon-followed').querySelector('div:nth-child(2)').innerHTML = ''; pinnedStreamers .sort((a, b) => ((a.viewers < b.viewers) ? 1 : -1)) .sort((a, b) => { if (a.isLive === b.isLive) return 0; return a.isLive ? -1 : 1; }) .forEach((data) => { document.getElementById('anon-followed').querySelector('div:nth-child(2)').innerHTML += pinnedStreamer({ ...data, }); }); document.querySelectorAll('.tps-remove-pinned-streamer').forEach((btn) => { btn.addEventListener('click', async (event) => { const id = event.target.getAttribute('data-id'); logger.debug(`Removing pinned streamer with id: ${id}`); await removeStreamer(id); logger.debug(`Removed pinned streamer with id: ${id}`); }); }); }; // HTML templates const pinnedHeader = () => ` `; const addBtn = () => ` `; const pinnedStreamer = ({ user, id, displayName, profileImageURL, isLive, viewers = '', category, }) => { const removeBtn = ``; const prettyViewers = stylizedViewers(viewers); return `