Files
KefinTweaks/search.js
T

854 lines
36 KiB
JavaScript

// Jellyfin Smart Search - Dual Input + Persistent Toggle + Query Copy
(function() {
'use strict';
const LOG = (...args) => console.log('[SmartSearch]', ...args);
const WARN = (...args) => console.warn('[SmartSearch]', ...args);
const ERR = (...args) => console.error('[SmartSearch]', ...args);
// Configuration
const CONFIG = {
enableJellyseerr: false, // Toggle for Jellyseerr integration
};
LOG('script load start');
// Monitor network requests to see what's happening
const originalFetch = window.fetch;
const originalXHR = window.XMLHttpRequest;
// Track if we should block XHR requests (only for the initial URL query)
let shouldBlockXHR = false;
let hasBlockedInitialSearch = false;
// Override fetch to log our requests
window.fetch = function(...args) {
LOG('FETCH request:', args[0]);
return originalFetch.apply(this, args);
};
// Override XHR to log and potentially block default search requests
const OriginalXHR = window.XMLHttpRequest;
window.XMLHttpRequest = function() {
const xhr = new OriginalXHR();
const originalOpen = xhr.open;
const originalSend = xhr.send;
xhr.open = function(method, url, ...args) {
LOG('XHR request:', method, url);
// Check if this is a search request we want to block
// Our smart search uses fetch(), so any XHR search request should be blocked
const isSearchRequest = url.includes('/Items?') &&
(url.includes('searchTerm=') || url.includes('query='));
if (isSearchRequest && shouldBlockXHR && !hasBlockedInitialSearch) {
LOG('Blocking initial default search XHR request:', url);
hasBlockedInitialSearch = true;
// Disable blocking after this first request
shouldBlockXHR = false;
LOG('XHR blocking disabled after initial search');
// Override send to do nothing
xhr.send = function() {
LOG('Initial default search XHR blocked');
// Simulate a successful response
setTimeout(() => {
xhr.readyState = 4;
xhr.status = 200;
xhr.responseText = '{"Items":[],"TotalRecordCount":0}';
if (xhr.onreadystatechange) {
xhr.onreadystatechange();
}
}, 100);
};
}
return originalOpen.apply(this, [method, url, ...args]);
};
xhr.send = function(...args) {
LOG('XHR sending:', args);
return originalSend.apply(this, args);
};
return xhr;
};
// Immediate interception for URL queries - run this as early as possible
(function() {
// Parse query from hash since Jellyfin uses hash-based routing
let queryFromUrl = null;
if (window.location.hash.includes('search.html') && window.location.hash.includes('query=')) {
const hashQuery = window.location.hash.split('query=')[1];
if (hashQuery) {
queryFromUrl = hashQuery.split('&')[0]; // Get query before any other params
}
}
LOG('Early interception - URL query:', queryFromUrl);
LOG('Current pathname:', window.location.pathname);
LOG('Current hash:', window.location.hash);
if (queryFromUrl && (window.location.pathname.includes('search.html') || window.location.hash.includes('search'))) {
LOG('URL query detected, setting up protection');
shouldBlockXHR = true; // Enable XHR blocking for initial URL query
// Just enable XHR blocking - don't touch the input
LOG('XHR blocking enabled for initial search');
}
})();
// config
const CORE_TYPES = ['Movie', 'Series', 'Episode', 'Person'];
const MUSIC_TYPES = ['MusicAlbum', 'Audio', 'Artist'];
const BOOKS_TYPES = ['Book', 'AudioBook'];
const OTHER_TYPES = ['Playlist', 'Photo', 'PhotoAlbum', 'TvChannel', 'LiveTvProgram', 'BoxSet'];
// Cache for search results by search term and type
const searchCache = new Map();
// Function to generate cache key
function getCacheKey(searchTerm, searchType) {
return `${searchTerm.toLowerCase().trim()}_${searchType}`;
}
// Function to check cache and return cached results if available
function getCachedResults(searchTerm, searchType) {
const cacheKey = getCacheKey(searchTerm, searchType);
const cached = searchCache.get(cacheKey);
LOG('Cache check:', { searchTerm, searchType, cacheKey, cached: !!cached });
return cached;
}
// Function to cache results
function cacheResults(searchTerm, searchType, results) {
const cacheKey = getCacheKey(searchTerm, searchType);
searchCache.set(cacheKey, results);
LOG('Cached results:', { searchTerm, searchType, cacheKey, resultCount: results.total });
}
// styles (dedupe by id)
function addCustomStyles() {
if (document.getElementById('smart-search-styles')) return;
LOG('adding styles');
const style = document.createElement('style');
style.id = 'smart-search-styles';
style.textContent = `
.smart-search-wrapper { margin: 12px 0 20px 0; }
.smart-search-input { width:100%; padding:8px; border-radius:4px; box-sizing:border-box; }
.smart-search-buttons { display:flex; gap:8px; margin-top:8px; flex-wrap:wrap; justify-content: center; }
.smart-search-primary, .smart-search-secondary { padding:8px 12px; border:none; border-radius:4px; cursor:pointer; }
.smart-search-primary { background:#00a4dc; color:#fff; }
.smart-search-secondary { background:#444; color:#fff; }
.smart-search-results { margin-top:12px; }
.smart-search-stats { color:#999; font-size:12px; margin-top:6px; display: none; }
#persistent-toggle-btn { display:none; position:relative; padding:8px 12px; background:#00a4dc; color:#fff; border:none; border-radius:4px; cursor:pointer; }
/* Smart search mode - hide search icon */
.smart-search-mode .searchfields-icon {
top: 37px;
position: absolute;
z-index: 1;
right: 0;
}
`;
document.head.appendChild(style);
}
// update search URL helper
function updateSearchUrl(searchTerm, searchType = 'videos') {
try {
// Jellyfin uses hash-based routing, so we need to update the hash
const baseHash = '#/search.html';
const params = new URLSearchParams();
if (searchTerm && searchTerm.trim()) {
params.set('query', searchTerm.trim());
}
params.set('type', searchType);
const newHash = `${baseHash}?${params.toString()}`;
window.history.replaceState({}, '', window.location.pathname + newHash);
LOG('Updated search URL hash:', newHash);
} catch (e) {
ERR('updateSearchUrl error', e);
}
}
// build URL helper
function buildSearchUrl(searchTerm, itemTypes, additionalParams = {}) {
try {
const userId = ApiClient.getCurrentUserId();
const baseUrl = ApiClient.serverAddress();
const params = new URLSearchParams({
userId,
limit: '100',
recursive: 'true',
searchTerm,
fields: 'PrimaryImageAspectRatio,CanDelete,MediaSourceCount',
imageTypeLimit: '1',
enableTotalRecordCount: 'false',
...additionalParams
});
itemTypes.forEach(t => params.append('includeItemTypes', t));
const url = `${baseUrl}/Items?${params.toString()}`;
LOG('buildSearchUrl ->', url);
return url;
} catch (e) {
ERR('buildSearchUrl error', e);
throw e;
}
}
// Show/hide loading spinner
function setLoadingSpinner(show) {
const spinner = document.querySelector('.docspinner.mdl-spinner');
if (spinner) {
if (show) {
spinner.classList.add('mdlSpinnerActive');
LOG('Loading spinner shown');
} else {
spinner.classList.remove('mdlSpinnerActive');
LOG('Loading spinner hidden');
}
}
}
// Fetch Jellyseerr results
async function fetchJellyseerrResults(searchTerm) {
try {
const jellyseerrUrl = `${ApiClient.serverAddress()}/JellyfinEnhanced/jellyseerr/search?query=${encodeURIComponent(searchTerm)}`;
LOG('Fetching Jellyseerr results:', jellyseerrUrl);
const response = await ApiClient.fetch({
url: jellyseerrUrl,
method: 'GET',
headers: {
'Accept': 'application/json',
'X-Jellyfin-User-Id': ApiClient.getCurrentUserId()
}
});
const data = await response.json();
LOG('Jellyseerr response:', data);
return data;
} catch (err) {
WARN('Jellyseerr fetch failed', err);
return { movies: [], tv: [] };
}
}
// Convert Jellyseerr item to our format
function convertJellyseerrItem(item, type) {
return {
Id: `jellyseerr_${item.id}`,
Name: item.title || item.name,
Type: type === 'movie' ? 'Movie' : 'Series',
MediaType: 'Video',
ImageTags: item.posterPath ? { Primary: item.posterPath } : null,
ProductionYear: item.releaseDate ? new Date(item.releaseDate).getFullYear() : null,
Overview: item.overview,
// Jellyseerr specific data
JellyseerrId: item.id,
JellyseerrType: type,
ExternalId: item.tmdbId || item.tvdbId,
IsJellyseerr: true
};
}
// perform smart search
async function performSmartSearch(searchTerm, searchType = 'core') {
LOG('performSmartSearch()', { searchTerm, searchType });
const results = { groupedItems: {}, total: 0 };
// Hide search suggestions
const searchSuggestions = document.querySelector('.searchSuggestions');
if (searchSuggestions) {
searchSuggestions.style.display = 'none';
}
// Show loading spinner
setLoadingSpinner(true);
try {
const typeGroups = searchType === 'music'
? MUSIC_TYPES
: (searchType === 'books'
? BOOKS_TYPES
: (searchType === 'all' ? [...CORE_TYPES, ...MUSIC_TYPES, ...BOOKS_TYPES, ...OTHER_TYPES] : CORE_TYPES));
const promises = typeGroups.map(type => {
let url;
if (type === 'Artist') {
// Use dedicated Artists endpoint for better results
const userId = ApiClient.getCurrentUserId();
const baseUrl = ApiClient.serverAddress();
url = `${baseUrl}/Artists?limit=100&searchTerm=${encodeURIComponent(searchTerm)}&fields=PrimaryImageAspectRatio&fields=CanDelete&fields=MediaSourceCount&imageTypeLimit=1&userId=${userId}&enableTotalRecordCount=false`;
} else {
url = buildSearchUrl(searchTerm, [type]);
}
return ApiClient.fetch({ url, method: 'GET' })
.then(resp => resp.json())
.catch(err => {
WARN('fetch failed for', type, err);
return { Items: [] };
});
});
// Add Jellyseerr search for core types (if enabled)
if (CONFIG.enableJellyseerr && (searchType === 'core' || searchType === 'all')) {
promises.push(fetchJellyseerrResults(searchTerm));
} else {
promises.push(Promise.resolve({ movies: [], tv: [] }));
}
const start = performance.now();
const responses = await Promise.all(promises);
const end = performance.now();
// Group results by type
responses.forEach((r, idx) => {
if (idx < typeGroups.length) {
// Regular Jellyfin results
const type = typeGroups[idx];
if (r && r.Items && r.Items.length > 0) {
// For Artists, use all items (they often don't have primary images)
// For other types, filter to show only those with images
let filteredItems;
if (type === 'Artist') {
// Artists: show all items, they often don't have primary images
filteredItems = r.Items;
} else {
// Other types: filter to show only those with images
filteredItems = r.Items.filter(item => item.ImageTags?.Primary);
}
results.groupedItems[type] = filteredItems;
results.total += filteredItems.length;
}
} else {
// Jellyseerr results
if (r && (r.movies || r.tv)) {
// Process Jellyseerr movies
if (r.movies && r.movies.length > 0) {
const jellyseerrMovies = r.movies.map(item => convertJellyseerrItem(item, 'movie'));
if (results.groupedItems['Movie']) {
results.groupedItems['Movie'] = [...results.groupedItems['Movie'], ...jellyseerrMovies];
} else {
results.groupedItems['Movie'] = jellyseerrMovies;
}
results.total += jellyseerrMovies.length;
}
// Process Jellyseerr TV shows
if (r.tv && r.tv.length > 0) {
const jellyseerrTv = r.tv.map(item => convertJellyseerrItem(item, 'tv'));
if (results.groupedItems['Series']) {
results.groupedItems['Series'] = [...results.groupedItems['Series'], ...jellyseerrTv];
} else {
results.groupedItems['Series'] = jellyseerrTv;
}
results.total += jellyseerrTv.length;
}
}
}
});
displaySmartResults(results, Math.round(end - start));
// Cache the results
cacheResults(searchTerm, searchType, results);
// Update URL with search query and type
const urlType = searchType === 'core' ? 'videos' : searchType;
updateSearchUrl(searchTerm, urlType);
// Hide loading spinner
setLoadingSpinner(false);
return results;
} catch (e) {
ERR('performSmartSearch error', e);
// Hide loading spinner on error too
setLoadingSpinner(false);
return results;
}
}
function createSmartItemElement(item) {
const serverId = ApiClient.serverId();
const div = document.createElement('div');
div.className = 'smart-search-item';
const img = document.createElement('img');
img.src = item.ImageTags?.Primary
? `${ApiClient.serverAddress()}/Items/${item.Id}/Images/Primary?tag=${item.ImageTags.Primary}&maxWidth=300`
: '/web/assets/img/icon-transparent.png';
img.alt = item.Name || 'Unknown';
img.onerror = () => { img.src = '/web/assets/img/icon-transparent.png'; };
const h = document.createElement('h3');
h.textContent = item.Name || 'Unknown';
h.title = h.textContent;
const p = document.createElement('p');
p.textContent = `${item.Type}${item.ProductionYear ? ` (${item.ProductionYear})` : ''}`;
p.title = p.textContent;
div.appendChild(img);
div.appendChild(h);
div.appendChild(p);
div.addEventListener('click', () => {
LOG('item click', { id: item.Id, name: item.Name });
try { Dashboard.navigate(`#!/details?id=${item.Id}&serverId=${serverId}`); } catch(e){ ERR(e); }
});
return div;
}
function getTypeDisplayName(itemType) {
const typeMap = {
'Movie': 'Movies',
'Series': 'TV Shows',
'Episode': 'Episodes',
'Person': 'People',
'MusicAlbum': 'Albums',
'Audio': 'Songs',
'Artist': 'Artists',
'Playlist': 'Playlists',
'Book': 'Books',
'AudioBook': 'Audiobooks',
'Photo': 'Photos',
'PhotoAlbum': 'Photo Albums',
'TvChannel': 'TV Channels',
'LiveTvProgram': 'Live TV',
'BoxSet': 'Collections'
};
return typeMap[itemType] || itemType;
}
// Create smart search results container lazily when needed
function ensureSmartResultsContainer() {
let smartResults = document.getElementById('smart-search-results');
if (!smartResults) {
LOG('Creating smart-search-results container');
smartResults = document.createElement('div');
smartResults.id = 'smart-search-results';
smartResults.className = 'smart-search-results emby-scroller';
const searchPage = document.getElementById('searchPage');
if (searchPage) {
searchPage.appendChild(smartResults);
} else {
// Fallback to body if #searchPage not found
document.body.appendChild(smartResults);
}
}
return smartResults;
}
function displaySmartResults(results, ms) {
const resultsContainer = ensureSmartResultsContainer();
const stats = document.getElementById('smart-search-stats');
if (!stats) return;
resultsContainer.innerHTML = '';
// Check if we have any grouped results
if (!results.groupedItems || Object.keys(results.groupedItems).length === 0) {
resultsContainer.innerHTML = '<p style="color:#999;text-align:center;">No results found</p>';
stats.textContent = `Search completed in ${ms}ms - 0 results`;
return;
}
// Create sections for each item type that has results
const frag = document.createDocumentFragment();
let totalShown = 0;
// Add hidden dummy section for testing/placeholder
const dummySection = document.createElement('div');
dummySection.className = 'verticalSection emby-scroller-container';
dummySection.style.display = 'none'; // Hidden dummy section
dummySection.setAttribute('data-dummy', 'true');
const dummyTitle = document.createElement('h2');
dummyTitle.className = 'sectionTitle sectionTitle-cards focuscontainer-x padded-left padded-right';
dummyTitle.textContent = 'Hidden Dummy Section';
dummySection.appendChild(dummyTitle);
frag.appendChild(dummySection);
// Define the order to display sections
const sectionOrder = ['Movie', 'Series', 'Episode', 'Person', 'MusicAlbum', 'Audio', 'Artist', 'Playlist', 'Book', 'AudioBook', 'Photo', 'PhotoAlbum', 'TvChannel', 'LiveTvProgram', 'BoxSet'];
sectionOrder.forEach((itemType, index) => {
if (results.groupedItems[itemType] && results.groupedItems[itemType].length > 0) {
const items = results.groupedItems[itemType];
const title = getTypeDisplayName(itemType);
const section = window.cardBuilder.renderCards(items, title, index);
frag.appendChild(section);
totalShown += items.length;
}
});
resultsContainer.appendChild(frag);
stats.textContent = `Search completed in ${ms}ms - ${results.total} results found`;
}
// main init
function initDualSearch() {
if (typeof ApiClient === 'undefined') { setTimeout(initDualSearch, 500); return; }
const originalInput = document.getElementById('searchTextInput');
if (!originalInput) { setTimeout(initDualSearch, 500); return; }
addCustomStyles();
// Handle URL query parameter on page load
// Parse query from hash since Jellyfin uses hash-based routing
let queryFromUrl = null;
if (window.location.hash.includes('search.html') && window.location.hash.includes('query=')) {
const hashQuery = window.location.hash.split('query=')[1];
if (hashQuery) {
queryFromUrl = hashQuery.split('&')[0]; // Get query before any other params
}
}
LOG('initDualSearch - URL query:', queryFromUrl);
LOG('initDualSearch - originalInput exists:', !!originalInput);
if (originalInput) {
LOG('initDualSearch - originalInput value:', originalInput.value);
}
if (queryFromUrl) {
LOG('initDualSearch - URL query detected, leaving input alone');
}
// create Smart search UI if not exist
let wrapper = document.getElementById('smart-search-wrapper');
if (!wrapper) {
wrapper = document.createElement('div');
wrapper.id = 'smart-search-wrapper';
wrapper.className = 'smart-search-wrapper';
const input = document.createElement('input');
input.id = 'smart-search-input';
input.className = 'smart-search-input emby-input searchfields-txtSearch';
input.type = 'text';
input.placeholder = 'Smart Search...';
const btnRow = document.createElement('div');
btnRow.className = 'smart-search-buttons';
const btnAll = document.createElement('button');
btnAll.id = 'smart-search-all';
btnAll.className = 'smart-search-secondary emby-button';
btnAll.textContent = 'All';
const btnCore = document.createElement('button');
btnCore.id = 'smart-search-core';
btnCore.className = 'smart-search-primary emby-button';
btnCore.textContent = 'Movies/TV';
const btnMusic = document.createElement('button');
btnMusic.id = 'smart-search-music';
btnMusic.className = 'smart-search-secondary emby-button';
btnMusic.textContent = 'Music';
const btnBooks = document.createElement('button');
btnBooks.id = 'smart-search-books';
btnBooks.className = 'smart-search-secondary emby-button';
btnBooks.textContent = 'Books';
btnRow.appendChild(btnAll);
btnRow.appendChild(btnCore);
btnRow.appendChild(btnMusic);
btnRow.appendChild(btnBooks);
const stats = document.createElement('div');
stats.id = 'smart-search-stats';
stats.className = 'smart-search-stats';
wrapper.appendChild(input);
wrapper.appendChild(btnRow);
wrapper.appendChild(stats);
originalInput.parentElement.insertBefore(wrapper, originalInput);
}
// Smart results container will be created lazily when needed
const smartInput = document.getElementById('smart-search-input');
const smartCoreBtn = document.getElementById('smart-search-core');
const smartMusicBtn = document.getElementById('smart-search-music');
const smartBooksBtn = document.getElementById('smart-search-books');
const smartAllBtn = document.getElementById('smart-search-all');
// Track current search type
let currentSearchType = 'videos';
// Function to update button states
function updateButtonStates(activeType) {
// Remove active class from all buttons
smartAllBtn.classList.remove('smart-search-primary');
smartAllBtn.classList.add('smart-search-secondary');
smartCoreBtn.classList.remove('smart-search-primary');
smartCoreBtn.classList.add('smart-search-secondary');
smartMusicBtn.classList.remove('smart-search-primary');
smartMusicBtn.classList.add('smart-search-secondary');
smartBooksBtn.classList.remove('smart-search-primary');
smartBooksBtn.classList.add('smart-search-secondary');
// Add active class to the selected button
if (activeType === 'all') {
smartAllBtn.classList.remove('smart-search-secondary');
smartAllBtn.classList.add('smart-search-primary');
} else if (activeType === 'videos') {
smartCoreBtn.classList.remove('smart-search-secondary');
smartCoreBtn.classList.add('smart-search-primary');
} else if (activeType === 'music') {
smartMusicBtn.classList.remove('smart-search-secondary');
smartMusicBtn.classList.add('smart-search-primary');
} else if (activeType === 'books') {
smartBooksBtn.classList.remove('smart-search-secondary');
smartBooksBtn.classList.add('smart-search-primary');
}
}
// Function to handle search type toggle
function setSearchType(type) {
LOG('setSearchType called:', { type, currentSearchType });
currentSearchType = type;
updateButtonStates(type);
// If there's a search term, check cache first
const searchTerm = smartInput.value.trim();
LOG('Search term from input:', searchTerm);
if (searchTerm) {
// Map URL type to internal type
const internalType = type === 'videos' ? 'core' : type;
LOG('Internal type mapping:', { type, internalType });
// Check if we have cached results for this search term and type
const cachedResults = getCachedResults(searchTerm, internalType);
if (cachedResults) {
LOG('Using cached results for', searchTerm, internalType);
displaySmartResults(cachedResults, 0); // 0ms since it's cached
// Update URL with search query and type even for cached results
const urlType = type === 'videos' ? 'videos' : type;
updateSearchUrl(searchTerm, urlType);
return;
}
// No cache hit, perform new search
LOG('No cache hit, performing new search');
performSmartSearch(searchTerm, internalType);
}
}
if (!wrapper.dataset.initialized) {
wrapper.dataset.initialized = 'true';
let timer = null;
const doSmartSearchDebounced = () => {
const term = (smartInput.value || '').trim();
if (!term) {
const resultsContainer = ensureSmartResultsContainer();
resultsContainer.innerHTML='';
setLoadingSpinner(false); // Hide spinner when clearing results
return;
}
if (timer) clearTimeout(timer);
const internalType = currentSearchType === 'videos' ? 'core' : currentSearchType;
timer = setTimeout(() => performSmartSearch(term, internalType), 300);
};
smartInput.addEventListener('input', doSmartSearchDebounced);
smartInput.addEventListener('keydown', (e)=>{ if(e.key==='Enter'){ e.preventDefault(); const internalType = currentSearchType === 'videos' ? 'core' : currentSearchType; performSmartSearch((smartInput.value||'').trim(), internalType); }});
smartCoreBtn.addEventListener('click', ()=>setSearchType('videos'));
smartMusicBtn.addEventListener('click', ()=>setSearchType('music'));
smartBooksBtn.addEventListener('click', ()=>setSearchType('books'));
smartAllBtn.addEventListener('click', ()=>setSearchType('all'));
// Parse type parameter from URL
let typeFromUrl = 'videos'; // default
if (window.location.hash.includes('type=')) {
const typeMatch = window.location.hash.match(/type=([^&]+)/);
if (typeMatch) {
typeFromUrl = typeMatch[1];
}
}
// Set initial search type and update UI
currentSearchType = typeFromUrl;
updateButtonStates(typeFromUrl);
// Handle URL query parameter - populate smart search and trigger search
if (queryFromUrl) {
smartInput.value = queryFromUrl;
// Trigger immediate search with the URL query
const internalType = typeFromUrl === 'videos' ? 'core' : typeFromUrl;
performSmartSearch(queryFromUrl, internalType);
}
}
// persistent toggle button
let toggleBtn = document.getElementById('persistent-toggle-btn');
if (!toggleBtn) {
toggleBtn = document.createElement('button');
toggleBtn.id = 'persistent-toggle-btn';
toggleBtn.textContent = 'Switch to Default Search';
const searchFields = document.querySelector('.searchFields');
if (searchFields) {
searchFields.appendChild(toggleBtn);
} else {
// Fallback to body if .searchFields not found
document.body.appendChild(toggleBtn);
}
}
let smartMode = true;
const setMode = (isSmart) => {
smartMode = isSmart;
LOG('setMode', smartMode);
if (smartMode) {
wrapper.style.display = '';
originalInput.style.display = 'none';
// Hide all search results containers
const searchResults = document.querySelectorAll('.searchResults,.padded-top,.padded-bottom-page');
searchResults.forEach(sr => sr.style.display = 'none');
const resultsContainer = ensureSmartResultsContainer();
resultsContainer.style.display = '';
toggleBtn.textContent='Switch to Default Search';
// Keep XHR blocking enabled for smart mode
shouldBlockXHR = true;
// Add smart-search-mode class to body for CSS targeting
document.body.classList.add('smart-search-mode');
} else {
wrapper.style.display = 'none';
originalInput.style.display = '';
// Show all search results containers
const searchResults = document.querySelectorAll('.searchResults,.padded-top,.padded-bottom-page');
searchResults.forEach(sr => sr.style.display = '');
const resultsContainer = ensureSmartResultsContainer();
resultsContainer.style.display = 'none';
toggleBtn.textContent='Switch to Smart Search';
// Disable XHR blocking for default search mode
shouldBlockXHR = false;
LOG('Disabled XHR blocking for default search mode');
// Remove smart-search-mode class from body
document.body.classList.remove('smart-search-mode');
// Input is left alone - let it keep its value
}
};
toggleBtn.addEventListener('click', ()=>setMode(!smartMode));
// initial mode
setMode(true);
}
function init() {
LOG('init() called');
LOG('Pathname:', window.location.pathname);
LOG('Hash:', window.location.hash);
if (window.location.pathname.includes('search.html') || window.location.hash.includes('search')) {
LOG('Search page detected');
// Check for URL query parameter and prevent default search early
// Parse query from hash since Jellyfin uses hash-based routing
let queryFromUrl = null;
if (window.location.hash.includes('search.html') && window.location.hash.includes('query=')) {
const hashQuery = window.location.hash.split('query=')[1];
if (hashQuery) {
queryFromUrl = hashQuery.split('&')[0]; // Get query before any other params
}
}
LOG('URL query in init:', queryFromUrl);
if (queryFromUrl) {
LOG('Query detected in init, enabling XHR blocking');
shouldBlockXHR = true;
// Also try to prevent search form submission
const searchForm = document.querySelector('form[action*="search"]');
LOG('Search form found:', !!searchForm);
if (searchForm) {
searchForm.addEventListener('submit', (e) => {
LOG('Search form submission prevented');
e.preventDefault();
e.stopPropagation();
return false;
});
}
// Try to hide search results containers immediately
const hideSearchResults = () => {
const searchResults = document.querySelectorAll('.searchResults,.padded-top,.padded-bottom-page');
LOG('Found search results containers:', searchResults.length);
searchResults.forEach(sr => {
sr.style.display = 'none';
LOG('Hidden search result container');
});
};
// Hide immediately and on DOM changes
hideSearchResults();
document.addEventListener('DOMContentLoaded', hideSearchResults);
// Use mutation observer to hide any new search results
const hideObserver = new MutationObserver(() => {
hideSearchResults();
});
hideObserver.observe(document.body, { childList: true, subtree: true });
// Stop observing after 10 seconds
setTimeout(() => {
LOG('Stopping hide observer after 10 seconds');
hideObserver.disconnect();
}, 10000);
}
LOG('Calling initDualSearch');
initDualSearch();
}
}
// Mutation observer to detect when search page content is loaded dynamically
function setupMutationObserver() {
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.type === 'childList') {
// Check if search input appears in the DOM
const searchInput = document.getElementById('searchTextInput');
if (searchInput && !document.getElementById('smart-search-wrapper')) {
LOG('Search input detected via mutation observer');
initDualSearch();
}
}
});
});
// Observe the entire document for changes
observer.observe(document.body, {
childList: true,
subtree: true
});
LOG('Mutation observer setup complete');
}
if (document.readyState==='loading') {
document.addEventListener('DOMContentLoaded', () => {
init();
setupMutationObserver();
});
} else {
init();
setupMutationObserver();
}
LOG('script load end');
})();