/** * Profile editor (Movie Hunt) - full-page editor like instance editor. * Open from Profiles list Edit; Back/Save; sections: Profile details, Upgrade & quality, Qualities. * Attaches to window.SettingsForms. */ (function() { 'use strict'; if (typeof window.SettingsForms === 'undefined') return; const Forms = window.SettingsForms; function escapeHtml(s) { if (s == null) return ''; return String(s).replace(/&/g, '&').replace(//g, '>').replace(/"/g, '"'); } function generateProfileEditorHtml(profile) { const p = profile || {}; const name = escapeHtml((p.name || '').trim() || 'Unnamed'); const isDefault = Boolean(p.is_default); const upgradesAllowed = p.upgrades_allowed !== false; const upgradeUntil = escapeHtml((p.upgrade_until_quality || 'WEB 2160p').trim()); const minScore = p.min_custom_format_score != null ? Number(p.min_custom_format_score) : -10000; const untilScore = p.upgrade_until_custom_format_score != null ? Number(p.upgrade_until_custom_format_score) : 5500; const increment = p.upgrade_score_increment != null ? Number(p.upgrade_score_increment) : 100; const language = escapeHtml((p.language || 'English').trim()); const qualities = Array.isArray(p.qualities) ? p.qualities : []; var checkedQualityNames = []; qualities.forEach(function(q) { if (q.enabled !== false) { var n = (q.name || q.id || '').trim(); if (n) checkedQualityNames.push(n); } }); if (checkedQualityNames.length === 0) { checkedQualityNames = ['WEB 2160p', 'WEB 1080p', 'WEB 720p']; } let qualitiesHtml = ''; qualities.forEach(function(q, i) { const qName = escapeHtml((q.name || q.id || '').trim() || 'Quality'); const checked = q.enabled !== false ? ' checked' : ''; qualitiesHtml += '
' + '' + '' + '' + '
'; }); var upgradeSelectOptions = ''; checkedQualityNames.forEach(function(opt) { var sel = opt === (p.upgrade_until_quality || 'WEB 2160p').trim() ? ' selected' : ''; upgradeSelectOptions += ''; }); if (upgradeSelectOptions === '') { upgradeSelectOptions = ''; } return '
' + '
' + '
Profile details
' + '
' + '
' + '' + '

A friendly name for this profile

' + '
' + '
' + '' + '' + '

The default profile is used when no other is selected

' + '
' + '
' + '
Upgrade & quality
' + '
' + '
' + '' + '' + '

If disabled, qualities will not be upgraded

' + '
' + '
' + '' + '

Once this quality is reached, no further upgrades will be grabbed

' + '
' + '
' + '' + '

Minimum custom format score allowed to download

' + '
' + '
' + '' + '

Once quality cutoff is met, upgrades stop when this score is reached

' + '
' + '
' + '' + '

Minimum improvement in score between existing and new release to consider an upgrade

' + '
' + '
' + '' + '

Language for releases

' + '
' + '
' + '
Qualities
' + '

Only checked qualities are wanted. Higher in the list is more preferred.

' + '
' + (qualitiesHtml || '

No qualities defined.

') + '
' + '
' + '
' + '
Custom format scores
' + '

Hunt Manager uses these scores to decide which release to grab. Higher total score means a better release. Start at 0; use the Recommend column (from TRaSH Guides) as a guide if you want. To incorporate customized formats for your movies, visit Custom Formats.

' + '
' + '' + '
Custom formatYour scoreRecommend
' + '' + '
'; } let _profileEditorScoresList = []; let _profileEditorScoresSortTimeout = null; let _profileEditorDirty = false; function renderProfileEditorScoresTable() { const tbody = document.getElementById('profile-editor-scores-tbody'); const emptyEl = document.getElementById('profile-editor-scores-empty'); const table = document.querySelector('.profile-editor-scores-table'); if (!tbody || _profileEditorScoresList.length === 0) { if (tbody) tbody.innerHTML = ''; if (emptyEl) emptyEl.style.display = 'block'; if (table) table.style.display = 'none'; return; } const sorted = _profileEditorScoresList.slice().sort(function(a, b) { return (b.score - a.score); }); let html = ''; for (let i = 0; i < sorted.length; i++) { const item = sorted[i]; const title = (item.title || item.name || 'Unnamed').replace(//g, '>'); const rec = item.recommended_score; const recText = (rec != null && !isNaN(rec)) ? String(rec) : '—'; html += '' + title + '' + '' + '' + recText + ''; } tbody.innerHTML = html; tbody.querySelectorAll('.profile-editor-score-input').forEach(function(input) { function scheduleSort(idx) { if (_profileEditorScoresSortTimeout) clearTimeout(_profileEditorScoresSortTimeout); _profileEditorScoresSortTimeout = setTimeout(function() { _profileEditorScoresSortTimeout = null; renderProfileEditorScoresTable(); }, 2000); } input.addEventListener('input', function() { const idx = parseInt(input.getAttribute('data-index'), 10); if (isNaN(idx)) return; let val = parseInt(input.value, 10); if (isNaN(val)) val = 0; const item = _profileEditorScoresList.find(function(o) { return o.index === idx; }); if (item) item.score = val; markProfileEditorDirty(); scheduleSort(idx); }); input.addEventListener('change', function() { const idx = parseInt(input.getAttribute('data-index'), 10); if (isNaN(idx)) return; let val = parseInt(input.value, 10); if (isNaN(val)) val = 0; const item = _profileEditorScoresList.find(function(o) { return o.index === idx; }); if (item) item.score = val; markProfileEditorDirty(); scheduleSort(idx); }); }); } function loadProfileEditorScoresTable() { const tbody = document.getElementById('profile-editor-scores-tbody'); const emptyEl = document.getElementById('profile-editor-scores-empty'); const table = document.querySelector('.profile-editor-scores-table'); if (!tbody) return; var state = Forms._currentProfileEditing; var apiUrl = (state && state.tvHunt) ? './api/tv-hunt/custom-formats' : './api/custom-formats'; fetch(apiUrl) .then(function(r) { return r.json(); }) .then(function(data) { const list = (data && data.custom_formats) ? data.custom_formats : []; if (list.length === 0) { _profileEditorScoresList = []; tbody.innerHTML = ''; if (emptyEl) emptyEl.style.display = 'block'; if (table) table.style.display = 'none'; return; } if (emptyEl) emptyEl.style.display = 'none'; if (table) table.style.display = 'table'; _profileEditorScoresList = list.map(function(item, i) { let score = item.score != null ? Number(item.score) : 0; if (isNaN(score)) score = 0; return { index: i, title: item.title || item.name || 'Unnamed', name: item.name || 'Unnamed', recommended_score: item.recommended_score != null ? item.recommended_score : item.recommended, score: score }; }); renderProfileEditorScoresTable(); }) .catch(function() { _profileEditorScoresList = []; tbody.innerHTML = ''; if (emptyEl) emptyEl.style.display = 'block'; if (table) table.style.display = 'none'; }); } function saveProfileEditorScores() { if (!_profileEditorScoresList || _profileEditorScoresList.length === 0) return Promise.resolve(); const tbody = document.getElementById('profile-editor-scores-tbody'); if (!tbody) return Promise.resolve(); const rows = tbody.querySelectorAll('tr[data-index]'); var scores = _profileEditorScoresList.slice().map(function(o) { return o.score; }); rows.forEach(function(row) { const idx = parseInt(row.getAttribute('data-index'), 10); const input = row.querySelector('.profile-editor-score-input'); if (isNaN(idx) || idx < 0 || idx >= scores.length || !input) return; let val = parseInt(input.value, 10); if (isNaN(val)) val = 0; scores[idx] = val; }); var state = Forms._currentProfileEditing; var scoresUrl = (state && state.tvHunt) ? './api/tv-hunt/custom-formats/scores' : './api/custom-formats/scores'; return fetch(scoresUrl, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ scores: scores }) }).then(function(r) { return r.json(); }); } function markProfileEditorDirty() { _profileEditorDirty = true; const saveBtn = document.getElementById('profile-editor-save'); if (saveBtn) { saveBtn.disabled = false; saveBtn.classList.add('enabled'); } } function confirmLeaveProfileEditor(done) { if (!_profileEditorDirty) { if (typeof done === 'function') done('discard'); return true; } if (window.HuntarrConfirm && window.HuntarrConfirm.show) { window.HuntarrConfirm.show({ title: 'Unsaved Changes', message: 'You have unsaved changes that will be lost if you leave.', confirmLabel: 'Go Back', cancelLabel: 'Leave', onConfirm: function() { // Stay on the editor — modal just closes, user can save manually }, onCancel: function() { if (typeof done === 'function') done('discard'); } }); } else { if (!confirm('You have unsaved changes that will be lost. Leave anyway?')) return; if (typeof done === 'function') done('discard'); } return false; } function getCheckedQualityNamesInOrder() { const list = document.getElementById('profile-editor-qualities'); if (!list) return []; const items = list.querySelectorAll('.profile-quality-item'); var names = []; items.forEach(function(item) { var cb = item.querySelector('.profile-quality-checkbox'); var label = item.querySelector('.quality-name'); if (cb && cb.checked && label) { var n = (label.textContent || '').trim(); if (n) names.push(n); } }); return names; } function refreshProfileEditorUpgradeUntilOptions() { const select = document.getElementById('profile-editor-upgrade-until'); if (!select) return; const names = getCheckedQualityNamesInOrder(); const currentValue = (select.value || '').trim(); var optionsHtml = ''; names.forEach(function(n) { var sel = n === currentValue ? ' selected' : ''; optionsHtml += ''; }); if (optionsHtml === '') { optionsHtml = ''; } select.innerHTML = optionsHtml; if (names.length > 0 && (currentValue === '' || names.indexOf(currentValue) === -1)) { select.value = names[0]; } } function setupProfileEditorChangeDetection() { const content = document.getElementById('profile-editor-content'); const saveBtn = document.getElementById('profile-editor-save'); if (!content || !saveBtn) return; content.querySelectorAll('input:not(.profile-quality-checkbox), select').forEach(function(el) { el.addEventListener('input', markProfileEditorDirty); el.addEventListener('change', markProfileEditorDirty); }); var qualitiesList = document.getElementById('profile-editor-qualities'); if (qualitiesList) { qualitiesList.addEventListener('change', function(e) { if (e.target && e.target.classList.contains('profile-quality-checkbox')) { markProfileEditorDirty(); refreshProfileEditorUpgradeUntilOptions(); } }); } } function setupProfileQualitiesDragDrop() { const list = document.getElementById('profile-editor-qualities'); if (!list) return; const items = list.querySelectorAll('.profile-quality-item'); if (items.length === 0) return; let draggedEl = null; items.forEach(function(item) { item.setAttribute('draggable', 'true'); item.addEventListener('dragstart', function(e) { draggedEl = item; e.dataTransfer.effectAllowed = 'move'; e.dataTransfer.setData('text/plain', item.getAttribute('data-quality-id') || ''); e.dataTransfer.setData('text/html', item.outerHTML); item.classList.add('profile-quality-dragging'); e.dataTransfer.setDragImage(item, 0, 0); }); item.addEventListener('dragend', function() { item.classList.remove('profile-quality-dragging'); list.querySelectorAll('.profile-quality-item').forEach(function(el) { el.classList.remove('profile-quality-drag-over'); }); draggedEl = null; }); item.addEventListener('dragover', function(e) { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; if (draggedEl && draggedEl !== item) { item.classList.add('profile-quality-drag-over'); } }); item.addEventListener('dragleave', function() { item.classList.remove('profile-quality-drag-over'); }); item.addEventListener('drop', function(e) { e.preventDefault(); item.classList.remove('profile-quality-drag-over'); if (!draggedEl || draggedEl === item) return; var parent = item.parentNode; parent.insertBefore(draggedEl, item); markProfileEditorDirty(); refreshProfileEditorUpgradeUntilOptions(); }); }); list.addEventListener('dragover', function(e) { e.preventDefault(); e.dataTransfer.dropEffect = 'move'; }); } Forms.openProfileEditor = function(index) { const list = Forms._profilesList; if (!list || !list[index]) { fetch('./api/profiles') .then(function(r) { return r.json(); }) .then(function(data) { const profiles = (data && data.profiles) ? data.profiles : []; Forms._profilesList = profiles; if (profiles[index]) { Forms._openProfileEditorWithProfile(index, profiles[index]); } else { if (window.huntarrUI && window.huntarrUI.showNotification) { window.huntarrUI.showNotification('Profile not found.', 'error'); } } }) .catch(function() { if (window.huntarrUI && window.huntarrUI.showNotification) { window.huntarrUI.showNotification('Failed to load profile.', 'error'); } }); return; } Forms._openProfileEditorWithProfile(index, list[index]); }; /** Open profile editor for TV Hunt (independent from Movie Hunt). */ Forms.openTVHuntProfileEditor = function(profileId) { window._profileEditorTVHunt = true; fetch('./api/tv-hunt/profiles', { cache: 'no-store' }) .then(function(r) { return r.json(); }) .then(function(data) { var list = (data && data.profiles) ? data.profiles : []; var profile = list.find(function(p) { return p.id === profileId; }); if (profile) { Forms._openProfileEditorWithProfile( { tvHunt: true, profileId: profileId, originalProfile: JSON.parse(JSON.stringify(profile)) }, profile ); } else { if (window.huntarrUI && window.huntarrUI.showNotification) { window.huntarrUI.showNotification('Profile not found.', 'error'); } } }) .catch(function() { if (window.huntarrUI && window.huntarrUI.showNotification) { window.huntarrUI.showNotification('Failed to load profile.', 'error'); } }); }; Forms._openProfileEditorWithProfile = function(indexOrState, profile) { _profileEditorDirty = false; var state; if (typeof indexOrState === 'object' && indexOrState !== null && indexOrState.tvHunt) { state = indexOrState; if (!state.originalProfile) state.originalProfile = JSON.parse(JSON.stringify(profile)); } else { state = { index: indexOrState, originalProfile: JSON.parse(JSON.stringify(profile)) }; } Forms._currentProfileEditing = state; const contentEl = document.getElementById('profile-editor-content'); const saveBtn = document.getElementById('profile-editor-save'); const backBtn = document.getElementById('profile-editor-back'); if (!contentEl) return; contentEl.innerHTML = generateProfileEditorHtml(profile); if (saveBtn) { saveBtn.disabled = true; saveBtn.classList.remove('enabled'); saveBtn.onclick = function() { Forms.saveProfileFromEditor(); }; } var nextSection = state.tvHunt ? 'tv-hunt-settings-profiles' : 'settings-profiles'; if (backBtn) { backBtn.onclick = function() { confirmLeaveProfileEditor(function(result) { if (result === 'save') Forms.saveProfileFromEditor(nextSection); else if (result === 'discard') Forms.cancelProfileEditor(nextSection); }); }; } setTimeout(function() { setupProfileEditorChangeDetection(); setupProfileQualitiesDragDrop(); refreshProfileEditorUpgradeUntilOptions(); loadProfileEditorScoresTable(); }, 100); if (window.huntarrUI && window.huntarrUI.switchSection) { window.huntarrUI.switchSection('profile-editor'); } }; Forms.saveProfileFromEditor = function(optionalNextSection) { const state = Forms._currentProfileEditing; if (!state) return; const isTVHunt = state.tvHunt && state.profileId; const nextSection = optionalNextSection || (isTVHunt ? 'tv-hunt-settings-profiles' : 'settings-profiles'); const index = state.index; const nameEl = document.getElementById('profile-editor-name'); const defaultEl = document.getElementById('profile-editor-default'); const upgradesEl = document.getElementById('profile-editor-upgrades'); const upgradeUntilEl = document.getElementById('profile-editor-upgrade-until'); const minScoreEl = document.getElementById('profile-editor-min-score'); const untilScoreEl = document.getElementById('profile-editor-until-score'); const incrementEl = document.getElementById('profile-editor-increment'); const languageEl = document.getElementById('profile-editor-language'); const qualitiesContainer = document.getElementById('profile-editor-qualities'); const name = (nameEl && nameEl.value) ? nameEl.value.trim() : 'Unnamed'; const isDefault = defaultEl ? defaultEl.checked : false; const upgradesAllowed = upgradesEl ? upgradesEl.checked : true; const upgradeUntil = (upgradeUntilEl && upgradeUntilEl.value) ? upgradeUntilEl.value.trim() : 'WEB 2160p'; const minScore = minScoreEl ? parseInt(minScoreEl.value, 10) : -10000; const untilScore = untilScoreEl ? parseInt(untilScoreEl.value, 10) : 5500; const increment = incrementEl ? parseInt(incrementEl.value, 10) : 100; const language = (languageEl && languageEl.value) ? languageEl.value.trim() : 'English'; const qualities = []; if (qualitiesContainer) { const items = qualitiesContainer.querySelectorAll('.profile-quality-item'); items.forEach(function(item, i) { const cb = item.querySelector('input[type="checkbox"]'); const label = item.querySelector('.quality-name'); qualities.push({ id: item.getAttribute('data-quality-id') || 'q' + i, name: label ? label.textContent.trim() : ('Quality ' + i), enabled: cb ? cb.checked : true, order: i }); }); } const body = { name: name, is_default: isDefault, upgrades_allowed: upgradesAllowed, upgrade_until_quality: upgradeUntil, min_custom_format_score: isNaN(minScore) ? -10000 : minScore, upgrade_until_custom_format_score: isNaN(untilScore) ? 5500 : untilScore, upgrade_score_increment: isNaN(increment) ? 100 : increment, language: language, qualities: qualities }; const saveBtn = document.getElementById('profile-editor-save'); if (saveBtn) saveBtn.disabled = true; var patchUrl = isTVHunt ? './api/tv-hunt/profiles/' + encodeURIComponent(state.profileId) : './api/profiles/' + index; fetch(patchUrl, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }) .then(function(r) { return r.json(); }) .then(function(data) { if (data.success) { _profileEditorDirty = false; saveProfileEditorScores().then(function() { if (window.huntarrUI && window.huntarrUI.showNotification) { window.huntarrUI.showNotification('Profile saved.', 'success'); } if (optionalNextSection != null && window.huntarrUI && window.huntarrUI.switchSection) { window.huntarrUI.switchSection(nextSection); } if (window.MediaHuntProfiles && typeof window.MediaHuntProfiles.refreshProfilesList === 'function' && window._mediaHuntProfilesMode) { window.MediaHuntProfiles.refreshProfilesList(window._mediaHuntProfilesMode); } else if (isTVHunt && window.TVHuntSettingsForms && window.TVHuntSettingsForms.refreshTVHuntProfilesList) { window.TVHuntSettingsForms.refreshTVHuntProfilesList(); } else if (Forms.refreshProfilesList) Forms.refreshProfilesList(); }).catch(function() { if (window.huntarrUI && window.huntarrUI.showNotification) { window.huntarrUI.showNotification('Profile saved; some scores may not have saved.', 'warning'); } if (optionalNextSection != null && window.huntarrUI && window.huntarrUI.switchSection) { window.huntarrUI.switchSection(nextSection); } if (window.MediaHuntProfiles && typeof window.MediaHuntProfiles.refreshProfilesList === 'function' && window._mediaHuntProfilesMode) { window.MediaHuntProfiles.refreshProfilesList(window._mediaHuntProfilesMode); } else if (isTVHunt && window.TVHuntSettingsForms && window.TVHuntSettingsForms.refreshTVHuntProfilesList) { window.TVHuntSettingsForms.refreshTVHuntProfilesList(); } else if (Forms.refreshProfilesList) Forms.refreshProfilesList(); }); } else { if (window.huntarrUI && window.huntarrUI.showNotification) { window.huntarrUI.showNotification(data.error || 'Failed to save.', 'error'); } if (saveBtn) saveBtn.disabled = false; saveBtn.classList.add('enabled'); } }) .catch(function() { if (window.huntarrUI && window.huntarrUI.showNotification) { window.huntarrUI.showNotification('Failed to save profile.', 'error'); } if (saveBtn) saveBtn.disabled = false; saveBtn.classList.add('enabled'); }); }; Forms.cancelProfileEditor = function(optionalNextSection) { var state = Forms._currentProfileEditing; _profileEditorDirty = false; Forms._currentProfileEditing = null; var defaultSection = (state && state.tvHunt) ? 'tv-hunt-settings-profiles' : 'settings-profiles'; if (window.huntarrUI && window.huntarrUI.switchSection) { window.huntarrUI.switchSection(optionalNextSection || defaultSection); } }; Forms.isProfileEditorDirty = function() { return !!_profileEditorDirty; }; Forms.confirmLeaveProfileEditor = function(callback) { confirmLeaveProfileEditor(callback); }; })();