// ==UserScript== // @name Twitch Emotes Cache // @namespace http://tampermonkey.net/ // @version 1.0 // @description Cache frequently used Twitch emotes to reduce load delay // @author You // @match https://*.twitch.tv/* // @icon https://yt3.googleusercontent.com/ytc/AIdro_nAFS_oYf_Gt3hs5y97Zri6PDs1-oDFyOcfCkjyHlgNEfQ=s900-c-k-c0x00ffffff-no-rj // @grant none // @license MIT // @downloadURL // @downloadURL none // ==/UserScript== (function() { 'use strict'; // Инициализация хранилища let emoteCache = JSON.parse(localStorage.getItem('twitchEmoteCache')) || {}; const MAX_CACHE_SIZE = 200; // Ограничение размера кэша const CACHE_EXPIRY = 60 * 60 * 1000; // 1 час (3600000 мс) let pendingSave = null; // Для дебонсинга let currentChannel = getCurrentChannel(); // Текущий канал // Получение текущего канала из URL function getCurrentChannel() { const path = window.location.pathname; const match = path.match(/^\/([a-zA-Z0-9_]+)/); return match ? match[1].toLowerCase() : 'global'; } // Очистка кэша при смене канала function clearChannelCache() { if (emoteCache[currentChannel]) { delete emoteCache[currentChannel]; saveCache(); } } // Сохранение кэша с дебонсингом function saveCache() { if (pendingSave) return; pendingSave = setTimeout(() => { requestIdleCallback(() => { localStorage.setItem('twitchEmoteCache', JSON.stringify(emoteCache)); pendingSave = null; }, { timeout: 1000 }); }, 1000); } // Кэширование эмодзи function cacheEmote(url, dataUrl, code, countIncrement = 1) { const now = Date.now(); const channel = currentChannel || 'global'; if (!emoteCache[channel]) emoteCache[channel] = {}; emoteCache[channel][url] = { dataUrl: dataUrl || emoteCache[channel][url]?.dataUrl, code: code || emoteCache[channel][url]?.code, timestamp: now, count: (emoteCache[channel][url]?.count || 0) + countIncrement }; // Ограничение размера кэша для канала const emoteKeys = Object.keys(emoteCache[channel]); if (emoteKeys.length > MAX_CACHE_SIZE) { const leastUsedKey = emoteKeys.reduce((a, b) => emoteCache[channel][a].count < emoteCache[channel][b].count ? a : b ); delete emoteCache[channel][leastUsedKey]; } saveCache(); } // Очистка устаревшего кэша function cleanOldCache() { const now = Date.now(); for (const channel in emoteCache) { for (const url in emoteCache[channel]) { if (now - emoteCache[channel][url].timestamp > CACHE_EXPIRY) { delete emoteCache[channel][url]; } } if (!Object.keys(emoteCache[channel]).length) { delete emoteCache[channel]; } } saveCache(); } // Загрузка эмодзи async function fetchAndCacheEmote(imgElement, url, code, countIncrement = 1) { try { const response = await fetch(url, { mode: 'cors' }); if (!response.ok) throw new Error('Network response was not ok'); const blob = await response.blob(); const reader = new FileReader(); reader.onloadend = () => { const dataUrl = reader.result; cacheEmote(url, dataUrl, code, countIncrement); if (imgElement) imgElement.src = dataUrl; }; reader.readAsDataURL(blob); } catch (error) { console.error('Ошибка при кэшировании эмодзи:', url, error); if (imgElement && code) { imgElement.src = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAB9SURBVEhLY2AYBaNgFIyCUTAKRsEoGAWjYBSMglEwCkbBKBgFo2AUjIJRMAoGIA0F0A4gDQXQDgANBdAOIA0F0A4gDQXQDgANBdAOIA0F0A4gDQXQDgANBdAOIA0F0A4gDQXQDgANBdAOIA0F0A4gDQXQDgANBdAOIA0F0A4gDQAANHYvmHjk1oAAAAAASUVORK5CYII='; // Заглушка imgElement.title = `Failed to load: ${code}`; } } } // Предзагрузка популярных эмодзи function preloadPopularEmotes() { requestIdleCallback(() => { const channel = currentChannel || 'global'; if (!emoteCache[channel]) return; const popularEmotes = Object.keys(emoteCache[channel]) .sort((a, b) => emoteCache[channel][b].count - emoteCache[channel][a].count) .slice(0, 20); // Топ-20 эмодзи popularEmotes.forEach((url) => { if (!emoteCache[channel][url].dataUrl) { fetchAndCacheEmote(null, url, emoteCache[channel][url].code, 0); } }); }, { timeout: 5000 }); } // Замена текстовых кодов на кэшированные эмодзи function replaceTextCodes() { const messages = document.querySelectorAll('.chat-line__message'); const channel = currentChannel || 'global'; if (!emoteCache[channel]) return; messages.forEach((message) => { const textNodes = getTextNodes(message); textNodes.forEach((node) => { let text = node.textContent; for (const url in emoteCache[channel]) { const emote = emoteCache[channel][url]; if (emote.code && text.includes(emote.code)) { const img = document.createElement('img'); img.className = 'chat-line__message--emote'; img.src = emote.dataUrl || 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAB9SURBVEhLY2AYBaNgFIyCUTAKRsEoGAWjYBSMglEwCkbBKBgFo2AUjIJRMAoGIA0F0A4gDQXQDgANBdAOIA0F0A4gDQXQDgANBdAOIA0F0A4gDQXQDgANBdAOIA0F0A4gDQXQDgANBdAOIA0F0A4gDQXQDgANBdAOIA0F0A4gDQAANHYvmHjk1oAAAAAASUVORK5CYII='; img.alt = emote.code; img.title = emote.code; const parts = text.split(emote.code); text = parts.join(''); node.parentNode.insertBefore(img, node); cacheEmote(url, emote.dataUrl, emote.code, 1); } } if (text !== node.textContent) { node.textContent = text; } }); }); } // Получение текстовых узлов function getTextNodes(node) { const textNodes = []; const walker = document.createTreeWalker(node, NodeFilter.SHOW_TEXT, null, false); let currentNode; while ((currentNode = walker.nextNode())) { textNodes.push(currentNode); } return textNodes; } // Обработка эмодзи в чате function processEmotes() { const messages = document.querySelectorAll('.chat-line__message'); const channel = currentChannel || 'global'; if (!emoteCache[channel]) emoteCache[channel] = {}; messages.forEach((message) => { const emotes = message.querySelectorAll(` img, .emote, .bttv-emote, .seventv-emote, .ffz-emote, .twitch-emote `); emotes.forEach((emote) => { const url = emote.src; if (!url) return; const code = emote.alt || emote.title || emote.getAttribute('data-tooltip-type') || ''; if (emoteCache[channel][url]?.dataUrl) { emote.src = emoteCache[channel][url].dataUrl; cacheEmote(url, null, code, 1); } else { fetchAndCacheEmote(emote, url, code, 1); } }); }); // Замена текстовых кодов replaceTextCodes(); } // Отслеживание смены канала function monitorChannelSwitch() { let lastChannel = currentChannel; // Проверка изменения URL window.addEventListener('popstate', () => { const newChannel = getCurrentChannel(); if (newChannel !== lastChannel) { clearChannelCache(); currentChannel = newChannel; lastChannel = newChannel; preloadPopularEmotes(); processEmotes(); } }); // Отслеживание изменений в DOM для SPA-навигации const observer = new MutationObserver(() => { const newChannel = getCurrentChannel(); if (newChannel !== lastChannel) { clearChannelCache(); currentChannel = newChannel; lastChannel = newChannel; preloadPopularEmotes(); processEmotes(); } }); observer.observe(document.body, { childList: true, subtree: true }); } // Наблюдатель за чатом const observer = new MutationObserver((mutations) => { let hasNewMessages = false; mutations.forEach((mutation) => { if (mutation.addedNodes.length && mutation.target.classList?.contains('chat-line__message')) { hasNewMessages = true; } }); if (hasNewMessages) { requestIdleCallback(() => processEmotes(), { timeout: 500 }); } }); // Инициализация cleanOldCache(); preloadPopularEmotes(); monitorChannelSwitch(); // Настройка наблюдателя за чатом const chatContainer = document.querySelector('.chat-scrollable-area__message-container') || document.body; observer.observe(chatContainer, { childList: true, subtree: true }); // Периодическая очистка кэша (каждые 30 минут) setInterval(cleanOldCache, 30 * 60 * 1000); // Первичная обработка чата requestIdleCallback(() => processEmotes(), { timeout: 1000 }); })();