diff --git a/desktop/src/api/anime/anime.controller.ts b/desktop/src/api/anime/anime.controller.ts index bfebb42..6f6c4cf 100644 --- a/desktop/src/api/anime/anime.controller.ts +++ b/desktop/src/api/anime/anime.controller.ts @@ -96,7 +96,7 @@ export async function searchInExtension(req: any, reply: FastifyReply) { export async function getWatchStream(req: WatchStreamRequest, reply: FastifyReply) { try { - const { animeId, episode, server, category, ext, source } = req.query; + const { animeId, episode, server, category, ext, source, extensionAnimeId } = req.query; const extension = getExtension(ext); if (!extension) return { error: "Extension not found" }; @@ -107,7 +107,8 @@ export async function getWatchStream(req: WatchStreamRequest, reply: FastifyRepl animeId, source, server, - category + category, + extensionAnimeId ); } catch (err) { const error = err as Error; diff --git a/desktop/src/api/anime/anime.service.ts b/desktop/src/api/anime/anime.service.ts index ce1836c..b51432d 100644 --- a/desktop/src/api/anime/anime.service.ts +++ b/desktop/src/api/anime/anime.service.ts @@ -276,7 +276,6 @@ export async function getAnimeInfoExtension(ext: Extension | null, id: string): if (!ext) return { error: "not found" }; const extName = ext.constructor.name; - const cached = await getCachedExtension(extName, id); if (cached) { try { @@ -341,6 +340,7 @@ export async function searchAnimeInExtension(ext: Extension | null, name: string averageScore: m.rating || m.score || null, format: 'ANIME', seasonYear: null, + url: m.url, isExtensionResult: true, })); } @@ -387,22 +387,41 @@ export async function searchEpisodesInExtension(ext: Extension | null, name: str startDate: { year: 0, month: 0, day: 0 } } }); - if (!matches || matches.length === 0) return []; - const res = matches[0]; + const normalizedQuery = normalize(query); + const scored = matches.map(match => { + const normalizedTitle = normalize(match.title); + const score = similarity(normalizedQuery, normalizedTitle); + + let bonus = 0; + if (normalizedTitle === normalizedQuery) { + bonus = 0.5; + } else if (normalizedTitle.toLowerCase().includes(normalizedQuery.toLowerCase())) { + bonus = 0.5; + } + + const finalScore = score + bonus; + + return { + match, + score: finalScore + }; + }); + + scored.sort((a, b) => b.score - a.score); + const bestMatches = scored.filter(s => s.score > 0.4); + + if (bestMatches.length === 0) return []; + const res = bestMatches[0].match; if (!res?.id) return []; - mediaId = res.id; - } else { mediaId = query; } const chapterList = await ext.findEpisodes(mediaId); - if (!Array.isArray(chapterList)) return []; - const result: Episode[] = chapterList.map(ep => ({ id: ep.id, number: ep.number, @@ -421,25 +440,24 @@ export async function searchEpisodesInExtension(ext: Extension | null, name: str return []; } -export async function getStreamData(extension: Extension, episode: string, id: string, source: string, server?: string, category?: string): Promise { +export async function getStreamData(extension: Extension, episode: string, id: string, source: string, server?: string, category?: string, extensionAnimeId?: string): Promise { const providerName = extension.constructor.name; const cacheKey = `anime:stream:${providerName}:${id}:${episode}:${server || 'default'}:${category || 'sub'}`; + if (!extensionAnimeId) { + const cached = await getCache(cacheKey); - const cached = await getCache(cacheKey); + if (cached) { + const isExpired = Date.now() - cached.created_at > CACHE_TTL_MS; - if (cached) { - const isExpired = Date.now() - cached.created_at > CACHE_TTL_MS; - - if (!isExpired) { - console.log(`[${providerName}] Stream data cache hit for episode ${episode}`); - try { - return JSON.parse(cached.result) as StreamData; - } catch (e) { - console.error(`[${providerName}] Error parsing cached stream data:`, e); + if (!isExpired) { + console.log(`[${providerName}] Stream data cache hit for episode ${episode}`); + try { + return JSON.parse(cached.result) as StreamData; + } catch (e) { + console.error(`[${providerName}] Error parsing cached stream data:`, e); + } } - } else { - console.log(`[${providerName}] Stream data cache expired for episode ${episode}`); } } @@ -448,13 +466,13 @@ export async function getStreamData(extension: Extension, episode: string, id: s } let episodes; - if (source === "anilist"){ - const anime: any = await getAnimeById(id) + if (source === "anilist" && !extensionAnimeId) { + const anime: any = await getAnimeById(id); episodes = await searchEpisodesInExtension(extension, extension.constructor.name, anime.title.romaji); + } else { + episodes = await extension.findEpisodes(extensionAnimeId ?? id); } - else{ - episodes = await extension.findEpisodes(id); - } + const targetEp = episodes.find(e => e.number === parseInt(episode)); if (!targetEp) { @@ -466,4 +484,47 @@ export async function getStreamData(extension: Extension, episode: string, id: s await setCache(cacheKey, streamData, CACHE_TTL_MS); return streamData; +} + +function similarity(s1: string, s2: string): number { + const str1 = normalize(s1); + const str2 = normalize(s2); + + const longer = str1.length > str2.length ? str1 : str2; + const shorter = str1.length > str2.length ? str2 : str1; + + if (longer.length === 0) return 1.0; + + const editDistance = levenshteinDistance(longer, shorter); + return (longer.length - editDistance) / longer.length; +} + +function levenshteinDistance(s1: string, s2: string): number { + const costs: number[] = []; + for (let i = 0; i <= s1.length; i++) { + let lastValue = i; + for (let j = 0; j <= s2.length; j++) { + if (i === 0) { + costs[j] = j; + } else if (j > 0) { + let newValue = costs[j - 1]; + if (s1.charAt(i - 1) !== s2.charAt(j - 1)) { + newValue = Math.min(Math.min(newValue, lastValue), costs[j]) + 1; + } + costs[j - 1] = lastValue; + lastValue = newValue; + } + } + if (i > 0) costs[s2.length] = lastValue; + } + return costs[s2.length]; +} + +function normalize(str: string): string { + return str + .toLowerCase() + .replace(/'/g, "'") // decodificar entidades HTML + .replace(/[^\w\s]/g, ' ') // convertir puntuación a espacios + .replace(/\s+/g, ' ') // normalizar espacios + .trim(); } \ No newline at end of file diff --git a/desktop/src/api/types.ts b/desktop/src/api/types.ts index d369c19..7874a80 100644 --- a/desktop/src/api/types.ts +++ b/desktop/src/api/types.ts @@ -62,6 +62,7 @@ export interface ExtensionSearchOptions { } export interface ExtensionSearchResult { + url: string; format: string; headers: any; id: string; @@ -158,6 +159,7 @@ export interface WatchStreamQuery { server?: string; category?: string; ext: string; + extensionAnimeId?: string; } export interface BookParams { diff --git a/desktop/src/scripts/anime/player.js b/desktop/src/scripts/anime/player.js index a7b61ac..b4ee5bb 100644 --- a/desktop/src/scripts/anime/player.js +++ b/desktop/src/scripts/anime/player.js @@ -22,6 +22,9 @@ const AnimePlayer = (function() { let plyrInstance = null; let hlsInstance = null; + let _manualExtensionId = null; + let _searchTimeout = null; + const els = { wrapper: null, playerWrapper: null, @@ -41,7 +44,12 @@ const AnimePlayer = (function() { dlAudioList: null, dlSubsList: null, dlConfirmBtn: null, - dlCancelBtn: null + dlCancelBtn: null, + manualMatchBtn: null, + matchModal: null, + matchInput: null, + matchList: null, + closeMatchModalBtn: null }; function init(animeId, initialSource, isLocal, animeData) { @@ -74,6 +82,32 @@ const AnimePlayer = (function() { els.dlSubsList = document.getElementById('dl-subs-list'); els.dlConfirmBtn = document.getElementById('confirm-dl-btn'); els.dlCancelBtn = document.getElementById('cancel-dl-btn'); + els.manualMatchBtn = document.getElementById('manual-match-btn'); + els.matchModal = document.getElementById('match-modal'); + els.matchInput = document.getElementById('match-search-input'); + els.matchList = document.getElementById('match-results-list'); + els.closeMatchModalBtn = document.getElementById('close-match-modal'); + + // Event Listeners para Manual Match + if (els.manualMatchBtn) els.manualMatchBtn.addEventListener('click', openMatchModal); + if (els.closeMatchModalBtn) els.closeMatchModalBtn.addEventListener('click', closeMatchModal); + + // Cerrar modal al hacer click fuera + if (els.matchModal) { + els.matchModal.addEventListener('click', (e) => { + if (e.target === els.matchModal) closeMatchModal(); + }); + } + + // Input de búsqueda con Debounce + if (els.matchInput) { + els.matchInput.addEventListener('input', (e) => { + clearTimeout(_searchTimeout); + _searchTimeout = setTimeout(() => { + executeMatchSearch(e.target.value); + }, 500); // Esperar 500ms tras dejar de escribir + }); + } const closeDlModalBtn = document.getElementById('close-download-modal'); @@ -138,6 +172,108 @@ const AnimePlayer = (function() { loadExtensionsList(); } + function openMatchModal() { + if (!els.matchModal) return; + + // Limpiar contenido previo + els.matchInput.value = ''; + els.matchList.innerHTML = `
Type to search in ${els.extSelect.value}...
`; + + // 1. Mostrar el contenedor (para que el navegador calcule el layout) + els.matchModal.style.display = 'flex'; + + // 2. Pequeño delay o forzar reflow para que la transición de opacidad funcione + requestAnimationFrame(() => { + els.matchModal.classList.add('show'); + }); + + setTimeout(() => els.matchInput.focus(), 100); + } + + function closeMatchModal() { + if (!els.matchModal) return; + els.matchModal.classList.remove('show'); + setTimeout(() => { + if (!els.matchModal.classList.contains('show')) { + els.matchModal.style.display = 'none'; + } + }, 300); + } + + async function executeMatchSearch(query) { + if (!query || query.trim().length < 2) return; + + const ext = els.extSelect.value; + if (!ext || ext === 'local') return; + + els.matchList.innerHTML = '
'; + + try { + const res = await fetch(`/api/search/${ext}?q=${encodeURIComponent(query)}`); + const data = await res.json(); + + renderMatchResults(data.results || []); + } catch (e) { + console.error("Match Search Error:", e); + els.matchList.innerHTML = '

Error searching extension.

'; + } + } + + function renderMatchResults(results) { + els.matchList.innerHTML = ''; + + if (results.length === 0) { + els.matchList.innerHTML = '

No results found.

'; + return; + } + + results.forEach(item => { + const div = document.createElement('div'); + div.className = 'match-item dl-item'; + + const img = (item.coverImage && item.coverImage.large) ? item.coverImage.large : "/public/assets/placeholder.svg"; + const title = item.title.english || item.title.romaji || item.title || 'Unknown'; + const externalUrl = item.url || '#'; // El parámetro URL del JSON + + div.innerHTML = ` + cover +
+ ${title} + ${item.releaseDate || item.year || ''} +
+ ${item.url ? ` + + + + + + + + ` : ''} + `; + + div.onclick = (e) => { + if (e.target.closest('.btn-view-source')) return; + selectManualMatch(item); + }; + + els.matchList.appendChild(div); + }); + } + + function selectManualMatch(item) { + // 1. Guardar el ID de la extensión + _manualExtensionId = item.id; + + console.log("Manual Match Selected:", _manualExtensionId, "for extension:", els.extSelect.value); + + // 2. Cerrar modal + closeMatchModal(); + + // 3. Recargar el stream con el nuevo ID + loadStream(); + } + async function openInMPV() { if (!_rawVideoData) { alert("No video loaded yet."); @@ -579,6 +715,11 @@ const AnimePlayer = (function() { if (shouldPlay && _currentEpisode > 0) loadStream(); return; } + if (els.manualMatchBtn) { + // No mostrar en local, sí en extensiones + els.manualMatchBtn.style.display = (selectedExt === 'local') ? 'none' : 'flex'; + } + _manualExtensionId = null; setLoading("Loading Extension Settings..."); try { @@ -690,14 +831,19 @@ const AnimePlayer = (function() { const extParam = `&ext=${currentExt}`; const realSource = _entrySource === 'local' ? 'anilist' : _entrySource; - const url = - `/api/watch/stream?animeId=${_animeId}` + + // AQUÍ AGREGAMOS EL PARÁMETRO OPCIONAL extensionAnimeId + let url = `/api/watch/stream?animeId=${_animeId}` + `&episode=${_currentEpisode}` + `&server=${encodeURIComponent(server)}` + `&category=${_audioMode}` + `${extParam}` + `&source=${realSource}`; + // INYECCIÓN DEL ID MANUAL + if (_manualExtensionId) { + url += `&extensionAnimeId=${encodeURIComponent(_manualExtensionId)}`; + } + try { const res = await fetch(url); const data = await res.json(); diff --git a/desktop/views/anime/anime.html b/desktop/views/anime/anime.html index 79a8209..1fb941e 100644 --- a/desktop/views/anime/anime.html +++ b/desktop/views/anime/anime.html @@ -87,6 +87,12 @@ MPV +
Sub
@@ -97,6 +103,20 @@
+
diff --git a/desktop/views/css/anime/player.css b/desktop/views/css/anime/player.css index d25c571..b599430 100644 --- a/desktop/views/css/anime/player.css +++ b/desktop/views/css/anime/player.css @@ -709,7 +709,7 @@ body.stop-scrolling { MODAL DE DESCARGAS - REDISEÑO "GLASS" ========================================= */ -#download-modal { +#download-modal, #match-modal { position: fixed !important; top: 0; left: 0; @@ -727,7 +727,7 @@ body.stop-scrolling { } /* Estado visible activado por JS */ -#download-modal.show { +#download-modal.show, #match-modal.show { display: flex !important; opacity: 1 !important; pointer-events: auto !important; @@ -752,7 +752,8 @@ body.stop-scrolling { overflow: hidden; } -#download-modal.show .download-settings-content { +#download-modal.show .download-settings-content, +#match-modal.show .download-settings-content { transform: scale(1); } @@ -916,4 +917,96 @@ body.stop-scrolling { background: #7c3aed; /* Un tono más oscuro del brand */ transform: translateY(-2px); box-shadow: 0 6px 20px rgba(139, 92, 246, 0.4); +} + +.glass-input { + width: 100%; + background: rgba(255, 255, 255, 0.1); + border: 1px solid rgba(255, 255, 255, 0.2); + color: white; + padding: 12px 16px; + border-radius: 8px; + font-size: 1rem; + outline: none; + transition: all 0.2s ease; +} + +.glass-input:focus { + background: rgba(255, 255, 255, 0.15); + border-color: var(--brand-color); + box-shadow: 0 0 0 2px rgba(139, 92, 246, 0.2); +} + +/* Reutilización para los resultados de búsqueda */ +.match-item { + display: flex; + align-items: center; + gap: 12px; + background: rgba(255, 255, 255, 0.05); + padding: 10px; + border-radius: 6px; + cursor: pointer; + transition: background 0.2s; +} + +.match-item:hover { + background: rgba(255, 255, 255, 0.15); +} + +.match-item img { + width: 40px; + height: 56px; + object-fit: cover; + border-radius: 4px; + background: #222; +} + +.match-title { + font-weight: 600; + font-size: 0.95rem; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + color: #fff; +} + +.match-meta { + font-size: 0.8rem; + color: #aaa; +} + +/* Estado activo (seleccionado actualmente si quisieras marcarlo) */ +.match-item.active { + border: 1px solid var(--brand-color); + background: rgba(139, 92, 246, 0.1); +} + +.btn-view-source { + display: flex; + align-items: center; + justify-content: center; + padding: 8px; + background: rgba(255, 255, 255, 0.1); + border-radius: 6px; + color: rgba(255, 255, 255, 0.7); + transition: all 0.2s ease; + border: 1px solid rgba(255, 255, 255, 0.1); + text-decoration: none; + margin-left: auto; /* Empuja el botón a la derecha */ +} + +.btn-view-source:hover { + background: var(--brand-color); + color: white; + border-color: var(--brand-color-light); + transform: scale(1.1); +} + +/* Ajuste para que el texto no choque con el botón */ +.match-info { + flex: 1; + margin-right: 10px; + display: flex; + flex-direction: column; + overflow: hidden; } \ No newline at end of file diff --git a/docker/src/api/anime/anime.controller.ts b/docker/src/api/anime/anime.controller.ts index b6bd693..76ab45b 100644 --- a/docker/src/api/anime/anime.controller.ts +++ b/docker/src/api/anime/anime.controller.ts @@ -87,7 +87,7 @@ export async function searchInExtension(req: any, reply: FastifyReply) { export async function getWatchStream(req: WatchStreamRequest, reply: FastifyReply) { try { - const { animeId, episode, server, category, ext, source } = req.query; + const { animeId, episode, server, category, ext, source, extensionAnimeId } = req.query; const extension = getExtension(ext); if (!extension) return { error: "Extension not found" }; @@ -98,7 +98,8 @@ export async function getWatchStream(req: WatchStreamRequest, reply: FastifyRepl animeId, source, server, - category + category, + extensionAnimeId ); } catch (err) { const error = err as Error; diff --git a/docker/src/api/anime/anime.service.ts b/docker/src/api/anime/anime.service.ts index ff02457..b51432d 100644 --- a/docker/src/api/anime/anime.service.ts +++ b/docker/src/api/anime/anime.service.ts @@ -340,6 +340,7 @@ export async function searchAnimeInExtension(ext: Extension | null, name: string averageScore: m.rating || m.score || null, format: 'ANIME', seasonYear: null, + url: m.url, isExtensionResult: true, })); } @@ -439,25 +440,24 @@ export async function searchEpisodesInExtension(ext: Extension | null, name: str return []; } -export async function getStreamData(extension: Extension, episode: string, id: string, source: string, server?: string, category?: string): Promise { +export async function getStreamData(extension: Extension, episode: string, id: string, source: string, server?: string, category?: string, extensionAnimeId?: string): Promise { const providerName = extension.constructor.name; const cacheKey = `anime:stream:${providerName}:${id}:${episode}:${server || 'default'}:${category || 'sub'}`; + if (!extensionAnimeId) { + const cached = await getCache(cacheKey); - const cached = await getCache(cacheKey); + if (cached) { + const isExpired = Date.now() - cached.created_at > CACHE_TTL_MS; - if (cached) { - const isExpired = Date.now() - cached.created_at > CACHE_TTL_MS; - - if (!isExpired) { - console.log(`[${providerName}] Stream data cache hit for episode ${episode}`); - try { - return JSON.parse(cached.result) as StreamData; - } catch (e) { - console.error(`[${providerName}] Error parsing cached stream data:`, e); + if (!isExpired) { + console.log(`[${providerName}] Stream data cache hit for episode ${episode}`); + try { + return JSON.parse(cached.result) as StreamData; + } catch (e) { + console.error(`[${providerName}] Error parsing cached stream data:`, e); + } } - } else { - console.log(`[${providerName}] Stream data cache expired for episode ${episode}`); } } @@ -466,13 +466,13 @@ export async function getStreamData(extension: Extension, episode: string, id: s } let episodes; - if (source === "anilist"){ - const anime: any = await getAnimeById(id) + if (source === "anilist" && !extensionAnimeId) { + const anime: any = await getAnimeById(id); episodes = await searchEpisodesInExtension(extension, extension.constructor.name, anime.title.romaji); + } else { + episodes = await extension.findEpisodes(extensionAnimeId ?? id); } - else{ - episodes = await extension.findEpisodes(id); - } + const targetEp = episodes.find(e => e.number === parseInt(episode)); if (!targetEp) { diff --git a/docker/src/api/types.ts b/docker/src/api/types.ts index d369c19..7874a80 100644 --- a/docker/src/api/types.ts +++ b/docker/src/api/types.ts @@ -62,6 +62,7 @@ export interface ExtensionSearchOptions { } export interface ExtensionSearchResult { + url: string; format: string; headers: any; id: string; @@ -158,6 +159,7 @@ export interface WatchStreamQuery { server?: string; category?: string; ext: string; + extensionAnimeId?: string; } export interface BookParams { diff --git a/docker/src/scripts/anime/player.js b/docker/src/scripts/anime/player.js index a7b61ac..b4ee5bb 100644 --- a/docker/src/scripts/anime/player.js +++ b/docker/src/scripts/anime/player.js @@ -22,6 +22,9 @@ const AnimePlayer = (function() { let plyrInstance = null; let hlsInstance = null; + let _manualExtensionId = null; + let _searchTimeout = null; + const els = { wrapper: null, playerWrapper: null, @@ -41,7 +44,12 @@ const AnimePlayer = (function() { dlAudioList: null, dlSubsList: null, dlConfirmBtn: null, - dlCancelBtn: null + dlCancelBtn: null, + manualMatchBtn: null, + matchModal: null, + matchInput: null, + matchList: null, + closeMatchModalBtn: null }; function init(animeId, initialSource, isLocal, animeData) { @@ -74,6 +82,32 @@ const AnimePlayer = (function() { els.dlSubsList = document.getElementById('dl-subs-list'); els.dlConfirmBtn = document.getElementById('confirm-dl-btn'); els.dlCancelBtn = document.getElementById('cancel-dl-btn'); + els.manualMatchBtn = document.getElementById('manual-match-btn'); + els.matchModal = document.getElementById('match-modal'); + els.matchInput = document.getElementById('match-search-input'); + els.matchList = document.getElementById('match-results-list'); + els.closeMatchModalBtn = document.getElementById('close-match-modal'); + + // Event Listeners para Manual Match + if (els.manualMatchBtn) els.manualMatchBtn.addEventListener('click', openMatchModal); + if (els.closeMatchModalBtn) els.closeMatchModalBtn.addEventListener('click', closeMatchModal); + + // Cerrar modal al hacer click fuera + if (els.matchModal) { + els.matchModal.addEventListener('click', (e) => { + if (e.target === els.matchModal) closeMatchModal(); + }); + } + + // Input de búsqueda con Debounce + if (els.matchInput) { + els.matchInput.addEventListener('input', (e) => { + clearTimeout(_searchTimeout); + _searchTimeout = setTimeout(() => { + executeMatchSearch(e.target.value); + }, 500); // Esperar 500ms tras dejar de escribir + }); + } const closeDlModalBtn = document.getElementById('close-download-modal'); @@ -138,6 +172,108 @@ const AnimePlayer = (function() { loadExtensionsList(); } + function openMatchModal() { + if (!els.matchModal) return; + + // Limpiar contenido previo + els.matchInput.value = ''; + els.matchList.innerHTML = `
Type to search in ${els.extSelect.value}...
`; + + // 1. Mostrar el contenedor (para que el navegador calcule el layout) + els.matchModal.style.display = 'flex'; + + // 2. Pequeño delay o forzar reflow para que la transición de opacidad funcione + requestAnimationFrame(() => { + els.matchModal.classList.add('show'); + }); + + setTimeout(() => els.matchInput.focus(), 100); + } + + function closeMatchModal() { + if (!els.matchModal) return; + els.matchModal.classList.remove('show'); + setTimeout(() => { + if (!els.matchModal.classList.contains('show')) { + els.matchModal.style.display = 'none'; + } + }, 300); + } + + async function executeMatchSearch(query) { + if (!query || query.trim().length < 2) return; + + const ext = els.extSelect.value; + if (!ext || ext === 'local') return; + + els.matchList.innerHTML = '
'; + + try { + const res = await fetch(`/api/search/${ext}?q=${encodeURIComponent(query)}`); + const data = await res.json(); + + renderMatchResults(data.results || []); + } catch (e) { + console.error("Match Search Error:", e); + els.matchList.innerHTML = '

Error searching extension.

'; + } + } + + function renderMatchResults(results) { + els.matchList.innerHTML = ''; + + if (results.length === 0) { + els.matchList.innerHTML = '

No results found.

'; + return; + } + + results.forEach(item => { + const div = document.createElement('div'); + div.className = 'match-item dl-item'; + + const img = (item.coverImage && item.coverImage.large) ? item.coverImage.large : "/public/assets/placeholder.svg"; + const title = item.title.english || item.title.romaji || item.title || 'Unknown'; + const externalUrl = item.url || '#'; // El parámetro URL del JSON + + div.innerHTML = ` + cover +
+ ${title} + ${item.releaseDate || item.year || ''} +
+ ${item.url ? ` + + + + + + + + ` : ''} + `; + + div.onclick = (e) => { + if (e.target.closest('.btn-view-source')) return; + selectManualMatch(item); + }; + + els.matchList.appendChild(div); + }); + } + + function selectManualMatch(item) { + // 1. Guardar el ID de la extensión + _manualExtensionId = item.id; + + console.log("Manual Match Selected:", _manualExtensionId, "for extension:", els.extSelect.value); + + // 2. Cerrar modal + closeMatchModal(); + + // 3. Recargar el stream con el nuevo ID + loadStream(); + } + async function openInMPV() { if (!_rawVideoData) { alert("No video loaded yet."); @@ -579,6 +715,11 @@ const AnimePlayer = (function() { if (shouldPlay && _currentEpisode > 0) loadStream(); return; } + if (els.manualMatchBtn) { + // No mostrar en local, sí en extensiones + els.manualMatchBtn.style.display = (selectedExt === 'local') ? 'none' : 'flex'; + } + _manualExtensionId = null; setLoading("Loading Extension Settings..."); try { @@ -690,14 +831,19 @@ const AnimePlayer = (function() { const extParam = `&ext=${currentExt}`; const realSource = _entrySource === 'local' ? 'anilist' : _entrySource; - const url = - `/api/watch/stream?animeId=${_animeId}` + + // AQUÍ AGREGAMOS EL PARÁMETRO OPCIONAL extensionAnimeId + let url = `/api/watch/stream?animeId=${_animeId}` + `&episode=${_currentEpisode}` + `&server=${encodeURIComponent(server)}` + `&category=${_audioMode}` + `${extParam}` + `&source=${realSource}`; + // INYECCIÓN DEL ID MANUAL + if (_manualExtensionId) { + url += `&extensionAnimeId=${encodeURIComponent(_manualExtensionId)}`; + } + try { const res = await fetch(url); const data = await res.json(); diff --git a/docker/views/anime/anime.html b/docker/views/anime/anime.html index 6744f76..9cb1499 100644 --- a/docker/views/anime/anime.html +++ b/docker/views/anime/anime.html @@ -61,13 +61,15 @@
+
+
Sub
@@ -78,6 +80,21 @@
+ +
diff --git a/docker/views/css/anime/player.css b/docker/views/css/anime/player.css index d25c571..b599430 100644 --- a/docker/views/css/anime/player.css +++ b/docker/views/css/anime/player.css @@ -709,7 +709,7 @@ body.stop-scrolling { MODAL DE DESCARGAS - REDISEÑO "GLASS" ========================================= */ -#download-modal { +#download-modal, #match-modal { position: fixed !important; top: 0; left: 0; @@ -727,7 +727,7 @@ body.stop-scrolling { } /* Estado visible activado por JS */ -#download-modal.show { +#download-modal.show, #match-modal.show { display: flex !important; opacity: 1 !important; pointer-events: auto !important; @@ -752,7 +752,8 @@ body.stop-scrolling { overflow: hidden; } -#download-modal.show .download-settings-content { +#download-modal.show .download-settings-content, +#match-modal.show .download-settings-content { transform: scale(1); } @@ -916,4 +917,96 @@ body.stop-scrolling { background: #7c3aed; /* Un tono más oscuro del brand */ transform: translateY(-2px); box-shadow: 0 6px 20px rgba(139, 92, 246, 0.4); +} + +.glass-input { + width: 100%; + background: rgba(255, 255, 255, 0.1); + border: 1px solid rgba(255, 255, 255, 0.2); + color: white; + padding: 12px 16px; + border-radius: 8px; + font-size: 1rem; + outline: none; + transition: all 0.2s ease; +} + +.glass-input:focus { + background: rgba(255, 255, 255, 0.15); + border-color: var(--brand-color); + box-shadow: 0 0 0 2px rgba(139, 92, 246, 0.2); +} + +/* Reutilización para los resultados de búsqueda */ +.match-item { + display: flex; + align-items: center; + gap: 12px; + background: rgba(255, 255, 255, 0.05); + padding: 10px; + border-radius: 6px; + cursor: pointer; + transition: background 0.2s; +} + +.match-item:hover { + background: rgba(255, 255, 255, 0.15); +} + +.match-item img { + width: 40px; + height: 56px; + object-fit: cover; + border-radius: 4px; + background: #222; +} + +.match-title { + font-weight: 600; + font-size: 0.95rem; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + color: #fff; +} + +.match-meta { + font-size: 0.8rem; + color: #aaa; +} + +/* Estado activo (seleccionado actualmente si quisieras marcarlo) */ +.match-item.active { + border: 1px solid var(--brand-color); + background: rgba(139, 92, 246, 0.1); +} + +.btn-view-source { + display: flex; + align-items: center; + justify-content: center; + padding: 8px; + background: rgba(255, 255, 255, 0.1); + border-radius: 6px; + color: rgba(255, 255, 255, 0.7); + transition: all 0.2s ease; + border: 1px solid rgba(255, 255, 255, 0.1); + text-decoration: none; + margin-left: auto; /* Empuja el botón a la derecha */ +} + +.btn-view-source:hover { + background: var(--brand-color); + color: white; + border-color: var(--brand-color-light); + transform: scale(1.1); +} + +/* Ajuste para que el texto no choque con el botón */ +.match-info { + flex: 1; + margin-right: 10px; + display: flex; + flex-direction: column; + overflow: hidden; } \ No newline at end of file