mirror of
https://github.com/plexguide/Huntarr.io.git
synced 2026-04-20 10:16:58 -04:00
Refactor
This commit is contained in:
@@ -337,130 +337,70 @@
|
||||
|
||||
/* ── Services Page ────────────────────────────────────────── */
|
||||
|
||||
.reqservices-section {
|
||||
margin-bottom: 32px;
|
||||
/* ── Services Page (uses shared instance-card system from instances-card.css) ── */
|
||||
/* Matches Media Hunt Instances bordered-section design exactly */
|
||||
|
||||
.reqservices-group {
|
||||
background: rgba(15, 23, 42, 0.4);
|
||||
border: 1px solid rgba(148, 163, 184, 0.08);
|
||||
border-radius: 12px;
|
||||
padding: 24px;
|
||||
margin: 12px 0 20px 0;
|
||||
}
|
||||
|
||||
.reqservices-section-header {
|
||||
.reqservices-group .profiles-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 12px;
|
||||
gap: 16px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.reqservices-section-title {
|
||||
font-size: 1.15rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
.reqservices-group .profiles-header h3 {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.reqservices-section-desc {
|
||||
font-size: 0.85rem;
|
||||
color: var(--text-muted);
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.reqservices-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 14px 16px;
|
||||
background: var(--glass-bg);
|
||||
border: 1px solid var(--glass-border);
|
||||
border-radius: var(--radius-md);
|
||||
margin-bottom: 8px;
|
||||
backdrop-filter: blur(var(--glass-blur));
|
||||
}
|
||||
|
||||
.reqservices-card-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.reqservices-card-icon {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: var(--radius-sm);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.reqservices-card-icon.movies {
|
||||
background: rgba(234, 179, 8, 0.12);
|
||||
color: #eab308;
|
||||
}
|
||||
|
||||
.reqservices-card-icon.tv {
|
||||
background: rgba(99, 102, 241, 0.12);
|
||||
color: #818cf8;
|
||||
}
|
||||
|
||||
.reqservices-card-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.reqservices-card-name {
|
||||
font-weight: 500;
|
||||
color: var(--text-primary);
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.reqservices-card-type {
|
||||
font-size: 0.78rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.reqservices-card-badges {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.reqservices-badge {
|
||||
padding: 2px 8px;
|
||||
border-radius: 10px;
|
||||
font-size: 0.7rem;
|
||||
padding: 0 0 12px 0;
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
color: #f1f5f9;
|
||||
border-bottom: 1px solid rgba(148, 163, 184, 0.12);
|
||||
}
|
||||
|
||||
.reqservices-badge-default {
|
||||
background: rgba(34, 197, 94, 0.15);
|
||||
color: #22c55e;
|
||||
.reqservices-group .profiles-help {
|
||||
color: rgba(255, 255, 255, 0.55);
|
||||
font-size: 13px;
|
||||
margin: 12px 0 0;
|
||||
}
|
||||
|
||||
.reqservices-badge-4k {
|
||||
background: rgba(234, 179, 8, 0.15);
|
||||
.reqservices-group .instances-card-grid {
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
gap: 14px;
|
||||
margin-top: 16px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.reqservices-group .instances-card-grid .add-instance-card {
|
||||
min-height: 100px;
|
||||
}
|
||||
|
||||
.reqservices-group .instances-card-grid .add-instance-card .add-icon {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.reqservices-group .instances-card-grid .add-instance-card .add-text {
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
|
||||
/* 4K inline badge (next to instance name in card header) */
|
||||
.reqservices-badge-4k-inline {
|
||||
font-size: 0.65rem;
|
||||
background: rgba(234, 179, 8, 0.25);
|
||||
color: #eab308;
|
||||
}
|
||||
|
||||
.reqservices-card-actions {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.reqservices-empty {
|
||||
padding: 32px;
|
||||
text-align: center;
|
||||
color: var(--text-muted);
|
||||
border: 2px dashed var(--border-color);
|
||||
border-radius: var(--radius-lg);
|
||||
}
|
||||
|
||||
.reqservices-empty i {
|
||||
font-size: 2rem;
|
||||
margin-bottom: 8px;
|
||||
display: block;
|
||||
opacity: 0.5;
|
||||
padding: 2px 7px;
|
||||
border-radius: 12px;
|
||||
text-transform: uppercase;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.5px;
|
||||
margin-left: 6px;
|
||||
}
|
||||
|
||||
/* ── Plex Import Modal ────────────────────────────────────── */
|
||||
|
||||
@@ -382,7 +382,8 @@
|
||||
#movie-hunt-activity-sub,
|
||||
#media-hunt-config-sub,
|
||||
#index-master-sub,
|
||||
#index-master-view-sub {
|
||||
#index-master-view-sub,
|
||||
#requestarr-user-support-sub {
|
||||
display: none;
|
||||
margin-top: 0;
|
||||
}
|
||||
@@ -391,13 +392,15 @@
|
||||
#movie-hunt-activity-sub.expanded,
|
||||
#media-hunt-config-sub.expanded,
|
||||
#index-master-sub.expanded,
|
||||
#index-master-view-sub.expanded {
|
||||
#index-master-view-sub.expanded,
|
||||
#requestarr-user-support-sub.expanded {
|
||||
display: block;
|
||||
}
|
||||
|
||||
#movie-hunt-collection-sub .nav-item-sub,
|
||||
#movie-hunt-activity-sub .nav-item-sub,
|
||||
#media-hunt-config-sub .nav-item-sub,
|
||||
#requestarr-user-support-sub .nav-item-sub,
|
||||
#index-master-sub .nav-item-sub,
|
||||
#index-master-view-sub .nav-item-sub {
|
||||
margin-top: 0;
|
||||
@@ -427,14 +430,27 @@
|
||||
}
|
||||
|
||||
/* Back button styling in config sub-group */
|
||||
.media-hunt-config-back {
|
||||
.media-hunt-config-back,
|
||||
.requestarr-user-support-back {
|
||||
opacity: 0.7;
|
||||
font-size: 0.85em;
|
||||
}
|
||||
.media-hunt-config-back:hover {
|
||||
.media-hunt-config-back:hover,
|
||||
.requestarr-user-support-back:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* When user-support sub-group is expanded, hide main Requests items + other toggles */
|
||||
#sidebar-group-requests.user-support-view > #requestarrDiscoverNav,
|
||||
#sidebar-group-requests.user-support-view > #requestarrTVNav,
|
||||
#sidebar-group-requests.user-support-view > #requestarrMoviesNav,
|
||||
#sidebar-group-requests.user-support-view > #requestarrHiddenNav,
|
||||
#sidebar-group-requests.user-support-view > #requestarrSmartHuntSettingsNav,
|
||||
#sidebar-group-requests.user-support-view > #requestarrSettingsNav,
|
||||
#sidebar-group-requests.user-support-view > #requestarrUserSupportToggle {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* ===== APP ICONS ===== */
|
||||
.app-icon-img {
|
||||
width: 20px;
|
||||
|
||||
@@ -186,7 +186,7 @@ let huntarrUI = {
|
||||
} else if (this.currentSection === 'movie-hunt-home' || this.currentSection === 'movie-hunt-collection' || this.currentSection === 'media-hunt-collection' || this.currentSection === 'activity-queue' || this.currentSection === 'activity-history' || this.currentSection === 'activity-blocklist' || this.currentSection === 'activity-logs' || this.currentSection === 'logs-media-hunt' || this.currentSection === 'settings-clients' || this.currentSection === 'movie-hunt-instance-editor') {
|
||||
console.log('[huntarrUI] Initialization - showing movie hunt sidebar');
|
||||
this.showMovieHuntSidebar();
|
||||
} else if (this.currentSection === 'requestarr' || this.currentSection === 'requestarr-discover' || this.currentSection === 'requestarr-movies' || this.currentSection === 'requestarr-tv' || this.currentSection === 'requestarr-hidden' || this.currentSection === 'requestarr-settings' || this.currentSection === 'requestarr-smarthunt-settings' || this.currentSection === 'requestarr-users' || this.currentSection === 'requestarr-services') {
|
||||
} else if (this.currentSection === 'requestarr' || this.currentSection === 'requestarr-discover' || this.currentSection === 'requestarr-movies' || this.currentSection === 'requestarr-tv' || this.currentSection === 'requestarr-hidden' || this.currentSection === 'requestarr-settings' || this.currentSection === 'requestarr-smarthunt-settings' || this.currentSection === 'requestarr-users' || this.currentSection === 'requestarr-services' || this.currentSection === 'requestarr-requests') {
|
||||
if (this._enableRequestarr === false) {
|
||||
console.log('[huntarrUI] Requestarr disabled - redirecting to home');
|
||||
this.switchSection('home');
|
||||
@@ -294,14 +294,18 @@ let huntarrUI = {
|
||||
.then(r => r.ok ? r.json() : null)
|
||||
.then(data => {
|
||||
var badge = document.getElementById('requestarr-pending-badge');
|
||||
if (!badge) return;
|
||||
var mirrors = document.querySelectorAll('.requestarr-pending-badge-mirror');
|
||||
var count = (data && data.count) || 0;
|
||||
if (count > 0) {
|
||||
badge.textContent = count > 99 ? '99+' : String(count);
|
||||
badge.style.display = '';
|
||||
} else {
|
||||
badge.style.display = 'none';
|
||||
var text = count > 99 ? '99+' : String(count);
|
||||
var show = count > 0;
|
||||
if (badge) {
|
||||
badge.textContent = text;
|
||||
badge.style.display = show ? '' : 'none';
|
||||
}
|
||||
mirrors.forEach(function(m) {
|
||||
m.textContent = text;
|
||||
m.style.display = show ? '' : 'none';
|
||||
});
|
||||
})
|
||||
.catch(function() {});
|
||||
},
|
||||
@@ -347,6 +351,7 @@ let huntarrUI = {
|
||||
'requestarrUsersNav',
|
||||
'requestarrServicesNav',
|
||||
'requestarrSettingsNav',
|
||||
'requestarrUserSupportToggle',
|
||||
];
|
||||
hideNavItems.forEach(function(id) {
|
||||
var el = document.getElementById(id);
|
||||
|
||||
19
frontend/static/js/dist/bundle-app.js
vendored
19
frontend/static/js/dist/bundle-app.js
vendored
@@ -188,7 +188,7 @@ let huntarrUI = {
|
||||
} else if (this.currentSection === 'movie-hunt-home' || this.currentSection === 'movie-hunt-collection' || this.currentSection === 'media-hunt-collection' || this.currentSection === 'activity-queue' || this.currentSection === 'activity-history' || this.currentSection === 'activity-blocklist' || this.currentSection === 'activity-logs' || this.currentSection === 'logs-media-hunt' || this.currentSection === 'settings-clients' || this.currentSection === 'movie-hunt-instance-editor') {
|
||||
console.log('[huntarrUI] Initialization - showing movie hunt sidebar');
|
||||
this.showMovieHuntSidebar();
|
||||
} else if (this.currentSection === 'requestarr' || this.currentSection === 'requestarr-discover' || this.currentSection === 'requestarr-movies' || this.currentSection === 'requestarr-tv' || this.currentSection === 'requestarr-hidden' || this.currentSection === 'requestarr-settings' || this.currentSection === 'requestarr-smarthunt-settings' || this.currentSection === 'requestarr-users' || this.currentSection === 'requestarr-services') {
|
||||
} else if (this.currentSection === 'requestarr' || this.currentSection === 'requestarr-discover' || this.currentSection === 'requestarr-movies' || this.currentSection === 'requestarr-tv' || this.currentSection === 'requestarr-hidden' || this.currentSection === 'requestarr-settings' || this.currentSection === 'requestarr-smarthunt-settings' || this.currentSection === 'requestarr-users' || this.currentSection === 'requestarr-services' || this.currentSection === 'requestarr-requests') {
|
||||
if (this._enableRequestarr === false) {
|
||||
console.log('[huntarrUI] Requestarr disabled - redirecting to home');
|
||||
this.switchSection('home');
|
||||
@@ -296,14 +296,18 @@ let huntarrUI = {
|
||||
.then(r => r.ok ? r.json() : null)
|
||||
.then(data => {
|
||||
var badge = document.getElementById('requestarr-pending-badge');
|
||||
if (!badge) return;
|
||||
var mirrors = document.querySelectorAll('.requestarr-pending-badge-mirror');
|
||||
var count = (data && data.count) || 0;
|
||||
if (count > 0) {
|
||||
badge.textContent = count > 99 ? '99+' : String(count);
|
||||
badge.style.display = '';
|
||||
} else {
|
||||
badge.style.display = 'none';
|
||||
var text = count > 99 ? '99+' : String(count);
|
||||
var show = count > 0;
|
||||
if (badge) {
|
||||
badge.textContent = text;
|
||||
badge.style.display = show ? '' : 'none';
|
||||
}
|
||||
mirrors.forEach(function(m) {
|
||||
m.textContent = text;
|
||||
m.style.display = show ? '' : 'none';
|
||||
});
|
||||
})
|
||||
.catch(function() {});
|
||||
},
|
||||
@@ -349,6 +353,7 @@ let huntarrUI = {
|
||||
'requestarrUsersNav',
|
||||
'requestarrServicesNav',
|
||||
'requestarrSettingsNav',
|
||||
'requestarrUserSupportToggle',
|
||||
];
|
||||
hideNavItems.forEach(function(id) {
|
||||
var el = document.getElementById(id);
|
||||
|
||||
138
frontend/static/js/dist/requestarr-bundle.js
vendored
138
frontend/static/js/dist/requestarr-bundle.js
vendored
@@ -1686,14 +1686,16 @@ class RequestarrSettings {
|
||||
|
||||
try {
|
||||
const _ts = Date.now();
|
||||
const [movieHuntResponse, radarrResponse, sonarrResponse] = await Promise.all([
|
||||
const [movieHuntResponse, radarrResponse, tvHuntResponse, sonarrResponse] = await Promise.all([
|
||||
fetch(`./api/requestarr/instances/movie_hunt?t=${_ts}`, { cache: 'no-store' }),
|
||||
fetch(`./api/requestarr/instances/radarr?t=${_ts}`, { cache: 'no-store' }),
|
||||
fetch(`./api/requestarr/instances/tv_hunt?t=${_ts}`, { cache: 'no-store' }),
|
||||
fetch(`./api/requestarr/instances/sonarr?t=${_ts}`, { cache: 'no-store' })
|
||||
]);
|
||||
|
||||
const movieHuntData = await movieHuntResponse.json();
|
||||
const radarrData = await radarrResponse.json();
|
||||
const tvHuntData = await tvHuntResponse.json();
|
||||
const sonarrData = await sonarrResponse.json();
|
||||
|
||||
const instanceOptions = [];
|
||||
@@ -1717,6 +1719,16 @@ class RequestarrSettings {
|
||||
}
|
||||
});
|
||||
|
||||
// TV Hunt instances
|
||||
(tvHuntData.instances || []).forEach(instance => {
|
||||
if (instance && instance.name) {
|
||||
instanceOptions.push({
|
||||
value: `tv_hunt::${instance.name}`,
|
||||
label: `TV Hunt \u2013 ${instance.name}`
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
(sonarrData.instances || []).forEach(instance => {
|
||||
if (instance && instance.name) {
|
||||
instanceOptions.push({
|
||||
@@ -3497,12 +3509,10 @@ class RequestarrContent {
|
||||
try {
|
||||
const _ts = Date.now();
|
||||
const [thResponse, sonarrResponse] = await Promise.all([
|
||||
fetch(`./api/tv-hunt/instances?t=${_ts}`, { cache: 'no-store' }),
|
||||
fetch(`./api/requestarr/instances/tv_hunt?t=${_ts}`, { cache: 'no-store' }),
|
||||
fetch(`./api/requestarr/instances/sonarr?t=${_ts}`, { cache: 'no-store' })
|
||||
]);
|
||||
let thData = await thResponse.json();
|
||||
if (!thResponse.ok || thData.error) thData = { instances: [] };
|
||||
else thData = { instances: (thData.instances || []).filter(i => i.enabled !== false) };
|
||||
const thData = await thResponse.json();
|
||||
const sonarrData = await sonarrResponse.json();
|
||||
|
||||
const allInstances = [
|
||||
@@ -3775,15 +3785,13 @@ class RequestarrContent {
|
||||
select.innerHTML = '<option value="">Loading instances...</option>';
|
||||
|
||||
try {
|
||||
// Fetch TV Hunt from Media Hunt API (canonical); Sonarr from requestarr
|
||||
// Fetch TV Hunt and Sonarr from requestarr (filtered by services config)
|
||||
const _ts = Date.now();
|
||||
const [thResponse, sonarrResponse] = await Promise.all([
|
||||
fetch(`./api/tv-hunt/instances?t=${_ts}`, { cache: 'no-store' }),
|
||||
fetch(`./api/requestarr/instances/tv_hunt?t=${_ts}`, { cache: 'no-store' }),
|
||||
fetch(`./api/requestarr/instances/sonarr?t=${_ts}`, { cache: 'no-store' })
|
||||
]);
|
||||
let thData = await thResponse.json();
|
||||
if (!thResponse.ok || thData.error) thData = { instances: [] };
|
||||
else thData = { instances: (thData.instances || []).filter(i => i.enabled !== false) };
|
||||
const thData = await thResponse.json();
|
||||
const sonarrData = await sonarrResponse.json();
|
||||
|
||||
const thInstances = (thData.instances || []).map(inst => ({
|
||||
@@ -7456,6 +7464,8 @@ window.RequestarrUsers = {
|
||||
* Manages which instances are available for media requests.
|
||||
* Movies = Radarr + Movie Hunt instances
|
||||
* TV = Sonarr + TV Hunt instances
|
||||
*
|
||||
* Uses the same instance-card design system as Media Hunt Instances.
|
||||
*/
|
||||
|
||||
window.RequestarrServices = {
|
||||
@@ -7495,27 +7505,38 @@ window.RequestarrServices = {
|
||||
const movieServices = this.services.filter(s => s.service_type === 'movies');
|
||||
const tvServices = this.services.filter(s => s.service_type === 'tv');
|
||||
|
||||
container.innerHTML = `
|
||||
${this._renderSection('Movies', 'movies', movieServices, 'fas fa-film')}
|
||||
${this._renderSection('TV', 'tv', tvServices, 'fas fa-tv')}
|
||||
`;
|
||||
container.innerHTML =
|
||||
this._renderSection('Movies', 'movies', movieServices, 'fa-film') +
|
||||
this._renderSection('TV', 'tv', tvServices, 'fa-tv');
|
||||
|
||||
// Wire up click handlers on the grids
|
||||
this._wireGrid('reqservices-movies-grid', 'movies');
|
||||
this._wireGrid('reqservices-tv-grid', 'tv');
|
||||
},
|
||||
|
||||
_renderSection(title, type, services, icon) {
|
||||
const cardsHtml = services.length ? services.map(s => this._renderCard(s, type)).join('') :
|
||||
`<div class="reqservices-empty">
|
||||
<i class="${icon}"></i>
|
||||
<p>No ${title.toLowerCase()} services configured</p>
|
||||
<p style="font-size:0.8rem;">Add an instance to enable ${title.toLowerCase()} requests</p>
|
||||
</div>`;
|
||||
_renderSection(title, type, services, iconClass) {
|
||||
const iconColor = type === 'movies' ? '#eab308' : '#818cf8';
|
||||
const gridId = `reqservices-${type}-grid`;
|
||||
const addLabel = `Add ${title} Server`;
|
||||
|
||||
return `<div class="reqservices-section">
|
||||
<div class="reqservices-section-header">
|
||||
<h3 class="reqservices-section-title"><i class="${icon}" style="color:${type === 'movies' ? '#eab308' : '#818cf8'};"></i> ${title}</h3>
|
||||
<button class="requsers-btn requsers-btn-primary requsers-btn-sm" onclick="RequestarrServices.openAddModal('${type}')"><i class="fas fa-plus"></i> Add ${title} Server</button>
|
||||
// Sort: default first (moves to the left)
|
||||
services.sort((a, b) => (b.is_default ? 1 : 0) - (a.is_default ? 1 : 0));
|
||||
|
||||
let cardsHtml = '';
|
||||
services.forEach(s => { cardsHtml += this._renderCard(s, type); });
|
||||
// Add-instance card (dashed)
|
||||
cardsHtml += `<div class="add-instance-card" data-action="add" data-type="${type}">` +
|
||||
`<div class="add-icon"><i class="fas fa-plus-circle"></i></div>` +
|
||||
`<div class="add-text">${addLabel}</div></div>`;
|
||||
|
||||
return `<div class="settings-group instances-settings-group reqservices-group">
|
||||
<div class="profiles-header">
|
||||
<div>
|
||||
<h3><i class="fas ${iconClass}" style="color:${iconColor};margin-right:8px;"></i>${title}</h3>
|
||||
<p class="profiles-help">Configure your ${title.toLowerCase()} servers below. You can connect multiple servers and mark defaults.</p>
|
||||
</div>
|
||||
</div>
|
||||
<p class="reqservices-section-desc">Configure your ${title.toLowerCase()} server${services.length !== 1 ? 's' : ''} below. You can connect multiple servers and mark defaults.</p>
|
||||
${cardsHtml}
|
||||
<div class="instance-card-grid instances-card-grid" id="${gridId}">${cardsHtml}</div>
|
||||
</div>`;
|
||||
},
|
||||
|
||||
@@ -7525,34 +7546,57 @@ window.RequestarrServices = {
|
||||
movie_hunt: 'Movie Hunt', tv_hunt: 'TV Hunt'
|
||||
}[service.app_type] || service.app_type;
|
||||
|
||||
const badges = [];
|
||||
if (service.is_default) badges.push('<span class="reqservices-badge reqservices-badge-default">Default</span>');
|
||||
if (service.is_4k) badges.push('<span class="reqservices-badge reqservices-badge-4k">4K</span>');
|
||||
const iconClass = type === 'movies' ? 'fa-film' : 'fa-tv';
|
||||
const isDefault = !!service.is_default;
|
||||
const is4k = !!service.is_4k;
|
||||
const statusClass = 'status-connected';
|
||||
const statusIcon = 'fa-check-circle';
|
||||
|
||||
return `<div class="reqservices-card" data-service-id="${service.id}">
|
||||
<div class="reqservices-card-left">
|
||||
<div class="reqservices-card-icon ${type}"><i class="fas ${type === 'movies' ? 'fa-film' : 'fa-tv'}"></i></div>
|
||||
<div class="reqservices-card-info">
|
||||
<span class="reqservices-card-name">${this._esc(service.instance_name)}</span>
|
||||
<span class="reqservices-card-type">${appLabel}</span>
|
||||
</div>
|
||||
<div class="reqservices-card-badges">${badges.join('')}</div>
|
||||
const defaultBadge = isDefault ? ' <span class="default-badge">Default</span>' : '';
|
||||
const fourKBadge = is4k ? ' <span class="reqservices-badge-4k-inline">4K</span>' : '';
|
||||
|
||||
// Default button only shown on non-default cards (like Media Hunt Instances)
|
||||
const defaultBtn = isDefault ? '' : `<button type="button" class="btn-card set-default" data-id="${service.id}"><i class="fas fa-star"></i> Default</button>`;
|
||||
const fourKBtn = is4k
|
||||
? `<button type="button" class="btn-card reqsvc-un4k" data-id="${service.id}" title="Remove 4K flag"><span style="font-weight:700;color:#eab308;">4K</span></button>`
|
||||
: `<button type="button" class="btn-card reqsvc-set4k" data-id="${service.id}" title="Mark as 4K"><span style="font-weight:700;color:var(--text-dim);">4K</span></button>`;
|
||||
const deleteBtn = `<button type="button" class="btn-card delete" data-id="${service.id}"><i class="fas fa-trash"></i> Delete</button>`;
|
||||
|
||||
return `<div class="instance-card${isDefault ? ' default-instance' : ''}" data-service-id="${service.id}">
|
||||
<div class="instance-card-header">
|
||||
<span class="instance-name"><i class="fas ${iconClass}" style="margin-right:8px;"></i>${this._esc(service.instance_name)}${defaultBadge}${fourKBadge}</span>
|
||||
<div class="instance-status-icon ${statusClass}"><i class="fas ${statusIcon}"></i></div>
|
||||
</div>
|
||||
<div class="reqservices-card-actions">
|
||||
<button class="requsers-btn requsers-btn-sm" style="background:var(--bg-tertiary);color:var(--text-secondary);" onclick="RequestarrServices.toggleDefault(${service.id}, ${!service.is_default})" title="${service.is_default ? 'Remove default' : 'Set as default'}">
|
||||
<i class="fas fa-star" style="color:${service.is_default ? '#22c55e' : 'var(--text-dim)'};"></i>
|
||||
</button>
|
||||
<button class="requsers-btn requsers-btn-sm" style="background:var(--bg-tertiary);color:var(--text-secondary);" onclick="RequestarrServices.toggle4K(${service.id}, ${!service.is_4k})" title="${service.is_4k ? 'Remove 4K flag' : 'Mark as 4K'}">
|
||||
<span style="font-weight:700;font-size:0.75rem;color:${service.is_4k ? '#eab308' : 'var(--text-dim)'};">4K</span>
|
||||
</button>
|
||||
<button class="requsers-btn requsers-btn-danger requsers-btn-sm" onclick="RequestarrServices.removeService(${service.id})" title="Remove"><i class="fas fa-trash"></i></button>
|
||||
<div class="instance-card-body">
|
||||
<div class="instance-detail"><i class="fas fa-server"></i><span>${appLabel}</span></div>
|
||||
</div>
|
||||
<div class="instance-card-footer">${defaultBtn}${fourKBtn}${deleteBtn}</div>
|
||||
</div>`;
|
||||
},
|
||||
|
||||
_wireGrid(gridId, type) {
|
||||
const grid = document.getElementById(gridId);
|
||||
if (!grid) return;
|
||||
grid.addEventListener('click', (e) => {
|
||||
const addCard = e.target.closest('.add-instance-card[data-action="add"]');
|
||||
if (addCard) { e.preventDefault(); this.openAddModal(type); return; }
|
||||
|
||||
const defaultBtn = e.target.closest('.btn-card.set-default');
|
||||
if (defaultBtn) { e.stopPropagation(); this.toggleDefault(parseInt(defaultBtn.dataset.id), true); return; }
|
||||
|
||||
const set4kBtn = e.target.closest('.btn-card.reqsvc-set4k');
|
||||
if (set4kBtn) { e.stopPropagation(); this.toggle4K(parseInt(set4kBtn.dataset.id), true); return; }
|
||||
|
||||
const un4kBtn = e.target.closest('.btn-card.reqsvc-un4k');
|
||||
if (un4kBtn) { e.stopPropagation(); this.toggle4K(parseInt(un4kBtn.dataset.id), false); return; }
|
||||
|
||||
const deleteBtn = e.target.closest('.btn-card.delete');
|
||||
if (deleteBtn) { e.stopPropagation(); this.removeService(parseInt(deleteBtn.dataset.id)); return; }
|
||||
});
|
||||
},
|
||||
|
||||
openAddModal(serviceType) {
|
||||
const available = (serviceType === 'movies') ? (this.available.movies || []) : (this.available.tv || []);
|
||||
// Filter out already-added instances
|
||||
const existing = new Set(this.services.filter(s => s.service_type === serviceType).map(s => `${s.app_type}:${s.instance_name}`));
|
||||
const filtered = available.filter(a => !existing.has(`${a.app_type}:${a.instance_name}`));
|
||||
|
||||
|
||||
@@ -249,12 +249,10 @@ export class RequestarrContent {
|
||||
try {
|
||||
const _ts = Date.now();
|
||||
const [thResponse, sonarrResponse] = await Promise.all([
|
||||
fetch(`./api/tv-hunt/instances?t=${_ts}`, { cache: 'no-store' }),
|
||||
fetch(`./api/requestarr/instances/tv_hunt?t=${_ts}`, { cache: 'no-store' }),
|
||||
fetch(`./api/requestarr/instances/sonarr?t=${_ts}`, { cache: 'no-store' })
|
||||
]);
|
||||
let thData = await thResponse.json();
|
||||
if (!thResponse.ok || thData.error) thData = { instances: [] };
|
||||
else thData = { instances: (thData.instances || []).filter(i => i.enabled !== false) };
|
||||
const thData = await thResponse.json();
|
||||
const sonarrData = await sonarrResponse.json();
|
||||
|
||||
const allInstances = [
|
||||
@@ -527,15 +525,13 @@ export class RequestarrContent {
|
||||
select.innerHTML = '<option value="">Loading instances...</option>';
|
||||
|
||||
try {
|
||||
// Fetch TV Hunt from Media Hunt API (canonical); Sonarr from requestarr
|
||||
// Fetch TV Hunt and Sonarr from requestarr (filtered by services config)
|
||||
const _ts = Date.now();
|
||||
const [thResponse, sonarrResponse] = await Promise.all([
|
||||
fetch(`./api/tv-hunt/instances?t=${_ts}`, { cache: 'no-store' }),
|
||||
fetch(`./api/requestarr/instances/tv_hunt?t=${_ts}`, { cache: 'no-store' }),
|
||||
fetch(`./api/requestarr/instances/sonarr?t=${_ts}`, { cache: 'no-store' })
|
||||
]);
|
||||
let thData = await thResponse.json();
|
||||
if (!thResponse.ok || thData.error) thData = { instances: [] };
|
||||
else thData = { instances: (thData.instances || []).filter(i => i.enabled !== false) };
|
||||
const thData = await thResponse.json();
|
||||
const sonarrData = await sonarrResponse.json();
|
||||
|
||||
const thInstances = (thData.instances || []).map(inst => ({
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
* Manages which instances are available for media requests.
|
||||
* Movies = Radarr + Movie Hunt instances
|
||||
* TV = Sonarr + TV Hunt instances
|
||||
*
|
||||
* Uses the same instance-card design system as Media Hunt Instances.
|
||||
*/
|
||||
|
||||
window.RequestarrServices = {
|
||||
@@ -42,27 +44,38 @@ window.RequestarrServices = {
|
||||
const movieServices = this.services.filter(s => s.service_type === 'movies');
|
||||
const tvServices = this.services.filter(s => s.service_type === 'tv');
|
||||
|
||||
container.innerHTML = `
|
||||
${this._renderSection('Movies', 'movies', movieServices, 'fas fa-film')}
|
||||
${this._renderSection('TV', 'tv', tvServices, 'fas fa-tv')}
|
||||
`;
|
||||
container.innerHTML =
|
||||
this._renderSection('Movies', 'movies', movieServices, 'fa-film') +
|
||||
this._renderSection('TV', 'tv', tvServices, 'fa-tv');
|
||||
|
||||
// Wire up click handlers on the grids
|
||||
this._wireGrid('reqservices-movies-grid', 'movies');
|
||||
this._wireGrid('reqservices-tv-grid', 'tv');
|
||||
},
|
||||
|
||||
_renderSection(title, type, services, icon) {
|
||||
const cardsHtml = services.length ? services.map(s => this._renderCard(s, type)).join('') :
|
||||
`<div class="reqservices-empty">
|
||||
<i class="${icon}"></i>
|
||||
<p>No ${title.toLowerCase()} services configured</p>
|
||||
<p style="font-size:0.8rem;">Add an instance to enable ${title.toLowerCase()} requests</p>
|
||||
</div>`;
|
||||
_renderSection(title, type, services, iconClass) {
|
||||
const iconColor = type === 'movies' ? '#eab308' : '#818cf8';
|
||||
const gridId = `reqservices-${type}-grid`;
|
||||
const addLabel = `Add ${title} Server`;
|
||||
|
||||
return `<div class="reqservices-section">
|
||||
<div class="reqservices-section-header">
|
||||
<h3 class="reqservices-section-title"><i class="${icon}" style="color:${type === 'movies' ? '#eab308' : '#818cf8'};"></i> ${title}</h3>
|
||||
<button class="requsers-btn requsers-btn-primary requsers-btn-sm" onclick="RequestarrServices.openAddModal('${type}')"><i class="fas fa-plus"></i> Add ${title} Server</button>
|
||||
// Sort: default first (moves to the left)
|
||||
services.sort((a, b) => (b.is_default ? 1 : 0) - (a.is_default ? 1 : 0));
|
||||
|
||||
let cardsHtml = '';
|
||||
services.forEach(s => { cardsHtml += this._renderCard(s, type); });
|
||||
// Add-instance card (dashed)
|
||||
cardsHtml += `<div class="add-instance-card" data-action="add" data-type="${type}">` +
|
||||
`<div class="add-icon"><i class="fas fa-plus-circle"></i></div>` +
|
||||
`<div class="add-text">${addLabel}</div></div>`;
|
||||
|
||||
return `<div class="settings-group instances-settings-group reqservices-group">
|
||||
<div class="profiles-header">
|
||||
<div>
|
||||
<h3><i class="fas ${iconClass}" style="color:${iconColor};margin-right:8px;"></i>${title}</h3>
|
||||
<p class="profiles-help">Configure your ${title.toLowerCase()} servers below. You can connect multiple servers and mark defaults.</p>
|
||||
</div>
|
||||
</div>
|
||||
<p class="reqservices-section-desc">Configure your ${title.toLowerCase()} server${services.length !== 1 ? 's' : ''} below. You can connect multiple servers and mark defaults.</p>
|
||||
${cardsHtml}
|
||||
<div class="instance-card-grid instances-card-grid" id="${gridId}">${cardsHtml}</div>
|
||||
</div>`;
|
||||
},
|
||||
|
||||
@@ -72,34 +85,57 @@ window.RequestarrServices = {
|
||||
movie_hunt: 'Movie Hunt', tv_hunt: 'TV Hunt'
|
||||
}[service.app_type] || service.app_type;
|
||||
|
||||
const badges = [];
|
||||
if (service.is_default) badges.push('<span class="reqservices-badge reqservices-badge-default">Default</span>');
|
||||
if (service.is_4k) badges.push('<span class="reqservices-badge reqservices-badge-4k">4K</span>');
|
||||
const iconClass = type === 'movies' ? 'fa-film' : 'fa-tv';
|
||||
const isDefault = !!service.is_default;
|
||||
const is4k = !!service.is_4k;
|
||||
const statusClass = 'status-connected';
|
||||
const statusIcon = 'fa-check-circle';
|
||||
|
||||
return `<div class="reqservices-card" data-service-id="${service.id}">
|
||||
<div class="reqservices-card-left">
|
||||
<div class="reqservices-card-icon ${type}"><i class="fas ${type === 'movies' ? 'fa-film' : 'fa-tv'}"></i></div>
|
||||
<div class="reqservices-card-info">
|
||||
<span class="reqservices-card-name">${this._esc(service.instance_name)}</span>
|
||||
<span class="reqservices-card-type">${appLabel}</span>
|
||||
</div>
|
||||
<div class="reqservices-card-badges">${badges.join('')}</div>
|
||||
const defaultBadge = isDefault ? ' <span class="default-badge">Default</span>' : '';
|
||||
const fourKBadge = is4k ? ' <span class="reqservices-badge-4k-inline">4K</span>' : '';
|
||||
|
||||
// Default button only shown on non-default cards (like Media Hunt Instances)
|
||||
const defaultBtn = isDefault ? '' : `<button type="button" class="btn-card set-default" data-id="${service.id}"><i class="fas fa-star"></i> Default</button>`;
|
||||
const fourKBtn = is4k
|
||||
? `<button type="button" class="btn-card reqsvc-un4k" data-id="${service.id}" title="Remove 4K flag"><span style="font-weight:700;color:#eab308;">4K</span></button>`
|
||||
: `<button type="button" class="btn-card reqsvc-set4k" data-id="${service.id}" title="Mark as 4K"><span style="font-weight:700;color:var(--text-dim);">4K</span></button>`;
|
||||
const deleteBtn = `<button type="button" class="btn-card delete" data-id="${service.id}"><i class="fas fa-trash"></i> Delete</button>`;
|
||||
|
||||
return `<div class="instance-card${isDefault ? ' default-instance' : ''}" data-service-id="${service.id}">
|
||||
<div class="instance-card-header">
|
||||
<span class="instance-name"><i class="fas ${iconClass}" style="margin-right:8px;"></i>${this._esc(service.instance_name)}${defaultBadge}${fourKBadge}</span>
|
||||
<div class="instance-status-icon ${statusClass}"><i class="fas ${statusIcon}"></i></div>
|
||||
</div>
|
||||
<div class="reqservices-card-actions">
|
||||
<button class="requsers-btn requsers-btn-sm" style="background:var(--bg-tertiary);color:var(--text-secondary);" onclick="RequestarrServices.toggleDefault(${service.id}, ${!service.is_default})" title="${service.is_default ? 'Remove default' : 'Set as default'}">
|
||||
<i class="fas fa-star" style="color:${service.is_default ? '#22c55e' : 'var(--text-dim)'};"></i>
|
||||
</button>
|
||||
<button class="requsers-btn requsers-btn-sm" style="background:var(--bg-tertiary);color:var(--text-secondary);" onclick="RequestarrServices.toggle4K(${service.id}, ${!service.is_4k})" title="${service.is_4k ? 'Remove 4K flag' : 'Mark as 4K'}">
|
||||
<span style="font-weight:700;font-size:0.75rem;color:${service.is_4k ? '#eab308' : 'var(--text-dim)'};">4K</span>
|
||||
</button>
|
||||
<button class="requsers-btn requsers-btn-danger requsers-btn-sm" onclick="RequestarrServices.removeService(${service.id})" title="Remove"><i class="fas fa-trash"></i></button>
|
||||
<div class="instance-card-body">
|
||||
<div class="instance-detail"><i class="fas fa-server"></i><span>${appLabel}</span></div>
|
||||
</div>
|
||||
<div class="instance-card-footer">${defaultBtn}${fourKBtn}${deleteBtn}</div>
|
||||
</div>`;
|
||||
},
|
||||
|
||||
_wireGrid(gridId, type) {
|
||||
const grid = document.getElementById(gridId);
|
||||
if (!grid) return;
|
||||
grid.addEventListener('click', (e) => {
|
||||
const addCard = e.target.closest('.add-instance-card[data-action="add"]');
|
||||
if (addCard) { e.preventDefault(); this.openAddModal(type); return; }
|
||||
|
||||
const defaultBtn = e.target.closest('.btn-card.set-default');
|
||||
if (defaultBtn) { e.stopPropagation(); this.toggleDefault(parseInt(defaultBtn.dataset.id), true); return; }
|
||||
|
||||
const set4kBtn = e.target.closest('.btn-card.reqsvc-set4k');
|
||||
if (set4kBtn) { e.stopPropagation(); this.toggle4K(parseInt(set4kBtn.dataset.id), true); return; }
|
||||
|
||||
const un4kBtn = e.target.closest('.btn-card.reqsvc-un4k');
|
||||
if (un4kBtn) { e.stopPropagation(); this.toggle4K(parseInt(un4kBtn.dataset.id), false); return; }
|
||||
|
||||
const deleteBtn = e.target.closest('.btn-card.delete');
|
||||
if (deleteBtn) { e.stopPropagation(); this.removeService(parseInt(deleteBtn.dataset.id)); return; }
|
||||
});
|
||||
},
|
||||
|
||||
openAddModal(serviceType) {
|
||||
const available = (serviceType === 'movies') ? (this.available.movies || []) : (this.available.tv || []);
|
||||
// Filter out already-added instances
|
||||
const existing = new Set(this.services.filter(s => s.service_type === serviceType).map(s => `${s.app_type}:${s.instance_name}`));
|
||||
const filtered = available.filter(a => !existing.has(`${a.app_type}:${a.instance_name}`));
|
||||
|
||||
|
||||
@@ -203,14 +203,16 @@ export class RequestarrSettings {
|
||||
|
||||
try {
|
||||
const _ts = Date.now();
|
||||
const [movieHuntResponse, radarrResponse, sonarrResponse] = await Promise.all([
|
||||
const [movieHuntResponse, radarrResponse, tvHuntResponse, sonarrResponse] = await Promise.all([
|
||||
fetch(`./api/requestarr/instances/movie_hunt?t=${_ts}`, { cache: 'no-store' }),
|
||||
fetch(`./api/requestarr/instances/radarr?t=${_ts}`, { cache: 'no-store' }),
|
||||
fetch(`./api/requestarr/instances/tv_hunt?t=${_ts}`, { cache: 'no-store' }),
|
||||
fetch(`./api/requestarr/instances/sonarr?t=${_ts}`, { cache: 'no-store' })
|
||||
]);
|
||||
|
||||
const movieHuntData = await movieHuntResponse.json();
|
||||
const radarrData = await radarrResponse.json();
|
||||
const tvHuntData = await tvHuntResponse.json();
|
||||
const sonarrData = await sonarrResponse.json();
|
||||
|
||||
const instanceOptions = [];
|
||||
@@ -234,6 +236,16 @@ export class RequestarrSettings {
|
||||
}
|
||||
});
|
||||
|
||||
// TV Hunt instances
|
||||
(tvHuntData.instances || []).forEach(instance => {
|
||||
if (instance && instance.name) {
|
||||
instanceOptions.push({
|
||||
value: `tv_hunt::${instance.name}`,
|
||||
label: `TV Hunt \u2013 ${instance.name}`
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
(sonarrData.instances || []).forEach(instance => {
|
||||
if (instance && instance.name) {
|
||||
instanceOptions.push({
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<!-- Bundled scripts (run: python scripts/build_js_bundles.py to rebuild) -->
|
||||
<script defer src="./static/js/dist/bundle-core.js?v=15"></script>
|
||||
<script defer src="./static/js/dist/bundle-app.js?v=29"></script>
|
||||
<script defer src="./static/js/dist/bundle-app.js?v=30"></script>
|
||||
<script defer src="./static/js/dist/bundle-settings.js?v=41"></script>
|
||||
<script defer src="./static/js/dist/bundle-features.js?v=19"></script>
|
||||
<script defer src="./static/js/dist/bundle-media.js?v=23"></script>
|
||||
<script defer src="./static/js/dist/bundle-misc.js?v=25"></script>
|
||||
<!-- Requestarr (Vite-bundled, minified) -->
|
||||
<script defer src="./static/js/dist/requestarr-bundle.js?v=32"></script>
|
||||
<script defer src="./static/js/dist/requestarr-bundle.js?v=37"></script>
|
||||
|
||||
@@ -47,27 +47,38 @@
|
||||
<div class="nav-icon-wrapper"><i class="fas fa-eye-slash"></i></div>
|
||||
<span>Hidden Media</span>
|
||||
</a>
|
||||
<a href="./#requestarr-requests" class="nav-item nav-item-sub" id="requestarrRequestsNav">
|
||||
<div class="nav-icon-wrapper"><i class="fas fa-inbox"></i></div>
|
||||
<span>Requests</span>
|
||||
<span class="nav-badge" id="requestarr-pending-badge" style="display:none;">0</span>
|
||||
</a>
|
||||
<a href="./#requestarr-smarthunt-settings" class="nav-item nav-item-sub" id="requestarrSmartHuntSettingsNav">
|
||||
<div class="nav-icon-wrapper"><i class="fas fa-fire"></i></div>
|
||||
<span>Smart Hunt</span>
|
||||
</a>
|
||||
<a href="./#requestarr-users" class="nav-item nav-item-sub" id="requestarrUsersNav">
|
||||
<div class="nav-icon-wrapper"><i class="fas fa-users"></i></div>
|
||||
<span>Users</span>
|
||||
</a>
|
||||
<a href="./#requestarr-services" class="nav-item nav-item-sub" id="requestarrServicesNav">
|
||||
<div class="nav-icon-wrapper"><i class="fas fa-server"></i></div>
|
||||
<span>Services</span>
|
||||
</a>
|
||||
<a href="./#requestarr-settings" class="nav-item nav-item-sub" id="requestarrSettingsNav">
|
||||
<div class="nav-icon-wrapper"><i class="fas fa-cog"></i></div>
|
||||
<span>Settings</span>
|
||||
</a>
|
||||
<a href="./#requestarr-users" class="nav-item nav-item-sub" id="requestarrUserSupportToggle">
|
||||
<div class="nav-icon-wrapper"><i class="fas fa-headset"></i></div>
|
||||
<span>User Support</span>
|
||||
<span class="nav-badge" id="requestarr-pending-badge" style="display:none;">0</span>
|
||||
</a>
|
||||
<div class="nav-sub-group" id="requestarr-user-support-sub">
|
||||
<a href="./#requestarr-discover" class="nav-item nav-item-sub requestarr-user-support-back" id="requestarrUserSupportBackNav">
|
||||
<div class="nav-icon-wrapper"><i class="fas fa-arrow-left"></i></div>
|
||||
<span>Back</span>
|
||||
</a>
|
||||
<a href="./#requestarr-services" class="nav-item nav-item-sub" id="requestarrServicesNav">
|
||||
<div class="nav-icon-wrapper"><i class="fas fa-server"></i></div>
|
||||
<span>Support Instances</span>
|
||||
</a>
|
||||
<a href="./#requestarr-users" class="nav-item nav-item-sub" id="requestarrUsersNav">
|
||||
<div class="nav-icon-wrapper"><i class="fas fa-users"></i></div>
|
||||
<span>Users</span>
|
||||
</a>
|
||||
<a href="./#requestarr-requests" class="nav-item nav-item-sub" id="requestarrRequestsNav">
|
||||
<div class="nav-icon-wrapper"><i class="fas fa-inbox"></i></div>
|
||||
<span>Requests</span>
|
||||
<span class="nav-badge requestarr-pending-badge-mirror" style="display:none;">0</span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -365,7 +376,8 @@ var ALL_SUB_GROUP_IDS = [
|
||||
'movie-hunt-activity-sub',
|
||||
'media-hunt-config-sub',
|
||||
'index-master-sub',
|
||||
'index-master-view-sub'
|
||||
'index-master-view-sub',
|
||||
'requestarr-user-support-sub'
|
||||
];
|
||||
|
||||
// ── Expand / collapse helpers ────────────────────────────────
|
||||
@@ -448,6 +460,8 @@ function setActiveNavItem() {
|
||||
var h = window.location.hash || '#home';
|
||||
var mhBody = document.getElementById('sidebar-group-media-hunt');
|
||||
if (mhBody) { mhBody.classList.remove('config-view'); mhBody.classList.remove('activity-view'); mhBody.classList.remove('indexmaster-view'); }
|
||||
var reqBody = document.getElementById('sidebar-group-requests');
|
||||
if (reqBody) { reqBody.classList.remove('user-support-view'); }
|
||||
|
||||
var selector = null;
|
||||
var group = null;
|
||||
@@ -487,11 +501,11 @@ function setActiveNavItem() {
|
||||
else if (h === '#requestarr-tv') { selector = '#requestarrTVNav'; group = 'sidebar-group-requests'; }
|
||||
else if (h === '#requestarr-movies') { selector = '#requestarrMoviesNav'; group = 'sidebar-group-requests'; }
|
||||
else if (h === '#requestarr-hidden') { selector = '#requestarrHiddenNav'; group = 'sidebar-group-requests'; }
|
||||
else if (h === '#requestarr-requests') { selector = '#requestarrRequestsNav'; group = 'sidebar-group-requests'; }
|
||||
else if (h === '#requestarr-smarthunt-settings') { selector = '#requestarrSmartHuntSettingsNav'; group = 'sidebar-group-requests'; }
|
||||
else if (h === '#requestarr-users') { selector = '#requestarrUsersNav'; group = 'sidebar-group-requests'; }
|
||||
else if (h === '#requestarr-services') { selector = '#requestarrServicesNav'; group = 'sidebar-group-requests'; }
|
||||
else if (h === '#requestarr-settings') { selector = '#requestarrSettingsNav'; group = 'sidebar-group-requests'; }
|
||||
else if (h === '#requestarr-users') { selector = '#requestarrUsersNav'; group = 'sidebar-group-requests'; subToExpand = 'requestarr-user-support-sub'; if (reqBody) reqBody.classList.add('user-support-view'); }
|
||||
else if (h === '#requestarr-requests') { selector = '#requestarrRequestsNav'; group = 'sidebar-group-requests'; subToExpand = 'requestarr-user-support-sub'; if (reqBody) reqBody.classList.add('user-support-view'); }
|
||||
else if (h === '#requestarr-services') { selector = '#requestarrServicesNav'; group = 'sidebar-group-requests'; subToExpand = 'requestarr-user-support-sub'; if (reqBody) reqBody.classList.add('user-support-view'); }
|
||||
|
||||
// ── Apps ──────────────────────────────────────
|
||||
else if (h === '#apps' || h === '#sonarr' || h === '#instance-editor') { selector = '#appsSonarrNav'; group = 'sidebar-group-apps'; }
|
||||
|
||||
@@ -2913,9 +2913,21 @@ class HuntarrDatabase:
|
||||
|
||||
def add_requestarr_service(self, service_type: str, app_type: str, instance_name: str,
|
||||
instance_id: int = None, is_default: bool = False, is_4k: bool = False) -> bool:
|
||||
"""Add an instance as a requestarr service."""
|
||||
"""Add an instance as a requestarr service. First in a section auto-becomes default. Only one default per service_type."""
|
||||
try:
|
||||
with self.get_connection() as conn:
|
||||
# If no services exist yet for this type, force default
|
||||
count = conn.execute(
|
||||
'SELECT COUNT(*) FROM requestarr_services WHERE service_type = ?', (service_type,)
|
||||
).fetchone()[0]
|
||||
if count == 0:
|
||||
is_default = True
|
||||
# If marking as default, clear other defaults in the same service_type
|
||||
if is_default:
|
||||
conn.execute(
|
||||
'UPDATE requestarr_services SET is_default = 0, updated_at = CURRENT_TIMESTAMP WHERE service_type = ?',
|
||||
(service_type,)
|
||||
)
|
||||
conn.execute('''
|
||||
INSERT OR REPLACE INTO requestarr_services
|
||||
(service_type, app_type, instance_name, instance_id, is_default, is_4k, enabled, created_at, updated_at)
|
||||
@@ -2939,17 +2951,25 @@ class HuntarrDatabase:
|
||||
return False
|
||||
|
||||
def update_requestarr_service(self, service_id: int, updates: Dict[str, Any]) -> bool:
|
||||
"""Update a requestarr service."""
|
||||
"""Update a requestarr service. Only one default per service_type."""
|
||||
try:
|
||||
allowed = {'is_default', 'is_4k', 'enabled'}
|
||||
filtered = {k: v for k, v in updates.items() if k in allowed}
|
||||
if not filtered:
|
||||
return True
|
||||
set_parts = [f'{k} = ?' for k in filtered]
|
||||
set_parts.append('updated_at = CURRENT_TIMESTAMP')
|
||||
values = list(filtered.values()) + [service_id]
|
||||
sql = f"UPDATE requestarr_services SET {', '.join(set_parts)} WHERE id = ?"
|
||||
with self.get_connection() as conn:
|
||||
# If setting as default, clear other defaults in the same service_type first
|
||||
if filtered.get('is_default') and int(filtered['is_default']):
|
||||
row = conn.execute('SELECT service_type FROM requestarr_services WHERE id = ?', (service_id,)).fetchone()
|
||||
if row:
|
||||
conn.execute(
|
||||
'UPDATE requestarr_services SET is_default = 0, updated_at = CURRENT_TIMESTAMP WHERE service_type = ? AND id != ?',
|
||||
(row['service_type'], service_id)
|
||||
)
|
||||
set_parts = [f'{k} = ?' for k in filtered]
|
||||
set_parts.append('updated_at = CURRENT_TIMESTAMP')
|
||||
values = list(filtered.values()) + [service_id]
|
||||
sql = f"UPDATE requestarr_services SET {', '.join(set_parts)} WHERE id = ?"
|
||||
conn.execute(sql, values)
|
||||
conn.commit()
|
||||
return True
|
||||
|
||||
Reference in New Issue
Block a user