Fix apostrophe matching, add URL input for track mapping, improve search caching
Some checks failed
CI / build-and-test (push) Has been cancelled

- Enhanced FuzzyMatcher to normalize apostrophes (', ', ', etc) for better matching
- Added Redis-only caching for search results (15 min TTL)
- Added pattern-based cache deletion for search and image keys
- Added URL input field in Map to Local modal to paste Jellyfin track URLs
- Added /api/admin/jellyfin/track/{id} endpoint to fetch track details by ID
- Fixed duplicate cache key declaration in GetSpotifyPlaylistTracksOrderedAsync
- Updated cache clearing to include new spotify:playlist:items:* keys
This commit is contained in:
2026-02-04 01:44:56 -05:00
parent 8a84237f13
commit aadda9b873
5 changed files with 342 additions and 8 deletions

View File

@@ -873,11 +873,22 @@
</div>
<div class="form-group">
<label>Search Jellyfin Tracks</label>
<input type="text" id="map-search-query" placeholder="Search by title or artist..." oninput="searchJellyfinTracks()">
<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...
Type to search for local tracks or paste a Jellyfin URL...
</p>
</div>
<input type="hidden" id="map-playlist-name">
@@ -1757,10 +1768,13 @@
const query = document.getElementById('map-search-query').value.trim();
if (!query) {
document.getElementById('map-search-results').innerHTML = '<p style="text-align: center; color: var(--text-secondary); padding: 20px;">Type to search for local tracks...</p>';
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>';
return;
}
// Clear URL input when searching
document.getElementById('map-jellyfin-url').value = '';
// Debounce search
clearTimeout(searchTimeout);
searchTimeout = setTimeout(async () => {
@@ -1792,6 +1806,77 @@
}, 300);
}
async function extractJellyfinId() {
const url = document.getElementById('map-jellyfin-url').value.trim();
if (!url) {
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>';
document.getElementById('map-selected-jellyfin-id').value = '';
document.getElementById('map-save-btn').disabled = true;
return;
}
// Clear search input when using URL
document.getElementById('map-search-query').value = '';
// Extract ID from URL patterns:
// https://jellyfin.example.com/web/#/details?id=XXXXX&serverId=...
// https://jellyfin.example.com/web/index.html#!/details?id=XXXXX
let jellyfinId = null;
try {
const idMatch = url.match(/[?&]id=([a-f0-9]+)/i);
if (idMatch) {
jellyfinId = idMatch[1];
}
} catch (e) {
// Invalid URL format
}
if (!jellyfinId) {
document.getElementById('map-search-results').innerHTML = '<p style="text-align: center; color: var(--error); padding: 20px;">Could not extract track ID from URL. Make sure it contains "?id=..."</p>';
document.getElementById('map-selected-jellyfin-id').value = '';
document.getElementById('map-save-btn').disabled = true;
return;
}
// Fetch track details to show preview
document.getElementById('map-search-results').innerHTML = '<div class="loading"><span class="spinner"></span> Loading track details...</div>';
try {
const res = await fetch('/api/admin/jellyfin/track/' + jellyfinId);
const track = await res.json();
if (res.ok && track.id) {
document.getElementById('map-selected-jellyfin-id').value = track.id;
document.getElementById('map-save-btn').disabled = false;
document.getElementById('map-search-results').innerHTML = `
<div class="track-item" style="border: 2px solid var(--accent); background: var(--bg-tertiary);">
<div class="track-info">
<h4>${escapeHtml(track.title)}</h4>
<span class="artists">${escapeHtml(track.artist)}</span>
</div>
<div class="track-meta">
${track.album ? escapeHtml(track.album) : ''}
</div>
</div>
<p style="text-align: center; color: var(--success); padding: 12px; margin-top: 8px;">
✓ Track loaded from URL. Click "Save Mapping" to confirm.
</p>
`;
} else {
document.getElementById('map-search-results').innerHTML = '<p style="text-align: center; color: var(--error); padding: 20px;">Track not found in Jellyfin</p>';
document.getElementById('map-selected-jellyfin-id').value = '';
document.getElementById('map-save-btn').disabled = true;
}
} catch (error) {
document.getElementById('map-search-results').innerHTML = '<p style="text-align: center; color: var(--error); padding: 20px;">Failed to load track details</p>';
document.getElementById('map-selected-jellyfin-id').value = '';
document.getElementById('map-save-btn').disabled = true;
}
}
function selectJellyfinTrack(jellyfinId, element) {
// Remove selection from all tracks
document.querySelectorAll('#map-search-results .track-item').forEach(el => {