Skip to content

Remove ItemKey::Unknown #521

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
Serial-ATA opened this issue May 7, 2025 · 3 comments
Open

Remove ItemKey::Unknown #521

Serial-ATA opened this issue May 7, 2025 · 3 comments
Labels
enhancement New feature or request

Comments

@Serial-ATA
Copy link
Owner

ItemKey::Unknown has served its purpose, and has had better alternatives for a couple years now. It's been a source of confusion numerous times, and its existence also requires extra verification logic on conversions. I'd like to see it gone.

Comment taken from #520

ItemKey::Unknown was kind of a hacky way to retain tags that couldn't be mapped to concrete ItemKeys. There have been alternatives for awhile now though, with SplitTag, MergeTag, and GlobalOptions::preserve_format_specific_items() (see #302 for more info on that).

I envision people taking their Tags, converting them to a concrete format (e.g. VorbisComments), and then adding their custom fields to that. I'd rather not have a way to generically add custom fields, since the mappings for the concrete ItemKey variants are known to be valid, but with Unknown, there's extra verification that needs to be done on conversions that could be skipped if I just get rid of it entirely.

You'd go from:

tag.insert_text(ItemKey::Unknown(String::from("FOO")), String::from("BAR"));

to:

let mut vorbis_comments: VorbisComments = tag.into();
vorbis_comments.insert(String::from("FOO"), String::from("BAR"));

ItemKey supports the keys that the vast majority of users will need, anything else would be niche and require a little extra effort working with the concrete formats.

@Serial-ATA Serial-ATA added the enhancement New feature or request label May 7, 2025
@Undead34
Copy link

Undead34 commented May 13, 2025

Hi! I understand the motivation behind simplifying the ItemKey enum and removing ItemKey::Unknown, but I'd like to offer a user perspective that may have been overlooked.

I'm currently building a music library and metadata API where users can scan, view, and edit tags generically across different formats (MP3, FLAC, M4A, etc.). For this use case, ItemKey::Unknown has been extremely valuable.

It allows me to support custom or non-standard tags like RATING, MOOD, BPM_LABEL, and others without needing to write backend-specific logic for each format. While these tags may seem niche from a spec point of view, they are actually very common in real-world scenarios — especially for advanced playback systems, smart playlisting, DJ software, and even consumer apps that want to store user preferences.

By removing ItemKey::Unknown, I would be forced to detect the tag backend (e.g., Vorbis, ID3v2, MP4) and write logic per format just to set or read a single custom field — which breaks the abstraction that Tag provides.

For example, I’ve encountered many high-quality FLAC files in the wild where fields like TRACKTOTAL or DISCTOTAL are not parsed via tag.track_total() or tag.disk_total() but are present as raw strings under those names. In my implementation, I handle this fallback like this:

metadata.set_total_tracks(tag.track_total().map(|n| n as u16).or_else(|| {
    tag.get_string(&ItemKey::Unknown("TRACKTOTAL".to_string()))
        .and_then(|s| s.trim().parse::<u16>().ok())
}));

metadata.set_total_discs(tag.disk_total().map(|n| n as u16).or_else(|| {
    tag.get_string(&ItemKey::Unknown("DISCTOTAL".to_string()))
        .and_then(|s| s.trim().parse::<u16>().ok())
}));

I completely understand if ItemKey::Unknown needs to be deprecated or discouraged for performance or consistency reasons. But completely removing it would break a lot of real, practical use cases that rely on flexible metadata editing.

Thanks for your work on Lofty — it’s a great library and I really appreciate the attention to correctness and long-term design!

@Serial-ATA
Copy link
Owner Author

@Undead34 Thanks for sharing your thoughts!

It allows me to support custom or non-standard tags like RATING, MOOD, BPM_LABEL [...] While these tags may seem niche from a spec point of view, they are actually very common in real-world scenarios

I'm always open to adding more ItemKey variants, feel free to suggest the ones that you have custom logic for (like BPM_LABEL, which I've haven't seen before).

And for the record, RATING and MOOD map to ItemKey::Popularimeter and ItemKey::Mood for Vorbis Comments. You can see the full mapping here:

gen_map!(
VORBIS_MAP;
"ALBUM" => AlbumTitle,
"DISCSUBTITLE" => SetSubtitle,
"GROUPING" => ContentGroup,
"TITLE" => TrackTitle,
"SUBTITLE" => TrackSubtitle,
"WORK" => Work,
"MOVEMENTNAME" => Movement,
"MOVEMENT" => MovementNumber,
"MOVEMENTTOTAL" => MovementTotal,
"ALBUMSORT" => AlbumTitleSortOrder,
"ALBUMARTISTSORT" => AlbumArtistSortOrder,
"TITLESORT" => TrackTitleSortOrder,
"ARTISTSORT" => TrackArtistSortOrder,
"ALBUMARTIST" => AlbumArtist,
"ARTIST" => TrackArtist,
"ARTISTS" => TrackArtists,
"ARRANGER" => Arranger,
"AUTHOR" | "WRITER" => Writer,
"COMPOSER" => Composer,
"CONDUCTOR" => Conductor,
"DIRECTOR" => Director,
"ENGINEER" => Engineer,
"LYRICIST" => Lyricist,
"DJMIXER" => MixDj,
"MIXER" => MixEngineer,
"PERFORMER" => Performer,
"PRODUCER" => Producer,
"PUBLISHER" => Publisher,
"LABEL" | "ORGANIZATION" => Label,
"REMIXER" | "MIXARTIST" => Remixer,
"DISCNUMBER" => DiscNumber,
"DISCTOTAL" | "TOTALDISCS" => DiscTotal,
"TRACKNUMBER" => TrackNumber,
"TRACKTOTAL" | "TOTALTRACKS" => TrackTotal,
"RATING" => Popularimeter,
"DATE" => RecordingDate,
"YEAR" => Year,
"ORIGINALDATE" | "ORIGINALYEAR" => OriginalReleaseDate,
"RELEASEDATE" => ReleaseDate,
"ISRC" => Isrc,
"BARCODE" => Barcode,
"CATALOGNUMBER" => CatalogNumber,
"COMPILATION" => FlagCompilation,
"MEDIA" => OriginalMediaType,
"ENCODEDBY" | "ENCODED-BY" | "ENCODED_BY" => EncodedBy,
"ENCODER" => EncoderSoftware,
"ENCODING" | "ENCODERSETTINGS" => EncoderSettings,
"REPLAYGAIN_ALBUM_GAIN" => ReplayGainAlbumGain,
"REPLAYGAIN_ALBUM_PEAK" => ReplayGainAlbumPeak,
"REPLAYGAIN_TRACK_GAIN" => ReplayGainTrackGain,
"REPLAYGAIN_TRACK_PEAK" => ReplayGainTrackPeak,
"GENRE" => Genre,
"COLOR" => Color,
"MOOD" => Mood,
"BPM" => Bpm,
// MusicBrainz Picard suggests "KEY" (VirtualDJ, Denon Engine DJ), but "INITIALKEY"
// seems to be more common (Rekordbox, Serato DJ, Traktor DJ, Mixxx).
// <https://picard-docs.musicbrainz.org/en/appendices/tag_mapping.html#initial-key>
// <https://github.com/beetbox/beets/issues/637#issuecomment-39528023>
"INITIALKEY" | "KEY" => InitialKey,
"COPYRIGHT" => CopyrightMessage,
"LICENSE" => License,
"COMMENT" => Comment,
"LANGUAGE" => Language,
"SCRIPT" => Script,
"LYRICS" => Lyrics,
"MUSICBRAINZ_TRACKID" => MusicBrainzRecordingId,
"MUSICBRAINZ_RELEASETRACKID" => MusicBrainzTrackId,
"MUSICBRAINZ_ALBUMID" => MusicBrainzReleaseId,
"MUSICBRAINZ_RELEASEGROUPID" => MusicBrainzReleaseGroupId,
"MUSICBRAINZ_ARTISTID" => MusicBrainzArtistId,
"MUSICBRAINZ_ALBUMARTISTID" => MusicBrainzReleaseArtistId,
"MUSICBRAINZ_WORKID" => MusicBrainzWorkId
);

For example, I’ve encountered many high-quality FLAC files in the wild where fields like TRACKTOTAL or DISCTOTAL are not parsed via tag.track_total() or tag.disk_total()

You should be able to do:

metadata.set_total_tracks(tag.track_total().map(|n| n as u16).or_else(|| {
    tag.get_string(&ItemKey::TrackTotal)
        .and_then(|s| s.trim().parse::<u16>().ok())
}));

metadata.set_total_discs(tag.disk_total().map(|n| n as u16).or_else(|| {
    tag.get_string(&ItemKey::DiscTotal)
        .and_then(|s| s.trim().parse::<u16>().ok())
}));

ItemKey::TrackTotal and ItemKey::DiscTotal should map to the expected keys:

"DISCTOTAL" | "TOTALDISCS" => DiscTotal,
"TRACKNUMBER" => TrackNumber,
"TRACKTOTAL" | "TOTALTRACKS" => TrackTotal,

Does that not work?

@Undead34
Copy link

Thanks for the clarification and for maintaining such a great library! 🙏 I just tested using ItemKey::TrackTotal and ItemKey::DiscTotal as you suggested and it works perfectly—my FLAC files now parse both fields without needing Unknown. I hadn’t tried it that way originally, so apologies for the extra noise!

When I have a free moment, I’d be happy to contribute a small docs PR with these examples to help others avoid the same confusion. Thanks again! 😊

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

No branches or pull requests

2 participants