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);
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user