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}")
|
||||
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…
Reference in a new issue