Add multi-artist tagging options: separator, multi-value tags, feat-in-title

Three new settings in Paths & Organization:
- Artist Tag Separator: choose comma, semicolon, or slash between artists
- Write multi-value ARTISTS tag: each artist as separate tag value for
  Navidrome/Jellyfin multi-artist linking (FLAC ARTISTS key, ID3 TPE1
  multi-value, MP4 multi-entry)
- Move featured artists to title: keep only primary artist in ARTIST
  tag, append others as (feat. ...) in track title

All opt-in with defaults matching current behavior. Raw artist list
stored on metadata dict for tag writers to access without re-parsing.
pull/344/head
Broque Thomas 4 weeks ago
parent fd014e2745
commit e39a3f2af7

@ -18827,12 +18827,18 @@ def _enhance_file_metadata(file_path: str, context: dict, artist: dict, album_in
# ── Write standard tags using format-specific API ──
track_num_str = f"{metadata.get('track_number', 1)}/{metadata.get('total_tracks', 1)}"
_write_multi = config_manager.get('metadata_enhancement.tags.write_multi_artist', False)
_artists_list = metadata.get('_artists_list', [])
if isinstance(audio_file.tags, ID3):
# MP3: write ID3 frames directly
if metadata.get('title'):
audio_file.tags.add(TIT2(encoding=3, text=[metadata['title']]))
if metadata.get('artist'):
audio_file.tags.add(TPE1(encoding=3, text=[metadata['artist']]))
# Multi-value: write each artist as separate TPE1 text value
if _write_multi and len(_artists_list) > 1:
audio_file.tags.add(TPE1(encoding=3, text=_artists_list))
if metadata.get('album_artist'):
audio_file.tags.add(TPE2(encoding=3, text=[metadata['album_artist']]))
if metadata.get('album'):
@ -18851,6 +18857,9 @@ def _enhance_file_metadata(file_path: str, context: dict, artist: dict, album_in
audio_file['title'] = [metadata['title']]
if metadata.get('artist'):
audio_file['artist'] = [metadata['artist']]
# Multi-value: write ARTISTS tag with individual values
if _write_multi and len(_artists_list) > 1:
audio_file['artists'] = _artists_list
if metadata.get('album_artist'):
audio_file['albumartist'] = [metadata['album_artist']]
if metadata.get('album'):
@ -18868,7 +18877,11 @@ def _enhance_file_metadata(file_path: str, context: dict, artist: dict, album_in
if metadata.get('title'):
audio_file['\xa9nam'] = [metadata['title']]
if metadata.get('artist'):
audio_file['\xa9ART'] = [metadata['artist']]
# Multi-value: write each artist as separate list entry
if _write_multi and len(_artists_list) > 1:
audio_file['\xa9ART'] = _artists_list
else:
audio_file['\xa9ART'] = [metadata['artist']]
if metadata.get('album_artist'):
audio_file['aART'] = [metadata['album_artist']]
if metadata.get('album'):
@ -19011,7 +19024,6 @@ def _extract_spotify_metadata(context: dict, artist: dict, album_info: dict) ->
# Handle multiple artists from Spotify data
original_search = context.get("original_search_result", {})
if 'artists' in original_search and isinstance(original_search['artists'], list) and len(original_search['artists']) > 0:
# Join all artists with semicolon separator (standard format)
all_artists = []
for a in original_search['artists']:
if isinstance(a, dict) and 'name' in a:
@ -19020,11 +19032,28 @@ 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)
# Configurable artist separator (default: comma-space)
_artist_sep = config_manager.get('metadata_enhancement.tags.artist_separator', ', ') or ', '
_feat_in_title = config_manager.get('metadata_enhancement.tags.feat_in_title', False)
# Featured artist in title mode: keep only primary artist, append rest to title
if _feat_in_title and len(all_artists) > 1:
metadata['artist'] = all_artists[0]
_feat_str = ', '.join(all_artists[1:])
_title = metadata.get('title', '')
if _title and not re.search(r'\b(feat\.?|ft\.?|featuring)\b', _title, re.IGNORECASE):
metadata['title'] = f"{_title} (feat. {_feat_str})"
else:
metadata['artist'] = _artist_sep.join(all_artists)
# Store raw artist list for multi-value tag writing
metadata['_artists_list'] = all_artists
print(f"Metadata: Using all artists: '{metadata['artist']}'")
else:
# Fallback to single artist
metadata['artist'] = artist.get('name', '')
metadata['_artists_list'] = [metadata['artist']] if metadata['artist'] else []
print(f"Metadata: Using primary artist: '{metadata['artist']}'")
# Resolve album_artist for consistent tagging across all tracks in an album.

@ -5112,6 +5112,30 @@
Full artist list is always preserved in file metadata tags.</small>
</div>
<div class="form-group">
<label>Artist Tag Separator:</label>
<select id="artist-separator" class="form-select">
<option value=", ">, (comma)</option>
<option value="; ">; (semicolon)</option>
<option value=" / ">/ (slash)</option>
</select>
<small class="settings-hint">Separator between multiple artists in the ARTIST tag</small>
</div>
<div class="form-group">
<label class="checkbox-label">
<input type="checkbox" id="write-multi-artist">
<span>Write multi-value ARTISTS tag</span>
</label>
<small class="settings-hint">Write each artist as a separate tag value for Navidrome/Jellyfin multi-artist support</small>
</div>
<div class="form-group">
<label class="checkbox-label">
<input type="checkbox" id="feat-in-title">
<span>Move featured artists to title</span>
</label>
<small class="settings-hint">Keep only primary artist in ARTIST tag, append others as (feat. ...) in title</small>
</div>
<div class="form-group">
<label class="checkbox-label">
<input type="checkbox" id="allow-duplicate-tracks" checked>

@ -6209,6 +6209,9 @@ async function loadSettingsData() {
document.getElementById('template-video-path').value = settings.file_organization?.templates?.video_path || '$artist/$title-video';
document.getElementById('disc-label').value = settings.file_organization?.disc_label || 'Disc';
document.getElementById('collab-artist-mode').value = settings.file_organization?.collab_artist_mode || 'first';
document.getElementById('artist-separator').value = settings.metadata_enhancement?.tags?.artist_separator || ', ';
document.getElementById('write-multi-artist').checked = settings.metadata_enhancement?.tags?.write_multi_artist || false;
document.getElementById('feat-in-title').checked = settings.metadata_enhancement?.tags?.feat_in_title || false;
document.getElementById('allow-duplicate-tracks').checked = settings.wishlist?.allow_duplicate_tracks !== false;
// Populate Playlist Sync settings
@ -7636,7 +7639,10 @@ async function saveSettings(quiet = false) {
lrclib_enabled: document.getElementById('lrclib-enabled').checked,
tags: {
quality_tag: _getTagConfig('metadata_enhancement.tags.quality_tag'),
genre_merge: _getTagConfig('metadata_enhancement.tags.genre_merge')
genre_merge: _getTagConfig('metadata_enhancement.tags.genre_merge'),
artist_separator: document.getElementById('artist-separator').value,
write_multi_artist: document.getElementById('write-multi-artist').checked,
feat_in_title: document.getElementById('feat-in-title').checked
}
},
musicbrainz: {

Loading…
Cancel
Save