feat: link series/movie titles to Sonarr/Radarr for admin users

- Series title links to Sonarr series page (/series/{titleSlug})
- Movie title links to Radarr movie page (/movie/{titleSlug})
- Links open in new tab, only shown for admin users
- Instance URL preserved through data aggregation for multi-instance support
This commit is contained in:
2026-05-15 21:34:01 +01:00
parent d09b0ab40a
commit 59b096a60a
3 changed files with 50 additions and 4 deletions

View File

@@ -42,6 +42,18 @@ function extractUserTag(tags, tagMap) {
return userTag ? userTag.label : null;
}
// Helper to build Sonarr web UI link for a series
function getSonarrLink(series) {
if (!series || !series._instanceUrl || !series.titleSlug) return null;
return `${series._instanceUrl}/series/${series.titleSlug}`;
}
// Helper to build Radarr web UI link for a movie
function getRadarrLink(movie) {
if (!movie || !movie._instanceUrl || !movie.titleSlug) return null;
return `${movie._instanceUrl}/movie/${movie.titleSlug}`;
}
// Get user downloads for authenticated user
router.get('/user-downloads', async (req, res) => {
try {
@@ -216,7 +228,10 @@ router.get('/user-downloads', async (req, res) => {
}
};
const sonarrSeries = {
data: sonarrSeriesResults.flatMap(s => s.data || [])
data: sonarrSeriesResults.flatMap(s => {
const inst = sonarrInstances.find(i => i.id === s.instance);
return (s.data || []).map(item => ({ ...item, _instanceUrl: inst ? inst.url : null }));
})
};
const radarrQueue = {
data: {
@@ -229,7 +244,10 @@ router.get('/user-downloads', async (req, res) => {
}
};
const radarrMovies = {
data: radarrMoviesResults.flatMap(m => m.data || [])
data: radarrMoviesResults.flatMap(m => {
const inst = radarrInstances.find(i => i.id === m.instance);
return (m.data || []).map(item => ({ ...item, _instanceUrl: inst ? inst.url : null }));
})
};
const radarrTags = {
data: radarrTagsResults.flatMap(t => t.data || [])
@@ -322,6 +340,7 @@ router.get('/user-downloads', async (req, res) => {
if (isAdmin) {
dlObj.downloadPath = slot.storage || null;
dlObj.targetPath = series.path || null;
dlObj.arrLink = getSonarrLink(series);
}
userDownloads.push(dlObj);
}
@@ -357,6 +376,7 @@ router.get('/user-downloads', async (req, res) => {
if (isAdmin) {
dlObj.downloadPath = slot.storage || null;
dlObj.targetPath = movie.path || null;
dlObj.arrLink = getRadarrLink(movie);
}
userDownloads.push(dlObj);
}
@@ -405,6 +425,7 @@ router.get('/user-downloads', async (req, res) => {
if (isAdmin) {
dlObj.downloadPath = slot.storage || null;
dlObj.targetPath = series.path || null;
dlObj.arrLink = getSonarrLink(series);
}
userDownloads.push(dlObj);
}
@@ -436,6 +457,7 @@ router.get('/user-downloads', async (req, res) => {
if (isAdmin) {
dlObj.downloadPath = slot.storage || null;
dlObj.targetPath = movie.path || null;
dlObj.arrLink = getRadarrLink(movie);
}
userDownloads.push(dlObj);
}
@@ -499,6 +521,7 @@ router.get('/user-downloads', async (req, res) => {
if (isAdmin) {
download.downloadPath = download.savePath || null;
download.targetPath = series.path || null;
download.arrLink = getSonarrLink(series);
}
userDownloads.push(download);
continue; // Skip to next torrent
@@ -527,6 +550,7 @@ router.get('/user-downloads', async (req, res) => {
if (isAdmin) {
download.downloadPath = download.savePath || null;
download.targetPath = movie.path || null;
download.arrLink = getRadarrLink(movie);
}
userDownloads.push(download);
continue; // Skip to next torrent
@@ -555,6 +579,7 @@ router.get('/user-downloads', async (req, res) => {
if (isAdmin) {
download.downloadPath = download.savePath || null;
download.targetPath = series.path || null;
download.arrLink = getSonarrLink(series);
}
userDownloads.push(download);
continue;
@@ -583,6 +608,7 @@ router.get('/user-downloads', async (req, res) => {
if (isAdmin) {
download.downloadPath = download.savePath || null;
download.targetPath = movie.path || null;
download.arrLink = getRadarrLink(movie);
}
userDownloads.push(download);
continue;