Fix duplicate artists in search, per-artist name refresh, global search track click

- search_artists() now filters by active media server — no more duplicate
  results from Plex/Jellyfin/Navidrome showing the same artist 3 times
- Per-artist Sync button re-fetches artist name from media server, catches
  renames (e.g., Plex changing "Kendrick Lamar" to "eastside k-boy")
- Global search track click opens download modal directly instead of
  navigating to enhanced search page (matches enhanced search behavior)
pull/253/head
Broque Thomas 1 month ago
parent 20c1828cde
commit 51a433a558

@ -4706,18 +4706,26 @@ class MusicDatabase:
logger.error(f"Error getting tracks for album {album_id}: {e}")
return []
def search_artists(self, query: str, limit: int = 50) -> List[DatabaseArtist]:
"""Search artists by name"""
def search_artists(self, query: str, limit: int = 50, server_source: str = None) -> List[DatabaseArtist]:
"""Search artists by name, optionally filtered by server source."""
try:
conn = self._get_connection()
cursor = conn.cursor()
cursor.execute("""
SELECT * FROM artists
WHERE name LIKE ?
ORDER BY name
LIMIT ?
""", (f"%{query}%", limit))
if server_source:
cursor.execute("""
SELECT * FROM artists
WHERE name LIKE ? AND server_source = ?
ORDER BY name
LIMIT ?
""", (f"%{query}%", server_source, limit))
else:
cursor.execute("""
SELECT * FROM artists
WHERE name LIKE ?
ORDER BY name
LIMIT ?
""", (f"%{query}%", limit))
rows = cursor.fetchall()

@ -7658,7 +7658,8 @@ def enhanced_search():
try:
# Search local database for artists (always)
database = get_database()
db_artists_objs = database.search_artists(query, limit=5)
active_server = config_manager.get_active_media_server()
db_artists_objs = database.search_artists(query, limit=5, server_source=active_server)
db_artists = []
for artist in db_artists_objs:
@ -14217,10 +14218,37 @@ def sync_artist_library(artist_id):
""", (db_artist_id,))
tracks = cursor.fetchall()
# Get artist name for logging
cursor.execute("SELECT name FROM artists WHERE id = ?", (db_artist_id,))
# Get current artist info
cursor.execute("SELECT name, server_source FROM artists WHERE id = ?", (db_artist_id,))
artist_row = cursor.fetchone()
artist_name = artist_row['name'] if artist_row else f'ID {db_artist_id}'
server_source = artist_row['server_source'] if artist_row else None
# Re-fetch artist name from media server (catches renames in Plex/Jellyfin/Navidrome)
name_updated = False
if server_source:
try:
server_artist = None
if server_source == 'plex' and plex_client and plex_client.server:
try:
server_artist = plex_client.server.fetchItem(int(db_artist_id))
except Exception:
pass
elif server_source in ('jellyfin', 'navidrome'):
media_client = {'jellyfin': jellyfin_client, 'navidrome': navidrome_client}.get(server_source)
if media_client and hasattr(media_client, 'get_artist_by_id'):
server_artist = media_client.get_artist_by_id(str(db_artist_id))
if server_artist and hasattr(server_artist, 'title') and server_artist.title:
new_name = server_artist.title
if new_name != artist_name:
cursor.execute("UPDATE artists SET name = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?",
(new_name, db_artist_id))
print(f"🔄 [Artist Sync] Name updated: '{artist_name}''{new_name}'")
artist_name = new_name
name_updated = True
except Exception as e:
print(f"⚠️ [Artist Sync] Could not refresh name from {server_source}: {e}")
stale_tracks = []
valid_tracks = 0
@ -14264,6 +14292,7 @@ def sync_artist_library(artist_id):
return jsonify({
"success": True,
"artist_name": artist_name,
"name_updated": name_updated,
"valid_tracks": valid_tracks,
"stale_removed": len(stale_tracks),
"empty_albums_removed": empty_albums_removed

@ -17317,7 +17317,7 @@ function _gsRender(data) {
h += tracks.map(t => {
const ar = t.artist || (t.artists ? t.artists.join(', ') : '');
const dur = t.duration_ms ? `${Math.floor(t.duration_ms / 60000)}:${String(Math.floor((t.duration_ms % 60000) / 1000)).padStart(2, '0')}` : '';
return `<div class="gsearch-track" onclick="_gsClickTrack('${_escToast(ar).replace(/'/g, "\\'")}', '${_escToast(t.name).replace(/'/g, "\\'")}')"><div class="gsearch-item-art" style="width:32px;height:32px;border-radius:6px">${t.image_url ? `<img src="${t.image_url}" loading="lazy">` : '🎵'}</div><div class="gsearch-item-info"><div class="gsearch-item-title">${_escToast(t.name)}</div><div class="gsearch-item-sub">${_escToast(ar)}${t.album ? ` · ${_escToast(t.album)}` : ''}</div></div><div class="gsearch-track-dur">${dur}</div><button class="gsearch-play-btn" onclick="event.stopPropagation(); _gsPlayTrack('${_escToast(t.name).replace(/'/g, "\\'")}', '${_escToast(ar).replace(/'/g, "\\'")}', '${_escToast(t.album || '').replace(/'/g, "\\'")}')" title="Stream">▶</button></div>`;
return `<div class="gsearch-track" onclick="_gsClickTrack('${_escToast(ar).replace(/'/g, "\\'")}', '${_escToast(t.name).replace(/'/g, "\\'")}', '${_escToast(t.album || '').replace(/'/g, "\\'")}', '${_escToast(t.id || '').replace(/'/g, "\\'")}', '${_escToast(t.image_url || '').replace(/'/g, "\\'")}', ${t.duration_ms || 0})"><div class="gsearch-item-art" style="width:32px;height:32px;border-radius:6px">${t.image_url ? `<img src="${t.image_url}" loading="lazy">` : '🎵'}</div><div class="gsearch-item-info"><div class="gsearch-item-title">${_escToast(t.name)}</div><div class="gsearch-item-sub">${_escToast(ar)}${t.album ? ` · ${_escToast(t.album)}` : ''}</div></div><div class="gsearch-track-dur">${dur}</div><button class="gsearch-play-btn" onclick="event.stopPropagation(); _gsPlayTrack('${_escToast(t.name).replace(/'/g, "\\'")}', '${_escToast(ar).replace(/'/g, "\\'")}', '${_escToast(t.album || '').replace(/'/g, "\\'")}')" title="Stream">▶</button></div>`;
}).join('');
h += '</div>';
} else if (loading.has('tracks')) {
@ -17428,13 +17428,43 @@ async function _gsClickAlbum(albumId, albumName, artistName, imageUrl, source) {
}
}
function _gsClickTrack(artistName, trackName) {
async function _gsClickTrack(artistName, trackName, albumName, trackId, imageUrl, durationMs) {
_gsDeactivate();
navigateToPage('downloads');
setTimeout(() => {
const input = document.getElementById('enhanced-search-input');
if (input) { input.value = `${artistName} ${trackName}`.trim(); input.dispatchEvent(new Event('input')); }
}, 300);
// Build enriched track + open download modal directly (same as enhanced search)
const virtualPlaylistId = `gsearch_track_${trackId || (artistName + '_' + trackName).replace(/\s/g, '_')}`;
const enrichedTrack = {
id: trackId || '',
name: trackName,
artists: [artistName],
album: { name: albumName || '', id: null, album_type: 'single', images: imageUrl ? [{ url: imageUrl }] : [], total_tracks: 1 },
duration_ms: durationMs || 0,
image_url: imageUrl || '',
};
const albumObject = {
name: albumName || '', id: null, album_type: 'single',
images: imageUrl ? [{ url: imageUrl }] : [],
artists: [{ name: artistName }], total_tracks: 1,
};
const artistObject = { id: null, name: artistName };
const playlistName = `${artistName} - ${trackName}`;
try {
showLoadingOverlay('Loading track...');
await openDownloadMissingModalForArtistAlbum(
virtualPlaylistId, playlistName, [enrichedTrack], albumObject, artistObject, false
);
} catch (e) {
console.error('Error opening track download:', e);
// Fallback: navigate to enhanced search
navigateToPage('downloads');
setTimeout(() => {
const input = document.getElementById('enhanced-search-input');
if (input) { input.value = `${artistName} ${trackName}`.trim(); input.dispatchEvent(new Event('input')); }
}, 300);
} finally {
hideLoadingOverlay();
}
}
async function _gsPlayTrack(trackName, artistName, albumName) {

Loading…
Cancel
Save