feat(downloads): allow writing multiple artists to metadata

This will write each artist separately to the metadata rather than as a single concatenated string.  This allows for better library searching if the player supports it.

If multiple artists are written to an m4a file, iTunes will only show the first artist.
This commit is contained in:
Daniel 2026-04-02 16:23:49 -05:00 committed by edideaur
parent 51e5e1973f
commit 5b727a103e
7 changed files with 66 additions and 10 deletions

View file

@ -4885,6 +4885,20 @@
</label>
</div>
</div>
<div class="settings-group">
<div class="setting-item">
<div class="info">
<span class="label">Write Artists Separately</span>
<span class="description"
>Write artists separately to metadata. Requires player support.</span
>
</div>
<label class="toggle-switch">
<input type="checkbox" id="write-artists-separately-toggle" />
<span class="slider"></span>
</label>
</div>
</div>
<div class="settings-group">
<div class="setting-item">
<div class="info">

View file

@ -261,6 +261,7 @@ export const modernSettings = new ModernSettings()
transformer: String,
},
})
.addProperty('writeArtistsSeparately', false)
.finalize() as ModernSettings & {
/** The last used directory handle for bulk downloads */
bulkDownloadFolder: FileSystemDirectoryHandle | null;
@ -286,4 +287,7 @@ export const modernSettings = new ModernSettings()
/** Filename template for downloads */
filenameTemplate: string;
/** Whether to write multiple artists to downloaded files */
writeArtistsSeparately: boolean;
};

View file

@ -1,7 +1,15 @@
import { getCoverBlob, getTrackTitle, getFullArtistString, getMimeType, getTrackCoverId } from './utils.js';
import {
getCoverBlob,
getTrackTitle,
getFullArtistString,
getMimeType,
getTrackCoverId,
getFullArtistArray,
} from './utils.js';
import { addMetadataWithTagLib, getMetadataWithTagLib } from './taglib.ts';
import { LyricsManager } from './lyrics.js';
import { Mp4Stik } from './taglib.types.ts';
import { modernSettings } from './ModernSettings.js';
/**
* @typedef {import('./container-classes.ts').Track} Track
@ -35,13 +43,15 @@ export async function addMetadataToAudio(audioBlob, track, api, _quality, prefet
/**
* @type {TagLibMetadata}
*/
const data = {};
const data = {
writeArtistsSeparately: modernSettings.writeArtistsSeparately,
};
try {
data.title = getTrackTitle(track);
data.artist = getFullArtistString(track);
data.artist = getFullArtistArray(track);
data.albumTitle = track.album?.title;
data.albumArtist = track.album?.artist?.name || track.artist?.name;
data.albumArtist = track.album?.artist?.name || getFullArtistString(track) || '';
data.trackNumber = track.trackNumber;
data.discNumber = track.volumeNumber ?? track.discNumber;
data.totalTracks = track.album?.numberOfTracksOnDisc ?? track.album?.numberOfTracks;

View file

@ -4568,6 +4568,15 @@ export async function initializeSettings(scrobbler, player, api, ui) {
});
}
// Write multiple artists toggle
const writeArtistsSeparatelyToggle = document.getElementById('write-artists-separately-toggle');
if (writeArtistsSeparatelyToggle) {
writeArtistsSeparatelyToggle.checked = modernSettings.writeArtistsSeparately;
writeArtistsSeparatelyToggle.addEventListener('change', (e) => {
modernSettings.writeArtistsSeparately = e.target.checked;
});
}
// Download Lyrics Toggle
const downloadLyricsToggle = document.getElementById('download-lyrics-toggle');
if (downloadLyricsToggle) {

View file

@ -16,7 +16,8 @@ export interface TagLibWorkerResponse<T> {
export interface TagLibMetadata {
title?: string;
artist?: string;
artist?: string | string[];
writeArtistsSeparately?: boolean;
albumTitle?: string;
albumArtist?: string;
trackNumber?: number;

View file

@ -29,8 +29,8 @@ import { FileSystemFileHandleStream } from '!/@dantheman827/taglib-ts/src/toolki
import { FlacFile } from '!/@dantheman827/taglib-ts/src/flac/flacFile.js';
import { MpegFile } from '!/@dantheman827/taglib-ts/src/mpeg/mpegFile.js';
import { Mp4File } from '!/@dantheman827/taglib-ts/src/mp4/mp4File.js';
import { OggFile } from '!/@dantheman827/taglib-ts/src/ogg/oggFile.js';
import { OggVorbisFile } from '!/@dantheman827/taglib-ts/src/ogg/vorbis/vorbisFile.js';
import { WavFile } from '!/@dantheman827/taglib-ts/src/riff/wav/wavFile';
export const isWorker = typeof WorkerGlobalScope !== 'undefined' && self instanceof WorkerGlobalScope;
@ -41,6 +41,7 @@ export async function addMetadataToAudio(message: _AddMetadataMessage): Promise<
filename,
title,
artist,
writeArtistsSeparately = false,
albumTitle,
albumArtist,
trackNumber,
@ -74,17 +75,24 @@ export async function addMetadataToAudio(message: _AddMetadataMessage): Promise<
}
const underlying = ref.file();
const isFlac = underlying instanceof FlacFile;
const isMp4 = underlying instanceof Mp4File;
const isMpeg = underlying instanceof MpegFile;
const isOgg = underlying instanceof OggVorbisFile;
const isWav = underlying instanceof WavFile;
const needsCombinedTrackDisc = isMp4 || isMpeg;
const artistArray = Array.isArray(artist) ? artist : artist ? [artist] : [];
const supportsMultiValuedArtist = writeArtistsSeparately && (isFlac || isOgg || isMp4);
doTimed('Tagging file', () => {
const props = ref.properties();
if (title) props.replace('TITLE', [title]);
if (artist) props.replace('ARTIST', [artist]);
if (artistArray.length) props.replace('ARTIST', supportsMultiValuedArtist ? artistArray : [artistArray.join('; ')]);
if (albumTitle) props.replace('ALBUM', [albumTitle]);
if (albumArtist || artist) props.replace('ALBUMARTIST', [albumArtist || artist!]);
if (albumArtist || artistArray.length) props.replace('ALBUMARTIST', albumArtist ? [albumArtist] : [artistArray.join('; ')]);
if (trackNumber) {
const trackStr =

View file

@ -616,10 +616,10 @@ export const getShareUrl = (path) => {
};
/**
* Builds a full artist string by combining the track's listed artists
* Builds a full artist array by combining the track's listed artists
* with any featured artists parsed from the title (feat./with).
*/
export function getFullArtistString(track) {
export function getFullArtistArray(track) {
const knownArtists =
Array.isArray(track.artists) && track.artists.length > 0
? track.artists.map((a) => (typeof a === 'string' ? a : a.name) || '').filter(Boolean)
@ -646,6 +646,16 @@ export function getFullArtistString(track) {
}
}
return knownArtists;
}
/**
* Builds a full artist string by combining the track's listed artists
* with any featured artists parsed from the title (feat./with).
*/
export function getFullArtistString(track) {
const knownArtists = getFullArtistArray(track);
return knownArtists.join('; ') || null;
}