From c56dbc1c4410e1b0ec31ca901809b6f627bbb6ed Mon Sep 17 00:00:00 2001 From: Tim Eisele Date: Sat, 7 Sep 2024 19:23:48 +0200 Subject: Enhance Trickplay (#11883) --- MediaBrowser.Model/Configuration/LibraryOptions.cs | 4 ++++ 1 file changed, 4 insertions(+) (limited to 'MediaBrowser.Model/Configuration/LibraryOptions.cs') diff --git a/MediaBrowser.Model/Configuration/LibraryOptions.cs b/MediaBrowser.Model/Configuration/LibraryOptions.cs index b0f5c2a11..688a6418d 100644 --- a/MediaBrowser.Model/Configuration/LibraryOptions.cs +++ b/MediaBrowser.Model/Configuration/LibraryOptions.cs @@ -24,6 +24,7 @@ namespace MediaBrowser.Model.Configuration EnablePhotos = true; SaveSubtitlesWithMedia = true; SaveLyricsWithMedia = false; + SaveTrickplayWithMedia = false; PathInfos = Array.Empty(); EnableAutomaticSeriesGrouping = true; SeasonZeroDisplayName = "Specials"; @@ -99,6 +100,9 @@ namespace MediaBrowser.Model.Configuration [DefaultValue(false)] public bool SaveLyricsWithMedia { get; set; } + [DefaultValue(false)] + public bool SaveTrickplayWithMedia { get; set; } + public string[] DisabledLyricFetchers { get; set; } public string[] LyricFetcherOrder { get; set; } -- cgit v1.2.3 From 5ceedced1c4a8bac5b5b7a5f2bd0913783bd427b Mon Sep 17 00:00:00 2001 From: JPVenson Date: Sat, 7 Sep 2024 22:56:51 +0200 Subject: Feature/media segments plugin api (#12359) --- CONTRIBUTORS.md | 1 + .../Localization/Core/en-US.json | 2 + .../Tasks/MediaSegmentExtractionTask.cs | 118 +++++++++++++++++++++ .../MediaSegments/MediaSegmentManager.cs | 102 +++++++++++++++++- .../MediaSegements/IMediaSegmentManager.cs | 17 +++ .../MediaSegements/IMediaSegmentProvider.cs | 36 +++++++ MediaBrowser.Model/Configuration/LibraryOptions.cs | 6 ++ .../Configuration/MetadataPluginType.cs | 3 +- .../MediaSegments/MediaSegmentGenerationRequest.cs | 14 +++ MediaBrowser.Providers/Manager/ProviderManager.cs | 15 ++- .../Manager/ProviderManagerTests.cs | 3 +- 11 files changed, 312 insertions(+), 5 deletions(-) create mode 100644 Emby.Server.Implementations/ScheduledTasks/Tasks/MediaSegmentExtractionTask.cs create mode 100644 MediaBrowser.Controller/MediaSegements/IMediaSegmentProvider.cs create mode 100644 MediaBrowser.Model/MediaSegments/MediaSegmentGenerationRequest.cs (limited to 'MediaBrowser.Model/Configuration/LibraryOptions.cs') diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index cdf8df17f..91faa2c2e 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -65,6 +65,7 @@ - [joshuaboniface](https://github.com/joshuaboniface) - [JustAMan](https://github.com/JustAMan) - [justinfenn](https://github.com/justinfenn) + - [JPVenson](https://github.com/JPVenson) - [KerryRJ](https://github.com/KerryRJ) - [Larvitar](https://github.com/Larvitar) - [LeoVerto](https://github.com/LeoVerto) diff --git a/Emby.Server.Implementations/Localization/Core/en-US.json b/Emby.Server.Implementations/Localization/Core/en-US.json index d248fc303..9702ab712 100644 --- a/Emby.Server.Implementations/Localization/Core/en-US.json +++ b/Emby.Server.Implementations/Localization/Core/en-US.json @@ -132,6 +132,8 @@ "TaskKeyframeExtractorDescription": "Extracts keyframes from video files to create more precise HLS playlists. This task may run for a long time.", "TaskCleanCollectionsAndPlaylists": "Clean up collections and playlists", "TaskCleanCollectionsAndPlaylistsDescription": "Removes items from collections and playlists that no longer exist.", + "TaskExtractMediaSegments": "Media Segment Scan", + "TaskExtractMediaSegmentsDescription": "Extracts or obtains media segments from MediaSegment enabled plugins.", "TaskMoveTrickplayImages": "Migrate Trickplay Image Location", "TaskMoveTrickplayImagesDescription": "Moves existing trickplay files according to the library settings." } diff --git a/Emby.Server.Implementations/ScheduledTasks/Tasks/MediaSegmentExtractionTask.cs b/Emby.Server.Implementations/ScheduledTasks/Tasks/MediaSegmentExtractionTask.cs new file mode 100644 index 000000000..d6fad7526 --- /dev/null +++ b/Emby.Server.Implementations/ScheduledTasks/Tasks/MediaSegmentExtractionTask.cs @@ -0,0 +1,118 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Jellyfin.Data.Enums; +using MediaBrowser.Controller; +using MediaBrowser.Controller.Dto; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Library; +using MediaBrowser.Model.Globalization; +using MediaBrowser.Model.Tasks; + +namespace Emby.Server.Implementations.ScheduledTasks.Tasks; + +/// +/// Task to obtain media segments. +/// +public class MediaSegmentExtractionTask : IScheduledTask +{ + /// + /// The library manager. + /// + private readonly ILibraryManager _libraryManager; + private readonly ILocalizationManager _localization; + private readonly IMediaSegmentManager _mediaSegmentManager; + private static readonly BaseItemKind[] _itemTypes = [BaseItemKind.Episode, BaseItemKind.Movie, BaseItemKind.Audio, BaseItemKind.AudioBook]; + + /// + /// Initializes a new instance of the class. + /// + /// The library manager. + /// The localization manager. + /// The segment manager. + public MediaSegmentExtractionTask(ILibraryManager libraryManager, ILocalizationManager localization, IMediaSegmentManager mediaSegmentManager) + { + _libraryManager = libraryManager; + _localization = localization; + _mediaSegmentManager = mediaSegmentManager; + } + + /// + public string Name => _localization.GetLocalizedString("TaskExtractMediaSegments"); + + /// + public string Description => _localization.GetLocalizedString("TaskExtractMediaSegmentsDescription"); + + /// + public string Category => _localization.GetLocalizedString("TasksLibraryCategory"); + + /// + public string Key => "TaskExtractMediaSegments"; + + /// + public async Task ExecuteAsync(IProgress progress, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + + progress.Report(0); + + var pagesize = 100; + + var query = new InternalItemsQuery + { + MediaTypes = new[] { MediaType.Video, MediaType.Audio }, + IsVirtualItem = false, + IncludeItemTypes = _itemTypes, + DtoOptions = new DtoOptions(true), + SourceTypes = new[] { SourceType.Library }, + Recursive = true, + Limit = pagesize + }; + + var numberOfVideos = _libraryManager.GetCount(query); + + var startIndex = 0; + var numComplete = 0; + + while (startIndex < numberOfVideos) + { + query.StartIndex = startIndex; + + var baseItems = _libraryManager.GetItemList(query); + var currentPageCount = baseItems.Count; + // TODO parallelize with Parallel.ForEach? + for (var i = 0; i < currentPageCount; i++) + { + cancellationToken.ThrowIfCancellationRequested(); + + var item = baseItems[i]; + // Only local files supported + if (item.IsFileProtocol && File.Exists(item.Path)) + { + await _mediaSegmentManager.RunSegmentPluginProviders(item, false, cancellationToken).ConfigureAwait(false); + } + + // Update progress + numComplete++; + double percent = (double)numComplete / numberOfVideos; + progress.Report(100 * percent); + } + + startIndex += pagesize; + } + + progress.Report(100); + } + + /// + public IEnumerable GetDefaultTriggers() + { + yield return new TaskTriggerInfo + { + Type = TaskTriggerInfo.TriggerInterval, + IntervalTicks = TimeSpan.FromHours(12).Ticks + }; + } +} diff --git a/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentManager.cs b/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentManager.cs index 7916d15c9..9953c05be 100644 --- a/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentManager.cs +++ b/Jellyfin.Server.Implementations/MediaSegments/MediaSegmentManager.cs @@ -1,14 +1,23 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; +using System.Globalization; using System.Linq; +using System.Threading; using System.Threading.Tasks; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; +using Jellyfin.Extensions; +using MediaBrowser.Common.Extensions; using MediaBrowser.Controller; using MediaBrowser.Controller.Entities; +using MediaBrowser.Controller.Entities.Audio; +using MediaBrowser.Controller.Library; +using MediaBrowser.Controller.Providers; +using MediaBrowser.Model; using MediaBrowser.Model.MediaSegments; using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Logging; namespace Jellyfin.Server.Implementations.MediaSegments; @@ -17,15 +26,89 @@ namespace Jellyfin.Server.Implementations.MediaSegments; /// public class MediaSegmentManager : IMediaSegmentManager { + private readonly ILogger _logger; private readonly IDbContextFactory _dbProvider; + private readonly IMediaSegmentProvider[] _segmentProviders; + private readonly ILibraryManager _libraryManager; /// /// Initializes a new instance of the class. /// + /// Logger. /// EFCore Database factory. - public MediaSegmentManager(IDbContextFactory dbProvider) + /// List of all media segment providers. + /// Library manager. + public MediaSegmentManager( + ILogger logger, + IDbContextFactory dbProvider, + IEnumerable segmentProviders, + ILibraryManager libraryManager) { + _logger = logger; _dbProvider = dbProvider; + + _segmentProviders = segmentProviders + .OrderBy(i => i is IHasOrder hasOrder ? hasOrder.Order : 0) + .ToArray(); + _libraryManager = libraryManager; + } + + /// + public async Task RunSegmentPluginProviders(BaseItem baseItem, bool overwrite, CancellationToken cancellationToken) + { + var libraryOptions = _libraryManager.GetLibraryOptions(baseItem); + var providers = _segmentProviders + .Where(e => !libraryOptions.DisabledMediaSegmentProviders.Contains(GetProviderId(e.Name))) + .OrderBy(i => + { + var index = libraryOptions.MediaSegmentProvideOrder.IndexOf(i.Name); + return index == -1 ? int.MaxValue : index; + }) + .ToList(); + + _logger.LogInformation("Start media segment extraction from providers with {CountProviders} enabled", providers.Count); + using var db = await _dbProvider.CreateDbContextAsync(cancellationToken).ConfigureAwait(false); + + if (!overwrite && (await db.MediaSegments.AnyAsync(e => e.ItemId.Equals(baseItem.Id), cancellationToken).ConfigureAwait(false))) + { + _logger.LogInformation("Skip {MediaPath} as it already contains media segments", baseItem.Path); + return; + } + + _logger.LogInformation("Clear existing Segments for {MediaPath}", baseItem.Path); + + await db.MediaSegments.Where(e => e.ItemId.Equals(baseItem.Id)).ExecuteDeleteAsync(cancellationToken).ConfigureAwait(false); + + // no need to recreate the request object every time. + var requestItem = new MediaSegmentGenerationRequest() { ItemId = baseItem.Id }; + + foreach (var provider in providers) + { + if (!await provider.Supports(baseItem).ConfigureAwait(false)) + { + _logger.LogDebug("Media Segment provider {ProviderName} does not support item with path {Path}", provider.Name, baseItem.Path); + continue; + } + + _logger.LogDebug("Run Media Segment provider {ProviderName}", provider.Name); + try + { + var segments = await provider.GetMediaSegments(requestItem, cancellationToken) + .ConfigureAwait(false); + + _logger.LogInformation("Media Segment provider {ProviderName} found {CountSegments} for {MediaPath}", provider.Name, segments.Count, baseItem.Path); + var providerId = GetProviderId(provider.Name); + foreach (var segment in segments) + { + segment.ItemId = baseItem.Id; + await CreateSegmentAsync(segment, providerId).ConfigureAwait(false); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Provider {ProviderName} failed to extract segments from {MediaPath}", provider.Name, baseItem.Path); + } + } } /// @@ -103,4 +186,21 @@ public class MediaSegmentManager : IMediaSegmentManager { return baseItem.MediaType is Data.Enums.MediaType.Video or Data.Enums.MediaType.Audio; } + + /// + public IEnumerable<(string Name, string Id)> GetSupportedProviders(BaseItem item) + { + if (item is not (Video or Audio)) + { + return []; + } + + return _segmentProviders + .Select(p => (p.Name, GetProviderId(p.Name))); + } + + private string GetProviderId(string name) + => name.ToLowerInvariant() + .GetMD5() + .ToString("N", CultureInfo.InvariantCulture); } diff --git a/MediaBrowser.Controller/MediaSegements/IMediaSegmentManager.cs b/MediaBrowser.Controller/MediaSegements/IMediaSegmentManager.cs index 67384f6f6..010d7edb4 100644 --- a/MediaBrowser.Controller/MediaSegements/IMediaSegmentManager.cs +++ b/MediaBrowser.Controller/MediaSegements/IMediaSegmentManager.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; using Jellyfin.Data.Entities; using Jellyfin.Data.Enums; @@ -13,6 +14,15 @@ namespace MediaBrowser.Controller; /// public interface IMediaSegmentManager { + /// + /// Uses all segment providers enabled for the 's library to get the Media Segments. + /// + /// The Item to evaluate. + /// If set, will remove existing segments and replace it with new ones otherwise will check for existing segments and if found any, stops. + /// stop request token. + /// A task that indicates the Operation is finished. + Task RunSegmentPluginProviders(BaseItem baseItem, bool overwrite, CancellationToken cancellationToken); + /// /// Returns if this item supports media segments. /// @@ -50,4 +60,11 @@ public interface IMediaSegmentManager /// True if there are any segments stored for the item, otherwise false. /// TODO: this should be async but as the only caller BaseItem.GetVersionInfo isn't async, this is also not. Venson. bool HasSegments(Guid itemId); + + /// + /// Gets a list of all registered Segment Providers and their IDs. + /// + /// The media item that should be tested for providers. + /// A list of all providers for the tested item. + IEnumerable<(string Name, string Id)> GetSupportedProviders(BaseItem item); } diff --git a/MediaBrowser.Controller/MediaSegements/IMediaSegmentProvider.cs b/MediaBrowser.Controller/MediaSegements/IMediaSegmentProvider.cs new file mode 100644 index 000000000..39bb58bef --- /dev/null +++ b/MediaBrowser.Controller/MediaSegements/IMediaSegmentProvider.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; +using MediaBrowser.Controller.Entities; +using MediaBrowser.Model; +using MediaBrowser.Model.MediaSegments; + +namespace MediaBrowser.Controller; + +/// +/// Provides methods for Obtaining the Media Segments from an Item. +/// +public interface IMediaSegmentProvider +{ + /// + /// Gets the provider name. + /// + string Name { get; } + + /// + /// Enumerates all Media Segments from an Media Item. + /// + /// Arguments to enumerate MediaSegments. + /// Abort token. + /// A list of all MediaSegments found from this provider. + Task> GetMediaSegments(MediaSegmentGenerationRequest request, CancellationToken cancellationToken); + + /// + /// Should return support state for the given item. + /// + /// The base item to extract segments from. + /// True if item is supported, otherwise false. + ValueTask Supports(BaseItem item); +} diff --git a/MediaBrowser.Model/Configuration/LibraryOptions.cs b/MediaBrowser.Model/Configuration/LibraryOptions.cs index 688a6418d..90ac377f4 100644 --- a/MediaBrowser.Model/Configuration/LibraryOptions.cs +++ b/MediaBrowser.Model/Configuration/LibraryOptions.cs @@ -11,6 +11,8 @@ namespace MediaBrowser.Model.Configuration { TypeOptions = Array.Empty(); DisabledSubtitleFetchers = Array.Empty(); + DisabledMediaSegmentProviders = Array.Empty(); + MediaSegmentProvideOrder = Array.Empty(); SubtitleFetcherOrder = Array.Empty(); DisabledLocalMetadataReaders = Array.Empty(); DisabledLyricFetchers = Array.Empty(); @@ -87,6 +89,10 @@ namespace MediaBrowser.Model.Configuration public string[] SubtitleFetcherOrder { get; set; } + public string[] DisabledMediaSegmentProviders { get; set; } + + public string[] MediaSegmentProvideOrder { get; set; } + public bool SkipSubtitlesIfEmbeddedSubtitlesPresent { get; set; } public bool SkipSubtitlesIfAudioTrackMatches { get; set; } diff --git a/MediaBrowser.Model/Configuration/MetadataPluginType.cs b/MediaBrowser.Model/Configuration/MetadataPluginType.cs index ef303726d..670d6e383 100644 --- a/MediaBrowser.Model/Configuration/MetadataPluginType.cs +++ b/MediaBrowser.Model/Configuration/MetadataPluginType.cs @@ -14,6 +14,7 @@ namespace MediaBrowser.Model.Configuration MetadataFetcher, MetadataSaver, SubtitleFetcher, - LyricFetcher + LyricFetcher, + MediaSegmentProvider } } diff --git a/MediaBrowser.Model/MediaSegments/MediaSegmentGenerationRequest.cs b/MediaBrowser.Model/MediaSegments/MediaSegmentGenerationRequest.cs new file mode 100644 index 000000000..8c1f44de8 --- /dev/null +++ b/MediaBrowser.Model/MediaSegments/MediaSegmentGenerationRequest.cs @@ -0,0 +1,14 @@ +using System; + +namespace MediaBrowser.Model; + +/// +/// Model containing the arguments for enumerating the requested media item. +/// +public record MediaSegmentGenerationRequest +{ + /// + /// Gets the Id to the BaseItem the segments should be extracted from. + /// + public Guid ItemId { get; init; } +} diff --git a/MediaBrowser.Providers/Manager/ProviderManager.cs b/MediaBrowser.Providers/Manager/ProviderManager.cs index 60d89a51b..81a9af68b 100644 --- a/MediaBrowser.Providers/Manager/ProviderManager.cs +++ b/MediaBrowser.Providers/Manager/ProviderManager.cs @@ -62,7 +62,7 @@ namespace MediaBrowser.Providers.Manager private readonly CancellationTokenSource _disposeCancellationTokenSource = new(); private readonly PriorityQueue<(Guid ItemId, MetadataRefreshOptions RefreshOptions), RefreshPriority> _refreshQueue = new(); private readonly IMemoryCache _memoryCache; - + private readonly IMediaSegmentManager _mediaSegmentManager; private readonly AsyncKeyedLocker _imageSaveLock = new(o => { o.PoolSize = 20; @@ -92,6 +92,7 @@ namespace MediaBrowser.Providers.Manager /// The BaseItem manager. /// The lyric manager. /// The memory cache. + /// The media segment manager. public ProviderManager( IHttpClientFactory httpClientFactory, ISubtitleManager subtitleManager, @@ -103,7 +104,8 @@ namespace MediaBrowser.Providers.Manager ILibraryManager libraryManager, IBaseItemManager baseItemManager, ILyricManager lyricManager, - IMemoryCache memoryCache) + IMemoryCache memoryCache, + IMediaSegmentManager mediaSegmentManager) { _logger = logger; _httpClientFactory = httpClientFactory; @@ -116,6 +118,7 @@ namespace MediaBrowser.Providers.Manager _baseItemManager = baseItemManager; _lyricManager = lyricManager; _memoryCache = memoryCache; + _mediaSegmentManager = mediaSegmentManager; } /// @@ -572,6 +575,14 @@ namespace MediaBrowser.Providers.Manager Type = MetadataPluginType.LyricFetcher })); + // Media segment providers + var mediaSegmentProviders = _mediaSegmentManager.GetSupportedProviders(dummy); + pluginList.AddRange(mediaSegmentProviders.Select(i => new MetadataPlugin + { + Name = i.Name, + Type = MetadataPluginType.MediaSegmentProvider + })); + summary.Plugins = pluginList.ToArray(); var supportedImageTypes = imageProviders.OfType() diff --git a/tests/Jellyfin.Providers.Tests/Manager/ProviderManagerTests.cs b/tests/Jellyfin.Providers.Tests/Manager/ProviderManagerTests.cs index cced2b1e2..c227883b5 100644 --- a/tests/Jellyfin.Providers.Tests/Manager/ProviderManagerTests.cs +++ b/tests/Jellyfin.Providers.Tests/Manager/ProviderManagerTests.cs @@ -574,7 +574,8 @@ namespace Jellyfin.Providers.Tests.Manager libraryManager.Object, baseItemManager!, Mock.Of(), - Mock.Of()); + Mock.Of(), + Mock.Of()); return providerManager; } -- cgit v1.2.3 From c6de7225b9f493bb18f5e7362d785ac8a71e5f32 Mon Sep 17 00:00:00 2001 From: gnattu Date: Sun, 8 Sep 2024 11:10:59 +0800 Subject: Add non-standard multi-value audio tag support (#12385) --- MediaBrowser.Model/Configuration/LibraryOptions.cs | 18 +++++++ .../MediaInfo/AudioFileProber.cs | 61 +++++++++++++++++++++- 2 files changed, 77 insertions(+), 2 deletions(-) (limited to 'MediaBrowser.Model/Configuration/LibraryOptions.cs') diff --git a/MediaBrowser.Model/Configuration/LibraryOptions.cs b/MediaBrowser.Model/Configuration/LibraryOptions.cs index 90ac377f4..04283cc9e 100644 --- a/MediaBrowser.Model/Configuration/LibraryOptions.cs +++ b/MediaBrowser.Model/Configuration/LibraryOptions.cs @@ -7,6 +7,8 @@ namespace MediaBrowser.Model.Configuration { public class LibraryOptions { + private static readonly char[] _defaultTagDelimiters = ['/', '|', ';', '\\']; + public LibraryOptions() { TypeOptions = Array.Empty(); @@ -30,6 +32,11 @@ namespace MediaBrowser.Model.Configuration PathInfos = Array.Empty(); EnableAutomaticSeriesGrouping = true; SeasonZeroDisplayName = "Specials"; + + PreferNonstandardArtistsTag = false; + UseCustomTagDelimiters = false; + CustomTagDelimiters = _defaultTagDelimiters; + DelimiterWhitelist = Array.Empty(); } public bool Enabled { get; set; } = true; @@ -113,6 +120,17 @@ namespace MediaBrowser.Model.Configuration public string[] LyricFetcherOrder { get; set; } + [DefaultValue(false)] + public bool PreferNonstandardArtistsTag { get; set; } + + [DefaultValue(false)] + public bool UseCustomTagDelimiters { get; set; } + + [DefaultValue(typeof(LibraryOptions), nameof(_defaultTagDelimiters))] + public char[] CustomTagDelimiters { get; set; } + + public string[] DelimiterWhitelist { get; set; } + public bool AutomaticallyAddToCollection { get; set; } public EmbeddedSubtitleOptions AllowEmbeddedSubtitles { get; set; } diff --git a/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs b/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs index 7e0773b6d..51ac558b8 100644 --- a/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs +++ b/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs @@ -6,7 +6,6 @@ using System.Threading; using System.Threading.Tasks; using ATL; using Jellyfin.Data.Enums; -using Jellyfin.Extensions; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; using MediaBrowser.Controller.Library; @@ -158,6 +157,7 @@ namespace MediaBrowser.Providers.MediaInfo /// 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); // ATL will fall back to filename as title when it does not understand the metadata @@ -175,6 +175,12 @@ namespace MediaBrowser.Providers.MediaInfo { 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.CustomTagDelimiters, libraryOptions.DelimiterWhitelist)).ToArray(); + } + foreach (var albumArtist in albumArtists) { if (!string.IsNullOrEmpty(albumArtist)) @@ -187,7 +193,26 @@ namespace MediaBrowser.Providers.MediaInfo } } - var performers = string.IsNullOrEmpty(track.Artist) ? mediaInfo.Artists : track.Artist.Split(InternalValueSeparator); + 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.CustomTagDelimiters, libraryOptions.DelimiterWhitelist)).ToArray(); + } + foreach (var performer in performers) { if (!string.IsNullOrEmpty(performer)) @@ -285,6 +310,12 @@ namespace MediaBrowser.Providers.MediaInfo 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.CustomTagDelimiters, libraryOptions.DelimiterWhitelist)).ToArray(); + } + audio.Genres = options.ReplaceAllMetadata || audio.Genres == null || audio.Genres.Length == 0 ? genres : audio.Genres; @@ -379,5 +410,31 @@ namespace MediaBrowser.Providers.MediaInfo 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; + } } } -- cgit v1.2.3 From 00ca4abbe1138d880fca36e1f99da14e6fab252a Mon Sep 17 00:00:00 2001 From: gnattu Date: Tue, 24 Sep 2024 05:15:46 +0800 Subject: Sanitize CustomTagDelimiters server side The API requires an array type and does not support runtime generated default value. Use server side helper function to sanitize it into char. --- MediaBrowser.Model/Configuration/LibraryOptions.cs | 20 +++++++++++++++++--- MediaBrowser.Providers/MediaInfo/AudioFileProber.cs | 6 +++--- 2 files changed, 20 insertions(+), 6 deletions(-) (limited to 'MediaBrowser.Model/Configuration/LibraryOptions.cs') diff --git a/MediaBrowser.Model/Configuration/LibraryOptions.cs b/MediaBrowser.Model/Configuration/LibraryOptions.cs index 04283cc9e..b0fcc2d0a 100644 --- a/MediaBrowser.Model/Configuration/LibraryOptions.cs +++ b/MediaBrowser.Model/Configuration/LibraryOptions.cs @@ -2,12 +2,13 @@ using System; using System.ComponentModel; +using System.Linq; namespace MediaBrowser.Model.Configuration { public class LibraryOptions { - private static readonly char[] _defaultTagDelimiters = ['/', '|', ';', '\\']; + private static readonly string[] _defaultTagDelimiters = ["/", "|", ";", "\\"]; public LibraryOptions() { @@ -126,8 +127,7 @@ namespace MediaBrowser.Model.Configuration [DefaultValue(false)] public bool UseCustomTagDelimiters { get; set; } - [DefaultValue(typeof(LibraryOptions), nameof(_defaultTagDelimiters))] - public char[] CustomTagDelimiters { get; set; } + public string[] CustomTagDelimiters { get; set; } public string[] DelimiterWhitelist { get; set; } @@ -149,5 +149,19 @@ namespace MediaBrowser.Model.Configuration return null; } + + public char[] GetCustomTagDelimiters() + { + return CustomTagDelimiters.Select(x => + { + var isChar = char.TryParse(x, out var c); + if (isChar) + { + return c; + } + + return null; + }).Where(x => x is not null).Select(x => x!.Value).ToArray(); + } } } diff --git a/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs b/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs index 80bb1a514..2e0d21c6a 100644 --- a/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs +++ b/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs @@ -178,7 +178,7 @@ namespace MediaBrowser.Providers.MediaInfo if (libraryOptions.UseCustomTagDelimiters) { - albumArtists = albumArtists.SelectMany(a => SplitWithCustomDelimiter(a, libraryOptions.CustomTagDelimiters, libraryOptions.DelimiterWhitelist)).ToArray(); + albumArtists = albumArtists.SelectMany(a => SplitWithCustomDelimiter(a, libraryOptions.GetCustomTagDelimiters(), libraryOptions.DelimiterWhitelist)).ToArray(); } foreach (var albumArtist in albumArtists) @@ -210,7 +210,7 @@ namespace MediaBrowser.Providers.MediaInfo if (libraryOptions.UseCustomTagDelimiters) { - performers = performers.SelectMany(p => SplitWithCustomDelimiter(p, libraryOptions.CustomTagDelimiters, libraryOptions.DelimiterWhitelist)).ToArray(); + performers = performers.SelectMany(p => SplitWithCustomDelimiter(p, libraryOptions.GetCustomTagDelimiters(), libraryOptions.DelimiterWhitelist)).ToArray(); } foreach (var performer in performers) @@ -313,7 +313,7 @@ namespace MediaBrowser.Providers.MediaInfo if (libraryOptions.UseCustomTagDelimiters) { - genres = genres.SelectMany(g => SplitWithCustomDelimiter(g, libraryOptions.CustomTagDelimiters, libraryOptions.DelimiterWhitelist)).ToArray(); + genres = genres.SelectMany(g => SplitWithCustomDelimiter(g, libraryOptions.GetCustomTagDelimiters(), libraryOptions.DelimiterWhitelist)).ToArray(); } audio.Genres = options.ReplaceAllMetadata || audio.Genres is null || audio.Genres.Length == 0 -- cgit v1.2.3 From 0ffddacf11795b8a50606b6515c1dc6828ad8dd0 Mon Sep 17 00:00:00 2001 From: gnattu Date: Tue, 24 Sep 2024 12:36:05 +0800 Subject: Move GetCustomTagDelimiters to Extension --- MediaBrowser.Model/Configuration/LibraryOptions.cs | 14 ---------- .../Extensions/LibraryOptionsExtension.cs | 32 ++++++++++++++++++++++ .../MediaInfo/AudioFileProber.cs | 1 + 3 files changed, 33 insertions(+), 14 deletions(-) create mode 100644 MediaBrowser.Model/Extensions/LibraryOptionsExtension.cs (limited to 'MediaBrowser.Model/Configuration/LibraryOptions.cs') diff --git a/MediaBrowser.Model/Configuration/LibraryOptions.cs b/MediaBrowser.Model/Configuration/LibraryOptions.cs index b0fcc2d0a..6054ba34e 100644 --- a/MediaBrowser.Model/Configuration/LibraryOptions.cs +++ b/MediaBrowser.Model/Configuration/LibraryOptions.cs @@ -149,19 +149,5 @@ namespace MediaBrowser.Model.Configuration return null; } - - public char[] GetCustomTagDelimiters() - { - return CustomTagDelimiters.Select(x => - { - var isChar = char.TryParse(x, out var c); - if (isChar) - { - return c; - } - - return null; - }).Where(x => x is not null).Select(x => x!.Value).ToArray(); - } } } diff --git a/MediaBrowser.Model/Extensions/LibraryOptionsExtension.cs b/MediaBrowser.Model/Extensions/LibraryOptionsExtension.cs new file mode 100644 index 000000000..4a814f22a --- /dev/null +++ b/MediaBrowser.Model/Extensions/LibraryOptionsExtension.cs @@ -0,0 +1,32 @@ +using System; +using System.Linq; +using MediaBrowser.Model.Configuration; + +namespace MediaBrowser.Model.Extensions; + +/// +/// Extensions for . +/// +public static class LibraryOptionsExtension +{ + /// + /// Get the custom tag delimiters. + /// + /// This LibraryOptions. + /// CustomTagDelimiters in char[]. + public static char[] GetCustomTagDelimiters(this LibraryOptions options) + { + ArgumentNullException.ThrowIfNull(options); + + return options.CustomTagDelimiters.Select(x => + { + var isChar = char.TryParse(x, out var c); + if (isChar) + { + return c; + } + + return null; + }).Where(x => x is not null).Select(x => x!.Value).ToArray(); + } +} diff --git a/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs b/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs index 2e0d21c6a..cb233d31e 100644 --- a/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs +++ b/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs @@ -16,6 +16,7 @@ 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; -- cgit v1.2.3