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)
This commit is contained in:
parent
20c1828cde
commit
51a433a558
3 changed files with 86 additions and 19 deletions
|
|
@ -4706,18 +4706,26 @@ class MusicDatabase:
|
||||||
logger.error(f"Error getting tracks for album {album_id}: {e}")
|
logger.error(f"Error getting tracks for album {album_id}: {e}")
|
||||||
return []
|
return []
|
||||||
|
|
||||||
def search_artists(self, query: str, limit: int = 50) -> List[DatabaseArtist]:
|
def search_artists(self, query: str, limit: int = 50, server_source: str = None) -> List[DatabaseArtist]:
|
||||||
"""Search artists by name"""
|
"""Search artists by name, optionally filtered by server source."""
|
||||||
try:
|
try:
|
||||||
conn = self._get_connection()
|
conn = self._get_connection()
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
|
|
||||||
cursor.execute("""
|
if server_source:
|
||||||
SELECT * FROM artists
|
cursor.execute("""
|
||||||
WHERE name LIKE ?
|
SELECT * FROM artists
|
||||||
ORDER BY name
|
WHERE name LIKE ? AND server_source = ?
|
||||||
LIMIT ?
|
ORDER BY name
|
||||||
""", (f"%{query}%", limit))
|
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()
|
rows = cursor.fetchall()
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7658,7 +7658,8 @@ def enhanced_search():
|
||||||
try:
|
try:
|
||||||
# Search local database for artists (always)
|
# Search local database for artists (always)
|
||||||
database = get_database()
|
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 = []
|
db_artists = []
|
||||||
for artist in db_artists_objs:
|
for artist in db_artists_objs:
|
||||||
|
|
@ -14217,10 +14218,37 @@ def sync_artist_library(artist_id):
|
||||||
""", (db_artist_id,))
|
""", (db_artist_id,))
|
||||||
tracks = cursor.fetchall()
|
tracks = cursor.fetchall()
|
||||||
|
|
||||||
# Get artist name for logging
|
# Get current artist info
|
||||||
cursor.execute("SELECT name FROM artists WHERE id = ?", (db_artist_id,))
|
cursor.execute("SELECT name, server_source FROM artists WHERE id = ?", (db_artist_id,))
|
||||||
artist_row = cursor.fetchone()
|
artist_row = cursor.fetchone()
|
||||||
artist_name = artist_row['name'] if artist_row else f'ID {db_artist_id}'
|
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 = []
|
stale_tracks = []
|
||||||
valid_tracks = 0
|
valid_tracks = 0
|
||||||
|
|
@ -14264,6 +14292,7 @@ def sync_artist_library(artist_id):
|
||||||
return jsonify({
|
return jsonify({
|
||||||
"success": True,
|
"success": True,
|
||||||
"artist_name": artist_name,
|
"artist_name": artist_name,
|
||||||
|
"name_updated": name_updated,
|
||||||
"valid_tracks": valid_tracks,
|
"valid_tracks": valid_tracks,
|
||||||
"stale_removed": len(stale_tracks),
|
"stale_removed": len(stale_tracks),
|
||||||
"empty_albums_removed": empty_albums_removed
|
"empty_albums_removed": empty_albums_removed
|
||||||
|
|
|
||||||
|
|
@ -17317,7 +17317,7 @@ function _gsRender(data) {
|
||||||
h += tracks.map(t => {
|
h += tracks.map(t => {
|
||||||
const ar = t.artist || (t.artists ? t.artists.join(', ') : '');
|
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')}` : '';
|
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('');
|
}).join('');
|
||||||
h += '</div>';
|
h += '</div>';
|
||||||
} else if (loading.has('tracks')) {
|
} 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();
|
_gsDeactivate();
|
||||||
navigateToPage('downloads');
|
|
||||||
setTimeout(() => {
|
// Build enriched track + open download modal directly (same as enhanced search)
|
||||||
const input = document.getElementById('enhanced-search-input');
|
const virtualPlaylistId = `gsearch_track_${trackId || (artistName + '_' + trackName).replace(/\s/g, '_')}`;
|
||||||
if (input) { input.value = `${artistName} ${trackName}`.trim(); input.dispatchEvent(new Event('input')); }
|
const enrichedTrack = {
|
||||||
}, 300);
|
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) {
|
async function _gsPlayTrack(trackName, artistName, albumName) {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue