support for extension sources on watchparties

This commit is contained in:
2026-01-11 18:16:49 +01:00
parent d9cb356b84
commit 34f9e44020
4 changed files with 330 additions and 54 deletions

View File

@@ -88,6 +88,7 @@ const RoomsApp = (function() {
// Anime Search Modal
animeSearchModal: document.getElementById('anime-search-modal'),
searchSourceSelect: document.getElementById('search-source-select'),
animeSearchInput: document.getElementById('anime-search-input'),
animeResults: document.getElementById('anime-results'),
closeSearchBtn: document.getElementById('close-search-modal'),
@@ -222,6 +223,24 @@ const RoomsApp = (function() {
);
extensionsReady = true;
// AÑADE ESTO AQUÍ: Llenar el dropdown de búsqueda inmediatamente
populateSearchDropdown();
}
// AÑADE ESTA NUEVA FUNCIÓN FUERA (o dentro del scope de RoomsApp)
function populateSearchDropdown() {
if (!elements.searchSourceSelect) return;
// Reiniciar y poner AniList primero
elements.searchSourceSelect.innerHTML = '<option value="anilist">AniList</option>';
extensionsStore.list.forEach(ext => {
const opt = document.createElement('option');
opt.value = ext;
opt.textContent = ext[0].toUpperCase() + ext.slice(1);
elements.searchSourceSelect.appendChild(opt);
});
}
function setupEventListeners() {
@@ -369,6 +388,16 @@ const RoomsApp = (function() {
});
elements.roomExtSelect.value = selectedAnimeData.source || extensionsStore.list[0];
if (elements.searchSourceSelect) {
elements.searchSourceSelect.innerHTML = '<option value="anilist">AniList</option>';
extensionsStore.list.forEach(ext => {
const opt = document.createElement('option');
opt.value = ext;
opt.textContent = ext[0].toUpperCase() + ext.slice(1);
elements.searchSourceSelect.appendChild(opt);
});
}
await onQuickExtensionChange(null, true);
}
@@ -459,8 +488,17 @@ const RoomsApp = (function() {
e.preventDefault();
e.stopPropagation();
let title, img, id;
let title, img, id, source;
// Try to get data from dataset (Extension Results)
if (itemLink.dataset.source) {
id = itemLink.dataset.id;
source = itemLink.dataset.source;
title = itemLink.dataset.title;
img = itemLink.dataset.image;
}
// Fallback to DOM parsing (AniList/SearchManager Results)
else {
const titleEl = itemLink.querySelector('.search-title');
const imgEl = itemLink.querySelector('.search-poster, img');
@@ -470,6 +508,8 @@ const RoomsApp = (function() {
const href = itemLink.getAttribute('href') || '';
const hrefParts = href.split('/').filter(p => p);
id = hrefParts[hrefParts.length - 1] || itemLink.dataset.id;
source = 'anilist';
}
if (!id) return;
@@ -477,12 +517,14 @@ const RoomsApp = (function() {
id: id,
title: title,
image: img,
source: 'anilist'
source: source // Set the detected source
};
const animeResultObj = {
id: id,
title: title,
cover: img
cover: img,
source: source // Pass to next function
};
showConfigStep();
@@ -649,18 +691,34 @@ const RoomsApp = (function() {
let episodeToPlay = activeContext.episode;
if (fromModal && elements.inpEpisode) episodeToPlay = elements.inpEpisode.value;
const ext = overrides.forceExtension ||
let ext = overrides.forceExtension ||
(fromModal ? (configState.extension || elements.selExtension?.value) : null) ||
activeContext.extension ||
(elements.roomExtSelect ? elements.roomExtSelect.value : null);
const server = overrides.forceServer ||
(fromModal ? (configState.server || elements.selServer?.value) : null) ||
activeContext.server ||
(elements.roomServerSelect ? elements.roomServerSelect.value : null);
const category = elements.roomSdToggle?.getAttribute('data-state') || activeContext.category || 'sub';
let currentSource = selectedAnimeData.source || 'anilist';
if (currentSource !== 'anilist') {
ext = currentSource;
}
let server = overrides.forceServer;
if (!server) {
if (fromModal) {
server = configState.server || elements.selServer?.value;
} else if (currentSource !== 'anilist' || ext === elements.roomExtSelect?.value) {
server = elements.roomServerSelect?.value;
}
}
if (!server && extensionsStore.settings[ext]) {
const extSettings = extensionsStore.settings[ext];
server = extSettings.episodeServers?.[0] || 'Default';
}
if (!ext || !server) {
console.warn("Faltan datos (ext o server).", {ext, server});
if (fromModal && elements.configError) {
@@ -681,8 +739,8 @@ const RoomsApp = (function() {
}
try {
const apiUrl = `/api/watch/stream?animeId=${selectedAnimeData.id}&episode=${episodeToPlay}&server=${encodeURIComponent(server)}&category=${category}&ext=${ext}&source=anilist`;
console.log('Fetching stream:', apiUrl);
const currentSource = selectedAnimeData.source || 'anilist';
const apiUrl = `/api/watch/stream?animeId=${selectedAnimeData.id}&episode=${episodeToPlay}&server=${encodeURIComponent(server)}&category=${category}&ext=${ext}&source=${currentSource}`; console.log('Fetching stream:', apiUrl);
const res = await fetch(apiUrl);
if (!res.ok) throw new Error(`Error ${res.status}: Failed to fetch stream`);
@@ -699,8 +757,13 @@ const RoomsApp = (function() {
let proxyUrl = `/api/proxy?url=${encodeURIComponent(source.url)}`;
const headers = data.headers || {};
if (headers['Referer']) proxyUrl += `&referer=${encodeURIComponent(headers['Referer'])}`;
if (headers['Referer']) {
proxyUrl += `&referer=${encodeURIComponent(headers['Referer'])}`;
}
if (headers['User-Agent']) {
proxyUrl += `&userAgent=${encodeURIComponent(headers['User-Agent'])}`;
}
const subtitles = (source.subtitles || []).map(sub => ({
label: sub.language,
srclang: sub.id || sub.language.toLowerCase().slice(0, 2),
@@ -797,14 +860,22 @@ const RoomsApp = (function() {
updateCountBtn();
try {
const response = await fetch(`/api/anime/${animeResult.id}?source=anilist`);
// Use the source from the result (default to anilist)
const sourceParam = animeResult.source || 'anilist';
// Fetch details using the specific source
const response = await fetch(`/api/anime/${animeResult.id}?source=${sourceParam}`);
if (!response.ok) throw new Error("Failed to load anime details");
const data = await response.json();
currentAnimeDetails = data;
// Save metadata
if (selectedAnimeData) {
selectedAnimeData.malId = data.idMal;
selectedAnimeData.malId = data.idMal; // Might be null for extensions, that's okay
// Ensure source is persisted
selectedAnimeData.source = sourceParam;
}
modalTotalEpisodes = data.episodes || 12;
@@ -813,9 +884,19 @@ const RoomsApp = (function() {
renderModalEpisodes();
setupModalPaginationControls();
// Auto-select the extension in the config dropdown if it matches
if (extensionsReady && elements.selExtension && sourceParam !== 'anilist') {
// If the extension we searched with is in the list, select it
if (Array.from(elements.selExtension.options).some(o => o.value === sourceParam)) {
elements.selExtension.value = sourceParam;
handleModalExtensionChange();
}
}
} catch (error) {
console.error("Error fetching details", error);
gridContainer.innerHTML = '';
// If details fail (common with strict scrapers), show manual input
document.querySelector('.manual-ep-input').style.display = 'block';
document.getElementById('modal-pagination').style.display = 'none';
}
@@ -1985,12 +2066,62 @@ const RoomsApp = (function() {
async function searchAnime() {
const query = elements.animeSearchInput.value.trim();
if (!query) return;
elements.animeResults.innerHTML = '<div style="padding:20px;text-align:center;color:#888;">Searching...</div>';
const source = elements.searchSourceSelect ? elements.searchSourceSelect.value : 'anilist';
elements.animeResults.innerHTML = '<div style="padding:20px;text-align:center;color:#888;"><div class="spinner" style="margin:0 auto 10px;"></div>Searching...</div>';
// 1. ANILIST SEARCH (Legacy)
if (source === 'anilist') {
if (window.SearchManager) {
await window.SearchManager.search(query, 'anime', elements.animeResults);
} else {
elements.animeResults.innerHTML = 'SearchManager not loaded';
}
return;
}
// 2. EXTENSION SEARCH
try {
const res = await fetch(`/api/search/${source}?q=${encodeURIComponent(query)}`);
const data = await res.json();
renderExtensionResults(data.results || [], source);
} catch (e) {
console.error("Search error:", e);
elements.animeResults.innerHTML = '<div style="padding:20px;text-align:center;color:#ff6b6b;">Search failed</div>';
}
}
function renderExtensionResults(results, sourceName) {
elements.animeResults.innerHTML = '';
if (results.length === 0) {
elements.animeResults.innerHTML = '<div style="padding:20px;text-align:center;color:#888;">No results found</div>';
return;
}
results.forEach(item => {
const title = item.title.english || item.title.romaji || item.title.native || 'Unknown Title';
const image = item.coverImage?.large || item.coverImage?.medium || '/public/assets/placeholder.svg';
// We add data-source attribute to identify where it came from
const div = document.createElement('a');
div.className = 'anime-result-item';
div.href = '#'; // Prevent navigation
div.dataset.id = item.id;
div.dataset.source = sourceName; // Important: Store the extension name
div.dataset.title = title;
div.dataset.image = image;
div.innerHTML = `
<img src="${image}" class="search-poster" loading="lazy">
<div class="search-info">
<div class="search-title">${escapeHtml(title)}</div>
<div class="search-meta" style="color:var(--brand-color)">${sourceName}</div>
</div>
`;
elements.animeResults.appendChild(div);
});
}
function escapeHtml(text) {

View File

@@ -206,7 +206,14 @@
<div id="step-search">
<h2 class="modal-title">Select Anime</h2>
<div class="search-bar">
<input type="text" id="anime-search-input" placeholder="Search anime..." />
<div class="quick-select-wrapper" style="min-width: 130px; background: rgba(255,255,255,0.05);">
<select id="search-source-select" class="quick-select">
<option value="anilist">AniList</option>
</select>
<div class="select-arrow"></div>
</div>
<input type="text" id="anime-search-input" placeholder="Search anime..." autocomplete="off"/>
<button id="anime-search-btn">Search</button>
</div>
<div id="anime-results" class="anime-results"></div>

View File

@@ -88,6 +88,7 @@ const RoomsApp = (function() {
// Anime Search Modal
animeSearchModal: document.getElementById('anime-search-modal'),
searchSourceSelect: document.getElementById('search-source-select'),
animeSearchInput: document.getElementById('anime-search-input'),
animeResults: document.getElementById('anime-results'),
closeSearchBtn: document.getElementById('close-search-modal'),
@@ -222,6 +223,24 @@ const RoomsApp = (function() {
);
extensionsReady = true;
// AÑADE ESTO AQUÍ: Llenar el dropdown de búsqueda inmediatamente
populateSearchDropdown();
}
// AÑADE ESTA NUEVA FUNCIÓN FUERA (o dentro del scope de RoomsApp)
function populateSearchDropdown() {
if (!elements.searchSourceSelect) return;
// Reiniciar y poner AniList primero
elements.searchSourceSelect.innerHTML = '<option value="anilist">AniList</option>';
extensionsStore.list.forEach(ext => {
const opt = document.createElement('option');
opt.value = ext;
opt.textContent = ext[0].toUpperCase() + ext.slice(1);
elements.searchSourceSelect.appendChild(opt);
});
}
function setupEventListeners() {
@@ -369,6 +388,16 @@ const RoomsApp = (function() {
});
elements.roomExtSelect.value = selectedAnimeData.source || extensionsStore.list[0];
if (elements.searchSourceSelect) {
elements.searchSourceSelect.innerHTML = '<option value="anilist">AniList</option>';
extensionsStore.list.forEach(ext => {
const opt = document.createElement('option');
opt.value = ext;
opt.textContent = ext[0].toUpperCase() + ext.slice(1);
elements.searchSourceSelect.appendChild(opt);
});
}
await onQuickExtensionChange(null, true);
}
@@ -459,8 +488,17 @@ const RoomsApp = (function() {
e.preventDefault();
e.stopPropagation();
let title, img, id;
let title, img, id, source;
// Try to get data from dataset (Extension Results)
if (itemLink.dataset.source) {
id = itemLink.dataset.id;
source = itemLink.dataset.source;
title = itemLink.dataset.title;
img = itemLink.dataset.image;
}
// Fallback to DOM parsing (AniList/SearchManager Results)
else {
const titleEl = itemLink.querySelector('.search-title');
const imgEl = itemLink.querySelector('.search-poster, img');
@@ -470,6 +508,8 @@ const RoomsApp = (function() {
const href = itemLink.getAttribute('href') || '';
const hrefParts = href.split('/').filter(p => p);
id = hrefParts[hrefParts.length - 1] || itemLink.dataset.id;
source = 'anilist';
}
if (!id) return;
@@ -477,12 +517,14 @@ const RoomsApp = (function() {
id: id,
title: title,
image: img,
source: 'anilist'
source: source // Set the detected source
};
const animeResultObj = {
id: id,
title: title,
cover: img
cover: img,
source: source // Pass to next function
};
showConfigStep();
@@ -649,18 +691,34 @@ const RoomsApp = (function() {
let episodeToPlay = activeContext.episode;
if (fromModal && elements.inpEpisode) episodeToPlay = elements.inpEpisode.value;
const ext = overrides.forceExtension ||
let ext = overrides.forceExtension ||
(fromModal ? (configState.extension || elements.selExtension?.value) : null) ||
activeContext.extension ||
(elements.roomExtSelect ? elements.roomExtSelect.value : null);
const server = overrides.forceServer ||
(fromModal ? (configState.server || elements.selServer?.value) : null) ||
activeContext.server ||
(elements.roomServerSelect ? elements.roomServerSelect.value : null);
const category = elements.roomSdToggle?.getAttribute('data-state') || activeContext.category || 'sub';
let currentSource = selectedAnimeData.source || 'anilist';
if (currentSource !== 'anilist') {
ext = currentSource;
}
let server = overrides.forceServer;
if (!server) {
if (fromModal) {
server = configState.server || elements.selServer?.value;
} else if (currentSource !== 'anilist' || ext === elements.roomExtSelect?.value) {
server = elements.roomServerSelect?.value;
}
}
if (!server && extensionsStore.settings[ext]) {
const extSettings = extensionsStore.settings[ext];
server = extSettings.episodeServers?.[0] || 'Default';
}
if (!ext || !server) {
console.warn("Faltan datos (ext o server).", {ext, server});
if (fromModal && elements.configError) {
@@ -681,8 +739,8 @@ const RoomsApp = (function() {
}
try {
const apiUrl = `/api/watch/stream?animeId=${selectedAnimeData.id}&episode=${episodeToPlay}&server=${encodeURIComponent(server)}&category=${category}&ext=${ext}&source=anilist`;
console.log('Fetching stream:', apiUrl);
const currentSource = selectedAnimeData.source || 'anilist';
const apiUrl = `/api/watch/stream?animeId=${selectedAnimeData.id}&episode=${episodeToPlay}&server=${encodeURIComponent(server)}&category=${category}&ext=${ext}&source=${currentSource}`; console.log('Fetching stream:', apiUrl);
const res = await fetch(apiUrl);
if (!res.ok) throw new Error(`Error ${res.status}: Failed to fetch stream`);
@@ -699,8 +757,13 @@ const RoomsApp = (function() {
let proxyUrl = `/api/proxy?url=${encodeURIComponent(source.url)}`;
const headers = data.headers || {};
if (headers['Referer']) proxyUrl += `&referer=${encodeURIComponent(headers['Referer'])}`;
if (headers['Referer']) {
proxyUrl += `&referer=${encodeURIComponent(headers['Referer'])}`;
}
if (headers['User-Agent']) {
proxyUrl += `&userAgent=${encodeURIComponent(headers['User-Agent'])}`;
}
const subtitles = (source.subtitles || []).map(sub => ({
label: sub.language,
srclang: sub.id || sub.language.toLowerCase().slice(0, 2),
@@ -797,14 +860,22 @@ const RoomsApp = (function() {
updateCountBtn();
try {
const response = await fetch(`/api/anime/${animeResult.id}?source=anilist`);
// Use the source from the result (default to anilist)
const sourceParam = animeResult.source || 'anilist';
// Fetch details using the specific source
const response = await fetch(`/api/anime/${animeResult.id}?source=${sourceParam}`);
if (!response.ok) throw new Error("Failed to load anime details");
const data = await response.json();
currentAnimeDetails = data;
// Save metadata
if (selectedAnimeData) {
selectedAnimeData.malId = data.idMal;
selectedAnimeData.malId = data.idMal; // Might be null for extensions, that's okay
// Ensure source is persisted
selectedAnimeData.source = sourceParam;
}
modalTotalEpisodes = data.episodes || 12;
@@ -813,9 +884,19 @@ const RoomsApp = (function() {
renderModalEpisodes();
setupModalPaginationControls();
// Auto-select the extension in the config dropdown if it matches
if (extensionsReady && elements.selExtension && sourceParam !== 'anilist') {
// If the extension we searched with is in the list, select it
if (Array.from(elements.selExtension.options).some(o => o.value === sourceParam)) {
elements.selExtension.value = sourceParam;
handleModalExtensionChange();
}
}
} catch (error) {
console.error("Error fetching details", error);
gridContainer.innerHTML = '';
// If details fail (common with strict scrapers), show manual input
document.querySelector('.manual-ep-input').style.display = 'block';
document.getElementById('modal-pagination').style.display = 'none';
}
@@ -1985,12 +2066,62 @@ const RoomsApp = (function() {
async function searchAnime() {
const query = elements.animeSearchInput.value.trim();
if (!query) return;
elements.animeResults.innerHTML = '<div style="padding:20px;text-align:center;color:#888;">Searching...</div>';
const source = elements.searchSourceSelect ? elements.searchSourceSelect.value : 'anilist';
elements.animeResults.innerHTML = '<div style="padding:20px;text-align:center;color:#888;"><div class="spinner" style="margin:0 auto 10px;"></div>Searching...</div>';
// 1. ANILIST SEARCH (Legacy)
if (source === 'anilist') {
if (window.SearchManager) {
await window.SearchManager.search(query, 'anime', elements.animeResults);
} else {
elements.animeResults.innerHTML = 'SearchManager not loaded';
}
return;
}
// 2. EXTENSION SEARCH
try {
const res = await fetch(`/api/search/${source}?q=${encodeURIComponent(query)}`);
const data = await res.json();
renderExtensionResults(data.results || [], source);
} catch (e) {
console.error("Search error:", e);
elements.animeResults.innerHTML = '<div style="padding:20px;text-align:center;color:#ff6b6b;">Search failed</div>';
}
}
function renderExtensionResults(results, sourceName) {
elements.animeResults.innerHTML = '';
if (results.length === 0) {
elements.animeResults.innerHTML = '<div style="padding:20px;text-align:center;color:#888;">No results found</div>';
return;
}
results.forEach(item => {
const title = item.title.english || item.title.romaji || item.title.native || 'Unknown Title';
const image = item.coverImage?.large || item.coverImage?.medium || '/public/assets/placeholder.svg';
// We add data-source attribute to identify where it came from
const div = document.createElement('a');
div.className = 'anime-result-item';
div.href = '#'; // Prevent navigation
div.dataset.id = item.id;
div.dataset.source = sourceName; // Important: Store the extension name
div.dataset.title = title;
div.dataset.image = image;
div.innerHTML = `
<img src="${image}" class="search-poster" loading="lazy">
<div class="search-info">
<div class="search-title">${escapeHtml(title)}</div>
<div class="search-meta" style="color:var(--brand-color)">${sourceName}</div>
</div>
`;
elements.animeResults.appendChild(div);
});
}
function escapeHtml(text) {

View File

@@ -204,7 +204,14 @@
<div id="step-search">
<h2 class="modal-title">Select Anime</h2>
<div class="search-bar">
<input type="text" id="anime-search-input" placeholder="Search anime..." />
<div class="quick-select-wrapper" style="min-width: 130px; background: rgba(255,255,255,0.05);">
<select id="search-source-select" class="quick-select">
<option value="anilist">AniList</option>
</select>
<div class="select-arrow"></div>
</div>
<input type="text" id="anime-search-input" placeholder="Search anime..." autocomplete="off"/>
<button id="anime-search-btn">Search</button>
</div>
<div id="anime-results" class="anime-results"></div>