Позволяет отправлять фото, гифки, звук и видео в комментарии ютуб
// ==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();
}
})();