diff --git a/CHANGELOG.md b/CHANGELOG.md index 2031120..3abcd15 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,12 @@ 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.28] - 2026-05-27 + +### Fixed + +- **Missing Sonarr Link on TV Requests (Issue #58)** — Resolved a bug where the Sonarr deep-link button was missing on TV request cards while Radarr links correctly appeared on movie request cards. Added support for all camelCase TVDB ID variants (`tvDbId`, `tvdbId`, `theTvdbId`, `theTvDbId`, `TvDbId`, `TheTvDbId`) on both backend link decoration (`server/utils/ombiHelpers.js`) and frontend rendering (`client/src/ui/requests.js`). Added a dedicated integration test to safeguard links decoration for TV requests. + ## [1.7.27] - 2026-05-27 ### Fixed diff --git a/client/src/ui/requests.js b/client/src/ui/requests.js index 3376b9f..a7f24a4 100644 --- a/client/src/ui/requests.js +++ b/client/src/ui/requests.js @@ -194,7 +194,7 @@ function createRequestCard(request) { const actions = document.createElement('span'); actions.className = 'service-icons-container'; - const id = request.theTvDbId || request.theMovieDbId || request.theTvdbId || request.theTmdbId || request.TvDbId || request.TheTvDbId || request.imdbId || request.ImdbId; + const id = request.theTvDbId || request.theTvdbId || request.tvDbId || request.tvdbId || request.TvDbId || request.TheTvDbId || request.theMovieDbId || request.theTmdbId || request.imdbId || request.ImdbId; if (state.ombiBaseUrl && id) { const ombiLink = document.createElement('a'); ombiLink.className = 'ombi-link'; diff --git a/package-lock.json b/package-lock.json index 1863455..1ce86b2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "sofarr", - "version": "1.7.27", + "version": "1.7.28", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "sofarr", - "version": "1.7.27", + "version": "1.7.28", "license": "MIT", "dependencies": { "axios": "^1.6.0", diff --git a/package.json b/package.json index 390dbef..0ace474 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sofarr", - "version": "1.7.27", + "version": "1.7.28", "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": { diff --git a/server/app.js b/server/app.js index 9490b9f..850b13a 100644 --- a/server/app.js +++ b/server/app.js @@ -133,7 +133,7 @@ function createApp({ skipRateLimits = false } = {}) { * version: * type: string * description: sofarr version - * example: "1.7.27" + * example: "1.7.28" * x-code-samples: * - lang: curl * label: cURL diff --git a/server/openapi.yaml b/server/openapi.yaml index 0ea9f10..c5abb04 100644 --- a/server/openapi.yaml +++ b/server/openapi.yaml @@ -22,7 +22,7 @@ info: ## SSE Streaming Real-time updates are available via Server-Sent Events at GET /api/dashboard/stream. - version: 1.7.27 + version: 1.7.28 contact: name: sofarr license: diff --git a/server/utils/ombiHelpers.js b/server/utils/ombiHelpers.js index fecf5b2..86d288b 100644 --- a/server/utils/ombiHelpers.js +++ b/server/utils/ombiHelpers.js @@ -115,10 +115,10 @@ async function decorateRequestsWithArrLinks(requests, isAdmin) { requests.forEach(req => { // Determine if it's TV or Movie. Often `mediaType` is set, or `type === 'Tv'` // Fallback to checking for TV specific IDs. - const isTv = req.mediaType === 'tv' || req.type === 'Tv' || req.tvDbId || req.theTvDbId; + const isTv = req.mediaType === 'tv' || req.type === 'Tv' || req.tvDbId || req.tvdbId || req.theTvDbId || req.theTvdbId || req.TvDbId || req.TheTvDbId; if (isTv) { - const tvdbId = req.theTvDbId || req.theMovieDbId || req.theTvdbId || req.theTmdbId || req.TvDbId || req.TheTvDbId; + const tvdbId = req.theTvDbId || req.theTvdbId || req.tvDbId || req.tvdbId || req.TvDbId || req.TheTvDbId || req.theMovieDbId || req.theTmdbId; if (!tvdbId) return; for (const instData of sonarrData) { diff --git a/tests/integration/ombi.test.js b/tests/integration/ombi.test.js index 27a9198..9f186f0 100644 --- a/tests/integration/ombi.test.js +++ b/tests/integration/ombi.test.js @@ -233,6 +233,48 @@ describe('GET /api/ombi/requests', () => { expect(res.body.requests.movie[0].requestedUser.userName).toBe('adminuser'); }); + it('decorates TV requests with Sonarr links using tvDbId camelCase property (Issue #58)', async () => { + // 1. Setup mock instance config + process.env.SONARR_INSTANCES = JSON.stringify([ + { id: 'sonarr-1', name: 'Test Sonarr', url: 'https://sonarr.test', apiKey: 'sonarr-key' } + ]); + + // Reset and re-initialize retrievers registry to pick up the Sonarr instance + arrRetrieverRegistry.retrievers.clear(); + arrRetrieverRegistry.initialized = false; + + // 2. Setup mock Ombi TV request carrying `tvDbId` instead of `theTvDbId` + const tvRequestsWithTvDbId = [ + { id: 4, title: 'Superman Show', requestedUser: { userName: 'adminuser' }, requestedByAlias: 'adminuser', type: 'tv', tvDbId: '101' } + ]; + + nock.cleanAll(); + setupOmbiRequestMocks(OMBI_REQUESTS.movie, tvRequestsWithTvDbId); + + // 3. Mock Sonarr API series call returning the series with matching tvdbId and titleSlug + nock('https://sonarr.test') + .get('/api/v3/series') + .reply(200, [ + { tvdbId: 101, title: 'Superman Show', titleSlug: 'superman-show' } + ]); + + const { cookies } = await authenticateUser(app, 'AdminUser', true); + + const res = await request(app) + .get('/api/ombi/requests?showAll=true') + .set('Cookie', cookies) + .expect(200); + + // 4. Assert decoration succeeded + const supermanShow = res.body.requests.tv.find(r => r.id === 4); + expect(supermanShow).toBeDefined(); + expect(supermanShow.arrLink).toBe('https://sonarr.test/series/superman-show'); + expect(supermanShow.arrType).toBe('sonarr'); + + // Clean up + delete process.env.SONARR_INSTANCES; + }); + it('handles case-insensitive username matching', async () => { const requestsWithMixedCase = [ { id: 1, title: 'Test Movie', requestedUser: { userName: 'TestUser' }, requestedByAlias: 'TestUser', type: 'movie' },