@@ -1543,17 +1576,21 @@
document.getElementById('tracks-list').innerHTML = data.tracks.map(t => {
let statusBadge = '';
+ let mapButton = '';
+
if (t.isLocal === true) {
statusBadge = '
Local';
} else if (t.isLocal === false) {
statusBadge = '
External';
+ // Add manual map button for external tracks
+ mapButton = `
`;
}
return `
${t.position + 1}
-
${escapeHtml(t.title)}${statusBadge}
+ ${escapeHtml(t.title)}${statusBadge}${mapButton}
${escapeHtml(t.artists.join(', '))}
@@ -1648,6 +1685,108 @@
return div.innerHTML;
}
+ // 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 = '
Type to search for local tracks...
';
+
+ openModal('manual-map-modal');
+ }
+
+ async function searchJellyfinTracks() {
+ const query = document.getElementById('map-search-query').value.trim();
+
+ if (!query) {
+ document.getElementById('map-search-results').innerHTML = '
Type to search for local tracks...
';
+ return;
+ }
+
+ // Debounce search
+ clearTimeout(searchTimeout);
+ searchTimeout = setTimeout(async () => {
+ document.getElementById('map-search-results').innerHTML = '
Searching...
';
+
+ try {
+ const res = await fetch('/api/admin/jellyfin/search?query=' + encodeURIComponent(query));
+ const data = await res.json();
+
+ if (data.tracks.length === 0) {
+ document.getElementById('map-search-results').innerHTML = '
No tracks found
';
+ return;
+ }
+
+ document.getElementById('map-search-results').innerHTML = data.tracks.map(t => `
+
+
+
${escapeHtml(t.title)}
+ ${escapeHtml(t.artist)}
+
+
+ ${t.album ? escapeHtml(t.album) : ''}
+
+
+ `).join('');
+ } catch (error) {
+ document.getElementById('map-search-results').innerHTML = '
Search failed
';
+ }
+ }, 300);
+ }
+
+ function selectJellyfinTrack(jellyfinId, element) {
+ // Remove selection from all tracks
+ document.querySelectorAll('#map-search-results .track-item').forEach(el => {
+ el.style.border = '2px solid transparent';
+ });
+
+ // Highlight selected track
+ element.style.border = '2px solid var(--primary)';
+
+ // Store selected ID and enable save button
+ document.getElementById('map-selected-jellyfin-id').value = jellyfinId;
+ document.getElementById('map-save-btn').disabled = false;
+ }
+
+ 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;
+
+ if (!jellyfinId) {
+ showToast('Please select a track', 'error');
+ return;
+ }
+
+ try {
+ const res = await fetch('/api/admin/playlists/' + encodeURIComponent(playlistName) + '/map', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ spotifyId, jellyfinId })
+ });
+
+ const data = await res.json();
+
+ if (res.ok) {
+ showToast('Track mapped successfully! Refresh the playlist to see changes.', 'success');
+ closeModal('manual-map-modal');
+ // Refresh the tracks view
+ viewTracks(playlistName);
+ } else {
+ showToast(data.error || 'Failed to save mapping', 'error');
+ }
+ } catch (error) {
+ showToast('Failed to save mapping', 'error');
+ }
+ }
+
function escapeJs(text) {
if (!text) return '';
return text.replace(/\\/g, '\\\\').replace(/'/g, "\\'").replace(/"/g, '\\"');