merge branch 'develop' into 'main' - Release v1.7.11
Build and Push Docker Image / build (push) Successful in 56s
Create Release / release (push) Successful in 48s
CI / Swagger Validation & Coverage (push) Successful in 3m3s
CI / Security audit (push) Successful in 2m31s
CI / Tests & coverage (push) Successful in 3m24s
Build and Push Docker Image / build (push) Successful in 56s
Create Release / release (push) Successful in 48s
CI / Swagger Validation & Coverage (push) Successful in 3m3s
CI / Security audit (push) Successful in 2m31s
CI / Tests & coverage (push) Successful in 3m24s
This commit is contained in:
@@ -4,6 +4,16 @@ All notable changes to this project will be documented in this file.
|
||||
Format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
||||
This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [1.7.11] - 2026-05-24
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Sonarr full-season and multi-episode blocklist-search failures** — Fixed a bug where clicking the "Blocklist & Search" button on television downloads representing full-season packs or multi-episode releases failed with a `400 Bad Request` error. We relaxed the API validation on `POST /api/dashboard/blocklist-search` to make `arrContentId` optional. In `DownloadMatcher.js`, we exposed `arrContentIds` (holding all episode IDs associated with the matched release) and `arrSeriesId` (holding the series ID).
|
||||
- **Enhanced blocklist re-search commands** — The backend now triggers multi-episode `EpisodeSearch` commands using the `episodeIds` array when blocklisting multi-episode releases, and falls back to a series-wide `SeriesSearch` command using the `seriesId` when no specific episode IDs can be resolved.
|
||||
- **OpenAPI Schema and Test Coverage** — Updated the OpenAPI specifications (`server/openapi.yaml`) to reflect optional/new parameters on `BlocklistSearchRequest`, and implemented new integration tests in `tests/integration/dashboard.test.js` to safeguard fallback and multi-episode search commands.
|
||||
|
||||
---
|
||||
|
||||
## [1.7.10] - 2026-05-24
|
||||
|
||||
### Fixed
|
||||
|
||||
@@ -97,6 +97,8 @@ export async function handleBlocklistSearch(download) {
|
||||
arrInstanceUrl: download.arrInstanceUrl,
|
||||
arrInstanceKey: download.arrInstanceKey,
|
||||
arrContentId: download.arrContentId,
|
||||
arrContentIds: download.arrContentIds,
|
||||
arrSeriesId: download.arrSeriesId,
|
||||
arrContentType: download.arrContentType
|
||||
})
|
||||
});
|
||||
|
||||
@@ -272,6 +272,8 @@ export async function handleBlocklistSearchClick(btn, download) {
|
||||
arrInstanceUrl: download.arrInstanceUrl,
|
||||
arrInstanceKey: download.arrInstanceKey,
|
||||
arrContentId: download.arrContentId,
|
||||
arrContentIds: download.arrContentIds,
|
||||
arrSeriesId: download.arrSeriesId,
|
||||
arrContentType: download.arrContentType,
|
||||
isAdmin: state.isAdmin,
|
||||
canBlocklist: download.canBlocklist
|
||||
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "sofarr",
|
||||
"version": "1.7.10",
|
||||
"version": "1.7.11",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "sofarr",
|
||||
"version": "1.7.10",
|
||||
"version": "1.7.11",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"axios": "^1.6.0",
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "sofarr",
|
||||
"version": "1.7.10",
|
||||
"version": "1.7.11",
|
||||
"description": "A personal media download dashboard that shows your downloads 'so far' while you relax on the sofa waiting for your *arr services to finish",
|
||||
"main": "server/index.js",
|
||||
"scripts": {
|
||||
|
||||
+10
-1
@@ -276,7 +276,6 @@ components:
|
||||
- arrQueueId
|
||||
- arrType
|
||||
- arrInstanceUrl
|
||||
- arrContentId
|
||||
- arrContentType
|
||||
properties:
|
||||
arrQueueId:
|
||||
@@ -301,6 +300,16 @@ components:
|
||||
type: integer
|
||||
description: episodeId (Sonarr) or movieId (Radarr)
|
||||
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:
|
||||
type: string
|
||||
enum: [episode, movie]
|
||||
|
||||
@@ -676,10 +676,10 @@ router.get('/stream', requireAuth, async (req, res) => {
|
||||
router.post('/blocklist-search', requireAuth, async (req, res) => {
|
||||
try {
|
||||
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) {
|
||||
console.error('[Blocklist] Missing required fields:', { arrQueueId, arrType, arrInstanceUrl, hasKey: !!arrInstanceKey, arrContentId, arrContentType });
|
||||
if (!arrQueueId || !arrType || !arrInstanceUrl || !arrContentType) {
|
||||
console.error('[Blocklist] Missing required fields:', { arrQueueId, arrType, arrInstanceUrl, hasKey: !!arrInstanceKey, arrContentType });
|
||||
return res.status(400).json({ error: 'Missing required fields' });
|
||||
}
|
||||
if (arrType !== 'sonarr' && arrType !== 'radarr') {
|
||||
@@ -724,8 +724,14 @@ router.post('/blocklist-search', requireAuth, async (req, res) => {
|
||||
// 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') {
|
||||
if (arrContentId) {
|
||||
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] };
|
||||
}
|
||||
|
||||
@@ -737,7 +743,7 @@ router.post('/blocklist-search', requireAuth, async (req, res) => {
|
||||
const { pollAllServices } = require('../utils/poller');
|
||||
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 });
|
||||
} catch (err) {
|
||||
console.error('[Dashboard] blocklist-search error:', sanitizeError(err));
|
||||
|
||||
@@ -209,6 +209,8 @@ async function matchSabSlots(slots, context) {
|
||||
dlObj.arrType = 'sonarr';
|
||||
dlObj.arrInstanceUrl = sonarrMatch._instanceUrl || null;
|
||||
dlObj.arrContentId = sonarrMatch.episodeId || null;
|
||||
dlObj.arrContentIds = sonarrMatch.episodeIds || null;
|
||||
dlObj.arrSeriesId = sonarrMatch.seriesId || null;
|
||||
dlObj.arrContentType = 'episode';
|
||||
if (isAdmin) {
|
||||
dlObj.downloadPath = slot.storage || null;
|
||||
@@ -451,6 +453,8 @@ async function matchTorrents(torrents, context) {
|
||||
download.arrType = 'sonarr';
|
||||
download.arrInstanceUrl = sonarrMatch._instanceUrl || null;
|
||||
download.arrContentId = sonarrMatch.episodeId || null;
|
||||
download.arrContentIds = sonarrMatch.episodeIds || null;
|
||||
download.arrSeriesId = sonarrMatch.seriesId || null;
|
||||
download.arrContentType = 'episode';
|
||||
if (isAdmin) {
|
||||
download.downloadPath = download.savePath || null;
|
||||
|
||||
@@ -918,6 +918,60 @@ describe('POST /api/dashboard/blocklist-search', () => {
|
||||
expect(res.status).toBe(502);
|
||||
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