< Summary - Jellyfin

Information
Class: MediaBrowser.Providers.MediaInfo.AudioFileProber
Assembly: MediaBrowser.Providers
File(s): /srv/git/jellyfin/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs
Line coverage
26%
Covered lines: 11
Uncovered lines: 30
Coverable lines: 41
Total lines: 523
Line coverage: 26.8%
Branch coverage
0%
Covered branches: 0
Total branches: 16
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
AddExternalLyrics(...)0%2040%
SplitWithCustomDelimiter(...)0%4260%
GetFirstMusicBrainzId(...)0%620%
GetSanitizedStringTag(...)0%2040%
TryGetSanitizedAdditionalFields(...)100%210%

File(s)

/srv/git/jellyfin/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs

#LineLine coverage
 1using System;
 2using System.Collections.Generic;
 3using System.Globalization;
 4using System.Linq;
 5using System.Threading;
 6using System.Threading.Tasks;
 7using ATL;
 8using Jellyfin.Data.Enums;
 9using Jellyfin.Extensions;
 10using MediaBrowser.Controller.Entities;
 11using MediaBrowser.Controller.Entities.Audio;
 12using MediaBrowser.Controller.Library;
 13using MediaBrowser.Controller.Lyrics;
 14using MediaBrowser.Controller.MediaEncoding;
 15using MediaBrowser.Controller.Persistence;
 16using MediaBrowser.Controller.Providers;
 17using MediaBrowser.Model.Dlna;
 18using MediaBrowser.Model.Dto;
 19using MediaBrowser.Model.Entities;
 20using MediaBrowser.Model.Extensions;
 21using MediaBrowser.Model.MediaInfo;
 22using Microsoft.Extensions.Logging;
 23using static Jellyfin.Extensions.StringExtensions;
 24
 25namespace MediaBrowser.Providers.MediaInfo
 26{
 27    /// <summary>
 28    /// Probes audio files for metadata.
 29    /// </summary>
 30    public class AudioFileProber
 31    {
 32        private const char InternalValueSeparator = '\u001F';
 33
 34        private readonly IMediaEncoder _mediaEncoder;
 35        private readonly ILibraryManager _libraryManager;
 36        private readonly ILogger<AudioFileProber> _logger;
 37        private readonly IMediaSourceManager _mediaSourceManager;
 38        private readonly LyricResolver _lyricResolver;
 39        private readonly ILyricManager _lyricManager;
 40        private readonly IMediaStreamRepository _mediaStreamRepository;
 41
 42        /// <summary>
 43        /// Initializes a new instance of the <see cref="AudioFileProber"/> class.
 44        /// </summary>
 45        /// <param name="logger">Instance of the <see cref="ILogger"/> interface.</param>
 46        /// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param>
 47        /// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param>
 48        /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
 49        /// <param name="lyricResolver">Instance of the <see cref="LyricResolver"/> interface.</param>
 50        /// <param name="lyricManager">Instance of the <see cref="ILyricManager"/> interface.</param>
 51        /// <param name="mediaStreamRepository">Instance of the <see cref="IMediaStreamRepository"/>.</param>
 52        public AudioFileProber(
 53            ILogger<AudioFileProber> logger,
 54            IMediaSourceManager mediaSourceManager,
 55            IMediaEncoder mediaEncoder,
 56            ILibraryManager libraryManager,
 57            LyricResolver lyricResolver,
 58            ILyricManager lyricManager,
 59            IMediaStreamRepository mediaStreamRepository)
 60        {
 2161            _mediaEncoder = mediaEncoder;
 2162            _libraryManager = libraryManager;
 2163            _logger = logger;
 2164            _mediaSourceManager = mediaSourceManager;
 2165            _lyricResolver = lyricResolver;
 2166            _lyricManager = lyricManager;
 2167            _mediaStreamRepository = mediaStreamRepository;
 2168            ATL.Settings.DisplayValueSeparator = InternalValueSeparator;
 2169            ATL.Settings.UseFileNameWhenNoTitle = false;
 2170            ATL.Settings.ID3v2_separatev2v3Values = false;
 2171        }
 72
 73        /// <summary>
 74        /// Probes the specified item for metadata.
 75        /// </summary>
 76        /// <param name="item">The item to probe.</param>
 77        /// <param name="options">The <see cref="MetadataRefreshOptions"/>.</param>
 78        /// <param name="cancellationToken">The <see cref="CancellationToken"/>.</param>
 79        /// <typeparam name="T">The type of item to resolve.</typeparam>
 80        /// <returns>A <see cref="Task"/> probing the item for metadata.</returns>
 81        public async Task<ItemUpdateType> Probe<T>(
 82            T item,
 83            MetadataRefreshOptions options,
 84            CancellationToken cancellationToken)
 85            where T : Audio
 86        {
 87            var path = item.Path;
 88            var protocol = item.PathProtocol ?? MediaProtocol.File;
 89
 90            if (!item.IsShortcut || options.EnableRemoteContentProbe)
 91            {
 92                if (item.IsShortcut)
 93                {
 94                    path = item.ShortcutPath;
 95                    protocol = _mediaSourceManager.GetPathProtocol(path);
 96                }
 97
 98                var result = await _mediaEncoder.GetMediaInfo(
 99                    new MediaInfoRequest
 100                    {
 101                        MediaType = DlnaProfileType.Audio,
 102                        MediaSource = new MediaSourceInfo
 103                        {
 104                            Path = path,
 105                            Protocol = protocol
 106                        }
 107                    },
 108                    cancellationToken).ConfigureAwait(false);
 109
 110                cancellationToken.ThrowIfCancellationRequested();
 111
 112                await FetchAsync(item, result, options, cancellationToken).ConfigureAwait(false);
 113            }
 114
 115            return ItemUpdateType.MetadataImport;
 116        }
 117
 118        /// <summary>
 119        /// Fetches the specified audio.
 120        /// </summary>
 121        /// <param name="audio">The <see cref="Audio"/>.</param>
 122        /// <param name="mediaInfo">The <see cref="Model.MediaInfo.MediaInfo"/>.</param>
 123        /// <param name="options">The <see cref="MetadataRefreshOptions"/>.</param>
 124        /// <param name="cancellationToken">The <see cref="CancellationToken"/>.</param>
 125        /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
 126        private async Task FetchAsync(
 127            Audio audio,
 128            Model.MediaInfo.MediaInfo mediaInfo,
 129            MetadataRefreshOptions options,
 130            CancellationToken cancellationToken)
 131        {
 132            audio.Container = mediaInfo.Container;
 133            audio.TotalBitrate = mediaInfo.Bitrate;
 134
 135            audio.RunTimeTicks = mediaInfo.RunTimeTicks;
 136
 137            // Add external lyrics first to prevent the lrc file get overwritten on first scan
 138            var mediaStreams = new List<MediaStream>(mediaInfo.MediaStreams);
 139            AddExternalLyrics(audio, mediaStreams, options);
 140            var tryExtractEmbeddedLyrics = mediaStreams.All(s => s.Type != MediaStreamType.Lyric);
 141
 142            if (!audio.IsLocked)
 143            {
 144                await FetchDataFromTags(audio, mediaInfo, options, tryExtractEmbeddedLyrics).ConfigureAwait(false);
 145                if (tryExtractEmbeddedLyrics)
 146                {
 147                    AddExternalLyrics(audio, mediaStreams, options);
 148                }
 149            }
 150
 151            audio.HasLyrics = mediaStreams.Any(s => s.Type == MediaStreamType.Lyric);
 152
 153            _mediaStreamRepository.SaveMediaStreams(audio.Id, mediaStreams, cancellationToken);
 154        }
 155
 156        /// <summary>
 157        /// Fetches data from the tags.
 158        /// </summary>
 159        /// <param name="audio">The <see cref="Audio"/>.</param>
 160        /// <param name="mediaInfo">The <see cref="Model.MediaInfo.MediaInfo"/>.</param>
 161        /// <param name="options">The <see cref="MetadataRefreshOptions"/>.</param>
 162        /// <param name="tryExtractEmbeddedLyrics">Whether to extract embedded lyrics to lrc file. </param>
 163        private async Task FetchDataFromTags(Audio audio, Model.MediaInfo.MediaInfo mediaInfo, MetadataRefreshOptions op
 164        {
 165            var libraryOptions = _libraryManager.GetLibraryOptions(audio);
 166            Track track = new Track(audio.Path);
 167
 168            if (track.MetadataFormats
 169                .All(mf => string.Equals(mf.ShortName, "ID3v1", StringComparison.OrdinalIgnoreCase)))
 170            {
 171                _logger.LogWarning("File {File} only has ID3v1 tags, some fields may be truncated", audio.Path);
 172            }
 173
 174            // We should never use the property setter of the ATL.Track class.
 175            // That setter is meant for its own tag parser and external editor usage and will have unwanted side effects
 176            // For example, setting the Year property will also set the Date property, which is not what we want here.
 177            // To properly handle fallback values, we make a clone of those fields when valid.
 178            var trackTitle = (string.IsNullOrEmpty(track.Title) ? mediaInfo.Name : track.Title)?.Trim();
 179            var trackAlbum = (string.IsNullOrEmpty(track.Album) ? mediaInfo.Album : track.Album)?.Trim();
 180            var trackYear = track.Year is null or 0 ? mediaInfo.ProductionYear : track.Year;
 181            var trackTrackNumber = track.TrackNumber is null or 0 ? mediaInfo.IndexNumber : track.TrackNumber;
 182            var trackDiscNumber = track.DiscNumber is null or 0 ? mediaInfo.ParentIndexNumber : track.DiscNumber;
 183
 184            // Some users may use a misbehaved tag editor that writes a null character in the tag when not allowed by th
 185            trackTitle = GetSanitizedStringTag(trackTitle, audio.Path);
 186            trackAlbum = GetSanitizedStringTag(trackAlbum, audio.Path);
 187            var trackAlbumArtist = GetSanitizedStringTag(track.AlbumArtist, audio.Path);
 188            var trackArist = GetSanitizedStringTag(track.Artist, audio.Path);
 189            var trackComposer = GetSanitizedStringTag(track.Composer, audio.Path);
 190            var trackGenre = GetSanitizedStringTag(track.Genre, audio.Path);
 191
 192            if (audio.SupportsPeople && !audio.LockedFields.Contains(MetadataField.Cast))
 193            {
 194                var people = new List<PersonInfo>();
 195                var albumArtists = string.IsNullOrEmpty(trackAlbumArtist) ? [] : trackAlbumArtist.Split(InternalValueSep
 196
 197                if (libraryOptions.UseCustomTagDelimiters)
 198                {
 199                    albumArtists = albumArtists.SelectMany(a => SplitWithCustomDelimiter(a, libraryOptions.GetCustomTagD
 200                }
 201
 202                foreach (var albumArtist in albumArtists)
 203                {
 204                    if (!string.IsNullOrWhiteSpace(albumArtist))
 205                    {
 206                        PeopleHelper.AddPerson(people, new PersonInfo
 207                        {
 208                            Name = albumArtist.Trim(),
 209                            Type = PersonKind.AlbumArtist
 210                        });
 211                    }
 212                }
 213
 214                string[]? performers = null;
 215                if (libraryOptions.PreferNonstandardArtistsTag)
 216                {
 217                    TryGetSanitizedAdditionalFields(track, "ARTISTS", out var artistsTagString);
 218                    if (artistsTagString is not null)
 219                    {
 220                        performers = artistsTagString.Split(InternalValueSeparator);
 221                    }
 222                }
 223
 224                if (performers is null || performers.Length == 0)
 225                {
 226                    performers = string.IsNullOrEmpty(trackArist) ? [] : trackArist.Split(InternalValueSeparator);
 227                }
 228
 229                if (libraryOptions.UseCustomTagDelimiters)
 230                {
 231                    performers = performers.SelectMany(p => SplitWithCustomDelimiter(p, libraryOptions.GetCustomTagDelim
 232                }
 233
 234                foreach (var performer in performers)
 235                {
 236                    if (!string.IsNullOrWhiteSpace(performer))
 237                    {
 238                        PeopleHelper.AddPerson(people, new PersonInfo
 239                        {
 240                            Name = performer.Trim(),
 241                            Type = PersonKind.Artist
 242                        });
 243                    }
 244                }
 245
 246                if (!string.IsNullOrWhiteSpace(trackComposer))
 247                {
 248                    foreach (var composer in trackComposer.Split(InternalValueSeparator))
 249                    {
 250                        if (!string.IsNullOrWhiteSpace(composer))
 251                        {
 252                            PeopleHelper.AddPerson(people, new PersonInfo
 253                            {
 254                                Name = composer.Trim(),
 255                                Type = PersonKind.Composer
 256                            });
 257                        }
 258                    }
 259                }
 260
 261                _libraryManager.UpdatePeople(audio, people);
 262
 263                if (options.ReplaceAllMetadata && performers.Length != 0)
 264                {
 265                    audio.Artists = performers;
 266                }
 267                else if (!options.ReplaceAllMetadata
 268                         && (audio.Artists is null || audio.Artists.Count == 0))
 269                {
 270                    audio.Artists = performers;
 271                }
 272
 273                if (albumArtists.Length == 0)
 274                {
 275                    // Album artists not provided, fall back to performers (artists).
 276                    albumArtists = performers;
 277                }
 278
 279                if (options.ReplaceAllMetadata && albumArtists.Length != 0)
 280                {
 281                    audio.AlbumArtists = albumArtists;
 282                }
 283                else if (!options.ReplaceAllMetadata
 284                         && (audio.AlbumArtists is null || audio.AlbumArtists.Count == 0))
 285                {
 286                    audio.AlbumArtists = albumArtists;
 287                }
 288            }
 289
 290            if (!audio.LockedFields.Contains(MetadataField.Name) && !string.IsNullOrEmpty(trackTitle))
 291            {
 292                audio.Name = trackTitle;
 293            }
 294
 295            if (options.ReplaceAllMetadata)
 296            {
 297                audio.Album = trackAlbum;
 298                audio.IndexNumber = trackTrackNumber;
 299                audio.ParentIndexNumber = trackDiscNumber;
 300            }
 301            else
 302            {
 303                audio.Album ??= trackAlbum;
 304                audio.IndexNumber ??= trackTrackNumber;
 305                audio.ParentIndexNumber ??= trackDiscNumber;
 306            }
 307
 308            if (track.Date.HasValue)
 309            {
 310                audio.PremiereDate = track.Date;
 311            }
 312
 313            if (trackYear.HasValue)
 314            {
 315                var year = trackYear.Value;
 316                audio.ProductionYear = year;
 317
 318                // ATL library handles such fallback this with its own internal logic, but we also need to handle it her
 319                if (!audio.PremiereDate.HasValue)
 320                {
 321                    try
 322                    {
 323                        audio.PremiereDate = new DateTime(year, 01, 01);
 324                    }
 325                    catch (ArgumentOutOfRangeException ex)
 326                    {
 327                        _logger.LogError(ex, "Error parsing YEAR tag in {File}. '{TagValue}' is an invalid year", audio.
 328                    }
 329                }
 330            }
 331
 332            if (!audio.LockedFields.Contains(MetadataField.Genres))
 333            {
 334                var genres = string.IsNullOrEmpty(trackGenre) ? [] : trackGenre.Split(InternalValueSeparator).Distinct(S
 335
 336                if (libraryOptions.UseCustomTagDelimiters)
 337                {
 338                    genres = genres.SelectMany(g => SplitWithCustomDelimiter(g, libraryOptions.GetCustomTagDelimiters(),
 339                }
 340
 341                genres = genres.Trimmed().Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
 342
 343                audio.Genres = options.ReplaceAllMetadata || audio.Genres is null || audio.Genres.Length == 0
 344                    ? genres
 345                    : audio.Genres;
 346            }
 347
 348            TryGetSanitizedAdditionalFields(track, "REPLAYGAIN_TRACK_GAIN", out var trackGainTag);
 349
 350            if (trackGainTag is not null)
 351            {
 352                if (trackGainTag.EndsWith("db", StringComparison.OrdinalIgnoreCase))
 353                {
 354                    trackGainTag = trackGainTag[..^2].Trim();
 355                }
 356
 357                if (float.TryParse(trackGainTag, NumberStyles.Float, CultureInfo.InvariantCulture, out var value) && flo
 358                {
 359                    audio.NormalizationGain = value;
 360                }
 361            }
 362
 363            if (options.ReplaceAllMetadata || !audio.TryGetProviderId(MetadataProvider.MusicBrainzArtist, out _))
 364            {
 365                if ((TryGetSanitizedAdditionalFields(track, "MUSICBRAINZ_ARTISTID", out var musicBrainzArtistTag)
 366                     || TryGetSanitizedAdditionalFields(track, "MusicBrainz Artist Id", out musicBrainzArtistTag))
 367                    && !string.IsNullOrEmpty(musicBrainzArtistTag))
 368                {
 369                    var id = GetFirstMusicBrainzId(musicBrainzArtistTag, libraryOptions.UseCustomTagDelimiters, libraryO
 370                    audio.TrySetProviderId(MetadataProvider.MusicBrainzArtist, id);
 371                }
 372            }
 373
 374            if (options.ReplaceAllMetadata || !audio.TryGetProviderId(MetadataProvider.MusicBrainzAlbumArtist, out _))
 375            {
 376                if ((TryGetSanitizedAdditionalFields(track, "MUSICBRAINZ_ALBUMARTISTID", out var musicBrainzReleaseArtis
 377                     || TryGetSanitizedAdditionalFields(track, "MusicBrainz Album Artist Id", out musicBrainzReleaseArti
 378                    && !string.IsNullOrEmpty(musicBrainzReleaseArtistIdTag))
 379                {
 380                    var id = GetFirstMusicBrainzId(musicBrainzReleaseArtistIdTag, libraryOptions.UseCustomTagDelimiters,
 381                    audio.TrySetProviderId(MetadataProvider.MusicBrainzAlbumArtist, id);
 382                }
 383            }
 384
 385            if (options.ReplaceAllMetadata || !audio.TryGetProviderId(MetadataProvider.MusicBrainzAlbum, out _))
 386            {
 387                if ((TryGetSanitizedAdditionalFields(track, "MUSICBRAINZ_ALBUMID", out var musicBrainzReleaseIdTag)
 388                     || TryGetSanitizedAdditionalFields(track, "MusicBrainz Album Id", out musicBrainzReleaseIdTag))
 389                    && !string.IsNullOrEmpty(musicBrainzReleaseIdTag))
 390                {
 391                    var id = GetFirstMusicBrainzId(musicBrainzReleaseIdTag, libraryOptions.UseCustomTagDelimiters, libra
 392                    audio.TrySetProviderId(MetadataProvider.MusicBrainzAlbum, id);
 393                }
 394            }
 395
 396            if (options.ReplaceAllMetadata || !audio.TryGetProviderId(MetadataProvider.MusicBrainzReleaseGroup, out _))
 397            {
 398                if ((TryGetSanitizedAdditionalFields(track, "MUSICBRAINZ_RELEASEGROUPID", out var musicBrainzReleaseGrou
 399                     || TryGetSanitizedAdditionalFields(track, "MusicBrainz Release Group Id", out musicBrainzReleaseGro
 400                    && !string.IsNullOrEmpty(musicBrainzReleaseGroupIdTag))
 401                {
 402                    var id = GetFirstMusicBrainzId(musicBrainzReleaseGroupIdTag, libraryOptions.UseCustomTagDelimiters, 
 403                    audio.TrySetProviderId(MetadataProvider.MusicBrainzReleaseGroup, id);
 404                }
 405            }
 406
 407            if (options.ReplaceAllMetadata || !audio.TryGetProviderId(MetadataProvider.MusicBrainzTrack, out _))
 408            {
 409                if ((TryGetSanitizedAdditionalFields(track, "MUSICBRAINZ_RELEASETRACKID", out var trackMbId)
 410                     || TryGetSanitizedAdditionalFields(track, "MusicBrainz Release Track Id", out trackMbId))
 411                    && !string.IsNullOrEmpty(trackMbId))
 412                {
 413                    var id = GetFirstMusicBrainzId(trackMbId, libraryOptions.UseCustomTagDelimiters, libraryOptions.GetC
 414                    audio.TrySetProviderId(MetadataProvider.MusicBrainzTrack, id);
 415                }
 416            }
 417
 418            if (options.ReplaceAllMetadata || !audio.TryGetProviderId(MetadataProvider.MusicBrainzRecording, out _))
 419            {
 420                if ((TryGetSanitizedAdditionalFields(track, "MUSICBRAINZ_TRACKID", out var recordingMbId)
 421                     || TryGetSanitizedAdditionalFields(track, "MusicBrainz Track Id", out recordingMbId))
 422                    && !string.IsNullOrEmpty(recordingMbId))
 423                {
 424                    audio.TrySetProviderId(MetadataProvider.MusicBrainzRecording, recordingMbId);
 425                }
 426                else if (TryGetSanitizedAdditionalFields(track, "UFID", out var ufIdValue) && !string.IsNullOrEmpty(ufId
 427                {
 428                    // If tagged with MB Picard, the format is 'http://musicbrainz.org\0<recording MBID>'
 429                    if (ufIdValue.Contains("musicbrainz.org", StringComparison.OrdinalIgnoreCase))
 430                    {
 431                        audio.TrySetProviderId(MetadataProvider.MusicBrainzRecording, ufIdValue.AsSpan().RightPart('\0')
 432                    }
 433                }
 434            }
 435
 436            // Save extracted lyrics if they exist,
 437            // and if the audio doesn't yet have lyrics.
 438            var lyrics = track.Lyrics.SynchronizedLyrics.Count > 0 ? track.Lyrics.FormatSynchToLRC() : track.Lyrics.Unsy
 439            if (!string.IsNullOrWhiteSpace(lyrics)
 440                && tryExtractEmbeddedLyrics)
 441            {
 442                await _lyricManager.SaveLyricAsync(audio, "lrc", lyrics).ConfigureAwait(false);
 443            }
 444        }
 445
 446        private void AddExternalLyrics(
 447            Audio audio,
 448            List<MediaStream> currentStreams,
 449            MetadataRefreshOptions options)
 450        {
 0451            var startIndex = currentStreams.Count == 0 ? 0 : (currentStreams.Select(i => i.Index).Max() + 1);
 0452            var externalLyricFiles = _lyricResolver.GetExternalStreams(audio, startIndex, options.DirectoryService, fals
 453
 0454            audio.LyricFiles = externalLyricFiles.Select(i => i.Path).Distinct().ToArray();
 0455            if (externalLyricFiles.Count > 0)
 456            {
 0457                currentStreams.Add(externalLyricFiles[0]);
 458            }
 0459        }
 460
 461        private List<string> SplitWithCustomDelimiter(string val, char[] tagDelimiters, string[] whitelist)
 462        {
 0463            var items = new List<string>();
 0464            var temp = val;
 0465            foreach (var whitelistItem in whitelist)
 466            {
 0467                if (string.IsNullOrWhiteSpace(whitelistItem))
 468                {
 469                    continue;
 470                }
 471
 0472                var originalTemp = temp;
 0473                temp = temp.Replace(whitelistItem, string.Empty, StringComparison.OrdinalIgnoreCase);
 474
 0475                if (!string.Equals(temp, originalTemp, StringComparison.OrdinalIgnoreCase))
 476                {
 0477                    items.Add(whitelistItem);
 478                }
 479            }
 480
 0481            var items2 = temp.Split(tagDelimiters, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntrie
 0482            items.AddRange(items2);
 483
 0484            return items;
 485        }
 486
 487        // MusicBrainz IDs are multi-value tags, so we need to split them
 488        // However, our current provider can only have one single ID, which means we need to pick the first one
 489        private string? GetFirstMusicBrainzId(string tag, bool useCustomTagDelimiters, char[] tagDelimiters, string[] wh
 490        {
 0491            var val = tag.Split(InternalValueSeparator).FirstOrDefault();
 0492            if (val is not null && useCustomTagDelimiters)
 493            {
 0494                val = SplitWithCustomDelimiter(val, tagDelimiters, whitelist).FirstOrDefault();
 495            }
 496
 0497            return val;
 498        }
 499
 500        private string? GetSanitizedStringTag(string? tag, string filePath)
 501        {
 0502            if (string.IsNullOrEmpty(tag))
 503            {
 0504                return null;
 505            }
 506
 0507            var result = tag.TruncateAtNull();
 0508            if (result.Length != tag.Length)
 509            {
 0510                _logger.LogWarning("Audio file {File} contains a null character in its tag, but this is not allowed by i
 511            }
 512
 0513            return result;
 514        }
 515
 516        private bool TryGetSanitizedAdditionalFields(Track track, string field, out string? value)
 517        {
 0518            var hasField = track.AdditionalFields.TryGetValue(field, out value);
 0519            value = GetSanitizedStringTag(value, track.Path);
 0520            return hasField;
 521        }
 522    }
 523}