Jut.su Auto+ Core Library

Ядро логики для Jut.su Auto+ (API, обсерверы, парсинг названий)

Този скрипт не може да бъде инсталиран директно. Това е библиотека за други скриптове и може да бъде използвана с мета-директива // @require https://update.greatest.deepsurf.us/scripts/565619/1752677/Jutsu%20Auto%2B%20Core%20Library.js

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey, Greasemonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да инсталирате разширение, като например Tampermonkey .

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Violentmonkey.

За да инсталирате този скрипт, трябва да имате инсталирано разширение като Tampermonkey или Userscripts.

За да инсталирате скрипта, трябва да инсталирате разширение като Tampermonkey.

За да инсталирате този скрипт, трябва да имате инсталиран скриптов мениджър.

(Вече имам скриптов мениджър, искам да го инсталирам!)

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да инсталирате разширение като Stylus.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

За да инсталирате този стил, трябва да имате инсталиран мениджър на потребителски стилове.

(Вече имам инсталиран мениджър на стиловете, искам да го инсталирам!)

// ==UserScript==
// @name            Jut.su Auto Plus Core Library
// @name:en         Jut.su Auto Plus Core Library
// @namespace       http://tampermonkey.net/
// @version         1.1.1
// @description     Ядро логики для Jut.su Auto Plus (API, обсерверы, парсинг названий, серверные функции донор/рецепиент)
// @description:en  Core logic for Jut.su Auto Plus (API, observers, title parsing, donor/recipient server functions)
// @author          Rodion
// @downloadURL     https://raw.githubusercontent.com/radik097/UserScripts/main/jutsu_plus/lib/JutsuCore.lib.js
// @updateURL       https://raw.githubusercontent.com/radik097/UserScripts/main/jutsu_plus/lib/JutsuCore.lib.js
// @match           https://jut.su/*
// @grant           GM_xmlhttpRequest
// @connect         jutsu.fun
// @connect         backup-domain.com
// @license         MIT
// ==/UserScript==

(function() {
	'use strict';
	
	// ========================================================================
	// JUT.SU AUTO PLUS CORE LIBRARY (Logic & API)
	// ========================================================================
	
	window.JutsuCore = (function() {
 
	const CONFIG = {
		baseUrl: 'https://consumet-api-yij6.onrender.com',
		retries: 2,
		timeout: 5000,
		providers: ['gogoanime', 'animekai', 'aniwatch'],
		servers: ['hd-1', 'vidstreaming', 'megacloud'],
		categories: ['sub', 'dub', 'raw']
	};

	const SERVER_CONFIG = {
		primary: 'https://jutsu.fun',
		fallback: 'https://backup-domain.com'
	};

	let currentServer = SERVER_CONFIG.primary;
 
	const logs = [];
	let debugMode = false;
 
	function setConfig(partial) {
		Object.assign(CONFIG, partial || {});
	}

	function setServerConfig(partial) {
		Object.assign(SERVER_CONFIG, partial || {});
		currentServer = SERVER_CONFIG.primary;
	}
 
	function setDebugMode(value) {
		debugMode = !!value;
	}
 
	function log(category, message, data = null) {
		const timestamp = new Date().toLocaleTimeString();
		const entry = {
			timestamp,
			category,
			message,
			data: data ? JSON.stringify(data, null, 2) : null
		};
		logs.push(entry);
		window.alisaLogs = logs;
 
		const shouldLog = debugMode || category.includes('ERROR') || category.includes('[API]');
		if (shouldLog && (category.includes('ERROR') || category.includes('[API]') || category.includes('[VIDEO]'))) {
			const style = `color: ${category.includes('ERROR') ? '#ff6b6b' : '#81a834'}; font-weight: bold;`;
			console.log(`%c[${timestamp}] ${category}%c ${message}`, style, 'color: inherit;');
			if (data && debugMode) console.log(data);
		}
	}
 
	function debug(message, details = null) {
		if (!debugMode) return;
		const timestamp = new Date().toLocaleTimeString();
		console.log(`%c[${timestamp}] [DEBUG] ${message}`, 'background: #ff9800; color: #fff; padding: 2px 6px; border-radius: 3px; font-weight: bold;');
		if (details) console.table(details);
	}
 
	function flushLogs() {
		if (!logs.length) return;
 
		const modeIndicator = debugMode ? ' DEBUG MODE' : '';
		console.group(`%cJut.su Auto+ Report — ${window.location.pathname}${modeIndicator}`, `background: ${debugMode ? '#ff9800' : '#4caf50'}; color: #fff; padding: 4px 8px; border-radius: 3px; font-weight: bold;`);
 
		logs.forEach((entry) => {
			const style = `color: ${entry.category.includes('ERROR') ? '#ff6b6b' : '#81a834'}; font-weight: bold;`;
			console.log(`%c[${entry.timestamp}] ${entry.category}%c ${entry.message}`, style, 'color: inherit; font-weight: normal;');
			if (entry.data) console.log(entry.data);
		});
 
		console.log(`%cTotal logs: ${logs.length} | Debug Mode: ${debugMode ? 'ON' : 'OFF'}`, 'color: #999; font-size: 11px;');
		console.groupEnd();
	}
 
	const observerManager = {
		observers: new Map(),
 
		create(name, callback, options = {}) {
			const defaultOptions = { childList: true, subtree: true, ...options };
			const observer = new MutationObserver(callback);
			this.observers.set(name, observer);
			log('[INIT]', `MutationObserver '${name}' created`, { options: defaultOptions });
			return observer;
		},
 
		observe(name, target = document.documentElement) {
			const observer = this.observers.get(name);
			if (!observer) {
				log('[ERROR]', `Observer '${name}' not found`);
				return null;
			}
			observer.observe(target, { childList: true, subtree: true });
			log('[INIT]', `Observer '${name}' attached to DOM`);
			return observer;
		},
 
		disconnect(name) {
			const observer = this.observers.get(name);
			if (!observer) return;
			observer.disconnect();
			this.observers.delete(name);
			log('[INIT]', `Observer '${name}' disconnected`);
		},
 
		disconnectAll() {
			this.observers.forEach((observer) => observer.disconnect());
			this.observers.clear();
			log('[INIT]', 'All observers disconnected');
		}
	};
 
	function validateAPIResponse(response) {
		if (!response || !response.responseText) {
			debug('Response validation failed: empty or missing');
			return null;
		}
 
		const text = response.responseText.trim();
		if (!text) {
			debug('Response text is empty after trim');
			return null;
		}
 
		if (response.status && (response.status >= 400 && response.status < 600)) {
			debug(`HTTP Error ${response.status}`, { responseLength: text.length });
			return null;
		}
 
		try {
			const parsed = JSON.parse(text);
			if (debugMode) {
				const resultCount = parsed.results?.length || parsed.data?.results?.length || 0;
				debug('✓ JSON parsed', { status: response.status, contentLength: text.length, resultCount: resultCount });
			}
			return parsed;
		} catch (e) {
			debug('JSON parse error', { error: e.message });
			return null;
		}
	}
 
	function gmRequestJson(url, contextLabel, retries = CONFIG.retries) {
		return new Promise((resolve) => {
			const startTime = performance.now();
			const attempt = (attemptNum) => {
				debug(`API request attempt ${attemptNum + 1}/${retries + 1}`, { url: url, context: contextLabel });
 
				GM_xmlhttpRequest({
					method: 'GET',
					url,
					timeout: CONFIG.timeout,
					headers: {
						'User-Agent': navigator.userAgent,
						'Referer': 'https://jut.su/',
						'Origin': 'https://jut.su',
						'Accept': 'application/json',
						'Accept-Language': 'en-US,en;q=0.9'
					},
					onload: (response) => {
						const duration = Math.round(performance.now() - startTime);
 
						if (debugMode) {
							console.log('%c[RAW RESPONSE]', 'background: #2196F3; color: #fff; padding: 2px 6px; border-radius: 3px; font-weight: bold;');
							console.log(`Status: ${response.status} | Duration: ${duration}ms | Length: ${response.responseText?.length || 0}`);
						}
 
						const data = validateAPIResponse(response);
						if (data) {
							debug('✅ API response parsed successfully', {
								context: contextLabel,
								status: response.status,
								duration: `${duration}ms`,
								resultCount: data.results?.length || data.data?.results?.length || 'N/A'
							});
							resolve(data);
							return;
						}
 
						if (attemptNum < retries) {
							debug(`🔄 Retrying (${attemptNum + 1}/${retries})...`, { delay: '1000ms' });
							setTimeout(() => attempt(attemptNum + 1), 1000);
						} else {
							log('[ERROR]', `API failure after ${retries + 1} attempts: ${contextLabel}`);
							debug('❌ All API retries exhausted', { context: contextLabel, totalDuration: `${duration}ms` });
							resolve(null);
						}
					},
					onerror: (error) => {
						const duration = Math.round(performance.now() - startTime);
						const errorMsg = error?.error?.message || error?.message || 'Unknown network error';
 
						if (attemptNum < retries) {
							debug(`⚠️ Request error, retrying (${attemptNum + 1}/${retries})...`, { error: errorMsg, delay: '1500ms' });
							setTimeout(() => attempt(attemptNum + 1), 1500);
						} else {
							log('[ERROR]', `Network error after retries: ${errorMsg}`, { context: contextLabel });
							debug('❌ Network error failed all retries', { error: errorMsg, totalDuration: `${duration}ms` });
							resolve(null);
						}
					},
					ontimeout: () => {
						const duration = Math.round(performance.now() - startTime);
						if (attemptNum < retries) {
							debug(`⏱️ Timeout, retrying (${attemptNum + 1}/${retries})...`, { duration: `${duration}ms` });
							setTimeout(() => attempt(attemptNum + 1), 2000);
						} else {
							log('[ERROR]', `Request timeout after ${retries + 1} attempts: ${contextLabel}`);
							debug('❌ Timeout on all retry attempts', { context: contextLabel, totalDuration: `${duration}ms` });
							resolve(null);
						}
					}
				});
			};
 
			attempt(0);
		});
	}

	function gmRequestServerJson(url, options = {}) {
		const { method = 'GET', data = null, contextLabel = 'server.request' } = options;

		return new Promise((resolve) => {
			const startTime = performance.now();

			if (debugMode) {
				debug(`🔄 Server request: ${method} ${url.replace(/^https?:\/\//, '')}`, {
					context: contextLabel,
					dataSize: data ? JSON.stringify(data).length : 0
				});
			}

			GM_xmlhttpRequest({
				method: method,
				url: url,
				timeout: CONFIG.timeout,
				headers: {
					'Content-Type': 'application/json',
					'Accept': 'application/json'
				},
				data: data ? JSON.stringify(data) : null,
				onload: (response) => {
					const duration = Math.round(performance.now() - startTime);
					const parsed = validateAPIResponse(response);
					if (parsed) {
						if (debugMode) {
							debug('✅ Server response parsed', {
								context: contextLabel,
								status: response.status,
								duration: `${duration}ms`,
								responseSize: response.responseText?.length || 0
							});
						}
						resolve({ ok: true, data: parsed });
						return;
					}

					if (debugMode) {
						debug('❌ Server response invalid', {
							context: contextLabel,
							status: response.status,
							duration: `${duration}ms`,
							responseStart: response.responseText?.substring(0, 100)
						});
					}
					resolve({ ok: false, error: `Invalid response (${response.status})`, status: response.status });
				},
				onerror: (error) => {
					const duration = Math.round(performance.now() - startTime);
					const errorMsg = error?.error?.message || error?.message || 'Unknown network error';
					debug('❌ Network error', {
						context: contextLabel,
						error: errorMsg,
						duration: `${duration}ms`
					});
					resolve({ ok: false, error: errorMsg, network: true });
				},
				ontimeout: () => {
					const duration = Math.round(performance.now() - startTime);
					debug('⏱️ Request timeout', {
						context: contextLabel,
						duration: `${duration}ms`
					});
					resolve({ ok: false, error: 'Request timeout', timeout: true });
				}
			});
		});
	}

	async function requestServerWithFallback(path, options) {
		const primaryUrl = `${currentServer}${path}`;
		
		if (debugMode) {
			debug(`📡 Trying primary server: ${SERVER_CONFIG.primary}`, {
				path: path.substring(0, 50)
			});
		}
		
		let response = await gmRequestServerJson(primaryUrl, options);

		if (response.ok) {
			if (debugMode) {
				debug('✅ Primary server succeeded');
			}
			return response;
		}

		if (currentServer === SERVER_CONFIG.primary && SERVER_CONFIG.fallback) {
			currentServer = SERVER_CONFIG.fallback;
			const fallbackUrl = `${currentServer}${path}`;
			
			if (debugMode) {
				debug(`📡 Trying fallback server: ${SERVER_CONFIG.fallback}`, {
					reason: response?.error,
					path: path.substring(0, 50)
				});
			}
			
			response = await gmRequestServerJson(fallbackUrl, options);
			if (response.ok) {
				if (debugMode) {
					debug('✅ Fallback server succeeded');
				}
				return response;
			}
			
			if (debugMode) {
				debug('❌ Both primary and fallback servers failed', {
					primaryError: response?.error,
					fallbackError: response?.error
				});
			}
		}

		return response;
	}

	async function contributeAnime(payload) {
		if (debugMode) {
			debug('🎬 Contribute anime: preparing to send donor link', {
				anime: payload?.animeId,
				episode: payload?.episode,
				quality: payload?.quality,
				urlLength: payload?.url?.length || 0
			});
		}
		
		const result = await requestServerWithFallback('/contribute', {
			method: 'POST',
			data: payload,
			contextLabel: 'server.contribute'
		});
		
		if (debugMode) {
			debug('🎬 Contribute anime: response received', {
				ok: result?.ok,
				error: result?.error,
				hasData: !!result?.data
			});
		}
		
		return result;
	}

	// ========================================================================
	// GITHUB SYNC DATABASE SYSTEM
	// ========================================================================

	const GitHubQueue = {
		queue: Promise.resolve(),
		async add(task) {
			this.queue = this.queue.then(async () => {
				try {
					await task();
				} catch (err) {
					log('[ERROR]', 'GitHub Queue task failed', { error: err.message });
				}
			});
			return this.queue;
		}
	};

	function isPlusUser() {
		// Detection of jut.su+ subscription
		const hasPlusClass = !!document.querySelector('.player_plus_active');
		const hasPlusPlayer = !!document.querySelector('#my-player.vjs-plus-player');
		const hasPlusVar = typeof window.is_plus !== 'undefined' ? !!window.is_plus : false;
		
		const result = hasPlusClass || hasPlusPlayer || hasPlusVar;
		if (debugMode) debug('Subscription check', { isPlus: result, hasPlusClass, hasPlusPlayer, hasPlusVar });
		return result;
	}

	async function githubFetch(repo, path) {
		if (!repo || !path) return null;
		
		const url = `https://raw.githubusercontent.com/${repo}/main/${path}?t=${Date.now()}`;
		try {
			const res = await fetch(url);
			if (!res.ok) {
				if (res.status !== 404) log('[ERROR]', `GitHub Fetch failed: ${res.status}`);
				return null;
			}
			return await res.json();
		} catch (e) {
			log('[ERROR]', 'GitHub Fetch exception', { error: e.message });
			return null;
		}
	}

	async function githubUpdate(repo, path, token, updateFn) {
		if (!repo || !path || !token || typeof updateFn !== 'function') {
			log('[ERROR]', 'githubUpdate: Missing parameters');
			return;
		}

		return GitHubQueue.add(async () => {
			const [owner, repoName] = repo.split('/');
			if (!owner || !repoName) {
				log('[ERROR]', 'githubUpdate: Invalid repo format (expected owner/repo)');
				return;
			}

			let octokit;
			try {
				// Use Octokit if available (from @require)
				const OctokitRef = window.Octokit || (typeof Octokit !== 'undefined' ? Octokit : null);
				if (OctokitRef) {
					octokit = new OctokitRef({ auth: token });
					debug('GitHub Sync: Using Octokit Core');
				}
			} catch (e) {
				debug('GitHub Sync: Octokit initialization failed, falling back to manual');
			}

			let sha = null;
			let currentData = {};

			try {
				if (octokit) {
					// 1. Get SHA and current data via Octokit
					const res = await octokit.request('GET /repos/{owner}/{repo}/contents/{path}', {
						owner,
						repo: repoName,
						path,
						headers: { 'cache-control': 'no-cache' }
					});

					if (res.status === 200) {
						sha = res.data.sha;
						const decoded = decodeURIComponent(escape(atob(res.data.content.replace(/\s/g, ''))));
						currentData = JSON.parse(decoded);
					}
				} else {
					// Manual Fallback (GM_xmlhttpRequest)
					const res = await new Promise(resolve => {
						GM_xmlhttpRequest({
							method: 'GET',
							url: `https://api.github.com/repos/${repo}/contents/${path}`,
							headers: { 'Authorization': `token ${token}`, 'Accept': 'application/vnd.github.v3+json' },
							onload: resolve,
							onerror: (e) => resolve({ status: 500, statusText: e.message })
						});
					});

					if (res.status === 200) {
						const data = JSON.parse(res.responseText);
						sha = data.sha;
						const decoded = decodeURIComponent(escape(atob(data.content.replace(/\s/g, ''))));
						currentData = JSON.parse(decoded);
					} else if (res.status !== 404) {
						throw new Error(`GitHub API error ${res.status}`);
					}
				}
			} catch (e) {
				if (e.status !== 404) {
					log('[ERROR]', 'GitHub Fetch failed during sync', { error: e.message });
					return; // Stop if it's a real error (not 404)
				}
				debug('GitHub file not found, will create new one');
			}

			// 2. Prepare new data
			const newData = updateFn(currentData);
			if (!newData) {
				debug('GitHub sync: No changes to push');
				return;
			}

			const jsonStr = JSON.stringify(newData, null, 2);
			const base64 = btoa(unescape(encodeURIComponent(jsonStr)));

			// 3. Push to GitHub
			try {
				if (octokit) {
					await octokit.request('PUT /repos/{owner}/{repo}/contents/{path}', {
						owner,
						repo: repoName,
						path,
						message: `Sync Links DB: ${new Date().toISOString()}`,
						content: base64,
						sha: sha
					});
				} else {
					const putRes = await new Promise(resolve => {
						GM_xmlhttpRequest({
							method: 'PUT',
							url: `https://api.github.com/repos/${repo}/contents/${path}`,
							headers: {
								'Authorization': `token ${token}`,
								'Accept': 'application/vnd.github.v3+json',
								'Content-Type': 'application/json'
							},
							data: JSON.stringify({
								message: `Sync Links DB: ${new Date().toISOString()}`,
								content: base64,
								sha: sha
							}),
							onload: resolve,
							onerror: (e) => resolve({ status: 500, statusText: e.message })
						});
					});

					if (putRes.status >= 400) throw new Error(`Status ${putRes.status}`);
				}
				debug('✅ GitHub Links DB successfully synced');
			} catch (err) {
				log('[ERROR]', 'GitHub Push failed', { error: err.message });
			}
		});
	}

	async function getDonorLinks(payload) {
		if (debugMode) {
			debug('📥 Get donor links: requesting from server', {
				anime: payload?.animeId,
				episode: payload?.episode
			});
		}
		
		const query = new URLSearchParams({
			anime: payload?.animeId || '',
			ep: payload?.episode || ''
		}).toString();
		const path = `/links?${query}`;

		const result = await requestServerWithFallback(path, {
			method: 'GET',
			data: null,
			contextLabel: 'server.links'
		});
		
		if (debugMode) {
			debug('📥 Get donor links: response received', {
				ok: result?.ok,
				error: result?.error,
				linksCount: result?.data?.data?.links?.length || 0
			});
		}
		
		return result;
	}
 
	function getEpisodeInfo() {
		const pathMatch = window.location.pathname.match(/season-(\d+).*episode-(\d+)/);
		if (pathMatch) {
			return { season: pathMatch[1], episode: pathMatch[2] };
		}
		const epMatch = window.location.pathname.match(/episode-(\d+)/);
		return { season: null, episode: epMatch ? epMatch[1] : null };
	}
 
	function buildTitleVariants(rawTitle, episode) {
		const variants = [];
		const seen = new Set();
		const hasCyrillic = (value) => /[\u0400-\u04FF]/.test(value || '');
		const translitMap = {
			'а': 'a', 'б': 'b', 'в': 'v', 'г': 'g', 'д': 'd', 'е': 'e', 'ё': 'yo',
			'ж': 'zh', 'з': 'z', 'и': 'i', 'й': 'y', 'к': 'k', 'л': 'l', 'м': 'm',
			'н': 'n', 'о': 'o', 'п': 'p', 'р': 'r', 'с': 's', 'т': 't', 'у': 'u',
			'ф': 'f', 'х': 'kh', 'ц': 'ts', 'ч': 'ch', 'ш': 'sh', 'щ': 'shch',
			'ъ': '', 'ы': 'y', 'ь': '', 'э': 'e', 'ю': 'yu', 'я': 'ya'
		};
		const translit = (value) => (value || '')
			.toLowerCase()
			.split('')
			.map((char) => translitMap[char] ?? char)
			.join('')
			.replace(/\s+/g, ' ')
			.trim();
		const add = (value) => {
			const normalized = (value || '').replace(/\s+/g, ' ').trim();
			if (!normalized || seen.has(normalized)) return;
			seen.add(normalized);
			variants.push(normalized);
		};
 
		const slug = (window.location.pathname.split('/').filter(Boolean)[0] || '')
			.replace(/[-_]/g, ' ')
			.replace(/\d+/g, '')
			.trim();
		add(slug);
 
		add(rawTitle);
 
		let cleaned = (rawTitle || '')
			.replace(/^\s*смотреть\s+/iu, '')
			.replace(/\s+на\s+jut\.su\s*$/iu, '')
			.replace(/\s*\(jut\.su\)\s*$/iu, '')
			.replace(/\s+(на\s+)?русском\s*$/iu, '')
			.replace(/\s*-\s*anime\s*$/iu, '')
			.replace(/\s*\[.*?\]\s*/g, '')
			.replace(/\s*\(.*?\)\s*/g, '')
			.trim();
 
		cleaned = cleaned
			.replace(/\s+(\d+)\s+(серия|серии|серий|епизод|episode|episode\s*\d+)\s*$/iu, '')
			.replace(/\s+(season\s+\d+\s+)?episode\s+\d+\s*$/iu, '')
			.replace(/\s+part\s+\d+\s*$/iu, '')
			.trim();
 
		add(cleaned);
 
		if (episode) {
			const noEp = (rawTitle || '')
				.replace(new RegExp(`\\b${episode}\\b\\s*(серия|серии|серий|епизод|episode)?`, 'iu'), '')
				.replace(/^\s*смотреть\s+/iu, '')
				.trim();
			add(noEp);
		}
 
		const words = rawTitle ? rawTitle.split(/\s+/) : [];
		if (words.length > 2) {
			add(words.slice(0, Math.min(3, words.length)).join(' ').replace(/^смотреть\s+/iu, '').trim());
		}
 
		if (hasCyrillic(cleaned)) add(translit(cleaned));
		if (hasCyrillic(rawTitle)) add(translit(rawTitle));
 
		const filtered = variants.filter((value) => value && !hasCyrillic(value));
		debug('Title variants generated', { original: rawTitle, variants: filtered, count: filtered.length });
		return filtered;
	}
 
	function pickEpisode(episodes, episodeNumber) {
		if (!Array.isArray(episodes) || !episodes.length) return null;
		if (episodeNumber) {
			const byNumber = episodes.find((ep) => String(ep.number ?? ep.episode ?? ep.episodeNumber) === String(episodeNumber));
			return byNumber || episodes[0];
		}
		return episodes[0];
	}
 
	function buildUrlMapFromSources(sources) {
		const urls = {};
		(sources || []).forEach((source) => {
			const quality = source.quality || (source.isM3U8 ? 'hls' : 'default');
			if (source.url && !urls[quality]) {
				urls[quality] = source.url;
			}
		});
		if (!urls.default && sources?.[0]?.url) {
			urls.default = sources[0].url;
		}
		return urls;
	}
 
	async function fetchOriginalTitle() {
		try {
			debug('Attempting to fetch title from page');
			const titleElement = document.querySelector('h1.post_title, h1, [data-test="title"]');
			if (titleElement) {
				const title = titleElement.textContent.trim();
				debug('Title found via element selector', {
					selector: 'h1.post_title, h1, [data-test="title"]',
					title: title
				});
				return title;
			}
			const fallbackTitle = document.title.split('—')[0].trim();
			debug('Title extracted from document.title', { title: fallbackTitle });
			return fallbackTitle;
		} catch (e) {
			log('[ERROR]', 'Failed to fetch original title', { error: e.message });
			debug('Exception while fetching title', {
				error: e.message,
				stack: e.stack?.substring(0, 200)
			});
			return null;
		}
	}
 
	async function fetchConsumetResults(title, episode) {
		try {
			const results = [];
 
			for (const provider of CONFIG.providers) {
				const searchUrl = `${CONFIG.baseUrl}/anime/${provider}/${encodeURIComponent(title)}?page=1`;
				const searchData = await gmRequestJson(searchUrl, `consumet.${provider}.search`);
				const searchResults = searchData?.results || [];
				if (!searchResults.length) continue;
 
				const limitedResults = searchResults.slice(0, 3);
 
				for (const item of limitedResults) {
					try {
						const infoUrl = `${CONFIG.baseUrl}/anime/${provider}/info/${encodeURIComponent(item.id)}`;
						const infoData = await gmRequestJson(infoUrl, `consumet.${provider}.info`);
						let episodes = infoData?.episodes || [];
						if (!episodes.length) {
							const epsUrl = `${CONFIG.baseUrl}/anime/${provider}/episodes/${encodeURIComponent(item.id)}`;
							const epsData = await gmRequestJson(epsUrl, `consumet.${provider}.episodes`);
							episodes = epsData?.episodes || epsData || [];
						}
 
						const episodeItem = pickEpisode(episodes, episode);
						const episodeId = episodeItem?.id || episodeItem?.episodeId;
						if (!episodeId) {
							debug('Consumet episodeId missing', { provider, title: item.title });
							continue;
						}
 
						let sources = [];
						for (const category of CONFIG.categories) {
							for (const server of CONFIG.servers) {
								const watchUrl = `${CONFIG.baseUrl}/anime/${provider}/watch/${encodeURIComponent(episodeId)}?server=${encodeURIComponent(server)}&category=${encodeURIComponent(category)}`;
								const watchData = await gmRequestJson(watchUrl, `consumet.${provider}.watch`);
								sources = watchData?.sources || [];
								if (sources.length) break;
							}
							if (sources.length) break;
						}
 
						if (!sources.length) continue;
 
						const urls = buildUrlMapFromSources(sources);
						results.push({
							id: item.id,
							title: item.title,
							provider: provider,
							type: 'stream',
							link: urls.default,
							urls: urls,
							quality: sources[0]?.quality || 'auto'
						});
					} catch (itemError) {
						debug('Consumet item processing error', { title: item.title, error: itemError.message });
						continue;
					}
				}
 
				if (results.length) break;
			}
 
			return results;
		} catch (err) {
			debug('Consumet provider error', { error: err.message });
			return [];
		}
	}
 
	window.alisaLogs = logs;
 
	return {
		setConfig,
		setServerConfig,
		setDebugMode,
		log,
		debug,
		flushLogs,
		observerManager,
		gmRequestJson,
		contributeAnime,
		getDonorLinks,
		getEpisodeInfo,
		buildTitleVariants,
		pickEpisode,
		buildUrlMapFromSources,
		fetchOriginalTitle,
		fetchConsumetResults,
		isPlusUser,
		githubFetch,
		githubUpdate
	};
	})();
	
})();