diff --git a/core/hifi_client.py b/core/hifi_client.py index 4eafa51f..04b74e45 100644 --- a/core/hifi_client.py +++ b/core/hifi_client.py @@ -303,7 +303,8 @@ class HiFiClient: names.append(a.get('name', '')) elif isinstance(a, str): names.append(a) - artist_name = ', '.join(n for n in names if n) or 'Unknown Artist' + # Use "/" as separator for multi-artist tracks (ID3v2 standard, recognized by Plex) + artist_name = '/'.join(n for n in names if n) or 'Unknown Artist' elif isinstance(artists_raw, dict): artist_name = artists_raw.get('name', 'Unknown Artist') elif isinstance(artists_raw, str): diff --git a/core/personalized_playlists.py b/core/personalized_playlists.py index b4c5aed7..4fbfb1cb 100644 --- a/core/personalized_playlists.py +++ b/core/personalized_playlists.py @@ -991,7 +991,7 @@ class PersonalizedPlaylistsService: all_tracks.append({ 'spotify_track_id': track['id'], 'track_name': track['name'], - 'artist_name': ', '.join([a['name'] for a in track.get('artists', [])]), + 'artist_name': '/'.join([a['name'] for a in track.get('artists', [])]), 'album_name': album_data.get('name', 'Unknown'), 'album_cover_url': album_data.get('images', [{}])[0].get('url') if album_data.get('images') else None, 'duration_ms': track.get('duration_ms', 0), @@ -1029,7 +1029,7 @@ class PersonalizedPlaylistsService: all_tracks.append({ 'spotify_track_id': track_id, 'track_name': track.get('name', ''), - 'artist_name': ', '.join(artist_names) if artist_names else 'Unknown', + 'artist_name': '/'.join(artist_names) if artist_names else 'Unknown', 'album_name': album_name, 'album_cover_url': album_cover, 'duration_ms': track.get('duration_ms', 0), diff --git a/core/seasonal_discovery.py b/core/seasonal_discovery.py index 6431218c..fb9485a1 100644 --- a/core/seasonal_discovery.py +++ b/core/seasonal_discovery.py @@ -537,7 +537,7 @@ class SeasonalDiscoveryService: seasonal_albums.append({ 'spotify_album_id': album.id, 'album_name': album.name, - 'artist_name': ', '.join(album.artists) if album.artists else 'Various Artists', + 'artist_name': '/'.join(album.artists) if album.artists else 'Various Artists', 'album_cover_url': album.image_url if hasattr(album, 'image_url') else None, 'release_date': album.release_date if hasattr(album, 'release_date') else None, 'popularity': getattr(album, 'popularity', 50) @@ -567,7 +567,7 @@ class SeasonalDiscoveryService: seasonal_albums.append({ 'spotify_album_id': album.id, # Column name is spotify_album_id but stores iTunes ID too 'album_name': album.name, - 'artist_name': ', '.join(album.artists) if album.artists else 'Various Artists', + 'artist_name': '/'.join(album.artists) if album.artists else 'Various Artists', 'album_cover_url': album.image_url if hasattr(album, 'image_url') else None, 'release_date': album.release_date if hasattr(album, 'release_date') else None, 'popularity': 50 # iTunes has no popularity — default mid-range diff --git a/core/tag_writer.py b/core/tag_writer.py index 09bf611a..02970942 100644 --- a/core/tag_writer.py +++ b/core/tag_writer.py @@ -305,7 +305,8 @@ def _write_id3(audio, title, artist, album_artist, album, year, genre, written.append('title') if artist: audio.tags.delall('TPE1') - audio.tags.add(TPE1(encoding=3, text=[artist])) + artists = _split_multi_artist(artist) + audio.tags.add(TPE1(encoding=3, text=artists)) written.append('artist') if album_artist: audio.tags.delall('TPE2') @@ -346,7 +347,8 @@ def _write_vorbis(audio, title, artist, album_artist, album, year, genre, audio['title'] = [title] written.append('title') if artist: - audio['artist'] = [artist] + artists = _split_multi_artist(artist) + audio['artist'] = artists written.append('artist') if album_artist: audio['albumartist'] = [album_artist] @@ -380,7 +382,8 @@ def _write_mp4(audio, title, artist, album_artist, album, year, genre, audio['\xa9nam'] = [title] written.append('title') if artist: - audio['\xa9ART'] = [artist] + artists = _split_multi_artist(artist) + audio['\xa9ART'] = artists written.append('artist') if album_artist: audio['aART'] = [album_artist] @@ -477,6 +480,19 @@ def _parse_track_num(val) -> Optional[int]: return None +def _split_multi_artist(artist_str: str) -> List[str]: + """Split a multi-artist string into individual artist names. + + Handles the '/' separator used by ID3v2.4 for multi-artist tracks. + Returns a list of individual artist names. + """ + if not artist_str: + return [] + if '/' in artist_str: + return [a.strip() for a in artist_str.split('/') if a.strip()] + return [artist_str] + + def _normalize_for_compare(val) -> str: """Normalize a value for comparison.""" if val is None: diff --git a/web_server.py b/web_server.py index 9f3eb2e3..2dce6a25 100644 --- a/web_server.py +++ b/web_server.py @@ -18035,8 +18035,11 @@ def _extract_spotify_metadata(context: dict, artist: dict, album_info: dict) -> all_artists.append(a) else: all_artists.append(str(a)) - metadata['artist'] = ', '.join(all_artists) - print(f"Metadata: Using all artists: '{metadata['artist']}'") +# Use "/" as separator for multi-artist tracks (ID3v2 standard, recognized by Plex) + # This allows Plex to properly identify individual artists instead of treating + # "Artist A, Artist B" as a single artist name + metadata['artist'] = '/'.join(all_artists) + print(f"🎵 Metadata: Using all artists: '{metadata['artist']}'") else: # Fallback to single artist metadata['artist'] = artist.get('name', '') @@ -33143,8 +33146,9 @@ def _extract_artist_names(artists): return [_extract_artist_name(a) for a in (artists or [])] def _join_artist_names(artists): - """Join artist names from a list that may contain dicts or strings.""" - return ', '.join(_extract_artist_names(artists)) + """Join artist names from a list that may contain dicts or strings. + Uses '/' as separator for multi-artist tracks (ID3v2 standard, recognized by Plex).""" + return '/'.join(_extract_artist_names(artists)) def _get_discovery_cache_key(title, artist): """Normalize title/artist for discovery cache lookup using matching_engine."""