Previewer Media on Chats 1.2.36

Preview media links including shortened URLs with optimization

// ==UserScript==
// @name         Previewer Media on Chats  1.2.36
// @namespace    http://tampermonkey.net/
// @version      1.2.36
// @description  Preview media links including shortened URLs with optimization 
// @author       Gullampis810, optimized by Grok
// @license      MIT
// @grant        GM_xmlhttpRequest
// @match        https://www.twitch.tv/*
// @match        https://grok.com/*
// @match        https://*.imgur.com/*
// @match        https://7tv.app/*
// @match        https://update.greasyfork.dpdns.org/scripts/530574/Previewer%20Media%20on%20Chats%20%201235.user.js
// @icon         https://yt3.googleusercontent.com/ytc/AOPolaS0epA6kuqQqudVFRN0l9aJ2ScCvwK0YqC7ojbU=s900-c-k-c0x00ffffff-no-rj
// ==/UserScript==

(function() {
    'use strict';

    const urlCache = new Map();
    let previewContainer = null;

    // Определяем тип файла по расширению или хосту
    function getFileType(url) {
        const cleanUrl = url.split('?')[0];
        const videoExtensions = ['.mp4', '.webm', '.ogg', '.mov', '.avi', '.mkv', '.flv', '.wmv', '.gifv'];
        const imageExtensions = ['.png', '.jpg', '.jpeg', '.svg', '.gif', '.webp', '.avif'];

        const extension = cleanUrl.substring(cleanUrl.lastIndexOf('.')).toLowerCase();

        if (videoExtensions.includes(extension)) return 'video';
        if (imageExtensions.includes(extension)) return 'image';

        // Специфичные хосты
        if (url.includes('gachi.gay')) return 'image';
        if (url.includes('kappa.lol')) return null; // Требует дополнительной проверки
        if (url.includes('imgur.com') || url.includes('i.imgur.com')) {
            if (extension === '.gifv') return 'video';
            return 'image';
        }
        if (url.includes('emote') || url.includes('cdn.7tv.app') || url.includes('7tv.app/emotes')) return 'image';
        return null;
    }

    // Трансформация URL для 7TV
    function transform7TVUrl(url) {
        const emoteIdMatch = url.match(/7tv\.app\/emotes\/([a-zA-Z0-9]+)/);
        if (emoteIdMatch && emoteIdMatch[1]) {
            return `https://cdn.7tv.app/emote/${emoteIdMatch[1]}/4x.webp`;
        }
        return url;
    }

    // Определяем тип файла по Content-Type
    function getFileTypeFromContentType(contentType) {
        if (!contentType) return null;
        if (contentType.includes('video')) return 'video';
        if (contentType.includes('image')) return 'image';
        return null;
    }

    // Разрешение коротких ссылок
    async function resolveShortUrl(url) {
        if (urlCache.has(url)) return urlCache.get(url);

        return new Promise((resolve) => {
            GM_xmlhttpRequest({
                method: 'HEAD',
                url: url,
                headers: { 'User-Agent': 'Mozilla/5.0 (compatible; PreWatcher/1.2.4)' },
                onload: (response) => {
                    const finalUrl = response.finalUrl || url;
                    const contentType = response.responseHeaders.match(/content-type: (.*)/i)?.[1];
                    const result = { resolvedUrl: finalUrl, contentType };
                    urlCache.set(url, result);
                    resolve(result);
                },
                onerror: () => resolve({ resolvedUrl: url, contentType: null })
            });
        });
    }

    // Тестирование, является ли ссылка изображением
    async function testIfImage(url) {
        return new Promise((resolve) => {
            const img = new Image();
            img.onload = () => resolve(true);
            img.onerror = () => resolve(false);
            img.src = url;
        });
    }

    // Тестирование, является ли ссылка видео
    async function testIfVideo(url) {
        return new Promise((resolve) => {
            const video = document.createElement('video');
            video.onloadedmetadata = () => resolve(true);
            video.onerror = () => resolve(false);
            video.oncanplay = () => resolve(true);
            video.src = url;
            video.load();
        });
    }

    // Извлечение медиа из Reddit
    async function extractMediaFromReddit(url) {
        try {
            const response = await fetch(url);
            const text = await response.text();
            const parser = new DOMParser();
            const doc = parser.parseFromString(text, 'text/html');

            const video = doc.querySelector('video source[src]');
            if (video) return { url: video.getAttribute('src'), type: 'video' };

            const img = doc.querySelector('img[src]');
            if (img) return { url: img.getAttribute('src'), type: 'image' };

            return null;
        } catch (error) {
            console.error('Ошибка при извлечении медиа из Reddit:', error);
            return null;
        }
    }

    // Создание и обновление контейнера предпросмотра
    function updatePreviewElement(url, type) {
        if (!previewContainer) {
            previewContainer = document.createElement('div');
            previewContainer.style.position = 'fixed';
            previewContainer.style.zIndex = '1000';
            previewContainer.style.background = '#0e1a1a';
            previewContainer.style.border = '1px solid #ccc';
            previewContainer.style.padding = '5px';
            previewContainer.style.boxShadow = '0 2px 5px rgba(0,0,0,0.2)';
            previewContainer.style.display = 'none';
            previewContainer.style.maxWidth = '400px';
            previewContainer.style.maxHeight = '300px';
            document.body.appendChild(previewContainer);
        }

        previewContainer.innerHTML = '';

        let element;
        if (type === 'video') {
            element = document.createElement('video');
            element.src = url;
            element.controls = true;
            element.muted = true;
        } else {
            element = document.createElement('img');
            element.src = url;
            element.draggable = false;
        }
        element.style.maxWidth = '100%';
        element.style.maxHeight = '100%';
        previewContainer.appendChild(element);
        return previewContainer;
    }

    // Обработка ссылок в чате
    async function processLinks() {
        const chatContainer = document.querySelector('.chat-scrollable-area__message-container');
        if (!chatContainer) return;

        const messages = chatContainer.querySelectorAll('.chat-line__message:not([data-processed])');
        for (let message of messages) {
            const link = message.querySelector('a[href]');
            if (!link || link.dataset.processed) continue;

            let url = link.getAttribute('href');
            let fileType = getFileType(url);
            let mediaUrl = url;

            // Трансформация URL для 7TV
            if (url.includes('7tv.app/emotes')) {
                mediaUrl = transform7TVUrl(url);
                fileType = getFileType(mediaUrl);
            }

            // Разрешение коротких ссылок и определение типа
            if (!fileType || url.includes('gachi.gay') || url.includes('kappa.lol') || url.includes('t.co') || url.includes('bit.ly') || url.includes('imgur.com')) {
                const { resolvedUrl, contentType } = await resolveShortUrl(url);
                mediaUrl = resolvedUrl;
                fileType = getFileType(mediaUrl) || getFileTypeFromContentType(contentType);

                // Если тип не определен, тестируем
                if (!fileType) {
                    const isVideo = await testIfVideo(mediaUrl);
                    if (isVideo) {
                        fileType = 'video';
                    } else {
                        const isImage = await testIfImage(mediaUrl);
                        fileType = isImage ? 'image' : null;
                    }
                }
            }

            // Обработка Reddit
            if (mediaUrl.includes('reddit.com') && !fileType) {
                const media = await extractMediaFromReddit(mediaUrl);
                if (media) {
                    mediaUrl = media.url;
                    fileType = media.type;
                }
            }

            if (!fileType) continue;

            link.dataset.mediaUrl = mediaUrl;
            link.dataset.fileType = fileType;
            link.dataset.processed = 'true';

            // Событие наведения для предпросмотра
            link.addEventListener('mouseenter', (e) => {
                const preview = updatePreviewElement(link.dataset.mediaUrl, link.dataset.fileType);
                preview.style.display = 'block';
                preview.style.left = `${e.pageX - 77}px`;
                preview.style.top = `${e.pageY + 10}px`;
                if (link.dataset.fileType === 'video') preview.querySelector('video')?.play();
            });

            // Событие ухода курсора
            link.addEventListener('mouseleave', () => {
                previewContainer.style.display = 'none';
                if (previewContainer.querySelector('video')) {
                    const video = previewContainer.querySelector('video');
                    video?.pause();
                    video.currentTime = 0;
                }
            });

            message.dataset.processed = 'true';
        }
    }

    // Дебаунс для оптимизации
    function debounce(func, wait) {
        let timeout;
        return function (...args) {
            clearTimeout(timeout);
            timeout = setTimeout(() => func.apply(this, args), wait);
        };
    }

    // Инициализация и наблюдение за изменениями
    const debouncedProcessLinks = debounce(processLinks, 500);
    document.addEventListener('DOMContentLoaded', debouncedProcessLinks);

    const observer = new MutationObserver(debouncedProcessLinks);
    observer.observe(document.body, { childList: true, subtree: true });

    window.previewLinks = debouncedProcessLinks;

    // Добавление стилей
    const style = document.createElement('style');
    style.textContent = `a[href] { position: relative; }`;
    document.head.appendChild(style);
})();
长期地址
遇到问题?请前往 GitHub 提 Issues,或加Q群1031348184

赞助商

Fishcpy

广告

Rainyun

注册一下就行

Rainyun

一年攒够 12 元