merge branch 'develop' into 'main' - Release v1.7.26
Create Release / release (push) Successful in 8s
CI / Security audit (push) Successful in 3m0s
CI / Swagger Validation & Coverage (push) Successful in 1m57s
Build and Push Docker Image / build (push) Successful in 1m26s
CI / Tests & coverage (push) Successful in 3m37s

This commit is contained in:
2026-05-27 22:50:31 +01:00
9 changed files with 99 additions and 81 deletions
+4 -10
View File
@@ -27,23 +27,17 @@ jobs:
if [[ "$BRANCH" == develop* ]]; then
# Sanitise branch name for tag: replace slashes with dashes
SAFE_BRANCH=$(echo "$BRANCH" | tr '/' '-')
TAGS="reg.i3omb.com/sofarr:${SAFE_BRANCH}"
TAGS="${TAGS},git.i3omb.com/gandalf/sofarr:${SAFE_BRANCH}"
TAGS="git.i3omb.com/gandalf/sofarr:${SAFE_BRANCH}"
echo "tags=${TAGS}" >> $GITHUB_OUTPUT
echo "Building develop image tags: ${TAGS}"
else
RELEASE_NAME=${BRANCH#release/}
# Primary registry tags
TAGS="reg.i3omb.com/sofarr:${VERSION}"
TAGS="${TAGS},reg.i3omb.com/sofarr:${RELEASE_NAME}"
TAGS="${TAGS},reg.i3omb.com/sofarr:latest"
# Gitea package registry tags
TAGS="${TAGS},git.i3omb.com/gandalf/sofarr:${VERSION}"
TAGS="git.i3omb.com/gandalf/sofarr:${VERSION}"
TAGS="${TAGS},git.i3omb.com/gandalf/sofarr:${RELEASE_NAME}"
TAGS="${TAGS},git.i3omb.com/gandalf/sofarr:latest"
echo "tags=${TAGS}" >> $GITHUB_OUTPUT
echo "Building release image tags: ${TAGS}"
fi
+6
View File
@@ -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.26] - 2026-05-27
### Fixed
- **Missing Ombi & *Arr Request Links (Issue #56)** — Resolved an issue where the Ombi and *Arr lookup buttons failed to appear against request cards. Added `decorateRequestsWithArrLinks` to aggregate IDs and query Radarr/Sonarr libraries during SSE stream decoration and backend REST fetching. Also fixed a frontend condition failing to generate Ombi links for TV requests by checking a broader set of ID properties (`theTvDbId`, `theTmdbId`, `imdbId`).
## [1.7.25] - 2026-05-27
### Fixed
+3 -2
View File
@@ -194,10 +194,11 @@ function createRequestCard(request) {
const actions = document.createElement('span');
actions.className = 'service-icons-container';
if (state.ombiBaseUrl && request.theMovieDbId) {
const id = request.theTvDbId || request.theMovieDbId || request.theTvdbId || request.theTmdbId || request.TvDbId || request.TheTvDbId || request.imdbId || request.ImdbId;
if (state.ombiBaseUrl && id) {
const ombiLink = document.createElement('a');
ombiLink.className = 'ombi-link';
ombiLink.href = `${state.ombiBaseUrl}/details/${request.mediaType || 'movie'}/${request.theMovieDbId}`;
ombiLink.href = `${state.ombiBaseUrl}/details/${request.mediaType || 'movie'}/${id}`;
ombiLink.target = '_blank';
ombiLink.title = 'View in Ombi';
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "sofarr",
"version": "1.7.25",
"version": "1.7.26",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "sofarr",
"version": "1.7.25",
"version": "1.7.26",
"license": "MIT",
"dependencies": {
"axios": "^1.6.0",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "sofarr",
"version": "1.7.25",
"version": "1.7.26",
"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": {
+1 -1
View File
@@ -22,7 +22,7 @@ info:
## SSE Streaming
Real-time updates are available via Server-Sent Events at GET /api/dashboard/stream.
version: 1.7.24
version: 1.7.26
contact:
name: sofarr
license:
+9 -3
View File
@@ -13,7 +13,7 @@ const { buildUserDownloads } = require('../services/DownloadBuilder');
const { onHistoryUpdate, offHistoryUpdate } = require('../utils/historyFetcher');
const arrRetrieverRegistry = require('../utils/arrRetrievers');
const { getOmbiInstances, getSonarrInstances, getRadarrInstances } = require('../utils/config');
const { extractRequestedUser, filterRequestsByUser } = require('../utils/ombiHelpers');
const { extractRequestedUser, filterRequestsByUser, decorateRequestsWithArrLinks } = require('../utils/ombiHelpers');
const { canBlocklist } = require('../services/DownloadAssembler');
@@ -525,8 +525,14 @@ router.get('/stream', requireAuth, async (req, res) => {
const showAllOmbi = showAll; // Use the same showAll flag for Ombi
const filteredOmbiMovieRequests = filterRequestsByUser(ombiRequests.movie || [], username, showAllOmbi);
const filteredOmbiTvRequests = filterRequestsByUser(ombiRequests.tv || [], username, showAllOmbi);
const filteredOmbiMovieRequests = filterRequestsByUser(ombiRequests.movie || [], username, showAllOmbi).map(r => ({ ...r, mediaType: 'movie' }));
const filteredOmbiTvRequests = filterRequestsByUser(ombiRequests.tv || [], username, showAllOmbi).map(r => ({ ...r, mediaType: 'tv' }));
// Admin only: add Sonarr/Radarr lookup links
if (isAdmin) {
const allFiltered = [...filteredOmbiMovieRequests, ...filteredOmbiTvRequests];
await decorateRequestsWithArrLinks(allFiltered, isAdmin);
}
const ombiRequestsFiltered = {
movie: filteredOmbiMovieRequests,
+6 -61
View File
@@ -4,7 +4,7 @@ const { logToFile } = require('../utils/logger');
const cache = require('../utils/cache');
const { getOmbiInstances, getWebhookSecret, getSofarrBaseUrl, getSofarrWebhookBaseUrl } = require('../utils/config');
const requireAuth = require('../middleware/requireAuth');
const { extractRequestedUser, filterRequestsByUser } = require('../utils/ombiHelpers');
const { extractRequestedUser, filterRequestsByUser, decorateRequestsWithArrLinks } = require('../utils/ombiHelpers');
const { applyRequestFilters } = require('../utils/ombiFilters');
const router = express.Router();
@@ -120,72 +120,17 @@ router.get('/requests', requireAuth, async (req, res) => {
const filteredMovieRequests = filterRequestsByUser(ombiRequests.movie || [], username, showAll);
const filteredTvRequests = filterRequestsByUser(ombiRequests.tv || [], username, showAll);
// Admin only: add Sonarr/Radarr lookup links
if (isAdmin) {
const sonarrRetrievers = arrRetrieverRegistry.getRetrieversByType('sonarr') || [];
const radarrRetrievers = arrRetrieverRegistry.getRetrieversByType('radarr') || [];
// Fetch all series and movies in parallel to match
const [sonarrData, radarrData] = await Promise.all([
Promise.all(sonarrRetrievers.map(async r => {
try {
const response = await require('axios').get(`${r.url}/api/v3/series`, {
headers: { 'X-Api-Key': r.apiKey }
});
return { instance: r, series: response.data || [] };
} catch {
return { instance: r, series: [] };
}
})),
Promise.all(radarrRetrievers.map(async r => {
try {
const response = await require('axios').get(`${r.url}/api/v3/movie`, {
headers: { 'X-Api-Key': r.apiKey }
});
return { instance: r, movies: response.data || [] };
} catch {
return { instance: r, movies: [] };
}
}))
]);
// For TV requests, find match in Sonarr
filteredTvRequests.forEach(req => {
const tvdbId = req.theTvDbId || req.theMovieDbId || req.theTvdbId || req.theTmdbId || req.TvDbId || req.TheTvDbId;
if (!tvdbId) return;
for (const instData of sonarrData) {
const match = instData.series.find(s => s && (s.tvdbId === parseInt(tvdbId, 10) || s.tmdbId === parseInt(tvdbId, 10)));
if (match && match.titleSlug) {
req.arrLink = `${instData.instance.url}/series/${match.titleSlug}`;
req.arrType = 'sonarr';
break;
}
}
});
// For Movie requests, find match in Radarr
filteredMovieRequests.forEach(req => {
const tmdbId = req.theMovieDbId || req.imdbId || req.theTmdbId || req.TheMovieDbId || req.ImdbId;
if (!tmdbId) return;
for (const instData of radarrData) {
const match = instData.movies.find(m => m && (m.tmdbId === parseInt(tmdbId, 10) || m.imdbId === tmdbId));
if (match && match.titleSlug) {
req.arrLink = `${instData.instance.url}/movie/${match.titleSlug}`;
req.arrType = 'radarr';
break;
}
}
});
}
// Tag with mediaType and flatten for filtering/sorting
const allRequests = [
...filteredMovieRequests.map(r => ({ ...r, mediaType: 'movie' })),
...filteredTvRequests.map(r => ({ ...r, mediaType: 'tv' }))
];
// Admin only: add Sonarr/Radarr lookup links
if (isAdmin) {
await decorateRequestsWithArrLinks(allRequests, isAdmin);
}
// Parse query params
let types = req.query.type;
let statuses = req.query.status;
+67 -1
View File
@@ -80,7 +80,73 @@ function filterRequestsByUser(requests, username, showAll) {
});
}
async function decorateRequestsWithArrLinks(requests, isAdmin) {
if (!isAdmin || !Array.isArray(requests)) return;
const arrRetrieverRegistry = require('./arrRetrievers');
await arrRetrieverRegistry.initialize();
const sonarrRetrievers = arrRetrieverRegistry.getRetrieversByType('sonarr') || [];
const radarrRetrievers = arrRetrieverRegistry.getRetrieversByType('radarr') || [];
const [sonarrData, radarrData] = await Promise.all([
Promise.all(sonarrRetrievers.map(async r => {
try {
const response = await require('axios').get(`${r.url}/api/v3/series`, {
headers: { 'X-Api-Key': r.apiKey }
});
return { instance: r, series: response.data || [] };
} catch {
return { instance: r, series: [] };
}
})),
Promise.all(radarrRetrievers.map(async r => {
try {
const response = await require('axios').get(`${r.url}/api/v3/movie`, {
headers: { 'X-Api-Key': r.apiKey }
});
return { instance: r, movies: response.data || [] };
} catch {
return { instance: r, movies: [] };
}
}))
]);
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;
if (isTv) {
const tvdbId = req.theTvDbId || req.theMovieDbId || req.theTvdbId || req.theTmdbId || req.TvDbId || req.TheTvDbId;
if (!tvdbId) return;
for (const instData of sonarrData) {
const match = instData.series.find(s => s && (s.tvdbId === parseInt(tvdbId, 10) || s.tmdbId === parseInt(tvdbId, 10)));
if (match && match.titleSlug) {
req.arrLink = `${instData.instance.url}/series/${match.titleSlug}`;
req.arrType = 'sonarr';
break;
}
}
} else {
const tmdbId = req.theMovieDbId || req.imdbId || req.theTmdbId || req.TheMovieDbId || req.ImdbId;
if (!tmdbId) return;
for (const instData of radarrData) {
const match = instData.movies.find(m => m && (m.tmdbId === parseInt(tmdbId, 10) || m.imdbId === tmdbId));
if (match && match.titleSlug) {
req.arrLink = `${instData.instance.url}/movie/${match.titleSlug}`;
req.arrType = 'radarr';
break;
}
}
}
});
}
module.exports = {
extractRequestedUser,
filterRequestsByUser
filterRequestsByUser,
decorateRequestsWithArrLinks
};