mirror of
https://github.com/plexguide/Huntarr.io.git
synced 2026-04-29 14:42:35 -04:00
454 lines
17 KiB
JavaScript
454 lines
17 KiB
JavaScript
/**
|
|
* Requestarr Home - Smart Hunt carousel + global search for Home section
|
|
*
|
|
* The three rotating carousels (Trending / Movies / TV) have been replaced
|
|
* with a single Smart Hunt carousel that uses the SmartHunt module.
|
|
*/
|
|
|
|
const HomeRequestarr = {
|
|
core: null,
|
|
searchTimeout: null,
|
|
elements: {},
|
|
defaultMovieInstance: null,
|
|
defaultTVInstance: null,
|
|
showTrending: true,
|
|
|
|
/** SmartHunt instance (created after core is ready) */
|
|
_smartHunt: null,
|
|
|
|
// Helpers to encode/decode compound instance values (movie_hunt:Name or radarr:Name)
|
|
_encodeInstance(appType, name) { return `${appType}:${name}`; },
|
|
_decodeInstance(compound) {
|
|
if (!compound || !compound.includes(':')) return { appType: 'radarr', name: compound || '' };
|
|
const idx = compound.indexOf(':');
|
|
return { appType: compound.substring(0, idx), name: compound.substring(idx + 1) };
|
|
},
|
|
|
|
init() {
|
|
this.cacheElements();
|
|
|
|
if (!this.elements.searchInput) {
|
|
return;
|
|
}
|
|
|
|
// Make this module globally accessible for auto-save visibility updates
|
|
window.HomeRequestarr = this;
|
|
|
|
// Auto-refresh dropdowns when any instance is added/deleted/renamed anywhere in the app
|
|
document.addEventListener('huntarr:instances-changed', () => {
|
|
this._populateInstanceDropdowns();
|
|
});
|
|
|
|
// Force hide initially if we can't determine setting yet
|
|
if (this.elements.discoverView) {
|
|
this.elements.discoverView.style.setProperty('display', 'none', 'important');
|
|
}
|
|
|
|
// Load settings first to determine if Smart Hunt should be shown
|
|
this.loadSettings()
|
|
.then(() => {
|
|
this.applyTrendingVisibility();
|
|
|
|
if (!this.showTrending) {
|
|
this.setupSearch();
|
|
return;
|
|
}
|
|
|
|
this.waitForCore()
|
|
.then((core) => {
|
|
this.core = core;
|
|
this.setupSearch();
|
|
this.loadDefaultInstances().then(() => {
|
|
this._initSmartHunt();
|
|
});
|
|
})
|
|
.catch(() => {
|
|
console.warn('[HomeRequestarr] Requestarr modules not ready within timeout');
|
|
});
|
|
});
|
|
},
|
|
|
|
/**
|
|
* Full refresh — called every time the user navigates to the Home page.
|
|
* Re-fetches settings from the server and re-applies all visibility and data.
|
|
*/
|
|
refresh() {
|
|
this.loadSettings().then(() => {
|
|
this.applyTrendingVisibility();
|
|
|
|
if (this.showTrending) {
|
|
if (this._smartHunt) {
|
|
// Smart Hunt already exists — reload instances + data
|
|
this.loadDefaultInstances().then(() => {
|
|
this._smartHunt.reload();
|
|
});
|
|
} else {
|
|
// Smart Hunt was not yet created (e.g. was disabled on first load)
|
|
this.waitForCore().then((core) => {
|
|
this.core = core;
|
|
this.loadDefaultInstances().then(() => {
|
|
this._initSmartHunt();
|
|
});
|
|
}).catch(() => {
|
|
console.warn('[HomeRequestarr] Could not init SmartHunt on refresh');
|
|
});
|
|
}
|
|
}
|
|
});
|
|
},
|
|
|
|
/** Create and load the SmartHunt carousel */
|
|
_initSmartHunt() {
|
|
const section = document.getElementById('home-smarthunt-section');
|
|
if (section) section.style.display = 'block';
|
|
|
|
if (!window.SmartHunt) {
|
|
console.warn('[HomeRequestarr] SmartHunt class not available yet');
|
|
return;
|
|
}
|
|
|
|
const self = this;
|
|
this._smartHunt = new window.SmartHunt({
|
|
carouselId: 'home-smarthunt-carousel',
|
|
core: this.core,
|
|
getMovieInstance: () => self.defaultMovieInstance || '',
|
|
getTVInstance: () => self.defaultTVInstance || '',
|
|
});
|
|
|
|
this._smartHunt.load();
|
|
},
|
|
|
|
async loadSettings() {
|
|
try {
|
|
const response = await fetch('./api/settings');
|
|
const data = await response.json();
|
|
if (data && data.general) {
|
|
const showPref = data.general.show_trending !== false;
|
|
const requestsEnabled = data.general.enable_requestarr !== false;
|
|
this.showTrending = showPref && requestsEnabled;
|
|
console.log('[HomeRequestarr] Show Smart Hunt on Home:', this.showTrending, '(show_trending:', showPref, ', requests enabled:', requestsEnabled, ')');
|
|
}
|
|
} catch (error) {
|
|
console.error('[HomeRequestarr] Error loading settings:', error);
|
|
this.showTrending = true;
|
|
}
|
|
},
|
|
|
|
applyTrendingVisibility() {
|
|
const requestsEnabled = !!(window.huntarrUI && window.huntarrUI._enableRequestarr !== false);
|
|
const card = this.elements.requestarrCard;
|
|
if (card) {
|
|
card.style.display = requestsEnabled ? '' : 'none';
|
|
}
|
|
const discoverView = this.elements.discoverView;
|
|
if (discoverView) {
|
|
const show = this.showTrending && requestsEnabled;
|
|
if (show) {
|
|
discoverView.style.setProperty('display', 'block', 'important');
|
|
} else {
|
|
discoverView.style.setProperty('display', 'none', 'important');
|
|
}
|
|
}
|
|
},
|
|
|
|
cacheElements() {
|
|
this.elements.requestarrCard = document.querySelector('.requestarr-home-card');
|
|
this.elements.searchInput = document.getElementById('home-requestarr-search-input');
|
|
this.elements.searchResultsView = document.getElementById('home-search-results-view');
|
|
this.elements.searchResultsGrid = document.getElementById('home-search-results-grid');
|
|
this.elements.discoverView = document.getElementById('home-requestarr-discover-view');
|
|
this.elements.smarthuntCarousel = document.getElementById('home-smarthunt-carousel');
|
|
this.elements.instanceControls = document.getElementById('home-instance-controls');
|
|
this.elements.movieInstanceSelect = document.getElementById('home-movie-instance-select');
|
|
this.elements.tvInstanceSelect = document.getElementById('home-tv-instance-select');
|
|
},
|
|
|
|
waitForCore() {
|
|
return new Promise((resolve, reject) => {
|
|
if (window.RequestarrDiscover) {
|
|
resolve(window.RequestarrDiscover);
|
|
return;
|
|
}
|
|
|
|
const startTime = Date.now();
|
|
const checkInterval = setInterval(() => {
|
|
if (window.RequestarrDiscover) {
|
|
clearInterval(checkInterval);
|
|
resolve(window.RequestarrDiscover);
|
|
return;
|
|
}
|
|
|
|
if (Date.now() - startTime > 2000) {
|
|
clearInterval(checkInterval);
|
|
reject(new Error('RequestarrDiscover not ready'));
|
|
}
|
|
}, 50);
|
|
});
|
|
},
|
|
|
|
async loadDefaultInstances() {
|
|
try {
|
|
const settingsResponse = await fetch('./api/requestarr/settings/default-instances');
|
|
const settingsData = await settingsResponse.json();
|
|
if (settingsData.success && settingsData.defaults) {
|
|
this.defaultMovieInstance = settingsData.defaults.movie_instance || null;
|
|
this.defaultTVInstance = settingsData.defaults.tv_instance || null;
|
|
}
|
|
} catch (error) {
|
|
console.error('[HomeRequestarr] Error loading default instances:', error);
|
|
this.defaultMovieInstance = null;
|
|
this.defaultTVInstance = null;
|
|
}
|
|
await this._populateInstanceDropdowns();
|
|
},
|
|
|
|
async _populateInstanceDropdowns() {
|
|
// Fetch bundle dropdown options once, then populate both selects
|
|
try {
|
|
const resp = await fetch(`./api/requestarr/bundles/dropdown?t=${Date.now()}`, { cache: 'no-store' });
|
|
if (!resp.ok) throw new Error('Failed to fetch bundle dropdown');
|
|
const data = await resp.json();
|
|
this._bundleMovieOptions = data.movie_options || [];
|
|
this._bundleTVOptions = data.tv_options || [];
|
|
} catch (e) {
|
|
console.warn('[HomeRequestarr] Error fetching bundle dropdown:', e);
|
|
this._bundleMovieOptions = [];
|
|
this._bundleTVOptions = [];
|
|
}
|
|
this._populateMovieInstanceDropdown();
|
|
this._populateTVInstanceDropdown();
|
|
if (this.elements.instanceControls) {
|
|
this.elements.instanceControls.style.display = 'flex';
|
|
}
|
|
}
|
|
,
|
|
|
|
async _populateMovieInstanceDropdown() {
|
|
const select = this.elements.movieInstanceSelect;
|
|
if (!select) return;
|
|
|
|
const options = this._bundleMovieOptions || [];
|
|
const previousValue = this.defaultMovieInstance || select.value || '';
|
|
|
|
select.innerHTML = '';
|
|
if (options.length === 0) {
|
|
select.innerHTML = '<option value="">No movie instances</option>';
|
|
return;
|
|
}
|
|
|
|
let matched = null;
|
|
options.forEach(opt => {
|
|
const el = document.createElement('option');
|
|
const val = opt.is_bundle
|
|
? this._encodeInstance(opt.primary_app_type, opt.primary_instance_name)
|
|
: opt.value;
|
|
el.value = val;
|
|
el.textContent = opt.label;
|
|
if (previousValue && val === previousValue) {
|
|
el.selected = true;
|
|
matched = val;
|
|
}
|
|
select.appendChild(el);
|
|
});
|
|
|
|
if (!matched && options.length > 0) {
|
|
select.options[0].selected = true;
|
|
matched = select.options[0].value;
|
|
}
|
|
|
|
if (matched) this.defaultMovieInstance = matched;
|
|
|
|
if (!select._homeChangeWired) {
|
|
select._homeChangeWired = true;
|
|
select.addEventListener('change', async () => {
|
|
this.defaultMovieInstance = select.value;
|
|
await this._saveServerDefaults();
|
|
this._syncRequestarrContent();
|
|
if (this._smartHunt) this._smartHunt.reload();
|
|
});
|
|
}
|
|
}
|
|
,
|
|
|
|
async _populateTVInstanceDropdown() {
|
|
const select = this.elements.tvInstanceSelect;
|
|
if (!select) return;
|
|
|
|
const options = this._bundleTVOptions || [];
|
|
const previousValue = this.defaultTVInstance || select.value || '';
|
|
|
|
select.innerHTML = '';
|
|
if (options.length === 0) {
|
|
select.innerHTML = '<option value="">No TV instances</option>';
|
|
return;
|
|
}
|
|
|
|
let matched = null;
|
|
options.forEach(opt => {
|
|
const el = document.createElement('option');
|
|
const val = opt.is_bundle
|
|
? this._encodeInstance(opt.primary_app_type, opt.primary_instance_name)
|
|
: opt.value;
|
|
el.value = val;
|
|
el.textContent = opt.label;
|
|
if (previousValue && val === previousValue) {
|
|
el.selected = true;
|
|
matched = val;
|
|
}
|
|
select.appendChild(el);
|
|
});
|
|
|
|
if (!matched && options.length > 0) {
|
|
select.options[0].selected = true;
|
|
matched = select.options[0].value;
|
|
}
|
|
|
|
if (matched) this.defaultTVInstance = matched;
|
|
|
|
if (!select._homeChangeWired) {
|
|
select._homeChangeWired = true;
|
|
select.addEventListener('change', async () => {
|
|
this.defaultTVInstance = select.value;
|
|
await this._saveServerDefaults();
|
|
this._syncRequestarrContent();
|
|
if (this._smartHunt) this._smartHunt.reload();
|
|
});
|
|
}
|
|
}
|
|
,
|
|
|
|
_saveServerDefaults() {
|
|
return fetch('./api/requestarr/settings/default-instances', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
movie_instance: this.defaultMovieInstance || '',
|
|
tv_instance: this.defaultTVInstance || ''
|
|
})
|
|
}).catch(e => console.warn('[HomeRequestarr] Failed to save defaults:', e));
|
|
},
|
|
|
|
_syncRequestarrContent() {
|
|
if (this.core && this.core.content) {
|
|
this.core.content.selectedMovieInstance = this.defaultMovieInstance;
|
|
this.core.content.selectedTVInstance = this.defaultTVInstance;
|
|
['movies-instance-select', 'discover-movie-instance-select'].forEach(id => {
|
|
const el = document.getElementById(id);
|
|
if (el && el.value !== this.defaultMovieInstance) el.value = this.defaultMovieInstance;
|
|
});
|
|
['tv-instance-select', 'discover-tv-instance-select'].forEach(id => {
|
|
const el = document.getElementById(id);
|
|
if (el && el.value !== this.defaultTVInstance) el.value = this.defaultTVInstance;
|
|
});
|
|
}
|
|
},
|
|
|
|
setupSearch() {
|
|
this.elements.searchInput.addEventListener('input', (event) => {
|
|
this.handleSearch(event.target.value);
|
|
});
|
|
},
|
|
|
|
handleSearch(query) {
|
|
if (this.searchTimeout) {
|
|
clearTimeout(this.searchTimeout);
|
|
}
|
|
|
|
if (!query.trim()) {
|
|
this.showDiscover();
|
|
return;
|
|
}
|
|
|
|
this.searchTimeout = setTimeout(() => {
|
|
this.performSearch(query);
|
|
}, 500);
|
|
},
|
|
|
|
showDiscover() {
|
|
if (this.elements.searchResultsView) {
|
|
this.elements.searchResultsView.style.display = 'none';
|
|
}
|
|
if (this.elements.discoverView) {
|
|
if (this.showTrending) {
|
|
this.elements.discoverView.style.setProperty('display', 'block', 'important');
|
|
} else {
|
|
this.elements.discoverView.style.setProperty('display', 'none', 'important');
|
|
}
|
|
}
|
|
},
|
|
|
|
showResults() {
|
|
if (this.elements.discoverView) {
|
|
this.elements.discoverView.style.display = 'none';
|
|
}
|
|
if (this.elements.searchResultsView) {
|
|
this.elements.searchResultsView.style.display = 'block';
|
|
}
|
|
},
|
|
|
|
async performSearch(query) {
|
|
this.showResults();
|
|
|
|
if (!this.elements.searchResultsGrid) {
|
|
return;
|
|
}
|
|
|
|
this.elements.searchResultsGrid.innerHTML = '<div class="loading-spinner"><i class="fas fa-spinner fa-spin"></i><p>Searching...</p></div>';
|
|
|
|
try {
|
|
// Ensure global blacklist is loaded for filtering
|
|
if (this.core && this.core.content && typeof this.core.content.loadHiddenMediaIds === 'function' && !this.core.content.globalBlacklistSet) {
|
|
await this.core.content.loadHiddenMediaIds();
|
|
}
|
|
|
|
const movieDecoded = this._decodeInstance(this.defaultMovieInstance);
|
|
const tvDecoded = this._decodeInstance(this.defaultTVInstance);
|
|
|
|
const [moviesResponse, tvResponse] = await Promise.all([
|
|
fetch(`./api/requestarr/search?q=${encodeURIComponent(query)}&app_type=${encodeURIComponent(movieDecoded.appType)}&instance_name=${encodeURIComponent(movieDecoded.name)}`),
|
|
fetch(`./api/requestarr/search?q=${encodeURIComponent(query)}&app_type=${encodeURIComponent(tvDecoded.appType)}&instance_name=${encodeURIComponent(tvDecoded.name)}`)
|
|
]);
|
|
|
|
const moviesData = await moviesResponse.json();
|
|
const tvData = await tvResponse.json();
|
|
|
|
const allResults = [
|
|
...(moviesData.results || []),
|
|
...(tvData.results || [])
|
|
];
|
|
|
|
allResults.sort((a, b) => (b.popularity || 0) - (a.popularity || 0));
|
|
|
|
if (allResults.length > 0) {
|
|
this.elements.searchResultsGrid.innerHTML = '';
|
|
allResults.forEach((item) => {
|
|
const suggestedInstance = item.media_type === 'movie'
|
|
? this.defaultMovieInstance
|
|
: this.defaultTVInstance;
|
|
const card = this.createMediaCard(item, suggestedInstance);
|
|
if (card) {
|
|
this.elements.searchResultsGrid.appendChild(card);
|
|
}
|
|
});
|
|
} else {
|
|
this.elements.searchResultsGrid.innerHTML = '<p style="color: #888; text-align: center; padding: 60px; width: 100%;">No results found</p>';
|
|
}
|
|
} catch (error) {
|
|
console.error('[HomeRequestarr] Error searching:', error);
|
|
this.elements.searchResultsGrid.innerHTML = '<p style="color: #ef4444; text-align: center; padding: 60px; width: 100%;">Search failed</p>';
|
|
}
|
|
},
|
|
|
|
createMediaCard(item, suggestedInstance = null) {
|
|
if (!this.core || !this.core.content || typeof this.core.content.createMediaCard !== 'function') {
|
|
return null;
|
|
}
|
|
|
|
return this.core.content.createMediaCard(item, suggestedInstance);
|
|
}
|
|
};
|
|
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
HomeRequestarr.init();
|
|
});
|