diff --git a/desktop/src/scripts/room.js b/desktop/src/scripts/room.js
index cf89c8c..75e29c9 100644
--- a/desktop/src/scripts/room.js
+++ b/desktop/src/scripts/room.js
@@ -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 = '';
+
+ 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 = '';
+
+ 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,17 +488,28 @@ const RoomsApp = (function() {
e.preventDefault();
e.stopPropagation();
- let title, img, id;
+ let title, img, id, source;
- const titleEl = itemLink.querySelector('.search-title');
- const imgEl = itemLink.querySelector('.search-poster, img');
+ // 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');
- title = titleEl ? titleEl.textContent : (itemLink.textContent.trim() || 'Unknown');
- img = imgEl ? (imgEl.src || imgEl.dataset.src || '/public/assets/placeholder.svg') : '/public/assets/placeholder.svg';
+ title = titleEl ? titleEl.textContent : (itemLink.textContent.trim() || 'Unknown');
+ img = imgEl ? (imgEl.src || imgEl.dataset.src || '/public/assets/placeholder.svg') : '/public/assets/placeholder.svg';
- const href = itemLink.getAttribute('href') || '';
- const hrefParts = href.split('/').filter(p => p);
- id = hrefParts[hrefParts.length - 1] || itemLink.dataset.id;
+ 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 = '
Searching...
';
- if (window.SearchManager) {
- await window.SearchManager.search(query, 'anime', elements.animeResults);
- } else {
- elements.animeResults.innerHTML = 'SearchManager not loaded';
+
+ const source = elements.searchSourceSelect ? elements.searchSourceSelect.value : 'anilist';
+ elements.animeResults.innerHTML = '';
+
+ // 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 = 'Search failed
';
+ }
+ }
+
+ function renderExtensionResults(results, sourceName) {
+ elements.animeResults.innerHTML = '';
+
+ if (results.length === 0) {
+ elements.animeResults.innerHTML = 'No results found
';
+ 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 = `
+
+
+
${escapeHtml(title)}
+
${sourceName}
+
+ `;
+
+ elements.animeResults.appendChild(div);
+ });
}
function escapeHtml(text) {
diff --git a/desktop/views/room.html b/desktop/views/room.html
index 12ffc68..b18564a 100644
--- a/desktop/views/room.html
+++ b/desktop/views/room.html
@@ -206,7 +206,14 @@
Select Anime
diff --git a/docker/src/scripts/room.js b/docker/src/scripts/room.js
index cf89c8c..75e29c9 100644
--- a/docker/src/scripts/room.js
+++ b/docker/src/scripts/room.js
@@ -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 = '
';
+
+ 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 = '
';
+
+ 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,17 +488,28 @@ const RoomsApp = (function() {
e.preventDefault();
e.stopPropagation();
- let title, img, id;
+ let title, img, id, source;
- const titleEl = itemLink.querySelector('.search-title');
- const imgEl = itemLink.querySelector('.search-poster, img');
+ // 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');
- title = titleEl ? titleEl.textContent : (itemLink.textContent.trim() || 'Unknown');
- img = imgEl ? (imgEl.src || imgEl.dataset.src || '/public/assets/placeholder.svg') : '/public/assets/placeholder.svg';
+ title = titleEl ? titleEl.textContent : (itemLink.textContent.trim() || 'Unknown');
+ img = imgEl ? (imgEl.src || imgEl.dataset.src || '/public/assets/placeholder.svg') : '/public/assets/placeholder.svg';
- const href = itemLink.getAttribute('href') || '';
- const hrefParts = href.split('/').filter(p => p);
- id = hrefParts[hrefParts.length - 1] || itemLink.dataset.id;
+ 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 = '
Searching...
';
- if (window.SearchManager) {
- await window.SearchManager.search(query, 'anime', elements.animeResults);
- } else {
- elements.animeResults.innerHTML = 'SearchManager not loaded';
+
+ const source = elements.searchSourceSelect ? elements.searchSourceSelect.value : 'anilist';
+ elements.animeResults.innerHTML = '
';
+
+ // 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 = '
Search failed
';
+ }
+ }
+
+ function renderExtensionResults(results, sourceName) {
+ elements.animeResults.innerHTML = '';
+
+ if (results.length === 0) {
+ elements.animeResults.innerHTML = '
No results found
';
+ 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 = `
+

+
+
${escapeHtml(title)}
+
${sourceName}
+
+ `;
+
+ elements.animeResults.appendChild(div);
+ });
}
function escapeHtml(text) {
diff --git a/docker/views/room.html b/docker/views/room.html
index 4e6b280..e5f9963 100644
--- a/docker/views/room.html
+++ b/docker/views/room.html
@@ -204,7 +204,14 @@