using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Threading; using System.Threading.Tasks; using ATL; using Jellyfin.Data.Enums; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Library; using MediaBrowser.Controller.Lyrics; using MediaBrowser.Controller.MediaEncoding; using MediaBrowser.Controller.Persistence; using MediaBrowser.Controller.Providers; using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; using MediaBrowser.Model.Extensions; using MediaBrowser.Model.MediaInfo; using Microsoft.Extensions.Logging; namespace MediaBrowser.Providers.MediaInfo { /// /// Probes audio files for metadata. /// public class AudioFileProber { private const char InternalValueSeparator = '\u001F'; private readonly IMediaEncoder _mediaEncoder; private readonly IItemRepository _itemRepo; private readonly ILibraryManager _libraryManager; private readonly ILogger _logger; private readonly IMediaSourceManager _mediaSourceManager; private readonly LyricResolver _lyricResolver; private readonly ILyricManager _lyricManager; /// /// Initializes a new instance of the class. /// /// Instance of the interface. /// Instance of the interface. /// Instance of the interface. /// Instance of the interface. /// Instance of the interface. /// Instance of the interface. /// Instance of the interface. public AudioFileProber( ILogger logger, IMediaSourceManager mediaSourceManager, IMediaEncoder mediaEncoder, IItemRepository itemRepo, ILibraryManager libraryManager, LyricResolver lyricResolver, ILyricManager lyricManager) { _mediaEncoder = mediaEncoder; _itemRepo = itemRepo; _libraryManager = libraryManager; _logger = logger; _mediaSourceManager = mediaSourceManager; _lyricResolver = lyricResolver; _lyricManager = lyricManager; ATL.Settings.DisplayValueSeparator = InternalValueSeparator; ATL.Settings.UseFileNameWhenNoTitle = false; ATL.Settings.ID3v2_separatev2v3Values = false; } /// /// Probes the specified item for metadata. /// /// The item to probe. /// The . /// The . /// The type of item to resolve. /// A probing the item for metadata. public async Task Probe( T item, MetadataRefreshOptions options, CancellationToken cancellationToken) where T : Audio { var path = item.Path; var protocol = item.PathProtocol ?? MediaProtocol.File; if (!item.IsShortcut || options.EnableRemoteContentProbe) { if (item.IsShortcut) { path = item.ShortcutPath; protocol = _mediaSourceManager.GetPathProtocol(path); } var result = await _mediaEncoder.GetMediaInfo( new MediaInfoRequest { MediaType = DlnaProfileType.Audio, MediaSource = new MediaSourceInfo { Path = path, Protocol = protocol } }, cancellationToken).ConfigureAwait(false); cancellationToken.ThrowIfCancellationRequested(); await FetchAsync(item, result, options, cancellationToken).ConfigureAwait(false); } return ItemUpdateType.MetadataImport; } /// /// Fetches the specified audio. /// /// The . /// The . /// The . /// The . /// A representing the asynchronous operation. private async Task FetchAsync( Audio audio, Model.MediaInfo.MediaInfo mediaInfo, MetadataRefreshOptions options, CancellationToken cancellationToken) { audio.Container = mediaInfo.Container; audio.TotalBitrate = mediaInfo.Bitrate; audio.RunTimeTicks = mediaInfo.RunTimeTicks; audio.Size = mediaInfo.Size; // Add external lyrics first to prevent the lrc file get overwritten on first scan var mediaStreams = new List(mediaInfo.MediaStreams); AddExternalLyrics(audio, mediaStreams, options); var tryExtractEmbeddedLyrics = mediaStreams.All(s => s.Type != MediaStreamType.Lyric); if (!audio.IsLocked) { await FetchDataFromTags(audio, mediaInfo, options, tryExtractEmbeddedLyrics).ConfigureAwait(false); if (tryExtractEmbeddedLyrics) { AddExternalLyrics(audio, mediaStreams, options); } } audio.HasLyrics = mediaStreams.Any(s => s.Type == MediaStreamType.Lyric); _itemRepo.SaveMediaStreams(audio.Id, mediaStreams, cancellationToken); } /// /// Fetches data from the tags. /// /// The . /// The . /// The . /// Whether to extract embedded lyrics to lrc file. private async Task FetchDataFromTags(Audio audio, Model.MediaInfo.MediaInfo mediaInfo, MetadataRefreshOptions options, bool tryExtractEmbeddedLyrics) { var libraryOptions = _libraryManager.GetLibraryOptions(audio); Track track = new Track(audio.Path); if (track.MetadataFormats .All(mf => string.Equals(mf.ShortName, "ID3v1", StringComparison.OrdinalIgnoreCase))) { _logger.LogWarning("File {File} only has ID3v1 tags, some fields may be truncated", audio.Path); } track.Title = string.IsNullOrEmpty(track.Title) ? mediaInfo.Name : track.Title; track.Album = string.IsNullOrEmpty(track.Album) ? mediaInfo.Album : track.Album; track.Year ??= mediaInfo.ProductionYear; track.TrackNumber ??= mediaInfo.IndexNumber; track.DiscNumber ??= mediaInfo.ParentIndexNumber; if (audio.SupportsPeople && !audio.LockedFields.Contains(MetadataField.Cast)) { var people = new List(); var albumArtists = string.IsNullOrEmpty(track.AlbumArtist) ? mediaInfo.AlbumArtists : track.AlbumArtist.Split(InternalValueSeparator); if (libraryOptions.UseCustomTagDelimiters) { albumArtists = albumArtists.SelectMany(a => SplitWithCustomDelimiter(a, libraryOptions.GetCustomTagDelimiters(), libraryOptions.DelimiterWhitelist)).ToArray(); } foreach (var albumArtist in albumArtists) { if (!string.IsNullOrEmpty(albumArtist)) { PeopleHelper.AddPerson(people, new PersonInfo { Name = albumArtist, Type = PersonKind.AlbumArtist }); } } string[]? performers = null; if (libraryOptions.PreferNonstandardArtistsTag) { track.AdditionalFields.TryGetValue("ARTISTS", out var artistsTagString); if (artistsTagString is not null) { performers = artistsTagString.Split(InternalValueSeparator); } } if (performers is null || performers.Length == 0) { performers = string.IsNullOrEmpty(track.Artist) ? mediaInfo.Artists : track.Artist.Split(InternalValueSeparator); } if (libraryOptions.UseCustomTagDelimiters) { performers = performers.SelectMany(p => SplitWithCustomDelimiter(p, libraryOptions.GetCustomTagDelimiters(), libraryOptions.DelimiterWhitelist)).ToArray(); } foreach (var performer in performers) { if (!string.IsNullOrEmpty(performer)) { PeopleHelper.AddPerson(people, new PersonInfo { Name = performer, Type = PersonKind.Artist }); } } foreach (var composer in track.Composer.Split(InternalValueSeparator)) { if (!string.IsNullOrEmpty(composer)) { PeopleHelper.AddPerson(people, new PersonInfo { Name = composer, Type = PersonKind.Composer }); } } _libraryManager.UpdatePeople(audio, people); if (options.ReplaceAllMetadata && performers.Length != 0) { audio.Artists = performers; } else if (!options.ReplaceAllMetadata && (audio.Artists is null || audio.Artists.Count == 0)) { audio.Artists = performers; } if (albumArtists.Length == 0) { // Album artists not provided, fall back to performers (artists). albumArtists = performers; } if (options.ReplaceAllMetadata && albumArtists.Length != 0) { audio.AlbumArtists = albumArtists; } else if (!options.ReplaceAllMetadata && (audio.AlbumArtists is null || audio.AlbumArtists.Count == 0)) { audio.AlbumArtists = albumArtists; } } if (!audio.LockedFields.Contains(MetadataField.Name) && !string.IsNullOrEmpty(track.Title)) { audio.Name = track.Title; } if (options.ReplaceAllMetadata) { audio.Album = track.Album; audio.IndexNumber = track.TrackNumber; audio.ParentIndexNumber = track.DiscNumber; } else { audio.Album ??= track.Album; audio.IndexNumber ??= track.TrackNumber; audio.ParentIndexNumber ??= track.DiscNumber; } if (track.Date.HasValue) { audio.PremiereDate = track.Date; } if (track.Year.HasValue) { var year = track.Year.Value; audio.ProductionYear = year; if (!audio.PremiereDate.HasValue) { try { audio.PremiereDate = new DateTime(year, 01, 01); } catch (ArgumentOutOfRangeException ex) { _logger.LogError(ex, "Error parsing YEAR tag in {File}. '{TagValue}' is an invalid year", audio.Path, track.Year); } } } if (!audio.LockedFields.Contains(MetadataField.Genres)) { var genres = string.IsNullOrEmpty(track.Genre) ? mediaInfo.Genres : track.Genre.Split(InternalValueSeparator).Distinct(StringComparer.OrdinalIgnoreCase).ToArray(); if (libraryOptions.UseCustomTagDelimiters) { genres = genres.SelectMany(g => SplitWithCustomDelimiter(g, libraryOptions.GetCustomTagDelimiters(), libraryOptions.DelimiterWhitelist)).ToArray(); } audio.Genres = options.ReplaceAllMetadata || audio.Genres is null || audio.Genres.Length == 0 ? genres : audio.Genres; } track.AdditionalFields.TryGetValue("REPLAYGAIN_TRACK_GAIN", out var trackGainTag); if (trackGainTag is not null) { if (trackGainTag.EndsWith("db", StringComparison.OrdinalIgnoreCase)) { trackGainTag = trackGainTag[..^2].Trim(); } if (float.TryParse(trackGainTag, NumberStyles.Float, CultureInfo.InvariantCulture, out var value)) { audio.NormalizationGain = value; } } if (options.ReplaceAllMetadata || !audio.TryGetProviderId(MetadataProvider.MusicBrainzArtist, out _)) { if ((track.AdditionalFields.TryGetValue("MUSICBRAINZ_ARTISTID", out var musicBrainzArtistTag) || track.AdditionalFields.TryGetValue("MusicBrainz Artist Id", out musicBrainzArtistTag)) && !string.IsNullOrEmpty(musicBrainzArtistTag)) { audio.TrySetProviderId(MetadataProvider.MusicBrainzArtist, musicBrainzArtistTag); } } if (options.ReplaceAllMetadata || !audio.TryGetProviderId(MetadataProvider.MusicBrainzAlbumArtist, out _)) { if ((track.AdditionalFields.TryGetValue("MUSICBRAINZ_ALBUMARTISTID", out var musicBrainzReleaseArtistIdTag) || track.AdditionalFields.TryGetValue("MusicBrainz Album Artist Id", out musicBrainzReleaseArtistIdTag)) && !string.IsNullOrEmpty(musicBrainzReleaseArtistIdTag)) { audio.TrySetProviderId(MetadataProvider.MusicBrainzAlbumArtist, musicBrainzReleaseArtistIdTag); } } if (options.ReplaceAllMetadata || !audio.TryGetProviderId(MetadataProvider.MusicBrainzAlbum, out _)) { if ((track.AdditionalFields.TryGetValue("MUSICBRAINZ_ALBUMID", out var musicBrainzReleaseIdTag) || track.AdditionalFields.TryGetValue("MusicBrainz Album Id", out musicBrainzReleaseIdTag)) && !string.IsNullOrEmpty(musicBrainzReleaseIdTag)) { audio.TrySetProviderId(MetadataProvider.MusicBrainzAlbum, musicBrainzReleaseIdTag); } } if (options.ReplaceAllMetadata || !audio.TryGetProviderId(MetadataProvider.MusicBrainzReleaseGroup, out _)) { if ((track.AdditionalFields.TryGetValue("MUSICBRAINZ_RELEASEGROUPID", out var musicBrainzReleaseGroupIdTag) || track.AdditionalFields.TryGetValue("MusicBrainz Release Group Id", out musicBrainzReleaseGroupIdTag)) && !string.IsNullOrEmpty(musicBrainzReleaseGroupIdTag)) { audio.TrySetProviderId(MetadataProvider.MusicBrainzReleaseGroup, musicBrainzReleaseGroupIdTag); } } if (options.ReplaceAllMetadata || !audio.TryGetProviderId(MetadataProvider.MusicBrainzTrack, out _)) { if ((track.AdditionalFields.TryGetValue("MUSICBRAINZ_RELEASETRACKID", out var trackMbId) || track.AdditionalFields.TryGetValue("MusicBrainz Release Track Id", out trackMbId)) && !string.IsNullOrEmpty(trackMbId)) { audio.TrySetProviderId(MetadataProvider.MusicBrainzTrack, trackMbId); } } // Save extracted lyrics if they exist, // and if the audio doesn't yet have lyrics. var lyrics = track.Lyrics.SynchronizedLyrics.Count > 0 ? track.Lyrics.FormatSynchToLRC() : track.Lyrics.UnsynchronizedLyrics; if (!string.IsNullOrWhiteSpace(lyrics) && tryExtractEmbeddedLyrics) { await _lyricManager.SaveLyricAsync(audio, "lrc", lyrics).ConfigureAwait(false); } } private void AddExternalLyrics( Audio audio, List currentStreams, MetadataRefreshOptions options) { var startIndex = currentStreams.Count == 0 ? 0 : (currentStreams.Select(i => i.Index).Max() + 1); var externalLyricFiles = _lyricResolver.GetExternalStreams(audio, startIndex, options.DirectoryService, false); audio.LyricFiles = externalLyricFiles.Select(i => i.Path).Distinct().ToArray(); if (externalLyricFiles.Count > 0) { currentStreams.Add(externalLyricFiles[0]); } } private List SplitWithCustomDelimiter(string val, char[] tagDelimiters, string[] whitelist) { var items = new List(); var temp = val; foreach (var whitelistItem in whitelist) { if (string.IsNullOrWhiteSpace(whitelistItem)) { continue; } var originalTemp = temp; temp = temp.Replace(whitelistItem, string.Empty, StringComparison.OrdinalIgnoreCase); if (!string.Equals(temp, originalTemp, StringComparison.OrdinalIgnoreCase)) { items.Add(whitelistItem); } } var items2 = temp.Split(tagDelimiters, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries).DistinctNames(); items.AddRange(items2); return items; } } }