feat: multi-tag badges for showAll — amber for unmatched, accent for matched
All checks were successful
Build and Push Docker Image / build (push) Successful in 27s

- server: add extractAllTags() returning all tag labels for a series/movie
- server: showAll now includes items with ANY tag (not just user-matched);
  non-admin path unchanged (must match current user's tag)
- server: replace userTag with allTags[] + matchedUserTag on every download object
- frontend: render all tags in header; unmatched tags amber (left), matched
  user tag in accent colour (rightmost); only visible in showAll mode
- css: add --unmatched-tag-bg/color variables to all three themes (light,
  dark, mono) and .download-user-badge.unmatched style
This commit is contained in:
2026-05-16 15:14:33 +01:00
parent de8563704a
commit 24b7797b60
3 changed files with 83 additions and 29 deletions

View File

@@ -40,6 +40,15 @@ function extractUserTag(tags, tagMap) {
return userTag ? userTag.label : null;
}
// Return all resolved tag labels for a series/movie
function extractAllTags(tags, tagMap) {
if (!tags || tags.length === 0) return [];
if (tagMap) {
return tags.map(id => tagMap.get(id)).filter(Boolean);
}
return tags.map(t => t && t.label).filter(Boolean);
}
// Replicate Ombi's StringHelper.SanitizeTagLabel: lowercase, replace non-alphanumeric with hyphen, collapse, trim
function sanitizeTagLabel(input) {
if (!input) return '';
@@ -224,8 +233,10 @@ router.get('/user-downloads', async (req, res) => {
if (sonarrMatch && sonarrMatch.seriesId) {
const series = seriesMap.get(sonarrMatch.seriesId) || sonarrMatch.series;
if (series) {
const userTag = extractUserTag(series.tags, sonarrTagMap);
if (userTag && (showAll || tagMatchesUser(userTag, username))) {
const matchedUserTag = extractUserTag(series.tags, sonarrTagMap);
const allTags = extractAllTags(series.tags, sonarrTagMap);
const hasAnyTag = allTags.length > 0;
if (showAll ? hasAnyTag : (matchedUserTag && tagMatchesUser(matchedUserTag, username))) {
const dlObj = {
type: 'series',
title: nzbName,
@@ -239,7 +250,8 @@ router.get('/user-downloads', async (req, res) => {
eta: slot.timeleft,
seriesName: series.title,
episodeInfo: sonarrMatch,
userTag: userTag
allTags,
matchedUserTag: matchedUserTag || null
};
const issues = getImportIssues(sonarrMatch);
if (issues) dlObj.importIssues = issues;
@@ -262,8 +274,10 @@ router.get('/user-downloads', async (req, res) => {
if (radarrMatch && radarrMatch.movieId) {
const movie = moviesMap.get(radarrMatch.movieId) || radarrMatch.movie;
if (movie) {
const userTag = extractUserTag(movie.tags, radarrTagMap);
if (userTag && (showAll || tagMatchesUser(userTag, username))) {
const matchedUserTag = extractUserTag(movie.tags, radarrTagMap);
const allTags = extractAllTags(movie.tags, radarrTagMap);
const hasAnyTag = allTags.length > 0;
if (showAll ? hasAnyTag : (matchedUserTag && tagMatchesUser(matchedUserTag, username))) {
const dlObj = {
type: 'movie',
title: nzbName,
@@ -277,7 +291,8 @@ router.get('/user-downloads', async (req, res) => {
eta: slot.timeleft,
movieName: movie.title,
movieInfo: radarrMatch,
userTag: userTag
allTags,
matchedUserTag: matchedUserTag || null
};
const issues = getImportIssues(radarrMatch);
if (issues) dlObj.importIssues = issues;
@@ -317,8 +332,10 @@ router.get('/user-downloads', async (req, res) => {
if (sonarrMatch && sonarrMatch.seriesId) {
const series = seriesMap.get(sonarrMatch.seriesId) || sonarrMatch.series;
if (series) {
const userTag = extractUserTag(series.tags, sonarrTagMap);
if (userTag && (showAll || tagMatchesUser(userTag, username))) {
const matchedUserTag = extractUserTag(series.tags, sonarrTagMap);
const allTags = extractAllTags(series.tags, sonarrTagMap);
const hasAnyTag = allTags.length > 0;
if (showAll ? hasAnyTag : (matchedUserTag && tagMatchesUser(matchedUserTag, username))) {
const dlObj = {
type: 'series',
title: nzbName,
@@ -328,7 +345,8 @@ router.get('/user-downloads', async (req, res) => {
completedAt: slot.completed_time,
seriesName: series.title,
episodeInfo: sonarrMatch,
userTag: userTag
allTags,
matchedUserTag: matchedUserTag || null
};
if (isAdmin) {
dlObj.downloadPath = slot.storage || null;
@@ -349,8 +367,10 @@ router.get('/user-downloads', async (req, res) => {
if (radarrMatch && radarrMatch.movieId) {
const movie = moviesMap.get(radarrMatch.movieId) || radarrMatch.movie;
if (movie) {
const userTag = extractUserTag(movie.tags, radarrTagMap);
if (userTag && (showAll || tagMatchesUser(userTag, username))) {
const matchedUserTag = extractUserTag(movie.tags, radarrTagMap);
const allTags = extractAllTags(movie.tags, radarrTagMap);
const hasAnyTag = allTags.length > 0;
if (showAll ? hasAnyTag : (matchedUserTag && tagMatchesUser(matchedUserTag, username))) {
const dlObj = {
type: 'movie',
title: nzbName,
@@ -360,7 +380,8 @@ router.get('/user-downloads', async (req, res) => {
completedAt: slot.completed_time,
movieName: movie.title,
movieInfo: radarrMatch,
userTag: userTag
allTags,
matchedUserTag: matchedUserTag || null
};
if (isAdmin) {
dlObj.downloadPath = slot.storage || null;
@@ -417,15 +438,18 @@ router.get('/user-downloads', async (req, res) => {
if (sonarrMatch && sonarrMatch.seriesId) {
const series = seriesMap.get(sonarrMatch.seriesId) || sonarrMatch.series;
if (series) {
const userTag = extractUserTag(series.tags, sonarrTagMap);
if (userTag && (showAll || tagMatchesUser(userTag, username))) {
const matchedUserTag = extractUserTag(series.tags, sonarrTagMap);
const allTags = extractAllTags(series.tags, sonarrTagMap);
const hasAnyTag = allTags.length > 0;
if (showAll ? hasAnyTag : (matchedUserTag && tagMatchesUser(matchedUserTag, username))) {
console.log(`[Dashboard] Matched torrent "${torrentName}" to Sonarr series "${series.title}"`);
const download = mapTorrentToDownload(torrent);
download.type = 'series';
download.coverArt = getCoverArt(series);
download.seriesName = series.title;
download.episodeInfo = sonarrMatch;
download.userTag = userTag;
download.allTags = allTags;
download.matchedUserTag = matchedUserTag || null;
const sonarrIssues = getImportIssues(sonarrMatch);
if (sonarrIssues) download.importIssues = sonarrIssues;
if (isAdmin) {
@@ -448,15 +472,18 @@ router.get('/user-downloads', async (req, res) => {
if (radarrMatch && radarrMatch.movieId) {
const movie = moviesMap.get(radarrMatch.movieId) || radarrMatch.movie;
if (movie) {
const userTag = extractUserTag(movie.tags, radarrTagMap);
if (userTag && (showAll || tagMatchesUser(userTag, username))) {
const matchedUserTag = extractUserTag(movie.tags, radarrTagMap);
const allTags = extractAllTags(movie.tags, radarrTagMap);
const hasAnyTag = allTags.length > 0;
if (showAll ? hasAnyTag : (matchedUserTag && tagMatchesUser(matchedUserTag, username))) {
console.log(`[Dashboard] Matched torrent "${torrentName}" to Radarr movie "${movie.title}"`);
const download = mapTorrentToDownload(torrent);
download.type = 'movie';
download.coverArt = getCoverArt(movie);
download.movieName = movie.title;
download.movieInfo = radarrMatch;
download.userTag = userTag;
download.allTags = allTags;
download.matchedUserTag = matchedUserTag || null;
const radarrIssues = getImportIssues(radarrMatch);
if (radarrIssues) download.importIssues = radarrIssues;
if (isAdmin) {
@@ -479,15 +506,18 @@ router.get('/user-downloads', async (req, res) => {
if (sonarrHistoryMatch && sonarrHistoryMatch.seriesId) {
const series = seriesMap.get(sonarrHistoryMatch.seriesId) || sonarrHistoryMatch.series;
if (series) {
const userTag = extractUserTag(series.tags, sonarrTagMap);
if (userTag && (showAll || tagMatchesUser(userTag, username))) {
const matchedUserTag = extractUserTag(series.tags, sonarrTagMap);
const allTags = extractAllTags(series.tags, sonarrTagMap);
const hasAnyTag = allTags.length > 0;
if (showAll ? hasAnyTag : (matchedUserTag && tagMatchesUser(matchedUserTag, username))) {
console.log(`[Dashboard] Matched torrent "${torrentName}" to Sonarr history "${series.title}"`);
const download = mapTorrentToDownload(torrent);
download.type = 'series';
download.coverArt = getCoverArt(series);
download.seriesName = series.title;
download.episodeInfo = sonarrHistoryMatch;
download.userTag = userTag;
download.allTags = allTags;
download.matchedUserTag = matchedUserTag || null;
if (isAdmin) {
download.downloadPath = download.savePath || null;
download.targetPath = series.path || null;
@@ -508,15 +538,18 @@ router.get('/user-downloads', async (req, res) => {
if (radarrHistoryMatch && radarrHistoryMatch.movieId) {
const movie = moviesMap.get(radarrHistoryMatch.movieId) || radarrHistoryMatch.movie;
if (movie) {
const userTag = extractUserTag(movie.tags, radarrTagMap);
if (userTag && (showAll || tagMatchesUser(userTag, username))) {
const matchedUserTag = extractUserTag(movie.tags, radarrTagMap);
const allTags = extractAllTags(movie.tags, radarrTagMap);
const hasAnyTag = allTags.length > 0;
if (showAll ? hasAnyTag : (matchedUserTag && tagMatchesUser(matchedUserTag, username))) {
console.log(`[Dashboard] Matched torrent "${torrentName}" to Radarr history "${movie.title}"`);
const download = mapTorrentToDownload(torrent);
download.type = 'movie';
download.coverArt = getCoverArt(movie);
download.movieName = movie.title;
download.movieInfo = radarrHistoryMatch;
download.userTag = userTag;
download.allTags = allTags;
download.matchedUserTag = matchedUserTag || null;
if (isAdmin) {
download.downloadPath = download.savePath || null;
download.targetPath = movie.path || null;