diff options
| author | Marc Brooks <IDisposable@gmail.com> | 2025-02-03 19:48:59 -0600 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-02-03 19:48:59 -0600 |
| commit | e8cbcde02ebd930a5eeb6c95e0875a9e30acb3e8 (patch) | |
| tree | 2ecd43f232012c8f037f4cd6fee4168e46d01aa3 /MediaBrowser.Providers | |
| parent | 6dc61a430ba3a8480399309f277e5debfd6403ba (diff) | |
| parent | d376b5fbc7cf3ae7440a606a9e885d70605956bd (diff) | |
Merge branch 'master' into sort-nfo-data
Diffstat (limited to 'MediaBrowser.Providers')
28 files changed, 287 insertions, 201 deletions
diff --git a/MediaBrowser.Providers/BoxSets/BoxSetMetadataService.cs b/MediaBrowser.Providers/BoxSets/BoxSetMetadataService.cs index df9d15ec2..b51ab4c08 100644 --- a/MediaBrowser.Providers/BoxSets/BoxSetMetadataService.cs +++ b/MediaBrowser.Providers/BoxSets/BoxSetMetadataService.cs @@ -39,7 +39,7 @@ namespace MediaBrowser.Providers.BoxSets protected override bool EnableUpdatingPremiereDateFromChildren => true; /// <inheritdoc /> - protected override IList<BaseItem> GetChildrenForMetadataUpdates(BoxSet item) + protected override IReadOnlyList<BaseItem> GetChildrenForMetadataUpdates(BoxSet item) { return item.GetLinkedChildren(); } @@ -54,7 +54,14 @@ namespace MediaBrowser.Providers.BoxSets if (mergeMetadataSettings) { - targetItem.LinkedChildren = sourceItem.LinkedChildren; + if (replaceData || targetItem.LinkedChildren.Length == 0) + { + targetItem.LinkedChildren = sourceItem.LinkedChildren; + } + else + { + targetItem.LinkedChildren = sourceItem.LinkedChildren.Concat(targetItem.LinkedChildren).Distinct().ToArray(); + } } } diff --git a/MediaBrowser.Providers/Chapters/ChapterManager.cs b/MediaBrowser.Providers/Chapters/ChapterManager.cs deleted file mode 100644 index 3cbfe7d4d..000000000 --- a/MediaBrowser.Providers/Chapters/ChapterManager.cs +++ /dev/null @@ -1,26 +0,0 @@ -#pragma warning disable CS1591 - -using System; -using System.Collections.Generic; -using MediaBrowser.Controller.Chapters; -using MediaBrowser.Controller.Persistence; -using MediaBrowser.Model.Entities; - -namespace MediaBrowser.Providers.Chapters -{ - public class ChapterManager : IChapterManager - { - private readonly IItemRepository _itemRepo; - - public ChapterManager(IItemRepository itemRepo) - { - _itemRepo = itemRepo; - } - - /// <inheritdoc /> - public void SaveChapters(Guid itemId, IReadOnlyList<ChapterInfo> chapters) - { - _itemRepo.SaveChapters(itemId, chapters); - } - } -} diff --git a/MediaBrowser.Providers/Lyric/LyricScheduledTask.cs b/MediaBrowser.Providers/Lyric/LyricScheduledTask.cs index 89d71e172..73912b579 100644 --- a/MediaBrowser.Providers/Lyric/LyricScheduledTask.cs +++ b/MediaBrowser.Providers/Lyric/LyricScheduledTask.cs @@ -162,7 +162,7 @@ public class LyricScheduledTask : IScheduledTask [ new TaskTriggerInfo { - Type = TaskTriggerInfo.TriggerInterval, + Type = TaskTriggerInfoType.IntervalTrigger, IntervalTicks = TimeSpan.FromHours(24).Ticks } ]; diff --git a/MediaBrowser.Providers/Manager/ImageSaver.cs b/MediaBrowser.Providers/Manager/ImageSaver.cs index 9a676cb2e..8f6aa2db3 100644 --- a/MediaBrowser.Providers/Manager/ImageSaver.cs +++ b/MediaBrowser.Providers/Manager/ImageSaver.cs @@ -291,6 +291,7 @@ namespace MediaBrowser.Providers.Manager var fileStreamOptions = AsyncFile.WriteOptions; fileStreamOptions.Mode = FileMode.Create; + fileStreamOptions.Options = FileOptions.WriteThrough; if (source.CanSeek) { fileStreamOptions.PreallocationSize = source.Length; diff --git a/MediaBrowser.Providers/Manager/ItemImageProvider.cs b/MediaBrowser.Providers/Manager/ItemImageProvider.cs index 36a7c2fab..64954818a 100644 --- a/MediaBrowser.Providers/Manager/ItemImageProvider.cs +++ b/MediaBrowser.Providers/Manager/ItemImageProvider.cs @@ -229,9 +229,7 @@ namespace MediaBrowser.Providers.Manager { var mimeType = MimeTypes.GetMimeType(response.Path); - var stream = AsyncFile.OpenRead(response.Path); - - await _providerManager.SaveImage(item, stream, mimeType, imageType, null, cancellationToken).ConfigureAwait(false); + await _providerManager.SaveImage(item, response.Path, mimeType, imageType, null, null, cancellationToken).ConfigureAwait(false); } } @@ -387,8 +385,8 @@ namespace MediaBrowser.Providers.Manager item.RemoveImages(images); - // Cleanup old metadata directory for episodes if empty - if (item is Episode) + // Cleanup old metadata directory for episodes if empty, as long as it's not a virtual item + if (item is Episode && !item.IsVirtualItem) { var oldLocalMetadataDirectory = Path.Combine(item.ContainingFolderPath, "metadata"); if (_fileSystem.DirectoryExists(oldLocalMetadataDirectory) && !_fileSystem.GetFiles(oldLocalMetadataDirectory).Any()) diff --git a/MediaBrowser.Providers/Manager/MetadataService.cs b/MediaBrowser.Providers/Manager/MetadataService.cs index 8af4ed2a8..778fbc712 100644 --- a/MediaBrowser.Providers/Manager/MetadataService.cs +++ b/MediaBrowser.Providers/Manager/MetadataService.cs @@ -74,10 +74,11 @@ namespace MediaBrowser.Providers.Manager public virtual async Task<ItemUpdateType> RefreshMetadata(BaseItem item, MetadataRefreshOptions refreshOptions, CancellationToken cancellationToken) { var itemOfType = (TItemType)item; - var updateType = ItemUpdateType.None; - var libraryOptions = LibraryManager.GetLibraryOptions(item); + var isFirstRefresh = item.DateLastRefreshed == default; + var hasRefreshedMetadata = true; + var hasRefreshedImages = true; var requiresRefresh = libraryOptions.AutomaticRefreshIntervalDays > 0 && (DateTime.UtcNow - item.DateLastRefreshed).TotalDays >= libraryOptions.AutomaticRefreshIntervalDays; @@ -131,9 +132,10 @@ namespace MediaBrowser.Providers.Manager People = LibraryManager.GetPeople(item) }; - bool hasRefreshedMetadata = true; - bool hasRefreshedImages = true; - var isFirstRefresh = item.DateLastRefreshed == default; + var beforeSaveResult = BeforeSave(itemOfType, isFirstRefresh || refreshOptions.ReplaceAllMetadata || refreshOptions.MetadataRefreshMode == MetadataRefreshMode.FullRefresh || requiresRefresh || refreshOptions.ForceSave, updateType); + updateType |= beforeSaveResult; + + updateType = await SaveInternal(item, refreshOptions, updateType, isFirstRefresh, requiresRefresh, metadataResult, cancellationToken).ConfigureAwait(false); // Next run metadata providers if (refreshOptions.MetadataRefreshMode != MetadataRefreshMode.None) @@ -188,43 +190,43 @@ namespace MediaBrowser.Providers.Manager } } - var beforeSaveResult = BeforeSave(itemOfType, isFirstRefresh || refreshOptions.ReplaceAllMetadata || refreshOptions.MetadataRefreshMode == MetadataRefreshMode.FullRefresh || requiresRefresh || refreshOptions.ForceSave, updateType); - updateType |= beforeSaveResult; + if (hasRefreshedMetadata && hasRefreshedImages) + { + item.DateLastRefreshed = DateTime.UtcNow; + } + + updateType = await SaveInternal(item, refreshOptions, updateType, isFirstRefresh, requiresRefresh, metadataResult, cancellationToken).ConfigureAwait(false); + + await AfterMetadataRefresh(itemOfType, refreshOptions, cancellationToken).ConfigureAwait(false); - // Save if changes were made, or it's never been saved before - if (refreshOptions.ForceSave || updateType > ItemUpdateType.None || isFirstRefresh || refreshOptions.ReplaceAllMetadata || requiresRefresh) + return updateType; + + async Task<ItemUpdateType> SaveInternal(BaseItem item, MetadataRefreshOptions refreshOptions, ItemUpdateType updateType, bool isFirstRefresh, bool requiresRefresh, MetadataResult<TItemType> metadataResult, CancellationToken cancellationToken) { - if (item.IsFileProtocol) + // Save if changes were made, or it's never been saved before + if (refreshOptions.ForceSave || updateType > ItemUpdateType.None || isFirstRefresh || refreshOptions.ReplaceAllMetadata || requiresRefresh) { - var file = TryGetFile(item.Path, refreshOptions.DirectoryService); - if (file is not null) + if (item.IsFileProtocol) { - item.DateModified = file.LastWriteTimeUtc; + var file = TryGetFile(item.Path, refreshOptions.DirectoryService); + if (file is not null) + { + item.DateModified = file.LastWriteTimeUtc; + } } - } - // If any of these properties are set then make sure the updateType is not None, just to force everything to save - if (refreshOptions.ForceSave || refreshOptions.ReplaceAllMetadata) - { - updateType |= ItemUpdateType.MetadataDownload; - } + // If any of these properties are set then make sure the updateType is not None, just to force everything to save + if (refreshOptions.ForceSave || refreshOptions.ReplaceAllMetadata) + { + updateType |= ItemUpdateType.MetadataDownload; + } - if (hasRefreshedMetadata && hasRefreshedImages) - { - item.DateLastRefreshed = DateTime.UtcNow; - } - else - { - item.DateLastRefreshed = default; + // Save to database + await SaveItemAsync(metadataResult, updateType, cancellationToken).ConfigureAwait(false); } - // Save to database - await SaveItemAsync(metadataResult, updateType, cancellationToken).ConfigureAwait(false); + return updateType; } - - await AfterMetadataRefresh(itemOfType, refreshOptions, cancellationToken).ConfigureAwait(false); - - return updateType; } private void ApplySearchResult(ItemLookupInfo lookupInfo, RemoteSearchResult result) @@ -322,17 +324,17 @@ namespace MediaBrowser.Providers.Manager return false; } - protected virtual IList<BaseItem> GetChildrenForMetadataUpdates(TItemType item) + protected virtual IReadOnlyList<BaseItem> GetChildrenForMetadataUpdates(TItemType item) { if (item is Folder folder) { return folder.GetRecursiveChildren(); } - return Array.Empty<BaseItem>(); + return []; } - protected virtual ItemUpdateType UpdateMetadataFromChildren(TItemType item, IList<BaseItem> children, bool isFullRefresh, ItemUpdateType currentUpdateType) + protected virtual ItemUpdateType UpdateMetadataFromChildren(TItemType item, IReadOnlyList<BaseItem> children, bool isFullRefresh, ItemUpdateType currentUpdateType) { var updateType = ItemUpdateType.None; @@ -371,7 +373,7 @@ namespace MediaBrowser.Providers.Manager return updateType; } - private ItemUpdateType UpdateCumulativeRunTimeTicks(TItemType item, IList<BaseItem> children) + private ItemUpdateType UpdateCumulativeRunTimeTicks(TItemType item, IReadOnlyList<BaseItem> children) { if (item is Folder folder && folder.SupportsCumulativeRunTimeTicks) { @@ -395,7 +397,7 @@ namespace MediaBrowser.Providers.Manager return ItemUpdateType.None; } - private ItemUpdateType UpdateDateLastMediaAdded(TItemType item, IList<BaseItem> children) + private ItemUpdateType UpdateDateLastMediaAdded(TItemType item, IReadOnlyList<BaseItem> children) { var updateType = ItemUpdateType.None; @@ -429,7 +431,7 @@ namespace MediaBrowser.Providers.Manager return updateType; } - private ItemUpdateType UpdatePremiereDate(TItemType item, IList<BaseItem> children) + private ItemUpdateType UpdatePremiereDate(TItemType item, IReadOnlyList<BaseItem> children) { var updateType = ItemUpdateType.None; @@ -467,7 +469,7 @@ namespace MediaBrowser.Providers.Manager return updateType; } - private ItemUpdateType UpdateGenres(TItemType item, IList<BaseItem> children) + private ItemUpdateType UpdateGenres(TItemType item, IReadOnlyList<BaseItem> children) { var updateType = ItemUpdateType.None; @@ -488,7 +490,7 @@ namespace MediaBrowser.Providers.Manager return updateType; } - private ItemUpdateType UpdateStudios(TItemType item, IList<BaseItem> children) + private ItemUpdateType UpdateStudios(TItemType item, IReadOnlyList<BaseItem> children) { var updateType = ItemUpdateType.None; @@ -509,7 +511,7 @@ namespace MediaBrowser.Providers.Manager return updateType; } - private ItemUpdateType UpdateOfficialRating(TItemType item, IList<BaseItem> children) + private ItemUpdateType UpdateOfficialRating(TItemType item, IReadOnlyList<BaseItem> children) { var updateType = ItemUpdateType.None; @@ -675,6 +677,7 @@ namespace MediaBrowser.Providers.Manager }; temp.Item.Path = item.Path; temp.Item.Id = item.Id; + temp.Item.ParentIndexNumber = item.ParentIndexNumber; temp.Item.PreferredMetadataCountryCode = item.PreferredMetadataCountryCode; temp.Item.PreferredMetadataLanguage = item.PreferredMetadataLanguage; @@ -728,7 +731,7 @@ namespace MediaBrowser.Providers.Manager refreshResult.UpdateType |= ItemUpdateType.ImageUpdate; } - MergeData(localItem, temp, Array.Empty<MetadataField>(), false, true); + MergeData(localItem, temp, [], false, true); refreshResult.UpdateType |= ItemUpdateType.MetadataImport; break; @@ -768,7 +771,7 @@ namespace MediaBrowser.Providers.Manager if (!options.RemoveOldMetadata) { // Add existing metadata to provider result if it does not exist there - MergeData(metadata, temp, Array.Empty<MetadataField>(), false, false); + MergeData(metadata, temp, [], false, false); } if (isLocalLocked) @@ -837,7 +840,7 @@ namespace MediaBrowser.Providers.Manager { result.Provider = provider.Name; - MergeData(result, temp, Array.Empty<MetadataField>(), replaceData, false); + MergeData(result, temp, [], replaceData, false); MergeNewData(temp.Item, id); refreshResult.UpdateType |= ItemUpdateType.MetadataDownload; @@ -1141,13 +1144,8 @@ namespace MediaBrowser.Providers.Manager } } - private static void MergePeople(List<PersonInfo> source, List<PersonInfo> target) + private static void MergePeople(IReadOnlyList<PersonInfo> source, IReadOnlyList<PersonInfo> target) { - if (target is null) - { - target = new List<PersonInfo>(); - } - foreach (var person in target) { var normalizedName = person.Name.RemoveDiacritics(); diff --git a/MediaBrowser.Providers/Manager/ProviderManager.cs b/MediaBrowser.Providers/Manager/ProviderManager.cs index 81a9af68b..6813cfa91 100644 --- a/MediaBrowser.Providers/Manager/ProviderManager.cs +++ b/MediaBrowser.Providers/Manager/ProviderManager.cs @@ -7,6 +7,7 @@ using System.Linq; using System.Net; using System.Net.Http; using System.Net.Mime; +using System.Runtime.ExceptionServices; using System.Threading; using System.Threading.Tasks; using AsyncKeyedLock; @@ -47,7 +48,7 @@ namespace MediaBrowser.Providers.Manager /// </summary> public class ProviderManager : IProviderManager, IDisposable { - private readonly object _refreshQueueLock = new(); + private readonly Lock _refreshQueueLock = new(); private readonly ILogger<ProviderManager> _logger; private readonly IHttpClientFactory _httpClientFactory; private readonly ILibraryMonitor _libraryMonitor; @@ -199,12 +200,20 @@ namespace MediaBrowser.Providers.Manager // TODO: Isolate this hack into the tvh plugin if (string.IsNullOrEmpty(contentType)) { + // Special case for imagecache if (url.Contains("/imagecache/", StringComparison.OrdinalIgnoreCase)) { contentType = MediaTypeNames.Image.Png; } else { + // Deduce content type from file extension + contentType = MimeTypes.GetMimeType(new Uri(url).GetLeftPart(UriPartial.Path)); + } + + // Throw if we still can't determine the content type + if (string.IsNullOrEmpty(contentType)) + { throw new HttpRequestException("Invalid image received: contentType not set.", null, response.StatusCode); } } @@ -251,15 +260,31 @@ namespace MediaBrowser.Providers.Manager } /// <inheritdoc/> - public Task SaveImage(BaseItem item, string source, string mimeType, ImageType type, int? imageIndex, bool? saveLocallyWithMedia, CancellationToken cancellationToken) + public async Task SaveImage(BaseItem item, string source, string mimeType, ImageType type, int? imageIndex, bool? saveLocallyWithMedia, CancellationToken cancellationToken) { if (string.IsNullOrWhiteSpace(source)) { throw new ArgumentNullException(nameof(source)); } - var fileStream = AsyncFile.OpenRead(source); - return new ImageSaver(_configurationManager, _libraryMonitor, _fileSystem, _logger).SaveImage(item, fileStream, mimeType, type, imageIndex, saveLocallyWithMedia, cancellationToken); + try + { + var fileStream = AsyncFile.OpenRead(source); + await new ImageSaver(_configurationManager, _libraryMonitor, _fileSystem, _logger) + .SaveImage(item, fileStream, mimeType, type, imageIndex, saveLocallyWithMedia, cancellationToken) + .ConfigureAwait(false); + } + finally + { + try + { + File.Delete(source); + } + catch (Exception ex) + { + _logger.LogError(ex, "Source file {Source} not found or in use, skip removing", source); + } + } } /// <inheritdoc/> @@ -1016,7 +1041,6 @@ namespace MediaBrowser.Providers.Manager /// <inheritdoc/> public void QueueRefresh(Guid itemId, MetadataRefreshOptions options, RefreshPriority priority) { - ArgumentNullException.ThrowIfNull(itemId); if (itemId.IsEmpty()) { throw new ArgumentException("Guid can't be empty", nameof(itemId)); diff --git a/MediaBrowser.Providers/MediaBrowser.Providers.csproj b/MediaBrowser.Providers/MediaBrowser.Providers.csproj index 9a65852f0..34b3104b0 100644 --- a/MediaBrowser.Providers/MediaBrowser.Providers.csproj +++ b/MediaBrowser.Providers/MediaBrowser.Providers.csproj @@ -23,15 +23,14 @@ <PackageReference Include="Microsoft.Extensions.Http" /> <PackageReference Include="Newtonsoft.Json" /> <PackageReference Include="PlaylistsNET" /> - <PackageReference Include="z440.atl.core"/> + <PackageReference Include="z440.atl.core" /> <PackageReference Include="TMDbLib" /> </ItemGroup> <PropertyGroup> - <TargetFramework>net8.0</TargetFramework> + <TargetFramework>net9.0</TargetFramework> <GenerateAssemblyInfo>false</GenerateAssemblyInfo> <GenerateDocumentationFile>true</GenerateDocumentationFile> - <CodeAnalysisRuleSet>../jellyfin.ruleset</CodeAnalysisRuleSet> </PropertyGroup> <!-- Code Analyzers --> diff --git a/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs b/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs index d113cabc7..1308e06f9 100644 --- a/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs +++ b/MediaBrowser.Providers/MediaInfo/AudioFileProber.cs @@ -17,6 +17,7 @@ using MediaBrowser.Controller.Sorting; using MediaBrowser.Model.Dlna; using MediaBrowser.Model.Dto; using MediaBrowser.Model.Entities; +using MediaBrowser.Model.Extensions; using MediaBrowser.Model.MediaInfo; using Microsoft.Extensions.Logging; @@ -28,6 +29,7 @@ namespace MediaBrowser.Providers.MediaInfo public class AudioFileProber { private const char InternalValueSeparator = '\u001F'; + private readonly IMediaEncoder _mediaEncoder; private readonly IItemRepository _itemRepo; private readonly ILibraryManager _libraryManager; @@ -35,6 +37,7 @@ namespace MediaBrowser.Providers.MediaInfo private readonly IMediaSourceManager _mediaSourceManager; private readonly LyricResolver _lyricResolver; private readonly ILyricManager _lyricManager; + private readonly IMediaStreamRepository _mediaStreamRepository; /// <summary> /// Initializes a new instance of the <see cref="AudioFileProber"/> class. @@ -46,6 +49,7 @@ namespace MediaBrowser.Providers.MediaInfo /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param> /// <param name="lyricResolver">Instance of the <see cref="LyricResolver"/> interface.</param> /// <param name="lyricManager">Instance of the <see cref="ILyricManager"/> interface.</param> + /// <param name="mediaStreamRepository">Instance of the <see cref="IMediaStreamRepository"/>.</param> public AudioFileProber( ILogger<AudioFileProber> logger, IMediaSourceManager mediaSourceManager, @@ -53,7 +57,8 @@ namespace MediaBrowser.Providers.MediaInfo IItemRepository itemRepo, ILibraryManager libraryManager, LyricResolver lyricResolver, - ILyricManager lyricManager) + ILyricManager lyricManager, + IMediaStreamRepository mediaStreamRepository) { _mediaEncoder = mediaEncoder; _itemRepo = itemRepo; @@ -62,7 +67,10 @@ namespace MediaBrowser.Providers.MediaInfo _mediaSourceManager = mediaSourceManager; _lyricResolver = lyricResolver; _lyricManager = lyricManager; + _mediaStreamRepository = mediaStreamRepository; ATL.Settings.DisplayValueSeparator = InternalValueSeparator; + ATL.Settings.UseFileNameWhenNoTitle = false; + ATL.Settings.ID3v2_separatev2v3Values = false; } /// <summary> @@ -146,7 +154,7 @@ namespace MediaBrowser.Providers.MediaInfo audio.HasLyrics = mediaStreams.Any(s => s.Type == MediaStreamType.Lyric); - _itemRepo.SaveMediaStreams(audio.Id, mediaStreams, cancellationToken); + _mediaStreamRepository.SaveMediaStreams(audio.Id, mediaStreams, cancellationToken); } /// <summary> @@ -161,12 +169,13 @@ namespace MediaBrowser.Providers.MediaInfo 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 - if (track.MetadataFormats.All(mf => mf.Equals(ATL.Factory.UNKNOWN_FORMAT))) + if (track.MetadataFormats + .All(mf => string.Equals(mf.ShortName, "ID3v1", StringComparison.OrdinalIgnoreCase))) { - track.Title = mediaInfo.Name; + _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; @@ -175,11 +184,11 @@ namespace MediaBrowser.Providers.MediaInfo if (audio.SupportsPeople && !audio.LockedFields.Contains(MetadataField.Cast)) { var people = new List<PersonInfo>(); - var albumArtists = string.IsNullOrEmpty(track.AlbumArtist) ? mediaInfo.AlbumArtists : track.AlbumArtist.Split(InternalValueSeparator); + var albumArtists = string.IsNullOrEmpty(track.AlbumArtist) ? [] : track.AlbumArtist.Split(InternalValueSeparator); 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) @@ -206,12 +215,12 @@ namespace MediaBrowser.Providers.MediaInfo if (performers is null || performers.Length == 0) { - performers = string.IsNullOrEmpty(track.Artist) ? mediaInfo.Artists : track.Artist.Split(InternalValueSeparator); + performers = string.IsNullOrEmpty(track.Artist) ? [] : track.Artist.Split(InternalValueSeparator); } 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) @@ -310,11 +319,11 @@ namespace MediaBrowser.Providers.MediaInfo if (!audio.LockedFields.Contains(MetadataField.Genres)) { - var genres = string.IsNullOrEmpty(track.Genre) ? mediaInfo.Genres : track.Genre.Split(InternalValueSeparator); + var genres = string.IsNullOrEmpty(track.Genre) ? [] : track.Genre.Split(InternalValueSeparator).Distinct(StringComparer.OrdinalIgnoreCase).ToArray(); 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(); } genres = genres.Trimmed().Distinct(StringComparer.OrdinalIgnoreCase).ToArray(); @@ -345,7 +354,8 @@ namespace MediaBrowser.Providers.MediaInfo || track.AdditionalFields.TryGetValue("MusicBrainz Artist Id", out musicBrainzArtistTag)) && !string.IsNullOrEmpty(musicBrainzArtistTag)) { - audio.TrySetProviderId(MetadataProvider.MusicBrainzArtist, musicBrainzArtistTag); + var id = GetFirstMusicBrainzId(musicBrainzArtistTag, libraryOptions.UseCustomTagDelimiters, libraryOptions.GetCustomTagDelimiters(), libraryOptions.DelimiterWhitelist); + audio.TrySetProviderId(MetadataProvider.MusicBrainzArtist, id); } } @@ -355,7 +365,8 @@ namespace MediaBrowser.Providers.MediaInfo || track.AdditionalFields.TryGetValue("MusicBrainz Album Artist Id", out musicBrainzReleaseArtistIdTag)) && !string.IsNullOrEmpty(musicBrainzReleaseArtistIdTag)) { - audio.TrySetProviderId(MetadataProvider.MusicBrainzAlbumArtist, musicBrainzReleaseArtistIdTag); + var id = GetFirstMusicBrainzId(musicBrainzReleaseArtistIdTag, libraryOptions.UseCustomTagDelimiters, libraryOptions.GetCustomTagDelimiters(), libraryOptions.DelimiterWhitelist); + audio.TrySetProviderId(MetadataProvider.MusicBrainzAlbumArtist, id); } } @@ -365,7 +376,8 @@ namespace MediaBrowser.Providers.MediaInfo || track.AdditionalFields.TryGetValue("MusicBrainz Album Id", out musicBrainzReleaseIdTag)) && !string.IsNullOrEmpty(musicBrainzReleaseIdTag)) { - audio.TrySetProviderId(MetadataProvider.MusicBrainzAlbum, musicBrainzReleaseIdTag); + var id = GetFirstMusicBrainzId(musicBrainzReleaseIdTag, libraryOptions.UseCustomTagDelimiters, libraryOptions.GetCustomTagDelimiters(), libraryOptions.DelimiterWhitelist); + audio.TrySetProviderId(MetadataProvider.MusicBrainzAlbum, id); } } @@ -375,7 +387,8 @@ namespace MediaBrowser.Providers.MediaInfo || track.AdditionalFields.TryGetValue("MusicBrainz Release Group Id", out musicBrainzReleaseGroupIdTag)) && !string.IsNullOrEmpty(musicBrainzReleaseGroupIdTag)) { - audio.TrySetProviderId(MetadataProvider.MusicBrainzReleaseGroup, musicBrainzReleaseGroupIdTag); + var id = GetFirstMusicBrainzId(musicBrainzReleaseGroupIdTag, libraryOptions.UseCustomTagDelimiters, libraryOptions.GetCustomTagDelimiters(), libraryOptions.DelimiterWhitelist); + audio.TrySetProviderId(MetadataProvider.MusicBrainzReleaseGroup, id); } } @@ -385,7 +398,8 @@ namespace MediaBrowser.Providers.MediaInfo || track.AdditionalFields.TryGetValue("MusicBrainz Release Track Id", out trackMbId)) && !string.IsNullOrEmpty(trackMbId)) { - audio.TrySetProviderId(MetadataProvider.MusicBrainzTrack, trackMbId); + var id = GetFirstMusicBrainzId(trackMbId, libraryOptions.UseCustomTagDelimiters, libraryOptions.GetCustomTagDelimiters(), libraryOptions.DelimiterWhitelist); + audio.TrySetProviderId(MetadataProvider.MusicBrainzTrack, id); } } @@ -439,5 +453,18 @@ namespace MediaBrowser.Providers.MediaInfo return items; } + + // MusicBrainz IDs are multi-value tags, so we need to split them + // However, our current provider can only have one single ID, which means we need to pick the first one + private string? GetFirstMusicBrainzId(string tag, bool useCustomTagDelimiters, char[] tagDelimiters, string[] whitelist) + { + var val = tag.Split(InternalValueSeparator).FirstOrDefault(); + if (val is not null && useCustomTagDelimiters) + { + val = SplitWithCustomDelimiter(val, tagDelimiters, whitelist).FirstOrDefault(); + } + + return val; + } } } diff --git a/MediaBrowser.Providers/MediaInfo/AudioImageProvider.cs b/MediaBrowser.Providers/MediaInfo/AudioImageProvider.cs index d1c0ddb37..cc2b3face 100644 --- a/MediaBrowser.Providers/MediaInfo/AudioImageProvider.cs +++ b/MediaBrowser.Providers/MediaInfo/AudioImageProvider.cs @@ -1,4 +1,4 @@ -#nullable disable +#pragma warning disable CA1826 // CA1826 Do not use Enumerable methods on Indexable collections. using System; using System.Collections.Generic; @@ -74,18 +74,17 @@ namespace MediaBrowser.Providers.MediaInfo return GetImage((Audio)item, imageStreams, cancellationToken); } - private async Task<DynamicImageResponse> GetImage(Audio item, List<MediaStream> imageStreams, CancellationToken cancellationToken) + private async Task<DynamicImageResponse> GetImage(Audio item, IReadOnlyList<MediaStream> imageStreams, CancellationToken cancellationToken) { var path = GetAudioImagePath(item); if (!File.Exists(path)) { - Directory.CreateDirectory(Path.GetDirectoryName(path)); - + var directoryName = Path.GetDirectoryName(path) ?? throw new InvalidOperationException($"Invalid path '{path}'"); + Directory.CreateDirectory(directoryName); var imageStream = imageStreams.FirstOrDefault(i => (i.Comment ?? string.Empty).Contains("front", StringComparison.OrdinalIgnoreCase)) ?? imageStreams.FirstOrDefault(i => (i.Comment ?? string.Empty).Contains("cover", StringComparison.OrdinalIgnoreCase)) ?? imageStreams.FirstOrDefault(); - var imageStreamIndex = imageStream?.Index; var tempFile = await _mediaEncoder.ExtractAudioImage(item.Path, imageStreamIndex, cancellationToken).ConfigureAwait(false); diff --git a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs index f486f150d..228c51959 100644 --- a/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs +++ b/MediaBrowser.Providers/MediaInfo/FFProbeVideoInfo.cs @@ -32,6 +32,7 @@ namespace MediaBrowser.Providers.MediaInfo public class FFProbeVideoInfo { private readonly ILogger<FFProbeVideoInfo> _logger; + private readonly IMediaSourceManager _mediaSourceManager; private readonly IMediaEncoder _mediaEncoder; private readonly IItemRepository _itemRepo; private readonly IBlurayExaminer _blurayExaminer; @@ -39,11 +40,12 @@ namespace MediaBrowser.Providers.MediaInfo private readonly IEncodingManager _encodingManager; private readonly IServerConfigurationManager _config; private readonly ISubtitleManager _subtitleManager; - private readonly IChapterManager _chapterManager; + private readonly IChapterRepository _chapterManager; private readonly ILibraryManager _libraryManager; private readonly AudioResolver _audioResolver; private readonly SubtitleResolver _subtitleResolver; - private readonly IMediaSourceManager _mediaSourceManager; + private readonly IMediaAttachmentRepository _mediaAttachmentRepository; + private readonly IMediaStreamRepository _mediaStreamRepository; public FFProbeVideoInfo( ILogger<FFProbeVideoInfo> logger, @@ -55,10 +57,12 @@ namespace MediaBrowser.Providers.MediaInfo IEncodingManager encodingManager, IServerConfigurationManager config, ISubtitleManager subtitleManager, - IChapterManager chapterManager, + IChapterRepository chapterManager, ILibraryManager libraryManager, AudioResolver audioResolver, - SubtitleResolver subtitleResolver) + SubtitleResolver subtitleResolver, + IMediaAttachmentRepository mediaAttachmentRepository, + IMediaStreamRepository mediaStreamRepository) { _logger = logger; _mediaSourceManager = mediaSourceManager; @@ -73,6 +77,9 @@ namespace MediaBrowser.Providers.MediaInfo _libraryManager = libraryManager; _audioResolver = audioResolver; _subtitleResolver = subtitleResolver; + _mediaAttachmentRepository = mediaAttachmentRepository; + _mediaStreamRepository = mediaStreamRepository; + _mediaStreamRepository = mediaStreamRepository; } public async Task<ItemUpdateType> ProbeVideo<T>( @@ -268,11 +275,11 @@ namespace MediaBrowser.Providers.MediaInfo video.HasSubtitles = mediaStreams.Any(i => i.Type == MediaStreamType.Subtitle); - _itemRepo.SaveMediaStreams(video.Id, mediaStreams, cancellationToken); + _mediaStreamRepository.SaveMediaStreams(video.Id, mediaStreams, cancellationToken); if (mediaAttachments.Any()) { - _itemRepo.SaveMediaAttachments(video.Id, mediaAttachments, cancellationToken); + _mediaAttachmentRepository.SaveMediaAttachments(video.Id, mediaAttachments, cancellationToken); } if (options.MetadataRefreshMode == MetadataRefreshMode.FullRefresh @@ -355,7 +362,7 @@ namespace MediaBrowser.Providers.MediaInfo blurayVideoStream.Codec = ffmpegVideoStream.Codec; blurayVideoStream.BitRate = blurayVideoStream.BitRate.GetValueOrDefault() == 0 ? ffmpegVideoStream.BitRate : blurayVideoStream.BitRate; blurayVideoStream.Width = blurayVideoStream.Width.GetValueOrDefault() == 0 ? ffmpegVideoStream.Width : blurayVideoStream.Width; - blurayVideoStream.Height = blurayVideoStream.Height.GetValueOrDefault() == 0 ? ffmpegVideoStream.Width : blurayVideoStream.Height; + blurayVideoStream.Height = blurayVideoStream.Height.GetValueOrDefault() == 0 ? ffmpegVideoStream.Height : blurayVideoStream.Height; blurayVideoStream.ColorRange = ffmpegVideoStream.ColorRange; blurayVideoStream.ColorSpace = ffmpegVideoStream.ColorSpace; blurayVideoStream.ColorTransfer = ffmpegVideoStream.ColorTransfer; @@ -628,7 +635,7 @@ namespace MediaBrowser.Providers.MediaInfo { var runtime = video.RunTimeTicks.GetValueOrDefault(); - // Only process files with a runtime higher than 0 and lower than 12h. The latter are likely corrupted. + // Only process files with a runtime greater than 0 and less than 12h. The latter are likely corrupted. if (runtime < 0 || runtime > TimeSpan.FromHours(12).Ticks) { throw new ArgumentException( diff --git a/MediaBrowser.Providers/MediaInfo/MediaInfoResolver.cs b/MediaBrowser.Providers/MediaInfo/MediaInfoResolver.cs index fbec4e963..f12390bc2 100644 --- a/MediaBrowser.Providers/MediaInfo/MediaInfoResolver.cs +++ b/MediaBrowser.Providers/MediaInfo/MediaInfoResolver.cs @@ -121,7 +121,7 @@ namespace MediaBrowser.Providers.MediaInfo mediaStream.Index = startIndex++; mediaStream.IsDefault = pathInfo.IsDefault || mediaStream.IsDefault; mediaStream.IsForced = pathInfo.IsForced || mediaStream.IsForced; - mediaStream.IsHearingImpaired = pathInfo.IsHearingImpaired || mediaStream.IsHearingImpaired; + mediaStream.IsHearingImpaired = pathInfo.IsHearingImpaired || mediaStream.IsHearingImpaired.GetValueOrDefault(); mediaStreams.Add(MergeMetadata(mediaStream, pathInfo)); } diff --git a/MediaBrowser.Providers/MediaInfo/ProbeProvider.cs b/MediaBrowser.Providers/MediaInfo/ProbeProvider.cs index 04da8fb88..1c2f8b913 100644 --- a/MediaBrowser.Providers/MediaInfo/ProbeProvider.cs +++ b/MediaBrowser.Providers/MediaInfo/ProbeProvider.cs @@ -61,12 +61,14 @@ namespace MediaBrowser.Providers.MediaInfo /// <param name="encodingManager">Instance of the <see cref="IEncodingManager"/> interface.</param> /// <param name="config">Instance of the <see cref="IServerConfigurationManager"/> interface.</param> /// <param name="subtitleManager">Instance of the <see cref="ISubtitleManager"/> interface.</param> - /// <param name="chapterManager">Instance of the <see cref="IChapterManager"/> interface.</param> + /// <param name="chapterManager">Instance of the <see cref="IChapterRepository"/> interface.</param> /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param> /// <param name="loggerFactory">Instance of the <see cref="ILoggerFactory"/>.</param> /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param> /// <param name="namingOptions">The <see cref="NamingOptions"/>.</param> /// <param name="lyricManager">Instance of the <see cref="ILyricManager"/> interface.</param> + /// <param name="mediaAttachmentRepository">Instance of the <see cref="IMediaAttachmentRepository"/> interface.</param> + /// <param name="mediaStreamRepository">Instance of the <see cref="IMediaStreamRepository"/> interface.</param> public ProbeProvider( IMediaSourceManager mediaSourceManager, IMediaEncoder mediaEncoder, @@ -76,12 +78,14 @@ namespace MediaBrowser.Providers.MediaInfo IEncodingManager encodingManager, IServerConfigurationManager config, ISubtitleManager subtitleManager, - IChapterManager chapterManager, + IChapterRepository chapterManager, ILibraryManager libraryManager, IFileSystem fileSystem, ILoggerFactory loggerFactory, NamingOptions namingOptions, - ILyricManager lyricManager) + ILyricManager lyricManager, + IMediaAttachmentRepository mediaAttachmentRepository, + IMediaStreamRepository mediaStreamRepository) { _logger = loggerFactory.CreateLogger<ProbeProvider>(); _audioResolver = new AudioResolver(loggerFactory.CreateLogger<AudioResolver>(), localization, mediaEncoder, fileSystem, namingOptions); @@ -101,7 +105,9 @@ namespace MediaBrowser.Providers.MediaInfo chapterManager, libraryManager, _audioResolver, - _subtitleResolver); + _subtitleResolver, + mediaAttachmentRepository, + mediaStreamRepository); _audioProber = new AudioFileProber( loggerFactory.CreateLogger<AudioFileProber>(), @@ -110,7 +116,8 @@ namespace MediaBrowser.Providers.MediaInfo itemRepo, libraryManager, _lyricResolver, - lyricManager); + lyricManager, + mediaStreamRepository); } /// <inheritdoc /> diff --git a/MediaBrowser.Providers/MediaInfo/SubtitleDownloader.cs b/MediaBrowser.Providers/MediaInfo/SubtitleDownloader.cs index 20fb4dab9..227f31025 100644 --- a/MediaBrowser.Providers/MediaInfo/SubtitleDownloader.cs +++ b/MediaBrowser.Providers/MediaInfo/SubtitleDownloader.cs @@ -31,7 +31,7 @@ namespace MediaBrowser.Providers.MediaInfo public async Task<List<string>> DownloadSubtitles( Video video, - List<MediaStream> mediaStreams, + IReadOnlyList<MediaStream> mediaStreams, bool skipIfEmbeddedSubtitlesPresent, bool skipIfAudioTrackMatches, bool requirePerfectMatch, @@ -68,7 +68,7 @@ namespace MediaBrowser.Providers.MediaInfo public Task<bool> DownloadSubtitles( Video video, - List<MediaStream> mediaStreams, + IReadOnlyList<MediaStream> mediaStreams, bool skipIfEmbeddedSubtitlesPresent, bool skipIfAudioTrackMatches, bool requirePerfectMatch, @@ -120,7 +120,7 @@ namespace MediaBrowser.Providers.MediaInfo private async Task<bool> DownloadSubtitles( Video video, - List<MediaStream> mediaStreams, + IReadOnlyList<MediaStream> mediaStreams, bool skipIfEmbeddedSubtitlesPresent, bool skipIfAudioTrackMatches, bool requirePerfectMatch, diff --git a/MediaBrowser.Providers/MediaInfo/SubtitleScheduledTask.cs b/MediaBrowser.Providers/MediaInfo/SubtitleScheduledTask.cs index 6eb75891a..938f3cb32 100644 --- a/MediaBrowser.Providers/MediaInfo/SubtitleScheduledTask.cs +++ b/MediaBrowser.Providers/MediaInfo/SubtitleScheduledTask.cs @@ -217,7 +217,7 @@ namespace MediaBrowser.Providers.MediaInfo return new[] { // Every so often - new TaskTriggerInfo { Type = TaskTriggerInfo.TriggerInterval, IntervalTicks = TimeSpan.FromHours(24).Ticks } + new TaskTriggerInfo { Type = TaskTriggerInfoType.IntervalTrigger, IntervalTicks = TimeSpan.FromHours(24).Ticks } }; } } diff --git a/MediaBrowser.Providers/MediaInfo/VideoImageProvider.cs b/MediaBrowser.Providers/MediaInfo/VideoImageProvider.cs index ba7ad4072..3d446053b 100644 --- a/MediaBrowser.Providers/MediaInfo/VideoImageProvider.cs +++ b/MediaBrowser.Providers/MediaInfo/VideoImageProvider.cs @@ -1,3 +1,5 @@ +#pragma warning disable CA1826 // CA1826 Do not use Enumerable methods on Indexable collections. + using System; using System.Collections.Generic; using System.Linq; diff --git a/MediaBrowser.Providers/Music/AlbumMetadataService.cs b/MediaBrowser.Providers/Music/AlbumMetadataService.cs index daebe85d6..0bcc301cb 100644 --- a/MediaBrowser.Providers/Music/AlbumMetadataService.cs +++ b/MediaBrowser.Providers/Music/AlbumMetadataService.cs @@ -47,11 +47,11 @@ namespace MediaBrowser.Providers.Music protected override bool EnableUpdatingStudiosFromChildren => true; /// <inheritdoc /> - protected override IList<BaseItem> GetChildrenForMetadataUpdates(MusicAlbum item) + protected override IReadOnlyList<BaseItem> GetChildrenForMetadataUpdates(MusicAlbum item) => item.GetRecursiveChildren(i => i is Audio); /// <inheritdoc /> - protected override ItemUpdateType UpdateMetadataFromChildren(MusicAlbum item, IList<BaseItem> children, bool isFullRefresh, ItemUpdateType currentUpdateType) + protected override ItemUpdateType UpdateMetadataFromChildren(MusicAlbum item, IReadOnlyList<BaseItem> children, bool isFullRefresh, ItemUpdateType currentUpdateType) { var updateType = base.UpdateMetadataFromChildren(item, children, isFullRefresh, currentUpdateType); diff --git a/MediaBrowser.Providers/Music/ArtistMetadataService.cs b/MediaBrowser.Providers/Music/ArtistMetadataService.cs index 1f342c0db..c47f9a500 100644 --- a/MediaBrowser.Providers/Music/ArtistMetadataService.cs +++ b/MediaBrowser.Providers/Music/ArtistMetadataService.cs @@ -1,6 +1,7 @@ #pragma warning disable CS1591 using System.Collections.Generic; +using System.Collections.Immutable; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities; using MediaBrowser.Controller.Entities.Audio; @@ -28,7 +29,7 @@ namespace MediaBrowser.Providers.Music protected override bool EnableUpdatingGenresFromChildren => true; /// <inheritdoc /> - protected override IList<BaseItem> GetChildrenForMetadataUpdates(MusicArtist item) + protected override IReadOnlyList<BaseItem> GetChildrenForMetadataUpdates(MusicArtist item) { return item.IsAccessedByName ? item.GetTaggedItems(new InternalItemsQuery diff --git a/MediaBrowser.Providers/Playlists/PlaylistMetadataService.cs b/MediaBrowser.Providers/Playlists/PlaylistMetadataService.cs index 43889bfbf..7be54453f 100644 --- a/MediaBrowser.Providers/Playlists/PlaylistMetadataService.cs +++ b/MediaBrowser.Providers/Playlists/PlaylistMetadataService.cs @@ -36,7 +36,7 @@ namespace MediaBrowser.Providers.Playlists protected override bool EnableUpdatingStudiosFromChildren => true; /// <inheritdoc /> - protected override IList<BaseItem> GetChildrenForMetadataUpdates(Playlist item) + protected override IReadOnlyList<BaseItem> GetChildrenForMetadataUpdates(Playlist item) => item.GetLinkedChildren(); /// <inheritdoc /> diff --git a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumProvider.cs b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumProvider.cs index daad9706c..ff30af879 100644 --- a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumProvider.cs +++ b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbAlbumProvider.cs @@ -148,21 +148,19 @@ namespace MediaBrowser.Providers.Plugins.AudioDb item.Overview = (overview ?? string.Empty).StripHtml(); } - internal Task EnsureInfo(string musicBrainzReleaseGroupId, CancellationToken cancellationToken) + internal async Task EnsureInfo(string musicBrainzReleaseGroupId, CancellationToken cancellationToken) { var xmlPath = GetAlbumInfoPath(_config.ApplicationPaths, musicBrainzReleaseGroupId); var fileInfo = _fileSystem.GetFileSystemInfo(xmlPath); - if (fileInfo.Exists) + if (fileInfo.Exists + && (DateTime.UtcNow - _fileSystem.GetLastWriteTimeUtc(fileInfo)).TotalDays <= 2) { - if ((DateTime.UtcNow - _fileSystem.GetLastWriteTimeUtc(fileInfo)).TotalDays <= 2) - { - return Task.CompletedTask; - } + return; } - return DownloadInfo(musicBrainzReleaseGroupId, cancellationToken); + await DownloadInfo(musicBrainzReleaseGroupId, cancellationToken).ConfigureAwait(false); } internal async Task DownloadInfo(string musicBrainzReleaseGroupId, CancellationToken cancellationToken) diff --git a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistProvider.cs b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistProvider.cs index 92742b1aa..00bd96282 100644 --- a/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistProvider.cs +++ b/MediaBrowser.Providers/Plugins/AudioDb/AudioDbArtistProvider.cs @@ -131,7 +131,7 @@ namespace MediaBrowser.Providers.Plugins.AudioDb item.Overview = (overview ?? string.Empty).StripHtml(); } - internal Task EnsureArtistInfo(string musicBrainzId, CancellationToken cancellationToken) + internal async Task EnsureArtistInfo(string musicBrainzId, CancellationToken cancellationToken) { var xmlPath = GetArtistInfoPath(_config.ApplicationPaths, musicBrainzId); @@ -140,10 +140,10 @@ namespace MediaBrowser.Providers.Plugins.AudioDb if (fileInfo.Exists && (DateTime.UtcNow - _fileSystem.GetLastWriteTimeUtc(fileInfo)).TotalDays <= 2) { - return Task.CompletedTask; + return; } - return DownloadArtistInfo(musicBrainzId, cancellationToken); + await DownloadArtistInfo(musicBrainzId, cancellationToken).ConfigureAwait(false); } internal async Task DownloadArtistInfo(string musicBrainzId, CancellationToken cancellationToken) diff --git a/MediaBrowser.Providers/Plugins/StudioImages/StudiosImageProvider.cs b/MediaBrowser.Providers/Plugins/StudioImages/StudiosImageProvider.cs index 5ca9f6f9a..a50d69df5 100644 --- a/MediaBrowser.Providers/Plugins/StudioImages/StudiosImageProvider.cs +++ b/MediaBrowser.Providers/Plugins/StudioImages/StudiosImageProvider.cs @@ -101,11 +101,11 @@ namespace MediaBrowser.Providers.Plugins.StudioImages return string.Format(CultureInfo.InvariantCulture, "{0}/images/{1}/{2}.jpg", GetRepositoryUrl(), image, filename); } - private Task EnsureThumbsList(string file, CancellationToken cancellationToken) + private async Task EnsureThumbsList(string file, CancellationToken cancellationToken) { string url = string.Format(CultureInfo.InvariantCulture, "{0}/thumbs.txt", GetRepositoryUrl()); - return EnsureList(url, file, _fileSystem, cancellationToken); + await EnsureList(url, file, _fileSystem, cancellationToken).ConfigureAwait(false); } /// <inheritdoc /> diff --git a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs index 582e05b79..ce10d4a8a 100644 --- a/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs +++ b/MediaBrowser.Providers/Plugins/Tmdb/Movies/TmdbMovieProvider.cs @@ -199,7 +199,7 @@ namespace MediaBrowser.Providers.Plugins.Tmdb.Movies }; movie.SetProviderId(MetadataProvider.Tmdb, tmdbId); - movie.SetProviderId(MetadataProvider.Imdb, movieResult.ImdbId); + movie.TrySetProviderId(MetadataProvider.Imdb, movieResult.ImdbId); if (movieResult.BelongsToCollection is not null) { movie.SetProviderId(MetadataProvider.TmdbCollection, movieResult.BelongsToCollection.Id.ToString(CultureInfo.InvariantCulture)); diff --git a/MediaBrowser.Providers/TV/EpisodeMetadataService.cs b/MediaBrowser.Providers/TV/EpisodeMetadataService.cs index d8855ec93..9b4793ee6 100644 --- a/MediaBrowser.Providers/TV/EpisodeMetadataService.cs +++ b/MediaBrowser.Providers/TV/EpisodeMetadataService.cs @@ -1,5 +1,3 @@ -#pragma warning disable CS1591 - using System; using MediaBrowser.Controller.Configuration; using MediaBrowser.Controller.Entities.TV; @@ -12,8 +10,19 @@ using Microsoft.Extensions.Logging; namespace MediaBrowser.Providers.TV { + /// <summary> + /// Service to manage episode metadata. + /// </summary> public class EpisodeMetadataService : MetadataService<Episode, EpisodeInfo> { + /// <summary> + /// Initializes a new instance of the <see cref="EpisodeMetadataService"/> class. + /// </summary> + /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param> + /// <param name="logger">Instance of the <see cref="ILogger{SeasonMetadataService}"/> interface.</param> + /// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param> + /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param> + /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param> public EpisodeMetadataService( IServerConfigurationManager serverConfigurationManager, ILogger<EpisodeMetadataService> logger, @@ -94,6 +103,11 @@ namespace MediaBrowser.Providers.TV { targetItem.IndexNumberEnd = sourceItem.IndexNumberEnd; } + + if (replaceData || !targetItem.ParentIndexNumber.HasValue) + { + targetItem.ParentIndexNumber = sourceItem.ParentIndexNumber; + } } } } diff --git a/MediaBrowser.Providers/TV/SeasonMetadataService.cs b/MediaBrowser.Providers/TV/SeasonMetadataService.cs index 54dcee41e..b27ccaa6a 100644 --- a/MediaBrowser.Providers/TV/SeasonMetadataService.cs +++ b/MediaBrowser.Providers/TV/SeasonMetadataService.cs @@ -1,5 +1,3 @@ -#pragma warning disable CS1591 - using System; using System.Collections.Generic; using System.Linq; @@ -15,8 +13,19 @@ using Microsoft.Extensions.Logging; namespace MediaBrowser.Providers.TV { + /// <summary> + /// Service to manage season metadata. + /// </summary> public class SeasonMetadataService : MetadataService<Season, SeasonInfo> { + /// <summary> + /// Initializes a new instance of the <see cref="SeasonMetadataService"/> class. + /// </summary> + /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param> + /// <param name="logger">Instance of the <see cref="ILogger{SeasonMetadataService}"/> interface.</param> + /// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param> + /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param> + /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param> public SeasonMetadataService( IServerConfigurationManager serverConfigurationManager, ILogger<SeasonMetadataService> logger, @@ -71,11 +80,11 @@ namespace MediaBrowser.Providers.TV } /// <inheritdoc /> - protected override IList<BaseItem> GetChildrenForMetadataUpdates(Season item) + protected override IReadOnlyList<BaseItem> GetChildrenForMetadataUpdates(Season item) => item.GetEpisodes(); /// <inheritdoc /> - protected override ItemUpdateType UpdateMetadataFromChildren(Season item, IList<BaseItem> children, bool isFullRefresh, ItemUpdateType currentUpdateType) + protected override ItemUpdateType UpdateMetadataFromChildren(Season item, IReadOnlyList<BaseItem> children, bool isFullRefresh, ItemUpdateType currentUpdateType) { var updateType = base.UpdateMetadataFromChildren(item, children, isFullRefresh, currentUpdateType); @@ -87,7 +96,7 @@ namespace MediaBrowser.Providers.TV return updateType; } - private ItemUpdateType SaveIsVirtualItem(Season item, IList<BaseItem> episodes) + private ItemUpdateType SaveIsVirtualItem(Season item, IReadOnlyList<BaseItem> episodes) { var isVirtualItem = item.LocationType == LocationType.Virtual && (episodes.Count == 0 || episodes.All(i => i.LocationType == LocationType.Virtual)); diff --git a/MediaBrowser.Providers/TV/SeriesMetadataService.cs b/MediaBrowser.Providers/TV/SeriesMetadataService.cs index 80c56351c..284415dce 100644 --- a/MediaBrowser.Providers/TV/SeriesMetadataService.cs +++ b/MediaBrowser.Providers/TV/SeriesMetadataService.cs @@ -1,5 +1,3 @@ -#pragma warning disable CS1591 - using System; using System.Collections.Generic; using System.Globalization; @@ -20,10 +18,22 @@ using Microsoft.Extensions.Logging; namespace MediaBrowser.Providers.TV { + /// <summary> + /// Service to manage series metadata. + /// </summary> public class SeriesMetadataService : MetadataService<Series, SeriesInfo> { private readonly ILocalizationManager _localizationManager; + /// <summary> + /// Initializes a new instance of the <see cref="SeriesMetadataService"/> class. + /// </summary> + /// <param name="serverConfigurationManager">Instance of the <see cref="IServerConfigurationManager"/> interface.</param> + /// <param name="logger">Instance of the <see cref="ILogger{SeasonMetadataService}"/> interface.</param> + /// <param name="providerManager">Instance of the <see cref="IProviderManager"/> interface.</param> + /// <param name="fileSystem">Instance of the <see cref="IFileSystem"/> interface.</param> + /// <param name="libraryManager">Instance of the <see cref="ILibraryManager"/> interface.</param> + /// <param name="localizationManager">Instance of the <see cref="ILocalizationManager"/> interface.</param> public SeriesMetadataService( IServerConfigurationManager serverConfigurationManager, ILogger<SeriesMetadataService> logger, @@ -36,6 +46,7 @@ namespace MediaBrowser.Providers.TV _localizationManager = localizationManager; } + /// <inheritdoc /> public override async Task<ItemUpdateType> RefreshMetadata(BaseItem item, MetadataRefreshOptions refreshOptions, CancellationToken cancellationToken) { if (item is Series series) @@ -129,38 +140,39 @@ namespace MediaBrowser.Providers.TV private void RemoveObsoleteEpisodes(Series series) { - var episodes = series.GetEpisodes(null, new DtoOptions(), true).OfType<Episode>().ToList(); - var numberOfEpisodes = episodes.Count; - // TODO: O(n^2), but can it be done faster without overcomplicating it? - for (var i = 0; i < numberOfEpisodes; i++) + var episodesBySeason = series.GetEpisodes(null, new DtoOptions(), true) + .OfType<Episode>() + .GroupBy(e => e.ParentIndexNumber) + .ToList(); + + foreach (var seasonEpisodes in episodesBySeason) { - var currentEpisode = episodes[i]; - // The outer loop only examines virtual episodes - if (!currentEpisode.IsVirtualItem) + List<Episode> nonPhysicalEpisodes = []; + List<Episode> physicalEpisodes = []; + foreach (var episode in seasonEpisodes) { - continue; - } + if (episode.IsVirtualItem || episode.IsMissingEpisode) + { + nonPhysicalEpisodes.Add(episode); + continue; + } - // Virtual episodes without an episode number are practically orphaned and should be deleted - if (!currentEpisode.IndexNumber.HasValue) - { - DeleteEpisode(currentEpisode); - continue; + physicalEpisodes.Add(episode); } - for (var j = i + 1; j < numberOfEpisodes; j++) + // Only consider non-physical episodes + foreach (var episode in nonPhysicalEpisodes) { - var comparisonEpisode = episodes[j]; - // The inner loop is only for "physical" episodes - if (comparisonEpisode.IsVirtualItem - || currentEpisode.ParentIndexNumber != comparisonEpisode.ParentIndexNumber - || !comparisonEpisode.ContainsEpisodeNumber(currentEpisode.IndexNumber.Value)) + // Episodes without an episode number are practically orphaned and should be deleted + // Episodes with a physical equivalent should be deleted (they are no longer missing) + var shouldKeep = episode.IndexNumber.HasValue && !physicalEpisodes.Any(e => e.ContainsEpisodeNumber(episode.IndexNumber.Value)); + + if (shouldKeep) { continue; } - DeleteEpisode(currentEpisode); - break; + DeleteEpisode(episode); } } } diff --git a/MediaBrowser.Providers/Trickplay/TrickplayImagesTask.cs b/MediaBrowser.Providers/Trickplay/TrickplayImagesTask.cs index 31c0eeb31..4310f93d4 100644 --- a/MediaBrowser.Providers/Trickplay/TrickplayImagesTask.cs +++ b/MediaBrowser.Providers/Trickplay/TrickplayImagesTask.cs @@ -63,7 +63,7 @@ public class TrickplayImagesTask : IScheduledTask { new TaskTriggerInfo { - Type = TaskTriggerInfo.TriggerDaily, + Type = TaskTriggerInfoType.DailyTrigger, TimeOfDayTicks = TimeSpan.FromHours(3).Ticks } }; diff --git a/MediaBrowser.Providers/Trickplay/TrickplayMoveImagesTask.cs b/MediaBrowser.Providers/Trickplay/TrickplayMoveImagesTask.cs index c581fd26c..c0b8a8c75 100644 --- a/MediaBrowser.Providers/Trickplay/TrickplayMoveImagesTask.cs +++ b/MediaBrowser.Providers/Trickplay/TrickplayMoveImagesTask.cs @@ -18,8 +18,6 @@ namespace MediaBrowser.Providers.Trickplay; /// </summary> public class TrickplayMoveImagesTask : IScheduledTask { - private const int QueryPageLimit = 100; - private readonly ILogger<TrickplayMoveImagesTask> _logger; private readonly ILibraryManager _libraryManager; private readonly ILocalizationManager _localization; @@ -62,32 +60,46 @@ public class TrickplayMoveImagesTask : IScheduledTask /// <inheritdoc /> public async Task ExecuteAsync(IProgress<double> progress, CancellationToken cancellationToken) { - var trickplayItems = await _trickplayManager.GetTrickplayItemsAsync().ConfigureAwait(false); - var query = new InternalItemsQuery + const int Limit = 100; + int itemCount = 0, offset = 0, previousCount; + + // This count may not be accurate, but just get something to show progress on the dashboard. + var totalVideoCount = _libraryManager.GetCount(new InternalItemsQuery { MediaTypes = [MediaType.Video], SourceTypes = [SourceType.Library], IsVirtualItem = false, IsFolder = false, - Recursive = true, - Limit = QueryPageLimit - }; - - var numberOfVideos = _libraryManager.GetCount(query); + Recursive = true + }); - var startIndex = 0; - var numComplete = 0; + var trickplayQuery = new InternalItemsQuery + { + MediaTypes = [MediaType.Video], + SourceTypes = [SourceType.Library], + IsVirtualItem = false, + IsFolder = false + }; - while (startIndex < numberOfVideos) + do { - query.StartIndex = startIndex; - var videos = _libraryManager.GetItemList(query).OfType<Video>().ToList(); - videos.RemoveAll(i => !trickplayItems.Contains(i.Id)); + var trickplayInfos = await _trickplayManager.GetTrickplayItemsAsync(Limit, offset).ConfigureAwait(false); + previousCount = trickplayInfos.Count; + offset += Limit; - foreach (var video in videos) + trickplayQuery.ItemIds = trickplayInfos.Select(i => i.ItemId).Distinct().ToArray(); + var items = _libraryManager.GetItemList(trickplayQuery); + foreach (var trickplayInfo in trickplayInfos) { cancellationToken.ThrowIfCancellationRequested(); + var video = items.OfType<Video>().FirstOrDefault(i => i.Id.Equals(trickplayInfo.ItemId)); + if (video is null) + { + continue; + } + + itemCount++; try { var libraryOptions = _libraryManager.GetLibraryOptions(video); @@ -97,13 +109,10 @@ public class TrickplayMoveImagesTask : IScheduledTask { _logger.LogError(ex, "Error moving trickplay files for {ItemName}", video.Name); } - - numComplete++; - progress.Report(100d * numComplete / numberOfVideos); } - startIndex += QueryPageLimit; - } + progress.Report(100d * itemCount / totalVideoCount); + } while (previousCount == Limit); progress.Report(100); } |
