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.
This commit is contained in:
parent
fd014e2745
commit
e39a3f2af7
3 changed files with 63 additions and 4 deletions
|
|
@ -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…
Reference in a new issue