YouTube Media Comments

Позволяет отправлять фото, гифки, звук и видео в комментарии ютуб

スクリプトをインストールするには、Tampermonkey, GreasemonkeyViolentmonkey のような拡張機能のインストールが必要です。

You will need to install an extension such as Tampermonkey to install this script.

スクリプトをインストールするには、TampermonkeyViolentmonkey のような拡張機能のインストールが必要です。

スクリプトをインストールするには、TampermonkeyUserscripts のような拡張機能のインストールが必要です。

このスクリプトをインストールするには、Tampermonkeyなどの拡張機能をインストールする必要があります。

このスクリプトをインストールするには、ユーザースクリプト管理ツールの拡張機能をインストールする必要があります。

(ユーザースクリプト管理ツールは設定済みなのでインストール!)

このスタイルをインストールするには、Stylusなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus などの拡張機能をインストールする必要があります。

このスタイルをインストールするには、Stylus tなどの拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

このスタイルをインストールするには、ユーザースタイル管理用の拡張機能をインストールする必要があります。

(ユーザースタイル管理ツールは設定済みなのでインストール!)

このスクリプトの質問や評価の投稿はこちら通報はこちらへお寄せください
// ==UserScript==
// @name         YouTube Media Comments
// @namespace    http://tampermonkey.net
// @license MIT
// @version      14.8
// @description  Позволяет отправлять фото, гифки, звук и видео в комментарии ютуб
// @author       XMA | youtube.com/@-XMA- | animego.me/user/894250
// @match        https://www.youtube.com/*
// @match        https://animego.me/*
// @grant        GM_addStyle
// @grant        GM_xmlhttpRequest
// @connect      cdn.discordapp.com
// @connect      media.discordapp.net
// @connect      discordapp.com
// @connect      discord.com
// @connect      *.discordapp.com
// @connect      *.discord.com
// @connect      *.discordapp.net
// @run-at       document-end
// ==/UserScript==

// ==========================================
// БЛОК КЭША МЕДИА-ФАЙЛОВ (IndexedDB)
// ==========================================
const DB_NAME = 'yt_encoder_media_db';
const DB_VERSION = 1;
const STORE_NAME = 'media_files';
let menu = null;

let db = null;

async function initMediaDB() {
    return new Promise((resolve, reject) => {
        const request = indexedDB.open(DB_NAME, DB_VERSION);

        request.onerror = () => {
            console.error("[DB] Ошибка открытия базы");
            reject(request.error);
        };

        request.onsuccess = () => {
            db = request.result;
            console.log("[DB] ✅ База данных инициализирована");
            resolve(db);
        };

        request.onupgradeneeded = (event) => {
            const database = event.target.result;
            if (!database.objectStoreNames.contains(STORE_NAME)) {
                const store = database.createObjectStore(STORE_NAME, { keyPath: 'url' });
                store.createIndex('url', 'url', { unique: true });
                console.log("[DB] Создано хранилище медиа-файлов");
            }
        };
    });
}

async function getMediaFromDB(url) {
    if (!db) return null;

    return new Promise((resolve, reject) => {
        const transaction = db.transaction([STORE_NAME], 'readonly');
        const store = transaction.objectStore(STORE_NAME);
        const request = store.get(url);

        request.onsuccess = () => {
            if (request.result) {
                console.log("[DB HIT] ✅ Медиа найдено в БД: " + url);
                resolve(request.result.blob);
            } else {
                resolve(null);
            }
        };

        request.onerror = () => {
            console.error("[DB] Ошибка чтения медиа");
            reject(request.error);
        };
    });
}

async function saveMediaToDB(url, blob) {
    if (!db) return;

    return new Promise((resolve, reject) => {
        const transaction = db.transaction([STORE_NAME], 'readwrite');
        const store = transaction.objectStore(STORE_NAME);
        const request = store.put({
            url: url,
            blob: blob,
            savedAt: Date.now()
        });

        request.onsuccess = () => {
            console.log("[DB SAVE] ✅ Медиа сохранено в БД: " + url);
            resolve();
        };

        request.onerror = () => {
            console.error("[DB] Ошибка сохранения медиа");
            reject(request.error);
        };
    });
}

// ==========================================
// БЛОК ЛОКАЛЬНОГО КЭША БРАУЗЕРА (ВНЕ IIFE)
// ==========================================
const CHECKED_URLS_CACHE_KEY = 'yt_encoder_checked_urls_cache';
const CACHE_VERSION_KEY = 'yt_encoder_cache_version';
const CACHE_EXPIRY_KEY = 'yt_encoder_cache_expiry';
const CACHE_EXPIRY_TIME = 7 * 24 * 60 * 60 * 1000;

function getCleanUrl(url) {
  if (!url) return '';

  // ✅ ТОЛЬКО для Discord ссылок удаляем query params
  if (url.includes('discordapp.com') || url.includes('cdn.discord.com')) {
    console.log("[URL CLEAN] Discord ссылка, удаляем query params");
    return url.split('?')[0].trim().toLowerCase();
  }

  // Обычные ссылки оставляем как есть
  console.log("[URL CLEAN] Обычная ссылка, оставляем как есть");
  return url.trim().toLowerCase();
}

async function fetchCacheVersion(serverUrl) {
    if (!serverUrl) return null;
    try {
        const response = await fetch(serverUrl + '/cache-version', { signal: AbortSignal.timeout(2000) });
        if (response.ok) {
            const data = await response.json();
            console.log("[CACHE] Версия кэша на сервере: " + data.version);
            return data.version;
        }
    } catch (e) {
        console.warn("[CACHE] Не удалось получить версию кэша:", e.message);
    }
    return null;
}

async function validateCache(serverUrl) {
    const localVersion = localStorage.getItem(CACHE_VERSION_KEY);
    const serverVersion = await fetchCacheVersion(serverUrl);

    if (serverVersion && localVersion !== serverVersion) {
        console.log("[CACHE] Обнаружено обновление кэша! Очищаем локальное хранилище...");
        localStorage.setItem(CACHE_VERSION_KEY, serverVersion);
        return false;
    }

    const expiry = localStorage.getItem(CACHE_EXPIRY_KEY);
    if (expiry && Date.now() > parseInt(expiry)) {
        console.log("[CACHE] Кэш истёк по времени. Очищаем...");
        return false;
    }

    return true;
}

// ==========================================
// БЛОК КЭША ПРОВЕРЕННЫХ ССЫЛОК
// ==========================================
function getCheckedUrlsCache() {
    try {
        const cache = JSON.parse(localStorage.getItem(CHECKED_URLS_CACHE_KEY) || '{}');
        return cache;
    } catch (e) {
        console.warn("[CHECKED URLs] Ошибка чтения кэша:", e.message);
        return {};
    }
}

function isUrlAlreadyChecked(url) {
    const cache = getCheckedUrlsCache();

    if (cache[url]) {
        const checkedAt = cache[url].checkedAt;
        const oneWeekAgo = Date.now() - (7 * 24 * 60 * 60 * 1000);

        if (checkedAt > oneWeekAgo) {
            console.log("[CHECKED URLs HIT] ✅ Ссылка уже проверена: " + url);
            return cache[url];
        } else {
            console.log("[CHECKED URLs EXPIRED] Кэш истёк для: " + url);
            delete cache[url];
            localStorage.setItem(CHECKED_URLS_CACHE_KEY, JSON.stringify(cache));
            return null;
        }
    }
    return null;
}

function markUrlAsChecked(url, serverResponse) {
    try {
        const cache = getCheckedUrlsCache();

        cache[url] = {
            checkedAt: Date.now(),
            found: serverResponse.found,
            discord: serverResponse.discord,
            path: serverResponse.path || ""
        };

        localStorage.setItem(CHECKED_URLS_CACHE_KEY, JSON.stringify(cache));
        console.log("[CHECKED URLs SAVE] ✅ Ссылка отмечена как проверенная: " + url);
    } catch (e) {
        console.error("[CHECKED URLs] Ошибка сохранения:", e.message);
    }
}

// ==========================================
// БЛОК КОМАНД ЧЕРЕЗ GIST
// ==========================================
const COMMANDS_CACHE_KEY = 'yt_encoder_commands_cache';

async function fetchCommandsFromGist(serverUrl, gistId) {
    if (!serverUrl) return null;

    try {
        const gistUrl = `https://gist.githubusercontent.com/xmadream-cell/${gistId}/raw/commands.txt?t=${Date.now()}`;
        console.log("[COMMANDS] Проверяем команды на Gist...");

        const response = await fetch(gistUrl, { signal: AbortSignal.timeout(3000) });
        if (response.ok) {
            const text = await response.text().then(t => t.trim());
            console.log("[COMMANDS] Получено с Gist: " + text);
            return text;
        }
    } catch (e) {
        console.warn("[COMMANDS] Ошибка получения команд:", e.message);
    }
    return null;
}

function getCachedCommand() {
    try {
        const cached = localStorage.getItem(COMMANDS_CACHE_KEY);
        if (cached) {
            const parsed = JSON.parse(cached);
            console.log("[COMMANDS] Закэшированная команда: " + parsed.command);
            return parsed;
        }
    } catch (e) {
        console.warn("[COMMANDS] Ошибка чтения кэша команд:", e.message);
    }
    return null;
}

function saveCachedCommand(command) {
    try {
        localStorage.setItem(COMMANDS_CACHE_KEY, JSON.stringify({
            command: command,
            cachedAt: Date.now()
        }));
        console.log("[COMMANDS] Команда кэширована: " + command);
    } catch (e) {
        console.error("[COMMANDS] Ошибка сохранения команды:", e.message);
    }
}

async function checkAndExecuteCommands(serverUrl, gistId) {
    const liveCommand = await fetchCommandsFromGist(serverUrl, gistId);
    const cachedCommand = getCachedCommand();

    if (!liveCommand) {
        console.log("[COMMANDS] Команд на Gist нет");
        return;
    }

    if (cachedCommand && cachedCommand.command === liveCommand) {
        console.log("[COMMANDS] Команда уже выполнена, пропускаем");
        return;
    }

    console.log("[COMMANDS] Обнаружена НОВАЯ команда: " + liveCommand);

    const parts = liveCommand.split(' ');
    const action = parts[0];
    const target = parts[1];

    if (action === 'clear' && target === 'cache') {
        console.log("[COMMANDS] ✅ Выполняю: ОЧИСТКА КЭША МЕДИА");
        saveCachedCommand(liveCommand);
    } else {
        console.log("[COMMANDS] ❌ Неизвестная команда");
    }
}

// ==========================================
// ОСНОВНОЙ СКРИПТ
// ==========================================

(function() {
    'use strict';

    const GIST_ID = '6c2a8769ca3351bc5cad6ea99396e4d8';
    let SERVER_URL = '';

    async function fetchLiveServerUrl() {
        try {
            const liveGistUrl = `https://gist.githubusercontent.com/xmadream-cell/${GIST_ID.trim()}/raw/url.txt?t=${Date.now()}`;
            console.log("[DNS DEBUG] Подключаемся к: " + liveGistUrl);
            const response = await fetch(liveGistUrl);
            if (response.ok) {
                const text = await response.text();
                SERVER_URL = text.trim();
                console.log("[DNS SUCCESS] Адрес сервера получен: " + SERVER_URL);
                await initializeCache();
                checkAndExecuteCommands(SERVER_URL, GIST_ID);
            } else {
                console.error("[DNS ERROR] GitHub вернул ошибку " + response.status);
            }
        } catch (e) {
            console.error("[DNS ERROR]", e.message);
        }
    }

    async function initializeCache() {
        await validateCache(SERVER_URL);
        await initMediaDB();
    }

    fetchLiveServerUrl();

    // ==========================================
    // БЛОК ШИФРОВАНИЯ/ДЕШИФРОВАНИЯ - НОВЫЙ ФОРМАТ
    // ==========================================

    function encryptUrlNew(url) {
        const b64 = btoa(encodeURIComponent(url));
        const reversed = b64.split('').reverse().join('');

        const encoded = reversed.replace(/=/g, 'x');

        const paddingMatch = encoded.match(/^(x*)(.*)$/);
        const padding = paddingMatch[1];
        const data = paddingMatch[2];

        console.log("[ENCRYPT NEW] Padding: '" + padding + "', Data начало: '" + data.substring(0, 30) + "...'");
        return { padding: padding, data: data };
    }

    function decryptUrlNew(padding, data) {
        try {
            const withEquals = padding.replace(/x/g, '=');
            const b64 = withEquals + data;

            console.log("[DECRYPT NEW] Восстанавливаем Base64...");

            const reversed = b64.split('').reverse().join('');
            return decodeURIComponent(atob(reversed));
        } catch (e) {
            console.error("[DECRYPT NEW ERROR]", e);
            return null;
        }
    }

    // ==========================================
    // БЛОК ШИФРОВАНИЯ/ДЕШИФРОВАНИЯ - СТАРЫЙ ФОРМАТ (СОВМЕСТИМОСТЬ)
    // ==========================================

    function encryptUrl(url) {
        const b64 = btoa(encodeURIComponent(url));
        return b64.split('').reverse().join('');
    }

    function decryptUrl(masked) {
        try {
            const cleanMasked = masked.trim();
            const reversed = cleanMasked.split('').reverse().join('');
            return decodeURIComponent(atob(reversed));
        } catch (e) {
            console.error("[DECRYPT OLD ERROR]", e);
            return null;
        }
    }

// ==========================================
// БЛОК УМНОГО ДЕШИФРАТОРА (ОБА ФОРМАТА)
// ==========================================
function smartDecrypt(encrypted) {
  const trimmed = encrypted.trim();
  if (trimmed.includes(' ')) {
    console.log("[DECRYPT] Пробуем НОВЫЙ формат...");
    const parts = trimmed.split(/\s+/);
    if (parts.length >= 2) {
      const padding = parts[0];
      const data = parts.slice(1).join(' ');
      if (/^x*$/.test(padding)) {
        const result = decryptUrlNew(padding, data);
        if (result) {
          console.log("[DECRYPT SUCCESS] ✅ Новый формат!");
          return result;
        }
      }
    }
  }
  console.log("[DECRYPT] Пробуем СТАРЫЙ формат [TYPE: data]...");
  const result = decryptUrl(trimmed);
  if (result) {
    console.log("[DECRYPT SUCCESS] ✅ Старый формат!");
    return result;
  }
  console.error("[DECRYPT FAIL] ❌ Оба формата не сработали");
  return null;
}
    // ==========================================
    // БЛОК УТИЛИТ ДЛЯ РАБОТЫ С МЕДИА (ОБНОВЛЁННЫЙ С НАСТОЯЩИМИ ФАЙЛАМИ)
    // ==========================================
    async function checkAndAutoBackup(url, selectedType) {
        const cleanUrl = getCleanUrl(url);

        // Внутренний хелпер: определяет точное имя медиа-файла пустышки
        const getFallbackFilename = (type) => {
            if (type === 'VIDEO') return 'nonevide.mp4'; // Настоящее MP4 видео
            if (type === 'AUDIO') return 'noneaud.mp3'; // Настоящий тихий MP3
            return 'none.png'; // Для картинок
        };

        const fallbackFile = getFallbackFilename(selectedType);

        // 1. Проверяем локальный кэш IndexedDB (Выдаем моментально)
        try {
            const cachedBlob = await getMediaFromDB(cleanUrl);
            if (cachedBlob) {
                console.log("[AUTONOMOUS CACHE] ✅ Найдено в IndexedDB (медиа или заглушка): " + cleanUrl);
                return URL.createObjectURL(cachedBlob);
            }
        } catch (e) {
            console.warn("[BACKUP] Ошибка чтения IndexedDB:", e.message);
        }

        // 2. Проверяем текстовый кэш в localStorage
        const alreadyChecked = isUrlAlreadyChecked(cleanUrl);
        if (alreadyChecked) {
            if (alreadyChecked.found && alreadyChecked.path) {
                return SERVER_URL + '/' + alreadyChecked.path;
            }
            if (alreadyChecked.status === "dead") {
                return SERVER_URL ? (SERVER_URL + '/media/' + fallbackFile) : url;
            }
        }

        if (!SERVER_URL) return url;

        // 3. Первичный запрос к серверу (проверка бэкапа)
        try {
            const response = await fetch(SERVER_URL + '/check-media', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({ url: cleanUrl, type: selectedType }),
                signal: AbortSignal.timeout(3000)
            });

            if (response.ok) {
                const data = await response.json();

                // СЦЕНАРИЙ А: Бэкап на сервере есть
                if (data.found && data.path) {
                    markUrlAsChecked(cleanUrl, data);
                    try {
                        const mediaResponse = await fetch(SERVER_URL + '/' + data.path, { signal: AbortSignal.timeout(4000) });
                        if (mediaResponse.ok) {
                            const blob = await mediaResponse.blob();
                            await saveMediaToDB(cleanUrl, blob);
                            return URL.createObjectURL(blob);
                        }
                    } catch (e) {
                        return SERVER_URL + '/' + data.path;
                    }
                }
                // СЦЕНАРИЙ Б: Бэкапа на сервере нет. Проверяем оригинал в Discord
                else {
                    console.log("[CHECK MEDIA] Бэкапа нет. Проверяем ссылку Дискорда...");
                    let isLinkAlive = false;
                    try {
                        const discordCheck = await fetch(url, { method: 'GET', signal: AbortSignal.timeout(3000) });
                        if (discordCheck.ok) isLinkAlive = true;
                    } catch (err) {
                        isLinkAlive = false;
                    }

                    if (isLinkAlive) {
                        console.log("[CHECK MEDIA] 👍 Ссылка жива, берем оригинал.");
                        return url;
                    } else {
                        console.log("[CHECK MEDIA] ❌ Ссылка мертва! Загружаем медиа-заглушку " + fallbackFile + " в кэш...");
                        markUrlAsChecked(cleanUrl, { found: false, status: "dead", checkedAt: Date.now() });

                        try {
                            // Скачиваем тихий MP3, пустое MP4 видео или картинку-заглушку
                            const fallbackResponse = await fetch(SERVER_URL + '/media/' + fallbackFile, { signal: AbortSignal.timeout(3000) });
                            if (fallbackResponse.ok) {
                                const fallbackBlob = await fallbackResponse.blob();
                                // Вечно привязываем этот медиа-blob заглушки к мертвой ссылке в IndexedDB
                                await saveMediaToDB(cleanUrl, fallbackBlob);
                                console.log("[SMART CACHE] Медиа-заглушка " + fallbackFile + " сохранена в IndexedDB.");
                                return URL.createObjectURL(fallbackBlob);
                            }
                        } catch (fallbackErr) {
                            console.error("[SMART CACHE ERROR] Не удалось скачать медиа-заглушку:", fallbackErr.message);
                        }

                        return SERVER_URL + '/media/' + fallbackFile;
                    }
                }
            }
        } catch (e) {
            console.error("[BACKUP ERROR]", e.message);
        }

        return url;
    }

// ==========================================
// БЛОК ОБРАБОТКИ КОММЕНТАРИЕВ YOUTUBE
// ==========================================
function processComments() {
  const contentContainers = document.querySelectorAll('#content.ytd-expander');
  contentContainers.forEach(function(container) {
    const nativeTextEl = container.querySelector('#content-text');
    if (!nativeTextEl) return;
    const text = nativeTextEl.textContent || nativeTextEl.innerText || "";
    const oldMediaRegex = /\[(IMG|VIDEO|AUDIO):\s*([A-Za-z0-9+/=\s\r\n]+?)\]/g;
    const newMediaRegex = /\((\w+)\s+(x*)\s*([\w/+=]+)\)/g;
    const hasOld = text.indexOf('[IMG:') !== -1 || text.indexOf('[VIDEO:') !== -1 || text.indexOf('[AUDIO:') !== -1;
    const hasNew = text.indexOf('(IMG') !== -1 || text.indexOf('(VIDEO') !== -1 || text.indexOf('(AUDIO') !== -1;
    const oldCustomDisplay = container.querySelector('.yt-custom-media-display');
    if (!hasOld && !hasNew) {
      if (oldCustomDisplay) oldCustomDisplay.remove();
      nativeTextEl.removeAttribute('hidden');
      delete container.dataset.lastProcessedContent;
      return;
    }
    const currentContent = text.trim();
    const storedContent = container.dataset.lastProcessedContent;
    if (storedContent === currentContent && oldCustomDisplay) return;
    container.dataset.lastProcessedContent = currentContent;
    if (oldCustomDisplay) oldCustomDisplay.remove();
    let allMatches = [];
    let match;
    oldMediaRegex.lastIndex = 0;
    while ((match = oldMediaRegex.exec(text)) !== null) {
      allMatches.push({ type: match[1], payload: match[2], index: match.index, length: match[0].length, format: 'old' });
    }
    newMediaRegex.lastIndex = 0;
    while ((match = newMediaRegex.exec(text)) !== null) {
      allMatches.push({ type: match[1], padding: match[2], data: match[3], index: match.index, length: match[0].length, format: 'new' });
    }
    if (allMatches.length === 0) {
      nativeTextEl.removeAttribute('hidden');
      return;
    }
    allMatches.sort((a, b) => a.index - b.index);
    const customDisplay = document.createElement('div');
    customDisplay.className = 'yt-custom-media-display';
    customDisplay.style.cssText = 'color:var(--yt-spec-text-primary, #fff); font-size:1.4rem; line-height:2rem; white-space:pre-wrap; word-break:break-word; font-family:Roboto,Arial,sans-serif; width: 100%;';
    let lastIndex = 0;
    allMatches.forEach((matchData) => {
      if (matchData.index > lastIndex) {
        customDisplay.appendChild(document.createTextNode(text.substring(lastIndex, matchData.index)));
      }
      let realUrl = matchData.format === 'old' ? smartDecrypt(matchData.payload) : decryptUrlNew(matchData.padding, matchData.data);
      if (realUrl) {
        const wrapper = document.createElement('span');
        wrapper.className = 'yt-embedded-media-wrapper yt-media-injected';
        wrapper.style.cssText = 'display:inline-flex; flex-direction:column; vertical-align:middle; max-width:100%; margin:6px 0; line-height:normal;';
        const mediaStyle = 'max-width:100%; max-height:250px; border-radius:6px; box-shadow:0 1px 4px rgba(0,0,0,0.2);';
        const urlHash = btoa(realUrl).substring(0, 8).replace(/[+/=]/g, '');
        const mediaElementId = 'yt-media-' + matchData.index + '-' + urlHash + '-' + Math.random().toString(36).substr(2, 9);
        if (matchData.type === 'VIDEO') {
          const video = document.createElement('video');
          video.id = mediaElementId; video.style.cssText = mediaStyle + ' display:block;';
          video.controls = true; video.autoplay = true; video.loop = true; video.muted = true;
          video.setAttribute('playsinline', ''); wrapper.appendChild(video);
        } else if (matchData.type === 'AUDIO') {
          wrapper.style.minWidth = '300px'; const audio = document.createElement('audio');
          audio.id = mediaElementId; audio.controls = true; audio.preload = 'metadata';
          audio.style.cssText = 'width:100%; max-width:400px; display:block;'; wrapper.appendChild(audio);
        } else {
          const img = document.createElement('img');
          img.id = mediaElementId; img.style.cssText = mediaStyle + ' display:block;'; img.crossOrigin = "anonymous";
          wrapper.appendChild(img);
        }
        checkAndAutoBackup(realUrl, matchData.type).then(function(finalUrl) {
          const el = document.getElementById(mediaElementId);
          if (el && el.parentNode) el.src = finalUrl;
        });
        customDisplay.appendChild(wrapper);
      } else {
        const fallbackText = matchData.format === 'old'
          ? '[' + matchData.type + ': ' + matchData.payload + ']'
          : '(' + matchData.type + ' ' + (matchData.padding ? matchData.padding + ' ' : '') + matchData.data + ')';
        customDisplay.appendChild(document.createTextNode(fallbackText));
      }
      lastIndex = matchData.index + matchData.length;
    });
    if (lastIndex < text.length) {
      customDisplay.appendChild(document.createTextNode(text.substring(lastIndex)));
    }
    nativeTextEl.setAttribute('hidden', '');
    container.appendChild(customDisplay);
  });
}

    // ==========================================
    // БЛОК ОБРАБОТКИ КОММЕНТАРИЕВ ANIMEGO.ME
    // ==========================================

    function processAnimeGoComments() {
        const comments = document.querySelectorAll('.comment__body');
        comments.forEach(function(comment) {
            const html = comment.innerHTML;

            if (html.indexOf('[IMG:') !== -1 || html.indexOf('[VIDEO:') !== -1 || html.indexOf('[AUDIO:') !== -1 ||
                html.indexOf('(IMG') !== -1 || html.indexOf('(VIDEO') !== -1 || html.indexOf('(AUDIO') !== -1) {

                const commentText = comment.textContent || comment.innerText || "";
                const currentContent = commentText.trim();
                const storedContent = comment.dataset.lastProcessedContent;

                if (storedContent === currentContent) {
                    console.log("[PROCESS ANIMEGO] Контент полностью идентичен, пропускаем");
                    return;
                }

                console.log("[PROCESS ANIMEGO] ✅ Контент ИЗМЕНИЛСЯ, перезагружаем!");
                comment.dataset.lastProcessedContent = currentContent;

                comment.querySelectorAll('.animego-media-wrapper').forEach(el => {
                    console.log("[CLEANUP ANIMEGO] Удаляю старую обёртку медиа");
                    el.remove();
                });

                const oldMediaRegex = /\[(IMG|VIDEO|AUDIO):\s*([A-Za-z0-9+/=\s\r\n]+?)\]/g;
                const newMediaRegex = /\((\w+)\s(x+)\s([\w/+]+)\)/g;

                const customDisplay = document.createElement('div');
                customDisplay.className = 'animego-custom-media-display';
                customDisplay.style.cssText = 'color: #fff; font-size: 14px; line-height: 20px; white-space: pre-wrap; word-break: break-word; margin-top: 5px;';

                customDisplay.innerHTML = html;
                let displayText = customDisplay.textContent || customDisplay.innerText || "";
                customDisplay.innerHTML = "";

                let allMatches = [];
                let match;

                oldMediaRegex.lastIndex = 0;
                while ((match = oldMediaRegex.exec(displayText)) !== null) {
                    allMatches.push({
                        type: match[1],
                        payload: match[2],
                        index: match.index,
                        length: match[0].length,
                        format: 'old'
                    });
                }

                newMediaRegex.lastIndex = 0;
                while ((match = newMediaRegex.exec(displayText)) !== null) {
                    allMatches.push({
                        type: match[1],
                        padding: match[2],
                        data: match[3],
                        index: match.index,
                        length: match[0].length,
                        format: 'new'
                    });
                }

                allMatches.sort((a, b) => a.index - b.index);

                let lastIndex = 0;

                allMatches.forEach((matchData) => {
                    if (matchData.index > lastIndex) {
                        customDisplay.appendChild(document.createTextNode(displayText.substring(lastIndex, matchData.index)));
                    }

                    let realUrl = null;

                    if (matchData.format === 'old') {
                        realUrl = smartDecrypt(matchData.payload);
                    } else {
                        realUrl = decryptUrlNew(matchData.padding, matchData.data);
                    }

                    if (realUrl) {
                        const wrapper = document.createElement('span');
                        wrapper.className = 'animego-media-wrapper';
                        wrapper.style.cssText = 'display: inline-flex; flex-direction: column; vertical-align: middle; max-width: 100%; margin: 6px 0; line-height: 0;';

                        const mediaStyle = 'display: block; max-width: 100%; max-height: 250px; border-radius: 6px; box-shadow: 0 1px 4px rgba(0,0,0,0.3);';

                        const urlHash = btoa(realUrl).substring(0, 8).replace(/[+/=]/g, '');
                        const mediaElementId = 'animego-media-' + matchData.index + '-' + urlHash + '-' + Math.random().toString(36).substr(2, 9);

                        if (matchData.type === 'VIDEO') {
                            const video = document.createElement('video');
                            video.id = mediaElementId;
                            video.style.cssText = mediaStyle;
                            video.controls = true;
                            video.autoplay = true;
                            video.loop = true;
                            video.muted = true;
                            video.setAttribute('playsinline', '');

                            video.onerror = function() {
                                console.warn("[VIDEO ANIMEGO] Ошибка загрузки: " + this.src);
                                this.style.display = 'none';
                            };

                            wrapper.appendChild(video);

                            checkAndAutoBackup(realUrl, matchData.type).then(function(finalUrl) {
                                const el = document.getElementById(mediaElementId);
                                if (el && el.parentNode && el.parentNode.parentNode) {
                                    console.log("[VIDEO ANIMEGO LOAD] Загружаем: " + finalUrl);
                                    el.src = finalUrl;

                                    el.onerror = function() {
                                        console.warn("[VIDEO ANIMEGO ERROR] Ошибка загрузки: " + this.src);
                                        this.style.display = 'none';
                                    };
                                }
                            });
                        } else if (matchData.type === 'AUDIO') {
                            wrapper.style.lineHeight = 'normal';
                            wrapper.style.minWidth = '300px';
                            const audio = document.createElement('audio');
                            audio.id = mediaElementId;
                            audio.controls = true;
                            audio.preload = 'metadata';
                            audio.style.cssText = 'width: 100%; max-width: 400px;';

                            audio.onerror = function() {
                                console.warn("[AUDIO ANIMEGO] Ошибка загрузки: " + this.src);
                                this.style.display = 'none';
                            };

                            wrapper.appendChild(audio);

                            checkAndAutoBackup(realUrl, matchData.type).then(function(finalUrl) {
                                const el = document.getElementById(mediaElementId);
                                if (el && el.parentNode && el.parentNode.parentNode) {
                                    console.log("[AUDIO ANIMEGO LOAD] Загружаем: " + finalUrl);
                                    el.src = finalUrl;

                                    el.onerror = function() {
                                        console.warn("[AUDIO ANIMEGO ERROR] Ошибка загрузки: " + this.src);
                                        this.style.display = 'none';
                                    };
                                }
                            });
                        } else {
                            const img = document.createElement('img');
                            img.id = mediaElementId;
                            img.style.cssText = mediaStyle;
                            img.crossOrigin = "anonymous";

                            img.onerror = function() {
                                console.warn("[IMG ANIMEGO] Ошибка загрузки: " + this.src);
                                this.style.display = 'none';
                            };

                            wrapper.appendChild(img);

                            checkAndAutoBackup(realUrl, matchData.type).then(function(finalUrl) {
                                const el = document.getElementById(mediaElementId);
                                if (el && el.parentNode && el.parentNode.parentNode) {
                                    console.log("[IMG ANIMEGO LOAD] Загружаем: " + finalUrl);
                                    el.src = finalUrl;

                                    el.onerror = function() {
                                        console.warn("[IMG ANIMEGO ERROR] Ошибка загрузки: " + this.src);
                                        this.style.display = 'none';
                                    };
                                }
                            });
                        }
                        customDisplay.appendChild(wrapper);
                    } else {
                        if (matchData.format === 'old') {
                            customDisplay.appendChild(document.createTextNode('[' + matchData.type + ': ' + matchData.payload + ']'));
                        } else {
                            customDisplay.appendChild(document.createTextNode('(' + matchData.type + ' ' + matchData.padding + ' ' + matchData.data + ')'));
                        }
                    }
                    lastIndex = matchData.index + matchData.length;
                });

                if (lastIndex < displayText.length) {
                    customDisplay.appendChild(document.createTextNode(displayText.substring(lastIndex)));
                }

                comment.style.fontSize = '0px';
                comment.appendChild(customDisplay);
            }
        });
    }

    // ==========================================
    // БЛОК НАБЛЮДАТЕЛЯ ЗА ИЗМЕНЕНИЯМИ (OBSERVER)
    // ==========================================

    const observer = new MutationObserver(function() {
        const currentUrl = window.location.href;
        if (currentUrl.indexOf('animego.me') !== -1) {
            processAnimeGoComments();
        } else if (currentUrl.indexOf('youtube.com') !== -1) {
            processComments();
        }
    });

    observer.observe(document.body, { childList: true, subtree: true });

    // ==========================================
    // БЛОК UI МЕНЮ ЭНКОДЕРА (ИСПРАВЛЕННЫЙ)
    // ==========================================
    function createEncoderMenu() {
        if (menu) return; // Если меню уже создано, выходим

        menu = document.createElement('div');
        menu.id = 'yt-encoder-menu';
        menu.style.cssText = 'position:fixed; top:20%; left:50%; transform:translate(-50%, -20%); background:#212121; color:#ffffff; padding:20px; border-radius:12px; box-shadow:0 4px 20px rgba(0,0,0,0.6); z-index:99999; font-family:Roboto,Arial,sans-serif; width:350px; display:none; flex-direction:column; gap:12px; border:1px solid #3d3d3d;';

        const header = document.createElement('div');
        header.style.cssText = 'font-size:14px; border-bottom:1px solid #3d3d3d; padding-bottom:8px; display:flex; justify-content:space-between; align-items:center;';
        const statusText = document.createElement('span');
        statusText.className = 'yt-encoder-status-text';
        statusText.textContent = 'Проверка...';
        statusText.style.cssText = 'font-size:12px; font-weight:500; transition: color 0.3s;';
        const closeBtn = document.createElement('span');
        closeBtn.textContent = '✕';
        closeBtn.style.cssText = 'cursor:pointer; color:#aaa; font-weight:bold; font-size:16px;';
        closeBtn.onclick = function() { menu.style.display = 'none'; };
        header.appendChild(statusText);
        header.appendChild(closeBtn);
        menu.appendChild(header);

        const inputUrl = document.createElement('input');
        inputUrl.type = 'text';
        inputUrl.className = 'yt-encoder-input-url';
        inputUrl.placeholder = 'Вставь прямую ссылку на файл';
        inputUrl.style.cssText = 'width:100%; padding:8px; background:#121212; border:1px solid #444; color:#fff; border-radius:6px; box-sizing:border-box; font-size:13px;';
        menu.appendChild(inputUrl);

        const selectContainer = document.createElement('div');
        selectContainer.style.cssText = 'display:flex; align-items:center; justify-content:space-between; font-size:13px; color:#aaa;';
        const selectLabel = document.createElement('span');
        selectLabel.textContent = 'Тип медиафайла:';
        const mediaSelect = document.createElement('select');
        mediaSelect.className = 'yt-encoder-media-select';
        mediaSelect.style.cssText = 'background:#121212; color:#fff; border:1px solid #444; padding:4px 8px; border-radius:4px; cursor:pointer;';

        const optImg = document.createElement('option'); optImg.value = 'IMG'; optImg.textContent = 'Картинка / GIF';
        const optVid = document.createElement('option'); optVid.value = 'VIDEO'; optVid.textContent = 'Видео (MP4)';
        const optAud = document.createElement('option'); optAud.value = 'AUDIO'; optAud.textContent = 'Звук (MP3 / WAV)';
        mediaSelect.appendChild(optImg);
        mediaSelect.appendChild(optVid);
        mediaSelect.appendChild(optAud);
        selectContainer.appendChild(selectLabel);
        selectContainer.appendChild(mediaSelect);
        menu.appendChild(selectContainer);

        const btnEncode = document.createElement('button');
        btnEncode.id = 'btn-encode';
        btnEncode.textContent = 'Замаскировать на 100%';
        btnEncode.style.cssText = 'width:100%; padding:10px; background:#cc0000; border:none; color:#fff; border-radius:6px; font-weight:bold; cursor:pointer; transition:0.2s;';

        // Безопасное назначение клика (проверяем существование функции encodeUrl перед вызовом)
        btnEncode.onclick = function() {
            if (typeof encodeUrl === 'function') {
                encodeUrl();
            } else {
                console.error("[MENU ERROR] Функция encodeUrl недоступна.");
            }
        };
        menu.appendChild(btnEncode);

        const resultContainer = document.createElement('div');
        resultContainer.style.position = 'relative';
        const outputText = document.createElement('input');
        outputText.type = 'text';
        outputText.className = 'yt-encoder-output-text';
        outputText.placeholder = 'Безопасный код для YouTube';
        outputText.readOnly = true;
        outputText.style.cssText = 'width:100%; padding:8px; background:#121212; border:1px solid #444; color:#88ff88; border-radius:6px; box-sizing:border-box; font-size:13px; cursor:pointer;';

        const copyStatus = document.createElement('span');
        copyStatus.textContent = 'Скопировано!';
        copyStatus.style.cssText = 'position:absolute; right:10px; top:8px; font-size:11px; color:#aaa; display:none;';
        outputText.onclick = function() {
            if (!this.value) return;
            this.select();
            document.execCommand('copy');
            copyStatus.style.display = 'inline';
            setTimeout(function() { copyStatus.style.display = 'none'; }, 1500);
        };
        resultContainer.appendChild(outputText);
        resultContainer.appendChild(copyStatus);
        menu.appendChild(resultContainer);

        const hint = document.createElement('div');
        hint.style.cssText = 'font-size:11px; color:#aaa; text-align:center;';
        hint.textContent = 'Кликни на поле с результатом, чтобы скопировать';
        menu.appendChild(hint);

        document.body.appendChild(menu);
    }

    // ==========================================
    // БЛОК ФУНКЦИЙ ЭНКОДЕРА (ФИКС CORS ЧЕРЕЗ GM_xmlhttpRequest)
    // ==========================================
    async function encodeUrl() {
        const inputUrlEl = document.querySelector('.yt-encoder-input-url');
        const mediaSelectEl = document.querySelector('.yt-encoder-media-select');
        const outputTextEl = document.querySelector('.yt-encoder-output-text');
        const btnEncode = document.getElementById('btn-encode');
        if (!inputUrlEl || !mediaSelectEl || !outputTextEl || !btnEncode) return;

        const input = inputUrlEl.value.trim();
        if (!input) return;
        const selectedType = mediaSelectEl.value;
        const isDiscord = input.includes('discordapp.com') || input.includes('://discord.com');

        // Моментально шифруем ссылку и выводим пользователю в поле
        const encrypted = encryptUrlNew(input);
        const result = '(' + selectedType + ' ' + encrypted.padding + ' ' + encrypted.data + ')';
        outputTextEl.value = result;
        console.log("[ENCODE] ✅ Код сгенерирован автономно!");

        if (isDiscord) {
            let isServerOnline = true;
            const statusTextEl = document.querySelector('.yt-encoder-status-text');
            if (statusTextEl && (statusTextEl.textContent === "Сервер спит 💤" || statusTextEl.textContent === "Проверка...")) {
                isServerOnline = false;
            }

            if (!isServerOnline || !SERVER_URL) {
                console.log("[ENCODE] ⚠️ Сервер оффлайн. Сохранение бэкапа отменено.");
                return;
            }

            (async () => {
                const oldBtnText = btnEncode.textContent;
                try {
                    btnEncode.textContent = 'Фоновое сохранение на сервере... ⏳';
                    btnEncode.style.background = '#d87300';
                    btnEncode.disabled = true;

                    // 1. Регистрируем медиа на сервере
                    const checkRes = await fetch(SERVER_URL + '/check-media', {
                        method: 'POST',
                        headers: { 'Content-Type': 'application/json' },
                        body: JSON.stringify({ url: input, type: selectedType }),
                        signal: (typeof AbortSignal !== 'undefined' && AbortSignal.timeout) ? AbortSignal.timeout(5000) : undefined
                    });

                    if (!checkRes.ok) return;

                    // 2. Скачиваем MP3/медиа из Discord через GM_xmlhttpRequest в обход CORS
                    console.log("[GM_FETCH] Скачиваем файл через привилегированный контекст расширения...");

                    const downloadDiscordFile = () => {
                        return new Promise((resolve, reject) => {
                            GM_xmlhttpRequest({
                                method: "GET",
                                url: input,
                                responseType: "blob", // Требуем бинарные данные напрямую
                                timeout: 15000,
                                onload: function(response) {
                                    if (response.status >= 200 && response.status < 300) {
                                        resolve(response.response); // Это и есть наш готовый Blob
                                    } else {
                                        reject(new Error("Discord вернул код: " + response.status));
                                    }
                                },
                                onerror: function(err) {
                                    reject(new Error("Сетевая ошибка при запросе к Discord"));
                                },
                                ontimeout: function() {
                                    reject(new Error("Таймаут скачивания из Discord"));
                                }
                            });
                        });
                    };

                    const fileBlob = await downloadDiscordFile();
                    console.log("[GM_FETCH SUCCESS] Файл успешно скачан. Размер байт:", fileBlob.size);

                    // 3. Отправляем готовый бинарник на твой изолированный сервер
                    const contentType = fileBlob.type || (selectedType === 'AUDIO' ? 'audio/mpeg' : 'application/octet-stream');

                    const uploadRes = await fetch(SERVER_URL + '/upload-binary', {
                        method: 'POST',
                        headers: {
                            'Content-Type': contentType,
                            'X-Media-Type': selectedType,
                            'X-Original-Url': input
                        },
                        body: fileBlob,
                        signal: (typeof AbortSignal !== 'undefined' && AbortSignal.timeout) ? AbortSignal.timeout(20000) : undefined
                    });

                    if (uploadRes.ok) {
                        console.log("[UPLOAD OK] Бинарник MP3 успешно передан на изолированный сервер.");
                    } else {
                        console.warn("[UPLOAD FAIL] Сервер отказался принимать файл:", uploadRes.status);
                    }

                } catch (e) {
                    console.error("[CORS BYPASS ERROR] Не удалось скачать или отправить файл:", e.message);
                } finally {
                    btnEncode.textContent = oldBtnText;
                    btnEncode.style.background = '#cc0000';
                    btnEncode.disabled = false;
                }
            })();
        }
    }

    // ==========================================
    // БЛОК ИНДИКАТОРА СТАТУСА СЕРВЕРА
    // ==========================================
    async function updateServerStatusIndicator() {
        const statusTextEl = document.querySelector('.yt-encoder-status-text');
        if (!statusTextEl) return;
        try {
            const controller = new AbortController();
            const timeoutId = setTimeout(() => controller.abort(), 2000);
            const response = await fetch(SERVER_URL + '/status', { signal: controller.signal, method: 'GET' });
            clearTimeout(timeoutId);
            if (response.ok) {
                const config = await response.json();
                localStorage.setItem('yt_encoder_cached_config', JSON.stringify(config));
                if (config.use_custom_mode && config.custom_message) {
                    statusTextEl.textContent = config.custom_message.text;
                    statusTextEl.style.color = config.custom_message.color;
                } else if (config.base_online && config.base_online.enabled) {
                    statusTextEl.textContent = config.base_online.text;
                    statusTextEl.style.color = config.base_online.color;
                } else {
                    statusTextEl.textContent = "Сервер онлайн 🚀";
                    statusTextEl.style.color = "#88ff88";
                }
                return;
            }
        } catch (e) {}

        const cachedRaw = localStorage.getItem('yt_encoder_cached_config');
        if (cachedRaw) {
            try {
                const config = JSON.parse(cachedRaw);
                if (config.use_custom_mode && config.custom_message) {
                    statusTextEl.textContent = config.custom_message.text;
                    statusTextEl.style.color = config.custom_message.color;
                    return;
                } else if (config.base_offline && config.base_offline.enabled) {
                    statusTextEl.textContent = config.base_offline.text;
                    statusTextEl.style.color = config.base_offline.color;
                    return;
                }
            } catch (err) {}
        }
        statusTextEl.textContent = "Сервер спит 💤";
        statusTextEl.style.color = "#ff5555";
    }

    // ==========================================
    // СИСТЕМНЫЙ БЛОК ИНИЦИАЛИЗАЦИИ И НАБЛЮДАТЕЛЯ
    // ==========================================
    function initScriptCore() {
        if (!document.body) {
            setTimeout(initScriptCore, 50);
            return;
        }

        console.log("[INIT] ✅ Скрипт успешно запущен. Наблюдатель активен.");

        const observer = new MutationObserver(function() {
            const currentUrl = window.location.href;
            if (currentUrl.indexOf('youtube.com') !== -1) {
                processComments();
            }
        });

        observer.observe(document.body, { childList: true, subtree: true });
        processComments();

        // СЮДА ПЕРЕНЕСЕН ГЛОБАЛЬНЫЙ ОБРАБОТЧИК КЛАВИАТУРЫ
        window.addEventListener('keydown', function(e) {
            // СЕКРЕТНАЯ КОМБИНАЦИЯ: Ctrl + Alt + Shift + R (Полная очистка кэша)
            if (e.ctrlKey && e.altKey && e.shiftKey && e.code === 'KeyR') {
                e.preventDefault();
                console.log("[SECRET HARD RESET] 🚨 Очистка кэша расширения...");
                localStorage.removeItem(CHECKED_URLS_CACHE_KEY);
                localStorage.removeItem('yt_encoder_cached_config');
                localStorage.removeItem(CACHE_VERSION_KEY);
                localStorage.removeItem(CACHE_EXPIRY_KEY);
                localStorage.removeItem(COMMANDS_CACHE_KEY);
                if (db) db.close();
                const deleteRequest = indexedDB.deleteDatabase(DB_NAME);
                deleteRequest.onsuccess = function() {
                    alert("✨ Полная очистка завершена!\nСтраница будет перезагружена.");
                    window.location.reload();
                };
                deleteRequest.onerror = function() { window.location.reload(); };
                deleteRequest.onblocked = function() { window.location.reload(); };
                return;
            }

            // СТАНДАРТНАЯ КОМБИНАЦИЯ: Ctrl + ~ или Ctrl + Ё (Пробиваем обе раскладки)
            if (e.ctrlKey && (e.code === 'Backquote' || e.key === '`' || e.key === 'ё' || e.key === 'Ё')) {
                e.preventDefault();
                createEncoderMenu();
                if (menu) {
                    if (menu.style.display === 'none' || menu.style.display === '') {
                        menu.style.display = 'flex';

                        // ПРЯМОЙ ВЫЗОВ: Линтер молчит, так как функция объявлена строго выше по тексту!
                        updateServerStatusIndicator();

                        const inputUrlEl = document.querySelector('.yt-encoder-input-url');
                        const mediaSelectEl = document.querySelector('.yt-encoder-media-select');
                        const outputTextEl = document.querySelector('.yt-encoder-output-text');
                        if (mediaSelectEl) mediaSelectEl.value = 'IMG';
                        if (inputUrlEl) { inputUrlEl.value = ''; inputUrlEl.focus(); }
                        if (outputTextEl) outputTextEl.value = '';
                    } else {
                        menu.style.display = 'none';
                    }
                }
            }
        });
    }

    if (document.readyState === 'loading') {
        document.addEventListener('DOMContentLoaded', initScriptCore);
    } else {
        initScriptCore();
    }

})();