Refactor: Extract tag functions to TagMatcher service
- Extract six pure tag-related functions from dashboard.js into new server/services/TagMatcher.js - Functions: sanitizeTagLabel, tagMatchesUser, extractAllTags, extractUserTag, getEmbyUsers, buildTagBadges - Update dashboard.js to import TagMatcher and replace all inline function calls - Add comprehensive unit tests in tests/unit/services/TagMatcher.test.js (26 tests passing for 5 pure functions) - Note: getEmbyUsers tests excluded due to CommonJS mocking complexity
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
// Copyright (c) 2026 Gordon Bolton. MIT License.
|
||||
const axios = require('axios');
|
||||
const cache = require('../utils/cache');
|
||||
|
||||
// Return all resolved tag labels for a series/movie.
|
||||
// For Radarr: tags is array of IDs, tagMap is id -> label mapping.
|
||||
// For Sonarr: tags are objects with a label property.
|
||||
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);
|
||||
}
|
||||
|
||||
// Return the tag label that matches the current username, or null.
|
||||
function extractUserTag(tags, tagMap, username) {
|
||||
const allLabels = extractAllTags(tags, tagMap);
|
||||
if (!allLabels.length) return null;
|
||||
if (username) {
|
||||
const match = allLabels.find(label => tagMatchesUser(label, username));
|
||||
if (match) return match;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Replicate Ombi's StringHelper.SanitizeTagLabel: lowercase, replace non-alphanumeric with hyphen, collapse, trim
|
||||
function sanitizeTagLabel(input) {
|
||||
if (!input) return '';
|
||||
return input.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '');
|
||||
}
|
||||
|
||||
// Check if a tag matches the username: exact match first, then sanitized match
|
||||
function tagMatchesUser(tag, username) {
|
||||
if (!tag || !username) return false;
|
||||
const tagLower = tag.toLowerCase();
|
||||
// Exact match (handles users whose tags weren't mangled)
|
||||
if (tagLower === username) return true;
|
||||
// Sanitized match (handles Ombi-mangled tags for email-style usernames)
|
||||
if (tagLower === sanitizeTagLabel(username)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
// 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(`${process.env.EMBY_URL}/Users`, {
|
||||
headers: { 'X-MediaBrowser-Token': process.env.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 };
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
extractAllTags,
|
||||
extractUserTag,
|
||||
sanitizeTagLabel,
|
||||
tagMatchesUser,
|
||||
getEmbyUsers,
|
||||
buildTagBadges
|
||||
};
|
||||
Reference in New Issue
Block a user