feat: blocklist & search button for import-pending downloads with caution
- Poller now stores _instanceKey alongside _instanceUrl on Sonarr/Radarr queue records - dashboard route threads arrQueueId/arrType/arrInstanceUrl/arrInstanceKey/arrContentId/arrContentType as admin-only fields on downloads with importIssues - POST /api/dashboard/blocklist-search: admin-only, removes queue item with blocklist=true then triggers EpisodeSearch/MoviesSearch - Button renders in download card header (admin + importIssues + arrQueueId only) - Confirm dialog, loading/success/error states on the button - Kicks a background poll on success so SSE reflects removed item promptly
This commit is contained in:
@@ -321,6 +321,14 @@ router.get('/user-downloads', requireAuth, async (req, res) => {
|
||||
dlObj.downloadPath = slot.storage || null;
|
||||
dlObj.targetPath = series.path || null;
|
||||
dlObj.arrLink = getSonarrLink(series);
|
||||
if (issues) {
|
||||
dlObj.arrQueueId = sonarrMatch.id;
|
||||
dlObj.arrType = 'sonarr';
|
||||
dlObj.arrInstanceUrl = sonarrMatch._instanceUrl || null;
|
||||
dlObj.arrInstanceKey = sonarrMatch._instanceKey || null;
|
||||
dlObj.arrContentId = sonarrMatch.episodeId || null;
|
||||
dlObj.arrContentType = 'episode';
|
||||
}
|
||||
}
|
||||
userDownloads.push(dlObj);
|
||||
}
|
||||
@@ -363,6 +371,14 @@ router.get('/user-downloads', requireAuth, async (req, res) => {
|
||||
dlObj.downloadPath = slot.storage || null;
|
||||
dlObj.targetPath = movie.path || null;
|
||||
dlObj.arrLink = getRadarrLink(movie);
|
||||
if (issues) {
|
||||
dlObj.arrQueueId = radarrMatch.id;
|
||||
dlObj.arrType = 'radarr';
|
||||
dlObj.arrInstanceUrl = radarrMatch._instanceUrl || null;
|
||||
dlObj.arrInstanceKey = radarrMatch._instanceKey || null;
|
||||
dlObj.arrContentId = radarrMatch.movieId || null;
|
||||
dlObj.arrContentType = 'movie';
|
||||
}
|
||||
}
|
||||
userDownloads.push(dlObj);
|
||||
}
|
||||
@@ -520,6 +536,14 @@ router.get('/user-downloads', requireAuth, async (req, res) => {
|
||||
download.downloadPath = download.savePath || null;
|
||||
download.targetPath = series.path || null;
|
||||
download.arrLink = getSonarrLink(series);
|
||||
if (sonarrIssues) {
|
||||
download.arrQueueId = sonarrMatch.id;
|
||||
download.arrType = 'sonarr';
|
||||
download.arrInstanceUrl = sonarrMatch._instanceUrl || null;
|
||||
download.arrInstanceKey = sonarrMatch._instanceKey || null;
|
||||
download.arrContentId = sonarrMatch.episodeId || null;
|
||||
download.arrContentType = 'episode';
|
||||
}
|
||||
}
|
||||
userDownloads.push(download);
|
||||
continue; // Skip to next torrent
|
||||
@@ -555,6 +579,14 @@ router.get('/user-downloads', requireAuth, async (req, res) => {
|
||||
download.downloadPath = download.savePath || null;
|
||||
download.targetPath = movie.path || null;
|
||||
download.arrLink = getRadarrLink(movie);
|
||||
if (radarrIssues) {
|
||||
download.arrQueueId = radarrMatch.id;
|
||||
download.arrType = 'radarr';
|
||||
download.arrInstanceUrl = radarrMatch._instanceUrl || null;
|
||||
download.arrInstanceKey = radarrMatch._instanceKey || null;
|
||||
download.arrContentId = radarrMatch.movieId || null;
|
||||
download.arrContentType = 'movie';
|
||||
}
|
||||
}
|
||||
userDownloads.push(download);
|
||||
continue; // Skip to next torrent
|
||||
@@ -1059,4 +1091,68 @@ router.get('/stream', requireAuth, async (req, res) => {
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* POST /api/dashboard/blocklist-search
|
||||
*
|
||||
* Admin-only. Removes a queue item from Sonarr/Radarr with blocklist=true
|
||||
* (so the release is not grabbed again), then immediately triggers a new
|
||||
* automatic search for the same episode/movie.
|
||||
*
|
||||
* Body: {
|
||||
* arrQueueId: number — Sonarr/Radarr queue record id
|
||||
* arrType: 'sonarr'|'radarr'
|
||||
* arrInstanceUrl: string — base URL of the arr instance
|
||||
* arrInstanceKey: string — API key for the arr instance
|
||||
* arrContentId: number — episodeId (Sonarr) or movieId (Radarr)
|
||||
* arrContentType: 'episode'|'movie'
|
||||
* }
|
||||
*/
|
||||
router.post('/blocklist-search', requireAuth, async (req, res) => {
|
||||
try {
|
||||
const user = req.user;
|
||||
if (!user.isAdmin) {
|
||||
return res.status(403).json({ error: 'Admin access required' });
|
||||
}
|
||||
|
||||
const { arrQueueId, arrType, arrInstanceUrl, arrInstanceKey, arrContentId, arrContentType } = req.body;
|
||||
|
||||
if (!arrQueueId || !arrType || !arrInstanceUrl || !arrInstanceKey || !arrContentId || !arrContentType) {
|
||||
return res.status(400).json({ error: 'Missing required fields' });
|
||||
}
|
||||
if (arrType !== 'sonarr' && arrType !== 'radarr') {
|
||||
return res.status(400).json({ error: 'arrType must be sonarr or radarr' });
|
||||
}
|
||||
|
||||
const headers = { 'X-Api-Key': arrInstanceKey };
|
||||
|
||||
// Step 1: Remove from queue with blocklist=true
|
||||
await axios.delete(`${arrInstanceUrl}/api/v3/queue/${arrQueueId}`, {
|
||||
headers,
|
||||
params: { removeFromClient: true, blocklist: true }
|
||||
});
|
||||
|
||||
// Step 2: Trigger a new automatic search
|
||||
let commandBody;
|
||||
if (arrType === 'sonarr' && arrContentType === 'episode') {
|
||||
commandBody = { name: 'EpisodeSearch', episodeIds: [arrContentId] };
|
||||
} else if (arrType === 'radarr' && arrContentType === 'movie') {
|
||||
commandBody = { name: 'MoviesSearch', movieIds: [arrContentId] };
|
||||
}
|
||||
|
||||
if (commandBody) {
|
||||
await axios.post(`${arrInstanceUrl}/api/v3/command`, commandBody, { headers });
|
||||
}
|
||||
|
||||
// Invalidate the poll cache so the next SSE push reflects the removed item
|
||||
const { pollAllServices } = require('../utils/poller');
|
||||
pollAllServices().catch(() => {});
|
||||
|
||||
console.log(`[Dashboard] Blocklist+search: ${arrType} queueId=${arrQueueId} contentId=${arrContentId} by ${user.name}`);
|
||||
res.json({ ok: true });
|
||||
} catch (err) {
|
||||
console.error('[Dashboard] blocklist-search error:', sanitizeError(err));
|
||||
res.status(502).json({ error: 'Failed to blocklist and search', details: sanitizeError(err) });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
||||
@@ -160,8 +160,11 @@ async function pollAllServices() {
|
||||
records: sonarrQueues.flatMap(q => {
|
||||
const inst = sonarrInstances.find(i => i.id === q.instance);
|
||||
const url = inst ? inst.url : null;
|
||||
const key = inst ? inst.apiKey : null;
|
||||
return (q.data.records || []).map(r => {
|
||||
if (r.series) r.series._instanceUrl = url;
|
||||
r._instanceUrl = url;
|
||||
r._instanceKey = key;
|
||||
return r;
|
||||
});
|
||||
})
|
||||
@@ -175,8 +178,11 @@ async function pollAllServices() {
|
||||
records: radarrQueues.flatMap(q => {
|
||||
const inst = radarrInstances.find(i => i.id === q.instance);
|
||||
const url = inst ? inst.url : null;
|
||||
const key = inst ? inst.apiKey : null;
|
||||
return (q.data.records || []).map(r => {
|
||||
if (r.movie) r.movie._instanceUrl = url;
|
||||
r._instanceUrl = url;
|
||||
r._instanceKey = key;
|
||||
return r;
|
||||
});
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user