fix: proper multi-user tag badges using full Emby user list
All checks were successful
Build and Push Docker Image / build (push) Successful in 28s

Server:
- Add getEmbyUsers(): fetches all Emby users, builds Map of
  lowercase/sanitized name -> display name, cached 60s
- Add buildTagBadges(allTags, embyUserMap): classifies each tag
  as { label, matchedUser: displayName|null } against the full
  Emby user database
- Attach tagBadges[] to every download object when showAll=true
  (all 10 construction sites across SABnzbd queue/history and
  qBittorrent queue/history blocks)
- matchedUserTag still set to the tag matching the *current* user
  for the non-showAll badge

Frontend:
- showAll mode: renders tagBadges[] — unmatched tags (no Emby user)
  amber leftmost, matched tags show Emby display name in accent
  colour rightmost
- Normal mode: renders matchedUserTag badge only (current user's tag)
This commit is contained in:
2026-05-16 15:29:50 +01:00
parent 1f4aa19a72
commit 8b81f16dac
2 changed files with 66 additions and 10 deletions

View File

@@ -434,16 +434,26 @@ function createDownloadCard(download) {
infoDiv.appendChild(movie);
}
if (showAll && download.allTags && download.allTags.length > 0) {
const unmatchedTags = download.allTags.filter(t => t !== download.matchedUserTag);
for (const tag of unmatchedTags) {
if (showAll && download.tagBadges && download.tagBadges.length > 0) {
// In showAll mode: render all tags classified by whether they match an Emby user.
// Unmatched (no known Emby user) → amber, leftmost.
// Matched → show Emby display name in accent colour, rightmost.
const unmatched = download.tagBadges.filter(b => !b.matchedUser);
const matched = download.tagBadges.filter(b => b.matchedUser);
for (const b of unmatched) {
const badge = document.createElement('span');
badge.className = 'download-user-badge unmatched';
badge.textContent = tag;
badge.textContent = b.label;
header.appendChild(badge);
}
}
if (download.matchedUserTag) {
for (const b of matched) {
const badge = document.createElement('span');
badge.className = 'download-user-badge';
badge.textContent = b.matchedUser;
header.appendChild(badge);
}
} else if (download.matchedUserTag) {
// Normal (non-showAll) view: show only the current user's matched tag
const matchedBadge = document.createElement('span');
matchedBadge.className = 'download-user-badge';
matchedBadge.textContent = download.matchedUserTag;

View File

@@ -94,6 +94,41 @@ function getRadarrLink(movie) {
return `${movie._instanceUrl}/movie/${movie.titleSlug}`;
}
// Fetch all Emby users and return a Map<lowerName -> displayName> (and sanitized variants).
// Result is cached for 60s to avoid hammering Emby on every dashboard poll.
async function getEmbyUsers() {
const cached = cache.get('emby:users');
if (cached) return cached;
try {
const response = await axios.get(`${EMBY_URL}/Users`, {
headers: { 'X-MediaBrowser-Token': EMBY_API_KEY }
});
// Build map: both raw lowercase and sanitized form -> display name
const map = new Map();
for (const u of response.data) {
const name = u.Name || '';
map.set(name.toLowerCase(), name);
map.set(sanitizeTagLabel(name), name);
}
cache.set('emby:users', map, 60000);
return map;
} catch (err) {
console.error('[Dashboard] Failed to fetch Emby users:', err.message);
return new Map();
}
}
// Classify each tag label: matched to a known Emby user, or unmatched.
// Returns array of { label, matchedUser: string|null }
function buildTagBadges(allTags, embyUserMap) {
return allTags.map(label => {
const lower = label.toLowerCase();
const sanitized = sanitizeTagLabel(label);
const displayName = embyUserMap.get(lower) || embyUserMap.get(sanitized) || null;
return { label, matchedUser: displayName };
});
}
// Track active dashboard clients: Map<username, { refreshRateMs, lastSeen }>
const activeClients = new Map();
const CLIENT_STALE_MS = 30000; // consider client gone after 30s of no requests
@@ -181,6 +216,9 @@ router.get('/user-downloads', async (req, res) => {
const sonarrTagMap = new Map(sonarrTagsResults.flatMap(t => t.data || []).map(t => [t.id, t.label]));
const radarrTagMap = new Map(radarrTags.data.map(t => [t.id, t.label]));
// When showing all downloads, fetch full Emby user list to classify tags
const embyUserMap = showAll ? await getEmbyUsers() : new Map();
console.log(`[Dashboard] Cache data - Series: ${seriesMap.size}, Movies: ${moviesMap.size}, qBit: ${qbittorrentTorrents.length}`);
// Match SABnzbd downloads to Sonarr/Radarr activity
@@ -244,7 +282,8 @@ router.get('/user-downloads', async (req, res) => {
seriesName: series.title,
episodeInfo: sonarrMatch,
allTags,
matchedUserTag: matchedUserTag || null
matchedUserTag: matchedUserTag || null,
tagBadges: showAll ? buildTagBadges(allTags, embyUserMap) : undefined
};
const issues = getImportIssues(sonarrMatch);
if (issues) dlObj.importIssues = issues;
@@ -285,7 +324,8 @@ router.get('/user-downloads', async (req, res) => {
movieName: movie.title,
movieInfo: radarrMatch,
allTags,
matchedUserTag: matchedUserTag || null
matchedUserTag: matchedUserTag || null,
tagBadges: showAll ? buildTagBadges(allTags, embyUserMap) : undefined
};
const issues = getImportIssues(radarrMatch);
if (issues) dlObj.importIssues = issues;
@@ -339,7 +379,8 @@ router.get('/user-downloads', async (req, res) => {
seriesName: series.title,
episodeInfo: sonarrMatch,
allTags,
matchedUserTag: matchedUserTag || null
matchedUserTag: matchedUserTag || null,
tagBadges: showAll ? buildTagBadges(allTags, embyUserMap) : undefined
};
if (isAdmin) {
dlObj.downloadPath = slot.storage || null;
@@ -374,7 +415,8 @@ router.get('/user-downloads', async (req, res) => {
movieName: movie.title,
movieInfo: radarrMatch,
allTags,
matchedUserTag: matchedUserTag || null
matchedUserTag: matchedUserTag || null,
tagBadges: showAll ? buildTagBadges(allTags, embyUserMap) : undefined
};
if (isAdmin) {
dlObj.downloadPath = slot.storage || null;
@@ -441,6 +483,7 @@ router.get('/user-downloads', async (req, res) => {
download.episodeInfo = sonarrMatch;
download.allTags = allTags;
download.matchedUserTag = matchedUserTag || null;
download.tagBadges = showAll ? buildTagBadges(allTags, embyUserMap) : undefined;
const sonarrIssues = getImportIssues(sonarrMatch);
if (sonarrIssues) download.importIssues = sonarrIssues;
if (isAdmin) {
@@ -475,6 +518,7 @@ router.get('/user-downloads', async (req, res) => {
download.movieInfo = radarrMatch;
download.allTags = allTags;
download.matchedUserTag = matchedUserTag || null;
download.tagBadges = showAll ? buildTagBadges(allTags, embyUserMap) : undefined;
const radarrIssues = getImportIssues(radarrMatch);
if (radarrIssues) download.importIssues = radarrIssues;
if (isAdmin) {
@@ -509,6 +553,7 @@ router.get('/user-downloads', async (req, res) => {
download.episodeInfo = sonarrHistoryMatch;
download.allTags = allTags;
download.matchedUserTag = matchedUserTag || null;
download.tagBadges = showAll ? buildTagBadges(allTags, embyUserMap) : undefined;
if (isAdmin) {
download.downloadPath = download.savePath || null;
download.targetPath = series.path || null;
@@ -541,6 +586,7 @@ router.get('/user-downloads', async (req, res) => {
download.movieInfo = radarrHistoryMatch;
download.allTags = allTags;
download.matchedUserTag = matchedUserTag || null;
download.tagBadges = showAll ? buildTagBadges(allTags, embyUserMap) : undefined;
if (isAdmin) {
download.downloadPath = download.savePath || null;
download.targetPath = movie.path || null;