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
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:
@@ -434,16 +434,26 @@ function createDownloadCard(download) {
|
|||||||
infoDiv.appendChild(movie);
|
infoDiv.appendChild(movie);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (showAll && download.allTags && download.allTags.length > 0) {
|
if (showAll && download.tagBadges && download.tagBadges.length > 0) {
|
||||||
const unmatchedTags = download.allTags.filter(t => t !== download.matchedUserTag);
|
// In showAll mode: render all tags classified by whether they match an Emby user.
|
||||||
for (const tag of unmatchedTags) {
|
// 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');
|
const badge = document.createElement('span');
|
||||||
badge.className = 'download-user-badge unmatched';
|
badge.className = 'download-user-badge unmatched';
|
||||||
badge.textContent = tag;
|
badge.textContent = b.label;
|
||||||
header.appendChild(badge);
|
header.appendChild(badge);
|
||||||
}
|
}
|
||||||
}
|
for (const b of matched) {
|
||||||
if (download.matchedUserTag) {
|
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');
|
const matchedBadge = document.createElement('span');
|
||||||
matchedBadge.className = 'download-user-badge';
|
matchedBadge.className = 'download-user-badge';
|
||||||
matchedBadge.textContent = download.matchedUserTag;
|
matchedBadge.textContent = download.matchedUserTag;
|
||||||
|
|||||||
@@ -94,6 +94,41 @@ function getRadarrLink(movie) {
|
|||||||
return `${movie._instanceUrl}/movie/${movie.titleSlug}`;
|
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 }>
|
// Track active dashboard clients: Map<username, { refreshRateMs, lastSeen }>
|
||||||
const activeClients = new Map();
|
const activeClients = new Map();
|
||||||
const CLIENT_STALE_MS = 30000; // consider client gone after 30s of no requests
|
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 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]));
|
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}`);
|
console.log(`[Dashboard] Cache data - Series: ${seriesMap.size}, Movies: ${moviesMap.size}, qBit: ${qbittorrentTorrents.length}`);
|
||||||
|
|
||||||
// Match SABnzbd downloads to Sonarr/Radarr activity
|
// Match SABnzbd downloads to Sonarr/Radarr activity
|
||||||
@@ -244,7 +282,8 @@ router.get('/user-downloads', async (req, res) => {
|
|||||||
seriesName: series.title,
|
seriesName: series.title,
|
||||||
episodeInfo: sonarrMatch,
|
episodeInfo: sonarrMatch,
|
||||||
allTags,
|
allTags,
|
||||||
matchedUserTag: matchedUserTag || null
|
matchedUserTag: matchedUserTag || null,
|
||||||
|
tagBadges: showAll ? buildTagBadges(allTags, embyUserMap) : undefined
|
||||||
};
|
};
|
||||||
const issues = getImportIssues(sonarrMatch);
|
const issues = getImportIssues(sonarrMatch);
|
||||||
if (issues) dlObj.importIssues = issues;
|
if (issues) dlObj.importIssues = issues;
|
||||||
@@ -285,7 +324,8 @@ router.get('/user-downloads', async (req, res) => {
|
|||||||
movieName: movie.title,
|
movieName: movie.title,
|
||||||
movieInfo: radarrMatch,
|
movieInfo: radarrMatch,
|
||||||
allTags,
|
allTags,
|
||||||
matchedUserTag: matchedUserTag || null
|
matchedUserTag: matchedUserTag || null,
|
||||||
|
tagBadges: showAll ? buildTagBadges(allTags, embyUserMap) : undefined
|
||||||
};
|
};
|
||||||
const issues = getImportIssues(radarrMatch);
|
const issues = getImportIssues(radarrMatch);
|
||||||
if (issues) dlObj.importIssues = issues;
|
if (issues) dlObj.importIssues = issues;
|
||||||
@@ -339,7 +379,8 @@ router.get('/user-downloads', async (req, res) => {
|
|||||||
seriesName: series.title,
|
seriesName: series.title,
|
||||||
episodeInfo: sonarrMatch,
|
episodeInfo: sonarrMatch,
|
||||||
allTags,
|
allTags,
|
||||||
matchedUserTag: matchedUserTag || null
|
matchedUserTag: matchedUserTag || null,
|
||||||
|
tagBadges: showAll ? buildTagBadges(allTags, embyUserMap) : undefined
|
||||||
};
|
};
|
||||||
if (isAdmin) {
|
if (isAdmin) {
|
||||||
dlObj.downloadPath = slot.storage || null;
|
dlObj.downloadPath = slot.storage || null;
|
||||||
@@ -374,7 +415,8 @@ router.get('/user-downloads', async (req, res) => {
|
|||||||
movieName: movie.title,
|
movieName: movie.title,
|
||||||
movieInfo: radarrMatch,
|
movieInfo: radarrMatch,
|
||||||
allTags,
|
allTags,
|
||||||
matchedUserTag: matchedUserTag || null
|
matchedUserTag: matchedUserTag || null,
|
||||||
|
tagBadges: showAll ? buildTagBadges(allTags, embyUserMap) : undefined
|
||||||
};
|
};
|
||||||
if (isAdmin) {
|
if (isAdmin) {
|
||||||
dlObj.downloadPath = slot.storage || null;
|
dlObj.downloadPath = slot.storage || null;
|
||||||
@@ -441,6 +483,7 @@ router.get('/user-downloads', async (req, res) => {
|
|||||||
download.episodeInfo = sonarrMatch;
|
download.episodeInfo = sonarrMatch;
|
||||||
download.allTags = allTags;
|
download.allTags = allTags;
|
||||||
download.matchedUserTag = matchedUserTag || null;
|
download.matchedUserTag = matchedUserTag || null;
|
||||||
|
download.tagBadges = showAll ? buildTagBadges(allTags, embyUserMap) : undefined;
|
||||||
const sonarrIssues = getImportIssues(sonarrMatch);
|
const sonarrIssues = getImportIssues(sonarrMatch);
|
||||||
if (sonarrIssues) download.importIssues = sonarrIssues;
|
if (sonarrIssues) download.importIssues = sonarrIssues;
|
||||||
if (isAdmin) {
|
if (isAdmin) {
|
||||||
@@ -475,6 +518,7 @@ router.get('/user-downloads', async (req, res) => {
|
|||||||
download.movieInfo = radarrMatch;
|
download.movieInfo = radarrMatch;
|
||||||
download.allTags = allTags;
|
download.allTags = allTags;
|
||||||
download.matchedUserTag = matchedUserTag || null;
|
download.matchedUserTag = matchedUserTag || null;
|
||||||
|
download.tagBadges = showAll ? buildTagBadges(allTags, embyUserMap) : undefined;
|
||||||
const radarrIssues = getImportIssues(radarrMatch);
|
const radarrIssues = getImportIssues(radarrMatch);
|
||||||
if (radarrIssues) download.importIssues = radarrIssues;
|
if (radarrIssues) download.importIssues = radarrIssues;
|
||||||
if (isAdmin) {
|
if (isAdmin) {
|
||||||
@@ -509,6 +553,7 @@ router.get('/user-downloads', async (req, res) => {
|
|||||||
download.episodeInfo = sonarrHistoryMatch;
|
download.episodeInfo = sonarrHistoryMatch;
|
||||||
download.allTags = allTags;
|
download.allTags = allTags;
|
||||||
download.matchedUserTag = matchedUserTag || null;
|
download.matchedUserTag = matchedUserTag || null;
|
||||||
|
download.tagBadges = showAll ? buildTagBadges(allTags, embyUserMap) : undefined;
|
||||||
if (isAdmin) {
|
if (isAdmin) {
|
||||||
download.downloadPath = download.savePath || null;
|
download.downloadPath = download.savePath || null;
|
||||||
download.targetPath = series.path || null;
|
download.targetPath = series.path || null;
|
||||||
@@ -541,6 +586,7 @@ router.get('/user-downloads', async (req, res) => {
|
|||||||
download.movieInfo = radarrHistoryMatch;
|
download.movieInfo = radarrHistoryMatch;
|
||||||
download.allTags = allTags;
|
download.allTags = allTags;
|
||||||
download.matchedUserTag = matchedUserTag || null;
|
download.matchedUserTag = matchedUserTag || null;
|
||||||
|
download.tagBadges = showAll ? buildTagBadges(allTags, embyUserMap) : undefined;
|
||||||
if (isAdmin) {
|
if (isAdmin) {
|
||||||
download.downloadPath = download.savePath || null;
|
download.downloadPath = download.savePath || null;
|
||||||
download.targetPath = movie.path || null;
|
download.targetPath = movie.path || null;
|
||||||
|
|||||||
Reference in New Issue
Block a user