added manual matching on books

This commit is contained in:
2026-01-02 16:54:40 +01:00
parent f5cfa29b64
commit 59fdadd288
18 changed files with 1235 additions and 494 deletions

View File

@@ -23,7 +23,6 @@ const AnimePlayer = (function() {
let hlsInstance = null;
let _manualExtensionId = null;
let _searchTimeout = null;
const els = {
wrapper: null,
@@ -46,10 +45,6 @@ const AnimePlayer = (function() {
dlConfirmBtn: null,
dlCancelBtn: null,
manualMatchBtn: null,
matchModal: null,
matchInput: null,
matchList: null,
closeMatchModalBtn: null
};
function init(animeId, initialSource, isLocal, animeData) {
@@ -83,31 +78,6 @@ const AnimePlayer = (function() {
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');
@@ -168,112 +138,33 @@ const AnimePlayer = (function() {
if(els.subDubToggle) els.subDubToggle.addEventListener('click', toggleAudioMode);
if(els.serverSelect) els.serverSelect.addEventListener('change', () => loadStream());
if(els.extSelect) els.extSelect.addEventListener('change', () => handleExtensionChange(true));
if (els.manualMatchBtn) {
els.manualMatchBtn.addEventListener('click', openMatchModal);
}
loadExtensionsList();
}
function openMatchModal() {
if (!els.matchModal) return;
const currentExt = els.extSelect.value;
if (!currentExt || currentExt === 'local') return;
// Limpiar contenido previo
els.matchInput.value = '';
els.matchList.innerHTML = `<div style="padding:20px; text-align:center; color:#777;">Type to search in ${els.extSelect.value}...</div>`;
// 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';
MatchModal.open({
provider: currentExt,
initialQuery: _animeTitle, // Variable existente en player.js
onSearch: async (query, prov) => {
const res = await fetch(`/api/search/${prov}?q=${encodeURIComponent(query)}`);
const data = await res.json();
return data.results || [];
},
onSelect: (item) => {
console.log("Selected Anime ID:", item.id);
_manualExtensionId = item.id;
loadStream();
}
}, 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 = '<div class="spinner" style="margin: 20px auto;"></div>';
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 = '<p style="color:#ef4444; text-align:center;">Error searching extension.</p>';
}
}
function renderMatchResults(results) {
els.matchList.innerHTML = '';
if (results.length === 0) {
els.matchList.innerHTML = '<p style="text-align:center; color:#999;">No results found.</p>';
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 = `
<img src="${img}" alt="cover">
<div class="match-info">
<span class="match-title">${title}</span>
<span class="match-meta">${item.releaseDate || item.year || ''}</span>
</div>
${item.url ? `
<a href="${externalUrl}" target="_blank" class="btn-view-source" title="View Source">
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path>
<polyline points="15 3 21 3 21 9"></polyline>
<line x1="10" y1="14" x2="21" y2="3"></line>
</svg>
</a>
` : ''}
`;
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.");

View File

@@ -11,6 +11,7 @@ let isLocal = false;
let currentLanguage = null;
let uniqueLanguages = [];
let isSortAscending = true;
let manualExtensionBookId = null;
const chapterPagination = Object.create(PaginationManager);
chapterPagination.init(6, () => renderChapterList());
@@ -36,6 +37,37 @@ async function init() {
await loadChapters();
await setupAddToListButton();
document.getElementById('manual-match-btn')?.addEventListener('click', () => {
const select = document.getElementById('provider-filter');
const provider = select.value;
// Obtener título para prellenar
const currentTitle = bookData?.title?.romaji || bookData?.title?.english || '';
MatchModal.open({
provider: provider,
initialQuery: currentTitle,
// Define CÓMO buscar
onSearch: async (query, prov) => {
const res = await fetch(`/api/search/books/${prov}?q=${encodeURIComponent(query)}`);
const data = await res.json();
return data.results || [];
},
// Define QUÉ hacer al seleccionar
onSelect: (item) => {
console.log("Selected Book ID:", item.id);
manualExtensionBookId = item.id;
// Lógica existente de tu book.js para recargar caps
loadChapters(provider);
// Feedback visual en el botón
const btn = document.getElementById('manual-match-btn');
if(btn) btn.style.color = '#22c55e';
}
});
});
} catch (err) {
console.error("Init Error:", err);
showError("Error loading book");
@@ -311,6 +343,9 @@ async function loadChapters(targetProvider = null) {
const source = extensionName || 'anilist';
fetchUrl = `/api/book/${bookId}/chapters?source=${source}`;
if (targetProvider !== 'all') fetchUrl += `&provider=${targetProvider}`;
if (manualExtensionBookId && targetProvider !== 'all') {
fetchUrl += `&extensionBookId=${manualExtensionBookId}`;
}
}
const res = await fetch(fetchUrl);
@@ -515,6 +550,8 @@ async function loadAvailableExtensions() {
function setupProviderFilter() {
const select = document.getElementById('provider-filter');
const manualBtn = document.getElementById('manual-match-btn'); // NUEVO
if (!select) return;
select.style.display = 'inline-block';
select.innerHTML = '';
@@ -538,11 +575,32 @@ function setupProviderFilter() {
select.appendChild(opt);
});
// Lógica de selección inicial
if (isLocal) select.value = 'local';
else if (extensionName && availableExtensions.includes(extensionName)) select.value = extensionName;
else if (availableExtensions.length > 0) select.value = availableExtensions[0];
select.onchange = () => loadChapters(select.value);
// Visibilidad inicial del botón manual
updateManualButtonVisibility(select.value);
select.onchange = () => {
// Al cambiar de proveedor, reseteamos la selección manual para evitar conflictos
manualExtensionBookId = null;
updateManualButtonVisibility(select.value);
loadChapters(select.value);
};
}
function updateManualButtonVisibility(provider) {
const btn = document.getElementById('manual-match-btn');
if (!btn) return;
// Solo mostrar si es un proveedor específico (no 'all' ni 'local')
if (provider !== 'all' && provider !== 'local') {
btn.style.display = 'flex';
} else {
btn.style.display = 'none';
}
}
function updateExtensionPill() {
@@ -569,9 +627,9 @@ function updateCustomAddButton() {
}
function setupModalClickOutside() {
const modal = document.getElementById('add-list-modal');
if (modal) {
modal.addEventListener('click', (e) => {
const addListModal = document.getElementById('add-list-modal');
if (addListModal) {
addListModal.addEventListener('click', (e) => {
if (e.target.id === 'add-list-modal') ListModalManager.close();
});
}

View File

@@ -0,0 +1,171 @@
const MatchModal = (function() {
let _config = {
onSearch: async (query, provider) => [], // Debe devolver Array de objetos
onSelect: (item, provider) => {},
provider: 'generic'
};
let elements = {};
let searchTimeout = null;
function init() {
if (document.getElementById('waifu-match-modal')) return;
// Inyectar HTML
const modalHTML = `
<div class="match-modal-overlay" id="waifu-match-modal">
<div class="match-modal-content">
<div class="match-header">
<h3 class="match-title">Manual Match <span id="match-provider-badge" style="opacity:0.6; font-size:0.8em; margin-left:8px;"></span></h3>
<button class="match-close-btn" id="match-close-btn">✕</button>
</div>
<div class="match-search-container">
<input type="text" id="match-input" class="match-input" placeholder="Search title..." autocomplete="off">
<button id="match-btn-action" class="match-search-btn">Search</button>
</div>
<div class="match-results-body" id="match-results-container">
<div class="match-msg">Type to start searching...</div>
</div>
</div>
</div>
`;
document.body.insertAdjacentHTML('beforeend', modalHTML);
// Cachear elementos
elements = {
overlay: document.getElementById('waifu-match-modal'),
input: document.getElementById('match-input'),
results: document.getElementById('match-results-container'),
badge: document.getElementById('match-provider-badge'),
closeBtn: document.getElementById('match-close-btn'),
searchBtn: document.getElementById('match-btn-action')
};
// Event Listeners
elements.closeBtn.onclick = close;
elements.overlay.onclick = (e) => { if(e.target === elements.overlay) close(); };
// Búsqueda al hacer clic
elements.searchBtn.onclick = () => performSearch(elements.input.value);
// Búsqueda al escribir (Debounce)
elements.input.addEventListener('input', (e) => {
clearTimeout(searchTimeout);
if(e.target.value.trim().length === 0) return;
searchTimeout = setTimeout(() => performSearch(e.target.value), 600);
});
// Enter key
elements.input.addEventListener('keypress', (e) => {
if (e.key === 'Enter') performSearch(elements.input.value);
});
}
function open(options) {
init(); // Asegurar que el DOM existe
_config = { ..._config, ...options };
// Resetear UI
elements.input.value = options.initialQuery || '';
elements.results.innerHTML = '<div class="match-msg">Search above to find matches...</div>';
elements.badge.innerText = options.provider ? `(${options.provider})` : '';
// Mostrar Modal
elements.overlay.classList.add('active');
// Auto-search si hay query inicial
if (options.initialQuery) {
performSearch(options.initialQuery);
}
setTimeout(() => elements.input.focus(), 100);
}
function close() {
if(elements.overlay) elements.overlay.classList.remove('active');
}
async function performSearch(query) {
if (!query || query.trim().length < 2) return;
elements.results.innerHTML = '<div class="match-spinner"></div>';
try {
// Ejecutar la función de búsqueda pasada en la config
const results = await _config.onSearch(query, _config.provider);
renderResults(results);
} catch (err) {
console.error(err);
elements.results.innerHTML = '<div class="match-msg error">Error searching provider.</div>';
}
}
function renderResults(results) {
elements.results.innerHTML = '';
if (!results || results.length === 0) {
elements.results.innerHTML = '<div class="match-msg">No matches found.</div>';
return;
}
const grid = document.createElement('div');
grid.className = 'match-list-grid';
results.forEach(item => {
const el = document.createElement('div');
el.className = 'match-item';
// Normalización de datos para asegurar compatibilidad con Anime/Libros
const img = item.coverImage?.large || item.coverImage || item.image || '/public/assets/no-image.png';
const title = item.title?.english || item.title?.romaji || item.title || 'Unknown Title';
const meta = item.releaseDate || item.year || item.startDate?.year || '';
const url = item.url || item.externalUrl || null;
el.innerHTML = `
<img src="${img}" class="match-poster" loading="lazy">
<div class="match-info">
<div class="match-item-title">${title}</div>
<div class="match-item-meta">${meta}</div>
</div>
`;
// Botón de enlace externo (si existe URL)
if (url) {
const linkBtn = document.createElement('a');
linkBtn.href = url;
linkBtn.target = "_blank";
linkBtn.className = "match-link-btn";
linkBtn.title = "View Source";
linkBtn.innerHTML = `
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path>
<polyline points="15 3 21 3 21 9"></polyline>
<line x1="10" y1="14" x2="21" y2="3"></line>
</svg>
`;
// Evitar que el click en el enlace dispare el select
linkBtn.onclick = (e) => e.stopPropagation();
el.appendChild(linkBtn);
}
// Click en la tarjeta selecciona
el.onclick = () => {
_config.onSelect(item);
close();
};
grid.appendChild(el);
});
elements.results.appendChild(grid);
}
return {
open,
close
};
})();