您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Preview media links including shortened URLs kappa.lol
// ==UserScript== // @name Previewer Media on Chats 1.2.39 // @namespace http://tampermonkey.net/ // @version 1.2.39 // @description Preview media links including shortened URLs kappa.lol // @author Gullampis810 // @license MIT // @grant GM_xmlhttpRequest // @match https://www.twitch.tv/* // @match https://*.imgur.com/* // @match https://7tv.app/* // @icon https://yt3.googleusercontent.com/ytc/AOPolaS0epA6kuqQqudVFRN0l9aJ2ScCvwK0YqC7ojbU=s900-c-k-c0x00ffffff-no-rj // @downloadURL // @updateURL // ==/UserScript== (function() { 'use strict'; const urlCache = new Map(); // Поддерживаемые типы файлов const fileTypes = { image: ['.png', '.jpg', '.jpeg', '.gif', '.webp', '.avif'] }; // Поддерживаемые платформы эмодзи и изображений const emotePlatforms = { '7tv.app': (url) => { const emoteIdMatch = url.match(/7tv\.app\/emotes\/([a-zA-Z0-9]+)/); return emoteIdMatch ? `https://cdn.7tv.app/emote/${emoteIdMatch[1]}/2x.webp` : url; }, 'frankerfacez.com': (url) => { const emoteIdMatch = url.match(/frankerfacez\.com\/emoticon\/(\d+)/); return emoteIdMatch ? `https://cdn.frankerfacez.com/emoticon/${emoteIdMatch[1]}/2` : url; }, 'betterttv.com': (url) => { const emoteIdMatch = url.match(/betterttv\.com\/emotes\/([a-zA-Z0-9]+)/); return emoteIdMatch ? `https://cdn.betterttv.net/emote/${emoteIdMatch[1]}/2x` : url; }, 'imgur.com': (url) => { const albumMatch = url.match(/imgur\.com\/a\/([a-zA-Z0-9]+)/); const imageMatch = url.match(/imgur\.com\/([a-zA-Z0-9]+)$/); if (albumMatch) return `https://i.imgur.com/${albumMatch[1]}.jpg`; if (imageMatch) return `https://i.imgur.com/${imageMatch[1]}.jpg`; return url; } }; // Определение типа файла function getFileType(url) { const cleanUrl = url.split('?')[0].toLowerCase(); const extension = cleanUrl.substring(cleanUrl.lastIndexOf('.')); console.debug(`Checking file type for URL: ${url}`); if (fileTypes.image.includes(extension)) { console.debug(`Extension matched: ${extension}`); return 'image'; } if (Object.keys(emotePlatforms).some(platform => url.includes(platform))) { console.debug(`Platform matched: ${url}`); return 'image'; } if (url.includes('cdn.7tv.app') || url.includes('7tv.app/emotes') || url.includes('i.imgur.com') || url.includes('yandex.net') || url.includes('susanin.news')) { console.debug(`Domain matched: ${url}`); return 'image'; } return null; } // Определение типа по Content-Type function getFileTypeFromContentType(contentType) { if (!contentType) { console.debug('No Content-Type received'); return null; } if (contentType.includes('image')) { console.debug(`Content-Type is image: ${contentType}`); return 'image'; } return null; } // Трансформация URL для эмодзи и Imgur function transformEmoteUrl(url) { for (const [platform, transformer] of Object.entries(emotePlatforms)) { if (url.includes(platform)) { const transformed = transformer(url); console.debug(`Transformed URL: ${url} -> ${transformed}`); return transformed; } } return url; } // Разрешение коротких URL и Imgur альбомов async function resolveShortUrl(url) { if (urlCache.has(url)) { console.debug(`Using cached URL: ${url}`); return urlCache.get(url); } try { const response = await fetch(url, { method: 'HEAD', headers: { 'User-Agent': 'Mozilla/5.0 (compatible; PreWatcher/1.2.4)' } }); const finalUrl = response.url || url; const contentType = response.headers.get('content-type'); const result = { resolvedUrl: finalUrl, contentType }; urlCache.set(url, result); console.debug(`Resolved URL: ${url} -> ${finalUrl}, Content-Type: ${contentType}`); return result; } catch (error) { console.error(`Error resolving URL ${url}:`, error); return { resolvedUrl: url, contentType: null }; } } // Извлечение прямой ссылки на изображение из Imgur async function extractImgurImage(url) { if (!url.includes('imgur.com/a/')) { console.debug(`Not an Imgur album: ${url}`); return url; } try { const response = await fetch(url); const text = await response.text(); const doc = new DOMParser().parseFromString(text, 'text/html'); const img = doc.querySelector('img[src*="i.imgur.com"]'); const directUrl = img ? img.getAttribute('src') : url; console.debug(`Extracted Imgur image: ${url} -> ${directUrl}`); return directUrl; } catch (error) { console.error(`Imgur extraction error for ${url}:`, error); return url; } } // Проверка доступности изображения и получение размеров async function testImage(url) { if (urlCache.has(url)) { console.debug(`Using cached image test for ${url}`); return urlCache.get(url); } return new Promise((resolve) => { const img = new Image(); let timedOut = false; const timeout = setTimeout(() => { timedOut = true; urlCache.set(url, { valid: false }); console.debug(`Image test timeout for ${url}`); resolve({ valid: false }); }, 2000); // Таймаут 2 секунды img.onload = () => { if (!timedOut) { clearTimeout(timeout); urlCache.set(url, { valid: true, width: img.naturalWidth, height: img.naturalHeight }); console.debug(`Image loaded: ${url}, size: ${img.naturalWidth}x${img.naturalHeight}`); resolve({ valid: true, width: img.naturalWidth, height: img.naturalHeight }); } }; img.onerror = () => { if (!timedOut) { clearTimeout(timeout); urlCache.set(url, { valid: false }); console.debug(`Image failed to load: ${url}`); resolve({ valid: false }); } }; img.src = url; }); } // Замена ссылки на изображение с сохранением URL и адаптивным размером // Замена ссылки на изображение с сохранением URL и адаптивным размером async function replaceLinkWithImage(link) { let url = link.getAttribute('href'); let fileType = getFileType(url); let mediaUrl = transformEmoteUrl(url); console.debug(`Processing link: ${url}`); // Проверяем короткие ссылки и Imgur альбомы if (!fileType || url.match(/t\.co|bit\.ly|imgur\.com/)) { const { resolvedUrl, contentType } = await resolveShortUrl(url); mediaUrl = resolvedUrl; fileType = getFileType(mediaUrl) || getFileTypeFromContentType(contentType); console.debug(`After resolve: ${mediaUrl}, fileType: ${fileType}`); } // Для Imgur альбомов извлекаем прямую ссылку if (mediaUrl.includes('imgur.com/a/')) { mediaUrl = await extractImgurImage(mediaUrl); fileType = getFileType(mediaUrl); console.debug(`After Imgur extraction: ${mediaUrl}`); } if (!fileType) { const imageInfo = await testImage(mediaUrl); fileType = imageInfo.valid ? 'image' : null; console.debug(`After testImage: fileType=${fileType}`); } if (!fileType) { console.debug(`Skipping non-image URL: ${url}`); return; } const imageInfo = await testImage(mediaUrl); if (!imageInfo.valid) { console.debug(`Image not valid: ${mediaUrl}`); return; } // Определяем размер изображения const maxSize = 512; let width = imageInfo.width; let height = imageInfo.height; if (width > maxSize || height > maxSize) { const ratio = Math.min(maxSize / width, maxSize / height); width = Math.round(width * ratio); height = Math.round(height * ratio); } // Создаем изображение const img = document.createElement('img'); Object.assign(img, { src: mediaUrl, alt: link.textContent, draggable: false }); Object.assign(img.style, { width: `520px`, height: `300px`, verticalAlign: 'middle', margin: '0 4px', objectFit: 'contain', pointerEvents: 'none' }); // Заменяем оригинальную ссылку только на изображение link.replaceWith(img); link.dataset.processed = 'true'; console.debug(`Replaced link with image (no link): ${url} -> ${mediaUrl}`); } // Обработка ссылок async function processLinks() { const chatContainer = document.querySelector('.chat-scrollable-area__message-container'); if (!chatContainer) { console.debug('Chat container not found'); return; } const messages = chatContainer.querySelectorAll('.chat-line__message:not([data-processed])'); for (const message of messages) { // Учитываем ссылки с классом ffz-tooltip link-fragment const links = message.querySelectorAll('a[href]:not([data-processed]), a.ffz-tooltip.link-fragment:not([data-processed])'); for (const link of links) { await replaceLinkWithImage(link); } message.dataset.processed = 'true'; } } // Дебаунс функция const debounce = (func, wait) => { let timeout; return (...args) => { clearTimeout(timeout); timeout = setTimeout(() => func.apply(this, args), wait); }; }; const debouncedProcessLinks = debounce(processLinks, 100); // 100 мс для быстрой обработки // Инициализация document.addEventListener('DOMContentLoaded', debouncedProcessLinks); new MutationObserver(debouncedProcessLinks).observe(document.body, { childList: true, subtree: true }); window.previewLinks = debouncedProcessLinks; // Добавление стилей const style = document.createElement('style'); style.textContent = ` .chat-line__message img { display: inline-block; vertical-align: middle; } .chat-line__message a { display: inline-block; vertical-align: middle; } `; document.head.appendChild(style); })();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址