mirror of
https://github.com/plexguide/Huntarr.io.git
synced 2026-04-20 16:16:51 -04:00
1351 lines
70 KiB
JavaScript
1351 lines
70 KiB
JavaScript
/**
|
|
* Requestarr Modal - Two-column poster + form layout (matches Movie Hunt design)
|
|
*/
|
|
|
|
/* encodeInstanceValue, decodeInstanceValue from requestarr-core-utils.js (loaded first) */
|
|
class RequestarrModal {
|
|
constructor(core) {
|
|
this.core = core;
|
|
}
|
|
|
|
// ========================================
|
|
// MODAL SYSTEM
|
|
// ========================================
|
|
|
|
async openModal(tmdbId, mediaType, suggestedInstance = null) {
|
|
const modal = document.getElementById('media-modal');
|
|
if (!modal) return;
|
|
|
|
// Load modal preferences from server
|
|
await this.loadModalPreferences();
|
|
|
|
// Move modal to body so it sits outside .app-container and is not blurred
|
|
if (modal.parentElement !== document.body) {
|
|
document.body.appendChild(modal);
|
|
}
|
|
|
|
document.body.classList.add('requestarr-modal-open');
|
|
modal.style.display = 'flex';
|
|
|
|
// Show loading state in the existing elements
|
|
const titleEl = document.getElementById('requestarr-modal-title');
|
|
const labelEl = document.getElementById('requestarr-modal-label');
|
|
const metaEl = document.getElementById('requestarr-modal-meta');
|
|
const statusContainer = document.getElementById('requestarr-modal-status-container');
|
|
const posterImg = document.getElementById('requestarr-modal-poster-img');
|
|
const requestBtn = document.getElementById('modal-request-btn');
|
|
const instanceSelect = document.getElementById('modal-instance-select');
|
|
const rootSelect = document.getElementById('modal-root-folder');
|
|
const qualitySelect = document.getElementById('modal-quality-profile');
|
|
|
|
if (titleEl) titleEl.textContent = 'Loading...';
|
|
if (labelEl) labelEl.textContent = mediaType === 'tv' ? 'Add Series' : 'Add Movie';
|
|
if (metaEl) metaEl.textContent = '';
|
|
if (statusContainer) statusContainer.innerHTML = '<span class="mh-req-badge mh-req-badge-loading"><i class="fas fa-spinner fa-spin"></i> Loading...</span>';
|
|
if (posterImg) posterImg.src = './static/images/blackout.jpg';
|
|
if (requestBtn) { requestBtn.disabled = true; requestBtn.textContent = 'Add to Library'; requestBtn.classList.remove('disabled', 'success'); }
|
|
if (instanceSelect) instanceSelect.innerHTML = '<option value="">Loading...</option>';
|
|
const instanceInfoIcon = document.getElementById('modal-instance-info-icon');
|
|
if (instanceInfoIcon) instanceInfoIcon.style.display = 'none';
|
|
if (rootSelect) rootSelect.innerHTML = '<option value="">Loading...</option>';
|
|
if (qualitySelect) qualitySelect.innerHTML = '<option value="">Loading...</option>';
|
|
|
|
// Always hide Movie-Hunt-only and TV-Hunt-only fields first; renderModal will show them if needed
|
|
// Uses class toggle because .mh-req-field has display:grid!important which overrides inline styles
|
|
const wrapMinInit = document.getElementById('requestarr-modal-min-availability-wrap');
|
|
const wrapStartInit = document.getElementById('requestarr-modal-start-search-wrap');
|
|
const wrapMonitorInit = document.getElementById('requestarr-modal-monitor-wrap');
|
|
if (wrapMinInit) wrapMinInit.classList.add('mh-hidden');
|
|
if (wrapStartInit) wrapStartInit.classList.add('mh-hidden');
|
|
if (wrapMonitorInit) wrapMonitorInit.classList.add('mh-hidden');
|
|
|
|
// Attach close handlers (use .onclick to avoid stacking)
|
|
const self = this;
|
|
const backdrop = document.getElementById('requestarr-modal-backdrop');
|
|
const closeBtn = document.getElementById('requestarr-modal-close');
|
|
const cancelBtn = document.getElementById('requestarr-modal-cancel');
|
|
const startCb = document.getElementById('modal-start-search');
|
|
const minSelect = document.getElementById('modal-minimum-availability');
|
|
|
|
if (backdrop) backdrop.onclick = () => self.closeModal();
|
|
if (closeBtn) closeBtn.onclick = () => self.closeModal();
|
|
if (cancelBtn) cancelBtn.onclick = () => self.closeModal();
|
|
if (requestBtn) requestBtn.onclick = () => self.submitRequest();
|
|
|
|
// Attach change listeners for preferences
|
|
if (startCb) {
|
|
startCb.onchange = () => {
|
|
this.saveModalPreferences({ start_search: startCb.checked });
|
|
};
|
|
}
|
|
if (minSelect) {
|
|
minSelect.onchange = () => {
|
|
this.saveModalPreferences({ minimum_availability: minSelect.value });
|
|
};
|
|
}
|
|
const rootSelectEl = document.getElementById('modal-root-folder');
|
|
if (rootSelectEl) {
|
|
rootSelectEl.onchange = () => this._updateRequestButtonFromRootFolder();
|
|
}
|
|
|
|
this.suggestedInstance = suggestedInstance;
|
|
|
|
try {
|
|
const response = await fetch(`./api/requestarr/details/${mediaType}/${tmdbId}`);
|
|
const data = await response.json();
|
|
|
|
if (data.tmdb_id) {
|
|
this.core.currentModal = data;
|
|
this.core.currentModalData = data;
|
|
this.renderModal(data);
|
|
} else {
|
|
throw new Error('Failed to load details');
|
|
}
|
|
} catch (error) {
|
|
console.error('[RequestarrModal] Error loading details:', error);
|
|
if (titleEl) titleEl.textContent = 'Error';
|
|
if (statusContainer) statusContainer.innerHTML = '<span class="mh-req-badge mh-req-badge-error"><i class="fas fa-exclamation-triangle"></i> Failed to load details</span>';
|
|
}
|
|
}
|
|
|
|
async loadModalPreferences() {
|
|
try {
|
|
const response = await fetch('./api/requestarr/settings/modal-preferences');
|
|
const result = await response.json();
|
|
if (result.success) {
|
|
this.preferences = result.preferences;
|
|
} else {
|
|
this.preferences = {
|
|
start_search: true,
|
|
minimum_availability: 'released',
|
|
movie_instance: '',
|
|
tv_instance: ''
|
|
};
|
|
}
|
|
} catch (error) {
|
|
console.error('[RequestarrModal] Error loading preferences:', error);
|
|
this.preferences = {
|
|
start_search: true,
|
|
minimum_availability: 'released',
|
|
movie_instance: '',
|
|
tv_instance: ''
|
|
};
|
|
}
|
|
}
|
|
|
|
async saveModalPreferences(prefs) {
|
|
try {
|
|
await fetch('./api/requestarr/settings/modal-preferences', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(prefs)
|
|
});
|
|
// Update local object
|
|
Object.assign(this.preferences, prefs);
|
|
} catch (error) {
|
|
console.error('[RequestarrModal] Error saving preferences:', error);
|
|
}
|
|
}
|
|
|
|
renderModal(data) {
|
|
const isTVShow = data.media_type === 'tv';
|
|
const isOwner = window._huntarrUserRole === 'owner';
|
|
const perms = window._huntarrUserPermissions || {};
|
|
|
|
// For movies, combine Movie Hunt + Radarr; for TV, combine TV Hunt + Sonarr
|
|
let uniqueInstances = [];
|
|
if (isTVShow) {
|
|
const thInstances = (this.core.instances.tv_hunt || []).map(inst => ({
|
|
...inst, appType: 'tv_hunt', compoundValue: encodeInstanceValue('tv_hunt', inst.name),
|
|
label: `TV Hunt \u2013 ${inst.name}`
|
|
}));
|
|
const sonarrInstances = (this.core.instances.sonarr || []).map(inst => ({
|
|
...inst, appType: 'sonarr', compoundValue: encodeInstanceValue('sonarr', inst.name),
|
|
label: `Sonarr \u2013 ${inst.name}`
|
|
}));
|
|
const seen = new Set();
|
|
thInstances.forEach(inst => {
|
|
if (!seen.has(inst.compoundValue)) {
|
|
seen.add(inst.compoundValue);
|
|
uniqueInstances.push(inst);
|
|
}
|
|
});
|
|
sonarrInstances.forEach(inst => {
|
|
if (!seen.has(inst.compoundValue)) {
|
|
seen.add(inst.compoundValue);
|
|
uniqueInstances.push(inst);
|
|
}
|
|
});
|
|
} else {
|
|
const mhInstances = this.core.instances.movie_hunt || [];
|
|
const radarrInstances = this.core.instances.radarr || [];
|
|
const seen = new Set();
|
|
mhInstances.forEach(inst => {
|
|
if (!seen.has(inst.name)) {
|
|
seen.add(inst.name);
|
|
uniqueInstances.push({
|
|
...inst,
|
|
appType: 'movie_hunt',
|
|
compoundValue: encodeInstanceValue('movie_hunt', inst.name),
|
|
label: `Movie Hunt \u2013 ${inst.name}`
|
|
});
|
|
}
|
|
});
|
|
radarrInstances.forEach(inst => {
|
|
if (!seen.has(`radarr-${inst.name}`)) {
|
|
seen.add(`radarr-${inst.name}`);
|
|
uniqueInstances.push({
|
|
...inst,
|
|
appType: 'radarr',
|
|
compoundValue: encodeInstanceValue('radarr', inst.name),
|
|
label: `Radarr \u2013 ${inst.name}`
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
// Populate poster
|
|
const posterImg = document.getElementById('requestarr-modal-poster-img');
|
|
if (posterImg) posterImg.src = data.poster_path || './static/images/blackout.jpg';
|
|
|
|
// Populate title
|
|
const titleEl = document.getElementById('requestarr-modal-title');
|
|
if (titleEl) titleEl.textContent = data.title || '';
|
|
|
|
// Populate label
|
|
const labelEl = document.getElementById('requestarr-modal-label');
|
|
if (labelEl) labelEl.textContent = isTVShow ? 'Request Series' : 'Request Movie';
|
|
|
|
// Populate meta (year, genres)
|
|
const metaEl = document.getElementById('requestarr-modal-meta');
|
|
if (metaEl) {
|
|
const parts = [];
|
|
if (data.year) parts.push(String(data.year));
|
|
if (data.genres && data.genres.length) {
|
|
const genreNames = data.genres
|
|
.slice(0, 3)
|
|
.map(g => typeof g === 'string' ? g : (g.name || ''))
|
|
.filter(Boolean);
|
|
if (genreNames.length) parts.push(genreNames.join(', '));
|
|
}
|
|
metaEl.textContent = parts.join(' \u00B7 ');
|
|
}
|
|
|
|
const fieldsContainer = document.querySelector('.mh-req-fields');
|
|
const startSearchWrap = document.getElementById('requestarr-modal-start-search-wrap');
|
|
const statusContainer = document.getElementById('requestarr-modal-status-container');
|
|
const requestBtn = document.getElementById('modal-request-btn');
|
|
const instanceSelect = document.getElementById('modal-instance-select');
|
|
|
|
// ── Non-owner simplified modal ──
|
|
if (!isOwner) {
|
|
// Show fields container (for the instance row) but hide everything except instance
|
|
if (fieldsContainer) fieldsContainer.style.display = '';
|
|
if (startSearchWrap) startSearchWrap.classList.add('mh-hidden');
|
|
this._clearImportBanner();
|
|
|
|
// Hide root folder, quality profile, monitor, movie monitor, min availability rows
|
|
const rootField = document.getElementById('modal-root-folder');
|
|
const qualityField = document.getElementById('modal-quality-profile');
|
|
if (rootField && rootField.closest('.mh-req-field')) rootField.closest('.mh-req-field').classList.add('mh-hidden');
|
|
if (qualityField && qualityField.closest('.mh-req-field')) qualityField.closest('.mh-req-field').classList.add('mh-hidden');
|
|
const monitorWrap = document.getElementById('requestarr-modal-monitor-wrap');
|
|
const movieMonitorWrap = document.getElementById('requestarr-modal-movie-monitor-wrap');
|
|
const minAvailWrap = document.getElementById('requestarr-modal-min-availability-wrap');
|
|
if (monitorWrap) monitorWrap.classList.add('mh-hidden');
|
|
if (movieMonitorWrap) movieMonitorWrap.classList.add('mh-hidden');
|
|
if (minAvailWrap) minAvailWrap.classList.add('mh-hidden');
|
|
|
|
// Resolve the page's current instance
|
|
const pageInstance = this.suggestedInstance
|
|
|| (isTVShow ? this.core.content.selectedTVInstance : this.core.content.selectedMovieInstance)
|
|
|| uniqueInstances[0]?.compoundValue || '';
|
|
|
|
// Populate instance dropdown with single option, greyed out
|
|
if (instanceSelect) {
|
|
instanceSelect.innerHTML = '';
|
|
const matched = uniqueInstances.find(inst => inst.compoundValue === pageInstance || inst.name === pageInstance);
|
|
const opt = document.createElement('option');
|
|
opt.value = pageInstance;
|
|
opt.textContent = matched ? matched.label : pageInstance;
|
|
instanceSelect.appendChild(opt);
|
|
instanceSelect.disabled = true;
|
|
instanceSelect.style.opacity = '0.6';
|
|
instanceSelect.onchange = null;
|
|
}
|
|
const instanceInfoIcon = document.getElementById('modal-instance-info-icon');
|
|
if (instanceInfoIcon) instanceInfoIcon.style.display = 'none';
|
|
|
|
// Show permissions status row below instance (same field styling)
|
|
const hasAutoApprove = isTVShow
|
|
? (perms.auto_approve || perms.auto_approve_tv)
|
|
: (perms.auto_approve || perms.auto_approve_movies);
|
|
|
|
// Remove any previous permissions row, then insert a new one
|
|
const existingPermRow = document.getElementById('requestarr-modal-permissions-row');
|
|
if (existingPermRow) existingPermRow.remove();
|
|
const permRow = document.createElement('div');
|
|
permRow.className = 'mh-req-field';
|
|
permRow.id = 'requestarr-modal-permissions-row';
|
|
const permLabel = document.createElement('label');
|
|
permLabel.textContent = 'Status';
|
|
const permValue = document.createElement('span');
|
|
permValue.className = 'mh-req-perm-status';
|
|
if (hasAutoApprove) {
|
|
permValue.innerHTML = '<i class="fas fa-check-circle"></i> Auto-Approved';
|
|
permValue.classList.add('mh-req-perm-approved');
|
|
} else {
|
|
permValue.innerHTML = '<i class="fas fa-clock"></i> Requires Approval';
|
|
permValue.classList.add('mh-req-perm-pending');
|
|
}
|
|
permRow.appendChild(permLabel);
|
|
permRow.appendChild(permValue);
|
|
// Insert after the instance field
|
|
const instanceField = instanceSelect ? instanceSelect.closest('.mh-req-field') : null;
|
|
if (instanceField && instanceField.parentNode) {
|
|
instanceField.parentNode.insertBefore(permRow, instanceField.nextSibling);
|
|
}
|
|
|
|
// Clear status container (permissions info is now in the field row)
|
|
if (statusContainer) statusContainer.innerHTML = '';
|
|
|
|
// Configure request button
|
|
if (requestBtn) {
|
|
requestBtn.disabled = !pageInstance;
|
|
requestBtn.classList.remove('disabled', 'success');
|
|
requestBtn.textContent = isTVShow ? 'Request Series' : 'Request Movie';
|
|
if (!pageInstance) requestBtn.classList.add('disabled');
|
|
}
|
|
// Push buttons to bottom-right of the form column
|
|
const actionsArea = document.querySelector('.mh-req-actions');
|
|
if (actionsArea) actionsArea.style.marginTop = 'auto';
|
|
return;
|
|
}
|
|
|
|
// ── Owner full modal (existing logic) ──
|
|
if (fieldsContainer) fieldsContainer.style.display = '';
|
|
const actionsArea = document.querySelector('.mh-req-actions');
|
|
if (actionsArea) actionsArea.style.marginTop = '';
|
|
// Remove permissions row if present from previous non-owner render
|
|
const existingPermRowOwner = document.getElementById('requestarr-modal-permissions-row');
|
|
if (existingPermRowOwner) existingPermRowOwner.remove();
|
|
// Re-show root/quality fields (may have been hidden by previous non-owner render)
|
|
const rootField = document.getElementById('modal-root-folder');
|
|
const qualityField = document.getElementById('modal-quality-profile');
|
|
if (rootField && rootField.closest('.mh-req-field')) rootField.closest('.mh-req-field').classList.remove('mh-hidden');
|
|
if (qualityField && qualityField.closest('.mh-req-field')) qualityField.closest('.mh-req-field').classList.remove('mh-hidden');
|
|
if (instanceSelect) {
|
|
instanceSelect.disabled = false;
|
|
instanceSelect.style.opacity = '';
|
|
}
|
|
|
|
const currentlySelectedInstance = isTVShow ? (this.preferences?.tv_instance || this.core.content.selectedTVInstance) : (this.preferences?.movie_instance || this.core.content.selectedMovieInstance);
|
|
const rawDefault = this.suggestedInstance || currentlySelectedInstance || uniqueInstances[0]?.compoundValue || uniqueInstances[0]?.name || '';
|
|
|
|
let defaultInstance = rawDefault;
|
|
let isMovieHunt = false;
|
|
if (!isTVShow && rawDefault) {
|
|
const matched = uniqueInstances.find(inst => inst.compoundValue === rawDefault || inst.name === rawDefault);
|
|
if (matched) {
|
|
defaultInstance = matched.compoundValue || matched.name;
|
|
isMovieHunt = matched.appType === 'movie_hunt';
|
|
}
|
|
} else if (isTVShow && rawDefault) {
|
|
const matched = uniqueInstances.find(inst => (inst.compoundValue || inst.name) === rawDefault || inst.name === rawDefault);
|
|
if (matched) {
|
|
defaultInstance = matched.compoundValue || matched.name;
|
|
isMovieHunt = matched.appType === 'movie_hunt';
|
|
}
|
|
}
|
|
const defaultDecoded = defaultInstance ? decodeInstanceValue(defaultInstance, isTVShow ? 'sonarr' : 'radarr') : {};
|
|
const isTVHunt = isTVShow && defaultDecoded.appType === 'tv_hunt';
|
|
|
|
console.log('[RequestarrModal] Resolved instance:', defaultInstance, 'isMovieHunt:', isMovieHunt, 'isTVHunt:', isTVHunt);
|
|
|
|
if (instanceSelect) {
|
|
instanceSelect.innerHTML = '';
|
|
const instanceInfoIcon = document.getElementById('modal-instance-info-icon');
|
|
if (instanceInfoIcon) instanceInfoIcon.style.display = 'none';
|
|
if (uniqueInstances.length === 0) {
|
|
instanceSelect.innerHTML = '<option value="">No Instance Configured</option>';
|
|
instanceSelect.classList.add('field-warning');
|
|
this._showInstanceInfoIcon();
|
|
} else {
|
|
instanceSelect.classList.remove('field-warning');
|
|
uniqueInstances.forEach(instance => {
|
|
const opt = document.createElement('option');
|
|
opt.value = instance.compoundValue || instance.name;
|
|
opt.textContent = instance.label || `${isTVShow ? (instance.appType === 'tv_hunt' ? 'TV Hunt' : 'Sonarr') : (instance.appType === 'movie_hunt' ? 'Movie Hunt' : 'Radarr')} \u2013 ${instance.name}`;
|
|
const isSelected = (instance.compoundValue || instance.name) === defaultInstance;
|
|
if (isSelected) opt.selected = true;
|
|
instanceSelect.appendChild(opt);
|
|
});
|
|
if (!defaultInstance && uniqueInstances.length > 0) {
|
|
instanceSelect.selectedIndex = 0;
|
|
}
|
|
}
|
|
instanceSelect.onchange = () => this.instanceChanged(instanceSelect.value);
|
|
}
|
|
|
|
const qualitySelect = document.getElementById('modal-quality-profile');
|
|
const effectiveInstance = (instanceSelect && instanceSelect.value) ? instanceSelect.value : defaultInstance;
|
|
if (qualitySelect) {
|
|
const profDecoded = effectiveInstance ? decodeInstanceValue(effectiveInstance, isTVShow ? 'sonarr' : 'radarr') : {};
|
|
const profileKey = `${profDecoded.appType || ''}-${profDecoded.name || ''}`;
|
|
const profiles = this.core.qualityProfiles[profileKey] || [];
|
|
const useHuntProfiles = isMovieHunt || isTVHunt;
|
|
|
|
if (profiles.length === 0 && effectiveInstance) {
|
|
qualitySelect.innerHTML = '<option value="">Loading profiles...</option>';
|
|
this.core.loadQualityProfilesForInstance(profDecoded.appType, profDecoded.name).then(newProfiles => {
|
|
if (newProfiles && newProfiles.length > 0) {
|
|
this._populateQualityProfiles(qualitySelect, newProfiles, useHuntProfiles);
|
|
} else {
|
|
this._populateQualityProfiles(qualitySelect, [], useHuntProfiles);
|
|
}
|
|
});
|
|
} else {
|
|
this._populateQualityProfiles(qualitySelect, profiles, useHuntProfiles);
|
|
}
|
|
}
|
|
|
|
if (requestBtn) {
|
|
requestBtn.disabled = false;
|
|
requestBtn.classList.remove('disabled', 'success');
|
|
requestBtn.textContent = 'Request';
|
|
}
|
|
this._applyMovieHuntModalMode(effectiveInstance, isTVShow, labelEl, requestBtn);
|
|
|
|
if (defaultInstance) {
|
|
if (statusContainer) {
|
|
statusContainer.innerHTML = '<span class="mh-req-badge mh-req-badge-loading"><i class="fas fa-spinner fa-spin"></i> Checking...</span>';
|
|
}
|
|
this.loadModalRootFolders(defaultInstance, isTVShow);
|
|
if (isTVShow) {
|
|
this.loadSeriesStatus(defaultInstance);
|
|
} else {
|
|
this.loadMovieStatus(defaultInstance);
|
|
}
|
|
} else {
|
|
if (statusContainer) {
|
|
statusContainer.innerHTML = '';
|
|
}
|
|
const rootSelect = document.getElementById('modal-root-folder');
|
|
if (rootSelect) {
|
|
rootSelect.innerHTML = '<option value="">Select an instance first</option>';
|
|
rootSelect.classList.remove('field-warning');
|
|
}
|
|
}
|
|
|
|
if (uniqueInstances.length === 0 && requestBtn) {
|
|
requestBtn.disabled = true;
|
|
requestBtn.classList.add('disabled');
|
|
}
|
|
}
|
|
|
|
async loadModalRootFolders(instanceName, isTVShow) {
|
|
const rootSelect = document.getElementById('modal-root-folder');
|
|
if (!rootSelect) return;
|
|
|
|
if (this._loadingModalRootFolders) return;
|
|
this._loadingModalRootFolders = true;
|
|
|
|
// Decode compound value to get app type and actual name (both movies and TV support compound)
|
|
const decoded = decodeInstanceValue(instanceName, isTVShow ? 'sonarr' : 'radarr');
|
|
const appType = decoded.appType;
|
|
const actualInstanceName = decoded.name;
|
|
rootSelect.innerHTML = '<option value="">Loading...</option>';
|
|
rootSelect.classList.remove('field-warning');
|
|
const infoIcon = document.getElementById('modal-root-folder-info-icon');
|
|
if (infoIcon) infoIcon.style.display = 'none';
|
|
|
|
try {
|
|
const response = await fetch(`./api/requestarr/rootfolders?app_type=${appType}&instance_name=${encodeURIComponent(actualInstanceName)}`);
|
|
const data = await response.json();
|
|
|
|
if (data.success && data.root_folders && data.root_folders.length > 0) {
|
|
const seenPaths = new Map();
|
|
data.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,
|
|
isDefault: !!rf.is_default
|
|
});
|
|
}
|
|
});
|
|
|
|
if (seenPaths.size === 0) {
|
|
rootSelect.innerHTML = '<option value="">No Root Configured</option>';
|
|
rootSelect.classList.add('field-warning');
|
|
this._showRootFolderInfoIcon(instanceName, isTVShow);
|
|
} else {
|
|
rootSelect.classList.remove('field-warning');
|
|
rootSelect.innerHTML = '';
|
|
let defaultFound = false;
|
|
let firstPath = null;
|
|
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)` : '');
|
|
if (rf.isDefault) {
|
|
opt.selected = true;
|
|
defaultFound = true;
|
|
}
|
|
if (!firstPath) firstPath = rf.path;
|
|
rootSelect.appendChild(opt);
|
|
});
|
|
if (!defaultFound && firstPath) {
|
|
rootSelect.value = firstPath;
|
|
}
|
|
}
|
|
} else {
|
|
rootSelect.innerHTML = '<option value="">No Root Configured</option>';
|
|
rootSelect.classList.add('field-warning');
|
|
this._showRootFolderInfoIcon(instanceName, isTVShow);
|
|
}
|
|
} catch (error) {
|
|
console.error('[RequestarrModal] Error loading root folders:', error);
|
|
rootSelect.innerHTML = '<option value="">No Root Configured</option>';
|
|
rootSelect.classList.add('field-warning');
|
|
this._showRootFolderInfoIcon(instanceName, isTVShow);
|
|
} finally {
|
|
this._loadingModalRootFolders = false;
|
|
this._updateRequestButtonFromRootFolder();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Show info icon when no instance configured; click navigates to Instances page.
|
|
*/
|
|
_showInstanceInfoIcon() {
|
|
const infoIcon = document.getElementById('modal-instance-info-icon');
|
|
if (!infoIcon) return;
|
|
infoIcon.style.display = '';
|
|
const self = this;
|
|
infoIcon.onclick = function(e) {
|
|
e.preventDefault();
|
|
self.closeModal();
|
|
if (window.location.hash !== '#media-hunt-instances') {
|
|
window.location.hash = '#media-hunt-instances';
|
|
} else {
|
|
window.dispatchEvent(new HashChangeEvent('hashchange'));
|
|
}
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Show info icon when no root configured; click navigates to Root Folders page with instance selected.
|
|
*/
|
|
_showRootFolderInfoIcon(instanceName, isTVShow) {
|
|
const decoded = decodeInstanceValue(instanceName, isTVShow ? 'sonarr' : 'radarr');
|
|
const appType = decoded.appType || '';
|
|
// Root Folders settings page only configures Movie Hunt and TV Hunt; hide icon for Sonarr/Radarr
|
|
if (appType !== 'movie_hunt' && appType !== 'tv_hunt') return;
|
|
const infoIcon = document.getElementById('modal-root-folder-info-icon');
|
|
if (!infoIcon) return;
|
|
infoIcon.style.display = '';
|
|
const self = this;
|
|
infoIcon.onclick = function(e) {
|
|
e.preventDefault();
|
|
const instanceSelect = document.getElementById('modal-instance-select');
|
|
const compoundValue = (instanceSelect && instanceSelect.value) || instanceName || '';
|
|
if (!compoundValue) return;
|
|
const decoded = decodeInstanceValue(compoundValue, isTVShow ? 'sonarr' : 'radarr');
|
|
try {
|
|
sessionStorage.setItem('requestarr-goto-root-instance', JSON.stringify({
|
|
appType: decoded.appType || (isTVShow ? 'tv_hunt' : 'movie_hunt'),
|
|
instanceName: decoded.name || ''
|
|
}));
|
|
} catch (err) {}
|
|
self.closeModal();
|
|
if (window.location.hash !== '#settings-root-folders') {
|
|
window.location.hash = '#settings-root-folders';
|
|
} else {
|
|
window.dispatchEvent(new HashChangeEvent('hashchange'));
|
|
}
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Disable Request button when no root folder is selected (user must pick a folder to request).
|
|
*/
|
|
_updateRequestButtonFromRootFolder() {
|
|
const requestBtn = document.getElementById('modal-request-btn');
|
|
const rootSelect = document.getElementById('modal-root-folder');
|
|
if (!requestBtn || !rootSelect) return;
|
|
const noRootFolder = !rootSelect.value || rootSelect.value.trim() === '';
|
|
const isCompleteOrInLibrary = requestBtn.textContent === 'Complete' || requestBtn.textContent === 'In Library' || requestBtn.textContent === 'Already in library';
|
|
if (noRootFolder && !isCompleteOrInLibrary) {
|
|
requestBtn.disabled = true;
|
|
requestBtn.classList.add('disabled');
|
|
} else if (!noRootFolder && (requestBtn.textContent === 'Request' || requestBtn.textContent === 'Add to Library')) {
|
|
requestBtn.disabled = false;
|
|
requestBtn.classList.remove('disabled');
|
|
}
|
|
}
|
|
|
|
async loadSeriesStatus(instanceName) {
|
|
if (!instanceName || !this.core.currentModalData) return;
|
|
|
|
const container = document.getElementById('requestarr-modal-status-container');
|
|
if (!container) return;
|
|
|
|
container.innerHTML = '<span class="mh-req-badge mh-req-badge-loading"><i class="fas fa-spinner fa-spin"></i> Checking...</span>';
|
|
|
|
const decoded = decodeInstanceValue(instanceName, 'sonarr');
|
|
const isTVHunt = decoded.appType === 'tv_hunt';
|
|
const addLabel = isTVHunt ? 'Add to Library' : 'Request';
|
|
|
|
try {
|
|
const response = await fetch(`./api/requestarr/series-status?tmdb_id=${this.core.currentModalData.tmdb_id}&instance=${encodeURIComponent(decoded.name)}&app_type=${encodeURIComponent(decoded.appType || 'sonarr')}`);
|
|
const status = await response.json();
|
|
const requestBtn = document.getElementById('modal-request-btn');
|
|
|
|
if (status.exists) {
|
|
const isComplete = status.missing_episodes === 0 && status.total_episodes > 0;
|
|
// Sync discover card badge — show may have been added after the card rendered
|
|
this._syncCardBadge(this.core.currentModalData.tmdb_id, isComplete, true);
|
|
|
|
if (isComplete) {
|
|
container.innerHTML = `<span class="mh-req-badge mh-req-badge-lib"><i class="fas fa-check-circle"></i> Complete (${status.available_episodes}/${status.total_episodes} episodes)</span>`;
|
|
if (requestBtn) { requestBtn.disabled = true; requestBtn.classList.add('disabled'); requestBtn.textContent = 'Complete'; }
|
|
this._clearImportBanner();
|
|
} else if (status.missing_episodes > 0) {
|
|
container.innerHTML = `<span class="mh-req-badge mh-req-badge-ok"><i class="fas fa-tv"></i> ${status.missing_episodes} missing episodes (${status.available_episodes}/${status.total_episodes})</span>`;
|
|
if (requestBtn) { requestBtn.disabled = false; requestBtn.classList.remove('disabled'); requestBtn.textContent = addLabel; }
|
|
this._updateRequestButtonFromRootFolder();
|
|
if (isTVHunt) this._checkForImport(instanceName);
|
|
} else {
|
|
container.innerHTML = '<span class="mh-req-badge mh-req-badge-lib"><i class="fas fa-check-circle"></i> In Library</span>';
|
|
if (requestBtn) { requestBtn.disabled = true; requestBtn.classList.add('disabled'); requestBtn.textContent = 'In Library'; }
|
|
this._clearImportBanner();
|
|
}
|
|
} else {
|
|
container.innerHTML = isTVHunt
|
|
? '<span class="mh-req-badge mh-req-badge-ok"><i class="fas fa-check-circle"></i> Available to add</span>'
|
|
: '<span class="mh-req-badge mh-req-badge-ok"><i class="fas fa-check-circle"></i> Available to request</span>';
|
|
if (requestBtn) { requestBtn.disabled = false; requestBtn.classList.remove('disabled'); requestBtn.textContent = addLabel; }
|
|
this._updateRequestButtonFromRootFolder();
|
|
// Check for importable files on disk for TV Hunt
|
|
if (isTVHunt) this._checkForImport(instanceName);
|
|
}
|
|
} catch (error) {
|
|
console.error('[RequestarrModal] Error loading series status:', error);
|
|
container.innerHTML = isTVHunt
|
|
? '<span class="mh-req-badge mh-req-badge-ok"><i class="fas fa-check-circle"></i> Available to add</span>'
|
|
: '<span class="mh-req-badge mh-req-badge-ok"><i class="fas fa-check-circle"></i> Available to request</span>';
|
|
}
|
|
}
|
|
|
|
async loadMovieStatus(instanceName) {
|
|
if (!instanceName || !this.core.currentModalData) return;
|
|
|
|
const container = document.getElementById('requestarr-modal-status-container');
|
|
if (!container) return;
|
|
|
|
container.innerHTML = '<span class="mh-req-badge mh-req-badge-loading"><i class="fas fa-spinner fa-spin"></i> Checking...</span>';
|
|
|
|
try {
|
|
const decoded = decodeInstanceValue(instanceName);
|
|
const isMovieHunt = decoded.appType === 'movie_hunt';
|
|
const appTypeParam = isMovieHunt ? '&app_type=movie_hunt' : '';
|
|
const response = await fetch(`./api/requestarr/movie-status?tmdb_id=${this.core.currentModalData.tmdb_id}&instance=${encodeURIComponent(decoded.name)}${appTypeParam}`);
|
|
const status = await response.json();
|
|
const requestBtn = document.getElementById('modal-request-btn');
|
|
|
|
if (status.in_library) {
|
|
container.innerHTML = '<span class="mh-req-badge mh-req-badge-lib"><i class="fas fa-check-circle"></i> Already in library</span>';
|
|
if (requestBtn) { requestBtn.disabled = true; requestBtn.classList.add('disabled'); requestBtn.textContent = 'Already in library'; }
|
|
this._syncCardBadge(this.core.currentModalData.tmdb_id, true);
|
|
this._clearImportBanner();
|
|
} else if (status.monitored) {
|
|
// Movie is in the collection (monitored) but file not downloaded yet
|
|
container.innerHTML = '<span class="mh-req-badge mh-req-badge-lib"><i class="fas fa-bookmark"></i> In library — downloading</span>';
|
|
if (requestBtn) { requestBtn.disabled = true; requestBtn.classList.add('disabled'); requestBtn.textContent = 'In Library'; }
|
|
this._syncCardBadge(this.core.currentModalData.tmdb_id, false, true);
|
|
this._clearImportBanner();
|
|
} else if (status.user_has_pending) {
|
|
// THIS user already has a pending request
|
|
container.innerHTML = '<span class="mh-req-badge mh-req-badge-warn"><i class="fas fa-clock"></i> Pending approval</span>';
|
|
if (requestBtn) { requestBtn.disabled = true; requestBtn.classList.add('disabled'); requestBtn.textContent = 'Pending Approval'; }
|
|
this._syncCardBadge(this.core.currentModalData.tmdb_id, false, false, true);
|
|
if (isMovieHunt) this._checkForImport(instanceName);
|
|
} else if (status.previously_requested) {
|
|
container.innerHTML = '<span class="mh-req-badge mh-req-badge-warn"><i class="fas fa-bookmark"></i> Already requested</span>';
|
|
if (requestBtn) { requestBtn.disabled = true; requestBtn.classList.add('disabled'); requestBtn.textContent = 'Already Requested'; }
|
|
this._syncCardBadge(this.core.currentModalData.tmdb_id, false, true);
|
|
// Still check for importable files even if previously requested
|
|
if (isMovieHunt) this._checkForImport(instanceName);
|
|
} else {
|
|
container.innerHTML = isMovieHunt
|
|
? '<span class="mh-req-badge mh-req-badge-ok"><i class="fas fa-check-circle"></i> Available to add</span>'
|
|
: '<span class="mh-req-badge mh-req-badge-ok"><i class="fas fa-check-circle"></i> Available to request</span>';
|
|
if (requestBtn) {
|
|
requestBtn.disabled = false;
|
|
requestBtn.classList.remove('disabled');
|
|
requestBtn.textContent = isMovieHunt ? 'Add to Library' : 'Request';
|
|
}
|
|
this._updateRequestButtonFromRootFolder();
|
|
// Check for importable files on disk
|
|
if (isMovieHunt) this._checkForImport(instanceName);
|
|
}
|
|
} catch (error) {
|
|
console.error('[RequestarrModal] Error loading movie status:', error);
|
|
const isMovieHunt = instanceName && decodeInstanceValue(instanceName).appType === 'movie_hunt';
|
|
container.innerHTML = isMovieHunt
|
|
? '<span class="mh-req-badge mh-req-badge-ok"><i class="fas fa-check-circle"></i> Available to add</span>'
|
|
: '<span class="mh-req-badge mh-req-badge-ok"><i class="fas fa-check-circle"></i> Available to request</span>';
|
|
const requestBtn = document.getElementById('modal-request-btn');
|
|
if (requestBtn) {
|
|
requestBtn.disabled = false;
|
|
requestBtn.classList.remove('disabled');
|
|
requestBtn.textContent = isMovieHunt ? 'Add to Library' : 'Request';
|
|
}
|
|
}
|
|
}
|
|
|
|
// ========================================
|
|
// IMPORT DETECTION
|
|
// ========================================
|
|
|
|
_clearImportBanner() {
|
|
const existing = document.getElementById('modal-import-banner');
|
|
if (existing) existing.remove();
|
|
const actionsArea = document.querySelector('.mh-req-actions');
|
|
if (actionsArea) actionsArea.classList.remove('import-available');
|
|
}
|
|
|
|
async _checkForImport(instanceName) {
|
|
this._clearImportBanner();
|
|
if (!this.core.currentModalData) return;
|
|
|
|
const isTVShow = this.core.currentModalData.media_type === 'tv';
|
|
const decoded = decodeInstanceValue(instanceName, isTVShow ? 'sonarr' : 'radarr');
|
|
const isMovieHunt = decoded.appType === 'movie_hunt';
|
|
const isTVHunt = decoded.appType === 'tv_hunt';
|
|
if (!isMovieHunt && !isTVHunt) return;
|
|
|
|
const tmdbId = this.core.currentModalData.tmdb_id;
|
|
if (!tmdbId) return;
|
|
|
|
// Resolve numeric instance ID from core.instances (backend expects integer)
|
|
const instKey = isTVHunt ? 'tv_hunt' : 'movie_hunt';
|
|
const instList = (this.core.instances && this.core.instances[instKey]) || [];
|
|
const instObj = instList.find(i => i.name === decoded.name);
|
|
const numericId = instObj ? instObj.id : '';
|
|
|
|
const apiBase = isMovieHunt ? './api/movie-hunt/import-check' : './api/tv-hunt/import-check';
|
|
|
|
try {
|
|
const resp = await fetch(`${apiBase}?tmdb_id=${tmdbId}&instance_id=${encodeURIComponent(numericId)}`);
|
|
const data = await resp.json();
|
|
if (!data.found || !data.matches || data.matches.length === 0) return;
|
|
|
|
const best = data.matches[0];
|
|
this._showImportBanner(best, instanceName);
|
|
} catch (err) {
|
|
console.warn('[RequestarrModal] Import check failed:', err);
|
|
}
|
|
}
|
|
|
|
_showImportBanner(match, instanceName) {
|
|
this._clearImportBanner();
|
|
|
|
const score = match.score;
|
|
const sizeGB = match.media_info ? (match.media_info.total_size / 1e9).toFixed(1) : '?';
|
|
const fileCount = match.media_info ? match.media_info.file_count : 0;
|
|
const mainFile = match.media_info ? match.media_info.main_file : '';
|
|
|
|
// Confidence label
|
|
let confidenceClass, confidenceLabel;
|
|
if (score >= 85) { confidenceClass = 'high'; confidenceLabel = 'High'; }
|
|
else if (score >= 65) { confidenceClass = 'medium'; confidenceLabel = 'Medium'; }
|
|
else { confidenceClass = 'low'; confidenceLabel = 'Low'; }
|
|
|
|
// Swap status badge to amber warning
|
|
const container = document.getElementById('requestarr-modal-status-container');
|
|
if (container) {
|
|
container.innerHTML = '<span class="mh-req-badge mh-req-badge-import"><i class="fas fa-exclamation-triangle"></i> Found on Disk</span>';
|
|
}
|
|
|
|
// Read current form selections for the settings summary
|
|
const instanceSelect = document.getElementById('modal-instance-select');
|
|
const rootSelect = document.getElementById('modal-root-folder');
|
|
const qualitySelect = document.getElementById('modal-quality-profile');
|
|
const instLabel = instanceSelect ? instanceSelect.options[instanceSelect.selectedIndex]?.text : '';
|
|
const rootLabel = rootSelect ? rootSelect.value : '';
|
|
const qualLabel = qualitySelect ? qualitySelect.options[qualitySelect.selectedIndex]?.text : '';
|
|
|
|
const banner = document.createElement('div');
|
|
banner.id = 'modal-import-banner';
|
|
banner.className = 'modal-import-banner';
|
|
banner.innerHTML =
|
|
'<div class="import-banner-header">' +
|
|
'<i class="fas fa-folder-open"></i>' +
|
|
'<span>Existing files detected on disk</span>' +
|
|
'<span class="import-confidence import-confidence-' + confidenceClass + '">' + score + '% ' + confidenceLabel + '</span>' +
|
|
'</div>' +
|
|
'<div class="import-banner-details">' +
|
|
'<div class="import-banner-folder" title="' + this._escBannerAttr(match.folder_path) + '">' +
|
|
'<i class="fas fa-folder"></i> ' + this._escBannerHtml(match.folder_name) +
|
|
'</div>' +
|
|
'<div class="import-banner-meta">' +
|
|
(mainFile ? '<span title="' + this._escBannerAttr(mainFile) + '"><i class="fas fa-film"></i> ' + this._escBannerHtml(mainFile) + '</span>' : '') +
|
|
'<span><i class="fas fa-hdd"></i> ' + sizeGB + ' GB</span>' +
|
|
(fileCount > 1 ? '<span><i class="fas fa-copy"></i> ' + fileCount + ' files</span>' : '') +
|
|
'</div>' +
|
|
'</div>' +
|
|
'<div class="import-banner-settings">' +
|
|
(instLabel ? '<span><i class="fas fa-server"></i>' + this._escBannerHtml(instLabel) + '</span>' : '') +
|
|
(rootLabel ? '<span><i class="fas fa-folder-open"></i>' + this._escBannerHtml(rootLabel) + '</span>' : '') +
|
|
(qualLabel ? '<span><i class="fas fa-sliders-h"></i>' + this._escBannerHtml(qualLabel) + '</span>' : '') +
|
|
'</div>' +
|
|
'<button class="import-banner-btn" id="modal-import-instead-btn">' +
|
|
'<i class="fas fa-download"></i> Import to Library' +
|
|
'</button>';
|
|
|
|
// Insert before the action buttons area
|
|
const actionsArea = document.querySelector('.mh-req-actions');
|
|
if (actionsArea) {
|
|
actionsArea.parentNode.insertBefore(banner, actionsArea);
|
|
actionsArea.classList.add('import-available');
|
|
} else {
|
|
// Fallback: insert at end of form column
|
|
const formCol = document.querySelector('.mh-req-form');
|
|
if (formCol) formCol.appendChild(banner);
|
|
}
|
|
|
|
// Wire up import button
|
|
const importBtn = document.getElementById('modal-import-instead-btn');
|
|
if (importBtn) {
|
|
importBtn.onclick = () => this._doImportInstead(match, instanceName);
|
|
}
|
|
|
|
// Demote the Add to Library button to secondary
|
|
const requestBtn = document.getElementById('modal-request-btn');
|
|
if (requestBtn && !requestBtn.disabled) {
|
|
requestBtn.textContent = 'Add as New';
|
|
}
|
|
|
|
// Update modal label to reflect import context
|
|
const labelEl = document.getElementById('requestarr-modal-label');
|
|
if (labelEl) labelEl.textContent = 'Import to Library';
|
|
}
|
|
|
|
async _doImportInstead(match, instanceName) {
|
|
const importBtn = document.getElementById('modal-import-instead-btn');
|
|
if (importBtn) {
|
|
importBtn.disabled = true;
|
|
importBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Importing...';
|
|
}
|
|
|
|
try {
|
|
const data = this.core.currentModalData;
|
|
const isTVShow = data.media_type === 'tv';
|
|
const decoded = decodeInstanceValue(instanceName, isTVShow ? 'sonarr' : 'radarr');
|
|
const isTVHunt = decoded.appType === 'tv_hunt';
|
|
const confirmUrl = isTVHunt ? './api/tv-hunt/import-media/confirm' : './api/movie-hunt/import-media/confirm';
|
|
|
|
// Read current form selections so import uses the same settings
|
|
const rootSelect = document.getElementById('modal-root-folder');
|
|
const qualitySelect = document.getElementById('modal-quality-profile');
|
|
const monitorSelect = document.getElementById('modal-monitor');
|
|
const rootFolder = (rootSelect && rootSelect.value) ? rootSelect.value : (match.root_folder || '');
|
|
const qualityProfile = qualitySelect ? qualitySelect.value : '';
|
|
const monitor = monitorSelect ? monitorSelect.value : '';
|
|
|
|
const body = {
|
|
folder_path: match.folder_path,
|
|
tmdb_id: data.tmdb_id,
|
|
title: data.title || data.name || '',
|
|
year: String(data.year || ''),
|
|
poster_path: data.poster_path || '',
|
|
root_folder: rootFolder,
|
|
instance_id: decoded.name,
|
|
quality_profile: qualityProfile,
|
|
monitor: monitor,
|
|
};
|
|
// TV confirm expects 'name' field
|
|
if (isTVHunt) {
|
|
body.name = data.title || data.name || '';
|
|
body.first_air_date = data.first_air_date || '';
|
|
}
|
|
|
|
const resp = await fetch(confirmUrl, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(body),
|
|
});
|
|
const result = await resp.json();
|
|
|
|
if (result.success) {
|
|
if (importBtn) {
|
|
importBtn.innerHTML = '<i class="fas fa-check"></i> Imported';
|
|
importBtn.classList.add('success');
|
|
}
|
|
this.core.showNotification(result.message || 'Imported successfully', 'success');
|
|
|
|
// Update status badge and card
|
|
const container = document.getElementById('requestarr-modal-status-container');
|
|
if (container) {
|
|
container.innerHTML = '<span class="mh-req-badge mh-req-badge-lib"><i class="fas fa-check-circle"></i> Already in library</span>';
|
|
}
|
|
const requestBtn = document.getElementById('modal-request-btn');
|
|
if (requestBtn) {
|
|
requestBtn.disabled = true;
|
|
requestBtn.classList.add('disabled');
|
|
requestBtn.textContent = 'Already in library';
|
|
}
|
|
this._syncCardBadge(data.tmdb_id, true);
|
|
|
|
// Notify detail page
|
|
window.dispatchEvent(new CustomEvent('requestarr-request-success', {
|
|
detail: { tmdbId: data.tmdb_id, mediaType: isTVHunt ? 'tv' : 'movie', appType: decoded.appType, instanceName: decoded.name }
|
|
}));
|
|
|
|
setTimeout(() => this.closeModal(), 2000);
|
|
} else {
|
|
if (importBtn) {
|
|
importBtn.disabled = false;
|
|
importBtn.innerHTML = '<i class="fas fa-download"></i> Import Instead';
|
|
}
|
|
this.core.showNotification(result.message || 'Import failed', 'error');
|
|
}
|
|
} catch (err) {
|
|
console.error('[RequestarrModal] Import error:', err);
|
|
if (importBtn) {
|
|
importBtn.disabled = false;
|
|
importBtn.innerHTML = '<i class="fas fa-download"></i> Import Instead';
|
|
}
|
|
this.core.showNotification('Import failed: ' + (err.message || 'Unknown error'), 'error');
|
|
}
|
|
}
|
|
|
|
_escBannerHtml(s) {
|
|
if (!s) return '';
|
|
const d = document.createElement('div');
|
|
d.textContent = s;
|
|
return d.innerHTML;
|
|
}
|
|
|
|
_escBannerAttr(s) {
|
|
if (!s) return '';
|
|
return s.replace(/&/g, '&').replace(/"/g, '"').replace(/</g, '<').replace(/>/g, '>');
|
|
}
|
|
|
|
/**
|
|
* When selected instance is Movie Hunt or TV Hunt, show "Add to Library" and
|
|
* the Start search checkbox + relevant fields. Otherwise "Request Movie" / "Request".
|
|
*/
|
|
_applyMovieHuntModalMode(instanceValue, isTVShow, labelEl, requestBtn) {
|
|
const wrapMin = document.getElementById('requestarr-modal-min-availability-wrap');
|
|
const wrapStart = document.getElementById('requestarr-modal-start-search-wrap');
|
|
const wrapMonitor = document.getElementById('requestarr-modal-monitor-wrap');
|
|
const wrapMovieMonitor = document.getElementById('requestarr-modal-movie-monitor-wrap');
|
|
const minSelect = document.getElementById('modal-minimum-availability');
|
|
const startCb = document.getElementById('modal-start-search');
|
|
const startLabel = wrapStart ? wrapStart.querySelector('span') : null;
|
|
const decoded = instanceValue ? decodeInstanceValue(instanceValue, isTVShow ? 'sonarr' : 'radarr') : {};
|
|
const isMovieHunt = !isTVShow && decoded.appType === 'movie_hunt';
|
|
const isTVHunt = isTVShow && decoded.appType === 'tv_hunt';
|
|
const isHuntInstance = isMovieHunt || isTVHunt;
|
|
// Use class toggle — .mh-req-field has display:grid!important which overrides inline styles
|
|
if (wrapMin) wrapMin.classList.toggle('mh-hidden', !isMovieHunt);
|
|
if (wrapStart) wrapStart.classList.toggle('mh-hidden', !isHuntInstance);
|
|
if (wrapMonitor) wrapMonitor.classList.toggle('mh-hidden', !isTVHunt);
|
|
if (wrapMovieMonitor) wrapMovieMonitor.classList.toggle('mh-hidden', !isMovieHunt);
|
|
|
|
// Update search label text for context
|
|
if (startLabel) startLabel.textContent = isTVHunt ? 'Start search for missing episodes' : 'Start search for missing movie';
|
|
|
|
// Use loaded preferences or defaults
|
|
if (minSelect) minSelect.value = this.preferences?.minimum_availability || 'released';
|
|
if (startCb) startCb.checked = this.preferences?.hasOwnProperty('start_search') ? this.preferences.start_search : true;
|
|
|
|
if (labelEl) labelEl.textContent = isHuntInstance ? 'Add to Library' : (isTVShow ? 'Request Series' : 'Request Movie');
|
|
if (requestBtn && !requestBtn.disabled) requestBtn.textContent = isHuntInstance ? 'Add to Library' : 'Request';
|
|
}
|
|
|
|
instanceChanged(instanceName) {
|
|
this._clearImportBanner();
|
|
const isTVShow = this.core.currentModalData.media_type === 'tv';
|
|
|
|
// Save to server modal preferences
|
|
if (isTVShow) {
|
|
this.saveModalPreferences({ tv_instance: instanceName });
|
|
} else {
|
|
this.saveModalPreferences({ movie_instance: instanceName });
|
|
}
|
|
console.log('[RequestarrModal] Instance changed to:', instanceName);
|
|
|
|
const labelEl = document.getElementById('requestarr-modal-label');
|
|
const requestBtn = document.getElementById('modal-request-btn');
|
|
this._applyMovieHuntModalMode(instanceName, isTVShow, labelEl, requestBtn);
|
|
|
|
// Reload root folders
|
|
this.loadModalRootFolders(instanceName, isTVShow);
|
|
|
|
// Update quality profile dropdown
|
|
const qualitySelect = document.getElementById('modal-quality-profile');
|
|
if (qualitySelect) {
|
|
const decoded = decodeInstanceValue(instanceName, isTVShow ? 'sonarr' : 'radarr');
|
|
const profileKey = `${decoded.appType}-${decoded.name}`;
|
|
const useHuntProfiles = decoded.appType === 'movie_hunt' || decoded.appType === 'tv_hunt';
|
|
const profiles = this.core.qualityProfiles[profileKey] || [];
|
|
|
|
if (profiles.length === 0 && instanceName) {
|
|
qualitySelect.innerHTML = '<option value="">Loading profiles...</option>';
|
|
this.core.loadQualityProfilesForInstance(decoded.appType, decoded.name).then(newProfiles => {
|
|
if (newProfiles && newProfiles.length > 0) {
|
|
this._populateQualityProfiles(qualitySelect, newProfiles, useHuntProfiles);
|
|
} else {
|
|
this._populateQualityProfiles(qualitySelect, [], useHuntProfiles);
|
|
}
|
|
});
|
|
} else {
|
|
this._populateQualityProfiles(qualitySelect, profiles, useHuntProfiles);
|
|
}
|
|
}
|
|
|
|
// Reload status
|
|
if (isTVShow) {
|
|
this.loadSeriesStatus(instanceName);
|
|
} else {
|
|
this.loadMovieStatus(instanceName);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Populate a quality profile dropdown, handling Movie Hunt vs Radarr/Sonarr differences.
|
|
* Movie Hunt: no "Any" placeholder, pre-select the default profile.
|
|
* Radarr/Sonarr: show "Any (Default)" as first option, no pre-selection.
|
|
*/
|
|
_populateQualityProfiles(selectEl, profiles, isMovieHunt) {
|
|
selectEl.innerHTML = '';
|
|
|
|
if (isMovieHunt) {
|
|
// Movie Hunt: list only real profiles, pre-select the default
|
|
if (profiles.length === 0) {
|
|
selectEl.innerHTML = '<option value="">No profiles configured</option>';
|
|
return;
|
|
}
|
|
let defaultIdx = profiles.findIndex(p => p.is_default);
|
|
if (defaultIdx === -1) defaultIdx = 0; // fallback to first
|
|
|
|
profiles.forEach((profile, idx) => {
|
|
const opt = document.createElement('option');
|
|
opt.value = profile.id;
|
|
opt.textContent = profile.name;
|
|
if (idx === defaultIdx) opt.selected = true;
|
|
selectEl.appendChild(opt);
|
|
});
|
|
} else {
|
|
// Radarr / Sonarr: "Any (Default)" placeholder, then real profiles
|
|
selectEl.innerHTML = '<option value="">Any (Default)</option>';
|
|
profiles.forEach(profile => {
|
|
if (profile.name.toLowerCase() !== 'any') {
|
|
const opt = document.createElement('option');
|
|
opt.value = profile.id;
|
|
opt.textContent = profile.name;
|
|
selectEl.appendChild(opt);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
async submitRequest() {
|
|
const isOwner = window._huntarrUserRole === 'owner';
|
|
const perms = window._huntarrUserPermissions || {};
|
|
const requestBtn = document.getElementById('modal-request-btn');
|
|
const instanceSelect = document.getElementById('modal-instance-select');
|
|
|
|
if (!this.core.currentModalData) {
|
|
this.core.showNotification('No media data available', 'error');
|
|
return;
|
|
}
|
|
|
|
const isTVShow = this.core.currentModalData.media_type === 'tv';
|
|
|
|
// Both owner and non-owner read instance from the dropdown (non-owner has it greyed out)
|
|
if (!instanceSelect || !instanceSelect.value) {
|
|
this.core.showNotification('No instance available for this request', 'error');
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const decoded = decodeInstanceValue(instanceSelect.value, isTVShow ? 'sonarr' : 'radarr');
|
|
const instanceName = decoded.name;
|
|
const appType = decoded.appType;
|
|
const isHuntApp = appType === 'movie_hunt' || appType === 'tv_hunt';
|
|
|
|
// Determine if this user has auto-approve (owners always do)
|
|
const hasAutoApprove = isOwner || (isTVShow
|
|
? (perms.auto_approve || perms.auto_approve_tv)
|
|
: (perms.auto_approve || perms.auto_approve_movies));
|
|
|
|
if (requestBtn) {
|
|
requestBtn.disabled = true;
|
|
requestBtn.classList.add('pressed');
|
|
requestBtn.textContent = hasAutoApprove
|
|
? (isHuntApp ? 'Adding...' : 'Requesting...')
|
|
: 'Submitting...';
|
|
}
|
|
|
|
// ── Non-auto-approve path: only create a pending request record ──
|
|
if (!hasAutoApprove) {
|
|
const trackResp = await fetch('./api/requestarr/requests', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
media_type: isTVShow ? 'tv' : 'movie',
|
|
tmdb_id: this.core.currentModalData.tmdb_id,
|
|
title: this.core.currentModalData.title || '',
|
|
year: String(this.core.currentModalData.year || ''),
|
|
poster_path: this.core.currentModalData.poster_path || '',
|
|
instance_name: instanceName,
|
|
app_type: appType,
|
|
})
|
|
});
|
|
const trackResult = await trackResp.json();
|
|
|
|
if (trackResp.ok && (trackResult.success || trackResult.request)) {
|
|
if (requestBtn) {
|
|
requestBtn.textContent = 'Submitted \u2713';
|
|
requestBtn.classList.add('success');
|
|
}
|
|
this.core.showNotification('Request submitted — awaiting owner approval.', 'success');
|
|
|
|
const tmdbId = this.core.currentModalData.tmdb_id;
|
|
const mediaType = this.core.currentModalData.media_type;
|
|
this._syncCardBadge(tmdbId, false, false, true);
|
|
window.dispatchEvent(new CustomEvent('requestarr-request-success', {
|
|
detail: { tmdbId, mediaType, appType, instanceName }
|
|
}));
|
|
if (window.huntarrUI && typeof window.huntarrUI._updatePendingRequestBadge === 'function') {
|
|
window.huntarrUI._updatePendingRequestBadge();
|
|
}
|
|
setTimeout(() => this.closeModal(), 2000);
|
|
} else {
|
|
const errorMsg = trackResult.error || trackResult.message || 'Failed to submit request';
|
|
this.core.showNotification(errorMsg, 'error');
|
|
if (requestBtn) {
|
|
requestBtn.disabled = false;
|
|
requestBtn.classList.remove('success', 'pressed');
|
|
requestBtn.textContent = 'Request';
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
// ── Auto-approve / owner path: trigger the search pipeline ──
|
|
const requestData = {
|
|
tmdb_id: this.core.currentModalData.tmdb_id,
|
|
media_type: this.core.currentModalData.media_type,
|
|
title: this.core.currentModalData.title,
|
|
year: this.core.currentModalData.year,
|
|
overview: this.core.currentModalData.overview || '',
|
|
poster_path: this.core.currentModalData.poster_path || '',
|
|
backdrop_path: this.core.currentModalData.backdrop_path || '',
|
|
instance: instanceName,
|
|
app_type: appType,
|
|
};
|
|
|
|
if (isOwner) {
|
|
// Owner sends full form data
|
|
const qualityProfileEl = document.getElementById('modal-quality-profile');
|
|
const rootFolderSelect = document.getElementById('modal-root-folder');
|
|
requestData.root_folder_path = (rootFolderSelect && rootFolderSelect.value) ? rootFolderSelect.value : undefined;
|
|
requestData.quality_profile = qualityProfileEl ? qualityProfileEl.value : '';
|
|
if (appType === 'movie_hunt') {
|
|
const startCb = document.getElementById('modal-start-search');
|
|
const minSelect = document.getElementById('modal-minimum-availability');
|
|
const movieMonitorSelect = document.getElementById('modal-movie-monitor');
|
|
requestData.start_search = startCb ? startCb.checked : true;
|
|
requestData.minimum_availability = (minSelect && minSelect.value) ? minSelect.value : 'released';
|
|
requestData.movie_monitor = (movieMonitorSelect && movieMonitorSelect.value) ? movieMonitorSelect.value : 'movie_only';
|
|
}
|
|
if (appType === 'tv_hunt') {
|
|
const monitorSelect = document.getElementById('modal-monitor');
|
|
const startCbTV = document.getElementById('modal-start-search');
|
|
requestData.monitor = (monitorSelect && monitorSelect.value) ? monitorSelect.value : 'all_episodes';
|
|
requestData.start_search = startCbTV ? startCbTV.checked : true;
|
|
}
|
|
} else {
|
|
// Non-owner with auto-approve: sensible defaults
|
|
if (appType === 'movie_hunt') {
|
|
requestData.start_search = true;
|
|
requestData.minimum_availability = 'released';
|
|
requestData.movie_monitor = 'movie_only';
|
|
} else if (appType === 'tv_hunt') {
|
|
requestData.start_search = true;
|
|
requestData.monitor = 'all_episodes';
|
|
}
|
|
}
|
|
|
|
const response = await fetch('./api/requestarr/request', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(requestData)
|
|
});
|
|
|
|
const result = await response.json();
|
|
|
|
if (result.success) {
|
|
if (requestBtn) {
|
|
requestBtn.textContent = isHuntApp ? 'Added \u2713' : 'Requested \u2713';
|
|
requestBtn.classList.add('success');
|
|
}
|
|
|
|
const successMsg = result.message || (isHuntApp ? 'Successfully added to library.' : `${isTVShow ? 'Series' : 'Movie'} requested successfully!`);
|
|
this.core.showNotification(successMsg, 'success');
|
|
|
|
// Create a request tracking record
|
|
try {
|
|
await fetch('./api/requestarr/requests', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
media_type: isTVShow ? 'tv' : 'movie',
|
|
tmdb_id: this.core.currentModalData.tmdb_id,
|
|
title: this.core.currentModalData.title || '',
|
|
year: String(this.core.currentModalData.year || ''),
|
|
poster_path: this.core.currentModalData.poster_path || '',
|
|
instance_name: instanceName,
|
|
app_type: appType,
|
|
})
|
|
});
|
|
} catch (trackErr) {
|
|
console.debug('[RequestarrModal] Request tracking record skipped:', trackErr);
|
|
}
|
|
|
|
const tmdbId = this.core.currentModalData.tmdb_id;
|
|
const mediaType = this.core.currentModalData.media_type;
|
|
this._syncCardBadge(tmdbId, false, true);
|
|
|
|
window.dispatchEvent(new CustomEvent('requestarr-request-success', {
|
|
detail: { tmdbId, mediaType, appType, instanceName }
|
|
}));
|
|
|
|
if (window.huntarrUI && typeof window.huntarrUI._updatePendingRequestBadge === 'function') {
|
|
window.huntarrUI._updatePendingRequestBadge();
|
|
}
|
|
|
|
setTimeout(() => { this._refreshCardStatusFromAPI(tmdbId); }, 3000);
|
|
setTimeout(() => { this._refreshCardStatusFromAPI(tmdbId); }, 8000);
|
|
setTimeout(() => this.closeModal(), 2000);
|
|
} else {
|
|
const errorMsg = result.message || result.error || 'Request failed';
|
|
this.core.showNotification(errorMsg, 'error');
|
|
if (requestBtn) {
|
|
requestBtn.disabled = false;
|
|
requestBtn.classList.remove('success');
|
|
requestBtn.textContent = isHuntApp ? 'Add to Library' : 'Request';
|
|
}
|
|
}
|
|
} catch (error) {
|
|
console.error('[RequestarrModal] Error submitting request:', error);
|
|
this.core.showNotification(error.message || 'Request failed', 'error');
|
|
if (requestBtn) {
|
|
requestBtn.disabled = false;
|
|
requestBtn.classList.remove('success');
|
|
requestBtn.textContent = 'Request';
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sync Discover card badges to match the real status.
|
|
* Called when the modal detects "Already in library", "Previously requested",
|
|
* or after a successful request.
|
|
*
|
|
* @param {number|string} tmdbId
|
|
* @param {boolean} inLibrary - Movie is downloaded / fully available
|
|
* @param {boolean} requested - Movie is requested but not yet downloaded
|
|
* @param {boolean} pending - Request is pending approval (non-auto-approve user)
|
|
*/
|
|
_syncCardBadge(tmdbId, inLibrary, requested, pending) {
|
|
const cards = document.querySelectorAll(`.media-card[data-tmdb-id="${tmdbId}"]`);
|
|
cards.forEach((card) => {
|
|
const badge = card.querySelector('.media-card-status-badge');
|
|
if (badge) {
|
|
if (inLibrary) {
|
|
badge.className = 'media-card-status-badge complete';
|
|
badge.innerHTML = '<i class="fas fa-check"></i>';
|
|
card.classList.add('in-library');
|
|
} else if (pending) {
|
|
badge.className = 'media-card-status-badge pending';
|
|
badge.innerHTML = '<i class="fas fa-clock"></i>';
|
|
// Do NOT add in-library class — pending is not in collection
|
|
} else if (requested) {
|
|
badge.className = 'media-card-status-badge partial';
|
|
badge.innerHTML = '<i class="fas fa-bookmark"></i>';
|
|
card.classList.add('in-library');
|
|
}
|
|
}
|
|
// If now in collection (either state), swap eye-slash → trash
|
|
if (inLibrary || requested) {
|
|
const hideBtn = card.querySelector('.media-card-hide-btn');
|
|
if (hideBtn) {
|
|
hideBtn.className = 'media-card-delete-btn';
|
|
hideBtn.title = 'Remove / Delete';
|
|
hideBtn.innerHTML = '<i class="fas fa-trash-alt"></i>';
|
|
}
|
|
const requestBtn = card.querySelector('.media-card-request-btn');
|
|
if (requestBtn) requestBtn.remove();
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* After a delay, re-check the actual library status from the API and sync card badges.
|
|
* Uses the currently selected instance so the backend knows which collection to check.
|
|
*/
|
|
async _refreshCardStatusFromAPI(tmdbId) {
|
|
try {
|
|
const instanceSelect = document.getElementById('modal-instance-select');
|
|
const instanceValue = instanceSelect ? instanceSelect.value : '';
|
|
if (!instanceValue) return;
|
|
|
|
const decoded = decodeInstanceValue(instanceValue);
|
|
const appTypeParam = decoded.appType === 'movie_hunt' ? '&app_type=movie_hunt' : '';
|
|
const resp = await fetch(`./api/requestarr/movie-status?tmdb_id=${tmdbId}&instance=${encodeURIComponent(decoded.name)}${appTypeParam}`);
|
|
const data = await resp.json();
|
|
|
|
this._syncCardBadge(tmdbId, data.in_library || false, data.previously_requested || data.monitored || false);
|
|
} catch (err) {
|
|
console.warn('[RequestarrModal] Failed to refresh card status from API:', err);
|
|
}
|
|
}
|
|
|
|
closeModal() {
|
|
const modal = document.getElementById('media-modal');
|
|
if (modal) modal.style.display = 'none';
|
|
this.core.currentModalData = null;
|
|
this._clearImportBanner();
|
|
// Reset fields visibility and instance select state for next open
|
|
const fieldsContainer = document.querySelector('.mh-req-fields');
|
|
if (fieldsContainer) fieldsContainer.style.display = '';
|
|
const rootField = document.getElementById('modal-root-folder');
|
|
const qualityField = document.getElementById('modal-quality-profile');
|
|
if (rootField && rootField.closest('.mh-req-field')) rootField.closest('.mh-req-field').classList.remove('mh-hidden');
|
|
if (qualityField && qualityField.closest('.mh-req-field')) qualityField.closest('.mh-req-field').classList.remove('mh-hidden');
|
|
const instanceSelect = document.getElementById('modal-instance-select');
|
|
if (instanceSelect) {
|
|
instanceSelect.disabled = false;
|
|
instanceSelect.style.opacity = '';
|
|
}
|
|
// Remove permissions row added by non-owner modal
|
|
const permRow = document.getElementById('requestarr-modal-permissions-row');
|
|
if (permRow) permRow.remove();
|
|
// Reset actions margin
|
|
const actionsArea = document.querySelector('.mh-req-actions');
|
|
if (actionsArea) actionsArea.style.marginTop = '';
|
|
document.body.classList.remove('requestarr-modal-open');
|
|
}
|
|
}
|