Previewer Media on Chats 1.2.31

Preview media links including shortened URLs

目前為 2025-03-26 提交的版本,檢視 最新版本

// ==UserScript==
// @name         Previewer Media on Chats  1.2.31
// @namespace    http://tampermonkey.net/
// @version      1.2.31
// @description  Preview media links including shortened URLs
// @author       Gullampis810
// @license      MIT
// @grant        GM_xmlhttpRequest
// @match        https://www.twitch.tv/*
// @match        https://grok.com/*
// @match        https://*.imgur.com/*
// @match        https://7tv.app/*
// @icon         https://yt3.googleusercontent.com/ytc/AOPolaS0epA6kuqQqudVFRN0l9aJ2ScCvwK0YqC7ojbU=s900-c-k-c0x00ffffff-no-rj
// ==/UserScript==

(function() {
    'use strict';

    // Функция для проверки типа файла по расширению или домену
    function getFileType(url) {
        const cleanUrl = url.split('?')[0];
        const videoExtensions = ['.mp4', '.webm', '.ogg', '.mov', '.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';

        // Добавляем поддержку gachi.gay
        if (url.includes('gachi.gay')) {
            return 'image'; // По умолчанию считаем изображением, уточняется через Content-Type
        }

        // Поддержка Imgur
        if (url.includes('imgur.com') || url.includes('i.imgur.com')) {
            if (extension === '.gifv') return 'video';
            if (imageExtensions.includes(extension)) return 'image';
            return 'image'; // По умолчанию Imgur — изображение
        }

        if (url.includes('emote') || url.includes('cdn.7tv.app') || url.includes('7tv.app/emotes')) return 'image';
        return null;
    }

    // Функция для преобразования ссылки 7TV в прямую ссылку на изображение
    function transform7TVUrl(url) {
        if (url.includes('7tv.app/emotes')) {
            const emoteIdMatch = url.match(/7tv\.app\/emotes\/([a-zA-Z0-9]+)/);
            if (emoteIdMatch && emoteIdMatch[1]) {
                const emoteId = emoteIdMatch[1];
                return `https://cdn.7tv.app/emote/${emoteId}/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;
    }

    // Функция для разрешения сокращенных ссылок и получения Content-Type
    async function resolveShortUrl(url) {
        try {
            const response = await fetch(url, {
                method: 'HEAD',
                redirect: 'follow',
                headers: {
                    'User-Agent': 'Mozilla/5.0 (compatible; PreWatcher/1.2.4)'
                }
            });
            const contentType = response.headers.get('Content-Type');
            return { resolvedUrl: response.url, contentType };
        } catch (error) {
            console.error('Ошибка при разрешении ссылки:', error);
            return { resolvedUrl: url, contentType: null };
        }
    }

    // Функция для извлечения медиа из поста 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 links = doc.querySelectorAll('a[href]');
            for (let link of links) {
                const href = link.getAttribute('href');
                const type = getFileType(href);
                if (type) return { url: href, type };
            }

            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('Ошибка при извлечении медиа:', error);
            return null;
        }
    }

    // Функция для создания элемента предпросмотра
    function createPreviewElement(url, type) {
        const container = document.createElement('div');
        container.style.position = 'fixed';
        container.style.zIndex = '1000';
        container.style.background = '#0e1a1a';
        container.style.border = '1px solid #ccc';
        container.style.padding = '5px';
        container.style.boxShadow = '0 2px 5px rgba(0,0,0,0.2)';
        container.style.display = 'none';
        container.style.left = '85%'; // Центрируем по горизонтали
        container.style.top = '50%'; // Центрируем по вертикали
        container.style.transform = 'translate(-50%, -50%)'; // Точное центрирование

        let element;
        if (type === 'video') {
            element = document.createElement('video');
            element.src = url;
            element.controls = true;
            element.muted = true;
            element.style.maxWidth = '400px';
            element.style.maxHeight = '300px';
        } else if (type === 'image') {
            element = document.createElement('img');
            element.src = url;
            element.style.maxWidth = '400px';
            element.style.maxHeight = '440px';
            element.draggable = false;
        } else {
            element = document.createElement('div');
            element.textContent = 'Предпросмотр недоступен';
            element.style.color = '#fff';
            element.style.padding = '10px';
        }

        if (element) {
            container.appendChild(element);
        }
        return container;
    }

    // Основная функция обработки ссылок
    async function processLinks() {
        const messages = document.querySelectorAll(
            '.message, .PostContent-imageWrapper, .PostContent-imageWrapper-rounded, .Gallery-Content--media, .imageContainer'
        );

        for (let message of messages) {
            let targetLink = message.querySelector('a[href]') ||
                            message.querySelector('img[src]:not(.image-placeholder)') ||
                            message.querySelector('img[src].image-placeholder');

            if (!targetLink || targetLink.dataset.processed) continue;

            if (targetLink.tagName === 'IMG' && targetLink.getAttribute('src').includes('cdn.7tv.app')) {
                if (message.querySelector('a[href]')) continue;
            }

            let url = targetLink.tagName === 'IMG' ? targetLink.getAttribute('src') : targetLink.getAttribute('href');
            let fileType = getFileType(url);
            let mediaUrl = url;
            let contentType = null;

            // Преобразуем ссылку 7TV
            if (url.includes('7tv.app/emotes')) {
                mediaUrl = transform7TVUrl(url);
                fileType = getFileType(mediaUrl);
            }

            // Разрешаем ссылки и проверяем Content-Type для gachi.gay и других случаев
            if (!fileType || url.includes('gachi.gay') || url.includes('kappa.lol') || url.includes('t.co') || url.includes('bit.ly')) {
                const { resolvedUrl, contentType: fetchedContentType } = await resolveShortUrl(url);
                mediaUrl = resolvedUrl;
                contentType = fetchedContentType;
                fileType = getFileType(mediaUrl) || getFileTypeFromContentType(contentType);
            }

            // Специальная обработка Imgur
            if (url.includes('imgur.com') && !fileType) {
                const { resolvedUrl, contentType: fetchedContentType } = await resolveShortUrl(url);
                mediaUrl = resolvedUrl;
                contentType = fetchedContentType;
                fileType = getFileType(mediaUrl) || getFileTypeFromContentType(contentType);
            }

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

            if (!fileType) continue;

            const preview = createPreviewElement(mediaUrl, fileType);
            document.body.appendChild(preview);

            message.addEventListener('mouseenter', () => {
                preview.style.display = 'block';
                if (fileType === 'video') preview.querySelector('video')?.play();
            });

            message.addEventListener('mouseleave', () => {
                preview.style.display = 'none';
                if (fileType === 'video') {
                    const video = preview.querySelector('video');
                    video?.pause();
                    video.currentTime = 0;
                }
            });

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

    // Инициализация
    document.addEventListener('DOMContentLoaded', () => processLinks());

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

    window.previewLinks = processLinks;

    const style = document.createElement('style');
    style.textContent = `
        a[href], img[src] {
            position: relative;
        }
    `;
    document.head.appendChild(style);
})();