< Summary - Jellyfin

Information
Class: MediaBrowser.Providers.MediaInfo.AudioFileProber
Assembly: MediaBrowser.Providers
File(s): /srv/git/jellyfin/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs
Line coverage
34%
Covered lines: 9
Uncovered lines: 17
Coverable lines: 26
Total lines: 440
Line coverage: 34.6%
Branch coverage
0%
Covered branches: 0
Total branches: 10
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%

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 MediaBrowser.Controller.Entities;
 10using MediaBrowser.Controller.Entities.Audio;
 11using MediaBrowser.Controller.Library;
 12using MediaBrowser.Controller.Lyrics;
 13using MediaBrowser.Controller.MediaEncoding;
 14using MediaBrowser.Controller.Persistence;
 15using MediaBrowser.Controller.Providers;
 16using MediaBrowser.Model.Dlna;
 17using MediaBrowser.Model.Dto;
 18using MediaBrowser.Model.Entities;
 19using MediaBrowser.Model.MediaInfo;
 20using Microsoft.Extensions.Logging;
 21
 22namespace MediaBrowser.Providers.MediaInfo
 23{
 24    /// <summary>
 25    /// Probes audio files for metadata.
 26    /// </summary>
 27    public class AudioFileProber
 28    {
 29        private const char InternalValueSeparator = '\u001F';
 30        private readonly IMediaEncoder _mediaEncoder;
 31        private readonly IItemRepository _itemRepo;
 32        private readonly ILibraryManager _libraryManager;
 33        private readonly ILogger<AudioFileProber> _logger;
 34        private readonly IMediaSourceManager _mediaSourceManager;
 35        private readonly LyricResolver _lyricResolver;
 36        private readonly ILyricManager _lyricManager;
 37
 38        /// <summary>
 39        /// Initializes a new instance of the <see cref="AudioFileProber"/> class.
 40        /// </summary>
 41        /// <param name="logger">Instance of the <see cref="ILogger"/> interface.</param>
 42        /// <param name="mediaSourceManager">Instance of the <see cref="IMediaSourceManager"/> interface.</param>
 43        /// <param name="mediaEncoder">Instance of the <see cref="IMediaEncoder"/> interface.</param>
 44        /// <param name="itemRepo">Instance of the <see cref="IItemRepository"/> interface.</param>
 45        /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param>
 46        /// <param name="lyricResolver">Instance of the <see cref="LyricResolver"/> interface.</param>
 47        /// <param name="lyricManager">Instance of the <see cref="ILyricManager"/> interface.</param>
 48        public AudioFileProber(
 49            ILogger<AudioFileProber> logger,
 50            IMediaSourceManager mediaSourceManager,
 51            IMediaEncoder mediaEncoder,
 52            IItemRepository itemRepo,
 53            ILibraryManager libraryManager,
 54            LyricResolver lyricResolver,
 55            ILyricManager lyricManager)
 56        {
 2257            _mediaEncoder = mediaEncoder;
 2258            _itemRepo = itemRepo;
 2259            _libraryManager = libraryManager;
 2260            _logger = logger;
 2261            _mediaSourceManager = mediaSourceManager;
 2262            _lyricResolver = lyricResolver;
 2263            _lyricManager = lyricManager;
 2264            ATL.Settings.DisplayValueSeparator = InternalValueSeparator;
 2265        }
 66
 67        /// <summary>
 68        /// Probes the specified item for metadata.
 69        /// </summary>
 70        /// <param name="item">The item to probe.</param>
 71        /// <param name="options">The <see cref="MetadataRefreshOptions"/>.</param>
 72        /// <param name="cancellationToken">The <see cref="CancellationToken"/>.</param>
 73        /// <typeparam name="T">The type of item to resolve.</typeparam>
 74        /// <returns>A <see cref="Task"/> probing the item for metadata.</returns>
 75        public async Task<ItemUpdateType> Probe<T>(
 76            T item,
 77            MetadataRefreshOptions options,
 78            CancellationToken cancellationToken)
 79            where T : Audio
 80        {
 81            var path = item.Path;
 82            var protocol = item.PathProtocol ?? MediaProtocol.File;
 83
 84            if (!item.IsShortcut || options.EnableRemoteContentProbe)
 85            {
 86                if (item.IsShortcut)
 87                {
 88                    path = item.ShortcutPath;
 89                    protocol = _mediaSourceManager.GetPathProtocol(path);
 90                }
 91
 92                var result = await _mediaEncoder.GetMediaInfo(
 93                    new MediaInfoRequest
 94                    {
 95                        MediaType = DlnaProfileType.Audio,
 96                        MediaSource = new MediaSourceInfo
 97                        {
 98                            Path = path,
 99                            Protocol = protocol
 100                        }
 101                    },
 102                    cancellationToken).ConfigureAwait(false);
 103
 104                cancellationToken.ThrowIfCancellationRequested();
 105
 106                await FetchAsync(item, result, options, cancellationToken).ConfigureAwait(false);
 107            }
 108
 109            return ItemUpdateType.MetadataImport;
 110        }
 111
 112        /// <summary>
 113        /// Fetches the specified audio.
 114        /// </summary>
 115        /// <param name="audio">The <see cref="Audio"/>.</param>
 116        /// <param name="mediaInfo">The <see cref="Model.MediaInfo.MediaInfo"/>.</param>
 117        /// <param name="options">The <see cref="MetadataRefreshOptions"/>.</param>
 118        /// <param name="cancellationToken">The <see cref="CancellationToken"/>.</param>
 119        /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
 120        private async Task FetchAsync(
 121            Audio audio,
 122            Model.MediaInfo.MediaInfo mediaInfo,
 123            MetadataRefreshOptions options,
 124            CancellationToken cancellationToken)
 125        {
 126            audio.Container = mediaInfo.Container;
 127            audio.TotalBitrate = mediaInfo.Bitrate;
 128
 129            audio.RunTimeTicks = mediaInfo.RunTimeTicks;
 130            audio.Size = mediaInfo.Size;
 131
 132            // Add external lyrics first to prevent the lrc file get overwritten on first scan
 133            var mediaStreams = new List<MediaStream>(mediaInfo.MediaStreams);
 134            AddExternalLyrics(audio, mediaStreams, options);
 135            var tryExtractEmbeddedLyrics = mediaStreams.All(s => s.Type != MediaStreamType.Lyric);
 136
 137            if (!audio.IsLocked)
 138            {
 139                await FetchDataFromTags(audio, mediaInfo, options, tryExtractEmbeddedLyrics).ConfigureAwait(false);
 140                if (tryExtractEmbeddedLyrics)
 141                {
 142                    AddExternalLyrics(audio, mediaStreams, options);
 143                }
 144            }
 145
 146            audio.HasLyrics = mediaStreams.Any(s => s.Type == MediaStreamType.Lyric);
 147
 148            _itemRepo.SaveMediaStreams(audio.Id, mediaStreams, cancellationToken);
 149        }
 150
 151        /// <summary>
 152        /// Fetches data from the tags.
 153        /// </summary>
 154        /// <param name="audio">The <see cref="Audio"/>.</param>
 155        /// <param name="mediaInfo">The <see cref="Model.MediaInfo.MediaInfo"/>.</param>
 156        /// <param name="options">The <see cref="MetadataRefreshOptions"/>.</param>
 157        /// <param name="tryExtractEmbeddedLyrics">Whether to extract embedded lyrics to lrc file. </param>
 158        private async Task FetchDataFromTags(Audio audio, Model.MediaInfo.MediaInfo mediaInfo, MetadataRefreshOptions op
 159        {
 160            var libraryOptions = _libraryManager.GetLibraryOptions(audio);
 161            Track track = new Track(audio.Path);
 162
 163            // ATL will fall back to filename as title when it does not understand the metadata
 164            if (track.MetadataFormats.All(mf => mf.Equals(ATL.Factory.UNKNOWN_FORMAT)))
 165            {
 166                track.Title = mediaInfo.Name;
 167            }
 168
 169            track.Album = string.IsNullOrEmpty(track.Album) ? mediaInfo.Album : track.Album;
 170            track.Year ??= mediaInfo.ProductionYear;
 171            track.TrackNumber ??= mediaInfo.IndexNumber;
 172            track.DiscNumber ??= mediaInfo.ParentIndexNumber;
 173
 174            if (audio.SupportsPeople && !audio.LockedFields.Contains(MetadataField.Cast))
 175            {
 176                var people = new List<PersonInfo>();
 177                var albumArtists = string.IsNullOrEmpty(track.AlbumArtist) ? mediaInfo.AlbumArtists : track.AlbumArtist.
 178
 179                if (libraryOptions.UseCustomTagDelimiters)
 180                {
 181                    albumArtists = albumArtists.SelectMany(a => SplitWithCustomDelimiter(a, libraryOptions.CustomTagDeli
 182                }
 183
 184                foreach (var albumArtist in albumArtists)
 185                {
 186                    if (!string.IsNullOrEmpty(albumArtist))
 187                    {
 188                        PeopleHelper.AddPerson(people, new PersonInfo
 189                        {
 190                            Name = albumArtist,
 191                            Type = PersonKind.AlbumArtist
 192                        });
 193                    }
 194                }
 195
 196                string[]? performers = null;
 197                if (libraryOptions.PreferNonstandardArtistsTag)
 198                {
 199                    track.AdditionalFields.TryGetValue("ARTISTS", out var artistsTagString);
 200                    if (artistsTagString is not null)
 201                    {
 202                        performers = artistsTagString.Split(InternalValueSeparator);
 203                    }
 204                }
 205
 206                if (performers is null || performers.Length == 0)
 207                {
 208                    performers = string.IsNullOrEmpty(track.Artist) ? mediaInfo.Artists : track.Artist.Split(InternalVal
 209                }
 210
 211                if (libraryOptions.UseCustomTagDelimiters)
 212                {
 213                    performers = performers.SelectMany(p => SplitWithCustomDelimiter(p, libraryOptions.CustomTagDelimite
 214                }
 215
 216                foreach (var performer in performers)
 217                {
 218                    if (!string.IsNullOrEmpty(performer))
 219                    {
 220                        PeopleHelper.AddPerson(people, new PersonInfo
 221                        {
 222                            Name = performer,
 223                            Type = PersonKind.Artist
 224                        });
 225                    }
 226                }
 227
 228                foreach (var composer in track.Composer.Split(InternalValueSeparator))
 229                {
 230                    if (!string.IsNullOrEmpty(composer))
 231                    {
 232                        PeopleHelper.AddPerson(people, new PersonInfo
 233                        {
 234                            Name = composer,
 235                            Type = PersonKind.Composer
 236                        });
 237                    }
 238                }
 239
 240                _libraryManager.UpdatePeople(audio, people);
 241
 242                if (options.ReplaceAllMetadata && performers.Length != 0)
 243                {
 244                    audio.Artists = performers;
 245                }
 246                else if (!options.ReplaceAllMetadata
 247                         && (audio.Artists is null || audio.Artists.Count == 0))
 248                {
 249                    audio.Artists = performers;
 250                }
 251
 252                if (albumArtists.Length == 0)
 253                {
 254                    // Album artists not provided, fall back to performers (artists).
 255                    albumArtists = performers;
 256                }
 257
 258                if (options.ReplaceAllMetadata && albumArtists.Length != 0)
 259                {
 260                    audio.AlbumArtists = albumArtists;
 261                }
 262                else if (!options.ReplaceAllMetadata
 263                         && (audio.AlbumArtists is null || audio.AlbumArtists.Count == 0))
 264                {
 265                    audio.AlbumArtists = albumArtists;
 266                }
 267            }
 268
 269            if (!audio.LockedFields.Contains(MetadataField.Name) && !string.IsNullOrEmpty(track.Title))
 270            {
 271                audio.Name = track.Title;
 272            }
 273
 274            if (options.ReplaceAllMetadata)
 275            {
 276                audio.Album = track.Album;
 277                audio.IndexNumber = track.TrackNumber;
 278                audio.ParentIndexNumber = track.DiscNumber;
 279            }
 280            else
 281            {
 282                audio.Album ??= track.Album;
 283                audio.IndexNumber ??= track.TrackNumber;
 284                audio.ParentIndexNumber ??= track.DiscNumber;
 285            }
 286
 287            if (track.Date.HasValue)
 288            {
 289                audio.PremiereDate = track.Date;
 290            }
 291
 292            if (track.Year.HasValue)
 293            {
 294                var year = track.Year.Value;
 295                audio.ProductionYear = year;
 296
 297                if (!audio.PremiereDate.HasValue)
 298                {
 299                    try
 300                    {
 301                        audio.PremiereDate = new DateTime(year, 01, 01);
 302                    }
 303                    catch (ArgumentOutOfRangeException ex)
 304                    {
 305                        _logger.LogError(ex, "Error parsing YEAR tag in {File}. '{TagValue}' is an invalid year", audio.
 306                    }
 307                }
 308            }
 309
 310            if (!audio.LockedFields.Contains(MetadataField.Genres))
 311            {
 312                var genres = string.IsNullOrEmpty(track.Genre) ? mediaInfo.Genres : track.Genre.Split(InternalValueSepar
 313
 314                if (libraryOptions.UseCustomTagDelimiters)
 315                {
 316                    genres = genres.SelectMany(g => SplitWithCustomDelimiter(g, libraryOptions.CustomTagDelimiters, libr
 317                }
 318
 319                audio.Genres = options.ReplaceAllMetadata || audio.Genres is null || audio.Genres.Length == 0
 320                    ? genres
 321                    : audio.Genres;
 322            }
 323
 324            track.AdditionalFields.TryGetValue("REPLAYGAIN_TRACK_GAIN", out var trackGainTag);
 325
 326            if (trackGainTag is not null)
 327            {
 328                if (trackGainTag.EndsWith("db", StringComparison.OrdinalIgnoreCase))
 329                {
 330                    trackGainTag = trackGainTag[..^2].Trim();
 331                }
 332
 333                if (float.TryParse(trackGainTag, NumberStyles.Float, CultureInfo.InvariantCulture, out var value))
 334                {
 335                    audio.NormalizationGain = value;
 336                }
 337            }
 338
 339            if (options.ReplaceAllMetadata || !audio.TryGetProviderId(MetadataProvider.MusicBrainzArtist, out _))
 340            {
 341                if ((track.AdditionalFields.TryGetValue("MUSICBRAINZ_ARTISTID", out var musicBrainzArtistTag)
 342                     || track.AdditionalFields.TryGetValue("MusicBrainz Artist Id", out musicBrainzArtistTag))
 343                    && !string.IsNullOrEmpty(musicBrainzArtistTag))
 344                {
 345                    audio.TrySetProviderId(MetadataProvider.MusicBrainzArtist, musicBrainzArtistTag);
 346                }
 347            }
 348
 349            if (options.ReplaceAllMetadata || !audio.TryGetProviderId(MetadataProvider.MusicBrainzAlbumArtist, out _))
 350            {
 351                if ((track.AdditionalFields.TryGetValue("MUSICBRAINZ_ALBUMARTISTID", out var musicBrainzReleaseArtistIdT
 352                     || track.AdditionalFields.TryGetValue("MusicBrainz Album Artist Id", out musicBrainzReleaseArtistId
 353                    && !string.IsNullOrEmpty(musicBrainzReleaseArtistIdTag))
 354                {
 355                    audio.TrySetProviderId(MetadataProvider.MusicBrainzAlbumArtist, musicBrainzReleaseArtistIdTag);
 356                }
 357            }
 358
 359            if (options.ReplaceAllMetadata || !audio.TryGetProviderId(MetadataProvider.MusicBrainzAlbum, out _))
 360            {
 361                if ((track.AdditionalFields.TryGetValue("MUSICBRAINZ_ALBUMID", out var musicBrainzReleaseIdTag)
 362                     || track.AdditionalFields.TryGetValue("MusicBrainz Album Id", out musicBrainzReleaseIdTag))
 363                    && !string.IsNullOrEmpty(musicBrainzReleaseIdTag))
 364                {
 365                    audio.TrySetProviderId(MetadataProvider.MusicBrainzAlbum, musicBrainzReleaseIdTag);
 366                }
 367            }
 368
 369            if (options.ReplaceAllMetadata || !audio.TryGetProviderId(MetadataProvider.MusicBrainzReleaseGroup, out _))
 370            {
 371                if ((track.AdditionalFields.TryGetValue("MUSICBRAINZ_RELEASEGROUPID", out var musicBrainzReleaseGroupIdT
 372                     || track.AdditionalFields.TryGetValue("MusicBrainz Release Group Id", out musicBrainzReleaseGroupId
 373                    && !string.IsNullOrEmpty(musicBrainzReleaseGroupIdTag))
 374                {
 375                    audio.TrySetProviderId(MetadataProvider.MusicBrainzReleaseGroup, musicBrainzReleaseGroupIdTag);
 376                }
 377            }
 378
 379            if (options.ReplaceAllMetadata || !audio.TryGetProviderId(MetadataProvider.MusicBrainzTrack, out _))
 380            {
 381                if ((track.AdditionalFields.TryGetValue("MUSICBRAINZ_RELEASETRACKID", out var trackMbId)
 382                     || track.AdditionalFields.TryGetValue("MusicBrainz Release Track Id", out trackMbId))
 383                    && !string.IsNullOrEmpty(trackMbId))
 384                {
 385                    audio.TrySetProviderId(MetadataProvider.MusicBrainzTrack, trackMbId);
 386                }
 387            }
 388
 389            // Save extracted lyrics if they exist,
 390            // and if the audio doesn't yet have lyrics.
 391            var lyrics = track.Lyrics.SynchronizedLyrics.Count > 0 ? track.Lyrics.FormatSynchToLRC() : track.Lyrics.Unsy
 392            if (!string.IsNullOrWhiteSpace(lyrics)
 393                && tryExtractEmbeddedLyrics)
 394            {
 395                await _lyricManager.SaveLyricAsync(audio, "lrc", lyrics).ConfigureAwait(false);
 396            }
 397        }
 398
 399        private void AddExternalLyrics(
 400            Audio audio,
 401            List<MediaStream> currentStreams,
 402            MetadataRefreshOptions options)
 403        {
 0404            var startIndex = currentStreams.Count == 0 ? 0 : (currentStreams.Select(i => i.Index).Max() + 1);
 0405            var externalLyricFiles = _lyricResolver.GetExternalStreams(audio, startIndex, options.DirectoryService, fals
 406
 0407            audio.LyricFiles = externalLyricFiles.Select(i => i.Path).Distinct().ToArray();
 0408            if (externalLyricFiles.Count > 0)
 409            {
 0410                currentStreams.Add(externalLyricFiles[0]);
 411            }
 0412        }
 413
 414        private List<string> SplitWithCustomDelimiter(string val, char[] tagDelimiters, string[] whitelist)
 415        {
 0416            var items = new List<string>();
 0417            var temp = val;
 0418            foreach (var whitelistItem in whitelist)
 419            {
 0420                if (string.IsNullOrWhiteSpace(whitelistItem))
 421                {
 422                    continue;
 423                }
 424
 0425                var originalTemp = temp;
 0426                temp = temp.Replace(whitelistItem, string.Empty, StringComparison.OrdinalIgnoreCase);
 427
 0428                if (!string.Equals(temp, originalTemp, StringComparison.OrdinalIgnoreCase))
 429                {
 0430                    items.Add(whitelistItem);
 431                }
 432            }
 433
 0434            var items2 = temp.Split(tagDelimiters, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntrie
 0435            items.AddRange(items2);
 436
 0437            return items;
 438        }
 439    }
 440}