mirror of
https://github.com/SoPat712/allstarr.git
synced 2026-02-09 23:55:10 -05:00
Fix missing track labeling and add external manual mapping support
- Fixed syntax errors in AdminController.cs (missing braces, duplicate code) - Implemented proper track status logic to distinguish between: * Local tracks: isLocal=true, externalProvider=null * External matched tracks: isLocal=false, externalProvider='SquidWTF' * Missing tracks: isLocal=null, externalProvider=null - Added external manual mapping support for SquidWTF/Deezer/Qobuz IDs - Updated frontend UI with dual mapping modes (Jellyfin vs External) - Extended ManualMappingRequest class with ExternalProvider + ExternalId fields - Updated SpotifyTrackMatchingService to handle external manual mappings - Fixed variable name conflicts and dynamic argument casting issues - All tests passing (225/225) Resolves issue where missing tracks incorrectly showed provider name instead of 'Missing' status.
This commit is contained in:
@@ -895,10 +895,12 @@
|
||||
<!-- Manual Track Mapping Modal -->
|
||||
<div class="modal" id="manual-map-modal">
|
||||
<div class="modal-content" style="max-width: 600px;">
|
||||
<h3>Map Track to Local File</h3>
|
||||
<h3>Map Track</h3>
|
||||
<p style="color: var(--text-secondary); margin-bottom: 16px;">
|
||||
This track is currently using an external provider. Search for and select the local Jellyfin track to use instead.
|
||||
Map this track to either a local Jellyfin track or provide an external provider ID.
|
||||
</p>
|
||||
|
||||
<!-- Track Info -->
|
||||
<div class="form-group">
|
||||
<label>Spotify Track (Position <span id="map-position"></span>)</label>
|
||||
<div style="background: var(--bg-primary); padding: 12px; border-radius: 8px; margin-bottom: 16px;">
|
||||
@@ -906,26 +908,61 @@
|
||||
<span style="color: var(--text-secondary);" id="map-spotify-artist"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Mapping Type Selection -->
|
||||
<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>
|
||||
<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>
|
||||
<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>
|
||||
|
||||
<!-- 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>
|
||||
<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>
|
||||
|
||||
<!-- External Mapping Section -->
|
||||
<div id="external-mapping-section" style="display: none;">
|
||||
<div class="form-group">
|
||||
<label>External Provider</label>
|
||||
<select id="map-external-provider" style="width: 100%;">
|
||||
<option value="SquidWTF">SquidWTF</option>
|
||||
<option value="Deezer">Deezer</option>
|
||||
<option value="Qobuz">Qobuz</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>External Provider ID</label>
|
||||
<input type="text" id="map-external-id" placeholder="Enter the provider-specific track ID..." oninput="validateExternalMapping()">
|
||||
<small style="color: var(--text-secondary); display: block; margin-top: 4px;">
|
||||
For SquidWTF: Use the track ID from the search results or URL<br>
|
||||
For Deezer: Use the track ID from Deezer URLs<br>
|
||||
For Qobuz: Use the track ID from Qobuz URLs
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input type="hidden" id="map-playlist-name">
|
||||
<input type="hidden" id="map-spotify-id">
|
||||
<input type="hidden" id="map-selected-jellyfin-id">
|
||||
@@ -1892,20 +1929,6 @@
|
||||
// Manual track mapping
|
||||
let searchTimeout = null;
|
||||
|
||||
function openManualMap(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;
|
||||
document.getElementById('map-search-query').value = '';
|
||||
document.getElementById('map-selected-jellyfin-id').value = '';
|
||||
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...</p>';
|
||||
|
||||
openModal('manual-map-modal');
|
||||
}
|
||||
|
||||
async function searchJellyfinTracks() {
|
||||
const query = document.getElementById('map-search-query').value.trim();
|
||||
|
||||
@@ -2035,15 +2058,93 @@
|
||||
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();
|
||||
const saveBtn = document.getElementById('map-save-btn');
|
||||
|
||||
// Enable save button if external ID is provided
|
||||
saveBtn.disabled = !externalId;
|
||||
}
|
||||
|
||||
// Update the openManualMap function to reset the modal state
|
||||
function openManualMap(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;
|
||||
|
||||
// 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 = '';
|
||||
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');
|
||||
}
|
||||
|
||||
// Update the saveManualMapping function to handle both types
|
||||
async function saveManualMapping() {
|
||||
const playlistName = document.getElementById('map-playlist-name').value;
|
||||
const spotifyId = document.getElementById('map-spotify-id').value;
|
||||
const jellyfinId = document.getElementById('map-selected-jellyfin-id').value;
|
||||
const mappingType = document.getElementById('map-type-select').value;
|
||||
const position = parseInt(document.getElementById('map-position').textContent) - 1; // Convert back to 0-indexed
|
||||
|
||||
if (!jellyfinId) {
|
||||
showToast('Please select a track', 'error');
|
||||
return;
|
||||
let requestBody = { spotifyId };
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// Show loading state
|
||||
@@ -2059,7 +2160,7 @@
|
||||
const res = await fetch('/api/admin/playlists/' + encodeURIComponent(playlistName) + '/map', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ spotifyId, jellyfinId }),
|
||||
body: JSON.stringify(requestBody),
|
||||
signal: controller.signal
|
||||
});
|
||||
|
||||
@@ -2067,7 +2168,8 @@
|
||||
const data = await res.json();
|
||||
|
||||
if (res.ok) {
|
||||
showToast('✓ Track mapped successfully - rebuilding playlist...', 'success');
|
||||
const mappingTypeText = mappingType === 'jellyfin' ? 'local Jellyfin track' : `${requestBody.externalProvider} ID`;
|
||||
showToast(`✓ Track mapped to ${mappingTypeText} - rebuilding playlist...`, 'success');
|
||||
closeModal('manual-map-modal');
|
||||
|
||||
// Show rebuilding indicator
|
||||
@@ -2075,38 +2177,36 @@
|
||||
|
||||
// Show detailed info toast after a moment
|
||||
setTimeout(() => {
|
||||
showToast('🔄 Searching external providers to rebuild playlist with your manual mapping...', 'info', 8000);
|
||||
if (mappingType === 'jellyfin') {
|
||||
showToast('🔄 Rebuilding playlist with your local track mapping...', 'info', 8000);
|
||||
} else {
|
||||
showToast(`🔄 Rebuilding playlist with your ${requestBody.externalProvider} mapping...`, 'info', 8000);
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
// Update the track in the UI without refreshing
|
||||
if (data.track) {
|
||||
const trackItem = document.querySelector(`.track-item[data-position="${position}"]`);
|
||||
if (trackItem) {
|
||||
// Update the track info
|
||||
const titleEl = trackItem.querySelector('.track-info h4');
|
||||
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');
|
||||
const statusBadge = trackItem.querySelector('.status-badge');
|
||||
const mapButton = trackItem.querySelector('.map-track-btn');
|
||||
const searchLink = trackItem.querySelector('.track-meta a');
|
||||
|
||||
if (titleEl) {
|
||||
// Remove the old status badge and map button, add new content
|
||||
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;
|
||||
}
|
||||
|
||||
if (artistEl) artistEl.textContent = data.track.artist;
|
||||
|
||||
// Remove the search link since it's now local
|
||||
if (searchLink) {
|
||||
const metaEl = trackItem.querySelector('.track-meta');
|
||||
if (metaEl) {
|
||||
// Keep album and ISRC, remove search link
|
||||
const albumText = data.track.album ? escapeHtml(data.track.album) : '';
|
||||
metaEl.innerHTML = albumText;
|
||||
}
|
||||
}
|
||||
} else if (titleEl && mappingType === 'external') {
|
||||
// For external mappings, update status badge to show provider
|
||||
const currentTitle = titleEl.textContent.split(' - ')[0]; // Remove old status
|
||||
const newStatusBadge = `<span class="status-badge warning" style="font-size:0.75rem;padding:2px 8px;margin-left:8px;"><span class="status-dot"></span>${escapeHtml(requestBody.externalProvider)}</span>`;
|
||||
titleEl.innerHTML = escapeHtml(currentTitle) + newStatusBadge;
|
||||
}
|
||||
|
||||
// Remove search link since it's now mapped
|
||||
const searchLink = trackItem.querySelector('.track-meta a');
|
||||
if (searchLink) {
|
||||
searchLink.remove();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user