fix: resolve blocklist & search failures on Sonarr season packs and multi-episode releases
Build and Push Docker Image / build (push) Successful in 1m31s
Licence Check / Licence compatibility and copyright header verification (push) Successful in 2m42s
CI / Security audit (push) Successful in 3m26s
CI / Swagger Validation & Coverage (push) Successful in 3m43s
CI / Tests & coverage (push) Successful in 4m19s
Build and Push Docker Image / build (push) Successful in 1m31s
Licence Check / Licence compatibility and copyright header verification (push) Successful in 2m42s
CI / Security audit (push) Successful in 3m26s
CI / Swagger Validation & Coverage (push) Successful in 3m43s
CI / Tests & coverage (push) Successful in 4m19s
This commit is contained in:
@@ -97,6 +97,8 @@ export async function handleBlocklistSearch(download) {
|
|||||||
arrInstanceUrl: download.arrInstanceUrl,
|
arrInstanceUrl: download.arrInstanceUrl,
|
||||||
arrInstanceKey: download.arrInstanceKey,
|
arrInstanceKey: download.arrInstanceKey,
|
||||||
arrContentId: download.arrContentId,
|
arrContentId: download.arrContentId,
|
||||||
|
arrContentIds: download.arrContentIds,
|
||||||
|
arrSeriesId: download.arrSeriesId,
|
||||||
arrContentType: download.arrContentType
|
arrContentType: download.arrContentType
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -272,6 +272,8 @@ export async function handleBlocklistSearchClick(btn, download) {
|
|||||||
arrInstanceUrl: download.arrInstanceUrl,
|
arrInstanceUrl: download.arrInstanceUrl,
|
||||||
arrInstanceKey: download.arrInstanceKey,
|
arrInstanceKey: download.arrInstanceKey,
|
||||||
arrContentId: download.arrContentId,
|
arrContentId: download.arrContentId,
|
||||||
|
arrContentIds: download.arrContentIds,
|
||||||
|
arrSeriesId: download.arrSeriesId,
|
||||||
arrContentType: download.arrContentType,
|
arrContentType: download.arrContentType,
|
||||||
isAdmin: state.isAdmin,
|
isAdmin: state.isAdmin,
|
||||||
canBlocklist: download.canBlocklist
|
canBlocklist: download.canBlocklist
|
||||||
|
|||||||
+10
-1
@@ -276,7 +276,6 @@ components:
|
|||||||
- arrQueueId
|
- arrQueueId
|
||||||
- arrType
|
- arrType
|
||||||
- arrInstanceUrl
|
- arrInstanceUrl
|
||||||
- arrContentId
|
|
||||||
- arrContentType
|
- arrContentType
|
||||||
properties:
|
properties:
|
||||||
arrQueueId:
|
arrQueueId:
|
||||||
@@ -301,6 +300,16 @@ components:
|
|||||||
type: integer
|
type: integer
|
||||||
description: episodeId (Sonarr) or movieId (Radarr)
|
description: episodeId (Sonarr) or movieId (Radarr)
|
||||||
example: 456
|
example: 456
|
||||||
|
arrContentIds:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: integer
|
||||||
|
description: Array of episodeIds for multi-episode packs (Sonarr)
|
||||||
|
example: [456, 457]
|
||||||
|
arrSeriesId:
|
||||||
|
type: integer
|
||||||
|
description: seriesId for fallback automatic series search (Sonarr)
|
||||||
|
example: 789
|
||||||
arrContentType:
|
arrContentType:
|
||||||
type: string
|
type: string
|
||||||
enum: [episode, movie]
|
enum: [episode, movie]
|
||||||
|
|||||||
@@ -676,10 +676,10 @@ router.get('/stream', requireAuth, async (req, res) => {
|
|||||||
router.post('/blocklist-search', requireAuth, async (req, res) => {
|
router.post('/blocklist-search', requireAuth, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const user = req.user;
|
const user = req.user;
|
||||||
const { arrQueueId, arrType, arrInstanceUrl, arrInstanceKey, arrContentId, arrContentType } = req.body;
|
const { arrQueueId, arrType, arrInstanceUrl, arrInstanceKey, arrContentId, arrContentIds, arrSeriesId, arrContentType } = req.body;
|
||||||
|
|
||||||
if (!arrQueueId || !arrType || !arrInstanceUrl || !arrContentId || !arrContentType) {
|
if (!arrQueueId || !arrType || !arrInstanceUrl || !arrContentType) {
|
||||||
console.error('[Blocklist] Missing required fields:', { arrQueueId, arrType, arrInstanceUrl, hasKey: !!arrInstanceKey, arrContentId, arrContentType });
|
console.error('[Blocklist] Missing required fields:', { arrQueueId, arrType, arrInstanceUrl, hasKey: !!arrInstanceKey, arrContentType });
|
||||||
return res.status(400).json({ error: 'Missing required fields' });
|
return res.status(400).json({ error: 'Missing required fields' });
|
||||||
}
|
}
|
||||||
if (arrType !== 'sonarr' && arrType !== 'radarr') {
|
if (arrType !== 'sonarr' && arrType !== 'radarr') {
|
||||||
@@ -724,8 +724,14 @@ router.post('/blocklist-search', requireAuth, async (req, res) => {
|
|||||||
// Step 2: Trigger a new automatic search
|
// Step 2: Trigger a new automatic search
|
||||||
let commandBody;
|
let commandBody;
|
||||||
if (arrType === 'sonarr' && arrContentType === 'episode') {
|
if (arrType === 'sonarr' && arrContentType === 'episode') {
|
||||||
commandBody = { name: 'EpisodeSearch', episodeIds: [arrContentId] };
|
if (arrContentId) {
|
||||||
} else if (arrType === 'radarr' && arrContentType === 'movie') {
|
commandBody = { name: 'EpisodeSearch', episodeIds: [arrContentId] };
|
||||||
|
} else if (Array.isArray(arrContentIds) && arrContentIds.length > 0) {
|
||||||
|
commandBody = { name: 'EpisodeSearch', episodeIds: arrContentIds.map(Number) };
|
||||||
|
} else if (arrSeriesId) {
|
||||||
|
commandBody = { name: 'SeriesSearch', seriesId: Number(arrSeriesId) };
|
||||||
|
}
|
||||||
|
} else if (arrType === 'radarr' && arrContentType === 'movie' && arrContentId) {
|
||||||
commandBody = { name: 'MoviesSearch', movieIds: [arrContentId] };
|
commandBody = { name: 'MoviesSearch', movieIds: [arrContentId] };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -737,7 +743,7 @@ router.post('/blocklist-search', requireAuth, async (req, res) => {
|
|||||||
const { pollAllServices } = require('../utils/poller');
|
const { pollAllServices } = require('../utils/poller');
|
||||||
pollAllServices().catch(() => {});
|
pollAllServices().catch(() => {});
|
||||||
|
|
||||||
console.log(`[Dashboard] Blocklist+search: ${arrType} queueId=${arrQueueId} contentId=${arrContentId} by ${user.name}`);
|
console.log(`[Dashboard] Blocklist+search: ${arrType} queueId=${arrQueueId} contentId=${arrContentId || 'none'} by ${user.name}`);
|
||||||
res.json({ ok: true });
|
res.json({ ok: true });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('[Dashboard] blocklist-search error:', sanitizeError(err));
|
console.error('[Dashboard] blocklist-search error:', sanitizeError(err));
|
||||||
|
|||||||
@@ -209,6 +209,8 @@ async function matchSabSlots(slots, context) {
|
|||||||
dlObj.arrType = 'sonarr';
|
dlObj.arrType = 'sonarr';
|
||||||
dlObj.arrInstanceUrl = sonarrMatch._instanceUrl || null;
|
dlObj.arrInstanceUrl = sonarrMatch._instanceUrl || null;
|
||||||
dlObj.arrContentId = sonarrMatch.episodeId || null;
|
dlObj.arrContentId = sonarrMatch.episodeId || null;
|
||||||
|
dlObj.arrContentIds = sonarrMatch.episodeIds || null;
|
||||||
|
dlObj.arrSeriesId = sonarrMatch.seriesId || null;
|
||||||
dlObj.arrContentType = 'episode';
|
dlObj.arrContentType = 'episode';
|
||||||
if (isAdmin) {
|
if (isAdmin) {
|
||||||
dlObj.downloadPath = slot.storage || null;
|
dlObj.downloadPath = slot.storage || null;
|
||||||
@@ -451,6 +453,8 @@ async function matchTorrents(torrents, context) {
|
|||||||
download.arrType = 'sonarr';
|
download.arrType = 'sonarr';
|
||||||
download.arrInstanceUrl = sonarrMatch._instanceUrl || null;
|
download.arrInstanceUrl = sonarrMatch._instanceUrl || null;
|
||||||
download.arrContentId = sonarrMatch.episodeId || null;
|
download.arrContentId = sonarrMatch.episodeId || null;
|
||||||
|
download.arrContentIds = sonarrMatch.episodeIds || null;
|
||||||
|
download.arrSeriesId = sonarrMatch.seriesId || null;
|
||||||
download.arrContentType = 'episode';
|
download.arrContentType = 'episode';
|
||||||
if (isAdmin) {
|
if (isAdmin) {
|
||||||
download.downloadPath = download.savePath || null;
|
download.downloadPath = download.savePath || null;
|
||||||
|
|||||||
@@ -918,6 +918,60 @@ describe('POST /api/dashboard/blocklist-search', () => {
|
|||||||
expect(res.status).toBe(502);
|
expect(res.status).toBe(502);
|
||||||
mockGetAllDownloads.mockRestore();
|
mockGetAllDownloads.mockRestore();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('returns 200 OK when arrContentId is null but arrSeriesId is present (fallback SeriesSearch)', async () => {
|
||||||
|
const app = createApp({ skipRateLimits: true });
|
||||||
|
const { cookies, csrf, csrfCookie } = await getAuthHeaders(app);
|
||||||
|
|
||||||
|
const downloadClientRegistry = require('../../server/utils/downloadClients');
|
||||||
|
const mockGetAllDownloads = vi.spyOn(downloadClientRegistry, 'getAllDownloads').mockResolvedValue([
|
||||||
|
{ arrQueueId: 1001, arrType: 'sonarr', importIssues: [], qbittorrent: null }
|
||||||
|
]);
|
||||||
|
|
||||||
|
nock(SONARR_BASE)
|
||||||
|
.delete('/api/v3/queue/1001')
|
||||||
|
.query({ removeFromClient: 'true', blocklist: 'true' })
|
||||||
|
.reply(200, {});
|
||||||
|
nock(SONARR_BASE)
|
||||||
|
.post('/api/v3/command', { name: 'SeriesSearch', seriesId: 42 })
|
||||||
|
.reply(200, {});
|
||||||
|
|
||||||
|
const res = await request(app)
|
||||||
|
.post('/api/dashboard/blocklist-search')
|
||||||
|
.set('Cookie', [...cookies, csrfCookie].join('; '))
|
||||||
|
.set('X-CSRF-Token', csrf)
|
||||||
|
.send({ arrQueueId: 1001, arrType: 'sonarr', arrInstanceUrl: SONARR_BASE, arrInstanceKey: 'sk', arrSeriesId: 42, arrContentType: 'episode' });
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
expect(res.body.ok).toBe(true);
|
||||||
|
mockGetAllDownloads.mockRestore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('triggers EpisodeSearch with multiple episode IDs when arrContentIds is provided', async () => {
|
||||||
|
const app = createApp({ skipRateLimits: true });
|
||||||
|
const { cookies, csrf, csrfCookie } = await getAuthHeaders(app);
|
||||||
|
|
||||||
|
const downloadClientRegistry = require('../../server/utils/downloadClients');
|
||||||
|
const mockGetAllDownloads = vi.spyOn(downloadClientRegistry, 'getAllDownloads').mockResolvedValue([
|
||||||
|
{ arrQueueId: 1001, arrType: 'sonarr', importIssues: [], qbittorrent: null }
|
||||||
|
]);
|
||||||
|
|
||||||
|
nock(SONARR_BASE)
|
||||||
|
.delete('/api/v3/queue/1001')
|
||||||
|
.query({ removeFromClient: 'true', blocklist: 'true' })
|
||||||
|
.reply(200, {});
|
||||||
|
nock(SONARR_BASE)
|
||||||
|
.post('/api/v3/command', { name: 'EpisodeSearch', episodeIds: [12, 13, 14] })
|
||||||
|
.reply(200, {});
|
||||||
|
|
||||||
|
const res = await request(app)
|
||||||
|
.post('/api/dashboard/blocklist-search')
|
||||||
|
.set('Cookie', [...cookies, csrfCookie].join('; '))
|
||||||
|
.set('X-CSRF-Token', csrf)
|
||||||
|
.send({ arrQueueId: 1001, arrType: 'sonarr', arrInstanceUrl: SONARR_BASE, arrInstanceKey: 'sk', arrContentIds: [12, 13, 14], arrContentType: 'episode' });
|
||||||
|
expect(res.status).toBe(200);
|
||||||
|
expect(res.body.ok).toBe(true);
|
||||||
|
mockGetAllDownloads.mockRestore();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
Reference in New Issue
Block a user