Remove local Jellyfin manual mapping, keep only external mappings

This commit is contained in:
2026-02-06 12:05:26 -05:00
parent a3830c54c4
commit 28c4f8f5df
3 changed files with 272 additions and 513 deletions

View File

@@ -686,17 +686,13 @@
</div>
</h2>
<p style="color: var(--text-secondary); margin-bottom: 12px;">
Manual mappings override automatic matching. <strong>Local (Jellyfin)</strong> mappings will be phased out in favor of the Spotify Import plugin.
Manual mappings override automatic matching for external providers (SquidWTF, Deezer, Qobuz). For local Jellyfin tracks, use the Spotify Import plugin instead.
</p>
<div id="mappings-summary" style="display: flex; gap: 20px; margin-bottom: 16px; padding: 12px; background: var(--bg-tertiary); border-radius: 6px;">
<div>
<span style="color: var(--text-secondary);">Total:</span>
<span style="font-weight: 600; margin-left: 8px;" id="mappings-total">0</span>
</div>
<div>
<span style="color: var(--text-secondary);">Jellyfin (Local):</span>
<span style="font-weight: 600; margin-left: 8px; color: var(--accent);" id="mappings-jellyfin">0</span>
</div>
<div>
<span style="color: var(--text-secondary);">External:</span>
<span style="font-weight: 600; margin-left: 8px; color: var(--success);" id="mappings-external">0</span>
@@ -942,9 +938,9 @@
<!-- Manual Track Mapping Modal -->
<div class="modal" id="manual-map-modal">
<div class="modal-content" style="max-width: 600px;">
<h3>Map Track</h3>
<h3>Map Track to External Provider</h3>
<p style="color: var(--text-secondary); margin-bottom: 16px;">
Map this track to either a local Jellyfin track or provide an external provider ID.
Map this track to an external provider (SquidWTF, Deezer, or Qobuz). For local Jellyfin tracks, use the Spotify Import plugin instead.
</p>
<!-- Track Info -->
@@ -956,41 +952,8 @@
</div>
</div>
<!-- Mapping Type Selection -->
<div class="form-group">
<label>Mapping Type</label>
<select id="map-type-select" onchange="toggleMappingType()" style="width: 100%;">
<option value="jellyfin">Map to Local Jellyfin Track</option>
<option value="external">Map to External Provider ID</option>
</select>
</div>
<!-- Jellyfin Mapping Section -->
<div id="jellyfin-mapping-section">
<div class="form-group">
<label>Search Jellyfin Tracks</label>
<input type="text" id="map-search-query" placeholder="Search by title, artist, or album..." oninput="searchJellyfinTracks()">
<small style="color: var(--text-secondary); display: block; margin-top: 4px;">
Tip: Use commas to search multiple terms (e.g., "It Ain't Easy, David Bowie")
</small>
</div>
<div style="text-align: center; color: var(--text-secondary); margin: 12px 0;">— OR —</div>
<div class="form-group">
<label>Paste Jellyfin Track URL</label>
<input type="text" id="map-jellyfin-url" placeholder="https://jellyfin.example.com/web/#/details?id=..." oninput="extractJellyfinId()">
<small style="color: var(--text-secondary); display: block; margin-top: 4px;">
Paste the full URL from your Jellyfin web interface
</small>
</div>
<div id="map-search-results" style="max-height: 300px; overflow-y: auto; margin-top: 12px;">
<p style="text-align: center; color: var(--text-secondary); padding: 20px;">
Type to search for local tracks or paste a Jellyfin URL...
</p>
</div>
</div>
<!-- External Mapping Section -->
<div id="external-mapping-section" style="display: none;">
<div id="external-mapping-section">
<div class="form-group">
<label>External Provider</label>
<select id="map-external-provider" style="width: 100%;">
@@ -1012,7 +975,6 @@
<input type="hidden" id="map-playlist-name">
<input type="hidden" id="map-spotify-id">
<input type="hidden" id="map-selected-jellyfin-id">
<div class="modal-actions">
<button onclick="closeModal('manual-map-modal')">Cancel</button>
<button class="primary" onclick="saveManualMapping()" id="map-save-btn" disabled>Save Mapping</button>
@@ -1388,9 +1350,8 @@
const res = await fetch('/api/admin/mappings/tracks');
const data = await res.json();
// Update summary
document.getElementById('mappings-total').textContent = data.totalCount || 0;
document.getElementById('mappings-jellyfin').textContent = data.jellyfinCount || 0;
// Update summary (only external now)
document.getElementById('mappings-total').textContent = data.externalCount || 0;
document.getElementById('mappings-external').textContent = data.externalCount || 0;
const tbody = document.getElementById('mappings-table-body');
@@ -1400,16 +1361,19 @@
return;
}
tbody.innerHTML = data.mappings.map((m, index) => {
const typeColor = m.type === 'jellyfin' ? 'var(--accent)' : 'var(--success)';
const typeBadge = `<span style="display:inline-block;padding:2px 8px;border-radius:4px;font-size:0.8rem;background:${typeColor}20;color:${typeColor};font-weight:500;">${m.type}</span>`;
// Filter to only show external mappings
const externalMappings = data.mappings.filter(m => m.type === 'external');
if (externalMappings.length === 0) {
tbody.innerHTML = '<tr><td colspan="6" style="text-align:center;color:var(--text-secondary);padding:40px;">No external mappings found. Local Jellyfin mappings should be managed via Spotify Import plugin.</td></tr>';
return;
}
tbody.innerHTML = externalMappings.map((m, index) => {
const typeColor = 'var(--success)';
const typeBadge = `<span style="display:inline-block;padding:2px 8px;border-radius:4px;font-size:0.8rem;background:${typeColor}20;color:${typeColor};font-weight:500;">external</span>`;
let targetDisplay = '';
if (m.type === 'jellyfin') {
targetDisplay = `<span style="font-family:monospace;font-size:0.85rem;">${m.jellyfinId}</span>`;
} else {
targetDisplay = `<span style="font-family:monospace;font-size:0.85rem;color:var(--success);">${m.externalProvider}/${m.externalId}</span>`;
}
const targetDisplay = `<span style="font-family:monospace;font-size:0.85rem;color:var(--success);">${m.externalProvider}/${m.externalId}</span>`;
const createdDate = m.createdAt ? new Date(m.createdAt).toLocaleString() : '-';
@@ -1442,7 +1406,7 @@
}
async function deleteTrackMapping(playlist, spotifyId) {
if (!confirm(`Remove manual mapping for ${spotifyId} in playlist "${playlist}"?\n\nThis will:\n• Delete the manual mapping from the cache\n• Allow the track to be matched automatically again\nFor local (Jellyfin) tracks: Stop injecting locally if now available via Spotify Import plugin\n• For external tracks: Allow re-matching with potentially better results\n\nThis action cannot be undone.`)) {
if (!confirm(`Remove manual external mapping for ${spotifyId} in playlist "${playlist}"?\n\nThis will:\n• Delete the manual mapping from the cache\n• Allow the track to be matched automatically again\nThe track may be re-matched with potentially better results\n\nThis action cannot be undone.`)) {
return;
}
@@ -2318,34 +2282,6 @@
document.getElementById('map-save-btn').disabled = false;
}
// Toggle between Jellyfin and external mapping modes
function toggleMappingType() {
const mappingType = document.getElementById('map-type-select').value;
const jellyfinSection = document.getElementById('jellyfin-mapping-section');
const externalSection = document.getElementById('external-mapping-section');
const saveBtn = document.getElementById('map-save-btn');
if (mappingType === 'jellyfin') {
jellyfinSection.style.display = 'block';
externalSection.style.display = 'none';
// Reset external fields
document.getElementById('map-external-id').value = '';
// Check if Jellyfin track is selected
const jellyfinId = document.getElementById('map-selected-jellyfin-id').value;
saveBtn.disabled = !jellyfinId;
} else {
jellyfinSection.style.display = 'none';
externalSection.style.display = 'block';
// Reset Jellyfin fields
document.getElementById('map-search-query').value = '';
document.getElementById('map-jellyfin-url').value = '';
document.getElementById('map-selected-jellyfin-id').value = '';
document.getElementById('map-search-results').innerHTML = '<p style="text-align: center; color: var(--text-secondary); padding: 20px;">Enter an external provider ID above</p>';
// Check if external mapping is valid
validateExternalMapping();
}
}
// Validate external mapping input
function validateExternalMapping() {
const externalId = document.getElementById('map-external-id').value.trim();
@@ -2355,7 +2291,7 @@
saveBtn.disabled = !externalId;
}
// Update the openManualMap function to reset the modal state
// Open manual mapping modal (external only)
function openManualMap(playlistName, position, title, artist, spotifyId) {
document.getElementById('map-playlist-name').value = playlistName;
document.getElementById('map-position').textContent = position + 1;
@@ -2363,75 +2299,39 @@
document.getElementById('map-spotify-artist').textContent = artist;
document.getElementById('map-spotify-id').value = spotifyId;
// Reset to Jellyfin mapping mode
document.getElementById('map-type-select').value = 'jellyfin';
document.getElementById('jellyfin-mapping-section').style.display = 'block';
document.getElementById('external-mapping-section').style.display = 'none';
// Reset all fields
document.getElementById('map-search-query').value = '';
document.getElementById('map-jellyfin-url').value = '';
document.getElementById('map-selected-jellyfin-id').value = '';
// Reset fields
document.getElementById('map-external-id').value = '';
document.getElementById('map-external-provider').value = 'SquidWTF';
document.getElementById('map-save-btn').disabled = true;
document.getElementById('map-search-results').innerHTML = '<p style="text-align: center; color: var(--text-secondary); padding: 20px;">Type to search for local tracks or paste a Jellyfin URL...</p>';
openModal('manual-map-modal');
}
// Open external mapping modal (pre-set to external mode)
// Alias for backward compatibility
function openExternalMap(playlistName, position, title, artist, spotifyId) {
document.getElementById('map-playlist-name').value = playlistName;
document.getElementById('map-position').textContent = position + 1;
document.getElementById('map-spotify-title').textContent = title;
document.getElementById('map-spotify-artist').textContent = artist;
document.getElementById('map-spotify-id').value = spotifyId;
// Set to external mapping mode
document.getElementById('map-type-select').value = 'external';
document.getElementById('jellyfin-mapping-section').style.display = 'none';
document.getElementById('external-mapping-section').style.display = 'block';
// Reset all fields
document.getElementById('map-search-query').value = '';
document.getElementById('map-jellyfin-url').value = '';
document.getElementById('map-selected-jellyfin-id').value = '';
document.getElementById('map-external-id').value = '';
document.getElementById('map-external-provider').value = 'SquidWTF';
document.getElementById('map-save-btn').disabled = true;
document.getElementById('map-search-results').innerHTML = '<p style="text-align: center; color: var(--text-secondary); padding: 20px;">Enter an external provider ID above</p>';
openModal('manual-map-modal');
openManualMap(playlistName, position, title, artist, spotifyId);
}
// Update the saveManualMapping function to handle both types
// Save manual mapping (external only)
async function saveManualMapping() {
const playlistName = document.getElementById('map-playlist-name').value;
const spotifyId = document.getElementById('map-spotify-id').value;
const mappingType = document.getElementById('map-type-select').value;
const position = parseInt(document.getElementById('map-position').textContent) - 1; // Convert back to 0-indexed
let requestBody = { spotifyId };
const externalProvider = document.getElementById('map-external-provider').value;
const externalId = document.getElementById('map-external-id').value.trim();
if (mappingType === 'jellyfin') {
const jellyfinId = document.getElementById('map-selected-jellyfin-id').value;
if (!jellyfinId) {
showToast('Please select a track', 'error');
return;
}
requestBody.jellyfinId = jellyfinId;
} else {
const externalProvider = document.getElementById('map-external-provider').value;
const externalId = document.getElementById('map-external-id').value.trim();
if (!externalId) {
showToast('Please enter an external provider ID', 'error');
return;
}
requestBody.externalProvider = externalProvider;
requestBody.externalId = externalId;
if (!externalId) {
showToast('Please enter an external provider ID', 'error');
return;
}
const requestBody = {
spotifyId,
externalProvider,
externalId
};
// Show loading state
const saveBtn = document.getElementById('map-save-btn');
const originalText = saveBtn.textContent;
@@ -2453,8 +2353,7 @@
const data = await res.json();
if (res.ok) {
const mappingTypeText = mappingType === 'jellyfin' ? 'local Jellyfin track' : `${requestBody.externalProvider} ID`;
showToast(`✓ Track mapped to ${mappingTypeText} - rebuilding playlist...`, 'success');
showToast(`✓ Track mapped to ${requestBody.externalProvider} - rebuilding playlist...`, 'success');
closeModal('manual-map-modal');
// Show rebuilding indicator
@@ -2462,27 +2361,15 @@
// Show detailed info toast after a moment
setTimeout(() => {
if (mappingType === 'jellyfin') {
showToast('🔄 Rebuilding playlist with your local track mapping...', 'info', 8000);
} else {
showToast(`🔄 Rebuilding playlist with your ${requestBody.externalProvider} mapping...`, 'info', 8000);
}
showToast(`🔄 Rebuilding playlist with your ${requestBody.externalProvider} mapping...`, 'info', 8000);
}, 1000);
// Update the track in the UI without refreshing
const trackItem = document.querySelector(`.track-item[data-position="${position}"]`);
if (trackItem) {
const titleEl = trackItem.querySelector('.track-info h4');
if (titleEl && mappingType === 'jellyfin' && data.track) {
// For Jellyfin mappings, update with actual track info
const titleText = data.track.title;
const newStatusBadge = '<span class="status-badge success" style="font-size:0.75rem;padding:2px 8px;margin-left:8px;"><span class="status-dot"></span>Local</span>';
titleEl.innerHTML = escapeHtml(titleText) + newStatusBadge;
const artistEl = trackItem.querySelector('.track-info .artists');
if (artistEl) artistEl.textContent = data.track.artist;
} else if (titleEl && mappingType === 'external') {
// For external mappings, update status badge to show provider
if (titleEl) {
// Update status badge to show provider
const currentTitle = titleEl.textContent.split(' - ')[0]; // Remove old status
const capitalizedProvider = capitalizeProvider(requestBody.externalProvider);
const newStatusBadge = `<span class="status-badge warning" style="font-size:0.75rem;padding:2px 8px;margin-left:8px;"><span class="status-dot"></span>${escapeHtml(capitalizedProvider)}</span>`;