4d61dd566f
- 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
87 lines
3.0 KiB
JavaScript
87 lines
3.0 KiB
JavaScript
// 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
|
|
};
|