/** * Requestarr Settings - Settings and history management */ export class RequestarrSettings { constructor(core) { this.core = core; this.hiddenMediaControlsInitialized = false; this.hiddenMediaItems = []; this.blacklistedTvGenres = []; this.blacklistedMovieGenres = []; this.tvGenresForBlacklist = []; this.movieGenresForBlacklist = []; this.hiddenMediaState = { mediaType: null, instanceValue: '', searchQuery: '', page: 1, pageSize: 20 }; } // ======================================== // HISTORY // ======================================== async loadHistory() { const container = document.getElementById('history-list'); if (!container) return; container.innerHTML = '

Loading history...

'; try { const response = await fetch('./api/requestarr/history'); const data = await response.json(); if (data.requests && data.requests.length > 0) { container.innerHTML = ''; // Use Promise.all to wait for all async createHistoryItem calls const items = await Promise.all( data.requests.map(request => this.createHistoryItem(request)) ); items.forEach(item => container.appendChild(item)); } else { container.innerHTML = '

No request history

'; } } catch (error) { console.error('[RequestarrDiscover] Error loading history:', error); container.innerHTML = '

Failed to load history

'; } } async createHistoryItem(request) { const item = document.createElement('div'); item.className = 'history-item'; const posterUrl = request.poster_path || './static/images/no-poster.png'; const date = new Date(request.requested_at).toLocaleDateString(); item.innerHTML = `
${request.title}
${request.title} (${request.year || 'N/A'})
Requested to ${request.app_type === 'radarr' ? 'Radarr' : 'Sonarr'} - ${request.instance_name} on ${date}
Requested
`; // Load and cache image asynchronously if (posterUrl && !posterUrl.includes('./static/images/') && window.getCachedTMDBImage && window.tmdbImageCache) { try { const cachedUrl = await window.getCachedTMDBImage(posterUrl, window.tmdbImageCache); if (cachedUrl && cachedUrl !== posterUrl) { const imgElement = item.querySelector('.history-poster img'); if (imgElement) imgElement.src = cachedUrl; } } catch (err) { console.error('[RequestarrSettings] Failed to cache history image:', err); } } return item; } // ======================================== // HIDDEN MEDIA // ======================================== async loadHiddenMedia(mediaType = null, page = 1) { const container = document.getElementById('hidden-media-grid'); if (!container) { return; } this.initializeHiddenMediaControls(); const mediaTypeChanged = this.hiddenMediaState.mediaType !== mediaType; if (mediaTypeChanged) { this.hiddenMediaState.mediaType = mediaType; this.hiddenMediaState.page = 1; } else { this.hiddenMediaState.page = page; } // Reset grid display for normal content container.style.display = 'grid'; container.style.alignItems = ''; container.style.justifyContent = ''; container.innerHTML = '

Loading hidden media...

'; try { const fetchKey = `${mediaType || 'all'}`; if (this.hiddenMediaFetchKey !== fetchKey) { this.hiddenMediaFetchKey = fetchKey; // Fetch personal hidden media and global blacklist in parallel const [personalItems, globalItems] = await Promise.all([ this.fetchHiddenMediaItems(mediaType), this.fetchGlobalBlacklistItems(mediaType) ]); // Merge: mark personal items, then add global items that aren't already in personal list personalItems.forEach(item => { item._source = 'personal'; }); const personalKeys = new Set(personalItems.map(i => `${i.tmdb_id}:${i.media_type}`)); const mergedGlobal = globalItems .filter(gi => !personalKeys.has(`${gi.tmdb_id}:${gi.media_type}`)) .map(gi => ({ ...gi, _source: 'global_blacklist' })); // Mark personal items that are also globally blacklisted const globalKeys = new Set(globalItems.map(gi => `${gi.tmdb_id}:${gi.media_type}`)); personalItems.forEach(item => { if (globalKeys.has(`${item.tmdb_id}:${item.media_type}`)) { item._source = 'global_blacklist'; } }); this.hiddenMediaItems = [...personalItems, ...mergedGlobal]; } this.renderHiddenMediaPage(); } catch (error) { console.error('[RequestarrSettings] Error loading hidden media:', error); container.innerHTML = '

Failed to load hidden media.

'; } } initializeHiddenMediaControls() { if (this.hiddenMediaControlsInitialized) { return; } const searchInput = document.getElementById('hidden-media-search'); if (searchInput) { searchInput.addEventListener('input', (event) => { const value = event.target.value || ''; clearTimeout(this.hiddenMediaSearchTimeout); this.hiddenMediaSearchTimeout = setTimeout(() => { this.hiddenMediaState.searchQuery = value.trim(); this.hiddenMediaState.page = 1; this.renderHiddenMediaPage(); }, 200); }); } this.hiddenMediaControlsInitialized = true; } async fetchHiddenMediaItems(mediaType) { const allItems = []; const pageSize = 200; let currentPage = 1; let totalPages = 1; const maxPages = 50; while (currentPage <= totalPages && currentPage <= maxPages) { let url = `./api/requestarr/hidden-media?page=${currentPage}&page_size=${pageSize}`; if (mediaType) { url += `&media_type=${mediaType}`; } const response = await fetch(url); if (!response.ok) { throw new Error(`Hidden media API error: ${response.status}`); } const data = await response.json(); if (data.hidden_media && data.hidden_media.length > 0) { allItems.push(...data.hidden_media); } totalPages = data.total_pages || 1; currentPage += 1; } return allItems; } async fetchGlobalBlacklistItems(mediaType) { try { const resp = await fetch('./api/requestarr/requests/global-blacklist/ids'); if (!resp.ok) return []; const data = await resp.json(); let items = data.items || []; if (mediaType) { items = items.filter(i => i.media_type === mediaType); } return items.map(i => ({ tmdb_id: i.tmdb_id, media_type: i.media_type, title: i.title || '', poster_path: i.poster_path || '' })); } catch (err) { console.error('[RequestarrSettings] Error fetching global blacklist:', err); return []; } } getFilteredHiddenMedia() { const query = (this.hiddenMediaState.searchQuery || '').toLowerCase(); let filtered = this.hiddenMediaItems.slice(); if (query) { filtered = filtered.filter(item => (item.title || '').toLowerCase().includes(query)); } filtered.sort((a, b) => { const titleA = (a.title || '').toLowerCase(); const titleB = (b.title || '').toLowerCase(); return titleA.localeCompare(titleB); }); return filtered; } renderHiddenMediaPage() { const container = document.getElementById('hidden-media-grid'); const paginationContainer = document.getElementById('hidden-media-pagination'); if (!container || !paginationContainer) { return; } const filtered = this.getFilteredHiddenMedia(); const pageSize = this.hiddenMediaState.pageSize; const totalPages = Math.max(1, Math.ceil(filtered.length / pageSize)); if (this.hiddenMediaState.page > totalPages) { this.hiddenMediaState.page = 1; } const startIndex = (this.hiddenMediaState.page - 1) * pageSize; const pageItems = filtered.slice(startIndex, startIndex + pageSize); if (pageItems.length > 0) { container.style.display = 'grid'; container.style.alignItems = ''; container.style.justifyContent = ''; container.innerHTML = ''; pageItems.forEach(item => { container.appendChild(this.createHiddenMediaCard(item)); }); if (totalPages > 1) { paginationContainer.style.display = 'flex'; document.getElementById('hidden-page-info').textContent = `Page ${this.hiddenMediaState.page} of ${totalPages}`; document.getElementById('hidden-prev-page').disabled = this.hiddenMediaState.page === 1; document.getElementById('hidden-next-page').disabled = this.hiddenMediaState.page === totalPages; } else { paginationContainer.style.display = 'none'; } } else { container.style.display = 'flex'; container.style.alignItems = 'center'; container.style.justifyContent = 'center'; container.innerHTML = `

No Blacklisted Media

Items you blacklist will appear here. Blacklisted media is hidden across all instances.

`; paginationContainer.style.display = 'none'; } this.setupHiddenMediaPagination(totalPages); } setupHiddenMediaPagination(totalPages) { const prevBtn = document.getElementById('hidden-prev-page'); const nextBtn = document.getElementById('hidden-next-page'); if (!prevBtn || !nextBtn) { return; } prevBtn.onclick = () => { if (this.hiddenMediaState.page > 1) { this.hiddenMediaState.page -= 1; this.renderHiddenMediaPage(); } }; nextBtn.onclick = () => { if (this.hiddenMediaState.page < totalPages) { this.hiddenMediaState.page += 1; this.renderHiddenMediaPage(); } }; } createHiddenMediaCard(item) { const card = document.createElement('div'); card.className = 'media-card'; card.setAttribute('data-tmdb-id', item.tmdb_id); card.setAttribute('data-media-type', item.media_type); const posterUrl = item.poster_path || './static/images/blackout.jpg'; const typeBadgeLabel = item.media_type === 'tv' ? 'TV' : 'Movie'; const isGlobalBlacklist = item._source === 'global_blacklist'; const isOwner = window._huntarrUserRole === 'owner'; // Scope badge: globally blacklisted items get red badge, personal get purple let scopeBadge = ''; if (isGlobalBlacklist) { scopeBadge = 'Globally Blacklisted'; } else { scopeBadge = 'Personal Blacklist'; } // Only show unhide button if NOT globally blacklisted (or if owner and it's a personal hide) const showUnhide = !isGlobalBlacklist || (isOwner && item._source !== 'global_blacklist'); const year = item.year || item.release_year || 'N/A'; const rating = item.vote_average ? parseFloat(item.vote_average).toFixed(1) : 'N/A'; card.innerHTML = `
${showUnhide ? '' : ''} ${item.title} ${typeBadgeLabel} ${scopeBadge}
`; // Update image from cache in background (non-blocking) if (posterUrl && !posterUrl.includes('./static/images/') && window.getCachedTMDBImage && window.tmdbImageCache) { const imgEl = card.querySelector('.media-card-poster img'); if (imgEl) { window.getCachedTMDBImage(posterUrl, window.tmdbImageCache).then(cachedUrl => { if (cachedUrl && cachedUrl !== posterUrl) imgEl.src = cachedUrl; }).catch(() => {}); } } const unhideBtn = card.querySelector('.media-card-unhide-btn'); if (unhideBtn) { unhideBtn.addEventListener('click', async (e) => { e.stopPropagation(); await this.unhideMedia(item.tmdb_id, item.media_type, item.title, card); }); } return card; } async unhideMedia(tmdbId, mediaType, title, cardElement) { const self = this; const doUnhide = async function() { try { const response = await fetch(`./api/requestarr/hidden-media/${tmdbId}/${mediaType}`, { method: 'DELETE' }); if (!response.ok) { throw new Error('Failed to unhide media'); } // Remove from local cache and re-render self.hiddenMediaItems = self.hiddenMediaItems.filter(item => { return !(item.tmdb_id === tmdbId && item.media_type === mediaType); }); self.renderHiddenMediaPage(); console.log(`[RequestarrSettings] Unhidden media: ${title} (${mediaType})`); } catch (error) { console.error('[RequestarrSettings] Error unhiding media:', error); if (window.huntarrUI && window.huntarrUI.showNotification) window.huntarrUI.showNotification('Failed to unhide media. Please try again.', 'error'); } }; window.HuntarrConfirm.show({ title: 'Unblacklist Media', message: `Remove "${title}" from your personal blacklist? It will appear in discovery again.`, confirmLabel: 'Unblacklist', onConfirm: function() { doUnhide(); } }); } // ======================================== // SETTINGS // ======================================== async loadSettings() { // Load discover filters await this.loadDiscoverFilters(); // Load blacklisted genres and wire UI await this.loadBlacklistedGenres(); // Legacy per-section save buttons (kept for backward compat if present) const saveFiltersBtn = document.getElementById('save-discover-filters'); if (saveFiltersBtn) { saveFiltersBtn.onclick = () => this.saveDiscoverFilters(); } const saveBlacklistedBtn = document.getElementById('save-blacklisted-genres-btn'); if (saveBlacklistedBtn) { saveBlacklistedBtn.onclick = () => this.saveBlacklistedGenres(); } // Unified toolbar save button const self = this; window._reqsetSaveAll = async function () { const btn = document.getElementById('reqset-save-all-btn'); if (btn) { btn.disabled = true; btn.innerHTML = ' Saving...'; } try { await self.saveDiscoverFilters(true); await self.saveBlacklistedGenres(true); if (window.huntarrUI && window.huntarrUI.showNotification) { window.huntarrUI.showNotification('All settings saved', 'success'); } } catch (e) { console.error('[Requestarr Settings] Save all error:', e); if (window.huntarrUI && window.huntarrUI.showNotification) { window.huntarrUI.showNotification('Error saving settings', 'error'); } } finally { if (btn) { btn.disabled = false; btn.innerHTML = ' Save'; } } }; } async loadBlacklistedGenres() { const tvSelect = document.getElementById('blacklist-tv-genre-select'); const movieSelect = document.getElementById('blacklist-movie-genre-select'); if (!tvSelect || !movieSelect) return; try { const [tvRes, movieRes, blacklistedRes] = await Promise.all([ fetch('./api/requestarr/genres/tv'), fetch('./api/requestarr/genres/movie'), fetch('./api/requestarr/settings/blacklisted-genres') ]); const tvData = await tvRes.json(); const movieData = await movieRes.json(); const blacklistedData = await blacklistedRes.json(); this.tvGenresForBlacklist = tvData.genres || []; this.movieGenresForBlacklist = movieData.genres || []; const tvIds = (blacklistedData.blacklisted_tv_genres || []).map(id => parseInt(id, 10)); const movieIds = (blacklistedData.blacklisted_movie_genres || []).map(id => parseInt(id, 10)); this.blacklistedTvGenres = tvIds.map(id => { const g = this.tvGenresForBlacklist.find(x => x.id === id); return { id, name: (g && g.name) ? g.name : `Genre ${id}` }; }); this.blacklistedMovieGenres = movieIds.map(id => { const g = this.movieGenresForBlacklist.find(x => x.id === id); return { id, name: (g && g.name) ? g.name : `Genre ${id}` }; }); this.populateBlacklistedDropdowns(); this.renderBlacklistedPills(); tvSelect.onchange = () => { const val = tvSelect.value; if (!val) return; const id = parseInt(val, 10); const g = this.tvGenresForBlacklist.find(x => x.id === id); if (g && !this.blacklistedTvGenres.some(x => x.id === id)) { this.blacklistedTvGenres.push({ id: g.id, name: g.name }); this.renderBlacklistedPills(); this.populateBlacklistedDropdowns(); } tvSelect.value = ''; }; movieSelect.onchange = () => { const val = movieSelect.value; if (!val) return; const id = parseInt(val, 10); const g = this.movieGenresForBlacklist.find(x => x.id === id); if (g && !this.blacklistedMovieGenres.some(x => x.id === id)) { this.blacklistedMovieGenres.push({ id: g.id, name: g.name }); this.renderBlacklistedPills(); this.populateBlacklistedDropdowns(); } movieSelect.value = ''; }; } catch (error) { console.error('[RequestarrDiscover] Error loading blacklisted genres:', error); } } populateBlacklistedDropdowns() { const tvSelect = document.getElementById('blacklist-tv-genre-select'); const movieSelect = document.getElementById('blacklist-movie-genre-select'); if (!tvSelect || !movieSelect) return; const tvIds = this.blacklistedTvGenres.map(g => g.id); const movieIds = this.blacklistedMovieGenres.map(g => g.id); tvSelect.innerHTML = ''; this.tvGenresForBlacklist.filter(g => !tvIds.includes(g.id)).forEach(g => { const opt = document.createElement('option'); opt.value = g.id; opt.textContent = g.name; tvSelect.appendChild(opt); }); movieSelect.innerHTML = ''; this.movieGenresForBlacklist.filter(g => !movieIds.includes(g.id)).forEach(g => { const opt = document.createElement('option'); opt.value = g.id; opt.textContent = g.name; movieSelect.appendChild(opt); }); } renderBlacklistedPills() { const tvList = document.getElementById('blacklisted-tv-genres-list'); const movieList = document.getElementById('blacklisted-movie-genres-list'); if (!tvList || !movieList) return; tvList.innerHTML = ''; this.blacklistedTvGenres.forEach(g => { const pill = document.createElement('span'); pill.className = 'blacklisted-genre-pill'; pill.innerHTML = `×${g.name}`; pill.querySelector('.remove-pill').onclick = () => { this.blacklistedTvGenres = this.blacklistedTvGenres.filter(x => x.id !== g.id); this.renderBlacklistedPills(); this.populateBlacklistedDropdowns(); }; tvList.appendChild(pill); }); movieList.innerHTML = ''; this.blacklistedMovieGenres.forEach(g => { const pill = document.createElement('span'); pill.className = 'blacklisted-genre-pill'; pill.innerHTML = `×${g.name}`; pill.querySelector('.remove-pill').onclick = () => { this.blacklistedMovieGenres = this.blacklistedMovieGenres.filter(x => x.id !== g.id); this.renderBlacklistedPills(); this.populateBlacklistedDropdowns(); }; movieList.appendChild(pill); }); } async saveBlacklistedGenres(silent = false) { const btn = document.getElementById('save-blacklisted-genres-btn'); if (btn) { btn.disabled = true; btn.innerHTML = ' Saving...'; } try { const response = await fetch('./api/requestarr/settings/blacklisted-genres', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ blacklisted_tv_genres: this.blacklistedTvGenres.map(g => g.id), blacklisted_movie_genres: this.blacklistedMovieGenres.map(g => g.id) }) }); const data = await response.json(); if (data.success) { if (!silent) { this.core.showNotification('Blacklisted genres saved.', 'success'); } } else { this.core.showNotification('Failed to save blacklisted genres', 'error'); } } catch (error) { console.error('[RequestarrDiscover] Error saving blacklisted genres:', error); this.core.showNotification('Failed to save blacklisted genres', 'error'); } finally { if (btn) { btn.disabled = false; btn.innerHTML = ' Save Blacklisted Genres'; } } } async loadDefaultInstances() { const { encodeInstanceValue, decodeInstanceValue } = await import('./requestarr-core.js'); const movieSelect = document.getElementById('default-movie-instance'); const tvSelect = document.getElementById('default-tv-instance'); if (!movieSelect || !tvSelect) return; try { // Load Movie Hunt instances const _ts = Date.now(); const movieHuntResponse = await fetch(`./api/requestarr/instances/movie_hunt?t=${_ts}`, { cache: 'no-store' }); const movieHuntData = await movieHuntResponse.json(); // Load Radarr instances const radarrResponse = await fetch(`./api/requestarr/instances/radarr?t=${_ts}`, { cache: 'no-store' }); const radarrData = await radarrResponse.json(); // Load Sonarr instances const sonarrResponse = await fetch(`./api/requestarr/instances/sonarr?t=${_ts}`, { cache: 'no-store' }); const sonarrData = await sonarrResponse.json(); // Load saved defaults const defaultsResponse = await fetch('./api/requestarr/settings/default-instances'); const defaultsData = await defaultsResponse.json(); let needsAutoSave = false; // Build combined movie instances list: Movie Hunt first, then Radarr const movieHuntInstances = (movieHuntData.instances || []); const radarrInstances = (radarrData.instances || []); const allMovieInstances = []; // Add Movie Hunt instances at the top movieHuntInstances.forEach(inst => { allMovieInstances.push({ value: encodeInstanceValue('movie_hunt', inst.name), label: `Movie Hunt - ${inst.name}`, appType: 'movie_hunt', name: inst.name }); }); // Add Radarr instances below radarrInstances.forEach(inst => { allMovieInstances.push({ value: encodeInstanceValue('radarr', inst.name), label: `Radarr - ${inst.name}`, appType: 'radarr', name: inst.name }); }); // Populate movie instances dropdown if (allMovieInstances.length > 0) { movieSelect.innerHTML = ''; allMovieInstances.forEach(inst => { const option = document.createElement('option'); option.value = inst.value; option.textContent = inst.label; movieSelect.appendChild(option); }); // Set selection: saved default or first instance (never leave blank) const savedMovie = defaultsData.success && defaultsData.defaults && defaultsData.defaults.movie_instance; if (savedMovie) { // Check if the saved value exists in our dropdown options // Support both new compound format and legacy plain name format let foundMatch = false; if (allMovieInstances.some(i => i.value === savedMovie)) { movieSelect.value = savedMovie; foundMatch = true; } else { // Backward compat: try matching legacy value (plain Radarr name without prefix) const legacyMatch = allMovieInstances.find(i => i.appType === 'radarr' && i.name === savedMovie); if (legacyMatch) { movieSelect.value = legacyMatch.value; foundMatch = true; needsAutoSave = true; // Re-save in new format } } if (!foundMatch) { movieSelect.value = allMovieInstances[0].value; needsAutoSave = true; } } else { movieSelect.value = allMovieInstances[0].value; needsAutoSave = true; } } else { movieSelect.innerHTML = ''; } // Populate TV instances (Sonarr only - unchanged) if (sonarrData.instances && sonarrData.instances.length > 0) { tvSelect.innerHTML = ''; sonarrData.instances.forEach(instance => { const option = document.createElement('option'); option.value = instance.name; option.textContent = `Sonarr - ${instance.name}`; tvSelect.appendChild(option); }); // Set selection: saved default or first instance (never leave blank) const savedTV = defaultsData.success && defaultsData.defaults && defaultsData.defaults.tv_instance; const tvExists = savedTV && sonarrData.instances.some(i => i.name === defaultsData.defaults.tv_instance); if (savedTV && tvExists) { tvSelect.value = defaultsData.defaults.tv_instance; } else { tvSelect.value = sonarrData.instances[0].name; needsAutoSave = true; } } else { tvSelect.innerHTML = ''; } // Ensure neither dropdown is ever blank when instances exist if (allMovieInstances.length > 0 && !movieSelect.value) { movieSelect.value = allMovieInstances[0].value; needsAutoSave = true; } if (sonarrData.instances && sonarrData.instances.length > 0 && !tvSelect.value) { tvSelect.value = sonarrData.instances[0].name; needsAutoSave = true; } // Auto-save if we selected first instances if (needsAutoSave) { console.log('[RequestarrSettings] Auto-saving first available instances as defaults'); await this.saveDefaultInstances(true); // Pass silent flag } } catch (error) { console.error('[RequestarrDiscover] Error loading default instances:', error); } } async saveDefaultInstances(silent = false) { const movieSelect = document.getElementById('default-movie-instance'); const tvSelect = document.getElementById('default-tv-instance'); const saveBtn = document.getElementById('save-default-instances'); if (!movieSelect || !tvSelect) return; if (saveBtn && !silent) { saveBtn.disabled = true; saveBtn.innerHTML = ' Saving...'; } try { const response = await fetch('./api/requestarr/settings/default-instances', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ movie_instance: movieSelect.value || '', tv_instance: tvSelect.value || '' }) }); const data = await response.json(); if (data.success) { if (!silent) { this.core.showNotification('Default instances saved! Reloading discovery content...', 'success'); await this.loadDefaultRootFolders(); await new Promise(resolve => setTimeout(resolve, 1000)); this.core.content.loadDiscoverContent(); } } else { if (!silent) { this.core.showNotification('Failed to save default instances', 'error'); } } } catch (error) { console.error('[RequestarrDiscover] Error saving default instances:', error); if (!silent) { this.core.showNotification('Failed to save default instances', 'error'); } } finally { if (saveBtn && !silent) { saveBtn.disabled = false; saveBtn.innerHTML = ' Save Default Instances'; } } } /** Default root folders per app (issue #806) */ async loadDefaultRootFolders() { const { decodeInstanceValue } = await import('./requestarr-core.js'); const radarrSelect = document.getElementById('default-root-folder-radarr'); const sonarrSelect = document.getElementById('default-root-folder-sonarr'); const movieInstanceSelect = document.getElementById('default-movie-instance'); const tvInstanceSelect = document.getElementById('default-tv-instance'); if (!radarrSelect || !sonarrSelect) return; // Prevent concurrent calls (race condition protection) if (this._loadingRootFolders) { console.log('[RequestarrSettings] loadDefaultRootFolders already in progress, skipping'); return; } this._loadingRootFolders = true; try { const defaultsRes = await fetch('./api/requestarr/settings/default-instances'); const rootFoldersRes = await fetch('./api/requestarr/settings/default-root-folders'); const defaultsData = await defaultsRes.json(); const savedRootData = rootFoldersRes.ok ? await rootFoldersRes.json() : {}; // Decode the movie instance compound value to get app type and name // Prioritize the current dropdown value (user may have just changed it) over saved default const movieInstanceRaw = (movieInstanceSelect && movieInstanceSelect.value) || (defaultsData.defaults && defaultsData.defaults.movie_instance) || ''; const tvInstance = (tvInstanceSelect && tvInstanceSelect.value) || (defaultsData.defaults && defaultsData.defaults.tv_instance) || ''; const movieDecoded = decodeInstanceValue(movieInstanceRaw); const movieAppType = movieDecoded.appType; // 'movie_hunt' or 'radarr' const movieInstanceName = movieDecoded.name; // Update the root folder label dynamically based on instance type const radarrLabel = document.querySelector('label[for="default-root-folder-radarr"]'); if (radarrLabel) { radarrLabel.textContent = movieAppType === 'movie_hunt' ? 'Default Root Folder (Movie Hunt)' : 'Default Root Folder (Radarr)'; } // Determine which saved path to use const savedMoviePath = movieAppType === 'movie_hunt' ? (savedRootData.default_root_folder_movie_hunt || '').trim() : (savedRootData.default_root_folder_radarr || '').trim(); const savedSonarrPath = (savedRootData.default_root_folder_sonarr || '').trim(); const fallbackLabel = movieAppType === 'movie_hunt' ? 'Movie Hunt' : 'Radarr'; // Movie root folders (from Radarr or Movie Hunt, depending on instance type) if (movieInstanceName) { const rfRes = await fetch(`./api/requestarr/rootfolders?app_type=${movieAppType}&instance_name=${encodeURIComponent(movieInstanceName)}`); const rfData = await rfRes.json(); console.log(`[RequestarrSettings] ${fallbackLabel} API returned`, rfData.root_folders?.length || 0, 'root folders'); if (rfData.success && rfData.root_folders && rfData.root_folders.length > 0) { // Use Map to dedupe by normalized path, keeping first occurrence const seenPaths = new Map(); rfData.root_folders.forEach(rf => { if (!rf || !rf.path) return; const originalPath = rf.path.trim(); const normalized = originalPath.replace(/\/+$/, '').toLowerCase(); if (!normalized) return; if (!seenPaths.has(normalized)) { seenPaths.set(normalized, { path: originalPath, freeSpace: rf.freeSpace }); } }); console.log(`[RequestarrSettings] After deduplication: ${seenPaths.size} unique ${fallbackLabel} root folders`); if (seenPaths.size === 0) { radarrSelect.innerHTML = ``; } else { radarrSelect.innerHTML = ''; seenPaths.forEach(rf => { const opt = document.createElement('option'); opt.value = rf.path; opt.textContent = rf.path + (rf.freeSpace != null ? ` (${Math.round(rf.freeSpace / 1e9)} GB free)` : ''); radarrSelect.appendChild(opt); }); if (savedMoviePath) radarrSelect.value = savedMoviePath; } } else { radarrSelect.innerHTML = ``; } } else { radarrSelect.innerHTML = ``; } // Sonarr root folders with bulletproof deduplication (unchanged) if (tvInstance) { const sfRes = await fetch(`./api/requestarr/rootfolders?app_type=sonarr&instance_name=${encodeURIComponent(tvInstance)}`); const sfData = await sfRes.json(); console.log('[RequestarrSettings] Sonarr API returned', sfData.root_folders?.length || 0, 'root folders'); if (sfData.success && sfData.root_folders && sfData.root_folders.length > 0) { const seenPaths = new Map(); sfData.root_folders.forEach(rf => { if (!rf || !rf.path) return; const originalPath = rf.path.trim(); const normalized = originalPath.replace(/\/+$/, '').toLowerCase(); if (!normalized) return; if (!seenPaths.has(normalized)) { seenPaths.set(normalized, { path: originalPath, freeSpace: rf.freeSpace }); } }); console.log('[RequestarrSettings] After deduplication:', seenPaths.size, 'unique Sonarr root folders'); if (seenPaths.size === 0) { sonarrSelect.innerHTML = ''; } else { sonarrSelect.innerHTML = ''; seenPaths.forEach(rf => { const opt = document.createElement('option'); opt.value = rf.path; opt.textContent = rf.path + (rf.freeSpace != null ? ` (${Math.round(rf.freeSpace / 1e9)} GB free)` : ''); sonarrSelect.appendChild(opt); }); if (savedSonarrPath) sonarrSelect.value = savedSonarrPath; } } else { sonarrSelect.innerHTML = ''; } } else { sonarrSelect.innerHTML = ''; } } catch (error) { console.error('[RequestarrSettings] Error loading default root folders:', error); radarrSelect.innerHTML = ''; sonarrSelect.innerHTML = ''; } finally { this._loadingRootFolders = false; } } async saveDefaultRootFolders() { const { decodeInstanceValue } = await import('./requestarr-core.js'); const radarrSelect = document.getElementById('default-root-folder-radarr'); const sonarrSelect = document.getElementById('default-root-folder-sonarr'); const movieInstanceSelect = document.getElementById('default-movie-instance'); const saveBtn = document.getElementById('save-default-root-folders'); if (!radarrSelect || !sonarrSelect) return; if (saveBtn) { saveBtn.disabled = true; saveBtn.innerHTML = ' Saving...'; } try { // Determine if the movie instance is Movie Hunt or Radarr const movieInstanceVal = movieInstanceSelect ? movieInstanceSelect.value : ''; const movieDecoded = decodeInstanceValue(movieInstanceVal); const body = { default_root_folder_sonarr: sonarrSelect.value || '' }; // Save the root folder path under the correct key based on instance type if (movieDecoded.appType === 'movie_hunt') { body.default_root_folder_movie_hunt = radarrSelect.value || ''; } else { body.default_root_folder_radarr = radarrSelect.value || ''; } const response = await fetch('./api/requestarr/settings/default-root-folders', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }); const data = await response.json(); if (data.success) { this.core.showNotification('Default root folders saved.', 'success'); } else { this.core.showNotification('Failed to save default root folders', 'error'); } } catch (error) { console.error('[RequestarrSettings] Error saving default root folders:', error); this.core.showNotification('Failed to save default root folders', 'error'); } finally { if (saveBtn) { saveBtn.disabled = false; saveBtn.innerHTML = ' Save Default Root Folders'; } } } async loadDiscoverFilters() { // Load regions - Full TMDB region list const regions = [ { code: '', name: 'All Regions', flag: '🌐' }, { code: 'AR', name: 'Argentina', flag: '🇦🇷' }, { code: 'AU', name: 'Australia', flag: '🇦🇺' }, { code: 'AT', name: 'Austria', flag: '🇦🇹' }, { code: 'BE', name: 'Belgium', flag: '🇧🇪' }, { code: 'BR', name: 'Brazil', flag: '🇧🇷' }, { code: 'CA', name: 'Canada', flag: '🇨🇦' }, { code: 'CL', name: 'Chile', flag: '🇨🇱' }, { code: 'CN', name: 'China', flag: '🇨🇳' }, { code: 'CO', name: 'Colombia', flag: '🇨🇴' }, { code: 'CZ', name: 'Czech Republic', flag: '🇨🇿' }, { code: 'DK', name: 'Denmark', flag: '🇩🇰' }, { code: 'FI', name: 'Finland', flag: '🇫🇮' }, { code: 'FR', name: 'France', flag: '🇫🇷' }, { code: 'DE', name: 'Germany', flag: '🇩🇪' }, { code: 'GR', name: 'Greece', flag: '🇬🇷' }, { code: 'HK', name: 'Hong Kong', flag: '🇭🇰' }, { code: 'HU', name: 'Hungary', flag: '🇭🇺' }, { code: 'IS', name: 'Iceland', flag: '🇮🇸' }, { code: 'IN', name: 'India', flag: '🇮🇳' }, { code: 'ID', name: 'Indonesia', flag: '🇮🇩' }, { code: 'IE', name: 'Ireland', flag: '🇮🇪' }, { code: 'IL', name: 'Israel', flag: '🇮🇱' }, { code: 'IT', name: 'Italy', flag: '🇮🇹' }, { code: 'JP', name: 'Japan', flag: '🇯🇵' }, { code: 'KR', name: 'South Korea', flag: '🇰🇷' }, { code: 'MY', name: 'Malaysia', flag: '🇲🇾' }, { code: 'MX', name: 'Mexico', flag: '🇲🇽' }, { code: 'NL', name: 'Netherlands', flag: '🇳🇱' }, { code: 'NZ', name: 'New Zealand', flag: '🇳🇿' }, { code: 'NO', name: 'Norway', flag: '🇳🇴' }, { code: 'PH', name: 'Philippines', flag: '🇵🇭' }, { code: 'PL', name: 'Poland', flag: '🇵🇱' }, { code: 'PT', name: 'Portugal', flag: '🇵🇹' }, { code: 'RO', name: 'Romania', flag: '🇷🇴' }, { code: 'RU', name: 'Russia', flag: '🇷🇺' }, { code: 'SA', name: 'Saudi Arabia', flag: '🇸🇦' }, { code: 'SG', name: 'Singapore', flag: '🇸🇬' }, { code: 'ZA', name: 'South Africa', flag: '🇿🇦' }, { code: 'ES', name: 'Spain', flag: '🇪🇸' }, { code: 'SE', name: 'Sweden', flag: '🇸🇪' }, { code: 'CH', name: 'Switzerland', flag: '🇨🇭' }, { code: 'TW', name: 'Taiwan', flag: '🇹🇼' }, { code: 'TH', name: 'Thailand', flag: '🇹🇭' }, { code: 'TR', name: 'Turkey', flag: '🇹🇷' }, { code: 'UA', name: 'Ukraine', flag: '🇺🇦' }, { code: 'AE', name: 'United Arab Emirates', flag: '🇦🇪' }, { code: 'GB', name: 'United Kingdom', flag: '🇬🇧' }, { code: 'US', name: 'United States', flag: '🇺🇸' } ]; // Keep All Regions at top, sort the rest alphabetically const allRegions = regions[0]; const otherRegions = regions.slice(1).sort((a, b) => a.name.localeCompare(b.name)); this.regions = [allRegions, ...otherRegions]; this.selectedRegion = 'US'; // Default // Initialize custom region select this.initializeRegionSelect(); // Initialize language multi-select this.initializeLanguageSelect(); // Initialize provider multi-select this.initializeProviderSelect(); // Load saved filters try { const response = await fetch('./api/requestarr/settings/filters'); const data = await response.json(); if (data.success && data.filters) { if (data.filters.region !== undefined) { this.selectedRegion = data.filters.region; this.updateRegionDisplay(); } if (data.filters.languages && data.filters.languages.length > 0) { this.selectedLanguages = data.filters.languages; } else { this.selectedLanguages = []; } this.renderLanguageTags(); if (data.filters.providers && data.filters.providers.length > 0) { this.selectedProviders = data.filters.providers; } else { this.selectedProviders = []; } } else { // No saved filters - default to US and All Languages this.selectedRegion = 'US'; this.updateRegionDisplay(); this.selectedLanguages = []; this.renderLanguageTags(); this.selectedProviders = []; } } catch (error) { console.error('[RequestarrDiscover] Error loading discover filters:', error); // On error, default to US and All Languages this.selectedRegion = 'US'; this.updateRegionDisplay(); this.selectedLanguages = []; this.renderLanguageTags(); this.selectedProviders = []; } await this.loadProviders(this.selectedRegion); } initializeRegionSelect() { const display = document.getElementById('region-select-display'); const dropdown = document.getElementById('region-dropdown'); const list = document.getElementById('region-list'); if (!display || !dropdown || !list) { return; } // Check if already initialized if (this.regionSelectInitialized) { return; } // Populate region list first this.renderRegionList(); // Toggle dropdown - Direct approach display.onclick = (e) => { e.stopPropagation(); e.preventDefault(); if (dropdown.style.display === 'none' || !dropdown.style.display) { dropdown.style.display = 'block'; display.classList.add('open'); } else { dropdown.style.display = 'none'; display.classList.remove('open'); } }; // Prevent dropdown from closing when clicking inside it dropdown.onclick = (e) => { e.stopPropagation(); }; // Close dropdown when clicking outside document.addEventListener('click', (e) => { if (!display.contains(e.target) && !dropdown.contains(e.target)) { dropdown.style.display = 'none'; display.classList.remove('open'); } }); this.regionSelectInitialized = true; } renderRegionList(filter = '') { const list = document.getElementById('region-list'); if (!list) return; const filteredRegions = this.regions.filter(region => region.name.toLowerCase().includes(filter) ); list.innerHTML = ''; filteredRegions.forEach(region => { const option = document.createElement('div'); option.className = 'custom-select-option'; option.textContent = `${region.flag} ${region.name}`; option.dataset.code = region.code; if (this.selectedRegion === region.code) { option.classList.add('selected'); } option.onclick = (e) => { e.stopPropagation(); this.selectedRegion = region.code; this.updateRegionDisplay(); this.renderRegionList(); // Re-render to update selected state document.getElementById('region-dropdown').style.display = 'none'; document.getElementById('region-select-display').classList.remove('open'); this.handleRegionChange(); }; list.appendChild(option); }); } updateRegionDisplay() { const selectedText = document.getElementById('region-selected-text'); if (!selectedText) return; const region = this.regions.find(r => r.code === this.selectedRegion); if (region) { selectedText.textContent = `${region.flag} ${region.name}`; } } initializeLanguageSelect() { const input = document.getElementById('discover-language'); const dropdown = document.getElementById('language-dropdown'); const languageList = document.getElementById('language-list'); if (!input || !dropdown || !languageList) { return; } // Check if already initialized if (this.languageSelectInitialized) { return; } this.selectedLanguages = this.selectedLanguages || []; // Common languages list this.languages = [ { code: 'ar', name: 'Arabic' }, { code: 'zh', name: 'Chinese' }, { code: 'da', name: 'Danish' }, { code: 'nl', name: 'Dutch' }, { code: 'en', name: 'English' }, { code: 'fi', name: 'Finnish' }, { code: 'fr', name: 'French' }, { code: 'de', name: 'German' }, { code: 'hi', name: 'Hindi' }, { code: 'it', name: 'Italian' }, { code: 'ja', name: 'Japanese' }, { code: 'ko', name: 'Korean' }, { code: 'no', name: 'Norwegian' }, { code: 'pl', name: 'Polish' }, { code: 'pt', name: 'Portuguese' }, { code: 'ru', name: 'Russian' }, { code: 'es', name: 'Spanish' }, { code: 'sv', name: 'Swedish' }, { code: 'th', name: 'Thai' }, { code: 'tr', name: 'Turkish' } ]; // Populate language list this.renderLanguageList(); // Toggle dropdown input.onclick = (e) => { e.stopPropagation(); const isVisible = dropdown.style.display === 'block'; dropdown.style.display = isVisible ? 'none' : 'block'; }; // Close dropdown when clicking outside document.addEventListener('click', (e) => { if (!dropdown.contains(e.target) && e.target !== input) { dropdown.style.display = 'none'; } }); this.languageSelectInitialized = true; } initializeProviderSelect() { const input = document.getElementById('discover-providers'); const dropdown = document.getElementById('provider-dropdown'); const providerList = document.getElementById('provider-list'); if (!input || !dropdown || !providerList) { return; } if (this.providerSelectInitialized) { return; } this.selectedProviders = this.selectedProviders || []; this.providers = this.providers || []; this.renderProviderList(); this.renderProviderTags(); input.onclick = (e) => { e.stopPropagation(); const isVisible = dropdown.style.display === 'block'; dropdown.style.display = isVisible ? 'none' : 'block'; }; document.addEventListener('click', (e) => { if (!dropdown.contains(e.target) && e.target !== input) { dropdown.style.display = 'none'; } }); this.providerSelectInitialized = true; } renderLanguageList(filter = '') { const languageList = document.getElementById('language-list'); if (!languageList) return; languageList.innerHTML = ''; const normalizedFilter = filter.trim().toLowerCase(); const showAllLanguages = !normalizedFilter || 'all languages'.includes(normalizedFilter); if (showAllLanguages) { const allItem = document.createElement('div'); allItem.className = 'language-item'; allItem.textContent = 'All Languages'; allItem.dataset.code = ''; if (this.selectedLanguages.length === 0) { allItem.classList.add('selected'); } allItem.addEventListener('click', () => { this.selectedLanguages = []; this.renderLanguageTags(); this.renderLanguageList(filter); const dropdown = document.getElementById('language-dropdown'); if (dropdown) { dropdown.style.display = 'none'; } }); languageList.appendChild(allItem); } this.languages.forEach(lang => { if (normalizedFilter && !lang.name.toLowerCase().includes(normalizedFilter)) { return; } const item = document.createElement('div'); item.className = 'language-item'; item.textContent = lang.name; item.dataset.code = lang.code; if (this.selectedLanguages.includes(lang.code)) { item.classList.add('selected'); } item.addEventListener('click', () => { const code = item.dataset.code; const index = this.selectedLanguages.indexOf(code); if (index > -1) { this.selectedLanguages.splice(index, 1); item.classList.remove('selected'); } else { this.selectedLanguages.push(code); item.classList.add('selected'); } this.renderLanguageTags(); // Close dropdown after selection const dropdown = document.getElementById('language-dropdown'); if (dropdown) { dropdown.style.display = 'none'; } }); languageList.appendChild(item); }); } renderLanguageTags() { const tagsContainer = document.getElementById('language-tags'); if (!tagsContainer) return; tagsContainer.innerHTML = ''; if (this.selectedLanguages.length === 0) { // Show "All Languages" as a tag/bubble instead of plain text const tag = document.createElement('div'); tag.className = 'language-tag'; tag.innerHTML = 'All Languages'; tag.style.cursor = 'default'; // No remove action for "All Languages" tagsContainer.appendChild(tag); return; } this.selectedLanguages.forEach(code => { const lang = this.languages.find(l => l.code === code); if (!lang) return; const tag = document.createElement('div'); tag.className = 'language-tag'; tag.innerHTML = ` ${lang.name} × `; tag.querySelector('.language-tag-remove').addEventListener('click', (e) => { e.stopPropagation(); const removeCode = e.target.dataset.code; this.selectedLanguages = this.selectedLanguages.filter(c => c !== removeCode); this.renderLanguageTags(); this.renderLanguageList(); }); tagsContainer.appendChild(tag); }); } async loadProviders(region) { try { const response = await fetch(`./api/requestarr/watch-providers/movie?region=${encodeURIComponent(region || '')}`); const data = await response.json(); this.providers = data.providers || []; const available = new Set(this.providers.map(provider => String(provider.provider_id))); this.selectedProviders = (this.selectedProviders || []).filter(code => available.has(code)); } catch (error) { console.error('[RequestarrDiscover] Error loading watch providers:', error); this.providers = []; } this.renderProviderList(); this.renderProviderTags(); } renderProviderList() { const providerList = document.getElementById('provider-list'); if (!providerList) return; providerList.innerHTML = ''; if (!this.providers || this.providers.length === 0) { providerList.innerHTML = '
No providers found
'; return; } this.providers.forEach(provider => { const providerId = String(provider.provider_id); const item = document.createElement('div'); item.className = 'language-item'; item.textContent = provider.provider_name; item.dataset.code = providerId; if (this.selectedProviders.includes(providerId)) { item.classList.add('selected'); } item.addEventListener('click', () => { const code = item.dataset.code; const index = this.selectedProviders.indexOf(code); if (index > -1) { this.selectedProviders.splice(index, 1); item.classList.remove('selected'); } else { this.selectedProviders.push(code); item.classList.add('selected'); } this.renderProviderTags(); const dropdown = document.getElementById('provider-dropdown'); if (dropdown) { dropdown.style.display = 'none'; } }); providerList.appendChild(item); }); } renderProviderTags() { const tagsContainer = document.getElementById('provider-tags'); if (!tagsContainer) return; tagsContainer.innerHTML = ''; if (!this.selectedProviders || this.selectedProviders.length === 0) { // Show "All Providers" as a tag/bubble instead of plain text const tag = document.createElement('div'); tag.className = 'language-tag'; tag.innerHTML = 'All Providers'; tag.style.cursor = 'default'; // No remove action for "All Providers" tagsContainer.appendChild(tag); return; } this.selectedProviders.forEach(code => { const provider = (this.providers || []).find(p => String(p.provider_id) === code); if (!provider) return; const tag = document.createElement('div'); tag.className = 'language-tag'; tag.innerHTML = ` ${provider.provider_name} × `; tag.querySelector('.language-tag-remove').addEventListener('click', (e) => { e.stopPropagation(); const removeCode = e.target.dataset.code; this.selectedProviders = this.selectedProviders.filter(c => c !== removeCode); this.renderProviderTags(); this.renderProviderList(); }); tagsContainer.appendChild(tag); }); } handleRegionChange() { this.selectedProviders = []; this.renderProviderTags(); this.renderProviderList(); this.loadProviders(this.selectedRegion); } async saveDiscoverFilters(silent = false) { const saveBtn = document.getElementById('save-discover-filters'); if (saveBtn) { saveBtn.disabled = true; saveBtn.innerHTML = ' Saving...'; } try { const response = await fetch('./api/requestarr/settings/filters', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ region: this.selectedRegion || '', languages: this.selectedLanguages || [], providers: this.selectedProviders || [] }) }); const data = await response.json(); if (data.success) { if (!silent) { this.core.showNotification('Filters saved! Reloading discover content...', 'success'); } // Reload all discover content with new filters setTimeout(() => { this.core.content.loadDiscoverContent(); }, 500); } else { this.core.showNotification('Failed to save discover filters', 'error'); } } catch (error) { console.error('[RequestarrDiscover] Error saving discover filters:', error); this.core.showNotification('Failed to save discover filters', 'error'); } finally { if (saveBtn) { saveBtn.disabled = false; saveBtn.innerHTML = ' Save Filters'; } } } // ======================================== // SMART HUNT SETTINGS // ======================================== async loadSmartHuntSettings() { try { const resp = await fetch('./api/requestarr/settings/smarthunt'); const data = await resp.json(); if (!data.success || !data.settings) return; const s = data.settings; // Populate toggles const hideLibEl = document.getElementById('smarthunt-hide-library'); if (hideLibEl) hideLibEl.checked = s.hide_library_items !== false; // Populate cache TTL dropdown const cacheTtlEl = document.getElementById('smarthunt-cache-ttl'); if (cacheTtlEl) cacheTtlEl.value = String(s.cache_ttl_minutes ?? 60); // Populate number fields const minRating = document.getElementById('smarthunt-min-rating'); if (minRating) minRating.value = s.min_tmdb_rating ?? 6.0; const minVotes = document.getElementById('smarthunt-min-votes'); if (minVotes) minVotes.value = s.min_vote_count ?? 50; const ys = document.getElementById('smarthunt-year-start'); if (ys) ys.value = s.year_start ?? 2000; const ye = document.getElementById('smarthunt-year-end'); if (ye) ye.value = s.year_end ?? (new Date().getFullYear() + 1); // Populate percentages const pcts = s.percentages || {}; const cats = ['similar_library', 'trending', 'hidden_gems', 'new_releases', 'top_rated', 'genre_mix', 'upcoming', 'random']; cats.forEach(cat => { const el = document.getElementById(`smarthunt-pct-${cat}`); if (el) el.value = pcts[cat] ?? 0; }); this._updateSmartHuntTotal(); this._wireSmartHuntEvents(); } catch (e) { console.error('[SmartHuntSettings] Error loading:', e); } } _wireSmartHuntEvents() { // Wire percentage inputs to update total if (this._smarthuntEventsWired) return; this._smarthuntEventsWired = true; document.querySelectorAll('.smarthunt-pct').forEach(input => { input.addEventListener('input', () => this._updateSmartHuntTotal()); }); // Save button const saveBtn = document.getElementById('smarthunt-save-btn'); if (saveBtn) { saveBtn.addEventListener('click', () => this.saveSmartHuntSettings()); } } _updateSmartHuntTotal() { const cats = ['similar_library', 'trending', 'hidden_gems', 'new_releases', 'top_rated', 'genre_mix', 'upcoming', 'random']; let total = 0; cats.forEach(cat => { const el = document.getElementById(`smarthunt-pct-${cat}`); if (el) total += parseInt(el.value) || 0; }); const totalEl = document.getElementById('smarthunt-total-value'); const barEl = document.getElementById('smarthunt-total-bar'); if (totalEl) totalEl.textContent = total; if (barEl) { barEl.classList.toggle('is-valid', total === 100); barEl.classList.toggle('is-invalid', total !== 100); } } async saveSmartHuntSettings() { const saveBtn = document.getElementById('smarthunt-save-btn'); if (saveBtn) { saveBtn.disabled = true; saveBtn.innerHTML = ' Saving...'; } try { const cats = ['similar_library', 'trending', 'hidden_gems', 'new_releases', 'top_rated', 'genre_mix', 'upcoming', 'random']; const percentages = {}; let total = 0; cats.forEach(cat => { const el = document.getElementById(`smarthunt-pct-${cat}`); const val = parseInt(el?.value) || 0; percentages[cat] = val; total += val; }); // Auto-adjust Random if total != 100 if (total !== 100) { const diff = 100 - total + (percentages.random || 0); if (diff >= 0 && diff <= 100) { percentages.random = diff; const randomEl = document.getElementById('smarthunt-pct-random'); if (randomEl) randomEl.value = diff; } else { // Proportionally scale all categories const factor = 100 / (total || 1); let runningTotal = 0; cats.forEach((cat, i) => { if (i < cats.length - 1) { percentages[cat] = Math.round(percentages[cat] * factor); runningTotal += percentages[cat]; } else { percentages[cat] = 100 - runningTotal; } }); // Update UI cats.forEach(cat => { const el = document.getElementById(`smarthunt-pct-${cat}`); if (el) el.value = percentages[cat]; }); } this._updateSmartHuntTotal(); } const settings = { enabled: true, // Smart Hunt is always enabled cache_ttl_minutes: parseInt(document.getElementById('smarthunt-cache-ttl')?.value) || 60, hide_library_items: document.getElementById('smarthunt-hide-library')?.checked ?? true, min_tmdb_rating: parseFloat(document.getElementById('smarthunt-min-rating')?.value) || 6.0, min_vote_count: parseInt(document.getElementById('smarthunt-min-votes')?.value) || 0, year_start: parseInt(document.getElementById('smarthunt-year-start')?.value) || 2000, year_end: parseInt(document.getElementById('smarthunt-year-end')?.value) || (new Date().getFullYear() + 1), percentages: percentages, }; const resp = await fetch('./api/requestarr/settings/smarthunt', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(settings), }); const data = await resp.json(); if (data.success) { this.core.showNotification('Smart Hunt settings saved successfully', 'success'); // Invalidate frontend cache if (window.invalidateSmartHuntCache) window.invalidateSmartHuntCache(); } else { this.core.showNotification('Failed to save Smart Hunt settings', 'error'); } } catch (e) { console.error('[SmartHuntSettings] Error saving:', e); this.core.showNotification('Failed to save Smart Hunt settings', 'error'); } finally { if (saveBtn) { saveBtn.disabled = false; saveBtn.innerHTML = ' Save'; } } } }